Skip to content

Commit 831ee7d

Browse files
xuyushun441-sysos-zhuangclaude
authored
fix(runtime,cli): resolve optional capability packages from the host app context (#1573)
loadCapabilities() and the CLI serve auto-register did a bare dynamic import of capability packages (e.g. @objectstack/service-ai-studio), which resolves relative to the framework package's own location. A private/optional package linked into the *app's* node_modules (the tenant runtime apps/objectos, or an enterprise objectos-ee install) is invisible there — it failed with "Cannot find package … imported from …/framework/…", so cloud tenants never loaded AI Studio despite declaring the dependency. Resolve via createRequire anchored at process.cwd() (the host app), then import the resolved path; fall back to a bare import for framework-owned packages. Verified: createRequire from the app dir resolves @objectstack/service-ai-studio to its dist; the old framework-relative resolution returns MODULE_NOT_FOUND. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2ee72d3 commit 831ee7d

2 files changed

Lines changed: 48 additions & 4 deletions

File tree

packages/cli/src/commands/serve.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,10 +1348,27 @@ export default class Serve extends Command {
13481348
(p: any) => p.name === 'com.objectstack.service-ai'
13491349
|| p.constructor?.name === 'AIServicePlugin'
13501350
);
1351+
// Resolve optional plugin packages from the HOST APP's context (the app
1352+
// being served declares them as deps — including private packages like
1353+
// @objectstack/service-ai-studio that the framework CLI itself does not
1354+
// depend on). A bare import would resolve relative to the CLI's location
1355+
// and miss a package linked into the app's node_modules. Falls back to a
1356+
// bare import for framework-owned packages.
1357+
const { createRequire: _createRequire } = await import('node:module');
1358+
const { pathToFileURL: _pathToFileURL } = await import('node:url');
1359+
const _nodePath = await import('node:path');
1360+
const _hostRequire = _createRequire(_nodePath.join(process.cwd(), 'package.json'));
1361+
const importFromHost = async (pkg: string): Promise<any> => {
1362+
try {
1363+
return await import(_pathToFileURL(_hostRequire.resolve(pkg)).href);
1364+
} catch {
1365+
return import(/* webpackIgnore: true */ pkg);
1366+
}
1367+
};
13511368
if (!hasAIPlugin && tierEnabled('ai')) {
13521369
try {
13531370
const aiPkg = '@objectstack/service-ai';
1354-
const { AIServicePlugin } = await import(/* webpackIgnore: true */ aiPkg);
1371+
const { AIServicePlugin } = await importFromHost(aiPkg);
13551372

13561373
// AIServicePlugin will auto-detect LLM provider from environment variables
13571374
// (AI_GATEWAY_MODEL, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY)
@@ -1379,7 +1396,7 @@ export default class Serve extends Command {
13791396
if (!hasAIStudio) {
13801397
try {
13811398
const studioPkg = '@objectstack/service-ai-studio';
1382-
const { AIStudioPlugin } = await import(/* webpackIgnore: true */ studioPkg);
1399+
const { AIStudioPlugin } = await importFromHost(studioPkg);
13831400
await kernel.use(new AIStudioPlugin());
13841401
trackPlugin('AIStudio');
13851402
} catch (err: unknown) {

packages/runtime/src/cloud/capability-loader.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,33 @@
1919
*/
2020

2121
import type { ObjectKernel } from '@objectstack/core';
22+
import { createRequire } from 'node:module';
23+
import { pathToFileURL } from 'node:url';
24+
import { join } from 'node:path';
25+
26+
/**
27+
* Import an optional capability package, resolving it from the HOST APP's
28+
* context first.
29+
*
30+
* The host app (a tenant runtime like apps/objectos, or an enterprise
31+
* objectos-ee install) is what declares optional plugin packages — including
32+
* private ones such as `@objectstack/service-ai-studio` that are NOT part of
33+
* the framework's own dependency graph. A bare `import(pkg)` from this file
34+
* resolves relative to the framework package's location, which cannot see a
35+
* package linked into the *app's* node_modules (the failure surfaces as
36+
* "Cannot find package … imported from …/framework/…"). Anchoring a
37+
* `createRequire` at the app's cwd resolves it from the app's node_modules.
38+
* Falls back to a bare import for framework-owned packages.
39+
*/
40+
async function importFromHost(pkg: string): Promise<any> {
41+
try {
42+
const req = createRequire(join(process.cwd(), 'package.json'));
43+
const resolved = req.resolve(pkg);
44+
return await import(pathToFileURL(resolved).href);
45+
} catch {
46+
return import(/* webpackIgnore: true */ pkg);
47+
}
48+
}
2249

2350
export interface CapabilitySpec {
2451
/** npm package name to import. */
@@ -161,7 +188,7 @@ export async function loadCapabilities(opts: LoadCapabilitiesOptions): Promise<s
161188
}
162189

163190
try {
164-
const mod: any = await import(/* webpackIgnore: true */ spec.pkg);
191+
const mod: any = await importFromHost(spec.pkg);
165192
const Ctor = mod[spec.export];
166193
if (!Ctor) {
167194
logger.warn?.(
@@ -187,7 +214,7 @@ export async function loadCapabilities(opts: LoadCapabilitiesOptions): Promise<s
187214
if (spec.extras) {
188215
for (const ex of spec.extras) {
189216
try {
190-
const exMod: any = await import(/* webpackIgnore: true */ ex.pkg);
217+
const exMod: any = await importFromHost(ex.pkg);
191218
const ExCtor = exMod[ex.export];
192219
if (ExCtor) {
193220
await kernel.use(new ExCtor());

0 commit comments

Comments
 (0)