oxphobia 0.0.1 → 0.0.2

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.
Files changed (3) hide show
  1. package/README.md +103 -0
  2. package/cli.js +138 -67
  3. package/package.json +5 -5
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # 📦 Oxphobia
2
+
3
+ **Oxphobia** is an ultra-fast JavaScript bundle size calculator powered by [Oxc (The Oxide JavaScript Tools)](https://github.com/oxc-project/oxc).
4
+
5
+ By leveraging the Rust-based `oxc-parser` and `oxc-minify`, it performs high-speed dependency analysis and minification to instantly measure the "Minified + Gzipped" size of npm packages or local projects.
6
+
7
+ ## ✨ Features
8
+
9
+ * 🚀 **Blazing Fast**: Powered by the Rust-based Oxc toolchain for incredibly quick parsing and compression.
10
+ * ☁️ **npm Package Support**: Recursively fetches and calculates the size of specified packages via [jsDelivr](https://www.jsdelivr.com/) .
11
+ * 📂 **Local Project Support**: Run it at your project root to automatically detect entry points and estimate the total size including dependencies.
12
+ * 🌳 **Simple Tree-Shaking**: Analyzes conditional branches based on `process.env.NODE_ENV` to exclude unnecessary code from the bundle.
13
+
14
+ ## 📦 Installation
15
+
16
+ You can run it directly via `npx` or install it globally.
17
+
18
+ ```bash
19
+ # Run directly
20
+ npx oxphobia [package-name]
21
+ pnpm dlx oxphobia [package-name]
22
+ yarn dlx oxphobia [package-name]
23
+ # or
24
+ dx npm:oxphobia [package-name]
25
+ # or
26
+ bunx oxphobia [package-name]
27
+ ```
28
+
29
+ Or install globally:
30
+
31
+ ```bash
32
+ npm install -g oxphobia
33
+ ```
34
+
35
+ ## 🚀 Usage
36
+
37
+ ### 1. Measure npm package size
38
+
39
+ Specify a package name to fetch sources from the CDN (jsDelivr) and calculate its size.
40
+
41
+ ```bash
42
+ npx oxphobia react
43
+ npx oxphobia lodash-es
44
+ npx oxphobia three
45
+ ```
46
+
47
+ ### 2. Measure local project size
48
+
49
+ Run without arguments to read the `package.json` in the current directory. It will identify the entry point from the `main`, `module`, or `exports` fields for analysis.
50
+
51
+ ```bash
52
+ cd my-awesome-project
53
+ npx oxphobia
54
+ ```
55
+
56
+ ### Example Output
57
+
58
+ ```text
59
+ 📦 Analyzing Package: react
60
+
61
+ 📥 Downloaded: https://cdn.jsdelivr.net/npm/react@18.2.0/index.js
62
+ 🔎 Dependencies: ./cjs/react.production.min.js
63
+ 📥 Downloaded: https://cdn.jsdelivr.net/npm/react@18.2.0/cjs/react.production.min.js
64
+ ...
65
+
66
+ ✅ Dependency resolution complete.
67
+ ⏳ Minifying with Oxc Minify...
68
+
69
+ ========================================
70
+ 📊 Result for "react"
71
+ ========================================
72
+ Files count : 2
73
+ Minified size : 6.42 KB (6,572 bytes)
74
+ Gzipped size : 2.75 KB (2,814 bytes)
75
+ ========================================
76
+ ```
77
+
78
+ ## 🛠️ How It Works
79
+
80
+ 1. **Parsing**: Uses `oxc-parser` to build an AST (Abstract Syntax Tree) and extracts dependencies from `import`, `require`, and `export` statements.
81
+ 2. **Resolution**:
82
+ * Local files are read from the file system.
83
+ * External packages are resolved and downloaded via the jsDelivr API.
84
+ 3. **Bundling**: Combines dependencies in-memory.
85
+ 4. **Minification**: Compresses the code (Mangle & Compress) using `oxc-minify`.
86
+ 5. **Measuring**: Compresses the minified code using Node.js `zlib` (Gzip) and calculates the final byte count.
87
+
88
+ ## ⚠️ Limitations
89
+
90
+ * Non-JS assets such as CSS and images are ignored.
91
+ * Advanced bundler configurations (e.g., Webpack/Rollup/Vite plugins) are not supported. It only tracks pure JS/ESM dependencies.
92
+
93
+ ## 💻 Requirements
94
+
95
+ * Node.js >= 18.12.0
96
+
97
+ ## 🤝 Contributing
98
+
99
+ Pull requests are welcome!
100
+
101
+ ## 📄 License
102
+
103
+ MIT
package/cli.js CHANGED
@@ -1,20 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import * as acorn from 'acorn';
4
- import { minify } from 'terser';
3
+ import { parseSync } from 'oxc-parser';
4
+ import { minifySync } from 'oxc-minify';
5
5
  import { gzipSync } from 'node:zlib';
6
6
  import { Buffer } from 'node:buffer';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
7
9
 
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
-
10
+ // --- Logger ---
18
11
  const log = {
19
12
  info: (msg) => console.log(`\x1b[90m${msg}\x1b[0m`),
20
13
  success: (msg) => console.log(`\x1b[32m${msg}\x1b[0m`),
@@ -24,11 +17,48 @@ const log = {
24
17
  warn: (msg) => console.log(`\x1b[33m${msg}\x1b[0m`),
25
18
  };
26
19
 
27
- // --- AST Utilities (Robust Walker) ---
20
+ // --- Command Line Arguments & Mode Detection ---
21
+ const args = process.argv.slice(2);
22
+ let targetPkg = args[0];
23
+ let isLocalMode = false;
24
+ let localProjectRoot = process.cwd();
25
+
26
+ // 引数がない場合はローカルモードとして動作
27
+ if (!targetPkg) {
28
+ const localPkgJsonPath = path.join(localProjectRoot, 'package.json');
29
+
30
+ if (fs.existsSync(localPkgJsonPath)) {
31
+ try {
32
+ const pkgData = JSON.parse(fs.readFileSync(localPkgJsonPath, 'utf-8'));
33
+ targetPkg = pkgData.name || 'local-project';
34
+ isLocalMode = true;
35
+
36
+ // エントリーポイントの特定
37
+ let entry = pkgData.main || pkgData.module || pkgData.exports?.['.'] || 'index.js';
38
+ if (typeof entry === 'object') entry = entry.import || entry.default || 'index.js';
39
+
40
+ // ローカルのエントリーファイルを絶対パスとしてセット
41
+ targetPkg = path.resolve(localProjectRoot, entry);
42
+
43
+ log.info(`📂 Local project detected: ${pkgData.name || 'unnamed'}`);
44
+ log.info(`🚀 Entry point: ${path.relative(process.cwd(), targetPkg)}`);
45
+ } catch (e) {
46
+ log.error('❌ Failed to read package.json');
47
+ process.exit(1);
48
+ }
49
+ } else {
50
+ console.error('\x1b[31mError: パッケージ名を指定するか、npmプロジェクトのルートで実行してください。\x1b[0m');
51
+ console.log('Usage: npx oxphobia <package-name>');
52
+ console.log(' npx oxphobia (inside a project)');
53
+ process.exit(1);
54
+ }
55
+ }
56
+
57
+ // --- AST Utilities (Oxc Walker) ---
28
58
 
29
59
  function isProcessEnvNodeEnv(node) {
30
60
  if (!node) return false;
31
- // process.env.NODE_ENV の検出
61
+ // process.env.NODE_ENV
32
62
  if (node.type === 'MemberExpression') {
33
63
  const propName = node.property && (node.property.name || node.property.value);
34
64
  if (propName === 'NODE_ENV') {
@@ -49,7 +79,7 @@ function evaluateEnvCondition(node) {
49
79
  if (!node || node.type !== 'BinaryExpression') return null;
50
80
 
51
81
  const getString = (n) => {
52
- if (n.type === 'Literal' && typeof n.value === 'string') return n.value;
82
+ if ((n.type === 'Literal' || n.type === 'StringLiteral') && typeof n.value === 'string') return n.value;
53
83
  return null;
54
84
  };
55
85
 
@@ -69,7 +99,7 @@ function evaluateEnvCondition(node) {
69
99
  }
70
100
 
71
101
  /**
72
- * 再帰的にASTを探索し、依存関係を抽出する
102
+ * Oxc ASTを探索し、依存関係を抽出する
73
103
  */
74
104
  function findDependencies(node, deps) {
75
105
  if (!node || typeof node !== 'object') return;
@@ -79,7 +109,7 @@ function findDependencies(node, deps) {
79
109
  return;
80
110
  }
81
111
 
82
- // --- 条件分岐のハンドリング (process.env.NODE_ENV) ---
112
+ // process.env.NODE_ENV ハンドリング
83
113
  if (node.type === 'IfStatement') {
84
114
  const isProd = evaluateEnvCondition(node.test);
85
115
  if (isProd === true) {
@@ -91,7 +121,7 @@ function findDependencies(node, deps) {
91
121
  }
92
122
  }
93
123
 
94
- if (node.type === 'ConditionalExpression') { // 三項演算子
124
+ if (node.type === 'ConditionalExpression') {
95
125
  const isProd = evaluateEnvCondition(node.test);
96
126
  if (isProd === true) {
97
127
  findDependencies(node.consequent, deps);
@@ -102,10 +132,10 @@ function findDependencies(node, deps) {
102
132
  }
103
133
  }
104
134
 
105
- // --- 依存関係の抽出 ---
106
135
  const extractString = (n) => {
107
136
  if (!n) return null;
108
- if (n.type === 'Literal' && typeof n.value === 'string') return n.value;
137
+ // Oxc Parser uses 'StringLiteral' often, but also supports ESTree 'Literal'
138
+ if ((n.type === 'Literal' || n.type === 'StringLiteral') && typeof n.value === 'string') return n.value;
109
139
  return null;
110
140
  };
111
141
 
@@ -117,7 +147,7 @@ function findDependencies(node, deps) {
117
147
  }
118
148
  }
119
149
 
120
- // Dynamic Import
150
+ // Dynamic Import: import('...')
121
151
  if (node.type === 'ImportExpression') {
122
152
  const val = extractString(node.source);
123
153
  if (val) deps.add(val);
@@ -126,22 +156,22 @@ function findDependencies(node, deps) {
126
156
  // CommonJS require()
127
157
  if (node.type === 'CallExpression') {
128
158
  const callee = node.callee;
159
+ // Oxc AST might represent callee differently if not standard ESTree, but usually Identifier works
129
160
  const isRequire = callee && callee.type === 'Identifier' && callee.name === 'require';
130
161
 
131
162
  if (isRequire && node.arguments && node.arguments.length > 0) {
163
+ // Oxc puts arguments in `arguments` vector
132
164
  const val = extractString(node.arguments[0]);
133
165
  if (val) deps.add(val);
134
166
  }
135
167
  }
136
168
 
137
- // --- 再帰探索 (重要なプロパティを網羅) ---
169
+ // 再帰探索キー (Oxc AST構造に対応)
138
170
  const keysToVisit = [
139
171
  'body', 'declarations', 'init', 'expression', 'callee', 'arguments',
140
172
  '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
173
+ 'exported', 'local', 'imported', 'program', 'statements',
174
+ 'elements', 'properties', 'value', 'block', 'handler', 'finalizer'
145
175
  ];
146
176
 
147
177
  for (const key of keysToVisit) {
@@ -149,11 +179,10 @@ function findDependencies(node, deps) {
149
179
  }
150
180
  }
151
181
 
152
-
153
182
  // --- Package Resolution ---
154
183
 
155
184
  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']);
185
+ 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', 'node:fs', 'node:path', 'node:process']);
157
186
 
158
187
  function parseBareSpecifier(specifier) {
159
188
  let pkgName = specifier, subpath = "";
@@ -189,7 +218,6 @@ async function resolvePackageUrl(specifier) {
189
218
  function resolveExports(exp) {
190
219
  if (typeof exp === 'string') return exp;
191
220
  if (typeof exp === 'object' && exp !== null) {
192
- // 優先順位: production -> node -> require -> default -> import -> browser
193
221
  const conditions = ['production', 'node', 'require', 'default', 'import', 'browser'];
194
222
  for (const cond of conditions) {
195
223
  if (cond in exp) return resolveExports(exp[cond]);
@@ -236,7 +264,28 @@ let activeTasks = 0;
236
264
  let onQueueEmpty = null;
237
265
  let hasError = false;
238
266
 
267
+ // ローカルファイルかどうかを判定
268
+ const isLocalFile = (url) => !url.startsWith('http');
269
+
239
270
  async function fetchFile(urls) {
271
+ // ローカルファイルの場合 (1つのパスしか来ない想定)
272
+ if (isLocalFile(urls[0])) {
273
+ const filePath = urls[0];
274
+ const extensions = ['', '.js', '.mjs', '.cjs', '.ts', '/index.js'];
275
+
276
+ for (const ext of extensions) {
277
+ const tryPath = filePath + ext;
278
+ if (fs.existsSync(tryPath) && fs.statSync(tryPath).isFile()) {
279
+ try {
280
+ const code = fs.readFileSync(tryPath, 'utf-8');
281
+ return { code, finalUrl: tryPath, isLocal: true };
282
+ } catch(e) { return null; }
283
+ }
284
+ }
285
+ throw new Error(`Local file not found: ${filePath}`);
286
+ }
287
+
288
+ // リモート (jsDelivr) の場合
240
289
  const tryFetch = async (u) => {
241
290
  try {
242
291
  const r = await fetch(u);
@@ -249,19 +298,33 @@ async function fetchFile(urls) {
249
298
  if (!res && !url.match(/\.(js|mjs|cjs|ts)$/)) {
250
299
  res = await tryFetch(url + '.js') || await tryFetch(url + '.mjs') || await tryFetch(url + '/index.js');
251
300
  }
252
- if (res) return { code: await res.text(), finalUrl: res.url };
301
+ if (res) return { code: await res.text(), finalUrl: res.url, isLocal: false };
253
302
  }
254
303
  throw new Error(`Fetch failed: ${urls[0]}`);
255
304
  }
256
305
 
257
306
  async function resolveUrl(url, baseUrl) {
258
- if (url.startsWith('http')) return [url];
259
307
  if (NODE_BUILTINS.has(url) || url.startsWith('node:')) return null;
260
-
308
+
309
+ // 絶対URL (http)
310
+ if (url.startsWith('http')) return [url];
311
+
312
+ // 相対パス (. or /)
261
313
  if (url.startsWith('.') || url.startsWith('/')) {
262
- if (!baseUrl) return [url];
314
+ if (!baseUrl) return [url]; // エントリーポイント等
315
+
316
+ // Baseがローカルファイルの場合 -> ファイルシステム上で解決
317
+ if (isLocalFile(baseUrl)) {
318
+ const dir = path.dirname(baseUrl);
319
+ return [path.resolve(dir, url)];
320
+ }
321
+
322
+ // BaseがURLの場合 -> URL結合
263
323
  return [new URL(url, baseUrl).href];
264
324
  }
325
+
326
+ // Bare Specifier (e.g. "react")
327
+ // ローカルモードでもリモートモードでも、外部パッケージは jsDelivr から引く仕様
265
328
  return await resolvePackageUrl(url);
266
329
  }
267
330
 
@@ -284,38 +347,42 @@ function enqueueFile(url, baseUrl) {
284
347
  return;
285
348
  }
286
349
 
287
- const { code, finalUrl } = fetchResult;
350
+ const { code, finalUrl, isLocal } = fetchResult;
288
351
 
289
352
  if (parsedUrls.has(finalUrl)) return;
290
353
  parsedUrls.add(finalUrl);
291
354
  if (primaryUrl !== finalUrl) parsedUrls.add(primaryUrl);
292
355
 
293
- log.fetch(` 📥 Downloaded: ${finalUrl}`);
356
+ if (isLocal) {
357
+ log.fetch(` 📁 Read Local: ${path.relative(process.cwd(), finalUrl)}`);
358
+ } else {
359
+ log.fetch(` 📥 Downloaded: ${finalUrl}`);
360
+ }
361
+
294
362
  bundleParts.push(code);
295
363
 
296
- // --- パース (Acornを使用) ---
297
- let ast;
364
+ // --- パース (Oxc Parserを使用) ---
365
+ let program;
298
366
  try {
299
- ast = acorn.parse(code, {
300
- ecmaVersion: 2022,
301
- sourceType: 'module' // まずESMとして試行
367
+ // Oxc parseSync returns { program, errors }
368
+ const ret = parseSync(finalUrl, code, {
369
+ sourceType: 'module', // ESMを基本とする
370
+ sourceFilename: finalUrl
302
371
  });
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; // パース失敗時は依存関係探索をスキップ
372
+
373
+ if (ret.errors.length > 0) {
374
+ // エラーがあってもASTが返る場合があるが、警告を出す
375
+ // log.warn(` ⚠️ Oxc Parse warnings for ${path.basename(finalUrl)}`);
313
376
  }
377
+ program = ret.program;
378
+ } catch (e) {
379
+ log.warn(` ⚠️ Parse failed for ${path.basename(finalUrl)}: ${e.message}`);
380
+ return;
314
381
  }
315
382
 
316
- if (ast) {
383
+ if (program) {
317
384
  const deps = new Set();
318
- findDependencies(ast, deps);
385
+ findDependencies(program, deps);
319
386
 
320
387
  if (deps.size > 0) {
321
388
  log.analyze(` 🔎 Dependencies: ${Array.from(deps).join(', ')}`);
@@ -340,9 +407,10 @@ function enqueueFile(url, baseUrl) {
340
407
  // --- Main Execution ---
341
408
 
342
409
  async function run() {
343
- log.success(`\n📦 Analyzing Package: ${pkg}\n`);
410
+ const displayTarget = isLocalMode ? path.relative(process.cwd(), targetPkg) : targetPkg;
411
+ log.success(`\n📦 Analyzing Package: ${displayTarget}\n`);
344
412
 
345
- enqueueFile(pkg, null);
413
+ enqueueFile(targetPkg, null);
346
414
 
347
415
  await new Promise(resolve => {
348
416
  if (activeTasks === 0) resolve();
@@ -350,29 +418,32 @@ async function run() {
350
418
  });
351
419
 
352
420
  if (bundleParts.length === 0) {
353
- log.error("\n❌ No files were successfully downloaded.");
421
+ log.error("\n❌ No files were successfully processed.");
354
422
  process.exit(1);
355
423
  }
356
424
 
357
425
  log.success(`\n✅ Dependency resolution complete.`);
358
- log.analyze(`⏳ Minifying with Terser (removing dead code for production)...`);
426
+ log.analyze(`⏳ Minifying with Oxc Minify...`);
359
427
 
360
428
  let minifiedCode = "";
361
429
  try {
362
430
  const combinedCode = bundleParts.join('\n');
363
431
 
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
- },
432
+ // Oxc Minify Execution
433
+ const result = minifySync("bundle.js", combinedCode, {
371
434
  mangle: true,
372
- format: { comments: false },
435
+ compress: {
436
+ dead_code: true, // Oxc may have different option keys, usually defaults are good
437
+ drop_console: false
438
+ },
439
+ sourceMap: false
373
440
  });
374
441
 
375
- minifiedCode = minifyResult.code || combinedCode;
442
+ minifiedCode = result.code;
443
+
444
+ // もしMinify結果が空の場合(エラー時など)、生コードを使用
445
+ if (!minifiedCode) throw new Error("Empty output");
446
+
376
447
  } catch(e) {
377
448
  log.error(`⚠️ Minification failed: ${e.message}. Using raw size.`);
378
449
  minifiedCode = bundleParts.join('\n');
@@ -382,7 +453,7 @@ async function run() {
382
453
  const gzipBytes = gzipSync(Buffer.from(minifiedCode)).length;
383
454
 
384
455
  console.log('\n========================================');
385
- console.log(` 📊 \x1b[1mResult for "${pkg}"\x1b[0m`);
456
+ console.log(` 📊 \x1b[1mResult for "${isLocalMode ? 'Local Project' : targetPkg}"\x1b[0m`);
386
457
  console.log('========================================');
387
458
  console.log(` Files count : \x1b[32m${bundleParts.length}\x1b[0m`);
388
459
  console.log(` Minified size : \x1b[33m${(minifiedBytes / 1024).toFixed(2)}\x1b[0m KB (${minifiedBytes.toLocaleString()} bytes)`);
@@ -390,4 +461,4 @@ async function run() {
390
461
  console.log('========================================\n');
391
462
  }
392
463
 
393
- run();
464
+ run();
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "oxphobia",
3
- "version": "0.0.1",
4
- "description": "Calculate minified & gzipped bundle size via JSDelivr + Oxc + Terser",
3
+ "version": "0.0.2",
4
+ "description": "Calculate minified & gzipped bundle size via JSDelivr + Oxc (Parser/Minifier)",
5
5
  "bin": {
6
6
  "oxphobia": "./cli.js"
7
7
  },
8
8
  "type": "module",
9
9
  "engines": {
10
- "node": ">=18.0.0"
10
+ "node": ">=18.12.0"
11
11
  },
12
12
  "dependencies": {
13
- "oxc-parser": "^0.60.0",
14
- "terser": "^5.31.0"
13
+ "oxc-minify": "^0.114.0",
14
+ "oxc-parser": "^0.114.0"
15
15
  }
16
16
  }