@cosmicdrift/kumiko-dev-server 0.12.2 → 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/CHANGELOG.md +124 -0
- package/bin/kumiko-schema-check.ts +159 -0
- package/package.json +6 -4
- package/src/__tests__/env-schema.integration.ts +50 -0
- package/src/__tests__/scaffold-app-feature.test.ts +88 -0
- package/src/__tests__/scaffold-app.test.ts +104 -0
- package/src/__tests__/scaffold-deploy.test.ts +11 -0
- package/src/__tests__/walkthrough.integration.ts +118 -0
- package/src/build-server-bundle.ts +19 -1
- package/src/index.ts +7 -0
- package/src/run-prod-app.ts +35 -11
- package/src/scaffold-app-feature.ts +154 -0
- package/src/scaffold-app.ts +333 -0
- package/templates/deploy/Dockerfile.template +17 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// DX-3.1 — walkthrough-snapshot-test. Reproduces the 3-command path from
|
|
2
|
+
// docs.kumiko.so/en/walkthrough/ in-process and asserts what the walkthrough
|
|
3
|
+
// claims. Catches drift in scaffoldApp + scaffoldAppFeature against the
|
|
4
|
+
// docs without an actual `bunx … && yarn install && bun run boot` CI run.
|
|
5
|
+
//
|
|
6
|
+
// What this test pins:
|
|
7
|
+
// - scaffoldApp produces the 6 files the walkthrough lists
|
|
8
|
+
// - scaffoldAppFeature scaffolds + auto-mounts (the diff-block shown)
|
|
9
|
+
// - composeFeatures(includeBundled:true) yields the exact feature-count
|
|
10
|
+
// the walkthrough advertises in "Expected output"
|
|
11
|
+
|
|
12
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
16
|
+
import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
17
|
+
import { createRegistry, defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
19
|
+
import { composeFeatures } from "../compose-features";
|
|
20
|
+
import { scaffoldApp } from "../scaffold-app";
|
|
21
|
+
import { scaffoldAppFeature } from "../scaffold-app-feature";
|
|
22
|
+
|
|
23
|
+
describe("walkthrough — DX-3.1 snapshot", () => {
|
|
24
|
+
let tmp: string;
|
|
25
|
+
let appRoot: string;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
tmp = mkdtempSync(join(tmpdir(), "walkthrough-"));
|
|
29
|
+
appRoot = join(tmp, "my-notes");
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("Step 1 (kumiko new app) — produces walkthrough's 6 files", () => {
|
|
36
|
+
const result = scaffoldApp({ name: "my-notes", destination: appRoot });
|
|
37
|
+
expect(result.files).toEqual([
|
|
38
|
+
"package.json",
|
|
39
|
+
"tsconfig.json",
|
|
40
|
+
"src/run-config.ts",
|
|
41
|
+
"bin/main.ts",
|
|
42
|
+
".env.example",
|
|
43
|
+
"README.md",
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("Step 2 (kumiko add feature) — auto-mounts + walkthrough diff matches", () => {
|
|
48
|
+
scaffoldApp({ name: "my-notes", destination: appRoot });
|
|
49
|
+
const result = scaffoldAppFeature({ name: "notes", appRoot });
|
|
50
|
+
expect(result.autoMounted).toBe(true);
|
|
51
|
+
|
|
52
|
+
const runConfig = readFileSync(join(appRoot, "src/run-config.ts"), "utf-8");
|
|
53
|
+
// Walkthrough's diff-block claims:
|
|
54
|
+
// + import { notesFeature } from "./features/notes";
|
|
55
|
+
// + notesFeature (in APP_FEATURES)
|
|
56
|
+
expect(runConfig).toContain(`import { notesFeature } from "./features/notes";`);
|
|
57
|
+
expect(runConfig).toContain("notesFeature");
|
|
58
|
+
// Foundation still mounted (createSecretsFeature + createSessionsFeature).
|
|
59
|
+
expect(runConfig).toContain("createSecretsFeature()");
|
|
60
|
+
expect(runConfig).toContain("createSessionsFeature()");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("Step 3 (boot validation) — scaffolded run-config matches walkthrough's APP_FEATURES claim", () => {
|
|
64
|
+
scaffoldApp({ name: "my-notes", destination: appRoot });
|
|
65
|
+
scaffoldAppFeature({ name: "notes", appRoot });
|
|
66
|
+
|
|
67
|
+
// Text-assert: scaffolded run-config.ts contains exactly the 3 features
|
|
68
|
+
// the walkthrough's diff-block shows (secrets + sessions + notesFeature).
|
|
69
|
+
// Dynamic-import would fail because /tmp can't resolve @cosmicdrift/*
|
|
70
|
+
// workspace symlinks — instead we reproduce the equivalent APP_FEATURES
|
|
71
|
+
// array in-process below.
|
|
72
|
+
const runConfig = readFileSync(join(appRoot, "src/run-config.ts"), "utf-8");
|
|
73
|
+
expect(runConfig).toContain("createSecretsFeature()");
|
|
74
|
+
expect(runConfig).toContain("createSessionsFeature()");
|
|
75
|
+
expect(runConfig).toContain("notesFeature");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("Step 3 (composeFeatures) — 3 explicit + 4 auto-mounted = 7 features", () => {
|
|
79
|
+
// Reproduces the scaffolded APP_FEATURES in-process. notesFeature gets
|
|
80
|
+
// a dummy defineFeature here — the scaffold-side of "notesFeature"
|
|
81
|
+
// (file-content) is pinned in test 2; this test pins the runtime-side
|
|
82
|
+
// (composeFeatures auto-prepend behaviour the walkthrough claims).
|
|
83
|
+
const notesFeature = defineFeature("notes", () => {});
|
|
84
|
+
const APP_FEATURES = [createSecretsFeature(), createSessionsFeature(), notesFeature];
|
|
85
|
+
|
|
86
|
+
const composed = composeFeatures(APP_FEATURES, { includeBundled: true });
|
|
87
|
+
// 3 explicit + 4 auto-mounted bundled = 7 total features.
|
|
88
|
+
expect(composed.length).toBe(7);
|
|
89
|
+
|
|
90
|
+
const composedNames = composed.map((f) => f.name).sort();
|
|
91
|
+
expect(composedNames).toEqual([
|
|
92
|
+
"auth-email-password",
|
|
93
|
+
"config",
|
|
94
|
+
"notes",
|
|
95
|
+
"secrets",
|
|
96
|
+
"sessions",
|
|
97
|
+
"tenant",
|
|
98
|
+
"user",
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
// validateBoot must pass (no missing-requires, no schema-errors).
|
|
102
|
+
expect(() => validateBoot(composed)).not.toThrow();
|
|
103
|
+
// Registry must contain all 7 features.
|
|
104
|
+
const registry = createRegistry(composed);
|
|
105
|
+
expect(registry.features.size).toBe(7);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("bin/main.ts contains the auth.admin stub the walkthrough relies on", () => {
|
|
109
|
+
scaffoldApp({ name: "my-notes", destination: appRoot });
|
|
110
|
+
const main = readFileSync(join(appRoot, "bin/main.ts"), "utf-8");
|
|
111
|
+
// composeFeatures(includeBundled:true)-trigger is `auth: { admin: { … } }`.
|
|
112
|
+
// Walkthrough explicitly says this is what auto-mounts the 4 bundled features.
|
|
113
|
+
expect(main).toContain("auth: {");
|
|
114
|
+
expect(main).toContain("admin: {");
|
|
115
|
+
expect(main).toContain("memberships:");
|
|
116
|
+
expect(main).toContain("runProdApp");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -82,7 +82,25 @@ const RUNTIME_EXTERNALS = [
|
|
|
82
82
|
// Markierung scheitert bun build an dynamic-imports (z.B. drizzle-kit →
|
|
83
83
|
// @libsql/client). Tree-Shake wirft sie eh aus dem Bundle — der Marker
|
|
84
84
|
// schaltet nur das resolution-during-build ab. NICHT in runtime-deps.
|
|
85
|
-
|
|
85
|
+
//
|
|
86
|
+
// drizzle-kit's dialect-resolver macht dynamic-imports zu allen DB-driver-
|
|
87
|
+
// packages (planetscale/libsql/sqlite/neon/vercel/mysql2). Wir nutzen nur
|
|
88
|
+
// postgres → diese werden never-loaded zur Runtime, aber der Bundler will
|
|
89
|
+
// sie resolven. Aufgedeckt durch C1 Empfehlung 4 (bundle-smoke).
|
|
90
|
+
const BUILD_ONLY_EXTERNALS = [
|
|
91
|
+
"meilisearch",
|
|
92
|
+
"pino",
|
|
93
|
+
"pino-pretty",
|
|
94
|
+
"@aws-sdk/*",
|
|
95
|
+
"@planetscale/database",
|
|
96
|
+
"@libsql/client",
|
|
97
|
+
"better-sqlite3",
|
|
98
|
+
"@neondatabase/serverless",
|
|
99
|
+
"@vercel/postgres",
|
|
100
|
+
"mysql2",
|
|
101
|
+
// ink (kumiko-tui) hat react-devtools-core als dev-only transitive import.
|
|
102
|
+
"react-devtools-core",
|
|
103
|
+
] as const;
|
|
86
104
|
|
|
87
105
|
export type BuildServerBundleOptions = {
|
|
88
106
|
/** App-Root. Default: process.cwd(). */
|
package/src/index.ts
CHANGED
|
@@ -54,6 +54,13 @@ export type {
|
|
|
54
54
|
SignupSetup,
|
|
55
55
|
} from "./run-prod-app";
|
|
56
56
|
export { runProdApp } from "./run-prod-app";
|
|
57
|
+
export type { ScaffoldAppOptions, ScaffoldAppResult } from "./scaffold-app";
|
|
58
|
+
export { scaffoldApp } from "./scaffold-app";
|
|
59
|
+
export type {
|
|
60
|
+
ScaffoldAppFeatureOptions,
|
|
61
|
+
ScaffoldAppFeatureResult,
|
|
62
|
+
} from "./scaffold-app-feature";
|
|
63
|
+
export { runConfigPathForApp, scaffoldAppFeature } from "./scaffold-app-feature";
|
|
57
64
|
export type {
|
|
58
65
|
ScaffoldDeployOptions,
|
|
59
66
|
ScaffoldDeployResult,
|
package/src/run-prod-app.ts
CHANGED
|
@@ -126,23 +126,28 @@ function readEnv(name: string): string | undefined {
|
|
|
126
126
|
return value === undefined || value === "" ? undefined : value;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
// `boot` is the C1 smoke-test path — validators run, no DB/Redis connect,
|
|
130
|
+
// exit after registry-build. Render-modes (human|json|pulumi|k8s|1)
|
|
131
|
+
// inspect the env-schema and exit before any feature wiring.
|
|
132
|
+
type RunMode = DryRunMode | "boot";
|
|
133
|
+
|
|
134
|
+
function parseRunMode(raw: string | undefined): RunMode | null {
|
|
134
135
|
if (!raw) return null;
|
|
135
136
|
const v = raw.toLowerCase();
|
|
136
137
|
if (v === "1" || v === "true" || v === "human") return "human";
|
|
137
|
-
if (v === "json" || v === "pulumi" || v === "k8s") return v;
|
|
138
|
+
if (v === "json" || v === "pulumi" || v === "k8s" || v === "boot") return v;
|
|
138
139
|
// biome-ignore lint/suspicious/noConsole: boot-time warn for typo discovery
|
|
139
140
|
console.warn(
|
|
140
141
|
`[runProdApp] KUMIKO_DRY_RUN_ENV="${raw}" unrecognized ` +
|
|
141
|
-
`(expected 1|human|json|pulumi|k8s); continuing with normal boot.`,
|
|
142
|
+
`(expected 1|human|json|pulumi|k8s|boot); continuing with normal boot.`,
|
|
142
143
|
);
|
|
143
144
|
return null;
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
function isRenderMode(mode: RunMode | null): mode is DryRunMode {
|
|
148
|
+
return mode !== null && mode !== "boot";
|
|
149
|
+
}
|
|
150
|
+
|
|
146
151
|
function defaultBootErrorReporter(err: KumikoBootError): never {
|
|
147
152
|
// biome-ignore lint/suspicious/noConsole: boot-time error, no logger configured yet
|
|
148
153
|
console.error(err.format());
|
|
@@ -485,13 +490,13 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
485
490
|
// at parse-time before the polyfill loads. Plain strings + .regex /
|
|
486
491
|
// .min / .email / .url cover every env-var shape we've actually
|
|
487
492
|
// needed in 9.1's audit (37 references, 25 distinct vars).
|
|
493
|
+
const envSource = options.envSource ?? process.env;
|
|
494
|
+
const runMode = parseRunMode(envSource["KUMIKO_DRY_RUN_ENV"]);
|
|
488
495
|
if (options.envSchema) {
|
|
489
|
-
|
|
490
|
-
const dryRunMode = parseDryRunMode(envSource["KUMIKO_DRY_RUN_ENV"]);
|
|
491
|
-
if (dryRunMode !== null) {
|
|
496
|
+
if (isRenderMode(runMode)) {
|
|
492
497
|
// biome-ignore lint/suspicious/noConsole: dry-run output IS the deliverable
|
|
493
498
|
console.log(
|
|
494
|
-
renderDryRun(options.envSchema,
|
|
499
|
+
renderDryRun(options.envSchema, runMode, {
|
|
495
500
|
...(options.pulumiPrefix ? { pulumiPrefix: options.pulumiPrefix } : {}),
|
|
496
501
|
sources: options.envSchema.sources,
|
|
497
502
|
}),
|
|
@@ -504,6 +509,9 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
504
509
|
}
|
|
505
510
|
return makeDryRunHandle();
|
|
506
511
|
}
|
|
512
|
+
// boot-mode AND normal-boot both run env-validation. boot-mode wants
|
|
513
|
+
// a real env-check (all required vars present + schema-valid) before
|
|
514
|
+
// it asserts feature-wiring works.
|
|
507
515
|
try {
|
|
508
516
|
parseEnv(options.envSchema.schema, envSource, {
|
|
509
517
|
sources: options.envSchema.sources,
|
|
@@ -554,6 +562,22 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
554
562
|
validateBoot(features);
|
|
555
563
|
const registry = createRegistry(features);
|
|
556
564
|
|
|
565
|
+
// C1 boot-mode exit: validators ran, registry built, no DB/Redis
|
|
566
|
+
// operations executed yet (postgres.js + ioredis are lazy). Tear down
|
|
567
|
+
// the lazy clients so Bun doesn't keep them open, then exit / return.
|
|
568
|
+
if (runMode === "boot") {
|
|
569
|
+
// biome-ignore lint/suspicious/noConsole: boot-mode output IS the deliverable
|
|
570
|
+
console.log(
|
|
571
|
+
`[runProdApp] boot validation OK (${features.length} features, ${registry.features.size} registry entries)`,
|
|
572
|
+
);
|
|
573
|
+
await closeDb();
|
|
574
|
+
redis.disconnect();
|
|
575
|
+
if (options.envSource === undefined) {
|
|
576
|
+
process.exit(0);
|
|
577
|
+
}
|
|
578
|
+
return makeDryRunHandle();
|
|
579
|
+
}
|
|
580
|
+
|
|
557
581
|
// Sprint-8a Tier-Composition auto-wire: scan features for a
|
|
558
582
|
// tenantTierResolver-extension. If found AND user didn't supply own
|
|
559
583
|
// effectiveFeatures, build the resolver here (db + registry are
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// scaffoldAppFeature — DX-2. Scaffolds a fresh feature inside an
|
|
2
|
+
// existing Kumiko-app workspace + auto-mounts it in src/run-config.ts.
|
|
3
|
+
//
|
|
4
|
+
// Sister to `scaffoldFeature` (which targets samples/recipes/ for the
|
|
5
|
+
// framework workspace). This one targets `src/features/<name>/` of an
|
|
6
|
+
// already-scaffolded app (output of `kumiko new app`).
|
|
7
|
+
//
|
|
8
|
+
// Auto-mount via ts-morph: opens src/run-config.ts, finds
|
|
9
|
+
// `export const APP_FEATURES = [...]`, prepends import + appends entry.
|
|
10
|
+
// User's promise "defineFeature → nichts woanders eintragen" is met
|
|
11
|
+
// for the run-config side. Drizzle FEATURE_IMPORT_REGISTRY is NOT
|
|
12
|
+
// touched here — DX-4 auto-discovery resolves that.
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
16
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
17
|
+
|
|
18
|
+
export type ScaffoldAppFeatureOptions = {
|
|
19
|
+
/** kebab-case feature name (e.g. "product-catalog"). */
|
|
20
|
+
readonly name: string;
|
|
21
|
+
/** App workspace root. Defaults to cwd. */
|
|
22
|
+
readonly appRoot?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ScaffoldAppFeatureResult = {
|
|
26
|
+
readonly featureName: string;
|
|
27
|
+
readonly featureDir: string;
|
|
28
|
+
readonly files: readonly string[];
|
|
29
|
+
/** Whether src/run-config.ts was auto-mounted. False if run-config
|
|
30
|
+
* is missing — caller gets the scaffolded files but must hand-mount. */
|
|
31
|
+
readonly autoMounted: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const KEBAB_RE = /^[a-z][a-z0-9-]*$/;
|
|
35
|
+
|
|
36
|
+
export function scaffoldAppFeature(options: ScaffoldAppFeatureOptions): ScaffoldAppFeatureResult {
|
|
37
|
+
if (!KEBAB_RE.test(options.name)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`scaffoldAppFeature: name must be kebab-case (a-z, 0-9, -); got "${options.name}"`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const appRoot = resolve(options.appRoot ?? process.cwd());
|
|
43
|
+
const featureDir = join(appRoot, "src", "features", options.name);
|
|
44
|
+
if (existsSync(featureDir)) {
|
|
45
|
+
throw new Error(`scaffoldAppFeature: ${featureDir} already exists — refusing to overwrite`);
|
|
46
|
+
}
|
|
47
|
+
mkdirSync(featureDir, { recursive: true });
|
|
48
|
+
|
|
49
|
+
const files: string[] = [];
|
|
50
|
+
const featureFile = join(featureDir, "feature.ts");
|
|
51
|
+
writeFileSync(featureFile, renderFeature(options.name));
|
|
52
|
+
files.push(`src/features/${options.name}/feature.ts`);
|
|
53
|
+
|
|
54
|
+
const indexFile = join(featureDir, "index.ts");
|
|
55
|
+
writeFileSync(indexFile, renderIndex(options.name));
|
|
56
|
+
files.push(`src/features/${options.name}/index.ts`);
|
|
57
|
+
|
|
58
|
+
const runConfigPath = join(appRoot, "src", "run-config.ts");
|
|
59
|
+
const autoMounted = existsSync(runConfigPath)
|
|
60
|
+
? mountInRunConfig(runConfigPath, options.name)
|
|
61
|
+
: false;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
featureName: options.name,
|
|
65
|
+
featureDir,
|
|
66
|
+
files,
|
|
67
|
+
autoMounted,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderFeature(name: string): string {
|
|
72
|
+
const camel = kebabToCamel(name);
|
|
73
|
+
return `// ${name} feature — scaffolded by \`kumiko add feature\`. Edit freely.
|
|
74
|
+
//
|
|
75
|
+
// Doc-Pointer: https://docs.kumiko.so/en/patterns/ for the \`r.*\` API
|
|
76
|
+
// (r.entity, r.writeHandler, r.queryHandler, hooks, …).
|
|
77
|
+
|
|
78
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
79
|
+
|
|
80
|
+
export const ${camel}Feature = defineFeature("${name}", (r) => {
|
|
81
|
+
// Starter: declare an entity. Drop and replace with your domain.
|
|
82
|
+
r.entity("${name}-item", {
|
|
83
|
+
fields: {
|
|
84
|
+
title: { type: "text", required: true },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderIndex(name: string): string {
|
|
92
|
+
const camel = kebabToCamel(name);
|
|
93
|
+
return `export { ${camel}Feature } from "./feature";\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function kebabToCamel(name: string): string {
|
|
97
|
+
return name.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ts-morph: open run-config, prepend import, append APP_FEATURES entry.
|
|
101
|
+
// Returns true on success, throws on shape-mismatch (caller swallows the
|
|
102
|
+
// scaffolded files but warns).
|
|
103
|
+
function mountInRunConfig(runConfigPath: string, name: string): boolean {
|
|
104
|
+
const camel = kebabToCamel(name);
|
|
105
|
+
const project = new Project({
|
|
106
|
+
skipAddingFilesFromTsConfig: true,
|
|
107
|
+
skipFileDependencyResolution: true,
|
|
108
|
+
});
|
|
109
|
+
const sf = project.addSourceFileAtPath(runConfigPath);
|
|
110
|
+
|
|
111
|
+
// Already mounted? short-circuit (idempotent re-runs).
|
|
112
|
+
const existingImport = sf.getImportDeclaration(`./features/${name}`);
|
|
113
|
+
if (existingImport) return true;
|
|
114
|
+
|
|
115
|
+
// 1. Prepend import after the last existing import.
|
|
116
|
+
const imports = sf.getImportDeclarations();
|
|
117
|
+
const insertIndex = imports.length > 0 ? imports[imports.length - 1]!.getChildIndex() + 1 : 0;
|
|
118
|
+
sf.insertImportDeclaration(insertIndex, {
|
|
119
|
+
moduleSpecifier: `./features/${name}`,
|
|
120
|
+
namedImports: [`${camel}Feature`],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 2. Find `export const APP_FEATURES = [...]` and append the new entry.
|
|
124
|
+
const appFeaturesDecl = sf.getVariableDeclaration("APP_FEATURES");
|
|
125
|
+
if (!appFeaturesDecl) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`mountInRunConfig: ${runConfigPath} has no 'APP_FEATURES' declaration — ` +
|
|
128
|
+
`cannot auto-mount. Hand-edit: add '${camel}Feature' to APP_FEATURES.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const initializer =
|
|
132
|
+
appFeaturesDecl.getInitializerIfKind(SyntaxKind.AsExpression) ??
|
|
133
|
+
appFeaturesDecl.getInitializer();
|
|
134
|
+
if (!initializer) {
|
|
135
|
+
throw new Error(`mountInRunConfig: APP_FEATURES has no initializer — cannot auto-mount.`);
|
|
136
|
+
}
|
|
137
|
+
// Strip `as const` wrapper if present.
|
|
138
|
+
const arr =
|
|
139
|
+
initializer.getKind() === SyntaxKind.AsExpression
|
|
140
|
+
? initializer.getFirstChildByKind(SyntaxKind.ArrayLiteralExpression)
|
|
141
|
+
: initializer.asKind(SyntaxKind.ArrayLiteralExpression);
|
|
142
|
+
if (!arr) {
|
|
143
|
+
throw new Error(`mountInRunConfig: APP_FEATURES is not an array literal — cannot auto-mount.`);
|
|
144
|
+
}
|
|
145
|
+
arr.addElement(`${camel}Feature`);
|
|
146
|
+
|
|
147
|
+
sf.saveSync();
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Re-export so consumers can hint at the file (e.g. for kumiko-cli output).
|
|
152
|
+
export function runConfigPathForApp(appRoot: string): string {
|
|
153
|
+
return join(appRoot, "src", "run-config.ts");
|
|
154
|
+
}
|