@vibecuting/component-project-helper 0.1.16 → 0.1.17
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/bin/component-project-helper.mjs +3 -3
- package/package.json +4 -3
- package/scripts/package-json.mjs +1 -1
- package/scripts/update-component-meta.mjs +60 -0
- package/src/decorators/index.ts +95 -17
- package/src/discovery/index.test.ts +86 -159
- package/src/discovery/index.ts +646 -102
- package/src/index.ts +49 -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/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
package/src/index.ts
CHANGED
|
@@ -1,26 +1,65 @@
|
|
|
1
|
+
export {
|
|
2
|
+
VIDEO_RESOURCE_METADATA_KEY,
|
|
3
|
+
BaseVideoResourceMetadataSchema,
|
|
4
|
+
ScenePluginMetadataSchema,
|
|
5
|
+
SceneSlotsSchema,
|
|
6
|
+
SceneSlotHintSchema,
|
|
7
|
+
ThemePluginMetadataSchema,
|
|
8
|
+
TransitionPluginMetadataSchema,
|
|
9
|
+
VideoResourceKindSchema,
|
|
10
|
+
VideoResourceMetadataSchema,
|
|
11
|
+
createVideoResourceAnnotation,
|
|
12
|
+
getVideoResourceMetadata,
|
|
13
|
+
type ScenePluginMetadata,
|
|
14
|
+
type SceneSlotHint,
|
|
15
|
+
type ThemePluginMetadata,
|
|
16
|
+
type TransitionPluginMetadata,
|
|
17
|
+
type VideoResourceAnnotation,
|
|
18
|
+
type VideoResourceDescriptor,
|
|
19
|
+
type VideoResourceKind,
|
|
20
|
+
type VideoResourceMetadata,
|
|
21
|
+
} from './resources/video-resource'
|
|
22
|
+
export {
|
|
23
|
+
sceneResourceDescriptor,
|
|
24
|
+
themeResourceDescriptor,
|
|
25
|
+
transitionResourceDescriptor,
|
|
26
|
+
videoResourceDescriptorDefinitions,
|
|
27
|
+
videoResourceDescriptors,
|
|
28
|
+
} from './resources/resource-descriptors'
|
|
29
|
+
export {
|
|
30
|
+
discoverComponentProjectComponents,
|
|
31
|
+
discoverVideoResources,
|
|
32
|
+
type DiscoveredVideoResource,
|
|
33
|
+
} from './discovery'
|
|
34
|
+
export {
|
|
35
|
+
generateVideoResourceMeta,
|
|
36
|
+
writeVideoResourceMeta,
|
|
37
|
+
} from './meta/generate-video-resource-meta'
|
|
38
|
+
export {
|
|
39
|
+
updateComponentMeta,
|
|
40
|
+
} from './meta/update-component-meta'
|
|
1
41
|
export {
|
|
2
42
|
VideoComponent,
|
|
3
43
|
defineComponentProjectComponentMetadata,
|
|
44
|
+
defineScenePluginMetadata,
|
|
45
|
+
defineThemePluginMetadata,
|
|
46
|
+
defineTransitionPluginMetadata,
|
|
47
|
+
getScenePluginMetadata,
|
|
4
48
|
getComponentProjectComponentMetadata,
|
|
49
|
+
getThemePluginMetadata,
|
|
50
|
+
getTransitionPluginMetadata,
|
|
51
|
+
ThemeComponent,
|
|
52
|
+
TransitionComponent,
|
|
5
53
|
} from './decorators'
|
|
6
54
|
export {
|
|
55
|
+
type ComponentProjectComponentMetadata,
|
|
7
56
|
ComponentProjectComponentMetadataSchema,
|
|
8
57
|
ComponentProjectGeneratedManifestSchema,
|
|
9
58
|
} from './schemas'
|
|
10
|
-
export {
|
|
11
|
-
discoverComponentProjectComponents,
|
|
12
|
-
type ComponentProjectComponentDiscovery,
|
|
13
|
-
} from './discovery'
|
|
14
|
-
export {
|
|
15
|
-
renderComponentProjectMarkdown,
|
|
16
|
-
type ComponentProjectMarkdownDocument,
|
|
17
|
-
} from './markdown'
|
|
18
59
|
export {
|
|
19
60
|
resolveComponentProjectGeneratedDocsDir,
|
|
20
61
|
resolveComponentProjectGeneratedManifestPath,
|
|
21
62
|
resolveComponentProjectInstallRoot,
|
|
22
63
|
} from './runtime'
|
|
23
|
-
export { runComponentProjectPostinstall } from './lifecycle/postinstall'
|
|
24
|
-
export { runComponentProjectPreuninstall } from './lifecycle/preuninstall'
|
|
25
64
|
|
|
26
65
|
export const componentProjectHelperPackageName = '@vibecuting/component-project-helper' as const
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
import type { DiscoveredVideoResource } from '../discovery/index.ts'
|
|
6
|
+
|
|
7
|
+
type ResourceEntry = {
|
|
8
|
+
packageName: string
|
|
9
|
+
packageVersion: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type GeneratedResourceRecord = {
|
|
13
|
+
packageName: string
|
|
14
|
+
exportName: string
|
|
15
|
+
metadata: DiscoveredVideoResource['metadata']
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type GeneratedModuleInput = {
|
|
19
|
+
projectRoot: string
|
|
20
|
+
resources: DiscoveredVideoResource[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function groupResourcesByPackage(resources: DiscoveredVideoResource[]): Map<string, DiscoveredVideoResource[]> {
|
|
24
|
+
const result = new Map<string, DiscoveredVideoResource[]>()
|
|
25
|
+
|
|
26
|
+
for (const resource of resources) {
|
|
27
|
+
const items = result.get(resource.packageName) ?? []
|
|
28
|
+
items.push(resource)
|
|
29
|
+
result.set(resource.packageName, items)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const [packageName, items] of result.entries()) {
|
|
33
|
+
result.set(
|
|
34
|
+
packageName,
|
|
35
|
+
items.sort((left, right) => left.exportName.localeCompare(right.exportName)),
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stableJson(value: unknown): string {
|
|
43
|
+
return JSON.stringify(
|
|
44
|
+
value,
|
|
45
|
+
(_key, nestedValue) => {
|
|
46
|
+
if (nestedValue && typeof nestedValue === 'object' && !Array.isArray(nestedValue)) {
|
|
47
|
+
return Object.fromEntries(
|
|
48
|
+
Object.entries(nestedValue as Record<string, unknown>).sort(([left], [right]) =>
|
|
49
|
+
left.localeCompare(right),
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return nestedValue
|
|
55
|
+
},
|
|
56
|
+
2,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createFingerprint(resources: DiscoveredVideoResource[]): string {
|
|
61
|
+
const hash = crypto.createHash('sha256')
|
|
62
|
+
hash.update(
|
|
63
|
+
stableJson(
|
|
64
|
+
resources.map((resource) => ({
|
|
65
|
+
packageName: resource.packageName,
|
|
66
|
+
packageVersion: resource.packageVersion,
|
|
67
|
+
exportName: resource.exportName,
|
|
68
|
+
annotationName: resource.annotationName,
|
|
69
|
+
metadata: resource.metadata,
|
|
70
|
+
sourceDigest: resource.sourceDigest,
|
|
71
|
+
})),
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
return `sha256:${hash.digest('hex')}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildImportSpecifier(projectRoot: string, resource: DiscoveredVideoResource): string {
|
|
78
|
+
if (resource.packageRoot !== projectRoot) {
|
|
79
|
+
return resource.packageName
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const relativePath = path
|
|
83
|
+
.relative(path.join(projectRoot, '.vibecuting'), path.join(projectRoot, resource.sourceFile))
|
|
84
|
+
.replaceAll(path.sep, '/')
|
|
85
|
+
.replace(/\.(tsx?|jsx?|mjs|cjs)$/, '')
|
|
86
|
+
|
|
87
|
+
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildImports(projectRoot: string, resources: DiscoveredVideoResource[]): string {
|
|
91
|
+
const grouped = new Map<string, DiscoveredVideoResource[]>()
|
|
92
|
+
|
|
93
|
+
for (const resource of resources) {
|
|
94
|
+
const specifier = buildImportSpecifier(projectRoot, resource)
|
|
95
|
+
const items = grouped.get(specifier) ?? []
|
|
96
|
+
items.push(resource)
|
|
97
|
+
grouped.set(specifier, items)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Array.from(grouped.entries())
|
|
101
|
+
.map(([specifier, packageResources]) => {
|
|
102
|
+
const names = packageResources.map((resource) => resource.exportName).join(', ')
|
|
103
|
+
return `import { ${names} } from '${specifier}'`
|
|
104
|
+
})
|
|
105
|
+
.join('\n')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildRegistryArray(
|
|
109
|
+
name: string,
|
|
110
|
+
kind: DiscoveredVideoResource['metadata']['resourceKind'],
|
|
111
|
+
resources: DiscoveredVideoResource[],
|
|
112
|
+
): string {
|
|
113
|
+
const imports = resources
|
|
114
|
+
.filter((resource) => resource.metadata.resourceKind === kind)
|
|
115
|
+
.map((resource) => resource.exportName)
|
|
116
|
+
|
|
117
|
+
const factoryName =
|
|
118
|
+
kind === 'scene'
|
|
119
|
+
? 'createScenePluginRegistry'
|
|
120
|
+
: kind === 'transition'
|
|
121
|
+
? 'createTransitionPluginRegistry'
|
|
122
|
+
: 'createSceneThemeRegistry'
|
|
123
|
+
|
|
124
|
+
return `export const ${name} = ${factoryName}([${imports.join(', ')}])`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildMetadataRecords(resources: DiscoveredVideoResource[]): GeneratedResourceRecord[] {
|
|
128
|
+
return resources.map((resource) => ({
|
|
129
|
+
packageName: resource.packageName,
|
|
130
|
+
exportName: resource.exportName,
|
|
131
|
+
metadata: resource.metadata,
|
|
132
|
+
}))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function generateVideoResourceMeta({
|
|
136
|
+
projectRoot,
|
|
137
|
+
resources,
|
|
138
|
+
}: GeneratedModuleInput): Promise<{ filePath: string; contents: string }> {
|
|
139
|
+
const sortedResources = [...resources].sort((left, right) => {
|
|
140
|
+
return (
|
|
141
|
+
left.packageName.localeCompare(right.packageName) ||
|
|
142
|
+
left.metadata.resourceKind.localeCompare(right.metadata.resourceKind) ||
|
|
143
|
+
left.metadata.pluginKey.localeCompare(right.metadata.pluginKey) ||
|
|
144
|
+
left.exportName.localeCompare(right.exportName)
|
|
145
|
+
)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const filePath = path.join(projectRoot, '.vibecuting', 'video-resource-meta.generated.ts')
|
|
149
|
+
const fingerprint = createFingerprint(sortedResources)
|
|
150
|
+
const packageEntries: ResourceEntry[] = []
|
|
151
|
+
const seenPackages = new Set<string>()
|
|
152
|
+
|
|
153
|
+
for (const resource of sortedResources) {
|
|
154
|
+
if (seenPackages.has(resource.packageName)) {
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
seenPackages.add(resource.packageName)
|
|
159
|
+
packageEntries.push({
|
|
160
|
+
packageName: resource.packageName,
|
|
161
|
+
packageVersion: resource.packageVersion,
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const contents = [
|
|
166
|
+
'// Generated by update-component-meta. Do not edit.',
|
|
167
|
+
'',
|
|
168
|
+
buildImports(projectRoot, sortedResources),
|
|
169
|
+
"import { createScenePluginRegistry, createSceneThemeRegistry, createTransitionPluginRegistry } from '@vibecuting/video-project-core'",
|
|
170
|
+
'',
|
|
171
|
+
`export const videoResourceMeta = ${stableJson({
|
|
172
|
+
schemaVersion: 1,
|
|
173
|
+
packageFingerprint: fingerprint,
|
|
174
|
+
packages: packageEntries,
|
|
175
|
+
resources: buildMetadataRecords(sortedResources),
|
|
176
|
+
})} as const`,
|
|
177
|
+
'',
|
|
178
|
+
buildRegistryArray('installedScenePluginRegistry', 'scene', sortedResources),
|
|
179
|
+
buildRegistryArray('installedTransitionPluginRegistry', 'transition', sortedResources),
|
|
180
|
+
buildRegistryArray('installedSceneThemeRegistry', 'theme', sortedResources),
|
|
181
|
+
'',
|
|
182
|
+
]
|
|
183
|
+
.filter(Boolean)
|
|
184
|
+
.join('\n')
|
|
185
|
+
|
|
186
|
+
return { filePath, contents }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function writeVideoResourceMeta(
|
|
190
|
+
input: GeneratedModuleInput,
|
|
191
|
+
): Promise<{ filePath: string; contents: string }> {
|
|
192
|
+
const generated = await generateVideoResourceMeta(input)
|
|
193
|
+
await fs.mkdir(path.dirname(generated.filePath), { recursive: true })
|
|
194
|
+
const tempPath = `${generated.filePath}.tmp`
|
|
195
|
+
await fs.writeFile(tempPath, `${generated.contents}\n`, 'utf8')
|
|
196
|
+
await fs.rename(tempPath, generated.filePath)
|
|
197
|
+
return generated
|
|
198
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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 { updateComponentMeta } from './update-component-meta'
|
|
8
|
+
|
|
9
|
+
test('writes and checks the unified video resource metadata file', async () => {
|
|
10
|
+
const projectRoot = await fs.mkdtemp(path.join(tmpdir(), 'component-project-helper-meta-'))
|
|
11
|
+
const packageRoot = path.join(projectRoot, 'node_modules', '@vibecuting', 'demo-pack')
|
|
12
|
+
const srcRoot = path.join(packageRoot, 'src')
|
|
13
|
+
|
|
14
|
+
await fs.mkdir(srcRoot, { recursive: true })
|
|
15
|
+
await fs.writeFile(
|
|
16
|
+
path.join(packageRoot, 'package.json'),
|
|
17
|
+
JSON.stringify({
|
|
18
|
+
name: '@vibecuting/demo-pack',
|
|
19
|
+
version: '0.1.0',
|
|
20
|
+
main: './src/index.ts',
|
|
21
|
+
types: './src/index.ts',
|
|
22
|
+
}),
|
|
23
|
+
'utf8',
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
await fs.writeFile(
|
|
27
|
+
path.join(srcRoot, 'index.ts'),
|
|
28
|
+
`
|
|
29
|
+
export const sceneMetadata = defineScenePluginMetadata({
|
|
30
|
+
resourceKind: 'scene',
|
|
31
|
+
name: 'DemoScene',
|
|
32
|
+
description: 'Demo scene resource',
|
|
33
|
+
sourceFile: 'src/index.ts',
|
|
34
|
+
pluginKey: 'demo.scene',
|
|
35
|
+
tags: ['demo'],
|
|
36
|
+
aspectRatio: '16:9',
|
|
37
|
+
sceneType: 'scene',
|
|
38
|
+
sceneFamily: 'custom',
|
|
39
|
+
rootLayout: 'absolute-fill',
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export const DemoScene = VideoComponent(sceneMetadata)(
|
|
43
|
+
defineSceneComponent({
|
|
44
|
+
family: 'custom',
|
|
45
|
+
propsSchema: null,
|
|
46
|
+
component: function DemoScene() {
|
|
47
|
+
return null
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
`,
|
|
52
|
+
'utf8',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
await updateComponentMeta(projectRoot)
|
|
56
|
+
|
|
57
|
+
const generatedFilePath = path.join(projectRoot, '.vibecuting', 'video-resource-meta.generated.ts')
|
|
58
|
+
const generatedContents = await fs.readFile(generatedFilePath, 'utf8')
|
|
59
|
+
|
|
60
|
+
expect(generatedContents).toContain('installedScenePluginRegistry')
|
|
61
|
+
expect(generatedContents).toContain('DemoScene')
|
|
62
|
+
|
|
63
|
+
await expect(updateComponentMeta(projectRoot, true)).resolves.toBeUndefined()
|
|
64
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { discoverVideoResources } from '../discovery/index.ts'
|
|
5
|
+
import { generateVideoResourceMeta } from './generate-video-resource-meta.ts'
|
|
6
|
+
|
|
7
|
+
export async function updateComponentMeta(projectRoot: string, checkOnly = false): Promise<void> {
|
|
8
|
+
const resources = await discoverVideoResources(projectRoot)
|
|
9
|
+
const generated = await generateVideoResourceMeta({ projectRoot, resources })
|
|
10
|
+
const currentContents = await readOptionalFile(generated.filePath)
|
|
11
|
+
|
|
12
|
+
if (checkOnly) {
|
|
13
|
+
if (currentContents?.trimEnd() !== generated.contents.trimEnd()) {
|
|
14
|
+
throw new Error('Generated video resource metadata is stale. Run pnpm run update-component-meta')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await fs.mkdir(path.dirname(generated.filePath), { recursive: true })
|
|
21
|
+
const tempFilePath = `${generated.filePath}.tmp`
|
|
22
|
+
await fs.writeFile(tempFilePath, `${generated.contents}\n`, 'utf8')
|
|
23
|
+
await fs.rename(tempFilePath, generated.filePath)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function readOptionalFile(filePath: string): Promise<string | undefined> {
|
|
27
|
+
try {
|
|
28
|
+
return await fs.readFile(filePath, 'utf8')
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
39
|
+
const checkOnly = process.argv.includes('--check')
|
|
40
|
+
const cwd = process.cwd()
|
|
41
|
+
|
|
42
|
+
updateComponentMeta(cwd, checkOnly).catch((error: unknown) => {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
44
|
+
console.error(message)
|
|
45
|
+
process.exitCode = 1
|
|
46
|
+
})
|
|
47
|
+
}
|
|
@@ -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
|