import { execFileSync } from "node:child_process"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(__dirname, "../.."); const schemaPath = resolve( repoRoot, "packages/workbench/snapshot/src/schema.json" ); const limitsPath = resolve( repoRoot, "packages/workbench/snapshot/src/limits.ts" ); const outputPath = resolve( repoRoot, "packages/workbench/service/workbench_snapshot_contract.gen.go" ); const checkOnly = process.argv.includes("--check"); const schema = JSON.parse(readFileSync(schemaPath, "utf8")); const limits = parseLimits(readFileSync(limitsPath, "utf8")); const nextSource = formatGoSource(renderGoContract(schema, limits)); const currentSource = readExistingText(outputPath); if (checkOnly) { if (currentSource !== nextSource) { throw new Error( "Go workbench snapshot contract is out of sync with packages/workbench/snapshot sources. Run pnpm sync:workbench-go-contract." ); } } else if (currentSource !== nextSource) { writeFileSync(outputPath, nextSource, "utf8"); } function renderGoContract(schema, limits) { const schemaVersion = readRequiredNumber( schema.properties?.schemaVersion?.const, "schemaVersion" ); const maxNodes = readRequiredNumber( schema.properties?.nodes?.maxItems, "nodes.maxItems" ); const maxNodeIDLength = readRequiredNumber( schema.$defs?.node?.properties?.id?.maxLength, "$defs.node.properties.id.maxLength" ); const maxKindLength = readRequiredNumber( schema.$defs?.node?.properties?.kind?.maxLength, "$defs.node.properties.kind.maxLength" ); const maxTitleLength = readRequiredNumber( schema.$defs?.node?.properties?.title?.maxLength, "$defs.node.properties.title.maxLength" ); const minFrameWidth = readRequiredNumber( schema.$defs?.frame?.properties?.width?.minimum, "$defs.frame.properties.width.minimum" ); const minFrameHeight = readRequiredNumber( schema.$defs?.frame?.properties?.height?.minimum, "$defs.frame.properties.height.minimum" ); const displayModes = readRequiredStringArray( schema.$defs?.node?.properties?.displayMode?.enum, "$defs.node.properties.displayMode.enum" ); const layoutPresetKinds = readRequiredStringArray( schema.$defs?.layoutPreset?.properties?.kind?.enum, "$defs.layoutPreset.properties.kind.enum" ); const maxSerializedBytes = readRequiredNumber( limits.maxSerializedBytes, "workbenchSnapshotLimits.maxSerializedBytes" ); const snapshotStruct = renderSnapshotStruct(); const nodeStruct = renderNodeStruct(); const spaceStruct = renderSpaceStruct(); const frameStruct = renderFrameStruct(); const layoutBasisStructs = renderLayoutBasisStructs(); const lockedLayoutStructs = renderLockedLayoutStructs(); const displayModeType = renderDisplayModeType(displayModes); const layoutPresetType = renderLayoutPresetType(layoutPresetKinds); return `// Code generated by tools/scripts/generate-workbench-go-contract.mjs. DO NOT EDIT. package workbenchservice const ( \tworkbenchSnapshotContractSchemaVersion = ${schemaVersion} \tworkbenchSnapshotContractMaxSerializedBytes = ${maxSerializedBytes} \tworkbenchSnapshotContractMaxNodes = ${maxNodes} \tworkbenchSnapshotContractMaxNodeIDLength = ${maxNodeIDLength} \tworkbenchSnapshotContractMaxKindLength = ${maxKindLength} \tworkbenchSnapshotContractMaxTitleLength = ${maxTitleLength} \tworkbenchSnapshotContractMinFrameWidth = ${minFrameWidth} \tworkbenchSnapshotContractMinFrameHeight = ${minFrameHeight} ) ${snapshotStruct} ${nodeStruct} ${spaceStruct} ${frameStruct} ${layoutBasisStructs} ${lockedLayoutStructs} ${displayModeType} ${layoutPresetType} var workbenchSnapshotContractDisplayModes = map[WorkbenchSnapshotDisplayMode]struct{}{ ${displayModes.map((value) => `\tWorkbenchSnapshotDisplayMode${toGoExport(value)}: {},`).join("\n")} } `; } function formatGoSource(source) { return execFileSync("gofmt", { encoding: "utf8", input: source }); } function renderSnapshotStruct() { return `type WorkbenchSnapshot struct { \tSchemaVersion int \`json:"schemaVersion"\` \tNodes []WorkbenchSnapshotNode \`json:"nodes"\` \tNodeStack *[]string \`json:"nodeStack,omitempty"\` \tActiveNodeID *string \`json:"activeNodeId,omitempty"\` \tSpaces *[]WorkbenchSnapshotSpace \`json:"spaces,omitempty"\` \tActiveSpaceID *string \`json:"activeSpaceId,omitempty"\` \tLayoutBasis *WorkbenchSnapshotLayoutBasis \`json:"layoutBasis,omitempty"\` \tLockedLayout *WorkbenchSnapshotLockedLayout \`json:"lockedLayout,omitempty"\` \tMetadata map[string]interface{} \`json:"metadata,omitempty"\` }`; } function renderNodeStruct() { return `type WorkbenchSnapshotNode struct { \tID string \`json:"id"\` \tKind string \`json:"kind"\` \tTitle string \`json:"title"\` \tFrame WorkbenchSnapshotFrame \`json:"frame"\` \tDisplayMode *WorkbenchSnapshotDisplayMode \`json:"displayMode,omitempty"\` \tRestoreFrame *WorkbenchSnapshotFrame \`json:"restoreFrame,omitempty"\` \tIsMinimized *bool \`json:"isMinimized,omitempty"\` \tMinimizedAtUnixMs *int64 \`json:"minimizedAtUnixMs,omitempty"\` \tData interface{} \`json:"data,omitempty"\` \tAdapterState map[string]interface{} \`json:"adapterState,omitempty"\` }`; } function renderSpaceStruct() { return `type WorkbenchSnapshotSpace struct { \tID string \`json:"id"\` \tName string \`json:"name"\` \tNodeIDs []string \`json:"nodeIds"\` \tFrame *WorkbenchSnapshotFrame \`json:"frame,omitempty"\` \tData interface{} \`json:"data,omitempty"\` }`; } function renderFrameStruct() { return `type WorkbenchSnapshotFrame struct { \tX float64 \`json:"x"\` \tY float64 \`json:"y"\` \tWidth float64 \`json:"width"\` \tHeight float64 \`json:"height"\` }`; } function renderLayoutBasisStructs() { return `type WorkbenchSnapshotSize struct { \tWidth float64 \`json:"width"\` \tHeight float64 \`json:"height"\` } type WorkbenchSnapshotSafeArea struct { \tTop float64 \`json:"top"\` \tRight float64 \`json:"right"\` \tBottom float64 \`json:"bottom"\` \tLeft float64 \`json:"left"\` } type WorkbenchSnapshotLayoutConstraints struct { \tMinWidth float64 \`json:"minWidth"\` \tMinHeight float64 \`json:"minHeight"\` \tSurfacePadding float64 \`json:"surfacePadding"\` \tSafeArea WorkbenchSnapshotSafeArea \`json:"safeArea"\` } type WorkbenchSnapshotLayoutBasis struct { \tSurfaceSize WorkbenchSnapshotSize \`json:"surfaceSize"\` \tLayoutConstraints WorkbenchSnapshotLayoutConstraints \`json:"layoutConstraints"\` }`; } function renderLockedLayoutStructs() { return `type WorkbenchSnapshotNormalizedFrame struct { \tX float64 \`json:"x"\` \tY float64 \`json:"y"\` \tWidth float64 \`json:"width"\` \tHeight float64 \`json:"height"\` } type WorkbenchSnapshotLayoutPreset struct { \tKind WorkbenchSnapshotLayoutPresetKind \`json:"kind"\` } type WorkbenchSnapshotLockedLayout struct { \tPreset WorkbenchSnapshotLayoutPreset \`json:"preset"\` \tNodeIDs []string \`json:"nodeIDs"\` \tNormalizedFrames map[string]WorkbenchSnapshotNormalizedFrame \`json:"normalizedFrames,omitempty"\` }`; } function renderDisplayModeType(displayModes) { return `type WorkbenchSnapshotDisplayMode string const ( ${displayModes.map((value) => `\tWorkbenchSnapshotDisplayMode${toGoExport(value)} WorkbenchSnapshotDisplayMode = "${value}"`).join("\n")} )`; } function renderLayoutPresetType(layoutPresetKinds) { return `type WorkbenchSnapshotLayoutPresetKind string const ( ${layoutPresetKinds.map((value) => `\tWorkbenchSnapshotLayoutPresetKind${toGoExport(value)} WorkbenchSnapshotLayoutPresetKind = "${value}"`).join("\n")} ) var workbenchSnapshotContractLayoutPresetKinds = map[WorkbenchSnapshotLayoutPresetKind]struct{}{ ${layoutPresetKinds.map((value) => `\tWorkbenchSnapshotLayoutPresetKind${toGoExport(value)}: {},`).join("\n")} }`; } function parseLimits(source) { const objectMatch = source.match( /workbenchSnapshotLimits\s*=\s*\{(?[\s\S]*?)\}\s*as const/ ); if (!objectMatch?.groups?.body) { throw new Error( `Unable to locate workbenchSnapshotLimits in ${limitsPath}` ); } const limits = {}; for (const match of objectMatch.groups.body.matchAll( /(?[A-Za-z0-9_]+)\s*:\s*(?[^,\n]+),?/g )) { const key = match.groups?.key; const value = match.groups?.value; if (!key || !value) { continue; } limits[key] = evaluateNumericExpression(value); } return limits; } function evaluateNumericExpression(source) { const expression = source.replaceAll("_", "").trim(); if (!/^[0-9+\-*/ ().]+$/.test(expression)) { throw new Error(`Unsupported numeric expression: ${source}`); } return Function(`"use strict"; return (${expression});`)(); } function readRequiredNumber(value, path) { if (typeof value !== "number" || !Number.isFinite(value)) { throw new Error(`Expected finite number at ${path}`); } return value; } function readRequiredStringArray(value, path) { if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) { throw new Error(`Expected string array at ${path}`); } return value; } function readExistingText(path) { if (!existsSync(path)) { return ""; } return readFileSync(path, "utf8"); } function toGoExport(value) { return value .split(/[^A-Za-z0-9]+/) .filter(Boolean) .map((segment) => segment[0].toUpperCase() + segment.slice(1)) .join(""); }