Skip to content

fix: catch syntax errors in loaders to prevent blocking app startup - #22

Open
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1781754921-loader-catch-syntax-errors
Open

devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1781754921-loader-catch-syntax-errors

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

A syntax error in a single service file previously crashed the entire module import chain, preventing the app from loading at all. This PR makes loading resilient so broken modules are skipped with a logged error.

Two layers of protection:

1. Generated barrel/root modules now use dynamic import() with .catch()

Previously buildPluginBarrel and buildPluginsRoot emitted static import "..." statements — if any module in the chain threw, it cascaded up and killed the app. Now service-file and barrel imports use:

await import("file:///path/to/service.ts").catch(e =>
  console.error("[zenbu-loader] failed to load module", url, e.message ?? e))

The registry import stays static since it's generated code that must succeed.

2. loadImpl file:// branch catches loader-phase errors

  • loadNativeStrippedTs (stripTypeScriptTypes) → try/catch, returns export {} on failure
  • nextLoad + appendAutoRegister for service files → try/catch with async .catch() for promise-based loaders

Both paths log [zenbu-loader] failed to load <path>: <message> and return an empty module so the rest of the app continues.

Link to Devin session: https://app.devin.ai/sessions/f973737f254e4f07aea5ee9ac3f80e53
Requested by: @RobPruzan


Open in Devin Review

- Switch generated barrel and plugins-root modules from static imports to
  dynamic import() with .catch() so a syntax error in one service file
  doesn't cascade and block the entire app from loading
- Wrap loadNativeStrippedTs (TS type stripping) in try/catch, returning
  an empty module on failure
- Wrap nextLoad + appendAutoRegister for service files in try/catch
- All caught errors are logged via console.error with [zenbu-loader] prefix

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
zenbu-website Ready Ready Preview, Comment Jun 18, 2026 4:08am

Request Review

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

Open in Devin Review

Comment thread packages/core/src/loaders/zenbu.ts Outdated
Comment on lines +112 to +119
function buildSafeSource(imports: string[]): string {
return imports
.map(
(specifier) =>
`await import(${JSON.stringify(specifier)}).catch(e => console.error("[zenbu-loader] failed to load module", ${JSON.stringify(specifier)} + ":", e.message ?? e))\n`,
)
.join("");
}

@devin-ai-integration devin-ai-integration Bot Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: Barrel imports now load concurrently instead of sequentially

The switch from static import to Promise.allSettled([import(...), ...]) means service files within a single plugin barrel now load concurrently rather than in the sequential order determined by the glob expansion. Previously, buildSource(imports) generated sequential static imports (import "a"\nimport "b") which are evaluated in dependency order. Now all imports fire simultaneously. This shouldn't cause issues because: (1) the runtime.register() mechanism at packages/core/src/runtime.ts:237-280 handles registrations independently per service key, (2) services declare explicit deps for ordering, and (3) evaluate() is called later during reconciliation, not at import time. However, if any plugin relies on implicit evaluation order of service file side effects beyond runtime.register, that assumption would break.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


return {
source: `${buildSource(imports)}import.meta.hot?.accept()\n`,
source: `${buildSafeSource(imports)}import.meta.hot?.accept()\n`,

@devin-ai-integration devin-ai-integration Bot Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🚩 Dynamic imports may affect dynohot's module graph pruning for removed service files

With static imports, dynohot can see the barrel's dependencies in the AST and track when a dependency is dropped (e.g., a service file deleted). With dynamic import(), the barrel-to-service relationship is invisible to dynohot's static analysis. Service files DO self-manage HMR via runtime.register() calling import.meta.hot.accept() and import.meta.hot.prune() (packages/core/src/runtime.ts:272-277), so individual file edits are handled correctly. However, the pruning path for a removed file depends on dynohot detecting that the module is no longer referenced. If dynohot relies on the static import graph for pruning decisions, a removed service file's prune callback might not fire, leaving a stale registration in the runtime. The existing deleted-file guard at packages/core/src/loaders/zenbu.ts:579-586 mitigates this for the reload case, but the unregister path through prune may be affected. Worth verifying with a manual test: delete a service file and confirm the service is properly unregistered.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

…ope error swallowing to service files only

- Use Promise.allSettled instead of sequential await import() to preserve
  parallel module loading behavior
- Only catch/swallow errors for service files in the native TS loader path;
  non-service files propagate errors naturally so developers see clear diagnostics

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview 26bd766a Deployed Jun 18, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant