feat(plugins): offer official bundles through on-demand installation
This commit is contained in:
1 parent
2b0dd1567d
commit
72aa4966cd
64 files changed
+1227
-245
No files matched your search
@@ -0,0 +1,107 @@
|
||||
/** Offline Official catalog ownership, completeness, and freshness checks. */
|
||||
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/boot/app-boot/src/official-bundles.ts'
|
||||
import { generateOfficialBundleCatalog, renderOfficialBundleCatalog, syncOfficialBundleCatalog } from './gen-official-bundle-catalog.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
const output = 'packages/boot/app-boot/src/official-bundles.generated.ts'
|
||||
|
||||
function write(root: string, path: string, content: unknown): void {
|
||||
mkdirSync(dirname(join(root, path)), { recursive: true })
|
||||
writeFileSync(join(root, path), typeof content === 'string' ? content : `${JSON.stringify(content)}\n`)
|
||||
}
|
||||
|
||||
function directory(name: string): string { return `packages/subagent/${name.slice('@deepseek-ai/dsh-'.length)}` }
|
||||
|
||||
function fixture(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-official-catalog-'))
|
||||
roots.push(root)
|
||||
write(root, 'apps/cli/package.json', { name: '@deepseek-ai/dsh' })
|
||||
for (const name of ON_DEMAND_BUNDLES) {
|
||||
const dir = directory(name)
|
||||
write(root, `${dir}/package.json`, {
|
||||
name, type: 'module', version: '1.2.3', icon: './icon.svg', dsh: { bundle: { patch: './cordis.patch.yml' } },
|
||||
exports: { '.': './must-not-run.js', './package.json': './package.json', './locale/*.json': './locale/*.json' },
|
||||
})
|
||||
write(root, `${dir}/must-not-run.js`, "throw new Error('metadata must not evaluate a provider')")
|
||||
write(root, `${dir}/icon.svg`, '<svg xmlns="http://www.w3.org/2000/svg"/>')
|
||||
write(root, `${dir}/locale/en.json`, { meta: { title: 'Native agent', description: 'Delegate a task.' } })
|
||||
write(root, `${dir}/locale/zh.json`, { meta: { title: '原生智能体', description: '委派任务。' } })
|
||||
}
|
||||
write(root, output, '')
|
||||
return root
|
||||
}
|
||||
|
||||
function updateManifest(root: string, name: string, changes: Record<string, unknown>): void {
|
||||
const path = `${directory(name)}/package.json`
|
||||
const manifest = JSON.parse(readFileSync(join(root, path), 'utf8')) as Record<string, unknown>
|
||||
write(root, path, { ...manifest, ...changes })
|
||||
}
|
||||
|
||||
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) })
|
||||
|
||||
describe('Official bundle catalog generation', () => {
|
||||
it('reads package resources without evaluating native provider entries or pinning a version', () => {
|
||||
const entries = generateOfficialBundleCatalog(fixture())
|
||||
expect(entries.map(entry => entry.packageName)).toEqual(ON_DEMAND_BUNDLES)
|
||||
expect(entries[0]?.meta).toEqual({
|
||||
title: { en: 'Native agent', zh: '原生智能体' }, description: { en: 'Delegate a task.', zh: '委派任务。' },
|
||||
icon: `data:image/svg+xml;base64,${Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"/>').toString('base64')}`,
|
||||
})
|
||||
expect(renderOfficialBundleCatalog(entries)).not.toContain('1.2.3')
|
||||
})
|
||||
|
||||
it.each(['private', 'not-bundle', 'missing', 'duplicate'])('rejects %s catalog membership', (kind) => {
|
||||
const root = fixture()
|
||||
const name = ON_DEMAND_BUNDLES[0]!
|
||||
if (kind === 'private') updateManifest(root, name, { private: true })
|
||||
if (kind === 'not-bundle') updateManifest(root, name, { dsh: {} })
|
||||
if (kind === 'missing') rmSync(join(root, directory(name)), { recursive: true })
|
||||
if (kind === 'duplicate') write(root, 'packages/subagent/duplicate/package.json', { name })
|
||||
expect(() => generateOfficialBundleCatalog(root)).toThrow(/public DSH bundle|duplicate workspace/)
|
||||
})
|
||||
|
||||
it.each([false, true])('rejects an on-demand provider in the installed dependency closure (indirect=%s)', (indirect) => {
|
||||
const root = fixture()
|
||||
const name = ON_DEMAND_BUNDLES[0]!
|
||||
write(root, 'apps/cli/package.json', { name: '@deepseek-ai/dsh', dependencies: { [indirect ? '@deepseek-ai/dsh-layer' : name]: '*' } })
|
||||
if (indirect) write(root, 'packages/bundle/layer/package.json', { name: '@deepseek-ai/dsh-layer', dependencies: { [name]: '*' } })
|
||||
expect(() => generateOfficialBundleCatalog(root)).toThrow('must not be installed by the DSH distribution')
|
||||
})
|
||||
|
||||
it('requires distinct nonempty catalog admission', () => {
|
||||
const root = fixture()
|
||||
expect(() => generateOfficialBundleCatalog(root, [])).toThrow('distinct package names')
|
||||
expect(() => generateOfficialBundleCatalog(root, [ON_DEMAND_BUNDLES[0]!, ON_DEMAND_BUNDLES[0]!])).toThrow('distinct package names')
|
||||
})
|
||||
|
||||
it.each(['icon', 'en', 'zh'])('rejects missing %s metadata', (resource) => {
|
||||
const root = fixture()
|
||||
const dir = directory(ON_DEMAND_BUNDLES[0]!)
|
||||
rmSync(join(root, dir, resource === 'icon' ? 'icon.svg' : `locale/${resource}.json`))
|
||||
expect(() => generateOfficialBundleCatalog(root)).toThrow()
|
||||
})
|
||||
|
||||
it.each(['en', 'zh'])('requires explicit %s fields instead of metadata fallback text', (language) => {
|
||||
const root = fixture()
|
||||
write(root, `${directory(ON_DEMAND_BUNDLES[0]!)}/locale/${language}.json`, { meta: { description: 'Only a description.' } })
|
||||
expect(() => generateOfficialBundleCatalog(root)).toThrow(`explicit ${language} title and description`)
|
||||
})
|
||||
|
||||
it('rejects stale output when package-owned display text changes', () => {
|
||||
const root = fixture()
|
||||
rmSync(join(root, output))
|
||||
syncOfficialBundleCatalog(root, false)
|
||||
expect(() => { syncOfficialBundleCatalog(root, true) }).not.toThrow()
|
||||
write(root, `${directory(ON_DEMAND_BUNDLES[0]!)}/locale/en.json`, { meta: { title: 'Changed', description: 'Delegate a task.' } })
|
||||
const before = readFileSync(join(root, output), 'utf8')
|
||||
expect(() => { syncOfficialBundleCatalog(root, true) }).toThrow('is stale')
|
||||
expect(readFileSync(join(root, output), 'utf8')).toBe(before)
|
||||
syncOfficialBundleCatalog(root, false)
|
||||
expect(readFileSync(join(root, output), 'utf8')).toContain('Changed')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,124 @@
|
||||
/** Embed package-owned display metadata for Official bundles whose providers install on demand. */
|
||||
|
||||
import { globSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
import type { LocalizedText } from '../packages/util/package-manifest/src/types.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../packages/boot/app-boot/src/official-bundle-packages.ts'
|
||||
import type { OfficialBundleCatalogEntry } from '../packages/boot/app-boot/src/official-bundles.ts'
|
||||
import { readPluginMeta, resolvePluginResource } from '../packages/boot/app-boot/src/package-meta.ts'
|
||||
|
||||
const OUTPUT = 'packages/boot/app-boot/src/official-bundles.generated.ts'
|
||||
|
||||
/**
|
||||
* Read complete metadata without importing or activating any provider code.
|
||||
* @param root - Repository root containing the catalog's public packages.
|
||||
* @param names - Catalog admission list; defaults to the installation-owned list.
|
||||
* @returns Offline entries in admission order, with metadata from the owning packages.
|
||||
*/
|
||||
export function generateOfficialBundleCatalog(root: string, names: readonly string[] = ON_DEMAND_BUNDLES): OfficialBundleCatalogEntry[] {
|
||||
if (names.length === 0 || new Set(names).size !== names.length) throw new Error('Official on-demand catalog must contain distinct package names')
|
||||
const manifests = new Map<string, { path: string; private: boolean; bundle: boolean; runtime: string[] }>()
|
||||
for (const path of globSync(['packages/*/*/package.json', 'apps/*/package.json', 'vendor/*/package.json'], { cwd: root }).map(path => path.replaceAll('\\', '/')).sort()) {
|
||||
const manifest = JSON.parse(readFileSync(resolve(root, path), 'utf8')) as {
|
||||
name: string
|
||||
private?: boolean
|
||||
dependencies?: Record<string, string>
|
||||
optionalDependencies?: Record<string, string>
|
||||
peerDependencies?: Record<string, string>
|
||||
dsh?: { bundle?: { patch?: unknown } }
|
||||
}
|
||||
if (manifests.has(manifest.name)) throw new Error(`duplicate workspace package ${manifest.name}`)
|
||||
const runtime = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const
|
||||
manifests.set(manifest.name, { path, private: manifest.private === true, bundle: manifest.dsh?.bundle?.patch !== undefined,
|
||||
runtime: runtime.flatMap(section => Object.keys(manifest[section] ?? {})) })
|
||||
}
|
||||
const visited = new Set<string>()
|
||||
const checkInstallation = (name: string): void => {
|
||||
if (visited.has(name)) return
|
||||
visited.add(name)
|
||||
if (names.includes(name)) throw new Error(`${name}: on-demand providers must not be installed by the DSH distribution`)
|
||||
for (const dependency of manifests.get(name)?.runtime ?? []) checkInstallation(dependency)
|
||||
}
|
||||
if (!manifests.has('@deepseek-ai/dsh')) throw new Error('Official catalog requires the DSH installation manifest')
|
||||
checkInstallation('@deepseek-ai/dsh')
|
||||
return names.map((packageName) => {
|
||||
const entry = manifests.get(packageName)
|
||||
if (entry === undefined || !entry.path.startsWith('packages/') || !packageName.startsWith('@deepseek-ai/dsh-') || entry.private || !entry.bundle) {
|
||||
throw new Error(`${packageName}: Official catalog entries must be public DSH bundle packages`)
|
||||
}
|
||||
const parentURL = pathToFileURL(resolve(root, entry.path)).href
|
||||
for (const language of ['en', 'zh']) {
|
||||
const file = resolvePluginResource(`${packageName}/locale/${language}.json`, parentURL)
|
||||
const dictionary = JSON.parse(readFileSync(file, 'utf8')) as { meta?: { title?: unknown; description?: unknown } }
|
||||
if (typeof dictionary.meta?.title !== 'string' || dictionary.meta.title.trim() === ''
|
||||
|| typeof dictionary.meta.description !== 'string' || dictionary.meta.description.trim() === '') {
|
||||
throw new Error(`${packageName}: Official metadata requires explicit ${language} title and description`)
|
||||
}
|
||||
}
|
||||
const meta = readPluginMeta(packageName, parentURL)
|
||||
if (meta?.error !== undefined) throw new Error(`${packageName}: ${meta.error}`)
|
||||
if (meta?.icon === undefined || typeof meta.title !== 'object' || typeof meta.description !== 'object') {
|
||||
throw new Error(`${packageName}: Official catalog entries require an icon and localized title and description`)
|
||||
}
|
||||
for (const value of [meta.title, meta.description]) {
|
||||
for (const language of ['en', 'zh']) {
|
||||
if (typeof value[language] !== 'string' || value[language].trim() === '') {
|
||||
throw new Error(`${packageName}: Official metadata requires ${language} text`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return { packageName, meta: { title: meta.title, description: meta.description, icon: meta.icon } }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a source-plane catalog with no package imports or pinned runtime version.
|
||||
* @param entries - Validated metadata in catalog order.
|
||||
* @returns Deterministic TypeScript source for app-boot's generated module.
|
||||
*/
|
||||
export function renderOfficialBundleCatalog(entries: readonly OfficialBundleCatalogEntry[]): string {
|
||||
return '/** Generated offline Official bundle metadata. Run pnpm gen-official-bundle-catalog; do not edit. */\n\n'
|
||||
+ "import type { OfficialBundleCatalogEntry } from './official-bundles.ts'\n\n"
|
||||
+ '/** Official on-demand entries, independent of installed packages and registry availability. */\n'
|
||||
+ 'export const OFFICIAL_ON_DEMAND_CATALOG: readonly OfficialBundleCatalogEntry[] = [\n'
|
||||
+ entries.map(entry => ' {\n'
|
||||
+ ` packageName: ${quote(entry.packageName)},\n`
|
||||
+ ' meta: {\n'
|
||||
+ ` title: ${localized(entry.meta.title)},\n`
|
||||
+ ` description: ${localized(entry.meta.description)},\n`
|
||||
+ ` icon: ${quote(entry.meta.icon)},\n`
|
||||
+ ' },\n },\n').join('')
|
||||
+ ']\n'
|
||||
}
|
||||
|
||||
const printer = ts.createPrinter()
|
||||
const sourceFile = ts.createSourceFile('catalog.ts', '', ts.ScriptTarget.Latest)
|
||||
|
||||
function quote(value: string): string {
|
||||
return printer.printNode(ts.EmitHint.Expression, ts.factory.createStringLiteral(value, true), sourceFile)
|
||||
}
|
||||
|
||||
function localized(value: LocalizedText): string {
|
||||
if (typeof value === 'string') return quote(value)
|
||||
return `{ ${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([language, text]) => `${quote(language)}: ${quote(text)}`).join(', ')} }`
|
||||
}
|
||||
|
||||
/**
|
||||
* Write or freshness-check metadata against the owning package resources.
|
||||
* @param root - Repository root containing source and metadata resources.
|
||||
* @param check - Reject drift without changing the generated file.
|
||||
*/
|
||||
export function syncOfficialBundleCatalog(root: string, check: boolean): void {
|
||||
const source = renderOfficialBundleCatalog(generateOfficialBundleCatalog(root))
|
||||
const path = resolve(root, OUTPUT)
|
||||
if (check) {
|
||||
if (readFileSync(path, 'utf8') !== source) throw new Error(`${OUTPUT} is stale; run pnpm gen-official-bundle-catalog`)
|
||||
} else writeFileSync(path, source)
|
||||
}
|
||||
|
||||
if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
syncOfficialBundleCatalog(resolve(import.meta.dirname, '..'), process.argv.includes('--check'))
|
||||
console.log('Official on-demand catalog matches package-owned metadata.')
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { officialClientBuildEnvironment, writeClientBuildRecord } from '../client-build-environment.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../../packages/boot/app-boot/src/official-bundles.ts'
|
||||
import { releaseFamily, type ReleaseMember } from './families.ts'
|
||||
import { compareVersions, nextVendorVersion, planShared, reachesPayload } from './bump.ts'
|
||||
|
||||
@@ -238,6 +239,52 @@ describe('release families', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('publishes on-demand catalog members before dsh without adding installed dependencies', () => {
|
||||
const dsh = member('apps/cli', '@deepseek-ai/dsh')
|
||||
const providers = ON_DEMAND_BUNDLES.map(name => member(`packages/subagent/${name}`, name))
|
||||
const plan = releaseFamily('dsh').publishOrder([dsh, ...providers])
|
||||
expect(plan.order.map(entry => entry.name)).toEqual([...ON_DEMAND_BUNDLES, dsh.name])
|
||||
expect(dsh.manifest).toEqual({})
|
||||
expect(plan.droppedPeerEdges).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects an advertised provider missing from the public release family', () => {
|
||||
expect(() => releaseFamily('dsh').publishOrder([member('apps/cli', '@deepseek-ai/dsh')]))
|
||||
.toThrow('Official catalog prerequisite')
|
||||
})
|
||||
|
||||
it('keeps catalog prerequisites ahead of a dsh peer declaration', () => {
|
||||
const dsh = member('apps/cli', '@deepseek-ai/dsh')
|
||||
const providers = ON_DEMAND_BUNDLES.map(name => member(`packages/subagent/${name}`, name, {
|
||||
peerDependencies: { '@deepseek-ai/dsh': 'workspace:*' },
|
||||
}))
|
||||
const plan = releaseFamily('dsh').publishOrder([dsh, ...providers])
|
||||
expect(plan.order.map(entry => entry.name)).toEqual([...ON_DEMAND_BUNDLES, dsh.name])
|
||||
expect(plan.droppedPeerEdges).toEqual(ON_DEMAND_BUNDLES.map(name => ({ consumer: name, peer: dsh.name })))
|
||||
})
|
||||
|
||||
it.each(['dependencies', 'optionalDependencies', 'peerDependencies'])(
|
||||
'requires exact packed DSH %s while preserving external ranges', (section) => {
|
||||
const pkg = member('packages/example/provider', '@deepseek-ai/dsh-provider')
|
||||
const family = releaseFamily('dsh')
|
||||
const manifest = { name: pkg.name, version: pkg.version, [section]: { '@deepseek-ai/dsh-tools': pkg.version, external: '^4.0.0' } }
|
||||
expect(() => { family.validatePackedManifest(pkg, manifest) }).not.toThrow()
|
||||
for (const range of ['workspace:*', '^0.0.1', '~0.0.1', '0.0.2', '*']) {
|
||||
expect(() => { family.validatePackedManifest(pkg, { ...manifest, [section]: { '@deepseek-ai/dsh-tools': range } }) })
|
||||
.toThrow(`packed ${section}.@deepseek-ai/dsh-tools must equal`)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('checks packed identities and preserves independent vendor versions', () => {
|
||||
const pkg = member('vendor/example', '@deepseek-ai/example')
|
||||
const family = releaseFamily('vendor')
|
||||
expect(() => { family.validatePackedManifest(pkg, { name: pkg.name, version: pkg.version, dependencies: { '@deepseek-ai/dsh-tools': '^4.0.0' } }) })
|
||||
.not.toThrow()
|
||||
expect(() => { family.validatePackedManifest(pkg, { name: '@deepseek-ai/other', version: pkg.version }) }).toThrow('packed identity')
|
||||
expect(() => { family.validatePackedManifest(pkg, { name: pkg.name, version: '0.0.2' }) }).toThrow('packed identity')
|
||||
})
|
||||
|
||||
it('reports a runtime dependency cycle instead of emitting an arbitrary order', () => {
|
||||
const dsh = releaseFamily('dsh')
|
||||
const members = [
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
readClientBuildRecord,
|
||||
} from '../client-build-environment.ts'
|
||||
import { validateTarballPayload } from '../publication-payload.ts'
|
||||
import { ON_DEMAND_BUNDLES } from '../../packages/boot/app-boot/src/official-bundle-packages.ts'
|
||||
|
||||
/**
|
||||
* Dependency sections a consumer must publish after, because npm resolves them
|
||||
@@ -248,6 +249,13 @@ export abstract class ReleaseFamily {
|
||||
return { order: ordered, droppedPeerEdges }
|
||||
}
|
||||
|
||||
/**
|
||||
* Packages that must already be published before this member can advertise them.
|
||||
* @param _member - Member being published.
|
||||
* @returns Release prerequisites that do not become installed dependencies.
|
||||
*/
|
||||
protected publicationPrerequisites(_member: ReleaseMember): readonly string[] { return [] }
|
||||
|
||||
/**
|
||||
* The family members one member declares in the given sections.
|
||||
* @param member - the dependent member.
|
||||
@@ -269,6 +277,13 @@ export abstract class ReleaseFamily {
|
||||
if (dependency !== undefined && dependency.name !== member.name) edges.push(dependency)
|
||||
}
|
||||
}
|
||||
if (sections.includes('dependencies')) {
|
||||
for (const name of this.publicationPrerequisites(member)) {
|
||||
const prerequisite = byName.get(name)
|
||||
if (prerequisite === undefined) throw new Error(`${member.name}: Official catalog prerequisite ${name} is missing from the public release family`)
|
||||
if (!edges.some(edge => edge.name === name)) edges.push(prerequisite)
|
||||
}
|
||||
}
|
||||
return edges.sort((left, right) => left.name.localeCompare(right.name))
|
||||
}
|
||||
|
||||
@@ -311,6 +326,17 @@ export abstract class ReleaseFamily {
|
||||
*/
|
||||
abstract validatePayload(member: ReleaseMember, files: readonly string[]): void
|
||||
|
||||
/**
|
||||
* Check the identity in the packed manifest before publishing its bytes.
|
||||
* @param member - Source member selected for this release.
|
||||
* @param manifest - Manifest read from its packed tarball.
|
||||
*/
|
||||
validatePackedManifest(member: ReleaseMember, manifest: Readonly<Record<string, unknown>>): void {
|
||||
if (manifest.name !== member.name || manifest.version !== member.version) {
|
||||
throw new Error(`${member.name}: packed identity must match ${member.name}@${member.version}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The executable that proves this family's artifacts install and run, or
|
||||
* `undefined` for a family that publishes no executable.
|
||||
@@ -369,6 +395,26 @@ class DshFamily extends ReleaseFamily {
|
||||
validateTarballPayload(files, member.name)
|
||||
}
|
||||
|
||||
/** Publish every advertised on-demand package before the installation that offers it. */
|
||||
protected override publicationPrerequisites(member: ReleaseMember): readonly string[] {
|
||||
return member.name === '@deepseek-ai/dsh' ? ON_DEMAND_BUNDLES : []
|
||||
}
|
||||
|
||||
/** Require the packed DSH runtime graph to stay on the shared exact release version. */
|
||||
override validatePackedManifest(member: ReleaseMember, manifest: Readonly<Record<string, unknown>>): void {
|
||||
super.validatePackedManifest(member, manifest)
|
||||
for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies']) {
|
||||
const dependencies = manifest[section]
|
||||
if (dependencies === null || typeof dependencies !== 'object' || Array.isArray(dependencies)) continue
|
||||
for (const [name, range] of Object.entries(dependencies)) {
|
||||
if (name !== '@deepseek-ai/dsh' && !name.startsWith('@deepseek-ai/dsh-')) continue
|
||||
if (range !== member.version) {
|
||||
throw new Error(`${member.name}: packed ${section}.${name} must equal ${member.version}, got ${String(range)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly installedEntry = { packageName: '@deepseek-ai/dsh', binPath: 'lib/bin.js' }
|
||||
}
|
||||
|
||||
|
||||
@@ -320,6 +320,19 @@ describe('release pack CLI', () => {
|
||||
expect(existsSync(join(root, 'packed', PUBLISH_ORDER_FILE))).toBe(false)
|
||||
})
|
||||
|
||||
it.each(['dependencies', 'optionalDependencies', 'peerDependencies'])(
|
||||
'rejects nonexact DSH %s in the produced tarball before recording publication', async (section) => {
|
||||
const root = fixture()
|
||||
packageFixture(root, 'packages/util/probe', '@deepseek-ai/dsh-probe', { [section]: { '@deepseek-ai/dsh-library': '^1.2.3' } })
|
||||
write(join(root, 'apps/web/dist/index.html'), '<main></main>')
|
||||
writeClientBuildRecord(root, officialClientBuildEnvironment(root, { DSH_CLIENT_COMMIT_HASH: commit }))
|
||||
const result = await runPack(root, ['--family', 'dsh'])
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(result.stderr).toContain(`packed ${section}.@deepseek-ai/dsh-library must equal 1.2.3`)
|
||||
expect(existsSync(join(root, 'packed', PUBLISH_ORDER_FILE))).toBe(false)
|
||||
},
|
||||
)
|
||||
|
||||
it('stops the ready pack hook and its stalled descendant before fixture cleanup', async () => {
|
||||
const root = fixture()
|
||||
packageFixture(root, 'vendor/stalled', '@deepseek-ai/stalled', { scripts: { prepack: 'node stalled.cjs' } })
|
||||
|
||||
@@ -14,7 +14,7 @@ import { parseArgs } from 'node:util'
|
||||
import { pnpmInvocation } from '../pnpm-invocation.ts'
|
||||
import { releaseFamily, tarballName, type ReleaseFamily, type ReleaseMember } from './families.ts'
|
||||
import { isEntry, runConcurrent } from './process.ts'
|
||||
import { PUBLISH_ORDER_FILE, tarballFiles } from './tarball.ts'
|
||||
import { PUBLISH_ORDER_FILE, packedManifest, tarballFiles } from './tarball.ts'
|
||||
|
||||
/** Where pack output lands when `--out` is omitted. */
|
||||
const DEFAULT_OUTPUT = 'dist/npm'
|
||||
@@ -31,6 +31,7 @@ function validatePackedMember(family: ReleaseFamily, member: ReleaseMember, dest
|
||||
const tarball = join(destination, filename)
|
||||
if (!existsSync(tarball)) throw new Error(`${member.name} produced no tarball at ${tarball}`)
|
||||
family.validatePayload(member, tarballFiles(tarball))
|
||||
family.validatePackedManifest(member, packedManifest(tarball))
|
||||
return filename
|
||||
}
|
||||
|
||||
|
||||
@@ -32,14 +32,23 @@ export function tarballFiles(tarball: string): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a packed tarball's own manifest.
|
||||
* @param tarball - absolute tarball path.
|
||||
* Read the manifest contained in a packed tarball, without consulting the checkout.
|
||||
* @param tarball - Absolute tarball path.
|
||||
* @returns The packed package.json object.
|
||||
*/
|
||||
export function packedManifest(tarball: string): Record<string, unknown> {
|
||||
const manifest: unknown = JSON.parse(capture('tar', ['-xOzf', basename(tarball), 'package/package.json'], { cwd: dirname(tarball) }))
|
||||
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) throw new Error(`${tarball} has no manifest`)
|
||||
return manifest as Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a packed tarball's own identity.
|
||||
* @param tarball - Absolute tarball path.
|
||||
* @returns The name and version the tarball declares.
|
||||
*/
|
||||
export function packedIdentity(tarball: string): PackedIdentity {
|
||||
const manifest: unknown = JSON.parse(capture('tar', ['-xOzf', basename(tarball), 'package/package.json'], { cwd: dirname(tarball) }))
|
||||
if (manifest === null || typeof manifest !== 'object') throw new Error(`${tarball} has no manifest`)
|
||||
const { name, version } = manifest as Record<string, unknown>
|
||||
const { name, version } = packedManifest(tarball)
|
||||
if (typeof name !== 'string' || typeof version !== 'string') throw new Error(`${tarball} manifest lacks name/version`)
|
||||
return { name, version }
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ describe('gate graph validation', () => {
|
||||
const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id))
|
||||
|
||||
expect(ids).toEqual([
|
||||
'rescope-vendor', 'publint', 'constraints', 'default-product-isolation', 'product-use', 'package-dependencies', 'application-entrypoints',
|
||||
'rescope-vendor', 'publint', 'constraints', 'default-product-isolation', 'product-use', 'official-bundle-catalog', 'package-dependencies', 'application-entrypoints',
|
||||
'dsh-package-licenses', 'node-next-types',
|
||||
'optional-dependency-imports', 'client-packages', 'client-ui-i18n', 'client-route-resolution', 'no-bare-dispatcher',
|
||||
'plugin-record-callers', 'no-unknown-casts',
|
||||
|
||||
@@ -346,6 +346,7 @@ function ciSharedStaticGates(): Gate[] {
|
||||
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
|
||||
pnpmScript('default-product-isolation', 'verify-default-product-isolation', { label: 'default product isolation' }),
|
||||
pnpmScript('product-use', 'verify-product-use', { label: 'product package classification' }),
|
||||
pnpmScript('official-bundle-catalog', 'verify-official-bundle-catalog', { label: 'Official bundle catalog' }),
|
||||
pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }),
|
||||
pnpmScript('constraints', 'constraints'),
|
||||
pnpmScript('package-dependencies', 'verify-package-dependencies', { label: 'package dependencies' }),
|
||||
@@ -760,6 +761,7 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
|
||||
pnpmScript('constraints', 'constraints'),
|
||||
pnpmScript('default-product-isolation', 'verify-default-product-isolation', { label: 'default product isolation' }),
|
||||
pnpmScript('product-use', 'verify-product-use', { label: 'product package classification' }),
|
||||
pnpmScript('official-bundle-catalog', 'verify-official-bundle-catalog', { label: 'Official bundle catalog' }),
|
||||
pnpmScript('package-dependencies', 'verify-package-dependencies', { label: 'package dependencies' }),
|
||||
pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }),
|
||||
pnpmScript('dsh-package-licenses', 'verify-dsh-package-licenses', { label: 'DSH package licenses' }),
|
||||
|
||||
Reference in new issue
Block a user