From 6c4688a6436113e5210c6177acd8c3cf8fc4793f Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Fri, 20 Dec 2024 17:29:52 -0800 Subject: [PATCH 1/5] feat(node/path): support `matchesGlob` --- src/js/node/path.ts | 41 ++++++++++++++++++ test/js/node/test/parallel/test-path-glob.js | 44 ++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 test/js/node/test/parallel/test-path-glob.js diff --git a/src/js/node/path.ts b/src/js/node/path.ts index f7364a82bb1dea..b996f8e01672cc 100644 --- a/src/js/node/path.ts +++ b/src/js/node/path.ts @@ -1,4 +1,6 @@ // Hardcoded module "node:path" +const { validateString } = require("internal/validators"); + const [bindingPosix, bindingWin32] = $cpp("Path.cpp", "createNodePathBinding"); const toNamespacedPathPosix = bindingPosix.toNamespacedPath.bind(bindingPosix); const toNamespacedPathWin32 = bindingWin32.toNamespacedPath.bind(bindingWin32); @@ -40,4 +42,43 @@ const win32 = { }; posix.win32 = win32.win32 = win32; posix.posix = posix; + +type Glob = import("bun").Glob; + +let LazyGlob: Glob | undefined; +function loadGlob(): LazyGlob { + LazyGlob = require("bun").Glob; +} + +// the most-recently used glob is memoized in case `matchesGlob` is called in a +// loop with the same pattern +let prevGlob: Glob | undefined; +let prevPattern: string | undefined; +function matchesGlob(path, pattern) { + let glob: Glob; + + validateString(path, "path"); + + if (prevGlob) { + $assert(prevPattern !== undefined); + if (prevPattern === pattern) { + glob = prevGlob; + } else { + if (LazyGlob === undefined) loadGlob(); + validateString(pattern, "pattern"); + glob = prevGlob = new LazyGlob(pattern); + prevPattern = pattern; + } + } else { + loadGlob(); // no prevGlob implies LazyGlob isn't loaded + validateString(pattern, "pattern"); + glob = prevGlob = new LazyGlob(pattern); + prevPattern = pattern; + } + + return glob.match(path); +} + +win32.matchesGlob = posix.matchesGlob = matchesGlob; + export default process.platform === "win32" ? win32 : posix; diff --git a/test/js/node/test/parallel/test-path-glob.js b/test/js/node/test/parallel/test-path-glob.js new file mode 100644 index 00000000000000..47647e12784e5a --- /dev/null +++ b/test/js/node/test/parallel/test-path-glob.js @@ -0,0 +1,44 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const path = require('path'); + +const globs = { + win32: [ + ['foo\\bar\\baz', 'foo\\[bcr]ar\\baz', true], // Matches 'bar' or 'car' in 'foo\\bar' + ['foo\\bar\\baz', 'foo\\[!bcr]ar\\baz', false], // Matches anything except 'bar' or 'car' in 'foo\\bar' + ['foo\\bar\\baz', 'foo\\[bc-r]ar\\baz', true], // Matches 'bar' or 'car' using range in 'foo\\bar' + ['foo\\bar\\baz', 'foo\\*\\!bar\\*\\baz', false], // Matches anything with 'foo' and 'baz' but not 'bar' in between + ['foo\\bar1\\baz', 'foo\\bar[0-9]\\baz', true], // Matches 'bar' followed by any digit in 'foo\\bar1' + ['foo\\bar5\\baz', 'foo\\bar[0-9]\\baz', true], // Matches 'bar' followed by any digit in 'foo\\bar5' + ['foo\\barx\\baz', 'foo\\bar[a-z]\\baz', true], // Matches 'bar' followed by any lowercase letter in 'foo\\barx' + ['foo\\bar\\baz\\boo', 'foo\\[bc-r]ar\\baz\\*', true], // Matches 'bar' or 'car' in 'foo\\bar' + ['foo\\bar\\baz', 'foo/**', true], // Matches anything in 'foo' + ['foo\\bar\\baz', '*', false], // No match + ], + posix: [ + ['foo/bar/baz', 'foo/[bcr]ar/baz', true], // Matches 'bar' or 'car' in 'foo/bar' + ['foo/bar/baz', 'foo/[!bcr]ar/baz', false], // Matches anything except 'bar' or 'car' in 'foo/bar' + ['foo/bar/baz', 'foo/[bc-r]ar/baz', true], // Matches 'bar' or 'car' using range in 'foo/bar' + ['foo/bar/baz', 'foo/*/!bar/*/baz', false], // Matches anything with 'foo' and 'baz' but not 'bar' in between + ['foo/bar1/baz', 'foo/bar[0-9]/baz', true], // Matches 'bar' followed by any digit in 'foo/bar1' + ['foo/bar5/baz', 'foo/bar[0-9]/baz', true], // Matches 'bar' followed by any digit in 'foo/bar5' + ['foo/barx/baz', 'foo/bar[a-z]/baz', true], // Matches 'bar' followed by any lowercase letter in 'foo/barx' + ['foo/bar/baz/boo', 'foo/[bc-r]ar/baz/*', true], // Matches 'bar' or 'car' in 'foo/bar' + ['foo/bar/baz', 'foo/**', true], // Matches anything in 'foo' + ['foo/bar/baz', '*', false], // No match + ], +}; + + +for (const [platform, platformGlobs] of Object.entries(globs)) { + for (const [pathStr, glob, expected] of platformGlobs) { + const actual = path[platform].matchesGlob(pathStr, glob); + assert.strictEqual(actual, expected, `Expected ${pathStr} to ` + (expected ? '' : 'not ') + `match ${glob} on ${platform}`); + } +} + +// Test for non-string input +assert.throws(() => path.matchesGlob(123, 'foo/bar/baz'), /.*must be of type string.*/); +assert.throws(() => path.matchesGlob('foo/bar/baz', 123), /.*must be of type string.*/); From 9831b72192a62ca5a9d6c120caf8ffa7a38d5f48 Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Fri, 20 Dec 2024 18:53:42 -0800 Subject: [PATCH 2/5] fix: handle backslashes on windows --- src/js/node/path.ts | 40 +++++++++++++++++++--------------- test/js/bun/glob/match.test.ts | 7 ++++++ 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/js/node/path.ts b/src/js/node/path.ts index b996f8e01672cc..9139ce2dedb034 100644 --- a/src/js/node/path.ts +++ b/src/js/node/path.ts @@ -54,31 +54,37 @@ function loadGlob(): LazyGlob { // loop with the same pattern let prevGlob: Glob | undefined; let prevPattern: string | undefined; -function matchesGlob(path, pattern) { - let glob: Glob; +const createMatchesGlob = isWindows => + function matchesGlob(path, pattern) { + let glob: Glob; - validateString(path, "path"); + validateString(path, "path"); - if (prevGlob) { - $assert(prevPattern !== undefined); - if (prevPattern === pattern) { - glob = prevGlob; + if (isWindows) { + pattern = pattern.replaceAll("/", "\\"); + } + + if (prevGlob) { + $assert(prevPattern !== undefined); + if (prevPattern === pattern) { + glob = prevGlob; + } else { + if (LazyGlob === undefined) loadGlob(); + validateString(pattern, "pattern"); + glob = prevGlob = new LazyGlob(pattern); + prevPattern = pattern; + } } else { - if (LazyGlob === undefined) loadGlob(); + loadGlob(); // no prevGlob implies LazyGlob isn't loaded validateString(pattern, "pattern"); glob = prevGlob = new LazyGlob(pattern); prevPattern = pattern; } - } else { - loadGlob(); // no prevGlob implies LazyGlob isn't loaded - validateString(pattern, "pattern"); - glob = prevGlob = new LazyGlob(pattern); - prevPattern = pattern; - } - return glob.match(path); -} + return glob.match(path); + }; -win32.matchesGlob = posix.matchesGlob = matchesGlob; +win32.matchesGlob = createMatchesGlob(true); +posix.matchesGlob = createMatchesGlob(false); export default process.platform === "win32" ? win32 : posix; diff --git a/test/js/bun/glob/match.test.ts b/test/js/bun/glob/match.test.ts index c09f8b7cd0ea5e..9a98d44c403b79 100644 --- a/test/js/bun/glob/match.test.ts +++ b/test/js/bun/glob/match.test.ts @@ -634,6 +634,13 @@ describe("Glob.match", () => { expect(new Glob("[^a-c]*").match("BewAre")).toBeTrue(); }); + test("square braces", () => { + expect(new Glob("src/*.[tj]s").match("src/foo.js")).toBeTrue(); + expect(new Glob("src/*.[tj]s").match("src/foo.ts")).toBeTrue(); + expect(new Glob("foo/ba[rz].md").match("foo/bar.md")).toBeTrue(); + expect(new Glob("foo/ba[rz].md").match("foo/baz.md")).toBeTrue(); + }); + test("bash wildmatch", () => { expect(new Glob("a[]-]b").match("aab")).toBeFalse(); expect(new Glob("[ten]").match("ten")).toBeFalse(); From 92ff953d0bc6ed1f44dab0694c350341dad4201b Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Fri, 20 Dec 2024 20:15:46 -0800 Subject: [PATCH 3/5] add custom path.matchesGlob unit tests --- src/js/node/path.ts | 43 +++++++------- test/js/node/path/matches-glob.test.ts | 79 ++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 23 deletions(-) create mode 100644 test/js/node/path/matches-glob.test.ts diff --git a/src/js/node/path.ts b/src/js/node/path.ts index 9139ce2dedb034..6704544e081e54 100644 --- a/src/js/node/path.ts +++ b/src/js/node/path.ts @@ -54,37 +54,34 @@ function loadGlob(): LazyGlob { // loop with the same pattern let prevGlob: Glob | undefined; let prevPattern: string | undefined; -const createMatchesGlob = isWindows => - function matchesGlob(path, pattern) { - let glob: Glob; +function matchesGlob(path, pattern) { + let glob: Glob; - validateString(path, "path"); + validateString(path, "path"); + console.log("before:", pattern); + pattern = pattern.replaceAll("\\", "/"); + console.log("after:", pattern); - if (isWindows) { - pattern = pattern.replaceAll("/", "\\"); - } - - if (prevGlob) { - $assert(prevPattern !== undefined); - if (prevPattern === pattern) { - glob = prevGlob; - } else { - if (LazyGlob === undefined) loadGlob(); - validateString(pattern, "pattern"); - glob = prevGlob = new LazyGlob(pattern); - prevPattern = pattern; - } + if (prevGlob) { + $assert(prevPattern !== undefined); + if (prevPattern === pattern) { + glob = prevGlob; } else { - loadGlob(); // no prevGlob implies LazyGlob isn't loaded + if (LazyGlob === undefined) loadGlob(); validateString(pattern, "pattern"); glob = prevGlob = new LazyGlob(pattern); prevPattern = pattern; } + } else { + loadGlob(); // no prevGlob implies LazyGlob isn't loaded + validateString(pattern, "pattern"); + glob = prevGlob = new LazyGlob(pattern); + prevPattern = pattern; + } - return glob.match(path); - }; + return glob.match(path); +} -win32.matchesGlob = createMatchesGlob(true); -posix.matchesGlob = createMatchesGlob(false); +posix.matchesGlob = win32.matchesGlob = matchesGlob; export default process.platform === "win32" ? win32 : posix; diff --git a/test/js/node/path/matches-glob.test.ts b/test/js/node/path/matches-glob.test.ts new file mode 100644 index 00000000000000..3b07c725aa5e45 --- /dev/null +++ b/test/js/node/path/matches-glob.test.ts @@ -0,0 +1,79 @@ +import path from "path"; + +describe("path.matchesGlob(path, glob)", () => { + const stringLikeObject = { + toString() { + return "hi"; + }, + }; + + it.each([ + // line break + null, + undefined, + 123, + stringLikeObject, + Symbol("hi"), + ])("throws if `path` is not a string", (notAString: any) => { + expect(() => path.matchesGlob(notAString, "*")).toThrow(TypeError); + }); + + it.each([ + // line break + null, + undefined, + 123, + stringLikeObject, + Symbol("hi"), + ])("throws if `glob` is not a string", (notAString: any) => { + expect(() => path.matchesGlob("hi", notAString)).toThrow(TypeError); + }); +}); + +describe("path.posix.matchesGlob(path, glob)", () => { + it.each([ + // line break + ["foo.js", "*.js"], + ["foo.js", "*.[tj]s"], + ["foo.ts", "*.[tj]s"], + ["foo.js", "**/*.js"], + ["foo.js", "**\\*.js"], + ["src/bar/foo.js", "**/*.js"], + ["foo/bar/baz", "foo/[bcr]ar/baz"], + ])("%s matches %s", (pathname, glob) => { + expect(path.posix.matchesGlob(pathname, glob)).toBeTrue(); + }); + it.each([ + // line break + ["foo.js", "*.ts"], + ["src/foo.js", "*.js"], + ["foo.js", "src/*.js"], + ["foo/bar", "*"], + ])("%s does not match %s", (pathname, glob) => { + expect(path.posix.matchesGlob(pathname, glob)).toBeFalse(); + }); +}); + +describe("path.win32.matchesGlob(path, glob)", () => { + it.each([ + // line break + ["foo.js", "*.js"], + ["foo.js", "*.[tj]s"], + ["foo.ts", "*.[tj]s"], + ["foo.js", "**\\*.js"], + ["src\\bar\\foo.js", "**\\*.js"], + ["src\\bar\\foo.js", "**/*.js"], + ["foo\\bar\\baz", "foo\\[bcr]ar\\baz"], + ["foo\\bar\\baz", "foo/[bcr]ar/baz"], + ])("%s matches %s", (pathname, glob) => { + expect(path.win32.matchesGlob(pathname, glob)).toBeTrue(); + }); + it.each([ + // line break + ["foo.js", "*.ts"], + ["foo.js", "src\\*.js"], + ["foo/bar", "*"], + ])("%s does not match %s", (pathname, glob) => { + expect(path.win32.matchesGlob(pathname, glob)).toBeFalse(); + }); +}); From f4d54dac5f15b547551ee6ca3a993d8940e4181f Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Fri, 20 Dec 2024 20:30:51 -0800 Subject: [PATCH 4/5] fixes --- src/js/node/path.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/js/node/path.ts b/src/js/node/path.ts index 6704544e081e54..8129e60bbf8cb8 100644 --- a/src/js/node/path.ts +++ b/src/js/node/path.ts @@ -54,13 +54,11 @@ function loadGlob(): LazyGlob { // loop with the same pattern let prevGlob: Glob | undefined; let prevPattern: string | undefined; -function matchesGlob(path, pattern) { +function matchesGlob(isWindows, path, pattern) { let glob: Glob; validateString(path, "path"); - console.log("before:", pattern); - pattern = pattern.replaceAll("\\", "/"); - console.log("after:", pattern); + if (isWindows) path = path.replaceAll("\\", "/"); if (prevGlob) { $assert(prevPattern !== undefined); @@ -69,12 +67,14 @@ function matchesGlob(path, pattern) { } else { if (LazyGlob === undefined) loadGlob(); validateString(pattern, "pattern"); + if (isWindows) pattern = pattern.replaceAll("\\", "/"); glob = prevGlob = new LazyGlob(pattern); prevPattern = pattern; } } else { loadGlob(); // no prevGlob implies LazyGlob isn't loaded validateString(pattern, "pattern"); + if (isWindows) pattern = pattern.replaceAll("\\", "/"); glob = prevGlob = new LazyGlob(pattern); prevPattern = pattern; } @@ -82,6 +82,8 @@ function matchesGlob(path, pattern) { return glob.match(path); } -posix.matchesGlob = win32.matchesGlob = matchesGlob; +// posix.matchesGlob = win32.matchesGlob = matchesGlob; +posix.matchesGlob = matchesGlob.bind(null, false); +win32.matchesGlob = matchesGlob.bind(null, true); export default process.platform === "win32" ? win32 : posix; From 505c17808e31abf4088b046725aa3e854dbb62df Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Fri, 27 Dec 2024 12:21:30 -0500 Subject: [PATCH 5/5] fix tests --- test/js/node/path/matches-glob.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/js/node/path/matches-glob.test.ts b/test/js/node/path/matches-glob.test.ts index 3b07c725aa5e45..8802be251bc2d4 100644 --- a/test/js/node/path/matches-glob.test.ts +++ b/test/js/node/path/matches-glob.test.ts @@ -37,10 +37,9 @@ describe("path.posix.matchesGlob(path, glob)", () => { ["foo.js", "*.[tj]s"], ["foo.ts", "*.[tj]s"], ["foo.js", "**/*.js"], - ["foo.js", "**\\*.js"], ["src/bar/foo.js", "**/*.js"], ["foo/bar/baz", "foo/[bcr]ar/baz"], - ])("%s matches %s", (pathname, glob) => { + ])("path '%s' matches pattern '%s'", (pathname, glob) => { expect(path.posix.matchesGlob(pathname, glob)).toBeTrue(); }); it.each([ @@ -49,7 +48,7 @@ describe("path.posix.matchesGlob(path, glob)", () => { ["src/foo.js", "*.js"], ["foo.js", "src/*.js"], ["foo/bar", "*"], - ])("%s does not match %s", (pathname, glob) => { + ])("path '%s' does not match pattern '%s'", (pathname, glob) => { expect(path.posix.matchesGlob(pathname, glob)).toBeFalse(); }); }); @@ -65,7 +64,7 @@ describe("path.win32.matchesGlob(path, glob)", () => { ["src\\bar\\foo.js", "**/*.js"], ["foo\\bar\\baz", "foo\\[bcr]ar\\baz"], ["foo\\bar\\baz", "foo/[bcr]ar/baz"], - ])("%s matches %s", (pathname, glob) => { + ])("path '%s' matches gattern '%s'", (pathname, glob) => { expect(path.win32.matchesGlob(pathname, glob)).toBeTrue(); }); it.each([ @@ -73,7 +72,7 @@ describe("path.win32.matchesGlob(path, glob)", () => { ["foo.js", "*.ts"], ["foo.js", "src\\*.js"], ["foo/bar", "*"], - ])("%s does not match %s", (pathname, glob) => { + ])("path '%s' does not match pattern '%s'", (pathname, glob) => { expect(path.win32.matchesGlob(pathname, glob)).toBeFalse(); }); });