lfify 1.1.0 → 1.2.0

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