sinwan-compiler 1.0.0 → 1.1.1
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/README.md +1 -1
- package/dist/{chunk-rshqq0t3.js → chunk-52vtv5h0.js} +496 -62
- package/dist/cli.js +1 -1
- package/dist/index.js +2 -2
- package/docs/api.md +1 -1
- package/docs/transform.md +10 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ bun add -d sinwan-compiler
|
|
|
19
19
|
|
|
20
20
|
App templates already depend on the Bun or Vite plugin. You only need this package if you call the compiler yourself.
|
|
21
21
|
|
|
22
|
-
The plugins
|
|
22
|
+
The plugins depend on `sinwan-compiler` `>=1.0.0 <2.0.0`. A new 1.x compiler is picked up with `bun update sinwan-compiler`. Publish a new plugin when its own API changes, or when the compiler reaches 2.0.
|
|
23
23
|
|
|
24
24
|
## Production analysis
|
|
25
25
|
|
|
@@ -1156,6 +1156,38 @@ function unwrapFunctionPath(fnPath) {
|
|
|
1156
1156
|
function ccCall(arg, ccName) {
|
|
1157
1157
|
return callExpression(identifier(ccName), [arg]);
|
|
1158
1158
|
}
|
|
1159
|
+
function withTypes(fn, from) {
|
|
1160
|
+
fn.typeParameters = from.typeParameters;
|
|
1161
|
+
fn.returnType = from.returnType;
|
|
1162
|
+
return fn;
|
|
1163
|
+
}
|
|
1164
|
+
function isReadBeforeDeclaration(path, name) {
|
|
1165
|
+
const start = path.node.start ?? 0;
|
|
1166
|
+
return (path.parentPath.scope.getBinding(name)?.referencePaths ?? []).some((ref) => (ref.node.start ?? 0) < start && !ref.getFunctionParent() && ref.parent?.type !== "ExportSpecifier" && !ref.findParent((parent) => parent.isTSType()));
|
|
1167
|
+
}
|
|
1168
|
+
function collectSpecifierExports(ast) {
|
|
1169
|
+
const exports = new Map;
|
|
1170
|
+
traverse(ast, {
|
|
1171
|
+
ExportNamedDeclaration(path) {
|
|
1172
|
+
const node = path.node;
|
|
1173
|
+
if (node.declaration || node.source || node.exportKind === "type")
|
|
1174
|
+
return;
|
|
1175
|
+
for (const spec of node.specifiers ?? []) {
|
|
1176
|
+
if (!isExportSpecifier(spec) || spec.exportKind === "type")
|
|
1177
|
+
continue;
|
|
1178
|
+
if (!isIdentifier(spec.local) || !isIdentifier(spec.exported))
|
|
1179
|
+
continue;
|
|
1180
|
+
if (isComponentName(spec.exported.name)) {
|
|
1181
|
+
exports.set(spec.local.name, spec.exported.name);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
return exports;
|
|
1187
|
+
}
|
|
1188
|
+
function isTopLevel(path) {
|
|
1189
|
+
return path.parent?.type === "Program";
|
|
1190
|
+
}
|
|
1159
1191
|
function autoWrapComponents2(ast, filename) {
|
|
1160
1192
|
const { aliases, alreadyImported } = collectCcAliases(ast);
|
|
1161
1193
|
const ccName = aliases.values().next().value;
|
|
@@ -1170,6 +1202,24 @@ function autoWrapComponents2(ast, filename) {
|
|
|
1170
1202
|
return false;
|
|
1171
1203
|
return true;
|
|
1172
1204
|
};
|
|
1205
|
+
const wrapDeclarators = (declarators, componentName) => {
|
|
1206
|
+
for (const dPath of declarators) {
|
|
1207
|
+
const id = dPath.node.id;
|
|
1208
|
+
if (!isIdentifier(id))
|
|
1209
|
+
continue;
|
|
1210
|
+
const name = componentName(id.name);
|
|
1211
|
+
if (name === undefined || !dPath.node.init)
|
|
1212
|
+
continue;
|
|
1213
|
+
const initPath = unwrapFunctionPath(dPath.get("init"));
|
|
1214
|
+
if (!isFunction(initPath.node))
|
|
1215
|
+
continue;
|
|
1216
|
+
if (!wrapIfCandidate(initPath, name))
|
|
1217
|
+
continue;
|
|
1218
|
+
dPath.node.init = ccCall(initPath.node, ccName ?? "cc");
|
|
1219
|
+
changed = true;
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
const specifierExports = collectSpecifierExports(ast);
|
|
1173
1223
|
traverse(ast, {
|
|
1174
1224
|
ExportNamedDeclaration(path) {
|
|
1175
1225
|
const decl = path.node.declaration;
|
|
@@ -1180,7 +1230,7 @@ function autoWrapComponents2(ast, filename) {
|
|
|
1180
1230
|
const fnPath = path.get("declaration");
|
|
1181
1231
|
if (!wrapIfCandidate(fnPath, name))
|
|
1182
1232
|
return;
|
|
1183
|
-
const fnExpr = functionExpression(decl.id, decl.params, decl.body, decl.generator, decl.async);
|
|
1233
|
+
const fnExpr = withTypes(functionExpression(decl.id, decl.params, decl.body, decl.generator, decl.async), decl);
|
|
1184
1234
|
const varDecl = variableDeclaration("const", [
|
|
1185
1235
|
variableDeclarator(identifier(name), ccCall(fnExpr, ccName ?? "cc"))
|
|
1186
1236
|
]);
|
|
@@ -1189,27 +1239,30 @@ function autoWrapComponents2(ast, filename) {
|
|
|
1189
1239
|
return;
|
|
1190
1240
|
}
|
|
1191
1241
|
if (isVariableDeclaration(decl)) {
|
|
1192
|
-
|
|
1193
|
-
const id = dPath.node.id;
|
|
1194
|
-
if (!isIdentifier(id) || !isComponentName(id.name))
|
|
1195
|
-
continue;
|
|
1196
|
-
const init = dPath.node.init ? unwrapParens(dPath.node.init) : dPath.node.init;
|
|
1197
|
-
if (!init)
|
|
1198
|
-
continue;
|
|
1199
|
-
let initPath = unwrapFunctionPath(dPath.get("init"));
|
|
1200
|
-
if (isCallExpression(initPath.node) && isIdentifier(initPath.node.callee) && aliases.has(initPath.node.callee.name)) {
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
if (!isFunction(initPath.node))
|
|
1204
|
-
continue;
|
|
1205
|
-
if (!wrapIfCandidate(initPath, id.name))
|
|
1206
|
-
continue;
|
|
1207
|
-
dPath.node.init = ccCall(initPath.node, ccName ?? "cc");
|
|
1208
|
-
changed = true;
|
|
1209
|
-
}
|
|
1242
|
+
wrapDeclarators(path.get("declaration.declarations"), (name) => isComponentName(name) ? name : undefined);
|
|
1210
1243
|
return;
|
|
1211
1244
|
}
|
|
1212
1245
|
},
|
|
1246
|
+
FunctionDeclaration(path) {
|
|
1247
|
+
const decl = path.node;
|
|
1248
|
+
if (!isTopLevel(path) || !isIdentifier(decl.id))
|
|
1249
|
+
return;
|
|
1250
|
+
const exported = specifierExports.get(decl.id.name);
|
|
1251
|
+
if (exported === undefined || !wrapIfCandidate(path, exported))
|
|
1252
|
+
return;
|
|
1253
|
+
if (isReadBeforeDeclaration(path, decl.id.name))
|
|
1254
|
+
return;
|
|
1255
|
+
const fnExpr = withTypes(functionExpression(decl.id, decl.params, decl.body, decl.generator, decl.async), decl);
|
|
1256
|
+
path.replaceWith(variableDeclaration("const", [
|
|
1257
|
+
variableDeclarator(identifier(decl.id.name), ccCall(fnExpr, ccName ?? "cc"))
|
|
1258
|
+
]));
|
|
1259
|
+
changed = true;
|
|
1260
|
+
},
|
|
1261
|
+
VariableDeclaration(path) {
|
|
1262
|
+
if (!isTopLevel(path))
|
|
1263
|
+
return;
|
|
1264
|
+
wrapDeclarators(path.get("declarations"), (name) => specifierExports.get(name));
|
|
1265
|
+
},
|
|
1213
1266
|
ExportDefaultDeclaration(path) {
|
|
1214
1267
|
const decl = isNode(path.node.declaration) ? unwrapParens(path.node.declaration) : path.node.declaration;
|
|
1215
1268
|
if (isFunctionDeclaration(decl) && isIdentifier(decl.id)) {
|
|
@@ -1217,7 +1270,7 @@ function autoWrapComponents2(ast, filename) {
|
|
|
1217
1270
|
const fnPath = path.get("declaration");
|
|
1218
1271
|
if (!wrapIfCandidate(fnPath, name))
|
|
1219
1272
|
return;
|
|
1220
|
-
const fnDecl = functionDeclaration(decl.id, decl.params, decl.body, decl.generator, decl.async);
|
|
1273
|
+
const fnDecl = withTypes(functionDeclaration(decl.id, decl.params, decl.body, decl.generator, decl.async), decl);
|
|
1221
1274
|
const exportDefault = exportDefaultDeclaration(ccCall(identifier(name), ccName ?? "cc"));
|
|
1222
1275
|
path.replaceWithMultiple([fnDecl, exportDefault]);
|
|
1223
1276
|
changed = true;
|
|
@@ -1227,7 +1280,7 @@ function autoWrapComponents2(ast, filename) {
|
|
|
1227
1280
|
const fnPath = path.get("declaration");
|
|
1228
1281
|
if (!wrapIfCandidate(fnPath, null))
|
|
1229
1282
|
return;
|
|
1230
|
-
const fnExpr = functionExpression(null, decl.params, decl.body, decl.generator, decl.async);
|
|
1283
|
+
const fnExpr = withTypes(functionExpression(null, decl.params, decl.body, decl.generator, decl.async), decl);
|
|
1231
1284
|
path.node.declaration = ccCall(fnExpr, ccName ?? "cc");
|
|
1232
1285
|
changed = true;
|
|
1233
1286
|
return;
|
|
@@ -2236,7 +2289,7 @@ class AnalyzerCache2 {
|
|
|
2236
2289
|
if (!fs.existsSync(dir)) {
|
|
2237
2290
|
fs.mkdirSync(dir, { recursive: true });
|
|
2238
2291
|
}
|
|
2239
|
-
fs.writeFileSync(this.cachePath, JSON.stringify(data
|
|
2292
|
+
fs.writeFileSync(this.cachePath, JSON.stringify(data));
|
|
2240
2293
|
}
|
|
2241
2294
|
restore() {
|
|
2242
2295
|
if (!this.cachePath || !fs.existsSync(this.cachePath))
|
|
@@ -2492,7 +2545,32 @@ function trackReactiveImports2(ast) {
|
|
|
2492
2545
|
});
|
|
2493
2546
|
return names;
|
|
2494
2547
|
}
|
|
2548
|
+
function fileStamp(file) {
|
|
2549
|
+
return fs2.statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? -1;
|
|
2550
|
+
}
|
|
2551
|
+
function freshScan(cache, file) {
|
|
2552
|
+
const entry = cache.get(file);
|
|
2553
|
+
if (!entry)
|
|
2554
|
+
return;
|
|
2555
|
+
for (const [stamped, mtime] of entry.stamps) {
|
|
2556
|
+
if (fileStamp(stamped) !== mtime)
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
return entry;
|
|
2560
|
+
}
|
|
2561
|
+
function mergeStamps(into, from) {
|
|
2562
|
+
for (const [file, mtime] of from)
|
|
2563
|
+
into.set(file, mtime);
|
|
2564
|
+
}
|
|
2495
2565
|
var pureImportCache = new Map;
|
|
2566
|
+
function pureExportsOf(filePath, resolveImport, depth = 0) {
|
|
2567
|
+
const cached = freshScan(pureImportCache, filePath);
|
|
2568
|
+
if (cached)
|
|
2569
|
+
return cached;
|
|
2570
|
+
const scan = scanPureExports(filePath, resolveImport, depth);
|
|
2571
|
+
pureImportCache.set(filePath, scan);
|
|
2572
|
+
return scan;
|
|
2573
|
+
}
|
|
2496
2574
|
var PURE_EXTENSIONS = [
|
|
2497
2575
|
".ts",
|
|
2498
2576
|
".tsx",
|
|
@@ -2613,13 +2691,14 @@ function resolveAnyImport(source, fromFile, resolveImport) {
|
|
|
2613
2691
|
}
|
|
2614
2692
|
function scanPureExports(filePath, resolveImport, depth = 0) {
|
|
2615
2693
|
const pure = new Map;
|
|
2694
|
+
const scan = { value: pure, stamps: new Map([[filePath, fileStamp(filePath)]]) };
|
|
2616
2695
|
if (depth > 10)
|
|
2617
|
-
return
|
|
2696
|
+
return scan;
|
|
2618
2697
|
let code;
|
|
2619
2698
|
try {
|
|
2620
2699
|
code = fs2.readFileSync(filePath, "utf-8");
|
|
2621
2700
|
} catch {
|
|
2622
|
-
return
|
|
2701
|
+
return scan;
|
|
2623
2702
|
}
|
|
2624
2703
|
let ast;
|
|
2625
2704
|
try {
|
|
@@ -2629,7 +2708,7 @@ function scanPureExports(filePath, resolveImport, depth = 0) {
|
|
|
2629
2708
|
sourceFilename: filePath
|
|
2630
2709
|
});
|
|
2631
2710
|
} catch {
|
|
2632
|
-
return
|
|
2711
|
+
return scan;
|
|
2633
2712
|
}
|
|
2634
2713
|
const hasPureJSDoc = (node) => node?.leadingComments?.some((c) => c.type === "CommentBlock" && c.value.includes("@sinwan-pure")) ?? false;
|
|
2635
2714
|
traverse(ast, {
|
|
@@ -2657,11 +2736,9 @@ function scanPureExports(filePath, resolveImport, depth = 0) {
|
|
|
2657
2736
|
if (source && p.node.specifiers) {
|
|
2658
2737
|
const resolved = resolveAnyImport(source, filePath, resolveImport);
|
|
2659
2738
|
if (resolved) {
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
pureImportCache.set(resolved, reExportPure);
|
|
2664
|
-
}
|
|
2739
|
+
const nested = pureExportsOf(resolved, resolveImport, depth + 1);
|
|
2740
|
+
mergeStamps(scan.stamps, nested.stamps);
|
|
2741
|
+
const reExportPure = nested.value;
|
|
2665
2742
|
for (const spec of p.node.specifiers ?? []) {
|
|
2666
2743
|
if (!isExportSpecifier(spec))
|
|
2667
2744
|
continue;
|
|
@@ -2692,11 +2769,9 @@ function scanPureExports(filePath, resolveImport, depth = 0) {
|
|
|
2692
2769
|
const resolved = resolveAnyImport(source, filePath, resolveImport);
|
|
2693
2770
|
if (!resolved)
|
|
2694
2771
|
return;
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
pureImportCache.set(resolved, reExportPure);
|
|
2699
|
-
}
|
|
2772
|
+
const nested = pureExportsOf(resolved, resolveImport, depth + 1);
|
|
2773
|
+
mergeStamps(scan.stamps, nested.stamps);
|
|
2774
|
+
const reExportPure = nested.value;
|
|
2700
2775
|
for (const [name, kind] of reExportPure) {
|
|
2701
2776
|
if (name !== "default") {
|
|
2702
2777
|
pure.set(name, kind);
|
|
@@ -2704,7 +2779,7 @@ function scanPureExports(filePath, resolveImport, depth = 0) {
|
|
|
2704
2779
|
}
|
|
2705
2780
|
}
|
|
2706
2781
|
});
|
|
2707
|
-
return
|
|
2782
|
+
return scan;
|
|
2708
2783
|
}
|
|
2709
2784
|
function collectPureImports(ast, filename, resolveImport) {
|
|
2710
2785
|
const result = {
|
|
@@ -2725,11 +2800,7 @@ function collectPureImports(ast, filename, resolveImport) {
|
|
|
2725
2800
|
const resolved = resolveAnyImport(source, absoluteFilename, resolveImport);
|
|
2726
2801
|
if (!resolved)
|
|
2727
2802
|
return;
|
|
2728
|
-
|
|
2729
|
-
if (!filePure) {
|
|
2730
|
-
filePure = scanPureExports(resolved, resolveImport);
|
|
2731
|
-
pureImportCache.set(resolved, filePure);
|
|
2732
|
-
}
|
|
2803
|
+
const filePure = pureExportsOf(resolved, resolveImport).value;
|
|
2733
2804
|
for (const spec of p.node.specifiers ?? []) {
|
|
2734
2805
|
if (isImportSpecifier(spec)) {
|
|
2735
2806
|
const imported = importedBindingName(spec.imported);
|
|
@@ -3684,11 +3755,16 @@ function hasSnapshotPragma(node) {
|
|
|
3684
3755
|
function isTypePosition(ref) {
|
|
3685
3756
|
return ref.findParent((parent) => parent.isTSType()) != null;
|
|
3686
3757
|
}
|
|
3758
|
+
function hasPureJSDoc(node) {
|
|
3759
|
+
return node?.leadingComments?.some((comment) => comment.type === "CommentBlock" && comment.value.includes("@sinwan-pure")) ?? false;
|
|
3760
|
+
}
|
|
3687
3761
|
function canRewriteDerivedReference(ref) {
|
|
3688
3762
|
if (!ref.isIdentifier() || !ref.isReferencedIdentifier())
|
|
3689
3763
|
return false;
|
|
3690
3764
|
if (isTypePosition(ref))
|
|
3691
3765
|
return true;
|
|
3766
|
+
if (isPureArrayReadReceiver(ref))
|
|
3767
|
+
return true;
|
|
3692
3768
|
let target = ref;
|
|
3693
3769
|
while ((target.parentPath?.isMemberExpression() || target.parentPath?.isOptionalMemberExpression()) && target.key === "object" || (target.parentPath?.isTSNonNullExpression() || target.parentPath?.isTSAsExpression() || target.parentPath?.isTSTypeAssertion() || target.parentPath?.isTSSatisfiesExpression() || target.parentPath?.isParenthesizedExpression()) && target.key === "expression") {
|
|
3694
3770
|
target = target.parentPath;
|
|
@@ -3696,12 +3772,165 @@ function canRewriteDerivedReference(ref) {
|
|
|
3696
3772
|
const parent = target.parentPath;
|
|
3697
3773
|
return !(parent?.isAssignmentExpression() && target.key === "left" || parent?.isUpdateExpression() || parent?.isUnaryExpression({ operator: "delete" }) || (parent?.isForInStatement() || parent?.isForOfStatement()) && target.key === "left" || target !== ref && (parent?.isCallExpression() || parent?.isOptionalCallExpression()) && target.key === "callee");
|
|
3698
3774
|
}
|
|
3699
|
-
|
|
3700
|
-
|
|
3775
|
+
var ARRAY_RESULT_READS = new Set(["map", "filter", "flatMap", "flat", "slice", "concat"]);
|
|
3776
|
+
var CALLBACK_ARRAY_READS = new Set([
|
|
3777
|
+
"map",
|
|
3778
|
+
"filter",
|
|
3779
|
+
"flatMap",
|
|
3780
|
+
"find",
|
|
3781
|
+
"findIndex",
|
|
3782
|
+
"some",
|
|
3783
|
+
"every",
|
|
3784
|
+
"reduce"
|
|
3785
|
+
]);
|
|
3786
|
+
var PURE_ARRAY_READS = new Set([
|
|
3787
|
+
...ARRAY_RESULT_READS,
|
|
3788
|
+
...CALLBACK_ARRAY_READS,
|
|
3789
|
+
"join",
|
|
3790
|
+
"at",
|
|
3791
|
+
"includes",
|
|
3792
|
+
"indexOf",
|
|
3793
|
+
"entries",
|
|
3794
|
+
"keys",
|
|
3795
|
+
"values"
|
|
3796
|
+
]);
|
|
3797
|
+
var DATE_READS = new Set([
|
|
3798
|
+
"getFullYear",
|
|
3799
|
+
"getMonth",
|
|
3800
|
+
"getDate",
|
|
3801
|
+
"getDay",
|
|
3802
|
+
"getTime",
|
|
3803
|
+
"getHours",
|
|
3804
|
+
"getMinutes",
|
|
3805
|
+
"getSeconds",
|
|
3806
|
+
"getMilliseconds",
|
|
3807
|
+
"valueOf"
|
|
3808
|
+
]);
|
|
3809
|
+
var PURE_METHOD_READS = new Set([...PURE_ARRAY_READS, ...DATE_READS]);
|
|
3810
|
+
function builtinKindOfType(node) {
|
|
3811
|
+
const typeNode = unwrapTypeNode(node);
|
|
3812
|
+
if (typeNode?.type === "TSArrayType")
|
|
3813
|
+
return "array";
|
|
3814
|
+
if (typeNode?.type !== "TSTypeReference")
|
|
3815
|
+
return null;
|
|
3816
|
+
const name = typeNode.typeName;
|
|
3817
|
+
if (!isIdentifier(name))
|
|
3818
|
+
return null;
|
|
3819
|
+
if (name.name === "Array" || name.name === "ReadonlyArray")
|
|
3820
|
+
return "array";
|
|
3821
|
+
return name.name === "Date" ? "date" : null;
|
|
3822
|
+
}
|
|
3823
|
+
function isGlobalArrayFrom(callee, scope) {
|
|
3824
|
+
return isMemberExpression(callee) && !callee.computed && isIdentifier(callee.object, { name: "Array" }) && isIdentifier(callee.property, { name: "from" }) && !scope.getBinding("Array");
|
|
3825
|
+
}
|
|
3826
|
+
function builtinKindOf(node, scope, seen = new Set) {
|
|
3827
|
+
const expr = unwrapParens(node);
|
|
3828
|
+
if (expr.type === "ArrayExpression")
|
|
3829
|
+
return "array";
|
|
3830
|
+
if (isCallExpression(expr)) {
|
|
3831
|
+
const callee = unwrapParens(expr.callee);
|
|
3832
|
+
if (isGlobalArrayFrom(callee, scope))
|
|
3833
|
+
return "array";
|
|
3834
|
+
if (isMemberExpression(callee)) {
|
|
3835
|
+
return !callee.computed && isIdentifier(callee.property) && ARRAY_RESULT_READS.has(callee.property.name) && builtinKindOf(callee.object, scope, seen) === "array" ? "array" : null;
|
|
3836
|
+
}
|
|
3837
|
+
const fn = isIdentifier(callee) ? scope.getBinding(callee.name)?.path : undefined;
|
|
3838
|
+
const declared = fn?.isVariableDeclarator() ? fn.node.init : fn?.node;
|
|
3839
|
+
return functionParamCount(declared) === null ? null : builtinKindOfType(declared?.returnType);
|
|
3840
|
+
}
|
|
3841
|
+
if (!isIdentifier(expr))
|
|
3842
|
+
return null;
|
|
3843
|
+
const binding = scope.getBinding(expr.name);
|
|
3844
|
+
if (!binding || binding.kind !== "const" || !binding.path.isVariableDeclarator() || seen.has(binding.path.node)) {
|
|
3845
|
+
return null;
|
|
3846
|
+
}
|
|
3847
|
+
seen.add(binding.path.node);
|
|
3848
|
+
const declarator = binding.path.node;
|
|
3849
|
+
return builtinKindOfType(declarator.id?.typeAnnotation) ?? (declarator.init ? builtinKindOf(declarator.init, binding.path.scope, seen) : null);
|
|
3850
|
+
}
|
|
3851
|
+
function functionParamCount(node) {
|
|
3852
|
+
if (!node || !isFunctionDeclaration(node) && !isFunctionExpression(node) && !isArrowFunctionExpression(node)) {
|
|
3853
|
+
return null;
|
|
3854
|
+
}
|
|
3855
|
+
return node.params.length;
|
|
3856
|
+
}
|
|
3857
|
+
function isPureArrayReadReceiver(ref) {
|
|
3858
|
+
const member = ref.parentPath;
|
|
3859
|
+
if (!member || !member.isMemberExpression() && !member.isOptionalMemberExpression() || ref.key !== "object" || member.node.computed || !isIdentifier(member.node.property) || !PURE_METHOD_READS.has(member.node.property.name)) {
|
|
3701
3860
|
return false;
|
|
3861
|
+
}
|
|
3862
|
+
const call = member.parentPath;
|
|
3863
|
+
return !!call && (call.isCallExpression() || call.isOptionalCallExpression()) && member.key === "callee";
|
|
3864
|
+
}
|
|
3865
|
+
function callParts(path) {
|
|
3702
3866
|
const call = unwrapCall(path.node);
|
|
3703
|
-
|
|
3704
|
-
|
|
3867
|
+
return { callee: unwrapParens(call.callee), args: call.arguments ?? [] };
|
|
3868
|
+
}
|
|
3869
|
+
function isSameFilePureFunction(binding) {
|
|
3870
|
+
const owner = binding.path.parentPath?.node;
|
|
3871
|
+
if (binding.path.isFunctionDeclaration()) {
|
|
3872
|
+
return hasPureJSDoc(binding.path.node) || owner?.type === "ExportNamedDeclaration" && hasPureJSDoc(owner);
|
|
3873
|
+
}
|
|
3874
|
+
if (!binding.path.isVariableDeclarator())
|
|
3875
|
+
return false;
|
|
3876
|
+
const declarator = binding.path.node;
|
|
3877
|
+
const exported = binding.path.parentPath?.parentPath?.node;
|
|
3878
|
+
return functionParamCount(declarator.init) != null && (hasPureJSDoc(declarator) || hasPureJSDoc(declarator.init) || hasPureJSDoc(owner) || exported?.type === "ExportNamedDeclaration" && hasPureJSDoc(exported));
|
|
3879
|
+
}
|
|
3880
|
+
function isPureFunctionReference(node, path, pureImports) {
|
|
3881
|
+
if (!isIdentifier(node))
|
|
3882
|
+
return false;
|
|
3883
|
+
const binding = path.scope.getBinding(node.name);
|
|
3884
|
+
if (!binding)
|
|
3885
|
+
return false;
|
|
3886
|
+
return isSameFilePureFunction(binding) || (binding.path.isImportSpecifier() || binding.path.isImportDefaultSpecifier()) && pureImports.direct.has(node.name);
|
|
3887
|
+
}
|
|
3888
|
+
function acceptCallback(arg, path, ctx) {
|
|
3889
|
+
if (isArrowFunctionExpression(arg) || isFunctionExpression(arg)) {
|
|
3890
|
+
ctx.callbacks.add(arg);
|
|
3891
|
+
return true;
|
|
3892
|
+
}
|
|
3893
|
+
return isPureFunctionReference(arg, path, ctx.pureImports);
|
|
3894
|
+
}
|
|
3895
|
+
function isBuiltinReadCall(path, callee, args, ctx) {
|
|
3896
|
+
if (isGlobalArrayFrom(callee, path.scope)) {
|
|
3897
|
+
return args.length < 2 || acceptCallback(args[1], path, ctx);
|
|
3898
|
+
}
|
|
3899
|
+
if (!isMemberExpression(callee) || callee.computed || !isIdentifier(callee.property)) {
|
|
3900
|
+
return false;
|
|
3901
|
+
}
|
|
3902
|
+
const method = callee.property.name;
|
|
3903
|
+
const kind = builtinKindOf(callee.object, path.scope);
|
|
3904
|
+
if (kind === "date")
|
|
3905
|
+
return DATE_READS.has(method);
|
|
3906
|
+
if (kind !== "array" || !PURE_ARRAY_READS.has(method))
|
|
3907
|
+
return false;
|
|
3908
|
+
return !CALLBACK_ARRAY_READS.has(method) || acceptCallback(args[0], path, ctx);
|
|
3909
|
+
}
|
|
3910
|
+
function inspectLocalGetterCall(path, ctx) {
|
|
3911
|
+
const { callee, args } = callParts(path);
|
|
3912
|
+
if (args.length !== 0 || !isIdentifier(callee))
|
|
3913
|
+
return null;
|
|
3914
|
+
const binding = path.scope.getBinding(callee.name);
|
|
3915
|
+
if (!binding?.path.isVariableDeclarator() || isSameFilePureFunction(binding)) {
|
|
3916
|
+
return null;
|
|
3917
|
+
}
|
|
3918
|
+
const declarator = binding.path;
|
|
3919
|
+
const getter = declarator.node.init;
|
|
3920
|
+
if (functionParamCount(getter) !== 0)
|
|
3921
|
+
return null;
|
|
3922
|
+
if (!isArrowFunctionExpression(getter) || getter.async || isBlockStatement(getter.body) || ctx.visiting.has(getter)) {
|
|
3923
|
+
return { safe: false, reactive: false };
|
|
3924
|
+
}
|
|
3925
|
+
ctx.visiting.add(getter);
|
|
3926
|
+
const result = inspectDerivedExpression(singlePath(declarator.get("init.body")), declarator.node.start ?? Infinity, ctx);
|
|
3927
|
+
ctx.visiting.delete(getter);
|
|
3928
|
+
return result;
|
|
3929
|
+
}
|
|
3930
|
+
function isPureDerivedCall(path, ctx) {
|
|
3931
|
+
const { callee, args } = callParts(path);
|
|
3932
|
+
if (isBuiltinReadCall(path, callee, args, ctx))
|
|
3933
|
+
return true;
|
|
3705
3934
|
if (isMemberExpression(callee) || isOptionalMemberExpression(callee)) {
|
|
3706
3935
|
const props = [];
|
|
3707
3936
|
let node = unwrapParens(callee);
|
|
@@ -3726,10 +3955,10 @@ function isPureDerivedCall(path, names, pureImports) {
|
|
|
3726
3955
|
if (!isIdentifier(node))
|
|
3727
3956
|
return false;
|
|
3728
3957
|
const root = node.name;
|
|
3729
|
-
if (pureImports.member.has(root)) {
|
|
3958
|
+
if (ctx.pureImports.member.has(root)) {
|
|
3730
3959
|
return true;
|
|
3731
3960
|
}
|
|
3732
|
-
const nsMembers = pureImports.namespace.get(root);
|
|
3961
|
+
const nsMembers = ctx.pureImports.namespace.get(root);
|
|
3733
3962
|
if (nsMembers && props.some((p) => nsMembers.has(p))) {
|
|
3734
3963
|
return true;
|
|
3735
3964
|
}
|
|
@@ -3741,17 +3970,14 @@ function isPureDerivedCall(path, names, pureImports) {
|
|
|
3741
3970
|
if (!binding) {
|
|
3742
3971
|
return ["Boolean", "Number", "String"].includes(callee.name) && args.length === 1;
|
|
3743
3972
|
}
|
|
3744
|
-
if ((
|
|
3973
|
+
if (isPureFunctionReference(callee, path, ctx.pureImports))
|
|
3745
3974
|
return true;
|
|
3746
|
-
}
|
|
3747
3975
|
if (args.length !== 0 || !binding.path.isVariableDeclarator())
|
|
3748
3976
|
return false;
|
|
3749
3977
|
const { id, init } = binding.path.node;
|
|
3750
|
-
return isArrayPattern(id) && id.elements[0] === binding.identifier && isCallExpression(init) && isIdentifier(init.callee) && names.useState.has(init.callee.name) && binding.path.scope.getBinding(init.callee.name)?.path.isImportSpecifier() === true;
|
|
3978
|
+
return isArrayPattern(id) && id.elements[0] === binding.identifier && isCallExpression(init) && isIdentifier(init.callee) && ctx.names.useState.has(init.callee.name) && binding.path.scope.getBinding(init.callee.name)?.path.isImportSpecifier() === true;
|
|
3751
3979
|
}
|
|
3752
|
-
function
|
|
3753
|
-
const init = singlePath(declaration.get("init"));
|
|
3754
|
-
init.assertExpression();
|
|
3980
|
+
function inspectDerivedExpression(root, before, ctx) {
|
|
3755
3981
|
let safe = true;
|
|
3756
3982
|
let reactive = false;
|
|
3757
3983
|
const inspect = (path) => {
|
|
@@ -3762,29 +3988,49 @@ function isDerivedInitializer(declaration, scope, names, derived, pureImports) {
|
|
|
3762
3988
|
const node = path.node;
|
|
3763
3989
|
if (path.isReferencedIdentifier()) {
|
|
3764
3990
|
const binding = path.scope.getBinding(path.node.name);
|
|
3765
|
-
if (binding && (!binding.constant || binding.kind === "let" || binding.kind === "var" || binding.path.isVariableDeclarator() && (binding.path.node.start ?? -1) >=
|
|
3991
|
+
if (binding && (!binding.constant || binding.kind === "let" || binding.kind === "var" || binding.path.isVariableDeclarator() && (binding.path.node.start ?? -1) >= before)) {
|
|
3766
3992
|
safe = false;
|
|
3767
3993
|
return;
|
|
3768
3994
|
}
|
|
3769
|
-
if (binding && derived.has(binding))
|
|
3995
|
+
if (binding && ctx.derived.has(binding))
|
|
3770
3996
|
reactive = true;
|
|
3771
|
-
if (isReactiveRead(node, scope))
|
|
3997
|
+
if (isReactiveRead(node, ctx.scope))
|
|
3772
3998
|
reactive = true;
|
|
3773
3999
|
} else if (path.isMemberExpression() || path.isOptionalMemberExpression()) {
|
|
3774
|
-
if (isReactiveRead(node, scope))
|
|
4000
|
+
if (isReactiveRead(node, ctx.scope))
|
|
3775
4001
|
reactive = true;
|
|
3776
4002
|
} else if (path.isCallExpression() || path.isOptionalCallExpression()) {
|
|
3777
|
-
|
|
4003
|
+
const getter = inspectLocalGetterCall(path, ctx);
|
|
4004
|
+
if (getter) {
|
|
4005
|
+
if (!getter.safe)
|
|
4006
|
+
safe = false;
|
|
4007
|
+
if (getter.reactive)
|
|
4008
|
+
reactive = true;
|
|
4009
|
+
} else if (!isPureDerivedCall(path, ctx)) {
|
|
3778
4010
|
safe = false;
|
|
3779
|
-
|
|
4011
|
+
}
|
|
4012
|
+
if (isGetterCall(path.node, ctx.scope))
|
|
3780
4013
|
reactive = true;
|
|
3781
|
-
} else if (path.isSpreadElement()) {} else if (!(path.isIdentifier() || path.isLiteral() || path.isTemplateLiteral() || path.isTemplateElement() || path.isBinaryExpression() || path.isLogicalExpression() || path.isConditionalExpression() || path.isObjectExpression() || path.isObjectProperty() || path.isArrayExpression() || path.isParenthesizedExpression() || path.node.type === "ChainExpression" || path.isUnaryExpression() && path.node.operator !== "delete" || path.isTSAsExpression() || path.isTSSatisfiesExpression() || path.isTSNonNullExpression() || path.isTSTypeAssertion())) {
|
|
4014
|
+
} else if ((path.isArrowFunctionExpression() || path.isFunctionExpression()) && ctx.callbacks.has(path.node)) {} else if (path.isSpreadElement()) {} else if (!(path.isIdentifier() || path.isLiteral() || path.isTemplateLiteral() || path.isTemplateElement() || path.isBinaryExpression() || path.isLogicalExpression() || path.isConditionalExpression() || path.isObjectExpression() || path.isObjectProperty() || path.isArrayExpression() || path.isParenthesizedExpression() || path.node.type === "ChainExpression" || path.isUnaryExpression() && path.node.operator !== "delete" || path.isTSAsExpression() || path.isTSSatisfiesExpression() || path.isTSNonNullExpression() || path.isTSTypeAssertion())) {
|
|
3782
4015
|
safe = false;
|
|
3783
4016
|
}
|
|
3784
4017
|
};
|
|
3785
|
-
inspect(
|
|
4018
|
+
inspect(root);
|
|
3786
4019
|
if (safe)
|
|
3787
|
-
|
|
4020
|
+
root.traverse({ enter: inspect });
|
|
4021
|
+
return { safe, reactive };
|
|
4022
|
+
}
|
|
4023
|
+
function isDerivedInitializer(declaration, scope, names, derived, pureImports) {
|
|
4024
|
+
const init = singlePath(declaration.get("init"));
|
|
4025
|
+
init.assertExpression();
|
|
4026
|
+
const { safe, reactive } = inspectDerivedExpression(init, declaration.node.start ?? Infinity, {
|
|
4027
|
+
scope,
|
|
4028
|
+
names,
|
|
4029
|
+
derived,
|
|
4030
|
+
pureImports,
|
|
4031
|
+
callbacks: new Set,
|
|
4032
|
+
visiting: new Set
|
|
4033
|
+
});
|
|
3788
4034
|
return safe && reactive;
|
|
3789
4035
|
}
|
|
3790
4036
|
function promoteReactiveConstInitializers(ast, options) {
|
|
@@ -3873,8 +4119,196 @@ function promoteReactiveConstInitializers(ast, options) {
|
|
|
3873
4119
|
}
|
|
3874
4120
|
});
|
|
3875
4121
|
}
|
|
4122
|
+
var readerParamCache = new Map;
|
|
4123
|
+
function unwrapTypeNode(node) {
|
|
4124
|
+
if (!node)
|
|
4125
|
+
return null;
|
|
4126
|
+
if (node.type === "TSTypeAnnotation" || node.type === "TSParenthesizedType") {
|
|
4127
|
+
return unwrapTypeNode(node.typeAnnotation ?? null);
|
|
4128
|
+
}
|
|
4129
|
+
return node;
|
|
4130
|
+
}
|
|
4131
|
+
function typeReferencesLiveRead(node) {
|
|
4132
|
+
const typeNode = unwrapTypeNode(node);
|
|
4133
|
+
if (!typeNode)
|
|
4134
|
+
return false;
|
|
4135
|
+
if (typeNode.type === "TSTypeReference") {
|
|
4136
|
+
const name = typeNode.typeName;
|
|
4137
|
+
return isIdentifier(name) && name.name === "LiveRead";
|
|
4138
|
+
}
|
|
4139
|
+
if (typeNode.type === "TSUnionType" || typeNode.type === "TSIntersectionType") {
|
|
4140
|
+
const types = typeNode.types ?? [];
|
|
4141
|
+
return types.some((part) => typeReferencesLiveRead(part));
|
|
4142
|
+
}
|
|
4143
|
+
return false;
|
|
4144
|
+
}
|
|
4145
|
+
function readerParamIndexes(params) {
|
|
4146
|
+
const indexes = [];
|
|
4147
|
+
for (const [index, param] of (params ?? []).entries()) {
|
|
4148
|
+
if (!param || param.type === "RestElement")
|
|
4149
|
+
continue;
|
|
4150
|
+
const annotated = param.type === "AssignmentPattern" ? param.left : param;
|
|
4151
|
+
const annotation = param.typeAnnotation ?? annotated?.typeAnnotation;
|
|
4152
|
+
if (typeReferencesLiveRead(annotation))
|
|
4153
|
+
indexes.push(index);
|
|
4154
|
+
}
|
|
4155
|
+
return indexes;
|
|
4156
|
+
}
|
|
4157
|
+
function functionParamsOf(node) {
|
|
4158
|
+
if (!node || !isFunctionDeclaration(node) && !isFunctionExpression(node) && !isArrowFunctionExpression(node)) {
|
|
4159
|
+
return null;
|
|
4160
|
+
}
|
|
4161
|
+
return node.params;
|
|
4162
|
+
}
|
|
4163
|
+
function scanReaderExports(filePath, resolveImport, depth = 0) {
|
|
4164
|
+
const cached = freshScan(readerParamCache, filePath);
|
|
4165
|
+
if (cached)
|
|
4166
|
+
return cached;
|
|
4167
|
+
const found = new Map;
|
|
4168
|
+
const scan = { value: found, stamps: new Map([[filePath, fileStamp(filePath)]]) };
|
|
4169
|
+
readerParamCache.set(filePath, scan);
|
|
4170
|
+
if (depth > 10)
|
|
4171
|
+
return scan;
|
|
4172
|
+
let code;
|
|
4173
|
+
try {
|
|
4174
|
+
code = fs2.readFileSync(filePath, "utf-8");
|
|
4175
|
+
} catch {
|
|
4176
|
+
return scan;
|
|
4177
|
+
}
|
|
4178
|
+
let ast;
|
|
4179
|
+
try {
|
|
4180
|
+
ast = parse(code, {
|
|
4181
|
+
sourceType: "module",
|
|
4182
|
+
plugins: ["jsx", "typescript"],
|
|
4183
|
+
sourceFilename: filePath
|
|
4184
|
+
});
|
|
4185
|
+
} catch {
|
|
4186
|
+
return scan;
|
|
4187
|
+
}
|
|
4188
|
+
const record = (name, params) => {
|
|
4189
|
+
if (!params)
|
|
4190
|
+
return;
|
|
4191
|
+
const indexes = readerParamIndexes(params);
|
|
4192
|
+
if (indexes.length > 0)
|
|
4193
|
+
found.set(name, indexes);
|
|
4194
|
+
};
|
|
4195
|
+
traverse(ast, {
|
|
4196
|
+
ExportNamedDeclaration(p) {
|
|
4197
|
+
const decl = p.node.declaration;
|
|
4198
|
+
if (isFunctionDeclaration(decl) && decl.id && isIdentifier(decl.id)) {
|
|
4199
|
+
record(decl.id.name, decl.params);
|
|
4200
|
+
} else if (decl && isVariableDeclaration(decl)) {
|
|
4201
|
+
for (const declarator of decl.declarations) {
|
|
4202
|
+
if (!isIdentifier(declarator.id))
|
|
4203
|
+
continue;
|
|
4204
|
+
record(declarator.id.name, functionParamsOf(declarator.init));
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
const source = moduleSourceValue(p.node.source);
|
|
4208
|
+
if (!source)
|
|
4209
|
+
return;
|
|
4210
|
+
const resolved = resolveAnyImport(source, filePath, resolveImport);
|
|
4211
|
+
if (!resolved)
|
|
4212
|
+
return;
|
|
4213
|
+
const nested = scanReaderExports(resolved, resolveImport, depth + 1);
|
|
4214
|
+
mergeStamps(scan.stamps, nested.stamps);
|
|
4215
|
+
for (const spec of p.node.specifiers ?? []) {
|
|
4216
|
+
if (!isExportSpecifier(spec))
|
|
4217
|
+
continue;
|
|
4218
|
+
const exported = importedBindingName(spec.exported);
|
|
4219
|
+
const local = importedBindingName(spec.local);
|
|
4220
|
+
if (!exported || !local)
|
|
4221
|
+
continue;
|
|
4222
|
+
const indexes = nested.value.get(local);
|
|
4223
|
+
if (indexes)
|
|
4224
|
+
found.set(exported, indexes);
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
});
|
|
4228
|
+
return scan;
|
|
4229
|
+
}
|
|
4230
|
+
function readerIndexesForCallee(path, imported) {
|
|
4231
|
+
const callee = unwrapParens(path.node.callee);
|
|
4232
|
+
if (!isIdentifier(callee))
|
|
4233
|
+
return [];
|
|
4234
|
+
const binding = path.scope.getBinding(callee.name);
|
|
4235
|
+
if (!binding)
|
|
4236
|
+
return [];
|
|
4237
|
+
if (binding.path.isImportSpecifier() || binding.path.isImportDefaultSpecifier()) {
|
|
4238
|
+
return imported.get(callee.name) ?? [];
|
|
4239
|
+
}
|
|
4240
|
+
if (binding.path.isFunctionDeclaration()) {
|
|
4241
|
+
return readerParamIndexes(binding.path.node.params);
|
|
4242
|
+
}
|
|
4243
|
+
if (binding.path.isVariableDeclarator()) {
|
|
4244
|
+
return readerParamIndexes(functionParamsOf(binding.path.node.init) ?? []);
|
|
4245
|
+
}
|
|
4246
|
+
return [];
|
|
4247
|
+
}
|
|
4248
|
+
function wrapLiveReadArguments(ast, options) {
|
|
4249
|
+
const names = trackReactiveImports2(ast);
|
|
4250
|
+
const imported = new Map;
|
|
4251
|
+
const filename = options.filename ? path2.resolve(options.filename) : "";
|
|
4252
|
+
traverse(ast, {
|
|
4253
|
+
ImportDeclaration(p) {
|
|
4254
|
+
const source = moduleSourceValue(p.node.source);
|
|
4255
|
+
if (!source || !filename)
|
|
4256
|
+
return;
|
|
4257
|
+
const resolved = resolveAnyImport(source, filename, options.resolveImport);
|
|
4258
|
+
if (!resolved)
|
|
4259
|
+
return;
|
|
4260
|
+
const exports = scanReaderExports(resolved, options.resolveImport).value;
|
|
4261
|
+
for (const spec of p.node.specifiers ?? []) {
|
|
4262
|
+
if (isImportSpecifier(spec)) {
|
|
4263
|
+
const exported = importedBindingName(spec.imported);
|
|
4264
|
+
const indexes = exported ? exports.get(exported) : undefined;
|
|
4265
|
+
if (indexes)
|
|
4266
|
+
imported.set(spec.local.name, indexes);
|
|
4267
|
+
} else if (isImportDefaultSpecifier(spec)) {
|
|
4268
|
+
const indexes = exports.get("default");
|
|
4269
|
+
if (indexes)
|
|
4270
|
+
imported.set(spec.local.name, indexes);
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
});
|
|
4275
|
+
const { functions } = collectComponentFunctions2(ast, names.cc);
|
|
4276
|
+
const scopes = new Map;
|
|
4277
|
+
traverse(ast, {
|
|
4278
|
+
CallExpression(callPath) {
|
|
4279
|
+
const indexes = readerIndexesForCallee(callPath, imported);
|
|
4280
|
+
if (indexes.length === 0)
|
|
4281
|
+
return;
|
|
4282
|
+
const fnPath = callPath.getFunctionParent();
|
|
4283
|
+
const fn = fnPath?.node;
|
|
4284
|
+
if (!fnPath || !fn || !isFunction(fn) || !functions.has(fn))
|
|
4285
|
+
return;
|
|
4286
|
+
let scope = scopes.get(fn);
|
|
4287
|
+
if (!scope) {
|
|
4288
|
+
scope = { bindings: new Map };
|
|
4289
|
+
trackPropBindings(scope, fn.params[0], getAllPropNames2(fn.params[0]));
|
|
4290
|
+
trackLocalScopeBindings(fnPath, names, scope, true);
|
|
4291
|
+
scopes.set(fn, scope);
|
|
4292
|
+
}
|
|
4293
|
+
const argPaths = callPath.get("arguments");
|
|
4294
|
+
const args = Array.isArray(argPaths) ? argPaths : [argPaths];
|
|
4295
|
+
for (const index of indexes) {
|
|
4296
|
+
const argPath = args[index];
|
|
4297
|
+
if (!argPath || argPath.node.type === "SpreadElement")
|
|
4298
|
+
continue;
|
|
4299
|
+
const expr = unwrapParens(argPath.node);
|
|
4300
|
+
if (!isExpression(expr) || isFunctionLike(expr))
|
|
4301
|
+
continue;
|
|
4302
|
+
if (!containsReactiveRead(expr, scope))
|
|
4303
|
+
continue;
|
|
4304
|
+
argPath.replaceWith(arrowFunctionExpression([], argPath.node));
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
});
|
|
4308
|
+
}
|
|
3876
4309
|
function wrapReactiveExpressions2(ast, options = {}) {
|
|
3877
4310
|
promoteReactiveConstInitializers(ast, options);
|
|
4311
|
+
wrapLiveReadArguments(ast, options);
|
|
3878
4312
|
let metadata = null;
|
|
3879
4313
|
if (options.analyzeMetadata) {
|
|
3880
4314
|
metadata = options.analyzeMetadata;
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
propagateReactiveProps2,
|
|
65
65
|
wrapReactiveExpressions2,
|
|
66
66
|
runAnalyzeCli2
|
|
67
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-52vtv5h0.js";
|
|
68
68
|
|
|
69
69
|
// src/live-cc-destructure.ts
|
|
70
70
|
var CC_SOURCE_MODULES = new Set(["sinwan", "sinwan/component"]);
|
|
@@ -216,7 +216,7 @@ function hasLocalSpecifier(stmt, local) {
|
|
|
216
216
|
function ensureComponentImport(ast, imported, local) {
|
|
217
217
|
let target = null;
|
|
218
218
|
for (const stmt of ast.program.body) {
|
|
219
|
-
if (!isImportDeclaration(stmt))
|
|
219
|
+
if (!isImportDeclaration(stmt) || stmt.importKind === "type")
|
|
220
220
|
continue;
|
|
221
221
|
if (hasLocalSpecifier(stmt, local))
|
|
222
222
|
return;
|
package/docs/api.md
CHANGED
|
@@ -29,7 +29,7 @@ See [Transform](transform.md) for wrapping rules and options.
|
|
|
29
29
|
|
|
30
30
|
### `autoWrapComponents(ast, filename?)`
|
|
31
31
|
|
|
32
|
-
Mutates an ESTree AST (oxc-parser): wrap exported component-like functions with `cc(...)`. Returns `true` if anything changed. `transformJSX` already runs this.
|
|
32
|
+
Mutates an ESTree AST (oxc-parser): wrap exported component-like functions (including `function X` / `const X` exported through `export { X }`) with `cc(...)`. Returns `true` if anything changed. `transformJSX` already runs this.
|
|
33
33
|
|
|
34
34
|
### `rewriteLiveCcDestructure(ast)`
|
|
35
35
|
|
package/docs/transform.md
CHANGED
|
@@ -32,7 +32,7 @@ Exported functions that look like components are wrapped for you. You can write
|
|
|
32
32
|
|
|
33
33
|
A candidate must be:
|
|
34
34
|
|
|
35
|
-
- **Exported** (named or default)
|
|
35
|
+
- **Exported** (named or default): `export function`, `export const`, `export default`, or a top-level `function` / `const` exported through `export { X }` or `export { X as Name }` (the exported name must be uppercase)
|
|
36
36
|
- **Uppercase** (or an anonymous `export default function ()`)
|
|
37
37
|
- **0 or 1 parameter**
|
|
38
38
|
- **Returning JSX** in that function (not only in a nested helper)
|
|
@@ -50,7 +50,9 @@ export const App = cc(function App() {
|
|
|
50
50
|
});
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
Not wrapped: lowercase helpers, generators, multi-argument functions,
|
|
53
|
+
Not wrapped: lowercase helpers, generators, multi-argument functions, functions that never return JSX, type-only exports, re-exports (`export { X } from "./x"`), components that are not exported, and a `function X` exported through `export { X }` that module code reads before its declaration (for example `const Group = Object.assign(X, { Item })` above it), because turning it into `const X = cc(...)` would make that read throw. Reads inside functions, export specifiers and types do not count. A `cc` parent passes reactive props to child components as getters, so a non-exported component that receives props from one must be wrapped with `cc(...)` explicitly. If `cc` is not imported yet, the compiler adds `import { cc } from "sinwan/component"`.
|
|
54
|
+
|
|
55
|
+
Type parameters and return types are kept on the wrapped function. A circular import that reads `X` at module load before this file finishes evaluating is not detected; `export function X` has the same limitation.
|
|
54
56
|
|
|
55
57
|
`cc` may be imported from `sinwan` or `sinwan/component`.
|
|
56
58
|
|
|
@@ -138,7 +140,10 @@ return (
|
|
|
138
140
|
|
|
139
141
|
- Only referenced, identifier-named `const` declarations directly in a `cc` setup body (including auto-`cc` components) are candidates. Dependencies are processed in declaration order, including multiple declarators in one statement.
|
|
140
142
|
- Pure read expressions are supported: member/index/optional access, arithmetic, comparisons, boolean/conditional expressions, template strings, object/array literals, and TypeScript expression annotations. Direct imported `useState` getter calls and unshadowed single-argument `Boolean`, `Number`, and `String` coercions are also supported. Property reads and coercions must be free of user-defined side effects.
|
|
141
|
-
- Calls to arbitrary helpers, hooks, factories or methods, assignments, updates, allocations, destructuring, `let`/`var`, mutable captures, and forward/TDZ references remain eager — **unless** the callee is
|
|
143
|
+
- Calls to arbitrary helpers, hooks, factories or methods, assignments, updates, allocations, destructuring, `let`/`var`, mutable captures, and forward/TDZ references remain eager — **unless** the callee is a function annotated with `/** @sinwan-pure */` (imported, or declared in the same file), a local zero-arity arrow getter whose expression body is itself safe (`const yearNow = () => …`, then `props.fromYear ?? yearNow() - 10`), or a built-in read on a known array or `Date` (below). See [`@sinwan-pure` JSDoc contract](#sinwan-pure-jsdoc-contract) below. A getter with a block body, side effects (`() => count++`) or a call back into itself stays eager. Explicit getter and `computed` initializers are unchanged.
|
|
144
|
+
- Built-in reads: `Array.from(...)`, non-mutating array reads (`map`, `filter`, `flatMap`, `flat`, `slice`, `concat`, `join`, `at`, `find`, `findIndex`, `some`, `every`, `reduce`, `includes`, `indexOf`, `entries`, `keys`, `values`) and `Date` getters (`getFullYear`, `getMonth`, `getDate`, `getDay`, `getTime`, `getHours`, `getMinutes`, `getSeconds`, `getMilliseconds`, `valueOf`) promote only on a receiver the compiler knows is an array or `Date`: an array literal, `Array.from(...)`, an array read that returns a new array, a `const` annotated `T[]` / `Array<T>` / `ReadonlyArray<T>` / `Date` or initialized from one of these, or a call to a same-file function with that return type annotation. A callback must be inline (its body is inspected) or a `@sinwan-pure` function; `items.map(track)` with an unannotated `track` stays eager. Reads on other values (`props.items.map(...)`, `props.query.filter(...)`) stay eager because the method may not be the built-in one; annotate a `const` (`const items: Item[] = props.items`) to opt in.
|
|
145
|
+
- A pure method read on a promoted local such as `monthList.map(...)` or `today.getFullYear()` is rewritten to `monthList().map(...)` / `today().getFullYear()`. Mutating reads such as `list.push` and `date.setFullYear` stay snapshots. A promoted local that holds a function is still called: `const close = props.onClose; close()` becomes `close()()`. Module, event-handler, render-callback, loop and nested-block declarations are not promoted.
|
|
146
|
+
- A reactive value passed to a parameter typed `LiveRead<T>` (`T | (() => T)`) is wrapped as `() => value`. An argument that is already a function is left alone. `createLiveState(..., props.selected)` therefore re-reads `props.selected` on each `state.value` access.
|
|
142
147
|
- Bare signal/store containers still pass through unchanged. Bare component prop-bag aliases retain their ordinary JavaScript value semantics. References are resolved by lexical binding, so shadowed locals and object keys are not rewritten.
|
|
143
148
|
- Every ordinary value use is called, including chained initializers, closures, event bodies, shorthand object values and setup `if` tests. A boolean does **not** become an accidentally truthy function. Setup control flow still runs once; use `Show`/`Switch` for reactive branches.
|
|
144
149
|
- Variables used as JSX tag names, in type queries, or as mutation/method-call receivers are left eager. Components containing direct `eval` are excluded.
|
|
@@ -170,12 +175,12 @@ export function cn(...inputs: ClassValue[]): string {
|
|
|
170
175
|
}
|
|
171
176
|
```
|
|
172
177
|
|
|
173
|
-
Both `export function` and `export const` declarations are recognized. The annotation must be a block comment (`/** */`) immediately before the `export` keyword.
|
|
178
|
+
Both `export function` and `export const` declarations are recognized. The annotation must be a block comment (`/** */`) immediately before the `export` keyword. A non-exported function in the same file is also recognized when the comment sits directly above `function` or `const`.
|
|
174
179
|
|
|
175
180
|
**What the compiler guarantees:**
|
|
176
181
|
|
|
177
182
|
- When `derivedLocals: true` is enabled, calls to `@sinwan-pure`-annotated imported functions are treated as safe expressions. The containing `const` initializer is promoted to a zero-argument getter, and references inside the function arguments are rewritten (e.g. `seamInactive` → `seamInactive()`).
|
|
178
|
-
- The compiler resolves the import path, reads the source file, parses it, and checks for `/** @sinwan-pure */` on exported declarations. Results are cached per resolved file path,
|
|
183
|
+
- The compiler resolves the import path, reads the source file, parses it, and checks for `/** @sinwan-pure */` on exported declarations. Results are cached per resolved file path and reused while that file, and any file it re-exports from, keeps its modification time, so a dev server picks up a new or removed annotation without a restart. The same applies to the `LiveRead<T>` parameter scan.
|
|
179
184
|
- All import types are scanned: relative imports (`./`, `../`), bare specifiers (`clsx`, `sinwan/reactivity`), scoped packages (`@scope/utils`), and subpath imports (`@scope/utils/merge`). The compiler walks up the directory tree to find `node_modules/<pkg>`, reads `package.json`, and resolves the entry point via `exports`, `module`, `main`, `source`, or `index` files.
|
|
180
185
|
- If the `resolveImport` callback is provided by the plugin, it is used for path resolution (handles tsconfig paths, bunfig aliases, workspace packages, and Bun/Vite module resolution). Otherwise, the compiler falls back to direct filesystem resolution with common extensions (`.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.mts`, `.cts`) and `index` files.
|
|
181
186
|
|