@nuasite/cms 0.5.0 → 0.6.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 +11 -11
- package/dist/editor.js +4608 -4448
- package/package.json +1 -1
- package/src/build-processor.ts +38 -1
- package/src/dev-middleware.ts +47 -2
- package/src/editor/components/collections-browser.tsx +10 -6
- package/src/editor/components/fields.tsx +7 -7
- package/src/editor/components/frontmatter-fields.tsx +163 -2
- package/src/editor/components/seo-editor.tsx +2 -1
- package/src/editor/components/toast/toast.tsx +19 -2
- package/src/editor/dom.ts +16 -5
- package/src/editor/editor.ts +18 -1
- package/src/editor/index.tsx +4 -2
- package/src/editor/types.ts +2 -0
- package/src/handlers/array-ops.ts +102 -0
- package/src/index.ts +1 -1
- package/src/source-finder/cross-file-tracker.ts +42 -0
- package/src/source-finder/element-finder.ts +18 -4
- package/src/source-finder/snippet-utils.ts +62 -1
- package/src/source-finder/types.ts +3 -0
- package/src/source-finder/variable-extraction.ts +2 -2
|
@@ -142,6 +142,108 @@ function extractElementBounds(
|
|
|
142
142
|
return bounds
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Extract property values from a specific array element in the frontmatter.
|
|
147
|
+
*
|
|
148
|
+
* Parses the frontmatter code with Babel, finds the array variable declaration,
|
|
149
|
+
* and returns the property values from the element at the given index.
|
|
150
|
+
* Used to resolve spread props for array-rendered components (e.g. `{...item}`).
|
|
151
|
+
*/
|
|
152
|
+
export function extractArrayElementProps(
|
|
153
|
+
frontmatterContent: string,
|
|
154
|
+
arrayVarName: string,
|
|
155
|
+
elementIndex: number,
|
|
156
|
+
): Record<string, any> | null {
|
|
157
|
+
let ast: ReturnType<typeof parseBabel>
|
|
158
|
+
try {
|
|
159
|
+
ast = parseBabel(frontmatterContent, {
|
|
160
|
+
sourceType: 'module',
|
|
161
|
+
plugins: ['typescript'],
|
|
162
|
+
errorRecovery: true,
|
|
163
|
+
})
|
|
164
|
+
} catch {
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const node of ast.program.body) {
|
|
169
|
+
const arrayExpr = findArrayExpression(node, arrayVarName)
|
|
170
|
+
if (arrayExpr && elementIndex < arrayExpr.elements.length) {
|
|
171
|
+
const element = arrayExpr.elements[elementIndex]
|
|
172
|
+
if (element?.type === 'ObjectExpression') {
|
|
173
|
+
return extractObjectValues(element, frontmatterContent)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function findArrayExpression(node: any, varName: string): any | null {
|
|
182
|
+
if (node.type === 'VariableDeclaration') {
|
|
183
|
+
for (const decl of node.declarations) {
|
|
184
|
+
if (
|
|
185
|
+
decl.id.type === 'Identifier'
|
|
186
|
+
&& decl.id.name === varName
|
|
187
|
+
&& decl.init?.type === 'ArrayExpression'
|
|
188
|
+
) {
|
|
189
|
+
return decl.init
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'VariableDeclaration') {
|
|
194
|
+
for (const decl of node.declaration.declarations) {
|
|
195
|
+
if (
|
|
196
|
+
decl.id.type === 'Identifier'
|
|
197
|
+
&& decl.id.name === varName
|
|
198
|
+
&& decl.init?.type === 'ArrayExpression'
|
|
199
|
+
) {
|
|
200
|
+
return decl.init
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function extractObjectValues(node: any, source: string): Record<string, any> {
|
|
208
|
+
const props: Record<string, any> = {}
|
|
209
|
+
for (const prop of node.properties) {
|
|
210
|
+
if (prop.type !== 'ObjectProperty') continue
|
|
211
|
+
const key = prop.key.type === 'Identifier' ? prop.key.name : prop.key.value
|
|
212
|
+
if (!key) continue
|
|
213
|
+
props[key] = extractAstValue(prop.value, source)
|
|
214
|
+
}
|
|
215
|
+
return props
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function extractAstValue(node: any, source: string): any {
|
|
219
|
+
switch (node.type) {
|
|
220
|
+
case 'StringLiteral':
|
|
221
|
+
return node.value
|
|
222
|
+
case 'NumericLiteral':
|
|
223
|
+
return node.value
|
|
224
|
+
case 'BooleanLiteral':
|
|
225
|
+
return node.value
|
|
226
|
+
case 'NullLiteral':
|
|
227
|
+
return null
|
|
228
|
+
case 'TemplateLiteral':
|
|
229
|
+
if (node.expressions.length === 0 && node.quasis.length === 1) {
|
|
230
|
+
return node.quasis[0].value.cooked
|
|
231
|
+
}
|
|
232
|
+
return source.slice(node.start, node.end)
|
|
233
|
+
case 'ArrayExpression':
|
|
234
|
+
return node.elements.map((el: any) => el ? extractAstValue(el, source) : null)
|
|
235
|
+
case 'ObjectExpression':
|
|
236
|
+
return extractObjectValues(node, source)
|
|
237
|
+
case 'UnaryExpression':
|
|
238
|
+
if (node.operator === '-' && node.argument.type === 'NumericLiteral') {
|
|
239
|
+
return -node.argument.value
|
|
240
|
+
}
|
|
241
|
+
return source.slice(node.start, node.end)
|
|
242
|
+
default:
|
|
243
|
+
return source.slice(node.start, node.end)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
145
247
|
/**
|
|
146
248
|
* Resolve the file, lines, invocation index, and array info for a component.
|
|
147
249
|
*/
|
package/src/index.ts
CHANGED
|
@@ -93,7 +93,7 @@ export default function nuaCms(options: NuaCmsOptions = {}): AstroIntegration {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
return {
|
|
96
|
-
name: '@nuasite/
|
|
96
|
+
name: '@nuasite/cms',
|
|
97
97
|
hooks: {
|
|
98
98
|
'astro:config:setup': async ({ updateConfig, command, injectScript, logger }) => {
|
|
99
99
|
// --- CMS Marker setup ---
|
|
@@ -158,6 +158,28 @@ async function searchDirForExpressionProp(
|
|
|
158
158
|
// we look for cardProps.title in the definitions
|
|
159
159
|
const spreadPropPath = `${spreadVarName}.${propName}`
|
|
160
160
|
|
|
161
|
+
// When spread is inside a .map() call, search for array element definitions
|
|
162
|
+
// e.g., packages.map(pkg => <Card {...pkg} />) -> look for packages[N].propName
|
|
163
|
+
if (spreadMatch.mapSourceArray) {
|
|
164
|
+
const mapSourceArray = spreadMatch.mapSourceArray
|
|
165
|
+
for (const def of cached.variableDefinitions) {
|
|
166
|
+
if (
|
|
167
|
+
def.name === propName
|
|
168
|
+
&& def.parentName?.startsWith(mapSourceArray + '[')
|
|
169
|
+
&& normalizeText(def.value) === normalizedSearch
|
|
170
|
+
) {
|
|
171
|
+
return {
|
|
172
|
+
file: path.relative(getProjectRoot(), fullPath),
|
|
173
|
+
line: def.line,
|
|
174
|
+
snippet: cached.lines[def.line - 1] || '',
|
|
175
|
+
type: 'variable',
|
|
176
|
+
variableName: buildDefinitionPath(def),
|
|
177
|
+
definitionLine: def.line,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
161
183
|
for (const def of cached.variableDefinitions) {
|
|
162
184
|
const defPath = buildDefinitionPath(def)
|
|
163
185
|
if (defPath === spreadPropPath) {
|
|
@@ -671,6 +693,26 @@ async function searchDirForAttributeProp(
|
|
|
671
693
|
// Try spread prop usage
|
|
672
694
|
const spreadMatch = findSpreadProp(cached.ast, componentName)
|
|
673
695
|
if (spreadMatch) {
|
|
696
|
+
// When spread is inside a .map() call, search for array element definitions
|
|
697
|
+
if (spreadMatch.mapSourceArray) {
|
|
698
|
+
const mapSourceArray = spreadMatch.mapSourceArray
|
|
699
|
+
for (const def of cached.variableDefinitions) {
|
|
700
|
+
if (
|
|
701
|
+
def.name === propName
|
|
702
|
+
&& def.parentName?.startsWith(mapSourceArray + '[')
|
|
703
|
+
) {
|
|
704
|
+
return {
|
|
705
|
+
file: path.relative(getProjectRoot(), fullPath),
|
|
706
|
+
line: def.line,
|
|
707
|
+
snippet: cached.lines[def.line - 1] || '',
|
|
708
|
+
type: 'variable',
|
|
709
|
+
variableName: buildDefinitionPath(def),
|
|
710
|
+
definitionLine: def.line,
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
674
716
|
const spreadPropPath = `${spreadMatch.spreadVarName}.${propName}`
|
|
675
717
|
for (const def of cached.variableDefinitions) {
|
|
676
718
|
const defPath = buildDefinitionPath(def)
|
|
@@ -353,7 +353,7 @@ export function findSpreadProp(
|
|
|
353
353
|
ast: AstroNode,
|
|
354
354
|
componentName: string,
|
|
355
355
|
): SpreadPropMatch | null {
|
|
356
|
-
function visit(node: AstroNode): SpreadPropMatch | null {
|
|
356
|
+
function visit(node: AstroNode, parentExpression: AstroNode | null): SpreadPropMatch | null {
|
|
357
357
|
// Check component nodes matching the name
|
|
358
358
|
if (node.type === 'component') {
|
|
359
359
|
const compNode = node as ComponentNode
|
|
@@ -362,20 +362,34 @@ export function findSpreadProp(
|
|
|
362
362
|
// Check for spread attributes: {...cardProps}
|
|
363
363
|
// In Astro AST: type='attribute', kind='spread', name=variable name
|
|
364
364
|
if (attr.type === 'attribute' && attr.kind === 'spread' && attr.name) {
|
|
365
|
-
|
|
365
|
+
const match: SpreadPropMatch = {
|
|
366
366
|
componentName,
|
|
367
367
|
spreadVarName: attr.name,
|
|
368
368
|
line: attr.position?.start.line ?? compNode.position?.start.line ?? 0,
|
|
369
369
|
}
|
|
370
|
+
|
|
371
|
+
// Check if this spread is inside a .map() call by examining parent expression
|
|
372
|
+
if (parentExpression) {
|
|
373
|
+
const exprText = getTextContent(parentExpression)
|
|
374
|
+
const mapMatch = exprText.match(/(\w+(?:\.\w+)*)\.map\s*\(\s*\(?(\w+)\)?\s*=>/)
|
|
375
|
+
if (mapMatch && mapMatch[2] === attr.name) {
|
|
376
|
+
match.mapSourceArray = mapMatch[1]
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return match
|
|
370
381
|
}
|
|
371
382
|
}
|
|
372
383
|
}
|
|
373
384
|
}
|
|
374
385
|
|
|
386
|
+
// Track the nearest ancestor expression node
|
|
387
|
+
const nextParentExpression = node.type === 'expression' ? node : parentExpression
|
|
388
|
+
|
|
375
389
|
// Recursively visit children
|
|
376
390
|
if ('children' in node && Array.isArray(node.children)) {
|
|
377
391
|
for (const child of node.children) {
|
|
378
|
-
const result = visit(child)
|
|
392
|
+
const result = visit(child, nextParentExpression)
|
|
379
393
|
if (result) return result
|
|
380
394
|
}
|
|
381
395
|
}
|
|
@@ -383,5 +397,5 @@ export function findSpreadProp(
|
|
|
383
397
|
return null
|
|
384
398
|
}
|
|
385
399
|
|
|
386
|
-
return visit(ast)
|
|
400
|
+
return visit(ast, null)
|
|
387
401
|
}
|
|
@@ -6,7 +6,7 @@ import type { Attribute, ManifestEntry } from '../types'
|
|
|
6
6
|
import { escapeRegex, generateSourceHash } from '../utils'
|
|
7
7
|
import { buildDefinitionPath } from './ast-extractors'
|
|
8
8
|
import { getCachedParsedFile } from './ast-parser'
|
|
9
|
-
import { findAttributeSourceLocation } from './cross-file-tracker'
|
|
9
|
+
import { findAttributeSourceLocation, searchForExpressionProp, searchForPropInParents } from './cross-file-tracker'
|
|
10
10
|
import { findImageElementNearLine, findImageSourceLocation } from './image-finder'
|
|
11
11
|
|
|
12
12
|
// ============================================================================
|
|
@@ -640,6 +640,67 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
640
640
|
sourceHash,
|
|
641
641
|
}] as const
|
|
642
642
|
}
|
|
643
|
+
|
|
644
|
+
// Cross-file search for prop-driven dynamic text
|
|
645
|
+
// When text comes from a prop (e.g., {title} where title = Astro.props.title),
|
|
646
|
+
// trace it to where the prop value is actually defined in a parent component
|
|
647
|
+
if (cached) {
|
|
648
|
+
// Extract expression variables from the snippet to find props
|
|
649
|
+
const exprPattern = /\{(\w+(?:\.\w+|\[\d+\])*)\}/g
|
|
650
|
+
let exprMatch: RegExpExecArray | null
|
|
651
|
+
while ((exprMatch = exprPattern.exec(sourceSnippet)) !== null) {
|
|
652
|
+
const exprPath = exprMatch[1]!
|
|
653
|
+
const baseVar = exprPath.match(/^(\w+)/)?.[1]
|
|
654
|
+
if (baseVar && cached.propAliases.has(baseVar)) {
|
|
655
|
+
const propName = cached.propAliases.get(baseVar)!
|
|
656
|
+
const componentFileName = path.basename(filePath)
|
|
657
|
+
const result = await searchForExpressionProp(
|
|
658
|
+
componentFileName, propName, exprPath, entry.text!,
|
|
659
|
+
)
|
|
660
|
+
if (result) {
|
|
661
|
+
const propSnippet = result.snippet ?? trimmedText
|
|
662
|
+
const propSourceHash = generateSourceHash(propSnippet)
|
|
663
|
+
return [id, {
|
|
664
|
+
...entry,
|
|
665
|
+
sourcePath: result.file,
|
|
666
|
+
sourceLine: result.line,
|
|
667
|
+
sourceSnippet: propSnippet,
|
|
668
|
+
variableName: result.variableName,
|
|
669
|
+
attributes,
|
|
670
|
+
colorClasses,
|
|
671
|
+
sourceHash: propSourceHash,
|
|
672
|
+
}] as const
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Search for quoted prop values in parent components
|
|
678
|
+
// (handles <Component title="literal text" />)
|
|
679
|
+
const srcDir = path.join(getProjectRoot(), 'src')
|
|
680
|
+
for (const searchDir of ['pages', 'components', 'layouts']) {
|
|
681
|
+
try {
|
|
682
|
+
const result = await searchForPropInParents(
|
|
683
|
+
path.join(srcDir, searchDir), trimmedText,
|
|
684
|
+
)
|
|
685
|
+
if (result) {
|
|
686
|
+
const parentSnippet = result.snippet ?? trimmedText
|
|
687
|
+
const propSourceHash = generateSourceHash(parentSnippet)
|
|
688
|
+
return [id, {
|
|
689
|
+
...entry,
|
|
690
|
+
sourcePath: result.file,
|
|
691
|
+
sourceLine: result.line,
|
|
692
|
+
sourceSnippet: parentSnippet,
|
|
693
|
+
variableName: result.variableName,
|
|
694
|
+
attributes,
|
|
695
|
+
colorClasses,
|
|
696
|
+
sourceHash: propSourceHash,
|
|
697
|
+
}] as const
|
|
698
|
+
}
|
|
699
|
+
} catch {
|
|
700
|
+
// Directory doesn't exist
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
643
704
|
}
|
|
644
705
|
|
|
645
706
|
// Original static content path
|
|
@@ -184,6 +184,9 @@ export interface SpreadPropMatch {
|
|
|
184
184
|
/** The variable name being spread (e.g., 'cardProps' from {...cardProps}) */
|
|
185
185
|
spreadVarName: string
|
|
186
186
|
line: number
|
|
187
|
+
/** Source array name when spread is inside a .map() call
|
|
188
|
+
* e.g., 'packages' from packages.map((pkg) => <Card {...pkg} />) */
|
|
189
|
+
mapSourceArray?: string
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
export interface ImageMatch {
|
|
@@ -300,8 +300,8 @@ export async function resolveImportPath(source: string, fromFile: string): Promi
|
|
|
300
300
|
for (const ext of extensions) {
|
|
301
301
|
const fullPath = basePath + ext
|
|
302
302
|
try {
|
|
303
|
-
await fs.
|
|
304
|
-
return fullPath
|
|
303
|
+
const stat = await fs.stat(fullPath)
|
|
304
|
+
if (stat.isFile()) return fullPath
|
|
305
305
|
} catch {
|
|
306
306
|
// File doesn't exist with this extension
|
|
307
307
|
}
|