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
package/bin/lib/alias/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { collectFiles } from "./collect.mjs";
|
|
|
7
7
|
import { discoverAlias } from "./discover.mjs";
|
|
8
8
|
import { rewriteCss } from "./rewrite-css.mjs";
|
|
9
9
|
import { rewriteSource } from "./rewrite.mjs";
|
|
10
|
-
import { loadTypeScript } from "./typescript.mjs";
|
|
10
|
+
import { loadTypeScript, typeScriptUnavailableReason } from "./typescript.mjs";
|
|
11
11
|
|
|
12
12
|
export { collectFiles } from "./collect.mjs";
|
|
13
13
|
export { discoverAlias } from "./discover.mjs";
|
|
@@ -15,7 +15,7 @@ export { rewriteCss } from "./rewrite-css.mjs";
|
|
|
15
15
|
export { rewriteSource } from "./rewrite.mjs";
|
|
16
16
|
export { isInsideBase, toAlias } from "./specifier.mjs";
|
|
17
17
|
export { readTsconfig } from "./tsconfig.mjs";
|
|
18
|
-
export { loadTypeScript } from "./typescript.mjs";
|
|
18
|
+
export { describeTypeScript, loadTypeScript, typeScriptUnavailableReason } from "./typescript.mjs";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Convert relative imports that climb out of their directory into alias imports.
|
|
@@ -42,7 +42,13 @@ export function aliasImports({ root, targets, dryRun = false }) {
|
|
|
42
42
|
const empty = { files: [], total: 0, errors: [] };
|
|
43
43
|
|
|
44
44
|
const ts = loadTypeScript(root);
|
|
45
|
-
if (!ts)
|
|
45
|
+
if (!ts) {
|
|
46
|
+
return {
|
|
47
|
+
status: "no-typescript",
|
|
48
|
+
reason: typeScriptUnavailableReason(root),
|
|
49
|
+
...empty,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
46
52
|
|
|
47
53
|
const alias = discoverAlias({ root, ts });
|
|
48
54
|
if (!alias) return { status: "no-alias", ...empty };
|
|
@@ -6,19 +6,62 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
6
6
|
|
|
7
7
|
const MAX_EXTENDS_DEPTH = 16;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Strip the JSONC a tsconfig is allowed to contain: `//` and block comments, and
|
|
11
|
+
* a trailing comma before `}` or `]`.
|
|
12
|
+
*
|
|
13
|
+
* Needed because `ts.readConfigFile` is not always available — TypeScript 7 does
|
|
14
|
+
* not ship the classic API — and a plain `JSON.parse` chokes on the comments
|
|
15
|
+
* every Vite-generated tsconfig has. Losing the whole config to a comment would
|
|
16
|
+
* silently drop the `strict`/`jsx`/`paths` checks.
|
|
17
|
+
*
|
|
18
|
+
* Comment markers inside a string are left alone, which matters: `"paths"` and
|
|
19
|
+
* `"extends"` values legitimately hold `//` (a URL, a Windows share).
|
|
20
|
+
*
|
|
21
|
+
* @param {string} text
|
|
22
|
+
* @returns {string} Plain JSON.
|
|
23
|
+
*/
|
|
24
|
+
export function stripJsonc(text) {
|
|
25
|
+
let out = "";
|
|
26
|
+
let i = 0;
|
|
27
|
+
while (i < text.length) {
|
|
28
|
+
const ch = text[i];
|
|
29
|
+
if (ch === '"') {
|
|
30
|
+
let j = i + 1;
|
|
31
|
+
while (j < text.length && text[j] !== '"') j += text[j] === "\\" ? 2 : 1;
|
|
32
|
+
out += text.slice(i, Math.min(j + 1, text.length));
|
|
33
|
+
i = j + 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
37
|
+
const end = text.indexOf("\n", i);
|
|
38
|
+
i = end < 0 ? text.length : end;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
42
|
+
const end = text.indexOf("*/", i + 2);
|
|
43
|
+
i = end < 0 ? text.length : end + 2;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
out += ch;
|
|
47
|
+
i += 1;
|
|
48
|
+
}
|
|
49
|
+
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
50
|
+
}
|
|
51
|
+
|
|
9
52
|
/**
|
|
10
53
|
* Parse one tsconfig file into its raw JSON object.
|
|
11
54
|
*
|
|
12
|
-
* Prefers `ts.readConfigFile`, which accepts the
|
|
13
|
-
*
|
|
14
|
-
* TypeScript
|
|
55
|
+
* Prefers `ts.readConfigFile`, which accepts the exact dialect `tsc` accepts.
|
|
56
|
+
* Falls back to a JSONC-tolerant parse so a project without the classic
|
|
57
|
+
* TypeScript API still gets its config read instead of nothing.
|
|
15
58
|
*
|
|
16
59
|
* @param {string} path - Absolute path to a tsconfig file.
|
|
17
60
|
* @param {object | null} ts - The `typescript` module, or `null`.
|
|
18
61
|
* @returns {object | null} The raw config object, or `null` when unreadable.
|
|
19
62
|
*/
|
|
20
63
|
function parseConfigFile(path, ts) {
|
|
21
|
-
if (ts) {
|
|
64
|
+
if (ts && typeof ts.readConfigFile === "function") {
|
|
22
65
|
const { config, error } = ts.readConfigFile(path, (p) => {
|
|
23
66
|
try {
|
|
24
67
|
return readFileSync(p, "utf8");
|
|
@@ -29,7 +72,7 @@ function parseConfigFile(path, ts) {
|
|
|
29
72
|
return error || !config ? null : config;
|
|
30
73
|
}
|
|
31
74
|
try {
|
|
32
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
75
|
+
return JSON.parse(stripJsonc(readFileSync(path, "utf8")));
|
|
33
76
|
} catch {
|
|
34
77
|
return null;
|
|
35
78
|
}
|
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The classic-API members every codemod in this CLI needs.
|
|
8
|
+
*
|
|
9
|
+
* TypeScript 7 is the native port: its package still installs under the name
|
|
10
|
+
* `typescript`, but the `.` export is a version stub — the JS API moved behind
|
|
11
|
+
* `typescript/unstable/*` with a different shape. So "the module resolved" no
|
|
12
|
+
* longer implies "the API is there", and calling into it blew up with
|
|
13
|
+
* `ts.readConfigFile is not a function` — a stack trace out of `tempest doctor`,
|
|
14
|
+
* which is the command people run first.
|
|
15
|
+
*/
|
|
16
|
+
const REQUIRED_API = ["readConfigFile", "createSourceFile", "forEachChild"];
|
|
17
|
+
|
|
18
|
+
/** True when the resolved module exposes the classic compiler API. */
|
|
19
|
+
function hasClassicApi(module) {
|
|
20
|
+
return Boolean(module) && REQUIRED_API.every((name) => typeof module[name] === "function");
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
/**
|
|
7
24
|
* Load `typescript` from the project's `node_modules`.
|
|
8
25
|
*
|
|
@@ -11,13 +28,66 @@ import { join } from "node:path";
|
|
|
11
28
|
* compiler version than the one that type-checks it would let syntax the app
|
|
12
29
|
* accepts fail here.
|
|
13
30
|
*
|
|
31
|
+
* Returns `null` both when TypeScript is absent and when the install does not
|
|
32
|
+
* expose the classic API, because callers treat the two the same way — report it
|
|
33
|
+
* and leave the source untouched. Use {@link describeTypeScript} to say which of
|
|
34
|
+
* the two happened.
|
|
35
|
+
*
|
|
14
36
|
* @param {string} root - Project root, the directory holding `node_modules`.
|
|
15
|
-
* @returns {object | null} The `typescript` module, or `null
|
|
37
|
+
* @returns {object | null} The `typescript` module, or `null`.
|
|
16
38
|
*/
|
|
17
39
|
export function loadTypeScript(root) {
|
|
18
40
|
try {
|
|
19
|
-
|
|
41
|
+
const module = createRequire(join(root, "package.json"))("typescript");
|
|
42
|
+
return hasClassicApi(module) ? module : null;
|
|
20
43
|
} catch {
|
|
21
44
|
return null;
|
|
22
45
|
}
|
|
23
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What the project's TypeScript install is, so a message can say the truth
|
|
50
|
+
* instead of "not installed" about a package that is right there.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} root - Project root.
|
|
53
|
+
* @returns {{ status: "ok" | "missing" | "api-unavailable", version: string | null }}
|
|
54
|
+
*/
|
|
55
|
+
export function describeTypeScript(root) {
|
|
56
|
+
let resolve_;
|
|
57
|
+
try {
|
|
58
|
+
resolve_ = createRequire(join(root, "package.json"));
|
|
59
|
+
} catch {
|
|
60
|
+
return { status: "missing", version: null };
|
|
61
|
+
}
|
|
62
|
+
let version = null;
|
|
63
|
+
try {
|
|
64
|
+
version = resolve_("typescript/package.json")?.version ?? null;
|
|
65
|
+
} catch {
|
|
66
|
+
return { status: "missing", version: null };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return {
|
|
70
|
+
status: hasClassicApi(resolve_("typescript")) ? "ok" : "api-unavailable",
|
|
71
|
+
version,
|
|
72
|
+
};
|
|
73
|
+
} catch {
|
|
74
|
+
return { status: "api-unavailable", version };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* One line explaining why a codemod cannot run, or `null` when it can.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} root - Project root.
|
|
82
|
+
* @returns {string | null}
|
|
83
|
+
*/
|
|
84
|
+
export function typeScriptUnavailableReason(root) {
|
|
85
|
+
const { status, version } = describeTypeScript(root);
|
|
86
|
+
if (status === "ok") return null;
|
|
87
|
+
if (status === "missing") return "typescript não instalado (npm i -D typescript)";
|
|
88
|
+
return (
|
|
89
|
+
`typescript ${version ?? "?"} não expõe a API clássica do compilador — o 7 publica só ` +
|
|
90
|
+
"`typescript/unstable/*`, com outra forma. Pra usar os codemods, tenha o TypeScript 6 " +
|
|
91
|
+
"instalado; o resto do comando segue normal"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { readTsconfig, stripJsonc } from "./tsconfig.mjs";
|
|
7
|
+
import { describeTypeScript, loadTypeScript, typeScriptUnavailableReason } from "./typescript.mjs";
|
|
8
|
+
|
|
9
|
+
let root;
|
|
10
|
+
|
|
11
|
+
/** Write a file under the fixture, creating parent directories. */
|
|
12
|
+
function write(rel, text) {
|
|
13
|
+
const full = join(root, rel);
|
|
14
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
15
|
+
writeFileSync(full, text);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fake an installed `typescript` whose main module exports `exports`.
|
|
20
|
+
*
|
|
21
|
+
* TypeScript 7 is exactly this shape: the package resolves, the version is there,
|
|
22
|
+
* and the classic API is not — it moved to `typescript/unstable/*`.
|
|
23
|
+
*/
|
|
24
|
+
function fakeTypeScript(version, exports) {
|
|
25
|
+
write(
|
|
26
|
+
"node_modules/typescript/package.json",
|
|
27
|
+
JSON.stringify({ name: "typescript", version, main: "index.js" }),
|
|
28
|
+
);
|
|
29
|
+
write("node_modules/typescript/index.js", `module.exports = ${exports};`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
root = mkdtempSync(join(tmpdir(), "tempest-ts-"));
|
|
34
|
+
write("package.json", JSON.stringify({ name: "fixture" }));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
rmSync(root, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("loadTypeScript", () => {
|
|
42
|
+
it("returns null when typescript is not installed", () => {
|
|
43
|
+
expect(loadTypeScript(root)).toBeNull();
|
|
44
|
+
expect(describeTypeScript(root)).toEqual({ status: "missing", version: null });
|
|
45
|
+
expect(typeScriptUnavailableReason(root)).toContain("não instalado");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns null for an install without the classic compiler API", () => {
|
|
49
|
+
fakeTypeScript("7.0.2", '{ version: "7.0.2", versionMajorMinor: "7.0" }');
|
|
50
|
+
expect(loadTypeScript(root)).toBeNull();
|
|
51
|
+
expect(describeTypeScript(root)).toEqual({ status: "api-unavailable", version: "7.0.2" });
|
|
52
|
+
const reason = typeScriptUnavailableReason(root);
|
|
53
|
+
expect(reason).toContain("7.0.2");
|
|
54
|
+
expect(reason).toContain("unstable");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns the module when the classic API is there", () => {
|
|
58
|
+
fakeTypeScript(
|
|
59
|
+
"6.0.3",
|
|
60
|
+
"{ readConfigFile: () => ({}), createSourceFile: () => ({}), forEachChild: () => {} }",
|
|
61
|
+
);
|
|
62
|
+
expect(loadTypeScript(root)).not.toBeNull();
|
|
63
|
+
expect(describeTypeScript(root)).toEqual({ status: "ok", version: "6.0.3" });
|
|
64
|
+
expect(typeScriptUnavailableReason(root)).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("treats a partial API as unavailable rather than half-usable", () => {
|
|
68
|
+
fakeTypeScript("7.1.0", "{ createSourceFile: () => ({}) }");
|
|
69
|
+
expect(loadTypeScript(root)).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("stripJsonc", () => {
|
|
74
|
+
it("removes line and block comments", () => {
|
|
75
|
+
expect(stripJsonc('{ // hi\n "a": 1 /* there */ }')).toBe('{ \n "a": 1 }');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("removes a trailing comma before a close", () => {
|
|
79
|
+
expect(JSON.parse(stripJsonc('{ "a": [1, 2,], }'))).toEqual({ a: [1, 2] });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("leaves comment markers inside strings alone", () => {
|
|
83
|
+
expect(JSON.parse(stripJsonc('{ "url": "https://x.dev/a", "p": "a/*b" }'))).toEqual({
|
|
84
|
+
url: "https://x.dev/a",
|
|
85
|
+
p: "a/*b",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("survives an unterminated comment instead of throwing", () => {
|
|
90
|
+
expect(() => stripJsonc('{ "a": 1 } /* forever')).not.toThrow();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("readTsconfig without the compiler API", () => {
|
|
95
|
+
it("still reads a tsconfig that has comments and a trailing comma", () => {
|
|
96
|
+
write(
|
|
97
|
+
"tsconfig.json",
|
|
98
|
+
'{\n // the app\n "compilerOptions": {\n "strict": true,\n "paths": { "@/*": ["./src/*"] },\n },\n}\n',
|
|
99
|
+
);
|
|
100
|
+
const config = readTsconfig({ root, ts: null });
|
|
101
|
+
expect(config.compilerOptions.strict).toBe(true);
|
|
102
|
+
expect(config.compilerOptions.paths["@/*"]).toEqual(["./src/*"]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("follows extends with the fallback parser", () => {
|
|
106
|
+
write("base.json", '{ "compilerOptions": { "jsx": "react-jsx" } } // base\n');
|
|
107
|
+
write(
|
|
108
|
+
"tsconfig.json",
|
|
109
|
+
'{ "extends": "./base.json", "compilerOptions": { "strict": true } }',
|
|
110
|
+
);
|
|
111
|
+
const config = readTsconfig({ root, ts: null });
|
|
112
|
+
expect(config.compilerOptions).toMatchObject({ jsx: "react-jsx", strict: true });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("ignores a `ts` that cannot read a config file", () => {
|
|
116
|
+
write("tsconfig.json", '{ "compilerOptions": { "strict": true } }');
|
|
117
|
+
const config = readTsconfig({ root, ts: { version: "7.0.2" } });
|
|
118
|
+
expect(config.compilerOptions.strict).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|