Skip to content

serve.ts loads an app's own plugins: [...] config entries with a bare import(), so a plugin package the APP declares resolves from the CLI #10908

Description

@os-elon

Filed unassigned from the domain:cli lane while implementing #10769 (the host-importer reachability class). Recording only — out of scope there, and it moves app-config plugin loading, so it wants its own decision.

The site

packages/cli/src/commands/serve.ts:2677 (measured on the #10769 branch):

if(typeofplugin==='string'){try{constimported=awaitimport(plugin);pluginToLoad=imported.default||imported;}catch(importError: any){thrownewError(`Failed to import plugin '${plugin}': ${importError.message}`);}}

plugin comes from the served app's own objectstack.config.tsplugins: [...] array. It is therefore an app-supplied specifier by construction — the single most app-owned specifier in the whole file — and it is loaded with a bare import(), which Node ESM resolves against the CLI's realpath.

So an app that writes plugins: ['@acme/my-plugin'] and declares @acme/my-plugin in its own package.json can only be served if that package happens to be hoisted somewhere the CLI can see. In a dev checkout it is; in a real distribution layout it is not. This is the same mechanism as cloud#1013 and #10645, on the documented extension path.

Why #10769 did not fix it

#10769 makes importFromHost reachable from every line of serve.ts and widens the source scan to every app-declarable optional load, classifying a package as app-declarable exactly when packages/cli's own manifest does not declare it. That classification is static, and this specifier is not: the scan cannot know what an app will put in its config. It is enumerated in that PR's UNRESOLVABLE_BARE_IMPORTS allowlist with a pointer to this card rather than being silently passed over.

What the fix probably is

await importFromHost(plugin) — the helper is now reachable at that line. createHostImporter is a strict superset of a bare import() for bare package names (declared by the host resolves from the host; undeclared falls back to the CLI's own resolution), and it passes non-package specifiers (relative paths, file:// URLs) straight through, which the plugins: ['./local-plugin.js'] spelling relies on.

The reason this was not just done in #10769:

  • it changes the error message on a missing plugin — the wrapper above nests importError.message, which becomes the long multi-line createHostRequire 认 NODE_PATH,于是 #4699 立下的「host app 必须自己声明」在 pnpm 工作区里根本没被强制 #4719 "declare it in that app's package.json" text. That is arguably a better diagnostic, but it is a visible change to a user-facing failure and nothing currently pins it;
  • an app that declares a plugin the CLI also declares would start getting its own copy instead of the CLI's. That is very likely correct, but it is a resolution-policy decision (see the sibling finding on the capability loop), not a mechanical repair.

Not a duplicate

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions