qleaner 1.0.22 → 1.0.24
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/command.js +15 -1
- package/controllers/image.js +248 -137
- package/controllers/list.js +1 -1
- package/package.json +4 -2
- package/utils/cssImages.js +4 -0
- package/utils/resolver.js +5 -1
package/command.js
CHANGED
|
@@ -28,7 +28,12 @@ async function getFiles(directory = "src", options, chalk) {
|
|
|
28
28
|
|
|
29
29
|
const files = await fg(contentPaths);
|
|
30
30
|
const imports = [];
|
|
31
|
+
|
|
32
|
+
let index = 0;
|
|
31
33
|
for (const file of files) {
|
|
34
|
+
index++;
|
|
35
|
+
console.clear();
|
|
36
|
+
console.log('Scanning file...', index, 'of', files.length);
|
|
32
37
|
const code = fs.readFileSync(file, "utf8");
|
|
33
38
|
const ast = parser.parse(code, {
|
|
34
39
|
sourceType: "module",
|
|
@@ -127,8 +132,11 @@ async function unUsedFiles(chalk, directory = "src", options) {
|
|
|
127
132
|
|
|
128
133
|
// debug log
|
|
129
134
|
// let debugCount = 0;
|
|
130
|
-
|
|
135
|
+
let index = 0;
|
|
131
136
|
for (const file of files) {
|
|
137
|
+
index++;
|
|
138
|
+
console.clear();
|
|
139
|
+
console.log('Checking file...', index, 'of', files.length);
|
|
132
140
|
const code = fs.readFileSync(file, "utf8");
|
|
133
141
|
if (needsRebuild(file, code, cache)) {
|
|
134
142
|
const ast = parser.parse(code, {
|
|
@@ -169,8 +177,13 @@ async function unUsedFiles(chalk, directory = "src", options) {
|
|
|
169
177
|
imports = cache[directory]? cache[directory].imports: [];
|
|
170
178
|
}
|
|
171
179
|
|
|
180
|
+
|
|
172
181
|
// debugCount = 0;
|
|
182
|
+
index = 0;
|
|
173
183
|
for (const file of files) {
|
|
184
|
+
index++;
|
|
185
|
+
console.clear();
|
|
186
|
+
console.log('Checking file...', index, 'of', files.length);
|
|
174
187
|
const code = fs.readFileSync(file, "utf8");
|
|
175
188
|
if (!cache[file].isImported || needsRebuild(file, code, cache)) {
|
|
176
189
|
let i = 0;
|
|
@@ -201,6 +214,7 @@ async function unUsedFiles(chalk, directory = "src", options) {
|
|
|
201
214
|
// console.log("debug hit", debugCount);
|
|
202
215
|
}
|
|
203
216
|
}
|
|
217
|
+
|
|
204
218
|
cache[directory] = {
|
|
205
219
|
imports: imports,
|
|
206
220
|
}
|
package/controllers/image.js
CHANGED
|
@@ -1,190 +1,301 @@
|
|
|
1
1
|
const fg = require("fast-glob");
|
|
2
2
|
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
3
4
|
const parser = require("@babel/parser");
|
|
4
5
|
const traverse = require("@babel/traverse").default;
|
|
6
|
+
const Table = require("cli-table3");
|
|
5
7
|
const { getCssImages } = require("../utils/cssImages");
|
|
6
|
-
const
|
|
7
|
-
const { createResolver } = require("../utils/resolver");
|
|
8
|
-
const { compareFiles, isExcludedFile, askDeleteFiles } = require("../utils/utils");
|
|
9
|
-
const Table = require('cli-table3');
|
|
8
|
+
const { askDeleteFiles } = require("../utils/utils");
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const unusedImages = [];
|
|
14
|
-
const imageRegex = /\.(png|jpg|jpeg|svg|gif|webp)$/i;
|
|
15
|
-
const contentPaths = [`${imageDirectory}/**/*.{png,jpg,jpeg,svg,gif,webp}`];
|
|
16
|
-
if (options.excludeDirAssets && options.excludeDirAssets.length > 0) {
|
|
17
|
-
options.excludeDirAssets.forEach((dir) => {
|
|
18
|
-
contentPaths.push(`!${dir}/**`);
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
if (options.excludeFileAsset && options.excludeFileAsset.length > 0) {
|
|
22
|
-
options.excludeFileAsset.forEach((file) => {
|
|
23
|
-
contentPaths.push(`!${imageDirectory}/**/${file}`);
|
|
24
|
-
});
|
|
25
|
-
}
|
|
10
|
+
// Regex for matching url("x.png") or url('x.png') or bg-[url('x.png')]
|
|
11
|
+
const URL_EXTRACT_REGEX = /url\((['"]?)([^"')]+)\1\)/gi;
|
|
26
12
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
13
|
+
// Normal image file regex
|
|
14
|
+
const IMAGE_REGEX = /\.(png|jpe?g|svg|gif|webp)$/i;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Normalize image paths for consistent matching.
|
|
18
|
+
*/
|
|
19
|
+
function normalize(value, imageDirectory) {
|
|
20
|
+
if (!value) return null;
|
|
21
|
+
|
|
22
|
+
// Extract url() paths
|
|
23
|
+
const match = [...value.matchAll(URL_EXTRACT_REGEX)];
|
|
24
|
+
if (match.length > 0) {
|
|
25
|
+
value = match[0][2];
|
|
32
26
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
|
|
28
|
+
// Remove query params
|
|
29
|
+
value = value.split("?")[0];
|
|
30
|
+
|
|
31
|
+
// Convert leading "/" to relative project path
|
|
32
|
+
if (value.startsWith("/")) {
|
|
33
|
+
return path.resolve(path.join(imageDirectory, value.slice(1)));
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
// Resolve relative paths
|
|
37
|
+
return path.resolve(path.join(imageDirectory, value));
|
|
38
|
+
}
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Extracts all strings in a tagged template literal (styled-components, css``)
|
|
42
|
+
*/
|
|
43
|
+
function extractFromTemplateLiteral(quasis) {
|
|
44
|
+
const results = [];
|
|
45
|
+
quasis.forEach((q) => {
|
|
46
|
+
const matches = [...q.value.raw.matchAll(URL_EXTRACT_REGEX)];
|
|
47
|
+
for (const m of matches) {
|
|
48
|
+
results.push(m[2]);
|
|
49
|
+
}
|
|
50
|
+
if (IMAGE_REGEX.test(q.value.raw)) {
|
|
51
|
+
results.push(q.value.raw);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return results;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract images from JSX style object: style={{ backgroundImage: "url('/img/a.png')" }}
|
|
59
|
+
*/
|
|
60
|
+
function extractFromJSXStyle(node, file, collected) {
|
|
61
|
+
if (!node || node.type !== "JSXExpressionContainer") return;
|
|
62
|
+
|
|
63
|
+
const expr = node.expression;
|
|
64
|
+
if (!expr || expr.type !== "ObjectExpression") return;
|
|
65
|
+
|
|
66
|
+
expr.properties.forEach((prop) => {
|
|
67
|
+
if (
|
|
68
|
+
prop.type !== "ObjectProperty" ||
|
|
69
|
+
!prop.key ||
|
|
70
|
+
!prop.value
|
|
71
|
+
) return;
|
|
72
|
+
|
|
73
|
+
const keyName = prop.key.name || prop.key.value;
|
|
74
|
+
|
|
75
|
+
if (!keyName) return;
|
|
76
|
+
|
|
77
|
+
const isImageField =
|
|
78
|
+
keyName.toLowerCase().includes("background") ||
|
|
79
|
+
keyName.toLowerCase().includes("image") ||
|
|
80
|
+
keyName.toLowerCase().includes("mask");
|
|
81
|
+
|
|
82
|
+
if (!isImageField) return;
|
|
83
|
+
|
|
84
|
+
// String literal
|
|
85
|
+
if (prop.value.type === "StringLiteral") {
|
|
86
|
+
const raw = prop.value.value;
|
|
87
|
+
const matches = [...raw.matchAll(URL_EXTRACT_REGEX)];
|
|
88
|
+
if (matches.length > 0) {
|
|
89
|
+
matches.forEach((m) =>
|
|
90
|
+
collected.add(JSON.stringify({ path: m[2], file }))
|
|
91
|
+
);
|
|
92
|
+
} else if (IMAGE_REGEX.test(raw)) {
|
|
93
|
+
collected.add(JSON.stringify({ path: raw, file }));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Template literal
|
|
98
|
+
if (prop.value.type === "TemplateLiteral") {
|
|
99
|
+
extractFromTemplateLiteral(prop.value.quasis).forEach((v) => {
|
|
100
|
+
collected.add(JSON.stringify({ path: v, file }));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* MAIN FUNCTION
|
|
108
|
+
*/
|
|
109
|
+
async function getUnusedImages(chalk, imageDirectory, codeDirectory, options) {
|
|
110
|
+
const used = new Set();
|
|
111
|
+
const unusedImages = [];
|
|
43
112
|
|
|
113
|
+
// ---- Collect image files in asset directory ----
|
|
114
|
+
const imageFiles = await fg([`${imageDirectory}/**/*.{png,jpg,jpeg,svg,gif,webp}`]);
|
|
115
|
+
|
|
116
|
+
// ---- Scan Code Files ----
|
|
117
|
+
const codeFiles = await fg([`${codeDirectory}/**/*.{js,jsx,ts,tsx}`]);
|
|
118
|
+
let index = 0;
|
|
119
|
+
for (const file of codeFiles) {
|
|
120
|
+
index++;
|
|
121
|
+
console.clear();
|
|
122
|
+
console.log('Scanning code files...', index, 'of', codeFiles.length);
|
|
44
123
|
const code = fs.readFileSync(file, "utf8");
|
|
124
|
+
|
|
45
125
|
const ast = parser.parse(code, {
|
|
46
126
|
sourceType: "module",
|
|
47
127
|
plugins: ["jsx", "typescript"],
|
|
48
128
|
});
|
|
49
129
|
|
|
50
130
|
traverse(ast, {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
131
|
+
/**
|
|
132
|
+
* import logo from './img/a.png'
|
|
133
|
+
*/
|
|
134
|
+
ImportDeclaration(pathNode) {
|
|
135
|
+
const val = pathNode.node.source.value;
|
|
136
|
+
if (IMAGE_REGEX.test(val)) {
|
|
137
|
+
used.add(JSON.stringify({ path: val, file }));
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* require('./img/a.png')
|
|
143
|
+
*/
|
|
144
|
+
CallExpression(pathNode) {
|
|
145
|
+
const callee = pathNode.node.callee;
|
|
146
|
+
const args = pathNode.node.arguments;
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
callee.type === "Identifier" &&
|
|
150
|
+
callee.name === "require" &&
|
|
151
|
+
args.length &&
|
|
152
|
+
args[0].type === "StringLiteral"
|
|
153
|
+
) {
|
|
154
|
+
const val = args[0].value;
|
|
155
|
+
if (IMAGE_REGEX.test(val)) {
|
|
156
|
+
used.add(JSON.stringify({ path: val, file }));
|
|
64
157
|
}
|
|
65
158
|
}
|
|
66
159
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
160
|
+
// dynamic import("./a.png")
|
|
161
|
+
if (
|
|
162
|
+
callee.type === "Import" &&
|
|
163
|
+
args.length &&
|
|
164
|
+
args[0].type === "StringLiteral"
|
|
165
|
+
) {
|
|
166
|
+
const val = args[0].value;
|
|
167
|
+
if (IMAGE_REGEX.test(val)) {
|
|
168
|
+
used.add(JSON.stringify({ path: val, file }));
|
|
75
169
|
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
76
172
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
173
|
+
/**
|
|
174
|
+
* <img src="...">
|
|
175
|
+
*/
|
|
176
|
+
JSXAttribute(attr) {
|
|
177
|
+
if (attr.node.name.name === "src") {
|
|
178
|
+
const v = attr.node.value;
|
|
179
|
+
if (!v) return;
|
|
180
|
+
|
|
181
|
+
if (v.type === "StringLiteral") {
|
|
182
|
+
if (IMAGE_REGEX.test(v.value)) {
|
|
183
|
+
used.add(JSON.stringify({ path: v.value, file }));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (v.type === "JSXExpressionContainer") {
|
|
188
|
+
const expr = v.expression;
|
|
189
|
+
|
|
190
|
+
if (expr.type === "StringLiteral") {
|
|
191
|
+
if (IMAGE_REGEX.test(expr.value)) {
|
|
192
|
+
used.add(JSON.stringify({ path: expr.value, file }));
|
|
85
193
|
}
|
|
86
|
-
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (expr.type === "TemplateLiteral") {
|
|
197
|
+
extractFromTemplateLiteral(expr.quasis).forEach((v) => {
|
|
198
|
+
used.add(JSON.stringify({ path: v, file }));
|
|
199
|
+
});
|
|
200
|
+
}
|
|
87
201
|
}
|
|
88
202
|
}
|
|
89
|
-
},
|
|
90
203
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
usedImages.push({
|
|
95
|
-
value: path.node.value,
|
|
96
|
-
file,
|
|
97
|
-
});
|
|
204
|
+
// Handle style={{ backgroundImage: "url(...)" }}
|
|
205
|
+
if (attr.node.name.name === "style") {
|
|
206
|
+
extractFromJSXStyle(attr.node.value, file, used);
|
|
98
207
|
}
|
|
99
208
|
},
|
|
100
209
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
210
|
+
/**
|
|
211
|
+
* String Literals anywhere
|
|
212
|
+
*/
|
|
213
|
+
StringLiteral(p) {
|
|
214
|
+
const val = p.node.value;
|
|
215
|
+
|
|
216
|
+
if (IMAGE_REGEX.test(val)) {
|
|
217
|
+
used.add(JSON.stringify({ path: val, file }));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// detect url("...") inside strings (e.g., Tailwind)
|
|
221
|
+
const matches = [...val.matchAll(URL_EXTRACT_REGEX)];
|
|
222
|
+
matches.forEach((m) =>
|
|
223
|
+
used.add(JSON.stringify({ path: m[2], file }))
|
|
224
|
+
);
|
|
111
225
|
},
|
|
112
226
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
file,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
227
|
+
/**
|
|
228
|
+
* Template Literals anywhere
|
|
229
|
+
*/
|
|
230
|
+
TemplateLiteral(p) {
|
|
231
|
+
extractFromTemplateLiteral(p.node.quasis).forEach((v) => {
|
|
232
|
+
used.add(JSON.stringify({ path: v, file }));
|
|
122
233
|
});
|
|
123
234
|
},
|
|
124
235
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
}
|
|
236
|
+
/**
|
|
237
|
+
* styled-components & css`` blocks
|
|
238
|
+
*/
|
|
239
|
+
TaggedTemplateExpression(p) {
|
|
240
|
+
const quasi = p.node.quasi;
|
|
241
|
+
const extracted = extractFromTemplateLiteral(quasi.quasis);
|
|
242
|
+
extracted.forEach((v) => used.add(JSON.stringify({ path: v, file })));
|
|
134
243
|
},
|
|
135
244
|
});
|
|
136
245
|
}
|
|
137
246
|
|
|
247
|
+
// Add CSS images
|
|
138
248
|
const cssImages = await getCssImages(codeDirectory);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
break;
|
|
154
|
-
}else if(i === combinedImages.length - 1) {
|
|
155
|
-
if(options.excludeFilePrint && options.excludeFilePrint.length > 0) {
|
|
156
|
-
if(!isExcludedFile(filePath, options.excludeFilePrint)) {
|
|
157
|
-
unusedImages.push(filePath);
|
|
158
|
-
}
|
|
159
|
-
}else{
|
|
160
|
-
unusedImages.push(filePath);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
i++;
|
|
164
|
-
}
|
|
249
|
+
cssImages.forEach((img) =>
|
|
250
|
+
used.add(JSON.stringify({ path: img.value, file: img.file }))
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Normalize all used images
|
|
254
|
+
const normalizedUsed = new Set();
|
|
255
|
+
index = 0;
|
|
256
|
+
for (const entry of used) {
|
|
257
|
+
index++;
|
|
258
|
+
console.clear();
|
|
259
|
+
console.log('Normalizing images...', index, 'of', used.size);
|
|
260
|
+
const { path: p } = JSON.parse(entry);
|
|
261
|
+
const normalized = normalize(p, imageDirectory);
|
|
262
|
+
if (normalized) normalizedUsed.add(normalized);
|
|
165
263
|
}
|
|
166
264
|
|
|
167
|
-
|
|
265
|
+
// ---- Determine unused ----
|
|
266
|
+
index = 0;
|
|
267
|
+
for (const img of imageFiles) {
|
|
268
|
+
index++;
|
|
269
|
+
console.clear();
|
|
270
|
+
console.log('Determining unused images...', index, 'of', imageFiles.length);
|
|
271
|
+
const full = path.resolve(img);
|
|
272
|
+
if (!normalizedUsed.has(full)) {
|
|
273
|
+
unusedImages.push(full);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// ---- Output table or list ----
|
|
277
|
+
if (options.table) {
|
|
168
278
|
const table = new Table({
|
|
169
|
-
head: options.dryRun ? [
|
|
170
|
-
colWidths: [100]
|
|
171
|
-
});
|
|
172
|
-
unusedImages.forEach(image => {
|
|
173
|
-
table.push([image]);
|
|
279
|
+
head: options.dryRun ? ["Unused Images (Would Delete)"] : ["Unused Images"],
|
|
280
|
+
colWidths: [100],
|
|
174
281
|
});
|
|
282
|
+
unusedImages.forEach((img) => table.push([img]));
|
|
175
283
|
console.log(table.toString());
|
|
176
|
-
}else{
|
|
177
|
-
unusedImages.forEach(
|
|
178
|
-
console.log(chalk.red(image));
|
|
179
|
-
});
|
|
284
|
+
} else {
|
|
285
|
+
unusedImages.forEach((img) => console.log(chalk.red(img)));
|
|
180
286
|
}
|
|
181
287
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
console.log(
|
|
185
|
-
|
|
288
|
+
// ---- deletion logic ----
|
|
289
|
+
if (options.dryRun) {
|
|
290
|
+
console.log(
|
|
291
|
+
chalk.cyan(
|
|
292
|
+
`\n[DRY RUN] Would delete ${unusedImages.length} file(s)`
|
|
293
|
+
)
|
|
294
|
+
);
|
|
295
|
+
} else if (unusedImages.length > 0) {
|
|
186
296
|
askDeleteFiles(unusedImages);
|
|
187
297
|
}
|
|
298
|
+
|
|
188
299
|
return unusedImages;
|
|
189
300
|
}
|
|
190
301
|
|
package/controllers/list.js
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qleaner",
|
|
3
3
|
"packageManager": "yarn@4.6.0",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.24",
|
|
5
5
|
"main": "command.js",
|
|
6
6
|
"bin": "./bin/cli.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"start": "node ./bin/cli.js",
|
|
9
|
-
"resolve": "node ./utils/resolver.js"
|
|
9
|
+
"resolve": "node ./utils/resolver.js",
|
|
10
|
+
"scan": "node ./scanning.js"
|
|
10
11
|
},
|
|
11
12
|
"devDependencies": {
|
|
12
13
|
"@types/node": "^20.10.5",
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
"@babel/parser": "^7.28.5",
|
|
17
18
|
"@babel/traverse": "^7.28.5",
|
|
18
19
|
"chalk": "^5.6.2",
|
|
20
|
+
"cli-progress": "^3.12.0",
|
|
19
21
|
"cli-table3": "^0.6.5",
|
|
20
22
|
"commander": "^14.0.2",
|
|
21
23
|
"enhanced-resolve": "^5.18.3",
|
package/utils/cssImages.js
CHANGED
|
@@ -25,7 +25,11 @@ async function getCssImages(directory = "src") {
|
|
|
25
25
|
`${directory}/**/*.{css,scss}`,
|
|
26
26
|
]);
|
|
27
27
|
let images = [];
|
|
28
|
+
let index = 0;
|
|
28
29
|
for (const file of cssFiles) {
|
|
30
|
+
index++;
|
|
31
|
+
console.clear();
|
|
32
|
+
console.log('Scanning CSS files...', index, 'of', cssFiles.length);
|
|
29
33
|
const css = fs.readFileSync(file, "utf-8");
|
|
30
34
|
images = extractCssImages(css, images, file);
|
|
31
35
|
}
|
package/utils/resolver.js
CHANGED