ferrovue 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +32 -13
  2. package/dist/attrs.d.ts +2 -52
  3. package/dist/attrs.js +18 -448
  4. package/dist/calls.d.ts +4 -0
  5. package/dist/calls.js +219 -0
  6. package/dist/children.d.ts +9 -0
  7. package/dist/children.js +273 -0
  8. package/dist/classes.d.ts +13 -0
  9. package/dist/classes.js +176 -0
  10. package/dist/cli.js +0 -17
  11. package/dist/client.js +0 -12
  12. package/dist/compiler.js +25 -90
  13. package/dist/component.js +10 -86
  14. package/dist/context.d.ts +4 -66
  15. package/dist/context.js +2 -33
  16. package/dist/diff.d.ts +0 -3
  17. package/dist/diff.js +0 -9
  18. package/dist/emitter.d.ts +0 -8
  19. package/dist/emitter.js +0 -12
  20. package/dist/expr.d.ts +0 -70
  21. package/dist/expr.js +27 -890
  22. package/dist/fallthrough.d.ts +9 -0
  23. package/dist/fallthrough.js +78 -0
  24. package/dist/fixture.d.ts +2 -1
  25. package/dist/fixture.js +37 -22
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +1 -0
  28. package/dist/islands.d.ts +1 -0
  29. package/dist/islands.js +1 -0
  30. package/dist/lists.d.ts +0 -11
  31. package/dist/lists.js +4 -39
  32. package/dist/loops.d.ts +3 -0
  33. package/dist/loops.js +164 -0
  34. package/dist/model.d.ts +14 -143
  35. package/dist/model.js +27 -12
  36. package/dist/narrowing.d.ts +19 -0
  37. package/dist/narrowing.js +126 -0
  38. package/dist/numbers.d.ts +8 -0
  39. package/dist/numbers.js +73 -0
  40. package/dist/parens.d.ts +0 -27
  41. package/dist/parens.js +0 -44
  42. package/dist/plugin.d.ts +84 -0
  43. package/dist/plugin.js +26 -0
  44. package/dist/plugins/i18n.d.ts +23 -0
  45. package/dist/{i18n.js → plugins/i18n.js} +76 -19
  46. package/dist/plugins/index.d.ts +2 -0
  47. package/dist/plugins/index.js +8 -0
  48. package/dist/plugins/router-link.d.ts +8 -0
  49. package/dist/{router.js → plugins/router-link.js} +19 -61
  50. package/dist/plugins/router.d.ts +42 -0
  51. package/dist/plugins/router.js +256 -0
  52. package/dist/plugins/scoped.d.ts +12 -0
  53. package/dist/plugins/scoped.js +125 -0
  54. package/dist/plugins/shared-types.d.ts +2 -0
  55. package/dist/plugins/shared-types.js +7 -0
  56. package/dist/plugins/stores.d.ts +37 -0
  57. package/dist/plugins/stores.js +377 -0
  58. package/dist/plugins/teleport.d.ts +5 -0
  59. package/dist/plugins/teleport.js +55 -0
  60. package/dist/rust.d.ts +1 -7
  61. package/dist/rust.js +38 -160
  62. package/dist/script.d.ts +0 -6
  63. package/dist/script.js +13 -128
  64. package/dist/slots.d.ts +13 -0
  65. package/dist/slots.js +160 -0
  66. package/dist/strings.d.ts +13 -0
  67. package/dist/strings.js +208 -0
  68. package/dist/styles.d.ts +14 -0
  69. package/dist/styles.js +181 -0
  70. package/dist/template.d.ts +1 -42
  71. package/dist/template.js +14 -738
  72. package/dist/testing.d.ts +1 -0
  73. package/dist/testing.js +1 -8
  74. package/dist/types.d.ts +2 -0
  75. package/dist/typescript.d.ts +0 -13
  76. package/dist/typescript.js +8 -41
  77. package/dist/vite.d.ts +1 -0
  78. package/dist/vite.js +1 -23
  79. package/package.json +7 -2
  80. package/dist/i18n.d.ts +0 -27
  81. package/dist/router.d.ts +0 -17
  82. package/dist/scoped.d.ts +0 -24
  83. package/dist/scoped.js +0 -177
  84. package/dist/stores.d.ts +0 -6
  85. package/dist/stores.js +0 -229
@@ -0,0 +1,9 @@
1
+ import { type Component, type N } from "./model.ts";
2
+ export declare function isDollarAttrs(n: N, bindings: Set<string>): boolean;
3
+ export declare function passedKey(child: Component, key: string): boolean;
4
+ export declare function attrsFlow(read: {
5
+ comp: Component;
6
+ ssr: string;
7
+ children: Map<string, string>;
8
+ attrsBindings: Set<string>;
9
+ }[]): void;
@@ -0,0 +1,78 @@
1
+ import { parse as parseJs } from "@babel/parser";
2
+ import { declares } from "./model.js";
3
+ export function isDollarAttrs(n, bindings) {
4
+ if (n?.type !== "MemberExpression" || n.object.type !== "Identifier")
5
+ return false;
6
+ const prop = n.computed ? n.property.value : n.property.name;
7
+ if (n.object.name === "_ctx")
8
+ return prop === "$attrs";
9
+ return n.object.name === "$setup" && prop !== undefined && bindings.has(prop);
10
+ }
11
+ export function passedKey(child, key) {
12
+ if (["", "key", "ref", "ref_for", "ref_key", "innerHTML", "textContent"].includes(key) || /^on[^a-z]/.test(key))
13
+ return false;
14
+ return !(key.endsWith("Modifiers") && declares(child, key === "modelModifiers" ? "modelValue" : key.slice(0, -"Modifiers".length)));
15
+ }
16
+ export function attrsFlow(read) {
17
+ const byName = new Map(read.map((r) => [r.comp.name, r.comp]));
18
+ const flows = read.map((r) => {
19
+ const calls = [];
20
+ const self = new Set();
21
+ const walk = (n) => {
22
+ if (!n || typeof n !== "object")
23
+ return;
24
+ if (Array.isArray(n)) {
25
+ n.forEach(walk);
26
+ return;
27
+ }
28
+ if (n.type === "VariableDeclarator" && n.init?.type === "CallExpression" && n.init.callee.name === "_resolveComponent") {
29
+ if (n.init.arguments[0]?.value === r.comp.name && n.init.arguments[1])
30
+ self.add(n.id.name);
31
+ }
32
+ if (n.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === "_ssrRenderComponent") {
33
+ const target = n.arguments[0];
34
+ const child = target?.type === "MemberExpression" && target.object.name === "$setup"
35
+ ? r.children.get(target.computed ? target.property.value : target.property.name)
36
+ : target?.type === "Identifier" && self.has(target.name)
37
+ ? r.comp.name
38
+ : undefined;
39
+ const props = n.arguments[1];
40
+ const parts = props?.type === "CallExpression" && props.callee.name === "_mergeProps" ? props.arguments : props ? [props] : [];
41
+ if (child) {
42
+ calls.push({
43
+ child,
44
+ keys: parts.filter((p) => p.type === "ObjectExpression").flatMap((p) => p.properties.filter((q) => q.type === "ObjectProperty" && !q.computed).map((q) => q.key.name ?? String(q.key.value))),
45
+ passesAttrs: parts.some((p) => p.type === "Identifier" && p.name === "_attrs"),
46
+ passesDollar: parts.some((p) => isDollarAttrs(p, r.attrsBindings)),
47
+ });
48
+ }
49
+ }
50
+ for (const [key, v] of Object.entries(n))
51
+ if (key !== "loc" && key !== "start" && key !== "end")
52
+ walk(v);
53
+ };
54
+ walk(parseJs(r.ssr, { sourceType: "module" }).program);
55
+ return { comp: r.comp, calls };
56
+ });
57
+ for (let changed = true; changed;) {
58
+ changed = false;
59
+ for (const f of flows) {
60
+ for (const c of f.calls) {
61
+ const child = byName.get(c.child);
62
+ if (!child)
63
+ continue;
64
+ const names = c.keys.filter((k) => passedKey(child, k));
65
+ if ((c.passesAttrs && f.comp.inheritAttrs) || c.passesDollar)
66
+ names.push(...f.comp.attrNames);
67
+ for (const name of names) {
68
+ if (declares(child, name) || child.attrNames.has(name))
69
+ continue;
70
+ child.attrNames.add(name);
71
+ changed = true;
72
+ }
73
+ if (c.passesAttrs && f.comp.inherits && !child.idsInAttrs)
74
+ child.idsInAttrs = changed = true;
75
+ }
76
+ }
77
+ }
78
+ }
package/dist/fixture.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type App, type Component } from "vue";
2
- import { type RouteRecordRaw } from "vue-router";
2
+ import type { RouteRecordRaw } from "vue-router";
3
+ /** A conformance fixture: the props, and `$slots`, `$route`, `$stores` and `$locale`. */
3
4
  export interface Fixture {
4
5
  props: Record<string, unknown>;
5
6
  slots: Record<string, string>;
package/dist/fixture.js CHANGED
@@ -1,20 +1,4 @@
1
- /* An app rendering one conformance fixture with the real Vue: its props, its slots' content, the
2
- * location it is rendered at under the real vue-router, Pinia's state and vue-i18n's locale.
3
- *
4
- * A fixture is a JSON object of props, plus keys that are not props:
5
- * - `$slots`: each slot's content as HTML, rendered as a static node so both sides write it
6
- * exactly as given; `routerView` is the page `<RouterView>` shows.
7
- * - `$route`: the reader's location, `/` when absent.
8
- * - `$stores`: Pinia's state, by store id — every store the component reads, in full, because a
9
- * store absent here is built from its own `state()`, which the server never runs.
10
- * - `$locale`: the locale, when the project translates.
11
- *
12
- * Nothing here reads a file or compiles anything, so a browser bundle can import it too: the
13
- * browser hydration tests (`packages/ferrovue/browser/`) build the same app there. */
14
1
  import { createApp, createSSRApp, createStaticVNode, defineComponent, h } from "vue";
15
- import { createPinia } from "pinia";
16
- import { createMemoryHistory, createRouter } from "vue-router";
17
- import { createI18n } from "vue-i18n";
18
2
  /** A fixture's JSON, split into the props and the rest. */
19
3
  export function readFixture(json) {
20
4
  const { $slots, $route, $stores, $locale, ...props } = json;
@@ -26,13 +10,41 @@ export function readFixture(json) {
26
10
  locale: $locale ?? null,
27
11
  };
28
12
  }
29
- /** The top-level nodes in a piece of HTML, which a static node needs to hydrate. */
30
13
  function nodeCount(html) {
31
14
  const t = document.createElement("template");
32
15
  t.innerHTML = html;
33
16
  return t.content.childNodes.length;
34
17
  }
35
18
  const staticNode = (html) => createStaticVNode(html, nodeCount(html));
19
+ function isMissing(error, name) {
20
+ const { code, message } = (error ?? {});
21
+ return ((code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") &&
22
+ typeof message === "string" &&
23
+ (message.includes(`'${name}'`) || message.includes(`"${name}"`)));
24
+ }
25
+ function missingPeer(name, needs, cause) {
26
+ return new Error(`ferrovue/testing: ${needs}, which needs \`${name}\`: install it in your project (\`npm install -D ${name}\`)`, { cause });
27
+ }
28
+ async function peer(name, needs, load) {
29
+ try {
30
+ return await load();
31
+ }
32
+ catch (error) {
33
+ throw isMissing(error, name) ? missingPeer(name, needs, error) : error;
34
+ }
35
+ }
36
+ async function optionalPinia(stores) {
37
+ try {
38
+ return await import("pinia");
39
+ }
40
+ catch (error) {
41
+ if (!isMissing(error, "pinia"))
42
+ throw error;
43
+ if (Object.keys(stores).length > 0)
44
+ throw missingPeer("pinia", "the fixture has `$stores`", error);
45
+ return null;
46
+ }
47
+ }
36
48
  /** Route records for vue-router, every route — nested ones too — given the fixture's view. */
37
49
  export function routeRecords(routes, View) {
38
50
  return routes.map((r) => typeof r === "string"
@@ -45,12 +57,14 @@ export async function fixtureApp(component, fixture, routes, options = {}) {
45
57
  .filter(([name]) => name !== "routerView")
46
58
  .map(([name, html]) => [name, () => [staticNode(html)]]));
47
59
  const app = (options.client ? createApp : createSSRApp)({ render: () => h(component, fixture.props, slots) });
48
- // As the client hydrates: the state the server rendered with, set before any store is first used.
49
- const pinia = createPinia();
50
- pinia.state.value = structuredClone(fixture.stores);
51
- app.use(pinia);
60
+ const pinia = await optionalPinia(fixture.stores);
61
+ if (pinia) {
62
+ const store = pinia.createPinia();
63
+ store.state.value = structuredClone(fixture.stores);
64
+ app.use(store);
65
+ }
52
66
  if (options.i18n) {
53
- // Messages read from the project's files at run time: their schema is not known to TypeScript.
67
+ const { createI18n } = await peer("vue-i18n", "the fixture is rendered with `i18n` options", () => import("vue-i18n"));
54
68
  const i18nOptions = {
55
69
  legacy: false,
56
70
  locale: fixture.locale ?? options.i18n.locale,
@@ -62,6 +76,7 @@ export async function fixtureApp(component, fixture, routes, options = {}) {
62
76
  app.use(createI18n(i18nOptions));
63
77
  }
64
78
  if (routes) {
79
+ const { createMemoryHistory, createRouter } = await peer("vue-router", "the fixture is rendered with routes", () => import("vue-router"));
65
80
  const view = fixture.slots.routerView ?? "";
66
81
  const View = defineComponent({ render: () => staticNode(view) });
67
82
  const router = createRouter({
@@ -0,0 +1,2 @@
1
+ export { hydrateState, mountIslands, type IslandComponent, type Islands, type MountOptions } from "./client.ts";
2
+ export type { Float, TrustedHtml } from "./types.ts";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { hydrateState, mountIslands } from "./client.js";
package/dist/islands.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Component } from "vue";
2
+ /** Every component that has an `island()`, by its `data-island` name, each a loader for `mountIslands`. */
2
3
  declare const islands: Record<string, () => Promise<{
3
4
  default: Component;
4
5
  }>>;
package/dist/islands.js CHANGED
@@ -1,3 +1,4 @@
1
+ /** Every component that has an `island()`, by its `data-island` name, each a loader for `mountIslands`. */
1
2
  const islands = (() => {
2
3
  throw new Error("ferrovue/islands is written by the Vite plugin: add `ferrovue()` from `ferrovue/vite` to the plugins of the Vite config");
3
4
  })();
package/dist/lists.d.ts CHANGED
@@ -1,19 +1,8 @@
1
1
  import { type N, type Scope, type Ty, type Val } from "./model.ts";
2
- /** A list as an iterator of its items, in the form a computed list holds them. */
3
2
  export declare function items(v: Val): string;
4
- /** A computed list: its iterator, and its items collected as `code` for anything that needs a
5
- * `Vec`. */
6
3
  export declare function computed(iter: string, of: Ty, lone?: boolean): Val;
7
- /** A list of any kind as the `Vec` of items a computed list collects: what a branch of `?:` or a
8
- * setup `let` holds, so that both branches have one type. Kept beyond the statement, its strings
9
- * own their text, which may live in a temporary (`name.toUpperCase().split(" ")`). */
10
4
  export declare function collected(v: Val): string;
11
- /** The computed list a setup `let` named `name` holds, read again by item. */
12
5
  export declare function heldList(name: string, of: Ty, lone?: boolean): Val;
13
- /** A list method other than `includes` and `join`: `null` when `method` is not one of them. */
14
6
  export declare function listMethod(s: Scope, target: Val, method: string, args: N[], n: N): Val | null;
15
- /** `Object.keys(r)`, `Object.values(r)`, and `Object.entries(r)`, which is only a `v-for`'s source. */
16
7
  export declare function objectCall(s: Scope, method: string, args: N[], n: N): Val;
17
- /** `list.includes(x)` and `list.join(sep)` of a computed list, or of a list of fractions or
18
- * booleans. */
19
8
  export declare function computedListMethod(s: Scope, target: Val, method: string, args: N[], n: N): Val | null;
package/dist/lists.js CHANGED
@@ -1,16 +1,11 @@
1
- /* Lists a template computes: array methods taking arrow functions (`filter`, `map`, `some`,
2
- * `every`, `find`, `findIndex`), `slice`, and `Object.keys` / `values` of a record.
3
- *
4
- * A computed list is an iterator of its items, each in one form whatever list it came from: a
5
- * string as a `Cow<str>` (borrowed from the props, or owned when `map` made it), a number or a
6
- * boolean copied, an object borrowed. That is what lets one method's result feed the next, a
7
- * `v-for`, `.join` or `.length`, without knowing where it came from. */
8
1
  import { BOOL, fail, FLOAT, INT, opt, sameTy, STR } from "./model.js";
9
2
  import { ctx } from "./context.js";
10
- import { asCow, yieldsCow, asF64, cond, expr, fieldVal, isNumber, isTemporary, lonely, meet } from "./expr.js";
3
+ import { expr, fieldVal } from "./expr.js";
4
+ import { cond } from "./narrowing.js";
5
+ import { asF64, isNumber } from "./numbers.js";
6
+ import { asCow, isTemporary, lonely, meet, yieldsCow } from "./strings.js";
11
7
  import { atom, operand, strArg, UNARY } from "./parens.js";
12
8
  const COW = "std::borrow::Cow::<str>";
13
- /** The type of a list's items, which must be one these methods can pass along. */
14
9
  function itemsTy(s, v, n) {
15
10
  if (v.ty.k !== "list")
16
11
  return fail(s.comp, "not a list", n);
@@ -20,45 +15,32 @@ function itemsTy(s, v, n) {
20
15
  }
21
16
  return of;
22
17
  }
23
- /** A list as an iterator of its items, in the form a computed list holds them. */
24
18
  export function items(v) {
25
19
  if (v.iter !== undefined)
26
20
  return v.iter;
27
21
  const of = v.ty.k === "list" || v.ty.k === "record" ? v.ty.of : v.ty;
28
- // A list of strings holds `Cow`s, an array literal `&str`s: either way `&**v` is the `&str`.
29
22
  if (of.k === "str")
30
23
  return `${atom(v.code)}.iter().map(|v| ${COW}::Borrowed(&**v))`;
31
24
  if (of.k === "struct" || of.k === "child")
32
25
  return `${atom(v.code)}.iter()`;
33
26
  return `${atom(v.code)}.iter().copied()`;
34
27
  }
35
- /** A computed list: its iterator, and its items collected as `code` for anything that needs a
36
- * `Vec`. */
37
28
  export function computed(iter, of, lone) {
38
29
  return { code: `${iter}.collect::<Vec<_>>()`, ty: { k: "list", of }, iter, ...(lone ? { lone } : {}) };
39
30
  }
40
- /** A list of any kind as the `Vec` of items a computed list collects: what a branch of `?:` or a
41
- * setup `let` holds, so that both branches have one type. Kept beyond the statement, its strings
42
- * own their text, which may live in a temporary (`name.toUpperCase().split(" ")`). */
43
31
  export function collected(v) {
44
32
  const of = v.ty.k === "list" ? v.ty.of : v.ty;
45
33
  return `${items(v)}${of.k === "str" ? `.map(|v| ${COW}::Owned(v.into_owned()))` : ""}.collect::<Vec<_>>()`;
46
34
  }
47
- /** The computed list a setup `let` named `name` holds, read again by item. */
48
35
  export function heldList(name, of, lone) {
49
36
  const iter = of.k === "str" ? `${name}.iter().map(|v| ${COW}::Borrowed(&**v))` : `${name}.iter().copied()`;
50
37
  return { code: name, ty: { k: "list", of }, iter, ...(lone ? { lone } : {}) };
51
38
  }
52
- /** An item as the closure that a method hands it to sees it: `byRef` for `filter` and `find`, which
53
- * pass `&item`. */
54
39
  function itemVal(of, param, byRef, lone) {
55
- // An object's fields are read through the reference; a number or a boolean is copied out of it.
56
40
  const scalar = of.k === "int" || of.k === "float" || of.k === "bool";
57
41
  const code = of.k === "str" ? (byRef ? `&**${param}` : `&*${param}`) : byRef && scalar ? `*${param}` : param;
58
42
  return { code, ty: of, ...(lone ? { lone } : {}) };
59
43
  }
60
- /** An arrow function's body, translated with its parameters bound — the item, and the index when it
61
- * names one — as the closure a method takes. `enumerate` says the items must come numbered. */
62
44
  function arrow(s, fn, list, of, byRef, method, body) {
63
45
  const comp = s.comp;
64
46
  if (fn?.type !== "ArrowFunctionExpression" || fn.async || fn.params.length > 2) {
@@ -70,7 +52,6 @@ function arrow(s, fn, list, of, byRef, method, body) {
70
52
  if (index && index.type !== "Identifier")
71
53
  fail(comp, "an arrow function's index is a plain name", index);
72
54
  const n = ++ctx.narrowCount;
73
- // A string item is named apart: `map` cannot hand back a borrow of it (see `mapped`).
74
55
  const p = of.k === "str" ? `fv_s${n}` : `fv_a${n}`;
75
56
  const i = `fv_i${n}`;
76
57
  const v = itemVal(of, p, byRef, list.lone);
@@ -81,7 +62,6 @@ function arrow(s, fn, list, of, byRef, method, body) {
81
62
  bound.push(item.name);
82
63
  }
83
64
  else if (item?.type === "ObjectPattern" && (of.k === "struct" || of.k === "child")) {
84
- // `({ id, label }) => …`: each name is that field of the item.
85
65
  for (const prop of item.properties) {
86
66
  if (prop.type !== "ObjectProperty" || prop.computed || prop.value.type !== "Identifier") {
87
67
  fail(comp, "a destructured arrow function parameter binds plain names, without defaults", prop);
@@ -96,7 +76,6 @@ function arrow(s, fn, list, of, byRef, method, body) {
96
76
  locals.set(index.name, { code: i, ty: INT });
97
77
  bound.push(index.name);
98
78
  }
99
- // The parameters hide whatever an outer `v-if` narrowed under the same names.
100
79
  const narrowed = new Map([...s.narrowed].filter(([path]) => !bound.some((b) => path === b || path.startsWith(`${b}.`))));
101
80
  const code = body({ ...s, locals, narrowed });
102
81
  const reads = (name) => new RegExp(`\\b${name}\\b`).test(code);
@@ -112,15 +91,11 @@ function arrow(s, fn, list, of, byRef, method, body) {
112
91
  }
113
92
  return { closure: `|(${reads(i) ? i : "_"}, ${pName})| { ${lets}${code} }`, enumerate: true };
114
93
  }
115
- /** What `map` makes of each item: a string as a `Cow` — owned when it was built, or borrows the
116
- * item, which the closure owns — a number or a boolean as it is, an object borrowed. */
117
94
  function mapped(s, v, n) {
118
95
  switch (v.ty.k) {
119
96
  case "str":
120
- // A routine's `Cow` may borrow the item, which the closure owns: owned.
121
97
  if (v.code.startsWith("&*fv::") && yieldsCow(v.code.slice(2)))
122
98
  return `${COW}::Owned(${v.code.slice(2)}.into_owned())`;
123
- // An item itself, of this list or one outside it, which the closure only borrows: copied.
124
99
  const item = /^&\**(fv_s\d+)$/.exec(v.code)?.[1];
125
100
  if (item !== undefined)
126
101
  return `${COW}::Owned(${item}.to_string())`;
@@ -138,7 +113,6 @@ function mapped(s, v, n) {
138
113
  return fail(s.comp, `\`.map()\` makes a list of strings, numbers, booleans or objects, not ${v.ty.k === "opt" ? "optional values" : "these"}`, n);
139
114
  }
140
115
  }
141
- /** A list method other than `includes` and `join`: `null` when `method` is not one of them. */
142
116
  export function listMethod(s, target, method, args, n) {
143
117
  const comp = s.comp;
144
118
  if (!["filter", "map", "some", "every", "find", "findIndex", "slice"].includes(method))
@@ -172,8 +146,6 @@ export function listMethod(s, target, method, args, n) {
172
146
  let out;
173
147
  const { closure, enumerate } = arrow(s, fn, target, of, false, method, (inner) => {
174
148
  out = expr(inner, fn.body);
175
- // An integer negated, multiplied or taken modulo may be JavaScript's -0, which a list of
176
- // `i64` loses: a list of fractions then. (Generated code spaces a binary `-`, never a unary one.)
177
149
  if (out.ty.k === "int" && out.f64 !== undefined && /-[\w(]|\s[*%]\s/.test(out.f64))
178
150
  out = { ...out, code: out.f64, ty: FLOAT };
179
151
  return mapped(inner, out, fn.body);
@@ -189,8 +161,6 @@ export function listMethod(s, target, method, args, n) {
189
161
  const { closure, enumerate } = arrow(s, fn, target, of, true, method, (inner) => cond(inner, fn.body));
190
162
  const found = enumerate ? `${it}.enumerate().find(${closure}).map(|(_, v)| v)` : `${it}.find(${closure})`;
191
163
  const lone = target.lone ? { lone: true } : {};
192
- // A string found is a `Cow` the statement holds, read as the `&str` an optional string is; one
193
- // kept beyond the statement owns its text, which may live in a temporary.
194
164
  if (of.k === "str")
195
165
  return { code: `${atom(found)}.as_deref()`, ty: opt(STR), held: `${found}.map(|v| ${COW}::Owned(v.into_owned()))`, ...lone };
196
166
  return { code: found, ty: opt(of), ...lone };
@@ -201,7 +171,6 @@ export function listMethod(s, target, method, args, n) {
201
171
  }
202
172
  }
203
173
  }
204
- /** `Object.keys(r)`, `Object.values(r)`, and `Object.entries(r)`, which is only a `v-for`'s source. */
205
174
  export function objectCall(s, method, args, n) {
206
175
  const comp = s.comp;
207
176
  const r = args.length === 1 ? expr(s, args[0]) : null;
@@ -220,8 +189,6 @@ export function objectCall(s, method, args, n) {
220
189
  return fail(comp, "`Object.entries()` is supported as the source of a `v-for`: `([key, value], i) in Object.entries(r)`", n);
221
190
  return fail(comp, `\`Object.${method}()\` is not supported: \`keys\`, \`values\` and \`entries\` of a record are`, n);
222
191
  }
223
- /** `list.includes(x)` and `list.join(sep)` of a computed list, or of a list of fractions or
224
- * booleans. */
225
192
  export function computedListMethod(s, target, method, args, n) {
226
193
  if (target.ty.k !== "list")
227
194
  return null;
@@ -232,7 +199,6 @@ export function computedListMethod(s, target, method, args, n) {
232
199
  fail(s.comp, "`.includes()` looks for a value of the list's own type", args[0]);
233
200
  if (of.k === "str")
234
201
  meet(s.comp, target, x, "`.includes()`", n, "equal");
235
- // `includes` finds `NaN`, which `==` never equals.
236
202
  const test = of.k === "str" ? `*v == ${x.code.startsWith("&*") ? `*${x.code.slice(2)}` : `*${operand(x.code, UNARY)}`}` : of.k === "float" ? `v == ${x.code} || (v.is_nan() && ${atom(x.code)}.is_nan())` : `v == ${x.code}`;
237
203
  return { code: `${items(target)}.any(|v| ${test})`, ty: BOOL };
238
204
  }
@@ -242,7 +208,6 @@ export function computedListMethod(s, target, method, args, n) {
242
208
  const sep = args.length ? expr(s, args[0]) : { code: '","', ty: STR };
243
209
  if (sep.ty.k !== "str")
244
210
  fail(s.comp, "`.join()` takes a string", args[0]);
245
- // Two halves of a pair would meet where items are joined without a separator between them.
246
211
  const parted = /^"[^"]/.test(sep.code);
247
212
  if (of.k === "str" && target.lone && (!parted || sep.lone))
248
213
  fail(s.comp, lonely("`.join()` with no literal separator"), n);
@@ -0,0 +1,3 @@
1
+ import { type N, type Scope } from "./model.ts";
2
+ import { Emitter } from "./emitter.ts";
3
+ export declare function list(s: Scope, e: Emitter, c: N): void;
package/dist/loops.js ADDED
@@ -0,0 +1,164 @@
1
+ import { fail, INT, snake, STR } from "./model.js";
2
+ import { ctx } from "./context.js";
3
+ import { expr, fieldVal } from "./expr.js";
4
+ import { isObjectCall } from "./calls.js";
5
+ import { atom, operand, OR } from "./parens.js";
6
+ import { Emitter } from "./emitter.js";
7
+ import { statements } from "./template.js";
8
+ export function list(s, e, c) {
9
+ const fn = c.arguments[1];
10
+ if (fn.type !== "ArrowFunctionExpression" || fn.body.type !== "BlockStatement") {
11
+ fail(s.comp, "unexpected `ssrRenderList` callback", c);
12
+ }
13
+ if (isObjectCall(c.arguments[0], "entries")) {
14
+ const [pair, index] = fn.params;
15
+ const r = c.arguments[0].arguments.length === 1 ? expr(s, c.arguments[0].arguments[0]) : null;
16
+ if (r?.ty.k !== "record")
17
+ return fail(s.comp, "`Object.entries()` takes a `Record<string, T>`", c.arguments[0]);
18
+ if (pair?.type !== "ArrayPattern" || pair.elements.length > 2 || pair.elements.some((x) => x?.type !== "Identifier")) {
19
+ return fail(s.comp, "a `v-for` over `Object.entries()` names its items `[key, value]`", pair ?? c);
20
+ }
21
+ recordLoop(s, e, r, fn, pair.elements[1], pair.elements[0], index);
22
+ return;
23
+ }
24
+ const src = expr(s, c.arguments[0]);
25
+ if (src.ty.k === "record") {
26
+ const [value, key, index] = fn.params;
27
+ recordLoop(s, e, src, fn, value, key, index);
28
+ return;
29
+ }
30
+ const [item, index] = fn.params;
31
+ const idx = index ? snake(index.name) : null;
32
+ const itemName = item.type === "Identifier" ? snake(item.name) : `fv_item${++ctx.narrowCount}`;
33
+ const inner = new Map(s.locals);
34
+ let of;
35
+ let walk;
36
+ let bound;
37
+ let itemLet = -1;
38
+ const loop = (it, i) => (i ? `for (${i}, ${it}) in ${atom(walk)}.enumerate()` : `for ${it} in ${walk}`);
39
+ if (src.ty.k === "list" && src.iter !== undefined) {
40
+ of = src.ty.of;
41
+ walk = src.iter;
42
+ bound = of.k === "str" ? `${itemName}_cow` : itemName;
43
+ e.open(loop(bound, idx));
44
+ if (of.k === "str") {
45
+ e.stmt(`let ${itemName}: &str = &${bound};`);
46
+ itemLet = e.lines.length - 1;
47
+ }
48
+ }
49
+ else if (src.ty.k === "int") {
50
+ of = INT;
51
+ walk = `1..=${operand(src.code, OR)}`;
52
+ bound = itemName;
53
+ e.open(loop(bound, idx));
54
+ }
55
+ else if (src.ty.k === "list") {
56
+ of = src.ty.of;
57
+ if (of.k === "undef")
58
+ fail(s.comp, "`v-for` over an empty array literal", c);
59
+ const itemCode = `${itemName}_ref`;
60
+ walk = `${atom(src.code)}.iter()`;
61
+ bound = itemCode;
62
+ e.open(loop(bound, idx));
63
+ if (of.k === "str")
64
+ e.stmt(`let ${itemName}: &str = ${itemCode};`);
65
+ else if (of.k === "struct" || of.k === "child")
66
+ e.stmt(`let ${itemName} = ${itemCode};`);
67
+ else
68
+ e.stmt(`let ${itemName} = *${itemName}_ref;`);
69
+ itemLet = e.lines.length - 1;
70
+ }
71
+ else {
72
+ return fail(s.comp, "`v-for` walks an array, or counts to a number", c);
73
+ }
74
+ let idxLet = -1;
75
+ if (idx) {
76
+ e.stmt(`let ${idx} = ${idx} as i64;`);
77
+ idxLet = e.lines.length - 1;
78
+ }
79
+ const bodyFrom = e.lines.length;
80
+ if (item.type === "Identifier")
81
+ inner.set(item.name, { code: itemName, ty: of, ...(src.lone ? { lone: true } : {}) });
82
+ else if (item.type === "ObjectPattern") {
83
+ for (const p of item.properties) {
84
+ if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
85
+ fail(s.comp, "a destructured `v-for` item binds plain names, without defaults", p);
86
+ }
87
+ inner.set(p.value.name, fieldVal(s.comp, itemName, of, p.key.name ?? p.key.value, p));
88
+ }
89
+ }
90
+ else
91
+ fail(s.comp, "a `v-for` item is a name or an object pattern", item);
92
+ if (index)
93
+ inner.set(index.name, { code: idx, ty: INT });
94
+ const before = e.literalBytes;
95
+ const fromProps = src.ty.k === "list" && src.iter === undefined && /^\(?props\./.test(src.code);
96
+ statements({ ...s, locals: inner, loop: fromProps ? { item: itemName, over: src.code } : undefined }, e, fn.body.body);
97
+ const unusedItem = !e.reads(itemName, bodyFrom);
98
+ const unusedIdx = idx !== null && !e.reads(idx, bodyFrom);
99
+ const head = bodyFrom - 1 - (idx ? 1 : 0) - (itemLet >= 0 ? 1 : 0);
100
+ e.replace(head, loop(bound, idx), loop(unusedItem ? "_" : bound, unusedIdx ? null : idx));
101
+ if (unusedIdx)
102
+ e.lines.splice(idxLet, 1);
103
+ if (unusedItem && itemLet >= 0)
104
+ e.lines.splice(itemLet, 1);
105
+ e.close();
106
+ const body = e.literalBytes - before;
107
+ e.literalBytes = before;
108
+ if (body > 0 && fromProps)
109
+ e.perItem.push(`${body} * ${atom(src.code)}.len()`);
110
+ else if (body > 0 && src.ty.k === "list" && s.loop && src.code.startsWith(`${s.loop.item}.`)) {
111
+ const outer = s.loop;
112
+ e.perItem.push(`${body} * ${atom(outer.over)}.iter().map(|${outer.item}| ${src.code}.len()).sum::<usize>()`);
113
+ }
114
+ }
115
+ function recordLoop(s, e, r, fn, value, key, index) {
116
+ if (r.ty.k !== "record")
117
+ return;
118
+ const of = r.ty.of;
119
+ for (const p of [key, index])
120
+ if (p && p.type !== "Identifier")
121
+ fail(s.comp, "a record's key and index in `v-for` are plain names", p);
122
+ const n = ++ctx.narrowCount;
123
+ const v = value?.type === "Identifier" ? snake(value.name) : `fv_value${n}`;
124
+ const k = key ? snake(key.name) : `fv_key${n}`;
125
+ const i = index ? snake(index.name) : `fv_index${n}`;
126
+ e.open(`for (${i}, (${k}, ${v}_ref)) in ${atom(r.code)}.iter().enumerate()`);
127
+ const head = e.lines.length - 1;
128
+ const indent = /^ */.exec(e.lines[head])[0];
129
+ const inner = new Map(s.locals);
130
+ e.stmt(of.k === "str" ? `let ${v}: &str = ${v}_ref;` : of.k === "int" || of.k === "float" || of.k === "bool" ? `let ${v} = *${v}_ref;` : `let ${v} = ${v}_ref;`);
131
+ e.stmt(`let ${i} = ${i} as i64;`);
132
+ const bodyFrom = e.lines.length;
133
+ if (value?.type === "Identifier")
134
+ inner.set(value.name, { code: v, ty: of });
135
+ else if (value?.type === "ObjectPattern" && (of.k === "struct" || of.k === "child")) {
136
+ for (const p of value.properties) {
137
+ if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
138
+ fail(s.comp, "a destructured `v-for` item binds plain names, without defaults", p);
139
+ }
140
+ inner.set(p.value.name, fieldVal(s.comp, v, of, p.key.name ?? p.key.value, p));
141
+ }
142
+ }
143
+ else if (value)
144
+ fail(s.comp, "a `v-for` item is a name, or an object pattern of an object", value);
145
+ if (key)
146
+ inner.set(key.name, { code: k, ty: STR });
147
+ if (index)
148
+ inner.set(index.name, { code: i, ty: INT });
149
+ const before = e.literalBytes;
150
+ statements({ ...s, locals: inner }, e, fn.body.body);
151
+ const reads = (name) => e.reads(name, bodyFrom);
152
+ const [readsV, readsK, readsI] = [reads(v), reads(k), reads(i)];
153
+ if (!readsI)
154
+ e.lines.splice(head + 2, 1);
155
+ if (!readsV)
156
+ e.lines.splice(head + 1, 1);
157
+ const pair = `(${readsK ? k : "_"}, ${readsV ? `${v}_ref` : "_"})`;
158
+ e.lines[head] = `${indent}for ${readsI ? `(${i}, ${pair}) in ${atom(r.code)}.iter().enumerate()` : `${pair} in ${atom(r.code)}.iter()`} {`;
159
+ e.close();
160
+ const body = e.literalBytes - before;
161
+ e.literalBytes = before;
162
+ if (body > 0 && /^\(?props\./.test(r.code))
163
+ e.perItem.push(`${body} * ${atom(r.code)}.len()`);
164
+ }