lfify 1.1.1 → 1.2.1
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/.github/workflows/ci.yml +26 -26
- package/.lfifyrc-sample.json +2 -1
- package/.prettierignore +5 -0
- package/.prettierrc +9 -0
- package/.vscode/extensions.json +3 -0
- package/.vscode/launch.json +2 -4
- package/.vscode/settings.json +16 -0
- package/CHANGELOG.md +29 -0
- package/README.md +8 -6
- 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/__mocks__/path.js +1 -1
- package/eslint.config.mjs +7 -5
- package/index.cjs +157 -61
- package/index.e2e.test.js +140 -0
- package/index.test.js +169 -103
- package/package.json +13 -4
- package/__mocks__/fs.js +0 -56
- package/__mocks__/micromatch.js +0 -39
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, unlink } = 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
|
/**
|
|
@@ -43,13 +53,8 @@ const DEFAULT_CONFIG = {
|
|
|
43
53
|
const SENSIBLE_DEFAULTS = {
|
|
44
54
|
entry: './',
|
|
45
55
|
include: ['**/*'],
|
|
46
|
-
exclude: [
|
|
47
|
-
|
|
48
|
-
'.git/**',
|
|
49
|
-
'dist/**',
|
|
50
|
-
'build/**',
|
|
51
|
-
'coverage/**'
|
|
52
|
-
]
|
|
56
|
+
exclude: ['node_modules/**', '.git/**', 'dist/**', 'build/**', 'coverage/**'],
|
|
57
|
+
logLevel: 'error',
|
|
53
58
|
};
|
|
54
59
|
|
|
55
60
|
/**
|
|
@@ -58,18 +63,49 @@ const SENSIBLE_DEFAULTS = {
|
|
|
58
63
|
*/
|
|
59
64
|
const CONFIG_SCHEMA = {
|
|
60
65
|
entry: (value) => typeof value === 'string',
|
|
61
|
-
include: (value) =>
|
|
62
|
-
|
|
66
|
+
include: (value) =>
|
|
67
|
+
Array.isArray(value) &&
|
|
68
|
+
value.length > 0 &&
|
|
69
|
+
value.every((item) => typeof item === 'string'),
|
|
70
|
+
exclude: (value) =>
|
|
71
|
+
Array.isArray(value) &&
|
|
72
|
+
value.length > 0 &&
|
|
73
|
+
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,15 +129,18 @@ 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') {
|
|
100
136
|
logger.error(`Configuration file not found: ${configPath}`, configPath);
|
|
101
137
|
} else {
|
|
102
|
-
logger.error(
|
|
138
|
+
logger.error(
|
|
139
|
+
`Error reading configuration file: ${err.message}`,
|
|
140
|
+
configPath,
|
|
141
|
+
);
|
|
103
142
|
}
|
|
104
|
-
|
|
143
|
+
|
|
105
144
|
if (require.main === module) {
|
|
106
145
|
process.exit(1);
|
|
107
146
|
}
|
|
@@ -120,7 +159,7 @@ async function resolveConfig(cliOptions) {
|
|
|
120
159
|
// Try to load config file if it exists
|
|
121
160
|
if (cliOptions.configPath) {
|
|
122
161
|
try {
|
|
123
|
-
const configContent = await
|
|
162
|
+
const configContent = await readFile(cliOptions.configPath, 'utf8');
|
|
124
163
|
fileConfig = JSON.parse(configContent);
|
|
125
164
|
|
|
126
165
|
// Validate config file fields
|
|
@@ -132,7 +171,10 @@ async function resolveConfig(cliOptions) {
|
|
|
132
171
|
} catch (err) {
|
|
133
172
|
if (err.code !== 'ENOENT') {
|
|
134
173
|
// Re-throw parsing/validation errors
|
|
135
|
-
logger.error(
|
|
174
|
+
logger.error(
|
|
175
|
+
`Error reading configuration file: ${err.message}`,
|
|
176
|
+
cliOptions.configPath,
|
|
177
|
+
);
|
|
136
178
|
throw err;
|
|
137
179
|
}
|
|
138
180
|
// ENOENT is okay - config file is optional now
|
|
@@ -140,17 +182,32 @@ async function resolveConfig(cliOptions) {
|
|
|
140
182
|
}
|
|
141
183
|
|
|
142
184
|
// Determine final values with precedence: CLI > config file > defaults
|
|
143
|
-
const hasCLIInclude =
|
|
144
|
-
|
|
185
|
+
const hasCLIInclude =
|
|
186
|
+
Array.isArray(cliOptions.include) && cliOptions.include.length > 0;
|
|
187
|
+
const hasCLIExclude =
|
|
188
|
+
Array.isArray(cliOptions.exclude) && cliOptions.exclude.length > 0;
|
|
145
189
|
const hasCLIEntry = typeof cliOptions.entry === 'string';
|
|
190
|
+
const hasCLILogLevel =
|
|
191
|
+
typeof cliOptions.logLevel === 'string' &&
|
|
192
|
+
LOG_LEVELS.includes(cliOptions.logLevel);
|
|
146
193
|
|
|
147
194
|
const hasFileConfig = fileConfig !== null;
|
|
148
|
-
const hasFileInclude =
|
|
149
|
-
|
|
195
|
+
const hasFileInclude =
|
|
196
|
+
hasFileConfig &&
|
|
197
|
+
Array.isArray(fileConfig.include) &&
|
|
198
|
+
fileConfig.include.length > 0;
|
|
199
|
+
const hasFileExclude =
|
|
200
|
+
hasFileConfig &&
|
|
201
|
+
Array.isArray(fileConfig.exclude) &&
|
|
202
|
+
fileConfig.exclude.length > 0;
|
|
150
203
|
const hasFileEntry = hasFileConfig && typeof fileConfig.entry === 'string';
|
|
204
|
+
const hasFileLogLevel =
|
|
205
|
+
hasFileConfig &&
|
|
206
|
+
fileConfig.logLevel &&
|
|
207
|
+
LOG_LEVELS.includes(fileConfig.logLevel);
|
|
151
208
|
|
|
152
209
|
// Resolve each config property
|
|
153
|
-
let include, exclude, entry;
|
|
210
|
+
let include, exclude, entry, logLevel;
|
|
154
211
|
|
|
155
212
|
// Include: CLI > file > default
|
|
156
213
|
if (hasCLIInclude) {
|
|
@@ -179,10 +236,20 @@ async function resolveConfig(cliOptions) {
|
|
|
179
236
|
entry = SENSIBLE_DEFAULTS.entry;
|
|
180
237
|
}
|
|
181
238
|
|
|
239
|
+
// LogLevel: CLI > file > default
|
|
240
|
+
if (hasCLILogLevel) {
|
|
241
|
+
logLevel = cliOptions.logLevel;
|
|
242
|
+
} else if (hasFileLogLevel) {
|
|
243
|
+
logLevel = fileConfig.logLevel;
|
|
244
|
+
} else {
|
|
245
|
+
logLevel = SENSIBLE_DEFAULTS.logLevel;
|
|
246
|
+
}
|
|
247
|
+
|
|
182
248
|
return {
|
|
183
|
-
entry:
|
|
249
|
+
entry: resolve(process.cwd(), entry),
|
|
184
250
|
include,
|
|
185
|
-
exclude
|
|
251
|
+
exclude,
|
|
252
|
+
logLevel,
|
|
186
253
|
};
|
|
187
254
|
}
|
|
188
255
|
|
|
@@ -193,7 +260,7 @@ async function resolveConfig(cliOptions) {
|
|
|
193
260
|
function parseArgs() {
|
|
194
261
|
const args = process.argv.slice(2);
|
|
195
262
|
const options = {
|
|
196
|
-
configPath: '.lfifyrc.json'
|
|
263
|
+
configPath: '.lfifyrc.json',
|
|
197
264
|
};
|
|
198
265
|
|
|
199
266
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -230,6 +297,13 @@ function parseArgs() {
|
|
|
230
297
|
i++;
|
|
231
298
|
}
|
|
232
299
|
break;
|
|
300
|
+
|
|
301
|
+
case '--log-level':
|
|
302
|
+
if (nextArg && LOG_LEVELS.includes(nextArg)) {
|
|
303
|
+
options.logLevel = nextArg;
|
|
304
|
+
i++;
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
233
307
|
}
|
|
234
308
|
}
|
|
235
309
|
|
|
@@ -243,8 +317,8 @@ function parseArgs() {
|
|
|
243
317
|
* @returns {boolean} - true if file should be processed
|
|
244
318
|
*/
|
|
245
319
|
function shouldProcessFile(filePath, config) {
|
|
246
|
-
const isIncluded =
|
|
247
|
-
const isExcluded =
|
|
320
|
+
const isIncluded = isMatch(filePath, config.include);
|
|
321
|
+
const isExcluded = isMatch(filePath, config.exclude);
|
|
248
322
|
|
|
249
323
|
return isIncluded && !isExcluded;
|
|
250
324
|
}
|
|
@@ -258,23 +332,28 @@ function shouldProcessFile(filePath, config) {
|
|
|
258
332
|
*/
|
|
259
333
|
async function convertCRLFtoLF(dirPath, config) {
|
|
260
334
|
try {
|
|
261
|
-
const entries = await
|
|
335
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
262
336
|
|
|
263
337
|
/**
|
|
264
338
|
* @todo Node.js is single-threaded, if I want to convert files in parallel, I need to use worker_threads
|
|
265
339
|
*/
|
|
266
|
-
await Promise.all(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
340
|
+
await Promise.all(
|
|
341
|
+
entries.map(async (entry) => {
|
|
342
|
+
const fullPath = join(dirPath, entry.name);
|
|
343
|
+
const relativePath = relative(process.cwd(), fullPath).replace(
|
|
344
|
+
/\\/g,
|
|
345
|
+
'/',
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (entry.isDirectory()) {
|
|
349
|
+
await convertCRLFtoLF(fullPath, config);
|
|
350
|
+
} else if (entry.isFile() && shouldProcessFile(relativePath, config)) {
|
|
351
|
+
await processFile(fullPath);
|
|
352
|
+
} else {
|
|
353
|
+
logger.info(`skipped: ${relativePath}`, fullPath);
|
|
354
|
+
}
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
278
357
|
} catch (err) {
|
|
279
358
|
logger.error(`error reading directory: ${dirPath}`, dirPath, err);
|
|
280
359
|
throw err;
|
|
@@ -288,35 +367,52 @@ async function convertCRLFtoLF(dirPath, config) {
|
|
|
288
367
|
* @throws {Error} - if there's an error reading or writing file
|
|
289
368
|
*/
|
|
290
369
|
async function processFile(filePath) {
|
|
370
|
+
const tmpPath = `${filePath}.tmp`;
|
|
371
|
+
const crlf2lf = new Transform({
|
|
372
|
+
transform(chunk, encoding, callback) {
|
|
373
|
+
const enc = encoding === 'buffer' ? 'utf8' : encoding;
|
|
374
|
+
const prev = this._leftover ?? '';
|
|
375
|
+
this._leftover = '';
|
|
376
|
+
const str = prev + chunk.toString(enc);
|
|
377
|
+
const safe = str.endsWith('\r') ? str.slice(0, -1) : str;
|
|
378
|
+
this._leftover = str.endsWith('\r') ? '\r' : '';
|
|
379
|
+
callback(null, safe.replace(/\r\n/g, '\n'));
|
|
380
|
+
},
|
|
381
|
+
flush(callback) {
|
|
382
|
+
callback(null, this._leftover ?? '');
|
|
383
|
+
},
|
|
384
|
+
});
|
|
291
385
|
try {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
* so I should use createReadStream and createWriteStream to handle large files
|
|
299
|
-
*/
|
|
300
|
-
await fs.writeFile(filePath, updatedContent, "utf8");
|
|
301
|
-
logger.info(`converted: ${filePath}`);
|
|
302
|
-
} else {
|
|
303
|
-
logger.info(`no need to convert: ${filePath}`, filePath);
|
|
304
|
-
}
|
|
386
|
+
await pipeline(
|
|
387
|
+
createReadStream(filePath, { encoding: 'utf8' }),
|
|
388
|
+
crlf2lf,
|
|
389
|
+
createWriteStream(tmpPath, { encoding: 'utf8' }),
|
|
390
|
+
);
|
|
391
|
+
logger.info(`converted ${filePath}`);
|
|
305
392
|
} catch (err) {
|
|
306
393
|
logger.error(`error processing file: ${filePath}`, filePath, err);
|
|
307
394
|
throw err;
|
|
308
395
|
}
|
|
396
|
+
try {
|
|
397
|
+
await rename(tmpPath, filePath);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
logger.error(`error rename file: ${tmpPath} to ${filePath}`);
|
|
400
|
+
unlink(tmpPath);
|
|
401
|
+
throw err;
|
|
402
|
+
}
|
|
309
403
|
}
|
|
310
404
|
|
|
311
405
|
async function main() {
|
|
312
406
|
const options = parseArgs();
|
|
313
407
|
const config = await resolveConfig(options);
|
|
314
408
|
|
|
409
|
+
logger.setLogLevel(config.logLevel);
|
|
410
|
+
|
|
315
411
|
logger.info(`converting CRLF to LF in: ${config.entry}`, config.entry);
|
|
316
412
|
|
|
317
413
|
await convertCRLFtoLF(config.entry, config);
|
|
318
414
|
|
|
319
|
-
logger.info(
|
|
415
|
+
logger.info('conversion completed.', config.entry);
|
|
320
416
|
}
|
|
321
417
|
|
|
322
418
|
if (require.main === module) {
|
|
@@ -0,0 +1,140 @@
|
|
|
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(
|
|
53
|
+
path.join(tempDir, '_node_modules'),
|
|
54
|
+
path.join(tempDir, 'node_modules'),
|
|
55
|
+
);
|
|
56
|
+
process.chdir(tempDir);
|
|
57
|
+
|
|
58
|
+
const config = await resolveConfig({});
|
|
59
|
+
await convertCRLFtoLF(config.entry, config);
|
|
60
|
+
|
|
61
|
+
const appJs = await fs.readFile(path.join(tempDir, 'src', 'app.js'));
|
|
62
|
+
expect(appJs.includes(CRLF)).toBe(false);
|
|
63
|
+
expect(appJs.equals(Buffer.from('console.log("app");\n', 'utf8'))).toBe(
|
|
64
|
+
true,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const readmeTxt = await fs.readFile(
|
|
68
|
+
path.join(tempDir, 'src', 'readme.txt'),
|
|
69
|
+
);
|
|
70
|
+
expect(readmeTxt.includes(CRLF)).toBe(false);
|
|
71
|
+
expect(readmeTxt.equals(Buffer.from('hello\nworld\n', 'utf8'))).toBe(
|
|
72
|
+
true,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const nodeModulesPkg = await fs.readFile(
|
|
76
|
+
path.join(tempDir, 'node_modules', 'pkg', 'index.js'),
|
|
77
|
+
);
|
|
78
|
+
expect(nodeModulesPkg.includes(CRLF)).toBe(true);
|
|
79
|
+
|
|
80
|
+
const gitConfig = await fs.readFile(path.join(tempDir, '.git', 'config'));
|
|
81
|
+
expect(gitConfig.includes(CRLF)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('US2: .lfifyrc.json', () => {
|
|
86
|
+
it('applies config include/exclude so only matching files are converted', async () => {
|
|
87
|
+
await copyDir(path.join(FIXTURES_DIR, 'with-config'), tempDir);
|
|
88
|
+
process.chdir(tempDir);
|
|
89
|
+
|
|
90
|
+
const config = await resolveConfig({ configPath: '.lfifyrc.json' });
|
|
91
|
+
await convertCRLFtoLF(config.entry, config);
|
|
92
|
+
|
|
93
|
+
const mainJs = await fs.readFile(path.join(tempDir, 'lib', 'main.js'));
|
|
94
|
+
expect(mainJs.includes(CRLF)).toBe(false);
|
|
95
|
+
expect(mainJs.equals(Buffer.from('const x = 1;\n', 'utf8'))).toBe(true);
|
|
96
|
+
|
|
97
|
+
const skipOtherJs = await fs.readFile(
|
|
98
|
+
path.join(tempDir, 'skip', 'other.js'),
|
|
99
|
+
);
|
|
100
|
+
expect(skipOtherJs.includes(CRLF)).toBe(true);
|
|
101
|
+
|
|
102
|
+
const docTxt = await fs.readFile(path.join(tempDir, 'doc.txt'));
|
|
103
|
+
expect(docTxt.includes(CRLF)).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('US3: CLI overrides config', () => {
|
|
108
|
+
it('CLI include overrides config so only .js files are converted', async () => {
|
|
109
|
+
await copyDir(path.join(FIXTURES_DIR, 'cli-override'), tempDir);
|
|
110
|
+
process.chdir(tempDir);
|
|
111
|
+
|
|
112
|
+
const config = await resolveConfig({
|
|
113
|
+
configPath: '.lfifyrc.json',
|
|
114
|
+
include: ['**/*.js'],
|
|
115
|
+
});
|
|
116
|
+
await convertCRLFtoLF(config.entry, config);
|
|
117
|
+
|
|
118
|
+
const aJs = await fs.readFile(path.join(tempDir, 'src', 'a.js'));
|
|
119
|
+
expect(aJs.includes(CRLF)).toBe(false);
|
|
120
|
+
expect(aJs.equals(Buffer.from('const a = 1;\n', 'utf8'))).toBe(true);
|
|
121
|
+
|
|
122
|
+
const bTxt = await fs.readFile(path.join(tempDir, 'src', 'b.txt'));
|
|
123
|
+
expect(bTxt.includes(CRLF)).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('US4: already LF', () => {
|
|
128
|
+
it('does not modify files that already use LF', async () => {
|
|
129
|
+
await copyDir(path.join(FIXTURES_DIR, 'already-lf'), tempDir);
|
|
130
|
+
process.chdir(tempDir);
|
|
131
|
+
|
|
132
|
+
const config = await resolveConfig({});
|
|
133
|
+
await convertCRLFtoLF(config.entry, config);
|
|
134
|
+
|
|
135
|
+
const fileJs = await fs.readFile(path.join(tempDir, 'src', 'file.js'));
|
|
136
|
+
expect(fileJs.includes(CRLF)).toBe(false);
|
|
137
|
+
expect(fileJs.equals(Buffer.from('const x = 1;\n', 'utf8'))).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|