ferrovue 0.3.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.
Files changed (85) hide show
  1. package/README.md +32 -13
  2. package/dist/attrs.d.ts +2 -52
  3. package/dist/attrs.js +18 -448
  4. package/dist/calls.d.ts +4 -0
  5. package/dist/calls.js +219 -0
  6. package/dist/children.d.ts +9 -0
  7. package/dist/children.js +273 -0
  8. package/dist/classes.d.ts +13 -0
  9. package/dist/classes.js +176 -0
  10. package/dist/cli.js +0 -17
  11. package/dist/client.js +0 -12
  12. package/dist/compiler.js +25 -90
  13. package/dist/component.js +10 -86
  14. package/dist/context.d.ts +4 -66
  15. package/dist/context.js +2 -33
  16. package/dist/diff.d.ts +0 -3
  17. package/dist/diff.js +0 -9
  18. package/dist/emitter.d.ts +0 -8
  19. package/dist/emitter.js +0 -12
  20. package/dist/expr.d.ts +0 -70
  21. package/dist/expr.js +27 -890
  22. package/dist/fallthrough.d.ts +9 -0
  23. package/dist/fallthrough.js +78 -0
  24. package/dist/fixture.d.ts +2 -1
  25. package/dist/fixture.js +37 -22
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +1 -0
  28. package/dist/islands.d.ts +1 -0
  29. package/dist/islands.js +1 -0
  30. package/dist/lists.d.ts +0 -11
  31. package/dist/lists.js +4 -39
  32. package/dist/loops.d.ts +3 -0
  33. package/dist/loops.js +164 -0
  34. package/dist/model.d.ts +14 -143
  35. package/dist/model.js +27 -12
  36. package/dist/narrowing.d.ts +19 -0
  37. package/dist/narrowing.js +126 -0
  38. package/dist/numbers.d.ts +8 -0
  39. package/dist/numbers.js +73 -0
  40. package/dist/parens.d.ts +0 -27
  41. package/dist/parens.js +0 -44
  42. package/dist/plugin.d.ts +84 -0
  43. package/dist/plugin.js +26 -0
  44. package/dist/plugins/i18n.d.ts +23 -0
  45. package/dist/{i18n.js → plugins/i18n.js} +76 -19
  46. package/dist/plugins/index.d.ts +2 -0
  47. package/dist/plugins/index.js +8 -0
  48. package/dist/plugins/router-link.d.ts +8 -0
  49. package/dist/{router.js → plugins/router-link.js} +19 -61
  50. package/dist/plugins/router.d.ts +42 -0
  51. package/dist/plugins/router.js +256 -0
  52. package/dist/plugins/scoped.d.ts +12 -0
  53. package/dist/plugins/scoped.js +125 -0
  54. package/dist/plugins/shared-types.d.ts +2 -0
  55. package/dist/plugins/shared-types.js +7 -0
  56. package/dist/plugins/stores.d.ts +37 -0
  57. package/dist/plugins/stores.js +377 -0
  58. package/dist/plugins/teleport.d.ts +5 -0
  59. package/dist/plugins/teleport.js +55 -0
  60. package/dist/rust.d.ts +1 -7
  61. package/dist/rust.js +38 -160
  62. package/dist/script.d.ts +0 -6
  63. package/dist/script.js +13 -128
  64. package/dist/slots.d.ts +13 -0
  65. package/dist/slots.js +160 -0
  66. package/dist/strings.d.ts +13 -0
  67. package/dist/strings.js +208 -0
  68. package/dist/styles.d.ts +14 -0
  69. package/dist/styles.js +181 -0
  70. package/dist/template.d.ts +1 -42
  71. package/dist/template.js +14 -738
  72. package/dist/testing.d.ts +1 -0
  73. package/dist/testing.js +1 -8
  74. package/dist/types.d.ts +2 -0
  75. package/dist/typescript.d.ts +0 -13
  76. package/dist/typescript.js +8 -41
  77. package/dist/vite.d.ts +1 -0
  78. package/dist/vite.js +1 -23
  79. package/package.json +7 -2
  80. package/dist/i18n.d.ts +0 -27
  81. package/dist/router.d.ts +0 -17
  82. package/dist/scoped.d.ts +0 -24
  83. package/dist/scoped.js +0 -177
  84. package/dist/stores.d.ts +0 -6
  85. package/dist/stores.js +0 -229
@@ -0,0 +1,176 @@
1
+ import { escapeHtml } from "@vue/shared";
2
+ import { fail, rustStr, STR } from "./model.js";
3
+ import { expr } from "./expr.js";
4
+ import { cond, known, truthy } from "./narrowing.js";
5
+ import { meet } from "./strings.js";
6
+ import { atom, bare, condition, logical, not, strArg } from "./parens.js";
7
+ import { Emitter } from "./emitter.js";
8
+ import { valueAttr } from "./attrs.js";
9
+ export function classItems(s, n) {
10
+ switch (n.type) {
11
+ case "StringLiteral": {
12
+ const t = n.value.trim();
13
+ return t ? [{ lit: t }] : [];
14
+ }
15
+ case "ArrayExpression":
16
+ return n.elements.flatMap((el) => (el ? classItems(s, el) : []));
17
+ case "ObjectExpression":
18
+ const literalNames = n.properties
19
+ .filter((p) => p.type === "ObjectProperty" && !p.computed)
20
+ .map((p) => (p.key.type === "Identifier" ? p.key.name : String(p.key.value)));
21
+ const arrayIndex = (k) => /^(0|[1-9]\d*)$/.test(k) && Number(k) < 2 ** 32 - 1;
22
+ if (n.properties.some((p) => p.type === "ObjectProperty" && p.computed) ||
23
+ literalNames.some(arrayIndex) ||
24
+ new Set(literalNames).size !== literalNames.length) {
25
+ const keys = [];
26
+ const entries = n.properties.map((p) => {
27
+ if (p.type !== "ObjectProperty")
28
+ fail(s.comp, "a class object holds `name: condition` pairs", p);
29
+ const key = p.computed ? expr(s, p.key) : { code: rustStr(p.key.type === "Identifier" ? p.key.name : String(p.key.value)), ty: STR };
30
+ if (key.ty.k !== "str")
31
+ fail(s.comp, "a computed class name is a string", p.key);
32
+ for (const other of keys)
33
+ meet(s.comp, other, key, "a class object's names", p.key, "equal");
34
+ keys.push(key);
35
+ return `(${bare(cond(s, p.value))}, ${bare(key.code)})`;
36
+ });
37
+ return [{ code: `&*fv::class_object(&[${entries.join(", ")}])` }];
38
+ }
39
+ return n.properties.flatMap((p) => {
40
+ if (p.type !== "ObjectProperty")
41
+ fail(s.comp, "a class object holds `name: condition` pairs", p);
42
+ let name;
43
+ if (!p.computed) {
44
+ const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
45
+ if (key !== key.trim() || !key)
46
+ fail(s.comp, `class name \`${key}\` has spaces around it`, p);
47
+ name = { lit: key };
48
+ }
49
+ else {
50
+ const k = expr(s, p.key);
51
+ if (k.ty.k !== "str")
52
+ fail(s.comp, "a computed class name is a string", p.key);
53
+ name = { code: `fv::js_trim(${strArg(k.code)})` };
54
+ }
55
+ const v = expr(s, p.value);
56
+ const on = known(v);
57
+ if (on !== undefined)
58
+ return on ? [name] : [];
59
+ const text = "lit" in name ? rustStr(name.lit) : name.code;
60
+ return [{ code: `if ${condition(truthy(v))} { ${bare(text)} } else { "" }` }];
61
+ });
62
+ case "LogicalExpression":
63
+ if (n.operator === "&&") {
64
+ return conditional(cond(s, n.left), classItems(s, n.right));
65
+ }
66
+ break;
67
+ case "ConditionalExpression":
68
+ if (n.consequent.type === "NullLiteral" || n.alternate.type === "NullLiteral") {
69
+ const branch = n.consequent.type === "NullLiteral" ? n.alternate : n.consequent;
70
+ const test = cond(s, n.test);
71
+ return conditional(n.consequent.type === "NullLiteral" ? not(test) : test, classItems(s, branch));
72
+ }
73
+ break;
74
+ }
75
+ const v = expr(s, n);
76
+ if (v.ty.k === "str")
77
+ return [{ code: v.code }];
78
+ if (v.ty.k === "opt" && v.ty.of.k === "str")
79
+ return [{ code: `${atom(v.code)}.unwrap_or("")` }];
80
+ if (v.ty.k === "undef")
81
+ return [];
82
+ return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
83
+ }
84
+ function conditional(test, items) {
85
+ if (test === "true" || test === "false")
86
+ return test === "true" ? items : [];
87
+ return items.map((it) => ({ code: `if ${condition(test)} { ${"lit" in it ? rustStr(it.lit) : bare(it.code)} } else { "" }` }));
88
+ }
89
+ export function renderClass(s, e, n, after = false) {
90
+ const items = classItems(s, n);
91
+ let wrote = after;
92
+ let i = 0;
93
+ for (; i < items.length; i++) {
94
+ const it = items[i];
95
+ if (!("lit" in it))
96
+ break;
97
+ e.lit((wrote ? " " : "") + escapeHtml(it.lit));
98
+ wrote = true;
99
+ }
100
+ if (i < items.length) {
101
+ const rest = items.slice(i).map((it) => ("lit" in it ? rustStr(it.lit) : it.code));
102
+ e.stmt(`fv::class_into(out, ${wrote}, &[${rest.join(", ")}]);`);
103
+ }
104
+ }
105
+ export function classPresent(s, n) {
106
+ if (n.fvMerged)
107
+ return n.elements.reduce((acc, el) => logical(acc, "||", classPresent(s, el)), "false");
108
+ switch (n.type) {
109
+ case "StringLiteral":
110
+ case "NullLiteral":
111
+ case "ArrayExpression":
112
+ case "ObjectExpression":
113
+ return "true";
114
+ case "LogicalExpression":
115
+ if (n.operator === "&&")
116
+ return logical(not(cond(s, n.left)), "||", classPresent(s, n.right));
117
+ break;
118
+ case "ConditionalExpression":
119
+ if (n.consequent.type === "NullLiteral")
120
+ return logical(cond(s, n.test), "||", classPresent(s, n.alternate));
121
+ if (n.alternate.type === "NullLiteral")
122
+ return logical(not(cond(s, n.test)), "||", classPresent(s, n.consequent));
123
+ break;
124
+ }
125
+ const v = expr(s, n);
126
+ if (v.ty.k === "undef")
127
+ return "false";
128
+ if (v.ty.k === "opt")
129
+ return `${atom(v.code)}.is_some()`;
130
+ return "true";
131
+ }
132
+ export function maybeEqual(s, n) {
133
+ if (n.type === "StringLiteral")
134
+ return true;
135
+ if (n.fvMerged || ["ArrayExpression", "ObjectExpression", "NullLiteral", "LogicalExpression", "ConditionalExpression"].includes(n.type))
136
+ return false;
137
+ const v = expr(s, n);
138
+ return v.ty.k === "str" || (v.ty.k === "opt" && v.ty.of.k === "str");
139
+ }
140
+ export function literalClass(n) {
141
+ if (n.type === "StringLiteral")
142
+ return n.value.trim();
143
+ if (!n.fvMerged)
144
+ return null;
145
+ const parts = n.elements.map(literalClass);
146
+ return parts.includes(null) ? null : parts.filter(Boolean).join(" ");
147
+ }
148
+ export function classAttr(s, n, side) {
149
+ switch (n.type) {
150
+ case "StringLiteral":
151
+ return `fv::Attr::str(${rustStr(n.value)})`;
152
+ case "NullLiteral":
153
+ return 'fv::Attr::str("")';
154
+ case "ArrayExpression":
155
+ case "ObjectExpression": {
156
+ const items = classItems(s, n);
157
+ const names = items.every((it) => "lit" in it)
158
+ ? rustStr(items.map((it) => ("lit" in it ? it.lit : "")).join(" "))
159
+ : `fv::class_names(&[${items.map((it) => ("lit" in it ? rustStr(it.lit) : it.code)).join(", ")}])`;
160
+ const literal = names.startsWith('"');
161
+ if (side === "vnode")
162
+ return literal ? `fv::Attr::str(${names})` : `fv::Attr::from(${names})`;
163
+ return `fv::Attr::Names(${literal ? `std::borrow::Cow::Borrowed(${names})` : `${names}.into()`})`;
164
+ }
165
+ case "LogicalExpression":
166
+ if (n.operator === "&&")
167
+ return `if ${condition(cond(s, n.left))} { ${classAttr(s, n.right, side)} } else { fv::Attr::str("") }`;
168
+ break;
169
+ case "ConditionalExpression":
170
+ return `if ${condition(cond(s, n.test))} { ${classAttr(s, n.consequent, side)} } else { ${classAttr(s, n.alternate, side)} }`;
171
+ }
172
+ const v = expr(s, n);
173
+ if (v.ty.k === "str" || v.ty.k === "undef" || (v.ty.k === "opt" && v.ty.of.k === "str"))
174
+ return valueAttr(s, v, n);
175
+ return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
176
+ }
package/dist/cli.js CHANGED
@@ -1,15 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* `ferrovue`: compile the project's components, run from its root.
3
- *
4
- * ferrovue write the Rust modules to the configured output directory
5
- * ferrovue --check write nothing; exit 1 if the committed modules are not what would be written
6
- * ferrovue --check --diff show a unified diff of differences
7
- * ferrovue --watch write them, then again whenever a component, store, type file, the routes
8
- * or the configuration changes, until interrupted
9
- * ferrovue init scaffold a starter ferrovue.config.json and components directory
10
- * ferrovue -c, --config path to configuration file (default: ferrovue.config.json)
11
- * ferrovue -v, --version print the version and exit
12
- * ferrovue -h, --help print this help and exit */
13
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, watch, writeFileSync } from "node:fs";
14
3
  import { dirname, join, relative, resolve, sep } from "node:path";
15
4
  import { CONFIG_FILE, GenError, generate, loadConfig, VERSION, write } from "./compiler.js";
@@ -30,7 +19,6 @@ Options:
30
19
  -v, --version print the version and exit
31
20
  -h, --help print this help and exit
32
21
  `;
33
- /** A refused construct, or a broken configuration: the message alone, which names the file. */
34
22
  function report(e) {
35
23
  if (e instanceof GenError || e instanceof SyntaxError)
36
24
  console.error(`error: ${e.message}`);
@@ -79,7 +67,6 @@ function check(config, showDiff = false) {
79
67
  have = readdirSync(dir).filter((f) => f.endsWith(".rs"));
80
68
  }
81
69
  catch {
82
- // An absent directory is simply stale.
83
70
  }
84
71
  const stale = [...want.keys()].filter((name) => {
85
72
  try {
@@ -120,8 +107,6 @@ function once(config) {
120
107
  const what = changed.length || removed.length ? `${changed.length} changed, ${removed.length} removed` : "nothing changed";
121
108
  console.log(`${config.out}: ${files.length} files, ${what}`);
122
109
  }
123
- /** Regenerate on every change to what the components are compiled from, reporting errors and
124
- * carrying on. The output directory itself, dependencies and build output are ignored. */
125
110
  function watchProject(configPath) {
126
111
  const run = () => {
127
112
  try {
@@ -138,7 +123,6 @@ function watchProject(configPath) {
138
123
  out = loadConfig(root, configPath).out;
139
124
  }
140
125
  catch {
141
- // A broken configuration ignores nothing; the next run reports it.
142
126
  }
143
127
  const parts = file.split(sep);
144
128
  return (parts.some((p) => p === "node_modules" || p === "target" || p === ".git" || p === "dist") ||
@@ -149,7 +133,6 @@ function watchProject(configPath) {
149
133
  watch(root, { recursive: true }, (_event, name) => {
150
134
  if (!name || ignored(relative(root, join(root, name))))
151
135
  return;
152
- // Editors write a file in several steps: one run once they are done.
153
136
  if (timer)
154
137
  clearTimeout(timer);
155
138
  timer = setTimeout(run, 50);
package/dist/client.js CHANGED
@@ -1,18 +1,9 @@
1
- /* What the browser does with a page ferrovue rendered. */
2
1
  import { createSSRApp } from "vue";
3
- /** A JSON string, skipped whole as it may hold the same letters, or a bare number JSON has no word
4
- * for, which the server writes as JavaScript does where `serde_json` alone would write `null`. */
5
2
  const BARE = /"(?:[^"\\]|\\.)*"|-?Infinity|NaN/g;
6
3
  const NON_FINITE = { NaN: Number.NaN, Infinity: Number.POSITIVE_INFINITY, "-Infinity": Number.NEGATIVE_INFINITY };
7
- /** `JSON.parse`, reading back the bare `NaN`, `Infinity` and `-Infinity` that an island's props and
8
- * the state script hold for a number that is not finite, so the client renders what the server did.
9
- * Throws as `JSON.parse` does on anything else that is not JSON. */
10
4
  function parseJson(text) {
11
- // Plain JSON: almost everything the server writes, and all it wrote before it wrote these.
12
5
  if (!/NaN|Infinity/.test(text))
13
6
  return JSON.parse(text);
14
- // Each token becomes a string that starts with more NULs in a row than any string in the text
15
- // holds (JSON has one way to write a NUL, `\u0000`), so no other value can be mistaken for one.
16
7
  let escaped = "\\u0000";
17
8
  while (text.includes(escaped))
18
9
  escaped += "\\u0000";
@@ -35,7 +26,6 @@ export async function mountIslands(components, options = {}) {
35
26
  const root = options.root ?? document;
36
27
  const report = options.onError ?? ((el, problem) => console.warn(`[ferrovue] island left unhydrated: ${problem}`, el));
37
28
  const elements = Array.from(root.querySelectorAll("[data-island]"));
38
- // Each component the page names, loaded once however many islands it has.
39
29
  const loading = new Map();
40
30
  for (const el of elements) {
41
31
  const name = el.dataset.island ?? "";
@@ -84,8 +74,6 @@ export async function mountIslands(components, options = {}) {
84
74
  },
85
75
  };
86
76
  }
87
- /** The component itself, or what its loader resolves to: a module's default export, or the
88
- * component. A loader that rejects gives its error, which leaves only its own islands unhydrated. */
89
77
  async function load(component) {
90
78
  if (typeof component !== "function" || "props" in component || "displayName" in component || "__vccOpts" in component)
91
79
  return component;
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, resolve } from "node:path";
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 { attrsFlow, scopeFlow } from "./scoped.js";
28
- import { componentSource, isIsland, modSource, routesSource, storesSource, typesSource } from "./rust.js";
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
- if (config.stores)
49
- readStores(root, config.stores);
50
- else {
51
- ctx.stores = new Map();
52
- ctx.storeStructs = new Map();
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,56 +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
- // Whether a component reads a store or the route is known once its setup is read; both reach
67
- // every component on the way down to one that does, and the route every one on the way down to
68
- // a `<RouterLink>`.
69
39
  const scopes = read.map((r) => scopeFor(r.comp, r.ast, components).scope);
70
- // Which roots may be handed scope ids, and which slot content given a slot scope id.
71
- scopeFlow(read.map((r, i) => ({ comp: r.comp, ssr: r.ssr, children: scopes[i].children })));
72
- // Which components may be passed attributes beyond their props.
73
- attrsFlow(read.map((r, i) => ({ comp: r.comp, ssr: r.ssr, children: scopes[i].children, attrsBindings: scopes[i].attrsBindings })));
74
- for (const c of components.values())
75
- c.usesRoute = c.routerLink || c.readsRoute;
76
- for (let changed = true; changed;) {
77
- changed = false;
78
- for (const c of components.values()) {
79
- if (!c.usesRoute && [...c.imports].some((i) => components.get(i)?.usesRoute)) {
80
- c.usesRoute = changed = true;
81
- }
82
- }
83
- }
84
- for (const c of components.values())
85
- c.usesI18n = c.readsI18n;
86
- for (let changed = true; changed;) {
87
- changed = false;
88
- for (const c of components.values()) {
89
- if (!c.usesI18n && [...c.imports].some((i) => components.get(i)?.usesI18n)) {
90
- c.usesI18n = changed = true;
91
- }
92
- }
93
- }
94
- for (const c of components.values())
95
- c.usesTeleports = c.readsTeleports;
96
- for (let changed = true; changed;) {
97
- changed = false;
98
- for (const c of components.values()) {
99
- if (!c.usesTeleports && [...c.imports].some((i) => components.get(i)?.usesTeleports)) {
100
- c.usesTeleports = changed = true;
101
- }
102
- }
103
- }
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);
104
44
  for (const c of components.values())
105
- c.usesStores = c.readsStores;
45
+ c.takes = new Set(renderParams().filter((p) => p.reads(c)).map((p) => p.name));
106
46
  for (let changed = true; changed;) {
107
47
  changed = false;
108
48
  for (const c of components.values()) {
109
- if (!c.usesStores && [...c.imports].some((i) => components.get(i)?.usesStores)) {
110
- c.usesStores = changed = true;
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
+ }
111
54
  }
112
55
  }
113
56
  }
114
57
  const out = new Map();
115
- // A child before its parents, which read the props its scoped slots pass.
116
58
  const ordered = [];
117
59
  const placed = new Set();
118
60
  const place = (r) => {
@@ -128,19 +70,13 @@ export function generate(root, config = loadConfig(root)) {
128
70
  };
129
71
  read.forEach(place);
130
72
  for (const r of ordered) {
131
- // Numbered per component, so a file's text depends on its own source alone.
132
73
  ctx.narrowCount = 0;
133
74
  out.set(`${r.comp.module}.rs`, componentSource(r.comp, r.ast, r.ssr, components));
134
75
  }
135
- out.set("mod.rs", modSource(read.map((r) => r.comp)));
136
- if (ctx.routes)
137
- out.set("route_table.rs", routesSource(ctx.routes, router.routes));
138
- if (ctx.stores.size)
139
- out.set("stores.rs", storesSource(config.stores.replace(/\/?$/, "/")));
140
- if (ctx.typeStructs.size)
141
- out.set("types.rs", typesSource());
142
- if (ctx.i18n)
143
- out.set("i18n.rs", i18nSource(ctx.i18n));
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);
144
80
  return out;
145
81
  }
146
82
  /** Write what \`generate\` produces to the configured directory: only the files whose text changed,
@@ -157,7 +93,6 @@ export function write(root, config = loadConfig(root)) {
157
93
  old = readFileSync(join(target, name), "utf8");
158
94
  }
159
95
  catch {
160
- // A new file.
161
96
  }
162
97
  if (old !== text) {
163
98
  writeFileSync(join(target, name), text);
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, resolve, sep } from "node:path";
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
- /** A Vue compiler error's position as a node \`fail\` can point at: its line and 1-based column,
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,61 +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
- attrNames: new Set(),
88
- idsInAttrs: false,
89
- };
43
+ const comp = blankComponent(name, snake(name), rel);
90
44
  comp.source = source;
91
45
  if (errors.length)
92
46
  fail(comp, String(errors[0]), vueErrorNode(errors[0]));
93
47
  if (!descriptor.scriptSetup || !descriptor.template) {
94
48
  fail(comp, "an island needs `<script setup lang=\"ts\">` and a `<template>`");
95
49
  }
96
- // A global `<style>` block changes no markup. A scoped one adds `data-v-…` attributes, whose id is
97
- // computed here as the bundler computes it. A CSS module renames classes, and `v-bind()` in CSS
98
- // writes variables onto the root, neither of which the server reproduces.
99
50
  for (const st of descriptor.styles) {
100
51
  if (st.module)
101
52
  fail(comp, "`<style module>` renames classes in the bundler; use a global or scoped `<style>`, or a stylesheet");
102
53
  }
103
54
  if (descriptor.cssVars.length)
104
55
  fail(comp, "`v-bind()` in `<style>` sets variables the server does not render; bind `:style` instead");
105
- if (descriptor.styles.some((st) => st.scoped))
106
- comp.scopeId = `data-v-${scopeHash(file, source)}`;
107
- comp.slotted = descriptor.slotted;
56
+ for (const p of ctx.plugins)
57
+ p.sfc?.(comp, descriptor, file, source);
108
58
  const script = compileScript(descriptor, { id: name });
109
59
  const ast = script.scriptSetupAst ?? [];
110
60
  tagAst(ast, "source");
111
- // A plain `<script>` beside the setup one may declare the types the setup block uses.
112
61
  const plainAst = script.scriptAst ?? [];
113
62
  tagAst(plainAst, "source");
114
63
  for (const s of [...plainAst, ...ast]) {
@@ -125,7 +74,6 @@ export function readComponent(file, root) {
125
74
  }
126
75
  typesImports(comp, [...plainAst, ...ast]);
127
76
  comp.inheritAttrs = inheritAttrs(comp, [...plainAst, ...ast]);
128
- // Types imported from elsewhere: a store's file, a shared `.ts` file, or another component.
129
77
  for (const st of [...plainAst, ...ast]) {
130
78
  if (st.type !== "ImportDeclaration")
131
79
  continue;
@@ -143,12 +91,8 @@ export function readComponent(file, root) {
143
91
  const typeFile = resolveImport(comp.file, from);
144
92
  if (!typeFile)
145
93
  continue;
146
- const module = typeFile.replace(/\.ts$/, "");
147
- if ([...ctx.stores.values()].some((x) => x.module === module)) {
148
- if (ctx.storeStructs.has(typeName))
149
- comp.importedTypes.set(sp.local.name, { k: "struct", name: typeName, store: true });
94
+ if (ctx.plugins.some((p) => p.importedType?.(comp, typeFile, typeName, sp.local.name)))
150
95
  continue;
151
- }
152
96
  if (ctx.helperModule !== null && from === ctx.helperModule && !isType)
153
97
  continue;
154
98
  readTypeFile(typeFile);
@@ -158,8 +102,6 @@ export function readComponent(file, root) {
158
102
  comp.aliases.set(sp.local.name, ctx.typeAliases.get(typeName));
159
103
  }
160
104
  }
161
- // Interfaces in two passes: every name first, so one may name another declared after it, or
162
- // itself — a tree node's children are tree nodes.
163
105
  const decls = declareTypes(comp, [...plainAst, ...ast], comp.structs, comp.aliases);
164
106
  for (const d of decls)
165
107
  comp.structs.set(d.name, structOf(comp, d.name, d.members, comp.structs));
@@ -174,9 +116,7 @@ export function readComponent(file, root) {
174
116
  if (found)
175
117
  propsTy = found;
176
118
  }
177
- // A component without `defineProps` takes no props.
178
119
  if (!propsTy) {
179
- // nothing to read
180
120
  }
181
121
  else if (propsTy.type === "TSTypeLiteral") {
182
122
  comp.props = structOf(comp, "Props", propsTy.members, comp.structs);
@@ -187,7 +127,6 @@ export function readComponent(file, root) {
187
127
  else {
188
128
  fail(comp, "`defineProps` takes a type literal or an interface declared in the same block", propsTy);
189
129
  }
190
- // `defineModel`: a prop of its own (`modelValue` unless named), which the binding reads.
191
130
  for (const st of ast) {
192
131
  if (st.type !== "VariableDeclaration")
193
132
  continue;
@@ -206,8 +145,6 @@ export function readComponent(file, root) {
206
145
  comp.models.set(d.id.name, named);
207
146
  }
208
147
  }
209
- // What Vue uses for an absent optional prop: the default `compileScript` resolved — from
210
- // `withDefaults`, a destructured default, or `defineModel`'s options — or `false` for a boolean.
211
148
  const defaults = runtimeDefaults(comp, script.content);
212
149
  for (const f of comp.props.fields) {
213
150
  if (f.ty.k !== "opt")
@@ -221,17 +158,11 @@ export function readComponent(file, root) {
221
158
  const compiled = compileTemplate({
222
159
  source: descriptor.template.content,
223
160
  filename: file,
224
- // As `@vitejs/plugin-vue` compiles it: the scope id written onto every element, and passed to
225
- // slot content only with `:slotted()` styles.
226
- id: comp.scopeId ?? name,
227
- scoped: comp.scopeId !== null,
228
- slotted: comp.slotted,
161
+ ...(claim((p) => p.templateOptions?.(comp)) ?? { id: name, scoped: false, slotted: false }),
229
162
  ssr: true,
230
163
  ssrCssVars: [],
231
164
  compilerOptions: { bindingMetadata: script.bindings, sourceMap: true },
232
165
  });
233
- // `ssrInjectFallthroughAttrs`: a root `<Transition>` or `<KeepAlive>` passes `_attrs` on only to
234
- // a lone element or component without `v-if` or `v-for`.
235
166
  const kids = (n) => n.children.filter((c) => c.type !== 3 && !(c.type === 2 && !c.content.trim()));
236
167
  const roots = descriptor.template.ast ? kids(descriptor.template.ast) : [];
237
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;
@@ -245,20 +176,13 @@ export function readComponent(file, root) {
245
176
  comp.templateStart = { line: start.line, column: start.column };
246
177
  if (compiled.map)
247
178
  comp.templateMap = new SourceMapConsumer(compiled.map);
248
- // A template error's position is within the template's content.
249
179
  if (compiled.errors.length)
250
180
  fail(comp, String(compiled.errors[0]), vueErrorNode(compiled.errors[0], comp.templateStart));
251
181
  for (const m of compiled.code.matchAll(/_ssrRenderSlot\(_ctx\.\$slots, "([^"]+)"/g)) {
252
182
  if (!comp.slotNames.includes(m[1]))
253
183
  comp.slotNames.push(m[1]);
254
184
  }
255
- // Resolved by name, as globally registered components are, or imported from `vue-router`.
256
- const imported = (exported) => [...plainAst, ...ast].some((st) => st.type === "ImportDeclaration" && st.source.value === "vue-router" &&
257
- st.specifiers.some((sp) => sp.type === "ImportSpecifier" && (sp.imported.name ?? sp.imported.value) === exported));
258
- comp.routerLink = compiled.code.includes('_resolveComponent("RouterLink")') || imported("RouterLink");
259
- comp.routerView = compiled.code.includes('_resolveComponent("RouterView")') || imported("RouterView");
260
- comp.readsRoute = compiled.code.includes("_ctx.$route");
261
- comp.readsI18n = compiled.code.includes("_ctx.$t(");
262
- comp.readsTeleports = compiled.code.includes("_ssrRenderTeleport(");
185
+ for (const p of ctx.plugins)
186
+ p.compiled?.(comp, compiled.code, [...plainAst, ...ast]);
263
187
  return { comp, ast, ssr: compiled.code };
264
188
  }