UNPKG

poku

Version:

🐷 Poku makes testing easy for Node.js, Bun, Deno, and you at the same time.

216 lines (215 loc) 9.03 kB
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 };