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/context.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { BOOL, FLOAT, GenError, INT, opt, STR } from "./model.js";
4
+ import { formatWarning } from "./diagnostics.js";
5
+ import { CONFIG_SCHEMA, TYPE_NAMES } from "./schema.js";
4
6
  export const CONFIG_FILE = "ferrovue.config.json";
5
- /** Read the project's configuration. */
6
- export function loadConfig(root, configPath) {
7
+ const warnOnConsole = (warning) => console.warn(formatWarning(warning));
8
+ /** Read the project's configuration, handing `warn` a warning for each deprecated key it holds. */
9
+ export function loadConfig(root, configPath, warn = warnOnConsole) {
7
10
  const filePath = configPath ? resolve(root, configPath) : join(root, CONFIG_FILE);
8
11
  const displayName = configPath ?? CONFIG_FILE;
9
12
  let text = "";
@@ -23,15 +26,172 @@ export function loadConfig(root, configPath) {
23
26
  catch (e) {
24
27
  throw new GenError("FV1103", `failed to parse \`${displayName}\`: ${e.message}`, { file: displayName });
25
28
  }
29
+ return checkConfig(raw, displayName, warn);
30
+ }
31
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
32
+ const isStrings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
33
+ function distance(a, b) {
34
+ let row = Array.from({ length: b.length + 1 }, (_, i) => i);
35
+ for (let i = 1; i <= a.length; i++) {
36
+ const next = [i];
37
+ for (let j = 1; j <= b.length; j++)
38
+ next.push(Math.min(row[j] + 1, next[j - 1] + 1, row[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)));
39
+ row = next;
40
+ }
41
+ return row[b.length];
42
+ }
43
+ /** The known key a misspelt one most likely means: the same but for case, or one or two edits
44
+ * away (one for a short key). */
45
+ function closest(key, known) {
46
+ let best = null;
47
+ let least = Math.min(2, Math.floor(key.length / 3)) + 1;
48
+ for (const k of known) {
49
+ const d = k.toLowerCase() === key.toLowerCase() ? 0 : distance(key, k);
50
+ if (d < least)
51
+ [best, least] = [k, d];
52
+ }
53
+ return best;
54
+ }
55
+ /** Check what a configuration file holds against `schema`, naming the first key that is unknown or
56
+ * of the wrong shape, and handing `warn` a warning for each deprecated key. */
57
+ export function checkConfig(raw, file, warn, schema = CONFIG_SCHEMA) {
58
+ const refuse = (code, what) => {
59
+ throw new GenError(code, what, { file });
60
+ };
61
+ const wrong = (key, shape) => refuse("FV1115", `\`${key}\` in ${file} is ${shape}`);
62
+ const keys = (o, path, node) => {
63
+ const props = (node && node.properties) || {};
64
+ for (const k of Object.keys(o)) {
65
+ if (props[k])
66
+ continue;
67
+ const known = Object.keys(props).filter((x) => !x.startsWith("$") && !props[x].deprecated);
68
+ const near = closest(k, known);
69
+ const hint = near ? `did you mean \`${near}\`?` : `it takes ${known.map((x) => `\`${x}\``).join(", ")}`;
70
+ refuse("FV1114", `${path ? `\`${path}\` in ${file}` : file} has no key \`${k}\`: ${hint}`);
71
+ }
72
+ };
73
+ const string = (o, key, path, required = false) => {
74
+ if (o[key] === undefined ? required : typeof o[key] !== "string")
75
+ wrong(path, "a string");
76
+ };
77
+ const typeName = (v, path) => {
78
+ if (typeof v !== "string" || !TYPE_NAMES.includes(v)) {
79
+ refuse("FV1107", `\`${path}\` in ${file} is ${JSON.stringify(v) ?? "missing"}, not a type a helper or twin takes: ${TYPE_NAMES.map((t) => `"${t}"`).join(", ")}`);
80
+ }
81
+ };
82
+ const routesSource = (v, path) => {
83
+ if (typeof v === "string" || (isObject(v) && typeof v.pages === "string" && Object.keys(v).length === 1))
84
+ return;
85
+ refuse("FV1238", `\`${path}\` in ${file} is a JSON file of routes, or \`{ "pages": "…" }\`: the folder of pages vue-router's file-based routing reads`);
86
+ };
87
+ if (!isObject(raw)) {
88
+ const what = Array.isArray(raw) ? "an array" : raw === null ? "`null`" : `a ${typeof raw}`;
89
+ return refuse("FV1113", `${file} holds ${what}, where it holds an object: \`{ "components": "components", "out": "src/generated" }\``);
90
+ }
91
+ const shape = schema.properties ?? {};
92
+ const entry = (node) => node?.additionalProperties || undefined;
93
+ keys(raw, "", schema);
26
94
  if (typeof raw.components !== "string" || typeof raw.out !== "string") {
27
- throw new GenError("FV1104", `${displayName} needs \`components\` and \`out\` directories`, { file: displayName });
95
+ throw new GenError("FV1104", `${file} needs \`components\` and \`out\` directories`, { file });
28
96
  }
29
97
  if (raw.scopeId !== undefined && raw.scopeId !== "filepath" && raw.scopeId !== "filepath-source") {
30
- throw new GenError("FV1105", `\`scopeId\` in ${displayName} is "filepath" or "filepath-source", as \`@vitejs/plugin-vue\` computes it`, { file: displayName });
98
+ throw new GenError("FV1105", `\`scopeId\` in ${file} is "filepath" or "filepath-source", as \`@vitejs/plugin-vue\` computes it`, { file });
31
99
  }
32
100
  if (raw.builders !== undefined && typeof raw.builders !== "boolean") {
33
- throw new GenError("FV1106", `\`builders\` in ${displayName} is \`true\` or \`false\`: whether each props struct gets \`new()\` and a setter per optional field`, { file: displayName });
101
+ throw new GenError("FV1106", `\`builders\` in ${file} is \`true\` or \`false\`: whether each props struct gets \`new()\` and a setter per optional field`, { file });
102
+ }
103
+ for (const key of ["$schema", "stores", "trustedHtml", "viteRoot"])
104
+ string(raw, key, key);
105
+ if (raw.isProduction !== undefined && typeof raw.isProduction !== "boolean")
106
+ wrong("isProduction", "`true` or `false`: whether `@vitejs/plugin-vue` compiles for production");
107
+ if (raw.cssModules !== undefined) {
108
+ const m = raw.cssModules;
109
+ if (!isObject(m))
110
+ return wrong("cssModules", "an object: `{ generateScopedName, hashPrefix?, context? }`");
111
+ keys(m, "cssModules", shape.cssModules);
112
+ string(m, "generateScopedName", "cssModules.generateScopedName", true);
113
+ string(m, "hashPrefix", "cssModules.hashPrefix");
114
+ string(m, "context", "cssModules.context");
34
115
  }
116
+ if (raw.clientDirectives !== undefined && !isStrings(raw.clientDirectives))
117
+ wrong("clientDirectives", 'a list of directive names, as `["focus"]`');
118
+ if (raw.routes !== undefined)
119
+ routesSource(raw.routes, "routes");
120
+ if (raw.router !== undefined) {
121
+ const r = raw.router;
122
+ if (!isObject(r))
123
+ return wrong("router", "an object: `{ routes, base?, linkActiveClass?, linkExactActiveClass? }`");
124
+ keys(r, "router", shape.router);
125
+ routesSource(r.routes, "router.routes");
126
+ for (const key of ["base", "linkActiveClass", "linkExactActiveClass"])
127
+ string(r, key, `router.${key}`);
128
+ }
129
+ if (raw.i18n !== undefined) {
130
+ const i = raw.i18n;
131
+ if (!isObject(i))
132
+ return wrong("i18n", "an object: `{ messages, locale?, fallbackLocale? }`");
133
+ keys(i, "i18n", shape.i18n);
134
+ string(i, "messages", "i18n.messages", true);
135
+ string(i, "locale", "i18n.locale");
136
+ if (i.fallbackLocale !== undefined && typeof i.fallbackLocale !== "string" && !isStrings(i.fallbackLocale))
137
+ wrong("i18n.fallbackLocale", "a locale or a list of them");
138
+ }
139
+ if (raw.helpers !== undefined) {
140
+ const h = raw.helpers;
141
+ if (!isObject(h))
142
+ return wrong("helpers", "an object: `{ module, functions }`");
143
+ keys(h, "helpers", shape.helpers);
144
+ string(h, "module", "helpers.module", true);
145
+ if (!isObject(h.functions))
146
+ return wrong("helpers.functions", "an object of functions by name, each `{ rust, params, returns, maxLen? }`");
147
+ for (const [name, spec] of Object.entries(h.functions)) {
148
+ const path = `helpers.functions.${name}`;
149
+ if (!isObject(spec))
150
+ return wrong(path, "an object: `{ rust, params, returns, maxLen? }`");
151
+ keys(spec, path, entry(shape.helpers?.properties?.functions));
152
+ string(spec, "rust", `${path}.rust`, true);
153
+ if (!Array.isArray(spec.params))
154
+ return wrong(`${path}.params`, 'the list of types the function takes, as `["int"]`');
155
+ spec.params.forEach((t, i) => typeName(t, `${path}.params[${i}]`));
156
+ typeName(spec.returns, `${path}.returns`);
157
+ if (spec.maxLen !== undefined && !(Number.isSafeInteger(spec.maxLen) && spec.maxLen >= 0))
158
+ wrong(`${path}.maxLen`, "a whole number, 0 or more");
159
+ }
160
+ }
161
+ if (raw.twins !== undefined) {
162
+ if (!isObject(raw.twins))
163
+ return wrong("twins", "an object of twins by component name, each `{ rust, props?, slots? }`");
164
+ for (const [name, spec] of Object.entries(raw.twins)) {
165
+ const path = `twins.${name}`;
166
+ if (!isObject(spec))
167
+ return wrong(path, "an object: `{ rust, props?, slots? }`");
168
+ keys(spec, path, entry(shape.twins));
169
+ if (typeof spec.rust !== "string")
170
+ refuse("FV1110", `\`${path}\` in ${file} needs \`rust\`, the path of the function that renders it, as \`crate::ui::v_btn\``);
171
+ if (spec.props !== undefined) {
172
+ if (!isObject(spec.props))
173
+ return wrong(`${path}.props`, 'an object of prop types by name, as `{ "label": "string" }`');
174
+ for (const [prop, t] of Object.entries(spec.props))
175
+ typeName(t, `${path}.props.${prop}`);
176
+ }
177
+ if (spec.slots !== undefined && !isStrings(spec.slots))
178
+ wrong(`${path}.slots`, 'a list of slot names, as `["default"]`');
179
+ }
180
+ }
181
+ const deprecated = (o, node, path) => {
182
+ if (!isObject(o))
183
+ return;
184
+ for (const [k, v] of Object.entries(o)) {
185
+ const at = path ? `${path}.${k}` : k;
186
+ const prop = node.properties?.[k] ?? (node.additionalProperties || undefined);
187
+ if (!prop)
188
+ continue;
189
+ if (prop.deprecated)
190
+ warn(new GenError("FV1117", `\`${at}\` in ${file} is deprecated since ${prop.deprecated.since}, and goes in the next major release: use ${prop.deprecated.use}`, { file }));
191
+ deprecated(v, prop, at);
192
+ }
193
+ };
194
+ deprecated(raw, schema, "");
35
195
  return raw;
36
196
  }
37
197
  export function tyOfName(name) {
@@ -54,6 +214,8 @@ export function tyOfName(name) {
54
214
  throw new GenError("FV1107", `unknown helper type \`${String(name)}\``);
55
215
  }
56
216
  }
217
+ /** The Rust type of an `InlineHtml` prop, and of a `TrustedHtml` one when `trustedHtml` names it. */
218
+ export const INLINE_HTML = "ferrovue::InlineHtml";
57
219
  /** Where `TrustedHtml` comes from. Only that import names the type `v-html` will write raw. */
58
220
  export const TYPES_MODULE = "ferrovue/types";
59
221
  export const ctx = {
@@ -68,10 +230,19 @@ export const ctx = {
68
230
  typeStructs: new Map(),
69
231
  typeAliases: new Map(),
70
232
  typeFiles: new Map(),
233
+ typeDecls: new Map(),
234
+ typeReached: new Set(),
71
235
  typeRead: new Set(),
72
236
  constDecls: new Map(),
73
237
  typeConsts: new Map(),
74
238
  components: new Map(),
75
239
  narrowCount: 0,
76
240
  componentsDir: "",
241
+ vnodeTag: null,
242
+ sources: new Map(),
77
243
  };
244
+ /** A component's text: what the run was given for its path, as an editor's unsaved buffer, or the
245
+ * file on disk. */
246
+ export function readSource(file) {
247
+ return ctx.sources.get(resolve(file)) ?? readFileSync(file, "utf8");
248
+ }
@@ -1,6 +1,6 @@
1
1
  import { type Code } from "./errors.ts";
2
2
  import { GenError } from "./model.ts";
3
- /** One refusal, as `ferrovue --format json` writes it. Lines and columns count from 1; the end is
3
+ /** One refusal or warning, as `ferrovue --format json` writes it. Lines and columns count from 1; the end is
4
4
  * exclusive, and `null` where it is not known. */
5
5
  export interface Diagnostic {
6
6
  file: string | null;
@@ -9,15 +9,17 @@ export interface Diagnostic {
9
9
  endLine: number | null;
10
10
  endColumn: number | null;
11
11
  code: Code;
12
- severity: "error";
12
+ severity: "error" | "warning";
13
13
  title: string;
14
14
  message: string;
15
15
  docs: string;
16
16
  }
17
17
  /** Whether the compiler raised `e` for its input: a refusal, or a file that does not parse. */
18
18
  export declare function isRefusal(e: unknown): e is GenError | SyntaxError;
19
- /** The diagnostic for a refusal. */
20
- export declare function diagnose(e: GenError | SyntaxError): Diagnostic;
19
+ /** The diagnostic for a refusal, or for a warning raised as a `GenError` and not thrown. */
20
+ export declare function diagnose(e: GenError | SyntaxError, severity?: Diagnostic["severity"]): Diagnostic;
21
21
  /** A refusal as the CLI and the Vite plugin show it: `error[FV0604]: `, the message with its
22
22
  * location and quoted line, and where the code is documented. */
23
23
  export declare function formatRefusal(e: GenError | SyntaxError): string;
24
+ /** A warning as the CLI shows it: `warning[FV1117]: `, the message, and where the code is documented. */
25
+ export declare function formatWarning(e: GenError): string;
@@ -5,8 +5,8 @@ const UNPARSED = "FV0001";
5
5
  export function isRefusal(e) {
6
6
  return e instanceof GenError || e instanceof SyntaxError;
7
7
  }
8
- /** The diagnostic for a refusal. */
9
- export function diagnose(e) {
8
+ /** The diagnostic for a refusal, or for a warning raised as a `GenError` and not thrown. */
9
+ export function diagnose(e, severity = "error") {
10
10
  const code = e instanceof GenError ? e.code : UNPARSED;
11
11
  const at = e instanceof GenError ? e.at : null;
12
12
  return {
@@ -16,7 +16,7 @@ export function diagnose(e) {
16
16
  endLine: at?.endLine ?? null,
17
17
  endColumn: at?.endColumn ?? null,
18
18
  code,
19
- severity: "error",
19
+ severity,
20
20
  title: ERRORS[code].title,
21
21
  message: e instanceof GenError ? e.what : e.message.split("\n")[0],
22
22
  docs: docsUrl(code),
@@ -28,3 +28,7 @@ export function formatRefusal(e) {
28
28
  const code = e instanceof GenError ? e.code : UNPARSED;
29
29
  return `error[${code}]: ${e.message}\n = docs: ${docsUrl(code)}`;
30
30
  }
31
+ /** A warning as the CLI shows it: `warning[FV1117]: `, the message, and where the code is documented. */
32
+ export function formatWarning(e) {
33
+ return `warning[${e.code}]: ${e.message}\n = docs: ${docsUrl(e.code)}`;
34
+ }
@@ -0,0 +1,18 @@
1
+ import { type Component, type N, type Scope } from "./model.ts";
2
+ import { Emitter } from "./emitter.ts";
3
+ /** Whether the content a parent gives `slot` of `child` is rendered as virtual nodes, as it is
4
+ * inside an element a `<component :is>` chooses. */
5
+ export declare function slotAsVnodes(child: Component, slot: string): boolean;
6
+ /** Whether a statement of a compiled template renders a `<component :is>`. */
7
+ export declare function isDynamicComponent(st: N): boolean;
8
+ /** Render `_ssrRenderVNode` of a `<component :is>`, or return false for a statement that is not one. */
9
+ export declare function dynamicComponent(s: Scope, e: Emitter, st: N): boolean;
10
+ /** Resolve every `<component :is>` of the components before any is generated: the slots whose
11
+ * content renders as virtual nodes, and the compiled templates as the analyses read them, with
12
+ * each `<component :is>` written out as its choices. */
13
+ export declare function prepareDynamic(read: {
14
+ comp: Component;
15
+ ast: N[];
16
+ ssr: string;
17
+ scope: Scope;
18
+ }[]): string[];