@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/src/index.ts CHANGED
@@ -1,26 +1,68 @@
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'
41
+ export {
42
+ updateComponentSkills,
43
+ } from './meta/update-component-skills'
1
44
  export {
2
45
  VideoComponent,
3
46
  defineComponentProjectComponentMetadata,
47
+ defineScenePluginMetadata,
48
+ defineThemePluginMetadata,
49
+ defineTransitionPluginMetadata,
50
+ getScenePluginMetadata,
4
51
  getComponentProjectComponentMetadata,
52
+ getThemePluginMetadata,
53
+ getTransitionPluginMetadata,
54
+ ThemeComponent,
55
+ TransitionComponent,
5
56
  } from './decorators'
6
57
  export {
58
+ type ComponentProjectComponentMetadata,
7
59
  ComponentProjectComponentMetadataSchema,
8
60
  ComponentProjectGeneratedManifestSchema,
9
61
  } from './schemas'
10
- export {
11
- discoverComponentProjectComponents,
12
- type ComponentProjectComponentDiscovery,
13
- } from './discovery'
14
- export {
15
- renderComponentProjectMarkdown,
16
- type ComponentProjectMarkdownDocument,
17
- } from './markdown'
18
62
  export {
19
63
  resolveComponentProjectGeneratedDocsDir,
20
64
  resolveComponentProjectGeneratedManifestPath,
21
65
  resolveComponentProjectInstallRoot,
22
66
  } from './runtime'
23
- export { runComponentProjectPostinstall } from './lifecycle/postinstall'
24
- export { runComponentProjectPreuninstall } from './lifecycle/preuninstall'
25
67
 
26
68
  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,66 @@
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 { updateComponentSkills } from './update-component-skills'
8
+
9
+ test('writes and checks the skill docs from generated metadata', async () => {
10
+ const projectRoot = await fs.mkdtemp(path.join(tmpdir(), 'component-project-helper-skills-'))
11
+ const generatedDir = path.join(projectRoot, '.vibecuting')
12
+ const generatedFilePath = path.join(generatedDir, 'video-resource-meta.generated.ts')
13
+
14
+ await fs.mkdir(generatedDir, { recursive: true })
15
+ await fs.writeFile(
16
+ generatedFilePath,
17
+ `
18
+ export const videoResourceMeta = {
19
+ packageFingerprint: 'sha256:demo',
20
+ packages: [{ packageName: '@vibecuting/demo-pack', packageVersion: '1.0.0' }],
21
+ resources: [
22
+ {
23
+ packageName: '@vibecuting/demo-pack',
24
+ exportName: 'DemoScene',
25
+ metadata: {
26
+ resourceKind: 'scene',
27
+ name: 'DemoScene',
28
+ description: 'Demo scene resource',
29
+ sourceFile: 'src/index.ts',
30
+ pluginKey: 'demo.scene',
31
+ tags: ['demo'],
32
+ aspectRatio: '16:9',
33
+ sceneType: 'scene',
34
+ sceneFamily: 'custom',
35
+ rootLayout: 'absolute-fill',
36
+ propsTypeName: 'DemoSceneProps',
37
+ slots: ['title'],
38
+ },
39
+ },
40
+ ],
41
+ } as const
42
+ `,
43
+ 'utf8',
44
+ )
45
+
46
+ await updateComponentSkills(projectRoot)
47
+
48
+ const docsDir = path.join(projectRoot, '.agents', 'skills', 'project-allow-component', 'components')
49
+ const generatedDocPath = path.join(docsDir, 'DemoScene.md')
50
+ const generatedDocContents = await fs.readFile(generatedDocPath, 'utf8')
51
+ const generatedManifestPath = path.join(
52
+ projectRoot,
53
+ '.agents',
54
+ 'skills',
55
+ 'project-allow-component',
56
+ 'component-project-helper.manifest.json',
57
+ )
58
+ const generatedManifestContents = await fs.readFile(generatedManifestPath, 'utf8')
59
+
60
+ expect(generatedDocContents).toContain('Demo scene resource')
61
+ expect(generatedDocContents).toContain('resourceKind: scene')
62
+ expect(generatedManifestContents).toContain('video-resource-meta.generated.ts')
63
+ expect(generatedManifestContents).toContain('DemoScene.md')
64
+
65
+ await expect(updateComponentSkills(projectRoot, true)).resolves.toBeUndefined()
66
+ })