@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,475 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export interface SourceLocation {
|
|
5
|
+
file: string
|
|
6
|
+
line: number
|
|
7
|
+
snippet?: string
|
|
8
|
+
type?: 'static' | 'variable' | 'prop' | 'computed'
|
|
9
|
+
variableName?: string
|
|
10
|
+
definitionLine?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface VariableReference {
|
|
14
|
+
name: string
|
|
15
|
+
pattern: string
|
|
16
|
+
definitionLine: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Find source file and line number for text content
|
|
21
|
+
*/
|
|
22
|
+
export async function findSourceLocation(
|
|
23
|
+
textContent: string,
|
|
24
|
+
tag: string,
|
|
25
|
+
): Promise<SourceLocation | undefined> {
|
|
26
|
+
const srcDir = path.join(process.cwd(), 'src')
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const searchDirs = [
|
|
30
|
+
path.join(srcDir, 'components'),
|
|
31
|
+
path.join(srcDir, 'pages'),
|
|
32
|
+
path.join(srcDir, 'layouts'),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
for (const dir of searchDirs) {
|
|
36
|
+
try {
|
|
37
|
+
const result = await searchDirectory(dir, textContent, tag)
|
|
38
|
+
if (result) {
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Directory doesn't exist, continue
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// If not found directly, try searching for prop values in parent components
|
|
47
|
+
for (const dir of searchDirs) {
|
|
48
|
+
try {
|
|
49
|
+
const result = await searchForPropInParents(dir, textContent, tag)
|
|
50
|
+
if (result) {
|
|
51
|
+
return result
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Directory doesn't exist, continue
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Search failed
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Recursively search directory for matching content
|
|
66
|
+
*/
|
|
67
|
+
async function searchDirectory(
|
|
68
|
+
dir: string,
|
|
69
|
+
textContent: string,
|
|
70
|
+
tag: string,
|
|
71
|
+
): Promise<SourceLocation | undefined> {
|
|
72
|
+
try {
|
|
73
|
+
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
74
|
+
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const fullPath = path.join(dir, entry.name)
|
|
77
|
+
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
const result = await searchDirectory(fullPath, textContent, tag)
|
|
80
|
+
if (result) return result
|
|
81
|
+
} else if (entry.isFile() && entry.name.endsWith('.astro')) {
|
|
82
|
+
const result = await searchAstroFile(fullPath, textContent, tag)
|
|
83
|
+
if (result) return result
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// Error reading directory
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Search a single Astro file for matching content
|
|
95
|
+
*/
|
|
96
|
+
async function searchAstroFile(
|
|
97
|
+
filePath: string,
|
|
98
|
+
textContent: string,
|
|
99
|
+
tag: string,
|
|
100
|
+
): Promise<SourceLocation | undefined> {
|
|
101
|
+
try {
|
|
102
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
103
|
+
const lines = content.split('\n')
|
|
104
|
+
|
|
105
|
+
const cleanText = cleanTextForSearch(textContent)
|
|
106
|
+
const textPreview = cleanText.slice(0, Math.min(30, cleanText.length))
|
|
107
|
+
|
|
108
|
+
// Extract variable references from frontmatter
|
|
109
|
+
const variableRefs = extractVariableReferences(content, cleanText)
|
|
110
|
+
|
|
111
|
+
// Collect all potential matches with scores and metadata
|
|
112
|
+
const matches: Array<{
|
|
113
|
+
line: number
|
|
114
|
+
score: number
|
|
115
|
+
type: 'static' | 'variable' | 'prop' | 'computed'
|
|
116
|
+
variableName?: string
|
|
117
|
+
definitionLine?: number
|
|
118
|
+
}> = []
|
|
119
|
+
|
|
120
|
+
// Search for tag usage with matching text or variable
|
|
121
|
+
for (let i = 0; i < lines.length; i++) {
|
|
122
|
+
const line = lines[i]?.trim().toLowerCase()
|
|
123
|
+
|
|
124
|
+
// Look for opening tag
|
|
125
|
+
if (line?.includes(`<${tag.toLowerCase()}`) && !line.startsWith(`</${tag.toLowerCase()}`)) {
|
|
126
|
+
// Collect content from this line and next few lines
|
|
127
|
+
const section = collectSection(lines, i, 5)
|
|
128
|
+
const sectionText = section.toLowerCase()
|
|
129
|
+
const sectionTextOnly = stripHtmlTags(section).toLowerCase()
|
|
130
|
+
|
|
131
|
+
let score = 0
|
|
132
|
+
let matched = false
|
|
133
|
+
|
|
134
|
+
// Check for variable reference match (highest priority)
|
|
135
|
+
if (variableRefs.length > 0) {
|
|
136
|
+
for (const varRef of variableRefs) {
|
|
137
|
+
// Check case-insensitively since sectionText is lowercased
|
|
138
|
+
if (sectionText.includes(`{`) && sectionText.includes(varRef.name.toLowerCase())) {
|
|
139
|
+
score = 100
|
|
140
|
+
matched = true
|
|
141
|
+
// Store match metadata - this is the USAGE line, we need DEFINITION line
|
|
142
|
+
matches.push({
|
|
143
|
+
line: i + 1,
|
|
144
|
+
score,
|
|
145
|
+
type: 'variable',
|
|
146
|
+
variableName: varRef.name,
|
|
147
|
+
definitionLine: varRef.definitionLine,
|
|
148
|
+
})
|
|
149
|
+
break
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check for direct text match (static content)
|
|
155
|
+
if (!matched && cleanText.length > 10 && sectionTextOnly.includes(textPreview)) {
|
|
156
|
+
// Score based on how much of the text matches
|
|
157
|
+
const matchLength = Math.min(cleanText.length, sectionTextOnly.length)
|
|
158
|
+
score = 50 + (matchLength / cleanText.length) * 40
|
|
159
|
+
matched = true
|
|
160
|
+
matches.push({ line: i + 1, score, type: 'static' })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check for short exact text match (static content)
|
|
164
|
+
if (!matched && cleanText.length > 0 && cleanText.length <= 10 && sectionTextOnly.includes(cleanText)) {
|
|
165
|
+
score = 80
|
|
166
|
+
matched = true
|
|
167
|
+
matches.push({ line: i + 1, score, type: 'static' })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Try matching first few words for longer text (static content)
|
|
171
|
+
if (!matched && cleanText.length > 20) {
|
|
172
|
+
const firstWords = cleanText.split(' ').slice(0, 3).join(' ')
|
|
173
|
+
if (firstWords && sectionTextOnly.includes(firstWords)) {
|
|
174
|
+
score = 40
|
|
175
|
+
matched = true
|
|
176
|
+
matches.push({ line: i + 1, score, type: 'static' })
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Return the best match (highest score)
|
|
183
|
+
if (matches.length > 0) {
|
|
184
|
+
const bestMatch = matches.reduce((best, current) => current.score > best.score ? current : best)
|
|
185
|
+
|
|
186
|
+
// Determine the editable line (definition for variables, usage for static)
|
|
187
|
+
const editableLine = bestMatch.type === 'variable' && bestMatch.definitionLine
|
|
188
|
+
? bestMatch.definitionLine
|
|
189
|
+
: bestMatch.line
|
|
190
|
+
|
|
191
|
+
// Get the complete source snippet (multi-line for static, single line for variables)
|
|
192
|
+
let snippet: string
|
|
193
|
+
if (bestMatch.type === 'static') {
|
|
194
|
+
// For static content, extract the complete tag content with indentation
|
|
195
|
+
snippet = extractCompleteTagSnippet(lines, editableLine - 1, tag)
|
|
196
|
+
} else {
|
|
197
|
+
// For variables/props, just the definition line with indentation
|
|
198
|
+
snippet = lines[editableLine - 1] || ''
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
file: path.relative(process.cwd(), filePath),
|
|
203
|
+
line: editableLine,
|
|
204
|
+
snippet,
|
|
205
|
+
type: bestMatch.type,
|
|
206
|
+
variableName: bestMatch.variableName,
|
|
207
|
+
definitionLine: bestMatch.type === 'variable' ? bestMatch.definitionLine : undefined,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// Error reading file
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return undefined
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Search for prop values passed to components
|
|
219
|
+
*/
|
|
220
|
+
async function searchForPropInParents(
|
|
221
|
+
dir: string,
|
|
222
|
+
textContent: string,
|
|
223
|
+
tag: string,
|
|
224
|
+
): Promise<SourceLocation | undefined> {
|
|
225
|
+
try {
|
|
226
|
+
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
227
|
+
const cleanText = cleanTextForSearch(textContent)
|
|
228
|
+
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
const fullPath = path.join(dir, entry.name)
|
|
231
|
+
|
|
232
|
+
if (entry.isDirectory()) {
|
|
233
|
+
const result = await searchForPropInParents(fullPath, textContent, tag)
|
|
234
|
+
if (result) return result
|
|
235
|
+
} else if (entry.isFile() && entry.name.endsWith('.astro')) {
|
|
236
|
+
const content = await fs.readFile(fullPath, 'utf-8')
|
|
237
|
+
const lines = content.split('\n')
|
|
238
|
+
|
|
239
|
+
// Look for component tags with prop values matching our text
|
|
240
|
+
for (let i = 0; i < lines.length; i++) {
|
|
241
|
+
const line = lines[i]
|
|
242
|
+
|
|
243
|
+
// Match component usage like <ComponentName propName="value" />
|
|
244
|
+
const componentMatch = line?.match(/<([A-Z]\w+)/)
|
|
245
|
+
if (!componentMatch) continue
|
|
246
|
+
|
|
247
|
+
// Collect only the opening tag (until first > or />), not nested content
|
|
248
|
+
let openingTag = ''
|
|
249
|
+
let endLine = i
|
|
250
|
+
for (let j = i; j < Math.min(i + 10, lines.length); j++) {
|
|
251
|
+
openingTag += ' ' + lines[j]
|
|
252
|
+
endLine = j
|
|
253
|
+
|
|
254
|
+
// Stop at the end of opening tag (either /> or >)
|
|
255
|
+
if (lines[j]?.includes('/>')) {
|
|
256
|
+
// Self-closing tag
|
|
257
|
+
break
|
|
258
|
+
} else if (lines[j]?.includes('>')) {
|
|
259
|
+
// Opening tag ends here, don't include nested content
|
|
260
|
+
// Truncate to just the opening tag part
|
|
261
|
+
const tagEndIndex = openingTag.indexOf('>')
|
|
262
|
+
if (tagEndIndex !== -1) {
|
|
263
|
+
openingTag = openingTag.substring(0, tagEndIndex + 1)
|
|
264
|
+
}
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Extract all prop values from the opening tag only
|
|
270
|
+
const propMatches = openingTag.matchAll(/(\w+)=["']([^"']+)["']/g)
|
|
271
|
+
for (const match of propMatches) {
|
|
272
|
+
const propName = match[1]
|
|
273
|
+
const propValue = match[2]
|
|
274
|
+
|
|
275
|
+
if (!propValue) {
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const normalizedValue = normalizeText(propValue)
|
|
280
|
+
|
|
281
|
+
if (normalizedValue === cleanText) {
|
|
282
|
+
// Find which line actually contains this prop
|
|
283
|
+
let propLine = i
|
|
284
|
+
let propLineIndex = i
|
|
285
|
+
|
|
286
|
+
for (let k = i; k <= endLine; k++) {
|
|
287
|
+
const line = lines[k]
|
|
288
|
+
if (!line) {
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (propName && line.includes(propName) && line.includes(propValue)) {
|
|
293
|
+
propLine = k
|
|
294
|
+
propLineIndex = k
|
|
295
|
+
break
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Extract complete component tag starting from where the component tag opens
|
|
300
|
+
const componentSnippetLines: string[] = []
|
|
301
|
+
for (let k = i; k <= endLine; k++) {
|
|
302
|
+
const line = lines[k]
|
|
303
|
+
if (!line) {
|
|
304
|
+
continue
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
componentSnippetLines.push(line)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const propSnippet = componentSnippetLines.join('\n')
|
|
311
|
+
|
|
312
|
+
// Found the prop being passed with our text value
|
|
313
|
+
return {
|
|
314
|
+
file: path.relative(process.cwd(), fullPath),
|
|
315
|
+
line: propLine + 1,
|
|
316
|
+
snippet: propSnippet,
|
|
317
|
+
type: 'prop',
|
|
318
|
+
variableName: propName,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
// Error reading directory
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return undefined
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Extract complete tag snippet including content and indentation
|
|
334
|
+
*/
|
|
335
|
+
function extractCompleteTagSnippet(lines: string[], startLine: number, tag: string): string {
|
|
336
|
+
const snippetLines: string[] = []
|
|
337
|
+
let depth = 0
|
|
338
|
+
let foundClosing = false
|
|
339
|
+
|
|
340
|
+
// Start from the opening tag line
|
|
341
|
+
for (let i = startLine; i < Math.min(startLine + 20, lines.length); i++) {
|
|
342
|
+
const line = lines[i]
|
|
343
|
+
|
|
344
|
+
if (!line) {
|
|
345
|
+
continue
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
snippetLines.push(line)
|
|
349
|
+
|
|
350
|
+
// Count opening and closing tags
|
|
351
|
+
const openTags = (line.match(new RegExp(`<${tag}[\\s>]`, 'gi')) || []).length
|
|
352
|
+
const selfClosing = (line.match(new RegExp(`<${tag}[^>]*/>`, 'gi')) || []).length
|
|
353
|
+
const closeTags = (line.match(new RegExp(`</${tag}>`, 'gi')) || []).length
|
|
354
|
+
|
|
355
|
+
depth += openTags - selfClosing - closeTags
|
|
356
|
+
|
|
357
|
+
// If we found a self-closing tag or closed all tags, we're done
|
|
358
|
+
if (selfClosing > 0 || (depth <= 0 && (closeTags > 0 || openTags > 0))) {
|
|
359
|
+
foundClosing = true
|
|
360
|
+
break
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// If we didn't find closing tag, just return the first line
|
|
365
|
+
if (!foundClosing && snippetLines.length > 1) {
|
|
366
|
+
return snippetLines[0]!
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return snippetLines.join('\n')
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Extract variable references from frontmatter
|
|
374
|
+
*/
|
|
375
|
+
function extractVariableReferences(content: string, targetText: string): VariableReference[] {
|
|
376
|
+
const refs: VariableReference[] = []
|
|
377
|
+
const frontmatterEnd = content.indexOf('---', 3)
|
|
378
|
+
|
|
379
|
+
if (frontmatterEnd <= 0) return refs
|
|
380
|
+
|
|
381
|
+
const frontmatter = content.substring(0, frontmatterEnd)
|
|
382
|
+
const lines = frontmatter.split('\n')
|
|
383
|
+
|
|
384
|
+
for (const line of lines) {
|
|
385
|
+
const trimmed = line.trim()
|
|
386
|
+
|
|
387
|
+
// Match quoted text (handling escaped quotes)
|
|
388
|
+
// Try single quotes with escaped quotes
|
|
389
|
+
let quotedMatch = trimmed.match(/'((?:[^'\\]|\\.)*)'/)
|
|
390
|
+
if (!quotedMatch) {
|
|
391
|
+
// Try double quotes with escaped quotes
|
|
392
|
+
quotedMatch = trimmed.match(/"((?:[^"\\]|\\.)*)"/)
|
|
393
|
+
}
|
|
394
|
+
if (!quotedMatch) {
|
|
395
|
+
// Try backticks (template literals) - but only if no ${} interpolation
|
|
396
|
+
const backtickMatch = trimmed.match(/`([^`]*)`/)
|
|
397
|
+
if (backtickMatch && !backtickMatch[1]?.includes('${')) {
|
|
398
|
+
quotedMatch = backtickMatch
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!quotedMatch?.[1]) continue
|
|
402
|
+
|
|
403
|
+
const value = normalizeText(quotedMatch[1])
|
|
404
|
+
const normalizedTarget = normalizeText(targetText)
|
|
405
|
+
|
|
406
|
+
if (value !== normalizedTarget) continue
|
|
407
|
+
|
|
408
|
+
// Try to extract variable name and line number
|
|
409
|
+
const lineNumber = lines.indexOf(line) + 1
|
|
410
|
+
|
|
411
|
+
// Pattern 1: Object property "key: 'value'"
|
|
412
|
+
const propMatch = trimmed.match(/(\w+)\s*:\s*['"`]/)
|
|
413
|
+
if (propMatch?.[1]) {
|
|
414
|
+
refs.push({
|
|
415
|
+
name: propMatch[1],
|
|
416
|
+
pattern: `{.*${propMatch[1]}`,
|
|
417
|
+
definitionLine: lineNumber,
|
|
418
|
+
})
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Pattern 2: Variable declaration "const name = 'value'"
|
|
423
|
+
const varMatch = trimmed.match(/(?:const|let|var)\s+(\w+)(?:\s*:\s*\w+)?\s*=/)
|
|
424
|
+
if (varMatch?.[1]) {
|
|
425
|
+
refs.push({
|
|
426
|
+
name: varMatch[1],
|
|
427
|
+
pattern: `{${varMatch[1]}}`,
|
|
428
|
+
definitionLine: lineNumber,
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return refs
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Collect text from multiple lines
|
|
438
|
+
*/
|
|
439
|
+
function collectSection(lines: string[], startLine: number, numLines: number): string {
|
|
440
|
+
let text = ''
|
|
441
|
+
for (let i = startLine; i < Math.min(startLine + numLines, lines.length); i++) {
|
|
442
|
+
text += ' ' + lines[i]?.trim().replace(/\s+/g, ' ')
|
|
443
|
+
}
|
|
444
|
+
return text
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Strip HTML tags from text
|
|
449
|
+
*/
|
|
450
|
+
function stripHtmlTags(text: string): string {
|
|
451
|
+
return text.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Normalize text for comparison (handles escaping and entities)
|
|
456
|
+
*/
|
|
457
|
+
function normalizeText(text: string): string {
|
|
458
|
+
return text
|
|
459
|
+
.trim()
|
|
460
|
+
.replace(/\\'/g, "'") // Escaped single quotes
|
|
461
|
+
.replace(/\\"/g, '"') // Escaped double quotes
|
|
462
|
+
.replace(/'/g, "'") // HTML entity for apostrophe
|
|
463
|
+
.replace(/"/g, '"') // HTML entity for quote
|
|
464
|
+
.replace(/'/g, "'") // HTML entity for apostrophe (alternative)
|
|
465
|
+
.replace(/&/g, '&') // HTML entity for ampersand
|
|
466
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
467
|
+
.toLowerCase()
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Clean text for search comparison
|
|
472
|
+
*/
|
|
473
|
+
function cleanTextForSearch(text: string): string {
|
|
474
|
+
return normalizeText(text)
|
|
475
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface CmsMarkerOptions {
|
|
2
|
+
attributeName?: string
|
|
3
|
+
includeTags?: string[] | null
|
|
4
|
+
excludeTags?: string[]
|
|
5
|
+
includeEmptyText?: boolean
|
|
6
|
+
generateManifest?: boolean
|
|
7
|
+
manifestFile?: string
|
|
8
|
+
markComponents?: boolean
|
|
9
|
+
componentDirs?: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ComponentProp {
|
|
13
|
+
name: string
|
|
14
|
+
type: string
|
|
15
|
+
required: boolean
|
|
16
|
+
defaultValue?: string
|
|
17
|
+
description?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ComponentDefinition {
|
|
21
|
+
name: string
|
|
22
|
+
file: string
|
|
23
|
+
props: ComponentProp[]
|
|
24
|
+
description?: string
|
|
25
|
+
slots?: string[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ManifestEntry {
|
|
29
|
+
id: string
|
|
30
|
+
file: string
|
|
31
|
+
tag: string
|
|
32
|
+
text: string
|
|
33
|
+
sourcePath?: string
|
|
34
|
+
sourceLine?: number
|
|
35
|
+
sourceSnippet?: string
|
|
36
|
+
sourceType?: 'static' | 'variable' | 'prop' | 'computed'
|
|
37
|
+
variableName?: string
|
|
38
|
+
childCmsIds?: string[]
|
|
39
|
+
parentComponentId?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ComponentInstance {
|
|
43
|
+
id: string
|
|
44
|
+
componentName: string
|
|
45
|
+
file: string
|
|
46
|
+
sourcePath: string
|
|
47
|
+
sourceLine: number
|
|
48
|
+
props: Record<string, any>
|
|
49
|
+
slots?: Record<string, string>
|
|
50
|
+
parentId?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CmsManifest {
|
|
54
|
+
entries: Record<string, ManifestEntry>
|
|
55
|
+
components: Record<string, ComponentInstance>
|
|
56
|
+
componentDefinitions: Record<string, ComponentDefinition>
|
|
57
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Plugin } from 'vite'
|
|
2
|
+
import type { ManifestWriter } from './manifest-writer'
|
|
3
|
+
import type { CmsMarkerOptions, ComponentDefinition } from './types'
|
|
4
|
+
import { createAstroTransformPlugin } from './astro-transform'
|
|
5
|
+
|
|
6
|
+
export interface VitePluginContext {
|
|
7
|
+
manifestWriter: ManifestWriter
|
|
8
|
+
componentDefinitions: Record<string, ComponentDefinition>
|
|
9
|
+
config: Required<CmsMarkerOptions>
|
|
10
|
+
idCounter: { value: number }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createVitePlugin(context: VitePluginContext): Plugin[] {
|
|
14
|
+
const { manifestWriter, componentDefinitions, config } = context
|
|
15
|
+
|
|
16
|
+
const virtualManifestPlugin: Plugin = {
|
|
17
|
+
name: 'cms-marker-virtual-manifest',
|
|
18
|
+
resolveId(id) {
|
|
19
|
+
if (id === '/@cms/manifest' || id === 'virtual:cms-manifest') {
|
|
20
|
+
return '\0virtual:cms-manifest'
|
|
21
|
+
}
|
|
22
|
+
if (id === '/@cms/components' || id === 'virtual:cms-components') {
|
|
23
|
+
return '\0virtual:cms-components'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
load(id) {
|
|
27
|
+
if (id === '\0virtual:cms-manifest') {
|
|
28
|
+
return `export default ${JSON.stringify(manifestWriter.getGlobalManifest())};`
|
|
29
|
+
}
|
|
30
|
+
if (id === '\0virtual:cms-components') {
|
|
31
|
+
return `export default ${JSON.stringify(componentDefinitions)};`
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Create the Astro transform plugin to inject source location attributes
|
|
37
|
+
const astroTransformPlugin = createAstroTransformPlugin({
|
|
38
|
+
markComponents: config.markComponents,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Note: We cannot use transformIndexHtml for static Astro builds because
|
|
42
|
+
// Astro generates HTML files directly without going through Vite's HTML pipeline.
|
|
43
|
+
// HTML processing is done in build-processor.ts after pages are generated.
|
|
44
|
+
return [astroTransformPlugin, virtualManifestPlugin]
|
|
45
|
+
}
|