Skip to content
This repository was archived by the owner on May 19, 2025. It is now read-only.

Commit 3485188

Browse files
authored
Fix precommit hook (#289)
* fix: precommit hook not seeing diffs when path to ignore set Signed-off-by: Paul-Xavier Ceccaldi <pix@wttj.co> * chore: release v3.1.4-beta.0 * fix: add tests on precommit hook code Signed-off-by: Paul-Xavier Ceccaldi <pix@wttj.co> * fix: add test on git command fail Signed-off-by: Paul-Xavier Ceccaldi <pix@wttj.co> * chore: release v3.1.4-beta.1 * chore: release v3.1.4 * chore: update README and js gardening Signed-off-by: Paul-Xavier Ceccaldi <pix@wttj.co> * chore: release v3.1.5 --------- Signed-off-by: Paul-Xavier Ceccaldi <pix@wttj.co>
1 parent eed48db commit 3485188

File tree

10 files changed

+430
-198
lines changed

10 files changed

+430
-198
lines changed

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,38 @@ All notable changes to this project will be documented in this file. Dates are d
44

55
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
66

7+
#### [3.1.5](https://github.com/WTTJ/front-config/compare/3.1.4...3.1.5)
8+
9+
- chore: update README and js gardening [`37c9dcf`](https://github.com/WTTJ/front-config/commit/37c9dcfd0abd23ef6ce187e82898479c5f8a8afc)
10+
11+
#### [3.1.4](https://github.com/WTTJ/front-config/compare/3.1.4-beta.1...3.1.4)
12+
13+
> 14 May 2025
14+
15+
- chore: release v3.1.4 [`c7e1815`](https://github.com/WTTJ/front-config/commit/c7e1815d247945310996e269cbd0877e95485c7d)
16+
17+
#### [3.1.4-beta.1](https://github.com/WTTJ/front-config/compare/3.1.4-beta.0...3.1.4-beta.1)
18+
19+
> 14 May 2025
20+
21+
- fix: add test on git command fail [`1dd46d2`](https://github.com/WTTJ/front-config/commit/1dd46d2b6a68710b91c9cb8b189d60570de73ae0)
22+
- fix: add tests on precommit hook code [`eb53763`](https://github.com/WTTJ/front-config/commit/eb5376358e6f952123a3adbf2dab3ae44862d247)
23+
- chore: release v3.1.4-beta.1 [`1b7e6b8`](https://github.com/WTTJ/front-config/commit/1b7e6b865dd328db9d24f86f8d32ba06edb5c301)
24+
25+
#### [3.1.4-beta.0](https://github.com/WTTJ/front-config/compare/3.1.3...3.1.4-beta.0)
26+
27+
> 28 April 2025
28+
29+
- chore(conf): configure tests and improve i18n tooling [`#285`](https://github.com/WTTJ/front-config/pull/285)
30+
- chore: release v3.1.4-beta.0 [`f2bad17`](https://github.com/WTTJ/front-config/commit/f2bad1721aeef241db72b70d9ad27caab67da877)
31+
- fix: precommit hook not seeing diffs when path to ignore set [`f83a17b`](https://github.com/WTTJ/front-config/commit/f83a17b16818ece63ec2b2e77003dfc9237900c4)
32+
733
#### [3.1.3](https://github.com/WTTJ/front-config/compare/3.1.3-beta.1...3.1.3)
834

35+
> 14 April 2025
36+
937
- Fix tests [`#286`](https://github.com/WTTJ/front-config/pull/286)
38+
- chore: release v3.1.3 [`f908c3b`](https://github.com/WTTJ/front-config/commit/f908c3b8e6a3f7032d487b09626aaf1239178475)
1039
- chore: fix vulnerability [`c2bc427`](https://github.com/WTTJ/front-config/commit/c2bc4277a382f78a053037d1451c47475b2ff68d)
1140
- style: add precision to README [`effb2f0`](https://github.com/WTTJ/front-config/commit/effb2f0f6d5b5a37145e44c6dbe372d474194aae)
1241

lib/i18n/README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Add the following code to your circleci configuration file and adapt it to your
5757

5858
```yaml
5959
jobs:
60-
update_translations:
60+
check_translations:
6161
<<: *defaults
6262
machine:
6363
image: default
@@ -67,7 +67,15 @@ Add the following code to your circleci configuration file and adapt it to your
6767
- run:
6868
name: Check i18n translations between code and generated source locales (en-US by default)
6969
command: |
70-
node path_to_your_project/node_modules/wttj-config/lib/i18n/check.mjs
70+
node ~/CHANGE_ME_FOR_YOUR_APP_NAME/node_modules/wttj-config/lib/i18n/check.mjs
71+
72+
update_translations:
73+
<<: *defaults
74+
machine:
75+
image: default
76+
steps:
77+
- *restore_repo
78+
- *restore_node_modules
7179
- run:
7280
name: Download and install lokalise-cli v2
7381
command: |
@@ -86,10 +94,14 @@ Add the following code to your circleci configuration file and adapt it to your
8694
workflows:
8795
test_and_build:
8896
jobs:
89-
- update_translations:
97+
- check_translations:
9098
context: i18n
9199
requires:
92100
- checkout
101+
- update_translations:
102+
context: i18n
103+
requires:
104+
- check_translations
93105
filters:
94106
branches:
95107
only: main

lib/i18n/commands.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { readFileSync } from 'node:fs'
2+
3+
export const stagedFilesNumberCommand = () => {
4+
const { config } = JSON.parse(readFileSync('package.json'))
5+
const excludeString = config.i18n.path_to_ignore
6+
? `":(exclude)${config.i18n.path_to_ignore}"`
7+
: ''
8+
const extractFromString = config.i18n.extract_from_pattern
9+
10+
return `git --no-pager diff --staged --name-only "${extractFromString}" ${excludeString} | wc -l`
11+
}

lib/i18n/commands.test.mjs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { promisify } from 'util'
2+
import { exec } from 'child_process'
3+
4+
import { describe, expect, it } from 'vitest'
5+
import { vol } from 'memfs'
6+
7+
import { stagedFilesNumberCommand } from './commands.mjs'
8+
9+
describe('test command functions', () => {
10+
it('should run git command without failing when path_to_ignore is set', async () => {
11+
vol.fromJSON({
12+
'package.json': JSON.stringify({
13+
config: {
14+
i18n: {
15+
app_name: 'your-app-name',
16+
default_language_filename: 'en-US',
17+
extract_from_pattern: 'src/**/*.js',
18+
locales_dir_path: '__mocks__/locales',
19+
path_to_ignore: 'pouet',
20+
},
21+
},
22+
}),
23+
})
24+
const execute = promisify(exec)
25+
let fail = false
26+
let output
27+
try {
28+
output = await execute(stagedFilesNumberCommand())
29+
} catch (e) {
30+
expect(e).toBeInstanceOf(Error)
31+
fail = true
32+
}
33+
expect(output.stderr).toBe('')
34+
expect(fail).toBe(false)
35+
})
36+
37+
it('should run git command without failing when path_to_ignore is not set', async () => {
38+
vol.fromJSON({
39+
'package.json': JSON.stringify({
40+
config: {
41+
i18n: {
42+
app_name: 'your-app-name',
43+
extract_from_pattern: 'src/**/*.js',
44+
locales_dir_path: '__mocks__/locales',
45+
default_language_filename: 'en-US',
46+
},
47+
},
48+
}),
49+
})
50+
const execute = promisify(exec)
51+
let fail = false
52+
let output
53+
try {
54+
output = await execute(stagedFilesNumberCommand())
55+
} catch (e) {
56+
expect(e).toBeInstanceOf(Error)
57+
fail = true
58+
}
59+
expect(output.stderr).toBe('')
60+
expect(fail).toBe(false)
61+
})
62+
})

lib/i18n/precommit.mjs

Lines changed: 2 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,3 @@
1-
import { readFileSync } from 'node:fs'
2-
import { promisify } from 'util'
3-
import { exec } from 'child_process'
1+
import { precommit } from './precommitFunction.mjs'
42

5-
import { isTranslationTaskNeeded } from './sync-utils.mjs'
6-
7-
const execute = promisify(exec)
8-
9-
// get i18n config variables from package.json
10-
const { config } = JSON.parse(readFileSync('package.json'))
11-
12-
const excludeString = config.i18n.path_to_ignore ? `:^${config.i18n.path_to_ignore}` : ''
13-
const { stdout: stagedFilesNumber } = await execute(
14-
`git --no-pager diff --staged ${excludeString} --name-only "${config.i18n.extract_from_pattern}" | wc -l`
15-
)
16-
17-
if (parseInt(stagedFilesNumber) === 0) {
18-
// exit early with no error because there is nothing to translate
19-
process.exit(0)
20-
}
21-
22-
// function that returns the list of staged files to extract translations from
23-
const getExtractFrom = async () => {
24-
const exclusionPaths = [config.i18n.path_to_ignore, '**/*.d.ts'].filter(Boolean)
25-
const exclusionString = exclusionPaths.map(path => `":(exclude)${path}"`).join(' ')
26-
const { stdout: extractFromFileList } = await execute(`
27-
git --no-pager diff --staged --name-only "${config.i18n.extract_from_pattern}" ${exclusionString}
28-
`)
29-
return extractFromFileList.replace(/\n/g, ' ').trim()
30-
}
31-
32-
// extract translations from staged files and create
33-
// object of new translations in a temp.json file
34-
const extractFromFileList = await getExtractFrom()
35-
const extractCommand = `yarn run formatjs extract ${extractFromFileList} --out-file ${config.i18n.locales_dir_path}/temp.json --flatten --extract-source-location`
36-
const { stderr: extractError } = await execute(extractCommand)
37-
// eslint-disable-next-line no-console
38-
extractError && console.error(extractError)
39-
40-
if (isTranslationTaskNeeded()) {
41-
// eslint-disable-next-line no-console
42-
console.error('=============================================')
43-
// eslint-disable-next-line no-console
44-
console.error(
45-
'⚠️ This commit is going to be aborted because new translations have been found. Please add synced locales and check that everything is OK before commiting again'
46-
)
47-
// eslint-disable-next-line no-console
48-
console.error('=============================================')
49-
// translation script needs to be launched because temp file translations are new
50-
// eslint-disable-next-line no-console
51-
console.log('🛠 Translating')
52-
// eslint-disable-next-line no-console
53-
console.log('=============================================')
54-
const { stdout: translationOutput } = await execute('yarn i18n:translate')
55-
// eslint-disable-next-line no-console
56-
console.log(translationOutput)
57-
58-
// we exit with error so that commit hook aborts because new translations need to be checked
59-
// and added to the commit
60-
process.exit(1)
61-
} else {
62-
// no need to translate anything either because temp file object is empty
63-
// or because the translations it contains already exist in source locale
64-
await execute(`rm -f ${config.i18n.locales_dir_path}/temp.json`)
65-
66-
process.exit(0)
67-
}
3+
precommit()

lib/i18n/precommitFunction.mjs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { readFileSync } from 'node:fs'
2+
import { promisify } from 'util'
3+
import { exec } from 'child_process'
4+
5+
import { isTranslationTaskNeeded } from './sync-utils.mjs'
6+
import { stagedFilesNumberCommand } from './commands.mjs'
7+
8+
export const translationTaskNeededMessage =
9+
'⚠️ This commit is going to be aborted because new translations have been found. Please add synced locales and check that everything is OK before commiting again'
10+
11+
export const precommit = async () => {
12+
const execute = promisify(exec)
13+
14+
// get i18n config variables from package.json
15+
const { config } = JSON.parse(readFileSync('package.json'))
16+
const { stdout: stagedFilesNumber } = await execute(stagedFilesNumberCommand())
17+
if (parseInt(stagedFilesNumber) === 0) {
18+
// exit early with no error because there is nothing to translate
19+
process.exit(0)
20+
}
21+
22+
// function that returns the list of staged files to extract translations from
23+
const getExtractFrom = async () => {
24+
const exclusionPaths = [config.i18n.path_to_ignore, '**/*.d.ts'].filter(Boolean)
25+
const exclusionString = exclusionPaths.map(path => `":(exclude)${path}"`).join(' ')
26+
const { stdout: extractFromFileList } = await execute(`
27+
git --no-pager diff --staged --name-only "${config.i18n.extract_from_pattern}" ${exclusionString}
28+
`)
29+
return extractFromFileList?.replace(/\n/g, ' ').trim()
30+
}
31+
32+
// extract translations from staged files and create
33+
// object of new translations in a temp.json file
34+
const extractFromFileList = await getExtractFrom()
35+
const extractCommand = `yarn run formatjs extract ${extractFromFileList} --out-file ${config.i18n.locales_dir_path}/temp.json --flatten --extract-source-location`
36+
const { stderr: extractError } = await execute(extractCommand)
37+
// eslint-disable-next-line no-console
38+
if (extractError) console.error(extractError)
39+
40+
if (isTranslationTaskNeeded()) {
41+
// eslint-disable-next-line no-console
42+
console.log('=============================================')
43+
// eslint-disable-next-line no-console
44+
console.error(translationTaskNeededMessage)
45+
// eslint-disable-next-line no-console
46+
console.log('=============================================')
47+
// translation script needs to be launched because temp file translations are new
48+
// eslint-disable-next-line no-console
49+
console.log('🛠 Translating')
50+
// eslint-disable-next-line no-console
51+
console.log('=============================================')
52+
const { stdout: translationOutput } = await execute('yarn i18n:translate')
53+
// eslint-disable-next-line no-console
54+
console.log(translationOutput)
55+
56+
// we exit with error so that commit hook aborts because new translations need to be checked
57+
// and added to the commit
58+
process.exit(1)
59+
} else {
60+
// no need to translate anything either because temp file object is empty
61+
// or because the translations it contains already exist in source locale
62+
await execute(`rm -f ${config.i18n.locales_dir_path}/temp.json`)
63+
64+
process.exit(0)
65+
}
66+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { vol } from 'memfs'
3+
4+
import { precommit, translationTaskNeededMessage } from './precommitFunction.mjs'
5+
6+
const mockExit = vi.spyOn(process, 'exit').mockImplementation(code => {
7+
throw new Error(`Process exited with code: ${code}`)
8+
})
9+
10+
const consoleMock = vi.spyOn(console, 'error').mockImplementation(() => undefined)
11+
12+
const mocks = vi.hoisted(() => {
13+
return {
14+
exec: vi.fn(),
15+
}
16+
})
17+
18+
vi.mock('child_process', () => {
19+
return {
20+
exec: mocks.exec,
21+
}
22+
})
23+
24+
beforeEach(() => {
25+
consoleMock.mockClear()
26+
mockExit.mockClear()
27+
})
28+
29+
describe('precommit function execution tests', () => {
30+
it('should exit with error code 0 when no staged files inside extract_from_pattern path', async () => {
31+
// simulate 0 staged files
32+
mocks.exec.mockImplementation((cmd, cb) => {
33+
if (cmd.includes('wc -l')) {
34+
return cb(null, { stdout: '0' })
35+
}
36+
return cb(null, { stdout: '' })
37+
})
38+
39+
try {
40+
await precommit()
41+
} catch (e) {
42+
expect(e.message).toBe('Process exited with code: 0')
43+
}
44+
mocks.exec.mockRestore()
45+
})
46+
47+
it('should exit with error code 1 when there are files to translate with new translations', async () => {
48+
// simulate staged files
49+
mocks.exec.mockImplementation((cmd, cb) => {
50+
if (cmd.includes('wc -l')) {
51+
return cb(null, { stdout: '1' })
52+
}
53+
return cb(null, { stdout: '' })
54+
})
55+
// simulate new translations
56+
vol.fromJSON({
57+
'__mocks__/locales/temp.json': JSON.stringify({ c: 'baz' }),
58+
'__mocks__/locales/en-US.json': JSON.stringify({ a: 'foo', b: 'bar' }),
59+
})
60+
try {
61+
await precommit()
62+
} catch (e) {
63+
expect(consoleMock).toHaveBeenCalledWith(translationTaskNeededMessage)
64+
expect(e.message).toBe('Process exited with code: 1')
65+
}
66+
mocks.exec.mockRestore()
67+
})
68+
69+
it('should exit with error code 0 when there are files to translate', async () => {
70+
// simulate staged files
71+
mocks.exec.mockImplementation((cmd, cb) => {
72+
if (cmd.includes('wc -l')) {
73+
return cb(null, { stdout: '1' })
74+
}
75+
return cb(null, { stdout: '' })
76+
})
77+
// simulate no new translations
78+
vol.fromJSON({
79+
'__mocks__/locales/temp.json': JSON.stringify({ a: 'foo', b: 'bar' }),
80+
'__mocks__/locales/en-US.json': JSON.stringify({ a: 'foo', b: 'bar' }),
81+
})
82+
try {
83+
await precommit()
84+
} catch (e) {
85+
expect(e.message).toBe('Process exited with code: 0')
86+
}
87+
mocks.exec.mockRestore()
88+
})
89+
})

lib/i18n/sync-utils.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const getNewToStaleIdMap = async () => {
4949
`git diff ${excludeString} '${config.i18n.extract_from_pattern}'`
5050
)
5151

52-
const diff = unstagedDiff.concat(stagedDiff)
52+
const diff = unstagedDiff?.concat(stagedDiff)
5353
if (diff) {
5454
const changesRegex = /(?<deleted>(?:^-[^-].+\n)+)(?<added>(?:^\+[^+].+\n)+)+/gm
5555
const changes = diff.matchAll(changesRegex)

0 commit comments

Comments
 (0)