@vibecuting/component-project-helper 0.1.1
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/README.md +27 -0
- package/bin/component-project-helper.mjs +16 -0
- package/package.json +35 -0
- package/scripts/discover-components.mjs +133 -0
- package/scripts/postinstall.mjs +60 -0
- package/scripts/preuninstall.mjs +34 -0
- package/scripts/render-markdown.mjs +32 -0
- package/scripts/runtime-root.mjs +34 -0
- package/scripts/schemas.mjs +16 -0
- package/src/decorators/index.test.ts +45 -0
- package/src/decorators/index.ts +40 -0
- package/src/discovery/index.ts +140 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +26 -0
- package/src/lifecycle/postinstall.test.ts +79 -0
- package/src/lifecycle/postinstall.ts +71 -0
- package/src/lifecycle/preuninstall.ts +39 -0
- package/src/markdown/index.test.ts +20 -0
- package/src/markdown/index.ts +43 -0
- package/src/runtime/index.ts +36 -0
- package/src/schemas/index.test.ts +17 -0
- package/src/schemas/index.ts +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @vibecuting/component-project-helper
|
|
2
|
+
|
|
3
|
+
组件工程辅助包。
|
|
4
|
+
|
|
5
|
+
职责:
|
|
6
|
+
|
|
7
|
+
- 作为可发布的 npm 包承载组件工程辅助逻辑
|
|
8
|
+
- 安装时扫描 `src/components/**/*.tsx`,把组件元数据生成到 `.agents/skills/project-allow-component/components/`
|
|
9
|
+
- 通过 `postinstall` / `preuninstall` 维护生成文件和清理清单
|
|
10
|
+
- 提供 `VideoComponent` 装饰器和 `defineComponentProjectComponentMetadata()` 注解定义,供后续需要元数据注解的组件复用
|
|
11
|
+
- `postinstall` / `preuninstall` 的实际执行入口在 `scripts/` 下,便于 npm 安装时直接运行
|
|
12
|
+
- 作为主工程之外的独立 helper 包,避免把辅助逻辑重新塞回主站代码里
|
|
13
|
+
|
|
14
|
+
目录结构:
|
|
15
|
+
|
|
16
|
+
- `src/decorators`:注解定义和元数据读写
|
|
17
|
+
- `src/schemas`:zod schema
|
|
18
|
+
- `src/discovery`:组件发现与解析
|
|
19
|
+
- `src/markdown`:Markdown 渲染
|
|
20
|
+
- `src/runtime`:安装根与输出路径解析
|
|
21
|
+
- `src/lifecycle`:postinstall / preuninstall 生命周期逻辑
|
|
22
|
+
|
|
23
|
+
常用命令:
|
|
24
|
+
|
|
25
|
+
- `pnpm typecheck`
|
|
26
|
+
- `pnpm test`
|
|
27
|
+
- `node ./bin/component-project-helper.mjs postinstall`
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const binDir = path.dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const scriptName = process.argv[2] === 'preuninstall' ? 'preuninstall.mjs' : 'postinstall.mjs'
|
|
9
|
+
const scriptPath = path.resolve(binDir, '../scripts', scriptName)
|
|
10
|
+
|
|
11
|
+
const result = spawnSync(process.execPath, [scriptPath], {
|
|
12
|
+
stdio: 'inherit',
|
|
13
|
+
env: process.env,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
process.exit(result.status ?? 1)
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibecuting/component-project-helper",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"scripts",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md",
|
|
15
|
+
"package.json"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"registry": "https://registry.npmjs.org",
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"postinstall": "node ./scripts/postinstall.mjs",
|
|
23
|
+
"preuninstall": "node ./scripts/preuninstall.mjs",
|
|
24
|
+
"typecheck": "node ../../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"test": "node ../../../node_modules/vitest/vitest.mjs run"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"reflect-metadata": "^0.2.2",
|
|
29
|
+
"typescript": "^5.9.3",
|
|
30
|
+
"zod": "4.1.12"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"vitest": "^3.2.4"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { ComponentProjectComponentMetadataSchema } from './schemas.mjs'
|
|
5
|
+
|
|
6
|
+
function extractBalancedObjectLiteral(source, startIndex) {
|
|
7
|
+
let depth = 0
|
|
8
|
+
let inString = false
|
|
9
|
+
let stringQuote = ''
|
|
10
|
+
let escaped = false
|
|
11
|
+
|
|
12
|
+
for (let index = startIndex; index < source.length; index += 1) {
|
|
13
|
+
const character = source[index]
|
|
14
|
+
|
|
15
|
+
if (inString) {
|
|
16
|
+
if (escaped) {
|
|
17
|
+
escaped = false
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (character === '\\') {
|
|
22
|
+
escaped = true
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (character === stringQuote) {
|
|
27
|
+
inString = false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (character === '"' || character === "'" || character === '`') {
|
|
34
|
+
inString = true
|
|
35
|
+
stringQuote = character
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (character === '{') {
|
|
40
|
+
depth += 1
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (character === '}') {
|
|
45
|
+
depth -= 1
|
|
46
|
+
if (depth === 0) {
|
|
47
|
+
return source.slice(startIndex, index + 1)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseMetadataLiteral(literal) {
|
|
56
|
+
try {
|
|
57
|
+
const value = Function(`"use strict"; return (${literal});`)()
|
|
58
|
+
const parsed = ComponentProjectComponentMetadataSchema.safeParse(value)
|
|
59
|
+
return parsed.success ? parsed.data : undefined
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readMetadataFromSource(source) {
|
|
66
|
+
const patterns = [
|
|
67
|
+
/componentMetadata\s*=\s*defineComponentProjectComponentMetadata\s*\(/m,
|
|
68
|
+
/componentMetadata\s*=\s*VideoComponent\s*\(/m,
|
|
69
|
+
/componentMetadata\s*=\s*/m,
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
for (const pattern of patterns) {
|
|
73
|
+
const match = pattern.exec(source)
|
|
74
|
+
if (!match) {
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const startIndex = source.indexOf('{', match.index)
|
|
79
|
+
if (startIndex < 0) {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const literal = extractBalancedObjectLiteral(source, startIndex)
|
|
84
|
+
if (!literal) {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const parsed = parseMetadataLiteral(literal)
|
|
89
|
+
if (parsed) {
|
|
90
|
+
return parsed
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const decoratorMatch = /@VideoComponent\s*\((\{[\s\S]*?\})\)/m.exec(source)
|
|
95
|
+
if (decoratorMatch) {
|
|
96
|
+
return parseMetadataLiteral(decoratorMatch[1])
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function discoverComponentProjectComponents(projectRoot) {
|
|
103
|
+
const componentsDir = path.join(projectRoot, 'src', 'components')
|
|
104
|
+
let entries = []
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
entries = await fs.readdir(componentsDir)
|
|
108
|
+
} catch {
|
|
109
|
+
return []
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const discovered = []
|
|
113
|
+
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
if (!entry.endsWith('.tsx')) {
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const absolutePath = path.join(componentsDir, entry)
|
|
120
|
+
const source = await fs.readFile(absolutePath, 'utf8')
|
|
121
|
+
const metadata = readMetadataFromSource(source)
|
|
122
|
+
if (!metadata) {
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
discovered.push({
|
|
127
|
+
...metadata,
|
|
128
|
+
sourceFile: path.relative(projectRoot, absolutePath),
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return discovered.sort((left, right) => left.name.localeCompare(right.name))
|
|
133
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { discoverComponentProjectComponents } from './discover-components.mjs'
|
|
5
|
+
import {
|
|
6
|
+
resolveComponentProjectGeneratedDocsDir,
|
|
7
|
+
resolveComponentProjectGeneratedManifestPath,
|
|
8
|
+
resolveComponentProjectInstallRoot,
|
|
9
|
+
} from './runtime-root.mjs'
|
|
10
|
+
import { renderComponentProjectMarkdown } from './render-markdown.mjs'
|
|
11
|
+
import { ComponentProjectGeneratedManifestSchema } from './schemas.mjs'
|
|
12
|
+
|
|
13
|
+
async function ensureDirectory(dir) {
|
|
14
|
+
await fs.mkdir(dir, { recursive: true })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function removeStaleGeneratedFiles(manifestPath, nextFiles) {
|
|
18
|
+
try {
|
|
19
|
+
const previous = ComponentProjectGeneratedManifestSchema.parse(
|
|
20
|
+
JSON.parse(await fs.readFile(manifestPath, 'utf8')),
|
|
21
|
+
)
|
|
22
|
+
const staleFiles = previous.files.filter((filePath) => !nextFiles.includes(filePath))
|
|
23
|
+
await Promise.all(staleFiles.map((filePath) => fs.rm(filePath, { force: true })))
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error?.code !== 'ENOENT') {
|
|
26
|
+
throw error
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runComponentProjectPostinstall(projectRoot = resolveComponentProjectInstallRoot()) {
|
|
32
|
+
const docsDir = resolveComponentProjectGeneratedDocsDir(projectRoot)
|
|
33
|
+
const manifestPath = resolveComponentProjectGeneratedManifestPath(projectRoot)
|
|
34
|
+
const components = await discoverComponentProjectComponents(projectRoot)
|
|
35
|
+
|
|
36
|
+
await ensureDirectory(docsDir)
|
|
37
|
+
|
|
38
|
+
const files = []
|
|
39
|
+
for (const component of components) {
|
|
40
|
+
const filePath = path.join(docsDir, `${component.name}.md`)
|
|
41
|
+
await fs.writeFile(filePath, renderComponentProjectMarkdown(component), 'utf8')
|
|
42
|
+
files.push(filePath)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await removeStaleGeneratedFiles(manifestPath, files)
|
|
46
|
+
|
|
47
|
+
const manifest = ComponentProjectGeneratedManifestSchema.parse({
|
|
48
|
+
projectRoot,
|
|
49
|
+
generatedAt: new Date().toISOString(),
|
|
50
|
+
docsDir,
|
|
51
|
+
files,
|
|
52
|
+
})
|
|
53
|
+
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)
|
|
57
|
+
|
|
58
|
+
if (isDirectRun) {
|
|
59
|
+
await runComponentProjectPostinstall()
|
|
60
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { resolveComponentProjectGeneratedManifestPath } from './runtime-root.mjs'
|
|
5
|
+
import { ComponentProjectGeneratedManifestSchema } from './schemas.mjs'
|
|
6
|
+
|
|
7
|
+
export async function runComponentProjectPreuninstall(
|
|
8
|
+
projectRoot = process.env.INIT_CWD ?? process.env.npm_config_local_prefix ?? process.cwd(),
|
|
9
|
+
) {
|
|
10
|
+
const manifestPath = resolveComponentProjectGeneratedManifestPath(projectRoot)
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const manifest = ComponentProjectGeneratedManifestSchema.parse(
|
|
14
|
+
JSON.parse(await fs.readFile(manifestPath, 'utf8')),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
for (const filePath of manifest.files) {
|
|
18
|
+
await fs.rm(filePath, { force: true })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await fs.rm(manifestPath, { force: true })
|
|
22
|
+
await fs.rm(manifest.docsDir, { recursive: true, force: true })
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error?.code !== 'ENOENT') {
|
|
25
|
+
throw error
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)
|
|
31
|
+
|
|
32
|
+
if (isDirectRun) {
|
|
33
|
+
await runComponentProjectPreuninstall()
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ComponentProjectComponentMetadataSchema } from './schemas.mjs'
|
|
2
|
+
|
|
3
|
+
function escapeInline(value) {
|
|
4
|
+
return value.replaceAll('`', '\\`')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function renderComponentProjectMarkdown(input) {
|
|
8
|
+
const metadata = ComponentProjectComponentMetadataSchema.parse(input)
|
|
9
|
+
const tags = metadata.tags.length > 0 ? metadata.tags : ['component']
|
|
10
|
+
|
|
11
|
+
return [
|
|
12
|
+
'---',
|
|
13
|
+
`name: ${metadata.name}`,
|
|
14
|
+
`description: ${metadata.description}`,
|
|
15
|
+
`sourceFile: ${metadata.sourceFile}`,
|
|
16
|
+
`propsTypeName: ${metadata.propsTypeName ?? ''}`,
|
|
17
|
+
'tags:',
|
|
18
|
+
...tags.map((tag) => ` - ${tag}`),
|
|
19
|
+
'---',
|
|
20
|
+
'',
|
|
21
|
+
`# ${metadata.name}`,
|
|
22
|
+
'',
|
|
23
|
+
metadata.description,
|
|
24
|
+
'',
|
|
25
|
+
'## Metadata',
|
|
26
|
+
'',
|
|
27
|
+
`- Source file: \`${escapeInline(metadata.sourceFile)}\``,
|
|
28
|
+
`- Props type: \`${escapeInline(metadata.propsTypeName ?? 'unknown')}\``,
|
|
29
|
+
`- Tags: ${tags.map((tag) => `\`${escapeInline(tag)}\``).join(', ')}`,
|
|
30
|
+
'',
|
|
31
|
+
].join('\n')
|
|
32
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
|
|
4
|
+
export function resolveComponentProjectInstallRoot() {
|
|
5
|
+
const candidates = [
|
|
6
|
+
process.env.INIT_CWD,
|
|
7
|
+
process.env.npm_config_local_prefix,
|
|
8
|
+
process.cwd(),
|
|
9
|
+
].filter((value) => Boolean(value && String(value).trim()))
|
|
10
|
+
|
|
11
|
+
return path.resolve(candidates[0] ?? process.cwd())
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveComponentProjectGeneratedDocsDir(projectRoot = resolveComponentProjectInstallRoot()) {
|
|
15
|
+
return path.join(
|
|
16
|
+
projectRoot,
|
|
17
|
+
'.agents',
|
|
18
|
+
'skills',
|
|
19
|
+
'project-allow-component',
|
|
20
|
+
'components',
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveComponentProjectGeneratedManifestPath(
|
|
25
|
+
projectRoot = resolveComponentProjectInstallRoot(),
|
|
26
|
+
) {
|
|
27
|
+
return path.join(
|
|
28
|
+
projectRoot,
|
|
29
|
+
'.agents',
|
|
30
|
+
'skills',
|
|
31
|
+
'project-allow-component',
|
|
32
|
+
'component-project-helper.manifest.json',
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const ComponentProjectComponentMetadataSchema = z.object({
|
|
4
|
+
name: z.string().min(1),
|
|
5
|
+
description: z.string().min(1),
|
|
6
|
+
sourceFile: z.string().min(1),
|
|
7
|
+
tags: z.array(z.string().min(1)).default([]),
|
|
8
|
+
propsTypeName: z.string().min(1).optional(),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const ComponentProjectGeneratedManifestSchema = z.object({
|
|
12
|
+
projectRoot: z.string().min(1),
|
|
13
|
+
generatedAt: z.string().min(1),
|
|
14
|
+
docsDir: z.string().min(1),
|
|
15
|
+
files: z.array(z.string().min(1)),
|
|
16
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
VideoComponent,
|
|
5
|
+
defineComponentProjectComponentMetadata,
|
|
6
|
+
getComponentProjectComponentMetadata,
|
|
7
|
+
} from './index'
|
|
8
|
+
|
|
9
|
+
test('attaches normalized metadata to decorated function components', () => {
|
|
10
|
+
const metadata = defineComponentProjectComponentMetadata({
|
|
11
|
+
name: 'SceneFrame',
|
|
12
|
+
description: 'Wraps the scene content in a consistent frame',
|
|
13
|
+
sourceFile: 'src/components/SceneFrame.tsx',
|
|
14
|
+
aspectRatio: '16:9',
|
|
15
|
+
sceneType: 'scene',
|
|
16
|
+
tags: ['layout', 'frame'],
|
|
17
|
+
propsTypeName: 'SceneFrameProps',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const SceneFrame = () => null
|
|
21
|
+
|
|
22
|
+
const decorated = VideoComponent(metadata)(SceneFrame)
|
|
23
|
+
|
|
24
|
+
expect(decorated).toBe(SceneFrame)
|
|
25
|
+
expect(getComponentProjectComponentMetadata(SceneFrame)).toEqual(metadata)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('attaches normalized metadata to decorated function components', () => {
|
|
29
|
+
const metadata = defineComponentProjectComponentMetadata({
|
|
30
|
+
name: 'SceneTitle',
|
|
31
|
+
description: 'Renders the title block for scene hero content',
|
|
32
|
+
sourceFile: 'src/components/SceneTitle.tsx',
|
|
33
|
+
aspectRatio: '16:9',
|
|
34
|
+
sceneType: 'intro',
|
|
35
|
+
tags: ['title', 'hero'],
|
|
36
|
+
propsTypeName: 'SceneTitleProps',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const SceneTitle = () => null
|
|
40
|
+
|
|
41
|
+
const decorated = VideoComponent(metadata)(SceneTitle)
|
|
42
|
+
|
|
43
|
+
expect(decorated).toBe(SceneTitle)
|
|
44
|
+
expect(getComponentProjectComponentMetadata(SceneTitle)).toEqual(metadata)
|
|
45
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import 'reflect-metadata'
|
|
2
|
+
|
|
3
|
+
import type { ComponentType } from 'react'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ComponentProjectComponentMetadataSchema,
|
|
7
|
+
type ComponentProjectComponentMetadata,
|
|
8
|
+
} from '../schemas'
|
|
9
|
+
|
|
10
|
+
export const COMPONENT_PROJECT_METADATA_KEY = Symbol.for(
|
|
11
|
+
'@vibecuting/component-project-helper/component-metadata',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export function defineComponentProjectComponentMetadata(
|
|
15
|
+
metadata: ComponentProjectComponentMetadata,
|
|
16
|
+
): ComponentProjectComponentMetadata {
|
|
17
|
+
return ComponentProjectComponentMetadataSchema.parse(metadata)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function VideoComponent(metadata: ComponentProjectComponentMetadata) {
|
|
21
|
+
const normalized = defineComponentProjectComponentMetadata(metadata)
|
|
22
|
+
|
|
23
|
+
return function componentProjectDecorator<T extends ComponentType<any>>(target: T): T {
|
|
24
|
+
Reflect.defineMetadata(COMPONENT_PROJECT_METADATA_KEY, normalized, target)
|
|
25
|
+
return target
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getComponentProjectComponentMetadata(
|
|
30
|
+
target: unknown,
|
|
31
|
+
): ComponentProjectComponentMetadata | undefined {
|
|
32
|
+
if (!target || (typeof target !== 'object' && typeof target !== 'function')) {
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Reflect.getMetadata(
|
|
37
|
+
COMPONENT_PROJECT_METADATA_KEY,
|
|
38
|
+
target,
|
|
39
|
+
) as ComponentProjectComponentMetadata | undefined
|
|
40
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ComponentProjectComponentMetadataSchema,
|
|
6
|
+
type ComponentProjectComponentMetadata,
|
|
7
|
+
} from '../schemas'
|
|
8
|
+
|
|
9
|
+
export type ComponentProjectComponentDiscovery = ComponentProjectComponentMetadata
|
|
10
|
+
|
|
11
|
+
function extractBalancedObjectLiteral(source: string, startIndex: number): string | undefined {
|
|
12
|
+
let depth = 0
|
|
13
|
+
let inString = false
|
|
14
|
+
let stringQuote = ''
|
|
15
|
+
let escaped = false
|
|
16
|
+
|
|
17
|
+
for (let index = startIndex; index < source.length; index += 1) {
|
|
18
|
+
const character = source[index]
|
|
19
|
+
|
|
20
|
+
if (inString) {
|
|
21
|
+
if (escaped) {
|
|
22
|
+
escaped = false
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (character === '\\') {
|
|
27
|
+
escaped = true
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (character === stringQuote) {
|
|
32
|
+
inString = false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (character === '"' || character === "'" || character === '`') {
|
|
39
|
+
inString = true
|
|
40
|
+
stringQuote = character
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (character === '{') {
|
|
45
|
+
depth += 1
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (character === '}') {
|
|
50
|
+
depth -= 1
|
|
51
|
+
if (depth === 0) {
|
|
52
|
+
return source.slice(startIndex, index + 1)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseMetadataLiteral(literal: string): ComponentProjectComponentMetadata | undefined {
|
|
61
|
+
try {
|
|
62
|
+
const value = Function(`"use strict"; return (${literal});`)()
|
|
63
|
+
const parsed = ComponentProjectComponentMetadataSchema.safeParse(value)
|
|
64
|
+
return parsed.success ? parsed.data : undefined
|
|
65
|
+
} catch {
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readMetadataFromSource(source: string): ComponentProjectComponentMetadata | undefined {
|
|
71
|
+
const candidates = [
|
|
72
|
+
/componentMetadata\s*=\s*defineComponentProjectComponentMetadata\s*\(/m,
|
|
73
|
+
/componentMetadata\s*=\s*VideoComponent\s*\(/m,
|
|
74
|
+
/componentMetadata\s*=\s*/m,
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
for (const pattern of candidates) {
|
|
78
|
+
const match = pattern.exec(source)
|
|
79
|
+
if (!match) {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const startIndex = source.indexOf('{', match.index)
|
|
84
|
+
if (startIndex < 0) {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const literal = extractBalancedObjectLiteral(source, startIndex)
|
|
89
|
+
if (!literal) {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const parsed = parseMetadataLiteral(literal)
|
|
94
|
+
if (parsed) {
|
|
95
|
+
return parsed
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const decoratorMatch = /@VideoComponent\s*\((\{[\s\S]*?\})\)/m.exec(source)
|
|
100
|
+
if (decoratorMatch) {
|
|
101
|
+
return parseMetadataLiteral(decoratorMatch[1])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function discoverComponentProjectComponents(
|
|
108
|
+
projectRoot: string,
|
|
109
|
+
): Promise<ComponentProjectComponentDiscovery[]> {
|
|
110
|
+
const componentsDir = path.join(projectRoot, 'src', 'components')
|
|
111
|
+
let entries: string[] = []
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
entries = await fs.readdir(componentsDir)
|
|
115
|
+
} catch {
|
|
116
|
+
return []
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const discovered: ComponentProjectComponentDiscovery[] = []
|
|
120
|
+
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (!entry.endsWith('.tsx')) {
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const absolutePath = path.join(componentsDir, entry)
|
|
127
|
+
const source = await fs.readFile(absolutePath, 'utf8')
|
|
128
|
+
const metadata = readMetadataFromSource(source)
|
|
129
|
+
if (!metadata) {
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
discovered.push({
|
|
134
|
+
...metadata,
|
|
135
|
+
sourceFile: path.relative(projectRoot, absolutePath),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return discovered.sort((left, right) => left.name.localeCompare(right.name))
|
|
140
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export {
|
|
2
|
+
VideoComponent,
|
|
3
|
+
defineComponentProjectComponentMetadata,
|
|
4
|
+
getComponentProjectComponentMetadata,
|
|
5
|
+
} from './decorators'
|
|
6
|
+
export {
|
|
7
|
+
ComponentProjectComponentMetadataSchema,
|
|
8
|
+
ComponentProjectGeneratedManifestSchema,
|
|
9
|
+
} from './schemas'
|
|
10
|
+
export {
|
|
11
|
+
discoverComponentProjectComponents,
|
|
12
|
+
type ComponentProjectComponentDiscovery,
|
|
13
|
+
} from './discovery'
|
|
14
|
+
export {
|
|
15
|
+
renderComponentProjectMarkdown,
|
|
16
|
+
type ComponentProjectMarkdownDocument,
|
|
17
|
+
} from './markdown'
|
|
18
|
+
export {
|
|
19
|
+
resolveComponentProjectGeneratedDocsDir,
|
|
20
|
+
resolveComponentProjectGeneratedManifestPath,
|
|
21
|
+
resolveComponentProjectInstallRoot,
|
|
22
|
+
} from './runtime'
|
|
23
|
+
export { runComponentProjectPostinstall } from './lifecycle/postinstall'
|
|
24
|
+
export { runComponentProjectPreuninstall } from './lifecycle/preuninstall'
|
|
25
|
+
|
|
26
|
+
export const componentProjectHelperPackageName = '@vibecuting/component-project-helper' as const
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
|
|
5
|
+
import { expect, test } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { runComponentProjectPostinstall } from './postinstall'
|
|
8
|
+
import { runComponentProjectPreuninstall } from './preuninstall'
|
|
9
|
+
|
|
10
|
+
async function createFixtureProjectRoot(): Promise<string> {
|
|
11
|
+
const projectRoot = await fs.mkdtemp(path.join(tmpdir(), 'component-project-helper-'))
|
|
12
|
+
await fs.mkdir(path.join(projectRoot, 'src', 'components'), { recursive: true })
|
|
13
|
+
|
|
14
|
+
await fs.writeFile(
|
|
15
|
+
path.join(projectRoot, 'src', 'components', 'SceneFrame.tsx'),
|
|
16
|
+
`export const componentMetadata = {
|
|
17
|
+
name: 'SceneFrame',
|
|
18
|
+
description: 'Wraps the scene content in a consistent frame',
|
|
19
|
+
sourceFile: 'src/components/SceneFrame.tsx',
|
|
20
|
+
aspectRatio: '16:9',
|
|
21
|
+
sceneType: 'scene',
|
|
22
|
+
tags: ['layout', 'frame'],
|
|
23
|
+
propsTypeName: 'SceneFrameProps',
|
|
24
|
+
} as const
|
|
25
|
+
`,
|
|
26
|
+
'utf8',
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
await fs.writeFile(
|
|
30
|
+
path.join(projectRoot, 'src', 'components', 'SceneTitle.tsx'),
|
|
31
|
+
`export const componentMetadata = {
|
|
32
|
+
name: 'SceneTitle',
|
|
33
|
+
description: 'Renders the title block for scene hero content',
|
|
34
|
+
sourceFile: 'src/components/SceneTitle.tsx',
|
|
35
|
+
aspectRatio: '16:9',
|
|
36
|
+
sceneType: 'intro',
|
|
37
|
+
tags: ['title'],
|
|
38
|
+
propsTypeName: 'SceneTitleProps',
|
|
39
|
+
} as const
|
|
40
|
+
`,
|
|
41
|
+
'utf8',
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return projectRoot
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test('postinstall generates markdown and preuninstall removes it', async () => {
|
|
48
|
+
const projectRoot = await createFixtureProjectRoot()
|
|
49
|
+
const docsDir = path.join(
|
|
50
|
+
projectRoot,
|
|
51
|
+
'.agents',
|
|
52
|
+
'skills',
|
|
53
|
+
'project-allow-component',
|
|
54
|
+
'components',
|
|
55
|
+
)
|
|
56
|
+
const manifestPath = path.join(
|
|
57
|
+
projectRoot,
|
|
58
|
+
'.agents',
|
|
59
|
+
'skills',
|
|
60
|
+
'project-allow-component',
|
|
61
|
+
'component-project-helper.manifest.json',
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
await runComponentProjectPostinstall(projectRoot)
|
|
65
|
+
|
|
66
|
+
await expect(fs.stat(path.join(docsDir, 'SceneFrame.md'))).resolves.toBeDefined()
|
|
67
|
+
await expect(fs.stat(path.join(docsDir, 'SceneTitle.md'))).resolves.toBeDefined()
|
|
68
|
+
await expect(fs.stat(manifestPath)).resolves.toBeDefined()
|
|
69
|
+
|
|
70
|
+
await runComponentProjectPreuninstall(projectRoot)
|
|
71
|
+
|
|
72
|
+
await expect(fs.stat(path.join(docsDir, 'SceneFrame.md'))).rejects.toMatchObject({
|
|
73
|
+
code: 'ENOENT',
|
|
74
|
+
})
|
|
75
|
+
await expect(fs.stat(path.join(docsDir, 'SceneTitle.md'))).rejects.toMatchObject({
|
|
76
|
+
code: 'ENOENT',
|
|
77
|
+
})
|
|
78
|
+
await expect(fs.stat(manifestPath)).rejects.toMatchObject({ code: 'ENOENT' })
|
|
79
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { ComponentProjectGeneratedManifestSchema } from '../schemas'
|
|
6
|
+
import { discoverComponentProjectComponents } from '../discovery'
|
|
7
|
+
import { renderComponentProjectMarkdown } from '../markdown'
|
|
8
|
+
import {
|
|
9
|
+
resolveComponentProjectGeneratedDocsDir,
|
|
10
|
+
resolveComponentProjectGeneratedManifestPath,
|
|
11
|
+
resolveComponentProjectInstallRoot,
|
|
12
|
+
} from '../runtime'
|
|
13
|
+
|
|
14
|
+
async function ensureDirectory(dir: string): Promise<void> {
|
|
15
|
+
await fs.mkdir(dir, { recursive: true })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function removeStaleGeneratedFiles(
|
|
19
|
+
manifestPath: string,
|
|
20
|
+
nextFiles: string[],
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
const previous = ComponentProjectGeneratedManifestSchema.parse(
|
|
24
|
+
JSON.parse(await fs.readFile(manifestPath, 'utf8')),
|
|
25
|
+
)
|
|
26
|
+
const staleFiles = previous.files.filter((filePath) => !nextFiles.includes(filePath))
|
|
27
|
+
await Promise.all(staleFiles.map((filePath) => fs.rm(filePath, { force: true })))
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
30
|
+
throw error
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runComponentProjectPostinstall(
|
|
36
|
+
projectRoot = resolveComponentProjectInstallRoot(),
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
const docsDir = resolveComponentProjectGeneratedDocsDir(projectRoot)
|
|
39
|
+
const manifestPath = resolveComponentProjectGeneratedManifestPath(projectRoot)
|
|
40
|
+
const components = await discoverComponentProjectComponents(projectRoot)
|
|
41
|
+
|
|
42
|
+
await ensureDirectory(docsDir)
|
|
43
|
+
|
|
44
|
+
const files: string[] = []
|
|
45
|
+
|
|
46
|
+
for (const component of components) {
|
|
47
|
+
const filePath = path.join(docsDir, `${component.name}.md`)
|
|
48
|
+
const markdown = renderComponentProjectMarkdown(component)
|
|
49
|
+
await fs.writeFile(filePath, markdown, 'utf8')
|
|
50
|
+
files.push(filePath)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await removeStaleGeneratedFiles(manifestPath, files)
|
|
54
|
+
|
|
55
|
+
const manifest = ComponentProjectGeneratedManifestSchema.parse({
|
|
56
|
+
projectRoot,
|
|
57
|
+
generatedAt: new Date().toISOString(),
|
|
58
|
+
docsDir,
|
|
59
|
+
files,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main(): Promise<void> {
|
|
66
|
+
await runComponentProjectPostinstall()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
70
|
+
await main()
|
|
71
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { ComponentProjectGeneratedManifestSchema } from '../schemas'
|
|
6
|
+
import { resolveComponentProjectGeneratedManifestPath } from '../runtime'
|
|
7
|
+
|
|
8
|
+
export async function runComponentProjectPreuninstall(
|
|
9
|
+
projectRoot = process.env.INIT_CWD ??
|
|
10
|
+
process.env.npm_config_local_prefix ??
|
|
11
|
+
process.cwd(),
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const manifestPath = resolveComponentProjectGeneratedManifestPath(projectRoot)
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const manifest = ComponentProjectGeneratedManifestSchema.parse(
|
|
17
|
+
JSON.parse(await fs.readFile(manifestPath, 'utf8')),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
for (const filePath of manifest.files) {
|
|
21
|
+
await fs.rm(filePath, { force: true })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await fs.rm(manifestPath, { force: true })
|
|
25
|
+
await fs.rm(manifest.docsDir, { recursive: true, force: true })
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
28
|
+
throw error
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function main(): Promise<void> {
|
|
34
|
+
await runComponentProjectPreuninstall()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
38
|
+
await main()
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { renderComponentProjectMarkdown } from './index'
|
|
4
|
+
|
|
5
|
+
test('renders markdown with metadata fields', () => {
|
|
6
|
+
const markdown = renderComponentProjectMarkdown({
|
|
7
|
+
name: 'SceneFrame',
|
|
8
|
+
description: 'Wraps the scene content in a consistent frame',
|
|
9
|
+
sourceFile: 'src/components/SceneFrame.tsx',
|
|
10
|
+
aspectRatio: '16:9',
|
|
11
|
+
sceneType: 'scene',
|
|
12
|
+
tags: ['layout', 'frame'],
|
|
13
|
+
propsTypeName: 'SceneFrameProps',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
expect(markdown).toContain('# SceneFrame')
|
|
17
|
+
expect(markdown).toContain('SceneFrameProps')
|
|
18
|
+
expect(markdown).toContain('src/components/SceneFrame.tsx')
|
|
19
|
+
expect(markdown).toContain('16:9')
|
|
20
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComponentProjectComponentMetadataSchema,
|
|
3
|
+
type ComponentProjectComponentMetadata,
|
|
4
|
+
} from '../schemas'
|
|
5
|
+
|
|
6
|
+
export type ComponentProjectMarkdownDocument = ComponentProjectComponentMetadata
|
|
7
|
+
|
|
8
|
+
function escapeInline(value: string): string {
|
|
9
|
+
return value.replaceAll('`', '\\`')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function renderComponentProjectMarkdown(
|
|
13
|
+
input: ComponentProjectMarkdownDocument,
|
|
14
|
+
): string {
|
|
15
|
+
const metadata = ComponentProjectComponentMetadataSchema.parse(input)
|
|
16
|
+
const tags = metadata.tags.length > 0 ? metadata.tags : ['component']
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
'---',
|
|
20
|
+
`name: ${metadata.name}`,
|
|
21
|
+
`description: ${metadata.description}`,
|
|
22
|
+
`sourceFile: ${metadata.sourceFile}`,
|
|
23
|
+
`aspectRatio: ${metadata.aspectRatio}`,
|
|
24
|
+
`sceneType: ${metadata.sceneType ?? ''}`,
|
|
25
|
+
`propsTypeName: ${metadata.propsTypeName ?? ''}`,
|
|
26
|
+
'tags:',
|
|
27
|
+
...tags.map((tag) => ` - ${tag}`),
|
|
28
|
+
'---',
|
|
29
|
+
'',
|
|
30
|
+
`# ${metadata.name}`,
|
|
31
|
+
'',
|
|
32
|
+
metadata.description,
|
|
33
|
+
'',
|
|
34
|
+
'## Metadata',
|
|
35
|
+
'',
|
|
36
|
+
`- Source file: \`${escapeInline(metadata.sourceFile)}\``,
|
|
37
|
+
`- Aspect ratio: \`${escapeInline(metadata.aspectRatio)}\``,
|
|
38
|
+
`- Scene type: \`${escapeInline(metadata.sceneType ?? 'unknown')}\``,
|
|
39
|
+
`- Props type: \`${escapeInline(metadata.propsTypeName ?? 'unknown')}\``,
|
|
40
|
+
`- Tags: ${tags.map((tag) => `\`${escapeInline(tag)}\``).join(', ')}`,
|
|
41
|
+
'',
|
|
42
|
+
].join('\n')
|
|
43
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
|
|
4
|
+
export function resolveComponentProjectInstallRoot(): string {
|
|
5
|
+
const candidates = [
|
|
6
|
+
process.env.INIT_CWD,
|
|
7
|
+
process.env.npm_config_local_prefix,
|
|
8
|
+
process.cwd(),
|
|
9
|
+
].filter((value): value is string => Boolean(value && value.trim()))
|
|
10
|
+
|
|
11
|
+
return path.resolve(candidates[0] ?? process.cwd())
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveComponentProjectGeneratedDocsDir(
|
|
15
|
+
projectRoot = resolveComponentProjectInstallRoot(),
|
|
16
|
+
): string {
|
|
17
|
+
return path.join(
|
|
18
|
+
projectRoot,
|
|
19
|
+
'.agents',
|
|
20
|
+
'skills',
|
|
21
|
+
'project-allow-component',
|
|
22
|
+
'components',
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveComponentProjectGeneratedManifestPath(
|
|
27
|
+
projectRoot = resolveComponentProjectInstallRoot(),
|
|
28
|
+
): string {
|
|
29
|
+
return path.join(
|
|
30
|
+
projectRoot,
|
|
31
|
+
'.agents',
|
|
32
|
+
'skills',
|
|
33
|
+
'project-allow-component',
|
|
34
|
+
'component-project-helper.manifest.json',
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { ComponentProjectComponentMetadataSchema } from './index'
|
|
4
|
+
|
|
5
|
+
test('validates component metadata', () => {
|
|
6
|
+
const metadata = ComponentProjectComponentMetadataSchema.parse({
|
|
7
|
+
name: 'SceneFrame',
|
|
8
|
+
description: 'Wraps the scene content in a consistent frame',
|
|
9
|
+
sourceFile: 'src/components/SceneFrame.tsx',
|
|
10
|
+
aspectRatio: '16:9',
|
|
11
|
+
sceneType: 'scene',
|
|
12
|
+
tags: ['layout', 'frame'],
|
|
13
|
+
propsTypeName: 'SceneFrameProps',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
expect(metadata.tags).toEqual(['layout', 'frame'])
|
|
17
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const ComponentProjectComponentMetadataSchema = z.object({
|
|
4
|
+
name: z.string().min(1),
|
|
5
|
+
description: z.string().min(1),
|
|
6
|
+
sourceFile: z.string().min(1),
|
|
7
|
+
aspectRatio: z.enum(['9:16', '16:9']),
|
|
8
|
+
sceneType: z.enum(['intro', 'scene', 'outro']).optional(),
|
|
9
|
+
tags: z.array(z.string().min(1)).default([]),
|
|
10
|
+
propsTypeName: z.string().min(1).optional(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const ComponentProjectGeneratedManifestSchema = z.object({
|
|
14
|
+
projectRoot: z.string().min(1),
|
|
15
|
+
generatedAt: z.string().min(1),
|
|
16
|
+
docsDir: z.string().min(1),
|
|
17
|
+
files: z.array(z.string().min(1)),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export type ComponentProjectComponentMetadata = z.infer<
|
|
21
|
+
typeof ComponentProjectComponentMetadataSchema
|
|
22
|
+
>
|
|
23
|
+
export type ComponentProjectGeneratedManifest = z.infer<
|
|
24
|
+
typeof ComponentProjectGeneratedManifestSchema
|
|
25
|
+
>
|