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.
@@ -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,16 @@
1
+ {
2
+ "entry": "./",
3
+ "include": [
4
+ "**/*.{js,ts,jsx,tsx}",
5
+ "**/*.{json,md}",
6
+ "**/*.{css,scss}",
7
+ "**/*.{html,vue}"
8
+ ],
9
+ "exclude": [
10
+ "node_modules/**",
11
+ ".git/**",
12
+ "dist/**",
13
+ "build/**",
14
+ "coverage/**"
15
+ ]
16
+ }
@@ -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.
@@ -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
+ }