@vibecuting/component-project-helper 0.1.16 → 0.1.18
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 -6
- package/bin/component-project-helper.mjs +3 -3
- package/package.json +9 -6
- package/scripts/package-json.mjs +1 -1
- package/scripts/update-component-meta.mjs +60 -0
- package/scripts/update-component-skills.mjs +63 -0
- package/src/decorators/index.ts +95 -17
- package/src/discovery/index.test.ts +86 -159
- package/src/discovery/index.ts +664 -102
- package/src/index.ts +52 -10
- package/src/meta/generate-video-resource-meta.ts +198 -0
- package/src/meta/update-component-meta.test.ts +64 -0
- package/src/meta/update-component-meta.ts +47 -0
- package/src/meta/update-component-skills.test.ts +66 -0
- package/src/meta/update-component-skills.ts +306 -0
- package/src/resources/resource-descriptors.ts +47 -0
- package/src/resources/video-resource-descriptors.json +17 -0
- package/src/resources/video-resource.ts +143 -0
- package/src/schemas/index.ts +24 -2
- package/scripts/postinstall.mjs +0 -112
- package/scripts/preuninstall.mjs +0 -45
- package/scripts/render-markdown.mjs +0 -36
- package/src/lifecycle/package-json.ts +0 -63
- package/src/lifecycle/postinstall.test.ts +0 -114
- package/src/lifecycle/postinstall.ts +0 -127
- package/src/lifecycle/preuninstall.ts +0 -54
- package/src/markdown/index.test.ts +0 -20
- package/src/markdown/index.ts +0 -43
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import ts from 'typescript'
|
|
4
|
+
|
|
5
|
+
import { evaluateStaticExpression } from '../discovery/index.ts'
|
|
6
|
+
import {
|
|
7
|
+
resolveComponentProjectGeneratedDocsDir,
|
|
8
|
+
resolveComponentProjectGeneratedManifestPath,
|
|
9
|
+
} from '../runtime/index.ts'
|
|
10
|
+
|
|
11
|
+
type VideoResourceMetaRecord = {
|
|
12
|
+
packageName: string
|
|
13
|
+
exportName: string
|
|
14
|
+
metadata: {
|
|
15
|
+
resourceKind: 'scene' | 'transition' | 'theme'
|
|
16
|
+
name: string
|
|
17
|
+
description: string
|
|
18
|
+
sourceFile: string
|
|
19
|
+
pluginKey: string
|
|
20
|
+
tags: string[]
|
|
21
|
+
aspectRatio?: '9:16' | '16:9'
|
|
22
|
+
sceneType?: 'intro' | 'scene' | 'outro'
|
|
23
|
+
sceneFamily?: string
|
|
24
|
+
themePreset?: string
|
|
25
|
+
slots?: Array<string | {
|
|
26
|
+
name: string
|
|
27
|
+
description?: string
|
|
28
|
+
required?: boolean
|
|
29
|
+
accepts?: 'text' | 'image' | 'video' | 'component' | 'any'
|
|
30
|
+
}>
|
|
31
|
+
propsTypeName?: string
|
|
32
|
+
rootLayout?: 'absolute-fill'
|
|
33
|
+
transitionKind?: 'transition' | 'overlay'
|
|
34
|
+
supportedSceneFamilies?: string[]
|
|
35
|
+
cssVariables?: string[]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type VideoResourceMetaFile = {
|
|
40
|
+
videoResourceMeta?: {
|
|
41
|
+
packageFingerprint?: string
|
|
42
|
+
packages?: Array<{
|
|
43
|
+
packageName: string
|
|
44
|
+
packageVersion: string
|
|
45
|
+
}>
|
|
46
|
+
resources: VideoResourceMetaRecord[]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type GeneratedSkillDoc = {
|
|
51
|
+
filePath: string
|
|
52
|
+
contents: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stringifyFrontmatterList(items: readonly string[]): string {
|
|
56
|
+
return items.map((item) => ` - ${item}`).join('\n')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stringifySlots(
|
|
60
|
+
slots: NonNullable<VideoResourceMetaRecord['metadata']['slots']>,
|
|
61
|
+
): string {
|
|
62
|
+
if (slots.length === 0) {
|
|
63
|
+
return '[]'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return slots
|
|
67
|
+
.map((slot) => {
|
|
68
|
+
if (typeof slot === 'string') {
|
|
69
|
+
return ` - ${slot}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const lines = [
|
|
73
|
+
` - name: ${slot.name}`,
|
|
74
|
+
slot.description ? ` description: ${slot.description}` : undefined,
|
|
75
|
+
slot.required !== undefined ? ` required: ${slot.required}` : undefined,
|
|
76
|
+
slot.accepts ? ` accepts: ${slot.accepts}` : undefined,
|
|
77
|
+
].filter(Boolean)
|
|
78
|
+
|
|
79
|
+
return lines.join('\n')
|
|
80
|
+
})
|
|
81
|
+
.join('\n')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sanitizeFileName(name: string): string {
|
|
85
|
+
return name.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getMetaFilePath(projectRoot: string): string {
|
|
89
|
+
return path.join(projectRoot, '.vibecuting', 'video-resource-meta.generated.ts')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readGeneratedVideoResourceMeta(projectRoot: string): Promise<NonNullable<VideoResourceMetaFile['videoResourceMeta']>> {
|
|
93
|
+
const metaFilePath = getMetaFilePath(projectRoot)
|
|
94
|
+
const sourceText = await fs.readFile(metaFilePath, 'utf8')
|
|
95
|
+
const sourceFile = ts.createSourceFile(metaFilePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
96
|
+
|
|
97
|
+
for (const statement of sourceFile.statements) {
|
|
98
|
+
if (!ts.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
103
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== 'videoResourceMeta') {
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!declaration.initializer) {
|
|
108
|
+
throw new Error(`${metaFilePath}: missing videoResourceMeta initializer`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const videoResourceMeta = evaluateStaticExpression(declaration.initializer, metaFilePath) as VideoResourceMetaFile['videoResourceMeta']
|
|
112
|
+
if (!videoResourceMeta) {
|
|
113
|
+
throw new Error(`${metaFilePath}: missing videoResourceMeta export`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return videoResourceMeta
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new Error(`${metaFilePath}: missing videoResourceMeta export`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderSceneMetadata(record: VideoResourceMetaRecord): string {
|
|
124
|
+
const { metadata } = record
|
|
125
|
+
const slots = metadata.slots ? stringifySlots(metadata.slots) : '[]'
|
|
126
|
+
|
|
127
|
+
return [
|
|
128
|
+
`- resourceKind: ${metadata.resourceKind}`,
|
|
129
|
+
` aspectRatio: ${metadata.aspectRatio ?? ''}`,
|
|
130
|
+
` sceneType: ${metadata.sceneType ?? ''}`,
|
|
131
|
+
` sceneFamily: ${metadata.sceneFamily ?? ''}`,
|
|
132
|
+
` themePreset: ${metadata.themePreset ?? ''}`,
|
|
133
|
+
` rootLayout: ${metadata.rootLayout ?? ''}`,
|
|
134
|
+
` propsTypeName: ${metadata.propsTypeName ?? ''}`,
|
|
135
|
+
` slots:`,
|
|
136
|
+
slots,
|
|
137
|
+
].join('\n')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderTransitionMetadata(record: VideoResourceMetaRecord): string {
|
|
141
|
+
const { metadata } = record
|
|
142
|
+
|
|
143
|
+
return [
|
|
144
|
+
`- resourceKind: ${metadata.resourceKind}`,
|
|
145
|
+
` transitionKind: ${metadata.transitionKind ?? ''}`,
|
|
146
|
+
].join('\n')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function renderThemeMetadata(record: VideoResourceMetaRecord): string {
|
|
150
|
+
const { metadata } = record
|
|
151
|
+
const supportedSceneFamilies = metadata.supportedSceneFamilies ?? []
|
|
152
|
+
const cssVariables = metadata.cssVariables ?? []
|
|
153
|
+
|
|
154
|
+
return [
|
|
155
|
+
`- resourceKind: ${metadata.resourceKind}`,
|
|
156
|
+
` supportedSceneFamilies:`,
|
|
157
|
+
supportedSceneFamilies.length > 0 ? stringifyFrontmatterList(supportedSceneFamilies) : ' []',
|
|
158
|
+
` cssVariables:`,
|
|
159
|
+
cssVariables.length > 0 ? stringifyFrontmatterList(cssVariables) : ' []',
|
|
160
|
+
].join('\n')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildDocContents(record: VideoResourceMetaRecord): string {
|
|
164
|
+
const { metadata, exportName } = record
|
|
165
|
+
const tags = metadata.tags ?? []
|
|
166
|
+
const bodyMetadata =
|
|
167
|
+
metadata.resourceKind === 'scene'
|
|
168
|
+
? renderSceneMetadata(record)
|
|
169
|
+
: metadata.resourceKind === 'transition'
|
|
170
|
+
? renderTransitionMetadata(record)
|
|
171
|
+
: renderThemeMetadata(record)
|
|
172
|
+
|
|
173
|
+
return [
|
|
174
|
+
'---',
|
|
175
|
+
`name: ${metadata.name}`,
|
|
176
|
+
`resourceKind: ${metadata.resourceKind}`,
|
|
177
|
+
`description: ${metadata.description}`,
|
|
178
|
+
`sourceFile: ${metadata.sourceFile}`,
|
|
179
|
+
`pluginKey: ${metadata.pluginKey}`,
|
|
180
|
+
`tags:`,
|
|
181
|
+
tags.length > 0 ? stringifyFrontmatterList(tags) : ' []',
|
|
182
|
+
'---',
|
|
183
|
+
'',
|
|
184
|
+
`# ${exportName}`,
|
|
185
|
+
'',
|
|
186
|
+
metadata.description,
|
|
187
|
+
'',
|
|
188
|
+
'## 元数据',
|
|
189
|
+
'',
|
|
190
|
+
`- 资源类型: \`${metadata.resourceKind}\``,
|
|
191
|
+
`- 源文件: \`${metadata.sourceFile}\``,
|
|
192
|
+
`- 插件键: \`${metadata.pluginKey}\``,
|
|
193
|
+
`- 标签: ${tags.length > 0 ? tags.map((tag) => `\`${tag}\``).join(', ') : '[]'}`,
|
|
194
|
+
'',
|
|
195
|
+
bodyMetadata,
|
|
196
|
+
'',
|
|
197
|
+
].join('\n')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function writeTextFile(filePath: string, contents: string): Promise<void> {
|
|
201
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
202
|
+
const tempPath = `${filePath}.tmp`
|
|
203
|
+
await fs.writeFile(tempPath, `${contents}\n`, 'utf8')
|
|
204
|
+
await fs.rename(tempPath, filePath)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function readOptionalFile(filePath: string): Promise<string | undefined> {
|
|
208
|
+
try {
|
|
209
|
+
return await fs.readFile(filePath, 'utf8')
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
212
|
+
return undefined
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
throw error
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function updateComponentSkills(
|
|
220
|
+
projectRoot: string,
|
|
221
|
+
checkOnly = false,
|
|
222
|
+
): Promise<void> {
|
|
223
|
+
const videoResourceMeta = await readGeneratedVideoResourceMeta(projectRoot)
|
|
224
|
+
const docsDir = resolveComponentProjectGeneratedDocsDir(projectRoot)
|
|
225
|
+
const manifestPath = resolveComponentProjectGeneratedManifestPath(projectRoot)
|
|
226
|
+
const resources = [...videoResourceMeta.resources].sort((left, right) => {
|
|
227
|
+
return (
|
|
228
|
+
left.metadata.resourceKind.localeCompare(right.metadata.resourceKind) ||
|
|
229
|
+
left.metadata.pluginKey.localeCompare(right.metadata.pluginKey) ||
|
|
230
|
+
left.exportName.localeCompare(right.exportName)
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const generatedDocs: GeneratedSkillDoc[] = resources.map((record) => {
|
|
235
|
+
const filePath = path.join(docsDir, `${sanitizeFileName(record.exportName)}.md`)
|
|
236
|
+
return { filePath, contents: buildDocContents(record) }
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const manifestContents = JSON.stringify(
|
|
240
|
+
{
|
|
241
|
+
projectRoot,
|
|
242
|
+
docsDir,
|
|
243
|
+
sourceMetaFile: getMetaFilePath(projectRoot),
|
|
244
|
+
packageFingerprint: videoResourceMeta.packageFingerprint ?? null,
|
|
245
|
+
files: generatedDocs.map((doc) => doc.filePath),
|
|
246
|
+
resources: resources.map((resource) => ({
|
|
247
|
+
packageName: resource.packageName,
|
|
248
|
+
exportName: resource.exportName,
|
|
249
|
+
resourceKind: resource.metadata.resourceKind,
|
|
250
|
+
pluginKey: resource.metadata.pluginKey,
|
|
251
|
+
})),
|
|
252
|
+
},
|
|
253
|
+
null,
|
|
254
|
+
2,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if (checkOnly) {
|
|
258
|
+
const currentManifest = await readOptionalFile(manifestPath)
|
|
259
|
+
if (currentManifest?.trimEnd() !== `${manifestContents}\n`.trimEnd()) {
|
|
260
|
+
throw new Error('Generated component skill manifest is stale. Run pnpm run update-component-skills')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const doc of generatedDocs) {
|
|
264
|
+
const currentDoc = await readOptionalFile(doc.filePath)
|
|
265
|
+
if (currentDoc?.trimEnd() !== doc.contents.trimEnd()) {
|
|
266
|
+
throw new Error(`Generated component skill doc is stale: ${doc.filePath}`)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await writeTextFile(manifestPath, manifestContents)
|
|
274
|
+
await fs.mkdir(docsDir, { recursive: true })
|
|
275
|
+
|
|
276
|
+
const desiredFiles = new Set(generatedDocs.map((doc) => doc.filePath))
|
|
277
|
+
|
|
278
|
+
await Promise.all(
|
|
279
|
+
generatedDocs.map((doc) => writeTextFile(doc.filePath, doc.contents)),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
const existingEntries = await fs.readdir(docsDir, { withFileTypes: true })
|
|
283
|
+
await Promise.all(
|
|
284
|
+
existingEntries
|
|
285
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
286
|
+
.map(async (entry) => {
|
|
287
|
+
const filePath = path.join(docsDir, entry.name)
|
|
288
|
+
if (desiredFiles.has(filePath)) {
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
await fs.unlink(filePath)
|
|
293
|
+
}),
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
298
|
+
const checkOnly = process.argv.includes('--check')
|
|
299
|
+
const cwd = process.cwd()
|
|
300
|
+
|
|
301
|
+
updateComponentSkills(cwd, checkOnly).catch((error: unknown) => {
|
|
302
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
303
|
+
console.error(message)
|
|
304
|
+
process.exitCode = 1
|
|
305
|
+
})
|
|
306
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import type {
|
|
3
|
+
ScenePluginMetadata,
|
|
4
|
+
ThemePluginMetadata,
|
|
5
|
+
TransitionPluginMetadata,
|
|
6
|
+
VideoResourceDescriptor,
|
|
7
|
+
VideoResourceMetadata,
|
|
8
|
+
} from './video-resource.ts'
|
|
9
|
+
import {
|
|
10
|
+
ScenePluginMetadataSchema,
|
|
11
|
+
ThemePluginMetadataSchema,
|
|
12
|
+
TransitionPluginMetadataSchema,
|
|
13
|
+
} from './video-resource.ts'
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url)
|
|
16
|
+
const videoResourceDescriptorDefinitionRows = require('./video-resource-descriptors.json')
|
|
17
|
+
|
|
18
|
+
export const videoResourceDescriptorDefinitions = videoResourceDescriptorDefinitionRows
|
|
19
|
+
|
|
20
|
+
export type VideoResourceDescriptorDefinition = (typeof videoResourceDescriptorDefinitions)[number]
|
|
21
|
+
|
|
22
|
+
export const sceneResourceDescriptor: VideoResourceDescriptor<ScenePluginMetadata> = {
|
|
23
|
+
kind: 'scene',
|
|
24
|
+
annotationName: 'VideoComponent',
|
|
25
|
+
metadataFactoryName: 'defineScenePluginMetadata',
|
|
26
|
+
schema: ScenePluginMetadataSchema,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const transitionResourceDescriptor: VideoResourceDescriptor<TransitionPluginMetadata> = {
|
|
30
|
+
kind: 'transition',
|
|
31
|
+
annotationName: 'TransitionComponent',
|
|
32
|
+
metadataFactoryName: 'defineTransitionPluginMetadata',
|
|
33
|
+
schema: TransitionPluginMetadataSchema,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const themeResourceDescriptor: VideoResourceDescriptor<ThemePluginMetadata> = {
|
|
37
|
+
kind: 'theme',
|
|
38
|
+
annotationName: 'ThemeComponent',
|
|
39
|
+
metadataFactoryName: 'defineThemePluginMetadata',
|
|
40
|
+
schema: ThemePluginMetadataSchema,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const videoResourceDescriptors = [
|
|
44
|
+
sceneResourceDescriptor,
|
|
45
|
+
transitionResourceDescriptor,
|
|
46
|
+
themeResourceDescriptor,
|
|
47
|
+
] as const satisfies readonly VideoResourceDescriptor<VideoResourceMetadata>[]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"kind": "scene",
|
|
4
|
+
"annotationName": "VideoComponent",
|
|
5
|
+
"metadataFactoryName": "defineScenePluginMetadata"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"kind": "transition",
|
|
9
|
+
"annotationName": "TransitionComponent",
|
|
10
|
+
"metadataFactoryName": "defineTransitionPluginMetadata"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"kind": "theme",
|
|
14
|
+
"annotationName": "ThemeComponent",
|
|
15
|
+
"metadataFactoryName": "defineThemePluginMetadata"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import 'reflect-metadata'
|
|
2
|
+
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
export const VideoResourceKindSchema = z.enum(['scene', 'transition', 'theme'])
|
|
6
|
+
|
|
7
|
+
export type VideoResourceKind = z.infer<typeof VideoResourceKindSchema>
|
|
8
|
+
|
|
9
|
+
export const BaseVideoResourceMetadataSchema = z.object({
|
|
10
|
+
resourceKind: VideoResourceKindSchema,
|
|
11
|
+
name: z.string().min(1),
|
|
12
|
+
description: z.string().min(1),
|
|
13
|
+
sourceFile: z.string().min(1),
|
|
14
|
+
pluginKey: z.string().min(1),
|
|
15
|
+
tags: z.array(z.string().min(1)).default([]),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const SceneSlotHintSchema = z.object({
|
|
19
|
+
name: z.string().min(1),
|
|
20
|
+
description: z.string().min(1).optional(),
|
|
21
|
+
required: z.boolean().optional(),
|
|
22
|
+
accepts: z.enum(['text', 'image', 'video', 'component', 'any']).optional(),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export const SceneSlotsSchema = z.union([
|
|
26
|
+
z.array(z.string().min(1)),
|
|
27
|
+
z.array(SceneSlotHintSchema),
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
const SceneFamilySchema = z.enum(['slide', 'canvas-2d', 'game-2d', 'three-3d', 'custom'])
|
|
31
|
+
|
|
32
|
+
export const ScenePluginMetadataSchema = BaseVideoResourceMetadataSchema.extend({
|
|
33
|
+
resourceKind: z.literal('scene'),
|
|
34
|
+
aspectRatio: z.enum(['9:16', '16:9']),
|
|
35
|
+
sceneType: z.enum(['intro', 'scene', 'outro']).optional(),
|
|
36
|
+
sceneFamily: SceneFamilySchema.default('custom'),
|
|
37
|
+
themePreset: z.string().min(1).optional(),
|
|
38
|
+
slots: SceneSlotsSchema.optional(),
|
|
39
|
+
propsTypeName: z.string().min(1).optional(),
|
|
40
|
+
rootLayout: z.literal('absolute-fill').default('absolute-fill'),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export const TransitionPluginMetadataSchema = BaseVideoResourceMetadataSchema.extend({
|
|
44
|
+
resourceKind: z.literal('transition'),
|
|
45
|
+
transitionKind: z.enum(['transition', 'overlay']),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export const ThemePluginMetadataSchema = BaseVideoResourceMetadataSchema.extend({
|
|
49
|
+
resourceKind: z.literal('theme'),
|
|
50
|
+
supportedSceneFamilies: z.array(SceneFamilySchema).default([]),
|
|
51
|
+
cssVariables: z.array(z.string().regex(/^--scene-/)).default([]),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
export const VideoResourceMetadataSchema = z.discriminatedUnion('resourceKind', [
|
|
55
|
+
ScenePluginMetadataSchema,
|
|
56
|
+
TransitionPluginMetadataSchema,
|
|
57
|
+
ThemePluginMetadataSchema,
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
export type ScenePluginMetadata = z.infer<typeof ScenePluginMetadataSchema>
|
|
61
|
+
export type SceneSlotHint = z.infer<typeof SceneSlotHintSchema>
|
|
62
|
+
export type TransitionPluginMetadata = z.infer<typeof TransitionPluginMetadataSchema>
|
|
63
|
+
export type ThemePluginMetadata = z.infer<typeof ThemePluginMetadataSchema>
|
|
64
|
+
export type VideoResourceMetadata = z.infer<typeof VideoResourceMetadataSchema>
|
|
65
|
+
|
|
66
|
+
export const VIDEO_RESOURCE_METADATA_KEY = Symbol.for(
|
|
67
|
+
'@vibecuting/component-project-helper/video-resource-metadata',
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
export type VideoResourceDescriptor<TMetadata extends VideoResourceMetadata> = {
|
|
71
|
+
kind: TMetadata['resourceKind']
|
|
72
|
+
annotationName: string
|
|
73
|
+
metadataFactoryName: string
|
|
74
|
+
schema: z.ZodType<TMetadata>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type VideoResourceAnnotation<TMetadata extends VideoResourceMetadata> = {
|
|
78
|
+
defineMetadata(input: TMetadata): TMetadata
|
|
79
|
+
annotate(metadata: TMetadata): <T extends object>(target: T) => T
|
|
80
|
+
getMetadata(target: unknown): TMetadata | undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createVideoResourceAnnotation<TMetadata extends VideoResourceMetadata>(
|
|
84
|
+
descriptor: VideoResourceDescriptor<TMetadata>,
|
|
85
|
+
): VideoResourceAnnotation<TMetadata> {
|
|
86
|
+
return {
|
|
87
|
+
defineMetadata(input) {
|
|
88
|
+
return descriptor.schema.parse(input)
|
|
89
|
+
},
|
|
90
|
+
annotate(metadata) {
|
|
91
|
+
const normalized = descriptor.schema.parse(metadata)
|
|
92
|
+
|
|
93
|
+
return function annotateTarget<T extends object>(target: T): T {
|
|
94
|
+
Reflect.defineMetadata(VIDEO_RESOURCE_METADATA_KEY, normalized, target)
|
|
95
|
+
return target
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
getMetadata(target) {
|
|
99
|
+
if (!target || (typeof target !== 'object' && typeof target !== 'function')) {
|
|
100
|
+
return undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const metadata = Reflect.getMetadata(VIDEO_RESOURCE_METADATA_KEY, target)
|
|
104
|
+
const parsed = descriptor.schema.safeParse(metadata)
|
|
105
|
+
return parsed.success ? parsed.data : undefined
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getVideoResourceMetadata<TMetadata extends VideoResourceMetadata>(
|
|
111
|
+
target: unknown,
|
|
112
|
+
expectedKind?: TMetadata['resourceKind'],
|
|
113
|
+
): TMetadata | undefined {
|
|
114
|
+
if (!target || (typeof target !== 'object' && typeof target !== 'function')) {
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const metadata = Reflect.getMetadata(VIDEO_RESOURCE_METADATA_KEY, target)
|
|
119
|
+
const parsed = VideoResourceMetadataSchema.safeParse(metadata)
|
|
120
|
+
if (!parsed.success) {
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (expectedKind && parsed.data.resourceKind !== expectedKind) {
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return parsed.data as TMetadata
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function defineScenePluginMetadata(input: ScenePluginMetadata): ScenePluginMetadata {
|
|
132
|
+
return ScenePluginMetadataSchema.parse(input)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function defineTransitionPluginMetadata(
|
|
136
|
+
input: TransitionPluginMetadata,
|
|
137
|
+
): TransitionPluginMetadata {
|
|
138
|
+
return TransitionPluginMetadataSchema.parse(input)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function defineThemePluginMetadata(input: ThemePluginMetadata): ThemePluginMetadata {
|
|
142
|
+
return ThemePluginMetadataSchema.parse(input)
|
|
143
|
+
}
|
package/src/schemas/index.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export {
|
|
4
|
+
BaseVideoResourceMetadataSchema,
|
|
5
|
+
ScenePluginMetadataSchema,
|
|
6
|
+
SceneSlotHintSchema,
|
|
7
|
+
SceneSlotsSchema,
|
|
8
|
+
ThemePluginMetadataSchema,
|
|
9
|
+
TransitionPluginMetadataSchema,
|
|
10
|
+
VideoResourceKindSchema,
|
|
11
|
+
VideoResourceMetadataSchema,
|
|
12
|
+
} from '../resources/video-resource.ts'
|
|
13
|
+
export type {
|
|
14
|
+
ScenePluginMetadata,
|
|
15
|
+
SceneSlotHint,
|
|
16
|
+
ThemePluginMetadata,
|
|
17
|
+
TransitionPluginMetadata,
|
|
18
|
+
VideoResourceKind,
|
|
19
|
+
VideoResourceMetadata,
|
|
20
|
+
} from '../resources/video-resource.ts'
|
|
21
|
+
|
|
22
|
+
const ComponentProjectComponentMetadataInputSchema = z.object({
|
|
4
23
|
name: z.string().min(1),
|
|
5
24
|
description: z.string().min(1),
|
|
6
25
|
sourceFile: z.string().min(1),
|
|
@@ -10,6 +29,9 @@ export const ComponentProjectComponentMetadataSchema = z.object({
|
|
|
10
29
|
propsTypeName: z.string().min(1).optional(),
|
|
11
30
|
})
|
|
12
31
|
|
|
32
|
+
export const ComponentProjectComponentMetadataSchema =
|
|
33
|
+
ComponentProjectComponentMetadataInputSchema
|
|
34
|
+
|
|
13
35
|
export const ComponentProjectGeneratedManifestSchema = z.object({
|
|
14
36
|
projectRoot: z.string().min(1),
|
|
15
37
|
generatedAt: z.string().min(1),
|
|
@@ -18,7 +40,7 @@ export const ComponentProjectGeneratedManifestSchema = z.object({
|
|
|
18
40
|
})
|
|
19
41
|
|
|
20
42
|
export type ComponentProjectComponentMetadata = z.infer<
|
|
21
|
-
typeof
|
|
43
|
+
typeof ComponentProjectComponentMetadataInputSchema
|
|
22
44
|
>
|
|
23
45
|
export type ComponentProjectGeneratedManifest = z.infer<
|
|
24
46
|
typeof ComponentProjectGeneratedManifestSchema
|
package/scripts/postinstall.mjs
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
import { discoverComponentProjectComponents } from './discover-components.mjs'
|
|
5
|
-
import { ensureComponentProjectSkillsScript } from './package-json.mjs'
|
|
6
|
-
import {
|
|
7
|
-
resolveComponentProjectGeneratedDocsDir,
|
|
8
|
-
resolveComponentProjectGeneratedManifestPath,
|
|
9
|
-
resolveComponentProjectInstallRoot,
|
|
10
|
-
} from './runtime-root.mjs'
|
|
11
|
-
import { renderComponentProjectMarkdown } from './render-markdown.mjs'
|
|
12
|
-
import { ComponentProjectGeneratedManifestSchema } from './schemas.mjs'
|
|
13
|
-
|
|
14
|
-
const supportsColor =
|
|
15
|
-
Boolean(process.stdout.isTTY) && process.env.NO_COLOR !== '1' && process.env.CI !== 'true'
|
|
16
|
-
|
|
17
|
-
const color = {
|
|
18
|
-
cyan: (value) => (supportsColor ? `\u001b[36m${value}\u001b[0m` : value),
|
|
19
|
-
green: (value) => (supportsColor ? `\u001b[32m${value}\u001b[0m` : value),
|
|
20
|
-
yellow: (value) => (supportsColor ? `\u001b[33m${value}\u001b[0m` : value),
|
|
21
|
-
dim: (value) => (supportsColor ? `\u001b[2m${value}\u001b[0m` : value),
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function ensureDirectory(dir) {
|
|
25
|
-
await fs.mkdir(dir, { recursive: true })
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function removeStaleGeneratedFiles(manifestPath, nextFiles) {
|
|
29
|
-
try {
|
|
30
|
-
const previous = ComponentProjectGeneratedManifestSchema.parse(
|
|
31
|
-
JSON.parse(await fs.readFile(manifestPath, 'utf8')),
|
|
32
|
-
)
|
|
33
|
-
const staleFiles = previous.files.filter((filePath) => !nextFiles.includes(filePath))
|
|
34
|
-
await Promise.all(staleFiles.map((filePath) => fs.rm(filePath, { force: true })))
|
|
35
|
-
} catch (error) {
|
|
36
|
-
if (error?.code !== 'ENOENT') {
|
|
37
|
-
throw error
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function runComponentProjectPostinstall(projectRoot = resolveComponentProjectInstallRoot()) {
|
|
43
|
-
const docsDir = resolveComponentProjectGeneratedDocsDir(projectRoot)
|
|
44
|
-
const manifestPath = resolveComponentProjectGeneratedManifestPath(projectRoot)
|
|
45
|
-
const components = await discoverComponentProjectComponents(projectRoot)
|
|
46
|
-
|
|
47
|
-
console.log(color.cyan('[component-project-helper] postinstall start'))
|
|
48
|
-
console.log(color.dim(`[component-project-helper] project root: ${projectRoot}`))
|
|
49
|
-
console.log(color.dim(`[component-project-helper] docs dir: ${docsDir}`))
|
|
50
|
-
console.log(color.dim(`[component-project-helper] manifest path: ${manifestPath}`))
|
|
51
|
-
console.log(
|
|
52
|
-
color.green(
|
|
53
|
-
`[component-project-helper] discovered ${components.length} component(s): ${
|
|
54
|
-
components.map((component) => component.name).join(', ') || '(none)'
|
|
55
|
-
}`,
|
|
56
|
-
),
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
await ensureDirectory(docsDir)
|
|
60
|
-
|
|
61
|
-
const files = []
|
|
62
|
-
for (const component of components) {
|
|
63
|
-
const filePath = path.join(docsDir, `${component.name}.md`)
|
|
64
|
-
await fs.writeFile(filePath, renderComponentProjectMarkdown(component), 'utf8')
|
|
65
|
-
files.push(filePath)
|
|
66
|
-
console.log(
|
|
67
|
-
color.green(
|
|
68
|
-
`[component-project-helper] wrote ${path.relative(projectRoot, filePath)} <- ${component.sourceFile}`,
|
|
69
|
-
),
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
console.log(
|
|
74
|
-
color.yellow(
|
|
75
|
-
`[component-project-helper] cleaning stale docs relative to manifest ${path.relative(
|
|
76
|
-
projectRoot,
|
|
77
|
-
manifestPath,
|
|
78
|
-
)}`,
|
|
79
|
-
),
|
|
80
|
-
)
|
|
81
|
-
await removeStaleGeneratedFiles(manifestPath, files)
|
|
82
|
-
await ensureComponentProjectSkillsScript(projectRoot)
|
|
83
|
-
console.log(color.green('[component-project-helper] ensured update-component-skills script'))
|
|
84
|
-
console.log('[component-project-helper] ensured stale docs cleaned')
|
|
85
|
-
|
|
86
|
-
const manifest = ComponentProjectGeneratedManifestSchema.parse({
|
|
87
|
-
projectRoot,
|
|
88
|
-
generatedAt: new Date().toISOString(),
|
|
89
|
-
docsDir,
|
|
90
|
-
files,
|
|
91
|
-
})
|
|
92
|
-
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
|
93
|
-
console.log(
|
|
94
|
-
color.cyan(
|
|
95
|
-
`[component-project-helper] postinstall complete, manifest files: ${files.length}`,
|
|
96
|
-
),
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function isDirectRun() {
|
|
101
|
-
if (!process.argv[1]) {
|
|
102
|
-
return false
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const entryPath = await fs.realpath(process.argv[1])
|
|
106
|
-
const currentPath = await fs.realpath(new URL(import.meta.url).pathname)
|
|
107
|
-
return entryPath === currentPath
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (await isDirectRun()) {
|
|
111
|
-
await runComponentProjectPostinstall()
|
|
112
|
-
}
|