poku
Version:
🐷 Poku makes testing easy for Node.js, Bun, Deno, and you at the same time.
216 lines (215 loc) • 9.03 kB
JavaScript
import process from "node:process";
import { listFiles, GLOBAL, getArg, onSigint, hr, log$1 as log, format, poku, errors, results, availableParallelism } from "../modules/_shared.js";
import { stat, readFile, readdir } from "node:fs/promises";
import { relative, dirname, join } from "node:path";
import { watch as watch$1 } from "node:fs";
import "node:os";
import "node:child_process";
import "node:assert";
import "node:assert/strict";
import "node:net";
const regex = {
extFilter: /\.(js|cjs|mjs|ts|cts|mts)$/,
dependency: /['"`](\.{1,2}\/[^'"`]+)['"`]/,
dotBar: /(\.\/)/g,
sep: /[/\\]+/g,
dot: /^\.+/
}, normalizePath = (filePath) => filePath.replace(regex.dotBar, "").replace(regex.dot, "").replace(regex.sep, "/"), getDeepImports = (content) => {
const paths = /* @__PURE__ */ new Set(), lines = content.split(`
`);
for (const line of lines)
if (line.indexOf("import") !== -1 || line.indexOf("require") !== -1 || line.indexOf(" from ") !== -1) {
const path = line.match(regex.dependency);
path && paths.add(normalizePath(path[1].replace(regex.extFilter, "")));
}
return paths;
}, findMatchingFiles = (srcFilesWithoutExt, srcFilesWithExt) => {
const matchingFiles = /* @__PURE__ */ new Set(), normalizedWithExtEntries = [];
for (const fileWithExt of srcFilesWithExt)
normalizedWithExtEntries.push({
normalized: normalizePath(fileWithExt),
original: fileWithExt
});
for (const srcFile of srcFilesWithoutExt) {
const normalizedSrcFile = normalizePath(srcFile);
for (const entry of normalizedWithExtEntries)
entry.normalized.indexOf(normalizedSrcFile) !== -1 && matchingFiles.add(entry.original);
}
return matchingFiles;
}, collectTestFiles = async (testPaths, testFilter, exclude) => {
const statsPromises = testPaths.map((testPath) => stat(testPath)), listFilesPromises = (await Promise.all(statsPromises)).map((stat2, index) => {
const testPath = testPaths[index];
return stat2.isDirectory() ? listFiles(testPath, {
filter: testFilter,
exclude
}) : stat2.isFile() && regex.extFilter.test(testPath) ? [testPath] : [];
}), nestedTestFiles = await Promise.all(listFilesPromises);
return new Set(nestedTestFiles.flat());
}, processDeepImports = async (srcFile, testFile, intersectedSrcFiles, importMap, processedFiles) => {
if (processedFiles.has(srcFile)) return;
processedFiles.add(srcFile);
const srcContent = await readFile(srcFile, "utf8"), deepImports = getDeepImports(srcContent), matchingFiles = findMatchingFiles(deepImports, intersectedSrcFiles);
for (const deepImport of matchingFiles)
importMap.has(deepImport) || importMap.set(deepImport, /* @__PURE__ */ new Set()), importMap.get(deepImport).add(normalizePath(testFile)), await processDeepImports(
deepImport,
testFile,
intersectedSrcFiles,
importMap,
processedFiles
);
}, createImportMap = async (allTestFiles, allSrcFiles, importMap, processedFiles) => {
const intersectedSrcFiles = new Set(
Array.from(allSrcFiles).filter((srcFile) => !allTestFiles.has(srcFile))
), normalizedSrcMap = /* @__PURE__ */ new Map();
for (const srcFile of intersectedSrcFiles)
normalizedSrcMap.set(srcFile, normalizePath(srcFile));
await Promise.all(
Array.from(allTestFiles).map(async (testFile) => {
const content = await readFile(testFile, "utf8");
for (const srcFile of intersectedSrcFiles) {
const relativePath = normalizePath(
relative(dirname(testFile), srcFile)
), normalizedSrcFile = normalizedSrcMap.get(srcFile);
(content.indexOf(relativePath.replace(regex.extFilter, "")) !== -1 || content.indexOf(normalizedSrcFile) !== -1) && (importMap.has(normalizedSrcFile) || importMap.set(normalizedSrcFile, /* @__PURE__ */ new Set()), importMap.get(normalizedSrcFile)?.add(normalizePath(testFile)), await processDeepImports(
srcFile,
testFile,
intersectedSrcFiles,
importMap,
processedFiles
));
}
})
);
}, mapTests = async (srcDir, testPaths, testFilter, exclude) => {
const importMap = /* @__PURE__ */ new Map(), processedFiles = /* @__PURE__ */ new Set(), [allTestFiles, allSrcFiles] = await Promise.all([
collectTestFiles(testPaths, testFilter, exclude),
listFiles(srcDir, {
filter: regex.extFilter,
exclude
})
]);
return await createImportMap(
allTestFiles,
new Set(allSrcFiles),
importMap,
processedFiles
), importMap;
};
class Watcher {
constructor(rootDir, callback) {
this.files = [], this.fileWatchers = /* @__PURE__ */ new Map(), this.dirWatchers = /* @__PURE__ */ new Map(), this.rootDir = rootDir, this.callback = callback;
}
watchFile(filePath) {
if (this.fileWatchers.has(filePath)) return;
const watcher = watch$1(
filePath,
(eventType) => this.callback(filePath, eventType)
);
this.fileWatchers.set(filePath, watcher);
}
unwatchFiles() {
for (const [filePath, watcher] of this.fileWatchers)
watcher.close(), this.fileWatchers.delete(filePath);
}
syncFileWatchers(filePaths) {
const next = new Set(filePaths);
for (const [filePath, watcher] of this.fileWatchers)
next.has(filePath) || (watcher.close(), this.fileWatchers.delete(filePath));
for (const filePath of filePaths) this.watchFile(filePath);
}
async watchDirectory(dir) {
if (this.dirWatchers.has(dir)) return;
const watcher = watch$1(dir, async (_, filename) => {
if (filename) {
const fullPath = join(dir, filename);
this.files = await listFiles(this.rootDir), this.syncFileWatchers(this.files);
try {
(await stat(fullPath)).isDirectory() && await this.watchDirectory(fullPath);
} catch {
}
}
});
this.dirWatchers.set(dir, watcher);
const entries = await readdir(dir, { withFileTypes: !0 });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const fullPath = join(dir, entry.name);
await this.watchDirectory(fullPath);
}
}
async start() {
if ((await stat(this.rootDir)).isDirectory()) {
this.files = await listFiles(this.rootDir), this.syncFileWatchers(this.files), await this.watchDirectory(this.rootDir);
return;
}
this.watchFile(this.rootDir);
}
stop() {
this.unwatchFiles(), this.unwatchDirectories();
}
unwatchDirectories() {
for (const [dirPath, watcher] of this.dirWatchers)
watcher.close(), this.dirWatchers.delete(dirPath);
}
}
const watch = async (path, callback) => {
const watcher = new Watcher(path, callback);
return await watcher.start(), watcher;
}, startWatch = async (dirs) => {
let isRunning = !1;
const { configs } = GLOBAL, watchers = /* @__PURE__ */ new Set(), executing = /* @__PURE__ */ new Set(), interval = Number(getArg("watchInterval")) || 1500, setIsRunning = (value) => {
isRunning = value;
}, resultsClear = () => {
errors.length = 0, results.passed = 0, results.failed = 0, results.skipped = 0, results.todo = 0;
}, listenStdin = async (input) => {
if (!(isRunning || executing.size > 0) && String(input).trim() === "rs") {
for (const watcher of watchers) watcher.stop();
watchers.clear(), resultsClear(), await poku(dirs);
}
};
process.stdin.removeListener("data", listenStdin), process.removeListener("SIGINT", onSigint), resultsClear();
const mappedTests = await mapTests(
".",
dirs,
configs.filter,
configs.exclude
);
for (const mappedTest of Array.from(mappedTests.keys()))
watch(mappedTest, async (file, event) => {
if (event === "change") {
const filePath = normalizePath(file);
if (executing.has(filePath) || isRunning || executing.size > 0) return;
setIsRunning(!0), executing.add(filePath), resultsClear();
const tests = mappedTests.get(filePath);
if (!tests) return;
await poku(Array.from(tests), {
...configs,
concurrency: configs.concurrency ?? Math.max(Math.floor(availableParallelism() / 2), 1)
}), setTimeout(() => {
executing.delete(filePath), setIsRunning(!1);
}, interval);
}
}).then((watcher) => {
watchers.add(watcher);
});
for (const dir of dirs)
watch(dir, (file, event) => {
if (event === "change") {
if (executing.has(file) || isRunning || executing.size > 0) return;
setIsRunning(!0), executing.add(file), resultsClear(), poku(file).then(
() => setTimeout(() => {
executing.delete(file), setIsRunning(!1);
}, interval)
);
}
}).then((watcher) => {
watchers.add(watcher);
});
hr(), log(`${format("Watching:").bold()} ${format(dirs.join(", ")).underline()}`), process.on("SIGTERM", () => {
for (const watcher of watchers) watcher.stop();
process.exit(0);
}), process.stdin.setEncoding("utf8"), process.stdin.on("data", listenStdin);
};
export {
startWatch
};