@stardeck-customer-apps/compose 0.5.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 +21 -1
- package/dist/cli.js +183 -28
- package/dist/index.js +183 -28
- package/dist/index.mjs +183 -28
- 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`
|
|
@@ -81,6 +81,26 @@ a silent fallback. It holds deployment facts: gitignore it (`apps/web/.gitignore
|
|
|
81
81
|
missing or wrong makes compose refuse rather than clobber your edit.
|
|
82
82
|
- Change `module.json` (or the Module's own source under `routes/`,
|
|
83
83
|
`contributions.ts`, `i18n/`, …) and re-run compose.
|
|
84
|
+
- To reword a Module string, don't edit the Module: put the same key in
|
|
85
|
+
`apps/web/src/lib/i18n/overrides/<locale>.json` (app-owned, composed last).
|
|
86
|
+
It may only replace keys a Module or `lib/i18n/substrate/` defines; an
|
|
87
|
+
override of a missing key fails compose and names the key. An override file
|
|
88
|
+
for a locale the Modules don't ship (e.g. `ja`) translates their keys for that
|
|
89
|
+
locale; the locale also needs a `lib/i18n/substrate/<locale>.json`. New keys
|
|
90
|
+
go in `lib/i18n/substrate/<locale>.json`, never in an override. Needs compose
|
|
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.
|
|
84
104
|
- `@stardeck-customer-apps/compose/runtime` exports `isModuleEnabledByList`,
|
|
85
105
|
the single enablement rule, and imports nothing — it is safe in client
|
|
86
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
|
|
|
@@ -4267,6 +4273,40 @@ function cloneTree(value) {
|
|
|
4267
4273
|
}
|
|
4268
4274
|
return clone;
|
|
4269
4275
|
}
|
|
4276
|
+
function collectLeafPaths(value, path3, out) {
|
|
4277
|
+
if (typeof value === "string") {
|
|
4278
|
+
out.push(path3);
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4281
|
+
assertMessageTree(value, path3);
|
|
4282
|
+
for (const [key, child] of Object.entries(value)) collectLeafPaths(child, `${path3}.${key}`, out);
|
|
4283
|
+
}
|
|
4284
|
+
function overrideInto(target, source, owner, path3, missing) {
|
|
4285
|
+
for (const [key, value] of Object.entries(source)) {
|
|
4286
|
+
const nextPath = path3 ? `${path3}.${key}` : key;
|
|
4287
|
+
if (!Object.prototype.hasOwnProperty.call(target, key)) {
|
|
4288
|
+
collectLeafPaths(value, nextPath, missing);
|
|
4289
|
+
continue;
|
|
4290
|
+
}
|
|
4291
|
+
const existing = target[key];
|
|
4292
|
+
if (typeof value === "string") {
|
|
4293
|
+
if (typeof existing !== "string") {
|
|
4294
|
+
throw new Error(
|
|
4295
|
+
`[ModuleRail] composition artifacts: i18n merge conflict at ${nextPath}: ${owner} has string leaf but another owner has an object`
|
|
4296
|
+
);
|
|
4297
|
+
}
|
|
4298
|
+
target[key] = value;
|
|
4299
|
+
continue;
|
|
4300
|
+
}
|
|
4301
|
+
assertMessageTree(value, nextPath);
|
|
4302
|
+
if (typeof existing === "string") {
|
|
4303
|
+
throw new Error(
|
|
4304
|
+
`[ModuleRail] composition artifacts: i18n merge conflict at ${nextPath}: ${owner} has object but another owner has a string leaf`
|
|
4305
|
+
);
|
|
4306
|
+
}
|
|
4307
|
+
overrideInto(existing, value, owner, nextPath, missing);
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4270
4310
|
function composeLocaleMessagesForPlan(layers) {
|
|
4271
4311
|
const result = createMessageTree();
|
|
4272
4312
|
const leafOwners = /* @__PURE__ */ new Map();
|
|
@@ -4318,7 +4358,17 @@ function composeLocaleMessagesForPlan(layers) {
|
|
|
4318
4358
|
}
|
|
4319
4359
|
for (const layer of layers) {
|
|
4320
4360
|
assertMessageTree(layer.messages, layer.owner);
|
|
4321
|
-
|
|
4361
|
+
if (!layer.override) {
|
|
4362
|
+
mergeInto(result, cloneTree(layer.messages), layer.owner, "");
|
|
4363
|
+
continue;
|
|
4364
|
+
}
|
|
4365
|
+
const missing = [];
|
|
4366
|
+
overrideInto(result, cloneTree(layer.messages), layer.owner, "", missing);
|
|
4367
|
+
if (missing.length > 0) {
|
|
4368
|
+
throw new Error(
|
|
4369
|
+
`[ModuleRail] composition artifacts: i18n override in ${layer.owner} replaces keys that no Module or substrate defines: ${missing.join(", ")}`
|
|
4370
|
+
);
|
|
4371
|
+
}
|
|
4322
4372
|
}
|
|
4323
4373
|
return result;
|
|
4324
4374
|
}
|
|
@@ -4411,7 +4461,7 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4411
4461
|
" type SurfaceSlotCatalog,",
|
|
4412
4462
|
'} from "@/lib/modules/contributions";',
|
|
4413
4463
|
'import { substrateSlotCatalogs } from "@/lib/modules/substrate-slots";',
|
|
4414
|
-
'import { generatedModuleRegistry, isModuleEnabled } from "@/modules.gen";',
|
|
4464
|
+
'import { generatedModuleRegistry, isHrefHeadless, isModuleEnabled } from "@/modules.gen";',
|
|
4415
4465
|
...[...moduleImportLines].sort((a, b) => {
|
|
4416
4466
|
const pathA = a.match(/from ("[^"]+")/)?.[1] ?? a;
|
|
4417
4467
|
const pathB = b.match(/from ("[^"]+")/)?.[1] ?? b;
|
|
@@ -4461,11 +4511,19 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4461
4511
|
"export function resolveModuleContributions(",
|
|
4462
4512
|
" registry: typeof generatedModuleRegistry = generatedModuleRegistry",
|
|
4463
4513
|
"): ResolvedModuleContributions {",
|
|
4464
|
-
"
|
|
4514
|
+
" const resolved = resolveContributions({",
|
|
4465
4515
|
" registry,",
|
|
4466
4516
|
" catalogs: moduleSlotCatalogs,",
|
|
4467
4517
|
" sources: moduleContributionSources,",
|
|
4468
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;",
|
|
4469
4527
|
"}",
|
|
4470
4528
|
"",
|
|
4471
4529
|
"export const resolvedModuleContributions: ResolvedModuleContributions =",
|
|
@@ -4497,6 +4555,7 @@ function emitModuleI18nGenTs(input) {
|
|
|
4497
4555
|
);
|
|
4498
4556
|
}
|
|
4499
4557
|
const targetLocales = substrate.map((l) => l.locale);
|
|
4558
|
+
const overrideByLocale = indexOverrideLocales(input.overrideLocales ?? [], targetLocales);
|
|
4500
4559
|
const installed = new Set(input.registry.modules.map((m) => m.name));
|
|
4501
4560
|
const allLayers = input.moduleLayers ? [...input.moduleLayers] : deriveModuleLocaleLayers(input.registry, input.facts);
|
|
4502
4561
|
const layers = allLayers.filter((layer) => installed.has(layer.owner.split("/")[0]));
|
|
@@ -4517,19 +4576,38 @@ function emitModuleI18nGenTs(input) {
|
|
|
4517
4576
|
const messages = layer.localeMessages[locale] ?? (locale === "en" ? layer.enMessages : layer.enMessages);
|
|
4518
4577
|
composeLayers.push({ owner: layer.sourceLabel, messages });
|
|
4519
4578
|
}
|
|
4579
|
+
const override = overrideByLocale.get(locale);
|
|
4580
|
+
if (override) {
|
|
4581
|
+
composeLayers.push({
|
|
4582
|
+
owner: overrideOwner(locale),
|
|
4583
|
+
messages: override.messages,
|
|
4584
|
+
override: true
|
|
4585
|
+
});
|
|
4586
|
+
}
|
|
4520
4587
|
composeLocaleMessagesForPlan(composeLayers);
|
|
4521
4588
|
}
|
|
4522
4589
|
const composeImport = 'import { composeLocaleMessages } from "@/lib/i18n/compose-messages";';
|
|
4523
4590
|
const moduleJsonImports = [];
|
|
4524
4591
|
const relativeSubstrateImports = [];
|
|
4525
4592
|
const substrateIdentByLocale = {};
|
|
4593
|
+
const overrideIdentByLocale = {};
|
|
4526
4594
|
targetLocales.forEach((locale, localeIdx) => {
|
|
4527
4595
|
const ident = `i18nSubstrateLoc${localeIdx}`;
|
|
4528
4596
|
relativeSubstrateImports.push(
|
|
4529
4597
|
`import ${ident} from ${JSON.stringify(`./lib/i18n/substrate/${locale}.json`)};`
|
|
4530
4598
|
);
|
|
4531
4599
|
substrateIdentByLocale[locale] = ident;
|
|
4600
|
+
if (overrideByLocale.has(locale)) {
|
|
4601
|
+
const overrideIdent = `i18nOverrideLoc${localeIdx}`;
|
|
4602
|
+
relativeSubstrateImports.push(
|
|
4603
|
+
`import ${overrideIdent} from ${JSON.stringify(`./${overrideOwner(locale)}`)};`
|
|
4604
|
+
);
|
|
4605
|
+
overrideIdentByLocale[locale] = overrideIdent;
|
|
4606
|
+
}
|
|
4532
4607
|
});
|
|
4608
|
+
relativeSubstrateImports.sort(
|
|
4609
|
+
(a, b) => compareImportSpecifiers(a.match(/ from (.+);$/)[1], b.match(/ from (.+);$/)[1])
|
|
4610
|
+
);
|
|
4533
4611
|
const layerIdents = [];
|
|
4534
4612
|
layers.forEach((layer, layerIdx) => {
|
|
4535
4613
|
const localeIdents = {};
|
|
@@ -4563,15 +4641,29 @@ function emitModuleI18nGenTs(input) {
|
|
|
4563
4641
|
(layer) => ` { owner: ${JSON.stringify(layer.owner)}, messages: ${layer.localeIdents[locale]} },`
|
|
4564
4642
|
)
|
|
4565
4643
|
];
|
|
4644
|
+
const overrideIdent = overrideIdentByLocale[locale];
|
|
4645
|
+
const messageLines = overrideIdent ? [
|
|
4646
|
+
" messages: composeLocaleMessages([",
|
|
4647
|
+
" ...[",
|
|
4648
|
+
...layerLines,
|
|
4649
|
+
" ].map((layer) => ({",
|
|
4650
|
+
" ...layer,",
|
|
4651
|
+
' disabled: layer.owner !== "substrate" && !layer.owner.split("/").every(enabled),',
|
|
4652
|
+
" })),",
|
|
4653
|
+
` { owner: ${JSON.stringify(overrideOwner(locale))}, messages: ${overrideIdent}, override: true },`,
|
|
4654
|
+
" ]),"
|
|
4655
|
+
] : [
|
|
4656
|
+
" messages: composeLocaleMessages([",
|
|
4657
|
+
...layerLines,
|
|
4658
|
+
" ].filter((layer) =>",
|
|
4659
|
+
' layer.owner === "substrate" || layer.owner.split("/").every(enabled)',
|
|
4660
|
+
" )),"
|
|
4661
|
+
];
|
|
4566
4662
|
bundleBlocks.push(
|
|
4567
4663
|
[
|
|
4568
4664
|
` ${JSON.stringify(locale)}: {`,
|
|
4569
4665
|
` locale: ${JSON.stringify(locale)},`,
|
|
4570
|
-
|
|
4571
|
-
...layerLines,
|
|
4572
|
-
" ].filter((layer) =>",
|
|
4573
|
-
' layer.owner === "substrate" || layer.owner.split("/").every(enabled)',
|
|
4574
|
-
" )),",
|
|
4666
|
+
...messageLines,
|
|
4575
4667
|
" }"
|
|
4576
4668
|
].join("\n")
|
|
4577
4669
|
);
|
|
@@ -4616,6 +4708,26 @@ function emitModuleI18nGenTs(input) {
|
|
|
4616
4708
|
""
|
|
4617
4709
|
].join("\n");
|
|
4618
4710
|
}
|
|
4711
|
+
function indexOverrideLocales(overrideLocales, targetLocales) {
|
|
4712
|
+
const byLocale = /* @__PURE__ */ new Map();
|
|
4713
|
+
for (const layer of overrideLocales) {
|
|
4714
|
+
if (!isValidLocaleBasename(layer.locale)) {
|
|
4715
|
+
throw new Error(
|
|
4716
|
+
`[ModuleRail] composition artifacts: unsafe override locale basename "${layer.locale}"`
|
|
4717
|
+
);
|
|
4718
|
+
}
|
|
4719
|
+
if (!targetLocales.includes(layer.locale)) {
|
|
4720
|
+
throw new Error(
|
|
4721
|
+
`[ModuleRail] composition artifacts: ${overrideOwner(layer.locale)} has no matching lib/i18n/substrate/${layer.locale}.json; add that substrate file to make ${layer.locale} an app locale`
|
|
4722
|
+
);
|
|
4723
|
+
}
|
|
4724
|
+
byLocale.set(layer.locale, layer);
|
|
4725
|
+
}
|
|
4726
|
+
return byLocale;
|
|
4727
|
+
}
|
|
4728
|
+
function overrideOwner(locale) {
|
|
4729
|
+
return `lib/i18n/overrides/${locale}.json`;
|
|
4730
|
+
}
|
|
4619
4731
|
function deriveModuleLocaleLayers(registry, facts) {
|
|
4620
4732
|
const layers = [];
|
|
4621
4733
|
const installed = new Set(registry.modules.map((m) => m.name));
|
|
@@ -4997,6 +5109,25 @@ function emitModulesGenTs(registry) {
|
|
|
4997
5109
|
' if (entry && entry.kind !== "feature") return true;',
|
|
4998
5110
|
' return raw.split(",").includes(name);',
|
|
4999
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
|
+
"}",
|
|
5000
5131
|
""
|
|
5001
5132
|
].join("\n");
|
|
5002
5133
|
}
|
|
@@ -5004,6 +5135,22 @@ var STUB_PRINT_WIDTH = 100;
|
|
|
5004
5135
|
function wrapAtPrintWidth(flat, broken) {
|
|
5005
5136
|
return flat.length > STUB_PRINT_WIDTH ? broken : [flat];
|
|
5006
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
|
+
}
|
|
5007
5154
|
function buildEndpointStubContent(input) {
|
|
5008
5155
|
const sorted = [...input.methods].sort(
|
|
5009
5156
|
(a, b) => METHOD_ORDER[a.method] - METHOD_ORDER[b.method] || a.pairKey.localeCompare(b.pairKey)
|
|
@@ -5011,24 +5158,15 @@ function buildEndpointStubContent(input) {
|
|
|
5011
5158
|
const exports2 = sorted.map((m) => {
|
|
5012
5159
|
const pair = emitTsString(m.pairKey);
|
|
5013
5160
|
const guards = [
|
|
5014
|
-
|
|
5015
|
-
...input.scope.kind === "enhancement" ? [
|
|
5161
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5162
|
+
...input.scope.kind === "enhancement" ? [{ call: "!isModuleEnabled", arg: input.scope.peer }] : []
|
|
5016
5163
|
];
|
|
5017
|
-
const condition = ` if (${guards.join(" || ")})`;
|
|
5018
|
-
const notFoundBody = "return new Response(null, { status: 404 });";
|
|
5019
5164
|
const message = emitTsString(`Missing endpoint handler for ${m.pairKey}`);
|
|
5020
5165
|
const importPath = emitTsString(input.importModule);
|
|
5021
5166
|
return [
|
|
5022
5167
|
`export async function ${m.method}(request: Request, context?: unknown): Promise<Response> {`,
|
|
5023
5168
|
" await initializeActiveEnhancements();",
|
|
5024
|
-
...
|
|
5025
|
-
...wrapAtPrintWidth(condition, [
|
|
5026
|
-
" if (",
|
|
5027
|
-
...guards.map((guard, i) => ` ${guard}${i < guards.length - 1 ? " ||" : ""}`),
|
|
5028
|
-
" )"
|
|
5029
|
-
]),
|
|
5030
|
-
` ${notFoundBody}`
|
|
5031
|
-
]),
|
|
5169
|
+
...emitGuardLines(guards, "return new Response(null, { status: 404 });"),
|
|
5032
5170
|
...wrapAtPrintWidth(` const { endpointHandlers } = await import(${importPath});`, [
|
|
5033
5171
|
" const { endpointHandlers } = await import(",
|
|
5034
5172
|
` ${importPath}`,
|
|
@@ -5058,12 +5196,16 @@ function buildEndpointStubContent(input) {
|
|
|
5058
5196
|
].join("\n");
|
|
5059
5197
|
}
|
|
5060
5198
|
function buildRouteStubContent(input) {
|
|
5199
|
+
const guards = [
|
|
5200
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5201
|
+
{ call: "isMountHeadless", arg: input.mountKey }
|
|
5202
|
+
];
|
|
5061
5203
|
const lines = [
|
|
5062
5204
|
formatRouteMarker(input.owner, input.nextRelativePath),
|
|
5063
5205
|
'import { notFound } from "next/navigation";',
|
|
5064
5206
|
// simple-import-sort puts packages and "@/" aliases in separate groups.
|
|
5065
5207
|
"",
|
|
5066
|
-
'import { isModuleEnabled } from "@/modules.gen";',
|
|
5208
|
+
'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
|
|
5067
5209
|
`import Page from ${emitTsString(input.importModule)};`
|
|
5068
5210
|
];
|
|
5069
5211
|
if (input.hasGenerateMetadata) {
|
|
@@ -5071,7 +5213,7 @@ function buildRouteStubContent(input) {
|
|
|
5071
5213
|
`import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
|
|
5072
5214
|
"",
|
|
5073
5215
|
"export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
|
|
5074
|
-
|
|
5216
|
+
...emitGuardLines(guards, "return {};"),
|
|
5075
5217
|
" return sourceGenerateMetadata(...args);",
|
|
5076
5218
|
"}"
|
|
5077
5219
|
);
|
|
@@ -5082,7 +5224,7 @@ function buildRouteStubContent(input) {
|
|
|
5082
5224
|
" Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
|
|
5083
5225
|
"",
|
|
5084
5226
|
"export default function ModulePage(props: PageProps) {",
|
|
5085
|
-
|
|
5227
|
+
...emitGuardLines(guards, "notFound();"),
|
|
5086
5228
|
" return <Page {...props} />;",
|
|
5087
5229
|
"}",
|
|
5088
5230
|
""
|
|
@@ -5284,7 +5426,6 @@ function planRouteStubs(input) {
|
|
|
5284
5426
|
}
|
|
5285
5427
|
for (const entry of entries) {
|
|
5286
5428
|
const nextRelativePath = entry.nextRelativePath;
|
|
5287
|
-
analyzeRouteEntryPath(nextRelativePath);
|
|
5288
5429
|
const destination = normalizeAppRouterDestinationPattern(nextRelativePath);
|
|
5289
5430
|
const path3 = routeEntryToAppPageFile(nextRelativePath);
|
|
5290
5431
|
const existing = routeDestinationOwners.get(destination);
|
|
@@ -5299,6 +5440,8 @@ function planRouteStubs(input) {
|
|
|
5299
5440
|
path: path3
|
|
5300
5441
|
});
|
|
5301
5442
|
const importModule = routeEntryImport(regMod.name, nextRelativePath);
|
|
5443
|
+
const analyzed = analyzeRouteEntryPath(nextRelativePath);
|
|
5444
|
+
const mount = mounts.find((claim) => routeEntryCoveredByMountClaim(analyzed, claim));
|
|
5302
5445
|
routeStubs.push({
|
|
5303
5446
|
path: path3,
|
|
5304
5447
|
owner: regMod.name,
|
|
@@ -5308,6 +5451,7 @@ function planRouteStubs(input) {
|
|
|
5308
5451
|
owner: regMod.name,
|
|
5309
5452
|
nextRelativePath,
|
|
5310
5453
|
importModule,
|
|
5454
|
+
mountKey: mountKey(regMod.name, mount),
|
|
5311
5455
|
hasGenerateMetadata: entry.hasGenerateMetadata
|
|
5312
5456
|
})
|
|
5313
5457
|
});
|
|
@@ -5494,7 +5638,8 @@ function planCompositionArtifacts(input) {
|
|
|
5494
5638
|
registry: input.registry,
|
|
5495
5639
|
facts: compositionEntryFacts,
|
|
5496
5640
|
substrateLocales,
|
|
5497
|
-
moduleLayers: input.moduleLocaleLayers
|
|
5641
|
+
moduleLayers: input.moduleLocaleLayers,
|
|
5642
|
+
overrideLocales: input.overrideLocales
|
|
5498
5643
|
});
|
|
5499
5644
|
initContent = emitModuleInitServerGenTs({
|
|
5500
5645
|
registry: input.registry,
|
|
@@ -6839,7 +6984,8 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6839
6984
|
`[ModuleRail] runId=${context.runId} collecting composition entry facts modules=${registry.modules.length}`
|
|
6840
6985
|
);
|
|
6841
6986
|
const substrateDir = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
|
|
6842
|
-
const
|
|
6987
|
+
const overridesDir = `${APP_PACKAGE_DIR}/src/lib/i18n/overrides`;
|
|
6988
|
+
const localeDirs = [substrateDir, overridesDir];
|
|
6843
6989
|
const conventionalPaths = [];
|
|
6844
6990
|
for (const mod of registry.modules) {
|
|
6845
6991
|
const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
|
|
@@ -6890,6 +7036,12 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6890
7036
|
messages: await readJsonObjectFile(localePath, files)
|
|
6891
7037
|
});
|
|
6892
7038
|
}
|
|
7039
|
+
const overrideLocales = [];
|
|
7040
|
+
for (const locale of localesByDir.get(overridesDir) ?? []) {
|
|
7041
|
+
const localePath = `${overridesDir}/${locale}.json`;
|
|
7042
|
+
assertRealLocaleJsonFilePresence(localePath, localePresences.get(localePath) ?? "unreadable");
|
|
7043
|
+
overrideLocales.push({ locale, messages: await readJsonObjectFile(localePath, files) });
|
|
7044
|
+
}
|
|
6893
7045
|
const facts = [];
|
|
6894
7046
|
const moduleLocaleLayers = [];
|
|
6895
7047
|
for (const mod of registry.modules) {
|
|
@@ -7016,9 +7168,9 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7016
7168
|
facts.sort((a, b) => a.moduleName.localeCompare(b.moduleName));
|
|
7017
7169
|
moduleLocaleLayers.sort((a, b) => a.sourceLabel.localeCompare(b.sourceLabel));
|
|
7018
7170
|
console.log(
|
|
7019
|
-
`[ModuleRail] runId=${context.runId} composition entry facts modules=${facts.length} substrateLocales=${substrateLocales.length} moduleLocaleLayers=${moduleLocaleLayers.length}`
|
|
7171
|
+
`[ModuleRail] runId=${context.runId} composition entry facts modules=${facts.length} substrateLocales=${substrateLocales.length} overrideLocales=${overrideLocales.map((layer) => layer.locale).join(",") || "none"} moduleLocaleLayers=${moduleLocaleLayers.length}`
|
|
7020
7172
|
);
|
|
7021
|
-
return { facts, substrateLocales, moduleLocaleLayers };
|
|
7173
|
+
return { facts, substrateLocales, overrideLocales, moduleLocaleLayers };
|
|
7022
7174
|
}
|
|
7023
7175
|
function selectComposedModules(presentNames, manifestsByName, enabledRaw) {
|
|
7024
7176
|
if (enabledRaw) {
|
|
@@ -7158,6 +7310,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
7158
7310
|
const {
|
|
7159
7311
|
facts: compositionEntryFacts,
|
|
7160
7312
|
substrateLocales,
|
|
7313
|
+
overrideLocales,
|
|
7161
7314
|
moduleLocaleLayers
|
|
7162
7315
|
} = await collectCompositionEntryFacts(context, registry);
|
|
7163
7316
|
const candidatePaths = /* @__PURE__ */ new Set([
|
|
@@ -7180,6 +7333,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
7180
7333
|
existingAppRoutes,
|
|
7181
7334
|
compositionEntryFacts,
|
|
7182
7335
|
substrateLocales,
|
|
7336
|
+
overrideLocales,
|
|
7183
7337
|
moduleLocaleLayers,
|
|
7184
7338
|
moduleDataStoreBindings
|
|
7185
7339
|
});
|
|
@@ -7248,6 +7402,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
7248
7402
|
existingAppRoutes,
|
|
7249
7403
|
compositionEntryFacts,
|
|
7250
7404
|
substrateLocales,
|
|
7405
|
+
overrideLocales,
|
|
7251
7406
|
moduleLocaleLayers,
|
|
7252
7407
|
moduleDataStoreBindings
|
|
7253
7408
|
});
|
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
|
|
|
@@ -4276,6 +4282,40 @@ function cloneTree(value) {
|
|
|
4276
4282
|
}
|
|
4277
4283
|
return clone;
|
|
4278
4284
|
}
|
|
4285
|
+
function collectLeafPaths(value, path3, out) {
|
|
4286
|
+
if (typeof value === "string") {
|
|
4287
|
+
out.push(path3);
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
assertMessageTree(value, path3);
|
|
4291
|
+
for (const [key, child] of Object.entries(value)) collectLeafPaths(child, `${path3}.${key}`, out);
|
|
4292
|
+
}
|
|
4293
|
+
function overrideInto(target, source, owner, path3, missing) {
|
|
4294
|
+
for (const [key, value] of Object.entries(source)) {
|
|
4295
|
+
const nextPath = path3 ? `${path3}.${key}` : key;
|
|
4296
|
+
if (!Object.prototype.hasOwnProperty.call(target, key)) {
|
|
4297
|
+
collectLeafPaths(value, nextPath, missing);
|
|
4298
|
+
continue;
|
|
4299
|
+
}
|
|
4300
|
+
const existing = target[key];
|
|
4301
|
+
if (typeof value === "string") {
|
|
4302
|
+
if (typeof existing !== "string") {
|
|
4303
|
+
throw new Error(
|
|
4304
|
+
`[ModuleRail] composition artifacts: i18n merge conflict at ${nextPath}: ${owner} has string leaf but another owner has an object`
|
|
4305
|
+
);
|
|
4306
|
+
}
|
|
4307
|
+
target[key] = value;
|
|
4308
|
+
continue;
|
|
4309
|
+
}
|
|
4310
|
+
assertMessageTree(value, nextPath);
|
|
4311
|
+
if (typeof existing === "string") {
|
|
4312
|
+
throw new Error(
|
|
4313
|
+
`[ModuleRail] composition artifacts: i18n merge conflict at ${nextPath}: ${owner} has object but another owner has a string leaf`
|
|
4314
|
+
);
|
|
4315
|
+
}
|
|
4316
|
+
overrideInto(existing, value, owner, nextPath, missing);
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4279
4319
|
function composeLocaleMessagesForPlan(layers) {
|
|
4280
4320
|
const result = createMessageTree();
|
|
4281
4321
|
const leafOwners = /* @__PURE__ */ new Map();
|
|
@@ -4327,7 +4367,17 @@ function composeLocaleMessagesForPlan(layers) {
|
|
|
4327
4367
|
}
|
|
4328
4368
|
for (const layer of layers) {
|
|
4329
4369
|
assertMessageTree(layer.messages, layer.owner);
|
|
4330
|
-
|
|
4370
|
+
if (!layer.override) {
|
|
4371
|
+
mergeInto(result, cloneTree(layer.messages), layer.owner, "");
|
|
4372
|
+
continue;
|
|
4373
|
+
}
|
|
4374
|
+
const missing = [];
|
|
4375
|
+
overrideInto(result, cloneTree(layer.messages), layer.owner, "", missing);
|
|
4376
|
+
if (missing.length > 0) {
|
|
4377
|
+
throw new Error(
|
|
4378
|
+
`[ModuleRail] composition artifacts: i18n override in ${layer.owner} replaces keys that no Module or substrate defines: ${missing.join(", ")}`
|
|
4379
|
+
);
|
|
4380
|
+
}
|
|
4331
4381
|
}
|
|
4332
4382
|
return result;
|
|
4333
4383
|
}
|
|
@@ -4420,7 +4470,7 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4420
4470
|
" type SurfaceSlotCatalog,",
|
|
4421
4471
|
'} from "@/lib/modules/contributions";',
|
|
4422
4472
|
'import { substrateSlotCatalogs } from "@/lib/modules/substrate-slots";',
|
|
4423
|
-
'import { generatedModuleRegistry, isModuleEnabled } from "@/modules.gen";',
|
|
4473
|
+
'import { generatedModuleRegistry, isHrefHeadless, isModuleEnabled } from "@/modules.gen";',
|
|
4424
4474
|
...[...moduleImportLines].sort((a, b) => {
|
|
4425
4475
|
const pathA = a.match(/from ("[^"]+")/)?.[1] ?? a;
|
|
4426
4476
|
const pathB = b.match(/from ("[^"]+")/)?.[1] ?? b;
|
|
@@ -4470,11 +4520,19 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4470
4520
|
"export function resolveModuleContributions(",
|
|
4471
4521
|
" registry: typeof generatedModuleRegistry = generatedModuleRegistry",
|
|
4472
4522
|
"): ResolvedModuleContributions {",
|
|
4473
|
-
"
|
|
4523
|
+
" const resolved = resolveContributions({",
|
|
4474
4524
|
" registry,",
|
|
4475
4525
|
" catalogs: moduleSlotCatalogs,",
|
|
4476
4526
|
" sources: moduleContributionSources,",
|
|
4477
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;",
|
|
4478
4536
|
"}",
|
|
4479
4537
|
"",
|
|
4480
4538
|
"export const resolvedModuleContributions: ResolvedModuleContributions =",
|
|
@@ -4506,6 +4564,7 @@ function emitModuleI18nGenTs(input) {
|
|
|
4506
4564
|
);
|
|
4507
4565
|
}
|
|
4508
4566
|
const targetLocales = substrate.map((l) => l.locale);
|
|
4567
|
+
const overrideByLocale = indexOverrideLocales(input.overrideLocales ?? [], targetLocales);
|
|
4509
4568
|
const installed = new Set(input.registry.modules.map((m) => m.name));
|
|
4510
4569
|
const allLayers = input.moduleLayers ? [...input.moduleLayers] : deriveModuleLocaleLayers(input.registry, input.facts);
|
|
4511
4570
|
const layers = allLayers.filter((layer) => installed.has(layer.owner.split("/")[0]));
|
|
@@ -4526,19 +4585,38 @@ function emitModuleI18nGenTs(input) {
|
|
|
4526
4585
|
const messages = layer.localeMessages[locale] ?? (locale === "en" ? layer.enMessages : layer.enMessages);
|
|
4527
4586
|
composeLayers.push({ owner: layer.sourceLabel, messages });
|
|
4528
4587
|
}
|
|
4588
|
+
const override = overrideByLocale.get(locale);
|
|
4589
|
+
if (override) {
|
|
4590
|
+
composeLayers.push({
|
|
4591
|
+
owner: overrideOwner(locale),
|
|
4592
|
+
messages: override.messages,
|
|
4593
|
+
override: true
|
|
4594
|
+
});
|
|
4595
|
+
}
|
|
4529
4596
|
composeLocaleMessagesForPlan(composeLayers);
|
|
4530
4597
|
}
|
|
4531
4598
|
const composeImport = 'import { composeLocaleMessages } from "@/lib/i18n/compose-messages";';
|
|
4532
4599
|
const moduleJsonImports = [];
|
|
4533
4600
|
const relativeSubstrateImports = [];
|
|
4534
4601
|
const substrateIdentByLocale = {};
|
|
4602
|
+
const overrideIdentByLocale = {};
|
|
4535
4603
|
targetLocales.forEach((locale, localeIdx) => {
|
|
4536
4604
|
const ident = `i18nSubstrateLoc${localeIdx}`;
|
|
4537
4605
|
relativeSubstrateImports.push(
|
|
4538
4606
|
`import ${ident} from ${JSON.stringify(`./lib/i18n/substrate/${locale}.json`)};`
|
|
4539
4607
|
);
|
|
4540
4608
|
substrateIdentByLocale[locale] = ident;
|
|
4609
|
+
if (overrideByLocale.has(locale)) {
|
|
4610
|
+
const overrideIdent = `i18nOverrideLoc${localeIdx}`;
|
|
4611
|
+
relativeSubstrateImports.push(
|
|
4612
|
+
`import ${overrideIdent} from ${JSON.stringify(`./${overrideOwner(locale)}`)};`
|
|
4613
|
+
);
|
|
4614
|
+
overrideIdentByLocale[locale] = overrideIdent;
|
|
4615
|
+
}
|
|
4541
4616
|
});
|
|
4617
|
+
relativeSubstrateImports.sort(
|
|
4618
|
+
(a, b) => compareImportSpecifiers(a.match(/ from (.+);$/)[1], b.match(/ from (.+);$/)[1])
|
|
4619
|
+
);
|
|
4542
4620
|
const layerIdents = [];
|
|
4543
4621
|
layers.forEach((layer, layerIdx) => {
|
|
4544
4622
|
const localeIdents = {};
|
|
@@ -4572,15 +4650,29 @@ function emitModuleI18nGenTs(input) {
|
|
|
4572
4650
|
(layer) => ` { owner: ${JSON.stringify(layer.owner)}, messages: ${layer.localeIdents[locale]} },`
|
|
4573
4651
|
)
|
|
4574
4652
|
];
|
|
4653
|
+
const overrideIdent = overrideIdentByLocale[locale];
|
|
4654
|
+
const messageLines = overrideIdent ? [
|
|
4655
|
+
" messages: composeLocaleMessages([",
|
|
4656
|
+
" ...[",
|
|
4657
|
+
...layerLines,
|
|
4658
|
+
" ].map((layer) => ({",
|
|
4659
|
+
" ...layer,",
|
|
4660
|
+
' disabled: layer.owner !== "substrate" && !layer.owner.split("/").every(enabled),',
|
|
4661
|
+
" })),",
|
|
4662
|
+
` { owner: ${JSON.stringify(overrideOwner(locale))}, messages: ${overrideIdent}, override: true },`,
|
|
4663
|
+
" ]),"
|
|
4664
|
+
] : [
|
|
4665
|
+
" messages: composeLocaleMessages([",
|
|
4666
|
+
...layerLines,
|
|
4667
|
+
" ].filter((layer) =>",
|
|
4668
|
+
' layer.owner === "substrate" || layer.owner.split("/").every(enabled)',
|
|
4669
|
+
" )),"
|
|
4670
|
+
];
|
|
4575
4671
|
bundleBlocks.push(
|
|
4576
4672
|
[
|
|
4577
4673
|
` ${JSON.stringify(locale)}: {`,
|
|
4578
4674
|
` locale: ${JSON.stringify(locale)},`,
|
|
4579
|
-
|
|
4580
|
-
...layerLines,
|
|
4581
|
-
" ].filter((layer) =>",
|
|
4582
|
-
' layer.owner === "substrate" || layer.owner.split("/").every(enabled)',
|
|
4583
|
-
" )),",
|
|
4675
|
+
...messageLines,
|
|
4584
4676
|
" }"
|
|
4585
4677
|
].join("\n")
|
|
4586
4678
|
);
|
|
@@ -4625,6 +4717,26 @@ function emitModuleI18nGenTs(input) {
|
|
|
4625
4717
|
""
|
|
4626
4718
|
].join("\n");
|
|
4627
4719
|
}
|
|
4720
|
+
function indexOverrideLocales(overrideLocales, targetLocales) {
|
|
4721
|
+
const byLocale = /* @__PURE__ */ new Map();
|
|
4722
|
+
for (const layer of overrideLocales) {
|
|
4723
|
+
if (!isValidLocaleBasename(layer.locale)) {
|
|
4724
|
+
throw new Error(
|
|
4725
|
+
`[ModuleRail] composition artifacts: unsafe override locale basename "${layer.locale}"`
|
|
4726
|
+
);
|
|
4727
|
+
}
|
|
4728
|
+
if (!targetLocales.includes(layer.locale)) {
|
|
4729
|
+
throw new Error(
|
|
4730
|
+
`[ModuleRail] composition artifacts: ${overrideOwner(layer.locale)} has no matching lib/i18n/substrate/${layer.locale}.json; add that substrate file to make ${layer.locale} an app locale`
|
|
4731
|
+
);
|
|
4732
|
+
}
|
|
4733
|
+
byLocale.set(layer.locale, layer);
|
|
4734
|
+
}
|
|
4735
|
+
return byLocale;
|
|
4736
|
+
}
|
|
4737
|
+
function overrideOwner(locale) {
|
|
4738
|
+
return `lib/i18n/overrides/${locale}.json`;
|
|
4739
|
+
}
|
|
4628
4740
|
function deriveModuleLocaleLayers(registry, facts) {
|
|
4629
4741
|
const layers = [];
|
|
4630
4742
|
const installed = new Set(registry.modules.map((m) => m.name));
|
|
@@ -5006,6 +5118,25 @@ function emitModulesGenTs(registry) {
|
|
|
5006
5118
|
' if (entry && entry.kind !== "feature") return true;',
|
|
5007
5119
|
' return raw.split(",").includes(name);',
|
|
5008
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
|
+
"}",
|
|
5009
5140
|
""
|
|
5010
5141
|
].join("\n");
|
|
5011
5142
|
}
|
|
@@ -5013,6 +5144,22 @@ var STUB_PRINT_WIDTH = 100;
|
|
|
5013
5144
|
function wrapAtPrintWidth(flat, broken) {
|
|
5014
5145
|
return flat.length > STUB_PRINT_WIDTH ? broken : [flat];
|
|
5015
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
|
+
}
|
|
5016
5163
|
function buildEndpointStubContent(input) {
|
|
5017
5164
|
const sorted = [...input.methods].sort(
|
|
5018
5165
|
(a, b) => METHOD_ORDER[a.method] - METHOD_ORDER[b.method] || a.pairKey.localeCompare(b.pairKey)
|
|
@@ -5020,24 +5167,15 @@ function buildEndpointStubContent(input) {
|
|
|
5020
5167
|
const exports2 = sorted.map((m) => {
|
|
5021
5168
|
const pair = emitTsString(m.pairKey);
|
|
5022
5169
|
const guards = [
|
|
5023
|
-
|
|
5024
|
-
...input.scope.kind === "enhancement" ? [
|
|
5170
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5171
|
+
...input.scope.kind === "enhancement" ? [{ call: "!isModuleEnabled", arg: input.scope.peer }] : []
|
|
5025
5172
|
];
|
|
5026
|
-
const condition = ` if (${guards.join(" || ")})`;
|
|
5027
|
-
const notFoundBody = "return new Response(null, { status: 404 });";
|
|
5028
5173
|
const message = emitTsString(`Missing endpoint handler for ${m.pairKey}`);
|
|
5029
5174
|
const importPath = emitTsString(input.importModule);
|
|
5030
5175
|
return [
|
|
5031
5176
|
`export async function ${m.method}(request: Request, context?: unknown): Promise<Response> {`,
|
|
5032
5177
|
" await initializeActiveEnhancements();",
|
|
5033
|
-
...
|
|
5034
|
-
...wrapAtPrintWidth(condition, [
|
|
5035
|
-
" if (",
|
|
5036
|
-
...guards.map((guard, i) => ` ${guard}${i < guards.length - 1 ? " ||" : ""}`),
|
|
5037
|
-
" )"
|
|
5038
|
-
]),
|
|
5039
|
-
` ${notFoundBody}`
|
|
5040
|
-
]),
|
|
5178
|
+
...emitGuardLines(guards, "return new Response(null, { status: 404 });"),
|
|
5041
5179
|
...wrapAtPrintWidth(` const { endpointHandlers } = await import(${importPath});`, [
|
|
5042
5180
|
" const { endpointHandlers } = await import(",
|
|
5043
5181
|
` ${importPath}`,
|
|
@@ -5067,12 +5205,16 @@ function buildEndpointStubContent(input) {
|
|
|
5067
5205
|
].join("\n");
|
|
5068
5206
|
}
|
|
5069
5207
|
function buildRouteStubContent(input) {
|
|
5208
|
+
const guards = [
|
|
5209
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5210
|
+
{ call: "isMountHeadless", arg: input.mountKey }
|
|
5211
|
+
];
|
|
5070
5212
|
const lines = [
|
|
5071
5213
|
formatRouteMarker(input.owner, input.nextRelativePath),
|
|
5072
5214
|
'import { notFound } from "next/navigation";',
|
|
5073
5215
|
// simple-import-sort puts packages and "@/" aliases in separate groups.
|
|
5074
5216
|
"",
|
|
5075
|
-
'import { isModuleEnabled } from "@/modules.gen";',
|
|
5217
|
+
'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
|
|
5076
5218
|
`import Page from ${emitTsString(input.importModule)};`
|
|
5077
5219
|
];
|
|
5078
5220
|
if (input.hasGenerateMetadata) {
|
|
@@ -5080,7 +5222,7 @@ function buildRouteStubContent(input) {
|
|
|
5080
5222
|
`import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
|
|
5081
5223
|
"",
|
|
5082
5224
|
"export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
|
|
5083
|
-
|
|
5225
|
+
...emitGuardLines(guards, "return {};"),
|
|
5084
5226
|
" return sourceGenerateMetadata(...args);",
|
|
5085
5227
|
"}"
|
|
5086
5228
|
);
|
|
@@ -5091,7 +5233,7 @@ function buildRouteStubContent(input) {
|
|
|
5091
5233
|
" Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
|
|
5092
5234
|
"",
|
|
5093
5235
|
"export default function ModulePage(props: PageProps) {",
|
|
5094
|
-
|
|
5236
|
+
...emitGuardLines(guards, "notFound();"),
|
|
5095
5237
|
" return <Page {...props} />;",
|
|
5096
5238
|
"}",
|
|
5097
5239
|
""
|
|
@@ -5293,7 +5435,6 @@ function planRouteStubs(input) {
|
|
|
5293
5435
|
}
|
|
5294
5436
|
for (const entry of entries) {
|
|
5295
5437
|
const nextRelativePath = entry.nextRelativePath;
|
|
5296
|
-
analyzeRouteEntryPath(nextRelativePath);
|
|
5297
5438
|
const destination = normalizeAppRouterDestinationPattern(nextRelativePath);
|
|
5298
5439
|
const path3 = routeEntryToAppPageFile(nextRelativePath);
|
|
5299
5440
|
const existing = routeDestinationOwners.get(destination);
|
|
@@ -5308,6 +5449,8 @@ function planRouteStubs(input) {
|
|
|
5308
5449
|
path: path3
|
|
5309
5450
|
});
|
|
5310
5451
|
const importModule = routeEntryImport(regMod.name, nextRelativePath);
|
|
5452
|
+
const analyzed = analyzeRouteEntryPath(nextRelativePath);
|
|
5453
|
+
const mount = mounts.find((claim) => routeEntryCoveredByMountClaim(analyzed, claim));
|
|
5311
5454
|
routeStubs.push({
|
|
5312
5455
|
path: path3,
|
|
5313
5456
|
owner: regMod.name,
|
|
@@ -5317,6 +5460,7 @@ function planRouteStubs(input) {
|
|
|
5317
5460
|
owner: regMod.name,
|
|
5318
5461
|
nextRelativePath,
|
|
5319
5462
|
importModule,
|
|
5463
|
+
mountKey: mountKey(regMod.name, mount),
|
|
5320
5464
|
hasGenerateMetadata: entry.hasGenerateMetadata
|
|
5321
5465
|
})
|
|
5322
5466
|
});
|
|
@@ -5503,7 +5647,8 @@ function planCompositionArtifacts(input) {
|
|
|
5503
5647
|
registry: input.registry,
|
|
5504
5648
|
facts: compositionEntryFacts,
|
|
5505
5649
|
substrateLocales,
|
|
5506
|
-
moduleLayers: input.moduleLocaleLayers
|
|
5650
|
+
moduleLayers: input.moduleLocaleLayers,
|
|
5651
|
+
overrideLocales: input.overrideLocales
|
|
5507
5652
|
});
|
|
5508
5653
|
initContent = emitModuleInitServerGenTs({
|
|
5509
5654
|
registry: input.registry,
|
|
@@ -6848,7 +6993,8 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6848
6993
|
`[ModuleRail] runId=${context.runId} collecting composition entry facts modules=${registry.modules.length}`
|
|
6849
6994
|
);
|
|
6850
6995
|
const substrateDir = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
|
|
6851
|
-
const
|
|
6996
|
+
const overridesDir = `${APP_PACKAGE_DIR}/src/lib/i18n/overrides`;
|
|
6997
|
+
const localeDirs = [substrateDir, overridesDir];
|
|
6852
6998
|
const conventionalPaths = [];
|
|
6853
6999
|
for (const mod of registry.modules) {
|
|
6854
7000
|
const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
|
|
@@ -6899,6 +7045,12 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6899
7045
|
messages: await readJsonObjectFile(localePath, files)
|
|
6900
7046
|
});
|
|
6901
7047
|
}
|
|
7048
|
+
const overrideLocales = [];
|
|
7049
|
+
for (const locale of localesByDir.get(overridesDir) ?? []) {
|
|
7050
|
+
const localePath = `${overridesDir}/${locale}.json`;
|
|
7051
|
+
assertRealLocaleJsonFilePresence(localePath, localePresences.get(localePath) ?? "unreadable");
|
|
7052
|
+
overrideLocales.push({ locale, messages: await readJsonObjectFile(localePath, files) });
|
|
7053
|
+
}
|
|
6902
7054
|
const facts = [];
|
|
6903
7055
|
const moduleLocaleLayers = [];
|
|
6904
7056
|
for (const mod of registry.modules) {
|
|
@@ -7025,9 +7177,9 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7025
7177
|
facts.sort((a, b) => a.moduleName.localeCompare(b.moduleName));
|
|
7026
7178
|
moduleLocaleLayers.sort((a, b) => a.sourceLabel.localeCompare(b.sourceLabel));
|
|
7027
7179
|
console.log(
|
|
7028
|
-
`[ModuleRail] runId=${context.runId} composition entry facts modules=${facts.length} substrateLocales=${substrateLocales.length} moduleLocaleLayers=${moduleLocaleLayers.length}`
|
|
7180
|
+
`[ModuleRail] runId=${context.runId} composition entry facts modules=${facts.length} substrateLocales=${substrateLocales.length} overrideLocales=${overrideLocales.map((layer) => layer.locale).join(",") || "none"} moduleLocaleLayers=${moduleLocaleLayers.length}`
|
|
7029
7181
|
);
|
|
7030
|
-
return { facts, substrateLocales, moduleLocaleLayers };
|
|
7182
|
+
return { facts, substrateLocales, overrideLocales, moduleLocaleLayers };
|
|
7031
7183
|
}
|
|
7032
7184
|
function selectComposedModules(presentNames, manifestsByName, enabledRaw) {
|
|
7033
7185
|
if (enabledRaw) {
|
|
@@ -7167,6 +7319,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
7167
7319
|
const {
|
|
7168
7320
|
facts: compositionEntryFacts,
|
|
7169
7321
|
substrateLocales,
|
|
7322
|
+
overrideLocales,
|
|
7170
7323
|
moduleLocaleLayers
|
|
7171
7324
|
} = await collectCompositionEntryFacts(context, registry);
|
|
7172
7325
|
const candidatePaths = /* @__PURE__ */ new Set([
|
|
@@ -7189,6 +7342,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
7189
7342
|
existingAppRoutes,
|
|
7190
7343
|
compositionEntryFacts,
|
|
7191
7344
|
substrateLocales,
|
|
7345
|
+
overrideLocales,
|
|
7192
7346
|
moduleLocaleLayers,
|
|
7193
7347
|
moduleDataStoreBindings
|
|
7194
7348
|
});
|
|
@@ -7257,6 +7411,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
7257
7411
|
existingAppRoutes,
|
|
7258
7412
|
compositionEntryFacts,
|
|
7259
7413
|
substrateLocales,
|
|
7414
|
+
overrideLocales,
|
|
7260
7415
|
moduleLocaleLayers,
|
|
7261
7416
|
moduleDataStoreBindings
|
|
7262
7417
|
});
|
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
|
|
|
@@ -4261,6 +4267,40 @@ function cloneTree(value) {
|
|
|
4261
4267
|
}
|
|
4262
4268
|
return clone;
|
|
4263
4269
|
}
|
|
4270
|
+
function collectLeafPaths(value, path3, out) {
|
|
4271
|
+
if (typeof value === "string") {
|
|
4272
|
+
out.push(path3);
|
|
4273
|
+
return;
|
|
4274
|
+
}
|
|
4275
|
+
assertMessageTree(value, path3);
|
|
4276
|
+
for (const [key, child] of Object.entries(value)) collectLeafPaths(child, `${path3}.${key}`, out);
|
|
4277
|
+
}
|
|
4278
|
+
function overrideInto(target, source, owner, path3, missing) {
|
|
4279
|
+
for (const [key, value] of Object.entries(source)) {
|
|
4280
|
+
const nextPath = path3 ? `${path3}.${key}` : key;
|
|
4281
|
+
if (!Object.prototype.hasOwnProperty.call(target, key)) {
|
|
4282
|
+
collectLeafPaths(value, nextPath, missing);
|
|
4283
|
+
continue;
|
|
4284
|
+
}
|
|
4285
|
+
const existing = target[key];
|
|
4286
|
+
if (typeof value === "string") {
|
|
4287
|
+
if (typeof existing !== "string") {
|
|
4288
|
+
throw new Error(
|
|
4289
|
+
`[ModuleRail] composition artifacts: i18n merge conflict at ${nextPath}: ${owner} has string leaf but another owner has an object`
|
|
4290
|
+
);
|
|
4291
|
+
}
|
|
4292
|
+
target[key] = value;
|
|
4293
|
+
continue;
|
|
4294
|
+
}
|
|
4295
|
+
assertMessageTree(value, nextPath);
|
|
4296
|
+
if (typeof existing === "string") {
|
|
4297
|
+
throw new Error(
|
|
4298
|
+
`[ModuleRail] composition artifacts: i18n merge conflict at ${nextPath}: ${owner} has object but another owner has a string leaf`
|
|
4299
|
+
);
|
|
4300
|
+
}
|
|
4301
|
+
overrideInto(existing, value, owner, nextPath, missing);
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4264
4304
|
function composeLocaleMessagesForPlan(layers) {
|
|
4265
4305
|
const result = createMessageTree();
|
|
4266
4306
|
const leafOwners = /* @__PURE__ */ new Map();
|
|
@@ -4312,7 +4352,17 @@ function composeLocaleMessagesForPlan(layers) {
|
|
|
4312
4352
|
}
|
|
4313
4353
|
for (const layer of layers) {
|
|
4314
4354
|
assertMessageTree(layer.messages, layer.owner);
|
|
4315
|
-
|
|
4355
|
+
if (!layer.override) {
|
|
4356
|
+
mergeInto(result, cloneTree(layer.messages), layer.owner, "");
|
|
4357
|
+
continue;
|
|
4358
|
+
}
|
|
4359
|
+
const missing = [];
|
|
4360
|
+
overrideInto(result, cloneTree(layer.messages), layer.owner, "", missing);
|
|
4361
|
+
if (missing.length > 0) {
|
|
4362
|
+
throw new Error(
|
|
4363
|
+
`[ModuleRail] composition artifacts: i18n override in ${layer.owner} replaces keys that no Module or substrate defines: ${missing.join(", ")}`
|
|
4364
|
+
);
|
|
4365
|
+
}
|
|
4316
4366
|
}
|
|
4317
4367
|
return result;
|
|
4318
4368
|
}
|
|
@@ -4405,7 +4455,7 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4405
4455
|
" type SurfaceSlotCatalog,",
|
|
4406
4456
|
'} from "@/lib/modules/contributions";',
|
|
4407
4457
|
'import { substrateSlotCatalogs } from "@/lib/modules/substrate-slots";',
|
|
4408
|
-
'import { generatedModuleRegistry, isModuleEnabled } from "@/modules.gen";',
|
|
4458
|
+
'import { generatedModuleRegistry, isHrefHeadless, isModuleEnabled } from "@/modules.gen";',
|
|
4409
4459
|
...[...moduleImportLines].sort((a, b) => {
|
|
4410
4460
|
const pathA = a.match(/from ("[^"]+")/)?.[1] ?? a;
|
|
4411
4461
|
const pathB = b.match(/from ("[^"]+")/)?.[1] ?? b;
|
|
@@ -4455,11 +4505,19 @@ function emitModuleContributionsGenTs(input) {
|
|
|
4455
4505
|
"export function resolveModuleContributions(",
|
|
4456
4506
|
" registry: typeof generatedModuleRegistry = generatedModuleRegistry",
|
|
4457
4507
|
"): ResolvedModuleContributions {",
|
|
4458
|
-
"
|
|
4508
|
+
" const resolved = resolveContributions({",
|
|
4459
4509
|
" registry,",
|
|
4460
4510
|
" catalogs: moduleSlotCatalogs,",
|
|
4461
4511
|
" sources: moduleContributionSources,",
|
|
4462
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;",
|
|
4463
4521
|
"}",
|
|
4464
4522
|
"",
|
|
4465
4523
|
"export const resolvedModuleContributions: ResolvedModuleContributions =",
|
|
@@ -4491,6 +4549,7 @@ function emitModuleI18nGenTs(input) {
|
|
|
4491
4549
|
);
|
|
4492
4550
|
}
|
|
4493
4551
|
const targetLocales = substrate.map((l) => l.locale);
|
|
4552
|
+
const overrideByLocale = indexOverrideLocales(input.overrideLocales ?? [], targetLocales);
|
|
4494
4553
|
const installed = new Set(input.registry.modules.map((m) => m.name));
|
|
4495
4554
|
const allLayers = input.moduleLayers ? [...input.moduleLayers] : deriveModuleLocaleLayers(input.registry, input.facts);
|
|
4496
4555
|
const layers = allLayers.filter((layer) => installed.has(layer.owner.split("/")[0]));
|
|
@@ -4511,19 +4570,38 @@ function emitModuleI18nGenTs(input) {
|
|
|
4511
4570
|
const messages = layer.localeMessages[locale] ?? (locale === "en" ? layer.enMessages : layer.enMessages);
|
|
4512
4571
|
composeLayers.push({ owner: layer.sourceLabel, messages });
|
|
4513
4572
|
}
|
|
4573
|
+
const override = overrideByLocale.get(locale);
|
|
4574
|
+
if (override) {
|
|
4575
|
+
composeLayers.push({
|
|
4576
|
+
owner: overrideOwner(locale),
|
|
4577
|
+
messages: override.messages,
|
|
4578
|
+
override: true
|
|
4579
|
+
});
|
|
4580
|
+
}
|
|
4514
4581
|
composeLocaleMessagesForPlan(composeLayers);
|
|
4515
4582
|
}
|
|
4516
4583
|
const composeImport = 'import { composeLocaleMessages } from "@/lib/i18n/compose-messages";';
|
|
4517
4584
|
const moduleJsonImports = [];
|
|
4518
4585
|
const relativeSubstrateImports = [];
|
|
4519
4586
|
const substrateIdentByLocale = {};
|
|
4587
|
+
const overrideIdentByLocale = {};
|
|
4520
4588
|
targetLocales.forEach((locale, localeIdx) => {
|
|
4521
4589
|
const ident = `i18nSubstrateLoc${localeIdx}`;
|
|
4522
4590
|
relativeSubstrateImports.push(
|
|
4523
4591
|
`import ${ident} from ${JSON.stringify(`./lib/i18n/substrate/${locale}.json`)};`
|
|
4524
4592
|
);
|
|
4525
4593
|
substrateIdentByLocale[locale] = ident;
|
|
4594
|
+
if (overrideByLocale.has(locale)) {
|
|
4595
|
+
const overrideIdent = `i18nOverrideLoc${localeIdx}`;
|
|
4596
|
+
relativeSubstrateImports.push(
|
|
4597
|
+
`import ${overrideIdent} from ${JSON.stringify(`./${overrideOwner(locale)}`)};`
|
|
4598
|
+
);
|
|
4599
|
+
overrideIdentByLocale[locale] = overrideIdent;
|
|
4600
|
+
}
|
|
4526
4601
|
});
|
|
4602
|
+
relativeSubstrateImports.sort(
|
|
4603
|
+
(a, b) => compareImportSpecifiers(a.match(/ from (.+);$/)[1], b.match(/ from (.+);$/)[1])
|
|
4604
|
+
);
|
|
4527
4605
|
const layerIdents = [];
|
|
4528
4606
|
layers.forEach((layer, layerIdx) => {
|
|
4529
4607
|
const localeIdents = {};
|
|
@@ -4557,15 +4635,29 @@ function emitModuleI18nGenTs(input) {
|
|
|
4557
4635
|
(layer) => ` { owner: ${JSON.stringify(layer.owner)}, messages: ${layer.localeIdents[locale]} },`
|
|
4558
4636
|
)
|
|
4559
4637
|
];
|
|
4638
|
+
const overrideIdent = overrideIdentByLocale[locale];
|
|
4639
|
+
const messageLines = overrideIdent ? [
|
|
4640
|
+
" messages: composeLocaleMessages([",
|
|
4641
|
+
" ...[",
|
|
4642
|
+
...layerLines,
|
|
4643
|
+
" ].map((layer) => ({",
|
|
4644
|
+
" ...layer,",
|
|
4645
|
+
' disabled: layer.owner !== "substrate" && !layer.owner.split("/").every(enabled),',
|
|
4646
|
+
" })),",
|
|
4647
|
+
` { owner: ${JSON.stringify(overrideOwner(locale))}, messages: ${overrideIdent}, override: true },`,
|
|
4648
|
+
" ]),"
|
|
4649
|
+
] : [
|
|
4650
|
+
" messages: composeLocaleMessages([",
|
|
4651
|
+
...layerLines,
|
|
4652
|
+
" ].filter((layer) =>",
|
|
4653
|
+
' layer.owner === "substrate" || layer.owner.split("/").every(enabled)',
|
|
4654
|
+
" )),"
|
|
4655
|
+
];
|
|
4560
4656
|
bundleBlocks.push(
|
|
4561
4657
|
[
|
|
4562
4658
|
` ${JSON.stringify(locale)}: {`,
|
|
4563
4659
|
` locale: ${JSON.stringify(locale)},`,
|
|
4564
|
-
|
|
4565
|
-
...layerLines,
|
|
4566
|
-
" ].filter((layer) =>",
|
|
4567
|
-
' layer.owner === "substrate" || layer.owner.split("/").every(enabled)',
|
|
4568
|
-
" )),",
|
|
4660
|
+
...messageLines,
|
|
4569
4661
|
" }"
|
|
4570
4662
|
].join("\n")
|
|
4571
4663
|
);
|
|
@@ -4610,6 +4702,26 @@ function emitModuleI18nGenTs(input) {
|
|
|
4610
4702
|
""
|
|
4611
4703
|
].join("\n");
|
|
4612
4704
|
}
|
|
4705
|
+
function indexOverrideLocales(overrideLocales, targetLocales) {
|
|
4706
|
+
const byLocale = /* @__PURE__ */ new Map();
|
|
4707
|
+
for (const layer of overrideLocales) {
|
|
4708
|
+
if (!isValidLocaleBasename(layer.locale)) {
|
|
4709
|
+
throw new Error(
|
|
4710
|
+
`[ModuleRail] composition artifacts: unsafe override locale basename "${layer.locale}"`
|
|
4711
|
+
);
|
|
4712
|
+
}
|
|
4713
|
+
if (!targetLocales.includes(layer.locale)) {
|
|
4714
|
+
throw new Error(
|
|
4715
|
+
`[ModuleRail] composition artifacts: ${overrideOwner(layer.locale)} has no matching lib/i18n/substrate/${layer.locale}.json; add that substrate file to make ${layer.locale} an app locale`
|
|
4716
|
+
);
|
|
4717
|
+
}
|
|
4718
|
+
byLocale.set(layer.locale, layer);
|
|
4719
|
+
}
|
|
4720
|
+
return byLocale;
|
|
4721
|
+
}
|
|
4722
|
+
function overrideOwner(locale) {
|
|
4723
|
+
return `lib/i18n/overrides/${locale}.json`;
|
|
4724
|
+
}
|
|
4613
4725
|
function deriveModuleLocaleLayers(registry, facts) {
|
|
4614
4726
|
const layers = [];
|
|
4615
4727
|
const installed = new Set(registry.modules.map((m) => m.name));
|
|
@@ -4991,6 +5103,25 @@ function emitModulesGenTs(registry) {
|
|
|
4991
5103
|
' if (entry && entry.kind !== "feature") return true;',
|
|
4992
5104
|
' return raw.split(",").includes(name);',
|
|
4993
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
|
+
"}",
|
|
4994
5125
|
""
|
|
4995
5126
|
].join("\n");
|
|
4996
5127
|
}
|
|
@@ -4998,6 +5129,22 @@ var STUB_PRINT_WIDTH = 100;
|
|
|
4998
5129
|
function wrapAtPrintWidth(flat, broken) {
|
|
4999
5130
|
return flat.length > STUB_PRINT_WIDTH ? broken : [flat];
|
|
5000
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
|
+
}
|
|
5001
5148
|
function buildEndpointStubContent(input) {
|
|
5002
5149
|
const sorted = [...input.methods].sort(
|
|
5003
5150
|
(a, b) => METHOD_ORDER[a.method] - METHOD_ORDER[b.method] || a.pairKey.localeCompare(b.pairKey)
|
|
@@ -5005,24 +5152,15 @@ function buildEndpointStubContent(input) {
|
|
|
5005
5152
|
const exports = sorted.map((m) => {
|
|
5006
5153
|
const pair = emitTsString(m.pairKey);
|
|
5007
5154
|
const guards = [
|
|
5008
|
-
|
|
5009
|
-
...input.scope.kind === "enhancement" ? [
|
|
5155
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5156
|
+
...input.scope.kind === "enhancement" ? [{ call: "!isModuleEnabled", arg: input.scope.peer }] : []
|
|
5010
5157
|
];
|
|
5011
|
-
const condition = ` if (${guards.join(" || ")})`;
|
|
5012
|
-
const notFoundBody = "return new Response(null, { status: 404 });";
|
|
5013
5158
|
const message = emitTsString(`Missing endpoint handler for ${m.pairKey}`);
|
|
5014
5159
|
const importPath = emitTsString(input.importModule);
|
|
5015
5160
|
return [
|
|
5016
5161
|
`export async function ${m.method}(request: Request, context?: unknown): Promise<Response> {`,
|
|
5017
5162
|
" await initializeActiveEnhancements();",
|
|
5018
|
-
...
|
|
5019
|
-
...wrapAtPrintWidth(condition, [
|
|
5020
|
-
" if (",
|
|
5021
|
-
...guards.map((guard, i) => ` ${guard}${i < guards.length - 1 ? " ||" : ""}`),
|
|
5022
|
-
" )"
|
|
5023
|
-
]),
|
|
5024
|
-
` ${notFoundBody}`
|
|
5025
|
-
]),
|
|
5163
|
+
...emitGuardLines(guards, "return new Response(null, { status: 404 });"),
|
|
5026
5164
|
...wrapAtPrintWidth(` const { endpointHandlers } = await import(${importPath});`, [
|
|
5027
5165
|
" const { endpointHandlers } = await import(",
|
|
5028
5166
|
` ${importPath}`,
|
|
@@ -5052,12 +5190,16 @@ function buildEndpointStubContent(input) {
|
|
|
5052
5190
|
].join("\n");
|
|
5053
5191
|
}
|
|
5054
5192
|
function buildRouteStubContent(input) {
|
|
5193
|
+
const guards = [
|
|
5194
|
+
{ call: "!isModuleEnabled", arg: input.owner },
|
|
5195
|
+
{ call: "isMountHeadless", arg: input.mountKey }
|
|
5196
|
+
];
|
|
5055
5197
|
const lines = [
|
|
5056
5198
|
formatRouteMarker(input.owner, input.nextRelativePath),
|
|
5057
5199
|
'import { notFound } from "next/navigation";',
|
|
5058
5200
|
// simple-import-sort puts packages and "@/" aliases in separate groups.
|
|
5059
5201
|
"",
|
|
5060
|
-
'import { isModuleEnabled } from "@/modules.gen";',
|
|
5202
|
+
'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
|
|
5061
5203
|
`import Page from ${emitTsString(input.importModule)};`
|
|
5062
5204
|
];
|
|
5063
5205
|
if (input.hasGenerateMetadata) {
|
|
@@ -5065,7 +5207,7 @@ function buildRouteStubContent(input) {
|
|
|
5065
5207
|
`import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
|
|
5066
5208
|
"",
|
|
5067
5209
|
"export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
|
|
5068
|
-
|
|
5210
|
+
...emitGuardLines(guards, "return {};"),
|
|
5069
5211
|
" return sourceGenerateMetadata(...args);",
|
|
5070
5212
|
"}"
|
|
5071
5213
|
);
|
|
@@ -5076,7 +5218,7 @@ function buildRouteStubContent(input) {
|
|
|
5076
5218
|
" Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
|
|
5077
5219
|
"",
|
|
5078
5220
|
"export default function ModulePage(props: PageProps) {",
|
|
5079
|
-
|
|
5221
|
+
...emitGuardLines(guards, "notFound();"),
|
|
5080
5222
|
" return <Page {...props} />;",
|
|
5081
5223
|
"}",
|
|
5082
5224
|
""
|
|
@@ -5278,7 +5420,6 @@ function planRouteStubs(input) {
|
|
|
5278
5420
|
}
|
|
5279
5421
|
for (const entry of entries) {
|
|
5280
5422
|
const nextRelativePath = entry.nextRelativePath;
|
|
5281
|
-
analyzeRouteEntryPath(nextRelativePath);
|
|
5282
5423
|
const destination = normalizeAppRouterDestinationPattern(nextRelativePath);
|
|
5283
5424
|
const path3 = routeEntryToAppPageFile(nextRelativePath);
|
|
5284
5425
|
const existing = routeDestinationOwners.get(destination);
|
|
@@ -5293,6 +5434,8 @@ function planRouteStubs(input) {
|
|
|
5293
5434
|
path: path3
|
|
5294
5435
|
});
|
|
5295
5436
|
const importModule = routeEntryImport(regMod.name, nextRelativePath);
|
|
5437
|
+
const analyzed = analyzeRouteEntryPath(nextRelativePath);
|
|
5438
|
+
const mount = mounts.find((claim) => routeEntryCoveredByMountClaim(analyzed, claim));
|
|
5296
5439
|
routeStubs.push({
|
|
5297
5440
|
path: path3,
|
|
5298
5441
|
owner: regMod.name,
|
|
@@ -5302,6 +5445,7 @@ function planRouteStubs(input) {
|
|
|
5302
5445
|
owner: regMod.name,
|
|
5303
5446
|
nextRelativePath,
|
|
5304
5447
|
importModule,
|
|
5448
|
+
mountKey: mountKey(regMod.name, mount),
|
|
5305
5449
|
hasGenerateMetadata: entry.hasGenerateMetadata
|
|
5306
5450
|
})
|
|
5307
5451
|
});
|
|
@@ -5488,7 +5632,8 @@ function planCompositionArtifacts(input) {
|
|
|
5488
5632
|
registry: input.registry,
|
|
5489
5633
|
facts: compositionEntryFacts,
|
|
5490
5634
|
substrateLocales,
|
|
5491
|
-
moduleLayers: input.moduleLocaleLayers
|
|
5635
|
+
moduleLayers: input.moduleLocaleLayers,
|
|
5636
|
+
overrideLocales: input.overrideLocales
|
|
5492
5637
|
});
|
|
5493
5638
|
initContent = emitModuleInitServerGenTs({
|
|
5494
5639
|
registry: input.registry,
|
|
@@ -6833,7 +6978,8 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6833
6978
|
`[ModuleRail] runId=${context.runId} collecting composition entry facts modules=${registry.modules.length}`
|
|
6834
6979
|
);
|
|
6835
6980
|
const substrateDir = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
|
|
6836
|
-
const
|
|
6981
|
+
const overridesDir = `${APP_PACKAGE_DIR}/src/lib/i18n/overrides`;
|
|
6982
|
+
const localeDirs = [substrateDir, overridesDir];
|
|
6837
6983
|
const conventionalPaths = [];
|
|
6838
6984
|
for (const mod of registry.modules) {
|
|
6839
6985
|
const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
|
|
@@ -6884,6 +7030,12 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6884
7030
|
messages: await readJsonObjectFile(localePath, files)
|
|
6885
7031
|
});
|
|
6886
7032
|
}
|
|
7033
|
+
const overrideLocales = [];
|
|
7034
|
+
for (const locale of localesByDir.get(overridesDir) ?? []) {
|
|
7035
|
+
const localePath = `${overridesDir}/${locale}.json`;
|
|
7036
|
+
assertRealLocaleJsonFilePresence(localePath, localePresences.get(localePath) ?? "unreadable");
|
|
7037
|
+
overrideLocales.push({ locale, messages: await readJsonObjectFile(localePath, files) });
|
|
7038
|
+
}
|
|
6887
7039
|
const facts = [];
|
|
6888
7040
|
const moduleLocaleLayers = [];
|
|
6889
7041
|
for (const mod of registry.modules) {
|
|
@@ -7010,9 +7162,9 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7010
7162
|
facts.sort((a, b) => a.moduleName.localeCompare(b.moduleName));
|
|
7011
7163
|
moduleLocaleLayers.sort((a, b) => a.sourceLabel.localeCompare(b.sourceLabel));
|
|
7012
7164
|
console.log(
|
|
7013
|
-
`[ModuleRail] runId=${context.runId} composition entry facts modules=${facts.length} substrateLocales=${substrateLocales.length} moduleLocaleLayers=${moduleLocaleLayers.length}`
|
|
7165
|
+
`[ModuleRail] runId=${context.runId} composition entry facts modules=${facts.length} substrateLocales=${substrateLocales.length} overrideLocales=${overrideLocales.map((layer) => layer.locale).join(",") || "none"} moduleLocaleLayers=${moduleLocaleLayers.length}`
|
|
7014
7166
|
);
|
|
7015
|
-
return { facts, substrateLocales, moduleLocaleLayers };
|
|
7167
|
+
return { facts, substrateLocales, overrideLocales, moduleLocaleLayers };
|
|
7016
7168
|
}
|
|
7017
7169
|
function selectComposedModules(presentNames, manifestsByName, enabledRaw) {
|
|
7018
7170
|
if (enabledRaw) {
|
|
@@ -7152,6 +7304,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
7152
7304
|
const {
|
|
7153
7305
|
facts: compositionEntryFacts,
|
|
7154
7306
|
substrateLocales,
|
|
7307
|
+
overrideLocales,
|
|
7155
7308
|
moduleLocaleLayers
|
|
7156
7309
|
} = await collectCompositionEntryFacts(context, registry);
|
|
7157
7310
|
const candidatePaths = /* @__PURE__ */ new Set([
|
|
@@ -7174,6 +7327,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
7174
7327
|
existingAppRoutes,
|
|
7175
7328
|
compositionEntryFacts,
|
|
7176
7329
|
substrateLocales,
|
|
7330
|
+
overrideLocales,
|
|
7177
7331
|
moduleLocaleLayers,
|
|
7178
7332
|
moduleDataStoreBindings
|
|
7179
7333
|
});
|
|
@@ -7242,6 +7396,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
7242
7396
|
existingAppRoutes,
|
|
7243
7397
|
compositionEntryFacts,
|
|
7244
7398
|
substrateLocales,
|
|
7399
|
+
overrideLocales,
|
|
7245
7400
|
moduleLocaleLayers,
|
|
7246
7401
|
moduleDataStoreBindings
|
|
7247
7402
|
});
|
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",
|