Skip to content

Commit d90d176

Browse files
hotlongCopilot
andcommitted
feat(service-cloud): load runtimeModule in FsAppBundleResolver
The cloud / multi-project boot path (project-kernel-factory.ts) consumes bundles via AppBundleResolver and feeds them straight into AppPlugin — unlike StandaloneStack and the http-dispatcher artifact-bind path, it had no place where the sibling runtimeModule ESM was dynamic-imported. Result: objectos serving CRM via filesystem bind would load the JSON artifact but every Hook handler would resolve to undefined and silently no-op. Move the runtime-module load into FsAppBundleResolver.loadOne(), since it already owns the artifact's absolute path and naturally caches per file. Bundles handed to project-kernel-factory now arrive with their `functions` map fully populated. Verified end-to-end with examples/app-crm: resolver returns a bundle with runtimeModule set and 16 typeof 'function' entries in bundle.functions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1f8d63f commit d90d176

1 file changed

Lines changed: 38 additions & 1 deletion

File tree

packages/services/service-cloud/src/fs-bundle-resolver.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
*/
3030

3131
import { readFile } from 'node:fs/promises';
32-
import { resolve as resolvePath, isAbsolute } from 'node:path';
32+
import { resolve as resolvePath, isAbsolute, dirname } from 'node:path';
33+
import { pathToFileURL } from 'node:url';
3334
import type { AppBundleResolver } from './project-kernel-factory.js';
3435

3536
const ENV_MAP_VAR = 'OS_PROJECT_ARTIFACTS';
@@ -65,6 +66,41 @@ function extractMetadataPaths(metadata: any): string[] {
6566
return out;
6667
}
6768

69+
/**
70+
* If the loaded artifact references a sibling runtime ESM (declarative
71+
* handler bundle produced by `objectstack build`), dynamic-import it
72+
* and merge its `functions` map into the bundle. Without this step
73+
* every Hook in the bundle boots with `handler === undefined` and
74+
* silently no-ops — see `packages/cli/src/utils/build-runtime.ts` for
75+
* the build-side counterpart.
76+
*
77+
* Mutates `bundle` in place. Failures are logged but non-fatal —
78+
* a bundle without runtime functions is still loadable (just inert).
79+
*/
80+
async function mergeRuntimeModule(bundle: any, artifactAbsPath: string): Promise<void> {
81+
const ref = bundle?.runtimeModule;
82+
if (typeof ref !== 'string' || ref.length === 0) return;
83+
const moduleAbsPath = isAbsolute(ref) ? ref : resolvePath(dirname(artifactAbsPath), ref);
84+
try {
85+
const mod: any = await import(pathToFileURL(moduleAbsPath).href);
86+
const fns = (mod && (mod.functions ?? mod.default?.functions)) ?? null;
87+
if (!fns || typeof fns !== 'object') {
88+
// eslint-disable-next-line no-console
89+
console.warn(`[FsAppBundleResolver] runtime module '${moduleAbsPath}' exported no \`functions\` map`);
90+
return;
91+
}
92+
const existing = (bundle.functions && typeof bundle.functions === 'object' && !Array.isArray(bundle.functions))
93+
? bundle.functions as Record<string, unknown>
94+
: {};
95+
bundle.functions = { ...existing, ...fns };
96+
} catch (err: any) {
97+
// eslint-disable-next-line no-console
98+
console.warn(
99+
`[FsAppBundleResolver] runtime module load FAILED: path='${moduleAbsPath}' error=${err?.message ?? err}`,
100+
);
101+
}
102+
}
103+
68104
export function createFsAppBundleResolver(): AppBundleResolver {
69105
const envMap = parseEnvMap(process.env[ENV_MAP_VAR]);
70106
const root = process.env[ARTIFACT_ROOT_VAR] ?? process.cwd();
@@ -76,6 +112,7 @@ export function createFsAppBundleResolver(): AppBundleResolver {
76112
try {
77113
const raw = await readFile(abs, 'utf-8');
78114
const parsed = JSON.parse(raw);
115+
await mergeRuntimeModule(parsed, abs);
79116
cache.set(abs, parsed);
80117
return parsed;
81118
} catch (err: any) {

0 commit comments

Comments
 (0)