Skip to content

Commit cbb7fbf

Browse files
committed
Fix ignore support in wds.js
Ignoring files outside the project directory, or folders at any level, was broken. This fixes it!
1 parent 891eaa2 commit cbb7fbf

File tree

7 files changed

+347
-3
lines changed

7 files changed

+347
-3
lines changed

spec/ProjectConfig.spec.ts

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
import fs from "fs-extra";
2+
import * as path from "path";
3+
import { fileURLToPath } from "url";
4+
import { beforeEach, describe, expect, it } from "vitest";
5+
import { projectConfig } from "../src/ProjectConfig.js";
6+
7+
const dirname = fileURLToPath(new URL(".", import.meta.url));
8+
9+
describe("ProjectConfig", () => {
10+
beforeEach(() => {
11+
projectConfig.cache.clear?.();
12+
});
13+
14+
describe("default configuration", () => {
15+
it("should load default config when wds.js does not exist", async () => {
16+
const nonExistentRoot = path.join(dirname, "fixtures/configs/non-existent");
17+
await fs.ensureDir(nonExistentRoot);
18+
19+
const config = await projectConfig(nonExistentRoot);
20+
21+
expect(config.root).toBe(nonExistentRoot);
22+
expect(config.extensions).toEqual([".ts", ".tsx", ".jsx"]);
23+
expect(config.esm).toBe(true);
24+
expect(config.includeGlob).toBe("**/*{.ts,.tsx,.jsx}");
25+
// Default ignores should be applied
26+
expect(config.includedMatcher("node_modules/package/index.ts")).toBe(false);
27+
expect(config.includedMatcher("types.d.ts")).toBe(false);
28+
expect(config.includedMatcher(".git/config.ts")).toBe(false);
29+
30+
await fs.remove(nonExistentRoot);
31+
});
32+
33+
it("should set cacheDir relative to root", async () => {
34+
const nonExistentRoot = path.join(dirname, "fixtures/configs/non-existent2");
35+
await fs.ensureDir(nonExistentRoot);
36+
37+
const config = await projectConfig(nonExistentRoot);
38+
39+
expect(config.cacheDir).toBe(path.join(nonExistentRoot, "node_modules/.cache/wds"));
40+
41+
await fs.remove(nonExistentRoot);
42+
});
43+
});
44+
45+
describe("config file loading", () => {
46+
it("should load empty config and merge with defaults", async () => {
47+
const configRoot = path.join(dirname, "fixtures/configs/empty-config");
48+
49+
const config = await projectConfig(configRoot);
50+
51+
expect(config.root).toBe(configRoot);
52+
expect(config.extensions).toEqual([".ts", ".tsx", ".jsx"]);
53+
expect(config.esm).toBe(true);
54+
});
55+
56+
it("should use config from existing wds.js", async () => {
57+
const configRoot = path.join(dirname, "fixtures/src/files_with_config");
58+
59+
const config = await projectConfig(configRoot);
60+
61+
expect(config.root).toBe(configRoot);
62+
expect(config.swc).toBeDefined();
63+
expect((config.swc as any).jsc.target).toBe("es5");
64+
});
65+
66+
it("should merge custom extensions with defaults", async () => {
67+
const configRoot = path.join(dirname, "fixtures/configs/with-extensions");
68+
69+
const config = await projectConfig(configRoot);
70+
71+
expect(config.extensions).toEqual([".ts", ".js"]);
72+
expect(config.includeGlob).toBe("**/*{.ts,.js}");
73+
});
74+
});
75+
76+
describe("file ignores", () => {
77+
it("should work with both absolute and relative paths", async () => {
78+
const configRoot = path.join(dirname, "fixtures/src/files_with_config");
79+
80+
const config = await projectConfig(configRoot);
81+
82+
// Relative paths
83+
expect(config.includedMatcher("simple.ts")).toBe(true);
84+
expect(config.includedMatcher("ignored.ts")).toBe(false);
85+
86+
// Absolute paths
87+
expect(config.includedMatcher(path.join(configRoot, "simple.ts"))).toBe(true);
88+
expect(config.includedMatcher(path.join(configRoot, "ignored.ts"))).toBe(false);
89+
90+
// Absolute paths outside root should be rejected
91+
expect(config.includedMatcher("/some/other/path/file.ts")).toBe(false);
92+
});
93+
94+
it("should match files with configured extensions", async () => {
95+
const configRoot = path.join(dirname, "fixtures/src/files_with_config");
96+
97+
const config = await projectConfig(configRoot);
98+
99+
expect(config.includedMatcher("simple.ts")).toBe(true);
100+
expect(config.includedMatcher("test.tsx")).toBe(true);
101+
expect(config.includedMatcher("test.jsx")).toBe(true);
102+
});
103+
104+
it("should not match files with wrong extensions", async () => {
105+
const configRoot = path.join(dirname, "fixtures/src/files_with_config");
106+
107+
const config = await projectConfig(configRoot);
108+
109+
expect(config.includedMatcher("test.js")).toBe(false);
110+
expect(config.includedMatcher("test.py")).toBe(false);
111+
expect(config.includedMatcher("README.md")).toBe(false);
112+
});
113+
114+
it("should not match explicitly ignored files", async () => {
115+
const configRoot = path.join(dirname, "fixtures/src/files_with_config");
116+
117+
const config = await projectConfig(configRoot);
118+
119+
expect(config.includedMatcher("ignored.ts")).toBe(false);
120+
expect(config.includedMatcher("simple.ts")).toBe(true);
121+
});
122+
123+
it("should not match .d.ts files", async () => {
124+
const configRoot = path.join(dirname, "fixtures/configs/empty-config");
125+
126+
const config = await projectConfig(configRoot);
127+
128+
expect(config.includedMatcher("types.d.ts")).toBe(false);
129+
expect(config.includedMatcher("src/types.d.ts")).toBe(false);
130+
expect(config.includedMatcher("types.ts")).toBe(true);
131+
});
132+
133+
it("should not match files in node_modules", async () => {
134+
const configRoot = path.join(dirname, "fixtures/configs/empty-config");
135+
136+
const config = await projectConfig(configRoot);
137+
138+
expect(config.includedMatcher("node_modules/package/index.ts")).toBe(false);
139+
expect(config.includedMatcher("src/node_modules/package/index.ts")).toBe(false);
140+
});
141+
142+
it("should not match the .git directory", async () => {
143+
const configRoot = path.join(dirname, "fixtures/configs/empty-config");
144+
145+
const config = await projectConfig(configRoot);
146+
147+
expect(config.includedMatcher(".git")).toBe(false);
148+
});
149+
150+
it("should not match files in .git directory", async () => {
151+
const configRoot = path.join(dirname, "fixtures/configs/empty-config");
152+
153+
const config = await projectConfig(configRoot);
154+
155+
expect(config.includedMatcher(".git/config.ts")).toBe(false);
156+
expect(config.includedMatcher(".git/hooks/pre-commit.ts")).toBe(false);
157+
});
158+
159+
it("should match files with glob pattern ignores", async () => {
160+
const configRoot = path.join(dirname, "fixtures/configs/basic-ignore");
161+
162+
const config = await projectConfig(configRoot);
163+
164+
expect(config.includedMatcher("src/file.ts")).toBe(true);
165+
expect(config.includedMatcher("src/ignored/file.ts")).toBe(false);
166+
expect(config.includedMatcher("file.test.ts")).toBe(false);
167+
expect(config.includedMatcher("src/file.test.ts")).toBe(false);
168+
});
169+
170+
it("should not match files outside project root even if they match extensions", async () => {
171+
const configRoot = path.join(dirname, "fixtures/configs/empty-config");
172+
173+
const config = await projectConfig(configRoot);
174+
175+
// These paths are outside the project root
176+
const outsideFile1 = path.resolve(configRoot, "../../outside-file.ts");
177+
const outsideFile2 = path.resolve(configRoot, "../sibling/file.tsx");
178+
179+
// Make paths relative to config root for micromatch
180+
const relativeOutside1 = path.relative(configRoot, outsideFile1);
181+
const relativeOutside2 = path.relative(configRoot, outsideFile2);
182+
183+
// Files starting with ../ are outside the root
184+
expect(relativeOutside1.startsWith("..")).toBe(true);
185+
expect(relativeOutside2.startsWith("..")).toBe(true);
186+
187+
// The matcher should not match files outside the project root
188+
// This tests the actual behavior of micromatch with cwd option
189+
expect(config.includedMatcher(relativeOutside1)).toBe(false);
190+
expect(config.includedMatcher(relativeOutside2)).toBe(false);
191+
});
192+
193+
it("should not match directories outside project root", async () => {
194+
const configRoot = path.join(dirname, "fixtures/configs/outside-root");
195+
196+
const config = await projectConfig(configRoot);
197+
const exampleDir = path.join(path.dirname(path.dirname(configRoot)), "tmp");
198+
199+
// Should work with both relative and absolute paths
200+
const relativeDir = path.relative(configRoot, exampleDir);
201+
const relativeFile = path.relative(configRoot, path.join(exampleDir, "file.ts"));
202+
203+
expect(config.includedMatcher(relativeDir)).toBe(false);
204+
expect(config.includedMatcher(relativeFile)).toBe(false);
205+
206+
// Should also work with absolute paths (and reject files outside root)
207+
expect(config.includedMatcher(exampleDir)).toBe(false);
208+
expect(config.includedMatcher(path.join(exampleDir, "file.ts"))).toBe(false);
209+
});
210+
211+
it("should handle absolute paths in ignore patterns", async () => {
212+
const tempRoot = path.join(dirname, "fixtures/configs/temp-absolute");
213+
await fs.ensureDir(tempRoot);
214+
const absoluteIgnore = "/some/absolute/path/*.ts";
215+
await fs.writeFile(path.join(tempRoot, "wds.js"), `module.exports = { ignore: ["${absoluteIgnore}"] };`);
216+
217+
const config = await projectConfig(tempRoot);
218+
219+
// Absolute path patterns should be preserved and work
220+
expect(config.includedMatcher("/some/absolute/path/file.ts")).toBe(false);
221+
expect(config.includedMatcher("src/file.ts")).toBe(true);
222+
223+
await fs.remove(tempRoot);
224+
});
225+
226+
it("should handle complex relative patterns outside root", async () => {
227+
const tempRoot = path.join(dirname, "fixtures/configs/temp-complex");
228+
await fs.ensureDir(tempRoot);
229+
await fs.writeFile(path.join(tempRoot, "wds.js"), `module.exports = { ignore: ["../../../**/*.test.ts", "../../sibling/**"] };`);
230+
231+
const config = await projectConfig(tempRoot);
232+
233+
// Test that files matching these patterns are excluded
234+
expect(config.includedMatcher("../../../some/file.test.ts")).toBe(false);
235+
expect(config.includedMatcher("../../sibling/file.ts")).toBe(false);
236+
expect(config.includedMatcher("src/file.ts")).toBe(true);
237+
238+
await fs.remove(tempRoot);
239+
});
240+
});
241+
242+
describe("cacheDir resolution", () => {
243+
it("should resolve relative cacheDirs to absolute paths", async () => {
244+
const tempRoot = path.join(dirname, "fixtures/configs/temp-cache");
245+
await fs.ensureDir(tempRoot);
246+
await fs.writeFile(path.join(tempRoot, "wds.js"), "module.exports = { cacheDir: '.cache/wds' };");
247+
248+
const config = await projectConfig(tempRoot);
249+
250+
expect(config.cacheDir).toBe(path.join(tempRoot, ".cache/wds"));
251+
expect(path.isAbsolute(config.cacheDir)).toBe(true);
252+
253+
await fs.remove(tempRoot);
254+
});
255+
256+
it("should keep absolute cacheDirs as-is", async () => {
257+
const tempRoot = path.join(dirname, "fixtures/configs/temp-cache-abs");
258+
const absoluteCacheDir = "/tmp/wds-cache";
259+
await fs.ensureDir(tempRoot);
260+
await fs.writeFile(path.join(tempRoot, "wds.js"), `module.exports = { cacheDir: "${absoluteCacheDir}" };`);
261+
262+
const config = await projectConfig(tempRoot);
263+
264+
expect(config.cacheDir).toBe(absoluteCacheDir);
265+
266+
await fs.remove(tempRoot);
267+
});
268+
});
269+
270+
it("should handle config with default export", async () => {
271+
const tempRoot = path.join(dirname, "fixtures/configs/temp-default");
272+
await fs.ensureDir(tempRoot);
273+
await fs.writeFile(path.join(tempRoot, "wds.js"), "module.exports.default = { extensions: ['.ts'] };");
274+
275+
const config = await projectConfig(tempRoot);
276+
277+
expect(config.extensions).toEqual([".ts"]);
278+
279+
await fs.remove(tempRoot);
280+
});
281+
282+
it("should handle config with esm: false", async () => {
283+
const tempRoot = path.join(dirname, "fixtures/configs/temp-cjs");
284+
await fs.ensureDir(tempRoot);
285+
await fs.writeFile(path.join(tempRoot, "wds.js"), "module.exports = { esm: false };");
286+
287+
const config = await projectConfig(tempRoot);
288+
289+
expect(config.esm).toBe(false);
290+
291+
await fs.remove(tempRoot);
292+
});
293+
294+
it("should generate correct includeGlob based on extensions", async () => {
295+
const tempRoot = path.join(dirname, "fixtures/configs/temp-glob");
296+
await fs.ensureDir(tempRoot);
297+
await fs.writeFile(path.join(tempRoot, "wds.js"), "module.exports = { extensions: ['.ts', '.js', '.mjs'] };");
298+
299+
const config = await projectConfig(tempRoot);
300+
301+
expect(config.includeGlob).toBe("**/*{.ts,.js,.mjs}");
302+
303+
await fs.remove(tempRoot);
304+
});
305+
});

spec/SwcCompiler.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ test("throws if the file is ignored", async () => {
4444
expect(error).toBeTruthy();
4545
expect(error?.ignoredFile).toBeTruthy();
4646
expect(error?.message).toMatch(
47-
/File .+ignored\.ts is imported but not being built because it is explicitly ignored in the wds project config\. It is being ignored by the provided glob pattern 'ignored\.ts', remove this pattern from the project config or don't import this file to fix./
47+
/File .+ignored\.ts is imported but not being built because it is explicitly ignored in the wds project config\. It is being ignored by the provided glob pattern '\*\*\/ignored\.ts', remove this pattern from the project config or don't import this file to fix./
4848
);
4949
});
5050

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
ignore: ["ignored/", "*.test.ts"]
3+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = {};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
ignore: ["../../some-external-file.ts", "../../../other-file.tsx", "../../tmp"]
3+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
extensions: [".ts", ".js"],
3+
ignore: ["**/*.spec.ts"]
4+
};

src/ProjectConfig.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export interface ProjectConfig {
1717
root: string;
1818
ignore: string[];
1919
includeGlob: string;
20+
/**
21+
* Checks if a file should be included in compilation.
22+
* Accepts both absolute and relative paths (relative to project root).
23+
* Returns false for files outside the project root.
24+
*/
2025
includedMatcher: (filePath: string) => boolean;
2126
swc?: SwcConfig;
2227
esm?: boolean;
@@ -66,9 +71,32 @@ export const projectConfig = _.memoize(async (root: string): Promise<ProjectConf
6671
}
6772

6873
// build inclusion glob and matcher
69-
result.ignore = _.uniq([`node_modules`, `**/*.d.ts`, `.git/**`, ...result.ignore]);
74+
// Normalize ignore patterns to ensure they work correctly with micromatch
75+
const normalizedIgnore = result.ignore.map((pattern) => {
76+
// If pattern doesn't start with **, ./, ../, or /, prepend **/ to match at any depth
77+
if (!pattern.startsWith("**/") && !pattern.startsWith("./") && !pattern.startsWith("../") && !pattern.startsWith("/")) {
78+
pattern = `**/${pattern}`;
79+
}
80+
if (pattern.endsWith("/")) {
81+
pattern = `${pattern}**`;
82+
}
83+
return pattern;
84+
});
85+
86+
result.ignore = _.uniq([`**/node_modules/**`, `**/*.d.ts`, `**/.git/**`, ...normalizedIgnore]);
7087
result.includeGlob = `**/*{${result.extensions.join(",")}}`;
71-
result.includedMatcher = micromatch.matcher(result.includeGlob, { cwd: result.root, ignore: result.ignore });
88+
89+
// Create a matcher that works with both absolute and relative paths
90+
const relativeMatcher = micromatch.matcher(result.includeGlob, { cwd: result.root, ignore: result.ignore });
91+
result.includedMatcher = (filePath: string) => {
92+
if (path.isAbsolute(filePath)) {
93+
const relativePath = path.relative(result.root, filePath);
94+
// Don't match files outside the project root
95+
if (relativePath.startsWith("..")) return false;
96+
return relativeMatcher(relativePath);
97+
}
98+
return relativeMatcher(filePath);
99+
};
72100

73101
return result;
74102
});

0 commit comments

Comments
 (0)