Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/default-basedir-to-cwd.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 9 additions & 3 deletions docs/guide/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 6 additions & 1 deletion src/cli/run-config-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
62 changes: 62 additions & 0 deletions tests/cli/run-modes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <no-hash> and does not abort other entries", async () => {
await writeConfigFile({
entries: {
Expand Down
Loading