From f32409744eec2366b3f0c2c03fa56d6622b8d6bd Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 8 Dec 2025 18:39:02 +0900 Subject: [PATCH 1/3] Support monorepo on turbopack --- .../next-plugin/src/__tests__/preload.test.ts | 16 +++++------ .../next-plugin/src/find-top-package-root.ts | 27 +++++++++++++++++++ packages/next-plugin/src/get-package-name.ts | 7 +++++ packages/next-plugin/src/has-localpackage.ts | 16 +++++++++++ packages/next-plugin/src/plugin.ts | 2 +- packages/next-plugin/src/preload.ts | 26 +++++++++++++++++- 6 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 packages/next-plugin/src/find-top-package-root.ts create mode 100644 packages/next-plugin/src/get-package-name.ts create mode 100644 packages/next-plugin/src/has-localpackage.ts diff --git a/packages/next-plugin/src/__tests__/preload.test.ts b/packages/next-plugin/src/__tests__/preload.test.ts index 6552fbdc..aaf2a62a 100644 --- a/packages/next-plugin/src/__tests__/preload.test.ts +++ b/packages/next-plugin/src/__tests__/preload.test.ts @@ -63,7 +63,7 @@ describe('preload', () => { const singleCss = false const cssDir = '/output/css' - preload(excludeRegex, libPackage, singleCss, cssDir) + preload(excludeRegex, libPackage, singleCss, cssDir, []) expect(globSync).toHaveBeenCalledWith( ['**/*.tsx', '**/*.ts', '**/*.js', '**/*.mjs'], @@ -81,7 +81,7 @@ describe('preload', () => { .mockReturnValueOnce('src/App.tsx') .mockReturnValueOnce('src/components/Button.tsx') .mockReturnValueOnce('.next/page.tsx') - preload(/node_modules/, '@devup-ui/react', false, '/output/css') + preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) expect(codeExtract).toHaveBeenCalledTimes(2) expect(codeExtract).toHaveBeenCalledWith( @@ -106,7 +106,7 @@ describe('preload', () => { [Symbol.dispose]: vi.fn(), }) - preload(/node_modules/, '@devup-ui/react', false, '/output/css') + preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) expect(writeFileSync).toHaveBeenCalledWith( join('/output/css', 'styles.css'), @@ -127,7 +127,7 @@ describe('preload', () => { }) vi.mocked(getCss).mockReturnValue('') - preload(/node_modules/, '@devup-ui/react', false, '/output/css') + preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) expect(writeFileSync).toHaveBeenCalledWith( join('/output/css', 'devup-ui.css'), @@ -147,7 +147,7 @@ describe('preload', () => { [Symbol.dispose]: vi.fn(), }) - preload(/node_modules/, '@devup-ui/react', false, '/output/css') + preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) expect(writeFileSync).toHaveBeenCalledWith( join('/output/css', 'styles.css'), @@ -167,7 +167,7 @@ describe('preload', () => { [Symbol.dispose]: vi.fn(), }) - preload(/node_modules/, '@devup-ui/react', false, '/output/css') + preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) expect(writeFileSync).toHaveBeenCalledWith( join('/output/css', 'styles.css'), @@ -181,7 +181,7 @@ describe('preload', () => { const singleCss = true const cssDir = '/custom/css/dir' - preload(/node_modules/, libPackage, singleCss, cssDir) + preload(/node_modules/, libPackage, singleCss, cssDir, []) expect(codeExtract).toHaveBeenCalledWith( expect.stringMatching(/App\.tsx$/), @@ -218,7 +218,7 @@ describe('preload', () => { [Symbol.dispose]: vi.fn(), }) - preload(/node_modules/, '@devup-ui/react', false, '/output/css') + preload(/node_modules/, '@devup-ui/react', false, '/output/css', []) expect(writeFileSync).toHaveBeenCalledTimes(3) expect(writeFileSync).toHaveBeenCalledWith( diff --git a/packages/next-plugin/src/find-top-package-root.ts b/packages/next-plugin/src/find-top-package-root.ts new file mode 100644 index 00000000..2f1fd248 --- /dev/null +++ b/packages/next-plugin/src/find-top-package-root.ts @@ -0,0 +1,27 @@ +import { existsSync } from 'node:fs' +import { dirname, join } from 'node:path' + +/** + * find package root + * + * Find the root of the package by checking the package.json file + * @returns + */ +export function findTopPackageRoot(pwd = process.cwd()) { + let current = pwd + let topWithPackage: string | null = null + + while (true) { + if (existsSync(join(current, 'package.json'))) { + topWithPackage = current + } + + const parent = dirname(current) + if (parent === current) { + break + } + current = parent + } + + return topWithPackage ?? pwd +} diff --git a/packages/next-plugin/src/get-package-name.ts b/packages/next-plugin/src/get-package-name.ts new file mode 100644 index 00000000..933982db --- /dev/null +++ b/packages/next-plugin/src/get-package-name.ts @@ -0,0 +1,7 @@ +import { readFileSync } from 'node:fs' + +export function getPackageName(packageJsonPath: string) { + const packageJson = readFileSync(packageJsonPath, 'utf-8') + const packageJsonObject = JSON.parse(packageJson) + return packageJsonObject.name +} diff --git a/packages/next-plugin/src/has-localpackage.ts b/packages/next-plugin/src/has-localpackage.ts new file mode 100644 index 00000000..f6af29cd --- /dev/null +++ b/packages/next-plugin/src/has-localpackage.ts @@ -0,0 +1,16 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' + +/** + * has local package + * + * Check if the include workspace:* package is a local package + * @returns + */ +export function hasLocalPackage() { + const packageJson = readFileSync(join(process.cwd(), 'package.json'), 'utf-8') + const packageJsonObject = JSON.parse(packageJson) + return Object.values(packageJsonObject.dependencies ?? {}).some( + (pkg: unknown) => typeof pkg === 'string' && pkg.includes('workspace:'), + ) +} diff --git a/packages/next-plugin/src/plugin.ts b/packages/next-plugin/src/plugin.ts index c0ac38e3..abda68e8 100644 --- a/packages/next-plugin/src/plugin.ts +++ b/packages/next-plugin/src/plugin.ts @@ -97,7 +97,7 @@ export function DevupUI( writeFileSync(join(cssDir, 'devup-ui.css'), getCss(null, false)) } else { // build - preload(excludeRegex, libPackage, singleCss, cssDir) + preload(excludeRegex, libPackage, singleCss, cssDir, include) } const defaultSheet = JSON.parse(exportSheet()) const defaultClassMap = JSON.parse(exportClassMap()) diff --git a/packages/next-plugin/src/preload.ts b/packages/next-plugin/src/preload.ts index 0c3e89f2..d3839eab 100644 --- a/packages/next-plugin/src/preload.ts +++ b/packages/next-plugin/src/preload.ts @@ -1,20 +1,44 @@ import { readFileSync, realpathSync, writeFileSync } from 'node:fs' -import { basename, join, relative } from 'node:path' +import { basename, dirname, join, relative } from 'node:path' import { codeExtract, getCss } from '@devup-ui/wasm' import { globSync } from 'glob' + +import { findTopPackageRoot } from './find-top-package-root' +import { getPackageName } from './get-package-name' +import { hasLocalPackage } from './has-localpackage' + export function preload( excludeRegex: RegExp, libPackage: string, singleCss: boolean, cssDir: string, + include: string[], + pwd = process.cwd(), ) { + if (include.length > 0 && hasLocalPackage()) { + const packageRoot = findTopPackageRoot() + const collected = globSync(['package.json', '!**/node_modules/**'], { + follow: true, + absolute: true, + cwd: packageRoot, + }) + .filter((file) => include.includes(getPackageName(file))) + .map((file) => dirname(file)) + + for (const file of collected) { + preload(excludeRegex, libPackage, singleCss, cssDir, include, file) + } + return + } const collected = globSync(['**/*.tsx', '**/*.ts', '**/*.js', '**/*.mjs'], { follow: true, absolute: true, + cwd: pwd, }) // fix multi core build issue collected.sort() + // console.log('collected', collected) for (const file of collected) { const filePath = relative(process.cwd(), realpathSync(file)) if ( From 1117f9d136ca7984ac2c0303ef194887e1ff6248 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 8 Dec 2025 18:46:03 +0900 Subject: [PATCH 2/3] Fix testcase --- .../changepack_log_hCDVUDEgLJRqiCNyqEOcJ.json | 5 +++++ apps/next/next.config.mjs | 4 +++- apps/next/package.json | 3 ++- apps/next/src/app/page.tsx | 2 ++ .../next-plugin/src/__tests__/plugin.test.ts | 1 + .../next-plugin/src/__tests__/preload.test.ts | 1 + pnpm-lock.yaml | 19 +++++++++++-------- 7 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 .changepacks/changepack_log_hCDVUDEgLJRqiCNyqEOcJ.json diff --git a/.changepacks/changepack_log_hCDVUDEgLJRqiCNyqEOcJ.json b/.changepacks/changepack_log_hCDVUDEgLJRqiCNyqEOcJ.json new file mode 100644 index 00000000..e563ff24 --- /dev/null +++ b/.changepacks/changepack_log_hCDVUDEgLJRqiCNyqEOcJ.json @@ -0,0 +1,5 @@ +{ + "changes": { "packages/next-plugin/package.json": "Patch" }, + "note": "Support monorepo on turbopack", + "date": "2025-12-08T09:38:36.536760600Z" +} diff --git a/apps/next/next.config.mjs b/apps/next/next.config.mjs index f290135f..6efaf4c9 100644 --- a/apps/next/next.config.mjs +++ b/apps/next/next.config.mjs @@ -6,4 +6,6 @@ const nextConfig = { /* config options here */ } -export default DevupUI(nextConfig) +export default DevupUI(nextConfig, { + include: ['vite-lib-example'], +}) diff --git a/apps/next/package.json b/apps/next/package.json index 260f3693..ea67aa4b 100644 --- a/apps/next/package.json +++ b/apps/next/package.json @@ -13,7 +13,8 @@ "react": "^19.2", "react-dom": "^19.2", "next": "^16.0", - "@devup-ui/react": "workspace:*" + "@devup-ui/react": "workspace:*", + "vite-lib-example": "workspace:*" }, "devDependencies": { "@devup-ui/next-plugin": "workspace:*", diff --git a/apps/next/src/app/page.tsx b/apps/next/src/app/page.tsx index f129aeb0..37a5e314 100644 --- a/apps/next/src/app/page.tsx +++ b/apps/next/src/app/page.tsx @@ -2,6 +2,7 @@ import { Box, css, styled, Text } from '@devup-ui/react' import { useState } from 'react' +import { Lib } from 'vite-lib-example' const color = 'yellow' const StyledFooter = styled.footer<{ type: '1' | '2' }>` @@ -38,6 +39,7 @@ export default function HomePage() { py="28px" > hello + hello { '@devup-ui/react', false, expect.any(String), + [], ) }) it('should create theme.d.ts file', async () => { diff --git a/packages/next-plugin/src/__tests__/preload.test.ts b/packages/next-plugin/src/__tests__/preload.test.ts index aaf2a62a..7e693be0 100644 --- a/packages/next-plugin/src/__tests__/preload.test.ts +++ b/packages/next-plugin/src/__tests__/preload.test.ts @@ -70,6 +70,7 @@ describe('preload', () => { { follow: true, absolute: true, + cwd: expect.any(String), }, ) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94b88a6a..173e3e7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,6 +150,9 @@ importers: react-dom: specifier: ^19.2 version: 19.2.0(react@19.2.0) + vite-lib-example: + specifier: workspace:* + version: link:../vite-lib devDependencies: '@devup-ui/next-plugin': specifier: workspace:* @@ -11636,8 +11639,8 @@ snapshots: '@next/eslint-plugin-next': 16.0.6 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.1(jiti@2.6.1)) @@ -11663,7 +11666,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -11674,7 +11677,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -11704,14 +11707,14 @@ snapshots: - bluebird - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -11744,7 +11747,7 @@ snapshots: eslint: 9.39.1(jiti@2.6.1) estraverse: 5.3.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -11755,7 +11758,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 From 6a037949870cc2baaade00f7d583328754931bae Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 8 Dec 2025 19:00:18 +0900 Subject: [PATCH 3/3] Add testcase --- .../next-plugin/src/__tests__/preload.test.ts | 74 +++++++++++++++++ .../next-plugin/src/__tests__/utils.test.ts | 83 +++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 packages/next-plugin/src/__tests__/utils.test.ts diff --git a/packages/next-plugin/src/__tests__/preload.test.ts b/packages/next-plugin/src/__tests__/preload.test.ts index 7e693be0..d46ec6fd 100644 --- a/packages/next-plugin/src/__tests__/preload.test.ts +++ b/packages/next-plugin/src/__tests__/preload.test.ts @@ -6,6 +6,9 @@ import { join } from 'node:path' import { codeExtract, getCss } from '@devup-ui/wasm' import { globSync } from 'glob' +import { findTopPackageRoot } from '../find-top-package-root' +import { getPackageName } from '../get-package-name' +import { hasLocalPackage } from '../has-localpackage' import { preload } from '../preload' // Mock dependencies @@ -33,6 +36,18 @@ vi.mock('@devup-ui/wasm', () => ({ getCss: vi.fn(), })) +vi.mock('../find-top-package-root', () => ({ + findTopPackageRoot: vi.fn(), +})) + +vi.mock('../get-package-name', () => ({ + getPackageName: vi.fn(), +})) + +vi.mock('../has-localpackage', () => ({ + hasLocalPackage: vi.fn(), +})) + describe('preload', () => { beforeEach(() => { vi.clearAllMocks() @@ -233,4 +248,63 @@ describe('preload', () => { 'utf-8', ) }) + + it('should recurse into local workspaces when include is provided', () => { + const files = ['src/App.tsx'] + vi.mocked(findTopPackageRoot).mockReturnValue('/repo') + vi.mocked(hasLocalPackage) + .mockReturnValueOnce(true) + .mockReturnValueOnce(false) + vi.mocked(globSync) + .mockReturnValueOnce([ + '/repo/packages/pkg-a/package.json', + '/repo/packages/pkg-b/package.json', + ]) + .mockReturnValueOnce(files) + vi.mocked(getPackageName) + .mockReturnValueOnce('pkg-a') + .mockReturnValueOnce('pkg-b') + vi.mocked(realpathSync).mockReturnValueOnce('src/App.tsx') + + preload(/node_modules/, '@devup-ui/react', false, '/output/css', ['pkg-a']) + + expect(findTopPackageRoot).toHaveBeenCalled() + expect(globSync).toHaveBeenCalledWith( + ['package.json', '!**/node_modules/**'], + { + follow: true, + absolute: true, + cwd: '/repo', + }, + ) + expect(codeExtract).toHaveBeenCalledTimes(1) + expect(realpathSync).toHaveBeenCalledWith('src/App.tsx') + }) + + it('should skip test and build outputs based on filters', () => { + vi.mocked(globSync).mockReturnValue([ + 'src/App.test.tsx', + '.next/page.tsx', + 'out/index.js', + 'src/keep.ts', + ]) + vi.mocked(realpathSync) + .mockReturnValueOnce('src/App.test.tsx') + .mockReturnValueOnce('.next/page.tsx') + .mockReturnValueOnce('out/index.js') + .mockReturnValueOnce('src/keep.ts') + + preload(/exclude/, '@devup-ui/react', false, '/output/css', []) + + expect(codeExtract).toHaveBeenCalledTimes(1) + expect(codeExtract).toHaveBeenCalledWith( + expect.stringMatching(/keep\.ts$/), + 'const Button = () =>
Hello
', + '@devup-ui/react', + '/output/css', + false, + false, + true, + ) + }) }) diff --git a/packages/next-plugin/src/__tests__/utils.test.ts b/packages/next-plugin/src/__tests__/utils.test.ts new file mode 100644 index 00000000..ec2ca3dc --- /dev/null +++ b/packages/next-plugin/src/__tests__/utils.test.ts @@ -0,0 +1,83 @@ +import type { PathLike } from 'node:fs' +import { join } from 'node:path' + +import { describe, expect, it, vi } from 'vitest' + +import { findTopPackageRoot } from '../find-top-package-root' +import { getPackageName } from '../get-package-name' +import { hasLocalPackage } from '../has-localpackage' + +vi.mock('node:fs', () => ({ + existsSync: vi.fn(), + readFileSync: vi.fn(), +})) + +const { existsSync, readFileSync } = await import('node:fs') + +describe('findTopPackageRoot', () => { + it('returns highest directory containing package.json', () => { + const root = join('/', 'repo') + const child = join(root, 'packages', 'pkg') + vi.mocked(existsSync).mockImplementation((path: PathLike) => { + if (path === join(root, 'package.json')) return true + return false + }) + + const result = findTopPackageRoot(child) + + expect(result).toBe(root) + }) + + it('falls back to cwd when no package.json found', () => { + const cwd = join('/', 'repo', 'packages', 'pkg') + vi.mocked(existsSync).mockReturnValue(false) + + const result = findTopPackageRoot(cwd) + + expect(result).toBe(cwd) + }) +}) + +describe('hasLocalPackage', () => { + it('detects workspace dependency', () => { + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify({ + dependencies: { + foo: 'workspace:*', + bar: '^1.0.0', + }, + }), + ) + + expect(hasLocalPackage()).toBe(true) + }) + + it('returns false when no workspace dependency', () => { + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify({ + dependencies: { + foo: '^1.0.0', + }, + }), + ) + + expect(hasLocalPackage()).toBe(false) + }) + + it('returns false when dependencies field is missing', () => { + vi.mocked(readFileSync).mockReturnValue('{}') + + expect(hasLocalPackage()).toBe(false) + }) +}) + +describe('getPackageName', () => { + it('reads and returns package name', () => { + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify({ name: '@scope/pkg' }), + ) + + expect(getPackageName('/path/package.json')).toBe('@scope/pkg') + expect(readFileSync).toHaveBeenCalledWith('/path/package.json', 'utf-8') + }) +})