tempest-react-sdk 0.76.0 → 0.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/create-tempest-app.e2e.test.mjs +112 -0
- package/bin/create-tempest-app.mjs +69 -28
- package/bin/lib/alias/collect.mjs +20 -3
- package/bin/lib/alias/index.mjs +12 -1
- package/bin/lib/alias/index.test.mjs +24 -0
- package/bin/lib/component/generate.mjs +191 -0
- package/bin/lib/component/generate.test.mjs +336 -0
- package/bin/lib/css/analyze.test.mjs +21 -1
- package/bin/lib/css/collect.mjs +14 -0
- package/bin/lib/css/extract.mjs +63 -5
- package/bin/lib/css/extract.test.mjs +110 -1
- package/bin/lib/css/fix.e2e.test.mjs +50 -0
- package/bin/lib/css/index.mjs +6 -1
- package/bin/lib/fs/contain.mjs +50 -0
- package/bin/lib/icons/generate.mjs +28 -3
- package/bin/lib/icons/generate.test.mjs +57 -1
- package/bin/lib/openapi/escape.mjs +224 -0
- package/bin/lib/openapi/generate.encode.test.mjs +120 -0
- package/bin/lib/openapi/generate.hostile.test.mjs +621 -0
- package/bin/lib/openapi/generate.mjs +129 -18
- package/bin/lib/openapi/generate.test.mjs +7 -2
- package/bin/lib/openapi/schema-to-zod.mjs +43 -4
- package/bin/tempest-gen.test.mjs +188 -0
- package/bin/tempest.mjs +263 -6
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +203 -58
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/refresh-queue.cjs +1 -1
- package/dist/auth/refresh-queue.cjs.map +1 -1
- package/dist/auth/refresh-queue.js +13 -6
- package/dist/auth/refresh-queue.js.map +1 -1
- package/dist/br/pix.cjs +1 -1
- package/dist/br/pix.cjs.map +1 -1
- package/dist/br/pix.js +85 -72
- package/dist/br/pix.js.map +1 -1
- package/dist/br.d.ts +19 -4
- package/dist/components/Chat/ChatAttachments.cjs +1 -1
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
- package/dist/components/Chat/ChatAttachments.js +46 -38
- package/dist/components/Chat/ChatAttachments.js.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.js +1 -1
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
- package/dist/components/CodeBlock/tokenize.cjs +1 -1
- package/dist/components/CodeBlock/tokenize.cjs.map +1 -1
- package/dist/components/CodeBlock/tokenize.js +54 -25
- package/dist/components/CodeBlock/tokenize.js.map +1 -1
- package/dist/components/Markdown/markdown-parse.cjs +7 -5
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
- package/dist/components/Markdown/markdown-parse.js +171 -90
- package/dist/components/Markdown/markdown-parse.js.map +1 -1
- package/dist/components/Toast/Toast.module.cjs +1 -1
- package/dist/components/Toast/Toast.module.cjs.map +1 -1
- package/dist/components/Toast/Toast.module.js +7 -3
- package/dist/components/Toast/Toast.module.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js +77 -51
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/data/create-data-provider.cjs +1 -1
- package/dist/data/create-data-provider.cjs.map +1 -1
- package/dist/data/create-data-provider.js +32 -21
- package/dist/data/create-data-provider.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +81 -73
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +39 -35
- package/dist/http/errors.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +141 -105
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/retry.cjs +1 -1
- package/dist/http/retry.cjs.map +1 -1
- package/dist/http/retry.js +5 -6
- package/dist/http/retry.js.map +1 -1
- package/dist/oauth/callback.cjs +2 -0
- package/dist/oauth/callback.cjs.map +1 -0
- package/dist/oauth/callback.js +42 -0
- package/dist/oauth/callback.js.map +1 -0
- package/dist/oauth/use-oauth-callback.cjs +1 -1
- package/dist/oauth/use-oauth-callback.cjs.map +1 -1
- package/dist/oauth/use-oauth-callback.js +13 -20
- package/dist/oauth/use-oauth-callback.js.map +1 -1
- package/dist/push/use-push-subscription.cjs +1 -1
- package/dist/push/use-push-subscription.cjs.map +1 -1
- package/dist/push/use-push-subscription.js +42 -42
- package/dist/push/use-push-subscription.js.map +1 -1
- package/dist/push/web-push-client.cjs +1 -1
- package/dist/push/web-push-client.cjs.map +1 -1
- package/dist/push/web-push-client.js +23 -3
- package/dist/push/web-push-client.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +68 -23
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/styles/component/Toast.css +6 -1
- package/dist/styles/feedback.css +6 -1
- package/dist/styles.css +1 -1
- package/dist/sw/background-sync.cjs +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js +272 -60
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache-inspect.cjs.map +1 -1
- package/dist/sw/cache-inspect.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +91 -64
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw/credential-headers.cjs +2 -0
- package/dist/sw/credential-headers.cjs.map +1 -0
- package/dist/sw/credential-headers.js +17 -0
- package/dist/sw/credential-headers.js.map +1 -0
- package/dist/sw/session-data.cjs +2 -0
- package/dist/sw/session-data.cjs.map +1 -0
- package/dist/sw/session-data.js +17 -0
- package/dist/sw/session-data.js.map +1 -0
- package/dist/sw.cjs +1 -1
- package/dist/sw.d.ts +309 -23
- package/dist/sw.js +7 -5
- package/dist/telemetry/app-error-queue.cjs +1 -1
- package/dist/telemetry/app-error-queue.cjs.map +1 -1
- package/dist/telemetry/app-error-queue.js +19 -14
- package/dist/telemetry/app-error-queue.js.map +1 -1
- package/dist/telemetry/app-error-reporter.cjs +1 -1
- package/dist/telemetry/app-error-reporter.cjs.map +1 -1
- package/dist/telemetry/app-error-reporter.js +75 -34
- package/dist/telemetry/app-error-reporter.js.map +1 -1
- package/dist/telemetry/app-error-transport.cjs +1 -1
- package/dist/telemetry/app-error-transport.cjs.map +1 -1
- package/dist/telemetry/app-error-transport.js +16 -3
- package/dist/telemetry/app-error-transport.js.map +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +1183 -64
- package/dist/tempest-react-sdk.js +160 -156
- package/dist/utils/csv.cjs +3 -3
- package/dist/utils/csv.cjs.map +1 -1
- package/dist/utils/csv.js +18 -14
- package/dist/utils/csv.js.map +1 -1
- package/dist/utils/owner-fingerprint.cjs +2 -0
- package/dist/utils/owner-fingerprint.cjs.map +1 -0
- package/dist/utils/owner-fingerprint.js +18 -0
- package/dist/utils/owner-fingerprint.js.map +1 -0
- package/dist/utils/safe-url.cjs +2 -0
- package/dist/utils/safe-url.cjs.map +1 -0
- package/dist/utils/safe-url.js +39 -0
- package/dist/utils/safe-url.js.map +1 -0
- package/dist/vite/tempest-csp.cjs +2 -2
- package/dist/vite/tempest-csp.cjs.map +1 -1
- package/dist/vite/tempest-csp.js +130 -64
- package/dist/vite/tempest-csp.js.map +1 -1
- package/dist/vite.d.ts +23 -5
- package/dist/webrtc/peer-link.cjs +1 -1
- package/dist/webrtc/peer-link.cjs.map +1 -1
- package/dist/webrtc/peer-link.js +2 -10
- package/dist/webrtc/peer-link.js.map +1 -1
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +110 -72
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/package.json +1 -1
- package/template-pwa/src/pwa.tsx +22 -0
- package/template-pwa/src/sw.ts +27 -14
- package/dist/components/Markdown/markdown-url.cjs +0 -2
- package/dist/components/Markdown/markdown-url.cjs.map +0 -1
- package/dist/components/Markdown/markdown-url.js +0 -28
- package/dist/components/Markdown/markdown-url.js.map +0 -1
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
symlinkSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
import { ESLint } from "eslint";
|
|
17
|
+
import ts from "typescript";
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
19
|
+
|
|
20
|
+
import { componentFiles, normalizeComponentName } from "./generate.mjs";
|
|
21
|
+
|
|
22
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const REPO = resolve(HERE, "..", "..", "..");
|
|
24
|
+
const CLI = join(REPO, "bin", "tempest.mjs");
|
|
25
|
+
const TEMPLATE = join(REPO, "template");
|
|
26
|
+
/**
|
|
27
|
+
* Component names that collide with something a generated file declares or
|
|
28
|
+
* imports: React built-in hooks (`use<Name>` would be `useMemo`), the `React`
|
|
29
|
+
* namespace, and globals the component function would shadow.
|
|
30
|
+
*/
|
|
31
|
+
const COLLIDING = [
|
|
32
|
+
"UserCard",
|
|
33
|
+
"Memo",
|
|
34
|
+
"State",
|
|
35
|
+
"Effect",
|
|
36
|
+
"Callback",
|
|
37
|
+
"LayoutEffect",
|
|
38
|
+
"React",
|
|
39
|
+
"ReactMemo",
|
|
40
|
+
"UseMemo",
|
|
41
|
+
"Object",
|
|
42
|
+
"Promise",
|
|
43
|
+
"Error",
|
|
44
|
+
];
|
|
45
|
+
const FILES = ["constants.ts", "hooks.ts", "index.tsx", "styles.module.css", "utils.ts"];
|
|
46
|
+
|
|
47
|
+
describe("normalizeComponentName", () => {
|
|
48
|
+
it.each([
|
|
49
|
+
["user card", "UserCard"],
|
|
50
|
+
["user-card", "UserCard"],
|
|
51
|
+
["user_card", "UserCard"],
|
|
52
|
+
["userCard", "UserCard"],
|
|
53
|
+
["UserCard", "UserCard"],
|
|
54
|
+
[" button ", "Button"],
|
|
55
|
+
["api client", "ApiClient"],
|
|
56
|
+
["APIClient", "APIClient"],
|
|
57
|
+
["card2 header", "Card2Header"],
|
|
58
|
+
])("%j → %s", (raw, expected) => {
|
|
59
|
+
expect(normalizeComponentName(raw)).toBe(expected);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it.each([
|
|
63
|
+
["", /Missing component name/],
|
|
64
|
+
[" ", /Missing component name/],
|
|
65
|
+
["2fa", /must start with a letter/],
|
|
66
|
+
["-card", /must start with a letter/],
|
|
67
|
+
["user/card", /use ASCII letters, digits/],
|
|
68
|
+
["../evil", /use ASCII letters, digits/],
|
|
69
|
+
['card"; alert(1)', /use ASCII letters, digits/],
|
|
70
|
+
["cartão", /use ASCII letters, digits/],
|
|
71
|
+
["con", /reserved device name on Windows/],
|
|
72
|
+
["NUL", /reserved device name on Windows/],
|
|
73
|
+
["aux", /reserved device name on Windows/],
|
|
74
|
+
["prn", /reserved device name on Windows/],
|
|
75
|
+
["com1", /reserved device name on Windows/],
|
|
76
|
+
["Lpt9", /reserved device name on Windows/],
|
|
77
|
+
])("refuses %j", (raw, message) => {
|
|
78
|
+
expect(() => normalizeComponentName(raw)).toThrow(message);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("componentFiles", () => {
|
|
83
|
+
it("emits exactly the five files the issue asks for", () => {
|
|
84
|
+
const { name, files } = componentFiles("user card");
|
|
85
|
+
expect(name).toBe("UserCard");
|
|
86
|
+
expect(Object.keys(files).sort()).toEqual(FILES);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("wires every file into index.tsx so none starts as dead code", () => {
|
|
90
|
+
const { files } = componentFiles("UserCard");
|
|
91
|
+
expect(files["index.tsx"]).toContain('from "./hooks"');
|
|
92
|
+
expect(files["index.tsx"]).toContain('from "./utils"');
|
|
93
|
+
expect(files["index.tsx"]).toContain('from "./styles.module.css"');
|
|
94
|
+
expect(files["hooks.ts"]).toContain('from "./constants"');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("renames the hook when use<Name> is a React built-in hook", () => {
|
|
98
|
+
expect(componentFiles("Memo").files["hooks.ts"]).toContain(
|
|
99
|
+
"export function useMemoComponent(",
|
|
100
|
+
);
|
|
101
|
+
expect(componentFiles("State").files["index.tsx"]).toContain("useStateComponent(title)");
|
|
102
|
+
expect(componentFiles("Card").files["hooks.ts"]).toContain("export function useCard(");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("writes no comment and no single-quoted string", () => {
|
|
106
|
+
const { files } = componentFiles("UserCard");
|
|
107
|
+
for (const [file, contents] of Object.entries(files)) {
|
|
108
|
+
expect(contents, file).not.toMatch(/\/\/|\/\*/);
|
|
109
|
+
expect(contents, file).not.toContain("'");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it.each(COLLIDING)(
|
|
114
|
+
"passes the app template's ESLint config as generated for %s",
|
|
115
|
+
async (componentName) => {
|
|
116
|
+
const dir = mkdtempSync(join(tmpdir(), "tempest-component-lint-"));
|
|
117
|
+
try {
|
|
118
|
+
const { files } = componentFiles(componentName);
|
|
119
|
+
for (const [file, contents] of Object.entries(files)) {
|
|
120
|
+
writeFileSync(join(dir, file), contents);
|
|
121
|
+
}
|
|
122
|
+
const eslint = new ESLint({
|
|
123
|
+
cwd: dir,
|
|
124
|
+
overrideConfigFile: join(TEMPLATE, "eslint.config.js"),
|
|
125
|
+
});
|
|
126
|
+
const results = await eslint.lintFiles(["*.ts", "*.tsx"]);
|
|
127
|
+
const messages = results.flatMap((r) =>
|
|
128
|
+
r.messages.map((m) => `${r.filePath}:${m.line} ${m.ruleId} ${m.message}`),
|
|
129
|
+
);
|
|
130
|
+
expect(results).toHaveLength(4);
|
|
131
|
+
expect(messages).toEqual([]);
|
|
132
|
+
} finally {
|
|
133
|
+
rmSync(dir, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
it.each(COLLIDING)(
|
|
139
|
+
"type-checks %s under the app template's strict tsconfig",
|
|
140
|
+
(componentName) => {
|
|
141
|
+
const { files } = componentFiles(componentName);
|
|
142
|
+
const tsconfig = ts.parseConfigFileTextToJson(
|
|
143
|
+
"tsconfig.json",
|
|
144
|
+
readFileSync(join(TEMPLATE, "tsconfig.json"), "utf8"),
|
|
145
|
+
).config;
|
|
146
|
+
const { options } = ts.convertCompilerOptionsFromJson(
|
|
147
|
+
{ ...tsconfig.compilerOptions, types: [], paths: undefined },
|
|
148
|
+
REPO,
|
|
149
|
+
);
|
|
150
|
+
const root = join(REPO, "__virtual_component__");
|
|
151
|
+
const virtual = new Map(
|
|
152
|
+
Object.entries(files)
|
|
153
|
+
.filter(([file]) => !file.endsWith(".css"))
|
|
154
|
+
.map(([file, contents]) => [join(root, file), contents]),
|
|
155
|
+
);
|
|
156
|
+
virtual.set(
|
|
157
|
+
join(root, "css-modules.d.ts"),
|
|
158
|
+
'declare module "*.module.css" {\n const classes: Readonly<Record<string, string>>;\n export default classes;\n}\n',
|
|
159
|
+
);
|
|
160
|
+
const host = ts.createCompilerHost(options);
|
|
161
|
+
const readFile = host.readFile.bind(host);
|
|
162
|
+
const fileExists = host.fileExists.bind(host);
|
|
163
|
+
host.readFile = (f) => virtual.get(f) ?? readFile(f);
|
|
164
|
+
host.fileExists = (f) => virtual.has(f) || fileExists(f);
|
|
165
|
+
const directoryExists = host.directoryExists?.bind(host);
|
|
166
|
+
host.directoryExists = (d) => d === root || (directoryExists?.(d) ?? true);
|
|
167
|
+
const getSourceFile = host.getSourceFile.bind(host);
|
|
168
|
+
host.getSourceFile = (f, lang) =>
|
|
169
|
+
virtual.has(f)
|
|
170
|
+
? ts.createSourceFile(f, virtual.get(f), lang)
|
|
171
|
+
: getSourceFile(f, lang);
|
|
172
|
+
const program = ts.createProgram([...virtual.keys()], options, host);
|
|
173
|
+
const diagnostics = ts
|
|
174
|
+
.getPreEmitDiagnostics(program)
|
|
175
|
+
.map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"));
|
|
176
|
+
expect(diagnostics).toEqual([]);
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("tempest gen component (CLI)", () => {
|
|
182
|
+
let cwd;
|
|
183
|
+
|
|
184
|
+
beforeEach(() => {
|
|
185
|
+
cwd = mkdtempSync(join(tmpdir(), "tempest-component-cli-"));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
afterEach(() => {
|
|
189
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Run the `tempest` CLI in the temporary project.
|
|
194
|
+
*
|
|
195
|
+
* @param {string[]} args - CLI arguments.
|
|
196
|
+
* @returns {{ status: number | null, out: string }}
|
|
197
|
+
*/
|
|
198
|
+
function tempest(...args) {
|
|
199
|
+
const result = spawnSync(process.execPath, [CLI, ...args], { cwd, encoding: "utf8" });
|
|
200
|
+
return { status: result.status, out: result.stdout + result.stderr };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
it("creates src/components/<Name>/ with the five files by default", () => {
|
|
204
|
+
const { status, out } = tempest("gen", "component", "user card");
|
|
205
|
+
expect(status).toBe(0);
|
|
206
|
+
expect(readdirSync(join(cwd, "src", "components", "UserCard")).sort()).toEqual(FILES);
|
|
207
|
+
expect(out).toContain('import { UserCard } from "@/components/UserCard";');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("accepts `tempest new component`, the spelling from the issue", () => {
|
|
211
|
+
const { status } = tempest("new", "component", "Button");
|
|
212
|
+
expect(status).toBe(0);
|
|
213
|
+
expect(existsSync(join(cwd, "src", "components", "Button", "index.tsx"))).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("refuses another `new` target instead of growing a second generator name", () => {
|
|
217
|
+
const { status, out } = tempest("new", "api");
|
|
218
|
+
expect(status).toBe(1);
|
|
219
|
+
expect(out).toMatch(/Unknown new target: api/);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("honors --out", () => {
|
|
223
|
+
const { status, out } = tempest("gen", "component", "Card", "--out", "src/ui");
|
|
224
|
+
expect(status).toBe(0);
|
|
225
|
+
expect(existsSync(join(cwd, "src", "ui", "Card", "hooks.ts"))).toBe(true);
|
|
226
|
+
expect(out).toContain('from "@/ui/Card"');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("refuses to touch an existing folder without --force", () => {
|
|
230
|
+
const dir = join(cwd, "src", "components", "Card");
|
|
231
|
+
mkdirSync(dir, { recursive: true });
|
|
232
|
+
writeFileSync(join(dir, "index.tsx"), "mine");
|
|
233
|
+
|
|
234
|
+
const { status, out } = tempest("gen", "component", "Card");
|
|
235
|
+
|
|
236
|
+
expect(status).toBe(1);
|
|
237
|
+
expect(out).toMatch(/already exists.*--force/);
|
|
238
|
+
expect(readFileSync(join(dir, "index.tsx"), "utf8")).toBe("mine");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("--force rewrites the five files and leaves the rest of the folder alone", () => {
|
|
242
|
+
const dir = join(cwd, "src", "components", "Card");
|
|
243
|
+
mkdirSync(dir, { recursive: true });
|
|
244
|
+
writeFileSync(join(dir, "index.tsx"), "mine");
|
|
245
|
+
writeFileSync(join(dir, "Card.test.tsx"), "keep");
|
|
246
|
+
|
|
247
|
+
const { status } = tempest("gen", "component", "Card", "--force");
|
|
248
|
+
|
|
249
|
+
expect(status).toBe(0);
|
|
250
|
+
expect(readFileSync(join(dir, "index.tsx"), "utf8")).toContain("export function Card(");
|
|
251
|
+
expect(readFileSync(join(dir, "Card.test.tsx"), "utf8")).toBe("keep");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("refuses to write through a symlinked folder even with --force", () => {
|
|
255
|
+
const outside = join(cwd, "outside");
|
|
256
|
+
mkdirSync(outside);
|
|
257
|
+
mkdirSync(join(cwd, "src", "components"), { recursive: true });
|
|
258
|
+
symlinkSync(outside, join(cwd, "src", "components", "Card"));
|
|
259
|
+
|
|
260
|
+
const { status, out } = tempest("gen", "component", "Card", "--force");
|
|
261
|
+
|
|
262
|
+
expect(status).toBe(1);
|
|
263
|
+
expect(out).toMatch(/symlink/);
|
|
264
|
+
expect(readdirSync(outside)).toEqual([]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it.each([
|
|
268
|
+
[["gen", "component", "Card", "--out", "../x"], /outside the project/],
|
|
269
|
+
[["gen", "component", "Card", "--out", "/tmp/tempest-outside-abs"], /outside the project/],
|
|
270
|
+
[["gen", "component", "Card", "--out", "--force"], /--out needs a folder/],
|
|
271
|
+
[["gen", "component", "Card", "--out"], /--out needs a folder/],
|
|
272
|
+
[["gen", "component", "Card", "--out", ""], /--out needs a folder/],
|
|
273
|
+
])("refuses an --out of %j", (args, message) => {
|
|
274
|
+
const { status, out } = tempest(...args);
|
|
275
|
+
expect(status).toBe(1);
|
|
276
|
+
expect(out).toMatch(message);
|
|
277
|
+
expect(existsSync(join(cwd, "src"))).toBe(false);
|
|
278
|
+
expect(existsSync(join(cwd, "--force"))).toBe(false);
|
|
279
|
+
expect(existsSync(join(dirname(cwd), "x"))).toBe(false);
|
|
280
|
+
expect(existsSync("/tmp/tempest-outside-abs")).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("refuses an --out whose existing ancestor is a symlink leaving the project", () => {
|
|
284
|
+
const outside = mkdtempSync(join(tmpdir(), "tempest-component-outside-"));
|
|
285
|
+
try {
|
|
286
|
+
mkdirSync(join(cwd, "src"));
|
|
287
|
+
symlinkSync(outside, join(cwd, "src", "components"));
|
|
288
|
+
|
|
289
|
+
const { status, out } = tempest("gen", "component", "Card");
|
|
290
|
+
|
|
291
|
+
expect(status).toBe(1);
|
|
292
|
+
expect(out).toMatch(/outside the project/);
|
|
293
|
+
expect(readdirSync(outside)).toEqual([]);
|
|
294
|
+
} finally {
|
|
295
|
+
rmSync(outside, { recursive: true, force: true });
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("--force validates all five targets before writing any of them", () => {
|
|
300
|
+
const dir = join(cwd, "src", "components", "Card");
|
|
301
|
+
mkdirSync(join(dir, "utils.ts"), { recursive: true });
|
|
302
|
+
writeFileSync(join(dir, "index.tsx"), "mine");
|
|
303
|
+
|
|
304
|
+
const { status, out } = tempest("gen", "component", "Card", "--force");
|
|
305
|
+
|
|
306
|
+
expect(status).toBe(1);
|
|
307
|
+
expect(out).toMatch(/utils\.ts is not a regular file/);
|
|
308
|
+
expect(readFileSync(join(dir, "index.tsx"), "utf8")).toBe("mine");
|
|
309
|
+
expect(readdirSync(dir).sort()).toEqual(["index.tsx", "utils.ts"]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("prints the path with a warning instead of a wrong import outside src/", () => {
|
|
313
|
+
const { status, out } = tempest("gen", "component", "Card", "--out", "lib/ui");
|
|
314
|
+
expect(status).toBe(0);
|
|
315
|
+
expect(out).not.toMatch(/import \{ Card \}/);
|
|
316
|
+
expect(out).toMatch(/lib\/ui\/Card.*outside src\/.*@\//s);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("keeps the @/ hint for a folder whose name merely starts with two dots", () => {
|
|
320
|
+
const { status, out } = tempest("gen", "component", "Card", "--out", "src/..ui");
|
|
321
|
+
expect(status).toBe(0);
|
|
322
|
+
expect(out).toContain('import { Card } from "@/..ui/Card";');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it.each([
|
|
326
|
+
[["gen", "component"], /Missing component name/],
|
|
327
|
+
[["gen", "component", "2fa"], /must start with a letter/],
|
|
328
|
+
[["gen", "component", "user", "card"], /Expected one component name, got 2/],
|
|
329
|
+
[["gen", "component", "Card", "--dry-run"], /Unknown option: --dry-run/],
|
|
330
|
+
])("fails with a clear message for %j", (args, message) => {
|
|
331
|
+
const { status, out } = tempest(...args);
|
|
332
|
+
expect(status).toBe(1);
|
|
333
|
+
expect(out).toMatch(message);
|
|
334
|
+
expect(existsSync(join(cwd, "src"))).toBe(false);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
@@ -64,6 +64,26 @@ describe("analyzeCss — collection", () => {
|
|
|
64
64
|
expect(analysis.stats.files).toBe(1);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
it("skips a sheet reached through a symlink leaving the project, so fix never rewrites it", () => {
|
|
68
|
+
const outside = mkdtempSync(join(tmpdir(), "tempest-css-outside-"));
|
|
69
|
+
try {
|
|
70
|
+
const before = ".a {\n color: red;\n color: red;\n}\n";
|
|
71
|
+
writeFileSync(join(outside, "a.css"), before);
|
|
72
|
+
symlinkSync(outside, join(root, "src"), "dir");
|
|
73
|
+
|
|
74
|
+
const analysis = analyze(["src"]);
|
|
75
|
+
applyCssFixes({ analysis });
|
|
76
|
+
|
|
77
|
+
expect(analysis.stats.files).toBe(0);
|
|
78
|
+
expect(analysis.skipped[0]).toMatchObject({
|
|
79
|
+
reason: "resolves outside the project through a symlink",
|
|
80
|
+
});
|
|
81
|
+
expect(readFileSync(join(outside, "a.css"), "utf8")).toBe(before);
|
|
82
|
+
} finally {
|
|
83
|
+
rmSync(outside, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
67
87
|
it("sorts findings error → warn → info", () => {
|
|
68
88
|
write("src/a.css", ".a {\n color: ;\n}\n.b {\n color: red;\n color: red;\n}\n");
|
|
69
89
|
const severities = analyze().findings.map((f) => f.severity);
|
package/bin/lib/css/collect.mjs
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
|
|
7
|
+
import { landsInside } from "../fs/contain.mjs";
|
|
8
|
+
|
|
7
9
|
const SKIP_DIRS = new Set([
|
|
8
10
|
"node_modules",
|
|
9
11
|
"dist",
|
|
@@ -19,6 +21,9 @@ const SKIP_DIRS = new Set([
|
|
|
19
21
|
".cache",
|
|
20
22
|
]);
|
|
21
23
|
|
|
24
|
+
/** `skipped` reason for a sheet that resolves outside the project. */
|
|
25
|
+
export const OUTSIDE_PROJECT = "resolves outside the project through a symlink";
|
|
26
|
+
|
|
22
27
|
/** Above this size a stylesheet is treated as generated, not authored. */
|
|
23
28
|
const MAX_BYTES = 512 * 1024;
|
|
24
29
|
|
|
@@ -35,6 +40,11 @@ export function looksMinified(text) {
|
|
|
35
40
|
/**
|
|
36
41
|
* Collect the project's own `.css` files.
|
|
37
42
|
*
|
|
43
|
+
* A sheet that resolves outside the project once symlinks are followed is
|
|
44
|
+
* skipped with a reason: an explicit target that is a link (`tempest fix src`
|
|
45
|
+
* with `src -> /elsewhere`) is walked through it, and `fix` would rewrite sheets
|
|
46
|
+
* that are not the project's.
|
|
47
|
+
*
|
|
38
48
|
* @param {object} params
|
|
39
49
|
* @param {string} params.root - Project root; relative targets resolve against it.
|
|
40
50
|
* @param {string[]} [params.targets] - Positional paths from the command line.
|
|
@@ -62,6 +72,10 @@ export function collectStylesheets({ root, targets = ["."], maxFiles = MAX_FILES
|
|
|
62
72
|
skipped.push({ path, reason: "larger than 512 KB — treated as generated" });
|
|
63
73
|
return;
|
|
64
74
|
}
|
|
75
|
+
if (!landsInside(root, path)) {
|
|
76
|
+
skipped.push({ path, reason: OUTSIDE_PROJECT });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
65
79
|
if (/\.min\.css$/.test(path)) {
|
|
66
80
|
skipped.push({ path, reason: "minified" });
|
|
67
81
|
return;
|
package/bin/lib/css/extract.mjs
CHANGED
|
@@ -18,13 +18,17 @@
|
|
|
18
18
|
// dead and take the remaining styles down with it;
|
|
19
19
|
// - the class is only ever read as `styles.row` / `styles["row"]`, never
|
|
20
20
|
// through a computed key or by handing the whole object around;
|
|
21
|
-
// -
|
|
22
|
-
//
|
|
21
|
+
// - no declaration carries a line terminator, so the appended rule is the
|
|
22
|
+
// same lines it was in the module and cannot open a new line of its own;
|
|
23
|
+
// - the target global sheet is a regular file (not a symlink) whose realpath is
|
|
24
|
+
// inside the project, AND some source file imports it, because appending to a
|
|
25
|
+
// stylesheet nobody loads is a silent no-op;
|
|
23
26
|
// - the new global name collides with nothing already in that sheet.
|
|
24
|
-
import {
|
|
27
|
+
import { readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
25
28
|
import { join, relative } from "node:path";
|
|
26
29
|
|
|
27
30
|
import { discoverAlias } from "../alias/discover.mjs";
|
|
31
|
+
import { landsInside, lstatOrNull } from "../fs/contain.mjs";
|
|
28
32
|
import { loadTypeScript, typeScriptUnavailableReason } from "../alias/typescript.mjs";
|
|
29
33
|
import { collectSources, indexClassUses, resolveSpecifier } from "./references.mjs";
|
|
30
34
|
import { parseCss } from "./parse.mjs";
|
|
@@ -33,6 +37,9 @@ import { declSignature } from "./semantic.mjs";
|
|
|
33
37
|
/** A repeated block is only worth extracting from this many declarations up. */
|
|
34
38
|
const MIN_DECLS = 3;
|
|
35
39
|
|
|
40
|
+
/** CSS newlines (`\n`, `\r`, `\f`) plus the two Unicode line separators. */
|
|
41
|
+
const LINE_TERMINATOR = /[\n\r\f\u2028\u2029]/;
|
|
42
|
+
|
|
36
43
|
/** Stylesheets treated as "the project's global sheet", in order of preference. */
|
|
37
44
|
const TARGET_CANDIDATES = [
|
|
38
45
|
"src/styles/globals.css",
|
|
@@ -82,7 +89,7 @@ export function classNamesIn(parsed) {
|
|
|
82
89
|
*/
|
|
83
90
|
export function findGlobalSheet({ root, explicit, alias }) {
|
|
84
91
|
const candidates = explicit ? [explicit] : TARGET_CANDIDATES;
|
|
85
|
-
const found = candidates.map((rel) => join(root, rel)).find(
|
|
92
|
+
const found = candidates.map((rel) => join(root, rel)).find((path) => lstatOrNull(path));
|
|
86
93
|
if (!found) {
|
|
87
94
|
return {
|
|
88
95
|
error: explicit
|
|
@@ -95,6 +102,9 @@ export function findGlobalSheet({ root, explicit, alias }) {
|
|
|
95
102
|
error: `${relative(root, found)} é um CSS Module — a folha alvo tem que ser global`,
|
|
96
103
|
};
|
|
97
104
|
}
|
|
105
|
+
const unsafe = unsafeTargetReason(root, found);
|
|
106
|
+
if (unsafe) return { error: `${relative(root, found)} ${unsafe} — recusado` };
|
|
107
|
+
const realFound = realpathSync(found);
|
|
98
108
|
|
|
99
109
|
const imported = collectSources({ root }).some((file) => {
|
|
100
110
|
let text;
|
|
@@ -106,7 +116,7 @@ export function findGlobalSheet({ root, explicit, alias }) {
|
|
|
106
116
|
if (!text.includes(".css")) return false;
|
|
107
117
|
for (const match of text.matchAll(/["']([^"']+\.css)["']/g)) {
|
|
108
118
|
const target = resolveSpecifier({ specifier: match[1], fromFile: file, alias });
|
|
109
|
-
if (target ===
|
|
119
|
+
if (target !== null && realpathOrNull(target) === realFound) return true;
|
|
110
120
|
}
|
|
111
121
|
return false;
|
|
112
122
|
});
|
|
@@ -118,6 +128,47 @@ export function findGlobalSheet({ root, explicit, alias }) {
|
|
|
118
128
|
return { path: found, file: relative(root, found) };
|
|
119
129
|
}
|
|
120
130
|
|
|
131
|
+
/**
|
|
132
|
+
* `realpath` that answers `null` for a missing or dangling path.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} path - Absolute path.
|
|
135
|
+
* @returns {string | null}
|
|
136
|
+
*/
|
|
137
|
+
function realpathOrNull(path) {
|
|
138
|
+
try {
|
|
139
|
+
return realpathSync(path);
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Why the candidate global sheet must not be appended to, or `null`.
|
|
147
|
+
*
|
|
148
|
+
* `writeFileSync` follows symlinks, so a `src/index.css` that is a link — or
|
|
149
|
+
* that sits under a linked folder pointing out of the project — carries the
|
|
150
|
+
* append to wherever it points: measured before this check, a link to a shell rc
|
|
151
|
+
* file got the extracted rule appended, and a custom property value with a
|
|
152
|
+
* newline in it became a line the shell ran.
|
|
153
|
+
*
|
|
154
|
+
* The rule matches the generators': the sheet itself must be a regular file
|
|
155
|
+
* (`lstat`, so a link — dangling included — is refused), and its `realpath` must
|
|
156
|
+
* land inside `realpath(root)`. A symlinked folder that stays inside the project
|
|
157
|
+
* (a monorepo's `src -> app/src`) is accepted — the write lands in the project
|
|
158
|
+
* either way, and refusing it would switch the pass off for a legitimate layout.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} root - Project root.
|
|
161
|
+
* @param {string} path - Absolute candidate path, which `lstat` sees.
|
|
162
|
+
* @returns {string | null} A phrase completing "`<file>` …", or `null`.
|
|
163
|
+
*/
|
|
164
|
+
function unsafeTargetReason(root, path) {
|
|
165
|
+
const stat = lstatOrNull(path);
|
|
166
|
+
if (stat?.isSymbolicLink()) return "é um symlink, e a escrita seguiria o link";
|
|
167
|
+
if (!stat?.isFile()) return "não é um arquivo comum";
|
|
168
|
+
if (!landsInside(root, path)) return "fica fora do projeto depois de resolver symlinks";
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
121
172
|
/** Group the analysed sheets by identical declaration block. */
|
|
122
173
|
function groupBySignature(sheets) {
|
|
123
174
|
const groups = new Map();
|
|
@@ -145,6 +196,13 @@ function occurrenceRefusal({ sheet, block, className, uses, opaqueModules }) {
|
|
|
145
196
|
}
|
|
146
197
|
if (!className) return `\`${block.prelude}\` não é uma classe simples`;
|
|
147
198
|
|
|
199
|
+
const broken = block.decls.find(
|
|
200
|
+
(d) => LINE_TERMINATOR.test(d.prop) || LINE_TERMINATOR.test(d.value),
|
|
201
|
+
);
|
|
202
|
+
if (broken) {
|
|
203
|
+
return `a declaração \`${broken.prop.split(LINE_TERMINATOR)[0]}\` tem quebra de linha — copiada pra folha global, viraria uma linha nova fora da regra`;
|
|
204
|
+
}
|
|
205
|
+
|
|
148
206
|
const mentions = sheet.parsed.blocks.filter(
|
|
149
207
|
(other) =>
|
|
150
208
|
other !== block && new RegExp(`\\.${className}(?![A-Za-z0-9_-])`).test(other.prelude),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
6
|
|
|
7
7
|
import { analyzeCss } from "./analyze.mjs";
|
|
@@ -295,6 +295,20 @@ describe("planExtraction — refusals", () => {
|
|
|
295
295
|
expect(result.message).toContain("é um CSS Module");
|
|
296
296
|
});
|
|
297
297
|
|
|
298
|
+
it("refuses a declaration with a line break instead of copying it into the global sheet", () => {
|
|
299
|
+
twoCopies();
|
|
300
|
+
const broken =
|
|
301
|
+
".row {\n --payload: x\necho INJECTED\n;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.title {\n font-weight: 600;\n}\n";
|
|
302
|
+
write("src/components/Card.module.css", broken);
|
|
303
|
+
write("src/components/List.module.css", broken.replace(".row", ".line"));
|
|
304
|
+
const result = plan();
|
|
305
|
+
expect(result.groups).toEqual([]);
|
|
306
|
+
expect(result.refusals.map((r) => r.reason)).toEqual([
|
|
307
|
+
expect.stringContaining("tem quebra de linha"),
|
|
308
|
+
expect.stringContaining("tem quebra de linha"),
|
|
309
|
+
]);
|
|
310
|
+
});
|
|
311
|
+
|
|
298
312
|
it("reports a missing TypeScript instead of guessing at the call sites", () => {
|
|
299
313
|
twoCopies();
|
|
300
314
|
rmSync(join(root, "node_modules", "typescript"), { recursive: true, force: true });
|
|
@@ -302,3 +316,98 @@ describe("planExtraction — refusals", () => {
|
|
|
302
316
|
expect(result.status).toBe("no-typescript");
|
|
303
317
|
});
|
|
304
318
|
});
|
|
319
|
+
|
|
320
|
+
describe("planExtraction — the global sheet stays inside the project (#483)", () => {
|
|
321
|
+
let outside;
|
|
322
|
+
|
|
323
|
+
beforeEach(() => {
|
|
324
|
+
project();
|
|
325
|
+
twoCopies();
|
|
326
|
+
outside = mkdtempSync(join(tmpdir(), "tempest-extract-outside-"));
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
afterEach(() => {
|
|
330
|
+
rmSync(outside, { recursive: true, force: true });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
/** Plan and apply, the way `tempest fix --extract-css` does. */
|
|
334
|
+
const run = () => {
|
|
335
|
+
const result = plan();
|
|
336
|
+
applyExtraction({ plan: result });
|
|
337
|
+
return result;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
it("refuses a src/index.css that is a symlink to a file outside the project", () => {
|
|
341
|
+
const victim = join(outside, "victimrc");
|
|
342
|
+
writeFileSync(victim, "echo ORIGINAL\n");
|
|
343
|
+
rmSync(join(root, "src/index.css"));
|
|
344
|
+
symlinkSync(victim, join(root, "src/index.css"));
|
|
345
|
+
|
|
346
|
+
const result = run();
|
|
347
|
+
|
|
348
|
+
expect(result.status).toBe("no-target");
|
|
349
|
+
expect(result.message).toContain("é um symlink");
|
|
350
|
+
expect(readFileSync(victim, "utf8")).toBe("echo ORIGINAL\n");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("refuses a dangling src/index.css symlink instead of falling through to another sheet", () => {
|
|
354
|
+
rmSync(join(root, "src/index.css"));
|
|
355
|
+
symlinkSync(join(outside, "not-yet.css"), join(root, "src/index.css"));
|
|
356
|
+
write("src/app.css", ":root {}\n");
|
|
357
|
+
write("src/main.tsx", 'import "./index.css";\nimport "./app.css";\n');
|
|
358
|
+
|
|
359
|
+
const result = run();
|
|
360
|
+
|
|
361
|
+
expect(result.status).toBe("no-target");
|
|
362
|
+
expect(result.message).toContain("src/index.css é um symlink");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("refuses a global sheet under a folder that is a symlink leaving the project", () => {
|
|
366
|
+
writeFileSync(join(outside, "globals.css"), ":root {}\n");
|
|
367
|
+
symlinkSync(outside, join(root, "src/styles"));
|
|
368
|
+
write("src/main.tsx", 'import "./styles/globals.css";\n');
|
|
369
|
+
|
|
370
|
+
const result = run();
|
|
371
|
+
|
|
372
|
+
expect(result.status).toBe("no-target");
|
|
373
|
+
expect(result.message).toContain("fica fora do projeto");
|
|
374
|
+
expect(readFileSync(join(outside, "globals.css"), "utf8")).toBe(":root {}\n");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Reversed on purpose. The first version of #483 refused any symlink on the
|
|
379
|
+
* path, so a monorepo layout like `src/styles -> theme` switched the pass off
|
|
380
|
+
* with a `!` and exit 0. The write lands inside the project either way; what
|
|
381
|
+
* is refused is a link on the sheet itself and a realpath that leaves the
|
|
382
|
+
* project — the same rule as `gen api`/`gen component`.
|
|
383
|
+
*/
|
|
384
|
+
it("accepts a symlinked folder that stays inside the project, as the generators do", () => {
|
|
385
|
+
write("theme/globals.css", ":root {}\n");
|
|
386
|
+
symlinkSync(join(root, "theme"), join(root, "src/styles"));
|
|
387
|
+
write("src/main.tsx", 'import "./styles/globals.css";\n');
|
|
388
|
+
|
|
389
|
+
const result = run();
|
|
390
|
+
|
|
391
|
+
expect(result.status).toBe("ok");
|
|
392
|
+
expect(result.groups).toHaveLength(1);
|
|
393
|
+
expect(readFileSync(join(root, "theme/globals.css"), "utf8")).toContain(".u-row {");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("refuses a --css-target outside the project", () => {
|
|
397
|
+
writeFileSync(join(outside, "x.css"), "");
|
|
398
|
+
const result = plan({ target: relative(root, join(outside, "x.css")) });
|
|
399
|
+
expect(result.status).toBe("no-target");
|
|
400
|
+
expect(result.message).toContain("fora do projeto");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("matches importers by realpath, so a project under a symlinked path still works", () => {
|
|
404
|
+
const linked = join(outside, "linked-project");
|
|
405
|
+
symlinkSync(root, linked);
|
|
406
|
+
const result = planExtraction({
|
|
407
|
+
analysis: analyzeCss({ root: linked, targets: ["."], selfDir: SELF_DIR }),
|
|
408
|
+
root: linked,
|
|
409
|
+
});
|
|
410
|
+
expect(result.status).toBe("ok");
|
|
411
|
+
expect(result.groups).toHaveLength(1);
|
|
412
|
+
});
|
|
413
|
+
});
|