feat: browser-safe validation (schema mirrors, no runtime fs) - #25
Conversation
The dspack-studio composer runs transformFromJson, validateCatalog, and loadProfile in the browser for live fidelity feedback; readFileSync at module scope made the library surface unbundleable. The reviewable .json schema documents stay; committed TS mirrors (catalog-meta.ts, profile-schema.ts) are what the runtime imports, with schema-mirrors.test.ts as the drift gate and browser-boundary.test.ts banning Node built-ins across src/transform, src/validate, src/targets. Build no longer copies JSON into dist. CLI untouched; catalogs and reports byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Final release verification (train pass)
0.4.1 release notes:
Publish after merge (manual): |
There was a problem hiding this comment.
Pull request overview
This PR makes the library’s validation/profile-loading surface browser-bundleable by removing runtime filesystem reads of JSON schema documents and replacing them with committed TypeScript “schema mirror” modules, backed by tests that prevent mirror/JSON drift and enforce a no-Node-builtins boundary for browser-targeted source directories.
Changes:
- Replace runtime
node:fsschema reads with TS mirror modules for catalog meta-schemas and the profile schema. - Add drift tests to assert
.jsonschema documents remain exactly equal to their TS mirrors. - Add a “browser boundary” test to forbid Node built-in imports in
src/transform,src/validate, andsrc/targets; simplify the build step accordingly.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/validate/meta/catalog-meta.ts | Adds TS mirrors for versioned catalog meta-schemas used at runtime. |
| src/validate/ajv.ts | Switches gate-2 meta-schema loading from runtime FS reads to TS mirror import. |
| src/transform/profile-schema.ts | Adds TS mirror of the profile JSON schema for browser-safe runtime use. |
| src/transform/profile-load.ts | Removes runtime FS schema load; imports/re-exports the TS mirror to keep public export stable. |
| src/schema-mirrors.test.ts | Adds drift tests ensuring JSON schema files and TS mirrors stay identical. |
| src/browser-boundary.test.ts | Adds a boundary test intended to ensure browser-bundle safety of library modules. |
| scripts/pack-test.sh | Updates commentary to reflect “schemas compiled in” packaging expectations. |
| package.json | Simplifies build (removes JSON copy steps) and bumps version to 0.4.1. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const source = readFileSync(file, "utf8"); | ||
| const imports = [...source.matchAll(/from\s+"([^"]+)"|import\s*\(\s*"([^"]+)"\s*\)|require\s*\(\s*"([^"]+)"\s*\)/g)].map( | ||
| (m) => m[1] ?? m[2] ?? m[3], | ||
| ); |
| import { readFileSync, readdirSync } from "node:fs"; | ||
| import { join } from "node:path"; | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| const BOUNDARY_DIRS = ["src/transform", "src/validate", "src/targets"]; | ||
|
|
||
| // Bare-specifier built-ins that would slip past a `node:` prefix check. | ||
| const BARE_BUILTINS = new Set([ | ||
| "fs", "path", "url", "os", "crypto", "util", "stream", "buffer", | ||
| "http", "https", "net", "tls", "child_process", "worker_threads", | ||
| ]); |
What / why
Phase 0 item 5 of the dspack-studio composer plan: the studio's mapper needs live in-browser fidelity feedback, which means running
transformFromJson,validateCatalog, andloadProfilein a browser bundle. Bothsrc/validate/ajv.tsandsrc/transform/profile-load.tsread schema JSON at runtime viafileURLToPath+readFileSyncat module scope, which makes the library surface unbundleable for the browser.Approach: mirror + drift test (the JSONs stay)
The
.jsonfiles remain the reviewable schema documents — docs/PROFILES.md linkssrc/transform/profile.v1.schema.json, and the README points reviewers atsrc/validate/meta/. What changes is how the runtime reaches them:src/validate/meta/catalog-meta.ts— committed TS mirror exporting both catalog-shape meta-schemas (catalogMetaSchemas, keyed byA2uiVersion);ajv.tsgate 2 imports it instead of reading files.src/transform/profile-schema.ts— committed TS mirror ofprofile.v1.schema.json;profile-load.tsimports and re-exports it, so the publicprofileSchemaexport is unchanged.src/schema-mirrors.test.ts— the drift gate: reads each.jsonwithnode:fs(fs is fine in tests) and assertstoStrictEqualwith the TS mirror. A failure means one side was edited without the other.cpsteps removed; dist no longer contains or reads JSON at runtime.Fail-first: boundary test output on unchanged code
src/browser-boundary.test.ts(modeled on dspack-gen's core-boundary test) statically scans every non-test module undersrc/transform/,src/validate/, andsrc/targets/and forbidsnode:*imports plus bare-specifier built-ins. Run against unchangedmain:Exactly the two known offenders; no other module in scope imports Node built-ins, so the boundary covers all three target dirs at full width (no carve-outs needed).
Verification
npx vitest run: 94 passed (94) — 74 existing + 17 boundary + 3 driftnpm run test:pack: pack-and-install, profile, and bin smokes all OK from the packed tarball with the trimmed buildnpm run build: clean; dist contains zero.jsonfiles and nonode:imports outsidecli.jsnpm run check:sync: in syncesbuild dist/index.js --bundle --platform=browsersucceeds (would fail on any unresolvednode:import)Scope boundaries
profileSchemapublic export unchanged.src/cli.tsis the Node CLI and stays out of the boundary — it owns filesystem I/O by design.node:fsfreely (they run under vitest).Suggested version: 0.4.1.
🤖 Generated with Claude Code