From 696617c59647c34c6445abdffa9aa30a1e071a46 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 3 Dec 2024 18:37:53 -0500 Subject: [PATCH 1/3] Add tests to ensure cross-workspace reloading works ok for swc in both lazy mode and not --- .gitignore | 4 +- .../reload-cross-workspace-lazy/main/.swcrc | 6 +++ .../main/package.json | 9 ++++ .../reload-cross-workspace-lazy/main/run.ts | 11 +++++ .../pnpm-lock.yaml | 15 ++++++ .../pnpm-workspace.yaml | 3 ++ .../reload-cross-workspace-lazy/side/.swcrc | 6 +++ .../side/package.json | 6 +++ .../reload-cross-workspace-lazy/side/run.ts | 1 + .../reload-cross-workspace-lazy/test.sh | 48 +++++++++++++++++++ .../reload-cross-workspace/main/package.json | 9 ++++ .../reload-cross-workspace/main/run.ts | 11 +++++ .../reload-cross-workspace/pnpm-lock.yaml | 15 ++++++ .../pnpm-workspace.yaml | 3 ++ .../reload-cross-workspace/side/package.json | 6 +++ .../reload-cross-workspace/side/run.ts | 1 + .../reload-cross-workspace/test.sh | 48 +++++++++++++++++++ package.json | 3 +- pnpm-lock.yaml | 23 +++++++++ src/index.ts | 7 +-- 20 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 integration-test/reload-cross-workspace-lazy/main/.swcrc create mode 100644 integration-test/reload-cross-workspace-lazy/main/package.json create mode 100644 integration-test/reload-cross-workspace-lazy/main/run.ts create mode 100644 integration-test/reload-cross-workspace-lazy/pnpm-lock.yaml create mode 100644 integration-test/reload-cross-workspace-lazy/pnpm-workspace.yaml create mode 100644 integration-test/reload-cross-workspace-lazy/side/.swcrc create mode 100644 integration-test/reload-cross-workspace-lazy/side/package.json create mode 100644 integration-test/reload-cross-workspace-lazy/side/run.ts create mode 100755 integration-test/reload-cross-workspace-lazy/test.sh create mode 100644 integration-test/reload-cross-workspace/main/package.json create mode 100644 integration-test/reload-cross-workspace/main/run.ts create mode 100644 integration-test/reload-cross-workspace/pnpm-lock.yaml create mode 100644 integration-test/reload-cross-workspace/pnpm-workspace.yaml create mode 100644 integration-test/reload-cross-workspace/side/package.json create mode 100644 integration-test/reload-cross-workspace/side/run.ts create mode 100755 integration-test/reload-cross-workspace/test.sh diff --git a/.gitignore b/.gitignore index 249eb08..8271dde 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ node_modules pkg .envrc.local -test/supervise/run-scratch.ts -test/reload/run-scratch.ts +**/run-scratch.ts +**/run-scratch.ts .direnv diff --git a/integration-test/reload-cross-workspace-lazy/main/.swcrc b/integration-test/reload-cross-workspace-lazy/main/.swcrc new file mode 100644 index 0000000..39078d8 --- /dev/null +++ b/integration-test/reload-cross-workspace-lazy/main/.swcrc @@ -0,0 +1,6 @@ +{ + "module": { + "type": "commonjs", + "lazy": true + } +} diff --git a/integration-test/reload-cross-workspace-lazy/main/package.json b/integration-test/reload-cross-workspace-lazy/main/package.json new file mode 100644 index 0000000..d6b97af --- /dev/null +++ b/integration-test/reload-cross-workspace-lazy/main/package.json @@ -0,0 +1,9 @@ +{ + "name": "main", + "version": "1.0.0", + "main": "index.js", + "type": "commonjs", + "dependencies": { + "side": "workspace:*" + } +} diff --git a/integration-test/reload-cross-workspace-lazy/main/run.ts b/integration-test/reload-cross-workspace-lazy/main/run.ts new file mode 100644 index 0000000..b4ba4fe --- /dev/null +++ b/integration-test/reload-cross-workspace-lazy/main/run.ts @@ -0,0 +1,11 @@ +import http from "http"; +import { message } from "side/run-scratch"; + +const requestListener = function (req, res) { + res.writeHead(200); + res.end(message); +}; + +const server = http.createServer(requestListener); +server.listen(8080); +console.warn("Listening on 8080"); diff --git a/integration-test/reload-cross-workspace-lazy/pnpm-lock.yaml b/integration-test/reload-cross-workspace-lazy/pnpm-lock.yaml new file mode 100644 index 0000000..4039f01 --- /dev/null +++ b/integration-test/reload-cross-workspace-lazy/pnpm-lock.yaml @@ -0,0 +1,15 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + main: + dependencies: + side: + specifier: workspace:* + version: link:../side + + side: {} diff --git a/integration-test/reload-cross-workspace-lazy/pnpm-workspace.yaml b/integration-test/reload-cross-workspace-lazy/pnpm-workspace.yaml new file mode 100644 index 0000000..20fd020 --- /dev/null +++ b/integration-test/reload-cross-workspace-lazy/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "main" + - "side" \ No newline at end of file diff --git a/integration-test/reload-cross-workspace-lazy/side/.swcrc b/integration-test/reload-cross-workspace-lazy/side/.swcrc new file mode 100644 index 0000000..39078d8 --- /dev/null +++ b/integration-test/reload-cross-workspace-lazy/side/.swcrc @@ -0,0 +1,6 @@ +{ + "module": { + "type": "commonjs", + "lazy": true + } +} diff --git a/integration-test/reload-cross-workspace-lazy/side/package.json b/integration-test/reload-cross-workspace-lazy/side/package.json new file mode 100644 index 0000000..ae75029 --- /dev/null +++ b/integration-test/reload-cross-workspace-lazy/side/package.json @@ -0,0 +1,6 @@ +{ + "name": "side", + "version": "1.0.0", + "main": "index.js", + "type": "commonjs" +} diff --git a/integration-test/reload-cross-workspace-lazy/side/run.ts b/integration-test/reload-cross-workspace-lazy/side/run.ts new file mode 100644 index 0000000..12c37a0 --- /dev/null +++ b/integration-test/reload-cross-workspace-lazy/side/run.ts @@ -0,0 +1 @@ +export const message = "Hello, World!"; diff --git a/integration-test/reload-cross-workspace-lazy/test.sh b/integration-test/reload-cross-workspace-lazy/test.sh new file mode 100755 index 0000000..d2f4d3d --- /dev/null +++ b/integration-test/reload-cross-workspace-lazy/test.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +set -e + + +# kill the server when this script exits +trap "kill -9 0" INT TERM +trap 'kill $(jobs -p)' EXIT + +# setup the pnpm workspace with multiple packages +cd $DIR +pnpm install + +# make a copy of the run.ts file in the side package for us to modify +cp $DIR/side/run.ts $DIR/side/run-scratch.ts + +# run a server in the main package in the background +$DIR/../../pkg/wds.bin.js $@ --watch --commands $DIR/main/run.ts & + +max_retry=5 +counter=0 + +set +e +until curl -s localhost:8080 | grep "World" +do + sleep 1 + [[ counter -eq $max_retry ]] && echo "Failed!" && exit 1 + echo "Trying again. Try #$counter" + ((counter++)) +done + +echo "Made initial request to server" + +# modify the file in the side package and expect the main script to reload +sed -i 's/Hello, World/Hey, Pluto/g' $DIR/side/run-scratch.ts + +counter=0 +until curl -s localhost:8080 | grep "Pluto" +do + sleep 1 + [[ counter -eq $max_retry ]] && echo "Failed!" && exit 1 + echo "Trying again. Try #$counter" + ((counter++)) +done + +echo "Made new request to reloaded server" + +exit 0 diff --git a/integration-test/reload-cross-workspace/main/package.json b/integration-test/reload-cross-workspace/main/package.json new file mode 100644 index 0000000..514359f --- /dev/null +++ b/integration-test/reload-cross-workspace/main/package.json @@ -0,0 +1,9 @@ +{ + "name": "main", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "dependencies": { + "side": "workspace:*" + } +} diff --git a/integration-test/reload-cross-workspace/main/run.ts b/integration-test/reload-cross-workspace/main/run.ts new file mode 100644 index 0000000..b4ba4fe --- /dev/null +++ b/integration-test/reload-cross-workspace/main/run.ts @@ -0,0 +1,11 @@ +import http from "http"; +import { message } from "side/run-scratch"; + +const requestListener = function (req, res) { + res.writeHead(200); + res.end(message); +}; + +const server = http.createServer(requestListener); +server.listen(8080); +console.warn("Listening on 8080"); diff --git a/integration-test/reload-cross-workspace/pnpm-lock.yaml b/integration-test/reload-cross-workspace/pnpm-lock.yaml new file mode 100644 index 0000000..4039f01 --- /dev/null +++ b/integration-test/reload-cross-workspace/pnpm-lock.yaml @@ -0,0 +1,15 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + main: + dependencies: + side: + specifier: workspace:* + version: link:../side + + side: {} diff --git a/integration-test/reload-cross-workspace/pnpm-workspace.yaml b/integration-test/reload-cross-workspace/pnpm-workspace.yaml new file mode 100644 index 0000000..20fd020 --- /dev/null +++ b/integration-test/reload-cross-workspace/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "main" + - "side" \ No newline at end of file diff --git a/integration-test/reload-cross-workspace/side/package.json b/integration-test/reload-cross-workspace/side/package.json new file mode 100644 index 0000000..6aa5b1f --- /dev/null +++ b/integration-test/reload-cross-workspace/side/package.json @@ -0,0 +1,6 @@ +{ + "name": "side", + "version": "1.0.0", + "main": "index.js", + "type": "module" +} diff --git a/integration-test/reload-cross-workspace/side/run.ts b/integration-test/reload-cross-workspace/side/run.ts new file mode 100644 index 0000000..12c37a0 --- /dev/null +++ b/integration-test/reload-cross-workspace/side/run.ts @@ -0,0 +1 @@ +export const message = "Hello, World!"; diff --git a/integration-test/reload-cross-workspace/test.sh b/integration-test/reload-cross-workspace/test.sh new file mode 100755 index 0000000..d2f4d3d --- /dev/null +++ b/integration-test/reload-cross-workspace/test.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +set -e + + +# kill the server when this script exits +trap "kill -9 0" INT TERM +trap 'kill $(jobs -p)' EXIT + +# setup the pnpm workspace with multiple packages +cd $DIR +pnpm install + +# make a copy of the run.ts file in the side package for us to modify +cp $DIR/side/run.ts $DIR/side/run-scratch.ts + +# run a server in the main package in the background +$DIR/../../pkg/wds.bin.js $@ --watch --commands $DIR/main/run.ts & + +max_retry=5 +counter=0 + +set +e +until curl -s localhost:8080 | grep "World" +do + sleep 1 + [[ counter -eq $max_retry ]] && echo "Failed!" && exit 1 + echo "Trying again. Try #$counter" + ((counter++)) +done + +echo "Made initial request to server" + +# modify the file in the side package and expect the main script to reload +sed -i 's/Hello, World/Hey, Pluto/g' $DIR/side/run-scratch.ts + +counter=0 +until curl -s localhost:8080 | grep "Pluto" +do + sleep 1 + [[ counter -eq $max_retry ]] && echo "Failed!" && exit 1 + echo "Trying again. Try #$counter" + ((counter++)) +done + +echo "Made new request to reloaded server" + +exit 0 diff --git a/package.json b/package.json index fc88d33..c606509 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "node": ">=16.0.0" }, "dependencies": { + "@pnpm/find-workspace-dir": "^1000.0.0", "@swc/core": "^1.9.3", "@swc/helpers": "^0.5.13", "find-root": "^1.1.0", @@ -47,8 +48,8 @@ "fs-extra": "^11.2.0", "globby": "^11.1.0", "lodash": "^4.17.20", - "oxc-resolver": "^1.12.0", "node-object-hash": "^3.0.0", + "oxc-resolver": "^1.12.0", "pkg-dir": "^5.0.0", "watcher": "^2.3.1", "write-file-atomic": "^6.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b737988..7e3cd95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@pnpm/find-workspace-dir': + specifier: ^1000.0.0 + version: 1000.0.0 '@swc/core': specifier: ^1.9.3 version: 1.9.3(@swc/helpers@0.5.13) @@ -572,6 +575,26 @@ packages: dev: false optional: true + /@pnpm/constants@1000.0.0: + resolution: {integrity: sha512-BvWyUBVRI8Vj9QSaGBQZuwy/iH9XiAba4bIAziV+jw9OP7TYfx05pdbDqJZyLBBTzchj4PGTmm1LnUarjZOA+g==} + engines: {node: '>=18.12'} + dev: false + + /@pnpm/error@1000.0.0: + resolution: {integrity: sha512-2umtIxzA2HtjjubZqaiKUpvFV0e9ac/gYW7oa2atxjjKMMIxWOOHjSpmIiev4bQQ5bQi/Sigs0b34Ra48VMpcw==} + engines: {node: '>=18.12'} + dependencies: + '@pnpm/constants': 1000.0.0 + dev: false + + /@pnpm/find-workspace-dir@1000.0.0: + resolution: {integrity: sha512-JLNYdBGHQqw+HE/Cod73io+gcPN6GQ0rgSmkjUSuTS3DmFM4CdNsvl5aCRz8wHZ5ZsFRHJwzIWHfZxcB63jHWA==} + engines: {node: '>=18.12'} + dependencies: + '@pnpm/error': 1000.0.0 + find-up: 5.0.0 + dev: false + /@rollup/rollup-android-arm-eabi@4.22.5: resolution: {integrity: sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==} cpu: [arm] diff --git a/src/index.ts b/src/index.ts index cf341e5..2b564d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ +import { findWorkspaceDir as findPnpmWorkspaceRoot } from "@pnpm/find-workspace-dir"; import findRoot from "find-root"; -import findWorkspaceRoot from "find-yarn-workspace-root"; +import findYarnWorkspaceRoot from "find-yarn-workspace-root"; import fs from "fs-extra"; import os from "os"; import path from "path"; @@ -159,10 +160,10 @@ export const wds = async (options: RunOptions) => { if (firstNonOptionArg && fs.existsSync(firstNonOptionArg)) { const absolutePath = path.resolve(firstNonOptionArg); projectRoot = findRoot(path.dirname(absolutePath)); - workspaceRoot = findWorkspaceRoot(projectRoot) || projectRoot; + workspaceRoot = (await findPnpmWorkspaceRoot(projectRoot)) || findYarnWorkspaceRoot(projectRoot) || projectRoot; } else { projectRoot = findRoot(process.cwd()); - workspaceRoot = findWorkspaceRoot(process.cwd()) || process.cwd(); + workspaceRoot = (await findPnpmWorkspaceRoot(process.cwd())) || findYarnWorkspaceRoot(process.cwd()) || process.cwd(); } let serverSocketPath: string; From 2a056042933533812a85c1ddfc998d3c0ca9dddc Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 3 Dec 2024 19:15:50 -0500 Subject: [PATCH 2/3] Absolutize ignored files so the comparison against the watcher yielded files works --- src/Options.ts | 17 -------------- src/Project.ts | 2 +- src/ProjectConfig.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++ src/Supervisor.ts | 2 +- src/SwcCompiler.ts | 4 ++-- src/index.ts | 11 +++++---- src/utils.ts | 37 ----------------------------- 7 files changed, 67 insertions(+), 62 deletions(-) delete mode 100644 src/Options.ts create mode 100644 src/ProjectConfig.ts diff --git a/src/Options.ts b/src/Options.ts deleted file mode 100644 index 3e97f39..0000000 --- a/src/Options.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Options as SwcOptions } from "@swc/core"; - -export type SwcConfig = string | SwcOptions; - -export interface RunOptions { - argv: string[]; - terminalCommands: boolean; - reloadOnChanges: boolean; -} - -export interface ProjectConfig { - ignore: string[]; - swc?: SwcConfig; - esm?: boolean; - extensions: string[]; - cacheDir: string; -} diff --git a/src/Project.ts b/src/Project.ts index d9c1a43..278231a 100644 --- a/src/Project.ts +++ b/src/Project.ts @@ -1,7 +1,7 @@ import _ from "lodash"; import type { Compiler } from "./Compiler.js"; -import type { ProjectConfig } from "./Options.js"; import { PathTrie } from "./PathTrie.js"; +import type { ProjectConfig } from "./ProjectConfig.js"; import type { Supervisor } from "./Supervisor.js"; import { log } from "./utils.js"; diff --git a/src/ProjectConfig.ts b/src/ProjectConfig.ts new file mode 100644 index 0000000..a58eb93 --- /dev/null +++ b/src/ProjectConfig.ts @@ -0,0 +1,56 @@ +import type { Options as SwcOptions } from "@swc/core"; +import fs from "fs-extra"; +import _ from "lodash"; +import path from "path"; +import { log } from "./utils.js"; + +export type SwcConfig = string | SwcOptions; + +export interface RunOptions { + argv: string[]; + terminalCommands: boolean; + reloadOnChanges: boolean; +} + +export interface ProjectConfig { + ignore: string[]; + swc?: SwcConfig; + esm?: boolean; + extensions: string[]; + cacheDir: string; +} + +export const projectConfig = async (root: string): Promise => { + const location = path.join(root, "wds.js"); + const base: ProjectConfig = { + ignore: [], + extensions: [".ts", ".tsx", ".jsx"], + cacheDir: path.join(root, "node_modules/.cache/wds"), + esm: true, + }; + + try { + await fs.access(location); + } catch (error: any) { + log.debug(`Not loading project config from ${location}`); + return base; + } + + let required = await import(location); + if (required.default) { + required = required.default; + } + log.debug(`Loaded project config from ${location}`); + const result = _.defaults(required, base); + + const projectRootDir = path.dirname(location); + // absolutize the cacheDir if not already + if (!result.cacheDir.startsWith("/")) { + result.cacheDir = path.resolve(projectRootDir, result.cacheDir); + } + + // absolutize the ignore paths if not already + result.ignore = result.ignore.map((p: string) => (p.startsWith("/") ? p : path.resolve(projectRootDir, p))); + + return result; +}; diff --git a/src/Supervisor.ts b/src/Supervisor.ts index 8143787..25baecd 100644 --- a/src/Supervisor.ts +++ b/src/Supervisor.ts @@ -2,8 +2,8 @@ import type { ChildProcess, StdioOptions } from "child_process"; import { spawn } from "child_process"; import { EventEmitter, once } from "events"; import { setTimeout } from "timers/promises"; -import type { RunOptions } from "./Options.js"; import type { Project } from "./Project.js"; +import type { RunOptions } from "./ProjectConfig.js"; import { log } from "./utils.js"; /** */ diff --git a/src/SwcCompiler.ts b/src/SwcCompiler.ts index bb9f4c9..959d820 100644 --- a/src/SwcCompiler.ts +++ b/src/SwcCompiler.ts @@ -13,8 +13,8 @@ import path from "path"; import { fileURLToPath } from "url"; import writeFileAtomic from "write-file-atomic"; import type { Compiler } from "./Compiler.js"; -import type { ProjectConfig } from "./Options.js"; -import { log, projectConfig } from "./utils.js"; +import { projectConfig, type ProjectConfig } from "./ProjectConfig.js"; +import { log } from "./utils.js"; const __filename = fileURLToPath(import.meta.url); const require = createRequire(import.meta.url); diff --git a/src/index.ts b/src/index.ts index 2b564d2..3e100d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,12 +9,12 @@ import { fileURLToPath } from "url"; import Watcher from "watcher"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; -import type { ProjectConfig, RunOptions } from "./Options.js"; import { Project } from "./Project.js"; +import { projectConfig, type ProjectConfig, type RunOptions } from "./ProjectConfig.js"; import { Supervisor } from "./Supervisor.js"; import { MissingDestinationError, SwcCompiler } from "./SwcCompiler.js"; import { MiniServer } from "./mini-server.js"; -import { log, projectConfig } from "./utils.js"; +import { log } from "./utils.js"; const dirname = fileURLToPath(new URL(".", import.meta.url)); @@ -63,15 +63,18 @@ const startTerminalCommandListener = (project: Project) => { return reader; }; +const gitDir = `${path.sep}.git${path.sep}`; +const nodeModulesDir = `${path.sep}node_modules${path.sep}`; + const startFilesystemWatcher = (project: Project) => { const watcher = new Watcher([project.workspaceRoot], { ignoreInitial: true, recursive: true, ignore: ((filePath: string) => { - if (filePath.includes("node_modules")) return true; + if (filePath.includes(nodeModulesDir)) return true; if (filePath.endsWith(".d.ts")) return true; if (filePath.endsWith(".map")) return true; - if (filePath.endsWith(".git")) return true; + if (filePath.includes(gitDir)) return true; if (filePath.endsWith(".DS_Store")) return true; if (filePath.endsWith(".tsbuildinfo")) return true; diff --git a/src/utils.ts b/src/utils.ts index f99dfc1..5a516c3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,10 +1,4 @@ -import fs from "fs-extra"; -import _ from "lodash"; -import path from "path"; import { threadId } from "worker_threads"; -// @ts-expect-error see https://github.com/microsoft/TypeScript/issues/52529, can't import types from .cts to .ts files that are ESM -import type { ProjectConfig } from "./Options"; - const logPrefix = `[wds pid=${process.pid} thread=${threadId}]`; export const log = { debug: (...args: any[]) => process.env["WDS_DEBUG"] && console.warn(logPrefix, ...args), @@ -20,34 +14,3 @@ export async function time(run: () => Promise) { return (diff[0] + diff[1] / 1e9).toFixed(5); } - -export const projectConfig = async (root: string): Promise => { - const location = path.join(root, "wds.js"); - const base: ProjectConfig = { - ignore: [], - extensions: [".ts", ".tsx", ".jsx"], - cacheDir: path.join(root, "node_modules/.cache/wds"), - esm: true, - }; - - try { - await fs.access(location); - } catch (error: any) { - log.debug(`Not loading project config from ${location}`); - return base; - } - - let required = await import(location); - if (required.default) { - required = required.default; - } - log.debug(`Loaded project config from ${location}`); - const result = _.defaults(required, base); - - // absolutize the cacheDir if not already - if (!result.cacheDir.startsWith("/")) { - result.cacheDir = path.resolve(path.dirname(location), result.cacheDir); - } - - return result; -}; From 373ec4cf77bea8a152d5672745f2bc1c82f6a41f Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 3 Dec 2024 19:54:15 -0500 Subject: [PATCH 3/3] Cleanup glob handling and do ignore checks in memory using micromatch --- .../reload-cross-workspace-lazy/test.sh | 2 + .../reload-cross-workspace/test.sh | 2 + package.json | 2 + pnpm-lock.yaml | 16 +++++++ spec/SwcCompiler.test.ts | 4 +- src/ProjectConfig.ts | 40 +++++++++++++----- src/SwcCompiler.ts | 42 ++++++++----------- src/index.ts | 9 ++-- 8 files changed, 77 insertions(+), 40 deletions(-) diff --git a/integration-test/reload-cross-workspace-lazy/test.sh b/integration-test/reload-cross-workspace-lazy/test.sh index d2f4d3d..e0ecbff 100755 --- a/integration-test/reload-cross-workspace-lazy/test.sh +++ b/integration-test/reload-cross-workspace-lazy/test.sh @@ -34,6 +34,8 @@ echo "Made initial request to server" # modify the file in the side package and expect the main script to reload sed -i 's/Hello, World/Hey, Pluto/g' $DIR/side/run-scratch.ts +echo "Made change to side package" + counter=0 until curl -s localhost:8080 | grep "Pluto" do diff --git a/integration-test/reload-cross-workspace/test.sh b/integration-test/reload-cross-workspace/test.sh index d2f4d3d..e0ecbff 100755 --- a/integration-test/reload-cross-workspace/test.sh +++ b/integration-test/reload-cross-workspace/test.sh @@ -34,6 +34,8 @@ echo "Made initial request to server" # modify the file in the side package and expect the main script to reload sed -i 's/Hello, World/Hey, Pluto/g' $DIR/side/run-scratch.ts +echo "Made change to side package" + counter=0 until curl -s localhost:8080 | grep "Pluto" do diff --git a/package.json b/package.json index c606509..d60e118 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "fs-extra": "^11.2.0", "globby": "^11.1.0", "lodash": "^4.17.20", + "micromatch": "^4.0.8", "node-object-hash": "^3.0.0", "oxc-resolver": "^1.12.0", "pkg-dir": "^5.0.0", @@ -62,6 +63,7 @@ "@types/find-root": "^1.1.4", "@types/fs-extra": "^11.0.4", "@types/lodash": "^4.17.13", + "@types/micromatch": "^4.0.9", "@types/node": "^22.9.3", "@types/write-file-atomic": "^4.0.3", "@types/yargs": "^15.0.19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e3cd95..efce6ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ dependencies: lodash: specifier: ^4.17.20 version: 4.17.21 + micromatch: + specifier: ^4.0.8 + version: 4.0.8 node-object-hash: specifier: ^3.0.0 version: 3.0.0 @@ -67,6 +70,9 @@ devDependencies: '@types/lodash': specifier: ^4.17.13 version: 4.17.13 + '@types/micromatch': + specifier: ^4.0.9 + version: 4.0.9 '@types/node': specifier: ^22.9.3 version: 22.9.3 @@ -863,6 +869,10 @@ packages: dev: false optional: true + /@types/braces@3.0.4: + resolution: {integrity: sha512-0WR3b8eaISjEW7RpZnclONaLFDf7buaowRHdqLp4vLj54AsSAYWfh3DRbfiYJY9XDxMgx1B4sE1Afw2PGpuHOA==} + dev: true + /@types/estree@1.0.6: resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} dev: true @@ -896,6 +906,12 @@ packages: resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} dev: true + /@types/micromatch@4.0.9: + resolution: {integrity: sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==} + dependencies: + '@types/braces': 3.0.4 + dev: true + /@types/minimist@1.2.5: resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} dev: true diff --git a/spec/SwcCompiler.test.ts b/spec/SwcCompiler.test.ts index 7c676fb..b6e0509 100644 --- a/spec/SwcCompiler.test.ts +++ b/spec/SwcCompiler.test.ts @@ -41,10 +41,10 @@ test("throws if the file is ignored", async () => { } } - expect(error).toBeDefined(); + expect(error).toBeTruthy(); expect(error?.ignoredFile).toBeTruthy(); expect(error?.message).toMatch( - /File .+ignored\.ts is imported but not being built because it is explicitly ignored in the wds project config\. It is being ignored by the provided glob pattern '!ignored\.ts', remove this pattern from the project config or don't import this file to fix./ + /File .+ignored\.ts is imported but not being built because it is explicitly ignored in the wds project config\. It is being ignored by the provided glob pattern 'ignored\.ts', remove this pattern from the project config or don't import this file to fix./ ); }); diff --git a/src/ProjectConfig.ts b/src/ProjectConfig.ts index a58eb93..eb8aba7 100644 --- a/src/ProjectConfig.ts +++ b/src/ProjectConfig.ts @@ -1,6 +1,7 @@ import type { Options as SwcOptions } from "@swc/core"; import fs from "fs-extra"; import _ from "lodash"; +import micromatch from "micromatch"; import path from "path"; import { log } from "./utils.js"; @@ -13,35 +14,50 @@ export interface RunOptions { } export interface ProjectConfig { + root: string; ignore: string[]; + includeGlob: string; + includedMatcher: (filePath: string) => boolean; swc?: SwcConfig; esm?: boolean; extensions: string[]; cacheDir: string; } -export const projectConfig = async (root: string): Promise => { +export const projectConfig = _.memoize(async (root: string): Promise => { const location = path.join(root, "wds.js"); const base: ProjectConfig = { - ignore: [], + root, extensions: [".ts", ".tsx", ".jsx"], cacheDir: path.join(root, "node_modules/.cache/wds"), esm: true, + /** The list of globby patterns to use when searching for files to build */ + includeGlob: `**/*`, + /** The list of globby patterns to ignore use when searching for files to build */ + ignore: [], + /** A micromatch matcher for userland checking if a file is included */ + includedMatcher: () => true, }; + let exists = false; try { await fs.access(location); + exists = true; } catch (error: any) { log.debug(`Not loading project config from ${location}`); - return base; } - let required = await import(location); - if (required.default) { - required = required.default; + let result: ProjectConfig; + if (exists) { + let required = await import(location); + if (required.default) { + required = required.default; + } + log.debug(`Loaded project config from ${location}`); + result = _.defaults(required, base); + } else { + result = base; } - log.debug(`Loaded project config from ${location}`); - const result = _.defaults(required, base); const projectRootDir = path.dirname(location); // absolutize the cacheDir if not already @@ -49,8 +65,10 @@ export const projectConfig = async (root: string): Promise => { result.cacheDir = path.resolve(projectRootDir, result.cacheDir); } - // absolutize the ignore paths if not already - result.ignore = result.ignore.map((p: string) => (p.startsWith("/") ? p : path.resolve(projectRootDir, p))); + // build inclusion glob and matcher + result.ignore = _.uniq([`node_modules`, `**/*.d.ts`, `.git/**`, ...result.ignore]); + result.includeGlob = `**/*{${result.extensions.join(",")}}`; + result.includedMatcher = micromatch.matcher(result.includeGlob, { cwd: result.root, ignore: result.ignore }); return result; -}; +}); diff --git a/src/SwcCompiler.ts b/src/SwcCompiler.ts index 959d820..2f417f4 100644 --- a/src/SwcCompiler.ts +++ b/src/SwcCompiler.ts @@ -13,7 +13,7 @@ import path from "path"; import { fileURLToPath } from "url"; import writeFileAtomic from "write-file-atomic"; import type { Compiler } from "./Compiler.js"; -import { projectConfig, type ProjectConfig } from "./ProjectConfig.js"; +import { projectConfig } from "./ProjectConfig.js"; import { log } from "./utils.js"; const __filename = fileURLToPath(import.meta.url); @@ -184,11 +184,14 @@ export class SwcCompiler implements Compiler { swcConfig = config.swc; } - const globs = [...this.fileGlobPatterns(config), ...this.ignoreFileGlobPatterns(config)]; + log.debug("searching for filenames", { filename, config }); - log.debug("searching for filenames", { filename, config, root, globs }); - - let fileNames = await globby(globs, { cwd: root, absolute: true }); + let fileNames = await globby(config.includeGlob, { + onlyFiles: true, + cwd: root, + absolute: true, + ignore: config.ignore, + }); if (process.platform === "win32") { fileNames = fileNames.map((fileName) => fileName.replace(/\//g, "\\")); @@ -276,16 +279,6 @@ export class SwcCompiler implements Compiler { } } - /** The list of globby patterns to use when searching for files to build */ - private fileGlobPatterns(config: ProjectConfig) { - return [`**/*{${config.extensions.join(",")}}`]; - } - - /** The list of globby patterns to ignore use when searching for files to build */ - private ignoreFileGlobPatterns(config: ProjectConfig) { - return [`!node_modules`, `!**/*.d.ts`, ...(config.ignore || []).map((ignore) => `!${ignore}`)]; - } - /** * Detect if a file is being ignored by the ignore glob patterns for a given project * @@ -294,16 +287,17 @@ export class SwcCompiler implements Compiler { private async isFilenameIgnored(filename: string): Promise { const root = findRoot(filename); const config = await projectConfig(root); - const includeGlobs = this.fileGlobPatterns(config); - const ignoreGlobs = this.ignoreFileGlobPatterns(config); - - const actual = await globby([...includeGlobs, ...ignoreGlobs], { cwd: root, absolute: true }); - const all = await globby(includeGlobs, { cwd: root, absolute: true }); - // if the file isn't returned when we use the ignores, but is when we don't use the ignores, it means were ignoring it. Figure out which ignore is causing this - if (!actual.includes(filename) && all.includes(filename)) { - for (const ignoreGlob of ignoreGlobs) { - const withThisIgnore = await globby([...includeGlobs, ignoreGlob], { cwd: root, absolute: true }); + // check if the file is ignored by any of the ignore patterns using micromatch + const included = config.includedMatcher(filename.replace(root + path.sep, "")); + if (!included) { + // figure out which ignore pattern is causing the file to be ignored for a better error message + for (const ignoreGlob of config.ignore) { + const withThisIgnore = await globby(config.includeGlob, { + cwd: root, + absolute: true, + ignore: [ignoreGlob], + }); if (!withThisIgnore.includes(filename)) { return ignoreGlob; } diff --git a/src/index.ts b/src/index.ts index 3e100d0..2c1bd44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,16 +70,19 @@ const startFilesystemWatcher = (project: Project) => { const watcher = new Watcher([project.workspaceRoot], { ignoreInitial: true, recursive: true, - ignore: ((filePath: string) => { + ignore: (filePath: string) => { if (filePath.includes(nodeModulesDir)) return true; + if (filePath == project.workspaceRoot) return false; + if (filePath == project.config.root) return false; if (filePath.endsWith(".d.ts")) return true; if (filePath.endsWith(".map")) return true; if (filePath.includes(gitDir)) return true; if (filePath.endsWith(".DS_Store")) return true; if (filePath.endsWith(".tsbuildinfo")) return true; - return project.config.ignore?.some((ignore) => filePath.startsWith(ignore)) ?? false; - }) as any, + // allow files that match the include glob to be watched, or directories (since they might contain files) + return !project.config.includedMatcher(filePath) && path.extname(filePath) != ""; + }, }); log.debug("started watcher", { root: project.workspaceRoot });