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
2 changes: 1 addition & 1 deletion dist/gitlab/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ fi
set +u
set -o pipefail
${r} | bash
`.trim()]}}const dt=2e3;function getVitePlusHome(e=process.platform,t=process.env){let n=e===`win32`?t.USERPROFILE||T():t.HOME||T();return y(n,`.vite-plus`)}function runInstallCommand(e,t,n=process.platform){let{command:r,args:i}=getInstallScriptCommand(e,n,t.VP_VPDIRS_AWARE===`1`),a={env:{...process.env,...t},stdio:`inherit`},o=w(r,i,a);if(n===`win32`&&o.error?.code===`ENOENT`&&(o=w(`powershell.exe`,i,a)),o.error)throw o.error;return o.status??1}async function installVitePlus(e,t={}){let n=t.platform??process.platform,r=t.env??process.env,i=t.prependPath,a=t.sleep??ee,o=t.runInstall??runInstallCommand,s=t.logWarningFn??console.warn,c={...Object.fromEntries(Object.entries(r).filter(e=>e[1]!==void 0)),VP_VERSION:e,VITE_PLUS_VERSION:e},l=supportsVitePlusDirs(e)?createVitePlusDirsFile():void 0;l?(c.VP_VPDIRS_AWARE=`1`,c[Q]=l):(delete c.VP_VPDIRS_AWARE,delete c[Q]);let u=pkgPrNewCommitSha(e);u&&(c.VP_PR_VERSION=u);let{pinned:d,latest:f}=getInstallScriptUrls(e,n),p=d.length+f.length,m=2*p,h=``,g=0;async function tryUrls(e){for(let t=0;t<2;t+=1)for(let t of e){g+=1;try{let e=o(t,c,n);if(e===0)return!0;h=`exit code ${e}`}catch(e){h=e instanceof Error?e.message:String(e)}g<m&&(s(`setup-vp: failed to install Vite+ from ${t} (${h}). Retrying in ${dt}ms... (attempt ${g+1}/${m})`),await a(dt))}return!1}function ensureBinInPath(){let t=resolveVitePlusBinDir(e,l,y(getVitePlusHome(n,r),`bin`)),a=n===`win32`?`;`:`:`;r.PATH?.split(a).includes(t)||(r.PATH=`${t}${a}${r.PATH||``}`,i?.(t))}try{if(d.length>0){if(await tryUrls(d)){ensureBinInPath();return}s(`setup-vp: could not fetch the install script pinned to Vite+ ${e}. Falling back to the latest install script. The latest script may not be compatible with ${e}.`)}if(await tryUrls(f)){ensureBinInPath();return}throw Error(`Failed to install Vite+ after ${m} attempts across ${p} URL(s): ${h}`)}finally{l&&removeVitePlusDirsFile(l)}}function copyCacheDirectory(e,t,r){e=h.resolve(e),t=h.resolve(t),n(e,t,{recursive:!0,force:r,errorOnExist:!1,filter:(n,i)=>{let s=a(i,{throwIfNoEntry:!1}),c=a(n);if(s&&!r)return c.isDirectory()&&s.isDirectory();if(s&&(s.isSymbolicLink()||c.isSymbolicLink())&&d(i,{recursive:!0,force:!0}),c.isSymbolicLink()){let r=h.resolve(h.dirname(n),u(n)),a=r;if(isWithin(r,e)){let n=h.join(t,h.relative(e,r));a=h.relative(h.dirname(i),n)||`.`}return o(h.dirname(i),{recursive:!0}),p(a,i,f(n,{throwIfNoEntry:!1})?.isDirectory()?`dir`:`file`),!1}return!0}})}function restoreCacheSnapshot(e,t,n=console.warn,r=!0){if(!e.ready||!e.cachePath||!e.lockFile||!e.lockType)return{hit:!1,save:()=>{}};let a=e.cachePath;if(isWithin(h.resolve(a),h.resolve(t))||isWithin(h.resolve(t),h.resolve(a)))return n(`setup-vp: package-manager cache overlaps the snapshot directory; skipping cache.`),{hit:!1,save:()=>{}};let s=h.join(t,process.platform,process.arch,e.lockType),l=h.join(s,`packages`),u=h.join(s,`lock-hash`),d=e.lockFile;function hashLockFile(){return te(`sha256`).update(c(d)).digest(`hex`)}let f;try{f=hashLockFile()}catch(e){return n(`setup-vp: could not read cache lock file: ${String(e)}`),{hit:!1,save:()=>{}}}let p=!1;try{r&&i(l)&&(o(a,{recursive:!0}),copyCacheDirectory(l,a,!1),p=c(u,`utf8`)===f)}catch(e){n(`setup-vp: could not restore package-manager cache: ${String(e)}`)}return{hit:p,save:()=>{try{if(!i(a))return;let e=hashLockFile();o(s,{recursive:!0}),copyCacheDirectory(a,l,!0),m(u,e,`utf8`)}catch(e){n(`setup-vp: could not save package-manager cache: ${String(e)}`)}}}}const ft=new Set([`true`,`True`,`TRUE`]),pt=new Set([`false`,`False`,`FALSE`]);function parseNodeManager(e){if(e){if(ft.has(e))return!0;if(pt.has(e))return!1;throw Error(`Invalid node-manager input: "${e}". Expected "true", "false", or unset.`)}}function supportsScopedEnv(e){let t=parseInstalledVpVersion(e);if(t.startsWith(`0.0.0-commit.`))return!0;let n=t.match(/^(\d+)\.(\d+)\.(\d+)/);if(!n)throw Error(`Could not determine the installed Vite+ version for env configuration.`);let[,r,i,a]=n.map(Number);return r>0||i>3||i===3&&a>=1}function nodeManagerOffArgs(e){return parseInstalledVpVersion(e).startsWith(`0.0.0-commit.`)&&console.info("setup-vp: node-manager: false requires a preview build that supports `vp env off node`. If this command is unsupported, use a newer preview or a stable release. setup-vp will not fall back to disabling package-manager management."),supportsScopedEnv(e)?[`env`,`off`,`node`]:[`env`,`off`]}const mt=[`npm`,`pnpm`,`yarn`,`bun`];function parsePackageManager(e){if(!e?.trim())return;let t;try{t=(0,Y.parse)(e)}catch(e){throw Error(`Invalid package-manager input: ${String(e)}`)}if(t!==``){if(typeof t==`boolean`)return t;if(t&&typeof t==`object`&&!Array.isArray(t)){for(let[e,n]of Object.entries(t))if(!mt.includes(e)||typeof n!=`boolean`)throw Error(`Invalid package-manager input: ${e} must be npm, pnpm, yarn, or bun with a boolean value.`);return t}throw Error(`Invalid package-manager input: expected a boolean or a mapping of npm, pnpm, yarn, and bun to booleans.`)}}function packageManagerArgs(e,t){if(e===void 0)return[];if(!supportsScopedEnv(t))throw Error(`package-manager configuration requires Vite+ 0.3.1 or newer.`);return typeof e==`boolean`?e?[]:[[`env`,`off`,`pm`]]:mt.filter(t=>e[t]===!1).map(e=>[`env`,`off`,e])}function readNpmrc(e){try{return c(e,`utf8`)}catch(e){if(e.code===`ENOENT`)return``;throw e}}function authKeyFor(e){return(e.replace(/^\w+:/,``)+`:_authtoken`).toLowerCase()}function analyzeProjectNpmrc(e){let t=new Set,n=new Set,r=new Set;for(let i of e.split(/\r?\n/)){let e=i.trim();if(!e||e.startsWith(`;`)||e.startsWith(`#`))continue;let a=e.indexOf(`=`);if(a<0)continue;let o=e.slice(0,a).trim().toLowerCase(),s=e.slice(a+1).trim();(o===`registry`||o.endsWith(`:registry`))&&(s.includes("${")||t.add(s.endsWith(`/`)?s:s+`/`)),o.startsWith(`//`)&&o.endsWith(`:_authtoken`)&&n.add(o);for(let e of s.matchAll(/\$\{(\w+)\}/g))r.add(e[1])}return{registriesNeedingAuth:[...t].filter(e=>!n.has(authKeyFor(e))),envVarRefs:r}}function readUserConfigLines(e,t){let n=e.HOME||e.USERPROFILE,r=e.NPM_CONFIG_USERCONFIG||e.PNPM_CONFIG_USERCONFIG||n&&h.join(n,`.npmrc`);return(r?readNpmrc(r):``).split(/\r?\n/).filter(e=>!t.has(e.split(`=`)[0].trim().toLowerCase()))}function isReservedAuthVariable(e){return[`GITHUB_TOKEN`,`CI_JOB_TOKEN`,`SYSTEM_ACCESSTOKEN`].includes(e)?!1:/^(?:PATH|HOME|USERPROFILE|TMPDIR|CI)$/i.test(e)||/^(?:SETUP_VP_|RUNNER_|GITHUB_|CI_|SYSTEM_|AGENT_|BUILD_|RELEASE_)/i.test(e)}function writeUserConfig(e,t,n){let r=s(h.join(E(),`setup-vp-npmrc-`)),i=h.join(r,`.npmrc`);m(i,e,{encoding:`utf8`,mode:384});for(let e of[`NPM_CONFIG_USERCONFIG`,`PNPM_CONFIG_USERCONFIG`])t[e]=i,n?.(e,i);return i}function configureAuth$1(e,t,n,r,i){if(!e){if(!i)return;let{registriesNeedingAuth:e,envVarRefs:t}=analyzeProjectNpmrc(readNpmrc(h.join(i,`.npmrc`))),a;if(n.NODE_AUTH_TOKEN&&e.length>0){let i=readUserConfigLines(n,new Set(e.map(e=>(e.replace(/^\w+:/,``)+`:_authtoken`).toLowerCase())));i.push(...e.map(e=>e.replace(/^\w+:/,``)+":_authToken=${NODE_AUTH_TOKEN}")),a=writeUserConfig(i.join(`
`.trim()]}}const dt=2e3;function getVitePlusHome(e=process.platform,t=process.env){let n=e===`win32`?t.USERPROFILE||T():t.HOME||T();return y(n,`.vite-plus`)}function runInstallCommand(e,t,n=process.platform){let{command:r,args:i}=getInstallScriptCommand(e,n,t.VP_VPDIRS_AWARE===`1`),a={env:{...process.env,...t},stdio:`inherit`},o=w(r,i,a);if(n===`win32`&&o.error?.code===`ENOENT`&&(o=w(`powershell.exe`,i,a)),o.error)throw o.error;return o.status??1}async function installVitePlus(e,t={}){let n=t.platform??process.platform,r=t.env??process.env,i=t.prependPath,a=t.sleep??ee,o=t.runInstall??runInstallCommand,s=t.logWarningFn??console.warn,c={...Object.fromEntries(Object.entries(r).filter(e=>e[1]!==void 0)),VP_VERSION:e,VITE_PLUS_VERSION:e},l=supportsVitePlusDirs(e)?createVitePlusDirsFile():void 0;l?(c.VP_VPDIRS_AWARE=`1`,c[Q]=l):(delete c.VP_VPDIRS_AWARE,delete c[Q]);let u=pkgPrNewCommitSha(e);u&&(c.VP_PR_VERSION=u);let{pinned:d,latest:f}=getInstallScriptUrls(e,n),p=d.length+f.length,m=2*p,h=``,g=0;async function tryUrls(e){for(let t=0;t<2;t+=1)for(let t of e){g+=1;try{let e=o(t,c,n);if(e===0)return!0;h=`exit code ${e}`}catch(e){h=e instanceof Error?e.message:String(e)}g<m&&(s(`setup-vp: failed to install Vite+ from ${t} (${h}). Retrying in ${dt}ms... (attempt ${g+1}/${m})`),await a(dt))}return!1}function ensureBinInPath(){let t=resolveVitePlusBinDir(e,l,y(getVitePlusHome(n,r),`bin`)),a=n===`win32`?`;`:`:`;r.PATH?.split(a).includes(t)||(r.PATH=`${t}${a}${r.PATH||``}`,i?.(t))}try{if(d.length>0){if(await tryUrls(d)){ensureBinInPath();return}s(`setup-vp: could not fetch the install script pinned to Vite+ ${e}. Falling back to the latest install script. The latest script may not be compatible with ${e}.`)}if(await tryUrls(f)){ensureBinInPath();return}throw Error(`Failed to install Vite+ after ${m} attempts across ${p} URL(s): ${h}`)}finally{l&&removeVitePlusDirsFile(l)}}function copyCacheDirectory(e,t,r){e=h.resolve(e),t=h.resolve(t),n(e,t,{recursive:!0,force:r,errorOnExist:!1,filter:(n,i)=>{let s=a(i,{throwIfNoEntry:!1}),c=a(n);if(s&&!r)return c.isDirectory()&&s.isDirectory();if(s&&(s.isSymbolicLink()||c.isSymbolicLink())&&d(i,{recursive:!0,force:!0}),c.isSymbolicLink()){let r=h.resolve(h.dirname(n),u(n)),a=r;if(isWithin(r,e)){let n=h.join(t,h.relative(e,r));a=h.relative(h.dirname(i),n)||`.`}return o(h.dirname(i),{recursive:!0}),p(a,i,f(r,{throwIfNoEntry:!1})?.isDirectory()?`dir`:`file`),!1}return!0}})}function restoreCacheSnapshot(e,t,n=console.warn,r=!0){if(!e.ready||!e.cachePath||!e.lockFile||!e.lockType)return{hit:!1,save:()=>{}};let a=e.cachePath;if(isWithin(h.resolve(a),h.resolve(t))||isWithin(h.resolve(t),h.resolve(a)))return n(`setup-vp: package-manager cache overlaps the snapshot directory; skipping cache.`),{hit:!1,save:()=>{}};let s=h.join(t,process.platform,process.arch,e.lockType),l=h.join(s,`packages`),u=h.join(s,`lock-hash`),d=e.lockFile;function hashLockFile(){return te(`sha256`).update(c(d)).digest(`hex`)}let f;try{f=hashLockFile()}catch(e){return n(`setup-vp: could not read cache lock file: ${String(e)}`),{hit:!1,save:()=>{}}}let p=!1;try{r&&i(l)&&(o(a,{recursive:!0}),copyCacheDirectory(l,a,!1),p=c(u,`utf8`)===f)}catch(e){n(`setup-vp: could not restore package-manager cache: ${String(e)}`)}return{hit:p,save:()=>{try{if(!i(a))return;let e=hashLockFile();o(s,{recursive:!0}),copyCacheDirectory(a,l,!0),m(u,e,`utf8`)}catch(e){n(`setup-vp: could not save package-manager cache: ${String(e)}`)}}}}const ft=new Set([`true`,`True`,`TRUE`]),pt=new Set([`false`,`False`,`FALSE`]);function parseNodeManager(e){if(e){if(ft.has(e))return!0;if(pt.has(e))return!1;throw Error(`Invalid node-manager input: "${e}". Expected "true", "false", or unset.`)}}function supportsScopedEnv(e){let t=parseInstalledVpVersion(e);if(t.startsWith(`0.0.0-commit.`))return!0;let n=t.match(/^(\d+)\.(\d+)\.(\d+)/);if(!n)throw Error(`Could not determine the installed Vite+ version for env configuration.`);let[,r,i,a]=n.map(Number);return r>0||i>3||i===3&&a>=1}function nodeManagerOffArgs(e){return parseInstalledVpVersion(e).startsWith(`0.0.0-commit.`)&&console.info("setup-vp: node-manager: false requires a preview build that supports `vp env off node`. If this command is unsupported, use a newer preview or a stable release. setup-vp will not fall back to disabling package-manager management."),supportsScopedEnv(e)?[`env`,`off`,`node`]:[`env`,`off`]}const mt=[`npm`,`pnpm`,`yarn`,`bun`];function parsePackageManager(e){if(!e?.trim())return;let t;try{t=(0,Y.parse)(e)}catch(e){throw Error(`Invalid package-manager input: ${String(e)}`)}if(t!==``){if(typeof t==`boolean`)return t;if(t&&typeof t==`object`&&!Array.isArray(t)){for(let[e,n]of Object.entries(t))if(!mt.includes(e)||typeof n!=`boolean`)throw Error(`Invalid package-manager input: ${e} must be npm, pnpm, yarn, or bun with a boolean value.`);return t}throw Error(`Invalid package-manager input: expected a boolean or a mapping of npm, pnpm, yarn, and bun to booleans.`)}}function packageManagerArgs(e,t){if(e===void 0)return[];if(!supportsScopedEnv(t))throw Error(`package-manager configuration requires Vite+ 0.3.1 or newer.`);return typeof e==`boolean`?e?[]:[[`env`,`off`,`pm`]]:mt.filter(t=>e[t]===!1).map(e=>[`env`,`off`,e])}function readNpmrc(e){try{return c(e,`utf8`)}catch(e){if(e.code===`ENOENT`)return``;throw e}}function authKeyFor(e){return(e.replace(/^\w+:/,``)+`:_authtoken`).toLowerCase()}function analyzeProjectNpmrc(e){let t=new Set,n=new Set,r=new Set;for(let i of e.split(/\r?\n/)){let e=i.trim();if(!e||e.startsWith(`;`)||e.startsWith(`#`))continue;let a=e.indexOf(`=`);if(a<0)continue;let o=e.slice(0,a).trim().toLowerCase(),s=e.slice(a+1).trim();(o===`registry`||o.endsWith(`:registry`))&&(s.includes("${")||t.add(s.endsWith(`/`)?s:s+`/`)),o.startsWith(`//`)&&o.endsWith(`:_authtoken`)&&n.add(o);for(let e of s.matchAll(/\$\{(\w+)\}/g))r.add(e[1])}return{registriesNeedingAuth:[...t].filter(e=>!n.has(authKeyFor(e))),envVarRefs:r}}function readUserConfigLines(e,t){let n=e.HOME||e.USERPROFILE,r=e.NPM_CONFIG_USERCONFIG||e.PNPM_CONFIG_USERCONFIG||n&&h.join(n,`.npmrc`);return(r?readNpmrc(r):``).split(/\r?\n/).filter(e=>!t.has(e.split(`=`)[0].trim().toLowerCase()))}function isReservedAuthVariable(e){return[`GITHUB_TOKEN`,`CI_JOB_TOKEN`,`SYSTEM_ACCESSTOKEN`].includes(e)?!1:/^(?:PATH|HOME|USERPROFILE|TMPDIR|CI)$/i.test(e)||/^(?:SETUP_VP_|RUNNER_|GITHUB_|CI_|SYSTEM_|AGENT_|BUILD_|RELEASE_)/i.test(e)}function writeUserConfig(e,t,n){let r=s(h.join(E(),`setup-vp-npmrc-`)),i=h.join(r,`.npmrc`);m(i,e,{encoding:`utf8`,mode:384});for(let e of[`NPM_CONFIG_USERCONFIG`,`PNPM_CONFIG_USERCONFIG`])t[e]=i,n?.(e,i);return i}function configureAuth$1(e,t,n,r,i){if(!e){if(!i)return;let{registriesNeedingAuth:e,envVarRefs:t}=analyzeProjectNpmrc(readNpmrc(h.join(i,`.npmrc`))),a;if(n.NODE_AUTH_TOKEN&&e.length>0){let i=readUserConfigLines(n,new Set(e.map(e=>(e.replace(/^\w+:/,``)+`:_authtoken`).toLowerCase())));i.push(...e.map(e=>e.replace(/^\w+:/,``)+":_authToken=${NODE_AUTH_TOKEN}")),a=writeUserConfig(i.join(`
`)+`
`,n,r),t.add(`NODE_AUTH_TOKEN`)}for(let e of t)!isReservedAuthVariable(e)&&n[e]&&r?.(e,n[e]);return a}let a;try{a=new URL(e)}catch{throw Error(`Invalid registry-url: "${e}". Must be a valid URL.`)}let o=a.href.endsWith(`/`)?a.href:`${a.href}/`,s=``;t&&(s=`${(t.startsWith(`@`)?t:`@${t}`).toLowerCase()}:`);let c=o.replace(/^\w+:/,``).toLowerCase(),l=readUserConfigLines(n,new Set([`${s}registry`,`${c}:_authtoken`]));l.push(`${c}:_authToken=\${NODE_AUTH_TOKEN}`,`${s}registry=${o}`);let u=writeUserConfig(l.filter(Boolean).join(`
`)+`
Expand Down
71 changes: 71 additions & 0 deletions src/ci/cache-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
realpathSync,
readlinkSync,
renameSync,
statSync,
symlinkSync,
unlinkSync,
} from "node:fs";
Expand All @@ -18,8 +19,14 @@ import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
import { restoreCacheSnapshot } from "./cache-snapshot.js";

vi.mock("node:fs", async (importOriginal) => {
const fs = await importOriginal<typeof import("node:fs")>();
return { ...fs, statSync: vi.fn(fs.statSync) };
});

const directories: string[] = [];
afterEach(() => {
vi.mocked(statSync).mockReset();
for (const dir of directories.splice(0)) rmSync(dir, { recursive: true, force: true });
});
function fixture() {
Expand Down Expand Up @@ -159,6 +166,70 @@ describe("GitLab cache snapshots", () => {
expect(restoreCacheSnapshot(metadata, cache).hit).toBe(true);
});

it("restores links whose target is readable but following the cached link fails", async () => {
const { root, store, cache, metadata } = fixture();
const target = path.join(root, "project");
const link = path.join(store, "project-link");
const cachedLink = path.join(
cache,
process.platform,
process.arch,
"npm/packages/project-link",
);
mkdirSync(target);
writeFileSync(path.join(target, "value"), "project data");
writeFileSync(path.join(store, "package"), "cached package");
symlinkSync(target, link, process.platform === "win32" ? "junction" : "dir");
const warn = vi.fn();
restoreCacheSnapshot(metadata, cache, warn).save();
rmSync(store, { recursive: true });

const fs = await vi.importActual<typeof import("node:fs")>("node:fs");
vi.mocked(statSync).mockImplementation((file, options) => {
if (file === cachedLink) {
throw Object.assign(new Error("operation not permitted, stat cached link"), {
code: "EPERM",
});
}
return fs.statSync(file, options);
});

expect(restoreCacheSnapshot(metadata, cache, warn).hit).toBe(true);
expect(readFileSync(path.join(link, "value"), "utf8")).toBe("project data");
expect(readFileSync(path.join(store, "package"), "utf8")).toBe("cached package");
expect(warn).not.toHaveBeenCalled();
});

it.skipIf(process.platform !== "win32")(
"repairs a directory link extracted as a file link before its target exists",
() => {
const { root, store, cache, metadata } = fixture();
const target = path.join(root, "project-from-artifacts");
const cachedLink = path.join(
cache,
process.platform,
process.arch,
"npm/packages/project-link",
);
const warn = vi.fn();
writeFileSync(path.join(store, "package"), "cached package");
restoreCacheSnapshot(metadata, cache, warn).save();
rmSync(store, { recursive: true });

// Cache extraction precedes artifacts. A missing directory target can
// cause an extractor to create a file symlink, which Windows cannot follow.
symlinkSync(target, cachedLink, "file");
mkdirSync(target);
writeFileSync(path.join(target, "value"), "project data");
expect(() => statSync(cachedLink)).toThrow(expect.objectContaining({ code: "EPERM" }));

expect(restoreCacheSnapshot(metadata, cache, warn).hit).toBe(true);
expect(readFileSync(path.join(store, "project-link/value"), "utf8")).toBe("project data");
expect(readFileSync(path.join(store, "package"), "utf8")).toBe("cached package");
expect(warn).not.toHaveBeenCalled();
},
);

it.each(["file", "directory"])("replaces a cached %s with a symlink on save", (entryKind) => {
const { store, cache, lockFile, metadata } = fixture();
const entry = path.join(store, "entry");
Expand Down
5 changes: 4 additions & 1 deletion src/ci/cache-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ function copyCacheDirectory(source: string, destination: string, overwrite: bool
relocated = path.relative(path.dirname(dest), destinationTarget) || ".";
}
mkdirSync(path.dirname(dest), { recursive: true });
// Archive extraction can create a Windows file link before its directory
// target exists. Stat the target itself: following that link raises EPERM
// even after artifacts have restored the target directory.
symlinkSync(
relocated,
dest,
statSync(src, { throwIfNoEntry: false })?.isDirectory() ? "dir" : "file",
statSync(target, { throwIfNoEntry: false })?.isDirectory() ? "dir" : "file",
);
return false;
}
Expand Down
Loading