ferrovue 0.2.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.
- package/README.md +103 -15
- package/dist/attrs.d.ts +8 -42
- package/dist/attrs.js +148 -297
- package/dist/calls.d.ts +4 -0
- package/dist/calls.js +219 -0
- package/dist/children.d.ts +9 -0
- package/dist/children.js +273 -0
- package/dist/classes.d.ts +13 -0
- package/dist/classes.js +176 -0
- package/dist/cli.js +121 -27
- package/dist/client.d.ts +11 -2
- package/dist/client.js +45 -9
- package/dist/compiler.d.ts +4 -0
- package/dist/compiler.js +37 -90
- package/dist/component.js +19 -82
- package/dist/context.d.ts +6 -68
- package/dist/context.js +25 -38
- package/dist/diff.d.ts +1 -0
- package/dist/diff.js +68 -0
- package/dist/emitter.d.ts +0 -8
- package/dist/emitter.js +0 -12
- package/dist/expr.d.ts +0 -70
- package/dist/expr.js +30 -855
- package/dist/fallthrough.d.ts +9 -0
- package/dist/fallthrough.js +78 -0
- package/dist/fixture.d.ts +2 -1
- package/dist/fixture.js +37 -22
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/islands.d.ts +6 -0
- package/dist/islands.js +5 -0
- package/dist/lists.d.ts +0 -11
- package/dist/lists.js +4 -39
- package/dist/loops.d.ts +3 -0
- package/dist/loops.js +164 -0
- package/dist/model.d.ts +22 -130
- package/dist/model.js +37 -9
- package/dist/narrowing.d.ts +19 -0
- package/dist/narrowing.js +126 -0
- package/dist/numbers.d.ts +8 -0
- package/dist/numbers.js +73 -0
- package/dist/parens.d.ts +0 -27
- package/dist/parens.js +0 -44
- package/dist/plugin.d.ts +84 -0
- package/dist/plugin.js +26 -0
- package/dist/plugins/i18n.d.ts +23 -0
- package/dist/{i18n.js → plugins/i18n.js} +76 -19
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +8 -0
- package/dist/plugins/router-link.d.ts +8 -0
- package/dist/{router.js → plugins/router-link.js} +65 -57
- package/dist/plugins/router.d.ts +42 -0
- package/dist/plugins/router.js +256 -0
- package/dist/plugins/scoped.d.ts +12 -0
- package/dist/{scoped.js → plugins/scoped.js} +55 -20
- package/dist/plugins/shared-types.d.ts +2 -0
- package/dist/plugins/shared-types.js +7 -0
- package/dist/plugins/stores.d.ts +37 -0
- package/dist/plugins/stores.js +377 -0
- package/dist/plugins/teleport.d.ts +5 -0
- package/dist/plugins/teleport.js +55 -0
- package/dist/rust.d.ts +2 -5
- package/dist/rust.js +66 -161
- package/dist/script.d.ts +0 -6
- package/dist/script.js +22 -121
- package/dist/slots.d.ts +13 -0
- package/dist/slots.js +160 -0
- package/dist/strings.d.ts +13 -0
- package/dist/strings.js +208 -0
- package/dist/styles.d.ts +14 -0
- package/dist/styles.js +181 -0
- package/dist/template.d.ts +2 -40
- package/dist/template.js +40 -691
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -8
- package/dist/types.d.ts +2 -0
- package/dist/typescript.d.ts +0 -13
- package/dist/typescript.js +8 -41
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +35 -14
- package/package.json +11 -2
- package/dist/i18n.d.ts +0 -27
- package/dist/router.d.ts +0 -17
- package/dist/scoped.d.ts +0 -10
- package/dist/stores.d.ts +0 -6
- package/dist/stores.js +0 -227
|
@@ -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 {
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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({
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { hydrateState, mountIslands } from "./client.js";
|
package/dist/islands.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Every component that has an `island()`, by its `data-island` name, each a loader for `mountIslands`. */
|
|
2
|
+
const islands = (() => {
|
|
3
|
+
throw new Error("ferrovue/islands is written by the Vite plugin: add `ferrovue()` from `ferrovue/vite` to the plugins of the Vite config");
|
|
4
|
+
})();
|
|
5
|
+
export default islands;
|
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 {
|
|
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);
|
package/dist/loops.d.ts
ADDED
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
|
+
}
|