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
package/dist/classes.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { escapeHtml } from "@vue/shared";
|
|
2
|
+
import { fail, rustStr, STR } from "./model.js";
|
|
3
|
+
import { expr } from "./expr.js";
|
|
4
|
+
import { cond, known, truthy } from "./narrowing.js";
|
|
5
|
+
import { meet } from "./strings.js";
|
|
6
|
+
import { atom, bare, condition, logical, not, strArg } from "./parens.js";
|
|
7
|
+
import { Emitter } from "./emitter.js";
|
|
8
|
+
import { valueAttr } from "./attrs.js";
|
|
9
|
+
export function classItems(s, n) {
|
|
10
|
+
switch (n.type) {
|
|
11
|
+
case "StringLiteral": {
|
|
12
|
+
const t = n.value.trim();
|
|
13
|
+
return t ? [{ lit: t }] : [];
|
|
14
|
+
}
|
|
15
|
+
case "ArrayExpression":
|
|
16
|
+
return n.elements.flatMap((el) => (el ? classItems(s, el) : []));
|
|
17
|
+
case "ObjectExpression":
|
|
18
|
+
const literalNames = n.properties
|
|
19
|
+
.filter((p) => p.type === "ObjectProperty" && !p.computed)
|
|
20
|
+
.map((p) => (p.key.type === "Identifier" ? p.key.name : String(p.key.value)));
|
|
21
|
+
const arrayIndex = (k) => /^(0|[1-9]\d*)$/.test(k) && Number(k) < 2 ** 32 - 1;
|
|
22
|
+
if (n.properties.some((p) => p.type === "ObjectProperty" && p.computed) ||
|
|
23
|
+
literalNames.some(arrayIndex) ||
|
|
24
|
+
new Set(literalNames).size !== literalNames.length) {
|
|
25
|
+
const keys = [];
|
|
26
|
+
const entries = n.properties.map((p) => {
|
|
27
|
+
if (p.type !== "ObjectProperty")
|
|
28
|
+
fail(s.comp, "a class object holds `name: condition` pairs", p);
|
|
29
|
+
const key = p.computed ? expr(s, p.key) : { code: rustStr(p.key.type === "Identifier" ? p.key.name : String(p.key.value)), ty: STR };
|
|
30
|
+
if (key.ty.k !== "str")
|
|
31
|
+
fail(s.comp, "a computed class name is a string", p.key);
|
|
32
|
+
for (const other of keys)
|
|
33
|
+
meet(s.comp, other, key, "a class object's names", p.key, "equal");
|
|
34
|
+
keys.push(key);
|
|
35
|
+
return `(${bare(cond(s, p.value))}, ${bare(key.code)})`;
|
|
36
|
+
});
|
|
37
|
+
return [{ code: `&*fv::class_object(&[${entries.join(", ")}])` }];
|
|
38
|
+
}
|
|
39
|
+
return n.properties.flatMap((p) => {
|
|
40
|
+
if (p.type !== "ObjectProperty")
|
|
41
|
+
fail(s.comp, "a class object holds `name: condition` pairs", p);
|
|
42
|
+
let name;
|
|
43
|
+
if (!p.computed) {
|
|
44
|
+
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
45
|
+
if (key !== key.trim() || !key)
|
|
46
|
+
fail(s.comp, `class name \`${key}\` has spaces around it`, p);
|
|
47
|
+
name = { lit: key };
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const k = expr(s, p.key);
|
|
51
|
+
if (k.ty.k !== "str")
|
|
52
|
+
fail(s.comp, "a computed class name is a string", p.key);
|
|
53
|
+
name = { code: `fv::js_trim(${strArg(k.code)})` };
|
|
54
|
+
}
|
|
55
|
+
const v = expr(s, p.value);
|
|
56
|
+
const on = known(v);
|
|
57
|
+
if (on !== undefined)
|
|
58
|
+
return on ? [name] : [];
|
|
59
|
+
const text = "lit" in name ? rustStr(name.lit) : name.code;
|
|
60
|
+
return [{ code: `if ${condition(truthy(v))} { ${bare(text)} } else { "" }` }];
|
|
61
|
+
});
|
|
62
|
+
case "LogicalExpression":
|
|
63
|
+
if (n.operator === "&&") {
|
|
64
|
+
return conditional(cond(s, n.left), classItems(s, n.right));
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
case "ConditionalExpression":
|
|
68
|
+
if (n.consequent.type === "NullLiteral" || n.alternate.type === "NullLiteral") {
|
|
69
|
+
const branch = n.consequent.type === "NullLiteral" ? n.alternate : n.consequent;
|
|
70
|
+
const test = cond(s, n.test);
|
|
71
|
+
return conditional(n.consequent.type === "NullLiteral" ? not(test) : test, classItems(s, branch));
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
const v = expr(s, n);
|
|
76
|
+
if (v.ty.k === "str")
|
|
77
|
+
return [{ code: v.code }];
|
|
78
|
+
if (v.ty.k === "opt" && v.ty.of.k === "str")
|
|
79
|
+
return [{ code: `${atom(v.code)}.unwrap_or("")` }];
|
|
80
|
+
if (v.ty.k === "undef")
|
|
81
|
+
return [];
|
|
82
|
+
return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
|
|
83
|
+
}
|
|
84
|
+
function conditional(test, items) {
|
|
85
|
+
if (test === "true" || test === "false")
|
|
86
|
+
return test === "true" ? items : [];
|
|
87
|
+
return items.map((it) => ({ code: `if ${condition(test)} { ${"lit" in it ? rustStr(it.lit) : bare(it.code)} } else { "" }` }));
|
|
88
|
+
}
|
|
89
|
+
export function renderClass(s, e, n, after = false) {
|
|
90
|
+
const items = classItems(s, n);
|
|
91
|
+
let wrote = after;
|
|
92
|
+
let i = 0;
|
|
93
|
+
for (; i < items.length; i++) {
|
|
94
|
+
const it = items[i];
|
|
95
|
+
if (!("lit" in it))
|
|
96
|
+
break;
|
|
97
|
+
e.lit((wrote ? " " : "") + escapeHtml(it.lit));
|
|
98
|
+
wrote = true;
|
|
99
|
+
}
|
|
100
|
+
if (i < items.length) {
|
|
101
|
+
const rest = items.slice(i).map((it) => ("lit" in it ? rustStr(it.lit) : it.code));
|
|
102
|
+
e.stmt(`fv::class_into(out, ${wrote}, &[${rest.join(", ")}]);`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function classPresent(s, n) {
|
|
106
|
+
if (n.fvMerged)
|
|
107
|
+
return n.elements.reduce((acc, el) => logical(acc, "||", classPresent(s, el)), "false");
|
|
108
|
+
switch (n.type) {
|
|
109
|
+
case "StringLiteral":
|
|
110
|
+
case "NullLiteral":
|
|
111
|
+
case "ArrayExpression":
|
|
112
|
+
case "ObjectExpression":
|
|
113
|
+
return "true";
|
|
114
|
+
case "LogicalExpression":
|
|
115
|
+
if (n.operator === "&&")
|
|
116
|
+
return logical(not(cond(s, n.left)), "||", classPresent(s, n.right));
|
|
117
|
+
break;
|
|
118
|
+
case "ConditionalExpression":
|
|
119
|
+
if (n.consequent.type === "NullLiteral")
|
|
120
|
+
return logical(cond(s, n.test), "||", classPresent(s, n.alternate));
|
|
121
|
+
if (n.alternate.type === "NullLiteral")
|
|
122
|
+
return logical(not(cond(s, n.test)), "||", classPresent(s, n.consequent));
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
const v = expr(s, n);
|
|
126
|
+
if (v.ty.k === "undef")
|
|
127
|
+
return "false";
|
|
128
|
+
if (v.ty.k === "opt")
|
|
129
|
+
return `${atom(v.code)}.is_some()`;
|
|
130
|
+
return "true";
|
|
131
|
+
}
|
|
132
|
+
export function maybeEqual(s, n) {
|
|
133
|
+
if (n.type === "StringLiteral")
|
|
134
|
+
return true;
|
|
135
|
+
if (n.fvMerged || ["ArrayExpression", "ObjectExpression", "NullLiteral", "LogicalExpression", "ConditionalExpression"].includes(n.type))
|
|
136
|
+
return false;
|
|
137
|
+
const v = expr(s, n);
|
|
138
|
+
return v.ty.k === "str" || (v.ty.k === "opt" && v.ty.of.k === "str");
|
|
139
|
+
}
|
|
140
|
+
export function literalClass(n) {
|
|
141
|
+
if (n.type === "StringLiteral")
|
|
142
|
+
return n.value.trim();
|
|
143
|
+
if (!n.fvMerged)
|
|
144
|
+
return null;
|
|
145
|
+
const parts = n.elements.map(literalClass);
|
|
146
|
+
return parts.includes(null) ? null : parts.filter(Boolean).join(" ");
|
|
147
|
+
}
|
|
148
|
+
export function classAttr(s, n, side) {
|
|
149
|
+
switch (n.type) {
|
|
150
|
+
case "StringLiteral":
|
|
151
|
+
return `fv::Attr::str(${rustStr(n.value)})`;
|
|
152
|
+
case "NullLiteral":
|
|
153
|
+
return 'fv::Attr::str("")';
|
|
154
|
+
case "ArrayExpression":
|
|
155
|
+
case "ObjectExpression": {
|
|
156
|
+
const items = classItems(s, n);
|
|
157
|
+
const names = items.every((it) => "lit" in it)
|
|
158
|
+
? rustStr(items.map((it) => ("lit" in it ? it.lit : "")).join(" "))
|
|
159
|
+
: `fv::class_names(&[${items.map((it) => ("lit" in it ? rustStr(it.lit) : it.code)).join(", ")}])`;
|
|
160
|
+
const literal = names.startsWith('"');
|
|
161
|
+
if (side === "vnode")
|
|
162
|
+
return literal ? `fv::Attr::str(${names})` : `fv::Attr::from(${names})`;
|
|
163
|
+
return `fv::Attr::Names(${literal ? `std::borrow::Cow::Borrowed(${names})` : `${names}.into()`})`;
|
|
164
|
+
}
|
|
165
|
+
case "LogicalExpression":
|
|
166
|
+
if (n.operator === "&&")
|
|
167
|
+
return `if ${condition(cond(s, n.left))} { ${classAttr(s, n.right, side)} } else { fv::Attr::str("") }`;
|
|
168
|
+
break;
|
|
169
|
+
case "ConditionalExpression":
|
|
170
|
+
return `if ${condition(cond(s, n.test))} { ${classAttr(s, n.consequent, side)} } else { ${classAttr(s, n.alternate, side)} }`;
|
|
171
|
+
}
|
|
172
|
+
const v = expr(s, n);
|
|
173
|
+
if (v.ty.k === "str" || v.ty.k === "undef" || (v.ty.k === "opt" && v.ty.of.k === "str"))
|
|
174
|
+
return valueAttr(s, v, n);
|
|
175
|
+
return fail(s.comp, "a class is a string, an array, or an object of conditions", n);
|
|
176
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,65 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* ferrovue --watch write them, then again whenever a component, store, type file, the routes
|
|
7
|
-
* or the configuration changes, until interrupted */
|
|
8
|
-
import { readFileSync, readdirSync, watch } from "node:fs";
|
|
9
|
-
import { join, relative, sep } from "node:path";
|
|
10
|
-
import { GenError, generate, loadConfig, write } from "./compiler.js";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, watch, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { CONFIG_FILE, GenError, generate, loadConfig, VERSION, write } from "./compiler.js";
|
|
5
|
+
import { unifiedDiff } from "./diff.js";
|
|
11
6
|
const root = process.cwd();
|
|
12
|
-
|
|
7
|
+
const HELP = `Usage: ferrovue [command] [options]
|
|
8
|
+
|
|
9
|
+
Compile Vue components to Rust render functions, run from the project root.
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
init scaffold starter ferrovue.config.json and components
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--check check that generated files match committed files without writing
|
|
16
|
+
-d, --diff show unified diff of changes when checking (use with --check)
|
|
17
|
+
--watch watch components, stores, routes and config for changes and regenerate
|
|
18
|
+
-c, --config <path> path to configuration file (default: ferrovue.config.json)
|
|
19
|
+
-v, --version print the version and exit
|
|
20
|
+
-h, --help print this help and exit
|
|
21
|
+
`;
|
|
13
22
|
function report(e) {
|
|
14
23
|
if (e instanceof GenError || e instanceof SyntaxError)
|
|
15
24
|
console.error(`error: ${e.message}`);
|
|
16
25
|
else
|
|
17
26
|
throw e;
|
|
18
27
|
}
|
|
19
|
-
function
|
|
28
|
+
function initProject(targetRoot, configPath) {
|
|
29
|
+
const configFile = configPath ? resolve(targetRoot, configPath) : join(targetRoot, CONFIG_FILE);
|
|
30
|
+
if (existsSync(configFile)) {
|
|
31
|
+
console.error(`error: configuration file already exists: ${relative(targetRoot, configFile) || CONFIG_FILE}`);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
const defaultComponents = "components";
|
|
35
|
+
const defaultOut = "src/generated";
|
|
36
|
+
mkdirSync(dirname(configFile), { recursive: true });
|
|
37
|
+
const config = {
|
|
38
|
+
components: defaultComponents,
|
|
39
|
+
out: defaultOut,
|
|
40
|
+
};
|
|
41
|
+
writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n");
|
|
42
|
+
console.log(`created ${relative(targetRoot, configFile) || CONFIG_FILE}`);
|
|
43
|
+
const compDir = join(targetRoot, defaultComponents);
|
|
44
|
+
mkdirSync(compDir, { recursive: true });
|
|
45
|
+
const helloVue = join(compDir, "Hello.vue");
|
|
46
|
+
if (!existsSync(helloVue)) {
|
|
47
|
+
writeFileSync(helloVue, `<script setup lang="ts">
|
|
48
|
+
defineProps<{ name: string }>();
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<p>Hello, {{ name }}!</p>
|
|
53
|
+
</template>
|
|
54
|
+
`);
|
|
55
|
+
console.log(`created ${join(defaultComponents, "Hello.vue")}`);
|
|
56
|
+
}
|
|
57
|
+
const outDir = join(targetRoot, defaultOut);
|
|
58
|
+
mkdirSync(outDir, { recursive: true });
|
|
59
|
+
console.log("\nNext steps:\n 1. Run `ferrovue` to generate Rust render functions into src/generated\n 2. In your Rust crate, declare `mod generated;` to use them");
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
function check(config, showDiff = false) {
|
|
20
63
|
const want = generate(root, config);
|
|
21
64
|
const dir = join(root, config.out);
|
|
22
65
|
let have = [];
|
|
@@ -24,7 +67,6 @@ function check(config) {
|
|
|
24
67
|
have = readdirSync(dir).filter((f) => f.endsWith(".rs"));
|
|
25
68
|
}
|
|
26
69
|
catch {
|
|
27
|
-
// An absent directory is simply stale.
|
|
28
70
|
}
|
|
29
71
|
const stale = [...want.keys()].filter((name) => {
|
|
30
72
|
try {
|
|
@@ -36,10 +78,24 @@ function check(config) {
|
|
|
36
78
|
});
|
|
37
79
|
const extra = have.filter((name) => !want.has(name));
|
|
38
80
|
if (stale.length || extra.length) {
|
|
39
|
-
|
|
81
|
+
const committed = (name) => {
|
|
82
|
+
try {
|
|
83
|
+
return readFileSync(join(dir, name), "utf8");
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
for (const name of stale) {
|
|
40
90
|
console.error(`stale: ${config.out}/${name}`);
|
|
41
|
-
|
|
91
|
+
if (showDiff)
|
|
92
|
+
console.error(unifiedDiff(`${config.out}/${name}`, committed(name), want.get(name)));
|
|
93
|
+
}
|
|
94
|
+
for (const name of extra) {
|
|
42
95
|
console.error(`not generated: ${config.out}/${name}`);
|
|
96
|
+
if (showDiff)
|
|
97
|
+
console.error(unifiedDiff(`${config.out}/${name}`, committed(name), ""));
|
|
98
|
+
}
|
|
43
99
|
console.error("run `ferrovue` to regenerate");
|
|
44
100
|
return 1;
|
|
45
101
|
}
|
|
@@ -51,12 +107,10 @@ function once(config) {
|
|
|
51
107
|
const what = changed.length || removed.length ? `${changed.length} changed, ${removed.length} removed` : "nothing changed";
|
|
52
108
|
console.log(`${config.out}: ${files.length} files, ${what}`);
|
|
53
109
|
}
|
|
54
|
-
|
|
55
|
-
* carrying on. The output directory itself, dependencies and build output are ignored. */
|
|
56
|
-
function watchProject() {
|
|
110
|
+
function watchProject(configPath) {
|
|
57
111
|
const run = () => {
|
|
58
112
|
try {
|
|
59
|
-
once(loadConfig(root));
|
|
113
|
+
once(loadConfig(root, configPath));
|
|
60
114
|
}
|
|
61
115
|
catch (e) {
|
|
62
116
|
report(e);
|
|
@@ -66,10 +120,9 @@ function watchProject() {
|
|
|
66
120
|
const ignored = (file) => {
|
|
67
121
|
let out = "";
|
|
68
122
|
try {
|
|
69
|
-
out = loadConfig(root).out;
|
|
123
|
+
out = loadConfig(root, configPath).out;
|
|
70
124
|
}
|
|
71
125
|
catch {
|
|
72
|
-
// A broken configuration ignores nothing; the next run reports it.
|
|
73
126
|
}
|
|
74
127
|
const parts = file.split(sep);
|
|
75
128
|
return (parts.some((p) => p === "node_modules" || p === "target" || p === ".git" || p === "dist") ||
|
|
@@ -80,7 +133,6 @@ function watchProject() {
|
|
|
80
133
|
watch(root, { recursive: true }, (_event, name) => {
|
|
81
134
|
if (!name || ignored(relative(root, join(root, name))))
|
|
82
135
|
return;
|
|
83
|
-
// Editors write a file in several steps: one run once they are done.
|
|
84
136
|
if (timer)
|
|
85
137
|
clearTimeout(timer);
|
|
86
138
|
timer = setTimeout(run, 50);
|
|
@@ -88,12 +140,54 @@ function watchProject() {
|
|
|
88
140
|
console.log("watching for changes (Ctrl-C to stop)");
|
|
89
141
|
}
|
|
90
142
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
143
|
+
const rawArgs = process.argv.slice(2);
|
|
144
|
+
let configPath;
|
|
145
|
+
const args = [];
|
|
146
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
147
|
+
const arg = rawArgs[i];
|
|
148
|
+
if (arg === "-c" || arg === "--config") {
|
|
149
|
+
if (i + 1 >= rawArgs.length || rawArgs[i + 1].startsWith("-")) {
|
|
150
|
+
console.error(`error: option '${arg}' requires an argument`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
configPath = rawArgs[++i];
|
|
154
|
+
}
|
|
155
|
+
else if (arg.startsWith("--config=")) {
|
|
156
|
+
configPath = arg.slice("--config=".length);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
args.push(arg);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const known = new Set(["init", "--check", "--diff", "-d", "--watch", "-v", "--version", "-h", "--help"]);
|
|
163
|
+
const unknown = args.find((a) => !known.has(a));
|
|
164
|
+
if (unknown !== undefined) {
|
|
165
|
+
console.error(`error: unknown option or command '${unknown}'\n\nRun \`ferrovue --help\` for usage.`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
if ((args.includes("--diff") || args.includes("-d")) && !args.includes("--check")) {
|
|
169
|
+
console.error("error: '--diff' requires '--check'");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
173
|
+
console.log(VERSION);
|
|
174
|
+
}
|
|
175
|
+
else if (args.includes("--help") || args.includes("-h")) {
|
|
176
|
+
console.log(HELP.trimEnd());
|
|
177
|
+
}
|
|
178
|
+
else if (args.includes("init")) {
|
|
179
|
+
process.exitCode = initProject(root, configPath);
|
|
180
|
+
}
|
|
181
|
+
else if (args.includes("--watch")) {
|
|
182
|
+
watchProject(configPath);
|
|
183
|
+
}
|
|
184
|
+
else if (args.includes("--check")) {
|
|
185
|
+
const showDiff = args.includes("--diff") || args.includes("-d");
|
|
186
|
+
process.exitCode = check(loadConfig(root, configPath), showDiff);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
once(loadConfig(root, configPath));
|
|
190
|
+
}
|
|
97
191
|
}
|
|
98
192
|
catch (e) {
|
|
99
193
|
report(e);
|
package/dist/client.d.ts
CHANGED
|
@@ -18,6 +18,14 @@ export interface MountOptions {
|
|
|
18
18
|
* those given or its props are malformed. Warns on the console by default. */
|
|
19
19
|
onError?: (element: Element, problem: string) => void;
|
|
20
20
|
}
|
|
21
|
+
/** A component, or a function that loads one — `() => import("./Counter.vue")` — so that the code
|
|
22
|
+
* of an island not on the page is never fetched. A function is taken for a loader as vue-router
|
|
23
|
+
* takes one for a lazy route: unless it has `props` or `displayName`, which mark a functional
|
|
24
|
+
* component, or `__vccOpts`, a class component. `ferrovue/islands` maps the name of every island
|
|
25
|
+
* to a loader. */
|
|
26
|
+
export type IslandComponent = Component | (() => Promise<Component | {
|
|
27
|
+
default: Component;
|
|
28
|
+
}>);
|
|
21
29
|
/** The islands mounted. */
|
|
22
30
|
export interface Islands {
|
|
23
31
|
/** One app per island, in document order. */
|
|
@@ -27,5 +35,6 @@ export interface Islands {
|
|
|
27
35
|
}
|
|
28
36
|
/** Hydrate every island on the page: each `<div data-island="Name" data-props="…">` that
|
|
29
37
|
* `Html::island` wrote becomes an app of the component of that name, given the props the server
|
|
30
|
-
* rendered it with, mounted where it is.
|
|
31
|
-
|
|
38
|
+
* rendered it with, mounted where it is. A loader is called only for an island the page holds, all
|
|
39
|
+
* of them at once, and the islands mount in document order once every one has loaded. */
|
|
40
|
+
export declare function mountIslands(components: Record<string, IslandComponent>, options?: MountOptions): Promise<Islands>;
|
package/dist/client.js
CHANGED
|
@@ -1,31 +1,56 @@
|
|
|
1
|
-
/* What the browser does with a page ferrovue rendered. */
|
|
2
1
|
import { createSSRApp } from "vue";
|
|
2
|
+
const BARE = /"(?:[^"\\]|\\.)*"|-?Infinity|NaN/g;
|
|
3
|
+
const NON_FINITE = { NaN: Number.NaN, Infinity: Number.POSITIVE_INFINITY, "-Infinity": Number.NEGATIVE_INFINITY };
|
|
4
|
+
function parseJson(text) {
|
|
5
|
+
if (!/NaN|Infinity/.test(text))
|
|
6
|
+
return JSON.parse(text);
|
|
7
|
+
let escaped = "\\u0000";
|
|
8
|
+
while (text.includes(escaped))
|
|
9
|
+
escaped += "\\u0000";
|
|
10
|
+
const tag = JSON.parse(`"${escaped}"`);
|
|
11
|
+
const quoted = text.replace(BARE, (match) => (match.startsWith('"') ? match : `"${escaped}${match}"`));
|
|
12
|
+
return JSON.parse(quoted, (_key, value) => (typeof value === "string" && value.startsWith(tag) ? NON_FINITE[value.slice(tag.length)] : value));
|
|
13
|
+
}
|
|
3
14
|
/** Give Pinia the state the server rendered with — what `ferrovue::state_script_into` wrote — before
|
|
4
15
|
* the app mounts, so every store starts from it and the hydrated markup agrees with the server's. */
|
|
5
16
|
export function hydrateState(pinia, id = "__pinia", doc = document) {
|
|
6
17
|
const text = doc.getElementById(id)?.textContent;
|
|
7
18
|
if (text)
|
|
8
|
-
pinia.state.value =
|
|
19
|
+
pinia.state.value = parseJson(text);
|
|
9
20
|
}
|
|
10
21
|
/** Hydrate every island on the page: each `<div data-island="Name" data-props="…">` that
|
|
11
22
|
* `Html::island` wrote becomes an app of the component of that name, given the props the server
|
|
12
|
-
* rendered it with, mounted where it is.
|
|
23
|
+
* rendered it with, mounted where it is. A loader is called only for an island the page holds, all
|
|
24
|
+
* of them at once, and the islands mount in document order once every one has loaded. */
|
|
13
25
|
export async function mountIslands(components, options = {}) {
|
|
14
26
|
const root = options.root ?? document;
|
|
15
27
|
const report = options.onError ?? ((el, problem) => console.warn(`[ferrovue] island left unhydrated: ${problem}`, el));
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
const elements = Array.from(root.querySelectorAll("[data-island]"));
|
|
29
|
+
const loading = new Map();
|
|
30
|
+
for (const el of elements) {
|
|
31
|
+
const name = el.dataset.island ?? "";
|
|
32
|
+
if (Object.hasOwn(components, name) && !loading.has(name))
|
|
33
|
+
loading.set(name, load(components[name]));
|
|
34
|
+
}
|
|
35
|
+
const [loaded] = await Promise.all([
|
|
36
|
+
Promise.all([...loading].map(async ([name, pending]) => [name, await pending])).then((pairs) => new Map(pairs)),
|
|
37
|
+
options.router?.isReady(),
|
|
38
|
+
]);
|
|
18
39
|
const apps = [];
|
|
19
|
-
for (const el of
|
|
40
|
+
for (const el of elements) {
|
|
20
41
|
const name = el.dataset.island ?? "";
|
|
21
|
-
const component =
|
|
42
|
+
const component = loaded.get(name);
|
|
22
43
|
if (!component) {
|
|
23
|
-
report(el, `no component called ${JSON.stringify(name)} was given`);
|
|
44
|
+
report(el, `no component called ${JSON.stringify(name)} was given (\`ferrovue/islands\` holds every component that has an \`island()\`)`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (component instanceof Error) {
|
|
48
|
+
report(el, `${name} did not load: ${component.message}`);
|
|
24
49
|
continue;
|
|
25
50
|
}
|
|
26
51
|
let props;
|
|
27
52
|
try {
|
|
28
|
-
props =
|
|
53
|
+
props = parseJson(el.dataset.props ?? "{}");
|
|
29
54
|
}
|
|
30
55
|
catch {
|
|
31
56
|
report(el, `the props of ${name} are not JSON`);
|
|
@@ -49,3 +74,14 @@ export async function mountIslands(components, options = {}) {
|
|
|
49
74
|
},
|
|
50
75
|
};
|
|
51
76
|
}
|
|
77
|
+
async function load(component) {
|
|
78
|
+
if (typeof component !== "function" || "props" in component || "displayName" in component || "__vccOpts" in component)
|
|
79
|
+
return component;
|
|
80
|
+
try {
|
|
81
|
+
const loaded = await component();
|
|
82
|
+
return typeof loaded === "object" && "default" in loaded ? loaded.default : loaded;
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
return e instanceof Error ? e : new Error(String(e));
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/compiler.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export interface Written {
|
|
|
10
10
|
changed: string[];
|
|
11
11
|
/** The files removed, which the components no longer produce. */
|
|
12
12
|
removed: string[];
|
|
13
|
+
/** Every component that has an `island()`, by the name `data-island` carries, with its `.vue`
|
|
14
|
+
* file relative to the root: what `ferrovue/islands` loads. */
|
|
15
|
+
islands: Record<string, string>;
|
|
13
16
|
}
|
|
14
17
|
/** Write what \`generate\` produces to the configured directory: only the files whose text changed,
|
|
15
18
|
* so a Rust build that watches them rebuilds no more than it must, and removing what no component
|
|
@@ -17,3 +20,4 @@ export interface Written {
|
|
|
17
20
|
export declare function write(root: string, config?: Config): Written;
|
|
18
21
|
export { CONFIG_FILE, loadConfig, TYPES_MODULE, type Config, type HelperSpec, type TypeName } from "./context.ts";
|
|
19
22
|
export { GenError } from "./model.ts";
|
|
23
|
+
export declare const VERSION: string;
|