From 93bfccf2a92074398ed2b32a6ce72ee03e50723f Mon Sep 17 00:00:00 2001 From: Vishnu Vardhan P Date: Tue, 24 Feb 2026 23:09:37 +0530 Subject: [PATCH 1/2] fix(server): resolve workspace root from workspace package patterns --- .../npm-outside/packages/my-lib/package.json | 3 + .../npm-outside/workspace/my-app/package.json | 3 + .../npm-outside/workspace/package.json | 7 + .../pnpm-outside/packages/my-lib/package.json | 3 + .../workspace/my-app/package.json | 3 + .../workspace/pnpm-workspace.yaml | 3 + .../node/server/__tests__/search-root.spec.ts | 14 ++ packages/vite/src/node/server/searchRoot.ts | 145 +++++++++++++++++- 8 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 packages/vite/src/node/server/__tests__/fixtures/npm-outside/packages/my-lib/package.json create mode 100644 packages/vite/src/node/server/__tests__/fixtures/npm-outside/workspace/my-app/package.json create mode 100644 packages/vite/src/node/server/__tests__/fixtures/npm-outside/workspace/package.json create mode 100644 packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/packages/my-lib/package.json create mode 100644 packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/workspace/my-app/package.json create mode 100644 packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/workspace/pnpm-workspace.yaml diff --git a/packages/vite/src/node/server/__tests__/fixtures/npm-outside/packages/my-lib/package.json b/packages/vite/src/node/server/__tests__/fixtures/npm-outside/packages/my-lib/package.json new file mode 100644 index 00000000000000..5c71cdb7d222ef --- /dev/null +++ b/packages/vite/src/node/server/__tests__/fixtures/npm-outside/packages/my-lib/package.json @@ -0,0 +1,3 @@ +{ + "name": "my-lib" +} diff --git a/packages/vite/src/node/server/__tests__/fixtures/npm-outside/workspace/my-app/package.json b/packages/vite/src/node/server/__tests__/fixtures/npm-outside/workspace/my-app/package.json new file mode 100644 index 00000000000000..c202acb8d5a237 --- /dev/null +++ b/packages/vite/src/node/server/__tests__/fixtures/npm-outside/workspace/my-app/package.json @@ -0,0 +1,3 @@ +{ + "name": "my-app" +} diff --git a/packages/vite/src/node/server/__tests__/fixtures/npm-outside/workspace/package.json b/packages/vite/src/node/server/__tests__/fixtures/npm-outside/workspace/package.json new file mode 100644 index 00000000000000..ed11455e895916 --- /dev/null +++ b/packages/vite/src/node/server/__tests__/fixtures/npm-outside/workspace/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "workspaces": [ + "my-app", + "../packages/*" + ] +} diff --git a/packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/packages/my-lib/package.json b/packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/packages/my-lib/package.json new file mode 100644 index 00000000000000..5c71cdb7d222ef --- /dev/null +++ b/packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/packages/my-lib/package.json @@ -0,0 +1,3 @@ +{ + "name": "my-lib" +} diff --git a/packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/workspace/my-app/package.json b/packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/workspace/my-app/package.json new file mode 100644 index 00000000000000..c202acb8d5a237 --- /dev/null +++ b/packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/workspace/my-app/package.json @@ -0,0 +1,3 @@ +{ + "name": "my-app" +} diff --git a/packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/workspace/pnpm-workspace.yaml b/packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/workspace/pnpm-workspace.yaml new file mode 100644 index 00000000000000..35898b01a964e3 --- /dev/null +++ b/packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/workspace/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - my-app + - ../packages/* diff --git a/packages/vite/src/node/server/__tests__/search-root.spec.ts b/packages/vite/src/node/server/__tests__/search-root.spec.ts index 6b14f84b425feb..ce651f11db0e90 100644 --- a/packages/vite/src/node/server/__tests__/search-root.spec.ts +++ b/packages/vite/src/node/server/__tests__/search-root.spec.ts @@ -19,6 +19,20 @@ describe('searchForWorkspaceRoot', () => { expect(resolved).toBe(resolve(dirname, 'fixtures/pnpm')) }) + test('pnpm with workspaces outside current root', () => { + const resolved = searchForWorkspaceRoot( + resolve(dirname, 'fixtures/pnpm-outside/workspace/my-app'), + ) + expect(resolved).toBe(resolve(dirname, 'fixtures/pnpm-outside')) + }) + + test('package.json workspaces outside current root', () => { + const resolved = searchForWorkspaceRoot( + resolve(dirname, 'fixtures/npm-outside/workspace/my-app'), + ) + expect(resolved).toBe(resolve(dirname, 'fixtures/npm-outside')) + }) + test('yarn', () => { const resolved = searchForWorkspaceRoot( resolve(dirname, 'fixtures/yarn/nested'), diff --git a/packages/vite/src/node/server/searchRoot.ts b/packages/vite/src/node/server/searchRoot.ts index 6a0c7ab42c5a6c..2302cee32ffb1d 100644 --- a/packages/vite/src/node/server/searchRoot.ts +++ b/packages/vite/src/node/server/searchRoot.ts @@ -1,5 +1,13 @@ import fs from 'node:fs' -import { dirname, join } from 'node:path' +import { + basename, + dirname, + isAbsolute, + join, + relative, + resolve, +} from 'node:path' +import picomatch from 'picomatch' import { isFileReadable } from '../utils' // https://github.com/vitejs/vite/issues/2820#issuecomment-812495079 @@ -22,19 +30,143 @@ const ROOT_FILES = [ // npm: https://docs.npmjs.com/cli/v7/using-npm/workspaces#installing-workspaces // yarn: https://classic.yarnpkg.com/en/docs/workspaces/#toc-how-to-use-it -function hasWorkspacePackageJSON(root: string): boolean { +function getWorkspacePackagePatterns(root: string): string[] | undefined { const path = join(root, 'package.json') if (!isFileReadable(path)) { - return false + return undefined } try { const content = JSON.parse(fs.readFileSync(path, 'utf-8')) || {} - return !!content.workspaces + if (Array.isArray(content.workspaces)) { + return content.workspaces.filter( + (workspace: unknown): workspace is string => + typeof workspace === 'string', + ) + } + + if (Array.isArray(content.workspaces?.packages)) { + return content.workspaces.packages.filter( + (workspace: unknown) => typeof workspace === 'string', + ) + } + + return undefined } catch { - return false + return undefined } } +function getPnpmWorkspacePatterns(root: string): string[] | undefined { + const path = join(root, 'pnpm-workspace.yaml') + if (!isFileReadable(path)) { + return undefined + } + + try { + const content = fs.readFileSync(path, 'utf-8') + const lines = content.split(/\r?\n/) + + const packages: string[] = [] + let inPackages = false + let packagesIndent = -1 + + for (const rawLine of lines) { + const withoutComment = rawLine.replace(/\s+#.*$/, '') + const line = withoutComment.trimEnd() + if (!line.trim()) continue + + const indent = withoutComment.length - withoutComment.trimStart().length + const trimmed = withoutComment.trimStart() + + if (!inPackages) { + if (trimmed === 'packages:' || trimmed === 'packages: []') { + inPackages = true + packagesIndent = indent + if (trimmed === 'packages: []') { + return [] + } + } + continue + } + + if (indent <= packagesIndent) { + break + } + + if (!trimmed.startsWith('- ')) { + continue + } + + const pattern = trimmed.slice(2).trim() + if (!pattern) continue + + const unquoted = + (pattern.startsWith('"') && pattern.endsWith('"')) || + (pattern.startsWith("'") && pattern.endsWith("'")) + ? pattern.slice(1, -1) + : pattern + + packages.push(unquoted) + } + + return packages.length ? packages : undefined + } catch { + return undefined + } +} + +function getWorkspacePackageBases(root: string, patterns: string[]): string[] { + const bases = patterns + .filter((pattern) => !pattern.startsWith('!')) + .map((pattern) => { + let { base } = picomatch.scan(pattern) + if (!base || base === '.') { + return root + } + + if (basename(base).includes('.')) { + base = dirname(base) + } + + return resolve(root, base) + }) + + return [root, ...bases] +} + +function getCommonAncestor(paths: string[]): string { + if (!paths.length) { + return '' + } + + let ancestor = resolve(paths[0]) + for (const path of paths.slice(1).map((path) => resolve(path))) { + while (ancestor !== dirname(ancestor)) { + const relation = relative(ancestor, path) + if ( + relation === '' || + (!relation.startsWith('..') && !isAbsolute(relation)) + ) { + break + } + ancestor = dirname(ancestor) + } + } + + return ancestor +} + +function getWorkspaceRoot(root: string): string | undefined { + const patterns = + getPnpmWorkspacePatterns(root) ?? getWorkspacePackagePatterns(root) + if (!patterns?.length) { + return undefined + } + + const packageBases = getWorkspacePackageBases(root, patterns) + return getCommonAncestor(packageBases) +} + function hasRootFile(root: string): boolean { return ROOT_FILES.some((file) => fs.existsSync(join(root, file))) } @@ -67,8 +199,9 @@ export function searchForWorkspaceRoot( current: string, root: string = searchForPackageRoot(current), ): string { + const workspaceRoot = getWorkspaceRoot(current) + if (workspaceRoot) return workspaceRoot if (hasRootFile(current)) return current - if (hasWorkspacePackageJSON(current)) return current const dir = dirname(current) // reach the fs root From 28d985fd19f47c2fb195de0ac0b9349fdea5862f Mon Sep 17 00:00:00 2001 From: Vishnu Vardhan P Date: Tue, 24 Feb 2026 23:15:15 +0530 Subject: [PATCH 2/2] chore: update lockfile for new workspace fixtures --- pnpm-lock.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d8efa3f23e1ef..b9e5247f6f2326 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -498,8 +498,18 @@ importers: packages/vite/src/node/server/__tests__/fixtures/none/nested: {} + packages/vite/src/node/server/__tests__/fixtures/npm-outside/packages/my-lib: {} + + packages/vite/src/node/server/__tests__/fixtures/npm-outside/workspace: {} + + packages/vite/src/node/server/__tests__/fixtures/npm-outside/workspace/my-app: {} + packages/vite/src/node/server/__tests__/fixtures/pnpm: {} + packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/packages/my-lib: {} + + packages/vite/src/node/server/__tests__/fixtures/pnpm-outside/workspace/my-app: {} + packages/vite/src/node/server/__tests__/fixtures/pnpm/nested: {} packages/vite/src/node/server/__tests__/fixtures/watcher: {}