fg-devkit 1.2.0 → 1.3.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.
package/README.md CHANGED
@@ -52,6 +52,24 @@ pnpm exec fgd prune-consumer-deps
52
52
  pnpm install
53
53
  ```
54
54
 
55
+ To sync standard consumer scripts to `fgd` commands:
56
+
57
+ ```bash
58
+ pnpm exec fgd sync-consumer-scripts
59
+ ```
60
+
61
+ To migrate from Husky + lint-staged to Lefthook:
62
+
63
+ ```bash
64
+ pnpm exec fgd setup-lefthook
65
+ ```
66
+
67
+ Run full consumer setup in one command:
68
+
69
+ ```bash
70
+ pnpm exec fgd full-consumer-setup
71
+ ```
72
+
55
73
  ## tsconfig in consuming repos
56
74
 
57
75
  Use `fgd sync-project-configs` (or `fgd sync-tooling-configs`) to write the local `tsconfig.json` template from `fg-devkit`.
@@ -49,10 +49,8 @@
49
49
  "devDependencies": {
50
50
  "@chromatic-com/storybook": "5.0.1",
51
51
  "@eslint/js": "9.39.1",
52
- "@intlify/eslint-plugin-vue-i18n": "4.3.0",
53
52
  "@intlify/unplugin-vue-i18n": "11.0.7",
54
53
  "@playwright/test": "1.59.1",
55
- "@rushstack/eslint-patch": "1.16.1",
56
54
  "@storybook/addon-a11y": "10.1.10",
57
55
  "@storybook/addon-docs": "10.1.10",
58
56
  "@storybook/addon-vitest": "10.2.12",
@@ -62,45 +60,27 @@
62
60
  "@tsconfig/node22": "22.0.5",
63
61
  "@types/lodash-es": "4.17.12",
64
62
  "@types/node": "22.16.5",
65
- "@typescript-eslint/eslint-plugin": "8.57.2",
66
- "@typescript-eslint/parser": "8.57.2",
67
63
  "@vitest/browser": "3.2.4",
68
- "@vitest/eslint-plugin": "1.6.14",
69
64
  "@vue/compiler-dom": "3.5.29",
70
- "@vue/eslint-config-prettier": "10.2.0",
71
65
  "@vue/eslint-config-standard": "8.0.1",
72
- "@vue/eslint-config-typescript": "14.7.0",
73
66
  "@vue/server-renderer": "3.5.29",
74
67
  "@vue/test-utils": "2.4.6",
75
- "@vue/tsconfig": "0.8.1",
76
68
  "@vuepic/vue-datepicker": "6.0.3",
77
69
  "arg": "5.0.2",
78
70
  "babel-eslint": "10.1.0",
79
- "commitizen": "4.3.1",
80
- "eslint": "10.0.2",
81
- "eslint-plugin-prettier": "5.5.5",
82
71
  "eslint-plugin-storybook": "10.1.10",
83
- "eslint-plugin-vue": "10.8.0",
84
72
  "globals": "15.11.0",
85
- "husky": "9.1.6",
86
73
  "jsdom": "26.1.0",
87
- "jsonc-eslint-parser": "2.4.1",
88
- "lint-staged": "16.4.0",
89
74
  "postcss": "8.5.6",
90
- "prettier": "3.8.1",
91
75
  "sass": "1.97.3",
92
76
  "start-server-and-test": "2.1.3",
93
77
  "storybook": "10.2.12",
94
78
  "tsx": "4.21.0",
95
- "typescript": "5.9.3",
96
79
  "vega": "6.2.0",
97
80
  "vite-plugin-dts": "4.5.4",
98
81
  "vite-plugin-vue-devtools": "8.0.7",
99
82
  "vitest": "3.2.4",
100
- "vue-eslint-parser": "10.4.0",
101
- "vue-tsc": "3.2.6",
102
- "yaml": "2.8.3",
103
- "yaml-eslint-parser": "1.3.1"
83
+ "yaml": "2.8.3"
104
84
  }
105
85
  },
106
86
  "managedAnywhere": {
@@ -0,0 +1,4 @@
1
+ pre-commit:
2
+ commands:
3
+ lint-check:
4
+ run: corepack pnpm lint-check
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fg-devkit",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -15,6 +15,7 @@
15
15
  "./tsconfig.app.sync.json": "./configs/tsconfig.app.sync.json",
16
16
  "./eslint.config.base.js": "./configs/eslint.config.base.js",
17
17
  "./prettier.config.js": "./configs/prettier.config.js",
18
+ "./lefthook.yml": "./configs/lefthook.yml",
18
19
  "./vscode-settings.json": "./configs/vscode-settings.json",
19
20
  "./dependency-policy.vue-workspace.json": "./configs/dependency-policy.vue-workspace.json",
20
21
  "./vuln-commit": "./scripts/vuln-commit.mjs",
@@ -24,6 +25,8 @@
24
25
  "./scripts/sync-vscode-settings.mjs": "./scripts/sync-vscode-settings.mjs",
25
26
  "./scripts/sync-tooling-configs.mjs": "./scripts/sync-tooling-configs.mjs",
26
27
  "./scripts/sync-project-configs.mjs": "./scripts/sync-project-configs.mjs",
28
+ "./scripts/sync-consumer-scripts.mjs": "./scripts/sync-consumer-scripts.mjs",
29
+ "./scripts/sync-consumer-lefthook.mjs": "./scripts/sync-consumer-lefthook.mjs",
27
30
  "./scripts/prune-consumer-deps.mjs": "./scripts/prune-consumer-deps.mjs",
28
31
  "./scripts/check-dependency-policy.mjs": "./scripts/check-dependency-policy.mjs",
29
32
  "./scripts/sync-dependency-policy.mjs": "./scripts/sync-dependency-policy.mjs",
@@ -60,11 +63,13 @@
60
63
  "@typescript-eslint/parser": "8.58.2",
61
64
  "@vitest/eslint-plugin": "1.6.16",
62
65
  "@vue/eslint-config-prettier": "10.2.0",
66
+ "@vue/eslint-config-typescript": "14.7.0",
63
67
  "@vue/tsconfig": "0.9.1",
64
68
  "commitizen": "4.3.1",
65
69
  "eslint": "10.2.1",
66
70
  "eslint-plugin-prettier": "5.5.5",
67
71
  "eslint-plugin-vue": "10.8.0",
72
+ "lefthook": "2.1.6",
68
73
  "prettier": "3.8.3",
69
74
  "typescript": "5.9.3",
70
75
  "vue-eslint-parser": "10.4.0",
@@ -8,7 +8,10 @@
8
8
  * pnpm exec fg-devkit sync-project-configs (VS Code + tooling sync; overwrites config files)
9
9
  * pnpm exec fg-devkit sync-tooling-configs (tooling sync only; overwrites config files)
10
10
  * pnpm exec fg-devkit sync-vscode-settings
11
+ * pnpm exec fg-devkit sync-consumer-scripts
12
+ * pnpm exec fg-devkit setup-lefthook
11
13
  * pnpm exec fg-devkit prune-consumer-deps [--dry-run]
14
+ * pnpm exec fg-devkit full-consumer-setup
12
15
  * pnpm exec fg-devkit create-release [patch|minor|major]
13
16
  * pnpm exec fg-devkit commit [cz-args...] (Commitizen; uses fg-devkit bundled cz adapter)
14
17
  * pnpm exec fg-devkit commit-vuln (staged package.json version bumps → vuln() commit)
@@ -77,6 +80,79 @@ function runReleaseScript(args = []) {
77
80
  return r.status ?? 1
78
81
  }
79
82
 
83
+ /** Run pnpm install in the consuming repo after setup steps. */
84
+ function runPnpmInstall() {
85
+ const r = spawnSync('pnpm', ['install'], {
86
+ stdio: 'inherit',
87
+ cwd: process.cwd(),
88
+ env: process.env,
89
+ })
90
+ if (r.error) {
91
+ console.error('[fg-devkit]', r.error.message)
92
+ return 1
93
+ }
94
+ return r.status ?? 1
95
+ }
96
+
97
+ /** Run Lefthook from fg-devkit bundled dependency set. */
98
+ function runBundledLefthook(args = []) {
99
+ let lefthookBin
100
+ try {
101
+ lefthookBin = require.resolve('lefthook/bin/index.js')
102
+ } catch {
103
+ console.error('[fg-devkit] Could not resolve bundled lefthook. Reinstall fg-devkit and retry.')
104
+ return 1
105
+ }
106
+
107
+ const r = spawnSync(process.execPath, [lefthookBin, ...args], {
108
+ stdio: 'inherit',
109
+ cwd: process.cwd(),
110
+ env: process.env,
111
+ })
112
+ if (r.error) {
113
+ console.error('[fg-devkit]', r.error.message)
114
+ return 1
115
+ }
116
+ return r.status ?? 1
117
+ }
118
+
119
+ /** Setup lefthook config and install git hooks in the consumer repository. */
120
+ function runSetupLefthook() {
121
+ const syncCode = runNode('sync-consumer-lefthook.mjs')
122
+ if (syncCode !== 0) return syncCode
123
+ return runBundledLefthook(['install', '--reset-hooks-path'])
124
+ }
125
+
126
+ /**
127
+ * Full consumer setup flow:
128
+ * - Step 7: remove unnecessary deps
129
+ * - Sync required consumer scripts for fgd commands
130
+ * - Setup lefthook and migrate from husky/lint-staged
131
+ * - Step 3: sync project configs
132
+ * - Step 2: sync dependency policy
133
+ * - Then run pnpm install
134
+ */
135
+ function runFullConsumerSetup() {
136
+ console.log('[fg-devkit] Running full consumer sync and setup')
137
+
138
+ const step7 = runNode('prune-consumer-deps.mjs')
139
+ if (step7 !== 0) return step7
140
+
141
+ const scriptsStep = runNode('sync-consumer-scripts.mjs')
142
+ if (scriptsStep !== 0) return scriptsStep
143
+
144
+ const hooksStep = runSetupLefthook()
145
+ if (hooksStep !== 0) return hooksStep
146
+
147
+ const step3 = runNode('sync-project-configs.mjs')
148
+ if (step3 !== 0) return step3
149
+
150
+ const step2 = runNode('sync-dependency-policy.mjs')
151
+ if (step2 !== 0) return step2
152
+
153
+ return runPnpmInstall()
154
+ }
155
+
80
156
  /** Prompt release type in CLI before invoking release.sh to avoid a "stuck" terminal feeling. */
81
157
  async function promptReleaseType(rl) {
82
158
  console.log('\nSelect release type:')
@@ -220,6 +296,8 @@ const DIRECT_COMMANDS = {
220
296
  'sync-project-configs': 'sync-project-configs.mjs',
221
297
  'sync-tooling-configs': 'sync-tooling-configs.mjs',
222
298
  'sync-vscode-settings': 'sync-vscode-settings.mjs',
299
+ 'sync-consumer-scripts': 'sync-consumer-scripts.mjs',
300
+ 'setup-lefthook': 'setup-lefthook',
223
301
  'prune-consumer-deps': 'prune-consumer-deps.mjs',
224
302
  }
225
303
 
@@ -233,7 +311,10 @@ Commands:
233
311
  sync-project-configs VS Code + tooling sync (overwrites config files)
234
312
  sync-tooling-configs Tooling sync only (overwrites config files)
235
313
  sync-vscode-settings Copy shared VS Code settings into .vscode/settings.json
314
+ sync-consumer-scripts Enforce standard fgd scripts in consumer package.json
315
+ setup-lefthook Setup lefthook and migrate from husky/lint-staged
236
316
  prune-consumer-deps [--dry-run] Remove consumer tooling deps bundled by fg-devkit
317
+ full-consumer-setup Run: step 7 -> scripts -> lefthook -> step 3 -> step 2 -> pnpm install
237
318
  create-release [bump] Version bump, git push, GitHub release (patch|minor|major; omit = prompt)
238
319
  (alias: createRelease)
239
320
  commit [args...] Commitizen (git-cz) with fg-devkit bundled adapter
@@ -285,9 +366,19 @@ const MENU = [
285
366
  },
286
367
  {
287
368
  key: '7',
288
- label: 'Remove deps unnecessary deps in package.json',
369
+ label: 'Remove unnecessary deps in package.json',
289
370
  run: () => runNode('prune-consumer-deps.mjs'),
290
371
  },
372
+ {
373
+ key: '8',
374
+ label: 'Full consumer setup (7 -> scripts -> lefthook -> 3 -> 2 -> pnpm install)',
375
+ run: () => runFullConsumerSetup(),
376
+ },
377
+ {
378
+ key: '9',
379
+ label: 'Setup lefthook (migrate from husky/lint-staged)',
380
+ run: () => runSetupLefthook(),
381
+ },
291
382
  ]
292
383
 
293
384
  async function runInteractiveMenu() {
@@ -348,6 +439,14 @@ async function main() {
348
439
  }
349
440
 
350
441
  if (sub) {
442
+ if (sub === 'setup-lefthook') {
443
+ process.exitCode = runSetupLefthook()
444
+ return
445
+ }
446
+ if (sub === 'full-consumer-setup') {
447
+ process.exitCode = runFullConsumerSetup()
448
+ return
449
+ }
351
450
  if (sub === 'create-release' || sub === 'createRelease') {
352
451
  process.exitCode = runReleaseScript(rest)
353
452
  return
@@ -20,13 +20,20 @@ const BUNDLED_TOOLING_PACKAGES = new Set([
20
20
  '@typescript-eslint/parser',
21
21
  '@vitest/eslint-plugin',
22
22
  '@vue/eslint-config-prettier',
23
+ '@vue/eslint-config-typescript',
23
24
  '@vue/tsconfig',
25
+ '@rushstack/eslint-patch',
24
26
  'commitizen',
25
27
  'eslint',
26
28
  'eslint-plugin-prettier',
27
29
  'eslint-plugin-vue',
30
+ 'husky',
31
+ 'jsonc-eslint-parser',
32
+ 'lint-staged',
33
+ 'lefthook',
28
34
  'prettier',
29
35
  'typescript',
36
+ 'yaml-eslint-parser',
30
37
  'vue-eslint-parser',
31
38
  'vue-tsc',
32
39
  ])
@@ -29,6 +29,7 @@ import { collectPinViolations } from './dependency-policy-pins.mjs'
29
29
 
30
30
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
31
31
  const fgDevkitRoot = path.resolve(scriptDir, '..')
32
+ const fgDevkitPackageJsonPath = path.join(fgDevkitRoot, 'package.json')
32
33
 
33
34
  const projectsRoot = process.argv[2]
34
35
  ? path.resolve(process.cwd(), process.argv[2])
@@ -41,6 +42,15 @@ const policyPath = process.argv[3]
41
42
  /** Relative paths under projectsRoot whose package.json files are ignored (maintainer choice). */
42
43
  /** Relative path prefixes under projectsRoot to skip when scanning (add repo names as needed). */
43
44
  const SKIP_DIR_PREFIXES = []
45
+ const EXTRA_TOOLING_EXCLUDES = new Set(['husky', 'lint-staged', '@rushstack/eslint-patch', 'jsonc-eslint-parser', 'yaml-eslint-parser'])
46
+ const EXCLUDED_POLICY_PACKAGES = (() => {
47
+ try {
48
+ const pkg = JSON.parse(fs.readFileSync(fgDevkitPackageJsonPath, 'utf8'))
49
+ return new Set([...Object.keys(pkg.dependencies ?? {}), ...EXTRA_TOOLING_EXCLUDES])
50
+ } catch {
51
+ return EXTRA_TOOLING_EXCLUDES
52
+ }
53
+ })()
44
54
 
45
55
  /** Recursively collect every package.json path under dir, skipping node_modules and .git. */
46
56
  function walk(dir, acc = []) {
@@ -147,6 +157,9 @@ const outAny = {}
147
157
 
148
158
  // Pick highest semver per package and assign to dependencies, devDependencies, or managedAnywhere.
149
159
  for (const [name, u] of usage) {
160
+ if (EXCLUDED_POLICY_PACKAGES.has(name)) {
161
+ continue
162
+ }
150
163
  if (u.partsList.length === 0) {
151
164
  continue
152
165
  }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Syncs Lefthook setup into a consuming repo:
4
+ * - writes lefthook.yml from fg-devkit template
5
+ * - removes legacy .husky directory (if present)
6
+ * - removes legacy lint-staged config block from package.json
7
+ * - removes legacy prepare hook scripts for husky/lefthook bootstrap
8
+ *
9
+ * Usage:
10
+ * node node_modules/fg-devkit/scripts/sync-consumer-lefthook.mjs
11
+ * pnpm exec fg-devkit setup-lefthook
12
+ */
13
+
14
+ import fs from 'node:fs'
15
+ import path from 'node:path'
16
+ import { fileURLToPath } from 'node:url'
17
+
18
+ const projectRoot = process.cwd()
19
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
20
+ const fgRoot = path.resolve(scriptDir, '..')
21
+
22
+ const lefthookTemplate = path.join(fgRoot, 'configs', 'lefthook.yml')
23
+ const lefthookTarget = path.join(projectRoot, 'lefthook.yml')
24
+ const huskyDir = path.join(projectRoot, '.husky')
25
+ const packageJsonPath = path.join(projectRoot, 'package.json')
26
+
27
+ if (!fs.existsSync(lefthookTemplate)) {
28
+ console.error(`[sync-consumer-lefthook] Missing template: ${lefthookTemplate}`)
29
+ process.exit(1)
30
+ }
31
+
32
+ if (!fs.existsSync(packageJsonPath)) {
33
+ console.error(`[sync-consumer-lefthook] package.json not found at ${packageJsonPath}`)
34
+ process.exit(1)
35
+ }
36
+
37
+ let pkg
38
+ try {
39
+ pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
40
+ } catch (err) {
41
+ console.error(`[sync-consumer-lefthook] Failed to parse package.json: ${err.message}`)
42
+ process.exit(1)
43
+ }
44
+
45
+ pkg.scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
46
+ if (
47
+ pkg.scripts.prepare &&
48
+ (pkg.scripts.prepare.includes('husky') || pkg.scripts.prepare.includes('setup-lefthook'))
49
+ ) {
50
+ delete pkg.scripts.prepare
51
+ }
52
+
53
+ if (pkg['lint-staged'] !== undefined) {
54
+ delete pkg['lint-staged']
55
+ }
56
+
57
+ try {
58
+ fs.copyFileSync(lefthookTemplate, lefthookTarget)
59
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
60
+ if (fs.existsSync(huskyDir)) {
61
+ fs.rmSync(huskyDir, { recursive: true, force: true })
62
+ }
63
+ console.log('[sync-consumer-lefthook] Synced lefthook.yml and removed .husky/lint-staged legacy setup.')
64
+ } catch (err) {
65
+ console.error(`[sync-consumer-lefthook] ${err.message}`)
66
+ process.exit(1)
67
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Syncs standard fg-devkit npm scripts into a consuming repo package.json.
4
+ *
5
+ * This updates only known script keys and leaves all other scripts untouched.
6
+ *
7
+ * Usage:
8
+ * node node_modules/fg-devkit/scripts/sync-consumer-scripts.mjs
9
+ * pnpm exec fg-devkit sync-consumer-scripts
10
+ */
11
+
12
+ import fs from 'node:fs'
13
+ import path from 'node:path'
14
+
15
+ const REQUIRED_SCRIPTS = {
16
+ 'lint:ts': 'pnpm exec fgd typecheck',
17
+ lint: 'pnpm exec fgd lint --fix .',
18
+ 'lint-check': 'pnpm exec fgd lint .',
19
+ 'sync:vscode:settings': 'pnpm exec fgd sync-vscode-settings',
20
+ 'create:release': 'pnpm exec fgd create-release',
21
+ }
22
+
23
+ function main() {
24
+ const packageJsonPath = path.join(process.cwd(), 'package.json')
25
+ if (!fs.existsSync(packageJsonPath)) {
26
+ console.error(`[sync-consumer-scripts] package.json not found at ${packageJsonPath}`)
27
+ process.exit(1)
28
+ }
29
+
30
+ let pkg
31
+ try {
32
+ pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
33
+ } catch (err) {
34
+ console.error(`[sync-consumer-scripts] Failed to parse package.json: ${err.message}`)
35
+ process.exit(1)
36
+ }
37
+
38
+ pkg.scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
39
+
40
+ const changed = []
41
+ for (const [name, value] of Object.entries(REQUIRED_SCRIPTS)) {
42
+ if (pkg.scripts[name] !== value) {
43
+ pkg.scripts[name] = value
44
+ changed.push(name)
45
+ }
46
+ }
47
+
48
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
49
+
50
+ if (changed.length === 0) {
51
+ console.log('[sync-consumer-scripts] package.json scripts already up to date.')
52
+ return
53
+ }
54
+
55
+ console.log('[sync-consumer-scripts] Updated scripts:')
56
+ for (const name of changed) {
57
+ console.log(` - ${name}`)
58
+ }
59
+ }
60
+
61
+ main()