@dfosco/storyboard-react 4.0.0-beta.3 → 4.0.0-beta.4

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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard-react",
3
- "version": "4.0.0-beta.3",
3
+ "version": "4.0.0-beta.4",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@dfosco/storyboard-core": "4.0.0-beta.3",
7
- "@dfosco/tiny-canvas": "4.0.0-beta.3",
6
+ "@dfosco/storyboard-core": "4.0.0-beta.4",
7
+ "@dfosco/tiny-canvas": "4.0.0-beta.4",
8
8
  "@neodrag/react": "^2.3.1",
9
9
  "glob": "^11.0.0",
10
10
  "jsonc-parser": "^3.3.1"
@@ -155,14 +155,101 @@ 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/**']
163
- // Scope to src/ all data files live there (avoids walking .worktrees/, public/, etc.)
164
- const files = globSync(`src/${GLOB_PATTERN}`, { cwd: root, ignore, absolute: false })
165
- const canvasFiles = globSync(`src/${CANVAS_GLOB_PATTERN}`, { cwd: root, ignore, absolute: false })
250
+ const ignore = ['node_modules/**', 'dist/**', '.git/**', '.worktrees/**', 'public/**']
251
+ const files = globSync(GLOB_PATTERN, { cwd: root, ignore, absolute: false })
252
+ const canvasFiles = globSync(CANVAS_GLOB_PATTERN, { cwd: root, ignore, absolute: false })
166
253
 
167
254
  // Detect nested .folder/ directories (not supported)
168
255
  // Scan directories directly since empty nested folders have no data files
@@ -340,6 +427,13 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes }, root)
340
427
  const resolvedFlowRoutes = {} // flow name → resolved route (for multi-flow logging)
341
428
  let i = 0
342
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
+
343
437
  for (const suffix of INDEX_KEYS) {
344
438
  for (const [name, absPath] of Object.entries(index[suffix])) {
345
439
  const varName = `_d${i++}`
@@ -350,18 +444,17 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes }, root)
350
444
 
351
445
  // Auto-fill gitAuthor for prototype metadata from git history
352
446
  if (suffix === 'prototype' && parsed && !parsed.gitAuthor) {
353
- const gitAuthor = getGitAuthor(root, absPath)
354
- if (gitAuthor) {
355
- parsed = { ...parsed, gitAuthor }
447
+ const meta = gitMeta.get(absPath)
448
+ if (meta?.gitAuthor) {
449
+ parsed = { ...parsed, gitAuthor: meta.gitAuthor }
356
450
  }
357
451
  }
358
452
 
359
453
  // Auto-fill lastModified from git history for prototypes
360
454
  if (suffix === 'prototype' && parsed) {
361
- const protoDir = path.dirname(absPath)
362
- const lastModified = getLastModified(root, protoDir)
363
- if (lastModified) {
364
- parsed = { ...parsed, lastModified }
455
+ const meta = gitMeta.get(absPath)
456
+ if (meta?.lastModified) {
457
+ parsed = { ...parsed, lastModified: meta.lastModified }
365
458
  }
366
459
  }
367
460
 
@@ -400,9 +493,9 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes }, root)
400
493
 
401
494
  // Auto-fill gitAuthor for canvas metadata from git history
402
495
  if (suffix === 'canvas' && parsed && !parsed.gitAuthor) {
403
- const gitAuthor = getGitAuthor(root, absPath)
404
- if (gitAuthor) {
405
- parsed = { ...parsed, gitAuthor }
496
+ const meta = gitMeta.get(absPath)
497
+ if (meta?.gitAuthor) {
498
+ parsed = { ...parsed, gitAuthor: meta.gitAuthor }
406
499
  }
407
500
  }
408
501