feat(bundle): accept an ordered patch file list in dsh.bundle.patch (#4722)

* feat(bundle): accept an ordered patch file list in dsh.bundle.patch

`dsh.bundle.patch` now accepts `string | string[]`. A list names patch
files applied in order as one bundle layer; each file's relative plugin
paths still resolve beside that file. `bundlePatchPaths` in dsh-app-boot
is the one resolver from a declaration to absolute paths, and every
reader uses it: profile loading, plugin-manager reconcile and bundle
rows, the client-runtime roster, and the workspace, cordis-config, and
product-isolation gates. `ProfileLayer.patchPath` becomes `patchPaths`.

* fix(bundle): keep declared patch files package-relative in the workspace gate

path.join produced backslash entries on Windows, so the expected files
list did not match package.json files. bundlePatchFiles returns the
validated declaration as written; bundlePatchPaths joins it.
This commit is contained in:
Turtle authored and GitHub committed 2026-09-20 18:20:24 +08:00
1 parent d495b2e5f2
commit 654caa4bbd
25 files changed
+166 -73

No files matched your search

@@ -232,6 +232,15 @@ describe('package payload constraints', () => {
'cordis.patch.yml',
'lib/types/**/*.d.ts',
])
expect(expectedDshPackageFiles({
name: '@deepseek-ai/dsh-private-profile',
dsh: { bundle: { patch: ['./cordis.patch.yml', './layers/web.patch.yml'] } },
})).toEqual([
'lib/index.js',
'cordis.patch.yml',
'layers/web.patch.yml',
'lib/types/**/*.d.ts',
])
})
it.each([
+5 -6
View File
@@ -13,7 +13,8 @@ import {
PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES,
} from './experimental-package-policy.ts'
import { hasTypertRemoteNavigation, isForbiddenPublicationFile } from './publication-payload.ts'
import { OPTIONAL_BUNDLES } from '../packages/boot/app-boot/src/profile.ts'
import type { DshBundleManifest } from '../packages/util/package-manifest/src/types.ts'
import { OPTIONAL_BUNDLES, bundlePatchFiles } from '../packages/boot/app-boot/src/profile.ts'
import { collectProjectReferenceFaceViolations } from './project-reference-faces.ts'
const root = resolve(import.meta.dirname, '..')
@@ -98,9 +99,7 @@ export interface PackageManifest {
dependencies?: Record<string, string>
optionalDependencies?: Record<string, string>
dsh?: {
bundle?: {
patch?: string
}
bundle?: DshBundleManifest
}
}
@@ -222,8 +221,8 @@ export function expectedDshPackageFiles(manifest: PackageManifest): readonly str
const target = exportDefault(manifest, resource)
if (target?.startsWith('./') && target.endsWith('.json')) localeFiles.add(target.slice(2))
}
const declaredPatch = manifest.dsh?.bundle?.patch
const bundleFiles = declaredPatch === undefined ? [] : [declaredPatch.replace(/^\.\//, '')]
const bundle = manifest.dsh?.bundle
const bundleFiles = bundle === undefined ? [] : bundlePatchFiles(bundle).map(file => file.replace(/^\.\//, ''))
const extras = [
...bundleFiles,
...(manifest.name ? packageFileExtras[manifest.name] ?? [] : []),
+9 -6
View File
@@ -14,6 +14,8 @@ import { globSync, readFileSync } from 'node:fs'
import { dirname, relative, resolve } from 'node:path'
import { Script } from 'node:vm'
import ts from 'typescript'
import type { DshBundleManifest } from '../packages/util/package-manifest/src/types.ts'
import { bundlePatchPaths } from '../packages/boot/app-boot/src/profile.ts'
import { cordisConfigFiles } from './cordis-config-files.ts'
import { isCordisGroupEntry, isJsExpr, loadCordisYaml } from './cordis-yaml.ts'
@@ -22,7 +24,7 @@ export interface PackageManifest {
dependencies?: Record<string, string>
devDependencies?: Record<string, string>
optionalDependencies?: Record<string, string>
dsh?: { bundle?: { patch?: string } }
dsh?: { bundle?: DshBundleManifest }
}
export interface PluginReference {
@@ -257,10 +259,11 @@ function validateAppResolution(): string[] {
for (const manifestPath of bundleManifests) {
const bundleDir = manifestPath.replace(/\/package\.json$/, '')
const manifest = readManifest(manifestPath)
const patch = manifest.dsh?.bundle?.patch
if (typeof patch !== 'string') continue
const patchFile = relative(root, resolve(root, bundleDir, patch)).replaceAll('\\', '/')
const references = pluginReferences.filter(reference => reference.file === patchFile)
const bundle = manifest.dsh?.bundle
if (bundle === undefined) continue
const patchFiles = new Set(bundlePatchPaths(resolve(root, bundleDir), bundle)
.map(file => relative(root, file).replaceAll('\\', '/')))
const references = pluginReferences.filter(reference => patchFiles.has(reference.file))
violations.push(...bundlePluginDependencyErrors(manifestPath, manifest, references))
}
return violations
@@ -362,7 +365,7 @@ function packageTestManifestPath(file: string): string | undefined {
*/
export function bundleManifestPaths(repoRoot: string = root): string[] {
return globSync('packages/*/*/package.json', { cwd: repoRoot })
.filter(path => typeof readManifest(path, repoRoot).dsh?.bundle?.patch === 'string')
.filter(path => readManifest(path, repoRoot).dsh?.bundle?.patch !== undefined)
.map(path => path.replaceAll('\\', '/'))
.sort()
}
+7 -6
View File
@@ -12,7 +12,8 @@ import ts from 'typescript'
import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
import { loadOverlayPatches } from '../packages/boot/app-boot/src/index.ts'
import { composeEntries } from '../packages/boot/app-boot/src/profile.ts'
import { bundlePatchPaths, composeEntries } from '../packages/boot/app-boot/src/profile.ts'
import type { DshBundleManifest } from '../packages/util/package-manifest/src/types.ts'
import { isCordisGroupEntry, loadCordisYaml } from './cordis-yaml.ts'
import {
collectRuntimeLocalSourceSpecifiers,
@@ -32,7 +33,7 @@ interface Manifest {
optionalDependencies?: Record<string, string>
peerDependencies?: Record<string, string>
devDependencies?: Record<string, string>
dsh?: { bundle?: { patch?: string }; configTrees?: Array<{ path: string }> }
dsh?: { bundle?: DshBundleManifest; configTrees?: Array<{ path: string }> }
}
interface Package {
@@ -238,9 +239,9 @@ export function verifyDefaultProductIsolation(root: string): ProductIsolationRes
}
const webLayers = selection.webBundles.flatMap((name) => {
const pkg = packages.get(name)
const patch = pkg?.manifest.dsh?.bundle?.patch
if (pkg === undefined || patch === undefined) return []
return [loadOverlayPatches('verify-default-product-isolation', resolve(pkg.directory, patch))]
const bundle = pkg?.manifest.dsh?.bundle
if (pkg === undefined || bundle === undefined) return []
return [bundlePatchPaths(pkg.directory, bundle).flatMap(file => loadOverlayPatches('verify-default-product-isolation', file))]
})
if (webLayers.length !== selection.webBundles.length) {
failures.push(`${PROFILE_SOURCE}: default Web bundle layers are incomplete`)
@@ -285,7 +286,7 @@ export function verifyDefaultProductIsolation(root: string): ProductIsolationRes
dependency(name, range, pkg, `${manifest.name} ${section}`)
}
}
if (manifest.dsh?.bundle?.patch !== undefined) scanConfig(resolve(pkg.directory, manifest.dsh.bundle.patch))
for (const file of manifest.dsh?.bundle === undefined ? [] : bundlePatchPaths(pkg.directory, manifest.dsh.bundle)) scanConfig(file)
for (const tree of manifest.dsh?.configTrees ?? []) {
const treePath = resolve(pkg.directory, tree.path)
const files = existsSync(treePath) && statSync(treePath).isDirectory()