diff --git a/.github/workflows/opf-ci.yml b/.github/workflows/opf-ci.yml index 0fd6d26..6732f12 100644 --- a/.github/workflows/opf-ci.yml +++ b/.github/workflows/opf-ci.yml @@ -42,3 +42,6 @@ jobs: - name: Verify npm package contents run: pnpm --filter @openpresentation/opf pack:dry-run + + - name: Verify packed package install + run: node packages/javascript/test/packed-install-smoke.mjs diff --git a/packages/javascript/README.md b/packages/javascript/README.md index 957b803..c57924c 100644 --- a/packages/javascript/README.md +++ b/packages/javascript/README.md @@ -61,6 +61,8 @@ import { validate, assertValid } from "@openpresentation/opf/validator"; import type { Presentation, Audience, Tone } from "@openpresentation/opf/types"; ``` +The root entry exports every schema, catalog, and validation helper for convenience. Prefer the focused subpaths above when a package consumer only needs one surface, so the root bundle's full catalog/schema payload is not loaded unnecessarily. + Validate catalog records locally: ```ts diff --git a/packages/javascript/package.json b/packages/javascript/package.json index 7073177..5b4eea3 100644 --- a/packages/javascript/package.json +++ b/packages/javascript/package.json @@ -68,6 +68,7 @@ "build": "pnpm run generate && tsup && node scripts/copy-spec.mjs", "typecheck": "pnpm run generate && tsc --noEmit", "test": "pnpm run build && node test/smoke.mjs", + "test:packed": "node test/packed-install-smoke.mjs", "pack:dry-run": "pnpm run build && npm pack --dry-run --cache /tmp/opf-npm-cache" }, "dependencies": { diff --git a/packages/javascript/test/packed-install-smoke.mjs b/packages/javascript/test/packed-install-smoke.mjs new file mode 100644 index 0000000..3d778d3 --- /dev/null +++ b/packages/javascript/test/packed-install-smoke.mjs @@ -0,0 +1,123 @@ +import assert from "node:assert/strict"; +import { execFile as execFileCallback } from "node:child_process"; +import { mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; + +const execFile = promisify(execFileCallback); +const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const tmpRoot = await mkdtemp(path.join(os.tmpdir(), "opf-packed-smoke-")); +const packDir = path.join(tmpRoot, "pack"); +const projectDir = path.join(tmpRoot, "project"); + +async function run(command, args, options = {}) { + try { + return await execFile(command, args, { + maxBuffer: 10 * 1024 * 1024, + ...options, + }); + } catch (error) { + const stdout = error.stdout ? `\nstdout:\n${error.stdout}` : ""; + const stderr = error.stderr ? `\nstderr:\n${error.stderr}` : ""; + error.message = `${error.message}${stdout}${stderr}`; + throw error; + } +} + +function assertTarIncludes(files, entry) { + assert.ok(files.includes(entry), `packed tarball should include ${entry}`); +} + +try { + await mkdir(packDir, { recursive: true }); + await mkdir(projectDir, { recursive: true }); + await writeFile( + path.join(projectDir, "package.json"), + `${JSON.stringify({ private: true, type: "module" }, null, 2)}\n`, + ); + + const packResult = await run("npm", ["pack", packageRoot, "--pack-destination", packDir]); + const tgzName = packResult.stdout.trim().split(/\r?\n/).at(-1); + assert.ok(tgzName?.endsWith(".tgz"), `npm pack did not return a tarball name: ${packResult.stdout}`); + + const tgzPath = path.join(packDir, tgzName); + const tarResult = await run("tar", ["-tzf", tgzPath]); + const files = tarResult.stdout.trim().split(/\r?\n/).filter(Boolean).sort(); + + for (const entry of [ + "package/LICENSE", + "package/README.md", + "package/package.json", + "package/dist/index.js", + "package/dist/schemas.js", + "package/dist/catalogs.js", + "package/dist/validator.js", + "package/dist/types.js", + "package/dist/spec/schemas/opf.schema.json", + "package/dist/spec/catalogs/audiences/board.json", + ]) { + assertTarIncludes(files, entry); + } + + assert.equal(files.includes("package/dist/spec/openapi.yaml"), false, "OpenAPI service contract should not ship in the npm package"); + assert.equal(files.some((file) => file.endsWith(".map")), false, "npm package should not ship source maps"); + + await run("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund", tgzPath], { cwd: projectDir }); + await writeFile( + path.join(projectDir, "smoke.mjs"), + `import assert from "node:assert/strict"; +import { + catalogs, + presentation, + validate, + validatePresentation, +} from "@openpresentation/opf"; +import { presentation as focusedPresentation } from "@openpresentation/opf/schemas"; +import { tones } from "@openpresentation/opf/catalogs"; +import { assertValid, validate as focusedValidate } from "@openpresentation/opf/validator"; +import * as typesRuntime from "@openpresentation/opf/types"; +import rawPresentation from "@openpresentation/opf/spec/schemas/opf.schema.json" with { type: "json" }; +import rawBoardAudience from "@openpresentation/opf/spec/catalogs/audiences/board.json" with { type: "json" }; + +assert.equal(presentation.$id, "https://openpresentation.org/schema/opf/v1"); +assert.equal(focusedPresentation.$id, presentation.$id); +assert.equal(rawPresentation.$id, presentation.$id); +assert.equal(rawBoardAudience.id, "board"); +assert.ok(catalogs.audiences.some((audience) => audience.id === "executives")); +assert.ok(tones.length > 0); +assert.deepEqual(Object.keys(typesRuntime), []); + +const validDeck = { + name: "Packed Package Smoke", + audience: ["executives"], + slides: [{ title: "Smoke Test", items: ["Root import", "Focused import", "Raw JSON import"] }], +}; + +assert.equal(validatePresentation(validDeck).valid, true); +assert.equal(validate(validDeck, "presentation").valid, true); +assert.equal(focusedValidate(validDeck, "presentation").valid, true); +assert.doesNotThrow(() => assertValid(validDeck)); + +const invalidDeck = { + name: "Invalid Packed Package Smoke", + slides: [{ type: "placeholder" }], +}; +const invalidResult = validatePresentation(invalidDeck); +assert.equal(invalidResult.valid, false, "invalid deck should fail validation"); +assert.ok(invalidResult.errors.length > 0, "invalid deck should return validation errors"); +`, + ); + await run(process.execPath, ["smoke.mjs"], { cwd: projectDir }); + + const { size } = await stat(tgzPath); + process.stdout.write(`Packed install smoke passed for ${tgzName} (${size} bytes).\n`); + process.stdout.write(`Packed tarball entries checked: ${files.length}.\n`); +} finally { + if (process.env.OPF_KEEP_PACKED_SMOKE_TMP) { + process.stdout.write(`Preserved packed smoke temp directory: ${tmpRoot}\n`); + } else { + await rm(tmpRoot, { recursive: true, force: true }); + } +} diff --git a/packages/javascript/tsup.config.ts b/packages/javascript/tsup.config.ts index 61aca20..fbd49c3 100644 --- a/packages/javascript/tsup.config.ts +++ b/packages/javascript/tsup.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ }, format: ["esm"], dts: true, - sourcemap: true, + sourcemap: false, clean: true, splitting: false, treeshake: true,