Skip to content

Commit 7b47038

Browse files
jrolfsgithub-actions[bot]
authored andcommitted
feat(scripts/pre-commit): add support for custom test command
1 parent 49196ec commit 7b47038

File tree

6 files changed

+260
-20
lines changed

6 files changed

+260
-20
lines changed

README.md

+29
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
- [Prettier](#prettier)
4040
- [Jest](#jest)
4141
- [Semantic Release](#semantic-release)
42+
- [Lint Staged](#lint-staged)
4243
- [License](#license)
4344
- [Maintenance](#maintenance)
4445

@@ -150,6 +151,34 @@ module.exports = {
150151
}
151152
```
152153

154+
#### Lint Staged
155+
156+
Or, for lint-staged (used in `pre-commit` script) in `lint-staged.config.js`:
157+
158+
```js
159+
module.exports = {
160+
...require.resolve('@hover/javascript/lint-staged'),
161+
'*.+(js|jsx|ts|tsx)': ['yarn some-custom-command'],
162+
}
163+
```
164+
165+
##### Custom Test Command
166+
167+
If all you want to do is run a custom test command, you can pass `--testCommand`
168+
to `hover-scripts pre-commit`. The built-in lint-staged configuration will be
169+
used with your custom command.
170+
171+
```json
172+
{
173+
"name": "my-package",
174+
"husky": {
175+
"hooks": {
176+
"pre-commit": "hover-scripts pre-commit --testCommand 'yarn test:custom' --findRelatedTests"
177+
}
178+
}
179+
}
180+
```
181+
153182
## License
154183

155184
MIT
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const {resolveHoverScripts, resolveBin} = require('../../utils')
2+
3+
const hoverScripts = resolveHoverScripts()
4+
const doctoc = resolveBin('doctoc')
5+
const defaultTestCommand = `${hoverScripts} test --findRelatedTests`
6+
7+
const buildConfig = (testCommand = defaultTestCommand) => ({
8+
'README.md': [`${doctoc} --maxlevel 4 --notitle`],
9+
'*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|graphql|mdx|vue)': [
10+
`${hoverScripts} format`,
11+
],
12+
'*.+(js|jsx|ts|tsx)': [`${hoverScripts} lint`, testCommand],
13+
})
14+
15+
module.exports = {buildConfig}

src/config/lintstagedrc.js

+2-14
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
1-
const {resolveHoverScripts, resolveBin} = require('../utils')
1+
const {buildConfig} = require('./helpers/build-lint-staged')
22

3-
const hoverScripts = resolveHoverScripts()
4-
const doctoc = resolveBin('doctoc')
5-
6-
module.exports = {
7-
'README.md': [`${doctoc} --maxlevel 4 --notitle`],
8-
'*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|graphql|mdx|vue)': [
9-
`${hoverScripts} format`,
10-
],
11-
'*.+(js|jsx|ts|tsx)': [
12-
`${hoverScripts} lint`,
13-
`${hoverScripts} test --findRelatedTests`,
14-
],
15-
}
3+
module.exports = buildConfig()

src/scripts/__tests__/__snapshots__/pre-commit.js.snap

+60
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,63 @@ Array [
4747
npm run validate,
4848
]
4949
`;
50+
51+
exports[`pre-commit overrides built-in test command with --test-command 1`] = `
52+
Array [
53+
lint-staged --config .test-tmp/hover-javascriptTMPSUFFIX/.lintstaged.json,
54+
npm run validate,
55+
]
56+
`;
57+
58+
exports[`pre-commit overrides built-in test command with --test-command 2`] = `
59+
Array [
60+
.test-tmp/hover-javascriptTMPSUFFIX/.lintstaged.json,
61+
{"README.md":["doctoc --maxlevel 4 --notitle"],"*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|graphql|mdx|vue)":["./src/index.js format"],"*.+(js|jsx|ts|tsx)":["./src/index.js lint","yarn test:custom --findRelatedTests foo.js"]},
62+
]
63+
`;
64+
65+
exports[`pre-commit overrides built-in test command with --test-command and forwards args 1`] = `
66+
Array [
67+
lint-staged --config .test-tmp/hover-javascriptTMPSUFFIX/.lintstaged.json --verbose,
68+
npm run validate,
69+
]
70+
`;
71+
72+
exports[`pre-commit overrides built-in test command with --test-command and forwards args 2`] = `
73+
Array [
74+
.test-tmp/hover-javascriptTMPSUFFIX/.lintstaged.json,
75+
{"README.md":["doctoc --maxlevel 4 --notitle"],"*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|graphql|mdx|vue)":["./src/index.js format"],"*.+(js|jsx|ts|tsx)":["./src/index.js lint","yarn test:custom --findRelatedTests foo.js"]},
76+
]
77+
`;
78+
79+
exports[`pre-commit overrides built-in test command with --testCommand 1`] = `
80+
Array [
81+
lint-staged --config .test-tmp/hover-javascriptTMPSUFFIX/.lintstaged.json,
82+
npm run validate,
83+
]
84+
`;
85+
86+
exports[`pre-commit overrides built-in test command with --testCommand 2`] = `
87+
Array [
88+
.test-tmp/hover-javascriptTMPSUFFIX/.lintstaged.json,
89+
{"README.md":["doctoc --maxlevel 4 --notitle"],"*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|graphql|mdx|vue)":["./src/index.js format"],"*.+(js|jsx|ts|tsx)":["./src/index.js lint","yarn test:custom --findRelatedTests foo.js"]},
90+
]
91+
`;
92+
93+
exports[`pre-commit overrides built-in test command with --testCommand and forwards args 1`] = `
94+
Array [
95+
lint-staged --config .test-tmp/hover-javascriptTMPSUFFIX/.lintstaged.json --verbose,
96+
npm run validate,
97+
]
98+
`;
99+
100+
exports[`pre-commit overrides built-in test command with --testCommand and forwards args 2`] = `
101+
Array [
102+
.test-tmp/hover-javascriptTMPSUFFIX/.lintstaged.json,
103+
{"README.md":["doctoc --maxlevel 4 --notitle"],"*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|graphql|mdx|vue)":["./src/index.js format"],"*.+(js|jsx|ts|tsx)":["./src/index.js lint","yarn test:custom --findRelatedTests foo.js"]},
104+
]
105+
`;
106+
107+
exports[`pre-commit throws an error when \`--config\` and \`--testCommand\` are used together 1`] = `[Error: @hover/javascript/pre-commit: --config and --testCommand cannot be used together (--testCommand only works with built-in lint-staged configuration)]`;
108+
109+
exports[`pre-commit throws an error when invalid \`--testCommand\` is provided 1`] = `[Error: @hover/javascript/pre-commit: invalid --testCommand]`;

src/scripts/__tests__/pre-commit.js

+84-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
1+
import path from 'path'
12
import cases from 'jest-in-case'
3+
import yargsParser from 'yargs-parser'
24
import {unquoteSerializer, winPathSerializer} from './helpers/serializers'
35

46
expect.addSnapshotSerializer(unquoteSerializer)
57
expect.addSnapshotSerializer(winPathSerializer)
68

9+
jest.mock('os', () => ({
10+
...jest.requireActual('os'),
11+
tmpdir: jest.fn(() => '.test-tmp'),
12+
}))
13+
14+
jest.mock('fs', () => ({
15+
...jest.requireActual('fs'),
16+
mkdtempSync: jest.fn(prefix => `${prefix}TMPSUFFIX`),
17+
rmdirSync: jest.fn(),
18+
writeFileSync: jest.fn(),
19+
}))
20+
721
cases(
822
'pre-commit',
923
({
@@ -12,9 +26,15 @@ cases(
1226
hasPkgProp = () => false,
1327
hasFile = () => false,
1428
ifScript = () => true,
29+
expectError = false,
1530
}) => {
1631
// beforeEach
1732
const {sync: crossSpawnSyncMock} = require('cross-spawn')
33+
const {
34+
writeFileSync: writeFileSyncMock,
35+
rmdirSync: rmdirSyncMock,
36+
} = require('fs')
37+
1838
const originalArgv = process.argv
1939
const originalExit = process.exit
2040
Object.assign(utils, {
@@ -34,12 +54,42 @@ cases(
3454
call => `${call[0]} ${call[1].join(' ')}`,
3555
)
3656
expect(commands).toMatchSnapshot()
57+
58+
// Specific tests for when a custom test command is supplied
59+
if (!!yargsParser(args).testCommand) {
60+
// ensure we don't pass `--testCommand` through to `lint-staged`
61+
expect(
62+
crossSpawnSyncMock.mock.calls.some(([_command, commandArgs]) =>
63+
commandArgs.some(
64+
a => a === '--testCommand' || a === '--test-command',
65+
),
66+
),
67+
).not.toBeTruthy()
68+
69+
const [writeFileSyncCall] = writeFileSyncMock.mock.calls
70+
71+
// Snapshot the config file we use with the custom test command
72+
expect(writeFileSyncCall).toMatchSnapshot()
73+
74+
// Make sure we clean up the temporary config file
75+
expect(rmdirSyncMock).toHaveBeenCalledWith(
76+
path.dirname(writeFileSyncCall[0]),
77+
{recursive: true},
78+
)
79+
}
3780
} catch (error) {
38-
throw error
81+
if (expectError) {
82+
expect(error).toMatchSnapshot()
83+
} else {
84+
throw error
85+
}
3986
} finally {
4087
// afterEach
4188
process.exit = originalExit
4289
process.argv = originalArgv
90+
91+
writeFileSyncMock.mockClear()
92+
4393
jest.resetModules()
4494
}
4595
},
@@ -63,5 +113,38 @@ cases(
63113
[`does not run validate script if it's not defined`]: {
64114
ifScript: () => false,
65115
},
116+
'throws an error when `--config` and `--testCommand` are used together': {
117+
expectError: true,
118+
args: [
119+
'--config',
120+
'some-config.js',
121+
'--testCommand',
122+
'"yarn test:unit --findRelatedTests"',
123+
],
124+
},
125+
'throws an error when invalid `--testCommand` is provided': {
126+
expectError: true,
127+
args: ['--testCommand', '--config', 'some-config.js'],
128+
},
129+
'overrides built-in test command with --testCommand': {
130+
args: ['--testCommand', '"yarn test:custom --findRelatedTests foo.js"'],
131+
},
132+
'overrides built-in test command with --test-command': {
133+
args: ['--test-command', '"yarn test:custom --findRelatedTests foo.js"'],
134+
},
135+
'overrides built-in test command with --testCommand and forwards args': {
136+
args: [
137+
'--verbose',
138+
'--testCommand',
139+
'"yarn test:custom --findRelatedTests foo.js"',
140+
],
141+
},
142+
'overrides built-in test command with --test-command and forwards args': {
143+
args: [
144+
'--verbose',
145+
'--test-command',
146+
'"yarn test:custom --findRelatedTests foo.js"',
147+
],
148+
},
66149
},
67150
)

src/scripts/pre-commit.js

+70-5
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,93 @@
11
const path = require('path')
2+
const fs = require('fs')
3+
const os = require('os')
24
const spawn = require('cross-spawn')
3-
const {hasPkgProp, hasFile, ifScript, resolveBin} = require('../utils')
5+
const yargsParser = require('yargs-parser')
6+
const {
7+
hasPkgProp,
8+
hasFile,
9+
ifScript,
10+
resolveBin,
11+
getPkgName,
12+
} = require('../utils')
13+
const {buildConfig} = require('../config/helpers/build-lint-staged')
414

515
const here = p => path.join(__dirname, p)
616
const hereRelative = p => here(p).replace(process.cwd(), '.')
717

818
const args = process.argv.slice(2)
19+
const {argv: parsedArgs, aliases} = yargsParser.detailed(args)
20+
21+
/**
22+
* Generate a temporary copy of the built-in lint-staged
23+
* configuration with a custom test command
24+
*
25+
* @param {string} command
26+
*
27+
* @returns {string} filename of generated config file
28+
*/
29+
const generateConfigWithTestCommand = command => {
30+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hover-javascript'))
31+
const tmpConfigFile = path.join(tmpDir, '.lintstaged.json')
32+
33+
fs.writeFileSync(tmpConfigFile, JSON.stringify(buildConfig(command)))
34+
35+
return tmpConfigFile
36+
}
37+
38+
if (parsedArgs.testCommand && typeof parsedArgs.testCommand !== 'string') {
39+
throw new Error(`${getPkgName()}/pre-commit: invalid --testCommand`)
40+
}
41+
42+
if (parsedArgs.config && parsedArgs.testCommand) {
43+
throw new Error(
44+
`${getPkgName()}/pre-commit: --config and --testCommand cannot be used together (--testCommand only works with built-in lint-staged configuration)`,
45+
)
46+
}
47+
48+
// Don't forward `--testCommand` or `--test-command`
49+
// flags through to `lint-staged` (yes, this is gross)
50+
const testCommandIndex = args.findIndex(
51+
a =>
52+
a === '--testCommand' ||
53+
(aliases.testCommand && aliases.testCommand.includes(a.replace(/^--/, ''))),
54+
)
55+
const argsToForward = [...args]
56+
if (testCommandIndex >= 0) {
57+
argsToForward.splice(testCommandIndex, 2)
58+
}
59+
60+
const useCustomBuiltInConfig = !!parsedArgs.testCommand
61+
const customBuiltInConfig = useCustomBuiltInConfig
62+
? generateConfigWithTestCommand(parsedArgs.testCommand)
63+
: null
964

1065
const useBuiltInConfig =
1166
!args.includes('--config') &&
1267
!hasFile('.lintstagedrc') &&
1368
!hasFile('lint-staged.config.js') &&
1469
!hasPkgProp('lint-staged')
1570

16-
const config = useBuiltInConfig
17-
? ['--config', hereRelative('../config/lintstagedrc.js')]
18-
: []
71+
const config =
72+
useBuiltInConfig || useCustomBuiltInConfig
73+
? [
74+
'--config',
75+
useCustomBuiltInConfig
76+
? customBuiltInConfig
77+
: hereRelative('../config/lintstagedrc.js'),
78+
]
79+
: []
1980

2081
const lintStagedResult = spawn.sync(
2182
resolveBin('lint-staged'),
22-
[...config, ...args],
83+
[...config, ...argsToForward],
2384
{stdio: 'inherit'},
2485
)
2586

87+
if (useCustomBuiltInConfig) {
88+
fs.rmdirSync(path.dirname(customBuiltInConfig), {recursive: true})
89+
}
90+
2691
if (lintStagedResult.status === 0 && ifScript('validate')) {
2792
const validateResult = spawn.sync('npm', ['run', 'validate'], {
2893
stdio: 'inherit',

0 commit comments

Comments
 (0)