Skip to content

Commit 2600467

Browse files
Copilothotlong
andcommitted
fix: AppPlugin getService crash on missing i18n/objectql services
ctx.getService() throws when a service is not registered, but AppPlugin.start() and loadTranslations() assumed it returned undefined. This caused plugin.app.com.example.crm to crash during startup when the i18n service was not registered in the dev workspace. Wrap both getService('objectql') and getService('i18n') calls in try/catch blocks while preserving the existing null-check fallback for backward compatibility with mocked contexts. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 0aad36a commit 2600467

3 files changed

Lines changed: 59 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Fixed
11+
- **AppPlugin getService crash on missing services**`AppPlugin.start()` and
12+
`loadTranslations()` now wrap `ctx.getService()` in try/catch, since the kernel's
13+
`getService` throws when a service is not registered (rather than returning `undefined`).
14+
This was the direct cause of `plugin.app.com.example.crm failed to start` — the i18n
15+
service was not registered, so `getService('i18n')` threw an unhandled exception.
1116
- **CLI serve: host config AppPlugin mis-wrap**`serve.ts` no longer wraps a host/aggregator config
1217
(one that already contains instantiated plugins in its `plugins` array) with an extra `AppPlugin`.
1318
This prevents the `plugin.app.dev-workspace failed to start` error and eliminates duplicate plugin

packages/runtime/src/app-plugin.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,22 @@ describe('AppPlugin', () => {
100100
);
101101
});
102102

103+
it('start should handle getService throwing for objectql', async () => {
104+
const bundle = { id: 'com.test.throw' };
105+
const plugin = new AppPlugin(bundle);
106+
107+
vi.mocked(mockContext.getService).mockImplementation(() => {
108+
throw new Error("[Kernel] Service 'objectql' not found");
109+
});
110+
111+
await plugin.start!(mockContext);
112+
113+
expect(mockContext.logger.warn).toHaveBeenCalledWith(
114+
expect.stringContaining('ObjectQL engine service not found'),
115+
expect.any(Object)
116+
);
117+
});
118+
103119
// ═══════════════════════════════════════════════════════════════
104120
// i18n translation auto-loading
105121
// ═══════════════════════════════════════════════════════════════
@@ -173,6 +189,26 @@ describe('AppPlugin', () => {
173189
);
174190
});
175191

192+
it('should skip translation loading when getService throws for i18n', async () => {
193+
vi.mocked(mockContext.getService).mockImplementation((name: string) => {
194+
if (name === 'objectql') return mockQL;
195+
throw new Error("[Kernel] Service 'i18n' not found");
196+
});
197+
198+
const bundle = {
199+
id: 'com.test.i18nthrow',
200+
translations: [{ en: { messages: { hello: 'Hello' } } }],
201+
};
202+
const plugin = new AppPlugin(bundle);
203+
await plugin.start!(mockContext);
204+
205+
// Should log debug but not throw
206+
expect(mockContext.logger.debug).toHaveBeenCalledWith(
207+
expect.stringContaining('No i18n service registered'),
208+
expect.any(Object)
209+
);
210+
});
211+
176212
it('should handle bundle with no translations gracefully', async () => {
177213
const bundle = { id: 'com.test.notrans' };
178214
const plugin = new AppPlugin(bundle);

packages/runtime/src/app-plugin.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,15 @@ export class AppPlugin implements Plugin {
6060

6161
// Execute Runtime Step
6262
// Retrieve ObjectQL engine from services
63-
// We cast to any/ObjectQL because ctx.getService returns unknown
64-
const ql = ctx.getService('objectql') as any;
65-
63+
// ctx.getService throws when a service is not registered, so we
64+
// must use try/catch instead of a null-check.
65+
let ql: any;
66+
try {
67+
ql = ctx.getService('objectql');
68+
} catch {
69+
// Service not registered — handled below
70+
}
71+
6672
if (!ql) {
6773
ctx.logger.warn('ObjectQL engine service not found', {
6874
appName: this.name,
@@ -201,7 +207,15 @@ export class AppPlugin implements Plugin {
201207
* this keeps AppPlugin resilient across server/dev/mock environments.
202208
*/
203209
private loadTranslations(ctx: PluginContext, appId: string): void {
204-
const i18nService = ctx.getService('i18n') as II18nService | undefined;
210+
// ctx.getService throws when a service is not registered, so we
211+
// must use try/catch to gracefully skip when no i18n plugin is loaded.
212+
let i18nService: II18nService | undefined;
213+
try {
214+
i18nService = ctx.getService('i18n') as II18nService;
215+
} catch {
216+
// Service not registered — handled below
217+
}
218+
205219
if (!i18nService) {
206220
ctx.logger.debug('[i18n] No i18n service registered; skipping translation loading', { appId });
207221
return;

0 commit comments

Comments
 (0)