-
Notifications
You must be signed in to change notification settings - Fork 475
feat: suport mockserver file as database and add dsl-genenrator skill #1792
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 6acc71e
feat: Implement a pluggable store abstraction with a new file-based s…
hexqi bf0be96
fix: Resolve P0 issues in store adapter implementation
hexqi b9ce9d3
feat: Implement file-based data storage for mock server entities and …
hexqi 5f2ce0a
fix(mockServer): filter nedb metadata in export-db-to-file
hexqi 8ee2d7a
chore: add base file data and dev:file cmd
hexqi c5eed6b
feat: add dsl generator skill
hexqi e382cb3
fix: windows start error
hexqi 0b02ba8
Merge remote-tracking branch 'hexqi/develop' into feat/file-as-database
hexqi 0a4b25a
Merge remote-tracking branch 'origin/develop' into feat/file-as-database
hexqi 641be77
chore: move skill from .claude to .agents directory
hexqi 3570654
fix: review
hexqi 7d01778
fix: add deep equality check and update query matching
hexqi 2b2a78b
fix: skill symlink
hexqi b59add3
docs: add guide for using Skill to generate pages
hexqi 278aa63
fix: docs
hexqi 0ba0b14
docs: update
hexqi 4b260f9
chore: replace tracked symlink with per-platform link script
hexqi 6eddb99
fix: update check script
hexqi 42baf46
refactor: update python script to nodejs mjs
hexqi e39da95
fix: docs
hexqi cee2ccb
feat: update skill component props and event binding rules
hexqi d5c287c
fix: review
hexqi e0b935e
fix: review
hexqi 1832605
fix(FileStore): add case sensitivity detection for file operations
hexqi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
614 changes: 614 additions & 0 deletions
614
.agents/skills/tinyengine-dsl-generator/references/components.md
Large diffs are not rendered by default.
Oops, something went wrong.
1,141 changes: 1,141 additions & 0 deletions
1,141
.agents/skills/tinyengine-dsl-generator/references/patterns.md
Large diffs are not rendered by default.
Oops, something went wrong.
595 changes: 595 additions & 0 deletions
595
.agents/skills/tinyengine-dsl-generator/references/protocol.md
Large diffs are not rendered by default.
Oops, something went wrong.
179 changes: 179 additions & 0 deletions
179
.agents/skills/tinyengine-dsl-generator/scripts/check_css.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } |
185 changes: 185 additions & 0 deletions
185
.agents/skills/tinyengine-dsl-generator/scripts/check_event_bindings.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.