Skip to content

Commit 42baf46

Browse files
committed
refactor: update python script to nodejs mjs
1 parent 6eddb99 commit 42baf46

12 files changed

Lines changed: 905 additions & 1063 deletions

.agents/skills/tinyengine-dsl-generator/SKILL.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Generate conformant DSL (JSON schemas) for TinyEngine low-code platform applicat
1515
| Generate block DSL | Describe reusable functionality with configurable props |
1616
| Generate app DSL | Describe multi-page application structure |
1717
| From screenshot | Provide image with description of desired layout |
18-
| Validate DSL | Run `scripts/validate_dsl.py <file>` |
18+
| Validate DSL | Run `node scripts/validate_dsl.mjs <file>` |
1919

2020
## DSL Generation Workflow
2121

@@ -152,7 +152,7 @@ Block receives props via `this.props.xxx` and emits events via `this.emit('event
152152
Run the validator:
153153

154154
```bash
155-
python3 /path/to/skill/scripts/validate_dsl.py <output-file>
155+
node /path/to/skill/scripts/validate_dsl.mjs <output-file>
156156
```
157157

158158
The validator checks:
@@ -666,6 +666,8 @@ Component catalog with props, events, and usage examples.
666666

667667
Template patterns for common page types and interaction flows.
668668

669-
### scripts/validate_dsl.py
669+
### scripts/validate_dsl.mjs
670670

671-
Python validator for generated DSL files. Run before finalizing output.
671+
Node.js validator (ESM, zero dependencies) for generated DSL files. Run before finalizing output.
672+
Also available: `check_event_bindings.mjs`, `check_css.mjs`, `validate_page.mjs`, and the
673+
`validate_all.sh` orchestrator (which chains all three checks).
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
#!/usr/bin/env node
2+
/**
3+
* TinyEngine CSS Syntax Checker
4+
*
5+
* 检查DSL中的CSS字段是否有语法错误(基础模式:括号匹配、基本语法,无需额外依赖)。
6+
*
7+
* Node.js port of check_css.py 的 basic 模式 —— 行为保持一致,零依赖。
8+
* (原 tinycss2 / postcss 模式依赖外部环境,已精简;basic 是默认且为编排脚本使用的模式。)
9+
*/
10+
11+
import fs from 'node:fs';
12+
import path from 'node:path';
13+
import { fileURLToPath } from 'node:url';
14+
15+
/** 普通对象判定(非 null、非数组) */
16+
function isPlainObject(value) {
17+
return typeof value === 'object' && value !== null && !Array.isArray(value);
18+
}
19+
20+
/**
21+
* 解包外层包装结构,返回内层 DSL(页面在 page_content 内,区块在 content 内)。
22+
* 仅当内层确实是含 componentName 的节点时才解包;否则原样返回。
23+
*/
24+
function extractInnerDsl(dslData) {
25+
if (isPlainObject(dslData)) {
26+
for (const key of ['page_content', 'content']) {
27+
const inner = dslData[key];
28+
if (isPlainObject(inner) && 'componentName' in inner) {
29+
return inner;
30+
}
31+
}
32+
}
33+
return dslData;
34+
}
35+
36+
/** CSS 语法检查器(基础模式,无需额外依赖) */
37+
export class BasicCssChecker {
38+
/** @param {*} dslData */
39+
constructor(dslData) {
40+
this.dsl = dslData;
41+
this.errors = [];
42+
this.warnings = [];
43+
}
44+
45+
/** 检查 CSS 语法 */
46+
check() {
47+
// 从外层包装(page_content/content)解包到内层 DSL 后再读取 css
48+
const inner = extractInnerDsl(this.dsl);
49+
const cssString = inner.css ?? '';
50+
51+
if (!cssString) {
52+
this.warnings.push('No CSS field found');
53+
return true;
54+
}
55+
56+
return this._checkCss(cssString);
57+
}
58+
59+
/** 基础检查:括号匹配、基本语法 */
60+
_checkCss(css) {
61+
// 检查括号匹配
62+
const stack = [];
63+
for (let i = 0; i < css.length; i++) {
64+
const char = css[i];
65+
if (char === '{') {
66+
stack.push([char, i]);
67+
} else if (char === '}') {
68+
if (stack.length === 0 || stack[stack.length - 1][0] !== '{') {
69+
this.errors.push(`Unmatched '}' at position ${i}`);
70+
return false;
71+
}
72+
stack.pop();
73+
} else if (char === '(') {
74+
stack.push([char, i]);
75+
} else if (char === ')') {
76+
if (stack.length === 0 || stack[stack.length - 1][0] !== '(') {
77+
this.errors.push(`Unmatched ')' at position ${i}`);
78+
return false;
79+
}
80+
stack.pop();
81+
}
82+
}
83+
84+
if (stack.length) {
85+
for (const [char, pos] of stack) {
86+
this.errors.push(`Unclosed '${char}' at position ${pos}`);
87+
}
88+
return false;
89+
}
90+
91+
// 移除注释进行检查
92+
const cssNoComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
93+
94+
// 检查是否有 CSS 规则
95+
if (!cssNoComments.includes('{')) {
96+
this.warnings.push('CSS may not contain any rules');
97+
}
98+
99+
// 检查分号使用
100+
const rules = [...cssNoComments.matchAll(/\{([^}]*)\}/g)].map((m) => m[1]);
101+
for (const rule of rules) {
102+
const properties = rule.split(';');
103+
// 最后一个可能为空
104+
for (const propRaw of properties.slice(0, -1)) {
105+
const prop = propRaw.trim();
106+
if (prop && !prop.includes(':')) {
107+
this.warnings.push(`Property without colon: ${prop.slice(0, 50)}`);
108+
}
109+
}
110+
}
111+
112+
return this.errors.length === 0;
113+
}
114+
115+
/** 生成报告 */
116+
report() {
117+
const lines = [];
118+
if (this.errors.length) {
119+
lines.push('❌ CSS Errors:');
120+
for (const error of this.errors) lines.push(` - ${error}`);
121+
}
122+
if (this.warnings.length) {
123+
lines.push('⚠️ CSS Warnings:');
124+
for (const warning of this.warnings) lines.push(` - ${warning}`);
125+
}
126+
if (this.errors.length === 0 && this.warnings.length === 0) {
127+
lines.push('✅ CSS check passed!');
128+
}
129+
return lines.join('\n');
130+
}
131+
}
132+
133+
// 可用模式表(与 Python 版的 checkers 字典对应,仅保留 basic)
134+
const CHECKERS = { basic: BasicCssChecker };
135+
136+
function main() {
137+
if (process.argv.length < 3) {
138+
console.log('Usage: check_css.mjs <dsl-file> [mode]');
139+
console.log(' mode: basic (default)');
140+
process.exit(1);
141+
}
142+
143+
const filePath = process.argv[2];
144+
const mode = process.argv[3] || 'basic';
145+
146+
// 读取 DSL 文件
147+
let dslData;
148+
try {
149+
const text = fs.readFileSync(filePath, 'utf8');
150+
dslData = JSON.parse(text);
151+
} catch (e) {
152+
if (e instanceof SyntaxError) {
153+
console.log(`❌ Invalid JSON: ${e.message}`);
154+
} else if (e.code === 'ENOENT') {
155+
console.log(`❌ File not found: ${filePath}`);
156+
} else {
157+
throw e;
158+
}
159+
process.exit(1);
160+
}
161+
162+
// 选择检查器
163+
const CheckerClass = CHECKERS[mode];
164+
if (!CheckerClass) {
165+
console.log(`❌ Unknown mode: ${mode}`);
166+
console.log(`Available modes: ${Object.keys(CHECKERS).join(', ')}`);
167+
process.exit(1);
168+
}
169+
170+
const checker = new CheckerClass(dslData);
171+
const isValid = checker.check();
172+
console.log(checker.report());
173+
process.exit(isValid ? 0 : 1);
174+
}
175+
176+
const __filename = fileURLToPath(import.meta.url);
177+
if (path.resolve(process.argv[1] || '') === __filename) {
178+
main();
179+
}

0 commit comments

Comments
 (0)