// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assert, assertEquals, assertRejects, assertStringIncludes, assertThrows, } from "@std/assert"; import { fromFileUrl, join, joinGlobs, normalize, relative } from "@std/path"; import { expandGlob, type ExpandGlobOptions, expandGlobSync, } from "./expand_glob.ts"; import { IS_DENO_2 } from "../internal/_is_deno_2.ts"; async function expandGlobArray( globString: string, options: ExpandGlobOptions, { forceRoot = "" } = {}, ): Promise { const paths = await Array.fromAsync( expandGlob(globString, options), ({ path }) => path, ); paths.sort(); const root = normalize(forceRoot || options.root || Deno.cwd()); for (const path of paths) { assert(path.startsWith(root)); } const relativePaths = paths.map( (path: string): string => relative(root, path) || ".", ); relativePaths.sort(); return relativePaths; } function expandGlobSyncArray( globString: string, options: ExpandGlobOptions, { forceRoot = "" } = {}, ): string[] { const pathsSync = [...expandGlobSync(globString, options)].map( ({ path }): string => path, ); pathsSync.sort(); const root = normalize(forceRoot || options.root || Deno.cwd()); for (const path of pathsSync) { assert(path.startsWith(root)); } const relativePaths = pathsSync.map( (path: string): string => relative(root, path) || ".", ); relativePaths.sort(); return relativePaths; } const EG_OPTIONS: ExpandGlobOptions = { root: fromFileUrl(new URL(join("testdata", "glob"), import.meta.url)), includeDirs: true, extended: false, }; Deno.test("expandGlob() with wildcard input returns all test data", async function () { const options = EG_OPTIONS; assertEquals(await expandGlobArray("*", options), [ "a[b]c", "abc", "abcdef", "abcdefghi", "link", "subdir", ]); }); Deno.test("expandGlobSync() with wildcard input returns all test data", function () { const options = EG_OPTIONS; assertEquals(expandGlobSyncArray("*", options), [ "a[b]c", "abc", "abcdef", "abcdefghi", "link", "subdir", ]); }); Deno.test("expandGlob() excludes items in `exclude` option", async function () { const options = { ...EG_OPTIONS, exclude: ["abc"] }; assertEquals(await expandGlobArray("*", options), [ "a[b]c", "abcdef", "abcdefghi", "link", "subdir", ]); assertEquals(expandGlobSyncArray("*", options), [ "a[b]c", "abcdef", "abcdefghi", "link", "subdir", ]); }); Deno.test("expandGlob() returns empty array if path doesn't exist", async function () { assertEquals(await expandGlobArray("nonexistent", EG_OPTIONS), []); assertEquals(expandGlobSyncArray("nonexistent", EG_OPTIONS), []); }); Deno.test( "expandGlob() throws permission error if the runtime doesn't have read permission", { permissions: {} }, async function () { { await assertRejects( async () => { await expandGlobArray("*", EG_OPTIONS); }, IS_DENO_2 // TODO(iuioiua): Just use `Deno.errors.NotCapable` once Deno 2 is released. // deno-lint-ignore no-explicit-any ? (Deno as any).errors.NotCapable : Deno.errors.PermissionDenied, "run again with the --allow-read flag", ); } { assertThrows( () => { expandGlobSyncArray("*", EG_OPTIONS); }, IS_DENO_2 // TODO(iuioiua): Just use `Deno.errors.NotCapable` once Deno 2 is released. // deno-lint-ignore no-explicit-any ? (Deno as any).errors.NotCapable : Deno.errors.PermissionDenied, "run again with the --allow-read flag", ); } }, ); Deno.test("expandGlob() with */ input returns subdirs", async function () { const options = EG_OPTIONS; assertEquals(await expandGlobArray("*/", options), [ "a[b]c", "subdir", ]); }); Deno.test("expandGlobSync() with */ input returns subdirs", function () { const options = EG_OPTIONS; assertEquals(expandGlobSyncArray("*/", options), [ "a[b]c", "subdir", ]); }); Deno.test("expandGlob() with subdir/../* input expands parent", async function () { const options = EG_OPTIONS; assertEquals(await expandGlobArray("subdir/../*", options), [ "a[b]c", "abc", "abcdef", "abcdefghi", "link", "subdir", ]); }); Deno.test("expandGlobSync() with subdir/../* input expands parent", function () { const options = EG_OPTIONS; assertEquals(expandGlobSyncArray("subdir/../*", options), [ "a[b]c", "abc", "abcdef", "abcdefghi", "link", "subdir", ]); }); Deno.test("expandGlob() accepts extended option set as true", async function () { const options = { ...EG_OPTIONS, extended: true }; assertEquals(await expandGlobArray("abc?(def|ghi)", options), [ "abc", "abcdef", ]); assertEquals(await expandGlobArray("abc*(def|ghi)", options), [ "abc", "abcdef", "abcdefghi", ]); assertEquals(await expandGlobArray("abc+(def|ghi)", options), [ "abcdef", "abcdefghi", ]); assertEquals(await expandGlobArray("abc@(def|ghi)", options), ["abcdef"]); assertEquals(await expandGlobArray("abc{def,ghi}", options), ["abcdef"]); assertEquals(await expandGlobArray("abc!(def|ghi)", options), ["abc"]); }); Deno.test("expandGlobSync() accepts extended option set as true", function () { const options = { ...EG_OPTIONS, extended: true }; assertEquals(expandGlobSyncArray("abc?(def|ghi)", options), [ "abc", "abcdef", ]); assertEquals(expandGlobSyncArray("abc*(def|ghi)", options), [ "abc", "abcdef", "abcdefghi", ]); assertEquals(expandGlobSyncArray("abc+(def|ghi)", options), [ "abcdef", "abcdefghi", ]); assertEquals(expandGlobSyncArray("abc@(def|ghi)", options), ["abcdef"]); assertEquals(expandGlobSyncArray("abc{def,ghi}", options), ["abcdef"]); assertEquals(expandGlobSyncArray("abc!(def|ghi)", options), ["abc"]); }); Deno.test("expandGlob() with globstar returns all dirs", async function () { const options = { ...EG_OPTIONS }; assertEquals( await expandGlobArray("**/abc", options), ["abc", join("subdir", "abc")], ); }); Deno.test("expandGlobSync() with globstar returns all dirs", function () { const options = { ...EG_OPTIONS }; assertEquals( expandGlobSyncArray("**/abc", options), ["abc", join("subdir", "abc")], ); }); Deno.test("expandGlob() with globstar parent returns all dirs", async function () { const options = { ...EG_OPTIONS, globstar: true }; assertEquals( await expandGlobArray(joinGlobs(["subdir", "**", ".."], options), options), ["."], ); }); Deno.test("expandGlobSync() with globstar parent returns all dirs", function () { const options = { ...EG_OPTIONS, globstar: true }; assertEquals( expandGlobSyncArray(joinGlobs(["subdir", "**", ".."], options), options), ["."], ); }); Deno.test("expandGlob() with globstar parent and globstar option set to false returns current dir", async function () { const options = { ...EG_OPTIONS, globstar: false }; assertEquals(await expandGlobArray("**", options), [ ".", "a[b]c", "abc", "abcdef", "abcdefghi", "link", "subdir", ]); }); Deno.test("expandGlobSync() with globstar parent and globstar option set to false returns current dir", function () { const options = { ...EG_OPTIONS, globstar: false }; assertEquals(expandGlobSyncArray("**", options), [ ".", "a[b]c", "abc", "abcdef", "abcdefghi", "link", "subdir", ]); }); Deno.test("expandGlob() accepts includeDirs option set to false", async function () { const options = { ...EG_OPTIONS, includeDirs: false }; assertEquals(await expandGlobArray("subdir", options), []); }); Deno.test("expandGlobSync() accepts includeDirs option set to false", function () { const options = { ...EG_OPTIONS, includeDirs: false }; assertEquals(expandGlobSyncArray("subdir", options), []); }); Deno.test( "expandGlob() throws permission error without fs permissions", async function () { const exampleUrl = new URL("testdata/expand_wildcard.js", import.meta.url); const command = new Deno.Command(Deno.execPath(), { args: [ "run", "--quiet", "--no-lock", exampleUrl.toString(), ], }); const { code, success, stdout, stderr } = await command.output(); const decoder = new TextDecoder(); assert(!success); assertEquals(code, 1); assertEquals(decoder.decode(stdout), ""); assertStringIncludes( decoder.decode(stderr), // TODO(iuioiua): Just use `Deno.errors.NotCapable` once Deno 2 is released. IS_DENO_2 ? "NotCapable" : "PermissionDenied", ); }, ); Deno.test("expandGlob() returns single entry when root is not glob", async function () { const options = { ...EG_OPTIONS, root: join(EG_OPTIONS.root!, "a[b]c") }; assertEquals(await expandGlobArray("*", options), ["foo"]); }); Deno.test("expandGlobSync() returns single entry when root is not glob", function () { const options = { ...EG_OPTIONS, root: join(EG_OPTIONS.root!, "a[b]c") }; assertEquals(expandGlobSyncArray("*", options), ["foo"]); }); Deno.test("expandGlob() accepts followSymlinks option set to true", async function () { const options = { ...EG_OPTIONS, root: join(EG_OPTIONS.root!, "link"), followSymlinks: true, }; assertEquals(await expandGlobArray("*", options), ["abc"]); }); Deno.test("expandGlobSync() accepts followSymlinks option set to true", function () { const options = { ...EG_OPTIONS, root: join(EG_OPTIONS.root!, "link"), followSymlinks: true, }; assertEquals(expandGlobSyncArray("*", options), ["abc"]); }); Deno.test("expandGlob() accepts followSymlinks option set to true with canonicalize", async function () { const options = { ...EG_OPTIONS, root: join(EG_OPTIONS.root!, "."), followSymlinks: true, }; assertEquals( await expandGlobArray("**/abc", options), ["abc", join("subdir", "abc")], ); }); Deno.test("expandGlobSync() accepts followSymlinks option set to true with canonicalize", function () { const options = { ...EG_OPTIONS, root: join(EG_OPTIONS.root!, "."), followSymlinks: true, }; assertEquals( expandGlobSyncArray("**/abc", options), ["abc", join("subdir", "abc")], ); }); Deno.test("expandGlob() accepts followSymlinks option set to true without canonicalize", async function () { const options = { ...EG_OPTIONS, root: join(EG_OPTIONS.root!, "."), followSymlinks: true, canonicalize: false, }; assertEquals( await expandGlobArray("**/abc", options), ["abc", join("link", "abc"), join("subdir", "abc")], ); }); Deno.test("expandGlobSync() accepts followSymlinks option set to true without canonicalize", function () { const options = { ...EG_OPTIONS, root: join(EG_OPTIONS.root!, "."), followSymlinks: true, canonicalize: false, }; assertEquals( expandGlobSyncArray("**/abc", options), ["abc", join("link", "abc"), join("subdir", "abc")], ); }); Deno.test( "expandGlob() does not require read permissions when root path is specified", { permissions: { read: [EG_OPTIONS.root!] }, }, async function () { const options = { root: EG_OPTIONS.root! }; assertEquals(await expandGlobArray("abc", options), ["abc"]); }, ); Deno.test( "expandGlobSync() does not require read permissions when root path is specified", { permissions: { read: [EG_OPTIONS.root!] }, }, function () { const options = { root: EG_OPTIONS.root! }; assertEquals(expandGlobSyncArray("abc", options), ["abc"]); }, ); Deno.test( "expandGlob() does not require read permissions when an absolute glob is specified", { permissions: { read: [EG_OPTIONS.root!] }, }, async function () { assertEquals( await expandGlobArray(`${EG_OPTIONS.root!}/abc`, {}, { forceRoot: EG_OPTIONS.root!, }), ["abc"], ); }, ); Deno.test( "expandGlobSync() does not require read permissions when an absolute glob is specified", { permissions: { read: [EG_OPTIONS.root!] }, }, function () { assertEquals( expandGlobSyncArray(`${EG_OPTIONS.root!}/abc`, {}, { forceRoot: EG_OPTIONS.root!, }), ["abc"], ); }, );