irgen 0.2.2 → 0.3.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 +24 -0
- package/README.md +25 -6
- package/dist/cli/check.d.ts +1 -0
- package/dist/cli/check.js +39 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +111 -0
- package/dist/cli/studio.d.ts +1 -0
- package/dist/cli/studio.js +369 -0
- package/dist/cli.js +15 -0
- package/dist/dsl/frontend-runtime.js +3 -1
- package/dist/dsl/runtime.js +3 -1
- package/dist/dsl/validator.d.ts +7 -0
- package/dist/dsl/validator.js +71 -0
- package/dist/emit/backend/adapters.d.ts +1 -0
- package/dist/emit/backend/adapters.js +63 -31
- package/dist/emit/backend/backend-tsmorph.js +146 -114
- package/dist/emit/backend/packaging.js +16 -5
- package/dist/emit/backend/server.js +10 -2
- package/dist/emit/frontend/frontend-components.js +39 -6
- package/dist/emit/frontend/frontend-error-boundary.d.ts +3 -0
- package/dist/emit/frontend/frontend-error-boundary.js +55 -0
- package/dist/emit/frontend/frontend-react.js +19 -3
- package/dist/ir/target/backend.policy.d.ts +116 -0
- package/dist/ir/target/backend.policy.js +17 -0
- package/dist/ir/target/electron.policy.d.ts +152 -152
- package/dist/ir/target/frontend.policy.d.ts +347 -28
- package/dist/ir/target/frontend.policy.js +8 -1
- package/package.json +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `irgen` project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-02-01
|
|
6
|
+
|
|
7
|
+
### New Features
|
|
8
|
+
- **Project Scaffolding**: Introduced `irgen init` for interactive project setup and best-practice templates.
|
|
9
|
+
- **Semantic Validator (Linker)**: New `irgen check` command to validate DSL integrity, including entity references, name uniqueness, and cross-file consistency.
|
|
10
|
+
- **Studio Preview Dashboard**: Real-time web-based visualization tool (`irgen studio`) to see project architecture and component trees as you edit.
|
|
11
|
+
- **Structured Logging**: Built-in `pino` integration in generated backends for production-ready observability.
|
|
12
|
+
- **Health & Metrics**: Automatic generation of `/health` and `/metrics` (Prometheus) endpoints.
|
|
13
|
+
- **Error Boundary Contract**: Policy-driven Error Boundary for frontend reliability and user feedback.
|
|
14
|
+
|
|
15
|
+
### Enhancements
|
|
16
|
+
- **CLI Robustness**: Resolved command fall-through issues and improved error reporting.
|
|
17
|
+
- **Documentation**: Significant updates to policy references, architecture guides, and roadmaps.
|
|
18
|
+
|
|
19
|
+
## [0.2.3] - 2026-01-21
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
- **Frontend Emitter**: Fixed `ReferenceError: rowActionIcons is not defined` in `frontend-react.ts` when rendering table row actions.
|
|
23
|
+
- **Frontend Components**: Resolved `[object Object]` AST leak in code blocks by ensuring `React` is correctly imported and utilized for runtime element creation.
|
|
24
|
+
|
|
25
|
+
### Documentation & Verification
|
|
26
|
+
- **Test Suite**: Verified all core golden tests (Backend, Electron, Static Site) pass with the latest fixes.
|
|
27
|
+
- **Cleanup**: Improved documentation quality and consistency in core emitters.
|
|
28
|
+
|
|
5
29
|
## [0.2.2] - 2026-01-18
|
|
6
30
|
|
|
7
31
|
### Core Extensions & Architecture (Phase 10)
|
package/README.md
CHANGED
|
@@ -18,15 +18,16 @@ irgen focuses on **architecture and determinism**, not convenience shortcuts. ir
|
|
|
18
18
|
## Key Features
|
|
19
19
|
|
|
20
20
|
### Backend (Node.js/TypeScript)
|
|
21
|
-
- **Generation Gap Architecture**: Separates generated base classes from user implementation.
|
|
22
|
-
- **
|
|
21
|
+
- **Generation Gap Architecture**: Separates generated base classes from user implementation.
|
|
22
|
+
- **Built-in Logging**: Structured JSON logging via `pino`.
|
|
23
|
+
- **Health-check Emitter**: Automatic `/health` and `/metrics` (Prometheus) generation.
|
|
23
24
|
- **Prisma Integration**: Database schema and client management out-of-the-box.
|
|
24
|
-
- **
|
|
25
|
-
- **Automated Testing**: Auto-generated `vitest` unit tests for services.
|
|
25
|
+
- **Automated Testing**: Auto-generated `vitest` unit tests.
|
|
26
26
|
|
|
27
27
|
### Frontend (React/Vite)
|
|
28
|
-
- **General-Purpose Webapp Generator**: Move beyond "dashboard-only" UIs.
|
|
29
|
-
- **
|
|
28
|
+
- **General-Purpose Webapp Generator**: Move beyond "dashboard-only" UIs.
|
|
29
|
+
- **Error Boundary Contract**: Wrap your application in a policy-driven Error Boundary.
|
|
30
|
+
- **Headless Client Runtime**: Decoupled `lib/runtime.ts` and React hooks.
|
|
30
31
|
- **Operation-Oriented Architecture**: Actions (Operations) are the fundamental units of the client, allowing for complex command-oriented UIs.
|
|
31
32
|
- **DataSource Abstraction**: Connect to multiple backends with pluggable `AuthStrategy`, `EnvelopeAdapter`, and `PaginationAdapter`.
|
|
32
33
|
- **Global Dark Mode**: Built-in persistence (`localStorage`) and toggle in a sleek, glassmorphism Navbar.
|
|
@@ -90,6 +91,17 @@ npx irgen examples/app.dsl.ts --targets=backend,frontend --outDir=generated/full
|
|
|
90
91
|
|
|
91
92
|
# Static-site (HTML-first)
|
|
92
93
|
npx irgen examples/docs.dsl.ts --targets=static-site --outDir=generated/static-docs
|
|
94
|
+
|
|
95
|
+
## Specialized Commands
|
|
96
|
+
|
|
97
|
+
### Project Scaffolding
|
|
98
|
+
npx irgen init my-new-project
|
|
99
|
+
|
|
100
|
+
### Semantic Validation (Linker)
|
|
101
|
+
npx irgen check examples/*.dsl.ts
|
|
102
|
+
|
|
103
|
+
### Preview Studio
|
|
104
|
+
npx irgen studio examples/app.dsl.ts
|
|
93
105
|
```
|
|
94
106
|
|
|
95
107
|
### Optional: enable PWA for frontend outputs
|
|
@@ -160,3 +172,10 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for details on:
|
|
|
160
172
|
- [x] Automated Testing
|
|
161
173
|
- [x] Rich Frontend Components & Routing
|
|
162
174
|
- [x] Frontend SSG/Hybrid (Vite prerender)
|
|
175
|
+
- [x] **v0.3.0 Release (Enterprise & Observability)**
|
|
176
|
+
- [x] Structured Logging (Pino)
|
|
177
|
+
- [x] Health-check & Metrics Emitters
|
|
178
|
+
- [x] Error Boundary Policy
|
|
179
|
+
- [x] `irgen init`, `check`, and `studio` commands
|
|
180
|
+
|
|
181
|
+
See [docs/ROADMAP-FUTURE.md](docs/ROADMAP-FUTURE.md) for the Stage 4 (Deployment & Cloud Native) plan.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCheck(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { aggregateDecls } from "../dsl/aggregator.js";
|
|
2
|
+
import { validateSemantics } from "../dsl/validator.js";
|
|
3
|
+
export async function runCheck(args) {
|
|
4
|
+
const dslFiles = args.filter(a => a.endsWith(".dsl.ts"));
|
|
5
|
+
if (dslFiles.length === 0) {
|
|
6
|
+
console.error("Usage: irgen check <dsl-file>...");
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
console.log(`Checking semantic integrity for: ${dslFiles.join(", ")}...`);
|
|
10
|
+
try {
|
|
11
|
+
const decl = await aggregateDecls(dslFiles);
|
|
12
|
+
const messages = validateSemantics(decl);
|
|
13
|
+
if (messages.length === 0) {
|
|
14
|
+
console.log("✅ No semantic errors found.");
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
let hasError = false;
|
|
18
|
+
for (const msg of messages) {
|
|
19
|
+
const icon = msg.type === "error" ? "❌" : "⚠️";
|
|
20
|
+
const loc = msg.location ? ` [${msg.location}]` : "";
|
|
21
|
+
console.log(`${icon} ${msg.type.toUpperCase()}: ${msg.message}${loc}`);
|
|
22
|
+
if (msg.type === "error") {
|
|
23
|
+
hasError = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (hasError) {
|
|
27
|
+
console.error("\nValidation failed.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log("\nValidation passed (with warnings).");
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
console.error("Failed to load or validate DSL:", err.message);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runInit(args: string[]): Promise<void>;
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export async function runInit(args) {
|
|
5
|
+
const defaultName = args[0] || "my-irgen-app";
|
|
6
|
+
const response = await prompts([
|
|
7
|
+
{
|
|
8
|
+
type: args[0] ? null : "text",
|
|
9
|
+
name: "projectName",
|
|
10
|
+
message: "Project name:",
|
|
11
|
+
initial: defaultName
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
type: "select",
|
|
15
|
+
name: "template",
|
|
16
|
+
message: "Select a startup template:",
|
|
17
|
+
choices: [
|
|
18
|
+
{ title: "Fullstack (Backend + Frontend)", value: "fullstack" },
|
|
19
|
+
{ title: "Backend Only", value: "backend" },
|
|
20
|
+
{ title: "Frontend Only", value: "frontend" },
|
|
21
|
+
],
|
|
22
|
+
initial: 0
|
|
23
|
+
}
|
|
24
|
+
]);
|
|
25
|
+
const projectName = response.projectName || defaultName;
|
|
26
|
+
const template = response.template || "fullstack";
|
|
27
|
+
const projectDir = path.resolve(process.cwd(), projectName);
|
|
28
|
+
if (fs.existsSync(projectDir)) {
|
|
29
|
+
console.error(`Error: Directory ${projectName} already exists.`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
console.log(`\nScaffolding project in ${projectDir}...`);
|
|
33
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
34
|
+
// 1. package.json
|
|
35
|
+
const pkgJson = {
|
|
36
|
+
name: projectName,
|
|
37
|
+
version: "0.0.1",
|
|
38
|
+
type: "module",
|
|
39
|
+
scripts: {
|
|
40
|
+
"gen": "irgen examples/app.dsl.ts --mode=combined",
|
|
41
|
+
"gen:backend": "irgen examples/app.dsl.ts --mode=backend",
|
|
42
|
+
"gen:frontend": "irgen examples/app.dsl.ts --mode=frontend",
|
|
43
|
+
},
|
|
44
|
+
dependencies: {
|
|
45
|
+
"irgen": "latest" // In a real scenario this would be the actual version
|
|
46
|
+
},
|
|
47
|
+
devDependencies: {
|
|
48
|
+
"typescript": "^5.0.0",
|
|
49
|
+
"tsx": "^4.0.0"
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
if (template === "backend") {
|
|
53
|
+
if (pkgJson.scripts["gen:frontend"])
|
|
54
|
+
delete pkgJson.scripts["gen:frontend"];
|
|
55
|
+
pkgJson.scripts["gen"] = "irgen examples/app.dsl.ts --mode=backend";
|
|
56
|
+
}
|
|
57
|
+
else if (template === "frontend") {
|
|
58
|
+
if (pkgJson.scripts["gen:backend"])
|
|
59
|
+
delete pkgJson.scripts["gen:backend"];
|
|
60
|
+
pkgJson.scripts["gen"] = "irgen examples/app.dsl.ts --mode=frontend";
|
|
61
|
+
}
|
|
62
|
+
fs.writeFileSync(path.join(projectDir, "package.json"), JSON.stringify(pkgJson, null, 2));
|
|
63
|
+
// 2. tsconfig.json
|
|
64
|
+
const tsConfig = {
|
|
65
|
+
compilerOptions: {
|
|
66
|
+
target: "ES2022",
|
|
67
|
+
module: "NodeNext",
|
|
68
|
+
moduleResolution: "NodeNext",
|
|
69
|
+
strict: true,
|
|
70
|
+
skipLibCheck: true,
|
|
71
|
+
outDir: "dist"
|
|
72
|
+
},
|
|
73
|
+
include: ["src/**/*", "examples/**/*"]
|
|
74
|
+
};
|
|
75
|
+
fs.writeFileSync(path.join(projectDir, "tsconfig.json"), JSON.stringify(tsConfig, null, 2));
|
|
76
|
+
// 3. Create folder structure
|
|
77
|
+
ensureDir(path.join(projectDir, "src", "ir", "target"));
|
|
78
|
+
ensureDir(path.join(projectDir, "examples"));
|
|
79
|
+
// 4. Default Policies
|
|
80
|
+
const policyContent = (type) => `
|
|
81
|
+
import { ${type}Policy } from "irgen";
|
|
82
|
+
|
|
83
|
+
export const My${type}Policy = ${type}Policy({
|
|
84
|
+
// Add policy overrides here
|
|
85
|
+
});
|
|
86
|
+
`.trim();
|
|
87
|
+
if (template !== "frontend") {
|
|
88
|
+
fs.writeFileSync(path.join(projectDir, "src/ir/target/backend.policy.ts"), policyContent("Backend"));
|
|
89
|
+
}
|
|
90
|
+
if (template !== "backend") {
|
|
91
|
+
fs.writeFileSync(path.join(projectDir, "src/ir/target/frontend.policy.ts"), policyContent("Frontend"));
|
|
92
|
+
}
|
|
93
|
+
// 5. Example DSL
|
|
94
|
+
let dslContent = `import { app } from "irgen";\n\napp("${projectName}", (t) => {\n`;
|
|
95
|
+
if (template !== "frontend") {
|
|
96
|
+
dslContent += ` t.entity("User", (e) => {\n e.field("email", "string", { unique: true });\n });\n`;
|
|
97
|
+
}
|
|
98
|
+
if (template !== "backend") {
|
|
99
|
+
dslContent += ` t.page("Home", { path: "/" }, (p) => {\n p.component("Welcome");\n });\n`;
|
|
100
|
+
}
|
|
101
|
+
dslContent += `});\n`;
|
|
102
|
+
fs.writeFileSync(path.join(projectDir, "examples/app.dsl.ts"), dslContent);
|
|
103
|
+
console.log("\nDone! Now run:\n");
|
|
104
|
+
console.log(` cd ${projectName}`);
|
|
105
|
+
console.log(" npm install");
|
|
106
|
+
console.log(" npm run gen");
|
|
107
|
+
}
|
|
108
|
+
function ensureDir(p) {
|
|
109
|
+
if (!fs.existsSync(p))
|
|
110
|
+
fs.mkdirSync(p, { recursive: true });
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runStudio(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import open from "open";
|
|
3
|
+
import { aggregateDecls } from "../dsl/aggregator.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
export async function runStudio(args) {
|
|
7
|
+
const dslFiles = args.filter(a => a.endsWith(".dsl.ts"));
|
|
8
|
+
if (dslFiles.length === 0) {
|
|
9
|
+
console.error("Usage: irgen studio <dsl-file>...");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
const app = express();
|
|
13
|
+
const port = 3000;
|
|
14
|
+
// Cache for IR
|
|
15
|
+
let currentIR = null;
|
|
16
|
+
const reload = async () => {
|
|
17
|
+
try {
|
|
18
|
+
console.log("Loading DSL for Studio...");
|
|
19
|
+
currentIR = await aggregateDecls(dslFiles);
|
|
20
|
+
console.log("DSL loaded successfully.");
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
console.error("Failed to load DSL in Studio:", err.message);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
await reload();
|
|
27
|
+
// Simple Watcher
|
|
28
|
+
dslFiles.forEach(file => {
|
|
29
|
+
fs.watch(path.resolve(process.cwd(), file), async (event) => {
|
|
30
|
+
if (event === "change") {
|
|
31
|
+
console.log(`Changes detected in ${file}, reloading...`);
|
|
32
|
+
await reload();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
// API
|
|
37
|
+
app.get("/api/ir", (req, res) => {
|
|
38
|
+
if (!currentIR)
|
|
39
|
+
return res.status(500).json({ error: "IR not loaded" });
|
|
40
|
+
res.json(currentIR);
|
|
41
|
+
});
|
|
42
|
+
// Static UI
|
|
43
|
+
// For now, let's embed a simple but beautiful HTML
|
|
44
|
+
app.get("/", (req, res) => {
|
|
45
|
+
res.send(getStudioHtml());
|
|
46
|
+
});
|
|
47
|
+
app.listen(port, () => {
|
|
48
|
+
console.log(`Studio Dashboard running at http://localhost:${port}`);
|
|
49
|
+
console.log("Press Ctrl+C to stop.");
|
|
50
|
+
if (process.env.OPEN_BROWSER !== "false") {
|
|
51
|
+
open(`http://localhost:${port}`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function getStudioHtml() {
|
|
56
|
+
return `
|
|
57
|
+
<!DOCTYPE html>
|
|
58
|
+
<html lang="en">
|
|
59
|
+
<head>
|
|
60
|
+
<meta charset="UTF-8">
|
|
61
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
62
|
+
<title>irgen Studio Dashboard</title>
|
|
63
|
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&family=JetBrains+Mono&display=swap" rel="stylesheet">
|
|
64
|
+
<style>
|
|
65
|
+
:root {
|
|
66
|
+
--bg: #0b0f19;
|
|
67
|
+
--sidebar: #111827;
|
|
68
|
+
--card: #1f2937;
|
|
69
|
+
--accent: #38bdf8;
|
|
70
|
+
--accent-glow: rgba(56, 189, 248, 0.4);
|
|
71
|
+
--text: #f3f4f6;
|
|
72
|
+
--text-dim: #9ca3af;
|
|
73
|
+
--success: #10b981;
|
|
74
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
75
|
+
}
|
|
76
|
+
* { box-sizing: border-box; }
|
|
77
|
+
body {
|
|
78
|
+
font-family: 'Outfit', sans-serif;
|
|
79
|
+
background: var(--bg);
|
|
80
|
+
color: var(--text);
|
|
81
|
+
margin: 0;
|
|
82
|
+
display: flex;
|
|
83
|
+
height: 100vh;
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
}
|
|
86
|
+
aside {
|
|
87
|
+
width: 280px;
|
|
88
|
+
background: var(--sidebar);
|
|
89
|
+
border-right: 1px solid var(--border);
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
padding: 24px;
|
|
93
|
+
}
|
|
94
|
+
main {
|
|
95
|
+
flex: 1;
|
|
96
|
+
padding: 40px;
|
|
97
|
+
overflow-y: auto;
|
|
98
|
+
background: radial-gradient(circle at 50% 50%, rgba(56, 189, 248, 0.03) 0%, transparent 100%);
|
|
99
|
+
}
|
|
100
|
+
.logo {
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
gap: 12px;
|
|
104
|
+
margin-bottom: 40px;
|
|
105
|
+
font-weight: 600;
|
|
106
|
+
font-size: 1.25rem;
|
|
107
|
+
color: var(--accent);
|
|
108
|
+
}
|
|
109
|
+
.logo svg { width: 32px; height: 32px; filter: drop-shadow(0 0 8px var(--accent-glow)); }
|
|
110
|
+
|
|
111
|
+
.nav-section { margin-bottom: 32px; }
|
|
112
|
+
.nav-label {
|
|
113
|
+
font-size: 11px;
|
|
114
|
+
text-transform: uppercase;
|
|
115
|
+
letter-spacing: 1px;
|
|
116
|
+
color: var(--text-dim);
|
|
117
|
+
margin-bottom: 12px;
|
|
118
|
+
opacity: 0.6;
|
|
119
|
+
}
|
|
120
|
+
.nav-item {
|
|
121
|
+
padding: 10px 12px;
|
|
122
|
+
border-radius: 8px;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
color: var(--text-dim);
|
|
125
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: 10px;
|
|
129
|
+
font-size: 14px;
|
|
130
|
+
margin-bottom: 4px;
|
|
131
|
+
}
|
|
132
|
+
.nav-item:hover { background: rgba(255,255,255,0.03); color: var(--text); }
|
|
133
|
+
.nav-item.active { background: rgba(56, 189, 248, 0.1); color: var(--accent); border-left: 3px solid var(--accent); border-radius: 0 8px 8px 0; margin-left: -24px; padding-left: 21px; }
|
|
134
|
+
|
|
135
|
+
.header {
|
|
136
|
+
display: flex;
|
|
137
|
+
justify-content: space-between;
|
|
138
|
+
align-items: flex-start;
|
|
139
|
+
margin-bottom: 40px;
|
|
140
|
+
}
|
|
141
|
+
.title-area h2 { margin: 0; font-size: 2rem; font-weight: 600; }
|
|
142
|
+
.title-area p { margin: 8px 0 0; color: var(--text-dim); }
|
|
143
|
+
|
|
144
|
+
.stats-row {
|
|
145
|
+
display: grid;
|
|
146
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
147
|
+
gap: 20px;
|
|
148
|
+
margin-bottom: 40px;
|
|
149
|
+
}
|
|
150
|
+
.stat-card {
|
|
151
|
+
background: var(--card);
|
|
152
|
+
padding: 24px;
|
|
153
|
+
border-radius: 16px;
|
|
154
|
+
border: 1px solid var(--border);
|
|
155
|
+
transition: transform 0.3s;
|
|
156
|
+
}
|
|
157
|
+
.stat-card:hover { transform: translateY(-5px); }
|
|
158
|
+
.stat-value { font-size: 24px; font-weight: 600; color: var(--accent); }
|
|
159
|
+
.stat-label { font-size: 12px; color: var(--text-dim); margin-top: 4px; }
|
|
160
|
+
|
|
161
|
+
.card {
|
|
162
|
+
background: var(--card);
|
|
163
|
+
border-radius: 16px;
|
|
164
|
+
padding: 32px;
|
|
165
|
+
border: 1px solid var(--border);
|
|
166
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
167
|
+
animation: fadeIn 0.5s ease-out;
|
|
168
|
+
}
|
|
169
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
170
|
+
|
|
171
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 24px; }
|
|
172
|
+
|
|
173
|
+
.badge {
|
|
174
|
+
font-size: 10px;
|
|
175
|
+
padding: 4px 8px;
|
|
176
|
+
border-radius: 6px;
|
|
177
|
+
font-weight: 600;
|
|
178
|
+
background: rgba(56, 189, 248, 0.1);
|
|
179
|
+
color: var(--accent);
|
|
180
|
+
border: 1px solid rgba(56, 189, 248, 0.2);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
|
184
|
+
th { text-align: left; padding: 12px; color: var(--text-dim); font-weight: 400; border-bottom: 1px solid var(--border); font-size: 13px; }
|
|
185
|
+
td { padding: 16px 12px; border-bottom: 1px solid var(--border); font-size: 14px; }
|
|
186
|
+
.mono { font-family: 'JetBrains Mono', monospace; font-size: 12px; }
|
|
187
|
+
|
|
188
|
+
.indicator {
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
gap: 8px;
|
|
192
|
+
background: rgba(16, 185, 129, 0.1);
|
|
193
|
+
color: var(--success);
|
|
194
|
+
padding: 6px 12px;
|
|
195
|
+
border-radius: 20px;
|
|
196
|
+
font-size: 12px;
|
|
197
|
+
font-weight: 600;
|
|
198
|
+
}
|
|
199
|
+
.pulse { width: 8px; height: 8px; background: var(--success); border-radius: 50%; animation: pulse 2s infinite; }
|
|
200
|
+
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); } 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } }
|
|
201
|
+
</style>
|
|
202
|
+
</head>
|
|
203
|
+
<body>
|
|
204
|
+
<aside>
|
|
205
|
+
<div class="logo">
|
|
206
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>
|
|
207
|
+
irgen Studio
|
|
208
|
+
</div>
|
|
209
|
+
<div class="nav-section">
|
|
210
|
+
<div class="nav-label">Overview</div>
|
|
211
|
+
<div class="nav-item active" onclick="renderOverview()">Project Overview</div>
|
|
212
|
+
</div>
|
|
213
|
+
<div id="nav-apps"></div>
|
|
214
|
+
</aside>
|
|
215
|
+
<main>
|
|
216
|
+
<div class="header">
|
|
217
|
+
<div class="title-area" id="header-content">
|
|
218
|
+
<h2>Design System</h2>
|
|
219
|
+
<p>Visualizing your project architecture</p>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="indicator">
|
|
222
|
+
<div class="pulse"></div>
|
|
223
|
+
Connected
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
<div id="content"></div>
|
|
227
|
+
</main>
|
|
228
|
+
|
|
229
|
+
<script>
|
|
230
|
+
let ir = null;
|
|
231
|
+
let selectedId = 'overview';
|
|
232
|
+
|
|
233
|
+
async function refresh() {
|
|
234
|
+
try {
|
|
235
|
+
const res = await fetch('/api/ir');
|
|
236
|
+
ir = await res.json();
|
|
237
|
+
renderNav();
|
|
238
|
+
if (selectedId === 'overview') renderOverview();
|
|
239
|
+
} catch (e) {
|
|
240
|
+
console.error("Failed to fetch IR", e);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function renderNav() {
|
|
245
|
+
const container = document.getElementById('nav-apps');
|
|
246
|
+
container.innerHTML = '';
|
|
247
|
+
ir.apps.forEach(app => {
|
|
248
|
+
const section = document.createElement('div');
|
|
249
|
+
section.className = 'nav-section';
|
|
250
|
+
section.innerHTML = \`<div class="nav-label">\${app.name}</div>\`;
|
|
251
|
+
|
|
252
|
+
app.entities?.forEach(ent => {
|
|
253
|
+
const item = document.createElement('div');
|
|
254
|
+
item.className = 'nav-item';
|
|
255
|
+
item.innerHTML = '<span>📦</span> ' + ent.name;
|
|
256
|
+
item.onclick = (event) => { selectItem(ent.name, event); renderEntity(ent); };
|
|
257
|
+
section.appendChild(item);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
app.pages?.forEach(page => {
|
|
261
|
+
const item = document.createElement('div');
|
|
262
|
+
item.className = 'nav-item';
|
|
263
|
+
item.innerHTML = '<span>📄</span> ' + page.name;
|
|
264
|
+
item.onclick = (event) => { selectItem(page.name, event); renderPage(page); };
|
|
265
|
+
section.appendChild(item);
|
|
266
|
+
});
|
|
267
|
+
container.appendChild(section);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function selectItem(name, event) {
|
|
272
|
+
selectedId = name;
|
|
273
|
+
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
|
274
|
+
event.currentTarget.classList.add('active');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function renderOverview() {
|
|
278
|
+
document.getElementById('header-content').innerHTML = '<h2>Project Overview</h2><p>Summary of discovered resources</p>';
|
|
279
|
+
let totalEntities = 0;
|
|
280
|
+
let totalPages = 0;
|
|
281
|
+
ir.apps.forEach(a => { totalEntities += (a.entities?.length || 0); totalPages += (a.pages?.length || 0); });
|
|
282
|
+
|
|
283
|
+
const content = document.getElementById('content');
|
|
284
|
+
content.innerHTML = \`
|
|
285
|
+
<div class="stats-row">
|
|
286
|
+
<div class="stat-card">
|
|
287
|
+
<div class="stat-value">\${ir.apps.length}</div>
|
|
288
|
+
<div class="stat-label">Applications</div>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="stat-card">
|
|
291
|
+
<div class="stat-value">\${totalEntities}</div>
|
|
292
|
+
<div class="stat-label">Data Entities</div>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="stat-card">
|
|
295
|
+
<div class="stat-value">\${totalPages}</div>
|
|
296
|
+
<div class="stat-label">UI Pages</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="card">
|
|
300
|
+
<h3>Metadata</h3>
|
|
301
|
+
<table class="mono">
|
|
302
|
+
\${Object.entries(ir.apps[0]?.meta || {}).map(([k,v]) => \`
|
|
303
|
+
<tr>
|
|
304
|
+
<td style="color: var(--accent)">\${k}</td>
|
|
305
|
+
<td>\${JSON.stringify(v)}</td>
|
|
306
|
+
</tr>
|
|
307
|
+
\`).join('')}
|
|
308
|
+
</table>
|
|
309
|
+
</div>
|
|
310
|
+
\`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function renderEntity(ent) {
|
|
314
|
+
document.getElementById('header-content').innerHTML = \`<h2>\${ent.name}</h2><p>Entity Model & API Operations</p>\`;
|
|
315
|
+
const content = document.getElementById('content');
|
|
316
|
+
content.innerHTML = \`
|
|
317
|
+
<div class="grid">
|
|
318
|
+
<div class="card">
|
|
319
|
+
<h3>Definition</h3>
|
|
320
|
+
<table>
|
|
321
|
+
<thead><tr><th>Field</th><th>Type</th></tr></thead>
|
|
322
|
+
<tbody>
|
|
323
|
+
\${Object.entries(ent.model || {}).map(([k,v]) => \`
|
|
324
|
+
<tr>
|
|
325
|
+
<td><strong>\${k}</strong></td>
|
|
326
|
+
<td class="mono" style="color: var(--accent)">\${v}</td>
|
|
327
|
+
</tr>
|
|
328
|
+
\`).join('')}
|
|
329
|
+
</tbody>
|
|
330
|
+
</table>
|
|
331
|
+
</div>
|
|
332
|
+
<div class="card">
|
|
333
|
+
<h3>Target Operations</h3>
|
|
334
|
+
<div style="display: flex; flex-wrap: wrap; gap: 8px">
|
|
335
|
+
\${ent.operations.map(op => \`<span class="badge">\${op.kind.toUpperCase()}: \${op.name}</span>\`).join('')}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
\`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function renderPage(page) {
|
|
343
|
+
document.getElementById('header-content').innerHTML = \`<h2>\${page.name}</h2><p>Route: <span class="mono">\${page.path}</span></p>\`;
|
|
344
|
+
const content = document.getElementById('content');
|
|
345
|
+
content.innerHTML = \`
|
|
346
|
+
<div class="card">
|
|
347
|
+
<h3>Component Tree</h3>
|
|
348
|
+
<div class="grid">
|
|
349
|
+
\${page.components.map(comp => \`
|
|
350
|
+
<div class="stat-card" style="background: rgba(255,255,255,0.02)">
|
|
351
|
+
<div style="display: flex; justify-content: space-between; align-items: center">
|
|
352
|
+
<strong>\${comp.name}</strong>
|
|
353
|
+
\${comp.entityRef ? '<span class="badge">Bound</span>' : ''}
|
|
354
|
+
</div>
|
|
355
|
+
\${comp.entityRef ? \`<div class="stat-label">Entity: \${comp.entityRef}</div>\` : ''}
|
|
356
|
+
</div>
|
|
357
|
+
\`).join('')}
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
\`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
refresh();
|
|
364
|
+
setInterval(refresh, 5000);
|
|
365
|
+
</script>
|
|
366
|
+
</body>
|
|
367
|
+
</html>
|
|
368
|
+
`;
|
|
369
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -46,6 +46,21 @@ Examples:
|
|
|
46
46
|
console.log("irgen 0.1.0");
|
|
47
47
|
process.exit(0);
|
|
48
48
|
}
|
|
49
|
+
if (process.argv[2] === "check") {
|
|
50
|
+
const { runCheck } = await import("./cli/check.js");
|
|
51
|
+
await runCheck(process.argv.slice(3));
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
if (process.argv[2] === "studio") {
|
|
55
|
+
const { runStudio } = await import("./cli/studio.js");
|
|
56
|
+
await runStudio(process.argv.slice(3));
|
|
57
|
+
return; // Keep alive as server
|
|
58
|
+
}
|
|
59
|
+
if (process.argv[2] === "init") {
|
|
60
|
+
const { runInit } = await import("./cli/init.js");
|
|
61
|
+
await runInit(process.argv.slice(3));
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
49
64
|
try {
|
|
50
65
|
const { register } = await import("tsx/esm/api");
|
|
51
66
|
register();
|
|
@@ -220,7 +220,9 @@ export async function loadFrontendDsl(entry) {
|
|
|
220
220
|
// reset
|
|
221
221
|
_global.__IR_CURRENT_FRONTEND = null;
|
|
222
222
|
try {
|
|
223
|
-
|
|
223
|
+
// Add cache buster to force re-execution if file is already in module cache
|
|
224
|
+
const cacheBuster = `?cb=${Date.now()}`;
|
|
225
|
+
await import(url + cacheBuster);
|
|
224
226
|
}
|
|
225
227
|
catch (err) {
|
|
226
228
|
// Some environments (tsx ESM loader) may fail resolving .ts imports via file URL.
|
package/dist/dsl/runtime.js
CHANGED
|
@@ -92,7 +92,9 @@ export async function loadDsl(entry) {
|
|
|
92
92
|
// reset
|
|
93
93
|
_global.__IR_CURRENT_BACKEND = null;
|
|
94
94
|
try {
|
|
95
|
-
|
|
95
|
+
// Add cache buster to force re-execution if file is already in module cache
|
|
96
|
+
const cacheBuster = `?cb=${Date.now()}`;
|
|
97
|
+
await import(url + cacheBuster);
|
|
96
98
|
}
|
|
97
99
|
catch (err) {
|
|
98
100
|
const errMessage = err instanceof Error ? err.message : String(err);
|