Skip to content
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ Versioning](https://semver.org/spec/v2.0.0.html).

### Changed

- Starting to improve error messages by drawing squiggles underneith the
specific part of the package.json file that's in error.
- [**Breaking**] A leading `/` on a `files` or `output` glob pattern is now
interpreted relative to the current package directory. Previously it was
interpreted relative to the root of the filesystem. In the case of `files`
(but not `output`), it is still possible to reference files outside of the
current package with a pattern like `../foo`.

- Starting to improve error messages by drawing squiggles underneath the
specific part of the `package.json` file that's in error.

## [0.3.1] - 2022-04-30

Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,10 +526,17 @@ The following glob syntaxes are supported in the `files` and `output` arrays:

Also note these details:

- Paths should always use `/` (forward-slash) delimiters, even on Windows.
- Paths are interpreted relative to the current package even if there is a
leading `/` (e.g. `/foo` is the same as `foo`).
- Whenever a directory is matched, all recursive children of that directory are
included.
- `files` are allowed to reach outside of the current package using e.g.
`../foo`. `output` files cannot reference files outside of the current
package.
- Symlinks in input `files` are followed, so that they are identified by their content.
- Symlinks in `output` files are cached as symlinks, so that restoring from cache doesn't create unnecessary copies.
- Symlinks in `output` files are cached as symlinks, so that restoring from
cache doesn't create unnecessary copies.
- The order of `!exclude` patterns is significant.
- Hidden/dot files are matched by `*` and `**`.
- Patterns are case-sensitive (if supported by the filesystem).
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
}
},
"devDependencies": {
"@types/braces": "^3.0.1",
"@types/node": "^14.0.0",
"@typescript-eslint/eslint-plugin": "^5.17.0",
"@typescript-eslint/parser": "^5.17.0",
Expand All @@ -255,6 +256,7 @@
},
"dependencies": {
"@actions/cache": "=2.0.2",
"braces": "^3.0.2",
"chokidar": "^3.5.3",
"fast-glob": "^3.2.11",
"jsonc-parser": "^3.0.0"
Expand Down
67 changes: 67 additions & 0 deletions src/test/cache-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1007,4 +1007,71 @@ export const registerCommonCacheTests = (
}
})
);

test(
'leading slash on output glob is package relative',
timeout(async ({rig}) => {
const cmdA = await rig.newCommand();
await rig.write({
'package.json': {
scripts: {
a: 'wireit',
},
wireit: {
a: {
command: cmdA.command,
files: ['input'],
output: ['/output'],
},
},
},
input: 'v0',
});

// Initial run with input v0.
{
const exec = rig.exec('npm run a');
const inv = await cmdA.nextInvocation();
await rig.write({output: 'v0'});
inv.exit(0);
const res = await exec.exit;
assert.equal(res.code, 0);
assert.equal(cmdA.numInvocations, 1);
assert.equal(await rig.read('output'), 'v0');
}

// Input changed to v1. Run again.
{
await rig.write({input: 'v1'});
const exec = rig.exec('npm run a');
const inv = await cmdA.nextInvocation();
await rig.write({output: 'v1'});
inv.exit(0);
const res = await exec.exit;
assert.equal(res.code, 0);
assert.equal(cmdA.numInvocations, 2);
assert.equal(await rig.read('output'), 'v1');
}

// Input changed back to v0. Output should be cached.
{
await rig.write({input: 'v0'});
const exec = rig.exec('npm run a');
const res = await exec.exit;
assert.equal(res.code, 0);
assert.equal(cmdA.numInvocations, 2);
assert.equal(await rig.read('output'), 'v0');
}

// Input changed back to v1. Output should be cached.
{
await rig.write({input: 'v1'});
const exec = rig.exec('npm run a');
const res = await exec.exit;
assert.equal(res.code, 0);
assert.equal(cmdA.numInvocations, 2);
assert.equal(await rig.read('output'), 'v1');
}
})
);
};
35 changes: 35 additions & 0 deletions src/test/clean.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,4 +531,39 @@ test(
})
);

test(
'leading slash on output glob is package relative',
timeout(async ({rig}) => {
const cmdA = await rig.newCommand();
await rig.write({
'package.json': {
scripts: {
a: 'wireit',
},
wireit: {
a: {
command: cmdA.command,
output: ['/output'],
},
},
},
output: 'foo',
});

// Output should exist before we run the script.
assert.ok(await rig.exists('output'));

// Output should be deleted between running the script and executing the
// command.
const exec = rig.exec('npm run a');
const inv = await cmdA.nextInvocation();
assert.not(await rig.exists('output'));

inv.exit(0);
const res = await exec.exit;
assert.equal(res.code, 0);
assert.equal(cmdA.numInvocations, 1);
})
);

test.run();
44 changes: 44 additions & 0 deletions src/test/freshness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1654,4 +1654,48 @@ test(
})
);

test(
'leading slash on files glob is package relative',
timeout(async ({rig}) => {
const cmdA = await rig.newCommand();
await rig.write({
'package.json': {
scripts: {
a: 'wireit',
},
wireit: {
a: {
command: cmdA.command,
files: ['/input.txt'],
},
},
},
'input.txt': 'v0',
});

// Initially stale, so command is invoked.
{
const exec = rig.exec('npm run a');
const inv = await cmdA.nextInvocation();
inv.exit(0);
const res = await exec.exit;
assert.equal(res.code, 0);
assert.equal(cmdA.numInvocations, 1);
}

// Input file changed, so script is stale, and command is invoked.
{
await rig.write({
'input.txt': 'v1',
});
const exec = rig.exec('npm run a');
const inv = await cmdA.nextInvocation();
inv.exit(0);
const res = await exec.exit;
assert.equal(res.code, 0);
assert.equal(cmdA.numInvocations, 2);
}
})
);

test.run();
57 changes: 55 additions & 2 deletions src/test/glob.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ test.before.each(async (ctx) => {
files,
patterns,
expected,
cwd = rig.temp,
cwd = '.',
absolute = false,
followSymlinks = true,
includeDirectories = false,
Expand Down Expand Up @@ -73,7 +73,7 @@ test.before.each(async (ctx) => {
let actual, error;
try {
actual = await glob(patterns, {
cwd,
cwd: rig.resolve(cwd),
absolute,
followSymlinks,
includeDirectories,
Expand Down Expand Up @@ -486,4 +486,57 @@ test('dirent tags symlinks to directories as directories when followSymlinks=tru
assert.not(actual[0].dirent.isSymbolicLink());
});

test('re-roots to cwd', ({check}) =>
check({
files: ['foo'],
patterns: ['/foo'],
expected: ['foo'],
}));

test('re-roots to cwd with exclusion', ({check}) =>
check({
files: ['foo', 'bar', 'baz'],
patterns: ['/*', '!/bar'],
expected: ['foo', 'baz'],
}));

test('re-rooting allows ../', ({check}) =>
check({
cwd: 'subdir',
files: ['foo', 'subdir/'],
patterns: ['../foo'],
expected: ['../foo'],
}));

// TODO(aomarks) This should be normalized to "foo" consistently. It currently
// differs on Windows between Node 14 and 16.
test.skip('re-rooting handles /./foo', ({check}) =>
check({
files: ['foo'],
patterns: ['/./foo'],
expected: [`.${pathlib.sep}foo`],
}));

test('re-rooting handles /../foo', ({check}) =>
check({
cwd: 'subdir',
files: ['foo', 'subdir/'],
patterns: ['/../foo'],
expected: ['../foo'],
}));

test('re-roots to cwd with braces', ({check}) =>
check({
files: ['foo', 'bar'],
patterns: ['{/foo,/bar}'],
expected: ['foo', 'bar'],
}));

test('braces can be escaped', ({check}) =>
check({
files: ['{foo,bar}'],
patterns: ['\\{foo,bar\\}'],
expected: ['{foo,bar}'],
}));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No one should write this, but how do we interpret /./foo or /../bar? Probably worth a test to make sure what we do is sensible

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We remove the leading /, which I think is correct.

Added tests.

test.run();
Loading