@stardeck-customer-apps/compose 0.13.0 → 0.14.0

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/SKILL.md CHANGED
@@ -12,7 +12,9 @@ Compose turns that declaration into the code Next.js actually builds:
12
12
  - `apps/web/src/modules.gen.ts` — the Module registry, `isModuleEnabled` and `isMountHeadless`
13
13
  - `apps/web/src/module-contributions.gen.ts`
14
14
  - `apps/web/src/module-i18n.gen.ts`
15
- - `apps/web/src/module-init.server.gen.ts`
15
+ - `apps/web/src/module-init.server.gen.ts` — memoized `initializeActiveEnhancements()`:
16
+ each enabled Module's root `initializeModule`, then each active slice's
17
+ `initializeEnhancement`
16
18
  - `apps/web/src/module-datastores.gen.ts`
17
19
  - `apps/web/src/module-server.gen.ts` — `MODULE_SERVER_REGISTRATIONS`, one entry
18
20
  per composed Module that ships `server/registrations.ts`
@@ -149,6 +151,38 @@ a silent fallback. It holds deployment facts: gitignore it (`apps/web/.gitignore
149
151
  the bundle. Compose does not check the export names or shapes. The file
150
152
  counts as `server/**/*.ts`, so the Module also needs `server/index.ts`.
151
153
  Needs compose 0.8.0 or later.
154
+ - **Module initializers.** Server registrations a Module makes into shared
155
+ registries (observers, resolvers, facets, drains) go in a root
156
+ `initialize.server.ts`, any Module kind:
157
+
158
+ ```ts
159
+ // src/modules/guest-ordering/initialize.server.ts — server-only, idempotent
160
+ import { registerPosCloseObserver } from "@/modules/pos/server";
161
+
162
+ import { wakeReconcileForClosedOrder } from "./server/reconcile";
163
+
164
+ export async function initializeModule() {
165
+ registerPosCloseObserver(wakeReconcileForClosedOrder);
166
+ }
167
+ ```
168
+
169
+ `initializeModule` must be a direct named function declaration callable with
170
+ zero required arguments (no arrow function, default export, re-export or
171
+ generator); compose fails otherwise. Compose imports it into
172
+ `module-init.server.gen.ts`, guarded by `isModuleEnabled("<module>")`, and
173
+ runs every Module initializer before any enhancement initializer, once per
174
+ instance. Every generated page stub (its render and its `generateMetadata`)
175
+ and every generated API route stub awaits `initializeActiveEnhancements()`
176
+ before its enablement guards, so the registrations exist in the route's own
177
+ module graph. Page stubs await it from compose 0.14.0; before that only API
178
+ stubs did. An `initialize()` export
179
+ run only from `instrumentation.ts` does not give you that: Next.js loads
180
+ instrumentation in a separate module graph, so on a cold instance the route
181
+ handlers and server components never see what it registered. A Module left
182
+ out by `--enabled` is not imported. Needs compose 0.14.0 or later (and
183
+ `@stardeck-customer-apps/eslint-plugin` 1.8.0 or later, whose
184
+ `no-module-internal-import` lets the generated file import it).
185
+
152
186
  - `@stardeck-customer-apps/compose/runtime` exports `isModuleEnabledByList`,
153
187
  the single enablement rule, and imports nothing — it is safe in client
154
188
  components.
package/dist/cli.js CHANGED
@@ -3560,17 +3560,20 @@ var import_typescript3 = __toESM(require("typescript"), 1);
3560
3560
  var CONVENTIONAL_ENTRY_EXPORT_NAME = {
3561
3561
  contributions: "contributions",
3562
3562
  slotCatalogs: "slotCatalogs",
3563
- initializeEnhancement: "initializeEnhancement"
3563
+ initializeEnhancement: "initializeEnhancement",
3564
+ initializeModule: "initializeModule"
3564
3565
  };
3565
3566
  var CONVENTIONAL_ENTRY_DECLARATION_KIND = {
3566
3567
  contributions: "variable",
3567
3568
  slotCatalogs: "variable",
3568
- initializeEnhancement: "function"
3569
+ initializeEnhancement: "function",
3570
+ initializeModule: "function"
3569
3571
  };
3570
3572
  var CONVENTIONAL_ENTRY_FILENAME = {
3571
3573
  contributions: "contributions.ts",
3572
3574
  slotCatalogs: "slots.ts",
3573
- initializeEnhancement: "initialize.server.ts"
3575
+ initializeEnhancement: "initialize.server.ts",
3576
+ initializeModule: "initialize.server.ts"
3574
3577
  };
3575
3578
  function hasModifierKind(modifiers, kind) {
3576
3579
  return Boolean(modifiers?.some((m) => m.kind === kind));
@@ -3669,7 +3672,7 @@ function analyzeConventionalEntryExportWithTs(typescript, sourceText, kind, file
3669
3672
  }
3670
3673
  if (!isZeroArgumentInvocable(typescript, stmt.parameters)) {
3671
3674
  errors.push(
3672
- `"${entryName}" must be invocable with zero arguments (generated caller uses initializeEnhancement()); required parameters are forbidden \u2014 use optional, default, rest, or a this-parameter only`
3675
+ `"${entryName}" must be invocable with zero arguments (generated caller uses ${entryName}()); required parameters are forbidden \u2014 use optional, default, rest, or a this-parameter only`
3673
3676
  );
3674
3677
  continue;
3675
3678
  }
@@ -4150,6 +4153,9 @@ function assertSlotCatalogsExport(source, pathLabel) {
4150
4153
  function assertInitializeEnhancementExport(source, pathLabel) {
4151
4154
  assertConventionalEntryExport(source, pathLabel, "initializeEnhancement");
4152
4155
  }
4156
+ function assertInitializeModuleExport(source, pathLabel) {
4157
+ assertConventionalEntryExport(source, pathLabel, "initializeModule");
4158
+ }
4153
4159
  function assertClientSafeConventionalEntryImports(source, pathLabel, kind) {
4154
4160
  const { analysisErrors } = analyzeClientSafeEntryImports(
4155
4161
  source,
@@ -4669,30 +4675,47 @@ function deriveModuleLocaleLayers(registry, facts) {
4669
4675
  }
4670
4676
  return layers;
4671
4677
  }
4672
- function emitModuleInitServerGenTs(input) {
4678
+ function moduleInitializerCalls(registry, facts) {
4679
+ const installed = new Set(registry.modules.map((m) => m.name));
4680
+ return facts.filter((f) => f.hasRootInitializeServer && installed.has(f.moduleName)).map((f) => f.moduleName).sort(compareNames).map((name, index) => {
4681
+ if (!isValidModuleName(name)) {
4682
+ throw new Error(`[ModuleRail] composition artifacts: invalid module name "${name}"`);
4683
+ }
4684
+ return {
4685
+ exportName: "initializeModule",
4686
+ guard: [name],
4687
+ ident: `initializeModule${index}`,
4688
+ importPath: `@/modules/${name}/initialize.server`
4689
+ };
4690
+ });
4691
+ }
4692
+ function enhancementInitializerCalls(registry, facts) {
4673
4693
  const calls = [];
4674
- const installed = new Set(input.registry.modules.map((m) => m.name));
4675
- let initializerIndex = 0;
4676
- for (const facts of [...input.facts].sort((a, b) => compareNames(a.moduleName, b.moduleName))) {
4677
- if (!installed.has(facts.moduleName)) continue;
4678
- const peers = activePeers(input.registry, facts.moduleName);
4679
- for (const enh of [...facts.enhancements].sort((a, b) => compareNames(a.peer, b.peer))) {
4694
+ const installed = new Set(registry.modules.map((m) => m.name));
4695
+ for (const f of [...facts].sort((a, b) => compareNames(a.moduleName, b.moduleName))) {
4696
+ if (!installed.has(f.moduleName)) continue;
4697
+ const peers = activePeers(registry, f.moduleName);
4698
+ for (const enh of [...f.enhancements].sort((a, b) => compareNames(a.peer, b.peer))) {
4680
4699
  if (!enh.active || !peers.has(enh.peer)) continue;
4681
4700
  if (!enh.hasInitializeServer) continue;
4682
- const ident = `initializeEnhancement${initializerIndex++}`;
4683
4701
  calls.push({
4684
- owner: facts.moduleName,
4685
- peer: enh.peer,
4686
- ident,
4687
- importPath: `@/modules/${facts.moduleName}/enhancements/${enh.peer}/initialize.server`
4702
+ exportName: "initializeEnhancement",
4703
+ guard: [f.moduleName, enh.peer],
4704
+ ident: `initializeEnhancement${calls.length}`,
4705
+ importPath: `@/modules/${f.moduleName}/enhancements/${enh.peer}/initialize.server`
4688
4706
  });
4689
4707
  }
4690
4708
  }
4691
- const importLines = calls.map(
4692
- (c) => `import { initializeEnhancement as ${c.ident} } from ${JSON.stringify(c.importPath)};`
4693
- );
4709
+ return calls;
4710
+ }
4711
+ function emitModuleInitServerGenTs(input) {
4712
+ const moduleCalls = moduleInitializerCalls(input.registry, input.facts);
4713
+ const enhancementCalls = enhancementInitializerCalls(input.registry, input.facts);
4714
+ const calls = [...moduleCalls, ...enhancementCalls];
4715
+ const importFor = (c) => `import { ${c.exportName} as ${c.ident} } from ${JSON.stringify(c.importPath)};`;
4716
+ const importLines = moduleCalls.length === 0 ? calls.map(importFor) : [...calls].sort((a, b) => compareImportSpecifiers(a.importPath, b.importPath)).map(importFor);
4694
4717
  const callLines = calls.length === 0 ? [" // Empty composition \u2014 no active enhancement initializers."] : calls.map(
4695
- (c) => ` if (enabled(${JSON.stringify(c.owner)}) && enabled(${JSON.stringify(c.peer)})) await ${c.ident}();`
4718
+ (c) => ` if (${c.guard.map((name) => `enabled(${JSON.stringify(name)})`).join(" && ")}) await ${c.ident}();`
4696
4719
  );
4697
4720
  return [
4698
4721
  formatInitMarker(),
@@ -5142,6 +5165,7 @@ function buildRouteStubContent(input) {
5142
5165
  'import { notFound } from "next/navigation";',
5143
5166
  // simple-import-sort puts packages and "@/" aliases in separate groups.
5144
5167
  "",
5168
+ 'import { initializeActiveEnhancements } from "@/module-init.server.gen";',
5145
5169
  'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
5146
5170
  `import Page from ${emitTsString(input.importModule)};`
5147
5171
  ];
@@ -5150,6 +5174,7 @@ function buildRouteStubContent(input) {
5150
5174
  `import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
5151
5175
  "",
5152
5176
  "export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
5177
+ " await initializeActiveEnhancements();",
5153
5178
  ...emitGuardLines(guards, "return {};"),
5154
5179
  " return sourceGenerateMetadata(...args);",
5155
5180
  "}"
@@ -5160,7 +5185,10 @@ function buildRouteStubContent(input) {
5160
5185
  "type PageProps =",
5161
5186
  " Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
5162
5187
  "",
5163
- "export default function ModulePage(props: PageProps) {",
5188
+ // Module and enhancement initializers run in this route's own module graph
5189
+ // (memoized), so a cold server render sees their registrations.
5190
+ "export default async function ModulePage(props: PageProps) {",
5191
+ " await initializeActiveEnhancements();",
5164
5192
  ...emitGuardLines(guards, "notFound();"),
5165
5193
  " return <Page {...props} />;",
5166
5194
  "}",
@@ -6958,6 +6986,9 @@ async function readConventionalEntryIfRealFile(path5, kind, presences, files) {
6958
6986
  case "initializeEnhancement":
6959
6987
  assertInitializeEnhancementExport(source, path5);
6960
6988
  break;
6989
+ case "initializeModule":
6990
+ assertInitializeModuleExport(source, path5);
6991
+ break;
6961
6992
  }
6962
6993
  return source;
6963
6994
  }
@@ -6994,7 +7025,11 @@ async function collectCompositionEntryFacts(context, registry) {
6994
7025
  for (const mod of registry.modules) {
6995
7026
  const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
6996
7027
  localeDirs.push(`${modulePath}/i18n`);
6997
- conventionalPaths.push(`${modulePath}/contributions.ts`, `${modulePath}/slots.ts`);
7028
+ conventionalPaths.push(
7029
+ `${modulePath}/contributions.ts`,
7030
+ `${modulePath}/slots.ts`,
7031
+ `${modulePath}/initialize.server.ts`
7032
+ );
6998
7033
  for (const enh of mod.enhancements) {
6999
7034
  const peerDir = `${modulePath}/enhancements/${enh.peer}`;
7000
7035
  localeDirs.push(`${peerDir}/i18n`);
@@ -7082,6 +7117,17 @@ async function collectCompositionEntryFacts(context, registry) {
7082
7117
  }
7083
7118
  }
7084
7119
  const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
7120
+ const hasRootInitializeServer = await readConventionalEntryIfRealFile(
7121
+ `${modulePath}/initialize.server.ts`,
7122
+ "initializeModule",
7123
+ conventionalPresences,
7124
+ files
7125
+ ) != null;
7126
+ if (hasRootInitializeServer) {
7127
+ console.log(
7128
+ `[ModuleRail] runId=${context.runId} module=${mod.name} has root initialize.server.ts (initializeModule)`
7129
+ );
7130
+ }
7085
7131
  const rootLocales = localesByDir.get(`${modulePath}/i18n`);
7086
7132
  if (rootLocales.length > 0 && !rootLocales.includes("en")) {
7087
7133
  throw new Error(
@@ -7122,6 +7168,7 @@ async function collectCompositionEntryFacts(context, registry) {
7122
7168
  hasRootContributions,
7123
7169
  hasRootSlots,
7124
7170
  hasServerRegistrations,
7171
+ hasRootInitializeServer,
7125
7172
  rootLocales,
7126
7173
  enhancements
7127
7174
  });
package/dist/index.js CHANGED
@@ -3689,17 +3689,20 @@ var import_typescript3 = __toESM(require("typescript"), 1);
3689
3689
  var CONVENTIONAL_ENTRY_EXPORT_NAME = {
3690
3690
  contributions: "contributions",
3691
3691
  slotCatalogs: "slotCatalogs",
3692
- initializeEnhancement: "initializeEnhancement"
3692
+ initializeEnhancement: "initializeEnhancement",
3693
+ initializeModule: "initializeModule"
3693
3694
  };
3694
3695
  var CONVENTIONAL_ENTRY_DECLARATION_KIND = {
3695
3696
  contributions: "variable",
3696
3697
  slotCatalogs: "variable",
3697
- initializeEnhancement: "function"
3698
+ initializeEnhancement: "function",
3699
+ initializeModule: "function"
3698
3700
  };
3699
3701
  var CONVENTIONAL_ENTRY_FILENAME = {
3700
3702
  contributions: "contributions.ts",
3701
3703
  slotCatalogs: "slots.ts",
3702
- initializeEnhancement: "initialize.server.ts"
3704
+ initializeEnhancement: "initialize.server.ts",
3705
+ initializeModule: "initialize.server.ts"
3703
3706
  };
3704
3707
  function hasModifierKind(modifiers, kind) {
3705
3708
  return Boolean(modifiers?.some((m) => m.kind === kind));
@@ -3798,7 +3801,7 @@ function analyzeConventionalEntryExportWithTs(typescript, sourceText, kind, file
3798
3801
  }
3799
3802
  if (!isZeroArgumentInvocable(typescript, stmt.parameters)) {
3800
3803
  errors.push(
3801
- `"${entryName}" must be invocable with zero arguments (generated caller uses initializeEnhancement()); required parameters are forbidden \u2014 use optional, default, rest, or a this-parameter only`
3804
+ `"${entryName}" must be invocable with zero arguments (generated caller uses ${entryName}()); required parameters are forbidden \u2014 use optional, default, rest, or a this-parameter only`
3802
3805
  );
3803
3806
  continue;
3804
3807
  }
@@ -4160,6 +4163,9 @@ function assertSlotCatalogsExport(source, pathLabel) {
4160
4163
  function assertInitializeEnhancementExport(source, pathLabel) {
4161
4164
  assertConventionalEntryExport(source, pathLabel, "initializeEnhancement");
4162
4165
  }
4166
+ function assertInitializeModuleExport(source, pathLabel) {
4167
+ assertConventionalEntryExport(source, pathLabel, "initializeModule");
4168
+ }
4163
4169
  function assertClientSafeConventionalEntryImports(source, pathLabel, kind) {
4164
4170
  const { analysisErrors } = analyzeClientSafeEntryImports(
4165
4171
  source,
@@ -4679,30 +4685,47 @@ function deriveModuleLocaleLayers(registry, facts) {
4679
4685
  }
4680
4686
  return layers;
4681
4687
  }
4682
- function emitModuleInitServerGenTs(input) {
4688
+ function moduleInitializerCalls(registry, facts) {
4689
+ const installed = new Set(registry.modules.map((m) => m.name));
4690
+ return facts.filter((f) => f.hasRootInitializeServer && installed.has(f.moduleName)).map((f) => f.moduleName).sort(compareNames).map((name, index) => {
4691
+ if (!isValidModuleName(name)) {
4692
+ throw new Error(`[ModuleRail] composition artifacts: invalid module name "${name}"`);
4693
+ }
4694
+ return {
4695
+ exportName: "initializeModule",
4696
+ guard: [name],
4697
+ ident: `initializeModule${index}`,
4698
+ importPath: `@/modules/${name}/initialize.server`
4699
+ };
4700
+ });
4701
+ }
4702
+ function enhancementInitializerCalls(registry, facts) {
4683
4703
  const calls = [];
4684
- const installed = new Set(input.registry.modules.map((m) => m.name));
4685
- let initializerIndex = 0;
4686
- for (const facts of [...input.facts].sort((a, b) => compareNames(a.moduleName, b.moduleName))) {
4687
- if (!installed.has(facts.moduleName)) continue;
4688
- const peers = activePeers(input.registry, facts.moduleName);
4689
- for (const enh of [...facts.enhancements].sort((a, b) => compareNames(a.peer, b.peer))) {
4704
+ const installed = new Set(registry.modules.map((m) => m.name));
4705
+ for (const f of [...facts].sort((a, b) => compareNames(a.moduleName, b.moduleName))) {
4706
+ if (!installed.has(f.moduleName)) continue;
4707
+ const peers = activePeers(registry, f.moduleName);
4708
+ for (const enh of [...f.enhancements].sort((a, b) => compareNames(a.peer, b.peer))) {
4690
4709
  if (!enh.active || !peers.has(enh.peer)) continue;
4691
4710
  if (!enh.hasInitializeServer) continue;
4692
- const ident = `initializeEnhancement${initializerIndex++}`;
4693
4711
  calls.push({
4694
- owner: facts.moduleName,
4695
- peer: enh.peer,
4696
- ident,
4697
- importPath: `@/modules/${facts.moduleName}/enhancements/${enh.peer}/initialize.server`
4712
+ exportName: "initializeEnhancement",
4713
+ guard: [f.moduleName, enh.peer],
4714
+ ident: `initializeEnhancement${calls.length}`,
4715
+ importPath: `@/modules/${f.moduleName}/enhancements/${enh.peer}/initialize.server`
4698
4716
  });
4699
4717
  }
4700
4718
  }
4701
- const importLines = calls.map(
4702
- (c) => `import { initializeEnhancement as ${c.ident} } from ${JSON.stringify(c.importPath)};`
4703
- );
4719
+ return calls;
4720
+ }
4721
+ function emitModuleInitServerGenTs(input) {
4722
+ const moduleCalls = moduleInitializerCalls(input.registry, input.facts);
4723
+ const enhancementCalls = enhancementInitializerCalls(input.registry, input.facts);
4724
+ const calls = [...moduleCalls, ...enhancementCalls];
4725
+ const importFor = (c) => `import { ${c.exportName} as ${c.ident} } from ${JSON.stringify(c.importPath)};`;
4726
+ const importLines = moduleCalls.length === 0 ? calls.map(importFor) : [...calls].sort((a, b) => compareImportSpecifiers(a.importPath, b.importPath)).map(importFor);
4704
4727
  const callLines = calls.length === 0 ? [" // Empty composition \u2014 no active enhancement initializers."] : calls.map(
4705
- (c) => ` if (enabled(${JSON.stringify(c.owner)}) && enabled(${JSON.stringify(c.peer)})) await ${c.ident}();`
4728
+ (c) => ` if (${c.guard.map((name) => `enabled(${JSON.stringify(name)})`).join(" && ")}) await ${c.ident}();`
4706
4729
  );
4707
4730
  return [
4708
4731
  formatInitMarker(),
@@ -5152,6 +5175,7 @@ function buildRouteStubContent(input) {
5152
5175
  'import { notFound } from "next/navigation";',
5153
5176
  // simple-import-sort puts packages and "@/" aliases in separate groups.
5154
5177
  "",
5178
+ 'import { initializeActiveEnhancements } from "@/module-init.server.gen";',
5155
5179
  'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
5156
5180
  `import Page from ${emitTsString(input.importModule)};`
5157
5181
  ];
@@ -5160,6 +5184,7 @@ function buildRouteStubContent(input) {
5160
5184
  `import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
5161
5185
  "",
5162
5186
  "export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
5187
+ " await initializeActiveEnhancements();",
5163
5188
  ...emitGuardLines(guards, "return {};"),
5164
5189
  " return sourceGenerateMetadata(...args);",
5165
5190
  "}"
@@ -5170,7 +5195,10 @@ function buildRouteStubContent(input) {
5170
5195
  "type PageProps =",
5171
5196
  " Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
5172
5197
  "",
5173
- "export default function ModulePage(props: PageProps) {",
5198
+ // Module and enhancement initializers run in this route's own module graph
5199
+ // (memoized), so a cold server render sees their registrations.
5200
+ "export default async function ModulePage(props: PageProps) {",
5201
+ " await initializeActiveEnhancements();",
5174
5202
  ...emitGuardLines(guards, "notFound();"),
5175
5203
  " return <Page {...props} />;",
5176
5204
  "}",
@@ -6968,6 +6996,9 @@ async function readConventionalEntryIfRealFile(path4, kind, presences, files) {
6968
6996
  case "initializeEnhancement":
6969
6997
  assertInitializeEnhancementExport(source, path4);
6970
6998
  break;
6999
+ case "initializeModule":
7000
+ assertInitializeModuleExport(source, path4);
7001
+ break;
6971
7002
  }
6972
7003
  return source;
6973
7004
  }
@@ -7004,7 +7035,11 @@ async function collectCompositionEntryFacts(context, registry) {
7004
7035
  for (const mod of registry.modules) {
7005
7036
  const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
7006
7037
  localeDirs.push(`${modulePath}/i18n`);
7007
- conventionalPaths.push(`${modulePath}/contributions.ts`, `${modulePath}/slots.ts`);
7038
+ conventionalPaths.push(
7039
+ `${modulePath}/contributions.ts`,
7040
+ `${modulePath}/slots.ts`,
7041
+ `${modulePath}/initialize.server.ts`
7042
+ );
7008
7043
  for (const enh of mod.enhancements) {
7009
7044
  const peerDir = `${modulePath}/enhancements/${enh.peer}`;
7010
7045
  localeDirs.push(`${peerDir}/i18n`);
@@ -7092,6 +7127,17 @@ async function collectCompositionEntryFacts(context, registry) {
7092
7127
  }
7093
7128
  }
7094
7129
  const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
7130
+ const hasRootInitializeServer = await readConventionalEntryIfRealFile(
7131
+ `${modulePath}/initialize.server.ts`,
7132
+ "initializeModule",
7133
+ conventionalPresences,
7134
+ files
7135
+ ) != null;
7136
+ if (hasRootInitializeServer) {
7137
+ console.log(
7138
+ `[ModuleRail] runId=${context.runId} module=${mod.name} has root initialize.server.ts (initializeModule)`
7139
+ );
7140
+ }
7095
7141
  const rootLocales = localesByDir.get(`${modulePath}/i18n`);
7096
7142
  if (rootLocales.length > 0 && !rootLocales.includes("en")) {
7097
7143
  throw new Error(
@@ -7132,6 +7178,7 @@ async function collectCompositionEntryFacts(context, registry) {
7132
7178
  hasRootContributions,
7133
7179
  hasRootSlots,
7134
7180
  hasServerRegistrations,
7181
+ hasRootInitializeServer,
7135
7182
  rootLocales,
7136
7183
  enhancements
7137
7184
  });
package/dist/index.mjs CHANGED
@@ -3670,17 +3670,20 @@ import ts3 from "typescript";
3670
3670
  var CONVENTIONAL_ENTRY_EXPORT_NAME = {
3671
3671
  contributions: "contributions",
3672
3672
  slotCatalogs: "slotCatalogs",
3673
- initializeEnhancement: "initializeEnhancement"
3673
+ initializeEnhancement: "initializeEnhancement",
3674
+ initializeModule: "initializeModule"
3674
3675
  };
3675
3676
  var CONVENTIONAL_ENTRY_DECLARATION_KIND = {
3676
3677
  contributions: "variable",
3677
3678
  slotCatalogs: "variable",
3678
- initializeEnhancement: "function"
3679
+ initializeEnhancement: "function",
3680
+ initializeModule: "function"
3679
3681
  };
3680
3682
  var CONVENTIONAL_ENTRY_FILENAME = {
3681
3683
  contributions: "contributions.ts",
3682
3684
  slotCatalogs: "slots.ts",
3683
- initializeEnhancement: "initialize.server.ts"
3685
+ initializeEnhancement: "initialize.server.ts",
3686
+ initializeModule: "initialize.server.ts"
3684
3687
  };
3685
3688
  function hasModifierKind(modifiers, kind) {
3686
3689
  return Boolean(modifiers?.some((m) => m.kind === kind));
@@ -3779,7 +3782,7 @@ function analyzeConventionalEntryExportWithTs(typescript, sourceText, kind, file
3779
3782
  }
3780
3783
  if (!isZeroArgumentInvocable(typescript, stmt.parameters)) {
3781
3784
  errors.push(
3782
- `"${entryName}" must be invocable with zero arguments (generated caller uses initializeEnhancement()); required parameters are forbidden \u2014 use optional, default, rest, or a this-parameter only`
3785
+ `"${entryName}" must be invocable with zero arguments (generated caller uses ${entryName}()); required parameters are forbidden \u2014 use optional, default, rest, or a this-parameter only`
3783
3786
  );
3784
3787
  continue;
3785
3788
  }
@@ -4141,6 +4144,9 @@ function assertSlotCatalogsExport(source, pathLabel) {
4141
4144
  function assertInitializeEnhancementExport(source, pathLabel) {
4142
4145
  assertConventionalEntryExport(source, pathLabel, "initializeEnhancement");
4143
4146
  }
4147
+ function assertInitializeModuleExport(source, pathLabel) {
4148
+ assertConventionalEntryExport(source, pathLabel, "initializeModule");
4149
+ }
4144
4150
  function assertClientSafeConventionalEntryImports(source, pathLabel, kind) {
4145
4151
  const { analysisErrors } = analyzeClientSafeEntryImports(
4146
4152
  source,
@@ -4660,30 +4666,47 @@ function deriveModuleLocaleLayers(registry, facts) {
4660
4666
  }
4661
4667
  return layers;
4662
4668
  }
4663
- function emitModuleInitServerGenTs(input) {
4669
+ function moduleInitializerCalls(registry, facts) {
4670
+ const installed = new Set(registry.modules.map((m) => m.name));
4671
+ return facts.filter((f) => f.hasRootInitializeServer && installed.has(f.moduleName)).map((f) => f.moduleName).sort(compareNames).map((name, index) => {
4672
+ if (!isValidModuleName(name)) {
4673
+ throw new Error(`[ModuleRail] composition artifacts: invalid module name "${name}"`);
4674
+ }
4675
+ return {
4676
+ exportName: "initializeModule",
4677
+ guard: [name],
4678
+ ident: `initializeModule${index}`,
4679
+ importPath: `@/modules/${name}/initialize.server`
4680
+ };
4681
+ });
4682
+ }
4683
+ function enhancementInitializerCalls(registry, facts) {
4664
4684
  const calls = [];
4665
- const installed = new Set(input.registry.modules.map((m) => m.name));
4666
- let initializerIndex = 0;
4667
- for (const facts of [...input.facts].sort((a, b) => compareNames(a.moduleName, b.moduleName))) {
4668
- if (!installed.has(facts.moduleName)) continue;
4669
- const peers = activePeers(input.registry, facts.moduleName);
4670
- for (const enh of [...facts.enhancements].sort((a, b) => compareNames(a.peer, b.peer))) {
4685
+ const installed = new Set(registry.modules.map((m) => m.name));
4686
+ for (const f of [...facts].sort((a, b) => compareNames(a.moduleName, b.moduleName))) {
4687
+ if (!installed.has(f.moduleName)) continue;
4688
+ const peers = activePeers(registry, f.moduleName);
4689
+ for (const enh of [...f.enhancements].sort((a, b) => compareNames(a.peer, b.peer))) {
4671
4690
  if (!enh.active || !peers.has(enh.peer)) continue;
4672
4691
  if (!enh.hasInitializeServer) continue;
4673
- const ident = `initializeEnhancement${initializerIndex++}`;
4674
4692
  calls.push({
4675
- owner: facts.moduleName,
4676
- peer: enh.peer,
4677
- ident,
4678
- importPath: `@/modules/${facts.moduleName}/enhancements/${enh.peer}/initialize.server`
4693
+ exportName: "initializeEnhancement",
4694
+ guard: [f.moduleName, enh.peer],
4695
+ ident: `initializeEnhancement${calls.length}`,
4696
+ importPath: `@/modules/${f.moduleName}/enhancements/${enh.peer}/initialize.server`
4679
4697
  });
4680
4698
  }
4681
4699
  }
4682
- const importLines = calls.map(
4683
- (c) => `import { initializeEnhancement as ${c.ident} } from ${JSON.stringify(c.importPath)};`
4684
- );
4700
+ return calls;
4701
+ }
4702
+ function emitModuleInitServerGenTs(input) {
4703
+ const moduleCalls = moduleInitializerCalls(input.registry, input.facts);
4704
+ const enhancementCalls = enhancementInitializerCalls(input.registry, input.facts);
4705
+ const calls = [...moduleCalls, ...enhancementCalls];
4706
+ const importFor = (c) => `import { ${c.exportName} as ${c.ident} } from ${JSON.stringify(c.importPath)};`;
4707
+ const importLines = moduleCalls.length === 0 ? calls.map(importFor) : [...calls].sort((a, b) => compareImportSpecifiers(a.importPath, b.importPath)).map(importFor);
4685
4708
  const callLines = calls.length === 0 ? [" // Empty composition \u2014 no active enhancement initializers."] : calls.map(
4686
- (c) => ` if (enabled(${JSON.stringify(c.owner)}) && enabled(${JSON.stringify(c.peer)})) await ${c.ident}();`
4709
+ (c) => ` if (${c.guard.map((name) => `enabled(${JSON.stringify(name)})`).join(" && ")}) await ${c.ident}();`
4687
4710
  );
4688
4711
  return [
4689
4712
  formatInitMarker(),
@@ -5133,6 +5156,7 @@ function buildRouteStubContent(input) {
5133
5156
  'import { notFound } from "next/navigation";',
5134
5157
  // simple-import-sort puts packages and "@/" aliases in separate groups.
5135
5158
  "",
5159
+ 'import { initializeActiveEnhancements } from "@/module-init.server.gen";',
5136
5160
  'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
5137
5161
  `import Page from ${emitTsString(input.importModule)};`
5138
5162
  ];
@@ -5141,6 +5165,7 @@ function buildRouteStubContent(input) {
5141
5165
  `import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
5142
5166
  "",
5143
5167
  "export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
5168
+ " await initializeActiveEnhancements();",
5144
5169
  ...emitGuardLines(guards, "return {};"),
5145
5170
  " return sourceGenerateMetadata(...args);",
5146
5171
  "}"
@@ -5151,7 +5176,10 @@ function buildRouteStubContent(input) {
5151
5176
  "type PageProps =",
5152
5177
  " Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
5153
5178
  "",
5154
- "export default function ModulePage(props: PageProps) {",
5179
+ // Module and enhancement initializers run in this route's own module graph
5180
+ // (memoized), so a cold server render sees their registrations.
5181
+ "export default async function ModulePage(props: PageProps) {",
5182
+ " await initializeActiveEnhancements();",
5155
5183
  ...emitGuardLines(guards, "notFound();"),
5156
5184
  " return <Page {...props} />;",
5157
5185
  "}",
@@ -6949,6 +6977,9 @@ async function readConventionalEntryIfRealFile(path4, kind, presences, files) {
6949
6977
  case "initializeEnhancement":
6950
6978
  assertInitializeEnhancementExport(source, path4);
6951
6979
  break;
6980
+ case "initializeModule":
6981
+ assertInitializeModuleExport(source, path4);
6982
+ break;
6952
6983
  }
6953
6984
  return source;
6954
6985
  }
@@ -6985,7 +7016,11 @@ async function collectCompositionEntryFacts(context, registry) {
6985
7016
  for (const mod of registry.modules) {
6986
7017
  const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
6987
7018
  localeDirs.push(`${modulePath}/i18n`);
6988
- conventionalPaths.push(`${modulePath}/contributions.ts`, `${modulePath}/slots.ts`);
7019
+ conventionalPaths.push(
7020
+ `${modulePath}/contributions.ts`,
7021
+ `${modulePath}/slots.ts`,
7022
+ `${modulePath}/initialize.server.ts`
7023
+ );
6989
7024
  for (const enh of mod.enhancements) {
6990
7025
  const peerDir = `${modulePath}/enhancements/${enh.peer}`;
6991
7026
  localeDirs.push(`${peerDir}/i18n`);
@@ -7073,6 +7108,17 @@ async function collectCompositionEntryFacts(context, registry) {
7073
7108
  }
7074
7109
  }
7075
7110
  const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
7111
+ const hasRootInitializeServer = await readConventionalEntryIfRealFile(
7112
+ `${modulePath}/initialize.server.ts`,
7113
+ "initializeModule",
7114
+ conventionalPresences,
7115
+ files
7116
+ ) != null;
7117
+ if (hasRootInitializeServer) {
7118
+ console.log(
7119
+ `[ModuleRail] runId=${context.runId} module=${mod.name} has root initialize.server.ts (initializeModule)`
7120
+ );
7121
+ }
7076
7122
  const rootLocales = localesByDir.get(`${modulePath}/i18n`);
7077
7123
  if (rootLocales.length > 0 && !rootLocales.includes("en")) {
7078
7124
  throw new Error(
@@ -7113,6 +7159,7 @@ async function collectCompositionEntryFacts(context, registry) {
7113
7159
  hasRootContributions,
7114
7160
  hasRootSlots,
7115
7161
  hasServerRegistrations,
7162
+ hasRootInitializeServer,
7116
7163
  rootLocales,
7117
7164
  enhancements
7118
7165
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/compose",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Regenerates a Stardeck app's Module-rail artifacts — the six src/*.gen.ts registries and every route/endpoint stub — from src/modules/*/module.json",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",