fix(core): fail fast with an actionable error for plugins without an entrypoint (#1416)#1560
Conversation
…entrypoint (emdash-cms#1416) generatePluginsModule assumed every plugin descriptor had a file entrypoint and emitted `import pluginDefN from "${descriptor.entrypoint}";`. For an in-process plugin -- an inline definePlugin({...}) result passed directly to plugins: [] -- entrypoint is undefined, so the generated virtual module contained `import pluginDef0 from "undefined";` and the build failed deep in Rollup with the cryptic `Rollup failed to resolve import "undefined" from "virtual:emdash/plugins"`. The generator now throws a clear error that names the offending plugin and explains that plugins: [] entries must resolve to a file/package entrypoint (bundled at build time), so an inline definePlugin({...}) result is not supported -- move the plugin into its own module and reference it via a factory that returns a descriptor with an entrypoint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: d19e6a7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
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 |
There was a problem hiding this comment.
Approach
This is the right change at the right boundary. EmDashConfig.plugins expects PluginDescriptor[] — descriptors whose entrypoint is bundled at build time and imported at runtime — which is a distinct layer from RuntimeDependencies.plugins: ResolvedPlugin[] (the in-process, hooks/routes-bearing shape that definePlugin({...}) returns, consumed by EmDashRuntime.create()). The #1416 reporter conflated the two by passing a definePlugin({...}) result (a ResolvedPlugin, no entrypoint) straight into plugins: []; the generator then emitted import pluginDef0 from "undefined"; and died deep in Rollup. Catching this at the descriptor→codegen boundary is correct, and it does not break the legitimate in-process runtime plugin path (that goes through RuntimeDependencies.plugins, never generatePluginsModule). Turning the cryptic Rollup resolution error into a named, actionable build error is a clear improvement, and the change is purely additive: valid configs are unaffected, invalid ones still fail — just clearly.
What I checked
- Traced
generatePluginsModule→ its only caller (thevirtual:emdash/pluginsloadhook invite-config.ts) → its only runtime importer (astro/middleware.ts). The middleware always imports the virtual module, so the guard fires on every build; the layering is sound and the error surfaces during the build rather than silently slipping through. - Confirmed
PluginDescriptor.entrypointis typed required, so the guard (if (!descriptor.entrypoint)) correctly catchesundefined/null/"", and there's no legitimate falsy-entrypoint case to regress. - Confirmed the runtime in-process path (
RuntimeDependencies.plugins: ResolvedPlugin[]inemdash-runtime.ts) is separate and untouched, sodefinePluginresults still work where they're actually supported. - The test reproduces the reporter's bypass-the-type scenario, asserts the message names the plugin and mentions
entrypoint, and guards against the cryptic"undefined"regression. Good TDD coverage for the fix. - Changeset targets the right package (
emdash) at the right bump (patch).
Findings
Two issues, neither a blocker:
- Changeset is framed as a commit message / PR description (
needs_fixing, AGENTS.md convention). It leads with afix(core):commit prefix and the body is a diff walkthrough naming the codegen function and quoting the exact emitted imports and the literal Rollup error string — the internal mechanics AGENTS.md says to leave out. Suggested a user-facing rewrite leading withFixes. - Sibling
generateSandboxedPluginsModulelacks the same guard (suggestion). The identical misuse onsandboxed: []still fails cryptically viarequire.resolve(undefined); the two generators are now inconsistent for the same bad input. Suggested a local guard or, preferably, hoisting the check into the shared config-time validation inindex.tsthat already iterates both arrays.
Implementation is clean and the fix achieves its stated goal. Verdict is comment — the changeset wording and the sandboxed-path consistency gap are worth addressing but don't block merge.
Findings
-
[needs fixing]
.changeset/fix-1416-plugins-missing-entrypoint.md:5-9This changeset reads like a commit message / PR description, not release notes. The title line uses a
fix(core):commit-message prefix, and the body is a diff walkthrough full of internal mechanics — the codegen function name (generatePluginsModule), the exact emitted import statements (import pluginDefN from "${descriptor.entrypoint}";,import pluginDef0 from "undefined";), and the literal Rollup error string (Rollup failed to resolve import "undefined" from "virtual:emdash/plugins").AGENTS.md (→ CONTRIBUTING.md § Changesets) is explicit: a changeset is release notes a user reads while upgrading — not a commit message, PR description, or summary of the diff; lead with a present-tense verb (
Fixes,Adds, …), describe the observable effect, and leave out internal mechanics. The other recent changesets in this repo (session-get-hang.md,layout-prefetch.md,forward-plugin-blocks.md) all lead with a present-tense verb and center the user-facing effect; none use atype(scope):prefix.Fixes a cryptic build failure when a plugin in `plugins: []` has no `entrypoint` — for example, an inline `definePlugin({...})` result passed directly instead of a factory that returns a descriptor. The build now fails fast with an actionable error naming the offending plugin and explaining that `plugins: []` entries must resolve to a file or package entrypoint. -
[suggestion]
packages/core/src/astro/integration/virtual-modules.ts:582The new entrypoint guard lives only in
generatePluginsModule(theplugins: []path). The siblinggenerateSandboxedPluginsModule(thesandboxed: []path) consumesdescriptor.entrypointhere without the same guard, so the identical misuse — a descriptor with noentrypoint— still fails cryptically viarequire.resolve(undefined)(TypeError: The "id" argument must be of type string) instead of the actionable, plugin-named message this PR introduces. The two generators are now inconsistent for the same class of bad input.This is out of scope for issue #1416 (which is about
plugins: []), so it's not blocking, but for consistency consider applying the same fail-fast guard here, or — better — hoisting the entrypoint check into the shared config-time validation block inindex.tsthat already iterates bothpluginDescriptorsandsandboxedDescriptors, so both arrays fail fast in one place before Vite'sloadhook runs.if (!descriptor.entrypoint) { throw new Error( `[emdash] Sandboxed plugin "${descriptor.id}" has no \`entrypoint\`. \`sandboxed: []\` requires plugins that resolve to a file/package entrypoint so they can be bundled at build time.`, ); } const bundleSpecifier = descriptor.entrypoint;
…elease note Per CONTRIBUTING.md, lead with the observable effect and drop the commit-message title line and internal codegen mechanics. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What does this PR do?
generatePluginsModuleassumed every plugin descriptor had a file entrypoint and emittedimport pluginDefN from "${descriptor.entrypoint}";. For an in-process plugin — an inlinedefinePlugin({...})result passed directly toplugins: []—entrypointisundefined, so the generated virtual module containedimport pluginDef0 from "undefined";and the build failed deep in Rollup with the crypticRollup failed to resolve import "undefined" from "virtual:emdash/plugins".The generator now throws a clear, actionable error that names the offending plugin and explains that
plugins: []entries must resolve to a file/package entrypoint (bundled at build time), so an inlinedefinePlugin({...})result is not supported — move the plugin into its own module and reference it via a factory that returns a descriptor with anentrypoint.Closes #1416
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (targeted: tests/unit/plugins — 600 tests)pnpm formathas been runAI-generated code disclosure