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,37 @@
|
|
|
1
|
+
import { type Component, type N, type Struct } from "../model.ts";
|
|
2
|
+
import { type Plugin } from "../plugin.ts";
|
|
3
|
+
declare module "../model.ts" {
|
|
4
|
+
interface StructTy {
|
|
5
|
+
store?: true;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface StoreGetter {
|
|
9
|
+
param: string | null;
|
|
10
|
+
body: N;
|
|
11
|
+
file: string;
|
|
12
|
+
setup?: true;
|
|
13
|
+
}
|
|
14
|
+
export interface Store {
|
|
15
|
+
hook: string;
|
|
16
|
+
id: string;
|
|
17
|
+
field: string;
|
|
18
|
+
state: string;
|
|
19
|
+
module: string;
|
|
20
|
+
getters: Map<string, StoreGetter>;
|
|
21
|
+
}
|
|
22
|
+
interface StoresRun {
|
|
23
|
+
dir: string | null;
|
|
24
|
+
stores: Map<string, Store>;
|
|
25
|
+
structs: Map<string, Struct>;
|
|
26
|
+
files: Map<string, string>;
|
|
27
|
+
readers: Set<Component>;
|
|
28
|
+
inProgress: Set<string>;
|
|
29
|
+
}
|
|
30
|
+
interface StoresScope {
|
|
31
|
+
hooks: Map<string, Store>;
|
|
32
|
+
storeToRefs: string | null;
|
|
33
|
+
values: Map<string, Store>;
|
|
34
|
+
}
|
|
35
|
+
export declare function storeHome(file: string): Component;
|
|
36
|
+
export declare const piniaStores: Plugin<StoresRun, StoresScope>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { parse as parseJs } from "@babel/parser";
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { blankComponent, BOOL, fail, FLOAT, GenError, INT, opt, sameTy, snake, STR, tagAst, UNDEF } from "../model.js";
|
|
5
|
+
import { ctx } from "../context.js";
|
|
6
|
+
import { structOf, tyOfTs, typesImports } from "../typescript.js";
|
|
7
|
+
import { patternNames, setupStatement } from "../script.js";
|
|
8
|
+
import { expr, fieldVal } from "../expr.js";
|
|
9
|
+
import { runOf, scopeOf } from "../plugin.js";
|
|
10
|
+
import { header, structSource } from "../rust.js";
|
|
11
|
+
function storeImport(comp, from) {
|
|
12
|
+
const { stores } = runOf(piniaStores);
|
|
13
|
+
if (!from.startsWith(".") || stores.size === 0)
|
|
14
|
+
return null;
|
|
15
|
+
const target = resolve(ctx.rootDir, dirname(comp.file), from).replace(/\.ts$/, "");
|
|
16
|
+
return [...stores.values()].some((s) => s.module === target) ? target : null;
|
|
17
|
+
}
|
|
18
|
+
function readStores(root, dir) {
|
|
19
|
+
const run = runOf(piniaStores);
|
|
20
|
+
const files = readdirSync(join(root, dir)).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts")).toSorted();
|
|
21
|
+
for (const f of files) {
|
|
22
|
+
const path = join(root, dir, f);
|
|
23
|
+
const comp = storeHome(relative(root, path));
|
|
24
|
+
comp.source = readFileSync(path, "utf8");
|
|
25
|
+
const ast = parseJs(comp.source, { sourceType: "module", plugins: ["typescript"] }).program.body;
|
|
26
|
+
tagAst(ast, "source");
|
|
27
|
+
typesImports(comp, ast);
|
|
28
|
+
const decls = ast.map((st) => (st.type === "ExportNamedDeclaration" ? st.declaration : st)).filter(Boolean);
|
|
29
|
+
const interfaces = decls.filter((d) => d.type === "TSInterfaceDeclaration");
|
|
30
|
+
for (const d of interfaces) {
|
|
31
|
+
if (run.structs.has(d.id.name))
|
|
32
|
+
fail(comp, `\`${d.id.name}\` is declared by another store too`, d);
|
|
33
|
+
run.structs.set(d.id.name, { name: d.id.name, fields: [] });
|
|
34
|
+
run.files.set(d.id.name, comp.file);
|
|
35
|
+
}
|
|
36
|
+
for (const d of interfaces) {
|
|
37
|
+
const st = structOf(comp, d.id.name, d.body.body, run.structs);
|
|
38
|
+
for (const field of st.fields)
|
|
39
|
+
field.ty = markStore(field.ty);
|
|
40
|
+
run.structs.set(d.id.name, st);
|
|
41
|
+
}
|
|
42
|
+
for (const d of decls) {
|
|
43
|
+
if (d.type !== "VariableDeclaration")
|
|
44
|
+
continue;
|
|
45
|
+
for (const v of d.declarations) {
|
|
46
|
+
const init = v.init;
|
|
47
|
+
if (init?.type !== "CallExpression" || init.callee.type !== "Identifier" || init.callee.name !== "defineStore")
|
|
48
|
+
continue;
|
|
49
|
+
const [id, options] = init.arguments;
|
|
50
|
+
if (id?.type !== "StringLiteral")
|
|
51
|
+
fail(comp, "a store's id is a string literal", init);
|
|
52
|
+
if (options?.type === "ArrowFunctionExpression" || options?.type === "FunctionExpression") {
|
|
53
|
+
setupStore(comp, v.id.name, id.value, options, path);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (options?.type !== "ObjectExpression")
|
|
57
|
+
fail(comp, "a store is `defineStore(id, { state, getters, actions })` or `defineStore(id, () => { … })`", init);
|
|
58
|
+
const state = options.properties.find((p) => (p.key?.name ?? p.key?.value) === "state");
|
|
59
|
+
const fn = state?.value ?? (state?.type === "ObjectMethod" ? state : null);
|
|
60
|
+
const ret = fn?.returnType?.typeAnnotation;
|
|
61
|
+
if (ret?.type !== "TSTypeReference" || !run.structs.has(ret.typeName.name)) {
|
|
62
|
+
fail(comp, "a store's `state` declares its return type, an interface in the same file: `state: (): State => ({ … })`", state ?? init);
|
|
63
|
+
}
|
|
64
|
+
const getters = new Map();
|
|
65
|
+
const getterObj = options.properties.find((p) => (p.key?.name ?? p.key?.value) === "getters");
|
|
66
|
+
if (getterObj && getterObj.value?.type !== "ObjectExpression")
|
|
67
|
+
fail(comp, "a store's `getters` is an object literal", getterObj);
|
|
68
|
+
for (const g of getterObj?.value.properties ?? []) {
|
|
69
|
+
const getterFn = g.type === "ObjectMethod" ? g : g.value;
|
|
70
|
+
const name = g.key?.name ?? g.key?.value;
|
|
71
|
+
if (!getterFn || !["ArrowFunctionExpression", "FunctionExpression", "ObjectMethod"].includes(getterFn.type)) {
|
|
72
|
+
fail(comp, `getter \`${name}\` is a function of the state`, g);
|
|
73
|
+
}
|
|
74
|
+
let body = getterFn.body;
|
|
75
|
+
if (body.type === "BlockStatement") {
|
|
76
|
+
const only = body.body.length === 1 ? body.body[0] : null;
|
|
77
|
+
body = only?.type === "ReturnStatement" ? only.argument : null;
|
|
78
|
+
}
|
|
79
|
+
const param = getterFn.params[0];
|
|
80
|
+
getters.set(name, { param: param?.type === "Identifier" ? param.name : null, body, file: comp.file });
|
|
81
|
+
}
|
|
82
|
+
run.stores.set(v.id.name, {
|
|
83
|
+
hook: v.id.name,
|
|
84
|
+
id: id.value,
|
|
85
|
+
field: snake(id.value),
|
|
86
|
+
state: ret.typeName.name,
|
|
87
|
+
module: path.replace(/\.ts$/, ""),
|
|
88
|
+
getters,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function setupStore(comp, hook, id, fn, path) {
|
|
95
|
+
const run = runOf(piniaStores);
|
|
96
|
+
if (fn.body.type !== "BlockStatement")
|
|
97
|
+
fail(comp, "a setup store's function returns its state from a block: `() => { …; return { … } }`", fn);
|
|
98
|
+
const refs = new Map();
|
|
99
|
+
const computeds = new Map();
|
|
100
|
+
const others = new Set();
|
|
101
|
+
let returned = null;
|
|
102
|
+
for (const st of fn.body.body) {
|
|
103
|
+
if (st.type === "ReturnStatement") {
|
|
104
|
+
returned = st.argument;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (st.type === "FunctionDeclaration") {
|
|
108
|
+
if (st.id)
|
|
109
|
+
others.add(st.id.name);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (st.type === "ExpressionStatement") {
|
|
113
|
+
setupStatement(comp, st);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (st.type !== "VariableDeclaration")
|
|
117
|
+
fail(comp, `\`${st.type}\` in a setup store is not supported`, st);
|
|
118
|
+
for (const d of st.declarations) {
|
|
119
|
+
if (d.id.type !== "Identifier") {
|
|
120
|
+
for (const name of patternNames(d.id))
|
|
121
|
+
others.add(name);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const init = d.init;
|
|
125
|
+
const callee = init?.type === "CallExpression" && init.callee.type === "Identifier" ? init.callee.name : null;
|
|
126
|
+
if (callee === "ref" || callee === "shallowRef") {
|
|
127
|
+
refs.set(d.id.name, refType(comp, init));
|
|
128
|
+
}
|
|
129
|
+
else if (callee === "computed") {
|
|
130
|
+
const getter = init.arguments[0];
|
|
131
|
+
let body = getter?.body;
|
|
132
|
+
if (body?.type === "BlockStatement") {
|
|
133
|
+
const only = body.body.length === 1 ? body.body[0] : null;
|
|
134
|
+
body = only?.type === "ReturnStatement" ? only.argument : null;
|
|
135
|
+
}
|
|
136
|
+
computeds.set(d.id.name, body);
|
|
137
|
+
}
|
|
138
|
+
else
|
|
139
|
+
others.add(d.id.name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (returned?.type !== "ObjectExpression")
|
|
143
|
+
fail(comp, "a setup store returns an object of its state, getters and actions", returned ?? fn);
|
|
144
|
+
const state = [];
|
|
145
|
+
const getters = new Map();
|
|
146
|
+
for (const p of returned.properties) {
|
|
147
|
+
if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier")
|
|
148
|
+
fail(comp, "a setup store returns plain names: `{ count, doubled, increment }`", p);
|
|
149
|
+
const key = p.key.name ?? p.key.value;
|
|
150
|
+
const local = p.value.name;
|
|
151
|
+
if (refs.has(local))
|
|
152
|
+
state.push({ js: key, rust: snake(key), ty: markStore(refs.get(local)) });
|
|
153
|
+
else if (computeds.has(local))
|
|
154
|
+
getters.set(key, { param: null, body: computeds.get(local), file: comp.file, setup: true });
|
|
155
|
+
else if (!others.has(local))
|
|
156
|
+
fail(comp, `\`${local}\` is not declared in the store's function`, p);
|
|
157
|
+
}
|
|
158
|
+
const name = id.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") + "State";
|
|
159
|
+
if (run.structs.has(name))
|
|
160
|
+
fail(comp, `\`${name}\` names this setup store's state; rename the interface`, fn);
|
|
161
|
+
run.structs.set(name, { name, fields: state });
|
|
162
|
+
run.files.set(name, comp.file);
|
|
163
|
+
run.stores.set(hook, { hook, id, field: snake(id), state: name, module: path.replace(/\.ts$/, ""), getters });
|
|
164
|
+
}
|
|
165
|
+
function refType(comp, init) {
|
|
166
|
+
const run = runOf(piniaStores);
|
|
167
|
+
const typed = init.typeParameters?.params?.[0];
|
|
168
|
+
const value = init.arguments[0];
|
|
169
|
+
if (typed) {
|
|
170
|
+
const ty = tyOfTs(comp, typed, run.structs);
|
|
171
|
+
return value ? ty : opt(ty);
|
|
172
|
+
}
|
|
173
|
+
const literal = (n) => {
|
|
174
|
+
if (n?.type === "StringLiteral" || (n?.type === "TemplateLiteral" && n.expressions.length === 0))
|
|
175
|
+
return STR;
|
|
176
|
+
if (n?.type === "NumericLiteral")
|
|
177
|
+
return Number.isInteger(n.value) ? INT : FLOAT;
|
|
178
|
+
if (n?.type === "UnaryExpression" && n.operator === "-")
|
|
179
|
+
return literal(n.argument);
|
|
180
|
+
if (n?.type === "BooleanLiteral")
|
|
181
|
+
return BOOL;
|
|
182
|
+
if (n?.type === "ArrayExpression" && n.elements.length) {
|
|
183
|
+
const of = literal(n.elements[0]);
|
|
184
|
+
return of && n.elements.every((el) => sameTy(literal(el) ?? UNDEF, of)) ? { k: "list", of } : null;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
};
|
|
188
|
+
return literal(value) ?? fail(comp, "a setup store's ref declares its type: `ref<string[]>([])`", init);
|
|
189
|
+
}
|
|
190
|
+
export function storeHome(file) {
|
|
191
|
+
return blankComponent(basename(file), "stores", file, runOf(piniaStores).structs);
|
|
192
|
+
}
|
|
193
|
+
function storesSource(dir) {
|
|
194
|
+
const { stores, structs: states, files } = runOf(piniaStores);
|
|
195
|
+
const home = storeHome(dir);
|
|
196
|
+
const testDerive = (src) => src.replace("#[cfg_attr(test, derive(serde::Deserialize))]", `#[cfg_attr(test, derive(${src.includes("Default, serde::Serialize") ? "" : "Default, "}serde::Deserialize))]\n#[cfg_attr(test, serde(default))]`);
|
|
197
|
+
const structs = [...states.values()]
|
|
198
|
+
.map((st) => testDerive(structSource(st, home, `/// \`${st.name}\` in \`${files.get(st.name)}\`.\n`)))
|
|
199
|
+
.join("\n");
|
|
200
|
+
const all = {
|
|
201
|
+
name: "Stores",
|
|
202
|
+
fields: [...stores.values()].map((st) => ({ js: st.id, rust: st.field, ty: { k: "struct", name: st.state, store: true } })),
|
|
203
|
+
};
|
|
204
|
+
const top = testDerive(structSource(all, home, "/// Every store's state, keyed by id as `pinia.state.value` is: what the page sends the client.\n"));
|
|
205
|
+
return `${header(dir, "the store files")}
|
|
206
|
+
//! The Pinia stores' state, which components read while they render on the server.
|
|
207
|
+
|
|
208
|
+
${/Cow</.test(structs) ? "use std::borrow::Cow;\n\n" : ""}${structs}
|
|
209
|
+
${top}`;
|
|
210
|
+
}
|
|
211
|
+
function mentions(n, name) {
|
|
212
|
+
if (!n || typeof n !== "object")
|
|
213
|
+
return false;
|
|
214
|
+
if (n.type === "Identifier" && n.name === name)
|
|
215
|
+
return true;
|
|
216
|
+
return Object.entries(n).some(([k, v]) => k !== "loc" && k !== "__fv" && (Array.isArray(v) ? v.some((x) => mentions(x, name)) : typeof v === "object" && mentions(v, name)));
|
|
217
|
+
}
|
|
218
|
+
function storeGetter(s, base, name, node) {
|
|
219
|
+
if (base.ty.k !== "struct" || !base.ty.store)
|
|
220
|
+
return null;
|
|
221
|
+
const stateName = base.ty.name;
|
|
222
|
+
const store = [...runOf(piniaStores).stores.values()].find((st) => st.state === stateName);
|
|
223
|
+
const g = store?.getters.get(name);
|
|
224
|
+
if (!g)
|
|
225
|
+
return null;
|
|
226
|
+
const where = `getter \`${name}\` in ${g.file}`;
|
|
227
|
+
if (!g.body)
|
|
228
|
+
fail(s.comp, `${where} returns a single expression`, node);
|
|
229
|
+
const uses = (n, type) => !!n && typeof n === "object" && (n.type === type || Object.entries(n).some(([k, v]) => k !== "loc" && (Array.isArray(v) ? v.some((x) => uses(x, type)) : typeof v === "object" && uses(v, type))));
|
|
230
|
+
if (uses(g.body, "ThisExpression"))
|
|
231
|
+
fail(s.comp, `${where} reads \`this\`; read the state through the getter's parameter`, node);
|
|
232
|
+
if (g.body.type === "ArrowFunctionExpression" || g.body.type === "FunctionExpression") {
|
|
233
|
+
fail(s.comp, `${where} returns a function, which takes arguments only the client passes`, node);
|
|
234
|
+
}
|
|
235
|
+
const locals = new Map();
|
|
236
|
+
if (g.param)
|
|
237
|
+
locals.set(g.param, { code: base.code, ty: base.ty });
|
|
238
|
+
const setup = new Map();
|
|
239
|
+
const refs = new Set();
|
|
240
|
+
if (g.setup) {
|
|
241
|
+
const fields = runOf(piniaStores).structs.get(stateName)?.fields ?? [];
|
|
242
|
+
for (const f of fields) {
|
|
243
|
+
setup.set(f.js, fieldVal(s.comp, base.code, base.ty, f.js, node));
|
|
244
|
+
refs.add(f.js);
|
|
245
|
+
}
|
|
246
|
+
for (const other of store.getters.keys()) {
|
|
247
|
+
if (other === name || !mentions(g.body, other))
|
|
248
|
+
continue;
|
|
249
|
+
if (runOf(piniaStores).inProgress.has(`${stateName}.${other}`))
|
|
250
|
+
fail(s.comp, `${where} and \`${other}\` read each other`, node);
|
|
251
|
+
runOf(piniaStores).inProgress.add(`${stateName}.${name}`);
|
|
252
|
+
try {
|
|
253
|
+
setup.set(other, storeGetter(s, base, other, node));
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
runOf(piniaStores).inProgress.delete(`${stateName}.${name}`);
|
|
257
|
+
}
|
|
258
|
+
refs.add(other);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
return expr({ ...s, locals, narrowed: new Map(), setup, refs, propsIdent: null }, g.body);
|
|
263
|
+
}
|
|
264
|
+
catch (e) {
|
|
265
|
+
if (e instanceof GenError)
|
|
266
|
+
throw new GenError(`${s.comp.file}: ${where}: ${e.message.replace(/^[^:]*: /, "")}`);
|
|
267
|
+
throw e;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function markStore(ty) {
|
|
271
|
+
if (ty.k === "struct")
|
|
272
|
+
return { ...ty, store: true };
|
|
273
|
+
if (ty.k === "opt" || ty.k === "list" || ty.k === "record")
|
|
274
|
+
return { ...ty, of: markStore(ty.of) };
|
|
275
|
+
return ty;
|
|
276
|
+
}
|
|
277
|
+
export const piniaStores = {
|
|
278
|
+
name: "stores",
|
|
279
|
+
configure: (config) => ({
|
|
280
|
+
dir: config.stores ?? null,
|
|
281
|
+
stores: new Map(),
|
|
282
|
+
structs: new Map(),
|
|
283
|
+
files: new Map(),
|
|
284
|
+
readers: new Set(),
|
|
285
|
+
inProgress: new Set(),
|
|
286
|
+
}),
|
|
287
|
+
prepare(root) {
|
|
288
|
+
const { dir } = runOf(piniaStores);
|
|
289
|
+
if (dir)
|
|
290
|
+
readStores(root, dir);
|
|
291
|
+
},
|
|
292
|
+
importedType(comp, file, name, local) {
|
|
293
|
+
const { stores, structs } = runOf(piniaStores);
|
|
294
|
+
const module = file.replace(/\.ts$/, "");
|
|
295
|
+
if (![...stores.values()].some((x) => x.module === module))
|
|
296
|
+
return false;
|
|
297
|
+
if (structs.has(name))
|
|
298
|
+
comp.importedTypes.set(local, { k: "struct", name, store: true });
|
|
299
|
+
return true;
|
|
300
|
+
},
|
|
301
|
+
struct: (ty) => (ty.store ? { st: runOf(piniaStores).structs.get(ty.name), module: "stores" } : null),
|
|
302
|
+
scope: () => ({ hooks: new Map(), storeToRefs: null, values: new Map() }),
|
|
303
|
+
scriptImport(s, st, from) {
|
|
304
|
+
const comp = s.comp;
|
|
305
|
+
const own = scopeOf(piniaStores, s);
|
|
306
|
+
if (storeImport(comp, from)) {
|
|
307
|
+
for (const sp of st.specifiers) {
|
|
308
|
+
const hook = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : null;
|
|
309
|
+
if (st.importKind === "type" || sp.importKind === "type" || (hook !== null && runOf(piniaStores).structs.has(hook)))
|
|
310
|
+
continue;
|
|
311
|
+
const store = hook ? runOf(piniaStores).stores.get(hook) : undefined;
|
|
312
|
+
if (!store || store.module !== storeImport(comp, from))
|
|
313
|
+
fail(comp, "import a store by its `use…` hook", sp);
|
|
314
|
+
own.hooks.set(sp.local.name, store);
|
|
315
|
+
}
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
if (from !== "pinia")
|
|
319
|
+
return false;
|
|
320
|
+
for (const sp of st.specifiers) {
|
|
321
|
+
const name = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : null;
|
|
322
|
+
if (name === "storeToRefs")
|
|
323
|
+
own.storeToRefs = sp.local.name;
|
|
324
|
+
else
|
|
325
|
+
s.clientOnly.set(sp.local.name, `\`${name}\` from Pinia does not run on the server`);
|
|
326
|
+
}
|
|
327
|
+
return true;
|
|
328
|
+
},
|
|
329
|
+
scriptBinding(s, d) {
|
|
330
|
+
const comp = s.comp;
|
|
331
|
+
const own = scopeOf(piniaStores, s);
|
|
332
|
+
const init = d.init;
|
|
333
|
+
const callee = init?.type === "CallExpression" && init.callee.type === "Identifier" ? init.callee.name : null;
|
|
334
|
+
if (d.id.type === "ObjectPattern" && callee !== null && callee === own.storeToRefs) {
|
|
335
|
+
const arg = init.arguments[0];
|
|
336
|
+
const store = arg?.type === "Identifier" ? own.values.get(arg.name) : undefined;
|
|
337
|
+
if (!store || init.arguments.length !== 1)
|
|
338
|
+
fail(comp, "`storeToRefs` takes a store bound in this setup", d);
|
|
339
|
+
for (const p of d.id.properties) {
|
|
340
|
+
if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
|
|
341
|
+
fail(comp, "`storeToRefs` is destructured into plain names", p);
|
|
342
|
+
}
|
|
343
|
+
const key = p.key.type === "Identifier" ? p.key.name : p.key.value;
|
|
344
|
+
const state = { code: `fv_stores.${store.field}`, ty: { k: "struct", name: store.state, store: true } };
|
|
345
|
+
s.setup.set(p.value.name, storeGetter(s, state, key, p) ?? fieldVal(comp, state.code, state.ty, key, p));
|
|
346
|
+
s.refs.add(p.value.name);
|
|
347
|
+
}
|
|
348
|
+
runOf(piniaStores).readers.add(comp);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
const hook = d.id.type === "Identifier" && callee !== null ? own.hooks.get(callee) : undefined;
|
|
352
|
+
if (!hook)
|
|
353
|
+
return false;
|
|
354
|
+
if (init.arguments.length)
|
|
355
|
+
fail(comp, "a store hook takes no arguments", d);
|
|
356
|
+
own.values.set(d.id.name, hook);
|
|
357
|
+
s.setup.set(d.id.name, { code: `fv_stores.${hook.field}`, ty: { k: "struct", name: hook.state, store: true } });
|
|
358
|
+
runOf(piniaStores).readers.add(comp);
|
|
359
|
+
return true;
|
|
360
|
+
},
|
|
361
|
+
member: (s, base, prop, n, computed) => (computed ? null : storeGetter(s, base, prop, n)),
|
|
362
|
+
params: [
|
|
363
|
+
{
|
|
364
|
+
name: "fv_stores",
|
|
365
|
+
ty: "&super::stores::Stores<'_>",
|
|
366
|
+
pageTy: "&'p super::stores::Stores<'p>",
|
|
367
|
+
reads: (c) => runOf(piniaStores).readers.has(c),
|
|
368
|
+
test: { lines: ["let state: stores::Stores = serde_json::from_value(fixture.stores.clone()).map_err(|e| e.to_string())?;"], arg: "&state", fixture: true },
|
|
369
|
+
fixtureField: ' #[serde(rename = "$stores", default = "Fixture::no_stores")]\n stores: serde_json::Value,',
|
|
370
|
+
fixtureDefault: " fn no_stores() -> serde_json::Value {\n serde_json::Value::Object(Default::default())\n }",
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
modules() {
|
|
374
|
+
const { dir, stores } = runOf(piniaStores);
|
|
375
|
+
return stores.size ? [["stores.rs", storesSource(dir.replace(/\/?$/, "/"))]] : [];
|
|
376
|
+
},
|
|
377
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { fail } from "../model.js";
|
|
2
|
+
import { expr } from "../expr.js";
|
|
3
|
+
import { cond } from "../narrowing.js";
|
|
4
|
+
import { bare, strArg } from "../parens.js";
|
|
5
|
+
import { statements } from "../template.js";
|
|
6
|
+
import { runOf } from "../plugin.js";
|
|
7
|
+
export const teleport = {
|
|
8
|
+
name: "teleport",
|
|
9
|
+
configure: () => new Set(),
|
|
10
|
+
compiled(comp, code) {
|
|
11
|
+
if (code.includes("_ssrRenderTeleport("))
|
|
12
|
+
runOf(teleport).add(comp);
|
|
13
|
+
},
|
|
14
|
+
statement(s, e, c, st) {
|
|
15
|
+
if (c.callee.type !== "Identifier" || c.callee.name !== "_ssrRenderTeleport")
|
|
16
|
+
return false;
|
|
17
|
+
const [, content, target, disabled] = c.arguments;
|
|
18
|
+
if (s.fill)
|
|
19
|
+
fail(s.comp, "a `<Teleport>` in slot content whose emptiness is decided at run time", st);
|
|
20
|
+
const to = expr(s, target);
|
|
21
|
+
if (to.ty.k !== "str")
|
|
22
|
+
fail(s.comp, "a `<Teleport>`'s `to` is a string", target);
|
|
23
|
+
const off = disabled ? bare(cond(s, disabled)) : "false";
|
|
24
|
+
if (content?.type !== "ArrowFunctionExpression" || content.body.type !== "BlockStatement")
|
|
25
|
+
fail(s.comp, "unexpected `<Teleport>` content", st);
|
|
26
|
+
e.open(`fv::teleport_into(out, fv_teleports, ${strArg(to.code)}, ${off}, &|out: &mut String|`);
|
|
27
|
+
statements(s, e, content.body.body);
|
|
28
|
+
e.close(");");
|
|
29
|
+
return true;
|
|
30
|
+
},
|
|
31
|
+
params: [
|
|
32
|
+
{
|
|
33
|
+
name: "fv_teleports",
|
|
34
|
+
ty: "&fv::Teleports",
|
|
35
|
+
pageTy: "&'p fv::Teleports",
|
|
36
|
+
reads: (c) => runOf(teleport).has(c),
|
|
37
|
+
test: { lines: ["let teleports = ferrovue::Teleports::new();"], arg: "&teleports", after: ["teleports_into(&mut out, teleports);"] },
|
|
38
|
+
testSupport: `/// What was teleported, after a marker, as the conformance suite writes Vue's: \`{"target":"…"}\`.
|
|
39
|
+
#[cfg(test)]
|
|
40
|
+
fn teleports_into(out: &mut String, teleports: ferrovue::Teleports) {
|
|
41
|
+
let targets = teleports.into_targets();
|
|
42
|
+
if targets.is_empty() {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
let pairs: Vec<String> = targets
|
|
46
|
+
.iter()
|
|
47
|
+
.map(|(t, html)| format!("{}:{}", serde_json::to_string(t).unwrap(), serde_json::to_string(html).unwrap()))
|
|
48
|
+
.collect();
|
|
49
|
+
out.push_str("<!--fv-teleports-->{");
|
|
50
|
+
out.push_str(&pairs.join(","));
|
|
51
|
+
out.push('}');
|
|
52
|
+
}`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
package/dist/rust.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { type Component, type N, type Struct, type Ty } from "./model.ts";
|
|
2
|
-
import { type RouteDef } from "./context.ts";
|
|
3
2
|
export declare function needsLifetime(ty: Ty, comp: Component, seen?: Set<string>): boolean;
|
|
4
3
|
export declare function structLifetime(st: Struct, comp: Component): boolean;
|
|
5
4
|
export declare function rustTy(ty: Ty, comp: Component): string;
|
|
6
5
|
export declare function structSource(st: Struct, comp: Component, doc: string): string;
|
|
7
6
|
export declare function header(source: string, edit?: string): string;
|
|
8
|
-
/** Rust expressions summing the lengths of the strings a value of type `ty` at `place` holds. */
|
|
9
7
|
export declare function textLen(comp: Component, place: string, ty: Ty, seen?: Set<string>): string[];
|
|
8
|
+
export declare function isIsland(comp: Component): boolean;
|
|
10
9
|
export declare function componentSource(comp: Component, ast: N[], ssr: string, components: Map<string, Component>): string;
|
|
11
|
-
export declare function modSource(comps: Component[]): string;
|
|
12
|
-
export declare function storesSource(dir: string): string;
|
|
10
|
+
export declare function modSource(comps: Component[], modules: string[]): string;
|
|
13
11
|
export declare function typesSource(): string;
|
|
14
|
-
export declare function routesSource(routes: RouteDef[], file: string): string;
|