Files
dify/packages/contracts/scripts/generate-compatibility-sources.ts
Stephen Zhouandautofix-ci[bot] 04ba877c1a feat(web): export OpenAPI mapper types and compatible contract subpaths (#44915)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-10-28 14:39:37 +00:00

106 lines
4.0 KiB
TypeScript

import { existsSync, globSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { basename, dirname, relative, resolve } from 'node:path'
import { format } from 'vite-plus/fmt'
const declarationHeader = '// Generated by @dify/contracts gen-compatibility-sources.\n'
function writeIfChanged(file: string, content: string) {
mkdirSync(dirname(file), { recursive: true })
const existing = existsSync(file) ? readFileSync(file, 'utf8') : undefined
// No-op generation must not restart the language server.
if (existing !== content) writeFileSync(file, content)
}
async function writeDescriptor(file: string, value: unknown) {
let content = `${JSON.stringify(value, null, 2)}\n`
if (file.endsWith('.json')) {
const formatted = await format(file, content)
if (formatted.errors.length) throw new Error(`Failed to format ${file}`)
content = formatted.code
}
writeIfChanged(file, content)
}
/** Thin mapper subpaths share one exporter configuration per backend source. */
export async function generateCompatibilitySources(
root = resolve(import.meta.dirname, '..'),
output = resolve(root, '../openapi-content-mapper/contracts'),
) {
const repo = resolve(root, '../..')
const sharedSources = {
community: resolve(output, 'community.source.json'),
enterprise: resolve(output, 'enterprise.source.json'),
}
for (const [name, file] of Object.entries(sharedSources)) {
const enterprise = name === 'enterprise'
await writeDescriptor(file, {
command: ['node', 'packages/contracts/scripts/export-compatibility-types.ts'],
cwd: relative(output, repo),
inputs: [
'packages/contracts/api-documents.ts',
'packages/contracts/typescript-generation.ts',
'packages/contracts/scripts/export-compatibility-types.ts',
'packages/contracts/package.json',
'pnpm-lock.yaml',
...(enterprise
? [
'packages/contracts/generated/enterprise*/types.gen.ts',
'packages/contracts/generated/enterprise*/openapi.gen.json',
]
: [
'packages/contracts/generated/api/**/types.gen.ts',
'api/**/*.py',
'dify-agent/src/**/*.py',
'dify-agent/pyproject.toml',
'api/pyproject.toml',
'api/uv.lock',
]),
],
...(enterprise
? {}
: {
env: ['UV_PROJECT_ENVIRONMENT'],
exclude: ['api/tests/**'],
optionalInputs: ['.env'],
}),
timeoutMs: 120000,
})
}
const modules = globSync('generated/**/types.gen.ts', { cwd: root }).sort()
const expected = new Set<string>()
for (const entry of modules) {
const module = entry.slice('generated/'.length, -'/types.gen.ts'.length)
const subpath = module.replace(/^api\//, '')
const file = resolve(output, `${subpath}.openapi-types`)
expected.add(file)
const declaration = resolve(output, `${subpath}.d.ts`)
expected.add(declaration)
// A native declaration entry avoids TS 7's missing-declaration hint for
// package exports targeting custom extensions. Types still come from the mapper.
writeIfChanged(declaration, `${declarationHeader}export type * from './${basename(file)}'\n`)
await writeDescriptor(file, {
extends: relative(
dirname(file),
sharedSources[module.startsWith('enterprise') ? 'enterprise' : 'community'],
),
args: [module],
})
}
for (const entry of globSync('**/*.openapi-types', { cwd: output })) {
const file = resolve(output, entry)
if (!expected.has(file)) {
const descriptor = JSON.parse(readFileSync(file, 'utf8'))
if (Object.values(sharedSources).includes(resolve(dirname(file), descriptor.extends ?? '')))
rmSync(file)
}
}
for (const entry of globSync('**/*.d.ts', { cwd: output })) {
const file = resolve(output, entry)
if (!expected.has(file) && readFileSync(file, 'utf8').startsWith(declarationHeader))
rmSync(file)
}
return modules
}
if (import.meta.main) await generateCompatibilitySources()