@transclude/core 0.1.1 → 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/LICENSE +1 -1
- package/README.md +65 -14
- package/bin/build.js +22 -4
- package/bin/check.js +30 -8
- package/bin/dev.js +10 -2
- package/bin/release.js +19 -4
- package/package.json +13 -4
- package/skills/transclude/SKILL.md +219 -0
- package/skills/transclude/references/elements.md +206 -0
- package/skills/transclude/references/fragments.md +168 -0
- package/skills/transclude/references/server.md +155 -0
- package/src/address.js +9 -2
- package/src/app.js +110 -81
- package/src/compiler/ambient.js +99 -0
- package/src/compiler/bind.js +34 -27
- package/src/compiler/codegen.js +59 -35
- package/src/compiler/directives.js +29 -0
- package/src/compiler/expr.js +10 -1
- package/src/compiler/html.js +41 -0
- package/src/compiler/index.js +63 -11
- package/src/compiler/script.js +40 -11
- package/src/compiler/shim.js +24 -45
- package/src/compiler/types.js +32 -5
- package/src/csp.js +7 -1
- package/src/document.js +63 -12
- package/src/extract.js +9 -8
- package/src/negotiate.js +1 -1
- package/src/plugin.js +28 -7
- package/src/precache.js +11 -1
- package/src/project.js +29 -1
- package/src/proxy.js +9 -1
- package/src/rewrite.js +12 -5
- package/src/routes.js +1 -1
- package/src/runtime/index.js +12 -9
- package/src/server.js +2 -2
- package/src/sitemap.js +3 -3
- package/src/static-cache.js +6 -3
- package/src/typecheck.js +105 -11
package/src/compiler/codegen.js
CHANGED
|
@@ -9,14 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { Scope, collectRefs, emit, parseExpr } from './expr.js';
|
|
11
11
|
import { splitInterpolations } from './interp.js';
|
|
12
|
+
import { parseEach as readEach } from './directives.js';
|
|
13
|
+
import { escapeAttr, escapeText, RAW_TEXT, VOID } from './html.js';
|
|
12
14
|
|
|
13
|
-
const VOID = new Set([
|
|
14
|
-
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
15
|
-
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
16
|
-
]);
|
|
17
15
|
|
|
18
|
-
// Content is not entity-decoded by the parser and must not be escaped by us.
|
|
19
|
-
const RAW_TEXT = new Set(['script', 'style']);
|
|
20
16
|
|
|
21
17
|
// Hoisted out of a page body into <head>.
|
|
22
18
|
const HEAD_TAGS = new Set(['title', 'meta', 'link', 'base']);
|
|
@@ -39,6 +35,28 @@ export class CompileError extends Error {
|
|
|
39
35
|
}
|
|
40
36
|
}
|
|
41
37
|
|
|
38
|
+
/**
|
|
39
|
+
* An attribute name is emitted exactly as written, so `${…}` in one reaches the
|
|
40
|
+
* page as those characters rather than as a value. Nothing downstream reads it,
|
|
41
|
+
* which made this the one interpolation mistake that rendered instead of
|
|
42
|
+
* failing. A spread parses as an attribute name too, and is caught here.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} el
|
|
45
|
+
* @throws {CompileError} when a name holds an interpolation
|
|
46
|
+
*/
|
|
47
|
+
function assertStaticAttrNames(el) {
|
|
48
|
+
for (const attr of el.attrs ?? []) {
|
|
49
|
+
if (!attr.name.includes('${')) continue;
|
|
50
|
+
|
|
51
|
+
throw new CompileError(
|
|
52
|
+
`<${el.tagName}> interpolates an attribute name: "${attr.name}". ` +
|
|
53
|
+
`Only a value takes \${…}, so write the name out. To choose between two, ` +
|
|
54
|
+
`put the condition in the value: class="\${cond ? 'a' : 'b'}".`,
|
|
55
|
+
el,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
/**
|
|
43
61
|
* @param {object[]} nodes
|
|
44
62
|
* @param {object} [opts]
|
|
@@ -46,14 +64,17 @@ export class CompileError extends Error {
|
|
|
46
64
|
*/
|
|
47
65
|
export function compileFragment(nodes, opts = {}) {
|
|
48
66
|
const gen = new Codegen(opts);
|
|
49
|
-
gen.emitChildren(nodes, gen.body, gen.rootScope, true);
|
|
67
|
+
gen.emitChildren(nodes, gen.body, gen.rootScope, { topLevel: true });
|
|
50
68
|
|
|
51
69
|
// `<html>` is read separately, because the fragment parser drops it: a nested
|
|
52
70
|
// html start tag is not something that can appear in a body, so parse5 throws
|
|
53
71
|
// it away attributes and all. `splitBlocks` reads it in document mode, where
|
|
54
72
|
// it is the element it names.
|
|
73
|
+
// `<body>` is read the same way, for the same reason.
|
|
55
74
|
const htmlNode = opts.html ?? null;
|
|
56
|
-
const
|
|
75
|
+
const bodyNode = opts.body ?? null;
|
|
76
|
+
const htmlAttrs = htmlNode?.attrs?.length ? gen.openTagAttrsJs(htmlNode, gen.rootScope) : null;
|
|
77
|
+
const bodyAttrs = bodyNode?.attrs?.length ? gen.openTagAttrsJs(bodyNode, gen.rootScope) : null;
|
|
57
78
|
|
|
58
79
|
const body = joinOut(gen.body);
|
|
59
80
|
const head = joinOut(gen.head);
|
|
@@ -86,6 +107,7 @@ export function compileFragment(nodes, opts = {}) {
|
|
|
86
107
|
title: title.code,
|
|
87
108
|
hasTitle: gen.title.length > 0,
|
|
88
109
|
htmlAttrs,
|
|
110
|
+
bodyAttrs,
|
|
89
111
|
warnings: gen.warnings,
|
|
90
112
|
reads: gen.reads,
|
|
91
113
|
components: [...gen.used.entries()].map(([tag, ref]) => ({ tag, ref })),
|
|
@@ -230,7 +252,7 @@ class Codegen {
|
|
|
230
252
|
|
|
231
253
|
// ---- traversal ----------------------------------------------------------
|
|
232
254
|
|
|
233
|
-
emitChildren(nodes, out, scope, topLevel = false) {
|
|
255
|
+
emitChildren(nodes, out, scope, { topLevel = false } = {}) {
|
|
234
256
|
let i = 0;
|
|
235
257
|
while (i < nodes.length) {
|
|
236
258
|
const node = nodes[i];
|
|
@@ -242,7 +264,7 @@ class Codegen {
|
|
|
242
264
|
if (slot) {
|
|
243
265
|
const target = this.slots.get(slot) ?? [];
|
|
244
266
|
this.slots.set(slot, target);
|
|
245
|
-
this.emitChildren(childrenOf(node), target, scope
|
|
267
|
+
this.emitChildren(childrenOf(node), target, scope);
|
|
246
268
|
i++;
|
|
247
269
|
continue;
|
|
248
270
|
}
|
|
@@ -336,7 +358,7 @@ class Codegen {
|
|
|
336
358
|
|
|
337
359
|
emitNodeAt(node, out, scope, topLevel) {
|
|
338
360
|
if (node.nodeName === '#text') {
|
|
339
|
-
this.emitText(node.value ?? '', out, scope, node
|
|
361
|
+
this.emitText(node.value ?? '', out, scope, node);
|
|
340
362
|
return;
|
|
341
363
|
}
|
|
342
364
|
// Authoring comments are stripped. They still count as "insignificant" when
|
|
@@ -553,6 +575,7 @@ class Codegen {
|
|
|
553
575
|
}
|
|
554
576
|
|
|
555
577
|
emitElement(el, out, scope, topLevel) {
|
|
578
|
+
assertStaticAttrNames(el);
|
|
556
579
|
const tag = el.tagName;
|
|
557
580
|
|
|
558
581
|
// In a layout, <slot> is where the child's content goes. In a component it
|
|
@@ -568,14 +591,14 @@ class Codegen {
|
|
|
568
591
|
return;
|
|
569
592
|
}
|
|
570
593
|
this.c(out, `if (${filled}) { __o += ${filled}; } else {`);
|
|
571
|
-
this.emitChildren(fallback, out, scope
|
|
594
|
+
this.emitChildren(fallback, out, scope);
|
|
572
595
|
this.c(out, `}`);
|
|
573
596
|
return;
|
|
574
597
|
}
|
|
575
598
|
|
|
576
599
|
// A <template> carrying a directive is structural: consumed, children emitted.
|
|
577
600
|
if (tag === 'template' && directivesOf(el).size > 0) {
|
|
578
|
-
this.emitChildren(childrenOf(el), out, scope
|
|
601
|
+
this.emitChildren(childrenOf(el), out, scope);
|
|
579
602
|
return;
|
|
580
603
|
}
|
|
581
604
|
|
|
@@ -605,11 +628,11 @@ class Codegen {
|
|
|
605
628
|
for (const child of childrenOf(el)) {
|
|
606
629
|
if (child.nodeName === '#text') {
|
|
607
630
|
assertRawTextSafe(tag, child.value ?? '', el);
|
|
608
|
-
this.emitText(child.value ?? '', target, scope, child, true);
|
|
631
|
+
this.emitText(child.value ?? '', target, scope, child, { raw: true });
|
|
609
632
|
}
|
|
610
633
|
}
|
|
611
634
|
} else {
|
|
612
|
-
this.emitChildren(childrenOf(el), target, scope
|
|
635
|
+
this.emitChildren(childrenOf(el), target, scope);
|
|
613
636
|
}
|
|
614
637
|
|
|
615
638
|
this.s(target, `</${tag}>`);
|
|
@@ -632,7 +655,7 @@ class Codegen {
|
|
|
632
655
|
this.c(out, `__o += __sh(${ref}, {${props}}${this.fragments ? ', __fragment' : ''});`);
|
|
633
656
|
|
|
634
657
|
// Light DOM children fill <slot>.
|
|
635
|
-
this.emitChildren(childrenOf(el), out, scope
|
|
658
|
+
this.emitChildren(childrenOf(el), out, scope);
|
|
636
659
|
|
|
637
660
|
this.s(out, `</${tag}>`);
|
|
638
661
|
}
|
|
@@ -750,7 +773,7 @@ class Codegen {
|
|
|
750
773
|
const uid = ++this.uid;
|
|
751
774
|
if (children.length) {
|
|
752
775
|
this.c(out, `const __fb${uid} = (() => { let __o = '';`);
|
|
753
|
-
this.emitChildren(children, out, scope
|
|
776
|
+
this.emitChildren(children, out, scope);
|
|
754
777
|
this.c(out, `return __o; })();`);
|
|
755
778
|
}
|
|
756
779
|
|
|
@@ -772,7 +795,7 @@ class Codegen {
|
|
|
772
795
|
const children = childrenOf(el);
|
|
773
796
|
if (children.length) {
|
|
774
797
|
this.c(out, `const __sl${id} = (() => { let __o = '';`);
|
|
775
|
-
this.emitChildren(children, out, scope
|
|
798
|
+
this.emitChildren(children, out, scope);
|
|
776
799
|
this.c(out, `return __o; })();`);
|
|
777
800
|
}
|
|
778
801
|
|
|
@@ -824,14 +847,18 @@ class Codegen {
|
|
|
824
847
|
}
|
|
825
848
|
|
|
826
849
|
/**
|
|
827
|
-
* `<html lang="en" data-theme="${theme}">` as `{ "lang": "en", "data-theme": theme }
|
|
850
|
+
* `<html lang="en" data-theme="${theme}">` as `{ "lang": "en", "data-theme": theme }`,
|
|
851
|
+
* and the same for `<body>`.
|
|
828
852
|
*
|
|
829
853
|
* An object rather than serialized markup, because the chain merges these by
|
|
830
854
|
* name: a root layout setting the theme and a page setting `dir` must both
|
|
831
855
|
* survive, and two `data-theme` attributes in one tag would leave the parser
|
|
832
856
|
* taking the first, which is the outermost. `renderDocument` serializes.
|
|
833
857
|
*/
|
|
834
|
-
|
|
858
|
+
openTagAttrsJs(el, scope) {
|
|
859
|
+
// `<html>` and `<body>` are read by a second parse, so neither reaches
|
|
860
|
+
// emitElement and neither is checked by it.
|
|
861
|
+
assertStaticAttrNames(el);
|
|
835
862
|
const pairs = el.attrs
|
|
836
863
|
.filter((attr) => !DIRECTIVES.has(attr.name))
|
|
837
864
|
.map((attr) => {
|
|
@@ -858,7 +885,7 @@ class Codegen {
|
|
|
858
885
|
.join(' + ');
|
|
859
886
|
}
|
|
860
887
|
|
|
861
|
-
emitText(value, out, scope, node, raw) {
|
|
888
|
+
emitText(value, out, scope, node, { raw = false } = {}) {
|
|
862
889
|
const start = node?.sourceCodeLocation?.startLine ?? this.at;
|
|
863
890
|
let line = start;
|
|
864
891
|
|
|
@@ -918,7 +945,9 @@ function assertRawTextSafe(tag, text, el) {
|
|
|
918
945
|
function isJsonCall(source) {
|
|
919
946
|
try {
|
|
920
947
|
const node = parseExpr(source);
|
|
921
|
-
|
|
948
|
+
if (node.type !== 'CallExpression') return false;
|
|
949
|
+
if (node.callee?.type !== 'Identifier') return false;
|
|
950
|
+
return node.callee.name === 'json';
|
|
922
951
|
} catch {
|
|
923
952
|
return false;
|
|
924
953
|
}
|
|
@@ -933,7 +962,7 @@ export const ANCHOR_CLOSE = '<!--]-->';
|
|
|
933
962
|
*
|
|
934
963
|
* @param {object[]} nodes
|
|
935
964
|
* @param {number} i where the `if` is
|
|
936
|
-
* @returns {{ chain: Array<{ node: object, kind: string, cond
|
|
965
|
+
* @returns {{ chain: Array<{ node: object, kind: string, cond: string|null }>, next: number }|null}
|
|
937
966
|
* null when the element carries no `if`, so there is no chain to gather
|
|
938
967
|
*/
|
|
939
968
|
export function gatherChain(nodes, i) {
|
|
@@ -952,7 +981,10 @@ export function gatherChain(nodes, i) {
|
|
|
952
981
|
continue;
|
|
953
982
|
}
|
|
954
983
|
if (d?.has('else')) {
|
|
955
|
-
|
|
984
|
+
// `cond: null` rather than absent, so every entry in the chain is one
|
|
985
|
+
// shape. `emitBranches` reads `kind` to decide, and never reads the
|
|
986
|
+
// condition of an `else`.
|
|
987
|
+
chain.push({ node: nodes[k], kind: 'else', cond: null });
|
|
956
988
|
next = k + 1;
|
|
957
989
|
}
|
|
958
990
|
break;
|
|
@@ -995,14 +1027,14 @@ function nextSignificant(nodes, from) {
|
|
|
995
1027
|
|
|
996
1028
|
|
|
997
1029
|
function parseEach(value, node) {
|
|
998
|
-
const
|
|
999
|
-
if (!
|
|
1030
|
+
const spec = readEach(value);
|
|
1031
|
+
if (!spec) {
|
|
1000
1032
|
throw new CompileError(
|
|
1001
1033
|
`each="${value}" is malformed. Expected each="item of list" or each="item, index of list"`,
|
|
1002
1034
|
node,
|
|
1003
1035
|
);
|
|
1004
1036
|
}
|
|
1005
|
-
return
|
|
1037
|
+
return spec;
|
|
1006
1038
|
}
|
|
1007
1039
|
|
|
1008
1040
|
// ---- output ---------------------------------------------------------------
|
|
@@ -1051,11 +1083,3 @@ function joinOut(entries) {
|
|
|
1051
1083
|
return { code: lines.join('\n'), at };
|
|
1052
1084
|
}
|
|
1053
1085
|
|
|
1054
|
-
// parse5 hands us decoded text, so static output has to be re-encoded.
|
|
1055
|
-
function escapeText(value) {
|
|
1056
|
-
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
function escapeAttr(value) {
|
|
1060
|
-
return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
1061
|
-
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// What a directive value means. One answer, because three files ask.
|
|
2
|
+
//
|
|
3
|
+
// `each` was parsed by the same regular expression written out in bind.js,
|
|
4
|
+
// codegen.js and shim.js. Three copies of one rule is three places to change and
|
|
5
|
+
// two to forget, and the three do different things with the answer: one emits a
|
|
6
|
+
// loop, one emits a binding, one emits the JS tsc checks. They agreed by luck.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `item of items`, or `item, i of items`.
|
|
10
|
+
*
|
|
11
|
+
* The value is an expression, not an interpolation: there is no `${}` in it. A
|
|
12
|
+
* parser that treated it as one would read the whole thing as text and get the
|
|
13
|
+
* volatile set wrong, which is a silent bug rather than a loud one.
|
|
14
|
+
*/
|
|
15
|
+
const EACH = /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+of\s+([\s\S]+?)\s*$/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The three parts of an `each`, or null when it does not parse.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} value the attribute as written
|
|
21
|
+
* @returns {{ item: string, index: string|null, list: string }|null}
|
|
22
|
+
*/
|
|
23
|
+
export function parseEach(value) {
|
|
24
|
+
const found = EACH.exec(value ?? '');
|
|
25
|
+
if (!found) return null;
|
|
26
|
+
|
|
27
|
+
const [, item, index, list] = found;
|
|
28
|
+
return { item, index: index ?? null, list };
|
|
29
|
+
}
|
package/src/compiler/expr.js
CHANGED
|
@@ -175,6 +175,14 @@ export function chainOf(node, scope, computed = []) {
|
|
|
175
175
|
return null;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Every jsep node key that can hold another node. Anything else on a node is a
|
|
180
|
+
* name, a value or a flag, and walking into one finds nothing.
|
|
181
|
+
*/
|
|
182
|
+
const CHILD_KEYS = [
|
|
183
|
+
'argument', 'left', 'right', 'test', 'consequent', 'alternate', 'object', 'property',
|
|
184
|
+
];
|
|
185
|
+
|
|
178
186
|
/**
|
|
179
187
|
* Every data or loop-variable path an expression reads.
|
|
180
188
|
*
|
|
@@ -212,9 +220,10 @@ export function collectRefs(node, scope, out = []) {
|
|
|
212
220
|
return out;
|
|
213
221
|
|
|
214
222
|
default:
|
|
215
|
-
for (const key of
|
|
223
|
+
for (const key of CHILD_KEYS) {
|
|
216
224
|
if (node[key]) collectRefs(node[key], scope, out);
|
|
217
225
|
}
|
|
226
|
+
|
|
218
227
|
for (const element of node.elements ?? []) collectRefs(element, scope, out);
|
|
219
228
|
return out;
|
|
220
229
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Facts about HTML that both compiler passes need.
|
|
2
|
+
//
|
|
3
|
+
// These are the spec's, not this codebase's, so they belong in one place and
|
|
4
|
+
// never change for a reason either pass would know about. Both lists were
|
|
5
|
+
// written out twice, in codegen.js and bind.js, and agreed by luck.
|
|
6
|
+
|
|
7
|
+
/** Elements with no closing tag and no children. */
|
|
8
|
+
export const VOID = new Set([
|
|
9
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
10
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Elements whose text is not entity-decoded by the parser, so escaping it would
|
|
15
|
+
* change what the browser reads. `&` is one character in prose and five in
|
|
16
|
+
* JavaScript.
|
|
17
|
+
*/
|
|
18
|
+
export const RAW_TEXT = new Set(['script', 'style']);
|
|
19
|
+
|
|
20
|
+
/** parse5 hands back decoded text, so static output has to be re-encoded. */
|
|
21
|
+
export function escapeText(value) {
|
|
22
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A static attribute value.
|
|
27
|
+
*
|
|
28
|
+
* Three characters, and `>` is deliberately not one of them: a quoted attribute
|
|
29
|
+
* value may hold one, and leaving it is what a serializer does. That has a
|
|
30
|
+
* consequence worth knowing, because `content="a > b"` then reaches the page
|
|
31
|
+
* with a bare `>` in it, and anything scanning compiled markup for the end of a
|
|
32
|
+
* tag has to be quote-aware rather than stopping at the first one. `mergeHead`
|
|
33
|
+
* in document.js is that scanner, and it had this wrong once.
|
|
34
|
+
*
|
|
35
|
+
* The runtime escapes `>` as well, so an interpolated value and a static one
|
|
36
|
+
* come out spelled differently and parse the same. That is not worth making
|
|
37
|
+
* agree: the runtime ships to a browser and must not import from here.
|
|
38
|
+
*/
|
|
39
|
+
export function escapeAttr(value) {
|
|
40
|
+
return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
41
|
+
}
|
package/src/compiler/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { parse, parseFragment } from 'parse5';
|
|
4
4
|
import { compileFragment, childrenOf, CompileError } from './codegen.js';
|
|
5
|
+
import { escapeAttr } from './html.js';
|
|
5
6
|
import { lineMap, sourceMap } from './sourcemap.js';
|
|
6
7
|
import { compileBindings } from './bind.js';
|
|
7
8
|
import {
|
|
@@ -15,7 +16,14 @@ import {
|
|
|
15
16
|
|
|
16
17
|
export { CompileError, ScriptError };
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
// Everything the generated page and layout modules define at the top level. A
|
|
20
|
+
// block naming one of these is checked whether it exports it or merely declares
|
|
21
|
+
// it, because both land in the same scope.
|
|
22
|
+
const PAGE_EXPORTS = new Set([
|
|
23
|
+
'css', 'load', 'render', 'renderHead', 'renderTitle', 'renderHtmlAttrs',
|
|
24
|
+
'layouts', 'client', 'elements', 'headScript', 'hasTitle', 'includes',
|
|
25
|
+
'renderBodyAttrs',
|
|
26
|
+
]);
|
|
19
27
|
const COMPONENT_EXPORTS = new Set([
|
|
20
28
|
'tag', 'light', 'css', 'elements', 'propDefs', 'propAttrs', 'stateDefs', 'members', 'render',
|
|
21
29
|
'coerce', 'def', 'init', 'define', 'default', 'bind', 'update', 'volatile', 'formAssociated',
|
|
@@ -107,10 +115,17 @@ export function splitBlocks(source) {
|
|
|
107
115
|
// away with its attributes. In document mode it is the element it names, and a
|
|
108
116
|
// `<html>` inside a script block or a comment is still not one, because this
|
|
109
117
|
// is the real parser rather than a search for a string.
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
const document = parse(source, { sourceCodeLocationInfo: true });
|
|
119
|
+
const html = document.childNodes.find((n) => n.nodeName === 'html') ?? null;
|
|
120
|
+
|
|
121
|
+
// `<body>` is dropped by the fragment parser for the same reason `<html>` is,
|
|
122
|
+
// and was silently going missing: writing `<body class="admin">` in a page
|
|
123
|
+
// produced no attribute and no error. In document mode it is a real element.
|
|
124
|
+
// parse5's ChildNode union does not carry `childNodes`; in document mode this
|
|
125
|
+
// one is the html element and does.
|
|
126
|
+
const parent = /** @type {{ childNodes?: Array<{ nodeName: string }> }|null} */ (html);
|
|
127
|
+
const body = parent?.childNodes?.find((n) => n.nodeName === 'body') ?? null;
|
|
128
|
+
const out = { server: null, properties: null, state: null, client: [], head: [], styles: [], nodes: [], html, body };
|
|
114
129
|
|
|
115
130
|
for (const node of doc.childNodes) {
|
|
116
131
|
if (node.nodeName === 'script') {
|
|
@@ -141,6 +156,12 @@ export function splitBlocks(source) {
|
|
|
141
156
|
// before first paint, or a `pagereveal` listener, which fires too early for
|
|
142
157
|
// any script in the body to see.
|
|
143
158
|
else if (attrs.has('head')) out.head.push({ ...block, attrs: node.attrs ?? [] });
|
|
159
|
+
// A `src` means there is no code here to compile, so this is markup: an
|
|
160
|
+
// ordinary external script the page wants in its body. Read as a client
|
|
161
|
+
// block it became an empty one and the tag was dropped, `src` and all,
|
|
162
|
+
// with nothing said. A nested `<script src>` was always markup; only a
|
|
163
|
+
// top-level one went missing.
|
|
164
|
+
else if (attrs.has('src')) out.nodes.push(node);
|
|
144
165
|
else out.client.push(block);
|
|
145
166
|
continue;
|
|
146
167
|
}
|
|
@@ -355,6 +376,21 @@ const MARK = {
|
|
|
355
376
|
title: '/*@transclude:title*/',
|
|
356
377
|
};
|
|
357
378
|
|
|
379
|
+
/**
|
|
380
|
+
* A page: the `<script server>` block, the markup, and whatever layouts wrap it.
|
|
381
|
+
*
|
|
382
|
+
* `filename` is what an error message says, which is the short route id.
|
|
383
|
+
* `sourcePath` is what the source map names, which is a path an editor opens.
|
|
384
|
+
*
|
|
385
|
+
* @param {string} source
|
|
386
|
+
* @param {{ components?: Map<string, string>, shadowTags?: Set<string>,
|
|
387
|
+
* runtime: string, filename?: string, sourcePath?: string|null,
|
|
388
|
+
* layouts?: string[],
|
|
389
|
+
* client?: { tags: string[], hasScript: boolean, needed: boolean } }} options
|
|
390
|
+
* @returns {{ code: string, map: object|null, warnings: string[],
|
|
391
|
+
* components: string[] }} the module, a line-level map or null when there is
|
|
392
|
+
* no markup to map, whatever the template warned about, and the tags it used
|
|
393
|
+
*/
|
|
358
394
|
export function compilePage(
|
|
359
395
|
source,
|
|
360
396
|
{
|
|
@@ -375,11 +411,18 @@ export function compilePage(
|
|
|
375
411
|
|
|
376
412
|
const server = blocks.server
|
|
377
413
|
? bindDefaultExport(blocks.server, '__load', where)
|
|
378
|
-
: { code: 'const __load = null;', exports: [], imports: [], defaultNode: null };
|
|
414
|
+
: { code: 'const __load = null;', exports: [], imports: [], declared: [], defaultNode: null };
|
|
379
415
|
assertNoCollisions(server.exports, PAGE_EXPORTS, where);
|
|
416
|
+
assertNoCollisions(server.declared ?? [], PAGE_EXPORTS, where, 'declares');
|
|
380
417
|
assertNoActionsObject(server.exports, where);
|
|
381
418
|
|
|
382
|
-
const template = compileFragment(blocks.nodes, {
|
|
419
|
+
const template = compileFragment(blocks.nodes, {
|
|
420
|
+
components,
|
|
421
|
+
shadowTags,
|
|
422
|
+
page: true,
|
|
423
|
+
html: blocks.html,
|
|
424
|
+
body: blocks.body,
|
|
425
|
+
});
|
|
383
426
|
assertIncludesResolve(template.regionIncludes, template.regions);
|
|
384
427
|
|
|
385
428
|
const code = `
|
|
@@ -413,6 +456,10 @@ export function renderHtmlAttrs(__d) {
|
|
|
413
456
|
return ${template.htmlAttrs ?? '{}'};
|
|
414
457
|
}
|
|
415
458
|
|
|
459
|
+
export function renderBodyAttrs(__d) {
|
|
460
|
+
return ${template.bodyAttrs ?? '{}'};
|
|
461
|
+
}
|
|
462
|
+
|
|
416
463
|
export function renderHead(__d) {
|
|
417
464
|
let __o = '';
|
|
418
465
|
${MARK.head}
|
|
@@ -485,8 +532,9 @@ export function compileLayout(source, { id, components = new Map(), shadowTags =
|
|
|
485
532
|
|
|
486
533
|
const server = blocks.server
|
|
487
534
|
? bindDefaultExport(blocks.server, '__load', where)
|
|
488
|
-
: { code: 'const __load = null;', exports: [], imports: [], defaultNode: null };
|
|
535
|
+
: { code: 'const __load = null;', exports: [], imports: [], declared: [], defaultNode: null };
|
|
489
536
|
assertNoCollisions(server.exports, PAGE_EXPORTS, where);
|
|
537
|
+
assertNoCollisions(server.declared ?? [], PAGE_EXPORTS, where, 'declares');
|
|
490
538
|
assertNoActionsObject(server.exports, where);
|
|
491
539
|
|
|
492
540
|
const template = compileFragment(blocks.nodes, {
|
|
@@ -495,6 +543,7 @@ export function compileLayout(source, { id, components = new Map(), shadowTags =
|
|
|
495
543
|
page: true,
|
|
496
544
|
layout: true,
|
|
497
545
|
html: blocks.html,
|
|
546
|
+
body: blocks.body,
|
|
498
547
|
});
|
|
499
548
|
|
|
500
549
|
const warnings = [...template.warnings];
|
|
@@ -527,6 +576,10 @@ export function renderHtmlAttrs(__d) {
|
|
|
527
576
|
return ${template.htmlAttrs ?? '{}'};
|
|
528
577
|
}
|
|
529
578
|
|
|
579
|
+
export function renderBodyAttrs(__d) {
|
|
580
|
+
return ${template.bodyAttrs ?? '{}'};
|
|
581
|
+
}
|
|
582
|
+
|
|
530
583
|
export function renderHead(__d) {
|
|
531
584
|
let __o = '';
|
|
532
585
|
${indent(template.head)}
|
|
@@ -539,7 +592,7 @@ ${slotBodies(template)}
|
|
|
539
592
|
return __out;
|
|
540
593
|
}
|
|
541
594
|
|
|
542
|
-
export default { css, headScript, elements, hasTitle, load, renderTitle, renderHead, renderHtmlAttrs, render };
|
|
595
|
+
export default { css, headScript, elements, hasTitle, load, renderTitle, renderHead, renderHtmlAttrs, renderBodyAttrs, render };
|
|
543
596
|
`;
|
|
544
597
|
|
|
545
598
|
return { code, warnings, components: template.components.map((c) => c.tag) };
|
|
@@ -844,8 +897,7 @@ function headScript(blocks, where) {
|
|
|
844
897
|
/** A static attribute, escaped the way an HTML serializer must. */
|
|
845
898
|
function serializeAttr({ name, value }) {
|
|
846
899
|
if (value === '') return ` ${name}`;
|
|
847
|
-
|
|
848
|
-
return ` ${name}="${escaped}"`;
|
|
900
|
+
return ` ${name}="${escapeAttr(value)}"`;
|
|
849
901
|
}
|
|
850
902
|
|
|
851
903
|
function elementsExport(used) {
|
package/src/compiler/script.js
CHANGED
|
@@ -34,7 +34,7 @@ const PARSE_OPTIONS = {
|
|
|
34
34
|
* @param {{ flags?: string[] }} [options]
|
|
35
35
|
* @returns {{ code: string, exports: string[],
|
|
36
36
|
* imports: Array<{ source: string, specifiers: string }>,
|
|
37
|
-
* defaultNode: object|null, flags: object }}
|
|
37
|
+
* declared: string[], defaultNode: object|null, flags: object }}
|
|
38
38
|
*/
|
|
39
39
|
export function bindDefaultExport(block, name, label, { flags = [] } = {}) {
|
|
40
40
|
const { code: source, line = 1 } = block;
|
|
@@ -56,9 +56,10 @@ export function bindDefaultExport(block, name, label, { flags = [] } = {}) {
|
|
|
56
56
|
const node = ast.body.find((s) => s.type === 'ExportDefaultDeclaration') ?? null;
|
|
57
57
|
const exports = namedExportsOf(ast, source, line, label).filter((n) => !flags.includes(n));
|
|
58
58
|
const imports = importsOf(ast);
|
|
59
|
+
const declared = topLevelNames(ast);
|
|
59
60
|
|
|
60
61
|
if (!node) {
|
|
61
|
-
return { code: `${code}\nconst ${name} = null;`, exports, imports, defaultNode: null, flags: found };
|
|
62
|
+
return { code: `${code}\nconst ${name} = null;`, exports, imports, declared, defaultNode: null, flags: found };
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
// Slicing the *declaration* rather than the statement means
|
|
@@ -71,7 +72,7 @@ export function bindDefaultExport(block, name, label, { flags = [] } = {}) {
|
|
|
71
72
|
';' +
|
|
72
73
|
code.slice(node.end);
|
|
73
74
|
|
|
74
|
-
return { code: rewritten, exports, imports, defaultNode: node.declaration, flags: found };
|
|
75
|
+
return { code: rewritten, exports, imports, declared, defaultNode: node.declaration, flags: found };
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
/**
|
|
@@ -368,6 +369,9 @@ function scopedNames(statements) {
|
|
|
368
369
|
return (statements ?? []).flatMap(declaredNames);
|
|
369
370
|
}
|
|
370
371
|
|
|
372
|
+
/** Keys acorn puts on every node. They hold positions, never child nodes. */
|
|
373
|
+
const NODE_BOOKKEEPING = new Set(['type', 'start', 'end', 'loc', 'range']);
|
|
374
|
+
|
|
371
375
|
/**
|
|
372
376
|
* Identifiers a subtree reads from outside itself.
|
|
373
377
|
*
|
|
@@ -405,8 +409,12 @@ function freeNames(node, bound, out) {
|
|
|
405
409
|
case 'ArrowFunctionExpression': {
|
|
406
410
|
const inner = new Set(bound);
|
|
407
411
|
if (node.id) inner.add(node.id.name);
|
|
408
|
-
for (const param of node.params)
|
|
409
|
-
|
|
412
|
+
for (const param of node.params) {
|
|
413
|
+
for (const name of patternNames(param)) inner.add(name);
|
|
414
|
+
}
|
|
415
|
+
if (node.body.type === 'BlockStatement') {
|
|
416
|
+
for (const name of scopedNames(node.body.body)) inner.add(name);
|
|
417
|
+
}
|
|
410
418
|
// A default is evaluated in the function's own scope, so it sees the params.
|
|
411
419
|
for (const param of node.params) freeNames(param, inner, out);
|
|
412
420
|
freeNames(node.body, inner, out);
|
|
@@ -462,7 +470,7 @@ function freeNames(node, bound, out) {
|
|
|
462
470
|
|
|
463
471
|
default:
|
|
464
472
|
for (const key of Object.keys(node)) {
|
|
465
|
-
if (key
|
|
473
|
+
if (NODE_BOOKKEEPING.has(key)) continue;
|
|
466
474
|
freeNames(node[key], bound, out);
|
|
467
475
|
}
|
|
468
476
|
return out;
|
|
@@ -506,19 +514,20 @@ function importsOf(ast) {
|
|
|
506
514
|
}
|
|
507
515
|
|
|
508
516
|
/**
|
|
509
|
-
* Guards against a block
|
|
517
|
+
* Guards against a block using a name the generated module already defines.
|
|
510
518
|
*
|
|
511
|
-
* @param {string[]}
|
|
519
|
+
* @param {string[]} names
|
|
512
520
|
* @param {Set<string>} reserved
|
|
513
521
|
* @param {string} label
|
|
522
|
+
* @param {string} [verb] how the block used it, for the message
|
|
514
523
|
* @returns {void}
|
|
515
524
|
* @throws naming the first collision
|
|
516
525
|
*/
|
|
517
|
-
export function assertNoCollisions(
|
|
518
|
-
for (const name of
|
|
526
|
+
export function assertNoCollisions(names, reserved, label, verb = 'exports') {
|
|
527
|
+
for (const name of names) {
|
|
519
528
|
if (reserved.has(name)) {
|
|
520
529
|
throw new ScriptError(
|
|
521
|
-
`${label}:
|
|
530
|
+
`${label}: ${verb} "${name}", which the generated module already defines. ` +
|
|
522
531
|
`Reserved: ${[...reserved].join(', ')}.`,
|
|
523
532
|
);
|
|
524
533
|
}
|
|
@@ -586,6 +595,26 @@ function namedExportsOf(ast, code, lineOffset, label) {
|
|
|
586
595
|
return names;
|
|
587
596
|
}
|
|
588
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Every name a block binds at the top level, imports included.
|
|
600
|
+
*
|
|
601
|
+
* The generated module puts this block's code beside its own `export const`
|
|
602
|
+
* statements, so any of these can collide, not only the exported ones. An
|
|
603
|
+
* import was the case that got through: `import { elements } from './x.js'`
|
|
604
|
+
* binds `elements` and exports nothing, so the export check never saw it and
|
|
605
|
+
* the build failed inside rolldown, pointing at a virtual module.
|
|
606
|
+
*
|
|
607
|
+
* @param {object} ast
|
|
608
|
+
* @returns {string[]}
|
|
609
|
+
*/
|
|
610
|
+
function topLevelNames(ast) {
|
|
611
|
+
return ast.body.flatMap((statement) =>
|
|
612
|
+
statement.type === 'ImportDeclaration'
|
|
613
|
+
? statement.specifiers.map((spec) => spec.local.name)
|
|
614
|
+
: declaredNames(statement),
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
589
618
|
function patternNames(node) {
|
|
590
619
|
switch (node.type) {
|
|
591
620
|
case 'Identifier':
|