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.
Files changed (108) hide show
  1. package/README.md +306 -62
  2. package/dist/args.d.ts +28 -0
  3. package/dist/args.js +69 -0
  4. package/dist/attrs.d.ts +2 -1
  5. package/dist/attrs.js +10 -4
  6. package/dist/calls.js +5 -2
  7. package/dist/children.d.ts +2 -2
  8. package/dist/children.js +84 -16
  9. package/dist/classes.d.ts +4 -0
  10. package/dist/classes.js +23 -1
  11. package/dist/cli.js +80 -81
  12. package/dist/client-render.d.ts +23 -0
  13. package/dist/client-render.js +143 -0
  14. package/dist/client.d.ts +74 -10
  15. package/dist/client.js +373 -42
  16. package/dist/compiler.d.ts +9 -5
  17. package/dist/compiler.js +80 -18
  18. package/dist/component.d.ts +7 -3
  19. package/dist/component.js +38 -27
  20. package/dist/conformance.d.ts +85 -0
  21. package/dist/conformance.js +327 -0
  22. package/dist/constants.js +4 -2
  23. package/dist/context.d.ts +66 -10
  24. package/dist/context.js +176 -5
  25. package/dist/diagnostics.d.ts +6 -4
  26. package/dist/diagnostics.js +7 -3
  27. package/dist/dynamic.d.ts +18 -0
  28. package/dist/dynamic.js +576 -0
  29. package/dist/editor.d.ts +18 -0
  30. package/dist/editor.js +59 -0
  31. package/dist/errors.d.ts +213 -4
  32. package/dist/errors.js +129 -4
  33. package/dist/expr.d.ts +2 -0
  34. package/dist/expr.js +91 -12
  35. package/dist/fallthrough.d.ts +1 -0
  36. package/dist/fallthrough.js +10 -2
  37. package/dist/file-routes.d.ts +33 -0
  38. package/dist/file-routes.js +224 -0
  39. package/dist/files.d.ts +13 -0
  40. package/dist/files.js +49 -0
  41. package/dist/fixture.d.ts +16 -0
  42. package/dist/fixture.js +22 -7
  43. package/dist/hydration.d.ts +12 -1
  44. package/dist/hydration.js +47 -1
  45. package/dist/index.d.ts +3 -3
  46. package/dist/index.js +1 -1
  47. package/dist/inputs.d.ts +24 -0
  48. package/dist/inputs.js +40 -0
  49. package/dist/link-router.d.ts +7 -3
  50. package/dist/link-router.js +8 -4
  51. package/dist/lists.d.ts +16 -1
  52. package/dist/lists.js +78 -4
  53. package/dist/loops.js +4 -4
  54. package/dist/model.d.ts +20 -1
  55. package/dist/model.js +46 -1
  56. package/dist/numbers.js +3 -2
  57. package/dist/page-routes.d.ts +10 -0
  58. package/dist/page-routes.js +10 -0
  59. package/dist/paths.d.ts +5 -0
  60. package/dist/paths.js +7 -0
  61. package/dist/plugin.d.ts +15 -1
  62. package/dist/plugin.js +3 -0
  63. package/dist/plugins/client-only.js +3 -0
  64. package/dist/plugins/css-modules.d.ts +39 -0
  65. package/dist/plugins/css-modules.js +135 -0
  66. package/dist/plugins/head.d.ts +15 -0
  67. package/dist/plugins/head.js +385 -0
  68. package/dist/plugins/i18n.js +5 -12
  69. package/dist/plugins/index.js +4 -1
  70. package/dist/plugins/provide.d.ts +44 -0
  71. package/dist/plugins/provide.js +530 -0
  72. package/dist/plugins/router-link.js +7 -3
  73. package/dist/plugins/router.d.ts +4 -1
  74. package/dist/plugins/router.js +58 -11
  75. package/dist/plugins/scoped.d.ts +10 -0
  76. package/dist/plugins/scoped.js +17 -4
  77. package/dist/plugins/shared-types.js +2 -1
  78. package/dist/plugins/stores.js +26 -15
  79. package/dist/routes.d.ts +5 -2
  80. package/dist/routes.js +8 -2
  81. package/dist/rust.d.ts +9 -1
  82. package/dist/rust.js +83 -32
  83. package/dist/schema.d.ts +25 -0
  84. package/dist/schema.js +73 -0
  85. package/dist/script.js +31 -8
  86. package/dist/settle.d.ts +2 -0
  87. package/dist/settle.js +5 -0
  88. package/dist/slots.js +20 -4
  89. package/dist/ssr.d.ts +3 -0
  90. package/dist/ssr.js +67 -0
  91. package/dist/strings.d.ts +3 -0
  92. package/dist/strings.js +16 -2
  93. package/dist/styles.d.ts +5 -1
  94. package/dist/styles.js +131 -4
  95. package/dist/template.js +92 -11
  96. package/dist/testing.d.ts +4 -4
  97. package/dist/testing.js +3 -30
  98. package/dist/types.d.ts +5 -0
  99. package/dist/typescript.d.ts +11 -1
  100. package/dist/typescript.js +131 -36
  101. package/dist/vhtml.d.ts +4 -0
  102. package/dist/vhtml.js +63 -0
  103. package/dist/vite.d.ts +25 -11
  104. package/dist/vite.js +166 -51
  105. package/dist/volar.d.ts +15 -0
  106. package/dist/volar.js +87 -0
  107. package/package.json +41 -7
  108. 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 { importsOf, readComponent } from "./component.js";
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 { componentSource, isIsland, modSource } from "./rust.js";
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
- export function generate(root, config = loadConfig(root)) {
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 = readdirSync(dir)
36
- .filter((f) => f.endsWith(".vue"))
37
- .toSorted()
38
- .map((f) => join(dir, f));
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"))));
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 all = read.map((r, i) => ({ comp: r.comp, ssr: r.ssr, children: scopes[i].children, attrsBindings: scopes[i].attrsBindings }));
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 = read.find((x) => x.comp.name === i);
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
- for (const r of ordered) {
111
+ const built = ordered.map((r) => {
77
112
  ctx.narrowCount = 0;
78
- out.set(`${r.comp.module}.rs`, componentSource(r.comp, r.ast, r.ssr, components));
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"));
@@ -1,8 +1,12 @@
1
1
  import { type Component, type N } from "./model.ts";
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): {
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, relative } from "node:path";
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
- /** 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]);
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
- export function readComponent(file, root, isChild) {
118
- const name = basename(file, ".vue");
119
- const source = readFileSync(file, "utf8");
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 = relative(root, file);
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
- 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));
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
- for (const st of descriptor.styles) {
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.structs.set(d.name, structOf(comp, d.name, d.members, comp.structs));
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.props = { name: "Props", fields: comp.structs.get(propsTy.typeName.name).fields };
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 {};