oxphobia 0.0.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/LICENSE +21 -0
- package/cli.js +393 -0
- package/package.json +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ihasq
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/cli.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as acorn from 'acorn';
|
|
4
|
+
import { minify } from 'terser';
|
|
5
|
+
import { gzipSync } from 'node:zlib';
|
|
6
|
+
import { Buffer } from 'node:buffer';
|
|
7
|
+
|
|
8
|
+
// コマンドライン引数の取得
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const pkg = args[0];
|
|
11
|
+
|
|
12
|
+
if (!pkg) {
|
|
13
|
+
console.error('\x1b[31mError: パッケージ名を指定してください。\x1b[0m');
|
|
14
|
+
console.log('Usage: node cli.js <package-name>');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const log = {
|
|
19
|
+
info: (msg) => console.log(`\x1b[90m${msg}\x1b[0m`),
|
|
20
|
+
success: (msg) => console.log(`\x1b[32m${msg}\x1b[0m`),
|
|
21
|
+
error: (msg) => console.error(`\x1b[31m${msg}\x1b[0m`),
|
|
22
|
+
fetch: (msg) => console.log(`\x1b[36m${msg}\x1b[0m`),
|
|
23
|
+
analyze: (msg) => console.log(`\x1b[35m${msg}\x1b[0m`),
|
|
24
|
+
warn: (msg) => console.log(`\x1b[33m${msg}\x1b[0m`),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// --- AST Utilities (Robust Walker) ---
|
|
28
|
+
|
|
29
|
+
function isProcessEnvNodeEnv(node) {
|
|
30
|
+
if (!node) return false;
|
|
31
|
+
// process.env.NODE_ENV の検出
|
|
32
|
+
if (node.type === 'MemberExpression') {
|
|
33
|
+
const propName = node.property && (node.property.name || node.property.value);
|
|
34
|
+
if (propName === 'NODE_ENV') {
|
|
35
|
+
const obj = node.object;
|
|
36
|
+
if (obj && obj.type === 'MemberExpression') {
|
|
37
|
+
const objProp = obj.property && (obj.property.name || obj.property.value);
|
|
38
|
+
if (objProp === 'env') {
|
|
39
|
+
const root = obj.object;
|
|
40
|
+
if (root && root.name === 'process') return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function evaluateEnvCondition(node) {
|
|
49
|
+
if (!node || node.type !== 'BinaryExpression') return null;
|
|
50
|
+
|
|
51
|
+
const getString = (n) => {
|
|
52
|
+
if (n.type === 'Literal' && typeof n.value === 'string') return n.value;
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let envSide = null, strSide = null;
|
|
57
|
+
|
|
58
|
+
if (isProcessEnvNodeEnv(node.left)) {
|
|
59
|
+
envSide = node.left; strSide = getString(node.right);
|
|
60
|
+
} else if (isProcessEnvNodeEnv(node.right)) {
|
|
61
|
+
envSide = node.right; strSide = getString(node.left);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (envSide && strSide !== null) {
|
|
65
|
+
if (node.operator === '===' || node.operator === '==') return strSide === 'production';
|
|
66
|
+
if (node.operator === '!==' || node.operator === '!=') return strSide !== 'production';
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 再帰的にASTを探索し、依存関係を抽出する
|
|
73
|
+
*/
|
|
74
|
+
function findDependencies(node, deps) {
|
|
75
|
+
if (!node || typeof node !== 'object') return;
|
|
76
|
+
|
|
77
|
+
if (Array.isArray(node)) {
|
|
78
|
+
for (const child of node) findDependencies(child, deps);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- 条件分岐のハンドリング (process.env.NODE_ENV) ---
|
|
83
|
+
if (node.type === 'IfStatement') {
|
|
84
|
+
const isProd = evaluateEnvCondition(node.test);
|
|
85
|
+
if (isProd === true) {
|
|
86
|
+
findDependencies(node.consequent, deps);
|
|
87
|
+
return;
|
|
88
|
+
} else if (isProd === false) {
|
|
89
|
+
if (node.alternate) findDependencies(node.alternate, deps);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (node.type === 'ConditionalExpression') { // 三項演算子
|
|
95
|
+
const isProd = evaluateEnvCondition(node.test);
|
|
96
|
+
if (isProd === true) {
|
|
97
|
+
findDependencies(node.consequent, deps);
|
|
98
|
+
return;
|
|
99
|
+
} else if (isProd === false) {
|
|
100
|
+
findDependencies(node.alternate, deps);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- 依存関係の抽出 ---
|
|
106
|
+
const extractString = (n) => {
|
|
107
|
+
if (!n) return null;
|
|
108
|
+
if (n.type === 'Literal' && typeof n.value === 'string') return n.value;
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// ESM Import / Export
|
|
113
|
+
if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration'].includes(node.type)) {
|
|
114
|
+
if (node.source) {
|
|
115
|
+
const val = extractString(node.source);
|
|
116
|
+
if (val) deps.add(val);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Dynamic Import
|
|
121
|
+
if (node.type === 'ImportExpression') {
|
|
122
|
+
const val = extractString(node.source);
|
|
123
|
+
if (val) deps.add(val);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// CommonJS require()
|
|
127
|
+
if (node.type === 'CallExpression') {
|
|
128
|
+
const callee = node.callee;
|
|
129
|
+
const isRequire = callee && callee.type === 'Identifier' && callee.name === 'require';
|
|
130
|
+
|
|
131
|
+
if (isRequire && node.arguments && node.arguments.length > 0) {
|
|
132
|
+
const val = extractString(node.arguments[0]);
|
|
133
|
+
if (val) deps.add(val);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- 再帰探索 (重要なプロパティを網羅) ---
|
|
138
|
+
const keysToVisit = [
|
|
139
|
+
'body', 'declarations', 'init', 'expression', 'callee', 'arguments',
|
|
140
|
+
'consequent', 'alternate', 'test', 'left', 'right', 'source', 'specifiers',
|
|
141
|
+
'exported', 'local', 'imported', 'program',
|
|
142
|
+
'elements', // 配列内 [require('a')]
|
|
143
|
+
'properties', 'value', // オブジェクト内 { a: require('a') }
|
|
144
|
+
'block', 'handler', 'finalizer' // try-catch
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
for (const key of keysToVisit) {
|
|
148
|
+
if (node[key]) findDependencies(node[key], deps);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
// --- Package Resolution ---
|
|
154
|
+
|
|
155
|
+
const JSDELIVR_BASE = "https://cdn.jsdelivr.net/npm/";
|
|
156
|
+
const NODE_BUILTINS = new Set(['fs', 'path', 'os', 'crypto', 'stream', 'http', 'https', 'zlib', 'url', 'util', 'buffer', 'events', 'assert', 'child_process', 'process', 'net', 'tls', 'dgram', 'dns', 'perf_hooks', 'worker_threads']);
|
|
157
|
+
|
|
158
|
+
function parseBareSpecifier(specifier) {
|
|
159
|
+
let pkgName = specifier, subpath = "";
|
|
160
|
+
if (specifier.startsWith("@")) {
|
|
161
|
+
const parts = specifier.split("/");
|
|
162
|
+
pkgName = parts[0] + "/" + (parts[1] || "");
|
|
163
|
+
subpath = parts.slice(2).join("/");
|
|
164
|
+
} else {
|
|
165
|
+
const parts = specifier.split("/");
|
|
166
|
+
pkgName = parts[0];
|
|
167
|
+
subpath = parts.slice(1).join("/");
|
|
168
|
+
}
|
|
169
|
+
return { pkgName, subpath };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let pkgResolveCache = new Map();
|
|
173
|
+
|
|
174
|
+
async function resolvePackageUrl(specifier) {
|
|
175
|
+
if (pkgResolveCache.has(specifier)) return pkgResolveCache.get(specifier);
|
|
176
|
+
|
|
177
|
+
const { pkgName, subpath } = parseBareSpecifier(specifier);
|
|
178
|
+
const url = `${JSDELIVR_BASE}${pkgName}/package.json`;
|
|
179
|
+
|
|
180
|
+
let pkgObj = {}, pkgBase = `${JSDELIVR_BASE}${pkgName}/`;
|
|
181
|
+
try {
|
|
182
|
+
const res = await fetch(url);
|
|
183
|
+
if (res.ok) {
|
|
184
|
+
pkgObj = await res.json();
|
|
185
|
+
pkgBase = `${JSDELIVR_BASE}${pkgObj.name}@${pkgObj.version}/`;
|
|
186
|
+
}
|
|
187
|
+
} catch(e) { /* ignore */ }
|
|
188
|
+
|
|
189
|
+
function resolveExports(exp) {
|
|
190
|
+
if (typeof exp === 'string') return exp;
|
|
191
|
+
if (typeof exp === 'object' && exp !== null) {
|
|
192
|
+
// 優先順位: production -> node -> require -> default -> import -> browser
|
|
193
|
+
const conditions = ['production', 'node', 'require', 'default', 'import', 'browser'];
|
|
194
|
+
for (const cond of conditions) {
|
|
195
|
+
if (cond in exp) return resolveExports(exp[cond]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let entries = [];
|
|
202
|
+
if (subpath) {
|
|
203
|
+
let exportKey = `./${subpath}`;
|
|
204
|
+
if (pkgObj.exports) {
|
|
205
|
+
let target = pkgObj.exports[exportKey] || pkgObj.exports[exportKey + '.js'];
|
|
206
|
+
if (target) {
|
|
207
|
+
const subEntry = resolveExports(target);
|
|
208
|
+
if (subEntry) entries.push(subEntry);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
entries.push(subpath);
|
|
212
|
+
} else {
|
|
213
|
+
if (pkgObj.exports) {
|
|
214
|
+
let resolved = resolveExports(pkgObj.exports['.'] || pkgObj.exports);
|
|
215
|
+
if (resolved) entries.push(resolved);
|
|
216
|
+
}
|
|
217
|
+
if (pkgObj.main) entries.push(pkgObj.main);
|
|
218
|
+
if (pkgObj.module) entries.push(pkgObj.module);
|
|
219
|
+
entries.push("index.js");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let targetUrls = Array.from(new Set(entries.filter(Boolean))).map(entry => {
|
|
223
|
+
if (entry.startsWith("./")) entry = entry.slice(2);
|
|
224
|
+
return new URL(entry, pkgBase).href;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
pkgResolveCache.set(specifier, targetUrls);
|
|
228
|
+
return targetUrls;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// --- Fetch & Parse Logic ---
|
|
232
|
+
|
|
233
|
+
let parsedUrls = new Set();
|
|
234
|
+
let bundleParts = [];
|
|
235
|
+
let activeTasks = 0;
|
|
236
|
+
let onQueueEmpty = null;
|
|
237
|
+
let hasError = false;
|
|
238
|
+
|
|
239
|
+
async function fetchFile(urls) {
|
|
240
|
+
const tryFetch = async (u) => {
|
|
241
|
+
try {
|
|
242
|
+
const r = await fetch(u);
|
|
243
|
+
return (r.ok) ? r : null;
|
|
244
|
+
} catch { return null; }
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
for (const url of urls) {
|
|
248
|
+
let res = await tryFetch(url);
|
|
249
|
+
if (!res && !url.match(/\.(js|mjs|cjs|ts)$/)) {
|
|
250
|
+
res = await tryFetch(url + '.js') || await tryFetch(url + '.mjs') || await tryFetch(url + '/index.js');
|
|
251
|
+
}
|
|
252
|
+
if (res) return { code: await res.text(), finalUrl: res.url };
|
|
253
|
+
}
|
|
254
|
+
throw new Error(`Fetch failed: ${urls[0]}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function resolveUrl(url, baseUrl) {
|
|
258
|
+
if (url.startsWith('http')) return [url];
|
|
259
|
+
if (NODE_BUILTINS.has(url) || url.startsWith('node:')) return null;
|
|
260
|
+
|
|
261
|
+
if (url.startsWith('.') || url.startsWith('/')) {
|
|
262
|
+
if (!baseUrl) return [url];
|
|
263
|
+
return [new URL(url, baseUrl).href];
|
|
264
|
+
}
|
|
265
|
+
return await resolvePackageUrl(url);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function enqueueFile(url, baseUrl) {
|
|
269
|
+
activeTasks++;
|
|
270
|
+
|
|
271
|
+
(async () => {
|
|
272
|
+
try {
|
|
273
|
+
const targetUrls = await resolveUrl(url, baseUrl);
|
|
274
|
+
if (!targetUrls || targetUrls.length === 0) return;
|
|
275
|
+
|
|
276
|
+
const primaryUrl = targetUrls[0];
|
|
277
|
+
if (parsedUrls.has(primaryUrl)) return;
|
|
278
|
+
|
|
279
|
+
let fetchResult;
|
|
280
|
+
try {
|
|
281
|
+
fetchResult = await fetchFile(targetUrls);
|
|
282
|
+
} catch (e) {
|
|
283
|
+
log.error(` ❌ Failed to fetch: ${url}`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const { code, finalUrl } = fetchResult;
|
|
288
|
+
|
|
289
|
+
if (parsedUrls.has(finalUrl)) return;
|
|
290
|
+
parsedUrls.add(finalUrl);
|
|
291
|
+
if (primaryUrl !== finalUrl) parsedUrls.add(primaryUrl);
|
|
292
|
+
|
|
293
|
+
log.fetch(` 📥 Downloaded: ${finalUrl}`);
|
|
294
|
+
bundleParts.push(code);
|
|
295
|
+
|
|
296
|
+
// --- パース (Acornを使用) ---
|
|
297
|
+
let ast;
|
|
298
|
+
try {
|
|
299
|
+
ast = acorn.parse(code, {
|
|
300
|
+
ecmaVersion: 2022,
|
|
301
|
+
sourceType: 'module' // まずESMとして試行
|
|
302
|
+
});
|
|
303
|
+
} catch (e) {
|
|
304
|
+
try {
|
|
305
|
+
// 失敗したらScript(CommonJS)として試行
|
|
306
|
+
ast = acorn.parse(code, {
|
|
307
|
+
ecmaVersion: 2022,
|
|
308
|
+
sourceType: 'script'
|
|
309
|
+
});
|
|
310
|
+
} catch (e2) {
|
|
311
|
+
log.warn(` ⚠️ Parse failed for ${finalUrl.split('/').pop()}: ${e2.message}`);
|
|
312
|
+
return; // パース失敗時は依存関係探索をスキップ
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (ast) {
|
|
317
|
+
const deps = new Set();
|
|
318
|
+
findDependencies(ast, deps);
|
|
319
|
+
|
|
320
|
+
if (deps.size > 0) {
|
|
321
|
+
log.analyze(` 🔎 Dependencies: ${Array.from(deps).join(', ')}`);
|
|
322
|
+
for (const dep of deps) {
|
|
323
|
+
enqueueFile(dep, finalUrl);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
} catch(e) {
|
|
329
|
+
hasError = true;
|
|
330
|
+
log.error(` 💥 Unexpected Error (${url}): ${e.message}`);
|
|
331
|
+
} finally {
|
|
332
|
+
activeTasks--;
|
|
333
|
+
if (activeTasks === 0 && onQueueEmpty) {
|
|
334
|
+
onQueueEmpty();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
})();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// --- Main Execution ---
|
|
341
|
+
|
|
342
|
+
async function run() {
|
|
343
|
+
log.success(`\n📦 Analyzing Package: ${pkg}\n`);
|
|
344
|
+
|
|
345
|
+
enqueueFile(pkg, null);
|
|
346
|
+
|
|
347
|
+
await new Promise(resolve => {
|
|
348
|
+
if (activeTasks === 0) resolve();
|
|
349
|
+
else onQueueEmpty = resolve;
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (bundleParts.length === 0) {
|
|
353
|
+
log.error("\n❌ No files were successfully downloaded.");
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
log.success(`\n✅ Dependency resolution complete.`);
|
|
358
|
+
log.analyze(`⏳ Minifying with Terser (removing dead code for production)...`);
|
|
359
|
+
|
|
360
|
+
let minifiedCode = "";
|
|
361
|
+
try {
|
|
362
|
+
const combinedCode = bundleParts.join('\n');
|
|
363
|
+
|
|
364
|
+
const minifyResult = await minify(combinedCode, {
|
|
365
|
+
compress: {
|
|
366
|
+
global_defs: { 'process.env.NODE_ENV': 'production' },
|
|
367
|
+
dead_code: true,
|
|
368
|
+
toplevel: false,
|
|
369
|
+
passes: 2
|
|
370
|
+
},
|
|
371
|
+
mangle: true,
|
|
372
|
+
format: { comments: false },
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
minifiedCode = minifyResult.code || combinedCode;
|
|
376
|
+
} catch(e) {
|
|
377
|
+
log.error(`⚠️ Minification failed: ${e.message}. Using raw size.`);
|
|
378
|
+
minifiedCode = bundleParts.join('\n');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const minifiedBytes = Buffer.byteLength(minifiedCode);
|
|
382
|
+
const gzipBytes = gzipSync(Buffer.from(minifiedCode)).length;
|
|
383
|
+
|
|
384
|
+
console.log('\n========================================');
|
|
385
|
+
console.log(` 📊 \x1b[1mResult for "${pkg}"\x1b[0m`);
|
|
386
|
+
console.log('========================================');
|
|
387
|
+
console.log(` Files count : \x1b[32m${bundleParts.length}\x1b[0m`);
|
|
388
|
+
console.log(` Minified size : \x1b[33m${(minifiedBytes / 1024).toFixed(2)}\x1b[0m KB (${minifiedBytes.toLocaleString()} bytes)`);
|
|
389
|
+
console.log(` Gzipped size : \x1b[32m\x1b[1m${(gzipBytes / 1024).toFixed(2)}\x1b[0m KB (${gzipBytes.toLocaleString()} bytes)`);
|
|
390
|
+
console.log('========================================\n');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
run();
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oxphobia",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Calculate minified & gzipped bundle size via JSDelivr + Oxc + Terser",
|
|
5
|
+
"bin": {
|
|
6
|
+
"oxphobia": "./cli.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18.0.0"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"oxc-parser": "^0.60.0",
|
|
14
|
+
"terser": "^5.31.0"
|
|
15
|
+
}
|
|
16
|
+
}
|