@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/LICENSE +21 -0
- package/README.md +45 -2
- package/dist/array.d.ts +5 -0
- package/dist/array.js +101 -0
- package/dist/checks.d.ts +4 -0
- package/dist/checks.js +53 -0
- package/dist/factories/fn.d.ts +8 -0
- package/dist/factories/fn.js +5 -0
- package/dist/factories/route.d.ts +5 -0
- package/dist/factories/route.js +31 -0
- package/dist/form-BZ2OH06z.js +48 -0
- package/dist/glue/preact.d.ts +4 -0
- package/dist/glue/preact.js +59 -0
- package/dist/glue/react.d.ts +4 -0
- package/dist/glue/react.js +55 -0
- package/dist/glue/solid.d.ts +4 -0
- package/dist/glue/solid.js +48 -0
- package/dist/glue/svelte.svelte.d.ts +4 -0
- package/dist/glue/svelte.svelte.js +62 -0
- package/dist/glue/vanilla.d.ts +4 -0
- package/dist/glue/vanilla.js +88 -0
- package/dist/glue/vue.d.ts +4 -0
- package/dist/glue/vue.js +47 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +30 -0
- package/dist/instance-C1aijv06.js +683 -0
- package/dist/names-D_TE3BpM.js +62 -0
- package/dist/paths-DqNafpnl.js +73 -0
- package/dist/route/files.d.ts +5 -0
- package/dist/route/files.js +34 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.js +79 -0
- package/dist/source-DdFTPjID.js +15 -0
- package/dist/types-CdCFdlFp.d.ts +200 -0
- package/dist/view--JGPLHEo.js +308 -0
- package/dist/view-5_kyMWP3.d.ts +331 -0
- package/dist/vite.d.ts +20 -0
- package/dist/vite.js +1154 -0
- package/package.json +98 -3
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) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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 };
|