fg-devkit 1.7.1 → 1.7.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/README.md CHANGED
@@ -45,6 +45,24 @@ pnpm exec fgd typecheck
45
45
  pnpm exec fgd typecheck-watch
46
46
  ```
47
47
 
48
+ ## Bundled Fallow (dead code analysis)
49
+
50
+ `fg-devkit` also ships Fallow for dead code analysis in consuming repos.
51
+
52
+ Run dead code analysis via CLI:
53
+
54
+ ```bash
55
+ pnpm exec fgd dead-code
56
+ ```
57
+
58
+ Or run Fallow directly with custom args:
59
+
60
+ ```bash
61
+ pnpm exec fgd fallow dead-code --json
62
+ ```
63
+
64
+ When you run `fgd sync-tooling-configs` (or `fgd sync-project-configs`), fg-devkit also writes a shared `.fallowrc.json` to the consuming repo.
65
+
48
66
  To remove redundant tooling dependencies from a consuming repo:
49
67
 
50
68
  ```bash
@@ -136,3 +154,54 @@ export default {
136
154
  ...basePrettierConfig,
137
155
  }
138
156
  ```
157
+
158
+ ## Local development with pnpm link
159
+
160
+ Use this flow to test local `fg-devkit` changes in a consuming repo.
161
+
162
+ In this repo (`fg-devkit`):
163
+
164
+ ```bash
165
+ pnpm install
166
+ pnpm link --global
167
+ ```
168
+
169
+ In the consuming repo:
170
+
171
+ ```bash
172
+ pnpm link --global fg-devkit
173
+ pnpm install
174
+ pnpm exec fgd --help
175
+ ```
176
+
177
+ To remove the global link in a consuming repo:
178
+
179
+ ```bash
180
+ pnpm unlink fg-devkit
181
+ pnpm install
182
+ ```
183
+
184
+ ## Install CLI in this repo
185
+
186
+ 1) Add this to your shell config (`~/.zshrc`) and update `FGD_DEVKIT_PATH`:
187
+
188
+ ```bash
189
+ cat <<'EOF' >> ~/.zshrc
190
+ # Smart fgd: use local project binary when available, otherwise fg-devkit script
191
+ unalias fgd 2>/dev/null
192
+ export FGD_DEVKIT_PATH="/Users/username/projects/fg-devkit"
193
+ fgd() {
194
+ if [ -x "./node_modules/.bin/fgd" ]; then
195
+ pnpm exec fgd "$@"
196
+ else
197
+ node "$FGD_DEVKIT_PATH/scripts/fg-devkit.mjs" "$@"
198
+ fi
199
+ }
200
+ EOF
201
+ ```
202
+
203
+ 2) Reload shell config:
204
+
205
+ ```bash
206
+ source ~/.zshrc
207
+ ```
@@ -6,10 +6,10 @@
6
6
  "@lexical/list": "0.30.0",
7
7
  "@lexical/rich-text": "0.30.0",
8
8
  "@piwikpro/vue-piwik-pro": "1.4.0",
9
- "@sentry/browser": "7.67.0",
10
- "@sentry/tracing": "7.37.2",
11
- "@sentry/vue": "7.37.2",
12
- "@sentry/wasm": "7.67.0",
9
+ "@sentry/browser": "10.50.0",
10
+ "@sentry/tracing": "7.120.4",
11
+ "@sentry/vue": "10.50.0",
12
+ "@sentry/wasm": "10.50.0",
13
13
  "@tailwindcss/postcss": "4.2.4",
14
14
  "@tanstack/vue-query": "5.99.2",
15
15
  "@types/leaflet": "1.5.7",
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
3
+ "ignorePatterns": [
4
+ "**/*.d.ts",
5
+ "**/*.generated.*",
6
+ "dist/**",
7
+ "build/**",
8
+ "coverage/**",
9
+ "src/style.css",
10
+ "index.html"
11
+ ],
12
+ "publicPackages": ["fds-vue-core"]
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fg-devkit",
3
- "version": "1.7.1",
3
+ "version": "1.7.4",
4
4
  "description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -72,6 +72,8 @@
72
72
  }
73
73
  },
74
74
  "dependencies": {
75
+ "@inquirer/confirm": "^6.0.12",
76
+ "@inquirer/select": "^5.1.4",
75
77
  "@intlify/eslint-plugin-vue-i18n": "4.3.0",
76
78
  "@typescript-eslint/eslint-plugin": "8.58.2",
77
79
  "@typescript-eslint/parser": "8.58.2",
@@ -83,6 +85,7 @@
83
85
  "eslint": "10.2.1",
84
86
  "eslint-plugin-prettier": "5.5.5",
85
87
  "eslint-plugin-vue": "10.8.0",
88
+ "fallow": "^2.52.2",
86
89
  "lefthook": "2.1.6",
87
90
  "npm-check-updates": "21.0.3",
88
91
  "prettier": "3.8.3",
@@ -34,7 +34,7 @@
34
34
  "@tanstack/vue-query-devtools": "6.1.24",
35
35
  "@types/node": "22.16.5",
36
36
  "@vitejs/plugin-vue": "6.0.6",
37
- "fg-devkit": "1.7.1",
37
+ "fg-devkit": "1.7.4",
38
38
  "start-server-and-test": "2.1.3",
39
39
  "tailwindcss": "4.2.4",
40
40
  "tsx": "4.21.0",
@@ -28,6 +28,8 @@
28
28
  * pnpm exec fg-devkit format-check [prettier-args...] (same as format plus --check)
29
29
  * pnpm exec fg-devkit typecheck [vue-tsc-args...] (run vue-tsc with fg-devkit bundled package)
30
30
  * pnpm exec fg-devkit typecheck-watch [vue-tsc-args...] (same as typecheck plus --watch)
31
+ * pnpm exec fg-devkit fallow [fallow-args...] (run Fallow with fg-devkit bundled package)
32
+ * pnpm exec fg-devkit dead-code [fallow-args...] (same as: fallow dead-code [args...])
31
33
  * pnpm exec fg-devkit poeditor-download [args...] (download translations using poeditor/.poeditor.json + env token)
32
34
  * pnpm exec fg-devkit poeditor-upload [args...] (upload translations using poeditor/.poeditor.json + env token)
33
35
  * Interactive menu (no subcommand):
@@ -405,6 +407,37 @@ function runBundledTypeScript(args = []) {
405
407
  return r.status ?? 1
406
408
  }
407
409
 
410
+ /** Run Fallow from fg-devkit's bundled dependency set. */
411
+ function runBundledFallow(args = []) {
412
+ let fallowBin
413
+ try {
414
+ const fallowPkgJson = require.resolve('fallow/package.json', {
415
+ paths: [fgRoot, process.cwd()],
416
+ })
417
+ const fallowPkgRoot = path.dirname(fallowPkgJson)
418
+ fallowBin = path.join(fallowPkgRoot, 'bin', 'fallow')
419
+ if (!fs.existsSync(fallowBin)) {
420
+ throw new Error('fallow bin not found')
421
+ }
422
+ } catch {
423
+ console.error('[fg-devkit] Could not resolve bundled fallow.')
424
+ console.error('[fg-devkit] Run pnpm install in fg-devkit and in the linked consumer, then retry.')
425
+ return 1
426
+ }
427
+
428
+ const r = spawnSync(process.execPath, [fallowBin, ...args], {
429
+ stdio: 'inherit',
430
+ cwd: process.cwd(),
431
+ env: process.env,
432
+ })
433
+ if (r.error) {
434
+ console.error('[fg-devkit] Could not run bundled fallow.')
435
+ console.error('[fg-devkit]', r.error.message)
436
+ return 1
437
+ }
438
+ return r.status ?? 1
439
+ }
440
+
408
441
  /** Map subcommand name → script file (extra argv after subcommand is forwarded). */
409
442
  const DIRECT_COMMANDS = {
410
443
  'check-deps': 'check-dependency-policy.mjs',
@@ -440,7 +473,7 @@ Commands:
440
473
  fleet-vulns [projects-root] [--summary-only] [--delay-ms=<ms>]
441
474
  Run pnpm audit across Vue repos (full output + combined summary)
442
475
  fleet-bump-fg-devkit [projects-root] [--dry-run]
443
- Bump fg-devkit to latest across Vue repos, then run pnpm install
476
+ Bump fg-devkit, run sync-deps, then pnpm install across Vue repos
444
477
  policy-upgrade [policy-file] [ncu-args...]
445
478
  Upgrade policy packages via npm-check-updates (default interactive)
446
479
  sync-project-configs VS Code + tooling sync (overwrites config files)
@@ -461,6 +494,8 @@ Commands:
461
494
  format-check [prettier-args...] Run Prettier with --check using fg-devkit bundled package
462
495
  typecheck [vue-tsc-args...] Run vue-tsc using fg-devkit bundled package
463
496
  typecheck-watch [vue-tsc-args...] Run vue-tsc with --watch using fg-devkit bundled package
497
+ fallow [fallow-args...] Run Fallow using fg-devkit bundled package
498
+ dead-code [fallow-args...] Run Fallow dead code analysis (shortcut for: fallow dead-code)
464
499
  poeditor-download [args...] Download locale files via PoEditor (reads poeditor/.poeditor.json by default)
465
500
  poeditor-upload [args...] Upload locale file via PoEditor (reads poeditor/.poeditor.json by default)
466
501
  scaffold-vue <target-dir> [--name …] [--slug …] [--force] …
@@ -488,7 +523,7 @@ const MENU = [
488
523
  },
489
524
  {
490
525
  key: '3',
491
- label: 'Sync configs (vscode, eslint, prettier, tsconfig)',
526
+ label: 'Sync configs (vscode, eslint, prettier, tsconfig, fallow)',
492
527
  run: () => runNode('sync-project-configs.mjs'),
493
528
  },
494
529
  {
@@ -536,6 +571,11 @@ const MENU = [
536
571
  label: 'Sync .cursor core coding standards',
537
572
  run: () => runNode('sync-core-coding-standards.mjs'),
538
573
  },
574
+ {
575
+ key: '13',
576
+ label: 'Fallow analysis',
577
+ run: () => 0,
578
+ },
539
579
  ]
540
580
 
541
581
  const MAINTAINER_MENU = [
@@ -576,7 +616,7 @@ const MAINTAINER_MENU = [
576
616
  },
577
617
  {
578
618
  key: 'm8',
579
- label: 'Maintainer: Bump fg-devkit in all Vue repos and run pnpm install',
619
+ label: 'Maintainer: Bump fg-devkit in all Vue repos, run sync-deps, and pnpm install',
580
620
  run: () => runNode('fleet-bump-fg-devkit.mjs'),
581
621
  },
582
622
  ]
@@ -633,6 +673,38 @@ async function runInteractiveMenu() {
633
673
  // Close menu readline before spawning interactive child processes.
634
674
  rl.close()
635
675
  out = runReleaseScript([bumpType])
676
+ } else if (item.key === '13') {
677
+ let subChoice = 'cancel'
678
+ let selectPrompt = null
679
+ try {
680
+ const inquirerSelect = await import('@inquirer/select')
681
+ selectPrompt = inquirerSelect.default
682
+ } catch {
683
+ // Optional dependency; fail with a clear message if not available.
684
+ }
685
+
686
+ if (selectPrompt) {
687
+ // Close menu readline before running interactive child prompt.
688
+ rl.close()
689
+ subChoice = await selectPrompt({
690
+ message: 'Fallow — choose analysis',
691
+ choices: [
692
+ { name: 'dead code', value: 'dead-code' },
693
+ { name: 'Cancel', value: 'cancel' },
694
+ ],
695
+ default: 'dead-code',
696
+ })
697
+ } else {
698
+ rl.close()
699
+ console.error('[fg-devkit] Option 13 requires @inquirer/select but it is not available.')
700
+ console.error('[fg-devkit] Run pnpm install in your project and try again.')
701
+ return 1
702
+ }
703
+
704
+ if (subChoice === 'cancel') {
705
+ return 0
706
+ }
707
+ out = runBundledFallow([subChoice])
636
708
  } else {
637
709
  // Close menu readline before spawning interactive child processes.
638
710
  rl.close()
@@ -705,6 +777,14 @@ async function main() {
705
777
  process.exitCode = runBundledTypeScript(rest.length > 0 ? ['--watch', ...rest] : ['--noEmit', '--watch', '-p', 'tsconfig.json'])
706
778
  return
707
779
  }
780
+ if (sub === 'fallow') {
781
+ process.exitCode = runBundledFallow(rest)
782
+ return
783
+ }
784
+ if (sub === 'dead-code') {
785
+ process.exitCode = runBundledFallow(['dead-code', ...rest])
786
+ return
787
+ }
708
788
  const script = DIRECT_COMMANDS[sub]
709
789
  if (!script) {
710
790
  console.error(`[fg-devkit] Unknown command: ${sub}`)
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Bumps fg-devkit to latest in Vue repositories and runs pnpm install.
3
+ * Bumps fg-devkit to latest in Vue repositories, syncs dependency policy, and runs pnpm install.
4
4
  *
5
5
  * Usage:
6
6
  * pnpm exec fg-devkit fleet-bump-fg-devkit [projects-root] [--dry-run]
@@ -82,10 +82,12 @@ let updated = 0
82
82
  for (const repoDir of uniqueRepos) {
83
83
  const repoName = path.basename(repoDir)
84
84
  const bumpArgs = ['up', 'fg-devkit@latest']
85
+ const syncDepsArgs = ['exec', 'fg-devkit', 'sync-deps']
85
86
  const installArgs = ['install']
86
87
 
87
88
  if (dryRun) {
88
89
  console.log(`[fleet-bump-fg-devkit] DRY RUN ${repoName}: pnpm ${bumpArgs.join(' ')}`)
90
+ console.log(`[fleet-bump-fg-devkit] DRY RUN ${repoName}: pnpm ${syncDepsArgs.join(' ')}`)
89
91
  console.log(`[fleet-bump-fg-devkit] DRY RUN ${repoName}: pnpm ${installArgs.join(' ')}`)
90
92
  continue
91
93
  }
@@ -114,6 +116,29 @@ for (const repoDir of uniqueRepos) {
114
116
  continue
115
117
  }
116
118
 
119
+ const syncDeps = spawnSync('pnpm', syncDepsArgs, {
120
+ cwd: repoDir,
121
+ stdio: 'inherit',
122
+ env: {
123
+ ...process.env,
124
+ npm_config_save_prefix: '',
125
+ npm_config_save_exact: 'true',
126
+ },
127
+ })
128
+
129
+ if (syncDeps.error) {
130
+ failures += 1
131
+ console.error(`[fleet-bump-fg-devkit] FAIL ${repoName}: ${syncDeps.error.message}`)
132
+ continue
133
+ }
134
+
135
+ const syncDepsStatus = syncDeps.status ?? 1
136
+ if (syncDepsStatus !== 0) {
137
+ failures += 1
138
+ console.error(`[fleet-bump-fg-devkit] FAIL ${repoName}: sync-deps exited with code ${syncDepsStatus}`)
139
+ continue
140
+ }
141
+
117
142
  const install = spawnSync('pnpm', installArgs, {
118
143
  cwd: repoDir,
119
144
  stdio: 'inherit',
@@ -12,6 +12,29 @@ import path from 'node:path'
12
12
 
13
13
  import { getFlagValue, getPoeditorToken, loadPoeditorConfig, parseFlags, resolvePathFromCwd } from './poeditor-common.mjs'
14
14
 
15
+ const MIN_UPLOAD_INTERVAL_MS = 20_000
16
+ const MAX_UPLOAD_RETRIES = 3
17
+ const RETRYABLE_UPLOAD_ERROR_CODES = new Set(['4048'])
18
+
19
+ function sleep(ms) {
20
+ return new Promise((resolve) => {
21
+ setTimeout(resolve, ms)
22
+ })
23
+ }
24
+
25
+ function parsePoeditorError(err) {
26
+ const message = err instanceof Error ? err.message : String(err)
27
+ const match = message.match(/Upload failed:\s*(\{.*\})$/)
28
+ if (!match) return null
29
+ try {
30
+ const payload = JSON.parse(match[1])
31
+ const code = payload?.response?.code
32
+ return typeof code === 'string' ? code : null
33
+ } catch {
34
+ return null
35
+ }
36
+ }
37
+
15
38
  function getUploadType(filePath) {
16
39
  const extension = path.extname(filePath).toLowerCase().replace('.', '')
17
40
  if (!extension) {
@@ -53,6 +76,30 @@ async function uploadFile({ token, projectId, language, filePath }) {
53
76
  )
54
77
  }
55
78
 
79
+ async function uploadFileWithRetry({ token, projectId, language, filePath }) {
80
+ let attempt = 0
81
+ while (true) {
82
+ attempt += 1
83
+ try {
84
+ await uploadFile({ token, projectId, language, filePath })
85
+ return
86
+ } catch (err) {
87
+ const errorCode = parsePoeditorError(err)
88
+ const isRetryable = errorCode && RETRYABLE_UPLOAD_ERROR_CODES.has(errorCode)
89
+ const retriesLeft = MAX_UPLOAD_RETRIES - attempt
90
+ if (!isRetryable || retriesLeft < 0) {
91
+ throw err
92
+ }
93
+
94
+ const waitMs = MIN_UPLOAD_INTERVAL_MS
95
+ console.warn(
96
+ `[poeditor-upload] Received retryable POEditor error code ${errorCode}. Retrying in ${Math.ceil(waitMs / 1000)}s (${retriesLeft} retries left).`,
97
+ )
98
+ await sleep(waitMs)
99
+ }
100
+ }
101
+ }
102
+
56
103
  async function main() {
57
104
  const { flags } = parseFlags(process.argv.slice(2))
58
105
  const cfg = loadPoeditorConfig(flags)
@@ -76,17 +123,26 @@ async function main() {
76
123
 
77
124
  const uploads = cfg.uploadFiles && typeof cfg.uploadFiles === 'object' ? Object.entries(cfg.uploadFiles) : []
78
125
  if (uploads.length > 0) {
126
+ let nextUploadAt = 0
79
127
  for (const [language, file] of uploads) {
128
+ const now = Date.now()
129
+ const waitBeforeUploadMs = Math.max(0, nextUploadAt - now)
130
+ if (waitBeforeUploadMs > 0) {
131
+ console.log(`[poeditor-upload] Waiting ${Math.ceil(waitBeforeUploadMs / 1000)}s before next upload...`)
132
+ await sleep(waitBeforeUploadMs)
133
+ }
134
+
80
135
  const filePath = resolvePathFromCwd(file)
81
- await uploadFile({ token, projectId, language, filePath })
136
+ await uploadFileWithRetry({ token, projectId, language, filePath })
82
137
  console.log(`[poeditor-upload] Upload completed for ${language}: ${filePath}`)
138
+ nextUploadAt = Date.now() + MIN_UPLOAD_INTERVAL_MS
83
139
  }
84
140
  return
85
141
  }
86
142
 
87
143
  const language = cfg.defaultLanguage
88
144
  const filePath = resolvePathFromCwd(cfg.uploadFile)
89
- await uploadFile({ token, projectId, language, filePath })
145
+ await uploadFileWithRetry({ token, projectId, language, filePath })
90
146
  console.log(`[poeditor-upload] Upload completed for ${language}: ${filePath}`)
91
147
  }
92
148
 
@@ -210,8 +210,6 @@ if (policyErrors.length > 0) {
210
210
  }
211
211
 
212
212
  fs.writeFileSync(policyPath, `${JSON.stringify(policy, null, 2)}\n`)
213
-
214
- const fgDevkitRoot = path.resolve(scriptDir, '..')
215
213
  syncScaffoldVueDepsFromPolicy({ fgDevkitRoot, policyPath })
216
214
 
217
215
  console.log(`[refresh-policy] projectsRoot=${projectsRoot}`)
@@ -4,6 +4,7 @@
4
4
  * - prettier.config.js — overwritten from fg-devkit prettier sync template
5
5
  * - eslint.config.js — overwritten from fg-devkit eslint sync template
6
6
  * - tsconfig.json — overwritten from fg-devkit tsconfig app sync template
7
+ * - .fallowrc.json — overwritten from fg-devkit fallow sync template
7
8
  *
8
9
  * This command is intentionally deterministic: each run rewrites files to the shared templates.
9
10
  *
@@ -46,10 +47,11 @@ if (!fgRoot) {
46
47
  const prettierSrc = path.join(fgRoot, 'configs', 'prettier.sync-stub.js')
47
48
  const eslintStubSrc = path.join(fgRoot, 'configs', 'eslint.sync-stub.js')
48
49
  const tsconfigSrc = path.join(fgRoot, 'configs', 'tsconfig.app.sync.json')
50
+ const fallowConfigSrc = path.join(fgRoot, 'configs', 'fallowrc.sync-stub.json')
49
51
 
50
- if (!fs.existsSync(prettierSrc) || !fs.existsSync(eslintStubSrc) || !fs.existsSync(tsconfigSrc)) {
52
+ if (!fs.existsSync(prettierSrc) || !fs.existsSync(eslintStubSrc) || !fs.existsSync(tsconfigSrc) || !fs.existsSync(fallowConfigSrc)) {
51
53
  console.error(
52
- '[sync-tooling] Missing prettier.sync-stub.js, eslint.sync-stub.js, or tsconfig.app.sync.json inside fg-devkit package.',
54
+ '[sync-tooling] Missing prettier.sync-stub.js, eslint.sync-stub.js, tsconfig.app.sync.json, or fallowrc.sync-stub.json inside fg-devkit package.',
53
55
  )
54
56
  process.exit(1)
55
57
  }
@@ -58,7 +60,8 @@ try {
58
60
  fs.copyFileSync(prettierSrc, path.join(projectRoot, 'prettier.config.js'))
59
61
  fs.copyFileSync(eslintStubSrc, path.join(projectRoot, 'eslint.config.js'))
60
62
  fs.copyFileSync(tsconfigSrc, path.join(projectRoot, 'tsconfig.json'))
61
- console.log('[sync-tooling] Updated prettier.config.js, eslint.config.js, and tsconfig.json.')
63
+ fs.copyFileSync(fallowConfigSrc, path.join(projectRoot, '.fallowrc.json'))
64
+ console.log('[sync-tooling] Updated prettier.config.js, eslint.config.js, tsconfig.json, and .fallowrc.json.')
62
65
  } catch (err) {
63
66
  console.error(`[sync-tooling] ${err.message}`)
64
67
  process.exit(1)