fg-devkit 1.15.1 → 1.16.1

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.
@@ -83,6 +83,7 @@
83
83
  "vega": "6.4.0",
84
84
  "vite-plugin-dts": "4.5.4",
85
85
  "vite-plugin-vue-devtools": "8.2.1",
86
+ "vite-plugin-vue-inspector": "7.0.0",
86
87
  "vitest": "4.1.10",
87
88
  "yaml": "2.9.0"
88
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fg-devkit",
3
- "version": "1.15.1",
3
+ "version": "1.16.1",
4
4
  "description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -26,6 +26,7 @@
26
26
  "./scripts/sync-tooling-configs.mjs": "./scripts/sync-tooling-configs.mjs",
27
27
  "./scripts/sync-project-configs.mjs": "./scripts/sync-project-configs.mjs",
28
28
  "./scripts/sync-consumer-scripts.mjs": "./scripts/sync-consumer-scripts.mjs",
29
+ "./scripts/sync-vite-plugin-vue-inspector.mjs": "./scripts/sync-vite-plugin-vue-inspector.mjs",
29
30
  "./scripts/sync-consumer-lefthook.mjs": "./scripts/sync-consumer-lefthook.mjs",
30
31
  "./scripts/policy-outdated.mjs": "./scripts/policy-outdated.mjs",
31
32
  "./scripts/policy-vulns.mjs": "./scripts/policy-vulns.mjs",
@@ -33,10 +33,11 @@
33
33
  "@tanstack/vue-query-devtools": "6.1.48",
34
34
  "@types/node": "22.16.5",
35
35
  "@vitejs/plugin-vue": "6.0.8",
36
- "fg-devkit": "1.15.1",
36
+ "fg-devkit": "1.16.1",
37
37
  "tailwindcss": "4.3.3",
38
38
  "tsx": "4.23.13",
39
- "vite": "7.3.5"
39
+ "vite": "7.3.5",
40
+ "vite-plugin-vue-inspector": "7.0.0"
40
41
  },
41
42
  "packageManager": "pnpm@11.25.0",
42
43
  "engines": {
@@ -4,6 +4,7 @@ import vue from '@vitejs/plugin-vue'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
6
6
  import tailwindcss from '@tailwindcss/vite'
7
+ import Inspector from 'vite-plugin-vue-inspector'
7
8
 
8
9
  export default defineConfig({
9
10
  plugins: [
@@ -19,6 +20,10 @@ export default defineConfig({
19
20
  strictMessage: false,
20
21
  }),
21
22
  tailwindcss(),
23
+ Inspector({
24
+ launchEditor: 'cursor',
25
+ toggleComboKey: 'meta-alt',
26
+ }),
22
27
  ],
23
28
  resolve: {
24
29
  alias: {
@@ -445,6 +445,7 @@ function runSyncConfigTarget(target) {
445
445
  if (target === 'tsconfig') return syncFileFromTemplate('configs/tsconfig.app.sync.json', 'tsconfig.json')
446
446
  if (target === 'fallow') return syncFileFromTemplate('configs/fallowrc.sync-stub.json', '.fallowrc.json')
447
447
  if (target === 'cursor') return runNode('sync-core-coding-standards.mjs')
448
+ if (target === 'vue-inspector') return runNode('sync-vite-plugin-vue-inspector.mjs')
448
449
  if (target === 'all') return runNode('sync-project-configs.mjs')
449
450
  console.error(`[fg-devkit] Unknown config sync target: ${target}`)
450
451
  return 1
@@ -477,6 +478,7 @@ const DIRECT_COMMANDS = {
477
478
  'sync-tooling-configs': 'sync-tooling-configs.mjs',
478
479
  'sync-vscode-settings': 'sync-vscode-settings.mjs',
479
480
  'sync-core-coding-standards': 'sync-core-coding-standards.mjs',
481
+ 'sync-vite-plugin-vue-inspector': 'sync-vite-plugin-vue-inspector.mjs',
480
482
  'sync-consumer-scripts': 'sync-consumer-scripts.mjs',
481
483
  'sync-release-notes-config': 'sync-release-notes-config.mjs',
482
484
  'generate-release-notes': 'generate-release-notes.mjs',
@@ -538,6 +540,7 @@ Commands:
538
540
  sync-tooling-configs Tooling sync only (overwrites config files)
539
541
  sync-vscode-settings Copy shared VS Code settings into .vscode/settings.json
540
542
  sync-core-coding-standards Copy .cursor core coding standards with consumer package name
543
+ sync-vite-plugin-vue-inspector Add/pin vite-plugin-vue-inspector, wire vite.config, install, commit
541
544
  sync-consumer-scripts Enforce standard fgd scripts in consumer package.json
542
545
  setup-lefthook Setup lefthook and migrate from husky/lint-staged
543
546
  full-consumer-setup Run: scripts -> lefthook -> step 3 -> step 2 -> pnpm install
@@ -836,6 +839,7 @@ async function runInteractiveMenu() {
836
839
  { name: 'tsconfig', value: 'tsconfig' },
837
840
  { name: 'Fallow', value: 'fallow' },
838
841
  { name: 'Cursor', value: 'cursor' },
842
+ { name: 'Vue inspector (vite-plugin-vue-inspector)', value: 'vue-inspector' },
839
843
  { name: 'All', value: 'all' },
840
844
  { name: 'Cancel', value: 'cancel' },
841
845
  ],
@@ -0,0 +1,479 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Ensures vite-plugin-vue-inspector is declared and wired in a Vue consumer repo.
4
+ *
5
+ * - Adds/pins the package in package.json devDependencies (version from dependency policy)
6
+ * - Ensures import + Inspector({ launchEditor: 'cursor', toggleComboKey: 'meta-alt' }) in vite.config.*
7
+ * - Runs pnpm install, then commits related files
8
+ *
9
+ * Does not overwrite the rest of vite.config. Idempotent when already up to date
10
+ * (skips install/commit).
11
+ *
12
+ * Usage:
13
+ * node node_modules/fg-devkit/scripts/sync-vite-plugin-vue-inspector.mjs
14
+ * pnpm exec fg-devkit sync-vite-plugin-vue-inspector
15
+ */
16
+
17
+ import fs from 'node:fs'
18
+ import path from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+ import { spawnSync } from 'node:child_process'
21
+
22
+ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
23
+
24
+ const PACKAGE_NAME = 'vite-plugin-vue-inspector'
25
+ const IMPORT_LINE = `import Inspector from '${PACKAGE_NAME}'`
26
+ const PLUGIN_OPTIONS = {
27
+ launchEditor: 'cursor',
28
+ toggleComboKey: 'meta-alt',
29
+ }
30
+ const COMMIT_MESSAGE = 'dep(vite-plugin-vue-inspector): add dependency and config'
31
+
32
+ const VITE_CONFIG_CANDIDATES = [
33
+ 'vite.config.ts',
34
+ 'vite.config.mts',
35
+ 'vite.config.js',
36
+ 'vite.config.mjs',
37
+ ]
38
+
39
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
40
+ const cwd = process.cwd()
41
+ const policyPath = resolveDependencyPolicyPath(scriptDir, cwd, process.argv)
42
+
43
+ function fail(message) {
44
+ console.error(`[sync-vite-plugin-vue-inspector] ${message}`)
45
+ process.exit(1)
46
+ }
47
+
48
+ function runPnpm(args) {
49
+ const result = spawnSync('pnpm', args, {
50
+ stdio: 'inherit',
51
+ cwd,
52
+ env: process.env,
53
+ })
54
+ if (result.error) {
55
+ fail(result.error.message)
56
+ }
57
+ return result.status ?? 1
58
+ }
59
+
60
+ function runGit(args, options = {}) {
61
+ const result = spawnSync('git', args, {
62
+ stdio: options.capture ? 'pipe' : 'inherit',
63
+ encoding: options.capture ? 'utf8' : undefined,
64
+ cwd,
65
+ env: process.env,
66
+ })
67
+ if (result.error) {
68
+ fail(result.error.message)
69
+ }
70
+ return { status: result.status ?? 1, stdout: result.stdout ?? '' }
71
+ }
72
+
73
+ function fileDiffersFromHead(filePath) {
74
+ const absolutePath = path.join(cwd, filePath)
75
+ if (!fs.existsSync(absolutePath)) return false
76
+
77
+ const tracked = runGit(['ls-files', '--error-unmatch', '--', filePath], { capture: true })
78
+ if (tracked.status !== 0) {
79
+ return true
80
+ }
81
+
82
+ const diff = runGit(['diff', '--quiet', 'HEAD', '--', filePath], { capture: true })
83
+ return diff.status !== 0
84
+ }
85
+
86
+ function stageExistingFiles(files) {
87
+ const existingFiles = files.filter((filePath) => fs.existsSync(path.join(cwd, filePath)))
88
+ if (existingFiles.length === 0) return 0
89
+ return runGit(['add', '--', ...existingFiles]).status
90
+ }
91
+
92
+ function resolvePluginVersion(policy) {
93
+ return (
94
+ policy.managedDependencies?.devDependencies?.[PACKAGE_NAME] ??
95
+ policy.managedAnywhere?.[PACKAGE_NAME]
96
+ )
97
+ }
98
+
99
+ function findViteConfigPath(projectRoot) {
100
+ for (const name of VITE_CONFIG_CANDIDATES) {
101
+ const candidate = path.join(projectRoot, name)
102
+ if (fs.existsSync(candidate)) return candidate
103
+ }
104
+ return null
105
+ }
106
+
107
+ function formatPluginCall(indent) {
108
+ return [
109
+ 'Inspector({',
110
+ `${indent} launchEditor: '${PLUGIN_OPTIONS.launchEditor}',`,
111
+ `${indent} toggleComboKey: '${PLUGIN_OPTIONS.toggleComboKey}',`,
112
+ `${indent}})`,
113
+ ].join('\n')
114
+ }
115
+
116
+ /** Find matching closing `)` for a call starting at openParenIndex (the `(`). */
117
+ function findMatchingParen(source, openParenIndex) {
118
+ let depth = 0
119
+ let inSingle = false
120
+ let inDouble = false
121
+ let inTemplate = false
122
+ let inLineComment = false
123
+ let inBlockComment = false
124
+
125
+ for (let i = openParenIndex; i < source.length; i++) {
126
+ const ch = source[i]
127
+ const next = source[i + 1]
128
+
129
+ if (inLineComment) {
130
+ if (ch === '\n') inLineComment = false
131
+ continue
132
+ }
133
+ if (inBlockComment) {
134
+ if (ch === '*' && next === '/') {
135
+ inBlockComment = false
136
+ i++
137
+ }
138
+ continue
139
+ }
140
+ if (inSingle) {
141
+ if (ch === '\\') {
142
+ i++
143
+ continue
144
+ }
145
+ if (ch === "'") inSingle = false
146
+ continue
147
+ }
148
+ if (inDouble) {
149
+ if (ch === '\\') {
150
+ i++
151
+ continue
152
+ }
153
+ if (ch === '"') inDouble = false
154
+ continue
155
+ }
156
+ if (inTemplate) {
157
+ if (ch === '\\') {
158
+ i++
159
+ continue
160
+ }
161
+ if (ch === '`') inTemplate = false
162
+ continue
163
+ }
164
+
165
+ if (ch === '/' && next === '/') {
166
+ inLineComment = true
167
+ i++
168
+ continue
169
+ }
170
+ if (ch === '/' && next === '*') {
171
+ inBlockComment = true
172
+ i++
173
+ continue
174
+ }
175
+ if (ch === "'") {
176
+ inSingle = true
177
+ continue
178
+ }
179
+ if (ch === '"') {
180
+ inDouble = true
181
+ continue
182
+ }
183
+ if (ch === '`') {
184
+ inTemplate = true
185
+ continue
186
+ }
187
+
188
+ if (ch === '(') depth++
189
+ else if (ch === ')') {
190
+ depth--
191
+ if (depth === 0) return i
192
+ }
193
+ }
194
+ return -1
195
+ }
196
+
197
+ /** Find the closing `]` of `plugins: [` starting at openBracketIndex. */
198
+ function findMatchingBracket(source, openBracketIndex) {
199
+ let depth = 0
200
+ let inSingle = false
201
+ let inDouble = false
202
+ let inTemplate = false
203
+ let inLineComment = false
204
+ let inBlockComment = false
205
+
206
+ for (let i = openBracketIndex; i < source.length; i++) {
207
+ const ch = source[i]
208
+ const next = source[i + 1]
209
+
210
+ if (inLineComment) {
211
+ if (ch === '\n') inLineComment = false
212
+ continue
213
+ }
214
+ if (inBlockComment) {
215
+ if (ch === '*' && next === '/') {
216
+ inBlockComment = false
217
+ i++
218
+ }
219
+ continue
220
+ }
221
+ if (inSingle) {
222
+ if (ch === '\\') {
223
+ i++
224
+ continue
225
+ }
226
+ if (ch === "'") inSingle = false
227
+ continue
228
+ }
229
+ if (inDouble) {
230
+ if (ch === '\\') {
231
+ i++
232
+ continue
233
+ }
234
+ if (ch === '"') inDouble = false
235
+ continue
236
+ }
237
+ if (inTemplate) {
238
+ if (ch === '\\') {
239
+ i++
240
+ continue
241
+ }
242
+ if (ch === '`') inTemplate = false
243
+ continue
244
+ }
245
+
246
+ if (ch === '/' && next === '/') {
247
+ inLineComment = true
248
+ i++
249
+ continue
250
+ }
251
+ if (ch === '/' && next === '*') {
252
+ inBlockComment = true
253
+ i++
254
+ continue
255
+ }
256
+ if (ch === "'") {
257
+ inSingle = true
258
+ continue
259
+ }
260
+ if (ch === '"') {
261
+ inDouble = true
262
+ continue
263
+ }
264
+ if (ch === '`') {
265
+ inTemplate = true
266
+ continue
267
+ }
268
+
269
+ if (ch === '[') depth++
270
+ else if (ch === ']') {
271
+ depth--
272
+ if (depth === 0) return i
273
+ }
274
+ }
275
+ return -1
276
+ }
277
+
278
+ function hasInspectorImport(source) {
279
+ return /from\s+['"]vite-plugin-vue-inspector['"]/.test(source)
280
+ }
281
+
282
+ function ensureImport(source) {
283
+ if (hasInspectorImport(source)) return { source, changed: false }
284
+
285
+ const importRe = /^import\s.+from\s+['"][^'"]+['"].*$/gm
286
+ let lastImportEnd = -1
287
+ for (const match of source.matchAll(importRe)) {
288
+ lastImportEnd = match.index + match[0].length
289
+ }
290
+
291
+ if (lastImportEnd === -1) {
292
+ return { source: `${IMPORT_LINE}\n${source}`, changed: true }
293
+ }
294
+
295
+ const before = source.slice(0, lastImportEnd)
296
+ const after = source.slice(lastImportEnd)
297
+ return { source: `${before}\n${IMPORT_LINE}${after}`, changed: true }
298
+ }
299
+
300
+ function isCanonicalInspectorCall(callText) {
301
+ const launchOk = /launchEditor\s*:\s*['"]cursor['"]/.test(callText)
302
+ const toggleOk = /toggleComboKey\s*:\s*['"]meta-alt['"]/.test(callText)
303
+ const onlyExpectedKeys = [...callText.matchAll(/(\w+)\s*:/g)]
304
+ .map((m) => m[1])
305
+ .every((key) => ['launchEditor', 'toggleComboKey'].includes(key))
306
+ return launchOk && toggleOk && onlyExpectedKeys
307
+ }
308
+
309
+ function ensurePluginCall(source) {
310
+ const callMatch = /\bInspector\s*\(/.exec(source)
311
+ if (callMatch) {
312
+ const openParen = callMatch.index + callMatch[0].length - 1
313
+ const closeParen = findMatchingParen(source, openParen)
314
+ if (closeParen === -1) {
315
+ fail('Found Inspector( but could not find matching closing parenthesis')
316
+ }
317
+ const callText = source.slice(callMatch.index, closeParen + 1)
318
+ if (isCanonicalInspectorCall(callText)) {
319
+ return { source, changed: false, action: null }
320
+ }
321
+ const indentMatch = source.slice(0, callMatch.index).match(/(^|\n)([ \t]*)$/)
322
+ const indent = indentMatch ? indentMatch[2] : ' '
323
+ const formatted = formatPluginCall(indent)
324
+ return {
325
+ source: source.slice(0, callMatch.index) + formatted + source.slice(closeParen + 1),
326
+ changed: true,
327
+ action: 'updated',
328
+ }
329
+ }
330
+
331
+ const pluginsMatch = /plugins\s*:\s*\[/.exec(source)
332
+ if (!pluginsMatch) {
333
+ fail('Could not find plugins: [...] in vite config')
334
+ }
335
+ const openBracket = pluginsMatch.index + pluginsMatch[0].length - 1
336
+ const closeBracket = findMatchingBracket(source, openBracket)
337
+ if (closeBracket === -1) {
338
+ fail('Could not find closing ] for plugins array')
339
+ }
340
+
341
+ const arrayBody = source.slice(openBracket + 1, closeBracket)
342
+ const indentMatch = arrayBody.match(/\n([ \t]+)\S/)
343
+ const itemIndent = indentMatch ? indentMatch[1] : ' '
344
+ const closeIndent = itemIndent.slice(0, Math.max(0, itemIndent.length - 2)) || ' '
345
+
346
+ const trimmedBody = arrayBody.trimEnd()
347
+ const bodyEnd = openBracket + 1 + trimmedBody.length
348
+ const needsComma = trimmedBody.length > 0 && !/,\s*$/.test(trimmedBody)
349
+ const pluginLine = formatPluginCall(itemIndent)
350
+ const insertion = `${needsComma ? ',' : ''}\n${itemIndent}${pluginLine},\n${closeIndent}`
351
+
352
+ return {
353
+ source: source.slice(0, bodyEnd) + insertion + source.slice(closeBracket),
354
+ changed: true,
355
+ action: 'added',
356
+ }
357
+ }
358
+
359
+ function syncPackageJson(projectRoot, version) {
360
+ const packageJsonPath = path.join(projectRoot, 'package.json')
361
+ if (!fs.existsSync(packageJsonPath)) {
362
+ fail(`package.json not found at ${packageJsonPath}`)
363
+ }
364
+
365
+ let pkg
366
+ try {
367
+ pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
368
+ } catch (err) {
369
+ fail(`Failed to parse package.json: ${err.message}`)
370
+ }
371
+
372
+ pkg.devDependencies =
373
+ pkg.devDependencies && typeof pkg.devDependencies === 'object' ? pkg.devDependencies : {}
374
+
375
+ const changes = []
376
+ if (pkg.dependencies?.[PACKAGE_NAME] !== undefined) {
377
+ delete pkg.dependencies[PACKAGE_NAME]
378
+ changes.push(`removed ${PACKAGE_NAME} from dependencies`)
379
+ }
380
+ if (pkg.devDependencies[PACKAGE_NAME] !== version) {
381
+ pkg.devDependencies[PACKAGE_NAME] = version
382
+ changes.push(`devDependencies.${PACKAGE_NAME} = ${version}`)
383
+ }
384
+
385
+ if (changes.length > 0) {
386
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
387
+ }
388
+ return changes
389
+ }
390
+
391
+ function syncViteConfig(projectRoot) {
392
+ const viteConfigPath = findViteConfigPath(projectRoot)
393
+ if (!viteConfigPath) {
394
+ fail(
395
+ `No vite.config.(ts|mts|js|mjs) found in ${projectRoot}. Add a Vite config before syncing the inspector plugin.`,
396
+ )
397
+ }
398
+
399
+ const configName = path.basename(viteConfigPath)
400
+ let source = fs.readFileSync(viteConfigPath, 'utf8')
401
+ const changes = []
402
+
403
+ const importResult = ensureImport(source)
404
+ source = importResult.source
405
+ if (importResult.changed) changes.push(`added import in ${configName}`)
406
+
407
+ const pluginResult = ensurePluginCall(source)
408
+ source = pluginResult.source
409
+ if (pluginResult.action === 'added') {
410
+ changes.push(`added Inspector() to plugins in ${configName}`)
411
+ } else if (pluginResult.action === 'updated') {
412
+ changes.push(`updated Inspector() options in ${configName}`)
413
+ }
414
+
415
+ if (importResult.changed || pluginResult.changed) {
416
+ fs.writeFileSync(viteConfigPath, source, 'utf8')
417
+ }
418
+
419
+ return { changes, configName }
420
+ }
421
+
422
+ function installAndCommit(viteConfigName) {
423
+ console.log('[sync-vite-plugin-vue-inspector] Running pnpm install…')
424
+ const installExitCode = runPnpm(['install'])
425
+ if (installExitCode !== 0) {
426
+ process.exit(installExitCode)
427
+ }
428
+
429
+ const candidateFiles = ['package.json', 'pnpm-lock.yaml', viteConfigName].filter(Boolean)
430
+ const filesToStage = candidateFiles.filter((filePath) => fileDiffersFromHead(filePath))
431
+ if (filesToStage.length === 0) {
432
+ console.log('[sync-vite-plugin-vue-inspector] No related changes to commit.')
433
+ return
434
+ }
435
+
436
+ const stageExitCode = stageExistingFiles(filesToStage)
437
+ if (stageExitCode !== 0) {
438
+ process.exit(stageExitCode)
439
+ }
440
+
441
+ console.log(`[sync-vite-plugin-vue-inspector] Staged ${filesToStage.length} file(s)`)
442
+ console.log(`[sync-vite-plugin-vue-inspector] Auto-committing: ${COMMIT_MESSAGE}`)
443
+ const commitExitCode = runGit(['commit', '-m', COMMIT_MESSAGE]).status
444
+ if (commitExitCode !== 0) {
445
+ process.exit(commitExitCode)
446
+ }
447
+ }
448
+
449
+ function main() {
450
+ if (!fs.existsSync(policyPath)) {
451
+ fail(`Could not find dependency policy: ${policyPath}`)
452
+ }
453
+
454
+ const policy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
455
+ const version = resolvePluginVersion(policy)
456
+ if (!version) {
457
+ fail(
458
+ `${PACKAGE_NAME} is not listed in the dependency policy (managedDependencies.devDependencies)`,
459
+ )
460
+ }
461
+
462
+ const pkgChanges = syncPackageJson(cwd, version)
463
+ const { changes: viteChanges, configName } = syncViteConfig(cwd)
464
+ const allChanges = [...pkgChanges, ...viteChanges]
465
+
466
+ if (allChanges.length === 0) {
467
+ console.log('[sync-vite-plugin-vue-inspector] Already up to date.')
468
+ return
469
+ }
470
+
471
+ console.log('[sync-vite-plugin-vue-inspector] Updated:')
472
+ for (const change of allChanges) {
473
+ console.log(` - ${change}`)
474
+ }
475
+
476
+ installAndCommit(configName)
477
+ }
478
+
479
+ main()