@dfosco/storyboard-react 4.0.0-beta.0 → 4.0.0-beta.10

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.
@@ -155,11 +155,99 @@ function getLastModified(root, dirPath) {
155
155
  }
156
156
  }
157
157
 
158
+ /**
159
+ * Batch-fetch git metadata (author + lastModified) for multiple files in a
160
+ * single subprocess, avoiding per-file git overhead during startup.
161
+ *
162
+ * Returns a Map<absPath, { gitAuthor: string|null, lastModified: string|null }>
163
+ */
164
+ function batchGitMetadata(root, filePaths) {
165
+ const result = new Map()
166
+ if (filePaths.length === 0) return result
167
+
168
+ // Initialize all entries
169
+ for (const fp of filePaths) {
170
+ result.set(fp, { gitAuthor: null, lastModified: null })
171
+ }
172
+
173
+ try {
174
+ // Batch lastModified: one git log call with all paths
175
+ // git log -1 gives the most recent commit touching any of these paths,
176
+ // but we need per-path data. Use --name-only to correlate.
177
+ // For efficiency, use a single git log with --format and --name-only
178
+ // that outputs one record per commit touching these files.
179
+ const allDirs = [...new Set(filePaths.map(fp => path.dirname(fp)))]
180
+ const dirsArg = allDirs.map(d => `"${d}"`).join(' ')
181
+
182
+ // Get lastModified per directory in one call using git log --format
183
+ // We output "MARKER<sep>dir<sep>date" per commit, then take the latest per dir.
184
+ const logResult = execSync(
185
+ `git log --format="%aI" --name-only -- ${dirsArg}`,
186
+ { cwd: root, encoding: 'utf-8', timeout: 10000, maxBuffer: 1024 * 1024 },
187
+ ).trim()
188
+
189
+ if (logResult) {
190
+ // Parse: alternating date lines and filename lines separated by blank lines
191
+ const blocks = logResult.split('\n\n')
192
+ const dirDates = new Map() // dir → most recent date
193
+ for (const block of blocks) {
194
+ const lines = block.split('\n').filter(Boolean)
195
+ if (lines.length < 2) continue
196
+ const date = lines[0]
197
+ for (let li = 1; li < lines.length; li++) {
198
+ const fileLine = lines[li].trim()
199
+ if (!fileLine) continue
200
+ const dir = path.dirname(path.resolve(root, fileLine))
201
+ if (!dirDates.has(dir)) {
202
+ dirDates.set(dir, date)
203
+ }
204
+ }
205
+ }
206
+ for (const fp of filePaths) {
207
+ const dir = path.dirname(fp)
208
+ const entry = result.get(fp)
209
+ if (dirDates.has(dir) && entry) {
210
+ entry.lastModified = dirDates.get(dir)
211
+ }
212
+ }
213
+ }
214
+ } catch { /* git not available or failed — leave nulls */ }
215
+
216
+ // Batch gitAuthor: use git log for each file's creation author.
217
+ // Unfortunately --follow --diff-filter=A doesn't combine well with multiple
218
+ // paths, so batch them in a single shell invocation using a for loop.
219
+ try {
220
+ const relPaths = filePaths.map(fp => path.relative(root, fp))
221
+ // Build a shell script that outputs "PATH<tab>AUTHOR" per file
222
+ const cmds = relPaths.map(rp =>
223
+ `echo -n "${rp}\\t"; git log --follow --diff-filter=A --format="%aN" -- "${rp}" | tail -1`
224
+ ).join('; ')
225
+ const authorResult = execSync(cmds, {
226
+ cwd: root, encoding: 'utf-8', timeout: 10000, shell: true, maxBuffer: 1024 * 1024,
227
+ }).trim()
228
+
229
+ if (authorResult) {
230
+ for (const line of authorResult.split('\n')) {
231
+ const tabIdx = line.indexOf('\t')
232
+ if (tabIdx < 0) continue
233
+ const relPath = line.slice(0, tabIdx)
234
+ const author = line.slice(tabIdx + 1).trim()
235
+ if (!author) continue
236
+ const absPath2 = path.resolve(root, relPath)
237
+ const entry = result.get(absPath2)
238
+ if (entry) entry.gitAuthor = author
239
+ }
240
+ }
241
+ } catch { /* git not available */ }
242
+
243
+ return result
244
+ }
245
+
158
246
  /**
159
247
  * Scan the repo for all data files, validate uniqueness, return the index.
160
248
  */
161
249
  function buildIndex(root) {
162
- const ignore = ['node_modules/**', 'dist/**', '.git/**']
250
+ const ignore = ['node_modules/**', 'dist/**', '.git/**', '.worktrees/**', 'public/**']
163
251
  const files = globSync(GLOB_PATTERN, { cwd: root, ignore, absolute: false })
164
252
  const canvasFiles = globSync(CANVAS_GLOB_PATTERN, { cwd: root, ignore, absolute: false })
165
253
 
@@ -339,6 +427,13 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes }, root)
339
427
  const resolvedFlowRoutes = {} // flow name → resolved route (for multi-flow logging)
340
428
  let i = 0
341
429
 
430
+ // Batch-fetch git metadata for all prototype + canvas files in 1-2 subprocesses
431
+ const gitPaths = [
432
+ ...Object.values(index.prototype || {}),
433
+ ...Object.values(index.canvas || {}),
434
+ ]
435
+ const gitMeta = batchGitMetadata(root, gitPaths)
436
+
342
437
  for (const suffix of INDEX_KEYS) {
343
438
  for (const [name, absPath] of Object.entries(index[suffix])) {
344
439
  const varName = `_d${i++}`
@@ -349,18 +444,17 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes }, root)
349
444
 
350
445
  // Auto-fill gitAuthor for prototype metadata from git history
351
446
  if (suffix === 'prototype' && parsed && !parsed.gitAuthor) {
352
- const gitAuthor = getGitAuthor(root, absPath)
353
- if (gitAuthor) {
354
- parsed = { ...parsed, gitAuthor }
447
+ const meta = gitMeta.get(absPath)
448
+ if (meta?.gitAuthor) {
449
+ parsed = { ...parsed, gitAuthor: meta.gitAuthor }
355
450
  }
356
451
  }
357
452
 
358
453
  // Auto-fill lastModified from git history for prototypes
359
454
  if (suffix === 'prototype' && parsed) {
360
- const protoDir = path.dirname(absPath)
361
- const lastModified = getLastModified(root, protoDir)
362
- if (lastModified) {
363
- parsed = { ...parsed, lastModified }
455
+ const meta = gitMeta.get(absPath)
456
+ if (meta?.lastModified) {
457
+ parsed = { ...parsed, lastModified: meta.lastModified }
364
458
  }
365
459
  }
366
460
 
@@ -399,9 +493,9 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes }, root)
399
493
 
400
494
  // Auto-fill gitAuthor for canvas metadata from git history
401
495
  if (suffix === 'canvas' && parsed && !parsed.gitAuthor) {
402
- const gitAuthor = getGitAuthor(root, absPath)
403
- if (gitAuthor) {
404
- parsed = { ...parsed, gitAuthor }
496
+ const meta = gitMeta.get(absPath)
497
+ if (meta?.gitAuthor) {
498
+ parsed = { ...parsed, gitAuthor: meta.gitAuthor }
405
499
  }
406
500
  }
407
501
 
@@ -561,6 +655,10 @@ export default function storyboardDataPlugin() {
561
655
  config() {
562
656
  return {
563
657
  optimizeDeps: {
658
+ // debug is CJS-only but micromark's development export does
659
+ // `import createDebug from 'debug'`. Vite must pre-bundle it
660
+ // so the ESM default import resolves correctly.
661
+ include: ['debug'],
564
662
  exclude: ['@dfosco/storyboard-react'],
565
663
  },
566
664
  }
@@ -581,6 +679,41 @@ export default function storyboardDataPlugin() {
581
679
  },
582
680
 
583
681
  configureServer(server) {
682
+ // ── Component isolate middleware ───────────────────────────────
683
+ // Serves a minimal HTML shell for iframe-isolated component widgets.
684
+ // The iframe loads componentIsolate.jsx which reads query params
685
+ // (module, export, theme) and renders a single canvas.jsx export.
686
+ const isolateEntryPath = new URL('../canvas/componentIsolate.jsx', import.meta.url).pathname
687
+ server.middlewares.use(async (req, res, next) => {
688
+ if (!req.url) return next()
689
+ let url = req.url
690
+ const baseNoTrail = (server.config.base || '/').replace(/\/$/, '')
691
+ if (baseNoTrail && url.startsWith(baseNoTrail)) {
692
+ url = url.slice(baseNoTrail.length) || '/'
693
+ }
694
+ if (!url.startsWith('/_storyboard/canvas/isolate')) return next()
695
+
696
+ const rawHtml = [
697
+ '<!DOCTYPE html>',
698
+ '<html><head>',
699
+ '<style>html,body{margin:0;padding:0;width:100%;height:100%}#root{width:100%;height:100%}</style>',
700
+ '</head><body>',
701
+ '<div id="root"></div>',
702
+ `<script type="module" src="/@fs${isolateEntryPath}"></script>`,
703
+ '</body></html>',
704
+ ].join('\n')
705
+
706
+ try {
707
+ const html = await server.transformIndexHtml(req.url, rawHtml)
708
+ res.writeHead(200, { 'Content-Type': 'text/html' })
709
+ res.end(html)
710
+ } catch (err) {
711
+ console.error('[storyboard] Component isolate HTML transform failed:', err)
712
+ res.writeHead(500, { 'Content-Type': 'text/plain' })
713
+ res.end('Component isolate failed')
714
+ }
715
+ })
716
+
584
717
  // Watch for data file changes in dev mode
585
718
  const watcher = server.watcher
586
719
  if (!buildResult) buildResult = buildIndex(root)
@@ -53,6 +53,12 @@ describe('storyboardDataPlugin', () => {
53
53
  expect(config.optimizeDeps.exclude).toContain('@dfosco/storyboard-react')
54
54
  })
55
55
 
56
+ it('config() includes debug in optimizeDeps for ESM/CJS interop', () => {
57
+ const plugin = storyboardDataPlugin()
58
+ const config = plugin.config()
59
+ expect(config.optimizeDeps.include).toContain('debug')
60
+ })
61
+
56
62
  it("resolveId returns resolved ID for 'virtual:storyboard-data-index'", () => {
57
63
  const plugin = createPlugin()
58
64
  expect(plugin.resolveId('virtual:storyboard-data-index')).toBe(RESOLVED_ID)