diff --git a/.changeset/capability-loop-host-copy-first.md b/.changeset/capability-loop-host-copy-first.md new file mode 100644 index 0000000000..5619243047 --- /dev/null +++ b/.changeset/capability-loop-host-copy-first.md @@ -0,0 +1,25 @@ +--- +"@objectstack/cli": patch +--- + +Stop claiming the `os serve` capability loop loads a "host copy first" (#10909). +Two module-header comments in `packages/cli/src/commands/serve.ts` — above the +`@objectstack/plugin-email` and `@objectstack/service-sms` imports — described +the capability loop (`Serve.CAPABILITY_PROVIDERS`, the `for (const cap of +requires)` block) as resolving `EmailServicePlugin`/`SmsServicePlugin` "host +copy first". Measured at head, the loop does a bare `await import(spec.pkg)` / +`await import(ex.pkg)` — no `importFromHost` in either path — which Node ESM +resolves against **this CLI's own** realpath, so the CLI's bundled copy always +wins; the host app's copy is never consulted. The comments described a +behaviour the code does not have. + +The corrected comments also name the contrast the file now actually contains: +`Serve.importConfigPlugin` (the served app's own `plugins: [...]` entries) IS +host-anchored — an app-declared package wins there — while the capability +loop is not. Making that split legible is the point of the fix, so the next +reader does not assume one resolution rule governs the whole file. + +Comment-only: no runtime path, resolution order, or accepted specifier changes. +All 21 `CAPABILITY_PROVIDERS` packages remain CLI-declared, so bare resolution +still finds every one of them today — this only corrects what the comment +claims about *how* that resolution happens. diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index 424a822d1f..15add70083 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -33,15 +33,19 @@ import { missingProviderMessage } from '../utils/capability-preflight.js'; // transports rather than restated here (#5132) — `resolveEmailCapabilityArg` // has to refuse exactly the configurations `makeTransport` cannot build, and // two literal lists for one vocabulary is the drift #5094 was filed for. Values -// only (no plugin class): `os serve` loads `EmailServicePlugin` itself through -// the capability loop's dynamic import, host copy first. +// only (no plugin class): the capability loop loads `EmailServicePlugin` itself +// with a bare `import()`, resolved against THIS CLI's own realpath — its +// bundled copy always wins, never the host app's. Contrast `importConfigPlugin` +// below, which IS host-anchored: an app-declared package wins there (#10909). import { isEmailTransportProvider, emailProviderRequiresApiKey, unsupportedProviderFix } from '@objectstack/plugin-email'; // The SMS provider vocabulary, read from the package that materialises the // transports, for the same reason and by the same rule as the mail one above // (#5713). `resolveSmsCapabilityArg` has to refuse exactly the tags // `makeSmsTransport` cannot build — restating `log`/`aliyun`/`twilio` here would // be the second literal #5094 was filed for. Values only (no plugin class): the -// capability loop dynamic-imports `SmsServicePlugin` itself, host copy first. +// capability loop dynamic-imports `SmsServicePlugin` itself the same way — a +// bare `import()` resolved against this CLI's own realpath, so its bundled +// copy wins, never the host's (#10909). import { isSmsTransportProvider, SMS_TRANSPORT_PROVIDERS } from '@objectstack/service-sms'; import { resolveObjectStackHome } from '@objectstack/runtime'; import { LOG_LEVELS, resolveLogLevel, readLogLevelEnv } from '../utils/log-level.js';