Skip to content

Commit

Permalink
feat(watch): recursive deep level support (#422)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
wellwelwel authored Jun 21, 2024
1 parent e42fab9 commit e1ffae0
Show file tree
Hide file tree
Showing 8 changed files with 384 additions and 107 deletions.
14 changes: 8 additions & 6 deletions src/bin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

/* c8 ignore start */

import process from 'node:process';
import { escapeRegExp } from '../modules/list-files.js';
import {
getArg,
Expand All @@ -16,7 +17,7 @@ import { write } from '../helpers/logs.js';
import { hr } from '../helpers/hr.js';
import { mapTests, normalizePath } from '../services/map-tests.js';
import { watch } from '../services/watch.js';
import { poku } from '../modules/poku.js';
import { onSigint, poku } from '../modules/poku.js';
import { kill } from '../modules/processes.js';
import type { Configs } from '../@types/poku.js';

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

process.removeListener('SIGINT', onSigint);
resultsClear();

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

poku(tests, options).then(() => {
poku(Array.from(tests), options).then(() => {
setTimeout(() => {
executing.delete(filePath);
}, interval);
});
}
});
}),
]);
});
});

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

hr();
write(`Watching: ${dirs.join(', ')}`);
write(`${format.bold('Watching:')} ${format.underline(dirs.join(', '))}`);
}
})();

Expand Down
13 changes: 8 additions & 5 deletions src/modules/list-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ const envFilter = process.env.FILTER?.trim()

export const getAllFiles = async (
dirPath: string,
files: string[] = [],
files: Set<string> = new Set(),
configs?: Configs
): Promise<string[]> => {
): Promise<Set<string>> => {
const currentFiles = await readdir(sanitizePath(dirPath));
const defaultRegExp = /\.(test|spec)\./i;
/* c8 ignore start */
Expand Down Expand Up @@ -75,7 +75,7 @@ export const getAllFiles = async (
}
}

if (filter.test(fullPath)) return files.push(fullPath);
if (filter.test(fullPath)) return files.add(fullPath);
if (stat.isDirectory()) await getAllFiles(fullPath, files, configs);
})
);
Expand All @@ -84,6 +84,9 @@ export const getAllFiles = async (
};

/* c8 ignore start */
export const listFiles = async (targetDir: string, configs?: Configs) =>
await getAllFiles(sanitizePath(targetDir), [], configs);
export const listFiles = async (
targetDir: string,
configs?: Configs
): Promise<string[]> =>
Array.from(await getAllFiles(sanitizePath(targetDir), new Set(), configs));
/* c8 ignore stop */
6 changes: 4 additions & 2 deletions src/modules/poku.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import { indentation } from '../configs/indentation.js';
import type { Code } from '../@types/code.js';
import type { Configs } from '../@types/poku.js';

process.once('SIGINT', () => {
export const onSigint = () => {
process.stdout.write('\u001B[?25h');
});
};

process.once('SIGINT', onSigint);

export async function poku(
targetPaths: string | string[],
Expand Down
162 changes: 133 additions & 29 deletions src/services/map-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { relative, dirname, sep } from 'node:path';
import { stat, readFile } from '../polyfills/fs.js';
import { listFiles } from '../modules/list-files.js';

const filter = /\.(js|cjs|mjs|ts|cts|mts)$/;
const importMap = new Map<string, Set<string>>();
const processedFiles = new Set<string>();

const extFilter = /\.(js|cjs|mjs|ts|cts|mts|jsx|tsx)$/;

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

/* c8 ignore next */
export const mapTests = async (srcDir: string, testPaths: string[]) => {
const allTestFiles: string[] = [];
const allSrcFiles = await listFiles(srcDir, { filter });
const importMap = new Map<string, string[]>();
export const getDeepImports = (content: string): Set<string> => {
const paths: Set<string> = new Set();
const lines = content.split('\n');

for (const line of lines) {
if (line.includes('import') || line.includes('require')) {
const path = line.match(/['"](\.{1,2}\/[^'"]+)['"]/);

if (path) paths.add(normalizePath(path[1].replace(extFilter, '')));
}
}

return paths;
};

export const findMatchingFiles = (
srcFilesWithoutExt: Set<string>,
srcFilesWithExt: Set<string>
): Set<string> => {
const matchingFiles = new Set<string>();

srcFilesWithoutExt.forEach((srcFile) => {
const normalizedSrcFile = normalizePath(srcFile);

srcFilesWithExt.forEach((fileWithExt) => {
const normalizedFileWithExt = normalizePath(fileWithExt);

if (normalizedFileWithExt.includes(normalizedSrcFile))
matchingFiles.add(fileWithExt);
});
});

return matchingFiles;
};

/* c8 ignore start */
const collectTestFiles = async (
testPaths: string[],
testFilter?: RegExp,
exclude?: RegExp | RegExp[]
): Promise<Set<string>> => {
const statsPromises = testPaths.map((testPath) => stat(testPath));

const stats = await Promise.all(statsPromises);

for (const testPath of testPaths) {
const stats = await stat(testPath);
const listFilesPromises = stats.map((stat, index) => {
const testPath = testPaths[index];

if (stats.isDirectory()) {
const testFiles = await listFiles(testPath, { filter });
if (stat.isDirectory())
return listFiles(testPath, {
filter: testFilter,
exclude,
});

allTestFiles.push(...testFiles);
} else if (stats.isFile() && filter.test(testPath))
allTestFiles.push(testPath);
if (stat.isFile() && extFilter.test(testPath)) return [testPath];
else return [];
});

const nestedTestFiles = await Promise.all(listFilesPromises);

return new Set(nestedTestFiles.flat());
};
/* c8 ignore stop */

/* c8 ignore start */
const processDeepImports = async (
srcFile: string,
testFile: string,
intersectedSrcFiles: Set<string>
) => {
if (processedFiles.has(srcFile)) return;
processedFiles.add(srcFile);

const srcContent = await readFile(srcFile, 'utf-8');
const deepImports = getDeepImports(srcContent);
const matchingFiles = findMatchingFiles(deepImports, intersectedSrcFiles);

for (const deepImport of matchingFiles) {
if (!importMap.has(deepImport)) importMap.set(deepImport, new Set());

importMap.get(deepImport)!.add(normalizePath(testFile));

await processDeepImports(deepImport, testFile, intersectedSrcFiles);
}
};
/* c8 ignore stop */

const createImportMap = async (
allTestFiles: Set<string>,
allSrcFiles: Set<string>
) => {
const intersectedSrcFiles = new Set(
Array.from(allSrcFiles).filter((srcFile) => !allTestFiles.has(srcFile))
);

for (const testFile of allTestFiles) {
const content = await readFile(testFile, 'utf-8');
await Promise.all(
Array.from(allTestFiles).map(async (testFile) => {
const content = await readFile(testFile, 'utf-8');

for (const srcFile of allSrcFiles) {
const relativePath = normalizePath(relative(dirname(testFile), srcFile));
const normalizedSrcFile = normalizePath(srcFile);
for (const srcFile of intersectedSrcFiles) {
const relativePath = normalizePath(
relative(dirname(testFile), srcFile)
);
const normalizedSrcFile = normalizePath(srcFile);

/* c8 ignore start */
if (
content.includes(relativePath.replace(filter, '')) ||
content.includes(normalizedSrcFile)
) {
if (!importMap.has(normalizedSrcFile))
importMap.set(normalizedSrcFile, []);
/* c8 ignore start */
if (
content.includes(relativePath.replace(extFilter, '')) ||
content.includes(normalizedSrcFile)
) {
if (!importMap.has(normalizedSrcFile))
importMap.set(normalizedSrcFile, new Set());
importMap.get(normalizedSrcFile)!.add(normalizePath(testFile));

importMap.get(normalizedSrcFile)!.push(normalizePath(testFile));
await processDeepImports(srcFile, testFile, intersectedSrcFiles);
}
/* c8 ignore stop */
}
/* c8 ignore stop */
}
}
})
);
};

/* c8 ignore next */
export const mapTests = async (
srcDir: string,
testPaths: string[],
testFilter?: RegExp,
exclude?: RegExp | RegExp[]
) => {
const [allTestFiles, allSrcFiles] = await Promise.all([
collectTestFiles(testPaths, testFilter, exclude),
listFiles(srcDir, {
filter: extFilter,
exclude,
}),
]);

await createImportMap(allTestFiles, new Set(allSrcFiles));

return importMap;
};
7 changes: 2 additions & 5 deletions src/services/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class Watcher {
}

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

const watcher = nodeWatch(dir, async (_, filename) => {
Expand Down Expand Up @@ -99,11 +100,7 @@ class Watcher {
await this.watchDirectory(this.rootDir);
} else this.watchFile(this.rootDir);
/* c8 ignore start */
} catch (err) {
throw new Error(
`Path does not exist or is not accessible: ${this.rootDir}`
);
}
} catch {}
/* c8 ignore stop */
}

Expand Down
Loading

0 comments on commit e1ffae0

Please sign in to comment.