@stardeck-customer-apps/compose 0.6.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 +29 -2
- package/dist/cli.js +135 -40
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +135 -40
- package/dist/index.mjs +135 -40
- package/package.json +2 -2
package/SKILL.md
CHANGED
|
@@ -9,11 +9,13 @@ Every Module in `apps/web/src/modules/*` declares its mounts, endpoints,
|
|
|
9
9
|
enhancements, contributions, i18n layers and data stores in `module.json`.
|
|
10
10
|
Compose turns that declaration into the code Next.js actually builds:
|
|
11
11
|
|
|
12
|
-
- `apps/web/src/modules.gen.ts` — the Module registry and `
|
|
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
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
|
|
|
@@ -89,6 +91,31 @@ a silent fallback. It holds deployment facts: gitignore it (`apps/web/.gitignore
|
|
|
89
91
|
locale; the locale also needs a `lib/i18n/substrate/<locale>.json`. New keys
|
|
90
92
|
go in `lib/i18n/substrate/<locale>.json`, never in an override. Needs compose
|
|
91
93
|
0.6.0 or later.
|
|
94
|
+
- **Headless mounts.** `NEXT_PUBLIC_STARDECK_HEADLESS_MOUNTS` (set by the
|
|
95
|
+
platform from the Modules tab) lists mount keys `<module>:<group>/<segment>`
|
|
96
|
+
from `module.json` `mounts`, comma-joined, e.g. `pos:(register)/register`.
|
|
97
|
+
The generated page for a listed mount returns not-found, and `admin.nav`
|
|
98
|
+
entries whose `href` is that mount's path (or below it) are dropped. The rest
|
|
99
|
+
of the Module keeps running: its other mounts, every endpoint, `server/`,
|
|
100
|
+
`client/`, initializers and other contributions. Build your own screen in
|
|
101
|
+
app code on the Module's `server/` entry and `client/` hooks, at a different
|
|
102
|
+
URL: the generated page still exists, so an app page at the same URL fails
|
|
103
|
+
compose as a route collision.
|
|
104
|
+
`isMountHeadless(key)` and `isHrefHeadless(href)` are exported from
|
|
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.
|
|
92
119
|
- `@stardeck-customer-apps/compose/runtime` exports `isModuleEnabledByList`,
|
|
93
120
|
the single enablement rule, and imports nothing — it is safe in client
|
|
94
121
|
components.
|
|
@@ -99,5 +126,5 @@ After each run compose rewrites `apps/web/src/app/.gitignore` with one entry per
|
|
|
99
126
|
stub it emits — stubs are scattered one-per-route and cannot be globbed. Commit
|
|
100
127
|
that file: it is deterministic output, and compose rewrites it only when the set
|
|
101
128
|
of stubs changes, so an unchanged compose leaves `git status` clean. It does not
|
|
102
|
-
list the
|
|
129
|
+
list the six `src/*.gen.ts`; a static `src/*.gen.ts` line in `apps/web/.gitignore`
|
|
103
130
|
covers those. Compose refuses to touch the file if line 1 is not its own header.
|
package/dist/cli.js
CHANGED
|
@@ -3891,10 +3891,12 @@ function analyzeClientSafeEntryImports(sourceText, fileName) {
|
|
|
3891
3891
|
var APP_WEB_PREFIX = "apps/web/";
|
|
3892
3892
|
var MODULE_STUB_MARKER = "@stardeck-module-rail-generated";
|
|
3893
3893
|
var ENABLED_MODULES_ENV = "NEXT_PUBLIC_STARDECK_ENABLED_MODULES";
|
|
3894
|
+
var HEADLESS_MOUNTS_ENV = "NEXT_PUBLIC_STARDECK_HEADLESS_MOUNTS";
|
|
3894
3895
|
var MODULE_CONTRIBUTIONS_GEN_PATH = `${APP_WEB_PREFIX}src/module-contributions.gen.ts`;
|
|
3895
3896
|
var MODULE_DATASTORES_GEN_PATH = `${APP_WEB_PREFIX}src/module-datastores.gen.ts`;
|
|
3896
3897
|
var MODULE_I18N_GEN_PATH = `${APP_WEB_PREFIX}src/module-i18n.gen.ts`;
|
|
3897
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`;
|
|
3898
3900
|
var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
|
|
3899
3901
|
var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
|
|
3900
3902
|
|
|
@@ -4127,6 +4129,11 @@ function isModuleEnabledByList(raw, name, kindOf) {
|
|
|
4127
4129
|
return raw.split(",").includes(name);
|
|
4128
4130
|
}
|
|
4129
4131
|
|
|
4132
|
+
// ../../packages/lib/src/shared/module-mount-key.ts
|
|
4133
|
+
function mountKey(moduleName, mount) {
|
|
4134
|
+
return `${moduleName}:${mount.group}/${mount.segment}`;
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4130
4137
|
// ../../packages/lib/src/server/module-rail/composition-artifacts.ts
|
|
4131
4138
|
var import_typescript4 = __toESM(require("typescript"), 1);
|
|
4132
4139
|
|
|
@@ -4135,7 +4142,8 @@ var COMPOSITION_RUNTIME_GEN_PATHS = [
|
|
|
4135
4142
|
MODULE_CONTRIBUTIONS_GEN_PATH,
|
|
4136
4143
|
MODULE_I18N_GEN_PATH,
|
|
4137
4144
|
MODULE_INIT_SERVER_GEN_PATH,
|
|
4138
|
-
MODULE_DATASTORES_GEN_PATH
|
|
4145
|
+
MODULE_DATASTORES_GEN_PATH,
|
|
4146
|
+
MODULE_SERVER_GEN_PATH
|
|
4139
4147
|
];
|
|
4140
4148
|
function compareNames(a, b) {
|
|
4141
4149
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -4156,6 +4164,9 @@ function formatI18nMarker() {
|
|
|
4156
4164
|
function formatInitMarker() {
|
|
4157
4165
|
return `// ${MODULE_STUB_MARKER} scope=init`;
|
|
4158
4166
|
}
|
|
4167
|
+
function formatServerMarker() {
|
|
4168
|
+
return `// ${MODULE_STUB_MARKER} scope=server`;
|
|
4169
|
+
}
|
|
4159
4170
|
function formatDatastoresMarker() {
|
|
4160
4171
|
return `// ${MODULE_STUB_MARKER} scope=datastores`;
|
|
4161
4172
|
}
|
|
@@ -4455,7 +4466,7 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4455
4466
|
" type SurfaceSlotCatalog,",
|
|
4456
4467
|
'} from "@/lib/modules/contributions";',
|
|
4457
4468
|
'import { substrateSlotCatalogs } from "@/lib/modules/substrate-slots";',
|
|
4458
|
-
'import { generatedModuleRegistry, isModuleEnabled } from "@/modules.gen";',
|
|
4469
|
+
'import { generatedModuleRegistry, isHrefHeadless, isModuleEnabled } from "@/modules.gen";',
|
|
4459
4470
|
...[...moduleImportLines].sort((a, b) => {
|
|
4460
4471
|
const pathA = a.match(/from ("[^"]+")/)?.[1] ?? a;
|
|
4461
4472
|
const pathB = b.match(/from ("[^"]+")/)?.[1] ?? b;
|
|
@@ -4505,11 +4516,19 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4505
4516
|
"export function resolveModuleContributions(",
|
|
4506
4517
|
" registry: typeof generatedModuleRegistry = generatedModuleRegistry",
|
|
4507
4518
|
"): ResolvedModuleContributions {",
|
|
4508
|
-
"
|
|
4519
|
+
" const resolved = resolveContributions({",
|
|
4509
4520
|
" registry,",
|
|
4510
4521
|
" catalogs: moduleSlotCatalogs,",
|
|
4511
4522
|
" sources: moduleContributionSources,",
|
|
4512
4523
|
" });",
|
|
4524
|
+
" // A nav entry into a headless mount would link to a page that is switched off.",
|
|
4525
|
+
' const nav = resolved["admin.nav"];',
|
|
4526
|
+
" if (nav) {",
|
|
4527
|
+
' resolved["admin.nav"] = nav.filter(',
|
|
4528
|
+
" (entry) => !isHrefHeadless((entry as { href?: unknown }).href)",
|
|
4529
|
+
" );",
|
|
4530
|
+
" }",
|
|
4531
|
+
" return resolved;",
|
|
4513
4532
|
"}",
|
|
4514
4533
|
"",
|
|
4515
4534
|
"export const resolvedModuleContributions: ResolvedModuleContributions =",
|
|
@@ -4791,6 +4810,42 @@ function emitModuleInitServerGenTs(input) {
|
|
|
4791
4810
|
""
|
|
4792
4811
|
].join("\n");
|
|
4793
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
|
+
}
|
|
4794
4849
|
|
|
4795
4850
|
// ../../packages/lib/src/server/module-rail/composition-artifacts.ts
|
|
4796
4851
|
var COMPOSITION_ARTIFACTS_GENERATED_BY = "module-rail";
|
|
@@ -4853,6 +4908,14 @@ function formatRouteMarker(owner, nextRelativePath) {
|
|
|
4853
4908
|
function formatRegistryMarker() {
|
|
4854
4909
|
return `// ${MODULE_STUB_MARKER} scope=registry`;
|
|
4855
4910
|
}
|
|
4911
|
+
var OWNERLESS_MARKER_KINDS = [
|
|
4912
|
+
"registry",
|
|
4913
|
+
"contributions",
|
|
4914
|
+
"datastores",
|
|
4915
|
+
"i18n",
|
|
4916
|
+
"init",
|
|
4917
|
+
"server"
|
|
4918
|
+
];
|
|
4856
4919
|
function parseCompositionArtifactMarker(source) {
|
|
4857
4920
|
const firstLine = source.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
4858
4921
|
const match = firstLine.match(
|
|
@@ -4863,26 +4926,8 @@ function parseCompositionArtifactMarker(source) {
|
|
|
4863
4926
|
if (!match) return null;
|
|
4864
4927
|
const owner = match[1];
|
|
4865
4928
|
const scope = match[2];
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
return { kind: "registry" };
|
|
4869
|
-
}
|
|
4870
|
-
if (scope === "contributions") {
|
|
4871
|
-
if (owner) return null;
|
|
4872
|
-
return { kind: "contributions" };
|
|
4873
|
-
}
|
|
4874
|
-
if (scope === "datastores") {
|
|
4875
|
-
if (owner) return null;
|
|
4876
|
-
return { kind: "datastores" };
|
|
4877
|
-
}
|
|
4878
|
-
if (scope === "i18n") {
|
|
4879
|
-
if (owner) return null;
|
|
4880
|
-
return { kind: "i18n" };
|
|
4881
|
-
}
|
|
4882
|
-
if (scope === "init") {
|
|
4883
|
-
if (owner) return null;
|
|
4884
|
-
return { kind: "init" };
|
|
4885
|
-
}
|
|
4929
|
+
const ownerless = OWNERLESS_MARKER_KINDS.find((kind) => kind === scope);
|
|
4930
|
+
if (ownerless) return owner ? null : { kind: ownerless };
|
|
4886
4931
|
if (!owner || !isValidModuleName(owner)) return null;
|
|
4887
4932
|
if (scope === "route") {
|
|
4888
4933
|
const nextRelativePath = match[3]?.trim();
|
|
@@ -5008,6 +5053,7 @@ function expectedMarkerKindForPath(path3) {
|
|
|
5008
5053
|
if (path3 === MODULE_DATASTORES_GEN_PATH) return "datastores";
|
|
5009
5054
|
if (path3 === MODULE_I18N_GEN_PATH) return "i18n";
|
|
5010
5055
|
if (path3 === MODULE_INIT_SERVER_GEN_PATH) return "init";
|
|
5056
|
+
if (path3 === MODULE_SERVER_GEN_PATH) return "server";
|
|
5011
5057
|
if (path3.startsWith(API_ROUTE_DIR_PREFIX) && path3.endsWith("/route.ts")) return "endpoint";
|
|
5012
5058
|
if (path3.startsWith(APP_DIR_PREFIX) && !path3.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path3.endsWith(suffix))) {
|
|
5013
5059
|
return "route";
|
|
@@ -5095,6 +5141,25 @@ function emitModulesGenTs(registry) {
|
|
|
5095
5141
|
' if (entry && entry.kind !== "feature") return true;',
|
|
5096
5142
|
' return raw.split(",").includes(name);',
|
|
5097
5143
|
"}",
|
|
5144
|
+
"",
|
|
5145
|
+
"/** Headless mount keys come from the platform at build time: that page is off, its Module stays on. */",
|
|
5146
|
+
"export function isMountHeadless(key: string): boolean {",
|
|
5147
|
+
` const raw = process.env.${HEADLESS_MOUNTS_ENV};`,
|
|
5148
|
+
' return raw ? raw.split(",").includes(key) : false;',
|
|
5149
|
+
"}",
|
|
5150
|
+
"",
|
|
5151
|
+
"/** True when `href` is a headless mount's URL or below it. */",
|
|
5152
|
+
"export function isHrefHeadless(href: unknown): boolean {",
|
|
5153
|
+
' if (typeof href !== "string") return false;',
|
|
5154
|
+
" const target = href.split(/[?#]/)[0];",
|
|
5155
|
+
" return generatedModuleRegistry.modules.some((mod) =>",
|
|
5156
|
+
" mod.mounts.some((mount) => {",
|
|
5157
|
+
" if (!isMountHeadless(`${mod.name}:${mount.group}/${mount.segment}`)) return false;",
|
|
5158
|
+
" const path = `/${mount.segment}`;",
|
|
5159
|
+
" return target === path || target.startsWith(`${path}/`);",
|
|
5160
|
+
" })",
|
|
5161
|
+
" );",
|
|
5162
|
+
"}",
|
|
5098
5163
|
""
|
|
5099
5164
|
].join("\n");
|
|
5100
5165
|
}
|
|
@@ -5102,6 +5167,22 @@ var STUB_PRINT_WIDTH = 100;
|
|
|
5102
5167
|
function wrapAtPrintWidth(flat, broken) {
|
|
5103
5168
|
return flat.length > STUB_PRINT_WIDTH ? broken : [flat];
|
|
5104
5169
|
}
|
|
5170
|
+
function emitGuardLines(guards, body) {
|
|
5171
|
+
const operands = guards.map((guard) => `${guard.call}(${emitTsString(guard.arg)})`);
|
|
5172
|
+
const condition = ` if (${operands.join(" || ")})`;
|
|
5173
|
+
const brokenOperands = guards.flatMap((guard, i) => {
|
|
5174
|
+
const suffix = i < guards.length - 1 ? " ||" : "";
|
|
5175
|
+
return wrapAtPrintWidth(` ${operands[i]}${suffix}`, [
|
|
5176
|
+
` ${guard.call}(`,
|
|
5177
|
+
` ${emitTsString(guard.arg)}`,
|
|
5178
|
+
` )${suffix}`
|
|
5179
|
+
]);
|
|
5180
|
+
});
|
|
5181
|
+
return wrapAtPrintWidth(`${condition} ${body}`, [
|
|
5182
|
+
...wrapAtPrintWidth(condition, [" if (", ...brokenOperands, " )"]),
|
|
5183
|
+
` ${body}`
|
|
5184
|
+
]);
|
|
5185
|
+
}
|
|
5105
5186
|
function buildEndpointStubContent(input) {
|
|
5106
5187
|
const sorted = [...input.methods].sort(
|
|
5107
5188
|
(a, b) => METHOD_ORDER[a.method] - METHOD_ORDER[b.method] || a.pairKey.localeCompare(b.pairKey)
|
|
@@ -5109,24 +5190,15 @@ function buildEndpointStubContent(input) {
|
|
|
5109
5190
|
const exports2 = sorted.map((m) => {
|
|
5110
5191
|
const pair = emitTsString(m.pairKey);
|
|
5111
5192
|
const guards = [
|
|
5112
|
-
|
|
5113
|
-
...input.scope.kind === "enhancement" ? [
|
|
5193
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5194
|
+
...input.scope.kind === "enhancement" ? [{ call: "!isModuleEnabled", arg: input.scope.peer }] : []
|
|
5114
5195
|
];
|
|
5115
|
-
const condition = ` if (${guards.join(" || ")})`;
|
|
5116
|
-
const notFoundBody = "return new Response(null, { status: 404 });";
|
|
5117
5196
|
const message = emitTsString(`Missing endpoint handler for ${m.pairKey}`);
|
|
5118
5197
|
const importPath = emitTsString(input.importModule);
|
|
5119
5198
|
return [
|
|
5120
5199
|
`export async function ${m.method}(request: Request, context?: unknown): Promise<Response> {`,
|
|
5121
5200
|
" await initializeActiveEnhancements();",
|
|
5122
|
-
...
|
|
5123
|
-
...wrapAtPrintWidth(condition, [
|
|
5124
|
-
" if (",
|
|
5125
|
-
...guards.map((guard, i) => ` ${guard}${i < guards.length - 1 ? " ||" : ""}`),
|
|
5126
|
-
" )"
|
|
5127
|
-
]),
|
|
5128
|
-
` ${notFoundBody}`
|
|
5129
|
-
]),
|
|
5201
|
+
...emitGuardLines(guards, "return new Response(null, { status: 404 });"),
|
|
5130
5202
|
...wrapAtPrintWidth(` const { endpointHandlers } = await import(${importPath});`, [
|
|
5131
5203
|
" const { endpointHandlers } = await import(",
|
|
5132
5204
|
` ${importPath}`,
|
|
@@ -5156,12 +5228,16 @@ function buildEndpointStubContent(input) {
|
|
|
5156
5228
|
].join("\n");
|
|
5157
5229
|
}
|
|
5158
5230
|
function buildRouteStubContent(input) {
|
|
5231
|
+
const guards = [
|
|
5232
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5233
|
+
{ call: "isMountHeadless", arg: input.mountKey }
|
|
5234
|
+
];
|
|
5159
5235
|
const lines = [
|
|
5160
5236
|
formatRouteMarker(input.owner, input.nextRelativePath),
|
|
5161
5237
|
'import { notFound } from "next/navigation";',
|
|
5162
5238
|
// simple-import-sort puts packages and "@/" aliases in separate groups.
|
|
5163
5239
|
"",
|
|
5164
|
-
'import { isModuleEnabled } from "@/modules.gen";',
|
|
5240
|
+
'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
|
|
5165
5241
|
`import Page from ${emitTsString(input.importModule)};`
|
|
5166
5242
|
];
|
|
5167
5243
|
if (input.hasGenerateMetadata) {
|
|
@@ -5169,7 +5245,7 @@ function buildRouteStubContent(input) {
|
|
|
5169
5245
|
`import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
|
|
5170
5246
|
"",
|
|
5171
5247
|
"export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
|
|
5172
|
-
|
|
5248
|
+
...emitGuardLines(guards, "return {};"),
|
|
5173
5249
|
" return sourceGenerateMetadata(...args);",
|
|
5174
5250
|
"}"
|
|
5175
5251
|
);
|
|
@@ -5180,7 +5256,7 @@ function buildRouteStubContent(input) {
|
|
|
5180
5256
|
" Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
|
|
5181
5257
|
"",
|
|
5182
5258
|
"export default function ModulePage(props: PageProps) {",
|
|
5183
|
-
|
|
5259
|
+
...emitGuardLines(guards, "notFound();"),
|
|
5184
5260
|
" return <Page {...props} />;",
|
|
5185
5261
|
"}",
|
|
5186
5262
|
""
|
|
@@ -5382,7 +5458,6 @@ function planRouteStubs(input) {
|
|
|
5382
5458
|
}
|
|
5383
5459
|
for (const entry of entries) {
|
|
5384
5460
|
const nextRelativePath = entry.nextRelativePath;
|
|
5385
|
-
analyzeRouteEntryPath(nextRelativePath);
|
|
5386
5461
|
const destination = normalizeAppRouterDestinationPattern(nextRelativePath);
|
|
5387
5462
|
const path3 = routeEntryToAppPageFile(nextRelativePath);
|
|
5388
5463
|
const existing = routeDestinationOwners.get(destination);
|
|
@@ -5397,6 +5472,8 @@ function planRouteStubs(input) {
|
|
|
5397
5472
|
path: path3
|
|
5398
5473
|
});
|
|
5399
5474
|
const importModule = routeEntryImport(regMod.name, nextRelativePath);
|
|
5475
|
+
const analyzed = analyzeRouteEntryPath(nextRelativePath);
|
|
5476
|
+
const mount = mounts.find((claim) => routeEntryCoveredByMountClaim(analyzed, claim));
|
|
5400
5477
|
routeStubs.push({
|
|
5401
5478
|
path: path3,
|
|
5402
5479
|
owner: regMod.name,
|
|
@@ -5406,6 +5483,7 @@ function planRouteStubs(input) {
|
|
|
5406
5483
|
owner: regMod.name,
|
|
5407
5484
|
nextRelativePath,
|
|
5408
5485
|
importModule,
|
|
5486
|
+
mountKey: mountKey(regMod.name, mount),
|
|
5409
5487
|
hasGenerateMetadata: entry.hasGenerateMetadata
|
|
5410
5488
|
})
|
|
5411
5489
|
});
|
|
@@ -5575,6 +5653,7 @@ function planCompositionArtifacts(input) {
|
|
|
5575
5653
|
let datastoresContent;
|
|
5576
5654
|
let i18nContent;
|
|
5577
5655
|
let initContent;
|
|
5656
|
+
let serverContent;
|
|
5578
5657
|
try {
|
|
5579
5658
|
contributionsContent = emitModuleContributionsGenTs({
|
|
5580
5659
|
registry: input.registry,
|
|
@@ -5599,6 +5678,10 @@ function planCompositionArtifacts(input) {
|
|
|
5599
5678
|
registry: input.registry,
|
|
5600
5679
|
facts: compositionEntryFacts
|
|
5601
5680
|
});
|
|
5681
|
+
serverContent = emitModuleServerGenTs({
|
|
5682
|
+
registry: input.registry,
|
|
5683
|
+
facts: compositionEntryFacts
|
|
5684
|
+
});
|
|
5602
5685
|
} catch (error) {
|
|
5603
5686
|
return {
|
|
5604
5687
|
ok: false,
|
|
@@ -5611,6 +5694,7 @@ function planCompositionArtifacts(input) {
|
|
|
5611
5694
|
desired.set(MODULE_DATASTORES_GEN_PATH, datastoresContent);
|
|
5612
5695
|
desired.set(MODULE_I18N_GEN_PATH, i18nContent);
|
|
5613
5696
|
desired.set(MODULE_INIT_SERVER_GEN_PATH, initContent);
|
|
5697
|
+
desired.set(MODULE_SERVER_GEN_PATH, serverContent);
|
|
5614
5698
|
for (const stub of stubs.endpointStubs) desired.set(stub.path, stub.content);
|
|
5615
5699
|
for (const stub of stubs.routeStubs) desired.set(stub.path, stub.content);
|
|
5616
5700
|
const endpointStubByPath = new Map(stubs.endpointStubs.map((stub) => [stub.path, stub]));
|
|
@@ -6881,6 +6965,7 @@ function readFailurePath(error) {
|
|
|
6881
6965
|
return typeof error === "object" && error !== null && "path" in error ? String(error.path) : "unknown";
|
|
6882
6966
|
}
|
|
6883
6967
|
var CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN = "conventional-entry-kind";
|
|
6968
|
+
var SERVER_REGISTRATIONS_FILE = "server/registrations.ts";
|
|
6884
6969
|
var COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN = "composition-artifact-kind";
|
|
6885
6970
|
async function classifyCompositionArtifactPathKind(context, path3) {
|
|
6886
6971
|
return classifySandboxPathPresenceKind(context, path3, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
|
|
@@ -6918,6 +7003,10 @@ async function readConventionalEntryIfRealFile(path3, kind, presences, files) {
|
|
|
6918
7003
|
}
|
|
6919
7004
|
return source;
|
|
6920
7005
|
}
|
|
7006
|
+
function hasRealServerRegistrations(modulePath, presences) {
|
|
7007
|
+
const path3 = `${modulePath}/${SERVER_REGISTRATIONS_FILE}`;
|
|
7008
|
+
return assertRealCompositionArtifactFilePresence(path3, presences.get(path3) ?? "unreadable") === "file";
|
|
7009
|
+
}
|
|
6921
7010
|
async function readJsonObjectFile(path3, files) {
|
|
6922
7011
|
const raw = files.get(path3);
|
|
6923
7012
|
let parsed;
|
|
@@ -6941,6 +7030,9 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6941
7030
|
const overridesDir = `${APP_PACKAGE_DIR}/src/lib/i18n/overrides`;
|
|
6942
7031
|
const localeDirs = [substrateDir, overridesDir];
|
|
6943
7032
|
const conventionalPaths = [];
|
|
7033
|
+
const serverRegistrationPaths = registry.modules.map(
|
|
7034
|
+
(mod) => `${MODULES_SANDBOX_DIR}/${mod.name}/${SERVER_REGISTRATIONS_FILE}`
|
|
7035
|
+
);
|
|
6944
7036
|
for (const mod of registry.modules) {
|
|
6945
7037
|
const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
|
|
6946
7038
|
localeDirs.push(`${modulePath}/i18n`);
|
|
@@ -6969,7 +7061,7 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6969
7061
|
const [conventionalPresences, localePresences] = await Promise.all([
|
|
6970
7062
|
classifySandboxPathPresenceKindMany(
|
|
6971
7063
|
context,
|
|
6972
|
-
conventionalPaths,
|
|
7064
|
+
[...conventionalPaths, ...serverRegistrationPaths],
|
|
6973
7065
|
CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN
|
|
6974
7066
|
),
|
|
6975
7067
|
classifySandboxPathPresenceKindMany(context, localePaths, LOCALE_JSON_KIND_PROBE_TOKEN)
|
|
@@ -7031,6 +7123,7 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7031
7123
|
);
|
|
7032
7124
|
}
|
|
7033
7125
|
}
|
|
7126
|
+
const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
|
|
7034
7127
|
const rootLocales = localesByDir.get(`${modulePath}/i18n`);
|
|
7035
7128
|
if (rootLocales.length > 0 && !rootLocales.includes("en")) {
|
|
7036
7129
|
throw new Error(
|
|
@@ -7070,6 +7163,7 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7070
7163
|
moduleName: mod.name,
|
|
7071
7164
|
hasRootContributions,
|
|
7072
7165
|
hasRootSlots,
|
|
7166
|
+
hasServerRegistrations,
|
|
7073
7167
|
rootLocales,
|
|
7074
7168
|
enhancements
|
|
7075
7169
|
});
|
|
@@ -7273,6 +7367,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
7273
7367
|
MODULE_DATASTORES_GEN_PATH,
|
|
7274
7368
|
MODULE_I18N_GEN_PATH,
|
|
7275
7369
|
MODULE_INIT_SERVER_GEN_PATH,
|
|
7370
|
+
MODULE_SERVER_GEN_PATH,
|
|
7276
7371
|
...existingGeneratedPaths,
|
|
7277
7372
|
...Object.keys(existingAppPages),
|
|
7278
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
|
|
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
|
|
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
|
@@ -4129,13 +4129,20 @@ function isModuleEnabledByList(raw, name, kindOf) {
|
|
|
4129
4129
|
var APP_WEB_PREFIX = "apps/web/";
|
|
4130
4130
|
var MODULE_STUB_MARKER = "@stardeck-module-rail-generated";
|
|
4131
4131
|
var ENABLED_MODULES_ENV = "NEXT_PUBLIC_STARDECK_ENABLED_MODULES";
|
|
4132
|
+
var HEADLESS_MOUNTS_ENV = "NEXT_PUBLIC_STARDECK_HEADLESS_MOUNTS";
|
|
4132
4133
|
var MODULE_CONTRIBUTIONS_GEN_PATH = `${APP_WEB_PREFIX}src/module-contributions.gen.ts`;
|
|
4133
4134
|
var MODULE_DATASTORES_GEN_PATH = `${APP_WEB_PREFIX}src/module-datastores.gen.ts`;
|
|
4134
4135
|
var MODULE_I18N_GEN_PATH = `${APP_WEB_PREFIX}src/module-i18n.gen.ts`;
|
|
4135
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`;
|
|
4136
4138
|
var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
|
|
4137
4139
|
var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
|
|
4138
4140
|
|
|
4141
|
+
// ../../packages/lib/src/shared/module-mount-key.ts
|
|
4142
|
+
function mountKey(moduleName, mount) {
|
|
4143
|
+
return `${moduleName}:${mount.group}/${mount.segment}`;
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4139
4146
|
// ../../packages/lib/src/server/module-rail/composition-artifacts.ts
|
|
4140
4147
|
var import_typescript4 = __toESM(require("typescript"), 1);
|
|
4141
4148
|
|
|
@@ -4144,7 +4151,8 @@ var COMPOSITION_RUNTIME_GEN_PATHS = [
|
|
|
4144
4151
|
MODULE_CONTRIBUTIONS_GEN_PATH,
|
|
4145
4152
|
MODULE_I18N_GEN_PATH,
|
|
4146
4153
|
MODULE_INIT_SERVER_GEN_PATH,
|
|
4147
|
-
MODULE_DATASTORES_GEN_PATH
|
|
4154
|
+
MODULE_DATASTORES_GEN_PATH,
|
|
4155
|
+
MODULE_SERVER_GEN_PATH
|
|
4148
4156
|
];
|
|
4149
4157
|
function compareNames(a, b) {
|
|
4150
4158
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -4165,6 +4173,9 @@ function formatI18nMarker() {
|
|
|
4165
4173
|
function formatInitMarker() {
|
|
4166
4174
|
return `// ${MODULE_STUB_MARKER} scope=init`;
|
|
4167
4175
|
}
|
|
4176
|
+
function formatServerMarker() {
|
|
4177
|
+
return `// ${MODULE_STUB_MARKER} scope=server`;
|
|
4178
|
+
}
|
|
4168
4179
|
function formatDatastoresMarker() {
|
|
4169
4180
|
return `// ${MODULE_STUB_MARKER} scope=datastores`;
|
|
4170
4181
|
}
|
|
@@ -4464,7 +4475,7 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4464
4475
|
" type SurfaceSlotCatalog,",
|
|
4465
4476
|
'} from "@/lib/modules/contributions";',
|
|
4466
4477
|
'import { substrateSlotCatalogs } from "@/lib/modules/substrate-slots";',
|
|
4467
|
-
'import { generatedModuleRegistry, isModuleEnabled } from "@/modules.gen";',
|
|
4478
|
+
'import { generatedModuleRegistry, isHrefHeadless, isModuleEnabled } from "@/modules.gen";',
|
|
4468
4479
|
...[...moduleImportLines].sort((a, b) => {
|
|
4469
4480
|
const pathA = a.match(/from ("[^"]+")/)?.[1] ?? a;
|
|
4470
4481
|
const pathB = b.match(/from ("[^"]+")/)?.[1] ?? b;
|
|
@@ -4514,11 +4525,19 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4514
4525
|
"export function resolveModuleContributions(",
|
|
4515
4526
|
" registry: typeof generatedModuleRegistry = generatedModuleRegistry",
|
|
4516
4527
|
"): ResolvedModuleContributions {",
|
|
4517
|
-
"
|
|
4528
|
+
" const resolved = resolveContributions({",
|
|
4518
4529
|
" registry,",
|
|
4519
4530
|
" catalogs: moduleSlotCatalogs,",
|
|
4520
4531
|
" sources: moduleContributionSources,",
|
|
4521
4532
|
" });",
|
|
4533
|
+
" // A nav entry into a headless mount would link to a page that is switched off.",
|
|
4534
|
+
' const nav = resolved["admin.nav"];',
|
|
4535
|
+
" if (nav) {",
|
|
4536
|
+
' resolved["admin.nav"] = nav.filter(',
|
|
4537
|
+
" (entry) => !isHrefHeadless((entry as { href?: unknown }).href)",
|
|
4538
|
+
" );",
|
|
4539
|
+
" }",
|
|
4540
|
+
" return resolved;",
|
|
4522
4541
|
"}",
|
|
4523
4542
|
"",
|
|
4524
4543
|
"export const resolvedModuleContributions: ResolvedModuleContributions =",
|
|
@@ -4800,6 +4819,42 @@ function emitModuleInitServerGenTs(input) {
|
|
|
4800
4819
|
""
|
|
4801
4820
|
].join("\n");
|
|
4802
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
|
+
}
|
|
4803
4858
|
|
|
4804
4859
|
// ../../packages/lib/src/server/module-rail/composition-artifacts.ts
|
|
4805
4860
|
var COMPOSITION_ARTIFACTS_GENERATED_BY = "module-rail";
|
|
@@ -4862,6 +4917,14 @@ function formatRouteMarker(owner, nextRelativePath) {
|
|
|
4862
4917
|
function formatRegistryMarker() {
|
|
4863
4918
|
return `// ${MODULE_STUB_MARKER} scope=registry`;
|
|
4864
4919
|
}
|
|
4920
|
+
var OWNERLESS_MARKER_KINDS = [
|
|
4921
|
+
"registry",
|
|
4922
|
+
"contributions",
|
|
4923
|
+
"datastores",
|
|
4924
|
+
"i18n",
|
|
4925
|
+
"init",
|
|
4926
|
+
"server"
|
|
4927
|
+
];
|
|
4865
4928
|
function parseCompositionArtifactMarker(source) {
|
|
4866
4929
|
const firstLine = source.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
4867
4930
|
const match = firstLine.match(
|
|
@@ -4872,26 +4935,8 @@ function parseCompositionArtifactMarker(source) {
|
|
|
4872
4935
|
if (!match) return null;
|
|
4873
4936
|
const owner = match[1];
|
|
4874
4937
|
const scope = match[2];
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
return { kind: "registry" };
|
|
4878
|
-
}
|
|
4879
|
-
if (scope === "contributions") {
|
|
4880
|
-
if (owner) return null;
|
|
4881
|
-
return { kind: "contributions" };
|
|
4882
|
-
}
|
|
4883
|
-
if (scope === "datastores") {
|
|
4884
|
-
if (owner) return null;
|
|
4885
|
-
return { kind: "datastores" };
|
|
4886
|
-
}
|
|
4887
|
-
if (scope === "i18n") {
|
|
4888
|
-
if (owner) return null;
|
|
4889
|
-
return { kind: "i18n" };
|
|
4890
|
-
}
|
|
4891
|
-
if (scope === "init") {
|
|
4892
|
-
if (owner) return null;
|
|
4893
|
-
return { kind: "init" };
|
|
4894
|
-
}
|
|
4938
|
+
const ownerless = OWNERLESS_MARKER_KINDS.find((kind) => kind === scope);
|
|
4939
|
+
if (ownerless) return owner ? null : { kind: ownerless };
|
|
4895
4940
|
if (!owner || !isValidModuleName(owner)) return null;
|
|
4896
4941
|
if (scope === "route") {
|
|
4897
4942
|
const nextRelativePath = match[3]?.trim();
|
|
@@ -5017,6 +5062,7 @@ function expectedMarkerKindForPath(path3) {
|
|
|
5017
5062
|
if (path3 === MODULE_DATASTORES_GEN_PATH) return "datastores";
|
|
5018
5063
|
if (path3 === MODULE_I18N_GEN_PATH) return "i18n";
|
|
5019
5064
|
if (path3 === MODULE_INIT_SERVER_GEN_PATH) return "init";
|
|
5065
|
+
if (path3 === MODULE_SERVER_GEN_PATH) return "server";
|
|
5020
5066
|
if (path3.startsWith(API_ROUTE_DIR_PREFIX) && path3.endsWith("/route.ts")) return "endpoint";
|
|
5021
5067
|
if (path3.startsWith(APP_DIR_PREFIX) && !path3.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path3.endsWith(suffix))) {
|
|
5022
5068
|
return "route";
|
|
@@ -5104,6 +5150,25 @@ function emitModulesGenTs(registry) {
|
|
|
5104
5150
|
' if (entry && entry.kind !== "feature") return true;',
|
|
5105
5151
|
' return raw.split(",").includes(name);',
|
|
5106
5152
|
"}",
|
|
5153
|
+
"",
|
|
5154
|
+
"/** Headless mount keys come from the platform at build time: that page is off, its Module stays on. */",
|
|
5155
|
+
"export function isMountHeadless(key: string): boolean {",
|
|
5156
|
+
` const raw = process.env.${HEADLESS_MOUNTS_ENV};`,
|
|
5157
|
+
' return raw ? raw.split(",").includes(key) : false;',
|
|
5158
|
+
"}",
|
|
5159
|
+
"",
|
|
5160
|
+
"/** True when `href` is a headless mount's URL or below it. */",
|
|
5161
|
+
"export function isHrefHeadless(href: unknown): boolean {",
|
|
5162
|
+
' if (typeof href !== "string") return false;',
|
|
5163
|
+
" const target = href.split(/[?#]/)[0];",
|
|
5164
|
+
" return generatedModuleRegistry.modules.some((mod) =>",
|
|
5165
|
+
" mod.mounts.some((mount) => {",
|
|
5166
|
+
" if (!isMountHeadless(`${mod.name}:${mount.group}/${mount.segment}`)) return false;",
|
|
5167
|
+
" const path = `/${mount.segment}`;",
|
|
5168
|
+
" return target === path || target.startsWith(`${path}/`);",
|
|
5169
|
+
" })",
|
|
5170
|
+
" );",
|
|
5171
|
+
"}",
|
|
5107
5172
|
""
|
|
5108
5173
|
].join("\n");
|
|
5109
5174
|
}
|
|
@@ -5111,6 +5176,22 @@ var STUB_PRINT_WIDTH = 100;
|
|
|
5111
5176
|
function wrapAtPrintWidth(flat, broken) {
|
|
5112
5177
|
return flat.length > STUB_PRINT_WIDTH ? broken : [flat];
|
|
5113
5178
|
}
|
|
5179
|
+
function emitGuardLines(guards, body) {
|
|
5180
|
+
const operands = guards.map((guard) => `${guard.call}(${emitTsString(guard.arg)})`);
|
|
5181
|
+
const condition = ` if (${operands.join(" || ")})`;
|
|
5182
|
+
const brokenOperands = guards.flatMap((guard, i) => {
|
|
5183
|
+
const suffix = i < guards.length - 1 ? " ||" : "";
|
|
5184
|
+
return wrapAtPrintWidth(` ${operands[i]}${suffix}`, [
|
|
5185
|
+
` ${guard.call}(`,
|
|
5186
|
+
` ${emitTsString(guard.arg)}`,
|
|
5187
|
+
` )${suffix}`
|
|
5188
|
+
]);
|
|
5189
|
+
});
|
|
5190
|
+
return wrapAtPrintWidth(`${condition} ${body}`, [
|
|
5191
|
+
...wrapAtPrintWidth(condition, [" if (", ...brokenOperands, " )"]),
|
|
5192
|
+
` ${body}`
|
|
5193
|
+
]);
|
|
5194
|
+
}
|
|
5114
5195
|
function buildEndpointStubContent(input) {
|
|
5115
5196
|
const sorted = [...input.methods].sort(
|
|
5116
5197
|
(a, b) => METHOD_ORDER[a.method] - METHOD_ORDER[b.method] || a.pairKey.localeCompare(b.pairKey)
|
|
@@ -5118,24 +5199,15 @@ function buildEndpointStubContent(input) {
|
|
|
5118
5199
|
const exports2 = sorted.map((m) => {
|
|
5119
5200
|
const pair = emitTsString(m.pairKey);
|
|
5120
5201
|
const guards = [
|
|
5121
|
-
|
|
5122
|
-
...input.scope.kind === "enhancement" ? [
|
|
5202
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5203
|
+
...input.scope.kind === "enhancement" ? [{ call: "!isModuleEnabled", arg: input.scope.peer }] : []
|
|
5123
5204
|
];
|
|
5124
|
-
const condition = ` if (${guards.join(" || ")})`;
|
|
5125
|
-
const notFoundBody = "return new Response(null, { status: 404 });";
|
|
5126
5205
|
const message = emitTsString(`Missing endpoint handler for ${m.pairKey}`);
|
|
5127
5206
|
const importPath = emitTsString(input.importModule);
|
|
5128
5207
|
return [
|
|
5129
5208
|
`export async function ${m.method}(request: Request, context?: unknown): Promise<Response> {`,
|
|
5130
5209
|
" await initializeActiveEnhancements();",
|
|
5131
|
-
...
|
|
5132
|
-
...wrapAtPrintWidth(condition, [
|
|
5133
|
-
" if (",
|
|
5134
|
-
...guards.map((guard, i) => ` ${guard}${i < guards.length - 1 ? " ||" : ""}`),
|
|
5135
|
-
" )"
|
|
5136
|
-
]),
|
|
5137
|
-
` ${notFoundBody}`
|
|
5138
|
-
]),
|
|
5210
|
+
...emitGuardLines(guards, "return new Response(null, { status: 404 });"),
|
|
5139
5211
|
...wrapAtPrintWidth(` const { endpointHandlers } = await import(${importPath});`, [
|
|
5140
5212
|
" const { endpointHandlers } = await import(",
|
|
5141
5213
|
` ${importPath}`,
|
|
@@ -5165,12 +5237,16 @@ function buildEndpointStubContent(input) {
|
|
|
5165
5237
|
].join("\n");
|
|
5166
5238
|
}
|
|
5167
5239
|
function buildRouteStubContent(input) {
|
|
5240
|
+
const guards = [
|
|
5241
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5242
|
+
{ call: "isMountHeadless", arg: input.mountKey }
|
|
5243
|
+
];
|
|
5168
5244
|
const lines = [
|
|
5169
5245
|
formatRouteMarker(input.owner, input.nextRelativePath),
|
|
5170
5246
|
'import { notFound } from "next/navigation";',
|
|
5171
5247
|
// simple-import-sort puts packages and "@/" aliases in separate groups.
|
|
5172
5248
|
"",
|
|
5173
|
-
'import { isModuleEnabled } from "@/modules.gen";',
|
|
5249
|
+
'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
|
|
5174
5250
|
`import Page from ${emitTsString(input.importModule)};`
|
|
5175
5251
|
];
|
|
5176
5252
|
if (input.hasGenerateMetadata) {
|
|
@@ -5178,7 +5254,7 @@ function buildRouteStubContent(input) {
|
|
|
5178
5254
|
`import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
|
|
5179
5255
|
"",
|
|
5180
5256
|
"export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
|
|
5181
|
-
|
|
5257
|
+
...emitGuardLines(guards, "return {};"),
|
|
5182
5258
|
" return sourceGenerateMetadata(...args);",
|
|
5183
5259
|
"}"
|
|
5184
5260
|
);
|
|
@@ -5189,7 +5265,7 @@ function buildRouteStubContent(input) {
|
|
|
5189
5265
|
" Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
|
|
5190
5266
|
"",
|
|
5191
5267
|
"export default function ModulePage(props: PageProps) {",
|
|
5192
|
-
|
|
5268
|
+
...emitGuardLines(guards, "notFound();"),
|
|
5193
5269
|
" return <Page {...props} />;",
|
|
5194
5270
|
"}",
|
|
5195
5271
|
""
|
|
@@ -5391,7 +5467,6 @@ function planRouteStubs(input) {
|
|
|
5391
5467
|
}
|
|
5392
5468
|
for (const entry of entries) {
|
|
5393
5469
|
const nextRelativePath = entry.nextRelativePath;
|
|
5394
|
-
analyzeRouteEntryPath(nextRelativePath);
|
|
5395
5470
|
const destination = normalizeAppRouterDestinationPattern(nextRelativePath);
|
|
5396
5471
|
const path3 = routeEntryToAppPageFile(nextRelativePath);
|
|
5397
5472
|
const existing = routeDestinationOwners.get(destination);
|
|
@@ -5406,6 +5481,8 @@ function planRouteStubs(input) {
|
|
|
5406
5481
|
path: path3
|
|
5407
5482
|
});
|
|
5408
5483
|
const importModule = routeEntryImport(regMod.name, nextRelativePath);
|
|
5484
|
+
const analyzed = analyzeRouteEntryPath(nextRelativePath);
|
|
5485
|
+
const mount = mounts.find((claim) => routeEntryCoveredByMountClaim(analyzed, claim));
|
|
5409
5486
|
routeStubs.push({
|
|
5410
5487
|
path: path3,
|
|
5411
5488
|
owner: regMod.name,
|
|
@@ -5415,6 +5492,7 @@ function planRouteStubs(input) {
|
|
|
5415
5492
|
owner: regMod.name,
|
|
5416
5493
|
nextRelativePath,
|
|
5417
5494
|
importModule,
|
|
5495
|
+
mountKey: mountKey(regMod.name, mount),
|
|
5418
5496
|
hasGenerateMetadata: entry.hasGenerateMetadata
|
|
5419
5497
|
})
|
|
5420
5498
|
});
|
|
@@ -5584,6 +5662,7 @@ function planCompositionArtifacts(input) {
|
|
|
5584
5662
|
let datastoresContent;
|
|
5585
5663
|
let i18nContent;
|
|
5586
5664
|
let initContent;
|
|
5665
|
+
let serverContent;
|
|
5587
5666
|
try {
|
|
5588
5667
|
contributionsContent = emitModuleContributionsGenTs({
|
|
5589
5668
|
registry: input.registry,
|
|
@@ -5608,6 +5687,10 @@ function planCompositionArtifacts(input) {
|
|
|
5608
5687
|
registry: input.registry,
|
|
5609
5688
|
facts: compositionEntryFacts
|
|
5610
5689
|
});
|
|
5690
|
+
serverContent = emitModuleServerGenTs({
|
|
5691
|
+
registry: input.registry,
|
|
5692
|
+
facts: compositionEntryFacts
|
|
5693
|
+
});
|
|
5611
5694
|
} catch (error) {
|
|
5612
5695
|
return {
|
|
5613
5696
|
ok: false,
|
|
@@ -5620,6 +5703,7 @@ function planCompositionArtifacts(input) {
|
|
|
5620
5703
|
desired.set(MODULE_DATASTORES_GEN_PATH, datastoresContent);
|
|
5621
5704
|
desired.set(MODULE_I18N_GEN_PATH, i18nContent);
|
|
5622
5705
|
desired.set(MODULE_INIT_SERVER_GEN_PATH, initContent);
|
|
5706
|
+
desired.set(MODULE_SERVER_GEN_PATH, serverContent);
|
|
5623
5707
|
for (const stub of stubs.endpointStubs) desired.set(stub.path, stub.content);
|
|
5624
5708
|
for (const stub of stubs.routeStubs) desired.set(stub.path, stub.content);
|
|
5625
5709
|
const endpointStubByPath = new Map(stubs.endpointStubs.map((stub) => [stub.path, stub]));
|
|
@@ -6890,6 +6974,7 @@ function readFailurePath(error) {
|
|
|
6890
6974
|
return typeof error === "object" && error !== null && "path" in error ? String(error.path) : "unknown";
|
|
6891
6975
|
}
|
|
6892
6976
|
var CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN = "conventional-entry-kind";
|
|
6977
|
+
var SERVER_REGISTRATIONS_FILE = "server/registrations.ts";
|
|
6893
6978
|
var COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN = "composition-artifact-kind";
|
|
6894
6979
|
async function classifyCompositionArtifactPathKind(context, path3) {
|
|
6895
6980
|
return classifySandboxPathPresenceKind(context, path3, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
|
|
@@ -6927,6 +7012,10 @@ async function readConventionalEntryIfRealFile(path3, kind, presences, files) {
|
|
|
6927
7012
|
}
|
|
6928
7013
|
return source;
|
|
6929
7014
|
}
|
|
7015
|
+
function hasRealServerRegistrations(modulePath, presences) {
|
|
7016
|
+
const path3 = `${modulePath}/${SERVER_REGISTRATIONS_FILE}`;
|
|
7017
|
+
return assertRealCompositionArtifactFilePresence(path3, presences.get(path3) ?? "unreadable") === "file";
|
|
7018
|
+
}
|
|
6930
7019
|
async function readJsonObjectFile(path3, files) {
|
|
6931
7020
|
const raw = files.get(path3);
|
|
6932
7021
|
let parsed;
|
|
@@ -6950,6 +7039,9 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6950
7039
|
const overridesDir = `${APP_PACKAGE_DIR}/src/lib/i18n/overrides`;
|
|
6951
7040
|
const localeDirs = [substrateDir, overridesDir];
|
|
6952
7041
|
const conventionalPaths = [];
|
|
7042
|
+
const serverRegistrationPaths = registry.modules.map(
|
|
7043
|
+
(mod) => `${MODULES_SANDBOX_DIR}/${mod.name}/${SERVER_REGISTRATIONS_FILE}`
|
|
7044
|
+
);
|
|
6953
7045
|
for (const mod of registry.modules) {
|
|
6954
7046
|
const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
|
|
6955
7047
|
localeDirs.push(`${modulePath}/i18n`);
|
|
@@ -6978,7 +7070,7 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6978
7070
|
const [conventionalPresences, localePresences] = await Promise.all([
|
|
6979
7071
|
classifySandboxPathPresenceKindMany(
|
|
6980
7072
|
context,
|
|
6981
|
-
conventionalPaths,
|
|
7073
|
+
[...conventionalPaths, ...serverRegistrationPaths],
|
|
6982
7074
|
CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN
|
|
6983
7075
|
),
|
|
6984
7076
|
classifySandboxPathPresenceKindMany(context, localePaths, LOCALE_JSON_KIND_PROBE_TOKEN)
|
|
@@ -7040,6 +7132,7 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7040
7132
|
);
|
|
7041
7133
|
}
|
|
7042
7134
|
}
|
|
7135
|
+
const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
|
|
7043
7136
|
const rootLocales = localesByDir.get(`${modulePath}/i18n`);
|
|
7044
7137
|
if (rootLocales.length > 0 && !rootLocales.includes("en")) {
|
|
7045
7138
|
throw new Error(
|
|
@@ -7079,6 +7172,7 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7079
7172
|
moduleName: mod.name,
|
|
7080
7173
|
hasRootContributions,
|
|
7081
7174
|
hasRootSlots,
|
|
7175
|
+
hasServerRegistrations,
|
|
7082
7176
|
rootLocales,
|
|
7083
7177
|
enhancements
|
|
7084
7178
|
});
|
|
@@ -7282,6 +7376,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
7282
7376
|
MODULE_DATASTORES_GEN_PATH,
|
|
7283
7377
|
MODULE_I18N_GEN_PATH,
|
|
7284
7378
|
MODULE_INIT_SERVER_GEN_PATH,
|
|
7379
|
+
MODULE_SERVER_GEN_PATH,
|
|
7285
7380
|
...existingGeneratedPaths,
|
|
7286
7381
|
...Object.keys(existingAppPages),
|
|
7287
7382
|
...Object.keys(existingAppRoutes)
|
package/dist/index.mjs
CHANGED
|
@@ -4114,13 +4114,20 @@ function isModuleEnabledByList(raw, name, kindOf) {
|
|
|
4114
4114
|
var APP_WEB_PREFIX = "apps/web/";
|
|
4115
4115
|
var MODULE_STUB_MARKER = "@stardeck-module-rail-generated";
|
|
4116
4116
|
var ENABLED_MODULES_ENV = "NEXT_PUBLIC_STARDECK_ENABLED_MODULES";
|
|
4117
|
+
var HEADLESS_MOUNTS_ENV = "NEXT_PUBLIC_STARDECK_HEADLESS_MOUNTS";
|
|
4117
4118
|
var MODULE_CONTRIBUTIONS_GEN_PATH = `${APP_WEB_PREFIX}src/module-contributions.gen.ts`;
|
|
4118
4119
|
var MODULE_DATASTORES_GEN_PATH = `${APP_WEB_PREFIX}src/module-datastores.gen.ts`;
|
|
4119
4120
|
var MODULE_I18N_GEN_PATH = `${APP_WEB_PREFIX}src/module-i18n.gen.ts`;
|
|
4120
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`;
|
|
4121
4123
|
var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
|
|
4122
4124
|
var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
|
|
4123
4125
|
|
|
4126
|
+
// ../../packages/lib/src/shared/module-mount-key.ts
|
|
4127
|
+
function mountKey(moduleName, mount) {
|
|
4128
|
+
return `${moduleName}:${mount.group}/${mount.segment}`;
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4124
4131
|
// ../../packages/lib/src/server/module-rail/composition-artifacts.ts
|
|
4125
4132
|
import ts4 from "typescript";
|
|
4126
4133
|
|
|
@@ -4129,7 +4136,8 @@ var COMPOSITION_RUNTIME_GEN_PATHS = [
|
|
|
4129
4136
|
MODULE_CONTRIBUTIONS_GEN_PATH,
|
|
4130
4137
|
MODULE_I18N_GEN_PATH,
|
|
4131
4138
|
MODULE_INIT_SERVER_GEN_PATH,
|
|
4132
|
-
MODULE_DATASTORES_GEN_PATH
|
|
4139
|
+
MODULE_DATASTORES_GEN_PATH,
|
|
4140
|
+
MODULE_SERVER_GEN_PATH
|
|
4133
4141
|
];
|
|
4134
4142
|
function compareNames(a, b) {
|
|
4135
4143
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -4150,6 +4158,9 @@ function formatI18nMarker() {
|
|
|
4150
4158
|
function formatInitMarker() {
|
|
4151
4159
|
return `// ${MODULE_STUB_MARKER} scope=init`;
|
|
4152
4160
|
}
|
|
4161
|
+
function formatServerMarker() {
|
|
4162
|
+
return `// ${MODULE_STUB_MARKER} scope=server`;
|
|
4163
|
+
}
|
|
4153
4164
|
function formatDatastoresMarker() {
|
|
4154
4165
|
return `// ${MODULE_STUB_MARKER} scope=datastores`;
|
|
4155
4166
|
}
|
|
@@ -4449,7 +4460,7 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4449
4460
|
" type SurfaceSlotCatalog,",
|
|
4450
4461
|
'} from "@/lib/modules/contributions";',
|
|
4451
4462
|
'import { substrateSlotCatalogs } from "@/lib/modules/substrate-slots";',
|
|
4452
|
-
'import { generatedModuleRegistry, isModuleEnabled } from "@/modules.gen";',
|
|
4463
|
+
'import { generatedModuleRegistry, isHrefHeadless, isModuleEnabled } from "@/modules.gen";',
|
|
4453
4464
|
...[...moduleImportLines].sort((a, b) => {
|
|
4454
4465
|
const pathA = a.match(/from ("[^"]+")/)?.[1] ?? a;
|
|
4455
4466
|
const pathB = b.match(/from ("[^"]+")/)?.[1] ?? b;
|
|
@@ -4499,11 +4510,19 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4499
4510
|
"export function resolveModuleContributions(",
|
|
4500
4511
|
" registry: typeof generatedModuleRegistry = generatedModuleRegistry",
|
|
4501
4512
|
"): ResolvedModuleContributions {",
|
|
4502
|
-
"
|
|
4513
|
+
" const resolved = resolveContributions({",
|
|
4503
4514
|
" registry,",
|
|
4504
4515
|
" catalogs: moduleSlotCatalogs,",
|
|
4505
4516
|
" sources: moduleContributionSources,",
|
|
4506
4517
|
" });",
|
|
4518
|
+
" // A nav entry into a headless mount would link to a page that is switched off.",
|
|
4519
|
+
' const nav = resolved["admin.nav"];',
|
|
4520
|
+
" if (nav) {",
|
|
4521
|
+
' resolved["admin.nav"] = nav.filter(',
|
|
4522
|
+
" (entry) => !isHrefHeadless((entry as { href?: unknown }).href)",
|
|
4523
|
+
" );",
|
|
4524
|
+
" }",
|
|
4525
|
+
" return resolved;",
|
|
4507
4526
|
"}",
|
|
4508
4527
|
"",
|
|
4509
4528
|
"export const resolvedModuleContributions: ResolvedModuleContributions =",
|
|
@@ -4785,6 +4804,42 @@ function emitModuleInitServerGenTs(input) {
|
|
|
4785
4804
|
""
|
|
4786
4805
|
].join("\n");
|
|
4787
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
|
+
}
|
|
4788
4843
|
|
|
4789
4844
|
// ../../packages/lib/src/server/module-rail/composition-artifacts.ts
|
|
4790
4845
|
var COMPOSITION_ARTIFACTS_GENERATED_BY = "module-rail";
|
|
@@ -4847,6 +4902,14 @@ function formatRouteMarker(owner, nextRelativePath) {
|
|
|
4847
4902
|
function formatRegistryMarker() {
|
|
4848
4903
|
return `// ${MODULE_STUB_MARKER} scope=registry`;
|
|
4849
4904
|
}
|
|
4905
|
+
var OWNERLESS_MARKER_KINDS = [
|
|
4906
|
+
"registry",
|
|
4907
|
+
"contributions",
|
|
4908
|
+
"datastores",
|
|
4909
|
+
"i18n",
|
|
4910
|
+
"init",
|
|
4911
|
+
"server"
|
|
4912
|
+
];
|
|
4850
4913
|
function parseCompositionArtifactMarker(source) {
|
|
4851
4914
|
const firstLine = source.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
4852
4915
|
const match = firstLine.match(
|
|
@@ -4857,26 +4920,8 @@ function parseCompositionArtifactMarker(source) {
|
|
|
4857
4920
|
if (!match) return null;
|
|
4858
4921
|
const owner = match[1];
|
|
4859
4922
|
const scope = match[2];
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
return { kind: "registry" };
|
|
4863
|
-
}
|
|
4864
|
-
if (scope === "contributions") {
|
|
4865
|
-
if (owner) return null;
|
|
4866
|
-
return { kind: "contributions" };
|
|
4867
|
-
}
|
|
4868
|
-
if (scope === "datastores") {
|
|
4869
|
-
if (owner) return null;
|
|
4870
|
-
return { kind: "datastores" };
|
|
4871
|
-
}
|
|
4872
|
-
if (scope === "i18n") {
|
|
4873
|
-
if (owner) return null;
|
|
4874
|
-
return { kind: "i18n" };
|
|
4875
|
-
}
|
|
4876
|
-
if (scope === "init") {
|
|
4877
|
-
if (owner) return null;
|
|
4878
|
-
return { kind: "init" };
|
|
4879
|
-
}
|
|
4923
|
+
const ownerless = OWNERLESS_MARKER_KINDS.find((kind) => kind === scope);
|
|
4924
|
+
if (ownerless) return owner ? null : { kind: ownerless };
|
|
4880
4925
|
if (!owner || !isValidModuleName(owner)) return null;
|
|
4881
4926
|
if (scope === "route") {
|
|
4882
4927
|
const nextRelativePath = match[3]?.trim();
|
|
@@ -5002,6 +5047,7 @@ function expectedMarkerKindForPath(path3) {
|
|
|
5002
5047
|
if (path3 === MODULE_DATASTORES_GEN_PATH) return "datastores";
|
|
5003
5048
|
if (path3 === MODULE_I18N_GEN_PATH) return "i18n";
|
|
5004
5049
|
if (path3 === MODULE_INIT_SERVER_GEN_PATH) return "init";
|
|
5050
|
+
if (path3 === MODULE_SERVER_GEN_PATH) return "server";
|
|
5005
5051
|
if (path3.startsWith(API_ROUTE_DIR_PREFIX) && path3.endsWith("/route.ts")) return "endpoint";
|
|
5006
5052
|
if (path3.startsWith(APP_DIR_PREFIX) && !path3.startsWith(API_ROUTE_DIR_PREFIX) && PAGE_FILE_SUFFIXES.some((suffix) => path3.endsWith(suffix))) {
|
|
5007
5053
|
return "route";
|
|
@@ -5089,6 +5135,25 @@ function emitModulesGenTs(registry) {
|
|
|
5089
5135
|
' if (entry && entry.kind !== "feature") return true;',
|
|
5090
5136
|
' return raw.split(",").includes(name);',
|
|
5091
5137
|
"}",
|
|
5138
|
+
"",
|
|
5139
|
+
"/** Headless mount keys come from the platform at build time: that page is off, its Module stays on. */",
|
|
5140
|
+
"export function isMountHeadless(key: string): boolean {",
|
|
5141
|
+
` const raw = process.env.${HEADLESS_MOUNTS_ENV};`,
|
|
5142
|
+
' return raw ? raw.split(",").includes(key) : false;',
|
|
5143
|
+
"}",
|
|
5144
|
+
"",
|
|
5145
|
+
"/** True when `href` is a headless mount's URL or below it. */",
|
|
5146
|
+
"export function isHrefHeadless(href: unknown): boolean {",
|
|
5147
|
+
' if (typeof href !== "string") return false;',
|
|
5148
|
+
" const target = href.split(/[?#]/)[0];",
|
|
5149
|
+
" return generatedModuleRegistry.modules.some((mod) =>",
|
|
5150
|
+
" mod.mounts.some((mount) => {",
|
|
5151
|
+
" if (!isMountHeadless(`${mod.name}:${mount.group}/${mount.segment}`)) return false;",
|
|
5152
|
+
" const path = `/${mount.segment}`;",
|
|
5153
|
+
" return target === path || target.startsWith(`${path}/`);",
|
|
5154
|
+
" })",
|
|
5155
|
+
" );",
|
|
5156
|
+
"}",
|
|
5092
5157
|
""
|
|
5093
5158
|
].join("\n");
|
|
5094
5159
|
}
|
|
@@ -5096,6 +5161,22 @@ var STUB_PRINT_WIDTH = 100;
|
|
|
5096
5161
|
function wrapAtPrintWidth(flat, broken) {
|
|
5097
5162
|
return flat.length > STUB_PRINT_WIDTH ? broken : [flat];
|
|
5098
5163
|
}
|
|
5164
|
+
function emitGuardLines(guards, body) {
|
|
5165
|
+
const operands = guards.map((guard) => `${guard.call}(${emitTsString(guard.arg)})`);
|
|
5166
|
+
const condition = ` if (${operands.join(" || ")})`;
|
|
5167
|
+
const brokenOperands = guards.flatMap((guard, i) => {
|
|
5168
|
+
const suffix = i < guards.length - 1 ? " ||" : "";
|
|
5169
|
+
return wrapAtPrintWidth(` ${operands[i]}${suffix}`, [
|
|
5170
|
+
` ${guard.call}(`,
|
|
5171
|
+
` ${emitTsString(guard.arg)}`,
|
|
5172
|
+
` )${suffix}`
|
|
5173
|
+
]);
|
|
5174
|
+
});
|
|
5175
|
+
return wrapAtPrintWidth(`${condition} ${body}`, [
|
|
5176
|
+
...wrapAtPrintWidth(condition, [" if (", ...brokenOperands, " )"]),
|
|
5177
|
+
` ${body}`
|
|
5178
|
+
]);
|
|
5179
|
+
}
|
|
5099
5180
|
function buildEndpointStubContent(input) {
|
|
5100
5181
|
const sorted = [...input.methods].sort(
|
|
5101
5182
|
(a, b) => METHOD_ORDER[a.method] - METHOD_ORDER[b.method] || a.pairKey.localeCompare(b.pairKey)
|
|
@@ -5103,24 +5184,15 @@ function buildEndpointStubContent(input) {
|
|
|
5103
5184
|
const exports = sorted.map((m) => {
|
|
5104
5185
|
const pair = emitTsString(m.pairKey);
|
|
5105
5186
|
const guards = [
|
|
5106
|
-
|
|
5107
|
-
...input.scope.kind === "enhancement" ? [
|
|
5187
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5188
|
+
...input.scope.kind === "enhancement" ? [{ call: "!isModuleEnabled", arg: input.scope.peer }] : []
|
|
5108
5189
|
];
|
|
5109
|
-
const condition = ` if (${guards.join(" || ")})`;
|
|
5110
|
-
const notFoundBody = "return new Response(null, { status: 404 });";
|
|
5111
5190
|
const message = emitTsString(`Missing endpoint handler for ${m.pairKey}`);
|
|
5112
5191
|
const importPath = emitTsString(input.importModule);
|
|
5113
5192
|
return [
|
|
5114
5193
|
`export async function ${m.method}(request: Request, context?: unknown): Promise<Response> {`,
|
|
5115
5194
|
" await initializeActiveEnhancements();",
|
|
5116
|
-
...
|
|
5117
|
-
...wrapAtPrintWidth(condition, [
|
|
5118
|
-
" if (",
|
|
5119
|
-
...guards.map((guard, i) => ` ${guard}${i < guards.length - 1 ? " ||" : ""}`),
|
|
5120
|
-
" )"
|
|
5121
|
-
]),
|
|
5122
|
-
` ${notFoundBody}`
|
|
5123
|
-
]),
|
|
5195
|
+
...emitGuardLines(guards, "return new Response(null, { status: 404 });"),
|
|
5124
5196
|
...wrapAtPrintWidth(` const { endpointHandlers } = await import(${importPath});`, [
|
|
5125
5197
|
" const { endpointHandlers } = await import(",
|
|
5126
5198
|
` ${importPath}`,
|
|
@@ -5150,12 +5222,16 @@ function buildEndpointStubContent(input) {
|
|
|
5150
5222
|
].join("\n");
|
|
5151
5223
|
}
|
|
5152
5224
|
function buildRouteStubContent(input) {
|
|
5225
|
+
const guards = [
|
|
5226
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5227
|
+
{ call: "isMountHeadless", arg: input.mountKey }
|
|
5228
|
+
];
|
|
5153
5229
|
const lines = [
|
|
5154
5230
|
formatRouteMarker(input.owner, input.nextRelativePath),
|
|
5155
5231
|
'import { notFound } from "next/navigation";',
|
|
5156
5232
|
// simple-import-sort puts packages and "@/" aliases in separate groups.
|
|
5157
5233
|
"",
|
|
5158
|
-
'import { isModuleEnabled } from "@/modules.gen";',
|
|
5234
|
+
'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
|
|
5159
5235
|
`import Page from ${emitTsString(input.importModule)};`
|
|
5160
5236
|
];
|
|
5161
5237
|
if (input.hasGenerateMetadata) {
|
|
@@ -5163,7 +5239,7 @@ function buildRouteStubContent(input) {
|
|
|
5163
5239
|
`import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
|
|
5164
5240
|
"",
|
|
5165
5241
|
"export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
|
|
5166
|
-
|
|
5242
|
+
...emitGuardLines(guards, "return {};"),
|
|
5167
5243
|
" return sourceGenerateMetadata(...args);",
|
|
5168
5244
|
"}"
|
|
5169
5245
|
);
|
|
@@ -5174,7 +5250,7 @@ function buildRouteStubContent(input) {
|
|
|
5174
5250
|
" Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
|
|
5175
5251
|
"",
|
|
5176
5252
|
"export default function ModulePage(props: PageProps) {",
|
|
5177
|
-
|
|
5253
|
+
...emitGuardLines(guards, "notFound();"),
|
|
5178
5254
|
" return <Page {...props} />;",
|
|
5179
5255
|
"}",
|
|
5180
5256
|
""
|
|
@@ -5376,7 +5452,6 @@ function planRouteStubs(input) {
|
|
|
5376
5452
|
}
|
|
5377
5453
|
for (const entry of entries) {
|
|
5378
5454
|
const nextRelativePath = entry.nextRelativePath;
|
|
5379
|
-
analyzeRouteEntryPath(nextRelativePath);
|
|
5380
5455
|
const destination = normalizeAppRouterDestinationPattern(nextRelativePath);
|
|
5381
5456
|
const path3 = routeEntryToAppPageFile(nextRelativePath);
|
|
5382
5457
|
const existing = routeDestinationOwners.get(destination);
|
|
@@ -5391,6 +5466,8 @@ function planRouteStubs(input) {
|
|
|
5391
5466
|
path: path3
|
|
5392
5467
|
});
|
|
5393
5468
|
const importModule = routeEntryImport(regMod.name, nextRelativePath);
|
|
5469
|
+
const analyzed = analyzeRouteEntryPath(nextRelativePath);
|
|
5470
|
+
const mount = mounts.find((claim) => routeEntryCoveredByMountClaim(analyzed, claim));
|
|
5394
5471
|
routeStubs.push({
|
|
5395
5472
|
path: path3,
|
|
5396
5473
|
owner: regMod.name,
|
|
@@ -5400,6 +5477,7 @@ function planRouteStubs(input) {
|
|
|
5400
5477
|
owner: regMod.name,
|
|
5401
5478
|
nextRelativePath,
|
|
5402
5479
|
importModule,
|
|
5480
|
+
mountKey: mountKey(regMod.name, mount),
|
|
5403
5481
|
hasGenerateMetadata: entry.hasGenerateMetadata
|
|
5404
5482
|
})
|
|
5405
5483
|
});
|
|
@@ -5569,6 +5647,7 @@ function planCompositionArtifacts(input) {
|
|
|
5569
5647
|
let datastoresContent;
|
|
5570
5648
|
let i18nContent;
|
|
5571
5649
|
let initContent;
|
|
5650
|
+
let serverContent;
|
|
5572
5651
|
try {
|
|
5573
5652
|
contributionsContent = emitModuleContributionsGenTs({
|
|
5574
5653
|
registry: input.registry,
|
|
@@ -5593,6 +5672,10 @@ function planCompositionArtifacts(input) {
|
|
|
5593
5672
|
registry: input.registry,
|
|
5594
5673
|
facts: compositionEntryFacts
|
|
5595
5674
|
});
|
|
5675
|
+
serverContent = emitModuleServerGenTs({
|
|
5676
|
+
registry: input.registry,
|
|
5677
|
+
facts: compositionEntryFacts
|
|
5678
|
+
});
|
|
5596
5679
|
} catch (error) {
|
|
5597
5680
|
return {
|
|
5598
5681
|
ok: false,
|
|
@@ -5605,6 +5688,7 @@ function planCompositionArtifacts(input) {
|
|
|
5605
5688
|
desired.set(MODULE_DATASTORES_GEN_PATH, datastoresContent);
|
|
5606
5689
|
desired.set(MODULE_I18N_GEN_PATH, i18nContent);
|
|
5607
5690
|
desired.set(MODULE_INIT_SERVER_GEN_PATH, initContent);
|
|
5691
|
+
desired.set(MODULE_SERVER_GEN_PATH, serverContent);
|
|
5608
5692
|
for (const stub of stubs.endpointStubs) desired.set(stub.path, stub.content);
|
|
5609
5693
|
for (const stub of stubs.routeStubs) desired.set(stub.path, stub.content);
|
|
5610
5694
|
const endpointStubByPath = new Map(stubs.endpointStubs.map((stub) => [stub.path, stub]));
|
|
@@ -6875,6 +6959,7 @@ function readFailurePath(error) {
|
|
|
6875
6959
|
return typeof error === "object" && error !== null && "path" in error ? String(error.path) : "unknown";
|
|
6876
6960
|
}
|
|
6877
6961
|
var CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN = "conventional-entry-kind";
|
|
6962
|
+
var SERVER_REGISTRATIONS_FILE = "server/registrations.ts";
|
|
6878
6963
|
var COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN = "composition-artifact-kind";
|
|
6879
6964
|
async function classifyCompositionArtifactPathKind(context, path3) {
|
|
6880
6965
|
return classifySandboxPathPresenceKind(context, path3, COMPOSITION_ARTIFACT_KIND_PROBE_TOKEN);
|
|
@@ -6912,6 +6997,10 @@ async function readConventionalEntryIfRealFile(path3, kind, presences, files) {
|
|
|
6912
6997
|
}
|
|
6913
6998
|
return source;
|
|
6914
6999
|
}
|
|
7000
|
+
function hasRealServerRegistrations(modulePath, presences) {
|
|
7001
|
+
const path3 = `${modulePath}/${SERVER_REGISTRATIONS_FILE}`;
|
|
7002
|
+
return assertRealCompositionArtifactFilePresence(path3, presences.get(path3) ?? "unreadable") === "file";
|
|
7003
|
+
}
|
|
6915
7004
|
async function readJsonObjectFile(path3, files) {
|
|
6916
7005
|
const raw = files.get(path3);
|
|
6917
7006
|
let parsed;
|
|
@@ -6935,6 +7024,9 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6935
7024
|
const overridesDir = `${APP_PACKAGE_DIR}/src/lib/i18n/overrides`;
|
|
6936
7025
|
const localeDirs = [substrateDir, overridesDir];
|
|
6937
7026
|
const conventionalPaths = [];
|
|
7027
|
+
const serverRegistrationPaths = registry.modules.map(
|
|
7028
|
+
(mod) => `${MODULES_SANDBOX_DIR}/${mod.name}/${SERVER_REGISTRATIONS_FILE}`
|
|
7029
|
+
);
|
|
6938
7030
|
for (const mod of registry.modules) {
|
|
6939
7031
|
const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
|
|
6940
7032
|
localeDirs.push(`${modulePath}/i18n`);
|
|
@@ -6963,7 +7055,7 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6963
7055
|
const [conventionalPresences, localePresences] = await Promise.all([
|
|
6964
7056
|
classifySandboxPathPresenceKindMany(
|
|
6965
7057
|
context,
|
|
6966
|
-
conventionalPaths,
|
|
7058
|
+
[...conventionalPaths, ...serverRegistrationPaths],
|
|
6967
7059
|
CONVENTIONAL_ENTRY_KIND_PROBE_TOKEN
|
|
6968
7060
|
),
|
|
6969
7061
|
classifySandboxPathPresenceKindMany(context, localePaths, LOCALE_JSON_KIND_PROBE_TOKEN)
|
|
@@ -7025,6 +7117,7 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7025
7117
|
);
|
|
7026
7118
|
}
|
|
7027
7119
|
}
|
|
7120
|
+
const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
|
|
7028
7121
|
const rootLocales = localesByDir.get(`${modulePath}/i18n`);
|
|
7029
7122
|
if (rootLocales.length > 0 && !rootLocales.includes("en")) {
|
|
7030
7123
|
throw new Error(
|
|
@@ -7064,6 +7157,7 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7064
7157
|
moduleName: mod.name,
|
|
7065
7158
|
hasRootContributions,
|
|
7066
7159
|
hasRootSlots,
|
|
7160
|
+
hasServerRegistrations,
|
|
7067
7161
|
rootLocales,
|
|
7068
7162
|
enhancements
|
|
7069
7163
|
});
|
|
@@ -7267,6 +7361,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
7267
7361
|
MODULE_DATASTORES_GEN_PATH,
|
|
7268
7362
|
MODULE_I18N_GEN_PATH,
|
|
7269
7363
|
MODULE_INIT_SERVER_GEN_PATH,
|
|
7364
|
+
MODULE_SERVER_GEN_PATH,
|
|
7270
7365
|
...existingGeneratedPaths,
|
|
7271
7366
|
...Object.keys(existingAppPages),
|
|
7272
7367
|
...Object.keys(existingAppRoutes)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/compose",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Regenerates a Stardeck app's Module-rail artifacts — the
|
|
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",
|