ferrovue 0.4.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 +198 -34
- package/dist/attrs.d.ts +1 -1
- package/dist/attrs.js +24 -22
- package/dist/calls.js +17 -15
- package/dist/children.d.ts +9 -2
- package/dist/children.js +48 -35
- package/dist/classes.js +11 -11
- package/dist/cli.js +31 -5
- package/dist/client.d.ts +35 -2
- package/dist/client.js +77 -2
- package/dist/compiler.d.ts +1 -1
- package/dist/compiler.js +7 -3
- package/dist/component.d.ts +3 -1
- package/dist/component.js +114 -19
- package/dist/constants.d.ts +42 -0
- package/dist/constants.js +292 -0
- package/dist/context.d.ts +25 -1
- package/dist/context.js +11 -5
- package/dist/diagnostics.d.ts +23 -0
- package/dist/diagnostics.js +30 -0
- package/dist/errors.d.ts +867 -0
- package/dist/errors.js +330 -0
- package/dist/expr.d.ts +1 -0
- package/dist/expr.js +120 -49
- package/dist/fixture.d.ts +3 -10
- package/dist/fixture.js +2 -6
- package/dist/hydration.d.ts +15 -0
- package/dist/hydration.js +80 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/link-router.d.ts +24 -0
- package/dist/link-router.js +36 -0
- package/dist/lists.js +30 -21
- package/dist/loops.js +14 -10
- package/dist/model.d.ts +32 -1
- package/dist/model.js +123 -6
- package/dist/narrowing.d.ts +8 -0
- package/dist/narrowing.js +40 -9
- package/dist/plugins/client-only.d.ts +8 -0
- package/dist/plugins/client-only.js +107 -0
- package/dist/plugins/i18n.js +14 -14
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/router-link.js +33 -30
- package/dist/plugins/router.js +9 -9
- package/dist/plugins/scoped.js +1 -1
- package/dist/plugins/shared-types.js +1 -1
- package/dist/plugins/stores.js +26 -24
- package/dist/plugins/teleport.js +3 -3
- 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.js +18 -14
- package/dist/script.d.ts +3 -0
- package/dist/script.js +119 -17
- package/dist/settle.d.ts +6 -0
- package/dist/settle.js +34 -0
- package/dist/slots.js +18 -18
- package/dist/strings.js +11 -11
- package/dist/styles.js +13 -13
- package/dist/template.js +11 -11
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +3 -2
- package/dist/typescript.d.ts +2 -1
- package/dist/typescript.js +46 -19
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +10 -9
- package/package.json +9 -5
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSSRApp } from "vue";
|
|
1
|
+
import { createSSRApp, createTextVNode, defineComponent, h, onMounted, ref } from "vue";
|
|
2
2
|
const BARE = /"(?:[^"\\]|\\.)*"|-?Infinity|NaN/g;
|
|
3
3
|
const NON_FINITE = { NaN: Number.NaN, Infinity: Number.POSITIVE_INFINITY, "-Infinity": Number.NEGATIVE_INFINITY };
|
|
4
4
|
function parseJson(text) {
|
|
@@ -11,7 +11,7 @@ function parseJson(text) {
|
|
|
11
11
|
const quoted = text.replace(BARE, (match) => (match.startsWith('"') ? match : `"${escaped}${match}"`));
|
|
12
12
|
return JSON.parse(quoted, (_key, value) => (typeof value === "string" && value.startsWith(tag) ? NON_FINITE[value.slice(tag.length)] : value));
|
|
13
13
|
}
|
|
14
|
-
/** Give Pinia the state the server rendered with
|
|
14
|
+
/** Give Pinia the state the server rendered with (what `ferrovue::state_script_into` wrote) before
|
|
15
15
|
* the app mounts, so every store starts from it and the hydrated markup agrees with the server's. */
|
|
16
16
|
export function hydrateState(pinia, id = "__pinia", doc = document) {
|
|
17
17
|
const text = doc.getElementById(id)?.textContent;
|
|
@@ -74,6 +74,32 @@ export async function mountIslands(components, options = {}) {
|
|
|
74
74
|
},
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
/** Content only the browser renders. The server writes the `#fallback` slot, or nothing; the
|
|
78
|
+
* browser hydrates that fallback, then swaps in the default slot once mounted. ferrovue does not
|
|
79
|
+
* compile the default slot, so anything may go there: a component library's components, code that
|
|
80
|
+
* reads `window`. The swap happens only where Vue runs on the page, as in an island. */
|
|
81
|
+
export const ClientOnly = Object.assign(defineComponent({
|
|
82
|
+
name: "ClientOnly",
|
|
83
|
+
inheritAttrs: false,
|
|
84
|
+
setup(_, { slots }) {
|
|
85
|
+
const mounted = ref(false);
|
|
86
|
+
onMounted(() => {
|
|
87
|
+
mounted.value = true;
|
|
88
|
+
});
|
|
89
|
+
return () => (mounted.value ? slots.default?.() : slots.fallback?.());
|
|
90
|
+
},
|
|
91
|
+
}), {
|
|
92
|
+
ssrRender(ctx, push, parent) {
|
|
93
|
+
const fallback = ctx.$slots.fallback;
|
|
94
|
+
if (!fallback) {
|
|
95
|
+
push("<!---->");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
push("<!--[-->");
|
|
99
|
+
fallback({}, push, parent, "");
|
|
100
|
+
push("<!--]-->");
|
|
101
|
+
},
|
|
102
|
+
});
|
|
77
103
|
async function load(component) {
|
|
78
104
|
if (typeof component !== "function" || "props" in component || "displayName" in component || "__vccOpts" in component)
|
|
79
105
|
return component;
|
|
@@ -85,3 +111,52 @@ async function load(component) {
|
|
|
85
111
|
return e instanceof Error ? e : new Error(String(e));
|
|
86
112
|
}
|
|
87
113
|
}
|
|
114
|
+
/** The app of a page: its root renders `layout` with the record's props, and each slot as the
|
|
115
|
+
* components the record names, in order, as a plain array, which hydrates the markup the server
|
|
116
|
+
* wrote with no fragment of its own. `components` must hold every component the record names. */
|
|
117
|
+
export function createPageApp(layout, record, components, options = {}) {
|
|
118
|
+
const slots = Object.fromEntries(Object.entries(record.slots).map(([name, parts]) => [
|
|
119
|
+
name,
|
|
120
|
+
() => (parts.length ? parts.map((part) => h(components[part.c], part.p)) : [createTextVNode("")]),
|
|
121
|
+
]));
|
|
122
|
+
const app = createSSRApp({ name: "FerrovuePage", render: () => h(layout, record.props, slots) });
|
|
123
|
+
if (options.pinia)
|
|
124
|
+
app.use(options.pinia);
|
|
125
|
+
if (options.router)
|
|
126
|
+
app.use(options.router);
|
|
127
|
+
for (const plugin of options.plugins ?? [])
|
|
128
|
+
app.use(plugin);
|
|
129
|
+
return app;
|
|
130
|
+
}
|
|
131
|
+
/** Hydrate a page `ferrovue::Page` rendered: read its record, load the components it names (and
|
|
132
|
+
* no others), wait for the router, and mount the layout on the container. Rejects, leaving the page
|
|
133
|
+
* as the server rendered it, when the container or the record is missing or a component is not
|
|
134
|
+
* given or does not load. */
|
|
135
|
+
export async function mountPage(layout, components, options = {}) {
|
|
136
|
+
const doc = options.doc ?? document;
|
|
137
|
+
const selector = options.container ?? "#app";
|
|
138
|
+
const container = typeof selector === "string" ? doc.querySelector(selector) : selector;
|
|
139
|
+
if (!container)
|
|
140
|
+
throw new Error(`[ferrovue] the page has no ${selector} to mount on`);
|
|
141
|
+
const id = options.record ?? "__fv_page";
|
|
142
|
+
const text = doc.getElementById(id)?.textContent;
|
|
143
|
+
if (!text)
|
|
144
|
+
throw new Error(`[ferrovue] the page has no record: no <script id="${id}">`);
|
|
145
|
+
const record = parseJson(text);
|
|
146
|
+
const names = new Set(Object.values(record.slots).flatMap((parts) => parts.map((part) => part.c)));
|
|
147
|
+
const loading = [...names].map(async (name) => {
|
|
148
|
+
if (!Object.hasOwn(components, name))
|
|
149
|
+
throw new Error(`[ferrovue] no component called ${JSON.stringify(name)} was given to mountPage`);
|
|
150
|
+
return [name, await loadOrThrow(name, components[name])];
|
|
151
|
+
});
|
|
152
|
+
const [root, parts] = await Promise.all([loadOrThrow("the layout", layout), Promise.all(loading), options.router?.isReady()]);
|
|
153
|
+
const app = createPageApp(root, record, Object.fromEntries(parts), options);
|
|
154
|
+
app.mount(container);
|
|
155
|
+
return app;
|
|
156
|
+
}
|
|
157
|
+
async function loadOrThrow(name, component) {
|
|
158
|
+
const result = await load(component);
|
|
159
|
+
if (result instanceof Error)
|
|
160
|
+
throw new Error(`[ferrovue] ${name} did not load: ${result.message}`, { cause: result });
|
|
161
|
+
return result;
|
|
162
|
+
}
|
package/dist/compiler.d.ts
CHANGED
|
@@ -18,6 +18,6 @@ export interface Written {
|
|
|
18
18
|
* so a Rust build that watches them rebuilds no more than it must, and removing what no component
|
|
19
19
|
* produces any more. */
|
|
20
20
|
export declare function write(root: string, config?: Config): Written;
|
|
21
|
-
export { CONFIG_FILE, loadConfig, TYPES_MODULE, type Config, type HelperSpec, type TypeName } from "./context.ts";
|
|
21
|
+
export { CONFIG_FILE, loadConfig, TYPES_MODULE, type Config, type HelperSpec, type TwinSpec, type TypeName } from "./context.ts";
|
|
22
22
|
export { GenError } from "./model.ts";
|
|
23
23
|
export declare const VERSION: string;
|
package/dist/compiler.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
3
|
import { ctx, loadConfig, tyOfName } from "./context.js";
|
|
4
|
-
import { readComponent } from "./component.js";
|
|
4
|
+
import { importsOf, readComponent } from "./component.js";
|
|
5
5
|
import { scopeFor } from "./script.js";
|
|
6
6
|
import { attrsFlow } from "./fallthrough.js";
|
|
7
7
|
import { componentSource, isIsland, modSource } from "./rust.js";
|
|
@@ -12,12 +12,15 @@ export function generate(root, config = loadConfig(root)) {
|
|
|
12
12
|
ctx.componentsDir = config.components.replace(/\/?$/, "/");
|
|
13
13
|
ctx.helperModule = config.helpers?.module ?? null;
|
|
14
14
|
ctx.trustedHtml = config.trustedHtml ?? null;
|
|
15
|
+
ctx.builders = config.builders ?? true;
|
|
15
16
|
ctx.clientDirectives = new Set(config.clientDirectives ?? []);
|
|
16
17
|
ctx.rootDir = root;
|
|
17
18
|
ctx.typeStructs = new Map();
|
|
18
19
|
ctx.typeAliases = new Map();
|
|
19
20
|
ctx.typeFiles = new Map();
|
|
20
21
|
ctx.typeRead = new Set();
|
|
22
|
+
ctx.constDecls = new Map();
|
|
23
|
+
ctx.typeConsts = new Map();
|
|
21
24
|
ctx.plugins = PLUGINS;
|
|
22
25
|
ctx.runs = new Map();
|
|
23
26
|
for (const p of PLUGINS)
|
|
@@ -33,7 +36,8 @@ export function generate(root, config = loadConfig(root)) {
|
|
|
33
36
|
.filter((f) => f.endsWith(".vue"))
|
|
34
37
|
.toSorted()
|
|
35
38
|
.map((f) => join(dir, f));
|
|
36
|
-
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"))));
|
|
37
41
|
const components = new Map(read.map((r) => [r.comp.name, r.comp]));
|
|
38
42
|
ctx.components = components;
|
|
39
43
|
const scopes = read.map((r) => scopeFor(r.comp, r.ast, components).scope);
|
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,11 +1,13 @@
|
|
|
1
|
+
import { parse as parseJs } from "@babel/parser";
|
|
1
2
|
import { compileScript, compileTemplate, parse as parseSfc } from "@vue/compiler-sfc";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import { SourceMapConsumer } from "source-map-js";
|
|
4
5
|
import { basename, relative } from "node:path";
|
|
5
|
-
import { blankComponent, fail, opt, snake, tagAst } from "./model.js";
|
|
6
|
+
import { absence, blankComponent, fail, GenError, opt, snake, sourceAt, tagAst } from "./model.js";
|
|
6
7
|
import { ctx } from "./context.js";
|
|
7
|
-
import { typesImports, declareTypes, defaultValue, definePropsType, readTypeFile, resolveImport, runtimeDefaults, structOf, tyOfTs } from "./typescript.js";
|
|
8
|
+
import { typesImports, declareTypes, defaultValue, definePropsType, ONE_NOTHING, readTypeFile, resolveImport, runtimeDefaults, structOf, tyOfTs } from "./typescript.js";
|
|
8
9
|
import { claim } from "./plugin.js";
|
|
10
|
+
import { asyncChildren } from "./script.js";
|
|
9
11
|
function vueErrorNode(err, within) {
|
|
10
12
|
const loc = err.loc;
|
|
11
13
|
if (!loc)
|
|
@@ -29,13 +31,90 @@ function inheritAttrs(comp, statements) {
|
|
|
29
31
|
if (p.type !== "ObjectProperty" || p.computed || (p.key.name ?? p.key.value) !== "inheritAttrs")
|
|
30
32
|
continue;
|
|
31
33
|
if (p.value.type !== "BooleanLiteral")
|
|
32
|
-
fail(comp, "`inheritAttrs` is `true` or `false`", p.value);
|
|
34
|
+
fail(comp, "FV0101", "`inheritAttrs` is `true` or `false`", p.value);
|
|
33
35
|
found = p.value.value;
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
return found;
|
|
37
39
|
}
|
|
38
|
-
|
|
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) {
|
|
39
118
|
const name = basename(file, ".vue");
|
|
40
119
|
const source = readFileSync(file, "utf8");
|
|
41
120
|
const { descriptor, errors } = parseSfc(source, { filename: file });
|
|
@@ -43,22 +122,26 @@ export function readComponent(file, root) {
|
|
|
43
122
|
const comp = blankComponent(name, snake(name), rel);
|
|
44
123
|
comp.source = source;
|
|
45
124
|
if (errors.length)
|
|
46
|
-
fail(comp, String(errors[0]), vueErrorNode(errors[0]));
|
|
47
|
-
if (
|
|
48
|
-
|
|
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));
|
|
49
129
|
}
|
|
130
|
+
if (!descriptor.template)
|
|
131
|
+
fail(comp, "FV0004", "a component needs a `<template>`: a render function is not translated", sourceAt(source, 0));
|
|
50
132
|
for (const st of descriptor.styles) {
|
|
51
133
|
if (st.module)
|
|
52
|
-
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));
|
|
53
135
|
}
|
|
136
|
+
const cssVar = descriptor.styles.find((st) => /\bv-bind\s*\(/.test(st.content));
|
|
54
137
|
if (descriptor.cssVars.length)
|
|
55
|
-
fail(comp, "`v-bind()` in `<style>` sets variables the server does not render; bind `:style` instead");
|
|
138
|
+
fail(comp, "FV1006", "`v-bind()` in `<style>` sets variables the server does not render; bind `:style` instead", cssVar ? blockAt(source, cssVar) : undefined);
|
|
56
139
|
for (const p of ctx.plugins)
|
|
57
140
|
p.sfc?.(comp, descriptor, file, source);
|
|
58
|
-
const script =
|
|
59
|
-
const ast = script
|
|
141
|
+
const script = descriptor.scriptSetup ? scriptOf(comp, descriptor) : null;
|
|
142
|
+
const ast = script?.scriptSetupAst ?? [];
|
|
60
143
|
tagAst(ast, "source");
|
|
61
|
-
const plainAst = script
|
|
144
|
+
const plainAst = script?.scriptAst ?? [];
|
|
62
145
|
tagAst(plainAst, "source");
|
|
63
146
|
for (const s of [...plainAst, ...ast]) {
|
|
64
147
|
if (s.type !== "ImportDeclaration" || !s.source.value.endsWith(".vue"))
|
|
@@ -72,6 +155,8 @@ export function readComponent(file, root) {
|
|
|
72
155
|
}
|
|
73
156
|
}
|
|
74
157
|
}
|
|
158
|
+
for (const loaded of asyncChildren(comp, ast).values())
|
|
159
|
+
comp.imports.add(loaded);
|
|
75
160
|
typesImports(comp, [...plainAst, ...ast]);
|
|
76
161
|
comp.inheritAttrs = inheritAttrs(comp, [...plainAst, ...ast]);
|
|
77
162
|
for (const st of [...plainAst, ...ast]) {
|
|
@@ -102,6 +187,8 @@ export function readComponent(file, root) {
|
|
|
102
187
|
comp.aliases.set(sp.local.name, ctx.typeAliases.get(typeName));
|
|
103
188
|
}
|
|
104
189
|
}
|
|
190
|
+
if (descriptor.scriptSetup)
|
|
191
|
+
generics(comp, source, descriptor.scriptSetup);
|
|
105
192
|
const decls = declareTypes(comp, [...plainAst, ...ast], comp.structs, comp.aliases);
|
|
106
193
|
for (const d of decls)
|
|
107
194
|
comp.structs.set(d.name, structOf(comp, d.name, d.members, comp.structs));
|
|
@@ -110,7 +197,7 @@ export function readComponent(file, root) {
|
|
|
110
197
|
const call = s.type === "ExpressionStatement" ? s.expression : s.type === "VariableDeclaration" ? s.declarations[0]?.init : null;
|
|
111
198
|
const inner = call?.type === "CallExpression" && call.callee.type === "Identifier" && call.callee.name === "withDefaults" ? call.arguments[0] : call;
|
|
112
199
|
if (inner?.type === "CallExpression" && inner.callee.type === "Identifier" && inner.callee.name === "defineProps" && !inner.typeParameters) {
|
|
113
|
-
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);
|
|
114
201
|
}
|
|
115
202
|
const found = definePropsType(call);
|
|
116
203
|
if (found)
|
|
@@ -125,7 +212,7 @@ export function readComponent(file, root) {
|
|
|
125
212
|
comp.props = { name: "Props", fields: comp.structs.get(propsTy.typeName.name).fields };
|
|
126
213
|
}
|
|
127
214
|
else {
|
|
128
|
-
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);
|
|
129
216
|
}
|
|
130
217
|
for (const st of ast) {
|
|
131
218
|
if (st.type !== "VariableDeclaration")
|
|
@@ -136,22 +223,29 @@ export function readComponent(file, root) {
|
|
|
136
223
|
continue;
|
|
137
224
|
const t = call.typeParameters?.params?.[0];
|
|
138
225
|
if (!t)
|
|
139
|
-
fail(comp, "`defineModel` needs its type: `defineModel<string>()`", call);
|
|
226
|
+
fail(comp, "FV0305", "`defineModel` needs its type: `defineModel<string>()`", call);
|
|
140
227
|
const named = call.arguments[0]?.type === "StringLiteral" ? call.arguments[0].value : "modelValue";
|
|
141
228
|
const options = call.arguments.find((a) => a.type === "ObjectExpression");
|
|
142
229
|
const required = options?.properties.some((p) => p.type === "ObjectProperty" && (p.key.name ?? p.key.value) === "required" && p.value.type === "BooleanLiteral" && p.value.value);
|
|
143
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);
|
|
144
233
|
comp.props.fields.push({ js: named, rust: snake(named), ty: required ? base : opt(base) });
|
|
145
234
|
comp.models.set(d.id.name, named);
|
|
146
235
|
}
|
|
147
236
|
}
|
|
148
|
-
const defaults = runtimeDefaults(comp, script
|
|
237
|
+
const defaults = runtimeDefaults(comp, script?.content ?? "");
|
|
149
238
|
for (const f of comp.props.fields) {
|
|
150
239
|
if (f.ty.k !== "opt")
|
|
151
240
|
continue;
|
|
152
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
|
+
}
|
|
153
247
|
if (node)
|
|
154
|
-
f.dflt = defaultValue(comp, f, node);
|
|
248
|
+
f.dflt = defaultValue(comp, f, node, sourceDefault(ast, f.js) ?? undefined);
|
|
155
249
|
else if (f.ty.of.k === "bool")
|
|
156
250
|
f.dflt = "false";
|
|
157
251
|
}
|
|
@@ -161,7 +255,7 @@ export function readComponent(file, root) {
|
|
|
161
255
|
...(claim((p) => p.templateOptions?.(comp)) ?? { id: name, scoped: false, slotted: false }),
|
|
162
256
|
ssr: true,
|
|
163
257
|
ssrCssVars: [],
|
|
164
|
-
compilerOptions: { bindingMetadata: script.bindings, sourceMap: true },
|
|
258
|
+
compilerOptions: { ...(script ? { bindingMetadata: script.bindings } : {}), sourceMap: true },
|
|
165
259
|
});
|
|
166
260
|
const kids = (n) => n.children.filter((c) => c.type !== 3 && !(c.type === 2 && !c.content.trim()));
|
|
167
261
|
const roots = descriptor.template.ast ? kids(descriptor.template.ast) : [];
|
|
@@ -172,12 +266,13 @@ export function readComponent(file, root) {
|
|
|
172
266
|
if (!lone)
|
|
173
267
|
comp.attrsDropped = { type: "VueTemplate", loc: { start: { line: wrapper.loc.start.line, column: wrapper.loc.start.column - 1 } }, __fv: "source" };
|
|
174
268
|
}
|
|
269
|
+
comp.templateAst = descriptor.template.ast;
|
|
175
270
|
const start = descriptor.template.loc.start;
|
|
176
271
|
comp.templateStart = { line: start.line, column: start.column };
|
|
177
272
|
if (compiled.map)
|
|
178
273
|
comp.templateMap = new SourceMapConsumer(compiled.map);
|
|
179
274
|
if (compiled.errors.length)
|
|
180
|
-
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));
|
|
181
276
|
for (const m of compiled.code.matchAll(/_ssrRenderSlot\(_ctx\.\$slots, "([^"]+)"/g)) {
|
|
182
277
|
if (!comp.slotNames.includes(m[1]))
|
|
183
278
|
comp.slotNames.push(m[1]);
|
|
@@ -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 {};
|