refactor(cordis): generate catalogs from Typert models
This commit is contained in:
@@ -1207,7 +1207,7 @@ class FaceAnalyzer {
|
||||
const owner = this.registrationForFile(declaration.getSourceFile().fileName)
|
||||
if (owner !== undefined) {
|
||||
if (owner.name !== from.name) {
|
||||
if (module === undefined || module.package !== owner.name) {
|
||||
if (module === undefined) {
|
||||
this.fail(site, `reference to ${symbol.name} crosses a package without an explicit package import`)
|
||||
}
|
||||
const exportName = authoredExportName(site, moduleSpecifier as string)
|
||||
|
||||
343
packages/typert/generator/tests/cordis-catalog-contract.spec.ts
Normal file
343
packages/typert/generator/tests/cordis-catalog-contract.spec.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* Model-extraction and negative-path contracts for the Cordis catalog generator
|
||||
* (`scripts/gen-cordis-catalog.ts`).
|
||||
*/
|
||||
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
collectEvents as collectEventsWithPolicy,
|
||||
collectServices as collectServicesWithPolicy,
|
||||
renderEvents as renderEventsWithPolicy,
|
||||
renderServices as renderServicesWithPolicy,
|
||||
} from '../src/cordis-catalog.ts'
|
||||
import type {
|
||||
CordisCatalogPolicy,
|
||||
EventEntry,
|
||||
ServiceEntry,
|
||||
} from '../src/cordis-catalog.ts'
|
||||
|
||||
const TEST_POLICY: CordisCatalogPolicy = {
|
||||
linkedTypePages: { SessionEvent: 'core.md' },
|
||||
foundationTypeNames: new Set(['AbortSignal', 'Promise', 'Readonly']),
|
||||
typeLinkExemptions: { PresetSpec: 'fixture deployment metadata' },
|
||||
inheritedEvents: [],
|
||||
inheritedServices: [],
|
||||
}
|
||||
|
||||
function collectEvents(root: string): EventEntry[] {
|
||||
return collectEventsWithPolicy(root, TEST_POLICY)
|
||||
}
|
||||
|
||||
function collectServices(root: string): ServiceEntry[] {
|
||||
return collectServicesWithPolicy(root, TEST_POLICY)
|
||||
}
|
||||
|
||||
function renderEvents(events: EventEntry[]): string {
|
||||
return renderEventsWithPolicy(events, TEST_POLICY)
|
||||
}
|
||||
|
||||
function renderServices(services: ServiceEntry[]): string {
|
||||
return renderServicesWithPolicy(services, TEST_POLICY)
|
||||
}
|
||||
|
||||
const TYPE_FIXTURES = [
|
||||
'export interface FixtureEntry {}',
|
||||
'interface SessionEvent {}',
|
||||
'interface PresetSpec {}',
|
||||
'interface MissingOne {}',
|
||||
'type missingTwo = string',
|
||||
'interface MissingServiceType {}',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
/** Materialize one independently compilable package and its host aggregate. */
|
||||
function writeProject(root: string, source: string): void {
|
||||
const packageRoot = join(root, 'packages', 'group', 'fix')
|
||||
const sourceRoot = join(packageRoot, 'src')
|
||||
mkdirSync(sourceRoot, { recursive: true })
|
||||
writeFileSync(join(root, 'tsconfig.host.json'), JSON.stringify({
|
||||
files: [],
|
||||
references: [{ path: './packages/group/fix' }],
|
||||
}))
|
||||
writeFileSync(join(packageRoot, 'package.json'), JSON.stringify({
|
||||
name: '@fixture/fix',
|
||||
private: true,
|
||||
type: 'module',
|
||||
exports: {
|
||||
'.': {
|
||||
types: './lib/types/index.d.ts',
|
||||
default: './lib/index.js',
|
||||
},
|
||||
},
|
||||
}))
|
||||
writeFileSync(join(packageRoot, 'tsconfig.json'), JSON.stringify({
|
||||
compilerOptions: {
|
||||
composite: true,
|
||||
module: 'ESNext',
|
||||
moduleResolution: 'Bundler',
|
||||
rootDir: 'src',
|
||||
target: 'ES2022',
|
||||
},
|
||||
include: ['src'],
|
||||
}))
|
||||
writeFileSync(join(sourceRoot, 'index.ts'), `${TYPE_FIXTURES}${source}`)
|
||||
}
|
||||
|
||||
/** Write a fixture package exposing one `interface Events` block and return the
|
||||
* scan root to hand `collectEvents`. */
|
||||
function fixtureRoot(eventsBlock: string): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'cordis-catalog-'))
|
||||
writeProject(
|
||||
root,
|
||||
`declare module 'cordis' {\n interface Events {\n${eventsBlock}\n }\n}\n`,
|
||||
)
|
||||
return root
|
||||
}
|
||||
|
||||
/** Write a fixture package exposing one `interface Context` entry (`ctx.fix` →
|
||||
* `FixService`) plus the class source, and return the scan root to hand
|
||||
* `collectServices`. */
|
||||
function serviceFixtureRoot(classSource: string): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'cordis-catalog-'))
|
||||
writeProject(
|
||||
root,
|
||||
`declare module 'cordis' {\n interface Context {\n fix: FixService\n }\n}\n\n${classSource}\n`,
|
||||
)
|
||||
return root
|
||||
}
|
||||
|
||||
const roots: string[] = []
|
||||
const make = (block: string): string => {
|
||||
const r = fixtureRoot(block)
|
||||
roots.push(r)
|
||||
return r
|
||||
}
|
||||
const makeService = (classSource: string): string => {
|
||||
const r = serviceFixtureRoot(classSource)
|
||||
roots.push(r)
|
||||
return r
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('gen-cordis-catalog collectEvents', () => {
|
||||
it('extracts a well-formed event with its @mode and JSDoc', () => {
|
||||
const events = collectEvents(make(
|
||||
' /**\n * A thing happened.\n * @param id - which thing.\n * @mode emit\n */\n \'fix/happened\'(id: string): void',
|
||||
))
|
||||
expect(events).toHaveLength(1)
|
||||
expect(events[0]).toMatchObject({ name: 'fix/happened', scope: 'fix', mode: 'emit', doc: 'A thing happened.' })
|
||||
expect(events[0]?.jsDoc).toBe('/**\n * A thing happened.\n * @param id - which thing.\n * @mode emit\n */')
|
||||
expect(renderEvents(events)).toContain("```ts cordis-catalog\n/**\n * A thing happened.\n * @param id - which thing.\n * @mode emit\n */\n'fix/happened'(id: string): void\n```")
|
||||
})
|
||||
|
||||
it('classifies a trailing-next signature as a waterfall', () => {
|
||||
const events = collectEvents(make(
|
||||
' /**\n * Intercept it.\n * @param x - the value under interception.\n * @mode waterfall\n */\n \'fix/intercept\'(x: number, next: () => Promise<number>): Promise<number>',
|
||||
))
|
||||
expect(events[0]?.mode).toBe('waterfall')
|
||||
})
|
||||
|
||||
it('accepts a parallel (awaited, no next) event by trusting the tag', () => {
|
||||
const events = collectEvents(make(
|
||||
' /**\n * Flush.\n * @mode parallel\n */\n \'fix/flush\'(): Promise<void> | void',
|
||||
))
|
||||
expect(events[0]?.mode).toBe('parallel')
|
||||
})
|
||||
|
||||
it('accepts linked, foundation, generic-parameter, and explicitly exempt signature types', () => {
|
||||
const events = collectEvents(make(
|
||||
' /**\n * Carry linked and foundation types.\n * @param value - the linked value.\n * @param preset - deployment metadata outside the core catalog.\n * @param signal - cancellation.\n * @mode parallel\n */\n \'fix/typed\'<T extends SessionEvent>(value: Readonly<T>, preset: PresetSpec, signal: AbortSignal): Promise<T>',
|
||||
))
|
||||
expect(events).toHaveLength(1)
|
||||
expect(renderEvents(events)).toContain('Types: [SessionEvent](../core-data-structures/core.md)')
|
||||
expect(renderEvents(events)).not.toContain('[PresetSpec]')
|
||||
})
|
||||
|
||||
it('aggregates every unclassified signature type with its source and remediation', () => {
|
||||
const expected = new RegExp([
|
||||
'2 signature type-link coverage violation\\(s\\)',
|
||||
'fix/one',
|
||||
'packages/group/fix/src/index.ts',
|
||||
'MissingOne',
|
||||
'fix/two',
|
||||
'packages/group/fix/src/index.ts',
|
||||
'missingTwo',
|
||||
'Add it to linkedTypePages',
|
||||
'foundationTypeNames',
|
||||
'typeLinkExemptions',
|
||||
].join('[\\s\\S]*'))
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * First.\n * @param value - first value.\n * @mode emit\n */\n \'fix/one\'(value: MissingOne): void\n /**\n * Second.\n * @param value - second value.\n * @mode emit\n */\n \'fix/two\'(value: missingTwo): void',
|
||||
))).toThrow(expected)
|
||||
})
|
||||
|
||||
it('hard-errors when an event is missing its @mode tag', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /** No mode here. */\n \'fix/untagged\'(): void',
|
||||
))).toThrow(/missing an @mode tag/)
|
||||
})
|
||||
|
||||
it('hard-errors when @mode contradicts a trailing-next (waterfall) shape', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * Mislabeled.\n * @param x - the value.\n * @mode emit\n */\n \'fix/wrong\'(x: number, next: () => Promise<number>): Promise<number>',
|
||||
))).toThrow(/trailing 'next' parameter .* tagged '@mode emit'/)
|
||||
})
|
||||
|
||||
it('hard-errors when @mode waterfall has no trailing next to delegate to', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * Not actually a waterfall.\n * @param id - which thing.\n * @mode waterfall\n */\n \'fix/nonext\'(id: string): void',
|
||||
))).toThrow(/tagged '@mode waterfall' but has no trailing 'next'/)
|
||||
})
|
||||
|
||||
it('hard-errors on an undocumented payload parameter', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * A thing happened.\n * @mode emit\n */\n \'fix/happened\'(id: string): void',
|
||||
))).toThrow(/is missing @param id/)
|
||||
})
|
||||
|
||||
it('hard-errors on a stale @param naming no real parameter', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * A thing happened.\n * @param id - which thing.\n * @param ghost - not a parameter.\n * @mode emit\n */\n \'fix/happened\'(id: string): void',
|
||||
))).toThrow(/@param ghost does not match any parameter/)
|
||||
})
|
||||
|
||||
it('hard-errors on an @param with an empty description', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * A thing happened.\n * @param id\n * @mode emit\n */\n \'fix/happened\'(id: string): void',
|
||||
))).toThrow(/@param id has an empty description/)
|
||||
})
|
||||
|
||||
it('hard-errors on an event whose JSDoc has no description prose', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * @param id - which thing.\n * @mode emit\n */\n \'fix/happened\'(id: string): void',
|
||||
))).toThrow(/no description prose/)
|
||||
})
|
||||
|
||||
it('exempts the `this` receiver and the trailing waterfall `next` from @param', () => {
|
||||
const events = collectEvents(make(
|
||||
' /**\n * Scoped interception.\n * @param x - the value under interception.\n * @mode waterfall\n */\n \'fix/scoped\'(this: object, x: number, next: () => Promise<number>): Promise<number>',
|
||||
))
|
||||
expect(events).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('hard-errors on a binding-pattern parameter @param cannot name', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /**\n * A thing happened.\n * @mode emit\n */\n \'fix/destructured\'({ id }: { id: string }): void',
|
||||
))).toThrow(/is a binding pattern/)
|
||||
})
|
||||
|
||||
it('aggregates every violation into one error instead of failing fast', () => {
|
||||
expect(() => collectEvents(make(
|
||||
' /** First. */\n \'fix/one\'(): void\n /** Second. */\n \'fix/two\'(): void',
|
||||
))).toThrow(/2 JSDoc completeness violation\(s\)[\s\S]*fix\/one[\s\S]*fix\/two/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('gen-cordis-catalog collectServices', () => {
|
||||
const WELL_FORMED = `/** Fixture service. */
|
||||
export class FixService {
|
||||
/**
|
||||
* Do the thing.
|
||||
* @param id - which thing to do.
|
||||
* @returns the outcome of doing it.
|
||||
*/
|
||||
run(id: string): string { return id }
|
||||
|
||||
/** Fire and forget (void needs no @returns). */
|
||||
poke(): void {}
|
||||
|
||||
/** Flush (Promise<void> needs no @returns either). */
|
||||
flush(): Promise<void> { return Promise.resolve() }
|
||||
}`
|
||||
|
||||
it('extracts a well-formed service with its methods and class JSDoc', () => {
|
||||
const services = collectServices(makeService(WELL_FORMED))
|
||||
expect(services).toHaveLength(1)
|
||||
expect(services[0]).toMatchObject({ key: 'fix', type: 'FixService', abstract: false, doc: 'Fixture service.' })
|
||||
expect(services[0]?.methods).toHaveLength(3)
|
||||
expect(services[0]?.methods[0]).toEqual({
|
||||
signature: 'run(id: string): string',
|
||||
jsDoc: '/**\n * Do the thing.\n * @param id - which thing to do.\n * @returns the outcome of doing it.\n */',
|
||||
})
|
||||
expect(renderServices(services)).toContain('```ts cordis-catalog\n/**\n * Do the thing.\n * @param id - which thing to do.\n * @returns the outcome of doing it.\n */\nrun(id: string): string\n\n/** Fire and forget (void needs no @returns). */\npoke(): void')
|
||||
})
|
||||
|
||||
it('hard-errors on an unclassified service-method signature type', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Use an unknown value.\n * @param value - the value.\n */\n run(value: MissingServiceType): void {}\n}',
|
||||
))).toThrow(/service method ctx\.fix\.run .* references unclassified type 'MissingServiceType'/)
|
||||
})
|
||||
|
||||
it('hard-errors on a public method with no JSDoc at all', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n run(id: string): string { return id }\n}',
|
||||
))).toThrow(/ctx\.fix\.run .* has no JSDoc/)
|
||||
})
|
||||
|
||||
it('hard-errors on an undocumented method parameter', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Do the thing.\n * @returns the outcome.\n */\n run(id: string): string { return id }\n}',
|
||||
))).toThrow(/ctx\.fix\.run .* is missing @param id/)
|
||||
})
|
||||
|
||||
it('hard-errors on a missing @returns for a non-void return type', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Do the thing.\n * @param id - which thing.\n */\n run(id: string): string { return id }\n}',
|
||||
))).toThrow(/is missing @returns \(return type: string\)/)
|
||||
})
|
||||
|
||||
it('hard-errors on an unannotated (inferred) return type', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Do the thing.\n * @param id - which thing.\n */\n run(id: string) { return id }\n}',
|
||||
))).toThrow(/missing an explicit type annotation/)
|
||||
})
|
||||
|
||||
it('hard-errors on a service class with no JSDoc', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'export class FixService {\n /** Fire and forget. */\n poke(): void {}\n}',
|
||||
))).toThrow(/class FixService has no JSDoc/)
|
||||
})
|
||||
|
||||
it('hard-errors on a stale method @param', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Fire and forget.\n * @param ghost - not a parameter.\n */\n poke(): void {}\n}',
|
||||
))).toThrow(/@param ghost does not match any parameter/)
|
||||
})
|
||||
|
||||
it('hard-errors on a method whose JSDoc is tags with no description prose', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * @param id - which thing.\n * @returns the outcome.\n */\n run(id: string): string { return id }\n}',
|
||||
))).toThrow(/no description prose above its block tags/)
|
||||
})
|
||||
|
||||
it('hard-errors on a method @param with an empty description', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Fire and forget.\n * @param id\n */\n poke(id: string): void {}\n}',
|
||||
))).toThrow(/@param id has an empty description/)
|
||||
})
|
||||
|
||||
it('hard-errors on an @returns with an empty description', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Do the thing.\n * @param id - which thing.\n * @returns\n */\n run(id: string): string { return id }\n}',
|
||||
))).toThrow(/@returns has an empty description/)
|
||||
})
|
||||
|
||||
it('hard-errors on a binding-pattern method parameter @param cannot name', () => {
|
||||
expect(() => collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n /**\n * Do the thing.\n */\n run({ id }: { id: string }): void {}\n}',
|
||||
))).toThrow(/is a binding pattern/)
|
||||
})
|
||||
|
||||
it('ignores private/protected/static members (not the ctx.<key> surface)', () => {
|
||||
const services = collectServices(makeService(
|
||||
'/** Fixture service. */\nexport class FixService {\n private hidden(id: string): string { return id }\n protected hook(): void {}\n static helper(): void {}\n}',
|
||||
))
|
||||
expect(services[0]?.methods).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
24
packages/typert/generator/tests/cordis-catalog.spec.ts
Normal file
24
packages/typert/generator/tests/cordis-catalog.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
projectCordisCatalog,
|
||||
renderEvents,
|
||||
renderServices,
|
||||
} from '../src/cordis-catalog.ts'
|
||||
import { CORDIS_CATALOG_POLICY } from '../../../../scripts/gen-cordis-catalog.ts'
|
||||
|
||||
const workspaceRoot = resolve(import.meta.dirname, '../../../..')
|
||||
|
||||
describe('Typert-backed Cordis catalog', () => {
|
||||
it('reproduces every committed catalog artifact byte for byte', { timeout: 480_000 }, () => {
|
||||
const { projector, model } = projectCordisCatalog(workspaceRoot, CORDIS_CATALOG_POLICY)
|
||||
const expected = (path: string): string => readFileSync(join(workspaceRoot, path), 'utf8')
|
||||
|
||||
expect(renderEvents([...model.events], CORDIS_CATALOG_POLICY)).toBe(expected('docs/cordis-catalog/events.md'))
|
||||
expect(renderServices([...model.services], CORDIS_CATALOG_POLICY)).toBe(expected('docs/cordis-catalog/services.md'))
|
||||
expect(projector.renderRuntimeApi(model)).toBe(
|
||||
expected('packages/cordis/tool-cordis/src/api-catalog.ts'),
|
||||
)
|
||||
})
|
||||
})
|
||||
68
packages/typert/generator/tests/tools-catalog.spec.ts
Normal file
68
packages/typert/generator/tests/tools-catalog.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
|
||||
import type { TypertContribution } from '@deepseek-ai/dsh-typert-registry/types'
|
||||
import { EVENT_API, SERVICE_API, TYPE_API } from '@deepseek-ai/dsh-tool-cordis/src/api-catalog.ts'
|
||||
import { WorkspaceAnalyzer } from '../src/analyzer.ts'
|
||||
import { FaceModelEmitter } from '../src/emitter.ts'
|
||||
|
||||
const workspaceRoot = resolve(import.meta.dirname, '../../../..')
|
||||
const temporaryRoots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of temporaryRoots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('model-driven dsh-tools generation', () => {
|
||||
it('round-trips the complete service and event structure through the runtime registry', { timeout: 30_000 }, async () => {
|
||||
const workspace = new WorkspaceAnalyzer({
|
||||
root: workspaceRoot,
|
||||
faces: ['host'],
|
||||
packages: ['@deepseek-ai/dsh-tools'],
|
||||
}).analyze()
|
||||
const host = workspace.faces.find(candidate => candidate.face === 'host')
|
||||
if (host === undefined) throw new Error('dsh-tools has no analyzed host face')
|
||||
const artifact = new FaceModelEmitter(host).emit('@deepseek-ai/dsh-tools')
|
||||
|
||||
const root = mkdtempSync(join(import.meta.dirname, '.generated-tools-'))
|
||||
temporaryRoots.push(root)
|
||||
const modulePath = join(root, 'host.mjs')
|
||||
writeFileSync(modulePath, artifact.js)
|
||||
const generated = await import(`${pathToFileURL(modulePath).href}?test=${Date.now()}`) as {
|
||||
TYPERT: TypertContribution
|
||||
}
|
||||
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(TypertRegistry)
|
||||
const dispose = ctx.typert.register(generated.TYPERT)
|
||||
const record = ctx.typert.getPackage('@deepseek-ai/dsh-tools', 'host')
|
||||
const service = record?.model.services.find(candidate => candidate.key === 'tools')
|
||||
expect(service).toBeDefined()
|
||||
expect({
|
||||
key: service?.key,
|
||||
summary: service?.summary,
|
||||
methods: service?.members
|
||||
.filter(member => member.kind === 'method' && !member.name.startsWith('['))
|
||||
.map(member => ({
|
||||
signature: member.signature,
|
||||
jsDoc: member.jsDoc ?? '',
|
||||
})),
|
||||
}).toEqual(SERVICE_API.find(candidate => candidate.key === 'tools'))
|
||||
expect(record?.model.events.filter(event => event.name.startsWith('tools/')).map(event => ({
|
||||
name: event.name,
|
||||
mode: event.mode,
|
||||
signature: event.signature,
|
||||
jsDoc: event.jsDoc ?? '',
|
||||
summary: event.summary,
|
||||
}))).toEqual(EVENT_API.filter(event => event.name.startsWith('tools/')))
|
||||
expect(service?.types.find(type => type.name === 'ToolDefinition')).toEqual(
|
||||
TYPE_API.find(type => type.name === 'ToolDefinition'),
|
||||
)
|
||||
|
||||
dispose()
|
||||
expect(ctx.typert.getPackage('@deepseek-ai/dsh-tools', 'host')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -489,6 +489,59 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => {
|
||||
&& node.target.symbol === payload?.id)).toBe(true)
|
||||
})
|
||||
|
||||
it('resolves explicit same-face package re-exports to their declaration owner', () => {
|
||||
const root = copyFixture('typert-same-face-reexport-')
|
||||
const packageRoot = join(root, 'packages/barrel')
|
||||
mkdirSync(join(packageRoot, 'src'), { recursive: true })
|
||||
writeFileSync(join(packageRoot, 'package.json'), JSON.stringify({
|
||||
name: '@fixture/barrel',
|
||||
private: true,
|
||||
type: 'module',
|
||||
exports: {
|
||||
'.': {
|
||||
types: './lib/types/index.d.ts',
|
||||
default: './lib/index.js',
|
||||
},
|
||||
},
|
||||
}, null, 2))
|
||||
writeFileSync(join(packageRoot, 'tsconfig.json'), JSON.stringify({
|
||||
extends: '../../tsconfig.base.json',
|
||||
compilerOptions: { rootDir: 'src', outDir: 'lib/types' },
|
||||
include: ['src'],
|
||||
references: [{ path: '../host' }],
|
||||
}, null, 2))
|
||||
writeFileSync(
|
||||
join(packageRoot, 'src/index.ts'),
|
||||
"export type { Payload } from '@fixture/host/models'\n",
|
||||
)
|
||||
const basePath = join(root, 'tsconfig.base.json')
|
||||
const base = JSON.parse(readFileSync(basePath, 'utf8')) as {
|
||||
compilerOptions: { paths: Record<string, string[]> }
|
||||
}
|
||||
base.compilerOptions.paths['@fixture/barrel'] = ['./packages/barrel/src/index.ts']
|
||||
writeFileSync(basePath, `${JSON.stringify(base, null, 2)}\n`)
|
||||
const aggregatePath = join(root, 'tsconfig.host.json')
|
||||
const aggregate = JSON.parse(readFileSync(aggregatePath, 'utf8')) as { references: { path: string }[] }
|
||||
aggregate.references.push({ path: './packages/barrel' })
|
||||
writeFileSync(aggregatePath, `${JSON.stringify(aggregate, null, 2)}\n`)
|
||||
addSameFacePackage(root, '@fixture/barrel', 'Payload')
|
||||
const consumerConfigPath = join(root, 'packages/consumer/tsconfig.json')
|
||||
const consumerConfig = JSON.parse(readFileSync(consumerConfigPath, 'utf8')) as {
|
||||
references: { path: string }[]
|
||||
}
|
||||
consumerConfig.references.push({ path: '../barrel' })
|
||||
writeFileSync(consumerConfigPath, `${JSON.stringify(consumerConfig, null, 2)}\n`)
|
||||
|
||||
const model = new WorkspaceAnalyzer({ root }).analyze()
|
||||
const host = model.faces.find(face => face.face === 'host')
|
||||
const payload = host?.graph.declarations.find(declaration => declaration.name === 'Payload')
|
||||
expect(host?.graph.nodes.some(node => node.id.includes('packages/consumer/src/index.ts')
|
||||
&& node.kind === 'reference'
|
||||
&& node.name === 'Payload'
|
||||
&& node.target.kind === 'declaration'
|
||||
&& node.target.symbol === payload?.id)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects same-face package imports outside package.json exports', () => {
|
||||
const root = copyFixture('typert-private-package-')
|
||||
writeFileSync(
|
||||
|
||||
Reference in New Issue
Block a user