fg-devkit 1.14.5 → 1.15.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fg-devkit",
3
- "version": "1.14.5",
3
+ "version": "1.15.0",
4
4
  "description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -33,7 +33,7 @@
33
33
  "@tanstack/vue-query-devtools": "6.1.38",
34
34
  "@types/node": "22.16.5",
35
35
  "@vitejs/plugin-vue": "6.0.8",
36
- "fg-devkit": "1.14.5",
36
+ "fg-devkit": "1.15.0",
37
37
  "tailwindcss": "4.3.3",
38
38
  "tsx": "4.23.12",
39
39
  "vite": "7.3.5"
@@ -559,7 +559,7 @@ Commands:
559
559
  fallow [fallow-args...] Run Fallow using fg-devkit bundled package
560
560
  dead-code [fallow-args...] Run Fallow dead code analysis (shortcut for: fallow dead-code)
561
561
  poeditor-download [args...] Download locale files via PoEditor (reads poeditor/.poeditor.json by default)
562
- poeditor-upload [args...] Upload locale file via PoEditor (reads poeditor/.poeditor.json by default)
562
+ poeditor-upload [args...] Upload locale file via PoEditor (reads poeditor/.poeditor.json by default; --sync-terms deletes missing terms)
563
563
  scaffold-vue <target-dir> [--name …] [--slug …] [--force] …
564
564
  Copy the bundled Vue scaffold into target-dir (see script header)
565
565
  sync-scaffold-vue-deps [policy-file]
@@ -589,11 +589,14 @@ function isFgDevkitMaintainerContext() {
589
589
 
590
590
  async function runInteractiveMenu() {
591
591
  let selectPrompt
592
+ let confirmPrompt
592
593
  try {
593
594
  const inquirerSelect = await import('@inquirer/select')
595
+ const inquirerConfirm = await import('@inquirer/confirm')
594
596
  selectPrompt = inquirerSelect.default
597
+ confirmPrompt = inquirerConfirm.default
595
598
  } catch {
596
- console.error('[fg-devkit] Interactive menu requires @inquirer/select but it is not available.')
599
+ console.error('[fg-devkit] Interactive menu requires @inquirer/select and @inquirer/confirm but they are not available.')
597
600
  console.error('[fg-devkit] Run pnpm install in your project and try again.')
598
601
  return 1
599
602
  }
@@ -769,7 +772,34 @@ async function runInteractiveMenu() {
769
772
  loop: false,
770
773
  })
771
774
  if (choice === 'cancel') continue
772
- return runSelectedAction(() => runNode(choice === 'download' ? 'poeditor-download.mjs' : 'poeditor-upload.mjs'))
775
+ if (choice === 'download') {
776
+ return runSelectedAction(() => runNode('poeditor-download.mjs'))
777
+ }
778
+
779
+ const uploadMode = await selectPrompt({
780
+ message: 'Upload mode',
781
+ choices: [
782
+ { name: 'Upload (keep terms missing from file)', value: 'upload' },
783
+ {
784
+ name: 'Upload + sync terms (delete terms missing from file)',
785
+ value: 'upload-sync',
786
+ },
787
+ { name: 'Cancel', value: 'cancel' },
788
+ ],
789
+ default: 'upload',
790
+ loop: false,
791
+ })
792
+ if (uploadMode === 'cancel') continue
793
+ if (uploadMode === 'upload-sync') {
794
+ const confirmed = await confirmPrompt({
795
+ message:
796
+ 'This is a destructive POEditor action: terms not present in the uploaded file will be permanently deleted from the project (including all language translations). Continue?',
797
+ default: false,
798
+ })
799
+ if (!confirmed) continue
800
+ }
801
+ const uploadArgs = uploadMode === 'upload-sync' ? ['--sync-terms'] : []
802
+ return runSelectedAction(() => runNode('poeditor-upload.mjs', uploadArgs))
773
803
  }
774
804
 
775
805
  if (group === 'dependency-policy') {
@@ -4,7 +4,10 @@
4
4
  *
5
5
  * Usage:
6
6
  * pnpm exec fg-devkit poeditor-upload [--config ./poeditor/.poeditor.json] [--project-id 123]
7
- * [--language sv] [--file ./src/lang/sv.json] [--api-token xxx]
7
+ * [--language sv] [--file ./src/lang/sv.json] [--api-token xxx] [--sync-terms]
8
+ *
9
+ * --sync-terms: Pass sync_terms=1 to PoEditor so terms missing from the uploaded file
10
+ * are deleted from the project (destructive; use with care).
8
11
  */
9
12
 
10
13
  import fs from 'node:fs'
@@ -22,6 +25,12 @@ function sleep(ms) {
22
25
  })
23
26
  }
24
27
 
28
+ function isTruthyFlag(value) {
29
+ if (value === undefined) return false
30
+ const normalized = String(value).trim().toLowerCase()
31
+ return normalized === 'true' || normalized === '1' || normalized === 'yes'
32
+ }
33
+
25
34
  function parsePoeditorError(err) {
26
35
  const message = err instanceof Error ? err.message : String(err)
27
36
  const match = message.match(/Upload failed:\s*(\{.*\})$/)
@@ -46,7 +55,7 @@ function getUploadType(filePath) {
46
55
  return extension
47
56
  }
48
57
 
49
- async function uploadFile({ token, projectId, language, filePath }) {
58
+ async function uploadFile({ token, projectId, language, filePath, syncTerms = false }) {
50
59
  const fileBuffer = fs.readFileSync(filePath)
51
60
  const fileName = path.basename(filePath)
52
61
  const uploadType = getUploadType(filePath)
@@ -56,6 +65,7 @@ async function uploadFile({ token, projectId, language, filePath }) {
56
65
  form.append('updating', 'terms_translations')
57
66
  form.append('language', language)
58
67
  form.append('overwrite', '1')
68
+ form.append('sync_terms', syncTerms ? '1' : '0')
59
69
  form.append('type', uploadType)
60
70
  form.append('file', new Blob([fileBuffer]), fileName)
61
71
 
@@ -70,18 +80,20 @@ async function uploadFile({ token, projectId, language, filePath }) {
70
80
  if (data?.response?.status !== 'success') {
71
81
  throw new Error(`Upload failed: ${JSON.stringify(data)}`)
72
82
  }
83
+ const terms = data?.result?.terms
73
84
  const translations = data?.result?.translations
85
+ const syncNote = syncTerms ? `, terms deleted: ${terms?.deleted ?? 0}` : ''
74
86
  console.log(
75
- `[poeditor-upload] Upload complete for "${language}" (parsed: ${translations?.parsed ?? 0}, updated: ${translations?.updated ?? 0}, added: ${translations?.added ?? 0})`,
87
+ `[poeditor-upload] Upload complete for "${language}" (parsed: ${translations?.parsed ?? 0}, updated: ${translations?.updated ?? 0}, added: ${translations?.added ?? 0}${syncNote})`,
76
88
  )
77
89
  }
78
90
 
79
- async function uploadFileWithRetry({ token, projectId, language, filePath }) {
91
+ async function uploadFileWithRetry({ token, projectId, language, filePath, syncTerms = false }) {
80
92
  let attempt = 0
81
93
  while (true) {
82
94
  attempt += 1
83
95
  try {
84
- await uploadFile({ token, projectId, language, filePath })
96
+ await uploadFile({ token, projectId, language, filePath, syncTerms })
85
97
  return
86
98
  } catch (err) {
87
99
  const errorCode = parsePoeditorError(err)
@@ -111,20 +123,32 @@ async function main() {
111
123
  }
112
124
 
113
125
  const projectId = getFlagValue(flags, 'project-id', cfg.projectId)
126
+ const syncTerms = isTruthyFlag(getFlagValue(flags, 'sync-terms'))
127
+ if (syncTerms) {
128
+ console.warn(
129
+ '[poeditor-upload] --sync-terms enabled: terms not present in the uploaded file will be deleted from PoEditor.',
130
+ )
131
+ }
132
+
114
133
  const hasExplicitLanguage = flags.has('language')
115
134
  const hasExplicitFile = flags.has('file')
116
135
  if (hasExplicitLanguage || hasExplicitFile) {
117
136
  const language = getFlagValue(flags, 'language', cfg.defaultLanguage)
118
137
  const filePath = resolvePathFromCwd(getFlagValue(flags, 'file', cfg.uploadFile))
119
- await uploadFile({ token, projectId, language, filePath })
138
+ await uploadFile({ token, projectId, language, filePath, syncTerms })
120
139
  console.log(`[poeditor-upload] Upload completed for ${language}: ${filePath}`)
121
140
  return
122
141
  }
123
142
 
124
143
  const uploads = cfg.uploadFiles && typeof cfg.uploadFiles === 'object' ? Object.entries(cfg.uploadFiles) : []
125
144
  if (uploads.length > 0) {
145
+ if (syncTerms && uploads.length > 1) {
146
+ console.warn(
147
+ '[poeditor-upload] --sync-terms with multiple uploadFiles: syncing only on the first file to avoid deleting terms present in other language files.',
148
+ )
149
+ }
126
150
  let nextUploadAt = 0
127
- for (const [language, file] of uploads) {
151
+ for (const [index, [language, file]] of uploads.entries()) {
128
152
  const now = Date.now()
129
153
  const waitBeforeUploadMs = Math.max(0, nextUploadAt - now)
130
154
  if (waitBeforeUploadMs > 0) {
@@ -133,7 +157,8 @@ async function main() {
133
157
  }
134
158
 
135
159
  const filePath = resolvePathFromCwd(file)
136
- await uploadFileWithRetry({ token, projectId, language, filePath })
160
+ const shouldSyncTerms = syncTerms && index === 0
161
+ await uploadFileWithRetry({ token, projectId, language, filePath, syncTerms: shouldSyncTerms })
137
162
  console.log(`[poeditor-upload] Upload completed for ${language}: ${filePath}`)
138
163
  nextUploadAt = Date.now() + MIN_UPLOAD_INTERVAL_MS
139
164
  }
@@ -142,7 +167,7 @@ async function main() {
142
167
 
143
168
  const language = cfg.defaultLanguage
144
169
  const filePath = resolvePathFromCwd(cfg.uploadFile)
145
- await uploadFileWithRetry({ token, projectId, language, filePath })
170
+ await uploadFileWithRetry({ token, projectId, language, filePath, syncTerms })
146
171
  console.log(`[poeditor-upload] Upload completed for ${language}: ${filePath}`)
147
172
  }
148
173