tempest-react-sdk 0.28.1 → 0.29.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/bin/lib/alias/index.mjs +9 -3
- package/bin/lib/alias/tsconfig.mjs +48 -5
- package/bin/lib/alias/typescript.mjs +72 -2
- package/bin/lib/alias/typescript.test.mjs +120 -0
- package/bin/lib/css/extract.mjs +405 -0
- package/bin/lib/css/extract.test.mjs +304 -0
- package/bin/lib/css/fix.e2e.test.mjs +92 -1
- package/bin/lib/css/index.mjs +10 -0
- package/bin/lib/css/references.mjs +238 -0
- package/bin/lib/doctor/doctor.e2e.test.mjs +44 -0
- package/bin/tempest.mjs +146 -9
- package/package.json +1 -1
|
@@ -155,6 +155,50 @@ describe("tempest doctor — generic findings still surface", () => {
|
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
describe("tempest doctor — a project on TypeScript 7", () => {
|
|
159
|
+
/**
|
|
160
|
+
* TypeScript 7 is the native port: same package name, and no classic compiler
|
|
161
|
+
* API — it moved to `typescript/unstable/*`. Before this was detected, `doctor`
|
|
162
|
+
* died with `ts.readConfigFile is not a function` on any such project, which is
|
|
163
|
+
* the worst possible failure for the command people run first.
|
|
164
|
+
*/
|
|
165
|
+
beforeEach(() => {
|
|
166
|
+
thirdPartyApp();
|
|
167
|
+
write("node_modules/typescript/package.json", {
|
|
168
|
+
name: "typescript",
|
|
169
|
+
version: "7.0.2",
|
|
170
|
+
main: "index.js",
|
|
171
|
+
});
|
|
172
|
+
write(
|
|
173
|
+
"node_modules/typescript/index.js",
|
|
174
|
+
'module.exports = { version: "7.0.2", versionMajorMinor: "7.0" };',
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("does not crash, and still reports the tsconfig checks", () => {
|
|
179
|
+
const { out, code } = doctor();
|
|
180
|
+
expect(out).not.toContain("is not a function");
|
|
181
|
+
expect(out).toContain("moduleResolution: bundler");
|
|
182
|
+
expect(code).toBe(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("says the codemods are unavailable, and why", () => {
|
|
186
|
+
const { out } = doctor();
|
|
187
|
+
expect(out).toContain("no classic compiler API");
|
|
188
|
+
expect(out).toContain("every other pass runs");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("reads a tsconfig with comments through the fallback parser", () => {
|
|
192
|
+
writeFileSync(
|
|
193
|
+
join(root, "tsconfig.json"),
|
|
194
|
+
'{\n // vite default\n "compilerOptions": { "strict": true, "jsx": "react-jsx", "moduleResolution": "bundler" },\n}\n',
|
|
195
|
+
);
|
|
196
|
+
const { out } = doctor();
|
|
197
|
+
expect(out).toContain("strict mode on");
|
|
198
|
+
expect(out).not.toContain("strict mode off");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
158
202
|
describe("tempest doctor — stylesheets", () => {
|
|
159
203
|
beforeEach(thirdPartyApp);
|
|
160
204
|
|
package/bin/tempest.mjs
CHANGED
|
@@ -12,9 +12,9 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
12
12
|
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { aliasImports } from "./lib/alias/index.mjs";
|
|
15
|
-
import { loadTypeScript } from "./lib/alias/typescript.mjs";
|
|
15
|
+
import { describeTypeScript, loadTypeScript } from "./lib/alias/typescript.mjs";
|
|
16
16
|
import { readTsconfig } from "./lib/alias/tsconfig.mjs";
|
|
17
|
-
import { analyzeCss, applyCssFixes } from "./lib/css/index.mjs";
|
|
17
|
+
import { analyzeCss, applyCssFixes, applyExtraction, planExtraction } from "./lib/css/index.mjs";
|
|
18
18
|
import { checkLucide } from "./lib/doctor/lucide.mjs";
|
|
19
19
|
import { generateRegistry, loadIconTables } from "./lib/icons/generate.mjs";
|
|
20
20
|
import { generate } from "./lib/openapi/generate.mjs";
|
|
@@ -535,6 +535,21 @@ function doctor() {
|
|
|
535
535
|
const tsc = readTsconfig({ root: ROOT, ts: loadTypeScript(ROOT) });
|
|
536
536
|
if (tsc) {
|
|
537
537
|
checks.push(["section", "TypeScript"]);
|
|
538
|
+
/*
|
|
539
|
+
* TypeScript 7 installs under the same package name and does not ship the
|
|
540
|
+
* classic compiler API — it lives behind `typescript/unstable/*` with a
|
|
541
|
+
* different shape. The tsconfig checks below keep working (they fall back to
|
|
542
|
+
* a JSONC-tolerant parse), but the codemods cannot run, and saying so here is
|
|
543
|
+
* better than each pass reporting it as if TypeScript were missing.
|
|
544
|
+
*/
|
|
545
|
+
const tsInstall = describeTypeScript(ROOT);
|
|
546
|
+
if (tsInstall.status === "api-unavailable") {
|
|
547
|
+
checks.push([
|
|
548
|
+
"info",
|
|
549
|
+
`typescript ${tsInstall.version} has no classic compiler API`,
|
|
550
|
+
"`tempest fix` skips the alias and --extract-css codemods — they need the TypeScript 6 API; every other pass runs",
|
|
551
|
+
]);
|
|
552
|
+
}
|
|
538
553
|
const co = tsc.compilerOptions;
|
|
539
554
|
// The `@/*` alias is the SDK's convention, not a health property: a project
|
|
540
555
|
// that has not adopted the SDK is not wrong for lacking it.
|
|
@@ -814,7 +829,36 @@ function lint(args) {
|
|
|
814
829
|
}
|
|
815
830
|
|
|
816
831
|
/** Flags `fix` owns itself; anything else is rejected rather than forwarded. */
|
|
817
|
-
const FIX_FLAGS = new Set(["--no-alias", "--no-css", "--dry-run"]);
|
|
832
|
+
const FIX_FLAGS = new Set(["--no-alias", "--no-css", "--dry-run", "--extract-css"]);
|
|
833
|
+
|
|
834
|
+
/** Flags of `fix` that consume the next argument as their value. */
|
|
835
|
+
const FIX_VALUE_FLAGS = new Set(["--css-target", "--css-prefix"]);
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Split the `fix` argv into flags, flag values and positional paths.
|
|
839
|
+
*
|
|
840
|
+
* `splitArgs` cannot do this: it classifies by leading `-`, so the value of
|
|
841
|
+
* `--css-target src/index.css` would land in the path list and `fix` would run
|
|
842
|
+
* against a single stylesheet instead of the project.
|
|
843
|
+
*
|
|
844
|
+
* @param {string[]} args
|
|
845
|
+
* @returns {{ flags: string[], values: Record<string, string>, paths: string[] }}
|
|
846
|
+
*/
|
|
847
|
+
function parseFixArgs(args) {
|
|
848
|
+
const flags = [];
|
|
849
|
+
const values = {};
|
|
850
|
+
const paths = [];
|
|
851
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
852
|
+
const arg = args[i];
|
|
853
|
+
if (FIX_VALUE_FLAGS.has(arg)) {
|
|
854
|
+
values[arg] = args[i + 1] ?? "";
|
|
855
|
+
i += 1;
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
(arg.startsWith("-") ? flags : paths).push(arg);
|
|
859
|
+
}
|
|
860
|
+
return { flags, values, paths };
|
|
861
|
+
}
|
|
818
862
|
|
|
819
863
|
/**
|
|
820
864
|
* Print the alias pass result.
|
|
@@ -826,7 +870,7 @@ const FIX_FLAGS = new Set(["--no-alias", "--no-css", "--dry-run"]);
|
|
|
826
870
|
function reportAlias(result, dryRun) {
|
|
827
871
|
if (result.status === "no-typescript") {
|
|
828
872
|
console.log(
|
|
829
|
-
`${c.yellow}!
|
|
873
|
+
`${c.yellow}! alias pass skipped${c.reset} ${c.dim}— ${result.reason ?? "typescript não instalado"}${c.reset}`,
|
|
830
874
|
);
|
|
831
875
|
return;
|
|
832
876
|
}
|
|
@@ -875,9 +919,12 @@ function severityMark(severity) {
|
|
|
875
919
|
* @param {object} params
|
|
876
920
|
* @param {string[]} params.targets - Positional paths from the command line.
|
|
877
921
|
* @param {boolean} params.dryRun
|
|
922
|
+
* @param {boolean} [params.extract] - Run the opt-in cross-file extraction codemod.
|
|
923
|
+
* @param {string} [params.target] - Global stylesheet to extract into.
|
|
924
|
+
* @param {string} [params.prefix] - Prefix for the extracted class names.
|
|
878
925
|
* @returns {number} Exit status contribution.
|
|
879
926
|
*/
|
|
880
|
-
function cssPass({ targets, dryRun }) {
|
|
927
|
+
function cssPass({ targets, dryRun, extract = false, target, prefix }) {
|
|
881
928
|
console.log(
|
|
882
929
|
`${c.dim}→ css (dedupe declarations · drop dead rules)${dryRun ? " [dry-run]" : ""}${c.reset}`,
|
|
883
930
|
);
|
|
@@ -939,20 +986,95 @@ function cssPass({ targets, dryRun }) {
|
|
|
939
986
|
` ${c.red}✗ ${analysis.counts.error} CSS syntax error(s)${c.reset} ${c.dim}— those files were not touched; fix them and run again${c.reset}`,
|
|
940
987
|
);
|
|
941
988
|
}
|
|
942
|
-
|
|
989
|
+
|
|
990
|
+
const extractStatus = extract
|
|
991
|
+
? extractPass({ targets, dryRun, target, prefix, dedupedFiles: result.files.length })
|
|
992
|
+
: 0;
|
|
993
|
+
|
|
994
|
+
return result.errors.length || analysis.counts.error || extractStatus ? 1 : 0;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* The opt-in extraction codemod: a block repeated across CSS Modules becomes one
|
|
999
|
+
* class in the project's global stylesheet, and the `styles.x` that pointed at
|
|
1000
|
+
* the local copies become the new class name.
|
|
1001
|
+
*
|
|
1002
|
+
* The analysis is re-run from disk rather than reused: the dedupe pass that just
|
|
1003
|
+
* finished may have rewritten these same files, and every edit here is a splice at
|
|
1004
|
+
* a recorded offset. Reusing the stale parse would splice at positions that no
|
|
1005
|
+
* longer exist.
|
|
1006
|
+
*
|
|
1007
|
+
* @param {object} params
|
|
1008
|
+
* @param {string[]} params.targets
|
|
1009
|
+
* @param {boolean} params.dryRun
|
|
1010
|
+
* @param {string} [params.target]
|
|
1011
|
+
* @param {string} [params.prefix]
|
|
1012
|
+
* @param {number} params.dedupedFiles - Files the dedupe pass touched, for the re-read note.
|
|
1013
|
+
* @returns {number} Exit status contribution.
|
|
1014
|
+
*/
|
|
1015
|
+
function extractPass({ targets, dryRun, target, prefix, dedupedFiles }) {
|
|
1016
|
+
console.log(
|
|
1017
|
+
`${c.dim}→ css extract (bloco repetido → classe global)${dryRun ? " [dry-run]" : ""}${c.reset}`,
|
|
1018
|
+
);
|
|
1019
|
+
let plan;
|
|
1020
|
+
try {
|
|
1021
|
+
const fresh = analyzeCss({ root: ROOT, targets, selfDir: SELF_DIR });
|
|
1022
|
+
if (dedupedFiles > 0 && !dryRun) {
|
|
1023
|
+
console.log(` ${c.dim}re-analisado depois da passada de dedupe${c.reset}`);
|
|
1024
|
+
}
|
|
1025
|
+
plan = planExtraction({ analysis: fresh, root: ROOT, target, prefix });
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
console.log(
|
|
1028
|
+
` ${c.red}✗ extraction failed${c.reset} ${c.dim}${err?.message ?? err}${c.reset}`,
|
|
1029
|
+
);
|
|
1030
|
+
return 1;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (plan.status !== "ok") {
|
|
1034
|
+
console.log(` ${c.yellow}! ${plan.message}${c.reset}`);
|
|
1035
|
+
return 0;
|
|
1036
|
+
}
|
|
1037
|
+
for (const refusal of plan.refusals) {
|
|
1038
|
+
console.log(
|
|
1039
|
+
` [${c.yellow}!${c.reset}] ${refusal.file}:${refusal.line} ${c.dim}não extraído — ${refusal.reason}${c.reset}`,
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
if (plan.groups.length === 0) {
|
|
1043
|
+
console.log(` ${c.dim}nada a extrair${c.reset}`);
|
|
1044
|
+
return 0;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const result = applyExtraction({ plan, dryRun });
|
|
1048
|
+
for (const file of result.files) {
|
|
1049
|
+
console.log(` ${file.file} ${c.dim}${file.changes.length}${c.reset}`);
|
|
1050
|
+
for (const change of file.changes) console.log(` ${c.dim}${change}${c.reset}`);
|
|
1051
|
+
}
|
|
1052
|
+
for (const err of result.errors) {
|
|
1053
|
+
console.log(` ${c.red}✗ ${err.file}${c.reset} ${c.dim}${err.message}${c.reset}`);
|
|
1054
|
+
}
|
|
1055
|
+
const verb = dryRun ? "moveria" : "movidas";
|
|
1056
|
+
console.log(
|
|
1057
|
+
` ${c.green}✓${c.reset} ${verb} ${result.moved} regra(s) local(is) para ${result.rules} classe(s) em ${plan.target.file}`,
|
|
1058
|
+
);
|
|
1059
|
+
return result.errors.length ? 1 : 0;
|
|
943
1060
|
}
|
|
944
1061
|
|
|
945
1062
|
function fix(args) {
|
|
946
|
-
const { flags, paths } =
|
|
1063
|
+
const { flags, values, paths } = parseFixArgs(args);
|
|
947
1064
|
const unknown = flags.filter((f) => !FIX_FLAGS.has(f));
|
|
948
1065
|
if (unknown.length) {
|
|
949
1066
|
console.error(
|
|
950
|
-
`${c.red}✗ Unknown flag for fix: ${unknown.join(", ")}${c.reset} — supported: ${[...FIX_FLAGS].join(", ")}`,
|
|
1067
|
+
`${c.red}✗ Unknown flag for fix: ${unknown.join(", ")}${c.reset} — supported: ${[...FIX_FLAGS, ...FIX_VALUE_FLAGS].join(", ")}`,
|
|
951
1068
|
);
|
|
952
1069
|
return 1;
|
|
953
1070
|
}
|
|
954
1071
|
const targets = paths.length ? paths : ["."];
|
|
955
1072
|
const dryRun = flags.includes("--dry-run");
|
|
1073
|
+
const extract = flags.includes("--extract-css");
|
|
1074
|
+
if (!extract && (values["--css-target"] || values["--css-prefix"])) {
|
|
1075
|
+
console.error(`${c.red}✗ --css-target/--css-prefix só valem com --extract-css.${c.reset}`);
|
|
1076
|
+
return 1;
|
|
1077
|
+
}
|
|
956
1078
|
|
|
957
1079
|
let aliasStatus = 0;
|
|
958
1080
|
if (!flags.includes("--no-alias")) {
|
|
@@ -962,7 +1084,15 @@ function fix(args) {
|
|
|
962
1084
|
aliasStatus = result.status === "error" ? 1 : 0;
|
|
963
1085
|
}
|
|
964
1086
|
|
|
965
|
-
const cssStatus = flags.includes("--no-css")
|
|
1087
|
+
const cssStatus = flags.includes("--no-css")
|
|
1088
|
+
? 0
|
|
1089
|
+
: cssPass({
|
|
1090
|
+
targets,
|
|
1091
|
+
dryRun,
|
|
1092
|
+
extract,
|
|
1093
|
+
target: values["--css-target"],
|
|
1094
|
+
prefix: values["--css-prefix"],
|
|
1095
|
+
});
|
|
966
1096
|
|
|
967
1097
|
if (dryRun) return aliasStatus || cssStatus;
|
|
968
1098
|
|
|
@@ -1125,6 +1255,13 @@ ${c.bold}fix options${c.reset}
|
|
|
1125
1255
|
write nothing
|
|
1126
1256
|
--no-alias Skip the alias pass
|
|
1127
1257
|
--no-css Skip the CSS pass (analysis and dead-code removal)
|
|
1258
|
+
--extract-css Move a declaration block repeated across CSS Modules into the
|
|
1259
|
+
project's global stylesheet and rewrite the styles.x reads that
|
|
1260
|
+
pointed at the local copies (opt-in — it changes what couples
|
|
1261
|
+
to what). Refuses anything it cannot prove safe, with a reason
|
|
1262
|
+
--css-target <f> Global stylesheet to extract into (default: the first of
|
|
1263
|
+
src/styles/globals.css, src/globals.css, src/index.css, …)
|
|
1264
|
+
--css-prefix <p> Prefix for extracted class names (default: u-)
|
|
1128
1265
|
|
|
1129
1266
|
${c.bold}Options${c.reset}
|
|
1130
1267
|
-h, --help Show this help
|