ferrovue 0.2.0 → 0.3.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 +73 -4
- package/dist/attrs.d.ts +21 -5
- package/dist/attrs.js +305 -24
- package/dist/cli.js +131 -20
- package/dist/client.d.ts +11 -2
- package/dist/client.js +56 -8
- package/dist/compiler.d.ts +4 -0
- package/dist/compiler.js +17 -5
- package/dist/component.js +13 -0
- package/dist/context.d.ts +2 -2
- package/dist/context.js +23 -5
- package/dist/diff.d.ts +4 -0
- package/dist/diff.js +77 -0
- package/dist/expr.js +39 -1
- package/dist/islands.d.ts +5 -0
- package/dist/islands.js +4 -0
- package/dist/model.d.ts +21 -0
- package/dist/model.js +13 -0
- package/dist/router.js +52 -2
- package/dist/rust.d.ts +3 -0
- package/dist/rust.js +37 -10
- package/dist/scoped.d.ts +15 -1
- package/dist/scoped.js +88 -1
- package/dist/script.js +18 -2
- package/dist/stores.js +2 -0
- package/dist/template.d.ts +8 -5
- package/dist/template.js +131 -58
- package/dist/vite.js +45 -2
- package/package.json +5 -1
package/dist/template.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/* The compiled template's statements: pushes, conditions, lists, child components and slots. */
|
|
2
|
-
import { fail, GenError, INT, rustStr, sameTy, snake, STR } from "./model.js";
|
|
2
|
+
import { camelize, declares, fail, GenError, INT, rustStr, sameTy, snake, STR, takesAttrs } from "./model.js";
|
|
3
3
|
import { ctx } from "./context.js";
|
|
4
4
|
import { markHome } from "./typescript.js";
|
|
5
5
|
import { asF64, boolOf, cond, expr, fieldVal, isObjectCall, known, narrowTo, presence, truthy } from "./expr.js";
|
|
6
6
|
import { atom, bare, CMP, condition, occurrences, operand, OR, strArg, UNARY } from "./parens.js";
|
|
7
7
|
import { Emitter } from "./emitter.js";
|
|
8
|
-
import { IGNORED_PROPS, interpolate, renderAttr, renderAttrs, renderClass, renderDynamicAttr, renderStyle } from "./attrs.js";
|
|
8
|
+
import { attrOf, dollarAttrs, IGNORED_PROPS, interpolate, isAttrs, mergedParts, renderAttr, renderAttrs, renderClass, renderDynamicAttr, renderStyle } from "./attrs.js";
|
|
9
|
+
import { passedKey } from "./scoped.js";
|
|
10
|
+
import { isSSRSafeAttrName, propsToAttrMap } from "@vue/shared";
|
|
9
11
|
import { routerLink } from "./router.js";
|
|
10
12
|
import { rustTy } from "./rust.js";
|
|
11
13
|
/** One `${...}` inside a pushed template literal. */
|
|
@@ -26,7 +28,7 @@ export function slot(s, e, n) {
|
|
|
26
28
|
case "_ssrRenderAttr":
|
|
27
29
|
if (a[0].type !== "StringLiteral")
|
|
28
30
|
fail(s.comp, "attribute names are literal", n);
|
|
29
|
-
renderAttr(e, a[0].value, expr(s, a[1]));
|
|
31
|
+
renderAttr(s, e, a[0].value, expr(s, a[1]), a[1]);
|
|
30
32
|
return;
|
|
31
33
|
// `:hidden`, whose rendering depends on the value's type: Vue cannot decide it at compile time.
|
|
32
34
|
case "_ssrRenderDynamicAttr":
|
|
@@ -87,24 +89,51 @@ export function isComment(text) {
|
|
|
87
89
|
return false;
|
|
88
90
|
return text.length <= 8 || !text.replace(/<!--[^]*?-->/gm, "").trim();
|
|
89
91
|
}
|
|
90
|
-
/** Whether a `_push` argument is content to `ssrRenderSlot
|
|
91
|
-
|
|
92
|
+
/** Whether a `_push` argument is content to `ssrRenderSlot` rather than only comments — `"run"`
|
|
93
|
+
* when that depends on the values it interpolates (`${of1}<!--[-->` is a comment when `of1` writes
|
|
94
|
+
* nothing). Vue asks it of each pushed string, so the generated code asks `fv::is_comment` of what
|
|
95
|
+
* that push wrote. */
|
|
96
|
+
export function pushesContent(n) {
|
|
92
97
|
if (n.type === "StringLiteral")
|
|
93
98
|
return !isComment(n.value);
|
|
94
99
|
if (n.type === "TemplateLiteral") {
|
|
100
|
+
const quasis = n.quasis.map((q) => q.value.cooked);
|
|
95
101
|
if (n.expressions.length === 0)
|
|
96
|
-
return !isComment(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
return !isComment(quasis[0]);
|
|
103
|
+
// A comment starts with `<!--` and ends with `-->`: literal text that cannot decides it now.
|
|
104
|
+
const first = quasis[0];
|
|
105
|
+
const last = quasis.at(-1);
|
|
106
|
+
if (!"<!--".startsWith(first) && !first.startsWith("<!--"))
|
|
107
|
+
return true;
|
|
108
|
+
if (!"-->".endsWith(last) && !last.endsWith("-->"))
|
|
109
|
+
return true;
|
|
110
|
+
/* An interpolated value is escaped, so it writes no `<` or `>`: every comment starts and ends in
|
|
111
|
+
* the literal text — unless a value is raw (`v-html`), or a marker could be split across text
|
|
112
|
+
* and a value. Then literal text left over once the comments are taken out is content, whatever
|
|
113
|
+
* the values write. */
|
|
114
|
+
const raw = n.expressions.some((x) => x.type !== "CallExpression" && !(x.type === "Identifier" && x.name === "_scopeId"));
|
|
115
|
+
const split = quasis.some((q, i) => (i > 0 && /^(?:-|--|!--|->|>)/.test(q)) || (i < quasis.length - 1 && /(?:<|<!|<!-|-|--)$/.test(q)));
|
|
116
|
+
if (!raw && !split && /\S/.test(quasis.join("\0").replace(/<!--[^]*?-->/g, "").replaceAll("\0", "")))
|
|
117
|
+
return true;
|
|
118
|
+
return "run";
|
|
101
119
|
}
|
|
102
120
|
// A component's render is a buffer, never a comment.
|
|
103
121
|
return true;
|
|
104
122
|
}
|
|
105
123
|
export function push(s, e, n) {
|
|
106
|
-
|
|
124
|
+
const content = s.fill ? pushesContent(n) : false;
|
|
125
|
+
if (content === true)
|
|
107
126
|
e.stmt("filled = true;");
|
|
127
|
+
if (content !== "run") {
|
|
128
|
+
pushed(s, e, n);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
e.stmt("let fv_chunk = out.len();");
|
|
132
|
+
pushed(s, e, n);
|
|
133
|
+
e.stmt("filled |= !fv::is_comment(&out[fv_chunk..]);");
|
|
134
|
+
}
|
|
135
|
+
/** What one `_push` writes. */
|
|
136
|
+
function pushed(s, e, n) {
|
|
108
137
|
const text = n.type === "StringLiteral" ? n.value : n.type === "TemplateLiteral" && !n.expressions.length ? n.quasis[0].value.cooked : null;
|
|
109
138
|
if (s.vnode && text === "<!---->") {
|
|
110
139
|
e.lit("<!--v-if-->");
|
|
@@ -165,9 +194,6 @@ function childAttrs(s, child, passesAttrs, inSlot, n) {
|
|
|
165
194
|
}
|
|
166
195
|
export function renderChild(s, e, n) {
|
|
167
196
|
const [target, rawProps, slots, , slotScopeId] = n.arguments;
|
|
168
|
-
// The parent's `_attrs`, which it passes on to a child that is its root.
|
|
169
|
-
const isAttrs = (a) => a?.type === "Identifier" && a.name === "_attrs";
|
|
170
|
-
const passesAttrs = isAttrs(rawProps) || (rawProps?.type === "CallExpression" && rawProps.arguments.some(isAttrs));
|
|
171
197
|
let local = null;
|
|
172
198
|
if (target.type === "MemberExpression" && target.object.name === "$setup") {
|
|
173
199
|
local = target.computed ? target.property.value : target.property.name;
|
|
@@ -175,56 +201,67 @@ export function renderChild(s, e, n) {
|
|
|
175
201
|
const isSelf = target.type === "Identifier" && target.name === s.selfAlias.name;
|
|
176
202
|
const childName = isSelf ? s.comp.name : local ? s.children.get(local) : undefined;
|
|
177
203
|
const child = childName ? s.components.get(childName) : undefined;
|
|
178
|
-
// Inside a `v-for`, Vue merges a `{ ref_for: true }` marker into the props; it renders nothing.
|
|
179
|
-
let props = rawProps;
|
|
180
|
-
if (props?.type === "CallExpression" && props.callee.type === "Identifier" && props.callee.name === "_mergeProps" &&
|
|
181
|
-
props.arguments.length === 2 && props.arguments[0].type === "ObjectExpression" &&
|
|
182
|
-
props.arguments[0].properties.every((p) => p.type === "ObjectProperty" && IGNORED_PROPS.has(p.key.name ?? p.key.value))) {
|
|
183
|
-
props = props.arguments[1];
|
|
184
|
-
}
|
|
185
|
-
// A component at the root of the template is handed the fallthrough attributes, which an island
|
|
186
|
-
// never has.
|
|
187
|
-
if (props?.type === "CallExpression" && props.callee.type === "Identifier" && props.callee.name === "_mergeProps" &&
|
|
188
|
-
props.arguments.length === 2 && props.arguments[1].type === "Identifier" && props.arguments[1].name === "_attrs") {
|
|
189
|
-
props = props.arguments[0];
|
|
190
|
-
}
|
|
191
|
-
// A child given no props at all: Vue passes `null`, or the empty fallthrough attributes.
|
|
192
|
-
if ((props?.type === "Identifier" && props.name === "_attrs") || props?.type === "NullLiteral" || !props) {
|
|
193
|
-
props = { type: "ObjectExpression", properties: [] };
|
|
194
|
-
}
|
|
195
204
|
if (!child)
|
|
196
205
|
fail(s.comp, "a child component must be an imported island", n);
|
|
197
206
|
if (child.routerView) {
|
|
198
207
|
fail(s.comp, `${child.name} holds \`<RouterView>\`: the server renders it at the top, never as a child`, n);
|
|
199
208
|
}
|
|
209
|
+
// What the props merge: object literals — a `{ ref_for: true }` marker inside a `v-for` among
|
|
210
|
+
// them, which renders nothing — the parent's `_attrs` when the child is its root, and `$attrs`.
|
|
211
|
+
const parts = !rawProps || rawProps.type === "NullLiteral" ? [] : mergedParts(rawProps);
|
|
212
|
+
const merges = parts.length > 0 && parts[0] !== rawProps;
|
|
213
|
+
const passesAttrs = parts.some(isAttrs);
|
|
214
|
+
const passed = (p) => s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p));
|
|
215
|
+
const marker = (p) => p.type === "ObjectExpression" && p.properties.length > 0 && p.properties.every((q) => q.type === "ObjectProperty" && IGNORED_PROPS.has(q.key.name ?? q.key.value));
|
|
216
|
+
const objects = parts.filter((p) => !isAttrs(p) && !dollarAttrs(s, p) && !marker(p));
|
|
217
|
+
// Attributes this component is passed reach the child's props too, where a prop would take them.
|
|
218
|
+
if (parts.some(passed)) {
|
|
219
|
+
for (const name of s.comp.attrNames) {
|
|
220
|
+
if (declares(child, name)) {
|
|
221
|
+
fail(s.comp, `\`${name}\`, an attribute ${s.comp.name} may be passed, would reach ${child.name} as its prop \`${camelize(name)}\`, which an attribute has no type for`, n);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const ids = childAttrs(s, child, passesAttrs, !!slotScopeId, n);
|
|
200
226
|
// `v-bind="x"`, where `x` is exactly the child's own `Props`: handed over as it is.
|
|
201
|
-
if (
|
|
202
|
-
const v = expr(s,
|
|
227
|
+
if (objects.length === 1 && objects[0].type !== "ObjectExpression") {
|
|
228
|
+
const v = expr(s, objects[0]);
|
|
203
229
|
// The component itself, however the template reached it: its own `Props` struct is the type.
|
|
204
230
|
const own = child.name === s.comp.name && v.ty.k === "struct" && v.ty.name === "Props";
|
|
205
231
|
if (own || (v.ty.k === "child" && v.ty.name === child.name)) {
|
|
206
|
-
callChild(s, e, child, v.code, slots,
|
|
232
|
+
callChild(s, e, child, v.code, slots, childAttrsArg(s, child, parts, merges, null, ids));
|
|
207
233
|
return;
|
|
208
234
|
}
|
|
209
235
|
fail(s.comp, `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, n);
|
|
210
236
|
}
|
|
237
|
+
for (const p of objects) {
|
|
238
|
+
if (p.type !== "ObjectExpression")
|
|
239
|
+
fail(s.comp, `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, p);
|
|
240
|
+
}
|
|
211
241
|
const given = new Map();
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
242
|
+
// Keys the child does not declare are attributes, which fall through to its root.
|
|
243
|
+
const fallthrough = new Set();
|
|
244
|
+
for (const obj of objects) {
|
|
245
|
+
for (const p of obj.properties) {
|
|
246
|
+
if (p.type !== "ObjectProperty" || p.computed)
|
|
247
|
+
fail(s.comp, "child props hold plain keys", p);
|
|
248
|
+
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
249
|
+
// Listeners, `v-model`'s `onUpdate:…` among them, run on the client alone; a model's modifiers
|
|
250
|
+
// change only what an update writes.
|
|
251
|
+
if (!passedKey(child, key))
|
|
252
|
+
continue;
|
|
253
|
+
const field = child.props.fields.find((f) => camelize(f.js) === camelize(key));
|
|
254
|
+
if (field) {
|
|
255
|
+
given.set(field.js, p.value);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (array(key))
|
|
259
|
+
fail(s.comp, `an attribute named \`${key}\`, which JavaScript would put before the others`, p);
|
|
260
|
+
const name = propsToAttrMap[key] ?? key.toLowerCase();
|
|
261
|
+
if (key !== "class" && key !== "style" && !isSSRSafeAttrName(name))
|
|
262
|
+
fail(s.comp, `unsafe attribute name \`${name}\``, p);
|
|
263
|
+
fallthrough.add(p);
|
|
226
264
|
}
|
|
227
|
-
given.set(key, p.value);
|
|
228
265
|
}
|
|
229
266
|
const inits = child.props.fields.map((f) => {
|
|
230
267
|
const node = given.get(f.js);
|
|
@@ -238,7 +275,42 @@ export function renderChild(s, e, n) {
|
|
|
238
275
|
const v = expr(s, node);
|
|
239
276
|
return fieldInit(f.rust, ownInto(s.comp, { ...v, ty: markHome(v.ty, s.comp.name) }, markHome(f.ty, child.name), node));
|
|
240
277
|
});
|
|
241
|
-
callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots,
|
|
278
|
+
callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots, childAttrsArg(s, child, parts, merges, fallthrough, ids));
|
|
279
|
+
}
|
|
280
|
+
/** Whether a name is an array index, which a JavaScript object lists before its other keys. */
|
|
281
|
+
function array(key) {
|
|
282
|
+
return /^(0|[1-9]\d*)$/.test(key) && Number(key) < 2 ** 32 - 1;
|
|
283
|
+
}
|
|
284
|
+
/** The last argument of a child's `render_scoped`: for a child a parent may pass attributes, the
|
|
285
|
+
* `fv::Attrs` this call passes — the keys of its props objects it does not declare, and what this
|
|
286
|
+
* component was passed, merged in their order — with the scope ids; for any other, the ids. */
|
|
287
|
+
function childAttrsArg(s, child, parts, merges, fallthrough, ids) {
|
|
288
|
+
const sources = [];
|
|
289
|
+
for (const p of parts) {
|
|
290
|
+
if (s.fallthrough !== null && ((isAttrs(p) && s.comp.inheritAttrs) || dollarAttrs(s, p))) {
|
|
291
|
+
sources.push(`${s.fallthrough}.list()`);
|
|
292
|
+
}
|
|
293
|
+
else if (p.type === "ObjectExpression" && fallthrough !== null) {
|
|
294
|
+
const entries = p.properties.filter((q) => fallthrough.has(q)).map((q) => {
|
|
295
|
+
const key = q.key.type === "Identifier" ? q.key.name : String(q.key.value);
|
|
296
|
+
return `(${rustStr(key)}, ${attrOf(s, key, q.value, "vnode")})`;
|
|
297
|
+
});
|
|
298
|
+
if (entries.length)
|
|
299
|
+
sources.push(`&[${entries.join(", ")}]`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!takesAttrs(child)) {
|
|
303
|
+
if (sources.length)
|
|
304
|
+
throw new GenError(`${s.comp.file}: ${child.name} is passed attributes it does not take`);
|
|
305
|
+
return ids;
|
|
306
|
+
}
|
|
307
|
+
if (!sources.length)
|
|
308
|
+
return ids === null ? "&fv::Attrs::NONE" : `&fv::Attrs::scoped(${ids})`;
|
|
309
|
+
// One object literal, or `_attrs` or `$attrs` alone, is the child's as it is: only `_mergeProps`
|
|
310
|
+
// normalises what it merges, even one object.
|
|
311
|
+
if (sources.length === 1 && !merges)
|
|
312
|
+
return `&fv::Attrs::new(${sources[0]}, ${ids ?? '""'})`;
|
|
313
|
+
return `&fv::Attrs::merged(&[${sources.join(", ")}], ${ids ?? '""'})`;
|
|
242
314
|
}
|
|
243
315
|
/** `name: value` in a struct literal, or the name alone when the value is a variable of that name. */
|
|
244
316
|
export function fieldInit(name, value) {
|
|
@@ -262,8 +334,8 @@ export function extraParams(c) {
|
|
|
262
334
|
(c.usesTeleports ? ", fv_teleports: &fv::Teleports" : ""));
|
|
263
335
|
}
|
|
264
336
|
/** `render(out, props[, slots][, route])` for a child, with the slot content this template gives
|
|
265
|
-
* it as closures; `render_scoped`, with the scope ids its root is handed last
|
|
266
|
-
* be handed some. */
|
|
337
|
+
* it as closures; `render_scoped`, with the scope ids its root is handed last — and the attributes
|
|
338
|
+
* it is passed, as `fv::Attrs` — for a child that may be handed some. */
|
|
267
339
|
export function callChild(s, e, child, propsCode, slots, attrs) {
|
|
268
340
|
const given = new Map();
|
|
269
341
|
if (slots && slots.type !== "NullLiteral") {
|
|
@@ -281,9 +353,10 @@ export function callChild(s, e, child, propsCode, slots, attrs) {
|
|
|
281
353
|
}
|
|
282
354
|
}
|
|
283
355
|
const m = `super::${child.module}`;
|
|
356
|
+
const scoped = child.inherits || takesAttrs(child);
|
|
284
357
|
const route = (child.usesRoute ? ", fv_route" : "") + (child.usesStores ? ", fv_stores" : "") + (child.usesI18n ? ", fv_i18n" : "") + (child.usesTeleports ? ", fv_teleports" : "") +
|
|
285
|
-
(
|
|
286
|
-
const render =
|
|
358
|
+
(scoped ? `, ${attrs ?? (takesAttrs(child) ? "&fv::Attrs::NONE" : '""')}` : "");
|
|
359
|
+
const render = scoped ? "render_scoped" : "render";
|
|
287
360
|
if (!takesSlots(child)) {
|
|
288
361
|
e.stmt(`${m}::${render}(out, ${propsCode}${route});`);
|
|
289
362
|
return;
|
|
@@ -304,7 +377,7 @@ export function callChild(s, e, child, propsCode, slots, attrs) {
|
|
|
304
377
|
const sidParam = sid === null ? "" : `, ${sid}: &str`;
|
|
305
378
|
/** The content's statements, then whether it pushed anything but comments. */
|
|
306
379
|
const content = (inner) => {
|
|
307
|
-
if (staticallyFilled(
|
|
380
|
+
if (staticallyFilled(body)) {
|
|
308
381
|
statements({ ...inner, fill: false }, e, body);
|
|
309
382
|
e.stmt("true");
|
|
310
383
|
}
|
|
@@ -356,7 +429,7 @@ export function callChild(s, e, child, propsCode, slots, attrs) {
|
|
|
356
429
|
unread(opened, sid);
|
|
357
430
|
e.close(")),");
|
|
358
431
|
}
|
|
359
|
-
else if (staticallyFilled(
|
|
432
|
+
else if (staticallyFilled(body)) {
|
|
360
433
|
e.open(`${field}: Some(fv::Slot::new(&|out: &mut String|`);
|
|
361
434
|
statements({ ...inner, fill: false }, e, body);
|
|
362
435
|
e.close(")),");
|
|
@@ -393,10 +466,10 @@ export function slotContent(s, value) {
|
|
|
393
466
|
}
|
|
394
467
|
/** Whether slot content always pushes something that is not a comment: a push of content outside
|
|
395
468
|
* any `if` or loop. Then nothing has to be decided at run time. */
|
|
396
|
-
export function staticallyFilled(
|
|
469
|
+
export function staticallyFilled(body) {
|
|
397
470
|
return body.some((st) => st.type === "ExpressionStatement" && st.expression.type === "CallExpression" &&
|
|
398
471
|
st.expression.callee.type === "Identifier" && st.expression.callee.name === "_push" &&
|
|
399
|
-
pushesContent(
|
|
472
|
+
pushesContent(st.expression.arguments[0]) === true);
|
|
400
473
|
}
|
|
401
474
|
/** `RowSlotProps`, `RowSlot`: the names of a scoped slot's props struct and content type. */
|
|
402
475
|
export function slotTypeName(slotName, suffix) {
|
package/dist/vite.js
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* A build regenerates them once and fails on a construct ferrovue refuses, naming the line in the
|
|
7
7
|
* `.vue` file. The dev server regenerates them whenever a component, store, type file, the routes
|
|
8
8
|
* or the configuration changes, and shows a refusal in its error overlay. Only files whose text
|
|
9
|
-
* changed are rewritten, so `cargo watch` rebuilds no more than it must.
|
|
9
|
+
* changed are rewritten, so `cargo watch` rebuilds no more than it must.
|
|
10
|
+
*
|
|
11
|
+
* It also writes `ferrovue/islands`: every component that has an `island()`, by name, each a lazy
|
|
12
|
+
* `import()` of its `.vue` file, for `mountIslands`. */
|
|
10
13
|
import { readdirSync, readFileSync } from "node:fs";
|
|
11
14
|
import { join, relative, resolve, sep } from "node:path";
|
|
12
15
|
import { CONFIG_FILE, GenError, loadConfig, write } from "./compiler.js";
|
|
@@ -60,13 +63,45 @@ export function scopeIdMismatch(root, vue) {
|
|
|
60
63
|
return null;
|
|
61
64
|
return `\`<style scoped>\` ids: @vitejs/plugin-vue hashes "${vue.mode}" from ${vue.root}, ${CONFIG_FILE} "${mode}" from ${viteRoot}; set \`scopeId\` and \`viteRoot\` to match, or plugin-vue's \`features.componentIdGenerator\``;
|
|
62
65
|
}
|
|
66
|
+
/** The import that names the islands, and the id the plugin gives the module it writes for it. */
|
|
67
|
+
const ISLANDS = "ferrovue/islands";
|
|
68
|
+
const ISLANDS_ID = `\0${ISLANDS}`;
|
|
69
|
+
/** The text of `ferrovue/islands`: each island's name, mapped to an `import()` of its `.vue` file,
|
|
70
|
+
* which the bundler splits into a chunk of its own. */
|
|
71
|
+
function islandsModule(root, islands) {
|
|
72
|
+
const entries = Object.entries(islands).map(([name, file]) => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(resolve(root, file).split(sep).join("/"))}),`);
|
|
73
|
+
return `export default {\n${entries.join("\n")}\n};\n`;
|
|
74
|
+
}
|
|
63
75
|
export default function ferrovue(options = {}) {
|
|
64
76
|
const root = resolve(options.root ?? process.cwd());
|
|
65
|
-
|
|
77
|
+
// The islands as the last generation found them, for `ferrovue/islands`.
|
|
78
|
+
let islands = null;
|
|
79
|
+
const regenerate = () => {
|
|
80
|
+
const written = write(root, loadConfig(root));
|
|
81
|
+
islands = written.islands;
|
|
82
|
+
return written;
|
|
83
|
+
};
|
|
66
84
|
let vue = null;
|
|
67
85
|
let building = true;
|
|
68
86
|
return {
|
|
69
87
|
name: "ferrovue",
|
|
88
|
+
// Before Vite's own resolver, which would find `ferrovue/islands` in the package instead.
|
|
89
|
+
enforce: "pre",
|
|
90
|
+
resolveId(id) {
|
|
91
|
+
return id === ISLANDS ? ISLANDS_ID : null;
|
|
92
|
+
},
|
|
93
|
+
load(id) {
|
|
94
|
+
if (id !== ISLANDS_ID)
|
|
95
|
+
return null;
|
|
96
|
+
try {
|
|
97
|
+
return islandsModule(root, islands ?? regenerate().islands);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
if (e instanceof GenError || e instanceof SyntaxError)
|
|
101
|
+
this.error(e.message);
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
70
105
|
configResolved(config) {
|
|
71
106
|
vue = vueScopeIds(config);
|
|
72
107
|
building = config.command === "build";
|
|
@@ -94,10 +129,18 @@ export default function ferrovue(options = {}) {
|
|
|
94
129
|
if (!affects(root, file))
|
|
95
130
|
return;
|
|
96
131
|
try {
|
|
132
|
+
const before = JSON.stringify(islands);
|
|
97
133
|
const { changed, removed } = regenerate();
|
|
98
134
|
if (changed.length || removed.length) {
|
|
99
135
|
server.config.logger.info(`ferrovue: ${changed.length} changed, ${removed.length} removed`, { timestamp: true });
|
|
100
136
|
}
|
|
137
|
+
// A component that became an island, or stopped being one: the page loads the islands
|
|
138
|
+
// afresh, as the server now writes them.
|
|
139
|
+
const loaded = server.moduleGraph.getModuleById(ISLANDS_ID);
|
|
140
|
+
if (loaded && JSON.stringify(islands) !== before) {
|
|
141
|
+
server.moduleGraph.invalidateModule(loaded);
|
|
142
|
+
server.ws.send({ type: "full-reload" });
|
|
143
|
+
}
|
|
101
144
|
}
|
|
102
145
|
catch (e) {
|
|
103
146
|
if (!(e instanceof GenError || e instanceof SyntaxError))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ferrovue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Vue templates compiled to Rust render functions: server-side rendering with no JavaScript at run time",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"types": "./dist/client.d.ts",
|
|
45
45
|
"default": "./dist/client.js"
|
|
46
46
|
},
|
|
47
|
+
"./islands": {
|
|
48
|
+
"types": "./dist/islands.d.ts",
|
|
49
|
+
"default": "./dist/islands.js"
|
|
50
|
+
},
|
|
47
51
|
"./vite": {
|
|
48
52
|
"types": "./dist/vite.d.ts",
|
|
49
53
|
"default": "./dist/vite.js"
|