Skip to content

Commit 61a3b32

Browse files
authored
Merge pull request #508 from dev-five-git/support-monorepo
Support monorepo
2 parents d2587b6 + 6a03794 commit 61a3b32

File tree

13 files changed

+266
-20
lines changed

13 files changed

+266
-20
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"changes": { "packages/next-plugin/package.json": "Patch" },
3+
"note": "Support monorepo on turbopack",
4+
"date": "2025-12-08T09:38:36.536760600Z"
5+
}

apps/next/next.config.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ const nextConfig = {
66
/* config options here */
77
}
88

9-
export default DevupUI(nextConfig)
9+
export default DevupUI(nextConfig, {
10+
include: ['vite-lib-example'],
11+
})

apps/next/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"react": "^19.2",
1414
"react-dom": "^19.2",
1515
"next": "^16.0",
16-
"@devup-ui/react": "workspace:*"
16+
"@devup-ui/react": "workspace:*",
17+
"vite-lib-example": "workspace:*"
1718
},
1819
"devDependencies": {
1920
"@devup-ui/next-plugin": "workspace:*",

apps/next/src/app/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { Box, css, styled, Text } from '@devup-ui/react'
44
import { useState } from 'react'
5+
import { Lib } from 'vite-lib-example'
56
const color = 'yellow'
67

78
const StyledFooter = styled.footer<{ type: '1' | '2' }>`
@@ -38,6 +39,7 @@ export default function HomePage() {
3839
py="28px"
3940
>
4041
<Box>hello</Box>
42+
<Lib />
4143
<Box>hello</Box>
4244
</Box>
4345
<Text

packages/next-plugin/src/__tests__/plugin.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ describe('DevupUINextPlugin', () => {
317317
'@devup-ui/react',
318318
false,
319319
expect.any(String),
320+
[],
320321
)
321322
})
322323
it('should create theme.d.ts file', async () => {

packages/next-plugin/src/__tests__/preload.test.ts

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { join } from 'node:path'
66
import { codeExtract, getCss } from '@devup-ui/wasm'
77
import { globSync } from 'glob'
88

9+
import { findTopPackageRoot } from '../find-top-package-root'
10+
import { getPackageName } from '../get-package-name'
11+
import { hasLocalPackage } from '../has-localpackage'
912
import { preload } from '../preload'
1013

1114
// Mock dependencies
@@ -33,6 +36,18 @@ vi.mock('@devup-ui/wasm', () => ({
3336
getCss: vi.fn(),
3437
}))
3538

39+
vi.mock('../find-top-package-root', () => ({
40+
findTopPackageRoot: vi.fn(),
41+
}))
42+
43+
vi.mock('../get-package-name', () => ({
44+
getPackageName: vi.fn(),
45+
}))
46+
47+
vi.mock('../has-localpackage', () => ({
48+
hasLocalPackage: vi.fn(),
49+
}))
50+
3651
describe('preload', () => {
3752
beforeEach(() => {
3853
vi.clearAllMocks()
@@ -63,13 +78,14 @@ describe('preload', () => {
6378
const singleCss = false
6479
const cssDir = '/output/css'
6580

66-
preload(excludeRegex, libPackage, singleCss, cssDir)
81+
preload(excludeRegex, libPackage, singleCss, cssDir, [])
6782

6883
expect(globSync).toHaveBeenCalledWith(
6984
['**/*.tsx', '**/*.ts', '**/*.js', '**/*.mjs'],
7085
{
7186
follow: true,
7287
absolute: true,
88+
cwd: expect.any(String),
7389
},
7490
)
7591
})
@@ -81,7 +97,7 @@ describe('preload', () => {
8197
.mockReturnValueOnce('src/App.tsx')
8298
.mockReturnValueOnce('src/components/Button.tsx')
8399
.mockReturnValueOnce('.next/page.tsx')
84-
preload(/node_modules/, '@devup-ui/react', false, '/output/css')
100+
preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
85101

86102
expect(codeExtract).toHaveBeenCalledTimes(2)
87103
expect(codeExtract).toHaveBeenCalledWith(
@@ -106,7 +122,7 @@ describe('preload', () => {
106122
[Symbol.dispose]: vi.fn(),
107123
})
108124

109-
preload(/node_modules/, '@devup-ui/react', false, '/output/css')
125+
preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
110126

111127
expect(writeFileSync).toHaveBeenCalledWith(
112128
join('/output/css', 'styles.css'),
@@ -127,7 +143,7 @@ describe('preload', () => {
127143
})
128144
vi.mocked(getCss).mockReturnValue('')
129145

130-
preload(/node_modules/, '@devup-ui/react', false, '/output/css')
146+
preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
131147

132148
expect(writeFileSync).toHaveBeenCalledWith(
133149
join('/output/css', 'devup-ui.css'),
@@ -147,7 +163,7 @@ describe('preload', () => {
147163
[Symbol.dispose]: vi.fn(),
148164
})
149165

150-
preload(/node_modules/, '@devup-ui/react', false, '/output/css')
166+
preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
151167

152168
expect(writeFileSync).toHaveBeenCalledWith(
153169
join('/output/css', 'styles.css'),
@@ -167,7 +183,7 @@ describe('preload', () => {
167183
[Symbol.dispose]: vi.fn(),
168184
})
169185

170-
preload(/node_modules/, '@devup-ui/react', false, '/output/css')
186+
preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
171187

172188
expect(writeFileSync).toHaveBeenCalledWith(
173189
join('/output/css', 'styles.css'),
@@ -181,7 +197,7 @@ describe('preload', () => {
181197
const singleCss = true
182198
const cssDir = '/custom/css/dir'
183199

184-
preload(/node_modules/, libPackage, singleCss, cssDir)
200+
preload(/node_modules/, libPackage, singleCss, cssDir, [])
185201

186202
expect(codeExtract).toHaveBeenCalledWith(
187203
expect.stringMatching(/App\.tsx$/),
@@ -218,7 +234,7 @@ describe('preload', () => {
218234
[Symbol.dispose]: vi.fn(),
219235
})
220236

221-
preload(/node_modules/, '@devup-ui/react', false, '/output/css')
237+
preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
222238

223239
expect(writeFileSync).toHaveBeenCalledTimes(3)
224240
expect(writeFileSync).toHaveBeenCalledWith(
@@ -232,4 +248,63 @@ describe('preload', () => {
232248
'utf-8',
233249
)
234250
})
251+
252+
it('should recurse into local workspaces when include is provided', () => {
253+
const files = ['src/App.tsx']
254+
vi.mocked(findTopPackageRoot).mockReturnValue('/repo')
255+
vi.mocked(hasLocalPackage)
256+
.mockReturnValueOnce(true)
257+
.mockReturnValueOnce(false)
258+
vi.mocked(globSync)
259+
.mockReturnValueOnce([
260+
'/repo/packages/pkg-a/package.json',
261+
'/repo/packages/pkg-b/package.json',
262+
])
263+
.mockReturnValueOnce(files)
264+
vi.mocked(getPackageName)
265+
.mockReturnValueOnce('pkg-a')
266+
.mockReturnValueOnce('pkg-b')
267+
vi.mocked(realpathSync).mockReturnValueOnce('src/App.tsx')
268+
269+
preload(/node_modules/, '@devup-ui/react', false, '/output/css', ['pkg-a'])
270+
271+
expect(findTopPackageRoot).toHaveBeenCalled()
272+
expect(globSync).toHaveBeenCalledWith(
273+
['package.json', '!**/node_modules/**'],
274+
{
275+
follow: true,
276+
absolute: true,
277+
cwd: '/repo',
278+
},
279+
)
280+
expect(codeExtract).toHaveBeenCalledTimes(1)
281+
expect(realpathSync).toHaveBeenCalledWith('src/App.tsx')
282+
})
283+
284+
it('should skip test and build outputs based on filters', () => {
285+
vi.mocked(globSync).mockReturnValue([
286+
'src/App.test.tsx',
287+
'.next/page.tsx',
288+
'out/index.js',
289+
'src/keep.ts',
290+
])
291+
vi.mocked(realpathSync)
292+
.mockReturnValueOnce('src/App.test.tsx')
293+
.mockReturnValueOnce('.next/page.tsx')
294+
.mockReturnValueOnce('out/index.js')
295+
.mockReturnValueOnce('src/keep.ts')
296+
297+
preload(/exclude/, '@devup-ui/react', false, '/output/css', [])
298+
299+
expect(codeExtract).toHaveBeenCalledTimes(1)
300+
expect(codeExtract).toHaveBeenCalledWith(
301+
expect.stringMatching(/keep\.ts$/),
302+
'const Button = () => <div>Hello</div>',
303+
'@devup-ui/react',
304+
'/output/css',
305+
false,
306+
false,
307+
true,
308+
)
309+
})
235310
})
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { PathLike } from 'node:fs'
2+
import { join } from 'node:path'
3+
4+
import { describe, expect, it, vi } from 'vitest'
5+
6+
import { findTopPackageRoot } from '../find-top-package-root'
7+
import { getPackageName } from '../get-package-name'
8+
import { hasLocalPackage } from '../has-localpackage'
9+
10+
vi.mock('node:fs', () => ({
11+
existsSync: vi.fn(),
12+
readFileSync: vi.fn(),
13+
}))
14+
15+
const { existsSync, readFileSync } = await import('node:fs')
16+
17+
describe('findTopPackageRoot', () => {
18+
it('returns highest directory containing package.json', () => {
19+
const root = join('/', 'repo')
20+
const child = join(root, 'packages', 'pkg')
21+
vi.mocked(existsSync).mockImplementation((path: PathLike) => {
22+
if (path === join(root, 'package.json')) return true
23+
return false
24+
})
25+
26+
const result = findTopPackageRoot(child)
27+
28+
expect(result).toBe(root)
29+
})
30+
31+
it('falls back to cwd when no package.json found', () => {
32+
const cwd = join('/', 'repo', 'packages', 'pkg')
33+
vi.mocked(existsSync).mockReturnValue(false)
34+
35+
const result = findTopPackageRoot(cwd)
36+
37+
expect(result).toBe(cwd)
38+
})
39+
})
40+
41+
describe('hasLocalPackage', () => {
42+
it('detects workspace dependency', () => {
43+
vi.mocked(readFileSync).mockReturnValue(
44+
JSON.stringify({
45+
dependencies: {
46+
foo: 'workspace:*',
47+
bar: '^1.0.0',
48+
},
49+
}),
50+
)
51+
52+
expect(hasLocalPackage()).toBe(true)
53+
})
54+
55+
it('returns false when no workspace dependency', () => {
56+
vi.mocked(readFileSync).mockReturnValue(
57+
JSON.stringify({
58+
dependencies: {
59+
foo: '^1.0.0',
60+
},
61+
}),
62+
)
63+
64+
expect(hasLocalPackage()).toBe(false)
65+
})
66+
67+
it('returns false when dependencies field is missing', () => {
68+
vi.mocked(readFileSync).mockReturnValue('{}')
69+
70+
expect(hasLocalPackage()).toBe(false)
71+
})
72+
})
73+
74+
describe('getPackageName', () => {
75+
it('reads and returns package name', () => {
76+
vi.mocked(readFileSync).mockReturnValue(
77+
JSON.stringify({ name: '@scope/pkg' }),
78+
)
79+
80+
expect(getPackageName('/path/package.json')).toBe('@scope/pkg')
81+
expect(readFileSync).toHaveBeenCalledWith('/path/package.json', 'utf-8')
82+
})
83+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { existsSync } from 'node:fs'
2+
import { dirname, join } from 'node:path'
3+
4+
/**
5+
* find package root
6+
*
7+
* Find the root of the package by checking the package.json file
8+
* @returns
9+
*/
10+
export function findTopPackageRoot(pwd = process.cwd()) {
11+
let current = pwd
12+
let topWithPackage: string | null = null
13+
14+
while (true) {
15+
if (existsSync(join(current, 'package.json'))) {
16+
topWithPackage = current
17+
}
18+
19+
const parent = dirname(current)
20+
if (parent === current) {
21+
break
22+
}
23+
current = parent
24+
}
25+
26+
return topWithPackage ?? pwd
27+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { readFileSync } from 'node:fs'
2+
3+
export function getPackageName(packageJsonPath: string) {
4+
const packageJson = readFileSync(packageJsonPath, 'utf-8')
5+
const packageJsonObject = JSON.parse(packageJson)
6+
return packageJsonObject.name
7+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { readFileSync } from 'node:fs'
2+
import { join } from 'node:path'
3+
4+
/**
5+
* has local package
6+
*
7+
* Check if the include workspace:* package is a local package
8+
* @returns
9+
*/
10+
export function hasLocalPackage() {
11+
const packageJson = readFileSync(join(process.cwd(), 'package.json'), 'utf-8')
12+
const packageJsonObject = JSON.parse(packageJson)
13+
return Object.values(packageJsonObject.dependencies ?? {}).some(
14+
(pkg: unknown) => typeof pkg === 'string' && pkg.includes('workspace:'),
15+
)
16+
}

0 commit comments

Comments
 (0)