@vibecuting/component-project-helper 0.1.6 → 0.1.8

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