ferrovue 0.1.0 → 0.2.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.
@@ -0,0 +1,6 @@
1
+ import { type Component, type N } from "./model.ts";
2
+ export declare function readComponent(file: string, root: string): {
3
+ comp: Component;
4
+ ast: N[];
5
+ ssr: string;
6
+ };
@@ -0,0 +1,251 @@
1
+ /* One `.vue` file read: its props, models, imported types, and Vue's SSR compilation of its template. */
2
+ import { compileScript, compileTemplate, parse as parseSfc } from "@vue/compiler-sfc";
3
+ import { createHash } from "node:crypto";
4
+ import { readFileSync } from "node:fs";
5
+ import { SourceMapConsumer } from "source-map-js";
6
+ import { basename, relative, resolve, sep } from "node:path";
7
+ import { fail, opt, snake, tagAst } from "./model.js";
8
+ import { ctx } from "./context.js";
9
+ 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. */
12
+ function vueErrorNode(err, within) {
13
+ const loc = err.loc;
14
+ if (!loc)
15
+ return undefined;
16
+ const { line, column } = loc.start;
17
+ 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
+ return { type: "VueError", loc: { start: at }, __fv: "source" };
19
+ }
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
+ function inheritAttrs(comp, statements) {
32
+ let found = true;
33
+ for (const st of statements) {
34
+ const call = st.type === "ExpressionStatement" ? st.expression : null;
35
+ const options = call?.type === "CallExpression" && call.callee.type === "Identifier" && call.callee.name === "defineOptions"
36
+ ? call.arguments[0]
37
+ : st.type === "ExportDefaultDeclaration"
38
+ ? st.declaration
39
+ : null;
40
+ if (options?.type !== "ObjectExpression")
41
+ continue;
42
+ for (const p of options.properties) {
43
+ if (p.type !== "ObjectProperty" || p.computed || (p.key.name ?? p.key.value) !== "inheritAttrs")
44
+ continue;
45
+ if (p.value.type !== "BooleanLiteral")
46
+ fail(comp, "`inheritAttrs` is `true` or `false`", p.value);
47
+ found = p.value.value;
48
+ }
49
+ }
50
+ return found;
51
+ }
52
+ export function readComponent(file, root) {
53
+ const name = basename(file, ".vue");
54
+ const source = readFileSync(file, "utf8");
55
+ const { descriptor, errors } = parseSfc(source, { filename: file });
56
+ 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
+ };
88
+ comp.source = source;
89
+ if (errors.length)
90
+ fail(comp, String(errors[0]), vueErrorNode(errors[0]));
91
+ if (!descriptor.scriptSetup || !descriptor.template) {
92
+ fail(comp, "an island needs `<script setup lang=\"ts\">` and a `<template>`");
93
+ }
94
+ // A global `<style>` block changes no markup. A scoped one adds `data-v-…` attributes, whose id is
95
+ // computed here as the bundler computes it. A CSS module renames classes, and `v-bind()` in CSS
96
+ // writes variables onto the root, neither of which the server reproduces.
97
+ for (const st of descriptor.styles) {
98
+ if (st.module)
99
+ fail(comp, "`<style module>` renames classes in the bundler; use a global or scoped `<style>`, or a stylesheet");
100
+ }
101
+ if (descriptor.cssVars.length)
102
+ fail(comp, "`v-bind()` in `<style>` sets variables the server does not render; bind `:style` instead");
103
+ if (descriptor.styles.some((st) => st.scoped))
104
+ comp.scopeId = `data-v-${scopeHash(file, source)}`;
105
+ comp.slotted = descriptor.slotted;
106
+ const script = compileScript(descriptor, { id: name });
107
+ const ast = script.scriptSetupAst ?? [];
108
+ tagAst(ast, "source");
109
+ // A plain `<script>` beside the setup one may declare the types the setup block uses.
110
+ const plainAst = script.scriptAst ?? [];
111
+ tagAst(plainAst, "source");
112
+ for (const s of [...plainAst, ...ast]) {
113
+ if (s.type !== "ImportDeclaration" || !s.source.value.endsWith(".vue"))
114
+ continue;
115
+ const from = basename(s.source.value, ".vue");
116
+ if (s.specifiers.some((sp) => sp.type === "ImportDefaultSpecifier"))
117
+ comp.imports.add(from);
118
+ for (const sp of s.specifiers) {
119
+ if (sp.type === "ImportSpecifier" && (sp.imported.name ?? sp.imported.value) === "Props") {
120
+ comp.childProps.set(sp.local.name, from);
121
+ }
122
+ }
123
+ }
124
+ typesImports(comp, [...plainAst, ...ast]);
125
+ comp.inheritAttrs = inheritAttrs(comp, [...plainAst, ...ast]);
126
+ // Types imported from elsewhere: a store's file, a shared `.ts` file, or another component.
127
+ for (const st of [...plainAst, ...ast]) {
128
+ if (st.type !== "ImportDeclaration")
129
+ continue;
130
+ const from = st.source.value;
131
+ for (const sp of st.specifiers) {
132
+ if (sp.type !== "ImportSpecifier")
133
+ continue;
134
+ const typeName = sp.imported.name ?? sp.imported.value;
135
+ if (from.endsWith(".vue")) {
136
+ if (typeName !== "Props")
137
+ comp.importedTypes.set(sp.local.name, { k: "struct", name: typeName, home: basename(from, ".vue") });
138
+ continue;
139
+ }
140
+ const isType = st.importKind === "type" || sp.importKind === "type";
141
+ const typeFile = resolveImport(comp.file, from);
142
+ if (!typeFile)
143
+ continue;
144
+ const module = typeFile.replace(/\.ts$/, "");
145
+ if ([...ctx.stores.values()].some((x) => x.module === module)) {
146
+ if (ctx.storeStructs.has(typeName))
147
+ comp.importedTypes.set(sp.local.name, { k: "struct", name: typeName, store: true });
148
+ continue;
149
+ }
150
+ if (ctx.helperModule !== null && from === ctx.helperModule && !isType)
151
+ continue;
152
+ readTypeFile(typeFile);
153
+ if (ctx.typeStructs.has(typeName))
154
+ comp.importedTypes.set(sp.local.name, { k: "struct", name: typeName, home: "types" });
155
+ else if (ctx.typeAliases.has(typeName))
156
+ comp.aliases.set(sp.local.name, ctx.typeAliases.get(typeName));
157
+ }
158
+ }
159
+ // Interfaces in two passes: every name first, so one may name another declared after it, or
160
+ // itself — a tree node's children are tree nodes.
161
+ const decls = declareTypes(comp, [...plainAst, ...ast], comp.structs, comp.aliases);
162
+ for (const d of decls)
163
+ comp.structs.set(d.name, structOf(comp, d.name, d.members, comp.structs));
164
+ let propsTy = null;
165
+ for (const s of ast) {
166
+ const call = s.type === "ExpressionStatement" ? s.expression : s.type === "VariableDeclaration" ? s.declarations[0]?.init : null;
167
+ const inner = call?.type === "CallExpression" && call.callee.type === "Identifier" && call.callee.name === "withDefaults" ? call.arguments[0] : call;
168
+ if (inner?.type === "CallExpression" && inner.callee.type === "Identifier" && inner.callee.name === "defineProps" && !inner.typeParameters) {
169
+ fail(comp, "props are declared with a type, `defineProps<{ ... }>()`: a runtime declaration has no Rust type", inner);
170
+ }
171
+ const found = definePropsType(call);
172
+ if (found)
173
+ propsTy = found;
174
+ }
175
+ // A component without `defineProps` takes no props.
176
+ if (!propsTy) {
177
+ // nothing to read
178
+ }
179
+ else if (propsTy.type === "TSTypeLiteral") {
180
+ comp.props = structOf(comp, "Props", propsTy.members, comp.structs);
181
+ }
182
+ else if (propsTy.type === "TSTypeReference" && comp.structs.has(propsTy.typeName.name)) {
183
+ comp.props = { name: "Props", fields: comp.structs.get(propsTy.typeName.name).fields };
184
+ }
185
+ else {
186
+ fail(comp, "`defineProps` takes a type literal or an interface declared in the same block", propsTy);
187
+ }
188
+ // `defineModel`: a prop of its own (`modelValue` unless named), which the binding reads.
189
+ for (const st of ast) {
190
+ if (st.type !== "VariableDeclaration")
191
+ continue;
192
+ for (const d of st.declarations) {
193
+ const call = d.init;
194
+ if (call?.type !== "CallExpression" || call.callee.type !== "Identifier" || call.callee.name !== "defineModel")
195
+ continue;
196
+ const t = call.typeParameters?.params?.[0];
197
+ if (!t)
198
+ fail(comp, "`defineModel` needs its type: `defineModel<string>()`", call);
199
+ const named = call.arguments[0]?.type === "StringLiteral" ? call.arguments[0].value : "modelValue";
200
+ const options = call.arguments.find((a) => a.type === "ObjectExpression");
201
+ const required = options?.properties.some((p) => p.type === "ObjectProperty" && (p.key.name ?? p.key.value) === "required" && p.value.type === "BooleanLiteral" && p.value.value);
202
+ const base = tyOfTs(comp, t, comp.structs);
203
+ comp.props.fields.push({ js: named, rust: snake(named), ty: required ? base : opt(base) });
204
+ comp.models.set(d.id.name, named);
205
+ }
206
+ }
207
+ // What Vue uses for an absent optional prop: the default `compileScript` resolved — from
208
+ // `withDefaults`, a destructured default, or `defineModel`'s options — or `false` for a boolean.
209
+ const defaults = runtimeDefaults(comp, script.content);
210
+ for (const f of comp.props.fields) {
211
+ if (f.ty.k !== "opt")
212
+ continue;
213
+ const node = defaults.get(f.js);
214
+ if (node)
215
+ f.dflt = defaultValue(comp, f, node);
216
+ else if (f.ty.of.k === "bool")
217
+ f.dflt = "false";
218
+ }
219
+ const compiled = compileTemplate({
220
+ source: descriptor.template.content,
221
+ filename: file,
222
+ // As `@vitejs/plugin-vue` compiles it: the scope id written onto every element, and passed to
223
+ // slot content only with `:slotted()` styles.
224
+ id: comp.scopeId ?? name,
225
+ scoped: comp.scopeId !== null,
226
+ slotted: comp.slotted,
227
+ ssr: true,
228
+ ssrCssVars: [],
229
+ compilerOptions: { bindingMetadata: script.bindings, sourceMap: true },
230
+ });
231
+ const start = descriptor.template.loc.start;
232
+ comp.templateStart = { line: start.line, column: start.column };
233
+ if (compiled.map)
234
+ comp.templateMap = new SourceMapConsumer(compiled.map);
235
+ // A template error's position is within the template's content.
236
+ if (compiled.errors.length)
237
+ fail(comp, String(compiled.errors[0]), vueErrorNode(compiled.errors[0], comp.templateStart));
238
+ for (const m of compiled.code.matchAll(/_ssrRenderSlot\(_ctx\.\$slots, "([^"]+)"/g)) {
239
+ if (!comp.slotNames.includes(m[1]))
240
+ comp.slotNames.push(m[1]);
241
+ }
242
+ // Resolved by name, as globally registered components are, or imported from `vue-router`.
243
+ const imported = (exported) => [...plainAst, ...ast].some((st) => st.type === "ImportDeclaration" && st.source.value === "vue-router" &&
244
+ st.specifiers.some((sp) => sp.type === "ImportSpecifier" && (sp.imported.name ?? sp.imported.value) === exported));
245
+ comp.routerLink = compiled.code.includes('_resolveComponent("RouterLink")') || imported("RouterLink");
246
+ comp.routerView = compiled.code.includes('_resolveComponent("RouterView")') || imported("RouterView");
247
+ comp.readsRoute = compiled.code.includes("_ctx.$route");
248
+ comp.readsI18n = compiled.code.includes("_ctx.$t(");
249
+ comp.readsTeleports = compiled.code.includes("_ssrRenderTeleport(");
250
+ return { comp, ast, ssr: compiled.code };
251
+ }
@@ -0,0 +1,151 @@
1
+ import type { I18nSetup } from "./i18n.ts";
2
+ import { type Component, type N, type Struct, type Ty } from "./model.ts";
3
+ /** A type a helper takes or returns, as the configuration spells it. */
4
+ export type TypeName = "string" | "string?" | "int" | "int?" | "float" | "float?" | "bool";
5
+ /** A function a template may call, and the Rust function that is its twin. */
6
+ export interface HelperSpec {
7
+ /** The Rust path the generated code calls, e.g. `crate::helpers::format_count`. */
8
+ rust: string;
9
+ params: TypeName[];
10
+ returns: TypeName;
11
+ /** The longest string it returns, which is what a call adds to the buffer reservation. */
12
+ maxLen?: number;
13
+ }
14
+ /** What `ferrovue.config.json` holds. Paths are relative to the project root. */
15
+ export interface Config {
16
+ /** The directory of `.vue` files to compile. */
17
+ components: string;
18
+ /** The directory the Rust modules are written to. Everything in it is replaced. */
19
+ out: string;
20
+ /** The module components import their helpers from, and the Rust twin of each export. */
21
+ helpers?: {
22
+ module: string;
23
+ functions: Record<string, HelperSpec>;
24
+ };
25
+ /** The Rust type a `TrustedHtml` prop is, which must implement `ferrovue::TrustedHtml`, e.g.
26
+ * `crate::sanitize::SafeHtml`. A type that borrows names the props' lifetime, `'a`:
27
+ * `crate::render::Trusted<'a>`. Without it, a component cannot use `v-html` at all. */
28
+ trustedHtml?: string;
29
+ /** A JSON file listing the app's routes as vue-router paths (`/users/:id`), which `<RouterLink>`
30
+ * resolves against. Without it, a component cannot use `<RouterLink>`. */
31
+ routes?: string;
32
+ /** The router, in full: its routes file, the history's base, and the class names
33
+ * `createRouter` gives active links. `routes` alone is shorthand for `{ routes }`. */
34
+ router?: {
35
+ routes: string;
36
+ base?: string;
37
+ linkActiveClass?: string;
38
+ linkExactActiveClass?: string;
39
+ };
40
+ /** The directory of Pinia stores (`.ts` files), whose state a component may read while it renders
41
+ * on the server. */
42
+ stores?: string;
43
+ /** Custom directives that render nothing on the server — no `getSSRProps` — by name, without
44
+ * `v-`: `["focus", "click-outside"]`. Any other custom directive is refused, since the server
45
+ * cannot know what its `getSSRProps` would add. */
46
+ clientDirectives?: string[];
47
+ /** vue-i18n: the directory of locale files (\`en.json\`, \`nl.json\`), the locale a page renders in
48
+ * when it names none, and the locales a missing message falls back to. */
49
+ i18n?: {
50
+ messages: string;
51
+ locale?: string;
52
+ fallbackLocale?: string | string[];
53
+ };
54
+ /** How a `<style scoped>` component's `data-v-` id is computed, which must be how
55
+ * `@vitejs/plugin-vue` computes it for the client: from the file's path (`"filepath"`, the
56
+ * plugin's choice in development, and with `features.componentIdGenerator: "filepath"`), or its
57
+ * path and source (`"filepath-source"`, the plugin's choice for a production build). */
58
+ scopeId?: ScopeIdMode;
59
+ /** Vite's root, from which a scope id hashes a component's path: the project root by default. */
60
+ viteRoot?: string;
61
+ }
62
+ export type ScopeIdMode = "filepath" | "filepath-source";
63
+ export declare const CONFIG_FILE = "ferrovue.config.json";
64
+ /** Read the project's configuration. */
65
+ export declare function loadConfig(root: string): Config;
66
+ export declare function tyOfName(name: TypeName): Ty;
67
+ /** A route as the routes file lists it: a vue-router path, and the name it may have. */
68
+ export interface RouteDef {
69
+ /** The path as written: relative to the parent's unless it starts with \`/\`. */
70
+ path: string;
71
+ name?: string;
72
+ /** The routes nested in it. */
73
+ children?: RouteDef[];
74
+ /** The path with its ancestors', as vue-router normalises it. */
75
+ fullPath: string;
76
+ }
77
+ /** Every route, nested ones included, parents first. */
78
+ export declare function allRoutes(routes: RouteDef[]): RouteDef[];
79
+ /** A getter: the parameter naming the state (an option store's), and the expression it returns.
80
+ * \`setup\` marks a setup store's \`computed\`, which reads the state's refs and the other getters by
81
+ * name, through \`.value\`. */
82
+ export interface StoreGetter {
83
+ param: string | null;
84
+ body: N;
85
+ file: string;
86
+ setup?: true;
87
+ }
88
+ /** A Pinia option store: `export const usePrefs = defineStore("prefs", { state: (): PrefsState => ... })`. */
89
+ export interface Store {
90
+ /** `usePrefs` */
91
+ hook: string;
92
+ /** `prefs`: its key in `pinia.state.value`. */
93
+ id: string;
94
+ /** `prefs`: its field in the generated `Stores`. */
95
+ field: string;
96
+ /** `PrefsState`: the interface its state is. */
97
+ state: string;
98
+ /** The file, without its extension, as an import names it once resolved. */
99
+ module: string;
100
+ /** Its getters: name → the parameter that names the state, and the expression returned. */
101
+ getters: Map<string, StoreGetter>;
102
+ }
103
+ /** Where `TrustedHtml` comes from. Only that import names the type `v-html` will write raw. */
104
+ export declare const TYPES_MODULE = "ferrovue/types";
105
+ /** What the configuration being compiled says, and what has been read so far: set up by
106
+ * `generate()` at the start of each run, which is synchronous, so one run never sees another's. */
107
+ export declare const ctx: {
108
+ /** The helpers of the configuration being compiled: name → Rust twin, typed. */
109
+ helpers: Record<string, {
110
+ rust: string;
111
+ params: Ty[];
112
+ ret: Ty;
113
+ maxLen: number;
114
+ }>;
115
+ helperModule: string | null;
116
+ trustedHtml: string | null;
117
+ /** Custom directives declared to render nothing on the server. */
118
+ clientDirectives: Set<string>;
119
+ /** The configured routes, when there are any. */
120
+ routes: RouteDef[] | null;
121
+ /** The history's base, and the class names active links take, from the configured router. */
122
+ routerBase: string;
123
+ linkActive: string;
124
+ linkExactActive: string;
125
+ /** The configured stores, by hook name, and every interface their files declare. */
126
+ stores: Map<string, Store>;
127
+ storeStructs: Map<string, Struct>;
128
+ /** The store file each of those interfaces is declared in. */
129
+ storeFiles: Map<string, string>;
130
+ /** The project root, for resolving a component's imports. */
131
+ rootDir: string;
132
+ /** Interfaces and object types declared in shared `.ts` files that components import, and type
133
+ * aliases there; the file each comes from. They are written once, to `types.rs`. */
134
+ typeStructs: Map<string, Struct>;
135
+ typeAliases: Map<string, any>;
136
+ typeFiles: Map<string, string>;
137
+ /** The shared files read so far. */
138
+ typeRead: Set<string>;
139
+ /** Every component being compiled, by name, for a type that names another one's props. */
140
+ components: Map<string, Component>;
141
+ /** Numbers the bindings narrowing introduces, restarted per component so that a file's text
142
+ * depends on its own source alone. */
143
+ narrowCount: number;
144
+ /** The directory the components being compiled come from, for the generated headers. */
145
+ componentsDir: string;
146
+ /** The configured locales and their messages, when there are any. */
147
+ i18n: I18nSetup | null;
148
+ /** How scope ids are computed, and the directory a component's path is hashed from. */
149
+ scopeId: ScopeIdMode;
150
+ viteRoot: string;
151
+ };
@@ -0,0 +1,84 @@
1
+ /* The project's configuration, and the state one `generate()` run builds up. */
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { BOOL, FLOAT, GenError, INT, opt, STR } from "./model.js";
5
+ export const CONFIG_FILE = "ferrovue.config.json";
6
+ /** Read the project's configuration. */
7
+ export function loadConfig(root) {
8
+ const raw = JSON.parse(readFileSync(join(root, CONFIG_FILE), "utf8"));
9
+ if (typeof raw.components !== "string" || typeof raw.out !== "string") {
10
+ throw new GenError(`${CONFIG_FILE} needs \`components\` and \`out\` directories`);
11
+ }
12
+ if (raw.scopeId !== undefined && raw.scopeId !== "filepath" && raw.scopeId !== "filepath-source") {
13
+ throw new GenError(`\`scopeId\` in ${CONFIG_FILE} is "filepath" or "filepath-source", as \`@vitejs/plugin-vue\` computes it`);
14
+ }
15
+ return raw;
16
+ }
17
+ export function tyOfName(name) {
18
+ switch (name) {
19
+ case "string":
20
+ return STR;
21
+ case "string?":
22
+ return opt(STR);
23
+ case "int":
24
+ return INT;
25
+ case "int?":
26
+ return opt(INT);
27
+ case "float":
28
+ return FLOAT;
29
+ case "float?":
30
+ return opt(FLOAT);
31
+ case "bool":
32
+ return BOOL;
33
+ default:
34
+ throw new GenError(`unknown helper type \`${String(name)}\``);
35
+ }
36
+ }
37
+ /** Every route, nested ones included, parents first. */
38
+ export function allRoutes(routes) {
39
+ return routes.flatMap((r) => [r, ...allRoutes(r.children ?? [])]);
40
+ }
41
+ /** Where `TrustedHtml` comes from. Only that import names the type `v-html` will write raw. */
42
+ export const TYPES_MODULE = "ferrovue/types";
43
+ /** What the configuration being compiled says, and what has been read so far: set up by
44
+ * `generate()` at the start of each run, which is synchronous, so one run never sees another's. */
45
+ export const ctx = {
46
+ /** The helpers of the configuration being compiled: name → Rust twin, typed. */
47
+ helpers: {},
48
+ helperModule: null,
49
+ trustedHtml: null,
50
+ /** Custom directives declared to render nothing on the server. */
51
+ clientDirectives: new Set(),
52
+ /** The configured routes, when there are any. */
53
+ routes: null,
54
+ /** The history's base, and the class names active links take, from the configured router. */
55
+ routerBase: "",
56
+ linkActive: "router-link-active",
57
+ linkExactActive: "router-link-exact-active",
58
+ /** The configured stores, by hook name, and every interface their files declare. */
59
+ stores: new Map(),
60
+ storeStructs: new Map(),
61
+ /** The store file each of those interfaces is declared in. */
62
+ storeFiles: new Map(),
63
+ /** The project root, for resolving a component's imports. */
64
+ rootDir: "",
65
+ /** Interfaces and object types declared in shared `.ts` files that components import, and type
66
+ * aliases there; the file each comes from. They are written once, to `types.rs`. */
67
+ typeStructs: new Map(),
68
+ typeAliases: new Map(),
69
+ typeFiles: new Map(),
70
+ /** The shared files read so far. */
71
+ typeRead: new Set(),
72
+ /** Every component being compiled, by name, for a type that names another one's props. */
73
+ components: new Map(),
74
+ /** Numbers the bindings narrowing introduces, restarted per component so that a file's text
75
+ * depends on its own source alone. */
76
+ narrowCount: 0,
77
+ /** The directory the components being compiled come from, for the generated headers. */
78
+ componentsDir: "",
79
+ /** The configured locales and their messages, when there are any. */
80
+ i18n: null,
81
+ /** How scope ids are computed, and the directory a component's path is hashed from. */
82
+ scopeId: "filepath-source",
83
+ viteRoot: "",
84
+ };
@@ -0,0 +1,23 @@
1
+ /** Statements, with adjacent literal pushes merged into one `push_str`. */
2
+ export declare class Emitter {
3
+ lines: string[];
4
+ /** Bytes of markup written once per render, part of what `render` reserves up front: the literal
5
+ * markup, and what the numbers written at run time are expected to take. */
6
+ literalBytes: number;
7
+ /** Rust expressions for the rest of the reservation: a loop's markup once per item. */
8
+ perItem: string[];
9
+ private pending;
10
+ private depth;
11
+ lit(s: string): void;
12
+ /** `bytes` more expected of a value written at run time. */
13
+ expect(bytes: number): void;
14
+ stmt(code: string): void;
15
+ open(code: string): void;
16
+ close(tail?: string): void;
17
+ /** Whether the lines from \`from\` on read \`name\`. Literals are skipped, so markup that happens to
18
+ * spell the name does not count, and so are fields of that name. */
19
+ reads(name: string, from: number): boolean;
20
+ /** Replace \`from\` with \`to\` in line \`index\`: an unused binding renamed to \`_\`. */
21
+ replace(index: number, from: string, to: string): void;
22
+ flush(): void;
23
+ }
@@ -0,0 +1,58 @@
1
+ /* The Rust statements a render is written as. */
2
+ import { rustChar, rustStr } from "./model.js";
3
+ import { occurrences } from "./parens.js";
4
+ /** Statements, with adjacent literal pushes merged into one `push_str`. */
5
+ export class Emitter {
6
+ lines = [];
7
+ /** Bytes of markup written once per render, part of what `render` reserves up front: the literal
8
+ * markup, and what the numbers written at run time are expected to take. */
9
+ literalBytes = 0;
10
+ /** Rust expressions for the rest of the reservation: a loop's markup once per item. */
11
+ perItem = [];
12
+ pending = "";
13
+ depth = 1;
14
+ lit(s) {
15
+ this.pending += s;
16
+ this.literalBytes += Buffer.byteLength(s);
17
+ }
18
+ /** `bytes` more expected of a value written at run time. */
19
+ expect(bytes) {
20
+ this.literalBytes += bytes;
21
+ }
22
+ stmt(code) {
23
+ this.flush();
24
+ this.lines.push(" ".repeat(this.depth) + code);
25
+ }
26
+ open(code) {
27
+ this.stmt(code ? code + " {" : "{");
28
+ this.depth++;
29
+ }
30
+ close(tail = "") {
31
+ this.flush();
32
+ this.depth--;
33
+ this.lines.push(" ".repeat(this.depth) + "}" + tail);
34
+ // `} else {` closes one block and opens the next.
35
+ if (tail.endsWith("{"))
36
+ this.depth++;
37
+ }
38
+ /** Whether the lines from \`from\` on read \`name\`. Literals are skipped, so markup that happens to
39
+ * spell the name does not count, and so are fields of that name. */
40
+ reads(name, from) {
41
+ this.flush();
42
+ return occurrences(this.lines.slice(from).join("\n"), name) > 0;
43
+ }
44
+ /** Replace \`from\` with \`to\` in line \`index\`: an unused binding renamed to \`_\`. */
45
+ replace(index, from, to) {
46
+ // A function, so a `$&` or `$'` in the generated text is not read as a replacement pattern.
47
+ this.lines[index] = this.lines[index].replace(from, () => to);
48
+ }
49
+ flush() {
50
+ if (!this.pending)
51
+ return;
52
+ const text = this.pending;
53
+ this.pending = "";
54
+ // One character is pushed as a `char`.
55
+ const push = /^.$/su.test(text) ? `out.push(${rustChar(text)});` : `out.push_str(${rustStr(text)});`;
56
+ this.lines.push(" ".repeat(this.depth) + push);
57
+ }
58
+ }
package/dist/expr.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ import { type Component, type N, type Scope, type Ty, type Val } from "./model.ts";
2
+ /** The refusal for a string that may hold half of a surrogate pair, used where that half matters. */
3
+ export declare function lonely(what: string): string;
4
+ /** Two strings meeting, where a half of a surrogate pair — which ferrovue holds as U+FFFD — would
5
+ * change the result:
6
+ * - `equal`: two halves may differ, and a half never equals a literal U+FFFD;
7
+ * - `order`: a half orders as a surrogate, below U+E000, where U+FFFD orders above — the same only
8
+ * against a literal of characters below U+D800, which both order after;
9
+ * - `search`: a half searched for (`b`) matches half of a whole pair in `a`;
10
+ * - `join`: two halves side by side join into one character.
11
+ * A string holding U+FFFD in the data still equals, or is found in, a half: README, "Strings". */
12
+ export declare function meet(comp: Component, a: Val, b: Val, what: string, n: N, how: "equal" | "order" | "search" | "join"): void;
13
+ /** Whether a string expression borrows from a temporary it builds — \`&*format!(…)\`, a call that
14
+ * returns a \`String\` — rather than from the props or the state. Such a borrow ends with the block
15
+ * it is in, so a branch of \`?:\` or \`||\` must hand back the string itself. */
16
+ export declare function isTemporary(v: Val): boolean;
17
+ /** A string as a \`Cow\`: borrowed when it lives on, owned when it is a temporary. The \`Cow\` is
18
+ * itself a temporary of the whole statement, so the \`&str\` taken from it lives to its end. A
19
+ * \`String\` built for the purpose is moved into it, and one that is already a \`Cow\` — a branch
20
+ * of \`?:\` or \`||\` — is taken as it is. */
21
+ export declare function asCow(v: Val): string;
22
+ /** Whether built code is a `Cow<str>` already: a runtime routine that borrows when it can
23
+ * (`js_slice`, `js_replace`, `js_pad_start`, …), or a fallback taken from one (`.unwrap_or(Cow…)`). */
24
+ export declare function yieldsCow(code: string): boolean;
25
+ /** Whether a value is a JavaScript number: an integer or a fraction. */
26
+ export declare function isNumber(ty: Ty): boolean;
27
+ /** A number as an `f64`, as JavaScript holds every number. */
28
+ export declare function asF64(v: Val): string;
29
+ /** A type as an error names it. */
30
+ export declare function describeTy(ty: Ty): string;
31
+ export declare function fieldVal(comp: Component, base: string, ty: Ty, js: string, node: N): Val;
32
+ /** A value's truthiness when it is known at generation time: a boolean constant, a string literal,
33
+ * a number JavaScript computed. */
34
+ export declare function known(v: Val): boolean | undefined;
35
+ export declare function truthy(v: Val): string;
36
+ /** A value passed where `want` is expected: a present value into an optional slot is wrapped. */
37
+ export declare function coerce(comp: Component, v: Val, want: Ty, node: N): string;
38
+ /** The JavaScript path an expression names — `$props.user`, `item.author.name` — or `null` when it
39
+ * is not a plain chain of names. */
40
+ export declare function pathOf(n: N): string | null;
41
+ /** A test that an optional value is present, as TypeScript narrows on it: `x`, `x !== undefined`,
42
+ * and, `negated`, `!x` and `x === undefined`, after which the value is present in the other branch. */
43
+ export interface Presence {
44
+ path: string;
45
+ /** The value bound where it is present, and its type there. */
46
+ pattern: (name: string) => string;
47
+ /** The `Option` the pattern matches: the value, or for a truthiness test only its truthy values. */
48
+ option: string;
49
+ /** The test alone, for a branch that never reads the value. */
50
+ present: string;
51
+ of: Ty;
52
+ negated: boolean;
53
+ /** Tested by truthiness, so the test itself is a boolean only when it is negated. */
54
+ truthy: boolean;
55
+ }
56
+ export declare function presence(s: Scope, n: N): Presence | null;
57
+ /** The scope with `p`'s value present, bound as `name`. */
58
+ export declare function narrowTo(s: Scope, p: Presence, name: string): Scope;
59
+ export declare function cond(s: Scope, n: N): string;
60
+ export declare function childOf(name: string): Component;
61
+ /** The reader's route, which the component then takes. */
62
+ export declare function theRoute(s: Scope, n: N): Val;
63
+ /** A field of the route: what `useRoute()` gives that the server knows as vue-router does. */
64
+ export declare function routeField(s: Scope, base: Val, prop: string, n: N): Val;
65
+ /** A Pinia getter read from a store's state, `prefs.doubled`: its expression, translated with the
66
+ * state it takes bound to that state. `null` when `name` is not a getter of that store. */
67
+ export declare function storeGetter(s: Scope, base: Val, name: string, node: N): Val | null;
68
+ /** A Rust string literal's text. */
69
+ export declare function unquote(literal: string): string;
70
+ export declare function expr(s: Scope, n: N): Val;
71
+ /** A boolean computed at run time, or known now when it folded to a constant. */
72
+ export declare function boolOf(code: string): Val;
73
+ export declare function call(s: Scope, n: N): Val;
74
+ /** Whether `n` is `Object.<method>(…)`. */
75
+ export declare function isObjectCall(n: N, method: string): boolean;
76
+ export declare function helperCall(s: Scope, name: string, args: N[], n: N): Val;