Skip to content
4 changes: 3 additions & 1 deletion designer-demo/engine.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import material from '@opentiny/tiny-engine-materials'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Default import may not match the package’s export shape — consider interop-safe import.

If @opentiny/tiny-engine-materials does not export a default (e.g., CommonJS export or named export), this will break at runtime. Use a namespace import and normalize to a material object for robustness.

Apply this diff:

-import material from '@opentiny/tiny-engine-materials'
+import * as materials from '@opentiny/tiny-engine-materials'
+const material = materials.default ?? materials.material ?? materials

To verify the export shape of the materials package and the dependency wiring, run:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Check demo dependency is declared"
rg -n -C2 '"@opentiny/tiny-engine-materials"\s*:\s*' designer-demo/package.json || {
  echo "Dependency not found in designer-demo/package.json"; exit 1; }

echo
echo "2) Locate the materials package and inspect export style"
# Path inferred from context (packages/materials/…)
if [ -d packages/materials ]; then
  echo "- package.json name:"
  rg -n -C1 '"name"\s*:\s*' packages/materials/package.json || true

  echo
  echo "- Look for default or CJS exports under packages/materials:"
  rg -n -C2 -S 'export\s+default|module\.exports\s*=|exports\.' packages/materials || true
else
  echo "packages/materials not found; adjust the path if the package lives elsewhere."
fi

Length of output: 1001


Ensure interop-safe import of @opentiny/tiny-engine-materials

The @opentiny/tiny-engine-materials package has no default export, so this will break at runtime:

designer-demo/engine.config.js:1

-import material from '@opentiny/tiny-engine-materials'
+import * as materials from '@opentiny/tiny-engine-materials'
+const material = materials.default ?? materials.material ?? materials;

This normalizes the import to work with both ESM and CJS export shapes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import material from '@opentiny/tiny-engine-materials'
import * as materials from '@opentiny/tiny-engine-materials'
const material = materials.default ?? materials.material ?? materials;
🤖 Prompt for AI Agents
In designer-demo/engine.config.js at line 1, the current statement imports a
package that has no default export; change the import to be interop-safe by
either importing the module namespace (import * as material from
'@opentiny/tiny-engine-materials') or normalizing a default import (import
materialImport from '@opentiny/tiny-engine-materials' then const material =
materialImport?.default ?? materialImport); update the file to use one of these
patterns so the code works with both ESM and CJS export shapes.


export default {
id: 'engine.config',
theme: 'light',
material: ['/mock/bundle.json'],
material: [material],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Switching from a mock JSON URL to the materials package looks good — confirm engine compatibility and document the migration.

Nice move away from hardcoded /mock/bundle.json to a shareable materials package; this should improve consistency across demos. Please confirm the engine accepts inlined material objects (not just URLs) in the material array and add a short migration note since the PR is marked “breaking.”

Optional (to ease migration/backward-compat): allow a fallback/toggle to the legacy bundle for now. Example:

-import * as materials from '@opentiny/tiny-engine-materials'
-const material = materials.default ?? materials.material ?? materials
+import * as materials from '@opentiny/tiny-engine-materials'
+const material = materials.default ?? materials.material ?? materials
+const usePkg = !!(import.meta?.env?.VITE_USE_MATERIAL_PKG ?? true)

 export default {
   id: 'engine.config',
   theme: 'light',
-  material: [material],
+  material: [usePkg ? material : '/mock/bundle.json'],
   scripts: [],
   styles: []
 }

Quick checks you can run:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Ensure no stale references to the old mock bundle remain"
rg -n -C2 '/mock/bundle\.json' || echo "No references found."

echo
echo "2) Grep for material array handling to ensure objects are supported (heuristic)"
rg -n -C3 -S '\bmaterial\b' packages | head -n 100 || true

Length of output: 14235


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Context around material in designer-demo/src/preview.js:"
rg -n -C3 'material' designer-demo/src/preview.js || true

echo
echo "Import statements in designer-demo/src/preview.js:"
rg -n 'import ' designer-demo/src/preview.js

Length of output: 608


Confirm inline material support & document breaking change migration

Engine already accepts in‐memory material objects (see “也可以传入 JSON 对象” examples in docs/api/frontend-api/preview-api.md and docs/extension-capabilities-overview/registry.md). To finish this breaking change:

• Update documentation

  • docs/api/frontend-api/preview-api.md: show both URL and inlined object usage, and add a “Migration” note for replacing /mock/bundle.json with the new package import.
  • docs/extension-capabilities-overview/registry.md: mirror the inline-object example and call out the breaking change.

• (Optional) provide a runtime toggle/fallback to the legacy mock bundle in designer-demo/engine.config.js:

 import * as materials from '@opentiny/tiny-engine-materials'
 const material = materials.default ?? materials.material ?? materials
+  const usePkg = Boolean(import.meta.env.VITE_USE_MATERIAL_PKG ?? true)

 export default {
   id: 'engine.config',
   theme: 'light',
-    material: [material],
+    material: [usePkg ? material : '/mock/bundle.json'],
   scripts: [],
   styles: []
 }

• (Out-of-scope for this PR) schedule follow-up to clean up stale /mock/bundle.json references in CLI templates, scripts, and other demos.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
material: [material],
import * as materials from '@opentiny/tiny-engine-materials'
const material = materials.default ?? materials.material ?? materials
const usePkg = Boolean(import.meta.env.VITE_USE_MATERIAL_PKG ?? true)
export default {
id: 'engine.config',
theme: 'light',
material: [usePkg ? material : '/mock/bundle.json'],
scripts: [],
styles: []
}
🤖 Prompt for AI Agents
In designer-demo/engine.config.js around line 6 (material: [material]), confirm
and support inline in-memory material objects and add a runtime fallback to
preserve legacy /mock/bundle.json behavior: update engine.config.js to accept
either an imported object or a URL/string — if material is a string treat it as
a URL and load the legacy bundle, otherwise treat it as the inlined object; also
update docs/api/frontend-api/preview-api.md to show both URL and inline-object
usage and add a “Migration” note explaining replacing /mock/bundle.json with the
new package import, and mirror the inline-object example and breaking-change
callout in docs/extension-capabilities-overview/registry.md.

scripts: [],
styles: []
}
1 change: 1 addition & 0 deletions designer-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"dependencies": {
"@opentiny/tiny-engine": "workspace:^",
"@opentiny/tiny-engine-materials": "workspace:*",
"@opentiny/tiny-engine-meta-register": "workspace:^",
"@opentiny/tiny-engine-utils": "workspace:*",
"@opentiny/vue": "~3.20.0",
Expand Down
52 changes: 52 additions & 0 deletions packages/materials/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# TinyEngine 官方物料

## 使用

### 持续构建

```bash
npm run serve
```

解释:

1. 会持续监听 src 目录下文件变动,持续构建出来物料产物
Comment thread
chilingling marked this conversation as resolved.
2. 会启动静态服务器。

### 将组件库分别进行构建,以及将所有组件库构建成一份物料产物

```bash
npm run build

# 构建成功会得到比如 ElementPlus.json、TinyVue.json 等组件库对应的 json,以及 all.json
```

### 将组件库分别进行构建

```bash
npm run build:split

# 构建成功会得到比如 ElementPlus.json、TinyVue.json 等组件库对应的 json
```

## 添加自己的物料

请先大致了解 TinyEngine 物料协议:[TinyEngine物料协议](https://opentiny.design/tiny-engine#/protocol)

src 目录功能约定结构:

```bash
src/
|__ ElementPlus 组件库名称
|__ Button.json ElementPlus Button组件
|__ Table.json ElementPlus Table 组件
```

所以,我们添加自己的物料可以大致分为两步:

1. 根据目录结构约定添加 xxx.json 组件文件
2. xxx.json 中根据物料协议进行书写。

## TODO

- [ ] 脚本自动生成组件库对应物料。
267 changes: 267 additions & 0 deletions packages/materials/buildMaterials.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import fsExtra from 'fs-extra'
import path from 'node:path'
import chokidar from 'chokidar'
import fg from 'fast-glob'
import { fileURLToPath } from 'node:url'
import httpServer from 'http-server'
import portFinder from 'portfinder'
import Logger from '../../scripts/logger.mjs'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

const logger = new Logger('buildMaterials')

// 物料文件存放文件夹名称
const materialsDir = path.resolve(__dirname, './src')

/**
* 校验组件文件数据
* @param {string} file 组件文件路径
* @param {object} component 组件数据
* @returns
*/
const validateComponent = (file, component) => {
const requiredFields = ['component']
const fields = Object.keys(component)
const requiredList = requiredFields.filter((field) => !fields.includes(field))

if (requiredList.length) {
logger.error(`组件文件 ${file} 缺少必要字段:${requiredList.join('、')}。`)

return false
}

if (!component.npm) {
logger.warn(`组件文件 ${file} 缺少 npm 字段,出码时将不能通过import语句导入组件。`)

return false
}
Comment on lines +35 to +39
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistent validation behavior: warning with failure.

The function logs a warning when the npm field is missing (line 36) but returns false (line 38), causing the component to be skipped. This is inconsistent—warnings typically shouldn't fail validation. Either return true to allow components without npm fields, or change the log level to error to match the failure behavior.

Apply this diff to align the log level with the behavior:

   if (!component.npm) {
-    logger.warn(`组件文件 ${file} 缺少 npm 字段,出码时将不能通过import语句导入组件。`)
+    logger.error(`组件文件 ${file} 缺少 npm 字段,出码时将不能通过import语句导入组件。`)
 
     return false
   }

Alternatively, if components without npm should be allowed:

   if (!component.npm) {
     logger.warn(`组件文件 ${file} 缺少 npm 字段,出码时将不能通过import语句导入组件。`)
 
-    return false
+    // Allow components without npm field
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!component.npm) {
logger.warn(`组件文件 ${file} 缺少 npm 字段,出码时将不能通过import语句导入组件。`)
return false
}
if (!component.npm) {
logger.error(`组件文件 ${file} 缺少 npm 字段,出码时将不能通过import语句导入组件。`)
return false
}
🤖 Prompt for AI Agents
In packages/materials/buildMaterials.mjs around lines 35 to 39, the code logs a
warning when component.npm is missing but then returns false (failing the
component); change the log level to logger.error so the log matches the failure
behavior (keep the return false), i.e., replace logger.warn with logger.error
and keep the existing message so that missing npm is treated as an error and
clearly recorded.


return true
}

const generateComponents = async (entry) => {
const files = await fg('*.json', { cwd: entry })

if (!files.length) {
return
}

const bundle = {
components: [],
snippets: [],
packages: []
}
const componentsMap = []

const metaInfo = fsExtra.readJsonSync(path.resolve(entry, 'meta.json'), { throws: false })

if (metaInfo?.package) {
bundle.packages.push(metaInfo.package)
}
if (metaInfo?.snippets) {
bundle.snippets = metaInfo.snippets
}

const componentFiles = files.filter((fileName) => {
if (fileName === 'meta.json') {
return false
}

// 下划线开头的组件文件不导出
return !fileName.startsWith('_')
})

componentFiles.forEach((file) => {
const material = fsExtra.readJsonSync(path.resolve(entry, file), { throws: false })

if (!material) {
const fileFullPath = path.join(process.cwd(), file)

logger.error(`文件格式有误 (${fileFullPath})`)

return
}
Comment on lines +76 to +85
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Incorrect path construction in error message.

Line 80 constructs the error path using path.join(process.cwd(), file), but file is a filename relative to entry, not relative to process.cwd(). This produces an incorrect path in the error message.

Apply this diff to fix the path construction:

     if (!material) {
-      const fileFullPath = path.join(process.cwd(), file)
+      const fileFullPath = path.resolve(entry, file)
 
       logger.error(`文件格式有误 (${fileFullPath})`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
componentFiles.forEach((file) => {
const material = fsExtra.readJsonSync(path.resolve(entry, file), { throws: false })
if (!material) {
const fileFullPath = path.join(process.cwd(), file)
logger.error(`文件格式有误 (${fileFullPath})`)
return
}
componentFiles.forEach((file) => {
const material = fsExtra.readJsonSync(path.resolve(entry, file), { throws: false })
if (!material) {
const fileFullPath = path.resolve(entry, file)
logger.error(`文件格式有误 (${fileFullPath})`)
return
}
🤖 Prompt for AI Agents
In packages/materials/buildMaterials.mjs around lines 76 to 85, the error path
is built with path.join(process.cwd(), file) but `file` is relative to `entry`;
change the construction to resolve using the entry directory (e.g.
path.resolve(entry, file) or path.join(entry, file)) so the error message shows
the correct full path, and update the logger.error call to use that resolved
path variable.


const valid = validateComponent(file, material)

if (!valid) return

const { snippets: componentSnippets, category, ...componentInfo } = material

bundle.components.push(componentInfo)

const snippet = bundle.snippets.find((item) => item.group === category)

if (snippet) {
if (!snippet.children) {
snippet.children = []
}

if (componentSnippets) {
snippet.children.push(...componentSnippets)
}
} else if (category && componentInfo && componentSnippets) {
bundle.snippets.push({
group: category,
children: componentSnippets || []
})
}

const npmInfo = componentInfo.npm
const { package: packageName = '', exportName = '' } = npmInfo

const mapItem = {
componentName: componentInfo.component,
package: packageName,
exportName
}

if (typeof npmInfo.destructuring === 'boolean') {
mapItem.destructuring = componentInfo.npm.destructuring
}

if (npmInfo.package) {
componentsMap.push(mapItem)
}
})

return {
bundle,
componentsMap
}
}

const getFrameworkWithData = (data) => {
return {
framework: 'Vue',
materials: data
}
}

const buildComponents = async (config = {}) => {
try {
const entries = await fg('*/', {
cwd: materialsDir,
onlyDirectories: true,
deep: 1
})

const { buildCombine = true } = config

const allBundles = {
components: [],
snippets: [],
packages: []
}
let allComponentsMap = []

for (const entry of entries) {
const res = await generateComponents(path.resolve(materialsDir, `${entry}`))

if (!res) {
continue
}

fsExtra.outputJSONSync(path.resolve(__dirname, `./dist/${entry}.json`), getFrameworkWithData(res.bundle), {
spaces: 2
})
fsExtra.outputJSONSync(path.resolve(__dirname, `./dist/${entry}.compsMap.json`), res.componentsMap, { spaces: 2 })

allBundles.components = allBundles.components.concat(res.bundle.components)
allComponentsMap = allComponentsMap.concat(res.componentsMap)
allBundles.packages = allBundles.packages.concat(res.bundle.packages)

for (const snippetItem of res.bundle.snippets) {
const snippet = allBundles.snippets.find((item) => item.group === snippetItem.group)

if (snippet) {
if (!snippet.children) {
snippet.children = []
}

snippet.children.push(...(snippetItem.children || []))
} else {
allBundles.snippets.push(snippetItem)
}
}
}

if (buildCombine) {
fsExtra.outputJSONSync(path.resolve(__dirname, `./dist/index.json`), getFrameworkWithData(allBundles), {
spaces: 2
})
fsExtra.outputJSONSync(path.resolve(__dirname, `./dist/index.compsMap.json`), allComponentsMap, { spaces: 2 })
}

logger.success('物料资产包构建成功')
} catch (error) {
logger.error(`物料资产包构建失败:${error}`)
}
}

// 持续构建
async function serve() {
// 监听materials下json文件的变化
const watcher = chokidar.watch(`${materialsDir}/**/*.json`, { ignoreInitial: true })

watcher.on('all', (event, file) => {
const eventMap = {
add: '新增',
change: '更新',
unlink: '删除'
}
const fileFullPath = path.join(process.cwd(), file)

logger.info(`${eventMap[event]}组件文件 (${fileFullPath})`)

// 监听物料文件变化,更新物料资产包
buildComponents()
})

// 第一次需要手动出发构建一遍
await buildComponents()

const staticServerPort = await portFinder.getPortPromise({ port: 4001 })

const server = httpServer.createServer({
caches: 1,
cors: true,
root: path.resolve(__dirname, './dist')
})

server.listen(staticServerPort, () => {
logger.success(`物料服务已启动 http://127.0.0.1:${staticServerPort}`)
})
}

// 单次构建,分组件库
function buildSplit() {
buildComponents({ buildCombine: false })
}

// 单次构建,合并所有组件库
function build() {
buildComponents()
}

function start() {
const commandsMap = {
serve: serve,
build: build,
'build:split': buildSplit
}

const command = process.argv.slice(2)

if (!commandsMap[command]) {
logger.error(`[@opentiny/tiny-engine-materials] 不支持${command}命令`)

return
}

commandsMap[command]()
}
Comment on lines +249 to +265
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Command argument extracted incorrectly.

Line 256 uses process.argv.slice(2), which returns an array, but line 258 uses command as if it were a string when accessing commandsMap[command]. This means commandsMap[command] will always be undefined, and the script will never execute any command.

Apply this diff to fix the bug:

-  const command = process.argv.slice(2)
+  const command = process.argv[2]
 
   if (!commandsMap[command]) {

Alternatively, extract the first element explicitly:

-  const command = process.argv.slice(2)
+  const [command] = process.argv.slice(2)
 
   if (!commandsMap[command]) {
🤖 Prompt for AI Agents
In packages/materials/buildMaterials.mjs around lines 249 to 265, the code uses
process.argv.slice(2) which returns an array but treats it as a string key when
looking up commandsMap, causing commandsMap[command] to always be undefined;
change the extraction to get the first argument (e.g., const command =
process.argv[2] or const [command] = process.argv.slice(2)) and then use that
string to check and invoke commandsMap[command], keeping the existing error
handling and return behavior if the command is not found.


start()
39 changes: 39 additions & 0 deletions packages/materials/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@opentiny/tiny-engine-materials",
"version": "1.0.0",
"description": "",
Comment thread
chilingling marked this conversation as resolved.
"main": "dist/index.json",
"module": "dist/index.json",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"serve": "node buildMaterials.mjs serve",
"build": "node buildMaterials.mjs build",
"build:split": "node buildMaterials.mjs build:split"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

外层也有两个物料的命令,看下是不是一起整合下

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

全体迁移完成后再整合

},
"keywords": [],
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "https://github.com/opentiny/tiny-engine",
"directory": "packages/materials"
},
"bugs": {
"url": "https://github.com/opentiny/tiny-engine/issues"
},
"author": "OpenTiny Team",
"license": "MIT",
"homepage": "https://opentiny.design/tiny-engine",
"devDependencies": {
"chokidar": "^3.5.3",
"fast-glob": "^3.3.2",
"fs-extra": "^11.2.0",
"http-server": "^14.1.1",
"portfinder": "^1.0.32"
}
}
Loading