Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`, `verify-translation-pairing --write` for the touched bilingual pairs, `gen-doc-graphs`, and one typert snapshot whose ids embed character offsets. `pnpm run rescope-vendor --check` verifies the result. Renames nine vendored packages (cordis, cosmokit, schemastery and the six @cordisjs plugins) and every reference that resolves them: manifest names and dependency keys, module specifiers including declare-module merges, cordis.yml plugin names, tsconfig paths, every Markdown fence, and `docs/` prose. Directory names, upstream versions, and dependency ranges are unchanged, so vendor/README.md still reads as an upstream snapshot; its manifest table gains an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed at each fork's origin. The tutorial tier follows the rename end to end: its yaml fences named plugins the Loader can no longer resolve, its `ts ignore-check` fences disagreed with the compiled fences beside them, and its prose quoted both. The contracts that told readers to keep upstream names — the root convention and the vendoring cookbook's tree comment and manifest invariant — now say to rescope instead. Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle purity gate now names the vendored libraries a browser bundle inlines, and the files where a bare `cordis` is an agent-preset id keep that product data.
153 lines
5.5 KiB
TypeScript
153 lines
5.5 KiB
TypeScript
/** Node-half composition diagnostics for package metadata and built client bundles. */
|
|
|
|
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
import { tmpdir } from 'node:os'
|
|
import { dirname, join } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import type { HttpServerService, WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
import { ClientModuleHostService } from '../src/index.ts'
|
|
|
|
let root: string | undefined
|
|
|
|
afterEach(() => {
|
|
if (root !== undefined) rmSync(root, { recursive: true, force: true })
|
|
root = undefined
|
|
})
|
|
|
|
/** Create a resolvable package whose client export points at the returned path. */
|
|
function writePackage(
|
|
packageName: string,
|
|
metadata: Record<string, unknown> = { dsh: { client: { platform: 'web' } } },
|
|
): string {
|
|
root ??= realpathSync(mkdtempSync(join(tmpdir(), 'dsh-client-modules-')))
|
|
const pkgRoot = join(root, 'node_modules', ...packageName.split('/'))
|
|
const clientPath = join(pkgRoot, 'lib', 'client.js')
|
|
mkdirSync(pkgRoot, { recursive: true })
|
|
writeFileSync(join(pkgRoot, 'package.json'), JSON.stringify({
|
|
name: packageName,
|
|
exports: {
|
|
'./client': './lib/client.js',
|
|
'./package.json': './package.json',
|
|
},
|
|
...metadata,
|
|
}))
|
|
return clientPath
|
|
}
|
|
|
|
/** Construct the node-half service and capture its plugin-bundle route. */
|
|
function constructWithRoute(packageNames: string[]): { service: ClientModuleHostService; route: WebRoute } {
|
|
const ctx = new Context()
|
|
ctx.baseUrl = pathToFileURL(root!).href + '/'
|
|
ctx.provide('loader', {
|
|
*entries() {
|
|
for (const packageName of packageNames) {
|
|
yield { options: { name: packageName }, fiber: {}, disabled: false }
|
|
}
|
|
},
|
|
})
|
|
let route: WebRoute | undefined
|
|
const httpServer: Pick<HttpServerService, 'port' | 'register' | 'tapIndex'> = {
|
|
port: 0,
|
|
register: (candidate) => {
|
|
if (candidate.path === '/plugins') route = candidate
|
|
return () => {}
|
|
},
|
|
tapIndex: () => () => {},
|
|
}
|
|
ctx.provide('httpServer', httpServer as HttpServerService)
|
|
const service = new ClientModuleHostService(ctx)
|
|
if (route === undefined) throw new Error('client bundle route was not registered')
|
|
return { service, route }
|
|
}
|
|
|
|
/** Construct the node-half service over the enabled fixture entries. */
|
|
function construct(packageNames: string[]): ClientModuleHostService {
|
|
return constructWithRoute(packageNames).service
|
|
}
|
|
|
|
describe('client bundle activation', () => {
|
|
it('allows sibling dsh roles', () => {
|
|
const currentName = '@fixture/current-client-field'
|
|
const clientPath = writePackage(currentName, {
|
|
dsh: {
|
|
bundle: { patch: './cordis.patch.yml' },
|
|
client: { platform: 'web' },
|
|
profile: { bundles: [] },
|
|
},
|
|
})
|
|
mkdirSync(dirname(clientPath), { recursive: true })
|
|
writeFileSync(clientPath, 'module.exports = {}\n')
|
|
expect(construct([currentName]).graph().entries.map(entry => entry.id)).toEqual([currentName])
|
|
})
|
|
|
|
it('groups missing bundles under one source-build instruction with a package/path list', () => {
|
|
const firstName = '@fixture/missing-first'
|
|
const secondName = '@fixture/missing-second'
|
|
const firstPath = writePackage(firstName)
|
|
const secondPath = writePackage(secondName)
|
|
expect(() => construct([firstName, secondName])).toThrow([
|
|
'client-modules: 2 client packages failed to compose:',
|
|
' client bundles not found; run `pnpm run build` before launch:',
|
|
` - package: ${firstName}`,
|
|
` path: ${firstPath}`,
|
|
` - package: ${secondName}`,
|
|
` path: ${secondPath}`,
|
|
].join('\n'))
|
|
})
|
|
|
|
it('does not report other bundle read failures as missing builds', () => {
|
|
const packageName = '@fixture/unreadable-client'
|
|
const clientPath = writePackage(packageName)
|
|
mkdirSync(clientPath, { recursive: true })
|
|
let thrown: unknown
|
|
try {
|
|
construct([packageName])
|
|
} catch (error) {
|
|
thrown = error
|
|
}
|
|
expect(String(thrown)).toContain('client-modules: 1 client package failed to compose:')
|
|
expect(String(thrown)).toContain(' other failures:')
|
|
expect(String(thrown)).toContain('EISDIR')
|
|
expect(String(thrown)).not.toContain('pnpm run build')
|
|
})
|
|
|
|
it('serves the source map beside a registered client bundle', async () => {
|
|
const packageName = '@fixture/source-map'
|
|
const clientPath = writePackage(packageName)
|
|
mkdirSync(dirname(clientPath), { recursive: true })
|
|
writeFileSync(clientPath, 'module.exports = {}\n')
|
|
const map = '{"version":3,"sources":["src/client/index.tsx"]}\n'
|
|
writeFileSync(`${clientPath}.map`, map)
|
|
const { route } = constructWithRoute([packageName])
|
|
let status = 0
|
|
let headers: Record<string, string> | undefined
|
|
let body = ''
|
|
const response = {
|
|
writeHead(nextStatus: number, nextHeaders?: Record<string, string>) {
|
|
status = nextStatus
|
|
headers = nextHeaders
|
|
return response
|
|
},
|
|
end(chunk?: Uint8Array) {
|
|
body = chunk === undefined ? '' : Buffer.from(chunk).toString('utf8')
|
|
return response
|
|
},
|
|
} as unknown as ServerResponse
|
|
|
|
await route.handler({
|
|
method: 'GET',
|
|
url: `/plugins/${packageName}/client.js.map`,
|
|
} as IncomingMessage, response)
|
|
|
|
expect(status).toBe(200)
|
|
expect(headers).toEqual({
|
|
'content-type': 'application/json; charset=utf-8',
|
|
'cache-control': 'no-cache',
|
|
})
|
|
expect(body).toBe(map)
|
|
})
|
|
})
|