sinwan-compiler 0.3.1 → 1.0.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/dist/analyze.d.ts +1 -1
- package/dist/ast/generate.d.ts +16 -0
- package/dist/ast/index.d.ts +8 -0
- package/dist/ast/node.d.ts +147 -0
- package/dist/ast/parse.d.ts +19 -0
- package/dist/ast/path.d.ts +116 -0
- package/dist/ast/traverse.d.ts +4 -0
- package/dist/ast/types.d.ts +267 -0
- package/dist/auto-cc.d.ts +1 -1
- package/dist/chunk-rshqq0t3.js +4190 -0
- package/dist/cli.js +1 -1
- package/dist/exports.d.ts +7 -6
- package/dist/index.js +345 -132
- package/dist/live-cc-destructure.d.ts +1 -1
- package/dist/reactive-wrap.d.ts +4 -2
- package/dist/transform.d.ts +3 -0
- package/docs/api.md +3 -3
- package/docs/architecture.md +18 -0
- package/docs/plugins.md +9 -8
- package/docs/transform.md +322 -21
- package/package.json +5 -9
- package/dist/chunk-fadpxq9v.js +0 -2428
package/dist/index.js
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
+
identifier,
|
|
4
|
+
stringLiteral,
|
|
5
|
+
numericLiteral,
|
|
6
|
+
callExpression,
|
|
7
|
+
memberExpression,
|
|
8
|
+
variableDeclarator,
|
|
9
|
+
variableDeclaration,
|
|
10
|
+
arrayExpression,
|
|
11
|
+
objectExpression,
|
|
12
|
+
objectProperty,
|
|
13
|
+
importSpecifier,
|
|
14
|
+
importDeclaration,
|
|
15
|
+
jsxExpressionContainer,
|
|
16
|
+
blockStatement,
|
|
17
|
+
returnStatement,
|
|
18
|
+
binaryExpression,
|
|
19
|
+
conditionalExpression,
|
|
20
|
+
unaryExpression,
|
|
21
|
+
isNode,
|
|
22
|
+
isIdentifier,
|
|
23
|
+
isStringLiteral,
|
|
24
|
+
isNumericLiteral,
|
|
25
|
+
isBooleanLiteral,
|
|
26
|
+
isNullLiteral,
|
|
27
|
+
isTemplateLiteral,
|
|
28
|
+
templateElementRaw,
|
|
29
|
+
isParenthesizedExpression,
|
|
30
|
+
unwrapParens,
|
|
31
|
+
isObjectExpression,
|
|
32
|
+
isObjectProperty,
|
|
33
|
+
isObjectPattern,
|
|
34
|
+
isRestElement,
|
|
35
|
+
isAssignmentPattern,
|
|
36
|
+
isBlockStatement,
|
|
37
|
+
isArrowFunctionExpression,
|
|
38
|
+
isFunction,
|
|
39
|
+
isImportDeclaration,
|
|
40
|
+
isImportSpecifier,
|
|
41
|
+
isJSXIdentifier,
|
|
42
|
+
isJSXOpeningElement,
|
|
43
|
+
isJSXMemberExpression,
|
|
44
|
+
isExpression,
|
|
45
|
+
importedBindingName,
|
|
46
|
+
moduleSourceValue,
|
|
47
|
+
singlePath,
|
|
48
|
+
traverse,
|
|
3
49
|
autoWrapComponents2,
|
|
50
|
+
parse,
|
|
4
51
|
analyzeModule2,
|
|
5
52
|
analyzeProject2,
|
|
6
53
|
AnalyzerCache2,
|
|
@@ -17,15 +64,9 @@ import {
|
|
|
17
64
|
propagateReactiveProps2,
|
|
18
65
|
wrapReactiveExpressions2,
|
|
19
66
|
runAnalyzeCli2
|
|
20
|
-
} from "./chunk-
|
|
21
|
-
|
|
22
|
-
// src/transform.ts
|
|
23
|
-
import { parse } from "@babel/parser";
|
|
67
|
+
} from "./chunk-rshqq0t3.js";
|
|
24
68
|
|
|
25
69
|
// src/live-cc-destructure.ts
|
|
26
|
-
import * as t from "@babel/types";
|
|
27
|
-
import _traverse from "@babel/traverse";
|
|
28
|
-
var traverse = typeof _traverse === "function" ? _traverse : _traverse.default ?? _traverse;
|
|
29
70
|
var CC_SOURCE_MODULES = new Set(["sinwan", "sinwan/component"]);
|
|
30
71
|
var LIVE_REST_LOCAL = "_$createLiveRest";
|
|
31
72
|
var SPREAD_LOCAL = "_$getSpreadProps";
|
|
@@ -35,13 +76,15 @@ function collectCcAliases(ast) {
|
|
|
35
76
|
const aliases = new Set;
|
|
36
77
|
traverse(ast, {
|
|
37
78
|
ImportDeclaration(path) {
|
|
38
|
-
const source = path.node.source
|
|
79
|
+
const source = path.node.source?.value;
|
|
80
|
+
if (typeof source !== "string")
|
|
81
|
+
return;
|
|
39
82
|
if (!CC_SOURCE_MODULES.has(source))
|
|
40
83
|
return;
|
|
41
|
-
for (const spec of path.node.specifiers) {
|
|
42
|
-
if (!
|
|
84
|
+
for (const spec of path.node.specifiers ?? []) {
|
|
85
|
+
if (!isImportSpecifier(spec))
|
|
43
86
|
continue;
|
|
44
|
-
const imported =
|
|
87
|
+
const imported = importedBindingName(spec.imported);
|
|
45
88
|
if (imported === "cc") {
|
|
46
89
|
aliases.add(spec.local.name);
|
|
47
90
|
}
|
|
@@ -53,26 +96,28 @@ function collectCcAliases(ast) {
|
|
|
53
96
|
function parseFlatObjectPattern(pattern) {
|
|
54
97
|
const props = [];
|
|
55
98
|
let rest = null;
|
|
56
|
-
for (const prop of pattern.properties) {
|
|
57
|
-
if (
|
|
58
|
-
if (!
|
|
99
|
+
for (const prop of pattern.properties ?? []) {
|
|
100
|
+
if (isRestElement(prop)) {
|
|
101
|
+
if (!isIdentifier(prop.argument))
|
|
59
102
|
return null;
|
|
60
103
|
rest = prop.argument.name;
|
|
61
104
|
continue;
|
|
62
105
|
}
|
|
63
|
-
if (!
|
|
106
|
+
if (!isObjectProperty(prop) || prop.computed)
|
|
64
107
|
return null;
|
|
65
|
-
if (!
|
|
108
|
+
if (!isIdentifier(prop.key) && !isStringLiteral(prop.key))
|
|
109
|
+
return null;
|
|
110
|
+
if (!prop.value)
|
|
66
111
|
return null;
|
|
67
112
|
let value = prop.value;
|
|
68
113
|
let defaultValue = null;
|
|
69
|
-
if (
|
|
114
|
+
if (isAssignmentPattern(value)) {
|
|
70
115
|
defaultValue = value.right;
|
|
71
116
|
value = value.left;
|
|
72
117
|
}
|
|
73
|
-
if (!
|
|
118
|
+
if (!isIdentifier(value))
|
|
74
119
|
return null;
|
|
75
|
-
const keyName =
|
|
120
|
+
const keyName = isIdentifier(prop.key) ? prop.key.name : prop.key.value;
|
|
76
121
|
props.push({
|
|
77
122
|
key: prop.key,
|
|
78
123
|
keyName,
|
|
@@ -97,36 +142,38 @@ function collectUsedNames(fnPath) {
|
|
|
97
142
|
const used = new Set;
|
|
98
143
|
fnPath.traverse({
|
|
99
144
|
Identifier(p) {
|
|
100
|
-
|
|
145
|
+
if (typeof p.node.name === "string")
|
|
146
|
+
used.add(p.node.name);
|
|
101
147
|
}
|
|
102
148
|
});
|
|
103
149
|
return used;
|
|
104
150
|
}
|
|
105
151
|
function memberAccess(propsName, key) {
|
|
106
|
-
if (
|
|
107
|
-
return
|
|
152
|
+
if (isIdentifier(key)) {
|
|
153
|
+
return memberExpression(identifier(propsName), identifier(key.name));
|
|
108
154
|
}
|
|
109
|
-
|
|
155
|
+
const computedKey = isStringLiteral(key) ? key.value : String(key.value ?? "");
|
|
156
|
+
return memberExpression(identifier(propsName), stringLiteral(computedKey), true);
|
|
110
157
|
}
|
|
111
158
|
function liveRead(propsName, binding) {
|
|
112
159
|
const read = memberAccess(propsName, binding.key);
|
|
113
160
|
if (!binding.defaultValue)
|
|
114
161
|
return read;
|
|
115
|
-
return
|
|
162
|
+
return conditionalExpression(binaryExpression("===", memberAccess(propsName, binding.key), unaryExpression("void", numericLiteral(0))), binding.defaultValue, read);
|
|
116
163
|
}
|
|
117
164
|
function ensureBlockBody(fn) {
|
|
118
|
-
if (
|
|
165
|
+
if (isBlockStatement(fn.body))
|
|
119
166
|
return fn.body;
|
|
120
|
-
const body =
|
|
167
|
+
const body = blockStatement([returnStatement(fn.body)]);
|
|
121
168
|
fn.body = body;
|
|
122
|
-
if (
|
|
169
|
+
if (isArrowFunctionExpression(fn)) {
|
|
123
170
|
fn.expression = false;
|
|
124
171
|
}
|
|
125
172
|
return body;
|
|
126
173
|
}
|
|
127
174
|
function replaceWithExpression(path, expr) {
|
|
128
175
|
const parent = path.parent;
|
|
129
|
-
if (
|
|
176
|
+
if (isObjectProperty(parent) && parent.shorthand && parent.value === path.node) {
|
|
130
177
|
parent.shorthand = false;
|
|
131
178
|
parent.value = expr;
|
|
132
179
|
return;
|
|
@@ -149,14 +196,14 @@ function rewriteSpreads(fnPath, liveNames, fnNode) {
|
|
|
149
196
|
fnPath.traverse({
|
|
150
197
|
JSXSpreadAttribute(p) {
|
|
151
198
|
const arg = p.node.argument;
|
|
152
|
-
if (!
|
|
199
|
+
if (!isIdentifier(arg) || !liveNames.has(arg.name))
|
|
153
200
|
return;
|
|
154
201
|
const binding = p.scope.getBinding(arg.name);
|
|
155
202
|
const fnParent = binding?.path.getFunctionParent();
|
|
156
203
|
if (fnParent && fnParent.node !== fnNode)
|
|
157
204
|
return;
|
|
158
|
-
p.node.argument =
|
|
159
|
-
|
|
205
|
+
p.node.argument = callExpression(identifier(SPREAD_LOCAL), [
|
|
206
|
+
identifier(arg.name)
|
|
160
207
|
]);
|
|
161
208
|
changed = true;
|
|
162
209
|
}
|
|
@@ -164,22 +211,22 @@ function rewriteSpreads(fnPath, liveNames, fnNode) {
|
|
|
164
211
|
return changed;
|
|
165
212
|
}
|
|
166
213
|
function hasLocalSpecifier(stmt, local) {
|
|
167
|
-
return stmt.specifiers.some((spec) =>
|
|
214
|
+
return stmt.specifiers.some((spec) => isImportSpecifier(spec) && spec.local.name === local);
|
|
168
215
|
}
|
|
169
216
|
function ensureComponentImport(ast, imported, local) {
|
|
170
217
|
let target = null;
|
|
171
218
|
for (const stmt of ast.program.body) {
|
|
172
|
-
if (!
|
|
219
|
+
if (!isImportDeclaration(stmt))
|
|
173
220
|
continue;
|
|
174
221
|
if (hasLocalSpecifier(stmt, local))
|
|
175
222
|
return;
|
|
176
|
-
if (stmt.source
|
|
223
|
+
if (moduleSourceValue(stmt.source) === "sinwan/component") {
|
|
177
224
|
target = stmt;
|
|
178
|
-
} else if (stmt.source
|
|
225
|
+
} else if (moduleSourceValue(stmt.source) === "sinwan" && target === null) {
|
|
179
226
|
target = stmt;
|
|
180
227
|
}
|
|
181
228
|
}
|
|
182
|
-
const spec =
|
|
229
|
+
const spec = importSpecifier(identifier(local), identifier(imported));
|
|
183
230
|
if (target) {
|
|
184
231
|
target.specifiers.push(spec);
|
|
185
232
|
}
|
|
@@ -193,16 +240,19 @@ function copyPatternType(pattern, id) {
|
|
|
193
240
|
}
|
|
194
241
|
}
|
|
195
242
|
function rewriteFunctionDestructure(fnPath) {
|
|
243
|
+
if (!isFunction(fnPath.node)) {
|
|
244
|
+
return { rest: false, spread: false };
|
|
245
|
+
}
|
|
196
246
|
const fn = fnPath.node;
|
|
197
247
|
const param = fn.params[0];
|
|
198
248
|
if (!param) {
|
|
199
249
|
return { rest: false, spread: false };
|
|
200
250
|
}
|
|
201
|
-
if (
|
|
251
|
+
if (isIdentifier(param)) {
|
|
202
252
|
const spread = rewriteSpreads(fnPath, new Set([param.name]), fn);
|
|
203
253
|
return { rest: false, spread };
|
|
204
254
|
}
|
|
205
|
-
if (!
|
|
255
|
+
if (!isObjectPattern(param)) {
|
|
206
256
|
return { rest: false, spread: false };
|
|
207
257
|
}
|
|
208
258
|
const flat = parseFlatObjectPattern(param);
|
|
@@ -233,16 +283,16 @@ function rewriteFunctionDestructure(fnPath) {
|
|
|
233
283
|
replaceWithExpression(p, liveRead(propsName, binding));
|
|
234
284
|
}
|
|
235
285
|
});
|
|
236
|
-
const propsId =
|
|
286
|
+
const propsId = identifier(propsName);
|
|
237
287
|
copyPatternType(param, propsId);
|
|
238
288
|
fn.params[0] = propsId;
|
|
239
289
|
const liveNames = new Set([propsName]);
|
|
240
290
|
if (flat.rest) {
|
|
241
291
|
const body = ensureBlockBody(fn);
|
|
242
|
-
body.body.unshift(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
292
|
+
body.body.unshift(variableDeclaration("const", [
|
|
293
|
+
variableDeclarator(identifier(flat.rest), callExpression(identifier(LIVE_REST_LOCAL), [
|
|
294
|
+
identifier(propsName),
|
|
295
|
+
arrayExpression(flat.props.map((binding) => stringLiteral(binding.keyName)))
|
|
246
296
|
]))
|
|
247
297
|
]));
|
|
248
298
|
liveNames.add(flat.rest);
|
|
@@ -259,13 +309,15 @@ function rewriteLiveCcDestructure(ast) {
|
|
|
259
309
|
traverse(ast, {
|
|
260
310
|
CallExpression(path) {
|
|
261
311
|
const callee = path.node.callee;
|
|
262
|
-
if (!
|
|
263
|
-
return;
|
|
264
|
-
const firstArg = path.node.arguments[0];
|
|
265
|
-
if (!firstArg || !t.isFunction(firstArg))
|
|
312
|
+
if (!isIdentifier(callee) || !aliases.has(callee.name))
|
|
266
313
|
return;
|
|
267
314
|
const argPaths = path.get("arguments");
|
|
268
|
-
|
|
315
|
+
let fnPath = Array.isArray(argPaths) ? argPaths[0] : argPaths;
|
|
316
|
+
if (!fnPath)
|
|
317
|
+
return;
|
|
318
|
+
while (isParenthesizedExpression(fnPath.node)) {
|
|
319
|
+
fnPath = singlePath(fnPath.get("expression"));
|
|
320
|
+
}
|
|
269
321
|
const result = rewriteFunctionDestructure(fnPath);
|
|
270
322
|
needsRest = needsRest || result.rest;
|
|
271
323
|
needsSpread = needsSpread || result.spread;
|
|
@@ -279,13 +331,42 @@ function rewriteLiveCcDestructure(ast) {
|
|
|
279
331
|
}
|
|
280
332
|
return { rest: needsRest, spread: needsSpread };
|
|
281
333
|
}
|
|
282
|
-
|
|
334
|
+
// src/ast/generate.ts
|
|
335
|
+
import { print } from "esrap";
|
|
336
|
+
import tsx from "esrap/languages/tsx";
|
|
337
|
+
function programOf(ast) {
|
|
338
|
+
if (ast.type === "File" && ast.program) {
|
|
339
|
+
return ast.program;
|
|
340
|
+
}
|
|
341
|
+
return ast;
|
|
342
|
+
}
|
|
343
|
+
function esrapComments(list) {
|
|
344
|
+
if (!list || list.length === 0)
|
|
345
|
+
return;
|
|
346
|
+
return list.map((comment) => ({
|
|
347
|
+
type: comment.type === "CommentLine" || comment.type === "Line" ? "Line" : "Block",
|
|
348
|
+
value: comment.value
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
function generate(ast, options = {}, _code) {
|
|
352
|
+
const root = programOf(ast);
|
|
353
|
+
const language = tsx({
|
|
354
|
+
quotes: "double",
|
|
355
|
+
getLeadingComments: (node) => esrapComments(node.leadingComments),
|
|
356
|
+
getTrailingComments: (node) => esrapComments(node.trailingComments)
|
|
357
|
+
});
|
|
358
|
+
const result = print(root, language, {
|
|
359
|
+
sourceMapSource: options.sourceFileName,
|
|
360
|
+
sourceMapContent: _code
|
|
361
|
+
});
|
|
362
|
+
const map = options.sourceMaps && result.map ? {
|
|
363
|
+
...result.map,
|
|
364
|
+
file: options.sourceFileName,
|
|
365
|
+
sources: options.sourceFileName ? [options.sourceFileName] : result.map.sources
|
|
366
|
+
} : options.sourceMaps ? result.map : null;
|
|
367
|
+
return { code: result.code, map };
|
|
368
|
+
}
|
|
283
369
|
// src/transform.ts
|
|
284
|
-
import _generate from "@babel/generator";
|
|
285
|
-
import * as t2 from "@babel/types";
|
|
286
|
-
import _traverse2 from "@babel/traverse";
|
|
287
|
-
var generate = typeof _generate === "function" ? _generate : _generate.default ?? _generate;
|
|
288
|
-
var traverse2 = typeof _traverse2 === "function" ? _traverse2 : _traverse2.default ?? _traverse2;
|
|
289
370
|
var COMPILER_TEMPLATE_SLOT_PROTOCOL = {
|
|
290
371
|
slotPrefix: "s",
|
|
291
372
|
encodeSlot(index) {
|
|
@@ -360,29 +441,41 @@ function htmlAttributeName(attrName) {
|
|
|
360
441
|
return attrName;
|
|
361
442
|
}
|
|
362
443
|
function jsxAttrName(attr) {
|
|
363
|
-
if (attr.type !== "JSXAttribute"
|
|
444
|
+
if (attr.type !== "JSXAttribute")
|
|
364
445
|
return;
|
|
446
|
+
const n = attr.name;
|
|
447
|
+
if (!n || typeof n === "string")
|
|
448
|
+
return;
|
|
449
|
+
if (n.type === "JSXIdentifier") {
|
|
450
|
+
return typeof n.name === "string" ? n.name : undefined;
|
|
451
|
+
}
|
|
452
|
+
if (n.type === "JSXNamespacedName") {
|
|
453
|
+
const namespace = n.namespace;
|
|
454
|
+
const name = n.name;
|
|
455
|
+
return `${namespace.name}:${name.name}`;
|
|
365
456
|
}
|
|
366
|
-
return attr.name.name;
|
|
367
457
|
}
|
|
368
458
|
function hasAttr(attributes, name) {
|
|
369
|
-
for (const attr of attributes) {
|
|
459
|
+
for (const attr of attributes ?? []) {
|
|
370
460
|
if (jsxAttrName(attr) === name)
|
|
371
461
|
return true;
|
|
372
462
|
}
|
|
373
463
|
return false;
|
|
374
464
|
}
|
|
375
465
|
function attrIsExpression(attributes, name) {
|
|
376
|
-
for (const attr of attributes) {
|
|
466
|
+
for (const attr of attributes ?? []) {
|
|
377
467
|
if (attr.type !== "JSXAttribute" || jsxAttrName(attr) !== name)
|
|
378
468
|
continue;
|
|
379
|
-
return attr.value
|
|
469
|
+
return isNode(attr.value) && attr.value.type === "JSXExpressionContainer";
|
|
380
470
|
}
|
|
381
471
|
return false;
|
|
382
472
|
}
|
|
383
473
|
function elementNeedsJsxEnhancer(node) {
|
|
384
|
-
const
|
|
385
|
-
|
|
474
|
+
const opening = node.openingElement;
|
|
475
|
+
if (!isJSXOpeningElement(opening))
|
|
476
|
+
return false;
|
|
477
|
+
const tagName = jsxNameToString(opening.name);
|
|
478
|
+
const attrs = opening.attributes;
|
|
386
479
|
switch (tagName) {
|
|
387
480
|
case "select":
|
|
388
481
|
return hasAttr(attrs, "defaultValue") || hasAttr(attrs, "value");
|
|
@@ -422,17 +515,17 @@ function warnSuspectedStyleInterpolation(attrValue, filename, line) {
|
|
|
422
515
|
console.warn('[Sinwan] JSX string literal style attribute contains "${...}" which will not be interpolated. ' + "Use style={`...${...}`} instead. (" + filename + location + ")");
|
|
423
516
|
}
|
|
424
517
|
function isLiteralStyleValue(expr) {
|
|
425
|
-
return
|
|
518
|
+
return isStringLiteral(expr) || isNumericLiteral(expr) || isBooleanLiteral(expr) || isNullLiteral(expr);
|
|
426
519
|
}
|
|
427
520
|
function isStaticStyleValue(expr) {
|
|
428
|
-
if (
|
|
521
|
+
if (isStringLiteral(expr))
|
|
429
522
|
return true;
|
|
430
|
-
if (
|
|
523
|
+
if (isTemplateLiteral(expr) && expr.expressions.length === 0)
|
|
431
524
|
return true;
|
|
432
|
-
if (!
|
|
525
|
+
if (!isObjectExpression(expr))
|
|
433
526
|
return false;
|
|
434
527
|
for (const prop of expr.properties) {
|
|
435
|
-
if (!
|
|
528
|
+
if (!isObjectProperty(prop))
|
|
436
529
|
return false;
|
|
437
530
|
if (prop.computed)
|
|
438
531
|
return false;
|
|
@@ -445,36 +538,36 @@ function toKebabCase(value) {
|
|
|
445
538
|
return value.includes("-") ? value : value.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
|
|
446
539
|
}
|
|
447
540
|
function serializeStyleValue(expr) {
|
|
448
|
-
if (
|
|
541
|
+
if (isStringLiteral(expr))
|
|
449
542
|
return expr.value;
|
|
450
|
-
if (
|
|
451
|
-
return expr.quasis[0]
|
|
543
|
+
if (isTemplateLiteral(expr) && (expr.expressions ?? []).length === 0) {
|
|
544
|
+
return templateElementRaw((expr.quasis ?? [])[0]);
|
|
452
545
|
}
|
|
453
|
-
if (!
|
|
546
|
+
if (!isObjectExpression(expr))
|
|
454
547
|
return "";
|
|
455
548
|
const parts = [];
|
|
456
549
|
for (const prop of expr.properties) {
|
|
457
|
-
if (!
|
|
550
|
+
if (!isObjectProperty(prop))
|
|
458
551
|
continue;
|
|
459
552
|
const key = prop.key;
|
|
460
553
|
let name = null;
|
|
461
|
-
if (
|
|
554
|
+
if (isIdentifier(key))
|
|
462
555
|
name = key.name;
|
|
463
|
-
else if (
|
|
556
|
+
else if (isStringLiteral(key))
|
|
464
557
|
name = key.value;
|
|
465
558
|
if (!name)
|
|
466
559
|
continue;
|
|
467
560
|
const val = prop.value;
|
|
468
|
-
if (
|
|
561
|
+
if (isNullLiteral(val))
|
|
469
562
|
continue;
|
|
470
|
-
if (
|
|
563
|
+
if (isBooleanLiteral(val) && !val.value)
|
|
471
564
|
continue;
|
|
472
565
|
let valueStr = "";
|
|
473
|
-
if (
|
|
566
|
+
if (isStringLiteral(val))
|
|
474
567
|
valueStr = val.value;
|
|
475
|
-
else if (
|
|
568
|
+
else if (isNumericLiteral(val))
|
|
476
569
|
valueStr = String(val.value);
|
|
477
|
-
else if (
|
|
570
|
+
else if (isBooleanLiteral(val))
|
|
478
571
|
valueStr = String(val.value);
|
|
479
572
|
if (valueStr === "")
|
|
480
573
|
continue;
|
|
@@ -483,7 +576,8 @@ function serializeStyleValue(expr) {
|
|
|
483
576
|
return parts.join(";");
|
|
484
577
|
}
|
|
485
578
|
function elementToHtml(node, slots, path, filename, counter) {
|
|
486
|
-
const
|
|
579
|
+
const opening = node.openingElement;
|
|
580
|
+
const tagName = jsxNameToString(opening?.name);
|
|
487
581
|
if (tagName === "")
|
|
488
582
|
return "";
|
|
489
583
|
const firstChar = tagName.charAt(0);
|
|
@@ -493,11 +587,13 @@ function elementToHtml(node, slots, path, filename, counter) {
|
|
|
493
587
|
const isVoid = VOID_ELEMENTS.has(tagName);
|
|
494
588
|
let html = `<${tagName}`;
|
|
495
589
|
let textareaDefault = "";
|
|
496
|
-
for (const attr of
|
|
590
|
+
for (const attr of opening?.attributes ?? []) {
|
|
497
591
|
if (attr.type === "JSXSpreadAttribute") {
|
|
498
592
|
throw new Error("Cannot hoist element with spread attributes");
|
|
499
593
|
}
|
|
500
|
-
const attrName = attr
|
|
594
|
+
const attrName = jsxAttrName(attr);
|
|
595
|
+
if (!attrName)
|
|
596
|
+
continue;
|
|
501
597
|
if (attrName === "children")
|
|
502
598
|
continue;
|
|
503
599
|
if (attrName === "ref") {
|
|
@@ -519,7 +615,7 @@ function elementToHtml(node, slots, path, filename, counter) {
|
|
|
519
615
|
const expr = attr.value.expression;
|
|
520
616
|
if (expr.type === "JSXEmptyExpression")
|
|
521
617
|
continue;
|
|
522
|
-
if (attrName === "style" &&
|
|
618
|
+
if (attrName === "style" && isExpression(expr) && isStaticStyleValue(expr)) {
|
|
523
619
|
const serialized = serializeStyleValue(expr);
|
|
524
620
|
html += ` ${htmlName}="${escapeHtml(serialized)}"`;
|
|
525
621
|
continue;
|
|
@@ -530,10 +626,12 @@ function elementToHtml(node, slots, path, filename, counter) {
|
|
|
530
626
|
name: attrName,
|
|
531
627
|
expr
|
|
532
628
|
});
|
|
533
|
-
|
|
629
|
+
if (attrName !== "class" && !attrName.startsWith("on")) {
|
|
630
|
+
html += ` ${htmlName}=""`;
|
|
631
|
+
}
|
|
534
632
|
continue;
|
|
535
633
|
}
|
|
536
|
-
if (attr.value?.type === "JSXText" || attr.value
|
|
634
|
+
if (attr.value?.type === "JSXText" || isStringLiteral(attr.value)) {
|
|
537
635
|
const rawValue = attr.value.value;
|
|
538
636
|
if (attrName === "style") {
|
|
539
637
|
warnSuspectedStyleInterpolation(rawValue, filename, attr.value.loc?.start?.line);
|
|
@@ -556,7 +654,61 @@ function elementToHtml(node, slots, path, filename, counter) {
|
|
|
556
654
|
html += `</${tagName}>`;
|
|
557
655
|
return html;
|
|
558
656
|
}
|
|
559
|
-
function
|
|
657
|
+
function getSoleDynamicChild(children) {
|
|
658
|
+
const meaningful = [];
|
|
659
|
+
for (const child of children) {
|
|
660
|
+
if (child.type === "JSXText") {
|
|
661
|
+
if (cleanJSXText(child.value ?? ""))
|
|
662
|
+
meaningful.push(child);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (child.type === "JSXExpressionContainer") {
|
|
666
|
+
if (child.expression.type === "JSXEmptyExpression")
|
|
667
|
+
continue;
|
|
668
|
+
meaningful.push(child);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (child.type === "JSXElement" || child.type === "JSXFragment") {
|
|
672
|
+
meaningful.push(child);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (meaningful.length !== 1)
|
|
676
|
+
return null;
|
|
677
|
+
const only = meaningful[0];
|
|
678
|
+
if (only.type === "JSXFragment")
|
|
679
|
+
return getSoleDynamicChild(only.children);
|
|
680
|
+
if (only.type === "JSXExpressionContainer")
|
|
681
|
+
return only;
|
|
682
|
+
if (only.type === "JSXElement") {
|
|
683
|
+
const childTag = jsxIdentifierTagName(only.openingElement?.name);
|
|
684
|
+
const isComponent = !!childTag && childTag.charAt(0) === childTag.charAt(0).toUpperCase();
|
|
685
|
+
if (isComponent || elementNeedsJsxEnhancer(only))
|
|
686
|
+
return only;
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
function childrenToHtml(children, slots, path, filename, startIndex, counter, allowParentFill = true) {
|
|
692
|
+
if (allowParentFill && startIndex === 0) {
|
|
693
|
+
const sole = getSoleDynamicChild(children);
|
|
694
|
+
if (sole != null) {
|
|
695
|
+
counter.value++;
|
|
696
|
+
if (sole.type === "JSXExpressionContainer") {
|
|
697
|
+
slots.push({
|
|
698
|
+
path: [...path],
|
|
699
|
+
type: "child",
|
|
700
|
+
expr: sole.expression
|
|
701
|
+
});
|
|
702
|
+
} else {
|
|
703
|
+
slots.push({
|
|
704
|
+
path: [...path],
|
|
705
|
+
type: "child",
|
|
706
|
+
expr: sole
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
return { html: "", childIndex: 0 };
|
|
710
|
+
}
|
|
711
|
+
}
|
|
560
712
|
let html = "";
|
|
561
713
|
let childIndex = startIndex;
|
|
562
714
|
for (const child of children) {
|
|
@@ -567,8 +719,18 @@ function childrenToHtml(children, slots, path, filename, startIndex, counter) {
|
|
|
567
719
|
childIndex++;
|
|
568
720
|
}
|
|
569
721
|
} else if (child.type === "JSXExpressionContainer") {
|
|
570
|
-
if (child.expression.type === "JSXEmptyExpression")
|
|
722
|
+
if (child.expression.type === "JSXEmptyExpression") {
|
|
723
|
+
const comments = [
|
|
724
|
+
...child.expression.leadingComments ?? [],
|
|
725
|
+
...child.leadingComments ?? []
|
|
726
|
+
];
|
|
727
|
+
for (const comment of comments) {
|
|
728
|
+
if (comment?.type === "CommentBlock" && typeof comment.value === "string") {
|
|
729
|
+
html += `<!--${comment.value.replace(/--/g, "")}-->`;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
571
732
|
continue;
|
|
733
|
+
}
|
|
572
734
|
const slot = nextSlotId(counter);
|
|
573
735
|
slots.push({
|
|
574
736
|
path: [...path, childIndex],
|
|
@@ -578,8 +740,7 @@ function childrenToHtml(children, slots, path, filename, startIndex, counter) {
|
|
|
578
740
|
html += slot;
|
|
579
741
|
childIndex++;
|
|
580
742
|
} else if (child.type === "JSXElement") {
|
|
581
|
-
const
|
|
582
|
-
const childTag = childName.type === "JSXIdentifier" ? childName.name : "";
|
|
743
|
+
const childTag = jsxIdentifierTagName(child.openingElement?.name);
|
|
583
744
|
const isFirstCharUpper = childTag && childTag.charAt(0) === childTag.charAt(0).toUpperCase();
|
|
584
745
|
if (isFirstCharUpper || elementNeedsJsxEnhancer(child)) {
|
|
585
746
|
const slot = nextSlotId(counter);
|
|
@@ -595,7 +756,7 @@ function childrenToHtml(children, slots, path, filename, startIndex, counter) {
|
|
|
595
756
|
childIndex++;
|
|
596
757
|
}
|
|
597
758
|
} else if (child.type === "JSXFragment") {
|
|
598
|
-
const fragResult = childrenToHtml(child.children, slots, path, filename, childIndex, counter);
|
|
759
|
+
const fragResult = childrenToHtml(child.children, slots, path, filename, childIndex, counter, false);
|
|
599
760
|
html += fragResult.html;
|
|
600
761
|
childIndex = fragResult.childIndex;
|
|
601
762
|
}
|
|
@@ -603,13 +764,43 @@ function childrenToHtml(children, slots, path, filename, startIndex, counter) {
|
|
|
603
764
|
return { html, childIndex };
|
|
604
765
|
}
|
|
605
766
|
function jsxNameToString(name) {
|
|
606
|
-
if (name
|
|
767
|
+
if (isJSXIdentifier(name))
|
|
607
768
|
return name.name;
|
|
608
|
-
if (name
|
|
769
|
+
if (isJSXMemberExpression(name)) {
|
|
609
770
|
return jsxNameToString(name.object) + "." + jsxNameToString(name.property);
|
|
610
771
|
}
|
|
611
772
|
return "";
|
|
612
773
|
}
|
|
774
|
+
function jsxIdentifierTagName(name) {
|
|
775
|
+
return isJSXIdentifier(name) ? name.name : "";
|
|
776
|
+
}
|
|
777
|
+
function rendererHelperImported(body, local) {
|
|
778
|
+
for (const statement of body) {
|
|
779
|
+
if (!isImportDeclaration(statement))
|
|
780
|
+
continue;
|
|
781
|
+
const source = moduleSourceValue(statement.source);
|
|
782
|
+
if (source !== "sinwan/renderer" && source !== "sinwan/renderer/web") {
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
786
|
+
if (isImportSpecifier(specifier) && isIdentifier(specifier.local) && specifier.local.name === local) {
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
function isStructuralJsxChild(path) {
|
|
794
|
+
let cur = path.parentPath;
|
|
795
|
+
while (cur) {
|
|
796
|
+
if (cur.isJSXExpressionContainer())
|
|
797
|
+
return false;
|
|
798
|
+
if (cur.isJSXElement())
|
|
799
|
+
return true;
|
|
800
|
+
cur = cur.parentPath;
|
|
801
|
+
}
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
613
804
|
function transformJSX(code, filename, options = {}) {
|
|
614
805
|
const ast = parse(code, {
|
|
615
806
|
sourceType: "module",
|
|
@@ -619,6 +810,8 @@ function transformJSX(code, filename, options = {}) {
|
|
|
619
810
|
autoWrapComponents2(ast, filename);
|
|
620
811
|
rewriteLiveCcDestructure(ast);
|
|
621
812
|
wrapReactiveExpressions2(ast, {
|
|
813
|
+
derivedLocals: options.derivedLocals,
|
|
814
|
+
dev: options.dev,
|
|
622
815
|
explicitBindings: options.explicitBindings,
|
|
623
816
|
analyze: options.analyze,
|
|
624
817
|
analyzeMetadata: options.analyzeMetadata,
|
|
@@ -628,18 +821,20 @@ function transformJSX(code, filename, options = {}) {
|
|
|
628
821
|
const hoist = options.hoist !== false;
|
|
629
822
|
const templates = [];
|
|
630
823
|
let templateCounter = 0;
|
|
631
|
-
|
|
824
|
+
traverse(ast, {
|
|
632
825
|
JSXElement(path) {
|
|
633
826
|
if (!hoist)
|
|
634
827
|
return;
|
|
635
|
-
if (path
|
|
828
|
+
if (isStructuralJsxChild(path))
|
|
636
829
|
return;
|
|
637
830
|
const opening = path.node.openingElement;
|
|
638
|
-
|
|
639
|
-
if (tagName.type !== "JSXIdentifier")
|
|
831
|
+
if (!isJSXOpeningElement(opening))
|
|
640
832
|
return;
|
|
641
|
-
|
|
642
|
-
|
|
833
|
+
if (!isJSXIdentifier(opening.name))
|
|
834
|
+
return;
|
|
835
|
+
const name = opening.name.name;
|
|
836
|
+
const first = name[0];
|
|
837
|
+
if (!first || first !== first.toLowerCase())
|
|
643
838
|
return;
|
|
644
839
|
if (elementNeedsJsxEnhancer(path.node))
|
|
645
840
|
return;
|
|
@@ -648,9 +843,9 @@ function transformJSX(code, filename, options = {}) {
|
|
|
648
843
|
const tmplId = `_$tmpl_${templateCounter++}`;
|
|
649
844
|
templates.push({ id: tmplId, template: extracted });
|
|
650
845
|
const dynamicExprs = extracted.slots.filter((s) => s.expr).map((s) => s.expr);
|
|
651
|
-
const templateExpr =
|
|
846
|
+
const templateExpr = callExpression(identifier("_$createTemplate"), [identifier(tmplId), arrayExpression(dynamicExprs)]);
|
|
652
847
|
const parentIsJSX = path.parentPath?.isJSXElement() || path.parentPath?.isJSXFragment();
|
|
653
|
-
path.replaceWith(parentIsJSX ?
|
|
848
|
+
path.replaceWith(parentIsJSX ? jsxExpressionContainer(templateExpr) : templateExpr);
|
|
654
849
|
} catch (err) {
|
|
655
850
|
if (options.dev) {
|
|
656
851
|
console.warn(`[Sinwan] template hoisting skipped in ${filename}: ${err.message}`);
|
|
@@ -660,13 +855,15 @@ function transformJSX(code, filename, options = {}) {
|
|
|
660
855
|
});
|
|
661
856
|
if (templates.length === 0) {
|
|
662
857
|
if (options.explicitBindings) {
|
|
663
|
-
const bindingImport =
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
],
|
|
669
|
-
ast.program.body.
|
|
858
|
+
const bindingImport = importDeclaration([
|
|
859
|
+
importSpecifier(identifier("_$bindText"), identifier("_$bindText")),
|
|
860
|
+
importSpecifier(identifier("_$bindAttr"), identifier("_$bindAttr")),
|
|
861
|
+
importSpecifier(identifier("_$bindStyle"), identifier("_$bindStyle")),
|
|
862
|
+
importSpecifier(identifier("_$bindClass"), identifier("_$bindClass"))
|
|
863
|
+
], stringLiteral("sinwan/renderer/web"));
|
|
864
|
+
bindingImport.specifiers = (bindingImport.specifiers ?? []).filter((candidate) => isImportSpecifier(candidate) && isIdentifier(candidate.local) && !rendererHelperImported(ast.program.body, candidate.local.name));
|
|
865
|
+
if (bindingImport.specifiers.length > 0)
|
|
866
|
+
ast.program.body.unshift(bindingImport);
|
|
670
867
|
}
|
|
671
868
|
const result = generate(ast, {
|
|
672
869
|
sourceMaps: true,
|
|
@@ -674,38 +871,49 @@ function transformJSX(code, filename, options = {}) {
|
|
|
674
871
|
});
|
|
675
872
|
return { code: result.code, map: result.map };
|
|
676
873
|
}
|
|
677
|
-
const templateDecls = templates.map(({ id, template }) =>
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
874
|
+
const templateDecls = templates.map(({ id, template }) => variableDeclaration("const", [
|
|
875
|
+
variableDeclarator(identifier(id), objectExpression([
|
|
876
|
+
objectProperty(identifier("html"), stringLiteral(template.html)),
|
|
877
|
+
objectProperty(identifier("slots"), arrayExpression(template.slots.map((slot) => objectExpression([
|
|
878
|
+
objectProperty(identifier("path"), arrayExpression(slot.path.map((i) => numericLiteral(i)))),
|
|
879
|
+
objectProperty(identifier("type"), stringLiteral(slot.type)),
|
|
683
880
|
...slot.name ? [
|
|
684
|
-
|
|
881
|
+
objectProperty(identifier("name"), stringLiteral(slot.name))
|
|
685
882
|
] : []
|
|
686
883
|
]))))
|
|
687
884
|
]))
|
|
688
885
|
]));
|
|
689
886
|
const importSpecifiers = [
|
|
690
|
-
|
|
887
|
+
importSpecifier(identifier("_$createTemplate"), identifier("_$createTemplate"))
|
|
691
888
|
];
|
|
692
889
|
if (options.explicitBindings) {
|
|
693
|
-
importSpecifiers.push(
|
|
890
|
+
importSpecifiers.push(importSpecifier(identifier("_$bindText"), identifier("_$bindText")), importSpecifier(identifier("_$bindAttr"), identifier("_$bindAttr")), importSpecifier(identifier("_$bindStyle"), identifier("_$bindStyle")), importSpecifier(identifier("_$bindClass"), identifier("_$bindClass")));
|
|
891
|
+
}
|
|
892
|
+
const freshSpecifiers = importSpecifiers.filter((specifier) => {
|
|
893
|
+
const local = specifier.local;
|
|
894
|
+
return isIdentifier(local) && !rendererHelperImported(ast.program.body, local.name);
|
|
895
|
+
});
|
|
896
|
+
ast.program.body.unshift(...templateDecls);
|
|
897
|
+
if (freshSpecifiers.length > 0) {
|
|
898
|
+
ast.program.body.unshift(importDeclaration(freshSpecifiers, stringLiteral("sinwan/renderer/web")));
|
|
694
899
|
}
|
|
695
|
-
const importDecl = t2.importDeclaration(importSpecifiers, t2.stringLiteral("sinwan/renderer"));
|
|
696
|
-
ast.program.body.unshift(importDecl, ...templateDecls);
|
|
697
900
|
const result = generate(ast, { sourceMaps: true, sourceFileName: filename });
|
|
698
901
|
return { code: result.code, map: result.map };
|
|
699
902
|
}
|
|
700
903
|
// src/exports.ts
|
|
701
|
-
import { parse as parse2 } from "@babel/parser";
|
|
702
904
|
function isComponentName(name) {
|
|
703
905
|
return !!name && name[0] === name[0].toUpperCase() && /^[A-Z]/.test(name);
|
|
704
906
|
}
|
|
907
|
+
function isTypeOnlyExport(node) {
|
|
908
|
+
return node.exportKind === "type";
|
|
909
|
+
}
|
|
910
|
+
function isIdent(n) {
|
|
911
|
+
return !!n && typeof n === "object" && n.type === "Identifier" && typeof n.name === "string";
|
|
912
|
+
}
|
|
705
913
|
function collectExportedComponents(code, filename) {
|
|
706
914
|
let ast;
|
|
707
915
|
try {
|
|
708
|
-
ast =
|
|
916
|
+
ast = parse(code, {
|
|
709
917
|
sourceType: "module",
|
|
710
918
|
plugins: ["jsx", "typescript"],
|
|
711
919
|
sourceFilename: filename
|
|
@@ -724,30 +932,35 @@ function collectExportedComponents(code, filename) {
|
|
|
724
932
|
};
|
|
725
933
|
for (const node of ast.program.body) {
|
|
726
934
|
if (node.type === "ExportNamedDeclaration") {
|
|
727
|
-
if (node
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
935
|
+
if (isTypeOnlyExport(node))
|
|
936
|
+
continue;
|
|
937
|
+
const declaration = node.declaration;
|
|
938
|
+
if (declaration) {
|
|
939
|
+
if (declaration.type === "FunctionDeclaration" && isIdent(declaration.id) && isComponentName(declaration.id.name)) {
|
|
940
|
+
add(declaration.id.name, declaration.id.name);
|
|
941
|
+
} else if (declaration.type === "VariableDeclaration") {
|
|
942
|
+
for (const d of declaration.declarations ?? []) {
|
|
943
|
+
if (isIdent(d.id) && isComponentName(d.id.name)) {
|
|
734
944
|
add(d.id.name, d.id.name);
|
|
735
945
|
}
|
|
736
946
|
}
|
|
737
947
|
}
|
|
738
948
|
} else if (node.specifiers && !node.source) {
|
|
739
949
|
for (const spec of node.specifiers) {
|
|
740
|
-
if (
|
|
950
|
+
if (isTypeOnlyExport(spec))
|
|
951
|
+
continue;
|
|
952
|
+
if (spec.type === "ExportSpecifier" && isIdent(spec.local) && isIdent(spec.exported) && isComponentName(spec.exported.name)) {
|
|
741
953
|
add(spec.local.name, spec.exported.name);
|
|
742
954
|
}
|
|
743
955
|
}
|
|
744
956
|
}
|
|
745
957
|
} else if (node.type === "ExportDefaultDeclaration") {
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
958
|
+
const raw = node.declaration;
|
|
959
|
+
const declaration = raw && typeof raw === "object" && "type" in raw ? unwrapParens(raw) : raw;
|
|
960
|
+
if (isIdent(declaration)) {
|
|
961
|
+
add(declaration.name, "default");
|
|
962
|
+
} else if (declaration.type === "FunctionDeclaration" && isIdent(declaration.id)) {
|
|
963
|
+
add(declaration.id.name, "default");
|
|
751
964
|
}
|
|
752
965
|
}
|
|
753
966
|
}
|