lfify 1.1.0 → 1.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/.github/workflows/cd.yml +6 -7
- package/.lfifyrc-sample.json +1 -0
- package/.releaserc.json +21 -0
- package/CHANGELOG.md +22 -0
- package/README.md +49 -13
- package/__fixtures__/already-lf/src/file.js +1 -0
- package/__fixtures__/cli-override/.lfifyrc.json +6 -0
- package/__fixtures__/cli-override/src/a.js +1 -0
- package/__fixtures__/cli-override/src/b.txt +1 -0
- package/__fixtures__/default-sensible/_git/config +2 -0
- package/__fixtures__/default-sensible/_node_modules/pkg/index.js +1 -0
- package/__fixtures__/default-sensible/src/app.js +1 -0
- package/__fixtures__/default-sensible/src/readme.txt +2 -0
- package/__fixtures__/with-config/.lfifyrc.json +6 -0
- package/__fixtures__/with-config/doc.txt +1 -0
- package/__fixtures__/with-config/lib/main.js +1 -0
- package/__fixtures__/with-config/skip/other.js +1 -0
- package/eslint.config.mjs +3 -2
- package/index.cjs +235 -35
- package/index.e2e.test.js +129 -0
- package/index.test.js +242 -43
- package/package.json +16 -5
- package/__mocks__/fs.js +0 -54
- package/__mocks__/micromatch.js +0 -23
package/.github/workflows/cd.yml
CHANGED
|
@@ -4,13 +4,11 @@ on:
|
|
|
4
4
|
branches:
|
|
5
5
|
- main
|
|
6
6
|
|
|
7
|
-
permissions:
|
|
8
|
-
contents: read # for checkout
|
|
9
|
-
|
|
10
7
|
jobs:
|
|
11
8
|
release:
|
|
12
9
|
name: Release
|
|
13
10
|
runs-on: ubuntu-latest
|
|
11
|
+
environment: npm
|
|
14
12
|
permissions:
|
|
15
13
|
contents: write # to be able to publish a GitHub release
|
|
16
14
|
issues: write # to be able to comment on released issues
|
|
@@ -22,9 +20,9 @@ jobs:
|
|
|
22
20
|
with:
|
|
23
21
|
fetch-depth: 0
|
|
24
22
|
- name: Setup Node.js
|
|
25
|
-
uses: actions/setup-node@
|
|
23
|
+
uses: actions/setup-node@v6
|
|
26
24
|
with:
|
|
27
|
-
node-version: "
|
|
25
|
+
node-version: "24"
|
|
28
26
|
- name: Install dependencies
|
|
29
27
|
run: npm clean-install
|
|
30
28
|
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
|
|
@@ -32,5 +30,6 @@ jobs:
|
|
|
32
30
|
- name: Release
|
|
33
31
|
env:
|
|
34
32
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
run: |
|
|
34
|
+
unset NODE_AUTH_TOKEN
|
|
35
|
+
npx semantic-release
|
package/.lfifyrc-sample.json
CHANGED
package/.releaserc.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"branches": ["main"],
|
|
3
|
+
"plugins": [
|
|
4
|
+
"@semantic-release/commit-analyzer",
|
|
5
|
+
"@semantic-release/release-notes-generator",
|
|
6
|
+
[
|
|
7
|
+
"@semantic-release/changelog",
|
|
8
|
+
{
|
|
9
|
+
"changelogFile": "CHANGELOG.md"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"@semantic-release/npm",
|
|
13
|
+
[
|
|
14
|
+
"@semantic-release/git",
|
|
15
|
+
{
|
|
16
|
+
"assets": ["package.json", "package-lock.json", "CHANGELOG.md"],
|
|
17
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
]
|
|
21
|
+
}
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# [1.2.0](https://github.com/GyeongHoKim/lfify/compare/v1.1.0...v1.2.0) (2026-03-16)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* add provenance field to true ([fa1a7ff](https://github.com/GyeongHoKim/lfify/commit/fa1a7ffe9c101ff24ec72dd2a083a0facf414511))
|
|
7
|
+
* add trailing slash to publishConfig.registry for npm OIDC ([85175cd](https://github.com/GyeongHoKim/lfify/commit/85175cd160e4bce4d12af559a23e925baf6d7bd6))
|
|
8
|
+
* chunk process ([a77316f](https://github.com/GyeongHoKim/lfify/commit/a77316f792f26466f083c0f47092c79f135bd55a))
|
|
9
|
+
* declare tar resolutions ([e3d654d](https://github.com/GyeongHoKim/lfify/commit/e3d654df7dc67c4edade2043ee62e3e7d64e35a0)), closes [semantic-release/#2951](https://github.com/GyeongHoKim/lfify/issues/2951)
|
|
10
|
+
* dependencies ([87c9031](https://github.com/GyeongHoKim/lfify/commit/87c903143fde7c8c82d94a94d338e5467968ed4a))
|
|
11
|
+
* node 24 and unset NODE_AUTH_TOKEN for npm OIDC ([a74d183](https://github.com/GyeongHoKim/lfify/commit/a74d183248f25b187ef418127d80917c86dd6d86))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* include, exclude arguments ([9f4f2dc](https://github.com/GyeongHoKim/lfify/commit/9f4f2dc962f28c1ada58d6da556a35ef9dc4411b))
|
|
17
|
+
* log level ([f44f9c4](https://github.com/GyeongHoKim/lfify/commit/f44f9c4f3cd846088728dffe67f33e0526e61ffb))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Reverts
|
|
21
|
+
|
|
22
|
+
* revert "ci: remove node_auth_token force reset" ([3d76f97](https://github.com/GyeongHoKim/lfify/commit/3d76f97ad85d9874b8998c2ac9940474eb95bb24))
|
package/README.md
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
# LFify
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
A lightweight Node.js library to convert CRLF to LF line endings.
|
|
6
|
-
It is useful when your development environment is Windows.
|
|
3
|
+
A lightweight Node.js program to convert CRLF to LF line endings.
|
|
4
|
+
It is useful when your development environment is Windows.
|
|
7
5
|
|
|
8
6
|
## Getting started
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
### Using CLI options (no config file needed)
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx lfify --include "**/*.js" --exclude "node_modules/**"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Using config file
|
|
15
|
+
|
|
16
|
+
Create `.lfifyrc.json`:
|
|
11
17
|
|
|
12
18
|
```json
|
|
13
19
|
{
|
|
@@ -25,22 +31,52 @@ create .lfifyrc.json
|
|
|
25
31
|
"build/**",
|
|
26
32
|
"coverage/**"
|
|
27
33
|
]
|
|
28
|
-
}
|
|
34
|
+
}
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
Then run:
|
|
32
38
|
|
|
33
39
|
```bash
|
|
34
|
-
npx
|
|
40
|
+
npx lfify
|
|
35
41
|
```
|
|
36
42
|
|
|
37
|
-
you can add options below.
|
|
38
|
-
|
|
39
43
|
## Options
|
|
40
44
|
|
|
41
|
-
| Option
|
|
42
|
-
|
|
43
|
-
| `--config <path>`
|
|
45
|
+
| Option | Description |
|
|
46
|
+
| -------------------- | --------------------------------------------------------------------------- |
|
|
47
|
+
| `--config <path>` | Specify a custom path for the configuration file. Default is `.lfifyrc.json`. |
|
|
48
|
+
| `--entry <path>` | Specify the entry directory to process. Default is `./`. |
|
|
49
|
+
| `--include <pattern>`| Glob pattern(s) to include. Can be used multiple times. |
|
|
50
|
+
| `--exclude <pattern>`| Glob pattern(s) to exclude. Can be used multiple times. |
|
|
51
|
+
|
|
52
|
+
## Examples
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Process all JavaScript files, exclude node_modules
|
|
56
|
+
npx lfify --include "**/*.js" --exclude "node_modules/**"
|
|
57
|
+
|
|
58
|
+
# Process multiple file types
|
|
59
|
+
npx lfify --include "**/*.js" --include "**/*.ts" --exclude "node_modules/**" --exclude ".git/**"
|
|
60
|
+
|
|
61
|
+
# Process files in a specific directory
|
|
62
|
+
npx lfify --entry ./src --include "**/*.js"
|
|
63
|
+
|
|
64
|
+
# Use a custom config file
|
|
65
|
+
npx lfify --config ./custom-config.json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Default behavior
|
|
69
|
+
|
|
70
|
+
When no config file is found and no CLI options are provided, lfify uses sensible defaults:
|
|
71
|
+
- **include**: `**/*` (all files)
|
|
72
|
+
- **exclude**: `node_modules/**`, `.git/**`, `dist/**`, `build/**`, `coverage/**`
|
|
73
|
+
|
|
74
|
+
## Priority
|
|
75
|
+
|
|
76
|
+
CLI options take precedence over config file values:
|
|
77
|
+
1. CLI arguments (highest)
|
|
78
|
+
2. Config file
|
|
79
|
+
3. Default values (lowest)
|
|
44
80
|
|
|
45
81
|
# Development
|
|
46
82
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const x = 1;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const a = 1;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
b content
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log("app");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
doc content
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const x = 1;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
skipped;
|
package/eslint.config.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import pluginJs from "@eslint/js";
|
|
|
4
4
|
|
|
5
5
|
/** @type {import('eslint').Linter.Config[]} */
|
|
6
6
|
export default [
|
|
7
|
-
{
|
|
7
|
+
{ ignores: ["**/__fixtures__/**"] },
|
|
8
|
+
{ files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
|
|
8
9
|
{
|
|
9
10
|
languageOptions: {
|
|
10
11
|
globals: {
|
|
@@ -14,4 +15,4 @@ export default [
|
|
|
14
15
|
},
|
|
15
16
|
},
|
|
16
17
|
pluginJs.configs.recommended,
|
|
17
|
-
];
|
|
18
|
+
];
|
package/index.cjs
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
3
|
+
const { readFile, readdir, rename } = require("fs/promises");
|
|
4
|
+
const { createReadStream, createWriteStream } = require("fs");
|
|
5
|
+
const { resolve, join, relative } = require("path");
|
|
6
|
+
const { isMatch } = require("micromatch");
|
|
7
|
+
const { Transform } = require("stream");
|
|
8
|
+
const { pipeline } = require("stream/promises");
|
|
9
|
+
|
|
10
|
+
/** @type {ReadonlyArray<string>} */
|
|
11
|
+
const LOG_LEVELS = ['error', 'warn', 'info'];
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* @typedef {Object} Config
|
|
9
15
|
* @property {string} entry - 처리할 시작 디렉토리 경로
|
|
10
16
|
* @property {string[]} include - 포함할 파일 패턴 목록
|
|
11
17
|
* @property {string[]} exclude - 제외할 파일 패턴 목록
|
|
18
|
+
* @property {'error'|'warn'|'info'} [logLevel] - 로그 레벨 (error: 에러만, warn: 에러+경고, info: 전체)
|
|
12
19
|
*/
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
22
|
* @typedef {Object} Logger
|
|
23
|
+
* @property {function(string): void} setLogLevel - 로그 레벨 설정 ('error'|'warn'|'info')
|
|
16
24
|
* @property {function(string, string): void} warn - 경고 메시지 출력
|
|
17
25
|
* @property {function(string, string, Error=): void} error - 에러 메시지 출력
|
|
18
26
|
* @property {function(string, string): void} info - 정보 메시지 출력
|
|
@@ -20,7 +28,11 @@ const micromatch = require("micromatch");
|
|
|
20
28
|
|
|
21
29
|
/**
|
|
22
30
|
* @typedef {Object} CommandOptions
|
|
23
|
-
* @property {string} configPath - 설정 파일 경로
|
|
31
|
+
* @property {string} [configPath] - 설정 파일 경로
|
|
32
|
+
* @property {string} [entry] - CLI로 지정한 entry 경로
|
|
33
|
+
* @property {string[]} [include] - CLI로 지정한 include 패턴
|
|
34
|
+
* @property {string[]} [exclude] - CLI로 지정한 exclude 패턴
|
|
35
|
+
* @property {'error'|'warn'|'info'} [logLevel] - CLI로 지정한 로그 레벨
|
|
24
36
|
*/
|
|
25
37
|
|
|
26
38
|
/**
|
|
@@ -30,7 +42,25 @@ const micromatch = require("micromatch");
|
|
|
30
42
|
const DEFAULT_CONFIG = {
|
|
31
43
|
entry: './',
|
|
32
44
|
include: [],
|
|
33
|
-
exclude: []
|
|
45
|
+
exclude: [],
|
|
46
|
+
logLevel: 'error'
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sensible defaults when no config file is provided
|
|
51
|
+
* @type {Config}
|
|
52
|
+
*/
|
|
53
|
+
const SENSIBLE_DEFAULTS = {
|
|
54
|
+
entry: './',
|
|
55
|
+
include: ['**/*'],
|
|
56
|
+
exclude: [
|
|
57
|
+
'node_modules/**',
|
|
58
|
+
'.git/**',
|
|
59
|
+
'dist/**',
|
|
60
|
+
'build/**',
|
|
61
|
+
'coverage/**'
|
|
62
|
+
],
|
|
63
|
+
logLevel: 'error'
|
|
34
64
|
};
|
|
35
65
|
|
|
36
66
|
/**
|
|
@@ -41,16 +71,41 @@ const CONFIG_SCHEMA = {
|
|
|
41
71
|
entry: (value) => typeof value === 'string',
|
|
42
72
|
include: (value) => Array.isArray(value) && value.length > 0 && value.every(item => typeof item === 'string'),
|
|
43
73
|
exclude: (value) => Array.isArray(value) && value.length > 0 && value.every(item => typeof item === 'string'),
|
|
74
|
+
logLevel: (value) => LOG_LEVELS.includes(value),
|
|
44
75
|
};
|
|
45
76
|
|
|
46
77
|
/**
|
|
47
|
-
* Logging utility
|
|
48
|
-
* @type {Logger}
|
|
78
|
+
* Logging utility. 기본은 error만 출력. setLogLevel로 변경 가능.
|
|
79
|
+
* @type {Logger & { _level: string, setLogLevel: function(string): void }}
|
|
49
80
|
*/
|
|
50
81
|
const logger = {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
82
|
+
_level: 'error',
|
|
83
|
+
|
|
84
|
+
setLogLevel(level) {
|
|
85
|
+
if (LOG_LEVELS.includes(level)) {
|
|
86
|
+
this._level = level;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
error(msg, path, err) {
|
|
91
|
+
if (err !== undefined) {
|
|
92
|
+
console.error(`${msg} ${path}`, err);
|
|
93
|
+
} else {
|
|
94
|
+
console.error(`${msg} ${path}`);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
warn(msg, path) {
|
|
99
|
+
if (LOG_LEVELS.indexOf(this._level) >= LOG_LEVELS.indexOf('warn')) {
|
|
100
|
+
console.warn(`${msg} ${path}`);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
info(msg, path) {
|
|
105
|
+
if (LOG_LEVELS.indexOf(this._level) >= LOG_LEVELS.indexOf('info')) {
|
|
106
|
+
console.log(`${msg} ${path}`);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
54
109
|
};
|
|
55
110
|
|
|
56
111
|
/**
|
|
@@ -61,7 +116,7 @@ const logger = {
|
|
|
61
116
|
*/
|
|
62
117
|
async function readConfig(configPath) {
|
|
63
118
|
try {
|
|
64
|
-
const configContent = await
|
|
119
|
+
const configContent = await readFile(configPath, 'utf8');
|
|
65
120
|
const config = JSON.parse(configContent);
|
|
66
121
|
|
|
67
122
|
// Validate required fields
|
|
@@ -74,7 +129,7 @@ async function readConfig(configPath) {
|
|
|
74
129
|
return {
|
|
75
130
|
...DEFAULT_CONFIG,
|
|
76
131
|
...config,
|
|
77
|
-
entry:
|
|
132
|
+
entry: resolve(process.cwd(), config.entry || DEFAULT_CONFIG.entry)
|
|
78
133
|
};
|
|
79
134
|
} catch (err) {
|
|
80
135
|
if (err.code === 'ENOENT') {
|
|
@@ -82,7 +137,7 @@ async function readConfig(configPath) {
|
|
|
82
137
|
} else {
|
|
83
138
|
logger.error(`Error reading configuration file: ${err.message}`, configPath);
|
|
84
139
|
}
|
|
85
|
-
|
|
140
|
+
|
|
86
141
|
if (require.main === module) {
|
|
87
142
|
process.exit(1);
|
|
88
143
|
}
|
|
@@ -90,6 +145,95 @@ async function readConfig(configPath) {
|
|
|
90
145
|
}
|
|
91
146
|
}
|
|
92
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Resolve final configuration from CLI options, config file, and defaults
|
|
150
|
+
* @param {CommandOptions} cliOptions - parsed CLI options
|
|
151
|
+
* @returns {Promise<Config>} - resolved configuration
|
|
152
|
+
*/
|
|
153
|
+
async function resolveConfig(cliOptions) {
|
|
154
|
+
let fileConfig = null;
|
|
155
|
+
|
|
156
|
+
// Try to load config file if it exists
|
|
157
|
+
if (cliOptions.configPath) {
|
|
158
|
+
try {
|
|
159
|
+
const configContent = await readFile(cliOptions.configPath, 'utf8');
|
|
160
|
+
fileConfig = JSON.parse(configContent);
|
|
161
|
+
|
|
162
|
+
// Validate config file fields
|
|
163
|
+
for (const [key, validator] of Object.entries(CONFIG_SCHEMA)) {
|
|
164
|
+
if (fileConfig[key] && !validator(fileConfig[key])) {
|
|
165
|
+
throw new Error(`Invalid "${key}" in configuration file`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
if (err.code !== 'ENOENT') {
|
|
170
|
+
// Re-throw parsing/validation errors
|
|
171
|
+
logger.error(`Error reading configuration file: ${err.message}`, cliOptions.configPath);
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
// ENOENT is okay - config file is optional now
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Determine final values with precedence: CLI > config file > defaults
|
|
179
|
+
const hasCLIInclude = Array.isArray(cliOptions.include) && cliOptions.include.length > 0;
|
|
180
|
+
const hasCLIExclude = Array.isArray(cliOptions.exclude) && cliOptions.exclude.length > 0;
|
|
181
|
+
const hasCLIEntry = typeof cliOptions.entry === 'string';
|
|
182
|
+
const hasCLILogLevel = typeof cliOptions.logLevel === 'string' && LOG_LEVELS.includes(cliOptions.logLevel);
|
|
183
|
+
|
|
184
|
+
const hasFileConfig = fileConfig !== null;
|
|
185
|
+
const hasFileInclude = hasFileConfig && Array.isArray(fileConfig.include) && fileConfig.include.length > 0;
|
|
186
|
+
const hasFileExclude = hasFileConfig && Array.isArray(fileConfig.exclude) && fileConfig.exclude.length > 0;
|
|
187
|
+
const hasFileEntry = hasFileConfig && typeof fileConfig.entry === 'string';
|
|
188
|
+
const hasFileLogLevel = hasFileConfig && fileConfig.logLevel && LOG_LEVELS.includes(fileConfig.logLevel);
|
|
189
|
+
|
|
190
|
+
// Resolve each config property
|
|
191
|
+
let include, exclude, entry, logLevel;
|
|
192
|
+
|
|
193
|
+
// Include: CLI > file > default
|
|
194
|
+
if (hasCLIInclude) {
|
|
195
|
+
include = cliOptions.include;
|
|
196
|
+
} else if (hasFileInclude) {
|
|
197
|
+
include = fileConfig.include;
|
|
198
|
+
} else {
|
|
199
|
+
include = SENSIBLE_DEFAULTS.include;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Exclude: CLI > file > default
|
|
203
|
+
if (hasCLIExclude) {
|
|
204
|
+
exclude = cliOptions.exclude;
|
|
205
|
+
} else if (hasFileExclude) {
|
|
206
|
+
exclude = fileConfig.exclude;
|
|
207
|
+
} else {
|
|
208
|
+
exclude = SENSIBLE_DEFAULTS.exclude;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Entry: CLI > file > default
|
|
212
|
+
if (hasCLIEntry) {
|
|
213
|
+
entry = cliOptions.entry;
|
|
214
|
+
} else if (hasFileEntry) {
|
|
215
|
+
entry = fileConfig.entry;
|
|
216
|
+
} else {
|
|
217
|
+
entry = SENSIBLE_DEFAULTS.entry;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// LogLevel: CLI > file > default
|
|
221
|
+
if (hasCLILogLevel) {
|
|
222
|
+
logLevel = cliOptions.logLevel;
|
|
223
|
+
} else if (hasFileLogLevel) {
|
|
224
|
+
logLevel = fileConfig.logLevel;
|
|
225
|
+
} else {
|
|
226
|
+
logLevel = SENSIBLE_DEFAULTS.logLevel;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
entry: resolve(process.cwd(), entry),
|
|
231
|
+
include,
|
|
232
|
+
exclude,
|
|
233
|
+
logLevel
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
93
237
|
/**
|
|
94
238
|
* Parse command line arguments
|
|
95
239
|
* @returns {CommandOptions} - parsed arguments
|
|
@@ -101,9 +245,46 @@ function parseArgs() {
|
|
|
101
245
|
};
|
|
102
246
|
|
|
103
247
|
for (let i = 0; i < args.length; i++) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
248
|
+
const arg = args[i];
|
|
249
|
+
const nextArg = args[i + 1];
|
|
250
|
+
|
|
251
|
+
switch (arg) {
|
|
252
|
+
case '--config':
|
|
253
|
+
if (nextArg) {
|
|
254
|
+
options.configPath = nextArg;
|
|
255
|
+
i++;
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
|
|
259
|
+
case '--entry':
|
|
260
|
+
if (nextArg) {
|
|
261
|
+
options.entry = nextArg;
|
|
262
|
+
i++;
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
case '--include':
|
|
267
|
+
if (nextArg) {
|
|
268
|
+
options.include = options.include || [];
|
|
269
|
+
options.include.push(nextArg);
|
|
270
|
+
i++;
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
|
|
274
|
+
case '--exclude':
|
|
275
|
+
if (nextArg) {
|
|
276
|
+
options.exclude = options.exclude || [];
|
|
277
|
+
options.exclude.push(nextArg);
|
|
278
|
+
i++;
|
|
279
|
+
}
|
|
280
|
+
break;
|
|
281
|
+
|
|
282
|
+
case '--log-level':
|
|
283
|
+
if (nextArg && LOG_LEVELS.includes(nextArg)) {
|
|
284
|
+
options.logLevel = nextArg;
|
|
285
|
+
i++;
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
107
288
|
}
|
|
108
289
|
}
|
|
109
290
|
|
|
@@ -117,8 +298,8 @@ function parseArgs() {
|
|
|
117
298
|
* @returns {boolean} - true if file should be processed
|
|
118
299
|
*/
|
|
119
300
|
function shouldProcessFile(filePath, config) {
|
|
120
|
-
const isIncluded =
|
|
121
|
-
const isExcluded =
|
|
301
|
+
const isIncluded = isMatch(filePath, config.include);
|
|
302
|
+
const isExcluded = isMatch(filePath, config.exclude);
|
|
122
303
|
|
|
123
304
|
return isIncluded && !isExcluded;
|
|
124
305
|
}
|
|
@@ -132,14 +313,14 @@ function shouldProcessFile(filePath, config) {
|
|
|
132
313
|
*/
|
|
133
314
|
async function convertCRLFtoLF(dirPath, config) {
|
|
134
315
|
try {
|
|
135
|
-
const entries = await
|
|
316
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
136
317
|
|
|
137
318
|
/**
|
|
138
319
|
* @todo Node.js is single-threaded, if I want to convert files in parallel, I need to use worker_threads
|
|
139
320
|
*/
|
|
140
321
|
await Promise.all(entries.map(async entry => {
|
|
141
|
-
const fullPath =
|
|
142
|
-
const relativePath =
|
|
322
|
+
const fullPath = join(dirPath, entry.name);
|
|
323
|
+
const relativePath = relative(process.cwd(), fullPath).replace(/\\/g, "/");
|
|
143
324
|
|
|
144
325
|
if (entry.isDirectory()) {
|
|
145
326
|
await convertCRLFtoLF(fullPath, config);
|
|
@@ -162,29 +343,45 @@ async function convertCRLFtoLF(dirPath, config) {
|
|
|
162
343
|
* @throws {Error} - if there's an error reading or writing file
|
|
163
344
|
*/
|
|
164
345
|
async function processFile(filePath) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
346
|
+
const tmpPath = `${filePath}.tmp`;
|
|
347
|
+
const crlf2lf = new Transform({
|
|
348
|
+
transform(chunk, encoding, callback) {
|
|
349
|
+
const enc = encoding === 'buffer' ? 'utf8' : encoding;
|
|
350
|
+
const prev = this._leftover ?? '';
|
|
351
|
+
this._leftover = '';
|
|
352
|
+
const str = prev + chunk.toString(enc);
|
|
353
|
+
const safe = str.endsWith('\r') ? str.slice(0, -1) : str;
|
|
354
|
+
this._leftover = str.endsWith('\r') ? '\r' : '';
|
|
355
|
+
callback(null, safe.replace(/\r\n/g, '\n'));
|
|
356
|
+
},
|
|
357
|
+
flush(callback) {
|
|
358
|
+
callback(null, this._leftover ?? '');
|
|
178
359
|
}
|
|
360
|
+
});
|
|
361
|
+
try {
|
|
362
|
+
await pipeline(
|
|
363
|
+
createReadStream(filePath, { encoding: 'utf8' }),
|
|
364
|
+
crlf2lf,
|
|
365
|
+
createWriteStream(tmpPath, { encoding: 'utf8' })
|
|
366
|
+
);
|
|
367
|
+
logger.info(`converted ${filePath}`);
|
|
179
368
|
} catch (err) {
|
|
180
369
|
logger.error(`error processing file: ${filePath}`, filePath, err);
|
|
181
370
|
throw err;
|
|
182
371
|
}
|
|
372
|
+
try {
|
|
373
|
+
await rename(tmpPath, filePath);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
logger.error(`error rename file: ${tmpPath} to ${filePath}`);
|
|
376
|
+
throw err;
|
|
377
|
+
}
|
|
183
378
|
}
|
|
184
379
|
|
|
185
380
|
async function main() {
|
|
186
381
|
const options = parseArgs();
|
|
187
|
-
const config = await
|
|
382
|
+
const config = await resolveConfig(options);
|
|
383
|
+
|
|
384
|
+
logger.setLogLevel(config.logLevel);
|
|
188
385
|
|
|
189
386
|
logger.info(`converting CRLF to LF in: ${config.entry}`, config.entry);
|
|
190
387
|
|
|
@@ -202,4 +399,7 @@ module.exports = {
|
|
|
202
399
|
processFile,
|
|
203
400
|
readConfig,
|
|
204
401
|
parseArgs,
|
|
402
|
+
resolveConfig,
|
|
403
|
+
shouldProcessFile,
|
|
404
|
+
SENSIBLE_DEFAULTS,
|
|
205
405
|
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { resolveConfig, convertCRLFtoLF } = require('./index.cjs');
|
|
5
|
+
|
|
6
|
+
const FIXTURES_DIR = path.join(__dirname, '__fixtures__');
|
|
7
|
+
const CRLF = Buffer.from([0x0d, 0x0a]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Recursively copy a directory from src to dest.
|
|
11
|
+
* @param {string} src - source directory
|
|
12
|
+
* @param {string} dest - destination directory
|
|
13
|
+
*/
|
|
14
|
+
async function copyDir(src, dest) {
|
|
15
|
+
await fs.mkdir(dest, { recursive: true });
|
|
16
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const srcPath = path.join(src, entry.name);
|
|
19
|
+
const destPath = path.join(dest, entry.name);
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
await copyDir(srcPath, destPath);
|
|
22
|
+
} else {
|
|
23
|
+
await fs.copyFile(srcPath, destPath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('E2E: CRLF to LF with real filesystem', () => {
|
|
29
|
+
let tempDir;
|
|
30
|
+
let originalCwd;
|
|
31
|
+
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
originalCwd = process.cwd();
|
|
34
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'lfify-e2e-'));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(async () => {
|
|
38
|
+
try {
|
|
39
|
+
process.chdir(originalCwd);
|
|
40
|
+
if (tempDir) {
|
|
41
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// best-effort cleanup
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('US1: default run (no config)', () => {
|
|
49
|
+
it('converts files under entry and excludes node_modules and .git', async () => {
|
|
50
|
+
await copyDir(path.join(FIXTURES_DIR, 'default-sensible'), tempDir);
|
|
51
|
+
await fs.rename(path.join(tempDir, '_git'), path.join(tempDir, '.git'));
|
|
52
|
+
await fs.rename(path.join(tempDir, '_node_modules'), path.join(tempDir, 'node_modules'));
|
|
53
|
+
process.chdir(tempDir);
|
|
54
|
+
|
|
55
|
+
const config = await resolveConfig({});
|
|
56
|
+
await convertCRLFtoLF(config.entry, config);
|
|
57
|
+
|
|
58
|
+
const appJs = await fs.readFile(path.join(tempDir, 'src', 'app.js'));
|
|
59
|
+
expect(appJs.includes(CRLF)).toBe(false);
|
|
60
|
+
expect(appJs.equals(Buffer.from('console.log("app");\n', 'utf8'))).toBe(true);
|
|
61
|
+
|
|
62
|
+
const readmeTxt = await fs.readFile(path.join(tempDir, 'src', 'readme.txt'));
|
|
63
|
+
expect(readmeTxt.includes(CRLF)).toBe(false);
|
|
64
|
+
expect(readmeTxt.equals(Buffer.from('hello\nworld\n', 'utf8'))).toBe(true);
|
|
65
|
+
|
|
66
|
+
const nodeModulesPkg = await fs.readFile(
|
|
67
|
+
path.join(tempDir, 'node_modules', 'pkg', 'index.js')
|
|
68
|
+
);
|
|
69
|
+
expect(nodeModulesPkg.includes(CRLF)).toBe(true);
|
|
70
|
+
|
|
71
|
+
const gitConfig = await fs.readFile(path.join(tempDir, '.git', 'config'));
|
|
72
|
+
expect(gitConfig.includes(CRLF)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('US2: .lfifyrc.json', () => {
|
|
77
|
+
it('applies config include/exclude so only matching files are converted', async () => {
|
|
78
|
+
await copyDir(path.join(FIXTURES_DIR, 'with-config'), tempDir);
|
|
79
|
+
process.chdir(tempDir);
|
|
80
|
+
|
|
81
|
+
const config = await resolveConfig({ configPath: '.lfifyrc.json' });
|
|
82
|
+
await convertCRLFtoLF(config.entry, config);
|
|
83
|
+
|
|
84
|
+
const mainJs = await fs.readFile(path.join(tempDir, 'lib', 'main.js'));
|
|
85
|
+
expect(mainJs.includes(CRLF)).toBe(false);
|
|
86
|
+
expect(mainJs.equals(Buffer.from('const x = 1;\n', 'utf8'))).toBe(true);
|
|
87
|
+
|
|
88
|
+
const skipOtherJs = await fs.readFile(path.join(tempDir, 'skip', 'other.js'));
|
|
89
|
+
expect(skipOtherJs.includes(CRLF)).toBe(true);
|
|
90
|
+
|
|
91
|
+
const docTxt = await fs.readFile(path.join(tempDir, 'doc.txt'));
|
|
92
|
+
expect(docTxt.includes(CRLF)).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('US3: CLI overrides config', () => {
|
|
97
|
+
it('CLI include overrides config so only .js files are converted', async () => {
|
|
98
|
+
await copyDir(path.join(FIXTURES_DIR, 'cli-override'), tempDir);
|
|
99
|
+
process.chdir(tempDir);
|
|
100
|
+
|
|
101
|
+
const config = await resolveConfig({
|
|
102
|
+
configPath: '.lfifyrc.json',
|
|
103
|
+
include: ['**/*.js']
|
|
104
|
+
});
|
|
105
|
+
await convertCRLFtoLF(config.entry, config);
|
|
106
|
+
|
|
107
|
+
const aJs = await fs.readFile(path.join(tempDir, 'src', 'a.js'));
|
|
108
|
+
expect(aJs.includes(CRLF)).toBe(false);
|
|
109
|
+
expect(aJs.equals(Buffer.from('const a = 1;\n', 'utf8'))).toBe(true);
|
|
110
|
+
|
|
111
|
+
const bTxt = await fs.readFile(path.join(tempDir, 'src', 'b.txt'));
|
|
112
|
+
expect(bTxt.includes(CRLF)).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('US4: already LF', () => {
|
|
117
|
+
it('does not modify files that already use LF', async () => {
|
|
118
|
+
await copyDir(path.join(FIXTURES_DIR, 'already-lf'), tempDir);
|
|
119
|
+
process.chdir(tempDir);
|
|
120
|
+
|
|
121
|
+
const config = await resolveConfig({});
|
|
122
|
+
await convertCRLFtoLF(config.entry, config);
|
|
123
|
+
|
|
124
|
+
const fileJs = await fs.readFile(path.join(tempDir, 'src', 'file.js'));
|
|
125
|
+
expect(fileJs.includes(CRLF)).toBe(false);
|
|
126
|
+
expect(fileJs.equals(Buffer.from('const x = 1;\n', 'utf8'))).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
package/index.test.js
CHANGED
|
@@ -1,41 +1,46 @@
|
|
|
1
|
-
const
|
|
1
|
+
const mock = require('mock-fs');
|
|
2
|
+
const { readConfig, parseArgs, processFile, resolveConfig, shouldProcessFile, SENSIBLE_DEFAULTS } = require('./index.cjs');
|
|
3
|
+
const fs = require('fs');
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
function baseMock(overrides = {}) {
|
|
6
|
+
return {
|
|
7
|
+
'src/file1.txt': 'hello\r\nworld\r\n',
|
|
8
|
+
'src/file2.js': 'console.log("test");\r\n',
|
|
9
|
+
'src/subdir/file3.txt': 'test\r\n',
|
|
10
|
+
'test/file1.txt': 'hello\r\nworld\r\n',
|
|
11
|
+
'test/file2.js': 'console.log("test");\r\n',
|
|
12
|
+
'test/subdir/file3.txt': 'test\r\n',
|
|
13
|
+
'node_modules/file.js': 'console.log("test");\r\n',
|
|
14
|
+
'node_modules/subdir/file4.txt': 'test\r\n',
|
|
15
|
+
'index.js': 'console.log("test");\r\n',
|
|
16
|
+
...overrides
|
|
17
|
+
};
|
|
18
|
+
}
|
|
6
19
|
|
|
7
20
|
describe('CRLF to LF Converter', () => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'./src/file2.js': 'console.log("test");\r\n',
|
|
11
|
-
'./src/subdir/file3.txt': 'test\r\n',
|
|
12
|
-
'./test/file1.txt': 'hello\r\nworld\r\n',
|
|
13
|
-
'./test/file2.js': 'console.log("test");\r\n',
|
|
14
|
-
'./test/subdir/file3.txt': 'test\r\n',
|
|
15
|
-
'./node_modules/file.js': 'console.log("test");\r\n',
|
|
16
|
-
'./node_modules/subdir/file4.txt': 'test\r\n',
|
|
17
|
-
'index.js': 'console.log("test");\r\n'
|
|
18
|
-
};
|
|
19
|
-
|
|
21
|
+
let originalArgv;
|
|
22
|
+
|
|
20
23
|
beforeEach(() => {
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
mock(baseMock());
|
|
25
|
+
originalArgv = process.argv;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
mock.restore();
|
|
30
|
+
process.argv = originalArgv;
|
|
23
31
|
});
|
|
24
32
|
|
|
25
33
|
describe('readConfig', () => {
|
|
26
34
|
it('should return config when valid config file is provided', async () => {
|
|
27
|
-
// arrange
|
|
28
35
|
const validConfig = {
|
|
29
36
|
entry: './',
|
|
30
37
|
include: ['*.js'],
|
|
31
38
|
exclude: ['node_modules/**']
|
|
32
39
|
};
|
|
33
|
-
|
|
40
|
+
mock(baseMock({ '.lfifyrc.json': JSON.stringify(validConfig) }));
|
|
34
41
|
|
|
35
|
-
// act
|
|
36
42
|
const config = await readConfig('.lfifyrc.json');
|
|
37
43
|
|
|
38
|
-
// assert
|
|
39
44
|
expect(config).toEqual(expect.objectContaining({
|
|
40
45
|
entry: expect.any(String),
|
|
41
46
|
include: expect.any(Array),
|
|
@@ -44,64 +49,258 @@ describe('CRLF to LF Converter', () => {
|
|
|
44
49
|
});
|
|
45
50
|
|
|
46
51
|
it('should throw error when config file is not found', async () => {
|
|
47
|
-
// act & assert
|
|
48
52
|
await expect(readConfig('.lfifyrc.json')).rejects.toThrow();
|
|
49
53
|
});
|
|
50
54
|
|
|
51
55
|
it('should throw error when config file is invalid json', async () => {
|
|
52
|
-
|
|
53
|
-
require('fs').__setConfig('invalid json');
|
|
56
|
+
mock(baseMock({ '.lfifyrc.json': 'invalid json' }));
|
|
54
57
|
|
|
55
|
-
// act & assert
|
|
56
58
|
await expect(readConfig('.lfifyrc.json')).rejects.toThrow();
|
|
57
59
|
});
|
|
58
60
|
});
|
|
59
61
|
|
|
60
62
|
describe('parseArgs', () => {
|
|
61
63
|
it('should return config path when --config option is provided', () => {
|
|
62
|
-
// arrange
|
|
63
64
|
process.argv = ['node', 'lfify', '--config', './path/for/test/.lfifyrc.json'];
|
|
64
65
|
|
|
65
|
-
// act
|
|
66
66
|
const options = parseArgs();
|
|
67
67
|
|
|
68
|
-
// assert
|
|
69
68
|
expect(options.configPath).toBe('./path/for/test/.lfifyrc.json');
|
|
70
69
|
});
|
|
71
70
|
|
|
72
71
|
it('should return default config path when --config option is not provided', () => {
|
|
73
|
-
// arrange
|
|
74
72
|
process.argv = ['node', 'lfify'];
|
|
75
73
|
|
|
76
|
-
// act
|
|
77
74
|
const options = parseArgs();
|
|
78
75
|
|
|
79
|
-
// assert
|
|
80
76
|
expect(options.configPath).toBe('.lfifyrc.json');
|
|
81
77
|
});
|
|
78
|
+
|
|
79
|
+
it('should return include patterns when single --include option is provided', () => {
|
|
80
|
+
process.argv = ['node', 'lfify', '--include', '**/*.js'];
|
|
81
|
+
const options = parseArgs();
|
|
82
|
+
expect(options.include).toEqual(['**/*.js']);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should return multiple include patterns when multiple --include options are provided', () => {
|
|
86
|
+
process.argv = ['node', 'lfify', '--include', '**/*.js', '--include', '**/*.ts'];
|
|
87
|
+
const options = parseArgs();
|
|
88
|
+
expect(options.include).toEqual(['**/*.js', '**/*.ts']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should return exclude patterns when --exclude option is provided', () => {
|
|
92
|
+
process.argv = ['node', 'lfify', '--exclude', 'node_modules/**'];
|
|
93
|
+
const options = parseArgs();
|
|
94
|
+
expect(options.exclude).toEqual(['node_modules/**']);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should return multiple exclude patterns when multiple --exclude options are provided', () => {
|
|
98
|
+
process.argv = ['node', 'lfify', '--exclude', 'dist/**', '--exclude', 'coverage/**'];
|
|
99
|
+
const options = parseArgs();
|
|
100
|
+
expect(options.exclude).toEqual(['dist/**', 'coverage/**']);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should return entry path when --entry option is provided', () => {
|
|
104
|
+
process.argv = ['node', 'lfify', '--entry', './src'];
|
|
105
|
+
const options = parseArgs();
|
|
106
|
+
expect(options.entry).toContain('src');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle all options together', () => {
|
|
110
|
+
process.argv = ['node', 'lfify', '--config', 'custom.json', '--include', '*.js', '--exclude', 'dist/**', '--entry', './lib', '--log-level', 'info'];
|
|
111
|
+
const options = parseArgs();
|
|
112
|
+
expect(options.configPath).toBe('custom.json');
|
|
113
|
+
expect(options.include).toEqual(['*.js']);
|
|
114
|
+
expect(options.exclude).toEqual(['dist/**']);
|
|
115
|
+
expect(options.entry).toContain('lib');
|
|
116
|
+
expect(options.logLevel).toBe('info');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should return undefined for include/exclude/entry when not provided', () => {
|
|
120
|
+
process.argv = ['node', 'lfify'];
|
|
121
|
+
const options = parseArgs();
|
|
122
|
+
expect(options.include).toBeUndefined();
|
|
123
|
+
expect(options.exclude).toBeUndefined();
|
|
124
|
+
expect(options.entry).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should return logLevel when --log-level option is provided', () => {
|
|
128
|
+
process.argv = ['node', 'lfify', '--log-level', 'warn'];
|
|
129
|
+
const options = parseArgs();
|
|
130
|
+
expect(options.logLevel).toBe('warn');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should accept error, warn, info for --log-level', () => {
|
|
134
|
+
for (const level of ['error', 'warn', 'info']) {
|
|
135
|
+
process.argv = ['node', 'lfify', '--log-level', level];
|
|
136
|
+
expect(parseArgs().logLevel).toBe(level);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
82
139
|
});
|
|
83
140
|
|
|
84
141
|
describe('shouldProcessFile', () => {
|
|
85
142
|
it('should return true when file matches include pattern and does not match exclude pattern', () => {
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
143
|
+
const config = { include: ['**/*.js'], exclude: ['node_modules/**'] };
|
|
144
|
+
expect(shouldProcessFile('src/app.js', config)).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should return false when file matches exclude pattern', () => {
|
|
148
|
+
const config = { include: ['**/*.js'], exclude: ['node_modules/**'] };
|
|
149
|
+
expect(shouldProcessFile('node_modules/pkg/index.js', config)).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should return false when file does not match include pattern', () => {
|
|
153
|
+
const config = { include: ['**/*.js'], exclude: ['node_modules/**'] };
|
|
154
|
+
expect(shouldProcessFile('src/readme.txt', config)).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should handle multiple include patterns', () => {
|
|
158
|
+
const config = { include: ['**/*.js', '**/*.ts'], exclude: ['node_modules/**'] };
|
|
159
|
+
expect(shouldProcessFile('src/app.js', config)).toBe(true);
|
|
160
|
+
expect(shouldProcessFile('src/app.ts', config)).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should handle multiple exclude patterns', () => {
|
|
164
|
+
const config = { include: ['**/*.js'], exclude: ['node_modules/**', 'dist/**', 'build/**', 'coverage/**'] };
|
|
165
|
+
expect(shouldProcessFile('src/file.js', config)).toBe(true);
|
|
166
|
+
expect(shouldProcessFile('node_modules/pkg/file.js', config)).toBe(false);
|
|
167
|
+
expect(shouldProcessFile('dist/bundle.js', config)).toBe(false);
|
|
168
|
+
expect(shouldProcessFile('test/unit.js', config)).toBe(true);
|
|
91
169
|
});
|
|
92
170
|
});
|
|
93
171
|
|
|
94
172
|
describe('processFile', () => {
|
|
95
173
|
it('should convert CRLF to LF when file is processed', async () => {
|
|
96
|
-
// arrange
|
|
97
174
|
const shouldbe = 'hello\nworld\n';
|
|
98
175
|
|
|
99
|
-
// act
|
|
100
176
|
await processFile('./src/file1.txt');
|
|
101
|
-
const content = await
|
|
177
|
+
const content = await fs.promises.readFile('./src/file1.txt', 'utf8');
|
|
102
178
|
|
|
103
|
-
// assert
|
|
104
179
|
expect(content).toBe(shouldbe);
|
|
105
|
-
})
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should not modify file when no CRLF exists', async () => {
|
|
183
|
+
mock(baseMock({ 'src/clean.txt': 'hello\nworld\n' }));
|
|
184
|
+
|
|
185
|
+
await processFile('./src/clean.txt');
|
|
186
|
+
const content = await fs.promises.readFile('./src/clean.txt', 'utf8');
|
|
187
|
+
|
|
188
|
+
expect(content).toBe('hello\nworld\n');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('resolveConfig', () => {
|
|
193
|
+
it('should use CLI options when provided without config file', async () => {
|
|
194
|
+
const options = {
|
|
195
|
+
include: ['**/*.js'],
|
|
196
|
+
exclude: ['node_modules/**'],
|
|
197
|
+
entry: './src'
|
|
198
|
+
};
|
|
199
|
+
const config = await resolveConfig(options);
|
|
200
|
+
|
|
201
|
+
expect(config.include).toEqual(['**/*.js']);
|
|
202
|
+
expect(config.exclude).toEqual(['node_modules/**']);
|
|
203
|
+
expect(config.entry).toContain('src');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should use sensible defaults when no config file and no CLI options', async () => {
|
|
207
|
+
const config = await resolveConfig({});
|
|
208
|
+
|
|
209
|
+
expect(config.include).toEqual(SENSIBLE_DEFAULTS.include);
|
|
210
|
+
expect(config.exclude).toEqual(SENSIBLE_DEFAULTS.exclude);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should override config file values with CLI options', async () => {
|
|
214
|
+
mock(baseMock({
|
|
215
|
+
'.lfifyrc.json': JSON.stringify({
|
|
216
|
+
entry: './',
|
|
217
|
+
include: ['**/*.md'],
|
|
218
|
+
exclude: ['dist/**']
|
|
219
|
+
})
|
|
220
|
+
}));
|
|
221
|
+
|
|
222
|
+
const options = {
|
|
223
|
+
configPath: '.lfifyrc.json',
|
|
224
|
+
include: ['**/*.js']
|
|
225
|
+
};
|
|
226
|
+
const config = await resolveConfig(options);
|
|
227
|
+
|
|
228
|
+
expect(config.include).toEqual(['**/*.js']);
|
|
229
|
+
expect(config.exclude).toEqual(['dist/**']);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should load config file when configPath is provided and file exists', async () => {
|
|
233
|
+
mock(baseMock({
|
|
234
|
+
'.lfifyrc.json': JSON.stringify({
|
|
235
|
+
entry: './lib',
|
|
236
|
+
include: ['**/*.ts'],
|
|
237
|
+
exclude: ['test/**']
|
|
238
|
+
})
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
const options = { configPath: '.lfifyrc.json' };
|
|
242
|
+
const config = await resolveConfig(options);
|
|
243
|
+
|
|
244
|
+
expect(config.include).toEqual(['**/*.ts']);
|
|
245
|
+
expect(config.exclude).toEqual(['test/**']);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should use defaults when config file not found and no CLI options', async () => {
|
|
249
|
+
const options = { configPath: 'nonexistent.json' };
|
|
250
|
+
const config = await resolveConfig(options);
|
|
251
|
+
|
|
252
|
+
expect(config.include).toEqual(SENSIBLE_DEFAULTS.include);
|
|
253
|
+
expect(config.exclude).toEqual(SENSIBLE_DEFAULTS.exclude);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should use CLI include with default exclude when only include provided', async () => {
|
|
257
|
+
const options = { include: ['**/*.js'] };
|
|
258
|
+
const config = await resolveConfig(options);
|
|
259
|
+
|
|
260
|
+
expect(config.include).toEqual(['**/*.js']);
|
|
261
|
+
expect(config.exclude).toEqual(SENSIBLE_DEFAULTS.exclude);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should use CLI exclude with default include when only exclude provided', async () => {
|
|
265
|
+
const options = { exclude: ['custom/**'] };
|
|
266
|
+
const config = await resolveConfig(options);
|
|
267
|
+
|
|
268
|
+
expect(config.include).toEqual(SENSIBLE_DEFAULTS.include);
|
|
269
|
+
expect(config.exclude).toEqual(['custom/**']);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should include logLevel in config, defaulting to error', async () => {
|
|
273
|
+
const config = await resolveConfig({});
|
|
274
|
+
expect(config.logLevel).toBe('error');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should use logLevel from config file when provided', async () => {
|
|
278
|
+
mock(baseMock({
|
|
279
|
+
'.lfifyrc.json': JSON.stringify({
|
|
280
|
+
entry: './',
|
|
281
|
+
include: ['**/*.js'],
|
|
282
|
+
exclude: ['node_modules/**'],
|
|
283
|
+
logLevel: 'info'
|
|
284
|
+
})
|
|
285
|
+
}));
|
|
286
|
+
const config = await resolveConfig({ configPath: '.lfifyrc.json' });
|
|
287
|
+
expect(config.logLevel).toBe('info');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should override config file logLevel with CLI --log-level', async () => {
|
|
291
|
+
mock(baseMock({
|
|
292
|
+
'.lfifyrc.json': JSON.stringify({
|
|
293
|
+
entry: './',
|
|
294
|
+
include: ['**/*.js'],
|
|
295
|
+
exclude: ['node_modules/**'],
|
|
296
|
+
logLevel: 'warn'
|
|
297
|
+
})
|
|
298
|
+
}));
|
|
299
|
+
const config = await resolveConfig({
|
|
300
|
+
configPath: '.lfifyrc.json',
|
|
301
|
+
logLevel: 'info'
|
|
302
|
+
});
|
|
303
|
+
expect(config.logLevel).toBe('info');
|
|
304
|
+
});
|
|
106
305
|
});
|
|
107
|
-
});
|
|
306
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lfify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "make your crlf to lf",
|
|
6
6
|
"main": "index.cjs",
|
|
@@ -9,15 +9,19 @@
|
|
|
9
9
|
"lfify": "./index.cjs"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test": "
|
|
12
|
+
"test": "npm run test:unit && npm run test:e2e",
|
|
13
|
+
"test:unit": "jest --testPathPattern=\"index\\.test\\.js\"",
|
|
14
|
+
"test:e2e": "jest --testPathPattern=\"index\\.e2e\\.test\\.js\"",
|
|
13
15
|
"lint": "eslint ."
|
|
14
16
|
},
|
|
15
17
|
"repository": {
|
|
16
18
|
"type": "git",
|
|
17
|
-
"url": "https://github.com/GyeongHoKim/lfify"
|
|
19
|
+
"url": "https://github.com/GyeongHoKim/lfify.git"
|
|
18
20
|
},
|
|
19
21
|
"publishConfig": {
|
|
20
|
-
"registry": "https://registry.npmjs.org"
|
|
22
|
+
"registry": "https://registry.npmjs.org/",
|
|
23
|
+
"provenance": true,
|
|
24
|
+
"tag": "latest"
|
|
21
25
|
},
|
|
22
26
|
"keywords": [
|
|
23
27
|
"eol",
|
|
@@ -35,8 +39,15 @@
|
|
|
35
39
|
},
|
|
36
40
|
"devDependencies": {
|
|
37
41
|
"@eslint/js": "^9.15.0",
|
|
42
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
43
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
44
|
+
"@semantic-release/git": "^10.0.1",
|
|
45
|
+
"@semantic-release/npm": "^13.1.3",
|
|
46
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
38
47
|
"eslint": "^9.15.0",
|
|
39
48
|
"globals": "^15.12.0",
|
|
40
|
-
"jest": "^29.7.0"
|
|
49
|
+
"jest": "^29.7.0",
|
|
50
|
+
"mock-fs": "^5.5.0",
|
|
51
|
+
"semantic-release": "^25.0.2"
|
|
41
52
|
}
|
|
42
53
|
}
|
package/__mocks__/fs.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
const fs = jest.createMockFromModule('fs');
|
|
2
|
-
|
|
3
|
-
const mockFiles = new Map();
|
|
4
|
-
|
|
5
|
-
function __setMockFiles(newMockFiles) {
|
|
6
|
-
mockFiles.clear();
|
|
7
|
-
for (const [path, content] of Object.entries(newMockFiles)) {
|
|
8
|
-
mockFiles.set(path, content);
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function __setConfig(stringifiedConfig, path = '.lfifyrc.json') {
|
|
13
|
-
mockFiles.set(path, stringifiedConfig);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const promises = {
|
|
17
|
-
/* eslint-disable-next-line no-unused-vars */
|
|
18
|
-
readFile: jest.fn().mockImplementation((path, ...rest) => {
|
|
19
|
-
if (mockFiles.has(path)) {
|
|
20
|
-
return Promise.resolve(mockFiles.get(path));
|
|
21
|
-
}
|
|
22
|
-
return Promise.reject(new Error(`ENOENT: no such file or directory, open '${path}'`));
|
|
23
|
-
}),
|
|
24
|
-
|
|
25
|
-
writeFile: jest.fn().mockImplementation((path, content) => {
|
|
26
|
-
mockFiles.set(path, content);
|
|
27
|
-
return Promise.resolve();
|
|
28
|
-
}),
|
|
29
|
-
|
|
30
|
-
/* eslint-disable-next-line no-unused-vars */
|
|
31
|
-
readdir: jest.fn().mockImplementation((path, ...rest) => {
|
|
32
|
-
const entries = [];
|
|
33
|
-
for (const filePath of mockFiles.keys()) {
|
|
34
|
-
if (filePath.startsWith(path)) {
|
|
35
|
-
const relativePath = filePath.slice(path.length + 1);
|
|
36
|
-
const name = relativePath.split('/')[0];
|
|
37
|
-
if (name && !entries.some(e => e.name === name)) {
|
|
38
|
-
entries.push({
|
|
39
|
-
name,
|
|
40
|
-
isFile: () => !name.includes('/'),
|
|
41
|
-
isDirectory: () => name.includes('/')
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return Promise.resolve(entries);
|
|
47
|
-
})
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
fs.promises = promises;
|
|
51
|
-
fs.__setMockFiles = __setMockFiles;
|
|
52
|
-
fs.__setConfig = __setConfig;
|
|
53
|
-
|
|
54
|
-
module.exports = fs;
|
package/__mocks__/micromatch.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
const micromatch = jest.createMockFromModule('micromatch');
|
|
2
|
-
|
|
3
|
-
micromatch.isMatch = jest.fn().mockImplementation((filePath, patterns) => {
|
|
4
|
-
if (!Array.isArray(patterns)) {
|
|
5
|
-
patterns = [patterns];
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
// 간단한 glob 패턴 매칭 구현
|
|
9
|
-
return patterns.some(pattern => {
|
|
10
|
-
// 정확한 매칭
|
|
11
|
-
if (pattern === filePath) return true;
|
|
12
|
-
|
|
13
|
-
// 와일드카드 매칭
|
|
14
|
-
const regexPattern = pattern
|
|
15
|
-
.replace(/\./g, '\\.')
|
|
16
|
-
.replace(/\*/g, '.*')
|
|
17
|
-
.replace(/\?/g, '.');
|
|
18
|
-
|
|
19
|
-
return new RegExp(`^${regexPattern}$`).test(filePath);
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
module.exports = micromatch;
|