@iamandersonp/prettier-staged 0.2.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/.editorconfig +13 -0
- package/.env.example +14 -0
- package/.git-hooks/commit-msg +25 -0
- package/.git-hooks/pre-commit +19 -0
- package/.git-hooks/pre-commit-sample +15 -0
- package/.git-hooks/prepare-commit-msg +42 -0
- package/.prettierrc +6 -0
- package/.versionrc.json +14 -0
- package/.vscode/extensions.json +17 -0
- package/.vscode/settings.json +6 -0
- package/CHANGELOG.md +49 -0
- package/LICENSE +674 -0
- package/README.md +176 -0
- package/commitlint.config.js +3 -0
- package/jest.config.js +38 -0
- package/package.json +44 -0
- package/src/index.js +44 -0
- package/src/install-hooks.js +165 -0
- package/tests/index.test.js +233 -0
- package/tests/install-hooks.test.js +295 -0
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# @iamandersonp/prettier-staged
|
|
2
|
+
|
|
3
|
+
An utitlty to auto format stagged files using prettier
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
To use as a dev dependency
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -D @iamandersonp/prettier-staged
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
To use as a global
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i -g @iamandersonp/prettier-staged
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
Create a command on your package.json
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
"prettier-staged": "prettier-staged",
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Automatic Hook Installation
|
|
28
|
+
|
|
29
|
+
When installed as a dependency (not during local development), prettier-staged automatically copies a pre-commit hook to your project's hooks directory. This provides a ready-to-use solution for formatting staged files.
|
|
30
|
+
|
|
31
|
+
### Configuration
|
|
32
|
+
|
|
33
|
+
You can configure the hooks directory using an optional `.env` file in your project root:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# .env (optional)
|
|
37
|
+
HOOKS_DIR=.git-hooks # Default value
|
|
38
|
+
# or customize:
|
|
39
|
+
HOOKS_DIR=custom-hooks
|
|
40
|
+
HOOKS_DIR="my hooks" # With spaces (quotes optional)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If no `.env` file exists, the default directory `.git-hooks` will be used.
|
|
44
|
+
|
|
45
|
+
#### Setting up your .env file
|
|
46
|
+
|
|
47
|
+
1. Copy the example configuration:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cp .env.example .env
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
2. Edit `.env` to customize your hooks directory (optional):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
HOOKS_DIR=.git-hooks # Use default
|
|
57
|
+
# or
|
|
58
|
+
HOOKS_DIR=git-hooks # Custom directory
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### What gets installed
|
|
62
|
+
|
|
63
|
+
- A `{HOOKS_DIR}/pre-commit` file that:
|
|
64
|
+
- Skips formatting during merge conflicts
|
|
65
|
+
- Formats staged files with Prettier
|
|
66
|
+
- Re-stages formatted files automatically
|
|
67
|
+
|
|
68
|
+
### How to use the copied hook
|
|
69
|
+
|
|
70
|
+
After installation, run this command in your project root to use the copied hook:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Using default .git-hooks directory
|
|
74
|
+
git config core.hooksPath .git-hooks
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Or if you customized HOOKS_DIR in .env:
|
|
79
|
+
git config core.hooksPath your-custom-directory
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Pro tip**: The installation script automatically configures Git for you, so this step is usually not needed!
|
|
83
|
+
|
|
84
|
+
### Hook behavior
|
|
85
|
+
|
|
86
|
+
- ✅ **Safe**: Only copies if `{HOOKS_DIR}/pre-commit` doesn't exist (won't overwrite)
|
|
87
|
+
- ✅ **Smart**: Only installs when added as a dependency, not during development
|
|
88
|
+
- ✅ **Executable**: Automatically sets proper permissions (`chmod +x`)
|
|
89
|
+
- ✅ **Configurable**: Hooks directory can be customized via `.env` file
|
|
90
|
+
- ✅ **Auto-configured**: Git hooks path is set automatically during installation
|
|
91
|
+
|
|
92
|
+
### Supported formats in .env
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# All these formats are valid:
|
|
96
|
+
HOOKS_DIR=.git-hooks # Basic format
|
|
97
|
+
HOOKS_DIR=".git-hooks" # With double quotes
|
|
98
|
+
HOOKS_DIR='.git-hooks' # With single quotes
|
|
99
|
+
HOOKS_DIR=custom-hooks-dir # Custom directory name
|
|
100
|
+
HOOKS_DIR="hooks with spaces" # Directories with spaces
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Note**: If the `.env` file doesn't exist or has errors, the default `.git-hooks` directory is used automatically.
|
|
104
|
+
|
|
105
|
+
### Custom implementation
|
|
106
|
+
|
|
107
|
+
If you prefer a custom implementation, you can create your own pre-commit hook using this example (adjust the hooks directory as needed):
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
#!/bin/sh
|
|
111
|
+
# Replace .git-hooks with your HOOKS_DIR if customized
|
|
112
|
+
|
|
113
|
+
if git ls-files -u | grep -q .; then
|
|
114
|
+
echo "⚠️ Merge in progress. Skipping Prettier to avoid issues."
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
npm run prettier-staged
|
|
119
|
+
|
|
120
|
+
STAGED_FILES=$(git diff --name-only --cached --diff-filter=ACM | grep -E '\.(html|ts|scss|css|json)$')
|
|
121
|
+
|
|
122
|
+
if [ -n "$STAGED_FILES" ]; then
|
|
123
|
+
echo "$STAGED_FILES" | xargs git add
|
|
124
|
+
fi
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Quick start example
|
|
128
|
+
|
|
129
|
+
1. **Install the package**:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npm install -D @iamandersonp/prettier-staged
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
2. **Optional: Configure hooks directory**:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
echo "HOOKS_DIR=.git-hooks" > .env
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
3. **The hook is automatically set up!** No additional configuration needed.
|
|
142
|
+
|
|
143
|
+
4. **Optional: Verify the setup**:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
git config core.hooksPath # Should show your hooks directory
|
|
147
|
+
ls -la .git-hooks/ # Should show the pre-commit hook
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Testing
|
|
151
|
+
|
|
152
|
+
This project includes comprehensive unit tests with Jest. The tests cover all the main functionality and edge cases.
|
|
153
|
+
|
|
154
|
+
### Available test commands
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# Run all tests
|
|
158
|
+
npm test
|
|
159
|
+
|
|
160
|
+
# Run tests in watch mode (automatically re-run on file changes)
|
|
161
|
+
npm run test:watch
|
|
162
|
+
|
|
163
|
+
# Run tests with coverage report
|
|
164
|
+
npm run test:coverage
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Test coverage
|
|
168
|
+
|
|
169
|
+
Current test coverage: **>95%** including:
|
|
170
|
+
|
|
171
|
+
- ✅ Successful file formatting scenarios
|
|
172
|
+
- ✅ No files to format scenarios
|
|
173
|
+
- ✅ Error handling (Prettier not found, syntax errors, general errors)
|
|
174
|
+
- ✅ Edge cases (files with spaces, whitespace trimming, all supported extensions)
|
|
175
|
+
|
|
176
|
+
The tests use mocks to simulate Git commands and Prettier execution without running actual commands, making tests fast and reliable.
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
// Configuración Jest para Node.js CLI tool
|
|
3
|
+
testEnvironment: 'node',
|
|
4
|
+
|
|
5
|
+
// Patrones de archivos de test
|
|
6
|
+
testMatch: ['**/tests/**/*.test.js', '**/tests/**/*.spec.js', '**/__tests__/**/*.js'],
|
|
7
|
+
|
|
8
|
+
// Configuración de coverage
|
|
9
|
+
collectCoverageFrom: ['src/**/*.js', '!**/node_modules/**', '!**/tests/**'],
|
|
10
|
+
|
|
11
|
+
// Threshold mínimo de coverage
|
|
12
|
+
coverageThreshold: {
|
|
13
|
+
global: {
|
|
14
|
+
branches: 85,
|
|
15
|
+
functions: 85,
|
|
16
|
+
lines: 85,
|
|
17
|
+
statements: 85
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// Formato de reportes de coverage
|
|
22
|
+
coverageReporters: ['text', 'html', 'lcov'],
|
|
23
|
+
|
|
24
|
+
// Directorio para reportes de coverage
|
|
25
|
+
coverageDirectory: 'coverage',
|
|
26
|
+
|
|
27
|
+
// Limpiar mocks automáticamente después de cada test
|
|
28
|
+
clearMocks: true,
|
|
29
|
+
|
|
30
|
+
// Restaurar mocks automáticamente después de cada test
|
|
31
|
+
restoreMocks: true,
|
|
32
|
+
|
|
33
|
+
// Configuración adicional para mejor output
|
|
34
|
+
verbose: true,
|
|
35
|
+
|
|
36
|
+
// Configuración de timeout para tests
|
|
37
|
+
testTimeout: 10000
|
|
38
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@iamandersonp/prettier-staged",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"main": "src/index.js",
|
|
5
|
+
"bin": {
|
|
6
|
+
"prettier-staged": "src/index.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "jest",
|
|
10
|
+
"test:watch": "jest --watch",
|
|
11
|
+
"test:coverage": "jest --coverage",
|
|
12
|
+
"prettier-staged": "src/index.js",
|
|
13
|
+
"release": "standard-version",
|
|
14
|
+
"setup:git-hooks": "node src/install-hooks.js",
|
|
15
|
+
"release:minor": "standard-version --release-as minor",
|
|
16
|
+
"release:patch": "standard-version --release-as patch",
|
|
17
|
+
"release:major": "standard-version --release-as major",
|
|
18
|
+
"postinstall": "npm run setup:git-hooks"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"prettier",
|
|
22
|
+
"staged",
|
|
23
|
+
"git",
|
|
24
|
+
"hooks"
|
|
25
|
+
],
|
|
26
|
+
"author": "Anderson Penaloza <info@iamanderson.dev>",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/iamandersonp/prettier-staged.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/iamandersonp/prettier-staged.git",
|
|
32
|
+
"license": "GPL-3.0-only",
|
|
33
|
+
"description": "An utility to auto format staged files using Prettier",
|
|
34
|
+
"readme": "README.md",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"prettier": "^3.8.1"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@commitlint/cli": "^20.5.0",
|
|
40
|
+
"@commitlint/config-conventional": "^20.5.0",
|
|
41
|
+
"jest": "^30.3.0",
|
|
42
|
+
"standard-version": "^9.5.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
const extensions = /\.(html|ts|scss|css|json)$/;
|
|
6
|
+
|
|
7
|
+
function runPrettierStaged() {
|
|
8
|
+
try {
|
|
9
|
+
const output = execSync('git diff --name-only --cached --diff-filter=ACM', {
|
|
10
|
+
encoding: 'utf-8'
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const files = output
|
|
14
|
+
.split('\n')
|
|
15
|
+
.map((file) => file.trim())
|
|
16
|
+
.filter((file) => extensions.test(file));
|
|
17
|
+
|
|
18
|
+
if (files.length > 0) {
|
|
19
|
+
console.log('🧼 Formatting staged files with Prettier:');
|
|
20
|
+
execSync('npx prettier --write ' + files.join(' '), { stdio: 'inherit' });
|
|
21
|
+
|
|
22
|
+
// Re-staging
|
|
23
|
+
execSync(`git add ${files.join(' ')}`);
|
|
24
|
+
} else {
|
|
25
|
+
console.log('✅ No staged files matching for formatting.');
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.status === 127) {
|
|
29
|
+
console.error("❌ Error: Prettier not found. Make sure it's installed:", error.message);
|
|
30
|
+
} else if (error.status === 2) {
|
|
31
|
+
console.error('❌ Error: Prettier found syntax errors:', error.message);
|
|
32
|
+
} else {
|
|
33
|
+
console.error('❌ Error running Prettier:', error.message);
|
|
34
|
+
}
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Only run if this file is executed directly (not required as a module)
|
|
40
|
+
if (require.main === module) {
|
|
41
|
+
runPrettierStaged();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { runPrettierStaged };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lee el archivo .env para obtener HOOKS_DIR, retorna el valor por defecto si no existe
|
|
8
|
+
*/
|
|
9
|
+
function getHooksDirFromEnv() {
|
|
10
|
+
const defaultHooksDir = '.git-hooks';
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(envPath)) {
|
|
16
|
+
return defaultHooksDir;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
20
|
+
const lines = envContent.split('\n');
|
|
21
|
+
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
const trimmedLine = line.trim();
|
|
24
|
+
if (trimmedLine.startsWith('HOOKS_DIR=')) {
|
|
25
|
+
const value = trimmedLine.substring('HOOKS_DIR='.length).trim();
|
|
26
|
+
// Remover comillas si existen (simples o dobles)
|
|
27
|
+
let cleanValue = value
|
|
28
|
+
.replace(/^['"]/, '') // Remover comilla de inicio
|
|
29
|
+
.replace(/['"]$/, ''); // Remover comilla de final
|
|
30
|
+
return cleanValue || defaultHooksDir;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return defaultHooksDir;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
// Si hay cualquier error leyendo el archivo, usar valor por defecto
|
|
37
|
+
console.warn('Warning: Could not read .env file, using default HOOKS_DIR:', error.message);
|
|
38
|
+
return defaultHooksDir;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Constante para el directorio de hooks (lee desde .env o usa valor por defecto)
|
|
43
|
+
const HOOKS_DIR = getHooksDirFromEnv();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Detects if the package is being installed as a dependency in another project
|
|
47
|
+
* vs being installed during local development
|
|
48
|
+
*/
|
|
49
|
+
function isExternalInstallation() {
|
|
50
|
+
const initCwd = process.env.INIT_CWD;
|
|
51
|
+
const currentCwd = process.cwd();
|
|
52
|
+
|
|
53
|
+
// If INIT_CWD is different from current working directory,
|
|
54
|
+
// we're being installed as a dependency
|
|
55
|
+
return !!(initCwd && initCwd !== currentCwd);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Gets the target project directory (where the package is being installed)
|
|
60
|
+
*/
|
|
61
|
+
function getTargetProjectDir() {
|
|
62
|
+
// INIT_CWD contains the directory where npm install was run
|
|
63
|
+
return process.env.INIT_CWD || process.cwd();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Copies the pre-commit hook to the target project's .git-hooks directory
|
|
68
|
+
* if it doesn't already exist
|
|
69
|
+
*/
|
|
70
|
+
function copyPreCommitHook() {
|
|
71
|
+
try {
|
|
72
|
+
const targetProjectDir = getTargetProjectDir();
|
|
73
|
+
const targetHooksDir = path.join(targetProjectDir, HOOKS_DIR);
|
|
74
|
+
const targetPreCommitPath = path.join(targetHooksDir, 'pre-commit');
|
|
75
|
+
|
|
76
|
+
// Check if pre-commit already exists
|
|
77
|
+
if (fs.existsSync(targetPreCommitPath)) {
|
|
78
|
+
console.log(`✅ ${HOOKS_DIR}/pre-commit already exists, skipping copy`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Get source pre-commit path
|
|
83
|
+
const sourcePreCommitPath = path.join(__dirname, '..', '.git-hooks', 'pre-commit-sample');
|
|
84
|
+
|
|
85
|
+
if (!fs.existsSync(sourcePreCommitPath)) {
|
|
86
|
+
console.warn('⚠️ Source pre-commit hook not found, skipping copy');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Ensure target directory exists
|
|
91
|
+
if (!fs.existsSync(targetHooksDir)) {
|
|
92
|
+
fs.mkdirSync(targetHooksDir, { recursive: true });
|
|
93
|
+
console.log(`📁 Created ${HOOKS_DIR} directory`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Copy the file
|
|
97
|
+
fs.copyFileSync(sourcePreCommitPath, targetPreCommitPath);
|
|
98
|
+
|
|
99
|
+
// Make it executable
|
|
100
|
+
fs.chmodSync(targetPreCommitPath, 0o755);
|
|
101
|
+
|
|
102
|
+
console.log(`✅ Copied pre-commit hook to ${HOOKS_DIR}/pre-commit`);
|
|
103
|
+
console.log(`💡 To use it, run: git config core.hooksPath ${HOOKS_DIR}`);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// Don't fail installation if hook copy fails
|
|
106
|
+
console.warn('⚠️ Failed to copy pre-commit hook:', error.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Sets up git hooks in the library's own .git-hooks directory
|
|
112
|
+
* (existing functionality)
|
|
113
|
+
*/
|
|
114
|
+
function setupLibraryGitHooks() {
|
|
115
|
+
const { execSync } = require('node:child_process');
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// Configure git to use .git-hooks directory
|
|
119
|
+
execSync(`git config core.hooksPath ${HOOKS_DIR}`, { stdio: 'ignore' });
|
|
120
|
+
console.log(`✅ Configured git to use ${HOOKS_DIR} directory`);
|
|
121
|
+
|
|
122
|
+
// Make hooks executable
|
|
123
|
+
execSync(`chmod +x ./${HOOKS_DIR}/*`, { stdio: 'ignore' });
|
|
124
|
+
console.log('✅ Made git hooks executable');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Silently fail if not in a git repository or permissions issue
|
|
127
|
+
console.warn('⚠️ Could not setup library git hooks:', error.message);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Main installation function
|
|
133
|
+
*/
|
|
134
|
+
function installHooks() {
|
|
135
|
+
console.log('🔧 Setting up prettier-staged hooks...');
|
|
136
|
+
|
|
137
|
+
// Always setup hooks for the library itself
|
|
138
|
+
setupLibraryGitHooks();
|
|
139
|
+
|
|
140
|
+
// Only copy to target project if this is an external installation
|
|
141
|
+
if (isExternalInstallation()) {
|
|
142
|
+
console.log('📦 Installing as dependency, copying pre-commit hook...');
|
|
143
|
+
copyPreCommitHook();
|
|
144
|
+
} else {
|
|
145
|
+
console.log('🏠 Running in development mode, skipping hook copy');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log('✨ Setup complete!');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Export for testing
|
|
152
|
+
module.exports = {
|
|
153
|
+
isExternalInstallation,
|
|
154
|
+
getTargetProjectDir,
|
|
155
|
+
copyPreCommitHook,
|
|
156
|
+
setupLibraryGitHooks,
|
|
157
|
+
installHooks,
|
|
158
|
+
getHooksDirFromEnv,
|
|
159
|
+
HOOKS_DIR
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Run if called directly
|
|
163
|
+
if (require.main === module) {
|
|
164
|
+
installHooks();
|
|
165
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const { execSync } = require('node:child_process');
|
|
2
|
+
|
|
3
|
+
// Mock completo del módulo child_process
|
|
4
|
+
jest.mock('node:child_process', () => ({
|
|
5
|
+
execSync: jest.fn()
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
const { runPrettierStaged } = require('../src/index.js');
|
|
9
|
+
|
|
10
|
+
describe('prettier-staged CLI', () => {
|
|
11
|
+
let consoleSpy, errorSpy, exitSpy;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// Reset all mocks
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
|
|
17
|
+
// Setup console spies
|
|
18
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
19
|
+
errorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
20
|
+
exitSpy = jest.spyOn(process, 'exit').mockImplementation();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
// Restore all spies
|
|
25
|
+
consoleSpy.mockRestore();
|
|
26
|
+
errorSpy.mockRestore();
|
|
27
|
+
exitSpy.mockRestore();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('Successful formatting', () => {
|
|
31
|
+
test('should format staged files with valid extensions', () => {
|
|
32
|
+
// Mock git diff to return files with valid extensions
|
|
33
|
+
execSync
|
|
34
|
+
.mockReturnValueOnce('src/app.ts\nstyles/main.scss\nconfig.json\n')
|
|
35
|
+
.mockReturnValueOnce(undefined) // prettier command
|
|
36
|
+
.mockReturnValueOnce(undefined); // git add command
|
|
37
|
+
|
|
38
|
+
// Execute the function
|
|
39
|
+
runPrettierStaged();
|
|
40
|
+
|
|
41
|
+
// Verify git diff was called
|
|
42
|
+
expect(execSync).toHaveBeenNthCalledWith(
|
|
43
|
+
1,
|
|
44
|
+
'git diff --name-only --cached --diff-filter=ACM',
|
|
45
|
+
{ encoding: 'utf-8' }
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Verify prettier was called with correct files
|
|
49
|
+
expect(execSync).toHaveBeenNthCalledWith(
|
|
50
|
+
2,
|
|
51
|
+
'npx prettier --write src/app.ts styles/main.scss config.json',
|
|
52
|
+
{ stdio: 'inherit' }
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Verify git add was called with formatted files
|
|
56
|
+
expect(execSync).toHaveBeenNthCalledWith(
|
|
57
|
+
3,
|
|
58
|
+
'git add src/app.ts styles/main.scss config.json'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Verify success message
|
|
62
|
+
expect(consoleSpy).toHaveBeenCalledWith('🧼 Formatting staged files with Prettier:');
|
|
63
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('should handle single file with valid extension', () => {
|
|
67
|
+
execSync
|
|
68
|
+
.mockReturnValueOnce('index.html\n')
|
|
69
|
+
.mockReturnValueOnce(undefined)
|
|
70
|
+
.mockReturnValueOnce(undefined);
|
|
71
|
+
|
|
72
|
+
runPrettierStaged();
|
|
73
|
+
|
|
74
|
+
expect(execSync).toHaveBeenNthCalledWith(2, 'npx prettier --write index.html', {
|
|
75
|
+
stdio: 'inherit'
|
|
76
|
+
});
|
|
77
|
+
expect(consoleSpy).toHaveBeenCalledWith('🧼 Formatting staged files with Prettier:');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('No files to format', () => {
|
|
82
|
+
test('should show message when no staged files', () => {
|
|
83
|
+
execSync.mockReturnValueOnce('\n'); // Empty git diff output
|
|
84
|
+
|
|
85
|
+
runPrettierStaged();
|
|
86
|
+
|
|
87
|
+
expect(execSync).toHaveBeenCalledTimes(1);
|
|
88
|
+
expect(consoleSpy).toHaveBeenCalledWith('✅ No staged files matching for formatting.');
|
|
89
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('should show message when staged files have invalid extensions', () => {
|
|
93
|
+
execSync.mockReturnValueOnce('README.md\nscript.py\nimage.png\n');
|
|
94
|
+
|
|
95
|
+
runPrettierStaged();
|
|
96
|
+
|
|
97
|
+
expect(execSync).toHaveBeenCalledTimes(1);
|
|
98
|
+
expect(consoleSpy).toHaveBeenCalledWith('✅ No staged files matching for formatting.');
|
|
99
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('should filter mixed files and format only valid ones', () => {
|
|
103
|
+
execSync
|
|
104
|
+
.mockReturnValueOnce('README.md\nsrc/app.ts\nimage.png\nstyle.css\n')
|
|
105
|
+
.mockReturnValueOnce(undefined)
|
|
106
|
+
.mockReturnValueOnce(undefined);
|
|
107
|
+
|
|
108
|
+
runPrettierStaged();
|
|
109
|
+
|
|
110
|
+
expect(execSync).toHaveBeenNthCalledWith(2, 'npx prettier --write src/app.ts style.css', {
|
|
111
|
+
stdio: 'inherit'
|
|
112
|
+
});
|
|
113
|
+
expect(consoleSpy).toHaveBeenCalledWith('🧼 Formatting staged files with Prettier:');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('Error handling', () => {
|
|
118
|
+
test('should handle Prettier not found error (status 127)', () => {
|
|
119
|
+
execSync.mockReturnValueOnce('src/app.ts\n');
|
|
120
|
+
|
|
121
|
+
const error = new Error('prettier: command not found');
|
|
122
|
+
error.status = 127;
|
|
123
|
+
execSync.mockImplementationOnce(() => {
|
|
124
|
+
throw error;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
runPrettierStaged();
|
|
128
|
+
|
|
129
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
130
|
+
"❌ Error: Prettier not found. Make sure it's installed:",
|
|
131
|
+
error.message
|
|
132
|
+
);
|
|
133
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('should handle Prettier syntax errors (status 2)', () => {
|
|
137
|
+
execSync.mockReturnValueOnce('src/broken.ts\n');
|
|
138
|
+
|
|
139
|
+
const error = new Error('Syntax error in file');
|
|
140
|
+
error.status = 2;
|
|
141
|
+
execSync.mockImplementationOnce(() => {
|
|
142
|
+
throw error;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
runPrettierStaged();
|
|
146
|
+
|
|
147
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
148
|
+
'❌ Error: Prettier found syntax errors:',
|
|
149
|
+
error.message
|
|
150
|
+
);
|
|
151
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('should handle general Prettier errors', () => {
|
|
155
|
+
execSync.mockReturnValueOnce('src/app.ts\n');
|
|
156
|
+
|
|
157
|
+
const error = new Error('General prettier error');
|
|
158
|
+
error.status = 1;
|
|
159
|
+
execSync.mockImplementationOnce(() => {
|
|
160
|
+
throw error;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
runPrettierStaged();
|
|
164
|
+
|
|
165
|
+
expect(errorSpy).toHaveBeenCalledWith('❌ Error running Prettier:', error.message);
|
|
166
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('should handle git command errors', () => {
|
|
170
|
+
const error = new Error('Git command failed');
|
|
171
|
+
error.status = 128;
|
|
172
|
+
execSync.mockImplementationOnce(() => {
|
|
173
|
+
throw error;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
runPrettierStaged();
|
|
177
|
+
|
|
178
|
+
expect(errorSpy).toHaveBeenCalledWith('❌ Error running Prettier:', error.message);
|
|
179
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('Edge cases', () => {
|
|
184
|
+
test('should handle files with spaces in names', () => {
|
|
185
|
+
execSync
|
|
186
|
+
.mockReturnValueOnce('src/my file.ts\nother file.css\n')
|
|
187
|
+
.mockReturnValueOnce(undefined)
|
|
188
|
+
.mockReturnValueOnce(undefined);
|
|
189
|
+
|
|
190
|
+
runPrettierStaged();
|
|
191
|
+
|
|
192
|
+
expect(execSync).toHaveBeenNthCalledWith(
|
|
193
|
+
2,
|
|
194
|
+
'npx prettier --write src/my file.ts other file.css',
|
|
195
|
+
{ stdio: 'inherit' }
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('should trim whitespace from file names', () => {
|
|
200
|
+
execSync
|
|
201
|
+
.mockReturnValueOnce(' src/app.ts \n style.css \n\n')
|
|
202
|
+
.mockReturnValueOnce(undefined)
|
|
203
|
+
.mockReturnValueOnce(undefined);
|
|
204
|
+
|
|
205
|
+
runPrettierStaged();
|
|
206
|
+
|
|
207
|
+
expect(execSync).toHaveBeenNthCalledWith(2, 'npx prettier --write src/app.ts style.css', {
|
|
208
|
+
stdio: 'inherit'
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('should handle all supported file extensions', () => {
|
|
213
|
+
const validFiles = [
|
|
214
|
+
'template.html',
|
|
215
|
+
'component.ts',
|
|
216
|
+
'styles.scss',
|
|
217
|
+
'reset.css',
|
|
218
|
+
'package.json'
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
execSync
|
|
222
|
+
.mockReturnValueOnce(validFiles.join('\n') + '\n')
|
|
223
|
+
.mockReturnValueOnce(undefined)
|
|
224
|
+
.mockReturnValueOnce(undefined);
|
|
225
|
+
|
|
226
|
+
runPrettierStaged();
|
|
227
|
+
|
|
228
|
+
expect(execSync).toHaveBeenNthCalledWith(2, `npx prettier --write ${validFiles.join(' ')}`, {
|
|
229
|
+
stdio: 'inherit'
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|