From 5b491e8cc632566d00da5bf7473a80ab1a122c84 Mon Sep 17 00:00:00 2001
From: Tianyi Cui <53024+tianyicui@users.noreply.github.com>
Date: Sat, 12 Sep 2026 21:20:43 +0800
Subject: [PATCH 1/3] fix(ci): skip product graph capture during dependency
analysis
---
apps/web/product-isolation.ts | 7 ++++++-
scripts/browser-bundled-externals.spec.ts | 23 ++++++++++++++++++++---
2 files changed, 26 insertions(+), 4 deletions(-)
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..17e708d6c0 100644
--- a/scripts/browser-bundled-externals.spec.ts
+++ b/scripts/browser-bundled-externals.spec.ts
@@ -1,13 +1,16 @@
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 { browserBundledExternals, browserPackageOfFile, browserSourceAliases } from './browser-bundled-externals.ts'
const roots: string[] = []
+const dependencyLinks: string[] = []
const repositoryRoot = resolve(import.meta.dirname, '..')
afterEach(() => {
+ for (const link of dependencyLinks.splice(0)) unlinkSync(link)
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
@@ -78,7 +81,9 @@ 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], ['Rollup', true], ['Rolldown', false], ['Rolldown', true],
+ ] as const)('follows shell aliases, CSS and lazy imports without writing output (%s, symlinked root: %s)', async (bundler, linked) => {
const root = fixture()
library(root, 'shell-lib')
library(root, 'lazy-lib')
@@ -96,7 +101,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 +115,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" } } }
}
}
From 00b2470eb6c8ed6dbeb3befbdd55446ef438e96e Mon Sep 17 00:00:00 2001
From: Tianyi Cui <53024+tianyicui@users.noreply.github.com>
Date: Sat, 12 Sep 2026 21:26:53 +0800
Subject: [PATCH 2/3] test: canonicalize Windows Vite fixture roots
---
scripts/web-product-bundle-isolation.spec.ts | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
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([
From 0e060aa67534c1d4e3457ada5d377b658a12ff21 Mon Sep 17 00:00:00 2001
From: Tianyi Cui <53024+tianyicui@users.noreply.github.com>
Date: Sat, 12 Sep 2026 21:30:51 +0800
Subject: [PATCH 3/3] fix(ci): canonicalize browser notice roots with native
realpath
---
scripts/browser-bundled-externals.spec.ts | 23 ++++++++++++++++++++---
scripts/browser-bundled-externals.ts | 4 ++--
2 files changed, 22 insertions(+), 5 deletions(-)
diff --git a/scripts/browser-bundled-externals.spec.ts b/scripts/browser-bundled-externals.spec.ts
index 17e708d6c0..0607c99ae5 100644
--- a/scripts/browser-bundled-externals.spec.ts
+++ b/scripts/browser-bundled-externals.spec.ts
@@ -2,14 +2,28 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync,
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 })
})
@@ -82,8 +96,9 @@ describe('browser dependency discovery', () => {
})
it.each([
- ['Rollup', false], ['Rollup', true], ['Rolldown', false], ['Rolldown', true],
- ] as const)('follows shell aliases, CSS and lazy imports without writing output (%s, symlinked root: %s)', async (bundler, linked) => {
+ ['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')
@@ -130,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()) {