@stardeck-customer-apps/compose 0.6.0 → 0.7.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 +13 -1
- package/dist/cli.js +64 -18
- package/dist/index.js +64 -18
- package/dist/index.mjs +64 -18
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ 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`
|
|
@@ -89,6 +89,18 @@ a silent fallback. It holds deployment facts: gitignore it (`apps/web/.gitignore
|
|
|
89
89
|
locale; the locale also needs a `lib/i18n/substrate/<locale>.json`. New keys
|
|
90
90
|
go in `lib/i18n/substrate/<locale>.json`, never in an override. Needs compose
|
|
91
91
|
0.6.0 or later.
|
|
92
|
+
- **Headless mounts.** `NEXT_PUBLIC_STARDECK_HEADLESS_MOUNTS` (set by the
|
|
93
|
+
platform from the Modules tab) lists mount keys `<module>:<group>/<segment>`
|
|
94
|
+
from `module.json` `mounts`, comma-joined, e.g. `pos:(register)/register`.
|
|
95
|
+
The generated page for a listed mount returns not-found, and `admin.nav`
|
|
96
|
+
entries whose `href` is that mount's path (or below it) are dropped. The rest
|
|
97
|
+
of the Module keeps running: its other mounts, every endpoint, `server/`,
|
|
98
|
+
`client/`, initializers and other contributions. Build your own screen in
|
|
99
|
+
app code on the Module's `server/` entry and `client/` hooks, at a different
|
|
100
|
+
URL: the generated page still exists, so an app page at the same URL fails
|
|
101
|
+
compose as a route collision.
|
|
102
|
+
`isMountHeadless(key)` and `isHrefHeadless(href)` are exported from
|
|
103
|
+
`@/modules.gen`. Needs compose 0.7.0 or later.
|
|
92
104
|
- `@stardeck-customer-apps/compose/runtime` exports `isModuleEnabledByList`,
|
|
93
105
|
the single enablement rule, and imports nothing — it is safe in client
|
|
94
106
|
components.
|
package/dist/cli.js
CHANGED
|
@@ -3891,6 +3891,7 @@ 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`;
|
|
@@ -4127,6 +4128,11 @@ function isModuleEnabledByList(raw, name, kindOf) {
|
|
|
4127
4128
|
return raw.split(",").includes(name);
|
|
4128
4129
|
}
|
|
4129
4130
|
|
|
4131
|
+
// ../../packages/lib/src/shared/module-mount-key.ts
|
|
4132
|
+
function mountKey(moduleName, mount) {
|
|
4133
|
+
return `${moduleName}:${mount.group}/${mount.segment}`;
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4130
4136
|
// ../../packages/lib/src/server/module-rail/composition-artifacts.ts
|
|
4131
4137
|
var import_typescript4 = __toESM(require("typescript"), 1);
|
|
4132
4138
|
|
|
@@ -4455,7 +4461,7 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4455
4461
|
" type SurfaceSlotCatalog,",
|
|
4456
4462
|
'} from "@/lib/modules/contributions";',
|
|
4457
4463
|
'import { substrateSlotCatalogs } from "@/lib/modules/substrate-slots";',
|
|
4458
|
-
'import { generatedModuleRegistry, isModuleEnabled } from "@/modules.gen";',
|
|
4464
|
+
'import { generatedModuleRegistry, isHrefHeadless, isModuleEnabled } from "@/modules.gen";',
|
|
4459
4465
|
...[...moduleImportLines].sort((a, b) => {
|
|
4460
4466
|
const pathA = a.match(/from ("[^"]+")/)?.[1] ?? a;
|
|
4461
4467
|
const pathB = b.match(/from ("[^"]+")/)?.[1] ?? b;
|
|
@@ -4505,11 +4511,19 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4505
4511
|
"export function resolveModuleContributions(",
|
|
4506
4512
|
" registry: typeof generatedModuleRegistry = generatedModuleRegistry",
|
|
4507
4513
|
"): ResolvedModuleContributions {",
|
|
4508
|
-
"
|
|
4514
|
+
" const resolved = resolveContributions({",
|
|
4509
4515
|
" registry,",
|
|
4510
4516
|
" catalogs: moduleSlotCatalogs,",
|
|
4511
4517
|
" sources: moduleContributionSources,",
|
|
4512
4518
|
" });",
|
|
4519
|
+
" // A nav entry into a headless mount would link to a page that is switched off.",
|
|
4520
|
+
' const nav = resolved["admin.nav"];',
|
|
4521
|
+
" if (nav) {",
|
|
4522
|
+
' resolved["admin.nav"] = nav.filter(',
|
|
4523
|
+
" (entry) => !isHrefHeadless((entry as { href?: unknown }).href)",
|
|
4524
|
+
" );",
|
|
4525
|
+
" }",
|
|
4526
|
+
" return resolved;",
|
|
4513
4527
|
"}",
|
|
4514
4528
|
"",
|
|
4515
4529
|
"export const resolvedModuleContributions: ResolvedModuleContributions =",
|
|
@@ -5095,6 +5109,25 @@ function emitModulesGenTs(registry) {
|
|
|
5095
5109
|
' if (entry && entry.kind !== "feature") return true;',
|
|
5096
5110
|
' return raw.split(",").includes(name);',
|
|
5097
5111
|
"}",
|
|
5112
|
+
"",
|
|
5113
|
+
"/** Headless mount keys come from the platform at build time: that page is off, its Module stays on. */",
|
|
5114
|
+
"export function isMountHeadless(key: string): boolean {",
|
|
5115
|
+
` const raw = process.env.${HEADLESS_MOUNTS_ENV};`,
|
|
5116
|
+
' return raw ? raw.split(",").includes(key) : false;',
|
|
5117
|
+
"}",
|
|
5118
|
+
"",
|
|
5119
|
+
"/** True when `href` is a headless mount's URL or below it. */",
|
|
5120
|
+
"export function isHrefHeadless(href: unknown): boolean {",
|
|
5121
|
+
' if (typeof href !== "string") return false;',
|
|
5122
|
+
" const target = href.split(/[?#]/)[0];",
|
|
5123
|
+
" return generatedModuleRegistry.modules.some((mod) =>",
|
|
5124
|
+
" mod.mounts.some((mount) => {",
|
|
5125
|
+
" if (!isMountHeadless(`${mod.name}:${mount.group}/${mount.segment}`)) return false;",
|
|
5126
|
+
" const path = `/${mount.segment}`;",
|
|
5127
|
+
" return target === path || target.startsWith(`${path}/`);",
|
|
5128
|
+
" })",
|
|
5129
|
+
" );",
|
|
5130
|
+
"}",
|
|
5098
5131
|
""
|
|
5099
5132
|
].join("\n");
|
|
5100
5133
|
}
|
|
@@ -5102,6 +5135,22 @@ var STUB_PRINT_WIDTH = 100;
|
|
|
5102
5135
|
function wrapAtPrintWidth(flat, broken) {
|
|
5103
5136
|
return flat.length > STUB_PRINT_WIDTH ? broken : [flat];
|
|
5104
5137
|
}
|
|
5138
|
+
function emitGuardLines(guards, body) {
|
|
5139
|
+
const operands = guards.map((guard) => `${guard.call}(${emitTsString(guard.arg)})`);
|
|
5140
|
+
const condition = ` if (${operands.join(" || ")})`;
|
|
5141
|
+
const brokenOperands = guards.flatMap((guard, i) => {
|
|
5142
|
+
const suffix = i < guards.length - 1 ? " ||" : "";
|
|
5143
|
+
return wrapAtPrintWidth(` ${operands[i]}${suffix}`, [
|
|
5144
|
+
` ${guard.call}(`,
|
|
5145
|
+
` ${emitTsString(guard.arg)}`,
|
|
5146
|
+
` )${suffix}`
|
|
5147
|
+
]);
|
|
5148
|
+
});
|
|
5149
|
+
return wrapAtPrintWidth(`${condition} ${body}`, [
|
|
5150
|
+
...wrapAtPrintWidth(condition, [" if (", ...brokenOperands, " )"]),
|
|
5151
|
+
` ${body}`
|
|
5152
|
+
]);
|
|
5153
|
+
}
|
|
5105
5154
|
function buildEndpointStubContent(input) {
|
|
5106
5155
|
const sorted = [...input.methods].sort(
|
|
5107
5156
|
(a, b) => METHOD_ORDER[a.method] - METHOD_ORDER[b.method] || a.pairKey.localeCompare(b.pairKey)
|
|
@@ -5109,24 +5158,15 @@ function buildEndpointStubContent(input) {
|
|
|
5109
5158
|
const exports2 = sorted.map((m) => {
|
|
5110
5159
|
const pair = emitTsString(m.pairKey);
|
|
5111
5160
|
const guards = [
|
|
5112
|
-
|
|
5113
|
-
...input.scope.kind === "enhancement" ? [
|
|
5161
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5162
|
+
...input.scope.kind === "enhancement" ? [{ call: "!isModuleEnabled", arg: input.scope.peer }] : []
|
|
5114
5163
|
];
|
|
5115
|
-
const condition = ` if (${guards.join(" || ")})`;
|
|
5116
|
-
const notFoundBody = "return new Response(null, { status: 404 });";
|
|
5117
5164
|
const message = emitTsString(`Missing endpoint handler for ${m.pairKey}`);
|
|
5118
5165
|
const importPath = emitTsString(input.importModule);
|
|
5119
5166
|
return [
|
|
5120
5167
|
`export async function ${m.method}(request: Request, context?: unknown): Promise<Response> {`,
|
|
5121
5168
|
" await initializeActiveEnhancements();",
|
|
5122
|
-
...
|
|
5123
|
-
...wrapAtPrintWidth(condition, [
|
|
5124
|
-
" if (",
|
|
5125
|
-
...guards.map((guard, i) => ` ${guard}${i < guards.length - 1 ? " ||" : ""}`),
|
|
5126
|
-
" )"
|
|
5127
|
-
]),
|
|
5128
|
-
` ${notFoundBody}`
|
|
5129
|
-
]),
|
|
5169
|
+
...emitGuardLines(guards, "return new Response(null, { status: 404 });"),
|
|
5130
5170
|
...wrapAtPrintWidth(` const { endpointHandlers } = await import(${importPath});`, [
|
|
5131
5171
|
" const { endpointHandlers } = await import(",
|
|
5132
5172
|
` ${importPath}`,
|
|
@@ -5156,12 +5196,16 @@ function buildEndpointStubContent(input) {
|
|
|
5156
5196
|
].join("\n");
|
|
5157
5197
|
}
|
|
5158
5198
|
function buildRouteStubContent(input) {
|
|
5199
|
+
const guards = [
|
|
5200
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5201
|
+
{ call: "isMountHeadless", arg: input.mountKey }
|
|
5202
|
+
];
|
|
5159
5203
|
const lines = [
|
|
5160
5204
|
formatRouteMarker(input.owner, input.nextRelativePath),
|
|
5161
5205
|
'import { notFound } from "next/navigation";',
|
|
5162
5206
|
// simple-import-sort puts packages and "@/" aliases in separate groups.
|
|
5163
5207
|
"",
|
|
5164
|
-
'import { isModuleEnabled } from "@/modules.gen";',
|
|
5208
|
+
'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
|
|
5165
5209
|
`import Page from ${emitTsString(input.importModule)};`
|
|
5166
5210
|
];
|
|
5167
5211
|
if (input.hasGenerateMetadata) {
|
|
@@ -5169,7 +5213,7 @@ function buildRouteStubContent(input) {
|
|
|
5169
5213
|
`import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
|
|
5170
5214
|
"",
|
|
5171
5215
|
"export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
|
|
5172
|
-
|
|
5216
|
+
...emitGuardLines(guards, "return {};"),
|
|
5173
5217
|
" return sourceGenerateMetadata(...args);",
|
|
5174
5218
|
"}"
|
|
5175
5219
|
);
|
|
@@ -5180,7 +5224,7 @@ function buildRouteStubContent(input) {
|
|
|
5180
5224
|
" Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
|
|
5181
5225
|
"",
|
|
5182
5226
|
"export default function ModulePage(props: PageProps) {",
|
|
5183
|
-
|
|
5227
|
+
...emitGuardLines(guards, "notFound();"),
|
|
5184
5228
|
" return <Page {...props} />;",
|
|
5185
5229
|
"}",
|
|
5186
5230
|
""
|
|
@@ -5382,7 +5426,6 @@ function planRouteStubs(input) {
|
|
|
5382
5426
|
}
|
|
5383
5427
|
for (const entry of entries) {
|
|
5384
5428
|
const nextRelativePath = entry.nextRelativePath;
|
|
5385
|
-
analyzeRouteEntryPath(nextRelativePath);
|
|
5386
5429
|
const destination = normalizeAppRouterDestinationPattern(nextRelativePath);
|
|
5387
5430
|
const path3 = routeEntryToAppPageFile(nextRelativePath);
|
|
5388
5431
|
const existing = routeDestinationOwners.get(destination);
|
|
@@ -5397,6 +5440,8 @@ function planRouteStubs(input) {
|
|
|
5397
5440
|
path: path3
|
|
5398
5441
|
});
|
|
5399
5442
|
const importModule = routeEntryImport(regMod.name, nextRelativePath);
|
|
5443
|
+
const analyzed = analyzeRouteEntryPath(nextRelativePath);
|
|
5444
|
+
const mount = mounts.find((claim) => routeEntryCoveredByMountClaim(analyzed, claim));
|
|
5400
5445
|
routeStubs.push({
|
|
5401
5446
|
path: path3,
|
|
5402
5447
|
owner: regMod.name,
|
|
@@ -5406,6 +5451,7 @@ function planRouteStubs(input) {
|
|
|
5406
5451
|
owner: regMod.name,
|
|
5407
5452
|
nextRelativePath,
|
|
5408
5453
|
importModule,
|
|
5454
|
+
mountKey: mountKey(regMod.name, mount),
|
|
5409
5455
|
hasGenerateMetadata: entry.hasGenerateMetadata
|
|
5410
5456
|
})
|
|
5411
5457
|
});
|
package/dist/index.js
CHANGED
|
@@ -4129,6 +4129,7 @@ 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`;
|
|
@@ -4136,6 +4137,11 @@ var MODULE_INIT_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-init.server.gen.t
|
|
|
4136
4137
|
var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
|
|
4137
4138
|
var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
|
|
4138
4139
|
|
|
4140
|
+
// ../../packages/lib/src/shared/module-mount-key.ts
|
|
4141
|
+
function mountKey(moduleName, mount) {
|
|
4142
|
+
return `${moduleName}:${mount.group}/${mount.segment}`;
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4139
4145
|
// ../../packages/lib/src/server/module-rail/composition-artifacts.ts
|
|
4140
4146
|
var import_typescript4 = __toESM(require("typescript"), 1);
|
|
4141
4147
|
|
|
@@ -4464,7 +4470,7 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4464
4470
|
" type SurfaceSlotCatalog,",
|
|
4465
4471
|
'} from "@/lib/modules/contributions";',
|
|
4466
4472
|
'import { substrateSlotCatalogs } from "@/lib/modules/substrate-slots";',
|
|
4467
|
-
'import { generatedModuleRegistry, isModuleEnabled } from "@/modules.gen";',
|
|
4473
|
+
'import { generatedModuleRegistry, isHrefHeadless, isModuleEnabled } from "@/modules.gen";',
|
|
4468
4474
|
...[...moduleImportLines].sort((a, b) => {
|
|
4469
4475
|
const pathA = a.match(/from ("[^"]+")/)?.[1] ?? a;
|
|
4470
4476
|
const pathB = b.match(/from ("[^"]+")/)?.[1] ?? b;
|
|
@@ -4514,11 +4520,19 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4514
4520
|
"export function resolveModuleContributions(",
|
|
4515
4521
|
" registry: typeof generatedModuleRegistry = generatedModuleRegistry",
|
|
4516
4522
|
"): ResolvedModuleContributions {",
|
|
4517
|
-
"
|
|
4523
|
+
" const resolved = resolveContributions({",
|
|
4518
4524
|
" registry,",
|
|
4519
4525
|
" catalogs: moduleSlotCatalogs,",
|
|
4520
4526
|
" sources: moduleContributionSources,",
|
|
4521
4527
|
" });",
|
|
4528
|
+
" // A nav entry into a headless mount would link to a page that is switched off.",
|
|
4529
|
+
' const nav = resolved["admin.nav"];',
|
|
4530
|
+
" if (nav) {",
|
|
4531
|
+
' resolved["admin.nav"] = nav.filter(',
|
|
4532
|
+
" (entry) => !isHrefHeadless((entry as { href?: unknown }).href)",
|
|
4533
|
+
" );",
|
|
4534
|
+
" }",
|
|
4535
|
+
" return resolved;",
|
|
4522
4536
|
"}",
|
|
4523
4537
|
"",
|
|
4524
4538
|
"export const resolvedModuleContributions: ResolvedModuleContributions =",
|
|
@@ -5104,6 +5118,25 @@ function emitModulesGenTs(registry) {
|
|
|
5104
5118
|
' if (entry && entry.kind !== "feature") return true;',
|
|
5105
5119
|
' return raw.split(",").includes(name);',
|
|
5106
5120
|
"}",
|
|
5121
|
+
"",
|
|
5122
|
+
"/** Headless mount keys come from the platform at build time: that page is off, its Module stays on. */",
|
|
5123
|
+
"export function isMountHeadless(key: string): boolean {",
|
|
5124
|
+
` const raw = process.env.${HEADLESS_MOUNTS_ENV};`,
|
|
5125
|
+
' return raw ? raw.split(",").includes(key) : false;',
|
|
5126
|
+
"}",
|
|
5127
|
+
"",
|
|
5128
|
+
"/** True when `href` is a headless mount's URL or below it. */",
|
|
5129
|
+
"export function isHrefHeadless(href: unknown): boolean {",
|
|
5130
|
+
' if (typeof href !== "string") return false;',
|
|
5131
|
+
" const target = href.split(/[?#]/)[0];",
|
|
5132
|
+
" return generatedModuleRegistry.modules.some((mod) =>",
|
|
5133
|
+
" mod.mounts.some((mount) => {",
|
|
5134
|
+
" if (!isMountHeadless(`${mod.name}:${mount.group}/${mount.segment}`)) return false;",
|
|
5135
|
+
" const path = `/${mount.segment}`;",
|
|
5136
|
+
" return target === path || target.startsWith(`${path}/`);",
|
|
5137
|
+
" })",
|
|
5138
|
+
" );",
|
|
5139
|
+
"}",
|
|
5107
5140
|
""
|
|
5108
5141
|
].join("\n");
|
|
5109
5142
|
}
|
|
@@ -5111,6 +5144,22 @@ var STUB_PRINT_WIDTH = 100;
|
|
|
5111
5144
|
function wrapAtPrintWidth(flat, broken) {
|
|
5112
5145
|
return flat.length > STUB_PRINT_WIDTH ? broken : [flat];
|
|
5113
5146
|
}
|
|
5147
|
+
function emitGuardLines(guards, body) {
|
|
5148
|
+
const operands = guards.map((guard) => `${guard.call}(${emitTsString(guard.arg)})`);
|
|
5149
|
+
const condition = ` if (${operands.join(" || ")})`;
|
|
5150
|
+
const brokenOperands = guards.flatMap((guard, i) => {
|
|
5151
|
+
const suffix = i < guards.length - 1 ? " ||" : "";
|
|
5152
|
+
return wrapAtPrintWidth(` ${operands[i]}${suffix}`, [
|
|
5153
|
+
` ${guard.call}(`,
|
|
5154
|
+
` ${emitTsString(guard.arg)}`,
|
|
5155
|
+
` )${suffix}`
|
|
5156
|
+
]);
|
|
5157
|
+
});
|
|
5158
|
+
return wrapAtPrintWidth(`${condition} ${body}`, [
|
|
5159
|
+
...wrapAtPrintWidth(condition, [" if (", ...brokenOperands, " )"]),
|
|
5160
|
+
` ${body}`
|
|
5161
|
+
]);
|
|
5162
|
+
}
|
|
5114
5163
|
function buildEndpointStubContent(input) {
|
|
5115
5164
|
const sorted = [...input.methods].sort(
|
|
5116
5165
|
(a, b) => METHOD_ORDER[a.method] - METHOD_ORDER[b.method] || a.pairKey.localeCompare(b.pairKey)
|
|
@@ -5118,24 +5167,15 @@ function buildEndpointStubContent(input) {
|
|
|
5118
5167
|
const exports2 = sorted.map((m) => {
|
|
5119
5168
|
const pair = emitTsString(m.pairKey);
|
|
5120
5169
|
const guards = [
|
|
5121
|
-
|
|
5122
|
-
...input.scope.kind === "enhancement" ? [
|
|
5170
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5171
|
+
...input.scope.kind === "enhancement" ? [{ call: "!isModuleEnabled", arg: input.scope.peer }] : []
|
|
5123
5172
|
];
|
|
5124
|
-
const condition = ` if (${guards.join(" || ")})`;
|
|
5125
|
-
const notFoundBody = "return new Response(null, { status: 404 });";
|
|
5126
5173
|
const message = emitTsString(`Missing endpoint handler for ${m.pairKey}`);
|
|
5127
5174
|
const importPath = emitTsString(input.importModule);
|
|
5128
5175
|
return [
|
|
5129
5176
|
`export async function ${m.method}(request: Request, context?: unknown): Promise<Response> {`,
|
|
5130
5177
|
" await initializeActiveEnhancements();",
|
|
5131
|
-
...
|
|
5132
|
-
...wrapAtPrintWidth(condition, [
|
|
5133
|
-
" if (",
|
|
5134
|
-
...guards.map((guard, i) => ` ${guard}${i < guards.length - 1 ? " ||" : ""}`),
|
|
5135
|
-
" )"
|
|
5136
|
-
]),
|
|
5137
|
-
` ${notFoundBody}`
|
|
5138
|
-
]),
|
|
5178
|
+
...emitGuardLines(guards, "return new Response(null, { status: 404 });"),
|
|
5139
5179
|
...wrapAtPrintWidth(` const { endpointHandlers } = await import(${importPath});`, [
|
|
5140
5180
|
" const { endpointHandlers } = await import(",
|
|
5141
5181
|
` ${importPath}`,
|
|
@@ -5165,12 +5205,16 @@ function buildEndpointStubContent(input) {
|
|
|
5165
5205
|
].join("\n");
|
|
5166
5206
|
}
|
|
5167
5207
|
function buildRouteStubContent(input) {
|
|
5208
|
+
const guards = [
|
|
5209
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5210
|
+
{ call: "isMountHeadless", arg: input.mountKey }
|
|
5211
|
+
];
|
|
5168
5212
|
const lines = [
|
|
5169
5213
|
formatRouteMarker(input.owner, input.nextRelativePath),
|
|
5170
5214
|
'import { notFound } from "next/navigation";',
|
|
5171
5215
|
// simple-import-sort puts packages and "@/" aliases in separate groups.
|
|
5172
5216
|
"",
|
|
5173
|
-
'import { isModuleEnabled } from "@/modules.gen";',
|
|
5217
|
+
'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
|
|
5174
5218
|
`import Page from ${emitTsString(input.importModule)};`
|
|
5175
5219
|
];
|
|
5176
5220
|
if (input.hasGenerateMetadata) {
|
|
@@ -5178,7 +5222,7 @@ function buildRouteStubContent(input) {
|
|
|
5178
5222
|
`import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
|
|
5179
5223
|
"",
|
|
5180
5224
|
"export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
|
|
5181
|
-
|
|
5225
|
+
...emitGuardLines(guards, "return {};"),
|
|
5182
5226
|
" return sourceGenerateMetadata(...args);",
|
|
5183
5227
|
"}"
|
|
5184
5228
|
);
|
|
@@ -5189,7 +5233,7 @@ function buildRouteStubContent(input) {
|
|
|
5189
5233
|
" Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
|
|
5190
5234
|
"",
|
|
5191
5235
|
"export default function ModulePage(props: PageProps) {",
|
|
5192
|
-
|
|
5236
|
+
...emitGuardLines(guards, "notFound();"),
|
|
5193
5237
|
" return <Page {...props} />;",
|
|
5194
5238
|
"}",
|
|
5195
5239
|
""
|
|
@@ -5391,7 +5435,6 @@ function planRouteStubs(input) {
|
|
|
5391
5435
|
}
|
|
5392
5436
|
for (const entry of entries) {
|
|
5393
5437
|
const nextRelativePath = entry.nextRelativePath;
|
|
5394
|
-
analyzeRouteEntryPath(nextRelativePath);
|
|
5395
5438
|
const destination = normalizeAppRouterDestinationPattern(nextRelativePath);
|
|
5396
5439
|
const path3 = routeEntryToAppPageFile(nextRelativePath);
|
|
5397
5440
|
const existing = routeDestinationOwners.get(destination);
|
|
@@ -5406,6 +5449,8 @@ function planRouteStubs(input) {
|
|
|
5406
5449
|
path: path3
|
|
5407
5450
|
});
|
|
5408
5451
|
const importModule = routeEntryImport(regMod.name, nextRelativePath);
|
|
5452
|
+
const analyzed = analyzeRouteEntryPath(nextRelativePath);
|
|
5453
|
+
const mount = mounts.find((claim) => routeEntryCoveredByMountClaim(analyzed, claim));
|
|
5409
5454
|
routeStubs.push({
|
|
5410
5455
|
path: path3,
|
|
5411
5456
|
owner: regMod.name,
|
|
@@ -5415,6 +5460,7 @@ function planRouteStubs(input) {
|
|
|
5415
5460
|
owner: regMod.name,
|
|
5416
5461
|
nextRelativePath,
|
|
5417
5462
|
importModule,
|
|
5463
|
+
mountKey: mountKey(regMod.name, mount),
|
|
5418
5464
|
hasGenerateMetadata: entry.hasGenerateMetadata
|
|
5419
5465
|
})
|
|
5420
5466
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -4114,6 +4114,7 @@ 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`;
|
|
@@ -4121,6 +4122,11 @@ var MODULE_INIT_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-init.server.gen.t
|
|
|
4121
4122
|
var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
|
|
4122
4123
|
var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
|
|
4123
4124
|
|
|
4125
|
+
// ../../packages/lib/src/shared/module-mount-key.ts
|
|
4126
|
+
function mountKey(moduleName, mount) {
|
|
4127
|
+
return `${moduleName}:${mount.group}/${mount.segment}`;
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4124
4130
|
// ../../packages/lib/src/server/module-rail/composition-artifacts.ts
|
|
4125
4131
|
import ts4 from "typescript";
|
|
4126
4132
|
|
|
@@ -4449,7 +4455,7 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4449
4455
|
" type SurfaceSlotCatalog,",
|
|
4450
4456
|
'} from "@/lib/modules/contributions";',
|
|
4451
4457
|
'import { substrateSlotCatalogs } from "@/lib/modules/substrate-slots";',
|
|
4452
|
-
'import { generatedModuleRegistry, isModuleEnabled } from "@/modules.gen";',
|
|
4458
|
+
'import { generatedModuleRegistry, isHrefHeadless, isModuleEnabled } from "@/modules.gen";',
|
|
4453
4459
|
...[...moduleImportLines].sort((a, b) => {
|
|
4454
4460
|
const pathA = a.match(/from ("[^"]+")/)?.[1] ?? a;
|
|
4455
4461
|
const pathB = b.match(/from ("[^"]+")/)?.[1] ?? b;
|
|
@@ -4499,11 +4505,19 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4499
4505
|
"export function resolveModuleContributions(",
|
|
4500
4506
|
" registry: typeof generatedModuleRegistry = generatedModuleRegistry",
|
|
4501
4507
|
"): ResolvedModuleContributions {",
|
|
4502
|
-
"
|
|
4508
|
+
" const resolved = resolveContributions({",
|
|
4503
4509
|
" registry,",
|
|
4504
4510
|
" catalogs: moduleSlotCatalogs,",
|
|
4505
4511
|
" sources: moduleContributionSources,",
|
|
4506
4512
|
" });",
|
|
4513
|
+
" // A nav entry into a headless mount would link to a page that is switched off.",
|
|
4514
|
+
' const nav = resolved["admin.nav"];',
|
|
4515
|
+
" if (nav) {",
|
|
4516
|
+
' resolved["admin.nav"] = nav.filter(',
|
|
4517
|
+
" (entry) => !isHrefHeadless((entry as { href?: unknown }).href)",
|
|
4518
|
+
" );",
|
|
4519
|
+
" }",
|
|
4520
|
+
" return resolved;",
|
|
4507
4521
|
"}",
|
|
4508
4522
|
"",
|
|
4509
4523
|
"export const resolvedModuleContributions: ResolvedModuleContributions =",
|
|
@@ -5089,6 +5103,25 @@ function emitModulesGenTs(registry) {
|
|
|
5089
5103
|
' if (entry && entry.kind !== "feature") return true;',
|
|
5090
5104
|
' return raw.split(",").includes(name);',
|
|
5091
5105
|
"}",
|
|
5106
|
+
"",
|
|
5107
|
+
"/** Headless mount keys come from the platform at build time: that page is off, its Module stays on. */",
|
|
5108
|
+
"export function isMountHeadless(key: string): boolean {",
|
|
5109
|
+
` const raw = process.env.${HEADLESS_MOUNTS_ENV};`,
|
|
5110
|
+
' return raw ? raw.split(",").includes(key) : false;',
|
|
5111
|
+
"}",
|
|
5112
|
+
"",
|
|
5113
|
+
"/** True when `href` is a headless mount's URL or below it. */",
|
|
5114
|
+
"export function isHrefHeadless(href: unknown): boolean {",
|
|
5115
|
+
' if (typeof href !== "string") return false;',
|
|
5116
|
+
" const target = href.split(/[?#]/)[0];",
|
|
5117
|
+
" return generatedModuleRegistry.modules.some((mod) =>",
|
|
5118
|
+
" mod.mounts.some((mount) => {",
|
|
5119
|
+
" if (!isMountHeadless(`${mod.name}:${mount.group}/${mount.segment}`)) return false;",
|
|
5120
|
+
" const path = `/${mount.segment}`;",
|
|
5121
|
+
" return target === path || target.startsWith(`${path}/`);",
|
|
5122
|
+
" })",
|
|
5123
|
+
" );",
|
|
5124
|
+
"}",
|
|
5092
5125
|
""
|
|
5093
5126
|
].join("\n");
|
|
5094
5127
|
}
|
|
@@ -5096,6 +5129,22 @@ var STUB_PRINT_WIDTH = 100;
|
|
|
5096
5129
|
function wrapAtPrintWidth(flat, broken) {
|
|
5097
5130
|
return flat.length > STUB_PRINT_WIDTH ? broken : [flat];
|
|
5098
5131
|
}
|
|
5132
|
+
function emitGuardLines(guards, body) {
|
|
5133
|
+
const operands = guards.map((guard) => `${guard.call}(${emitTsString(guard.arg)})`);
|
|
5134
|
+
const condition = ` if (${operands.join(" || ")})`;
|
|
5135
|
+
const brokenOperands = guards.flatMap((guard, i) => {
|
|
5136
|
+
const suffix = i < guards.length - 1 ? " ||" : "";
|
|
5137
|
+
return wrapAtPrintWidth(` ${operands[i]}${suffix}`, [
|
|
5138
|
+
` ${guard.call}(`,
|
|
5139
|
+
` ${emitTsString(guard.arg)}`,
|
|
5140
|
+
` )${suffix}`
|
|
5141
|
+
]);
|
|
5142
|
+
});
|
|
5143
|
+
return wrapAtPrintWidth(`${condition} ${body}`, [
|
|
5144
|
+
...wrapAtPrintWidth(condition, [" if (", ...brokenOperands, " )"]),
|
|
5145
|
+
` ${body}`
|
|
5146
|
+
]);
|
|
5147
|
+
}
|
|
5099
5148
|
function buildEndpointStubContent(input) {
|
|
5100
5149
|
const sorted = [...input.methods].sort(
|
|
5101
5150
|
(a, b) => METHOD_ORDER[a.method] - METHOD_ORDER[b.method] || a.pairKey.localeCompare(b.pairKey)
|
|
@@ -5103,24 +5152,15 @@ function buildEndpointStubContent(input) {
|
|
|
5103
5152
|
const exports = sorted.map((m) => {
|
|
5104
5153
|
const pair = emitTsString(m.pairKey);
|
|
5105
5154
|
const guards = [
|
|
5106
|
-
|
|
5107
|
-
...input.scope.kind === "enhancement" ? [
|
|
5155
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5156
|
+
...input.scope.kind === "enhancement" ? [{ call: "!isModuleEnabled", arg: input.scope.peer }] : []
|
|
5108
5157
|
];
|
|
5109
|
-
const condition = ` if (${guards.join(" || ")})`;
|
|
5110
|
-
const notFoundBody = "return new Response(null, { status: 404 });";
|
|
5111
5158
|
const message = emitTsString(`Missing endpoint handler for ${m.pairKey}`);
|
|
5112
5159
|
const importPath = emitTsString(input.importModule);
|
|
5113
5160
|
return [
|
|
5114
5161
|
`export async function ${m.method}(request: Request, context?: unknown): Promise<Response> {`,
|
|
5115
5162
|
" await initializeActiveEnhancements();",
|
|
5116
|
-
...
|
|
5117
|
-
...wrapAtPrintWidth(condition, [
|
|
5118
|
-
" if (",
|
|
5119
|
-
...guards.map((guard, i) => ` ${guard}${i < guards.length - 1 ? " ||" : ""}`),
|
|
5120
|
-
" )"
|
|
5121
|
-
]),
|
|
5122
|
-
` ${notFoundBody}`
|
|
5123
|
-
]),
|
|
5163
|
+
...emitGuardLines(guards, "return new Response(null, { status: 404 });"),
|
|
5124
5164
|
...wrapAtPrintWidth(` const { endpointHandlers } = await import(${importPath});`, [
|
|
5125
5165
|
" const { endpointHandlers } = await import(",
|
|
5126
5166
|
` ${importPath}`,
|
|
@@ -5150,12 +5190,16 @@ function buildEndpointStubContent(input) {
|
|
|
5150
5190
|
].join("\n");
|
|
5151
5191
|
}
|
|
5152
5192
|
function buildRouteStubContent(input) {
|
|
5193
|
+
const guards = [
|
|
5194
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5195
|
+
{ call: "isMountHeadless", arg: input.mountKey }
|
|
5196
|
+
];
|
|
5153
5197
|
const lines = [
|
|
5154
5198
|
formatRouteMarker(input.owner, input.nextRelativePath),
|
|
5155
5199
|
'import { notFound } from "next/navigation";',
|
|
5156
5200
|
// simple-import-sort puts packages and "@/" aliases in separate groups.
|
|
5157
5201
|
"",
|
|
5158
|
-
'import { isModuleEnabled } from "@/modules.gen";',
|
|
5202
|
+
'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
|
|
5159
5203
|
`import Page from ${emitTsString(input.importModule)};`
|
|
5160
5204
|
];
|
|
5161
5205
|
if (input.hasGenerateMetadata) {
|
|
@@ -5163,7 +5207,7 @@ function buildRouteStubContent(input) {
|
|
|
5163
5207
|
`import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
|
|
5164
5208
|
"",
|
|
5165
5209
|
"export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
|
|
5166
|
-
|
|
5210
|
+
...emitGuardLines(guards, "return {};"),
|
|
5167
5211
|
" return sourceGenerateMetadata(...args);",
|
|
5168
5212
|
"}"
|
|
5169
5213
|
);
|
|
@@ -5174,7 +5218,7 @@ function buildRouteStubContent(input) {
|
|
|
5174
5218
|
" Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
|
|
5175
5219
|
"",
|
|
5176
5220
|
"export default function ModulePage(props: PageProps) {",
|
|
5177
|
-
|
|
5221
|
+
...emitGuardLines(guards, "notFound();"),
|
|
5178
5222
|
" return <Page {...props} />;",
|
|
5179
5223
|
"}",
|
|
5180
5224
|
""
|
|
@@ -5376,7 +5420,6 @@ function planRouteStubs(input) {
|
|
|
5376
5420
|
}
|
|
5377
5421
|
for (const entry of entries) {
|
|
5378
5422
|
const nextRelativePath = entry.nextRelativePath;
|
|
5379
|
-
analyzeRouteEntryPath(nextRelativePath);
|
|
5380
5423
|
const destination = normalizeAppRouterDestinationPattern(nextRelativePath);
|
|
5381
5424
|
const path3 = routeEntryToAppPageFile(nextRelativePath);
|
|
5382
5425
|
const existing = routeDestinationOwners.get(destination);
|
|
@@ -5391,6 +5434,8 @@ function planRouteStubs(input) {
|
|
|
5391
5434
|
path: path3
|
|
5392
5435
|
});
|
|
5393
5436
|
const importModule = routeEntryImport(regMod.name, nextRelativePath);
|
|
5437
|
+
const analyzed = analyzeRouteEntryPath(nextRelativePath);
|
|
5438
|
+
const mount = mounts.find((claim) => routeEntryCoveredByMountClaim(analyzed, claim));
|
|
5394
5439
|
routeStubs.push({
|
|
5395
5440
|
path: path3,
|
|
5396
5441
|
owner: regMod.name,
|
|
@@ -5400,6 +5445,7 @@ function planRouteStubs(input) {
|
|
|
5400
5445
|
owner: regMod.name,
|
|
5401
5446
|
nextRelativePath,
|
|
5402
5447
|
importModule,
|
|
5448
|
+
mountKey: mountKey(regMod.name, mount),
|
|
5403
5449
|
hasGenerateMetadata: entry.hasGenerateMetadata
|
|
5404
5450
|
})
|
|
5405
5451
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/compose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
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",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|