feat(tooling): build-time codegen convention — perry.codegen + ajv/standalone (#1680) - #1801
Merged
Merged
Conversation
…andalone (#1680) Phase 2 of #1677. Many libraries that would JIT a function at runtime (`ajv`, Prisma, Drizzle, …) ship an eval-free build-time mode that emits plain source. Where that exists, an AOT compiler should consume the generated output instead of running a JIT — zero new evaluation infra, highest ROI. Adds a `perry.codegen` convention to the host package.json: an array of build commands (bare string or `{ command, label }`) Perry runs before module collection, in the package.json's directory, then compiles the generated output natively. Read ONLY from the host package.json — never a dependency's — same trust boundary as `perry.compilePackages` (a transitive dep can't smuggle in a build command). A failing step fails the build with its captured stdout/stderr. `--no-codegen` / `PERRY_SKIP_CODEGEN=1` skips the steps for reproducible / sandboxed builds whose output is committed. - types.rs: `CodegenStep { label, command }` + `codegen_steps` / `codegen_dir` on `CompilationContext`. - host_config.rs: parse `perry.codegen` from the host package.json. - codegen_steps.rs: the runner (sh -c in codegen_dir, bail on failure) + unit tests (runs/produces output, skip, failure-diagnostics, no-op). - compile.rs: run the steps between config load and module collection. - `--no-codegen` CLI flag (+ dev.rs / run/mod.rs construction sites). Proof: `test-files/test_ajv_standalone.ts` imports a vendored `ajv/standalone` validator (committed, real ajv output) and validates inputs — byte-for-byte parity vs `node --experimental-strip-types`, and the compiled binary links zero V8 symbols. The end-to-end hook (declared step regenerates the validator, then Perry compiles it) was verified locally with ajv installed. Docs: project-config.md documents the convention with the ajv worked example (gen script + import) and the same pattern for prisma generate / drizzle-kit introspect / kysely-codegen / Vue SFC. flags.md gets `--no-codegen`. Scope note: the committed sample uses a structural schema (object shape / required / additionalProperties). ajv's numeric (`minimum`), integer (`% 1`), per-property `typeof`, and `validate.errors` (function-object property) paths surfaced separate Perry codegen-correctness bugs, filed as follow-ups — they are orthogonal to this build-convention work.
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 2 of #1677 — build-time codegen convention (
perry.codegen) + ajv/standaloneCloses #1680. Stacked on #1678 + #1679 (both merged); rebased onto current
main.What
Many libraries that would JIT a function at runtime (
ajv, Prisma, Drizzle, kysely, Vue SFC) ship an eval-free build-time mode that emits plain source. Where that exists, an AOT compiler should consume the generated output instead of running a JIT — zero new evaluation infra, highest ROI (the issue's framing).This adds a
perry.codegenconvention to the hostpackage.json: an array of build commands Perry runs before module collection, then compiles the generated output natively.{ "perry": { "codegen": [ { "label": "ajv validators", "command": "node scripts/generate-validators.mjs" } ] } }{ command, label }. Commands run in declaration order, cwd = the package.json's directory.compilePackages(a transitive dep can't smuggle in a build command).--no-codegen/PERRY_SKIP_CODEGEN=1skips the steps (reproducible / sandboxed builds with committed output).Acceptance
ajv/standalone, compiles, byte-parity vs node, no V8.test-files/test_ajv_standalone.tsimports a vendored realajv/standalonevalidator and validates 8 inputs — byte-for-byte withnode --experimental-strip-types; the binary links 0 V8 symbols (nm | grep -c v8:: → 0).docs/src/getting-started/project-config.md(#### codegen): the declaration format, the ajv worked example (gen script + import), and the same pattern forprisma generate/drizzle-kit introspect/kysely-codegen/ Vue SFC.--no-codegenadded todocs/src/cli/flags.md.generated/dir → the step runs →validator.cjsappears → compiles → correct output.Implementation
CodegenStep+codegen_steps/codegen_dironCompilationContext(types.rs) · parseperry.codegen(host_config.rs) · runner with 4 unit tests (codegen_steps.rs: runs/produces, skip, failure-diagnostics, no-op) · wired between config-load and module-collection (compile.rs) ·--no-codegenflag.Scope note (follow-ups filed)
The committed sample uses a structural schema (object shape / required / additionalProperties). While testing, ajv's denser generated code surfaced separate Perry codegen-correctness bugs — numeric
minimum, integer% 1, per-propertytypeofof boolean/number values, andvalidate.errors(custom property on a function object). These are orthogonal to the build-convention work here and are filed as granular follow-up issues; they don't block consumingajv/standaloneoutput for structural validation.Note:
parity/compile-smokeare tag-gated (don't run on PRs) — the release sweep is the real Phase-2 gate before #1681 starts.