fg-devkit 1.15.1 → 1.16.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.
@@ -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.0",
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.0",
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 and wire it in vite.config
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,406 @@
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
+ *
8
+ * Does not overwrite the rest of vite.config. Idempotent.
9
+ *
10
+ * Usage:
11
+ * node node_modules/fg-devkit/scripts/sync-vite-plugin-vue-inspector.mjs
12
+ * pnpm exec fg-devkit sync-vite-plugin-vue-inspector
13
+ */
14
+
15
+ import fs from 'node:fs'
16
+ import path from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+
19
+ import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
20
+
21
+ const PACKAGE_NAME = 'vite-plugin-vue-inspector'
22
+ const IMPORT_LINE = `import Inspector from '${PACKAGE_NAME}'`
23
+ const PLUGIN_OPTIONS = {
24
+ launchEditor: 'cursor',
25
+ toggleComboKey: 'meta-alt',
26
+ }
27
+
28
+ const VITE_CONFIG_CANDIDATES = [
29
+ 'vite.config.ts',
30
+ 'vite.config.mts',
31
+ 'vite.config.js',
32
+ 'vite.config.mjs',
33
+ ]
34
+
35
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
36
+ const cwd = process.cwd()
37
+ const policyPath = resolveDependencyPolicyPath(scriptDir, cwd, process.argv)
38
+
39
+ function fail(message) {
40
+ console.error(`[sync-vite-plugin-vue-inspector] ${message}`)
41
+ process.exit(1)
42
+ }
43
+
44
+ function resolvePluginVersion(policy) {
45
+ return (
46
+ policy.managedDependencies?.devDependencies?.[PACKAGE_NAME] ??
47
+ policy.managedAnywhere?.[PACKAGE_NAME]
48
+ )
49
+ }
50
+
51
+ function findViteConfigPath(projectRoot) {
52
+ for (const name of VITE_CONFIG_CANDIDATES) {
53
+ const candidate = path.join(projectRoot, name)
54
+ if (fs.existsSync(candidate)) return candidate
55
+ }
56
+ return null
57
+ }
58
+
59
+ function formatPluginCall(indent) {
60
+ return [
61
+ 'Inspector({',
62
+ `${indent} launchEditor: '${PLUGIN_OPTIONS.launchEditor}',`,
63
+ `${indent} toggleComboKey: '${PLUGIN_OPTIONS.toggleComboKey}',`,
64
+ `${indent}})`,
65
+ ].join('\n')
66
+ }
67
+
68
+ /** Find matching closing `)` for a call starting at openParenIndex (the `(`). */
69
+ function findMatchingParen(source, openParenIndex) {
70
+ let depth = 0
71
+ let inSingle = false
72
+ let inDouble = false
73
+ let inTemplate = false
74
+ let inLineComment = false
75
+ let inBlockComment = false
76
+
77
+ for (let i = openParenIndex; i < source.length; i++) {
78
+ const ch = source[i]
79
+ const next = source[i + 1]
80
+
81
+ if (inLineComment) {
82
+ if (ch === '\n') inLineComment = false
83
+ continue
84
+ }
85
+ if (inBlockComment) {
86
+ if (ch === '*' && next === '/') {
87
+ inBlockComment = false
88
+ i++
89
+ }
90
+ continue
91
+ }
92
+ if (inSingle) {
93
+ if (ch === '\\') {
94
+ i++
95
+ continue
96
+ }
97
+ if (ch === "'") inSingle = false
98
+ continue
99
+ }
100
+ if (inDouble) {
101
+ if (ch === '\\') {
102
+ i++
103
+ continue
104
+ }
105
+ if (ch === '"') inDouble = false
106
+ continue
107
+ }
108
+ if (inTemplate) {
109
+ if (ch === '\\') {
110
+ i++
111
+ continue
112
+ }
113
+ if (ch === '`') inTemplate = false
114
+ continue
115
+ }
116
+
117
+ if (ch === '/' && next === '/') {
118
+ inLineComment = true
119
+ i++
120
+ continue
121
+ }
122
+ if (ch === '/' && next === '*') {
123
+ inBlockComment = true
124
+ i++
125
+ continue
126
+ }
127
+ if (ch === "'") {
128
+ inSingle = true
129
+ continue
130
+ }
131
+ if (ch === '"') {
132
+ inDouble = true
133
+ continue
134
+ }
135
+ if (ch === '`') {
136
+ inTemplate = true
137
+ continue
138
+ }
139
+
140
+ if (ch === '(') depth++
141
+ else if (ch === ')') {
142
+ depth--
143
+ if (depth === 0) return i
144
+ }
145
+ }
146
+ return -1
147
+ }
148
+
149
+ /** Find the closing `]` of `plugins: [` starting at openBracketIndex. */
150
+ function findMatchingBracket(source, openBracketIndex) {
151
+ let depth = 0
152
+ let inSingle = false
153
+ let inDouble = false
154
+ let inTemplate = false
155
+ let inLineComment = false
156
+ let inBlockComment = false
157
+
158
+ for (let i = openBracketIndex; i < source.length; i++) {
159
+ const ch = source[i]
160
+ const next = source[i + 1]
161
+
162
+ if (inLineComment) {
163
+ if (ch === '\n') inLineComment = false
164
+ continue
165
+ }
166
+ if (inBlockComment) {
167
+ if (ch === '*' && next === '/') {
168
+ inBlockComment = false
169
+ i++
170
+ }
171
+ continue
172
+ }
173
+ if (inSingle) {
174
+ if (ch === '\\') {
175
+ i++
176
+ continue
177
+ }
178
+ if (ch === "'") inSingle = false
179
+ continue
180
+ }
181
+ if (inDouble) {
182
+ if (ch === '\\') {
183
+ i++
184
+ continue
185
+ }
186
+ if (ch === '"') inDouble = false
187
+ continue
188
+ }
189
+ if (inTemplate) {
190
+ if (ch === '\\') {
191
+ i++
192
+ continue
193
+ }
194
+ if (ch === '`') inTemplate = false
195
+ continue
196
+ }
197
+
198
+ if (ch === '/' && next === '/') {
199
+ inLineComment = true
200
+ i++
201
+ continue
202
+ }
203
+ if (ch === '/' && next === '*') {
204
+ inBlockComment = true
205
+ i++
206
+ continue
207
+ }
208
+ if (ch === "'") {
209
+ inSingle = true
210
+ continue
211
+ }
212
+ if (ch === '"') {
213
+ inDouble = true
214
+ continue
215
+ }
216
+ if (ch === '`') {
217
+ inTemplate = true
218
+ continue
219
+ }
220
+
221
+ if (ch === '[') depth++
222
+ else if (ch === ']') {
223
+ depth--
224
+ if (depth === 0) return i
225
+ }
226
+ }
227
+ return -1
228
+ }
229
+
230
+ function hasInspectorImport(source) {
231
+ return /from\s+['"]vite-plugin-vue-inspector['"]/.test(source)
232
+ }
233
+
234
+ function ensureImport(source) {
235
+ if (hasInspectorImport(source)) return { source, changed: false }
236
+
237
+ const importRe = /^import\s.+from\s+['"][^'"]+['"].*$/gm
238
+ let lastImportEnd = -1
239
+ for (const match of source.matchAll(importRe)) {
240
+ lastImportEnd = match.index + match[0].length
241
+ }
242
+
243
+ if (lastImportEnd === -1) {
244
+ return { source: `${IMPORT_LINE}\n${source}`, changed: true }
245
+ }
246
+
247
+ const before = source.slice(0, lastImportEnd)
248
+ const after = source.slice(lastImportEnd)
249
+ return { source: `${before}\n${IMPORT_LINE}${after}`, changed: true }
250
+ }
251
+
252
+ function isCanonicalInspectorCall(callText) {
253
+ const launchOk = /launchEditor\s*:\s*['"]cursor['"]/.test(callText)
254
+ const toggleOk = /toggleComboKey\s*:\s*['"]meta-alt['"]/.test(callText)
255
+ const onlyExpectedKeys =
256
+ [...callText.matchAll(/(\w+)\s*:/g)].map((m) => m[1]).every((key) =>
257
+ ['launchEditor', 'toggleComboKey'].includes(key),
258
+ )
259
+ return launchOk && toggleOk && onlyExpectedKeys
260
+ }
261
+
262
+ function ensurePluginCall(source) {
263
+ const callMatch = /\bInspector\s*\(/.exec(source)
264
+ if (callMatch) {
265
+ const openParen = callMatch.index + callMatch[0].length - 1
266
+ const closeParen = findMatchingParen(source, openParen)
267
+ if (closeParen === -1) {
268
+ fail('Found Inspector( but could not find matching closing parenthesis')
269
+ }
270
+ const callText = source.slice(callMatch.index, closeParen + 1)
271
+ if (isCanonicalInspectorCall(callText)) {
272
+ return { source, changed: false, action: null }
273
+ }
274
+ const indentMatch = source.slice(0, callMatch.index).match(/(^|\n)([ \t]*)$/)
275
+ const indent = indentMatch ? indentMatch[2] : ' '
276
+ const formatted = formatPluginCall(indent)
277
+ return {
278
+ source: source.slice(0, callMatch.index) + formatted + source.slice(closeParen + 1),
279
+ changed: true,
280
+ action: 'updated',
281
+ }
282
+ }
283
+
284
+ const pluginsMatch = /plugins\s*:\s*\[/.exec(source)
285
+ if (!pluginsMatch) {
286
+ fail('Could not find plugins: [...] in vite config')
287
+ }
288
+ const openBracket = pluginsMatch.index + pluginsMatch[0].length - 1
289
+ const closeBracket = findMatchingBracket(source, openBracket)
290
+ if (closeBracket === -1) {
291
+ fail('Could not find closing ] for plugins array')
292
+ }
293
+
294
+ const arrayBody = source.slice(openBracket + 1, closeBracket)
295
+ const indentMatch = arrayBody.match(/\n([ \t]+)\S/)
296
+ const itemIndent = indentMatch ? indentMatch[1] : ' '
297
+ const closeIndent = itemIndent.slice(0, Math.max(0, itemIndent.length - 2)) || ' '
298
+
299
+ const trimmedBody = arrayBody.trimEnd()
300
+ const bodyEnd = openBracket + 1 + trimmedBody.length
301
+ const needsComma = trimmedBody.length > 0 && !/,\s*$/.test(trimmedBody)
302
+ const pluginLine = formatPluginCall(itemIndent)
303
+ const insertion = `${needsComma ? ',' : ''}\n${itemIndent}${pluginLine},\n${closeIndent}`
304
+
305
+ return {
306
+ source: source.slice(0, bodyEnd) + insertion + source.slice(closeBracket),
307
+ changed: true,
308
+ action: 'added',
309
+ }
310
+ }
311
+
312
+ function syncPackageJson(projectRoot, version) {
313
+ const packageJsonPath = path.join(projectRoot, 'package.json')
314
+ if (!fs.existsSync(packageJsonPath)) {
315
+ fail(`package.json not found at ${packageJsonPath}`)
316
+ }
317
+
318
+ let pkg
319
+ try {
320
+ pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
321
+ } catch (err) {
322
+ fail(`Failed to parse package.json: ${err.message}`)
323
+ }
324
+
325
+ pkg.devDependencies =
326
+ pkg.devDependencies && typeof pkg.devDependencies === 'object' ? pkg.devDependencies : {}
327
+
328
+ const changes = []
329
+ if (pkg.dependencies?.[PACKAGE_NAME] !== undefined) {
330
+ delete pkg.dependencies[PACKAGE_NAME]
331
+ changes.push(`removed ${PACKAGE_NAME} from dependencies`)
332
+ }
333
+ if (pkg.devDependencies[PACKAGE_NAME] !== version) {
334
+ pkg.devDependencies[PACKAGE_NAME] = version
335
+ changes.push(`devDependencies.${PACKAGE_NAME} = ${version}`)
336
+ }
337
+
338
+ if (changes.length > 0) {
339
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
340
+ }
341
+ return changes
342
+ }
343
+
344
+ function syncViteConfig(projectRoot) {
345
+ const viteConfigPath = findViteConfigPath(projectRoot)
346
+ if (!viteConfigPath) {
347
+ fail(
348
+ `No vite.config.(ts|mts|js|mjs) found in ${projectRoot}. Add a Vite config before syncing the inspector plugin.`,
349
+ )
350
+ }
351
+
352
+ const configName = path.basename(viteConfigPath)
353
+ let source = fs.readFileSync(viteConfigPath, 'utf8')
354
+ const changes = []
355
+
356
+ const importResult = ensureImport(source)
357
+ source = importResult.source
358
+ if (importResult.changed) changes.push(`added import in ${configName}`)
359
+
360
+ const pluginResult = ensurePluginCall(source)
361
+ source = pluginResult.source
362
+ if (pluginResult.action === 'added') {
363
+ changes.push(`added Inspector() to plugins in ${configName}`)
364
+ } else if (pluginResult.action === 'updated') {
365
+ changes.push(`updated Inspector() options in ${configName}`)
366
+ }
367
+
368
+ if (importResult.changed || pluginResult.changed) {
369
+ fs.writeFileSync(viteConfigPath, source, 'utf8')
370
+ }
371
+
372
+ return changes
373
+ }
374
+
375
+ function main() {
376
+ if (!fs.existsSync(policyPath)) {
377
+ fail(`Could not find dependency policy: ${policyPath}`)
378
+ }
379
+
380
+ const policy = JSON.parse(fs.readFileSync(policyPath, 'utf8'))
381
+ const version = resolvePluginVersion(policy)
382
+ if (!version) {
383
+ fail(
384
+ `${PACKAGE_NAME} is not listed in the dependency policy (managedDependencies.devDependencies)`,
385
+ )
386
+ }
387
+
388
+ const pkgChanges = syncPackageJson(cwd, version)
389
+ const viteChanges = syncViteConfig(cwd)
390
+ const allChanges = [...pkgChanges, ...viteChanges]
391
+
392
+ if (allChanges.length === 0) {
393
+ console.log('[sync-vite-plugin-vue-inspector] Already up to date.')
394
+ return
395
+ }
396
+
397
+ console.log('[sync-vite-plugin-vue-inspector] Updated:')
398
+ for (const change of allChanges) {
399
+ console.log(` - ${change}`)
400
+ }
401
+ console.log(
402
+ '[sync-vite-plugin-vue-inspector] Run pnpm install if the dependency was added or pinned.',
403
+ )
404
+ }
405
+
406
+ main()