mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-04 02:00:10 +00:00
* fix(gateway): restore local read probes for paired devices Prefer cached device auth bound to the exact Gateway origin while retaining the supported unscoped fallback for default local probes. Keep remote and SSH credential isolation unchanged. Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> * fix(gateway): restore local read probes for paired devices Worked on by: - @IWhatsskill Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> OpenClaw-Publication: b644d92a-5bf7-4320-9520-477e0583f559 * test(gateway): simplify probe regression setup Remove the redundant single-use URL variable without changing the Gateway target or assertions. Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> * fix(gateway): isolate cached credentials on remote loopback probes Carry target provenance into status, audit, and daemon probes. Apply the existing SSH credential boundary to remote loopback transports while preserving local pairing, distinct remote origins, and explicit remote credentials. Document the credential path and cover configured and environment-selected targets at the final connect frame. Reuse the existing asynchronous session fixture cleanup to drain agent resources before shared-state retirement. This addresses the repeated CI teardown failure without changing production lifecycle safeguards. Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> * fix(gateway): verify restart targets before credential probes Validate the unmanaged listener, lock, and foreground owner before probing, revalidate after the probe, and preserve the intent-delivery fence. Keep paired device identity for explicit remote authentication while the scoped client excludes cached bearer tokens. Cover the unverified restart connect boundary, replacement during the probe, legacy intent-write replacement, and real Gateway recovery of a cached-token-only remote setup. Reuse the existing owner test fixture to keep the lifecycle test within its line cap. Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> * fix(gateway): keep local probes working after SSH tunnels Preserve the implicit local client's device token before consulting the origin cache, which may still contain another Gateway's tunnel token. Retain origin-only fallback and existing remote credential isolation. Extend the existing connect-frame table and real Gateway recovery test. The collision fails with device token mismatch before this correction; all 46 focused tests pass afterward. No new production abstraction. Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> * fix(gateway): keep foreign origin tokens out of local probes Require origin-only implicit local credentials to match the existing read-only local pairing record. Carry the exact verified entry through preparedDeviceAuth so a concurrent cache replacement cannot change the bearer sent by the client. Keep explicit-auth identity recovery unchanged. Extend the existing real Gateway suite and scope table. Before repair, the foreign-origin and cache-replacement cases fail. All 47 focused tests now pass; actual receive-side capture confirms that foreign bearers are omitted while local, explicit-auth, and pinned-cache paths succeed. Reuse existing stores and client contracts; add no schema, configuration, permissions, or authentication manager. Document the local issuer boundary. Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> * fix(gateway): bind paired credentials to selected SSH routes Reuse selected-route credential ownership across enrollment, RPC clients and diagnostics. Own and verify SSH tunnel lifetime, preserve explicit target selection and TLS identity, and retain local issuer validation. Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> * test(gateway): align route probe fixtures and isolate ambient auth Preserve exact local/configured target assertions and isolate setup tests from inherited shared credentials. Clarify the existing operator-owned SSH alias and upgrade boundary without changing runtime behavior. Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> * docs(gateway): clarify route rollback and database compatibility Distinguish retained credential rows from binary rollback across an independently upgraded state schema. Published-client proof preserves the old token but correctly rejects newer-schema access without compatible pre-update state. Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> * fix(gateway): recover SSH tunnels from restricted local ports Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> * fix(sdk): preserve published WebSocket client types Keep the shipped ws options and constructor signatures at the plugin boundary while Gateway and worker transports retain their internal Node TLS types. Add a strict public consumer regression and document the compatibility contract. Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> * style(gateway): use function type for WebSocket constructor Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> --------- Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
466 lines
19 KiB
TypeScript
Executable file
466 lines
19 KiB
TypeScript
Executable file
#!/usr/bin/env -S node --import tsx
|
|
|
|
/**
|
|
* Verifies that public plugin-sdk subpaths are present in the compiled dist output.
|
|
*
|
|
* Run after the package build to catch missing exports or leaked repo-only type aliases
|
|
* before release.
|
|
*/
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
statSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { reportLimitViolations } from "./lib/check-limits.mts";
|
|
import { resolveRepoToolBinPath } from "./lib/local-check-runtime.mts";
|
|
import { createNativeTypeScriptParser } from "./lib/native-typescript.mts";
|
|
import {
|
|
MAX_PRIVATE_QA_PUBLIC_PLUGIN_SDK_DECLARATION_BYTES,
|
|
MAX_PUBLIC_PLUGIN_SDK_DECLARATION_BYTES,
|
|
PLUGIN_SDK_DECLARATION_OUTPUT_VARIANCE_BYTES,
|
|
evaluatePluginSdkDeclarationBudget,
|
|
isPrivateQaPluginSdkBuild,
|
|
} from "./lib/plugin-sdk-declaration-budget.mts";
|
|
import { publicPluginSdkEntrypoints, publicPluginSdkSubpaths } from "./lib/plugin-sdk-entries.mts";
|
|
import { findUndeclaredBundlerHelperDtsExports } from "./lib/sanitize-bundler-helper-dts-exports.mts";
|
|
|
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = resolve(scriptDir, "..");
|
|
const tsgoPath = resolveRepoToolBinPath("tsgo", { cwd: repoRoot });
|
|
const forbiddenPublicDeclarationSpecifiers = ["@openclaw/llm-core"];
|
|
const FORBIDDEN_PUBLIC_PROTOCOL_REGISTRY_RE = /\bdeclare\s+const\s+ProtocolSchemas(?:\$\d+)?\b/u;
|
|
const RELATIVE_DECLARATION_SPECIFIER_RE = /\b(?:from|import)\s*(?:\(\s*)?["']([^"']+)["']/gu;
|
|
const requiredSubpathExports: Record<string, string[]> = {
|
|
"diagnostic-flags": ["isDiagnosticFlagEnabled"],
|
|
"diagnostic-runtime": ["areDiagnosticsEnabledForProcess", "createSubsystemLogger"],
|
|
"secret-input-runtime": [
|
|
"assertPluginCapabilitySecretAvailable",
|
|
"coerceSecretRef",
|
|
"hasConfiguredSecretInput",
|
|
"isSecretRef",
|
|
"normalizeResolvedSecretInputString",
|
|
"normalizeSecretInputString",
|
|
"resolveSecretInputString",
|
|
],
|
|
};
|
|
|
|
// These private runtime facades have declarations only in the private-QA profile.
|
|
// Do not require their types from ordinary public-package builds.
|
|
const privateRuntimeConsumers = isPrivateQaPluginSdkBuild(process.env)
|
|
? `import { SessionManager, type SessionEntry } from "openclaw/plugin-sdk/agent-sessions";
|
|
import type { drainPendingDeliveries } from "openclaw/plugin-sdk/delivery-queue-runtime";
|
|
|
|
type RecoveryDeliver = NonNullable<Parameters<typeof drainPendingDeliveries>[0]["deliver"]>;
|
|
type RecoveryParams = Parameters<RecoveryDeliver>[0];
|
|
type RecoveryContextIsPrivate = RequireNever<Extract<keyof RecoveryParams, PrivateQueueContextKeys>>;
|
|
|
|
// Private facade declarations must preserve callable access to persist.
|
|
declare const sessionManager: SessionManager;
|
|
declare const sessionEntry: SessionEntry;
|
|
sessionManager.persist(sessionEntry);
|
|
sessionManager.persist(sessionEntry, {});
|
|
// @ts-expect-error Persist still requires a complete session entry.
|
|
sessionManager.persist({});`
|
|
: "";
|
|
|
|
let missing = 0;
|
|
|
|
{
|
|
const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-plugin-sdk-consumer-"));
|
|
const consumerRoot = join(tempRoot, "consumer");
|
|
try {
|
|
mkdirSync(consumerRoot, { recursive: true });
|
|
writeFileSync(
|
|
join(consumerRoot, "index.ts"),
|
|
`import { buildChannelConfigSchema, DmPolicySchema } from "openclaw/plugin-sdk/channel-config-schema";
|
|
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
import type { sendDurableMessageBatch } from "openclaw/plugin-sdk/channel-outbound";
|
|
import type {
|
|
EmbeddingBatchChunk,
|
|
EmbeddingBatchOptions,
|
|
EmbeddingProviderBatchRuntime,
|
|
} from "openclaw/plugin-sdk/embedding-provider-runtime-contract";
|
|
import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
|
|
import { identityEntryAuthenticationClassifier, meetsIdentifierAuthentication } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
import type {
|
|
ChannelIngressIdentitySubjectInput,
|
|
IdentifierAuthentication,
|
|
} from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
// @ts-expect-error Host admission evidence is intentionally private to core.
|
|
import type { ChannelAdmissionEvidence } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
// @ts-expect-error Plugins cannot mint host admission evidence.
|
|
import { prepareHostChannelContextAdmissionEvidence } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
// @ts-expect-error Plugins cannot register host evidence owners.
|
|
import { registerChannelAdmissionEvidenceOwner } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
import { createPluginRuntimeStore, type PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
|
|
import type { buildModelsProviderData, buildPreparedModelsProviderData, ModelsProviderData } from "openclaw/plugin-sdk/models-provider-runtime";
|
|
import type { buildModelsProviderData as buildCommandAuthModelsProviderData } from "openclaw/plugin-sdk/command-auth";
|
|
import type { ClientRequestArgs } from "node:http";
|
|
import type { ClientOptions as PublishedClientOptions, WebSocket as PublishedWebSocket } from "ws";
|
|
import { WebSocket, type ClientOptions } from "openclaw/plugin-sdk/websocket-runtime";
|
|
import { z } from "zod";
|
|
${privateRuntimeConsumers}
|
|
|
|
type RequireNever<T extends never> = T;
|
|
type RequireTrue<T extends true> = T;
|
|
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends
|
|
(<T>() => T extends B ? 1 : 2) ? true : false;
|
|
type PrivateQueueContextKeys =
|
|
| "conversationDeliveryTarget"
|
|
| "deliveryQueueStateContext"
|
|
| "databaseAgentId"
|
|
| "supervisorMode"
|
|
| "env";
|
|
type SendParams = Parameters<typeof sendDurableMessageBatch>[0];
|
|
type KeysOfUnion<T> = T extends unknown ? keyof T : never;
|
|
// Database context stays private even when public aliases derive from core types.
|
|
type SendContextIsPrivate = RequireNever<Extract<keyof SendParams, PrivateQueueContextKeys>>;
|
|
type CompletionContextIsPrivate = RequireNever<
|
|
Extract<KeysOfUnion<NonNullable<SendParams["deliveryCompletion"]>>, PrivateQueueContextKeys>
|
|
>;
|
|
type QueueOwner = NonNullable<SendParams["deliveryQueueOwner"]>;
|
|
type FailureRecorder = Parameters<QueueOwner["fail"]>[0];
|
|
type FailureRecorderArgsUnchanged = RequireTrue<Equal<Parameters<FailureRecorder>, [
|
|
id: string,
|
|
error: string,
|
|
stateDir?: string,
|
|
expectedPlatformSendAttemptId?: string | null,
|
|
]>>;
|
|
type AckOptionsUnchanged = RequireTrue<Equal<NonNullable<Parameters<QueueOwner["ack"]>[0]>, {
|
|
retainSpoolArtifacts?: boolean;
|
|
suppressCompletionReceipt?: boolean;
|
|
expectedPlatformSendAttemptId?: string | null;
|
|
}>>;
|
|
|
|
// Compile-only consumers retain the WebSocket contract shipped in v2026.9.6.
|
|
type WebSocketOptionsUnchanged = RequireTrue<Equal<ClientOptions, PublishedClientOptions>>;
|
|
type WebSocketConstructorUnchanged = RequireTrue<Equal<typeof WebSocket, typeof PublishedWebSocket>>;
|
|
const legacyWebSocketOptions: ClientOptions = {
|
|
checkServerIdentity: (hostname, certificate) => hostname.length > 0 && certificate.length > 0,
|
|
};
|
|
const legacySocket = new WebSocket("wss://gateway.example", legacyWebSocketOptions);
|
|
new WebSocket(null);
|
|
new WebSocket(new URL("wss://gateway.example"), {
|
|
checkServerIdentity: (hostname, certificate) => hostname.length > 0 && certificate.length > 0,
|
|
});
|
|
new WebSocket("wss://gateway.example", ["fixture"], {
|
|
checkServerIdentity: (hostname, certificate) => hostname.length > 0 && certificate.length > 0,
|
|
});
|
|
new WebSocket("wss://gateway.example", "fixture", legacyWebSocketOptions);
|
|
const httpWebSocketOptions: ClientRequestArgs = { agent: false };
|
|
new WebSocket("wss://gateway.example", undefined, httpWebSocketOptions);
|
|
const legacySocketArgs: ConstructorParameters<typeof WebSocket> = [
|
|
new URL("wss://gateway.example"), undefined, legacyWebSocketOptions,
|
|
];
|
|
new WebSocket(...legacySocketArgs);
|
|
const closedSocketState: typeof PublishedWebSocket.CLOSED = WebSocket.CLOSED;
|
|
legacySocket.on("message", (_data, isBinary) => {
|
|
const binary: boolean = isBinary;
|
|
void binary;
|
|
});
|
|
void closedSocketState;
|
|
|
|
// Stable v2026.7.1-2 consumers construct these results and supply typed adapters.
|
|
const legacyModelsData = {
|
|
byProvider: new Map<string, Set<string>>(),
|
|
providers: [],
|
|
resolvedDefault: { provider: "fixture-provider", model: "fixture-model" },
|
|
modelNames: new Map<string, string>(),
|
|
};
|
|
const modelsData: ModelsProviderData = legacyModelsData;
|
|
const modelsAdapter: typeof buildModelsProviderData = async () => legacyModelsData;
|
|
const commandAuthModelsAdapter: typeof buildCommandAuthModelsProviderData = modelsAdapter;
|
|
void modelsData;
|
|
void commandAuthModelsAdapter;
|
|
declare const preparedModelsData: Awaited<ReturnType<typeof buildPreparedModelsProviderData>>;
|
|
const preparedCatalog: { id: string; provider: string; contextWindow?: number }[] = preparedModelsData.modelCatalog;
|
|
void preparedCatalog;
|
|
// @ts-expect-error Prepared selections require their typed catalog metadata.
|
|
const incompletePrepared: typeof preparedModelsData = legacyModelsData;
|
|
void incompletePrepared;
|
|
void defineToolPlugin;
|
|
|
|
const identifierAuthentication: IdentifierAuthentication = "verified";
|
|
const meetsMinimum: boolean = meetsIdentifierAuthentication(identifierAuthentication, "asserted");
|
|
void meetsMinimum;
|
|
const subject: ChannelIngressIdentitySubjectInput = {
|
|
stableId: "provider-user-id",
|
|
authentication: { "provider-user-id": identifierAuthentication },
|
|
};
|
|
void subject;
|
|
const classifyEntryAuthentication = identityEntryAuthenticationClassifier({
|
|
primary: { authentication: identifierAuthentication },
|
|
});
|
|
const entryAuthentication: IdentifierAuthentication | undefined = classifyEntryAuthentication("provider-user-id");
|
|
void entryAuthentication;
|
|
|
|
const batchEmbed: EmbeddingProviderBatchRuntime["batchEmbed"] = async (options: EmbeddingBatchOptions) => {
|
|
const chunks: EmbeddingBatchChunk[] = options.chunks;
|
|
return chunks.map(() => [1]);
|
|
};
|
|
const batchRuntimes: EmbeddingProviderBatchRuntime[] = [
|
|
{ batchEmbed },
|
|
{ batchEmbed, sourceWideBatchEmbed: true },
|
|
{ batchEmbed, sourceWideBatchEmbed: false },
|
|
];
|
|
const batchRuntimeWithInternalPolicy = {
|
|
batchEmbed,
|
|
// @ts-expect-error Cache identity is not part of the public batch contract.
|
|
cacheKeyData: {},
|
|
} satisfies EmbeddingProviderBatchRuntime;
|
|
void batchRuntimes;
|
|
void batchRuntimeWithInternalPolicy;
|
|
|
|
const runtimeStore = createPluginRuntimeStore<PluginRuntime>({
|
|
pluginId: "package-consumer",
|
|
errorMessage: "package consumer runtime not initialized",
|
|
});
|
|
export const configSchema = buildChannelConfigSchema(
|
|
z.object({ dmPolicy: DmPolicySchema.optional() }),
|
|
);
|
|
|
|
declare const plugin: Parameters<typeof defineChannelPluginEntry>[0]["plugin"];
|
|
export default defineChannelPluginEntry({
|
|
id: "package-consumer",
|
|
name: "Package Consumer",
|
|
description: "Published Plugin SDK declaration compatibility fixture",
|
|
plugin,
|
|
setRuntime: runtimeStore.setRuntime,
|
|
});
|
|
`,
|
|
);
|
|
writeFileSync(join(consumerRoot, "package.json"), '{"private":true,"type":"module"}\n');
|
|
// Keep skipLibCheck on for this in-tree consumer: workspace @openclaw/ai
|
|
// declaration caches can omit .d.mts while still shipping .mjs, which makes
|
|
// skipLibCheck:false fail with TS7016 before the helper scan below. Packed
|
|
// release-check still uses skipLibCheck:false against a complete tarball.
|
|
writeFileSync(
|
|
join(consumerRoot, "tsconfig.json"),
|
|
`{
|
|
"compilerOptions": {
|
|
"lib": ["DOM", "DOM.Iterable", "ES2023"],
|
|
"module": "NodeNext",
|
|
"moduleResolution": "NodeNext",
|
|
"noEmit": true,
|
|
"skipLibCheck": true,
|
|
"strict": true,
|
|
"types": []
|
|
},
|
|
"include": ["index.ts"]
|
|
}
|
|
`,
|
|
);
|
|
const openclawPackagePath = join(consumerRoot, "node_modules", "openclaw");
|
|
mkdirSync(dirname(openclawPackagePath), { recursive: true });
|
|
symlinkSync(repoRoot, openclawPackagePath, process.platform === "win32" ? "junction" : "dir");
|
|
for (const dependency of ["zod", "ws", "@types/ws"]) {
|
|
const dependencyPath = join(consumerRoot, "node_modules", dependency);
|
|
mkdirSync(dirname(dependencyPath), { recursive: true });
|
|
symlinkSync(
|
|
join(repoRoot, "node_modules", dependency),
|
|
dependencyPath,
|
|
process.platform === "win32" ? "junction" : "dir",
|
|
);
|
|
}
|
|
|
|
const result = spawnSync(
|
|
tsgoPath,
|
|
["-p", join(consumerRoot, "tsconfig.json"), "--pretty", "false"],
|
|
{ cwd: consumerRoot, encoding: "utf8" },
|
|
);
|
|
if (result.error) {
|
|
console.error("BROKEN PLUGIN SDK CONSUMER: failed to start tsgo");
|
|
console.error(result.error.message);
|
|
missing += 1;
|
|
} else if (result.status !== 0) {
|
|
console.error("BROKEN PLUGIN SDK CONSUMER: mixed public subpaths are not assignable");
|
|
process.stderr.write(result.stdout || "");
|
|
process.stderr.write(result.stderr || "");
|
|
missing += 1;
|
|
}
|
|
} finally {
|
|
rmSync(tempRoot, { force: true, recursive: true });
|
|
}
|
|
}
|
|
|
|
for (const entry of publicPluginSdkSubpaths) {
|
|
const jsPath = resolve(scriptDir, "..", "dist", "plugin-sdk", `${entry}.js`);
|
|
const dtsPath = resolve(scriptDir, "..", "dist", "plugin-sdk", `${entry}.d.ts`);
|
|
if (!existsSync(jsPath)) {
|
|
console.error(`MISSING SUBPATH JS: dist/plugin-sdk/${entry}.js`);
|
|
missing += 1;
|
|
}
|
|
if (!existsSync(dtsPath)) {
|
|
console.error(`MISSING SUBPATH DTS: dist/plugin-sdk/${entry}.d.ts`);
|
|
missing += 1;
|
|
}
|
|
}
|
|
|
|
for (const [entry, names] of Object.entries(requiredSubpathExports)) {
|
|
const jsPath = resolve(scriptDir, "..", "dist", "plugin-sdk", `${entry}.js`);
|
|
if (!existsSync(jsPath)) {
|
|
continue;
|
|
}
|
|
let runtime: Record<string, unknown>;
|
|
try {
|
|
runtime = (await import(pathToFileURL(jsPath).href)) as Record<string, unknown>;
|
|
} catch (err) {
|
|
console.error(`BROKEN SUBPATH JS: dist/plugin-sdk/${entry}.js`);
|
|
console.error(err instanceof Error ? err.message : String(err));
|
|
missing += 1;
|
|
continue;
|
|
}
|
|
for (const name of names) {
|
|
if (typeof runtime[name] !== "function") {
|
|
console.error(`MISSING SUBPATH EXPORT: dist/plugin-sdk/${entry}.js#${name}`);
|
|
missing += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
const distDir = resolve(scriptDir, "..", "dist");
|
|
const declarationPaths = new Set<string>();
|
|
// Publication checks always start at public roots. Private QA entries are local-only,
|
|
// but their unified-build chunk topology can still change declarations reachable here.
|
|
const declarationQueue = publicPluginSdkEntrypoints.map((entry: string) =>
|
|
resolve(distDir, "plugin-sdk", `${entry}.d.ts`),
|
|
);
|
|
while (declarationQueue.length > 0) {
|
|
const dtsPath = declarationQueue.pop();
|
|
if (!dtsPath || declarationPaths.has(dtsPath)) {
|
|
continue;
|
|
}
|
|
if (!existsSync(dtsPath)) {
|
|
console.error(`MISSING PUBLIC DTS DEPENDENCY: ${relative(resolve(scriptDir, ".."), dtsPath)}`);
|
|
missing += 1;
|
|
continue;
|
|
}
|
|
declarationPaths.add(dtsPath);
|
|
const dtsContent = readFileSync(dtsPath, "utf8");
|
|
if (FORBIDDEN_PUBLIC_PROTOCOL_REGISTRY_RE.test(dtsContent)) {
|
|
console.error(
|
|
`FORBIDDEN PUBLIC DTS REGISTRY: ${relative(resolve(scriptDir, ".."), dtsPath)} retains ProtocolSchemas`,
|
|
);
|
|
missing += 1;
|
|
}
|
|
for (const match of dtsContent.matchAll(RELATIVE_DECLARATION_SPECIFIER_RE)) {
|
|
const specifier = match[1];
|
|
if (!specifier?.startsWith(".")) {
|
|
continue;
|
|
}
|
|
const declarationSpecifier = specifier.endsWith(".js")
|
|
? `${specifier.slice(0, -3)}.d.ts`
|
|
: `${specifier}.d.ts`;
|
|
const importedPath = resolve(dirname(dtsPath), declarationSpecifier);
|
|
if (importedPath.startsWith(`${distDir}${sep}`)) {
|
|
declarationQueue.push(importedPath);
|
|
}
|
|
}
|
|
for (const specifier of forbiddenPublicDeclarationSpecifiers) {
|
|
if (dtsContent.includes(`"${specifier}`) || dtsContent.includes(`'${specifier}`)) {
|
|
console.error(
|
|
`FORBIDDEN PUBLIC DTS SPECIFIER: ${relative(resolve(scriptDir, ".."), dtsPath)} imports ${specifier}`,
|
|
);
|
|
missing += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
const declarationBytes = Array.from(declarationPaths).reduce<number>(
|
|
(total, dtsPath) => total + statSync(dtsPath).size,
|
|
0,
|
|
);
|
|
const declarationBudget = evaluatePluginSdkDeclarationBudget({
|
|
buildPrivateQa: isPrivateQaPluginSdkBuild(process.env),
|
|
declarationBytes,
|
|
});
|
|
if (declarationBudget.shouldFail) {
|
|
const budgetLabel =
|
|
declarationBudget.budgetKind === "private-qa-public-entry"
|
|
? "PRIVATE QA PUBLIC-ENTRY PLUGIN SDK"
|
|
: "PLUGIN SDK";
|
|
if (
|
|
reportLimitViolations([
|
|
{
|
|
file: "scripts/lib/plugin-sdk-declaration-budget.mts",
|
|
title: "Plugin SDK declaration size budget",
|
|
message: `${budgetLabel} DTS TOO LARGE: ${declarationBytes} bytes exceeds ${declarationBudget.budgetBytes} bytes. Budget: ${declarationBudget.ratchetBytes}-byte ratchet + ${declarationBudget.varianceBytes}-byte Rolldown output variance. Keep plugin SDK declarations in the canonical unified tsdown graph.`,
|
|
},
|
|
])
|
|
) {
|
|
missing += 1;
|
|
}
|
|
} else if (declarationBudget.budgetKind === "private-qa-public-entry") {
|
|
console.log(
|
|
`Private QA build public-entry declaration graph: ${declarationBytes}/${declarationBudget.budgetBytes} bytes (${MAX_PRIVATE_QA_PUBLIC_PLUGIN_SDK_DECLARATION_BYTES}-byte ratchet + ${PLUGIN_SDK_DECLARATION_OUTPUT_VARIANCE_BYTES}-byte output variance); publication ratchet ${MAX_PUBLIC_PLUGIN_SDK_DECLARATION_BYTES} bytes is not applied.`,
|
|
);
|
|
} else {
|
|
console.log(
|
|
`Public plugin SDK declaration graph: ${declarationBytes}/${declarationBudget.budgetBytes} bytes (${MAX_PUBLIC_PLUGIN_SDK_DECLARATION_BYTES}-byte ratchet + ${PLUGIN_SDK_DECLARATION_OUTPUT_VARIANCE_BYTES}-byte output variance).`,
|
|
);
|
|
}
|
|
|
|
{
|
|
const rootDist = resolve(scriptDir, "..", "dist");
|
|
if (!existsSync(rootDist)) {
|
|
console.error("UNDECLARED BUNDLER HELPER DTS EXPORT: missing dist/ for helper export scan");
|
|
missing += 1;
|
|
} else {
|
|
// tsx's synchronous lexer misparses emitted `using` helpers with this shebang.
|
|
const parser = createNativeTypeScriptParser({ cwd: repoRoot });
|
|
try {
|
|
const queue = [rootDist];
|
|
const visitedDirs = new Set<string>();
|
|
while (queue.length > 0) {
|
|
const dir = queue.pop()!;
|
|
if (visitedDirs.has(dir)) {
|
|
continue;
|
|
}
|
|
visitedDirs.add(dir);
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const fullPath = join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
queue.push(fullPath);
|
|
continue;
|
|
}
|
|
if (!entry.isFile() || !/\.d\.(?:ts|mts|cts)$/u.test(entry.name)) {
|
|
continue;
|
|
}
|
|
const sourceText = readFileSync(fullPath, "utf8");
|
|
for (const finding of findUndeclaredBundlerHelperDtsExports(
|
|
sourceText,
|
|
fullPath,
|
|
parser,
|
|
)) {
|
|
console.error(
|
|
`UNDECLARED BUNDLER HELPER DTS EXPORT: ${relative(resolve(scriptDir, ".."), fullPath)}:${finding.line} exports ${finding.name} without a local declaration`,
|
|
);
|
|
missing += 1;
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
parser.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (missing > 0) {
|
|
console.error(`\nERROR: ${missing} plugin-sdk artifact check(s) failed.`);
|
|
console.error("This will break published plugin-sdk artifacts.");
|
|
console.error("Check generated d.ts rewrites, subpath entries, type compatibility, and rebuild.");
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`OK: All ${publicPluginSdkSubpaths.length} public plugin-sdk subpaths verified.`);
|