diff --git a/packages/flow-schema/package.json b/packages/flow-schema/package.json index c078cc7..397d85b 100644 --- a/packages/flow-schema/package.json +++ b/packages/flow-schema/package.json @@ -17,6 +17,10 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, + "./structure": { + "types": "./dist/structure.d.ts", + "import": "./dist/structure.js" + }, "./schema/v1": "./dist/generated/flow.v1.schema.json" }, "files": [ diff --git a/packages/flow-schema/src/index.ts b/packages/flow-schema/src/index.ts index 137e41f..6700aa1 100644 --- a/packages/flow-schema/src/index.ts +++ b/packages/flow-schema/src/index.ts @@ -11,12 +11,15 @@ // in-repo copy so the Phase 3 swap is a dependency change, not a code // change. -import { Ajv2020 } from 'ajv/dist/2020.js'; -import type { ErrorObject } from 'ajv'; - // The schema as a generated TS module, not a JSON import: import // attributes (`with { type: 'json' }`) don't survive every consumer // bundler. The raw JSON is still shipped at the `./schema/v1` export. +// +// Note: Ajv-based structural validation (`validateStructure`) deliberately +// does NOT live here — it compiles a schema via runtime code generation, +// which crashes code-gen-disallowed runtimes (Cloudflare Workers) at +// import time. It lives behind the `@wavekat/flow-schema/structure` +// subpath so this barrel stays Ajv-free and safe to bundle on the edge. import schema from './generated/schema.js'; // Model: generated types + hand-written constants & helpers. @@ -50,20 +53,3 @@ export { setNodeValue, stampIdentity, } from './mutate.js'; - -const ajv = new Ajv2020({ allErrors: true, strict: false }); -const validateFn = ajv.compile(schema); - -export type StructuralResult = - | { valid: true } - | { valid: false; errors: ErrorObject[] }; - -/** - * Validate an already-parsed document against the normative JSON Schema - * (structure only — no reachability / exit-wiring / hours semantics; use - * `checkFlow` / `validateFlow` for those). - */ -export function validateStructure(doc: unknown): StructuralResult { - const valid = validateFn(doc); - return valid ? { valid: true } : { valid: false, errors: validateFn.errors ?? [] }; -} diff --git a/packages/flow-schema/src/structure.ts b/packages/flow-schema/src/structure.ts new file mode 100644 index 0000000..2d9cc2d --- /dev/null +++ b/packages/flow-schema/src/structure.ts @@ -0,0 +1,40 @@ +// JSON-Schema structural validation — kept OUT of the package barrel. +// +// Ajv compiles a schema by *generating JavaScript source and `new +// Function`-ing it* into a validator (its whole speed model is JIT). Any +// runtime that disallows code-generation-from-strings — notably the +// Cloudflare Workers isolate the platform API runs on — throws +// `EvalError: Code generation from strings disallowed` the moment this +// module is evaluated. Because the compile happens at import time, merely +// importing the barrel used to crash such consumers at startup even when +// they never called `validateStructure`. +// +// So this lives behind the `@wavekat/flow-schema/structure` subpath: the +// main entry (`.`) stays pure hand-written code with no Ajv in its module +// graph, and only Node-side consumers that actually want meta-schema +// structural validation opt into the Ajv dependency. This subpath is NOT +// safe to import from a Workers/edge bundle. + +import { Ajv2020 } from 'ajv/dist/2020.js'; +import type { ErrorObject } from 'ajv'; + +import schema from './generated/schema.js'; + +const ajv = new Ajv2020({ allErrors: true, strict: false }); +const validateFn = ajv.compile(schema); + +export type StructuralResult = + | { valid: true } + | { valid: false; errors: ErrorObject[] }; + +/** + * Validate an already-parsed document against the normative JSON Schema + * (structure only — no reachability / exit-wiring / hours semantics; use + * `checkFlow` / `validateFlow` for those). + * + * Node-side only — see the module header on Workers incompatibility. + */ +export function validateStructure(doc: unknown): StructuralResult { + const valid = validateFn(doc); + return valid ? { valid: true } : { valid: false, errors: validateFn.errors ?? [] }; +} diff --git a/packages/flow-schema/test/conformance.test.ts b/packages/flow-schema/test/conformance.test.ts index 31b7656..ce64b23 100644 --- a/packages/flow-schema/test/conformance.test.ts +++ b/packages/flow-schema/test/conformance.test.ts @@ -13,7 +13,8 @@ import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { parse } from 'yaml'; -import { checkFlow, validateStructure } from '../src/index.js'; +import { checkFlow } from '../src/index.js'; +import { validateStructure } from '../src/structure.js'; const here = dirname(fileURLToPath(import.meta.url)); const corpusRoot = resolve(here, '..', '..', '..', 'conformance', 'v1');