Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
1297798
tmp save
mmaietta Oct 8, 2025
8092a37
tmp save
mmaietta Oct 8, 2025
9aee8ef
test
mmaietta Oct 8, 2025
89f2070
prettier
mmaietta Oct 8, 2025
4d6f516
tmp save
mmaietta Oct 9, 2025
135a300
new tests for package manager!
mmaietta Oct 10, 2025
0175b9c
tmp save
mmaietta Oct 10, 2025
7ecd88a
making good progress
mmaietta Oct 11, 2025
0207f33
cleaning up
mmaietta Oct 11, 2025
6d4ab3a
tmp save. adding more corepack tests for yarn. new yarn collector log…
mmaietta Oct 11, 2025
1e09359
some be actually working
mmaietta Oct 11, 2025
fdcc7ca
yeah that works
mmaietta Oct 11, 2025
9d37a74
yes yes yes i think
mmaietta Oct 11, 2025
3ae7050
Merge commit '0835fbcac0a0cfb0f34355699812cc85db035ad4' into packagem…
mmaietta Oct 12, 2025
029c21e
coooorepack
mmaietta Oct 12, 2025
3ab95f5
tmp save
mmaietta Oct 13, 2025
15ced87
retry
mmaietta Oct 14, 2025
7a35d87
tmp save?
mmaietta Oct 14, 2025
128bdf4
adding a version map. Let's default to yarn v1 for tests for now
mmaietta Oct 16, 2025
ad28576
let's force specific versions via corepack
mmaietta Oct 16, 2025
115d4f7
temp save
mmaietta Oct 16, 2025
1575a45
temp save
mmaietta Oct 16, 2025
15021ca
temp save
mmaietta Oct 16, 2025
6430b0f
Merge commit '144c5ed2f9bdc9a811828a7be8f06658e1c28702' into packagem…
mmaietta Oct 16, 2025
e28486d
cleanup post-merge master
mmaietta Oct 16, 2025
e32c725
force older version of yarn for native module test
mmaietta Oct 17, 2025
8516903
converting to async extraction
mmaietta Oct 17, 2025
4a89a20
tmp save
mmaietta Oct 17, 2025
8d2bdb2
converting to async
mmaietta Oct 18, 2025
995a662
specifically force yarn
mmaietta Oct 18, 2025
c52a787
allow overrides via packTester config directly
mmaietta Oct 18, 2025
7443142
cleaning up fixtures to base required files?
mmaietta Oct 19, 2025
daeefdf
update it
mmaietta Oct 19, 2025
ee4670e
tmp save
mmaietta Oct 19, 2025
5bdec92
better file extraction
mmaietta Oct 19, 2025
67a7318
cleanup
mmaietta Oct 19, 2025
2e4ff3e
refactor
mmaietta Oct 20, 2025
8b9a591
remove arg
mmaietta Oct 20, 2025
182472d
update
mmaietta Oct 20, 2025
65ec783
updates
mmaietta Oct 20, 2025
9f9a3a4
cleanup
mmaietta Oct 20, 2025
d8c013f
update test
mmaietta Oct 20, 2025
aff34a2
converting node collector to async
mmaietta Oct 20, 2025
de73a1f
convert a few things to async
mmaietta Oct 20, 2025
18ce7c8
comments, tmp save
mmaietta Oct 20, 2025
f0778d6
cleanup
mmaietta Oct 20, 2025
f561ab7
oops
mmaietta Oct 20, 2025
81aaa50
oops...again
mmaietta Oct 20, 2025
7fae227
force pm and use temp dir for testing asarUtil
mmaietta Oct 20, 2025
2f58d70
got em? only check symlink path against system files after determinin…
mmaietta Oct 21, 2025
a054dc6
tmp save
mmaietta Oct 21, 2025
528478f
adding new unit test
mmaietta Oct 21, 2025
f3137c6
clean up
mmaietta Oct 21, 2025
7d5d747
splitting out a yarn berry module collector to separate logic and red…
mmaietta Oct 22, 2025
6fa14ed
big changes
mmaietta Oct 22, 2025
ea406fb
cleanup
mmaietta Oct 22, 2025
1e24e26
tmp save
mmaietta Oct 22, 2025
25011a0
clean up logs, migrate to async
mmaietta Oct 22, 2025
c4e09d9
tmp save
mmaietta Oct 22, 2025
9f1eba4
CI retest please
mmaietta Oct 23, 2025
148db8e
more unit tests
mmaietta Oct 23, 2025
c8c381e
updates
mmaietta Oct 23, 2025
dc83df0
tmp save
mmaietta Oct 23, 2025
e05cc7d
clear changes to snapshots and rerun
mmaietta Oct 23, 2025
93c3d8e
try again
mmaietta Oct 23, 2025
e32de5c
tmp save
mmaietta Oct 23, 2025
4ee6fd4
resnapshot
mmaietta Oct 24, 2025
53617f2
cleanup
mmaietta Oct 24, 2025
b1aedb7
herp derp
mmaietta Oct 24, 2025
f07ef9a
two-package.json snapshot update
mmaietta Oct 24, 2025
1af1c11
tmp savae
mmaietta Oct 25, 2025
7d5940d
test it
mmaietta Oct 25, 2025
beef780
test again. stricter typesafety enforced on tsdoc
mmaietta Oct 25, 2025
255b6b6
tmp save new module resolution logic
mmaietta Oct 25, 2025
2fe6fc5
tmp save
mmaietta Oct 25, 2025
b8fd81c
stuff
mmaietta Oct 26, 2025
89b2c76
oh shit
mmaietta Oct 26, 2025
bbd0f48
new snapshots dropped
mmaietta Oct 26, 2025
e9f8df8
so try me maybe
mmaietta Oct 26, 2025
28a901e
add fixture entry point to match package.json
mmaietta Oct 26, 2025
6e5a054
complex module resolution I guess works
mmaietta Oct 27, 2025
6463747
just use filter first to allow packages that don't have a package.jso…
mmaietta Oct 27, 2025
8ba119a
clean up
mmaietta Oct 27, 2025
9d16d9f
F yarn optional dependency resolution lol
mmaietta Oct 27, 2025
9e8bc20
yep getting closer
mmaietta Oct 28, 2025
002b9af
lockfile snapshots
mmaietta Oct 28, 2025
bc082f4
wonky yarn berry during CI mode
mmaietta Oct 28, 2025
f3286c8
tmp save
mmaietta Oct 28, 2025
c7a2326
it works!
mmaietta Oct 29, 2025
e80e00e
temp save
mmaietta Oct 29, 2025
cfae40e
got em??
mmaietta Oct 29, 2025
8761e60
I think we got it
mmaietta Oct 29, 2025
aa8059f
tmptmp savae
mmaietta Oct 29, 2025
1b9daa1
optimize?
mmaietta Oct 29, 2025
f037ae3
memoize
mmaietta Oct 30, 2025
88f6579
update mac snapshot
mmaietta Oct 30, 2025
ceffe6c
normalize path for windows
mmaietta Oct 30, 2025
01c002c
normalize
mmaietta Oct 31, 2025
ceb7b78
try again
mmaietta Nov 2, 2025
50c65a0
add more logging
mmaietta Nov 2, 2025
c5dbb14
try again
mmaietta Nov 2, 2025
f1ccb9e
try again
mmaietta Nov 2, 2025
45c61ef
try again
mmaietta Nov 2, 2025
68e6c42
log less
mmaietta Nov 3, 2025
8992583
log visible
mmaietta Nov 3, 2025
99f2b88
tmp save
mmaietta Nov 3, 2025
d659dd8
idk anymore, lets log more
mmaietta Nov 3, 2025
37cc5cd
try filtering out types, why are they being collected anyways
mmaietta Nov 3, 2025
d6a6d41
retry
mmaietta Nov 4, 2025
833e6b8
change up logging
mmaietta Nov 4, 2025
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
4 changes: 2 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
- ArtifactPublisherTest,BuildTest,ExtraBuildTest,RepoSlugTest,binDownloadTest,configurationValidationTest,filenameUtilTest,filesTest,globTest,httpExecutorTest,ignoreTest,macroExpanderTest,mainEntryTest,urlUtilTest,extraMetadataTest,linuxArchiveTest,linuxPackagerTest,HoistedNodeModuleTest,MemoLazyTest,HoistTest,ExtraBuildResourcesTest,utilTest
- snapTest,debTest,fpmTest,protonTest
- winPackagerTest,winCodeSignTest,webInstallerTest
- oneClickInstallerTest,assistedInstallerTest
- oneClickInstallerTest,assistedInstallerTest,packageManagerTest
- concurrentBuildsTest
steps:
- name: Checkout code repository
Expand Down Expand Up @@ -188,7 +188,7 @@ jobs:
fail-fast: false
matrix:
testFiles:
- winCodeSignTest,differentialUpdateTest,squirrelWindowsTest
- winCodeSignTest,differentialUpdateTest,squirrelWindowsTest,HoistedNodeModuleTest
- appxTest,msiTest,portableTest,assistedInstallerTest,protonTest
- BuildTest,oneClickInstallerTest,winPackagerTest,nsisUpdaterTest,webInstallerTest
- concurrentBuildsTest
Expand Down
39 changes: 26 additions & 13 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Vitest TEST_FILES",
"runtimeExecutable": "pnpm",
"program": "ci:test",
"console": "integratedTerminal",
"internalConsoleOptions": "openOnFirstSessionStart",
"env": {
"TEST_FILES": "macPackagerTest",
"UPDATE_SNAPSHOT": "false"
}
}
{
"type": "node",
"request": "launch",
"name": "Vitest TEST_FILES",
"runtimeExecutable": "pnpm",
"program": "ci:test",
"console": "integratedTerminal",
"internalConsoleOptions": "openOnFirstSessionStart",
"nodeArgs": [
"TEST_FILES=packageManagerTest",
"UPDATE_SNAPSHOT=false",
"DEBUG=electron-builder",
"TEST_SEQUENTIAL=true" // Run tests sequentially to debug issues that may be hidden by concurrency (console log pollution when DEBUG flag set)
],
"env": {
"TEST_FILES": "globTest",
"UPDATE_SNAPSHOT": "false",
"DEBUG": "electron-builder",
"TEST_SEQUENTIAL": "true" // Run tests sequentially to debug issues that may be hidden by concurrency (console log pollution when DEBUG flag set)
},
"skipFiles": [
"<node_internals>/**", // Skip Node’s internal modules
"${workspaceFolder}/**/node_modules/**/*.js", // Skip libraries
"**/*.js" // Optionally skip all compiled JS
]
}
]
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"prettier": "prettier 'packages/**/*.{ts,js}' test/src/**/*.ts --write",
"///": "Please see https://github.com/electron-userland/electron-builder/blob/master/CONTRIBUTING.md#run-test-using-cli how to run particular test instead full (and very slow) run",
"test-all": "pnpm compile && pnpm pretest && pnpm ci:test",
"test-linux": "docker run --rm -e CI=${CI:-1} -e DEBUG=${DEBUG:-} -e UPDATE_SNAPSHOT=${UPDATE_SNAPSHOT:-false} -e TEST_FILES=\"${TEST_FILES:-HoistedNodeModuleTest}\" -w /project -v $(pwd):/project -v $(pwd)-node-modules:/project/node_modules -v $HOME/Library/Caches/electron:/root/.cache/electron -v $HOME/Library/Caches/electron-builder:/root/.cache/electron-builder ${ADDITIONAL_DOCKER_ARGS} ${TEST_RUNNER_IMAGE_TAG:-electronuserland/builder:22-wine-mono} /bin/bash -c \"corepack enable && pnpm install && pnpm ci:test\"",
"test-linux": "docker run --rm -e CI=${CI:-true} -e DEBUG=${DEBUG:-} -e UPDATE_SNAPSHOT=${UPDATE_SNAPSHOT:-false} -e TEST_FILES=\"${TEST_FILES:-HoistedNodeModuleTest}\" -w /project -v $(pwd):/project -v $(pwd)-node-modules:/project/node_modules -v $HOME/Library/Caches/electron:/root/.cache/electron -v $HOME/Library/Caches/electron-builder:/root/.cache/electron-builder ${ADDITIONAL_DOCKER_ARGS} ${TEST_RUNNER_IMAGE_TAG:-electronuserland/builder:22-wine-mono} /bin/bash -c \"corepack enable && pnpm install && pnpm ci:test\"",
"test-update": "UPDATE_SNAPSHOT=true vitest run",
"test-ui": "vitest --ui",
"docker-images": "docker/build.sh",
Expand All @@ -28,7 +28,7 @@
"generate-changeset": "pnpm changeset",
"generate-schema": "typescript-json-schema packages/app-builder-lib/tsconfig-scheme.json Configuration --out packages/app-builder-lib/scheme.json --noExtraProps --useTypeOfKeyword --strictNullChecks --required && node ./scripts/fix-schema.js",
"generate-all": "pnpm generate-schema && pnpm prettier",
"ci:test": "vitest run --no-update",
"ci:test": "vitest run --no-update --allowOnly",
"ci:version": "pnpm i && pnpm changelog && changeset version && node scripts/update-package-version-export.js && pnpm compile && pnpm generate-all && git add .",
"ci:publish": "pnpm i && pnpm compile && pnpm publish -r --tag next && changeset tag",
"docs:prebuild": "docker build -t mkdocs-dockerfile -f mkdocs-dockerfile . ",
Expand Down
101 changes: 91 additions & 10 deletions packages/app-builder-lib/src/asar/asarUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { PlatformPackager } from "../platformPackager"
import { ResolvedFileSet, getDestinationPath } from "../util/appFileCopier"
import { detectUnpackedDirs } from "./unpackDetector"
import { Readable } from "stream"
import * as os from "os"

/** @internal */
export class AsarPackager {
Expand Down Expand Up @@ -122,7 +123,7 @@ export class AsarPackager {
transformedData: string | Buffer | undefined
isUnpacked: (path: string) => boolean
}): Promise<AsarStreamType> {
const { isUnpacked, transformedData, file, destination, stat, fileSet } = options
const { isUnpacked, transformedData, file, destination, stat } = options
const unpacked = isUnpacked(destination)

if (!stat.isFile() && !stat.isSymbolicLink()) {
Expand All @@ -143,13 +144,8 @@ export class AsarPackager {
return { path: destination, streamGenerator, unpacked, type: "file", stat: { mode: stat.mode, size } }
}

const realPathFile = await fs.realpath(file)
const realPathRelative = path.relative(fileSet.src, realPathFile)
const isOutsidePackage = realPathRelative.startsWith("..")
if (isOutsidePackage) {
log.error({ source: log.filePath(file), realPathFile: log.filePath(realPathFile) }, `unable to copy, file is symlinked outside the package`)
throw new Error(`Cannot copy file (${path.basename(file)}) symlinked to file (${path.basename(realPathFile)}) outside the package as that violates asar security integrity`)
}
// verify that the file is not a direct link or symlinked to access/copy a system file
await this.protectSystemAndUnsafePaths(file)

const config = {
path: destination,
Expand All @@ -158,14 +154,17 @@ export class AsarPackager {
stat,
}

// not a symlink, stream directly
if (file === realPathFile) {
// file, stream directly
if (!stat.isSymbolicLink()) {
return {
...config,
type: "file",
}
}

// guard against symlink pointing to outside workspace root
await this.protectSystemAndUnsafePaths(file, await this.packager.info.getWorkspaceRoot())

// okay, it must be a symlink. evaluate link to be relative to source file in asar
let link = await readlink(file)
if (path.isAbsolute(link)) {
Expand Down Expand Up @@ -230,4 +229,86 @@ export class AsarPackager {
transformedFiles,
}
}

private async getProtectedPaths(): Promise<string[]> {
const systemPaths = [
// Generic *nix
"/usr",
"/lib",
"/bin",
"/sbin",
"/System",
"/Library",
"/private/etc",
"/private/var/db",
"/private/var/root",
"/private/var/log",
"/private/tmp",

// macOS legacy symlinks
"/etc",
"/var",
"/tmp",

// Windows
process.env.SystemRoot,
process.env.WINDIR,
// process.env.ProgramFiles,
// process.env["ProgramFiles(x86)"],
// process.env.ProgramData,
// process.env.CommonProgramFiles,
// process.env["CommonProgramFiles(x86)"],
]
.filter(Boolean)
.map(p => path.resolve(p as string))

// Normalize to real paths to prevent symlink bypasses
const resolvedPaths: string[] = []
for (const p of systemPaths) {
try {
resolvedPaths.push(await fs.realpath(p))
} catch {
resolvedPaths.push(path.resolve(p))
}
}

return resolvedPaths
}

private async protectSystemAndUnsafePaths(file: string, workspaceRoot?: string): Promise<boolean> {
const resolved = await fs.realpath(file).catch(() => path.resolve(file))

const scan = async () => {
if (workspaceRoot) {
const workspace = path.resolve(workspaceRoot)

if (!resolved.startsWith(workspace)) {
return true
}
}

// Allow temp & cache folders
const tmpdir = await fs.realpath(os.tmpdir())
if (resolved.startsWith(tmpdir)) {
return false
}

const blockedSystemPaths = await this.getProtectedPaths()
for (const sys of blockedSystemPaths) {
if (resolved.startsWith(sys)) {
return true
}
}

return false
}

const unsafe = await scan()

if (unsafe) {
log.error({ source: file, realPath: resolved }, `unable to copy, file is from outside the package to a system or unsafe path`)
throw new Error(`Cannot copy file [${file}] symlinked to file [${resolved}] outside the package to a system or unsafe path`)
}
return unsafe
}
}
143 changes: 117 additions & 26 deletions packages/app-builder-lib/src/node-module-collector/index.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,147 @@
import { NpmNodeModulesCollector } from "./npmNodeModulesCollector"
import { PnpmNodeModulesCollector } from "./pnpmNodeModulesCollector"
import { YarnNodeModulesCollector } from "./yarnNodeModulesCollector"
import { detectPackageManagerByLockfile, detectPackageManagerByEnv, PM, getPackageManagerCommand, detectYarnBerry } from "./packageManager"
import { detectPackageManagerByFile, detectPackageManagerByEnv, PM, getPackageManagerCommand, detectYarnBerry as detectIfYarnBerry } from "./packageManager"
import { NodeModuleInfo } from "./types"
import { TmpDir } from "temp-file"
import * as path from "path"
import * as fs from "fs-extra"
import { log, spawn } from "builder-util"
import { YarnBerryNodeModulesCollector } from "./yarnBerryNodeModulesCollector"
import { CancellationToken } from "builder-util-runtime"

export async function getCollectorByPackageManager(pm: PM, rootDir: string, tempDirManager: TmpDir) {
export { PM, getPackageManagerCommand }

export function getCollectorByPackageManager(pm: PM, rootDir: string, tempDirManager: TmpDir) {
switch (pm) {
case PM.PNPM:
if (await PnpmNodeModulesCollector.isPnpmProjectHoisted(rootDir)) {
return new NpmNodeModulesCollector(rootDir, tempDirManager)
}
return new PnpmNodeModulesCollector(rootDir, tempDirManager)
case PM.NPM:
case PM.BUN:
return new NpmNodeModulesCollector(rootDir, tempDirManager)
case PM.YARN:
return new YarnNodeModulesCollector(rootDir, tempDirManager)
case PM.YARN_BERRY:
return new YarnBerryNodeModulesCollector(rootDir, tempDirManager)
case PM.BUN:
case PM.NPM:
default:
return new NpmNodeModulesCollector(rootDir, tempDirManager)
}
}

export async function getNodeModules(pm: PM, rootDir: string, tempDirManager: TmpDir): Promise<NodeModuleInfo[]> {
const collector = await getCollectorByPackageManager(pm, rootDir, tempDirManager)
return collector.getNodeModules()
export function getNodeModules(
pm: PM,
{
rootDir,
tempDirManager,
cancellationToken,
packageName,
}: {
rootDir: string
tempDirManager: TmpDir
cancellationToken: CancellationToken
packageName: string
}
): Promise<NodeModuleInfo[]> {
const collector = getCollectorByPackageManager(pm, rootDir, tempDirManager)
return collector.getNodeModules({ cancellationToken, packageName })
}

export function detectPackageManager(dirs: string[]): PM {
export async function detectPackageManager(searchPaths: string[]): Promise<{ pm: PM; corepackConfig: string | undefined; resolvedDirectory: string | undefined }> {
let pm: PM | null = null
const dedupedPaths = Array.from(new Set(searchPaths)) // reduce file operations, dedupe paths since primary use case has projectDir === appDir

const resolveYarnVersion = (pm: PM) => {
if (pm === PM.YARN) {
return detectYarnBerry()
const resolveIfYarn = (pm: PM, version: string, cwd: string) => (pm === PM.YARN ? detectIfYarnBerry(cwd, version) : pm)

for (const dir of dedupedPaths) {
const packageJsonPath = path.join(dir, "package.json")
const packageManager = fs.existsSync(packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))?.packageManager : undefined
if (packageManager) {
const [pm, version] = packageManager.split("@")
if (Object.values(PM).includes(pm as PM)) {
const resolvedPackageManager = await resolveIfYarn(pm as PM, version, dir)
log.info({ resolvedPackageManager, packageManager, cwd: dir }, "packageManager field detected in package.json")
return { pm: resolvedPackageManager, corepackConfig: packageManager, resolvedDirectory: dir }
}
}
return pm
}

for (const dir of dirs) {
pm = detectPackageManagerByLockfile(dir)
pm = await detectPackageManagerByFile(dir)
if (pm) {
return resolveYarnVersion(pm)
const resolvedPackageManager = await resolveIfYarn(pm, "", dir)
log.info({ resolvedPackageManager, cwd: dir }, "packageManager detected by file")
return { pm: resolvedPackageManager, resolvedDirectory: dir, corepackConfig: undefined }
}
}

pm = detectPackageManagerByEnv()
if (pm) {
return resolveYarnVersion(pm)
pm = detectPackageManagerByEnv() || PM.NPM
const cwd = process.env.npm_package_json ? path.dirname(process.env.npm_package_json) : (process.env.INIT_CWD ?? process.cwd())
const resolvedPackageManager = await resolveIfYarn(pm, "", cwd)
log.info({ resolvedPackageManager, detected: cwd }, "packageManager not detected by file, falling back to environment detection")
return { pm: resolvedPackageManager, resolvedDirectory: undefined, corepackConfig: undefined }
}

export async function findWorkspaceRoot(pm: PM, cwd: string): Promise<string | undefined> {
let command: { command: string; args: string[] } | undefined

switch (pm) {
case PM.PNPM:
command = { command: "pnpm", args: ["root", "-w"] }
break

case PM.YARN_BERRY:
command = { command: "yarn", args: ["config", "get", "workspaceRoot"] }
break

case PM.YARN: {
command = { command: "yarn", args: ["workspaces", "info", "--silent"] }
break
}

case PM.BUN:
command = { command: "bun", args: ["pm", "ls", "--json"] }
break

case PM.NPM:
default:
command = { command: "npm", args: ["prefix", "-w"] }
break
}

// Default to npm
return PM.NPM
const output = await spawn(command.command, command.args, { cwd, stdio: ["ignore", "pipe", "ignore"] })
.then(it => {
const out = it?.trim()
if (pm === PM.YARN) {
JSON.parse(out) // if JSON valid, workspace detected
return findNearestWithWorkspacesField(cwd)
} else if (pm === PM.BUN) {
const json = JSON.parse(out)
if (Array.isArray(json) && json.length > 0) {
return findNearestWithWorkspacesField(cwd)
}
}
return !out?.length || out === "undefined" ? undefined : out
})
.catch(() => findNearestWithWorkspacesField(cwd))

log.info({ root: output }, output ? "workspace root detected" : "workspace root not detected")
return output
}

export { PM, getPackageManagerCommand }
async function findNearestWithWorkspacesField(dir: string): Promise<string | undefined> {
let current = dir
while (true) {
const pkgPath = path.join(current, "package.json")
try {
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"))
if (pkg.workspaces) {
return current
}
} catch {
// ignore
}
const parent = path.dirname(current)
if (parent === current) {
break
}
current = parent
}
return undefined
}
Loading
Loading