Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a9f6985
fix: Automatically serialize `page_content` to JSON string to resolve…
hexqi Mar 5, 2026
6acc71e
feat: Implement a pluggable store abstraction with a new file-based s…
hexqi Mar 5, 2026
bf0be96
fix: Resolve P0 issues in store adapter implementation
hexqi Mar 5, 2026
b9ce9d3
feat: Implement file-based data storage for mock server entities and …
hexqi Mar 5, 2026
5f2ce0a
fix(mockServer): filter nedb metadata in export-db-to-file
hexqi Mar 5, 2026
8ee2d7a
chore: add base file data and dev:file cmd
hexqi Mar 18, 2026
c5eed6b
feat: add dsl generator skill
hexqi Mar 18, 2026
e382cb3
fix: windows start error
hexqi Mar 23, 2026
0b02ba8
Merge remote-tracking branch 'hexqi/develop' into feat/file-as-database
hexqi Mar 23, 2026
0a4b25a
Merge remote-tracking branch 'origin/develop' into feat/file-as-database
hexqi Jun 22, 2026
641be77
chore: move skill from .claude to .agents directory
hexqi Jun 22, 2026
3570654
fix: review
hexqi Jun 22, 2026
7d01778
fix: add deep equality check and update query matching
hexqi Jun 22, 2026
2b2a78b
fix: skill symlink
hexqi Jun 25, 2026
b59add3
docs: add guide for using Skill to generate pages
hexqi Jun 25, 2026
278aa63
fix: docs
hexqi Jun 25, 2026
0ba0b14
docs: update
hexqi Jun 25, 2026
4b260f9
chore: replace tracked symlink with per-platform link script
hexqi Jun 25, 2026
6eddb99
fix: update check script
hexqi Jun 25, 2026
42baf46
refactor: update python script to nodejs mjs
hexqi Jun 25, 2026
e39da95
fix: docs
hexqi Jun 25, 2026
cee2ccb
feat: update skill component props and event binding rules
hexqi Jun 25, 2026
d5c287c
fix: review
hexqi Jun 26, 2026
e0b935e
fix: review
hexqi Jun 29, 2026
1832605
fix(FileStore): add case sensitivity detection for file operations
hexqi Jun 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
673 changes: 673 additions & 0 deletions .agents/skills/tinyengine-dsl-generator/SKILL.md
Comment thread
chilingling marked this conversation as resolved.

Large diffs are not rendered by default.

614 changes: 614 additions & 0 deletions .agents/skills/tinyengine-dsl-generator/references/components.md

Large diffs are not rendered by default.

1,141 changes: 1,141 additions & 0 deletions .agents/skills/tinyengine-dsl-generator/references/patterns.md

Large diffs are not rendered by default.

595 changes: 595 additions & 0 deletions .agents/skills/tinyengine-dsl-generator/references/protocol.md

Large diffs are not rendered by default.

179 changes: 179 additions & 0 deletions .agents/skills/tinyengine-dsl-generator/scripts/check_css.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#!/usr/bin/env node
/**
* TinyEngine CSS Syntax Checker
*
* 检查DSL中的CSS字段是否有语法错误(基础模式:括号匹配、基本语法,无需额外依赖)。
*
* Node.js port of check_css.py 的 basic 模式 —— 行为保持一致,零依赖。
* (原 tinycss2 / postcss 模式依赖外部环境,已精简;basic 是默认且为编排脚本使用的模式。)
*/

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

/** 普通对象判定(非 null、非数组) */
function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

/**
* 解包外层包装结构,返回内层 DSL(页面在 page_content 内,区块在 content 内)。
* 仅当内层确实是含 componentName 的节点时才解包;否则原样返回。
*/
function extractInnerDsl(dslData) {
if (isPlainObject(dslData)) {
for (const key of ['page_content', 'content']) {
const inner = dslData[key];
if (isPlainObject(inner) && 'componentName' in inner) {
return inner;
}
}
}
return dslData;
}

/** CSS 语法检查器(基础模式,无需额外依赖) */
export class BasicCssChecker {
/** @param {*} dslData */
constructor(dslData) {
this.dsl = dslData;
this.errors = [];
this.warnings = [];
}

/** 检查 CSS 语法 */
check() {
// 从外层包装(page_content/content)解包到内层 DSL 后再读取 css
const inner = extractInnerDsl(this.dsl);
const cssString = inner.css ?? '';

if (!cssString) {
this.warnings.push('No CSS field found');
return true;
}

return this._checkCss(cssString);
}

/** 基础检查:括号匹配、基本语法 */
_checkCss(css) {
// 检查括号匹配
const stack = [];
for (let i = 0; i < css.length; i++) {
const char = css[i];
if (char === '{') {
stack.push([char, i]);
} else if (char === '}') {
if (stack.length === 0 || stack[stack.length - 1][0] !== '{') {
this.errors.push(`Unmatched '}' at position ${i}`);
return false;
}
stack.pop();
} else if (char === '(') {
stack.push([char, i]);
} else if (char === ')') {
if (stack.length === 0 || stack[stack.length - 1][0] !== '(') {
this.errors.push(`Unmatched ')' at position ${i}`);
return false;
}
stack.pop();
}
}

if (stack.length) {
for (const [char, pos] of stack) {
this.errors.push(`Unclosed '${char}' at position ${pos}`);
}
return false;
}

// 移除注释进行检查
const cssNoComments = css.replace(/\/\*[\s\S]*?\*\//g, '');

// 检查是否有 CSS 规则
if (!cssNoComments.includes('{')) {
this.warnings.push('CSS may not contain any rules');
}

// 检查分号使用
const rules = [...cssNoComments.matchAll(/\{([^}]*)\}/g)].map((m) => m[1]);
for (const rule of rules) {
const properties = rule.split(';');
// 最后一个可能为空
for (const propRaw of properties.slice(0, -1)) {
const prop = propRaw.trim();
if (prop && !prop.includes(':')) {
this.warnings.push(`Property without colon: ${prop.slice(0, 50)}`);
}
}
}

return this.errors.length === 0;
}

/** 生成报告 */
report() {
const lines = [];
if (this.errors.length) {
lines.push('❌ CSS Errors:');
for (const error of this.errors) lines.push(` - ${error}`);
}
if (this.warnings.length) {
lines.push('⚠️ CSS Warnings:');
for (const warning of this.warnings) lines.push(` - ${warning}`);
}
if (this.errors.length === 0 && this.warnings.length === 0) {
lines.push('✅ CSS check passed!');
}
return lines.join('\n');
}
}

// 可用模式表(与 Python 版的 checkers 字典对应,仅保留 basic)
const CHECKERS = { basic: BasicCssChecker };

function main() {
if (process.argv.length < 3) {
console.log('Usage: check_css.mjs <dsl-file> [mode]');
console.log(' mode: basic (default)');
process.exit(1);
}

const filePath = process.argv[2];
const mode = process.argv[3] || 'basic';

// 读取 DSL 文件
let dslData;
try {
const text = fs.readFileSync(filePath, 'utf8');
dslData = JSON.parse(text);
} catch (e) {
if (e instanceof SyntaxError) {
console.log(`❌ Invalid JSON: ${e.message}`);
} else if (e.code === 'ENOENT') {
console.log(`❌ File not found: ${filePath}`);
} else {
throw e;
}
process.exit(1);
}

// 选择检查器
const CheckerClass = CHECKERS[mode];
if (!CheckerClass) {
console.log(`❌ Unknown mode: ${mode}`);
console.log(`Available modes: ${Object.keys(CHECKERS).join(', ')}`);
process.exit(1);
}

const checker = new CheckerClass(dslData);
const isValid = checker.check();
console.log(checker.report());
process.exit(isValid ? 0 : 1);
}

const __filename = fileURLToPath(import.meta.url);
if (path.resolve(process.argv[1] || '') === __filename) {
main();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#!/usr/bin/env node
/**
* TinyEngine Event Binding Checker
*
* 检查DSL文件中的事件绑定是否正确使用JSExpression引用方法,
* 而不是在value中直接写函数定义。
*
* Node.js port of check_event_bindings.py — 行为保持一致,零依赖。
*/

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

/** 普通对象判定(非 null、非数组) */
function isPlainObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

/**
* 解包外层包装结构,返回内层 DSL。
* 落盘的页面/区块文件是"外层包装 + 内层 DSL":
* - 页面 DSL 在 page_content 内
* - 区块 DSL 在 content 内
* 仅当内层确实是含 componentName 的节点时才解包,避免误吞同名普通字段;否则原样返回。
*/
function extractInnerDsl(dslData) {
if (isPlainObject(dslData)) {
for (const key of ['page_content', 'content']) {
const inner = dslData[key];
if (isPlainObject(inner) && 'componentName' in inner) {
return inner;
}
}
}
return dslData;
}

const EVENT_KEYS = [
'onClick', 'onChange', 'onKeyup', 'onKeyDown', 'onKeyPress',
'onFocus', 'onBlur', 'onSubmit', 'onInput', 'onTabClick',
'onCurrentChange', 'onSizeChange', 'onCheckChange',
'onNodeClick', 'onRowClick', 'onCellClick',
];

export class EventBindingChecker {
/** @param {*} dslData */
constructor(dslData) {
this.dsl = dslData;
this.errors = [];
this.warnings = [];
}

/** 检查所有事件绑定 */
check() {
// 从外层包装(page_content/content)解包到内层 DSL 后再检查
const inner = extractInnerDsl(this.dsl);
this._checkNode(inner);
return this.errors.length === 0;
}

/** 递归检查节点 */
_checkNode(node) {
if (isPlainObject(node)) {
// 检查当前节点的事件绑定
this._checkEventBindings(node);

// 递归检查子节点(仅当 children 是数组;字符串子节点无需处理)
if (Array.isArray(node.children)) {
for (const child of node.children) {
this._checkNode(child);
}
}
} else if (Array.isArray(node)) {
for (const item of node) {
this._checkNode(item);
}
}
}

/** 检查单个节点的事件绑定(含 props 内的事件绑定) */
_checkEventBindings(node) {
const component = Object.prototype.hasOwnProperty.call(node, 'componentName') ? node.componentName : 'unknown';

// 两处都需要校验,避免漏检 props 内的事件。
this._checkEventHolder(node, component);
if (isPlainObject(node.props)) {
this._checkEventHolder(node.props, component);
}
}

/** 检查某个属性容器(节点本身或其 props)内的事件绑定 */
_checkEventHolder(holder, component) {
// 检查所有可能的事件属性
for (const key of EVENT_KEYS) {
if (key in holder) {
const value = holder[key];
if (isPlainObject(value)) {
this._checkEventValue(component, key, value);
}
}
}

// 也检查以 'on' 开头的属性
for (const [key, value] of Object.entries(holder)) {
if (key.startsWith('on') && !EVENT_KEYS.includes(key)) {
if (isPlainObject(value)) {
this._checkEventValue(component, key, value);
}
}
}
}

/** 检查事件值 */
_checkEventValue(component, eventKey, value) {
const valueType = value.type;
const valueContent = Object.prototype.hasOwnProperty.call(value, 'value') ? value.value : '';

// 错误1: 使用 JSFunction 类型进行事件绑定
if (valueType === 'JSFunction') {
this.errors.push(
`${component}.${eventKey}: 使用了JSFunction类型,应该使用JSExpression引用methods中的方法`
);
}

// 错误2: JSExpression 的 value 中包含函数定义
if (valueType === 'JSExpression' && typeof valueContent === 'string' && valueContent.startsWith('function')) {
this.errors.push(
`${component}.${eventKey}: JSExpression的value中包含函数定义 '${valueContent.slice(0, 30)}...',` +
`应该引用方法如 'this.methodName'`
);
}
}

/** 生成报告 */
report() {
const lines = [];
if (this.errors.length) {
lines.push('❌ 发现事件绑定错误:');
for (const error of this.errors) lines.push(` - ${error}`);
}
if (this.warnings.length) {
lines.push('⚠️ 警告:');
for (const warning of this.warnings) lines.push(` - ${warning}`);
}
if (this.errors.length === 0 && this.warnings.length === 0) {
lines.push('✅ 所有事件绑定检查通过!');
}
return lines.join('\n');
}
}

function main() {
if (process.argv.length < 3) {
console.log('Usage: check_event_bindings.mjs <dsl-file>');
process.exit(1);
}

const filePath = process.argv[2];

let dslData;
try {
const text = fs.readFileSync(filePath, 'utf8');
dslData = JSON.parse(text);
} catch (e) {
if (e instanceof SyntaxError) {
console.log(`❌ Invalid JSON: ${e.message}`);
} else if (e.code === 'ENOENT') {
console.log(`❌ File not found: ${filePath}`);
} else {
throw e;
}
process.exit(1);
}

const checker = new EventBindingChecker(dslData);
const isValid = checker.check();
console.log(checker.report());
process.exit(isValid ? 0 : 1);
}

const __filename = fileURLToPath(import.meta.url);
if (path.resolve(process.argv[1] || '') === __filename) {
main();
}
Loading
Loading