@menapo/machine 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 +135 -2
- package/dist/chart-BWjU2VF4.js +395 -0
- package/dist/compiled.d.ts +58 -0
- package/dist/compiled.js +43 -0
- package/dist/component-DDXOyYUI.d.ts +147 -0
- package/dist/dev-BAz6e3W-.js +79 -0
- package/dist/dom.d.ts +187 -0
- package/dist/dom.js +659 -0
- package/dist/glue/preact.d.ts +4 -0
- package/dist/glue/preact.js +54 -0
- package/dist/glue/react.d.ts +4 -0
- package/dist/glue/react.js +50 -0
- package/dist/glue/solid.d.ts +4 -0
- package/dist/glue/solid.js +42 -0
- package/dist/glue/svelte.svelte.d.ts +4 -0
- package/dist/glue/svelte.svelte.js +57 -0
- package/dist/glue/vanilla.d.ts +4 -0
- package/dist/glue/vanilla.js +83 -0
- package/dist/glue/vue.d.ts +4 -0
- package/dist/glue/vue.js +41 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +582 -0
- package/dist/inspect-BmyLUhcy.js +74 -0
- package/dist/inspect-C3qosuCm.d.ts +22 -0
- package/dist/instance-BM2UqeXW.js +418 -0
- package/dist/machine-BstWHtWL.d.ts +335 -0
- package/dist/naming-Csj5186j.js +492 -0
- package/dist/records-7FFepzo9.js +10 -0
- package/dist/source-DdFTPjID.js +15 -0
- package/dist/test.d.ts +63 -0
- package/dist/test.js +262 -0
- package/dist/types-Bk8svtO6.d.ts +580 -0
- package/dist/vicss.d.ts +19 -0
- package/dist/vicss.js +51 -0
- package/dist/vite.d.ts +27 -0
- package/dist/vite.js +4281 -0
- package/package.json +87 -3
package/dist/vite.js
ADDED
|
@@ -0,0 +1,4281 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { parseSync } from "vite";
|
|
5
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
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 = 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(node, visit) {
|
|
225
|
+
if (Array.isArray(node)) for (const child of node) walk(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(node[key], visit);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const nameOf$2 = (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$2(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$2(specifier.local);
|
|
284
|
+
const exported = nameOf$2(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(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$2(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, ...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 ../../node_modules/.pnpm/@jridgewell+sourcemap-codec@1.6.0/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.mjs
|
|
389
|
+
var comma = ",".charCodeAt(0);
|
|
390
|
+
var semicolon = ";".charCodeAt(0);
|
|
391
|
+
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
392
|
+
var intToChar$1 = /* @__PURE__ */ new Uint8Array(64);
|
|
393
|
+
var charToInt = /* @__PURE__ */ new Uint8Array(128);
|
|
394
|
+
for (let i = 0; i < chars.length; i++) {
|
|
395
|
+
const c = chars.charCodeAt(i);
|
|
396
|
+
intToChar$1[i] = c;
|
|
397
|
+
charToInt[c] = i;
|
|
398
|
+
}
|
|
399
|
+
function encodeInteger(builder, num) {
|
|
400
|
+
do {
|
|
401
|
+
let clamped = num & 31;
|
|
402
|
+
num >>>= 5;
|
|
403
|
+
if (num > 0) clamped |= 32;
|
|
404
|
+
builder.write(intToChar$1[clamped]);
|
|
405
|
+
} while (num > 0);
|
|
406
|
+
}
|
|
407
|
+
function encodeSign(num) {
|
|
408
|
+
return num < 0 ? -num << 1 | 1 : num << 1;
|
|
409
|
+
}
|
|
410
|
+
var bufLength = 16384;
|
|
411
|
+
var td = typeof TextDecoder !== "undefined" ? /* @__PURE__ */ new TextDecoder() : typeof Buffer !== "undefined" ? { decode(buf) {
|
|
412
|
+
return Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength).toString();
|
|
413
|
+
} } : { decode(buf) {
|
|
414
|
+
let out = "";
|
|
415
|
+
for (let i = 0; i < buf.length; i++) out += String.fromCharCode(buf[i]);
|
|
416
|
+
return out;
|
|
417
|
+
} };
|
|
418
|
+
var StringWriter = class {
|
|
419
|
+
constructor() {
|
|
420
|
+
this.pos = 0;
|
|
421
|
+
this.out = "";
|
|
422
|
+
this.buffer = new Uint8Array(bufLength);
|
|
423
|
+
}
|
|
424
|
+
write(v) {
|
|
425
|
+
const { buffer } = this;
|
|
426
|
+
buffer[this.pos++] = v;
|
|
427
|
+
if (this.pos === bufLength) {
|
|
428
|
+
this.out += td.decode(buffer);
|
|
429
|
+
this.pos = 0;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
flush() {
|
|
433
|
+
const { buffer, out, pos } = this;
|
|
434
|
+
return pos > 0 ? out + td.decode(buffer.subarray(0, pos)) : out;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
function encodeRangeMappings(decoded) {
|
|
438
|
+
if (decoded.length === 0) return "";
|
|
439
|
+
const writer = new StringWriter();
|
|
440
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
441
|
+
const indices = decoded[i];
|
|
442
|
+
if (i > 0) writer.write(semicolon);
|
|
443
|
+
let index = 0;
|
|
444
|
+
for (let j = 0; j < indices.length; j++) {
|
|
445
|
+
const offset = indices[j];
|
|
446
|
+
encodeInteger(writer, offset - index);
|
|
447
|
+
index = offset;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return writer.flush();
|
|
451
|
+
}
|
|
452
|
+
function encode(decoded) {
|
|
453
|
+
const writer = new StringWriter();
|
|
454
|
+
let sourcesIndex = 0;
|
|
455
|
+
let sourceLine = 0;
|
|
456
|
+
let sourceColumn = 0;
|
|
457
|
+
let namesIndex = 0;
|
|
458
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
459
|
+
const line = decoded[i];
|
|
460
|
+
if (i > 0) writer.write(semicolon);
|
|
461
|
+
if (line.length === 0) continue;
|
|
462
|
+
let genColumn = 0;
|
|
463
|
+
for (let j = 0; j < line.length; j++) {
|
|
464
|
+
const segment = line[j];
|
|
465
|
+
if (j > 0) writer.write(comma);
|
|
466
|
+
encodeInteger(writer, encodeSign(segment[0] - genColumn));
|
|
467
|
+
genColumn = segment[0];
|
|
468
|
+
if (segment.length === 1) continue;
|
|
469
|
+
encodeInteger(writer, encodeSign(segment[1] - sourcesIndex));
|
|
470
|
+
encodeInteger(writer, encodeSign(segment[2] - sourceLine));
|
|
471
|
+
encodeInteger(writer, encodeSign(segment[3] - sourceColumn));
|
|
472
|
+
sourcesIndex = segment[1];
|
|
473
|
+
sourceLine = segment[2];
|
|
474
|
+
sourceColumn = segment[3];
|
|
475
|
+
if (segment.length === 4) continue;
|
|
476
|
+
encodeInteger(writer, encodeSign(segment[4] - namesIndex));
|
|
477
|
+
namesIndex = segment[4];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return writer.flush();
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region ../../node_modules/.pnpm/magic-string@1.4.3/node_modules/magic-string/dist/index.mjs
|
|
484
|
+
var BitSet = class BitSet {
|
|
485
|
+
constructor(arg) {
|
|
486
|
+
this.bits = arg instanceof BitSet ? arg.bits.slice() : [];
|
|
487
|
+
}
|
|
488
|
+
add(n) {
|
|
489
|
+
this.bits[n >> 5] |= 1 << (n & 31);
|
|
490
|
+
}
|
|
491
|
+
has(n) {
|
|
492
|
+
return !!(this.bits[n >> 5] & 1 << (n & 31));
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
var Chunk = class Chunk {
|
|
496
|
+
constructor(start, end, content) {
|
|
497
|
+
this.start = start;
|
|
498
|
+
this.end = end;
|
|
499
|
+
this.original = content;
|
|
500
|
+
this.intro = "";
|
|
501
|
+
this.outro = "";
|
|
502
|
+
this.content = content;
|
|
503
|
+
this.storeName = false;
|
|
504
|
+
this.edited = false;
|
|
505
|
+
this.previous = null;
|
|
506
|
+
this.next = null;
|
|
507
|
+
}
|
|
508
|
+
appendLeft(content) {
|
|
509
|
+
this.outro += content;
|
|
510
|
+
}
|
|
511
|
+
appendRight(content) {
|
|
512
|
+
this.intro = this.intro + content;
|
|
513
|
+
}
|
|
514
|
+
clone() {
|
|
515
|
+
const chunk = new Chunk(this.start, this.end, this.original);
|
|
516
|
+
chunk.intro = this.intro;
|
|
517
|
+
chunk.outro = this.outro;
|
|
518
|
+
chunk.content = this.content;
|
|
519
|
+
chunk.storeName = this.storeName;
|
|
520
|
+
chunk.edited = this.edited;
|
|
521
|
+
return chunk;
|
|
522
|
+
}
|
|
523
|
+
contains(index) {
|
|
524
|
+
return this.start < index && index < this.end;
|
|
525
|
+
}
|
|
526
|
+
eachNext(fn) {
|
|
527
|
+
fn(this);
|
|
528
|
+
let chunk = this.next;
|
|
529
|
+
while (chunk) {
|
|
530
|
+
fn(chunk);
|
|
531
|
+
chunk = chunk.next;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
eachPrevious(fn) {
|
|
535
|
+
fn(this);
|
|
536
|
+
let chunk = this.previous;
|
|
537
|
+
while (chunk) {
|
|
538
|
+
fn(chunk);
|
|
539
|
+
chunk = chunk.previous;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
edit(content, storeName, contentOnly) {
|
|
543
|
+
this.content = content;
|
|
544
|
+
if (!contentOnly) {
|
|
545
|
+
this.intro = "";
|
|
546
|
+
this.outro = "";
|
|
547
|
+
}
|
|
548
|
+
this.storeName = storeName;
|
|
549
|
+
this.edited = true;
|
|
550
|
+
return this;
|
|
551
|
+
}
|
|
552
|
+
prependLeft(content) {
|
|
553
|
+
this.outro = content + this.outro;
|
|
554
|
+
}
|
|
555
|
+
prependRight(content) {
|
|
556
|
+
this.intro = content + this.intro;
|
|
557
|
+
}
|
|
558
|
+
reset() {
|
|
559
|
+
this.intro = "";
|
|
560
|
+
this.outro = "";
|
|
561
|
+
if (this.edited) {
|
|
562
|
+
this.content = this.original;
|
|
563
|
+
this.storeName = false;
|
|
564
|
+
this.edited = false;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
split(index) {
|
|
568
|
+
const sliceIndex = index - this.start;
|
|
569
|
+
const originalBefore = this.original.slice(0, sliceIndex);
|
|
570
|
+
const originalAfter = this.original.slice(sliceIndex);
|
|
571
|
+
this.original = originalBefore;
|
|
572
|
+
const newChunk = new Chunk(index, this.end, originalAfter);
|
|
573
|
+
newChunk.outro = this.outro;
|
|
574
|
+
this.outro = "";
|
|
575
|
+
this.end = index;
|
|
576
|
+
if (this.edited) {
|
|
577
|
+
newChunk.edit("", false, true);
|
|
578
|
+
this.content = "";
|
|
579
|
+
} else this.content = originalBefore;
|
|
580
|
+
newChunk.next = this.next;
|
|
581
|
+
if (newChunk.next) newChunk.next.previous = newChunk;
|
|
582
|
+
newChunk.previous = this;
|
|
583
|
+
this.next = newChunk;
|
|
584
|
+
return newChunk;
|
|
585
|
+
}
|
|
586
|
+
toString() {
|
|
587
|
+
return this.intro + this.content + this.outro;
|
|
588
|
+
}
|
|
589
|
+
trimEnd(rx) {
|
|
590
|
+
this.outro = this.outro.replace(rx, "");
|
|
591
|
+
if (this.outro.length) return true;
|
|
592
|
+
const trimmed = this.content.replace(rx, "");
|
|
593
|
+
if (trimmed.length) {
|
|
594
|
+
if (trimmed !== this.content) {
|
|
595
|
+
if (this.edited) this.edit(trimmed, this.storeName, true);
|
|
596
|
+
else this.split(this.start + trimmed.length).edit("", void 0, true);
|
|
597
|
+
}
|
|
598
|
+
return true;
|
|
599
|
+
} else {
|
|
600
|
+
this.edit("", void 0, true);
|
|
601
|
+
this.intro = this.intro.replace(rx, "");
|
|
602
|
+
if (this.intro.length) return true;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
trimStart(rx) {
|
|
606
|
+
this.intro = this.intro.replace(rx, "");
|
|
607
|
+
if (this.intro.length) return true;
|
|
608
|
+
const trimmed = this.content.replace(rx, "");
|
|
609
|
+
if (trimmed.length) {
|
|
610
|
+
if (trimmed !== this.content) {
|
|
611
|
+
if (this.edited) this.edit(trimmed, this.storeName, true);
|
|
612
|
+
else {
|
|
613
|
+
this.split(this.end - trimmed.length);
|
|
614
|
+
this.edit("", void 0, true);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return true;
|
|
618
|
+
} else {
|
|
619
|
+
this.edit("", void 0, true);
|
|
620
|
+
this.outro = this.outro.replace(rx, "");
|
|
621
|
+
if (this.outro.length) return true;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
/**
|
|
626
|
+
* The single error type thrown by MagicString.
|
|
627
|
+
*
|
|
628
|
+
* Every message is prefixed with `[MagicString]` so its source is obvious at a
|
|
629
|
+
* glance, and is kept short and consistent in tone.
|
|
630
|
+
*/
|
|
631
|
+
var MagicStringError = class extends Error {
|
|
632
|
+
name = "MagicStringError";
|
|
633
|
+
constructor(message, options) {
|
|
634
|
+
super(`[MagicString] ${message}`, options);
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
function getBtoa() {
|
|
638
|
+
/* v8 ignore next -- environment fallback, unreachable when `btoa` is present */
|
|
639
|
+
if (typeof globalThis !== "undefined" && typeof globalThis.btoa === "function") return (str) => globalThis.btoa(unescape(encodeURIComponent(str)));
|
|
640
|
+
const buffer = globalThis["Buffer"];
|
|
641
|
+
if (buffer) return (str) => buffer.from(str, "utf-8").toString("base64");
|
|
642
|
+
return () => {
|
|
643
|
+
throw new MagicStringError("unsupported environment: `btoa` or `Buffer` is required");
|
|
644
|
+
};
|
|
645
|
+
/* v8 ignore stop */
|
|
646
|
+
}
|
|
647
|
+
const btoa = /* #__PURE__ */ getBtoa();
|
|
648
|
+
var SourceMap = class {
|
|
649
|
+
constructor(properties) {
|
|
650
|
+
this.version = 3;
|
|
651
|
+
this.file = properties.file;
|
|
652
|
+
this.sources = properties.sources;
|
|
653
|
+
this.sourcesContent = properties.sourcesContent;
|
|
654
|
+
this.names = properties.names;
|
|
655
|
+
this.mappings = typeof properties.mappings === "string" ? properties.mappings : encode(properties.mappings);
|
|
656
|
+
if (typeof properties.x_google_ignoreList !== "undefined") this.x_google_ignoreList = properties.x_google_ignoreList;
|
|
657
|
+
if (typeof properties.debugId !== "undefined") this.debugId = properties.debugId;
|
|
658
|
+
if (typeof properties.rangeMappings !== "undefined") {
|
|
659
|
+
let shouldOutputRangeMapping = false;
|
|
660
|
+
for (const line of properties.rangeMappings) if (line.length !== 0) {
|
|
661
|
+
shouldOutputRangeMapping = true;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
if (shouldOutputRangeMapping) this.rangeMappings = encodeRangeMappings(properties.rangeMappings);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Returns the equivalent of `JSON.stringify(map)`
|
|
669
|
+
*/
|
|
670
|
+
toString() {
|
|
671
|
+
return JSON.stringify(this);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Returns a DataURI containing the sourcemap. Useful for doing this sort of thing:
|
|
675
|
+
* `generateMap(options?: SourceMapOptions): SourceMap;`
|
|
676
|
+
*/
|
|
677
|
+
toUrl() {
|
|
678
|
+
return `data:application/json;charset=utf-8;base64,${btoa(this.toString())}`;
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
function getLocator(source) {
|
|
682
|
+
const lineOffsets = [0];
|
|
683
|
+
for (let i = source.indexOf("\n"); i !== -1; i = source.indexOf("\n", i + 1)) lineOffsets.push(i + 1);
|
|
684
|
+
return function locate(index) {
|
|
685
|
+
let i = 0;
|
|
686
|
+
let j = lineOffsets.length;
|
|
687
|
+
while (i < j) {
|
|
688
|
+
const m = i + j >> 1;
|
|
689
|
+
if (index < lineOffsets[m]) j = m;
|
|
690
|
+
else i = m + 1;
|
|
691
|
+
}
|
|
692
|
+
const line = i - 1;
|
|
693
|
+
return {
|
|
694
|
+
line,
|
|
695
|
+
column: index - lineOffsets[line]
|
|
696
|
+
};
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function getRelativePath(from, to) {
|
|
700
|
+
const fromParts = from.split(/[/\\]/);
|
|
701
|
+
const toParts = to.split(/[/\\]/);
|
|
702
|
+
fromParts.pop();
|
|
703
|
+
while (fromParts[0] === toParts[0]) {
|
|
704
|
+
fromParts.shift();
|
|
705
|
+
toParts.shift();
|
|
706
|
+
}
|
|
707
|
+
if (fromParts.length) {
|
|
708
|
+
let i = fromParts.length;
|
|
709
|
+
while (i--) fromParts[i] = "..";
|
|
710
|
+
}
|
|
711
|
+
return fromParts.concat(toParts).join("/");
|
|
712
|
+
}
|
|
713
|
+
function guessIndent(code) {
|
|
714
|
+
const lines = code.split("\n");
|
|
715
|
+
const tabbed = lines.filter((line) => /^\t+/.test(line));
|
|
716
|
+
const spaced = lines.filter((line) => /^ {2,}/.test(line));
|
|
717
|
+
if (tabbed.length === 0 && spaced.length === 0) return null;
|
|
718
|
+
if (tabbed.length >= spaced.length) return " ";
|
|
719
|
+
const min = spaced.reduce((previous, current) => {
|
|
720
|
+
const numSpaces = /^ +/.exec(current)[0].length;
|
|
721
|
+
return Math.min(numSpaces, previous);
|
|
722
|
+
}, Infinity);
|
|
723
|
+
return " ".repeat(min);
|
|
724
|
+
}
|
|
725
|
+
const toString = Object.prototype.toString;
|
|
726
|
+
function isObject(thing) {
|
|
727
|
+
return toString.call(thing) === "[object Object]";
|
|
728
|
+
}
|
|
729
|
+
const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
730
|
+
const intToChar = /* #__PURE__ */ (() => {
|
|
731
|
+
const chars = /* @__PURE__ */ new Uint8Array(64);
|
|
732
|
+
for (let i = 0; i < 64; i++) chars[i] = BASE64_CHARS.charCodeAt(i);
|
|
733
|
+
return chars;
|
|
734
|
+
})();
|
|
735
|
+
const COMMA = 44;
|
|
736
|
+
const SEMICOLON = 59;
|
|
737
|
+
const BUFFER_SIZE = 16384;
|
|
738
|
+
const FLUSH_THRESHOLD = 16348;
|
|
739
|
+
const scratch = /* #__PURE__ */ new Uint8Array(BUFFER_SIZE);
|
|
740
|
+
function writeVlq(pos, num) {
|
|
741
|
+
num = num < 0 ? -num << 1 | 1 : num << 1;
|
|
742
|
+
do {
|
|
743
|
+
let clamped = num & 31;
|
|
744
|
+
num >>>= 5;
|
|
745
|
+
if (num > 0) clamped |= 32;
|
|
746
|
+
scratch[pos++] = intToChar[clamped];
|
|
747
|
+
} while (num > 0);
|
|
748
|
+
return pos;
|
|
749
|
+
}
|
|
750
|
+
const decoder = /* #__PURE__ */ new TextDecoder();
|
|
751
|
+
var MappingsEncoder = class {
|
|
752
|
+
constructor() {
|
|
753
|
+
this.out = "";
|
|
754
|
+
this.pos = 0;
|
|
755
|
+
this.lines = [];
|
|
756
|
+
this.buffered = 0;
|
|
757
|
+
this.needsComma = false;
|
|
758
|
+
this.prevGenColumn = 0;
|
|
759
|
+
this.prevSourceIndex = 0;
|
|
760
|
+
this.prevSourceLine = 0;
|
|
761
|
+
this.prevSourceColumn = 0;
|
|
762
|
+
this.prevNameIndex = 0;
|
|
763
|
+
}
|
|
764
|
+
endLine(segments) {
|
|
765
|
+
this.lines.push(segments);
|
|
766
|
+
this.buffered += segments.length + 1;
|
|
767
|
+
if (this.buffered >= 4096) this.drain(null);
|
|
768
|
+
}
|
|
769
|
+
segments(segments) {
|
|
770
|
+
this.drain(segments);
|
|
771
|
+
}
|
|
772
|
+
finish(segments) {
|
|
773
|
+
this.drain(segments);
|
|
774
|
+
this.flush();
|
|
775
|
+
return this.out;
|
|
776
|
+
}
|
|
777
|
+
drain(trailing) {
|
|
778
|
+
const lines = this.lines;
|
|
779
|
+
const lineCount = lines.length;
|
|
780
|
+
for (let l = 0; l <= lineCount; l++) {
|
|
781
|
+
const line = l < lineCount ? lines[l] : trailing;
|
|
782
|
+
if (line === null) break;
|
|
783
|
+
for (let i = 0; i < line.length; i++) {
|
|
784
|
+
if (this.pos > FLUSH_THRESHOLD) this.flush();
|
|
785
|
+
const segment = line[i];
|
|
786
|
+
if (this.needsComma) scratch[this.pos++] = COMMA;
|
|
787
|
+
this.needsComma = true;
|
|
788
|
+
this.pos = writeVlq(this.pos, segment[0] - this.prevGenColumn);
|
|
789
|
+
this.prevGenColumn = segment[0];
|
|
790
|
+
this.pos = writeVlq(this.pos, segment[1] - this.prevSourceIndex);
|
|
791
|
+
this.prevSourceIndex = segment[1];
|
|
792
|
+
this.pos = writeVlq(this.pos, segment[2] - this.prevSourceLine);
|
|
793
|
+
this.prevSourceLine = segment[2];
|
|
794
|
+
this.pos = writeVlq(this.pos, segment[3] - this.prevSourceColumn);
|
|
795
|
+
this.prevSourceColumn = segment[3];
|
|
796
|
+
if (segment.length === 5) {
|
|
797
|
+
this.pos = writeVlq(this.pos, segment[4] - this.prevNameIndex);
|
|
798
|
+
this.prevNameIndex = segment[4];
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (l < lineCount) {
|
|
802
|
+
if (this.pos > FLUSH_THRESHOLD) this.flush();
|
|
803
|
+
scratch[this.pos++] = SEMICOLON;
|
|
804
|
+
this.needsComma = false;
|
|
805
|
+
this.prevGenColumn = 0;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
lines.length = 0;
|
|
809
|
+
this.buffered = 0;
|
|
810
|
+
this.flush();
|
|
811
|
+
}
|
|
812
|
+
flush() {
|
|
813
|
+
this.out += decoder.decode(scratch.subarray(0, this.pos));
|
|
814
|
+
this.pos = 0;
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
const NEWLINE_CHAR$1 = 10;
|
|
818
|
+
function isWordCode(code) {
|
|
819
|
+
return code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 95;
|
|
820
|
+
}
|
|
821
|
+
var Mappings = class {
|
|
822
|
+
constructor(hires, encoder = null) {
|
|
823
|
+
this.hires = hires;
|
|
824
|
+
this.generatedCodeLine = 0;
|
|
825
|
+
this.generatedCodeColumn = 0;
|
|
826
|
+
this.raw = [];
|
|
827
|
+
this.rawSegments = this.raw[this.generatedCodeLine] = [];
|
|
828
|
+
this.rawRangeMappings = [];
|
|
829
|
+
this.rawRangeMappingsIndices = this.rawRangeMappings[this.generatedCodeLine] = [];
|
|
830
|
+
this.encoder = encoder;
|
|
831
|
+
}
|
|
832
|
+
nextLine() {
|
|
833
|
+
if (this.encoder === null) {
|
|
834
|
+
this.generatedCodeLine += 1;
|
|
835
|
+
this.raw[this.generatedCodeLine] = this.rawSegments = [];
|
|
836
|
+
} else {
|
|
837
|
+
this.encoder.endLine(this.rawSegments);
|
|
838
|
+
this.rawSegments = [];
|
|
839
|
+
this.generatedCodeLine += 1;
|
|
840
|
+
}
|
|
841
|
+
this.generatedCodeColumn = 0;
|
|
842
|
+
this.rawRangeMappings[this.generatedCodeLine] = this.rawRangeMappingsIndices = [];
|
|
843
|
+
}
|
|
844
|
+
addEdit(sourceIndex, content, loc, nameIndex) {
|
|
845
|
+
if (content.length) {
|
|
846
|
+
const contentLengthMinusOne = content.length - 1;
|
|
847
|
+
let contentLineEnd = content.indexOf("\n", 0);
|
|
848
|
+
let previousContentLineEnd = -1;
|
|
849
|
+
while (contentLineEnd >= 0 && contentLengthMinusOne > contentLineEnd) {
|
|
850
|
+
const segment = [
|
|
851
|
+
this.generatedCodeColumn,
|
|
852
|
+
sourceIndex,
|
|
853
|
+
loc.line,
|
|
854
|
+
loc.column
|
|
855
|
+
];
|
|
856
|
+
if (nameIndex >= 0) segment.push(nameIndex);
|
|
857
|
+
this.rawSegments.push(segment);
|
|
858
|
+
this.nextLine();
|
|
859
|
+
previousContentLineEnd = contentLineEnd;
|
|
860
|
+
contentLineEnd = content.indexOf("\n", contentLineEnd + 1);
|
|
861
|
+
}
|
|
862
|
+
const segment = [
|
|
863
|
+
this.generatedCodeColumn,
|
|
864
|
+
sourceIndex,
|
|
865
|
+
loc.line,
|
|
866
|
+
loc.column
|
|
867
|
+
];
|
|
868
|
+
if (nameIndex >= 0) segment.push(nameIndex);
|
|
869
|
+
this.rawSegments.push(segment);
|
|
870
|
+
this.advance(content.slice(previousContentLineEnd + 1));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
addUneditedChunk(sourceIndex, chunk, original, loc, sourcemapLocations) {
|
|
874
|
+
const end = chunk.end;
|
|
875
|
+
let i = chunk.start;
|
|
876
|
+
if (this.hires) {
|
|
877
|
+
const boundary = this.hires === "boundary";
|
|
878
|
+
const experimentalRange = this.hires === "experimental-range";
|
|
879
|
+
const encoder = experimentalRange ? null : this.encoder;
|
|
880
|
+
let charInHiresBoundary = false;
|
|
881
|
+
while (i < end) {
|
|
882
|
+
if (encoder !== null && this.rawSegments.length >= 4096) {
|
|
883
|
+
encoder.segments(this.rawSegments);
|
|
884
|
+
this.rawSegments.length = 0;
|
|
885
|
+
}
|
|
886
|
+
if (experimentalRange && i + 1 >= end) this.rawSegments.push([
|
|
887
|
+
this.generatedCodeColumn,
|
|
888
|
+
sourceIndex,
|
|
889
|
+
loc.line,
|
|
890
|
+
loc.column
|
|
891
|
+
]);
|
|
892
|
+
const code = original.charCodeAt(i);
|
|
893
|
+
if (code === NEWLINE_CHAR$1) {
|
|
894
|
+
loc.line += 1;
|
|
895
|
+
loc.column = 0;
|
|
896
|
+
this.nextLine();
|
|
897
|
+
charInHiresBoundary = false;
|
|
898
|
+
} else {
|
|
899
|
+
if (boundary) {
|
|
900
|
+
if (isWordCode(code)) {
|
|
901
|
+
if (!charInHiresBoundary) {
|
|
902
|
+
this.rawSegments.push([
|
|
903
|
+
this.generatedCodeColumn,
|
|
904
|
+
sourceIndex,
|
|
905
|
+
loc.line,
|
|
906
|
+
loc.column
|
|
907
|
+
]);
|
|
908
|
+
charInHiresBoundary = true;
|
|
909
|
+
}
|
|
910
|
+
} else {
|
|
911
|
+
this.rawSegments.push([
|
|
912
|
+
this.generatedCodeColumn,
|
|
913
|
+
sourceIndex,
|
|
914
|
+
loc.line,
|
|
915
|
+
loc.column
|
|
916
|
+
]);
|
|
917
|
+
charInHiresBoundary = false;
|
|
918
|
+
}
|
|
919
|
+
} else if (experimentalRange) {
|
|
920
|
+
if (i === chunk.start) {
|
|
921
|
+
this.rawRangeMappingsIndices.push(this.rawSegments.length);
|
|
922
|
+
this.rawSegments.push([
|
|
923
|
+
this.generatedCodeColumn,
|
|
924
|
+
sourceIndex,
|
|
925
|
+
loc.line,
|
|
926
|
+
loc.column
|
|
927
|
+
]);
|
|
928
|
+
}
|
|
929
|
+
} else this.rawSegments.push([
|
|
930
|
+
this.generatedCodeColumn,
|
|
931
|
+
sourceIndex,
|
|
932
|
+
loc.line,
|
|
933
|
+
loc.column
|
|
934
|
+
]);
|
|
935
|
+
loc.column += 1;
|
|
936
|
+
this.generatedCodeColumn += 1;
|
|
937
|
+
}
|
|
938
|
+
i += 1;
|
|
939
|
+
}
|
|
940
|
+
} else {
|
|
941
|
+
const bits = sourcemapLocations.bits;
|
|
942
|
+
while (i < end) {
|
|
943
|
+
let newline = original.indexOf("\n", i);
|
|
944
|
+
if (newline === -1 || newline > end) newline = end;
|
|
945
|
+
if (newline > i) {
|
|
946
|
+
this.rawSegments.push([
|
|
947
|
+
this.generatedCodeColumn,
|
|
948
|
+
sourceIndex,
|
|
949
|
+
loc.line,
|
|
950
|
+
loc.column
|
|
951
|
+
]);
|
|
952
|
+
for (let w = i + 1 >> 5, last = newline - 1 >> 5; w <= last; w++) {
|
|
953
|
+
let word = bits[w];
|
|
954
|
+
if (!word) continue;
|
|
955
|
+
const base = w << 5;
|
|
956
|
+
while (word) {
|
|
957
|
+
const lowest = word & -word;
|
|
958
|
+
const index = base + 31 - Math.clz32(lowest);
|
|
959
|
+
if (index > i && index < newline) {
|
|
960
|
+
const offset = index - i;
|
|
961
|
+
this.rawSegments.push([
|
|
962
|
+
this.generatedCodeColumn + offset,
|
|
963
|
+
sourceIndex,
|
|
964
|
+
loc.line,
|
|
965
|
+
loc.column + offset
|
|
966
|
+
]);
|
|
967
|
+
}
|
|
968
|
+
word ^= lowest;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
loc.column += newline - i;
|
|
972
|
+
this.generatedCodeColumn += newline - i;
|
|
973
|
+
}
|
|
974
|
+
if (newline === end) break;
|
|
975
|
+
loc.line += 1;
|
|
976
|
+
loc.column = 0;
|
|
977
|
+
this.nextLine();
|
|
978
|
+
i = newline + 1;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
advance(str) {
|
|
983
|
+
if (!str) return;
|
|
984
|
+
const lastNewline = str.lastIndexOf("\n");
|
|
985
|
+
for (let i = str.indexOf("\n"); i !== -1; i = str.indexOf("\n", i + 1)) this.nextLine();
|
|
986
|
+
this.generatedCodeColumn += str.length - lastNewline - 1;
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
const n = "\n";
|
|
990
|
+
const NEWLINE_CHAR = "\n".charCodeAt(0);
|
|
991
|
+
const CR_CHAR = "\r".charCodeAt(0);
|
|
992
|
+
const warned = {
|
|
993
|
+
insertLeft: false,
|
|
994
|
+
insertRight: false,
|
|
995
|
+
storeName: false
|
|
996
|
+
};
|
|
997
|
+
/**
|
|
998
|
+
* Expands the `$` substitution patterns that `String.prototype.replace` accepts
|
|
999
|
+
* in a string replacement.
|
|
1000
|
+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_a_parameter
|
|
1001
|
+
*
|
|
1002
|
+
* `captures` holds the capture groups in order, so `captures[0]` is `$1`.
|
|
1003
|
+
* `namedCaptures` is `undefined` when the pattern has no named groups, and
|
|
1004
|
+
* `$<name>` is then literal text - which is also the case for a string search
|
|
1005
|
+
* value, where there are no groups of either kind.
|
|
1006
|
+
*/
|
|
1007
|
+
function expandReplacement(replacement, matched, position, str, captures, namedCaptures) {
|
|
1008
|
+
if (!replacement.includes("$")) return replacement;
|
|
1009
|
+
let result = "";
|
|
1010
|
+
let index = 0;
|
|
1011
|
+
while (index < replacement.length) {
|
|
1012
|
+
const dollar = replacement.indexOf("$", index);
|
|
1013
|
+
if (dollar === -1) {
|
|
1014
|
+
result += replacement.slice(index);
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
result += replacement.slice(index, dollar);
|
|
1018
|
+
const char = replacement[dollar + 1];
|
|
1019
|
+
let expansion = "$";
|
|
1020
|
+
let consumed = 1;
|
|
1021
|
+
if (char === "$") consumed = 2;
|
|
1022
|
+
else if (char === "&") {
|
|
1023
|
+
expansion = matched;
|
|
1024
|
+
consumed = 2;
|
|
1025
|
+
} else if (char === "`") {
|
|
1026
|
+
expansion = str.slice(0, position);
|
|
1027
|
+
consumed = 2;
|
|
1028
|
+
} else if (char === "'") {
|
|
1029
|
+
expansion = str.slice(position + matched.length);
|
|
1030
|
+
consumed = 2;
|
|
1031
|
+
} else if (char === "<" && namedCaptures !== void 0) {
|
|
1032
|
+
const close = replacement.indexOf(">", dollar + 2);
|
|
1033
|
+
if (close !== -1) {
|
|
1034
|
+
expansion = namedCaptures[replacement.slice(dollar + 2, close)] ?? "";
|
|
1035
|
+
consumed = close + 1 - dollar;
|
|
1036
|
+
}
|
|
1037
|
+
} else if (char >= "0" && char <= "9") {
|
|
1038
|
+
const second = replacement[dollar + 2];
|
|
1039
|
+
const double = second >= "0" && second <= "9" ? Number(char + second) : NaN;
|
|
1040
|
+
const single = Number(char);
|
|
1041
|
+
if (double >= 1 && double <= captures.length) {
|
|
1042
|
+
expansion = captures[double - 1] ?? "";
|
|
1043
|
+
consumed = 3;
|
|
1044
|
+
} else if (single >= 1 && single <= captures.length) {
|
|
1045
|
+
expansion = captures[single - 1] ?? "";
|
|
1046
|
+
consumed = 2;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
result += expansion;
|
|
1050
|
+
index = dollar + consumed;
|
|
1051
|
+
}
|
|
1052
|
+
return result;
|
|
1053
|
+
}
|
|
1054
|
+
var MagicString = class MagicString {
|
|
1055
|
+
constructor(string, options = {}) {
|
|
1056
|
+
const chunk = new Chunk(0, string.length, string);
|
|
1057
|
+
Object.defineProperties(this, {
|
|
1058
|
+
original: {
|
|
1059
|
+
writable: true,
|
|
1060
|
+
value: string
|
|
1061
|
+
},
|
|
1062
|
+
outro: {
|
|
1063
|
+
writable: true,
|
|
1064
|
+
value: ""
|
|
1065
|
+
},
|
|
1066
|
+
intro: {
|
|
1067
|
+
writable: true,
|
|
1068
|
+
value: ""
|
|
1069
|
+
},
|
|
1070
|
+
firstChunk: {
|
|
1071
|
+
writable: true,
|
|
1072
|
+
value: chunk
|
|
1073
|
+
},
|
|
1074
|
+
lastChunk: {
|
|
1075
|
+
writable: true,
|
|
1076
|
+
value: chunk
|
|
1077
|
+
},
|
|
1078
|
+
lastSearchedChunk: {
|
|
1079
|
+
writable: true,
|
|
1080
|
+
value: chunk
|
|
1081
|
+
},
|
|
1082
|
+
byStart: {
|
|
1083
|
+
writable: true,
|
|
1084
|
+
value: /* @__PURE__ */ new Map([[0, chunk]])
|
|
1085
|
+
},
|
|
1086
|
+
byEnd: {
|
|
1087
|
+
writable: true,
|
|
1088
|
+
value: /* @__PURE__ */ new Map([[string.length, chunk]])
|
|
1089
|
+
},
|
|
1090
|
+
filename: {
|
|
1091
|
+
writable: true,
|
|
1092
|
+
value: options.filename
|
|
1093
|
+
},
|
|
1094
|
+
indentExclusionRanges: {
|
|
1095
|
+
writable: true,
|
|
1096
|
+
value: options.indentExclusionRanges
|
|
1097
|
+
},
|
|
1098
|
+
sourcemapLocations: {
|
|
1099
|
+
writable: true,
|
|
1100
|
+
value: new BitSet()
|
|
1101
|
+
},
|
|
1102
|
+
storedNames: {
|
|
1103
|
+
writable: true,
|
|
1104
|
+
value: {}
|
|
1105
|
+
},
|
|
1106
|
+
indentStr: {
|
|
1107
|
+
writable: true,
|
|
1108
|
+
value: void 0
|
|
1109
|
+
},
|
|
1110
|
+
ignoreList: {
|
|
1111
|
+
writable: true,
|
|
1112
|
+
value: options.ignoreList
|
|
1113
|
+
},
|
|
1114
|
+
offset: {
|
|
1115
|
+
writable: true,
|
|
1116
|
+
value: options.offset || 0
|
|
1117
|
+
},
|
|
1118
|
+
hasMovedChunks: {
|
|
1119
|
+
writable: true,
|
|
1120
|
+
value: false
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Adds the specified character index (with respect to the original string) to sourcemap mappings, if `hires` is false.
|
|
1126
|
+
*/
|
|
1127
|
+
addSourcemapLocation(char) {
|
|
1128
|
+
this.sourcemapLocations.add(char);
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Appends the specified content to the end of the string.
|
|
1132
|
+
*/
|
|
1133
|
+
append(content) {
|
|
1134
|
+
if (typeof content !== "string") throw new MagicStringError(`content must be a string, got ${typeof content}`);
|
|
1135
|
+
this.outro += content;
|
|
1136
|
+
return this;
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Appends the specified content at the index in the original string.
|
|
1140
|
+
* If a range *ending* with index is subsequently moved, the insert will be moved with it.
|
|
1141
|
+
* See also `s.prependLeft(...)`.
|
|
1142
|
+
*/
|
|
1143
|
+
appendLeft(index, content) {
|
|
1144
|
+
index = index + this.offset;
|
|
1145
|
+
if (typeof content !== "string") throw new MagicStringError(`content must be a string, got ${typeof content}`);
|
|
1146
|
+
this._split(index);
|
|
1147
|
+
const chunk = this.byEnd.get(index);
|
|
1148
|
+
if (chunk) chunk.appendLeft(content);
|
|
1149
|
+
else this.intro += content;
|
|
1150
|
+
return this;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Appends the specified content at the index in the original string.
|
|
1154
|
+
* If a range *starting* with index is subsequently moved, the insert will be moved with it.
|
|
1155
|
+
* See also `s.prependRight(...)`.
|
|
1156
|
+
*/
|
|
1157
|
+
appendRight(index, content) {
|
|
1158
|
+
index = index + this.offset;
|
|
1159
|
+
if (typeof content !== "string") throw new MagicStringError(`content must be a string, got ${typeof content}`);
|
|
1160
|
+
this._split(index);
|
|
1161
|
+
const chunk = this.byStart.get(index);
|
|
1162
|
+
if (chunk) chunk.appendRight(content);
|
|
1163
|
+
else this.outro += content;
|
|
1164
|
+
return this;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Does what you'd expect.
|
|
1168
|
+
*/
|
|
1169
|
+
clone() {
|
|
1170
|
+
const cloned = new MagicString(this.original, {
|
|
1171
|
+
filename: this.filename,
|
|
1172
|
+
ignoreList: this.ignoreList,
|
|
1173
|
+
offset: this.offset
|
|
1174
|
+
});
|
|
1175
|
+
let originalChunk = this.firstChunk;
|
|
1176
|
+
let clonedChunk = cloned.firstChunk = cloned.lastSearchedChunk = originalChunk.clone();
|
|
1177
|
+
while (originalChunk) {
|
|
1178
|
+
cloned.byStart.set(clonedChunk.start, clonedChunk);
|
|
1179
|
+
cloned.byEnd.set(clonedChunk.end, clonedChunk);
|
|
1180
|
+
const nextOriginalChunk = originalChunk.next;
|
|
1181
|
+
const nextClonedChunk = nextOriginalChunk && nextOriginalChunk.clone();
|
|
1182
|
+
if (nextClonedChunk) {
|
|
1183
|
+
clonedChunk.next = nextClonedChunk;
|
|
1184
|
+
nextClonedChunk.previous = clonedChunk;
|
|
1185
|
+
clonedChunk = nextClonedChunk;
|
|
1186
|
+
}
|
|
1187
|
+
originalChunk = nextOriginalChunk;
|
|
1188
|
+
}
|
|
1189
|
+
cloned.lastChunk = clonedChunk;
|
|
1190
|
+
if (this.indentExclusionRanges) cloned.indentExclusionRanges = this.indentExclusionRanges.slice();
|
|
1191
|
+
cloned.sourcemapLocations = new BitSet(this.sourcemapLocations);
|
|
1192
|
+
cloned.storedNames = { ...this.storedNames };
|
|
1193
|
+
cloned.intro = this.intro;
|
|
1194
|
+
cloned.outro = this.outro;
|
|
1195
|
+
cloned.hasMovedChunks = this.hasMovedChunks;
|
|
1196
|
+
return cloned;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Generates a sourcemap object with raw mappings in array form, rather than encoded as a string.
|
|
1200
|
+
* Useful if you need to manipulate the sourcemap further, but most of the time you will use `generateMap` instead.
|
|
1201
|
+
*/
|
|
1202
|
+
generateDecodedMap(options) {
|
|
1203
|
+
options = options || {};
|
|
1204
|
+
const mappings = new Mappings(options.hires);
|
|
1205
|
+
const names = this._generateMappings(mappings);
|
|
1206
|
+
return {
|
|
1207
|
+
...this._mapProperties(options, names),
|
|
1208
|
+
mappings: mappings.raw,
|
|
1209
|
+
rangeMappings: mappings.rawRangeMappings
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Generates a version 3 sourcemap.
|
|
1214
|
+
*/
|
|
1215
|
+
generateMap(options) {
|
|
1216
|
+
options = options || {};
|
|
1217
|
+
const encoder = new MappingsEncoder();
|
|
1218
|
+
const mappings = new Mappings(options.hires, encoder);
|
|
1219
|
+
const names = this._generateMappings(mappings);
|
|
1220
|
+
return new SourceMap({
|
|
1221
|
+
...this._mapProperties(options, names),
|
|
1222
|
+
mappings: encoder.finish(mappings.rawSegments),
|
|
1223
|
+
rangeMappings: mappings.rawRangeMappings
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
/** @internal */
|
|
1227
|
+
_generateMappings(mappings) {
|
|
1228
|
+
const sourceIndex = 0;
|
|
1229
|
+
const names = Object.keys(this.storedNames);
|
|
1230
|
+
const locate = getLocator(this.original);
|
|
1231
|
+
if (this.intro) mappings.advance(this.intro);
|
|
1232
|
+
this.firstChunk.eachNext((chunk) => {
|
|
1233
|
+
const loc = locate(chunk.start);
|
|
1234
|
+
if (chunk.intro.length) mappings.advance(chunk.intro);
|
|
1235
|
+
if (chunk.edited) mappings.addEdit(sourceIndex, chunk.content, loc, chunk.storeName ? names.indexOf(chunk.original) : -1);
|
|
1236
|
+
else mappings.addUneditedChunk(sourceIndex, chunk, this.original, loc, this.sourcemapLocations);
|
|
1237
|
+
if (chunk.outro.length) mappings.advance(chunk.outro);
|
|
1238
|
+
});
|
|
1239
|
+
if (this.outro) mappings.advance(this.outro);
|
|
1240
|
+
return names;
|
|
1241
|
+
}
|
|
1242
|
+
/** @internal */
|
|
1243
|
+
_mapProperties(options, names) {
|
|
1244
|
+
return {
|
|
1245
|
+
file: options.file ? options.file.split(/[/\\]/).pop() : void 0,
|
|
1246
|
+
sources: [options.source ? getRelativePath(options.file || "", options.source) : options.file || ""],
|
|
1247
|
+
sourcesContent: options.includeContent ? [this.original] : void 0,
|
|
1248
|
+
names,
|
|
1249
|
+
x_google_ignoreList: this.ignoreList ? [0] : void 0
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
/** @internal */
|
|
1253
|
+
_ensureindentStr() {
|
|
1254
|
+
if (this.indentStr === void 0) this.indentStr = guessIndent(this.original);
|
|
1255
|
+
}
|
|
1256
|
+
/** @internal */
|
|
1257
|
+
_getRawIndentString() {
|
|
1258
|
+
this._ensureindentStr();
|
|
1259
|
+
return this.indentStr;
|
|
1260
|
+
}
|
|
1261
|
+
getIndentString() {
|
|
1262
|
+
this._ensureindentStr();
|
|
1263
|
+
return this.indentStr === null ? " " : this.indentStr;
|
|
1264
|
+
}
|
|
1265
|
+
indent(indentStr, options) {
|
|
1266
|
+
const pattern = /^[^\r\n]/gm;
|
|
1267
|
+
if (isObject(indentStr)) {
|
|
1268
|
+
options = indentStr;
|
|
1269
|
+
indentStr = void 0;
|
|
1270
|
+
}
|
|
1271
|
+
if (indentStr === void 0) {
|
|
1272
|
+
this._ensureindentStr();
|
|
1273
|
+
indentStr = this.indentStr || " ";
|
|
1274
|
+
}
|
|
1275
|
+
if (indentStr === "") return this;
|
|
1276
|
+
const resolvedIndentStr = indentStr;
|
|
1277
|
+
options = options || {};
|
|
1278
|
+
const isExcluded = {};
|
|
1279
|
+
if (options.exclude) (typeof options.exclude[0] === "number" ? [options.exclude] : options.exclude).forEach((exclusion) => {
|
|
1280
|
+
for (let i = exclusion[0]; i < exclusion[1]; i += 1) isExcluded[i] = true;
|
|
1281
|
+
});
|
|
1282
|
+
let shouldIndentNextCharacter = options.indentStart !== false;
|
|
1283
|
+
const indentPiece = (str) => {
|
|
1284
|
+
if (str === "") return str;
|
|
1285
|
+
const indented = str.replace(pattern, (match, offset) => offset > 0 || shouldIndentNextCharacter ? `${resolvedIndentStr}${match}` : match);
|
|
1286
|
+
shouldIndentNextCharacter = str[str.length - 1] === "\n";
|
|
1287
|
+
return indented;
|
|
1288
|
+
};
|
|
1289
|
+
this.intro = indentPiece(this.intro);
|
|
1290
|
+
let charIndex = 0;
|
|
1291
|
+
let chunk = this.firstChunk;
|
|
1292
|
+
const indentAt = (index) => {
|
|
1293
|
+
shouldIndentNextCharacter = false;
|
|
1294
|
+
if (index === chunk.start) chunk.appendRight(resolvedIndentStr);
|
|
1295
|
+
else {
|
|
1296
|
+
this._splitChunk(chunk, index);
|
|
1297
|
+
chunk = chunk.next;
|
|
1298
|
+
chunk.prependRight(resolvedIndentStr);
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
while (chunk) {
|
|
1302
|
+
const end = chunk.end;
|
|
1303
|
+
if (!isExcluded[chunk.start]) chunk.intro = indentPiece(chunk.intro);
|
|
1304
|
+
if (chunk.edited) {
|
|
1305
|
+
if (!isExcluded[charIndex]) chunk.content = indentPiece(chunk.content);
|
|
1306
|
+
} else if (options.exclude) {
|
|
1307
|
+
charIndex = chunk.start;
|
|
1308
|
+
while (charIndex < end) {
|
|
1309
|
+
if (!isExcluded[charIndex]) {
|
|
1310
|
+
const char = this.original.charCodeAt(charIndex);
|
|
1311
|
+
if (char === NEWLINE_CHAR) shouldIndentNextCharacter = true;
|
|
1312
|
+
else if (char !== CR_CHAR && shouldIndentNextCharacter) indentAt(charIndex);
|
|
1313
|
+
}
|
|
1314
|
+
charIndex += 1;
|
|
1315
|
+
}
|
|
1316
|
+
} else {
|
|
1317
|
+
charIndex = chunk.start;
|
|
1318
|
+
while (charIndex < end) {
|
|
1319
|
+
if (!shouldIndentNextCharacter) {
|
|
1320
|
+
const nextLine = this.original.indexOf(n, charIndex);
|
|
1321
|
+
if (nextLine === -1 || nextLine >= end) break;
|
|
1322
|
+
shouldIndentNextCharacter = true;
|
|
1323
|
+
charIndex = nextLine + 1;
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
const char = this.original.charCodeAt(charIndex);
|
|
1327
|
+
if (char === NEWLINE_CHAR || char === CR_CHAR) {
|
|
1328
|
+
charIndex += 1;
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
indentAt(charIndex);
|
|
1332
|
+
charIndex += 1;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (!isExcluded[chunk.end - 1]) chunk.outro = indentPiece(chunk.outro);
|
|
1336
|
+
charIndex = chunk.end;
|
|
1337
|
+
chunk = chunk.next;
|
|
1338
|
+
}
|
|
1339
|
+
this.outro = indentPiece(this.outro);
|
|
1340
|
+
return this;
|
|
1341
|
+
}
|
|
1342
|
+
/** @internal */
|
|
1343
|
+
insert() {
|
|
1344
|
+
throw new MagicStringError("insert() is deprecated, use appendLeft() or prependRight()");
|
|
1345
|
+
}
|
|
1346
|
+
/** @internal */
|
|
1347
|
+
insertLeft(index, content) {
|
|
1348
|
+
if (!warned.insertLeft) {
|
|
1349
|
+
console.warn("magicString.insertLeft(...) is deprecated. Use magicString.appendLeft(...) instead");
|
|
1350
|
+
warned.insertLeft = true;
|
|
1351
|
+
}
|
|
1352
|
+
return this.appendLeft(index, content);
|
|
1353
|
+
}
|
|
1354
|
+
/** @internal */
|
|
1355
|
+
insertRight(index, content) {
|
|
1356
|
+
if (!warned.insertRight) {
|
|
1357
|
+
console.warn("magicString.insertRight(...) is deprecated. Use magicString.prependRight(...) instead");
|
|
1358
|
+
warned.insertRight = true;
|
|
1359
|
+
}
|
|
1360
|
+
return this.prependRight(index, content);
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Moves the characters from `start` and `end` to `index`.
|
|
1364
|
+
*
|
|
1365
|
+
* `affinity` controls where the range is anchored at `index`. With the
|
|
1366
|
+
* default `'right'`, it is inserted before the content that starts at `index`;
|
|
1367
|
+
* with `'left'`, it is inserted after the content that ends at `index`. The
|
|
1368
|
+
* two differ only when other content has already been moved to that boundary,
|
|
1369
|
+
* mirroring the `appendLeft`/`appendRight` distinction.
|
|
1370
|
+
*/
|
|
1371
|
+
move(start, end, index, affinity = "right") {
|
|
1372
|
+
start = start + this.offset;
|
|
1373
|
+
end = end + this.offset;
|
|
1374
|
+
index = index + this.offset;
|
|
1375
|
+
if (start === end) return this;
|
|
1376
|
+
if (index >= start && index <= end) throw new MagicStringError("cannot move a selection inside itself");
|
|
1377
|
+
this._split(start);
|
|
1378
|
+
this._split(end);
|
|
1379
|
+
this._split(index);
|
|
1380
|
+
const first = this.byStart.get(start);
|
|
1381
|
+
const last = this.byEnd.get(end);
|
|
1382
|
+
if (this.hasMovedChunks) {
|
|
1383
|
+
let cursor = first;
|
|
1384
|
+
while (cursor !== last) {
|
|
1385
|
+
cursor = cursor.next;
|
|
1386
|
+
if (!cursor || cursor.start < start || cursor.end > end) throw new MagicStringError(`cannot move ${start} to ${end} because an earlier move split that range`);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
const oldLeft = first.previous;
|
|
1390
|
+
const oldRight = last.next;
|
|
1391
|
+
let newLeft;
|
|
1392
|
+
let newRight;
|
|
1393
|
+
if (affinity === "left") {
|
|
1394
|
+
newLeft = this.byEnd.get(index) ?? null;
|
|
1395
|
+
if (!newLeft) {
|
|
1396
|
+
if (first === this.firstChunk) return this;
|
|
1397
|
+
newRight = this.firstChunk;
|
|
1398
|
+
} else {
|
|
1399
|
+
if (newLeft.next === first) return this;
|
|
1400
|
+
newRight = newLeft.next;
|
|
1401
|
+
}
|
|
1402
|
+
} else {
|
|
1403
|
+
newRight = this.byStart.get(index) ?? null;
|
|
1404
|
+
if (!newRight) {
|
|
1405
|
+
if (last === this.lastChunk) return this;
|
|
1406
|
+
newLeft = this.lastChunk;
|
|
1407
|
+
} else {
|
|
1408
|
+
if (newRight.previous === last) return this;
|
|
1409
|
+
newLeft = newRight.previous;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (oldLeft) oldLeft.next = oldRight;
|
|
1413
|
+
if (oldRight) oldRight.previous = oldLeft;
|
|
1414
|
+
if (newLeft) newLeft.next = first;
|
|
1415
|
+
if (newRight) newRight.previous = last;
|
|
1416
|
+
if (!first.previous) this.firstChunk = last.next;
|
|
1417
|
+
if (!last.next) {
|
|
1418
|
+
this.lastChunk = first.previous;
|
|
1419
|
+
this.lastChunk.next = null;
|
|
1420
|
+
}
|
|
1421
|
+
first.previous = newLeft;
|
|
1422
|
+
last.next = newRight || null;
|
|
1423
|
+
if (!newLeft) this.firstChunk = first;
|
|
1424
|
+
if (!newRight) this.lastChunk = last;
|
|
1425
|
+
this.hasMovedChunks = true;
|
|
1426
|
+
return this;
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Replaces the characters from `start` to `end` with `content`, along with the appended/prepended content in
|
|
1430
|
+
* that range. The same restrictions as `s.remove()` apply.
|
|
1431
|
+
*
|
|
1432
|
+
* The fourth argument is optional. It can have a storeName property - if true, the original name will be stored
|
|
1433
|
+
* for later inclusion in a sourcemap's names array - and a contentOnly property which determines whether only
|
|
1434
|
+
* the content is overwritten, or anything that was appended/prepended to the range as well.
|
|
1435
|
+
*
|
|
1436
|
+
* It may be preferred to use `s.update(...)` instead if you wish to avoid overwriting the appended/prepended content.
|
|
1437
|
+
*/
|
|
1438
|
+
overwrite(start, end, content, options) {
|
|
1439
|
+
const optionObject = typeof options === "object" && options ? options : {};
|
|
1440
|
+
return this.update(start, end, content, {
|
|
1441
|
+
...optionObject,
|
|
1442
|
+
overwrite: !optionObject.contentOnly
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Replaces the characters from `start` to `end` with `content`. The same restrictions as `s.remove()` apply.
|
|
1447
|
+
*
|
|
1448
|
+
* The fourth argument is optional. It can have a storeName property - if true, the original name will be stored
|
|
1449
|
+
* for later inclusion in a sourcemap's names array - and an overwrite property which determines whether only
|
|
1450
|
+
* the content is overwritten, or anything that was appended/prepended to the range as well.
|
|
1451
|
+
*/
|
|
1452
|
+
update(start, end, content, options) {
|
|
1453
|
+
start = start + this.offset;
|
|
1454
|
+
end = end + this.offset;
|
|
1455
|
+
if (typeof content !== "string") throw new MagicStringError(`content must be a string, got ${typeof content}`);
|
|
1456
|
+
if (this.original.length !== 0) {
|
|
1457
|
+
if (start < 0) start = Math.max(0, start + this.original.length);
|
|
1458
|
+
if (end < 0) end = Math.max(0, end + this.original.length);
|
|
1459
|
+
}
|
|
1460
|
+
if (start < 0) throw new MagicStringError(`start ${start} is out of bounds`);
|
|
1461
|
+
if (end > this.original.length) throw new MagicStringError(`end ${end} is out of bounds`);
|
|
1462
|
+
if (start === end) throw new MagicStringError(`cannot overwrite a zero-length range at ${start}, use appendLeft() or prependRight()`);
|
|
1463
|
+
if (start > end) throw new MagicStringError(`end must be greater than start (start: ${start}, end: ${end})`);
|
|
1464
|
+
this._split(start);
|
|
1465
|
+
this._split(end);
|
|
1466
|
+
if (options === true) {
|
|
1467
|
+
if (!warned.storeName) {
|
|
1468
|
+
console.warn("The final argument to magicString.overwrite(...) should be an options object. See https://github.com/rich-harris/magic-string");
|
|
1469
|
+
warned.storeName = true;
|
|
1470
|
+
}
|
|
1471
|
+
options = { storeName: true };
|
|
1472
|
+
}
|
|
1473
|
+
const optionObject = typeof options === "object" && options ? options : {};
|
|
1474
|
+
const storeName = optionObject.storeName || false;
|
|
1475
|
+
const overwrite = optionObject.overwrite || false;
|
|
1476
|
+
if (storeName) {
|
|
1477
|
+
const original = this.original.slice(start, end);
|
|
1478
|
+
Object.defineProperty(this.storedNames, original, {
|
|
1479
|
+
writable: true,
|
|
1480
|
+
value: true,
|
|
1481
|
+
enumerable: true
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
const first = this.byStart.get(start);
|
|
1485
|
+
const last = this.byEnd.get(end);
|
|
1486
|
+
/* v8 ignore else -- unreachable: a valid start/end always yields a `first` chunk */
|
|
1487
|
+
if (first) {
|
|
1488
|
+
let chunk = first;
|
|
1489
|
+
while (chunk !== last) {
|
|
1490
|
+
if (chunk.next !== this.byStart.get(chunk.end)) throw new MagicStringError("cannot overwrite across a split point");
|
|
1491
|
+
chunk = chunk.next;
|
|
1492
|
+
chunk.edit("", false, !overwrite);
|
|
1493
|
+
}
|
|
1494
|
+
first.edit(content, storeName, !overwrite);
|
|
1495
|
+
} else {
|
|
1496
|
+
const newChunk = new Chunk(start, end, "").edit(content, storeName);
|
|
1497
|
+
last.next = newChunk;
|
|
1498
|
+
newChunk.previous = last;
|
|
1499
|
+
}
|
|
1500
|
+
return this;
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Prepends the string with the specified content.
|
|
1504
|
+
*/
|
|
1505
|
+
prepend(content) {
|
|
1506
|
+
if (typeof content !== "string") throw new MagicStringError(`content must be a string, got ${typeof content}`);
|
|
1507
|
+
this.intro = content + this.intro;
|
|
1508
|
+
return this;
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Same as `s.appendLeft(...)`, except that the inserted content will go *before* any previous appends or prepends at index
|
|
1512
|
+
*/
|
|
1513
|
+
prependLeft(index, content) {
|
|
1514
|
+
index = index + this.offset;
|
|
1515
|
+
if (typeof content !== "string") throw new MagicStringError(`content must be a string, got ${typeof content}`);
|
|
1516
|
+
this._split(index);
|
|
1517
|
+
const chunk = this.byEnd.get(index);
|
|
1518
|
+
if (chunk) chunk.prependLeft(content);
|
|
1519
|
+
else this.intro = content + this.intro;
|
|
1520
|
+
return this;
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Same as `s.appendRight(...)`, except that the inserted content will go *before* any previous appends or prepends at `index`
|
|
1524
|
+
*/
|
|
1525
|
+
prependRight(index, content) {
|
|
1526
|
+
index = index + this.offset;
|
|
1527
|
+
if (typeof content !== "string") throw new MagicStringError(`content must be a string, got ${typeof content}`);
|
|
1528
|
+
this._split(index);
|
|
1529
|
+
const chunk = this.byStart.get(index);
|
|
1530
|
+
if (chunk) chunk.prependRight(content);
|
|
1531
|
+
else this.outro = content + this.outro;
|
|
1532
|
+
return this;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Removes the characters from `start` to `end` (of the original string, **not** the generated string).
|
|
1536
|
+
* Content appended or prepended at positions strictly inside the range is removed with it, while
|
|
1537
|
+
* content attached at `start` or `end` is preserved — use `s.overwrite(start, end, '')` to remove
|
|
1538
|
+
* the range including its edge inserts.
|
|
1539
|
+
* Repeated removals and removals with partially overlapping ranges are allowed.
|
|
1540
|
+
*/
|
|
1541
|
+
remove(start, end) {
|
|
1542
|
+
start = start + this.offset;
|
|
1543
|
+
end = end + this.offset;
|
|
1544
|
+
if (this.original.length !== 0) {
|
|
1545
|
+
if (start < 0) start = Math.max(0, start + this.original.length);
|
|
1546
|
+
if (end < 0) end = Math.max(0, end + this.original.length);
|
|
1547
|
+
}
|
|
1548
|
+
if (start === end) return this;
|
|
1549
|
+
if (start < 0 || end > this.original.length) throw new MagicStringError(`range ${start}–${end} is out of bounds`);
|
|
1550
|
+
if (start > end) throw new MagicStringError(`end must be greater than start (start: ${start}, end: ${end})`);
|
|
1551
|
+
this._split(start);
|
|
1552
|
+
this._split(end);
|
|
1553
|
+
let chunk = this.byStart.get(start);
|
|
1554
|
+
while (chunk) {
|
|
1555
|
+
if (chunk.start > start) chunk.intro = "";
|
|
1556
|
+
if (chunk.end < end) chunk.outro = "";
|
|
1557
|
+
chunk.edit("", false, true);
|
|
1558
|
+
chunk = end > chunk.end ? this.byStart.get(chunk.end) : null;
|
|
1559
|
+
}
|
|
1560
|
+
return this;
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Reset the modified characters from `start` to `end` (of the original string, **not** the generated string).
|
|
1564
|
+
*/
|
|
1565
|
+
reset(start, end) {
|
|
1566
|
+
start = start + this.offset;
|
|
1567
|
+
end = end + this.offset;
|
|
1568
|
+
if (this.original.length !== 0) {
|
|
1569
|
+
if (start < 0) start = Math.max(0, start + this.original.length);
|
|
1570
|
+
if (end < 0) end = Math.max(0, end + this.original.length);
|
|
1571
|
+
}
|
|
1572
|
+
if (start === end) return this;
|
|
1573
|
+
if (start < 0 || end > this.original.length) throw new MagicStringError(`range ${start}–${end} is out of bounds`);
|
|
1574
|
+
if (start > end) throw new MagicStringError(`end must be greater than start (start: ${start}, end: ${end})`);
|
|
1575
|
+
this._split(start);
|
|
1576
|
+
this._split(end);
|
|
1577
|
+
let chunk = this.byStart.get(start);
|
|
1578
|
+
while (chunk) {
|
|
1579
|
+
chunk.reset();
|
|
1580
|
+
chunk = end > chunk.end ? this.byStart.get(chunk.end) : null;
|
|
1581
|
+
}
|
|
1582
|
+
return this;
|
|
1583
|
+
}
|
|
1584
|
+
lastChar() {
|
|
1585
|
+
if (this.outro.length) return this.outro[this.outro.length - 1];
|
|
1586
|
+
let chunk = this.lastChunk;
|
|
1587
|
+
while (chunk) {
|
|
1588
|
+
if (chunk.outro.length) return chunk.outro[chunk.outro.length - 1];
|
|
1589
|
+
if (chunk.content.length) return chunk.content[chunk.content.length - 1];
|
|
1590
|
+
if (chunk.intro.length) return chunk.intro[chunk.intro.length - 1];
|
|
1591
|
+
chunk = chunk.previous;
|
|
1592
|
+
}
|
|
1593
|
+
if (this.intro.length) return this.intro[this.intro.length - 1];
|
|
1594
|
+
return "";
|
|
1595
|
+
}
|
|
1596
|
+
lastLine() {
|
|
1597
|
+
let lineIndex = this.outro.lastIndexOf(n);
|
|
1598
|
+
if (lineIndex !== -1) return this.outro.substr(lineIndex + 1);
|
|
1599
|
+
let lineStr = this.outro;
|
|
1600
|
+
let chunk = this.lastChunk;
|
|
1601
|
+
while (chunk) {
|
|
1602
|
+
if (chunk.outro.length > 0) {
|
|
1603
|
+
lineIndex = chunk.outro.lastIndexOf(n);
|
|
1604
|
+
if (lineIndex !== -1) return chunk.outro.substr(lineIndex + 1) + lineStr;
|
|
1605
|
+
lineStr = chunk.outro + lineStr;
|
|
1606
|
+
}
|
|
1607
|
+
if (chunk.content.length > 0) {
|
|
1608
|
+
lineIndex = chunk.content.lastIndexOf(n);
|
|
1609
|
+
if (lineIndex !== -1) return chunk.content.substr(lineIndex + 1) + lineStr;
|
|
1610
|
+
lineStr = chunk.content + lineStr;
|
|
1611
|
+
}
|
|
1612
|
+
if (chunk.intro.length > 0) {
|
|
1613
|
+
lineIndex = chunk.intro.lastIndexOf(n);
|
|
1614
|
+
if (lineIndex !== -1) return chunk.intro.substr(lineIndex + 1) + lineStr;
|
|
1615
|
+
lineStr = chunk.intro + lineStr;
|
|
1616
|
+
}
|
|
1617
|
+
chunk = chunk.previous;
|
|
1618
|
+
}
|
|
1619
|
+
lineIndex = this.intro.lastIndexOf(n);
|
|
1620
|
+
if (lineIndex !== -1) return this.intro.substr(lineIndex + 1) + lineStr;
|
|
1621
|
+
return this.intro + lineStr;
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Returns the content of the generated string that corresponds to the slice between `start` and `end` of the original string.
|
|
1625
|
+
* Throws error if the indices are for characters that were already removed.
|
|
1626
|
+
*/
|
|
1627
|
+
slice(start = 0, end = this.original.length - this.offset) {
|
|
1628
|
+
start = start + this.offset;
|
|
1629
|
+
end = end + this.offset;
|
|
1630
|
+
if (this.original.length !== 0) {
|
|
1631
|
+
if (start < 0) start = Math.max(0, start + this.original.length);
|
|
1632
|
+
if (end < 0) end = Math.max(0, end + this.original.length);
|
|
1633
|
+
}
|
|
1634
|
+
let result = "";
|
|
1635
|
+
let chunk = this.firstChunk;
|
|
1636
|
+
while (chunk && (chunk.start > start || chunk.end <= start)) {
|
|
1637
|
+
if (chunk.start < end && chunk.end >= end || end === 0 && chunk.start === 0) return result;
|
|
1638
|
+
chunk = chunk.next;
|
|
1639
|
+
}
|
|
1640
|
+
if (chunk && chunk.edited && chunk.start !== start) throw new MagicStringError(`cannot use edited character ${start} as slice start anchor`);
|
|
1641
|
+
const startChunk = chunk;
|
|
1642
|
+
while (chunk) {
|
|
1643
|
+
if (end === 0 && chunk.start === 0) break;
|
|
1644
|
+
if (chunk.intro && (startChunk !== chunk || chunk.start === start)) result += chunk.intro;
|
|
1645
|
+
const containsEnd = chunk.start < end && chunk.end >= end;
|
|
1646
|
+
if (containsEnd && chunk.edited && chunk.end !== end) throw new MagicStringError(`cannot use edited character ${end} as slice end anchor`);
|
|
1647
|
+
const sliceStart = startChunk === chunk ? start - chunk.start : 0;
|
|
1648
|
+
const sliceEnd = containsEnd ? chunk.content.length + end - chunk.end : chunk.content.length;
|
|
1649
|
+
result += chunk.content.slice(sliceStart, sliceEnd);
|
|
1650
|
+
if (chunk.outro && (!containsEnd || chunk.end === end)) result += chunk.outro;
|
|
1651
|
+
if (containsEnd) break;
|
|
1652
|
+
chunk = chunk.next;
|
|
1653
|
+
}
|
|
1654
|
+
return result;
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Returns a clone of `s`, with all content before the `start` and `end` characters of the original string removed.
|
|
1658
|
+
*/
|
|
1659
|
+
snip(start, end) {
|
|
1660
|
+
const clone = this.clone();
|
|
1661
|
+
clone.remove(0, start);
|
|
1662
|
+
clone.remove(end, clone.original.length);
|
|
1663
|
+
return clone;
|
|
1664
|
+
}
|
|
1665
|
+
/** @internal */
|
|
1666
|
+
_split(index) {
|
|
1667
|
+
if (this.byStart.get(index) || this.byEnd.get(index)) return;
|
|
1668
|
+
let chunk = this.lastSearchedChunk;
|
|
1669
|
+
let previousChunk = chunk;
|
|
1670
|
+
const searchForward = index > chunk.end;
|
|
1671
|
+
while (chunk) {
|
|
1672
|
+
if (chunk.contains(index)) return this._splitChunk(chunk, index);
|
|
1673
|
+
chunk = searchForward ? this.byStart.get(chunk.end) : this.byEnd.get(chunk.start);
|
|
1674
|
+
if (chunk === previousChunk) return;
|
|
1675
|
+
previousChunk = chunk;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
/** @internal */
|
|
1679
|
+
_splitChunk(chunk, index) {
|
|
1680
|
+
if (chunk.edited && chunk.content.length) {
|
|
1681
|
+
const loc = getLocator(this.original)(index);
|
|
1682
|
+
throw new MagicStringError(`cannot split a chunk that has already been edited (${loc.line}:${loc.column} – "${chunk.original}")`);
|
|
1683
|
+
}
|
|
1684
|
+
const newChunk = chunk.split(index);
|
|
1685
|
+
this.byEnd.set(index, chunk);
|
|
1686
|
+
this.byStart.set(index, newChunk);
|
|
1687
|
+
this.byEnd.set(newChunk.end, newChunk);
|
|
1688
|
+
if (chunk === this.lastChunk) this.lastChunk = newChunk;
|
|
1689
|
+
this.lastSearchedChunk = chunk;
|
|
1690
|
+
return true;
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Returns the generated string.
|
|
1694
|
+
*/
|
|
1695
|
+
toString() {
|
|
1696
|
+
let str = this.intro;
|
|
1697
|
+
let chunk = this.firstChunk;
|
|
1698
|
+
while (chunk) {
|
|
1699
|
+
str += chunk.toString();
|
|
1700
|
+
chunk = chunk.next;
|
|
1701
|
+
}
|
|
1702
|
+
return str + this.outro;
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Returns true if the resulting source is empty (disregarding white space).
|
|
1706
|
+
*/
|
|
1707
|
+
isEmpty() {
|
|
1708
|
+
if (this.intro.length && this.intro.trim()) return false;
|
|
1709
|
+
let chunk = this.firstChunk;
|
|
1710
|
+
while (chunk) {
|
|
1711
|
+
if (chunk.intro.length && chunk.intro.trim() || chunk.content.length && chunk.content.trim() || chunk.outro.length && chunk.outro.trim()) return false;
|
|
1712
|
+
chunk = chunk.next;
|
|
1713
|
+
}
|
|
1714
|
+
if (this.outro.length && this.outro.trim()) return false;
|
|
1715
|
+
return true;
|
|
1716
|
+
}
|
|
1717
|
+
length() {
|
|
1718
|
+
let chunk = this.firstChunk;
|
|
1719
|
+
let length = 0;
|
|
1720
|
+
while (chunk) {
|
|
1721
|
+
length += chunk.intro.length + chunk.content.length + chunk.outro.length;
|
|
1722
|
+
chunk = chunk.next;
|
|
1723
|
+
}
|
|
1724
|
+
return length;
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Removes empty lines from the start and end.
|
|
1728
|
+
*/
|
|
1729
|
+
trimLines() {
|
|
1730
|
+
return this.trim("[\\r\\n]");
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Trims content matching `charType` (defaults to `\s`, i.e. whitespace) from the start and end.
|
|
1734
|
+
*/
|
|
1735
|
+
trim(charType) {
|
|
1736
|
+
return this.trimStart(charType).trimEnd(charType);
|
|
1737
|
+
}
|
|
1738
|
+
/** @internal */
|
|
1739
|
+
trimEndAborted(charType) {
|
|
1740
|
+
const rx = new RegExp(`${charType || "\\s"}+$`);
|
|
1741
|
+
this.outro = this.outro.replace(rx, "");
|
|
1742
|
+
if (this.outro.length) return true;
|
|
1743
|
+
let chunk = this.lastChunk;
|
|
1744
|
+
do {
|
|
1745
|
+
const end = chunk.end;
|
|
1746
|
+
const aborted = chunk.trimEnd(rx);
|
|
1747
|
+
if (chunk.end !== end) {
|
|
1748
|
+
if (this.lastChunk === chunk) this.lastChunk = chunk.next;
|
|
1749
|
+
this.byEnd.set(chunk.end, chunk);
|
|
1750
|
+
this.byStart.set(chunk.next.start, chunk.next);
|
|
1751
|
+
this.byEnd.set(chunk.next.end, chunk.next);
|
|
1752
|
+
}
|
|
1753
|
+
if (aborted) return true;
|
|
1754
|
+
chunk = chunk.previous;
|
|
1755
|
+
} while (chunk);
|
|
1756
|
+
this.intro = this.intro.replace(rx, "");
|
|
1757
|
+
return this.intro.length > 0;
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Trims content matching `charType` (defaults to `\s`, i.e. whitespace) from the end.
|
|
1761
|
+
*/
|
|
1762
|
+
trimEnd(charType) {
|
|
1763
|
+
this.trimEndAborted(charType);
|
|
1764
|
+
return this;
|
|
1765
|
+
}
|
|
1766
|
+
/** @internal */
|
|
1767
|
+
trimStartAborted(charType) {
|
|
1768
|
+
const rx = new RegExp(`^${charType || "\\s"}+`);
|
|
1769
|
+
this.intro = this.intro.replace(rx, "");
|
|
1770
|
+
if (this.intro.length) return true;
|
|
1771
|
+
let chunk = this.firstChunk;
|
|
1772
|
+
do {
|
|
1773
|
+
const end = chunk.end;
|
|
1774
|
+
const aborted = chunk.trimStart(rx);
|
|
1775
|
+
if (chunk.end !== end) {
|
|
1776
|
+
if (chunk === this.lastChunk) this.lastChunk = chunk.next;
|
|
1777
|
+
this.byEnd.set(chunk.end, chunk);
|
|
1778
|
+
this.byStart.set(chunk.next.start, chunk.next);
|
|
1779
|
+
this.byEnd.set(chunk.next.end, chunk.next);
|
|
1780
|
+
}
|
|
1781
|
+
if (aborted) return true;
|
|
1782
|
+
chunk = chunk.next;
|
|
1783
|
+
} while (chunk);
|
|
1784
|
+
this.outro = this.outro.replace(rx, "");
|
|
1785
|
+
return this.outro.length > 0;
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Trims content matching `charType` (defaults to `\s`, i.e. whitespace) from the start.
|
|
1789
|
+
*/
|
|
1790
|
+
trimStart(charType) {
|
|
1791
|
+
this.trimStartAborted(charType);
|
|
1792
|
+
return this;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Indicates if the string has been changed.
|
|
1796
|
+
*/
|
|
1797
|
+
hasChanged() {
|
|
1798
|
+
if (this.intro || this.outro) return true;
|
|
1799
|
+
let outputIndex = 0;
|
|
1800
|
+
let chunk = this.firstChunk;
|
|
1801
|
+
while (chunk) {
|
|
1802
|
+
if (chunk.intro || chunk.outro) return true;
|
|
1803
|
+
if (!chunk.edited && chunk.start === outputIndex) outputIndex = chunk.end;
|
|
1804
|
+
else {
|
|
1805
|
+
if (!this.original.startsWith(chunk.content, outputIndex)) return true;
|
|
1806
|
+
outputIndex += chunk.content.length;
|
|
1807
|
+
}
|
|
1808
|
+
chunk = chunk.next;
|
|
1809
|
+
}
|
|
1810
|
+
return outputIndex !== this.original.length;
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Whether the original range [start, end) has had any of its characters
|
|
1814
|
+
* removed. `replace`/`replaceAll` search `original`, so a match can land on
|
|
1815
|
+
* text that is no longer in the output - overwriting it would resurrect the
|
|
1816
|
+
* removed characters, so such matches are skipped instead.
|
|
1817
|
+
*
|
|
1818
|
+
* @internal
|
|
1819
|
+
*/
|
|
1820
|
+
_hasRemovedContent(start, end) {
|
|
1821
|
+
let chunk = this.byStart.get(start);
|
|
1822
|
+
if (!chunk) {
|
|
1823
|
+
chunk = this.lastSearchedChunk;
|
|
1824
|
+
const searchForward = start >= chunk.end;
|
|
1825
|
+
while (chunk && !chunk.contains(start)) chunk = searchForward ? this.byStart.get(chunk.end) : this.byEnd.get(chunk.start);
|
|
1826
|
+
if (chunk) this.lastSearchedChunk = chunk;
|
|
1827
|
+
}
|
|
1828
|
+
while (chunk && chunk.start < end) {
|
|
1829
|
+
if (chunk.content === "") return true;
|
|
1830
|
+
chunk = this.byStart.get(chunk.end);
|
|
1831
|
+
}
|
|
1832
|
+
return false;
|
|
1833
|
+
}
|
|
1834
|
+
/** @internal */
|
|
1835
|
+
_replaceRegexp(searchValue, replacement) {
|
|
1836
|
+
function getReplacement(match, str) {
|
|
1837
|
+
if (typeof replacement === "string") return expandReplacement(replacement, match[0], match.index, str, match.slice(1), match.groups);
|
|
1838
|
+
else return match.groups === void 0 ? replacement(match[0], ...match.slice(1), match.index, str) : replacement(match[0], ...match.slice(1), match.index, str, match.groups);
|
|
1839
|
+
}
|
|
1840
|
+
const replaceMatch = (match) => {
|
|
1841
|
+
/* v8 ignore next 2 -- `match.index` is always defined for matches from `matchAll` */
|
|
1842
|
+
if (match.index == null) return false;
|
|
1843
|
+
if (this._hasRemovedContent(match.index, match.index + match[0].length)) return false;
|
|
1844
|
+
const replacement = getReplacement(match, this.original);
|
|
1845
|
+
if (replacement === match[0]) return true;
|
|
1846
|
+
if (match[0].length === 0) this.appendRight(match.index - this.offset, replacement);
|
|
1847
|
+
else this.overwrite(match.index - this.offset, match.index + match[0].length - this.offset, replacement);
|
|
1848
|
+
return true;
|
|
1849
|
+
};
|
|
1850
|
+
if (searchValue.global) {
|
|
1851
|
+
searchValue.lastIndex = 0;
|
|
1852
|
+
for (const match of this.original.matchAll(searchValue)) replaceMatch(match);
|
|
1853
|
+
} else {
|
|
1854
|
+
const match = this.original.match(searchValue);
|
|
1855
|
+
if (match && !replaceMatch(match)) {
|
|
1856
|
+
const global = new RegExp(searchValue.source, `${searchValue.flags}g`);
|
|
1857
|
+
for (const next of this.original.matchAll(global)) if (replaceMatch(next)) break;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
return this;
|
|
1861
|
+
}
|
|
1862
|
+
/** @internal */
|
|
1863
|
+
_replaceString(string, replacement) {
|
|
1864
|
+
const { original } = this;
|
|
1865
|
+
let index = original.indexOf(string);
|
|
1866
|
+
while (index !== -1) {
|
|
1867
|
+
if (this._hasRemovedContent(index, index + string.length)) {
|
|
1868
|
+
index = original.indexOf(string, index + string.length);
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
if (typeof replacement === "function") replacement = replacement(string, index, original);
|
|
1872
|
+
else replacement = expandReplacement(replacement, string, index, original, [], void 0);
|
|
1873
|
+
if (string !== replacement) {
|
|
1874
|
+
if (string.length === 0) this.appendRight(index - this.offset, replacement);
|
|
1875
|
+
else this.overwrite(index - this.offset, index + string.length - this.offset, replacement);
|
|
1876
|
+
}
|
|
1877
|
+
break;
|
|
1878
|
+
}
|
|
1879
|
+
return this;
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* String replacement with RegExp or string.
|
|
1883
|
+
*/
|
|
1884
|
+
replace(searchValue, replacement) {
|
|
1885
|
+
if (typeof searchValue === "string") return this._replaceString(searchValue, replacement);
|
|
1886
|
+
return this._replaceRegexp(searchValue, replacement);
|
|
1887
|
+
}
|
|
1888
|
+
/** @internal */
|
|
1889
|
+
_replaceAllString(string, replacement) {
|
|
1890
|
+
const { original } = this;
|
|
1891
|
+
const stringLength = string.length;
|
|
1892
|
+
if (stringLength === 0) {
|
|
1893
|
+
for (let index = 0; index <= original.length; index += 1) {
|
|
1894
|
+
if (this._hasRemovedContent(index, index)) continue;
|
|
1895
|
+
const _replacement = typeof replacement === "function" ? replacement("", index, original) : expandReplacement(replacement, "", index, original, [], void 0);
|
|
1896
|
+
if (_replacement !== "") this.appendRight(index - this.offset, _replacement);
|
|
1897
|
+
}
|
|
1898
|
+
return this;
|
|
1899
|
+
}
|
|
1900
|
+
for (let index = original.indexOf(string); index !== -1; index = original.indexOf(string, index + stringLength)) {
|
|
1901
|
+
if (this._hasRemovedContent(index, index + stringLength)) continue;
|
|
1902
|
+
const previous = original.slice(index, index + stringLength);
|
|
1903
|
+
const _replacement = typeof replacement === "function" ? replacement(previous, index, original) : expandReplacement(replacement, previous, index, original, [], void 0);
|
|
1904
|
+
if (previous !== _replacement) this.overwrite(index - this.offset, index + stringLength - this.offset, _replacement);
|
|
1905
|
+
}
|
|
1906
|
+
return this;
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Same as `s.replace`, but replace all matched strings instead of just one.
|
|
1910
|
+
*/
|
|
1911
|
+
replaceAll(searchValue, replacement) {
|
|
1912
|
+
if (typeof searchValue === "string") return this._replaceAllString(searchValue, replacement);
|
|
1913
|
+
if (!searchValue.global) throw new MagicStringError("replaceAll() requires a global RegExp");
|
|
1914
|
+
return this._replaceRegexp(searchValue, replacement);
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
//#endregion
|
|
1918
|
+
//#region src/core/env.ts
|
|
1919
|
+
/**
|
|
1920
|
+
* Whether dev checks and messages run (PLAN §1, principle 4). Vite replaces
|
|
1921
|
+
* `import.meta.env.DEV` with a literal, so a production build drops every
|
|
1922
|
+
* `if (DEV)` branch and the messages in it (tests/production.test.ts checks
|
|
1923
|
+
* the bundle). Without Vite (plain Node, wrangler's esbuild, a CDN) `env` is
|
|
1924
|
+
* undefined, and the checks are off instead of throwing.
|
|
1925
|
+
*/
|
|
1926
|
+
const DEV = !!import.meta.env?.DEV;
|
|
1927
|
+
/**
|
|
1928
|
+
* Whether normalising a machine checks its config: in development, and in
|
|
1929
|
+
* the plugin's build-time compilation (M7), which runs this normaliser in
|
|
1930
|
+
* Node, where `import.meta.env` is undefined, and turns its errors into
|
|
1931
|
+
* build errors. Only {@link checkAlways} assigns it, and only the plugin
|
|
1932
|
+
* imports that, so an app's bundle never does and the bundler reads it as
|
|
1933
|
+
* `DEV`, dropping the checks from production builds as before.
|
|
1934
|
+
*/
|
|
1935
|
+
let CHECK = DEV;
|
|
1936
|
+
/** Turns the normaliser's checks on in this process: the plugin's build side only. */
|
|
1937
|
+
const checkAlways = () => void (CHECK = true);
|
|
1938
|
+
/**
|
|
1939
|
+
* Dev-only bookkeeping: how deep the running code is inside actions, so a
|
|
1940
|
+
* context write elsewhere can be named (D5). Only read and written behind
|
|
1941
|
+
* `DEV`, so production builds drop it.
|
|
1942
|
+
*/
|
|
1943
|
+
const dev = { acting: 0 };
|
|
1944
|
+
//#endregion
|
|
1945
|
+
//#region src/core/dev.ts
|
|
1946
|
+
/**
|
|
1947
|
+
* Dev errors and warnings (PLAN §4.11): each names the machine, what went
|
|
1948
|
+
* wrong and the fix, behind a code. Every caller sits behind `if (DEV)`, so
|
|
1949
|
+
* none of this reaches a production bundle.
|
|
1950
|
+
*/
|
|
1951
|
+
/** `[machine:CODE] name: message`. */
|
|
1952
|
+
const message = (code, machine, text) => `[machine:${code}] ${machine}: ${text}`;
|
|
1953
|
+
function fail(code, machine, text) {
|
|
1954
|
+
throw new Error(message(code, machine, text));
|
|
1955
|
+
}
|
|
1956
|
+
/** Edit distance, for "did you mean". */
|
|
1957
|
+
function distance(a, b) {
|
|
1958
|
+
let row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
1959
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1960
|
+
const next = [i];
|
|
1961
|
+
for (let j = 1; j <= b.length; j++) next[j] = Math.min(row[j] + 1, next[j - 1] + 1, row[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1962
|
+
row = next;
|
|
1963
|
+
}
|
|
1964
|
+
return row[b.length];
|
|
1965
|
+
}
|
|
1966
|
+
/** `; did you mean "closed"?` for the nearest of `options`, or "" when none is close. */
|
|
1967
|
+
function suggest(name, options) {
|
|
1968
|
+
let best = "";
|
|
1969
|
+
let score = Math.max(2, Math.floor(name.length / 3)) + 1;
|
|
1970
|
+
for (const option of options) {
|
|
1971
|
+
const d = distance(name.toLowerCase(), option.toLowerCase());
|
|
1972
|
+
if (d < score) [best, score] = [option, d];
|
|
1973
|
+
}
|
|
1974
|
+
return best ? `; did you mean "${best}"?` : "";
|
|
1975
|
+
}
|
|
1976
|
+
/** Keys users bring from XState and Zag, with what they are called here. */
|
|
1977
|
+
const RENAMED = {
|
|
1978
|
+
target: "to",
|
|
1979
|
+
guard: "if",
|
|
1980
|
+
cond: "if",
|
|
1981
|
+
actions: "do",
|
|
1982
|
+
effects: "effect",
|
|
1983
|
+
onEntry: "entry",
|
|
1984
|
+
onExit: "exit"
|
|
1985
|
+
};
|
|
1986
|
+
/** Fails on a key `config` should not have, naming the right one. */
|
|
1987
|
+
function checkKeys(config, allowed, machine, where) {
|
|
1988
|
+
for (const key of Object.keys(config)) {
|
|
1989
|
+
if (allowed.includes(key)) continue;
|
|
1990
|
+
fail("UNKNOWN_KEY", machine, `${where} has an unknown key "${key}"${RENAMED[key] ? `; use "${RENAMED[key]}".` : suggest(key, allowed) || "."} Allowed: ${allowed.join(", ")}.`);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
/** The DOM's spellings a part may not use, and React's, which every glue maps to its framework's. */
|
|
1994
|
+
const SPELLED = {
|
|
1995
|
+
class: "className",
|
|
1996
|
+
for: "htmlFor"
|
|
1997
|
+
};
|
|
1998
|
+
/**
|
|
1999
|
+
* Dev only: each part's handlers send an event some state handles, or are
|
|
2000
|
+
* functions; its keys are spelled as React spells them (`@menapo/glue/naming`).
|
|
2001
|
+
* Checked by the normaliser, so the plugin's compilation fails the build on
|
|
2002
|
+
* them, and when a compiled machine loads in development.
|
|
2003
|
+
*/
|
|
2004
|
+
function checkParts(config, name, handled) {
|
|
2005
|
+
const parts = config.parts ?? {};
|
|
2006
|
+
for (const part of Object.keys(parts)) for (const key of Object.keys(parts[part])) {
|
|
2007
|
+
const value = parts[part][key];
|
|
2008
|
+
if (/^on[A-Z]/.test(key)) {
|
|
2009
|
+
if (typeof value !== "string" && typeof value !== "function") fail("BAD_HANDLER", name, `part "${part}" has ${key}: ${String(value)}; an on* key takes an event name or a function of (view, event).`);
|
|
2010
|
+
if (typeof value === "string" && !handled.has(value)) fail("UNKNOWN_EVENT", name, `part "${part}" sends "${value}" from ${key}, which no state handles${suggest(value, handled) || "."} Handle it in a state's \`on\`, or send another event.`);
|
|
2011
|
+
} else if (key in SPELLED || /^on[a-z]/.test(key)) fail("PART_KEY", name, `part "${part}" has "${key}"; parts use React's spellings, which each framework's glue maps to its own, so write "${SPELLED[key] ?? `on${key[2].toUpperCase()}${key.slice(3)}`}"${key in SPELLED ? "" : " with the event's words capitalised (onKeyDown, onDoubleClick)"}.`);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
//#endregion
|
|
2015
|
+
//#region src/core/effects.ts
|
|
2016
|
+
const AFTER = { type: "machine.after" };
|
|
2017
|
+
/** An `after` timer: setTimeout's extra arguments spare a closure per timer. */
|
|
2018
|
+
function fire(instance, rules) {
|
|
2019
|
+
if (!instance.b) instance.go(rules, AFTER);
|
|
2020
|
+
}
|
|
2021
|
+
/** `[state, cleanup or timer, …]`: what an instance keeps while its states are active. */
|
|
2022
|
+
const keep = (instance, at, item) => void (instance.r ??= []).push(at, item);
|
|
2023
|
+
/** `instance.b` once stopped (the interpreter's STOPPED). */
|
|
2024
|
+
const STOPPED = 2;
|
|
2025
|
+
/**
|
|
2026
|
+
* Sets the timers and starts the effects of `states`, entered in the event
|
|
2027
|
+
* that just settled and still active, while the instance is busy: what an
|
|
2028
|
+
* effect sends runs once all have started. The glue's component calls it
|
|
2029
|
+
* only once the framework has committed the DOM, and never on a server.
|
|
2030
|
+
*
|
|
2031
|
+
* An effect that stops the instance ends it: nothing after it starts, and
|
|
2032
|
+
* the cleanup it returns runs at once. An effect that throws does not keep
|
|
2033
|
+
* the others from starting: the event runs to completion and tells its
|
|
2034
|
+
* subscribers, then the first error is thrown.
|
|
2035
|
+
*/
|
|
2036
|
+
function activate(instance, states) {
|
|
2037
|
+
let thrown;
|
|
2038
|
+
for (const at of states) {
|
|
2039
|
+
if (instance.b === STOPPED) break;
|
|
2040
|
+
if (at.af) for (const [ms, rules] of at.af) keep(instance, at, setTimeout(fire, ms, instance, rules));
|
|
2041
|
+
if (at.ef) for (const effect of at.ef) {
|
|
2042
|
+
if (instance.b === STOPPED) break;
|
|
2043
|
+
try {
|
|
2044
|
+
const cleanup = effect(instance);
|
|
2045
|
+
if (typeof cleanup === "function") {
|
|
2046
|
+
if (instance.b === STOPPED) cleanup();
|
|
2047
|
+
else keep(instance, at, cleanup);
|
|
2048
|
+
}
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
thrown ??= [error];
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
if (thrown) {
|
|
2055
|
+
instance.settle();
|
|
2056
|
+
throw thrown[0];
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Stops what `instance` keeps for state `at`, latest first, and forgets it
|
|
2061
|
+
* if it waits to start; without `at` (the instance stops), everything. A
|
|
2062
|
+
* cleanup that throws does not keep the others from running: the first
|
|
2063
|
+
* error is thrown once all have.
|
|
2064
|
+
*/
|
|
2065
|
+
function release(instance, at) {
|
|
2066
|
+
const { r, x } = instance;
|
|
2067
|
+
let thrown;
|
|
2068
|
+
if (!at) instance.r = instance.x = void 0;
|
|
2069
|
+
else x?.splice(x.indexOf(at) >>> 0, 1);
|
|
2070
|
+
if (r) {
|
|
2071
|
+
for (let k = r.length - 2; k >= 0; k -= 2) if (!at || r[k] === at) {
|
|
2072
|
+
const item = r.splice(k, 2)[1];
|
|
2073
|
+
try {
|
|
2074
|
+
if (typeof item === "function") item();
|
|
2075
|
+
else clearTimeout(item);
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
thrown ??= [error];
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
if (thrown) throw thrown[0];
|
|
2082
|
+
}
|
|
2083
|
+
//#endregion
|
|
2084
|
+
//#region ../glue/src/record.ts
|
|
2085
|
+
/**
|
|
2086
|
+
* The record kit's state lives in (machine PLAN D5, spike S1; store PLAN
|
|
2087
|
+
* D3): values in an array behind one accessor per field, defined once per
|
|
2088
|
+
* key set on a class's prototype, and a change mask with one bit per field.
|
|
2089
|
+
* A read is a few ns, and no proxy or snapshot is involved. `@menapo/machine`
|
|
2090
|
+
* lays out context and props by `bit`, `@menapo/store` its stores by `bit`
|
|
2091
|
+
* and `defineAccessors`, so the bit layout exists once. Machine writes its
|
|
2092
|
+
* accessor loop out: the call cost its toggle's size gate 12 B.
|
|
2093
|
+
*
|
|
2094
|
+
* Read tracking stays with each owner: machine records one mask per
|
|
2095
|
+
* derived value, the store one mask per store a derived value read.
|
|
2096
|
+
*/
|
|
2097
|
+
/** The bit of field `n`: fields past the 31st share the last bit. */
|
|
2098
|
+
const bit = (n) => 1 << Math.min(n, 31);
|
|
2099
|
+
//#endregion
|
|
2100
|
+
//#region src/core/props.ts
|
|
2101
|
+
/** Marks a `bindable()` value and carries its {@link Control}. */
|
|
2102
|
+
const BOUND = Symbol();
|
|
2103
|
+
/**
|
|
2104
|
+
* Records each declared prop's default, and hands a `bindable()` one to its
|
|
2105
|
+
* {@link Control}: what the normaliser does, and a compiled machine at load.
|
|
2106
|
+
*/
|
|
2107
|
+
function bind(tables, props) {
|
|
2108
|
+
tables.pn.forEach((prop, j) => {
|
|
2109
|
+
const value = props[prop];
|
|
2110
|
+
const control = value?.[BOUND];
|
|
2111
|
+
tables.pd.push(control ? value.value : value);
|
|
2112
|
+
if (control) (tables.k = control).bind(tables, j, prop, value, tables.Q.prototype);
|
|
2113
|
+
else tables.pl.push(j);
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Takes the owner's props; a missing plain prop falls back to its default.
|
|
2118
|
+
* From `setProps` (`queue`): taken once the event being handled is done,
|
|
2119
|
+
* as one event of their own when none is, never once stopped.
|
|
2120
|
+
*/
|
|
2121
|
+
function own(instance, owner, queue) {
|
|
2122
|
+
if (queue) {
|
|
2123
|
+
if (instance.b !== 2) {
|
|
2124
|
+
instance.n = owner;
|
|
2125
|
+
if (!instance.b) instance.go(void 0, instance.event);
|
|
2126
|
+
}
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
const { M, props } = instance;
|
|
2130
|
+
const previous = instance.o;
|
|
2131
|
+
instance.o = owner;
|
|
2132
|
+
for (const j of M.pl) {
|
|
2133
|
+
const given = owner[M.pn[j]];
|
|
2134
|
+
const value = given === void 0 ? M.pd[j] : given;
|
|
2135
|
+
if (props.v[j] !== value) {
|
|
2136
|
+
props.v[j] = value;
|
|
2137
|
+
props.m |= bit(1 + M.nm.length - M.pn.length + j);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
if (instance.s) M.k?.own(instance, previous);
|
|
2141
|
+
}
|
|
2142
|
+
//#endregion
|
|
2143
|
+
//#region src/core/store.ts
|
|
2144
|
+
/**
|
|
2145
|
+
* Context and props as objects with one accessor per field, defined once per
|
|
2146
|
+
* machine on a prototype (D5, spike S1), with the bit layout of
|
|
2147
|
+
* `@menapo/glue`'s record, which the store shares: a read is a few ns, a write that
|
|
2148
|
+
* changes the value sets the field's bit in a change mask, and no proxy or
|
|
2149
|
+
* snapshot is involved. Reads also record their bits while a derived value
|
|
2150
|
+
* is being computed, so it is recomputed only when what it read changes.
|
|
2151
|
+
*/
|
|
2152
|
+
/** Bits read since the innermost derived value started computing. */
|
|
2153
|
+
let reads = 0;
|
|
2154
|
+
const track = (bits) => {
|
|
2155
|
+
reads |= bits;
|
|
2156
|
+
};
|
|
2157
|
+
/** Dev only: the name of the machine a store belongs to, for messages. */
|
|
2158
|
+
const nameOf$1 = (store) => store.i.M.name;
|
|
2159
|
+
/** A class with an accessor per name; `get(k)` and `set(k)` make each one's pair. */
|
|
2160
|
+
function accessors(names, get, set) {
|
|
2161
|
+
class Values {
|
|
2162
|
+
v;
|
|
2163
|
+
m = 0;
|
|
2164
|
+
i;
|
|
2165
|
+
constructor(values, instance) {
|
|
2166
|
+
this.v = values;
|
|
2167
|
+
this.i = instance;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
names.forEach((name, k) => {
|
|
2171
|
+
Object.defineProperty(Values.prototype, name, {
|
|
2172
|
+
configurable: true,
|
|
2173
|
+
get: get(k),
|
|
2174
|
+
set: set?.(k, name)
|
|
2175
|
+
});
|
|
2176
|
+
});
|
|
2177
|
+
return Values;
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Context (`writable`) or props, over `this.v[k]`. Context is written by
|
|
2181
|
+
* actions only (D5: anywhere else throws in dev); props are the owner's,
|
|
2182
|
+
* and only bindable ones (which `bindable` redefines) can be written.
|
|
2183
|
+
*/
|
|
2184
|
+
function storeClass(names, first, writable) {
|
|
2185
|
+
const Values = accessors(names, (k) => {
|
|
2186
|
+
const mask = bit(first + k);
|
|
2187
|
+
return function() {
|
|
2188
|
+
reads |= mask;
|
|
2189
|
+
return this.v[k];
|
|
2190
|
+
};
|
|
2191
|
+
}, (k, name) => {
|
|
2192
|
+
const mask = bit(first + k);
|
|
2193
|
+
return writable ? function(value) {
|
|
2194
|
+
if (DEV && !dev.acting) fail("CONTEXT_WRITE", nameOf$1(this), `context.${name} was written outside an action. Write context in a transition's \`do\`, \`entry\` or \`exit\`, and send an event from here instead.`);
|
|
2195
|
+
if (this.v[k] !== value) {
|
|
2196
|
+
this.v[k] = value;
|
|
2197
|
+
this.m |= mask;
|
|
2198
|
+
}
|
|
2199
|
+
} : DEV ? function() {
|
|
2200
|
+
fail("PROPS_WRITE", nameOf$1(this), `props.${name} belongs to the owner and is read-only. Declare it \`${name}: bindable(…)\` to let actions write it.`);
|
|
2201
|
+
} : void 0;
|
|
2202
|
+
});
|
|
2203
|
+
if (writable) Values.s = (machine, given) => {
|
|
2204
|
+
const values = machine.df.slice();
|
|
2205
|
+
if (given) for (const key of Object.keys(given)) {
|
|
2206
|
+
const at = machine.nm.indexOf(key);
|
|
2207
|
+
if (DEV && !(at >= 0 && at < values.length)) fail("UNKNOWN_FIELD", machine.name, `start() was given context.${key}, which the machine does not declare; add it to \`context\`.`);
|
|
2208
|
+
values[at] = given[key];
|
|
2209
|
+
}
|
|
2210
|
+
return values;
|
|
2211
|
+
};
|
|
2212
|
+
return Values;
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* The derived values' class: each getter returns the cached value while no
|
|
2216
|
+
* field it read has changed, else recomputes it with its reads recorded.
|
|
2217
|
+
* A value computed while a field it read is changed but not yet told is
|
|
2218
|
+
* recomputed on every read until the notification forgets it.
|
|
2219
|
+
*/
|
|
2220
|
+
function derivedClass(names, fns) {
|
|
2221
|
+
const Derived = accessors(names, (k) => function() {
|
|
2222
|
+
const c = this.v;
|
|
2223
|
+
const instance = this.i;
|
|
2224
|
+
const read = c[2 * k];
|
|
2225
|
+
if (read === void 0 || read & instance.live()) {
|
|
2226
|
+
const outer = reads;
|
|
2227
|
+
reads = 0;
|
|
2228
|
+
c[2 * k + 1] = fns[k](instance);
|
|
2229
|
+
c[2 * k] = reads;
|
|
2230
|
+
reads |= outer;
|
|
2231
|
+
} else reads |= read;
|
|
2232
|
+
return c[2 * k + 1];
|
|
2233
|
+
});
|
|
2234
|
+
/** Forgets the values that read what `changed` says changed: the interpreter calls it as it notifies. */
|
|
2235
|
+
Derived.prototype.f = function(changed) {
|
|
2236
|
+
const c = this.v;
|
|
2237
|
+
for (let k = 0; k < c.length; k += 2) if (c[k] & changed) c[k] = void 0;
|
|
2238
|
+
};
|
|
2239
|
+
return Derived;
|
|
2240
|
+
}
|
|
2241
|
+
//#endregion
|
|
2242
|
+
//#region src/core/machine.ts
|
|
2243
|
+
/**
|
|
2244
|
+
* `defineMachine`: the config normalised once into tables (PLAN §5.2).
|
|
2245
|
+
* Each state becomes a record linked to its parent, numbered depth first
|
|
2246
|
+
* (so an ancestor's index is below its descendants'), with the leaf it
|
|
2247
|
+
* settles in when entered, its entry/exit/effect lists, `after` timers,
|
|
2248
|
+
* `always` rules and one `Map` from event name to what it does, its
|
|
2249
|
+
* ancestors' rules folded in after its own (D20).
|
|
2250
|
+
*/
|
|
2251
|
+
const STATE_KEYS = [
|
|
2252
|
+
"initial",
|
|
2253
|
+
"states",
|
|
2254
|
+
"on",
|
|
2255
|
+
"entry",
|
|
2256
|
+
"exit",
|
|
2257
|
+
"effect",
|
|
2258
|
+
"after",
|
|
2259
|
+
"always",
|
|
2260
|
+
"type",
|
|
2261
|
+
"done"
|
|
2262
|
+
];
|
|
2263
|
+
const MACHINE_KEYS = [
|
|
2264
|
+
"name",
|
|
2265
|
+
"context",
|
|
2266
|
+
"props",
|
|
2267
|
+
"events",
|
|
2268
|
+
"derived",
|
|
2269
|
+
"actions",
|
|
2270
|
+
"guards",
|
|
2271
|
+
"effects",
|
|
2272
|
+
"initial",
|
|
2273
|
+
"states",
|
|
2274
|
+
"on",
|
|
2275
|
+
"parts",
|
|
2276
|
+
"expose"
|
|
2277
|
+
];
|
|
2278
|
+
const RULE_KEYS = [
|
|
2279
|
+
"to",
|
|
2280
|
+
"if",
|
|
2281
|
+
"do"
|
|
2282
|
+
];
|
|
2283
|
+
const list = (value) => value === void 0 || Array.isArray(value) ? value : [value];
|
|
2284
|
+
const NOTHING = /* @__PURE__ */ new Map();
|
|
2285
|
+
/**
|
|
2286
|
+
* False while the plugin's compilation (M7) normalises at build time, so the
|
|
2287
|
+
* tables it emits hold data only and the features' runtimes wire them when
|
|
2288
|
+
* the compiled machine loads. Only {@link prepared} assigns it, and only the
|
|
2289
|
+
* plugin imports that, so an app's bundle reads it as `true`.
|
|
2290
|
+
*/
|
|
2291
|
+
let wiring = true;
|
|
2292
|
+
/** `normalise(config)` without the features' wiring: the plugin's build side only. */
|
|
2293
|
+
function prepared(config) {
|
|
2294
|
+
wiring = false;
|
|
2295
|
+
try {
|
|
2296
|
+
return normalise(config);
|
|
2297
|
+
} finally {
|
|
2298
|
+
wiring = true;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
/** Config → tables: what `defineMachine` does at runtime, and the plugin at build time ({@link prepared}). */
|
|
2302
|
+
function normalise(config) {
|
|
2303
|
+
const name = CHECK ? config.name ?? "machine" : "";
|
|
2304
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
2305
|
+
const states = [];
|
|
2306
|
+
const root = {
|
|
2307
|
+
i: -1,
|
|
2308
|
+
path: ""
|
|
2309
|
+
};
|
|
2310
|
+
/** The raw config of `root`, then of each state. */
|
|
2311
|
+
const raws = [config];
|
|
2312
|
+
if (CHECK) checkKeys(config, MACHINE_KEYS, name, "the machine");
|
|
2313
|
+
const where = (from) => from === root ? "the machine" : `"${from.path}"`;
|
|
2314
|
+
const below = (up, key) => up === root ? key : `${up.path}.${key}`;
|
|
2315
|
+
/** Dev only: the state and target being resolved, for `find`'s message; set before each call. */
|
|
2316
|
+
let asked = [root, ""];
|
|
2317
|
+
const find = (path) => {
|
|
2318
|
+
const found = byPath.get(path);
|
|
2319
|
+
if (CHECK && !found && asked[1] !== void 0) {
|
|
2320
|
+
const [from, target] = asked;
|
|
2321
|
+
const last = (p) => p.slice(p.lastIndexOf(".") + 1);
|
|
2322
|
+
fail("UNKNOWN_TARGET", name, `${where(from)} → "${target}" does not exist${suggest(last(path), [...byPath.keys()].map(last)) || `; the states are ${[...byPath.keys()].map((p) => `"${p}"`).join(", ")}.`}`);
|
|
2323
|
+
}
|
|
2324
|
+
return found;
|
|
2325
|
+
};
|
|
2326
|
+
/**
|
|
2327
|
+
* Named implementations (D8), in one map: the types keep each kind's
|
|
2328
|
+
* names apart, and development rejects a name used in the wrong kind or
|
|
2329
|
+
* declared in two.
|
|
2330
|
+
*/
|
|
2331
|
+
const named = {
|
|
2332
|
+
...config.actions,
|
|
2333
|
+
...config.guards,
|
|
2334
|
+
...config.effects
|
|
2335
|
+
};
|
|
2336
|
+
if (CHECK) {
|
|
2337
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2338
|
+
for (const kind of [
|
|
2339
|
+
"actions",
|
|
2340
|
+
"guards",
|
|
2341
|
+
"effects"
|
|
2342
|
+
]) for (const key of Object.keys(config[kind] ?? {})) {
|
|
2343
|
+
if (seen.has(key)) fail("NAME_CLASH", name, `"${key}" is declared in \`${kind}\` and in another of \`actions\`, \`guards\` and \`effects\`; give each its own name.`);
|
|
2344
|
+
seen.add(key);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
/** Functions as written, or the ones they name. */
|
|
2348
|
+
const fns = (value) => value && list(value).map((f) => typeof f === "string" ? named[f] : f);
|
|
2349
|
+
/** Dev only: each name in `value` is one of `config[kind]`. */
|
|
2350
|
+
const known = (value, kind, from) => {
|
|
2351
|
+
for (const f of list(value) ?? []) if (typeof f !== "function" && !config[kind]?.[f]) fail("UNKNOWN_NAME", name, `${where(from)} names "${String(f)}", which is not in \`${kind}\`${suggest(String(f), Object.keys(config[kind] ?? {})) || "."} Declare it in \`${kind}\`, or write the function in place.`);
|
|
2352
|
+
};
|
|
2353
|
+
/** Numbers `up`'s children depth first, then sets the leaf entering `up` settles in. */
|
|
2354
|
+
const walk = (up, raw) => {
|
|
2355
|
+
const children = raw.states;
|
|
2356
|
+
if (children) {
|
|
2357
|
+
for (const key of Object.keys(children)) {
|
|
2358
|
+
const child = children[key];
|
|
2359
|
+
const state = {
|
|
2360
|
+
i: states.length,
|
|
2361
|
+
path: below(up, key),
|
|
2362
|
+
up,
|
|
2363
|
+
fi: child.type === "final"
|
|
2364
|
+
};
|
|
2365
|
+
if (CHECK) {
|
|
2366
|
+
if (/[.#]/.test(key)) fail("STATE_NAME", name, `state "${key}" has a "." or "#" in its name; targets use them as separators, so rename it.`);
|
|
2367
|
+
checkKeys(child, STATE_KEYS, name, `state "${state.path}"`);
|
|
2368
|
+
const type = child.type;
|
|
2369
|
+
if (type !== void 0 && type !== "final" && typeof type !== "object") fail("BAD_TYPE", name, `"${state.path}" has \`type: ${JSON.stringify(type)}\`; a state's type is "final", or \`parallel\` imported from @menapo/machine (\`type: parallel\`).`);
|
|
2370
|
+
}
|
|
2371
|
+
byPath.set(state.path, state);
|
|
2372
|
+
states.push(state);
|
|
2373
|
+
raws.push(child);
|
|
2374
|
+
walk(state, child);
|
|
2375
|
+
}
|
|
2376
|
+
if (CHECK && raw.initial === void 0 && typeof raw.type !== "object") fail("NO_INITIAL", name, `${up === root ? "the machine has no" : `"${up.path}" has child states but no`} \`initial\`; name one of: ${Object.keys(children).join(", ")}.`);
|
|
2377
|
+
}
|
|
2378
|
+
if (CHECK) asked = [up, raw.initial];
|
|
2379
|
+
up.lf = children ? find(below(up, raw.initial))?.lf : up;
|
|
2380
|
+
};
|
|
2381
|
+
walk(root, config);
|
|
2382
|
+
/** `#path` from the root, `.child` below `from`, else a sibling of `from` or of its nearest ancestor that has one. */
|
|
2383
|
+
const resolve = (target, from) => {
|
|
2384
|
+
if (CHECK) asked = [from, target];
|
|
2385
|
+
if (target[0] === "#") return find(target.slice(1));
|
|
2386
|
+
if (target[0] === ".") return find(below(from, target.slice(1)));
|
|
2387
|
+
for (let up = from.up; up && up !== root; up = up.up) {
|
|
2388
|
+
const found = byPath.get(`${up.path}.${target}`);
|
|
2389
|
+
if (found) return found;
|
|
2390
|
+
}
|
|
2391
|
+
return find(target);
|
|
2392
|
+
};
|
|
2393
|
+
const rules = (transitions, from) => list(transitions).map((item) => {
|
|
2394
|
+
if (CHECK && (typeof item !== "object" || item === null) && typeof item !== "string" && typeof item !== "function") fail("BAD_TRANSITION", name, `${where(from)} has a transition that is ${String(item)}; write a target name, an action, or { to, if, do }.`);
|
|
2395
|
+
const rule = typeof item === "string" ? { to: item } : typeof item === "function" ? { do: item } : item;
|
|
2396
|
+
if (CHECK) {
|
|
2397
|
+
checkKeys(rule, RULE_KEYS, name, `a transition in ${where(from)}`);
|
|
2398
|
+
known(rule.if, "guards", from);
|
|
2399
|
+
known(rule.do, "actions", from);
|
|
2400
|
+
if (rule.to === "") resolve("", from);
|
|
2401
|
+
}
|
|
2402
|
+
return {
|
|
2403
|
+
to: rule.to && resolve(rule.to, from),
|
|
2404
|
+
if: rule.if && fns(rule.if)[0],
|
|
2405
|
+
do: fns(rule.do)
|
|
2406
|
+
};
|
|
2407
|
+
});
|
|
2408
|
+
const handled = /* @__PURE__ */ new Set();
|
|
2409
|
+
[root, ...states].forEach((state, k) => {
|
|
2410
|
+
const raw = raws[k];
|
|
2411
|
+
const up = state.up;
|
|
2412
|
+
const inherited = up?.on ?? NOTHING;
|
|
2413
|
+
let on = state.fi ? NOTHING : inherited;
|
|
2414
|
+
if (raw.on && !state.fi) {
|
|
2415
|
+
const folded = on = new Map(inherited);
|
|
2416
|
+
for (const type of Object.keys(raw.on)) {
|
|
2417
|
+
if (CHECK) {
|
|
2418
|
+
const declared = config.events;
|
|
2419
|
+
if (declared && !(type in declared)) fail("UNKNOWN_EVENT", name, `${where(state)} handles "${type}", which is not in \`events\`${suggest(type, Object.keys(declared)) || "."} Declare it in \`events\`.`);
|
|
2420
|
+
handled.add(type);
|
|
2421
|
+
}
|
|
2422
|
+
const mine = rules(raw.on[type], state);
|
|
2423
|
+
const outer = inherited.get(type);
|
|
2424
|
+
folded.set(type, {
|
|
2425
|
+
ev: outer?.ev ?? { type },
|
|
2426
|
+
ru: outer ? mine.concat(outer.ru) : mine
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
if (CHECK) {
|
|
2431
|
+
if (raw.done && raw.type?.kind !== "parallel") fail("DONE_KEY", name, `${where(state)} has \`done\`, which is taken when every region of a parallel state is final; give the state \`type: parallel\`, or use \`always\`.`);
|
|
2432
|
+
}
|
|
2433
|
+
if (CHECK && state.fi) {
|
|
2434
|
+
const region = up !== root && (raws[up.up.i + 1]?.type)?.kind;
|
|
2435
|
+
if (up !== root && region !== "parallel") fail("FINAL_STATE", name, `"${state.path}" is final but not top-level; a final state ends the machine at the top level, or a region of a parallel state, so move it or drop \`type: "final"\`.`);
|
|
2436
|
+
for (const key of [
|
|
2437
|
+
"states",
|
|
2438
|
+
"on",
|
|
2439
|
+
"effect",
|
|
2440
|
+
"after",
|
|
2441
|
+
"always"
|
|
2442
|
+
]) if (raw[key]) fail("FINAL_STATE", name, `final state "${state.path}" has \`${key}\`, but the machine stops there; move it to another state.`);
|
|
2443
|
+
}
|
|
2444
|
+
const always = raw.always && rules(raw.always, state);
|
|
2445
|
+
const after = raw.after;
|
|
2446
|
+
state.on = on;
|
|
2447
|
+
if (CHECK) {
|
|
2448
|
+
known(raw.entry, "actions", state);
|
|
2449
|
+
known(raw.exit, "actions", state);
|
|
2450
|
+
known(raw.effect, "effects", state);
|
|
2451
|
+
}
|
|
2452
|
+
state.en = fns(raw.entry);
|
|
2453
|
+
state.ex = fns(raw.exit);
|
|
2454
|
+
state.ef = fns(raw.effect);
|
|
2455
|
+
state.af = after && Object.keys(after).map((ms) => {
|
|
2456
|
+
if (CHECK && !(Number(ms) >= 0)) fail("BAD_DELAY", name, `"${state.path}" has \`after: { ${ms}: … }\`; a delay is a number of milliseconds.`);
|
|
2457
|
+
return [+ms, rules(after[ms], state)];
|
|
2458
|
+
});
|
|
2459
|
+
state.al = always && up?.al ? always.concat(up.al) : always || up?.al;
|
|
2460
|
+
});
|
|
2461
|
+
if (CHECK) checkParts(config, name, handled);
|
|
2462
|
+
const context = config.context ?? {};
|
|
2463
|
+
const fields = Object.keys(context);
|
|
2464
|
+
const defaults = Object.values(context);
|
|
2465
|
+
if (CHECK) {
|
|
2466
|
+
for (const value of defaults) if (Array.isArray(value) || Object.getPrototypeOf(value ?? 0) === Object.prototype) Object.freeze(value);
|
|
2467
|
+
}
|
|
2468
|
+
const props = config.props ?? {};
|
|
2469
|
+
const propNames = Object.keys(props);
|
|
2470
|
+
const Props = propNames.length ? storeClass(propNames, 1 + fields.length, false) : void 0;
|
|
2471
|
+
const derived = config.derived;
|
|
2472
|
+
const tables = {
|
|
2473
|
+
ss: states,
|
|
2474
|
+
i0: root.lf,
|
|
2475
|
+
df: defaults,
|
|
2476
|
+
C: fields.length ? storeClass(fields, 1, true) : void 0,
|
|
2477
|
+
pn: propNames,
|
|
2478
|
+
pd: [],
|
|
2479
|
+
Q: Props,
|
|
2480
|
+
pl: [],
|
|
2481
|
+
nm: fields.concat(propNames),
|
|
2482
|
+
D: derived && derivedClass(Object.keys(derived), Object.values(derived)),
|
|
2483
|
+
k: void 0,
|
|
2484
|
+
bd: [],
|
|
2485
|
+
c: config,
|
|
2486
|
+
r: normalise,
|
|
2487
|
+
A: activate,
|
|
2488
|
+
X: release,
|
|
2489
|
+
P: own
|
|
2490
|
+
};
|
|
2491
|
+
if (CHECK) Object.assign(tables, {
|
|
2492
|
+
name,
|
|
2493
|
+
handled
|
|
2494
|
+
});
|
|
2495
|
+
bind(tables, props);
|
|
2496
|
+
for (const raw of raws) raw.type?.f?.(tables, raws, rules);
|
|
2497
|
+
return tables;
|
|
2498
|
+
}
|
|
2499
|
+
Object.freeze({});
|
|
2500
|
+
function run(fns, view) {
|
|
2501
|
+
if (fns) {
|
|
2502
|
+
if (DEV) dev.acting++;
|
|
2503
|
+
for (const fn of fns) fn(view);
|
|
2504
|
+
if (DEV) dev.acting--;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
//#endregion
|
|
2508
|
+
//#region src/core/bindable.ts
|
|
2509
|
+
/**
|
|
2510
|
+
* `bindable`: a prop the owner may control or leave to the machine (D9, as
|
|
2511
|
+
* spike S2 refined it). One transition serves both modes:
|
|
2512
|
+
*
|
|
2513
|
+
* - uncontrolled, the machine moves or stores the value and tells the owner
|
|
2514
|
+
* through `on…Change`;
|
|
2515
|
+
* - controlled, a transition that would change the value runs its actions
|
|
2516
|
+
* when the event is handled and asks the owner; only the move waits. When
|
|
2517
|
+
* the owner's new value arrives, the machine takes the waiting transition's
|
|
2518
|
+
* target, and the target's entry sees the event that asked. Props that do
|
|
2519
|
+
* not grant it drop it (a veto); an owner that changes the value unasked
|
|
2520
|
+
* moves the machine to the matching state.
|
|
2521
|
+
*
|
|
2522
|
+
* All of it lives here and reaches the core only through the
|
|
2523
|
+
* {@link Control} a `bindable()` value carries, so an app that never imports
|
|
2524
|
+
* `bindable` ships none of it.
|
|
2525
|
+
*/
|
|
2526
|
+
/** What entry actions see when the owner moves a bound boolean unasked. */
|
|
2527
|
+
const PROPS = { type: "machine.props" };
|
|
2528
|
+
/**
|
|
2529
|
+
* A controlled-or-uncontrolled prop: `open: bindable(false, { states: {
|
|
2530
|
+
* true: "open", false: "closed" } })` gives the owner `open`, `defaultOpen`
|
|
2531
|
+
* and `onOpenChange`; `value: bindable("")` is data that actions write as
|
|
2532
|
+
* `props.value = next`.
|
|
2533
|
+
*/
|
|
2534
|
+
const bindable = (value, options) => ({
|
|
2535
|
+
[BOUND]: control,
|
|
2536
|
+
value,
|
|
2537
|
+
...options
|
|
2538
|
+
});
|
|
2539
|
+
const capital = (name) => name[0].toUpperCase() + name.slice(1);
|
|
2540
|
+
/** True when leaf `at` is inside `state`. */
|
|
2541
|
+
function inside$1(at, state) {
|
|
2542
|
+
for (; at; at = at.up) if (at === state) return true;
|
|
2543
|
+
return false;
|
|
2544
|
+
}
|
|
2545
|
+
const bounds = (machine) => machine.bd;
|
|
2546
|
+
const control = {
|
|
2547
|
+
bind(machine, prop, name, bindable, proto) {
|
|
2548
|
+
const { states, changed } = bindable;
|
|
2549
|
+
const find = (path) => {
|
|
2550
|
+
const state = machine.ss.find((s) => s.path === path);
|
|
2551
|
+
if (CHECK && !state) fail("UNKNOWN_TARGET", machine.name, `bindable "${name}" names state "${path}", which does not exist; use a path from the root: ${machine.ss.map((s) => `"${s.path}"`).join(", ")}.`);
|
|
2552
|
+
return state;
|
|
2553
|
+
};
|
|
2554
|
+
if (CHECK && changed !== void 0 && !machine.handled.has(changed)) {
|
|
2555
|
+
const handled = [...machine.handled];
|
|
2556
|
+
fail("UNKNOWN_EVENT", machine.name, `bindable "${name}" sends "${changed}" when the owner changes it, but no state handles it${suggest(changed, handled) || "."} Handle it in a state's \`on\`, or name an event one does.`);
|
|
2557
|
+
}
|
|
2558
|
+
const b = {
|
|
2559
|
+
prop,
|
|
2560
|
+
name,
|
|
2561
|
+
bit: bit(1 + machine.nm.length - machine.pn.length + prop),
|
|
2562
|
+
default: `default${capital(name)}`,
|
|
2563
|
+
change: `on${capital(name)}Change`,
|
|
2564
|
+
yes: states && find(states.true),
|
|
2565
|
+
no: states && find(states.false),
|
|
2566
|
+
changed
|
|
2567
|
+
};
|
|
2568
|
+
bounds(machine).push(b);
|
|
2569
|
+
Object.defineProperty(proto, name, {
|
|
2570
|
+
get() {
|
|
2571
|
+
const instance = this.i;
|
|
2572
|
+
track(b.bit);
|
|
2573
|
+
const given = instance.o[name];
|
|
2574
|
+
if (given !== void 0) return given;
|
|
2575
|
+
if (!b.yes) return this.v[prop];
|
|
2576
|
+
track(1);
|
|
2577
|
+
return inside$1(instance.s, b.yes);
|
|
2578
|
+
},
|
|
2579
|
+
set(value) {
|
|
2580
|
+
const owner = this.i.o;
|
|
2581
|
+
if (DEV) {
|
|
2582
|
+
if (b.yes) fail("BOUND_WRITE", machine.name, `props.${name} follows the state; move to "${b.yes.path}" or "${b.no.path}" instead of writing it.`);
|
|
2583
|
+
if (!dev.acting) fail("CONTEXT_WRITE", machine.name, `props.${name} was written outside an action. Write it in a transition's \`do\`, \`entry\` or \`exit\`, and send an event from here instead.`);
|
|
2584
|
+
}
|
|
2585
|
+
const controlled = owner[name] !== void 0;
|
|
2586
|
+
if ((controlled ? owner[name] : this.v[prop]) === value) return;
|
|
2587
|
+
if (!controlled) {
|
|
2588
|
+
this.v[prop] = value;
|
|
2589
|
+
this.m |= b.bit;
|
|
2590
|
+
}
|
|
2591
|
+
owner[b.change]?.(value);
|
|
2592
|
+
}
|
|
2593
|
+
});
|
|
2594
|
+
},
|
|
2595
|
+
initial(instance, leaf) {
|
|
2596
|
+
const owner = instance.o;
|
|
2597
|
+
for (const b of bounds(instance.M)) {
|
|
2598
|
+
const given = owner[b.name] !== void 0 ? owner[b.name] : owner[b.default];
|
|
2599
|
+
if (given === void 0) continue;
|
|
2600
|
+
if (!b.yes) instance.props.v[b.prop] = given;
|
|
2601
|
+
else if (!!given !== inside$1(leaf, b.yes)) leaf = (given ? b.yes : b.no).lf;
|
|
2602
|
+
}
|
|
2603
|
+
return leaf;
|
|
2604
|
+
},
|
|
2605
|
+
hold(instance, rule) {
|
|
2606
|
+
for (const b of bounds(instance.M)) {
|
|
2607
|
+
if (!b.yes || instance.o[b.name] === void 0) continue;
|
|
2608
|
+
const next = inside$1(rule.to.lf, b.yes);
|
|
2609
|
+
if (inside$1(instance.s, b.yes) === next) continue;
|
|
2610
|
+
run(rule.do, instance);
|
|
2611
|
+
instance.p = {
|
|
2612
|
+
to: rule.to,
|
|
2613
|
+
event: instance.event,
|
|
2614
|
+
b
|
|
2615
|
+
};
|
|
2616
|
+
instance.o[b.change]?.(next);
|
|
2617
|
+
return true;
|
|
2618
|
+
}
|
|
2619
|
+
return false;
|
|
2620
|
+
},
|
|
2621
|
+
moved(instance, from) {
|
|
2622
|
+
for (const b of bounds(instance.M)) {
|
|
2623
|
+
if (!b.yes || instance.o[b.name] !== void 0) continue;
|
|
2624
|
+
const now = inside$1(instance.s, b.yes);
|
|
2625
|
+
if (now !== inside$1(from, b.yes)) {
|
|
2626
|
+
instance.props.m |= b.bit;
|
|
2627
|
+
instance.o[b.change]?.(now);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
},
|
|
2631
|
+
own(instance, previous) {
|
|
2632
|
+
for (const b of bounds(instance.M)) {
|
|
2633
|
+
const value = instance.o[b.name];
|
|
2634
|
+
if (value !== previous[b.name]) {
|
|
2635
|
+
instance.props.m |= b.bit;
|
|
2636
|
+
if (b.changed && value !== void 0) instance.dispatch({
|
|
2637
|
+
type: b.changed,
|
|
2638
|
+
value
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
if (!b.yes) continue;
|
|
2642
|
+
const pending = instance.p?.b === b ? instance.p : void 0;
|
|
2643
|
+
if (pending) instance.p = void 0;
|
|
2644
|
+
if (value === void 0 || !!value === inside$1(instance.s, b.yes)) continue;
|
|
2645
|
+
if (pending && inside$1(pending.to.lf, b.yes) === !!value) {
|
|
2646
|
+
instance.event = pending.event;
|
|
2647
|
+
instance.move(pending.to, void 0);
|
|
2648
|
+
} else {
|
|
2649
|
+
instance.event = PROPS;
|
|
2650
|
+
instance.move(value ? b.yes : b.no, void 0);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
//#endregion
|
|
2656
|
+
//#region src/features/marks.ts
|
|
2657
|
+
/** The feature's mark on a raw state config, if it has one. */
|
|
2658
|
+
const markOf = (raw) => typeof raw?.type === "object" ? raw.type : void 0;
|
|
2659
|
+
//#endregion
|
|
2660
|
+
//#region src/features/records.ts
|
|
2661
|
+
const records = /* @__PURE__ */ new WeakMap();
|
|
2662
|
+
const recorded = (instance, history) => records.get(instance)?.get(history);
|
|
2663
|
+
function record(instance, history, states) {
|
|
2664
|
+
let map = records.get(instance);
|
|
2665
|
+
if (!map) records.set(instance, map = /* @__PURE__ */ new Map());
|
|
2666
|
+
map.set(history, states);
|
|
2667
|
+
}
|
|
2668
|
+
//#endregion
|
|
2669
|
+
//#region src/features/resume.ts
|
|
2670
|
+
function wireHistory(tables) {
|
|
2671
|
+
const states = tables.ss;
|
|
2672
|
+
const histories = states.filter((s) => s.h !== void 0);
|
|
2673
|
+
/** History state → the state its parent enters when nothing is recorded yet: its initial child. */
|
|
2674
|
+
const initial = /* @__PURE__ */ new Map();
|
|
2675
|
+
for (const h of histories) {
|
|
2676
|
+
const parent = h.up;
|
|
2677
|
+
let child = parent.lf;
|
|
2678
|
+
while (child.up !== parent) child = child.up;
|
|
2679
|
+
initial.set(h, child);
|
|
2680
|
+
const deep = h.h;
|
|
2681
|
+
const keep = (view) => {
|
|
2682
|
+
const leaf = view.s;
|
|
2683
|
+
let child = leaf;
|
|
2684
|
+
while (child.up !== parent) child = child.up;
|
|
2685
|
+
record(view, h, [deep ? leaf : child]);
|
|
2686
|
+
};
|
|
2687
|
+
parent.ex = [keep, ...parent.ex ?? []];
|
|
2688
|
+
}
|
|
2689
|
+
const all = /* @__PURE__ */ new Set();
|
|
2690
|
+
for (const state of [states[0].up, ...states]) {
|
|
2691
|
+
for (const handler of state.on.values()) for (const rule of handler.ru) all.add(rule);
|
|
2692
|
+
for (const rule of state.al ?? []) all.add(rule);
|
|
2693
|
+
for (const [, rules] of state.af ?? []) for (const rule of rules) all.add(rule);
|
|
2694
|
+
}
|
|
2695
|
+
for (const rule of all) {
|
|
2696
|
+
const h = rule.to;
|
|
2697
|
+
if (!h || !initial.has(h)) continue;
|
|
2698
|
+
const guard = rule.if;
|
|
2699
|
+
const routed = rule;
|
|
2700
|
+
routed.h = h;
|
|
2701
|
+
routed.if = (view) => (!guard || guard(view)) && !!(routed.to = recorded(view, h)?.[0] ?? initial.get(h));
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
//#endregion
|
|
2705
|
+
//#region src/features/history.ts
|
|
2706
|
+
/**
|
|
2707
|
+
* `history()` (PLAN §4.9): a pseudo-state that stands for where its parent
|
|
2708
|
+
* was when last exited. A transition to it enters that again: the parent's
|
|
2709
|
+
* active child (shallow, its own initial states below it) or its active
|
|
2710
|
+
* leaves (`history("deep")`); before the parent was ever exited, the
|
|
2711
|
+
* parent's initial state. It is never active itself.
|
|
2712
|
+
*
|
|
2713
|
+
* Its mark carries what prepares a machine's tables, which the normaliser
|
|
2714
|
+
* calls when it meets the mark: each history state is marked (`h`: deep or
|
|
2715
|
+
* not) and checked; `resume.ts` wires them. Importing it has no effect. A
|
|
2716
|
+
* machine with parallel states leaves both to `parallel`'s configurations,
|
|
2717
|
+
* which read the same marker and the same records. A compiled machine (M7)
|
|
2718
|
+
* has its tables prepared at build time, so it ships `resume.ts` alone.
|
|
2719
|
+
*/
|
|
2720
|
+
/** The machines whose history states are prepared. */
|
|
2721
|
+
const done = /* @__PURE__ */ new WeakSet();
|
|
2722
|
+
/** Prepares a machine's history states, then wires them: what the marks carry. */
|
|
2723
|
+
const prepare$1 = (tables, raws) => {
|
|
2724
|
+
if (done.has(tables) || raws.some((raw) => markOf(raw)?.kind === "parallel")) return;
|
|
2725
|
+
done.add(tables);
|
|
2726
|
+
tables.ss.forEach((state, k) => {
|
|
2727
|
+
const mark = markOf(raws[k + 1]);
|
|
2728
|
+
if (mark?.kind !== "history") return;
|
|
2729
|
+
const parent = state.up;
|
|
2730
|
+
if (CHECK) {
|
|
2731
|
+
if (parent.i < 0) fail("HISTORY", tables.name, `"${state.path}" is a history state of the machine itself, which is never exited; put it inside the state whose history it keeps.`);
|
|
2732
|
+
if (`${parent.path}.${raws[parent.i + 1].initial}` === state.path) fail("HISTORY", tables.name, `"${parent.path}" starts in its history state "${state.path}"; make another state its \`initial\`.`);
|
|
2733
|
+
}
|
|
2734
|
+
state.h = !!mark.deep;
|
|
2735
|
+
});
|
|
2736
|
+
if (wiring) wireHistory(tables);
|
|
2737
|
+
};
|
|
2738
|
+
const SHALLOW = /* @__PURE__ */ Object.freeze({
|
|
2739
|
+
kind: "history",
|
|
2740
|
+
deep: false,
|
|
2741
|
+
f: prepare$1
|
|
2742
|
+
});
|
|
2743
|
+
const DEEP = /* @__PURE__ */ Object.freeze({
|
|
2744
|
+
kind: "history",
|
|
2745
|
+
deep: true,
|
|
2746
|
+
f: prepare$1
|
|
2747
|
+
});
|
|
2748
|
+
/**
|
|
2749
|
+
* A history pseudo-state: `resume: history()` returns to the child its
|
|
2750
|
+
* parent was last in, `history("deep")` to the very states inside it.
|
|
2751
|
+
*/
|
|
2752
|
+
const history = (type) => ({ type: type === "deep" ? DEEP : SHALLOW });
|
|
2753
|
+
//#endregion
|
|
2754
|
+
//#region src/features/chart.ts
|
|
2755
|
+
/**
|
|
2756
|
+
* The runtime of `type: parallel` (PLAN §4.9, D12): what a machine with
|
|
2757
|
+
* parallel states runs on, wired by {@link wireParallel} once its tables are
|
|
2758
|
+
* prepared (`parallel.ts`, which the normaliser runs, or the plugin at build
|
|
2759
|
+
* time for a compiled machine, M7). Only a machine that declares a parallel
|
|
2760
|
+
* state ships this module.
|
|
2761
|
+
*
|
|
2762
|
+
* The core keeps one leaf (`instance.s`). In a machine with parallel
|
|
2763
|
+
* states that leaf is a configuration: an object shaped like a state, one
|
|
2764
|
+
* per set of active states, made once per machine and reused, whose `path`
|
|
2765
|
+
* lists the active leaves, whose `on` holds every event an active state
|
|
2766
|
+
* handles, and whose `up` chain names every active state for `is()`. Each
|
|
2767
|
+
* of its handlers is one rule whose guard selects the transitions (SCXML's
|
|
2768
|
+
* selection: per active leaf in document order, its own handler first,
|
|
2769
|
+
* then its ancestors'; of two whose exits overlap, the one declared deeper,
|
|
2770
|
+
* else the earlier) and whose action takes them all as one microstep:
|
|
2771
|
+
* exits in reverse document order, the transitions' actions, entries in
|
|
2772
|
+
* document order. The core's own `move` is never used; it runs the queue,
|
|
2773
|
+
* `always`, timers, effects and notifications as for any machine.
|
|
2774
|
+
*
|
|
2775
|
+
* Where a transition's exits stop is the core's rule: at the nearest common
|
|
2776
|
+
* ancestor of the leaf it was found from and the target's parent, so a
|
|
2777
|
+
* transition into a state's own subtree never exits that state; when that
|
|
2778
|
+
* ancestor is parallel, the next one up, so every region is left together.
|
|
2779
|
+
*
|
|
2780
|
+
* A region whose active child is a `type: "final"` state is done; when
|
|
2781
|
+
* every region of a parallel state is, the state's `done` transition is
|
|
2782
|
+
* taken after the event that finished it (an internal `machine.done`).
|
|
2783
|
+
* History states inside parallel machines are handled here too, with the
|
|
2784
|
+
* records `history` keeps.
|
|
2785
|
+
*/
|
|
2786
|
+
const DONE = { type: "machine.done" };
|
|
2787
|
+
/** Each parallel machine's chart, by its tables. */
|
|
2788
|
+
const charts = /* @__PURE__ */ new WeakMap();
|
|
2789
|
+
/** `at` strictly inside `up`: depth-first numbering puts a subtree in one range. */
|
|
2790
|
+
const inside = (at, up) => at.i > up.i && at.i <= up.e;
|
|
2791
|
+
/** What a leaf takes events through: a final state in a region has none of its own, its region's apply. */
|
|
2792
|
+
const handlersOf = (chart, leaf) => leaf.fi && leaf.up !== chart.r ? leaf.up.on : leaf.on;
|
|
2793
|
+
const configOf = (instance) => instance.s;
|
|
2794
|
+
/** The configuration of these active states (document order), made once per machine. */
|
|
2795
|
+
function configuration(chart, a) {
|
|
2796
|
+
const key = a.map((s) => s.i).join();
|
|
2797
|
+
let made = chart.m.get(key);
|
|
2798
|
+
if (!made) {
|
|
2799
|
+
const l = a.filter((s, k) => !(a[k + 1] && inside(a[k + 1], s)));
|
|
2800
|
+
let up;
|
|
2801
|
+
for (const s of a) up = {
|
|
2802
|
+
path: s.path,
|
|
2803
|
+
up
|
|
2804
|
+
};
|
|
2805
|
+
const on = /* @__PURE__ */ new Map();
|
|
2806
|
+
for (const leaf of l) for (const [type, handler] of handlersOf(chart, leaf)) if (!on.has(type)) on.set(type, {
|
|
2807
|
+
ev: handler.ev,
|
|
2808
|
+
ru: chart.e
|
|
2809
|
+
});
|
|
2810
|
+
if (a.some((s) => s.dn)) on.set(DONE.type, {
|
|
2811
|
+
ev: DONE,
|
|
2812
|
+
ru: chart.f
|
|
2813
|
+
});
|
|
2814
|
+
chart.m.set(key, made = {
|
|
2815
|
+
i: -2,
|
|
2816
|
+
path: l.map((s) => s.path).join(" "),
|
|
2817
|
+
up,
|
|
2818
|
+
fi: l.length === 1 && l[0].fi && l[0].up === chart.r,
|
|
2819
|
+
on,
|
|
2820
|
+
al: l.some((s) => s.al) ? chart.a : void 0,
|
|
2821
|
+
a,
|
|
2822
|
+
l
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
return made;
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* The transitions a guard selected, read by the action right after it: the
|
|
2829
|
+
* core runs a rule's action as soon as its guard passed (`take`), so one
|
|
2830
|
+
* module binding is enough.
|
|
2831
|
+
*/
|
|
2832
|
+
let picked = [];
|
|
2833
|
+
/**
|
|
2834
|
+
* Keeps the transitions found (rule, leaf, …) that SCXML would: one per
|
|
2835
|
+
* rule, and of two whose exits overlap the one declared deeper, else the
|
|
2836
|
+
* earlier. True when any is left, kept in {@link picked}.
|
|
2837
|
+
*/
|
|
2838
|
+
function choose(instance, found) {
|
|
2839
|
+
const taken = [];
|
|
2840
|
+
next: for (let k = 0; k < found.length; k += 2) {
|
|
2841
|
+
const rule = found[k];
|
|
2842
|
+
const leaf = found[k + 1];
|
|
2843
|
+
if (taken.some((t) => t[0] === rule)) continue;
|
|
2844
|
+
let domain;
|
|
2845
|
+
if (rule.to) {
|
|
2846
|
+
domain = leaf;
|
|
2847
|
+
let b = rule.to.up;
|
|
2848
|
+
while (domain !== b) if (domain.i > b.i) domain = domain.up;
|
|
2849
|
+
else b = b.up;
|
|
2850
|
+
while (domain.p) domain = domain.up;
|
|
2851
|
+
}
|
|
2852
|
+
const exits = domain ? configOf(instance).a.filter((s) => inside(s, domain)) : [];
|
|
2853
|
+
for (let j = 0; j < taken.length; j++) {
|
|
2854
|
+
const other = taken[j];
|
|
2855
|
+
if (!exits.some((s) => other[2].includes(s))) continue;
|
|
2856
|
+
if (inside(rule.s ?? leaf, other[0].s ?? leaf)) taken.splice(j--, 1);
|
|
2857
|
+
else continue next;
|
|
2858
|
+
}
|
|
2859
|
+
taken.push([
|
|
2860
|
+
rule,
|
|
2861
|
+
domain,
|
|
2862
|
+
exits
|
|
2863
|
+
]);
|
|
2864
|
+
}
|
|
2865
|
+
picked = taken;
|
|
2866
|
+
return taken.length > 0;
|
|
2867
|
+
}
|
|
2868
|
+
/** Pushes the first rule of `rules` whose guard passes, found from `leaf`. */
|
|
2869
|
+
function first(instance, rules, leaf, found) {
|
|
2870
|
+
for (const rule of rules) if (!rule.if || rule.if(instance)) return found.push(rule, leaf);
|
|
2871
|
+
}
|
|
2872
|
+
/** Per active leaf, its first transition for the event being handled, or (`always`) its first eventless one. */
|
|
2873
|
+
function pick(chart, instance, always) {
|
|
2874
|
+
const found = [];
|
|
2875
|
+
for (const leaf of configOf(instance).l) {
|
|
2876
|
+
const rules = always ? leaf.al : handlersOf(chart, leaf).get(instance.event.type)?.ru;
|
|
2877
|
+
if (rules) first(instance, rules, leaf, found);
|
|
2878
|
+
}
|
|
2879
|
+
return choose(instance, found);
|
|
2880
|
+
}
|
|
2881
|
+
/** A state's own rules (a timer's, a `done`): taken from the first active leaf inside it. */
|
|
2882
|
+
function pickFrom(instance, rules, state) {
|
|
2883
|
+
const { a, l } = configOf(instance);
|
|
2884
|
+
const found = [];
|
|
2885
|
+
if (a.includes(state)) first(instance, rules, l.find((s) => s === state || inside(s, state)), found);
|
|
2886
|
+
return choose(instance, found);
|
|
2887
|
+
}
|
|
2888
|
+
/** A rule whose guard picks the transitions and whose action takes them. */
|
|
2889
|
+
const routed = (pick) => ({
|
|
2890
|
+
to: void 0,
|
|
2891
|
+
if: pick,
|
|
2892
|
+
do: [step]
|
|
2893
|
+
});
|
|
2894
|
+
/** The action of every configuration rule: the microstep of what its guard picked. */
|
|
2895
|
+
const step = (view) => {
|
|
2896
|
+
const instance = view;
|
|
2897
|
+
const taken = picked;
|
|
2898
|
+
const { a, l } = configOf(instance);
|
|
2899
|
+
const exits = /* @__PURE__ */ new Set();
|
|
2900
|
+
for (const t of taken) for (const s of t[2]) exits.add(s);
|
|
2901
|
+
for (const s of exits) for (const h of s.hs) record(instance, h, h.h ? l.filter((leaf) => inside(leaf, s)) : a.filter((c) => c.up === s));
|
|
2902
|
+
for (const s of [...exits].sort((x, y) => y.i - x.i)) instance.exit(s);
|
|
2903
|
+
for (const t of taken) run(t[0].do, instance);
|
|
2904
|
+
const enter = /* @__PURE__ */ new Set();
|
|
2905
|
+
for (const [rule, domain] of taken) if (rule.to) {
|
|
2906
|
+
descend(instance, rule.to, enter);
|
|
2907
|
+
ascend(instance, rule.to, domain, enter);
|
|
2908
|
+
}
|
|
2909
|
+
settle(instance, a.filter((s) => !exits.has(s)), enter);
|
|
2910
|
+
};
|
|
2911
|
+
/** Adds `state` and what entering it enters (SCXML's addDescendantStatesToEnter). */
|
|
2912
|
+
function descend(instance, state, enter) {
|
|
2913
|
+
if (state.h !== void 0) {
|
|
2914
|
+
const parent = state.up;
|
|
2915
|
+
const states = recorded(instance, state) ?? (parent.p ? parent.k : [parent.n]);
|
|
2916
|
+
for (const s of states) descend(instance, s, enter);
|
|
2917
|
+
for (const s of states) ascend(instance, s, parent, enter);
|
|
2918
|
+
} else {
|
|
2919
|
+
enter.add(state);
|
|
2920
|
+
if (state.p) for (const region of state.k) regions(instance, region, enter);
|
|
2921
|
+
else if (state.n) descend(instance, state.n, enter);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
/** Enters `region` by default unless a state in it is entered already. */
|
|
2925
|
+
function regions(instance, region, enter) {
|
|
2926
|
+
for (const s of enter) if (s === region || inside(s, region)) return;
|
|
2927
|
+
descend(instance, region, enter);
|
|
2928
|
+
}
|
|
2929
|
+
/** Adds the ancestors of `state` below `stop`, and the other regions of the parallel ones. */
|
|
2930
|
+
function ascend(instance, state, stop, enter) {
|
|
2931
|
+
for (let up = state.up; up !== stop && up.i >= 0; up = up.up) {
|
|
2932
|
+
enter.add(up);
|
|
2933
|
+
if (up.p) for (const region of up.k) regions(instance, region, enter);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
/**
|
|
2937
|
+
* Enters the new states in document order, each added to the configuration
|
|
2938
|
+
* just before its entry actions run (so `is()` there sees it and its
|
|
2939
|
+
* ancestors, not what is entered after it, as in the core): their timers
|
|
2940
|
+
* and effects start once the event settles, and a final state that
|
|
2941
|
+
* completes a parallel state queues that state's `done`.
|
|
2942
|
+
*/
|
|
2943
|
+
function settle(instance, kept, enter) {
|
|
2944
|
+
const chart = charts.get(instance.M);
|
|
2945
|
+
let active = kept;
|
|
2946
|
+
instance.s = configuration(chart, active);
|
|
2947
|
+
for (const s of [...enter].sort((x, y) => x.i - y.i)) {
|
|
2948
|
+
active = [...active, s].sort((x, y) => x.i - y.i);
|
|
2949
|
+
instance.s = configuration(chart, active);
|
|
2950
|
+
instance.d = 1;
|
|
2951
|
+
run(s.en, instance);
|
|
2952
|
+
if (s.af || s.ef) (instance.x ??= []).push(s);
|
|
2953
|
+
const done = s.fi && s.up.up?.dn;
|
|
2954
|
+
if (done && !instance.q?.includes(done.event) && complete(instance, s.up.up)) {
|
|
2955
|
+
const q = instance.q ??= [];
|
|
2956
|
+
let k = 0;
|
|
2957
|
+
while (k < q.length && chart.d.has(q[k])) k++;
|
|
2958
|
+
q.splice(k, 0, done.event);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
/** SCXML's isInFinalState: a compound state whose active child is final, a parallel one whose regions all are. */
|
|
2963
|
+
const complete = (instance, state) => state.p ? state.k.every((region) => complete(instance, region)) : configOf(instance).a.some((s) => s.up === state && s.fi);
|
|
2964
|
+
/** The runtime of a machine whose tables `parallel.ts` prepared: its chart, its timers' rules, its first configuration. */
|
|
2965
|
+
function wireParallel(tables) {
|
|
2966
|
+
const states = tables.ss;
|
|
2967
|
+
const root = states[0].up;
|
|
2968
|
+
const chart = {
|
|
2969
|
+
r: root,
|
|
2970
|
+
m: /* @__PURE__ */ new Map(),
|
|
2971
|
+
d: /* @__PURE__ */ new Map(),
|
|
2972
|
+
e: [routed((view) => pick(chart, view))],
|
|
2973
|
+
a: [routed((view) => pick(chart, view, true))],
|
|
2974
|
+
f: [routed((view) => {
|
|
2975
|
+
const state = chart.d.get(view.event);
|
|
2976
|
+
return !!state && complete(view, state) && pickFrom(view, state.dn.rules, state);
|
|
2977
|
+
})]
|
|
2978
|
+
};
|
|
2979
|
+
charts.set(tables, chart);
|
|
2980
|
+
for (const s of [root, ...states]) {
|
|
2981
|
+
if (s.af) s.af = s.af.map(([ms, list]) => [ms, [{
|
|
2982
|
+
...routed((view) => pickFrom(view, list, s)),
|
|
2983
|
+
w: list
|
|
2984
|
+
}]]);
|
|
2985
|
+
if (s.dn) chart.d.set(s.dn.event, s);
|
|
2986
|
+
}
|
|
2987
|
+
tables.i0 = {
|
|
2988
|
+
i: -2,
|
|
2989
|
+
up: root,
|
|
2990
|
+
en: [(view) => {
|
|
2991
|
+
const enter = /* @__PURE__ */ new Set();
|
|
2992
|
+
descend(view, root.n, enter);
|
|
2993
|
+
settle(view, [], enter);
|
|
2994
|
+
}]
|
|
2995
|
+
};
|
|
2996
|
+
}
|
|
2997
|
+
//#endregion
|
|
2998
|
+
//#region src/features/parallel.ts
|
|
2999
|
+
/**
|
|
3000
|
+
* `type: parallel` (PLAN §4.9, D12): a state whose children, its regions,
|
|
3001
|
+
* are all active at once. Only a machine that declares one ships this
|
|
3002
|
+
* module: `parallel` is imported to be written, and the mark carries what
|
|
3003
|
+
* prepares the machine's tables for the configurations of `chart.ts` (the
|
|
3004
|
+
* regions, initial children and subtrees of each state, the state that
|
|
3005
|
+
* declares each rule, a parallel state's `done`) and wires them, which the
|
|
3006
|
+
* normaliser calls when it meets the mark. Importing it has no effect.
|
|
3007
|
+
* A compiled machine (M7) has its tables prepared at build time, so it ships
|
|
3008
|
+
* `chart.ts` without this module.
|
|
3009
|
+
*/
|
|
3010
|
+
/** Prepares a machine with parallel states, then wires it: what the mark carries. */
|
|
3011
|
+
const prepare = (tables, raws, rules) => {
|
|
3012
|
+
const states = tables.ss;
|
|
3013
|
+
const root = states[0].up;
|
|
3014
|
+
if (root.k) return;
|
|
3015
|
+
const all = [root, ...states];
|
|
3016
|
+
const byPath = new Map(states.map((s) => [s.path, s]));
|
|
3017
|
+
for (const s of all) {
|
|
3018
|
+
const k = s.i + 1;
|
|
3019
|
+
const mark = markOf(raws[k]);
|
|
3020
|
+
const first = raws[k].initial;
|
|
3021
|
+
s.k = [];
|
|
3022
|
+
s.hs = [];
|
|
3023
|
+
s.p = mark?.kind === "parallel";
|
|
3024
|
+
s.h = mark?.kind === "history" ? !!mark.deep : void 0;
|
|
3025
|
+
s.n = first === void 0 ? void 0 : byPath.get(s === root ? first : `${s.path}.${first}`);
|
|
3026
|
+
for (let up = s; up; up = up.up) up.e = s.i;
|
|
3027
|
+
if (s.up) (s.h === void 0 ? s.up.k : s.up.hs).push(s);
|
|
3028
|
+
}
|
|
3029
|
+
if (CHECK) {
|
|
3030
|
+
for (const s of states) if (s.h !== void 0 && s.up.n === s) fail("HISTORY", tables.name, `"${s.up.path}" starts in its history state "${s.path}"; make another state its \`initial\`.`);
|
|
3031
|
+
if (tables.bd.some((b) => b.yes)) fail("PARALLEL", tables.name, "a bindable prop follows states (`bindable(…, { states })`) in a machine with parallel states, which is not supported yet; bind the value to data and move with events.");
|
|
3032
|
+
}
|
|
3033
|
+
/** Marks the first `count` rules of `list` as declared by `s`. */
|
|
3034
|
+
const own = (list, count, s) => {
|
|
3035
|
+
for (let k = 0; k < count; k++) list[k].s = s;
|
|
3036
|
+
};
|
|
3037
|
+
for (const s of all) {
|
|
3038
|
+
for (const [type, handler] of s.on) {
|
|
3039
|
+
const outer = s.up?.on.get(type);
|
|
3040
|
+
if (outer !== handler) own(handler.ru, handler.ru.length - (outer?.ru.length ?? 0), s);
|
|
3041
|
+
}
|
|
3042
|
+
if (s.al && s.al !== s.up?.al) own(s.al, s.al.length - (s.up?.al?.length ?? 0), s);
|
|
3043
|
+
for (const [, list] of s.af ?? []) own(list, list.length, s);
|
|
3044
|
+
const done = raws[s.i + 1]?.done;
|
|
3045
|
+
if (done && s.p) {
|
|
3046
|
+
const list = rules(done, s);
|
|
3047
|
+
own(list, list.length, s);
|
|
3048
|
+
s.dn = {
|
|
3049
|
+
rules: list,
|
|
3050
|
+
event: { type: DONE.type }
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
if (wiring) wireParallel(tables);
|
|
3055
|
+
};
|
|
3056
|
+
/** Marks a state whose regions are all active at once: `editor: { type: parallel, states: { … } }`. */
|
|
3057
|
+
const parallel = /* @__PURE__ */ Object.freeze({
|
|
3058
|
+
kind: "parallel",
|
|
3059
|
+
f: prepare
|
|
3060
|
+
});
|
|
3061
|
+
//#endregion
|
|
3062
|
+
//#region src/vite/compile/analyse.ts
|
|
3063
|
+
/** Why a config cannot be compiled, and where. */
|
|
3064
|
+
var NotLiteral = class extends Error {
|
|
3065
|
+
node;
|
|
3066
|
+
/** What to write instead, when it is not "write the structure out". */
|
|
3067
|
+
fix;
|
|
3068
|
+
constructor(node, reason, fix) {
|
|
3069
|
+
super(reason);
|
|
3070
|
+
this.node = node;
|
|
3071
|
+
this.fix = fix;
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
/** The fix for a target or a name the build cannot read. */
|
|
3075
|
+
const WRITE_IT = "Write the target or name as a string literal in the config, or the function in place, to compile it.";
|
|
3076
|
+
/** An identifier's or a private name's text. */
|
|
3077
|
+
const nameOf = (node) => node.type === "Identifier" || node.type === "PrivateIdentifier" ? node.name : "…";
|
|
3078
|
+
/** Strips what TypeScript adds around an expression (`as const`, `!`, parentheses). */
|
|
3079
|
+
function bare(node) {
|
|
3080
|
+
for (;;) switch (node.type) {
|
|
3081
|
+
case "TSAsExpression":
|
|
3082
|
+
case "TSSatisfiesExpression":
|
|
3083
|
+
case "TSNonNullExpression":
|
|
3084
|
+
case "TSTypeAssertion":
|
|
3085
|
+
case "ParenthesizedExpression":
|
|
3086
|
+
node = node.expression;
|
|
3087
|
+
break;
|
|
3088
|
+
default: return node;
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
/** A property's key as the runtime sees it, or undefined when computed. */
|
|
3092
|
+
function keyOf(property) {
|
|
3093
|
+
if (property.computed) return void 0;
|
|
3094
|
+
const { key } = property;
|
|
3095
|
+
if (key.type === "Identifier") return key.name;
|
|
3096
|
+
if (key.type === "Literal" && (typeof key.value === "string" || typeof key.value === "number")) return String(key.value);
|
|
3097
|
+
}
|
|
3098
|
+
/** A literal object's properties in order; NotLiteral on a spread, a computed key or a method. */
|
|
3099
|
+
function entries(node, what) {
|
|
3100
|
+
const object = bare(node);
|
|
3101
|
+
if (object.type !== "ObjectExpression") throw new NotLiteral(node, `${what} is not an object literal`);
|
|
3102
|
+
return object.properties.map((property) => {
|
|
3103
|
+
if (property.type !== "Property") throw new NotLiteral(property, `${what} has a spread`);
|
|
3104
|
+
const key = keyOf(property);
|
|
3105
|
+
if (key === void 0) throw new NotLiteral(property, `${what} has a computed key`);
|
|
3106
|
+
if (property.kind !== "init") throw new NotLiteral(property, `${what}.${key} is an accessor`);
|
|
3107
|
+
if (property.method) throw new NotLiteral(property, `${what}.${key} is a method`, `Write it as \`${key}: (view) => …\` to compile it.`);
|
|
3108
|
+
return [key, property];
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
const PLAIN_KEY$1 = /^[A-Za-z_$][\w$]*$/;
|
|
3112
|
+
const member = (object, key) => PLAIN_KEY$1.test(key) ? `${object}.${key}` : `${object}[${JSON.stringify(key)}]`;
|
|
3113
|
+
/**
|
|
3114
|
+
* Reads the config `node` of a `defineMachine` call; NotLiteral when its
|
|
3115
|
+
* structure is not written out.
|
|
3116
|
+
*/
|
|
3117
|
+
function analyse(node, scope) {
|
|
3118
|
+
const kept = [];
|
|
3119
|
+
const refs = /* @__PURE__ */ new Map();
|
|
3120
|
+
const unseen = /* @__PURE__ */ new Map();
|
|
3121
|
+
const c = /* @__PURE__ */ new Map();
|
|
3122
|
+
const context = [];
|
|
3123
|
+
const props = [];
|
|
3124
|
+
const derived = [];
|
|
3125
|
+
const written = /* @__PURE__ */ new Map();
|
|
3126
|
+
const keep = (value) => kept.push(value) - 1;
|
|
3127
|
+
const placeholder = (ref) => {
|
|
3128
|
+
const fn = function() {};
|
|
3129
|
+
refs.set(fn, ref);
|
|
3130
|
+
return fn;
|
|
3131
|
+
};
|
|
3132
|
+
/** The imported name `local` stands for, when it comes from this package. */
|
|
3133
|
+
const imported = (local) => {
|
|
3134
|
+
const from = scope.imports.get(local);
|
|
3135
|
+
return from && scope.isPackage(from[0]) ? from[1] : void 0;
|
|
3136
|
+
};
|
|
3137
|
+
/** A string written in place or held by a module `const`. */
|
|
3138
|
+
const string = (value) => {
|
|
3139
|
+
const n = bare(value);
|
|
3140
|
+
if (n.type === "Literal" && typeof n.value === "string") return n.value;
|
|
3141
|
+
if (n.type === "TemplateLiteral" && n.expressions.length === 0) return n.quasis[0]?.value.cooked ?? void 0;
|
|
3142
|
+
if (n.type === "Identifier" && !scope.imports.has(n.name)) {
|
|
3143
|
+
const init = scope.values.get(n.name);
|
|
3144
|
+
if (init && init !== "function") return string(init);
|
|
3145
|
+
}
|
|
3146
|
+
};
|
|
3147
|
+
/** A literal the runtime would reject (`null`, a number), passed through for its error. */
|
|
3148
|
+
const other = (value) => {
|
|
3149
|
+
const n = bare(value);
|
|
3150
|
+
if (n.type === "Literal") return n.value;
|
|
3151
|
+
if (n.type === "Identifier" && n.name === "undefined") return void 0;
|
|
3152
|
+
return NaN;
|
|
3153
|
+
};
|
|
3154
|
+
/**
|
|
3155
|
+
* A function: written in place or declared in the module. Where a target
|
|
3156
|
+
* or a name may stand, an identifier or a member whose value the build
|
|
3157
|
+
* cannot see (an import, a \`let\`, an enum's member) may be a string as
|
|
3158
|
+
* well as a function, so the machine is left to the runtime normaliser,
|
|
3159
|
+
* which reads the value (D14: both forms behave the same). A call's
|
|
3160
|
+
* result, in place or in a module \`const\`, is taken for a function, and
|
|
3161
|
+
* checked when the machine loads in development (\`unseen\`).
|
|
3162
|
+
*/
|
|
3163
|
+
const fn = (value, where) => {
|
|
3164
|
+
const n = bare(value);
|
|
3165
|
+
const ph = placeholder(`$[${keep(value)}]`);
|
|
3166
|
+
if (n.type === "ArrowFunctionExpression" || n.type === "FunctionExpression") return ph;
|
|
3167
|
+
if (n.type === "Identifier") {
|
|
3168
|
+
const init = scope.imports.has(n.name) ? void 0 : scope.values.get(n.name);
|
|
3169
|
+
if (init === "function") return ph;
|
|
3170
|
+
const i = init && bare(init);
|
|
3171
|
+
if (i?.type === "ArrowFunctionExpression" || i?.type === "FunctionExpression") return ph;
|
|
3172
|
+
if (i?.type === "ObjectExpression" || i?.type === "ArrayExpression") throw new NotLiteral(value, `${where} is \`${n.name}\`, written outside the config`);
|
|
3173
|
+
if (!init) {
|
|
3174
|
+
const from = scope.imports.get(n.name)?.[0];
|
|
3175
|
+
throw new NotLiteral(value, `${where} is \`${n.name}\`, ${from === void 0 ? "not a module `const`" : `imported from "${from}"`}, which may be a target or a name as well as a function`, WRITE_IT);
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
if (n.type === "MemberExpression") throw new NotLiteral(value, `${where} is \`${n.object.type === "Identifier" ? n.object.name : "…"}${n.computed ? "[…]" : `.${nameOf(n.property)}`}\`, a member the build cannot read, which may be a target or a name as well as a function`, WRITE_IT);
|
|
3179
|
+
unseen.set(ph, where);
|
|
3180
|
+
return ph;
|
|
3181
|
+
};
|
|
3182
|
+
/** One function or name, or a list of them (`entry`, `do`). */
|
|
3183
|
+
const fns = (value, where) => {
|
|
3184
|
+
const n = bare(value);
|
|
3185
|
+
if (n.type === "ArrayExpression") return n.elements.map((element, k) => {
|
|
3186
|
+
if (!element || element.type === "SpreadElement") throw new NotLiteral(value, `${where} has a spread or a hole`);
|
|
3187
|
+
return string(element) ?? fn(element, `${where}[${k}]`);
|
|
3188
|
+
});
|
|
3189
|
+
return string(value) ?? fn(value, where);
|
|
3190
|
+
};
|
|
3191
|
+
/** A transition: a target, a function, `{ to, if, do }`, or a list of them. */
|
|
3192
|
+
const transition = (value, where) => {
|
|
3193
|
+
const n = bare(value);
|
|
3194
|
+
const target = string(value);
|
|
3195
|
+
if (target !== void 0) return target;
|
|
3196
|
+
if (n.type === "ArrayExpression") return n.elements.map((element, k) => {
|
|
3197
|
+
if (!element || element.type === "SpreadElement") throw new NotLiteral(value, `${where} has a spread or a hole`);
|
|
3198
|
+
return transition(element, `${where}[${k}]`);
|
|
3199
|
+
});
|
|
3200
|
+
if (n.type === "ObjectExpression") {
|
|
3201
|
+
const rule = {};
|
|
3202
|
+
for (const [key, property] of entries(n, where)) if (key === "to") {
|
|
3203
|
+
const to = string(property.value);
|
|
3204
|
+
if (to === void 0 && bare(property.value).type !== "Literal") throw new NotLiteral(property.value, `${where}.to is not a string literal`);
|
|
3205
|
+
rule.to = to ?? other(property.value);
|
|
3206
|
+
} else if (key === "if") rule.if = fns(property.value, `${where}.if`);
|
|
3207
|
+
else if (key === "do") rule.do = fns(property.value, `${where}.do`);
|
|
3208
|
+
else rule[key] = other(property.value);
|
|
3209
|
+
return rule;
|
|
3210
|
+
}
|
|
3211
|
+
if (n.type === "Literal") return n.value;
|
|
3212
|
+
return fn(value, where);
|
|
3213
|
+
};
|
|
3214
|
+
const transitions = (value, where) => {
|
|
3215
|
+
const out = {};
|
|
3216
|
+
for (const [key, property] of entries(value, where)) out[key] = transition(property.value, member(where, key));
|
|
3217
|
+
return out;
|
|
3218
|
+
};
|
|
3219
|
+
const state = (value, path, where, at = value.start) => {
|
|
3220
|
+
const n = bare(value);
|
|
3221
|
+
if (n.type === "CallExpression" && n.callee.type === "Identifier" && imported(n.callee.name) === "history") {
|
|
3222
|
+
const [arg] = n.arguments;
|
|
3223
|
+
if (arg && arg.type !== "SpreadElement" && string(arg) !== "deep") throw new NotLiteral(arg, `${where}: history() takes nothing or "deep"`);
|
|
3224
|
+
written.set(path, {
|
|
3225
|
+
node: value,
|
|
3226
|
+
at,
|
|
3227
|
+
keys: /* @__PURE__ */ new Map()
|
|
3228
|
+
});
|
|
3229
|
+
return history(arg ? "deep" : void 0);
|
|
3230
|
+
}
|
|
3231
|
+
const keys = /* @__PURE__ */ new Map();
|
|
3232
|
+
const out = {};
|
|
3233
|
+
for (const [key, property] of entries(value, where)) {
|
|
3234
|
+
keys.set(key, property);
|
|
3235
|
+
const at = member(where, key);
|
|
3236
|
+
const v = property.value;
|
|
3237
|
+
switch (key) {
|
|
3238
|
+
case "initial": {
|
|
3239
|
+
const initial = string(v);
|
|
3240
|
+
if (initial === void 0) throw new NotLiteral(v, `${at} is not a string literal`);
|
|
3241
|
+
out.initial = initial;
|
|
3242
|
+
break;
|
|
3243
|
+
}
|
|
3244
|
+
case "states": {
|
|
3245
|
+
const states = {};
|
|
3246
|
+
for (const [name, child] of entries(v, at)) states[name] = state(child.value, path ? `${path}.${name}` : name, member(at, name), child.start);
|
|
3247
|
+
out.states = states;
|
|
3248
|
+
break;
|
|
3249
|
+
}
|
|
3250
|
+
case "on":
|
|
3251
|
+
case "after":
|
|
3252
|
+
out[key] = transitions(v, at);
|
|
3253
|
+
break;
|
|
3254
|
+
case "entry":
|
|
3255
|
+
case "exit":
|
|
3256
|
+
case "effect":
|
|
3257
|
+
out[key] = fns(v, at);
|
|
3258
|
+
break;
|
|
3259
|
+
case "always":
|
|
3260
|
+
case "done":
|
|
3261
|
+
out[key] = transition(v, at);
|
|
3262
|
+
break;
|
|
3263
|
+
case "type": {
|
|
3264
|
+
const b = bare(v);
|
|
3265
|
+
if (b.type === "Identifier" && imported(b.name) === "parallel") out.type = parallel;
|
|
3266
|
+
else if (string(v) !== void 0) out.type = string(v);
|
|
3267
|
+
else if (b.type === "Literal") out.type = b.value;
|
|
3268
|
+
else throw new NotLiteral(v, `${at} is not "final" or \`parallel\``);
|
|
3269
|
+
break;
|
|
3270
|
+
}
|
|
3271
|
+
default: out[key] = machineKey(key, v, at) ?? other(v);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
written.set(path, {
|
|
3275
|
+
node: value,
|
|
3276
|
+
at,
|
|
3277
|
+
keys
|
|
3278
|
+
});
|
|
3279
|
+
return out;
|
|
3280
|
+
};
|
|
3281
|
+
/** The keys only the machine has; undefined for any other. */
|
|
3282
|
+
const machineKey = (key, v, at) => {
|
|
3283
|
+
switch (key) {
|
|
3284
|
+
case "name":
|
|
3285
|
+
case "expose":
|
|
3286
|
+
c.set(key, keep(v));
|
|
3287
|
+
return key === "name" ? string(v) : {};
|
|
3288
|
+
case "parts":
|
|
3289
|
+
c.set(key, keep(v));
|
|
3290
|
+
return parts(v);
|
|
3291
|
+
case "events": {
|
|
3292
|
+
const b = bare(v);
|
|
3293
|
+
if (b.type !== "ObjectExpression") return void 0;
|
|
3294
|
+
try {
|
|
3295
|
+
return Object.fromEntries(entries(b, at).map(([event]) => [event, null]));
|
|
3296
|
+
} catch {
|
|
3297
|
+
return;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
case "context":
|
|
3301
|
+
case "props":
|
|
3302
|
+
case "derived": {
|
|
3303
|
+
const list = key === "context" ? context : key === "props" ? props : derived;
|
|
3304
|
+
const out = {};
|
|
3305
|
+
for (const [field, property] of entries(v, at)) {
|
|
3306
|
+
list.push([field, keep(property.value)]);
|
|
3307
|
+
out[field] = key === "derived" ? function() {} : null;
|
|
3308
|
+
if (key === "props") out[field] = bound(property.value) ?? null;
|
|
3309
|
+
}
|
|
3310
|
+
return out;
|
|
3311
|
+
}
|
|
3312
|
+
case "actions":
|
|
3313
|
+
case "guards":
|
|
3314
|
+
case "effects": {
|
|
3315
|
+
const named = entries(v, at);
|
|
3316
|
+
c.set(key, keep(v));
|
|
3317
|
+
return Object.fromEntries(named.map(([name]) => [name, placeholder(member(member("c", key), name))]));
|
|
3318
|
+
}
|
|
3319
|
+
default: return;
|
|
3320
|
+
}
|
|
3321
|
+
};
|
|
3322
|
+
/**
|
|
3323
|
+
* What the normaliser checks of `parts` (each handler's event, each key's
|
|
3324
|
+
* spelling), read where written; parts built elsewhere are checked when
|
|
3325
|
+
* the compiled machine loads in development. The code itself stays in place.
|
|
3326
|
+
*/
|
|
3327
|
+
const parts = (value) => {
|
|
3328
|
+
const out = {};
|
|
3329
|
+
try {
|
|
3330
|
+
for (const [name, part] of entries(value, "parts")) {
|
|
3331
|
+
const read = {};
|
|
3332
|
+
for (const [key, property] of entries(part.value, name)) {
|
|
3333
|
+
const n = bare(property.value);
|
|
3334
|
+
read[key] = string(property.value) ?? (n.type === "Literal" ? n.value : () => {});
|
|
3335
|
+
}
|
|
3336
|
+
out[name] = read;
|
|
3337
|
+
}
|
|
3338
|
+
} catch {
|
|
3339
|
+
return {};
|
|
3340
|
+
}
|
|
3341
|
+
return out;
|
|
3342
|
+
};
|
|
3343
|
+
/** `bindable(…, { states, changed })` with literal options, so the build checks its states. */
|
|
3344
|
+
const bound = (value) => {
|
|
3345
|
+
const n = bare(value);
|
|
3346
|
+
if (n.type !== "CallExpression" || n.callee.type !== "Identifier" || imported(n.callee.name) !== "bindable") return void 0;
|
|
3347
|
+
const options = n.arguments[1];
|
|
3348
|
+
if (!options) return bindable(null);
|
|
3349
|
+
try {
|
|
3350
|
+
const read = {};
|
|
3351
|
+
for (const [key, property] of entries(options, "bindable")) if (key === "states") {
|
|
3352
|
+
const states = {};
|
|
3353
|
+
for (const [side, s] of entries(property.value, "states")) states[side] = string(s.value);
|
|
3354
|
+
read.states = states;
|
|
3355
|
+
} else read[key] = string(property.value);
|
|
3356
|
+
return bindable(null, read);
|
|
3357
|
+
} catch {
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3360
|
+
};
|
|
3361
|
+
return {
|
|
3362
|
+
raw: state(node, "", "the config"),
|
|
3363
|
+
kept,
|
|
3364
|
+
refs,
|
|
3365
|
+
unseen,
|
|
3366
|
+
c,
|
|
3367
|
+
context,
|
|
3368
|
+
props,
|
|
3369
|
+
derived,
|
|
3370
|
+
written
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
//#endregion
|
|
3374
|
+
//#region src/vite/compile/emit.ts
|
|
3375
|
+
const PLAIN_KEY = /^[A-Za-z_$][\w$]*$/;
|
|
3376
|
+
const key = (k) => PLAIN_KEY.test(k) ? k : JSON.stringify(k);
|
|
3377
|
+
/** `function <name>($, c = {…}) { … }`: builds `analysis`'s tables. */
|
|
3378
|
+
function emit(tables, analysis, options) {
|
|
3379
|
+
const { runtime: use } = options;
|
|
3380
|
+
const states = tables.ss;
|
|
3381
|
+
const all = [states[0]?.up ?? tables.i0.up, ...states];
|
|
3382
|
+
const names = new Map(all.map((s, k) => [s, k ? `s${k - 1}` : "r"]));
|
|
3383
|
+
const count = /* @__PURE__ */ new Map();
|
|
3384
|
+
const visit = (value) => {
|
|
3385
|
+
if (typeof value !== "object" || value === null || names.has(value)) return;
|
|
3386
|
+
const n = (count.get(value) ?? 0) + 1;
|
|
3387
|
+
count.set(value, n);
|
|
3388
|
+
if (n > 1) return;
|
|
3389
|
+
if (value instanceof Map) for (const [, v] of value) visit(v);
|
|
3390
|
+
else if (Array.isArray(value)) for (const v of value) visit(v);
|
|
3391
|
+
else for (const v of Object.values(value)) visit(v);
|
|
3392
|
+
};
|
|
3393
|
+
for (const s of all) for (const v of Object.values(s)) visit(v);
|
|
3394
|
+
const lines = [];
|
|
3395
|
+
let shared = 0;
|
|
3396
|
+
const declared = /* @__PURE__ */ new Map();
|
|
3397
|
+
const val = (value, where) => {
|
|
3398
|
+
if (value === void 0) return "void 0";
|
|
3399
|
+
if (value === null) return "null";
|
|
3400
|
+
if (typeof value === "boolean") return value ? "!0" : "!1";
|
|
3401
|
+
if (typeof value === "number" || typeof value === "string") return JSON.stringify(value);
|
|
3402
|
+
if (typeof value === "function") {
|
|
3403
|
+
const ref = analysis.refs.get(value);
|
|
3404
|
+
if (ref === void 0) throw new Error(`[machine:COMPILE] ${where}: a function the build did not place`);
|
|
3405
|
+
const unseen = analysis.unseen.get(value);
|
|
3406
|
+
return unseen !== void 0 && options.dev ? `${use("fn")}(${ref}, ${JSON.stringify(unseen)}, ${JSON.stringify(options.machine)})` : ref;
|
|
3407
|
+
}
|
|
3408
|
+
if (typeof value !== "object") throw new Error(`[machine:COMPILE] ${where}: ${typeof value}`);
|
|
3409
|
+
const named = names.get(value) ?? declared.get(value);
|
|
3410
|
+
if (named) return named;
|
|
3411
|
+
let out;
|
|
3412
|
+
if (value instanceof Map) out = `new Map(${value.size ? `[${[...value].map(([k, v]) => `[${JSON.stringify(k)},${val(v, `${where}.${k}`)}]`).join(",")}]` : ""})`;
|
|
3413
|
+
else if (Array.isArray(value)) out = `[${value.map((v, k) => val(v, `${where}[${k}]`)).join(",")}]`;
|
|
3414
|
+
else out = `{${Object.entries(value).map(([k, v]) => `${key(k)}:${val(v, `${where}.${k}`)}`).join(",")}}`;
|
|
3415
|
+
if ((count.get(value) ?? 0) > 1) {
|
|
3416
|
+
const name = `o${shared++}`;
|
|
3417
|
+
lines.push(`const ${name}=${out};`);
|
|
3418
|
+
declared.set(value, name);
|
|
3419
|
+
return name;
|
|
3420
|
+
}
|
|
3421
|
+
return out;
|
|
3422
|
+
};
|
|
3423
|
+
const later = [];
|
|
3424
|
+
const done = /* @__PURE__ */ new Set();
|
|
3425
|
+
/** Whether `value` reaches a state not declared yet. */
|
|
3426
|
+
const ahead = (value, seen = /* @__PURE__ */ new Set()) => {
|
|
3427
|
+
if (typeof value !== "object" || value === null || seen.has(value)) return false;
|
|
3428
|
+
if (names.has(value)) return !done.has(value);
|
|
3429
|
+
seen.add(value);
|
|
3430
|
+
return (value instanceof Map ? [...value.values()] : Object.values(value)).some((v) => ahead(v, seen));
|
|
3431
|
+
};
|
|
3432
|
+
for (const s of all) {
|
|
3433
|
+
const fields = Object.entries(s).map(([k, v]) => {
|
|
3434
|
+
if (typeof v !== "object" || v === null) return `${key(k)}:${val(v, `${s.path}.${k}`)}`;
|
|
3435
|
+
if (done.has(v) || !ahead(v)) return `${key(k)}:${val(v, `${s.path}.${k}`)}`;
|
|
3436
|
+
later.push([s, k]);
|
|
3437
|
+
return `${key(k)}:void 0`;
|
|
3438
|
+
});
|
|
3439
|
+
lines.push(`const ${names.get(s)}={${fields.join(",")}};`);
|
|
3440
|
+
done.add(s);
|
|
3441
|
+
}
|
|
3442
|
+
for (const [s, k] of later) {
|
|
3443
|
+
const value = val(s[k], `${s.path}.${k}`);
|
|
3444
|
+
lines.push(`${names.get(s)}.${k}=${value};`);
|
|
3445
|
+
}
|
|
3446
|
+
const fields = analysis.context.map(([f]) => f);
|
|
3447
|
+
const propNames = analysis.props.map(([p]) => p);
|
|
3448
|
+
const effects = states.some((s) => s.af || s.ef);
|
|
3449
|
+
const ref = (k) => `$[${k}]`;
|
|
3450
|
+
const list = (items) => `[${items.map((i) => JSON.stringify(i)).join(",")}]`;
|
|
3451
|
+
if (propNames.length) lines.push(`const p=${list(propNames)};`);
|
|
3452
|
+
const props = propNames.length > 0;
|
|
3453
|
+
const t = [
|
|
3454
|
+
`ss:[${states.map((s) => names.get(s)).join(",")}]`,
|
|
3455
|
+
`i0:${val(tables.i0, "initial")}`,
|
|
3456
|
+
fields.length && `df:[${analysis.context.map(([, k]) => ref(k)).join(",")}]`,
|
|
3457
|
+
fields.length && `C:${use("storeClass")}(${list(fields)},1,!0)`,
|
|
3458
|
+
`pn:${props ? "p" : "[]"}`,
|
|
3459
|
+
props && "pd:[]",
|
|
3460
|
+
props && `Q:${use("storeClass")}(p,${1 + fields.length},!1)`,
|
|
3461
|
+
props && "pl:[]",
|
|
3462
|
+
`nm:${list([...fields, ...propNames])}`,
|
|
3463
|
+
analysis.derived.length && `D:${use("derivedClass")}(${list(analysis.derived.map(([d]) => d))},[${analysis.derived.map(([, k]) => ref(k)).join(",")}])`,
|
|
3464
|
+
"k:void 0",
|
|
3465
|
+
props && "bd:[]",
|
|
3466
|
+
"c",
|
|
3467
|
+
`r:(c)=>${options.name}($,c)`,
|
|
3468
|
+
effects && `A:${use("activate")}`,
|
|
3469
|
+
effects && `X:${use("release")}`,
|
|
3470
|
+
props && `P:${use("own")}`
|
|
3471
|
+
].filter(Boolean);
|
|
3472
|
+
lines.push(`const t={${t.join(",")}};`);
|
|
3473
|
+
if (options.dev) lines.push(`${use("devTables")}(t);`);
|
|
3474
|
+
if (propNames.length) lines.push(`${use("bind")}(t,{${analysis.props.map(([p, k]) => `${key(p)}:${ref(k)}`).join(",")}});`);
|
|
3475
|
+
if (states.some((s) => s.p)) lines.push(`${use("wireParallel")}(t);`);
|
|
3476
|
+
else if (states.some((s) => s.h !== void 0)) lines.push(`${use("wireHistory")}(t);`);
|
|
3477
|
+
lines.push("return t;");
|
|
3478
|
+
const c = [...analysis.c].map(([k, index]) => `${k}:${ref(index)}`).join(",");
|
|
3479
|
+
return `function ${options.name}($,c={${c}}){${lines.join("")}}`;
|
|
3480
|
+
}
|
|
3481
|
+
//#endregion
|
|
3482
|
+
//#region src/vite/compile/module.ts
|
|
3483
|
+
/**
|
|
3484
|
+
* Compiling the machines of one module (PLAN §5.6, D14, M7). Each top-level
|
|
3485
|
+
* `const x = defineMachine({ … })` whose structure is literal is checked by
|
|
3486
|
+
* the runtime normaliser at build time (its dev errors become build errors,
|
|
3487
|
+
* with the file, line and column) and replaced by a call that builds its
|
|
3488
|
+
* tables: `__machine0([…])`, the array holding the config's code where it
|
|
3489
|
+
* is written (functions, context and props values, parts…), so every other
|
|
3490
|
+
* line and column stays. The builders and their imports are appended to the
|
|
3491
|
+
* module. A config that is not literal stays a `defineMachine` call,
|
|
3492
|
+
* normalised at runtime, with a warning that says why.
|
|
3493
|
+
*/
|
|
3494
|
+
checkAlways();
|
|
3495
|
+
/** A machine's config the normaliser rejects: the build fails with it. */
|
|
3496
|
+
var CompileError = class extends Error {
|
|
3497
|
+
file;
|
|
3498
|
+
offset;
|
|
3499
|
+
line;
|
|
3500
|
+
column;
|
|
3501
|
+
constructor(file, at) {
|
|
3502
|
+
super(`${at.message}\n at ${file}:${at.line}:${at.column + 1}`);
|
|
3503
|
+
this.file = file;
|
|
3504
|
+
this.offset = at.offset;
|
|
3505
|
+
this.line = at.line;
|
|
3506
|
+
this.column = at.column;
|
|
3507
|
+
}
|
|
3508
|
+
};
|
|
3509
|
+
/** `code`'s machines compiled, or null when it has none to compile or warn about. */
|
|
3510
|
+
async function compileModule(code, file, options) {
|
|
3511
|
+
if (!code.includes("defineMachine")) return null;
|
|
3512
|
+
const { program, errors } = parseSync(file, code);
|
|
3513
|
+
if (errors.length) return null;
|
|
3514
|
+
const imports = /* @__PURE__ */ new Map();
|
|
3515
|
+
const values = /* @__PURE__ */ new Map();
|
|
3516
|
+
const sources = /* @__PURE__ */ new Set();
|
|
3517
|
+
for (const node of program.body) {
|
|
3518
|
+
if (node.type === "ImportDeclaration") {
|
|
3519
|
+
if (node.importKind === "type") continue;
|
|
3520
|
+
for (const specifier of node.specifiers) {
|
|
3521
|
+
if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type") continue;
|
|
3522
|
+
const name = specifier.imported.type === "Identifier" ? specifier.imported.name : String(specifier.imported.value);
|
|
3523
|
+
imports.set(specifier.local.name, [node.source.value, name]);
|
|
3524
|
+
sources.add(node.source.value);
|
|
3525
|
+
}
|
|
3526
|
+
continue;
|
|
3527
|
+
}
|
|
3528
|
+
const declaration = node.type === "ExportNamedDeclaration" ? node.declaration : node;
|
|
3529
|
+
if (declaration?.type === "FunctionDeclaration" && declaration.id) values.set(declaration.id.name, "function");
|
|
3530
|
+
if (declaration?.type === "VariableDeclaration" && declaration.kind === "const") {
|
|
3531
|
+
for (const { id, init } of declaration.declarations) if (id.type === "Identifier" && init) values.set(id.name, init);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
const ours = /* @__PURE__ */ new Set();
|
|
3535
|
+
for (const source of sources) if (await options.isPackage(source)) ours.add(source);
|
|
3536
|
+
const scope = {
|
|
3537
|
+
imports,
|
|
3538
|
+
values,
|
|
3539
|
+
isPackage: (source) => ours.has(source)
|
|
3540
|
+
};
|
|
3541
|
+
const define = [...imports].filter(([, [s, n]]) => ours.has(s) && n === "defineMachine");
|
|
3542
|
+
if (!define.length) return null;
|
|
3543
|
+
const defines = new Set(define.map(([local]) => local));
|
|
3544
|
+
const lines = [0];
|
|
3545
|
+
for (let i = code.indexOf("\n"); i >= 0; i = code.indexOf("\n", i + 1)) lines.push(i + 1);
|
|
3546
|
+
const at = (offset, message) => {
|
|
3547
|
+
let low = 0;
|
|
3548
|
+
let high = lines.length - 1;
|
|
3549
|
+
while (low < high) {
|
|
3550
|
+
const mid = low + high + 1 >> 1;
|
|
3551
|
+
if (lines[mid] <= offset) low = mid;
|
|
3552
|
+
else high = mid - 1;
|
|
3553
|
+
}
|
|
3554
|
+
return {
|
|
3555
|
+
message,
|
|
3556
|
+
offset,
|
|
3557
|
+
line: low + 1,
|
|
3558
|
+
column: offset - lines[low]
|
|
3559
|
+
};
|
|
3560
|
+
};
|
|
3561
|
+
let prefix = "__machine";
|
|
3562
|
+
while (code.includes(prefix)) prefix += "$";
|
|
3563
|
+
const used = /* @__PURE__ */ new Set();
|
|
3564
|
+
const runtime = (name) => (used.add(name), `${prefix}_${name}`);
|
|
3565
|
+
const s = new MagicString(code);
|
|
3566
|
+
const builders = [];
|
|
3567
|
+
const compiled = [];
|
|
3568
|
+
const warnings = [];
|
|
3569
|
+
for (const node of program.body) {
|
|
3570
|
+
const declaration = node.type === "ExportNamedDeclaration" ? node.declaration : node;
|
|
3571
|
+
if (declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") continue;
|
|
3572
|
+
for (const { id, init } of declaration.declarations) {
|
|
3573
|
+
const call = init && bare(init);
|
|
3574
|
+
if (id.type !== "Identifier" || call?.type !== "CallExpression" || call.callee.type !== "Identifier" || !defines.has(call.callee.name)) continue;
|
|
3575
|
+
const [config] = call.arguments;
|
|
3576
|
+
if (!config || config.type === "SpreadElement" || call.arguments.length !== 1) continue;
|
|
3577
|
+
let analysis;
|
|
3578
|
+
try {
|
|
3579
|
+
analysis = analyse(config, scope);
|
|
3580
|
+
} catch (error) {
|
|
3581
|
+
if (!(error instanceof NotLiteral)) throw error;
|
|
3582
|
+
warnings.push(at(error.node.start, `[machine:NOT_COMPILED] ${id.name}: ${error.message}, so this machine is normalised at runtime, as without the plugin. ${error.fix ?? "Write its states, targets and names out in the config to compile it."}`));
|
|
3583
|
+
continue;
|
|
3584
|
+
}
|
|
3585
|
+
let tables;
|
|
3586
|
+
try {
|
|
3587
|
+
tables = prepared(analysis.raw);
|
|
3588
|
+
} catch (error) {
|
|
3589
|
+
const message = error instanceof Error ? error.message : "";
|
|
3590
|
+
if (!message.startsWith("[machine:")) throw error;
|
|
3591
|
+
throw new CompileError(file, at(locate(message, analysis, config), message));
|
|
3592
|
+
}
|
|
3593
|
+
const name = `${prefix}${compiled.length}`;
|
|
3594
|
+
builders.push(emit(tables, analysis, {
|
|
3595
|
+
name,
|
|
3596
|
+
runtime,
|
|
3597
|
+
dev: options.dev,
|
|
3598
|
+
machine: analysis.raw.name ?? id.name
|
|
3599
|
+
}));
|
|
3600
|
+
replace(s, code, call, analysis.kept, `/* @__PURE__ */ ${name}([`, "])");
|
|
3601
|
+
compiled.push(id.name);
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
if (!compiled.length && !warnings.length) return null;
|
|
3605
|
+
if (compiled.length) {
|
|
3606
|
+
const names = [...used].sort().map((n) => `${n} as ${prefix}_${n}`);
|
|
3607
|
+
s.append(`\n${builders.join("\n")}\n`);
|
|
3608
|
+
if (names.length) s.append(`import { ${names.join(", ")} } from ${JSON.stringify(options.runtime)};\n`);
|
|
3609
|
+
}
|
|
3610
|
+
return {
|
|
3611
|
+
code: s.toString(),
|
|
3612
|
+
map: s.generateMap({
|
|
3613
|
+
hires: "boundary",
|
|
3614
|
+
source: file,
|
|
3615
|
+
includeContent: true
|
|
3616
|
+
}),
|
|
3617
|
+
compiled,
|
|
3618
|
+
warnings
|
|
3619
|
+
};
|
|
3620
|
+
}
|
|
3621
|
+
/**
|
|
3622
|
+
* Overwrites `call` around the nodes it keeps, so each stays where it is
|
|
3623
|
+
* written: `open` before the first, a comma between two, `close` after the
|
|
3624
|
+
* last. Each gap keeps its line breaks, so no line moves.
|
|
3625
|
+
*/
|
|
3626
|
+
function replace(s, code, call, kept, open, close) {
|
|
3627
|
+
const breaks = (from, to) => code.slice(from, to).replace(/[^\n]/g, "");
|
|
3628
|
+
let from = call.start;
|
|
3629
|
+
let text = open;
|
|
3630
|
+
for (const node of kept) {
|
|
3631
|
+
if (node.start < from) throw new Error("[machine:COMPILE] kept code out of order");
|
|
3632
|
+
s.overwrite(from, node.start, text + breaks(from, node.start));
|
|
3633
|
+
from = node.end;
|
|
3634
|
+
text = ",";
|
|
3635
|
+
}
|
|
3636
|
+
s.overwrite(from, call.end, (kept.length ? "" : open) + breaks(from, call.end) + close);
|
|
3637
|
+
}
|
|
3638
|
+
/**
|
|
3639
|
+
* Where a normaliser's message points in the config: the state it names
|
|
3640
|
+
* (its first quoted path, or the machine), then, inside that state's own
|
|
3641
|
+
* keys, a key or string it quotes after it.
|
|
3642
|
+
*/
|
|
3643
|
+
function locate(message, analysis, config) {
|
|
3644
|
+
const quoted = [...message.matchAll(/"([^"]*)"|`([^`]*)`/g)].flatMap((m) => m[1] !== void 0 ? [m[1]] : m[2].match(/[\w$]+/g) ?? []);
|
|
3645
|
+
const k = quoted.findIndex((q) => analysis.written.has(q));
|
|
3646
|
+
const state = k >= 0 ? quoted[k] : "";
|
|
3647
|
+
const written = analysis.written.get(state);
|
|
3648
|
+
const tokens = new Set(quoted.slice(k + 1));
|
|
3649
|
+
if (!written) return config.start;
|
|
3650
|
+
let found;
|
|
3651
|
+
const search = (node, top) => {
|
|
3652
|
+
if (!node || found !== void 0) return;
|
|
3653
|
+
const n = bare(node);
|
|
3654
|
+
if (n.type === "ObjectExpression") for (const property of n.properties) {
|
|
3655
|
+
if (property.type !== "Property") continue;
|
|
3656
|
+
const name = keyOf(property);
|
|
3657
|
+
if (top && name === "states") continue;
|
|
3658
|
+
if (name !== void 0 && tokens.has(name)) return void (found = property.start);
|
|
3659
|
+
search(property.value, false);
|
|
3660
|
+
if (found !== void 0) return;
|
|
3661
|
+
}
|
|
3662
|
+
else if (n.type === "ArrayExpression") {
|
|
3663
|
+
for (const element of n.elements) if (element && element.type !== "SpreadElement") search(element, false);
|
|
3664
|
+
} else if (n.type === "Literal" && typeof n.value === "string" && tokens.has(n.value)) found = n.start;
|
|
3665
|
+
};
|
|
3666
|
+
search(written.node, true);
|
|
3667
|
+
if (found === void 0 && state === "") for (const key of [
|
|
3668
|
+
"props",
|
|
3669
|
+
"actions",
|
|
3670
|
+
"guards",
|
|
3671
|
+
"effects"
|
|
3672
|
+
]) search(written.keys.get(key)?.value, false);
|
|
3673
|
+
return found ?? written.at;
|
|
3674
|
+
}
|
|
3675
|
+
//#endregion
|
|
3676
|
+
//#region src/vite/compile/plugin.ts
|
|
3677
|
+
function compilePlugin(options) {
|
|
3678
|
+
let dev = true;
|
|
3679
|
+
return {
|
|
3680
|
+
name: "machine:compile",
|
|
3681
|
+
enforce: "pre",
|
|
3682
|
+
configResolved(config) {
|
|
3683
|
+
dev = !config.isProduction;
|
|
3684
|
+
},
|
|
3685
|
+
transform: {
|
|
3686
|
+
filter: {
|
|
3687
|
+
id: {
|
|
3688
|
+
include: /\.[cm]?[jt]sx?(?:\?|$)/,
|
|
3689
|
+
exclude: /\/node_modules\//
|
|
3690
|
+
},
|
|
3691
|
+
code: "defineMachine"
|
|
3692
|
+
},
|
|
3693
|
+
async handler(code, id) {
|
|
3694
|
+
if (id.startsWith("\0") || options.include && !options.include(id)) return null;
|
|
3695
|
+
const isPackage = async (source) => source === "@menapo/machine" || source.startsWith(".") && (await this.resolve(source, id))?.id === options.index;
|
|
3696
|
+
try {
|
|
3697
|
+
const out = await compileModule(code, id.split("?")[0], {
|
|
3698
|
+
runtime: options.runtime,
|
|
3699
|
+
dev,
|
|
3700
|
+
isPackage
|
|
3701
|
+
});
|
|
3702
|
+
if (!out) return null;
|
|
3703
|
+
for (const warning of out.warnings) this.warn(warning.message, warning.offset);
|
|
3704
|
+
return out.compiled.length ? {
|
|
3705
|
+
code: out.code,
|
|
3706
|
+
map: out.map
|
|
3707
|
+
} : null;
|
|
3708
|
+
} catch (error) {
|
|
3709
|
+
if (error instanceof CompileError) this.error(error.message, error.offset);
|
|
3710
|
+
throw error;
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
};
|
|
3715
|
+
}
|
|
3716
|
+
//#endregion
|
|
3717
|
+
//#region src/vite/config.ts
|
|
3718
|
+
/** The package's entries an app's code imports (`/vite` runs in Vite itself). */
|
|
3719
|
+
const ENTRIES = [
|
|
3720
|
+
"@menapo/machine",
|
|
3721
|
+
"@menapo/machine/compiled",
|
|
3722
|
+
"@menapo/machine/dom",
|
|
3723
|
+
"@menapo/machine/test",
|
|
3724
|
+
"@menapo/machine/vicss"
|
|
3725
|
+
];
|
|
3726
|
+
/**
|
|
3727
|
+
* `machine:config`: every entry served as its own files, never pre-bundled,
|
|
3728
|
+
* and bundled into server builds, never left to Node. The glue modules the
|
|
3729
|
+
* plugin resolves `useMachine` to import the runtime's files directly; a
|
|
3730
|
+
* pre-bundled or external copy of the entries beside them would be a
|
|
3731
|
+
* second runtime, whose instances the first does not know (a context write
|
|
3732
|
+
* would throw, parts would stop tracking, `inspect` would see nothing).
|
|
3733
|
+
*/
|
|
3734
|
+
const configPlugin = () => ({
|
|
3735
|
+
name: "machine:config",
|
|
3736
|
+
config: () => ({ optimizeDeps: { exclude: ENTRIES } }),
|
|
3737
|
+
configEnvironment(name, config) {
|
|
3738
|
+
return (config.consumer ?? (name === "client" ? "client" : "server")) === "server" ? { resolve: { noExternal: ["@menapo/machine"] } } : void 0;
|
|
3739
|
+
}
|
|
3740
|
+
});
|
|
3741
|
+
//#endregion
|
|
3742
|
+
//#region ../glue/src/devtools.ts
|
|
3743
|
+
/** A panel: an iframe of the page served at `<base><path>`. */
|
|
3744
|
+
const dockEntry$1 = (panel, base, path) => ({
|
|
3745
|
+
...panel,
|
|
3746
|
+
category: "framework",
|
|
3747
|
+
type: "iframe",
|
|
3748
|
+
url: base + path
|
|
3749
|
+
});
|
|
3750
|
+
/** Serves `page()` at `<anything>/<path>` while `on()` holds; other requests pass. */
|
|
3751
|
+
function serveDock(server, path, on, page) {
|
|
3752
|
+
server.middlewares.use((req, res, next) => {
|
|
3753
|
+
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
3754
|
+
if (!on() || req.method !== "GET" || !pathname.endsWith(`/${path}`)) return next();
|
|
3755
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
3756
|
+
res.setHeader("cache-control", "no-store");
|
|
3757
|
+
res.end(page());
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
/**
|
|
3761
|
+
* The page's bridge as the panel finds it: the global `name` on the nearest
|
|
3762
|
+
* ancestor window that has it (Vite DevTools may nest the iframe). A panel
|
|
3763
|
+
* page runs from its own source text, so it is handed this function's
|
|
3764
|
+
* source (`${findBridge}`) rather than importing it.
|
|
3765
|
+
*/
|
|
3766
|
+
function findBridge(name) {
|
|
3767
|
+
for (let w = window.parent;; w = w.parent) {
|
|
3768
|
+
try {
|
|
3769
|
+
const found = w[name];
|
|
3770
|
+
if (found) return found;
|
|
3771
|
+
} catch {}
|
|
3772
|
+
if (w === w.parent) return void 0;
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
//#endregion
|
|
3776
|
+
//#region src/vite/devtools/protocol.ts
|
|
3777
|
+
/** The dock page Vite DevTools embeds, under the dev server's `base`. */
|
|
3778
|
+
const DOCK_PATH = "__machine/dock";
|
|
3779
|
+
/** The page module the plugin adds to every client module that imports `@menapo/machine`. */
|
|
3780
|
+
const PAGE_ID = "virtual:machine/devtools";
|
|
3781
|
+
/**
|
|
3782
|
+
* The page global through which the dock, a same-origin iframe, reaches the
|
|
3783
|
+
* page's machines: it is a document of its own, with no module graph.
|
|
3784
|
+
*/
|
|
3785
|
+
const BRIDGE = "__machineDevtools";
|
|
3786
|
+
//#endregion
|
|
3787
|
+
//#region src/vite/devtools/dock.ts
|
|
3788
|
+
/**
|
|
3789
|
+
* The Vite DevTools dock panel (PLAN §5.8): one self-contained document the
|
|
3790
|
+
* dev server serves at `<base>__machine/dock`, embedded by Vite DevTools as
|
|
3791
|
+
* a same-origin iframe of the app's page, as vi18n's dock is. It finds the
|
|
3792
|
+
* page's {@link Bridge} on an ancestor window and shows, for the instance
|
|
3793
|
+
* chosen:
|
|
3794
|
+
*
|
|
3795
|
+
* - its current state and its context and props;
|
|
3796
|
+
* - the events it took, newest first, each with the fields it changed;
|
|
3797
|
+
* - a form that sends it one of the events its state handles;
|
|
3798
|
+
* - its states as a read-only tree: the active ones marked, each state's own
|
|
3799
|
+
* transitions, `after` timers and `always` rules, each linked to where it
|
|
3800
|
+
* is written when the plugin found its source.
|
|
3801
|
+
*
|
|
3802
|
+
* `dock` runs from its own source text in the page, so it uses nothing but
|
|
3803
|
+
* its arguments and globals (the bridge lookup is `@menapo/glue`'s, handed
|
|
3804
|
+
* in as source); it reads the core's tables, typed by the core.
|
|
3805
|
+
*/
|
|
3806
|
+
function dock(global, find) {
|
|
3807
|
+
const bridge = find(global);
|
|
3808
|
+
const $ = (id) => document.getElementById(id);
|
|
3809
|
+
const h = (tag, props = {}, ...children) => {
|
|
3810
|
+
const element = Object.assign(document.createElement(tag), props);
|
|
3811
|
+
for (const child of children) if (child) element.append(child);
|
|
3812
|
+
return element;
|
|
3813
|
+
};
|
|
3814
|
+
const format = (value) => {
|
|
3815
|
+
let text;
|
|
3816
|
+
try {
|
|
3817
|
+
text = typeof value === "function" ? "ƒ" : JSON.stringify(value) ?? String(value);
|
|
3818
|
+
} catch {
|
|
3819
|
+
text = String(value);
|
|
3820
|
+
}
|
|
3821
|
+
return text.length > 80 ? `${text.slice(0, 79)}…` : text;
|
|
3822
|
+
};
|
|
3823
|
+
const link = (source, at) => {
|
|
3824
|
+
if (!source || !at) return null;
|
|
3825
|
+
const where = `${source.file}:${at[0]}:${at[1]}`;
|
|
3826
|
+
return h("a", {
|
|
3827
|
+
className: "source",
|
|
3828
|
+
textContent: "source",
|
|
3829
|
+
title: where,
|
|
3830
|
+
href: `../__open-in-editor?file=${encodeURIComponent(where)}`,
|
|
3831
|
+
onclick(event) {
|
|
3832
|
+
event.preventDefault();
|
|
3833
|
+
fetch(this.href);
|
|
3834
|
+
}
|
|
3835
|
+
});
|
|
3836
|
+
};
|
|
3837
|
+
const nameOf = ({ instance, source }) => instance.M.name !== "machine" ? instance.M.name : source?.binding ?? "machine";
|
|
3838
|
+
if (!bridge) {
|
|
3839
|
+
$("empty").textContent = "No machines here: open this panel from the app's page, where Vite DevTools shows it.";
|
|
3840
|
+
return;
|
|
3841
|
+
}
|
|
3842
|
+
let chosen = 0;
|
|
3843
|
+
let current;
|
|
3844
|
+
const instances = $("instances");
|
|
3845
|
+
const type = $("type");
|
|
3846
|
+
const data = $("payload");
|
|
3847
|
+
const error = $("error");
|
|
3848
|
+
/** A state's own transitions, `after` timers and `always` rules, as list items. */
|
|
3849
|
+
const transitions = (state, up, source) => {
|
|
3850
|
+
const own = source?.states[state.path];
|
|
3851
|
+
const items = [];
|
|
3852
|
+
const add = (label, rules, at) => {
|
|
3853
|
+
for (const rule of rules.flatMap((r) => r.w ?? [r])) {
|
|
3854
|
+
const to = rule.h ?? rule.to;
|
|
3855
|
+
const target = to ? to.path : "(stays)";
|
|
3856
|
+
items.push(h("li", {}, h("code", { textContent: label }), ` → ${target}`, rule.if ? " if …" : "", " ", link(source, at)));
|
|
3857
|
+
}
|
|
3858
|
+
};
|
|
3859
|
+
for (const [event, handler] of state.on) {
|
|
3860
|
+
if (up?.on.get(event) === handler) continue;
|
|
3861
|
+
const inherited = up?.on.get(event)?.ru.length ?? 0;
|
|
3862
|
+
add(event, handler.ru.slice(0, handler.ru.length - inherited), own?.on[event]);
|
|
3863
|
+
}
|
|
3864
|
+
for (const [ms, rules] of state.af ?? []) add(`after ${ms} ms`, rules, own?.after[ms]);
|
|
3865
|
+
if (state.al && state.al !== up?.al) {
|
|
3866
|
+
const inherited = up?.al?.length ?? 0;
|
|
3867
|
+
add("always", state.al.slice(0, state.al.length - inherited), own?.always);
|
|
3868
|
+
}
|
|
3869
|
+
return items.length ? h("ul", { className: "transitions" }, ...items) : null;
|
|
3870
|
+
};
|
|
3871
|
+
const graph = ({ instance, source }) => {
|
|
3872
|
+
const tree = h("ul");
|
|
3873
|
+
const lists = /* @__PURE__ */ new Map();
|
|
3874
|
+
const root = instance.M.ss[0]?.up;
|
|
3875
|
+
const anywhere = root && transitions(root, void 0, source);
|
|
3876
|
+
const chain = [];
|
|
3877
|
+
for (let at = instance.s; at; at = at.up) chain.push(at);
|
|
3878
|
+
const actives = new Set(instance.s.a ?? chain);
|
|
3879
|
+
for (const state of instance.M.ss) {
|
|
3880
|
+
const active = actives.has(state);
|
|
3881
|
+
const leaf = active && !instance.M.ss.some((s) => s.up === state && actives.has(s));
|
|
3882
|
+
const parent = state.up === root ? tree : lists.get(state.up);
|
|
3883
|
+
const name = state.path.slice(state.path.lastIndexOf(".") + 1);
|
|
3884
|
+
const item = h("li", {}, h("span", {
|
|
3885
|
+
className: active ? "state active" : "state",
|
|
3886
|
+
textContent: name + (state.fi ? " (final)" : ""),
|
|
3887
|
+
ariaCurrent: leaf ? "true" : null
|
|
3888
|
+
}), " ", link(source, source?.states[state.path]?.at), transitions(state, state.up, source));
|
|
3889
|
+
parent.append(item);
|
|
3890
|
+
const children = h("ul");
|
|
3891
|
+
lists.set(state, children);
|
|
3892
|
+
item.append(children);
|
|
3893
|
+
}
|
|
3894
|
+
for (const list of lists.values()) if (!list.childElementCount) list.remove();
|
|
3895
|
+
return h("div", {}, anywhere && h("div", {}, h("span", {
|
|
3896
|
+
className: "muted",
|
|
3897
|
+
textContent: "In any state"
|
|
3898
|
+
}), anywhere), tree);
|
|
3899
|
+
};
|
|
3900
|
+
const render = () => {
|
|
3901
|
+
queued = false;
|
|
3902
|
+
const live = bridge.live();
|
|
3903
|
+
current = live.find((l) => l.n === chosen) ?? live[0];
|
|
3904
|
+
chosen = current?.n ?? 0;
|
|
3905
|
+
instances.replaceChildren(...live.map((l) => h("option", {
|
|
3906
|
+
value: String(l.n),
|
|
3907
|
+
textContent: `${nameOf(l)} #${l.n}`,
|
|
3908
|
+
selected: l === current
|
|
3909
|
+
})));
|
|
3910
|
+
$("main").hidden = !current;
|
|
3911
|
+
$("empty").textContent = current ? "" : "No machine is running on this page.";
|
|
3912
|
+
if (!current) return;
|
|
3913
|
+
const { instance, steps } = current;
|
|
3914
|
+
$("state").textContent = instance.s.path;
|
|
3915
|
+
const { nm: names, pn: propNames } = instance.M;
|
|
3916
|
+
const fields = names.length - propNames.length;
|
|
3917
|
+
$("data").replaceChildren(...names.map((name, n) => {
|
|
3918
|
+
const store = n < fields ? instance.context : instance.props;
|
|
3919
|
+
return h("tr", {}, h("th", {
|
|
3920
|
+
scope: "row",
|
|
3921
|
+
textContent: name
|
|
3922
|
+
}), h("td", {
|
|
3923
|
+
className: "muted",
|
|
3924
|
+
textContent: n < fields ? "context" : "prop"
|
|
3925
|
+
}), h("td", {}, h("code", { textContent: format(store[name]) })));
|
|
3926
|
+
}));
|
|
3927
|
+
const events = [...instance.s.on.keys()];
|
|
3928
|
+
if (events.join("\0") !== [...type.options].map((o) => o.value).join("\0")) type.replaceChildren(...events.map((e) => h("option", {
|
|
3929
|
+
value: e,
|
|
3930
|
+
textContent: e
|
|
3931
|
+
})));
|
|
3932
|
+
$("submit").disabled = !events.length;
|
|
3933
|
+
$("log").replaceChildren(...steps.slice().reverse().map(({ event, state, changed }) => {
|
|
3934
|
+
const { type: name, ...payload } = event;
|
|
3935
|
+
return h("tr", {}, h("td", {}, h("code", { textContent: name }), Object.keys(payload).length ? ` ${format(payload)}` : ""), h("td", { textContent: state }), h("td", {}, ...Object.entries(changed).map(([field, value]) => h("div", {}, h("code", { textContent: `${field} = ${format(value)}` })))));
|
|
3936
|
+
}));
|
|
3937
|
+
$("graph").replaceChildren(graph(current));
|
|
3938
|
+
};
|
|
3939
|
+
let queued = false;
|
|
3940
|
+
const schedule = () => {
|
|
3941
|
+
if (!queued) {
|
|
3942
|
+
queued = true;
|
|
3943
|
+
setTimeout(render, 0);
|
|
3944
|
+
}
|
|
3945
|
+
};
|
|
3946
|
+
instances.onchange = () => {
|
|
3947
|
+
chosen = Number(instances.value);
|
|
3948
|
+
error.value = "";
|
|
3949
|
+
render();
|
|
3950
|
+
};
|
|
3951
|
+
$("send").onsubmit = (event) => {
|
|
3952
|
+
event.preventDefault();
|
|
3953
|
+
error.value = "";
|
|
3954
|
+
try {
|
|
3955
|
+
const payload = data.value.trim() ? JSON.parse(data.value) : void 0;
|
|
3956
|
+
if (payload !== void 0 && (typeof payload !== "object" || !payload || Array.isArray(payload))) throw new Error("the data is an object of the event's fields, like {\"by\": 2}");
|
|
3957
|
+
current?.instance.send(type.value, payload);
|
|
3958
|
+
} catch (thrown) {
|
|
3959
|
+
error.value = thrown instanceof Error ? thrown.message : String(thrown);
|
|
3960
|
+
}
|
|
3961
|
+
render();
|
|
3962
|
+
};
|
|
3963
|
+
const stop = bridge.subscribe(schedule);
|
|
3964
|
+
addEventListener("pagehide", stop);
|
|
3965
|
+
render();
|
|
3966
|
+
}
|
|
3967
|
+
const STYLE = `
|
|
3968
|
+
:root { color-scheme: light dark; font: 13px/1.4 system-ui, sans-serif; }
|
|
3969
|
+
body { margin: 0; padding: 8px 12px; background: Canvas; color: CanvasText; }
|
|
3970
|
+
header, form { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
3971
|
+
h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; margin: 16px 0 4px; opacity: .8; }
|
|
3972
|
+
table { width: 100%; border-collapse: collapse; }
|
|
3973
|
+
th, td { padding: 3px 6px; border-top: 1px solid #8883; text-align: left; vertical-align: top; }
|
|
3974
|
+
th[scope=row] { font-weight: 500; }
|
|
3975
|
+
code { font: 12px ui-monospace, monospace; }
|
|
3976
|
+
ul { margin: 0; padding-left: 16px; list-style: none; }
|
|
3977
|
+
.transitions { padding-left: 12px; opacity: .85; }
|
|
3978
|
+
.state.active { font-weight: 700; }
|
|
3979
|
+
.state.active::after { content: " ●"; }
|
|
3980
|
+
#data td:last-child { width: 100%; }
|
|
3981
|
+
.muted, .source { opacity: .7; }
|
|
3982
|
+
input { flex: 1; min-width: 120px; font: inherit; }
|
|
3983
|
+
output { color: #d33; }
|
|
3984
|
+
`;
|
|
3985
|
+
/** The dock's page: one self-contained document. */
|
|
3986
|
+
function dockPage() {
|
|
3987
|
+
return `<!doctype html>
|
|
3988
|
+
<html lang="en">
|
|
3989
|
+
<head><meta charset="utf-8"><title>Machines</title><style>${STYLE}</style></head>
|
|
3990
|
+
<body>
|
|
3991
|
+
<p id="empty" role="status"></p>
|
|
3992
|
+
<main id="main" hidden>
|
|
3993
|
+
<header>
|
|
3994
|
+
<label>Machine <select id="instances"></select></label>
|
|
3995
|
+
<span>State <b id="state"></b></span>
|
|
3996
|
+
</header>
|
|
3997
|
+
<h2 id="h-data">Context and props</h2>
|
|
3998
|
+
<table aria-labelledby="h-data"><tbody id="data"></tbody></table>
|
|
3999
|
+
<h2 id="h-send">Send</h2>
|
|
4000
|
+
<form id="send" aria-labelledby="h-send">
|
|
4001
|
+
<select id="type" aria-label="Event"></select>
|
|
4002
|
+
<input id="payload" aria-label="Event data, as JSON" placeholder='{"by": 2}' autocomplete="off">
|
|
4003
|
+
<button id="submit">Send</button>
|
|
4004
|
+
<output id="error" role="alert"></output>
|
|
4005
|
+
</form>
|
|
4006
|
+
<h2 id="h-log">Events</h2>
|
|
4007
|
+
<table aria-labelledby="h-log">
|
|
4008
|
+
<thead><tr><th scope="col">Event</th><th scope="col">State after</th><th scope="col">Changed</th></tr></thead>
|
|
4009
|
+
<tbody id="log"></tbody>
|
|
4010
|
+
</table>
|
|
4011
|
+
<h2 id="h-graph">States</h2>
|
|
4012
|
+
<div id="graph" role="region" aria-labelledby="h-graph"></div>
|
|
4013
|
+
</main>
|
|
4014
|
+
<script type="module">(${dock.toString()})(${JSON.stringify(BRIDGE)}, ${findBridge.toString()});<\/script>
|
|
4015
|
+
</body>
|
|
4016
|
+
</html>
|
|
4017
|
+
`;
|
|
4018
|
+
}
|
|
4019
|
+
//#endregion
|
|
4020
|
+
//#region src/vite/devtools/page.ts
|
|
4021
|
+
/** Returns the function the plugin's transform registers each machine's source with. */
|
|
4022
|
+
function page(inspect, global, keep, limit) {
|
|
4023
|
+
const entries = [];
|
|
4024
|
+
const byInstance = /* @__PURE__ */ new WeakMap();
|
|
4025
|
+
const sources = /* @__PURE__ */ new WeakMap();
|
|
4026
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4027
|
+
let count = 0;
|
|
4028
|
+
/** Drops stopped (2) and collected instances: gone from the page, so from the panel. */
|
|
4029
|
+
const prune = () => {
|
|
4030
|
+
for (let k = 0; k < entries.length; k++) {
|
|
4031
|
+
const instance = entries[k].ref.deref();
|
|
4032
|
+
if (!instance || instance.b === 2) entries.splice(k--, 1);
|
|
4033
|
+
}
|
|
4034
|
+
};
|
|
4035
|
+
inspect(({ instance, event, state, changed }) => {
|
|
4036
|
+
let entry = byInstance.get(instance);
|
|
4037
|
+
if (!entry) {
|
|
4038
|
+
entry = {
|
|
4039
|
+
n: ++count,
|
|
4040
|
+
ref: new WeakRef(instance),
|
|
4041
|
+
steps: []
|
|
4042
|
+
};
|
|
4043
|
+
byInstance.set(instance, entry);
|
|
4044
|
+
if (entries.push(entry) > limit) {
|
|
4045
|
+
prune();
|
|
4046
|
+
if (entries.length > limit) entries.splice(0, entries.length - limit);
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
if (entry.steps.push({
|
|
4050
|
+
event,
|
|
4051
|
+
state,
|
|
4052
|
+
changed
|
|
4053
|
+
}) > keep) entry.steps.shift();
|
|
4054
|
+
for (const listener of listeners) listener();
|
|
4055
|
+
});
|
|
4056
|
+
globalThis[global] = {
|
|
4057
|
+
live() {
|
|
4058
|
+
const live = [];
|
|
4059
|
+
prune();
|
|
4060
|
+
for (const { n, ref, steps } of entries) {
|
|
4061
|
+
const instance = ref.deref();
|
|
4062
|
+
live.push({
|
|
4063
|
+
n,
|
|
4064
|
+
instance,
|
|
4065
|
+
steps,
|
|
4066
|
+
source: sources.get(instance.M)
|
|
4067
|
+
});
|
|
4068
|
+
}
|
|
4069
|
+
return live;
|
|
4070
|
+
},
|
|
4071
|
+
subscribe(listener) {
|
|
4072
|
+
listeners.add(listener);
|
|
4073
|
+
return () => void listeners.delete(listener);
|
|
4074
|
+
}
|
|
4075
|
+
};
|
|
4076
|
+
return (machine, source) => void sources.set(machine, source);
|
|
4077
|
+
}
|
|
4078
|
+
//#endregion
|
|
4079
|
+
//#region src/vite/devtools/sources.ts
|
|
4080
|
+
/**
|
|
4081
|
+
* Where machines are written, for the panel's links to each state and
|
|
4082
|
+
* transition (PLAN §5.8). In dev with Vite DevTools on, every client module
|
|
4083
|
+
* that imports `@menapo/machine` gets two things appended, so no line or
|
|
4084
|
+
* column of its own code moves:
|
|
4085
|
+
*
|
|
4086
|
+
* - an import of the page module, which then runs before the module does,
|
|
4087
|
+
* so the panel sees every instance from the first one started;
|
|
4088
|
+
* - for each `const x = defineMachine({ … })` at the top level with a
|
|
4089
|
+
* literal config, a call that registers `x`'s source: the file, and per
|
|
4090
|
+
* state path where the state, each `on` event, each `after` delay and its
|
|
4091
|
+
* `always` are written.
|
|
4092
|
+
*
|
|
4093
|
+
* A config built by helpers is registered as far as it is literal, as M7's
|
|
4094
|
+
* compilation will take it (D14).
|
|
4095
|
+
*/
|
|
4096
|
+
const PACKAGE = "@menapo/machine";
|
|
4097
|
+
/** A literal object's properties by key; computed keys and spreads are skipped. */
|
|
4098
|
+
function properties(node) {
|
|
4099
|
+
const map = /* @__PURE__ */ new Map();
|
|
4100
|
+
if (node?.type !== "ObjectExpression") return map;
|
|
4101
|
+
for (const property of node.properties) {
|
|
4102
|
+
if (property.type !== "Property" || property.computed) continue;
|
|
4103
|
+
const { key } = property;
|
|
4104
|
+
if (key.type === "Identifier") map.set(key.name, property);
|
|
4105
|
+
else if (key.type === "Literal" && (typeof key.value === "string" || typeof key.value === "number")) map.set(String(key.value), property);
|
|
4106
|
+
}
|
|
4107
|
+
return map;
|
|
4108
|
+
}
|
|
4109
|
+
/** `code` with the page module's import and its machines' sources appended, or null when it imports nothing from the package. */
|
|
4110
|
+
function withSources(code, file) {
|
|
4111
|
+
const { program, errors } = parseSync(file, code);
|
|
4112
|
+
if (errors.length) return null;
|
|
4113
|
+
const define = /* @__PURE__ */ new Set();
|
|
4114
|
+
let imports = false;
|
|
4115
|
+
for (const node of program.body) {
|
|
4116
|
+
if (node.type !== "ImportDeclaration" || node.source.value !== PACKAGE) continue;
|
|
4117
|
+
imports = true;
|
|
4118
|
+
for (const specifier of node.specifiers) if (specifier.type === "ImportSpecifier" && (specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value) === "defineMachine") define.add(specifier.local.name);
|
|
4119
|
+
}
|
|
4120
|
+
if (!imports) return null;
|
|
4121
|
+
const lines = [0];
|
|
4122
|
+
for (let i = code.indexOf("\n"); i >= 0; i = code.indexOf("\n", i + 1)) lines.push(i + 1);
|
|
4123
|
+
const loc = (offset) => {
|
|
4124
|
+
let low = 0;
|
|
4125
|
+
let high = lines.length - 1;
|
|
4126
|
+
while (low < high) {
|
|
4127
|
+
const mid = low + high + 1 >> 1;
|
|
4128
|
+
if (lines[mid] <= offset) low = mid;
|
|
4129
|
+
else high = mid - 1;
|
|
4130
|
+
}
|
|
4131
|
+
return [low + 1, offset - lines[low] + 1];
|
|
4132
|
+
};
|
|
4133
|
+
const keys = (node) => {
|
|
4134
|
+
const out = {};
|
|
4135
|
+
for (const [key, property] of properties(node)) out[key] = loc(property.start);
|
|
4136
|
+
return out;
|
|
4137
|
+
};
|
|
4138
|
+
let out = `\nimport { source as __machineSource } from ${JSON.stringify(PAGE_ID)};\n`;
|
|
4139
|
+
for (const statement of program.body) {
|
|
4140
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
4141
|
+
if (declaration?.type !== "VariableDeclaration") continue;
|
|
4142
|
+
for (const { id, init } of declaration.declarations) {
|
|
4143
|
+
if (id.type !== "Identifier" || init?.type !== "CallExpression" || init.callee.type !== "Identifier" || !define.has(init.callee.name) || init.arguments[0]?.type !== "ObjectExpression") continue;
|
|
4144
|
+
const config = properties(init.arguments[0]);
|
|
4145
|
+
const states = { "": {
|
|
4146
|
+
at: loc(init.start),
|
|
4147
|
+
on: keys(config.get("on")?.value),
|
|
4148
|
+
after: {}
|
|
4149
|
+
} };
|
|
4150
|
+
const walk = (node, prefix) => {
|
|
4151
|
+
for (const [key, property] of properties(node)) {
|
|
4152
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
4153
|
+
const state = properties(property.value);
|
|
4154
|
+
const always = state.get("always");
|
|
4155
|
+
states[path] = {
|
|
4156
|
+
at: loc(property.start),
|
|
4157
|
+
on: keys(state.get("on")?.value),
|
|
4158
|
+
after: keys(state.get("after")?.value),
|
|
4159
|
+
...always ? { always: loc(always.start) } : {}
|
|
4160
|
+
};
|
|
4161
|
+
walk(state.get("states")?.value, path);
|
|
4162
|
+
}
|
|
4163
|
+
};
|
|
4164
|
+
walk(config.get("states")?.value, "");
|
|
4165
|
+
const source = {
|
|
4166
|
+
file,
|
|
4167
|
+
binding: id.name,
|
|
4168
|
+
states
|
|
4169
|
+
};
|
|
4170
|
+
out += `__machineSource(${id.name}, ${JSON.stringify(source)});\n`;
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
return code + out;
|
|
4174
|
+
}
|
|
4175
|
+
//#endregion
|
|
4176
|
+
//#region src/vite/devtools/plugin.ts
|
|
4177
|
+
/**
|
|
4178
|
+
* `machine:devtools` (PLAN §5.8): the panel in the Vite DevTools dock, by
|
|
4179
|
+
* the mechanism vi18n's dock uses. Vite DevTools calls a plugin's
|
|
4180
|
+
* `devtools.setup` only when the user turned it on and installed
|
|
4181
|
+
* `@vitejs/devtools`, so the plugin imports nothing from it and, until
|
|
4182
|
+
* `setup` runs, does nothing: no page module, no transform, no route.
|
|
4183
|
+
*
|
|
4184
|
+
* Once on, in dev only:
|
|
4185
|
+
*
|
|
4186
|
+
* - `setup` registers the dock: an iframe of `<base>__machine/dock`, served
|
|
4187
|
+
* here as one self-contained document (`dock.ts`);
|
|
4188
|
+
* - every client module that imports `@menapo/machine` imports the page
|
|
4189
|
+
* module (`virtual:machine/devtools`), which watches instances through
|
|
4190
|
+
* `inspect` (D17) and puts the bridge the dock reads on the page, and
|
|
4191
|
+
* registers its machines' sources (`sources.ts`).
|
|
4192
|
+
*
|
|
4193
|
+
* A build never runs this plugin, so production bundles import neither the
|
|
4194
|
+
* page module nor `inspect`, and the core's hook calls drop out with them.
|
|
4195
|
+
*/
|
|
4196
|
+
const dockEntry = (base) => dockEntry$1({
|
|
4197
|
+
id: "machine",
|
|
4198
|
+
title: "Machines",
|
|
4199
|
+
icon: "ph:graph-duotone"
|
|
4200
|
+
}, base, DOCK_PATH);
|
|
4201
|
+
/** The page module: the app's own `inspect`, handed to {@link page}'s source. */
|
|
4202
|
+
const pageModule = () => `import { inspect } from "@menapo/machine";\nexport const source = (${page.toString()})(inspect, ${JSON.stringify(BRIDGE)}, 100, 500);\n`;
|
|
4203
|
+
const RESOLVED = "\0" + PAGE_ID;
|
|
4204
|
+
function devtoolsPlugin() {
|
|
4205
|
+
let on = false;
|
|
4206
|
+
return {
|
|
4207
|
+
name: "machine:devtools",
|
|
4208
|
+
apply: "serve",
|
|
4209
|
+
enforce: "pre",
|
|
4210
|
+
devtools: { setup(context) {
|
|
4211
|
+
on = true;
|
|
4212
|
+
context.docks.register(dockEntry(context.viteConfig.base));
|
|
4213
|
+
} },
|
|
4214
|
+
configureServer(server) {
|
|
4215
|
+
serveDock(server, DOCK_PATH, () => on, dockPage);
|
|
4216
|
+
},
|
|
4217
|
+
resolveId: {
|
|
4218
|
+
filter: { id: /^virtual:machine\/devtools$/ },
|
|
4219
|
+
handler: (id) => "\0" + id
|
|
4220
|
+
},
|
|
4221
|
+
load: {
|
|
4222
|
+
filter: { id: /^\0virtual:machine\/devtools$/ },
|
|
4223
|
+
handler: (id) => id === RESOLVED ? {
|
|
4224
|
+
code: pageModule(),
|
|
4225
|
+
moduleType: "js"
|
|
4226
|
+
} : null
|
|
4227
|
+
},
|
|
4228
|
+
transform: {
|
|
4229
|
+
filter: {
|
|
4230
|
+
id: {
|
|
4231
|
+
include: /\.[cm]?[jt]sx?(?:\?|$)/,
|
|
4232
|
+
exclude: /\/node_modules\//
|
|
4233
|
+
},
|
|
4234
|
+
code: "@menapo/machine"
|
|
4235
|
+
},
|
|
4236
|
+
handler(code, id) {
|
|
4237
|
+
if (!on || this.environment.config.consumer !== "client") return null;
|
|
4238
|
+
const out = withSources(code, id.split("?")[0]);
|
|
4239
|
+
return out === null ? null : {
|
|
4240
|
+
code: out,
|
|
4241
|
+
map: null
|
|
4242
|
+
};
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
4247
|
+
//#endregion
|
|
4248
|
+
//#region src/vite.ts
|
|
4249
|
+
/**
|
|
4250
|
+
* `@menapo/machine/vite`: the plugin that resolves `useMachine` per
|
|
4251
|
+
* framework, validates and precompiles machines, and adds the DevTools panel
|
|
4252
|
+
* (PLAN §5.5, §5.6, §5.8). The only entry that may import from the build side.
|
|
4253
|
+
*
|
|
4254
|
+
* M3 brought the resolution: each module's `useMachine` comes from the glue
|
|
4255
|
+
* of its framework (`src/glue/`), on `@menapo/glue`'s adapters. M6 brought
|
|
4256
|
+
* the DevTools panel, M7 the compilation of literal machines (`compile/`).
|
|
4257
|
+
*/
|
|
4258
|
+
const GLUE = posix(fileURLToPath(new URL("./glue/", import.meta.url)));
|
|
4259
|
+
const EXT = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
|
|
4260
|
+
const INDEX = posix(fileURLToPath(new URL("./index" + EXT, import.meta.url)));
|
|
4261
|
+
/** `plugins: [machine()]` in a Vite config. */
|
|
4262
|
+
function machine(options = {}) {
|
|
4263
|
+
return [
|
|
4264
|
+
configPlugin(),
|
|
4265
|
+
devtoolsPlugin(),
|
|
4266
|
+
...options.compile === false ? [] : [compilePlugin({
|
|
4267
|
+
runtime: "@menapo/machine/compiled",
|
|
4268
|
+
index: INDEX
|
|
4269
|
+
})],
|
|
4270
|
+
gluePlugin({
|
|
4271
|
+
name: "machine:glue",
|
|
4272
|
+
package: "@menapo/machine",
|
|
4273
|
+
hooks: ["useMachine"],
|
|
4274
|
+
module: (framework) => GLUE + (framework === "svelte" ? "svelte.svelte" : framework) + EXT,
|
|
4275
|
+
dirs: [GLUE],
|
|
4276
|
+
framework: options.framework
|
|
4277
|
+
})
|
|
4278
|
+
];
|
|
4279
|
+
}
|
|
4280
|
+
//#endregion
|
|
4281
|
+
export { machine };
|