ferrovue 0.3.0 → 0.5.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 +225 -42
- package/dist/attrs.d.ts +3 -53
- package/dist/attrs.js +41 -469
- package/dist/calls.d.ts +4 -0
- package/dist/calls.js +221 -0
- package/dist/children.d.ts +16 -0
- package/dist/children.js +286 -0
- package/dist/classes.d.ts +13 -0
- package/dist/classes.js +176 -0
- package/dist/cli.js +31 -22
- package/dist/client.d.ts +35 -2
- package/dist/client.js +77 -14
- package/dist/compiler.d.ts +1 -1
- package/dist/compiler.js +31 -92
- package/dist/component.d.ts +3 -1
- package/dist/component.js +123 -104
- package/dist/constants.d.ts +42 -0
- package/dist/constants.js +292 -0
- package/dist/context.d.ts +29 -67
- package/dist/context.js +13 -38
- package/dist/diagnostics.d.ts +23 -0
- package/dist/diagnostics.js +30 -0
- 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/errors.d.ts +867 -0
- package/dist/errors.js +330 -0
- package/dist/expr.d.ts +1 -70
- package/dist/expr.js +142 -934
- package/dist/fallthrough.d.ts +9 -0
- package/dist/fallthrough.js +78 -0
- package/dist/fixture.d.ts +4 -10
- package/dist/fixture.js +38 -27
- package/dist/hydration.d.ts +15 -0
- package/dist/hydration.js +80 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/islands.d.ts +1 -0
- package/dist/islands.js +1 -0
- package/dist/link-router.d.ts +24 -0
- package/dist/link-router.js +36 -0
- package/dist/lists.d.ts +0 -11
- package/dist/lists.js +34 -60
- package/dist/loops.d.ts +3 -0
- package/dist/loops.js +168 -0
- package/dist/model.d.ts +46 -144
- package/dist/model.js +150 -18
- package/dist/narrowing.d.ts +27 -0
- package/dist/narrowing.js +157 -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/client-only.d.ts +8 -0
- package/dist/plugins/client-only.js +107 -0
- package/dist/plugins/i18n.d.ts +23 -0
- package/dist/{i18n.js → plugins/i18n.js} +88 -31
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +10 -0
- package/dist/plugins/router-link.d.ts +8 -0
- package/dist/{router.js → plugins/router-link.js} +52 -91
- 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 +379 -0
- package/dist/plugins/teleport.d.ts +5 -0
- package/dist/plugins/teleport.js +55 -0
- package/dist/plugins/twins.d.ts +10 -0
- package/dist/plugins/twins.js +164 -0
- package/dist/routes.d.ts +10 -0
- package/dist/routes.js +6 -0
- package/dist/rust.d.ts +1 -7
- package/dist/rust.js +56 -174
- package/dist/script.d.ts +3 -6
- package/dist/script.js +127 -140
- package/dist/settle.d.ts +6 -0
- package/dist/settle.js +34 -0
- 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 +25 -749
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +4 -10
- package/dist/types.d.ts +2 -0
- package/dist/typescript.d.ts +2 -14
- package/dist/typescript.js +53 -59
- package/dist/vite.d.ts +2 -1
- package/dist/vite.js +11 -32
- package/package.json +15 -6
- 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/compiler.js
CHANGED
|
@@ -1,56 +1,32 @@
|
|
|
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 {
|
|
2
|
+
import { basename, join } from "node:path";
|
|
21
3
|
import { ctx, loadConfig, tyOfName } from "./context.js";
|
|
22
|
-
import { readComponent } from "./component.js";
|
|
23
|
-
import { readRoutes } from "./router.js";
|
|
24
|
-
import { readStores } from "./stores.js";
|
|
25
|
-
import { i18nSource, readLocales } from "./i18n.js";
|
|
4
|
+
import { importsOf, readComponent } from "./component.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
|
-
|
|
35
|
-
ctx.routes = router ? readRoutes(root, router.routes) : null;
|
|
36
|
-
ctx.routerBase = router?.base ?? "";
|
|
15
|
+
ctx.builders = config.builders ?? true;
|
|
37
16
|
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
17
|
ctx.rootDir = root;
|
|
42
|
-
ctx.scopeId = config.scopeId ?? "filepath-source";
|
|
43
|
-
ctx.viteRoot = resolve(root, config.viteRoot ?? ".");
|
|
44
18
|
ctx.typeStructs = new Map();
|
|
45
19
|
ctx.typeAliases = new Map();
|
|
46
20
|
ctx.typeFiles = new Map();
|
|
47
21
|
ctx.typeRead = new Set();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
22
|
+
ctx.constDecls = new Map();
|
|
23
|
+
ctx.typeConsts = new Map();
|
|
24
|
+
ctx.plugins = PLUGINS;
|
|
25
|
+
ctx.runs = new Map();
|
|
26
|
+
for (const p of PLUGINS)
|
|
27
|
+
ctx.runs.set(p, p.configure?.(config, root));
|
|
28
|
+
for (const p of PLUGINS)
|
|
29
|
+
p.prepare?.(root);
|
|
54
30
|
ctx.helpers = Object.fromEntries(Object.entries(config.helpers?.functions ?? {}).map(([name, h]) => [
|
|
55
31
|
name,
|
|
56
32
|
{ rust: h.rust, params: h.params.map(tyOfName), ret: tyOfName(h.returns), maxLen: h.maxLen ?? 0 },
|
|
@@ -60,59 +36,29 @@ export function generate(root, config = loadConfig(root)) {
|
|
|
60
36
|
.filter((f) => f.endsWith(".vue"))
|
|
61
37
|
.toSorted()
|
|
62
38
|
.map((f) => join(dir, f));
|
|
63
|
-
const
|
|
39
|
+
const children = new Set(files.flatMap((f) => importsOf(readFileSync(f, "utf8")).filter((c) => c !== basename(f, ".vue"))));
|
|
40
|
+
const read = files.map((f) => readComponent(f, root, children.has(basename(f, ".vue"))));
|
|
64
41
|
const components = new Map(read.map((r) => [r.comp.name, r.comp]));
|
|
65
42
|
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
43
|
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
|
-
}
|
|
44
|
+
const all = read.map((r, i) => ({ comp: r.comp, ssr: r.ssr, children: scopes[i].children, attrsBindings: scopes[i].attrsBindings }));
|
|
45
|
+
for (const p of PLUGINS)
|
|
46
|
+
p.analyse?.(all);
|
|
47
|
+
attrsFlow(all);
|
|
104
48
|
for (const c of components.values())
|
|
105
|
-
c.
|
|
49
|
+
c.takes = new Set(renderParams().filter((p) => p.reads(c)).map((p) => p.name));
|
|
106
50
|
for (let changed = true; changed;) {
|
|
107
51
|
changed = false;
|
|
108
52
|
for (const c of components.values()) {
|
|
109
|
-
|
|
110
|
-
c.
|
|
53
|
+
for (const name of [...c.imports].flatMap((i) => [...(components.get(i)?.takes ?? [])])) {
|
|
54
|
+
if (!c.takes.has(name)) {
|
|
55
|
+
c.takes.add(name);
|
|
56
|
+
changed = true;
|
|
57
|
+
}
|
|
111
58
|
}
|
|
112
59
|
}
|
|
113
60
|
}
|
|
114
61
|
const out = new Map();
|
|
115
|
-
// A child before its parents, which read the props its scoped slots pass.
|
|
116
62
|
const ordered = [];
|
|
117
63
|
const placed = new Set();
|
|
118
64
|
const place = (r) => {
|
|
@@ -128,19 +74,13 @@ export function generate(root, config = loadConfig(root)) {
|
|
|
128
74
|
};
|
|
129
75
|
read.forEach(place);
|
|
130
76
|
for (const r of ordered) {
|
|
131
|
-
// Numbered per component, so a file's text depends on its own source alone.
|
|
132
77
|
ctx.narrowCount = 0;
|
|
133
78
|
out.set(`${r.comp.module}.rs`, componentSource(r.comp, r.ast, r.ssr, components));
|
|
134
79
|
}
|
|
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));
|
|
80
|
+
const modules = PLUGINS.flatMap((p) => p.modules?.() ?? []);
|
|
81
|
+
out.set("mod.rs", modSource(read.map((r) => r.comp), modules.map(([file]) => file)));
|
|
82
|
+
for (const [file, text] of modules)
|
|
83
|
+
out.set(file, text);
|
|
144
84
|
return out;
|
|
145
85
|
}
|
|
146
86
|
/** Write what \`generate\` produces to the configured directory: only the files whose text changed,
|
|
@@ -157,7 +97,6 @@ export function write(root, config = loadConfig(root)) {
|
|
|
157
97
|
old = readFileSync(join(target, name), "utf8");
|
|
158
98
|
}
|
|
159
99
|
catch {
|
|
160
|
-
// A new file.
|
|
161
100
|
}
|
|
162
101
|
if (old !== text) {
|
|
163
102
|
writeFileSync(join(target, name), text);
|
package/dist/component.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type Component, type N } from "./model.ts";
|
|
2
|
-
|
|
2
|
+
/** The `.vue` files a component imports, by name. */
|
|
3
|
+
export declare function importsOf(source: string): string[];
|
|
4
|
+
export declare function readComponent(file: string, root: string, isChild: boolean): {
|
|
3
5
|
comp: Component;
|
|
4
6
|
ast: N[];
|
|
5
7
|
ssr: string;
|
package/dist/component.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import { parse as parseJs } from "@babel/parser";
|
|
2
2
|
import { compileScript, compileTemplate, parse as parseSfc } from "@vue/compiler-sfc";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
3
|
import { readFileSync } from "node:fs";
|
|
5
4
|
import { SourceMapConsumer } from "source-map-js";
|
|
6
|
-
import { basename, relative
|
|
7
|
-
import { fail, opt, snake, tagAst } from "./model.js";
|
|
5
|
+
import { basename, relative } from "node:path";
|
|
6
|
+
import { absence, blankComponent, fail, GenError, opt, snake, sourceAt, tagAst } from "./model.js";
|
|
8
7
|
import { ctx } from "./context.js";
|
|
9
|
-
import { typesImports, declareTypes, defaultValue, definePropsType, readTypeFile, resolveImport, runtimeDefaults, structOf, tyOfTs } from "./typescript.js";
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
import { typesImports, declareTypes, defaultValue, definePropsType, ONE_NOTHING, readTypeFile, resolveImport, runtimeDefaults, structOf, tyOfTs } from "./typescript.js";
|
|
9
|
+
import { claim } from "./plugin.js";
|
|
10
|
+
import { asyncChildren } from "./script.js";
|
|
12
11
|
function vueErrorNode(err, within) {
|
|
13
12
|
const loc = err.loc;
|
|
14
13
|
if (!loc)
|
|
@@ -17,17 +16,6 @@ function vueErrorNode(err, within) {
|
|
|
17
16
|
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
17
|
return { type: "VueError", loc: { start: at }, __fv: "source" };
|
|
19
18
|
}
|
|
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
19
|
function inheritAttrs(comp, statements) {
|
|
32
20
|
let found = true;
|
|
33
21
|
for (const st of statements) {
|
|
@@ -43,73 +31,117 @@ function inheritAttrs(comp, statements) {
|
|
|
43
31
|
if (p.type !== "ObjectProperty" || p.computed || (p.key.name ?? p.key.value) !== "inheritAttrs")
|
|
44
32
|
continue;
|
|
45
33
|
if (p.value.type !== "BooleanLiteral")
|
|
46
|
-
fail(comp, "`inheritAttrs` is `true` or `false`", p.value);
|
|
34
|
+
fail(comp, "FV0101", "`inheritAttrs` is `true` or `false`", p.value);
|
|
47
35
|
found = p.value.value;
|
|
48
36
|
}
|
|
49
37
|
}
|
|
50
38
|
return found;
|
|
51
39
|
}
|
|
52
|
-
|
|
40
|
+
function sourceDefault(ast, key) {
|
|
41
|
+
const named = (p) => (p.key?.name ?? p.key?.value) === key;
|
|
42
|
+
for (const st of ast) {
|
|
43
|
+
for (const d of st.type === "VariableDeclaration" ? st.declarations : []) {
|
|
44
|
+
if (d.id.type !== "ObjectPattern" || !definePropsType(d.init))
|
|
45
|
+
continue;
|
|
46
|
+
const p = d.id.properties.find((q) => q.type === "ObjectProperty" && named(q));
|
|
47
|
+
if (p?.value.type === "AssignmentPattern")
|
|
48
|
+
return p.value.right;
|
|
49
|
+
}
|
|
50
|
+
const call = st.type === "ExpressionStatement" ? st.expression : st.type === "VariableDeclaration" ? st.declarations[0]?.init : null;
|
|
51
|
+
if (call?.type !== "CallExpression" || call.callee.name !== "withDefaults" || call.arguments[1]?.type !== "ObjectExpression")
|
|
52
|
+
continue;
|
|
53
|
+
const p = call.arguments[1].properties.find((q) => q.type === "ObjectProperty" && named(q));
|
|
54
|
+
if (p)
|
|
55
|
+
return p.value;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function blockAt(source, block) {
|
|
60
|
+
return sourceAt(source, source.lastIndexOf(`<${block.type}`, block.loc.start.offset));
|
|
61
|
+
}
|
|
62
|
+
/** The `.vue` files a component imports, by name. */
|
|
63
|
+
export function importsOf(source) {
|
|
64
|
+
return [...source.matchAll(/\bfrom\s*["'](?:[^"']*\/)?([^"'/]+)\.vue["']/g)].map((m) => m[1]);
|
|
65
|
+
}
|
|
66
|
+
function generics(comp, source, block) {
|
|
67
|
+
const text = block.attrs.generic;
|
|
68
|
+
if (typeof text !== "string")
|
|
69
|
+
return;
|
|
70
|
+
const open = source.lastIndexOf("<script", block.loc.start.offset);
|
|
71
|
+
const attr = source.slice(open, block.loc.start.offset).search(/\bgeneric\s*=\s*["']/);
|
|
72
|
+
const valueAt = open + attr + /^generic\s*=\s*["']/.exec(source.slice(open + attr))[0].length;
|
|
73
|
+
const at = sourceAt(source, valueAt).loc.start;
|
|
74
|
+
const prefix = "type T<";
|
|
75
|
+
let params;
|
|
76
|
+
try {
|
|
77
|
+
params = parseJs(`${prefix}${text}> = 0;`, { sourceType: "module", plugins: ["typescript"] }).program.body[0].typeParameters.params;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return fail(comp, "FV0301", "`generic` is a list of type parameters, as `T extends string`", sourceAt(source, valueAt));
|
|
81
|
+
}
|
|
82
|
+
const rebase = (n) => {
|
|
83
|
+
if (!n || typeof n !== "object")
|
|
84
|
+
return;
|
|
85
|
+
if (Array.isArray(n))
|
|
86
|
+
return n.forEach(rebase);
|
|
87
|
+
const start = n.loc?.start;
|
|
88
|
+
if (start && typeof n.type === "string") {
|
|
89
|
+
n.loc = { ...n.loc, start: start.line === 1 ? { line: at.line, column: at.column + start.column - prefix.length } : { line: at.line + start.line - 1, column: start.column } };
|
|
90
|
+
}
|
|
91
|
+
for (const [k, v] of Object.entries(n))
|
|
92
|
+
if (k !== "loc" && v && typeof v === "object")
|
|
93
|
+
rebase(v);
|
|
94
|
+
};
|
|
95
|
+
rebase(params);
|
|
96
|
+
tagAst(params, "source");
|
|
97
|
+
for (const p of params) {
|
|
98
|
+
const name = typeof p.name === "string" ? p.name : p.name.name;
|
|
99
|
+
if (!p.constraint)
|
|
100
|
+
fail(comp, "FV0302", `\`${name}\` in \`generic\` has no constraint: the server renders \`${name}\` as the type it extends, \`${name} extends string\``, p);
|
|
101
|
+
comp.aliases.set(name, p.constraint);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function scriptOf(comp, descriptor) {
|
|
105
|
+
try {
|
|
106
|
+
return compileScript(descriptor, { id: comp.name });
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
if (!(e instanceof SyntaxError))
|
|
110
|
+
throw e;
|
|
111
|
+
const at = e.loc;
|
|
112
|
+
const block = descriptor.scriptSetup.loc.start;
|
|
113
|
+
const where = !at || descriptor.script ? {} : at.line === 1 ? { line: block.line, column: block.column + at.column } : { line: block.line + at.line - 1, column: at.column + 1 };
|
|
114
|
+
throw new GenError("FV0001", e.message, { file: comp.file, ...where }, e.message.split("\n")[0]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function readComponent(file, root, isChild) {
|
|
53
118
|
const name = basename(file, ".vue");
|
|
54
119
|
const source = readFileSync(file, "utf8");
|
|
55
120
|
const { descriptor, errors } = parseSfc(source, { filename: file });
|
|
56
121
|
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
|
-
};
|
|
122
|
+
const comp = blankComponent(name, snake(name), rel);
|
|
90
123
|
comp.source = source;
|
|
91
124
|
if (errors.length)
|
|
92
|
-
fail(comp, String(errors[0]), vueErrorNode(errors[0]));
|
|
93
|
-
if (
|
|
94
|
-
|
|
125
|
+
fail(comp, "FV0002", String(errors[0]), vueErrorNode(errors[0]));
|
|
126
|
+
if (descriptor.script && !descriptor.scriptSetup) {
|
|
127
|
+
const needs = isChild ? "a child component must have" : "an island needs";
|
|
128
|
+
fail(comp, "FV0003", `${needs} \`<script setup lang="ts">\`, or no script at all: a \`<script>\` without \`setup\` (the Options API, \`defineComponent\`) is not translated`, blockAt(source, descriptor.script));
|
|
95
129
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// writes variables onto the root, neither of which the server reproduces.
|
|
130
|
+
if (!descriptor.template)
|
|
131
|
+
fail(comp, "FV0004", "a component needs a `<template>`: a render function is not translated", sourceAt(source, 0));
|
|
99
132
|
for (const st of descriptor.styles) {
|
|
100
133
|
if (st.module)
|
|
101
|
-
fail(comp, "`<style module>` renames classes in the bundler; use a global or scoped `<style>`, or a stylesheet");
|
|
134
|
+
fail(comp, "FV1005", "`<style module>` renames classes in the bundler; use a global or scoped `<style>`, or a stylesheet", blockAt(source, st));
|
|
102
135
|
}
|
|
136
|
+
const cssVar = descriptor.styles.find((st) => /\bv-bind\s*\(/.test(st.content));
|
|
103
137
|
if (descriptor.cssVars.length)
|
|
104
|
-
fail(comp, "`v-bind()` in `<style>` sets variables the server does not render; bind `:style` instead");
|
|
105
|
-
|
|
106
|
-
comp
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const ast = script.scriptSetupAst ?? [];
|
|
138
|
+
fail(comp, "FV1006", "`v-bind()` in `<style>` sets variables the server does not render; bind `:style` instead", cssVar ? blockAt(source, cssVar) : undefined);
|
|
139
|
+
for (const p of ctx.plugins)
|
|
140
|
+
p.sfc?.(comp, descriptor, file, source);
|
|
141
|
+
const script = descriptor.scriptSetup ? scriptOf(comp, descriptor) : null;
|
|
142
|
+
const ast = script?.scriptSetupAst ?? [];
|
|
110
143
|
tagAst(ast, "source");
|
|
111
|
-
|
|
112
|
-
const plainAst = script.scriptAst ?? [];
|
|
144
|
+
const plainAst = script?.scriptAst ?? [];
|
|
113
145
|
tagAst(plainAst, "source");
|
|
114
146
|
for (const s of [...plainAst, ...ast]) {
|
|
115
147
|
if (s.type !== "ImportDeclaration" || !s.source.value.endsWith(".vue"))
|
|
@@ -123,9 +155,10 @@ export function readComponent(file, root) {
|
|
|
123
155
|
}
|
|
124
156
|
}
|
|
125
157
|
}
|
|
158
|
+
for (const loaded of asyncChildren(comp, ast).values())
|
|
159
|
+
comp.imports.add(loaded);
|
|
126
160
|
typesImports(comp, [...plainAst, ...ast]);
|
|
127
161
|
comp.inheritAttrs = inheritAttrs(comp, [...plainAst, ...ast]);
|
|
128
|
-
// Types imported from elsewhere: a store's file, a shared `.ts` file, or another component.
|
|
129
162
|
for (const st of [...plainAst, ...ast]) {
|
|
130
163
|
if (st.type !== "ImportDeclaration")
|
|
131
164
|
continue;
|
|
@@ -143,12 +176,8 @@ export function readComponent(file, root) {
|
|
|
143
176
|
const typeFile = resolveImport(comp.file, from);
|
|
144
177
|
if (!typeFile)
|
|
145
178
|
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 });
|
|
179
|
+
if (ctx.plugins.some((p) => p.importedType?.(comp, typeFile, typeName, sp.local.name)))
|
|
150
180
|
continue;
|
|
151
|
-
}
|
|
152
181
|
if (ctx.helperModule !== null && from === ctx.helperModule && !isType)
|
|
153
182
|
continue;
|
|
154
183
|
readTypeFile(typeFile);
|
|
@@ -158,8 +187,8 @@ export function readComponent(file, root) {
|
|
|
158
187
|
comp.aliases.set(sp.local.name, ctx.typeAliases.get(typeName));
|
|
159
188
|
}
|
|
160
189
|
}
|
|
161
|
-
|
|
162
|
-
|
|
190
|
+
if (descriptor.scriptSetup)
|
|
191
|
+
generics(comp, source, descriptor.scriptSetup);
|
|
163
192
|
const decls = declareTypes(comp, [...plainAst, ...ast], comp.structs, comp.aliases);
|
|
164
193
|
for (const d of decls)
|
|
165
194
|
comp.structs.set(d.name, structOf(comp, d.name, d.members, comp.structs));
|
|
@@ -168,15 +197,13 @@ export function readComponent(file, root) {
|
|
|
168
197
|
const call = s.type === "ExpressionStatement" ? s.expression : s.type === "VariableDeclaration" ? s.declarations[0]?.init : null;
|
|
169
198
|
const inner = call?.type === "CallExpression" && call.callee.type === "Identifier" && call.callee.name === "withDefaults" ? call.arguments[0] : call;
|
|
170
199
|
if (inner?.type === "CallExpression" && inner.callee.type === "Identifier" && inner.callee.name === "defineProps" && !inner.typeParameters) {
|
|
171
|
-
fail(comp, "props are declared with a type, `defineProps<{ ... }>()`: a runtime declaration has no Rust type", inner);
|
|
200
|
+
fail(comp, "FV0303", "props are declared with a type, `defineProps<{ ... }>()`: a runtime declaration has no Rust type", inner);
|
|
172
201
|
}
|
|
173
202
|
const found = definePropsType(call);
|
|
174
203
|
if (found)
|
|
175
204
|
propsTy = found;
|
|
176
205
|
}
|
|
177
|
-
// A component without `defineProps` takes no props.
|
|
178
206
|
if (!propsTy) {
|
|
179
|
-
// nothing to read
|
|
180
207
|
}
|
|
181
208
|
else if (propsTy.type === "TSTypeLiteral") {
|
|
182
209
|
comp.props = structOf(comp, "Props", propsTy.members, comp.structs);
|
|
@@ -185,9 +212,8 @@ export function readComponent(file, root) {
|
|
|
185
212
|
comp.props = { name: "Props", fields: comp.structs.get(propsTy.typeName.name).fields };
|
|
186
213
|
}
|
|
187
214
|
else {
|
|
188
|
-
fail(comp, "`defineProps` takes a type literal or an interface declared in the same block", propsTy);
|
|
215
|
+
fail(comp, "FV0304", "`defineProps` takes a type literal or an interface declared in the same block", propsTy);
|
|
189
216
|
}
|
|
190
|
-
// `defineModel`: a prop of its own (`modelValue` unless named), which the binding reads.
|
|
191
217
|
for (const st of ast) {
|
|
192
218
|
if (st.type !== "VariableDeclaration")
|
|
193
219
|
continue;
|
|
@@ -197,41 +223,40 @@ export function readComponent(file, root) {
|
|
|
197
223
|
continue;
|
|
198
224
|
const t = call.typeParameters?.params?.[0];
|
|
199
225
|
if (!t)
|
|
200
|
-
fail(comp, "`defineModel` needs its type: `defineModel<string>()`", call);
|
|
226
|
+
fail(comp, "FV0305", "`defineModel` needs its type: `defineModel<string>()`", call);
|
|
201
227
|
const named = call.arguments[0]?.type === "StringLiteral" ? call.arguments[0].value : "modelValue";
|
|
202
228
|
const options = call.arguments.find((a) => a.type === "ObjectExpression");
|
|
203
229
|
const required = options?.properties.some((p) => p.type === "ObjectProperty" && (p.key.name ?? p.key.value) === "required" && p.value.type === "BooleanLiteral" && p.value.value);
|
|
204
230
|
const base = tyOfTs(comp, t, comp.structs);
|
|
231
|
+
if (!required && absence(base) === "null")
|
|
232
|
+
fail(comp, "FV0306", `a \`defineModel\` of \`T | null\` that is not \`required\` may be absent, which is \`undefined\`, or \`null\`: ${ONE_NOTHING}`, call);
|
|
205
233
|
comp.props.fields.push({ js: named, rust: snake(named), ty: required ? base : opt(base) });
|
|
206
234
|
comp.models.set(d.id.name, named);
|
|
207
235
|
}
|
|
208
236
|
}
|
|
209
|
-
|
|
210
|
-
// `withDefaults`, a destructured default, or `defineModel`'s options — or `false` for a boolean.
|
|
211
|
-
const defaults = runtimeDefaults(comp, script.content);
|
|
237
|
+
const defaults = runtimeDefaults(comp, script?.content ?? "");
|
|
212
238
|
for (const f of comp.props.fields) {
|
|
213
239
|
if (f.ty.k !== "opt")
|
|
214
240
|
continue;
|
|
215
241
|
const node = defaults.get(f.js);
|
|
242
|
+
if (f.ty.none !== undefined) {
|
|
243
|
+
if (node)
|
|
244
|
+
fail(comp, "FV0307", `a default for \`${f.js}\`, which is \`T | null\`: Vue gives it only when \`${f.js}\` is absent, which its Rust type, an \`Option\`, cannot be; fall back in the template with \`??\``, sourceDefault(ast, f.js) ?? node);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
216
247
|
if (node)
|
|
217
|
-
f.dflt = defaultValue(comp, f, node);
|
|
248
|
+
f.dflt = defaultValue(comp, f, node, sourceDefault(ast, f.js) ?? undefined);
|
|
218
249
|
else if (f.ty.of.k === "bool")
|
|
219
250
|
f.dflt = "false";
|
|
220
251
|
}
|
|
221
252
|
const compiled = compileTemplate({
|
|
222
253
|
source: descriptor.template.content,
|
|
223
254
|
filename: file,
|
|
224
|
-
|
|
225
|
-
// slot content only with `:slotted()` styles.
|
|
226
|
-
id: comp.scopeId ?? name,
|
|
227
|
-
scoped: comp.scopeId !== null,
|
|
228
|
-
slotted: comp.slotted,
|
|
255
|
+
...(claim((p) => p.templateOptions?.(comp)) ?? { id: name, scoped: false, slotted: false }),
|
|
229
256
|
ssr: true,
|
|
230
257
|
ssrCssVars: [],
|
|
231
|
-
compilerOptions: { bindingMetadata: script.bindings, sourceMap: true },
|
|
258
|
+
compilerOptions: { ...(script ? { bindingMetadata: script.bindings } : {}), sourceMap: true },
|
|
232
259
|
});
|
|
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
260
|
const kids = (n) => n.children.filter((c) => c.type !== 3 && !(c.type === 2 && !c.content.trim()));
|
|
236
261
|
const roots = descriptor.template.ast ? kids(descriptor.template.ast) : [];
|
|
237
262
|
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;
|
|
@@ -241,24 +266,18 @@ export function readComponent(file, root) {
|
|
|
241
266
|
if (!lone)
|
|
242
267
|
comp.attrsDropped = { type: "VueTemplate", loc: { start: { line: wrapper.loc.start.line, column: wrapper.loc.start.column - 1 } }, __fv: "source" };
|
|
243
268
|
}
|
|
269
|
+
comp.templateAst = descriptor.template.ast;
|
|
244
270
|
const start = descriptor.template.loc.start;
|
|
245
271
|
comp.templateStart = { line: start.line, column: start.column };
|
|
246
272
|
if (compiled.map)
|
|
247
273
|
comp.templateMap = new SourceMapConsumer(compiled.map);
|
|
248
|
-
// A template error's position is within the template's content.
|
|
249
274
|
if (compiled.errors.length)
|
|
250
|
-
fail(comp, String(compiled.errors[0]), vueErrorNode(compiled.errors[0], comp.templateStart));
|
|
275
|
+
fail(comp, "FV0005", String(compiled.errors[0]), vueErrorNode(compiled.errors[0], comp.templateStart));
|
|
251
276
|
for (const m of compiled.code.matchAll(/_ssrRenderSlot\(_ctx\.\$slots, "([^"]+)"/g)) {
|
|
252
277
|
if (!comp.slotNames.includes(m[1]))
|
|
253
278
|
comp.slotNames.push(m[1]);
|
|
254
279
|
}
|
|
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(");
|
|
280
|
+
for (const p of ctx.plugins)
|
|
281
|
+
p.compiled?.(comp, compiled.code, [...plainAst, ...ast]);
|
|
263
282
|
return { comp, ast, ssr: compiled.code };
|
|
264
283
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Component, type N, type Scope, type Val } from "./model.ts";
|
|
2
|
+
export type Const = {
|
|
3
|
+
k: "scalar";
|
|
4
|
+
js: Scalar;
|
|
5
|
+
} | {
|
|
6
|
+
k: "list";
|
|
7
|
+
items: Const[];
|
|
8
|
+
node: N;
|
|
9
|
+
home: Component;
|
|
10
|
+
path: string;
|
|
11
|
+
typed: string | null;
|
|
12
|
+
} | {
|
|
13
|
+
k: "object";
|
|
14
|
+
fields: Map<string, Const>;
|
|
15
|
+
node: N;
|
|
16
|
+
home: Component;
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
type Scalar = string | number | boolean | null;
|
|
20
|
+
export interface Declared {
|
|
21
|
+
node: N;
|
|
22
|
+
home: Component;
|
|
23
|
+
exported: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Remember the constants and enums a file declares at its top level, to evaluate when one is used. */
|
|
26
|
+
export declare function declareConsts(home: Component, body: N[]): Map<string, Declared>;
|
|
27
|
+
/** What a declared constant or enum stands for, evaluated once. */
|
|
28
|
+
export declare function constOfDecl(d: Declared): Const;
|
|
29
|
+
/** The type an enum stands for: the union of its members' literal types. */
|
|
30
|
+
export declare function enumType(home: Component, d: N): N;
|
|
31
|
+
/** What a constant is as a value in Rust; an object has none, only its fields. */
|
|
32
|
+
export declare function constVal(c: Const, at: {
|
|
33
|
+
comp: Component;
|
|
34
|
+
node: N;
|
|
35
|
+
what: string;
|
|
36
|
+
}): Val;
|
|
37
|
+
/** The constant an expression names: an imported or enum binding, or a field of one. */
|
|
38
|
+
export declare function constOf(s: Scope, n: N): {
|
|
39
|
+
c: Const;
|
|
40
|
+
what: string;
|
|
41
|
+
} | null;
|
|
42
|
+
export {};
|