@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,221 @@
1
+ // Expression layer.
2
+ //
3
+ // jsep gives a small grammar: no assignment, no arrow functions, no object
4
+ // literals, no `new`. That keeps a template declarative rather than a second place
5
+ // to write code, and it is what makes collecting references below possible. The
6
+ // same thing lets `${user.nmae}` be checked against a known shape.
7
+
8
+ import jsep from 'jsep';
9
+
10
+ if (!jsep.binary_ops['??']) jsep.addBinaryOp('??', 1);
11
+
12
+ // Identifiers resolvable without being template data.
13
+ export const GLOBALS = new Set([
14
+ 'html',
15
+ // The one interpolation a <script> may carry. It has to resolve to the runtime
16
+ // function rather than to a field of the page's data, or the guard that lets it
17
+ // through would be naming something the author never has.
18
+ 'json',
19
+ 'Math',
20
+ 'JSON',
21
+ 'String',
22
+ 'Number',
23
+ 'Boolean',
24
+ 'Array',
25
+ 'Object',
26
+ 'Date',
27
+ 'isNaN',
28
+ 'parseInt',
29
+ 'parseFloat',
30
+ 'undefined',
31
+ 'NaN',
32
+ 'Infinity',
33
+ ]);
34
+
35
+ /**
36
+ * The names in scope at a point in the template, innermost first.
37
+ *
38
+ * A name a scope holds is a loop variable or a block binding and compiles to
39
+ * itself. Anything else is a field of the page's data and compiles to a lookup,
40
+ * which is what makes `${title}` mean `__d.title` with nothing declared.
41
+ */
42
+ export class Scope {
43
+ constructor(parent = null) {
44
+ this.parent = parent;
45
+ this.vars = new Map();
46
+ }
47
+
48
+ declare(name, js, shape) {
49
+ this.vars.set(name, { js, shape });
50
+ }
51
+
52
+ lookup(name) {
53
+ for (let s = this; s; s = s.parent) {
54
+ if (s.vars.has(name)) return s.vars.get(name);
55
+ }
56
+ return null;
57
+ }
58
+
59
+ // Used for the shadowing warning: does an *enclosing* scope already bind this?
60
+ outerHas(name) {
61
+ for (let s = this.parent; s; s = s.parent) {
62
+ if (s.vars.has(name)) return true;
63
+ }
64
+ return false;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * A template expression as an AST.
70
+ *
71
+ * Directive values are expressions, not interpolations: `each="tag of tags"` has
72
+ * no `${}` around it, so it is parsed rather than split.
73
+ *
74
+ * @param {string} source
75
+ * @returns {object} a jsep node
76
+ * @throws on an empty or unparseable expression
77
+ */
78
+ export function parseExpr(source) {
79
+ const src = String(source).trim();
80
+ if (!src) throw new Error('empty expression');
81
+ return jsep(src);
82
+ }
83
+
84
+ /**
85
+ * An AST back to JavaScript, with every free name resolved against `scope`.
86
+ *
87
+ * @param {object} node a jsep node
88
+ * @param {Scope} scope
89
+ * @returns {string} an expression, safe to place inside the generated render
90
+ */
91
+ export function emit(node, scope) {
92
+ switch (node.type) {
93
+ case 'Literal':
94
+ return node.raw !== undefined ? node.raw : JSON.stringify(node.value);
95
+
96
+ case 'Identifier': {
97
+ const local = scope.lookup(node.name);
98
+ if (local) return local.js;
99
+ if (GLOBALS.has(node.name)) return node.name;
100
+ return `__d[${JSON.stringify(node.name)}]`;
101
+ }
102
+
103
+ case 'MemberExpression':
104
+ return node.computed
105
+ ? `${emit(node.object, scope)}[${emit(node.property, scope)}]`
106
+ : `${emit(node.object, scope)}.${node.property.name}`;
107
+
108
+ case 'CallExpression':
109
+ return `${emit(node.callee, scope)}(${node.arguments.map((a) => emit(a, scope)).join(', ')})`;
110
+
111
+ case 'UnaryExpression':
112
+ return `(${node.operator}${emit(node.argument, scope)})`;
113
+
114
+ case 'BinaryExpression':
115
+ case 'LogicalExpression':
116
+ return `(${emit(node.left, scope)} ${node.operator} ${emit(node.right, scope)})`;
117
+
118
+ case 'ConditionalExpression':
119
+ return `(${emit(node.test, scope)} ? ${emit(node.consequent, scope)} : ${emit(node.alternate, scope)})`;
120
+
121
+ case 'ArrayExpression':
122
+ return `[${node.elements.map((e) => emit(e, scope)).join(', ')}]`;
123
+
124
+ case 'Compound':
125
+ throw new Error('expected a single expression, found several (stray `;` or `,`?)');
126
+
127
+ case 'ThisExpression':
128
+ throw new Error('`this` is not available in templates');
129
+
130
+ default:
131
+ throw new Error(`unsupported expression node: ${node.type}`);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * @typedef {object} Chain
137
+ * @property {'data'|'scope'} base what the path is rooted at
138
+ * @property {string} name the root's name
139
+ * @property {string[]} path the static segments read from it
140
+ * @property {boolean} open false once a computed access ended the path
141
+ * @property {object} [shape] the loop variable's element type, when known
142
+ */
143
+
144
+ /**
145
+ * The longest static property path rooted at template data or a loop variable,
146
+ * or null when the root is something we cannot follow (a call result, a
147
+ * literal). A computed access ends the path, so `a.b[i].c` gives `a.b`. Past `[i]`
148
+ * there is no way to know what is being read.
149
+ *
150
+ * @param {object} node
151
+ * @param {Scope} scope
152
+ * @param {object[]} [computed] collects the subscript expressions, which are
153
+ * themselves reads and have to be walked separately
154
+ * @returns {Chain|null}
155
+ */
156
+ export function chainOf(node, scope, computed = []) {
157
+ if (node.type === 'Identifier') {
158
+ const local = scope.lookup(node.name);
159
+ if (local) return { base: 'scope', name: node.name, shape: local.shape, path: [], open: true };
160
+ if (GLOBALS.has(node.name)) return null;
161
+ return { base: 'data', name: node.name, path: [], open: true };
162
+ }
163
+
164
+ if (node.type === 'MemberExpression') {
165
+ const inner = chainOf(node.object, scope, computed);
166
+ if (!inner) return null;
167
+ if (node.computed) {
168
+ computed.push(node.property);
169
+ return { ...inner, open: false };
170
+ }
171
+ if (inner.open) return { ...inner, path: [...inner.path, node.property.name] };
172
+ return inner;
173
+ }
174
+
175
+ return null;
176
+ }
177
+
178
+ /**
179
+ * Every data or loop-variable path an expression reads.
180
+ *
181
+ * This is what decides which bindings are volatile, and so whether a light
182
+ * element can write an update in place or needs a shadow root to rebuild.
183
+ *
184
+ * @param {object} node
185
+ * @param {Scope} scope
186
+ * @param {Chain[]} [out] accumulator, so a caller can collect across several
187
+ * @returns {Chain[]} one entry per read, in the order they were found
188
+ */
189
+ export function collectRefs(node, scope, out = []) {
190
+ if (!node || typeof node !== 'object') return out;
191
+
192
+ switch (node.type) {
193
+ case 'Identifier':
194
+ case 'MemberExpression': {
195
+ const computed = [];
196
+ const chain = chainOf(node, scope, computed);
197
+ if (chain) {
198
+ out.push(chain);
199
+ for (const inner of computed) collectRefs(inner, scope, out);
200
+ } else if (node.type === 'MemberExpression') {
201
+ collectRefs(node.object, scope, out);
202
+ if (node.computed) collectRefs(node.property, scope, out);
203
+ }
204
+ return out;
205
+ }
206
+
207
+ case 'CallExpression':
208
+ // The callee path is collected in full: a method that does not exist is
209
+ // as much a typo as a property that does not.
210
+ collectRefs(node.callee, scope, out);
211
+ for (const arg of node.arguments) collectRefs(arg, scope, out);
212
+ return out;
213
+
214
+ default:
215
+ for (const key of ['argument', 'left', 'right', 'test', 'consequent', 'alternate', 'object', 'property']) {
216
+ if (node[key]) collectRefs(node[key], scope, out);
217
+ }
218
+ for (const element of node.elements ?? []) collectRefs(element, scope, out);
219
+ return out;
220
+ }
221
+ }