From 660a056b8743ecdca083b56d66655036122d9235 Mon Sep 17 00:00:00 2001 From: xingyu Date: Sun, 12 Jul 2026 17:07:28 +0800 Subject: [PATCH] fix(extensions): make docs containment cross-platform (#874) --- kun/src/extensions/host-process.ts | 44 ++++++++++++------- .../scripts/generate-manifest-schema.mjs | 2 +- scripts/check-extension-docs.test.mjs | 24 +++++++++- scripts/check-extension-release-gate.mjs | 2 + scripts/lib/extension-docs-validation.mjs | 13 +++++- 5 files changed, 66 insertions(+), 19 deletions(-) diff --git a/kun/src/extensions/host-process.ts b/kun/src/extensions/host-process.ts index 60a2f9a71..33c10f2db 100644 --- a/kun/src/extensions/host-process.ts +++ b/kun/src/extensions/host-process.ts @@ -370,11 +370,22 @@ export class ExtensionHostProcess { state: this._state }) } - return this.peer!.request('extension.invoke', { method, params }, { - signal: options.signal, - timeoutMs: options.timeoutMs ?? this.limits.operationTimeoutMs, - resetTimeoutOnStream: options.resetTimeoutOnStream - }) + try { + return await this.peer!.request('extension.invoke', { method, params }, { + signal: options.signal, + timeoutMs: options.timeoutMs ?? this.limits.operationTimeoutMs, + resetTimeoutOnStream: options.resetTimeoutOnStream + }) + } catch (error) { + if ( + this.exitPromise !== undefined && + this.child !== undefined && + (this.child.exitCode !== null || this.child.signalCode !== null) + ) { + await this.exitPromise + } + throw error + } } async notify(method: string, params: JsonValue): Promise { @@ -539,16 +550,19 @@ export class ExtensionHostProcess { await this.log.write('lifecycle', `exited expected=${expected} code=${code} signal=${signal}`) .catch(() => undefined) await this.log.flush().catch(() => undefined) - this.resolveExit?.() - this.resolveExit = undefined - await this.options.onExit?.({ - extensionId: this.principal.extensionId, - lifecycleNonce: this.lifecycleNonce, - expected, - code, - signal, - ...(this._lastError === undefined ? {} : { error: this._lastError }) - }) + try { + await this.options.onExit?.({ + extensionId: this.principal.extensionId, + lifecycleNonce: this.lifecycleNonce, + expected, + code, + signal, + ...(this._lastError === undefined ? {} : { error: this._lastError }) + }) + } finally { + this.resolveExit?.() + this.resolveExit = undefined + } } private send(envelope: RpcEnvelope): Promise { diff --git a/packages/extension-api/scripts/generate-manifest-schema.mjs b/packages/extension-api/scripts/generate-manifest-schema.mjs index 3fe7f36f1..286d5c0f9 100644 --- a/packages/extension-api/scripts/generate-manifest-schema.mjs +++ b/packages/extension-api/scripts/generate-manifest-schema.mjs @@ -29,7 +29,7 @@ const output = `${JSON.stringify(schema, null, 2)}\n` if (process.argv.includes('--check')) { const current = await readFile(outputPath, 'utf8').catch(() => '') - if (current !== output) { + if (current.replace(/\r\n/gu, '\n') !== output) { console.error('EXT_SCHEMA_STALE: schema/kun-extension.schema.json is not generated from ExtensionManifestSchema') process.exitCode = 1 } diff --git a/scripts/check-extension-docs.test.mjs b/scripts/check-extension-docs.test.mjs index b04e5bee0..0252bc66d 100644 --- a/scripts/check-extension-docs.test.mjs +++ b/scripts/check-extension-docs.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' -import { dirname, join } from 'node:path' +import { dirname, join, posix, win32 } from 'node:path' import test from 'node:test' import { fileURLToPath } from 'node:url' import { @@ -9,6 +9,7 @@ import { SDK_SNAPSHOTS_BEGIN, SDK_SNAPSHOTS_END, githubHeadingSlug, + isPathWithinRoot, renderApiExportsRegion, renderSdkSnapshotsRegion, validateBilingualPair, @@ -110,6 +111,27 @@ test('detects generated API inventory and Changelog public-surface drift', () => )[0].includes('drifted')) }) +test('contains public SDK declarations across native and mixed Windows separators', () => { + assert.equal( + isPathWithinRoot( + 'D:\\a\\Kun\\Kun\\packages\\extension-api\\src', + 'D:/a/Kun/Kun/packages/extension-api/src/accounts.ts', + win32 + ), + true + ) + assert.equal( + isPathWithinRoot( + 'D:\\a\\Kun\\Kun\\packages\\extension-api\\src', + 'D:/a/Kun/Kun/packages/extension-api/src-escape/accounts.ts', + win32 + ), + false + ) + assert.equal(isPathWithinRoot('/repo/packages/api/src', '/repo/packages/api/src/index.ts', posix), true) + assert.equal(isPathWithinRoot('/repo/packages/api/src', '/repo/packages/other/index.ts', posix), false) +}) + function fixture(name) { return readFile(join(fixtures, name), 'utf8') } diff --git a/scripts/check-extension-release-gate.mjs b/scripts/check-extension-release-gate.mjs index bd8884bdb..9a5b4e41f 100644 --- a/scripts/check-extension-release-gate.mjs +++ b/scripts/check-extension-release-gate.mjs @@ -193,6 +193,7 @@ function requirePublishDependencies(document, workflowLabel) { } function requireOrderedSourceMarkers(source, label, markers) { + source = source.replace(/\r\n/gu, '\n') let priorIndex = -1 for (const marker of markers) { const index = source.indexOf(marker, priorIndex + 1) @@ -202,6 +203,7 @@ function requireOrderedSourceMarkers(source, label, markers) { } function requireSourceMarkersAfter(source, label, priorMarker, markers) { + source = source.replace(/\r\n/gu, '\n') const priorIndex = source.indexOf(priorMarker) check(priorIndex >= 0, `${label} is missing required gate marker: ${priorMarker}`) for (const marker of markers) { diff --git a/scripts/lib/extension-docs-validation.mjs b/scripts/lib/extension-docs-validation.mjs index 37a8cc4db..9b45d8ec9 100644 --- a/scripts/lib/extension-docs-validation.mjs +++ b/scripts/lib/extension-docs-validation.mjs @@ -1,6 +1,6 @@ import { createHash } from 'node:crypto' import { access, readFile, readdir } from 'node:fs/promises' -import { dirname, extname, join, relative, resolve, sep } from 'node:path' +import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path' import ts from 'typescript' export const API_EXPORTS_BEGIN = '' @@ -244,7 +244,7 @@ export async function inspectPublicSdkPackages(root) { const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0] if (!declaration) throw new Error(`Public export ${exportSymbol.name} has no declaration`) const sourceFile = declaration.getSourceFile().fileName - if (!sourceFile.startsWith(`${join(packageRoot, 'src')}${sep}`) && sourceFile !== entryPath) { + if (!isPathWithinRoot(join(packageRoot, 'src'), sourceFile)) { throw new Error(`Public export ${exportSymbol.name} escapes ${definition.name}: ${sourceFile}`) } return { @@ -271,6 +271,15 @@ export async function inspectPublicSdkPackages(root) { return result } +export function isPathWithinRoot(root, candidate, pathApi = { isAbsolute, relative, sep }) { + const relativePath = pathApi.relative(root, candidate) + return relativePath === '' || ( + relativePath !== '..' && + !relativePath.startsWith(`..${pathApi.sep}`) && + !pathApi.isAbsolute(relativePath) + ) +} + export function renderApiExportsRegion(sdkPackages, locale) { const summaryLabels = locale === 'zh' ? ['SDK 包', '版本', '公开入口', '公开导出数', '公开 surface SHA-256']