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 6552fbdc..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()
@@ -63,13 +78,14 @@ 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'],
{
follow: true,
absolute: true,
+ cwd: expect.any(String),
},
)
})
@@ -81,7 +97,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 +122,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 +143,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 +163,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 +183,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 +197,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 +234,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(
@@ -232,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')
+ })
+})
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 (
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