From 3d0ff938c2e5af4f4856d6b49cb51e0cafdfacc3 Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 18 Oct 2025 15:18:06 +0200 Subject: [PATCH 1/4] fix(add): dependency detection --- .changeset/salty-apes-play.md | 5 +++++ packages/cli/commands/add/utils.ts | 15 ++++++++++++++ packages/cli/commands/add/workspace.ts | 25 +++++++++++++----------- packages/cli/package.json | 4 +++- packages/cli/tests/commands/add/utils.ts | 24 +++++++++++++++++++++++ packages/cli/vitest.config.ts | 2 +- 6 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 .changeset/salty-apes-play.md create mode 100644 packages/cli/tests/commands/add/utils.ts diff --git a/.changeset/salty-apes-play.md b/.changeset/salty-apes-play.md new file mode 100644 index 000000000..f99f3f23b --- /dev/null +++ b/.changeset/salty-apes-play.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +fix(add): dependency detection now checks each package.json until the workspace root (included) diff --git a/packages/cli/commands/add/utils.ts b/packages/cli/commands/add/utils.ts index 5a36dab01..5ff9a67c9 100644 --- a/packages/cli/commands/add/utils.ts +++ b/packages/cli/commands/add/utils.ts @@ -125,3 +125,18 @@ export function getHighlighter(): Highlighter { website: (str) => pc.whiteBright(str) }; } + +export function getAllPaths(from: string, to: string): string[] { + const relativePath = path.relative(from, to); + const pathSegments = relativePath.split(path.sep).filter(Boolean); + + const allPathsUntilWorkspaceRoot = [from]; + let currentPath = from; + + for (const segment of pathSegments) { + currentPath = path.join(currentPath, segment); + allPathsUntilWorkspaceRoot.push(currentPath); + } + + return allPathsUntilWorkspaceRoot; +} diff --git a/packages/cli/commands/add/workspace.ts b/packages/cli/commands/add/workspace.ts index de706bb65..15835a0d1 100644 --- a/packages/cli/commands/add/workspace.ts +++ b/packages/cli/commands/add/workspace.ts @@ -5,7 +5,7 @@ import { common, object, type AstTypes } from '@sveltejs/cli-core/js'; import { parseScript } from '@sveltejs/cli-core/parsers'; import { detect } from 'package-manager-detector'; import type { OptionValues, PackageManager, Workspace } from '@sveltejs/cli-core'; -import { commonFilePaths, getPackageJson, readFile } from './utils.ts'; +import { commonFilePaths, getAllPaths, getPackageJson, readFile } from './utils.ts'; import { getUserAgent } from '../../utils/package-manager.ts'; type CreateWorkspaceOptions = { @@ -32,18 +32,19 @@ export async function createWorkspace({ : commonFilePaths.viteConfig; let dependencies: Record = {}; - let directory = resolvedCwd; - const root = findRoot(resolvedCwd); - while (directory && directory !== root) { - if (fs.existsSync(path.join(directory, commonFilePaths.packageJson))) { - const { data: packageJson } = getPackageJson(directory); + const directory = resolvedCwd; + const workspaceRoot = findWorkspaceRoot(directory); + const allPathsUntilWorkspaceRoot = getAllPaths(workspaceRoot, directory); + for (const dir of allPathsUntilWorkspaceRoot) { + if (fs.existsSync(path.join(dir, commonFilePaths.packageJson))) { + const { data: packageJson } = getPackageJson(dir); dependencies = { + ...dependencies, + // prioritize deeper dependency versions ...packageJson.devDependencies, - ...packageJson.dependencies, - ...dependencies + ...packageJson.dependencies }; } - directory = path.dirname(directory); } // removes the version ranges (e.g. `^` is removed from: `^9.0.0`) for (const [key, value] of Object.entries(dependencies)) { @@ -61,14 +62,16 @@ export async function createWorkspace({ }; } -function findRoot(cwd: string): string { +function findWorkspaceRoot(cwd: string): string { const { root } = path.parse(cwd); let directory = cwd; while (directory && directory !== root) { if (fs.existsSync(path.join(directory, commonFilePaths.packageJson))) { + // in pnpm it can be a file if (fs.existsSync(path.join(directory, 'pnpm-workspace.yaml'))) { return directory; } + // in other package managers it's a workspaces key in the package.json const { data } = getPackageJson(directory); if (data.workspaces) { return directory; @@ -76,7 +79,7 @@ function findRoot(cwd: string): string { } directory = path.dirname(directory); } - return root; + return cwd; } function parseKitOptions(cwd: string) { diff --git a/packages/cli/package.json b/packages/cli/package.json index caa634cde..ec5dfb4a1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -13,7 +13,9 @@ "scripts": { "check": "tsc", "format": "pnpm lint --write", - "lint": "prettier --check . --config ../../prettier.config.js --ignore-path ../../.gitignore --ignore-path .gitignore --ignore-path ../../.prettierignore" + "lint": "prettier --check . --config ../../prettier.config.js --ignore-path ../../.gitignore --ignore-path .gitignore --ignore-path ../../.prettierignore", + "test": "vitest run", + "test:ui": "vitest --ui" }, "files": [ "dist" diff --git a/packages/cli/tests/commands/add/utils.ts b/packages/cli/tests/commands/add/utils.ts new file mode 100644 index 000000000..a23030342 --- /dev/null +++ b/packages/cli/tests/commands/add/utils.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { getAllPaths } from '../../../commands/add/utils.ts'; + +describe('getAllPaths', () => { + it('should return all levels', () => { + const allPathsUntilWorkspaceRoot = getAllPaths('/a', '/a/b/c'); + expect(allPathsUntilWorkspaceRoot).toEqual(['/a', '/a/b', '/a/b/c']); + }); + + it('should return the only path', () => { + const allPathsUntilWorkspaceRoot = getAllPaths('/a', '/a'); + expect(allPathsUntilWorkspaceRoot).toEqual(['/a']); + }); + + it('should have the correct order', () => { + const allPathsUntilWorkspaceRoot = getAllPaths('/a/b', '/a'); + expect(allPathsUntilWorkspaceRoot).toEqual(['/a/b', '/a']); + }); + + it('should even navigate to ..', () => { + const allPathsUntilWorkspaceRoot = getAllPaths('/a', '/b/c'); + expect(allPathsUntilWorkspaceRoot).toEqual(['/a', '/', '/b', '/b/c']); + }); +}); diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts index 3be07ef11..ab4a27702 100644 --- a/packages/cli/vitest.config.ts +++ b/packages/cli/vitest.config.ts @@ -3,7 +3,7 @@ import { defineProject } from 'vitest/config'; export default defineProject({ test: { name: 'cli', - include: ['./tests/**/index.ts', './tests/*.ts'], + include: ['./tests/**/*.ts'], expect: { requireAssertions: true } From dabe2a47efd49b659c7cf88fcd4cffea8d5d4e3b Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 18 Oct 2025 15:59:22 +0200 Subject: [PATCH 2/4] simpler solution --- .changeset/salty-apes-play.md | 2 +- packages/cli/commands/add/workspace.ts | 22 ++++++++++++---------- packages/cli/package.json | 4 +--- packages/cli/tests/commands/add/utils.ts | 24 ------------------------ packages/cli/vitest.config.ts | 2 +- 5 files changed, 15 insertions(+), 39 deletions(-) delete mode 100644 packages/cli/tests/commands/add/utils.ts diff --git a/.changeset/salty-apes-play.md b/.changeset/salty-apes-play.md index f99f3f23b..8ac717edf 100644 --- a/.changeset/salty-apes-play.md +++ b/.changeset/salty-apes-play.md @@ -2,4 +2,4 @@ 'sv': patch --- -fix(add): dependency detection now checks each package.json until the workspace root (included) +fix(add): include monorepo root in dependency detection diff --git a/packages/cli/commands/add/workspace.ts b/packages/cli/commands/add/workspace.ts index 15835a0d1..69c3719c0 100644 --- a/packages/cli/commands/add/workspace.ts +++ b/packages/cli/commands/add/workspace.ts @@ -5,7 +5,7 @@ import { common, object, type AstTypes } from '@sveltejs/cli-core/js'; import { parseScript } from '@sveltejs/cli-core/parsers'; import { detect } from 'package-manager-detector'; import type { OptionValues, PackageManager, Workspace } from '@sveltejs/cli-core'; -import { commonFilePaths, getAllPaths, getPackageJson, readFile } from './utils.ts'; +import { commonFilePaths, getPackageJson, readFile } from './utils.ts'; import { getUserAgent } from '../../utils/package-manager.ts'; type CreateWorkspaceOptions = { @@ -32,19 +32,19 @@ export async function createWorkspace({ : commonFilePaths.viteConfig; let dependencies: Record = {}; - const directory = resolvedCwd; - const workspaceRoot = findWorkspaceRoot(directory); - const allPathsUntilWorkspaceRoot = getAllPaths(workspaceRoot, directory); - for (const dir of allPathsUntilWorkspaceRoot) { - if (fs.existsSync(path.join(dir, commonFilePaths.packageJson))) { - const { data: packageJson } = getPackageJson(dir); + let directory = resolvedCwd; + const rootWorkspace = findWorkspaceRoot(directory); + const { root } = path.parse(directory); + while (directory && directory.length >= rootWorkspace.length && directory !== root) { + if (fs.existsSync(path.join(directory, commonFilePaths.packageJson))) { + const { data: packageJson } = getPackageJson(directory); dependencies = { - ...dependencies, - // prioritize deeper dependency versions ...packageJson.devDependencies, - ...packageJson.dependencies + ...packageJson.dependencies, + ...dependencies }; } + directory = path.dirname(directory); } // removes the version ranges (e.g. `^` is removed from: `^9.0.0`) for (const [key, value] of Object.entries(dependencies)) { @@ -79,6 +79,8 @@ function findWorkspaceRoot(cwd: string): string { } directory = path.dirname(directory); } + // We didn't find a workspace root, so we return the original directory + // it's a standalone project return cwd; } diff --git a/packages/cli/package.json b/packages/cli/package.json index ec5dfb4a1..caa634cde 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -13,9 +13,7 @@ "scripts": { "check": "tsc", "format": "pnpm lint --write", - "lint": "prettier --check . --config ../../prettier.config.js --ignore-path ../../.gitignore --ignore-path .gitignore --ignore-path ../../.prettierignore", - "test": "vitest run", - "test:ui": "vitest --ui" + "lint": "prettier --check . --config ../../prettier.config.js --ignore-path ../../.gitignore --ignore-path .gitignore --ignore-path ../../.prettierignore" }, "files": [ "dist" diff --git a/packages/cli/tests/commands/add/utils.ts b/packages/cli/tests/commands/add/utils.ts deleted file mode 100644 index a23030342..000000000 --- a/packages/cli/tests/commands/add/utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { getAllPaths } from '../../../commands/add/utils.ts'; - -describe('getAllPaths', () => { - it('should return all levels', () => { - const allPathsUntilWorkspaceRoot = getAllPaths('/a', '/a/b/c'); - expect(allPathsUntilWorkspaceRoot).toEqual(['/a', '/a/b', '/a/b/c']); - }); - - it('should return the only path', () => { - const allPathsUntilWorkspaceRoot = getAllPaths('/a', '/a'); - expect(allPathsUntilWorkspaceRoot).toEqual(['/a']); - }); - - it('should have the correct order', () => { - const allPathsUntilWorkspaceRoot = getAllPaths('/a/b', '/a'); - expect(allPathsUntilWorkspaceRoot).toEqual(['/a/b', '/a']); - }); - - it('should even navigate to ..', () => { - const allPathsUntilWorkspaceRoot = getAllPaths('/a', '/b/c'); - expect(allPathsUntilWorkspaceRoot).toEqual(['/a', '/', '/b', '/b/c']); - }); -}); diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts index ab4a27702..3be07ef11 100644 --- a/packages/cli/vitest.config.ts +++ b/packages/cli/vitest.config.ts @@ -3,7 +3,7 @@ import { defineProject } from 'vitest/config'; export default defineProject({ test: { name: 'cli', - include: ['./tests/**/*.ts'], + include: ['./tests/**/index.ts', './tests/*.ts'], expect: { requireAssertions: true } From d250e83ffc2c8601cc6d2a9a113aa79dcc3fe461 Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 18 Oct 2025 15:59:49 +0200 Subject: [PATCH 3/4] cleanup --- packages/cli/commands/add/utils.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/cli/commands/add/utils.ts b/packages/cli/commands/add/utils.ts index 5ff9a67c9..5a36dab01 100644 --- a/packages/cli/commands/add/utils.ts +++ b/packages/cli/commands/add/utils.ts @@ -125,18 +125,3 @@ export function getHighlighter(): Highlighter { website: (str) => pc.whiteBright(str) }; } - -export function getAllPaths(from: string, to: string): string[] { - const relativePath = path.relative(from, to); - const pathSegments = relativePath.split(path.sep).filter(Boolean); - - const allPathsUntilWorkspaceRoot = [from]; - let currentPath = from; - - for (const segment of pathSegments) { - currentPath = path.join(currentPath, segment); - allPathsUntilWorkspaceRoot.push(currentPath); - } - - return allPathsUntilWorkspaceRoot; -} From 5f4e119147abc5cee6c44bbeb2c6b359bc6c7f7e Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 18 Oct 2025 16:08:18 +0200 Subject: [PATCH 4/4] naming --- packages/cli/commands/add/workspace.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/cli/commands/add/workspace.ts b/packages/cli/commands/add/workspace.ts index 69c3719c0..a4a0578a5 100644 --- a/packages/cli/commands/add/workspace.ts +++ b/packages/cli/commands/add/workspace.ts @@ -33,9 +33,14 @@ export async function createWorkspace({ let dependencies: Record = {}; let directory = resolvedCwd; - const rootWorkspace = findWorkspaceRoot(directory); + const workspaceRoot = findWorkspaceRoot(directory); const { root } = path.parse(directory); - while (directory && directory.length >= rootWorkspace.length && directory !== root) { + while ( + // we have a directory + directory && + // we are still in the workspace (including the workspace root) + directory.length >= workspaceRoot.length + ) { if (fs.existsSync(path.join(directory, commonFilePaths.packageJson))) { const { data: packageJson } = getPackageJson(directory); dependencies = { @@ -44,6 +49,7 @@ export async function createWorkspace({ ...dependencies }; } + if (root === directory) break; // we are at the root root, let's stop directory = path.dirname(directory); } // removes the version ranges (e.g. `^` is removed from: `^9.0.0`)