ferrovue 0.3.0 → 0.4.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/README.md +32 -13
- package/dist/attrs.d.ts +2 -52
- package/dist/attrs.js +18 -448
- package/dist/calls.d.ts +4 -0
- package/dist/calls.js +219 -0
- package/dist/children.d.ts +9 -0
- package/dist/children.js +273 -0
- package/dist/classes.d.ts +13 -0
- package/dist/classes.js +176 -0
- package/dist/cli.js +0 -17
- package/dist/client.js +0 -12
- package/dist/compiler.js +25 -90
- package/dist/component.js +10 -86
- package/dist/context.d.ts +4 -66
- package/dist/context.js +2 -33
- package/dist/diff.d.ts +0 -3
- package/dist/diff.js +0 -9
- package/dist/emitter.d.ts +0 -8
- package/dist/emitter.js +0 -12
- package/dist/expr.d.ts +0 -70
- package/dist/expr.js +27 -890
- package/dist/fallthrough.d.ts +9 -0
- package/dist/fallthrough.js +78 -0
- package/dist/fixture.d.ts +2 -1
- package/dist/fixture.js +37 -22
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/islands.d.ts +1 -0
- package/dist/islands.js +1 -0
- package/dist/lists.d.ts +0 -11
- package/dist/lists.js +4 -39
- package/dist/loops.d.ts +3 -0
- package/dist/loops.js +164 -0
- package/dist/model.d.ts +14 -143
- package/dist/model.js +27 -12
- package/dist/narrowing.d.ts +19 -0
- package/dist/narrowing.js +126 -0
- package/dist/numbers.d.ts +8 -0
- package/dist/numbers.js +73 -0
- package/dist/parens.d.ts +0 -27
- package/dist/parens.js +0 -44
- package/dist/plugin.d.ts +84 -0
- package/dist/plugin.js +26 -0
- package/dist/plugins/i18n.d.ts +23 -0
- package/dist/{i18n.js → plugins/i18n.js} +76 -19
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +8 -0
- package/dist/plugins/router-link.d.ts +8 -0
- package/dist/{router.js → plugins/router-link.js} +19 -61
- package/dist/plugins/router.d.ts +42 -0
- package/dist/plugins/router.js +256 -0
- package/dist/plugins/scoped.d.ts +12 -0
- package/dist/plugins/scoped.js +125 -0
- package/dist/plugins/shared-types.d.ts +2 -0
- package/dist/plugins/shared-types.js +7 -0
- package/dist/plugins/stores.d.ts +37 -0
- package/dist/plugins/stores.js +377 -0
- package/dist/plugins/teleport.d.ts +5 -0
- package/dist/plugins/teleport.js +55 -0
- package/dist/rust.d.ts +1 -7
- package/dist/rust.js +38 -160
- package/dist/script.d.ts +0 -6
- package/dist/script.js +13 -128
- package/dist/slots.d.ts +13 -0
- package/dist/slots.js +160 -0
- package/dist/strings.d.ts +13 -0
- package/dist/strings.js +208 -0
- package/dist/styles.d.ts +14 -0
- package/dist/styles.js +181 -0
- package/dist/template.d.ts +1 -42
- package/dist/template.js +14 -738
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -8
- package/dist/types.d.ts +2 -0
- package/dist/typescript.d.ts +0 -13
- package/dist/typescript.js +8 -41
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +1 -23
- package/package.json +7 -2
- package/dist/i18n.d.ts +0 -27
- package/dist/router.d.ts +0 -17
- package/dist/scoped.d.ts +0 -24
- package/dist/scoped.js +0 -177
- package/dist/stores.d.ts +0 -6
- package/dist/stores.js +0 -229
package/dist/classes.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { escapeHtml } from "@vue/shared";
|
|
2
|
+
import { fail, rustStr, STR } from "./model.js";
|
|
3
|
+
import { expr } from "./expr.js";
|
|
4
|
+
import { cond, known, truthy } from "./narrowing.js";
|
|
5
|
+
import { meet } from "./strings.js";
|
|
6
|
+
import { atom, bare, condition, logical, not, strArg } from "./parens.js";
|
|
7
|
+
import { Emitter } from "./emitter.js";
|
|
8
|
+
import { valueAttr } from "./attrs.js";
|
|
9
|
+
export function classItems(s, n) {
|
|
10
|
+
switch (n.type) {
|
|
11
|
+
case "StringLiteral": {
|
|
12
|
+
const t = n.value.trim();
|
|
13
|
+
return t ? [{ lit: t }] : [];
|
|
14
|
+
}
|
|
15
|
+
case "ArrayExpression":
|
|
16
|
+
return n.elements.flatMap((el) => (el ? classItems(s, el) : []));
|
|
17
|
+
case "ObjectExpression":
|
|
18
|
+
const literalNames = n.properties
|
|
19
|
+
.filter((p) => p.type === "ObjectProperty" && !p.computed)
|
|
20
|
+
.map((p) => (p.key.type === "Identifier" ? p.key.name : String(p.key.value)));
|
|
21
|
+
const arrayIndex = (k) => /^(0|[1-9]\d*)$/.test(k) && Number(k) < 2 ** 32 - 1;
|
|
22
|
+
if (n.properties.some((p) => p.type === "ObjectProperty" && p.computed) ||
|
|
23
|
+
literalNames.some(arrayIndex) ||
|
|
24
|
+
new Set(literalNames).size !== literalNames.length) {
|
|
25
|
+
const keys = [];
|
|
26
|
+
const entries = n.properties.map((p) => {
|
|
27
|
+
if (p.type !== "ObjectProperty")
|
|
28
|
+
fail(s.comp, "a class object holds `name: condition` pairs", p);
|
|
29
|
+
const key = p.computed ? expr(s, p.key) : { code: rustStr(p.key.type === "Identifier" ? p.key.name : String(p.key.value)), ty: STR };
|
|
30
|
+
if (key.ty.k !== "str")
|
|
31
|
+
fail(s.comp, "a computed class name is a string", p.key);
|
|
32
|
+
for (const other of keys)
|
|
33
|
+
meet(s.comp, other, key, "a class object's names", p.key, "equal");
|
|
34
|
+
keys.push(key);
|
|
35
|
+
return `(${bare(cond(s, p.value))}, ${bare(key.code)})`;
|
|
36
|
+
});
|
|
37
|
+
return [{ code: `&*fv::class_object(&[${entries.join(", ")}])` }];
|
|
38
|
+
}
|
|
39
|
+
return n.properties.flatMap((p) => {
|
|
40
|
+
if (p.type !== "ObjectProperty")
|
|
41
|
+
fail(s.comp, "a class object holds `name: condition` pairs", p);
|
|
42
|
+
let name;
|
|
43
|
+
if (!p.computed) {
|
|
44
|
+
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
45
|
+
if (key !== key.trim() || !key)
|
|
46
|
+
fail(s.comp, `class name \`${key}\` has spaces around it`, p);
|
|
47
|
+
name = { lit: key };
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const k = expr(s, p.key);
|
|
51
|
+
if (k.ty.k !== "str")
|
|
52
|
+
fail(s.comp, "a computed class name is a string", p.key);
|
|
53
|
+
name = { code: `fv::js_trim(${strArg(k.code)})` };
|
|
54
|
+
}
|
|
55
|
+
const v = expr(s, p.value);
|
|
56
|
+
const on = known(v);
|
|
57
|
+
if (on !== undefined)
|
|
58
|
+
return on ? [name] : [];
|
|
59
|
+
const text = "lit" in name ? rustStr(name.lit) : name.code;
|
|
60
|
+
return [{ code: `if ${condition(truthy(v))} { ${bare(text)} } else { "" }` }];
|
|
61
|
+
});
|
|
62
|
+
case "LogicalExpression":
|
|
63
|
+
if (n.operator === "&&") {
|
|
64
|
+
return conditional(cond(s, n.left), classItems(s, n.right));
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
case "ConditionalExpression":
|
|
68
|
+
if (n.consequent.type === "NullLiteral" || n.alternate.type === "NullLiteral") {
|
|
69
|
+
const branch = n.consequent.type === "NullLiteral" ? n.alternate : n.consequent;
|
|
70
|
+
const test = cond(s, n.test);
|
|
71
|
+
return conditional(n.consequent.type === "NullLiteral" ? not(test) : test, classItems(s, branch));
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
const v = expr(s, n);
|
|
76
|
+
if (v.ty.k === "str")
|
|
77
|
+
return [{ code: v.code }];
|
|
78
|
+
if (v.ty.k === "opt" && v.ty.of.k === "str")
|
|
79
|
+
return [{ code: `${atom(v.code)}.unwrap_or("")` }];
|
|
80
|
+
if (v.ty.k === "undef")
|
|
81
|
+
return [];
|
|
82
|
+
return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
|
|
83
|
+
}
|
|
84
|
+
function conditional(test, items) {
|
|
85
|
+
if (test === "true" || test === "false")
|
|
86
|
+
return test === "true" ? items : [];
|
|
87
|
+
return items.map((it) => ({ code: `if ${condition(test)} { ${"lit" in it ? rustStr(it.lit) : bare(it.code)} } else { "" }` }));
|
|
88
|
+
}
|
|
89
|
+
export function renderClass(s, e, n, after = false) {
|
|
90
|
+
const items = classItems(s, n);
|
|
91
|
+
let wrote = after;
|
|
92
|
+
let i = 0;
|
|
93
|
+
for (; i < items.length; i++) {
|
|
94
|
+
const it = items[i];
|
|
95
|
+
if (!("lit" in it))
|
|
96
|
+
break;
|
|
97
|
+
e.lit((wrote ? " " : "") + escapeHtml(it.lit));
|
|
98
|
+
wrote = true;
|
|
99
|
+
}
|
|
100
|
+
if (i < items.length) {
|
|
101
|
+
const rest = items.slice(i).map((it) => ("lit" in it ? rustStr(it.lit) : it.code));
|
|
102
|
+
e.stmt(`fv::class_into(out, ${wrote}, &[${rest.join(", ")}]);`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function classPresent(s, n) {
|
|
106
|
+
if (n.fvMerged)
|
|
107
|
+
return n.elements.reduce((acc, el) => logical(acc, "||", classPresent(s, el)), "false");
|
|
108
|
+
switch (n.type) {
|
|
109
|
+
case "StringLiteral":
|
|
110
|
+
case "NullLiteral":
|
|
111
|
+
case "ArrayExpression":
|
|
112
|
+
case "ObjectExpression":
|
|
113
|
+
return "true";
|
|
114
|
+
case "LogicalExpression":
|
|
115
|
+
if (n.operator === "&&")
|
|
116
|
+
return logical(not(cond(s, n.left)), "||", classPresent(s, n.right));
|
|
117
|
+
break;
|
|
118
|
+
case "ConditionalExpression":
|
|
119
|
+
if (n.consequent.type === "NullLiteral")
|
|
120
|
+
return logical(cond(s, n.test), "||", classPresent(s, n.alternate));
|
|
121
|
+
if (n.alternate.type === "NullLiteral")
|
|
122
|
+
return logical(not(cond(s, n.test)), "||", classPresent(s, n.consequent));
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
const v = expr(s, n);
|
|
126
|
+
if (v.ty.k === "undef")
|
|
127
|
+
return "false";
|
|
128
|
+
if (v.ty.k === "opt")
|
|
129
|
+
return `${atom(v.code)}.is_some()`;
|
|
130
|
+
return "true";
|
|
131
|
+
}
|
|
132
|
+
export function maybeEqual(s, n) {
|
|
133
|
+
if (n.type === "StringLiteral")
|
|
134
|
+
return true;
|
|
135
|
+
if (n.fvMerged || ["ArrayExpression", "ObjectExpression", "NullLiteral", "LogicalExpression", "ConditionalExpression"].includes(n.type))
|
|
136
|
+
return false;
|
|
137
|
+
const v = expr(s, n);
|
|
138
|
+
return v.ty.k === "str" || (v.ty.k === "opt" && v.ty.of.k === "str");
|
|
139
|
+
}
|
|
140
|
+
export function literalClass(n) {
|
|
141
|
+
if (n.type === "StringLiteral")
|
|
142
|
+
return n.value.trim();
|
|
143
|
+
if (!n.fvMerged)
|
|
144
|
+
return null;
|
|
145
|
+
const parts = n.elements.map(literalClass);
|
|
146
|
+
return parts.includes(null) ? null : parts.filter(Boolean).join(" ");
|
|
147
|
+
}
|
|
148
|
+
export function classAttr(s, n, side) {
|
|
149
|
+
switch (n.type) {
|
|
150
|
+
case "StringLiteral":
|
|
151
|
+
return `fv::Attr::str(${rustStr(n.value)})`;
|
|
152
|
+
case "NullLiteral":
|
|
153
|
+
return 'fv::Attr::str("")';
|
|
154
|
+
case "ArrayExpression":
|
|
155
|
+
case "ObjectExpression": {
|
|
156
|
+
const items = classItems(s, n);
|
|
157
|
+
const names = items.every((it) => "lit" in it)
|
|
158
|
+
? rustStr(items.map((it) => ("lit" in it ? it.lit : "")).join(" "))
|
|
159
|
+
: `fv::class_names(&[${items.map((it) => ("lit" in it ? rustStr(it.lit) : it.code)).join(", ")}])`;
|
|
160
|
+
const literal = names.startsWith('"');
|
|
161
|
+
if (side === "vnode")
|
|
162
|
+
return literal ? `fv::Attr::str(${names})` : `fv::Attr::from(${names})`;
|
|
163
|
+
return `fv::Attr::Names(${literal ? `std::borrow::Cow::Borrowed(${names})` : `${names}.into()`})`;
|
|
164
|
+
}
|
|
165
|
+
case "LogicalExpression":
|
|
166
|
+
if (n.operator === "&&")
|
|
167
|
+
return `if ${condition(cond(s, n.left))} { ${classAttr(s, n.right, side)} } else { fv::Attr::str("") }`;
|
|
168
|
+
break;
|
|
169
|
+
case "ConditionalExpression":
|
|
170
|
+
return `if ${condition(cond(s, n.test))} { ${classAttr(s, n.consequent, side)} } else { ${classAttr(s, n.alternate, side)} }`;
|
|
171
|
+
}
|
|
172
|
+
const v = expr(s, n);
|
|
173
|
+
if (v.ty.k === "str" || v.ty.k === "undef" || (v.ty.k === "opt" && v.ty.of.k === "str"))
|
|
174
|
+
return valueAttr(s, v, n);
|
|
175
|
+
return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
|
|
176
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* `ferrovue`: compile the project's components, run from its root.
|
|
3
|
-
*
|
|
4
|
-
* ferrovue write the Rust modules to the configured output directory
|
|
5
|
-
* ferrovue --check write nothing; exit 1 if the committed modules are not what would be written
|
|
6
|
-
* ferrovue --check --diff show a unified diff of differences
|
|
7
|
-
* ferrovue --watch write them, then again whenever a component, store, type file, the routes
|
|
8
|
-
* or the configuration changes, until interrupted
|
|
9
|
-
* ferrovue init scaffold a starter ferrovue.config.json and components directory
|
|
10
|
-
* ferrovue -c, --config path to configuration file (default: ferrovue.config.json)
|
|
11
|
-
* ferrovue -v, --version print the version and exit
|
|
12
|
-
* ferrovue -h, --help print this help and exit */
|
|
13
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, watch, writeFileSync } from "node:fs";
|
|
14
3
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
15
4
|
import { CONFIG_FILE, GenError, generate, loadConfig, VERSION, write } from "./compiler.js";
|
|
@@ -30,7 +19,6 @@ Options:
|
|
|
30
19
|
-v, --version print the version and exit
|
|
31
20
|
-h, --help print this help and exit
|
|
32
21
|
`;
|
|
33
|
-
/** A refused construct, or a broken configuration: the message alone, which names the file. */
|
|
34
22
|
function report(e) {
|
|
35
23
|
if (e instanceof GenError || e instanceof SyntaxError)
|
|
36
24
|
console.error(`error: ${e.message}`);
|
|
@@ -79,7 +67,6 @@ function check(config, showDiff = false) {
|
|
|
79
67
|
have = readdirSync(dir).filter((f) => f.endsWith(".rs"));
|
|
80
68
|
}
|
|
81
69
|
catch {
|
|
82
|
-
// An absent directory is simply stale.
|
|
83
70
|
}
|
|
84
71
|
const stale = [...want.keys()].filter((name) => {
|
|
85
72
|
try {
|
|
@@ -120,8 +107,6 @@ function once(config) {
|
|
|
120
107
|
const what = changed.length || removed.length ? `${changed.length} changed, ${removed.length} removed` : "nothing changed";
|
|
121
108
|
console.log(`${config.out}: ${files.length} files, ${what}`);
|
|
122
109
|
}
|
|
123
|
-
/** Regenerate on every change to what the components are compiled from, reporting errors and
|
|
124
|
-
* carrying on. The output directory itself, dependencies and build output are ignored. */
|
|
125
110
|
function watchProject(configPath) {
|
|
126
111
|
const run = () => {
|
|
127
112
|
try {
|
|
@@ -138,7 +123,6 @@ function watchProject(configPath) {
|
|
|
138
123
|
out = loadConfig(root, configPath).out;
|
|
139
124
|
}
|
|
140
125
|
catch {
|
|
141
|
-
// A broken configuration ignores nothing; the next run reports it.
|
|
142
126
|
}
|
|
143
127
|
const parts = file.split(sep);
|
|
144
128
|
return (parts.some((p) => p === "node_modules" || p === "target" || p === ".git" || p === "dist") ||
|
|
@@ -149,7 +133,6 @@ function watchProject(configPath) {
|
|
|
149
133
|
watch(root, { recursive: true }, (_event, name) => {
|
|
150
134
|
if (!name || ignored(relative(root, join(root, name))))
|
|
151
135
|
return;
|
|
152
|
-
// Editors write a file in several steps: one run once they are done.
|
|
153
136
|
if (timer)
|
|
154
137
|
clearTimeout(timer);
|
|
155
138
|
timer = setTimeout(run, 50);
|
package/dist/client.js
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
/* What the browser does with a page ferrovue rendered. */
|
|
2
1
|
import { createSSRApp } from "vue";
|
|
3
|
-
/** A JSON string, skipped whole as it may hold the same letters, or a bare number JSON has no word
|
|
4
|
-
* for, which the server writes as JavaScript does where `serde_json` alone would write `null`. */
|
|
5
2
|
const BARE = /"(?:[^"\\]|\\.)*"|-?Infinity|NaN/g;
|
|
6
3
|
const NON_FINITE = { NaN: Number.NaN, Infinity: Number.POSITIVE_INFINITY, "-Infinity": Number.NEGATIVE_INFINITY };
|
|
7
|
-
/** `JSON.parse`, reading back the bare `NaN`, `Infinity` and `-Infinity` that an island's props and
|
|
8
|
-
* the state script hold for a number that is not finite, so the client renders what the server did.
|
|
9
|
-
* Throws as `JSON.parse` does on anything else that is not JSON. */
|
|
10
4
|
function parseJson(text) {
|
|
11
|
-
// Plain JSON: almost everything the server writes, and all it wrote before it wrote these.
|
|
12
5
|
if (!/NaN|Infinity/.test(text))
|
|
13
6
|
return JSON.parse(text);
|
|
14
|
-
// Each token becomes a string that starts with more NULs in a row than any string in the text
|
|
15
|
-
// holds (JSON has one way to write a NUL, `\u0000`), so no other value can be mistaken for one.
|
|
16
7
|
let escaped = "\\u0000";
|
|
17
8
|
while (text.includes(escaped))
|
|
18
9
|
escaped += "\\u0000";
|
|
@@ -35,7 +26,6 @@ export async function mountIslands(components, options = {}) {
|
|
|
35
26
|
const root = options.root ?? document;
|
|
36
27
|
const report = options.onError ?? ((el, problem) => console.warn(`[ferrovue] island left unhydrated: ${problem}`, el));
|
|
37
28
|
const elements = Array.from(root.querySelectorAll("[data-island]"));
|
|
38
|
-
// Each component the page names, loaded once however many islands it has.
|
|
39
29
|
const loading = new Map();
|
|
40
30
|
for (const el of elements) {
|
|
41
31
|
const name = el.dataset.island ?? "";
|
|
@@ -84,8 +74,6 @@ export async function mountIslands(components, options = {}) {
|
|
|
84
74
|
},
|
|
85
75
|
};
|
|
86
76
|
}
|
|
87
|
-
/** The component itself, or what its loader resolves to: a module's default export, or the
|
|
88
|
-
* component. A loader that rejects gives its error, which leaves only its own islands unhydrated. */
|
|
89
77
|
async function load(component) {
|
|
90
78
|
if (typeof component !== "function" || "props" in component || "displayName" in component || "__vccOpts" in component)
|
|
91
79
|
return component;
|
package/dist/compiler.js
CHANGED
|
@@ -1,56 +1,29 @@
|
|
|
1
|
-
/* ferrovue's compiler: `.vue` → Rust render functions.
|
|
2
|
-
*
|
|
3
|
-
* The input is not the template. It is what `@vue/compiler-sfc` compiles the template to for
|
|
4
|
-
* server rendering — straight-line `_push` calls, `if`, `_ssrRenderList`, and a handful of
|
|
5
|
-
* `@vue/server-renderer` helpers — so element structure, attribute order, whitespace and the
|
|
6
|
-
* fragment markers are decided by Vue, and this file only has to translate a small, closed
|
|
7
|
-
* vocabulary of JavaScript into Rust that writes the same bytes. The generated code calls the
|
|
8
|
-
* `ferrovue` crate for everything Vue's runtime does.
|
|
9
|
-
*
|
|
10
|
-
* Anything outside that vocabulary is an error naming the component and the construct, never a
|
|
11
|
-
* best guess: a guess that is wrong by one byte is a hydration mismatch in a browser, found by a
|
|
12
|
-
* reader.
|
|
13
|
-
*
|
|
14
|
-
* A project describes itself in `ferrovue.config.json` at its root — see `Config` — and runs the
|
|
15
|
-
* `ferrovue` command there. `generate()` is also what a drift test calls, so the committed files can
|
|
16
|
-
* be held to exactly what this produces.
|
|
17
|
-
*
|
|
18
|
-
* This module is the public API; the translation lives in the modules beside it. */
|
|
19
1
|
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
20
|
-
import { join
|
|
2
|
+
import { join } from "node:path";
|
|
21
3
|
import { ctx, loadConfig, tyOfName } from "./context.js";
|
|
22
4
|
import { readComponent } from "./component.js";
|
|
23
|
-
import { readRoutes } from "./router.js";
|
|
24
|
-
import { readStores } from "./stores.js";
|
|
25
|
-
import { i18nSource, readLocales } from "./i18n.js";
|
|
26
5
|
import { scopeFor } from "./script.js";
|
|
27
|
-
import { attrsFlow
|
|
28
|
-
import { componentSource, isIsland, modSource
|
|
6
|
+
import { attrsFlow } from "./fallthrough.js";
|
|
7
|
+
import { componentSource, isIsland, modSource } from "./rust.js";
|
|
8
|
+
import { renderParams } from "./plugin.js";
|
|
9
|
+
import { PLUGINS } from "./plugins/index.js";
|
|
29
10
|
/** Every generated file, keyed by its name in the output directory. */
|
|
30
11
|
export function generate(root, config = loadConfig(root)) {
|
|
31
12
|
ctx.componentsDir = config.components.replace(/\/?$/, "/");
|
|
32
13
|
ctx.helperModule = config.helpers?.module ?? null;
|
|
33
14
|
ctx.trustedHtml = config.trustedHtml ?? null;
|
|
34
|
-
const router = config.router ?? (config.routes ? { routes: config.routes } : null);
|
|
35
|
-
ctx.routes = router ? readRoutes(root, router.routes) : null;
|
|
36
|
-
ctx.routerBase = router?.base ?? "";
|
|
37
15
|
ctx.clientDirectives = new Set(config.clientDirectives ?? []);
|
|
38
|
-
ctx.i18n = config.i18n ? readLocales(root, config.i18n) : null;
|
|
39
|
-
ctx.linkActive = router?.linkActiveClass ?? "router-link-active";
|
|
40
|
-
ctx.linkExactActive = router?.linkExactActiveClass ?? "router-link-exact-active";
|
|
41
16
|
ctx.rootDir = root;
|
|
42
|
-
ctx.scopeId = config.scopeId ?? "filepath-source";
|
|
43
|
-
ctx.viteRoot = resolve(root, config.viteRoot ?? ".");
|
|
44
17
|
ctx.typeStructs = new Map();
|
|
45
18
|
ctx.typeAliases = new Map();
|
|
46
19
|
ctx.typeFiles = new Map();
|
|
47
20
|
ctx.typeRead = new Set();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
ctx.
|
|
52
|
-
|
|
53
|
-
|
|
21
|
+
ctx.plugins = PLUGINS;
|
|
22
|
+
ctx.runs = new Map();
|
|
23
|
+
for (const p of PLUGINS)
|
|
24
|
+
ctx.runs.set(p, p.configure?.(config, root));
|
|
25
|
+
for (const p of PLUGINS)
|
|
26
|
+
p.prepare?.(root);
|
|
54
27
|
ctx.helpers = Object.fromEntries(Object.entries(config.helpers?.functions ?? {}).map(([name, h]) => [
|
|
55
28
|
name,
|
|
56
29
|
{ rust: h.rust, params: h.params.map(tyOfName), ret: tyOfName(h.returns), maxLen: h.maxLen ?? 0 },
|
|
@@ -63,56 +36,25 @@ export function generate(root, config = loadConfig(root)) {
|
|
|
63
36
|
const read = files.map((f) => readComponent(f, root));
|
|
64
37
|
const components = new Map(read.map((r) => [r.comp.name, r.comp]));
|
|
65
38
|
ctx.components = components;
|
|
66
|
-
// Whether a component reads a store or the route is known once its setup is read; both reach
|
|
67
|
-
// every component on the way down to one that does, and the route every one on the way down to
|
|
68
|
-
// a `<RouterLink>`.
|
|
69
39
|
const scopes = read.map((r) => scopeFor(r.comp, r.ast, components).scope);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
attrsFlow(
|
|
74
|
-
for (const c of components.values())
|
|
75
|
-
c.usesRoute = c.routerLink || c.readsRoute;
|
|
76
|
-
for (let changed = true; changed;) {
|
|
77
|
-
changed = false;
|
|
78
|
-
for (const c of components.values()) {
|
|
79
|
-
if (!c.usesRoute && [...c.imports].some((i) => components.get(i)?.usesRoute)) {
|
|
80
|
-
c.usesRoute = changed = true;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
for (const c of components.values())
|
|
85
|
-
c.usesI18n = c.readsI18n;
|
|
86
|
-
for (let changed = true; changed;) {
|
|
87
|
-
changed = false;
|
|
88
|
-
for (const c of components.values()) {
|
|
89
|
-
if (!c.usesI18n && [...c.imports].some((i) => components.get(i)?.usesI18n)) {
|
|
90
|
-
c.usesI18n = changed = true;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
for (const c of components.values())
|
|
95
|
-
c.usesTeleports = c.readsTeleports;
|
|
96
|
-
for (let changed = true; changed;) {
|
|
97
|
-
changed = false;
|
|
98
|
-
for (const c of components.values()) {
|
|
99
|
-
if (!c.usesTeleports && [...c.imports].some((i) => components.get(i)?.usesTeleports)) {
|
|
100
|
-
c.usesTeleports = changed = true;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
40
|
+
const all = read.map((r, i) => ({ comp: r.comp, ssr: r.ssr, children: scopes[i].children, attrsBindings: scopes[i].attrsBindings }));
|
|
41
|
+
for (const p of PLUGINS)
|
|
42
|
+
p.analyse?.(all);
|
|
43
|
+
attrsFlow(all);
|
|
104
44
|
for (const c of components.values())
|
|
105
|
-
c.
|
|
45
|
+
c.takes = new Set(renderParams().filter((p) => p.reads(c)).map((p) => p.name));
|
|
106
46
|
for (let changed = true; changed;) {
|
|
107
47
|
changed = false;
|
|
108
48
|
for (const c of components.values()) {
|
|
109
|
-
|
|
110
|
-
c.
|
|
49
|
+
for (const name of [...c.imports].flatMap((i) => [...(components.get(i)?.takes ?? [])])) {
|
|
50
|
+
if (!c.takes.has(name)) {
|
|
51
|
+
c.takes.add(name);
|
|
52
|
+
changed = true;
|
|
53
|
+
}
|
|
111
54
|
}
|
|
112
55
|
}
|
|
113
56
|
}
|
|
114
57
|
const out = new Map();
|
|
115
|
-
// A child before its parents, which read the props its scoped slots pass.
|
|
116
58
|
const ordered = [];
|
|
117
59
|
const placed = new Set();
|
|
118
60
|
const place = (r) => {
|
|
@@ -128,19 +70,13 @@ export function generate(root, config = loadConfig(root)) {
|
|
|
128
70
|
};
|
|
129
71
|
read.forEach(place);
|
|
130
72
|
for (const r of ordered) {
|
|
131
|
-
// Numbered per component, so a file's text depends on its own source alone.
|
|
132
73
|
ctx.narrowCount = 0;
|
|
133
74
|
out.set(`${r.comp.module}.rs`, componentSource(r.comp, r.ast, r.ssr, components));
|
|
134
75
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
out.set("stores.rs", storesSource(config.stores.replace(/\/?$/, "/")));
|
|
140
|
-
if (ctx.typeStructs.size)
|
|
141
|
-
out.set("types.rs", typesSource());
|
|
142
|
-
if (ctx.i18n)
|
|
143
|
-
out.set("i18n.rs", i18nSource(ctx.i18n));
|
|
76
|
+
const modules = PLUGINS.flatMap((p) => p.modules?.() ?? []);
|
|
77
|
+
out.set("mod.rs", modSource(read.map((r) => r.comp), modules.map(([file]) => file)));
|
|
78
|
+
for (const [file, text] of modules)
|
|
79
|
+
out.set(file, text);
|
|
144
80
|
return out;
|
|
145
81
|
}
|
|
146
82
|
/** Write what \`generate\` produces to the configured directory: only the files whose text changed,
|
|
@@ -157,7 +93,6 @@ export function write(root, config = loadConfig(root)) {
|
|
|
157
93
|
old = readFileSync(join(target, name), "utf8");
|
|
158
94
|
}
|
|
159
95
|
catch {
|
|
160
|
-
// A new file.
|
|
161
96
|
}
|
|
162
97
|
if (old !== text) {
|
|
163
98
|
writeFileSync(join(target, name), text);
|
package/dist/component.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
/* One `.vue` file read: its props, models, imported types, and Vue's SSR compilation of its template. */
|
|
2
1
|
import { compileScript, compileTemplate, parse as parseSfc } from "@vue/compiler-sfc";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
2
|
import { readFileSync } from "node:fs";
|
|
5
3
|
import { SourceMapConsumer } from "source-map-js";
|
|
6
|
-
import { basename, relative
|
|
7
|
-
import { fail, opt, snake, tagAst } from "./model.js";
|
|
4
|
+
import { basename, relative } from "node:path";
|
|
5
|
+
import { blankComponent, fail, opt, snake, tagAst } from "./model.js";
|
|
8
6
|
import { ctx } from "./context.js";
|
|
9
7
|
import { typesImports, declareTypes, defaultValue, definePropsType, readTypeFile, resolveImport, runtimeDefaults, structOf, tyOfTs } from "./typescript.js";
|
|
10
|
-
|
|
11
|
-
* offset by where the template starts when the error is in the template's content. */
|
|
8
|
+
import { claim } from "./plugin.js";
|
|
12
9
|
function vueErrorNode(err, within) {
|
|
13
10
|
const loc = err.loc;
|
|
14
11
|
if (!loc)
|
|
@@ -17,17 +14,6 @@ function vueErrorNode(err, within) {
|
|
|
17
14
|
const at = !within ? { line, column: column - 1 } : line === 1 ? { line: within.line, column: within.column - 1 + column - 1 } : { line: within.line + line - 1, column: column - 1 };
|
|
18
15
|
return { type: "VueError", loc: { start: at }, __fv: "source" };
|
|
19
16
|
}
|
|
20
|
-
/** `getHash` in `@vitejs/plugin-vue`: the first 8 hex digits of the SHA-256 of the file's path from
|
|
21
|
-
* Vite's root, with `/` between its parts, followed in `"filepath-source"` mode by its source. */
|
|
22
|
-
function scopeHash(file, source) {
|
|
23
|
-
const path = relative(ctx.viteRoot, resolve(file)).split(sep).join("/");
|
|
24
|
-
return createHash("sha256")
|
|
25
|
-
.update(ctx.scopeId === "filepath" ? path : path + source)
|
|
26
|
-
.digest("hex")
|
|
27
|
-
.slice(0, 8);
|
|
28
|
-
}
|
|
29
|
-
/** `inheritAttrs` as `defineOptions({ inheritAttrs })` or a plain `<script>`'s `export default`
|
|
30
|
-
* sets it: a literal, since it decides what reaches the root. */
|
|
31
17
|
function inheritAttrs(comp, statements) {
|
|
32
18
|
let found = true;
|
|
33
19
|
for (const st of statements) {
|
|
@@ -54,61 +40,24 @@ export function readComponent(file, root) {
|
|
|
54
40
|
const source = readFileSync(file, "utf8");
|
|
55
41
|
const { descriptor, errors } = parseSfc(source, { filename: file });
|
|
56
42
|
const rel = relative(root, file);
|
|
57
|
-
const comp =
|
|
58
|
-
name,
|
|
59
|
-
module: snake(name),
|
|
60
|
-
file: rel,
|
|
61
|
-
props: { name: "Props", fields: [] },
|
|
62
|
-
structs: new Map(),
|
|
63
|
-
trustedName: null,
|
|
64
|
-
floatName: null,
|
|
65
|
-
childProps: new Map(),
|
|
66
|
-
imports: new Set(),
|
|
67
|
-
slotNames: [],
|
|
68
|
-
routerView: false,
|
|
69
|
-
routerLink: false,
|
|
70
|
-
readsRoute: false,
|
|
71
|
-
usesRoute: false,
|
|
72
|
-
readsStores: false,
|
|
73
|
-
usesStores: false,
|
|
74
|
-
readsI18n: false,
|
|
75
|
-
usesI18n: false,
|
|
76
|
-
readsTeleports: false,
|
|
77
|
-
usesTeleports: false,
|
|
78
|
-
models: new Map(),
|
|
79
|
-
aliases: new Map(),
|
|
80
|
-
importedTypes: new Map(),
|
|
81
|
-
slotShapes: new Map(),
|
|
82
|
-
scopeId: null,
|
|
83
|
-
slotted: false,
|
|
84
|
-
inheritAttrs: true,
|
|
85
|
-
inherits: false,
|
|
86
|
-
passesSlotIds: false,
|
|
87
|
-
attrNames: new Set(),
|
|
88
|
-
idsInAttrs: false,
|
|
89
|
-
};
|
|
43
|
+
const comp = blankComponent(name, snake(name), rel);
|
|
90
44
|
comp.source = source;
|
|
91
45
|
if (errors.length)
|
|
92
46
|
fail(comp, String(errors[0]), vueErrorNode(errors[0]));
|
|
93
47
|
if (!descriptor.scriptSetup || !descriptor.template) {
|
|
94
48
|
fail(comp, "an island needs `<script setup lang=\"ts\">` and a `<template>`");
|
|
95
49
|
}
|
|
96
|
-
// A global `<style>` block changes no markup. A scoped one adds `data-v-…` attributes, whose id is
|
|
97
|
-
// computed here as the bundler computes it. A CSS module renames classes, and `v-bind()` in CSS
|
|
98
|
-
// writes variables onto the root, neither of which the server reproduces.
|
|
99
50
|
for (const st of descriptor.styles) {
|
|
100
51
|
if (st.module)
|
|
101
52
|
fail(comp, "`<style module>` renames classes in the bundler; use a global or scoped `<style>`, or a stylesheet");
|
|
102
53
|
}
|
|
103
54
|
if (descriptor.cssVars.length)
|
|
104
55
|
fail(comp, "`v-bind()` in `<style>` sets variables the server does not render; bind `:style` instead");
|
|
105
|
-
|
|
106
|
-
comp
|
|
107
|
-
comp.slotted = descriptor.slotted;
|
|
56
|
+
for (const p of ctx.plugins)
|
|
57
|
+
p.sfc?.(comp, descriptor, file, source);
|
|
108
58
|
const script = compileScript(descriptor, { id: name });
|
|
109
59
|
const ast = script.scriptSetupAst ?? [];
|
|
110
60
|
tagAst(ast, "source");
|
|
111
|
-
// A plain `<script>` beside the setup one may declare the types the setup block uses.
|
|
112
61
|
const plainAst = script.scriptAst ?? [];
|
|
113
62
|
tagAst(plainAst, "source");
|
|
114
63
|
for (const s of [...plainAst, ...ast]) {
|
|
@@ -125,7 +74,6 @@ export function readComponent(file, root) {
|
|
|
125
74
|
}
|
|
126
75
|
typesImports(comp, [...plainAst, ...ast]);
|
|
127
76
|
comp.inheritAttrs = inheritAttrs(comp, [...plainAst, ...ast]);
|
|
128
|
-
// Types imported from elsewhere: a store's file, a shared `.ts` file, or another component.
|
|
129
77
|
for (const st of [...plainAst, ...ast]) {
|
|
130
78
|
if (st.type !== "ImportDeclaration")
|
|
131
79
|
continue;
|
|
@@ -143,12 +91,8 @@ export function readComponent(file, root) {
|
|
|
143
91
|
const typeFile = resolveImport(comp.file, from);
|
|
144
92
|
if (!typeFile)
|
|
145
93
|
continue;
|
|
146
|
-
|
|
147
|
-
if ([...ctx.stores.values()].some((x) => x.module === module)) {
|
|
148
|
-
if (ctx.storeStructs.has(typeName))
|
|
149
|
-
comp.importedTypes.set(sp.local.name, { k: "struct", name: typeName, store: true });
|
|
94
|
+
if (ctx.plugins.some((p) => p.importedType?.(comp, typeFile, typeName, sp.local.name)))
|
|
150
95
|
continue;
|
|
151
|
-
}
|
|
152
96
|
if (ctx.helperModule !== null && from === ctx.helperModule && !isType)
|
|
153
97
|
continue;
|
|
154
98
|
readTypeFile(typeFile);
|
|
@@ -158,8 +102,6 @@ export function readComponent(file, root) {
|
|
|
158
102
|
comp.aliases.set(sp.local.name, ctx.typeAliases.get(typeName));
|
|
159
103
|
}
|
|
160
104
|
}
|
|
161
|
-
// Interfaces in two passes: every name first, so one may name another declared after it, or
|
|
162
|
-
// itself — a tree node's children are tree nodes.
|
|
163
105
|
const decls = declareTypes(comp, [...plainAst, ...ast], comp.structs, comp.aliases);
|
|
164
106
|
for (const d of decls)
|
|
165
107
|
comp.structs.set(d.name, structOf(comp, d.name, d.members, comp.structs));
|
|
@@ -174,9 +116,7 @@ export function readComponent(file, root) {
|
|
|
174
116
|
if (found)
|
|
175
117
|
propsTy = found;
|
|
176
118
|
}
|
|
177
|
-
// A component without `defineProps` takes no props.
|
|
178
119
|
if (!propsTy) {
|
|
179
|
-
// nothing to read
|
|
180
120
|
}
|
|
181
121
|
else if (propsTy.type === "TSTypeLiteral") {
|
|
182
122
|
comp.props = structOf(comp, "Props", propsTy.members, comp.structs);
|
|
@@ -187,7 +127,6 @@ export function readComponent(file, root) {
|
|
|
187
127
|
else {
|
|
188
128
|
fail(comp, "`defineProps` takes a type literal or an interface declared in the same block", propsTy);
|
|
189
129
|
}
|
|
190
|
-
// `defineModel`: a prop of its own (`modelValue` unless named), which the binding reads.
|
|
191
130
|
for (const st of ast) {
|
|
192
131
|
if (st.type !== "VariableDeclaration")
|
|
193
132
|
continue;
|
|
@@ -206,8 +145,6 @@ export function readComponent(file, root) {
|
|
|
206
145
|
comp.models.set(d.id.name, named);
|
|
207
146
|
}
|
|
208
147
|
}
|
|
209
|
-
// What Vue uses for an absent optional prop: the default `compileScript` resolved — from
|
|
210
|
-
// `withDefaults`, a destructured default, or `defineModel`'s options — or `false` for a boolean.
|
|
211
148
|
const defaults = runtimeDefaults(comp, script.content);
|
|
212
149
|
for (const f of comp.props.fields) {
|
|
213
150
|
if (f.ty.k !== "opt")
|
|
@@ -221,17 +158,11 @@ export function readComponent(file, root) {
|
|
|
221
158
|
const compiled = compileTemplate({
|
|
222
159
|
source: descriptor.template.content,
|
|
223
160
|
filename: file,
|
|
224
|
-
|
|
225
|
-
// slot content only with `:slotted()` styles.
|
|
226
|
-
id: comp.scopeId ?? name,
|
|
227
|
-
scoped: comp.scopeId !== null,
|
|
228
|
-
slotted: comp.slotted,
|
|
161
|
+
...(claim((p) => p.templateOptions?.(comp)) ?? { id: name, scoped: false, slotted: false }),
|
|
229
162
|
ssr: true,
|
|
230
163
|
ssrCssVars: [],
|
|
231
164
|
compilerOptions: { bindingMetadata: script.bindings, sourceMap: true },
|
|
232
165
|
});
|
|
233
|
-
// `ssrInjectFallthroughAttrs`: a root `<Transition>` or `<KeepAlive>` passes `_attrs` on only to
|
|
234
|
-
// a lone element or component without `v-if` or `v-for`.
|
|
235
166
|
const kids = (n) => n.children.filter((c) => c.type !== 3 && !(c.type === 2 && !c.content.trim()));
|
|
236
167
|
const roots = descriptor.template.ast ? kids(descriptor.template.ast) : [];
|
|
237
168
|
const wrapper = roots.length === 1 && roots[0].type === 1 && roots[0].tagType === 1 && ["Transition", "transition", "KeepAlive", "keep-alive"].includes(roots[0].tag) ? roots[0] : null;
|
|
@@ -245,20 +176,13 @@ export function readComponent(file, root) {
|
|
|
245
176
|
comp.templateStart = { line: start.line, column: start.column };
|
|
246
177
|
if (compiled.map)
|
|
247
178
|
comp.templateMap = new SourceMapConsumer(compiled.map);
|
|
248
|
-
// A template error's position is within the template's content.
|
|
249
179
|
if (compiled.errors.length)
|
|
250
180
|
fail(comp, String(compiled.errors[0]), vueErrorNode(compiled.errors[0], comp.templateStart));
|
|
251
181
|
for (const m of compiled.code.matchAll(/_ssrRenderSlot\(_ctx\.\$slots, "([^"]+)"/g)) {
|
|
252
182
|
if (!comp.slotNames.includes(m[1]))
|
|
253
183
|
comp.slotNames.push(m[1]);
|
|
254
184
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
st.specifiers.some((sp) => sp.type === "ImportSpecifier" && (sp.imported.name ?? sp.imported.value) === exported));
|
|
258
|
-
comp.routerLink = compiled.code.includes('_resolveComponent("RouterLink")') || imported("RouterLink");
|
|
259
|
-
comp.routerView = compiled.code.includes('_resolveComponent("RouterView")') || imported("RouterView");
|
|
260
|
-
comp.readsRoute = compiled.code.includes("_ctx.$route");
|
|
261
|
-
comp.readsI18n = compiled.code.includes("_ctx.$t(");
|
|
262
|
-
comp.readsTeleports = compiled.code.includes("_ssrRenderTeleport(");
|
|
185
|
+
for (const p of ctx.plugins)
|
|
186
|
+
p.compiled?.(comp, compiled.code, [...plainAst, ...ast]);
|
|
263
187
|
return { comp, ast, ssr: compiled.code };
|
|
264
188
|
}
|