ferrovue 0.2.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 +103 -15
- package/dist/attrs.d.ts +8 -42
- package/dist/attrs.js +148 -297
- 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 +121 -27
- package/dist/client.d.ts +11 -2
- package/dist/client.js +45 -9
- package/dist/compiler.d.ts +4 -0
- package/dist/compiler.js +37 -90
- package/dist/component.js +19 -82
- package/dist/context.d.ts +6 -68
- package/dist/context.js +25 -38
- package/dist/diff.d.ts +1 -0
- package/dist/diff.js +68 -0
- package/dist/emitter.d.ts +0 -8
- package/dist/emitter.js +0 -12
- package/dist/expr.d.ts +0 -70
- package/dist/expr.js +30 -855
- 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 +6 -0
- package/dist/islands.js +5 -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 +22 -130
- package/dist/model.js +37 -9
- 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} +65 -57
- 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/{scoped.js → plugins/scoped.js} +55 -20
- 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 +2 -5
- package/dist/rust.js +66 -161
- package/dist/script.d.ts +0 -6
- package/dist/script.js +22 -121
- 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 +2 -40
- package/dist/template.js +40 -691
- 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 +35 -14
- package/package.json +11 -2
- package/dist/i18n.d.ts +0 -27
- package/dist/router.d.ts +0 -17
- package/dist/scoped.d.ts +0 -10
- package/dist/stores.d.ts +0 -6
- package/dist/stores.js +0 -227
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 {
|
|
28
|
-
import { componentSource,
|
|
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,54 +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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
scopeFlow(read.map((r, i) => ({ comp: r.comp, ssr: r.ssr, children: children[i] })));
|
|
72
|
-
for (const c of components.values())
|
|
73
|
-
c.usesRoute = c.routerLink || c.readsRoute;
|
|
74
|
-
for (let changed = true; changed;) {
|
|
75
|
-
changed = false;
|
|
76
|
-
for (const c of components.values()) {
|
|
77
|
-
if (!c.usesRoute && [...c.imports].some((i) => components.get(i)?.usesRoute)) {
|
|
78
|
-
c.usesRoute = changed = true;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
for (const c of components.values())
|
|
83
|
-
c.usesI18n = c.readsI18n;
|
|
84
|
-
for (let changed = true; changed;) {
|
|
85
|
-
changed = false;
|
|
86
|
-
for (const c of components.values()) {
|
|
87
|
-
if (!c.usesI18n && [...c.imports].some((i) => components.get(i)?.usesI18n)) {
|
|
88
|
-
c.usesI18n = changed = true;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
for (const c of components.values())
|
|
93
|
-
c.usesTeleports = c.readsTeleports;
|
|
94
|
-
for (let changed = true; changed;) {
|
|
95
|
-
changed = false;
|
|
96
|
-
for (const c of components.values()) {
|
|
97
|
-
if (!c.usesTeleports && [...c.imports].some((i) => components.get(i)?.usesTeleports)) {
|
|
98
|
-
c.usesTeleports = changed = true;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
39
|
+
const scopes = read.map((r) => scopeFor(r.comp, r.ast, components).scope);
|
|
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);
|
|
102
44
|
for (const c of components.values())
|
|
103
|
-
c.
|
|
45
|
+
c.takes = new Set(renderParams().filter((p) => p.reads(c)).map((p) => p.name));
|
|
104
46
|
for (let changed = true; changed;) {
|
|
105
47
|
changed = false;
|
|
106
48
|
for (const c of components.values()) {
|
|
107
|
-
|
|
108
|
-
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
|
+
}
|
|
109
54
|
}
|
|
110
55
|
}
|
|
111
56
|
}
|
|
112
57
|
const out = new Map();
|
|
113
|
-
// A child before its parents, which read the props its scoped slots pass.
|
|
114
58
|
const ordered = [];
|
|
115
59
|
const placed = new Set();
|
|
116
60
|
const place = (r) => {
|
|
@@ -126,19 +70,13 @@ export function generate(root, config = loadConfig(root)) {
|
|
|
126
70
|
};
|
|
127
71
|
read.forEach(place);
|
|
128
72
|
for (const r of ordered) {
|
|
129
|
-
// Numbered per component, so a file's text depends on its own source alone.
|
|
130
73
|
ctx.narrowCount = 0;
|
|
131
74
|
out.set(`${r.comp.module}.rs`, componentSource(r.comp, r.ast, r.ssr, components));
|
|
132
75
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
out.set("stores.rs", storesSource(config.stores.replace(/\/?$/, "/")));
|
|
138
|
-
if (ctx.typeStructs.size)
|
|
139
|
-
out.set("types.rs", typesSource());
|
|
140
|
-
if (ctx.i18n)
|
|
141
|
-
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);
|
|
142
80
|
return out;
|
|
143
81
|
}
|
|
144
82
|
/** Write what \`generate\` produces to the configured directory: only the files whose text changed,
|
|
@@ -155,7 +93,6 @@ export function write(root, config = loadConfig(root)) {
|
|
|
155
93
|
old = readFileSync(join(target, name), "utf8");
|
|
156
94
|
}
|
|
157
95
|
catch {
|
|
158
|
-
// A new file.
|
|
159
96
|
}
|
|
160
97
|
if (old !== text) {
|
|
161
98
|
writeFileSync(join(target, name), text);
|
|
@@ -165,7 +102,17 @@ export function write(root, config = loadConfig(root)) {
|
|
|
165
102
|
const removed = readdirSync(target).filter((f) => f.endsWith(".rs") && !files.has(f));
|
|
166
103
|
for (const f of removed)
|
|
167
104
|
rmSync(join(target, f));
|
|
168
|
-
|
|
105
|
+
const islands = Object.fromEntries([...ctx.components.values()].filter(isIsland).map((c) => [c.name, c.file]));
|
|
106
|
+
return { files: [...files.keys()], changed, removed, islands };
|
|
169
107
|
}
|
|
170
108
|
export { CONFIG_FILE, loadConfig, TYPES_MODULE } from "./context.js";
|
|
171
109
|
export { GenError } from "./model.js";
|
|
110
|
+
export const VERSION = (() => {
|
|
111
|
+
try {
|
|
112
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
113
|
+
return pkg.version ?? "unknown";
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return "unknown";
|
|
117
|
+
}
|
|
118
|
+
})();
|
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,59 +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
|
-
};
|
|
43
|
+
const comp = blankComponent(name, snake(name), rel);
|
|
88
44
|
comp.source = source;
|
|
89
45
|
if (errors.length)
|
|
90
46
|
fail(comp, String(errors[0]), vueErrorNode(errors[0]));
|
|
91
47
|
if (!descriptor.scriptSetup || !descriptor.template) {
|
|
92
48
|
fail(comp, "an island needs `<script setup lang=\"ts\">` and a `<template>`");
|
|
93
49
|
}
|
|
94
|
-
// A global `<style>` block changes no markup. A scoped one adds `data-v-…` attributes, whose id is
|
|
95
|
-
// computed here as the bundler computes it. A CSS module renames classes, and `v-bind()` in CSS
|
|
96
|
-
// writes variables onto the root, neither of which the server reproduces.
|
|
97
50
|
for (const st of descriptor.styles) {
|
|
98
51
|
if (st.module)
|
|
99
52
|
fail(comp, "`<style module>` renames classes in the bundler; use a global or scoped `<style>`, or a stylesheet");
|
|
100
53
|
}
|
|
101
54
|
if (descriptor.cssVars.length)
|
|
102
55
|
fail(comp, "`v-bind()` in `<style>` sets variables the server does not render; bind `:style` instead");
|
|
103
|
-
|
|
104
|
-
comp
|
|
105
|
-
comp.slotted = descriptor.slotted;
|
|
56
|
+
for (const p of ctx.plugins)
|
|
57
|
+
p.sfc?.(comp, descriptor, file, source);
|
|
106
58
|
const script = compileScript(descriptor, { id: name });
|
|
107
59
|
const ast = script.scriptSetupAst ?? [];
|
|
108
60
|
tagAst(ast, "source");
|
|
109
|
-
// A plain `<script>` beside the setup one may declare the types the setup block uses.
|
|
110
61
|
const plainAst = script.scriptAst ?? [];
|
|
111
62
|
tagAst(plainAst, "source");
|
|
112
63
|
for (const s of [...plainAst, ...ast]) {
|
|
@@ -123,7 +74,6 @@ export function readComponent(file, root) {
|
|
|
123
74
|
}
|
|
124
75
|
typesImports(comp, [...plainAst, ...ast]);
|
|
125
76
|
comp.inheritAttrs = inheritAttrs(comp, [...plainAst, ...ast]);
|
|
126
|
-
// Types imported from elsewhere: a store's file, a shared `.ts` file, or another component.
|
|
127
77
|
for (const st of [...plainAst, ...ast]) {
|
|
128
78
|
if (st.type !== "ImportDeclaration")
|
|
129
79
|
continue;
|
|
@@ -141,12 +91,8 @@ export function readComponent(file, root) {
|
|
|
141
91
|
const typeFile = resolveImport(comp.file, from);
|
|
142
92
|
if (!typeFile)
|
|
143
93
|
continue;
|
|
144
|
-
|
|
145
|
-
if ([...ctx.stores.values()].some((x) => x.module === module)) {
|
|
146
|
-
if (ctx.storeStructs.has(typeName))
|
|
147
|
-
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)))
|
|
148
95
|
continue;
|
|
149
|
-
}
|
|
150
96
|
if (ctx.helperModule !== null && from === ctx.helperModule && !isType)
|
|
151
97
|
continue;
|
|
152
98
|
readTypeFile(typeFile);
|
|
@@ -156,8 +102,6 @@ export function readComponent(file, root) {
|
|
|
156
102
|
comp.aliases.set(sp.local.name, ctx.typeAliases.get(typeName));
|
|
157
103
|
}
|
|
158
104
|
}
|
|
159
|
-
// Interfaces in two passes: every name first, so one may name another declared after it, or
|
|
160
|
-
// itself — a tree node's children are tree nodes.
|
|
161
105
|
const decls = declareTypes(comp, [...plainAst, ...ast], comp.structs, comp.aliases);
|
|
162
106
|
for (const d of decls)
|
|
163
107
|
comp.structs.set(d.name, structOf(comp, d.name, d.members, comp.structs));
|
|
@@ -172,9 +116,7 @@ export function readComponent(file, root) {
|
|
|
172
116
|
if (found)
|
|
173
117
|
propsTy = found;
|
|
174
118
|
}
|
|
175
|
-
// A component without `defineProps` takes no props.
|
|
176
119
|
if (!propsTy) {
|
|
177
|
-
// nothing to read
|
|
178
120
|
}
|
|
179
121
|
else if (propsTy.type === "TSTypeLiteral") {
|
|
180
122
|
comp.props = structOf(comp, "Props", propsTy.members, comp.structs);
|
|
@@ -185,7 +127,6 @@ export function readComponent(file, root) {
|
|
|
185
127
|
else {
|
|
186
128
|
fail(comp, "`defineProps` takes a type literal or an interface declared in the same block", propsTy);
|
|
187
129
|
}
|
|
188
|
-
// `defineModel`: a prop of its own (`modelValue` unless named), which the binding reads.
|
|
189
130
|
for (const st of ast) {
|
|
190
131
|
if (st.type !== "VariableDeclaration")
|
|
191
132
|
continue;
|
|
@@ -204,8 +145,6 @@ export function readComponent(file, root) {
|
|
|
204
145
|
comp.models.set(d.id.name, named);
|
|
205
146
|
}
|
|
206
147
|
}
|
|
207
|
-
// What Vue uses for an absent optional prop: the default `compileScript` resolved — from
|
|
208
|
-
// `withDefaults`, a destructured default, or `defineModel`'s options — or `false` for a boolean.
|
|
209
148
|
const defaults = runtimeDefaults(comp, script.content);
|
|
210
149
|
for (const f of comp.props.fields) {
|
|
211
150
|
if (f.ty.k !== "opt")
|
|
@@ -219,33 +158,31 @@ export function readComponent(file, root) {
|
|
|
219
158
|
const compiled = compileTemplate({
|
|
220
159
|
source: descriptor.template.content,
|
|
221
160
|
filename: file,
|
|
222
|
-
|
|
223
|
-
// slot content only with `:slotted()` styles.
|
|
224
|
-
id: comp.scopeId ?? name,
|
|
225
|
-
scoped: comp.scopeId !== null,
|
|
226
|
-
slotted: comp.slotted,
|
|
161
|
+
...(claim((p) => p.templateOptions?.(comp)) ?? { id: name, scoped: false, slotted: false }),
|
|
227
162
|
ssr: true,
|
|
228
163
|
ssrCssVars: [],
|
|
229
164
|
compilerOptions: { bindingMetadata: script.bindings, sourceMap: true },
|
|
230
165
|
});
|
|
166
|
+
const kids = (n) => n.children.filter((c) => c.type !== 3 && !(c.type === 2 && !c.content.trim()));
|
|
167
|
+
const roots = descriptor.template.ast ? kids(descriptor.template.ast) : [];
|
|
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;
|
|
169
|
+
if (wrapper) {
|
|
170
|
+
const inner = kids(wrapper);
|
|
171
|
+
const lone = inner.length === 1 && inner[0].type === 1 && (inner[0].tagType === 0 || inner[0].tagType === 1) && !inner[0].props.some((p) => p.type === 7 && ["if", "else-if", "else", "for"].includes(p.name));
|
|
172
|
+
if (!lone)
|
|
173
|
+
comp.attrsDropped = { type: "VueTemplate", loc: { start: { line: wrapper.loc.start.line, column: wrapper.loc.start.column - 1 } }, __fv: "source" };
|
|
174
|
+
}
|
|
231
175
|
const start = descriptor.template.loc.start;
|
|
232
176
|
comp.templateStart = { line: start.line, column: start.column };
|
|
233
177
|
if (compiled.map)
|
|
234
178
|
comp.templateMap = new SourceMapConsumer(compiled.map);
|
|
235
|
-
// A template error's position is within the template's content.
|
|
236
179
|
if (compiled.errors.length)
|
|
237
180
|
fail(comp, String(compiled.errors[0]), vueErrorNode(compiled.errors[0], comp.templateStart));
|
|
238
181
|
for (const m of compiled.code.matchAll(/_ssrRenderSlot\(_ctx\.\$slots, "([^"]+)"/g)) {
|
|
239
182
|
if (!comp.slotNames.includes(m[1]))
|
|
240
183
|
comp.slotNames.push(m[1]);
|
|
241
184
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
st.specifiers.some((sp) => sp.type === "ImportSpecifier" && (sp.imported.name ?? sp.imported.value) === exported));
|
|
245
|
-
comp.routerLink = compiled.code.includes('_resolveComponent("RouterLink")') || imported("RouterLink");
|
|
246
|
-
comp.routerView = compiled.code.includes('_resolveComponent("RouterView")') || imported("RouterView");
|
|
247
|
-
comp.readsRoute = compiled.code.includes("_ctx.$route");
|
|
248
|
-
comp.readsI18n = compiled.code.includes("_ctx.$t(");
|
|
249
|
-
comp.readsTeleports = compiled.code.includes("_ssrRenderTeleport(");
|
|
185
|
+
for (const p of ctx.plugins)
|
|
186
|
+
p.compiled?.(comp, compiled.code, [...plainAst, ...ast]);
|
|
250
187
|
return { comp, ast, ssr: compiled.code };
|
|
251
188
|
}
|
package/dist/context.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { type Component, type
|
|
1
|
+
import type { Plugin } from "./plugin.ts";
|
|
2
|
+
import { type Component, type Struct, type Ty } from "./model.ts";
|
|
3
3
|
/** A type a helper takes or returns, as the configuration spells it. */
|
|
4
4
|
export type TypeName = "string" | "string?" | "int" | "int?" | "float" | "float?" | "bool";
|
|
5
5
|
/** A function a template may call, and the Rust function that is its twin. */
|
|
@@ -44,7 +44,7 @@ export interface Config {
|
|
|
44
44
|
* `v-`: `["focus", "click-outside"]`. Any other custom directive is refused, since the server
|
|
45
45
|
* cannot know what its `getSSRProps` would add. */
|
|
46
46
|
clientDirectives?: string[];
|
|
47
|
-
/** vue-i18n: the directory of locale files (
|
|
47
|
+
/** vue-i18n: the directory of locale files (`en.json`, `nl.json`), the locale a page renders in
|
|
48
48
|
* when it names none, and the locales a missing message falls back to. */
|
|
49
49
|
i18n?: {
|
|
50
50
|
messages: string;
|
|
@@ -62,50 +62,11 @@ export interface Config {
|
|
|
62
62
|
export type ScopeIdMode = "filepath" | "filepath-source";
|
|
63
63
|
export declare const CONFIG_FILE = "ferrovue.config.json";
|
|
64
64
|
/** Read the project's configuration. */
|
|
65
|
-
export declare function loadConfig(root: string): Config;
|
|
65
|
+
export declare function loadConfig(root: string, configPath?: string): Config;
|
|
66
66
|
export declare function tyOfName(name: TypeName): Ty;
|
|
67
|
-
/** A route as the routes file lists it: a vue-router path, and the name it may have. */
|
|
68
|
-
export interface RouteDef {
|
|
69
|
-
/** The path as written: relative to the parent's unless it starts with \`/\`. */
|
|
70
|
-
path: string;
|
|
71
|
-
name?: string;
|
|
72
|
-
/** The routes nested in it. */
|
|
73
|
-
children?: RouteDef[];
|
|
74
|
-
/** The path with its ancestors', as vue-router normalises it. */
|
|
75
|
-
fullPath: string;
|
|
76
|
-
}
|
|
77
|
-
/** Every route, nested ones included, parents first. */
|
|
78
|
-
export declare function allRoutes(routes: RouteDef[]): RouteDef[];
|
|
79
|
-
/** A getter: the parameter naming the state (an option store's), and the expression it returns.
|
|
80
|
-
* \`setup\` marks a setup store's \`computed\`, which reads the state's refs and the other getters by
|
|
81
|
-
* name, through \`.value\`. */
|
|
82
|
-
export interface StoreGetter {
|
|
83
|
-
param: string | null;
|
|
84
|
-
body: N;
|
|
85
|
-
file: string;
|
|
86
|
-
setup?: true;
|
|
87
|
-
}
|
|
88
|
-
/** A Pinia option store: `export const usePrefs = defineStore("prefs", { state: (): PrefsState => ... })`. */
|
|
89
|
-
export interface Store {
|
|
90
|
-
/** `usePrefs` */
|
|
91
|
-
hook: string;
|
|
92
|
-
/** `prefs`: its key in `pinia.state.value`. */
|
|
93
|
-
id: string;
|
|
94
|
-
/** `prefs`: its field in the generated `Stores`. */
|
|
95
|
-
field: string;
|
|
96
|
-
/** `PrefsState`: the interface its state is. */
|
|
97
|
-
state: string;
|
|
98
|
-
/** The file, without its extension, as an import names it once resolved. */
|
|
99
|
-
module: string;
|
|
100
|
-
/** Its getters: name → the parameter that names the state, and the expression returned. */
|
|
101
|
-
getters: Map<string, StoreGetter>;
|
|
102
|
-
}
|
|
103
67
|
/** Where `TrustedHtml` comes from. Only that import names the type `v-html` will write raw. */
|
|
104
68
|
export declare const TYPES_MODULE = "ferrovue/types";
|
|
105
|
-
/** What the configuration being compiled says, and what has been read so far: set up by
|
|
106
|
-
* `generate()` at the start of each run, which is synchronous, so one run never sees another's. */
|
|
107
69
|
export declare const ctx: {
|
|
108
|
-
/** The helpers of the configuration being compiled: name → Rust twin, typed. */
|
|
109
70
|
helpers: Record<string, {
|
|
110
71
|
rust: string;
|
|
111
72
|
params: Ty[];
|
|
@@ -114,38 +75,15 @@ export declare const ctx: {
|
|
|
114
75
|
}>;
|
|
115
76
|
helperModule: string | null;
|
|
116
77
|
trustedHtml: string | null;
|
|
117
|
-
/** Custom directives declared to render nothing on the server. */
|
|
118
78
|
clientDirectives: Set<string>;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
/** The history's base, and the class names active links take, from the configured router. */
|
|
122
|
-
routerBase: string;
|
|
123
|
-
linkActive: string;
|
|
124
|
-
linkExactActive: string;
|
|
125
|
-
/** The configured stores, by hook name, and every interface their files declare. */
|
|
126
|
-
stores: Map<string, Store>;
|
|
127
|
-
storeStructs: Map<string, Struct>;
|
|
128
|
-
/** The store file each of those interfaces is declared in. */
|
|
129
|
-
storeFiles: Map<string, string>;
|
|
130
|
-
/** The project root, for resolving a component's imports. */
|
|
79
|
+
plugins: readonly Plugin[];
|
|
80
|
+
runs: Map<Plugin<unknown, unknown>, unknown>;
|
|
131
81
|
rootDir: string;
|
|
132
|
-
/** Interfaces and object types declared in shared `.ts` files that components import, and type
|
|
133
|
-
* aliases there; the file each comes from. They are written once, to `types.rs`. */
|
|
134
82
|
typeStructs: Map<string, Struct>;
|
|
135
83
|
typeAliases: Map<string, any>;
|
|
136
84
|
typeFiles: Map<string, string>;
|
|
137
|
-
/** The shared files read so far. */
|
|
138
85
|
typeRead: Set<string>;
|
|
139
|
-
/** Every component being compiled, by name, for a type that names another one's props. */
|
|
140
86
|
components: Map<string, Component>;
|
|
141
|
-
/** Numbers the bindings narrowing introduces, restarted per component so that a file's text
|
|
142
|
-
* depends on its own source alone. */
|
|
143
87
|
narrowCount: number;
|
|
144
|
-
/** The directory the components being compiled come from, for the generated headers. */
|
|
145
88
|
componentsDir: string;
|
|
146
|
-
/** The configured locales and their messages, when there are any. */
|
|
147
|
-
i18n: I18nSetup | null;
|
|
148
|
-
/** How scope ids are computed, and the directory a component's path is hashed from. */
|
|
149
|
-
scopeId: ScopeIdMode;
|
|
150
|
-
viteRoot: string;
|
|
151
89
|
};
|
package/dist/context.js
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
|
-
/* The project's configuration, and the state one `generate()` run builds up. */
|
|
2
1
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
4
3
|
import { BOOL, FLOAT, GenError, INT, opt, STR } from "./model.js";
|
|
5
4
|
export const CONFIG_FILE = "ferrovue.config.json";
|
|
6
5
|
/** Read the project's configuration. */
|
|
7
|
-
export function loadConfig(root) {
|
|
8
|
-
const
|
|
6
|
+
export function loadConfig(root, configPath) {
|
|
7
|
+
const filePath = configPath ? resolve(root, configPath) : join(root, CONFIG_FILE);
|
|
8
|
+
const displayName = configPath ?? CONFIG_FILE;
|
|
9
|
+
let text = "";
|
|
10
|
+
try {
|
|
11
|
+
text = readFileSync(filePath, "utf8");
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
if (e.code === "ENOENT") {
|
|
15
|
+
throw new GenError(`cannot find \`${displayName}\` in ${root}`);
|
|
16
|
+
}
|
|
17
|
+
throw e;
|
|
18
|
+
}
|
|
19
|
+
let raw;
|
|
20
|
+
try {
|
|
21
|
+
raw = JSON.parse(text);
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
throw new GenError(`failed to parse \`${displayName}\`: ${e.message}`);
|
|
25
|
+
}
|
|
9
26
|
if (typeof raw.components !== "string" || typeof raw.out !== "string") {
|
|
10
|
-
throw new GenError(`${
|
|
27
|
+
throw new GenError(`${displayName} needs \`components\` and \`out\` directories`);
|
|
11
28
|
}
|
|
12
29
|
if (raw.scopeId !== undefined && raw.scopeId !== "filepath" && raw.scopeId !== "filepath-source") {
|
|
13
|
-
throw new GenError(`\`scopeId\` in ${
|
|
30
|
+
throw new GenError(`\`scopeId\` in ${displayName} is "filepath" or "filepath-source", as \`@vitejs/plugin-vue\` computes it`);
|
|
14
31
|
}
|
|
15
32
|
return raw;
|
|
16
33
|
}
|
|
@@ -34,51 +51,21 @@ export function tyOfName(name) {
|
|
|
34
51
|
throw new GenError(`unknown helper type \`${String(name)}\``);
|
|
35
52
|
}
|
|
36
53
|
}
|
|
37
|
-
/** Every route, nested ones included, parents first. */
|
|
38
|
-
export function allRoutes(routes) {
|
|
39
|
-
return routes.flatMap((r) => [r, ...allRoutes(r.children ?? [])]);
|
|
40
|
-
}
|
|
41
54
|
/** Where `TrustedHtml` comes from. Only that import names the type `v-html` will write raw. */
|
|
42
55
|
export const TYPES_MODULE = "ferrovue/types";
|
|
43
|
-
/** What the configuration being compiled says, and what has been read so far: set up by
|
|
44
|
-
* `generate()` at the start of each run, which is synchronous, so one run never sees another's. */
|
|
45
56
|
export const ctx = {
|
|
46
|
-
/** The helpers of the configuration being compiled: name → Rust twin, typed. */
|
|
47
57
|
helpers: {},
|
|
48
58
|
helperModule: null,
|
|
49
59
|
trustedHtml: null,
|
|
50
|
-
/** Custom directives declared to render nothing on the server. */
|
|
51
60
|
clientDirectives: new Set(),
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/** The history's base, and the class names active links take, from the configured router. */
|
|
55
|
-
routerBase: "",
|
|
56
|
-
linkActive: "router-link-active",
|
|
57
|
-
linkExactActive: "router-link-exact-active",
|
|
58
|
-
/** The configured stores, by hook name, and every interface their files declare. */
|
|
59
|
-
stores: new Map(),
|
|
60
|
-
storeStructs: new Map(),
|
|
61
|
-
/** The store file each of those interfaces is declared in. */
|
|
62
|
-
storeFiles: new Map(),
|
|
63
|
-
/** The project root, for resolving a component's imports. */
|
|
61
|
+
plugins: [],
|
|
62
|
+
runs: new Map(),
|
|
64
63
|
rootDir: "",
|
|
65
|
-
/** Interfaces and object types declared in shared `.ts` files that components import, and type
|
|
66
|
-
* aliases there; the file each comes from. They are written once, to `types.rs`. */
|
|
67
64
|
typeStructs: new Map(),
|
|
68
65
|
typeAliases: new Map(),
|
|
69
66
|
typeFiles: new Map(),
|
|
70
|
-
/** The shared files read so far. */
|
|
71
67
|
typeRead: new Set(),
|
|
72
|
-
/** Every component being compiled, by name, for a type that names another one's props. */
|
|
73
68
|
components: new Map(),
|
|
74
|
-
/** Numbers the bindings narrowing introduces, restarted per component so that a file's text
|
|
75
|
-
* depends on its own source alone. */
|
|
76
69
|
narrowCount: 0,
|
|
77
|
-
/** The directory the components being compiled come from, for the generated headers. */
|
|
78
70
|
componentsDir: "",
|
|
79
|
-
/** The configured locales and their messages, when there are any. */
|
|
80
|
-
i18n: null,
|
|
81
|
-
/** How scope ids are computed, and the directory a component's path is hashed from. */
|
|
82
|
-
scopeId: "filepath-source",
|
|
83
|
-
viteRoot: "",
|
|
84
71
|
};
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function unifiedDiff(path: string, before: string, after: string): string;
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function unifiedDiff(path, before, after) {
|
|
2
|
+
const lines = (text) => (text === "" ? [] : text.replace(/\n$/, "").split("\n"));
|
|
3
|
+
const a = lines(before);
|
|
4
|
+
const b = lines(after);
|
|
5
|
+
let head = 0;
|
|
6
|
+
while (head < a.length && head < b.length && a[head] === b[head])
|
|
7
|
+
head++;
|
|
8
|
+
let tail = 0;
|
|
9
|
+
while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail])
|
|
10
|
+
tail++;
|
|
11
|
+
const midA = a.slice(head, a.length - tail);
|
|
12
|
+
const midB = b.slice(head, b.length - tail);
|
|
13
|
+
if (!midA.length && !midB.length)
|
|
14
|
+
return "";
|
|
15
|
+
const lcs = Array.from({ length: midA.length + 1 }, () => new Uint32Array(midB.length + 1));
|
|
16
|
+
for (let i = midA.length - 1; i >= 0; i--) {
|
|
17
|
+
for (let j = midB.length - 1; j >= 0; j--) {
|
|
18
|
+
lcs[i][j] = midA[i] === midB[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const edits = a.slice(0, head).map((text) => ({ op: " ", text }));
|
|
22
|
+
let i = 0;
|
|
23
|
+
let j = 0;
|
|
24
|
+
while (i < midA.length || j < midB.length) {
|
|
25
|
+
if (i < midA.length && j < midB.length && midA[i] === midB[j]) {
|
|
26
|
+
edits.push({ op: " ", text: midA[i++] });
|
|
27
|
+
j++;
|
|
28
|
+
}
|
|
29
|
+
else if (j < midB.length && (i === midA.length || lcs[i][j + 1] > lcs[i + 1][j]))
|
|
30
|
+
edits.push({ op: "+", text: midB[j++] });
|
|
31
|
+
else
|
|
32
|
+
edits.push({ op: "-", text: midA[i++] });
|
|
33
|
+
}
|
|
34
|
+
edits.push(...a.slice(a.length - tail).map((text) => ({ op: " ", text })));
|
|
35
|
+
const CONTEXT = 3;
|
|
36
|
+
const hunks = [];
|
|
37
|
+
edits.forEach((e, k) => {
|
|
38
|
+
if (e.op === " ")
|
|
39
|
+
return;
|
|
40
|
+
const from = Math.max(0, k - CONTEXT);
|
|
41
|
+
const to = Math.min(edits.length - 1, k + CONTEXT);
|
|
42
|
+
const last = hunks.at(-1);
|
|
43
|
+
if (last && from <= last[1] + 1)
|
|
44
|
+
last[1] = to;
|
|
45
|
+
else
|
|
46
|
+
hunks.push([from, to]);
|
|
47
|
+
});
|
|
48
|
+
const out = [`--- ${a.length ? `a/${path}` : "/dev/null"}`, `+++ ${b.length ? `b/${path}` : "/dev/null"}`];
|
|
49
|
+
let lineA = 1;
|
|
50
|
+
let lineB = 1;
|
|
51
|
+
const at = edits.map((e) => {
|
|
52
|
+
const here = [lineA, lineB];
|
|
53
|
+
if (e.op !== "+")
|
|
54
|
+
lineA++;
|
|
55
|
+
if (e.op !== "-")
|
|
56
|
+
lineB++;
|
|
57
|
+
return here;
|
|
58
|
+
});
|
|
59
|
+
for (const [from, to] of hunks) {
|
|
60
|
+
const span = edits.slice(from, to + 1);
|
|
61
|
+
const countA = span.filter((e) => e.op !== "+").length;
|
|
62
|
+
const countB = span.filter((e) => e.op !== "-").length;
|
|
63
|
+
const startA = countA ? at[from][0] : at[from][0] - 1;
|
|
64
|
+
const startB = countB ? at[from][1] : at[from][1] - 1;
|
|
65
|
+
out.push(`@@ -${startA},${countA} +${startB},${countB} @@`, ...span.map((e) => `${e.op}${e.text}`));
|
|
66
|
+
}
|
|
67
|
+
return out.join("\n");
|
|
68
|
+
}
|