@codefast/cli 0.3.12 → 0.3.13-canary.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 +206 -51
- package/dist/{bin.js → bin.mjs} +4 -1
- package/dist/commands/arrange.mjs +107 -0
- package/dist/commands/mirror.mjs +66 -0
- package/dist/commands/tag.mjs +58 -0
- package/dist/lib/arrange/application/analyze.mjs +92 -0
- package/dist/lib/arrange/application/group-file.mjs +113 -0
- package/dist/lib/arrange/application/run-target.mjs +65 -0
- package/dist/lib/arrange/domain/ast/ast-helpers.mjs +115 -0
- package/dist/lib/arrange/domain/ast/collectors-cn.mjs +61 -0
- package/dist/lib/arrange/domain/ast/collectors-jsx.mjs +20 -0
- package/dist/lib/arrange/domain/ast/collectors-tv.mjs +170 -0
- package/dist/lib/arrange/domain/ast/targets.mjs +106 -0
- package/dist/lib/arrange/domain/constants.mjs +169 -0
- package/dist/lib/arrange/domain/errors.mjs +19 -0
- package/dist/lib/arrange/domain/grouping.mjs +235 -0
- package/dist/lib/arrange/domain/imports.mjs +54 -0
- package/dist/lib/arrange/domain/tokenizer.mjs +216 -0
- package/dist/lib/arrange/index.mjs +15 -0
- package/dist/lib/arrange/infra/walk.mjs +33 -0
- package/dist/lib/arrange/presentation/formatters.mjs +52 -0
- package/dist/lib/arrange/presentation/report.mjs +24 -0
- package/dist/lib/config/domain/schema.mjs +25 -0
- package/dist/lib/config/index.mjs +3 -0
- package/dist/lib/config/infra/loader.mjs +78 -0
- package/dist/lib/infra/caught-unknown-message.mjs +20 -0
- package/dist/lib/infra/config-reporter.mjs +10 -0
- package/dist/lib/infra/node-io.mjs +30 -0
- package/dist/lib/infra/workspace/repo-root.mjs +22 -0
- package/dist/lib/mirror/application/engine.mjs +248 -0
- package/dist/lib/mirror/application/sync.mjs +162 -0
- package/dist/lib/mirror/domain/constants.mjs +12 -0
- package/dist/lib/mirror/domain/errors.mjs +15 -0
- package/dist/lib/mirror/domain/types.mjs +1 -0
- package/dist/lib/mirror/index.mjs +2 -0
- package/dist/lib/mirror/infra/dirent-list.mjs +14 -0
- package/dist/lib/mirror/infra/package-filter.mjs +26 -0
- package/dist/lib/mirror/infra/path-normalizer.mjs +7 -0
- package/dist/lib/mirror/infra/update-pkg.mjs +105 -0
- package/dist/lib/mirror/infra/workspace-packages.mjs +159 -0
- package/dist/lib/mirror/presentation/reporter.mjs +111 -0
- package/dist/lib/tag/application/engine.mjs +287 -0
- package/dist/lib/tag/domain/types.mjs +1 -0
- package/dist/lib/tag/index.mjs +4 -0
- package/dist/lib/tag/infra/target-resolver.mjs +170 -0
- package/dist/lib/tag/presentation/colors.mjs +12 -0
- package/dist/lib/tag/presentation/tag-presenter.mjs +63 -0
- package/dist/program.mjs +34 -0
- package/package.json +11 -11
- package/dist/bin.d.ts +0 -3
- package/dist/bin.d.ts.map +0 -1
- package/dist/commands/arrange.d.ts +0 -3
- package/dist/commands/arrange.d.ts.map +0 -1
- package/dist/commands/arrange.js +0 -114
- package/dist/commands/mirror.d.ts +0 -4
- package/dist/commands/mirror.d.ts.map +0 -1
- package/dist/commands/mirror.js +0 -63
- package/dist/lib/arrange/analyze.d.ts +0 -4
- package/dist/lib/arrange/analyze.d.ts.map +0 -1
- package/dist/lib/arrange/analyze.js +0 -105
- package/dist/lib/arrange/ast/collectors-cn.d.ts +0 -10
- package/dist/lib/arrange/ast/collectors-cn.d.ts.map +0 -1
- package/dist/lib/arrange/ast/collectors-cn.js +0 -84
- package/dist/lib/arrange/ast/collectors-jsx.d.ts +0 -4
- package/dist/lib/arrange/ast/collectors-jsx.d.ts.map +0 -1
- package/dist/lib/arrange/ast/collectors-jsx.js +0 -18
- package/dist/lib/arrange/ast/collectors-tv.d.ts +0 -13
- package/dist/lib/arrange/ast/collectors-tv.d.ts.map +0 -1
- package/dist/lib/arrange/ast/collectors-tv.js +0 -273
- package/dist/lib/arrange/ast/targets.d.ts +0 -8
- package/dist/lib/arrange/ast/targets.d.ts.map +0 -1
- package/dist/lib/arrange/ast/targets.js +0 -143
- package/dist/lib/arrange/ast/utils.d.ts +0 -36
- package/dist/lib/arrange/ast/utils.d.ts.map +0 -1
- package/dist/lib/arrange/ast/utils.js +0 -138
- package/dist/lib/arrange/constants.d.ts +0 -68
- package/dist/lib/arrange/constants.d.ts.map +0 -1
- package/dist/lib/arrange/constants.js +0 -179
- package/dist/lib/arrange/errors.d.ts +0 -14
- package/dist/lib/arrange/errors.d.ts.map +0 -1
- package/dist/lib/arrange/errors.js +0 -16
- package/dist/lib/arrange/formatters.d.ts +0 -16
- package/dist/lib/arrange/formatters.d.ts.map +0 -1
- package/dist/lib/arrange/formatters.js +0 -57
- package/dist/lib/arrange/group-file.d.ts +0 -4
- package/dist/lib/arrange/group-file.d.ts.map +0 -1
- package/dist/lib/arrange/group-file.js +0 -115
- package/dist/lib/arrange/grouping.d.ts +0 -41
- package/dist/lib/arrange/grouping.d.ts.map +0 -1
- package/dist/lib/arrange/grouping.js +0 -280
- package/dist/lib/arrange/imports.d.ts +0 -6
- package/dist/lib/arrange/imports.d.ts.map +0 -1
- package/dist/lib/arrange/imports.js +0 -74
- package/dist/lib/arrange/report.d.ts +0 -4
- package/dist/lib/arrange/report.d.ts.map +0 -1
- package/dist/lib/arrange/report.js +0 -37
- package/dist/lib/arrange/run-target.d.ts +0 -4
- package/dist/lib/arrange/run-target.d.ts.map +0 -1
- package/dist/lib/arrange/run-target.js +0 -28
- package/dist/lib/arrange/tokenizer.d.ts +0 -37
- package/dist/lib/arrange/tokenizer.d.ts.map +0 -1
- package/dist/lib/arrange/tokenizer.js +0 -390
- package/dist/lib/arrange/types.d.ts +0 -104
- package/dist/lib/arrange/types.d.ts.map +0 -1
- package/dist/lib/arrange/types.js +0 -7
- package/dist/lib/arrange/walk.d.ts +0 -4
- package/dist/lib/arrange/walk.d.ts.map +0 -1
- package/dist/lib/arrange/walk.js +0 -34
- package/dist/lib/arrange.d.ts +0 -41
- package/dist/lib/arrange.d.ts.map +0 -1
- package/dist/lib/arrange.js +0 -38
- package/dist/lib/infra/fs-contract.d.ts +0 -28
- package/dist/lib/infra/fs-contract.d.ts.map +0 -1
- package/dist/lib/infra/node-io.d.ts +0 -4
- package/dist/lib/infra/node-io.d.ts.map +0 -1
- package/dist/lib/infra/node-io.js +0 -27
- package/dist/lib/mirror/config.d.ts +0 -7
- package/dist/lib/mirror/config.d.ts.map +0 -1
- package/dist/lib/mirror/config.js +0 -146
- package/dist/lib/mirror/constants.d.ts +0 -10
- package/dist/lib/mirror/constants.d.ts.map +0 -1
- package/dist/lib/mirror/constants.js +0 -13
- package/dist/lib/mirror/engine.d.ts +0 -12
- package/dist/lib/mirror/engine.d.ts.map +0 -1
- package/dist/lib/mirror/engine.js +0 -248
- package/dist/lib/mirror/errors.d.ts +0 -10
- package/dist/lib/mirror/errors.d.ts.map +0 -1
- package/dist/lib/mirror/errors.js +0 -12
- package/dist/lib/mirror/package-filter.d.ts +0 -8
- package/dist/lib/mirror/package-filter.d.ts.map +0 -1
- package/dist/lib/mirror/package-filter.js +0 -33
- package/dist/lib/mirror/reporter.d.ts +0 -24
- package/dist/lib/mirror/reporter.d.ts.map +0 -1
- package/dist/lib/mirror/reporter.js +0 -126
- package/dist/lib/mirror/sync.d.ts +0 -4
- package/dist/lib/mirror/sync.d.ts.map +0 -1
- package/dist/lib/mirror/sync.js +0 -135
- package/dist/lib/mirror/types.d.ts +0 -81
- package/dist/lib/mirror/types.d.ts.map +0 -1
- package/dist/lib/mirror/update-pkg.d.ts +0 -13
- package/dist/lib/mirror/update-pkg.d.ts.map +0 -1
- package/dist/lib/mirror/update-pkg.js +0 -39
- package/dist/lib/mirror/workspace-packages.d.ts +0 -26
- package/dist/lib/mirror/workspace-packages.d.ts.map +0 -1
- package/dist/lib/mirror/workspace-packages.js +0 -160
- package/dist/lib/mirror.d.ts +0 -7
- package/dist/lib/mirror.d.ts.map +0 -1
- package/dist/lib/mirror.js +0 -5
- package/dist/lib/repo-root.d.ts +0 -7
- package/dist/lib/repo-root.d.ts.map +0 -1
- package/dist/lib/repo-root.js +0 -23
- package/dist/lib/shared/utils.d.ts +0 -9
- package/dist/lib/shared/utils.d.ts.map +0 -1
- package/dist/lib/shared/utils.js +0 -12
- package/dist/program.d.ts +0 -4
- package/dist/program.d.ts.map +0 -1
- package/dist/program.js +0 -38
- /package/dist/lib/{infra/fs-contract.js → arrange/domain/types.mjs} +0 -0
- /package/dist/lib/{mirror/types.js → infra/fs-contract.mjs} +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { applyEditsDescending, buildKnownCnTvBindings, lineOf, unwrapCnInsideTvCallReplacement } from "../domain/ast/ast-helpers.mjs";
|
|
2
|
+
import { listAllCnCallsInsideTvInSourceFile } from "../domain/ast/collectors-tv.mjs";
|
|
3
|
+
import { collectGroupTargets, planGroupEditForTarget, targetReplaceStart } from "../domain/ast/targets.mjs";
|
|
4
|
+
import { ensureCnImport } from "../domain/imports.mjs";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
//#region src/lib/arrange/application/group-file.ts
|
|
7
|
+
function printGroupFilePreview(logger, args) {
|
|
8
|
+
const { out } = logger;
|
|
9
|
+
const { filePath, reportTotal, cnInTvNoReplacement, cnInTvCalls, unwrapReplacementByCall, sourceText, sf, unwrapEdits, plannedGroupEdits } = args;
|
|
10
|
+
let header = `\n── ${filePath} (${reportTotal} site(s)`;
|
|
11
|
+
if (cnInTvNoReplacement > 0) header += `; plus ${cnInTvNoReplacement} cn() inside tv left unchanged (0 args)`;
|
|
12
|
+
header += `) ──`;
|
|
13
|
+
out(header);
|
|
14
|
+
for (const call of cnInTvCalls) {
|
|
15
|
+
const replacement = unwrapReplacementByCall.get(call);
|
|
16
|
+
if (replacement === void 0) {
|
|
17
|
+
out(` Line ${lineOf(sf, call)} [tv ⊃ cn]: cn(...) has no arguments — skipped`);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const start = call.getStart(sf);
|
|
21
|
+
const end = call.getEnd();
|
|
22
|
+
if (sourceText.slice(start, end) === replacement) continue;
|
|
23
|
+
out(` Line ${lineOf(sf, call)} [tv ⊃ cn → string/array]:`);
|
|
24
|
+
out(` ${replacement.split("\n").join("\n ")}`);
|
|
25
|
+
}
|
|
26
|
+
if (unwrapEdits.length > 0 && plannedGroupEdits.length > 0) out(" ([cn] / [tv] / [JSX className] lines below reflect content after unwrap of cn inside tv.)");
|
|
27
|
+
for (const plan of plannedGroupEdits) {
|
|
28
|
+
out(` Line ${lineOf(plan.lineSf, plan.reportNode)} [${plan.label}]:`);
|
|
29
|
+
out(` ${plan.replacement.split("\n").join("\n ")}`);
|
|
30
|
+
out(` // Buckets: ${JSON.stringify(plan.bucketSummary)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function applyGroupFileWrites(filePath, options, fs, logger, unwrapEdits, textAfterUnwrap, plannedGroupEdits, cnInTvNoReplacement) {
|
|
34
|
+
const groupEdits = plannedGroupEdits.map((plannedEdit) => ({
|
|
35
|
+
start: plannedEdit.start,
|
|
36
|
+
end: plannedEdit.end,
|
|
37
|
+
replacement: plannedEdit.replacement,
|
|
38
|
+
jsxCn: plannedEdit.jsxCn
|
|
39
|
+
}));
|
|
40
|
+
const touchedJsxCn = groupEdits.some((groupEdit) => groupEdit.jsxCn);
|
|
41
|
+
let newText = groupEdits.length > 0 ? applyEditsDescending(textAfterUnwrap, groupEdits) : textAfterUnwrap;
|
|
42
|
+
const changed = unwrapEdits.length + groupEdits.length;
|
|
43
|
+
if (changed > 0) {
|
|
44
|
+
if (touchedJsxCn) newText = ensureCnImport(newText, filePath, options.cnImport);
|
|
45
|
+
fs.writeFileSync(filePath, newText, "utf8");
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
filePath,
|
|
49
|
+
totalFound: changed + cnInTvNoReplacement,
|
|
50
|
+
changed
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function groupFile(filePath, options, fs, logger) {
|
|
54
|
+
const sourceText = fs.readFileSync(filePath, "utf8");
|
|
55
|
+
const sf = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
56
|
+
const cnInTvCalls = listAllCnCallsInsideTvInSourceFile(sf, buildKnownCnTvBindings(sf));
|
|
57
|
+
const unwrapPlans = cnInTvCalls.map((call) => {
|
|
58
|
+
const replacement = unwrapCnInsideTvCallReplacement(call, sourceText, sf);
|
|
59
|
+
if (replacement === void 0) return;
|
|
60
|
+
return {
|
|
61
|
+
start: call.getStart(sf),
|
|
62
|
+
end: call.getEnd(),
|
|
63
|
+
replacement,
|
|
64
|
+
call
|
|
65
|
+
};
|
|
66
|
+
}).filter((planOrUndefined) => planOrUndefined !== void 0);
|
|
67
|
+
const unwrapReplacementByCall = new Map(unwrapPlans.map((unwrapPlan) => [unwrapPlan.call, unwrapPlan.replacement]));
|
|
68
|
+
const unwrapEdits = unwrapPlans.filter((unwrapPlan) => sourceText.slice(unwrapPlan.start, unwrapPlan.end) !== unwrapPlan.replacement);
|
|
69
|
+
const textAfterUnwrap = unwrapEdits.length > 0 ? applyEditsDescending(sourceText, unwrapEdits) : sourceText;
|
|
70
|
+
const groupTargets = collectGroupTargets(ts.createSourceFile(filePath, textAfterUnwrap, ts.ScriptTarget.Latest, true, filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS), filePath);
|
|
71
|
+
const cnInTvNoReplacement = cnInTvCalls.length - unwrapPlans.length;
|
|
72
|
+
const sortedTargets = [...groupTargets].sort((leftTarget, rightTarget) => targetReplaceStart(rightTarget) - targetReplaceStart(leftTarget));
|
|
73
|
+
const plannedGroupEdits = [];
|
|
74
|
+
for (const groupTarget of sortedTargets) {
|
|
75
|
+
const plan = planGroupEditForTarget(groupTarget, textAfterUnwrap, options.withClassName);
|
|
76
|
+
if (plan === void 0) continue;
|
|
77
|
+
if (textAfterUnwrap.slice(plan.start, plan.end) === plan.replacement) continue;
|
|
78
|
+
plannedGroupEdits.push(plan);
|
|
79
|
+
}
|
|
80
|
+
const editSitesCount = unwrapEdits.length + plannedGroupEdits.length;
|
|
81
|
+
const reportTotal = editSitesCount + cnInTvNoReplacement;
|
|
82
|
+
if (cnInTvCalls.length === 0 && groupTargets.length === 0) return {
|
|
83
|
+
filePath,
|
|
84
|
+
totalFound: 0,
|
|
85
|
+
changed: 0
|
|
86
|
+
};
|
|
87
|
+
if (!options.write) {
|
|
88
|
+
if (editSitesCount === 0 && cnInTvNoReplacement === 0) return {
|
|
89
|
+
filePath,
|
|
90
|
+
totalFound: 0,
|
|
91
|
+
changed: 0
|
|
92
|
+
};
|
|
93
|
+
printGroupFilePreview(logger, {
|
|
94
|
+
filePath,
|
|
95
|
+
reportTotal,
|
|
96
|
+
cnInTvNoReplacement,
|
|
97
|
+
cnInTvCalls,
|
|
98
|
+
unwrapReplacementByCall,
|
|
99
|
+
sourceText,
|
|
100
|
+
sf,
|
|
101
|
+
unwrapEdits,
|
|
102
|
+
plannedGroupEdits
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
filePath,
|
|
106
|
+
totalFound: reportTotal,
|
|
107
|
+
changed: 0
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return applyGroupFileWrites(filePath, options, fs, logger, unwrapEdits, textAfterUnwrap, plannedGroupEdits, cnInTvNoReplacement);
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
export { groupFile };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { messageFromCaughtUnknown } from "../../infra/caught-unknown-message.mjs";
|
|
2
|
+
import { walkTsxFiles } from "../infra/walk.mjs";
|
|
3
|
+
import { groupFile } from "./group-file.mjs";
|
|
4
|
+
import { ArrangeError, ArrangeErrorCode } from "../domain/errors.mjs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
//#region src/lib/arrange/application/run-target.ts
|
|
7
|
+
function runOnTarget(target, options, fs, logger) {
|
|
8
|
+
const { out } = logger;
|
|
9
|
+
if (!fs.existsSync(target)) throw new ArrangeError(ArrangeErrorCode.TARGET_NOT_FOUND, `Not found: ${target}`);
|
|
10
|
+
const filePaths = fs.statSync(target).isDirectory() ? walkTsxFiles(target, fs) : [target];
|
|
11
|
+
const modifiedFiles = [];
|
|
12
|
+
let totalFound = 0;
|
|
13
|
+
let totalChanged = 0;
|
|
14
|
+
for (const filePath of filePaths) {
|
|
15
|
+
const result = groupFile(filePath, options, fs, logger);
|
|
16
|
+
totalFound += result.totalFound;
|
|
17
|
+
totalChanged += result.changed;
|
|
18
|
+
if (result.changed > 0) modifiedFiles.push(result.filePath);
|
|
19
|
+
}
|
|
20
|
+
out(`\nTotal: ${filePaths.length} file(s), ${totalFound} site(s) (cn/tv/JSX className) to review.`);
|
|
21
|
+
if (options.write) out(`Applied: ${totalChanged} site(s) updated.`);
|
|
22
|
+
else out(`(Run "apply" to write changes, or "pnpm cli:arrange-apply" / "pnpm exec codefast arrange apply")`);
|
|
23
|
+
if (options.write ? totalChanged > 0 : totalFound > 0) out("Note: class order may change across concern groups — smoke-test the UI if you rely on cascade order.");
|
|
24
|
+
return {
|
|
25
|
+
filePaths,
|
|
26
|
+
modifiedFiles,
|
|
27
|
+
totalFound,
|
|
28
|
+
totalChanged
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function runArrangeOnAfterWriteHook(logger, hook, modifiedFiles) {
|
|
32
|
+
if (!hook || modifiedFiles.length === 0) return;
|
|
33
|
+
try {
|
|
34
|
+
await hook({ files: modifiedFiles });
|
|
35
|
+
} catch (caughtHookError) {
|
|
36
|
+
logger.err(`[arrange] onAfterWrite hook failed: ${messageFromCaughtUnknown(caughtHookError)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* CLI entry: run arrange preview/apply using config injected by the command layer.
|
|
41
|
+
* @returns Process exit code (`0`, or `1` when the target path is missing).
|
|
42
|
+
*/
|
|
43
|
+
async function runArrangeSync(opts) {
|
|
44
|
+
opts.rootDir;
|
|
45
|
+
const { fs, logger } = opts;
|
|
46
|
+
const resolvedTarget = path.resolve(opts.targetPath);
|
|
47
|
+
let result;
|
|
48
|
+
try {
|
|
49
|
+
result = runOnTarget(resolvedTarget, {
|
|
50
|
+
write: opts.write,
|
|
51
|
+
withClassName: !!opts.withClassName,
|
|
52
|
+
cnImport: opts.cnImport
|
|
53
|
+
}, fs, logger);
|
|
54
|
+
} catch (caughtError) {
|
|
55
|
+
if (caughtError instanceof ArrangeError && caughtError.code === ArrangeErrorCode.TARGET_NOT_FOUND) {
|
|
56
|
+
logger.err(caughtError.message);
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
throw caughtError;
|
|
60
|
+
}
|
|
61
|
+
if (opts.write && result.modifiedFiles.length > 0) await runArrangeOnAfterWriteHook(logger, opts.config?.onAfterWrite, result.modifiedFiles);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { runArrangeSync, runOnTarget };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { EMPTY_CN_TV_BINDINGS } from "../constants.mjs";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
//#region src/lib/arrange/domain/ast/ast-helpers.ts
|
|
4
|
+
/** Known module specifiers that export `cn` / `tv`. */
|
|
5
|
+
const KNOWN_CN_TV_MODULES = new Set([
|
|
6
|
+
"@codefast/tailwind-variants",
|
|
7
|
+
"clsx",
|
|
8
|
+
"class-variance-authority",
|
|
9
|
+
"tailwind-variants",
|
|
10
|
+
"#lib/utils",
|
|
11
|
+
"~/lib/utils",
|
|
12
|
+
"@/lib/utils"
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* True for typical shadcn-style re-exports: `./utils`, `@/lib/utils`, `…/utils/…`,
|
|
16
|
+
* or a dedicated `cn.ts` / `cn.tsx` module.
|
|
17
|
+
*/
|
|
18
|
+
function moduleLooksLikeCnTvReexport(moduleSpecifier) {
|
|
19
|
+
const norm = moduleSpecifier.replace(/\\/g, "/");
|
|
20
|
+
if (/(?:^|[./])utils(?:\/|$)/.test(norm)) return true;
|
|
21
|
+
if (/\/utils\//.test(norm) || /\/utils$/.test(norm)) return true;
|
|
22
|
+
if (/(?:^|\/)cn\.tsx?$/.test(norm)) return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build a set of local binding names that are imported from a known cn/tv
|
|
27
|
+
* module in `sf`.
|
|
28
|
+
*/
|
|
29
|
+
function buildKnownCnTvBindings(sf) {
|
|
30
|
+
const bindings = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const stmt of sf.statements) {
|
|
32
|
+
if (!ts.isImportDeclaration(stmt) || !stmt.importClause) continue;
|
|
33
|
+
if (stmt.importClause.isTypeOnly) continue;
|
|
34
|
+
const spec = stmt.moduleSpecifier;
|
|
35
|
+
if (!ts.isStringLiteral(spec)) continue;
|
|
36
|
+
const moduleSpecifier = spec.text;
|
|
37
|
+
if (!KNOWN_CN_TV_MODULES.has(moduleSpecifier) && !moduleLooksLikeCnTvReexport(moduleSpecifier)) continue;
|
|
38
|
+
const clause = stmt.importClause;
|
|
39
|
+
if (clause.name) bindings.add(clause.name.text);
|
|
40
|
+
if (!clause.namedBindings) continue;
|
|
41
|
+
if (ts.isNamedImports(clause.namedBindings)) {
|
|
42
|
+
for (const namedImport of clause.namedBindings.elements) bindings.add(namedImport.name.text);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
bindings.add(clause.namedBindings.name.text);
|
|
46
|
+
}
|
|
47
|
+
return bindings;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns true when `expr` is an identifier whose name matches `name` AND
|
|
51
|
+
* that name is listed in `knownBindings` from a recognized import.
|
|
52
|
+
*/
|
|
53
|
+
function isCnOrTvIdentifier(expr, name, knownBindings = EMPTY_CN_TV_BINDINGS) {
|
|
54
|
+
if (knownBindings.size === 0) return false;
|
|
55
|
+
if (ts.isIdentifier(expr) && expr.text === name) return knownBindings.has(expr.text);
|
|
56
|
+
if (ts.isPropertyAccessExpression(expr) && expr.name.text === name) return ts.isIdentifier(expr.expression) && knownBindings.has(expr.expression.text);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
function propertyAssignmentNameText(prop) {
|
|
60
|
+
if (ts.isIdentifier(prop.name)) return prop.name.text;
|
|
61
|
+
if (ts.isStringLiteral(prop.name)) return prop.name.text;
|
|
62
|
+
}
|
|
63
|
+
function lineOf(sf, tsNode) {
|
|
64
|
+
return sf.getLineAndCharacterOfPosition(tsNode.getStart(sf)).line + 1;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns only the leading whitespace (tabs / spaces) of the line containing
|
|
68
|
+
* `pos`.
|
|
69
|
+
*/
|
|
70
|
+
function indentOfLineContaining(source, pos) {
|
|
71
|
+
const searchPos = Math.max(0, Math.min(pos, source.length));
|
|
72
|
+
const prevLineBreak = Math.max(source.lastIndexOf("\n", searchPos - 1), source.lastIndexOf("\r", searchPos - 1));
|
|
73
|
+
const lineStart = prevLineBreak === -1 ? 0 : prevLineBreak + 1;
|
|
74
|
+
let lineEnd = source.length;
|
|
75
|
+
for (let i = lineStart; i < source.length; i++) {
|
|
76
|
+
const ch = source[i];
|
|
77
|
+
if (ch === "\n" || ch === "\r") {
|
|
78
|
+
lineEnd = i;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const line = source.slice(lineStart, lineEnd);
|
|
83
|
+
return /^[\t ]*/.exec(line)?.[0] ?? "";
|
|
84
|
+
}
|
|
85
|
+
function applyEditsDescending(sourceText, edits) {
|
|
86
|
+
const sorted = [...edits].sort((editA, editB) => editB.start - editA.start);
|
|
87
|
+
let out = sourceText;
|
|
88
|
+
for (const edit of sorted) out = out.slice(0, edit.start) + edit.replacement + out.slice(edit.end);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Replace `cn(...)` nested in `tv({...})` with a plain string (one arg) or a string
|
|
93
|
+
* array (multiple args). Returns undefined when there are zero arguments.
|
|
94
|
+
*/
|
|
95
|
+
function unwrapCnInsideTvCallReplacement(call, sourceText, sf) {
|
|
96
|
+
const args = call.arguments;
|
|
97
|
+
if (args.length === 0) return;
|
|
98
|
+
const baseIndent = indentOfLineContaining(sourceText, call.getStart(sf));
|
|
99
|
+
const innerIndent = `${baseIndent} `;
|
|
100
|
+
if (args.length === 1) {
|
|
101
|
+
const firstArg = args[0];
|
|
102
|
+
return sourceText.slice(firstArg.getStart(sf), firstArg.getEnd());
|
|
103
|
+
}
|
|
104
|
+
const lines = ["["];
|
|
105
|
+
for (let i = 0; i < args.length; i++) {
|
|
106
|
+
const arg = args[i];
|
|
107
|
+
const piece = sourceText.slice(arg.getStart(sf), arg.getEnd());
|
|
108
|
+
const comma = i < args.length - 1 || args.length > 1 ? "," : "";
|
|
109
|
+
lines.push(`${innerIndent}${piece}${comma}`);
|
|
110
|
+
}
|
|
111
|
+
lines.push(`${baseIndent}]`);
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
export { KNOWN_CN_TV_MODULES, applyEditsDescending, buildKnownCnTvBindings, indentOfLineContaining, isCnOrTvIdentifier, lineOf, moduleLooksLikeCnTvReexport, propertyAssignmentNameText, unwrapCnInsideTvCallReplacement };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import "../constants.mjs";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
//#region src/lib/arrange/domain/ast/collectors-cn.ts
|
|
4
|
+
function forEachStringLiteralInClassExpression(expr, sink, depth = 0, options) {
|
|
5
|
+
if (depth > 12) return;
|
|
6
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
|
|
7
|
+
sink(expr);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (ts.isParenthesizedExpression(expr)) {
|
|
11
|
+
forEachStringLiteralInClassExpression(expr.expression, sink, depth + 1, options);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (ts.isAsExpression(expr) || ts.isSatisfiesExpression(expr)) {
|
|
15
|
+
forEachStringLiteralInClassExpression(expr.expression, sink, depth + 1, options);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (ts.isNonNullExpression(expr)) {
|
|
19
|
+
forEachStringLiteralInClassExpression(expr.expression, sink, depth + 1, options);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (ts.isConditionalExpression(expr)) {
|
|
23
|
+
if (options?.descendIntoConditional === false) return;
|
|
24
|
+
forEachStringLiteralInClassExpression(expr.whenTrue, sink, depth + 1, options);
|
|
25
|
+
forEachStringLiteralInClassExpression(expr.whenFalse, sink, depth + 1, options);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
29
|
+
forEachStringLiteralInClassExpression(expr.left, sink, depth + 1, options);
|
|
30
|
+
forEachStringLiteralInClassExpression(expr.right, sink, depth + 1, options);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (ts.isArrayLiteralExpression(expr)) for (const arrayElement of expr.elements) {
|
|
34
|
+
if (ts.isSpreadElement(arrayElement)) continue;
|
|
35
|
+
forEachStringLiteralInClassExpression(arrayElement, sink, depth + 1, options);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function isUnsafeLiteralForCnStyleApplySplit(classLiteral) {
|
|
39
|
+
return ts.isArrayLiteralExpression(classLiteral.parent);
|
|
40
|
+
}
|
|
41
|
+
const CN_APPLY_LITERAL_WALK_OPTS = { descendIntoConditional: false };
|
|
42
|
+
function collectUnconditionalTailwindLiteralsFromCnArguments(args) {
|
|
43
|
+
const staticLits = [];
|
|
44
|
+
for (const arg of args) if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
|
|
45
|
+
if (!isUnsafeLiteralForCnStyleApplySplit(arg)) staticLits.push(arg);
|
|
46
|
+
} else forEachStringLiteralInClassExpression(arg, (lit) => {
|
|
47
|
+
if (!isUnsafeLiteralForCnStyleApplySplit(lit)) staticLits.push(lit);
|
|
48
|
+
}, 0, CN_APPLY_LITERAL_WALK_OPTS);
|
|
49
|
+
return staticLits;
|
|
50
|
+
}
|
|
51
|
+
function mergeCnUnconditionalLiteralPoolForTest(argsSnippet, options) {
|
|
52
|
+
const callee = options?.callee ?? "cn";
|
|
53
|
+
const sourceText = `${callee}(${argsSnippet});`;
|
|
54
|
+
const stmt = ts.createSourceFile("mergeCnUnconditionalLiteralPoolForTest.ts", sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS).statements[0];
|
|
55
|
+
if (!ts.isExpressionStatement(stmt)) throw new Error("expected expression statement");
|
|
56
|
+
const call = stmt.expression;
|
|
57
|
+
if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression) || call.expression.text !== callee) throw new Error(`expected ${callee}(...) call`);
|
|
58
|
+
return collectUnconditionalTailwindLiteralsFromCnArguments(call.arguments).map((literal) => literal.text).join(" ");
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { CN_APPLY_LITERAL_WALK_OPTS, collectUnconditionalTailwindLiteralsFromCnArguments, forEachStringLiteralInClassExpression, isUnsafeLiteralForCnStyleApplySplit, mergeCnUnconditionalLiteralPoolForTest };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
//#region src/lib/arrange/domain/ast/collectors-jsx.ts
|
|
3
|
+
function jsxClassNameStaticLiteral(jsxClassNameAttribute) {
|
|
4
|
+
if (!ts.isIdentifier(jsxClassNameAttribute.name) || jsxClassNameAttribute.name.text !== "className") return;
|
|
5
|
+
const init = jsxClassNameAttribute.initializer;
|
|
6
|
+
if (!init) return;
|
|
7
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) return {
|
|
8
|
+
lit: init,
|
|
9
|
+
valueNode: init
|
|
10
|
+
};
|
|
11
|
+
if (ts.isJsxExpression(init) && init.expression) {
|
|
12
|
+
const jsxExpressionBody = init.expression;
|
|
13
|
+
if (ts.isStringLiteral(jsxExpressionBody) || ts.isNoSubstitutionTemplateLiteral(jsxExpressionBody)) return {
|
|
14
|
+
lit: jsxExpressionBody,
|
|
15
|
+
valueNode: init
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { jsxClassNameStaticLiteral };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import "../constants.mjs";
|
|
2
|
+
import { tokenizeClassString } from "../tokenizer.mjs";
|
|
3
|
+
import { CN_APPLY_LITERAL_WALK_OPTS, collectUnconditionalTailwindLiteralsFromCnArguments, forEachStringLiteralInClassExpression, isUnsafeLiteralForCnStyleApplySplit } from "./collectors-cn.mjs";
|
|
4
|
+
import { buildKnownCnTvBindings, isCnOrTvIdentifier, propertyAssignmentNameText } from "./ast-helpers.mjs";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
//#region src/lib/arrange/domain/ast/collectors-tv.ts
|
|
7
|
+
function traverseTvObject(sf, obj, visitor, depth = 0, knownBindings) {
|
|
8
|
+
if (depth > 12) return;
|
|
9
|
+
for (const prop of obj.properties) {
|
|
10
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
11
|
+
const init = prop.initializer;
|
|
12
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) visitor(init, sf, void 0);
|
|
13
|
+
else if (ts.isArrayLiteralExpression(init)) for (const arrayElement of init.elements) {
|
|
14
|
+
if (ts.isSpreadElement(arrayElement)) continue;
|
|
15
|
+
if (ts.isStringLiteral(arrayElement) || ts.isNoSubstitutionTemplateLiteral(arrayElement)) visitor(arrayElement, sf, void 0);
|
|
16
|
+
else if (ts.isCallExpression(arrayElement) && isCnOrTvIdentifier(arrayElement.expression, "cn", knownBindings)) for (const arg of arrayElement.arguments) forEachStringLiteralInClassExpression(arg, (lit) => {
|
|
17
|
+
visitor(lit, sf, arrayElement);
|
|
18
|
+
});
|
|
19
|
+
else if (ts.isObjectLiteralExpression(arrayElement)) for (const objectProperty of arrayElement.properties) {
|
|
20
|
+
if (!ts.isPropertyAssignment(objectProperty)) continue;
|
|
21
|
+
const propName = propertyAssignmentNameText(objectProperty);
|
|
22
|
+
if (propName !== "className" && propName !== "class") continue;
|
|
23
|
+
const innerInit = objectProperty.initializer;
|
|
24
|
+
if (ts.isStringLiteral(innerInit) || ts.isNoSubstitutionTemplateLiteral(innerInit)) visitor(innerInit, sf, void 0);
|
|
25
|
+
else if (ts.isArrayLiteralExpression(innerInit)) for (const nestedArrayElement of innerInit.elements) {
|
|
26
|
+
if (ts.isSpreadElement(nestedArrayElement)) continue;
|
|
27
|
+
if (ts.isStringLiteral(nestedArrayElement) || ts.isNoSubstitutionTemplateLiteral(nestedArrayElement)) visitor(nestedArrayElement, sf, void 0);
|
|
28
|
+
}
|
|
29
|
+
else if (ts.isCallExpression(innerInit) && isCnOrTvIdentifier(innerInit.expression, "cn", knownBindings)) for (const arg of innerInit.arguments) forEachStringLiteralInClassExpression(arg, (lit) => {
|
|
30
|
+
visitor(lit, sf, innerInit);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (ts.isObjectLiteralExpression(init)) traverseTvObject(sf, init, visitor, depth + 1, knownBindings);
|
|
35
|
+
else if (ts.isCallExpression(init) && isCnOrTvIdentifier(init.expression, "cn", knownBindings)) for (const arg of init.arguments) forEachStringLiteralInClassExpression(arg, (lit) => {
|
|
36
|
+
visitor(lit, sf, init);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function collectCnCallsInsideTv(sf, obj, knownBindings, depth = 0) {
|
|
41
|
+
if (depth > 12) return [];
|
|
42
|
+
const calls = [];
|
|
43
|
+
for (const prop of obj.properties) {
|
|
44
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
45
|
+
const init = prop.initializer;
|
|
46
|
+
if (ts.isArrayLiteralExpression(init)) for (const arrayElement of init.elements) {
|
|
47
|
+
if (ts.isSpreadElement(arrayElement)) continue;
|
|
48
|
+
if (ts.isCallExpression(arrayElement) && isCnOrTvIdentifier(arrayElement.expression, "cn", knownBindings)) calls.push(arrayElement);
|
|
49
|
+
else if (ts.isObjectLiteralExpression(arrayElement)) for (const objectProperty of arrayElement.properties) {
|
|
50
|
+
if (!ts.isPropertyAssignment(objectProperty)) continue;
|
|
51
|
+
const propName = propertyAssignmentNameText(objectProperty);
|
|
52
|
+
if (propName !== "className" && propName !== "class") continue;
|
|
53
|
+
const innerInit = objectProperty.initializer;
|
|
54
|
+
if (ts.isCallExpression(innerInit) && isCnOrTvIdentifier(innerInit.expression, "cn", knownBindings)) calls.push(innerInit);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (ts.isObjectLiteralExpression(init)) calls.push(...collectCnCallsInsideTv(sf, init, knownBindings, depth + 1));
|
|
58
|
+
else if (ts.isCallExpression(init) && isCnOrTvIdentifier(init.expression, "cn", knownBindings)) calls.push(init);
|
|
59
|
+
}
|
|
60
|
+
return calls;
|
|
61
|
+
}
|
|
62
|
+
function listAllCnCallsInsideTvInSourceFile(sf, knownBindings) {
|
|
63
|
+
const calls = [];
|
|
64
|
+
const visitTypeScriptSubtree = (tsNode) => {
|
|
65
|
+
if (ts.isCallExpression(tsNode) && isCnOrTvIdentifier(tsNode.expression, "tv", knownBindings)) {
|
|
66
|
+
const arg0 = tsNode.arguments[0];
|
|
67
|
+
if (arg0 && ts.isObjectLiteralExpression(arg0)) calls.push(...collectCnCallsInsideTv(sf, arg0, knownBindings, 0));
|
|
68
|
+
}
|
|
69
|
+
ts.forEachChild(tsNode, visitTypeScriptSubtree);
|
|
70
|
+
};
|
|
71
|
+
visitTypeScriptSubtree(sf);
|
|
72
|
+
return calls;
|
|
73
|
+
}
|
|
74
|
+
function makeStringNode(nodes, sf, isTvContext, cnCall) {
|
|
75
|
+
return {
|
|
76
|
+
nodes,
|
|
77
|
+
sf,
|
|
78
|
+
isTvContext,
|
|
79
|
+
cnCall,
|
|
80
|
+
get primaryClassLiteral() {
|
|
81
|
+
return this.nodes[0];
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function slotClassString(stringNode) {
|
|
86
|
+
return stringNode.nodes.map((literal) => literal.text).join(" ");
|
|
87
|
+
}
|
|
88
|
+
function emitTvSlot(lits, sf, cnCall, results, seenNodePos) {
|
|
89
|
+
if (lits.length === 0) return;
|
|
90
|
+
const firstPos = lits[0].getStart(sf);
|
|
91
|
+
if (seenNodePos.has(firstPos)) return;
|
|
92
|
+
seenNodePos.add(firstPos);
|
|
93
|
+
if (lits.reduce((accumulatedTokenCount, literal) => accumulatedTokenCount + tokenizeClassString(literal.text).length, 0) < 2) return;
|
|
94
|
+
results.push(makeStringNode(lits, sf, true, cnCall));
|
|
95
|
+
}
|
|
96
|
+
function collectTvSlots(sf, obj, knownBindings, results, seenNodePos, depth = 0) {
|
|
97
|
+
if (depth > 12) return;
|
|
98
|
+
for (const prop of obj.properties) {
|
|
99
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
100
|
+
const init = prop.initializer;
|
|
101
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) emitTvSlot([init], sf, void 0, results, seenNodePos);
|
|
102
|
+
else if (ts.isArrayLiteralExpression(init)) {
|
|
103
|
+
const arrayPos = init.getStart(sf);
|
|
104
|
+
if (seenNodePos.has(arrayPos)) continue;
|
|
105
|
+
seenNodePos.add(arrayPos);
|
|
106
|
+
const staticLits = [];
|
|
107
|
+
for (const arrayElement of init.elements) {
|
|
108
|
+
if (ts.isSpreadElement(arrayElement)) continue;
|
|
109
|
+
if (ts.isStringLiteral(arrayElement) || ts.isNoSubstitutionTemplateLiteral(arrayElement)) staticLits.push(arrayElement);
|
|
110
|
+
else if (ts.isCallExpression(arrayElement) && isCnOrTvIdentifier(arrayElement.expression, "cn", knownBindings)) for (const arg of arrayElement.arguments) forEachStringLiteralInClassExpression(arg, (lit) => {
|
|
111
|
+
if (!isUnsafeLiteralForCnStyleApplySplit(lit)) staticLits.push(lit);
|
|
112
|
+
}, 0, CN_APPLY_LITERAL_WALK_OPTS);
|
|
113
|
+
else if (ts.isObjectLiteralExpression(arrayElement)) for (const objectProperty of arrayElement.properties) {
|
|
114
|
+
if (!ts.isPropertyAssignment(objectProperty)) continue;
|
|
115
|
+
const propName = propertyAssignmentNameText(objectProperty);
|
|
116
|
+
if (propName !== "className" && propName !== "class") continue;
|
|
117
|
+
const innerInit = objectProperty.initializer;
|
|
118
|
+
if (ts.isStringLiteral(innerInit) || ts.isNoSubstitutionTemplateLiteral(innerInit)) emitTvSlot([innerInit], sf, void 0, results, seenNodePos);
|
|
119
|
+
else if (ts.isArrayLiteralExpression(innerInit)) {
|
|
120
|
+
const innerLits = [];
|
|
121
|
+
for (const nestedArrayElement of innerInit.elements) if (!ts.isSpreadElement(nestedArrayElement) && (ts.isStringLiteral(nestedArrayElement) || ts.isNoSubstitutionTemplateLiteral(nestedArrayElement))) innerLits.push(nestedArrayElement);
|
|
122
|
+
emitTvSlot(innerLits, sf, void 0, results, seenNodePos);
|
|
123
|
+
} else if (ts.isCallExpression(innerInit) && isCnOrTvIdentifier(innerInit.expression, "cn", knownBindings)) {
|
|
124
|
+
const cnLits = [];
|
|
125
|
+
for (const arg of innerInit.arguments) forEachStringLiteralInClassExpression(arg, (lit) => {
|
|
126
|
+
if (!isUnsafeLiteralForCnStyleApplySplit(lit)) cnLits.push(lit);
|
|
127
|
+
}, 0, CN_APPLY_LITERAL_WALK_OPTS);
|
|
128
|
+
emitTvSlot(cnLits, sf, innerInit, results, seenNodePos);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (staticLits.length > 0) emitTvSlot(staticLits, sf, void 0, results, seenNodePos);
|
|
133
|
+
} else if (ts.isObjectLiteralExpression(init)) collectTvSlots(sf, init, knownBindings, results, seenNodePos, depth + 1);
|
|
134
|
+
else if (ts.isCallExpression(init) && isCnOrTvIdentifier(init.expression, "cn", knownBindings)) {
|
|
135
|
+
const cnLits = [];
|
|
136
|
+
for (const arg of init.arguments) forEachStringLiteralInClassExpression(arg, (lit) => {
|
|
137
|
+
if (!isUnsafeLiteralForCnStyleApplySplit(lit)) cnLits.push(lit);
|
|
138
|
+
}, 0, CN_APPLY_LITERAL_WALK_OPTS);
|
|
139
|
+
emitTvSlot(cnLits, sf, init, results, seenNodePos);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function collectGroupableStringNodes(sf) {
|
|
144
|
+
const results = [];
|
|
145
|
+
const seenNodePos = /* @__PURE__ */ new Set();
|
|
146
|
+
const knownBindings = buildKnownCnTvBindings(sf);
|
|
147
|
+
const visitTypeScriptSubtree = (tsNode) => {
|
|
148
|
+
if (ts.isCallExpression(tsNode)) {
|
|
149
|
+
if (isCnOrTvIdentifier(tsNode.expression, "cn", knownBindings)) {
|
|
150
|
+
const callPos = tsNode.getStart(sf);
|
|
151
|
+
if (seenNodePos.has(callPos)) {
|
|
152
|
+
ts.forEachChild(tsNode, visitTypeScriptSubtree);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
seenNodePos.add(callPos);
|
|
156
|
+
const staticLits = collectUnconditionalTailwindLiteralsFromCnArguments(tsNode.arguments);
|
|
157
|
+
const totalTokens = staticLits.reduce((accumulatedTokenCount, literal) => accumulatedTokenCount + tokenizeClassString(literal.text).length, 0);
|
|
158
|
+
if (staticLits.length > 0 && totalTokens >= 2) results.push(makeStringNode(staticLits, sf, false, tsNode));
|
|
159
|
+
} else if (isCnOrTvIdentifier(tsNode.expression, "tv", knownBindings)) {
|
|
160
|
+
const arg0 = tsNode.arguments[0];
|
|
161
|
+
if (arg0 && ts.isObjectLiteralExpression(arg0)) collectTvSlots(sf, arg0, knownBindings, results, seenNodePos);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
ts.forEachChild(tsNode, visitTypeScriptSubtree);
|
|
165
|
+
};
|
|
166
|
+
visitTypeScriptSubtree(sf);
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
export { collectCnCallsInsideTv, collectGroupableStringNodes, collectTvSlots, emitTvSlot, listAllCnCallsInsideTvInSourceFile, makeStringNode, slotClassString, traverseTvObject };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import "../constants.mjs";
|
|
2
|
+
import { tokenizeClassString } from "../tokenizer.mjs";
|
|
3
|
+
import { isUnsafeLiteralForCnStyleApplySplit } from "./collectors-cn.mjs";
|
|
4
|
+
import { jsxClassNameStaticLiteral } from "./collectors-jsx.mjs";
|
|
5
|
+
import { indentOfLineContaining } from "./ast-helpers.mjs";
|
|
6
|
+
import { collectGroupableStringNodes, slotClassString } from "./collectors-tv.mjs";
|
|
7
|
+
import { areCnTailwindPartitionsEquivalent, suggestCnGroups, summarizeGroupBucketLabels } from "../grouping.mjs";
|
|
8
|
+
import { escapeTsStringLiteralContent, formatArray, formatJsxCnAttributeValue } from "../../presentation/formatters.mjs";
|
|
9
|
+
import ts from "typescript";
|
|
10
|
+
//#region src/lib/arrange/domain/ast/targets.ts
|
|
11
|
+
function targetReplaceStart(target) {
|
|
12
|
+
if (target.kind === "cnArg") return target.item.cnCall ? target.item.cnCall.getStart(target.item.sf) : target.item.primaryClassLiteral.getStart(target.item.sf);
|
|
13
|
+
return target.valueNode.getStart(target.sf);
|
|
14
|
+
}
|
|
15
|
+
function collectLongJsxClassNameTargets(sf) {
|
|
16
|
+
const results = [];
|
|
17
|
+
const visitTypeScriptSubtree = (tsNode) => {
|
|
18
|
+
if (ts.isJsxAttribute(tsNode)) {
|
|
19
|
+
const parsed = jsxClassNameStaticLiteral(tsNode);
|
|
20
|
+
if (parsed && tokenizeClassString(parsed.lit.text).length >= 2) results.push({
|
|
21
|
+
kind: "jsxClassName",
|
|
22
|
+
sf,
|
|
23
|
+
lit: parsed.lit,
|
|
24
|
+
valueNode: parsed.valueNode
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
ts.forEachChild(tsNode, visitTypeScriptSubtree);
|
|
28
|
+
};
|
|
29
|
+
visitTypeScriptSubtree(sf);
|
|
30
|
+
return results;
|
|
31
|
+
}
|
|
32
|
+
function collectGroupTargets(sf, filePath) {
|
|
33
|
+
const cnPart = collectGroupableStringNodes(sf).map((stringNode) => ({
|
|
34
|
+
kind: "cnArg",
|
|
35
|
+
item: stringNode
|
|
36
|
+
}));
|
|
37
|
+
if (!filePath.endsWith(".tsx")) return cnPart;
|
|
38
|
+
return [...cnPart, ...collectLongJsxClassNameTargets(sf)];
|
|
39
|
+
}
|
|
40
|
+
function formatCnCallReplacement(stringNode, sourceText, withClassName) {
|
|
41
|
+
const call = stringNode.cnCall;
|
|
42
|
+
const sf = stringNode.sf;
|
|
43
|
+
const baseIndent = indentOfLineContaining(sourceText, call.getStart(sf));
|
|
44
|
+
const argIndent = `${baseIndent} `;
|
|
45
|
+
const dynamicArgTexts = [];
|
|
46
|
+
for (const arg of call.arguments) if (!((ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) && !isUnsafeLiteralForCnStyleApplySplit(arg))) dynamicArgTexts.push(sourceText.slice(arg.getStart(sf), arg.getEnd()));
|
|
47
|
+
const pool = slotClassString(stringNode);
|
|
48
|
+
const groups = pool.trim() ? suggestCnGroups(pool) : [];
|
|
49
|
+
const allArgs = [];
|
|
50
|
+
if (groups.length > 1) for (const group of groups) allArgs.push(`${argIndent}"${escapeTsStringLiteralContent(group)}"`);
|
|
51
|
+
else if (groups.length === 1) allArgs.push(`${argIndent}"${escapeTsStringLiteralContent(groups[0])}"`);
|
|
52
|
+
for (const dynamicArgumentSource of dynamicArgTexts) allArgs.push(`${argIndent}${dynamicArgumentSource}`);
|
|
53
|
+
if (withClassName) allArgs.push(`${argIndent}className`);
|
|
54
|
+
const commaAfterLastArg = allArgs.length > 1;
|
|
55
|
+
return `cn(\n${allArgs.map((argLine, lineIndex) => lineIndex < allArgs.length - 1 || commaAfterLastArg ? `${argLine},` : `${argLine}`).join("\n")}\n${baseIndent})`;
|
|
56
|
+
}
|
|
57
|
+
function planGroupEditForTarget(target, textAfterUnwrap, withClassName) {
|
|
58
|
+
if (target.kind === "jsxClassName") {
|
|
59
|
+
const groups = suggestCnGroups(target.lit.text);
|
|
60
|
+
if (groups.length <= 1) return;
|
|
61
|
+
const start = target.valueNode.getStart(target.sf);
|
|
62
|
+
return {
|
|
63
|
+
start,
|
|
64
|
+
end: target.valueNode.getEnd(),
|
|
65
|
+
replacement: formatJsxCnAttributeValue(groups, textAfterUnwrap, start),
|
|
66
|
+
bucketSummary: summarizeGroupBucketLabels(groups),
|
|
67
|
+
jsxCn: true,
|
|
68
|
+
lineSf: target.sf,
|
|
69
|
+
reportNode: target.lit,
|
|
70
|
+
label: "JSX className"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const groups = suggestCnGroups(slotClassString(target.item));
|
|
74
|
+
if (groups.length <= 1) return;
|
|
75
|
+
if (areCnTailwindPartitionsEquivalent(target.item.nodes.map((classLiteral) => classLiteral.text), groups)) return;
|
|
76
|
+
if (!target.item.cnCall) {
|
|
77
|
+
const anchorClassLiteral = target.item.primaryClassLiteral;
|
|
78
|
+
const parentArray = target.item.nodes.length > 1 && ts.isArrayLiteralExpression(anchorClassLiteral.parent) ? anchorClassLiteral.parent : null;
|
|
79
|
+
const start = parentArray ? parentArray.getStart(target.item.sf) : anchorClassLiteral.getStart(target.item.sf);
|
|
80
|
+
const end = parentArray ? parentArray.getEnd() : anchorClassLiteral.getEnd();
|
|
81
|
+
const baseIndent = indentOfLineContaining(textAfterUnwrap, start);
|
|
82
|
+
return {
|
|
83
|
+
start,
|
|
84
|
+
end,
|
|
85
|
+
replacement: formatArray(groups).split("\n").map((line, lineIndex) => lineIndex === 0 ? line : `${baseIndent}${line}`).join("\n"),
|
|
86
|
+
bucketSummary: summarizeGroupBucketLabels(groups),
|
|
87
|
+
jsxCn: false,
|
|
88
|
+
lineSf: target.item.sf,
|
|
89
|
+
reportNode: anchorClassLiteral,
|
|
90
|
+
label: target.item.isTvContext ? "tv" : "cn"
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const call = target.item.cnCall;
|
|
94
|
+
return {
|
|
95
|
+
start: call.getStart(target.item.sf),
|
|
96
|
+
end: call.getEnd(),
|
|
97
|
+
replacement: formatCnCallReplacement(target.item, textAfterUnwrap, withClassName),
|
|
98
|
+
bucketSummary: summarizeGroupBucketLabels(groups),
|
|
99
|
+
jsxCn: false,
|
|
100
|
+
lineSf: target.item.sf,
|
|
101
|
+
reportNode: target.item.primaryClassLiteral,
|
|
102
|
+
label: target.item.isTvContext ? "tv" : "cn"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { collectGroupTargets, collectLongJsxClassNameTargets, formatCnCallReplacement, planGroupEditForTarget, targetReplaceStart };
|