mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
Signed-off-by: avivkeller <me@aviv.sh> PR-URL: https://github.com/nodejs/node/pull/65392 Refs: https://openjs-foundation.slack.com/archives/C019Y2T6STH/p1787069174254509 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
490 lines
16 KiB
JavaScript
490 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
const common = require('../common');
|
|
const assert = require('assert');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const tmpdir = require('../common/tmpdir');
|
|
|
|
const cases = /* [path, pattern, matches] */ [
|
|
// Path separators.
|
|
['a/b', 'a/b', true],
|
|
['a/b', 'a//b', true], // Repeated separators collapse.
|
|
['a//b', 'a/b', true],
|
|
['a/', 'a', true], // A trailing separator on the path is ignored.
|
|
['a/b', 'a/b/', false],
|
|
['', '', true],
|
|
['a', '', false],
|
|
['', '*', false],
|
|
['/a', '/a', true],
|
|
['a', '/a', false],
|
|
|
|
// `*` matches any run of characters within one segment.
|
|
['abc', '*', true],
|
|
['a/b', '*', false],
|
|
['a/b', '*/*', true],
|
|
['abc', 'a*c', true],
|
|
['ac', 'a*c', true],
|
|
['a/b', 'a*b', false],
|
|
['a.txt', '*.txt', true],
|
|
['a.c', '*.*', true],
|
|
['abc', '*.*', false],
|
|
|
|
// `?` matches exactly one character within one segment.
|
|
['abc', 'a?c', true],
|
|
['ac', 'a?c', false],
|
|
['a/c', 'a?c', false],
|
|
|
|
// Bracket expressions.
|
|
['a', '[abc]', true],
|
|
['d', '[abc]', false],
|
|
['b', '[a-c]', true],
|
|
['d', '[!abc]', true],
|
|
['d', '[^abc]', true],
|
|
['a', '[!abc]', false],
|
|
[']', '[]]', true], // A `]` first in the set is a literal `]`.
|
|
['a', '[]a]', true],
|
|
['-', '[-a]', true],
|
|
['-', '[a-]', true],
|
|
['[a', '[a', true], // An unterminated `[` is a literal `[`.
|
|
['a', '[a', false],
|
|
['/', '[/]', false], // A set never matches a separator.
|
|
['*', '[*]', true], // Sets are how magic characters are escaped.
|
|
['x', '[*]', false],
|
|
['?', '[?]', true],
|
|
['{', '[{]', true],
|
|
|
|
// POSIX classes cover the whole of Unicode.
|
|
['5', '[[:digit:]]', true],
|
|
['a', '[[:digit:]]', false],
|
|
['a', '[![:digit:]]', true],
|
|
['5', '[![:digit:]]', false],
|
|
['a', '[[:alpha:]]', true],
|
|
['é', '[[:alpha:]]', true],
|
|
['é', '[a-zA-Z]', false],
|
|
['_', '[[:word:]]', true],
|
|
['A', '[[:upper:]]', true],
|
|
['a', '[[:foo:]]', false], // An unknown class is not a class.
|
|
['a', '[[=a=]]', false], // Equivalence classes are unsupported.
|
|
['ch', '[[.ch.]]', false], // So are collating symbols.
|
|
|
|
// A `**` alone in a segment matches zero or more segments.
|
|
['a/b/c', 'a/**/c', true],
|
|
['a/c', 'a/**/c', true],
|
|
['a/x/y/c', 'a/**/c', true],
|
|
['a/b/c/d', 'a/**', true],
|
|
['a/b/c', 'a/**/**/c', true],
|
|
['a/b/c', '**/c', true],
|
|
['a/b', '**', true],
|
|
// Anywhere else, `**` is just a `*`.
|
|
['a/xb', 'a/**b', true],
|
|
['a/xb', 'a/*b', true],
|
|
['a/x/b', 'a/**b', false],
|
|
// And it does not descend into dot directories.
|
|
['a/.b/c', 'a/**/c', false],
|
|
['.a/b', '**', false],
|
|
|
|
// Extended globs.
|
|
['abc', '@(abc|def)', true],
|
|
['def', '@(abc|def)', true],
|
|
['abcdef', '@(abc|def)', false],
|
|
['ac', 'a?(b)c', true],
|
|
['abc', 'a?(b)c', true],
|
|
['abbc', 'a?(b)c', false],
|
|
['ac', 'a*(b)c', true],
|
|
['abbc', 'a*(b)c', true],
|
|
['ac', 'a+(b)c', false],
|
|
['abc', 'a+(b)c', true],
|
|
['abbc', 'a+(b)c', true],
|
|
['abd', '!(abc)', true],
|
|
['abc', '!(abc)', false],
|
|
['abc', '*(a|@(b|c))', true], // They nest.
|
|
['index.ts', '*.@(js|ts)', true],
|
|
['index.css', '!(*.js)', true],
|
|
['ab', '!(a)*', false],
|
|
|
|
// Brace expansion.
|
|
['a', '{a,b}', true],
|
|
['c', '{a,b}', false],
|
|
['abd', 'a{b,c}d', true],
|
|
['a', 'a{,b}', true],
|
|
['ab', 'a{,b}', true],
|
|
['ab', 'a{b,{c,d}}', true], // Braces nest.
|
|
['ac', 'a{b,{c,d}}', true],
|
|
['ad', 'a{b,{c,d}}', true],
|
|
['a1b', 'a{1..3}b', true], // Numeric sequences.
|
|
['a4b', 'a{1..3}b', false],
|
|
['a-1b', 'a{-1..1}b', true],
|
|
['a01b', 'a{01..03}b', true], // Zero padding is kept.
|
|
['a1b', 'a{1..9..3}b', true], // Sequences may take a step.
|
|
['a4b', 'a{1..9..3}b', true],
|
|
['a5b', 'a{1..9..3}b', false],
|
|
['ac', 'a{b..d}', true], // Alphabetic sequences.
|
|
['a{b}c', 'a{b}c', true], // No comma and no sequence means no expansion.
|
|
['abc', 'a{b}c', false],
|
|
['{}', '{}', true],
|
|
|
|
// A leading dot is only matched by a literal dot.
|
|
['.hidden', '*', false],
|
|
['.hidden', '.*', true],
|
|
['.a', '?a', false],
|
|
['.a', '[.]a', true],
|
|
['a/.b/c', 'a/*/c', false],
|
|
['a/.b/c', 'a/.*/c', true],
|
|
// `.` and `..` are never matched by a wildcard.
|
|
['.', '*', false],
|
|
['..', '*', false],
|
|
['.', '.', true],
|
|
|
|
// There is no escape character: a backslash is a path separator.
|
|
['a/b', 'a\\b', true],
|
|
['*', '\\*', false],
|
|
['a*b', 'a\\*b', false],
|
|
|
|
// A leading `!` does not negate and a leading `#` does not comment.
|
|
['!a', '!a', true],
|
|
['a', '!a', false],
|
|
['#a', '#a', true],
|
|
|
|
// Patterns are normalized for walking a file system.
|
|
['a/c', 'a/b/../c', true],
|
|
['a/b', 'a/./b', true],
|
|
];
|
|
|
|
// These are the odd cases that minimatch explicitly tests, so
|
|
// we ensure compatiility.
|
|
//
|
|
// See https://github.com/isaacs/minimatch/blob/main/test/patterns.js
|
|
const minimatchCases = [
|
|
// Runs of stars and question marks collapse
|
|
['abc', 'a***c', true],
|
|
['abc', 'a*****?c', true],
|
|
['abc', '?*****??', true],
|
|
['abc', '*****??', true],
|
|
['abc', '?***?****c', true],
|
|
['abc', '?***?****?', true],
|
|
['abc', '*******c', true],
|
|
['abc', '*******?', true],
|
|
['abc', '??**********?****?', false],
|
|
['abc', '*c*?**', false],
|
|
['abc', 'a********???*******', false],
|
|
['abcdecdhjk', 'a*cd**?**??k', true],
|
|
['abcdecdhjk', 'a**?**cd**?**??***k**', true],
|
|
['a/b/b', 'a/*/b', true],
|
|
|
|
// Bracket expression edge cases
|
|
['[', '[', true],
|
|
['[abc', '[*', true],
|
|
['a', '[]', false],
|
|
['p', '[a-z]', true],
|
|
[']', '[]-]', true],
|
|
['-', '[-abc]', true],
|
|
['-', '[abc-]', true],
|
|
['[', '[[]', true],
|
|
['a]b', 'a[]]*', true],
|
|
['a[]b', 'a[]*', true],
|
|
['a[[]b', 'a[[]*', true],
|
|
['a', '[z-a]', false],
|
|
['fffff', '[z-af]*', true],
|
|
['fffff', '[f-gz-a]*', true],
|
|
['a', '[a-0][a-Ā]', false],
|
|
['a', '[a-b-c]', true],
|
|
['[!ab', '[!a*', true],
|
|
['[ab', '[!a*', false],
|
|
['[#ab', '[#a*', true],
|
|
|
|
// POSIX classes
|
|
['åéîøü', '[[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]]', true],
|
|
['aeiou', '[[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]]', true],
|
|
['0f7fa', '[[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]]', false],
|
|
['aeiou', '[[:ascii:]][[:ascii:]][[:ascii:]][[:ascii:]][[:ascii:]]', true],
|
|
['åéîøü', '[[:ascii:]][[:ascii:]][[:ascii:]][[:ascii:]][[:ascii:]]', false],
|
|
['0f7fa', '[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]',
|
|
true],
|
|
['99999', '[[:xdigit:]][[:xdigit:]]???', true],
|
|
['0f7fa', '[[:graph:]]f*', true],
|
|
['fffff', '[[:graph:][:digit:]]f*', true],
|
|
['åéîøü', '[[:alnum:]][[:alnum:]][[:alnum:]][[:alnum:]][[:alnum:]]', true],
|
|
['a', '[a-[:alpha:]*]', false],
|
|
|
|
// Extended globs
|
|
['foo.bar', '*.!(js)', true],
|
|
['foo.', '*.!(js)', true],
|
|
['boo.js.boo', '*.!(js)', true],
|
|
['foo.js.js', '*.!(js)', true],
|
|
['foo.js', '*.!(js)', false],
|
|
['blar.js', '*.!(js)', false],
|
|
['ac', '+(a)!(b)+(c)', true],
|
|
['acc', '+(a)!(b)+(c)', true],
|
|
['adc', '+(a)!(b)+(c)', true],
|
|
['abc', '+(a)!(b)+(c)', false],
|
|
['bac', '+(a)!(b)+(c)', false],
|
|
['a/b/c/bar/x', 'a/b/*/!(bar)/*', false],
|
|
['a/b/c/baz/x', 'a/b/*/!(bar)/*', true],
|
|
['ax', 'a?(b*)', false],
|
|
['ax', '?(a*|b)', true],
|
|
['x-ab', '?(x-!(y)|z)b', true],
|
|
['zb', '?(x-!(y)|z)b', true],
|
|
['x-a', '?(x-!(y)|z)', true],
|
|
['fool', '@(foo)*', true],
|
|
['oof', '@(foo)*', false],
|
|
['a/b', '*(a/b)', false],
|
|
['aa', '*(?)', true],
|
|
['aa.', '+(?)', true],
|
|
['ab', '+(a|?)', true],
|
|
['b.a', '+(a|!(b))', true],
|
|
['.aa', '+(.|a|!(b))', true],
|
|
['aa', '+(a|.)', true],
|
|
['x', '+()*(x|a)', true],
|
|
['a.y', '+(x|a[^)]y)', true],
|
|
|
|
// Extended globs that never close stay literal
|
|
['!(a|B', '!(a|B', true],
|
|
['B', '!(a|B', false],
|
|
['?(a|B', '?(a|B', true],
|
|
['B', '?(a|B', false],
|
|
['+(a|B', '+(a|B', true],
|
|
['B', '+(a|B', false],
|
|
['*(a|B', '*(a|B', true],
|
|
['B', '*(a|B', false],
|
|
['@(a|B', '@(a|B', true],
|
|
['B', '@(a|B', false],
|
|
|
|
// Nested extglobs collapse into one set
|
|
['xy', '@(!(a|b))y', true],
|
|
['by', '@(!(a|b))y', false],
|
|
['xy', '!(a|b)y', true],
|
|
['by', '!(a|b)y', false],
|
|
['xb', 'x@(!(a|b))', false],
|
|
['xy', 'x@(!(a|b))', true],
|
|
['xb', 'x!(a|b)', false],
|
|
['x', '@(!(a|b))', true],
|
|
['b', '@(!(a|b))', false],
|
|
['a', '!(!(a|b))', true],
|
|
['x', '!(!(a|b))', false],
|
|
['ab', '?(*(a|b))', true],
|
|
['', '?(+(a|b))', true],
|
|
['ab', '@(*(a|b))', true],
|
|
['axb', '@(a*b)', true],
|
|
|
|
// Braces expand before anything else is parsed
|
|
['ab', '*(a|{b),c)}', true],
|
|
['ac', '*(a|{b),c)}', true],
|
|
['ad', '*(a|{b),c)}', false],
|
|
['ab', '*(a|{b,c})', true],
|
|
['bc', '*(a|{b|c,c})', true],
|
|
['d)', '{a,*(b|c,d)}', true],
|
|
['a{b{cdf}g}h', 'a{b{c{d,e}f}g}h', true],
|
|
['za,c}d', 'z{a,b},c}d', true],
|
|
['z{a,bcd', 'z{a,b{,c}d', true],
|
|
// `${...}` is left alone by brace expansion.
|
|
/* eslint-disable no-template-curly-in-string */
|
|
['a${c}${d}', '{a,b}${c}${d}', true],
|
|
['${a}${b}c', '${a}${b}{c,d}', true],
|
|
/* eslint-enable no-template-curly-in-string */
|
|
['/a', '{/?,*}', true],
|
|
['bb', '{/?,*}', true],
|
|
['/b/b', '{/?,*}', false],
|
|
['/asdf/asdf/asdf', '{/*,*}', false],
|
|
['c', '{c*,./c*}', true],
|
|
['a5b', 'a{00..05}b', false],
|
|
['a05b', 'a{00..05}b', true],
|
|
|
|
// Globstar and dot directories
|
|
['a/b/.x/c', '**/.x/**', true],
|
|
['a/b/.x/c/d/e', '**/.x/**', true],
|
|
['.x/a/b', '**/.x/**', true],
|
|
['a/.x/b/.x/c', '**/.x/**', false],
|
|
['.x/.y', '**/.x/**', false],
|
|
['.x/a/b', '.x/**/*', true],
|
|
['.x/a/b', '.x/*/**', true],
|
|
['.x/a/b', '.x/**/*/**', true],
|
|
['.x/.y', '.x/**/*', false],
|
|
['.x/', '.x/**/**/*', false],
|
|
['a/.d', '**', false],
|
|
['.a/.d', '**', false],
|
|
['a/b', '**', true],
|
|
['x/y/z', 'x/y/*/z', false],
|
|
['x/y/w/z', 'x/y/*/z', true],
|
|
|
|
// A segment followed by `..` cancels
|
|
['x/a/b/c', 'x/*/../a/b/c', true],
|
|
['x/y/a/b/c', 'x/*/../a/b/c', false],
|
|
['a/b/c', 'x/*/../../a/b/c', true],
|
|
['x/a/b/c', 'x/*/../../a/b/c', false],
|
|
['x/y/a/b/c', 'x/z/../*/a/b/c', true],
|
|
['x/z/a/b/c', 'x/z/../*/a/b/c', true],
|
|
['a/c/b', 'a/*/b', true],
|
|
['a/./b', 'a/*/b', false],
|
|
['a/.d/b', 'a/*/b', false],
|
|
['a/.d/b', 'a/.*/b', true],
|
|
|
|
// The optimized single-segment shapes
|
|
['a', '?', true],
|
|
['js', '??', true],
|
|
['.a', '??', false],
|
|
['.js', '???', false],
|
|
['a.js', '?.js', true],
|
|
['.js', '?js', false],
|
|
['a.js', '*.js', true],
|
|
['js', '*js', true],
|
|
['.a.js', '*.js', false],
|
|
['a.js', '*.*', true],
|
|
['.a', '*.*', false],
|
|
['.a.js', '.*', true],
|
|
['a', '*', true],
|
|
['.a', '*', false],
|
|
['x.y', '*.y', true],
|
|
['.z', '*.z', false],
|
|
];
|
|
|
|
// Most POSIX classes lower to Unicode property escapes (`\p{…}`), which V8
|
|
// only supports when built with ICU. Without it, such a pattern throws.
|
|
const kIntlClasses =
|
|
/\[:(?:alnum|alpha|blank|cntrl|digit|graph|lower|print|punct|space|upper|word):\]/;
|
|
|
|
for (const [subject, pattern, expected] of [...cases, ...minimatchCases]) {
|
|
if (!common.hasIntl && kIntlClasses.test(pattern)) continue;
|
|
assert.strictEqual(
|
|
path.posix.matchesGlob(subject, pattern),
|
|
expected,
|
|
`expected ${JSON.stringify(subject)} to ` +
|
|
`${expected ? '' : 'not '}match ${JSON.stringify(pattern)}`,
|
|
);
|
|
}
|
|
|
|
// However long a run of stars gets, it is equivalent to a single one.
|
|
assert.strictEqual(
|
|
path.posix.matchesGlob('a/b/b', `a/${'*'.repeat(100)}/b`),
|
|
path.posix.matchesGlob('a/b/b', 'a/*/b'),
|
|
);
|
|
|
|
// A pattern longer than the 64 KiB cap is rejected rather than matched.
|
|
assert.throws(() => path.matchesGlob('a', 'a'.repeat(64 * 1024 + 1)), {
|
|
name: 'TypeError',
|
|
message: 'pattern is too long',
|
|
});
|
|
assert.strictEqual(path.matchesGlob('a', 'a'.repeat(64 * 1024)), false);
|
|
|
|
// Braces and extended globs are parsed by recursing once per level of nesting,
|
|
// so a pattern that nests deeper than the parser can recurse is rejected
|
|
// instead of running the stack out. `{,` and `@(` cost two or three code units
|
|
// per level, which leaves room for thousands of levels under the 64 KiB cap.
|
|
{
|
|
const braces = (n) => `${'{,'.repeat(n)}a${'}'.repeat(n)}`;
|
|
const extglobs = (n) => `${'@('.repeat(n)}a${')'.repeat(n)}`;
|
|
assert.strictEqual(path.matchesGlob('a', braces(256)), true);
|
|
assert.strictEqual(path.matchesGlob('a', extglobs(256)), true);
|
|
for (const pattern of [
|
|
braces(257),
|
|
braces(20000),
|
|
extglobs(257),
|
|
extglobs(20000),
|
|
// An extended glob that adopts its child leaves minimatch's extDepth
|
|
// where it is, so the nesting cap is the only thing bounding this one.
|
|
'@('.repeat(20000),
|
|
]) {
|
|
assert.throws(() => path.matchesGlob('a', pattern), {
|
|
name: 'TypeError',
|
|
message: 'pattern nests too deeply',
|
|
}, pattern.slice(0, 8));
|
|
}
|
|
// fs.glob() compiles its patterns the same way.
|
|
assert.throws(() => fs.globSync(braces(20000)), {
|
|
name: 'TypeError',
|
|
message: 'pattern nests too deeply',
|
|
});
|
|
}
|
|
|
|
// Expanding braces holds literal braces aside as a code unit the pattern does
|
|
// not itself use. Naming every unit one could be drawn from leaves none free,
|
|
// which takes all 65408 units at or above U+0080 -- and that still fits under
|
|
// the 64 KiB cap. Reject such a pattern rather than rewrite one of its own
|
|
// literals into a brace.
|
|
{
|
|
let every = '';
|
|
for (let u = 0x80; u <= 0xFFFF; u++) every += String.fromCharCode(u);
|
|
assert.strictEqual(every.length, 64 * 1024 - 0x80);
|
|
assert.throws(() => path.matchesGlob('\uFDD0', `{\uFDD0,${every}}`), {
|
|
name: 'TypeError',
|
|
message: 'pattern is too long',
|
|
});
|
|
// Two spare units are all it takes to expand normally again.
|
|
assert.strictEqual(
|
|
path.matchesGlob('\uFDD0', `{\uFDD0,${every.slice(2)}}`),
|
|
true,
|
|
);
|
|
}
|
|
|
|
// Nesting past the extended glob and globstar caps stops matching rather
|
|
// than costing an unbounded amount of work.
|
|
assert.strictEqual(
|
|
path.matchesGlob('a'.repeat(100), `${'*('.repeat(100)}a${')'.repeat(100)}`),
|
|
true,
|
|
);
|
|
assert.strictEqual(path.matchesGlob('a/'.repeat(500), '**/'.repeat(500)), true);
|
|
|
|
// A nested extended glob against a long subject is what makes a backtracking
|
|
// matcher blow up: `minimatch` needs minutes on the first of these. Segments
|
|
// are matched by advancing every reachable position at once instead, so the
|
|
// cost stays bounded. A regression to backtracking would hang here.
|
|
{
|
|
const subject = `${'a'.repeat(101)}z`;
|
|
const start = process.hrtime.bigint();
|
|
for (const pattern of [
|
|
'*(*(*(a|a)))',
|
|
'*(+(*(a|b)|c)|d)',
|
|
'*(*(+(+(?(@(a|t)|u)|v)|w)|x)|y)',
|
|
'+(a|*(b|c)|d)a',
|
|
]) {
|
|
assert.strictEqual(path.posix.matchesGlob(subject, pattern), false, pattern);
|
|
}
|
|
// Chained non-adjacent globstars, from minimatch's GHSA-7r86-cg39-jmmj.
|
|
const globstars = `${Array.from({ length: 50 }, () => '**/a').join('/')}/b/**`;
|
|
assert.strictEqual(
|
|
path.posix.matchesGlob(`${Array(100).fill('a').join('/')}/a`, globstars),
|
|
false,
|
|
);
|
|
const elapsed = Number(process.hrtime.bigint() - start) / 1e6;
|
|
assert.ok(
|
|
elapsed < common.platformTimeout(5000),
|
|
`adversarial patterns took ${elapsed}ms, matching is backtracking`,
|
|
);
|
|
}
|
|
|
|
// The walker has to agree with the matcher, so run a few of the same rules
|
|
// against a real directory tree.
|
|
tmpdir.refresh();
|
|
const files = [
|
|
'a/b/c.js',
|
|
'a/b/c.ts',
|
|
'a/d.js',
|
|
'a/.hidden/e.js',
|
|
'a/.dotfile.js',
|
|
'f.js',
|
|
];
|
|
for (const file of files) {
|
|
fs.mkdirSync(tmpdir.resolve(path.dirname(file)), { recursive: true });
|
|
fs.writeFileSync(tmpdir.resolve(file), '');
|
|
}
|
|
|
|
function globbed(pattern) {
|
|
return fs.globSync(pattern, { cwd: tmpdir.path })
|
|
.map((p) => p.split(path.sep).join('/'))
|
|
.sort();
|
|
}
|
|
|
|
// `**` crosses directories but skips dot directories, `*` does not cross.
|
|
assert.deepStrictEqual(globbed('**/*.js'), ['a/b/c.js', 'a/d.js', 'f.js']);
|
|
assert.deepStrictEqual(globbed('*.js'), ['f.js']);
|
|
assert.deepStrictEqual(globbed('a/*/*.js'), ['a/b/c.js']);
|
|
// A leading dot needs a literal dot, in a file name and in a directory name.
|
|
assert.deepStrictEqual(globbed('a/.*.js'), ['a/.dotfile.js']);
|
|
assert.deepStrictEqual(globbed('a/.*/*.js'), ['a/.hidden/e.js']);
|
|
// Braces and extended globs expand the same way here as in matchesGlob().
|
|
assert.deepStrictEqual(globbed('a/b/*.{js,ts}'), ['a/b/c.js', 'a/b/c.ts']);
|
|
assert.deepStrictEqual(globbed('a/b/*.@(js|ts)'), ['a/b/c.js', 'a/b/c.ts']);
|
|
assert.deepStrictEqual(globbed('a/b/!(*.ts)'), ['a/b/c.js']);
|
|
// A `<segment>/..` pair cancels.
|
|
assert.deepStrictEqual(globbed('a/b/../d.js'), ['a/d.js']);
|