lfify 1.1.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 +36 -0
- package/.github/workflows/ci.yml +42 -0
- package/.lfifyrc-sample.json +16 -0
- package/.vscode/launch.json +31 -0
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/__mocks__/fs.js +54 -0
- package/__mocks__/micromatch.js +23 -0
- package/__mocks__/path.js +17 -0
- package/eslint.config.mjs +17 -0
- package/index.cjs +205 -0
- package/index.test.js +107 -0
- package/package.json +42 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read # for checkout
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release:
|
|
12
|
+
name: Release
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
permissions:
|
|
15
|
+
contents: write # to be able to publish a GitHub release
|
|
16
|
+
issues: write # to be able to comment on released issues
|
|
17
|
+
pull-requests: write # to be able to comment on released pull requests
|
|
18
|
+
id-token: write # to enable use of OIDC for npm provenance
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
with:
|
|
23
|
+
fetch-depth: 0
|
|
24
|
+
- name: Setup Node.js
|
|
25
|
+
uses: actions/setup-node@v4
|
|
26
|
+
with:
|
|
27
|
+
node-version: "lts/*"
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: npm clean-install
|
|
30
|
+
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
|
|
31
|
+
run: npm audit signatures
|
|
32
|
+
- name: Release
|
|
33
|
+
env:
|
|
34
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
35
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
36
|
+
run: npx semantic-release
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Lint and Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
push:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v3
|
|
15
|
+
|
|
16
|
+
- name: Setup Node.js
|
|
17
|
+
uses: actions/setup-node@v3
|
|
18
|
+
with:
|
|
19
|
+
node-version: '18'
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: npm ci
|
|
23
|
+
|
|
24
|
+
- name: Run linting
|
|
25
|
+
run: npm run lint || true
|
|
26
|
+
|
|
27
|
+
test:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v3
|
|
32
|
+
|
|
33
|
+
- name: Setup Node.js
|
|
34
|
+
uses: actions/setup-node@v3
|
|
35
|
+
with:
|
|
36
|
+
node-version: '18'
|
|
37
|
+
|
|
38
|
+
- name: Install dependencies
|
|
39
|
+
run: npm ci
|
|
40
|
+
|
|
41
|
+
- name: Run tests
|
|
42
|
+
run: npm test
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Use IntelliSense to learn about possible attributes.
|
|
3
|
+
// Hover to view descriptions of existing attributes.
|
|
4
|
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"configurations": [
|
|
7
|
+
{
|
|
8
|
+
"type": "node",
|
|
9
|
+
"request": "launch",
|
|
10
|
+
"name": "Jest Tests Debug",
|
|
11
|
+
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
|
|
12
|
+
"args": [
|
|
13
|
+
"--runInBand",
|
|
14
|
+
"--watchAll=false",
|
|
15
|
+
"--testTimeout=100000",
|
|
16
|
+
"--detectOpenHandles"
|
|
17
|
+
],
|
|
18
|
+
"console": "integratedTerminal",
|
|
19
|
+
"internalConsoleOptions": "neverOpen"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"type": "node",
|
|
23
|
+
"request": "launch",
|
|
24
|
+
"name": "Launch Program",
|
|
25
|
+
"skipFiles": [
|
|
26
|
+
"<node_internals>/**"
|
|
27
|
+
],
|
|
28
|
+
"program": "${workspaceFolder}/index.cjs"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 GyeongHo Kim
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# LFify
|
|
2
|
+
|
|
3
|
+
> ⚠️ **Warning**: All files must be encoded in UTF-8. The library is being developed to automatically convert UTF-8 with BOM to UTF-8 without BOM. Using different encodings may cause unexpected issues.
|
|
4
|
+
|
|
5
|
+
A lightweight Node.js library to convert CRLF to LF line endings.
|
|
6
|
+
It is useful when your development environment is Windows.
|
|
7
|
+
|
|
8
|
+
## Getting started
|
|
9
|
+
|
|
10
|
+
create .lfifyrc.json
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"entry": "./",
|
|
15
|
+
"include": [
|
|
16
|
+
"**/*.{js,ts,jsx,tsx}",
|
|
17
|
+
"**/*.{json,md}",
|
|
18
|
+
"**/*.{css,scss}",
|
|
19
|
+
"**/*.{html,vue}"
|
|
20
|
+
],
|
|
21
|
+
"exclude": [
|
|
22
|
+
"node_modules/**",
|
|
23
|
+
".git/**",
|
|
24
|
+
"dist/**",
|
|
25
|
+
"build/**",
|
|
26
|
+
"coverage/**"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
and then
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx lifify
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
you can add options below.
|
|
38
|
+
|
|
39
|
+
## Options
|
|
40
|
+
|
|
41
|
+
| Option | Description |
|
|
42
|
+
|--------|-------------|
|
|
43
|
+
| `--config <path>` | Specify a custom path for the configuration file. Default is `.lfifyrc.json` in the current directory. |
|
|
44
|
+
|
|
45
|
+
# Development
|
|
46
|
+
|
|
47
|
+
## Prerequisites
|
|
48
|
+
|
|
49
|
+
- Node.js 18 or higher
|
|
50
|
+
- npm
|
|
51
|
+
|
|
52
|
+
## Setup
|
|
53
|
+
|
|
54
|
+
Clone the repository:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/GyeongHoKim/lfify.git
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Install dependencies:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
# Testing
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm test
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
# Linting
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm run lint
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
# Contributing
|
|
79
|
+
|
|
80
|
+
1. Fork the repository
|
|
81
|
+
2. Create a new branch
|
|
82
|
+
3. Make your changes
|
|
83
|
+
4. Run `npm run lint` to check your code
|
|
84
|
+
5. Run `npm test` to check your code
|
|
85
|
+
6. Submit a pull request
|
|
86
|
+
|
|
87
|
+
# Issues and Support
|
|
88
|
+
|
|
89
|
+
If you have any issues or feedback, please open an [issue](https://github.com/GyeongHoKim/lfify/issues) on the GitHub repository.
|
package/__mocks__/fs.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
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;
|
|
@@ -0,0 +1,23 @@
|
|
|
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;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const path = jest.createMockFromModule('path');
|
|
2
|
+
|
|
3
|
+
const actualPath = jest.requireActual('path');
|
|
4
|
+
|
|
5
|
+
path.join = jest.fn().mockImplementation((...paths) => {
|
|
6
|
+
return actualPath.join(...paths);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
path.resolve = jest.fn().mockImplementation((...paths) => {
|
|
10
|
+
return actualPath.resolve(...paths);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
path.relative = jest.fn().mockImplementation((from, to) => {
|
|
14
|
+
return actualPath.relative(from, to);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
module.exports = path;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import globals from "globals";
|
|
2
|
+
import pluginJs from "@eslint/js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/** @type {import('eslint').Linter.Config[]} */
|
|
6
|
+
export default [
|
|
7
|
+
{files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}},
|
|
8
|
+
{
|
|
9
|
+
languageOptions: {
|
|
10
|
+
globals: {
|
|
11
|
+
...globals.node,
|
|
12
|
+
...globals.jest,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
pluginJs.configs.recommended,
|
|
17
|
+
];
|
package/index.cjs
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs").promises;
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const micromatch = require("micromatch");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} Config
|
|
9
|
+
* @property {string} entry - 처리할 시작 디렉토리 경로
|
|
10
|
+
* @property {string[]} include - 포함할 파일 패턴 목록
|
|
11
|
+
* @property {string[]} exclude - 제외할 파일 패턴 목록
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} Logger
|
|
16
|
+
* @property {function(string, string): void} warn - 경고 메시지 출력
|
|
17
|
+
* @property {function(string, string, Error=): void} error - 에러 메시지 출력
|
|
18
|
+
* @property {function(string, string): void} info - 정보 메시지 출력
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} CommandOptions
|
|
23
|
+
* @property {string} configPath - 설정 파일 경로
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Default configuration
|
|
28
|
+
* @type {Config}
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_CONFIG = {
|
|
31
|
+
entry: './',
|
|
32
|
+
include: [],
|
|
33
|
+
exclude: []
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Configuration validation schema
|
|
38
|
+
* @type {Object.<string, function(*): boolean>}
|
|
39
|
+
*/
|
|
40
|
+
const CONFIG_SCHEMA = {
|
|
41
|
+
entry: (value) => typeof value === 'string',
|
|
42
|
+
include: (value) => Array.isArray(value) && value.length > 0 && value.every(item => typeof item === 'string'),
|
|
43
|
+
exclude: (value) => Array.isArray(value) && value.length > 0 && value.every(item => typeof item === 'string'),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Logging utility
|
|
48
|
+
* @type {Logger}
|
|
49
|
+
*/
|
|
50
|
+
const logger = {
|
|
51
|
+
warn: (msg, path) => console.warn(`${msg} ${path}`),
|
|
52
|
+
error: (msg, path, err) => console.error(`${msg} ${path}`, err),
|
|
53
|
+
info: (msg, path) => console.log(`${msg} ${path}`),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Read and validate configuration file
|
|
58
|
+
* @param {string} configPath - path to configuration file
|
|
59
|
+
* @returns {Promise<Config>} - validated configuration
|
|
60
|
+
* @throws {Error} - if configuration is invalid or file is not found
|
|
61
|
+
*/
|
|
62
|
+
async function readConfig(configPath) {
|
|
63
|
+
try {
|
|
64
|
+
const configContent = await fs.readFile(configPath, 'utf8');
|
|
65
|
+
const config = JSON.parse(configContent);
|
|
66
|
+
|
|
67
|
+
// Validate required fields
|
|
68
|
+
for (const [key, validator] of Object.entries(CONFIG_SCHEMA)) {
|
|
69
|
+
if (config[key] && !validator(config[key])) {
|
|
70
|
+
throw new Error(`Invalid "${key}" in configuration file`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...DEFAULT_CONFIG,
|
|
76
|
+
...config,
|
|
77
|
+
entry: path.resolve(process.cwd(), config.entry || DEFAULT_CONFIG.entry)
|
|
78
|
+
};
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err.code === 'ENOENT') {
|
|
81
|
+
logger.error(`Configuration file not found: ${configPath}`, configPath);
|
|
82
|
+
} else {
|
|
83
|
+
logger.error(`Error reading configuration file: ${err.message}`, configPath);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (require.main === module) {
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse command line arguments
|
|
95
|
+
* @returns {CommandOptions} - parsed arguments
|
|
96
|
+
*/
|
|
97
|
+
function parseArgs() {
|
|
98
|
+
const args = process.argv.slice(2);
|
|
99
|
+
const options = {
|
|
100
|
+
configPath: '.lfifyrc.json'
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < args.length; i++) {
|
|
104
|
+
if (args[i] === '--config' && args[i + 1]) {
|
|
105
|
+
options.configPath = args[i + 1];
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return options;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if file should be processed based on include/exclude patterns
|
|
115
|
+
* @param {string} filePath - relative file path
|
|
116
|
+
* @param {Config} config - configuration object
|
|
117
|
+
* @returns {boolean} - true if file should be processed
|
|
118
|
+
*/
|
|
119
|
+
function shouldProcessFile(filePath, config) {
|
|
120
|
+
const isIncluded = micromatch.isMatch(filePath, config.include);
|
|
121
|
+
const isExcluded = micromatch.isMatch(filePath, config.exclude);
|
|
122
|
+
|
|
123
|
+
return isIncluded && !isExcluded;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* traverse a specific directory recursively and convert all files' CRLF to LF
|
|
128
|
+
* @param {string} dirPath - directory path to search
|
|
129
|
+
* @param {Config} config - configuration object
|
|
130
|
+
* @returns {Promise<void>}
|
|
131
|
+
* @throws {Error} - if there's an error reading directory or processing files
|
|
132
|
+
*/
|
|
133
|
+
async function convertCRLFtoLF(dirPath, config) {
|
|
134
|
+
try {
|
|
135
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @todo Node.js is single-threaded, if I want to convert files in parallel, I need to use worker_threads
|
|
139
|
+
*/
|
|
140
|
+
await Promise.all(entries.map(async entry => {
|
|
141
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
142
|
+
const relativePath = path.relative(process.cwd(), fullPath).replace(/\\/g, "/");
|
|
143
|
+
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
await convertCRLFtoLF(fullPath, config);
|
|
146
|
+
} else if (entry.isFile() && shouldProcessFile(relativePath, config)) {
|
|
147
|
+
await processFile(fullPath);
|
|
148
|
+
} else {
|
|
149
|
+
logger.info(`skipped: ${relativePath}`, fullPath);
|
|
150
|
+
}
|
|
151
|
+
}));
|
|
152
|
+
} catch (err) {
|
|
153
|
+
logger.error(`error reading directory: ${dirPath}`, dirPath, err);
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* convert CRLF to LF
|
|
160
|
+
* @param {string} filePath - full path of the file to process
|
|
161
|
+
* @returns {Promise<void>}
|
|
162
|
+
* @throws {Error} - if there's an error reading or writing file
|
|
163
|
+
*/
|
|
164
|
+
async function processFile(filePath) {
|
|
165
|
+
try {
|
|
166
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
167
|
+
const updatedContent = content.replace(/\r\n/g, "\n");
|
|
168
|
+
|
|
169
|
+
if (content !== updatedContent) {
|
|
170
|
+
/**
|
|
171
|
+
* @todo V8 javascript engine with 32-bit system cannot handle more than 2GB file,
|
|
172
|
+
* so I should use createReadStream and createWriteStream to handle large files
|
|
173
|
+
*/
|
|
174
|
+
await fs.writeFile(filePath, updatedContent, "utf8");
|
|
175
|
+
logger.info(`converted: ${filePath}`);
|
|
176
|
+
} else {
|
|
177
|
+
logger.info(`no need to convert: ${filePath}`, filePath);
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
logger.error(`error processing file: ${filePath}`, filePath, err);
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function main() {
|
|
186
|
+
const options = parseArgs();
|
|
187
|
+
const config = await readConfig(options.configPath);
|
|
188
|
+
|
|
189
|
+
logger.info(`converting CRLF to LF in: ${config.entry}`, config.entry);
|
|
190
|
+
|
|
191
|
+
await convertCRLFtoLF(config.entry, config);
|
|
192
|
+
|
|
193
|
+
logger.info("conversion completed.", config.entry);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (require.main === module) {
|
|
197
|
+
main();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
convertCRLFtoLF,
|
|
202
|
+
processFile,
|
|
203
|
+
readConfig,
|
|
204
|
+
parseArgs,
|
|
205
|
+
};
|
package/index.test.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const { readConfig, parseArgs, processFile } = require('./index.cjs');
|
|
2
|
+
|
|
3
|
+
jest.mock('fs');
|
|
4
|
+
jest.mock('path');
|
|
5
|
+
jest.mock('micromatch');
|
|
6
|
+
|
|
7
|
+
describe('CRLF to LF Converter', () => {
|
|
8
|
+
const MOCK_FILE_INFO = {
|
|
9
|
+
'./src/file1.txt': 'hello\r\nworld\r\n',
|
|
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
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
require('fs').__setMockFiles(MOCK_FILE_INFO);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('readConfig', () => {
|
|
26
|
+
it('should return config when valid config file is provided', async () => {
|
|
27
|
+
// arrange
|
|
28
|
+
const validConfig = {
|
|
29
|
+
entry: './',
|
|
30
|
+
include: ['*.js'],
|
|
31
|
+
exclude: ['node_modules/**']
|
|
32
|
+
};
|
|
33
|
+
require('fs').__setConfig(JSON.stringify(validConfig));
|
|
34
|
+
|
|
35
|
+
// act
|
|
36
|
+
const config = await readConfig('.lfifyrc.json');
|
|
37
|
+
|
|
38
|
+
// assert
|
|
39
|
+
expect(config).toEqual(expect.objectContaining({
|
|
40
|
+
entry: expect.any(String),
|
|
41
|
+
include: expect.any(Array),
|
|
42
|
+
exclude: expect.any(Array)
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should throw error when config file is not found', async () => {
|
|
47
|
+
// act & assert
|
|
48
|
+
await expect(readConfig('.lfifyrc.json')).rejects.toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should throw error when config file is invalid json', async () => {
|
|
52
|
+
// arrange
|
|
53
|
+
require('fs').__setConfig('invalid json');
|
|
54
|
+
|
|
55
|
+
// act & assert
|
|
56
|
+
await expect(readConfig('.lfifyrc.json')).rejects.toThrow();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('parseArgs', () => {
|
|
61
|
+
it('should return config path when --config option is provided', () => {
|
|
62
|
+
// arrange
|
|
63
|
+
process.argv = ['node', 'lfify', '--config', './path/for/test/.lfifyrc.json'];
|
|
64
|
+
|
|
65
|
+
// act
|
|
66
|
+
const options = parseArgs();
|
|
67
|
+
|
|
68
|
+
// assert
|
|
69
|
+
expect(options.configPath).toBe('./path/for/test/.lfifyrc.json');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should return default config path when --config option is not provided', () => {
|
|
73
|
+
// arrange
|
|
74
|
+
process.argv = ['node', 'lfify'];
|
|
75
|
+
|
|
76
|
+
// act
|
|
77
|
+
const options = parseArgs();
|
|
78
|
+
|
|
79
|
+
// assert
|
|
80
|
+
expect(options.configPath).toBe('.lfifyrc.json');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('shouldProcessFile', () => {
|
|
85
|
+
it('should return true when file matches include pattern and does not match exclude pattern', () => {
|
|
86
|
+
/**
|
|
87
|
+
* This function uses micromatch to check config.include and config.exclude
|
|
88
|
+
* so this test case is already tested in micromatch's test file
|
|
89
|
+
* so I'm not going to test this function
|
|
90
|
+
*/
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('processFile', () => {
|
|
95
|
+
it('should convert CRLF to LF when file is processed', async () => {
|
|
96
|
+
// arrange
|
|
97
|
+
const shouldbe = 'hello\nworld\n';
|
|
98
|
+
|
|
99
|
+
// act
|
|
100
|
+
await processFile('./src/file1.txt');
|
|
101
|
+
const content = await require('fs').promises.readFile('./src/file1.txt', 'utf8');
|
|
102
|
+
|
|
103
|
+
// assert
|
|
104
|
+
expect(content).toBe(shouldbe);
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lfify",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "make your crlf to lf",
|
|
6
|
+
"main": "index.cjs",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"bin": {
|
|
9
|
+
"lfify": "./index.cjs"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"lint": "eslint ."
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/GyeongHoKim/lfify"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"registry": "https://registry.npmjs.org"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"eol",
|
|
24
|
+
"lf",
|
|
25
|
+
"crlf"
|
|
26
|
+
],
|
|
27
|
+
"author": "GyeongHoKim",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/GyeongHoKim/lfify/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/GyeongHoKim/lfify#readme",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"micromatch": "^4.0.8"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@eslint/js": "^9.15.0",
|
|
38
|
+
"eslint": "^9.15.0",
|
|
39
|
+
"globals": "^15.12.0",
|
|
40
|
+
"jest": "^29.7.0"
|
|
41
|
+
}
|
|
42
|
+
}
|