qunitx-cli 0.9.4 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/bin/qunitx.js +71 -0
  2. package/dist/cli.js +2009 -0
  3. package/package.json +16 -7
  4. package/cli.ts +0 -39
  5. package/deno.json +0 -18
  6. package/deno.lock +0 -646
  7. package/lib/commands/generate.ts +0 -33
  8. package/lib/commands/help.ts +0 -37
  9. package/lib/commands/init.ts +0 -77
  10. package/lib/commands/run/tests-in-browser.ts +0 -221
  11. package/lib/commands/run.ts +0 -279
  12. package/lib/servers/http.ts +0 -321
  13. package/lib/setup/bind-server-to-port.ts +0 -14
  14. package/lib/setup/browser.ts +0 -101
  15. package/lib/setup/config.ts +0 -55
  16. package/lib/setup/default-project-config-values.ts +0 -9
  17. package/lib/setup/file-watcher.ts +0 -134
  18. package/lib/setup/fs-tree.ts +0 -60
  19. package/lib/setup/keyboard-events.ts +0 -38
  20. package/lib/setup/test-file-paths.ts +0 -74
  21. package/lib/setup/web-server.ts +0 -274
  22. package/lib/setup/write-output-static-files.ts +0 -33
  23. package/lib/tap/display-final-result.ts +0 -25
  24. package/lib/tap/display-test-result.ts +0 -109
  25. package/lib/tap/dump-yaml.ts +0 -84
  26. package/lib/types.ts +0 -61
  27. package/lib/utils/chromium-args.ts +0 -18
  28. package/lib/utils/color.ts +0 -66
  29. package/lib/utils/early-chrome.ts +0 -39
  30. package/lib/utils/find-chrome.ts +0 -38
  31. package/lib/utils/find-internal-assets-from-html.ts +0 -18
  32. package/lib/utils/find-project-root.ts +0 -20
  33. package/lib/utils/indent-string.ts +0 -24
  34. package/lib/utils/listen-to-keyboard-key.ts +0 -57
  35. package/lib/utils/parse-cli-flags.ts +0 -95
  36. package/lib/utils/path-exists.ts +0 -21
  37. package/lib/utils/perf-logger.ts +0 -25
  38. package/lib/utils/pre-launch-chrome.ts +0 -45
  39. package/lib/utils/read-boilerplate.ts +0 -15
  40. package/lib/utils/resolve-port-number-for.ts +0 -29
  41. package/lib/utils/run-user-module.ts +0 -28
  42. package/lib/utils/search-in-parent-directories.ts +0 -25
  43. package/lib/utils/time-counter.ts +0 -19
@@ -1,321 +0,0 @@
1
- import http from 'node:http';
2
- // @deno-types="npm:@types/ws"
3
- import WebSocket, { WebSocketServer } from 'ws';
4
- import bindServerToPort from '../setup/bind-server-to-port.ts';
5
-
6
- declare module 'node:http' {
7
- interface IncomingMessage {
8
- send: (data: string) => void;
9
- path: string;
10
- query: Record<string, string>;
11
- params: Record<string, string>;
12
- }
13
- interface ServerResponse {
14
- json: (data: unknown) => void;
15
- }
16
- }
17
-
18
- type NodeServerWithWSS = http.Server & { wss: WebSocketServer };
19
-
20
- /** Route handler function signature for registered GET/POST/etc. routes. */
21
- export type RouteHandler = (
22
- req: http.IncomingMessage,
23
- res: http.ServerResponse,
24
- ) => void | Promise<void>;
25
- /** Middleware function signature — call `next()` to continue the chain. */
26
- export type Middleware = (
27
- req: http.IncomingMessage,
28
- res: http.ServerResponse,
29
- next: () => void,
30
- ) => void;
31
-
32
- interface Route {
33
- path: string;
34
- handler: RouteHandler;
35
- paramNames: string[];
36
- isWildcard: boolean;
37
- paramValues?: string[];
38
- }
39
-
40
- /** Map of file extensions to their corresponding MIME type strings. */
41
- export const MIME_TYPES: Record<string, string> = {
42
- html: 'text/html; charset=UTF-8',
43
- js: 'application/javascript',
44
- css: 'text/css',
45
- png: 'image/png',
46
- jpg: 'image/jpg',
47
- gif: 'image/gif',
48
- ico: 'image/x-icon',
49
- svg: 'image/svg+xml',
50
- };
51
-
52
- /** Minimal HTTP + WebSocket server used to serve test bundles and push reload events. */
53
- export default class HTTPServer {
54
- /** Registered routes keyed by HTTP method then path. */
55
- routes: Record<string, Record<string, Route>>;
56
- /** Registered middleware functions, applied in order before each route handler. */
57
- middleware: Middleware[];
58
- /** Underlying Node.js HTTP server instance. */
59
- _server: http.Server;
60
- /** WebSocket server attached to the HTTP server for live-reload broadcasts. */
61
- wss: WebSocketServer;
62
-
63
- /**
64
- * Creates and starts a plain `http.createServer` instance on the given port.
65
- * @returns {Promise<object>}
66
- */
67
- static serve(
68
- config: { port: number; onListen?: (s: object) => void; onError?: (e: Error) => void } = {
69
- port: 1234,
70
- },
71
- handler: (req: http.IncomingMessage, res: http.ServerResponse) => void,
72
- ): Promise<http.Server> {
73
- const onListen = config.onListen || ((_server: object) => {});
74
- const onError = config.onError || ((_error: Error) => {});
75
-
76
- return new Promise((resolve, reject) => {
77
- const server = http.createServer((req, res) => {
78
- return handler(req, res);
79
- });
80
- server
81
- .on('error', (error) => {
82
- onError(error);
83
- reject(error);
84
- })
85
- .once('listening', () => {
86
- onListen(Object.assign({ hostname: '127.0.0.1', server }, config));
87
- resolve(server);
88
- });
89
- (server as NodeServerWithWSS).wss = new WebSocketServer({ server });
90
- (server as NodeServerWithWSS).wss.on('error', (error: Error) => {
91
- console.log('# [WebSocketServer] Error:');
92
- console.trace(error);
93
- });
94
-
95
- bindServerToPort(server as unknown as HTTPServer, config as { port: number });
96
- });
97
- }
98
-
99
- constructor() {
100
- this.routes = {
101
- GET: {},
102
- POST: {},
103
- DELETE: {},
104
- PUT: {},
105
- };
106
- this.middleware = [];
107
- this._server = http.createServer((req, res) => {
108
- req.send = (data: string) => {
109
- res.setHeader('Content-Type', 'text/plain');
110
- res.end(data);
111
- };
112
- res.json = (data: unknown) => {
113
- res.setHeader('Content-Type', 'application/json');
114
- res.end(JSON.stringify(data));
115
- };
116
-
117
- return this.#handleRequest(req, res);
118
- });
119
- this.wss = new WebSocketServer({ server: this._server });
120
- this.wss.on('error', (error) => {
121
- console.log('# [WebSocketServer] Error:');
122
- console.log(error);
123
- });
124
- }
125
-
126
- /**
127
- * Closes the underlying HTTP server and all active connections, returning a
128
- * Promise that resolves once the server is fully closed.
129
- * @returns {Promise<void>}
130
- */
131
- close(): Promise<void> {
132
- this._server.closeAllConnections?.();
133
- return new Promise((resolve) => this._server.close(resolve as () => void));
134
- }
135
-
136
- /** Registers a GET route handler. */
137
- get(path: string, handler: RouteHandler): void {
138
- this.#registerRouteHandler('GET', path, handler);
139
- }
140
-
141
- /**
142
- * Starts listening on the given port (0 = OS-assigned).
143
- * @returns {Promise<void>}
144
- */
145
- listen(port = 0, callback: () => void = () => {}): Promise<void> {
146
- return new Promise((resolve, reject) => {
147
- const onError = (err: Error) => {
148
- this._server.off('listening', onListening);
149
- reject(err);
150
- };
151
- const onListening = () => {
152
- this._server.off('error', onError);
153
- resolve(callback());
154
- };
155
- this._server.once('error', onError);
156
- this._server.once('listening', onListening);
157
- this._server.listen(port);
158
- });
159
- }
160
-
161
- /** Broadcasts a message to all connected WebSocket clients. */
162
- publish(data: string): void {
163
- this.wss.clients.forEach((client) => {
164
- if (client.readyState === WebSocket.OPEN) {
165
- client.send(data);
166
- }
167
- });
168
- }
169
-
170
- /** Registers a POST route handler. */
171
- post(path: string, handler: RouteHandler): void {
172
- this.#registerRouteHandler('POST', path, handler);
173
- }
174
-
175
- /** Registers a DELETE route handler. */
176
- delete(path: string, handler: RouteHandler): void {
177
- this.#registerRouteHandler('DELETE', path, handler);
178
- }
179
-
180
- /** Registers a PUT route handler. */
181
- put(path: string, handler: RouteHandler): void {
182
- this.#registerRouteHandler('PUT', path, handler);
183
- }
184
-
185
- /** Adds a middleware function to the chain. */
186
- use(middleware: Middleware): void {
187
- this.middleware.push(middleware);
188
- }
189
-
190
- #registerRouteHandler(method: string, path: string, handler: RouteHandler): void {
191
- if (!this.routes[method]) {
192
- this.routes[method] = {};
193
- }
194
-
195
- this.routes[method][path] = {
196
- path,
197
- handler,
198
- paramNames: this.#extractParamNames(path),
199
- isWildcard: path === '/*',
200
- };
201
- }
202
-
203
- #handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
204
- const { method, url } = req;
205
- const urlObj = new URL(url!, 'http://localhost');
206
- const pathname = urlObj.pathname;
207
- req.path = pathname;
208
- req.query = Object.fromEntries(urlObj.searchParams);
209
- const matchingRoute = this.#findRouteHandler(method!, pathname);
210
-
211
- if (matchingRoute) {
212
- req.params = this.#extractParams(matchingRoute, pathname);
213
- this.#runMiddleware(req, res, matchingRoute.handler);
214
- } else {
215
- res.statusCode = 404;
216
- res.setHeader('Content-Type', 'text/plain');
217
- res.end('Not found');
218
- }
219
- }
220
-
221
- #runMiddleware(
222
- req: http.IncomingMessage,
223
- res: http.ServerResponse,
224
- callback: RouteHandler,
225
- ): void {
226
- let index = 0;
227
- const next = () => {
228
- if (index >= this.middleware.length) {
229
- callback(req, res);
230
- } else {
231
- const middleware = this.middleware[index];
232
- index++;
233
- middleware(req, res, next);
234
- }
235
- };
236
- next();
237
- }
238
-
239
- #findRouteHandler(method: string, url: string): Route | null {
240
- const routes = this.routes[method];
241
- if (!routes) {
242
- return null;
243
- }
244
-
245
- return (
246
- routes[url] ||
247
- Object.values(routes).find((route) => {
248
- const { path, isWildcard } = route;
249
-
250
- if (!isWildcard && !path.includes(':')) {
251
- return false;
252
- }
253
-
254
- if (isWildcard || this.#matchPathSegments(path, url)) {
255
- if (route.paramNames.length > 0) {
256
- const regexPattern = this.#buildRegexPattern(path, route.paramNames);
257
- const regex = new RegExp(`^${regexPattern}$`);
258
- const regexMatches = regex.exec(url);
259
- if (regexMatches) {
260
- route.paramValues = regexMatches.slice(1);
261
- }
262
- }
263
- return true;
264
- }
265
-
266
- return false;
267
- }) ||
268
- routes['/*'] ||
269
- null
270
- );
271
- }
272
-
273
- #matchPathSegments(path: string, url: string): boolean {
274
- const pathSegments = path.split('/');
275
- const urlSegments = url.split('/');
276
-
277
- if (pathSegments.length !== urlSegments.length) {
278
- return false;
279
- }
280
-
281
- for (let i = 0; i < pathSegments.length; i++) {
282
- const pathSegment = pathSegments[i];
283
- const urlSegment = urlSegments[i];
284
-
285
- if (pathSegment.startsWith(':')) {
286
- continue;
287
- }
288
-
289
- if (pathSegment !== urlSegment) {
290
- return false;
291
- }
292
- }
293
-
294
- return true;
295
- }
296
-
297
- #buildRegexPattern(path: string, _paramNames: string[]): string {
298
- let regexPattern = path.replace(/:[^/]+/g, '([^/]+)');
299
- regexPattern = regexPattern.replace(/\//g, '\\/');
300
-
301
- return regexPattern;
302
- }
303
-
304
- #extractParamNames(path: string): string[] {
305
- const paramRegex = /:(\w+)/g;
306
- const paramMatches = path.match(paramRegex);
307
-
308
- return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
309
- }
310
-
311
- #extractParams(route: Route, _url: string): Record<string, string> {
312
- const { paramNames, paramValues } = route;
313
- const params: Record<string, string> = {};
314
-
315
- for (let i = 0; i < paramNames.length; i++) {
316
- params[paramNames[i]] = paramValues![i];
317
- }
318
-
319
- return params;
320
- }
321
- }
@@ -1,14 +0,0 @@
1
- import type HTTPServer from '../servers/http.ts';
2
-
3
- /**
4
- * Binds an HTTPServer to an OS-assigned port and writes the resolved port back to `config.port`.
5
- * @returns {Promise<object>}
6
- */
7
- export default async function bindServerToPort(
8
- server: HTTPServer,
9
- config: { port: number },
10
- ): Promise<HTTPServer> {
11
- await server.listen(0);
12
- config.port = (server._server.address() as import('node:net').AddressInfo).port;
13
- return server;
14
- }
@@ -1,101 +0,0 @@
1
- import setupWebServer from './web-server.ts';
2
- import bindServerToPort from './bind-server-to-port.ts';
3
- import findChrome from '../utils/find-chrome.ts';
4
- import CHROMIUM_ARGS from '../utils/chromium-args.ts';
5
- import { earlyBrowserPromise } from '../utils/early-chrome.ts';
6
- import { perfLog } from '../utils/perf-logger.ts';
7
- import type { Browser } from 'playwright-core';
8
- import type { Config, CachedContent, Connections } from '../types.ts';
9
-
10
- // Playwright-core starts loading the moment run.js imports this module.
11
- // browser.js is intentionally the first import in run.js so playwright-core
12
- // starts loading before heavier deps (esbuild, chokidar) queue up I/O reads
13
- // and saturate libuv's thread pool, which would delay the dynamic import resolution.
14
- // early-chrome.ts (statically imported by cli.ts) already started Chrome pre-launch,
15
- // so both race in parallel — Chrome is typically ready when playwright-core finishes.
16
- const playwrightCorePromise = import('playwright-core');
17
- perfLog('browser.js: playwright-core import started');
18
-
19
- /**
20
- * Launches a browser for the given config.browser type.
21
- * For chromium: connects via CDP to the pre-launched Chrome (fast path) or falls
22
- * back to chromium.launch() if pre-launch failed.
23
- * For firefox/webkit: uses playwright's standard launch (requires `npx playwright install [browser]`).
24
- * @returns {Promise<object>}
25
- */
26
- export async function launchBrowser(config: Config): Promise<Browser> {
27
- const browserName = config.browser || 'chromium';
28
-
29
- if (browserName === 'chromium') {
30
- const waitStart = Date.now();
31
- const [playwrightCore, earlyChrome] = await Promise.all([
32
- playwrightCorePromise,
33
- earlyBrowserPromise,
34
- ]);
35
- perfLog(
36
- `browser.js: playwright-core + earlyChrome resolved in ${Date.now() - waitStart}ms, earlyChrome:`,
37
- earlyChrome?.cdpEndpoint ?? null,
38
- );
39
-
40
- if (earlyChrome) {
41
- const connectStart = Date.now();
42
- const browser = await playwrightCore.chromium.connectOverCDP({
43
- endpointURL: earlyChrome.cdpEndpoint,
44
- });
45
- perfLog(`browser.js: connectOverCDP took ${Date.now() - connectStart}ms`);
46
- return browser;
47
- }
48
-
49
- // Pre-launch failed (Chrome not found, wrong version, etc.) — fall back to normal launch.
50
- const executablePath = await findChrome();
51
- const launchOptions = { args: CHROMIUM_ARGS, headless: true };
52
- if (executablePath) launchOptions.executablePath = executablePath;
53
- return playwrightCore.chromium.launch(launchOptions);
54
- }
55
-
56
- const playwrightCore = await playwrightCorePromise;
57
- return playwrightCore[browserName].launch({ headless: true });
58
- }
59
-
60
- /**
61
- * Launches a Playwright browser (or reuses an existing one), starts the web server, and returns the page/server/browser connection object.
62
- * @returns {Promise<{server: object, browser: object, page: object}>}
63
- */
64
- export default async function setupBrowser(
65
- config: Config,
66
- cachedContent: CachedContent,
67
- existingBrowser: Browser | null = null,
68
- ): Promise<Connections> {
69
- const setupStart = Date.now();
70
- const [server, resolvedExistingBrowser] = await Promise.all([
71
- setupWebServer(config, cachedContent),
72
- Promise.resolve(existingBrowser),
73
- ]);
74
- perfLog(`browser.js: setupWebServer took ${Date.now() - setupStart}ms`);
75
-
76
- const browser = resolvedExistingBrowser || (await launchBrowser(config));
77
-
78
- const pageStart = Date.now();
79
- const [page] = await Promise.all([browser.newPage(), bindServerToPort(server, config)]);
80
- perfLog(`browser.js: newPage + bindServerToPort took ${Date.now() - pageStart}ms`);
81
-
82
- await page.addInitScript(() => {
83
- window.IS_PLAYWRIGHT = true;
84
- });
85
-
86
- page.on('console', async (msg) => {
87
- if (!config.debug) return;
88
- try {
89
- const values = await Promise.all(msg.args().map((arg) => arg.jsonValue()));
90
- console.log(...values);
91
- } catch {
92
- console.log(msg.text());
93
- }
94
- });
95
- page.on('pageerror', (error) => {
96
- console.log(error.toString());
97
- console.error(error.toString());
98
- });
99
-
100
- return { server, browser, page };
101
- }
@@ -1,55 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import defaultProjectConfigValues from './default-project-config-values.ts';
3
- import findProjectRoot from '../utils/find-project-root.ts';
4
- import setupFSTree from './fs-tree.ts';
5
- import setupTestFilePaths from './test-file-paths.ts';
6
- import parseCliFlags from '../utils/parse-cli-flags.ts';
7
- import type { Config } from '../types.ts';
8
-
9
- /**
10
- * Builds the merged qunitx config from package.json settings and CLI flags.
11
- * @returns {Promise<object>}
12
- */
13
- export default async function setupConfig(): Promise<Config> {
14
- const projectRoot = await findProjectRoot();
15
- const cliConfigFlags = parseCliFlags(projectRoot);
16
- const projectPackageJSON = await readConfigFromPackageJSON(projectRoot);
17
- const inputs = cliConfigFlags.inputs.concat(readInputsFromPackageJSON(projectPackageJSON));
18
- const config = {
19
- ...defaultProjectConfigValues,
20
- htmlPaths: [] as string[],
21
- ...((projectPackageJSON.qunitx as Partial<Config>) || {}),
22
- ...cliConfigFlags,
23
- projectRoot,
24
- inputs,
25
- testFileLookupPaths: setupTestFilePaths(projectRoot, inputs),
26
- lastFailedTestFiles: null as string[] | null,
27
- lastRanTestFiles: null as string[] | null,
28
- COUNTER: { testCount: 0, failCount: 0, skipCount: 0, passCount: 0, errorCount: 0 },
29
- _testRunDone: null as (() => void) | null,
30
- _resetTestTimeout: null as (() => void) | null,
31
- };
32
- config.htmlPaths = normalizeHTMLPaths(config.projectRoot, config.htmlPaths);
33
- config.fsTree = await setupFSTree(config.testFileLookupPaths, config);
34
-
35
- return config as Config;
36
- }
37
-
38
- async function readConfigFromPackageJSON(projectRoot: string) {
39
- const packageJSON = await fs.readFile(`${projectRoot}/package.json`);
40
-
41
- return JSON.parse(packageJSON.toString()) as { qunitx?: unknown; [key: string]: unknown };
42
- }
43
-
44
- function normalizeHTMLPaths(projectRoot: string, htmlPaths: string[]): string[] {
45
- return Array.from(new Set(htmlPaths.map((htmlPath) => `${projectRoot}/${htmlPath}`)));
46
- }
47
-
48
- function readInputsFromPackageJSON(packageJSON: {
49
- qunitx?: unknown;
50
- [key: string]: unknown;
51
- }): string[] {
52
- const qunitx = packageJSON.qunitx as { inputs?: string[] } | undefined;
53
-
54
- return qunitx && qunitx.inputs ? qunitx.inputs : [];
55
- }
@@ -1,9 +0,0 @@
1
- /** Default qunitx config values: build output directory, test timeout (ms), fail-fast flag, HTTP server port, and tracked file extensions. */
2
- export default {
3
- output: 'tmp',
4
- timeout: 20000,
5
- failFast: false,
6
- port: 1234,
7
- extensions: ['js', 'ts'],
8
- browser: 'chromium',
9
- };
@@ -1,134 +0,0 @@
1
- import fs from 'node:fs';
2
- import { stat } from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { green, magenta, red, yellow } from '../utils/color.ts';
5
- import type { FSWatcher } from 'node:fs';
6
- import type { Config, FSTree } from '../types.ts';
7
-
8
- /**
9
- * Starts `fs.watch` watchers for each lookup path and calls `onEventFunc` on JS/TS file changes, debounced via a flag.
10
- * Uses `config.fsTree` to distinguish `unlink` (tracked file) from `unlinkDir` (directory) on deletion.
11
- * @returns {object}
12
- */
13
- export default function setupFileWatchers(
14
- testFileLookupPaths: string[],
15
- config: Config,
16
- onEventFunc: (event: string, file: string) => unknown,
17
- onFinishFunc: ((path: string, event: string) => void) | null | undefined,
18
- ): { fileWatchers: Record<string, FSWatcher>; killFileWatchers: () => Record<string, FSWatcher> } {
19
- const extensions = config.extensions || ['js', 'ts'];
20
- const fileWatchers = testFileLookupPaths.reduce((watchers, watchPath) => {
21
- let ready = false;
22
- const watcher = fs.watch(watchPath, { recursive: true }, async (eventType, filename) => {
23
- if (!ready || !filename) return;
24
- const fullPath = path.join(watchPath, filename);
25
- if (eventType === 'change') {
26
- return handleWatchEvent(config, extensions, 'change', fullPath, onEventFunc, onFinishFunc);
27
- }
28
- try {
29
- const s = await stat(fullPath);
30
- handleWatchEvent(
31
- config,
32
- extensions,
33
- s.isDirectory() ? 'addDir' : 'add',
34
- fullPath,
35
- onEventFunc,
36
- onFinishFunc,
37
- );
38
- } catch {
39
- const event = config.fsTree && fullPath in config.fsTree ? 'unlink' : 'unlinkDir';
40
- handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
41
- }
42
- });
43
- setImmediate(() => {
44
- ready = true;
45
- });
46
- return Object.assign(watchers, { [watchPath]: watcher });
47
- }, {});
48
-
49
- return {
50
- fileWatchers,
51
- killFileWatchers() {
52
- Object.keys(fileWatchers).forEach((watcherKey) => fileWatchers[watcherKey].close());
53
-
54
- return fileWatchers;
55
- },
56
- };
57
- }
58
-
59
- /**
60
- * Routes a file-system event to fsTree mutation and optional rebuild trigger.
61
- * `unlinkDir` bypasses the extension filter so deleted directories always clean up fsTree.
62
- * @returns {void}
63
- */
64
- export function handleWatchEvent(
65
- config: Config,
66
- extensions: string[],
67
- event: string,
68
- filePath: string,
69
- onEventFunc: (event: string, file: string) => unknown,
70
- onFinishFunc: ((path: string, event: string) => void) | null | undefined,
71
- ): void {
72
- const isFileEvent = extensions.some((ext) => filePath.endsWith(`.${ext}`));
73
-
74
- if (!isFileEvent && event !== 'unlinkDir') return;
75
-
76
- mutateFSTree(config.fsTree, event, filePath);
77
-
78
- console.log(
79
- '#',
80
- magenta().bold('=================================================================='),
81
- );
82
- console.log('#', getEventColor(event), filePath.split(config.projectRoot)[1]);
83
- console.log(
84
- '#',
85
- magenta().bold('=================================================================='),
86
- );
87
-
88
- if (!config._building) {
89
- config._building = true;
90
-
91
- const result = onEventFunc(event, filePath);
92
-
93
- if (!(result instanceof Promise)) {
94
- config._building = false;
95
-
96
- return result;
97
- }
98
-
99
- result
100
- .then(() => {
101
- onFinishFunc ? onFinishFunc(event, filePath) : null;
102
- })
103
- .catch((error) => {
104
- console.error('#', red('Build error:'), error.message || error);
105
- })
106
- .finally(() => (config._building = false));
107
- }
108
- }
109
-
110
- /**
111
- * Mutates `fsTree` in place based on a chokidar file-system event.
112
- * @returns {void}
113
- */
114
- export function mutateFSTree(fsTree: FSTree, event: string, path: string): void {
115
- if (event === 'add') {
116
- fsTree[path] = null;
117
- } else if (event === 'unlink') {
118
- delete fsTree[path];
119
- } else if (event === 'unlinkDir') {
120
- for (const treePath of Object.keys(fsTree)) {
121
- if (treePath.startsWith(path)) delete fsTree[treePath];
122
- }
123
- }
124
- }
125
-
126
- function getEventColor(event: string): unknown {
127
- if (event === 'change') {
128
- return yellow('CHANGED:');
129
- } else if (event === 'add' || event === 'addDir') {
130
- return green('ADDED:');
131
- } else if (event === 'unlink' || event === 'unlinkDir') {
132
- return red('REMOVED:');
133
- }
134
- }
@@ -1,60 +0,0 @@
1
- import fs, { glob as fsGlob } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import type { FSTree } from '../types.ts';
4
-
5
- function isGlob(str: string): boolean {
6
- return /[*?{[]/.test(str);
7
- }
8
-
9
- async function readDirRecursive(dir: string, filter: (name: string) => boolean): Promise<string[]> {
10
- const entries = await fs.readdir(dir, { recursive: true, withFileTypes: true });
11
- return entries
12
- .filter((e) => e.isFile() && filter(e.name))
13
- .map((e) => path.join(e.parentPath, e.name));
14
- }
15
-
16
- /**
17
- * Resolves an array of file paths, directories, or glob patterns into a flat `{ absolutePath: null }` map.
18
- * @returns {Promise<object>}
19
- */
20
- export default async function buildFSTree(
21
- fileAbsolutePaths: string[],
22
- config: { extensions?: string[] } = {},
23
- ): Promise<FSTree> {
24
- const targetExtensions = config.extensions || ['js', 'ts'];
25
- const fsTree = {};
26
-
27
- await Promise.all(
28
- fileAbsolutePaths.map(async (fileAbsolutePath) => {
29
- try {
30
- if (isGlob(fileAbsolutePath)) {
31
- for await (const fileName of fsGlob(fileAbsolutePath)) {
32
- if (targetExtensions.some((ext) => fileName.endsWith(`.${ext}`))) {
33
- fsTree[fileName] = null;
34
- }
35
- }
36
- } else {
37
- const entry = await fs.stat(fileAbsolutePath);
38
-
39
- if (entry.isFile()) {
40
- fsTree[fileAbsolutePath] = null;
41
- } else if (entry.isDirectory()) {
42
- const fileNames = await readDirRecursive(fileAbsolutePath, (name) => {
43
- return targetExtensions.some((extension) => name.endsWith(`.${extension}`));
44
- });
45
-
46
- fileNames.forEach((fileName) => {
47
- fsTree[fileName] = null;
48
- });
49
- }
50
- }
51
- } catch (error) {
52
- console.error(error);
53
-
54
- return process.exit(1);
55
- }
56
- }),
57
- );
58
-
59
- return fsTree;
60
- }