diff --git a/.changeset/default-basedir-to-cwd.md b/.changeset/default-basedir-to-cwd.md new file mode 100644 index 0000000..f09e804 --- /dev/null +++ b/.changeset/default-basedir-to-cwd.md @@ -0,0 +1,26 @@ +--- +"@maastrich/hashup": minor +--- + +Default `baseDir` is now the current working directory, not the +config file's directory. + +Globs and relative entry paths in `hashup.json` anchor at `cwd` unless +an explicit `baseDir` is set (top-level or per-entry). Running +`hashup --cwd ./pkg` or `cd pkg && hashup` both resolve `src/**/*.ts` +against `./pkg/`, which is what most users expect when invoking from +a subdirectory. + +**To keep the old config-relative behavior**, add `"baseDir": "."` to +`hashup.json`: + +```json +{ + "baseDir": ".", + "entries": { "app": { "entry": "src/**/*.ts" } } +} +``` + +Explicit `baseDir` values (top-level or per-entry) still resolve +against the config file's directory. The CLI `--base-dir` flag still +resolves against the real cwd and wins over both. diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 5e33526..4f45666 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -101,9 +101,15 @@ interface HashupConfig { Resolution rules: -- Relative paths in the config resolve against the config file's directory, - unless a `baseDir` is set (top-level or per-entry). -- `--base-dir` on the command line overrides both. +- Globs and relative entry paths resolve against the current working + directory by default. `hashup --cwd ./pkg` or `cd pkg && hashup` both + anchor the run at `./pkg/`. +- An explicit `baseDir` in `hashup.json` (top-level or per-entry) is + resolved against the **config file's directory**, so + `"baseDir": "."` pins the run to wherever the config lives regardless + of where you invoke from. +- `--base-dir` on the command line wins over both and is resolved + against the current working directory. - Entry names must be unique (it's a record). Output order matches insertion order in the JSON file. diff --git a/src/cli/run-config-mode.ts b/src/cli/run-config-mode.ts index 5553926..8911945 100644 --- a/src/cli/run-config-mode.ts +++ b/src/cli/run-config-mode.ts @@ -84,7 +84,12 @@ function resolveRootBase({ cwd, configDir, override, fromFile }: ResolveRootBase if (fromFile !== undefined) { return resolveFrom(configDir, fromFile); } - return configDir; + // Default to the current working directory, not the config's + // directory. Running `hashup --cwd ./pkg` or `cd pkg && hashup` + // resolves globs against the invocation point, which is what most + // users expect; to get the old config-relative behavior, set + // `"baseDir": "."` in `hashup.json`. + return cwd; } /** diff --git a/tests/cli/run-modes.test.ts b/tests/cli/run-modes.test.ts index 4278d3c..09eb1c7 100644 --- a/tests/cli/run-modes.test.ts +++ b/tests/cli/run-modes.test.ts @@ -224,6 +224,68 @@ describe("runConfigMode", () => { expect(third.ok && third.output).not.toBe(first.ok && first.output); }); + test("default baseDir is cwd, not the config's directory", async () => { + // Layout: + // workDir/ + // src/a.ts (the "cwd" version) + // pkg/hashup.json (globs src/**/*.ts) + // pkg/src/a.ts (the "configDir" version) + // Running with cwd=workDir and -c pkg/hashup.json, the default + // baseDir should now be workDir, so the glob matches workDir/src/a.ts. + await mkdir(join(workDir, "pkg", "src"), { recursive: true }); + await writeFile( + join(workDir, "pkg", "src", "a.ts"), + "export const fromPkg = true;\n", // distinct content from workDir/src/a.ts + ); + await writeFile( + join(workDir, "pkg", "hashup.json"), + JSON.stringify({ entries: { a: { entry: "src/**/*.ts" } } }), + ); + + const cwdRun = await runConfigMode({ + cwd: workDir, + configPath: "pkg/hashup.json", + baseDirOverride: undefined, + json: true, + files: true, + }); + expect(cwdRun.ok).toBe(true); + if (cwdRun.ok) { + const parsed = JSON.parse(cwdRun.output); + // Default baseDir is cwd (workDir) → matches workDir/src/a.ts, + // NOT workDir/pkg/src/a.ts + expect(parsed.a.files).toEqual( + expect.arrayContaining([expect.stringContaining(`${workDir}/src/a.ts`)]), + ); + expect(parsed.a.files).not.toContain(`${workDir}/pkg/src/a.ts`); + } + }); + + test('config-relative behavior is still reachable via baseDir: "."', async () => { + await mkdir(join(workDir, "pkg", "src"), { recursive: true }); + await writeFile(join(workDir, "pkg", "src", "a.ts"), "export const fromPkg = true;\n"); + await writeFile( + join(workDir, "pkg", "hashup.json"), + JSON.stringify({ baseDir: ".", entries: { a: { entry: "src/**/*.ts" } } }), + ); + + const result = await runConfigMode({ + cwd: workDir, + configPath: "pkg/hashup.json", + baseDirOverride: undefined, + json: true, + files: true, + }); + expect(result.ok).toBe(true); + if (result.ok) { + const parsed = JSON.parse(result.output); + // baseDir: "." resolves against configDir → pkg/src/a.ts + expect(parsed.a.files).toEqual( + expect.arrayContaining([expect.stringContaining(`${workDir}/pkg/src/a.ts`)]), + ); + } + }); + test("zero-match glob emits and does not abort other entries", async () => { await writeConfigFile({ entries: {