@vibecuting/component-project-helper 0.1.7 → 0.1.8
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 +4 -1
- package/package.json +1 -1
- package/scripts/discover-components.mjs +42 -19
- package/scripts/render-markdown.mjs +4 -0
- package/scripts/runtime-root.mjs +31 -2
- package/scripts/schemas.mjs +2 -0
- package/src/discovery/index.test.ts +70 -0
- package/src/discovery/index.ts +43 -19
- package/src/lifecycle/package-json.ts +47 -0
- package/src/lifecycle/postinstall.test.ts +55 -20
- package/src/lifecycle/postinstall.ts +2 -0
- package/src/lifecycle/preuninstall.ts +3 -0
- package/src/runtime/index.ts +31 -2
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ NPM 地址:
|
|
|
12
12
|
- 提供组件注解、schema、发现器、Markdown 生成和安装期生命周期逻辑。
|
|
13
13
|
- 安装时扫描 `src/components/**/*.tsx`,把组件元数据生成到 `.agents/skills/project-allow-component/components/`。
|
|
14
14
|
- 通过 `postinstall` / `preuninstall` 维护生成文件和清理清单。
|
|
15
|
+
- 安装时会给宿主工程的 `package.json` 补上 `update-component-skills`,它会调用同一套 `postinstall` 逻辑。
|
|
16
|
+
- 宿主工程也可以直接运行 `pnpm update-component-skills` 手动刷新同一套生成结果。
|
|
15
17
|
- 作为主工程之外的独立 helper 包,避免把辅助逻辑重新塞回主站代码里。
|
|
16
18
|
|
|
17
19
|
目录结构:
|
|
@@ -28,8 +30,9 @@ NPM 地址:
|
|
|
28
30
|
- `pnpm typecheck`
|
|
29
31
|
- `pnpm test`
|
|
30
32
|
- `node ./bin/component-project-helper.mjs postinstall`
|
|
33
|
+
- `pnpm update-component-skills`
|
|
31
34
|
|
|
32
35
|
发布与安装:
|
|
33
36
|
|
|
34
|
-
- 发布到
|
|
37
|
+
- 发布到 npmjs 公共 registry 时使用 `@vibecuting/component-project-helper`
|
|
35
38
|
- 组件模板和主工程只依赖这个包的已发布版本,不依赖 workspace 软链接
|
package/package.json
CHANGED
|
@@ -62,7 +62,47 @@ function parseMetadataLiteral(literal) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
async function collectComponentSourceFiles(directory) {
|
|
66
|
+
let entries = []
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
entries = await fs.readdir(directory, { withFileTypes: true })
|
|
70
|
+
} catch {
|
|
71
|
+
return []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const files = []
|
|
75
|
+
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const entryPath = path.join(directory, entry.name)
|
|
78
|
+
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
files.push(...(await collectComponentSourceFiles(entryPath)))
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!entry.isFile()) {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (entry.name.endsWith('.d.ts')) {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {
|
|
93
|
+
files.push(entryPath)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return files
|
|
98
|
+
}
|
|
99
|
+
|
|
65
100
|
function readMetadataFromSource(source) {
|
|
101
|
+
const decoratorMatch = /@VideoComponent\s*\((\{[\s\S]*?\})\)/m.exec(source)
|
|
102
|
+
if (decoratorMatch) {
|
|
103
|
+
return parseMetadataLiteral(decoratorMatch[1])
|
|
104
|
+
}
|
|
105
|
+
|
|
66
106
|
const patterns = [
|
|
67
107
|
/componentMetadata\s*=\s*defineComponentProjectComponentMetadata\s*\(/m,
|
|
68
108
|
/componentMetadata\s*=\s*VideoComponent\s*\(/m,
|
|
@@ -91,32 +131,15 @@ function readMetadataFromSource(source) {
|
|
|
91
131
|
}
|
|
92
132
|
}
|
|
93
133
|
|
|
94
|
-
const decoratorMatch = /@VideoComponent\s*\((\{[\s\S]*?\})\)/m.exec(source)
|
|
95
|
-
if (decoratorMatch) {
|
|
96
|
-
return parseMetadataLiteral(decoratorMatch[1])
|
|
97
|
-
}
|
|
98
|
-
|
|
99
134
|
return undefined
|
|
100
135
|
}
|
|
101
136
|
|
|
102
137
|
export async function discoverComponentProjectComponents(projectRoot) {
|
|
103
138
|
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
139
|
const discovered = []
|
|
140
|
+
const sourceFiles = await collectComponentSourceFiles(componentsDir)
|
|
113
141
|
|
|
114
|
-
for (const
|
|
115
|
-
if (!entry.endsWith('.tsx')) {
|
|
116
|
-
continue
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const absolutePath = path.join(componentsDir, entry)
|
|
142
|
+
for (const absolutePath of sourceFiles) {
|
|
120
143
|
const source = await fs.readFile(absolutePath, 'utf8')
|
|
121
144
|
const metadata = readMetadataFromSource(source)
|
|
122
145
|
if (!metadata) {
|
|
@@ -13,6 +13,8 @@ export function renderComponentProjectMarkdown(input) {
|
|
|
13
13
|
`name: ${metadata.name}`,
|
|
14
14
|
`description: ${metadata.description}`,
|
|
15
15
|
`sourceFile: ${metadata.sourceFile}`,
|
|
16
|
+
`aspectRatio: ${metadata.aspectRatio}`,
|
|
17
|
+
`sceneType: ${metadata.sceneType ?? ''}`,
|
|
16
18
|
`propsTypeName: ${metadata.propsTypeName ?? ''}`,
|
|
17
19
|
'tags:',
|
|
18
20
|
...tags.map((tag) => ` - ${tag}`),
|
|
@@ -25,6 +27,8 @@ export function renderComponentProjectMarkdown(input) {
|
|
|
25
27
|
'## Metadata',
|
|
26
28
|
'',
|
|
27
29
|
`- Source file: \`${escapeInline(metadata.sourceFile)}\``,
|
|
30
|
+
`- Aspect ratio: \`${escapeInline(metadata.aspectRatio)}\``,
|
|
31
|
+
`- Scene type: \`${escapeInline(metadata.sceneType ?? 'unknown')}\``,
|
|
28
32
|
`- Props type: \`${escapeInline(metadata.propsTypeName ?? 'unknown')}\``,
|
|
29
33
|
`- Tags: ${tags.map((tag) => `\`${escapeInline(tag)}\``).join(', ')}`,
|
|
30
34
|
'',
|
package/scripts/runtime-root.mjs
CHANGED
|
@@ -1,14 +1,43 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
+
import fs from 'node:fs'
|
|
2
3
|
import process from 'node:process'
|
|
3
4
|
|
|
5
|
+
function hasComponentProjectSourceDir(directory) {
|
|
6
|
+
return fs.existsSync(path.join(directory, 'src', 'components'))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function findComponentProjectInstallRoot(startDirectory) {
|
|
10
|
+
let currentDirectory = path.resolve(startDirectory)
|
|
11
|
+
|
|
12
|
+
for (;;) {
|
|
13
|
+
if (hasComponentProjectSourceDir(currentDirectory)) {
|
|
14
|
+
return currentDirectory
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const parentDirectory = path.dirname(currentDirectory)
|
|
18
|
+
if (parentDirectory === currentDirectory) {
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
currentDirectory = parentDirectory
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
4
26
|
export function resolveComponentProjectInstallRoot() {
|
|
5
27
|
const candidates = [
|
|
28
|
+
process.cwd(),
|
|
6
29
|
process.env.INIT_CWD,
|
|
7
30
|
process.env.npm_config_local_prefix,
|
|
8
|
-
process.cwd(),
|
|
9
31
|
].filter((value) => Boolean(value && String(value).trim()))
|
|
10
32
|
|
|
11
|
-
|
|
33
|
+
for (const candidate of candidates) {
|
|
34
|
+
const resolved = findComponentProjectInstallRoot(candidate)
|
|
35
|
+
if (resolved) {
|
|
36
|
+
return resolved
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return path.resolve(process.cwd())
|
|
12
41
|
}
|
|
13
42
|
|
|
14
43
|
export function resolveComponentProjectGeneratedDocsDir(projectRoot = resolveComponentProjectInstallRoot()) {
|
package/scripts/schemas.mjs
CHANGED
|
@@ -4,6 +4,8 @@ export const ComponentProjectComponentMetadataSchema = z.object({
|
|
|
4
4
|
name: z.string().min(1),
|
|
5
5
|
description: z.string().min(1),
|
|
6
6
|
sourceFile: z.string().min(1),
|
|
7
|
+
aspectRatio: z.enum(['9:16', '16:9']),
|
|
8
|
+
sceneType: z.enum(['intro', 'scene', 'outro']).optional(),
|
|
7
9
|
tags: z.array(z.string().min(1)).default([]),
|
|
8
10
|
propsTypeName: z.string().min(1).optional(),
|
|
9
11
|
})
|
|
@@ -0,0 +1,70 @@
|
|
|
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 { discoverComponentProjectComponents } from './index'
|
|
8
|
+
|
|
9
|
+
test('discovers decorated components recursively', async () => {
|
|
10
|
+
const projectRoot = await fs.mkdtemp(path.join(tmpdir(), 'component-project-helper-discovery-'))
|
|
11
|
+
const componentsDir = path.join(projectRoot, 'src', 'components', 'nested')
|
|
12
|
+
await fs.mkdir(componentsDir, { recursive: true })
|
|
13
|
+
|
|
14
|
+
await fs.writeFile(
|
|
15
|
+
path.join(projectRoot, 'src', 'components', 'SceneFrame.tsx'),
|
|
16
|
+
`
|
|
17
|
+
@VideoComponent({
|
|
18
|
+
name: 'SceneFrame',
|
|
19
|
+
description: 'Wraps the scene content in a consistent frame',
|
|
20
|
+
sourceFile: 'src/components/SceneFrame.tsx',
|
|
21
|
+
aspectRatio: '16:9',
|
|
22
|
+
sceneType: 'scene',
|
|
23
|
+
tags: ['layout', 'frame'],
|
|
24
|
+
propsTypeName: 'SceneFrameProps',
|
|
25
|
+
})
|
|
26
|
+
export const SceneFrame = () => null
|
|
27
|
+
`,
|
|
28
|
+
'utf8',
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
await fs.writeFile(
|
|
32
|
+
path.join(componentsDir, 'SceneTitle.tsx'),
|
|
33
|
+
`
|
|
34
|
+
@VideoComponent({
|
|
35
|
+
name: 'SceneTitle',
|
|
36
|
+
description: 'Renders the title block for scene hero content',
|
|
37
|
+
sourceFile: 'src/components/nested/SceneTitle.tsx',
|
|
38
|
+
aspectRatio: '9:16',
|
|
39
|
+
sceneType: 'intro',
|
|
40
|
+
tags: ['title', 'hero'],
|
|
41
|
+
propsTypeName: 'SceneTitleProps',
|
|
42
|
+
})
|
|
43
|
+
export const SceneTitle = () => null
|
|
44
|
+
`,
|
|
45
|
+
'utf8',
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const discovered = await discoverComponentProjectComponents(projectRoot)
|
|
49
|
+
|
|
50
|
+
expect(discovered).toEqual([
|
|
51
|
+
{
|
|
52
|
+
name: 'SceneFrame',
|
|
53
|
+
description: 'Wraps the scene content in a consistent frame',
|
|
54
|
+
sourceFile: 'src/components/SceneFrame.tsx',
|
|
55
|
+
aspectRatio: '16:9',
|
|
56
|
+
sceneType: 'scene',
|
|
57
|
+
tags: ['layout', 'frame'],
|
|
58
|
+
propsTypeName: 'SceneFrameProps',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'SceneTitle',
|
|
62
|
+
description: 'Renders the title block for scene hero content',
|
|
63
|
+
sourceFile: 'src/components/nested/SceneTitle.tsx',
|
|
64
|
+
aspectRatio: '9:16',
|
|
65
|
+
sceneType: 'intro',
|
|
66
|
+
tags: ['title', 'hero'],
|
|
67
|
+
propsTypeName: 'SceneTitleProps',
|
|
68
|
+
},
|
|
69
|
+
])
|
|
70
|
+
})
|
package/src/discovery/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
|
+
import type { Dirent } from 'node:fs'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
|
|
4
5
|
import {
|
|
@@ -8,6 +9,41 @@ import {
|
|
|
8
9
|
|
|
9
10
|
export type ComponentProjectComponentDiscovery = ComponentProjectComponentMetadata
|
|
10
11
|
|
|
12
|
+
async function collectComponentSourceFiles(directory: string): Promise<string[]> {
|
|
13
|
+
let entries: Dirent[] = []
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
entries = await fs.readdir(directory, { withFileTypes: true })
|
|
17
|
+
} catch {
|
|
18
|
+
return []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const files: string[] = []
|
|
22
|
+
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const entryPath = path.join(directory, entry.name)
|
|
25
|
+
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
files.push(...(await collectComponentSourceFiles(entryPath)))
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!entry.isFile()) {
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (entry.name.endsWith('.d.ts')) {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {
|
|
40
|
+
files.push(entryPath)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return files
|
|
45
|
+
}
|
|
46
|
+
|
|
11
47
|
function extractBalancedObjectLiteral(source: string, startIndex: number): string | undefined {
|
|
12
48
|
let depth = 0
|
|
13
49
|
let inString = false
|
|
@@ -68,6 +104,11 @@ function parseMetadataLiteral(literal: string): ComponentProjectComponentMetadat
|
|
|
68
104
|
}
|
|
69
105
|
|
|
70
106
|
function readMetadataFromSource(source: string): ComponentProjectComponentMetadata | undefined {
|
|
107
|
+
const decoratorMatch = /@VideoComponent\s*\((\{[\s\S]*?\})\)/m.exec(source)
|
|
108
|
+
if (decoratorMatch) {
|
|
109
|
+
return parseMetadataLiteral(decoratorMatch[1])
|
|
110
|
+
}
|
|
111
|
+
|
|
71
112
|
const candidates = [
|
|
72
113
|
/componentMetadata\s*=\s*defineComponentProjectComponentMetadata\s*\(/m,
|
|
73
114
|
/componentMetadata\s*=\s*VideoComponent\s*\(/m,
|
|
@@ -96,11 +137,6 @@ function readMetadataFromSource(source: string): ComponentProjectComponentMetada
|
|
|
96
137
|
}
|
|
97
138
|
}
|
|
98
139
|
|
|
99
|
-
const decoratorMatch = /@VideoComponent\s*\((\{[\s\S]*?\})\)/m.exec(source)
|
|
100
|
-
if (decoratorMatch) {
|
|
101
|
-
return parseMetadataLiteral(decoratorMatch[1])
|
|
102
|
-
}
|
|
103
|
-
|
|
104
140
|
return undefined
|
|
105
141
|
}
|
|
106
142
|
|
|
@@ -108,22 +144,10 @@ export async function discoverComponentProjectComponents(
|
|
|
108
144
|
projectRoot: string,
|
|
109
145
|
): Promise<ComponentProjectComponentDiscovery[]> {
|
|
110
146
|
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
147
|
const discovered: ComponentProjectComponentDiscovery[] = []
|
|
148
|
+
const sourceFiles = await collectComponentSourceFiles(componentsDir)
|
|
120
149
|
|
|
121
|
-
for (const
|
|
122
|
-
if (!entry.endsWith('.tsx')) {
|
|
123
|
-
continue
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const absolutePath = path.join(componentsDir, entry)
|
|
150
|
+
for (const absolutePath of sourceFiles) {
|
|
127
151
|
const source = await fs.readFile(absolutePath, 'utf8')
|
|
128
152
|
const metadata = readMetadataFromSource(source)
|
|
129
153
|
if (!metadata) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export const componentProjectSkillsScriptName = 'update-component-skills' as const
|
|
5
|
+
export const componentProjectSkillsScriptCommand =
|
|
6
|
+
'node ./node_modules/@vibecuting/component-project-helper/scripts/postinstall.mjs' as const
|
|
7
|
+
|
|
8
|
+
async function readPackageJson(projectRoot: string): Promise<Record<string, unknown>> {
|
|
9
|
+
const packageJsonPath = path.join(projectRoot, 'package.json')
|
|
10
|
+
return JSON.parse(await fs.readFile(packageJsonPath, 'utf8'))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function writePackageJson(
|
|
14
|
+
projectRoot: string,
|
|
15
|
+
packageJson: Record<string, unknown>,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const packageJsonPath = path.join(projectRoot, 'package.json')
|
|
18
|
+
await fs.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function ensureComponentProjectSkillsScript(projectRoot: string): Promise<void> {
|
|
22
|
+
const packageJson = await readPackageJson(projectRoot)
|
|
23
|
+
const scripts = (packageJson.scripts as Record<string, string> | undefined) ?? {}
|
|
24
|
+
|
|
25
|
+
if (scripts[componentProjectSkillsScriptName] !== componentProjectSkillsScriptCommand) {
|
|
26
|
+
packageJson.scripts = {
|
|
27
|
+
...scripts,
|
|
28
|
+
[componentProjectSkillsScriptName]: componentProjectSkillsScriptCommand,
|
|
29
|
+
}
|
|
30
|
+
await writePackageJson(projectRoot, packageJson)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function removeComponentProjectSkillsScript(projectRoot: string): Promise<void> {
|
|
35
|
+
const packageJson = await readPackageJson(projectRoot)
|
|
36
|
+
const scripts = (packageJson.scripts as Record<string, string> | undefined) ?? {}
|
|
37
|
+
|
|
38
|
+
if (scripts[componentProjectSkillsScriptName] !== componentProjectSkillsScriptCommand) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const nextScripts = { ...scripts }
|
|
43
|
+
delete nextScripts[componentProjectSkillsScriptName]
|
|
44
|
+
packageJson.scripts = nextScripts
|
|
45
|
+
|
|
46
|
+
await writePackageJson(projectRoot, packageJson)
|
|
47
|
+
}
|
|
@@ -9,34 +9,53 @@ import { runComponentProjectPreuninstall } from './preuninstall'
|
|
|
9
9
|
|
|
10
10
|
async function createFixtureProjectRoot(): Promise<string> {
|
|
11
11
|
const projectRoot = await fs.mkdtemp(path.join(tmpdir(), 'component-project-helper-'))
|
|
12
|
-
await fs.mkdir(path.join(projectRoot, 'src', 'components'), { recursive: true })
|
|
12
|
+
await fs.mkdir(path.join(projectRoot, 'src', 'components', 'nested'), { recursive: true })
|
|
13
|
+
await fs.writeFile(
|
|
14
|
+
path.join(projectRoot, 'package.json'),
|
|
15
|
+
JSON.stringify(
|
|
16
|
+
{
|
|
17
|
+
name: 'component-project-fixture',
|
|
18
|
+
private: true,
|
|
19
|
+
scripts: {
|
|
20
|
+
build: 'echo build',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
null,
|
|
24
|
+
2,
|
|
25
|
+
),
|
|
26
|
+
'utf8',
|
|
27
|
+
)
|
|
13
28
|
|
|
14
29
|
await fs.writeFile(
|
|
15
30
|
path.join(projectRoot, 'src', 'components', 'SceneFrame.tsx'),
|
|
16
|
-
`
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
`
|
|
32
|
+
@VideoComponent({
|
|
33
|
+
name: 'SceneFrame',
|
|
34
|
+
description: 'Wraps the scene content in a consistent frame',
|
|
35
|
+
sourceFile: 'src/components/SceneFrame.tsx',
|
|
36
|
+
aspectRatio: '16:9',
|
|
37
|
+
sceneType: 'scene',
|
|
38
|
+
tags: ['layout', 'frame'],
|
|
39
|
+
propsTypeName: 'SceneFrameProps',
|
|
40
|
+
})
|
|
41
|
+
export const SceneFrame = () => null
|
|
25
42
|
`,
|
|
26
43
|
'utf8',
|
|
27
44
|
)
|
|
28
45
|
|
|
29
46
|
await fs.writeFile(
|
|
30
|
-
path.join(projectRoot, 'src', 'components', 'SceneTitle.tsx'),
|
|
31
|
-
`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
path.join(projectRoot, 'src', 'components', 'nested', 'SceneTitle.tsx'),
|
|
48
|
+
`
|
|
49
|
+
@VideoComponent({
|
|
50
|
+
name: 'SceneTitle',
|
|
51
|
+
description: 'Renders the title block for scene hero content',
|
|
52
|
+
sourceFile: 'src/components/nested/SceneTitle.tsx',
|
|
53
|
+
aspectRatio: '9:16',
|
|
54
|
+
sceneType: 'intro',
|
|
55
|
+
tags: ['title'],
|
|
56
|
+
propsTypeName: 'SceneTitleProps',
|
|
57
|
+
})
|
|
58
|
+
export const SceneTitle = () => null
|
|
40
59
|
`,
|
|
41
60
|
'utf8',
|
|
42
61
|
)
|
|
@@ -66,6 +85,17 @@ test('postinstall generates markdown and preuninstall removes it', async () => {
|
|
|
66
85
|
await expect(fs.stat(path.join(docsDir, 'SceneFrame.md'))).resolves.toBeDefined()
|
|
67
86
|
await expect(fs.stat(path.join(docsDir, 'SceneTitle.md'))).resolves.toBeDefined()
|
|
68
87
|
await expect(fs.stat(manifestPath)).resolves.toBeDefined()
|
|
88
|
+
await expect(await fs.readFile(path.join(docsDir, 'SceneTitle.md'), 'utf8')).toContain(
|
|
89
|
+
'src/components/nested/SceneTitle.tsx',
|
|
90
|
+
)
|
|
91
|
+
await expect(await fs.readFile(path.join(docsDir, 'SceneTitle.md'), 'utf8')).toContain(
|
|
92
|
+
'9:16',
|
|
93
|
+
)
|
|
94
|
+
await expect(
|
|
95
|
+
JSON.parse(await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8')).scripts[
|
|
96
|
+
'update-component-skills'
|
|
97
|
+
],
|
|
98
|
+
).toBe('node ./node_modules/@vibecuting/component-project-helper/scripts/postinstall.mjs')
|
|
69
99
|
|
|
70
100
|
await runComponentProjectPreuninstall(projectRoot)
|
|
71
101
|
|
|
@@ -76,4 +106,9 @@ test('postinstall generates markdown and preuninstall removes it', async () => {
|
|
|
76
106
|
code: 'ENOENT',
|
|
77
107
|
})
|
|
78
108
|
await expect(fs.stat(manifestPath)).rejects.toMatchObject({ code: 'ENOENT' })
|
|
109
|
+
await expect(
|
|
110
|
+
JSON.parse(await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8')).scripts[
|
|
111
|
+
'update-component-skills'
|
|
112
|
+
],
|
|
113
|
+
).toBeUndefined()
|
|
79
114
|
})
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
resolveComponentProjectGeneratedManifestPath,
|
|
11
11
|
resolveComponentProjectInstallRoot,
|
|
12
12
|
} from '../runtime'
|
|
13
|
+
import { ensureComponentProjectSkillsScript } from './package-json'
|
|
13
14
|
|
|
14
15
|
async function ensureDirectory(dir: string): Promise<void> {
|
|
15
16
|
await fs.mkdir(dir, { recursive: true })
|
|
@@ -51,6 +52,7 @@ export async function runComponentProjectPostinstall(
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
await removeStaleGeneratedFiles(manifestPath, files)
|
|
55
|
+
await ensureComponentProjectSkillsScript(projectRoot)
|
|
54
56
|
|
|
55
57
|
const manifest = ComponentProjectGeneratedManifestSchema.parse({
|
|
56
58
|
projectRoot,
|
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
4
4
|
|
|
5
5
|
import { ComponentProjectGeneratedManifestSchema } from '../schemas'
|
|
6
6
|
import { resolveComponentProjectGeneratedManifestPath } from '../runtime'
|
|
7
|
+
import { removeComponentProjectSkillsScript } from './package-json'
|
|
7
8
|
|
|
8
9
|
export async function runComponentProjectPreuninstall(
|
|
9
10
|
projectRoot = process.env.INIT_CWD ??
|
|
@@ -28,6 +29,8 @@ export async function runComponentProjectPreuninstall(
|
|
|
28
29
|
throw error
|
|
29
30
|
}
|
|
30
31
|
}
|
|
32
|
+
|
|
33
|
+
await removeComponentProjectSkillsScript(projectRoot)
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
async function main(): Promise<void> {
|
package/src/runtime/index.ts
CHANGED
|
@@ -1,14 +1,43 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
+
import fs from 'node:fs'
|
|
2
3
|
import process from 'node:process'
|
|
3
4
|
|
|
5
|
+
function hasComponentProjectSourceDir(directory: string): boolean {
|
|
6
|
+
return fs.existsSync(path.join(directory, 'src', 'components'))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function findComponentProjectInstallRoot(startDirectory: string): string | undefined {
|
|
10
|
+
let currentDirectory = path.resolve(startDirectory)
|
|
11
|
+
|
|
12
|
+
for (;;) {
|
|
13
|
+
if (hasComponentProjectSourceDir(currentDirectory)) {
|
|
14
|
+
return currentDirectory
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const parentDirectory = path.dirname(currentDirectory)
|
|
18
|
+
if (parentDirectory === currentDirectory) {
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
currentDirectory = parentDirectory
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
4
26
|
export function resolveComponentProjectInstallRoot(): string {
|
|
5
27
|
const candidates = [
|
|
28
|
+
process.cwd(),
|
|
6
29
|
process.env.INIT_CWD,
|
|
7
30
|
process.env.npm_config_local_prefix,
|
|
8
|
-
process.cwd(),
|
|
9
31
|
].filter((value): value is string => Boolean(value && value.trim()))
|
|
10
32
|
|
|
11
|
-
|
|
33
|
+
for (const candidate of candidates) {
|
|
34
|
+
const resolved = findComponentProjectInstallRoot(candidate)
|
|
35
|
+
if (resolved) {
|
|
36
|
+
return resolved
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return path.resolve(process.cwd())
|
|
12
41
|
}
|
|
13
42
|
|
|
14
43
|
export function resolveComponentProjectGeneratedDocsDir(
|