@stardeck-customer-apps/compose 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +35 -1
- package/dist/cli.js +94 -33
- package/dist/index.js +94 -33
- package/dist/index.mjs +94 -33
- package/package.json +2 -2
package/SKILL.md
CHANGED
|
@@ -12,7 +12,9 @@ Compose turns that declaration into the code Next.js actually builds:
|
|
|
12
12
|
- `apps/web/src/modules.gen.ts` — the Module registry, `isModuleEnabled` and `isMountHeadless`
|
|
13
13
|
- `apps/web/src/module-contributions.gen.ts`
|
|
14
14
|
- `apps/web/src/module-i18n.gen.ts`
|
|
15
|
-
- `apps/web/src/module-init.server.gen.ts`
|
|
15
|
+
- `apps/web/src/module-init.server.gen.ts` — memoized `initializeActiveEnhancements()`:
|
|
16
|
+
each enabled Module's root `initializeModule`, then each active slice's
|
|
17
|
+
`initializeEnhancement`
|
|
16
18
|
- `apps/web/src/module-datastores.gen.ts`
|
|
17
19
|
- `apps/web/src/module-server.gen.ts` — `MODULE_SERVER_REGISTRATIONS`, one entry
|
|
18
20
|
per composed Module that ships `server/registrations.ts`
|
|
@@ -149,6 +151,38 @@ a silent fallback. It holds deployment facts: gitignore it (`apps/web/.gitignore
|
|
|
149
151
|
the bundle. Compose does not check the export names or shapes. The file
|
|
150
152
|
counts as `server/**/*.ts`, so the Module also needs `server/index.ts`.
|
|
151
153
|
Needs compose 0.8.0 or later.
|
|
154
|
+
- **Module initializers.** Server registrations a Module makes into shared
|
|
155
|
+
registries (observers, resolvers, facets, drains) go in a root
|
|
156
|
+
`initialize.server.ts`, any Module kind:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// src/modules/guest-ordering/initialize.server.ts — server-only, idempotent
|
|
160
|
+
import { registerPosCloseObserver } from "@/modules/pos/server";
|
|
161
|
+
|
|
162
|
+
import { wakeReconcileForClosedOrder } from "./server/reconcile";
|
|
163
|
+
|
|
164
|
+
export async function initializeModule() {
|
|
165
|
+
registerPosCloseObserver(wakeReconcileForClosedOrder);
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`initializeModule` must be a direct named function declaration callable with
|
|
170
|
+
zero required arguments (no arrow function, default export, re-export or
|
|
171
|
+
generator); compose fails otherwise. Compose imports it into
|
|
172
|
+
`module-init.server.gen.ts`, guarded by `isModuleEnabled("<module>")`, and
|
|
173
|
+
runs every Module initializer before any enhancement initializer, once per
|
|
174
|
+
instance. Every generated page stub (its render and its `generateMetadata`)
|
|
175
|
+
and every generated API route stub awaits `initializeActiveEnhancements()`
|
|
176
|
+
before its enablement guards, so the registrations exist in the route's own
|
|
177
|
+
module graph. Page stubs await it from compose 0.14.0; before that only API
|
|
178
|
+
stubs did. An `initialize()` export
|
|
179
|
+
run only from `instrumentation.ts` does not give you that: Next.js loads
|
|
180
|
+
instrumentation in a separate module graph, so on a cold instance the route
|
|
181
|
+
handlers and server components never see what it registered. A Module left
|
|
182
|
+
out by `--enabled` is not imported. Needs compose 0.14.0 or later (and
|
|
183
|
+
`@stardeck-customer-apps/eslint-plugin` 1.8.0 or later, whose
|
|
184
|
+
`no-module-internal-import` lets the generated file import it).
|
|
185
|
+
|
|
152
186
|
- `@stardeck-customer-apps/compose/runtime` exports `isModuleEnabledByList`,
|
|
153
187
|
the single enablement rule, and imports nothing — it is safe in client
|
|
154
188
|
components.
|
package/dist/cli.js
CHANGED
|
@@ -2249,14 +2249,22 @@ var datastoreRequirementSchema = import_zod.z.object({
|
|
|
2249
2249
|
/** Checked against the connected store's accessLevel at install. */
|
|
2250
2250
|
access: import_zod.z.enum(["read", "write", "admin"]).default("write")
|
|
2251
2251
|
}).strict();
|
|
2252
|
-
var
|
|
2252
|
+
var MODULE_PERMISSION_DEFAULT_ROLES = ["manager", "member", "viewer"];
|
|
2253
|
+
var modulePermissionClaimSchema = import_zod.z.object({
|
|
2253
2254
|
/**
|
|
2254
2255
|
* Lowercase resource:action with one colon and no underscores. This grammar
|
|
2255
2256
|
* is deliberately narrower than the platform permission tool's charset.
|
|
2256
2257
|
*/
|
|
2257
2258
|
key: import_zod.z.string().regex(/^[a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*$/, "must be a lowercase resource:action key").max(100, "must be at most 100 characters (platform permission tool limit)"),
|
|
2258
2259
|
/** Human label for the deployment auth config row. */
|
|
2259
|
-
description: descriptionSchema
|
|
2260
|
+
description: descriptionSchema,
|
|
2261
|
+
/**
|
|
2262
|
+
* The default organization roles that hold this key when the platform
|
|
2263
|
+
* first registers it on an app (a Blueprint fork, a Module install). Admin
|
|
2264
|
+
* holds every key without being listed; External Viewer never holds one.
|
|
2265
|
+
* Changing a role's grants afterwards is the organization's business.
|
|
2266
|
+
*/
|
|
2267
|
+
roles: import_zod.z.array(import_zod.z.enum(MODULE_PERMISSION_DEFAULT_ROLES)).optional()
|
|
2260
2268
|
}).strict();
|
|
2261
2269
|
var seedBundleSchema = import_zod.z.object({
|
|
2262
2270
|
table: tableNameSchema,
|
|
@@ -2339,7 +2347,7 @@ var moduleManifestSchema = import_zod.z.object({
|
|
|
2339
2347
|
* Keys this module's guards enforce. Install registers them in deployment
|
|
2340
2348
|
* auth config; registration only — declaring is never granting.
|
|
2341
2349
|
*/
|
|
2342
|
-
permissions: import_zod.z.array(
|
|
2350
|
+
permissions: import_zod.z.array(modulePermissionClaimSchema).optional(),
|
|
2343
2351
|
/**
|
|
2344
2352
|
* Optional starter rows for tables this module owns. Replay is
|
|
2345
2353
|
* `INSERT ... ON CONFLICT (conflictTarget) DO NOTHING` — idempotent by
|
|
@@ -3552,17 +3560,20 @@ var import_typescript3 = __toESM(require("typescript"), 1);
|
|
|
3552
3560
|
var CONVENTIONAL_ENTRY_EXPORT_NAME = {
|
|
3553
3561
|
contributions: "contributions",
|
|
3554
3562
|
slotCatalogs: "slotCatalogs",
|
|
3555
|
-
initializeEnhancement: "initializeEnhancement"
|
|
3563
|
+
initializeEnhancement: "initializeEnhancement",
|
|
3564
|
+
initializeModule: "initializeModule"
|
|
3556
3565
|
};
|
|
3557
3566
|
var CONVENTIONAL_ENTRY_DECLARATION_KIND = {
|
|
3558
3567
|
contributions: "variable",
|
|
3559
3568
|
slotCatalogs: "variable",
|
|
3560
|
-
initializeEnhancement: "function"
|
|
3569
|
+
initializeEnhancement: "function",
|
|
3570
|
+
initializeModule: "function"
|
|
3561
3571
|
};
|
|
3562
3572
|
var CONVENTIONAL_ENTRY_FILENAME = {
|
|
3563
3573
|
contributions: "contributions.ts",
|
|
3564
3574
|
slotCatalogs: "slots.ts",
|
|
3565
|
-
initializeEnhancement: "initialize.server.ts"
|
|
3575
|
+
initializeEnhancement: "initialize.server.ts",
|
|
3576
|
+
initializeModule: "initialize.server.ts"
|
|
3566
3577
|
};
|
|
3567
3578
|
function hasModifierKind(modifiers, kind) {
|
|
3568
3579
|
return Boolean(modifiers?.some((m) => m.kind === kind));
|
|
@@ -3661,7 +3672,7 @@ function analyzeConventionalEntryExportWithTs(typescript, sourceText, kind, file
|
|
|
3661
3672
|
}
|
|
3662
3673
|
if (!isZeroArgumentInvocable(typescript, stmt.parameters)) {
|
|
3663
3674
|
errors.push(
|
|
3664
|
-
`"${entryName}" must be invocable with zero arguments (generated caller uses
|
|
3675
|
+
`"${entryName}" must be invocable with zero arguments (generated caller uses ${entryName}()); required parameters are forbidden \u2014 use optional, default, rest, or a this-parameter only`
|
|
3665
3676
|
);
|
|
3666
3677
|
continue;
|
|
3667
3678
|
}
|
|
@@ -3961,11 +3972,18 @@ ${result.error}`);
|
|
|
3961
3972
|
}
|
|
3962
3973
|
|
|
3963
3974
|
// ../../packages/lib/src/server/sandbox/redact-secrets.ts
|
|
3975
|
+
var SECRET_NAME = String.raw`[A-Z][A-Z0-9_]*(?:_SECRET|_SECRET_KEY|_API_KEY|_KEY|_TOKEN|_PASSWORD)`;
|
|
3976
|
+
var SECRET_ASSIGNMENT = new RegExp(
|
|
3977
|
+
String.raw`\b(${SECRET_NAME})\s*=\s*(?:'(?:[^']|'\\'')*'|"[^"]*"|[^\s&"',;}\]]+)`,
|
|
3978
|
+
"g"
|
|
3979
|
+
);
|
|
3980
|
+
var JSON_SECRET_PAIR = new RegExp(
|
|
3981
|
+
String.raw`"(${SECRET_NAME})"(\s*:\s*)"(?:[^"\\\n]|\\.)*"`,
|
|
3982
|
+
"g"
|
|
3983
|
+
);
|
|
3984
|
+
var SECRET_QUOTE_OPENER = new RegExp(String.raw`\b${SECRET_NAME}\s*=\s*(['"])`, "g");
|
|
3964
3985
|
function redactSecrets(text) {
|
|
3965
|
-
return text.replace(/x-access-token:[^@\s]+@/g, "x-access-token:***@").replace(/https:\/\/(?!x-access-token:)[^/\s:@]+:[^/\s@]+@/gi, "https://***:***@").replace(/\b(gh[pousr]_)[A-Za-z0-9]+/g, "$1***").replace(/\b(github_pat_)[A-Za-z0-9_]+/g, "$1***").replace(/\b(postgres(?:ql)?:\/\/)([^:\s/@]+):([^@\s/]+)@/gi, "$1$2:***@").replace(/\b(npm_[A-Za-z0-9]+)/g, "npm_***").replace(/(_authToken=)([^\s&]+)/gi, "$1***").replace(
|
|
3966
|
-
/\b([A-Z][A-Z0-9_]*(?:_SECRET|_SECRET_KEY|_API_KEY|_KEY|_TOKEN|_PASSWORD))\s*=\s*(?:'(?:[^']|'\\'')*'|"[^"]*"|[^\s&"',;}\]]+)/g,
|
|
3967
|
-
"$1=***"
|
|
3968
|
-
).replace(/\b(Authorization:\s*(?:Bearer|Basic)\s+)\S+/gi, "$1***");
|
|
3986
|
+
return text.replace(/x-access-token:[^@\s]+@/g, "x-access-token:***@").replace(/https:\/\/(?!x-access-token:)[^/\s:@]+:[^/\s@]+@/gi, "https://***:***@").replace(/\b(gh[pousr]_)[A-Za-z0-9]+/g, "$1***").replace(/\b(github_pat_)[A-Za-z0-9_]+/g, "$1***").replace(/\b(postgres(?:ql)?:\/\/)([^:\s/@]+):([^@\s/]+)@/gi, "$1$2:***@").replace(/\b(npm_[A-Za-z0-9]+)/g, "npm_***").replace(/(_authToken=)([^\s&]+)/gi, "$1***").replace(SECRET_ASSIGNMENT, "$1=***").replace(JSON_SECRET_PAIR, '"$1"$2"***"').replace(/\b(Authorization:\s*(?:Bearer|Basic)\s+)\S+/gi, "$1***");
|
|
3969
3987
|
}
|
|
3970
3988
|
|
|
3971
3989
|
// ../../packages/lib/src/server/sandbox/sh-quote.ts
|
|
@@ -4135,6 +4153,9 @@ function assertSlotCatalogsExport(source, pathLabel) {
|
|
|
4135
4153
|
function assertInitializeEnhancementExport(source, pathLabel) {
|
|
4136
4154
|
assertConventionalEntryExport(source, pathLabel, "initializeEnhancement");
|
|
4137
4155
|
}
|
|
4156
|
+
function assertInitializeModuleExport(source, pathLabel) {
|
|
4157
|
+
assertConventionalEntryExport(source, pathLabel, "initializeModule");
|
|
4158
|
+
}
|
|
4138
4159
|
function assertClientSafeConventionalEntryImports(source, pathLabel, kind) {
|
|
4139
4160
|
const { analysisErrors } = analyzeClientSafeEntryImports(
|
|
4140
4161
|
source,
|
|
@@ -4654,30 +4675,47 @@ function deriveModuleLocaleLayers(registry, facts) {
|
|
|
4654
4675
|
}
|
|
4655
4676
|
return layers;
|
|
4656
4677
|
}
|
|
4657
|
-
function
|
|
4678
|
+
function moduleInitializerCalls(registry, facts) {
|
|
4679
|
+
const installed = new Set(registry.modules.map((m) => m.name));
|
|
4680
|
+
return facts.filter((f) => f.hasRootInitializeServer && installed.has(f.moduleName)).map((f) => f.moduleName).sort(compareNames).map((name, index) => {
|
|
4681
|
+
if (!isValidModuleName(name)) {
|
|
4682
|
+
throw new Error(`[ModuleRail] composition artifacts: invalid module name "${name}"`);
|
|
4683
|
+
}
|
|
4684
|
+
return {
|
|
4685
|
+
exportName: "initializeModule",
|
|
4686
|
+
guard: [name],
|
|
4687
|
+
ident: `initializeModule${index}`,
|
|
4688
|
+
importPath: `@/modules/${name}/initialize.server`
|
|
4689
|
+
};
|
|
4690
|
+
});
|
|
4691
|
+
}
|
|
4692
|
+
function enhancementInitializerCalls(registry, facts) {
|
|
4658
4693
|
const calls = [];
|
|
4659
|
-
const installed = new Set(
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
const
|
|
4664
|
-
for (const enh of [...facts.enhancements].sort((a, b) => compareNames(a.peer, b.peer))) {
|
|
4694
|
+
const installed = new Set(registry.modules.map((m) => m.name));
|
|
4695
|
+
for (const f of [...facts].sort((a, b) => compareNames(a.moduleName, b.moduleName))) {
|
|
4696
|
+
if (!installed.has(f.moduleName)) continue;
|
|
4697
|
+
const peers = activePeers(registry, f.moduleName);
|
|
4698
|
+
for (const enh of [...f.enhancements].sort((a, b) => compareNames(a.peer, b.peer))) {
|
|
4665
4699
|
if (!enh.active || !peers.has(enh.peer)) continue;
|
|
4666
4700
|
if (!enh.hasInitializeServer) continue;
|
|
4667
|
-
const ident = `initializeEnhancement${initializerIndex++}`;
|
|
4668
4701
|
calls.push({
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
ident
|
|
4672
|
-
importPath: `@/modules/${
|
|
4702
|
+
exportName: "initializeEnhancement",
|
|
4703
|
+
guard: [f.moduleName, enh.peer],
|
|
4704
|
+
ident: `initializeEnhancement${calls.length}`,
|
|
4705
|
+
importPath: `@/modules/${f.moduleName}/enhancements/${enh.peer}/initialize.server`
|
|
4673
4706
|
});
|
|
4674
4707
|
}
|
|
4675
4708
|
}
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4709
|
+
return calls;
|
|
4710
|
+
}
|
|
4711
|
+
function emitModuleInitServerGenTs(input) {
|
|
4712
|
+
const moduleCalls = moduleInitializerCalls(input.registry, input.facts);
|
|
4713
|
+
const enhancementCalls = enhancementInitializerCalls(input.registry, input.facts);
|
|
4714
|
+
const calls = [...moduleCalls, ...enhancementCalls];
|
|
4715
|
+
const importFor = (c) => `import { ${c.exportName} as ${c.ident} } from ${JSON.stringify(c.importPath)};`;
|
|
4716
|
+
const importLines = moduleCalls.length === 0 ? calls.map(importFor) : [...calls].sort((a, b) => compareImportSpecifiers(a.importPath, b.importPath)).map(importFor);
|
|
4679
4717
|
const callLines = calls.length === 0 ? [" // Empty composition \u2014 no active enhancement initializers."] : calls.map(
|
|
4680
|
-
(c) => ` if (
|
|
4718
|
+
(c) => ` if (${c.guard.map((name) => `enabled(${JSON.stringify(name)})`).join(" && ")}) await ${c.ident}();`
|
|
4681
4719
|
);
|
|
4682
4720
|
return [
|
|
4683
4721
|
formatInitMarker(),
|
|
@@ -5127,6 +5165,7 @@ function buildRouteStubContent(input) {
|
|
|
5127
5165
|
'import { notFound } from "next/navigation";',
|
|
5128
5166
|
// simple-import-sort puts packages and "@/" aliases in separate groups.
|
|
5129
5167
|
"",
|
|
5168
|
+
'import { initializeActiveEnhancements } from "@/module-init.server.gen";',
|
|
5130
5169
|
'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
|
|
5131
5170
|
`import Page from ${emitTsString(input.importModule)};`
|
|
5132
5171
|
];
|
|
@@ -5135,6 +5174,7 @@ function buildRouteStubContent(input) {
|
|
|
5135
5174
|
`import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
|
|
5136
5175
|
"",
|
|
5137
5176
|
"export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
|
|
5177
|
+
" await initializeActiveEnhancements();",
|
|
5138
5178
|
...emitGuardLines(guards, "return {};"),
|
|
5139
5179
|
" return sourceGenerateMetadata(...args);",
|
|
5140
5180
|
"}"
|
|
@@ -5145,7 +5185,10 @@ function buildRouteStubContent(input) {
|
|
|
5145
5185
|
"type PageProps =",
|
|
5146
5186
|
" Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
|
|
5147
5187
|
"",
|
|
5148
|
-
|
|
5188
|
+
// Module and enhancement initializers run in this route's own module graph
|
|
5189
|
+
// (memoized), so a cold server render sees their registrations.
|
|
5190
|
+
"export default async function ModulePage(props: PageProps) {",
|
|
5191
|
+
" await initializeActiveEnhancements();",
|
|
5149
5192
|
...emitGuardLines(guards, "notFound();"),
|
|
5150
5193
|
" return <Page {...props} />;",
|
|
5151
5194
|
"}",
|
|
@@ -5945,11 +5988,10 @@ var creationSourceBlueprintSchema = import_zod7.z.object({
|
|
|
5945
5988
|
* `null`/absent means every feature Module: the platform stores that as the
|
|
5946
5989
|
* explicit list of feature Modules in the published Blueprint version the fork
|
|
5947
5990
|
* is cut from, so a Module a later Blueprint update adds arrives off. `[]` is
|
|
5948
|
-
*
|
|
5949
|
-
*
|
|
5950
|
-
* back as "all" (`module-enablement.ts`) — the opposite of an empty selection.
|
|
5991
|
+
* a custom app: no feature Module, Library Modules only (`resolvePlatformEnv`
|
|
5992
|
+
* emits it as a set value naming no Module).
|
|
5951
5993
|
*/
|
|
5952
|
-
enabledModules: import_zod7.z.array(moduleNameSchema).
|
|
5994
|
+
enabledModules: import_zod7.z.array(moduleNameSchema).max(100).nullable().optional()
|
|
5953
5995
|
});
|
|
5954
5996
|
var creationSourceSchema = import_zod7.z.discriminatedUnion("type", [
|
|
5955
5997
|
creationSourceBlankSchema,
|
|
@@ -6944,6 +6986,9 @@ async function readConventionalEntryIfRealFile(path5, kind, presences, files) {
|
|
|
6944
6986
|
case "initializeEnhancement":
|
|
6945
6987
|
assertInitializeEnhancementExport(source, path5);
|
|
6946
6988
|
break;
|
|
6989
|
+
case "initializeModule":
|
|
6990
|
+
assertInitializeModuleExport(source, path5);
|
|
6991
|
+
break;
|
|
6947
6992
|
}
|
|
6948
6993
|
return source;
|
|
6949
6994
|
}
|
|
@@ -6980,7 +7025,11 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6980
7025
|
for (const mod of registry.modules) {
|
|
6981
7026
|
const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
|
|
6982
7027
|
localeDirs.push(`${modulePath}/i18n`);
|
|
6983
|
-
conventionalPaths.push(
|
|
7028
|
+
conventionalPaths.push(
|
|
7029
|
+
`${modulePath}/contributions.ts`,
|
|
7030
|
+
`${modulePath}/slots.ts`,
|
|
7031
|
+
`${modulePath}/initialize.server.ts`
|
|
7032
|
+
);
|
|
6984
7033
|
for (const enh of mod.enhancements) {
|
|
6985
7034
|
const peerDir = `${modulePath}/enhancements/${enh.peer}`;
|
|
6986
7035
|
localeDirs.push(`${peerDir}/i18n`);
|
|
@@ -7068,6 +7117,17 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7068
7117
|
}
|
|
7069
7118
|
}
|
|
7070
7119
|
const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
|
|
7120
|
+
const hasRootInitializeServer = await readConventionalEntryIfRealFile(
|
|
7121
|
+
`${modulePath}/initialize.server.ts`,
|
|
7122
|
+
"initializeModule",
|
|
7123
|
+
conventionalPresences,
|
|
7124
|
+
files
|
|
7125
|
+
) != null;
|
|
7126
|
+
if (hasRootInitializeServer) {
|
|
7127
|
+
console.log(
|
|
7128
|
+
`[ModuleRail] runId=${context.runId} module=${mod.name} has root initialize.server.ts (initializeModule)`
|
|
7129
|
+
);
|
|
7130
|
+
}
|
|
7071
7131
|
const rootLocales = localesByDir.get(`${modulePath}/i18n`);
|
|
7072
7132
|
if (rootLocales.length > 0 && !rootLocales.includes("en")) {
|
|
7073
7133
|
throw new Error(
|
|
@@ -7108,6 +7168,7 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7108
7168
|
hasRootContributions,
|
|
7109
7169
|
hasRootSlots,
|
|
7110
7170
|
hasServerRegistrations,
|
|
7171
|
+
hasRootInitializeServer,
|
|
7111
7172
|
rootLocales,
|
|
7112
7173
|
enhancements
|
|
7113
7174
|
});
|
package/dist/index.js
CHANGED
|
@@ -2087,11 +2087,18 @@ ${result.error}`);
|
|
|
2087
2087
|
}
|
|
2088
2088
|
|
|
2089
2089
|
// ../../packages/lib/src/server/sandbox/redact-secrets.ts
|
|
2090
|
+
var SECRET_NAME = String.raw`[A-Z][A-Z0-9_]*(?:_SECRET|_SECRET_KEY|_API_KEY|_KEY|_TOKEN|_PASSWORD)`;
|
|
2091
|
+
var SECRET_ASSIGNMENT = new RegExp(
|
|
2092
|
+
String.raw`\b(${SECRET_NAME})\s*=\s*(?:'(?:[^']|'\\'')*'|"[^"]*"|[^\s&"',;}\]]+)`,
|
|
2093
|
+
"g"
|
|
2094
|
+
);
|
|
2095
|
+
var JSON_SECRET_PAIR = new RegExp(
|
|
2096
|
+
String.raw`"(${SECRET_NAME})"(\s*:\s*)"(?:[^"\\\n]|\\.)*"`,
|
|
2097
|
+
"g"
|
|
2098
|
+
);
|
|
2099
|
+
var SECRET_QUOTE_OPENER = new RegExp(String.raw`\b${SECRET_NAME}\s*=\s*(['"])`, "g");
|
|
2090
2100
|
function redactSecrets(text) {
|
|
2091
|
-
return text.replace(/x-access-token:[^@\s]+@/g, "x-access-token:***@").replace(/https:\/\/(?!x-access-token:)[^/\s:@]+:[^/\s@]+@/gi, "https://***:***@").replace(/\b(gh[pousr]_)[A-Za-z0-9]+/g, "$1***").replace(/\b(github_pat_)[A-Za-z0-9_]+/g, "$1***").replace(/\b(postgres(?:ql)?:\/\/)([^:\s/@]+):([^@\s/]+)@/gi, "$1$2:***@").replace(/\b(npm_[A-Za-z0-9]+)/g, "npm_***").replace(/(_authToken=)([^\s&]+)/gi, "$1***").replace(
|
|
2092
|
-
/\b([A-Z][A-Z0-9_]*(?:_SECRET|_SECRET_KEY|_API_KEY|_KEY|_TOKEN|_PASSWORD))\s*=\s*(?:'(?:[^']|'\\'')*'|"[^"]*"|[^\s&"',;}\]]+)/g,
|
|
2093
|
-
"$1=***"
|
|
2094
|
-
).replace(/\b(Authorization:\s*(?:Bearer|Basic)\s+)\S+/gi, "$1***");
|
|
2101
|
+
return text.replace(/x-access-token:[^@\s]+@/g, "x-access-token:***@").replace(/https:\/\/(?!x-access-token:)[^/\s:@]+:[^/\s@]+@/gi, "https://***:***@").replace(/\b(gh[pousr]_)[A-Za-z0-9]+/g, "$1***").replace(/\b(github_pat_)[A-Za-z0-9_]+/g, "$1***").replace(/\b(postgres(?:ql)?:\/\/)([^:\s/@]+):([^@\s/]+)@/gi, "$1$2:***@").replace(/\b(npm_[A-Za-z0-9]+)/g, "npm_***").replace(/(_authToken=)([^\s&]+)/gi, "$1***").replace(SECRET_ASSIGNMENT, "$1=***").replace(JSON_SECRET_PAIR, '"$1"$2"***"').replace(/\b(Authorization:\s*(?:Bearer|Basic)\s+)\S+/gi, "$1***");
|
|
2095
2102
|
}
|
|
2096
2103
|
|
|
2097
2104
|
// ../../packages/lib/src/server/sandbox/sh-quote.ts
|
|
@@ -2371,14 +2378,22 @@ var datastoreRequirementSchema = import_zod2.z.object({
|
|
|
2371
2378
|
/** Checked against the connected store's accessLevel at install. */
|
|
2372
2379
|
access: import_zod2.z.enum(["read", "write", "admin"]).default("write")
|
|
2373
2380
|
}).strict();
|
|
2374
|
-
var
|
|
2381
|
+
var MODULE_PERMISSION_DEFAULT_ROLES = ["manager", "member", "viewer"];
|
|
2382
|
+
var modulePermissionClaimSchema = import_zod2.z.object({
|
|
2375
2383
|
/**
|
|
2376
2384
|
* Lowercase resource:action with one colon and no underscores. This grammar
|
|
2377
2385
|
* is deliberately narrower than the platform permission tool's charset.
|
|
2378
2386
|
*/
|
|
2379
2387
|
key: import_zod2.z.string().regex(/^[a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*$/, "must be a lowercase resource:action key").max(100, "must be at most 100 characters (platform permission tool limit)"),
|
|
2380
2388
|
/** Human label for the deployment auth config row. */
|
|
2381
|
-
description: descriptionSchema
|
|
2389
|
+
description: descriptionSchema,
|
|
2390
|
+
/**
|
|
2391
|
+
* The default organization roles that hold this key when the platform
|
|
2392
|
+
* first registers it on an app (a Blueprint fork, a Module install). Admin
|
|
2393
|
+
* holds every key without being listed; External Viewer never holds one.
|
|
2394
|
+
* Changing a role's grants afterwards is the organization's business.
|
|
2395
|
+
*/
|
|
2396
|
+
roles: import_zod2.z.array(import_zod2.z.enum(MODULE_PERMISSION_DEFAULT_ROLES)).optional()
|
|
2382
2397
|
}).strict();
|
|
2383
2398
|
var seedBundleSchema = import_zod2.z.object({
|
|
2384
2399
|
table: tableNameSchema,
|
|
@@ -2461,7 +2476,7 @@ var moduleManifestSchema = import_zod2.z.object({
|
|
|
2461
2476
|
* Keys this module's guards enforce. Install registers them in deployment
|
|
2462
2477
|
* auth config; registration only — declaring is never granting.
|
|
2463
2478
|
*/
|
|
2464
|
-
permissions: import_zod2.z.array(
|
|
2479
|
+
permissions: import_zod2.z.array(modulePermissionClaimSchema).optional(),
|
|
2465
2480
|
/**
|
|
2466
2481
|
* Optional starter rows for tables this module owns. Replay is
|
|
2467
2482
|
* `INSERT ... ON CONFLICT (conflictTarget) DO NOTHING` — idempotent by
|
|
@@ -3674,17 +3689,20 @@ var import_typescript3 = __toESM(require("typescript"), 1);
|
|
|
3674
3689
|
var CONVENTIONAL_ENTRY_EXPORT_NAME = {
|
|
3675
3690
|
contributions: "contributions",
|
|
3676
3691
|
slotCatalogs: "slotCatalogs",
|
|
3677
|
-
initializeEnhancement: "initializeEnhancement"
|
|
3692
|
+
initializeEnhancement: "initializeEnhancement",
|
|
3693
|
+
initializeModule: "initializeModule"
|
|
3678
3694
|
};
|
|
3679
3695
|
var CONVENTIONAL_ENTRY_DECLARATION_KIND = {
|
|
3680
3696
|
contributions: "variable",
|
|
3681
3697
|
slotCatalogs: "variable",
|
|
3682
|
-
initializeEnhancement: "function"
|
|
3698
|
+
initializeEnhancement: "function",
|
|
3699
|
+
initializeModule: "function"
|
|
3683
3700
|
};
|
|
3684
3701
|
var CONVENTIONAL_ENTRY_FILENAME = {
|
|
3685
3702
|
contributions: "contributions.ts",
|
|
3686
3703
|
slotCatalogs: "slots.ts",
|
|
3687
|
-
initializeEnhancement: "initialize.server.ts"
|
|
3704
|
+
initializeEnhancement: "initialize.server.ts",
|
|
3705
|
+
initializeModule: "initialize.server.ts"
|
|
3688
3706
|
};
|
|
3689
3707
|
function hasModifierKind(modifiers, kind) {
|
|
3690
3708
|
return Boolean(modifiers?.some((m) => m.kind === kind));
|
|
@@ -3783,7 +3801,7 @@ function analyzeConventionalEntryExportWithTs(typescript, sourceText, kind, file
|
|
|
3783
3801
|
}
|
|
3784
3802
|
if (!isZeroArgumentInvocable(typescript, stmt.parameters)) {
|
|
3785
3803
|
errors.push(
|
|
3786
|
-
`"${entryName}" must be invocable with zero arguments (generated caller uses
|
|
3804
|
+
`"${entryName}" must be invocable with zero arguments (generated caller uses ${entryName}()); required parameters are forbidden \u2014 use optional, default, rest, or a this-parameter only`
|
|
3787
3805
|
);
|
|
3788
3806
|
continue;
|
|
3789
3807
|
}
|
|
@@ -4145,6 +4163,9 @@ function assertSlotCatalogsExport(source, pathLabel) {
|
|
|
4145
4163
|
function assertInitializeEnhancementExport(source, pathLabel) {
|
|
4146
4164
|
assertConventionalEntryExport(source, pathLabel, "initializeEnhancement");
|
|
4147
4165
|
}
|
|
4166
|
+
function assertInitializeModuleExport(source, pathLabel) {
|
|
4167
|
+
assertConventionalEntryExport(source, pathLabel, "initializeModule");
|
|
4168
|
+
}
|
|
4148
4169
|
function assertClientSafeConventionalEntryImports(source, pathLabel, kind) {
|
|
4149
4170
|
const { analysisErrors } = analyzeClientSafeEntryImports(
|
|
4150
4171
|
source,
|
|
@@ -4664,30 +4685,47 @@ function deriveModuleLocaleLayers(registry, facts) {
|
|
|
4664
4685
|
}
|
|
4665
4686
|
return layers;
|
|
4666
4687
|
}
|
|
4667
|
-
function
|
|
4688
|
+
function moduleInitializerCalls(registry, facts) {
|
|
4689
|
+
const installed = new Set(registry.modules.map((m) => m.name));
|
|
4690
|
+
return facts.filter((f) => f.hasRootInitializeServer && installed.has(f.moduleName)).map((f) => f.moduleName).sort(compareNames).map((name, index) => {
|
|
4691
|
+
if (!isValidModuleName(name)) {
|
|
4692
|
+
throw new Error(`[ModuleRail] composition artifacts: invalid module name "${name}"`);
|
|
4693
|
+
}
|
|
4694
|
+
return {
|
|
4695
|
+
exportName: "initializeModule",
|
|
4696
|
+
guard: [name],
|
|
4697
|
+
ident: `initializeModule${index}`,
|
|
4698
|
+
importPath: `@/modules/${name}/initialize.server`
|
|
4699
|
+
};
|
|
4700
|
+
});
|
|
4701
|
+
}
|
|
4702
|
+
function enhancementInitializerCalls(registry, facts) {
|
|
4668
4703
|
const calls = [];
|
|
4669
|
-
const installed = new Set(
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
const
|
|
4674
|
-
for (const enh of [...facts.enhancements].sort((a, b) => compareNames(a.peer, b.peer))) {
|
|
4704
|
+
const installed = new Set(registry.modules.map((m) => m.name));
|
|
4705
|
+
for (const f of [...facts].sort((a, b) => compareNames(a.moduleName, b.moduleName))) {
|
|
4706
|
+
if (!installed.has(f.moduleName)) continue;
|
|
4707
|
+
const peers = activePeers(registry, f.moduleName);
|
|
4708
|
+
for (const enh of [...f.enhancements].sort((a, b) => compareNames(a.peer, b.peer))) {
|
|
4675
4709
|
if (!enh.active || !peers.has(enh.peer)) continue;
|
|
4676
4710
|
if (!enh.hasInitializeServer) continue;
|
|
4677
|
-
const ident = `initializeEnhancement${initializerIndex++}`;
|
|
4678
4711
|
calls.push({
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
ident
|
|
4682
|
-
importPath: `@/modules/${
|
|
4712
|
+
exportName: "initializeEnhancement",
|
|
4713
|
+
guard: [f.moduleName, enh.peer],
|
|
4714
|
+
ident: `initializeEnhancement${calls.length}`,
|
|
4715
|
+
importPath: `@/modules/${f.moduleName}/enhancements/${enh.peer}/initialize.server`
|
|
4683
4716
|
});
|
|
4684
4717
|
}
|
|
4685
4718
|
}
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4719
|
+
return calls;
|
|
4720
|
+
}
|
|
4721
|
+
function emitModuleInitServerGenTs(input) {
|
|
4722
|
+
const moduleCalls = moduleInitializerCalls(input.registry, input.facts);
|
|
4723
|
+
const enhancementCalls = enhancementInitializerCalls(input.registry, input.facts);
|
|
4724
|
+
const calls = [...moduleCalls, ...enhancementCalls];
|
|
4725
|
+
const importFor = (c) => `import { ${c.exportName} as ${c.ident} } from ${JSON.stringify(c.importPath)};`;
|
|
4726
|
+
const importLines = moduleCalls.length === 0 ? calls.map(importFor) : [...calls].sort((a, b) => compareImportSpecifiers(a.importPath, b.importPath)).map(importFor);
|
|
4689
4727
|
const callLines = calls.length === 0 ? [" // Empty composition \u2014 no active enhancement initializers."] : calls.map(
|
|
4690
|
-
(c) => ` if (
|
|
4728
|
+
(c) => ` if (${c.guard.map((name) => `enabled(${JSON.stringify(name)})`).join(" && ")}) await ${c.ident}();`
|
|
4691
4729
|
);
|
|
4692
4730
|
return [
|
|
4693
4731
|
formatInitMarker(),
|
|
@@ -5137,6 +5175,7 @@ function buildRouteStubContent(input) {
|
|
|
5137
5175
|
'import { notFound } from "next/navigation";',
|
|
5138
5176
|
// simple-import-sort puts packages and "@/" aliases in separate groups.
|
|
5139
5177
|
"",
|
|
5178
|
+
'import { initializeActiveEnhancements } from "@/module-init.server.gen";',
|
|
5140
5179
|
'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
|
|
5141
5180
|
`import Page from ${emitTsString(input.importModule)};`
|
|
5142
5181
|
];
|
|
@@ -5145,6 +5184,7 @@ function buildRouteStubContent(input) {
|
|
|
5145
5184
|
`import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
|
|
5146
5185
|
"",
|
|
5147
5186
|
"export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
|
|
5187
|
+
" await initializeActiveEnhancements();",
|
|
5148
5188
|
...emitGuardLines(guards, "return {};"),
|
|
5149
5189
|
" return sourceGenerateMetadata(...args);",
|
|
5150
5190
|
"}"
|
|
@@ -5155,7 +5195,10 @@ function buildRouteStubContent(input) {
|
|
|
5155
5195
|
"type PageProps =",
|
|
5156
5196
|
" Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
|
|
5157
5197
|
"",
|
|
5158
|
-
|
|
5198
|
+
// Module and enhancement initializers run in this route's own module graph
|
|
5199
|
+
// (memoized), so a cold server render sees their registrations.
|
|
5200
|
+
"export default async function ModulePage(props: PageProps) {",
|
|
5201
|
+
" await initializeActiveEnhancements();",
|
|
5159
5202
|
...emitGuardLines(guards, "notFound();"),
|
|
5160
5203
|
" return <Page {...props} />;",
|
|
5161
5204
|
"}",
|
|
@@ -5955,11 +5998,10 @@ var creationSourceBlueprintSchema = import_zod7.z.object({
|
|
|
5955
5998
|
* `null`/absent means every feature Module: the platform stores that as the
|
|
5956
5999
|
* explicit list of feature Modules in the published Blueprint version the fork
|
|
5957
6000
|
* is cut from, so a Module a later Blueprint update adds arrives off. `[]` is
|
|
5958
|
-
*
|
|
5959
|
-
*
|
|
5960
|
-
* back as "all" (`module-enablement.ts`) — the opposite of an empty selection.
|
|
6001
|
+
* a custom app: no feature Module, Library Modules only (`resolvePlatformEnv`
|
|
6002
|
+
* emits it as a set value naming no Module).
|
|
5961
6003
|
*/
|
|
5962
|
-
enabledModules: import_zod7.z.array(moduleNameSchema).
|
|
6004
|
+
enabledModules: import_zod7.z.array(moduleNameSchema).max(100).nullable().optional()
|
|
5963
6005
|
});
|
|
5964
6006
|
var creationSourceSchema = import_zod7.z.discriminatedUnion("type", [
|
|
5965
6007
|
creationSourceBlankSchema,
|
|
@@ -6954,6 +6996,9 @@ async function readConventionalEntryIfRealFile(path4, kind, presences, files) {
|
|
|
6954
6996
|
case "initializeEnhancement":
|
|
6955
6997
|
assertInitializeEnhancementExport(source, path4);
|
|
6956
6998
|
break;
|
|
6999
|
+
case "initializeModule":
|
|
7000
|
+
assertInitializeModuleExport(source, path4);
|
|
7001
|
+
break;
|
|
6957
7002
|
}
|
|
6958
7003
|
return source;
|
|
6959
7004
|
}
|
|
@@ -6990,7 +7035,11 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6990
7035
|
for (const mod of registry.modules) {
|
|
6991
7036
|
const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
|
|
6992
7037
|
localeDirs.push(`${modulePath}/i18n`);
|
|
6993
|
-
conventionalPaths.push(
|
|
7038
|
+
conventionalPaths.push(
|
|
7039
|
+
`${modulePath}/contributions.ts`,
|
|
7040
|
+
`${modulePath}/slots.ts`,
|
|
7041
|
+
`${modulePath}/initialize.server.ts`
|
|
7042
|
+
);
|
|
6994
7043
|
for (const enh of mod.enhancements) {
|
|
6995
7044
|
const peerDir = `${modulePath}/enhancements/${enh.peer}`;
|
|
6996
7045
|
localeDirs.push(`${peerDir}/i18n`);
|
|
@@ -7078,6 +7127,17 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7078
7127
|
}
|
|
7079
7128
|
}
|
|
7080
7129
|
const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
|
|
7130
|
+
const hasRootInitializeServer = await readConventionalEntryIfRealFile(
|
|
7131
|
+
`${modulePath}/initialize.server.ts`,
|
|
7132
|
+
"initializeModule",
|
|
7133
|
+
conventionalPresences,
|
|
7134
|
+
files
|
|
7135
|
+
) != null;
|
|
7136
|
+
if (hasRootInitializeServer) {
|
|
7137
|
+
console.log(
|
|
7138
|
+
`[ModuleRail] runId=${context.runId} module=${mod.name} has root initialize.server.ts (initializeModule)`
|
|
7139
|
+
);
|
|
7140
|
+
}
|
|
7081
7141
|
const rootLocales = localesByDir.get(`${modulePath}/i18n`);
|
|
7082
7142
|
if (rootLocales.length > 0 && !rootLocales.includes("en")) {
|
|
7083
7143
|
throw new Error(
|
|
@@ -7118,6 +7178,7 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7118
7178
|
hasRootContributions,
|
|
7119
7179
|
hasRootSlots,
|
|
7120
7180
|
hasServerRegistrations,
|
|
7181
|
+
hasRootInitializeServer,
|
|
7121
7182
|
rootLocales,
|
|
7122
7183
|
enhancements
|
|
7123
7184
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -2068,11 +2068,18 @@ ${result.error}`);
|
|
|
2068
2068
|
}
|
|
2069
2069
|
|
|
2070
2070
|
// ../../packages/lib/src/server/sandbox/redact-secrets.ts
|
|
2071
|
+
var SECRET_NAME = String.raw`[A-Z][A-Z0-9_]*(?:_SECRET|_SECRET_KEY|_API_KEY|_KEY|_TOKEN|_PASSWORD)`;
|
|
2072
|
+
var SECRET_ASSIGNMENT = new RegExp(
|
|
2073
|
+
String.raw`\b(${SECRET_NAME})\s*=\s*(?:'(?:[^']|'\\'')*'|"[^"]*"|[^\s&"',;}\]]+)`,
|
|
2074
|
+
"g"
|
|
2075
|
+
);
|
|
2076
|
+
var JSON_SECRET_PAIR = new RegExp(
|
|
2077
|
+
String.raw`"(${SECRET_NAME})"(\s*:\s*)"(?:[^"\\\n]|\\.)*"`,
|
|
2078
|
+
"g"
|
|
2079
|
+
);
|
|
2080
|
+
var SECRET_QUOTE_OPENER = new RegExp(String.raw`\b${SECRET_NAME}\s*=\s*(['"])`, "g");
|
|
2071
2081
|
function redactSecrets(text) {
|
|
2072
|
-
return text.replace(/x-access-token:[^@\s]+@/g, "x-access-token:***@").replace(/https:\/\/(?!x-access-token:)[^/\s:@]+:[^/\s@]+@/gi, "https://***:***@").replace(/\b(gh[pousr]_)[A-Za-z0-9]+/g, "$1***").replace(/\b(github_pat_)[A-Za-z0-9_]+/g, "$1***").replace(/\b(postgres(?:ql)?:\/\/)([^:\s/@]+):([^@\s/]+)@/gi, "$1$2:***@").replace(/\b(npm_[A-Za-z0-9]+)/g, "npm_***").replace(/(_authToken=)([^\s&]+)/gi, "$1***").replace(
|
|
2073
|
-
/\b([A-Z][A-Z0-9_]*(?:_SECRET|_SECRET_KEY|_API_KEY|_KEY|_TOKEN|_PASSWORD))\s*=\s*(?:'(?:[^']|'\\'')*'|"[^"]*"|[^\s&"',;}\]]+)/g,
|
|
2074
|
-
"$1=***"
|
|
2075
|
-
).replace(/\b(Authorization:\s*(?:Bearer|Basic)\s+)\S+/gi, "$1***");
|
|
2082
|
+
return text.replace(/x-access-token:[^@\s]+@/g, "x-access-token:***@").replace(/https:\/\/(?!x-access-token:)[^/\s:@]+:[^/\s@]+@/gi, "https://***:***@").replace(/\b(gh[pousr]_)[A-Za-z0-9]+/g, "$1***").replace(/\b(github_pat_)[A-Za-z0-9_]+/g, "$1***").replace(/\b(postgres(?:ql)?:\/\/)([^:\s/@]+):([^@\s/]+)@/gi, "$1$2:***@").replace(/\b(npm_[A-Za-z0-9]+)/g, "npm_***").replace(/(_authToken=)([^\s&]+)/gi, "$1***").replace(SECRET_ASSIGNMENT, "$1=***").replace(JSON_SECRET_PAIR, '"$1"$2"***"').replace(/\b(Authorization:\s*(?:Bearer|Basic)\s+)\S+/gi, "$1***");
|
|
2076
2083
|
}
|
|
2077
2084
|
|
|
2078
2085
|
// ../../packages/lib/src/server/sandbox/sh-quote.ts
|
|
@@ -2352,14 +2359,22 @@ var datastoreRequirementSchema = z2.object({
|
|
|
2352
2359
|
/** Checked against the connected store's accessLevel at install. */
|
|
2353
2360
|
access: z2.enum(["read", "write", "admin"]).default("write")
|
|
2354
2361
|
}).strict();
|
|
2355
|
-
var
|
|
2362
|
+
var MODULE_PERMISSION_DEFAULT_ROLES = ["manager", "member", "viewer"];
|
|
2363
|
+
var modulePermissionClaimSchema = z2.object({
|
|
2356
2364
|
/**
|
|
2357
2365
|
* Lowercase resource:action with one colon and no underscores. This grammar
|
|
2358
2366
|
* is deliberately narrower than the platform permission tool's charset.
|
|
2359
2367
|
*/
|
|
2360
2368
|
key: z2.string().regex(/^[a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*$/, "must be a lowercase resource:action key").max(100, "must be at most 100 characters (platform permission tool limit)"),
|
|
2361
2369
|
/** Human label for the deployment auth config row. */
|
|
2362
|
-
description: descriptionSchema
|
|
2370
|
+
description: descriptionSchema,
|
|
2371
|
+
/**
|
|
2372
|
+
* The default organization roles that hold this key when the platform
|
|
2373
|
+
* first registers it on an app (a Blueprint fork, a Module install). Admin
|
|
2374
|
+
* holds every key without being listed; External Viewer never holds one.
|
|
2375
|
+
* Changing a role's grants afterwards is the organization's business.
|
|
2376
|
+
*/
|
|
2377
|
+
roles: z2.array(z2.enum(MODULE_PERMISSION_DEFAULT_ROLES)).optional()
|
|
2363
2378
|
}).strict();
|
|
2364
2379
|
var seedBundleSchema = z2.object({
|
|
2365
2380
|
table: tableNameSchema,
|
|
@@ -2442,7 +2457,7 @@ var moduleManifestSchema = z2.object({
|
|
|
2442
2457
|
* Keys this module's guards enforce. Install registers them in deployment
|
|
2443
2458
|
* auth config; registration only — declaring is never granting.
|
|
2444
2459
|
*/
|
|
2445
|
-
permissions: z2.array(
|
|
2460
|
+
permissions: z2.array(modulePermissionClaimSchema).optional(),
|
|
2446
2461
|
/**
|
|
2447
2462
|
* Optional starter rows for tables this module owns. Replay is
|
|
2448
2463
|
* `INSERT ... ON CONFLICT (conflictTarget) DO NOTHING` — idempotent by
|
|
@@ -3655,17 +3670,20 @@ import ts3 from "typescript";
|
|
|
3655
3670
|
var CONVENTIONAL_ENTRY_EXPORT_NAME = {
|
|
3656
3671
|
contributions: "contributions",
|
|
3657
3672
|
slotCatalogs: "slotCatalogs",
|
|
3658
|
-
initializeEnhancement: "initializeEnhancement"
|
|
3673
|
+
initializeEnhancement: "initializeEnhancement",
|
|
3674
|
+
initializeModule: "initializeModule"
|
|
3659
3675
|
};
|
|
3660
3676
|
var CONVENTIONAL_ENTRY_DECLARATION_KIND = {
|
|
3661
3677
|
contributions: "variable",
|
|
3662
3678
|
slotCatalogs: "variable",
|
|
3663
|
-
initializeEnhancement: "function"
|
|
3679
|
+
initializeEnhancement: "function",
|
|
3680
|
+
initializeModule: "function"
|
|
3664
3681
|
};
|
|
3665
3682
|
var CONVENTIONAL_ENTRY_FILENAME = {
|
|
3666
3683
|
contributions: "contributions.ts",
|
|
3667
3684
|
slotCatalogs: "slots.ts",
|
|
3668
|
-
initializeEnhancement: "initialize.server.ts"
|
|
3685
|
+
initializeEnhancement: "initialize.server.ts",
|
|
3686
|
+
initializeModule: "initialize.server.ts"
|
|
3669
3687
|
};
|
|
3670
3688
|
function hasModifierKind(modifiers, kind) {
|
|
3671
3689
|
return Boolean(modifiers?.some((m) => m.kind === kind));
|
|
@@ -3764,7 +3782,7 @@ function analyzeConventionalEntryExportWithTs(typescript, sourceText, kind, file
|
|
|
3764
3782
|
}
|
|
3765
3783
|
if (!isZeroArgumentInvocable(typescript, stmt.parameters)) {
|
|
3766
3784
|
errors.push(
|
|
3767
|
-
`"${entryName}" must be invocable with zero arguments (generated caller uses
|
|
3785
|
+
`"${entryName}" must be invocable with zero arguments (generated caller uses ${entryName}()); required parameters are forbidden \u2014 use optional, default, rest, or a this-parameter only`
|
|
3768
3786
|
);
|
|
3769
3787
|
continue;
|
|
3770
3788
|
}
|
|
@@ -4126,6 +4144,9 @@ function assertSlotCatalogsExport(source, pathLabel) {
|
|
|
4126
4144
|
function assertInitializeEnhancementExport(source, pathLabel) {
|
|
4127
4145
|
assertConventionalEntryExport(source, pathLabel, "initializeEnhancement");
|
|
4128
4146
|
}
|
|
4147
|
+
function assertInitializeModuleExport(source, pathLabel) {
|
|
4148
|
+
assertConventionalEntryExport(source, pathLabel, "initializeModule");
|
|
4149
|
+
}
|
|
4129
4150
|
function assertClientSafeConventionalEntryImports(source, pathLabel, kind) {
|
|
4130
4151
|
const { analysisErrors } = analyzeClientSafeEntryImports(
|
|
4131
4152
|
source,
|
|
@@ -4645,30 +4666,47 @@ function deriveModuleLocaleLayers(registry, facts) {
|
|
|
4645
4666
|
}
|
|
4646
4667
|
return layers;
|
|
4647
4668
|
}
|
|
4648
|
-
function
|
|
4669
|
+
function moduleInitializerCalls(registry, facts) {
|
|
4670
|
+
const installed = new Set(registry.modules.map((m) => m.name));
|
|
4671
|
+
return facts.filter((f) => f.hasRootInitializeServer && installed.has(f.moduleName)).map((f) => f.moduleName).sort(compareNames).map((name, index) => {
|
|
4672
|
+
if (!isValidModuleName(name)) {
|
|
4673
|
+
throw new Error(`[ModuleRail] composition artifacts: invalid module name "${name}"`);
|
|
4674
|
+
}
|
|
4675
|
+
return {
|
|
4676
|
+
exportName: "initializeModule",
|
|
4677
|
+
guard: [name],
|
|
4678
|
+
ident: `initializeModule${index}`,
|
|
4679
|
+
importPath: `@/modules/${name}/initialize.server`
|
|
4680
|
+
};
|
|
4681
|
+
});
|
|
4682
|
+
}
|
|
4683
|
+
function enhancementInitializerCalls(registry, facts) {
|
|
4649
4684
|
const calls = [];
|
|
4650
|
-
const installed = new Set(
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
const
|
|
4655
|
-
for (const enh of [...facts.enhancements].sort((a, b) => compareNames(a.peer, b.peer))) {
|
|
4685
|
+
const installed = new Set(registry.modules.map((m) => m.name));
|
|
4686
|
+
for (const f of [...facts].sort((a, b) => compareNames(a.moduleName, b.moduleName))) {
|
|
4687
|
+
if (!installed.has(f.moduleName)) continue;
|
|
4688
|
+
const peers = activePeers(registry, f.moduleName);
|
|
4689
|
+
for (const enh of [...f.enhancements].sort((a, b) => compareNames(a.peer, b.peer))) {
|
|
4656
4690
|
if (!enh.active || !peers.has(enh.peer)) continue;
|
|
4657
4691
|
if (!enh.hasInitializeServer) continue;
|
|
4658
|
-
const ident = `initializeEnhancement${initializerIndex++}`;
|
|
4659
4692
|
calls.push({
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
ident
|
|
4663
|
-
importPath: `@/modules/${
|
|
4693
|
+
exportName: "initializeEnhancement",
|
|
4694
|
+
guard: [f.moduleName, enh.peer],
|
|
4695
|
+
ident: `initializeEnhancement${calls.length}`,
|
|
4696
|
+
importPath: `@/modules/${f.moduleName}/enhancements/${enh.peer}/initialize.server`
|
|
4664
4697
|
});
|
|
4665
4698
|
}
|
|
4666
4699
|
}
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4700
|
+
return calls;
|
|
4701
|
+
}
|
|
4702
|
+
function emitModuleInitServerGenTs(input) {
|
|
4703
|
+
const moduleCalls = moduleInitializerCalls(input.registry, input.facts);
|
|
4704
|
+
const enhancementCalls = enhancementInitializerCalls(input.registry, input.facts);
|
|
4705
|
+
const calls = [...moduleCalls, ...enhancementCalls];
|
|
4706
|
+
const importFor = (c) => `import { ${c.exportName} as ${c.ident} } from ${JSON.stringify(c.importPath)};`;
|
|
4707
|
+
const importLines = moduleCalls.length === 0 ? calls.map(importFor) : [...calls].sort((a, b) => compareImportSpecifiers(a.importPath, b.importPath)).map(importFor);
|
|
4670
4708
|
const callLines = calls.length === 0 ? [" // Empty composition \u2014 no active enhancement initializers."] : calls.map(
|
|
4671
|
-
(c) => ` if (
|
|
4709
|
+
(c) => ` if (${c.guard.map((name) => `enabled(${JSON.stringify(name)})`).join(" && ")}) await ${c.ident}();`
|
|
4672
4710
|
);
|
|
4673
4711
|
return [
|
|
4674
4712
|
formatInitMarker(),
|
|
@@ -5118,6 +5156,7 @@ function buildRouteStubContent(input) {
|
|
|
5118
5156
|
'import { notFound } from "next/navigation";',
|
|
5119
5157
|
// simple-import-sort puts packages and "@/" aliases in separate groups.
|
|
5120
5158
|
"",
|
|
5159
|
+
'import { initializeActiveEnhancements } from "@/module-init.server.gen";',
|
|
5121
5160
|
'import { isModuleEnabled, isMountHeadless } from "@/modules.gen";',
|
|
5122
5161
|
`import Page from ${emitTsString(input.importModule)};`
|
|
5123
5162
|
];
|
|
@@ -5126,6 +5165,7 @@ function buildRouteStubContent(input) {
|
|
|
5126
5165
|
`import { generateMetadata as sourceGenerateMetadata } from ${emitTsString(input.importModule)};`,
|
|
5127
5166
|
"",
|
|
5128
5167
|
"export async function generateMetadata(...args: Parameters<typeof sourceGenerateMetadata>) {",
|
|
5168
|
+
" await initializeActiveEnhancements();",
|
|
5129
5169
|
...emitGuardLines(guards, "return {};"),
|
|
5130
5170
|
" return sourceGenerateMetadata(...args);",
|
|
5131
5171
|
"}"
|
|
@@ -5136,7 +5176,10 @@ function buildRouteStubContent(input) {
|
|
|
5136
5176
|
"type PageProps =",
|
|
5137
5177
|
" Parameters<typeof Page> extends [infer P, ...unknown[]] ? NonNullable<P> : Record<never, never>;",
|
|
5138
5178
|
"",
|
|
5139
|
-
|
|
5179
|
+
// Module and enhancement initializers run in this route's own module graph
|
|
5180
|
+
// (memoized), so a cold server render sees their registrations.
|
|
5181
|
+
"export default async function ModulePage(props: PageProps) {",
|
|
5182
|
+
" await initializeActiveEnhancements();",
|
|
5140
5183
|
...emitGuardLines(guards, "notFound();"),
|
|
5141
5184
|
" return <Page {...props} />;",
|
|
5142
5185
|
"}",
|
|
@@ -5936,11 +5979,10 @@ var creationSourceBlueprintSchema = z7.object({
|
|
|
5936
5979
|
* `null`/absent means every feature Module: the platform stores that as the
|
|
5937
5980
|
* explicit list of feature Modules in the published Blueprint version the fork
|
|
5938
5981
|
* is cut from, so a Module a later Blueprint update adds arrives off. `[]` is
|
|
5939
|
-
*
|
|
5940
|
-
*
|
|
5941
|
-
* back as "all" (`module-enablement.ts`) — the opposite of an empty selection.
|
|
5982
|
+
* a custom app: no feature Module, Library Modules only (`resolvePlatformEnv`
|
|
5983
|
+
* emits it as a set value naming no Module).
|
|
5942
5984
|
*/
|
|
5943
|
-
enabledModules: z7.array(moduleNameSchema).
|
|
5985
|
+
enabledModules: z7.array(moduleNameSchema).max(100).nullable().optional()
|
|
5944
5986
|
});
|
|
5945
5987
|
var creationSourceSchema = z7.discriminatedUnion("type", [
|
|
5946
5988
|
creationSourceBlankSchema,
|
|
@@ -6935,6 +6977,9 @@ async function readConventionalEntryIfRealFile(path4, kind, presences, files) {
|
|
|
6935
6977
|
case "initializeEnhancement":
|
|
6936
6978
|
assertInitializeEnhancementExport(source, path4);
|
|
6937
6979
|
break;
|
|
6980
|
+
case "initializeModule":
|
|
6981
|
+
assertInitializeModuleExport(source, path4);
|
|
6982
|
+
break;
|
|
6938
6983
|
}
|
|
6939
6984
|
return source;
|
|
6940
6985
|
}
|
|
@@ -6971,7 +7016,11 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
6971
7016
|
for (const mod of registry.modules) {
|
|
6972
7017
|
const modulePath = `${MODULES_SANDBOX_DIR}/${mod.name}`;
|
|
6973
7018
|
localeDirs.push(`${modulePath}/i18n`);
|
|
6974
|
-
conventionalPaths.push(
|
|
7019
|
+
conventionalPaths.push(
|
|
7020
|
+
`${modulePath}/contributions.ts`,
|
|
7021
|
+
`${modulePath}/slots.ts`,
|
|
7022
|
+
`${modulePath}/initialize.server.ts`
|
|
7023
|
+
);
|
|
6975
7024
|
for (const enh of mod.enhancements) {
|
|
6976
7025
|
const peerDir = `${modulePath}/enhancements/${enh.peer}`;
|
|
6977
7026
|
localeDirs.push(`${peerDir}/i18n`);
|
|
@@ -7059,6 +7108,17 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7059
7108
|
}
|
|
7060
7109
|
}
|
|
7061
7110
|
const hasServerRegistrations = hasRealServerRegistrations(modulePath, conventionalPresences);
|
|
7111
|
+
const hasRootInitializeServer = await readConventionalEntryIfRealFile(
|
|
7112
|
+
`${modulePath}/initialize.server.ts`,
|
|
7113
|
+
"initializeModule",
|
|
7114
|
+
conventionalPresences,
|
|
7115
|
+
files
|
|
7116
|
+
) != null;
|
|
7117
|
+
if (hasRootInitializeServer) {
|
|
7118
|
+
console.log(
|
|
7119
|
+
`[ModuleRail] runId=${context.runId} module=${mod.name} has root initialize.server.ts (initializeModule)`
|
|
7120
|
+
);
|
|
7121
|
+
}
|
|
7062
7122
|
const rootLocales = localesByDir.get(`${modulePath}/i18n`);
|
|
7063
7123
|
if (rootLocales.length > 0 && !rootLocales.includes("en")) {
|
|
7064
7124
|
throw new Error(
|
|
@@ -7099,6 +7159,7 @@ async function collectCompositionEntryFacts(context, registry) {
|
|
|
7099
7159
|
hasRootContributions,
|
|
7100
7160
|
hasRootSlots,
|
|
7101
7161
|
hasServerRegistrations,
|
|
7162
|
+
hasRootInitializeServer,
|
|
7102
7163
|
rootLocales,
|
|
7103
7164
|
enhancements
|
|
7104
7165
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/compose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Regenerates a Stardeck app's Module-rail artifacts — the six src/*.gen.ts registries and every route/endpoint stub — from src/modules/*/module.json",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"build": "rm -rf dist && tsup",
|
|
35
35
|
"dev": "tsup --watch",
|
|
36
36
|
"format": "prettier --write . && oxlint . --fix",
|
|
37
|
-
"typecheck": "
|
|
37
|
+
"typecheck": "tsgo -p tsconfig.test.json",
|
|
38
38
|
"lint": "oxlint src/",
|
|
39
39
|
"test": "vitest run",
|
|
40
40
|
"test:watch": "vitest"
|