phantom-build 0.1.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.
Files changed (94) hide show
  1. package/README.md +378 -0
  2. package/dist/analyzer.d.ts +11 -0
  3. package/dist/analyzer.d.ts.map +1 -0
  4. package/dist/analyzer.js +330 -0
  5. package/dist/analyzer.js.map +1 -0
  6. package/dist/ast-compat.d.ts +11 -0
  7. package/dist/ast-compat.d.ts.map +1 -0
  8. package/dist/ast-compat.js +84 -0
  9. package/dist/ast-compat.js.map +1 -0
  10. package/dist/classify/boundary.d.ts +30 -0
  11. package/dist/classify/boundary.d.ts.map +1 -0
  12. package/dist/classify/boundary.js +145 -0
  13. package/dist/classify/boundary.js.map +1 -0
  14. package/dist/classify/browser-globals.d.ts +29 -0
  15. package/dist/classify/browser-globals.d.ts.map +1 -0
  16. package/dist/classify/browser-globals.js +197 -0
  17. package/dist/classify/browser-globals.js.map +1 -0
  18. package/dist/classify/index.d.ts +14 -0
  19. package/dist/classify/index.d.ts.map +1 -0
  20. package/dist/classify/index.js +294 -0
  21. package/dist/classify/index.js.map +1 -0
  22. package/dist/classify/lazy-llm.d.ts +122 -0
  23. package/dist/classify/lazy-llm.d.ts.map +1 -0
  24. package/dist/classify/lazy-llm.js +142 -0
  25. package/dist/classify/lazy-llm.js.map +1 -0
  26. package/dist/classify/lazy.d.ts +23 -0
  27. package/dist/classify/lazy.d.ts.map +1 -0
  28. package/dist/classify/lazy.js +686 -0
  29. package/dist/classify/lazy.js.map +1 -0
  30. package/dist/classify/llm-client.d.ts +59 -0
  31. package/dist/classify/llm-client.d.ts.map +1 -0
  32. package/dist/classify/llm-client.js +193 -0
  33. package/dist/classify/llm-client.js.map +1 -0
  34. package/dist/classify/purity.d.ts +21 -0
  35. package/dist/classify/purity.d.ts.map +1 -0
  36. package/dist/classify/purity.js +47 -0
  37. package/dist/classify/purity.js.map +1 -0
  38. package/dist/classify/react-patterns.d.ts +15 -0
  39. package/dist/classify/react-patterns.d.ts.map +1 -0
  40. package/dist/classify/react-patterns.js +82 -0
  41. package/dist/classify/react-patterns.js.map +1 -0
  42. package/dist/classify/taint.d.ts +32 -0
  43. package/dist/classify/taint.d.ts.map +1 -0
  44. package/dist/classify/taint.js +68 -0
  45. package/dist/classify/taint.js.map +1 -0
  46. package/dist/cli.d.ts +3 -0
  47. package/dist/cli.d.ts.map +1 -0
  48. package/dist/cli.js +109 -0
  49. package/dist/cli.js.map +1 -0
  50. package/dist/extract/chunk-module.d.ts +20 -0
  51. package/dist/extract/chunk-module.d.ts.map +1 -0
  52. package/dist/extract/chunk-module.js +163 -0
  53. package/dist/extract/chunk-module.js.map +1 -0
  54. package/dist/extract/client-stub.d.ts +25 -0
  55. package/dist/extract/client-stub.d.ts.map +1 -0
  56. package/dist/extract/client-stub.js +233 -0
  57. package/dist/extract/client-stub.js.map +1 -0
  58. package/dist/extract/import-resolver.d.ts +20 -0
  59. package/dist/extract/import-resolver.d.ts.map +1 -0
  60. package/dist/extract/import-resolver.js +51 -0
  61. package/dist/extract/import-resolver.js.map +1 -0
  62. package/dist/extract/index.d.ts +20 -0
  63. package/dist/extract/index.d.ts.map +1 -0
  64. package/dist/extract/index.js +105 -0
  65. package/dist/extract/index.js.map +1 -0
  66. package/dist/extract/lazy-transform.d.ts +14 -0
  67. package/dist/extract/lazy-transform.d.ts.map +1 -0
  68. package/dist/extract/lazy-transform.js +473 -0
  69. package/dist/extract/lazy-transform.js.map +1 -0
  70. package/dist/index.d.ts +4 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +3 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/plugin.d.ts +7 -0
  75. package/dist/plugin.d.ts.map +1 -0
  76. package/dist/plugin.js +535 -0
  77. package/dist/plugin.js.map +1 -0
  78. package/dist/runtime/index.d.ts +28 -0
  79. package/dist/runtime/index.d.ts.map +1 -0
  80. package/dist/runtime/index.js +73 -0
  81. package/dist/runtime/index.js.map +1 -0
  82. package/dist/types.d.ts +219 -0
  83. package/dist/types.d.ts.map +1 -0
  84. package/dist/types.js +2 -0
  85. package/dist/types.js.map +1 -0
  86. package/dist/vite.d.ts +3 -0
  87. package/dist/vite.d.ts.map +1 -0
  88. package/dist/vite.js +3 -0
  89. package/dist/vite.js.map +1 -0
  90. package/dist/webpack.d.ts +3 -0
  91. package/dist/webpack.d.ts.map +1 -0
  92. package/dist/webpack.js +3 -0
  93. package/dist/webpack.js.map +1 -0
  94. package/package.json +79 -0
package/README.md ADDED
@@ -0,0 +1,378 @@
1
+ # Phantom
2
+
3
+ **Automatic code-splitting for React event handlers and components.**
4
+
5
+ Phantom is a build plugin that analyzes your React code, extracts event handlers into lazy-loaded chunks, and wraps below-fold components in `React.lazy` + `Suspense` — all automatically, with zero config changes to your components.
6
+
7
+ ```
8
+ npm install phantom-build
9
+ ```
10
+
11
+ ## What It Does
12
+
13
+ Phantom runs at build time (Vite or Webpack) and does two things:
14
+
15
+ **1. Handler extraction** — Event handlers that touch browser APIs (`window`, `document`, `localStorage`, etc.) are extracted into separate chunks and loaded on-demand when the user first interacts.
16
+
17
+ **2. Lazy component wrapping** — Child components below the fold in route-level pages are automatically wrapped in `React.lazy()` + `<Suspense>`, so they don't block initial page load.
18
+
19
+ Your source code stays unchanged. Phantom transforms the output at build time.
20
+
21
+ ## Quick Start
22
+
23
+ ### Vite
24
+
25
+ ```ts
26
+ // vite.config.ts
27
+ import { defineConfig } from 'vite';
28
+ import react from '@vitejs/plugin-react';
29
+ import phantom from 'phantom-build/vite';
30
+
31
+ export default defineConfig({
32
+ plugins: [
33
+ phantom(),
34
+ react(),
35
+ ],
36
+ });
37
+ ```
38
+
39
+ ### Webpack
40
+
41
+ ```js
42
+ // webpack.config.js
43
+ import phantom from 'phantom-build/webpack';
44
+
45
+ export default {
46
+ // ...
47
+ module: {
48
+ rules: [
49
+ {
50
+ test: /\.tsx?$/,
51
+ use: {
52
+ loader: 'ts-loader',
53
+ options: { transpileOnly: true },
54
+ },
55
+ exclude: /node_modules/,
56
+ },
57
+ ],
58
+ },
59
+ plugins: [
60
+ phantom(),
61
+ ],
62
+ };
63
+ ```
64
+
65
+ That's it. Run your build and Phantom handles the rest.
66
+
67
+ ## How It Works
68
+
69
+ ### Handler Extraction
70
+
71
+ Given this component:
72
+
73
+ ```tsx
74
+ export function InteractiveComponent() {
75
+ const inputRef = useRef<HTMLInputElement>(null);
76
+
77
+ const handleClick = (e: React.MouseEvent) => {
78
+ window.location.href = `/product/${e.target.dataset.id}`;
79
+ };
80
+
81
+ const handleScroll = () => {
82
+ window.scrollTo(0, 0);
83
+ localStorage.setItem('scrolled', 'true');
84
+ };
85
+
86
+ return (
87
+ <div onClick={handleClick}>
88
+ <button onClick={handleScroll}>Top</button>
89
+ </div>
90
+ );
91
+ }
92
+ ```
93
+
94
+ Phantom produces:
95
+
96
+ - **Client code** — handlers are replaced with lightweight stubs that lazy-load the real logic on first click
97
+ - **Chunk modules** — each handler's logic lives in its own small file, loaded on demand
98
+
99
+ The stub calls `e.preventDefault()` and `e.stopPropagation()` synchronously (before the import), so critical event behavior is never delayed. The heavy logic (`window.location.href`, `localStorage`, etc.) loads asynchronously.
100
+
101
+ ### Lazy Component Wrapping
102
+
103
+ Given a route-level page component:
104
+
105
+ ```tsx
106
+ import { CartItems } from './CartItems';
107
+ import { OrderSummary } from './OrderSummary';
108
+ import { PaymentForm } from './PaymentForm';
109
+ import { AddressForm } from './AddressForm';
110
+ import { PromoCode } from './PromoCode';
111
+
112
+ export default function CheckoutPage({ order, user }) {
113
+ const [showPromo, setShowPromo] = useState(false);
114
+
115
+ return (
116
+ <CartProvider cartId={order.cartId}>
117
+ <CartItems items={order.items} />
118
+ <OrderSummary totals={order.totals} />
119
+ <PaymentForm userId={user.id} />
120
+ <AddressForm userId={user.id} />
121
+ {showPromo && <PromoCode cartId={order.cartId} />}
122
+ </CartProvider>
123
+ );
124
+ }
125
+ ```
126
+
127
+ Phantom transforms this to:
128
+
129
+ ```tsx
130
+ import { CartItems } from './CartItems';
131
+ import { OrderSummary } from './OrderSummary';
132
+
133
+ const PaymentForm = lazy(() =>
134
+ import('./PaymentForm').then(m => ({ default: m.PaymentForm }))
135
+ );
136
+ const AddressForm = lazy(() =>
137
+ import('./AddressForm').then(m => ({ default: m.AddressForm }))
138
+ );
139
+ const PromoCode = lazy(() =>
140
+ import('./PromoCode').then(m => ({ default: m.PromoCode }))
141
+ );
142
+
143
+ export default function CheckoutPage({ order, user }) {
144
+ const [showPromo, setShowPromo] = useState(false);
145
+
146
+ return (
147
+ <CartProvider cartId={order.cartId}>
148
+ {/* Kept static: above fold */}
149
+ <CartItems items={order.items} />
150
+ <OrderSummary totals={order.totals} />
151
+
152
+ {/* Lazy: adjacent siblings share one Suspense boundary */}
153
+ <Suspense fallback={null}>
154
+ <PaymentForm userId={user.id} />
155
+ <AddressForm userId={user.id} />
156
+ </Suspense>
157
+
158
+ {/* Lazy: conditionally rendered, own boundary */}
159
+ {showPromo && (
160
+ <Suspense fallback={null}>
161
+ <PromoCode cartId={order.cartId} />
162
+ </Suspense>
163
+ )}
164
+ </CartProvider>
165
+ );
166
+ }
167
+ ```
168
+
169
+ **What stays static (not lazified):**
170
+ - Components above the fold (positions 0-1 in the JSX tree)
171
+ - Context providers (must hydrate before consumers)
172
+ - Components with no meaningful JS cost (pure display, no handlers/effects/state)
173
+
174
+ **What gets lazified:**
175
+ - Components below the fold (position 2+)
176
+ - Conditionally rendered components (`{flag && <Component />}`)
177
+ - Components with handlers, effects, or state (worth deferring)
178
+
179
+ ## Configuration
180
+
181
+ ```ts
182
+ phantom({
183
+ // Confidence threshold for handler extraction (0.0 - 1.0)
184
+ // Lower = more aggressive extraction. Default: 0.8
185
+ confidenceThreshold: 0.8,
186
+
187
+ // Enable/disable lazy component wrapping. Default: true
188
+ enableLazy: true,
189
+
190
+ // Output path for the build manifest. Default: "phantom.manifest.json"
191
+ manifestPath: 'phantom.manifest.json',
192
+
193
+ // Suppress console output during build. Default: false
194
+ silent: false,
195
+
196
+ // Cerebras API key for LLM-assisted optimization (optional)
197
+ cerebrasApiKey: process.env.CEREBRAS_API_KEY,
198
+
199
+ // Cerebras model ID. Default: "qwen-3-32b"
200
+ cerebrasModel: 'qwen-3-32b',
201
+ })
202
+ ```
203
+
204
+ ### LLM-Assisted Optimization
205
+
206
+ Phantom's heuristics handle ~80% of cases correctly. For the remaining 20% that require judgment (grouping related components, choosing prefetch strategies for ambiguous cases), you can enable LLM refinement:
207
+
208
+ ```bash
209
+ # .env
210
+ CEREBRAS_API_KEY=your_key_here
211
+ ```
212
+
213
+ ```ts
214
+ phantom({
215
+ cerebrasApiKey: process.env.CEREBRAS_API_KEY,
216
+ })
217
+ ```
218
+
219
+ The LLM call is batched across modules (one API call per build), results are cached to disk (`*.lazy-cache.json`), and failures fall back to heuristics silently. The LLM never blocks your build.
220
+
221
+ ## CLI
222
+
223
+ Phantom includes a CLI for analyzing individual files:
224
+
225
+ ```bash
226
+ npx phantom analyze src/components/CheckoutPage.tsx
227
+ ```
228
+
229
+ Output:
230
+
231
+ ```
232
+ Phantom Analysis: src/components/CheckoutPage.tsx
233
+ ════════════════════════════════════════════════════════════
234
+
235
+ Name Class Conf Extracted?
236
+ ────────────────────────────── ──────────────────── ────── ──────────
237
+ handleTogglePromo EventHandler 0.95 ✓ yes
238
+ → JSX event handler prop: onClick
239
+ → Browser API: window referenced
240
+
241
+ Segments: 5
242
+ Threshold: 0.8
243
+ Chunks extracted: 1
244
+ seg_abc123 (0.1 KB)
245
+
246
+ Lazy Components:
247
+ Name Strategy Group
248
+ ───────────────────────── ──────────── ───────────────
249
+ PaymentForm viewport group_0
250
+ AddressForm viewport group_0
251
+ PromoCode interaction (solo)
252
+
253
+ Kept Static:
254
+ CartItems → Position 0 in route component — above fold
255
+ OrderSummary → Position 1 in route component — above fold
256
+ CartProvider → Context provider — must hydrate before consumers
257
+ ```
258
+
259
+ ### CLI Options
260
+
261
+ ```
262
+ phantom analyze <file> [options]
263
+
264
+ Options:
265
+ --threshold <number> Confidence threshold for extraction (default: 0.8)
266
+ --help, -h Show this help message
267
+ ```
268
+
269
+ ## Build Manifest
270
+
271
+ Each build produces a `phantom.manifest.json` describing all extractions:
272
+
273
+ ```json
274
+ {
275
+ "version": 1,
276
+ "entries": [
277
+ {
278
+ "segmentId": "seg_01b6063a6ad3",
279
+ "sourceFile": "/src/InteractiveComponent.tsx",
280
+ "virtualId": "phantom:seg_01b6063a6ad3.chunk.js",
281
+ "name": "handleClick",
282
+ "kind": "handler"
283
+ },
284
+ {
285
+ "segmentId": "lazy_PaymentForm",
286
+ "sourceFile": "/src/CheckoutPage.tsx",
287
+ "virtualId": "./PaymentForm",
288
+ "name": "lazy(PaymentForm)",
289
+ "kind": "lazy"
290
+ }
291
+ ],
292
+ "stats": {
293
+ "totalModulesProcessed": 42,
294
+ "totalSegmentsExtracted": 12
295
+ }
296
+ }
297
+ ```
298
+
299
+ ## Architecture Overview
300
+
301
+ Phantom processes each module through a 5-phase pipeline:
302
+
303
+ 1. **Parse** — OXC parser produces an ESTree AST; eslint-scope resolves variable bindings
304
+ 2. **Classify** — Three-pass analysis (taint, purity, boundary detection) classifies each function as `EventHandler`, `PureComputation`, `ClientInteractive`, `Shared`, or `Ambiguous`
305
+ 3. **Lazy Detection** — Identifies child component imports that should be `React.lazy` wrapped, using JSX position, conditionality, and cross-module component profiles
306
+ 4. **Extract** — Rewrites the AST: handler bodies move to virtual chunk modules, component imports become `lazy()` declarations, JSX gets `<Suspense>` wrappers
307
+ 5. **LLM Refinement** (optional) — Batched API call refines prefetch strategies and Suspense grouping for edge cases
308
+
309
+ ### Classification Rules
310
+
311
+ | Classification | Criteria | Action |
312
+ |---|---|---|
313
+ | `EventHandler` | Used exclusively as JSX event prop (`onClick`, `onSubmit`, etc.), references browser APIs | Extracted to lazy chunk |
314
+ | `PureComputation` | No browser globals, no side effects, deterministic | Kept inline |
315
+ | `ClientInteractive` | Browser APIs but not an event handler (effects, observers) | Kept inline |
316
+ | `Shared` | Mix of pure and impure, or component render function | Kept inline |
317
+ | `Ambiguous` | Below confidence threshold | Kept inline (conservative) |
318
+
319
+ ### Prefetch Strategies
320
+
321
+ | Strategy | When Used | Behavior |
322
+ |---|---|---|
323
+ | `viewport` | Below-fold components | Load when element enters viewport (IntersectionObserver) |
324
+ | `interaction` | Conditionally rendered components | Load on user interaction that triggers render |
325
+ | `idle` | Components with effects | Load during `requestIdleCallback` |
326
+ | `immediate` | LLM-determined critical paths | Load immediately after initial render |
327
+
328
+ ## How the Runtime Works
329
+
330
+ The `phantom-build/runtime` module provides `__phantom_lazy`, which:
331
+
332
+ 1. On first invocation, dynamically imports the handler chunk
333
+ 2. Caches the loaded function for instant subsequent calls
334
+ 3. Deduplicates concurrent imports (rapid clicks don't trigger multiple fetches)
335
+ 4. Cleans up on failure so retries work
336
+
337
+ ```ts
338
+ import { __phantom_lazy } from 'phantom-build/runtime';
339
+
340
+ // Generated stub (you never write this — Phantom does):
341
+ const handleClick = (e) => {
342
+ e.preventDefault(); // synchronous — runs immediately
343
+ e.persist?.(); // React <17 compat
344
+ __phantom_lazy(
345
+ () => import('phantom:seg_abc123.chunk.js'),
346
+ 'seg_abc123',
347
+ e, // forwarded args
348
+ inputRef, // captured variables
349
+ );
350
+ };
351
+ ```
352
+
353
+ ## Barrel File Support
354
+
355
+ Phantom resolves through barrel files automatically. If your components use index re-exports:
356
+
357
+ ```ts
358
+ // components/index.ts
359
+ export { PaymentForm } from './PaymentForm';
360
+ export { AddressForm } from './AddressForm';
361
+ ```
362
+
363
+ ```tsx
364
+ // CheckoutPage.tsx
365
+ import { PaymentForm, AddressForm } from './components';
366
+ ```
367
+
368
+ The generated `lazy()` calls will target the actual component modules (`./components/PaymentForm`), not the barrel file — producing optimal chunk splitting.
369
+
370
+ ## Requirements
371
+
372
+ - Node.js >= 18
373
+ - React 16.6+ (for `React.lazy` and `Suspense`)
374
+ - Vite or Webpack
375
+
376
+ ## License
377
+
378
+ MIT
@@ -0,0 +1,11 @@
1
+ import type { AnalyzedModule, AnalysisResult, PhantomPluginOptions } from './types.js';
2
+ /**
3
+ * Parse a module and perform scope analysis.
4
+ * Returns the AST, function dependency info, and import info.
5
+ */
6
+ export declare function parseModule(code: string, path: string): AnalyzedModule;
7
+ /**
8
+ * Analyze a single module: parse → scope analysis → classify → extract.
9
+ */
10
+ export declare function analyzeModule(code: string, path: string, _options?: PhantomPluginOptions): AnalysisResult;
11
+ //# sourceMappingURL=analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EAId,oBAAoB,EAErB,MAAM,YAAY,CAAC;AAYpB;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,CAyCtE;AAsQD;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,oBAAoB,GAC9B,cAAc,CAkDhB"}