1/**
2 * Config synthesis for driving SolidStart programmatically while keeping the
3 * project's own `vite.config.*` authoritative.
4 *
5 * SolidStart 2's `solidStart()` is a plain Vite plugin: it declares the
6 * `client` / `ssr` environments and a `builder.buildApp` that builds both.
7 * The server half — turning the SSR bundle into a deployable server — is
8 * `@solidjs/vite-plugin-nitro-2`, a second Vite plugin whose `buildApp`
9 * replaces SolidStart's and runs nitro over the emitted SSR bundle.
10 *
11 * That plugin takes its nitro configuration as a plain argument and always
12 * passes a `preset` down to nitro (its own default is `"node-server"`), so
13 * neither `NITRO_PRESET` nor a `nitro.config.ts` can override it. The only
14 * seam that gets the last word is the plugin instance itself — so this
15 * integration constructs it:
16 *
17 * 1. The project's `vite.config.*` loads natively (its `solidStart()` and any
18 * other plugins are untouched).
19 * 2. The integration APPENDS its own `nitroV2Plugin(...)` instance — loaded
20 * from the *project's* `node_modules` — through vite's inline config.
21 * Inline plugins are concatenated after the config file's, and vite merges
22 * plugin `config()` results in order, so the appended instance's
23 * `builder.buildApp` (and therefore its nitro config) wins.
24 * 3. {@link resolveNitroConfig} assembles that nitro config: the deploy
25 * target's preset is enforced last, after the caller's `nitro` overrides
26 * and the target's own {@link NitroConfigSlice} pass.
27 *
28 * Conflict policy: a project that registers `nitroV2Plugin` in its own
29 * `vite.config.*` would have its nitro options silently dropped (our
30 * appended instance replaces its `buildApp`), so the build fails with
31 * {@link pluginConflictMessage} instead. Nitro options belong on the
32 * integration (`nitro: { ... }`), which forwards them.
33 *
34 * The preset-name helpers are duplicated from the Nuxt integration rather
35 * than shared: they are pure string functions, and a shared module would
36 * couple two independent framework packages through framework-core's public
37 * surface for ~20 lines.
38 */
39
40/** A minimal structural slice of a nitro config (`NitroConfig`). */
41export interface NitroConfigSlice extends Record<string, unknown> {
42 preset?: string | undefined;
43 rootDir?: string | undefined;
44 /** Nitro's work directory. Forced under `rootDir` so fixture clones don't share a parent-workspace cache. */
45 buildDir?: string | undefined;
46 awsLambda?: Record<string, unknown> | undefined;
47 output?: NitroOutputConfigSlice | undefined;
48}
49
50/** The `output` slice of a nitro config this integration reads. */
51export interface NitroOutputConfigSlice {
52 dir?: string | undefined;
53 serverDir?: string | undefined;
54 publicDir?: string | undefined;
55}
56
57/** The plugin name `@solidjs/vite-plugin-nitro-2` registers. */
58export const NITRO_PLUGIN_NAME = "solid-start-vite-plugin-nitro";
59
60/**
61 * Normalize a nitro preset name for comparison: nitro treats `aws_lambda`,
62 * `aws-lambda`, and `awsLambda` as the same preset (kebab/snake/camel
63 * insensitive).
64 */
65export const normalizePresetName = (name: string): string =>
66 name
67 .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
68 .replace(/[_-]+/g, "-")
69 .toLowerCase();
70
71/** Whether two preset names identify the same nitro preset. */
72export const isSamePreset = (a: string, b: string): boolean =>
73 normalizePresetName(a) === normalizePresetName(b);
74
75/** The actionable error message for a caller-configured foreign preset. */
76export const presetConflictMessage = (
77 userPreset: string,
78 targetPreset: string,
79): string =>
80 `The integration was given \`nitro.preset: "${userPreset}"\`, but this deploy target ` +
81 `builds through the "${targetPreset}" preset — a "${userPreset}" build would never be ` +
82 "deployed. Remove `nitro.preset` and let the deploy target select it, or run the " +
83 `framework's own CLI for a "${userPreset}" build outside this integration.`;
84
85/** The actionable error message for a project-registered nitro plugin. */
86export const pluginConflictMessage = (): string =>
87 "The project's vite config registers `nitroV2Plugin()` from " +
88 "`@solidjs/vite-plugin-nitro-2`. This integration appends its own instance so the " +
89 "deploy target owns the nitro preset, which would silently discard the project's " +
90 "nitro options. Remove `nitroV2Plugin()` from the vite config and pass its options " +
91 "to the integration instead (`nitro: { ... }`, e.g. `nitro: { prerender: { crawlLinks: true } }`).";
92
93export interface ResolveNitroConfigInput {
94 /** The deploy target's nitro preset — always the last word. */
95 readonly preset: string;
96 /** Absolute project root, forwarded to nitro as `rootDir`. */
97 readonly rootDir: string;
98 /** Caller-supplied nitro overrides (the integration's `nitro` option). */
99 readonly nitro?: Record<string, unknown> | undefined;
100 /**
101 * Extra last-word mutation contributed by the deploy target (e.g. the AWS
102 * target's `awsLambda.streaming`). Runs after the preset is enforced.
103 */
104 readonly configure?: ((nitroConfig: NitroConfigSlice) => void) | undefined;
105}
106
107/**
108 * Assemble the nitro config handed to the appended `nitroV2Plugin(...)`
109 * instance: the caller's overrides first, then the owned keys (`preset`,
110 * `rootDir`) enforced over them, then the deploy target's pass.
111 */
112export const resolveNitroConfig = (
113 input: ResolveNitroConfigInput,
114): NitroConfigSlice => {
115 const config: NitroConfigSlice = { ...input.nitro };
116 config.preset = input.preset;
117 config.rootDir = input.rootDir;
118 // Keep nitro's `.nitro` cache inside this project. A fixture clone
119 // resolves nitro from the parent workspace; nitro's default cache then
120 // lands in that workspace and leaks other tests' traced modules.
121 config.buildDir = `${input.rootDir}/.nitro`;
122 input.configure?.(config);
123 return config;
124};
125
126/**
127 * The caller's foreign preset, or `undefined` when none was given or it is
128 * (an alias of) the target's own.
129 */
130export const findPresetConflict = (
131 nitro: Record<string, unknown> | undefined,
132 targetPreset: string,
133): string | undefined => {
134 const preset = nitro?.["preset"];
135 return typeof preset === "string" &&
136 preset.length > 0 &&
137 !isSamePreset(preset, targetPreset)
138 ? preset
139 : undefined;
140};
141
142/**
143 * Resolve the absolute nitro output directories for a build at `root`,
144 * honoring an `output` override on the nitro config. Mirrors nitro's own
145 * defaults (`.output`, `.output/server`, `.output/public`).
146 */
147export const resolveNitroOutputDirs = (
148 config: NitroConfigSlice,
149 root: string,
150 resolve: (...segments: Array<string>) => string,
151): { dir: string; serverDir: string; publicDir: string } => {
152 const dir = resolve(root, config.output?.dir ?? ".output");
153 return {
154 dir,
155 serverDir: resolve(dir, config.output?.serverDir ?? "server"),
156 publicDir: resolve(dir, config.output?.publicDir ?? "public"),
157 };
158};
159
160/** Marker set on the plugin instances this integration appends. */
161const INJECTED = Symbol.for(
162 "@alchemy.run/frontend-frameworks/solidstart/injected",
163);
164
165/** Flatten a vite `plugins` value (nested arrays, falsy entries) to objects. */
166const flattenPlugins = (
167 value: unknown,
168 out: Array<Record<string | symbol, unknown>>,
169): void => {
170 if (Array.isArray(value)) {
171 for (const entry of value) flattenPlugins(entry, out);
172 return;
173 }
174 if (value !== null && typeof value === "object") {
175 out.push(value as Record<string | symbol, unknown>);
176 }
177};
178
179/** Stamp every plugin object this integration created so it is recognizable. */
180export const markInjectedPlugins = <T>(plugins: T): T => {
181 const flat: Array<Record<string | symbol, unknown>> = [];
182 flattenPlugins(plugins, flat);
183 for (const plugin of flat) {
184 plugin[INJECTED] = true;
185 }
186 return plugins;
187};
188
189/**
190 * Whether a vite `plugins` value contains a nitro plugin instance this
191 * integration did NOT create (see {@link pluginConflictMessage}).
192 */
193export const hasForeignNitroPlugin = (plugins: unknown): boolean => {
194 const flat: Array<Record<string | symbol, unknown>> = [];
195 flattenPlugins(plugins, flat);
196 return flat.some(
197 (plugin) =>
198 plugin["name"] === NITRO_PLUGIN_NAME && plugin[INJECTED] !== true,
199 );
200};