diff --git a/apps/web/product-isolation.ts b/apps/web/product-isolation.ts index ee78af5ce9..c716329078 100644 --- a/apps/web/product-isolation.ts +++ b/apps/web/product-isolation.ts @@ -33,7 +33,12 @@ export function productWebBundleIsolation(repository: string, webRoot: string): return [{ name: 'dsh-product-web-chunk-inputs', apply: 'build', - generateBundle: { order: 'pre', handler(_options, bundle) { inputs.captureChunks(bundle) } }, + generateBundle: { + order: 'pre', + handler(_options, bundle) { + if (!dependencyAnalysis) inputs.captureChunks(bundle) + }, + }, }, { name: 'dsh-product-web-bundle-isolation', apply: 'build', diff --git a/scripts/browser-bundled-externals.spec.ts b/scripts/browser-bundled-externals.spec.ts index 549f2bded5..0607c99ae5 100644 --- a/scripts/browser-bundled-externals.spec.ts +++ b/scripts/browser-bundled-externals.spec.ts @@ -1,13 +1,30 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs' +import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { browserBundledExternals, browserPackageOfFile, browserSourceAliases } from './browser-bundled-externals.ts' +const filesystem = vi.hoisted(() => ({ preservedAlias: undefined as string | undefined })) +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + realpathSync: Object.assign((...args: Parameters) => { + // Windows' JavaScript realpath can retain an 8.3 alias that Vite's native resolution expands. + if (typeof args[0] === 'string' && args[0] === filesystem.preservedAlias) return args[0] + return actual.realpathSync(...args) + }, { native: actual.realpathSync.native }), + } +}) + const roots: string[] = [] +const dependencyLinks: string[] = [] const repositoryRoot = resolve(import.meta.dirname, '..') afterEach(() => { + filesystem.preservedAlias = undefined + for (const link of dependencyLinks.splice(0)) unlinkSync(link) for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) }) @@ -78,7 +95,10 @@ describe('browser dependency discovery', () => { await expect(browserBundledExternals(root)).rejects.toThrow('has no browser build config') }) - it.each([false, true])('follows shell aliases, CSS and lazy imports without writing output (symlinked root: %s)', async (linked) => { + it.each([ + ['Rollup', false, false], ['Rollup', true, false], ['Rollup', true, true], + ['Rolldown', false, false], ['Rolldown', true, false], ['Rolldown', true, true], + ] as const)('follows shell aliases, CSS and lazy imports without writing output (%s, symlinked root: %s, retained alias: %s)', async (bundler, linked, preserveAlias) => { const root = fixture() library(root, 'shell-lib') library(root, 'lazy-lib') @@ -96,7 +116,13 @@ describe('browser dependency discovery', () => { ].join('\n')) const app = join(root, 'apps/web') write(root, 'apps/web/package.json', '{"name":"@fixture/web","type":"module","exports":{"./dist/*":"./dist/*"}}') - symlinkSync(resolve(repositoryRoot, 'apps/web/node_modules'), join(app, 'node_modules'), 'junction') + const owner = bundler === 'Rollup' ? resolve(repositoryRoot, 'apps/web/package.json') + : createRequire(resolve(repositoryRoot, 'package.json')).resolve('vitest/package.json') + const viteDirectory = dirname(createRequire(owner).resolve('vite/package.json')) + const viteLink = join(app, 'node_modules/vite') + mkdirSync(dirname(viteLink), { recursive: true }) + symlinkSync(viteDirectory, viteLink, 'junction') + dependencyLinks.push(viteLink) write(root, 'apps/web/index.html', '') write(root, 'apps/web/main.ts', 'import { output, lazy } from "@fixture/static"; console.log(output); void lazy()') write(root, 'apps/web/vite.config.ts', ` @@ -104,7 +130,13 @@ describe('browser dependency discovery', () => { export default ({ mode }) => { if (mode !== 'production') throw new Error('notice discovery must use the production build mode') return { - plugins: productWebBundleIsolation(${JSON.stringify(root)}, ${JSON.stringify(app)}), + plugins: [...productWebBundleIsolation(${JSON.stringify(root)}, ${JSON.stringify(app)}), { + name: 'fixture-bundler-engine', + generateBundle() { + const actual = typeof this.meta.rolldownVersion === 'string' ? 'Rolldown' : 'Rollup' + if (actual !== ${JSON.stringify(bundler)}) throw new Error('unexpected bundler: ' + actual) + }, + }], build: { rollupOptions: { input: { index: ${JSON.stringify(join(app, 'index.html'))}, preview: "missing-preview.ts" } } } } } @@ -113,9 +145,11 @@ describe('browser dependency discovery', () => { const scanRoot = linked ? join(fixture(), 'linked') : root if (linked) symlinkSync(root, scanRoot, 'junction') + if (preserveAlias) filesystem.preservedAlias = scanRoot try { expect(await browserBundledExternals(scanRoot)).toEqual(new Set(['shell-lib', 'lazy-lib', 'asset-lib'])) } finally { + filesystem.preservedAlias = undefined if (linked) unlinkSync(scanRoot) } expect(readFileSync(join(app, 'dist/sentinel.txt'), 'utf8')).toBe('untouched') diff --git a/scripts/browser-bundled-externals.ts b/scripts/browser-bundled-externals.ts index 8675fdd006..7669a234ea 100644 --- a/scripts/browser-bundled-externals.ts +++ b/scripts/browser-bundled-externals.ts @@ -166,8 +166,8 @@ async function collectShell( * @returns Names of distributed browser inputs, excluding workspace packages and erased types. */ export async function browserBundledExternals(root: string): Promise> { - // Vite resolves HTML inputs to real paths, so its root must use the same spelling. - root = realpathSync(root) + // Vite resolves HTML through native realpath, including Windows 8.3 alias expansion. + root = realpathSync.native(root) const manifests = new Map() for (const glob of ['packages/*/*/package.json', 'vendor/*/package.json']) { for (const path of globSync(glob, { cwd: root }).sort()) { diff --git a/scripts/web-product-bundle-isolation.spec.ts b/scripts/web-product-bundle-isolation.spec.ts index b5e14dab94..a3c9005a02 100644 --- a/scripts/web-product-bundle-isolation.spec.ts +++ b/scripts/web-product-bundle-isolation.spec.ts @@ -25,7 +25,8 @@ const vite = createRequire(new URL('../apps/web/package.json', import.meta.url)) } function fixture() { - const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-web-bundle-isolation-'))) + // Vite resolves inputs with native realpath, which expands Windows short names. + const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-web-bundle-isolation-'))) const web = join(root, 'apps/web') const links: string[] = [] onTestFinished(() => { @@ -89,7 +90,12 @@ describe('default Web bundle input isolation', () => { }) it('allows experimental code in the separately emitted preview entry', async () => { - await expect(fixture().run()).resolves.toBeDefined() + await expect(fixture().run()).resolves.toMatchObject({ + output: expect.arrayContaining([ + expect.objectContaining({ type: 'asset', fileName: 'index.html' }), + expect.objectContaining({ type: 'chunk', name: 'bootstrap' }), + ]) as unknown, + }) }) it.each([