diff --git a/.changepacks/changepack_log_gfbYKEAW0SjV8Y_4ve5rm.json b/.changepacks/changepack_log_gfbYKEAW0SjV8Y_4ve5rm.json
new file mode 100644
index 00000000..d210648a
--- /dev/null
+++ b/.changepacks/changepack_log_gfbYKEAW0SjV8Y_4ve5rm.json
@@ -0,0 +1,5 @@
+{
+ "changes": { "packages/next-plugin/package.json": "Patch" },
+ "note": "Fix workspace issue",
+ "date": "2025-12-30T11:25:19.439193400Z"
+}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 9a13d512..3f261898 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -1,125 +1,116 @@
-name: Publish Package to npm
-
-on:
- push:
- branches:
- - main
- pull_request:
- branches:
- - main
-permissions: write-all
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: false
-
-jobs:
- benchmark:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v5
-
- - uses: actions-rust-lang/setup-rust-toolchain@v1
- - name: Cargo tarpaulin and fmt
- run: |
- cargo install cargo-tarpaulin
- rustup component add rustfmt clippy
- - uses: pnpm/action-setup@v4
- name: Install pnpm
- with:
- run_install: false
-
- - uses: jetli/wasm-pack-action@v0.4.0
- with:
- version: 'latest'
- - name: Install Node.js
- uses: actions/setup-node@v4
- with:
- registry-url: "https://registry.npmjs.org"
- node-version: 22
- cache: 'pnpm'
- - run: pnpm i
- - run: pnpm build
- - name: Benchmark
- run: pnpm benchmark
-
- publish:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v5
-
- - uses: actions-rust-lang/setup-rust-toolchain@v1
- - name: Cargo tarpaulin and fmt
- run: |
- cargo install cargo-tarpaulin
- rustup component add rustfmt clippy
- - uses: pnpm/action-setup@v4
- name: Install pnpm
- with:
- run_install: false
-
- - uses: jetli/wasm-pack-action@v0.4.0
- with:
- version: 'latest'
- - name: Install Node.js
- uses: actions/setup-node@v4
- with:
- registry-url: "https://registry.npmjs.org"
- node-version: 22
- cache: 'pnpm'
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- - run: pnpm i
- - run: pnpm build
- - run: |
- pnpm lint
- # rust coverage issue
- echo 'max_width = 100000' > .rustfmt.toml
- echo 'tab_spaces = 4' >> .rustfmt.toml
- echo 'newline_style = "Unix"' >> .rustfmt.toml
- echo 'fn_call_width = 100000' >> .rustfmt.toml
- echo 'fn_params_layout = "Compressed"' >> .rustfmt.toml
- echo 'chain_width = 100000' >> .rustfmt.toml
- echo 'merge_derives = true' >> .rustfmt.toml
- echo 'use_small_heuristics = "Default"' >> .rustfmt.toml
- cargo fmt
- - run: pnpm test
- - name: Format Rollback
- run: |
- rm -rf .rustfmt.toml
- cargo fmt
- - name: Build Landing
- run: |
- pnpm -F components build-storybook
- mv ./packages/components/storybook-static ./apps/landing/public/storybook
- pnpm -F landing build
- - name: Upload artifact
- uses: actions/upload-pages-artifact@v3
- with:
- path: ./apps/landing/out
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- - uses: actions/deploy-pages@v4
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- - name: Upload to codecov.io
- uses: codecov/codecov-action@v5
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- fail_ci_if_error: true
-
- - uses: changepacks/action@main
- id: changepacks
-
- - name: Publish to npm
- run: |
- echo '${{ steps.changepacks.outputs.changepacks }}' | jq -r '.[]' | while read package; do
- package_dir=$(dirname "$package")
- echo "Publishing package: $package_dir"
- cd "$package_dir"
- pnpm publish --access public --no-git-checks
- cd "${{ github.workspace }}"
- done
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- if: steps.changepacks.outputs.changepacks != ''
+name: Publish Package to npm
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+permissions: write-all
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ benchmark:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - uses: actions-rust-lang/setup-rust-toolchain@v1
+ - name: Cargo tarpaulin and fmt
+ run: |
+ cargo install cargo-tarpaulin
+ rustup component add rustfmt clippy
+ - uses: pnpm/action-setup@v4
+ name: Install pnpm
+ with:
+ run_install: false
+
+ - uses: jetli/wasm-pack-action@v0.4.0
+ with:
+ version: 'latest'
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ registry-url: "https://registry.npmjs.org"
+ node-version: 22
+ cache: 'pnpm'
+ - run: pnpm i
+ - run: pnpm build
+ - name: Benchmark
+ run: pnpm benchmark
+
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - uses: actions-rust-lang/setup-rust-toolchain@v1
+ - name: Cargo tarpaulin and fmt
+ run: |
+ cargo install cargo-tarpaulin
+ rustup component add rustfmt clippy
+ - uses: pnpm/action-setup@v4
+ name: Install pnpm
+ with:
+ run_install: false
+
+ - uses: jetli/wasm-pack-action@v0.4.0
+ with:
+ version: 'latest'
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ registry-url: "https://registry.npmjs.org"
+ node-version: 22
+ cache: 'pnpm'
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ - run: pnpm i
+ - run: pnpm build
+ - run: |
+ pnpm lint
+ # rust coverage issue
+ echo 'max_width = 100000' > .rustfmt.toml
+ echo 'tab_spaces = 4' >> .rustfmt.toml
+ echo 'newline_style = "Unix"' >> .rustfmt.toml
+ echo 'fn_call_width = 100000' >> .rustfmt.toml
+ echo 'fn_params_layout = "Compressed"' >> .rustfmt.toml
+ echo 'chain_width = 100000' >> .rustfmt.toml
+ echo 'merge_derives = true' >> .rustfmt.toml
+ echo 'use_small_heuristics = "Default"' >> .rustfmt.toml
+ cargo fmt
+ - run: pnpm test
+ - name: Format Rollback
+ run: |
+ rm -rf .rustfmt.toml
+ cargo fmt
+ - name: Build Landing
+ run: |
+ pnpm -F components build-storybook
+ mv ./packages/components/storybook-static ./apps/landing/public/storybook
+ pnpm -F landing build
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: ./apps/landing/out
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ - uses: actions/deploy-pages@v4
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ - name: Upload to codecov.io
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: true
+
+ - uses: changepacks/action@main
+ id: changepacks
+ with:
+ publish: true
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/packages/next-plugin/package.json b/packages/next-plugin/package.json
index bd135feb..f547d438 100644
--- a/packages/next-plugin/package.json
+++ b/packages/next-plugin/package.json
@@ -52,7 +52,7 @@
"@devup-ui/webpack-plugin": "workspace:^",
"next": "^16.1",
"@devup-ui/wasm": "workspace:^",
- "glob": "^13.0"
+ "tinyglobby": "^0.2"
},
"devDependencies": {
"vite": "^7.3",
diff --git a/packages/next-plugin/src/__tests__/preload.test.ts b/packages/next-plugin/src/__tests__/preload.test.ts
index d46ec6fd..66a42008 100644
--- a/packages/next-plugin/src/__tests__/preload.test.ts
+++ b/packages/next-plugin/src/__tests__/preload.test.ts
@@ -1,310 +1,337 @@
-import { realpathSync, writeFileSync } from 'node:fs'
-import { readFileSync } from 'node:fs'
-import { existsSync } from 'node:fs'
-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
-vi.mock('node:fs')
-vi.mock('@devup-ui/wasm')
-vi.mock('glob')
-
-// Mock globSync
-vi.mock('node:fs', () => ({
- readFileSync: vi.fn(),
- writeFileSync: vi.fn(),
- mkdirSync: vi.fn(),
- existsSync: vi.fn(),
- realpathSync: vi.fn().mockReturnValue('src/App.tsx'),
-}))
-
-vi.mock('glob', () => ({
- globSync: vi.fn(),
-}))
-
-// Mock @devup-ui/wasm
-vi.mock('@devup-ui/wasm', () => ({
- codeExtract: vi.fn(),
- registerTheme: vi.fn(),
- 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()
-
- // Default mock implementations
- vi.mocked(globSync).mockReturnValue([
- 'src/App.tsx',
- 'src/components/Button.tsx',
- ])
- vi.mocked(readFileSync).mockReturnValue(
- 'const Button = () =>
Hello
',
- )
- vi.mocked(codeExtract).mockReturnValue({
- free: vi.fn(),
- cssFile: 'styles.css',
- css: '.button { color: red; }',
- code: '',
- map: '',
- updatedBaseStyle: false,
- [Symbol.dispose]: vi.fn(),
- })
- vi.mocked(existsSync).mockReturnValue(true)
- })
-
- it('should find project root and collect files', () => {
- const excludeRegex = /node_modules/
- const libPackage = '@devup-ui/react'
- const singleCss = false
- const cssDir = '/output/css'
-
- preload(excludeRegex, libPackage, singleCss, cssDir, [])
-
- expect(globSync).toHaveBeenCalledWith(
- ['**/*.tsx', '**/*.ts', '**/*.js', '**/*.mjs'],
- {
- follow: true,
- absolute: true,
- cwd: expect.any(String),
- },
- )
- })
-
- it('should process each collected file', () => {
- const files = ['src/App.tsx', 'src/components/Button.tsx', '.next/page.tsx']
- vi.mocked(globSync).mockReturnValue(files)
- vi.mocked(realpathSync)
- .mockReturnValueOnce('src/App.tsx')
- .mockReturnValueOnce('src/components/Button.tsx')
- .mockReturnValueOnce('.next/page.tsx')
- preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
-
- expect(codeExtract).toHaveBeenCalledTimes(2)
- expect(codeExtract).toHaveBeenCalledWith(
- expect.stringMatching(/App\.tsx$/),
- 'const Button = () => Hello
',
- '@devup-ui/react',
- '/output/css',
- false,
- false,
- true,
- )
- })
-
- it('should write CSS file when cssFile is returned', () => {
- vi.mocked(codeExtract).mockReturnValue({
- cssFile: 'styles.css',
- css: '.button { color: red; }',
- free: vi.fn(),
- code: '',
- map: '',
- updatedBaseStyle: false,
- [Symbol.dispose]: vi.fn(),
- })
-
- preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
-
- expect(writeFileSync).toHaveBeenCalledWith(
- join('/output/css', 'styles.css'),
- '.button { color: red; }',
- 'utf-8',
- )
- })
-
- it('should not write CSS file when cssFile is null', () => {
- vi.mocked(codeExtract).mockReturnValue({
- cssFile: undefined,
- css: '.button { color: red; }',
- free: vi.fn(),
- code: '',
- map: '',
- updatedBaseStyle: false,
- [Symbol.dispose]: vi.fn(),
- })
- vi.mocked(getCss).mockReturnValue('')
-
- preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
-
- expect(writeFileSync).toHaveBeenCalledWith(
- join('/output/css', 'devup-ui.css'),
- '',
- 'utf-8',
- )
- })
-
- it('should handle empty CSS content', () => {
- vi.mocked(codeExtract).mockReturnValue({
- cssFile: 'styles.css',
- css: '',
- free: vi.fn(),
- code: '',
- map: '',
- updatedBaseStyle: false,
- [Symbol.dispose]: vi.fn(),
- })
-
- preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
-
- expect(writeFileSync).toHaveBeenCalledWith(
- join('/output/css', 'styles.css'),
- '',
- 'utf-8',
- )
- })
-
- it('should handle undefined CSS content', () => {
- vi.mocked(codeExtract).mockReturnValue({
- cssFile: 'styles.css',
- css: undefined,
- free: vi.fn(),
- code: '',
- map: '',
- updatedBaseStyle: false,
- [Symbol.dispose]: vi.fn(),
- })
-
- preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
-
- expect(writeFileSync).toHaveBeenCalledWith(
- join('/output/css', 'styles.css'),
- '',
- 'utf-8',
- )
- })
-
- it('should pass correct parameters to codeExtract', () => {
- const libPackage = '@devup-ui/react'
- const singleCss = true
- const cssDir = '/custom/css/dir'
-
- preload(/node_modules/, libPackage, singleCss, cssDir, [])
-
- expect(codeExtract).toHaveBeenCalledWith(
- expect.stringMatching(/App\.tsx$/),
- 'const Button = () => Hello
',
- libPackage,
- cssDir,
- singleCss,
- false,
- true,
- )
- })
-
- it('should handle multiple files with different CSS outputs', () => {
- const files = ['src/App.tsx', 'src/components/Button.tsx']
- vi.mocked(globSync).mockReturnValue(files)
-
- vi.mocked(codeExtract)
- .mockReturnValueOnce({
- cssFile: 'app.css',
- css: '.app { margin: 0; }',
- free: vi.fn(),
- code: '',
- map: '',
- updatedBaseStyle: false,
- [Symbol.dispose]: vi.fn(),
- })
- .mockReturnValueOnce({
- free: vi.fn(),
- cssFile: 'button.css',
- css: '.button { color: blue; }',
- code: '',
- map: '',
- updatedBaseStyle: false,
- [Symbol.dispose]: vi.fn(),
- })
-
- preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
-
- expect(writeFileSync).toHaveBeenCalledTimes(3)
- expect(writeFileSync).toHaveBeenCalledWith(
- join('/output/css', 'app.css'),
- '.app { margin: 0; }',
- 'utf-8',
- )
- expect(writeFileSync).toHaveBeenCalledWith(
- join('/output/css', 'button.css'),
- '.button { color: blue; }',
- '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,
- )
- })
-})
+import { realpathSync, writeFileSync } from 'node:fs'
+import { readFileSync } from 'node:fs'
+import { existsSync } from 'node:fs'
+import { join } from 'node:path'
+
+import { codeExtract, getCss } from '@devup-ui/wasm'
+import { globSync } from 'tinyglobby'
+
+import { findTopPackageRoot } from '../find-top-package-root'
+import { getPackageName } from '../get-package-name'
+import { hasLocalPackage } from '../has-localpackage'
+import { preload } from '../preload'
+
+// Mock dependencies
+vi.mock('node:fs')
+vi.mock('@devup-ui/wasm')
+vi.mock('tinyglobby')
+
+// Mock globSync
+vi.mock('node:fs', () => ({
+ readFileSync: vi.fn(),
+ writeFileSync: vi.fn(),
+ mkdirSync: vi.fn(),
+ existsSync: vi.fn(),
+ realpathSync: vi.fn().mockReturnValue('src/App.tsx'),
+}))
+
+vi.mock('tinyglobby', () => ({
+ globSync: vi.fn(),
+}))
+
+// Mock @devup-ui/wasm
+vi.mock('@devup-ui/wasm', () => ({
+ codeExtract: vi.fn(),
+ registerTheme: vi.fn(),
+ 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()
+
+ // Default mock implementations
+ vi.mocked(globSync).mockReturnValue([
+ 'src/App.tsx',
+ 'src/components/Button.tsx',
+ ])
+ vi.mocked(readFileSync).mockReturnValue(
+ 'const Button = () => Hello
',
+ )
+ vi.mocked(codeExtract).mockReturnValue({
+ free: vi.fn(),
+ cssFile: 'styles.css',
+ css: '.button { color: red; }',
+ code: '',
+ map: '',
+ updatedBaseStyle: false,
+ [Symbol.dispose]: vi.fn(),
+ })
+ vi.mocked(existsSync).mockReturnValue(true)
+ })
+
+ it('should find project root and collect files', () => {
+ const excludeRegex = /node_modules/
+ const libPackage = '@devup-ui/react'
+ const singleCss = false
+ const cssDir = '/output/css'
+
+ preload(excludeRegex, libPackage, singleCss, cssDir, [])
+
+ expect(globSync).toHaveBeenCalledWith(
+ ['**/*.tsx', '**/*.ts', '**/*.js', '**/*.mjs'],
+ {
+ followSymbolicLinks: true,
+ absolute: true,
+ cwd: expect.any(String),
+ },
+ )
+ })
+
+ it('should process each collected file', () => {
+ const files = ['src/App.tsx', 'src/components/Button.tsx', '.next/page.tsx']
+ vi.mocked(globSync).mockReturnValue(files)
+ vi.mocked(realpathSync)
+ .mockReturnValueOnce('src/App.tsx')
+ .mockReturnValueOnce('src/components/Button.tsx')
+ .mockReturnValueOnce('.next/page.tsx')
+ preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
+
+ expect(codeExtract).toHaveBeenCalledTimes(2)
+ expect(codeExtract).toHaveBeenCalledWith(
+ expect.stringMatching(/App\.tsx$/),
+ 'const Button = () => Hello
',
+ '@devup-ui/react',
+ '/output/css',
+ false,
+ false,
+ true,
+ )
+ })
+
+ it('should write CSS file when cssFile is returned', () => {
+ vi.mocked(codeExtract).mockReturnValue({
+ cssFile: 'styles.css',
+ css: '.button { color: red; }',
+ free: vi.fn(),
+ code: '',
+ map: '',
+ updatedBaseStyle: false,
+ [Symbol.dispose]: vi.fn(),
+ })
+
+ preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
+
+ expect(writeFileSync).toHaveBeenCalledWith(
+ join('/output/css', 'styles.css'),
+ '.button { color: red; }',
+ 'utf-8',
+ )
+ })
+
+ it('should not write CSS file when cssFile is null', () => {
+ vi.mocked(codeExtract).mockReturnValue({
+ cssFile: undefined,
+ css: '.button { color: red; }',
+ free: vi.fn(),
+ code: '',
+ map: '',
+ updatedBaseStyle: false,
+ [Symbol.dispose]: vi.fn(),
+ })
+ vi.mocked(getCss).mockReturnValue('')
+
+ preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
+
+ expect(writeFileSync).toHaveBeenCalledWith(
+ join('/output/css', 'devup-ui.css'),
+ '',
+ 'utf-8',
+ )
+ })
+
+ it('should handle empty CSS content', () => {
+ vi.mocked(codeExtract).mockReturnValue({
+ cssFile: 'styles.css',
+ css: '',
+ free: vi.fn(),
+ code: '',
+ map: '',
+ updatedBaseStyle: false,
+ [Symbol.dispose]: vi.fn(),
+ })
+
+ preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
+
+ expect(writeFileSync).toHaveBeenCalledWith(
+ join('/output/css', 'styles.css'),
+ '',
+ 'utf-8',
+ )
+ })
+
+ it('should handle undefined CSS content', () => {
+ vi.mocked(codeExtract).mockReturnValue({
+ cssFile: 'styles.css',
+ css: undefined,
+ free: vi.fn(),
+ code: '',
+ map: '',
+ updatedBaseStyle: false,
+ [Symbol.dispose]: vi.fn(),
+ })
+
+ preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
+
+ expect(writeFileSync).toHaveBeenCalledWith(
+ join('/output/css', 'styles.css'),
+ '',
+ 'utf-8',
+ )
+ })
+
+ it('should pass correct parameters to codeExtract', () => {
+ const libPackage = '@devup-ui/react'
+ const singleCss = true
+ const cssDir = '/custom/css/dir'
+
+ preload(/node_modules/, libPackage, singleCss, cssDir, [])
+
+ expect(codeExtract).toHaveBeenCalledWith(
+ expect.stringMatching(/App\.tsx$/),
+ 'const Button = () => Hello
',
+ libPackage,
+ cssDir,
+ singleCss,
+ false,
+ true,
+ )
+ })
+
+ it('should handle multiple files with different CSS outputs', () => {
+ const files = ['src/App.tsx', 'src/components/Button.tsx']
+ vi.mocked(globSync).mockReturnValue(files)
+
+ vi.mocked(codeExtract)
+ .mockReturnValueOnce({
+ cssFile: 'app.css',
+ css: '.app { margin: 0; }',
+ free: vi.fn(),
+ code: '',
+ map: '',
+ updatedBaseStyle: false,
+ [Symbol.dispose]: vi.fn(),
+ })
+ .mockReturnValueOnce({
+ free: vi.fn(),
+ cssFile: 'button.css',
+ css: '.button { color: blue; }',
+ code: '',
+ map: '',
+ updatedBaseStyle: false,
+ [Symbol.dispose]: vi.fn(),
+ })
+
+ preload(/node_modules/, '@devup-ui/react', false, '/output/css', [])
+
+ expect(writeFileSync).toHaveBeenCalledTimes(3)
+ expect(writeFileSync).toHaveBeenCalledWith(
+ join('/output/css', 'app.css'),
+ '.app { margin: 0; }',
+ 'utf-8',
+ )
+ expect(writeFileSync).toHaveBeenCalledWith(
+ join('/output/css', 'button.css'),
+ '.button { color: blue; }',
+ '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/**'],
+ {
+ followSymbolicLinks: true,
+ absolute: true,
+ cwd: '/repo',
+ },
+ )
+ expect(codeExtract).toHaveBeenCalledTimes(3)
+ 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,
+ )
+ })
+
+ it('should return early when nested is true and include packages exist', () => {
+ vi.mocked(findTopPackageRoot).mockReturnValue('/repo')
+ vi.mocked(hasLocalPackage).mockReturnValue(true)
+ // Return empty array so no recursive calls happen, but include.length > 0 check passes
+ vi.mocked(globSync).mockReturnValue([])
+ vi.mocked(getPackageName).mockReturnValue('pkg-a')
+
+ // Call with nested = true (7th parameter)
+ preload(
+ /node_modules/,
+ '@devup-ui/react',
+ false,
+ '/output/css',
+ ['pkg-a'],
+ '/some/path',
+ true,
+ )
+
+ // When nested is true, it should return early after processing includes
+ // and not write the final devup-ui.css file
+ expect(writeFileSync).not.toHaveBeenCalledWith(
+ expect.stringContaining('devup-ui.css'),
+ expect.any(String),
+ 'utf-8',
+ )
+ })
+})
diff --git a/packages/next-plugin/src/preload.ts b/packages/next-plugin/src/preload.ts
index d3839eab..6ffbe240 100644
--- a/packages/next-plugin/src/preload.ts
+++ b/packages/next-plugin/src/preload.ts
@@ -1,65 +1,65 @@
-import { readFileSync, realpathSync, writeFileSync } from 'node:fs'
-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 (
- /\.(test(-d)?|d|spec)\.(tsx|ts|js|mjs)$/.test(filePath) ||
- /^(out|.next)[/\\]/.test(filePath) ||
- excludeRegex.test(filePath)
- )
- continue
- const { cssFile, css } = codeExtract(
- filePath,
- readFileSync(filePath, 'utf-8'),
- libPackage,
- cssDir,
- singleCss,
- false,
- true,
- )
-
- if (cssFile) {
- writeFileSync(join(cssDir, basename(cssFile!)), css ?? '', 'utf-8')
- }
- }
- writeFileSync(join(cssDir, 'devup-ui.css'), getCss(null, false), 'utf-8')
-}
+import { readFileSync, realpathSync, writeFileSync } from 'node:fs'
+import { basename, dirname, join, relative } from 'node:path'
+
+import { codeExtract, getCss } from '@devup-ui/wasm'
+import { globSync } from 'tinyglobby'
+
+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(),
+ nested = false,
+) {
+ if (include.length > 0 && hasLocalPackage()) {
+ const packageRoot = findTopPackageRoot()
+ const collected = globSync(['package.json', '!**/node_modules/**'], {
+ followSymbolicLinks: 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, true)
+ }
+ if (nested) return
+ }
+ const collected = globSync(['**/*.tsx', '**/*.ts', '**/*.js', '**/*.mjs'], {
+ followSymbolicLinks: true,
+ absolute: true,
+ cwd: pwd,
+ })
+ // fix multi core build issue
+ collected.sort()
+ for (const file of collected) {
+ const filePath = relative(process.cwd(), realpathSync(file))
+ if (
+ /\.(test(-d)?|d|spec)\.(tsx|ts|js|mjs)$/.test(filePath) ||
+ /^(out|.next)[/\\]/.test(filePath) ||
+ excludeRegex.test(filePath)
+ )
+ continue
+ const { cssFile, css } = codeExtract(
+ filePath,
+ readFileSync(filePath, 'utf-8'),
+ libPackage,
+ cssDir,
+ singleCss,
+ false,
+ true,
+ )
+
+ if (cssFile) {
+ writeFileSync(join(cssDir, basename(cssFile!)), css ?? '', 'utf-8')
+ }
+ }
+ writeFileSync(join(cssDir, 'devup-ui.css'), getCss(null, false), 'utf-8')
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 61886968..34024f63 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -757,12 +757,12 @@ importers:
'@devup-ui/webpack-plugin':
specifier: workspace:^
version: link:../webpack-plugin
- glob:
- specifier: ^13.0
- version: 13.0.0
next:
specifier: ^16.1
version: 16.1.1(@babel/core@7.28.5)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ tinyglobby:
+ specifier: ^0.2
+ version: 0.2.15
devDependencies:
'@types/webpack':
specifier: ^5.28