@stardeck-customer-apps/compose 0.7.0 → 0.8.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
@@ -14,6 +14,8 @@ Compose turns that declaration into the code Next.js actually builds:
14
14
  - `apps/web/src/module-i18n.gen.ts`
15
15
  - `apps/web/src/module-init.server.gen.ts`
16
16
  - `apps/web/src/module-datastores.gen.ts`
17
+ - `apps/web/src/module-server.gen.ts` — `MODULE_SERVER_REGISTRATIONS`, one entry
18
+ per composed Module that ships `server/registrations.ts`
17
19
  - one `page.tsx` / `route.ts` stub under `apps/web/src/app/**` per declared
18
20
  mount and endpoint
19
21
 
@@ -101,6 +103,19 @@ a silent fallback. It holds deployment facts: gitignore it (`apps/web/.gitignore
101
103
  compose as a route collision.
102
104
  `isMountHeadless(key)` and `isHrefHeadless(href)` are exported from
103
105
  `@/modules.gen`. Needs compose 0.7.0 or later.
106
+ - **Server registrations.** Per-Module server wiring that the app layer runs
107
+ (seeds, schedule handlers, and similar) goes in the Module's
108
+ `server/registrations.ts`, any Module kind. Compose imports each composed
109
+ Module's file as a namespace into `module-server.gen.ts`:
110
+ `MODULE_SERVER_REGISTRATIONS` is a
111
+ `readonly { module: string; registrations: Readonly<Record<string, unknown>> }[]`.
112
+ A Module left out by `--enabled` is not imported at all, so its server code
113
+ drops out of the build. App code reads that array from `@/module-server.gen`
114
+ and narrows the exports it expects (e.g. `registrations.seed`); it must never
115
+ import a Module's seed or schedules directly, or a disabled Module stays in
116
+ the bundle. Compose does not check the export names or shapes. The file
117
+ counts as `server/**/*.ts`, so the Module also needs `server/index.ts`.
118
+ Needs compose 0.8.0 or later.
104
119
  - `@stardeck-customer-apps/compose/runtime` exports `isModuleEnabledByList`,
105
120
  the single enablement rule, and imports nothing — it is safe in client
106
121
  components.
@@ -111,5 +126,5 @@ After each run compose rewrites `apps/web/src/app/.gitignore` with one entry per
111
126
  stub it emits — stubs are scattered one-per-route and cannot be globbed. Commit
112
127
  that file: it is deterministic output, and compose rewrites it only when the set
113
128
  of stubs changes, so an unchanged compose leaves `git status` clean. It does not
114
- list the five `src/*.gen.ts`; a static `src/*.gen.ts` line in `apps/web/.gitignore`
129
+ list the six `src/*.gen.ts`; a static `src/*.gen.ts` line in `apps/web/.gitignore`
115
130
  covers those. Compose refuses to touch the file if line 1 is not its own header.
package/dist/cli.js CHANGED
@@ -3896,6 +3896,7 @@ var MODULE_CONTRIBUTIONS_GEN_PATH = `${APP_WEB_PREFIX}src/module-contributions.g
3896
3896
  var MODULE_DATASTORES_GEN_PATH = `${APP_WEB_PREFIX}src/module-datastores.gen.ts`;
3897
3897
  var MODULE_I18N_GEN_PATH = `${APP_WEB_PREFIX}src/module-i18n.gen.ts`;
3898
3898
  var MODULE_INIT_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-init.server.gen.ts`;
3899
+ var MODULE_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-server.gen.ts`;
3899
3900
  var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
3900
3901
  var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
3901
3902
 
@@ -4141,7 +4142,8 @@ var COMPOSITION_RUNTIME_GEN_PATHS = [
4141
4142
  MODULE_CONTRIBUTIONS_GEN_PATH,
4142
4143
  MODULE_I18N_GEN_PATH,
4143
4144
  MODULE_INIT_SERVER_GEN_PATH,
4144
- MODULE_DATASTORES_GEN_PATH
4145
+ MODULE_DATASTORES_GEN_PATH,
4146
+ MODULE_SERVER_GEN_PATH
4145
4147
  ];
4146
4148
  function compareNames(a, b) {
4147
4149
  return a < b ? -1 : a > b ? 1 : 0;
@@ -4162,6 +4164,9 @@ function formatI18nMarker() {
4162
4164
  function formatInitMarker() {
4163
4165
  return `// ${MODULE_STUB_MARKER} scope=init`;
4164
4166
  }
4167
+ function formatServerMarker() {
4168
+ return `// ${MODULE_STUB_MARKER} scope=server`;
4169
+ }
4165
4170
  function formatDatastoresMarker() {
4166
4171
  return `// ${MODULE_STUB_MARKER} scope=datastores`;
4167
4172
  }
@@ -4805,6 +4810,42 @@ function emitModuleInitServerGenTs(input) {
4805
4810
  ""
4806
4811
  ].join("\n");
4807
4812
  }
4813
+ function emitModuleServerGenTs(input) {
4814
+ const installed = new Set(input.registry.modules.map((m) => m.name));
4815
+ const moduleNames = input.facts.filter((f) => f.hasServerRegistrations && installed.has(f.moduleName)).map((f) => f.moduleName).sort(compareNames);
4816
+ for (const name of moduleNames) {
4817
+ if (!isValidModuleName(name)) {
4818
+ throw new Error(`[ModuleRail] composition artifacts: invalid module name "${name}"`);
4819
+ }
4820
+ }
4821
+ const importLines = moduleNames.map(
4822
+ (name, index) => [
4823
+ `@/modules/${name}/server/registrations`,
4824
+ `import * as serverRegistrations${index} from ${JSON.stringify(`@/modules/${name}/server/registrations`)};`
4825
+ ]
4826
+ ).sort((a, b) => compareImportSpecifiers(a[0], b[0])).map(([, line]) => line);
4827
+ const entryLines = moduleNames.map(
4828
+ (name, index) => ` { module: ${JSON.stringify(name)}, registrations: serverRegistrations${index} },`
4829
+ );
4830
+ return [
4831
+ formatServerMarker(),
4832
+ "/** DO NOT EDIT \u2014 owned by the module install rail. */",
4833
+ "",
4834
+ ...importLines,
4835
+ ...importLines.length > 0 ? [""] : [],
4836
+ "export type ModuleServerRegistration = {",
4837
+ " module: string;",
4838
+ " registrations: Readonly<Record<string, unknown>>;",
4839
+ "};",
4840
+ "",
4841
+ ...entryLines.length === 0 ? ["export const MODULE_SERVER_REGISTRATIONS: readonly ModuleServerRegistration[] = [];"] : [
4842
+ "export const MODULE_SERVER_REGISTRATIONS: readonly ModuleServerRegistration[] = [",
4843
+ ...entryLines,
4844
+ "];"
4845
+ ],
4846
+ ""
4847
+ ].join("\n");
4848
+ }
4808
4849
 
4809
4850
  // ../../packages/lib/src/server/module-rail/composition-artifacts.ts
4810
4851
  var COMPOSITION_ARTIFACTS_GENERATED_BY = "module-rail";
@@ -4867,6 +4908,14 @@ function formatRouteMarker(owner, nextRelativePath) {
4867
4908
  function formatRegistryMarker() {
4868
4909
  return `// ${MODULE_STUB_MARKER} scope=registry`;
4869
4910
  }
4911
+ var OWNERLESS_MARKER_KINDS = [
4912
+ "registry",
4913
+ "contributions",
4914
+ "datastores",
4915
+ "i18n",
4916
+ "init",
4917
+ "server"
4918
+ ];
4870
4919
  function parseCompositionArtifactMarker(source) {
4871
4920
  const firstLine = source.split(/\r?\n/, 1)[0]?.trim() ?? "";
4872
4921
  const match = firstLine.match(
@@ -4877,26 +4926,8 @@ function parseCompositionArtifactMarker(source) {
4877
4926
  if (!match) return null;
4878
4927
  const owner = match[1];
4879
4928
  const scope = match[2];
4880
- if (scope === "registry") {
4881
- if (owner) return null;
4882
- return { kind: "registry" };
4883
- }
4884
- if (scope === "contributions") {
4885
- if (owner) return null;
4886
- return { kind: "contributions" };
4887
- }
4888
- if (scope === "datastores") {
4889
- if (owner) return null;
4890
- return { kind: "datastores" };
4891
- }
4892
- if (scope === "i18n") {
4893
- if (owner) return null;
4894
- return { kind: "i18n" };
4895
- }
4896
- if (scope === "init") {
4897
- if (owner) return null;
4898
- return { kind: "init" };
4899
- }
4929
+ const ownerless = OWNERLESS_MARKER_KINDS.find((kind) => kind === scope);
4930
+ if (ownerless) return owner ? null : { kind: ownerless };
4900
4931
  if (!owner || !isValidModuleName(owner)) return null;
4901
4932
  if (scope === "route") {
4902
4933
  const nextRelativePath = match[3]?.trim();
@@ -5022,6 +5053,7 @@ function expectedMarkerKindForPath(path3) {
5022
5053
  if (path3 === MODULE_DATASTORES_GEN_PATH) return "datastores";
5023
5054
  if (path3 === MODULE_I18N_GEN_PATH) return "i18n";
5024
5055
  if (path3 === MODULE_INIT_SERVER_GEN_PATH) return "init";
5056
+ if (path3 === MODULE_SERVER_GEN_PATH) return "server";
5025
5057
  if (path3.startsWith(API_ROUTE_DIR_PREFIX) && path3.endsWith("/route.ts")) return "endpoint";
5026
5058
  if (path3.startsWith(APP_DIR_PREFIX) && !path3.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path3.endsWith(suffix))) {
5027
5059
  return "route";
@@ -5621,6 +5653,7 @@ function planCompositionArtifacts(input) {
5621
5653
  let datastoresContent;
5622
5654
  let i18nContent;
5623
5655
  let initContent;
5656
+ let serverContent;
5624
5657
  try {
5625
5658
  contributionsContent = emitModuleContributionsGenTs({
5626
5659
  registry: input.registry,
@@ -5645,6 +5678,10 @@ function planCompositionArtifacts(input) {
5645
5678
  registry: input.registry,
5646
5679
  facts: compositionEntryFacts
5647
5680
  });
5681
+ serverContent = emitModuleServerGenTs({
5682
+ registry: input.registry,
5683
+ facts: compositionEntryFacts
5684
+ });
5648
5685
  } catch (error) {
5649
5686
  return {
5650
5687
  ok: false,
@@ -5657,6 +5694,7 @@ function planCompositionArtifacts(input) {
5657
5694
  desired.set(MODULE_DATASTORES_GEN_PATH, datastoresContent);
5658
5695
  desired.set(MODULE_I18N_GEN_PATH, i18nContent);
5659
5696
  desired.set(MODULE_INIT_SERVER_GEN_PATH, initContent);
5697
+ desired.set(MODULE_SERVER_GEN_PATH, serverContent);
5660
5698
  for (const stub of stubs.endpointStubs) desired.set(stub.path, stub.content);
5661
5699
  for (const stub of stubs.routeStubs) desired.set(stub.path, stub.content);
5662
5700
  const endpointStubByPath = new Map(stubs.endpointStubs.map((stub) => [stub.path, stub]));
@@ -6927,6 +6965,7 @@ function readFailurePath(error) {
6927
6965
  return typeof error === "object" && error !== null && "path" in error ? String(error.path) : "unknown";
6928
6966
  }
6929
6967
  var CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN = "conventional-entry-kind";
6968
+ var SERVER_REGISTRATIONS_FILE = "server/registrations.ts";
6930
6969
  var COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN = "composition-artifact-kind";
6931
6970
  async function classifyCompositionArtifactPathKind(context, path3) {
6932
6971
  return classifySandboxPathPresenceKind(context, path3, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
@@ -6964,6 +7003,10 @@ async function readConventionalEntryIfRealFile(path3, kind, presences, files) {
6964
7003
  }
6965
7004
  return source;
6966
7005
  }
7006
+ function hasRealServerRegistrations(modulePath, presences) {
7007
+ const path3 = `${modulePath}/${SERVER_REGISTRATIONS_FILE}`;
7008
+ return assertRealCompositionArtifactFilePresence(path3, presences.get(path3) ?? "unreadable") === "file";
7009
+ }
6967
7010
  async function readJsonObjectFile(path3, files) {
6968
7011
  const raw = files.get(path3);
6969
7012
  let parsed;
@@ -6987,6 +7030,9 @@ async function collectCompositionEntryFacts(context, registry) {
6987
7030
  const overridesDir = `${APP_PACKAGE_DIR}/src/lib/i18n/overrides`;
6988
7031
  const localeDirs = [substrateDir, overridesDir];
6989
7032
  const conventionalPaths = [];
7033
+ const serverRegistrationPaths = registry.modules.map(
7034
+ (mod) => `${MODULES_SANDBOX_DIR}/${mod.name}/${SERVER_REGISTRATIONS_FILE}`
7035
+ );
6990
7036
  for (const mod of registry.modules) {
6991
7037
  const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
6992
7038
  localeDirs.push(`${modulePath}/i18n`);
@@ -7015,7 +7061,7 @@ async function collectCompositionEntryFacts(context, registry) {
7015
7061
  const [conventionalPresences, localePresences] = await Promise.all([
7016
7062
  classifySandboxPathPresenceKindMany(
7017
7063
  context,
7018
- conventionalPaths,
7064
+ [...conventionalPaths, ...serverRegistrationPaths],
7019
7065
  CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN
7020
7066
  ),
7021
7067
  classifySandboxPathPresenceKindMany(context, localePaths, LOCALE_JSON_KIND_PROBE_TOKEN)
@@ -7077,6 +7123,7 @@ async function collectCompositionEntryFacts(context, registry) {
7077
7123
  );
7078
7124
  }
7079
7125
  }
7126
+ const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
7080
7127
  const rootLocales = localesByDir.get(`${modulePath}/i18n`);
7081
7128
  if (rootLocales.length > 0 && !rootLocales.includes("en")) {
7082
7129
  throw new Error(
@@ -7116,6 +7163,7 @@ async function collectCompositionEntryFacts(context, registry) {
7116
7163
  moduleName: mod.name,
7117
7164
  hasRootContributions,
7118
7165
  hasRootSlots,
7166
+ hasServerRegistrations,
7119
7167
  rootLocales,
7120
7168
  enhancements
7121
7169
  });
@@ -7319,6 +7367,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7319
7367
  MODULE_DATASTORES_GEN_PATH,
7320
7368
  MODULE_I18N_GEN_PATH,
7321
7369
  MODULE_INIT_SERVER_GEN_PATH,
7370
+ MODULE_SERVER_GEN_PATH,
7322
7371
  ...existingGeneratedPaths,
7323
7372
  ...Object.keys(existingAppPages),
7324
7373
  ...Object.keys(existingAppRoutes)
package/dist/index.d.mts CHANGED
@@ -89,7 +89,7 @@ declare function assertSupportedTypescript(versionMajorMinor: string): void;
89
89
  */
90
90
  declare function escapeGitignore(pattern: string): string;
91
91
  /**
92
- * Regenerate the Module-rail artifacts (the five `src/*.gen.ts` files and every
92
+ * Regenerate the Module-rail artifacts (the six `src/*.gen.ts` files and every
93
93
  * route/endpoint stub under `src/app`) for the app rooted at `options.cwd`.
94
94
  */
95
95
  declare function compose(options: ComposeOptions): Promise<ComposeResult>;
package/dist/index.d.ts CHANGED
@@ -89,7 +89,7 @@ declare function assertSupportedTypescript(versionMajorMinor: string): void;
89
89
  */
90
90
  declare function escapeGitignore(pattern: string): string;
91
91
  /**
92
- * Regenerate the Module-rail artifacts (the five `src/*.gen.ts` files and every
92
+ * Regenerate the Module-rail artifacts (the six `src/*.gen.ts` files and every
93
93
  * route/endpoint stub under `src/app`) for the app rooted at `options.cwd`.
94
94
  */
95
95
  declare function compose(options: ComposeOptions): Promise<ComposeResult>;
package/dist/index.js CHANGED
@@ -4134,6 +4134,7 @@ var MODULE_CONTRIBUTIONS_GEN_PATH = `${APP_WEB_PREFIX}src/module-contributions.g
4134
4134
  var MODULE_DATASTORES_GEN_PATH = `${APP_WEB_PREFIX}src/module-datastores.gen.ts`;
4135
4135
  var MODULE_I18N_GEN_PATH = `${APP_WEB_PREFIX}src/module-i18n.gen.ts`;
4136
4136
  var MODULE_INIT_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-init.server.gen.ts`;
4137
+ var MODULE_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-server.gen.ts`;
4137
4138
  var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
4138
4139
  var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
4139
4140
 
@@ -4150,7 +4151,8 @@ var COMPOSITION_RUNTIME_GEN_PATHS = [
4150
4151
  MODULE_CONTRIBUTIONS_GEN_PATH,
4151
4152
  MODULE_I18N_GEN_PATH,
4152
4153
  MODULE_INIT_SERVER_GEN_PATH,
4153
- MODULE_DATASTORES_GEN_PATH
4154
+ MODULE_DATASTORES_GEN_PATH,
4155
+ MODULE_SERVER_GEN_PATH
4154
4156
  ];
4155
4157
  function compareNames(a, b) {
4156
4158
  return a < b ? -1 : a > b ? 1 : 0;
@@ -4171,6 +4173,9 @@ function formatI18nMarker() {
4171
4173
  function formatInitMarker() {
4172
4174
  return `// ${MODULE_STUB_MARKER} scope=init`;
4173
4175
  }
4176
+ function formatServerMarker() {
4177
+ return `// ${MODULE_STUB_MARKER} scope=server`;
4178
+ }
4174
4179
  function formatDatastoresMarker() {
4175
4180
  return `// ${MODULE_STUB_MARKER} scope=datastores`;
4176
4181
  }
@@ -4814,6 +4819,42 @@ function emitModuleInitServerGenTs(input) {
4814
4819
  ""
4815
4820
  ].join("\n");
4816
4821
  }
4822
+ function emitModuleServerGenTs(input) {
4823
+ const installed = new Set(input.registry.modules.map((m) => m.name));
4824
+ const moduleNames = input.facts.filter((f) => f.hasServerRegistrations && installed.has(f.moduleName)).map((f) => f.moduleName).sort(compareNames);
4825
+ for (const name of moduleNames) {
4826
+ if (!isValidModuleName(name)) {
4827
+ throw new Error(`[ModuleRail] composition artifacts: invalid module name "${name}"`);
4828
+ }
4829
+ }
4830
+ const importLines = moduleNames.map(
4831
+ (name, index) => [
4832
+ `@/modules/${name}/server/registrations`,
4833
+ `import * as serverRegistrations${index} from ${JSON.stringify(`@/modules/${name}/server/registrations`)};`
4834
+ ]
4835
+ ).sort((a, b) => compareImportSpecifiers(a[0], b[0])).map(([, line]) => line);
4836
+ const entryLines = moduleNames.map(
4837
+ (name, index) => ` { module: ${JSON.stringify(name)}, registrations: serverRegistrations${index} },`
4838
+ );
4839
+ return [
4840
+ formatServerMarker(),
4841
+ "/** DO NOT EDIT \u2014 owned by the module install rail. */",
4842
+ "",
4843
+ ...importLines,
4844
+ ...importLines.length > 0 ? [""] : [],
4845
+ "export type ModuleServerRegistration = {",
4846
+ " module: string;",
4847
+ " registrations: Readonly<Record<string, unknown>>;",
4848
+ "};",
4849
+ "",
4850
+ ...entryLines.length === 0 ? ["export const MODULE_SERVER_REGISTRATIONS: readonly ModuleServerRegistration[] = [];"] : [
4851
+ "export const MODULE_SERVER_REGISTRATIONS: readonly ModuleServerRegistration[] = [",
4852
+ ...entryLines,
4853
+ "];"
4854
+ ],
4855
+ ""
4856
+ ].join("\n");
4857
+ }
4817
4858
 
4818
4859
  // ../../packages/lib/src/server/module-rail/composition-artifacts.ts
4819
4860
  var COMPOSITION_ARTIFACTS_GENERATED_BY = "module-rail";
@@ -4876,6 +4917,14 @@ function formatRouteMarker(owner, nextRelativePath) {
4876
4917
  function formatRegistryMarker() {
4877
4918
  return `// ${MODULE_STUB_MARKER} scope=registry`;
4878
4919
  }
4920
+ var OWNERLESS_MARKER_KINDS = [
4921
+ "registry",
4922
+ "contributions",
4923
+ "datastores",
4924
+ "i18n",
4925
+ "init",
4926
+ "server"
4927
+ ];
4879
4928
  function parseCompositionArtifactMarker(source) {
4880
4929
  const firstLine = source.split(/\r?\n/, 1)[0]?.trim() ?? "";
4881
4930
  const match = firstLine.match(
@@ -4886,26 +4935,8 @@ function parseCompositionArtifactMarker(source) {
4886
4935
  if (!match) return null;
4887
4936
  const owner = match[1];
4888
4937
  const scope = match[2];
4889
- if (scope === "registry") {
4890
- if (owner) return null;
4891
- return { kind: "registry" };
4892
- }
4893
- if (scope === "contributions") {
4894
- if (owner) return null;
4895
- return { kind: "contributions" };
4896
- }
4897
- if (scope === "datastores") {
4898
- if (owner) return null;
4899
- return { kind: "datastores" };
4900
- }
4901
- if (scope === "i18n") {
4902
- if (owner) return null;
4903
- return { kind: "i18n" };
4904
- }
4905
- if (scope === "init") {
4906
- if (owner) return null;
4907
- return { kind: "init" };
4908
- }
4938
+ const ownerless = OWNERLESS_MARKER_KINDS.find((kind) => kind === scope);
4939
+ if (ownerless) return owner ? null : { kind: ownerless };
4909
4940
  if (!owner || !isValidModuleName(owner)) return null;
4910
4941
  if (scope === "route") {
4911
4942
  const nextRelativePath = match[3]?.trim();
@@ -5031,6 +5062,7 @@ function expectedMarkerKindForPath(path3) {
5031
5062
  if (path3 === MODULE_DATASTORES_GEN_PATH) return "datastores";
5032
5063
  if (path3 === MODULE_I18N_GEN_PATH) return "i18n";
5033
5064
  if (path3 === MODULE_INIT_SERVER_GEN_PATH) return "init";
5065
+ if (path3 === MODULE_SERVER_GEN_PATH) return "server";
5034
5066
  if (path3.startsWith(API_ROUTE_DIR_PREFIX) && path3.endsWith("/route.ts")) return "endpoint";
5035
5067
  if (path3.startsWith(APP_DIR_PREFIX) && !path3.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path3.endsWith(suffix))) {
5036
5068
  return "route";
@@ -5630,6 +5662,7 @@ function planCompositionArtifacts(input) {
5630
5662
  let datastoresContent;
5631
5663
  let i18nContent;
5632
5664
  let initContent;
5665
+ let serverContent;
5633
5666
  try {
5634
5667
  contributionsContent = emitModuleContributionsGenTs({
5635
5668
  registry: input.registry,
@@ -5654,6 +5687,10 @@ function planCompositionArtifacts(input) {
5654
5687
  registry: input.registry,
5655
5688
  facts: compositionEntryFacts
5656
5689
  });
5690
+ serverContent = emitModuleServerGenTs({
5691
+ registry: input.registry,
5692
+ facts: compositionEntryFacts
5693
+ });
5657
5694
  } catch (error) {
5658
5695
  return {
5659
5696
  ok: false,
@@ -5666,6 +5703,7 @@ function planCompositionArtifacts(input) {
5666
5703
  desired.set(MODULE_DATASTORES_GEN_PATH, datastoresContent);
5667
5704
  desired.set(MODULE_I18N_GEN_PATH, i18nContent);
5668
5705
  desired.set(MODULE_INIT_SERVER_GEN_PATH, initContent);
5706
+ desired.set(MODULE_SERVER_GEN_PATH, serverContent);
5669
5707
  for (const stub of stubs.endpointStubs) desired.set(stub.path, stub.content);
5670
5708
  for (const stub of stubs.routeStubs) desired.set(stub.path, stub.content);
5671
5709
  const endpointStubByPath = new Map(stubs.endpointStubs.map((stub) => [stub.path, stub]));
@@ -6936,6 +6974,7 @@ function readFailurePath(error) {
6936
6974
  return typeof error === "object" && error !== null && "path" in error ? String(error.path) : "unknown";
6937
6975
  }
6938
6976
  var CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN = "conventional-entry-kind";
6977
+ var SERVER_REGISTRATIONS_FILE = "server/registrations.ts";
6939
6978
  var COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN = "composition-artifact-kind";
6940
6979
  async function classifyCompositionArtifactPathKind(context, path3) {
6941
6980
  return classifySandboxPathPresenceKind(context, path3, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
@@ -6973,6 +7012,10 @@ async function readConventionalEntryIfRealFile(path3, kind, presences, files) {
6973
7012
  }
6974
7013
  return source;
6975
7014
  }
7015
+ function hasRealServerRegistrations(modulePath, presences) {
7016
+ const path3 = `${modulePath}/${SERVER_REGISTRATIONS_FILE}`;
7017
+ return assertRealCompositionArtifactFilePresence(path3, presences.get(path3) ?? "unreadable") === "file";
7018
+ }
6976
7019
  async function readJsonObjectFile(path3, files) {
6977
7020
  const raw = files.get(path3);
6978
7021
  let parsed;
@@ -6996,6 +7039,9 @@ async function collectCompositionEntryFacts(context, registry) {
6996
7039
  const overridesDir = `${APP_PACKAGE_DIR}/src/lib/i18n/overrides`;
6997
7040
  const localeDirs = [substrateDir, overridesDir];
6998
7041
  const conventionalPaths = [];
7042
+ const serverRegistrationPaths = registry.modules.map(
7043
+ (mod) => `${MODULES_SANDBOX_DIR}/${mod.name}/${SERVER_REGISTRATIONS_FILE}`
7044
+ );
6999
7045
  for (const mod of registry.modules) {
7000
7046
  const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
7001
7047
  localeDirs.push(`${modulePath}/i18n`);
@@ -7024,7 +7070,7 @@ async function collectCompositionEntryFacts(context, registry) {
7024
7070
  const [conventionalPresences, localePresences] = await Promise.all([
7025
7071
  classifySandboxPathPresenceKindMany(
7026
7072
  context,
7027
- conventionalPaths,
7073
+ [...conventionalPaths, ...serverRegistrationPaths],
7028
7074
  CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN
7029
7075
  ),
7030
7076
  classifySandboxPathPresenceKindMany(context, localePaths, LOCALE_JSON_KIND_PROBE_TOKEN)
@@ -7086,6 +7132,7 @@ async function collectCompositionEntryFacts(context, registry) {
7086
7132
  );
7087
7133
  }
7088
7134
  }
7135
+ const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
7089
7136
  const rootLocales = localesByDir.get(`${modulePath}/i18n`);
7090
7137
  if (rootLocales.length > 0 && !rootLocales.includes("en")) {
7091
7138
  throw new Error(
@@ -7125,6 +7172,7 @@ async function collectCompositionEntryFacts(context, registry) {
7125
7172
  moduleName: mod.name,
7126
7173
  hasRootContributions,
7127
7174
  hasRootSlots,
7175
+ hasServerRegistrations,
7128
7176
  rootLocales,
7129
7177
  enhancements
7130
7178
  });
@@ -7328,6 +7376,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7328
7376
  MODULE_DATASTORES_GEN_PATH,
7329
7377
  MODULE_I18N_GEN_PATH,
7330
7378
  MODULE_INIT_SERVER_GEN_PATH,
7379
+ MODULE_SERVER_GEN_PATH,
7331
7380
  ...existingGeneratedPaths,
7332
7381
  ...Object.keys(existingAppPages),
7333
7382
  ...Object.keys(existingAppRoutes)
package/dist/index.mjs CHANGED
@@ -4119,6 +4119,7 @@ var MODULE_CONTRIBUTIONS_GEN_PATH = `${APP_WEB_PREFIX}src/module-contributions.g
4119
4119
  var MODULE_DATASTORES_GEN_PATH = `${APP_WEB_PREFIX}src/module-datastores.gen.ts`;
4120
4120
  var MODULE_I18N_GEN_PATH = `${APP_WEB_PREFIX}src/module-i18n.gen.ts`;
4121
4121
  var MODULE_INIT_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-init.server.gen.ts`;
4122
+ var MODULE_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-server.gen.ts`;
4122
4123
  var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
4123
4124
  var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
4124
4125
 
@@ -4135,7 +4136,8 @@ var COMPOSITION_RUNTIME_GEN_PATHS = [
4135
4136
  MODULE_CONTRIBUTIONS_GEN_PATH,
4136
4137
  MODULE_I18N_GEN_PATH,
4137
4138
  MODULE_INIT_SERVER_GEN_PATH,
4138
- MODULE_DATASTORES_GEN_PATH
4139
+ MODULE_DATASTORES_GEN_PATH,
4140
+ MODULE_SERVER_GEN_PATH
4139
4141
  ];
4140
4142
  function compareNames(a, b) {
4141
4143
  return a < b ? -1 : a > b ? 1 : 0;
@@ -4156,6 +4158,9 @@ function formatI18nMarker() {
4156
4158
  function formatInitMarker() {
4157
4159
  return `// ${MODULE_STUB_MARKER} scope=init`;
4158
4160
  }
4161
+ function formatServerMarker() {
4162
+ return `// ${MODULE_STUB_MARKER} scope=server`;
4163
+ }
4159
4164
  function formatDatastoresMarker() {
4160
4165
  return `// ${MODULE_STUB_MARKER} scope=datastores`;
4161
4166
  }
@@ -4799,6 +4804,42 @@ function emitModuleInitServerGenTs(input) {
4799
4804
  ""
4800
4805
  ].join("\n");
4801
4806
  }
4807
+ function emitModuleServerGenTs(input) {
4808
+ const installed = new Set(input.registry.modules.map((m) => m.name));
4809
+ const moduleNames = input.facts.filter((f) => f.hasServerRegistrations && installed.has(f.moduleName)).map((f) => f.moduleName).sort(compareNames);
4810
+ for (const name of moduleNames) {
4811
+ if (!isValidModuleName(name)) {
4812
+ throw new Error(`[ModuleRail] composition artifacts: invalid module name "${name}"`);
4813
+ }
4814
+ }
4815
+ const importLines = moduleNames.map(
4816
+ (name, index) => [
4817
+ `@/modules/${name}/server/registrations`,
4818
+ `import * as serverRegistrations${index} from ${JSON.stringify(`@/modules/${name}/server/registrations`)};`
4819
+ ]
4820
+ ).sort((a, b) => compareImportSpecifiers(a[0], b[0])).map(([, line]) => line);
4821
+ const entryLines = moduleNames.map(
4822
+ (name, index) => ` { module: ${JSON.stringify(name)}, registrations: serverRegistrations${index} },`
4823
+ );
4824
+ return [
4825
+ formatServerMarker(),
4826
+ "/** DO NOT EDIT \u2014 owned by the module install rail. */",
4827
+ "",
4828
+ ...importLines,
4829
+ ...importLines.length > 0 ? [""] : [],
4830
+ "export type ModuleServerRegistration = {",
4831
+ " module: string;",
4832
+ " registrations: Readonly<Record<string, unknown>>;",
4833
+ "};",
4834
+ "",
4835
+ ...entryLines.length === 0 ? ["export const MODULE_SERVER_REGISTRATIONS: readonly ModuleServerRegistration[] = [];"] : [
4836
+ "export const MODULE_SERVER_REGISTRATIONS: readonly ModuleServerRegistration[] = [",
4837
+ ...entryLines,
4838
+ "];"
4839
+ ],
4840
+ ""
4841
+ ].join("\n");
4842
+ }
4802
4843
 
4803
4844
  // ../../packages/lib/src/server/module-rail/composition-artifacts.ts
4804
4845
  var COMPOSITION_ARTIFACTS_GENERATED_BY = "module-rail";
@@ -4861,6 +4902,14 @@ function formatRouteMarker(owner, nextRelativePath) {
4861
4902
  function formatRegistryMarker() {
4862
4903
  return `// ${MODULE_STUB_MARKER} scope=registry`;
4863
4904
  }
4905
+ var OWNERLESS_MARKER_KINDS = [
4906
+ "registry",
4907
+ "contributions",
4908
+ "datastores",
4909
+ "i18n",
4910
+ "init",
4911
+ "server"
4912
+ ];
4864
4913
  function parseCompositionArtifactMarker(source) {
4865
4914
  const firstLine = source.split(/\r?\n/, 1)[0]?.trim() ?? "";
4866
4915
  const match = firstLine.match(
@@ -4871,26 +4920,8 @@ function parseCompositionArtifactMarker(source) {
4871
4920
  if (!match) return null;
4872
4921
  const owner = match[1];
4873
4922
  const scope = match[2];
4874
- if (scope === "registry") {
4875
- if (owner) return null;
4876
- return { kind: "registry" };
4877
- }
4878
- if (scope === "contributions") {
4879
- if (owner) return null;
4880
- return { kind: "contributions" };
4881
- }
4882
- if (scope === "datastores") {
4883
- if (owner) return null;
4884
- return { kind: "datastores" };
4885
- }
4886
- if (scope === "i18n") {
4887
- if (owner) return null;
4888
- return { kind: "i18n" };
4889
- }
4890
- if (scope === "init") {
4891
- if (owner) return null;
4892
- return { kind: "init" };
4893
- }
4923
+ const ownerless = OWNERLESS_MARKER_KINDS.find((kind) => kind === scope);
4924
+ if (ownerless) return owner ? null : { kind: ownerless };
4894
4925
  if (!owner || !isValidModuleName(owner)) return null;
4895
4926
  if (scope === "route") {
4896
4927
  const nextRelativePath = match[3]?.trim();
@@ -5016,6 +5047,7 @@ function expectedMarkerKindForPath(path3) {
5016
5047
  if (path3 === MODULE_DATASTORES_GEN_PATH) return "datastores";
5017
5048
  if (path3 === MODULE_I18N_GEN_PATH) return "i18n";
5018
5049
  if (path3 === MODULE_INIT_SERVER_GEN_PATH) return "init";
5050
+ if (path3 === MODULE_SERVER_GEN_PATH) return "server";
5019
5051
  if (path3.startsWith(API_ROUTE_DIR_PREFIX) && path3.endsWith("/route.ts")) return "endpoint";
5020
5052
  if (path3.startsWith(APP_DIR_PREFIX) && !path3.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path3.endsWith(suffix))) {
5021
5053
  return "route";
@@ -5615,6 +5647,7 @@ function planCompositionArtifacts(input) {
5615
5647
  let datastoresContent;
5616
5648
  let i18nContent;
5617
5649
  let initContent;
5650
+ let serverContent;
5618
5651
  try {
5619
5652
  contributionsContent = emitModuleContributionsGenTs({
5620
5653
  registry: input.registry,
@@ -5639,6 +5672,10 @@ function planCompositionArtifacts(input) {
5639
5672
  registry: input.registry,
5640
5673
  facts: compositionEntryFacts
5641
5674
  });
5675
+ serverContent = emitModuleServerGenTs({
5676
+ registry: input.registry,
5677
+ facts: compositionEntryFacts
5678
+ });
5642
5679
  } catch (error) {
5643
5680
  return {
5644
5681
  ok: false,
@@ -5651,6 +5688,7 @@ function planCompositionArtifacts(input) {
5651
5688
  desired.set(MODULE_DATASTORES_GEN_PATH, datastoresContent);
5652
5689
  desired.set(MODULE_I18N_GEN_PATH, i18nContent);
5653
5690
  desired.set(MODULE_INIT_SERVER_GEN_PATH, initContent);
5691
+ desired.set(MODULE_SERVER_GEN_PATH, serverContent);
5654
5692
  for (const stub of stubs.endpointStubs) desired.set(stub.path, stub.content);
5655
5693
  for (const stub of stubs.routeStubs) desired.set(stub.path, stub.content);
5656
5694
  const endpointStubByPath = new Map(stubs.endpointStubs.map((stub) => [stub.path, stub]));
@@ -6921,6 +6959,7 @@ function readFailurePath(error) {
6921
6959
  return typeof error === "object" && error !== null && "path" in error ? String(error.path) : "unknown";
6922
6960
  }
6923
6961
  var CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN = "conventional-entry-kind";
6962
+ var SERVER_REGISTRATIONS_FILE = "server/registrations.ts";
6924
6963
  var COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN = "composition-artifact-kind";
6925
6964
  async function classifyCompositionArtifactPathKind(context, path3) {
6926
6965
  return classifySandboxPathPresenceKind(context, path3, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
@@ -6958,6 +6997,10 @@ async function readConventionalEntryIfRealFile(path3, kind, presences, files) {
6958
6997
  }
6959
6998
  return source;
6960
6999
  }
7000
+ function hasRealServerRegistrations(modulePath, presences) {
7001
+ const path3 = `${modulePath}/${SERVER_REGISTRATIONS_FILE}`;
7002
+ return assertRealCompositionArtifactFilePresence(path3, presences.get(path3) ?? "unreadable") === "file";
7003
+ }
6961
7004
  async function readJsonObjectFile(path3, files) {
6962
7005
  const raw = files.get(path3);
6963
7006
  let parsed;
@@ -6981,6 +7024,9 @@ async function collectCompositionEntryFacts(context, registry) {
6981
7024
  const overridesDir = `${APP_PACKAGE_DIR}/src/lib/i18n/overrides`;
6982
7025
  const localeDirs = [substrateDir, overridesDir];
6983
7026
  const conventionalPaths = [];
7027
+ const serverRegistrationPaths = registry.modules.map(
7028
+ (mod) => `${MODULES_SANDBOX_DIR}/${mod.name}/${SERVER_REGISTRATIONS_FILE}`
7029
+ );
6984
7030
  for (const mod of registry.modules) {
6985
7031
  const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
6986
7032
  localeDirs.push(`${modulePath}/i18n`);
@@ -7009,7 +7055,7 @@ async function collectCompositionEntryFacts(context, registry) {
7009
7055
  const [conventionalPresences, localePresences] = await Promise.all([
7010
7056
  classifySandboxPathPresenceKindMany(
7011
7057
  context,
7012
- conventionalPaths,
7058
+ [...conventionalPaths, ...serverRegistrationPaths],
7013
7059
  CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN
7014
7060
  ),
7015
7061
  classifySandboxPathPresenceKindMany(context, localePaths, LOCALE_JSON_KIND_PROBE_TOKEN)
@@ -7071,6 +7117,7 @@ async function collectCompositionEntryFacts(context, registry) {
7071
7117
  );
7072
7118
  }
7073
7119
  }
7120
+ const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
7074
7121
  const rootLocales = localesByDir.get(`${modulePath}/i18n`);
7075
7122
  if (rootLocales.length > 0 && !rootLocales.includes("en")) {
7076
7123
  throw new Error(
@@ -7110,6 +7157,7 @@ async function collectCompositionEntryFacts(context, registry) {
7110
7157
  moduleName: mod.name,
7111
7158
  hasRootContributions,
7112
7159
  hasRootSlots,
7160
+ hasServerRegistrations,
7113
7161
  rootLocales,
7114
7162
  enhancements
7115
7163
  });
@@ -7313,6 +7361,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7313
7361
  MODULE_DATASTORES_GEN_PATH,
7314
7362
  MODULE_I18N_GEN_PATH,
7315
7363
  MODULE_INIT_SERVER_GEN_PATH,
7364
+ MODULE_SERVER_GEN_PATH,
7316
7365
  ...existingGeneratedPaths,
7317
7366
  ...Object.keys(existingAppPages),
7318
7367
  ...Object.keys(existingAppRoutes)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/compose",
3
- "version": "0.7.0",
4
- "description": "Regenerates a Stardeck app's Module-rail artifacts — the five src/*.gen.ts registries and every route/endpoint stub — from src/modules/*/module.json",
3
+ "version": "0.8.0",
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",
7
7
  "types": "dist/index.d.ts",