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