diff --git a/scripts/verify-packed-manifests.mjs b/scripts/verify-packed-manifests.mjs index 8fa0da0f5f..aa4ac03064 100644 --- a/scripts/verify-packed-manifests.mjs +++ b/scripts/verify-packed-manifests.mjs @@ -1,7 +1,15 @@ #!/usr/bin/env node import { execFileSync } from "node:child_process"; -import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; import { extname, join, posix } from "node:path"; import { tmpdir } from "node:os"; import { fileURLToPath } from "node:url"; @@ -272,25 +280,139 @@ function verifyPackedWorkspace(workspace, filename) { verifyPackedJavaScriptImports(workspace, filename, packedFiles); } -function verifyWorkspace(workspace) { +export function packageExportSpecifier(packageName, exportKey) { + return exportKey === "." ? packageName : `${packageName}/${exportKey.replace(/^\.\//, "")}`; +} + +export function listPackedExportContracts(packedWorkspaces) { + return packedWorkspaces.flatMap(({ packedPackage }) => + Object.entries(packedPackage.exports ?? {}).map(([exportKey, target]) => ({ + specifier: packageExportSpecifier(packedPackage.name, exportKey), + typechecked: Boolean(target?.types), + })), + ); +} + +function writeConsumerFixture(packDir, packedWorkspaces) { + const fixtureDir = join(packDir, "consumer"); + mkdirSync(fixtureDir); + const rootPackage = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")); + const dependencies = Object.fromEntries( + packedWorkspaces.map(({ filename, packedPackage }) => [packedPackage.name, `file:${filename}`]), + ); + dependencies.typescript = rootPackage.devDependencies.typescript; + dependencies["@types/node"] = rootPackage.devDependencies["@types/node"]; + writeFileSync( + join(fixtureDir, "package.json"), + JSON.stringify( + { name: "packed-consumer", private: true, type: "module", dependencies }, + null, + 2, + ), + ); + + const contracts = listPackedExportContracts(packedWorkspaces); + const typeImports = contracts + .filter(({ typechecked }) => typechecked) + .map(({ specifier }) => `import ${JSON.stringify(specifier)};`) + .join("\n"); + writeFileSync(join(fixtureDir, "consumer.ts"), `${typeImports}\n`); + writeFileSync( + join(fixtureDir, "tsconfig.json"), + JSON.stringify( + { + compilerOptions: { + target: "ES2022", + module: "NodeNext", + moduleResolution: "NodeNext", + lib: ["ES2022", "DOM", "DOM.Iterable"], + strict: true, + skipLibCheck: true, + noEmit: true, + }, + include: ["consumer.ts"], + }, + null, + 2, + ), + ); + + const specifiers = contracts.map(({ specifier }) => specifier); + writeFileSync( + join(fixtureDir, "consumer-smoke.mjs"), + `const specifiers = ${JSON.stringify(specifiers, null, 2)};\n` + + `for (const specifier of specifiers) import.meta.resolve(specifier);\n` + + `const sdk = await import("@hyperframes/sdk");\n` + + `if (typeof sdk.openComposition !== "function") throw new Error("SDK root export missing");\n` + + `const memory = await import("@hyperframes/sdk/adapters/memory");\n` + + `if (typeof memory.createMemoryAdapter !== "function") throw new Error("memory adapter missing");\n` + + `const fsAdapter = await import("@hyperframes/sdk/adapters/fs");\n` + + `if (typeof fsAdapter.createFsAdapter !== "function") throw new Error("fs adapter missing");\n` + + `await import("@hyperframes/core");\n` + + `await import("@hyperframes/parsers");\n` + + `await import("@hyperframes/lint");\n` + + `await import("@hyperframes/studio-server");\n` + + `console.log(\`Resolved \${specifiers.length} packed exports and loaded public SDK adapters.\`);\n`, + ); + return fixtureDir; +} + +function verifyPackedConsumer(packDir, packedWorkspaces) { + const fixtureDir = writeConsumerFixture(packDir, packedWorkspaces); + execFileSync("bun", ["install", "--ignore-scripts"], { + cwd: fixtureDir, + encoding: "utf8", + stdio: "pipe", + }); + execFileSync(process.execPath, [join(fixtureDir, "node_modules", "typescript", "bin", "tsc")], { + cwd: fixtureDir, + encoding: "utf8", + stdio: "pipe", + }); + const smokeOutput = execFileSync("node", ["consumer-smoke.mjs"], { + cwd: fixtureDir, + encoding: "utf8", + }); + const cliOutput = execFileSync( + join(fixtureDir, "node_modules", ".bin", "hyperframes"), + ["--help"], + { + cwd: fixtureDir, + encoding: "utf8", + env: { ...process.env, HYPERFRAMES_TELEMETRY_DISABLED: "1" }, + }, + ); + if (!cliOutput.toLowerCase().includes("hyperframes")) { + throw new Error("Packed CLI help did not identify HyperFrames"); + } + console.log(smokeOutput.trim()); + console.log("Verified clean packed consumer install, type resolution, and CLI startup."); +} + +function packAndVerifyWorkspace(workspace, packDir) { const sourcePackageJson = readWorkspacePackage(workspace); - if (sourcePackageJson.private) return; + if (sourcePackageJson.private) return null; assertPublishedExportsMatchSource(workspace, sourcePackageJson); + const filename = packWorkspace(workspace, packDir); + verifyPackedWorkspace(workspace, filename); + const packedPackage = readPackedPackage(filename); + console.log(`Verified ${workspace}: packed manifest is publish-safe.`); + return { workspace, filename, packedPackage }; +} +function main() { const packDir = mkdtempSync(join(tmpdir(), "hyperframes-pack-")); try { - verifyPackedWorkspace(workspace, packWorkspace(workspace, packDir)); - console.log(`Verified ${workspace}: packed manifest is publish-safe.`); + const packedWorkspaces = listWorkspacePackageDirs() + .map((workspace) => packAndVerifyWorkspace(workspace, packDir)) + .filter(Boolean); + verifyPackedConsumer(packDir, packedWorkspaces); } finally { rmSync(packDir, { force: true, recursive: true }); } } -function main() { - listWorkspacePackageDirs().forEach(verifyWorkspace); -} - if (process.argv[1] === fileURLToPath(import.meta.url)) { main(); } diff --git a/scripts/verify-packed-manifests.test.mjs b/scripts/verify-packed-manifests.test.mjs index 3c5713a75b..1747f0cf1d 100644 --- a/scripts/verify-packed-manifests.test.mjs +++ b/scripts/verify-packed-manifests.test.mjs @@ -4,9 +4,38 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { describe, it } from "node:test"; -import { listPackedJavaScriptImportIssues } from "./verify-packed-manifests.mjs"; +import { + listPackedExportContracts, + listPackedJavaScriptImportIssues, + packageExportSpecifier, +} from "./verify-packed-manifests.mjs"; describe("packed manifest verifier", () => { + it("derives consumer specifiers from the packed export map", () => { + assert.equal(packageExportSpecifier("@hyperframes/sdk", "."), "@hyperframes/sdk"); + assert.equal( + packageExportSpecifier("@hyperframes/sdk", "./adapters/fs"), + "@hyperframes/sdk/adapters/fs", + ); + assert.deepEqual( + listPackedExportContracts([ + { + packedPackage: { + name: "@hyperframes/example", + exports: { + ".": { import: "./dist/index.js", types: "./dist/index.d.ts" }, + "./runtime": "./dist/runtime.js", + }, + }, + }, + ]), + [ + { specifier: "@hyperframes/example", typechecked: true }, + { specifier: "@hyperframes/example/runtime", typechecked: false }, + ], + ); + }); + function withPackedFiles(files, packedFiles, callback) { const dir = mkdtempSync(join(tmpdir(), "hyperframes-pack-test-")); try {