@stardeck-customer-apps/compose 0.1.0 → 0.2.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 +33 -6
- package/dist/cli.js +96 -4
- package/dist/index.d.mts +54 -1
- package/dist/index.d.ts +54 -1
- package/dist/index.js +101 -5
- package/dist/index.mjs +96 -4
- package/package.json +2 -2
package/SKILL.md
CHANGED
|
@@ -28,12 +28,30 @@ it manually — `npx stardeck-compose` before `next dev` / `next build` / your
|
|
|
28
28
|
typecheck — after changing any `module.json`. The `predev` / `prebuild` /
|
|
29
29
|
`pretypecheck` / `pretest` wiring that makes this automatic has not landed yet.
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
Needs `typescript` >=5 <7 (it parses your route entries with the compiler API).
|
|
32
|
+
|
|
33
|
+
**compose has no platform database.** The two facts the platform rail reads from
|
|
34
|
+
one — the data stores connected to the app, and each Module's install row — come
|
|
35
|
+
from `apps/web/stardeck.compose.json` instead:
|
|
36
|
+
|
|
37
|
+
```jsonc
|
|
38
|
+
{
|
|
39
|
+
"connectedStores": [
|
|
40
|
+
{ "storeId": "ds_123", "slug": "media", "bindingKey": null, "accessLevel": "write" }
|
|
41
|
+
],
|
|
42
|
+
"installs": {
|
|
43
|
+
"pos": { "installedVersion": "7.49.0", "datastoreBindings": { "media": "ds_123" } }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The platform writes it at deploy time; write it by hand to compose locally
|
|
49
|
+
against real stores. It is an input, not a result — compose still resolves and
|
|
50
|
+
validates the bindings itself. Without the file, `module-datastores.gen.ts`
|
|
51
|
+
composes empty and every registry version reads `unreleased`, so an app with
|
|
52
|
+
bound stores must not compose without it. A malformed file is a hard error, never
|
|
53
|
+
a silent fallback. It holds deployment facts: gitignore it (`apps/web/.gitignore`:
|
|
54
|
+
`stardeck.compose.json`).
|
|
37
55
|
|
|
38
56
|
- `--enabled` — comma-separated **feature** Module names. Unset composes every
|
|
39
57
|
installed Module. Library and contract Modules are always composed. A name
|
|
@@ -57,3 +75,12 @@ bindings with nothing.
|
|
|
57
75
|
- `@stardeck-customer-apps/compose/runtime` exports `isModuleEnabledByList`,
|
|
58
76
|
the single enablement rule, and imports nothing — it is safe in client
|
|
59
77
|
components.
|
|
78
|
+
|
|
79
|
+
## Generated ignore list
|
|
80
|
+
|
|
81
|
+
After each run compose rewrites `apps/web/src/app/.gitignore` with one entry per
|
|
82
|
+
stub it emits — stubs are scattered one-per-route and cannot be globbed. Commit
|
|
83
|
+
that file: it is deterministic output, and compose rewrites it only when the set
|
|
84
|
+
of stubs changes, so an unchanged compose leaves `git status` clean. It does not
|
|
85
|
+
list the five `src/*.gen.ts`; a static `src/*.gen.ts` line in `apps/web/.gitignore`
|
|
86
|
+
covers those. Compose refuses to touch the file if line 1 is not its own header.
|
package/dist/cli.js
CHANGED
|
@@ -1886,6 +1886,9 @@ var MODULE_INIT_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-init.server.gen.t
|
|
|
1886
1886
|
var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
|
|
1887
1887
|
var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
|
|
1888
1888
|
|
|
1889
|
+
// src/cli.ts
|
|
1890
|
+
var import_typescript5 = __toESM(require("typescript"));
|
|
1891
|
+
|
|
1889
1892
|
// src/index.ts
|
|
1890
1893
|
var import_node_child_process2 = require("child_process");
|
|
1891
1894
|
var import_node_fs2 = __toESM(require("fs"));
|
|
@@ -3546,6 +3549,7 @@ function planCompositionArtifacts(input) {
|
|
|
3546
3549
|
endpointStubs: stubs.endpointStubs,
|
|
3547
3550
|
routeStubs: stubs.routeStubs,
|
|
3548
3551
|
writes,
|
|
3552
|
+
desiredPaths: [...desired.keys()].sort(compareNames2),
|
|
3549
3553
|
deletes
|
|
3550
3554
|
}
|
|
3551
3555
|
};
|
|
@@ -5116,6 +5120,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
5116
5120
|
registryPath: planned.plan.registryPath,
|
|
5117
5121
|
writePaths: planned.plan.writes.map((w) => w.path),
|
|
5118
5122
|
deletePaths: planned.plan.deletes,
|
|
5123
|
+
artifactPaths: planned.plan.desiredPaths,
|
|
5119
5124
|
changed,
|
|
5120
5125
|
composedModules: composedNames,
|
|
5121
5126
|
excludedModules
|
|
@@ -5125,7 +5130,89 @@ var HOME_I18N_SUBSTRATE_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
|
|
|
5125
5130
|
var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
|
|
5126
5131
|
|
|
5127
5132
|
// src/index.ts
|
|
5133
|
+
var import_zod9 = require("zod");
|
|
5128
5134
|
var MODULES_DIR2 = MODULES_SANDBOX_DIR;
|
|
5135
|
+
var COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
|
|
5136
|
+
var composeInputsSchema = import_zod9.z.strictObject({
|
|
5137
|
+
connectedStores: import_zod9.z.array(
|
|
5138
|
+
import_zod9.z.strictObject({
|
|
5139
|
+
storeId: import_zod9.z.string().min(1),
|
|
5140
|
+
slug: import_zod9.z.string().min(1),
|
|
5141
|
+
bindingKey: import_zod9.z.string().min(1).nullish(),
|
|
5142
|
+
accessLevel: import_zod9.z.enum(["read", "write", "admin"])
|
|
5143
|
+
})
|
|
5144
|
+
).default([]),
|
|
5145
|
+
installs: import_zod9.z.record(
|
|
5146
|
+
import_zod9.z.string(),
|
|
5147
|
+
import_zod9.z.strictObject({
|
|
5148
|
+
installedVersion: import_zod9.z.string().min(1),
|
|
5149
|
+
datastoreBindings: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.string()).nullish()
|
|
5150
|
+
})
|
|
5151
|
+
).default({})
|
|
5152
|
+
});
|
|
5153
|
+
function readComposeInputs(cwd) {
|
|
5154
|
+
const file = import_node_path2.default.join(cwd, COMPOSE_INPUTS_PATH);
|
|
5155
|
+
if (!import_node_fs2.default.existsSync(file)) return { connectedStores: [], installs: {} };
|
|
5156
|
+
let raw;
|
|
5157
|
+
try {
|
|
5158
|
+
raw = JSON.parse(import_node_fs2.default.readFileSync(file, "utf8"));
|
|
5159
|
+
} catch (error) {
|
|
5160
|
+
throw new Error(
|
|
5161
|
+
`[compose] ${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
5162
|
+
);
|
|
5163
|
+
}
|
|
5164
|
+
const parsed = composeInputsSchema.safeParse(raw);
|
|
5165
|
+
if (!parsed.success) {
|
|
5166
|
+
const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
|
|
5167
|
+
throw new Error(`[compose] ${file} is invalid: ${issues}`);
|
|
5168
|
+
}
|
|
5169
|
+
return {
|
|
5170
|
+
connectedStores: parsed.data.connectedStores.map((store) => ({
|
|
5171
|
+
storeId: store.storeId,
|
|
5172
|
+
slug: store.slug,
|
|
5173
|
+
bindingKey: store.bindingKey ?? null,
|
|
5174
|
+
accessLevel: store.accessLevel
|
|
5175
|
+
})),
|
|
5176
|
+
installs: parsed.data.installs
|
|
5177
|
+
};
|
|
5178
|
+
}
|
|
5179
|
+
function assertSupportedTypescript(versionMajorMinor) {
|
|
5180
|
+
const major = Number.parseInt(versionMajorMinor.split(".")[0] ?? "", 10);
|
|
5181
|
+
if (!Number.isInteger(major) || major < 5 || major >= 7) {
|
|
5182
|
+
throw new Error(`[compose] typescript ${versionMajorMinor} is not supported (need >=5 <7)`);
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
var APP_DIR_PREFIX2 = "apps/web/src/app/";
|
|
5186
|
+
var STUB_GITIGNORE_PATH = "apps/web/src/app/.gitignore";
|
|
5187
|
+
var STUB_GITIGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
|
|
5188
|
+
function escapeGitignore(pattern) {
|
|
5189
|
+
const escaped = pattern.replace(/[\\[\]*?]/g, (char) => `\\${char}`);
|
|
5190
|
+
return /^[#!]/.test(escaped) ? `\\${escaped}` : escaped;
|
|
5191
|
+
}
|
|
5192
|
+
function stubGitignoreEntries(artifactPaths) {
|
|
5193
|
+
return artifactPaths.filter((artifact) => artifact.startsWith(APP_DIR_PREFIX2)).map((artifact) => escapeGitignore(`/${artifact.slice(APP_DIR_PREFIX2.length)}`)).sort();
|
|
5194
|
+
}
|
|
5195
|
+
function writeStubGitignore(cwd, artifactPaths) {
|
|
5196
|
+
const entries = stubGitignoreEntries(artifactPaths);
|
|
5197
|
+
const content = [STUB_GITIGNORE_HEADER, ...entries].join("\n") + "\n";
|
|
5198
|
+
const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
|
|
5199
|
+
const existing = import_node_fs2.default.existsSync(file) ? import_node_fs2.default.readFileSync(file, "utf8") : null;
|
|
5200
|
+
if (existing !== content) {
|
|
5201
|
+
import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
|
|
5202
|
+
import_node_fs2.default.writeFileSync(file, content);
|
|
5203
|
+
}
|
|
5204
|
+
return entries.length;
|
|
5205
|
+
}
|
|
5206
|
+
function assertStubGitignoreOwned(cwd) {
|
|
5207
|
+
const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
|
|
5208
|
+
if (!import_node_fs2.default.existsSync(file)) return;
|
|
5209
|
+
const firstLine = import_node_fs2.default.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
|
|
5210
|
+
if (firstLine !== STUB_GITIGNORE_HEADER) {
|
|
5211
|
+
throw new Error(
|
|
5212
|
+
`[compose] refusing to overwrite ${STUB_GITIGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
|
|
5213
|
+
);
|
|
5214
|
+
}
|
|
5215
|
+
}
|
|
5129
5216
|
function assertAppRoot(cwd) {
|
|
5130
5217
|
const modulesPath = import_node_path2.default.join(cwd, MODULES_DIR2);
|
|
5131
5218
|
if (!import_node_fs2.default.existsSync(modulesPath)) {
|
|
@@ -5210,12 +5297,14 @@ function removeDir(absolute) {
|
|
|
5210
5297
|
}
|
|
5211
5298
|
async function compose(options) {
|
|
5212
5299
|
assertAppRoot(options.cwd);
|
|
5300
|
+
assertStubGitignoreOwned(options.cwd);
|
|
5301
|
+
const inputs = readComposeInputs(options.cwd);
|
|
5213
5302
|
if (options.prune) assertModuleTreeClean(options.cwd);
|
|
5214
5303
|
const sandbox = localFsSandbox(options.cwd);
|
|
5215
5304
|
const deps = {
|
|
5216
5305
|
sandbox,
|
|
5217
|
-
getModuleInstall: async () => null,
|
|
5218
|
-
listDatabaseStoresForProject: async () =>
|
|
5306
|
+
getModuleInstall: async (_db, _projectId, moduleName) => inputs.installs[moduleName] ?? null,
|
|
5307
|
+
listDatabaseStoresForProject: async () => inputs.connectedStores
|
|
5219
5308
|
};
|
|
5220
5309
|
const context = {
|
|
5221
5310
|
db: {},
|
|
@@ -5234,6 +5323,7 @@ async function compose(options) {
|
|
|
5234
5323
|
const result = await reconcileCompositionArtifacts(context, {
|
|
5235
5324
|
enabledModulesRaw: options.enabled
|
|
5236
5325
|
});
|
|
5326
|
+
const gitignoreEntries = options.enabled ? null : writeStubGitignore(options.cwd, result.artifactPaths);
|
|
5237
5327
|
let pruned = 0;
|
|
5238
5328
|
if (options.prune) {
|
|
5239
5329
|
for (const name of result.excludedModules) {
|
|
@@ -5250,12 +5340,14 @@ async function compose(options) {
|
|
|
5250
5340
|
excludedModules: result.excludedModules,
|
|
5251
5341
|
writes: result.writePaths.length,
|
|
5252
5342
|
deletes: result.deletePaths.length,
|
|
5253
|
-
pruned
|
|
5343
|
+
pruned,
|
|
5344
|
+
gitignoreEntries
|
|
5254
5345
|
};
|
|
5255
5346
|
}
|
|
5256
5347
|
|
|
5257
5348
|
// src/cli.ts
|
|
5258
5349
|
async function main() {
|
|
5350
|
+
assertSupportedTypescript(import_typescript5.default.versionMajorMinor);
|
|
5259
5351
|
const { values } = (0, import_node_util.parseArgs)({
|
|
5260
5352
|
options: {
|
|
5261
5353
|
enabled: { type: "string" },
|
|
@@ -5276,7 +5368,7 @@ async function main() {
|
|
|
5276
5368
|
prune: values.prune
|
|
5277
5369
|
});
|
|
5278
5370
|
console.log(
|
|
5279
|
-
`[compose] modules=${result.composedModules.join(",") || "none"} excluded=${result.excludedModules.join(",") || "none"} writes=${result.writes} deletes=${result.deletes} pruned=${result.pruned}`
|
|
5371
|
+
`[compose] modules=${result.composedModules.join(",") || "none"} excluded=${result.excludedModules.join(",") || "none"} writes=${result.writes} deletes=${result.deletes} pruned=${result.pruned} gitignore=${result.gitignoreEntries ?? "kept"} stub paths`
|
|
5280
5372
|
);
|
|
5281
5373
|
}
|
|
5282
5374
|
main().catch((error) => {
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** A data store connected to a project, as the composition rail consumes it. */
|
|
4
|
+
type ConnectedStore = {
|
|
5
|
+
storeId: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
bindingKey: string | null;
|
|
8
|
+
accessLevel: "read" | "write" | "admin";
|
|
9
|
+
};
|
|
10
|
+
|
|
1
11
|
interface ComposeOptions {
|
|
2
12
|
/** Repo root of the customer app (the directory holding `apps/web`). */
|
|
3
13
|
cwd: string;
|
|
@@ -16,11 +26,54 @@ interface ComposeResult {
|
|
|
16
26
|
deletes: number;
|
|
17
27
|
/** Directories removed by `prune` (excluded Modules + orphaned enhancements). */
|
|
18
28
|
pruned: number;
|
|
29
|
+
/** Stub paths listed in the generated `src/app/.gitignore`. */
|
|
30
|
+
/** Null when a partial (`--enabled`) compose left the committed list untouched. */
|
|
31
|
+
gitignoreEntries: number | null;
|
|
19
32
|
}
|
|
33
|
+
/** Repo-relative inputs file: the platform's two database reads, as JSON. */
|
|
34
|
+
declare const COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
|
|
35
|
+
declare const composeInputsSchema: z.ZodObject<{
|
|
36
|
+
connectedStores: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
37
|
+
storeId: z.ZodString;
|
|
38
|
+
slug: z.ZodString;
|
|
39
|
+
bindingKey: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
40
|
+
accessLevel: z.ZodEnum<{
|
|
41
|
+
read: "read";
|
|
42
|
+
write: "write";
|
|
43
|
+
admin: "admin";
|
|
44
|
+
}>;
|
|
45
|
+
}, z.core.$strict>>>;
|
|
46
|
+
installs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
47
|
+
installedVersion: z.ZodString;
|
|
48
|
+
datastoreBindings: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
49
|
+
}, z.core.$strict>>>;
|
|
50
|
+
}, z.core.$strict>;
|
|
51
|
+
type ComposeInstall = z.infer<typeof composeInputsSchema>["installs"][string];
|
|
52
|
+
interface ComposeInputs {
|
|
53
|
+
connectedStores: ConnectedStore[];
|
|
54
|
+
installs: Record<string, ComposeInstall>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The platform rail resolves data-store bindings and installed versions from
|
|
58
|
+
* its database. A standalone app has none, so compose reads the same two facts
|
|
59
|
+
* from a file the platform writes at deploy time (or a developer writes by
|
|
60
|
+
* hand). Absent file: no stores, every version `unreleased`. A malformed file
|
|
61
|
+
* is fatal — silently composing empty bindings is how an app loses its stores.
|
|
62
|
+
*/
|
|
63
|
+
declare function readComposeInputs(cwd: string): ComposeInputs;
|
|
64
|
+
/** Compose needs the TypeScript API the emitters parse route entries with. */
|
|
65
|
+
declare function assertSupportedTypescript(versionMajorMinor: string): void;
|
|
66
|
+
/**
|
|
67
|
+
* gitignore patterns are fnmatch, so a Next.js path segment is not a literal:
|
|
68
|
+
* `[lang]` is a character class and `/[lang]/…/page.tsx` ignores nothing.
|
|
69
|
+
* Escapes every metacharacter, plus a leading `#`/`!` (the caller's `/` prefix
|
|
70
|
+
* rules those out today; a total helper keeps it that way).
|
|
71
|
+
*/
|
|
72
|
+
declare function escapeGitignore(pattern: string): string;
|
|
20
73
|
/**
|
|
21
74
|
* Regenerate the Module-rail artifacts (the five `src/*.gen.ts` files and every
|
|
22
75
|
* route/endpoint stub under `src/app`) for the app rooted at `options.cwd`.
|
|
23
76
|
*/
|
|
24
77
|
declare function compose(options: ComposeOptions): Promise<ComposeResult>;
|
|
25
78
|
|
|
26
|
-
export { type ComposeOptions, type ComposeResult, compose };
|
|
79
|
+
export { COMPOSE_INPUTS_PATH, type ComposeInputs, type ComposeOptions, type ComposeResult, assertSupportedTypescript, compose, escapeGitignore, readComposeInputs };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** A data store connected to a project, as the composition rail consumes it. */
|
|
4
|
+
type ConnectedStore = {
|
|
5
|
+
storeId: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
bindingKey: string | null;
|
|
8
|
+
accessLevel: "read" | "write" | "admin";
|
|
9
|
+
};
|
|
10
|
+
|
|
1
11
|
interface ComposeOptions {
|
|
2
12
|
/** Repo root of the customer app (the directory holding `apps/web`). */
|
|
3
13
|
cwd: string;
|
|
@@ -16,11 +26,54 @@ interface ComposeResult {
|
|
|
16
26
|
deletes: number;
|
|
17
27
|
/** Directories removed by `prune` (excluded Modules + orphaned enhancements). */
|
|
18
28
|
pruned: number;
|
|
29
|
+
/** Stub paths listed in the generated `src/app/.gitignore`. */
|
|
30
|
+
/** Null when a partial (`--enabled`) compose left the committed list untouched. */
|
|
31
|
+
gitignoreEntries: number | null;
|
|
19
32
|
}
|
|
33
|
+
/** Repo-relative inputs file: the platform's two database reads, as JSON. */
|
|
34
|
+
declare const COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
|
|
35
|
+
declare const composeInputsSchema: z.ZodObject<{
|
|
36
|
+
connectedStores: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
37
|
+
storeId: z.ZodString;
|
|
38
|
+
slug: z.ZodString;
|
|
39
|
+
bindingKey: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
40
|
+
accessLevel: z.ZodEnum<{
|
|
41
|
+
read: "read";
|
|
42
|
+
write: "write";
|
|
43
|
+
admin: "admin";
|
|
44
|
+
}>;
|
|
45
|
+
}, z.core.$strict>>>;
|
|
46
|
+
installs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
47
|
+
installedVersion: z.ZodString;
|
|
48
|
+
datastoreBindings: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
49
|
+
}, z.core.$strict>>>;
|
|
50
|
+
}, z.core.$strict>;
|
|
51
|
+
type ComposeInstall = z.infer<typeof composeInputsSchema>["installs"][string];
|
|
52
|
+
interface ComposeInputs {
|
|
53
|
+
connectedStores: ConnectedStore[];
|
|
54
|
+
installs: Record<string, ComposeInstall>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The platform rail resolves data-store bindings and installed versions from
|
|
58
|
+
* its database. A standalone app has none, so compose reads the same two facts
|
|
59
|
+
* from a file the platform writes at deploy time (or a developer writes by
|
|
60
|
+
* hand). Absent file: no stores, every version `unreleased`. A malformed file
|
|
61
|
+
* is fatal — silently composing empty bindings is how an app loses its stores.
|
|
62
|
+
*/
|
|
63
|
+
declare function readComposeInputs(cwd: string): ComposeInputs;
|
|
64
|
+
/** Compose needs the TypeScript API the emitters parse route entries with. */
|
|
65
|
+
declare function assertSupportedTypescript(versionMajorMinor: string): void;
|
|
66
|
+
/**
|
|
67
|
+
* gitignore patterns are fnmatch, so a Next.js path segment is not a literal:
|
|
68
|
+
* `[lang]` is a character class and `/[lang]/…/page.tsx` ignores nothing.
|
|
69
|
+
* Escapes every metacharacter, plus a leading `#`/`!` (the caller's `/` prefix
|
|
70
|
+
* rules those out today; a total helper keeps it that way).
|
|
71
|
+
*/
|
|
72
|
+
declare function escapeGitignore(pattern: string): string;
|
|
20
73
|
/**
|
|
21
74
|
* Regenerate the Module-rail artifacts (the five `src/*.gen.ts` files and every
|
|
22
75
|
* route/endpoint stub under `src/app`) for the app rooted at `options.cwd`.
|
|
23
76
|
*/
|
|
24
77
|
declare function compose(options: ComposeOptions): Promise<ComposeResult>;
|
|
25
78
|
|
|
26
|
-
export { type ComposeOptions, type ComposeResult, compose };
|
|
79
|
+
export { COMPOSE_INPUTS_PATH, type ComposeInputs, type ComposeOptions, type ComposeResult, assertSupportedTypescript, compose, escapeGitignore, readComposeInputs };
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
|
|
33
|
+
COMPOSE_INPUTS_PATH: () => COMPOSE_INPUTS_PATH,
|
|
34
|
+
assertSupportedTypescript: () => assertSupportedTypescript,
|
|
35
|
+
compose: () => compose,
|
|
36
|
+
escapeGitignore: () => escapeGitignore,
|
|
37
|
+
readComposeInputs: () => readComposeInputs
|
|
34
38
|
});
|
|
35
39
|
module.exports = __toCommonJS(index_exports);
|
|
36
40
|
var import_node_child_process2 = require("child_process");
|
|
@@ -3554,6 +3558,7 @@ function planCompositionArtifacts(input) {
|
|
|
3554
3558
|
endpointStubs: stubs.endpointStubs,
|
|
3555
3559
|
routeStubs: stubs.routeStubs,
|
|
3556
3560
|
writes,
|
|
3561
|
+
desiredPaths: [...desired.keys()].sort(compareNames2),
|
|
3557
3562
|
deletes
|
|
3558
3563
|
}
|
|
3559
3564
|
};
|
|
@@ -5124,6 +5129,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
5124
5129
|
registryPath: planned.plan.registryPath,
|
|
5125
5130
|
writePaths: planned.plan.writes.map((w) => w.path),
|
|
5126
5131
|
deletePaths: planned.plan.deletes,
|
|
5132
|
+
artifactPaths: planned.plan.desiredPaths,
|
|
5127
5133
|
changed,
|
|
5128
5134
|
composedModules: composedNames,
|
|
5129
5135
|
excludedModules
|
|
@@ -5133,7 +5139,89 @@ var HOME_I18N_SUBSTRATE_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
|
|
|
5133
5139
|
var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
|
|
5134
5140
|
|
|
5135
5141
|
// src/index.ts
|
|
5142
|
+
var import_zod9 = require("zod");
|
|
5136
5143
|
var MODULES_DIR2 = MODULES_SANDBOX_DIR;
|
|
5144
|
+
var COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
|
|
5145
|
+
var composeInputsSchema = import_zod9.z.strictObject({
|
|
5146
|
+
connectedStores: import_zod9.z.array(
|
|
5147
|
+
import_zod9.z.strictObject({
|
|
5148
|
+
storeId: import_zod9.z.string().min(1),
|
|
5149
|
+
slug: import_zod9.z.string().min(1),
|
|
5150
|
+
bindingKey: import_zod9.z.string().min(1).nullish(),
|
|
5151
|
+
accessLevel: import_zod9.z.enum(["read", "write", "admin"])
|
|
5152
|
+
})
|
|
5153
|
+
).default([]),
|
|
5154
|
+
installs: import_zod9.z.record(
|
|
5155
|
+
import_zod9.z.string(),
|
|
5156
|
+
import_zod9.z.strictObject({
|
|
5157
|
+
installedVersion: import_zod9.z.string().min(1),
|
|
5158
|
+
datastoreBindings: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.string()).nullish()
|
|
5159
|
+
})
|
|
5160
|
+
).default({})
|
|
5161
|
+
});
|
|
5162
|
+
function readComposeInputs(cwd) {
|
|
5163
|
+
const file = import_node_path2.default.join(cwd, COMPOSE_INPUTS_PATH);
|
|
5164
|
+
if (!import_node_fs2.default.existsSync(file)) return { connectedStores: [], installs: {} };
|
|
5165
|
+
let raw;
|
|
5166
|
+
try {
|
|
5167
|
+
raw = JSON.parse(import_node_fs2.default.readFileSync(file, "utf8"));
|
|
5168
|
+
} catch (error) {
|
|
5169
|
+
throw new Error(
|
|
5170
|
+
`[compose] ${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
5171
|
+
);
|
|
5172
|
+
}
|
|
5173
|
+
const parsed = composeInputsSchema.safeParse(raw);
|
|
5174
|
+
if (!parsed.success) {
|
|
5175
|
+
const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
|
|
5176
|
+
throw new Error(`[compose] ${file} is invalid: ${issues}`);
|
|
5177
|
+
}
|
|
5178
|
+
return {
|
|
5179
|
+
connectedStores: parsed.data.connectedStores.map((store) => ({
|
|
5180
|
+
storeId: store.storeId,
|
|
5181
|
+
slug: store.slug,
|
|
5182
|
+
bindingKey: store.bindingKey ?? null,
|
|
5183
|
+
accessLevel: store.accessLevel
|
|
5184
|
+
})),
|
|
5185
|
+
installs: parsed.data.installs
|
|
5186
|
+
};
|
|
5187
|
+
}
|
|
5188
|
+
function assertSupportedTypescript(versionMajorMinor) {
|
|
5189
|
+
const major = Number.parseInt(versionMajorMinor.split(".")[0] ?? "", 10);
|
|
5190
|
+
if (!Number.isInteger(major) || major < 5 || major >= 7) {
|
|
5191
|
+
throw new Error(`[compose] typescript ${versionMajorMinor} is not supported (need >=5 <7)`);
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5194
|
+
var APP_DIR_PREFIX2 = "apps/web/src/app/";
|
|
5195
|
+
var STUB_GITIGNORE_PATH = "apps/web/src/app/.gitignore";
|
|
5196
|
+
var STUB_GITIGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
|
|
5197
|
+
function escapeGitignore(pattern) {
|
|
5198
|
+
const escaped = pattern.replace(/[\\[\]*?]/g, (char) => `\\${char}`);
|
|
5199
|
+
return /^[#!]/.test(escaped) ? `\\${escaped}` : escaped;
|
|
5200
|
+
}
|
|
5201
|
+
function stubGitignoreEntries(artifactPaths) {
|
|
5202
|
+
return artifactPaths.filter((artifact) => artifact.startsWith(APP_DIR_PREFIX2)).map((artifact) => escapeGitignore(`/${artifact.slice(APP_DIR_PREFIX2.length)}`)).sort();
|
|
5203
|
+
}
|
|
5204
|
+
function writeStubGitignore(cwd, artifactPaths) {
|
|
5205
|
+
const entries = stubGitignoreEntries(artifactPaths);
|
|
5206
|
+
const content = [STUB_GITIGNORE_HEADER, ...entries].join("\n") + "\n";
|
|
5207
|
+
const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
|
|
5208
|
+
const existing = import_node_fs2.default.existsSync(file) ? import_node_fs2.default.readFileSync(file, "utf8") : null;
|
|
5209
|
+
if (existing !== content) {
|
|
5210
|
+
import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
|
|
5211
|
+
import_node_fs2.default.writeFileSync(file, content);
|
|
5212
|
+
}
|
|
5213
|
+
return entries.length;
|
|
5214
|
+
}
|
|
5215
|
+
function assertStubGitignoreOwned(cwd) {
|
|
5216
|
+
const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
|
|
5217
|
+
if (!import_node_fs2.default.existsSync(file)) return;
|
|
5218
|
+
const firstLine = import_node_fs2.default.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
|
|
5219
|
+
if (firstLine !== STUB_GITIGNORE_HEADER) {
|
|
5220
|
+
throw new Error(
|
|
5221
|
+
`[compose] refusing to overwrite ${STUB_GITIGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
|
|
5222
|
+
);
|
|
5223
|
+
}
|
|
5224
|
+
}
|
|
5137
5225
|
function assertAppRoot(cwd) {
|
|
5138
5226
|
const modulesPath = import_node_path2.default.join(cwd, MODULES_DIR2);
|
|
5139
5227
|
if (!import_node_fs2.default.existsSync(modulesPath)) {
|
|
@@ -5218,12 +5306,14 @@ function removeDir(absolute) {
|
|
|
5218
5306
|
}
|
|
5219
5307
|
async function compose(options) {
|
|
5220
5308
|
assertAppRoot(options.cwd);
|
|
5309
|
+
assertStubGitignoreOwned(options.cwd);
|
|
5310
|
+
const inputs = readComposeInputs(options.cwd);
|
|
5221
5311
|
if (options.prune) assertModuleTreeClean(options.cwd);
|
|
5222
5312
|
const sandbox = localFsSandbox(options.cwd);
|
|
5223
5313
|
const deps = {
|
|
5224
5314
|
sandbox,
|
|
5225
|
-
getModuleInstall: async () => null,
|
|
5226
|
-
listDatabaseStoresForProject: async () =>
|
|
5315
|
+
getModuleInstall: async (_db, _projectId, moduleName) => inputs.installs[moduleName] ?? null,
|
|
5316
|
+
listDatabaseStoresForProject: async () => inputs.connectedStores
|
|
5227
5317
|
};
|
|
5228
5318
|
const context = {
|
|
5229
5319
|
db: {},
|
|
@@ -5242,6 +5332,7 @@ async function compose(options) {
|
|
|
5242
5332
|
const result = await reconcileCompositionArtifacts(context, {
|
|
5243
5333
|
enabledModulesRaw: options.enabled
|
|
5244
5334
|
});
|
|
5335
|
+
const gitignoreEntries = options.enabled ? null : writeStubGitignore(options.cwd, result.artifactPaths);
|
|
5245
5336
|
let pruned = 0;
|
|
5246
5337
|
if (options.prune) {
|
|
5247
5338
|
for (const name of result.excludedModules) {
|
|
@@ -5258,10 +5349,15 @@ async function compose(options) {
|
|
|
5258
5349
|
excludedModules: result.excludedModules,
|
|
5259
5350
|
writes: result.writePaths.length,
|
|
5260
5351
|
deletes: result.deletePaths.length,
|
|
5261
|
-
pruned
|
|
5352
|
+
pruned,
|
|
5353
|
+
gitignoreEntries
|
|
5262
5354
|
};
|
|
5263
5355
|
}
|
|
5264
5356
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5265
5357
|
0 && (module.exports = {
|
|
5266
|
-
|
|
5358
|
+
COMPOSE_INPUTS_PATH,
|
|
5359
|
+
assertSupportedTypescript,
|
|
5360
|
+
compose,
|
|
5361
|
+
escapeGitignore,
|
|
5362
|
+
readComposeInputs
|
|
5267
5363
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -3520,6 +3520,7 @@ function planCompositionArtifacts(input) {
|
|
|
3520
3520
|
endpointStubs: stubs.endpointStubs,
|
|
3521
3521
|
routeStubs: stubs.routeStubs,
|
|
3522
3522
|
writes,
|
|
3523
|
+
desiredPaths: [...desired.keys()].sort(compareNames2),
|
|
3523
3524
|
deletes
|
|
3524
3525
|
}
|
|
3525
3526
|
};
|
|
@@ -5090,6 +5091,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
5090
5091
|
registryPath: planned.plan.registryPath,
|
|
5091
5092
|
writePaths: planned.plan.writes.map((w) => w.path),
|
|
5092
5093
|
deletePaths: planned.plan.deletes,
|
|
5094
|
+
artifactPaths: planned.plan.desiredPaths,
|
|
5093
5095
|
changed,
|
|
5094
5096
|
composedModules: composedNames,
|
|
5095
5097
|
excludedModules
|
|
@@ -5099,7 +5101,89 @@ var HOME_I18N_SUBSTRATE_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
|
|
|
5099
5101
|
var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
|
|
5100
5102
|
|
|
5101
5103
|
// src/index.ts
|
|
5104
|
+
import { z as z9 } from "zod";
|
|
5102
5105
|
var MODULES_DIR2 = MODULES_SANDBOX_DIR;
|
|
5106
|
+
var COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
|
|
5107
|
+
var composeInputsSchema = z9.strictObject({
|
|
5108
|
+
connectedStores: z9.array(
|
|
5109
|
+
z9.strictObject({
|
|
5110
|
+
storeId: z9.string().min(1),
|
|
5111
|
+
slug: z9.string().min(1),
|
|
5112
|
+
bindingKey: z9.string().min(1).nullish(),
|
|
5113
|
+
accessLevel: z9.enum(["read", "write", "admin"])
|
|
5114
|
+
})
|
|
5115
|
+
).default([]),
|
|
5116
|
+
installs: z9.record(
|
|
5117
|
+
z9.string(),
|
|
5118
|
+
z9.strictObject({
|
|
5119
|
+
installedVersion: z9.string().min(1),
|
|
5120
|
+
datastoreBindings: z9.record(z9.string(), z9.string()).nullish()
|
|
5121
|
+
})
|
|
5122
|
+
).default({})
|
|
5123
|
+
});
|
|
5124
|
+
function readComposeInputs(cwd) {
|
|
5125
|
+
const file = path2.join(cwd, COMPOSE_INPUTS_PATH);
|
|
5126
|
+
if (!fs2.existsSync(file)) return { connectedStores: [], installs: {} };
|
|
5127
|
+
let raw;
|
|
5128
|
+
try {
|
|
5129
|
+
raw = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
5130
|
+
} catch (error) {
|
|
5131
|
+
throw new Error(
|
|
5132
|
+
`[compose] ${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
5133
|
+
);
|
|
5134
|
+
}
|
|
5135
|
+
const parsed = composeInputsSchema.safeParse(raw);
|
|
5136
|
+
if (!parsed.success) {
|
|
5137
|
+
const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
|
|
5138
|
+
throw new Error(`[compose] ${file} is invalid: ${issues}`);
|
|
5139
|
+
}
|
|
5140
|
+
return {
|
|
5141
|
+
connectedStores: parsed.data.connectedStores.map((store) => ({
|
|
5142
|
+
storeId: store.storeId,
|
|
5143
|
+
slug: store.slug,
|
|
5144
|
+
bindingKey: store.bindingKey ?? null,
|
|
5145
|
+
accessLevel: store.accessLevel
|
|
5146
|
+
})),
|
|
5147
|
+
installs: parsed.data.installs
|
|
5148
|
+
};
|
|
5149
|
+
}
|
|
5150
|
+
function assertSupportedTypescript(versionMajorMinor) {
|
|
5151
|
+
const major = Number.parseInt(versionMajorMinor.split(".")[0] ?? "", 10);
|
|
5152
|
+
if (!Number.isInteger(major) || major < 5 || major >= 7) {
|
|
5153
|
+
throw new Error(`[compose] typescript ${versionMajorMinor} is not supported (need >=5 <7)`);
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
var APP_DIR_PREFIX2 = "apps/web/src/app/";
|
|
5157
|
+
var STUB_GITIGNORE_PATH = "apps/web/src/app/.gitignore";
|
|
5158
|
+
var STUB_GITIGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
|
|
5159
|
+
function escapeGitignore(pattern) {
|
|
5160
|
+
const escaped = pattern.replace(/[\\[\]*?]/g, (char) => `\\${char}`);
|
|
5161
|
+
return /^[#!]/.test(escaped) ? `\\${escaped}` : escaped;
|
|
5162
|
+
}
|
|
5163
|
+
function stubGitignoreEntries(artifactPaths) {
|
|
5164
|
+
return artifactPaths.filter((artifact) => artifact.startsWith(APP_DIR_PREFIX2)).map((artifact) => escapeGitignore(`/${artifact.slice(APP_DIR_PREFIX2.length)}`)).sort();
|
|
5165
|
+
}
|
|
5166
|
+
function writeStubGitignore(cwd, artifactPaths) {
|
|
5167
|
+
const entries = stubGitignoreEntries(artifactPaths);
|
|
5168
|
+
const content = [STUB_GITIGNORE_HEADER, ...entries].join("\n") + "\n";
|
|
5169
|
+
const file = path2.join(cwd, STUB_GITIGNORE_PATH);
|
|
5170
|
+
const existing = fs2.existsSync(file) ? fs2.readFileSync(file, "utf8") : null;
|
|
5171
|
+
if (existing !== content) {
|
|
5172
|
+
fs2.mkdirSync(path2.dirname(file), { recursive: true });
|
|
5173
|
+
fs2.writeFileSync(file, content);
|
|
5174
|
+
}
|
|
5175
|
+
return entries.length;
|
|
5176
|
+
}
|
|
5177
|
+
function assertStubGitignoreOwned(cwd) {
|
|
5178
|
+
const file = path2.join(cwd, STUB_GITIGNORE_PATH);
|
|
5179
|
+
if (!fs2.existsSync(file)) return;
|
|
5180
|
+
const firstLine = fs2.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
|
|
5181
|
+
if (firstLine !== STUB_GITIGNORE_HEADER) {
|
|
5182
|
+
throw new Error(
|
|
5183
|
+
`[compose] refusing to overwrite ${STUB_GITIGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
|
|
5184
|
+
);
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5103
5187
|
function assertAppRoot(cwd) {
|
|
5104
5188
|
const modulesPath = path2.join(cwd, MODULES_DIR2);
|
|
5105
5189
|
if (!fs2.existsSync(modulesPath)) {
|
|
@@ -5184,12 +5268,14 @@ function removeDir(absolute) {
|
|
|
5184
5268
|
}
|
|
5185
5269
|
async function compose(options) {
|
|
5186
5270
|
assertAppRoot(options.cwd);
|
|
5271
|
+
assertStubGitignoreOwned(options.cwd);
|
|
5272
|
+
const inputs = readComposeInputs(options.cwd);
|
|
5187
5273
|
if (options.prune) assertModuleTreeClean(options.cwd);
|
|
5188
5274
|
const sandbox = localFsSandbox(options.cwd);
|
|
5189
5275
|
const deps = {
|
|
5190
5276
|
sandbox,
|
|
5191
|
-
getModuleInstall: async () => null,
|
|
5192
|
-
listDatabaseStoresForProject: async () =>
|
|
5277
|
+
getModuleInstall: async (_db, _projectId, moduleName) => inputs.installs[moduleName] ?? null,
|
|
5278
|
+
listDatabaseStoresForProject: async () => inputs.connectedStores
|
|
5193
5279
|
};
|
|
5194
5280
|
const context = {
|
|
5195
5281
|
db: {},
|
|
@@ -5208,6 +5294,7 @@ async function compose(options) {
|
|
|
5208
5294
|
const result = await reconcileCompositionArtifacts(context, {
|
|
5209
5295
|
enabledModulesRaw: options.enabled
|
|
5210
5296
|
});
|
|
5297
|
+
const gitignoreEntries = options.enabled ? null : writeStubGitignore(options.cwd, result.artifactPaths);
|
|
5211
5298
|
let pruned = 0;
|
|
5212
5299
|
if (options.prune) {
|
|
5213
5300
|
for (const name of result.excludedModules) {
|
|
@@ -5224,9 +5311,14 @@ async function compose(options) {
|
|
|
5224
5311
|
excludedModules: result.excludedModules,
|
|
5225
5312
|
writes: result.writePaths.length,
|
|
5226
5313
|
deletes: result.deletePaths.length,
|
|
5227
|
-
pruned
|
|
5314
|
+
pruned,
|
|
5315
|
+
gitignoreEntries
|
|
5228
5316
|
};
|
|
5229
5317
|
}
|
|
5230
5318
|
export {
|
|
5231
|
-
|
|
5319
|
+
COMPOSE_INPUTS_PATH,
|
|
5320
|
+
assertSupportedTypescript,
|
|
5321
|
+
compose,
|
|
5322
|
+
escapeGitignore,
|
|
5323
|
+
readComposeInputs
|
|
5232
5324
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/compose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"zod": "^4.3.5"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"typescript": ">=5.0.0"
|
|
54
|
+
"typescript": ">=5.0.0 <7"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@eslint/js": "^9.0.0",
|