@firsthandjs/compiler 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.
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/dist/api.d.ts +13 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/chunk-HHKCDDN2.js +841 -0
- package/dist/html.d.ts +19 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/ids.d.ts +9 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/transform.d.ts +33 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/vite.d.ts +18 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +28 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Firsthand contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @firsthandjs/compiler
|
|
2
|
+
|
|
3
|
+
The build-time half of Firsthand. Nothing here ships to the browser.
|
|
4
|
+
|
|
5
|
+
**Documentation:** [guide](https://github.com/firsthandjs/firsthand/blob/main/docs/guide/14-building.md) · [API reference](https://github.com/firsthandjs/firsthand/blob/main/docs/reference/compiler.md) · [all docs](https://github.com/firsthandjs/firsthand/blob/main/docs/README.md)
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// vite.config.ts
|
|
9
|
+
import { firsthand } from '@firsthandjs/compiler/vite';
|
|
10
|
+
|
|
11
|
+
export default { plugins: [firsthand({ packageName: 'my-app' })] };
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
What it does:
|
|
15
|
+
|
|
16
|
+
- hoists static markup into one `<template>` per shape, cloned per instance;
|
|
17
|
+
- turns every dynamic expression into a thunk handed to a specialised part;
|
|
18
|
+
- rewrites a keyed `.map()` into a list part with live item reads;
|
|
19
|
+
- gives each `component(...)` a stable build id that survives minification;
|
|
20
|
+
- refuses to compile props destructuring, because that would snapshot.
|
|
21
|
+
|
|
22
|
+
It emits calls against the published protocol in `@firsthandjs/dom/internal` and
|
|
23
|
+
has no privileged access to the runtime.
|
|
24
|
+
|
|
25
|
+
Full documentation: the [repository README](../../README.md) and
|
|
26
|
+
[ADR-0009](../../docs/adr/0009-compiler-templates-and-thunks.md).
|
|
27
|
+
|
|
28
|
+
MIT licensed.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type FirsthandPluginOptions } from './transform.js';
|
|
2
|
+
export interface TransformOptions extends FirsthandPluginOptions {
|
|
3
|
+
filename?: string;
|
|
4
|
+
/**
|
|
5
|
+
* Parse TypeScript syntax. The annotations are left in the output: stripping
|
|
6
|
+
* them is the bundler's job, and doing it here would duplicate work and
|
|
7
|
+
* source-map hops.
|
|
8
|
+
*/
|
|
9
|
+
typescript?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Compiles one module. Used by the Vite plugin and by the compiler tests. */
|
|
12
|
+
export declare function transform(code: string, options?: TransformOptions): string;
|
|
13
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAGA,OAAwB,EAAE,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAE9E,MAAM,WAAW,gBAAiB,SAAQ,sBAAsB;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAW9E"}
|
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
// packages/compiler/src/ids.ts
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
function stableId(packageName, filename) {
|
|
4
|
+
const normalised = filename.split(String.fromCharCode(92)).join("/").replace(/^(?:.*\/)?(?:src|lib)\//, "");
|
|
5
|
+
return createHash("sha256").update(`${packageName}:${normalised}`).digest("hex").slice(0, 8);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// packages/compiler/src/transform.ts
|
|
9
|
+
import * as t from "@babel/types";
|
|
10
|
+
|
|
11
|
+
// packages/compiler/src/html.ts
|
|
12
|
+
var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
13
|
+
"area",
|
|
14
|
+
"base",
|
|
15
|
+
"br",
|
|
16
|
+
"col",
|
|
17
|
+
"embed",
|
|
18
|
+
"hr",
|
|
19
|
+
"img",
|
|
20
|
+
"input",
|
|
21
|
+
"link",
|
|
22
|
+
"meta",
|
|
23
|
+
"param",
|
|
24
|
+
"source",
|
|
25
|
+
"track",
|
|
26
|
+
"wbr"
|
|
27
|
+
]);
|
|
28
|
+
var BOOLEAN_PROPERTIES = /* @__PURE__ */ new Set([
|
|
29
|
+
"checked",
|
|
30
|
+
"disabled",
|
|
31
|
+
"multiple",
|
|
32
|
+
"muted",
|
|
33
|
+
"readOnly",
|
|
34
|
+
"selected",
|
|
35
|
+
"autofocus",
|
|
36
|
+
"autoplay",
|
|
37
|
+
"controls",
|
|
38
|
+
"loop",
|
|
39
|
+
"open",
|
|
40
|
+
"required"
|
|
41
|
+
]);
|
|
42
|
+
var DOM_PROPERTIES = /* @__PURE__ */ new Set([
|
|
43
|
+
"value",
|
|
44
|
+
"textContent",
|
|
45
|
+
"innerHTML",
|
|
46
|
+
"innerText",
|
|
47
|
+
"scrollTop",
|
|
48
|
+
"scrollLeft",
|
|
49
|
+
"volume",
|
|
50
|
+
"currentTime",
|
|
51
|
+
"playbackRate",
|
|
52
|
+
"srcObject"
|
|
53
|
+
]);
|
|
54
|
+
var SVG_ELEMENTS = /* @__PURE__ */ new Set([
|
|
55
|
+
"svg",
|
|
56
|
+
"path",
|
|
57
|
+
"circle",
|
|
58
|
+
"rect",
|
|
59
|
+
"line",
|
|
60
|
+
"polyline",
|
|
61
|
+
"polygon",
|
|
62
|
+
"ellipse",
|
|
63
|
+
"g",
|
|
64
|
+
"defs",
|
|
65
|
+
"use",
|
|
66
|
+
"text",
|
|
67
|
+
"tspan",
|
|
68
|
+
"symbol",
|
|
69
|
+
"mask",
|
|
70
|
+
"pattern",
|
|
71
|
+
"clipPath",
|
|
72
|
+
"linearGradient",
|
|
73
|
+
"radialGradient",
|
|
74
|
+
"stop",
|
|
75
|
+
"filter"
|
|
76
|
+
]);
|
|
77
|
+
function escapeText(value) {
|
|
78
|
+
return value.replace(/&/g, "&").replace(/</g, "<");
|
|
79
|
+
}
|
|
80
|
+
function escapeAttribute(value) {
|
|
81
|
+
return value.replace(/&/g, "&").replace(/"/g, """);
|
|
82
|
+
}
|
|
83
|
+
function eventName(attribute) {
|
|
84
|
+
return attribute.slice(2).toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// packages/compiler/src/transform.ts
|
|
88
|
+
var RUNTIME = "@firsthandjs/dom/internal";
|
|
89
|
+
var CORE = "@firsthandjs/core";
|
|
90
|
+
function firsthandPlugin(_api, options = {}) {
|
|
91
|
+
return {
|
|
92
|
+
name: "firsthand",
|
|
93
|
+
visitor: {
|
|
94
|
+
Program: {
|
|
95
|
+
enter(_path, state) {
|
|
96
|
+
state.firsthand = {
|
|
97
|
+
imports: /* @__PURE__ */ new Map(),
|
|
98
|
+
templates: [],
|
|
99
|
+
counter: 0,
|
|
100
|
+
moduleId: stableId(options.packageName ?? "app", state.filename ?? "module")
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
exit(path, state) {
|
|
104
|
+
const { imports, templates } = state.firsthand;
|
|
105
|
+
if (templates.length > 0) {
|
|
106
|
+
path.node.body.unshift(t.variableDeclaration("const", templates));
|
|
107
|
+
}
|
|
108
|
+
for (const [source, names] of groupBySource(imports)) {
|
|
109
|
+
path.node.body.unshift(
|
|
110
|
+
t.importDeclaration(
|
|
111
|
+
names.map(([exported, local]) => t.importSpecifier(local, t.identifier(exported))),
|
|
112
|
+
t.stringLiteral(source)
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
// Babel enters the outermost JSX node first. Its nested elements are
|
|
119
|
+
// folded into the template and discarded; the nodes that survive (a
|
|
120
|
+
// component's children, a dynamic expression) are re-visited later, by
|
|
121
|
+
// which time they are no longer inside JSX.
|
|
122
|
+
JSXElement(path, state) {
|
|
123
|
+
rewriteKeyedMaps(path, state);
|
|
124
|
+
path.replaceWith(compileNode(path, state));
|
|
125
|
+
},
|
|
126
|
+
JSXFragment(path, state) {
|
|
127
|
+
rewriteKeyedMaps(path, state);
|
|
128
|
+
path.replaceWith(compileNode(path, state));
|
|
129
|
+
},
|
|
130
|
+
CallExpression(path, state) {
|
|
131
|
+
annotateComponent(path, state);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function groupBySource(imports) {
|
|
137
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const [key, local] of imports) {
|
|
139
|
+
const separator = key.indexOf("#");
|
|
140
|
+
const source = key.slice(0, separator);
|
|
141
|
+
const exported = key.slice(separator + 1);
|
|
142
|
+
const bucket = grouped.get(source);
|
|
143
|
+
if (bucket === void 0) {
|
|
144
|
+
grouped.set(source, [[exported, local]]);
|
|
145
|
+
} else {
|
|
146
|
+
bucket.push([exported, local]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return grouped;
|
|
150
|
+
}
|
|
151
|
+
function runtime(state, name, source = RUNTIME) {
|
|
152
|
+
const key = `${source}#${name}`;
|
|
153
|
+
let local = state.firsthand.imports.get(key);
|
|
154
|
+
if (local === void 0) {
|
|
155
|
+
local = t.identifier(`_$${name}`);
|
|
156
|
+
state.firsthand.imports.set(key, local);
|
|
157
|
+
}
|
|
158
|
+
return t.cloneNode(local);
|
|
159
|
+
}
|
|
160
|
+
function annotateComponent(path, state) {
|
|
161
|
+
const callee = path.node.callee;
|
|
162
|
+
if (!t.isIdentifier(callee, { name: "component" }) || path.node.arguments.length > 2) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const binding = path.scope.getBinding("component");
|
|
166
|
+
if (binding === void 0 || !isFirsthandImport(binding)) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const setup = path.node.arguments[0];
|
|
170
|
+
if (!t.isArrowFunctionExpression(setup) && !t.isFunctionExpression(setup)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const name = declaredName(path);
|
|
174
|
+
rewritePropsDestructuring(path, name, state);
|
|
175
|
+
path.node.arguments = [
|
|
176
|
+
setup,
|
|
177
|
+
path.node.arguments[1] ?? t.identifier("undefined"),
|
|
178
|
+
t.stringLiteral(`${state.firsthand.moduleId}/${name}`),
|
|
179
|
+
t.stringLiteral(name)
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
function isFirsthandImport(binding) {
|
|
183
|
+
const parent = binding.path.parentPath;
|
|
184
|
+
if (!parent.isImportDeclaration()) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
const source = parent.node.source.value;
|
|
188
|
+
return source === CORE || source.startsWith("@firsthandjs/") || source === "firsthand";
|
|
189
|
+
}
|
|
190
|
+
function declaredName(path) {
|
|
191
|
+
const parent = path.parentPath;
|
|
192
|
+
if (parent.isVariableDeclarator() && t.isIdentifier(parent.node.id)) {
|
|
193
|
+
return parent.node.id.name;
|
|
194
|
+
}
|
|
195
|
+
return "Component";
|
|
196
|
+
}
|
|
197
|
+
function rewritePropsDestructuring(call, name, state) {
|
|
198
|
+
const setup = call.get("arguments.0");
|
|
199
|
+
const params = setup.get("params");
|
|
200
|
+
const param = params[0];
|
|
201
|
+
if (param === void 0 || param.isIdentifier()) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!param.isObjectPattern()) {
|
|
205
|
+
throw call.buildCodeFrameError(
|
|
206
|
+
`${name}: a component's props parameter must be an object pattern or a plain identifier. Props are an object, so an array pattern cannot destructure them.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const propsId = setup.scope.generateUidIdentifier("props");
|
|
210
|
+
const reads = [];
|
|
211
|
+
const restNames = [];
|
|
212
|
+
collectPattern(param.node, t.cloneNode(propsId), reads, restNames, call, name);
|
|
213
|
+
setup.scope.crawl();
|
|
214
|
+
const references = [];
|
|
215
|
+
for (const read of [...reads, ...restNames.map((r) => ({ local: r.local, access: propsId }))]) {
|
|
216
|
+
const binding = setup.scope.getBinding(read.local);
|
|
217
|
+
if (binding.constantViolations.length > 0) {
|
|
218
|
+
throw call.buildCodeFrameError(
|
|
219
|
+
`${name}: \`${read.local}\` is destructured from props and then assigned to. Props are readonly; assign to a signal instead.`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
references.push({ paths: binding.referencePaths, access: read.access });
|
|
223
|
+
}
|
|
224
|
+
param.replaceWith(propsId);
|
|
225
|
+
for (const entry of references) {
|
|
226
|
+
for (const reference of entry.paths) {
|
|
227
|
+
if (entry.access !== propsId) {
|
|
228
|
+
reference.replaceWith(t.cloneNode(entry.access));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (restNames.length > 0) {
|
|
233
|
+
if (!setup.get("body").isBlockStatement()) {
|
|
234
|
+
const body = setup.node.body;
|
|
235
|
+
setup.node.body = t.blockStatement([t.returnStatement(body)]);
|
|
236
|
+
}
|
|
237
|
+
const block = setup.get("body");
|
|
238
|
+
for (const rest of restNames) {
|
|
239
|
+
block.node.body.unshift(
|
|
240
|
+
t.variableDeclaration("const", [
|
|
241
|
+
t.variableDeclarator(
|
|
242
|
+
t.identifier(rest.local),
|
|
243
|
+
t.callExpression(runtime(state, "rest"), [
|
|
244
|
+
t.cloneNode(propsId),
|
|
245
|
+
t.arrayExpression(rest.omit.map((key) => t.stringLiteral(key)))
|
|
246
|
+
])
|
|
247
|
+
)
|
|
248
|
+
])
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function collectPattern(pattern, base, reads, rests, call, name) {
|
|
254
|
+
const seen = [];
|
|
255
|
+
for (const property of pattern.properties) {
|
|
256
|
+
if (t.isRestElement(property)) {
|
|
257
|
+
rests.push({ local: property.argument.name, omit: [...seen] });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (property.computed || !t.isIdentifier(property.key)) {
|
|
261
|
+
throw call.buildCodeFrameError(
|
|
262
|
+
`${name}: props can only be destructured by plain key. A computed key would have to be resolved at setup time, which is exactly the snapshot this rewrite avoids.`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
seen.push(property.key.name);
|
|
266
|
+
const access = t.memberExpression(
|
|
267
|
+
t.cloneNode(base),
|
|
268
|
+
t.identifier(property.key.name)
|
|
269
|
+
);
|
|
270
|
+
const value = property.value;
|
|
271
|
+
if (t.isIdentifier(value)) {
|
|
272
|
+
reads.push({ local: value.name, access });
|
|
273
|
+
} else if (t.isAssignmentPattern(value) && t.isIdentifier(value.left)) {
|
|
274
|
+
reads.push({
|
|
275
|
+
local: value.left.name,
|
|
276
|
+
access: t.logicalExpression("??", access, value.right)
|
|
277
|
+
});
|
|
278
|
+
} else if (t.isObjectPattern(value)) {
|
|
279
|
+
collectPattern(value, access, reads, rests, call, name);
|
|
280
|
+
} else {
|
|
281
|
+
throw call.buildCodeFrameError(
|
|
282
|
+
`${name}: this destructuring pattern cannot be rewritten into live reads. Take the props object and read from it where you need the value.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function compileNode(path, state) {
|
|
288
|
+
if (path.isJSXFragment()) {
|
|
289
|
+
return t.arrayExpression(compileChildren(path.node.children, state));
|
|
290
|
+
}
|
|
291
|
+
const node = path.node;
|
|
292
|
+
if (isComponentTag(node)) {
|
|
293
|
+
return compileComponent(path, state);
|
|
294
|
+
}
|
|
295
|
+
return compileTemplate(path, state);
|
|
296
|
+
}
|
|
297
|
+
function isComponentTag(node) {
|
|
298
|
+
const name = node.openingElement.name;
|
|
299
|
+
if (t.isJSXMemberExpression(name)) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
if (!t.isJSXIdentifier(name)) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
return /^[A-Z]/.test(name.name);
|
|
306
|
+
}
|
|
307
|
+
function tagExpression(name) {
|
|
308
|
+
if (t.isJSXIdentifier(name)) {
|
|
309
|
+
return t.identifier(name.name);
|
|
310
|
+
}
|
|
311
|
+
return t.memberExpression(tagExpression(name.object), t.identifier(name.property.name));
|
|
312
|
+
}
|
|
313
|
+
function compileComponent(path, state) {
|
|
314
|
+
const node = path.node;
|
|
315
|
+
const properties = [];
|
|
316
|
+
for (const attribute of node.openingElement.attributes) {
|
|
317
|
+
if (t.isJSXSpreadAttribute(attribute)) {
|
|
318
|
+
properties.push(t.spreadElement(attribute.argument));
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const name = attributeName(attribute);
|
|
322
|
+
const value = attributeValue(attribute);
|
|
323
|
+
if (value === null || isStaticValue(value)) {
|
|
324
|
+
properties.push(t.objectProperty(propertyKey(name), value ?? t.booleanLiteral(true)));
|
|
325
|
+
} else {
|
|
326
|
+
properties.push(
|
|
327
|
+
t.objectMethod("get", propertyKey(name), [], t.blockStatement([t.returnStatement(value)]))
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const children = compileChildren(node.children, state);
|
|
332
|
+
if (children.length === 1) {
|
|
333
|
+
properties.push(
|
|
334
|
+
t.objectMethod(
|
|
335
|
+
"get",
|
|
336
|
+
t.identifier("children"),
|
|
337
|
+
[],
|
|
338
|
+
t.blockStatement([t.returnStatement(children[0])])
|
|
339
|
+
)
|
|
340
|
+
);
|
|
341
|
+
} else if (children.length > 1) {
|
|
342
|
+
properties.push(
|
|
343
|
+
t.objectMethod(
|
|
344
|
+
"get",
|
|
345
|
+
t.identifier("children"),
|
|
346
|
+
[],
|
|
347
|
+
t.blockStatement([t.returnStatement(t.arrayExpression(children))])
|
|
348
|
+
)
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return t.callExpression(runtime(state, "createComponent"), [
|
|
352
|
+
// `isComponentTag` has already excluded namespaced names.
|
|
353
|
+
tagExpression(node.openingElement.name),
|
|
354
|
+
t.objectExpression(properties)
|
|
355
|
+
]);
|
|
356
|
+
}
|
|
357
|
+
function propertyKey(name) {
|
|
358
|
+
return t.isValidIdentifier(name) ? t.identifier(name) : t.stringLiteral(name);
|
|
359
|
+
}
|
|
360
|
+
function attributeName(attribute) {
|
|
361
|
+
const name = attribute.name;
|
|
362
|
+
return t.isJSXNamespacedName(name) ? `${name.namespace.name}:${name.name.name}` : name.name;
|
|
363
|
+
}
|
|
364
|
+
function attributeValue(attribute) {
|
|
365
|
+
const value = attribute.value;
|
|
366
|
+
if (value === null || value === void 0) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
if (t.isStringLiteral(value)) {
|
|
370
|
+
return value;
|
|
371
|
+
}
|
|
372
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
373
|
+
return value.expression;
|
|
374
|
+
}
|
|
375
|
+
throw new Error("An element is not a valid attribute value. Wrap it in braces: attr={<El />}.");
|
|
376
|
+
}
|
|
377
|
+
function isStaticValue(value) {
|
|
378
|
+
return t.isStringLiteral(value) || t.isNumericLiteral(value) || t.isBooleanLiteral(value) || t.isNullLiteral(value);
|
|
379
|
+
}
|
|
380
|
+
function compileTemplate(path, state) {
|
|
381
|
+
const build = {
|
|
382
|
+
html: [],
|
|
383
|
+
statements: [],
|
|
384
|
+
next: /* @__PURE__ */ (() => {
|
|
385
|
+
let n = 0;
|
|
386
|
+
return () => t.identifier(`_el$${String(++n)}`);
|
|
387
|
+
})()
|
|
388
|
+
};
|
|
389
|
+
const root = t.identifier("_el$");
|
|
390
|
+
emitElement(path.node, build, root, state);
|
|
391
|
+
const templateId = t.identifier(`_tmpl$${String(++state.firsthand.counter)}`);
|
|
392
|
+
state.firsthand.templates.push(
|
|
393
|
+
t.variableDeclarator(
|
|
394
|
+
templateId,
|
|
395
|
+
t.addComment(
|
|
396
|
+
t.callExpression(runtime(state, "template"), [t.stringLiteral(build.html.join(""))]),
|
|
397
|
+
"leading",
|
|
398
|
+
"#__PURE__"
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
);
|
|
402
|
+
const body = [
|
|
403
|
+
t.variableDeclaration("const", [
|
|
404
|
+
t.variableDeclarator(root, t.callExpression(t.cloneNode(templateId), []))
|
|
405
|
+
]),
|
|
406
|
+
...build.statements,
|
|
407
|
+
t.returnStatement(t.cloneNode(root))
|
|
408
|
+
];
|
|
409
|
+
return t.callExpression(t.arrowFunctionExpression([], t.blockStatement(body)), []);
|
|
410
|
+
}
|
|
411
|
+
function emitElement(node, build, self, state) {
|
|
412
|
+
const name = node.openingElement.name;
|
|
413
|
+
if (!t.isJSXIdentifier(name)) {
|
|
414
|
+
const namespaced = name;
|
|
415
|
+
throw new Error(
|
|
416
|
+
`Namespaced element names are not supported: <${namespaced.namespace.name}:${namespaced.name.name}>. Write the element without a namespace; SVG children are resolved by the parser.`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const tag = name.name;
|
|
420
|
+
build.html.push(`<${tag}`);
|
|
421
|
+
const deferred = [];
|
|
422
|
+
for (const attribute of node.openingElement.attributes) {
|
|
423
|
+
emitAttribute(attribute, build, self, state, deferred, tag);
|
|
424
|
+
}
|
|
425
|
+
build.html.push(">");
|
|
426
|
+
for (const emit of deferred) {
|
|
427
|
+
emit();
|
|
428
|
+
}
|
|
429
|
+
emitChildren(node.children, build, self, state);
|
|
430
|
+
if (!VOID_ELEMENTS.has(tag)) {
|
|
431
|
+
build.html.push(`</${tag}>`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function emitAttribute(attribute, build, self, state, deferred, tag) {
|
|
435
|
+
if (t.isJSXSpreadAttribute(attribute)) {
|
|
436
|
+
deferred.push(() => {
|
|
437
|
+
build.statements.push(
|
|
438
|
+
expressionStatement2(
|
|
439
|
+
t.callExpression(runtime(state, "bind"), [
|
|
440
|
+
t.arrowFunctionExpression(
|
|
441
|
+
[],
|
|
442
|
+
t.callExpression(runtime(state, "spread"), [t.cloneNode(self), attribute.argument])
|
|
443
|
+
)
|
|
444
|
+
])
|
|
445
|
+
)
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const name = attributeName(attribute);
|
|
451
|
+
const value = attributeValue(attribute);
|
|
452
|
+
if (name === "ref") {
|
|
453
|
+
deferred.push(() => {
|
|
454
|
+
build.statements.push(
|
|
455
|
+
expressionStatement2(t.callExpression(value, [t.cloneNode(self)]))
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (name.startsWith("on") && name.length > 2 && /[A-Z:]/.test(name[2])) {
|
|
461
|
+
const parts = name.split(":");
|
|
462
|
+
const literal = parts[0] === "on";
|
|
463
|
+
const type = literal ? parts[1] : eventName(parts[0]);
|
|
464
|
+
const modifier = parts[literal ? 2 : 1];
|
|
465
|
+
deferred.push(() => {
|
|
466
|
+
const args = [
|
|
467
|
+
t.cloneNode(self),
|
|
468
|
+
t.stringLiteral(type),
|
|
469
|
+
value
|
|
470
|
+
];
|
|
471
|
+
if (modifier === "native") {
|
|
472
|
+
args.push(t.booleanLiteral(true));
|
|
473
|
+
} else if (modifier !== void 0) {
|
|
474
|
+
args.push(
|
|
475
|
+
t.objectExpression([t.objectProperty(propertyKey(modifier), t.booleanLiteral(true))])
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
build.statements.push(expressionStatement2(t.callExpression(runtime(state, "on"), args)));
|
|
479
|
+
});
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (value !== null && isStaticValue(value) && canInlineAttribute(name)) {
|
|
483
|
+
if (t.isBooleanLiteral(value) && !value.value) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (t.isNullLiteral(value)) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const literal = t.isStringLiteral(value) ? value.value : t.isNumericLiteral(value) ? String(value.value) : "";
|
|
490
|
+
build.html.push(` ${name}="${escapeAttribute(literal)}"`);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (value === null) {
|
|
494
|
+
build.html.push(` ${name}=""`);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
deferred.push(() => {
|
|
498
|
+
build.statements.push(
|
|
499
|
+
expressionStatement2(
|
|
500
|
+
t.callExpression(runtime(state, "bind"), [
|
|
501
|
+
t.arrowFunctionExpression([], dynamicAttributeCall(name, value, self, state, tag))
|
|
502
|
+
])
|
|
503
|
+
)
|
|
504
|
+
);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
function canInlineAttribute(name) {
|
|
508
|
+
return !name.startsWith("prop:") && !name.startsWith("attr:") && !DOM_PROPERTIES.has(name) && !BOOLEAN_PROPERTIES.has(name);
|
|
509
|
+
}
|
|
510
|
+
function dynamicAttributeCall(name, value, self, state, tag) {
|
|
511
|
+
if (name.startsWith("prop:")) {
|
|
512
|
+
return t.callExpression(runtime(state, "setProperty"), [
|
|
513
|
+
t.cloneNode(self),
|
|
514
|
+
t.stringLiteral(name.slice(5)),
|
|
515
|
+
value
|
|
516
|
+
]);
|
|
517
|
+
}
|
|
518
|
+
if (name.startsWith("attr:")) {
|
|
519
|
+
return t.callExpression(runtime(state, "setAttribute"), [
|
|
520
|
+
t.cloneNode(self),
|
|
521
|
+
t.stringLiteral(name.slice(5)),
|
|
522
|
+
value
|
|
523
|
+
]);
|
|
524
|
+
}
|
|
525
|
+
if (name === "class" || name === "className" || name === "style") {
|
|
526
|
+
return t.callExpression(runtime(state, "applyProp"), [
|
|
527
|
+
t.cloneNode(self),
|
|
528
|
+
t.stringLiteral(name),
|
|
529
|
+
value
|
|
530
|
+
]);
|
|
531
|
+
}
|
|
532
|
+
if (BOOLEAN_PROPERTIES.has(name)) {
|
|
533
|
+
return t.callExpression(runtime(state, "setBoolean"), [
|
|
534
|
+
t.cloneNode(self),
|
|
535
|
+
t.stringLiteral(name),
|
|
536
|
+
value
|
|
537
|
+
]);
|
|
538
|
+
}
|
|
539
|
+
if (DOM_PROPERTIES.has(name) && !SVG_ELEMENTS.has(tag)) {
|
|
540
|
+
return t.callExpression(runtime(state, "setProperty"), [
|
|
541
|
+
t.cloneNode(self),
|
|
542
|
+
t.stringLiteral(name),
|
|
543
|
+
value
|
|
544
|
+
]);
|
|
545
|
+
}
|
|
546
|
+
return t.callExpression(runtime(state, "setAttribute"), [
|
|
547
|
+
t.cloneNode(self),
|
|
548
|
+
t.stringLiteral(name),
|
|
549
|
+
value
|
|
550
|
+
]);
|
|
551
|
+
}
|
|
552
|
+
var LIST_CALL = /* @__PURE__ */ Symbol("firsthand.list");
|
|
553
|
+
function rewriteKeyedMaps(path, state) {
|
|
554
|
+
path.traverse({
|
|
555
|
+
CallExpression(call) {
|
|
556
|
+
if (!isChildPosition(call)) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const callee = call.node.callee;
|
|
560
|
+
if (!t.isMemberExpression(callee) || callee.computed || !t.isIdentifier(callee.property, { name: "map" }) || call.node.arguments.length !== 1) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const callback = call.get("arguments.0");
|
|
564
|
+
if (!callback.isArrowFunctionExpression() && !callback.isFunctionExpression()) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const params = callback.node.params;
|
|
568
|
+
const itemParam = params[0];
|
|
569
|
+
const indexParam = params[1];
|
|
570
|
+
if (!t.isIdentifier(itemParam) || indexParam !== void 0 && !t.isIdentifier(indexParam)) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const root = keyedRoot(callback);
|
|
574
|
+
if (root === null) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const keyExpression = t.cloneNode(root.key);
|
|
578
|
+
root.element.openingElement.attributes = root.element.openingElement.attributes.filter(
|
|
579
|
+
(attribute) => attribute !== root.attribute
|
|
580
|
+
);
|
|
581
|
+
const itemCell = callback.scope.generateUidIdentifier("item");
|
|
582
|
+
const indexCell = callback.scope.generateUidIdentifier("index");
|
|
583
|
+
callback.scope.crawl();
|
|
584
|
+
liveRead(callback, itemParam.name, itemCell);
|
|
585
|
+
if (indexParam !== void 0) {
|
|
586
|
+
liveRead(callback, indexParam.name, indexCell);
|
|
587
|
+
}
|
|
588
|
+
callback.node.params = [itemCell, indexCell];
|
|
589
|
+
const listCall = t.callExpression(runtime(state, "list"), [
|
|
590
|
+
t.arrowFunctionExpression([], callee.object),
|
|
591
|
+
t.arrowFunctionExpression(
|
|
592
|
+
[t.cloneNode(itemParam), indexParam ?? callback.scope.generateUidIdentifier("i")],
|
|
593
|
+
keyExpression
|
|
594
|
+
),
|
|
595
|
+
callback.node
|
|
596
|
+
]);
|
|
597
|
+
listCall[LIST_CALL] = true;
|
|
598
|
+
call.replaceWith(listCall);
|
|
599
|
+
call.skip();
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
function isChildPosition(call) {
|
|
604
|
+
let child = call;
|
|
605
|
+
let parent = child.parentPath;
|
|
606
|
+
while (!parent.isJSXExpressionContainer()) {
|
|
607
|
+
const key = child.key;
|
|
608
|
+
const throughConditional = parent.isConditionalExpression() && (key === "consequent" || key === "alternate");
|
|
609
|
+
const throughLogical = parent.isLogicalExpression() && key === "right";
|
|
610
|
+
if (!throughConditional && !throughLogical) {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
child = parent;
|
|
614
|
+
parent = parent.parentPath;
|
|
615
|
+
}
|
|
616
|
+
const holder = parent.parentPath;
|
|
617
|
+
return holder.isJSXElement() || holder.isJSXFragment();
|
|
618
|
+
}
|
|
619
|
+
function keyedRoot(callback) {
|
|
620
|
+
const body = callback.node.body;
|
|
621
|
+
let element = null;
|
|
622
|
+
if (t.isJSXElement(body)) {
|
|
623
|
+
element = body;
|
|
624
|
+
} else if (t.isBlockStatement(body)) {
|
|
625
|
+
const last = body.body[body.body.length - 1];
|
|
626
|
+
if (t.isReturnStatement(last) && t.isJSXElement(last.argument)) {
|
|
627
|
+
element = last.argument;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (element === null || !t.isJSXElement(element)) {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
for (const attribute of element.openingElement.attributes) {
|
|
634
|
+
if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: "key" }) && t.isJSXExpressionContainer(attribute.value)) {
|
|
635
|
+
return { element, attribute, key: attribute.value.expression };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
function liveRead(scopePath, name, cell) {
|
|
641
|
+
const binding = scopePath.scope.getBinding(name);
|
|
642
|
+
for (const reference of binding.referencePaths) {
|
|
643
|
+
reference.replaceWith(t.memberExpression(t.cloneNode(cell), t.identifier("value")));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function emitChildren(children, build, self, state) {
|
|
647
|
+
const entries = planChildren(children);
|
|
648
|
+
let previous = null;
|
|
649
|
+
let previousIndex = 0;
|
|
650
|
+
let index = 0;
|
|
651
|
+
const reference = () => {
|
|
652
|
+
const id = build.next();
|
|
653
|
+
build.statements.push(
|
|
654
|
+
t.variableDeclaration("const", [
|
|
655
|
+
t.variableDeclarator(id, navigate(self, previous, previousIndex, index))
|
|
656
|
+
])
|
|
657
|
+
);
|
|
658
|
+
previous = id;
|
|
659
|
+
previousIndex = index;
|
|
660
|
+
return id;
|
|
661
|
+
};
|
|
662
|
+
for (let i = 0; i < entries.length; i++) {
|
|
663
|
+
const entry = entries[i];
|
|
664
|
+
if (entry.kind === "text") {
|
|
665
|
+
build.html.push(escapeText(entry.text));
|
|
666
|
+
index++;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (entry.kind === "element") {
|
|
670
|
+
const element = entry.element;
|
|
671
|
+
if (needsReference(element)) {
|
|
672
|
+
const id = reference();
|
|
673
|
+
emitElement(element, build, id, state);
|
|
674
|
+
} else {
|
|
675
|
+
emitElement(element, build, t.identifier("_unused$"), state);
|
|
676
|
+
}
|
|
677
|
+
index++;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const isLast = i === entries.length - 1;
|
|
681
|
+
const args = [
|
|
682
|
+
t.cloneNode(self),
|
|
683
|
+
// A list part is already a thunk that owns its rows; wrapping it would
|
|
684
|
+
// rebuild the whole list on every evaluation.
|
|
685
|
+
entry.kind === "list" ? entry.expression : t.arrowFunctionExpression([], entry.expression)
|
|
686
|
+
];
|
|
687
|
+
if (!isLast) {
|
|
688
|
+
build.html.push("<!>");
|
|
689
|
+
const id = reference();
|
|
690
|
+
args.push(t.cloneNode(id));
|
|
691
|
+
}
|
|
692
|
+
build.statements.push(expressionStatement2(t.callExpression(runtime(state, "insert"), args)));
|
|
693
|
+
index++;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function needsReference(element) {
|
|
697
|
+
for (const attribute of element.openingElement.attributes) {
|
|
698
|
+
if (t.isJSXSpreadAttribute(attribute)) {
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
const name = attributeName(attribute);
|
|
702
|
+
if (name === "ref" || name.startsWith("on")) {
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
const value = attributeValue(attribute);
|
|
706
|
+
if (value !== null && (!isStaticValue(value) || !canInlineAttribute(name))) {
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
for (const entry of planChildren(element.children)) {
|
|
711
|
+
if (entry.kind === "dynamic" || entry.kind === "list") {
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
if (entry.kind === "element" && needsReference(entry.element)) {
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
function navigate(self, previous, previousIndex, index) {
|
|
721
|
+
let expression;
|
|
722
|
+
let steps;
|
|
723
|
+
if (previous === null) {
|
|
724
|
+
expression = t.memberExpression(t.cloneNode(self), t.identifier("firstChild"));
|
|
725
|
+
steps = index;
|
|
726
|
+
} else {
|
|
727
|
+
expression = t.cloneNode(previous);
|
|
728
|
+
steps = index - previousIndex;
|
|
729
|
+
}
|
|
730
|
+
for (let i = 0; i < steps; i++) {
|
|
731
|
+
expression = t.memberExpression(expression, t.identifier("nextSibling"));
|
|
732
|
+
}
|
|
733
|
+
return expression;
|
|
734
|
+
}
|
|
735
|
+
function planChildren(children) {
|
|
736
|
+
const entries = [];
|
|
737
|
+
let pending = "";
|
|
738
|
+
const flush = () => {
|
|
739
|
+
if (pending !== "") {
|
|
740
|
+
entries.push({ kind: "text", text: pending });
|
|
741
|
+
pending = "";
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
for (const child of children) {
|
|
745
|
+
if (t.isJSXText(child)) {
|
|
746
|
+
pending += cleanText(child.value);
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (t.isJSXExpressionContainer(child)) {
|
|
750
|
+
const expression = child.expression;
|
|
751
|
+
if (t.isJSXEmptyExpression(expression)) {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
if (t.isStringLiteral(expression) || t.isNumericLiteral(expression)) {
|
|
755
|
+
pending += String(expression.value);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
flush();
|
|
759
|
+
const isList = expression[LIST_CALL] === true;
|
|
760
|
+
entries.push({ kind: isList ? "list" : "dynamic", expression });
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
flush();
|
|
764
|
+
if (t.isJSXElement(child) && !isComponentTag(child)) {
|
|
765
|
+
entries.push({ kind: "element", element: child });
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
entries.push({ kind: "dynamic", expression: child });
|
|
769
|
+
}
|
|
770
|
+
flush();
|
|
771
|
+
return entries;
|
|
772
|
+
}
|
|
773
|
+
function cleanText(value) {
|
|
774
|
+
const lines = value.split(/\r\n|\n|\r/);
|
|
775
|
+
const kept = [];
|
|
776
|
+
for (let i = 0; i < lines.length; i++) {
|
|
777
|
+
let line = lines[i];
|
|
778
|
+
if (i > 0) {
|
|
779
|
+
line = line.replace(/^[\t ]+/, "");
|
|
780
|
+
}
|
|
781
|
+
if (i < lines.length - 1) {
|
|
782
|
+
line = line.replace(/[\t ]+$/, "");
|
|
783
|
+
}
|
|
784
|
+
if (line !== "") {
|
|
785
|
+
kept.push(line);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return kept.join(" ");
|
|
789
|
+
}
|
|
790
|
+
function compileChildren(children, state) {
|
|
791
|
+
const result = [];
|
|
792
|
+
for (const entry of planChildren(children)) {
|
|
793
|
+
if (entry.kind === "text") {
|
|
794
|
+
result.push(t.stringLiteral(entry.text));
|
|
795
|
+
} else if (entry.kind === "element") {
|
|
796
|
+
result.push(entry.element);
|
|
797
|
+
} else if (entry.kind === "list") {
|
|
798
|
+
result.push(t.callExpression(runtime(state, "part"), [entry.expression]));
|
|
799
|
+
} else {
|
|
800
|
+
result.push(
|
|
801
|
+
t.callExpression(runtime(state, "part"), [
|
|
802
|
+
t.arrowFunctionExpression([], entry.expression)
|
|
803
|
+
])
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return result;
|
|
808
|
+
}
|
|
809
|
+
function expressionStatement2(expression) {
|
|
810
|
+
return t.expressionStatement(expression);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// packages/compiler/src/api.ts
|
|
814
|
+
import { transformSync } from "@babel/core";
|
|
815
|
+
import jsxSyntax from "@babel/plugin-syntax-jsx";
|
|
816
|
+
import typescriptSyntax from "@babel/plugin-syntax-typescript";
|
|
817
|
+
function transform(code, options = {}) {
|
|
818
|
+
const result = transformSync(code, {
|
|
819
|
+
...options.filename === void 0 ? {} : { filename: options.filename },
|
|
820
|
+
babelrc: false,
|
|
821
|
+
configFile: false,
|
|
822
|
+
sourceMaps: false,
|
|
823
|
+
plugins: buildPlugins(options)
|
|
824
|
+
});
|
|
825
|
+
return result.code;
|
|
826
|
+
}
|
|
827
|
+
function buildPlugins(options) {
|
|
828
|
+
const plugins = [];
|
|
829
|
+
if (options.typescript === true) {
|
|
830
|
+
plugins.push([typescriptSyntax, { isTSX: true }]);
|
|
831
|
+
}
|
|
832
|
+
plugins.push(jsxSyntax);
|
|
833
|
+
plugins.push([firsthandPlugin, { packageName: options.packageName }]);
|
|
834
|
+
return plugins;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
export {
|
|
838
|
+
stableId,
|
|
839
|
+
firsthandPlugin,
|
|
840
|
+
transform
|
|
841
|
+
};
|
package/dist/html.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Static tables and escaping used while building a template's HTML. */
|
|
2
|
+
/** Elements that must not be closed. */
|
|
3
|
+
export declare const VOID_ELEMENTS: Set<string>;
|
|
4
|
+
/** Attributes written as boolean DOM properties rather than attributes. */
|
|
5
|
+
export declare const BOOLEAN_PROPERTIES: Set<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Attributes whose dynamic form must be a DOM property.
|
|
8
|
+
*
|
|
9
|
+
* `value` is the canonical example: setting the attribute does not change what
|
|
10
|
+
* the user sees once the field has been edited.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DOM_PROPERTIES: Set<string>;
|
|
13
|
+
/** SVG elements, so the compiler can emit namespaced templates. */
|
|
14
|
+
export declare const SVG_ELEMENTS: Set<string>;
|
|
15
|
+
export declare function escapeText(value: string): string;
|
|
16
|
+
export declare function escapeAttribute(value: string): string;
|
|
17
|
+
/** `onPointerDown` -> `pointerdown`. */
|
|
18
|
+
export declare function eventName(attribute: string): string;
|
|
19
|
+
//# sourceMappingURL=html.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AAAA,wEAAwE;AAExE,wCAAwC;AACxC,eAAO,MAAM,aAAa,aAexB,CAAC;AAEH,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,aAa7B,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,cAAc,aAWzB,CAAC;AAEH,mEAAmE;AACnE,eAAO,MAAM,YAAY,aAsBvB,CAAC;AAEH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wCAAwC;AACxC,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEnD"}
|
package/dist/ids.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A short, stable identifier for a module (ADR-0004).
|
|
3
|
+
*
|
|
4
|
+
* Derived from the package name and the module path relative to the project, so
|
|
5
|
+
* it survives minification — unlike `Function.name` — and stays the same across
|
|
6
|
+
* builds, unlike a runtime counter.
|
|
7
|
+
*/
|
|
8
|
+
export declare function stableId(packageName: string, filename: string): string;
|
|
9
|
+
//# sourceMappingURL=ids.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../src/ids.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMtE"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@firsthandjs/compiler` — the build-time half of Firsthand.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here ships to the browser. The transform emits calls against the
|
|
5
|
+
* published protocol in `@firsthandjs/dom/internal`, so compiled output is code a
|
|
6
|
+
* developer could have written by hand (ARCHITECTURE section 1.1).
|
|
7
|
+
*/
|
|
8
|
+
export { default as firsthandPlugin } from './transform.js';
|
|
9
|
+
export type { FirsthandPluginOptions } from './transform.js';
|
|
10
|
+
export { transform } from './api.js';
|
|
11
|
+
export { stableId } from './ids.js';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC5D,YAAY,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Firsthand TSX transform (ADR-0009).
|
|
3
|
+
*
|
|
4
|
+
* Static markup becomes one `<template>` per shape, created once and cloned per
|
|
5
|
+
* instance. Every dynamic expression becomes a thunk handed to a specialised
|
|
6
|
+
* DOM part, and the runtime decides by observation whether an effect needs to
|
|
7
|
+
* be retained. The compiler never tries to decide statically whether an
|
|
8
|
+
* expression is reactive — that question is undecidable in general, and being
|
|
9
|
+
* wrong about it means silently missing updates.
|
|
10
|
+
*
|
|
11
|
+
* The transform emits calls against the published protocol in
|
|
12
|
+
* `@firsthandjs/dom/internal` and has no privileged access to the runtime.
|
|
13
|
+
*/
|
|
14
|
+
import type { PluginObject } from '@babel/core';
|
|
15
|
+
import * as t from '@babel/types';
|
|
16
|
+
interface FirsthandState {
|
|
17
|
+
imports: Map<string, t.Identifier>;
|
|
18
|
+
templates: t.VariableDeclarator[];
|
|
19
|
+
counter: number;
|
|
20
|
+
moduleId: string;
|
|
21
|
+
}
|
|
22
|
+
declare module '@babel/core' {
|
|
23
|
+
interface PluginPass {
|
|
24
|
+
firsthand: FirsthandState;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export interface FirsthandPluginOptions {
|
|
28
|
+
/** Package name used when hashing stable component ids (ADR-0004). */
|
|
29
|
+
packageName?: string;
|
|
30
|
+
}
|
|
31
|
+
export default function firsthandPlugin(_api: unknown, options?: FirsthandPluginOptions): PluginObject;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=transform.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAc,MAAM,aAAa,CAAC;AAE5D,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAelC,UAAU,cAAc;IACtB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;IACnC,SAAS,EAAE,CAAC,CAAC,kBAAkB,EAAE,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,OAAO,QAAQ,aAAa,CAAC;IAC3B,UAAU,UAAU;QAClB,SAAS,EAAE,cAAc,CAAC;KAC3B;CACF;AAUD,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,CAAC,OAAO,UAAU,eAAe,CACrC,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,sBAA2B,GACnC,YAAY,CAgDd"}
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FirsthandPluginOptions } from './transform.js';
|
|
2
|
+
/** Minimal shape of the Vite plugin contract, so the package needs no Vite dependency. */
|
|
3
|
+
export interface VitePluginLike {
|
|
4
|
+
name: string;
|
|
5
|
+
enforce: 'pre';
|
|
6
|
+
transform(code: string, id: string): {
|
|
7
|
+
code: string;
|
|
8
|
+
map: null;
|
|
9
|
+
} | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Vite/Rollup plugin.
|
|
13
|
+
*
|
|
14
|
+
* Runs before the bundler's TypeScript step: Firsthand parses TS syntax but leaves
|
|
15
|
+
* the annotations in place, so exactly one tool strips them.
|
|
16
|
+
*/
|
|
17
|
+
export declare function firsthand(options?: FirsthandPluginOptions): VitePluginLike;
|
|
18
|
+
//# sourceMappingURL=vite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAE7D,0FAA0F;AAC1F,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,KAAK,CAAC;IACf,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CACzE;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,GAAE,sBAA2B,GAAG,cAAc,CAmB9E"}
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
transform
|
|
3
|
+
} from "./chunk-HHKCDDN2.js";
|
|
4
|
+
|
|
5
|
+
// packages/compiler/src/vite.ts
|
|
6
|
+
function firsthand(options = {}) {
|
|
7
|
+
return {
|
|
8
|
+
name: "firsthand",
|
|
9
|
+
enforce: "pre",
|
|
10
|
+
transform(code, id) {
|
|
11
|
+
const file = id.split("?")[0];
|
|
12
|
+
if (!file.endsWith(".tsx") && !file.endsWith(".jsx")) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
code: transform(code, {
|
|
17
|
+
filename: file,
|
|
18
|
+
typescript: file.endsWith(".tsx"),
|
|
19
|
+
...options
|
|
20
|
+
}),
|
|
21
|
+
map: null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
firsthand
|
|
28
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@firsthandjs/compiler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Build-time TSX transform for Firsthand: static markup into templates, dynamic expressions into DOM parts.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./vite": {
|
|
14
|
+
"types": "./dist/vite.d.ts",
|
|
15
|
+
"default": "./dist/vite.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@babel/core": "^8.0.6",
|
|
27
|
+
"@babel/plugin-syntax-jsx": "^8.0.0",
|
|
28
|
+
"@babel/types": "^8.0.6",
|
|
29
|
+
"@babel/plugin-syntax-typescript": "^8.0.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20.11.0"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"provenance": true
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/firsthandjs/firsthand.git",
|
|
41
|
+
"directory": "packages/compiler"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/firsthandjs/firsthand/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/firsthandjs/firsthand#readme",
|
|
47
|
+
"keywords": [
|
|
48
|
+
"reactive",
|
|
49
|
+
"signals",
|
|
50
|
+
"fine-grained",
|
|
51
|
+
"web-components",
|
|
52
|
+
"ui",
|
|
53
|
+
"framework",
|
|
54
|
+
"jsx",
|
|
55
|
+
"no-virtual-dom"
|
|
56
|
+
]
|
|
57
|
+
}
|