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 +25 -0
- package/README.md +3 -0
- package/dist/cli/check.js +16 -0
- package/dist/cli/extensions.d.ts +1 -0
- package/dist/cli/extensions.js +72 -0
- package/dist/cli/init.js +38 -2
- package/dist/cli/studio.js +37 -8
- package/dist/cli/template-registry.d.ts +15 -0
- package/dist/cli/template-registry.js +16 -0
- package/dist/cli.js +13 -56
- package/dist/dsl/frontend-runtime.js +5 -3
- package/dist/dsl/validator-registry.d.ts +11 -0
- package/dist/dsl/validator-registry.js +13 -0
- package/dist/dsl/validator.js +15 -0
- package/dist/extensions/context.d.ts +7 -0
- package/dist/extensions/context.js +14 -3
- package/dist/ir/decl/bundle.js +2 -1
- package/dist/ir/target/backend.policy.d.ts +6 -6
- package/dist/ir/target/electron.policy.d.ts +102 -102
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.js +32 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
package/dist/cli/studio.js
CHANGED
|
@@ -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 => {
|
|
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.
|
|
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 ?
|
|
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
|
-
|
|
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 (
|
|
252
|
+
if (_global.__IR_FRONTEND_APPS.length === 0)
|
|
250
253
|
throw new Error(`Frontend DSL did not call frontend(...)`);
|
|
251
|
-
|
|
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();
|
package/dist/dsl/validator.js
CHANGED
|
@@ -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
|
-
|
|
51
|
+
root: base,
|
|
52
|
+
});
|
|
42
53
|
}
|
package/dist/ir/decl/bundle.js
CHANGED
|
@@ -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
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
988
|
+
level?: "info" | "error" | "warn" | "debug" | undefined;
|
|
989
989
|
redact?: string[] | undefined;
|
|
990
990
|
} | undefined;
|
|
991
991
|
health?: {
|