Skip to content

Commit e1ffae0

Browse files
authored
feat(watch): recursive deep level support (#422)
* fix(watch): improve deep level support * fix(watch): recursive deep level support * chore: remove debug * ci: include dynamic import test * chore: improve performance * chore: revert findMatchingFiles * ci: increase watch timer * ci: add tests * chore: use filter from CLI args * chore: improve logs * docs: adapt watch notes
1 parent e42fab9 commit e1ffae0

File tree

8 files changed

+384
-107
lines changed

8 files changed

+384
-107
lines changed

src/bin/index.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
/* c8 ignore start */
44

5+
import process from 'node:process';
56
import { escapeRegExp } from '../modules/list-files.js';
67
import {
78
getArg,
@@ -16,7 +17,7 @@ import { write } from '../helpers/logs.js';
1617
import { hr } from '../helpers/hr.js';
1718
import { mapTests, normalizePath } from '../services/map-tests.js';
1819
import { watch } from '../services/watch.js';
19-
import { poku } from '../modules/poku.js';
20+
import { onSigint, poku } from '../modules/poku.js';
2021
import { kill } from '../modules/processes.js';
2122
import type { Configs } from '../@types/poku.js';
2223

@@ -118,9 +119,10 @@ import type { Configs } from '../@types/poku.js';
118119
fileResults.fail.clear();
119120
};
120121

122+
process.removeListener('SIGINT', onSigint);
121123
resultsClear();
122124

123-
mapTests('.', dirs).then((mappedTests) => [
125+
mapTests('.', dirs, options.filter, options.exclude).then((mappedTests) => {
124126
Array.from(mappedTests.keys()).forEach((mappedTest) => {
125127
watch(mappedTest, (file, event) => {
126128
if (event === 'change') {
@@ -133,15 +135,15 @@ import type { Configs } from '../@types/poku.js';
133135
const tests = mappedTests.get(filePath);
134136
if (!tests) return;
135137

136-
poku(tests, options).then(() => {
138+
poku(Array.from(tests), options).then(() => {
137139
setTimeout(() => {
138140
executing.delete(filePath);
139141
}, interval);
140142
});
141143
}
142144
});
143-
}),
144-
]);
145+
});
146+
});
145147

146148
dirs.forEach((dir) => {
147149
watch(dir, (file, event) => {
@@ -161,7 +163,7 @@ import type { Configs } from '../@types/poku.js';
161163
});
162164

163165
hr();
164-
write(`Watching: ${dirs.join(', ')}`);
166+
write(`${format.bold('Watching:')} ${format.underline(dirs.join(', '))}`);
165167
}
166168
})();
167169

src/modules/list-files.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ const envFilter = process.env.FILTER?.trim()
3535

3636
export const getAllFiles = async (
3737
dirPath: string,
38-
files: string[] = [],
38+
files: Set<string> = new Set(),
3939
configs?: Configs
40-
): Promise<string[]> => {
40+
): Promise<Set<string>> => {
4141
const currentFiles = await readdir(sanitizePath(dirPath));
4242
const defaultRegExp = /\.(test|spec)\./i;
4343
/* c8 ignore start */
@@ -75,7 +75,7 @@ export const getAllFiles = async (
7575
}
7676
}
7777

78-
if (filter.test(fullPath)) return files.push(fullPath);
78+
if (filter.test(fullPath)) return files.add(fullPath);
7979
if (stat.isDirectory()) await getAllFiles(fullPath, files, configs);
8080
})
8181
);
@@ -84,6 +84,9 @@ export const getAllFiles = async (
8484
};
8585

8686
/* c8 ignore start */
87-
export const listFiles = async (targetDir: string, configs?: Configs) =>
88-
await getAllFiles(sanitizePath(targetDir), [], configs);
87+
export const listFiles = async (
88+
targetDir: string,
89+
configs?: Configs
90+
): Promise<string[]> =>
91+
Array.from(await getAllFiles(sanitizePath(targetDir), new Set(), configs));
8992
/* c8 ignore stop */

src/modules/poku.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ import { indentation } from '../configs/indentation.js';
1616
import type { Code } from '../@types/code.js';
1717
import type { Configs } from '../@types/poku.js';
1818

19-
process.once('SIGINT', () => {
19+
export const onSigint = () => {
2020
process.stdout.write('\u001B[?25h');
21-
});
21+
};
22+
23+
process.once('SIGINT', onSigint);
2224

2325
export async function poku(
2426
targetPaths: string | string[],

src/services/map-tests.ts

Lines changed: 133 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { relative, dirname, sep } from 'node:path';
33
import { stat, readFile } from '../polyfills/fs.js';
44
import { listFiles } from '../modules/list-files.js';
55

6-
const filter = /\.(js|cjs|mjs|ts|cts|mts)$/;
6+
const importMap = new Map<string, Set<string>>();
7+
const processedFiles = new Set<string>();
8+
9+
const extFilter = /\.(js|cjs|mjs|ts|cts|mts|jsx|tsx)$/;
710

811
export const normalizePath = (filePath: string) =>
912
filePath
@@ -12,43 +15,144 @@ export const normalizePath = (filePath: string) =>
1215
.replace(/[/\\]+/g, sep)
1316
.replace(/\\/g, '/');
1417

15-
/* c8 ignore next */
16-
export const mapTests = async (srcDir: string, testPaths: string[]) => {
17-
const allTestFiles: string[] = [];
18-
const allSrcFiles = await listFiles(srcDir, { filter });
19-
const importMap = new Map<string, string[]>();
18+
export const getDeepImports = (content: string): Set<string> => {
19+
const paths: Set<string> = new Set();
20+
const lines = content.split('\n');
21+
22+
for (const line of lines) {
23+
if (line.includes('import') || line.includes('require')) {
24+
const path = line.match(/['"](\.{1,2}\/[^'"]+)['"]/);
25+
26+
if (path) paths.add(normalizePath(path[1].replace(extFilter, '')));
27+
}
28+
}
29+
30+
return paths;
31+
};
32+
33+
export const findMatchingFiles = (
34+
srcFilesWithoutExt: Set<string>,
35+
srcFilesWithExt: Set<string>
36+
): Set<string> => {
37+
const matchingFiles = new Set<string>();
38+
39+
srcFilesWithoutExt.forEach((srcFile) => {
40+
const normalizedSrcFile = normalizePath(srcFile);
41+
42+
srcFilesWithExt.forEach((fileWithExt) => {
43+
const normalizedFileWithExt = normalizePath(fileWithExt);
44+
45+
if (normalizedFileWithExt.includes(normalizedSrcFile))
46+
matchingFiles.add(fileWithExt);
47+
});
48+
});
49+
50+
return matchingFiles;
51+
};
52+
53+
/* c8 ignore start */
54+
const collectTestFiles = async (
55+
testPaths: string[],
56+
testFilter?: RegExp,
57+
exclude?: RegExp | RegExp[]
58+
): Promise<Set<string>> => {
59+
const statsPromises = testPaths.map((testPath) => stat(testPath));
60+
61+
const stats = await Promise.all(statsPromises);
2062

21-
for (const testPath of testPaths) {
22-
const stats = await stat(testPath);
63+
const listFilesPromises = stats.map((stat, index) => {
64+
const testPath = testPaths[index];
2365

24-
if (stats.isDirectory()) {
25-
const testFiles = await listFiles(testPath, { filter });
66+
if (stat.isDirectory())
67+
return listFiles(testPath, {
68+
filter: testFilter,
69+
exclude,
70+
});
2671

27-
allTestFiles.push(...testFiles);
28-
} else if (stats.isFile() && filter.test(testPath))
29-
allTestFiles.push(testPath);
72+
if (stat.isFile() && extFilter.test(testPath)) return [testPath];
73+
else return [];
74+
});
75+
76+
const nestedTestFiles = await Promise.all(listFilesPromises);
77+
78+
return new Set(nestedTestFiles.flat());
79+
};
80+
/* c8 ignore stop */
81+
82+
/* c8 ignore start */
83+
const processDeepImports = async (
84+
srcFile: string,
85+
testFile: string,
86+
intersectedSrcFiles: Set<string>
87+
) => {
88+
if (processedFiles.has(srcFile)) return;
89+
processedFiles.add(srcFile);
90+
91+
const srcContent = await readFile(srcFile, 'utf-8');
92+
const deepImports = getDeepImports(srcContent);
93+
const matchingFiles = findMatchingFiles(deepImports, intersectedSrcFiles);
94+
95+
for (const deepImport of matchingFiles) {
96+
if (!importMap.has(deepImport)) importMap.set(deepImport, new Set());
97+
98+
importMap.get(deepImport)!.add(normalizePath(testFile));
99+
100+
await processDeepImports(deepImport, testFile, intersectedSrcFiles);
30101
}
102+
};
103+
/* c8 ignore stop */
104+
105+
const createImportMap = async (
106+
allTestFiles: Set<string>,
107+
allSrcFiles: Set<string>
108+
) => {
109+
const intersectedSrcFiles = new Set(
110+
Array.from(allSrcFiles).filter((srcFile) => !allTestFiles.has(srcFile))
111+
);
31112

32-
for (const testFile of allTestFiles) {
33-
const content = await readFile(testFile, 'utf-8');
113+
await Promise.all(
114+
Array.from(allTestFiles).map(async (testFile) => {
115+
const content = await readFile(testFile, 'utf-8');
34116

35-
for (const srcFile of allSrcFiles) {
36-
const relativePath = normalizePath(relative(dirname(testFile), srcFile));
37-
const normalizedSrcFile = normalizePath(srcFile);
117+
for (const srcFile of intersectedSrcFiles) {
118+
const relativePath = normalizePath(
119+
relative(dirname(testFile), srcFile)
120+
);
121+
const normalizedSrcFile = normalizePath(srcFile);
38122

39-
/* c8 ignore start */
40-
if (
41-
content.includes(relativePath.replace(filter, '')) ||
42-
content.includes(normalizedSrcFile)
43-
) {
44-
if (!importMap.has(normalizedSrcFile))
45-
importMap.set(normalizedSrcFile, []);
123+
/* c8 ignore start */
124+
if (
125+
content.includes(relativePath.replace(extFilter, '')) ||
126+
content.includes(normalizedSrcFile)
127+
) {
128+
if (!importMap.has(normalizedSrcFile))
129+
importMap.set(normalizedSrcFile, new Set());
130+
importMap.get(normalizedSrcFile)!.add(normalizePath(testFile));
46131

47-
importMap.get(normalizedSrcFile)!.push(normalizePath(testFile));
132+
await processDeepImports(srcFile, testFile, intersectedSrcFiles);
133+
}
134+
/* c8 ignore stop */
48135
}
49-
/* c8 ignore stop */
50-
}
51-
}
136+
})
137+
);
138+
};
139+
140+
/* c8 ignore next */
141+
export const mapTests = async (
142+
srcDir: string,
143+
testPaths: string[],
144+
testFilter?: RegExp,
145+
exclude?: RegExp | RegExp[]
146+
) => {
147+
const [allTestFiles, allSrcFiles] = await Promise.all([
148+
collectTestFiles(testPaths, testFilter, exclude),
149+
listFiles(srcDir, {
150+
filter: extFilter,
151+
exclude,
152+
}),
153+
]);
154+
155+
await createImportMap(allTestFiles, new Set(allSrcFiles));
52156

53157
return importMap;
54158
};

src/services/watch.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class Watcher {
5252
}
5353

5454
private async watchDirectory(dir: string) {
55+
/* c8 ignore next */
5556
if (this.dirWatchers.has(dir)) return;
5657

5758
const watcher = nodeWatch(dir, async (_, filename) => {
@@ -99,11 +100,7 @@ class Watcher {
99100
await this.watchDirectory(this.rootDir);
100101
} else this.watchFile(this.rootDir);
101102
/* c8 ignore start */
102-
} catch (err) {
103-
throw new Error(
104-
`Path does not exist or is not accessible: ${this.rootDir}`
105-
);
106-
}
103+
} catch {}
107104
/* c8 ignore stop */
108105
}
109106

0 commit comments

Comments
 (0)