@nuasite/cms 0.2.2 → 0.3.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.
Files changed (55) hide show
  1. package/README.md +81 -73
  2. package/dist/src/build-processor.d.ts.map +1 -1
  3. package/dist/src/component-registry.d.ts +6 -2
  4. package/dist/src/component-registry.d.ts.map +1 -1
  5. package/dist/src/dev-middleware.d.ts.map +1 -1
  6. package/dist/src/editor/api.d.ts +14 -0
  7. package/dist/src/editor/api.d.ts.map +1 -1
  8. package/dist/src/editor/components/ai-chat.d.ts.map +1 -1
  9. package/dist/src/editor/components/block-editor.d.ts.map +1 -1
  10. package/dist/src/editor/components/color-toolbar.d.ts.map +1 -1
  11. package/dist/src/editor/components/editable-highlights.d.ts.map +1 -1
  12. package/dist/src/editor/components/outline.d.ts.map +1 -1
  13. package/dist/src/editor/constants.d.ts +1 -0
  14. package/dist/src/editor/constants.d.ts.map +1 -1
  15. package/dist/src/editor/dom.d.ts +9 -0
  16. package/dist/src/editor/dom.d.ts.map +1 -1
  17. package/dist/src/editor/editor.d.ts.map +1 -1
  18. package/dist/src/editor/history.d.ts.map +1 -1
  19. package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -1
  20. package/dist/src/editor/index.d.ts.map +1 -1
  21. package/dist/src/editor/storage.d.ts +2 -0
  22. package/dist/src/editor/storage.d.ts.map +1 -1
  23. package/dist/src/handlers/array-ops.d.ts +59 -0
  24. package/dist/src/handlers/array-ops.d.ts.map +1 -0
  25. package/dist/src/handlers/component-ops.d.ts +26 -0
  26. package/dist/src/handlers/component-ops.d.ts.map +1 -1
  27. package/dist/src/index.d.ts.map +1 -1
  28. package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -1
  29. package/dist/src/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +1 -1
  31. package/src/build-processor.ts +27 -0
  32. package/src/component-registry.ts +125 -76
  33. package/src/dev-middleware.ts +85 -16
  34. package/src/editor/api.ts +72 -0
  35. package/src/editor/components/ai-chat.tsx +0 -1
  36. package/src/editor/components/block-editor.tsx +92 -17
  37. package/src/editor/components/color-toolbar.tsx +7 -1
  38. package/src/editor/components/editable-highlights.tsx +4 -1
  39. package/src/editor/components/outline.tsx +11 -6
  40. package/src/editor/constants.ts +1 -0
  41. package/src/editor/dom.ts +46 -1
  42. package/src/editor/editor.ts +5 -2
  43. package/src/editor/history.ts +1 -6
  44. package/src/editor/hooks/useBlockEditorHandlers.ts +86 -29
  45. package/src/editor/index.tsx +24 -8
  46. package/src/editor/storage.ts +24 -0
  47. package/src/handlers/array-ops.ts +452 -0
  48. package/src/handlers/component-ops.ts +269 -18
  49. package/src/handlers/markdown-ops.ts +7 -4
  50. package/src/handlers/request-utils.ts +1 -1
  51. package/src/handlers/source-writer.ts +4 -5
  52. package/src/index.ts +15 -10
  53. package/src/manifest-writer.ts +1 -1
  54. package/src/source-finder/cross-file-tracker.ts +1 -1
  55. package/src/source-finder/search-index.ts +1 -1
@@ -32,7 +32,6 @@ import {
32
32
  toggleShowOriginal,
33
33
  } from './editor'
34
34
  import { performRedo, performUndo } from './history'
35
- import CMS_STYLES from './styles.css?inline'
36
35
  import {
37
36
  useAIHandlers,
38
37
  useBlockEditorHandlers,
@@ -52,7 +51,8 @@ import {
52
51
  updateSettings,
53
52
  } from './signals'
54
53
  import * as signals from './signals'
55
- import { hasPendingEntryNavigation, loadSettingsFromStorage, saveSettingsToStorage } from './storage'
54
+ import { hasPendingEntryNavigation, loadEditingState, loadSettingsFromStorage, saveSettingsToStorage } from './storage'
55
+ import CMS_STYLES from './styles.css?inline'
56
56
  import { generateCSSVariables, resolveTheme } from './themes'
57
57
 
58
58
  const CmsUI = () => {
@@ -80,6 +80,13 @@ const CmsUI = () => {
80
80
  }).catch(() => {})
81
81
  }, [])
82
82
 
83
+ // Auto-restore edit mode if it was active before a page refresh (e.g. after save triggers HMR)
84
+ useEffect(() => {
85
+ if (loadEditingState() && !signals.isEditing.value) {
86
+ startEditMode(config, updateUI)
87
+ }
88
+ }, [config, updateUI])
89
+
83
90
  // Auto-open markdown editor when there's a pending entry navigation from collections browser
84
91
  useEffect(() => {
85
92
  if (hasPendingEntryNavigation()) {
@@ -178,12 +185,21 @@ const CmsUI = () => {
178
185
  }, [])
179
186
 
180
187
  // Color toolbar handlers
181
- const handleColorToolbarChange = useCallback((colorType: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText', oldClass: string, newClass: string, previousClassName: string, previousStyleCssText: string) => {
182
- const targetId = signals.colorEditorState.value.targetElementId
183
- if (!targetId) return
184
-
185
- handleColorChange(config, targetId, colorType, oldClass, newClass, updateUI, previousClassName, previousStyleCssText)
186
- }, [config, updateUI])
188
+ const handleColorToolbarChange = useCallback(
189
+ (
190
+ colorType: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText',
191
+ oldClass: string,
192
+ newClass: string,
193
+ previousClassName: string,
194
+ previousStyleCssText: string,
195
+ ) => {
196
+ const targetId = signals.colorEditorState.value.targetElementId
197
+ if (!targetId) return
198
+
199
+ handleColorChange(config, targetId, colorType, oldClass, newClass, updateUI, previousClassName, previousStyleCssText)
200
+ },
201
+ [config, updateUI],
202
+ )
187
203
 
188
204
  const handleColorToolbarClose = useCallback(() => {
189
205
  signals.closeColorEditor()
@@ -254,6 +254,30 @@ export function loadPendingEntryNavigation(): PendingEntryNavigation | null {
254
254
  }
255
255
  }
256
256
 
257
+ // ============================================================================
258
+ // Editing State (persist edit mode across HMR/refresh)
259
+ // ============================================================================
260
+
261
+ export function saveEditingState(isEditing: boolean): void {
262
+ try {
263
+ if (isEditing) {
264
+ sessionStorage.setItem(STORAGE_KEYS.IS_EDITING, '1')
265
+ } else {
266
+ sessionStorage.removeItem(STORAGE_KEYS.IS_EDITING)
267
+ }
268
+ } catch (e) {
269
+ console.warn('[CMS] Failed to save editing state:', e)
270
+ }
271
+ }
272
+
273
+ export function loadEditingState(): boolean {
274
+ try {
275
+ return sessionStorage.getItem(STORAGE_KEYS.IS_EDITING) === '1'
276
+ } catch {
277
+ return false
278
+ }
279
+ }
280
+
257
281
  // ============================================================================
258
282
  // Clear All
259
283
  // ============================================================================
@@ -0,0 +1,452 @@
1
+ import { parse as parseBabel } from '@babel/parser'
2
+ import fs from 'node:fs/promises'
3
+ import { getProjectRoot } from '../config'
4
+ import type { ManifestWriter } from '../manifest-writer'
5
+ import type { CmsManifest, ComponentInstance } from '../types'
6
+ import { acquireFileLock, normalizePagePath, resolveAndValidatePath } from '../utils'
7
+ import {
8
+ findComponentInvocationFile,
9
+ findComponentInvocationLine,
10
+ findFrontmatterEnd,
11
+ getComponentOccurrenceIndex,
12
+ getIndentation,
13
+ normalizeFilePath,
14
+ } from './component-ops'
15
+
16
+ export interface AddArrayItemRequest {
17
+ referenceComponentId: string
18
+ position: 'before' | 'after'
19
+ props: Record<string, unknown>
20
+ meta?: { source: string; url: string }
21
+ }
22
+
23
+ export interface AddArrayItemResponse {
24
+ success: boolean
25
+ message?: string
26
+ sourceFile?: string
27
+ error?: string
28
+ }
29
+
30
+ export interface RemoveArrayItemRequest {
31
+ componentId: string
32
+ meta?: { source: string; url: string }
33
+ }
34
+
35
+ export interface RemoveArrayItemResponse {
36
+ success: boolean
37
+ message?: string
38
+ sourceFile?: string
39
+ error?: string
40
+ }
41
+
42
+ /**
43
+ * Scan backwards from a component invocation line to find a `.map(` pattern
44
+ * and extract the array variable name.
45
+ *
46
+ * Looks for patterns like:
47
+ * {packages.map((pkg) => <PackageCard {...pkg} />)}
48
+ * {items.map(item => (
49
+ */
50
+ export function detectArrayPattern(
51
+ lines: string[],
52
+ invocationLineIndex: number,
53
+ ): { arrayVarName: string; mapLineIndex: number } | null {
54
+ // Search up to 5 lines above (the `.map(` may be on the same line or a few lines above)
55
+ const searchStart = Math.max(0, invocationLineIndex - 5)
56
+ for (let i = invocationLineIndex; i >= searchStart; i--) {
57
+ const line = lines[i]!
58
+ // Match patterns like: {varName.map( or varName.map(
59
+ const match = line.match(/\{?\s*(\w+)\.map\s*\(/)
60
+ if (match) {
61
+ return { arrayVarName: match[1]!, mapLineIndex: i }
62
+ }
63
+ }
64
+ return null
65
+ }
66
+
67
+ interface ArrayElementBounds {
68
+ startLine: number
69
+ endLine: number
70
+ }
71
+
72
+ /**
73
+ * Parse frontmatter with Babel, walk the AST to find the array variable declaration,
74
+ * and return the line bounds of each element.
75
+ *
76
+ * @param frontmatterContent - The raw frontmatter code (between --- delimiters)
77
+ * @param frontmatterStartLine - The 0-indexed line where frontmatter content starts in the file
78
+ * (line after the opening `---`)
79
+ * @param arrayVarName - The variable name of the array to find
80
+ */
81
+ export function findArrayDeclaration(
82
+ frontmatterContent: string,
83
+ frontmatterStartLine: number,
84
+ arrayVarName: string,
85
+ ): ArrayElementBounds[] | null {
86
+ let ast: ReturnType<typeof parseBabel>
87
+ try {
88
+ ast = parseBabel(frontmatterContent, {
89
+ sourceType: 'module',
90
+ plugins: ['typescript'],
91
+ errorRecovery: true,
92
+ })
93
+ } catch {
94
+ return null
95
+ }
96
+
97
+ // Walk the top-level statements to find the array declaration
98
+ for (const node of ast.program.body) {
99
+ // Handle: const foo = [...]
100
+ if (node.type === 'VariableDeclaration') {
101
+ for (const decl of node.declarations) {
102
+ if (
103
+ decl.id.type === 'Identifier'
104
+ && decl.id.name === arrayVarName
105
+ && decl.init?.type === 'ArrayExpression'
106
+ ) {
107
+ return extractElementBounds(decl.init.elements, frontmatterStartLine)
108
+ }
109
+ }
110
+ }
111
+ // Handle: export const foo = [...]
112
+ if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'VariableDeclaration') {
113
+ for (const decl of node.declaration.declarations) {
114
+ if (
115
+ decl.id.type === 'Identifier'
116
+ && decl.id.name === arrayVarName
117
+ && decl.init?.type === 'ArrayExpression'
118
+ ) {
119
+ return extractElementBounds(decl.init.elements, frontmatterStartLine)
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ return null
126
+ }
127
+
128
+ function extractElementBounds(
129
+ elements: any[],
130
+ frontmatterStartLine: number,
131
+ ): ArrayElementBounds[] {
132
+ const bounds: ArrayElementBounds[] = []
133
+ for (const el of elements) {
134
+ if (el && el.loc) {
135
+ bounds.push({
136
+ // Babel loc is 1-indexed; convert to 0-indexed file lines
137
+ startLine: el.loc.start.line - 1 + frontmatterStartLine,
138
+ endLine: el.loc.end.line - 1 + frontmatterStartLine,
139
+ })
140
+ }
141
+ }
142
+ return bounds
143
+ }
144
+
145
+ /**
146
+ * Resolve the file, lines, invocation index, and array info for a component.
147
+ */
148
+ async function resolveArrayContext(
149
+ component: ComponentInstance,
150
+ manifest: CmsManifest,
151
+ pageUrl: string,
152
+ ) {
153
+ const projectRoot = getProjectRoot()
154
+
155
+ const invocation = await findComponentInvocationFile(
156
+ projectRoot,
157
+ pageUrl,
158
+ manifest,
159
+ component,
160
+ )
161
+
162
+ const filePath = invocation?.filePath
163
+ ?? normalizeFilePath(component.invocationSourcePath ?? component.sourcePath)
164
+
165
+ const fullPath = resolveAndValidatePath(filePath)
166
+ const content = await fs.readFile(fullPath, 'utf-8')
167
+ const lines = content.split('\n')
168
+
169
+ let refLineIndex: number
170
+ if (invocation) {
171
+ refLineIndex = invocation.lineIndex
172
+ } else {
173
+ const occurrenceIndex = getComponentOccurrenceIndex(manifest, component)
174
+ refLineIndex = findComponentInvocationLine(lines, component.componentName, occurrenceIndex)
175
+ }
176
+
177
+ if (refLineIndex < 0 || refLineIndex >= lines.length) {
178
+ return null
179
+ }
180
+
181
+ const pattern = detectArrayPattern(lines, refLineIndex)
182
+ if (!pattern) {
183
+ return null
184
+ }
185
+
186
+ // Extract frontmatter content
187
+ const fmEnd = findFrontmatterEnd(lines)
188
+ if (fmEnd === 0) return null // No frontmatter
189
+
190
+ // frontmatterStartLine is the line after the opening ---
191
+ const frontmatterStartLine = 1 // Line 0 is `---`, line 1 is first content line
192
+ const frontmatterContent = lines.slice(1, fmEnd - 1).join('\n')
193
+
194
+ const elementBounds = findArrayDeclaration(
195
+ frontmatterContent,
196
+ frontmatterStartLine,
197
+ pattern.arrayVarName,
198
+ )
199
+
200
+ if (!elementBounds || elementBounds.length === 0) {
201
+ return null
202
+ }
203
+
204
+ // Determine which array element this component corresponds to.
205
+ // The invocationIndex tells us the Nth occurrence of this component in the template,
206
+ // which maps directly to the Nth array element.
207
+ const occurrenceIndex = getComponentOccurrenceIndex(manifest, component)
208
+ // Count only components with the same name AND same invocationSourcePath to get array index
209
+ const sameSourceComponents = Object.values(manifest.components)
210
+ .filter(c =>
211
+ c.componentName === component.componentName
212
+ && c.invocationSourcePath === component.invocationSourcePath
213
+ )
214
+ const arrayIndex = sameSourceComponents.findIndex(c => c.id === component.id)
215
+
216
+ if (arrayIndex < 0 || arrayIndex >= elementBounds.length) {
217
+ return null
218
+ }
219
+
220
+ return {
221
+ filePath,
222
+ fullPath,
223
+ lines,
224
+ content,
225
+ elementBounds,
226
+ arrayIndex,
227
+ frontmatterContent,
228
+ frontmatterStartLine,
229
+ arrayVarName: pattern.arrayVarName,
230
+ occurrenceIndex,
231
+ }
232
+ }
233
+
234
+ export async function handleRemoveArrayItem(
235
+ request: RemoveArrayItemRequest,
236
+ manifestWriter: ManifestWriter,
237
+ ): Promise<RemoveArrayItemResponse> {
238
+ const { componentId, meta } = request
239
+
240
+ if (!meta?.url) {
241
+ return { success: false, error: 'Page URL is required in meta' }
242
+ }
243
+
244
+ const pagePath = normalizePagePath(meta.url)
245
+ const pageData = manifestWriter.getPageManifest(pagePath)
246
+ if (!pageData) {
247
+ return { success: false, error: 'Page manifest not found' }
248
+ }
249
+
250
+ const manifest: CmsManifest = {
251
+ entries: pageData.entries,
252
+ components: pageData.components,
253
+ componentDefinitions: manifestWriter.getComponentDefinitions(),
254
+ }
255
+
256
+ const component = manifest.components[componentId]
257
+ if (!component) {
258
+ return { success: false, error: `Component '${componentId}' not found in manifest` }
259
+ }
260
+
261
+ try {
262
+ const ctx = await resolveArrayContext(component, manifest, meta.url)
263
+ if (!ctx) {
264
+ return { success: false, error: 'Could not detect array pattern for this component' }
265
+ }
266
+
267
+ const { fullPath, lines, elementBounds, arrayIndex } = ctx
268
+
269
+ const release = await acquireFileLock(fullPath)
270
+ try {
271
+ // Re-read the file to avoid stale data
272
+ const freshContent = await fs.readFile(fullPath, 'utf-8')
273
+ const freshLines = freshContent.split('\n')
274
+
275
+ const bounds = elementBounds[arrayIndex]!
276
+ let removeStart = bounds.startLine
277
+ let removeEnd = bounds.endLine
278
+
279
+ // Clean up trailing comma on the line after the element, or leading comma
280
+ // Check if there's a trailing comma after the element's end line
281
+ const afterEndLine = freshLines[removeEnd]
282
+ if (afterEndLine !== undefined) {
283
+ // If the element's end line has a trailing comma, it'll be removed with the element
284
+ // But we also need to handle the case where the PREVIOUS element's trailing comma
285
+ // now becomes the last element (remove its trailing comma)
286
+ }
287
+
288
+ // Check line after removeEnd for a comma-only or blank line
289
+ if (removeEnd + 1 < freshLines.length && freshLines[removeEnd + 1]!.trim() === '') {
290
+ removeEnd++
291
+ }
292
+
293
+ // Remove the element lines
294
+ freshLines.splice(removeStart, removeEnd - removeStart + 1)
295
+
296
+ // Clean up: if the previous element now ends with a trailing comma
297
+ // and there's a closing bracket right after, remove the comma
298
+ if (removeStart > 0 && removeStart <= freshLines.length) {
299
+ const prevLine = freshLines[removeStart - 1]!
300
+ const nextLine = freshLines[removeStart]
301
+ if (nextLine !== undefined && nextLine.trim().startsWith(']') && prevLine.trimEnd().endsWith(',')) {
302
+ freshLines[removeStart - 1] = prevLine.replace(/,\s*$/, '')
303
+ }
304
+ }
305
+
306
+ await fs.writeFile(fullPath, freshLines.join('\n'), 'utf-8')
307
+
308
+ return {
309
+ success: true,
310
+ message: `Successfully removed array item (${component.componentName} at index ${arrayIndex})`,
311
+ sourceFile: ctx.filePath,
312
+ }
313
+ } finally {
314
+ release()
315
+ }
316
+ } catch (error) {
317
+ const message = error instanceof Error ? error.message : String(error)
318
+ return { success: false, error: message }
319
+ }
320
+ }
321
+
322
+ export async function handleAddArrayItem(
323
+ request: AddArrayItemRequest,
324
+ manifestWriter: ManifestWriter,
325
+ ): Promise<AddArrayItemResponse> {
326
+ const { referenceComponentId, position, props, meta } = request
327
+
328
+ if (!meta?.url) {
329
+ return { success: false, error: 'Page URL is required in meta' }
330
+ }
331
+
332
+ const pagePath = normalizePagePath(meta.url)
333
+ const pageData = manifestWriter.getPageManifest(pagePath)
334
+ if (!pageData) {
335
+ return { success: false, error: 'Page manifest not found' }
336
+ }
337
+
338
+ const manifest: CmsManifest = {
339
+ entries: pageData.entries,
340
+ components: pageData.components,
341
+ componentDefinitions: manifestWriter.getComponentDefinitions(),
342
+ }
343
+
344
+ const referenceComponent = manifest.components[referenceComponentId]
345
+ if (!referenceComponent) {
346
+ return { success: false, error: `Reference component '${referenceComponentId}' not found in manifest` }
347
+ }
348
+
349
+ try {
350
+ const ctx = await resolveArrayContext(referenceComponent, manifest, meta.url)
351
+ if (!ctx) {
352
+ return { success: false, error: 'Could not detect array pattern for this component' }
353
+ }
354
+
355
+ const { fullPath, elementBounds, arrayIndex } = ctx
356
+
357
+ const release = await acquireFileLock(fullPath)
358
+ try {
359
+ const freshContent = await fs.readFile(fullPath, 'utf-8')
360
+ const freshLines = freshContent.split('\n')
361
+
362
+ const refBounds = elementBounds[arrayIndex]!
363
+
364
+ // Generate JS object literal from props
365
+ const newElement = generateObjectLiteral(props)
366
+
367
+ // Get indentation from the reference element
368
+ const indentation = getIndentation(freshLines[refBounds.startLine]!)
369
+
370
+ // Indent the new element
371
+ const indentedLines = newElement
372
+ .split('\n')
373
+ .map((line, i) => {
374
+ if (i === 0) return indentation + line
375
+ if (line.trim()) return indentation + line
376
+ return line
377
+ })
378
+ .join('\n')
379
+
380
+ if (position === 'before') {
381
+ // Insert before the reference element
382
+ const insertLine = refBounds.startLine
383
+ freshLines.splice(insertLine, 0, indentedLines + ',')
384
+ } else {
385
+ // Insert after the reference element
386
+ const insertLine = refBounds.endLine + 1
387
+ // Ensure the reference element has a trailing comma
388
+ const refEndLine = freshLines[refBounds.endLine]!
389
+ if (!refEndLine.trimEnd().endsWith(',')) {
390
+ freshLines[refBounds.endLine] = refEndLine.replace(/(\s*)$/, ',$1')
391
+ }
392
+ freshLines.splice(insertLine, 0, indentedLines + ',')
393
+ }
394
+
395
+ // Clean up trailing comma before closing bracket
396
+ // Find the closing ] and remove comma from the last element
397
+ for (let i = freshLines.length - 1; i >= 0; i--) {
398
+ if (freshLines[i]!.trim().startsWith(']')) {
399
+ const prev = freshLines[i - 1]
400
+ if (prev && prev.trimEnd().endsWith(',')) {
401
+ // Check if this is the array we're editing by scanning backwards
402
+ // to find the array variable
403
+ freshLines[i - 1] = prev.replace(/,(\s*)$/, '$1')
404
+ }
405
+ break
406
+ }
407
+ }
408
+
409
+ await fs.writeFile(fullPath, freshLines.join('\n'), 'utf-8')
410
+
411
+ return {
412
+ success: true,
413
+ message: `Successfully added array item ${position} index ${arrayIndex}`,
414
+ sourceFile: ctx.filePath,
415
+ }
416
+ } finally {
417
+ release()
418
+ }
419
+ } catch (error) {
420
+ const message = error instanceof Error ? error.message : String(error)
421
+ return { success: false, error: message }
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Generate a JavaScript object literal string from props.
427
+ * Example: { name: 'Components', slug: 'components' }
428
+ */
429
+ function generateObjectLiteral(props: Record<string, unknown>): string {
430
+ const entries = Object.entries(props)
431
+ if (entries.length === 0) return '{}'
432
+
433
+ const parts = entries.map(([key, value]) => {
434
+ const safeKey = /^[a-zA-Z_$]\w*$/.test(key) ? key : `'${key}'`
435
+ return `${safeKey}: ${formatValue(value)}`
436
+ })
437
+
438
+ if (parts.length <= 3 && parts.join(', ').length < 60) {
439
+ return `{ ${parts.join(', ')} }`
440
+ }
441
+
442
+ return `{\n\t${parts.join(',\n\t')},\n}`
443
+ }
444
+
445
+ function formatValue(value: unknown): string {
446
+ if (typeof value === 'string') return `'${value.replace(/'/g, "\\'")}'`
447
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
448
+ if (value === null || value === undefined) return 'undefined'
449
+ if (Array.isArray(value)) return `[${value.map(formatValue).join(', ')}]`
450
+ if (typeof value === 'object') return generateObjectLiteral(value as Record<string, unknown>)
451
+ return String(value)
452
+ }