mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 13:51:25 +08:00
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
287 lines
12 KiB
JavaScript
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 })
|
|
}
|
|
})
|