lfify 1.1.1 → 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 +4 -2
- package/.lfifyrc-sample.json +1 -0
- package/CHANGELOG.md +22 -0
- 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 +106 -35
- package/index.e2e.test.js +129 -0
- package/index.test.js +108 -99
- package/package.json +7 -3
- package/__mocks__/fs.js +0 -56
- package/__mocks__/micromatch.js +0 -39
package/.github/workflows/cd.yml
CHANGED
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
- name: Setup Node.js
|
|
23
23
|
uses: actions/setup-node@v6
|
|
24
24
|
with:
|
|
25
|
-
node-version: "
|
|
25
|
+
node-version: "24"
|
|
26
26
|
- name: Install dependencies
|
|
27
27
|
run: npm clean-install
|
|
28
28
|
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
|
|
@@ -30,4 +30,6 @@ jobs:
|
|
|
30
30
|
- name: Release
|
|
31
31
|
env:
|
|
32
32
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
33
|
-
run:
|
|
33
|
+
run: |
|
|
34
|
+
unset NODE_AUTH_TOKEN
|
|
35
|
+
npx semantic-release
|
package/.lfifyrc-sample.json
CHANGED
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))
|
|
@@ -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 - 정보 메시지 출력
|
|
@@ -24,6 +32,7 @@ const micromatch = require("micromatch");
|
|
|
24
32
|
* @property {string} [entry] - CLI로 지정한 entry 경로
|
|
25
33
|
* @property {string[]} [include] - CLI로 지정한 include 패턴
|
|
26
34
|
* @property {string[]} [exclude] - CLI로 지정한 exclude 패턴
|
|
35
|
+
* @property {'error'|'warn'|'info'} [logLevel] - CLI로 지정한 로그 레벨
|
|
27
36
|
*/
|
|
28
37
|
|
|
29
38
|
/**
|
|
@@ -33,7 +42,8 @@ const micromatch = require("micromatch");
|
|
|
33
42
|
const DEFAULT_CONFIG = {
|
|
34
43
|
entry: './',
|
|
35
44
|
include: [],
|
|
36
|
-
exclude: []
|
|
45
|
+
exclude: [],
|
|
46
|
+
logLevel: 'error'
|
|
37
47
|
};
|
|
38
48
|
|
|
39
49
|
/**
|
|
@@ -49,7 +59,8 @@ const SENSIBLE_DEFAULTS = {
|
|
|
49
59
|
'dist/**',
|
|
50
60
|
'build/**',
|
|
51
61
|
'coverage/**'
|
|
52
|
-
]
|
|
62
|
+
],
|
|
63
|
+
logLevel: 'error'
|
|
53
64
|
};
|
|
54
65
|
|
|
55
66
|
/**
|
|
@@ -60,16 +71,41 @@ const CONFIG_SCHEMA = {
|
|
|
60
71
|
entry: (value) => typeof value === 'string',
|
|
61
72
|
include: (value) => Array.isArray(value) && value.length > 0 && value.every(item => typeof item === 'string'),
|
|
62
73
|
exclude: (value) => Array.isArray(value) && value.length > 0 && value.every(item => typeof item === 'string'),
|
|
74
|
+
logLevel: (value) => LOG_LEVELS.includes(value),
|
|
63
75
|
};
|
|
64
76
|
|
|
65
77
|
/**
|
|
66
|
-
* Logging utility
|
|
67
|
-
* @type {Logger}
|
|
78
|
+
* Logging utility. 기본은 error만 출력. setLogLevel로 변경 가능.
|
|
79
|
+
* @type {Logger & { _level: string, setLogLevel: function(string): void }}
|
|
68
80
|
*/
|
|
69
81
|
const logger = {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
+
},
|
|
73
109
|
};
|
|
74
110
|
|
|
75
111
|
/**
|
|
@@ -80,7 +116,7 @@ const logger = {
|
|
|
80
116
|
*/
|
|
81
117
|
async function readConfig(configPath) {
|
|
82
118
|
try {
|
|
83
|
-
const configContent = await
|
|
119
|
+
const configContent = await readFile(configPath, 'utf8');
|
|
84
120
|
const config = JSON.parse(configContent);
|
|
85
121
|
|
|
86
122
|
// Validate required fields
|
|
@@ -93,7 +129,7 @@ async function readConfig(configPath) {
|
|
|
93
129
|
return {
|
|
94
130
|
...DEFAULT_CONFIG,
|
|
95
131
|
...config,
|
|
96
|
-
entry:
|
|
132
|
+
entry: resolve(process.cwd(), config.entry || DEFAULT_CONFIG.entry)
|
|
97
133
|
};
|
|
98
134
|
} catch (err) {
|
|
99
135
|
if (err.code === 'ENOENT') {
|
|
@@ -101,7 +137,7 @@ async function readConfig(configPath) {
|
|
|
101
137
|
} else {
|
|
102
138
|
logger.error(`Error reading configuration file: ${err.message}`, configPath);
|
|
103
139
|
}
|
|
104
|
-
|
|
140
|
+
|
|
105
141
|
if (require.main === module) {
|
|
106
142
|
process.exit(1);
|
|
107
143
|
}
|
|
@@ -120,7 +156,7 @@ async function resolveConfig(cliOptions) {
|
|
|
120
156
|
// Try to load config file if it exists
|
|
121
157
|
if (cliOptions.configPath) {
|
|
122
158
|
try {
|
|
123
|
-
const configContent = await
|
|
159
|
+
const configContent = await readFile(cliOptions.configPath, 'utf8');
|
|
124
160
|
fileConfig = JSON.parse(configContent);
|
|
125
161
|
|
|
126
162
|
// Validate config file fields
|
|
@@ -143,14 +179,16 @@ async function resolveConfig(cliOptions) {
|
|
|
143
179
|
const hasCLIInclude = Array.isArray(cliOptions.include) && cliOptions.include.length > 0;
|
|
144
180
|
const hasCLIExclude = Array.isArray(cliOptions.exclude) && cliOptions.exclude.length > 0;
|
|
145
181
|
const hasCLIEntry = typeof cliOptions.entry === 'string';
|
|
182
|
+
const hasCLILogLevel = typeof cliOptions.logLevel === 'string' && LOG_LEVELS.includes(cliOptions.logLevel);
|
|
146
183
|
|
|
147
184
|
const hasFileConfig = fileConfig !== null;
|
|
148
185
|
const hasFileInclude = hasFileConfig && Array.isArray(fileConfig.include) && fileConfig.include.length > 0;
|
|
149
186
|
const hasFileExclude = hasFileConfig && Array.isArray(fileConfig.exclude) && fileConfig.exclude.length > 0;
|
|
150
187
|
const hasFileEntry = hasFileConfig && typeof fileConfig.entry === 'string';
|
|
188
|
+
const hasFileLogLevel = hasFileConfig && fileConfig.logLevel && LOG_LEVELS.includes(fileConfig.logLevel);
|
|
151
189
|
|
|
152
190
|
// Resolve each config property
|
|
153
|
-
let include, exclude, entry;
|
|
191
|
+
let include, exclude, entry, logLevel;
|
|
154
192
|
|
|
155
193
|
// Include: CLI > file > default
|
|
156
194
|
if (hasCLIInclude) {
|
|
@@ -179,10 +217,20 @@ async function resolveConfig(cliOptions) {
|
|
|
179
217
|
entry = SENSIBLE_DEFAULTS.entry;
|
|
180
218
|
}
|
|
181
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
|
+
|
|
182
229
|
return {
|
|
183
|
-
entry:
|
|
230
|
+
entry: resolve(process.cwd(), entry),
|
|
184
231
|
include,
|
|
185
|
-
exclude
|
|
232
|
+
exclude,
|
|
233
|
+
logLevel
|
|
186
234
|
};
|
|
187
235
|
}
|
|
188
236
|
|
|
@@ -230,6 +278,13 @@ function parseArgs() {
|
|
|
230
278
|
i++;
|
|
231
279
|
}
|
|
232
280
|
break;
|
|
281
|
+
|
|
282
|
+
case '--log-level':
|
|
283
|
+
if (nextArg && LOG_LEVELS.includes(nextArg)) {
|
|
284
|
+
options.logLevel = nextArg;
|
|
285
|
+
i++;
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
233
288
|
}
|
|
234
289
|
}
|
|
235
290
|
|
|
@@ -243,8 +298,8 @@ function parseArgs() {
|
|
|
243
298
|
* @returns {boolean} - true if file should be processed
|
|
244
299
|
*/
|
|
245
300
|
function shouldProcessFile(filePath, config) {
|
|
246
|
-
const isIncluded =
|
|
247
|
-
const isExcluded =
|
|
301
|
+
const isIncluded = isMatch(filePath, config.include);
|
|
302
|
+
const isExcluded = isMatch(filePath, config.exclude);
|
|
248
303
|
|
|
249
304
|
return isIncluded && !isExcluded;
|
|
250
305
|
}
|
|
@@ -258,14 +313,14 @@ function shouldProcessFile(filePath, config) {
|
|
|
258
313
|
*/
|
|
259
314
|
async function convertCRLFtoLF(dirPath, config) {
|
|
260
315
|
try {
|
|
261
|
-
const entries = await
|
|
316
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
262
317
|
|
|
263
318
|
/**
|
|
264
319
|
* @todo Node.js is single-threaded, if I want to convert files in parallel, I need to use worker_threads
|
|
265
320
|
*/
|
|
266
321
|
await Promise.all(entries.map(async entry => {
|
|
267
|
-
const fullPath =
|
|
268
|
-
const relativePath =
|
|
322
|
+
const fullPath = join(dirPath, entry.name);
|
|
323
|
+
const relativePath = relative(process.cwd(), fullPath).replace(/\\/g, "/");
|
|
269
324
|
|
|
270
325
|
if (entry.isDirectory()) {
|
|
271
326
|
await convertCRLFtoLF(fullPath, config);
|
|
@@ -288,30 +343,46 @@ async function convertCRLFtoLF(dirPath, config) {
|
|
|
288
343
|
* @throws {Error} - if there's an error reading or writing file
|
|
289
344
|
*/
|
|
290
345
|
async function processFile(filePath) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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 ?? '');
|
|
304
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}`);
|
|
305
368
|
} catch (err) {
|
|
306
369
|
logger.error(`error processing file: ${filePath}`, filePath, err);
|
|
307
370
|
throw err;
|
|
308
371
|
}
|
|
372
|
+
try {
|
|
373
|
+
await rename(tmpPath, filePath);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
logger.error(`error rename file: ${tmpPath} to ${filePath}`);
|
|
376
|
+
throw err;
|
|
377
|
+
}
|
|
309
378
|
}
|
|
310
379
|
|
|
311
380
|
async function main() {
|
|
312
381
|
const options = parseArgs();
|
|
313
382
|
const config = await resolveConfig(options);
|
|
314
383
|
|
|
384
|
+
logger.setLogLevel(config.logLevel);
|
|
385
|
+
|
|
315
386
|
logger.info(`converting CRLF to LF in: ${config.entry}`, config.entry);
|
|
316
387
|
|
|
317
388
|
await convertCRLFtoLF(config.entry, config);
|
|
@@ -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,48 +1,46 @@
|
|
|
1
|
+
const mock = require('mock-fs');
|
|
1
2
|
const { readConfig, parseArgs, processFile, resolveConfig, shouldProcessFile, SENSIBLE_DEFAULTS } = require('./index.cjs');
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
|
|
16
|
-
'./node_modules/subdir/file4.txt': 'test\r\n',
|
|
17
|
-
'index.js': 'console.log("test");\r\n'
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
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
|
|
18
17
|
};
|
|
18
|
+
}
|
|
19
19
|
|
|
20
|
+
describe('CRLF to LF Converter', () => {
|
|
20
21
|
let originalArgv;
|
|
21
22
|
|
|
22
23
|
beforeEach(() => {
|
|
23
|
-
|
|
24
|
-
require('fs').__setMockFiles(MOCK_FILE_INFO);
|
|
24
|
+
mock(baseMock());
|
|
25
25
|
originalArgv = process.argv;
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
afterEach(() => {
|
|
29
|
+
mock.restore();
|
|
29
30
|
process.argv = originalArgv;
|
|
30
31
|
});
|
|
31
32
|
|
|
32
33
|
describe('readConfig', () => {
|
|
33
34
|
it('should return config when valid config file is provided', async () => {
|
|
34
|
-
// arrange
|
|
35
35
|
const validConfig = {
|
|
36
36
|
entry: './',
|
|
37
37
|
include: ['*.js'],
|
|
38
38
|
exclude: ['node_modules/**']
|
|
39
39
|
};
|
|
40
|
-
|
|
40
|
+
mock(baseMock({ '.lfifyrc.json': JSON.stringify(validConfig) }));
|
|
41
41
|
|
|
42
|
-
// act
|
|
43
42
|
const config = await readConfig('.lfifyrc.json');
|
|
44
43
|
|
|
45
|
-
// assert
|
|
46
44
|
expect(config).toEqual(expect.objectContaining({
|
|
47
45
|
entry: expect.any(String),
|
|
48
46
|
include: expect.any(Array),
|
|
@@ -51,39 +49,30 @@ describe('CRLF to LF Converter', () => {
|
|
|
51
49
|
});
|
|
52
50
|
|
|
53
51
|
it('should throw error when config file is not found', async () => {
|
|
54
|
-
// act & assert
|
|
55
52
|
await expect(readConfig('.lfifyrc.json')).rejects.toThrow();
|
|
56
53
|
});
|
|
57
54
|
|
|
58
55
|
it('should throw error when config file is invalid json', async () => {
|
|
59
|
-
|
|
60
|
-
require('fs').__setConfig('invalid json');
|
|
56
|
+
mock(baseMock({ '.lfifyrc.json': 'invalid json' }));
|
|
61
57
|
|
|
62
|
-
// act & assert
|
|
63
58
|
await expect(readConfig('.lfifyrc.json')).rejects.toThrow();
|
|
64
59
|
});
|
|
65
60
|
});
|
|
66
61
|
|
|
67
62
|
describe('parseArgs', () => {
|
|
68
63
|
it('should return config path when --config option is provided', () => {
|
|
69
|
-
// arrange
|
|
70
64
|
process.argv = ['node', 'lfify', '--config', './path/for/test/.lfifyrc.json'];
|
|
71
65
|
|
|
72
|
-
// act
|
|
73
66
|
const options = parseArgs();
|
|
74
67
|
|
|
75
|
-
// assert
|
|
76
68
|
expect(options.configPath).toBe('./path/for/test/.lfifyrc.json');
|
|
77
69
|
});
|
|
78
70
|
|
|
79
71
|
it('should return default config path when --config option is not provided', () => {
|
|
80
|
-
// arrange
|
|
81
72
|
process.argv = ['node', 'lfify'];
|
|
82
73
|
|
|
83
|
-
// act
|
|
84
74
|
const options = parseArgs();
|
|
85
75
|
|
|
86
|
-
// assert
|
|
87
76
|
expect(options.configPath).toBe('.lfifyrc.json');
|
|
88
77
|
});
|
|
89
78
|
|
|
@@ -106,33 +95,25 @@ describe('CRLF to LF Converter', () => {
|
|
|
106
95
|
});
|
|
107
96
|
|
|
108
97
|
it('should return multiple exclude patterns when multiple --exclude options are provided', () => {
|
|
109
|
-
process.argv = ['node', 'lfify', '--exclude', '
|
|
98
|
+
process.argv = ['node', 'lfify', '--exclude', 'dist/**', '--exclude', 'coverage/**'];
|
|
110
99
|
const options = parseArgs();
|
|
111
|
-
expect(options.exclude).toEqual(['
|
|
100
|
+
expect(options.exclude).toEqual(['dist/**', 'coverage/**']);
|
|
112
101
|
});
|
|
113
102
|
|
|
114
103
|
it('should return entry path when --entry option is provided', () => {
|
|
115
104
|
process.argv = ['node', 'lfify', '--entry', './src'];
|
|
116
105
|
const options = parseArgs();
|
|
117
|
-
expect(options.entry).
|
|
106
|
+
expect(options.entry).toContain('src');
|
|
118
107
|
});
|
|
119
108
|
|
|
120
109
|
it('should handle all options together', () => {
|
|
121
|
-
process.argv = [
|
|
122
|
-
'node', 'lfify',
|
|
123
|
-
'--entry', './src',
|
|
124
|
-
'--include', '**/*.js',
|
|
125
|
-
'--include', '**/*.ts',
|
|
126
|
-
'--exclude', 'node_modules/**',
|
|
127
|
-
'--config', 'custom.json'
|
|
128
|
-
];
|
|
110
|
+
process.argv = ['node', 'lfify', '--config', 'custom.json', '--include', '*.js', '--exclude', 'dist/**', '--entry', './lib', '--log-level', 'info'];
|
|
129
111
|
const options = parseArgs();
|
|
130
|
-
expect(options).
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
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');
|
|
136
117
|
});
|
|
137
118
|
|
|
138
119
|
it('should return undefined for include/exclude/entry when not provided', () => {
|
|
@@ -142,77 +123,68 @@ describe('CRLF to LF Converter', () => {
|
|
|
142
123
|
expect(options.exclude).toBeUndefined();
|
|
143
124
|
expect(options.entry).toBeUndefined();
|
|
144
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
|
+
});
|
|
145
139
|
});
|
|
146
140
|
|
|
147
141
|
describe('shouldProcessFile', () => {
|
|
148
142
|
it('should return true when file matches include pattern and does not match exclude pattern', () => {
|
|
149
|
-
const config = {
|
|
150
|
-
|
|
151
|
-
exclude: ['node_modules/**']
|
|
152
|
-
};
|
|
153
|
-
expect(shouldProcessFile('src/file.js', config)).toBe(true);
|
|
143
|
+
const config = { include: ['**/*.js'], exclude: ['node_modules/**'] };
|
|
144
|
+
expect(shouldProcessFile('src/app.js', config)).toBe(true);
|
|
154
145
|
});
|
|
155
146
|
|
|
156
147
|
it('should return false when file matches exclude pattern', () => {
|
|
157
|
-
const config = {
|
|
158
|
-
|
|
159
|
-
exclude: ['node_modules/**']
|
|
160
|
-
};
|
|
161
|
-
expect(shouldProcessFile('node_modules/package/index.js', config)).toBe(false);
|
|
148
|
+
const config = { include: ['**/*.js'], exclude: ['node_modules/**'] };
|
|
149
|
+
expect(shouldProcessFile('node_modules/pkg/index.js', config)).toBe(false);
|
|
162
150
|
});
|
|
163
151
|
|
|
164
152
|
it('should return false when file does not match include pattern', () => {
|
|
165
|
-
const config = {
|
|
166
|
-
|
|
167
|
-
exclude: ['node_modules/**']
|
|
168
|
-
};
|
|
169
|
-
expect(shouldProcessFile('src/file.txt', config)).toBe(false);
|
|
153
|
+
const config = { include: ['**/*.js'], exclude: ['node_modules/**'] };
|
|
154
|
+
expect(shouldProcessFile('src/readme.txt', config)).toBe(false);
|
|
170
155
|
});
|
|
171
156
|
|
|
172
157
|
it('should handle multiple include patterns', () => {
|
|
173
|
-
const config = {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
};
|
|
177
|
-
expect(shouldProcessFile('src/file.js', config)).toBe(true);
|
|
178
|
-
expect(shouldProcessFile('src/file.ts', config)).toBe(true);
|
|
179
|
-
expect(shouldProcessFile('src/file.txt', config)).toBe(false);
|
|
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);
|
|
180
161
|
});
|
|
181
162
|
|
|
182
163
|
it('should handle multiple exclude patterns', () => {
|
|
183
|
-
const config = {
|
|
184
|
-
include: ['**/*.js'],
|
|
185
|
-
exclude: ['node_modules/**', 'dist/**', 'test/**']
|
|
186
|
-
};
|
|
164
|
+
const config = { include: ['**/*.js'], exclude: ['node_modules/**', 'dist/**', 'build/**', 'coverage/**'] };
|
|
187
165
|
expect(shouldProcessFile('src/file.js', config)).toBe(true);
|
|
188
|
-
expect(shouldProcessFile('node_modules/pkg/
|
|
166
|
+
expect(shouldProcessFile('node_modules/pkg/file.js', config)).toBe(false);
|
|
189
167
|
expect(shouldProcessFile('dist/bundle.js', config)).toBe(false);
|
|
190
|
-
expect(shouldProcessFile('test/unit.js', config)).toBe(
|
|
168
|
+
expect(shouldProcessFile('test/unit.js', config)).toBe(true);
|
|
191
169
|
});
|
|
192
170
|
});
|
|
193
171
|
|
|
194
172
|
describe('processFile', () => {
|
|
195
173
|
it('should convert CRLF to LF when file is processed', async () => {
|
|
196
|
-
// arrange
|
|
197
174
|
const shouldbe = 'hello\nworld\n';
|
|
198
175
|
|
|
199
|
-
// act
|
|
200
176
|
await processFile('./src/file1.txt');
|
|
201
|
-
const content = await
|
|
177
|
+
const content = await fs.promises.readFile('./src/file1.txt', 'utf8');
|
|
202
178
|
|
|
203
|
-
// assert
|
|
204
179
|
expect(content).toBe(shouldbe);
|
|
205
180
|
});
|
|
206
181
|
|
|
207
182
|
it('should not modify file when no CRLF exists', async () => {
|
|
208
|
-
|
|
209
|
-
require('fs').__setMockFiles({ './src/clean.txt': 'hello\nworld\n' });
|
|
183
|
+
mock(baseMock({ 'src/clean.txt': 'hello\nworld\n' }));
|
|
210
184
|
|
|
211
|
-
// act
|
|
212
185
|
await processFile('./src/clean.txt');
|
|
213
|
-
const content = await
|
|
186
|
+
const content = await fs.promises.readFile('./src/clean.txt', 'utf8');
|
|
214
187
|
|
|
215
|
-
// assert
|
|
216
188
|
expect(content).toBe('hello\nworld\n');
|
|
217
189
|
});
|
|
218
190
|
});
|
|
@@ -239,27 +211,31 @@ describe('CRLF to LF Converter', () => {
|
|
|
239
211
|
});
|
|
240
212
|
|
|
241
213
|
it('should override config file values with CLI options', async () => {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
214
|
+
mock(baseMock({
|
|
215
|
+
'.lfifyrc.json': JSON.stringify({
|
|
216
|
+
entry: './',
|
|
217
|
+
include: ['**/*.md'],
|
|
218
|
+
exclude: ['dist/**']
|
|
219
|
+
})
|
|
246
220
|
}));
|
|
247
221
|
|
|
248
222
|
const options = {
|
|
249
223
|
configPath: '.lfifyrc.json',
|
|
250
|
-
include: ['**/*.js']
|
|
224
|
+
include: ['**/*.js']
|
|
251
225
|
};
|
|
252
226
|
const config = await resolveConfig(options);
|
|
253
227
|
|
|
254
|
-
expect(config.include).toEqual(['**/*.js']);
|
|
255
|
-
expect(config.exclude).toEqual(['dist/**']);
|
|
228
|
+
expect(config.include).toEqual(['**/*.js']);
|
|
229
|
+
expect(config.exclude).toEqual(['dist/**']);
|
|
256
230
|
});
|
|
257
231
|
|
|
258
232
|
it('should load config file when configPath is provided and file exists', async () => {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
233
|
+
mock(baseMock({
|
|
234
|
+
'.lfifyrc.json': JSON.stringify({
|
|
235
|
+
entry: './lib',
|
|
236
|
+
include: ['**/*.ts'],
|
|
237
|
+
exclude: ['test/**']
|
|
238
|
+
})
|
|
263
239
|
}));
|
|
264
240
|
|
|
265
241
|
const options = { configPath: '.lfifyrc.json' };
|
|
@@ -270,7 +246,6 @@ describe('CRLF to LF Converter', () => {
|
|
|
270
246
|
});
|
|
271
247
|
|
|
272
248
|
it('should use defaults when config file not found and no CLI options', async () => {
|
|
273
|
-
// No config file set up - mock will throw ENOENT
|
|
274
249
|
const options = { configPath: 'nonexistent.json' };
|
|
275
250
|
const config = await resolveConfig(options);
|
|
276
251
|
|
|
@@ -293,5 +268,39 @@ describe('CRLF to LF Converter', () => {
|
|
|
293
268
|
expect(config.include).toEqual(SENSIBLE_DEFAULTS.include);
|
|
294
269
|
expect(config.exclude).toEqual(['custom/**']);
|
|
295
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
|
+
});
|
|
296
305
|
});
|
|
297
|
-
});
|
|
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,7 +9,9 @@
|
|
|
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": {
|
|
@@ -17,7 +19,8 @@
|
|
|
17
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,
|
|
21
24
|
"tag": "latest"
|
|
22
25
|
},
|
|
23
26
|
"keywords": [
|
|
@@ -44,6 +47,7 @@
|
|
|
44
47
|
"eslint": "^9.15.0",
|
|
45
48
|
"globals": "^15.12.0",
|
|
46
49
|
"jest": "^29.7.0",
|
|
50
|
+
"mock-fs": "^5.5.0",
|
|
47
51
|
"semantic-release": "^25.0.2"
|
|
48
52
|
}
|
|
49
53
|
}
|
package/__mocks__/fs.js
DELETED
|
@@ -1,56 +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
|
-
const error = new Error(`ENOENT: no such file or directory, open '${path}'`);
|
|
23
|
-
error.code = 'ENOENT';
|
|
24
|
-
return Promise.reject(error);
|
|
25
|
-
}),
|
|
26
|
-
|
|
27
|
-
writeFile: jest.fn().mockImplementation((path, content) => {
|
|
28
|
-
mockFiles.set(path, content);
|
|
29
|
-
return Promise.resolve();
|
|
30
|
-
}),
|
|
31
|
-
|
|
32
|
-
/* eslint-disable-next-line no-unused-vars */
|
|
33
|
-
readdir: jest.fn().mockImplementation((path, ...rest) => {
|
|
34
|
-
const entries = [];
|
|
35
|
-
for (const filePath of mockFiles.keys()) {
|
|
36
|
-
if (filePath.startsWith(path)) {
|
|
37
|
-
const relativePath = filePath.slice(path.length + 1);
|
|
38
|
-
const name = relativePath.split('/')[0];
|
|
39
|
-
if (name && !entries.some(e => e.name === name)) {
|
|
40
|
-
entries.push({
|
|
41
|
-
name,
|
|
42
|
-
isFile: () => !name.includes('/'),
|
|
43
|
-
isDirectory: () => name.includes('/')
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return Promise.resolve(entries);
|
|
49
|
-
})
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
fs.promises = promises;
|
|
53
|
-
fs.__setMockFiles = __setMockFiles;
|
|
54
|
-
fs.__setConfig = __setConfig;
|
|
55
|
-
|
|
56
|
-
module.exports = fs;
|
package/__mocks__/micromatch.js
DELETED
|
@@ -1,39 +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
|
-
// glob 패턴을 정규식으로 변환
|
|
14
|
-
// 플레이스홀더를 사용해서 순서 문제 해결
|
|
15
|
-
const GLOBSTAR_SLASH = '___GLOBSTARSLASH___';
|
|
16
|
-
const GLOBSTAR = '___GLOBSTAR___';
|
|
17
|
-
|
|
18
|
-
let regexPattern = pattern
|
|
19
|
-
// **/ 패턴을 플레이스홀더로 임시 변환
|
|
20
|
-
.replace(/\*\*\//g, GLOBSTAR_SLASH)
|
|
21
|
-
// ** 패턴을 플레이스홀더로 임시 변환
|
|
22
|
-
.replace(/\*\*/g, GLOBSTAR)
|
|
23
|
-
// {a,b} 패턴
|
|
24
|
-
.replace(/\{([^}]+)\}/g, (_, group) => `(${group.split(',').join('|')})`)
|
|
25
|
-
// 특수문자 이스케이프 (*, ?, / 제외)
|
|
26
|
-
.replace(/[.+^$|()[\]\\]/g, '\\$&')
|
|
27
|
-
// * 패턴: 슬래시를 제외한 모든 것 매칭
|
|
28
|
-
.replace(/\*/g, '[^/]*')
|
|
29
|
-
// ? 패턴: 슬래시를 제외한 한 문자 매칭
|
|
30
|
-
.replace(/\?/g, '[^/]')
|
|
31
|
-
// 플레이스홀더를 실제 정규식으로 변환
|
|
32
|
-
.replace(new RegExp(GLOBSTAR_SLASH, 'g'), '(?:.*/)?')
|
|
33
|
-
.replace(new RegExp(GLOBSTAR, 'g'), '.*');
|
|
34
|
-
|
|
35
|
-
return new RegExp(`^${regexPattern}$`).test(filePath);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
module.exports = micromatch;
|