fimo 0.15.0-staging.2 → 0.15.0-staging.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +1 -0
  2. package/assets/content-templates/forms/framework.eta +90 -0
  3. package/assets/content-templates/forms/react.eta +42 -21
  4. package/assets/skills/fimo-cli/references/forms.md +2 -2
  5. package/assets/skills/fimo-cli/references/init.md +10 -8
  6. package/assets/skills/fimo-cli/references/projects.md +1 -1
  7. package/assets/skills/fimo-code/references/forms.md +24 -10
  8. package/assets/skills/fimo-code/references/frameworks/astro.md +9 -0
  9. package/assets/skills/fimo-code/references/frameworks/next.md +25 -0
  10. package/assets/skills/fimo-code/references/frameworks/sveltekit.md +33 -5
  11. package/assets/skills/fimo-code/references/labels.md +1 -1
  12. package/dist/build/sveltekit/config.d.ts +2 -1
  13. package/dist/build/sveltekit/config.js +15 -5
  14. package/dist/build/sveltekit/files/edge.js +68 -0
  15. package/dist/build/sveltekit/files/serverless.js +42 -0
  16. package/dist/cli/bundle.json +1 -1
  17. package/dist/cli/index.js +12 -12
  18. package/dist/runtime/astro/server.d.ts +2 -0
  19. package/dist/runtime/astro/server.js +2 -0
  20. package/dist/runtime/forms/client.d.ts +13 -0
  21. package/dist/runtime/forms/client.js +83 -0
  22. package/dist/runtime/forms/index.d.ts +2 -0
  23. package/dist/runtime/forms/index.js +1 -0
  24. package/dist/runtime/index.d.ts +2 -0
  25. package/dist/runtime/index.js +4 -0
  26. package/dist/runtime/next/client.d.ts +2 -0
  27. package/dist/runtime/next/client.js +2 -0
  28. package/dist/runtime/next/server.d.ts +2 -0
  29. package/dist/runtime/next/server.js +2 -0
  30. package/dist/runtime/react-router/index.d.ts +1 -0
  31. package/dist/runtime/react-router/index.js +1 -0
  32. package/dist/runtime/sveltekit/forms.d.ts +15 -0
  33. package/dist/runtime/sveltekit/forms.js +15 -0
  34. package/dist/runtime/sveltekit/index.d.ts +2 -0
  35. package/dist/runtime/sveltekit/index.js +1 -0
  36. package/dist/runtime/sveltekit/labels.d.ts +7 -3
  37. package/dist/runtime/sveltekit/labels.js +1 -1
  38. package/dist/runtime/sveltekit/runtime.d.ts +2 -1
  39. package/dist/runtime/sveltekit/runtime.js +8 -2
  40. package/dist/runtime/templates.d.ts +1 -1
  41. package/dist/runtime/templates.js +1 -1
  42. package/dist/scripts/sync-forms.d.ts +1 -0
  43. package/dist/scripts/sync-forms.js +78 -26
  44. package/package.json +9 -4
  45. package/templates/astro/.fimo/config.json +21 -0
  46. package/templates/astro/AGENTS.md +37 -0
  47. package/templates/astro/CLAUDE.md +5 -0
  48. package/templates/astro/_gitignore +15 -0
  49. package/templates/astro/astro.config.mjs +7 -0
  50. package/templates/astro/package.json +22 -0
  51. package/templates/astro/pnpm-workspace.yaml +7 -0
  52. package/templates/astro/src/components/ProjectOverview.astro +42 -0
  53. package/templates/astro/src/env.d.ts +1 -0
  54. package/templates/astro/src/layouts/Base.astro +81 -0
  55. package/templates/astro/src/pages/index.astro +10 -0
  56. package/templates/astro/src/pages/projects/[slug].astro +26 -0
  57. package/templates/astro/src/pages/projects/index.astro +10 -0
  58. package/templates/astro/src/schemas/Project.json +9 -0
  59. package/templates/astro/src/schemas/Project.ts +29 -0
  60. package/templates/astro/src/schemas/Project.types.ts +70 -0
  61. package/templates/astro/src/schemas/SiteSettings.json +10 -0
  62. package/templates/astro/src/schemas/SiteSettings.ts +31 -0
  63. package/templates/astro/src/schemas/SiteSettings.types.ts +69 -0
  64. package/templates/astro/tsconfig.json +5 -0
  65. package/templates/react-router/package.json +1 -1
  66. package/templates/sveltekit/src/routes/+layout.server.ts +1 -2
package/README.md CHANGED
@@ -28,6 +28,7 @@ Run the CLI with `npx` — no global install, always the latest:
28
28
  ```bash
29
29
  npx fimo@latest login
30
30
  npx fimo@latest init my-site # default React Router scaffold
31
+ # or: npx fimo@latest init my-astro-site --framework astro
31
32
  # or: npx fimo@latest init my-next-site --framework nextjs
32
33
  # or: npx fimo@latest init my-svelte-site --framework sveltekit
33
34
  cd my-site
@@ -0,0 +1,90 @@
1
+ <%
2
+ const quote = (value) => JSON.stringify(value);
3
+ const localName = (key, index) => `field_${index}_${key.replace(/[^a-zA-Z0-9_$]/g, '_')}`;
4
+
5
+ function zodForField(field) {
6
+ if (field.type === 'hidden') {
7
+ return 'z.string().optional()';
8
+ }
9
+
10
+ if (field.type === 'file') {
11
+ let schema = `z.custom<File>((value) => typeof File !== "undefined" && value instanceof File, ${quote('Expected a file')})`;
12
+ if (!field.required) schema += '.optional()';
13
+ return schema;
14
+ }
15
+
16
+ if (field.type === 'checkbox') {
17
+ let schema = `z.boolean({ message: ${quote('Must be true or false')} })`;
18
+ if (field.required) schema += `.refine((value) => value, ${quote(`${field.label} is required`)})`;
19
+ return schema;
20
+ }
21
+
22
+ if (field.type === 'number') {
23
+ let schema = 'z.coerce.number()';
24
+ if (field.validation?.min !== undefined) schema += `.min(${field.validation.min})`;
25
+ if (field.validation?.max !== undefined) schema += `.max(${field.validation.max})`;
26
+ if (!field.required) schema += '.optional()';
27
+ return schema;
28
+ }
29
+
30
+ if (field.type === 'select' && field.options?.length) {
31
+ let schema = `z.enum(${JSON.stringify(field.options)} as [string, ...string[]])`;
32
+ if (!field.required) schema += '.optional()';
33
+ return schema;
34
+ }
35
+
36
+ let schema = 'z.string()';
37
+ if (field.required) schema += `.min(1, ${quote(`${field.label} is required`)})`;
38
+ if (field.type === 'email') schema += `.email(${quote('Invalid email address')})`;
39
+ if (field.type === 'url') schema += `.url(${quote('Invalid URL')})`;
40
+ if (field.validation?.minLength !== undefined) schema += `.min(${field.validation.minLength})`;
41
+ if (field.validation?.maxLength !== undefined) schema += `.max(${field.validation.maxLength})`;
42
+ if (field.validation?.pattern) schema += `.regex(new RegExp(${quote(field.validation.pattern)}))`;
43
+ if (!field.required) schema += '.optional()';
44
+ return schema;
45
+ }
46
+ -%>
47
+ <% if (form.clientDirective) { %><%= form.clientDirective %>
48
+ <% } %>// AUTO-GENERATED by `fimo` from src/forms/<%= form.name %>.json - DO NOT EDIT.
49
+
50
+ import { z } from "zod";
51
+ import { formClient as defaultFormClient } from "<%= form.runtimeImport %>";
52
+ import type { FormClient } from "fimo/forms";
53
+
54
+ export const FORM_ID = <%= quote(form.id ?? 'undefined') %>;
55
+
56
+ export const <%= form.pascal %>Schema = z.object({
57
+ <% Object.entries(form.fields).forEach(([key, field]) => { -%>
58
+ <%= JSON.stringify(key) %>: <%= zodForField(field) %>,
59
+ <% }); -%>
60
+ });
61
+
62
+ export type <%= form.pascal %>Data = z.infer<typeof <%= form.pascal %>Schema>;
63
+
64
+ /** Convert a browser form submission into the typed shape expected by Fimo. */
65
+ export function parse<%= form.pascal %>FormData(formData: FormData): <%= form.pascal %>Data {
66
+ const data: Record<string, unknown> = {};
67
+ <% Object.entries(form.fields).forEach(([key, field], index) => { -%>
68
+ <% if (field.type === 'checkbox') { -%>
69
+ data[<%= JSON.stringify(key) %>] = ['on', 'true', '1'].includes(formData.get(<%= JSON.stringify(key) %>) as string);
70
+ <% } else if (field.type === 'number') { -%>
71
+ const <%= localName(key, index) %> = formData.get(<%= JSON.stringify(key) %>);
72
+ if (<%= localName(key, index) %> !== null && <%= localName(key, index) %> !== '') {
73
+ data[<%= JSON.stringify(key) %>] = Number(<%= localName(key, index) %>);
74
+ }
75
+ <% } else { -%>
76
+ const <%= localName(key, index) %> = formData.get(<%= JSON.stringify(key) %>);
77
+ if (<%= localName(key, index) %> !== null) {
78
+ data[<%= JSON.stringify(key) %>] = <%= localName(key, index) %>;
79
+ }
80
+ <% } -%>
81
+ <% }); -%>
82
+ return <%= form.pascal %>Schema.parse(data);
83
+ }
84
+
85
+ export function submit<%= form.pascal %>(
86
+ data: <%= form.pascal %>Data,
87
+ client: FormClient = defaultFormClient
88
+ ) {
89
+ return client.submit(FORM_ID, data);
90
+ }
@@ -10,24 +10,24 @@
10
10
  */
11
11
 
12
12
  const hasFileFields = Object.values(form.fields).some(f => f.type === 'file');
13
+ const quote = (value) => JSON.stringify(value);
14
+ const localName = (key, index) => `field_${index}_${key.replace(/[^a-zA-Z0-9_$]/g, '_')}`;
13
15
 
14
- function zodForField(key, field) {
15
- const lines = [];
16
-
16
+ function zodForField(field) {
17
17
  if (field.type === 'hidden') {
18
18
  return 'z.string().optional()';
19
19
  }
20
20
 
21
21
  if (field.type === 'file') {
22
- let s = 'z.instanceof(File)';
23
- if (!field.required) s = 'z.instanceof(File).optional()';
22
+ let s = `z.custom<File>((value) => typeof File !== "undefined" && value instanceof File, ${quote('Expected a file')})`;
23
+ if (!field.required) s += '.optional()';
24
24
  return s;
25
25
  }
26
26
 
27
27
  if (field.type === 'checkbox') {
28
- let s = 'z.boolean()';
28
+ let s = `z.boolean({ message: ${quote('Must be true or false')} })`;
29
29
  if (field.required) {
30
- s += `.refine(v => v, "${field.label} is required")`;
30
+ s += `.refine(v => v, ${quote(`${field.label} is required`)})`;
31
31
  }
32
32
  return s;
33
33
  }
@@ -41,8 +41,7 @@ function zodForField(key, field) {
41
41
  }
42
42
 
43
43
  if (field.type === 'select' && field.options?.length) {
44
- const opts = field.options.map(o => `"${o}"`).join(', ');
45
- let s = `z.enum([${opts}])`;
44
+ let s = `z.enum(${JSON.stringify(field.options)} as [string, ...string[]])`;
46
45
  if (!field.required) s += '.optional()';
47
46
  return s;
48
47
  }
@@ -51,7 +50,7 @@ function zodForField(key, field) {
51
50
  let s = 'z.string()';
52
51
 
53
52
  if (field.required) {
54
- s += `.min(1, "${field.label} is required")`;
53
+ s += `.min(1, ${quote(`${field.label} is required`)})`;
55
54
  }
56
55
 
57
56
  if (field.type === 'email') {
@@ -62,7 +61,7 @@ function zodForField(key, field) {
62
61
 
63
62
  if (field.validation?.minLength) s += `.min(${field.validation.minLength})`;
64
63
  if (field.validation?.maxLength) s += `.max(${field.validation.maxLength})`;
65
- if (field.validation?.pattern) s += `.regex(new RegExp("${field.validation.pattern}"))`;
64
+ if (field.validation?.pattern) s += `.regex(new RegExp(${quote(field.validation.pattern)}))`;
66
65
 
67
66
  if (!field.required) s += '.optional()';
68
67
 
@@ -79,16 +78,33 @@ const fileFieldKeys = Object.entries(form.fields)
79
78
 
80
79
  import { z } from "zod";
81
80
 
82
- export const FORM_ID = "<%= form.id %>";
81
+ export const FORM_ID = <%= quote(form.id ?? 'undefined') %>;
83
82
 
84
83
  export const <%= form.pascal %>Schema = z.object({
85
84
  <% Object.entries(form.fields).forEach(([key, field]) => { -%>
86
- <%= key %>: <%= zodForField(key, field) %>,
85
+ <%= JSON.stringify(key) %>: <%= zodForField(field) %>,
87
86
  <% }); -%>
88
87
  });
89
88
 
90
89
  export type <%= form.pascal %>Data = z.infer<typeof <%= form.pascal %>Schema>;
91
90
 
91
+ /** Convert a browser form submission into the typed shape expected by Fimo. */
92
+ export function parse<%= form.pascal %>FormData(formData: FormData): <%= form.pascal %>Data {
93
+ const data: Record<string, unknown> = {};
94
+ <% Object.entries(form.fields).forEach(([key, field], index) => { -%>
95
+ <% if (field.type === 'checkbox') { -%>
96
+ data[<%= JSON.stringify(key) %>] = ['on', 'true', '1'].includes(formData.get(<%= JSON.stringify(key) %>) as string);
97
+ <% } else if (field.type === 'number') { -%>
98
+ const <%= localName(key, index) %> = formData.get(<%= JSON.stringify(key) %>);
99
+ if (<%= localName(key, index) %> !== null && <%= localName(key, index) %> !== '') data[<%= JSON.stringify(key) %>] = Number(<%= localName(key, index) %>);
100
+ <% } else { -%>
101
+ const <%= localName(key, index) %> = formData.get(<%= JSON.stringify(key) %>);
102
+ if (<%= localName(key, index) %> !== null) data[<%= JSON.stringify(key) %>] = <%= localName(key, index) %>;
103
+ <% } -%>
104
+ <% }); -%>
105
+ return <%= form.pascal %>Schema.parse(data);
106
+ }
107
+
92
108
  // @ts-ignore - meta.env is a vite feature
93
109
  const base = import.meta.env.VITE_API_URL;
94
110
  <% if (hasFileFields) { %>
@@ -135,8 +151,13 @@ async function uploadFile(file: File): Promise<string> {
135
151
  <% } %>
136
152
  export async function submit<%= form.pascal %>(data: <%= form.pascal %>Data): Promise<{
137
153
  success: boolean;
154
+ status: number;
138
155
  errors?: Record<string, string>;
139
156
  }> {
157
+ if (!FORM_ID || FORM_ID === "undefined") {
158
+ throw new Error(`Form "${FORM_ID || "unknown"}" has not been pushed. Run fimo forms push first.`);
159
+ }
160
+
140
161
  <% if (hasFileFields) { -%>
141
162
  const payload: Record<string, unknown> = { ...data };
142
163
 
@@ -144,9 +165,9 @@ export async function submit<%= form.pascal %>(data: <%= form.pascal %>Data): Pr
144
165
  await Promise.all([
145
166
  <% fileFieldKeys.forEach((key) => { -%>
146
167
  (async () => {
147
- const file = data.<%= key %>;
148
- if (file instanceof File) {
149
- payload.<%= key %> = await uploadFile(file);
168
+ const file = data[<%= JSON.stringify(key) %>];
169
+ if (typeof File !== "undefined" && file instanceof File) {
170
+ payload[<%= JSON.stringify(key) %>] = await uploadFile(file);
150
171
  }
151
172
  })(),
152
173
  <% }); -%>
@@ -160,7 +181,7 @@ export async function submit<%= form.pascal %>(data: <%= form.pascal %>Data): Pr
160
181
  body: JSON.stringify(payload),
161
182
  });
162
183
  } catch {
163
- return { success: false };
184
+ return { success: false, status: 0 };
164
185
  }
165
186
  <% } else { -%>
166
187
  let res: Response;
@@ -171,18 +192,18 @@ export async function submit<%= form.pascal %>(data: <%= form.pascal %>Data): Pr
171
192
  body: JSON.stringify(data),
172
193
  });
173
194
  } catch {
174
- return { success: false };
195
+ return { success: false, status: 0 };
175
196
  }
176
197
  <% } -%>
177
198
 
178
199
  if (res.ok) {
179
- return { success: true };
200
+ return { success: true, status: res.status };
180
201
  }
181
202
 
182
203
  if (res.status === 422) {
183
204
  const json = await res.json();
184
- return { success: false, errors: json.errors };
205
+ return { success: false, status: res.status, errors: json.errors };
185
206
  }
186
207
 
187
- return { success: false };
208
+ return { success: false, status: res.status };
188
209
  }
@@ -8,7 +8,7 @@ This file covers the **CLI workflow** for forms. Use it when the user wants to a
8
8
 
9
9
  1. Write `src/forms/{name}.json` — kebab-case filename (e.g. `contact-us.json`). For the JSON spec and field types, run `fimo skills get fimo-code --ref forms`.
10
10
  2. Register with `fimo forms push {name}` — creates the form, obtains its `id`, patches it back into the JSON file.
11
- 3. Pre-build generates `src/forms/{name}.ts` (Zod schema + `submitX` helper). Use the same `fimo-code` reference for the generated API.
11
+ 3. Validation/build generates `src/forms/{name}.ts` (Zod schema, `parseXFormData`, and `submitX` helper). Next.js also gets `{name}.client.ts`; other adapters keep one module. Use the same `fimo-code` reference for the generated API.
12
12
  4. Write the application form using the generated `@/forms/{name}` module and the existing framework conventions.
13
13
 
14
14
  Steps 1, 3, and 4 reference the `fimo-code` skill (`fimo skills get fimo-code`). Step 2 is the CLI command.
@@ -54,7 +54,7 @@ Use this when the user asks "who filled out the contact form?", "how many signup
54
54
 
55
55
  ## When to run sync manually
56
56
 
57
- The `fimo/vite` plugin regenerates `src/forms/*.ts` after form JSON changes during development. Before a production build, regenerate explicitly with:
57
+ The `fimo/vite` plugin regenerates legacy Vite `src/forms/*.ts` after form JSON changes during development. Framework-aware generation runs during validation. Before a production build, regenerate explicitly with:
58
58
 
59
59
  ```bash
60
60
  fimo validate # runs all phases: codegen, lint, route validation, label checks
@@ -11,7 +11,7 @@ fimo init [dir] [options]
11
11
  `fimo init` runs the full onboarding in one shot:
12
12
 
13
13
  1. **Signs in automatically** — if there's no session it opens the browser loopback login. In `--json` / non-TTY with no session it fails with `NOT_AUTHENTICATED` (run `fimo login` first).
14
- 2. **Resolves the project** — with `--project-id <id>` it sets up the named Fimo project: a project with no code yet is scaffolded as a fresh site, while an existing repository is cloned. Without it, `init` creates a NEW project. An interactive TTY shows a "Create new / Link existing" picker; non-interactive shells default to create-new. New projects use React Router by default; `--framework nextjs` and `--framework sveltekit` select minimal framework-native shells. An optional `--starter fimo-site@1` adds a content-led starter only to a newly created project without `--project-id`.
14
+ 2. **Resolves the project** — with `--project-id <id>` it sets up the named Fimo project: a project with no code yet is scaffolded as a fresh site, while an existing repository is cloned. Without it, `init` creates a NEW project. An interactive TTY shows a "Create new / Link existing" picker; non-interactive shells default to create-new. New projects use React Router by default; `--framework astro`, `--framework nextjs`, and `--framework sveltekit` select minimal framework-native shells. An optional `--starter fimo-site@1` adds a content-led starter only to a newly created project without `--project-id`.
15
15
  3. **Installs dependencies** — prompts for the package manager, or honor `--package-manager <pm>` / `--no-install`.
16
16
  4. **Optionally sets up Fimo skills** — confirm prompt, or `--skills` / `--no-skills`.
17
17
 
@@ -21,7 +21,7 @@ fimo init [dir] [options]
21
21
  | `--project-id <id>` | no | Set up the named Fimo project: scaffold and push a fresh site when it has no code yet, otherwise clone it. |
22
22
  | `--org <id>` | no | Target organization **id** when creating (use the `ID` column from `fimo orgs list`, NOT the slug). Picker shown interactively if omitted and TTY. |
23
23
  | `--env <name>` | no | Env to check out when linking an existing project (defaults to `main`). |
24
- | `--framework <framework>` | no | New-project scaffold: `react-router` (default), `nextjs` (App Router), or `sveltekit`. Existing repositories are preserved. |
24
+ | `--framework <framework>` | no | New-project scaffold: `react-router` (default), `astro`, `nextjs` (App Router), or `sveltekit`. Existing repositories are preserved. |
25
25
  | `--starter <id>` | no | Opt-in project starter for create-new (currently `fimo-site@1`). It seeds CMS content, labels, a contact form, and manual-only agents. Rejects `--project-id`. |
26
26
  | `--package-manager <pm>` | no | `npm` \| `pnpm` \| `yarn` \| `bun`. |
27
27
  | `--install` / `--no-install` | yes in non-TTY | Install deps after checkout (or skip it). |
@@ -65,15 +65,17 @@ Omit `--framework` for the existing React Router scaffold, or select a
65
65
  minimal framework-native shell explicitly:
66
66
 
67
67
  ```bash
68
+ fimo init my-astro-site --framework astro --org <organizationId>
68
69
  fimo init my-next-site --framework nextjs --org <organizationId>
69
70
  fimo init my-svelte-site --framework sveltekit --org <organizationId>
70
71
  ```
71
72
 
72
- Both shells include their framework adapter, hosted-preview connector, and
73
- `.fimo/config.json#runtime.framework`. They intentionally contain no schema,
74
- labels, or designed website. After init, load the project rules and `fimo-code`
75
- to build the requested site. The current `fimo-site@1` content starter supports
76
- React Router only.
73
+ Each shell includes its framework adapter, hosted-preview connector, and
74
+ `.fimo/config.json#runtime.framework`. The Astro shell also includes a minimal
75
+ content-backed example with static, on-demand, and dynamic routes plus an English default locale;
76
+ the other shells intentionally contain no schema, labels, or designed website.
77
+ After init, load the project rules and `fimo-code` to build the requested site.
78
+ The current `fimo-site@1` content starter supports React Router only.
77
79
 
78
80
  For `--agents`, pass the AI tool you're running in: `--agents=claude-code` for Claude Code, `--agents=cursor` for Cursor, `--agents=codex` for Codex, etc. When the flag is omitted, env-var detection of the invoking harness (e.g. `CLAUDECODE` / `CLAUDE_CODE_ENTRYPOINT` → claude-code) auto-targets that agent; other agents must be selected explicitly via `--agents`/`-a` or the interactive picker.
79
81
 
@@ -85,7 +87,7 @@ Pass `--project-id <id>` to set up a known Fimo project. If the project has no c
85
87
 
86
88
  `fimo init` (create-new path) scaffolds:
87
89
 
88
- - The selected project shell: React Router by default, Next.js App Router with `--framework nextjs`, or SvelteKit with `--framework sveltekit`
90
+ - The selected project shell: React Router by default, Astro with `--framework astro`, Next.js App Router with `--framework nextjs`, or SvelteKit with `--framework sveltekit`
89
91
  - `.fimo/project.json` — links this directory to the remote project (read-only after creation)
90
92
  - `.fimo/config.json` — project, route, locale, SEO, and agent policy
91
93
  - `.fimo/agents/` — versioned agent bundles
@@ -6,7 +6,7 @@ Commands for listing projects across an org, cloning a remote project locally, a
6
6
 
7
7
  ```bash
8
8
  fimo projects list [--org <id>] # list projects in an org (defaults to active org)
9
- fimo projects create [dir] [--org <id>] [--framework react-router|nextjs] [--install|--no-install]
9
+ fimo projects create [dir] [--org <id>] [--framework react-router|astro|nextjs|sveltekit] [--install|--no-install]
10
10
  fimo projects clone [projectId] [dir] [--env <name>] # clone an existing project locally
11
11
  fimo projects delete [projectId] [-C <dir>] [-y] # delete (defaults to current project)
12
12
  fimo projects publish [projectId] [-C <dir>] # make the project live (publish from main)
@@ -2,7 +2,7 @@
2
2
 
3
3
  > For the CLI workflow (`fimo forms push`, "Add a contact form" recipe), see **`fimo-cli/references/forms.md`**.
4
4
 
5
- This file covers the **package surface**: the JSON spec you write, the generated Zod schema + `submitX` helper you import, the React patterns. Version-locked to the project's `fimo` package.
5
+ This file covers the **package surface**: the JSON spec, generated Zod schema, `parseXFormData`, and `submitX` helper. Version-locked to the project's `fimo` package. The application owns form markup, actions, routing, locale, and UX.
6
6
 
7
7
  ## Form JSON spec
8
8
 
@@ -43,7 +43,7 @@ This file covers the **package surface**: the JSON spec you write, the generated
43
43
 
44
44
  ## Generated TS client
45
45
 
46
- After pre-build (or `fimo deploy`), `src/forms/{name}.ts` exists with:
46
+ After `fimo forms push` and the next validation/build, `src/forms/{name}.ts` exists with:
47
47
 
48
48
  ```ts
49
49
  export const FORM_ID = '<uuid>';
@@ -51,20 +51,28 @@ export const ContactUsSchema = z.object({
51
51
  /* fields */
52
52
  });
53
53
  export type ContactUsData = z.infer<typeof ContactUsSchema>;
54
- export async function submitContactUs(data): Promise<{ success: boolean; errors?: Record<string, string> }>;
54
+ export function parseContactUsFormData(formData: FormData): ContactUsData;
55
+ export function submitContactUs(
56
+ data: ContactUsData,
57
+ client?
58
+ ): Promise<{
59
+ success: boolean;
60
+ status: number;
61
+ errors?: Record<string, string>;
62
+ }>;
55
63
  ```
56
64
 
57
- Re-exported by `src/forms/index.ts`.
65
+ The schema, parser, submitter, and form id are re-exported by `src/forms/index.ts`.
58
66
 
59
- The `submitX` helper handles fetch + file uploads (presigned URLs) automatically. Never hand-roll fetch or validation.
67
+ The `submitX` helper uses the framework adapter's shared Fimo transport and handles presigned file uploads automatically. Never hand-roll the Fimo API fetch or validation.
60
68
 
61
- ## React code pattern
69
+ ## Common code pattern
62
70
 
63
- Always import from `@/forms/{name}` — never hand-roll fetch, never hand-roll validation.
71
+ Always import the generated module. Keep the native form and pending/error state in the framework's normal pattern:
64
72
 
65
73
  ```tsx
66
74
  import { useState } from 'react';
67
- import { ContactUsSchema, submitContactUs } from '@/forms/contact-us';
75
+ import { ContactUsSchema, parseContactUsFormData, submitContactUs } from '@/forms/contact-us';
68
76
  import type { ContactUsData } from '@/forms/contact-us';
69
77
 
70
78
  export function ContactForm() {
@@ -76,8 +84,7 @@ export function ContactForm() {
76
84
  setErrors({});
77
85
  setLoading(true);
78
86
 
79
- const raw = Object.fromEntries(new FormData(e.currentTarget));
80
- const parsed = ContactUsSchema.safeParse(raw);
87
+ const parsed = ContactUsSchema.safeParse(parseContactUsFormData(new FormData(e.currentTarget)));
81
88
  if (!parsed.success) {
82
89
  const fieldErrors: Record<string, string> = {};
83
90
  for (const issue of parsed.error.issues) fieldErrors[issue.path[0] as string] = issue.message;
@@ -97,6 +104,13 @@ export function ContactForm() {
97
104
 
98
105
  **File fields**: pass the raw `File` from `<input type="file">` to `submitX` — it handles the presigned upload automatically. **Never base64-encode.**
99
106
 
107
+ ## Framework bindings
108
+
109
+ - **Vite/React:** `submitX(data)` keeps using the existing `fimo/ui` runtime and preserves the current generated API.
110
+ - **Next.js:** server modules import `@/forms/name`; client components import `@/forms/name.client`. Both expose the same `submitX(data)` shape.
111
+ - **Astro:** import the generated server module and call `submitX(data)` in an Astro action or endpoint. Astro keeps the HTML form and redirect.
112
+ - **SvelteKit:** use `createFimoFormClient({ event })` in a server action and pass it as the second argument: `submitX(data, client)`.
113
+
100
114
  ## Labels and copy
101
115
 
102
116
  Wrap every visible string in `t('key')` and create the DB value with `fimo labels set` — see `references/labels.md`. This includes form labels, placeholders, button text, success/error messages.
@@ -75,6 +75,15 @@ const [settings, stories, labels] = await Promise.all([
75
75
 
76
76
  The native surface is `Text`, `RichText`, `Image`, `StaticImage`, `Video`, `Date`, `Boolean`, and `Json` (component table: `references/ui.md`). Pass tracked values whole so Studio can resolve their source; use `String(value)` only for attributes and URLs.
77
77
 
78
+ ## Forms
79
+
80
+ Generated forms use the native Astro server adapter. Keep the form element,
81
+ action, redirect, and error UI in Astro; call `submitX(data)` from an Astro
82
+ action or endpoint. Use `parseXFormData(new FormData(request))` to normalize
83
+ browser values before submitting. Do not import the server form module into a
84
+ client island; add a project endpoint if the form is intentionally browser-
85
+ submitted.
86
+
78
87
  ## Static and on-demand rendering
79
88
 
80
89
  Keep the application's per-route rendering choice:
@@ -82,6 +82,31 @@ Never write the registry by hand and never create alternate `*.schema.json` file
82
82
 
83
83
  ## Server and client boundaries
84
84
 
85
+ Forms follow the same boundary. Server actions and route handlers import the
86
+ generated server form module; a Client Component imports its generated
87
+ `.client.ts` module. Both expose `submitX(data)` and use the shared Fimo form
88
+ transport; do not call the tenant API directly.
89
+
90
+ ```tsx
91
+ 'use client';
92
+
93
+ import { parseContactUsFormData, submitContactUs } from '@/forms/contact-us.client';
94
+
95
+ export function ContactForm() {
96
+ return (
97
+ <form
98
+ action={async (formData) => {
99
+ await submitContactUs(parseContactUsFormData(formData));
100
+ }}
101
+ >
102
+ <input name="email" type="email" required />
103
+ <textarea name="message" required />
104
+ <button type="submit">Send</button>
105
+ </form>
106
+ );
107
+ }
108
+ ```
109
+
85
110
  Server Components, route handlers, `getStaticProps`, and `getServerSideProps` import from `fimo/next` and generated server modules:
86
111
 
87
112
  ```ts
@@ -59,17 +59,28 @@ export async function load(event) {
59
59
  }
60
60
  ```
61
61
 
62
- Resolve the locale with the application's existing policy, then fetch labels in
63
- the root server layout and publish the snapshot to descendants:
62
+ Pass `createFimo` the locale the application resolved. Omit it and Fimo sends
63
+ `.fimo/config.json#i18n.defaultLocale`, or no locale at all when none is
64
+ configured, so the API answers with the project default. Never hardcode a locale
65
+ to satisfy the signature.
66
+
67
+ `getLabels` follows the same rule: pass the locale the application resolved, or
68
+ omit it and the API answers with the project default, whose locale the returned
69
+ snapshot reports. Fetch labels in the root server layout and publish the
70
+ snapshot to descendants. Fimo resolves its injected content API URL internally;
71
+ do not import a Fimo environment variable or hardcode a tenant URL:
64
72
 
65
73
  ```ts
66
74
  // src/routes/+layout.server.ts
67
- import { env } from '$env/dynamic/public';
68
75
  import { getLabels } from 'fimo/sveltekit';
69
76
 
70
77
  export async function load(event) {
71
- const locale = 'en'; // replace with the application's resolved locale
72
- return { labels: await getLabels({ apiUrl: env.PUBLIC_FIMO_API_URL, event, locale }) };
78
+ // If the app resolves a locale from its URL or session, pass it:
79
+ // const locale = resolveLocale(event.url); // your app's resolver
80
+ // return { labels: await getLabels({ event, locale }) };
81
+
82
+ // Otherwise, omit it and use the Fimo project default.
83
+ return { labels: await getLabels({ event }) };
73
84
  }
74
85
  ```
75
86
 
@@ -94,6 +105,23 @@ component that renders them. Use `Text`, `RichText`, `Image`, `Video`, `Date`,
94
105
  formats plain numeric values for display; numeric fields currently have no
95
106
  source metadata, so it is not a Studio-editable primitive.
96
107
 
108
+ ## Forms
109
+
110
+ Use a `+page.server` action so SvelteKit owns the request and redirect. Pass its
111
+ event to `createFimoFormClient` and use the generated parser/helper:
112
+
113
+ ```ts
114
+ import { createFimoFormClient } from 'fimo/sveltekit';
115
+ import { parseContactUsFormData, submitContactUs } from '$lib/forms/contact-us';
116
+
117
+ export const actions = {
118
+ default: async (event) => {
119
+ const data = parseContactUsFormData(await event.request.formData());
120
+ return submitContactUs(data, createFimoFormClient({ event }));
121
+ },
122
+ };
123
+ ```
124
+
97
125
  ## Live preview
98
126
 
99
127
  `FimoProvider` owns the Studio browser bridge. On mount it reads
@@ -41,7 +41,7 @@ The label contract is shared; acquisition differs by runtime:
41
41
  - Next server: `getLabels({ locale? })` from `fimo/next`.
42
42
  - Next client: `useLabels()` from `fimo/next/client`, under `FimoProvider`.
43
43
  - React Router/Vite client: `Label` and `useLabels()` from `fimo/ui`, under the existing framework integration.
44
- - SvelteKit: resolve the locale in the root server load and return `getLabels({ apiUrl, event, locale })`. Wrap the root layout with `FimoProvider` from `fimo/sveltekit/components`; the snapshot supplies its locale. Descendants use `Label` from that entrypoint or `useLabels().t()` from `fimo/sveltekit`. Pass the same resolved locale to `createFimo({ content, event, locale })` in content loads.
44
+ - SvelteKit: pass the application's resolved locale to `getLabels({ apiUrl, event, locale })`, or omit it to let the API choose the project default. Wrap the root layout with `FimoProvider` from `fimo/sveltekit/components`; the snapshot supplies its locale. Descendants use `Label` from that entrypoint or `useLabels().t()` from `fimo/sveltekit`. Apply the same explicit-or-omitted rule to `createFimo({ content, event, locale? })` in content loads.
45
45
  - Astro server page: `getLabels({ locale? })` from `fimo/astro`; no provider.
46
46
 
47
47
  Load the matching framework reference before choosing a provider or server reader.
@@ -1,6 +1,7 @@
1
+ import type { Adapter } from '@sveltejs/kit';
1
2
  /**
2
3
  * Configure a SvelteKit project for Fimo hosting without exposing the hosting
3
4
  * provider in the generated application.
4
5
  */
5
- export declare function fimo(): import("@sveltejs/kit").Adapter;
6
+ export declare function fimo(): Adapter;
6
7
  //# sourceMappingURL=config.d.ts.map
@@ -1,8 +1,18 @@
1
- import adapter from '@sveltejs/adapter-vercel';
1
+ var __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,"name",{value,configurable:!0});import fs from"node:fs";import path from"node:path";import process2 from"node:process";import{fileURLToPath}from"node:url";import{nodeFileTrace}from"@vercel/nft";import esbuild from"esbuild";import process from"node:process";function get_pathname(route){let i=1,pathname=route.segments.map(segment=>{if(!segment.dynamic)return"/"+segment.content;let parts=segment.content.split(/\[(.+?)\](?!\])/);return parts.length===3&&!parts[0]&&!parts[2]&&(parts[1].startsWith("...")||parts[1][0]==="[")?`$${i++}`:"/"+parts.map((content,j)=>j%2?`$${i++}`:content).join("")}).join("");return pathname[0]==="/"?pathname.slice(1):pathname}__name(get_pathname,"get_pathname");function pattern_to_src(pattern){let src=pattern.slice(1,-2).replace(/\\\//g,"/");return src==="^/"&&(src="^/?"),src=src.replace(/\(\?:\/\((.+?)\)\)/g,"(/$1)"),src}__name(pattern_to_src,"pattern_to_src");var integer=/^\d+$/;function parse_isr_expiration(value,route_id){if(value===!1||value==="false")return!1;let err=__name(desc=>{throw new Error(`Invalid isr.expiration value: ${JSON.stringify(value)} (${desc}, in ${route_id})`)},"err"),parsed;return typeof value=="string"?(integer.test(value)||err("value was a string but could not be parsed as an integer"),parsed=Number.parseInt(value,10)):(Number.isInteger(value)||err("should be an integer"),parsed=value),Number.isNaN(parsed)&&err("should be a number"),parsed<0&&err("should be non-negative"),parsed}__name(parse_isr_expiration,"parse_isr_expiration");function resolve_runtime(default_key,override_key){let key=(override_key??default_key??get_default_runtime()).replace("experimental_","");return assert_is_valid_runtime(key),key}__name(resolve_runtime,"resolve_runtime");var valid_node_versions=[20,22,24],formatter=new Intl.ListFormat("en",{type:"disjunction"});function get_default_runtime(){let major=Number(process.version.slice(1).split(".")[0]);if(!valid_node_versions.includes(major))throw new Error(`Unsupported Node.js version: ${process.version}. Please use Node ${formatter.format(valid_node_versions.map(v=>`${v}`))} to build your project, or explicitly specify a runtime in your adapter configuration.`);return`nodejs${major}.x`}__name(get_default_runtime,"get_default_runtime");var valid_runtimes=["nodejs20.x","nodejs22.x","nodejs24.x","bun1.x","edge"];function assert_is_valid_runtime(key){if(!valid_runtimes.includes(key))throw new Error(`Unsupported runtime: ${key}. Supported runtimes are: ${valid_runtimes.join(", ")}. See the Node.js Version section in your Vercel project settings for info on the currently supported versions.`)}__name(assert_is_valid_runtime,"assert_is_valid_runtime");import{VERSION}from"@sveltejs/kit";var name="@sveltejs/adapter-vercel",INTERNAL="![-]",[kit_major,kit_minor]=VERSION.split("."),compatible_node_modules=["async_hooks","events","buffer","assert","util"],plugin=__name(function(defaults={}){if("edge"in defaults)throw new Error("{ edge: true } has been removed in favour of { runtime: 'edge' }");return{name,async adapt(builder){if(!builder.routes)throw new Error("@sveltejs/adapter-vercel >=2.x (possibly installed through @sveltejs/adapter-auto) requires @sveltejs/kit version 1.5 or higher. Either downgrade the adapter or upgrade @sveltejs/kit");let dir=".vercel/output",tmp=builder.getBuildDirectory("vercel-tmp");if(builder.rimraf(dir),builder.rimraf(tmp),fs.existsSync("vercel.json")){let vercel_file=fs.readFileSync("vercel.json","utf-8"),vercel_config=JSON.parse(vercel_file);validate_vercel_json(builder,vercel_config)}let files=fileURLToPath(new URL("./files",import.meta.url).href),dirs={static:`${dir}/static${builder.config.kit.paths.base}`,functions:`${dir}/functions`};builder.log.minor("Copying assets..."),builder.writeClient(dirs.static),builder.writePrerendered(dirs.static);let static_config=static_vercel_config(builder,defaults,dirs.static);builder.log.minor("Generating serverless function...");async function generate_serverless_function(name2,config,routes){let dir2=`${dirs.functions}/${name2}.func`,relativePath=path.posix.relative(tmp,builder.getServerDirectory());builder.copy(`${files}/serverless.js`,`${tmp}/index.js`,{replace:{SERVER:`${relativePath}/index.js`,MANIFEST:"./manifest.js"}}),builder.hasServerInstrumentationFile?.()&&builder.instrument?.({entrypoint:`${tmp}/index.js`,instrumentation:`${builder.getServerDirectory()}/instrumentation.server.js`}),write(`${tmp}/manifest.js`,`export const manifest = ${builder.generateManifest({relativePath,routes})};
2
+ `),await create_function_bundle(builder,`${tmp}/index.js`,dir2,config);for(let asset of builder.findServerAssets(routes))builder.copy(`${builder.getServerDirectory()}/${asset}`,`${dir2}/${asset}`)}__name(generate_serverless_function,"generate_serverless_function");let warned=!1;async function generate_edge_function(name2,config,routes){warned||(warned=!0,builder.log.warn("The `runtime: 'edge'` option is deprecated, and will be removed in a future version of adapter-vercel"));let tmp2=builder.getBuildDirectory(`vercel-tmp/${name2}`),relativePath=path.posix.relative(tmp2,builder.getServerDirectory());builder.copy(`${files}/edge.js`,`${tmp2}/edge.js`,{replace:{SERVER:`${relativePath}/index.js`,MANIFEST:"./manifest.js"}}),write(`${tmp2}/manifest.js`,`export const manifest = ${builder.generateManifest({relativePath,routes})};
3
+ `);try{let outdir=`${dirs.functions}/${name2}.func`,esbuild_config={target:"es2020",bundle:!0,platform:"browser",conditions:["edge-light","module"],format:"esm",external:[...compatible_node_modules,...compatible_node_modules.map(id=>`node:${id}`),...config.external||[]],sourcemap:"linked",banner:{js:"globalThis.global = globalThis;"},loader:{".wasm":"copy",".woff":"copy",".woff2":"copy",".ttf":"copy",".eot":"copy",".otf":"copy"}},result=await esbuild.build({entryPoints:[`${tmp2}/edge.js`],outfile:`${outdir}/index.js`,...esbuild_config}),instrumentation_result;builder.hasServerInstrumentationFile?.()&&(instrumentation_result=await esbuild.build({entryPoints:[`${builder.getServerDirectory()}/instrumentation.server.js`],outfile:`${outdir}/instrumentation.server.js`,...esbuild_config}),builder.instrument?.({entrypoint:`${outdir}/index.js`,instrumentation:`${outdir}/instrumentation.server.js`,module:{generateText:generate_traced_edge_module}}));let warnings=instrumentation_result?[...result.warnings,...instrumentation_result.warnings]:result.warnings;if(warnings.length>0){let formatted=await esbuild.formatMessages(warnings,{kind:"warning",color:!0});console.error(formatted.join(`
4
+ `))}}catch(err){let error=err;for(let e of error.errors)for(let node of e.notes){let match=/The package "(.+)" wasn't found on the file system but is built into node/.exec(node.text);match&&(node.text=`Cannot use "${match[1]}" when deploying to Vercel Edge Functions.`)}let formatted=await esbuild.formatMessages(error.errors,{kind:"error",color:!0});throw console.error(formatted.join(`
5
+ `)),new Error(`Bundling with esbuild failed with ${error.errors.length} ${error.errors.length===1?"error":"errors"}`,{cause:err})}write(`${dirs.functions}/${name2}.func/.vc-config.json`,JSON.stringify({runtime:config.runtime,regions:config.regions,entrypoint:"index.js",framework:{slug:"sveltekit",version:VERSION}},null," "))}__name(generate_edge_function,"generate_edge_function");let groups=new Map,conflicts=new Map,functions=new Map,isr_config=new Map,ignored_isr=new Set;for(let route of builder.routes){let runtime=resolve_runtime(defaults.runtime,route.config.runtime),config={...defaults,...route.config,runtime};if(is_prerendered(route)){config.isr&&ignored_isr.add(route.id);continue}if(config.isr){let directory=path.relative(".",builder.config.kit.files.routes+route.id);if(runtime==="edge")throw new Error(`${directory}: Routes using \`isr\` must use a Node.js or Bun runtime (for example 'nodejs24.x' or 'experimental_bun1.x')`);if(config.isr.allowQuery?.includes("__pathname"))throw new Error(`${directory}: \`__pathname\` is a reserved query parameter for \`isr.allowQuery\``);isr_config.set(route,{expiration:config.isr.expiration,bypassToken:config.isr.bypassToken,allowQuery:["__pathname",...config.isr.allowQuery??[]],group:isr_config.size+1,passQuery:!0})}let hash=hash_config(config),pattern=route.pattern.toString(),existing=conflicts.get(pattern);if(existing){if(existing.hash!==hash)throw new Error(`The ${route.id} and ${existing.route_id} routes must be merged into a single function that matches the ${route.pattern} regex, but they have incompatible configs. You must either rename one of the routes, or make their configs match.`)}else conflicts.set(pattern,{hash,route_id:route.id});let id=config.split?`${hash}-${groups.size}`:hash,group=groups.get(id);group||(group={i:groups.size,config,routes:[]},groups.set(id,group)),group.routes.push(route)}if(ignored_isr.size){builder.log.warn(`
6
+ Warning: The following routes have an ISR config which is ignored because the route is prerendered:`);for(let ignored of ignored_isr)console.log(` - ${ignored}`);console.log(`Either remove the "prerender" option from these routes to use ISR, or remove the ISR config.
7
+ `)}let singular=groups.size===1;for(let group of groups.values()){let generate_function=group.config.runtime==="edge"?generate_edge_function:generate_serverless_function,name2=singular?`${INTERNAL}/catchall`:`${INTERNAL}/${group.i}`;await generate_function(name2,group.config,group.routes);for(let route of group.routes)functions.set(route.pattern.toString(),name2)}if(!singular){let runtime=resolve_runtime(defaults.runtime);await(runtime==="edge"?generate_edge_function:generate_serverless_function)(`${INTERNAL}/catchall`,{...defaults,runtime},[])}if(builder.config.kit.experimental.remoteFunctions){let app_path=builder.getAppPath(),remote_symlink_path=`${path.join(dirs.functions,app_path,"remote")}.func`,target=path.join(dirs.functions,INTERNAL,"catchall.func");builder.mkdirp(path.join(dirs.functions,app_path));let relative=path.relative(path.dirname(remote_symlink_path),target);fs.symlinkSync(relative,remote_symlink_path),static_config.routes.push({src:`/${app_path}/remote/.+`,dest:`/${app_path}/remote`})}for(let route of builder.routes){if(is_prerendered(route))continue;let pattern=route.pattern.toString(),src=pattern_to_src(pattern),name2=functions.get(pattern),isr=isr_config.get(route);if(isr){let isr_name=route.id.slice(1)||"__root__",base=`${dirs.functions}/${isr_name}`;builder.mkdirp(base);let target=`${dirs.functions}/${name2}.func`,relative=path.relative(path.dirname(base),target);fs.symlinkSync(relative,`${base}.func`),fs.symlinkSync(`../${relative}`,`${base}/__data.json.func`);let pathname=get_pathname(route),json=JSON.stringify({...isr,expiration:parse_isr_expiration(isr.expiration,route.id)},null," ");write(`${base}.prerender-config.json`,json),write(`${base}/__data.json.prerender-config.json`,json);let q=`?__pathname=/${pathname}`;static_config.routes.push({src:src+"$",dest:`/${isr_name}${q}`}),static_config.routes.push({src:src+"/__data.json$",dest:`/${isr_name}/__data.json${q}`})}else{let is_root=route.id==="/",route_fs_name=is_root?"index":route.id.slice(1),route_dest_name=is_root?"":route.id.slice(1),base_dir=path.join(dirs.functions,route_fs_name),main_symlink_path=`${base_dir}.func`,data_symlink_path=path.join(base_dir,"__data.json.func"),target=path.join(dirs.functions,`${name2}.func`);builder.mkdirp(base_dir);let relative_for_main=path.relative(path.dirname(main_symlink_path),target),relative_for_data=path.relative(path.dirname(data_symlink_path),target);fs.symlinkSync(relative_for_main,main_symlink_path),fs.symlinkSync(relative_for_data,data_symlink_path),static_config.routes.push({src:src+"(?:/__data.json)?$",dest:`/${route_dest_name}`})}}builder.config.kit.router?.resolution==="server"&&(await generate_edge_function(`${builder.config.kit.appDir}/route`,{external:"external"in defaults?defaults.external:void 0,runtime:"edge"},[]),static_config.routes.push({src:`${builder.config.kit.paths.base}/(|.+/)__route\\.js`,dest:`${builder.config.kit.paths.base}/${builder.config.kit.appDir}/route`})),static_config.routes.push({src:"/.*",dest:`/${INTERNAL}/catchall`}),builder.log.minor("Writing routes..."),write(`${dir}/config.json`,JSON.stringify(static_config,null," "))},supports:{read:__name(({config,route})=>{if((config.runtime??defaults.runtime)==="edge"&&kit_major==="2"&&kit_minor<"25")throw new Error(`${name}: Cannot use \`read\` from \`$app/server\` in route \`${route.id}\` configured with \`runtime: 'edge'\` and SvelteKit < 2.25.0`);return!0},"read"),instrumentation:__name(()=>!0,"instrumentation")}}},"plugin");function hash_config(config){return[config.runtime??"",config.external??"",config.regions??"",config.memory??"",config.maxDuration??"",!!config.isr].join("/")}__name(hash_config,"hash_config");function write(file,data){try{fs.mkdirSync(path.dirname(file),{recursive:!0})}catch{}fs.writeFileSync(file,data)}__name(write,"write");function static_vercel_config(builder,config,dir){let prerendered_redirects=[],overrides={},images=config.images;for(let[src,redirect]of builder.prerendered.redirects)src.replace(/\/$/,"")===redirect.location.replace(/\/$/,"")||(src=src.replace(/\/?$/,"/?")),prerendered_redirects.push({src,headers:{Location:redirect.location},status:redirect.status});for(let[path2,page]of builder.prerendered.pages){let overrides_path=path2.slice(1);if(path2!=="/"){let counterpart_route=path2+"/";path2.endsWith("/")&&(counterpart_route=path2.slice(0,-1),overrides_path=path2.slice(1,-1)),prerendered_redirects.push({src:path2,dest:counterpart_route},{src:counterpart_route,status:308,headers:{Location:path2}})}overrides[page.file]={path:overrides_path}}let routes=[{src:".*",continue:!0,transforms:[{type:"request.query",op:"delete",target:{key:"__pathname"}}]},...prerendered_redirects,{src:`/${builder.getAppPath()}/immutable/.+`,headers:{"cache-control":"public, immutable, max-age=31536000"}}];if(process2.env.VERCEL_SKEW_PROTECTION_ENABLED){routes.push({src:"/.*",has:[{type:"header",key:"Sec-Fetch-Dest",value:"document"}],headers:{"Set-Cookie":`__vdpl=${process2.env.VERCEL_DEPLOYMENT_ID}; Path=${builder.config.kit.paths.base}/; SameSite=Strict; Secure; HttpOnly`},continue:!0});let base=`${dir}/${builder.config.kit.appDir}/immutable/entry`,entry=fs.readdirSync(base).find(file=>file.startsWith("start."));if(!entry)throw new Error("Could not find entry point");routes.splice(-2,0,{src:`/${builder.getAppPath()}/immutable/entry/${entry}`,headers:{"Set-Cookie":`__vdpl=; Path=/${builder.getAppPath()}/version.json; SameSite=Strict; Secure; HttpOnly`},continue:!0})}return routes.push({handle:"filesystem"}),routes.push({src:`/${builder.getAppPath()}/immutable/.+`,status:404,headers:{"cache-control":"no-store"},continue:!1}),{version:3,routes,overrides,images}}__name(static_vercel_config,"static_vercel_config");async function create_function_bundle(builder,entry,dir,config){fs.rmSync(dir,{force:!0,recursive:!0});let base=entry;for(;base!==(base=path.dirname(base)););let traced=await nodeFileTrace([entry],{base}),resolution_failures=new Map;if(traced.warnings.forEach(error=>{if(!error.message.startsWith("Failed to resolve dependency node:")&&!error.message.startsWith("Failed to parse"))if(error.message.startsWith("Failed to resolve dependency")){let match=/Cannot find module '(.+?)' loaded from (.+)/,[,module,importer]=match.exec(error.message)??[,error.message,"(unknown)"];resolution_failures.has(importer)||resolution_failures.set(importer,[]),resolution_failures.get(importer).push(module)}else throw error}),resolution_failures.size>0){let cwd=process2.cwd();builder.log.warn("Warning: The following modules failed to locate dependencies that may (or may not) be required for your app to work:");for(let[importer,modules]of resolution_failures){console.error(` ${path.relative(cwd,importer)}`);for(let module of modules)console.error(` - \x1B[1m\x1B[36m${module}\x1B[39m\x1B[22m`)}}let files=Array.from(traced.fileList),common_parts=files[0]?.split(path.sep)??[];for(let i=1;i<files.length;i+=1){let parts=files[i].split(path.sep);for(let j=0;j<common_parts.length;j+=1)if(parts[j]!==common_parts[j]){common_parts=common_parts.slice(0,j);break}}let ancestor=base+common_parts.join(path.sep);for(let file of traced.fileList){let source=base+file,dest=path.join(dir,path.relative(ancestor,source)),is_dir=fs.statSync(source).isDirectory(),realpath=fs.realpathSync(source);try{fs.mkdirSync(path.dirname(dest),{recursive:!0})}catch{}if(source!==realpath){let realdest=path.join(dir,path.relative(ancestor,realpath));fs.symlinkSync(path.relative(path.dirname(dest),realdest),dest,is_dir?"dir":"file")}else is_dir||fs.copyFileSync(source,dest)}write(`${dir}/.vc-config.json`,JSON.stringify({runtime:config.runtime,regions:config.regions,memory:config.memory,maxDuration:config.maxDuration,handler:path.relative(base+ancestor,entry),launcherType:"Nodejs",experimentalResponseStreaming:!config.isr,framework:{slug:"sveltekit",version:VERSION}},null," ")),write(`${dir}/package.json`,JSON.stringify({type:"module"}))}__name(create_function_bundle,"create_function_bundle");function validate_vercel_json(builder,vercel_config){if(builder.routes.length>0&&!builder.routes[0].api)return;let crons=Array.isArray(vercel_config?.crons)?vercel_config.crons:[],valid_routes=builder.routes.filter(route=>route.api.methods.includes("GET")),unmatched_paths=[];for(let cron of crons){if(typeof cron!="object"||cron===null||!("path"in cron))continue;let{path:path2}=cron;typeof path2=="string"&&(valid_routes.some(route=>route.pattern.test(path2))||unmatched_paths.push(path2))}if(unmatched_paths.length){builder.log.warn("\nWarning: vercel.json defines cron tasks that use paths that do not correspond to an API route with a GET handler (ignore this if the request is handled in your `handle` hook):");for(let path2 of unmatched_paths)console.log(` - ${path2}`);console.log("")}}__name(validate_vercel_json,"validate_vercel_json");function is_prerendered(route){return route.prerender===!0||route.prerender==="auto"&&route.segments.every(segment=>!segment.dynamic)}__name(is_prerendered,"is_prerendered");function generate_traced_edge_module({instrumentation,start}){return`import './${instrumentation}';
8
+ const promise = import('./${start}');
9
+
2
10
  /**
3
- * Configure a SvelteKit project for Fimo hosting without exposing the hosting
4
- * provider in the generated application.
11
+ * @param {import('http').IncomingMessage} req
12
+ * @param {import('http').ServerResponse} res
5
13
  */
6
- export function fimo() {
7
- return adapter({ runtime: 'nodejs24.x' });
14
+ export default async (req, res) => {
15
+ const { default: handler } = await promise;
16
+ return handler(req, res);
8
17
  }
18
+ `}__name(generate_traced_edge_module,"generate_traced_edge_module");var adapter_vercel_default=plugin;function fimo(){return adapter_vercel_default({runtime:"nodejs24.x"})}__name(fimo,"fimo");export{fimo};