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.
- package/README.md +1 -0
- package/assets/content-templates/forms/framework.eta +90 -0
- package/assets/content-templates/forms/react.eta +42 -21
- package/assets/skills/fimo-cli/references/forms.md +2 -2
- package/assets/skills/fimo-cli/references/init.md +10 -8
- package/assets/skills/fimo-cli/references/projects.md +1 -1
- package/assets/skills/fimo-code/references/forms.md +24 -10
- package/assets/skills/fimo-code/references/frameworks/astro.md +9 -0
- package/assets/skills/fimo-code/references/frameworks/next.md +25 -0
- package/assets/skills/fimo-code/references/frameworks/sveltekit.md +33 -5
- package/assets/skills/fimo-code/references/labels.md +1 -1
- package/dist/build/sveltekit/config.d.ts +2 -1
- package/dist/build/sveltekit/config.js +15 -5
- package/dist/build/sveltekit/files/edge.js +68 -0
- package/dist/build/sveltekit/files/serverless.js +42 -0
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +12 -12
- package/dist/runtime/astro/server.d.ts +2 -0
- package/dist/runtime/astro/server.js +2 -0
- package/dist/runtime/forms/client.d.ts +13 -0
- package/dist/runtime/forms/client.js +83 -0
- package/dist/runtime/forms/index.d.ts +2 -0
- package/dist/runtime/forms/index.js +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/next/client.d.ts +2 -0
- package/dist/runtime/next/client.js +2 -0
- package/dist/runtime/next/server.d.ts +2 -0
- package/dist/runtime/next/server.js +2 -0
- package/dist/runtime/react-router/index.d.ts +1 -0
- package/dist/runtime/react-router/index.js +1 -0
- package/dist/runtime/sveltekit/forms.d.ts +15 -0
- package/dist/runtime/sveltekit/forms.js +15 -0
- package/dist/runtime/sveltekit/index.d.ts +2 -0
- package/dist/runtime/sveltekit/index.js +1 -0
- package/dist/runtime/sveltekit/labels.d.ts +7 -3
- package/dist/runtime/sveltekit/labels.js +1 -1
- package/dist/runtime/sveltekit/runtime.d.ts +2 -1
- package/dist/runtime/sveltekit/runtime.js +8 -2
- package/dist/runtime/templates.d.ts +1 -1
- package/dist/runtime/templates.js +1 -1
- package/dist/scripts/sync-forms.d.ts +1 -0
- package/dist/scripts/sync-forms.js +78 -26
- package/package.json +9 -4
- package/templates/astro/.fimo/config.json +21 -0
- package/templates/astro/AGENTS.md +37 -0
- package/templates/astro/CLAUDE.md +5 -0
- package/templates/astro/_gitignore +15 -0
- package/templates/astro/astro.config.mjs +7 -0
- package/templates/astro/package.json +22 -0
- package/templates/astro/pnpm-workspace.yaml +7 -0
- package/templates/astro/src/components/ProjectOverview.astro +42 -0
- package/templates/astro/src/env.d.ts +1 -0
- package/templates/astro/src/layouts/Base.astro +81 -0
- package/templates/astro/src/pages/index.astro +10 -0
- package/templates/astro/src/pages/projects/[slug].astro +26 -0
- package/templates/astro/src/pages/projects/index.astro +10 -0
- package/templates/astro/src/schemas/Project.json +9 -0
- package/templates/astro/src/schemas/Project.ts +29 -0
- package/templates/astro/src/schemas/Project.types.ts +70 -0
- package/templates/astro/src/schemas/SiteSettings.json +10 -0
- package/templates/astro/src/schemas/SiteSettings.ts +31 -0
- package/templates/astro/src/schemas/SiteSettings.types.ts +69 -0
- package/templates/astro/tsconfig.json +5 -0
- package/templates/react-router/package.json +1 -1
- 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(
|
|
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 =
|
|
23
|
-
if (!field.required) s
|
|
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 =
|
|
28
|
+
let s = `z.boolean({ message: ${quote('Must be true or false')} })`;
|
|
29
29
|
if (field.required) {
|
|
30
|
-
s += `.refine(v => v,
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
148
|
-
if (file instanceof File) {
|
|
149
|
-
payload
|
|
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.
|
|
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
|
|
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
|
-
|
|
73
|
-
`.fimo/config.json#runtime.framework`.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
65
|
+
The schema, parser, submitter, and form id are re-exported by `src/forms/index.ts`.
|
|
58
66
|
|
|
59
|
-
The `submitX` helper
|
|
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
|
-
##
|
|
69
|
+
## Common code pattern
|
|
62
70
|
|
|
63
|
-
Always import
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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:
|
|
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():
|
|
6
|
+
export declare function fimo(): Adapter;
|
|
6
7
|
//# sourceMappingURL=config.d.ts.map
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import adapter from
|
|
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
|
-
*
|
|
4
|
-
*
|
|
11
|
+
* @param {import('http').IncomingMessage} req
|
|
12
|
+
* @param {import('http').ServerResponse} res
|
|
5
13
|
*/
|
|
6
|
-
export
|
|
7
|
-
|
|
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};
|