ferrovue 0.6.0 → 0.7.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 +106 -13
- package/dist/args.d.ts +28 -0
- package/dist/args.js +69 -0
- package/dist/attrs.js +3 -0
- package/dist/calls.js +5 -2
- package/dist/children.js +18 -3
- package/dist/cli.js +80 -81
- package/dist/client-render.d.ts +23 -0
- package/dist/client-render.js +143 -0
- package/dist/client.d.ts +65 -6
- package/dist/client.js +269 -27
- package/dist/compiler.d.ts +9 -5
- package/dist/compiler.js +48 -14
- package/dist/component.d.ts +7 -3
- package/dist/component.js +37 -25
- package/dist/conformance.d.ts +24 -3
- package/dist/conformance.js +58 -7
- package/dist/constants.js +4 -2
- package/dist/context.d.ts +51 -4
- package/dist/context.js +173 -5
- package/dist/diagnostics.d.ts +6 -4
- package/dist/diagnostics.js +7 -3
- package/dist/editor.d.ts +18 -0
- package/dist/editor.js +59 -0
- package/dist/errors.d.ts +45 -3
- package/dist/errors.js +33 -3
- package/dist/expr.d.ts +2 -0
- package/dist/expr.js +91 -12
- package/dist/fallthrough.d.ts +1 -0
- package/dist/fallthrough.js +10 -2
- package/dist/files.d.ts +13 -0
- package/dist/files.js +49 -0
- package/dist/fixture.d.ts +3 -3
- package/dist/hydration.d.ts +12 -1
- package/dist/hydration.js +47 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/inputs.d.ts +24 -0
- package/dist/inputs.js +40 -0
- package/dist/link-router.d.ts +6 -2
- package/dist/link-router.js +7 -4
- package/dist/lists.d.ts +12 -1
- package/dist/lists.js +76 -2
- package/dist/model.d.ts +9 -1
- package/dist/model.js +2 -0
- package/dist/page-routes.d.ts +1 -0
- package/dist/paths.d.ts +5 -0
- package/dist/paths.js +7 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugins/css-modules.d.ts +39 -0
- package/dist/plugins/css-modules.js +135 -0
- package/dist/plugins/head.js +4 -4
- package/dist/plugins/i18n.js +5 -12
- package/dist/plugins/index.js +2 -1
- package/dist/plugins/provide.js +4 -2
- package/dist/plugins/router.js +3 -2
- package/dist/plugins/scoped.d.ts +10 -0
- package/dist/plugins/scoped.js +17 -4
- package/dist/plugins/shared-types.js +2 -1
- package/dist/plugins/stores.js +7 -6
- package/dist/rust.d.ts +9 -1
- package/dist/rust.js +38 -15
- package/dist/schema.d.ts +25 -0
- package/dist/schema.js +73 -0
- package/dist/script.js +12 -5
- package/dist/slots.js +2 -0
- package/dist/ssr.js +42 -4
- package/dist/styles.d.ts +5 -1
- package/dist/styles.js +131 -4
- package/dist/testing.d.ts +2 -1
- package/dist/testing.js +1 -1
- package/dist/typescript.d.ts +11 -1
- package/dist/typescript.js +124 -33
- package/dist/vite.d.ts +18 -10
- package/dist/vite.js +106 -51
- package/dist/volar.d.ts +15 -0
- package/dist/volar.js +87 -0
- package/package.json +27 -7
- package/schema.json +333 -0
package/dist/plugins/scoped.d.ts
CHANGED
|
@@ -4,9 +4,19 @@ import { type Plugin } from "../plugin.ts";
|
|
|
4
4
|
interface ScopedRun {
|
|
5
5
|
mode: ScopeIdMode;
|
|
6
6
|
viteRoot: string;
|
|
7
|
+
production: boolean;
|
|
7
8
|
ids: Map<Component, string>;
|
|
9
|
+
cssVars: Map<Component, {
|
|
10
|
+
id: string;
|
|
11
|
+
vars: string[];
|
|
12
|
+
}>;
|
|
8
13
|
slotted: Set<Component>;
|
|
9
14
|
}
|
|
10
15
|
export declare function scopeIdOf(comp: Component): string | null;
|
|
16
|
+
/** The id `@vitejs/plugin-vue` gives a component, `descriptor.id`: a hash of its path from Vite's
|
|
17
|
+
* root, followed by its source unless `mode` hashes the path alone. */
|
|
18
|
+
export declare function componentId(file: string, source: string, mode: ScopeIdMode, viteRoot: string): string;
|
|
19
|
+
/** The component's id, `componentId`, as the configuration computes it. */
|
|
20
|
+
export declare function scopeHash(file: string, source: string): string;
|
|
11
21
|
export declare const scoped: Plugin<ScopedRun>;
|
|
12
22
|
export {};
|
package/dist/plugins/scoped.js
CHANGED
|
@@ -7,14 +7,20 @@ import { runOf } from "../plugin.js";
|
|
|
7
7
|
export function scopeIdOf(comp) {
|
|
8
8
|
return runOf(scoped).ids.get(comp) ?? null;
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/** The id `@vitejs/plugin-vue` gives a component, `descriptor.id`: a hash of its path from Vite's
|
|
11
|
+
* root, followed by its source unless `mode` hashes the path alone. */
|
|
12
|
+
export function componentId(file, source, mode, viteRoot) {
|
|
12
13
|
const path = relative(viteRoot, resolvePath(file)).split(sep).join("/");
|
|
13
14
|
return createHash("sha256")
|
|
14
15
|
.update(mode === "filepath" ? path : path + source)
|
|
15
16
|
.digest("hex")
|
|
16
17
|
.slice(0, 8);
|
|
17
18
|
}
|
|
19
|
+
/** The component's id, `componentId`, as the configuration computes it. */
|
|
20
|
+
export function scopeHash(file, source) {
|
|
21
|
+
const { mode, viteRoot } = runOf(scoped);
|
|
22
|
+
return componentId(file, source, mode, viteRoot);
|
|
23
|
+
}
|
|
18
24
|
function flowOf(comp, ssr, children) {
|
|
19
25
|
const calls = [];
|
|
20
26
|
const outlets = [];
|
|
@@ -106,19 +112,26 @@ export const scoped = {
|
|
|
106
112
|
configure: (config, root) => ({
|
|
107
113
|
mode: config.scopeId ?? "filepath-source",
|
|
108
114
|
viteRoot: resolvePath(root, config.viteRoot ?? "."),
|
|
115
|
+
production: config.isProduction ?? true,
|
|
109
116
|
ids: new Map(),
|
|
117
|
+
cssVars: new Map(),
|
|
110
118
|
slotted: new Set(),
|
|
111
119
|
}),
|
|
112
120
|
sfc(comp, descriptor, file, source) {
|
|
113
121
|
const run = runOf(scoped);
|
|
122
|
+
const hash = descriptor.styles.some((st) => st.scoped) || descriptor.cssVars.length ? scopeHash(file, source) : null;
|
|
114
123
|
if (descriptor.styles.some((st) => st.scoped))
|
|
115
|
-
run.ids.set(comp, `data-v-${
|
|
124
|
+
run.ids.set(comp, `data-v-${hash}`);
|
|
125
|
+
if (descriptor.cssVars.length)
|
|
126
|
+
run.cssVars.set(comp, { id: `data-v-${hash}`, vars: descriptor.cssVars });
|
|
116
127
|
if (descriptor.slotted)
|
|
117
128
|
run.slotted.add(comp);
|
|
118
129
|
},
|
|
119
130
|
templateOptions(comp) {
|
|
131
|
+
const run = runOf(scoped);
|
|
120
132
|
const id = scopeIdOf(comp);
|
|
121
|
-
|
|
133
|
+
const vars = run.cssVars.get(comp);
|
|
134
|
+
return { id: id ?? vars?.id ?? comp.name, scoped: id !== null, slotted: run.slotted.has(comp), ssrCssVars: vars?.vars ?? [], isProd: run.production };
|
|
122
135
|
},
|
|
123
136
|
analyse: (read) => scopeFlow(read),
|
|
124
137
|
childIds,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ctx } from "../context.js";
|
|
2
2
|
import { typesSource } from "../rust.js";
|
|
3
|
+
import { typeWritten } from "../typescript.js";
|
|
3
4
|
import {} from "../plugin.js";
|
|
4
5
|
export const sharedTypes = {
|
|
5
6
|
name: "types",
|
|
6
|
-
modules: () => (ctx.typeStructs.
|
|
7
|
+
modules: () => ([...ctx.typeStructs.keys()].some(typeWritten) || ctx.typeConsts.size ? [["types.rs", typesSource()]] : []),
|
|
7
8
|
};
|
package/dist/plugins/stores.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { relativePath } from "../paths.js";
|
|
4
4
|
import { absence, blankComponent, BOOL, fail, failIn, FLOAT, GenError, INT, opt, sameTy, snake, STR, tagAst, UNDEF } from "../model.js";
|
|
5
5
|
import { ctx } from "../context.js";
|
|
6
|
+
import { listDir, parseTs } from "../files.js";
|
|
6
7
|
import { ONE_NOTHING, structOf, tyOfTs, typesImports } from "../typescript.js";
|
|
7
8
|
import { patternNames, setupStatement } from "../script.js";
|
|
8
9
|
import { expr, fieldVal } from "../expr.js";
|
|
@@ -17,12 +18,12 @@ function storeImport(comp, from) {
|
|
|
17
18
|
}
|
|
18
19
|
function readStores(root, dir) {
|
|
19
20
|
const run = runOf(piniaStores);
|
|
20
|
-
const files =
|
|
21
|
+
const files = listDir(root, dir, "stores").filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts")).toSorted();
|
|
21
22
|
for (const f of files) {
|
|
22
23
|
const path = join(root, dir, f);
|
|
23
|
-
const comp = storeHome(
|
|
24
|
+
const comp = storeHome(relativePath(root, path));
|
|
24
25
|
comp.source = readFileSync(path, "utf8");
|
|
25
|
-
const ast =
|
|
26
|
+
const ast = parseTs(comp);
|
|
26
27
|
tagAst(ast, "source");
|
|
27
28
|
typesImports(comp, ast);
|
|
28
29
|
const decls = ast.map((st) => (st.type === "ExportNamedDeclaration" ? st.declaration : st)).filter(Boolean);
|
package/dist/rust.d.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { type Component, type N, type Struct, type Ty } from "./model.ts";
|
|
2
2
|
export declare function needsLifetime(ty: Ty, comp: Component, seen?: Set<string>): boolean;
|
|
3
3
|
export declare function structLifetime(st: Struct, comp: Component): boolean;
|
|
4
|
+
export declare function localTy(ty: Ty, comp: Component): string;
|
|
4
5
|
export declare function rustTy(ty: Ty, comp: Component): string;
|
|
5
6
|
export declare function structSource(st: Struct, comp: Component, doc: string): string;
|
|
7
|
+
/** The version of generated code this compiler writes, which `mod.rs` hands `ferrovue::__compat!`:
|
|
8
|
+
* the highest version the `ferrovue` crate of the same release supports. RELEASING.md says when
|
|
9
|
+
* it changes. */
|
|
10
|
+
export declare const GENERATED_VERSION = 1;
|
|
11
|
+
/** How every file ferrovue writes begins. */
|
|
12
|
+
export declare const GENERATED = "// @generated by ferrovue";
|
|
6
13
|
export declare function header(source: string, edit?: string): string;
|
|
7
14
|
export declare function textLen(comp: Component, place: string, ty: Ty, seen?: Set<string>): string[];
|
|
8
15
|
export declare function isIsland(comp: Component): boolean;
|
|
9
|
-
|
|
16
|
+
/** The component's module, built once nothing the server renders reaches more of its interfaces. */
|
|
17
|
+
export declare function componentSource(comp: Component, ast: N[], ssr: string, components: Map<string, Component>): () => string;
|
|
10
18
|
export declare function modSource(comps: Component[], modules: string[]): string;
|
|
11
19
|
export declare function typesSource(): string;
|
package/dist/rust.js
CHANGED
|
@@ -2,11 +2,12 @@ import { parse as parseJs } from "@babel/parser";
|
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { blankComponent, fail, GenError, rustStr, snake, tagAst, takesAttrs } from "./model.js";
|
|
4
4
|
import { ctx, INLINE_HTML } from "./context.js";
|
|
5
|
-
import { lookupStruct } from "./typescript.js";
|
|
5
|
+
import { lookupStruct, typeWritten } from "./typescript.js";
|
|
6
6
|
import { childOf } from "./expr.js";
|
|
7
7
|
import { Emitter } from "./emitter.js";
|
|
8
8
|
import { occurrences } from "./parens.js";
|
|
9
9
|
import { statements } from "./template.js";
|
|
10
|
+
import { inlineCssVars } from "./styles.js";
|
|
10
11
|
import { slotFieldBorrows, slotFieldTy, slotTypeName } from "./slots.js";
|
|
11
12
|
import { extraParams, fieldInit, takesSlots } from "./children.js";
|
|
12
13
|
import { paramsOf, renderParams, slotContextOf, slotFieldsOf } from "./plugin.js";
|
|
@@ -39,6 +40,9 @@ export function needsLifetime(ty, comp, seen = new Set()) {
|
|
|
39
40
|
export function structLifetime(st, comp) {
|
|
40
41
|
return st.fields.some((f) => needsLifetime(f.ty, comp, new Set([st.name])));
|
|
41
42
|
}
|
|
43
|
+
export function localTy(ty, comp) {
|
|
44
|
+
return rustTy(ty, comp).replace(/\bCow<'a, /g, "std::borrow::Cow<").replace(/'a\b/g, "'_");
|
|
45
|
+
}
|
|
42
46
|
export function rustTy(ty, comp) {
|
|
43
47
|
switch (ty.k) {
|
|
44
48
|
case "str":
|
|
@@ -134,8 +138,14 @@ ${fields}
|
|
|
134
138
|
}
|
|
135
139
|
${ctx.builders ? `\n${builderSource(st, comp, life)}\n` : ""}`;
|
|
136
140
|
}
|
|
141
|
+
/** The version of generated code this compiler writes, which `mod.rs` hands `ferrovue::__compat!`:
|
|
142
|
+
* the highest version the `ferrovue` crate of the same release supports. RELEASING.md says when
|
|
143
|
+
* it changes. */
|
|
144
|
+
export const GENERATED_VERSION = 1;
|
|
145
|
+
/** How every file ferrovue writes begins. */
|
|
146
|
+
export const GENERATED = "// @generated by ferrovue";
|
|
137
147
|
export function header(source, edit = "the `.vue` file") {
|
|
138
|
-
return
|
|
148
|
+
return `${GENERATED} from ${source}. Do not edit: change ${edit} and run
|
|
139
149
|
// \`ferrovue\`.
|
|
140
150
|
`;
|
|
141
151
|
}
|
|
@@ -174,8 +184,14 @@ export function textLen(comp, place, ty, seen = new Set()) {
|
|
|
174
184
|
return [];
|
|
175
185
|
}
|
|
176
186
|
}
|
|
187
|
+
function manyArgs(count) {
|
|
188
|
+
return count > 7 ? "#[allow(clippy::too_many_arguments)]\n" : "";
|
|
189
|
+
}
|
|
190
|
+
function renderParamCount(comp) {
|
|
191
|
+
return (takesSlots(comp) ? 1 : 0) + paramsOf(comp).length;
|
|
192
|
+
}
|
|
177
193
|
function renderSource(comp, life, args, e) {
|
|
178
|
-
const doc =
|
|
194
|
+
const doc = `/// Write the component's server render into \`out\`.\n${manyArgs(2 + renderParamCount(comp))}`;
|
|
179
195
|
const props = e.reads("props", 0) ? "props" : "_props";
|
|
180
196
|
const unread = (name) => {
|
|
181
197
|
const shadow = e.lines.findIndex((l) => l.trimStart().startsWith(`let ${name} = `));
|
|
@@ -194,13 +210,14 @@ function renderSource(comp, life, args, e) {
|
|
|
194
210
|
|
|
195
211
|
/// [\`render\`], with ${what}.
|
|
196
212
|
#[doc(hidden)]
|
|
197
|
-
pub fn render_scoped(out: &mut String, ${props}: &Props${life}${extraParams(comp, unread)}, ${attrs}: ${ty}) {
|
|
213
|
+
${manyArgs(3 + renderParamCount(comp))}pub fn render_scoped(out: &mut String, ${props}: &Props${life}${extraParams(comp, unread)}, ${attrs}: ${ty}) {
|
|
198
214
|
${e.lines.join("\n")}
|
|
199
215
|
}`;
|
|
200
216
|
}
|
|
201
217
|
export function isIsland(comp) {
|
|
202
218
|
return !takesSlots(comp) && comp.takes.size === 0;
|
|
203
219
|
}
|
|
220
|
+
/** The component's module, built once nothing the server renders reaches more of its interfaces. */
|
|
204
221
|
export function componentSource(comp, ast, ssr, components) {
|
|
205
222
|
if (takesAttrs(comp) && comp.inheritAttrs && comp.attrsDropped) {
|
|
206
223
|
fail(comp, "FV0416", `a root \`<Transition>\` or \`<KeepAlive>\` around a \`v-if\` in ${comp.name}, which a parent passes attributes: Vue's server drops them, where its client puts them on the element; set \`inheritAttrs: false\` and bind \`$attrs\` on the element`, comp.attrsDropped);
|
|
@@ -216,7 +233,7 @@ export function componentSource(comp, ast, ssr, components) {
|
|
|
216
233
|
const opening = [...preludes.flatMap((p) => p.before), ...lets, ...preludes.flatMap((p) => p.after)];
|
|
217
234
|
for (const l of opening)
|
|
218
235
|
e.stmt(l);
|
|
219
|
-
statements(scope, e, fn.body.body);
|
|
236
|
+
statements(scope, e, inlineCssVars(comp, fn.body.body));
|
|
220
237
|
for (let i = opening.length - 1; i >= 0; i--) {
|
|
221
238
|
const name = /^let (\w+)/.exec(opening[i])?.[1];
|
|
222
239
|
if (name !== undefined && !e.reads(name, i + 1))
|
|
@@ -232,11 +249,6 @@ export function componentSource(comp, ast, ssr, components) {
|
|
|
232
249
|
const named = life ? "<'a>" : "";
|
|
233
250
|
const owned = life ? "<'a>" : "";
|
|
234
251
|
const ownedLife = life ? "'a" : "'static";
|
|
235
|
-
const structs = [...comp.structs.values()]
|
|
236
|
-
.filter((st) => st.name !== "Props" && !st.slot)
|
|
237
|
-
.map((st) => structSource(st, comp, `/// \`${st.name}\` in \`${basename(comp.file)}\`.\n`))
|
|
238
|
-
.join("\n");
|
|
239
|
-
const usesCow = /Cow</.test(structs + structSource(comp.props, comp, ""));
|
|
240
252
|
const plain = isIsland(comp);
|
|
241
253
|
const context = slotContextOf(comp);
|
|
242
254
|
const contextTys = context.map((p) => `, ${p.ty}`).join("");
|
|
@@ -310,20 +322,28 @@ pub fn into_island${owned}(props: Props${named}) -> fv::Html<${ownedLife}, Props
|
|
|
310
322
|
}
|
|
311
323
|
`
|
|
312
324
|
: `/// The component's markup, for a maud page that shows it.
|
|
313
|
-
pub fn html${gen}(props: &'p Props${named}${params}) -> fv::Html<'p, Props${named}, impl Fn(&mut String, &Props${named}) + 'p> {
|
|
325
|
+
${manyArgs(1 + renderParamCount(comp))}pub fn html${gen}(props: &'p Props${named}${params}) -> fv::Html<'p, Props${named}, impl Fn(&mut String, &Props${named}) + 'p> {
|
|
314
326
|
fv::Html::markup(props, move |out: &mut String, props: &Props${named}| render(out, props${args}))
|
|
315
327
|
}
|
|
316
328
|
`;
|
|
317
|
-
|
|
329
|
+
const render = renderSource(comp, life, args, e);
|
|
330
|
+
return () => {
|
|
331
|
+
const structs = [...comp.structs.values()]
|
|
332
|
+
.filter((st) => st.name !== "Props" && !st.slot && (!comp.typeDecls.has(st.name) || comp.reached.has(st.name)))
|
|
333
|
+
.map((st) => structSource(st, comp, `/// \`${st.name}\` in \`${basename(comp.file)}\`.\n`))
|
|
334
|
+
.join("\n");
|
|
335
|
+
const usesCow = /Cow</.test(structs + structSource(comp.props, comp, ""));
|
|
336
|
+
return `${header(comp.file)}
|
|
318
337
|
${usesCow ? "use std::borrow::Cow;\n\n" : ""}use ferrovue as fv;
|
|
319
338
|
|
|
320
339
|
/// The component's name, as \`data-island\` carries it.
|
|
321
340
|
pub const NAME: &str = ${rustStr(comp.name)};
|
|
322
341
|
|
|
323
342
|
${structs ? structs + "\n" : ""}${structSource(comp.props, comp, `/// The props \`${basename(comp.file)}\` declares.\n`)}
|
|
324
|
-
${slotsStruct}${
|
|
343
|
+
${slotsStruct}${render}
|
|
325
344
|
|
|
326
345
|
${wrappers}`;
|
|
346
|
+
};
|
|
327
347
|
}
|
|
328
348
|
function readsFixture(c) {
|
|
329
349
|
return takesSlots(c) || paramsOf(c).some((p) => p.test.fixture);
|
|
@@ -401,6 +421,8 @@ ${defaults.map((d) => `${d}\n\n`).join("")} fn slot(&self, name: &str) -> Opt
|
|
|
401
421
|
${ctx.builders ? "// `into_` forms, `NAME`, a constructor and a setter per optional prop) and an app calls only what\n// it needs." : "// `into_` forms and `NAME`) and an app calls only what it needs."}
|
|
402
422
|
#![allow(dead_code)]
|
|
403
423
|
|
|
424
|
+
ferrovue::__compat!(${GENERATED_VERSION});
|
|
425
|
+
|
|
404
426
|
${comps.map((c) => `pub mod ${c.module};`).join("\n")}${modules.map((m) => `\npub mod ${m.replace(/\.rs$/, "")};`).join("")}
|
|
405
427
|
${fixture}
|
|
406
428
|
${renderParams()
|
|
@@ -420,8 +442,9 @@ ${arms}
|
|
|
420
442
|
}
|
|
421
443
|
export function typesSource() {
|
|
422
444
|
const home = blankComponent("types", "types", "types", ctx.typeStructs);
|
|
423
|
-
const
|
|
424
|
-
const
|
|
445
|
+
const written = [...ctx.typeStructs.values()].filter((st) => typeWritten(st.name));
|
|
446
|
+
const files = [...new Set([...written.map((st) => ctx.typeFiles.get(st.name)), ...[...ctx.typeConsts.values()].map((c) => c.file)])].toSorted();
|
|
447
|
+
const structs = written
|
|
425
448
|
.map((st) => structSource(st, home, `/// \`${st.name}\` in \`${ctx.typeFiles.get(st.name)}\`.\n`))
|
|
426
449
|
.join("\n");
|
|
427
450
|
const consts = [...ctx.typeConsts.values()].map((c) => `${c.text}\n`).join("");
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** A key or option that still works and goes in the next major release. */
|
|
2
|
+
export interface Deprecation {
|
|
3
|
+
/** The version that deprecated it. */
|
|
4
|
+
since: string;
|
|
5
|
+
/** What to write instead, as the warning names it: `` `router.routes` ``. */
|
|
6
|
+
use: string;
|
|
7
|
+
}
|
|
8
|
+
/** The part of JSON Schema (draft-07) that describes `ferrovue.config.json`, with `deprecated` naming
|
|
9
|
+
* what replaces a key. Descriptions come from the TSDoc of `Config` when `schema.json` is written. */
|
|
10
|
+
export interface Schema {
|
|
11
|
+
type?: "string" | "boolean" | "integer" | "object" | "array";
|
|
12
|
+
enum?: readonly string[];
|
|
13
|
+
default?: unknown;
|
|
14
|
+
minimum?: number;
|
|
15
|
+
items?: Schema;
|
|
16
|
+
properties?: Record<string, Schema>;
|
|
17
|
+
required?: readonly string[];
|
|
18
|
+
additionalProperties?: false | Schema;
|
|
19
|
+
oneOf?: readonly Schema[];
|
|
20
|
+
deprecated?: Deprecation;
|
|
21
|
+
}
|
|
22
|
+
/** The types a helper or twin takes, as the configuration spells them. */
|
|
23
|
+
export declare const TYPE_NAMES: readonly string[];
|
|
24
|
+
/** What `ferrovue.config.json` may hold: the keys the compiler reads, their types and defaults. */
|
|
25
|
+
export declare const CONFIG_SCHEMA: Schema;
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const STRING = { type: "string" };
|
|
2
|
+
/** The types a helper or twin takes, as the configuration spells them. */
|
|
3
|
+
export const TYPE_NAMES = ["string", "string?", "int", "int?", "float", "float?", "bool"];
|
|
4
|
+
const TYPE_NAME = { type: "string", enum: TYPE_NAMES };
|
|
5
|
+
const ROUTES = { oneOf: [STRING, { type: "object", properties: { pages: STRING }, required: ["pages"], additionalProperties: false }] };
|
|
6
|
+
/** What `ferrovue.config.json` may hold: the keys the compiler reads, their types and defaults. */
|
|
7
|
+
export const CONFIG_SCHEMA = {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
$schema: STRING,
|
|
11
|
+
components: STRING,
|
|
12
|
+
out: STRING,
|
|
13
|
+
helpers: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
module: STRING,
|
|
17
|
+
functions: {
|
|
18
|
+
type: "object",
|
|
19
|
+
additionalProperties: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: { rust: STRING, params: { type: "array", items: TYPE_NAME }, returns: TYPE_NAME, maxLen: { type: "integer", minimum: 0, default: 0 } },
|
|
22
|
+
required: ["rust", "params", "returns"],
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ["module", "functions"],
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
},
|
|
30
|
+
twins: {
|
|
31
|
+
type: "object",
|
|
32
|
+
additionalProperties: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: { rust: STRING, props: { type: "object", additionalProperties: TYPE_NAME }, slots: { type: "array", items: STRING } },
|
|
35
|
+
required: ["rust"],
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
trustedHtml: STRING,
|
|
40
|
+
routes: ROUTES,
|
|
41
|
+
router: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
routes: ROUTES,
|
|
45
|
+
base: { type: "string", default: "" },
|
|
46
|
+
linkActiveClass: { type: "string", default: "router-link-active" },
|
|
47
|
+
linkExactActiveClass: { type: "string", default: "router-link-exact-active" },
|
|
48
|
+
},
|
|
49
|
+
required: ["routes"],
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
},
|
|
52
|
+
stores: STRING,
|
|
53
|
+
clientDirectives: { type: "array", items: STRING },
|
|
54
|
+
i18n: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: { messages: STRING, locale: { type: "string", default: "en" }, fallbackLocale: { oneOf: [STRING, { type: "array", items: STRING }] } },
|
|
57
|
+
required: ["messages"],
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
},
|
|
60
|
+
scopeId: { type: "string", enum: ["filepath", "filepath-source"], default: "filepath-source" },
|
|
61
|
+
viteRoot: { type: "string", default: "." },
|
|
62
|
+
isProduction: { type: "boolean", default: true },
|
|
63
|
+
cssModules: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: { generateScopedName: STRING, hashPrefix: { type: "string", default: "" }, context: STRING },
|
|
66
|
+
required: ["generateScopedName"],
|
|
67
|
+
additionalProperties: false,
|
|
68
|
+
},
|
|
69
|
+
builders: { type: "boolean", default: true },
|
|
70
|
+
},
|
|
71
|
+
required: ["components", "out"],
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
};
|
package/dist/script.js
CHANGED
|
@@ -3,8 +3,8 @@ import { fail, nothing, GenError, snake, takesAttrs, UNDEF } from "./model.js";
|
|
|
3
3
|
import { CONFIG_FILE, ctx } from "./context.js";
|
|
4
4
|
import { definePropsType, resolveImport, tyOfTs } from "./typescript.js";
|
|
5
5
|
import { constOfDecl, declareConsts } from "./constants.js";
|
|
6
|
-
import {
|
|
7
|
-
import { expr, fieldVal } from "./expr.js";
|
|
6
|
+
import { localTy } from "./rust.js";
|
|
7
|
+
import { expr, fieldVal, typedBy } from "./expr.js";
|
|
8
8
|
import { collected, heldList } from "./lists.js";
|
|
9
9
|
import { bare, operand, UNARY } from "./parens.js";
|
|
10
10
|
import { known } from "./narrowing.js";
|
|
@@ -53,8 +53,8 @@ export function setupSource(init) {
|
|
|
53
53
|
return setupSource(init.expression);
|
|
54
54
|
return init;
|
|
55
55
|
}
|
|
56
|
-
function
|
|
57
|
-
return
|
|
56
|
+
function statedTy(init) {
|
|
57
|
+
return init.type === "TSAsExpression" || init.type === "TSSatisfiesExpression" ? init.typeAnnotation : undefined;
|
|
58
58
|
}
|
|
59
59
|
function emptyRef(comp, init) {
|
|
60
60
|
if (init?.type !== "CallExpression" || init.callee.type !== "Identifier" || !["ref", "shallowRef"].includes(init.callee.name))
|
|
@@ -281,10 +281,12 @@ export function scopeFor(comp, ast, components) {
|
|
|
281
281
|
scope.propsIdent = local;
|
|
282
282
|
continue;
|
|
283
283
|
}
|
|
284
|
-
const source = setupSource(init0);
|
|
285
284
|
const isRef = init0.type === "CallExpression" && init0.callee.type === "Identifier" && ["ref", "shallowRef", "computed"].includes(init0.callee.name);
|
|
286
285
|
if (isRef)
|
|
287
286
|
scope.refs.add(local);
|
|
287
|
+
const typed = isRef ? init0.typeParameters?.params?.[0] : (d.id.typeAnnotation?.typeAnnotation ?? statedTy(init0));
|
|
288
|
+
const untyped = setupSource(init0);
|
|
289
|
+
const source = untyped && typed ? typedBy(untyped, typed) : untyped;
|
|
288
290
|
let v;
|
|
289
291
|
try {
|
|
290
292
|
const empty = emptyRef(comp, init0);
|
|
@@ -306,6 +308,11 @@ export function scopeFor(comp, ast, components) {
|
|
|
306
308
|
}
|
|
307
309
|
const name = `s_${snake(local).replace(/^r#/, "")}`;
|
|
308
310
|
const lone = v.lone ? { lone: true } : {};
|
|
311
|
+
if (v.slice) {
|
|
312
|
+
lets.push(`let ${name} = ${bare(v.code)};`);
|
|
313
|
+
scope.setup.set(local, { code: name, ty: v.ty, slice: true, ...lone });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
309
316
|
if (v.ty.k === "list" && v.iter !== undefined) {
|
|
310
317
|
lets.push(`let ${name} = ${collected(v)};`);
|
|
311
318
|
scope.setup.set(local, heldList(name, v.ty.of, v.lone));
|
package/dist/slots.js
CHANGED
package/dist/ssr.js
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { compileScript, compileTemplate, parse as parseSfc } from "@vue/compiler-sfc";
|
|
3
|
+
import { getEscapedCssVarName } from "@vue/shared";
|
|
3
4
|
import * as vue from "vue";
|
|
4
5
|
import * as serverRenderer from "vue/server-renderer";
|
|
6
|
+
const KEY = /(?:^|[{,])\s*"((?:[^"\\]|\\.)*)"\s*:/g;
|
|
7
|
+
function cssVarNames(file, component, count) {
|
|
8
|
+
const text = ["setup", "ssrRender"]
|
|
9
|
+
.map((k) => component[k])
|
|
10
|
+
.map((f) => (typeof f === "function" ? f.toString() : ""))
|
|
11
|
+
.join("\n");
|
|
12
|
+
const keys = (start) => {
|
|
13
|
+
const at = start.exec(text);
|
|
14
|
+
return at ? [...text.slice(at.index + at[0].length - 1).matchAll(KEY)].slice(0, count).map((m) => m[1]) : [];
|
|
15
|
+
};
|
|
16
|
+
const server = keys(/_cssVars = \{\s*style:\s*\{/);
|
|
17
|
+
if (server.length === count && server.every((n) => n.startsWith(":--")))
|
|
18
|
+
return server.map((n) => n.slice(3));
|
|
19
|
+
const client = keys(/useCssVars\)?\(\s*\(?_ctx\)?\s*=>\s*\(\{/);
|
|
20
|
+
if (client.length !== count) {
|
|
21
|
+
throw new Error(`${file}: \`v-bind()\` in \`<style>\`: the component holds none of the variables \`@vitejs/plugin-vue\` names; load it through the plugin`);
|
|
22
|
+
}
|
|
23
|
+
return client.map((k) => getEscapedCssVarName(JSON.parse(`"${k}"`), true));
|
|
24
|
+
}
|
|
5
25
|
/** Give a component compiled for the browser the `ssrRender` its SSR build would have. */
|
|
6
26
|
export function attachSsrRender(file, name, component) {
|
|
7
27
|
const { descriptor } = parseSfc(readFileSync(file, "utf8"), { filename: file });
|
|
@@ -10,20 +30,38 @@ export function attachSsrRender(file, name, component) {
|
|
|
10
30
|
if (!scopeId && descriptor.styles.some((st) => st.scoped)) {
|
|
11
31
|
throw new Error(`${file}: a \`<style scoped>\` component without \`__scopeId\`: load it through \`@vitejs/plugin-vue\``);
|
|
12
32
|
}
|
|
13
|
-
const
|
|
33
|
+
const varNames = descriptor.cssVars.length ? cssVarNames(file, component, descriptor.cssVars.length) : [];
|
|
34
|
+
let named = 0;
|
|
35
|
+
const code = compileTemplate({
|
|
14
36
|
source: descriptor.template.content,
|
|
15
37
|
filename: file,
|
|
16
38
|
id: scopeId ?? name,
|
|
17
39
|
scoped: !!scopeId,
|
|
18
40
|
slotted: descriptor.slotted,
|
|
19
41
|
ssr: true,
|
|
20
|
-
ssrCssVars:
|
|
21
|
-
|
|
22
|
-
|
|
42
|
+
ssrCssVars: descriptor.cssVars,
|
|
43
|
+
isProd: true,
|
|
44
|
+
compilerOptions: script?.bindings ? { bindingMetadata: script.bindings } : {},
|
|
45
|
+
}).code.replace(/^(\s*)":--[^"]*":/gm, (_, indent) => `${indent}":--${varNames[named++]}":`);
|
|
23
46
|
const body = code
|
|
24
47
|
.replace(/import \{([^}]*)\} from "vue"/g, (_, names) => `const {${names.replace(/ as /g, ": ")}} = __vue;`)
|
|
25
48
|
.replace(/import \{([^}]*)\} from "vue\/server-renderer"/g, (_, names) => `const {${names.replace(/ as /g, ": ")}} = __sr;`)
|
|
26
49
|
.replace("export function ssrRender", "return function ssrRender");
|
|
27
50
|
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
28
51
|
component.ssrRender = new Function("__vue", "__sr", body)(vue, serverRenderer);
|
|
52
|
+
const declared = /\n {2}const _cssVars = (\{ style: \{[^]*?\n\}\})\n/.exec(code)?.[1];
|
|
53
|
+
if (declared !== undefined)
|
|
54
|
+
exposeCssVars(component, body.slice(0, body.indexOf("return function ssrRender")), declared);
|
|
55
|
+
}
|
|
56
|
+
function exposeCssVars(component, imports, declared) {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
58
|
+
const vars = new Function("__vue", "__sr", `${imports}return (_ctx, $props, $setup) => (${declared}).style;`)(vue, serverRenderer);
|
|
59
|
+
const setup = component.setup;
|
|
60
|
+
component.setup = (props, ctx) => {
|
|
61
|
+
const instance = vue.getCurrentInstance();
|
|
62
|
+
if (instance && !instance.getCssVars) {
|
|
63
|
+
instance.getCssVars = () => Object.fromEntries(Object.entries(vars(instance.proxy, instance.props, instance.setupState)).map(([k, v]) => [k.slice(3).replace(/\\(.)/g, "$1"), v]));
|
|
64
|
+
}
|
|
65
|
+
return setup?.(props, ctx);
|
|
66
|
+
};
|
|
29
67
|
}
|
package/dist/styles.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { type N, type Scope } from "./model.ts";
|
|
1
|
+
import { type Component, type N, type Scope } from "./model.ts";
|
|
2
2
|
import { Emitter } from "./emitter.ts";
|
|
3
|
+
/** Put the object Vue's server compiler declares as `_cssVars`, the `v-bind()` variables in
|
|
4
|
+
* `<style>`, in place of each use of it in the compiled template's statements, with each value
|
|
5
|
+
* marked to be written as `ssrRenderStyle` writes a variable. */
|
|
6
|
+
export declare function inlineCssVars(comp: Component, body: N[]): N[];
|
|
3
7
|
export declare function mergedStyle(s: Scope, e: Emitter, n: N): void;
|
|
4
8
|
export declare function styleAttr(s: Scope, n: N): string;
|
|
5
9
|
export interface StyleItem {
|