Skip to content

feat(core): forward portableTextBlocks and fieldWidgets for standard/sandboxed plugins#1484

Merged
ascorbic merged 3 commits into
emdash-cms:mainfrom
swissky:forward-plugin-portable-text-blocks
Jun 18, 2026
Merged

feat(core): forward portableTextBlocks and fieldWidgets for standard/sandboxed plugins#1484
ascorbic merged 3 commits into
emdash-cms:mainfrom
swissky:forward-plugin-portable-text-blocks

Conversation

@swissky

@swissky swissky commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Standard- and sandboxed-format plugins can declare portableTextBlocks and fieldWidgets, but adaptSandboxEntry only copied adminPages / adminWidgets onto the resolved admin config — so those two declarative fields were silently dropped for every non-native plugin. The admin editor already reads them from the manifest (slash-menu entries + Block Kit forms), so the data simply never arrived: a non-native plugin could never contribute a custom Portable Text block or field widget.

This forwards both fields the same way adminPages / adminWidgets already flow:

  • plugins/adapt-sandbox-entry.ts — copy descriptor.portableTextBlocks / fieldWidgets onto admin.
  • astro/integration/runtime.tsPluginDescriptor carries the two fields (typed via the existing PortableTextBlockConfig / FieldWidgetConfig).
  • astro/integration/virtual-modules.ts — the in-process adaptSandboxEntry(...) call and the sandboxed-entry codegen pass them through.
  • emdash-runtime.tsSandboxedPluginEntry and the marketplace manifest cache carry them; the three adaptSandboxEntry call sites forward them; the sandboxed-entry and marketplace manifest loops emit them (this also resolves the pre-existing TODO in the sandboxed-plugin loop).

The site-side render component (componentsEntry) stays native-only — out of scope here.

Closes #1483

Type of change

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

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Cursor + Claude Opus 4.8

Screenshots / test output

```
$ pnpm --filter emdash typecheck # tsgo --noEmit — clean
$ pnpm --filter emdash exec vitest run tests/unit/plugins/adapt-sandbox-entry.test.ts
Test Files 1 passed (1)
Tests 28 passed (28) # +3 new: portable text blocks, field widgets, omitted-config
```

oxlint + prettier --check clean on the changed files. No admin UI strings were added (i18n N/A).

…sandboxed plugins

Standard- and sandboxed-format plugins could declare adminPages and
adminWidgets, but their declarative portableTextBlocks and fieldWidgets were
dropped in adaptSandboxEntry, so only native-format plugins surfaced them.
The admin editor already reads both from the manifest (slash-menu entries +
Block Kit forms), so the data simply never arrived for non-native plugins.

Forward them like the existing admin config:
- adaptSandboxEntry copies descriptor.portableTextBlocks / fieldWidgets onto admin
- PluginDescriptor and SandboxedPluginEntry carry the fields
- the in-process and codegen plugin builders pass them through
- the sandboxed-entry and marketplace manifest loops emit them

The site-side render component (componentsEntry) remains native-only. Resolves
the pre-existing TODO in the sandboxed-plugin manifest loop.
@changeset-bot

changeset-bot Bot commented Jun 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4571fac

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

This PR includes changesets to release 14 packages
Name Type
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/admin Minor
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds 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

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet area/core size/M labels Jun 15, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jun 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@1484

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@1484

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@1484

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@1484

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@1484

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@1484

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 4571fac

@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Jun 15, 2026
@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Jun 16, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator

Sorry about the label churn: I'm trying to fix the review bot

@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Jun 16, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

event probe: COMMENT

emdashbot[bot]
emdashbot Bot previously approved these changes Jun 16, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

event probe: APPROVE

emdashbot[bot]
emdashbot Bot previously requested changes Jun 16, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

event probe: REQUEST_CHANGES

@emdashbot emdashbot Bot dismissed stale reviews from themself June 16, 2026 11:36

automated event probe — dismissing

@swissky

swissky commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Sorry about the label churn: I'm trying to fix the review bot

@ascorbic easy. Starting 2026 we are all slaves of our bots

@ascorbic ascorbic removed the bot:review Trigger an emdashbot code review on this PR label Jun 16, 2026
@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Jun 16, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔬 Local test of hardened postReview (bad-anchor finding forces the body-only fallback).


Findings

  • [suggestion] does/not/exist.ts:99999

    Intentionally bad anchor to exercise the fallback path.

@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Jun 16, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jun 16, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator

/review

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice catch on the dropped portableTextBlocks/fieldWidgets — the in-process standard-format path (config-time descriptor → virtual-modules.tsadaptSandboxEntry) is fully wired by this PR and that's the case the new tests exercise. Good.

However, the marketplace/R2 branches added in emdash-runtime.ts (reading bundle.manifest.admin?.portableTextBlocks / ?.fieldWidgets at lines ~943 and ~1884) are effectively dead — three independent layers strip those fields before they ever reach the runtime:

  1. pluginAdminConfigSchema in plugins/manifest-schema.ts declares fieldWidgets but not portableTextBlocks at all. With a plain z.object(...), Zod's default behaviour strips unknown keys, so even a hand-crafted manifest.json carrying admin.portableTextBlocks would have it removed by safeParse() in loadBundleFromR2 and extractBundle.
  2. extractManifest() in both core/src/cli/commands/bundle-utils.ts and plugin-cli/src/bundle/utils.ts explicitly writes only { settingsSchema, pages, widgets } into the bundled manifest. Neither portableTextBlocks nor fieldWidgets is ever copied into a manifest.json written into a bundle.
  3. core/src/cli/commands/bundle.ts:336-345 hard-errors (consola.error + process.exit(1)) when a plugin declares portableTextBlocks, telling the author it requires native/trusted mode and cannot be bundled. And plugin-cli/src/bundle/api.ts:217 plus core/src/cli/commands/bundle.ts:568 still emit warnings to the same effect.

So for the actual marketplace path the PR description calls out ("the marketplace manifest cache carry them"), the new code can never read a non-undefined value. The new tests don't cover this path, and there's no integration test that round-trips a portableTextBlocks-carrying plugin through extractManifest → pluginManifestSchema → loadBundleFromR2.

To actually fix the marketplace branch, the PR would also need to (a) add portableTextBlocks to pluginAdminConfigSchema, (b) copy both fields in both extractManifest() implementations, (c) remove the bundler error in bundle.ts:336-345 and update the stale "will be ignored in sandboxed plugins" warnings in bundle.ts:568 and plugin-cli/src/bundle/api.ts:217, and (d) add a test that actually exercises an R2-loaded bundle. As-is, the in-process standard-format fix lands but the changeset and the resolved-TODO claim oversell the marketplace coverage.

One small nit on adminMode for sandboxed/marketplace plugins as well — see the inline note.

Comment thread packages/core/src/emdash-runtime.ts Outdated
w.size === "full" || w.size === "half" || w.size === "third" ? w.size : undefined,
})),
portableTextBlocks: bundle.manifest.admin?.portableTextBlocks,
fieldWidgets: bundle.manifest.admin?.fieldWidgets,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These two lines for marketplace bundles can't fire in practice. pluginManifestSchema in plugins/manifest-schema.ts doesn't include portableTextBlocks in pluginAdminConfigSchema (line 201–226), so Zod's default object behaviour silently strips it during pluginManifestSchema.safeParse(...) inside loadBundleFromR2. And extractManifest() in both core/src/cli/commands/bundle-utils.ts:161 and packages/plugin-cli/src/bundle/utils.ts:157 only copies { settingsSchema, pages, widgets }portableTextBlocks and fieldWidgets are never written into the bundled manifest in the first place. On top of that, core/src/cli/commands/bundle.ts:336-345 hard-errors when a plugin declares portableTextBlocks, so the bundler refuses to produce such a bundle at all.

To make the marketplace branch real (rather than a no-op that just typechecks), the PR also needs to: extend pluginAdminConfigSchema to validate/preserve both fields, update both extractManifest() implementations to copy them, drop the hard-error in bundle.ts, and update the stale warning at plugin-cli/src/bundle/api.ts:217 and core/src/cli/commands/bundle.ts:568 ("will be ignored in sandboxed plugins"). Worth adding a round-trip test through extractManifest → pluginManifestSchema → loadBundleFromR2 to lock the behaviour in.

Comment thread packages/core/src/emdash-runtime.ts Outdated
w.size === "full" || w.size === "half" || w.size === "third" ? w.size : undefined,
})),
portableTextBlocks: bundle.manifest.admin?.portableTextBlocks,
fieldWidgets: bundle.manifest.admin?.fieldWidgets,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same dead-branch issue as the earlier marketplace sync site at line 943-944: the manifest schema strips portableTextBlocks, neither extractManifest() writes the field, and the bundler currently refuses to emit it. This block will always observe undefined until the bundle/parse pipeline is updated.

version: meta.version,
enabled,
sandboxed: true,
adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor — adminMode for sandboxed and marketplace plugins is still computed only from hasAdminPages || hasWidgets. The configured-plugins branch above (line 2097-2102) has the same shape. With this PR, a plugin can now contribute portable-text blocks or field widgets and nothing else, in which case adminMode stays "none". That's currently harmless because getPluginBlocks in packages/admin/src/lib/pluginBlocks.ts reads portableTextBlocks straight from the manifest regardless of adminMode, and the sidebar only consults adminMode for admin-pages rendering. Worth a quick comment noting that adminMode === "none" doesn't imply "no admin contributions" anymore, so future readers don't gate on it.


const result = adaptSandboxEntry(def, descriptor);

expect(result.admin.portableTextBlocks).toBeUndefined();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The three new cases cover adaptSandboxEntry in isolation, which is fine, but the PR's headline claim ("the marketplace manifest cache carry them") isn't exercised anywhere. A test that drives a synthetic PluginBundle through pluginManifestSchema.safeParse → reconcileManifestAccess and asserts that admin.portableTextBlocks / admin.fieldWidgets survive would have caught the issue that the manifest schema strips portableTextBlocks. Worth adding before claiming the marketplace path works.

Comment thread .changeset/forward-plugin-blocks.md Outdated

Standard- and sandboxed-format plugins could already declare admin pages and dashboard widgets, but their declarative Portable Text block types and field widgets were dropped during adaptation — only native-format plugins surfaced them. Since the admin editor reads these from the manifest, the slash-menu entries and Block Kit forms never appeared for non-native plugins.

`adaptSandboxEntry` now forwards both, and the admin manifest is emitted for sandboxed and marketplace plugins too, so a plugin of any format can contribute Portable Text blocks and field widgets. The site-side render component (`componentsEntry`) still requires native format, which is unchanged.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The changeset says "the admin manifest is emitted for sandboxed and marketplace plugins too" — that's accurate for sandboxed-from-config (the virtual-modules.ts codegen path) but overclaims for marketplace bundles loaded from R2. As detailed in the review summary, the manifest schema and both extractManifest() implementations drop these fields, and the bundler refuses to emit them, so a marketplace-installed plugin still can't actually contribute a portable-text block via this fix. Consider scoping the changeset wording to standard/sandboxed-from-config until the bundle/parse layer is updated.

@ask-bonk

ask-bonk Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Summary:

The PR correctly fixes the in-process standard-format plugin path (config-time descriptor → virtual modules → adaptSandboxEntry), but the marketplace/R2 branches added in emdash-runtime.ts are dead code — three layers strip the new fields before they ever reach the runtime:

  1. pluginAdminConfigSchema doesn't declare portableTextBlocks (Zod strips it on parse).
  2. Both extractManifest() implementations only copy { settingsSchema, pages, widgets }.
  3. The bundler in cli/commands/bundle.ts:336-345 hard-errors when a plugin declares portableTextBlocks.

Posted as a single batch review with the summary + 5 line-anchored comments on the relevant lines.

github run

… marketplace branches

Addresses review on emdash-cms#1484: the marketplace/R2 reads of portableTextBlocks/
fieldWidgets were dead (manifest schema strips them, extractManifest doesn't
write them, and the bundler deliberately refuses them). Remove those branches,
scope the changeset accordingly, and note adminMode doesn't gate admin
contributions. Full marketplace support is deferred to a follow-up.
@swissky

swissky commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review — you're right that the marketplace/R2 path was dead (schema strips the fields, neither extractManifest() writes them, and the bundler deliberately refuses them). Rather than reverse that deliberate guard in this PR, I've scoped it to what actually works (standard + sandboxed-from-config), pushed in 4571fac:

  • Removed the dead marketplace branches in emdash-runtime.ts (the bundle.manifest.admin?.portableTextBlocks/fieldWidgets reads at the two sync sites, the marketplaceManifestCache type fields, and the meta.admin?.… reads) so there's no no-op code that just typechecks.
  • adminMode: added a comment noting adminMode === "none" doesn't imply "no admin contributions" — plugins can carry portableTextBlocks/fieldWidgets regardless, and the admin reads those from the manifest.
  • Changeset: scoped the wording to standard/sandboxed-from-config and added a note that marketplace bundles aren't covered yet.

The in-process and sandboxed-from-config paths (the ones the existing tests exercise) are unchanged and still forward both fields.

Full marketplace support (extend pluginAdminConfigSchema, both extractManifest() implementations, drop the bundle.ts hard-error + stale warnings, and a round-trip test) is a larger change that reverses an intentional "can't be bundled for the marketplace" guard — happy to do it as a follow-up PR if you'd like that for marketplace plugins.

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jun 16, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator

/review

@ask-bonk

ask-bonk Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

The changes look clean and consistent. The author has:

  1. Forwarded portableTextBlocks and fieldWidgets through adaptSandboxEntry
  2. Added them to PluginDescriptor
  3. Generated them in both codegen paths in virtual-modules.ts
  4. Added them to SandboxedPluginEntry and emitted them at all three call sites
  5. Removed the dead marketplace branches noted in the prior review
  6. Removed the obsolete TODO
  7. Scoped the changeset wording to match what actually works
  8. Added a helpful comment about adminMode semantics
  9. Added 3 new unit tests

The prior review's main concerns (dead marketplace branches, overclaiming changeset, missing adminMode comment) have all been addressed in commit 4571fac. The PR is now well-scoped.

LGTM!

github run

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a focused, well-scoped fix. The problem is real: standard/sandboxed-format plugins could declare declarative portableTextBlocks and fieldWidgets, but adaptSandboxEntry only forwarded adminPages/adminWidgets, so the editor never saw them. The approach is consistent with how EmDash already treats these two formats — wrap the sandboxed default export and surface declarative admin contributions through the manifest — and the PR correctly leaves the native-only componentsEntry path untouched.

I checked the diff, traced where portableTextBlocks/fieldWidgets now flow (adaptSandboxEntry, generatePluginsModule, generateSandboxedPluginsModule, loadBypassedPlugins, and the manifest builder in EmDashRuntime), compared them against the existing adminPages/adminWidgets paths, verified the PluginDescriptor/SandboxedPluginEntry type updates, and reviewed the tests and changeset. The implementation is clean and the test covers the core adapter behavior plus the omission case. The marketplace/R2 bundle gap is explicitly out of scope and called out in the changeset, so I’m not treating that as a regression.

No issues found.

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Jun 17, 2026

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks!

@ascorbic ascorbic merged commit d46abfd into emdash-cms:main Jun 18, 2026
46 checks passed
@emdashbot emdashbot Bot mentioned this pull request Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core bot:review Trigger an emdashbot code review on this PR overlap review/approved Approved; no new commits since size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Standard/sandboxed plugins can't contribute Portable Text blocks or field widgets

2 participants