@open-mercato/shared 0.6.7-develop.6828.1.ab1620a63e → 0.6.7-develop.6834.1.e76f4b8cbc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -1
- package/dist/lib/bootstrap/dynamicLoader.js +54 -14
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/di/container.js +12 -1
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/i18n/server.js +4 -4
- package/dist/lib/i18n/server.js.map +3 -3
- package/dist/lib/logger/extension.js +29 -0
- package/dist/lib/logger/extension.js.map +7 -0
- package/dist/lib/logger/index.js +47 -3
- package/dist/lib/logger/index.js.map +3 -3
- package/dist/lib/modules/registry.js +5 -1
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/telemetry/runtime.js +35 -0
- package/dist/lib/telemetry/runtime.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +98 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +211 -0
- package/src/lib/bootstrap/dynamicLoader.ts +111 -32
- package/src/lib/di/__tests__/container-app-di-absent.test.ts +17 -1
- package/src/lib/di/container.ts +19 -1
- package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +29 -0
- package/src/lib/i18n/server.ts +6 -2
- package/src/lib/logger/__tests__/logger.test.ts +77 -0
- package/src/lib/logger/extension.ts +59 -0
- package/src/lib/logger/index.ts +59 -1
- package/src/lib/modules/registry.ts +10 -0
- package/src/lib/telemetry/runtime.ts +72 -0
package/dist/lib/logger/index.js
CHANGED
|
@@ -1,12 +1,53 @@
|
|
|
1
1
|
import { selectTransport } from "./transport.js";
|
|
2
|
-
import {
|
|
2
|
+
import { getLoggerExtension } from "./extension.js";
|
|
3
|
+
import { isLevelEnabled } from "./level.js";
|
|
4
|
+
import { getLogLevel, isLevelEnabled as isLevelEnabled2, resetLogLevelCache, OM_LOG_LEVEL_ENV } from "./level.js";
|
|
3
5
|
import { resetServerLoggerCache, OM_LOG_DESTINATION_ENV } from "./transport.server.js";
|
|
4
6
|
import { resetLogPrettyCache, OM_LOG_PRETTY_ENV } from "./transport.pretty.js";
|
|
7
|
+
import {
|
|
8
|
+
getLoggerExtension as getLoggerExtension2,
|
|
9
|
+
registerLoggerExtension,
|
|
10
|
+
resetLoggerExtension
|
|
11
|
+
} from "./extension.js";
|
|
5
12
|
const loggerRegistry = /* @__PURE__ */ new Map();
|
|
13
|
+
function createExtendedLogger(namespace, transport, bindings = {}) {
|
|
14
|
+
const emit = (level, msg, fields) => {
|
|
15
|
+
if (!isLevelEnabled(level)) return;
|
|
16
|
+
const extension = getLoggerExtension();
|
|
17
|
+
let context = {};
|
|
18
|
+
try {
|
|
19
|
+
context = extension?.enrich?.() ?? {};
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
const mergedFields = { ...fields, ...context };
|
|
23
|
+
transport[level](msg, mergedFields);
|
|
24
|
+
try {
|
|
25
|
+
extension?.emit?.({
|
|
26
|
+
level,
|
|
27
|
+
namespace,
|
|
28
|
+
message: msg,
|
|
29
|
+
fields: { ...bindings, ...mergedFields },
|
|
30
|
+
time: Date.now()
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
debug: (msg, fields) => emit("debug", msg, fields),
|
|
37
|
+
info: (msg, fields) => emit("info", msg, fields),
|
|
38
|
+
warn: (msg, fields) => emit("warn", msg, fields),
|
|
39
|
+
error: (msg, fields) => emit("error", msg, fields),
|
|
40
|
+
child: (childBindings) => createExtendedLogger(
|
|
41
|
+
namespace,
|
|
42
|
+
transport.child(childBindings),
|
|
43
|
+
{ ...bindings, ...childBindings }
|
|
44
|
+
)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
6
47
|
function createLogger(namespace) {
|
|
7
48
|
const existing = loggerRegistry.get(namespace);
|
|
8
49
|
if (existing) return existing;
|
|
9
|
-
const created = selectTransport(namespace);
|
|
50
|
+
const created = createExtendedLogger(namespace, selectTransport(namespace));
|
|
10
51
|
loggerRegistry.set(namespace, created);
|
|
11
52
|
return created;
|
|
12
53
|
}
|
|
@@ -19,9 +60,12 @@ export {
|
|
|
19
60
|
OM_LOG_PRETTY_ENV,
|
|
20
61
|
createLogger,
|
|
21
62
|
getLogLevel,
|
|
22
|
-
|
|
63
|
+
getLoggerExtension2 as getLoggerExtension,
|
|
64
|
+
isLevelEnabled2 as isLevelEnabled,
|
|
65
|
+
registerLoggerExtension,
|
|
23
66
|
resetLogLevelCache,
|
|
24
67
|
resetLogPrettyCache,
|
|
68
|
+
resetLoggerExtension,
|
|
25
69
|
resetLoggerRegistry,
|
|
26
70
|
resetServerLoggerCache
|
|
27
71
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/logger/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { selectTransport } from './transport'\n\nexport type { LogLevel } from './level'\nexport { getLogLevel, isLevelEnabled, resetLogLevelCache, OM_LOG_LEVEL_ENV } from './level'\nexport { resetServerLoggerCache, OM_LOG_DESTINATION_ENV } from './transport.server'\nexport { resetLogPrettyCache, OM_LOG_PRETTY_ENV } from './transport.pretty'\n\nexport type LogBindings = Record<string, unknown>\n\nexport interface Logger {\n debug(msg: string, fields?: LogBindings): void\n info(msg: string, fields?: LogBindings): void\n warn(msg: string, fields?: LogBindings): void\n error(msg: string, fields?: LogBindings): void\n /** Returns a logger with `bindings` merged into every subsequent line. */\n child(bindings: LogBindings): Logger\n}\n\nconst loggerRegistry = new Map<string, Logger>()\n\n/** Create (or reuse) a namespaced logger. `namespace` is attached as `name`. */\nexport function createLogger(namespace: string): Logger {\n const existing = loggerRegistry.get(namespace)\n if (existing) return existing\n const created = selectTransport(namespace)\n loggerRegistry.set(namespace, created)\n return created\n}\n\n/** Internal: clear cached loggers so tests can re-run transport selection. */\nexport function resetLoggerRegistry(): void {\n loggerRegistry.clear()\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,uBAAuB;
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import { selectTransport } from './transport'\nimport { getLoggerExtension } from './extension'\nimport { isLevelEnabled, type LogLevel } from './level'\n\nexport type { LogLevel } from './level'\nexport { getLogLevel, isLevelEnabled, resetLogLevelCache, OM_LOG_LEVEL_ENV } from './level'\nexport { resetServerLoggerCache, OM_LOG_DESTINATION_ENV } from './transport.server'\nexport { resetLogPrettyCache, OM_LOG_PRETTY_ENV } from './transport.pretty'\nexport {\n getLoggerExtension,\n registerLoggerExtension,\n resetLoggerExtension,\n} from './extension'\nexport type { LoggerExtension, LoggerExtensionRecord } from './extension'\n\nexport type LogBindings = Record<string, unknown>\n\nexport interface Logger {\n debug(msg: string, fields?: LogBindings): void\n info(msg: string, fields?: LogBindings): void\n warn(msg: string, fields?: LogBindings): void\n error(msg: string, fields?: LogBindings): void\n /** Returns a logger with `bindings` merged into every subsequent line. */\n child(bindings: LogBindings): Logger\n}\n\nconst loggerRegistry = new Map<string, Logger>()\n\nfunction createExtendedLogger(\n namespace: string,\n transport: Logger,\n bindings: LogBindings = {},\n): Logger {\n const emit = (\n level: LogLevel,\n msg: string,\n fields?: LogBindings,\n ): void => {\n if (!isLevelEnabled(level)) return\n\n const extension = getLoggerExtension()\n let context: LogBindings = {}\n try {\n context = extension?.enrich?.() ?? {}\n } catch {\n // Observability must never alter application behavior.\n }\n\n const mergedFields = { ...fields, ...context }\n transport[level](msg, mergedFields)\n\n try {\n extension?.emit?.({\n level,\n namespace,\n message: msg,\n fields: { ...bindings, ...mergedFields },\n time: Date.now(),\n })\n } catch {\n // Remote logging is best-effort; the local line has already been written.\n }\n }\n\n return {\n debug: (msg, fields) => emit('debug', msg, fields),\n info: (msg, fields) => emit('info', msg, fields),\n warn: (msg, fields) => emit('warn', msg, fields),\n error: (msg, fields) => emit('error', msg, fields),\n child: (childBindings) =>\n createExtendedLogger(\n namespace,\n transport.child(childBindings),\n { ...bindings, ...childBindings },\n ),\n }\n}\n\n/** Create (or reuse) a namespaced logger. `namespace` is attached as `name`. */\nexport function createLogger(namespace: string): Logger {\n const existing = loggerRegistry.get(namespace)\n if (existing) return existing\n const created = createExtendedLogger(namespace, selectTransport(namespace))\n loggerRegistry.set(namespace, created)\n return created\n}\n\n/** Internal: clear cached loggers so tests can re-run transport selection. */\nexport function resetLoggerRegistry(): void {\n loggerRegistry.clear()\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,sBAAqC;AAG9C,SAAS,aAAa,kBAAAA,iBAAgB,oBAAoB,wBAAwB;AAClF,SAAS,wBAAwB,8BAA8B;AAC/D,SAAS,qBAAqB,yBAAyB;AACvD;AAAA,EACE,sBAAAC;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAcP,MAAM,iBAAiB,oBAAI,IAAoB;AAE/C,SAAS,qBACP,WACA,WACA,WAAwB,CAAC,GACjB;AACR,QAAM,OAAO,CACX,OACA,KACA,WACS;AACT,QAAI,CAAC,eAAe,KAAK,EAAG;AAE5B,UAAM,YAAY,mBAAmB;AACrC,QAAI,UAAuB,CAAC;AAC5B,QAAI;AACF,gBAAU,WAAW,SAAS,KAAK,CAAC;AAAA,IACtC,QAAQ;AAAA,IAER;AAEA,UAAM,eAAe,EAAE,GAAG,QAAQ,GAAG,QAAQ;AAC7C,cAAU,KAAK,EAAE,KAAK,YAAY;AAElC,QAAI;AACF,iBAAW,OAAO;AAAA,QAChB;AAAA,QACA;AAAA,QACA,SAAS;AAAA,QACT,QAAQ,EAAE,GAAG,UAAU,GAAG,aAAa;AAAA,QACvC,MAAM,KAAK,IAAI;AAAA,MACjB,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO;AAAA,IACL,OAAO,CAAC,KAAK,WAAW,KAAK,SAAS,KAAK,MAAM;AAAA,IACjD,MAAM,CAAC,KAAK,WAAW,KAAK,QAAQ,KAAK,MAAM;AAAA,IAC/C,MAAM,CAAC,KAAK,WAAW,KAAK,QAAQ,KAAK,MAAM;AAAA,IAC/C,OAAO,CAAC,KAAK,WAAW,KAAK,SAAS,KAAK,MAAM;AAAA,IACjD,OAAO,CAAC,kBACN;AAAA,MACE;AAAA,MACA,UAAU,MAAM,aAAa;AAAA,MAC7B,EAAE,GAAG,UAAU,GAAG,cAAc;AAAA,IAClC;AAAA,EACJ;AACF;AAGO,SAAS,aAAa,WAA2B;AACtD,QAAM,WAAW,eAAe,IAAI,SAAS;AAC7C,MAAI,SAAU,QAAO;AACrB,QAAM,UAAU,qBAAqB,WAAW,gBAAgB,SAAS,CAAC;AAC1E,iBAAe,IAAI,WAAW,OAAO;AACrC,SAAO;AACT;AAGO,SAAS,sBAA4B;AAC1C,iBAAe,MAAM;AACvB;",
|
|
6
|
+
"names": ["isLevelEnabled", "getLoggerExtension"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/modules/registry.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Module } from '@open-mercato/shared/modules/registry'\nimport { applyModuleOverridesToModules } from '@open-mercato/shared/modules/overrides'\nimport { invalidateDictionaryCache } from '../i18n/dictionary-cache'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'modules-registry' })\n\n// Registration pattern for publishable packages.\n// Use globalThis to survive tsx/esbuild module duplication where the same\n// registry.ts file can be loaded as multiple module instances when mixing\n// dynamic and static imports \u2014 for example a standalone integration test\n// bootstraps via the source path while a worker handler resolves it through\n// node_modules/@open-mercato/shared/dist/. Mirrors the same workaround used\n// by `getDiRegistrars()` in `../di/container.ts`.\nconst GLOBAL_KEY = '__openMercatoModulesRegistry__'\n\nfunction getGlobalModules(): Module[] | null {\n return (globalThis as any)[GLOBAL_KEY] ?? null\n}\n\nfunction setGlobalModules(modules: Module[]): void {\n ;(globalThis as any)[GLOBAL_KEY] = modules\n}\n\nfunction hasRuntimeContracts(entry: Module): boolean {\n return Object.keys(entry).some((key) => key !== 'id' && key !== 'translations')\n}\n\nfunction isI18nOnlyRegistration(modules: Module[]): boolean {\n return modules.length > 0 && modules.every((entry) => !hasRuntimeContracts(entry))\n}\n\nfunction mergeI18nModules(existing: Module[], incoming: Module[]): Module[] {\n const incomingById = new Map(incoming.map((entry) => [entry.id, entry]))\n const existingIds = new Set(existing.map((entry) => entry.id))\n const merged = existing.map((entry) => {\n const i18nModule = incomingById.get(entry.id)\n if (!i18nModule?.translations) return entry\n return {\n ...entry,\n translations: i18nModule.translations,\n }\n })\n\n for (const entry of incoming) {\n if (!existingIds.has(entry.id)) merged.push(entry)\n }\n\n return merged\n}\n\nexport function registerModules(modules: Module[]) {\n const existing = getGlobalModules()\n if (existing !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Modules re-registered (this may occur during HMR)')\n }\n const nextModules = applyModuleOverridesToModules(modules)\n const shouldMergeI18nOnly = existing !== null\n && existing.some(hasRuntimeContracts)\n && isI18nOnlyRegistration(nextModules)\n setGlobalModules(shouldMergeI18nOnly ? mergeI18nModules(existing, nextModules) : nextModules)\n invalidateDictionaryCache()\n}\n\nexport function getModules(): Module[] {\n const modules = getGlobalModules()\n if (!modules) {\n throw new Error('[Bootstrap] Modules not registered. Call registerModules() at bootstrap.')\n }\n return modules\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,qCAAqC;AAC9C,SAAS,iCAAiC;AAC1C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,mBAAmB,CAAC;AAS7E,MAAM,aAAa;AAEnB,SAAS,mBAAoC;AAC3C,SAAQ,WAAmB,UAAU,KAAK;AAC5C;AAEA,SAAS,iBAAiB,SAAyB;AACjD;AAAC,EAAC,WAAmB,UAAU,IAAI;AACrC;AAEA,SAAS,oBAAoB,OAAwB;AACnD,SAAO,OAAO,KAAK,KAAK,EAAE,KAAK,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,cAAc;AAChF;AAEA,SAAS,uBAAuB,SAA4B;AAC1D,SAAO,QAAQ,SAAS,KAAK,QAAQ,MAAM,CAAC,UAAU,CAAC,oBAAoB,KAAK,CAAC;AACnF;AAEA,SAAS,iBAAiB,UAAoB,UAA8B;AAC1E,QAAM,eAAe,IAAI,IAAI,SAAS,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC;AACvE,QAAM,cAAc,IAAI,IAAI,SAAS,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC;AAC7D,QAAM,SAAS,SAAS,IAAI,CAAC,UAAU;AACrC,UAAM,aAAa,aAAa,IAAI,MAAM,EAAE;AAC5C,QAAI,CAAC,YAAY,aAAc,QAAO;AACtC,WAAO;AAAA,MACL,GAAG;AAAA,MACH,cAAc,WAAW;AAAA,IAC3B;AAAA,EACF,CAAC;AAED,aAAW,SAAS,UAAU;AAC5B,QAAI,CAAC,YAAY,IAAI,MAAM,EAAE,EAAG,QAAO,KAAK,KAAK;AAAA,EACnD;AAEA,SAAO;AACT;AAEO,SAAS,gBAAgB,SAAmB;AACjD,QAAM,WAAW,iBAAiB;AAClC,MAAI,aAAa,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC/D,WAAO,MAAM,mDAAmD;AAAA,EAClE;AACA,QAAM,cAAc,8BAA8B,OAAO;AACzD,QAAM,sBAAsB,aAAa,QACpC,SAAS,KAAK,mBAAmB,KACjC,uBAAuB,WAAW;AACvC,mBAAiB,sBAAsB,iBAAiB,UAAU,WAAW,IAAI,WAAW;AAC5F,4BAA0B;AAC5B;AAEO,SAAS,aAAuB;AACrC,QAAM,UAAU,iBAAiB;AACjC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,0EAA0E;AAAA,EAC5F;AACA,SAAO;AACT;",
|
|
4
|
+
"sourcesContent": ["import type { Module } from '@open-mercato/shared/modules/registry'\nimport { applyModuleOverridesToModules } from '@open-mercato/shared/modules/overrides'\nimport { invalidateDictionaryCache } from '../i18n/dictionary-cache'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'modules-registry' })\n\n// Registration pattern for publishable packages.\n// Use globalThis to survive tsx/esbuild module duplication where the same\n// registry.ts file can be loaded as multiple module instances when mixing\n// dynamic and static imports \u2014 for example a standalone integration test\n// bootstraps via the source path while a worker handler resolves it through\n// node_modules/@open-mercato/shared/dist/. Mirrors the same workaround used\n// by `getDiRegistrars()` in `../di/container.ts`.\nconst GLOBAL_KEY = '__openMercatoModulesRegistry__'\n\nfunction getGlobalModules(): Module[] | null {\n return (globalThis as any)[GLOBAL_KEY] ?? null\n}\n\nfunction setGlobalModules(modules: Module[]): void {\n ;(globalThis as any)[GLOBAL_KEY] = modules\n}\n\nfunction hasRuntimeContracts(entry: Module): boolean {\n return Object.keys(entry).some((key) => key !== 'id' && key !== 'translations')\n}\n\nfunction isI18nOnlyRegistration(modules: Module[]): boolean {\n return modules.length > 0 && modules.every((entry) => !hasRuntimeContracts(entry))\n}\n\nfunction mergeI18nModules(existing: Module[], incoming: Module[]): Module[] {\n const incomingById = new Map(incoming.map((entry) => [entry.id, entry]))\n const existingIds = new Set(existing.map((entry) => entry.id))\n const merged = existing.map((entry) => {\n const i18nModule = incomingById.get(entry.id)\n if (!i18nModule?.translations) return entry\n return {\n ...entry,\n translations: i18nModule.translations,\n }\n })\n\n for (const entry of incoming) {\n if (!existingIds.has(entry.id)) merged.push(entry)\n }\n\n return merged\n}\n\nexport function registerModules(modules: Module[]) {\n const existing = getGlobalModules()\n if (existing !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Modules re-registered (this may occur during HMR)')\n }\n const nextModules = applyModuleOverridesToModules(modules)\n const shouldMergeI18nOnly = existing !== null\n && existing.some(hasRuntimeContracts)\n && isI18nOnlyRegistration(nextModules)\n setGlobalModules(shouldMergeI18nOnly ? mergeI18nModules(existing, nextModules) : nextModules)\n invalidateDictionaryCache()\n}\n\nexport function getModules(): Module[] {\n const modules = getGlobalModules()\n if (!modules) {\n throw new Error('[Bootstrap] Modules not registered. Call registerModules() at bootstrap.')\n }\n return modules\n}\n\n/**\n * Non-throwing counterpart of `getModules()` for call sites that have a\n * meaningful degraded behavior when bootstrap has not run \u2014 route unit tests\n * exercise a single handler without `registerModules()`, and a hard throw there\n * turns an unrelated assertion into a bootstrap error.\n */\nexport function tryGetModules(): Module[] | null {\n return getGlobalModules()\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,qCAAqC;AAC9C,SAAS,iCAAiC;AAC1C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,mBAAmB,CAAC;AAS7E,MAAM,aAAa;AAEnB,SAAS,mBAAoC;AAC3C,SAAQ,WAAmB,UAAU,KAAK;AAC5C;AAEA,SAAS,iBAAiB,SAAyB;AACjD;AAAC,EAAC,WAAmB,UAAU,IAAI;AACrC;AAEA,SAAS,oBAAoB,OAAwB;AACnD,SAAO,OAAO,KAAK,KAAK,EAAE,KAAK,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,cAAc;AAChF;AAEA,SAAS,uBAAuB,SAA4B;AAC1D,SAAO,QAAQ,SAAS,KAAK,QAAQ,MAAM,CAAC,UAAU,CAAC,oBAAoB,KAAK,CAAC;AACnF;AAEA,SAAS,iBAAiB,UAAoB,UAA8B;AAC1E,QAAM,eAAe,IAAI,IAAI,SAAS,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC;AACvE,QAAM,cAAc,IAAI,IAAI,SAAS,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC;AAC7D,QAAM,SAAS,SAAS,IAAI,CAAC,UAAU;AACrC,UAAM,aAAa,aAAa,IAAI,MAAM,EAAE;AAC5C,QAAI,CAAC,YAAY,aAAc,QAAO;AACtC,WAAO;AAAA,MACL,GAAG;AAAA,MACH,cAAc,WAAW;AAAA,IAC3B;AAAA,EACF,CAAC;AAED,aAAW,SAAS,UAAU;AAC5B,QAAI,CAAC,YAAY,IAAI,MAAM,EAAE,EAAG,QAAO,KAAK,KAAK;AAAA,EACnD;AAEA,SAAO;AACT;AAEO,SAAS,gBAAgB,SAAmB;AACjD,QAAM,WAAW,iBAAiB;AAClC,MAAI,aAAa,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC/D,WAAO,MAAM,mDAAmD;AAAA,EAClE;AACA,QAAM,cAAc,8BAA8B,OAAO;AACzD,QAAM,sBAAsB,aAAa,QACpC,SAAS,KAAK,mBAAmB,KACjC,uBAAuB,WAAW;AACvC,mBAAiB,sBAAsB,iBAAiB,UAAU,WAAW,IAAI,WAAW;AAC5F,4BAA0B;AAC5B;AAEO,SAAS,aAAuB;AACrC,QAAM,UAAU,iBAAiB;AACjC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,0EAA0E;AAAA,EAC5F;AACA,SAAO;AACT;AAQO,SAAS,gBAAiC;AAC/C,SAAO,iBAAiB;AAC1B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const GLOBAL_KEY = /* @__PURE__ */ Symbol.for("@open-mercato/shared.telemetryRuntime");
|
|
2
|
+
const ENABLED_BACKENDS = /* @__PURE__ */ new Set(["console", "signoz", "newrelic", "otlp"]);
|
|
3
|
+
function store() {
|
|
4
|
+
const globalStore = globalThis;
|
|
5
|
+
let current = globalStore[GLOBAL_KEY];
|
|
6
|
+
if (!current) {
|
|
7
|
+
current = {};
|
|
8
|
+
globalStore[GLOBAL_KEY] = current;
|
|
9
|
+
}
|
|
10
|
+
return current;
|
|
11
|
+
}
|
|
12
|
+
function isTelemetryBackendEnabled(raw) {
|
|
13
|
+
const value = raw ?? (typeof process === "undefined" ? void 0 : process.env.TELEMETRY_BACKEND);
|
|
14
|
+
return ENABLED_BACKENDS.has((value ?? "").trim().toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
function registerTelemetryRuntime(runtime) {
|
|
17
|
+
store().active = runtime;
|
|
18
|
+
return () => {
|
|
19
|
+
const current = store();
|
|
20
|
+
if (current.active === runtime) current.active = void 0;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function getTelemetryRuntime() {
|
|
24
|
+
return store().active;
|
|
25
|
+
}
|
|
26
|
+
function resetTelemetryRuntime() {
|
|
27
|
+
store().active = void 0;
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
getTelemetryRuntime,
|
|
31
|
+
isTelemetryBackendEnabled,
|
|
32
|
+
registerTelemetryRuntime,
|
|
33
|
+
resetTelemetryRuntime
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/telemetry/runtime.ts"],
|
|
4
|
+
"sourcesContent": ["export type TelemetryTraceCarrier = Record<string, string>\n\nexport type TelemetryRuntime = {\n /**\n * True only when the active SDK may safely use the process-global W3C\n * propagator for cross-boundary extraction.\n */\n canUseGlobalTracePropagation(): boolean\n captureTraceContext(): TelemetryTraceCarrier\n continueTrace<T>(\n carrier: TelemetryTraceCarrier | undefined,\n name: string,\n fn: () => T,\n options?: { kind?: 'internal' | 'server' | 'client' | 'producer' | 'consumer' },\n ): T\n recordHttpDuration(method: string, route: string, status: number, startedAt: number): void\n reportError(\n error: unknown,\n context?: {\n module?: string\n attributes?: Record<string, string | number | boolean | undefined>\n },\n ): void\n shutdown(): Promise<void>\n}\n\nconst GLOBAL_KEY = Symbol.for('@open-mercato/shared.telemetryRuntime')\nconst ENABLED_BACKENDS = new Set(['console', 'signoz', 'newrelic', 'otlp'])\n\ntype TelemetryRuntimeStore = {\n active?: TelemetryRuntime\n}\n\nfunction store(): TelemetryRuntimeStore {\n const globalStore = globalThis as unknown as Record<symbol, TelemetryRuntimeStore | undefined>\n let current = globalStore[GLOBAL_KEY]\n if (!current) {\n current = {}\n globalStore[GLOBAL_KEY] = current\n }\n return current\n}\n\n/**\n * This check is intentionally owned by shared code so hosts can decide whether\n * to dynamically import the telemetry package without evaluating that package.\n */\nexport function isTelemetryBackendEnabled(raw?: string): boolean {\n const value = raw ?? (\n typeof process === 'undefined'\n ? undefined\n : process.env.TELEMETRY_BACKEND\n )\n return ENABLED_BACKENDS.has((value ?? '').trim().toLowerCase())\n}\n\nexport function registerTelemetryRuntime(runtime: TelemetryRuntime): () => void {\n store().active = runtime\n return () => {\n const current = store()\n if (current.active === runtime) current.active = undefined\n }\n}\n\nexport function getTelemetryRuntime(): TelemetryRuntime | undefined {\n return store().active\n}\n\n/** Test-only: clear the process-wide telemetry bridge. */\nexport function resetTelemetryRuntime(): void {\n store().active = undefined\n}\n"],
|
|
5
|
+
"mappings": "AA0BA,MAAM,aAAa,uBAAO,IAAI,uCAAuC;AACrE,MAAM,mBAAmB,oBAAI,IAAI,CAAC,WAAW,UAAU,YAAY,MAAM,CAAC;AAM1E,SAAS,QAA+B;AACtC,QAAM,cAAc;AACpB,MAAI,UAAU,YAAY,UAAU;AACpC,MAAI,CAAC,SAAS;AACZ,cAAU,CAAC;AACX,gBAAY,UAAU,IAAI;AAAA,EAC5B;AACA,SAAO;AACT;AAMO,SAAS,0BAA0B,KAAuB;AAC/D,QAAM,QAAQ,QACZ,OAAO,YAAY,cACf,SACA,QAAQ,IAAI;AAElB,SAAO,iBAAiB,KAAK,SAAS,IAAI,KAAK,EAAE,YAAY,CAAC;AAChE;AAEO,SAAS,yBAAyB,SAAuC;AAC9E,QAAM,EAAE,SAAS;AACjB,SAAO,MAAM;AACX,UAAM,UAAU,MAAM;AACtB,QAAI,QAAQ,WAAW,QAAS,SAAQ,SAAS;AAAA,EACnD;AACF;AAEO,SAAS,sBAAoD;AAClE,SAAO,MAAM,EAAE;AACjB;AAGO,SAAS,wBAA8B;AAC5C,QAAM,EAAE,SAAS;AACnB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.6834.1.e76f4b8cbc';\nexport const appVersion = APP_VERSION;\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6834.1.e76f4b8cbc",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"@mikro-orm/core": "^7.1.8",
|
|
106
106
|
"@mikro-orm/decorators": "^7.1.8",
|
|
107
107
|
"@mikro-orm/postgresql": "^7.1.8",
|
|
108
|
-
"@open-mercato/cache": "0.6.7-develop.
|
|
108
|
+
"@open-mercato/cache": "0.6.7-develop.6834.1.e76f4b8cbc",
|
|
109
109
|
"@types/sanitize-html": "^2.16.1",
|
|
110
110
|
"dotenv": "^17.4.2",
|
|
111
111
|
"pino": "^10.3.1",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*
|
|
4
|
+
* The CLI bundle's `@/` alias must mirror what the app tsconfig maps, because the dynamic
|
|
5
|
+
* bootstrap path compiles app sources (`src/di.ts`) and generated registries that import app
|
|
6
|
+
* modules through the alias. The tsconfig maps `@/.mercato/*` to the app root and every other
|
|
7
|
+
* `@/*` to the app's `src/` directory; resolving everything against the app root silently sends
|
|
8
|
+
* `@/lib/x` to a path that does not exist.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs'
|
|
11
|
+
import os from 'node:os'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
|
|
14
|
+
import { createCliBundlePlugins } from '../dynamicLoader'
|
|
15
|
+
|
|
16
|
+
async function bundleWithCliPlugins(appRoot: string, entry: string): Promise<string> {
|
|
17
|
+
const esbuild = await import('esbuild')
|
|
18
|
+
const outfile = path.join(appRoot, 'bundle.mjs')
|
|
19
|
+
await esbuild.build({
|
|
20
|
+
entryPoints: [entry],
|
|
21
|
+
outfile,
|
|
22
|
+
absWorkingDir: appRoot,
|
|
23
|
+
bundle: true,
|
|
24
|
+
format: 'esm',
|
|
25
|
+
platform: 'node',
|
|
26
|
+
target: 'node18',
|
|
27
|
+
plugins: createCliBundlePlugins(appRoot),
|
|
28
|
+
})
|
|
29
|
+
return fs.readFileSync(outfile, 'utf8')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('createCliBundlePlugins — @/ alias resolution', () => {
|
|
33
|
+
let appRoot: string
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'om-alias-resolver-'))
|
|
37
|
+
fs.mkdirSync(path.join(appRoot, 'src', 'lib'), { recursive: true })
|
|
38
|
+
fs.mkdirSync(path.join(appRoot, 'src', 'modules', 'demo'), { recursive: true })
|
|
39
|
+
fs.mkdirSync(path.join(appRoot, '.mercato', 'generated'), { recursive: true })
|
|
40
|
+
|
|
41
|
+
fs.writeFileSync(
|
|
42
|
+
path.join(appRoot, 'src', 'lib', 'marker.ts'),
|
|
43
|
+
"export const srcLibMarker = 'resolved-from-src-lib'\n",
|
|
44
|
+
)
|
|
45
|
+
fs.writeFileSync(
|
|
46
|
+
path.join(appRoot, 'src', 'modules', 'demo', 'index.ts'),
|
|
47
|
+
"export const srcModuleMarker = 'resolved-from-src-module-index'\n",
|
|
48
|
+
)
|
|
49
|
+
fs.writeFileSync(
|
|
50
|
+
path.join(appRoot, '.mercato', 'generated', 'registry.generated.ts'),
|
|
51
|
+
"export const generatedMarker = 'resolved-from-generated'\n",
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
fs.rmSync(appRoot, { recursive: true, force: true })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('resolves @/* against the app src directory and @/.mercato/* against the app root', async () => {
|
|
60
|
+
const entry = path.join(appRoot, '.mercato', 'generated', 'entry.ts')
|
|
61
|
+
fs.writeFileSync(
|
|
62
|
+
entry,
|
|
63
|
+
[
|
|
64
|
+
"import { srcLibMarker } from '@/lib/marker'",
|
|
65
|
+
"import { srcModuleMarker } from '@/modules/demo'",
|
|
66
|
+
"import { generatedMarker } from '@/.mercato/generated/registry.generated'",
|
|
67
|
+
'export const markers = [srcLibMarker, srcModuleMarker, generatedMarker]',
|
|
68
|
+
'',
|
|
69
|
+
].join('\n'),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const output = await bundleWithCliPlugins(appRoot, entry)
|
|
73
|
+
|
|
74
|
+
expect(output).toContain('resolved-from-src-lib')
|
|
75
|
+
expect(output).toContain('resolved-from-src-module-index')
|
|
76
|
+
expect(output).toContain('resolved-from-generated')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('prefers an app-root match when the specifier has no src counterpart', async () => {
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
path.join(appRoot, 'root-only.ts'),
|
|
82
|
+
"export const rootOnlyMarker = 'resolved-from-app-root'\n",
|
|
83
|
+
)
|
|
84
|
+
const entry = path.join(appRoot, '.mercato', 'generated', 'entry.ts')
|
|
85
|
+
fs.writeFileSync(
|
|
86
|
+
entry,
|
|
87
|
+
[
|
|
88
|
+
"import { rootOnlyMarker } from '@/root-only'",
|
|
89
|
+
'export const markers = [rootOnlyMarker]',
|
|
90
|
+
'',
|
|
91
|
+
].join('\n'),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
const output = await bundleWithCliPlugins(appRoot, entry)
|
|
95
|
+
|
|
96
|
+
expect(output).toContain('resolved-from-app-root')
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*
|
|
4
|
+
* Regression guard: the worker/CLI/scheduler bootstrap path (bootstrapFromAppRoot) must register
|
|
5
|
+
* the app-level DI hook (`src/di.ts`) the same way the Next.js runtime does through its own
|
|
6
|
+
* `src/bootstrap.ts`. Without it, `getAppDiRegistrar()` stays null in those processes, so the app's
|
|
7
|
+
* registrations silently never run there and every request container falls back to a doomed
|
|
8
|
+
* `import('@/di')` — the `@/` alias does not exist in a plain Node ESM process, so it throws
|
|
9
|
+
* ERR_MODULE_NOT_FOUND once per created container (once per queued job).
|
|
10
|
+
*
|
|
11
|
+
* Like the #4327 guard next to it, this test authors both the `.ts` sources and fresh compiled
|
|
12
|
+
* `.mjs` siblings so compileAndImport takes its cache path and never invokes esbuild. The `.mjs`
|
|
13
|
+
* stubs use module.exports because Jest's CJS runtime handles the dynamic import().
|
|
14
|
+
*
|
|
15
|
+
* `bootstrapFromAppRoot` reaches the factory through `import('./factory.js')`, a specifier with no
|
|
16
|
+
* on-disk counterpart under Jest's CJS resolver, so it is mocked virtually.
|
|
17
|
+
*/
|
|
18
|
+
jest.mock('../../logger', () => {
|
|
19
|
+
const logger = {
|
|
20
|
+
debug: jest.fn(),
|
|
21
|
+
info: jest.fn(),
|
|
22
|
+
warn: jest.fn(),
|
|
23
|
+
error: jest.fn(),
|
|
24
|
+
child: () => logger,
|
|
25
|
+
}
|
|
26
|
+
return { createLogger: () => logger }
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const createBootstrapMock = jest.fn(() => () => {})
|
|
30
|
+
|
|
31
|
+
jest.mock(
|
|
32
|
+
'../factory.js',
|
|
33
|
+
() => ({
|
|
34
|
+
createBootstrap: (...args: unknown[]) => createBootstrapMock(...(args as [])),
|
|
35
|
+
waitForAsyncRegistration: async () => {},
|
|
36
|
+
}),
|
|
37
|
+
{ virtual: true },
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
import fs from 'node:fs'
|
|
41
|
+
import os from 'node:os'
|
|
42
|
+
import path from 'node:path'
|
|
43
|
+
import crypto from 'node:crypto'
|
|
44
|
+
import { createLogger } from '../../logger'
|
|
45
|
+
import { bootstrapFromAppRoot } from '../dynamicLoader'
|
|
46
|
+
|
|
47
|
+
const mockedLogger = createLogger('shared') as unknown as {
|
|
48
|
+
debug: jest.Mock
|
|
49
|
+
error: jest.Mock
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const APP_DI_CALL_COUNTER_KEY = '__omTestAppDiRegisterCalls__'
|
|
53
|
+
|
|
54
|
+
const GENERATED_MODULES: Record<string, { ts: string; compiled: string }> = {
|
|
55
|
+
'entities.ids.generated': { ts: 'export const E = {}', compiled: 'module.exports = { E: {} }' },
|
|
56
|
+
'modules.cli.generated': { ts: 'export const modules = []', compiled: 'module.exports = { modules: [] }' },
|
|
57
|
+
'entities.generated': { ts: 'export const entities = []', compiled: 'module.exports = { entities: [] }' },
|
|
58
|
+
'di.generated': { ts: 'export const diRegistrars = []', compiled: 'module.exports = { diRegistrars: [] }' },
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const APP_TSCONFIG = JSON.stringify({
|
|
62
|
+
compilerOptions: {
|
|
63
|
+
experimentalDecorators: true,
|
|
64
|
+
emitDecoratorMetadata: true,
|
|
65
|
+
useDefineForClassFields: false,
|
|
66
|
+
target: 'ES2022',
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const APP_DI_TS = 'export function register() {}'
|
|
71
|
+
|
|
72
|
+
const APP_DI_COMPILED = [
|
|
73
|
+
'module.exports = {',
|
|
74
|
+
' register: () => {',
|
|
75
|
+
` globalThis['${APP_DI_CALL_COUNTER_KEY}'] = (globalThis['${APP_DI_CALL_COUNTER_KEY}'] ?? 0) + 1`,
|
|
76
|
+
' },',
|
|
77
|
+
'}',
|
|
78
|
+
].join('\n')
|
|
79
|
+
|
|
80
|
+
function hash(content: string): string {
|
|
81
|
+
return crypto.createHash('sha256').update(content).digest('hex')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeCompiledPair(
|
|
85
|
+
appRoot: string,
|
|
86
|
+
sourcePath: string,
|
|
87
|
+
compiledPath: string,
|
|
88
|
+
source: { ts: string; compiled: string },
|
|
89
|
+
) {
|
|
90
|
+
fs.writeFileSync(sourcePath, source.ts)
|
|
91
|
+
fs.writeFileSync(compiledPath, source.compiled)
|
|
92
|
+
const sourceRelativePath = path.relative(appRoot, sourcePath).split(path.sep).join('/')
|
|
93
|
+
fs.writeFileSync(`${compiledPath}.cache.json`, JSON.stringify({
|
|
94
|
+
version: 4,
|
|
95
|
+
inputHash: hash(JSON.stringify({
|
|
96
|
+
version: 4,
|
|
97
|
+
sourceHash: hash(source.ts),
|
|
98
|
+
tsconfigHashes: {
|
|
99
|
+
'tsconfig.json': hash(APP_TSCONFIG),
|
|
100
|
+
},
|
|
101
|
+
})),
|
|
102
|
+
outputHash: hash(source.compiled),
|
|
103
|
+
dependencies: {
|
|
104
|
+
[sourceRelativePath]: hash(source.ts),
|
|
105
|
+
},
|
|
106
|
+
}))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const createdAppRoots: string[] = []
|
|
110
|
+
|
|
111
|
+
function createAppRoot(appDi: { ts: string; compiled: string } | null): string {
|
|
112
|
+
const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'om-bootstrap-app-di-'))
|
|
113
|
+
const generatedDir = path.join(appRoot, '.mercato', 'generated')
|
|
114
|
+
fs.mkdirSync(generatedDir, { recursive: true })
|
|
115
|
+
fs.writeFileSync(path.join(appRoot, 'tsconfig.json'), APP_TSCONFIG)
|
|
116
|
+
|
|
117
|
+
for (const [baseName, source] of Object.entries(GENERATED_MODULES)) {
|
|
118
|
+
writeCompiledPair(
|
|
119
|
+
appRoot,
|
|
120
|
+
path.join(generatedDir, `${baseName}.ts`),
|
|
121
|
+
path.join(generatedDir, `${baseName}.mjs`),
|
|
122
|
+
source,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (appDi) {
|
|
127
|
+
fs.mkdirSync(path.join(appRoot, 'src'), { recursive: true })
|
|
128
|
+
writeCompiledPair(
|
|
129
|
+
appRoot,
|
|
130
|
+
path.join(appRoot, 'src', 'di.ts'),
|
|
131
|
+
path.join(generatedDir, 'app-di.compiled.mjs'),
|
|
132
|
+
appDi,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
createdAppRoots.push(appRoot)
|
|
137
|
+
return appRoot
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function appDiRegistrarArgument(): unknown {
|
|
141
|
+
const [, options] = createBootstrapMock.mock.calls[0] as unknown as [unknown, Record<string, unknown>]
|
|
142
|
+
return options?.appDiRegistrar
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
describe('bootstrapFromAppRoot — app-level DI reaches worker/CLI processes', () => {
|
|
146
|
+
afterAll(() => {
|
|
147
|
+
for (const root of createdAppRoots) {
|
|
148
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
createBootstrapMock.mockClear()
|
|
154
|
+
mockedLogger.debug.mockClear()
|
|
155
|
+
mockedLogger.error.mockClear()
|
|
156
|
+
delete (globalThis as Record<string, unknown>)[APP_DI_CALL_COUNTER_KEY]
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('passes the app DI register() from src/di.ts to createBootstrap', async () => {
|
|
160
|
+
const appRoot = createAppRoot({ ts: APP_DI_TS, compiled: APP_DI_COMPILED })
|
|
161
|
+
|
|
162
|
+
await bootstrapFromAppRoot(appRoot)
|
|
163
|
+
|
|
164
|
+
const registrar = appDiRegistrarArgument()
|
|
165
|
+
expect(typeof registrar).toBe('function')
|
|
166
|
+
|
|
167
|
+
await (registrar as (container: unknown) => unknown)({})
|
|
168
|
+
expect((globalThis as Record<string, unknown>)[APP_DI_CALL_COUNTER_KEY]).toBe(1)
|
|
169
|
+
expect(mockedLogger.error).not.toHaveBeenCalled()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('bootstraps without an app DI registrar when src/di.ts is absent', async () => {
|
|
173
|
+
const appRoot = createAppRoot(null)
|
|
174
|
+
|
|
175
|
+
await bootstrapFromAppRoot(appRoot)
|
|
176
|
+
|
|
177
|
+
expect(createBootstrapMock).toHaveBeenCalledTimes(1)
|
|
178
|
+
expect(appDiRegistrarArgument()).toBeUndefined()
|
|
179
|
+
expect(mockedLogger.error).not.toHaveBeenCalled()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('reports an error and keeps bootstrapping when src/di.ts fails to load', async () => {
|
|
183
|
+
const appRoot = createAppRoot({
|
|
184
|
+
ts: APP_DI_TS,
|
|
185
|
+
compiled: "throw new Error('src/di.ts is broken')",
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
await bootstrapFromAppRoot(appRoot)
|
|
189
|
+
|
|
190
|
+
expect(createBootstrapMock).toHaveBeenCalledTimes(1)
|
|
191
|
+
expect(appDiRegistrarArgument()).toBeUndefined()
|
|
192
|
+
expect(mockedLogger.error).toHaveBeenCalledTimes(1)
|
|
193
|
+
const [message, fields] = mockedLogger.error.mock.calls[0] as [string, Record<string, unknown>]
|
|
194
|
+
expect(message).toContain('Failed to load the app-level DI module')
|
|
195
|
+
expect((fields.err as Error).message).toContain('src/di.ts is broken')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('reports an error and keeps bootstrapping when src/di.ts exports no register()', async () => {
|
|
199
|
+
const appRoot = createAppRoot({
|
|
200
|
+
ts: APP_DI_TS,
|
|
201
|
+
compiled: 'module.exports = {}',
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
await bootstrapFromAppRoot(appRoot)
|
|
205
|
+
|
|
206
|
+
expect(createBootstrapMock).toHaveBeenCalledTimes(1)
|
|
207
|
+
expect(appDiRegistrarArgument()).toBeUndefined()
|
|
208
|
+
expect(mockedLogger.error).toHaveBeenCalledTimes(1)
|
|
209
|
+
expect(mockedLogger.error.mock.calls[0][0]).toContain('exports no register()')
|
|
210
|
+
})
|
|
211
|
+
})
|