@chidchanun/bcp 0.3.1 → 0.3.2

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.
@@ -1082,6 +1082,382 @@ function withTimeout(promise, timeoutMs, label) {
1082
1082
  );
1083
1083
  }
1084
1084
 
1085
+ // packages/server/src/modules.ts
1086
+ var MODULE_V2_KIND = "bcp-module-v2";
1087
+ var ModuleDependencyError = class extends Error {
1088
+ constructor(message) {
1089
+ super(message);
1090
+ this.name = "ModuleDependencyError";
1091
+ }
1092
+ };
1093
+ var ModuleLifecycleError = class extends Error {
1094
+ module;
1095
+ phase;
1096
+ cause;
1097
+ constructor(moduleName, phase, cause) {
1098
+ super(
1099
+ `BCP Modules: ${phase} failed for module "${moduleName}": ${formatError(cause)}`
1100
+ );
1101
+ this.name = "ModuleLifecycleError";
1102
+ this.module = moduleName;
1103
+ this.phase = phase;
1104
+ this.cause = cause;
1105
+ }
1106
+ };
1107
+ function isModuleDefinition(value) {
1108
+ return Boolean(
1109
+ value && typeof value === "object" && value.kind === MODULE_V2_KIND
1110
+ );
1111
+ }
1112
+ function composeModules(roots = []) {
1113
+ const ordered = resolveModuleOrder(roots);
1114
+ const providers = [];
1115
+ const plugins = [];
1116
+ const services = [];
1117
+ const resources = [];
1118
+ const serviceKeys = /* @__PURE__ */ new Set();
1119
+ for (const module of ordered) {
1120
+ providers.push(
1121
+ ...module.providers ?? []
1122
+ );
1123
+ plugins.push(
1124
+ ...module.plugins ?? []
1125
+ );
1126
+ resources.push(
1127
+ ...module.resources ?? []
1128
+ );
1129
+ for (const entry of module.services ?? []) {
1130
+ const [key] = entry;
1131
+ if (serviceKeys.has(key)) {
1132
+ throw new ModuleDependencyError(
1133
+ `BCP Modules: duplicate shared service ${formatServiceKey(key)} while composing module "${module.name}".`
1134
+ );
1135
+ }
1136
+ serviceKeys.add(key);
1137
+ services.push(entry);
1138
+ }
1139
+ validateModuleExports(
1140
+ module
1141
+ );
1142
+ }
1143
+ return {
1144
+ modules: ordered,
1145
+ providers,
1146
+ plugins,
1147
+ services,
1148
+ resources,
1149
+ records() {
1150
+ return ordered.map(
1151
+ (module) => ({
1152
+ name: module.name,
1153
+ ...module.version ? {
1154
+ version: module.version
1155
+ } : {},
1156
+ imports: (module.imports ?? []).map(
1157
+ (dependency) => dependency.name
1158
+ ),
1159
+ providers: (module.providers ?? []).map(
1160
+ (provider) => provider.token.description
1161
+ ),
1162
+ exports: (module.exports ?? []).map(
1163
+ (token) => token.description
1164
+ ),
1165
+ plugins: (module.plugins ?? []).map(
1166
+ (plugin) => plugin.name
1167
+ ),
1168
+ resources: (module.resources ?? []).map(
1169
+ (resource) => resource.name
1170
+ )
1171
+ })
1172
+ );
1173
+ },
1174
+ exportedTokens(moduleName) {
1175
+ const normalized = normalizeName3(
1176
+ moduleName,
1177
+ "module name"
1178
+ );
1179
+ const module = ordered.find(
1180
+ (entry) => entry.name === normalized
1181
+ );
1182
+ if (!module) {
1183
+ throw new ModuleDependencyError(
1184
+ `BCP Modules: module "${normalized}" is not part of this composition.`
1185
+ );
1186
+ }
1187
+ return [
1188
+ ...module.exports ?? []
1189
+ ];
1190
+ },
1191
+ createLifecycleResources(context) {
1192
+ return ordered.flatMap(
1193
+ (module) => [
1194
+ ...module.resources ?? [],
1195
+ createModuleLifecycleResource(
1196
+ module,
1197
+ context
1198
+ )
1199
+ ]
1200
+ );
1201
+ }
1202
+ };
1203
+ }
1204
+ function resolveModuleOrder(roots) {
1205
+ const byName = /* @__PURE__ */ new Map();
1206
+ const visiting = /* @__PURE__ */ new Set();
1207
+ const visited = /* @__PURE__ */ new Set();
1208
+ const order = [];
1209
+ const visit = (module, path) => {
1210
+ validateModuleDefinition(module);
1211
+ const name = module.name;
1212
+ const existing = byName.get(name);
1213
+ if (existing && existing !== module) {
1214
+ throw new ModuleDependencyError(
1215
+ `BCP Modules: duplicate module name "${name}" refers to different definitions.`
1216
+ );
1217
+ }
1218
+ byName.set(name, module);
1219
+ if (visited.has(name)) {
1220
+ return;
1221
+ }
1222
+ if (visiting.has(name)) {
1223
+ throw new ModuleDependencyError(
1224
+ `BCP Modules: circular module dependency detected: ${[
1225
+ ...path,
1226
+ name
1227
+ ].join(" -> ")}.`
1228
+ );
1229
+ }
1230
+ visiting.add(name);
1231
+ for (const dependency of module.imports ?? []) {
1232
+ visit(
1233
+ dependency,
1234
+ [
1235
+ ...path,
1236
+ name
1237
+ ]
1238
+ );
1239
+ }
1240
+ visiting.delete(name);
1241
+ visited.add(name);
1242
+ order.push(module);
1243
+ };
1244
+ for (const root of roots) {
1245
+ visit(root, []);
1246
+ }
1247
+ return order;
1248
+ }
1249
+ function createModuleLifecycleResource(module, shared) {
1250
+ const config = parseModuleConfig(
1251
+ module.schema,
1252
+ module.config
1253
+ );
1254
+ const context = {
1255
+ name: module.name,
1256
+ config,
1257
+ container: shared.container,
1258
+ services: shared.services,
1259
+ hooks: shared.hooks
1260
+ };
1261
+ let setupComplete = false;
1262
+ let disposed = false;
1263
+ return {
1264
+ name: `bcp:module:${module.name}`,
1265
+ async start() {
1266
+ try {
1267
+ if (!setupComplete) {
1268
+ await runModuleHook(
1269
+ module,
1270
+ "setup",
1271
+ module.setup,
1272
+ context
1273
+ );
1274
+ setupComplete = true;
1275
+ }
1276
+ await runModuleHook(
1277
+ module,
1278
+ "start",
1279
+ module.start,
1280
+ context
1281
+ );
1282
+ } catch (error) {
1283
+ if (!disposed) {
1284
+ disposed = true;
1285
+ try {
1286
+ await runModuleHook(
1287
+ module,
1288
+ "dispose",
1289
+ module.dispose,
1290
+ context
1291
+ );
1292
+ } catch (disposeError) {
1293
+ throw new AggregateError(
1294
+ [
1295
+ error,
1296
+ disposeError
1297
+ ],
1298
+ `BCP Modules: startup and cleanup failed for module "${module.name}".`
1299
+ );
1300
+ }
1301
+ }
1302
+ throw error;
1303
+ }
1304
+ },
1305
+ ready() {
1306
+ return true;
1307
+ },
1308
+ async stop() {
1309
+ const errors = [];
1310
+ try {
1311
+ await runModuleHook(
1312
+ module,
1313
+ "stop",
1314
+ module.stop,
1315
+ context
1316
+ );
1317
+ } catch (error) {
1318
+ errors.push(error);
1319
+ }
1320
+ if (!disposed) {
1321
+ disposed = true;
1322
+ try {
1323
+ await runModuleHook(
1324
+ module,
1325
+ "dispose",
1326
+ module.dispose,
1327
+ context
1328
+ );
1329
+ } catch (error) {
1330
+ errors.push(error);
1331
+ }
1332
+ }
1333
+ if (errors.length > 0) {
1334
+ throw new AggregateError(
1335
+ errors,
1336
+ `BCP Modules: shutdown failed for module "${module.name}".`
1337
+ );
1338
+ }
1339
+ },
1340
+ diagnostics() {
1341
+ return {
1342
+ name: module.name,
1343
+ ...module.version ? {
1344
+ version: module.version
1345
+ } : {},
1346
+ imports: (module.imports ?? []).map(
1347
+ (dependency) => dependency.name
1348
+ ),
1349
+ exports: (module.exports ?? []).map(
1350
+ (token) => token.description
1351
+ )
1352
+ };
1353
+ }
1354
+ };
1355
+ }
1356
+ async function runModuleHook(module, phase, hook, context) {
1357
+ if (!hook) {
1358
+ return;
1359
+ }
1360
+ try {
1361
+ await hook(context);
1362
+ } catch (error) {
1363
+ throw new ModuleLifecycleError(
1364
+ module.name,
1365
+ phase,
1366
+ error
1367
+ );
1368
+ }
1369
+ }
1370
+ function validateModuleDefinition(module) {
1371
+ if (!module || typeof module !== "object") {
1372
+ throw new TypeError(
1373
+ "BCP Modules: module must be an object."
1374
+ );
1375
+ }
1376
+ if (module.kind !== MODULE_V2_KIND) {
1377
+ throw new TypeError(
1378
+ `BCP Modules: module kind must be "${MODULE_V2_KIND}". Use defineModule().`
1379
+ );
1380
+ }
1381
+ normalizeName3(
1382
+ module.name,
1383
+ "module name"
1384
+ );
1385
+ for (const [label, value] of [
1386
+ ["imports", module.imports],
1387
+ ["providers", module.providers],
1388
+ ["exports", module.exports],
1389
+ ["plugins", module.plugins],
1390
+ ["resources", module.resources]
1391
+ ]) {
1392
+ if (value !== void 0 && !Array.isArray(value)) {
1393
+ throw new TypeError(
1394
+ `BCP Modules: ${label} must be an array.`
1395
+ );
1396
+ }
1397
+ }
1398
+ for (const [phase, hook] of [
1399
+ ["setup", module.setup],
1400
+ ["start", module.start],
1401
+ ["stop", module.stop],
1402
+ ["dispose", module.dispose]
1403
+ ]) {
1404
+ if (hook !== void 0 && typeof hook !== "function") {
1405
+ throw new TypeError(
1406
+ `BCP Modules: ${phase} must be a function.`
1407
+ );
1408
+ }
1409
+ }
1410
+ }
1411
+ function validateModuleExports(module) {
1412
+ const available = /* @__PURE__ */ new Set();
1413
+ for (const provider of module.providers ?? []) {
1414
+ available.add(
1415
+ provider.token.id
1416
+ );
1417
+ }
1418
+ for (const dependency of module.imports ?? []) {
1419
+ for (const token of dependency.exports ?? []) {
1420
+ available.add(token.id);
1421
+ }
1422
+ }
1423
+ for (const token of module.exports ?? []) {
1424
+ if (!available.has(token.id)) {
1425
+ throw new ModuleDependencyError(
1426
+ `BCP Modules: module "${module.name}" exports "${token.description}" but does not provide or import it.`
1427
+ );
1428
+ }
1429
+ }
1430
+ }
1431
+ function parseModuleConfig(parser, value) {
1432
+ if (!parser) {
1433
+ return value;
1434
+ }
1435
+ if (typeof parser === "function") {
1436
+ return parser(value);
1437
+ }
1438
+ return parser.parse(value);
1439
+ }
1440
+ function normalizeName3(value, label) {
1441
+ if (typeof value !== "string") {
1442
+ throw new TypeError(
1443
+ `BCP Modules: ${label} must be a string.`
1444
+ );
1445
+ }
1446
+ const normalized = value.trim();
1447
+ if (!normalized) {
1448
+ throw new TypeError(
1449
+ `BCP Modules: ${label} cannot be empty.`
1450
+ );
1451
+ }
1452
+ return normalized;
1453
+ }
1454
+ function formatServiceKey(key) {
1455
+ return typeof key === "symbol" ? key.description ? `Symbol(${key.description})` : key.toString() : key;
1456
+ }
1457
+ function formatError(value) {
1458
+ return value instanceof Error ? value.message : String(value);
1459
+ }
1460
+
1085
1461
  // packages/server/src/plugins.ts
1086
1462
  var PluginDependencyError = class extends Error {
1087
1463
  constructor(message) {
@@ -1095,7 +1471,7 @@ var PluginLifecycleError = class extends Error {
1095
1471
  cause;
1096
1472
  constructor(plugin, phase, cause) {
1097
1473
  super(
1098
- `BCP Plugins: ${phase} failed for plugin "${plugin}": ${formatError(cause)}`
1474
+ `BCP Plugins: ${phase} failed for plugin "${plugin}": ${formatError2(cause)}`
1099
1475
  );
1100
1476
  this.name = "PluginLifecycleError";
1101
1477
  this.plugin = plugin;
@@ -1109,7 +1485,7 @@ function defineModule(module) {
1109
1485
  "BCP Plugins: module must be an object."
1110
1486
  );
1111
1487
  }
1112
- normalizeName3(
1488
+ normalizeName4(
1113
1489
  module.name,
1114
1490
  "module name"
1115
1491
  );
@@ -1130,7 +1506,7 @@ function createPluginServiceRegistry(initial) {
1130
1506
  assertServiceKey(key);
1131
1507
  if (values.has(key)) {
1132
1508
  throw new Error(
1133
- `BCP Plugins: duplicate initial service ${formatServiceKey(key)}.`
1509
+ `BCP Plugins: duplicate initial service ${formatServiceKey2(key)}.`
1134
1510
  );
1135
1511
  }
1136
1512
  values.set(key, value);
@@ -1141,7 +1517,7 @@ function createPluginServiceRegistry(initial) {
1141
1517
  assertServiceKey(key);
1142
1518
  if (values.has(key) && !options.replace) {
1143
1519
  throw new Error(
1144
- `BCP Plugins: service ${formatServiceKey(key)} is already registered.`
1520
+ `BCP Plugins: service ${formatServiceKey2(key)} is already registered.`
1145
1521
  );
1146
1522
  }
1147
1523
  values.set(key, value);
@@ -1150,7 +1526,7 @@ function createPluginServiceRegistry(initial) {
1150
1526
  assertServiceKey(key);
1151
1527
  if (!values.has(key)) {
1152
1528
  throw new Error(
1153
- `BCP Plugins: service ${formatServiceKey(key)} is not registered.`
1529
+ `BCP Plugins: service ${formatServiceKey2(key)} is not registered.`
1154
1530
  );
1155
1531
  }
1156
1532
  return values.get(key);
@@ -1179,7 +1555,7 @@ function createPluginHookBus() {
1179
1555
  const hooks = /* @__PURE__ */ new Map();
1180
1556
  const bus = {
1181
1557
  on(name, handler) {
1182
- const normalized = normalizeName3(
1558
+ const normalized = normalizeName4(
1183
1559
  name,
1184
1560
  "hook name"
1185
1561
  );
@@ -1209,7 +1585,7 @@ function createPluginHookBus() {
1209
1585
  };
1210
1586
  },
1211
1587
  async emit(name, payload) {
1212
- const normalized = normalizeName3(
1588
+ const normalized = normalizeName4(
1213
1589
  name,
1214
1590
  "hook name"
1215
1591
  );
@@ -1225,7 +1601,7 @@ function createPluginHookBus() {
1225
1601
  },
1226
1602
  listenerCount(name) {
1227
1603
  return hooks.get(
1228
- normalizeName3(
1604
+ normalizeName4(
1229
1605
  name,
1230
1606
  "hook name"
1231
1607
  )
@@ -1237,7 +1613,7 @@ function createPluginHookBus() {
1237
1613
  return;
1238
1614
  }
1239
1615
  hooks.delete(
1240
- normalizeName3(
1616
+ normalizeName4(
1241
1617
  name,
1242
1618
  "hook name"
1243
1619
  )
@@ -1470,7 +1846,7 @@ function createPluginHost(options = {}) {
1470
1846
  },
1471
1847
  plugin(name) {
1472
1848
  const entry = entries.get(
1473
- normalizeName3(
1849
+ normalizeName4(
1474
1850
  name,
1475
1851
  "plugin name"
1476
1852
  )
@@ -1504,7 +1880,7 @@ function createPluginHost(options = {}) {
1504
1880
  validatePluginDefinition(
1505
1881
  definition
1506
1882
  );
1507
- const name = normalizeName3(
1883
+ const name = normalizeName4(
1508
1884
  definition.name,
1509
1885
  "plugin name"
1510
1886
  );
@@ -1721,7 +2097,7 @@ function validatePluginDefinition(definition) {
1721
2097
  "BCP Plugins: plugin definition must be an object."
1722
2098
  );
1723
2099
  }
1724
- const name = normalizeName3(
2100
+ const name = normalizeName4(
1725
2101
  definition.name,
1726
2102
  "plugin name"
1727
2103
  );
@@ -1764,7 +2140,7 @@ function normalizeDependencyList(value, plugin, field) {
1764
2140
  );
1765
2141
  }
1766
2142
  const normalized = value.map(
1767
- (item) => normalizeName3(
2143
+ (item) => normalizeName4(
1768
2144
  item,
1769
2145
  `${field} dependency`
1770
2146
  )
@@ -1805,7 +2181,7 @@ function requireContext(entry) {
1805
2181
  }
1806
2182
  function markFailed(entry, error) {
1807
2183
  entry.record.state = "failed";
1808
- entry.record.error = formatError(error);
2184
+ entry.record.error = formatError2(error);
1809
2185
  }
1810
2186
  function cloneRecord(record) {
1811
2187
  return {
@@ -1825,7 +2201,7 @@ function isPluginModule(value) {
1825
2201
  )
1826
2202
  );
1827
2203
  }
1828
- function normalizeName3(value, field) {
2204
+ function normalizeName4(value, field) {
1829
2205
  const text = String(value ?? "").trim();
1830
2206
  if (!text) {
1831
2207
  throw new TypeError(
@@ -1843,14 +2219,14 @@ function normalizeOptionalVersion(value) {
1843
2219
  if (value === void 0) {
1844
2220
  return void 0;
1845
2221
  }
1846
- return normalizeName3(
2222
+ return normalizeName4(
1847
2223
  value,
1848
2224
  "plugin version"
1849
2225
  );
1850
2226
  }
1851
2227
  function assertServiceKey(key) {
1852
2228
  if (typeof key === "string") {
1853
- normalizeName3(
2229
+ normalizeName4(
1854
2230
  key,
1855
2231
  "service key"
1856
2232
  );
@@ -1862,10 +2238,10 @@ function assertServiceKey(key) {
1862
2238
  );
1863
2239
  }
1864
2240
  }
1865
- function formatServiceKey(key) {
2241
+ function formatServiceKey2(key) {
1866
2242
  return typeof key === "symbol" ? String(key) : `"${key}"`;
1867
2243
  }
1868
- function formatError(error) {
2244
+ function formatError2(error) {
1869
2245
  if (error instanceof Error) {
1870
2246
  return error.message || error.name;
1871
2247
  }
@@ -1889,7 +2265,7 @@ var ApplicationLifecycleError = class extends Error {
1889
2265
  cause;
1890
2266
  constructor(phase, cause) {
1891
2267
  super(
1892
- `BCP Application: ${phase} failed: ${formatError2(cause)}`
2268
+ `BCP Application: ${phase} failed: ${formatError3(cause)}`
1893
2269
  );
1894
2270
  this.name = "ApplicationLifecycleError";
1895
2271
  this.phase = phase;
@@ -1902,7 +2278,7 @@ function defineApp(definition) {
1902
2278
  }
1903
2279
  function createApp(definition) {
1904
2280
  defineApp(definition);
1905
- const name = normalizeName4(
2281
+ const name = normalizeName5(
1906
2282
  definition.name,
1907
2283
  "application name"
1908
2284
  );
@@ -1913,15 +2289,33 @@ function createApp(definition) {
1913
2289
  definition.schema,
1914
2290
  definition.config
1915
2291
  );
2292
+ const moduleInputs = definition.modules ?? [];
2293
+ const modules = composeModules(
2294
+ moduleInputs.filter(
2295
+ isModuleDefinition
2296
+ )
2297
+ );
2298
+ const legacyModules = moduleInputs.filter(
2299
+ (module) => !isModuleDefinition(module)
2300
+ );
1916
2301
  const container = createServiceContainer({
1917
2302
  name: `${name}:container`,
1918
- providers: definition.providers
2303
+ providers: [
2304
+ ...modules.providers,
2305
+ ...definition.providers ?? []
2306
+ ]
1919
2307
  });
1920
2308
  const plugins = createPluginHost({
1921
- plugins: definition.plugins,
1922
- modules: definition.modules,
2309
+ plugins: [
2310
+ ...modules.plugins,
2311
+ ...definition.plugins ?? []
2312
+ ],
2313
+ modules: legacyModules,
1923
2314
  configs: definition.pluginConfigs,
1924
- services: definition.services
2315
+ services: [
2316
+ ...modules.services,
2317
+ ...definition.services ?? []
2318
+ ]
1925
2319
  });
1926
2320
  const deployment = createDeploymentRuntime({
1927
2321
  ...definition.deployment ?? {},
@@ -1944,6 +2338,7 @@ function createApp(definition) {
1944
2338
  } : {},
1945
2339
  config,
1946
2340
  container,
2341
+ modules,
1947
2342
  services: plugins.services,
1948
2343
  hooks: plugins.hooks,
1949
2344
  plugins,
@@ -1999,6 +2394,13 @@ function createApp(definition) {
1999
2394
  };
2000
2395
  }
2001
2396
  });
2397
+ for (const resource of modules.createLifecycleResources({
2398
+ container,
2399
+ services: plugins.services,
2400
+ hooks: plugins.hooks
2401
+ })) {
2402
+ deployment.addResource(resource);
2403
+ }
2002
2404
  for (const resource of definition.resources ?? []) {
2003
2405
  deployment.addResource(resource);
2004
2406
  }
@@ -2010,6 +2412,7 @@ function createApp(definition) {
2010
2412
  config,
2011
2413
  context,
2012
2414
  container,
2415
+ modules,
2013
2416
  services: plugins.services,
2014
2417
  hooks: plugins.hooks,
2015
2418
  plugins,
@@ -2272,11 +2675,12 @@ function createApp(definition) {
2272
2675
  } : {},
2273
2676
  state,
2274
2677
  services: plugins.services.keys().map(
2275
- formatServiceKey2
2678
+ formatServiceKey3
2276
2679
  ),
2277
2680
  containerProviders: container.graph().map(
2278
2681
  (node) => node.description
2279
2682
  ),
2683
+ modules: modules.records(),
2280
2684
  pluginCount: plugins.plugins().length
2281
2685
  };
2282
2686
  }
@@ -2358,7 +2762,7 @@ function validateDefinition(definition) {
2358
2762
  "BCP Application: definition must be an object."
2359
2763
  );
2360
2764
  }
2361
- normalizeName4(
2765
+ normalizeName5(
2362
2766
  definition.name,
2363
2767
  "application name"
2364
2768
  );
@@ -2398,7 +2802,7 @@ function validateDefinition(definition) {
2398
2802
  }
2399
2803
  }
2400
2804
  }
2401
- function normalizeName4(value, label) {
2805
+ function normalizeName5(value, label) {
2402
2806
  if (typeof value !== "string") {
2403
2807
  throw new TypeError(
2404
2808
  `BCP Application: ${label} must be a string.`
@@ -2423,10 +2827,10 @@ function normalizeOptionalText2(value) {
2423
2827
  }
2424
2828
  return value.trim() || void 0;
2425
2829
  }
2426
- function formatServiceKey2(key) {
2830
+ function formatServiceKey3(key) {
2427
2831
  return typeof key === "symbol" ? key.description ? `Symbol(${key.description})` : key.toString() : key;
2428
2832
  }
2429
- function formatError2(value) {
2833
+ function formatError3(value) {
2430
2834
  if (value instanceof Error) {
2431
2835
  return value.message;
2432
2836
  }