@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.
Files changed (54) hide show
  1. package/dist/types/collection-scanner.d.ts +12 -0
  2. package/dist/types/collection-scanner.d.ts.map +1 -0
  3. package/dist/types/component-registry.d.ts +15 -0
  4. package/dist/types/component-registry.d.ts.map +1 -0
  5. package/dist/types/content-config-ast.d.ts +45 -0
  6. package/dist/types/content-config-ast.d.ts.map +1 -0
  7. package/dist/types/core.d.ts +44 -0
  8. package/dist/types/core.d.ts.map +1 -0
  9. package/dist/types/fs/glob.d.ts +3 -0
  10. package/dist/types/fs/glob.d.ts.map +1 -0
  11. package/dist/types/fs/node-fs.d.ts +7 -0
  12. package/dist/types/fs/node-fs.d.ts.map +1 -0
  13. package/dist/types/fs/types.d.ts +33 -0
  14. package/dist/types/fs/types.d.ts.map +1 -0
  15. package/dist/types/handlers/entry-ops.d.ts +69 -0
  16. package/dist/types/handlers/entry-ops.d.ts.map +1 -0
  17. package/dist/types/handlers/page-ops.d.ts +14 -0
  18. package/dist/types/handlers/page-ops.d.ts.map +1 -0
  19. package/dist/types/handlers/redirect-ops.d.ts +10 -0
  20. package/dist/types/handlers/redirect-ops.d.ts.map +1 -0
  21. package/dist/types/index.d.ts +12 -0
  22. package/dist/types/index.d.ts.map +1 -0
  23. package/dist/types/media/contember.d.ts +18 -0
  24. package/dist/types/media/contember.d.ts.map +1 -0
  25. package/dist/types/media/index.d.ts +5 -0
  26. package/dist/types/media/index.d.ts.map +1 -0
  27. package/dist/types/media/local.d.ts +12 -0
  28. package/dist/types/media/local.d.ts.map +1 -0
  29. package/dist/types/media/project-images.d.ts +15 -0
  30. package/dist/types/media/project-images.d.ts.map +1 -0
  31. package/dist/types/media/s3.d.ts +12 -0
  32. package/dist/types/media/s3.d.ts.map +1 -0
  33. package/dist/types/shared.d.ts +24 -0
  34. package/dist/types/shared.d.ts.map +1 -0
  35. package/dist/types/tsconfig.tsbuildinfo +1 -0
  36. package/package.json +55 -0
  37. package/src/collection-scanner.ts +935 -0
  38. package/src/component-registry.ts +308 -0
  39. package/src/content-config-ast.ts +536 -0
  40. package/src/core.ts +167 -0
  41. package/src/fs/glob.ts +32 -0
  42. package/src/fs/node-fs.ts +138 -0
  43. package/src/fs/types.ts +26 -0
  44. package/src/handlers/entry-ops.ts +528 -0
  45. package/src/handlers/page-ops.ts +203 -0
  46. package/src/handlers/redirect-ops.ts +139 -0
  47. package/src/index.ts +41 -0
  48. package/src/media/contember.ts +90 -0
  49. package/src/media/index.ts +4 -0
  50. package/src/media/local.ts +147 -0
  51. package/src/media/project-images.ts +82 -0
  52. package/src/media/s3.ts +151 -0
  53. package/src/shared.ts +65 -0
  54. 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
+ }