irgen 0.3.0 → 0.3.2

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 CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes to the `irgen` project will be documented in this file.
4
4
 
5
+ ## [0.3.2] - 2026-02-04
6
+
7
+ ### Enhancements
8
+ - **CLI Execution Order**: Refactored the core entry point to ensure TypeScript support (`tsx`) and extensions (`--ext`) are fully loaded before subcommands (`init`, `studio`, `check`) are executed.
9
+ - **Detailed CLI Help**: Added specialized help messages for `init`, `studio`, and `check` commands, providing better guidance on parameters and extension usage.
10
+ - **Resilient Studio Dashboard**: Updated the Studio UI to be more robust when visualizing complex Hybrid applications (Backend + Frontend) and added better error handling for malformed IR.
11
+
12
+ ### Bug Fixes
13
+ - **Studio Data Loading**: Fixed an issue where the Studio dashboard would appear empty when using extension-specific DSLs due to late extension loading.
14
+
15
+ ## [0.3.1] - 2026-02-03
16
+
17
+ ### New Features
18
+ - **🐘 PHP Hybrid App Platform**: Major enhancement to the `php-shared-hosting` extension, transforming it into a full-stack platform for shared hosting.
19
+ - **Multi-App Support**: Host any number of React SPAs (Admin, Portal, User App) on a single shared hosting account with automatic dynamic routing.
20
+ - **MySQL/REST Engine**: DSL `entity()` declarations automatically generate secure PHP REST controllers using a shared PDO-based storage engine.
21
+ - **Aesthetic Blogging**: Premium flat-file blogging system with refined typography, dark mode, RSS, and Sitemap.
22
+ - **Extension CLI Contributions**: Extensions can now contribute custom logic to core CLI commands.
23
+ - **Pluggable Validators**: Use `ctx.registerValidator` to add custom semantic checks to `irgen check`.
24
+ - **Pluggable Templates**: Use `ctx.registerTemplate` for project starters in `irgen init`.
25
+
26
+ ### Enhancements
27
+ - **Improved Extension Loading**: Unified extension loader with robust path resolution for local files and npm packages.
28
+ - **Improved Multi-DSL Aggragation**: Core now supports flattening and merging applications from multiple DSL files into a single IR bundle.
29
+
5
30
  ## [0.3.0] - 2026-02-01
6
31
 
7
32
  ### New Features
package/README.md CHANGED
@@ -92,6 +92,9 @@ npx irgen examples/app.dsl.ts --targets=backend,frontend --outDir=generated/full
92
92
  # Static-site (HTML-first)
93
93
  npx irgen examples/docs.dsl.ts --targets=static-site --outDir=generated/static-docs
94
94
 
95
+ > [!TIP]
96
+ > **Working with multiple DSL files?** Instead of passing multiple files to the CLI, use a **single entry point** and `import` your other DSL files. This ensures your project structure remains clean and prevents CLI ambiguity.
97
+
95
98
  ## Specialized Commands
96
99
 
97
100
  ### Project Scaffolding
package/dist/cli/check.js CHANGED
@@ -1,6 +1,22 @@
1
1
  import { aggregateDecls } from "../dsl/aggregator.js";
2
2
  import { validateSemantics } from "../dsl/validator.js";
3
3
  export async function runCheck(args) {
4
+ if (args.includes("--help") || args.includes("-h")) {
5
+ console.log(`
6
+ irgen check — Semantic validation for irgen DSL files
7
+
8
+ Usage:
9
+ irgen check <dsl-file>... [options]
10
+
11
+ Options:
12
+ --ext=<path> Load extensions (for extension-specific validation)
13
+ --help, -h Show this help message
14
+
15
+ Example:
16
+ npx irgen check app.dsl.ts ui.dsl.ts --ext=irgen-ext-php-shared-hosting
17
+ `);
18
+ return;
19
+ }
4
20
  const dslFiles = args.filter(a => a.endsWith(".dsl.ts"));
5
21
  if (dslFiles.length === 0) {
6
22
  console.error("Usage: irgen check <dsl-file>...");
@@ -0,0 +1 @@
1
+ export declare function loadExtensions(extModules: string[]): Promise<void>;
@@ -0,0 +1,72 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { pathToFileURL } from "node:url";
4
+ import { createRequire } from "node:module";
5
+ import { createExtensionContext } from "../extensions/context.js";
6
+ const require = createRequire(import.meta.url);
7
+ export async function loadExtensions(extModules) {
8
+ if (!extModules.length)
9
+ return;
10
+ const ctx = createExtensionContext();
11
+ const pickExtensionFn = (mod) => {
12
+ const candidate = (mod?.default ?? mod?.extension ?? mod);
13
+ if (typeof candidate === "function")
14
+ return candidate;
15
+ if (candidate && typeof candidate === "object") {
16
+ if (typeof candidate.default === "function")
17
+ return candidate.default;
18
+ if (typeof candidate.extension === "function")
19
+ return candidate.extension;
20
+ }
21
+ return null;
22
+ };
23
+ const resolveExtensionModule = (spec) => {
24
+ const isPathLike = spec.startsWith(".") || spec.startsWith("/") || /^[A-Za-z]:[\\/]/.test(spec) || fs.existsSync(path.resolve(process.cwd(), spec));
25
+ if (isPathLike) {
26
+ let abs = path.isAbsolute(spec) ? spec : path.resolve(process.cwd(), spec);
27
+ if (!fs.existsSync(abs)) {
28
+ // Try with common extensions if not found
29
+ let found = false;
30
+ for (const ext of [".ts", ".js", ".mjs"]) {
31
+ if (fs.existsSync(abs + ext)) {
32
+ abs = abs + ext;
33
+ found = true;
34
+ break;
35
+ }
36
+ }
37
+ if (!found)
38
+ throw new Error(`extension module not found: ${spec}`);
39
+ }
40
+ return pathToFileURL(abs).href;
41
+ }
42
+ try {
43
+ const resolved = require.resolve(spec, { paths: [process.cwd()] });
44
+ return pathToFileURL(resolved).href;
45
+ }
46
+ catch (e) {
47
+ // Fallback for namespaced packages or modules that might not be in node_modules yet
48
+ return spec;
49
+ }
50
+ };
51
+ for (const modPath of extModules) {
52
+ try {
53
+ const modUrl = resolveExtensionModule(modPath);
54
+ const imported = await import(modUrl);
55
+ const fn = pickExtensionFn(imported);
56
+ if (typeof fn === "function") {
57
+ const metadata = imported.extensionMetadata || imported.metadata || {};
58
+ const name = metadata.name || path.basename(modPath, path.extname(modPath)).replace(/^irgen-ext-/, "");
59
+ const version = metadata.version ? ` v${metadata.version}` : "";
60
+ ctx.logger.info(`Loading extension: ${name}${version}`);
61
+ const namespacedCtx = ctx.namespace(name);
62
+ await fn(namespacedCtx, imported.options ?? undefined);
63
+ }
64
+ else {
65
+ console.warn(`extension module ${modPath} did not export a function`);
66
+ }
67
+ }
68
+ catch (err) {
69
+ console.error(`Failed to load extension "${modPath}": ${err.message}`);
70
+ }
71
+ }
72
+ }
package/dist/cli/init.js CHANGED
@@ -2,10 +2,36 @@ import prompts from "prompts";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  export async function runInit(args) {
5
- const defaultName = args[0] || "my-irgen-app";
5
+ if (args.includes("--help") || args.includes("-h")) {
6
+ const { templateRegistry } = await import("./template-registry.js");
7
+ const extTemplates = templateRegistry.getTemplates();
8
+ console.log(`
9
+ irgen init — Scaffold a new irgen project
10
+
11
+ Usage:
12
+ irgen init <projectName> [options]
13
+
14
+ Options:
15
+ --ext=<path> Load extensions (provides extra templates)
16
+ --help, -h Show this help message
17
+
18
+ Available Templates:
19
+ - fullstack Standard Backend + Frontend (React)
20
+ - backend Node.js/Prisma backend only
21
+ - frontend React/Vite frontend only
22
+ ${extTemplates.map(t => ` - ${t.id.padEnd(18)} ${t.title}`).join("\n")}
23
+
24
+ Example:
25
+ npx irgen init my-blog --ext=irgen-ext-php-shared-hosting --template=php-blog
26
+ `);
27
+ return;
28
+ }
29
+ const { templateRegistry } = await import("./template-registry.js");
30
+ const extTemplates = templateRegistry.getTemplates();
31
+ const defaultName = args.find(a => !a.startsWith("-")) || "my-irgen-app";
6
32
  const response = await prompts([
7
33
  {
8
- type: args[0] ? null : "text",
34
+ type: (args.find(a => !a.startsWith("-"))) ? null : "text",
9
35
  name: "projectName",
10
36
  message: "Project name:",
11
37
  initial: defaultName
@@ -18,6 +44,7 @@ export async function runInit(args) {
18
44
  { title: "Fullstack (Backend + Frontend)", value: "fullstack" },
19
45
  { title: "Backend Only", value: "backend" },
20
46
  { title: "Frontend Only", value: "frontend" },
47
+ ...extTemplates.map(t => ({ title: t.title, value: t.id }))
21
48
  ],
22
49
  initial: 0
23
50
  }
@@ -29,6 +56,15 @@ export async function runInit(args) {
29
56
  console.error(`Error: Directory ${projectName} already exists.`);
30
57
  process.exit(1);
31
58
  }
59
+ // Handle extension templates
60
+ const extTemplate = templateRegistry.getTemplate(template);
61
+ if (extTemplate) {
62
+ console.log(`\nScaffolding project in ${projectDir} using template ${extTemplate.title}...`);
63
+ fs.mkdirSync(projectDir, { recursive: true });
64
+ await extTemplate.generate(projectDir, response);
65
+ console.log("\nDone!");
66
+ return;
67
+ }
32
68
  console.log(`\nScaffolding project in ${projectDir}...`);
33
69
  fs.mkdirSync(projectDir, { recursive: true });
34
70
  // 1. package.json
@@ -4,6 +4,25 @@ import { aggregateDecls } from "../dsl/aggregator.js";
4
4
  import path from "node:path";
5
5
  import fs from "node:fs";
6
6
  export async function runStudio(args) {
7
+ if (args.includes("--help") || args.includes("-h")) {
8
+ console.log(`
9
+ irgen studio — Interactive DSL Dashboard
10
+
11
+ Usage:
12
+ irgen studio <dsl-file>... [options]
13
+
14
+ Options:
15
+ --ext=<path> Load extensions (to visualize custom mappers/emitters)
16
+ --help, -h Show this help message
17
+
18
+ The Studio provides a real-time visual representation of your system's
19
+ entities, pages, and components as defined in your DSL.
20
+
21
+ Example:
22
+ npx irgen studio app.dsl.ts ui.dsl.ts --ext=irgen-ext-php-shared-hosting
23
+ `);
24
+ return;
25
+ }
7
26
  const dslFiles = args.filter(a => a.endsWith(".dsl.ts"));
8
27
  if (dslFiles.length === 0) {
9
28
  console.error("Usage: irgen studio <dsl-file>...");
@@ -40,7 +59,6 @@ export async function runStudio(args) {
40
59
  res.json(currentIR);
41
60
  });
42
61
  // Static UI
43
- // For now, let's embed a simple but beautiful HTML
44
62
  app.get("/", (req, res) => {
45
63
  res.send(getStudioHtml());
46
64
  });
@@ -278,14 +296,17 @@ function getStudioHtml() {
278
296
  document.getElementById('header-content').innerHTML = '<h2>Project Overview</h2><p>Summary of discovered resources</p>';
279
297
  let totalEntities = 0;
280
298
  let totalPages = 0;
281
- ir.apps.forEach(a => { totalEntities += (a.entities?.length || 0); totalPages += (a.pages?.length || 0); });
299
+ ir.apps.forEach(a => {
300
+ if (a.entities) totalEntities += a.entities.length;
301
+ if (a.pages) totalPages += a.pages.length;
302
+ });
282
303
 
283
304
  const content = document.getElementById('content');
284
305
  content.innerHTML = \`
285
306
  <div class="stats-row">
286
307
  <div class="stat-card">
287
308
  <div class="stat-value">\${ir.apps.length}</div>
288
- <div class="stat-label">Applications</div>
309
+ <div class="stat-label">Applications / Modules</div>
289
310
  </div>
290
311
  <div class="stat-card">
291
312
  <div class="stat-value">\${totalEntities}</div>
@@ -297,14 +318,22 @@ function getStudioHtml() {
297
318
  </div>
298
319
  </div>
299
320
  <div class="card">
300
- <h3>Metadata</h3>
321
+ <h3>Metadata & Configuration</h3>
301
322
  <table class="mono">
302
- \${Object.entries(ir.apps[0]?.meta || {}).map(([k,v]) => \`
323
+ \${Object.entries(ir.meta || {}).map(([k,v]) => \`
303
324
  <tr>
304
- <td style="color: var(--accent)">\${k}</td>
305
- <td>\${JSON.stringify(v)}</td>
325
+ <td style="color: var(--accent); width: 200px">\${k}</td>
326
+ <td><pre style="margin:0; white-space: pre-wrap">\${JSON.stringify(v, null, 2)}</pre></td>
306
327
  </tr>
307
328
  \`).join('')}
329
+ \${ir.apps.map(app =>
330
+ Object.entries(app.meta || {}).map(([k,v]) => \`
331
+ <tr>
332
+ <td style="color: var(--text-dim)">[\${app.name}] \${k}</td>
333
+ <td><pre style="margin:0; white-space: pre-wrap">\${JSON.stringify(v, null, 2)}</pre></td>
334
+ </tr>
335
+ \`).join('')
336
+ ).join('')}
308
337
  </table>
309
338
  </div>
310
339
  \`;
@@ -352,7 +381,7 @@ function getStudioHtml() {
352
381
  <strong>\${comp.name}</strong>
353
382
  \${comp.entityRef ? '<span class="badge">Bound</span>' : ''}
354
383
  </div>
355
- \${comp.entityRef ? \`<div class="stat-label">Entity: \${comp.entityRef}</div>\` : ''}
384
+ \${comp.entityRef ? \\\`<div class="stat-label">Entity: \\\${comp.entityRef}</div>\\\` : ''}
356
385
  </div>
357
386
  \`).join('')}
358
387
  </div>
@@ -0,0 +1,15 @@
1
+ export type TemplateDefinition = {
2
+ id: string;
3
+ title: string;
4
+ description?: string;
5
+ generate: (projectDir: string, options: any) => Promise<void> | void;
6
+ };
7
+ declare class TemplateRegistry {
8
+ private templates;
9
+ register(template: TemplateDefinition): void;
10
+ getTemplates(): TemplateDefinition[];
11
+ getTemplate(id: string): TemplateDefinition | undefined;
12
+ clear(): void;
13
+ }
14
+ export declare const templateRegistry: TemplateRegistry;
15
+ export {};
@@ -0,0 +1,16 @@
1
+ class TemplateRegistry {
2
+ templates = new Map();
3
+ register(template) {
4
+ this.templates.set(template.id, template);
5
+ }
6
+ getTemplates() {
7
+ return Array.from(this.templates.values());
8
+ }
9
+ getTemplate(id) {
10
+ return this.templates.get(id);
11
+ }
12
+ clear() {
13
+ this.templates.clear();
14
+ }
15
+ }
16
+ export const templateRegistry = new TemplateRegistry();
package/dist/cli.js CHANGED
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
- import fs from "node:fs";
4
- import { pathToFileURL } from "node:url";
5
- import { createRequire } from "node:module";
6
3
  import { aggregateDecls } from "./dsl/aggregator.js";
7
4
  import { registerBuiltins, runMapper } from "./mappers/index.js";
5
+ import { loadExtensions } from "./cli/extensions.js";
8
6
  // Guard: require a modern Node (tsx loader relies on module.register)
9
7
  const NODE_MAJOR = Number(process.versions.node.split(".")[0]);
10
8
  if (Number.isFinite(NODE_MAJOR) && NODE_MAJOR < 18) {
@@ -46,6 +44,17 @@ Examples:
46
44
  console.log("irgen 0.1.0");
47
45
  process.exit(0);
48
46
  }
47
+ try {
48
+ const { register } = await import("tsx/esm/api");
49
+ register();
50
+ }
51
+ catch (err) {
52
+ console.warn("tsx loader unavailable; falling back to manual TS transpile where supported.", err instanceof Error ? err.message : err);
53
+ }
54
+ const rawArgs = process.argv.slice(2);
55
+ const extFlags = rawArgs.filter(a => a.startsWith("--ext="));
56
+ const extModules = extFlags.flatMap(f => f.replace("--ext=", "").split(",")).filter(Boolean);
57
+ await loadExtensions(extModules);
49
58
  if (process.argv[2] === "check") {
50
59
  const { runCheck } = await import("./cli/check.js");
51
60
  await runCheck(process.argv.slice(3));
@@ -61,13 +70,6 @@ Examples:
61
70
  await runInit(process.argv.slice(3));
62
71
  process.exit(0);
63
72
  }
64
- try {
65
- const { register } = await import("tsx/esm/api");
66
- register();
67
- }
68
- catch (err) {
69
- console.warn("tsx loader unavailable; falling back to manual TS transpile where supported.", err instanceof Error ? err.message : err);
70
- }
71
73
  const modeFlag = process.argv.find(a => a.startsWith("--mode=")) ?? "--mode=backend";
72
74
  const mode = modeFlag.split("=")[1];
73
75
  // Additional flags: --emitters (list emitters), --emitter=<name> (run one emitter)
@@ -75,9 +77,6 @@ Examples:
75
77
  const emitterFlag = process.argv.find(a => a.startsWith("--emitter=")) ?? null;
76
78
  const emitterName = emitterFlag ? emitterFlag.split("=")[1] : null;
77
79
  // parse positional args: first DSL entry and optional outDir
78
- const rawArgs = process.argv.slice(2);
79
- const extFlags = rawArgs.filter(a => a.startsWith("--ext="));
80
- const extModules = extFlags.flatMap(f => f.replace("--ext=", "").split(",")).filter(Boolean);
81
80
  const outDirFlag = process.argv.find(a => a.startsWith("--outDir=")) ?? null;
82
81
  const entries = rawArgs.filter(a => a.endsWith(".dsl.ts"));
83
82
  const entry = entries[0];
@@ -198,49 +197,7 @@ Examples:
198
197
  console.log("INSPECT-DECL:", JSON.stringify(unified, null, 2));
199
198
  // ensure built-in mappers are available before extensions register/compose
200
199
  registerBuiltins();
201
- async function loadExtensions() {
202
- if (!extModules.length)
203
- return;
204
- const { createExtensionContext } = await import("./extensions/context.js");
205
- const ctx = createExtensionContext();
206
- const require = createRequire(import.meta.url);
207
- const pickExtensionFn = (mod) => {
208
- const candidate = (mod?.default ?? mod?.extension ?? mod);
209
- if (typeof candidate === "function")
210
- return candidate;
211
- if (candidate && typeof candidate === "object") {
212
- if (typeof candidate.default === "function")
213
- return candidate.default;
214
- if (typeof candidate.extension === "function")
215
- return candidate.extension;
216
- }
217
- return null;
218
- };
219
- const resolveExtensionModule = (spec) => {
220
- const isPathLike = path.isAbsolute(spec) || spec.startsWith(".") || spec.startsWith("/") || /^[A-Za-z]:[\\/]/.test(spec);
221
- if (isPathLike) {
222
- const abs = path.isAbsolute(spec) ? spec : path.resolve(process.cwd(), spec);
223
- if (!fs.existsSync(abs)) {
224
- throw new Error(`extension module not found: ${spec}`);
225
- }
226
- return pathToFileURL(abs).href;
227
- }
228
- const resolved = require.resolve(spec, { paths: [process.cwd()] });
229
- return pathToFileURL(resolved).href;
230
- };
231
- for (const modPath of extModules) {
232
- const modUrl = resolveExtensionModule(modPath);
233
- const imported = await import(modUrl);
234
- const fn = pickExtensionFn(imported);
235
- if (typeof fn === "function") {
236
- await fn(ctx, imported.options ?? undefined);
237
- }
238
- else {
239
- console.warn(`extension module ${modPath} did not export a function`);
240
- }
241
- }
242
- }
243
- await loadExtensions();
200
+ await loadExtensions(extModules);
244
201
  const bundlePolicies = unified?.meta?.policies;
245
202
  const pickPolicy = (src, target) => {
246
203
  if (!src)
@@ -3,6 +3,7 @@ import { pathToFileURL } from "node:url";
3
3
  import { DeclFrontendAppSchema } from "../ir/decl/frontend.raw.schema.js";
4
4
  // Use globalThis to share state across multiple module instances (e.g., src vs dist)
5
5
  const _global = globalThis;
6
+ _global.__IR_FRONTEND_APPS = _global.__IR_FRONTEND_APPS || [];
6
7
  _global.__IR_CURRENT_FRONTEND = _global.__IR_CURRENT_FRONTEND || null;
7
8
  function assert(cond, msg) {
8
9
  if (!cond)
@@ -212,12 +213,14 @@ export function frontend(name, optsOrFn, maybeFn) {
212
213
  });
213
214
  const parsed = DeclFrontendAppSchema.parse(_global.__IR_CURRENT_FRONTEND);
214
215
  _global.__IR_CURRENT_FRONTEND = parsed;
216
+ _global.__IR_FRONTEND_APPS.push(parsed);
215
217
  return parsed;
216
218
  }
217
219
  export async function loadFrontendDsl(entry) {
218
220
  const abs = path.resolve(process.cwd(), entry);
219
221
  const url = pathToFileURL(abs).href;
220
222
  // reset
223
+ _global.__IR_FRONTEND_APPS = [];
221
224
  _global.__IR_CURRENT_FRONTEND = null;
222
225
  try {
223
226
  // Add cache buster to force re-execution if file is already in module cache
@@ -246,8 +249,7 @@ export async function loadFrontendDsl(entry) {
246
249
  throw new Error(`Failed to load frontend DSL (${entry}): ${err2?.message ?? err2}`);
247
250
  }
248
251
  }
249
- if (!_global.__IR_CURRENT_FRONTEND)
252
+ if (_global.__IR_FRONTEND_APPS.length === 0)
250
253
  throw new Error(`Frontend DSL did not call frontend(...)`);
251
- const parsed = DeclFrontendAppSchema.parse(_global.__IR_CURRENT_FRONTEND);
252
- return parsed;
254
+ return _global.__IR_FRONTEND_APPS.length === 1 ? _global.__IR_FRONTEND_APPS[0] : _global.__IR_FRONTEND_APPS;
253
255
  }
@@ -0,0 +1,11 @@
1
+ import { DeclBundle } from "../ir/decl/index.js";
2
+ import { ValidationMessage } from "./validator.js";
3
+ export type ValidatorFn = (bundle: DeclBundle) => ValidationMessage[];
4
+ declare class ValidatorRegistry {
5
+ private validators;
6
+ register(id: string, validator: ValidatorFn): void;
7
+ getValidators(): ValidatorFn[];
8
+ clear(): void;
9
+ }
10
+ export declare const validatorRegistry: ValidatorRegistry;
11
+ export {};
@@ -0,0 +1,13 @@
1
+ class ValidatorRegistry {
2
+ validators = new Map();
3
+ register(id, validator) {
4
+ this.validators.set(id, validator);
5
+ }
6
+ getValidators() {
7
+ return Array.from(this.validators.values());
8
+ }
9
+ clear() {
10
+ this.validators.clear();
11
+ }
12
+ }
13
+ export const validatorRegistry = new ValidatorRegistry();
@@ -1,3 +1,4 @@
1
+ import { validatorRegistry } from "./validator-registry.js";
1
2
  export function validateSemantics(bundle) {
2
3
  const messages = [];
3
4
  const entities = new Set();
@@ -67,5 +68,19 @@ export function validateSemantics(bundle) {
67
68
  }
68
69
  }
69
70
  }
71
+ // 4. Extension Validators (New in v0.3.1)
72
+ const extValidators = validatorRegistry.getValidators();
73
+ for (const v of extValidators) {
74
+ try {
75
+ const extMessages = v(bundle);
76
+ messages.push(...extMessages);
77
+ }
78
+ catch (err) {
79
+ messages.push({
80
+ type: "warning",
81
+ message: `Extension validator failed: ${err.message}`
82
+ });
83
+ }
84
+ }
70
85
  return messages;
71
86
  }
@@ -3,6 +3,9 @@ import { engine as loweringEngine } from "../lowering/engine.js";
3
3
  import { emitterEngine } from "../emit/engine.js";
4
4
  import { registerTargetEmitter } from "../emit/registry.js";
5
5
  import * as frontendRegistry from "../emit/frontend/registry.js";
6
+ import { ValidatorFn } from "../dsl/validator-registry.js";
7
+ import { TemplateDefinition } from "../cli/template-registry.js";
8
+ import { Logger } from "../utils/logger.js";
6
9
  export type ExtensionContext = {
7
10
  registerMapper: typeof registerMapper;
8
11
  unregisterMapper: typeof unregisterMapper;
@@ -16,7 +19,11 @@ export type ExtensionContext = {
16
19
  registerEnvelopeAdapter: typeof frontendRegistry.envelopeAdapters.register;
17
20
  registerPaginationAdapter: typeof frontendRegistry.paginationAdapters.register;
18
21
  registerUIComponent: typeof frontendRegistry.uiComponents.register;
22
+ registerValidator: (id: string, fn: ValidatorFn) => void;
23
+ registerTemplate: (template: TemplateDefinition) => void;
24
+ logger: Logger;
19
25
  namespace: (ns: string) => ExtensionContext;
26
+ root: ExtensionContext;
20
27
  };
21
28
  export declare function createExtensionContext(): ExtensionContext;
22
29
  export type { MapperFn } from "../types/extension.js";
@@ -3,6 +3,9 @@ import { engine as loweringEngine } from "../lowering/engine.js";
3
3
  import { emitterEngine } from "../emit/engine.js";
4
4
  import { registerTargetEmitter } from "../emit/registry.js";
5
5
  import * as frontendRegistry from "../emit/frontend/registry.js";
6
+ import { validatorRegistry } from "../dsl/validator-registry.js";
7
+ import { templateRegistry } from "../cli/template-registry.js";
8
+ import { logger as baseLogger } from "../utils/logger.js";
6
9
  function namespaced(ctx, ns) {
7
10
  const prefix = (name) => (name.includes(":") ? name : `${ns}:${name}`);
8
11
  return {
@@ -19,11 +22,16 @@ function namespaced(ctx, ns) {
19
22
  registerEnvelopeAdapter: (id, item, options) => ctx.registerEnvelopeAdapter(prefix(id), item, options),
20
23
  registerPaginationAdapter: (id, item, options) => ctx.registerPaginationAdapter(prefix(id), item, options),
21
24
  registerUIComponent: (id, item, options) => ctx.registerUIComponent(prefix(id), item, options),
25
+ registerValidator: (id, fn) => ctx.registerValidator(prefix(id), fn),
26
+ registerTemplate: (template) => ctx.registerTemplate({ ...template, id: prefix(template.id) }),
27
+ logger: ctx.logger.child(ns),
22
28
  namespace: (child) => namespaced(ctx, `${ns}:${child}`),
29
+ root: ctx.root,
23
30
  };
24
31
  }
25
32
  export function createExtensionContext() {
26
- const base = {
33
+ const base = {};
34
+ return Object.assign(base, {
27
35
  registerMapper,
28
36
  unregisterMapper,
29
37
  listMappers,
@@ -36,7 +44,10 @@ export function createExtensionContext() {
36
44
  registerEnvelopeAdapter: frontendRegistry.envelopeAdapters.register.bind(frontendRegistry.envelopeAdapters),
37
45
  registerPaginationAdapter: frontendRegistry.paginationAdapters.register.bind(frontendRegistry.paginationAdapters),
38
46
  registerUIComponent: frontendRegistry.uiComponents.register.bind(frontendRegistry.uiComponents),
47
+ registerValidator: (id, fn) => validatorRegistry.register(id, fn),
48
+ registerTemplate: (template) => templateRegistry.register(template),
49
+ logger: baseLogger,
39
50
  namespace: (ns) => namespaced(base, ns),
40
- };
41
- return base;
51
+ root: base,
52
+ });
42
53
  }
@@ -1,6 +1,7 @@
1
1
  // Pure bundle constructor: only wraps values; merge/conflict resolution lives in aggregator/normalize.
2
2
  export function asBundle(apps, meta) {
3
- const bundle = { apps: Array.isArray(apps) ? apps : [apps] };
3
+ const appsArr = Array.isArray(apps) ? apps : [apps];
4
+ const bundle = { apps: appsArr.flat() };
4
5
  if (meta)
5
6
  bundle.meta = meta;
6
7
  return bundle;
@@ -437,12 +437,12 @@ export declare const BackendLoggingPolicySchema: z.ZodDefault<z.ZodObject<{
437
437
  }, "strip", z.ZodTypeAny, {
438
438
  enabled: boolean;
439
439
  format: "json" | "pretty";
440
- level: "info" | "error" | "debug" | "warn";
440
+ level: "info" | "error" | "warn" | "debug";
441
441
  redact: string[];
442
442
  }, {
443
443
  enabled?: boolean | undefined;
444
444
  format?: "json" | "pretty" | undefined;
445
- level?: "info" | "error" | "debug" | "warn" | undefined;
445
+ level?: "info" | "error" | "warn" | "debug" | undefined;
446
446
  redact?: string[] | undefined;
447
447
  }>>;
448
448
  export declare const BackendHealthPolicySchema: z.ZodDefault<z.ZodObject<{
@@ -770,12 +770,12 @@ export declare const BackendPolicySchema: z.ZodDefault<z.ZodObject<{
770
770
  }, "strip", z.ZodTypeAny, {
771
771
  enabled: boolean;
772
772
  format: "json" | "pretty";
773
- level: "info" | "error" | "debug" | "warn";
773
+ level: "info" | "error" | "warn" | "debug";
774
774
  redact: string[];
775
775
  }, {
776
776
  enabled?: boolean | undefined;
777
777
  format?: "json" | "pretty" | undefined;
778
- level?: "info" | "error" | "debug" | "warn" | undefined;
778
+ level?: "info" | "error" | "warn" | "debug" | undefined;
779
779
  redact?: string[] | undefined;
780
780
  }>>;
781
781
  health: z.ZodDefault<z.ZodObject<{
@@ -902,7 +902,7 @@ export declare const BackendPolicySchema: z.ZodDefault<z.ZodObject<{
902
902
  logging: {
903
903
  enabled: boolean;
904
904
  format: "json" | "pretty";
905
- level: "info" | "error" | "debug" | "warn";
905
+ level: "info" | "error" | "warn" | "debug";
906
906
  redact: string[];
907
907
  };
908
908
  health: {
@@ -985,7 +985,7 @@ export declare const BackendPolicySchema: z.ZodDefault<z.ZodObject<{
985
985
  logging?: {
986
986
  enabled?: boolean | undefined;
987
987
  format?: "json" | "pretty" | undefined;
988
- level?: "info" | "error" | "debug" | "warn" | undefined;
988
+ level?: "info" | "error" | "warn" | "debug" | undefined;
989
989
  redact?: string[] | undefined;
990
990
  } | undefined;
991
991
  health?: {