@vibecuting/component-project-helper 0.1.15 → 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.
@@ -5,10 +5,10 @@ import path from 'node:path'
5
5
  import { fileURLToPath } from 'node:url'
6
6
 
7
7
  const binDir = path.dirname(fileURLToPath(import.meta.url))
8
- const scriptName = process.argv[2] === 'preuninstall' ? 'preuninstall.mjs' : 'postinstall.mjs'
9
- const scriptPath = path.resolve(binDir, '../scripts', scriptName)
8
+ const checkOnly = process.argv.includes('--check')
9
+ const scriptPath = path.resolve(binDir, '../scripts', 'update-component-meta.mjs')
10
10
 
11
- const result = spawnSync(process.execPath, [scriptPath], {
11
+ const result = spawnSync(process.execPath, checkOnly ? [scriptPath, '--check'] : [scriptPath], {
12
12
  stdio: 'inherit',
13
13
  env: process.env,
14
14
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecuting/component-project-helper",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "type": "module",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "main": "./src/index.ts",
@@ -20,8 +20,8 @@
20
20
  "access": "public"
21
21
  },
22
22
  "scripts": {
23
- "postinstall": "node ./scripts/postinstall.mjs",
24
- "preuninstall": "node ./scripts/preuninstall.mjs",
23
+ "update-component-meta": "node ./scripts/update-component-meta.mjs",
24
+ "update-component-skills": "pnpm run update-component-meta",
25
25
  "lint": "node ../../../scripts/run-eslint.mjs .",
26
26
  "typecheck": "node ../../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
27
27
  "test": "node ../../../node_modules/vitest/vitest.mjs run"
@@ -29,6 +29,7 @@
29
29
  "dependencies": {
30
30
  "reflect-metadata": "^0.2.2",
31
31
  "typescript": "^5.9.3",
32
+ "tsx": "^4.19.2",
32
33
  "zod": "4.1.12"
33
34
  },
34
35
  "devDependencies": {
@@ -3,7 +3,7 @@ import path from 'node:path'
3
3
 
4
4
  export const componentProjectSkillsScriptName = 'update-component-skills'
5
5
  export const componentProjectSkillsScriptCommand =
6
- 'node ./node_modules/@vibecuting/component-project-helper/scripts/postinstall.mjs'
6
+ 'node ./node_modules/@vibecuting/component-project-helper/scripts/update-component-meta.mjs'
7
7
 
8
8
  async function readPackageJson(projectRoot) {
9
9
  const packageJsonPath = path.join(projectRoot, 'package.json')
@@ -0,0 +1,60 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { createRequire } from "node:module";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+
7
+ const require = createRequire(import.meta.url);
8
+
9
+ function resolveLoaderPath() {
10
+ const candidates = [
11
+ path.resolve(process.cwd(), "node_modules/tsx/dist/loader.mjs"),
12
+ path.resolve(
13
+ process.cwd(),
14
+ "../../../project-templates/video-project/node_modules/tsx/dist/loader.mjs",
15
+ ),
16
+ ];
17
+
18
+ for (const candidate of candidates) {
19
+ if (existsSync(candidate)) {
20
+ return candidate;
21
+ }
22
+ }
23
+
24
+ return require.resolve("tsx");
25
+ }
26
+
27
+ const scriptPath = path.relative(process.cwd(), fileURLToPath(import.meta.url));
28
+
29
+ if (!process.env.COMPONENT_PROJECT_HELPER_UPDATE_COMPONENT_META_LOADER) {
30
+ const loaderPath = resolveLoaderPath();
31
+ const result = spawnSync(
32
+ process.execPath,
33
+ ["--import", loaderPath, scriptPath, ...process.argv.slice(2)],
34
+ {
35
+ cwd: process.cwd(),
36
+ env: {
37
+ ...process.env,
38
+ COMPONENT_PROJECT_HELPER_UPDATE_COMPONENT_META_LOADER: "1",
39
+ },
40
+ stdio: "inherit",
41
+ },
42
+ );
43
+
44
+ if (result.error) {
45
+ throw result.error;
46
+ }
47
+
48
+ process.exit(result.status ?? 1);
49
+ }
50
+
51
+ const helperModule = await import(
52
+ pathToFileURL(
53
+ path.resolve(process.cwd(), "src/meta/update-component-meta.ts"),
54
+ ).href
55
+ );
56
+
57
+ await helperModule.updateComponentMeta(
58
+ process.cwd(),
59
+ process.argv.includes("--check"),
60
+ );
@@ -43,3 +43,26 @@ test('attaches normalized metadata to decorated function components', () => {
43
43
  expect(decorated).toBe(SceneTitle)
44
44
  expect(getComponentProjectComponentMetadata(SceneTitle)).toEqual(metadata)
45
45
  })
46
+
47
+ test('attaches metadata to components with required props', () => {
48
+ type RequiredProps = {
49
+ title: string
50
+ }
51
+
52
+ const metadata = defineComponentProjectComponentMetadata({
53
+ name: 'RequiredPropsScene',
54
+ description: 'Exercises required props support',
55
+ sourceFile: 'src/components/RequiredPropsScene.tsx',
56
+ aspectRatio: '16:9',
57
+ sceneType: 'scene',
58
+ tags: ['required', 'props'],
59
+ propsTypeName: 'RequiredProps',
60
+ })
61
+
62
+ const RequiredPropsScene = (_props: RequiredProps) => null
63
+
64
+ const decorated = VideoComponent(metadata)(RequiredPropsScene)
65
+
66
+ expect(decorated).toBe(RequiredPropsScene)
67
+ expect(getComponentProjectComponentMetadata(RequiredPropsScene)).toEqual(metadata)
68
+ })
@@ -1,40 +1,116 @@
1
- import 'reflect-metadata'
2
-
3
- import type { ComponentType } from 'react'
4
-
1
+ import type {
2
+ ScenePluginMetadata,
3
+ ThemePluginMetadata,
4
+ TransitionPluginMetadata,
5
+ VideoResourceMetadata,
6
+ } from '../resources/video-resource'
7
+ import {
8
+ VIDEO_RESOURCE_METADATA_KEY,
9
+ createVideoResourceAnnotation,
10
+ defineScenePluginMetadata as defineScenePluginMetadataBase,
11
+ defineThemePluginMetadata as defineThemePluginMetadataBase,
12
+ defineTransitionPluginMetadata as defineTransitionPluginMetadataBase,
13
+ getVideoResourceMetadata,
14
+ ScenePluginMetadataSchema,
15
+ type VideoResourceDescriptor,
16
+ } from '../resources/video-resource'
17
+ import {
18
+ sceneResourceDescriptor,
19
+ themeResourceDescriptor,
20
+ transitionResourceDescriptor,
21
+ } from '../resources/resource-descriptors'
5
22
  import {
6
23
  ComponentProjectComponentMetadataSchema,
7
- type ComponentProjectComponentMetadata,
24
+ type ComponentProjectComponentMetadata as LegacyComponentProjectComponentMetadata,
8
25
  } from '../schemas'
9
26
 
10
- export const COMPONENT_PROJECT_METADATA_KEY = Symbol.for(
11
- '@vibecuting/component-project-helper/component-metadata',
12
- )
27
+ export const COMPONENT_PROJECT_METADATA_KEY = VIDEO_RESOURCE_METADATA_KEY
28
+
29
+ export type ComponentProjectComponentMetadata = LegacyComponentProjectComponentMetadata
13
30
 
14
31
  export function defineComponentProjectComponentMetadata(
15
32
  metadata: ComponentProjectComponentMetadata,
16
33
  ): ComponentProjectComponentMetadata {
17
- return ComponentProjectComponentMetadataSchema.parse(metadata)
34
+ return ComponentProjectComponentMetadataSchema.parse(metadata) as ComponentProjectComponentMetadata
18
35
  }
19
36
 
20
- export function VideoComponent(metadata: ComponentProjectComponentMetadata) {
21
- const normalized = defineComponentProjectComponentMetadata(metadata)
37
+ const sceneAnnotation = createVideoResourceAnnotation(sceneResourceDescriptor)
38
+ const transitionAnnotation = createVideoResourceAnnotation(transitionResourceDescriptor)
39
+ const themeAnnotation = createVideoResourceAnnotation(themeResourceDescriptor)
22
40
 
23
- return function componentProjectDecorator<T extends ComponentType<any>>(target: T): T {
24
- Reflect.defineMetadata(COMPONENT_PROJECT_METADATA_KEY, normalized, target)
25
- return target
41
+ function normalizeSceneMetadata(
42
+ metadata: ComponentProjectComponentMetadata | ScenePluginMetadata,
43
+ ): ScenePluginMetadata {
44
+ if ('resourceKind' in metadata) {
45
+ return ScenePluginMetadataSchema.parse(metadata)
26
46
  }
47
+
48
+ return ScenePluginMetadataSchema.parse({
49
+ resourceKind: 'scene',
50
+ name: metadata.name,
51
+ description: metadata.description,
52
+ sourceFile: metadata.sourceFile,
53
+ aspectRatio: metadata.aspectRatio,
54
+ sceneType: metadata.sceneType,
55
+ sceneFamily: 'custom',
56
+ themePreset: undefined,
57
+ slots: undefined,
58
+ propsTypeName: metadata.propsTypeName,
59
+ rootLayout: 'absolute-fill',
60
+ tags: metadata.tags,
61
+ pluginKey: metadata.name,
62
+ })
63
+ }
64
+
65
+ export function VideoComponent(
66
+ metadata: ComponentProjectComponentMetadata | ScenePluginMetadata,
67
+ ) {
68
+ return sceneAnnotation.annotate(normalizeSceneMetadata(metadata))
27
69
  }
70
+ export const TransitionComponent = transitionAnnotation.annotate
71
+ export const ThemeComponent = themeAnnotation.annotate
72
+
73
+ export const defineScenePluginMetadata = defineScenePluginMetadataBase
74
+ export const defineTransitionPluginMetadata = defineTransitionPluginMetadataBase
75
+ export const defineThemePluginMetadata = defineThemePluginMetadataBase
28
76
 
29
77
  export function getComponentProjectComponentMetadata(
30
78
  target: unknown,
31
79
  ): ComponentProjectComponentMetadata | undefined {
32
- if (!target || (typeof target !== 'object' && typeof target !== 'function')) {
80
+ const metadata = getVideoResourceMetadata<ScenePluginMetadata>(target, 'scene')
81
+ if (!metadata) {
33
82
  return undefined
34
83
  }
35
84
 
36
- return Reflect.getMetadata(
37
- COMPONENT_PROJECT_METADATA_KEY,
38
- target,
39
- ) as ComponentProjectComponentMetadata | undefined
85
+ const { resourceKind, pluginKey, rootLayout, sceneFamily, themePreset, slots, ...legacy } =
86
+ metadata
87
+ void resourceKind
88
+ void pluginKey
89
+ void rootLayout
90
+ void sceneFamily
91
+ void themePreset
92
+ void slots
93
+ return legacy as ComponentProjectComponentMetadata
94
+ }
95
+
96
+ export function getScenePluginMetadata(target: unknown): ScenePluginMetadata | undefined {
97
+ return getVideoResourceMetadata<ScenePluginMetadata>(target, 'scene')
98
+ }
99
+
100
+ export function getTransitionPluginMetadata(
101
+ target: unknown,
102
+ ): TransitionPluginMetadata | undefined {
103
+ return getVideoResourceMetadata<TransitionPluginMetadata>(target, 'transition')
104
+ }
105
+
106
+ export function getThemePluginMetadata(target: unknown): ThemePluginMetadata | undefined {
107
+ return getVideoResourceMetadata<ThemePluginMetadata>(target, 'theme')
108
+ }
109
+
110
+ export type {
111
+ ScenePluginMetadata,
112
+ ThemePluginMetadata,
113
+ TransitionPluginMetadata,
114
+ VideoResourceMetadata,
115
+ VideoResourceDescriptor,
40
116
  }
@@ -4,183 +4,110 @@ import { tmpdir } from 'node:os'
4
4
 
5
5
  import { expect, test } from 'vitest'
6
6
 
7
- import { discoverComponentProjectComponents } from './index'
7
+ import { discoverVideoResources } from './index'
8
8
 
9
- test('discovers decorated components recursively', async () => {
9
+ test('discovers scene, transition and theme resources from installed packages', async () => {
10
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 })
11
+ const packageRoot = path.join(projectRoot, 'node_modules', '@vibecuting', 'demo-pack')
12
+ const srcRoot = path.join(packageRoot, 'src')
13
13
 
14
+ await fs.mkdir(srcRoot, { recursive: true })
14
15
  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
- })
71
-
72
- test('discovers decorated components from installed video-project-core', async () => {
73
- const projectRoot = await fs.mkdtemp(path.join(tmpdir(), 'component-project-helper-package-'))
74
- const videoProjectCoreComponentDir = path.join(
75
- projectRoot,
76
- 'node_modules',
77
- '@vibecuting',
78
- 'video-project-core',
79
- 'src',
80
- 'core',
81
- 'intro',
82
- )
83
- await fs.mkdir(videoProjectCoreComponentDir, { recursive: true })
84
-
85
- await fs.writeFile(
86
- path.join(videoProjectCoreComponentDir, 'DefaultIntroBumper.tsx'),
87
- `
88
- @VideoComponent({
89
- name: 'DefaultIntroBumper',
90
- description: 'Default remotion intro bumper',
91
- sourceFile: 'node_modules/@vibecuting/video-project-core/src/core/intro/DefaultIntroBumper.tsx',
92
- aspectRatio: '16:9',
93
- sceneType: 'intro',
94
- tags: ['intro', 'core'],
95
- propsTypeName: 'DefaultIntroBumperProps',
96
- })
97
- export const DefaultIntroBumper = () => null
98
- `,
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
+ }),
99
23
  'utf8',
100
24
  )
101
25
 
102
- const videoProjectHelperComponentDir = path.join(
103
- projectRoot,
104
- 'node_modules',
105
- '@vibecuting',
106
- 'video-project-helper',
107
- 'src',
108
- 'core',
109
- 'scene',
110
- )
111
- await fs.mkdir(videoProjectHelperComponentDir, { recursive: true })
112
-
113
26
  await fs.writeFile(
114
- path.join(videoProjectHelperComponentDir, 'DefaultSceneFrame.tsx'),
27
+ path.join(srcRoot, 'index.ts'),
115
28
  `
116
- @VideoComponent({
117
- name: 'DefaultSceneFrame',
118
- description: 'Default helper scene frame',
119
- sourceFile: 'node_modules/@vibecuting/video-project-helper/src/core/scene/DefaultSceneFrame.tsx',
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'],
120
36
  aspectRatio: '16:9',
121
37
  sceneType: 'scene',
122
- tags: ['scene', 'helper'],
123
- propsTypeName: 'DefaultSceneFrameProps',
38
+ sceneFamily: 'custom',
39
+ rootLayout: 'absolute-fill',
124
40
  })
125
- export const DefaultSceneFrame = () => null
126
- `,
127
- 'utf8',
128
- )
129
41
 
130
- const discovered = await discoverComponentProjectComponents(projectRoot)
131
-
132
- expect(discovered).toEqual([
133
- {
134
- name: 'DefaultIntroBumper',
135
- description: 'Default remotion intro bumper',
136
- sourceFile: 'node_modules/@vibecuting/video-project-core/src/core/intro/DefaultIntroBumper.tsx',
137
- aspectRatio: '16:9',
138
- sceneType: 'intro',
139
- tags: ['intro', 'core'],
140
- propsTypeName: 'DefaultIntroBumperProps',
141
- },
142
- {
143
- name: 'DefaultSceneFrame',
144
- description: 'Default helper scene frame',
145
- sourceFile:
146
- 'node_modules/@vibecuting/video-project-helper/src/core/scene/DefaultSceneFrame.tsx',
147
- aspectRatio: '16:9',
148
- sceneType: 'scene',
149
- tags: ['scene', 'helper'],
150
- propsTypeName: 'DefaultSceneFrameProps',
151
- },
152
- ])
153
- })
154
-
155
- test('ignores test files from installed packages', async () => {
156
- const projectRoot = await fs.mkdtemp(path.join(tmpdir(), 'component-project-helper-tests-'))
157
- const packageTestDir = path.join(
158
- projectRoot,
159
- 'node_modules',
160
- '@vibecuting',
161
- 'video-project-helper',
162
- 'src',
163
- )
164
- await fs.mkdir(packageTestDir, { recursive: true })
42
+ export const transitionMetadata = defineTransitionPluginMetadata({
43
+ resourceKind: 'transition',
44
+ name: 'DemoTransition',
45
+ description: 'Demo transition resource',
46
+ sourceFile: 'src/index.ts',
47
+ pluginKey: 'demo.transition',
48
+ tags: ['demo'],
49
+ transitionKind: 'transition',
50
+ })
165
51
 
166
- await fs.writeFile(
167
- path.join(packageTestDir, 'index.test.ts'),
168
- `
169
- @VideoComponent({
170
- name: 'ShouldBeIgnored',
171
- description: 'This fixture lives in a test file and must not be discovered',
172
- sourceFile: 'node_modules/@vibecuting/video-project-helper/src/index.test.ts',
173
- aspectRatio: '16:9',
174
- sceneType: 'scene',
175
- tags: ['test'],
176
- propsTypeName: 'ShouldBeIgnoredProps',
52
+ export const themeMetadata = defineThemePluginMetadata({
53
+ resourceKind: 'theme',
54
+ name: 'DemoTheme',
55
+ description: 'Demo theme resource',
56
+ sourceFile: 'src/index.ts',
57
+ pluginKey: 'demo.theme',
58
+ tags: ['demo'],
59
+ supportedSceneFamilies: ['custom'],
60
+ cssVariables: ['--scene-bg'],
177
61
  })
178
- export const ShouldBeIgnored = () => null
62
+
63
+ export const DemoScene = VideoComponent(sceneMetadata)(
64
+ defineSceneComponent({
65
+ family: 'custom',
66
+ propsSchema: null,
67
+ component: function DemoScene() {
68
+ return null
69
+ },
70
+ }),
71
+ )
72
+
73
+ export const DemoTransition = TransitionComponent(transitionMetadata)(
74
+ defineTransitionPlugin({
75
+ key: 'demo.transition',
76
+ kind: 'transition',
77
+ metadata: transitionMetadata,
78
+ propsSchema: null,
79
+ renderBoundary: () => null,
80
+ getBoundaryDurationInFrames: () => 1,
81
+ getTimelineOverlapInFrames: () => 1,
82
+ }),
83
+ )
84
+
85
+ export const DemoTheme = ThemeComponent(themeMetadata)(
86
+ defineSceneTheme({
87
+ key: 'demo.theme',
88
+ name: 'Demo theme',
89
+ variables: {
90
+ '--scene-bg': '#000000',
91
+ },
92
+ }),
93
+ )
179
94
  `,
180
95
  'utf8',
181
96
  )
182
97
 
183
- const discovered = await discoverComponentProjectComponents(projectRoot)
98
+ const discovered = await discoverVideoResources(projectRoot)
184
99
 
185
- expect(discovered).toEqual([])
100
+ expect(discovered.map((resource) => resource.exportName)).toEqual([
101
+ 'DemoScene',
102
+ 'DemoTheme',
103
+ 'DemoTransition',
104
+ ])
105
+ expect(discovered.map((resource) => resource.metadata.resourceKind)).toEqual([
106
+ 'scene',
107
+ 'theme',
108
+ 'transition',
109
+ ])
110
+ expect(discovered[0]?.metadata.pluginKey).toBe('demo.scene')
111
+ expect(discovered[1]?.metadata.pluginKey).toBe('demo.theme')
112
+ expect(discovered[2]?.metadata.pluginKey).toBe('demo.transition')
186
113
  })