Skip to content

refactor(plugin-cli): validate probed plugin shape with Zod#1145

Merged
ascorbic merged 2 commits into
mainfrom
chore/lint-pipeline-zod
May 22, 2026
Merged

refactor(plugin-cli): validate probed plugin shape with Zod#1145
ascorbic merged 2 commits into
mainfrom
chore/lint-pipeline-zod

Conversation

@ascorbic
Copy link
Copy Markdown
Collaborator

What does this PR do?

Refactors probeAndAssemble in packages/plugin-cli/src/build/pipeline.ts to validate the imported default export of a probed plugin with a Zod schema instead of a hand-rolled if/throw chain. Removes the last 9 no-unsafe-type-assertion warnings flagged by oxlint in this file (the follow-up to #1140).

Design

  • New file: packages/plugin-cli/src/build/probe-schema.ts. Defines ProbedDefaultSchema plus HookEntrySchema / RouteEntrySchema. A normaliseEntry preprocess maps the bare-function form ("content:beforeSave": async (e, ctx) => {...}) to the config form ({ handler: fn }) so the schema only has to validate one shape per entry.
  • parseProbedDefault(pluginEntry, definition) in pipeline.ts runs the schema and translates the first Zod issue back to a BuildPipelineError matching the existing message format. Exported for test access.
  • safeStringify survives values JSON.stringify can't serialise (BigInt, cycles, functions, symbols) by falling back to describeShape.

Behaviour preserved

  • BuildPipelineError code (INVALID_PLUGIN_FORMAT) and message templates unchanged for documented field violations ("continue" or "abort", "finite number", "non-negative finite number").
  • Bare-function hooks/routes still accepted and normalised.
  • Unknown extra keys on the default export still pass through (matches the old code, which only read definition.hooks / definition.routes).
  • Non-record outer collections (hooks: [], hooks: null, etc.) still silently produce no harvested entries instead of failing the build.

Behaviour changed (deliberate)

  • Exotic-object hook/route entries (Date, RegExp, Promise, class instances, Map, ...) now produce the standard "must be a function or { handler, ... }. Got object." message rather than a misleading "missing handler" message reached by the schema descending into the wrong-shaped object.
  • BigInt / cyclic field values used to crash JSON.stringify with a raw TypeError; they now render through safeStringify (10n for BigInts, object / function / symbol for unrepresentable values).

Tests

Adds packages/plugin-cli/tests/probe-schema.test.ts with 32 cases covering valid shapes, hook validation errors, route validation errors, non-record collection coercion, and the safeStringify edge cases. The full plugin-cli suite (296 tests) passes.

Adversarial review

Two passes through the adversarial-reviewer sub-agent. First pass surfaced the BigInt crash and the misleading exotic-object error; both fixed. Second pass surfaced the Promise-as-entry Zod TypeError; fixed in the same pass by tightening normaliseEntry to also reject objects with non-Object.prototype prototypes. Defensive try/catch around safeParse left in place as belt-and-suspenders.

Closes #

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

(Mostly refactor; the exotic-object and BigInt error-message behaviour changes are deliberate improvements covered by new tests.)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes (no warnings introduced in plugin-cli; pre-existing warnings in packages/workerd/ from feat: Node.js plugin isolation via workerd sandbox #426 are out of scope)
  • pnpm test passes (@emdash-cms/plugin-cli: 296 tests)
  • pnpm format has been run
  • I have added/updated tests for my changes (probe-schema.test.ts, 32 new cases)
  • User-visible strings in the admin UI are wrapped for translation — N/A
  • I have added a changeset@emdash-cms/plugin-cli patch
  • New features link to an approved Discussion — N/A

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.7 (Innie via OpenCode)

Screenshots / test output

n/a

ascorbic added 2 commits May 22, 2026 14:13
Replaces the hand-rolled `if`/`throw` chain in `probeAndAssemble`
with a single Zod schema (`ProbedDefaultSchema` in
`probe-schema.ts`) and translates Zod issues back into the same
`BuildPipelineError` format plugin authors already see. Removes the
last 9 `no-unsafe-type-assertion` warnings in `pipeline.ts` and
clears the file's lint debt.

Key design choices:

- `normaliseEntry` (preprocess) maps bare-function hooks/routes to
  `{ handler }` so the schema only validates one shape per entry.
- Only plain objects (prototype is `Object.prototype` or null) reach
  per-field validation; exotic objects (Date, RegExp, Promise, class
  instances) are reduced to `null` so the entry-shape error fires
  instead of a misleading 'missing handler' message.
- Non-record outer collections (`hooks: []`, `hooks: null`, etc.)
  coerce to an empty record so the build doesn't fail on the kind of
  shapes the original `Object.keys` iteration silently no-op'd on.
- A `safeStringify` helper survives BigInts, cycles, functions, and
  symbols when embedding the offending value in the error message.

New tests in `probe-schema.test.ts` (32 cases) lock in the error-
message contract: code, exact message strings for the documented
field violations, and behavioural choices around exotic values.
Copilot AI review requested due to automatic review settings May 22, 2026 13:23
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 22, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache ad7897e May 22 2026, 01:26 PM

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 22, 2026

🦋 Changeset detected

Latest commit: ad7897e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@emdash-cms/plugin-cli Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-perf-coordinator ad7897e May 22 2026, 01:24 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 22, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground ad7897e May 22 2026, 01:26 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-i18n ad7897e May 22 2026, 01:24 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs ad7897e May 22 2026, 01:25 PM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 22, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1145

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1145

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1145

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1145

emdash

npm i https://pkg.pr.new/emdash@1145

create-emdash

npm i https://pkg.pr.new/create-emdash@1145

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1145

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1145

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1145

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1145

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1145

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1145

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1145

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1145

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1145

commit: ad7897e

@github-actions
Copy link
Copy Markdown
Contributor

Scope check

This PR changes 729 lines across 5 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors @emdash-cms/plugin-cli’s probe-time validation of a sandboxed plugin’s default export by replacing manual runtime checks in probeAndAssemble with Zod-based parsing/normalization, while preserving (and testing) the existing user-facing error message contract.

Changes:

  • Added probe-schema.ts with Zod schemas + preprocessors to normalize/validate probed hook/route entries.
  • Reworked probeAndAssemble to use parseProbedDefault() and added safer error-value rendering (safeStringify) for previously-crashy values (e.g., BigInt/cycles).
  • Added a focused test suite (probe-schema.test.ts) to lock in parsing behavior and error message strings; added a changeset.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pnpm-lock.yaml Updates lockfile (adds kysely@0.27.6 snapshot/entry).
packages/plugin-cli/tests/probe-schema.test.ts Adds coverage for default-export parsing, normalization, and error message contracts (incl. safeStringify cases).
packages/plugin-cli/src/build/probe-schema.ts Introduces Zod schemas and preprocessors for probed plugin default export validation.
packages/plugin-cli/src/build/pipeline.ts Replaces hand-rolled validation with Zod parse + error translation, and adds safeStringify + assembly helpers.
.changeset/slick-peaches-wear.md Documents the patch-level change for the plugin CLI release notes.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +42 to +47
* - Anything else (built-ins like `Date`, `RegExp`, `Promise`, `Map`,
* author-defined class instances, primitives) is reduced to `null`
* so the schema produces a single "expected object" issue at the
* entry root. Without this, the schema would reach into the wrong-
* shaped object for `handler` and report a misleading "missing
* handler" issue.
Comment on lines +5 to +11
Refactors the build pipeline's runtime validation of the probed plugin's
default export to use a Zod schema. Error messages keep the same format
(`hook "X" must be a function or { handler, ... }`, `hook "X" has
invalid FIELD VALUE (...)`). Exotic-object entries (Date, RegExp,
Promise, class instances) now produce the wrong-shape error instead of
falling through to a misleading "missing handler" error. BigInt /
cyclic-object / function / symbol field values are rendered safely in
@ascorbic ascorbic merged commit 463c7a2 into main May 22, 2026
41 checks passed
@ascorbic ascorbic deleted the chore/lint-pipeline-zod branch May 22, 2026 15:00
@emdashbot emdashbot Bot mentioned this pull request May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants