@nuasite/cms-marker 0.0.42
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 +240 -0
- package/package.json +42 -0
- package/src/astro-transform.ts +193 -0
- package/src/build-processor.ts +164 -0
- package/src/component-registry.ts +382 -0
- package/src/dev-middleware.ts +183 -0
- package/src/html-processor.ts +359 -0
- package/src/index.ts +91 -0
- package/src/manifest-writer.ts +153 -0
- package/src/source-finder.ts +475 -0
- package/src/tsconfig.json +6 -0
- package/src/types.ts +57 -0
- package/src/vite-plugin.ts +45 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import type { ComponentDefinition, ComponentProp } from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scans Astro component files and extracts their definitions including props
|
|
7
|
+
*/
|
|
8
|
+
export class ComponentRegistry {
|
|
9
|
+
private components: Map<string, ComponentDefinition> = new Map()
|
|
10
|
+
private componentDirs: string[]
|
|
11
|
+
|
|
12
|
+
constructor(componentDirs: string[] = ['src/components']) {
|
|
13
|
+
this.componentDirs = componentDirs
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Scan all component directories and build the registry
|
|
18
|
+
*/
|
|
19
|
+
async scan(): Promise<void> {
|
|
20
|
+
for (const dir of this.componentDirs) {
|
|
21
|
+
const fullPath = path.join(process.cwd(), dir)
|
|
22
|
+
try {
|
|
23
|
+
await this.scanDirectory(fullPath, dir)
|
|
24
|
+
} catch {
|
|
25
|
+
// Directory doesn't exist, skip
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get all registered components
|
|
32
|
+
*/
|
|
33
|
+
getComponents(): Record<string, ComponentDefinition> {
|
|
34
|
+
return Object.fromEntries(this.components)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get a specific component by name
|
|
39
|
+
*/
|
|
40
|
+
getComponent(name: string): ComponentDefinition | undefined {
|
|
41
|
+
return this.components.get(name)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Scan a directory recursively for .astro files
|
|
46
|
+
*/
|
|
47
|
+
private async scanDirectory(dir: string, relativePath: string): Promise<void> {
|
|
48
|
+
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
49
|
+
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
const fullPath = path.join(dir, entry.name)
|
|
52
|
+
const relPath = path.join(relativePath, entry.name)
|
|
53
|
+
|
|
54
|
+
if (entry.isDirectory()) {
|
|
55
|
+
await this.scanDirectory(fullPath, relPath)
|
|
56
|
+
} else if (entry.isFile() && entry.name.endsWith('.astro')) {
|
|
57
|
+
await this.parseComponent(fullPath, relPath)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse a single Astro component file
|
|
64
|
+
*/
|
|
65
|
+
private async parseComponent(filePath: string, relativePath: string): Promise<void> {
|
|
66
|
+
try {
|
|
67
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
68
|
+
const componentName = path.basename(filePath, '.astro')
|
|
69
|
+
|
|
70
|
+
const props = await this.extractProps(content)
|
|
71
|
+
const slots = this.extractSlots(content)
|
|
72
|
+
const description = this.extractDescription(content)
|
|
73
|
+
|
|
74
|
+
this.components.set(componentName, {
|
|
75
|
+
name: componentName,
|
|
76
|
+
file: relativePath,
|
|
77
|
+
props,
|
|
78
|
+
slots: slots.length > 0 ? slots : undefined,
|
|
79
|
+
description,
|
|
80
|
+
})
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.warn(`[ComponentRegistry] Failed to parse ${filePath}:`, error)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse Props content and extract individual property definitions
|
|
88
|
+
* Handles multi-line properties with nested types
|
|
89
|
+
*/
|
|
90
|
+
private parsePropsContent(propsContent: string): ComponentProp[] {
|
|
91
|
+
const props: ComponentProp[] = []
|
|
92
|
+
let i = 0
|
|
93
|
+
const content = propsContent.trim()
|
|
94
|
+
|
|
95
|
+
while (i < content.length) {
|
|
96
|
+
// Skip whitespace and newlines
|
|
97
|
+
while (i < content.length && /\s/.test(content[i])) i++
|
|
98
|
+
if (i >= content.length) break
|
|
99
|
+
|
|
100
|
+
// Skip comments
|
|
101
|
+
if (content[i] === '/' && content[i + 1] === '/') {
|
|
102
|
+
// Skip to end of line
|
|
103
|
+
while (i < content.length && content[i] !== '\n') i++
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (content[i] === '/' && content[i + 1] === '*') {
|
|
108
|
+
// Skip block comment
|
|
109
|
+
while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/')) i++
|
|
110
|
+
i += 2
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Extract property name
|
|
115
|
+
const nameStart = i
|
|
116
|
+
while (i < content.length && /\w/.test(content[i])) i++
|
|
117
|
+
const name = content.substring(nameStart, i)
|
|
118
|
+
|
|
119
|
+
if (!name) break
|
|
120
|
+
|
|
121
|
+
// Skip whitespace
|
|
122
|
+
while (i < content.length && /\s/.test(content[i])) i++
|
|
123
|
+
|
|
124
|
+
// Check for optional marker
|
|
125
|
+
const optional = content[i] === '?'
|
|
126
|
+
if (optional) i++
|
|
127
|
+
|
|
128
|
+
// Skip whitespace
|
|
129
|
+
while (i < content.length && /\s/.test(content[i])) i++
|
|
130
|
+
|
|
131
|
+
// Expect colon
|
|
132
|
+
if (content[i] !== ':') break
|
|
133
|
+
i++
|
|
134
|
+
|
|
135
|
+
// Skip whitespace
|
|
136
|
+
while (i < content.length && /\s/.test(content[i])) i++
|
|
137
|
+
|
|
138
|
+
// Extract type (up to semicolon, handling nested braces)
|
|
139
|
+
const typeStart = i
|
|
140
|
+
let braceDepth = 0
|
|
141
|
+
let angleDepth = 0
|
|
142
|
+
while (i < content.length) {
|
|
143
|
+
if (content[i] === '{') braceDepth++
|
|
144
|
+
else if (content[i] === '}') braceDepth--
|
|
145
|
+
else if (content[i] === '<') angleDepth++
|
|
146
|
+
else if (content[i] === '>') angleDepth--
|
|
147
|
+
else if (content[i] === ';' && braceDepth === 0 && angleDepth === 0) break
|
|
148
|
+
i++
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const type = content.substring(typeStart, i).trim()
|
|
152
|
+
|
|
153
|
+
// Skip the semicolon
|
|
154
|
+
if (content[i] === ';') i++
|
|
155
|
+
|
|
156
|
+
// Skip whitespace
|
|
157
|
+
while (i < content.length && /[ \t]/.test(content[i])) i++
|
|
158
|
+
|
|
159
|
+
// Check for inline comment
|
|
160
|
+
let description: string | undefined
|
|
161
|
+
if (content[i] === '/' && content[i + 1] === '/') {
|
|
162
|
+
i += 2
|
|
163
|
+
const commentStart = i
|
|
164
|
+
while (i < content.length && content[i] !== '\n') i++
|
|
165
|
+
description = content.substring(commentStart, i).trim()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (name && type) {
|
|
169
|
+
props.push({
|
|
170
|
+
name,
|
|
171
|
+
type,
|
|
172
|
+
required: !optional,
|
|
173
|
+
description,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return props
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Extract content between balanced braces after a pattern match
|
|
183
|
+
* Properly handles nested objects
|
|
184
|
+
*/
|
|
185
|
+
private extractBalancedBraces(text: string, pattern: RegExp): string | null {
|
|
186
|
+
const match = text.match(pattern)
|
|
187
|
+
if (!match || match.index === undefined) return null
|
|
188
|
+
|
|
189
|
+
// Find the opening brace position (right after the match)
|
|
190
|
+
const startIndex = match.index + match[0].length
|
|
191
|
+
let depth = 1 // We already have one opening brace
|
|
192
|
+
let i = startIndex
|
|
193
|
+
|
|
194
|
+
// Find the matching closing brace
|
|
195
|
+
while (i < text.length && depth > 0) {
|
|
196
|
+
if (text[i] === '{') {
|
|
197
|
+
depth++
|
|
198
|
+
} else if (text[i] === '}') {
|
|
199
|
+
depth--
|
|
200
|
+
}
|
|
201
|
+
i++
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (depth !== 0) return null // Unbalanced braces
|
|
205
|
+
|
|
206
|
+
// Extract content between braces (excluding the braces themselves)
|
|
207
|
+
return text.substring(startIndex, i - 1)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extract props from component frontmatter
|
|
212
|
+
*/
|
|
213
|
+
private async extractProps(content: string): Promise<ComponentProp[]> {
|
|
214
|
+
const props: ComponentProp[] = []
|
|
215
|
+
|
|
216
|
+
// Find the frontmatter section
|
|
217
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
218
|
+
if (!frontmatterMatch) return props
|
|
219
|
+
|
|
220
|
+
const frontmatter = frontmatterMatch[1]
|
|
221
|
+
|
|
222
|
+
// Look for Props interface
|
|
223
|
+
const propsInterfaceContent = this.extractBalancedBraces(frontmatter, /interface\s+Props\s*\{/)
|
|
224
|
+
if (propsInterfaceContent) {
|
|
225
|
+
const extractedProps = this.parsePropsContent(propsInterfaceContent)
|
|
226
|
+
props.push(...extractedProps)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Look for type Props = { ... }
|
|
230
|
+
if (props.length === 0) {
|
|
231
|
+
const typePropsContent = this.extractBalancedBraces(frontmatter, /type\s+Props\s*=\s*\{/)
|
|
232
|
+
if (typePropsContent) {
|
|
233
|
+
const extractedProps = this.parsePropsContent(typePropsContent)
|
|
234
|
+
props.push(...extractedProps)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const destructureMatch = frontmatter?.match(/const\s*\{([^}]+)\}\s*=\s*Astro\.props/)
|
|
239
|
+
if (destructureMatch) {
|
|
240
|
+
const destructureContent = destructureMatch[1]
|
|
241
|
+
|
|
242
|
+
const defaultMatches = destructureContent?.matchAll(/(\w+)\s*=\s*(['"`]?)([^'"`},]+)\2/g) ?? []
|
|
243
|
+
for (const match of defaultMatches) {
|
|
244
|
+
const propName = match[1]
|
|
245
|
+
const defaultValue = match[3]
|
|
246
|
+
const existingProp = props.find(p => p.name === propName)
|
|
247
|
+
if (existingProp) {
|
|
248
|
+
existingProp.defaultValue = defaultValue
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return props
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Parse a single prop line from interface/type
|
|
258
|
+
*/
|
|
259
|
+
private parsePropLine(line: string): ComponentProp | null {
|
|
260
|
+
const trimmed = line.trim()
|
|
261
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*')) return null
|
|
262
|
+
|
|
263
|
+
// Match: propName?: type; or propName: type;
|
|
264
|
+
const match = trimmed.match(/^(\w+)(\?)?:\s*([^;]+);?\s*(\/\/.*)?$/)
|
|
265
|
+
if (!match) return null
|
|
266
|
+
|
|
267
|
+
const [, name, optional, typeStr, comment] = match
|
|
268
|
+
|
|
269
|
+
if (!name || !typeStr) return null
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
name,
|
|
273
|
+
type: typeStr?.trim(),
|
|
274
|
+
required: !optional,
|
|
275
|
+
description: comment ? comment.replace(/^\/\/\s*/, '').trim() : undefined,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Extract slot names from template
|
|
281
|
+
*/
|
|
282
|
+
private extractSlots(content: string): string[] {
|
|
283
|
+
const slots: string[] = []
|
|
284
|
+
|
|
285
|
+
// Find <slot> elements with name attribute
|
|
286
|
+
const slotMatches = content.matchAll(/<slot\s+name=["']([^"']+)["']/g)
|
|
287
|
+
for (const match of slotMatches) {
|
|
288
|
+
if (match[1]) {
|
|
289
|
+
slots.push(match[1])
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check for default slot (unnamed slot) - match any <slot> tag without a name attribute
|
|
294
|
+
const allSlotTags = content.matchAll(/<slot(?:\s+[^>]*)?\s*\/?>/g)
|
|
295
|
+
for (const match of allSlotTags) {
|
|
296
|
+
const tag = match[0]
|
|
297
|
+
// Check if this slot tag doesn't have a name attribute
|
|
298
|
+
if (!/name\s*=/.test(tag)) {
|
|
299
|
+
if (!slots.includes('default')) {
|
|
300
|
+
slots.unshift('default')
|
|
301
|
+
}
|
|
302
|
+
break // Only need to find one default slot
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return slots
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Extract component description from JSDoc comment
|
|
311
|
+
*/
|
|
312
|
+
private extractDescription(content: string): string | undefined {
|
|
313
|
+
// Look for JSDoc comment at the start of frontmatter
|
|
314
|
+
const match = content.match(/^---\n\/\*\*\s*([\s\S]*?)\s*\*\//)
|
|
315
|
+
if (match?.[1]) {
|
|
316
|
+
return match[1]
|
|
317
|
+
.split('\n')
|
|
318
|
+
.map(line => line.replace(/^\s*\*\s?/, '').trim())
|
|
319
|
+
.filter(Boolean)
|
|
320
|
+
.join(' ')
|
|
321
|
+
}
|
|
322
|
+
return undefined
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Parse component usage in an Astro file to extract prop values
|
|
328
|
+
*/
|
|
329
|
+
export function parseComponentUsage(
|
|
330
|
+
content: string,
|
|
331
|
+
componentName: string,
|
|
332
|
+
): Array<{ line: number; props: Record<string, string> }> {
|
|
333
|
+
const usages: Array<{ line: number; props: Record<string, string> }> = []
|
|
334
|
+
const lines = content.split('\n')
|
|
335
|
+
|
|
336
|
+
// Match component usage: <ComponentName prop="value" />
|
|
337
|
+
const componentRegex = new RegExp(
|
|
338
|
+
`<${componentName}\\s+([^>]*?)\\s*\\/?>`,
|
|
339
|
+
'g',
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
let lineIndex = 0
|
|
343
|
+
let charIndex = 0
|
|
344
|
+
|
|
345
|
+
for (let i = 0; i < lines.length; i++) {
|
|
346
|
+
const line = lines[i]
|
|
347
|
+
const lineMatches = line?.matchAll(new RegExp(componentRegex.source, 'g')) || []
|
|
348
|
+
|
|
349
|
+
for (const match of lineMatches) {
|
|
350
|
+
const propsString = match[1]
|
|
351
|
+
const props = parsePropsString(propsString)
|
|
352
|
+
|
|
353
|
+
usages.push({
|
|
354
|
+
line: i + 1,
|
|
355
|
+
props,
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return usages
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Parse props string from component tag
|
|
365
|
+
*/
|
|
366
|
+
function parsePropsString(propsString?: string): Record<string, string> {
|
|
367
|
+
const props: Record<string, string> = {}
|
|
368
|
+
|
|
369
|
+
// Match prop="value" or prop={expression} or prop (boolean)
|
|
370
|
+
const propMatches = propsString?.matchAll(
|
|
371
|
+
/(\w+)(?:=(?:["']([^"']*)["']|\{([^}]*)\}))?/g,
|
|
372
|
+
) || []
|
|
373
|
+
|
|
374
|
+
for (const match of propMatches) {
|
|
375
|
+
const [, name, stringValue, expressionValue] = match
|
|
376
|
+
if (name) {
|
|
377
|
+
props[name] = stringValue ?? expressionValue ?? 'true'
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return props
|
|
382
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { ViteDevServer } from 'vite'
|
|
2
|
+
import { processHtml } from './html-processor'
|
|
3
|
+
import type { ManifestWriter } from './manifest-writer'
|
|
4
|
+
import type { CmsMarkerOptions, ComponentDefinition } from './types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the normalized page path from a URL
|
|
8
|
+
* For example: /about/ -> /about
|
|
9
|
+
* /about -> /about
|
|
10
|
+
* / -> /
|
|
11
|
+
*/
|
|
12
|
+
function normalizePagePath(url: string): string {
|
|
13
|
+
// Remove query string and hash
|
|
14
|
+
let pagePath = url.split('?')[0]?.split('#')[0] ?? ''
|
|
15
|
+
// Remove trailing slash (but keep root /)
|
|
16
|
+
if (pagePath.length > 1 && pagePath.endsWith('/')) {
|
|
17
|
+
pagePath = pagePath.slice(0, -1)
|
|
18
|
+
}
|
|
19
|
+
return pagePath || '/'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createDevMiddleware(
|
|
23
|
+
server: ViteDevServer,
|
|
24
|
+
config: Required<CmsMarkerOptions>,
|
|
25
|
+
manifestWriter: ManifestWriter,
|
|
26
|
+
componentDefinitions: Record<string, ComponentDefinition>,
|
|
27
|
+
idCounter: { value: number },
|
|
28
|
+
) {
|
|
29
|
+
// Serve global CMS manifest (component definitions and settings)
|
|
30
|
+
server.middlewares.use((req, res, next) => {
|
|
31
|
+
if (req.url === '/cms-manifest.json') {
|
|
32
|
+
res.setHeader('Content-Type', 'application/json')
|
|
33
|
+
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
34
|
+
res.end(JSON.stringify(
|
|
35
|
+
{
|
|
36
|
+
componentDefinitions,
|
|
37
|
+
},
|
|
38
|
+
null,
|
|
39
|
+
2,
|
|
40
|
+
))
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
next()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Serve per-page manifest endpoints (e.g., /about.json for /about page)
|
|
47
|
+
server.middlewares.use((req, res, next) => {
|
|
48
|
+
const url = req.url || ''
|
|
49
|
+
|
|
50
|
+
// Match /*.json pattern (but not files that actually exist)
|
|
51
|
+
const match = url.match(/^\/(.*)\.json$/)
|
|
52
|
+
if (match) {
|
|
53
|
+
// Convert manifest path to page path
|
|
54
|
+
// e.g., /about.json -> /about
|
|
55
|
+
// /index.json -> /
|
|
56
|
+
// /blog/post.json -> /blog/post
|
|
57
|
+
let pagePath = '/' + match[1]
|
|
58
|
+
if (pagePath === '/index') {
|
|
59
|
+
pagePath = '/'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const pageData = manifestWriter.getPageManifest(pagePath)
|
|
63
|
+
|
|
64
|
+
// Only serve if we have manifest data for this page
|
|
65
|
+
if (pageData) {
|
|
66
|
+
res.setHeader('Content-Type', 'application/json')
|
|
67
|
+
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
68
|
+
res.end(JSON.stringify(
|
|
69
|
+
{
|
|
70
|
+
page: pagePath,
|
|
71
|
+
entries: pageData.entries,
|
|
72
|
+
components: pageData.components,
|
|
73
|
+
componentDefinitions,
|
|
74
|
+
},
|
|
75
|
+
null,
|
|
76
|
+
2,
|
|
77
|
+
))
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
next()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Transform HTML responses
|
|
85
|
+
server.middlewares.use((req, res, next) => {
|
|
86
|
+
const originalWrite = res.write
|
|
87
|
+
const originalEnd = res.end
|
|
88
|
+
const chunks: Buffer[] = []
|
|
89
|
+
const requestUrl = req.url || 'unknown'
|
|
90
|
+
|
|
91
|
+
// Intercept response chunks
|
|
92
|
+
res.write = function(chunk: any, ...args: any[]) {
|
|
93
|
+
if (chunk) {
|
|
94
|
+
chunks.push(Buffer.from(chunk))
|
|
95
|
+
}
|
|
96
|
+
return true
|
|
97
|
+
} as any
|
|
98
|
+
|
|
99
|
+
res.end = function(chunk: any, ...args: any[]) {
|
|
100
|
+
if (chunk) {
|
|
101
|
+
chunks.push(Buffer.from(chunk))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if this is an HTML response
|
|
105
|
+
const contentType = res.getHeader('content-type')
|
|
106
|
+
if (contentType && typeof contentType === 'string' && contentType.includes('text/html')) {
|
|
107
|
+
const html = Buffer.concat(chunks).toString('utf8')
|
|
108
|
+
const pagePath = normalizePagePath(requestUrl)
|
|
109
|
+
|
|
110
|
+
// Process HTML asynchronously
|
|
111
|
+
processHtmlForDev(html, pagePath, config, idCounter)
|
|
112
|
+
.then(({ html: transformed, entries, components }) => {
|
|
113
|
+
// Store in manifest writer
|
|
114
|
+
manifestWriter.addPage(pagePath, entries, components)
|
|
115
|
+
|
|
116
|
+
// Restore original methods and send transformed HTML
|
|
117
|
+
res.write = originalWrite
|
|
118
|
+
res.end = originalEnd
|
|
119
|
+
|
|
120
|
+
return res.end(transformed, ...args)
|
|
121
|
+
})
|
|
122
|
+
.catch((error) => {
|
|
123
|
+
console.error('[astro-cms-marker] Error transforming HTML:', error)
|
|
124
|
+
|
|
125
|
+
// Restore original methods and send original content
|
|
126
|
+
res.write = originalWrite
|
|
127
|
+
res.end = originalEnd
|
|
128
|
+
|
|
129
|
+
if (chunks.length > 0) {
|
|
130
|
+
return res.end(Buffer.concat(chunks), ...args)
|
|
131
|
+
}
|
|
132
|
+
return res.end(...args)
|
|
133
|
+
})
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Restore original methods and send original content
|
|
138
|
+
res.write = originalWrite
|
|
139
|
+
res.end = originalEnd
|
|
140
|
+
|
|
141
|
+
if (chunks.length > 0) {
|
|
142
|
+
return res.end(Buffer.concat(chunks), ...args)
|
|
143
|
+
}
|
|
144
|
+
return res.end(...args)
|
|
145
|
+
} as any
|
|
146
|
+
|
|
147
|
+
next()
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function processHtmlForDev(
|
|
152
|
+
html: string,
|
|
153
|
+
pagePath: string,
|
|
154
|
+
config: Required<CmsMarkerOptions>,
|
|
155
|
+
idCounter: { value: number },
|
|
156
|
+
) {
|
|
157
|
+
// In dev mode, reset counter per page for consistent IDs during HMR
|
|
158
|
+
let pageCounter = 0
|
|
159
|
+
const idGenerator = () => `cms-${pageCounter++}`
|
|
160
|
+
|
|
161
|
+
const result = await processHtml(
|
|
162
|
+
html,
|
|
163
|
+
pagePath,
|
|
164
|
+
{
|
|
165
|
+
attributeName: config.attributeName,
|
|
166
|
+
includeTags: config.includeTags,
|
|
167
|
+
excludeTags: config.excludeTags,
|
|
168
|
+
includeEmptyText: config.includeEmptyText,
|
|
169
|
+
generateManifest: config.generateManifest,
|
|
170
|
+
markComponents: config.markComponents,
|
|
171
|
+
componentDirs: config.componentDirs,
|
|
172
|
+
},
|
|
173
|
+
idGenerator,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
// In dev mode, we use the source info from Astro compiler attributes
|
|
177
|
+
// which is already extracted by html-processor, so no need to call findSourceLocation
|
|
178
|
+
return {
|
|
179
|
+
html: result.html,
|
|
180
|
+
entries: result.entries,
|
|
181
|
+
components: result.components,
|
|
182
|
+
}
|
|
183
|
+
}
|