@nuasite/cms-core 0.43.0-beta.1
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/dist/types/collection-scanner.d.ts +12 -0
- package/dist/types/collection-scanner.d.ts.map +1 -0
- package/dist/types/component-registry.d.ts +15 -0
- package/dist/types/component-registry.d.ts.map +1 -0
- package/dist/types/content-config-ast.d.ts +45 -0
- package/dist/types/content-config-ast.d.ts.map +1 -0
- package/dist/types/core.d.ts +44 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/types/fs/glob.d.ts +3 -0
- package/dist/types/fs/glob.d.ts.map +1 -0
- package/dist/types/fs/node-fs.d.ts +7 -0
- package/dist/types/fs/node-fs.d.ts.map +1 -0
- package/dist/types/fs/types.d.ts +33 -0
- package/dist/types/fs/types.d.ts.map +1 -0
- package/dist/types/handlers/entry-ops.d.ts +69 -0
- package/dist/types/handlers/entry-ops.d.ts.map +1 -0
- package/dist/types/handlers/page-ops.d.ts +14 -0
- package/dist/types/handlers/page-ops.d.ts.map +1 -0
- package/dist/types/handlers/redirect-ops.d.ts +10 -0
- package/dist/types/handlers/redirect-ops.d.ts.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/media/contember.d.ts +18 -0
- package/dist/types/media/contember.d.ts.map +1 -0
- package/dist/types/media/index.d.ts +5 -0
- package/dist/types/media/index.d.ts.map +1 -0
- package/dist/types/media/local.d.ts +12 -0
- package/dist/types/media/local.d.ts.map +1 -0
- package/dist/types/media/project-images.d.ts +15 -0
- package/dist/types/media/project-images.d.ts.map +1 -0
- package/dist/types/media/s3.d.ts +12 -0
- package/dist/types/media/s3.d.ts.map +1 -0
- package/dist/types/shared.d.ts +24 -0
- package/dist/types/shared.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +55 -0
- package/src/collection-scanner.ts +935 -0
- package/src/component-registry.ts +308 -0
- package/src/content-config-ast.ts +536 -0
- package/src/core.ts +167 -0
- package/src/fs/glob.ts +32 -0
- package/src/fs/node-fs.ts +138 -0
- package/src/fs/types.ts +26 -0
- package/src/handlers/entry-ops.ts +528 -0
- package/src/handlers/page-ops.ts +203 -0
- package/src/handlers/redirect-ops.ts +139 -0
- package/src/index.ts +41 -0
- package/src/media/contember.ts +90 -0
- package/src/media/index.ts +4 -0
- package/src/media/local.ts +147 -0
- package/src/media/project-images.ts +82 -0
- package/src/media/s3.ts +151 -0
- package/src/shared.ts +65 -0
- package/src/tsconfig.json +9 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { parse as parseBabel } from '@babel/parser'
|
|
2
|
+
import type { ComponentDefinition, ComponentProp } from '@nuasite/cms-types'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import type { CmsFileSystem } from './fs/types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Scan Astro component files under the given directories and extract their
|
|
8
|
+
* definitions (props, slots, description, preview width) over the FileSystem
|
|
9
|
+
* port.
|
|
10
|
+
*
|
|
11
|
+
* Ported verbatim from `@nuasite/cms`'s `ComponentRegistry` so that the
|
|
12
|
+
* `componentDefinitions` resolved internally by `updateEntry` (for MDX import
|
|
13
|
+
* injection) match the manifest-fed definitions used by the legacy handler. The
|
|
14
|
+
* only behavioral difference is the I/O boundary: directory walking and file
|
|
15
|
+
* reads go through `CmsFileSystem` instead of `node:fs` + `getProjectRoot()`.
|
|
16
|
+
*/
|
|
17
|
+
export async function scanComponentDefinitions(
|
|
18
|
+
fs: CmsFileSystem,
|
|
19
|
+
componentDirs: string[] = ['src/components'],
|
|
20
|
+
): Promise<Record<string, ComponentDefinition>> {
|
|
21
|
+
const components: Record<string, ComponentDefinition> = {}
|
|
22
|
+
|
|
23
|
+
for (const dir of componentDirs) {
|
|
24
|
+
await scanDirectory(fs, dir, components)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return components
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function scanDirectory(
|
|
31
|
+
fs: CmsFileSystem,
|
|
32
|
+
relativePath: string,
|
|
33
|
+
components: Record<string, ComponentDefinition>,
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
const entries = await fs.list(relativePath)
|
|
36
|
+
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const relPath = `${relativePath}/${entry.name}`
|
|
39
|
+
|
|
40
|
+
if (entry.isDirectory) {
|
|
41
|
+
await scanDirectory(fs, relPath, components)
|
|
42
|
+
} else if (entry.name.endsWith('.astro')) {
|
|
43
|
+
await parseComponent(fs, relPath, components)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function parseComponent(
|
|
49
|
+
fs: CmsFileSystem,
|
|
50
|
+
relativePath: string,
|
|
51
|
+
components: Record<string, ComponentDefinition>,
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
const content = await fs.readFile(relativePath)
|
|
54
|
+
const componentName = path.basename(relativePath, '.astro')
|
|
55
|
+
|
|
56
|
+
const props = extractProps(content)
|
|
57
|
+
const slots = extractSlots(content)
|
|
58
|
+
const description = extractDescription(content)
|
|
59
|
+
const previewWidth = extractPreviewWidth(content)
|
|
60
|
+
|
|
61
|
+
components[componentName] = {
|
|
62
|
+
name: componentName,
|
|
63
|
+
file: relativePath,
|
|
64
|
+
props,
|
|
65
|
+
slots: slots.length > 0 ? slots : undefined,
|
|
66
|
+
description,
|
|
67
|
+
previewWidth,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse Props content using @babel/parser AST for correct TypeScript handling.
|
|
73
|
+
* Wraps the content in a synthetic interface and walks TSPropertySignature nodes.
|
|
74
|
+
*/
|
|
75
|
+
function parsePropsContent(propsContent: string): ComponentProp[] {
|
|
76
|
+
const props: ComponentProp[] = []
|
|
77
|
+
|
|
78
|
+
const synthetic = `interface _Props {\n${propsContent}\n}`
|
|
79
|
+
let ast: ReturnType<typeof parseBabel>
|
|
80
|
+
try {
|
|
81
|
+
ast = parseBabel(synthetic, {
|
|
82
|
+
sourceType: 'module',
|
|
83
|
+
plugins: ['typescript'],
|
|
84
|
+
errorRecovery: true,
|
|
85
|
+
})
|
|
86
|
+
} catch {
|
|
87
|
+
return props
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const interfaceNode = ast.program.body[0]
|
|
91
|
+
if (!interfaceNode || interfaceNode.type !== 'TSInterfaceDeclaration') return props
|
|
92
|
+
|
|
93
|
+
const lines = synthetic.split('\n')
|
|
94
|
+
|
|
95
|
+
for (const member of interfaceNode.body.body) {
|
|
96
|
+
if (member.type !== 'TSPropertySignature') continue
|
|
97
|
+
if (member.key.type !== 'Identifier') continue
|
|
98
|
+
|
|
99
|
+
const name = member.key.name
|
|
100
|
+
const optional = !!member.optional
|
|
101
|
+
|
|
102
|
+
let type = 'unknown'
|
|
103
|
+
if (member.typeAnnotation?.typeAnnotation) {
|
|
104
|
+
const ta = member.typeAnnotation.typeAnnotation
|
|
105
|
+
if (ta.loc) {
|
|
106
|
+
const startLine = ta.loc.start.line - 1
|
|
107
|
+
const endLine = ta.loc.end.line - 1
|
|
108
|
+
if (startLine === endLine) {
|
|
109
|
+
type = lines[startLine]!.slice(ta.loc.start.column, ta.loc.end.column).trim()
|
|
110
|
+
} else {
|
|
111
|
+
const parts: string[] = []
|
|
112
|
+
for (let l = startLine; l <= endLine; l++) {
|
|
113
|
+
if (l === startLine) parts.push(lines[l]!.slice(ta.loc.start.column))
|
|
114
|
+
else if (l === endLine) parts.push(lines[l]!.slice(0, ta.loc.end.column))
|
|
115
|
+
else parts.push(lines[l]!)
|
|
116
|
+
}
|
|
117
|
+
type = parts.join('\n').trim()
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
type = typeAnnotationToString(ta)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let description: string | undefined
|
|
125
|
+
|
|
126
|
+
if (member.loc) {
|
|
127
|
+
const lineIdx = member.loc.end.line - 1
|
|
128
|
+
const sourceLine = lines[lineIdx]
|
|
129
|
+
if (sourceLine) {
|
|
130
|
+
const commentMatch = sourceLine.match(/\/\/\s*(.+?)\s*$/)
|
|
131
|
+
if (commentMatch?.[1]) {
|
|
132
|
+
description = commentMatch[1]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!description && member.leadingComments && member.leadingComments.length > 0) {
|
|
138
|
+
const last = member.leadingComments[member.leadingComments.length - 1]!
|
|
139
|
+
if (last.type === 'CommentBlock') {
|
|
140
|
+
description = last.value
|
|
141
|
+
.split('\n')
|
|
142
|
+
.map((l: string) => l.replace(/^\s*\*\s?/, '').trim())
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.join(' ')
|
|
145
|
+
} else if (last.type === 'CommentLine' && last.loc && member.loc) {
|
|
146
|
+
const commentLineContent = lines[last.loc.start.line - 1]?.trim()
|
|
147
|
+
if (commentLineContent?.startsWith('//')) {
|
|
148
|
+
description = last.value.trim()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (name && type) {
|
|
154
|
+
props.push({ name, type, required: !optional, description })
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return props
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function typeAnnotationToString(node: any): string {
|
|
162
|
+
switch (node.type) {
|
|
163
|
+
case 'TSStringKeyword':
|
|
164
|
+
return 'string'
|
|
165
|
+
case 'TSNumberKeyword':
|
|
166
|
+
return 'number'
|
|
167
|
+
case 'TSBooleanKeyword':
|
|
168
|
+
return 'boolean'
|
|
169
|
+
case 'TSAnyKeyword':
|
|
170
|
+
return 'any'
|
|
171
|
+
case 'TSVoidKeyword':
|
|
172
|
+
return 'void'
|
|
173
|
+
case 'TSNullKeyword':
|
|
174
|
+
return 'null'
|
|
175
|
+
case 'TSUndefinedKeyword':
|
|
176
|
+
return 'undefined'
|
|
177
|
+
case 'TSUnknownKeyword':
|
|
178
|
+
return 'unknown'
|
|
179
|
+
case 'TSNeverKeyword':
|
|
180
|
+
return 'never'
|
|
181
|
+
case 'TSObjectKeyword':
|
|
182
|
+
return 'object'
|
|
183
|
+
case 'TSArrayType':
|
|
184
|
+
return `${typeAnnotationToString(node.elementType)}[]`
|
|
185
|
+
case 'TSUnionType':
|
|
186
|
+
return node.types.map((t: any) => typeAnnotationToString(t)).join(' | ')
|
|
187
|
+
case 'TSIntersectionType':
|
|
188
|
+
return node.types.map((t: any) => typeAnnotationToString(t)).join(' & ')
|
|
189
|
+
case 'TSLiteralType':
|
|
190
|
+
if (node.literal.type === 'StringLiteral') return `'${node.literal.value}'`
|
|
191
|
+
return String(node.literal.value)
|
|
192
|
+
case 'TSTypeReference':
|
|
193
|
+
if (node.typeName?.type === 'Identifier') return node.typeName.name
|
|
194
|
+
return 'unknown'
|
|
195
|
+
case 'TSParenthesizedType':
|
|
196
|
+
return `(${typeAnnotationToString(node.typeAnnotation)})`
|
|
197
|
+
default:
|
|
198
|
+
return 'unknown'
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Extract content between balanced braces after a pattern match. */
|
|
203
|
+
function extractBalancedBraces(text: string, pattern: RegExp): string | null {
|
|
204
|
+
const match = text.match(pattern)
|
|
205
|
+
if (!match || match.index === undefined) return null
|
|
206
|
+
|
|
207
|
+
const startIndex = match.index + match[0].length
|
|
208
|
+
let depth = 1
|
|
209
|
+
let i = startIndex
|
|
210
|
+
|
|
211
|
+
while (i < text.length && depth > 0) {
|
|
212
|
+
if (text[i] === '{') {
|
|
213
|
+
depth++
|
|
214
|
+
} else if (text[i] === '}') {
|
|
215
|
+
depth--
|
|
216
|
+
}
|
|
217
|
+
i++
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (depth !== 0) return null
|
|
221
|
+
|
|
222
|
+
return text.substring(startIndex, i - 1)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function extractProps(content: string): ComponentProp[] {
|
|
226
|
+
const props: ComponentProp[] = []
|
|
227
|
+
|
|
228
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
229
|
+
if (!frontmatterMatch?.[1]) return props
|
|
230
|
+
|
|
231
|
+
const frontmatter = frontmatterMatch[1]
|
|
232
|
+
|
|
233
|
+
const propsInterfaceContent = extractBalancedBraces(frontmatter, /interface\s+Props\s*\{/)
|
|
234
|
+
if (propsInterfaceContent) {
|
|
235
|
+
props.push(...parsePropsContent(propsInterfaceContent))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (props.length === 0) {
|
|
239
|
+
const typePropsContent = extractBalancedBraces(frontmatter, /type\s+Props\s*=\s*\{/)
|
|
240
|
+
if (typePropsContent) {
|
|
241
|
+
props.push(...parsePropsContent(typePropsContent))
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const destructureMatch = frontmatter?.match(/const\s*\{([^}]+)\}\s*=\s*Astro\.props/)
|
|
246
|
+
if (destructureMatch) {
|
|
247
|
+
const destructureContent = destructureMatch[1]
|
|
248
|
+
|
|
249
|
+
const defaultMatches = destructureContent?.matchAll(/(\w+)\s*=\s*(['"`]?)([^'"`},]+)\2/g) ?? []
|
|
250
|
+
for (const match of defaultMatches) {
|
|
251
|
+
const propName = match[1]
|
|
252
|
+
const defaultValue = match[3]
|
|
253
|
+
const existingProp = props.find(p => p.name === propName)
|
|
254
|
+
if (existingProp) {
|
|
255
|
+
existingProp.defaultValue = defaultValue
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return props
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function extractSlots(content: string): string[] {
|
|
264
|
+
const slots: string[] = []
|
|
265
|
+
|
|
266
|
+
const slotMatches = content.matchAll(/<slot\s+name=["']([^"']+)["']/g)
|
|
267
|
+
for (const match of slotMatches) {
|
|
268
|
+
if (match[1]) {
|
|
269
|
+
slots.push(match[1])
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const allSlotTags = content.matchAll(/<slot(?:\s+[^>]*)?\s*\/?>/g)
|
|
274
|
+
for (const match of allSlotTags) {
|
|
275
|
+
const tag = match[0]
|
|
276
|
+
if (!/name\s*=/.test(tag)) {
|
|
277
|
+
if (!slots.includes('default')) {
|
|
278
|
+
slots.unshift('default')
|
|
279
|
+
}
|
|
280
|
+
break
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return slots
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function extractDescription(content: string): string | undefined {
|
|
288
|
+
const match = content.match(/^---\n\/\*\*\s*([\s\S]*?)\s*\*\//)
|
|
289
|
+
if (match?.[1]) {
|
|
290
|
+
return match[1]
|
|
291
|
+
.split('\n')
|
|
292
|
+
.map(line => line.replace(/^\s*\*\s?/, '').trim())
|
|
293
|
+
.filter(Boolean)
|
|
294
|
+
.join(' ')
|
|
295
|
+
}
|
|
296
|
+
return undefined
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function extractPreviewWidth(content: string): number | undefined {
|
|
300
|
+
const match = content.match(/^---\n\/\*\*\s*([\s\S]*?)\s*\*\//)
|
|
301
|
+
if (match?.[1]) {
|
|
302
|
+
const widthMatch = match[1].match(/@previewWidth\s+(\d+)/)
|
|
303
|
+
if (widthMatch?.[1]) {
|
|
304
|
+
return parseInt(widthMatch[1], 10)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return undefined
|
|
308
|
+
}
|