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.
Files changed (68) hide show
  1. package/README.md +198 -34
  2. package/dist/attrs.d.ts +1 -1
  3. package/dist/attrs.js +24 -22
  4. package/dist/calls.js +17 -15
  5. package/dist/children.d.ts +9 -2
  6. package/dist/children.js +48 -35
  7. package/dist/classes.js +11 -11
  8. package/dist/cli.js +31 -5
  9. package/dist/client.d.ts +35 -2
  10. package/dist/client.js +77 -2
  11. package/dist/compiler.d.ts +1 -1
  12. package/dist/compiler.js +7 -3
  13. package/dist/component.d.ts +3 -1
  14. package/dist/component.js +114 -19
  15. package/dist/constants.d.ts +42 -0
  16. package/dist/constants.js +292 -0
  17. package/dist/context.d.ts +25 -1
  18. package/dist/context.js +11 -5
  19. package/dist/diagnostics.d.ts +23 -0
  20. package/dist/diagnostics.js +30 -0
  21. package/dist/errors.d.ts +867 -0
  22. package/dist/errors.js +330 -0
  23. package/dist/expr.d.ts +1 -0
  24. package/dist/expr.js +120 -49
  25. package/dist/fixture.d.ts +3 -10
  26. package/dist/fixture.js +2 -6
  27. package/dist/hydration.d.ts +15 -0
  28. package/dist/hydration.js +80 -0
  29. package/dist/index.d.ts +2 -1
  30. package/dist/index.js +2 -1
  31. package/dist/link-router.d.ts +24 -0
  32. package/dist/link-router.js +36 -0
  33. package/dist/lists.js +30 -21
  34. package/dist/loops.js +14 -10
  35. package/dist/model.d.ts +32 -1
  36. package/dist/model.js +123 -6
  37. package/dist/narrowing.d.ts +8 -0
  38. package/dist/narrowing.js +40 -9
  39. package/dist/plugins/client-only.d.ts +8 -0
  40. package/dist/plugins/client-only.js +107 -0
  41. package/dist/plugins/i18n.js +14 -14
  42. package/dist/plugins/index.js +3 -1
  43. package/dist/plugins/router-link.js +33 -30
  44. package/dist/plugins/router.js +9 -9
  45. package/dist/plugins/scoped.js +1 -1
  46. package/dist/plugins/shared-types.js +1 -1
  47. package/dist/plugins/stores.js +26 -24
  48. package/dist/plugins/teleport.js +3 -3
  49. package/dist/plugins/twins.d.ts +10 -0
  50. package/dist/plugins/twins.js +164 -0
  51. package/dist/routes.d.ts +10 -0
  52. package/dist/routes.js +6 -0
  53. package/dist/rust.js +18 -14
  54. package/dist/script.d.ts +3 -0
  55. package/dist/script.js +119 -17
  56. package/dist/settle.d.ts +6 -0
  57. package/dist/settle.js +34 -0
  58. package/dist/slots.js +18 -18
  59. package/dist/strings.js +11 -11
  60. package/dist/styles.js +13 -13
  61. package/dist/template.js +11 -11
  62. package/dist/testing.d.ts +1 -0
  63. package/dist/testing.js +3 -2
  64. package/dist/typescript.d.ts +2 -1
  65. package/dist/typescript.js +46 -19
  66. package/dist/vite.d.ts +1 -1
  67. package/dist/vite.js +10 -9
  68. 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 — what `ferrovue::state_script_into` wrote — before
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
+ }
@@ -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 read = files.map((f) => readComponent(f, root));
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);
@@ -1,5 +1,7 @@
1
1
  import { type Component, type N } from "./model.ts";
2
- export declare function readComponent(file: string, root: string): {
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
- export function readComponent(file, root) {
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 (!descriptor.scriptSetup || !descriptor.template) {
48
- fail(comp, "an island needs `<script setup lang=\"ts\">` and a `<template>`");
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 = compileScript(descriptor, { id: name });
59
- const ast = script.scriptSetupAst ?? [];
141
+ const script = descriptor.scriptSetup ? scriptOf(comp, descriptor) : null;
142
+ const ast = script?.scriptSetupAst ?? [];
60
143
  tagAst(ast, "source");
61
- const plainAst = script.scriptAst ?? [];
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.content);
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 {};