ferrovue 0.14.0 → 0.14.1

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.
package/README.md CHANGED
@@ -57,7 +57,7 @@ the configuration at your project root yourself:
57
57
 
58
58
  ```json
59
59
  {
60
- "$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.14.0/schema.json",
60
+ "$schema": "https://cdn.jsdelivr.net/npm/ferrovue@0.14.1/schema.json",
61
61
  "components": "client/components",
62
62
  "out": "src/generated"
63
63
  }
@@ -226,7 +226,7 @@ ferrovue compiles `<script setup lang="ts">` components, and components with no
226
226
  |---|---|
227
227
  | Prop types | `string`, `number` (integers, `i64`, written and computed as JavaScript does, exact within ±2⁵³), `Float` from `ferrovue/types` (fractions, `f64`), `boolean`, unions of two or more string literals (`"sm" \| "md"`, an alias, or written in place), each a Rust enum generated for it (`Size::Md`, `"md".parse::<Size>()`, written and serialised as its literal; see the crate's [`props`](https://docs.rs/ferrovue/latest/ferrovue/guide/props/index.html#unions-of-string-literals) guide), or an enum of the application's own that `unions` names for the alias; a list of such a union, a list of its enum (`Size[]` is `Vec<Size>`); `T \| undefined`, `T \| null` (an `Option` written as `null`), arrays (`T[]`, `Array<T>`, `readonly T[]`), interfaces and object type aliases (recursive ones too), object types written in place, mapped types and `Record`s over a union of names, indexed types (`Card["size"]`), tuples (`[label: string, n: number]`, a tuple struct written as an array), dictionaries (`Record<string, T>`, `{ [key: string]: T }`, `ferrovue::props::Record` in Rust, which keeps JavaScript's order of keys), another component's exported `Props`, `TrustedHtml`, `InlineHtml`, `Date` (a prop, optional or nullable too, and inside lists, dictionaries and interfaces: `ferrovue::date::JsDate`, or `chrono::DateTime<Utc>` with the `chrono` feature) |
228
228
  | Shared types | Interfaces and type aliases imported from `.ts` files (generated once, into `types.rs`; imported by a relative path or through an alias the project declares, in its tsconfig.json's `compilerOptions.paths` or, with the Vite plugin, Vite's `resolve.alias`: `@/lib/format`), from a store's file, or from another component's `.vue` file; objects of a shared type can be passed between components. A TypeScript `enum` of string members is a Rust enum, as a union of string literals is, its variants named after its members; one of numbers is a number |
229
- | Constants | Constants imported from `.ts` files, object constants declared in `<script setup>`, and `enum`s (imported, or declared in the component, with members set to such constants or to earlier members), evaluated when the component is compiled: strings, numbers, booleans, `null`, arithmetic and joined strings of them as JavaScript evaluates them (`60 * MINUTE`, `` `${BASE}/feed` ``, `"#" + 1.5`, and `**` where it gives an exact integer within ±2⁵³), lists of them, objects of them read field by field (`LABELS.save`, `Tone.Loud`, `Rank[5]`), and lists of objects of them, which become a `const` in `types.rs` (`{ value, label }` items get a type named after the constant, `OptionsItem`, unless the constant is typed with an interface: `OPTIONS: Option[]`; a field that is `null` in some items is `T | null`); lookup tables: an object constant (`as const`, `Record<Size, T>` or untyped, keys computed from enum members too) read by a key whose type is a union of string or integer literals or an `enum` (a prop, a `computed`, a `v-for` item's field, a `?:` of literals, or built from one by `.toUpperCase()`, `.toLowerCase()` or a template literal), or by a constant number (`O[HALF]` reads `O["0.5"]`), compiled to a `match` over the members (over the variants of a union's enum, with no catch-all arm; a table of a union's members, `Record<Tone, Tone>`, reaches a union prop as a `match` from variant to variant), read on by field (`THEME[tone].fg`) in text, attributes, `:class` and `:style`, with `undefined` for a member the object lacks (`ICONS[kind] ?? "none"`, `ICONS[kind]?.label`, `v-if="ICONS[kind]"`, `kind && ICONS[kind]`); a free-text key tested with `Object.hasOwn(ROLES, key)`, against the object's own keys, which narrows the key for `ROLES[key]` in what follows; `Object.keys()` of an object constant; a constant `new Map([[key, value], …])` of string or integer keys, read with `.get(key)` (`undefined` for a key it lacks) and `.has(key)`. See the crate's [`props`](https://docs.rs/ferrovue/latest/ferrovue/guide/props/index.html#constants) guide |
229
+ | Constants | Constants imported from `.ts` files (exported on their declaration, by name in `export { A, B as C }`, as `export default A`, or passed on with `export { A } from` and `export * from`), object constants declared in `<script setup>`, and `enum`s (imported, or declared in the component, with members set to such constants or to earlier members), evaluated when the component is compiled: strings, numbers, booleans, `null`, arithmetic and joined strings of them as JavaScript evaluates them (`60 * MINUTE`, `` `${BASE}/feed` ``, `"#" + 1.5`, and `**` where it gives an exact integer within ±2⁵³), lists of them, objects of them read field by field (`LABELS.save`, `Tone.Loud`, `Rank[5]`), and lists of objects of them, which become a `const` in `types.rs` (`{ value, label }` items get a type named after the constant, `OptionsItem`, unless the constant is typed with an interface: `OPTIONS: Option[]`; a field that is `null` in some items is `T | null`); lookup tables: an object constant (`as const`, `Record<Size, T>` or untyped, keys computed from enum members too) read by a key whose type is a union of string or integer literals or an `enum` (a prop, a `computed`, a `v-for` item's field, a `?:` of literals, or built from one by `.toUpperCase()`, `.toLowerCase()` or a template literal), or by a constant number (`O[HALF]` reads `O["0.5"]`), compiled to a `match` over the members (over the variants of a union's enum, with no catch-all arm; a table of a union's members, `Record<Tone, Tone>`, reaches a union prop as a `match` from variant to variant), read on by field (`THEME[tone].fg`) in text, attributes, `:class` and `:style`, with `undefined` for a member the object lacks (`ICONS[kind] ?? "none"`, `ICONS[kind]?.label`, `v-if="ICONS[kind]"`, `kind && ICONS[kind]`); a free-text key tested with `Object.hasOwn(ROLES, key)`, against the object's own keys, which narrows the key for `ROLES[key]` in what follows; `Object.keys()` of an object constant; a constant `new Map([[key, value], …])` of string or integer keys, read with `.get(key)` (`undefined` for a key it lacks) and `.has(key)`. See the crate's [`props`](https://docs.rs/ferrovue/latest/ferrovue/guide/props/index.html#constants) guide |
230
230
  | Props | Props declared in place, by an interface of the component, or by one imported from a `.ts` file by a relative path (`defineProps<CardProps>()`), extended or joined with `&`; `withDefaults`, destructured props with defaults and new names (`const { size = "md", label: text } = defineProps<…>()`), the props object (`const props = defineProps<…>()`, then `props.label` in the template or in script code), optional booleans (Vue casts an absent one to `false`), `defineModel` (named, required, with defaults), components with no props. A generic component (`<script setup generic="T extends Item">`) renders each type parameter as its constraint, which is all the template can rely on |
231
231
  | Setup | `ref`/`shallowRef`, `computed` (an expression, or a body of statements: see Statements; one that may throw, through `.repeat()`, `.padStart()`, a date, `$t` or a helper, is evaluated where the render first reads it, once, as Vue does), refs that start empty, typed by their type argument (`ref<Row[]>([])`, `ref<Row>()`, `ref<string \| undefined>(undefined)`), refs of a union of string literals, held as its enum (`ref<Choice>(props.tone ?? "")`, a member, a value of the union, a lookup table's value, or a `?:` or `??` of them), plain `const`/`let`, `.value` in script code, helpers with Rust twins, functions (see Functions), a plain `<script>` block beside setup. Lifecycle hooks, `watch` (not `immediate`), `defineEmits`, `defineSlots`, `defineOptions`, `defineExpose` and template refs (`ref(null)`, `useTemplateRef`) are client-only, as is a function only an event handler calls. The template may name them only from event handlers, which the server drops |
232
232
  | Statements | in a `computed`'s body and in a list method's callback (`.map()`, `.filter()`, `.find()`, `.some()`, `.reduce()`, a `.toSorted()` comparator and the others): `const` and `let` of any value the template may compute, destructured too (`const { name, posts: count } = author`); `if` / `else if` / `else` and early `return`, narrowing what follows as TypeScript narrows it (`if (!user) return ""; return user.name`, `if (!a \|\| !a.title) return "-"`); `switch` over a string or a number, with stacked `case`s, `default` and `break`; assignments to the body's `let`s (`=`, `+=`, `-=`, `*=`, `++`, `--` and the others), with the type TypeScript gives them; `for (const x of list)` over a list, `list.entries()`, `Object.keys()` or `Object.entries()`, `for (const ch of text)` over a string by its characters (a character beyond U+FFFF is one), and counting loops `for (let i = 0; i < n; i++)` to an integer bound, with `break`, `continue`, nested loops and `return` inside them, carrying strings, numbers and booleans from one item to the next (`let total = 0; for (const r of rows) total += r.n;`, thousands separators, the largest of a list). Each value is evaluated once, where JavaScript evaluates it, and only on the path that reaches it. `while`, `do…while` and `for…in` loops, assignments to props, refs, `const`s, parameters and fields, `try`, `throw`, `var` and a path that ends without `return` are refused. See the crate's [`script_logic`](https://docs.rs/ferrovue/latest/ferrovue/guide/script_logic/index.html) guide |
package/dist/attrs.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { type N, type Scope, type Val } from "./model.ts";
1
+ import { type N, type Piece, type Scope, type Val } from "./model.ts";
2
2
  import { type ErrorCode } from "./errors.ts";
3
3
  import { Emitter } from "./emitter.ts";
4
+ export declare function displayPieces(e: Emitter, pieces: Piece[]): void;
4
5
  export declare function display(e: Emitter, v: Val): boolean;
5
6
  export declare function interpolate(s: Scope, e: Emitter, v: Val, n: N): void;
6
7
  export declare function attrValue(e: Emitter, v: Val): void;
package/dist/attrs.js CHANGED
@@ -6,23 +6,29 @@ import { CONFIG_FILE } from "./config.js";
6
6
  import { ctx } from "./context.js";
7
7
  import { describeTy, expr, shown } from "./expr.js";
8
8
  import { known, truthy } from "./narrowing.js";
9
- import { asCow, isTemporary, unquote } from "./strings.js";
10
- import { atom, bare, condition, ifElse, not, receiver, scrutinee, strArg } from "./parens.js";
9
+ import { asCow, isTemporary, piecesOf, unquote } from "./strings.js";
10
+ import { atom, bare, condition, ifElse, logical, not, receiver, scrutinee, strArg } from "./parens.js";
11
11
  import { Emitter } from "./emitter.js";
12
12
  import { isDollarAttrs } from "./fallthrough.js";
13
13
  import { claim } from "./plugin.js";
14
14
  import { classAttr, classPresent, literalClass, maybeEqual, renderClass } from "./classes.js";
15
15
  import { mergedStyle, renderStyle, styleAttr } from "./styles.js";
16
16
  const NUMBER_BYTES = 6;
17
- function piecesOf(v) {
18
- const f = v.format;
19
- if (v.ty.k !== "str" || f === undefined || v.code !== `&*format!(${rustStr(f.text)}, ${f.args.join(", ")})`)
20
- return undefined;
21
- return f.pieces;
22
- }
23
17
  function emptyTest(v) {
24
18
  if (neverEmpty(v.ty))
25
19
  return "false";
20
+ const branches = v.branches;
21
+ if (branches !== undefined && branches.of === v.code) {
22
+ const [yes, no] = [emptyTest(branches.yes), emptyTest(branches.no)];
23
+ const binds = branches.test.startsWith("let ");
24
+ if (yes === no && !binds)
25
+ return yes;
26
+ if (!binds && yes === "false")
27
+ return logical(not(branches.test), "&&", no);
28
+ if (!binds && no === "false")
29
+ return logical(branches.test, "&&", yes);
30
+ return `if ${branches.test} { ${yes} } else { ${no} }`;
31
+ }
26
32
  const pieces = piecesOf(v);
27
33
  if (pieces === undefined)
28
34
  return `${receiver(v.code)}.is_empty()`;
@@ -31,17 +37,32 @@ function emptyTest(v) {
31
37
  return "false";
32
38
  return strs.map((p) => `${receiver(p.code)}.is_empty()`).join(" && ");
33
39
  }
40
+ export function displayPieces(e, pieces) {
41
+ for (const p of pieces) {
42
+ if (typeof p === "string")
43
+ e.lit(escapeHtml(p));
44
+ else if ("shown" in p)
45
+ e.stmt(`{ use std::fmt::Write as _; let _ = write!(out, "{}", ${p.shown}); }`);
46
+ else
47
+ display(e, p);
48
+ }
49
+ }
34
50
  export function display(e, v) {
35
51
  const pieces = piecesOf(v);
36
52
  if (pieces !== undefined) {
37
- for (const p of pieces) {
38
- if (typeof p === "string")
39
- e.lit(escapeHtml(p));
40
- else if ("shown" in p)
41
- e.stmt(`{ use std::fmt::Write as _; let _ = write!(out, "{}", ${p.shown}); }`);
42
- else
43
- display(e, p);
44
- }
53
+ displayPieces(e, pieces);
54
+ return true;
55
+ }
56
+ const branches = v.branches;
57
+ if (branches !== undefined && branches.of === v.code) {
58
+ e.open(`if ${branches.test}`);
59
+ display(e, branches.yes);
60
+ e.close(" else {");
61
+ const at = e.lines.length;
62
+ display(e, branches.no);
63
+ e.close();
64
+ if (e.lines.length === at + 1)
65
+ e.lines.splice(at - 1, 2, e.lines[at - 1].replace(/\} else \{$/, "}"));
45
66
  return true;
46
67
  }
47
68
  if (v.ty.k === "str" && known(v) !== undefined)
package/dist/classes.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { type N, type Scope } from "./model.ts";
1
+ import { type N, type Scope, type Val } from "./model.ts";
2
2
  import { Emitter } from "./emitter.ts";
3
3
  export type ClassItem = {
4
4
  lit: string;
5
5
  } | {
6
6
  code: string;
7
+ join?: Val;
7
8
  };
8
9
  export declare function classItems(s: Scope, n: N): ClassItem[];
9
10
  export declare function renderClass(s: Scope, e: Emitter, n: N, after?: boolean): void;
package/dist/classes.js CHANGED
@@ -3,10 +3,10 @@ import { directiveOwner, fail, nothing, rustStr, STR } from "./model.js";
3
3
  import { expr } from "./expr.js";
4
4
  import { choice, cond, known, narrowing, narrowTo, presence, truthy } from "./narrowing.js";
5
5
  import { pickPresent } from "./constants.js";
6
- import { meet, unquote } from "./strings.js";
6
+ import { meet, piecesOf, unquote } from "./strings.js";
7
7
  import { atom, bare, condition, logical, not, occurrences, strArg } from "./parens.js";
8
8
  import { Emitter } from "./emitter.js";
9
- import { stringAttr } from "./attrs.js";
9
+ import { displayPieces, stringAttr } from "./attrs.js";
10
10
  import { fresh, jsHint } from "./names.js";
11
11
  export function classItems(s, n) {
12
12
  switch (n.type) {
@@ -79,7 +79,7 @@ export function classItems(s, n) {
79
79
  if (literal !== null && literal === literal.trim())
80
80
  return [{ lit: literal }];
81
81
  if (v.ty.k === "str")
82
- return [{ code: v.code }];
82
+ return [piecesOf(v) === undefined ? { code: v.code } : { code: v.code, join: v }];
83
83
  if (v.ty.k === "opt" && v.ty.of.k === "str")
84
84
  return [{ code: `${atom(v.code)}.unwrap_or("")` }];
85
85
  if (nothing(v.ty))
@@ -118,10 +118,49 @@ export function renderClass(s, e, n, after = false) {
118
118
  e.lit((wrote ? " " : "") + escapeHtml(it.lit));
119
119
  wrote = true;
120
120
  }
121
- if (i < items.length) {
122
- const rest = items.slice(i).map((it) => ("lit" in it ? rustStr(it.lit) : it.code));
123
- e.stmt(`fv::attrs::class_into(out, ${wrote}, &[${rest.join(", ")}]);`);
121
+ let group = [];
122
+ let at = null;
123
+ for (; i < items.length; i++) {
124
+ const it = items[i];
125
+ const parts = "join" in it && it.join !== undefined ? trimmedParts(it.join) : null;
126
+ if (parts === null) {
127
+ if (!group.length && !wrote && items.slice(i + 1).some((later) => "join" in later && later.join !== undefined)) {
128
+ at = fresh("at", "class");
129
+ e.stmt(`let ${at} = out.len();`);
130
+ }
131
+ group.push("lit" in it ? rustStr(it.lit) : it.code);
132
+ continue;
133
+ }
134
+ if (group.length)
135
+ e.stmt(`fv::attrs::class_into(out, ${wrote}, &[${group.join(", ")}]);`);
136
+ if (wrote)
137
+ e.lit(" ");
138
+ else if (group.length && at !== null)
139
+ e.stmt(`if out.len() != ${at} { out.push(' '); }`);
140
+ displayPieces(e, parts);
141
+ [group, at, wrote] = [[], null, true];
142
+ }
143
+ if (group.length)
144
+ e.stmt(`fv::attrs::class_into(out, ${wrote}, &[${group.join(", ")}]);`);
145
+ }
146
+ /** The pieces of a joined class name with the spaces `normalizeClass` trims taken off its ends,
147
+ * when only its first or last piece may hold them and the join writes a name: null otherwise. */
148
+ function trimmedParts(v) {
149
+ const pieces = [...piecesOf(v)];
150
+ const solid = (p, start) => p !== undefined && (typeof p === "string" ? p !== "" && p === (start ? p.trimStart() : p.trimEnd()) : "shown" in p || p.ty.k !== "str");
151
+ for (const start of [true, false]) {
152
+ const edge = start ? 0 : pieces.length - 1;
153
+ const next = start ? 1 : pieces.length - 2;
154
+ const p = pieces[edge];
155
+ if (typeof p === "string")
156
+ pieces[edge] = start ? p.trimStart() : p.trimEnd();
157
+ if (solid(pieces[edge], start))
158
+ continue;
159
+ if (typeof p === "string" || "shown" in p || pieces.length < 2 || !solid(pieces[next], start))
160
+ return null;
161
+ pieces[edge] = { code: `fv::strings::js_trim_${start ? "start" : "end"}(${strArg(p.code)})`, ty: STR };
124
162
  }
163
+ return pieces;
125
164
  }
126
165
  const UNWRITTEN = new Set(["on", "if", "else-if", "else", "for", "slot", "once", "memo", "cloak", "pre"]);
127
166
  /** The class list of an element in content Vue renders from virtual nodes, in the order Vue's
@@ -2,9 +2,9 @@ import { absence, enumOf, fail, FLOAT, joinAbsence, joinTy, literalsOf, literalT
2
2
  import { GenError } from "./errors.js";
3
3
  import { annotatedStr, exactlyEnum, intoEnum, knownLiteral } from "./enums.js";
4
4
  import { ctx } from "./context.js";
5
- import { atom, bare, enclosed, occurrences, strArg } from "./parens.js";
5
+ import { atom, bare, condition, enclosed, occurrences, strArg } from "./parens.js";
6
6
  import { collected, sliced, typedEmpty, untypedEmpty } from "./lists.js";
7
- import { asCow, isTemporary, loneOf } from "./strings.js";
7
+ import { asCow, isTemporary, loneOf, piecesOf } from "./strings.js";
8
8
  import { asF64, intFromF64, isCast, isNumber } from "./numbers.js";
9
9
  import { choice, known, literalNarrowing, narrowTo, presence, truthTested, truthy } from "./narrowing.js";
10
10
  import { ownedStruct, ownedStructs } from "./objects.js";
@@ -65,7 +65,14 @@ function branches(s, n, t) {
65
65
  a = { ...a, ty: b.ty };
66
66
  if (emptyB && a.ty.k === "list")
67
67
  b = { ...b, ty: a.ty };
68
- return choiceOf({ comp, n, a, b, emptyA, emptyB, choose: chooser(t, p, name, a), lone: loneOf(a, b) });
68
+ const [x, y] = [a, b];
69
+ const head = () => {
70
+ if (!p)
71
+ return { test: condition(truthy(t)), yes: x, no: y };
72
+ const [present, absent] = p.negated ? [y, x] : [x, y];
73
+ return { test: occurrences(present.code, name) ? p.pattern(name) : condition(p.present), yes: present, no: absent };
74
+ };
75
+ return choiceOf({ comp, n, a, b, emptyA, emptyB, choose: chooser(t, p, name, a), head, lone: loneOf(a, b) });
69
76
  }
70
77
  function chooser(t, p, name, a) {
71
78
  const second = (code) => (a.ty.k === "str" ? strArg(code) : bare(code));
@@ -90,7 +97,9 @@ function choiceOf(c) {
90
97
  if (owned !== null)
91
98
  return owned;
92
99
  if (sameTy(a.ty, b.ty) && a.ty.k === "str" && (isTemporary(a) || isTemporary(b))) {
93
- return { code: `&*(${choose(asCow(a), asCow(b))})`, ty: STR, ...lone };
100
+ const code = `&*(${choose(asCow(a), asCow(b))})`;
101
+ const joins = (v) => piecesOf(v) !== undefined || v.branches?.of === v.code;
102
+ return { code, ty: STR, ...lone, ...(joins(a) || joins(b) ? { branches: { of: code, ...c.head() } } : {}) };
94
103
  }
95
104
  if (sameTy(a.ty, b.ty) && a.ty.k === "list" && a.ty.of.k !== "undef")
96
105
  return listChoice(c);
@@ -1,4 +1,4 @@
1
- import { type Component, type N, type Scope, type Val } from "./model.ts";
1
+ import { type Component, type Fn, type N, type Scope, type Val } from "./model.ts";
2
2
  export type Const = {
3
3
  k: "scalar";
4
4
  js: Scalar;
@@ -25,10 +25,30 @@ type Scalar = string | number | boolean | null;
25
25
  export interface Declared {
26
26
  node: N;
27
27
  home: Component;
28
- exported: boolean;
29
28
  }
30
29
  /** Remember the constants and enums a file declares at its top level, to evaluate when one is used. */
31
30
  export declare function declareConsts(home: Component, body: N[]): Map<string, Declared>;
31
+ /** A name a `.ts` file exports: one of its own, by its local name, or one of another file's, by the
32
+ * name that file exports it as (`null` for a module that is no `.ts` file of the project, or for a
33
+ * namespace, `export * as ns from`). */
34
+ export type Export = {
35
+ local: string;
36
+ } | {
37
+ file: string | null;
38
+ name: string | null;
39
+ };
40
+ /** What a `.ts` file exports: its names, and the files whose names `export * from` passes on. */
41
+ export interface Exports {
42
+ names: Map<string, Export>;
43
+ stars: (string | null)[];
44
+ }
45
+ /** The names a `.ts` file exports: by `export const`, `export function`, `export enum`,
46
+ * `export { a, b as c }`, `export default a`, and from other files by `export { a } from` and `export * from`. */
47
+ export declare function exportsOf(rel: string, body: N[]): Exports;
48
+ /** The constant or enum a `.ts` file exports as `name`. */
49
+ export declare function exportedConst(file: string, name: string): Declared | undefined;
50
+ /** The function a `.ts` file exports as `name`. */
51
+ export declare function exportedFunction(file: string, name: string): Fn | undefined;
32
52
  /** What a declared constant or enum stands for, evaluated once. */
33
53
  export declare function constOfDecl(d: Declared): Const;
34
54
  /** The type an enum stands for: the union of its members' literal types. */
package/dist/constants.js CHANGED
@@ -2,7 +2,8 @@ import { absence, blankComponent, BOOL, distinctFields, enumOf, enumTy, fail, FL
2
2
  import { GenError } from "./errors.js";
3
3
  import { aliasEnum, knownLiteral, matchOver, overVariants } from "./enums.js";
4
4
  const TYPES_HOME = blankComponent("types", "types", "types");
5
- import { ONE_NOTHING, reachType, tyOfTs } from "./typescript.js";
5
+ import { ONE_NOTHING, reachType, readTypeFile, tyOfTs } from "./typescript.js";
6
+ import { resolveImport } from "./resolve.js";
6
7
  import { ctx } from "./context.js";
7
8
  import { asF64, exactPower, numberVal } from "./numbers.js";
8
9
  import { describeTy, expr } from "./expr.js";
@@ -18,19 +19,100 @@ let enumScope = null;
18
19
  export function declareConsts(home, body) {
19
20
  const found = new Map();
20
21
  for (const st of body) {
21
- const exported = st.type === "ExportNamedDeclaration";
22
- const d = exported ? st.declaration : st;
22
+ const d = st.type === "ExportNamedDeclaration" ? st.declaration : st;
23
23
  if (d?.type === "TSEnumDeclaration")
24
- found.set(d.id.name, { node: d, home, exported });
24
+ found.set(d.id.name, { node: d, home });
25
25
  if (d?.type === "VariableDeclaration" && d.kind === "const") {
26
26
  for (const v of d.declarations)
27
27
  if (v.id.type === "Identifier" && v.init)
28
- found.set(v.id.name, { node: v, home, exported });
28
+ found.set(v.id.name, { node: v, home });
29
29
  }
30
30
  }
31
31
  files.set(home, found);
32
32
  return found;
33
33
  }
34
+ /** The names a `.ts` file exports: by `export const`, `export function`, `export enum`,
35
+ * `export { a, b as c }`, `export default a`, and from other files by `export { a } from` and `export * from`. */
36
+ export function exportsOf(rel, body) {
37
+ const names = new Map();
38
+ const stars = [];
39
+ const nameOf = (n) => n.name ?? n.value;
40
+ for (const st of body) {
41
+ if (st.type === "ExportDefaultDeclaration") {
42
+ const d = st.declaration;
43
+ names.set("default", { local: d.type === "Identifier" ? d.name : "default" });
44
+ }
45
+ if (st.exportKind === "type")
46
+ continue;
47
+ if (st.type === "ExportAllDeclaration") {
48
+ const file = resolveImport(rel, st.source.value);
49
+ if (st.exported)
50
+ names.set(nameOf(st.exported), { file: null, name: null });
51
+ else
52
+ stars.push(file);
53
+ }
54
+ if (st.type !== "ExportNamedDeclaration")
55
+ continue;
56
+ const d = st.declaration;
57
+ if (d?.type === "VariableDeclaration")
58
+ for (const v of d.declarations)
59
+ if (v.id.type === "Identifier")
60
+ names.set(v.id.name, { local: v.id.name });
61
+ if (d?.id?.type === "Identifier")
62
+ names.set(d.id.name, { local: d.id.name });
63
+ if (d)
64
+ continue;
65
+ const from = st.source ? resolveImport(rel, st.source.value) : undefined;
66
+ for (const sp of st.specifiers) {
67
+ if (sp.exportKind === "type")
68
+ continue;
69
+ const as = nameOf(sp.exported);
70
+ if (sp.type === "ExportNamespaceSpecifier")
71
+ names.set(as, { file: null, name: null });
72
+ else if (sp.type === "ExportSpecifier")
73
+ names.set(as, from === undefined ? { local: sp.local.name } : { file: from, name: nameOf(sp.local) });
74
+ }
75
+ }
76
+ return { names, stars };
77
+ }
78
+ /** The file and local name of what a `.ts` file exports as `name`, followed through `export … from`
79
+ * and `export * from`; `undefined` when nothing is exported by that name, or two `export *` give it. */
80
+ function exportOrigin(file, name, seen = new Set()) {
81
+ const key = `${file}\0${name}`;
82
+ if (seen.has(key))
83
+ return undefined;
84
+ seen.add(key);
85
+ readTypeFile(file);
86
+ const exports = ctx.tsModules.get(file)?.exports;
87
+ const e = exports?.names.get(name);
88
+ if (e !== undefined) {
89
+ if ("local" in e)
90
+ return { file, local: e.local };
91
+ return e.file === null || e.name === null ? undefined : exportOrigin(e.file, e.name, seen);
92
+ }
93
+ if (name === "default")
94
+ return undefined;
95
+ let found;
96
+ for (const star of exports?.stars ?? []) {
97
+ const o = star === null ? undefined : exportOrigin(star, name, new Set(seen));
98
+ if (o === undefined)
99
+ continue;
100
+ if (found !== undefined && (found.file !== o.file || found.local !== o.local))
101
+ return undefined;
102
+ found = o;
103
+ }
104
+ return found;
105
+ }
106
+ /** The constant or enum a `.ts` file exports as `name`. */
107
+ export function exportedConst(file, name) {
108
+ const o = exportOrigin(file, name);
109
+ return o === undefined ? undefined : ctx.constDecls.get(o.file)?.get(o.local);
110
+ }
111
+ /** The function a `.ts` file exports as `name`. */
112
+ export function exportedFunction(file, name) {
113
+ const o = exportOrigin(file, name);
114
+ return o === undefined ? undefined : ctx.tsModules.get(o.file)?.functions.get(o.local);
115
+ }
34
116
  /** What a declared constant or enum stands for, evaluated once. */
35
117
  export function constOfDecl(d) {
36
118
  const done = evaluated.get(d.node);
package/dist/context.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Config, TypeName } from "./config.ts";
2
2
  import type { Plugin } from "./plugin.ts";
3
- import type { Declared } from "./constants.ts";
3
+ import type { Declared, Exports } from "./constants.ts";
4
4
  import type { Alias } from "./compiler.ts";
5
5
  import type { TsProject } from "./resolve.ts";
6
6
  import { type Component, type EnumDef, type Fn, type N, type Struct, type Ty } from "./model.ts";
@@ -51,10 +51,8 @@ export declare const ctx: {
51
51
  tsModules: Map<string, {
52
52
  home: Component;
53
53
  body: N[];
54
- functions: Map<string, {
55
- fn: Fn;
56
- exported: boolean;
57
- }>;
54
+ functions: Map<string, Fn>;
55
+ exports: Exports;
58
56
  }>;
59
57
  typeConsts: Map<string, {
60
58
  of: unknown;
package/dist/dynamic.js CHANGED
@@ -4,6 +4,7 @@ import { basename } from "node:path";
4
4
  import { camelize, directiveOwner, fail, isAttribute, literalsOf, placeOf, rustStr, templateAst } from "./model.js";
5
5
  import { GenError } from "./errors.js";
6
6
  import { ctx } from "./context.js";
7
+ import { exportedConst } from "./constants.js";
7
8
  import { matchOver } from "./enums.js";
8
9
  import { expr } from "./expr.js";
9
10
  import { cond, known } from "./narrowing.js";
@@ -208,8 +209,8 @@ function tableOf(s, n) {
208
209
  if (!sp)
209
210
  continue;
210
211
  const file = resolveImport(s.comp.file, st.source.value);
211
- const d = file === null ? undefined : ctx.constDecls.get(file)?.get(sp.imported.name ?? sp.imported.value);
212
- if (!d?.exported || d.node.type !== "VariableDeclarator")
212
+ const d = file === null ? undefined : exportedConst(file, sp.imported.name ?? sp.imported.value);
213
+ if (!d || d.node.type !== "VariableDeclarator")
213
214
  return null;
214
215
  const entries = objectEntries(d.home, name, d.node.init);
215
216
  if (!entries)
@@ -5,10 +5,7 @@ export declare function functionOf(st: N): {
5
5
  node: N;
6
6
  } | null;
7
7
  /** The functions a `.ts` file declares at its top level, exported or not. */
8
- export declare function declareFunctions(home: Component, body: N[], file: string): Map<string, {
9
- fn: Fn;
10
- exported: boolean;
11
- }>;
8
+ export declare function declareFunctions(home: Component, body: N[], file: string): Map<string, Fn>;
12
9
  /** A call of a function the server translates: its arguments evaluated in order at the call, each
13
10
  * bound once, then its body, with JavaScript's defaults for what is not passed. */
14
11
  export declare function callFunction(s: Scope, name: string, fn: Fn, args: N[], n: N): Val;
package/dist/functions.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { fail, placeOf } from "./model.js";
2
2
  import { GenError } from "./errors.js";
3
- import { constOfDecl } from "./constants.js";
3
+ import { constOfDecl, exportedConst, exportedFunction } from "./constants.js";
4
4
  import { ctx } from "./context.js";
5
5
  import { expr, typedBy } from "./expr.js";
6
6
  import { bodyExpr, bound, destructured } from "./blocks.js";
@@ -21,16 +21,15 @@ export function declareFunctions(home, body, file) {
21
21
  const found = new Map();
22
22
  for (const st of body) {
23
23
  if (st.type === "ExportDefaultDeclaration" && (st.declaration.type === "FunctionDeclaration" || FUNCTION_INITS.has(st.declaration.type))) {
24
- found.set("default", { fn: { name: st.declaration.id?.name ?? "default", node: st.declaration, home, file }, exported: true });
24
+ found.set("default", { name: st.declaration.id?.name ?? "default", node: st.declaration, home, file });
25
25
  continue;
26
26
  }
27
- const exported = st.type === "ExportNamedDeclaration";
28
- const d = exported ? st.declaration : st;
27
+ const d = st.type === "ExportNamedDeclaration" ? st.declaration : st;
29
28
  const decls = d?.type === "VariableDeclaration" && d.kind === "const" ? d.declarations : [d];
30
29
  for (const x of decls) {
31
30
  const f = functionOf(x);
32
31
  if (f)
33
- found.set(f.name, { fn: { name: f.name, node: f.node, home, file }, exported });
32
+ found.set(f.name, { name: f.name, node: f.node, home, file });
34
33
  }
35
34
  }
36
35
  return found;
@@ -54,7 +53,7 @@ function moduleOf(file) {
54
53
  m.refusedBy.set(local, e);
55
54
  }
56
55
  };
57
- for (const [name, { fn }] of read.functions) {
56
+ for (const [name, fn] of read.functions) {
58
57
  m.functions.set(name, fn);
59
58
  m.clientOnly.set(name, `it is a function of ${read.home.file}, which a template or a function may call, but not read as a value`);
60
59
  }
@@ -72,11 +71,11 @@ function moduleOf(file) {
72
71
  if (sp.importKind === "type")
73
72
  continue;
74
73
  const imported = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : "default";
75
- const f = other?.functions.get(imported);
76
- const d = from !== null ? ctx.constDecls.get(from)?.get(imported) : undefined;
77
- if (f?.exported)
78
- m.functions.set(sp.local.name, f.fn);
79
- else if (d?.exported && sp.type === "ImportSpecifier")
74
+ const f = from !== null ? exportedFunction(from, imported) : undefined;
75
+ const d = from !== null ? exportedConst(from, imported) : undefined;
76
+ if (f)
77
+ m.functions.set(sp.local.name, f);
78
+ else if (d && sp.type === "ImportSpecifier")
80
79
  constant(sp.local.name, d, `it is imported from ${other.home.file}`);
81
80
  else
82
81
  m.clientOnly.set(sp.local.name, `it is imported from \`${st.source.value}\`, which the server does not run`);
@@ -20,7 +20,7 @@ export interface LinkRoutesOptions {
20
20
  /** A Vue plugin giving `<RouterLink>` and `useRoute()` the page's route, with no router. */
21
21
  export interface LinkRoutes {
22
22
  /** Registers vue-router's `<RouterLink>` and provides the page's route to it, to `useRoute()`
23
- * and to `$route`. */
23
+ * and to `$route`, and the small router to `useRouter()` and `$router`. */
24
24
  install(app: App): void;
25
25
  /** `to` resolved as vue-router's `router.resolve(to)` resolves it on this page. */
26
26
  resolve(to: RouteLocationRaw): RouteLocationResolved;
@@ -427,7 +427,8 @@ export function linkRoutes(routes, options) {
427
427
  resolve: resolve,
428
428
  install(app) {
429
429
  app.component("RouterLink", RouterLink);
430
- app.config.globalProperties.$route = current;
430
+ app.config.globalProperties.$router = router;
431
+ Object.defineProperty(app.config.globalProperties, "$route", { enumerable: true, get: () => router.currentRoute.value });
431
432
  app.provide(routerKey, router);
432
433
  app.provide(routeLocationKey, reactive);
433
434
  },
package/dist/model.d.ts CHANGED
@@ -82,6 +82,14 @@ export interface Val {
82
82
  args: string[];
83
83
  pieces: Piece[];
84
84
  };
85
+ /** For a string `?:` chooses where a branch is a join: the test the Rust `if` takes and the
86
+ * value of each branch, while `code` is `of`. */
87
+ branches?: {
88
+ of: string;
89
+ test: string;
90
+ yes: Val;
91
+ no: Val;
92
+ };
85
93
  owned?: true;
86
94
  /** For a value of a generated enum, the Rust expression of the enum (an `Option` of it when the
87
95
  * value is optional), where `code` is its string. */
@@ -1,7 +1,7 @@
1
1
  import { absence, distinctFields, enumOf, fail, nothing, rustStr, sameTy, snake } from "../model.js";
2
2
  import { GenError } from "../errors.js";
3
3
  import { ctx } from "../context.js";
4
- import {} from "../constants.js";
4
+ import { exportedConst } from "../constants.js";
5
5
  import { resolveImport } from "../resolve.js";
6
6
  import { bareDates, datePlan, lookupStruct, markHome, readTypeFile, tyOfTs } from "../typescript.js";
7
7
  import { coerce, describeTy, expr, fieldVal } from "../expr.js";
@@ -368,12 +368,11 @@ export const loaders = {
368
368
  if (file === null)
369
369
  return false;
370
370
  readTypeFile(file);
371
- const decls = ctx.constDecls.get(file);
372
371
  for (const sp of st.specifiers) {
373
372
  if (sp.type !== "ImportSpecifier" || sp.importKind === "type")
374
373
  continue;
375
- const d = decls?.get(sp.imported.name ?? sp.imported.value);
376
- if (d?.exported && isLoader(d, file))
374
+ const d = exportedConst(file, sp.imported.name ?? sp.imported.value);
375
+ if (d && isLoader(d, file))
377
376
  scopeOf(loaders, s).locals.set(sp.local.name, { d, file });
378
377
  }
379
378
  return false;
@@ -1,6 +1,6 @@
1
1
  import { absence, blankComponent, camelize, fail, nothing, opt, sameTy, snake } from "../model.js";
2
2
  import { ctx } from "../context.js";
3
- import {} from "../constants.js";
3
+ import { exportedConst } from "../constants.js";
4
4
  import { lookupStruct, markHome, refuseDates, tyOfTs } from "../typescript.js";
5
5
  import { resolveImport } from "../resolve.js";
6
6
  import { describeTy, expr } from "../expr.js";
@@ -8,7 +8,6 @@ import { slotFieldBorrows, slotFieldTy, slotFieldValue } from "../slots.js";
8
8
  import { fieldInit, ownInto } from "../children.js";
9
9
  import { setupSource } from "../script.js";
10
10
  import { header } from "../rust.js";
11
- import { relativePath } from "../paths.js";
12
11
  import { heldStr, isTemporary } from "../strings.js";
13
12
  import { runOf, scopeOf, slotFieldsOf } from "../plugin.js";
14
13
  const REF_CALLS = new Set(["ref", "shallowRef", "computed", "toRef", "customRef", "defineModel"]);
@@ -105,10 +104,11 @@ function keyOf(comp, n, script) {
105
104
  if (n?.type === "Identifier") {
106
105
  const imported = importOf(script, n.name);
107
106
  const file = imported ? resolveImport(comp.file, imported.from) : null;
108
- const decl = file !== null ? ctx.constDecls.get(file)?.get(imported.name) : undefined;
109
- if (decl?.exported && decl.node.type === "VariableDeclarator" && symbolInit(decl.node.init)) {
110
- const rel = relativePath(ctx.rootDir, file);
111
- return keyNamed(comp, n, `${rel}#${imported.name}`, `\`${imported.name}\``, rel, snake(imported.name), () => declaredOf(decl));
107
+ const decl = file !== null ? exportedConst(file, imported.name) : undefined;
108
+ if (decl?.node.type === "VariableDeclarator" && symbolInit(decl.node.init)) {
109
+ const rel = decl.home.file;
110
+ const own = decl.node.id.name;
111
+ return keyNamed(comp, n, `${rel}#${own}`, `\`${own}\``, rel, snake(own), () => declaredOf(decl));
112
112
  }
113
113
  }
114
114
  return fail(comp, "FV1601", "an injection key is a string literal, or a `Symbol` exported from one of the project's `.ts` files: `export const ThemeKey: InjectionKey<string> = Symbol()`", n);
package/dist/script.js CHANGED
@@ -5,7 +5,7 @@ import { CONFIG_FILE } from "./config.js";
5
5
  import { ctx } from "./context.js";
6
6
  import { definePropsType, readTypeFile, refuseDates, tyOfTs } from "./typescript.js";
7
7
  import { resolveImport } from "./resolve.js";
8
- import { constOfDecl, declareConsts } from "./constants.js";
8
+ import { constOfDecl, declareConsts, exportedConst, exportedFunction } from "./constants.js";
9
9
  import { localTy } from "./rust.js";
10
10
  import { expr, fieldVal, typedBy } from "./expr.js";
11
11
  import { atom, occurrences } from "./parens.js";
@@ -278,29 +278,27 @@ function importedKey(sp) {
278
278
  return sp.imported.name ?? sp.imported.value;
279
279
  return sp.type === "ImportDefaultSpecifier" ? "default" : undefined;
280
280
  }
281
- function importedValue(scope, sp, decls, fns) {
281
+ function importedValue(scope, sp, file) {
282
282
  const key = importedKey(sp);
283
283
  if (key === undefined)
284
284
  return;
285
- const f = fns?.get(key);
286
- if (f?.exported) {
287
- scope.functions.set(sp.local.name, f.fn);
288
- scope.clientOnly.set(sp.local.name, `it is a function imported from ${f.fn.home.file}, which the template and a \`computed\` may call, and only an event handler may name without calling it`);
285
+ const f = exportedFunction(file, key);
286
+ if (f) {
287
+ scope.functions.set(sp.local.name, f);
288
+ scope.clientOnly.set(sp.local.name, `it is a function imported from ${f.home.file}, which the template and a \`computed\` may call, and only an event handler may name without calling it`);
289
289
  return;
290
290
  }
291
- const d = decls.get(key);
292
- if (d?.exported)
291
+ const d = exportedConst(file, key);
292
+ if (d)
293
293
  evaluable(scope, sp.local.name, d, `it is imported from ${d.home.file}, where it is not a constant the compiler evaluates`);
294
294
  }
295
295
  function moduleValues(scope, st, file) {
296
296
  if (st.specifiers.some((sp) => sp.type === "ImportDefaultSpecifier"))
297
297
  readTypeFile(file);
298
- const decls = ctx.constDecls.get(file);
299
- if (decls === undefined)
298
+ if (!ctx.constDecls.has(file))
300
299
  return;
301
- const fns = ctx.tsModules.get(file)?.functions;
302
300
  for (const sp of st.specifiers)
303
- importedValue(scope, sp, decls, fns);
301
+ importedValue(scope, sp, file);
304
302
  }
305
303
  function moduleImport(scope, st, from) {
306
304
  const typeOnly = st.importKind === "type";
package/dist/strings.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Component, type N, type Scope, type Ty, type Val } from "./model.ts";
1
+ import { type Component, type N, type Piece, type Scope, type Ty, type Val } from "./model.ts";
2
2
  import type { ErrorCode } from "./errors.ts";
3
3
  /** \`/pattern/flags.test(s)\` of a regular expression literal. */
4
4
  export declare function regexTest(s: Scope, re: N, args: N[], n: N): Val;
@@ -25,6 +25,8 @@ export declare function stringOf(comp: Component, v: Val, n: N, what: string, co
25
25
  /** Whether every character `ty` may write as text is ASCII: a number, a boolean, an absent value
26
26
  * (`undefined`, `null`), a string known to be ASCII, or a list of them. */
27
27
  export declare function writesAscii(ty: Ty): boolean;
28
+ /** The pieces of a string `format!` builds, while its code is that `format!`. */
29
+ export declare function piecesOf(v: Val): Piece[] | undefined;
28
30
  export declare function formatted(comp: Component, given: (string | Val)[], n: N, what: string): Val;
29
31
  type Surrogates = "none" | "all" | "some";
30
32
  interface RegexPiece {
package/dist/strings.js CHANGED
@@ -190,6 +190,13 @@ export function writesAscii(ty) {
190
190
  function mayBeEmpty(v) {
191
191
  return v.ty.k === "str" && !nonEmptyLiteral(v);
192
192
  }
193
+ /** The pieces of a string `format!` builds, while its code is that `format!`. */
194
+ export function piecesOf(v) {
195
+ const f = v.format;
196
+ if (v.ty.k !== "str" || f === undefined || v.code !== `&*format!(${rustStr(f.text)}, ${f.args.join(", ")})`)
197
+ return undefined;
198
+ return f.pieces;
199
+ }
193
200
  export function formatted(comp, given, n, what) {
194
201
  let fmt = "";
195
202
  const args = [];
@@ -9,7 +9,7 @@ import { CONFIG_FILE, TYPES_MODULE } from "./config.js";
9
9
  import { ctx, INLINE_HTML } from "./context.js";
10
10
  import { childOf } from "./expr.js";
11
11
  import { claim } from "./plugin.js";
12
- import { declareConsts, enumType } from "./constants.js";
12
+ import { declareConsts, enumType, exportsOf } from "./constants.js";
13
13
  import { parseTs } from "./files.js";
14
14
  import { declareFunctions } from "./functions.js";
15
15
  import { refuseUnresolved, resolveImport } from "./resolve.js";
@@ -360,7 +360,7 @@ export function readTypeFile(file) {
360
360
  tagAst(body, "source");
361
361
  typesImports(home, body);
362
362
  ctx.constDecls.set(file, declareConsts(home, body));
363
- ctx.tsModules.set(file, { home, body, functions: declareFunctions(home, body, file) });
363
+ ctx.tsModules.set(file, { home, body, functions: declareFunctions(home, body, file), exports: exportsOf(rel, body) });
364
364
  for (const d of declareTypes(home, body, new Map(), ctx.typeAliases)) {
365
365
  const known = ctx.typeDecls.get(d.name);
366
366
  if (known === undefined && ctx.typeStructs.has(d.name))
@@ -378,7 +378,8 @@ export function readTypeFile(file) {
378
378
  ctx.typeFiles.set(d.name, rel);
379
379
  }
380
380
  for (const st of body) {
381
- if (st.type !== "ImportDeclaration" || st.source.value === TYPES_MODULE || st.source.value.endsWith(".vue"))
381
+ const forwards = (st.type === "ExportNamedDeclaration" && st.source) || st.type === "ExportAllDeclaration";
382
+ if ((st.type !== "ImportDeclaration" && !forwards) || st.source.value === TYPES_MODULE || st.source.value.endsWith(".vue"))
382
383
  continue;
383
384
  const imported = resolveImport(rel, st.source.value);
384
385
  if (imported !== null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ferrovue",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Vue templates compiled to Rust render functions: server-side rendering with no JavaScript at run time",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": {