Skip to content

fix: bundle sandbox entries with esbuild & normalize CodeBlock languages#179

Open
yomna-shousha wants to merge 4 commits intoemdash-cms:mainfrom
yomna-shousha:fix/sandbox-ts-transpile-and-codeblock-lang
Open

fix: bundle sandbox entries with esbuild & normalize CodeBlock languages#179
yomna-shousha wants to merge 4 commits intoemdash-cms:mainfrom
yomna-shousha:fix/sandbox-ts-transpile-and-codeblock-lang

Conversation

@yomna-shousha
Copy link
Copy Markdown

@yomna-shousha yomna-shousha commented Apr 3, 2026

Problem

Two bugs prevent sandboxed plugins from working correctly:

1. Sandbox entries fail with Unexpected token '{'

generateSandboxedPluginsModule() reads plugin entrypoints with readFileSync() and embeds the raw source as a string in a Worker Loader isolate. This breaks when:

  1. TypeScript syntax — Worker Loader executes modules as JavaScript. TypeScript constructs like routeCtx.input as { type: string } cause V8 to throw Unexpected token '{'.
  2. Unresolved imports — Bare specifiers like import { definePlugin } from "emdash" survive as-is. Worker Loader only provides plugin.js and sandbox-plugin.js modules, so V8 fails during module linking.

Repro: Configure any plugin in sandboxed: [...], then visit its admin settings page. The API route returns:

{"error":{"code":"ROUTE_ERROR","message":"Unexpected token '{'"}}

2. CodeBlock crashes on unsupported language values

Kumo's CodeBlock component has a hardcoded variant map with 5 languages (ts, tsx, jsonc, bash, css). Passing any other value (e.g. "json") causes:

TypeError: Cannot read properties of undefined (reading 'classes')

The webhook-notifier plugin returns language: "json" in its Block Kit code blocks, which crashes the admin UI after Bug 1 is fixed.

Solution

Commit 1: Bundle sandbox entries with esbuild (packages/core)

Replace readFileSync() with an async esbuild.build() call that:

  • Bundles all imports into a single self-contained ES module
  • Transpiles TypeScript/TSX/JSX via the appropriate loader
  • Shims "emdash" with a virtual module providing definePlugin as an identity function (matching standard-format behaviour in define-plugin.ts — this follows the same pattern as the CLI bundler in packages/core/src/cli/commands/bundle.ts:360-379)
  • Wraps errors with the plugin ID and file path for debuggability

generateSandboxedPluginsModule() is now async (returns Promise<string>), which is compatible with Vite's load() hook.

Commit 2: Normalize CodeBlock language values (packages/blocks)

  • Add normalizeLang() to CodeBlockComponent that maps aliases (jsonjsonc, javascript/jsts, sh/shellbash) and falls back to "bash" for unknown values
  • Export a new CodeLanguage type that includes both canonical and alias values
  • Expand CODE_LANGUAGES in validation to accept aliases
  • Update codeBlock() builder to use CodeLanguage

Commit 3: Fix plugin language values (packages/plugins)

Change language: "json""jsonc" in webhook-notifier and sandboxed-test sandbox entries. First-party plugins should use canonical values; the renderer normalization is a safety net for third-party plugins.

Testing

Verified with a Cloudflare Workers deployment:

  1. astro build succeeds with plugins in sandboxed: [...]
  2. The bundled output contains the esbuild shim (var definePlugin = (d) => d;) and fully resolved, transpiled JavaScript
  3. The admin settings page at /_emdash/admin/plugins/webhook-notifier/settings renders correctly — form fields, JSON code preview, and action buttons all work

Related

A separate PR to cloudflare/kumo adds a defensive ?.classes ?? fallback in codeVariants() so Kumo itself doesn't crash on unknown lang values. That fix is defense-in-depth — this PR's blocks-layer normalization is sufficient on its own.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 3, 2026

🦋 Changeset detected

Latest commit: d50f98e

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

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
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

The sandbox runner embeds plugin entrypoints at build time via
generateSandboxedPluginsModule(). Previously it used readFileSync()
to read the raw source and embed it as-is. This breaks when:

1. The entrypoint is TypeScript (.ts/.tsx) — Worker Loader executes
   modules as JavaScript, so TS syntax like 'as { ... }' causes
   'Unexpected token {' at runtime.

2. The entrypoint has bare imports (e.g. 'import { definePlugin }
   from "emdash"') — Worker Loader only provides 'plugin.js' and
   'sandbox-plugin.js' modules, so unresolved specifiers crash
   during V8 module linking.

Replace readFileSync with an async esbuild.build() call that:
- Bundles all imports into a single self-contained ES module
- Transpiles TypeScript/TSX/JSX via the appropriate loader
- Shims 'emdash' with a virtual module that provides definePlugin
  as an identity function (matching standard-format behaviour in
  define-plugin.ts)
- Wraps errors with the plugin ID and file path for debuggability
- Handles .ts, .tsx, .mts, .cts, and .jsx extensions

generateSandboxedPluginsModule is now async (returns Promise<string>),
which is compatible with Vite's load() hook.
Kumo's CodeBlock component only supports 5 language values: ts, tsx,
jsonc, bash, css. Passing any other value (e.g. 'json', 'javascript',
'shell') causes a TypeError because the component does an unguarded
map lookup: KUMO_CODE_VARIANTS.lang[lang].classes

Add a normalizeLang() function to CodeBlockComponent that:
- Maps common aliases to their Kumo equivalents (json -> jsonc,
  javascript/js -> ts, sh/shell -> bash)
- Falls back to 'bash' for unknown languages
- Uses a local SupportedLang type to avoid coupling to Kumo internals

Also:
- Export a new CodeLanguage type from types.ts that includes both
  canonical Kumo values and accepted aliases
- Update the codeBlock() builder to use CodeLanguage
- Expand CODE_LANGUAGES in validation.ts to accept aliases
- Re-export CodeBlock and CodeLanguage from server.ts
The Block Kit CodeBlock type and Kumo's CodeBlock component use
'jsonc' for JSON content. Both webhook-notifier and sandboxed-test
were using 'json' which is not in the canonical set and would fail
validation if validateBlocks() were enforced.

While the blocks renderer now normalizes 'json' -> 'jsonc' as a
safety net, first-party plugins should use the canonical value.
@yomna-shousha yomna-shousha force-pushed the fix/sandbox-ts-transpile-and-codeblock-lang branch from f5d9d79 to d50f98e Compare April 3, 2026 12:55
@ascorbic
Copy link
Copy Markdown
Collaborator

ascorbic commented Apr 7, 2026

Hi Yomna. Thanks for the PR. The bundling fix shouldn't be needed: the CLI already has a bundler for plugins and it's a bug in the webhook notifier plugin that's exporting the TypeScript source instead of the compiled JS. If you could scope this down to just the codeblock fix then I can merge it. Meanwhile I'll fix the webhook notifier plugin bug.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

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.

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