ferrovue 0.5.0 → 0.7.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 +306 -62
- package/dist/args.d.ts +28 -0
- package/dist/args.js +69 -0
- package/dist/attrs.d.ts +2 -1
- package/dist/attrs.js +10 -4
- package/dist/calls.js +5 -2
- package/dist/children.d.ts +2 -2
- package/dist/children.js +84 -16
- package/dist/classes.d.ts +4 -0
- package/dist/classes.js +23 -1
- package/dist/cli.js +80 -81
- package/dist/client-render.d.ts +23 -0
- package/dist/client-render.js +143 -0
- package/dist/client.d.ts +74 -10
- package/dist/client.js +373 -42
- package/dist/compiler.d.ts +9 -5
- package/dist/compiler.js +80 -18
- package/dist/component.d.ts +7 -3
- package/dist/component.js +38 -27
- package/dist/conformance.d.ts +85 -0
- package/dist/conformance.js +327 -0
- package/dist/constants.js +4 -2
- package/dist/context.d.ts +66 -10
- package/dist/context.js +176 -5
- package/dist/diagnostics.d.ts +6 -4
- package/dist/diagnostics.js +7 -3
- package/dist/dynamic.d.ts +18 -0
- package/dist/dynamic.js +576 -0
- package/dist/editor.d.ts +18 -0
- package/dist/editor.js +59 -0
- package/dist/errors.d.ts +213 -4
- package/dist/errors.js +129 -4
- package/dist/expr.d.ts +2 -0
- package/dist/expr.js +91 -12
- package/dist/fallthrough.d.ts +1 -0
- package/dist/fallthrough.js +10 -2
- package/dist/file-routes.d.ts +33 -0
- package/dist/file-routes.js +224 -0
- package/dist/files.d.ts +13 -0
- package/dist/files.js +49 -0
- package/dist/fixture.d.ts +16 -0
- package/dist/fixture.js +22 -7
- package/dist/hydration.d.ts +12 -1
- package/dist/hydration.js +47 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/inputs.d.ts +24 -0
- package/dist/inputs.js +40 -0
- package/dist/link-router.d.ts +7 -3
- package/dist/link-router.js +8 -4
- package/dist/lists.d.ts +16 -1
- package/dist/lists.js +78 -4
- package/dist/loops.js +4 -4
- package/dist/model.d.ts +20 -1
- package/dist/model.js +46 -1
- package/dist/numbers.js +3 -2
- package/dist/page-routes.d.ts +10 -0
- package/dist/page-routes.js +10 -0
- package/dist/paths.d.ts +5 -0
- package/dist/paths.js +7 -0
- package/dist/plugin.d.ts +15 -1
- package/dist/plugin.js +3 -0
- package/dist/plugins/client-only.js +3 -0
- package/dist/plugins/css-modules.d.ts +39 -0
- package/dist/plugins/css-modules.js +135 -0
- package/dist/plugins/head.d.ts +15 -0
- package/dist/plugins/head.js +385 -0
- package/dist/plugins/i18n.js +5 -12
- package/dist/plugins/index.js +4 -1
- package/dist/plugins/provide.d.ts +44 -0
- package/dist/plugins/provide.js +530 -0
- package/dist/plugins/router-link.js +7 -3
- package/dist/plugins/router.d.ts +4 -1
- package/dist/plugins/router.js +58 -11
- package/dist/plugins/scoped.d.ts +10 -0
- package/dist/plugins/scoped.js +17 -4
- package/dist/plugins/shared-types.js +2 -1
- package/dist/plugins/stores.js +26 -15
- package/dist/routes.d.ts +5 -2
- package/dist/routes.js +8 -2
- package/dist/rust.d.ts +9 -1
- package/dist/rust.js +83 -32
- package/dist/schema.d.ts +25 -0
- package/dist/schema.js +73 -0
- package/dist/script.js +31 -8
- package/dist/settle.d.ts +2 -0
- package/dist/settle.js +5 -0
- package/dist/slots.js +20 -4
- package/dist/ssr.d.ts +3 -0
- package/dist/ssr.js +67 -0
- package/dist/strings.d.ts +3 -0
- package/dist/strings.js +16 -2
- package/dist/styles.d.ts +5 -1
- package/dist/styles.js +131 -4
- package/dist/template.js +92 -11
- package/dist/testing.d.ts +4 -4
- package/dist/testing.js +3 -30
- package/dist/types.d.ts +5 -0
- package/dist/typescript.d.ts +11 -1
- package/dist/typescript.js +131 -36
- package/dist/vhtml.d.ts +4 -0
- package/dist/vhtml.js +63 -0
- package/dist/vite.d.ts +25 -11
- package/dist/vite.js +166 -51
- package/dist/volar.d.ts +15 -0
- package/dist/volar.js +87 -0
- package/package.json +41 -7
- package/schema.json +333 -0
package/dist/compiler.js
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { basename, join } from "node:path";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { relativePath } from "./paths.js";
|
|
3
4
|
import { ctx, loadConfig, tyOfName } from "./context.js";
|
|
4
|
-
import {
|
|
5
|
+
import { failIn, snake } from "./model.js";
|
|
6
|
+
import { readComponent, refuseOptionsApi } from "./component.js";
|
|
7
|
+
import { listDir } from "./files.js";
|
|
5
8
|
import { scopeFor } from "./script.js";
|
|
6
9
|
import { attrsFlow } from "./fallthrough.js";
|
|
7
|
-
import {
|
|
10
|
+
import { prepareDynamic } from "./dynamic.js";
|
|
11
|
+
import { componentSource, GENERATED, isIsland, modSource } from "./rust.js";
|
|
12
|
+
import { requireWhole } from "./typescript.js";
|
|
8
13
|
import { renderParams } from "./plugin.js";
|
|
9
14
|
import { PLUGINS } from "./plugins/index.js";
|
|
10
|
-
/** Every generated file, keyed by its name in the output directory.
|
|
11
|
-
|
|
15
|
+
/** Every generated file, keyed by its name in the output directory. `sources` holds the text of
|
|
16
|
+
* components by path, read in place of their files, as an editor's unsaved buffers. */
|
|
17
|
+
export function generate(root, config = loadConfig(root), sources = new Map()) {
|
|
18
|
+
ctx.sources = new Map([...sources].map(([file, text]) => [resolve(root, file), text]));
|
|
12
19
|
ctx.componentsDir = config.components.replace(/\/?$/, "/");
|
|
13
20
|
ctx.helperModule = config.helpers?.module ?? null;
|
|
14
21
|
ctx.trustedHtml = config.trustedHtml ?? null;
|
|
15
22
|
ctx.builders = config.builders ?? true;
|
|
16
23
|
ctx.clientDirectives = new Set(config.clientDirectives ?? []);
|
|
17
24
|
ctx.rootDir = root;
|
|
25
|
+
ctx.vnodeTag = null;
|
|
18
26
|
ctx.typeStructs = new Map();
|
|
19
27
|
ctx.typeAliases = new Map();
|
|
20
28
|
ctx.typeFiles = new Map();
|
|
29
|
+
ctx.typeDecls = new Map();
|
|
30
|
+
ctx.typeReached = new Set();
|
|
21
31
|
ctx.typeRead = new Set();
|
|
22
32
|
ctx.constDecls = new Map();
|
|
23
33
|
ctx.typeConsts = new Map();
|
|
@@ -32,16 +42,41 @@ export function generate(root, config = loadConfig(root)) {
|
|
|
32
42
|
{ rust: h.rust, params: h.params.map(tyOfName), ret: tyOfName(h.returns), maxLen: h.maxLen ?? 0 },
|
|
33
43
|
]));
|
|
34
44
|
const dir = join(root, config.components);
|
|
35
|
-
const files =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
const files = [
|
|
46
|
+
...listDir(root, config.components, "components")
|
|
47
|
+
.filter((f) => f.endsWith(".vue"))
|
|
48
|
+
.toSorted()
|
|
49
|
+
.map((f) => ({ file: join(dir, f), name: basename(f, ".vue") })),
|
|
50
|
+
...PLUGINS.flatMap((p) => p.components?.() ?? []).map((c) => ({ file: join(root, c.file), name: c.name })),
|
|
51
|
+
];
|
|
52
|
+
const modulesOf = new Map();
|
|
53
|
+
for (const f of files) {
|
|
54
|
+
const rel = relativePath(root, f.file);
|
|
55
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(f.name)) {
|
|
56
|
+
failIn(rel, "FV0007", `the component is called \`${f.name}\` after its file, which is not a Rust name: name the file in PascalCase, letters and digits (\`UserCard.vue\`)`);
|
|
57
|
+
}
|
|
58
|
+
const file = moduleFile(snake(f.name));
|
|
59
|
+
if (file === "mod.rs")
|
|
60
|
+
failIn(rel, "FV0007", `the component's module would be \`mod.rs\`, which holds the generated modules: rename the file`);
|
|
61
|
+
const taken = modulesOf.get(file);
|
|
62
|
+
if (taken)
|
|
63
|
+
failIn(rel, "FV0007", `the component's module would be \`${file}\`, as ${taken}'s is: rename one of them`);
|
|
64
|
+
modulesOf.set(file, rel);
|
|
65
|
+
}
|
|
66
|
+
const read = files.map((f) => readComponent(f.file, root, f.name));
|
|
67
|
+
const children = new Set(read.flatMap((r) => [...r.comp.imports, ...r.comp.childProps.values()].filter((c) => c !== r.comp.name)));
|
|
68
|
+
for (const r of read)
|
|
69
|
+
if (r.optionsApi)
|
|
70
|
+
refuseOptionsApi(r.comp, r.optionsApi, children.has(r.comp.name));
|
|
71
|
+
const byName = new Map(read.map((r) => [r.comp.name, r]));
|
|
41
72
|
const components = new Map(read.map((r) => [r.comp.name, r.comp]));
|
|
42
73
|
ctx.components = components;
|
|
74
|
+
for (const c of components.values())
|
|
75
|
+
for (const f of c.props.fields)
|
|
76
|
+
requireWhole(c, f.ty);
|
|
43
77
|
const scopes = read.map((r) => scopeFor(r.comp, r.ast, components).scope);
|
|
44
|
-
const
|
|
78
|
+
const analysed = prepareDynamic(read.map((r, i) => ({ ...r, scope: scopes[i] })));
|
|
79
|
+
const all = read.map((r, i) => ({ comp: r.comp, ssr: analysed[i], children: scopes[i].children, attrsBindings: scopes[i].attrsBindings }));
|
|
45
80
|
for (const p of PLUGINS)
|
|
46
81
|
p.analyse?.(all);
|
|
47
82
|
attrsFlow(all);
|
|
@@ -66,23 +101,49 @@ export function generate(root, config = loadConfig(root)) {
|
|
|
66
101
|
return;
|
|
67
102
|
placed.add(r.comp.name);
|
|
68
103
|
for (const i of r.comp.imports) {
|
|
69
|
-
const dep =
|
|
104
|
+
const dep = byName.get(i);
|
|
70
105
|
if (dep)
|
|
71
106
|
place(dep);
|
|
72
107
|
}
|
|
73
108
|
ordered.push(r);
|
|
74
109
|
};
|
|
75
110
|
read.forEach(place);
|
|
76
|
-
|
|
111
|
+
const built = ordered.map((r) => {
|
|
77
112
|
ctx.narrowCount = 0;
|
|
78
|
-
|
|
113
|
+
const file = moduleFile(r.comp.module);
|
|
114
|
+
out.set(file, "");
|
|
115
|
+
return { file, source: componentSource(r.comp, r.ast, r.ssr, components) };
|
|
116
|
+
});
|
|
117
|
+
const reached = () => ctx.typeReached.size + [...components.values()].reduce((n, c) => n + c.reached.size, 0);
|
|
118
|
+
let modules = [];
|
|
119
|
+
for (let before = -1; before !== reached();) {
|
|
120
|
+
before = reached();
|
|
121
|
+
for (const { file, source } of built)
|
|
122
|
+
out.set(file, source());
|
|
123
|
+
modules = PLUGINS.flatMap((p) => p.modules?.() ?? []);
|
|
79
124
|
}
|
|
80
|
-
const modules = PLUGINS.flatMap((p) => p.modules?.() ?? []);
|
|
81
125
|
out.set("mod.rs", modSource(read.map((r) => r.comp), modules.map(([file]) => file)));
|
|
82
|
-
for (const [file, text] of modules)
|
|
126
|
+
for (const [file, text] of modules) {
|
|
127
|
+
const taken = modulesOf.get(file);
|
|
128
|
+
if (taken)
|
|
129
|
+
failIn(taken, "FV0007", `the component's module would be \`${file}\`, which ferrovue writes for the project's ${file.replace(/\.rs$/, "").replaceAll("_", " ")}: rename the file`);
|
|
83
130
|
out.set(file, text);
|
|
131
|
+
}
|
|
84
132
|
return out;
|
|
85
133
|
}
|
|
134
|
+
/** Whether a file starts with the header ferrovue writes, so `write` removes only what an earlier
|
|
135
|
+
* run wrote and never a module of the app's own put in the output directory by mistake. */
|
|
136
|
+
export function isGenerated(file) {
|
|
137
|
+
try {
|
|
138
|
+
return readFileSync(file, "utf8").startsWith(GENERATED);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function moduleFile(module) {
|
|
145
|
+
return `${module.replace(/^r#/, "")}.rs`;
|
|
146
|
+
}
|
|
86
147
|
/** Write what \`generate\` produces to the configured directory: only the files whose text changed,
|
|
87
148
|
* so a Rust build that watches them rebuilds no more than it must, and removing what no component
|
|
88
149
|
* produces any more. */
|
|
@@ -103,7 +164,7 @@ export function write(root, config = loadConfig(root)) {
|
|
|
103
164
|
changed.push(name);
|
|
104
165
|
}
|
|
105
166
|
}
|
|
106
|
-
const removed = readdirSync(target).filter((f) => f.endsWith(".rs") && !files.has(f));
|
|
167
|
+
const removed = readdirSync(target).filter((f) => f.endsWith(".rs") && !files.has(f) && isGenerated(join(target, f)));
|
|
107
168
|
for (const f of removed)
|
|
108
169
|
rmSync(join(target, f));
|
|
109
170
|
const islands = Object.fromEntries([...ctx.components.values()].filter(isIsland).map((c) => [c.name, c.file]));
|
|
@@ -111,6 +172,7 @@ export function write(root, config = loadConfig(root)) {
|
|
|
111
172
|
}
|
|
112
173
|
export { CONFIG_FILE, loadConfig, TYPES_MODULE } from "./context.js";
|
|
113
174
|
export { GenError } from "./model.js";
|
|
175
|
+
export { docsUrl, ERRORS } from "./errors.js";
|
|
114
176
|
export const VERSION = (() => {
|
|
115
177
|
try {
|
|
116
178
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
package/dist/component.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { type Component, type N } from "./model.ts";
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
export declare function
|
|
2
|
+
/** Refuse a component written with the Options API, which `readComponent` read no further: as a
|
|
3
|
+
* child when another component imports it, as an island otherwise. */
|
|
4
|
+
export declare function refuseOptionsApi(comp: Component, script: N, isChild: boolean): never;
|
|
5
|
+
/** A component, read and its template compiled. One written with the Options API is read no
|
|
6
|
+
* further than its `<script>`, `optionsApi`, for `refuseOptionsApi` once every component is read. */
|
|
7
|
+
export declare function readComponent(file: string, root: string, name?: string): {
|
|
5
8
|
comp: Component;
|
|
6
9
|
ast: N[];
|
|
7
10
|
ssr: string;
|
|
11
|
+
optionsApi?: N;
|
|
8
12
|
};
|
package/dist/component.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { parse as parseJs } from "@babel/parser";
|
|
2
2
|
import { compileScript, compileTemplate, parse as parseSfc } from "@vue/compiler-sfc";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
3
|
import { SourceMapConsumer } from "source-map-js";
|
|
5
|
-
import { basename
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
import { relativePath } from "./paths.js";
|
|
6
6
|
import { absence, blankComponent, fail, GenError, opt, snake, sourceAt, tagAst } from "./model.js";
|
|
7
|
-
import { ctx } from "./context.js";
|
|
8
|
-
import { typesImports, declareTypes, defaultValue, definePropsType, ONE_NOTHING, readTypeFile, resolveImport, runtimeDefaults, structOf, tyOfTs } from "./typescript.js";
|
|
7
|
+
import { ctx, readSource } from "./context.js";
|
|
8
|
+
import { typesImports, declareTypes, defaultValue, definePropsType, ONE_NOTHING, reachLocal, readTypeFile, resolveImport, runtimeDefaults, structOf, tyOfTs } from "./typescript.js";
|
|
9
9
|
import { claim } from "./plugin.js";
|
|
10
10
|
import { asyncChildren } from "./script.js";
|
|
11
11
|
function vueErrorNode(err, within) {
|
|
@@ -59,9 +59,17 @@ function sourceDefault(ast, key) {
|
|
|
59
59
|
function blockAt(source, block) {
|
|
60
60
|
return sourceAt(source, source.lastIndexOf(`<${block.type}`, block.loc.start.offset));
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
function cssVarsAt(source, descriptor) {
|
|
63
|
+
const at = new Map();
|
|
64
|
+
for (const st of descriptor.styles) {
|
|
65
|
+
for (const m of st.content.matchAll(/v-bind\s*\(\s*(?:'([^']*)'|"([^"]*)"|([^)'"]*))/g)) {
|
|
66
|
+
const raw = m[1] ?? m[2] ?? m[3].trim();
|
|
67
|
+
if (!at.has(raw))
|
|
68
|
+
at.set(raw, sourceAt(source, st.loc.start.offset + m.index));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const block = descriptor.styles.find((st) => /\bv-bind\s*\(/.test(st.content));
|
|
72
|
+
return descriptor.cssVars.map((v) => at.get(v) ?? (block ? blockAt(source, block) : sourceAt(source, 0)));
|
|
65
73
|
}
|
|
66
74
|
function generics(comp, source, block) {
|
|
67
75
|
const text = block.attrs.generic;
|
|
@@ -114,28 +122,27 @@ function scriptOf(comp, descriptor) {
|
|
|
114
122
|
throw new GenError("FV0001", e.message, { file: comp.file, ...where }, e.message.split("\n")[0]);
|
|
115
123
|
}
|
|
116
124
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
/** Refuse a component written with the Options API, which `readComponent` read no further: as a
|
|
126
|
+
* child when another component imports it, as an island otherwise. */
|
|
127
|
+
export function refuseOptionsApi(comp, script, isChild) {
|
|
128
|
+
const needs = isChild ? "a child component must have" : "an island needs";
|
|
129
|
+
return fail(comp, "FV0003", `${needs} \`<script setup lang="ts">\`, or no script at all: a \`<script>\` without \`setup\` (the Options API, \`defineComponent\`) is not translated`, script);
|
|
130
|
+
}
|
|
131
|
+
/** A component, read and its template compiled. One written with the Options API is read no
|
|
132
|
+
* further than its `<script>`, `optionsApi`, for `refuseOptionsApi` once every component is read. */
|
|
133
|
+
export function readComponent(file, root, name = basename(file, ".vue")) {
|
|
134
|
+
const source = readSource(file);
|
|
120
135
|
const { descriptor, errors } = parseSfc(source, { filename: file });
|
|
121
|
-
const rel =
|
|
136
|
+
const rel = relativePath(root, file);
|
|
122
137
|
const comp = blankComponent(name, snake(name), rel);
|
|
123
138
|
comp.source = source;
|
|
124
139
|
if (errors.length)
|
|
125
|
-
fail(comp, "FV0002", String(errors[0]), vueErrorNode(errors[0]));
|
|
126
|
-
if (descriptor.script && !descriptor.scriptSetup)
|
|
127
|
-
|
|
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));
|
|
129
|
-
}
|
|
140
|
+
fail(comp, "FV0002", String(errors[0]).replaceAll(file, rel), vueErrorNode(errors[0]) ?? sourceAt(source, 0));
|
|
141
|
+
if (descriptor.script && !descriptor.scriptSetup)
|
|
142
|
+
return { comp, ast: [], ssr: "", optionsApi: blockAt(source, descriptor.script) };
|
|
130
143
|
if (!descriptor.template)
|
|
131
144
|
fail(comp, "FV0004", "a component needs a `<template>`: a render function is not translated", sourceAt(source, 0));
|
|
132
|
-
|
|
133
|
-
if (st.module)
|
|
134
|
-
fail(comp, "FV1005", "`<style module>` renames classes in the bundler; use a global or scoped `<style>`, or a stylesheet", blockAt(source, st));
|
|
135
|
-
}
|
|
136
|
-
const cssVar = descriptor.styles.find((st) => /\bv-bind\s*\(/.test(st.content));
|
|
137
|
-
if (descriptor.cssVars.length)
|
|
138
|
-
fail(comp, "FV1006", "`v-bind()` in `<style>` sets variables the server does not render; bind `:style` instead", cssVar ? blockAt(source, cssVar) : undefined);
|
|
145
|
+
comp.cssVarsAt = cssVarsAt(source, descriptor);
|
|
139
146
|
for (const p of ctx.plugins)
|
|
140
147
|
p.sfc?.(comp, descriptor, file, source);
|
|
141
148
|
const script = descriptor.scriptSetup ? scriptOf(comp, descriptor) : null;
|
|
@@ -191,7 +198,7 @@ export function readComponent(file, root, isChild) {
|
|
|
191
198
|
generics(comp, source, descriptor.scriptSetup);
|
|
192
199
|
const decls = declareTypes(comp, [...plainAst, ...ast], comp.structs, comp.aliases);
|
|
193
200
|
for (const d of decls)
|
|
194
|
-
comp.
|
|
201
|
+
comp.typeDecls.set(d.name, { members: d.members, node: d.node });
|
|
195
202
|
let propsTy = null;
|
|
196
203
|
for (const s of ast) {
|
|
197
204
|
const call = s.type === "ExpressionStatement" ? s.expression : s.type === "VariableDeclaration" ? s.declarations[0]?.init : null;
|
|
@@ -209,7 +216,12 @@ export function readComponent(file, root, isChild) {
|
|
|
209
216
|
comp.props = structOf(comp, "Props", propsTy.members, comp.structs);
|
|
210
217
|
}
|
|
211
218
|
else if (propsTy.type === "TSTypeReference" && comp.structs.has(propsTy.typeName.name)) {
|
|
212
|
-
comp
|
|
219
|
+
reachLocal(comp, propsTy.typeName.name);
|
|
220
|
+
const st = comp.structs.get(propsTy.typeName.name);
|
|
221
|
+
const refused = st.refused?.values().next().value;
|
|
222
|
+
if (refused !== undefined)
|
|
223
|
+
throw refused;
|
|
224
|
+
comp.props = { name: "Props", fields: st.fields };
|
|
213
225
|
}
|
|
214
226
|
else {
|
|
215
227
|
fail(comp, "FV0304", "`defineProps` takes a type literal or an interface declared in the same block", propsTy);
|
|
@@ -252,9 +264,8 @@ export function readComponent(file, root, isChild) {
|
|
|
252
264
|
const compiled = compileTemplate({
|
|
253
265
|
source: descriptor.template.content,
|
|
254
266
|
filename: file,
|
|
255
|
-
...(claim((p) => p.templateOptions?.(comp)) ?? { id: name, scoped: false, slotted: false }),
|
|
267
|
+
...(claim((p) => p.templateOptions?.(comp)) ?? { id: name, scoped: false, slotted: false, ssrCssVars: [], isProd: true }),
|
|
256
268
|
ssr: true,
|
|
257
|
-
ssrCssVars: [],
|
|
258
269
|
compilerOptions: { ...(script ? { bindingMetadata: script.bindings } : {}), sourceMap: true },
|
|
259
270
|
});
|
|
260
271
|
const kids = (n) => n.children.filter((c) => c.type !== 3 && !(c.type === 2 && !c.content.trim()));
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { App, Component } from "vue";
|
|
2
|
+
import { type Config } from "./compiler.ts";
|
|
3
|
+
import { type RouteEntry, type RouterOptions } from "./fixture.ts";
|
|
4
|
+
/** What `conformanceSuite` checks: the project, its components and its fixtures. `pinia`,
|
|
5
|
+
* `vueRouter` and `vueI18n` are the application's own modules (`pinia: await import("pinia")`),
|
|
6
|
+
* which the fixtures install so that its stores and composables find them. */
|
|
7
|
+
export interface ConformanceOptions extends Pick<RouterOptions, "pinia" | "vueRouter" | "vueI18n"> {
|
|
8
|
+
/** The project's `ferrovue.config.json`. */
|
|
9
|
+
config: string;
|
|
10
|
+
/** Every component in the configured `components` directory, by file name or by the path
|
|
11
|
+
* `import.meta.glob` gives it: `import.meta.glob("../components/*.vue", { eager: true })`; and
|
|
12
|
+
* with a folder of pages, every page, by the path `import.meta.glob("../pages/**\/*.vue")` gives
|
|
13
|
+
* it or by its component name (`BooksId`). */
|
|
14
|
+
components: Record<string, Component | {
|
|
15
|
+
default: Component;
|
|
16
|
+
}>;
|
|
17
|
+
/** The fixtures, one directory per component holding `<case>.json` and `<case>.html`:
|
|
18
|
+
* `fixtures` beside the configuration when not given. */
|
|
19
|
+
fixtures?: string;
|
|
20
|
+
/** Write each fixture's `.html` from Vue's render instead of comparing it: when
|
|
21
|
+
* `FERROVUE_FIXTURES_WRITE=1` is set, if not given. */
|
|
22
|
+
record?: boolean | undefined;
|
|
23
|
+
/** Check that a fresh client render of each fixture shows its recorded HTML, which an
|
|
24
|
+
* application that navigates between pages relies on: on when `clientDiffers` is given. */
|
|
25
|
+
clientRender?: boolean;
|
|
26
|
+
/** The fixtures (`Component/case.json`) whose fresh client render Vue itself makes differ from
|
|
27
|
+
* its server render, which the check of client renders then expects to differ: a `v-model`
|
|
28
|
+
* `<select>` whose value matches no option, which the client leaves with none selected; an
|
|
29
|
+
* attribute such as `disabled` bound on an element with no such property, which the client
|
|
30
|
+
* writes as `"true"`; a `<TransitionGroup>`'s `name`, which only the server writes; a slot's scope
|
|
31
|
+
* ids; and an async component beside a `<ClientOnly>`, whose content shows by the time the
|
|
32
|
+
* component has loaded. */
|
|
33
|
+
clientDiffers?: string[];
|
|
34
|
+
}
|
|
35
|
+
interface TestApi {
|
|
36
|
+
describe(name: string, body: () => void): void;
|
|
37
|
+
it(name: string, body: () => Promise<void> | void): void;
|
|
38
|
+
}
|
|
39
|
+
export declare const TELEPORTS = "<!--fv-teleports-->";
|
|
40
|
+
export declare const HEAD = "<!--fv-head-->";
|
|
41
|
+
/** What unhead's server renderer wrote for a recorded render's head, as `renderSSRHead` gives it. */
|
|
42
|
+
export interface RecordedHead {
|
|
43
|
+
headTags: string;
|
|
44
|
+
bodyTags: string;
|
|
45
|
+
bodyTagsOpen: string;
|
|
46
|
+
htmlAttrs: string;
|
|
47
|
+
bodyAttrs: string;
|
|
48
|
+
}
|
|
49
|
+
/** The head a recorded render wrote after its body, if it wrote one. */
|
|
50
|
+
export declare function recordedHead(html: string): RecordedHead | null;
|
|
51
|
+
/** Render a fixture's app as the conformance suite records it: Vue's HTML, then what was teleported
|
|
52
|
+
* and what `useHead` asked for, each after its marker. */
|
|
53
|
+
export declare function renderFixture(app: App): Promise<string>;
|
|
54
|
+
/** Put a recorded render's head into the document, as a page would write it: its tags in `<head>`,
|
|
55
|
+
* and its attributes on `<html>` and `<body>`. */
|
|
56
|
+
export declare function placeHead(head: RecordedHead | null, doc?: Document): void;
|
|
57
|
+
/** A recorded render as a page body to hydrate: the main HTML in `<div id="root">`, what was
|
|
58
|
+
* teleported in each target, and the tags the head puts at the start and end of `<body>`. */
|
|
59
|
+
export declare function hydrationBody(html: string): string;
|
|
60
|
+
/** Where two renders first differ, with the text around it in each. */
|
|
61
|
+
export declare function firstDifference(recorded: string, rendered: string): string;
|
|
62
|
+
/** The routes and the router and vue-i18n options a project's fixtures render with, from its configuration. */
|
|
63
|
+
export declare function fixtureOptions(root: string, config: Config): {
|
|
64
|
+
routes: RouteEntry[] | null;
|
|
65
|
+
options: RouterOptions;
|
|
66
|
+
};
|
|
67
|
+
/** Render a fixture's app on the client from scratch, with `createApp` (`fixtureApp`'s `client`
|
|
68
|
+
* option), into a document laid out as `hydrationBody` lays out its recorded render, and say where
|
|
69
|
+
* the client's DOM differs from the main HTML the browser parsed, as `renderedShape` compares
|
|
70
|
+
* them: as mounting leaves it, where `<ClientOnly>` still shows its fallback, and, where that
|
|
71
|
+
* differs while async components are loading, once they have loaded. What is teleported and the
|
|
72
|
+
* head are not compared. `null` where they agree. */
|
|
73
|
+
export declare function clientRenderDifference(app: App, html: string): Promise<string | null>;
|
|
74
|
+
/** Register the conformance suite with the given `describe` and `it`. */
|
|
75
|
+
export declare function registerConformance(api: TestApi, options: ConformanceOptions): void;
|
|
76
|
+
/** Register vitest tests holding a project's components to Vue, as ferrovue's own suite does:
|
|
77
|
+
* every component has fixtures and every fixture a component, the generated Rust is what the
|
|
78
|
+
* generator writes now, Vue renders each fixture to its recorded `.html` (or records it, with
|
|
79
|
+
* `FERROVUE_FIXTURES_WRITE=1`), each recorded `.html` hydrates with no warning and the server's
|
|
80
|
+
* nodes kept. With `clientRender` or `clientDiffers`, a fresh client render of each fixture shows
|
|
81
|
+
* its recorded `.html`, as `clientRenderDifference` compares them, except the fixtures listed in
|
|
82
|
+
* `clientDiffers`: what an application that navigates between pages relies on when it renders the
|
|
83
|
+
* next page from its record. Await it at the top of a test file run in a DOM environment such as happy-dom. */
|
|
84
|
+
export declare function conformanceSuite(options: ConformanceOptions): Promise<void>;
|
|
85
|
+
export {};
|