@transclude/core 0.1.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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -0
  3. package/bin/build.js +469 -0
  4. package/bin/check.js +78 -0
  5. package/bin/dev.js +348 -0
  6. package/bin/release.js +176 -0
  7. package/bin/serve.bun.js +15 -0
  8. package/bin/serve.deno.js +15 -0
  9. package/bin/serve.js +12 -0
  10. package/editor/server.js +172 -0
  11. package/editor/vscode/extension.js +49 -0
  12. package/editor/vscode/package.json +32 -0
  13. package/editor/vscode/syntaxes/transclude.injection.json +41 -0
  14. package/package.json +82 -0
  15. package/src/address.js +183 -0
  16. package/src/app.js +492 -0
  17. package/src/cache.js +137 -0
  18. package/src/compiler/bind.js +496 -0
  19. package/src/compiler/codegen.js +1061 -0
  20. package/src/compiler/expr.js +221 -0
  21. package/src/compiler/index.js +964 -0
  22. package/src/compiler/interp.js +82 -0
  23. package/src/compiler/script.js +620 -0
  24. package/src/compiler/shim.js +756 -0
  25. package/src/compiler/sourcemap.js +140 -0
  26. package/src/compiler/types.js +163 -0
  27. package/src/compress.js +104 -0
  28. package/src/cookies.js +157 -0
  29. package/src/csp.js +192 -0
  30. package/src/document.js +604 -0
  31. package/src/extract.js +339 -0
  32. package/src/feed.js +194 -0
  33. package/src/include.js +89 -0
  34. package/src/lookup.js +49 -0
  35. package/src/negotiate.js +95 -0
  36. package/src/plugin.js +423 -0
  37. package/src/pool.js +29 -0
  38. package/src/precache.js +68 -0
  39. package/src/production.js +159 -0
  40. package/src/project.js +110 -0
  41. package/src/proxy.js +319 -0
  42. package/src/public-files.js +77 -0
  43. package/src/rewrite.js +281 -0
  44. package/src/routes.js +199 -0
  45. package/src/runtime/index.js +1345 -0
  46. package/src/server.js +183 -0
  47. package/src/sitemap.js +124 -0
  48. package/src/static-cache.js +170 -0
  49. package/src/typecheck.js +492 -0
  50. package/src/worker.js +87 -0
@@ -0,0 +1,496 @@
1
+ // A second pass over the same parse5 tree the renderer walks, emitting code
2
+ // that *updates* the rendered DOM instead of producing it.
3
+ //
4
+ // Nothing has to be worked out at runtime. The compiler already knows where every
5
+ // ${} lands, so a binding is a path. No diffing, no vdom, and the only thing in
6
+ // the served HTML that exists for the client is a pair of comment anchors around
7
+ // each `if` and `each`.
8
+ //
9
+ // Addressing works in two modes. Where the shape up to a node is known it is
10
+ // `parent.childNodes[i]`. Past a block, whose node count is not known at compile
11
+ // time, the walk switches to a cursor stepping sibling by sibling from the
12
+ // block's closing anchor. Both only ever run once: after bind, everything
13
+ // is held by reference.
14
+ //
15
+ // A block is not a wall. Each branch of an `if` and the item of an `each` get
16
+ // their own bind/update pair, so a block is re-rendered only when its
17
+ // structure changes, meaning a different branch or a new key. Its contents are
18
+ // written into like anything else. That is what keeps a form field inside an
19
+ // `if` from being destroyed because some text beside it changed.
20
+ //
21
+ // What it will not bind, it reports as `volatile`: prop names whose change
22
+ // means the caller repaints the whole shadow root. Being conservative is free.
23
+
24
+ import { Scope, collectRefs, emit, parseExpr } from './expr.js';
25
+ import { splitInterpolations } from './interp.js';
26
+ import { childrenOf, gatherChain } from './codegen.js';
27
+
28
+ const DIRECTIVES = new Set(['if', 'else-if', 'else', 'each', 'key']);
29
+ const VOID = new Set([
30
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
31
+ 'link', 'meta', 'param', 'source', 'track', 'wbr',
32
+ ]);
33
+ const RAW_TEXT = new Set(['script', 'style']);
34
+ const EACH = /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+of\s+([\s\S]+?)\s*$/;
35
+
36
+ /**
37
+ * @param {object[]} nodes the same parse5 nodes the renderer walked
38
+ * @param {object} [opts]
39
+ * @returns {{ locate: string, writes: string, cursors: object, parts: string,
40
+ * volatile: string[] }} `volatile` is what it could not bind, which is what
41
+ * decides whether a light element may update in place
42
+ */
43
+ export function compileBindings(nodes, opts = {}) {
44
+ const gen = new Bindgen(opts);
45
+ gen.walk(nodes, { parent: '__root', index: opts.rootOffset ?? 0 });
46
+ const root = gen.frame;
47
+ return {
48
+ locate: root.locate.join('\n'),
49
+ writes: root.writes.join('\n'),
50
+ cursors: root.cursors,
51
+ parts: gen.parts.join('\n'),
52
+ volatile: [...gen.volatile].sort(),
53
+ };
54
+ }
55
+
56
+ /** One emission context: the body of bind/update for the template or a part. */
57
+ class Frame {
58
+ constructor(scope, loopArgs = []) {
59
+ this.scope = scope;
60
+ // The loop variables a block here would need, in the order codegen emits
61
+ // its parameters. Both passes count two per level, named or not.
62
+ this.loopArgs = loopArgs;
63
+ this.locate = [];
64
+ this.writes = [];
65
+ this.slot = 0;
66
+ this.cursors = 0;
67
+ this.gaveUp = false;
68
+ }
69
+ }
70
+
71
+ class Bindgen {
72
+ constructor({
73
+ components = new Map(),
74
+ shadowTags = new Set(),
75
+ blockOf = new Map(),
76
+ refs = new Map(),
77
+ } = {}) {
78
+ this.components = components;
79
+ this.shadowTags = shadowTags;
80
+ // Which tree node owns which compiled block. Sharing the map is what keeps
81
+ // this pass and the renderer from drifting apart over the same tree.
82
+ this.blockOf = blockOf;
83
+ // tag -> the local name the renderer imported that component under.
84
+ this.refs = refs;
85
+ this.frames = [new Frame(new Scope())];
86
+ this.parts = [];
87
+ this.volatile = new Set();
88
+ }
89
+
90
+ get frame() {
91
+ return this.frames[this.frames.length - 1];
92
+ }
93
+
94
+ get scope() {
95
+ return this.frame.scope;
96
+ }
97
+
98
+ next() {
99
+ return this.frame.slot++;
100
+ }
101
+
102
+ cursor() {
103
+ return `__c${this.frame.cursors++}`;
104
+ }
105
+
106
+ locate(line) {
107
+ this.frame.locate.push(line);
108
+ }
109
+
110
+ write(line) {
111
+ this.frame.writes.push(line);
112
+ }
113
+
114
+ // ---- giving up ----------------------------------------------------------
115
+
116
+ giveUp(source, scope = this.scope) {
117
+ let ast;
118
+ try {
119
+ ast = parseExpr(source);
120
+ } catch {
121
+ return;
122
+ }
123
+ for (const ref of collectRefs(ast, scope)) {
124
+ if (ref.base === 'data') this.volatile.add(ref.name);
125
+ }
126
+ }
127
+
128
+ giveUpText(value, scope = this.scope) {
129
+ for (const part of splitInterpolations(value ?? '')) {
130
+ if (part.type === 'expr') this.giveUp(part.value, scope);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * A directive's value is an expression, not an interpolation, so
136
+ * `if="tags.length"` has no ${} in it and reading it as text finds nothing.
137
+ */
138
+ giveUpAll(node, scope = this.scope) {
139
+ const attrs = node.attrs ?? [];
140
+ let inner = scope;
141
+
142
+ const each = attrs.find((attr) => attr.name === 'each');
143
+ const spec = each && EACH.exec(each.value);
144
+ if (spec) {
145
+ this.giveUp(spec[3], scope);
146
+ inner = new Scope(scope);
147
+ // The name only has to exist for collectRefs to stop calling it data.
148
+ inner.declare(spec[1], spec[1]);
149
+ if (spec[2]) inner.declare(spec[2], spec[2]);
150
+ }
151
+
152
+ for (const attr of attrs) {
153
+ if (attr.name === 'each') continue;
154
+ if (attr.name === 'if' || attr.name === 'else-if') {
155
+ this.giveUp(attr.value, scope);
156
+ continue;
157
+ }
158
+ if (attr.name === 'key') {
159
+ this.giveUp(attr.value, inner);
160
+ continue;
161
+ }
162
+ if (attr.name === 'else') continue;
163
+ this.giveUpText(attr.value, inner);
164
+ }
165
+
166
+ for (const child of childrenOf(node)) {
167
+ if (child.nodeName === '#text') this.giveUpText(child.value, inner);
168
+ else if (child.tagName) this.giveUpAll(child, inner);
169
+ }
170
+ }
171
+
172
+ abandon(slot) {
173
+ if (slot.kind === 'text') {
174
+ for (const node of slot.nodes) this.giveUpText(node.value);
175
+ return;
176
+ }
177
+ for (const node of slot.nodes) this.giveUpAll(node);
178
+ }
179
+
180
+ js(source) {
181
+ return emit(parseExpr(source), this.scope);
182
+ }
183
+
184
+ // ---- traversal ----------------------------------------------------------
185
+
186
+ /**
187
+ * `at` is either `{ parent, index }`, when the shape up to here is known, or
188
+ * `{ parent, from }`, a node expression to start stepping from.
189
+ */
190
+ walk(nodes, at) {
191
+ // `bare` means the directive on these nodes has already been consumed by
192
+ // the block that is asking for them. Without it a branch would find its own
193
+ // `if` again and recurse into itself forever.
194
+ const rendered = renderedChildren(nodes, at.bare);
195
+ const parentJs = at.parent;
196
+ let index = at.index ?? 0;
197
+ let cursor = null;
198
+
199
+ if (at.from) {
200
+ cursor = this.cursor();
201
+ this.locate(`${cursor} = ${at.from};`);
202
+ }
203
+
204
+ for (let i = 0; i < rendered.length; i++) {
205
+ const slot = rendered[i];
206
+ const here = cursor ?? `${parentJs}.childNodes[${index}]`;
207
+ const advance = (from) => {
208
+ if (cursor) this.locate(`${cursor} = ${from}.nextSibling;`);
209
+ else index++;
210
+ };
211
+
212
+ if (slot.kind === 'block') {
213
+ const ref = this.bindBlock(slot, here);
214
+ if (ref === null) {
215
+ for (const rest of rendered.slice(i)) this.abandon(rest);
216
+ this.frame.gaveUp = true;
217
+ return;
218
+ }
219
+ // Past a block the node count is not knowable, so addressing becomes
220
+ // relative from here on.
221
+ cursor = cursor ?? this.cursor();
222
+ this.locate(`${cursor} = __b[${ref}].end.nextSibling;`);
223
+ continue;
224
+ }
225
+
226
+ if (slot.kind === 'text') {
227
+ const bound = this.bindText(slot, parentJs, here);
228
+ if (!bound) {
229
+ for (const rest of rendered.slice(i)) this.abandon(rest);
230
+ this.frame.gaveUp = true;
231
+ return;
232
+ }
233
+ advance(bound.from ?? here);
234
+ // A split leaves the static tail behind as its own node.
235
+ if (cursor && bound.suffix) this.locate(`${cursor} = ${cursor}.nextSibling;`);
236
+ continue;
237
+ }
238
+
239
+ this.bindElement(slot.nodes[0], here, cursor === null);
240
+ advance(here);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * The block itself, plus a bind/update pair per branch (or per item) so its
246
+ * contents are written into rather than rebuilt.
247
+ */
248
+ bindBlock(slot, here) {
249
+ const id = this.blockOf.get(slot.nodes[0]);
250
+ if (id === undefined) return null;
251
+
252
+ const ref = this.next();
253
+ const args = `[${this.frame.loopArgs.join(', ')}]`;
254
+ this.locate(`__b[${ref}] = __blockAt(${here}, __blk${id}, __d, ${args});`);
255
+ this.write(`__ok = __updateBlock(__b[${ref}], __blk${id}, __d, ${args}) && __ok;`);
256
+
257
+ if (slot.each) this.emitItemPart(id, slot.nodes[0]);
258
+ else this.emitBranchParts(id, slot.branches);
259
+ return ref;
260
+ }
261
+
262
+ /**
263
+ * Which branch of the chain is showing. The runtime compares it with what it
264
+ * last saw: the same branch means write into it, a different one means
265
+ * rebuild. -1 is "none of them", which an `if` with no `else` can be.
266
+ */
267
+ emitBranchParts(id, branches) {
268
+ const pick = branches.reduceRight(
269
+ (rest, branch, at) =>
270
+ branch.kind === 'else' ? String(at) : `${this.js(branch.cond)} ? ${at} : ${rest}`,
271
+ '-1',
272
+ );
273
+ // The condition may read the loop variables, so pick takes them too. The
274
+ // runtime hands every piece of a block the same arguments.
275
+ const outer = this.frame.loopArgs;
276
+ this.parts.push(`__blk${id}.pick = (${['__d', ...outer].join(', ')}) => (${pick});`);
277
+
278
+ const parts = branches.map((branch) => {
279
+ const content = contentOf(branch.node);
280
+ return this.emitPart(content, outer, undefined, content[0] === branch.node, outer);
281
+ });
282
+ if (parts.every(Boolean)) this.parts.push(`__blk${id}.parts = [${parts.join(', ')}];`);
283
+ }
284
+
285
+ /** One part, reused for every item the loop produces. */
286
+ emitItemPart(id, element) {
287
+ const spec = EACH.exec(element.attrs.find((attr) => attr.name === 'each').value);
288
+ if (!spec) return;
289
+
290
+ const outer = this.frame.loopArgs;
291
+ const depth = outer.length / 2;
292
+ const item = `__it${depth}`;
293
+ const index = `__i${depth}`;
294
+
295
+ const scope = new Scope(this.scope);
296
+ scope.declare(spec[1], item);
297
+ if (spec[2]) scope.declare(spec[2], index);
298
+
299
+ const inner = [...outer, item, index];
300
+ // Same shape as a branch: the element itself, whose `each` is already
301
+ // consumed, or a template's content, which carries no directive at all.
302
+ const content = contentOf(element);
303
+ const part = this.emitPart(content, inner, scope, content[0] === element, inner);
304
+ if (part) this.parts.push(`__blk${id}.parts = [${part}];`);
305
+ }
306
+
307
+ /**
308
+ * A part is bind/update over a run of nodes starting at a node handed in
309
+ * rather than at a known index. Null where anything inside could not be
310
+ * bound, which leaves the block re-rendering as a whole. Still correct, just
311
+ * less precise.
312
+ */
313
+ emitPart(nodes, extra, scope = new Scope(this.scope), bare = false, loopArgs = []) {
314
+ this.frames.push(new Frame(scope, loopArgs));
315
+ const frame = this.frame;
316
+
317
+ this.locate('const __p = __n.parentNode;');
318
+ this.walk(nodes, { parent: '__p', from: '__n', bare });
319
+ this.frames.pop();
320
+
321
+ if (frame.gaveUp) return null;
322
+
323
+ const args = ['__d', ...extra].join(', ');
324
+ const cursors = frame.cursors
325
+ ? `let ${Array.from({ length: frame.cursors }, (_, i) => `__c${i}`).join(', ')};\n`
326
+ : '';
327
+ return (
328
+ `{ bind: (__n, ${args}) => { const __b = [];\n${cursors}${frame.locate.join('\n')}\n` +
329
+ `return __b; }, update: (__b, ${args}) => { let __ok = true;\n` +
330
+ `${frame.writes.join('\n')}\nreturn __ok; } }`
331
+ );
332
+ }
333
+
334
+ /**
335
+ * A ${} in mixed content is not its own text node. `Hello ${name}!` is one node
336
+ * reading "Hello Ada!". Both static sides have lengths known at compile time,
337
+ * so the dynamic middle splits out of it exactly.
338
+ *
339
+ * Two or more of them have no findable boundary between them, so the whole
340
+ * node is rewritten instead. Same result, one node rather than five.
341
+ */
342
+ bindText(slot, parentJs, here) {
343
+ const combined = slot.nodes.map((node) => node.value ?? '').join('');
344
+ const parts = splitInterpolations(combined);
345
+ const exprs = parts.filter((part) => part.type === 'expr');
346
+
347
+ if (!exprs.length) return { from: null, suffix: 0 };
348
+
349
+ let sources;
350
+ try {
351
+ sources = parts.map((part) =>
352
+ part.type === 'expr' ? this.js(part.value) : JSON.stringify(part.value),
353
+ );
354
+ } catch {
355
+ return null;
356
+ }
357
+
358
+ if (exprs.length > 1) {
359
+ const ref = this.next();
360
+ this.locate(`__b[${ref}] = __textAt(${parentJs}, ${here}, 0, 0);`);
361
+ this.write(`__ok = __setParts(__b[${ref}], [${sources.join(', ')}]) && __ok;`);
362
+ return { from: `__b[${ref}]`, suffix: 0 };
363
+ }
364
+
365
+ const at = parts.indexOf(exprs[0]);
366
+ const prefix = parts.slice(0, at).reduce((n, part) => n + part.value.length, 0);
367
+ const suffix = parts.slice(at + 1).reduce((n, part) => n + part.value.length, 0);
368
+
369
+ const ref = this.next();
370
+ this.locate(`__b[${ref}] = __textAt(${parentJs}, ${here}, ${prefix}, ${suffix});`);
371
+ this.write(`__ok = __setText(__b[${ref}], ${sources[at]}) && __ok;`);
372
+ return { from: `__b[${ref}]`, suffix };
373
+ }
374
+
375
+ bindElement(node, nodeExpr, stable) {
376
+ const tag = node.tagName;
377
+ const isComponent = this.components.has(tag);
378
+ // A light component's children in the DOM are its *own* rendered markup,
379
+ // not what we emitted here, and nothing repaints when its attributes
380
+ // change. A shadow one owns its shadow root and reacts to its attributes,
381
+ // so writing one is how an update reaches down into it.
382
+ const light = isComponent && !this.shadowTags.has(tag);
383
+
384
+ let ref = null;
385
+ // `update` runs long after `bind` returned, so anything it touches has to be
386
+ // held in the bindings array. A path like `__root.childNodes[0]` means nothing
387
+ // there.
388
+ const slotFor = () => {
389
+ if (ref === null) {
390
+ ref = this.next();
391
+ this.locate(`__b[${ref}] = ${nodeExpr};`);
392
+ }
393
+ return `__b[${ref}]`;
394
+ };
395
+ // Descending only ever happens inside `bind`, so a stable path is fine.
396
+ const parentExpr = () => (ref !== null ? `__b[${ref}]` : stable ? nodeExpr : slotFor());
397
+
398
+ for (const attr of node.attrs ?? []) {
399
+ if (DIRECTIVES.has(attr.name)) continue;
400
+ const parts = splitInterpolations(attr.value);
401
+ if (!parts.some((part) => part.type === 'expr')) continue;
402
+
403
+ if (light) {
404
+ this.giveUpText(attr.value);
405
+ continue;
406
+ }
407
+
408
+ let value;
409
+ try {
410
+ value =
411
+ parts.length === 1
412
+ ? this.js(parts[0].value)
413
+ : parts
414
+ .map((part) =>
415
+ part.type === 'expr' ? `__str(${this.js(part.value)})` : JSON.stringify(part.value),
416
+ )
417
+ .join(' + ');
418
+ } catch {
419
+ this.giveUpText(attr.value);
420
+ continue;
421
+ }
422
+ // A shadow child reads its attributes back through its own converters,
423
+ // so an update has to write them the way it will read them.
424
+ const childRef = this.refs.get(tag) ?? null;
425
+ this.write(
426
+ childRef
427
+ ? `__setAttrProp(${childRef}, ${slotFor()}, ${JSON.stringify(attr.name)}, ${value});`
428
+ : `__setAttr(${slotFor()}, ${JSON.stringify(attr.name)}, ${value});`,
429
+ );
430
+ }
431
+
432
+ if (VOID.has(tag)) return;
433
+
434
+ // A component renders its own insides; a plain <template> keeps its content
435
+ // in a DocumentFragment nothing here can address; raw text is not markup.
436
+ if (isComponent || tag === 'template' || RAW_TEXT.has(tag)) {
437
+ for (const child of childrenOf(node)) {
438
+ if (child.nodeName === '#text') this.giveUpText(child.value);
439
+ else if (child.tagName) this.giveUpAll(child);
440
+ }
441
+ return;
442
+ }
443
+
444
+ const children = childrenOf(node);
445
+ if (children.length) this.walk(children, { parent: parentExpr(), index: 0 });
446
+ }
447
+ }
448
+
449
+ // ---- helpers --------------------------------------------------------------
450
+
451
+ /** What a branch or an item renders: itself, or a template's content. */
452
+ function contentOf(node) {
453
+ return node.tagName === 'template' ? childrenOf(node) : [node];
454
+ }
455
+
456
+ /**
457
+ * The child list as the browser will build it: comments are dropped by the
458
+ * renderer, adjacent text runs become one node, and an if/else chain is a
459
+ * single region, including the whitespace between its branches, which the renderer
460
+ * never emits.
461
+ */
462
+ function renderedChildren(nodes, bare = false) {
463
+ const out = [];
464
+ let i = 0;
465
+
466
+ while (i < nodes.length) {
467
+ const node = nodes[i];
468
+
469
+ const chain = !bare && node.tagName ? gatherChain(nodes, i) : null;
470
+ if (chain) {
471
+ out.push({
472
+ kind: 'block',
473
+ nodes: chain.chain.map((branch) => branch.node),
474
+ branches: chain.chain,
475
+ });
476
+ i = chain.next;
477
+ continue;
478
+ }
479
+ i++;
480
+
481
+ if (node.nodeName === '#comment') continue;
482
+
483
+ if (node.nodeName === '#text') {
484
+ const last = out[out.length - 1];
485
+ if (last?.kind === 'text') last.nodes.push(node);
486
+ else out.push({ kind: 'text', nodes: [node] });
487
+ continue;
488
+ }
489
+ if (!node.tagName) continue;
490
+
491
+ const each = !bare && (node.attrs ?? []).some((attr) => attr.name === 'each');
492
+ const structural = !bare && (node.attrs ?? []).some((attr) => DIRECTIVES.has(attr.name));
493
+ out.push({ kind: structural ? 'block' : 'element', nodes: [node], each });
494
+ }
495
+ return out;
496
+ }