Files
Stephen Zhouandautofix-ci[bot] 226110d611 feat(web): set up @dify/openapi-content-mapper foundations (#44627)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-10-26 05:06:34 +00:00

287 lines
12 KiB
JavaScript

/* oxlint-disable vitest/no-import-node-test, antfu/no-import-dist -- Integration tests exercise the built package in native Node, including its module hooks. */
import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
import { mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { test } from 'node:test'
import { pathToFileURL } from 'node:url'
import { gzipSync } from 'node:zlib'
import { build } from 'vite'
import { parse, stringify } from 'yaml'
import { openapi } from '../dist/tooling/vite.js'
/* oxlint-enable vitest/no-import-node-test, antfu/no-import-dist */
const root = resolve(import.meta.dirname, '..')
const fixture = parse(readFileSync(join(root, 'examples/petstore/pets.openapi.yaml'), 'utf8'))
test('sidecar star re-exports use Vite aliases and TypeScript extension resolution', async () => {
const dir = mkdtempSync(join(root, 'test/.sidecar-resolve-'))
try {
writeFileSync(
join(dir, 'api.openapi'),
JSON.stringify({
openapi: '3.1.0',
paths: { '/pets': { get: { responses: { 200: { description: 'OK' } } } } },
}),
)
writeFileSync(join(dir, 'api.openapi.queries.ts'), "export * from './defaults.js';")
writeFileSync(join(dir, 'defaults.ts'), "export * from '@policies';")
writeFileSync(
join(dir, 'policies.ts'),
"const policy = {queryOptions: {staleTime: 42}}; export {policy as 'GET /pets'};",
)
writeFileSync(join(dir, 'api.openapi.errors.ts'), "export * from './errors.js';")
writeFileSync(
join(dir, 'errors.ts'),
"const errors = {CONFLICT: {status: 409}}; export {errors as 'GET /pets'};",
)
writeFileSync(
join(dir, 'entry.ts'),
`
import {createClient} from '@dify/openapi-content-mapper';
import {api, query} from './api.openapi';
export const staleTime = query.pets.get.queryOptions().staleTime;
export async function fail() {
const client = createClient({baseUrl: 'https://example.test', fetch: async () => Response.json({code: 'CONFLICT'}, {status: 409})});
return api.pets.get(undefined, {client}).catch(error => ({code: error.code, defined: error.defined}));
}`,
)
await build({
configFile: false,
root: dir,
logLevel: 'silent',
plugins: [openapi()],
resolve: { alias: { '@policies': join(dir, 'policies.ts') } },
build: { lib: { entry: join(dir, 'entry.ts'), formats: ['es'], fileName: () => 'entry.js' } },
})
const result = await import(pathToFileURL(join(dir, 'dist/entry.js')).href)
assert.equal(result.staleTime, 42)
assert.deepEqual(await result.fail(), { code: 'CONFLICT', defined: true })
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
for (const mode of ['api', 'query'])
test(`${mode}: error chunks are shared per document and stay lazy with distinct maps`, async () => {
const dir = mkdtempSync(join(root, 'test/.shared-errors-'))
const loaded = `error-sidecars:${dir}`
globalThis[loaded] = []
try {
writeFileSync(join(dir, 'shared.ts'), `export const errorCode = index => 'ERROR_' + index;`)
for (const document of ['console', 'marketplace']) {
const paths = {}
const maps = [
`import { errorCode } from './shared'; globalThis[${JSON.stringify(loaded)}].push('${document}');`,
]
for (let index = 0; index < 20; index++) {
paths[`/operation${index}`] = { get: { responses: { 200: { description: 'OK' } } } }
maps.push(
`const map${index} = { [errorCode(${index})]: { status: 409 } }; export { map${index} as 'GET /operation${index}' };`,
)
}
writeFileSync(join(dir, `${document}.openapi`), JSON.stringify({ openapi: '3.1.0', paths }))
writeFileSync(join(dir, `${document}.openapi.errors.ts`), maps.join('\n'))
}
writeFileSync(
join(dir, 'entry.ts'),
`
import { createClient } from '@dify/openapi-content-mapper';
export { errorCode } from './shared';
import { ${mode} as console } from './console.openapi';
import { ${mode} as marketplace } from './marketplace.openapi';
export async function run(document, index, code) {
const client = createClient({ baseUrl: 'https://example.test', fetch: async () =>
code ? Response.json({ code }, { status: 409 }) : Response.json({ ok: true }) });
const operation = (document === 'console' ? console : marketplace)['operation' + index].get;
try {
return await ${mode === 'api' ? 'operation(undefined, { client })' : 'operation.call(undefined, { client })'};
} catch (error) { return { code: error.code, defined: error.defined }; }
}
`,
)
const result = await build({
configFile: false,
root: dir,
logLevel: 'silent',
plugins: [openapi()],
build: {
minify: true,
rolldownOptions: { preserveEntrySignatures: 'allow-extension' },
lib: { entry: join(dir, 'entry.ts'), formats: ['es'], fileName: 'entry' },
},
})
const chunks = (Array.isArray(result) ? result : [result])
.flatMap((output) => output.output)
.filter((file) => file.type === 'chunk')
const errorChunks = chunks.filter((chunk) =>
Object.keys(chunk.modules).some((id) => id.includes('?openapi-errors=')),
)
assert.equal(errorChunks.length, 2, 'Forty operations need only one error chunk per document')
for (const chunk of errorChunks) {
const documents = new Set(
Object.keys(chunk.modules)
.filter((id) => id.includes('?openapi-errors='))
.map((id) => id.split('?')[0]),
)
assert.equal(documents.size, 1, 'Independent documents must not share error chunks')
}
const entry = chunks.find((chunk) => chunk.isEntry)
const initial = new Set()
const visit = (file) => {
if (initial.has(file)) return
initial.add(file)
chunks.find((chunk) => chunk.fileName === file)?.imports.forEach(visit)
}
visit(entry.fileName)
assert(errorChunks.every((chunk) => !initial.has(chunk.fileName)))
const { run } = await import(pathToFileURL(join(dir, 'dist', entry.fileName)).href)
assert.deepEqual(globalThis[loaded], [])
assert.deepEqual(await run('console', 0), { ok: true })
assert.deepEqual(globalThis[loaded], [], 'Success must not load error validators')
assert.deepEqual(await run('console', 0, 'ERROR_0'), { code: 'ERROR_0', defined: true })
assert.deepEqual(await run('console', 1, 'ERROR_0'), { code: 'ERROR_0', defined: false })
assert.deepEqual(await run('console', 1, 'ERROR_1'), { code: 'ERROR_1', defined: true })
assert.deepEqual(globalThis[loaded], ['console'])
assert.deepEqual(await run('marketplace', 0, 'ERROR_0'), { code: 'ERROR_0', defined: true })
assert.deepEqual(globalThis[loaded], ['console', 'marketplace'])
} finally {
delete globalThis[loaded]
rmSync(dir, { recursive: true, force: true })
}
})
for (const mode of ['nested', 'query'])
test(`${mode}: Vite browser bundle drops 1,000 unused operations and all schema metadata`, async (t) => {
const dir = mkdtempSync(join(root, 'test/.bundle-'))
try {
async function bundle(spec, extension = 'yaml') {
const file = `api.${extension}`
if (extension === 'openapi') {
writeFileSync(join(dir, 'original.json'), JSON.stringify(spec))
symlinkSync('original.json', join(dir, file))
} else writeFileSync(join(dir, file), stringify(spec))
writeFileSync(
join(dir, 'entry.ts'),
mode === 'nested'
? `import { api } from './${file}'; export const getPet = api.pets.byPetId.get;`
: `import { query } from './${file}'; export const getPet = query.pets.byPetId.get.queryOptions;`,
)
const result = await build({
configFile: false,
root: dir,
logLevel: 'silent',
plugins: [openapi()],
build: {
write: false,
minify: true,
lib: { entry: join(dir, 'entry.ts'), formats: ['es'] },
},
})
return (Array.isArray(result) ? result : [result])
.flatMap((output) => output.output)
.filter((file) => file.type === 'chunk')
.map((file) => file.code)
.join('\n')
}
const small = await bundle(fixture)
const large = structuredClone(fixture)
large.info.description = 'SCHEMA_METADATA_MUST_DISAPPEAR'.repeat(2000)
for (let i = 0; i < 1000; i++)
large.paths[`/pets/UNUSED_ENDPOINT_${i}`] = {
get: {
operationId: `unused${i}`,
responses: {
200: {
description: 'unused',
content: { 'application/json': { schema: { type: 'string' } } },
},
},
},
}
const output = await bundle(large)
assert(!output.includes('UNUSED_ENDPOINT_'))
assert(!output.includes('SCHEMA_METADATA_MUST_DISAPPEAR'))
assert(!output.includes('components'))
assert(!output.includes('YAMLParseError'))
assert(!output.includes('parseDocument'))
assert(output.includes('/pets/{petId}'))
assert.equal(
output.length,
small.length,
'Adding unused operations must not increase the bundle',
)
assert(Buffer.byteLength(output) < 30000, `Unexpected runtime size: ${output.length}`)
const json = await bundle(fixture, 'openapi')
const stripBundleComments = (code) => code.replace(/^\/\/.*$/gm, '')
assert.equal(
stripBundleComments(json),
stripBundleComments(small),
'JSON and YAML must produce equivalent runtime code',
)
t.diagnostic(
`One operation: ${Buffer.byteLength(output)} bytes, ${gzipSync(output).length} bytes gzip; unchanged with 1,000 extra operations`,
)
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
for (const mode of ['api', 'query'])
test(`${mode}: unused operation policies and error validators are removed`, async () => {
const dir = mkdtempSync(join(root, 'test/.policies-bundle-'))
try {
async function bundle(count) {
const spec = structuredClone(fixture)
const defaults = [
`const used = { queryOptions: { staleTime: 1234 } }; export { used as 'GET /pets/{petId}' };`,
]
const errors = [
`const used = { NOT_FOUND: { status: 404 } }; export { used as 'GET /pets/{petId}' };`,
]
for (let i = 0; i < count; i++) {
const path = `/UNUSED_OPERATION_${i}`
spec.paths[path] = { get: { responses: { 200: { description: 'OK' } } } }
defaults.push(
`const policy${i} = { queryOptions: { meta: { marker: 'UNUSED_POLICY_${i}' } } }; export { policy${i} as ${JSON.stringify(`GET ${path}`)} };`,
)
errors.push(
`const errors${i} = { UNUSED: { status: 400, message: 'UNUSED_VALIDATOR_${i}' } }; export { errors${i} as ${JSON.stringify(`GET ${path}`)} };`,
)
}
writeFileSync(join(dir, 'api.openapi'), JSON.stringify(spec))
writeFileSync(join(dir, 'api.openapi.queries.ts'), defaults.join('\n'))
writeFileSync(join(dir, 'api.openapi.errors.ts'), errors.join('\n'))
writeFileSync(
join(dir, 'entry.ts'),
`import { ${mode} } from './api.openapi'; export const operation = ${mode}.pets.byPetId.get${mode === 'query' ? '.queryOptions' : ''};`,
)
const result = await build({
configFile: false,
root: dir,
logLevel: 'silent',
plugins: [openapi()],
build: {
write: false,
minify: true,
rolldownOptions: { preserveEntrySignatures: 'allow-extension' },
lib: { entry: join(dir, 'entry.ts'), formats: ['es'] },
},
})
return (Array.isArray(result) ? result : [result])
.flatMap((r) => r.output)
.filter((f) => f.type === 'chunk')
.map((f) => f.code)
.join('\n')
}
const small = await bundle(0)
const large = await bundle(1000)
assert.equal(large, small)
assert(!large.includes('UNUSED_'))
if (mode === 'query') assert(large.includes('1234'))
else assert(!large.includes('1234'), 'API calls do not retain query defaults')
} finally {
rmSync(dir, { recursive: true, force: true })
}
})