@menapo/forms 0.0.0-stage → 0.1.0

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/dist/vite.js ADDED
@@ -0,0 +1,1154 @@
1
+ import { builtinModules, createRequire } from "node:module";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parseSync, runnerImport } from "vite";
6
+ //#region ../glue/src/detect.ts
7
+ /**
8
+ * Which framework a module renders with (machine PLAN §5.5), the way vicss
9
+ * decides it: `.vue` and `.svelte` (with `.svelte.js`/`.svelte.ts` rune
10
+ * modules) by extension; JSX by its `@jsxImportSource` pragma, else the
11
+ * project's JSX framework ({@link jsxFrameworkOf}); plain scripts by the
12
+ * plugin's `framework` option, else the framework the root `package.json`
13
+ * depends on, else none (vanilla). Plain data in and out, so it is
14
+ * unit-tested without Vite.
15
+ *
16
+ * vi18n has a detection of its own (its `vite/glue/detect.ts`, by plugin
17
+ * names); it is published, so moving it onto this module waits for its
18
+ * next minor (machine PLAN §11).
19
+ */
20
+ /** Root dependencies that name a project's framework, the first found winning. */
21
+ const DEPENDENCIES = [
22
+ ["vue", "vue"],
23
+ ["svelte", "svelte"],
24
+ ["solid-js", "solid"],
25
+ ["preact", "preact"],
26
+ ["react", "react"]
27
+ ];
28
+ /** `preact`, `solid-js` and `vue` (and their subpaths) by name; any other JSX source takes React's. */
29
+ function jsxFramework(source) {
30
+ const name = source.split("/")[0];
31
+ if (name === "preact") return "preact";
32
+ if (name === "solid-js") return "solid";
33
+ if (name === "vue") return "vue";
34
+ return "react";
35
+ }
36
+ /** The framework of module `id` with source `code`. */
37
+ function frameworkOf(id, code, settings) {
38
+ const file = id.replace(/[?#].*$/, "").toLowerCase();
39
+ if (file.endsWith(".vue")) return "vue";
40
+ if (file.endsWith(".svelte") || /\.svelte\.[cm]?[jt]s$/.test(file)) return "svelte";
41
+ if (/\.[jt]sx$/.test(file)) {
42
+ const pragma = /@jsxImportSource\s+([^\s*]+)/.exec(code)?.[1];
43
+ return pragma ? jsxFramework(pragma) : settings.jsx;
44
+ }
45
+ return settings.scripts;
46
+ }
47
+ /** The names a `package.json` depends on, if it can be read. */
48
+ function dependencies(root) {
49
+ const file = join(root, "package.json");
50
+ if (!existsSync(file)) return void 0;
51
+ try {
52
+ const pkg = JSON.parse(readFileSync(file, "utf8"));
53
+ return /* @__PURE__ */ new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})]);
54
+ } catch {
55
+ return;
56
+ }
57
+ }
58
+ /** The framework the `package.json` in `root` depends on, if a known one. */
59
+ function rootFramework(root) {
60
+ const names = dependencies(root);
61
+ return names && DEPENDENCIES.find(([name]) => names.has(name))?.[1];
62
+ }
63
+ /**
64
+ * JSX frameworks by the names of the Vite plugins that compile them, as
65
+ * vi18n recognises them: a prefix up to a separator, or the exact name.
66
+ */
67
+ const JSX_PLUGINS = [
68
+ ["react", /^(?:vite:react|@vitejs\/plugin-react)(?:[:/-]|$)/],
69
+ ["preact", /^(?:preact:|vite:preact(?:[:/-]|$)|@preact\/|prefresh(?:[:/-]|$)|preact$)/],
70
+ ["solid", /^(?:solid:|vite-plugin-solid(?:[:/-]|$)|solid-start(?:[:/-]|$)|solid$)/],
71
+ ["vue", /^(?:vite:vue-jsx|@vitejs\/plugin-vue-jsx)(?:[:/-]|$)/]
72
+ ];
73
+ /** The one JSX framework whose Vite plugin is in `plugins`; none when there are none or several. */
74
+ function pluginFramework(plugins) {
75
+ const found = /* @__PURE__ */ new Set();
76
+ for (const plugin of plugins) {
77
+ const name = plugin?.name;
78
+ if (typeof name !== "string") continue;
79
+ for (const [framework, pattern] of JSX_PLUGINS) if (pattern.test(name)) found.add(framework);
80
+ }
81
+ return found.size === 1 ? [...found][0] : void 0;
82
+ }
83
+ /** tsconfig's JSON with comments and trailing commas, as TypeScript reads it. */
84
+ function parseJsonc(text) {
85
+ let out = "";
86
+ for (let i = 0; i < text.length; i++) {
87
+ const c = text[i];
88
+ if (c === "\"") {
89
+ const end = /"(?:[^"\\]|\\.)*"/y;
90
+ end.lastIndex = i;
91
+ const string = end.exec(text)?.[0] ?? c;
92
+ out += string;
93
+ i += string.length - 1;
94
+ } else if (c === "/" && text[i + 1] === "/") {
95
+ while (i < text.length && text[i] !== "\n") i++;
96
+ out += "\n";
97
+ } else if (c === "/" && text[i + 1] === "*") {
98
+ const end = text.indexOf("*/", i + 2);
99
+ i = end < 0 ? text.length : end + 1;
100
+ } else out += c;
101
+ }
102
+ return JSON.parse(out.replace(/,(\s*[}\]])/g, "$1"));
103
+ }
104
+ /** A tsconfig named by `extends` or a reference: a file, a directory's `tsconfig.json`, or a package's. */
105
+ function tsconfigPath(from, name) {
106
+ const local = name.startsWith(".") || name.startsWith("/");
107
+ let path;
108
+ try {
109
+ path = local ? resolve(dirname(from), name) : createRequire(from).resolve(name);
110
+ } catch {
111
+ return;
112
+ }
113
+ if (existsSync(path) && statSync(path).isDirectory()) path = join(path, "tsconfig.json");
114
+ else if (!existsSync(path) && existsSync(`${path}.json`)) path = `${path}.json`;
115
+ return existsSync(path) ? path : void 0;
116
+ }
117
+ /**
118
+ * `compilerOptions.jsxImportSource` as TypeScript resolves it for `file`:
119
+ * its own, else its `extends`' (the last one first); then, for a solution
120
+ * file (`references`, as create-vite writes), the first referenced project
121
+ * that sets one.
122
+ */
123
+ function tsconfigJsxSource(file, seen = /* @__PURE__ */ new Set()) {
124
+ if (seen.has(file) || !existsSync(file)) return void 0;
125
+ seen.add(file);
126
+ let config;
127
+ try {
128
+ config = parseJsonc(readFileSync(file, "utf8"));
129
+ } catch {
130
+ return;
131
+ }
132
+ const own = config.compilerOptions?.jsxImportSource;
133
+ if (typeof own === "string") return own;
134
+ const bases = typeof config.extends === "string" ? [config.extends] : config.extends ?? [];
135
+ for (const base of [...bases].reverse()) {
136
+ const path = tsconfigPath(file, base);
137
+ const found = path && tsconfigJsxSource(path, seen);
138
+ if (found) return found;
139
+ }
140
+ for (const reference of config.references ?? []) {
141
+ const path = tsconfigPath(file, reference.path);
142
+ const found = path && tsconfigJsxSource(path, seen);
143
+ if (found) return found;
144
+ }
145
+ }
146
+ /**
147
+ * The framework of JSX without a pragma: the JSX import source the Vite
148
+ * config gives Oxc or esbuild; else the one JSX framework whose Vite plugin
149
+ * is installed (`vite-plugin-solid`, the Preact preset); else the root
150
+ * tsconfig's `jsxImportSource`, through `extends` and `references`; else
151
+ * Preact, Solid or Vue when the root `package.json` depends on it; else React.
152
+ */
153
+ function jsxFrameworkOf(config, root) {
154
+ const oxc = config.oxc;
155
+ const esbuild = config.esbuild;
156
+ const fromVite = (oxc && oxc.jsx?.importSource) ?? (esbuild && esbuild.jsxImportSource);
157
+ if (typeof fromVite === "string") return jsxFramework(fromVite);
158
+ const plugin = pluginFramework(config.plugins ?? []);
159
+ if (plugin) return plugin;
160
+ const fromTsconfig = tsconfigJsxSource(join(root, "tsconfig.json"));
161
+ if (fromTsconfig) return jsxFramework(fromTsconfig);
162
+ const names = dependencies(root);
163
+ if (names?.has("preact")) return "preact";
164
+ if (names?.has("solid-js")) return "solid";
165
+ if (names?.has("vue") && !names.has("react")) return "vue";
166
+ return "react";
167
+ }
168
+ //#endregion
169
+ //#region ../glue/src/vite.ts
170
+ /**
171
+ * The build side of the shared glue (machine PLAN §5.1, §5.5): a Vite
172
+ * plugin that resolves a package's hooks (`useMachine` from
173
+ * `@menapo/machine`) per module to that module's framework, so one import
174
+ * works in React, Preact, Vue, Svelte, Solid and plain scripts, and no
175
+ * package has a `/react` or `/vue` entry (repo rule: entries by role).
176
+ *
177
+ * - **Per module:** each module importing a hook gets the hook from
178
+ * `virtual:<package>/glue/<framework>`, which resolves to the package's
179
+ * glue module for that framework; the rest of the import stays. Every
180
+ * form that reaches a hook is rewritten: named imports (string names
181
+ * too), a namespace import's members, re-exports, `export *` and
182
+ * `import()`.
183
+ * - **The app's framework:** a glue module and the adapters it uses import
184
+ * `react`, `vue`… by name. Those imports resolve from the project root, as
185
+ * the app's own do, so the app's single copy is used wherever the glue
186
+ * module lives (a workspace package, `node_modules`); one that does not
187
+ * resolve there fails the build, naming the fix.
188
+ */
189
+ /**
190
+ * A path as Vite writes ids, with forward slashes on every platform:
191
+ * `fileURLToPath` gives backslashes on Windows, and a prefix test of one
192
+ * against the other never matches.
193
+ */
194
+ const posix = (path) => path.replace(/\\/g, "/");
195
+ /** Whether `id` is inside one of `dirs`, both compared as Vite writes them. */
196
+ const within = (dirs, id) => dirs.some((dir) => posix(id).startsWith(posix(dir)));
197
+ /** The adapters' own directory: their framework imports resolve from the project root too. */
198
+ const OWN$2 = dirname(fileURLToPath(import.meta.url));
199
+ /** Packages a glue module imports by name: resolved from the project root. */
200
+ const FRAMEWORK_IMPORT = /^(?:react|react-dom|preact|vue|svelte|solid-js)(?:\/|$)/;
201
+ /** The modules the rewrite reads: scripts and framework files, never `?raw` text or an asset's URL. */
202
+ const MODULE = /\.(?:[cm]?[jt]sx?|vue|svelte)(?:\?|$)/;
203
+ const NOT_CODE = /[?&](?:raw|url|inline|worker|sharedworker)(?:[&=]|$)/;
204
+ /** The scripts of a module: the whole of it, or a `.vue`/`.svelte` file's `<script>` blocks. */
205
+ function scripts(code, file) {
206
+ const framework = /\.(?:vue|svelte)$/.test(file);
207
+ if (!framework || !code.includes("<script")) return [{
208
+ offset: 0,
209
+ source: code,
210
+ name: framework ? "x.ts" : file
211
+ }];
212
+ const out = [];
213
+ for (const match of code.matchAll(/(<script\b([^>]*)>)([\s\S]*?)<\/script>/g)) {
214
+ const lang = /\blang\s*=\s*["']([jt]sx?)["']/.exec(match[2])?.[1] ?? "js";
215
+ out.push({
216
+ offset: match.index + match[1].length,
217
+ source: match[3],
218
+ name: `x.${lang}`
219
+ });
220
+ }
221
+ return out;
222
+ }
223
+ /** Every node below `node`, depth first. */
224
+ function walk$2(node, visit) {
225
+ if (Array.isArray(node)) for (const child of node) walk$2(child, visit);
226
+ else if (node && typeof node === "object" && typeof node.type === "string") {
227
+ visit(node);
228
+ for (const key of Object.keys(node)) if (key !== "parent") walk$2(node[key], visit);
229
+ }
230
+ }
231
+ const nameOf = (node) => node.type === "Identifier" ? node.name : String(node.value);
232
+ /** A name as a specifier writes it: bare when it is an identifier, else quoted (`"a-b"`). */
233
+ const spell = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
234
+ /**
235
+ * Rewrites every way module `code` reaches `hooks` of `pkg` so they come
236
+ * from `target`; type-only imports and specifiers stay. Replacements keep
237
+ * the lines they replace, so every other position in the module stands.
238
+ * Null when the module reaches no hook.
239
+ */
240
+ function rewrite(code, file, pkg, hooks, target) {
241
+ const edits = [];
242
+ const to = JSON.stringify(target);
243
+ const from = JSON.stringify(pkg);
244
+ const lines = (start, end) => "\n".repeat(code.slice(start, end).split("\n").length - 1);
245
+ for (const { offset, source, name } of scripts(code, file)) {
246
+ const { program, errors } = parseSync(name, source);
247
+ if (errors.length) continue;
248
+ const at = (node) => [offset + node.start, offset + node.end];
249
+ /** Namespace imports of the package: their hook members are rewritten to `__glue_<hook>`. */
250
+ const namespaces = /* @__PURE__ */ new Set();
251
+ /** Where the hooks those members read are imported: before the namespace's import, on its line. */
252
+ let hidden;
253
+ let used = false;
254
+ for (const statement of program.body) if (statement.type === "ImportDeclaration" && statement.source.value === pkg) {
255
+ if (statement.importKind === "type") continue;
256
+ const mine = [];
257
+ const rest = [];
258
+ let namespace;
259
+ for (const specifier of statement.specifiers) if (specifier.type === "ImportNamespaceSpecifier") namespace = specifier.local.name;
260
+ else if (specifier.type === "ImportSpecifier") {
261
+ const imported = nameOf(specifier.imported);
262
+ const local = specifier.local.name;
263
+ const text = imported === local ? local : `${spell(imported)} as ${local}`;
264
+ if (specifier.importKind !== "type" && hooks.includes(imported)) mine.push(text);
265
+ else rest.push(specifier.importKind === "type" ? `type ${text}` : text);
266
+ }
267
+ if (namespace !== void 0) {
268
+ namespaces.add(namespace);
269
+ hidden ??= offset + statement.start;
270
+ }
271
+ if (!mine.length) continue;
272
+ const [start, end] = at(statement);
273
+ const kept = rest.length ? `import { ${rest.join(", ")} } from ${from}; ` : "";
274
+ edits.push([
275
+ start,
276
+ end,
277
+ `${kept}import { ${mine.join(", ")} } from ${to};${lines(start, end)}`
278
+ ]);
279
+ } else if (statement.type === "ExportNamedDeclaration" && statement.source?.value === pkg && statement.exportKind !== "type") {
280
+ const mine = [];
281
+ const rest = [];
282
+ for (const specifier of statement.specifiers) {
283
+ const local = nameOf(specifier.local);
284
+ const exported = nameOf(specifier.exported);
285
+ const text = local === exported ? spell(local) : `${spell(local)} as ${spell(exported)}`;
286
+ if (specifier.exportKind !== "type" && hooks.includes(local)) mine.push(text);
287
+ else rest.push(specifier.exportKind === "type" ? `type ${text}` : text);
288
+ }
289
+ if (!mine.length) continue;
290
+ const [start, end] = at(statement);
291
+ const kept = rest.length ? `export { ${rest.join(", ")} } from ${from}; ` : "";
292
+ edits.push([
293
+ start,
294
+ end,
295
+ `${kept}export { ${mine.join(", ")} } from ${to};${lines(start, end)}`
296
+ ]);
297
+ } else if (statement.type === "ExportAllDeclaration" && statement.source.value === pkg && !statement.exported && statement.exportKind !== "type") {
298
+ const [, end] = at(statement);
299
+ edits.push([
300
+ end,
301
+ end,
302
+ ` export { ${hooks.join(", ")} } from ${to};`
303
+ ]);
304
+ }
305
+ walk$2(program.body, (node) => {
306
+ if (node.type === "ImportExpression" && node.source.type === "Literal" && node.source.value === pkg) {
307
+ const [start, end] = at(node);
308
+ edits.push([
309
+ start,
310
+ end,
311
+ `Promise.all([import(${from}), import(${to})]).then(([m, g]) => ({ ...m, ...g }))`
312
+ ]);
313
+ } else if (node.type === "MemberExpression" && node.object.type === "Identifier" && namespaces.has(node.object.name)) {
314
+ const key = node.computed ? node.property.type === "Literal" && typeof node.property.value === "string" ? node.property.value : void 0 : nameOf(node.property);
315
+ if (key === void 0 || !hooks.includes(key)) return;
316
+ const [start, end] = at(node);
317
+ edits.push([
318
+ start,
319
+ end,
320
+ `__glue_${key}`
321
+ ]);
322
+ used = true;
323
+ }
324
+ });
325
+ if (used && hidden !== void 0) {
326
+ const names = hooks.map((hook) => `${hook} as __glue_${hook}`).join(", ");
327
+ edits.push([
328
+ hidden,
329
+ hidden,
330
+ `import { ${names} } from ${to}; `
331
+ ]);
332
+ }
333
+ }
334
+ if (!edits.length) return null;
335
+ let out = code;
336
+ for (const [start, end, text] of edits.sort((a, b) => b[0] - a[0] || b[1] - a[1])) out = out.slice(0, start) + text + out.slice(end);
337
+ return out;
338
+ }
339
+ /** The plugin resolving `options.hooks` per framework. */
340
+ function gluePlugin(options) {
341
+ const prefix = `virtual:${options.package}/glue/`;
342
+ const dirs = [OWN$2, ...options.dirs].map(posix);
343
+ let settings = {
344
+ scripts: "vanilla",
345
+ jsx: "react"
346
+ };
347
+ let root = process.cwd();
348
+ return {
349
+ name: options.name,
350
+ enforce: "pre",
351
+ configResolved(config) {
352
+ root = config.root;
353
+ settings = {
354
+ scripts: options.framework ?? rootFramework(root) ?? "vanilla",
355
+ jsx: jsxFrameworkOf(config, root)
356
+ };
357
+ },
358
+ async resolveId(source, importer) {
359
+ if (source.startsWith(prefix)) return posix(options.module(source.slice(prefix.length)));
360
+ if (importer && FRAMEWORK_IMPORT.test(source) && within(dirs, importer)) {
361
+ const found = await this.resolve(source, join(root, "index.html"), { skipSelf: true });
362
+ if (found) return found;
363
+ this.error(`[${options.name}] the ${options.package} glue imports "${source}", which does not resolve from the project root (${root}): install it there, or tell the plugin the framework (its \`framework\` option for plain scripts, a \`@jsxImportSource\` pragma or tsconfig's \`jsxImportSource\` for JSX).`);
364
+ }
365
+ return null;
366
+ },
367
+ transform: {
368
+ filter: {
369
+ id: {
370
+ include: MODULE,
371
+ exclude: NOT_CODE
372
+ },
373
+ code: options.package
374
+ },
375
+ handler(code, id) {
376
+ if (id.startsWith("\0") || within(dirs, id)) return null;
377
+ const framework = frameworkOf(id, code, settings);
378
+ const out = rewrite(code, posix(id).replace(/[?#].*$/, ""), options.package, options.hooks, prefix + framework);
379
+ return out === null ? null : {
380
+ code: out,
381
+ map: null
382
+ };
383
+ }
384
+ }
385
+ };
386
+ }
387
+ //#endregion
388
+ //#region ../glue/src/devtools.ts
389
+ /** A panel: an iframe of the page served at `<base><path>`. */
390
+ const dockEntry = (panel, base, path) => ({
391
+ ...panel,
392
+ category: "framework",
393
+ type: "iframe",
394
+ url: base + path
395
+ });
396
+ /** Serves `page()` at `<anything>/<path>` while `on()` holds; other requests pass. */
397
+ function serveDock(server, path, on, page) {
398
+ server.middlewares.use((req, res, next) => {
399
+ const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
400
+ if (!on() || req.method !== "GET" || !pathname.endsWith(`/${path}`)) return next();
401
+ res.setHeader("content-type", "text/html; charset=utf-8");
402
+ res.setHeader("cache-control", "no-store");
403
+ res.end(page());
404
+ });
405
+ }
406
+ /**
407
+ * The page's bridge as the panel finds it: the global `name` on the nearest
408
+ * ancestor window that has it (Vite DevTools may nest the iframe). A panel
409
+ * page runs from its own source text, so it is handed this function's
410
+ * source (`${findBridge}`) rather than importing it.
411
+ */
412
+ function findBridge(name) {
413
+ for (let w = window.parent;; w = w.parent) {
414
+ try {
415
+ const found = w[name];
416
+ if (found) return found;
417
+ } catch {}
418
+ if (w === w.parent) return void 0;
419
+ }
420
+ }
421
+ //#endregion
422
+ //#region src/vite/devtools.ts
423
+ /**
424
+ * `forms:devtools` (D18, M6): a panel in the Vite DevTools dock listing the
425
+ * forms rendered on the page with their values, the errors shown, what is
426
+ * touched and dirty, and the last submits with their timings and errors,
427
+ * on the shared dock plumbing (`@menapo/glue/devtools`), as query's panel.
428
+ * Vite DevTools calls `devtools.setup` only when the user turned it on, so
429
+ * until then this plugin does nothing; a build never runs it.
430
+ */
431
+ const OWN$1 = posix(fileURLToPath(new URL("..", import.meta.url)));
432
+ const DOCK_PATH = "__forms/dock";
433
+ const PAGE_ID = "virtual:forms/devtools";
434
+ const BRIDGE = "__formsDevtools";
435
+ /** The page's half, sent as source text: watches the page's forms through `inspect`. */
436
+ function page(inspect, global) {
437
+ const listeners = /* @__PURE__ */ new Set();
438
+ const live = /* @__PURE__ */ new Set();
439
+ const recent = [];
440
+ const text = (error) => error === void 0 ? void 0 : String(error.message ?? error);
441
+ const json = (value) => JSON.stringify(value, (_, v) => typeof Blob !== "undefined" && v instanceof Blob ? `[${v.type || "file"}, ${v.size} B]` : v);
442
+ inspect((event) => {
443
+ const instance = event.instance;
444
+ if (event.kind === "gone") live.delete(instance);
445
+ else live.add(instance);
446
+ if (event.kind === "submit") {
447
+ recent.unshift({
448
+ form: instance.F.n,
449
+ ms: Math.round(event.ms),
450
+ error: text(event.error)
451
+ });
452
+ if (recent.length > 50) recent.pop();
453
+ }
454
+ for (const listener of listeners) listener();
455
+ });
456
+ globalThis[global] = {
457
+ rows: () => [...live].map((i) => ({
458
+ form: i.F.n,
459
+ id: i.id,
460
+ values: json(i.v) ?? "{}",
461
+ errors: [...i.r.entries()].filter(([, r]) => r.e !== void 0).map(([path, r]) => ({
462
+ path,
463
+ message: r.e
464
+ })),
465
+ touched: [...i.r.entries()].filter(([, r]) => r.t).map(([path]) => path),
466
+ dirty: [...i.dp],
467
+ pending: i.pending,
468
+ error: text(i.error)
469
+ })),
470
+ recent: () => recent,
471
+ subscribe(listener) {
472
+ listeners.add(listener);
473
+ return () => void listeners.delete(listener);
474
+ }
475
+ };
476
+ }
477
+ const pageModule = () => `import { inspect } from "@menapo/forms";\n(${page.toString()})(inspect, ${JSON.stringify(BRIDGE)});\n`;
478
+ /** The dock's document: each form, then the recent submits. */
479
+ const dockPage = () => `<!doctype html>
480
+ <html lang="en"><head><meta charset="utf-8"><title>Forms</title>
481
+ <style>
482
+ body{font:13px system-ui,sans-serif;margin:0;color:CanvasText;background:Canvas}
483
+ table{border-collapse:collapse;width:100%}th,td{text-align:left;vertical-align:top;padding:4px 8px;border-bottom:1px solid #8884}
484
+ th{position:sticky;top:0;background:Canvas}code{font:12px ui-monospace,monospace;white-space:pre-wrap}.error{color:#c33}
485
+ h2{font-size:13px;margin:12px 8px 4px}
486
+ </style></head><body>
487
+ <table><thead><tr><th scope="col">Form</th><th scope="col">Values</th><th scope="col">Errors shown</th><th scope="col">Touched</th><th scope="col">Dirty</th><th scope="col">State</th></tr></thead><tbody id="rows"></tbody></table>
488
+ <p id="none" hidden>No form is rendered.</p>
489
+ <h2>Recent submits</h2>
490
+ <table><thead><tr><th scope="col">Form</th><th scope="col">Time</th><th scope="col">Result</th></tr></thead><tbody id="recent"></tbody></table>
491
+ <script type="module">
492
+ const findBridge = ${findBridge.toString()};
493
+ const bridge = findBridge(${JSON.stringify(BRIDGE)});
494
+ const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
495
+ function render() {
496
+ const rows = bridge ? bridge.rows() : [];
497
+ document.getElementById("none").hidden = rows.length > 0;
498
+ document.getElementById("rows").innerHTML = rows.map((r) => '<tr><td><code>' + esc(r.form) + '</code></td><td><code>' + esc(r.values) + '</code></td><td class="error">' + r.errors.map((e) => esc(e.path) + ': ' + esc(e.message)).join('<br>') + '</td><td>' + esc(r.touched.join(', ')) + '</td><td>' + esc(r.dirty.join(', ')) + '</td><td>' + (r.pending ? 'submitting' : r.error ? '<span class="error">' + esc(r.error) + '</span>' : 'idle') + '</td></tr>').join("");
499
+ document.getElementById("recent").innerHTML = (bridge ? bridge.recent() : []).map((s) => '<tr><td><code>' + esc(s.form) + '</code></td><td>' + s.ms + ' ms</td><td class="' + (s.error ? 'error' : '') + '">' + (s.error ? esc(s.error) : 'ok') + '</td></tr>').join("");
500
+ }
501
+ if (bridge) { bridge.subscribe(render); }
502
+ render();
503
+ <\/script></body></html>`;
504
+ const formsDockEntry = (base) => dockEntry({
505
+ id: "forms",
506
+ title: "Forms",
507
+ icon: "ph:textbox-duotone"
508
+ }, base, DOCK_PATH);
509
+ const RESOLVED = "\0" + PAGE_ID;
510
+ function devtoolsPlugin() {
511
+ let on = false;
512
+ return {
513
+ name: "forms:devtools",
514
+ apply: "serve",
515
+ devtools: { setup(context) {
516
+ on = true;
517
+ context.docks.register(formsDockEntry(context.viteConfig.base));
518
+ } },
519
+ configureServer(server) {
520
+ serveDock(server, DOCK_PATH, () => on, dockPage);
521
+ },
522
+ resolveId: {
523
+ filter: { id: /^virtual:forms\/devtools$/ },
524
+ handler: (id) => "\0" + id
525
+ },
526
+ load: {
527
+ filter: { id: /^\0virtual:forms\/devtools$/ },
528
+ handler: (id) => id === RESOLVED ? {
529
+ code: pageModule(),
530
+ moduleType: "js"
531
+ } : null
532
+ },
533
+ transform: {
534
+ filter: {
535
+ id: {
536
+ include: /\.[cm]?[jt]sx?(?:\?|$)/,
537
+ exclude: /\/node_modules\//
538
+ },
539
+ code: "@menapo/forms"
540
+ },
541
+ handler(code, id) {
542
+ if (!on || this.environment.config.consumer !== "client" || posix(id).startsWith(OWN$1)) return null;
543
+ return {
544
+ code: `${code}\nimport ${JSON.stringify(PAGE_ID)};\n`,
545
+ map: null
546
+ };
547
+ }
548
+ }
549
+ };
550
+ }
551
+ //#endregion
552
+ //#region src/vite/names.ts
553
+ /**
554
+ * Definitions as the plugin rewrites them (D1, D4, D13, D17): each
555
+ * top-level `const x = form(…)` is pointed at the factory of its kind,
556
+ * route or function, and gets a hidden last argument, `{ n:
557
+ * "src/forms.ts#x", s, f, c, a }`: its name for DevTools and errors, a route
558
+ * form's browser schema (a `virtual:` module, D4), the field spec, the
559
+ * checks module when its options have `checks`, and the array module when
560
+ * its spec or its defaults have an array (D14). A call that isn't a
561
+ * top-level declaration (a new form each render) is reported. Plain data
562
+ * in and out, so it is unit-tested without Vite.
563
+ */
564
+ const ROUTE = /^(POST|PUT|PATCH|DELETE) (\/\S*)$/;
565
+ /** The schema module of a route: `virtual:@menapo/forms/schema/post/orgs/:slug`. */
566
+ const SCHEMA_PREFIX = "virtual:@menapo/forms/schema/";
567
+ const schemaId = (route) => {
568
+ const [, method, pattern] = ROUTE.exec(route);
569
+ return SCHEMA_PREFIX + method.toLowerCase() + pattern;
570
+ };
571
+ function walk$1(node, visit) {
572
+ if (Array.isArray(node)) for (const child of node) walk$1(child, visit);
573
+ else if (node && typeof node === "object" && typeof node.type === "string") {
574
+ visit(node);
575
+ for (const key of Object.keys(node)) if (key !== "parent") walk$1(node[key], visit);
576
+ }
577
+ }
578
+ /** Rewrites module `code`; with no options, only finds its definitions. */
579
+ function annotate(code, file, options) {
580
+ const none = {
581
+ code: null,
582
+ definitions: [],
583
+ warnings: []
584
+ };
585
+ const { program, errors } = parseSync(file, code);
586
+ if (errors.length) return none;
587
+ const local = /* @__PURE__ */ new Set();
588
+ for (const statement of program.body) if (statement.type === "ImportDeclaration" && statement.source.value === "@menapo/forms" && statement.importKind !== "type") {
589
+ for (const s of statement.specifiers) if (s.type === "ImportSpecifier" && s.importKind !== "type" && (s.imported.type === "Identifier" ? s.imported.name : s.imported.value) === "form") local.add(s.local.name);
590
+ }
591
+ if (!local.size) return none;
592
+ const named = /* @__PURE__ */ new Set();
593
+ const edits = [];
594
+ const found = [];
595
+ const imports = /* @__PURE__ */ new Map();
596
+ let schemas = 0;
597
+ for (const statement of program.body) {
598
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
599
+ if (declaration?.type !== "VariableDeclaration") continue;
600
+ for (const declarator of declaration.declarations) {
601
+ const call = declarator.init;
602
+ if (call?.type !== "CallExpression" || call.callee.type !== "Identifier" || !local.has(call.callee.name) || declarator.id.type !== "Identifier" || !call.arguments.length || call.arguments.some((a) => a.type === "SpreadElement")) continue;
603
+ named.add(call);
604
+ const first = call.arguments[0];
605
+ const route = first.type === "Literal" && typeof first.value === "string" && ROUTE.test(first.value) ? first.value : void 0;
606
+ const definition = route ? {
607
+ name: declarator.id.name,
608
+ kind: "route",
609
+ route
610
+ } : {
611
+ name: declarator.id.name,
612
+ kind: "fn"
613
+ };
614
+ found.push(definition);
615
+ if (!options) continue;
616
+ const at = route ? 1 : 2;
617
+ const given = call.arguments[at];
618
+ const option = (name) => given?.type === "ObjectExpression" ? given.properties.find((p) => p.type === "Property" && !p.computed && (p.key.type === "Identifier" && p.key.name === name || p.key.type === "Literal" && p.key.value === name)) : void 0;
619
+ const checks = option("checks") !== void 0;
620
+ let arrays = false;
621
+ walk$1(option("defaults"), (node) => void (arrays ||= node.type === "ArrayExpression"));
622
+ const parts = [`n: ${JSON.stringify(`${file}#${declarator.id.name}`)}`];
623
+ if (route) {
624
+ const id = `__forms_schema${schemas++}`;
625
+ imports.set(id, `default from ${JSON.stringify(schemaId(route))}`);
626
+ parts.push(`s: ${id}`);
627
+ }
628
+ const spec = options.spec(definition);
629
+ if (spec && Object.keys(spec).length) parts.push(`f: ${JSON.stringify(spec)}`);
630
+ if (arrays || Object.values(spec ?? {}).some((field) => field.t === "array")) {
631
+ imports.set("__forms_array", `{ arrayField as __forms_array } from ${JSON.stringify(options.modules.array)}`);
632
+ parts.push("a: __forms_array");
633
+ }
634
+ if (checks) {
635
+ imports.set("__forms_checks", `{ checks as __forms_checks } from ${JSON.stringify(options.modules.checks)}`);
636
+ parts.push("c: __forms_checks");
637
+ }
638
+ const factory = route ? "__forms_routeForm" : "__forms_fnForm";
639
+ imports.set(factory, `{ ${route ? "routeForm" : "fnForm"} as ${factory} } from ${JSON.stringify(route ? options.modules.route : options.modules.fn)}`);
640
+ edits.push([
641
+ call.callee.start,
642
+ call.callee.end,
643
+ factory
644
+ ]);
645
+ const last = call.arguments.at(-1);
646
+ const missing = at + 1 - call.arguments.length;
647
+ edits.push([
648
+ last.end,
649
+ last.end,
650
+ `${", undefined".repeat(Math.max(0, missing))}, { ${parts.join(", ")} }`
651
+ ]);
652
+ }
653
+ }
654
+ const warnings = [];
655
+ walk$1(program.body, (node) => {
656
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && local.has(node.callee.name) && !named.has(node)) {
657
+ const line = code.slice(0, node.start).split("\n").length;
658
+ warnings.push(`[forms:INLINE_FORM] ${file}:${line}: form() inside a component makes a new form each render; export it from a module (export const x = form(…)) and import it.`);
659
+ }
660
+ });
661
+ if (!options || !edits.length) return {
662
+ code: null,
663
+ definitions: found,
664
+ warnings
665
+ };
666
+ const at = program.body.findLast((s) => s.type === "ImportDeclaration").end;
667
+ const lines = [...imports.entries()].map(([id, spec]) => spec.startsWith("default ") ? `import ${id} ${spec.slice(8)};` : `import ${spec};`);
668
+ edits.push([
669
+ at,
670
+ at,
671
+ lines.join(" ")
672
+ ]);
673
+ let out = code;
674
+ for (const [start, end, text] of edits.sort((a, b) => b[0] - a[0] || b[1] - a[1])) out = out.slice(0, start) + text + out.slice(end);
675
+ return {
676
+ code: out,
677
+ definitions: found,
678
+ warnings
679
+ };
680
+ }
681
+ //#endregion
682
+ //#region src/vite/slice.ts
683
+ /**
684
+ * The schema slice (D4, spike S1): the browser's copy of an operation's
685
+ * input schema, cut from the module that defines the operation. It keeps
686
+ * the `input` expression and the top-level declarations it reads,
687
+ * transitively, with their imports; `run`, `meta` and whatever only they
688
+ * read stay on the server. A slice is refused, naming the fix, when it
689
+ * would read a binding that isn't a top-level declaration or import, or
690
+ * import a module the browser can't or mustn't load (Node's built-ins, the
691
+ * server side of `@menapo/api`, Hono, a `server/` directory or a
692
+ * `.server.` module, or a local module that imports one of those).
693
+ *
694
+ * `toStandardJsonSchema(x)` from `@valibot/to-json-schema` is cut to `x`:
695
+ * the browser validates and never converts, so the converter stays out of
696
+ * its bundle. Plain data in and out, so it is unit-tested without Vite.
697
+ */
698
+ /** Bare imports the browser must not load: the server side. */
699
+ const SERVER = /^(?:node:|hono(?:\/|$)|@hono\/|@menapo\/api(?!\/client)(?:\/|$)|@menapo\/query\/server)/;
700
+ const BUILTIN = new Set(builtinModules);
701
+ /** Local modules that say they are the server's. */
702
+ const SERVER_FILE = /(?:^|\/)server\/|\.server\.[cm]?[jt]sx?$/;
703
+ const EXTENSIONS = [
704
+ "",
705
+ ".ts",
706
+ ".tsx",
707
+ ".js",
708
+ ".jsx",
709
+ ".mts",
710
+ ".mjs",
711
+ "/index.ts",
712
+ "/index.js"
713
+ ];
714
+ var SliceError = class extends Error {};
715
+ /** Every node below `node`, depth first, skipping TypeScript's types (they read no values). */
716
+ function walk(node, visit, parent, key = "") {
717
+ if (Array.isArray(node)) {
718
+ for (const child of node) walk(child, visit, parent, key);
719
+ return;
720
+ }
721
+ if (!node || typeof node !== "object" || typeof node.type !== "string") return;
722
+ const n = node;
723
+ if (visit(n, parent, key) === false) return;
724
+ if (n.type.startsWith("TS")) {
725
+ const expression = n.expression;
726
+ if (expression) walk(expression, visit, n, "expression");
727
+ return;
728
+ }
729
+ for (const k of Object.keys(n)) {
730
+ if (k === "parent" || k === "typeAnnotation" || k === "returnType" || k === "typeParameters" || k === "typeArguments") continue;
731
+ walk(n[k], visit, n, k);
732
+ }
733
+ }
734
+ /** Names a pattern binds: `a`, `{ a, b: c }`, `[d, ...e]`. */
735
+ function bound(pattern, out) {
736
+ if (!pattern) return;
737
+ switch (pattern.type) {
738
+ case "Identifier":
739
+ out.add(pattern.name);
740
+ break;
741
+ case "ObjectPattern":
742
+ for (const p of pattern.properties) bound(p.type === "RestElement" ? p.argument : p.value, out);
743
+ break;
744
+ case "ArrayPattern":
745
+ for (const e of pattern.elements) bound(e, out);
746
+ break;
747
+ case "RestElement":
748
+ bound(pattern.argument, out);
749
+ break;
750
+ case "AssignmentPattern": bound(pattern.left, out);
751
+ }
752
+ }
753
+ /** The free names `node` reads (approximately: names bound in nested functions are left out). */
754
+ function reads(node) {
755
+ const local = /* @__PURE__ */ new Set();
756
+ walk(node, (n) => {
757
+ if (n.type === "ArrowFunctionExpression" || n.type === "FunctionExpression" || n.type === "FunctionDeclaration") {
758
+ for (const param of n.params) bound(param, local);
759
+ if (n.type !== "ArrowFunctionExpression" && n.id) local.add(n.id.name);
760
+ } else if (n.type === "VariableDeclarator") bound(n.id, local);
761
+ else if (n.type === "CatchClause") bound(n.param, local);
762
+ });
763
+ const out = /* @__PURE__ */ new Set();
764
+ walk(node, (n, parent, key) => {
765
+ if (n.type !== "Identifier") return;
766
+ if (parent?.type === "MemberExpression" && key === "property" && !parent.computed) return;
767
+ if ((parent?.type === "Property" || parent?.type === "MethodDefinition" || parent?.type === "PropertyDefinition") && key === "key" && !parent.computed) return;
768
+ if (parent?.type === "LabeledStatement" || parent?.type === "BreakStatement" || parent?.type === "ContinueStatement") return;
769
+ if (!local.has(n.name)) out.add(n.name);
770
+ });
771
+ return out;
772
+ }
773
+ /** A local module's file, as Node or Vite would find it. */
774
+ function resolveLocal(from, source) {
775
+ const base = resolve(dirname(from), source);
776
+ for (const ext of EXTENSIONS) if (existsSync(base + ext) && statSync(base + ext).isFile()) return base + ext;
777
+ }
778
+ /** Why `source`, imported from `from`, can't reach the browser; undefined when it can. */
779
+ function serverOnly(from, source, seen) {
780
+ if (SERVER.test(source) || BUILTIN.has(source)) return source;
781
+ if (!source.startsWith(".") && !source.startsWith("/")) return void 0;
782
+ if (SERVER_FILE.test(source)) return source;
783
+ const file = resolveLocal(from, source);
784
+ if (!file || seen.has(file)) return void 0;
785
+ seen.add(file);
786
+ if (SERVER_FILE.test(file)) return source;
787
+ const { program, errors } = parseSync(file, readFileSync(file, "utf8"));
788
+ if (errors.length) return void 0;
789
+ for (const statement of program.body) {
790
+ const next = statement.type === "ImportDeclaration" && statement.importKind !== "type" || (statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration") && statement.source && statement.exportKind !== "type" ? statement.source.value : void 0;
791
+ if (next === void 0) continue;
792
+ const why = serverOnly(file, next, seen);
793
+ if (why) return `${source}, which imports ${why}`;
794
+ }
795
+ }
796
+ const GLOBALS = new Set(Object.getOwnPropertyNames(globalThis).concat([
797
+ "undefined",
798
+ "NaN",
799
+ "Infinity",
800
+ "arguments"
801
+ ]));
802
+ /**
803
+ * The browser module for the expression at `[start, end)` of `file`: its
804
+ * declarations and imports, and `export default` the expression.
805
+ */
806
+ function slice(code, file, start, end, options) {
807
+ const { program, errors } = parseSync(file, code);
808
+ if (errors.length) throw new SliceError(`[forms:SCHEMA_SLICE] ${options.label}: ${file} doesn't parse.`);
809
+ const bindings = /* @__PURE__ */ new Map();
810
+ /** Import specifiers by local name: the statement's source and the imported name (`*`, `default`). */
811
+ const imports = /* @__PURE__ */ new Map();
812
+ let converter;
813
+ for (const raw of program.body) {
814
+ const statement = (raw.type === "ExportNamedDeclaration" || raw.type === "ExportDefaultDeclaration") && raw.declaration ? raw.declaration : raw;
815
+ if (statement.type === "ImportDeclaration") {
816
+ if (statement.importKind === "type") continue;
817
+ for (const s of statement.specifiers) {
818
+ if (s.type === "ImportSpecifier" && s.importKind === "type") continue;
819
+ const imported = s.type === "ImportNamespaceSpecifier" ? "*" : s.type === "ImportDefaultSpecifier" ? "default" : s.imported.type === "Identifier" ? s.imported.name : String(s.imported.value);
820
+ imports.set(s.local.name, {
821
+ source: statement.source.value,
822
+ imported
823
+ });
824
+ if (statement.source.value === "@valibot/to-json-schema" && imported === "toStandardJsonSchema") converter = s.local.name;
825
+ }
826
+ } else if (statement.type === "VariableDeclaration") for (const d of statement.declarations) {
827
+ const names = /* @__PURE__ */ new Set();
828
+ bound(d.id, names);
829
+ for (const name of names) bindings.set(name, {
830
+ statement: raw,
831
+ node: d
832
+ });
833
+ }
834
+ else if ((statement.type === "FunctionDeclaration" || statement.type === "ClassDeclaration") && statement.id) bindings.set(statement.id.name, {
835
+ statement: raw,
836
+ node: statement
837
+ });
838
+ }
839
+ /** The node of the expression. */
840
+ let target;
841
+ walk(program.body, (n) => {
842
+ if (target) return false;
843
+ if (n.start === start && n.end === end) {
844
+ target = n;
845
+ return false;
846
+ }
847
+ return n.start <= start && n.end >= end;
848
+ });
849
+ if (!target) throw new SliceError(`[forms:SCHEMA_SLICE] ${options.label}: no expression at ${file}:${start}.`);
850
+ const kept = /* @__PURE__ */ new Map();
851
+ const used = /* @__PURE__ */ new Map();
852
+ const via = /* @__PURE__ */ new Map();
853
+ const queue = [...reads(target)].map((name) => [name, void 0]);
854
+ const seen = /* @__PURE__ */ new Set();
855
+ while (queue.length) {
856
+ const [name, from] = queue.shift();
857
+ if (seen.has(name)) continue;
858
+ seen.add(name);
859
+ if (from) via.set(name, from);
860
+ const chain = () => {
861
+ const steps = [];
862
+ for (let at = name; at; at = via.get(at)) steps.unshift(at);
863
+ return steps.length > 1 ? `reads ${steps[0]}, which ${steps.slice(1).map((s) => `reads ${s}`).join(", which ")}` : `reads ${steps[0]}`;
864
+ };
865
+ const imported = imports.get(name);
866
+ if (imported) {
867
+ if (name === converter) continue;
868
+ const why = serverOnly(file, imported.source, /* @__PURE__ */ new Set());
869
+ if (why) throw new SliceError(`[forms:SCHEMA_SLICE] ${options.label}: the input ${chain()} from ${why}, and the browser can't load that. Move the schema into a module that imports only your schema library (api/<name>.schema.ts) and import it in both places.`);
870
+ used.set(name, imported);
871
+ continue;
872
+ }
873
+ const binding = bindings.get(name);
874
+ if (!binding) {
875
+ if (GLOBALS.has(name)) continue;
876
+ throw new SliceError(`[forms:SCHEMA_SLICE] ${options.label}: the input ${chain()}, which isn't a top-level declaration or import of ${file}. Declare it at the top of the module, or move the schema into a module of its own and import it.`);
877
+ }
878
+ let declarators = kept.get(binding.statement);
879
+ if (!declarators) kept.set(binding.statement, declarators = /* @__PURE__ */ new Set());
880
+ declarators.add(binding.node);
881
+ const read = binding.node.type === "VariableDeclarator" ? binding.node.init ? reads(binding.node.init) : /* @__PURE__ */ new Set() : reads(binding.node);
882
+ for (const next of read) if (!seen.has(next)) queue.push([next, name]);
883
+ }
884
+ /** `code[node]` with `toStandardJsonSchema(x)` cut to `x`. */
885
+ const text = (node) => {
886
+ const edits = [];
887
+ if (converter) walk(node, (n) => {
888
+ if (n.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === converter && n.arguments[0]) {
889
+ const arg = n.arguments[0];
890
+ edits.push([
891
+ n.start,
892
+ n.end,
893
+ `(${text(arg)})`
894
+ ]);
895
+ return false;
896
+ }
897
+ });
898
+ let out = "";
899
+ let at = node.start;
900
+ for (const [s, e, t] of edits.sort((a, b) => a[0] - b[0])) {
901
+ out += code.slice(at, s) + t;
902
+ at = e;
903
+ }
904
+ return out + code.slice(at, node.end);
905
+ };
906
+ const lines = [];
907
+ const bySource = /* @__PURE__ */ new Map();
908
+ for (const [local, { source, imported }] of used) {
909
+ const spec = source.startsWith(".") ? resolveLocal(file, source) ?? resolve(dirname(file), source) : source;
910
+ if (imported === "*") lines.push(`import * as ${local} from ${JSON.stringify(spec)};`);
911
+ else if (imported === "default") lines.push(`import ${local} from ${JSON.stringify(spec)};`);
912
+ else {
913
+ const list = bySource.get(spec) ?? [];
914
+ const name = /^[A-Za-z_$][\w$]*$/.test(imported) ? imported : JSON.stringify(imported);
915
+ list.push(imported === local ? local : `${name} as ${local}`);
916
+ bySource.set(spec, list);
917
+ }
918
+ }
919
+ for (const [spec, list] of bySource) lines.push(`import { ${list.join(", ")} } from ${JSON.stringify(spec)};`);
920
+ for (const raw of program.body) {
921
+ const declarators = kept.get(raw);
922
+ if (!declarators) continue;
923
+ const statement = (raw.type === "ExportNamedDeclaration" || raw.type === "ExportDefaultDeclaration") && raw.declaration ? raw.declaration : raw;
924
+ if (statement.type === "VariableDeclaration") lines.push(`${statement.kind} ${statement.declarations.filter((d) => declarators.has(d)).map((d) => text(d)).join(", ")};`);
925
+ else lines.push(text(statement));
926
+ }
927
+ lines.push(options.as ? `const ${options.as} = ${text(target)};` : `export default ${text(target)};`);
928
+ return lines.join("\n") + "\n";
929
+ }
930
+ //#endregion
931
+ //#region src/vite/spec.ts
932
+ const TYPES = /* @__PURE__ */ new Set([
933
+ "string",
934
+ "number",
935
+ "integer",
936
+ "boolean",
937
+ "array",
938
+ "object"
939
+ ]);
940
+ /** The one non-null type of a property: `["string", "null"]` and `anyOf` alike. */
941
+ function typeOf(schema) {
942
+ const t = schema.type;
943
+ if (typeof t === "string") return {
944
+ type: t,
945
+ schema
946
+ };
947
+ if (Array.isArray(t)) return {
948
+ type: t.find((x) => x !== "null"),
949
+ schema
950
+ };
951
+ const one = (schema.anyOf ?? schema.oneOf)?.filter((o) => o.type !== "null");
952
+ if (one?.length === 1) return typeOf(one[0]);
953
+ if (Array.isArray(schema.enum) && schema.enum.every((x) => typeof x === "string")) return {
954
+ type: "string",
955
+ schema
956
+ };
957
+ return {
958
+ type: void 0,
959
+ schema
960
+ };
961
+ }
962
+ function object(schema, path, out) {
963
+ const properties = schema.properties ?? {};
964
+ const required = new Set(schema.required ?? []);
965
+ for (const [key, property] of Object.entries(properties)) field(property, path ? `${path}.${key}` : key, required.has(key), out);
966
+ }
967
+ function field(property, path, required, out) {
968
+ const { type, schema } = typeOf(property);
969
+ if (type === "object") {
970
+ object(schema, path, out);
971
+ return;
972
+ }
973
+ const spec = {};
974
+ if (type && TYPES.has(type)) spec.t = type;
975
+ if (required) spec.r = 1;
976
+ if (typeof schema.minLength === "number") spec.mn = schema.minLength;
977
+ if (typeof schema.maxLength === "number") spec.mx = schema.maxLength;
978
+ if (typeof schema.minimum === "number") spec.lo = schema.minimum;
979
+ if (typeof schema.maximum === "number") spec.hi = schema.maximum;
980
+ const description = property.description ?? schema.description;
981
+ if (typeof description === "string") spec.d = description;
982
+ out[path] = spec;
983
+ if (type === "array" && schema.items && typeof schema.items === "object") field(schema.items, `${path}.*`, false, out);
984
+ }
985
+ /** The spec of an input's JSON Schema, with its upload fields. */
986
+ function specOf(input, files) {
987
+ const out = {};
988
+ if (input) object(input, "", out);
989
+ for (const [name, limits] of Object.entries(files ?? {})) out[name] = {
990
+ t: "file",
991
+ r: 1,
992
+ ...limits.types && { a: limits.types.join(",") }
993
+ };
994
+ return out;
995
+ }
996
+ //#endregion
997
+ //#region src/vite/index.ts
998
+ /**
999
+ * `@menapo/forms/vite` (PLAN §6.2): resolves `useForm` per framework
1000
+ * through the shared glue (G3), points each definition at the factory of
1001
+ * its kind with its name and field spec (D17, D13), emits a route form's
1002
+ * browser schema as a slice of the operation's own module (D4), read from
1003
+ * api's plugin (`menapo:api`, api D32), and adds the DevTools panel (D18).
1004
+ * The only entry that imports from the build side.
1005
+ */
1006
+ const EXT = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
1007
+ const ROOT = new URL(EXT === ".ts" ? "../" : "./", import.meta.url);
1008
+ const file = (path) => posix(fileURLToPath(new URL(path + EXT, ROOT)));
1009
+ const GLUE = posix(fileURLToPath(new URL("glue/", ROOT)));
1010
+ const MODULES = {
1011
+ fn: file("factories/fn"),
1012
+ route: file("factories/route"),
1013
+ checks: EXT === ".ts" ? file("checks/index") : file("checks"),
1014
+ array: EXT === ".ts" ? file("array/index") : file("array")
1015
+ };
1016
+ const FILES = file("route/files");
1017
+ const OWN = posix(fileURLToPath(ROOT));
1018
+ const SCRIPT = /\.[cm]?[jt]sx?(?:\?|$)/;
1019
+ /** JSON Schema as `~standard.jsonSchema` gives it; transforms ignored (S5, api's `errorMode`). */
1020
+ const JSON_OPTIONS = {
1021
+ target: "draft-2020-12",
1022
+ libraryOptions: { errorMode: "ignore" }
1023
+ };
1024
+ /** A function form's spec, from its schema's Standard JSON Schema form (S5). */
1025
+ function fnSpec(definition) {
1026
+ const convert = (definition?.s)?.["~standard"]?.jsonSchema?.input;
1027
+ if (!convert) return void 0;
1028
+ try {
1029
+ return specOf(convert(JSON_OPTIONS));
1030
+ } catch {
1031
+ return;
1032
+ }
1033
+ }
1034
+ /** `plugins: [forms()]` in a Vite config, beside api's and query's. */
1035
+ function forms(options = {}) {
1036
+ let config;
1037
+ let api;
1038
+ let operations;
1039
+ const warned = /* @__PURE__ */ new Set();
1040
+ const ops = () => operations ??= (async () => {
1041
+ const map = /* @__PURE__ */ new Map();
1042
+ for (const op of await api?.operations().catch(() => []) ?? []) map.set(`${op.method.toUpperCase()} ${op.pattern}`, op);
1043
+ return map;
1044
+ })();
1045
+ return [
1046
+ {
1047
+ name: "forms:names",
1048
+ config: () => ({ optimizeDeps: { exclude: ["@menapo/forms", "@menapo/forms/server"] } }),
1049
+ configEnvironment(name, environment) {
1050
+ return (environment.consumer ?? (name === "client" ? "client" : "server")) === "server" ? { resolve: { noExternal: ["@menapo/forms"] } } : void 0;
1051
+ },
1052
+ configResolved(resolved) {
1053
+ config = resolved;
1054
+ api = resolved.plugins.find((p) => p.name === "menapo:api")?.api;
1055
+ },
1056
+ resolveId: {
1057
+ filter: { id: /^virtual:@menapo\/forms\/schema\// },
1058
+ handler: (id) => `\0${id}`
1059
+ },
1060
+ load: {
1061
+ filter: { id: /^\0virtual:@menapo\/forms\/schema\// },
1062
+ async handler(id) {
1063
+ const rest = id.slice(30);
1064
+ const slash = rest.indexOf("/");
1065
+ const route = `${rest.slice(0, slash).toUpperCase()} ${rest.slice(slash)}`;
1066
+ const op = (await ops()).get(route);
1067
+ const say = (message) => {
1068
+ if (!warned.has(message)) this.warn(message);
1069
+ warned.add(message);
1070
+ };
1071
+ let input = "undefined";
1072
+ let body = "";
1073
+ if (!api) say(`[forms:NO_API_PLUGIN] form("${route}") validates only on the server: add api() from "@menapo/api/vite" to vite.config.ts, before forms(), so the browser runs the operation's own schema.`);
1074
+ else if (!op) this.error(`[forms:NO_OPERATION] form("${route}"): the app has no ${route} operation.`);
1075
+ else if (op.source) try {
1076
+ body = slice(readFileSync(op.source.file, "utf8"), op.source.file, op.source.start, op.source.end, {
1077
+ label: `form("${route}")`,
1078
+ as: "__forms_input"
1079
+ });
1080
+ input = "__forms_input";
1081
+ } catch (error) {
1082
+ if (error instanceof SliceError) this.error(error.message);
1083
+ throw error;
1084
+ }
1085
+ else if (op.input) say(`[forms:NO_SCHEMA_SOURCE] form("${route}") validates only on the server: write its input inline in defineApi({ "${op.pattern}": { ${op.method}: { input } } }), so the plugin finds it.`);
1086
+ if (op?.files && Object.keys(op.files).length) return {
1087
+ code: `${body}import { withFiles } from ${JSON.stringify(FILES)};\nexport default withFiles(${input}, ${JSON.stringify(op.files)});\n`,
1088
+ moduleType: "ts"
1089
+ };
1090
+ return {
1091
+ code: `${body}export default ${input};\n`,
1092
+ moduleType: "ts"
1093
+ };
1094
+ }
1095
+ },
1096
+ transform: {
1097
+ filter: {
1098
+ id: {
1099
+ include: SCRIPT,
1100
+ exclude: /\/node_modules\//
1101
+ },
1102
+ code: "@menapo/forms"
1103
+ },
1104
+ async handler(code, id) {
1105
+ if (id.startsWith("\0") || within([OWN], id)) return null;
1106
+ const path = posix(relative(config.root, id.replace(/[?#].*$/, "")));
1107
+ const map = await ops();
1108
+ let module;
1109
+ const loaded = /* @__PURE__ */ new Map();
1110
+ if (/\bform\(\s*[^"'`\s]/.test(code)) try {
1111
+ module = (await runnerImport(id.replace(/[?#].*$/, ""), {
1112
+ root: config.root,
1113
+ configFile: false,
1114
+ logLevel: "silent"
1115
+ })).module;
1116
+ } catch {
1117
+ module = void 0;
1118
+ }
1119
+ const spec = (definition) => {
1120
+ if (definition.kind === "route") {
1121
+ const op = map.get(definition.route);
1122
+ return op && specOf(op.input, op.files);
1123
+ }
1124
+ if (!loaded.has(definition.name)) loaded.set(definition.name, fnSpec(module?.[definition.name]));
1125
+ return loaded.get(definition.name);
1126
+ };
1127
+ const out = annotate(code, path, {
1128
+ spec,
1129
+ modules: MODULES
1130
+ });
1131
+ for (const warning of out.warnings) this.warn(warning);
1132
+ return out.code === null ? null : {
1133
+ code: out.code,
1134
+ map: null
1135
+ };
1136
+ }
1137
+ },
1138
+ handleHotUpdate() {
1139
+ operations = void 0;
1140
+ }
1141
+ },
1142
+ devtoolsPlugin(),
1143
+ gluePlugin({
1144
+ name: "forms:glue",
1145
+ package: "@menapo/forms",
1146
+ hooks: ["useForm"],
1147
+ module: (framework) => GLUE + (framework === "svelte" ? "svelte.svelte" : framework) + EXT,
1148
+ dirs: [GLUE],
1149
+ framework: options.framework
1150
+ })
1151
+ ];
1152
+ }
1153
+ //#endregion
1154
+ export { forms as default, forms };