@nuasite/cms 0.1.0
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 +237 -0
- package/dist/src/build-processor.d.ts +20 -0
- package/dist/src/build-processor.d.ts.map +1 -0
- package/dist/src/collection-scanner.d.ts +6 -0
- package/dist/src/collection-scanner.d.ts.map +1 -0
- package/dist/src/component-registry.d.ts +63 -0
- package/dist/src/component-registry.d.ts.map +1 -0
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/dev-middleware.d.ts +20 -0
- package/dist/src/dev-middleware.d.ts.map +1 -0
- package/dist/src/editor/ai.d.ts +60 -0
- package/dist/src/editor/ai.d.ts.map +1 -0
- package/dist/src/editor/api.d.ts +140 -0
- package/dist/src/editor/api.d.ts.map +1 -0
- package/dist/src/editor/color-utils.d.ts +106 -0
- package/dist/src/editor/color-utils.d.ts.map +1 -0
- package/dist/src/editor/components/ai-chat.d.ts +11 -0
- package/dist/src/editor/components/ai-chat.d.ts.map +1 -0
- package/dist/src/editor/components/ai-tooltip.d.ts +12 -0
- package/dist/src/editor/components/ai-tooltip.d.ts.map +1 -0
- package/dist/src/editor/components/attribute-editor.d.ts +5 -0
- package/dist/src/editor/components/attribute-editor.d.ts.map +1 -0
- package/dist/src/editor/components/block-editor.d.ts +12 -0
- package/dist/src/editor/components/block-editor.d.ts.map +1 -0
- package/dist/src/editor/components/collections-browser.d.ts +2 -0
- package/dist/src/editor/components/collections-browser.d.ts.map +1 -0
- package/dist/src/editor/components/color-toolbar.d.ts +12 -0
- package/dist/src/editor/components/color-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/confirm-dialog.d.ts +2 -0
- package/dist/src/editor/components/confirm-dialog.d.ts.map +1 -0
- package/dist/src/editor/components/create-page-modal.d.ts +2 -0
- package/dist/src/editor/components/create-page-modal.d.ts.map +1 -0
- package/dist/src/editor/components/editable-highlights.d.ts +9 -0
- package/dist/src/editor/components/editable-highlights.d.ts.map +1 -0
- package/dist/src/editor/components/error-boundary.d.ts +32 -0
- package/dist/src/editor/components/error-boundary.d.ts.map +1 -0
- package/dist/src/editor/components/fields.d.ts +75 -0
- package/dist/src/editor/components/fields.d.ts.map +1 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts +29 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts.map +1 -0
- package/dist/src/editor/components/highlight-overlay.d.ts +64 -0
- package/dist/src/editor/components/highlight-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/image-overlay.d.ts +12 -0
- package/dist/src/editor/components/image-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts +6 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts +10 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts.map +1 -0
- package/dist/src/editor/components/media-library.d.ts +2 -0
- package/dist/src/editor/components/media-library.d.ts.map +1 -0
- package/dist/src/editor/components/outline.d.ts +21 -0
- package/dist/src/editor/components/outline.d.ts.map +1 -0
- package/dist/src/editor/components/redirect-countdown.d.ts +2 -0
- package/dist/src/editor/components/redirect-countdown.d.ts.map +1 -0
- package/dist/src/editor/components/seo-editor.d.ts +2 -0
- package/dist/src/editor/components/seo-editor.d.ts.map +1 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts +8 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast-container.d.ts +7 -0
- package/dist/src/editor/components/toast/toast-container.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast.d.ts +7 -0
- package/dist/src/editor/components/toast/toast.d.ts.map +1 -0
- package/dist/src/editor/components/toast/types.d.ts +7 -0
- package/dist/src/editor/components/toast/types.d.ts.map +1 -0
- package/dist/src/editor/components/toolbar.d.ts +21 -0
- package/dist/src/editor/components/toolbar.d.ts.map +1 -0
- package/dist/src/editor/config.d.ts +4 -0
- package/dist/src/editor/config.d.ts.map +1 -0
- package/dist/src/editor/constants.d.ts +101 -0
- package/dist/src/editor/constants.d.ts.map +1 -0
- package/dist/src/editor/context.d.ts +14 -0
- package/dist/src/editor/context.d.ts.map +1 -0
- package/dist/src/editor/dom.d.ts +77 -0
- package/dist/src/editor/dom.d.ts.map +1 -0
- package/dist/src/editor/editor.d.ts +64 -0
- package/dist/src/editor/editor.d.ts.map +1 -0
- package/dist/src/editor/history.d.ts +20 -0
- package/dist/src/editor/history.d.ts.map +1 -0
- package/dist/src/editor/hooks/index.d.ts +14 -0
- package/dist/src/editor/hooks/index.d.ts.map +1 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts +22 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts +18 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts +26 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts +12 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts +23 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts +19 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts.map +1 -0
- package/dist/src/editor/hooks/utils.d.ts +32 -0
- package/dist/src/editor/hooks/utils.d.ts.map +1 -0
- package/dist/src/editor/index.d.ts +12 -0
- package/dist/src/editor/index.d.ts.map +1 -0
- package/dist/src/editor/lib/cn.d.ts +3 -0
- package/dist/src/editor/lib/cn.d.ts.map +1 -0
- package/dist/src/editor/manifest.d.ts +19 -0
- package/dist/src/editor/manifest.d.ts.map +1 -0
- package/dist/src/editor/markdown-api.d.ts +36 -0
- package/dist/src/editor/markdown-api.d.ts.map +1 -0
- package/dist/src/editor/signals.d.ts +242 -0
- package/dist/src/editor/signals.d.ts.map +1 -0
- package/dist/src/editor/storage.d.ts +27 -0
- package/dist/src/editor/storage.d.ts.map +1 -0
- package/dist/src/editor/text-styling.d.ts +350 -0
- package/dist/src/editor/text-styling.d.ts.map +1 -0
- package/dist/src/editor/themes.d.ts +38 -0
- package/dist/src/editor/themes.d.ts.map +1 -0
- package/dist/src/editor/types.d.ts +454 -0
- package/dist/src/editor/types.d.ts.map +1 -0
- package/dist/src/error-collector.d.ts +56 -0
- package/dist/src/error-collector.d.ts.map +1 -0
- package/dist/src/handlers/component-ops.d.ts +34 -0
- package/dist/src/handlers/component-ops.d.ts.map +1 -0
- package/dist/src/handlers/markdown-ops.d.ts +41 -0
- package/dist/src/handlers/markdown-ops.d.ts.map +1 -0
- package/dist/src/handlers/request-utils.d.ts +20 -0
- package/dist/src/handlers/request-utils.d.ts.map +1 -0
- package/dist/src/handlers/source-writer.d.ts +51 -0
- package/dist/src/handlers/source-writer.d.ts.map +1 -0
- package/dist/src/html-processor.d.ts +63 -0
- package/dist/src/html-processor.d.ts.map +1 -0
- package/dist/src/index.d.ts +41 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/manifest-writer.d.ts +111 -0
- package/dist/src/manifest-writer.d.ts.map +1 -0
- package/dist/src/media/contember.d.ts +15 -0
- package/dist/src/media/contember.d.ts.map +1 -0
- package/dist/src/media/local.d.ts +9 -0
- package/dist/src/media/local.d.ts.map +1 -0
- package/dist/src/media/s3.d.ts +12 -0
- package/dist/src/media/s3.d.ts.map +1 -0
- package/dist/src/media/types.d.ts +40 -0
- package/dist/src/media/types.d.ts.map +1 -0
- package/dist/src/preview-generator.d.ts +19 -0
- package/dist/src/preview-generator.d.ts.map +1 -0
- package/dist/src/seo-processor.d.ts +23 -0
- package/dist/src/seo-processor.d.ts.map +1 -0
- package/dist/src/source-finder/ast-extractors.d.ts +35 -0
- package/dist/src/source-finder/ast-extractors.d.ts.map +1 -0
- package/dist/src/source-finder/ast-parser.d.ts +16 -0
- package/dist/src/source-finder/ast-parser.d.ts.map +1 -0
- package/dist/src/source-finder/cache.d.ts +18 -0
- package/dist/src/source-finder/cache.d.ts.map +1 -0
- package/dist/src/source-finder/collection-finder.d.ts +29 -0
- package/dist/src/source-finder/collection-finder.d.ts.map +1 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts +39 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -0
- package/dist/src/source-finder/element-finder.d.ts +42 -0
- package/dist/src/source-finder/element-finder.d.ts.map +1 -0
- package/dist/src/source-finder/image-finder.d.ts +24 -0
- package/dist/src/source-finder/image-finder.d.ts.map +1 -0
- package/dist/src/source-finder/index.d.ts +9 -0
- package/dist/src/source-finder/index.d.ts.map +1 -0
- package/dist/src/source-finder/search-index.d.ts +27 -0
- package/dist/src/source-finder/search-index.d.ts.map +1 -0
- package/dist/src/source-finder/snippet-utils.d.ts +90 -0
- package/dist/src/source-finder/snippet-utils.d.ts.map +1 -0
- package/dist/src/source-finder/source-lookup.d.ts +16 -0
- package/dist/src/source-finder/source-lookup.d.ts.map +1 -0
- package/dist/src/source-finder/types.d.ts +167 -0
- package/dist/src/source-finder/types.d.ts.map +1 -0
- package/dist/src/source-finder/variable-extraction.d.ts +37 -0
- package/dist/src/source-finder/variable-extraction.d.ts.map +1 -0
- package/dist/src/tailwind-colors.d.ts +54 -0
- package/dist/src/tailwind-colors.d.ts.map +1 -0
- package/dist/src/tsconfig.tsbuildinfo +1 -0
- package/dist/src/types.d.ts +367 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +61 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/vite-plugin.d.ts +14 -0
- package/dist/src/vite-plugin.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +80 -0
- package/src/build-processor.ts +784 -0
- package/src/collection-scanner.ts +304 -0
- package/src/component-registry.ts +393 -0
- package/src/config.ts +74 -0
- package/src/dev-middleware.ts +525 -0
- package/src/dist/src/tsconfig.tsbuildinfo +1 -0
- package/src/editor/ai.ts +185 -0
- package/src/editor/api.ts +513 -0
- package/src/editor/color-utils.ts +556 -0
- package/src/editor/components/ai-chat.tsx +632 -0
- package/src/editor/components/ai-tooltip.tsx +179 -0
- package/src/editor/components/attribute-editor.tsx +596 -0
- package/src/editor/components/block-editor.tsx +546 -0
- package/src/editor/components/collections-browser.tsx +248 -0
- package/src/editor/components/color-toolbar.tsx +314 -0
- package/src/editor/components/confirm-dialog.tsx +69 -0
- package/src/editor/components/create-page-modal.tsx +163 -0
- package/src/editor/components/editable-highlights.tsx +260 -0
- package/src/editor/components/error-boundary.tsx +87 -0
- package/src/editor/components/fields.tsx +387 -0
- package/src/editor/components/frontmatter-fields.tsx +469 -0
- package/src/editor/components/highlight-overlay.ts +229 -0
- package/src/editor/components/image-overlay.tsx +230 -0
- package/src/editor/components/markdown-editor-overlay.tsx +505 -0
- package/src/editor/components/markdown-inline-editor.tsx +780 -0
- package/src/editor/components/media-library.tsx +297 -0
- package/src/editor/components/outline.tsx +402 -0
- package/src/editor/components/redirect-countdown.tsx +45 -0
- package/src/editor/components/seo-editor.tsx +498 -0
- package/src/editor/components/text-style-toolbar.tsx +362 -0
- package/src/editor/components/toast/toast-container.tsx +15 -0
- package/src/editor/components/toast/toast.tsx +49 -0
- package/src/editor/components/toast/types.ts +7 -0
- package/src/editor/components/toolbar.tsx +366 -0
- package/src/editor/config.ts +12 -0
- package/src/editor/constants.ts +106 -0
- package/src/editor/context.tsx +38 -0
- package/src/editor/dom.ts +357 -0
- package/src/editor/editor.ts +1510 -0
- package/src/editor/env.d.ts +4 -0
- package/src/editor/history.ts +355 -0
- package/src/editor/hooks/index.ts +19 -0
- package/src/editor/hooks/useAIHandlers.ts +345 -0
- package/src/editor/hooks/useBlockEditorHandlers.ts +206 -0
- package/src/editor/hooks/useElementDetection.ts +284 -0
- package/src/editor/hooks/useImageHoverDetection.ts +102 -0
- package/src/editor/hooks/useTextSelection.ts +187 -0
- package/src/editor/hooks/useTooltipState.ts +126 -0
- package/src/editor/hooks/utils.ts +101 -0
- package/src/editor/index.tsx +481 -0
- package/src/editor/lib/cn.ts +4 -0
- package/src/editor/manifest.ts +25 -0
- package/src/editor/markdown-api.ts +209 -0
- package/src/editor/signals.ts +1351 -0
- package/src/editor/storage.ts +266 -0
- package/src/editor/styles.css +465 -0
- package/src/editor/text-styling.ts +773 -0
- package/src/editor/themes.ts +210 -0
- package/src/editor/types.ts +591 -0
- package/src/error-collector.ts +106 -0
- package/src/handlers/component-ops.ts +463 -0
- package/src/handlers/markdown-ops.ts +202 -0
- package/src/handlers/request-utils.ts +151 -0
- package/src/handlers/source-writer.ts +649 -0
- package/src/html-processor.ts +1108 -0
- package/src/index.ts +284 -0
- package/src/manifest-writer.ts +371 -0
- package/src/media/contember.ts +84 -0
- package/src/media/local.ts +114 -0
- package/src/media/s3.ts +133 -0
- package/src/media/types.ts +33 -0
- package/src/preview-generator.ts +293 -0
- package/src/seo-processor.ts +567 -0
- package/src/source-finder/ast-extractors.ts +185 -0
- package/src/source-finder/ast-parser.ts +150 -0
- package/src/source-finder/cache.ts +76 -0
- package/src/source-finder/collection-finder.ts +335 -0
- package/src/source-finder/cross-file-tracker.ts +741 -0
- package/src/source-finder/element-finder.ts +387 -0
- package/src/source-finder/image-finder.ts +283 -0
- package/src/source-finder/index.ts +37 -0
- package/src/source-finder/search-index.ts +525 -0
- package/src/source-finder/snippet-utils.ts +668 -0
- package/src/source-finder/source-lookup.ts +200 -0
- package/src/source-finder/types.ts +210 -0
- package/src/source-finder/variable-extraction.ts +406 -0
- package/src/tailwind-colors.ts +874 -0
- package/src/tsconfig.json +25 -0
- package/src/types.ts +406 -0
- package/src/utils.ts +186 -0
- package/src/vite-plugin.ts +42 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { parse as parseYaml } from 'yaml'
|
|
4
|
+
import { getProjectRoot } from './config'
|
|
5
|
+
import type { CollectionDefinition, CollectionEntryInfo, FieldDefinition, FieldType } from './types'
|
|
6
|
+
|
|
7
|
+
/** Regex patterns for type inference */
|
|
8
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}/
|
|
9
|
+
const URL_PATTERN = /^(https?:\/\/|\/)/
|
|
10
|
+
const IMAGE_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|svg|avif)$/i
|
|
11
|
+
|
|
12
|
+
/** Maximum unique values before treating as free-form text instead of select */
|
|
13
|
+
const MAX_SELECT_OPTIONS = 10
|
|
14
|
+
|
|
15
|
+
/** Minimum length for textarea detection */
|
|
16
|
+
const TEXTAREA_MIN_LENGTH = 200
|
|
17
|
+
|
|
18
|
+
/** Field names that should never be inferred as select (always free-text) */
|
|
19
|
+
const FREE_TEXT_FIELD_NAMES = new Set([
|
|
20
|
+
'title',
|
|
21
|
+
'name',
|
|
22
|
+
'description',
|
|
23
|
+
'summary',
|
|
24
|
+
'excerpt',
|
|
25
|
+
'subtitle',
|
|
26
|
+
'heading',
|
|
27
|
+
'headline',
|
|
28
|
+
'slug',
|
|
29
|
+
'alt',
|
|
30
|
+
'caption',
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Observed values for a single field across multiple files
|
|
35
|
+
*/
|
|
36
|
+
interface FieldObservation {
|
|
37
|
+
name: string
|
|
38
|
+
values: unknown[]
|
|
39
|
+
presentCount: number
|
|
40
|
+
totalEntries: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse YAML frontmatter from markdown content
|
|
45
|
+
*/
|
|
46
|
+
function parseFrontmatter(content: string): Record<string, unknown> | null {
|
|
47
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
48
|
+
if (!match?.[1]) return null
|
|
49
|
+
|
|
50
|
+
return parseYaml(match[1]) as Record<string, unknown> | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Infer the field type from a value
|
|
55
|
+
*/
|
|
56
|
+
function inferFieldType(value: unknown, key: string): FieldType {
|
|
57
|
+
if (value === null || value === undefined) {
|
|
58
|
+
return 'text'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof value === 'boolean') {
|
|
62
|
+
return 'boolean'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof value === 'number') {
|
|
66
|
+
return 'number'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
return 'array'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof value === 'object') {
|
|
74
|
+
return 'object'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof value === 'string') {
|
|
78
|
+
// Check for date pattern
|
|
79
|
+
if (DATE_PATTERN.test(value)) {
|
|
80
|
+
return 'date'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check for image paths
|
|
84
|
+
if (IMAGE_EXTENSIONS.test(value)) {
|
|
85
|
+
return 'image'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for image-specific field names (exact word boundaries, not substrings)
|
|
89
|
+
const lowerKey = key.toLowerCase()
|
|
90
|
+
if (/(?:^|[_-])(?:image|thumbnail|cover|avatar|logo|icon|banner|photo)(?:$|[_-])/.test(lowerKey)) {
|
|
91
|
+
return 'image'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check for URLs
|
|
95
|
+
if (URL_PATTERN.test(value)) {
|
|
96
|
+
return 'url'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check for textarea (long text or contains newlines)
|
|
100
|
+
if (value.includes('\n') || value.length > TEXTAREA_MIN_LENGTH) {
|
|
101
|
+
return 'textarea'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return 'text'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return 'text'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Merge field observations from multiple files to determine final field definition
|
|
112
|
+
*/
|
|
113
|
+
function mergeFieldObservations(observations: FieldObservation[]): FieldDefinition[] {
|
|
114
|
+
const fields: FieldDefinition[] = []
|
|
115
|
+
|
|
116
|
+
for (const obs of observations) {
|
|
117
|
+
const nonNullValues = obs.values.filter(v => v !== null && v !== undefined)
|
|
118
|
+
if (nonNullValues.length === 0) continue
|
|
119
|
+
|
|
120
|
+
// Determine type by consensus (most common inferred type)
|
|
121
|
+
const typeCounts = new Map<FieldType, number>()
|
|
122
|
+
for (const value of nonNullValues) {
|
|
123
|
+
const type = inferFieldType(value, obs.name)
|
|
124
|
+
typeCounts.set(type, (typeCounts.get(type) || 0) + 1)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Get most common type
|
|
128
|
+
let fieldType: FieldType = 'text'
|
|
129
|
+
let maxCount = 0
|
|
130
|
+
for (const [type, count] of typeCounts) {
|
|
131
|
+
if (count > maxCount) {
|
|
132
|
+
maxCount = count
|
|
133
|
+
fieldType = type
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const field: FieldDefinition = {
|
|
138
|
+
name: obs.name,
|
|
139
|
+
type: fieldType,
|
|
140
|
+
required: obs.presentCount === obs.totalEntries,
|
|
141
|
+
examples: nonNullValues.slice(0, 3),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// For text fields, check if we should treat as select (limited unique values)
|
|
145
|
+
if (fieldType === 'text' && !FREE_TEXT_FIELD_NAMES.has(obs.name.toLowerCase())) {
|
|
146
|
+
const uniqueValues = [...new Set(nonNullValues.map(v => String(v)))]
|
|
147
|
+
const uniqueRatio = uniqueValues.length / nonNullValues.length
|
|
148
|
+
// Only treat as select if unique values are limited AND not nearly all unique
|
|
149
|
+
// (a high unique ratio means entries have distinct values, indicating free-text)
|
|
150
|
+
if (uniqueValues.length > 0 && uniqueValues.length <= MAX_SELECT_OPTIONS && nonNullValues.length >= 2 && uniqueRatio <= 0.8) {
|
|
151
|
+
field.type = 'select'
|
|
152
|
+
field.options = uniqueValues.sort()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// For arrays, try to infer item type
|
|
157
|
+
if (fieldType === 'array') {
|
|
158
|
+
const allItems = nonNullValues.flatMap(v => (Array.isArray(v) ? v : []))
|
|
159
|
+
if (allItems.length > 0) {
|
|
160
|
+
const itemType = inferFieldType(allItems[0], obs.name)
|
|
161
|
+
field.itemType = itemType
|
|
162
|
+
|
|
163
|
+
// Check if array items should be select
|
|
164
|
+
if (itemType === 'text') {
|
|
165
|
+
const uniqueItems = [...new Set(allItems.map(v => String(v)))]
|
|
166
|
+
if (uniqueItems.length <= MAX_SELECT_OPTIONS * 2) {
|
|
167
|
+
field.options = uniqueItems.sort()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fields.push(field)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return fields
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Scan a single collection directory and infer its schema
|
|
181
|
+
*/
|
|
182
|
+
async function scanCollection(collectionPath: string, collectionName: string, contentDir: string): Promise<CollectionDefinition | null> {
|
|
183
|
+
try {
|
|
184
|
+
const entries = await fs.readdir(collectionPath, { withFileTypes: true })
|
|
185
|
+
const markdownFiles = entries.filter(e => e.isFile() && (e.name.endsWith('.md') || e.name.endsWith('.mdx')))
|
|
186
|
+
|
|
187
|
+
if (markdownFiles.length === 0) return null
|
|
188
|
+
|
|
189
|
+
// Determine file extension (prefer md, use mdx if that's all we have)
|
|
190
|
+
const hasMd = markdownFiles.some(f => f.name.endsWith('.md'))
|
|
191
|
+
const fileExtension: 'md' | 'mdx' = hasMd ? 'md' : 'mdx'
|
|
192
|
+
|
|
193
|
+
// Collect field observations and entry info across all files
|
|
194
|
+
const fieldMap = new Map<string, FieldObservation>()
|
|
195
|
+
const entryInfos: CollectionEntryInfo[] = []
|
|
196
|
+
let hasDraft = false
|
|
197
|
+
|
|
198
|
+
for (const file of markdownFiles) {
|
|
199
|
+
const filePath = path.join(collectionPath, file.name)
|
|
200
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
201
|
+
const frontmatter = parseFrontmatter(content)
|
|
202
|
+
|
|
203
|
+
// Collect entry info
|
|
204
|
+
const slug = file.name.replace(/\.(md|mdx)$/, '')
|
|
205
|
+
const entryInfo: CollectionEntryInfo = {
|
|
206
|
+
slug,
|
|
207
|
+
sourcePath: path.join(contentDir, collectionName, file.name),
|
|
208
|
+
}
|
|
209
|
+
if (frontmatter) {
|
|
210
|
+
if (typeof frontmatter.title === 'string') {
|
|
211
|
+
entryInfo.title = frontmatter.title
|
|
212
|
+
}
|
|
213
|
+
if (typeof frontmatter.draft === 'boolean' && frontmatter.draft) {
|
|
214
|
+
entryInfo.draft = true
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
entryInfos.push(entryInfo)
|
|
218
|
+
|
|
219
|
+
if (!frontmatter) continue
|
|
220
|
+
|
|
221
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
222
|
+
if (key === 'draft' && typeof value === 'boolean') {
|
|
223
|
+
hasDraft = true
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let obs = fieldMap.get(key)
|
|
227
|
+
if (!obs) {
|
|
228
|
+
obs = {
|
|
229
|
+
name: key,
|
|
230
|
+
values: [],
|
|
231
|
+
presentCount: 0,
|
|
232
|
+
totalEntries: markdownFiles.length,
|
|
233
|
+
}
|
|
234
|
+
fieldMap.set(key, obs)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
obs.values.push(value)
|
|
238
|
+
obs.presentCount++
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Sort entries alphabetically by title (fallback to slug)
|
|
243
|
+
entryInfos.sort((a, b) => {
|
|
244
|
+
const aLabel = a.title ?? a.slug
|
|
245
|
+
const bLabel = b.title ?? b.slug
|
|
246
|
+
return aLabel.localeCompare(bLabel)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
// Update totalEntries for all observations
|
|
250
|
+
for (const obs of fieldMap.values()) {
|
|
251
|
+
obs.totalEntries = markdownFiles.length
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const fields = mergeFieldObservations(Array.from(fieldMap.values()))
|
|
255
|
+
|
|
256
|
+
// Generate a human-readable label
|
|
257
|
+
const label = collectionName
|
|
258
|
+
.replace(/[-_]/g, ' ')
|
|
259
|
+
.replace(/\b\w/g, c => c.toUpperCase())
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
name: collectionName,
|
|
263
|
+
label,
|
|
264
|
+
path: path.join(contentDir, collectionName),
|
|
265
|
+
entryCount: markdownFiles.length,
|
|
266
|
+
fields,
|
|
267
|
+
supportsDraft: hasDraft,
|
|
268
|
+
fileExtension,
|
|
269
|
+
entries: entryInfos,
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Scan all collections in the content directory
|
|
278
|
+
*/
|
|
279
|
+
export async function scanCollections(contentDir: string = 'src/content'): Promise<Record<string, CollectionDefinition>> {
|
|
280
|
+
const projectRoot = getProjectRoot()
|
|
281
|
+
const fullContentDir = path.isAbsolute(contentDir) ? contentDir : path.join(projectRoot, contentDir)
|
|
282
|
+
|
|
283
|
+
const collections: Record<string, CollectionDefinition> = {}
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const entries = await fs.readdir(fullContentDir, { withFileTypes: true })
|
|
287
|
+
|
|
288
|
+
const scanPromises = entries
|
|
289
|
+
.filter(entry => entry.isDirectory() && !entry.name.startsWith('_') && !entry.name.startsWith('.'))
|
|
290
|
+
.map(async entry => {
|
|
291
|
+
const collectionPath = path.join(fullContentDir, entry.name)
|
|
292
|
+
const definition = await scanCollection(collectionPath, entry.name, contentDir)
|
|
293
|
+
if (definition) {
|
|
294
|
+
collections[entry.name] = definition
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
await Promise.all(scanPromises)
|
|
299
|
+
} catch {
|
|
300
|
+
// Content directory doesn't exist or isn't readable
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return collections
|
|
304
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { getProjectRoot } from './config'
|
|
4
|
+
import { getErrorCollector } from './error-collector'
|
|
5
|
+
import type { ComponentDefinition, ComponentProp } from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Scans Astro component files and extracts their definitions including props
|
|
9
|
+
*/
|
|
10
|
+
export class ComponentRegistry {
|
|
11
|
+
private components: Map<string, ComponentDefinition> = new Map()
|
|
12
|
+
private componentDirs: string[]
|
|
13
|
+
|
|
14
|
+
constructor(componentDirs: string[] = ['src/components']) {
|
|
15
|
+
this.componentDirs = componentDirs
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Scan all component directories and build the registry
|
|
20
|
+
*/
|
|
21
|
+
async scan(): Promise<void> {
|
|
22
|
+
for (const dir of this.componentDirs) {
|
|
23
|
+
const fullPath = path.join(getProjectRoot(), dir)
|
|
24
|
+
try {
|
|
25
|
+
await this.scanDirectory(fullPath, dir)
|
|
26
|
+
} catch {
|
|
27
|
+
// Directory doesn't exist, skip
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get all registered components
|
|
34
|
+
*/
|
|
35
|
+
getComponents(): Record<string, ComponentDefinition> {
|
|
36
|
+
return Object.fromEntries(this.components)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get a specific component by name
|
|
41
|
+
*/
|
|
42
|
+
getComponent(name: string): ComponentDefinition | undefined {
|
|
43
|
+
return this.components.get(name)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Scan a directory recursively for .astro files
|
|
48
|
+
*/
|
|
49
|
+
private async scanDirectory(dir: string, relativePath: string): Promise<void> {
|
|
50
|
+
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
51
|
+
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const fullPath = path.join(dir, entry.name)
|
|
54
|
+
const relPath = path.join(relativePath, entry.name)
|
|
55
|
+
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
await this.scanDirectory(fullPath, relPath)
|
|
58
|
+
} else if (entry.isFile() && entry.name.endsWith('.astro')) {
|
|
59
|
+
await this.parseComponent(fullPath, relPath)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse a single Astro component file
|
|
66
|
+
*/
|
|
67
|
+
private async parseComponent(filePath: string, relativePath: string): Promise<void> {
|
|
68
|
+
try {
|
|
69
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
70
|
+
const componentName = path.basename(filePath, '.astro')
|
|
71
|
+
|
|
72
|
+
const props = await this.extractProps(content)
|
|
73
|
+
const slots = this.extractSlots(content)
|
|
74
|
+
const description = this.extractDescription(content)
|
|
75
|
+
const previewWidth = this.extractPreviewWidth(content)
|
|
76
|
+
|
|
77
|
+
this.components.set(componentName, {
|
|
78
|
+
name: componentName,
|
|
79
|
+
file: relativePath,
|
|
80
|
+
props,
|
|
81
|
+
slots: slots.length > 0 ? slots : undefined,
|
|
82
|
+
description,
|
|
83
|
+
previewWidth,
|
|
84
|
+
})
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.warn(`[ComponentRegistry] Failed to parse ${filePath}:`, error)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse Props content and extract individual property definitions
|
|
92
|
+
* Handles multi-line properties with nested types
|
|
93
|
+
*/
|
|
94
|
+
private parsePropsContent(propsContent: string): ComponentProp[] {
|
|
95
|
+
const props: ComponentProp[] = []
|
|
96
|
+
let i = 0
|
|
97
|
+
const content = propsContent.trim()
|
|
98
|
+
|
|
99
|
+
while (i < content.length) {
|
|
100
|
+
// Skip whitespace and newlines
|
|
101
|
+
while (i < content.length && /\s/.test(content[i] ?? '')) i++
|
|
102
|
+
if (i >= content.length) break
|
|
103
|
+
|
|
104
|
+
// Skip comments
|
|
105
|
+
if (content[i] === '/' && content[i + 1] === '/') {
|
|
106
|
+
// Skip to end of line
|
|
107
|
+
while (i < content.length && content[i] !== '\n') i++
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (content[i] === '/' && content[i + 1] === '*') {
|
|
112
|
+
// Skip block comment
|
|
113
|
+
while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/')) i++
|
|
114
|
+
i += 2
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Extract property name
|
|
119
|
+
const nameStart = i
|
|
120
|
+
while (i < content.length && /\w/.test(content[i] ?? '')) i++
|
|
121
|
+
const name = content.substring(nameStart, i)
|
|
122
|
+
|
|
123
|
+
if (!name) break
|
|
124
|
+
|
|
125
|
+
// Skip whitespace
|
|
126
|
+
while (i < content.length && /\s/.test(content[i] ?? '')) i++
|
|
127
|
+
|
|
128
|
+
// Check for optional marker
|
|
129
|
+
const optional = content[i] === '?'
|
|
130
|
+
if (optional) i++
|
|
131
|
+
|
|
132
|
+
// Skip whitespace
|
|
133
|
+
while (i < content.length && /\s/.test(content[i] ?? '')) i++
|
|
134
|
+
|
|
135
|
+
// Expect colon
|
|
136
|
+
if (content[i] !== ':') break
|
|
137
|
+
i++
|
|
138
|
+
|
|
139
|
+
// Skip whitespace
|
|
140
|
+
while (i < content.length && /\s/.test(content[i] ?? '')) i++
|
|
141
|
+
|
|
142
|
+
// Extract type (up to semicolon, handling nested braces)
|
|
143
|
+
const typeStart = i
|
|
144
|
+
let braceDepth = 0
|
|
145
|
+
let angleDepth = 0
|
|
146
|
+
while (i < content.length) {
|
|
147
|
+
if (content[i] === '{') braceDepth++
|
|
148
|
+
else if (content[i] === '}') braceDepth--
|
|
149
|
+
else if (content[i] === '<') angleDepth++
|
|
150
|
+
else if (content[i] === '>') angleDepth--
|
|
151
|
+
else if (content[i] === ';' && braceDepth === 0 && angleDepth === 0) break
|
|
152
|
+
i++
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const type = content.substring(typeStart, i).trim()
|
|
156
|
+
|
|
157
|
+
// Skip the semicolon
|
|
158
|
+
if (content[i] === ';') i++
|
|
159
|
+
|
|
160
|
+
// Skip whitespace
|
|
161
|
+
while (i < content.length && /[ \t]/.test(content[i] ?? '')) i++
|
|
162
|
+
|
|
163
|
+
// Check for inline comment
|
|
164
|
+
let description: string | undefined
|
|
165
|
+
if (content[i] === '/' && content[i + 1] === '/') {
|
|
166
|
+
i += 2
|
|
167
|
+
const commentStart = i
|
|
168
|
+
while (i < content.length && content[i] !== '\n') i++
|
|
169
|
+
description = content.substring(commentStart, i).trim()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (name && type) {
|
|
173
|
+
props.push({
|
|
174
|
+
name,
|
|
175
|
+
type,
|
|
176
|
+
required: !optional,
|
|
177
|
+
description,
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return props
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Extract content between balanced braces after a pattern match
|
|
187
|
+
* Properly handles nested objects
|
|
188
|
+
*/
|
|
189
|
+
private extractBalancedBraces(text: string, pattern: RegExp): string | null {
|
|
190
|
+
const match = text.match(pattern)
|
|
191
|
+
if (!match || match.index === undefined) return null
|
|
192
|
+
|
|
193
|
+
// Find the opening brace position (right after the match)
|
|
194
|
+
const startIndex = match.index + match[0].length
|
|
195
|
+
let depth = 1 // We already have one opening brace
|
|
196
|
+
let i = startIndex
|
|
197
|
+
|
|
198
|
+
// Find the matching closing brace
|
|
199
|
+
while (i < text.length && depth > 0) {
|
|
200
|
+
if (text[i] === '{') {
|
|
201
|
+
depth++
|
|
202
|
+
} else if (text[i] === '}') {
|
|
203
|
+
depth--
|
|
204
|
+
}
|
|
205
|
+
i++
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (depth !== 0) return null // Unbalanced braces
|
|
209
|
+
|
|
210
|
+
// Extract content between braces (excluding the braces themselves)
|
|
211
|
+
return text.substring(startIndex, i - 1)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Extract props from component frontmatter
|
|
216
|
+
*/
|
|
217
|
+
private async extractProps(content: string): Promise<ComponentProp[]> {
|
|
218
|
+
const props: ComponentProp[] = []
|
|
219
|
+
|
|
220
|
+
// Find the frontmatter section
|
|
221
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
222
|
+
if (!frontmatterMatch?.[1]) return props
|
|
223
|
+
|
|
224
|
+
const frontmatter = frontmatterMatch[1]
|
|
225
|
+
|
|
226
|
+
// Look for Props interface
|
|
227
|
+
const propsInterfaceContent = this.extractBalancedBraces(frontmatter, /interface\s+Props\s*\{/)
|
|
228
|
+
if (propsInterfaceContent) {
|
|
229
|
+
const extractedProps = this.parsePropsContent(propsInterfaceContent)
|
|
230
|
+
props.push(...extractedProps)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Look for type Props = { ... }
|
|
234
|
+
if (props.length === 0) {
|
|
235
|
+
const typePropsContent = this.extractBalancedBraces(frontmatter, /type\s+Props\s*=\s*\{/)
|
|
236
|
+
if (typePropsContent) {
|
|
237
|
+
const extractedProps = this.parsePropsContent(typePropsContent)
|
|
238
|
+
props.push(...extractedProps)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const destructureMatch = frontmatter?.match(/const\s*\{([^}]+)\}\s*=\s*Astro\.props/)
|
|
243
|
+
if (destructureMatch) {
|
|
244
|
+
const destructureContent = destructureMatch[1]
|
|
245
|
+
|
|
246
|
+
const defaultMatches = destructureContent?.matchAll(/(\w+)\s*=\s*(['"`]?)([^'"`},]+)\2/g) ?? []
|
|
247
|
+
for (const match of defaultMatches) {
|
|
248
|
+
const propName = match[1]
|
|
249
|
+
const defaultValue = match[3]
|
|
250
|
+
const existingProp = props.find(p => p.name === propName)
|
|
251
|
+
if (existingProp) {
|
|
252
|
+
existingProp.defaultValue = defaultValue
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return props
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Extract slot names from template
|
|
262
|
+
*/
|
|
263
|
+
private extractSlots(content: string): string[] {
|
|
264
|
+
const slots: string[] = []
|
|
265
|
+
|
|
266
|
+
// Find <slot> elements with name attribute
|
|
267
|
+
const slotMatches = content.matchAll(/<slot\s+name=["']([^"']+)["']/g)
|
|
268
|
+
for (const match of slotMatches) {
|
|
269
|
+
if (match[1]) {
|
|
270
|
+
slots.push(match[1])
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check for default slot (unnamed slot) - match any <slot> tag without a name attribute
|
|
275
|
+
const allSlotTags = content.matchAll(/<slot(?:\s+[^>]*)?\s*\/?>/g)
|
|
276
|
+
for (const match of allSlotTags) {
|
|
277
|
+
const tag = match[0]
|
|
278
|
+
// Check if this slot tag doesn't have a name attribute
|
|
279
|
+
if (!/name\s*=/.test(tag)) {
|
|
280
|
+
if (!slots.includes('default')) {
|
|
281
|
+
slots.unshift('default')
|
|
282
|
+
}
|
|
283
|
+
break // Only need to find one default slot
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return slots
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Extract component description from JSDoc comment
|
|
292
|
+
*/
|
|
293
|
+
private extractDescription(content: string): string | undefined {
|
|
294
|
+
// Look for JSDoc comment at the start of frontmatter
|
|
295
|
+
const match = content.match(/^---\n\/\*\*\s*([\s\S]*?)\s*\*\//)
|
|
296
|
+
if (match?.[1]) {
|
|
297
|
+
return match[1]
|
|
298
|
+
.split('\n')
|
|
299
|
+
.map(line => line.replace(/^\s*\*\s?/, '').trim())
|
|
300
|
+
.filter(Boolean)
|
|
301
|
+
.join(' ')
|
|
302
|
+
}
|
|
303
|
+
return undefined
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Extract @previewWidth value from JSDoc comment
|
|
308
|
+
*/
|
|
309
|
+
private extractPreviewWidth(content: string): number | undefined {
|
|
310
|
+
const match = content.match(/^---\n\/\*\*\s*([\s\S]*?)\s*\*\//)
|
|
311
|
+
if (match?.[1]) {
|
|
312
|
+
const widthMatch = match[1].match(/@previewWidth\s+(\d+)/)
|
|
313
|
+
if (widthMatch?.[1]) {
|
|
314
|
+
return parseInt(widthMatch[1], 10)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return undefined
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Parse component usage in an Astro file to extract prop values
|
|
323
|
+
*/
|
|
324
|
+
export function parseComponentUsage(
|
|
325
|
+
content: string,
|
|
326
|
+
componentName: string,
|
|
327
|
+
): Array<{ line: number; props: Record<string, string> }> {
|
|
328
|
+
const usages: Array<{ line: number; props: Record<string, string> }> = []
|
|
329
|
+
const lines = content.split('\n')
|
|
330
|
+
|
|
331
|
+
// Match component usage: <ComponentName prop="value" />
|
|
332
|
+
const componentRegex = new RegExp(
|
|
333
|
+
`<${componentName}\\s+([^>]*?)\\s*\\/?>`,
|
|
334
|
+
'g',
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
for (let i = 0; i < lines.length; i++) {
|
|
338
|
+
const line = lines[i]
|
|
339
|
+
const lineMatches = line?.matchAll(new RegExp(componentRegex.source, 'g')) || []
|
|
340
|
+
|
|
341
|
+
for (const match of lineMatches) {
|
|
342
|
+
const propsString = match[1]
|
|
343
|
+
const props = parsePropsString(propsString)
|
|
344
|
+
|
|
345
|
+
usages.push({
|
|
346
|
+
line: i + 1,
|
|
347
|
+
props,
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return usages
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Parse props string from component tag
|
|
357
|
+
*/
|
|
358
|
+
function parsePropsString(propsString?: string): Record<string, string> {
|
|
359
|
+
const props: Record<string, string> = {}
|
|
360
|
+
if (!propsString) return props
|
|
361
|
+
|
|
362
|
+
// Match prop="value" or prop='value' or prop={expression} or prop (boolean)
|
|
363
|
+
// For expressions, handle nested braces by counting depth
|
|
364
|
+
const regex = /(\w+)(?:=(?:"([^"]*)"|'([^']*)'|\{))?/g
|
|
365
|
+
let match: RegExpExecArray | null
|
|
366
|
+
while ((match = regex.exec(propsString)) !== null) {
|
|
367
|
+
const name = match[1]
|
|
368
|
+
if (!name) continue
|
|
369
|
+
|
|
370
|
+
if (match[2] !== undefined) {
|
|
371
|
+
props[name] = match[2]
|
|
372
|
+
} else if (match[3] !== undefined) {
|
|
373
|
+
props[name] = match[3]
|
|
374
|
+
} else if (match[0].endsWith('{')) {
|
|
375
|
+
// Expression: count braces to find the matching close
|
|
376
|
+
let depth = 1
|
|
377
|
+
const start = regex.lastIndex
|
|
378
|
+
let i = start
|
|
379
|
+
while (i < propsString.length && depth > 0) {
|
|
380
|
+
if (propsString[i] === '{') depth++
|
|
381
|
+
else if (propsString[i] === '}') depth--
|
|
382
|
+
i++
|
|
383
|
+
}
|
|
384
|
+
props[name] = propsString.slice(start, i - 1)
|
|
385
|
+
regex.lastIndex = i
|
|
386
|
+
} else {
|
|
387
|
+
// Boolean prop
|
|
388
|
+
props[name] = 'true'
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return props
|
|
393
|
+
}
|