api-tracer-kit 1.0.0 → 1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ - `api-tracer setup` wires the tracer into an application in one command. It
6
+ detects the framework, writes the module that configures the tracer with the
7
+ environment gate that bundler will actually fold, adds the import to the entry
8
+ file, gitignores the data directory, and warns if anything else already calls
9
+ `init()`. Safe to re-run.
10
+ - Full documentation set under `docs/`, shipped in the package.
11
+
12
+ ## 1.0.1
13
+
14
+ - `init()` now warns when it is called a second time with options, naming the
15
+ ones it ignored. It stays idempotent — a double-init must never install a
16
+ second set of interceptors — but silently dropping the second caller's
17
+ configuration made for a symptom (a tracer that runs but reports nowhere) that
18
+ looked nothing like its cause.
19
+
3
20
  ## 1.0.0
4
21
 
5
22
  First release. Extracted from an in-repo API console and made standalone.
package/README.md CHANGED
@@ -3,13 +3,17 @@
3
3
  Trace every API call an application makes — and map, exercise and watch the
4
4
  endpoints it has.
5
5
 
6
+ [![npm](https://img.shields.io/npm/v/api-tracer-kit.svg)](https://www.npmjs.com/package/api-tracer-kit)
7
+ [![license](https://img.shields.io/npm/l/api-tracer-kit.svg)](./LICENSE)
8
+
6
9
  Two halves that work on their own or together:
7
10
 
8
- - **the tracer** — a framework-free library that records every `fetch`, `XMLHttpRequest`
9
- and axios call, with no change to your API layer and no bundler configuration.
11
+ - **the tracer** — a framework-free library that records every `fetch`,
12
+ `XMLHttpRequest` and axios call, with no change to your API layer and no
13
+ bundler configuration
10
14
  - **the console** — a CLI and web UI that reads your source into an endpoint
11
15
  catalog, receives the tracer's traffic, and lets you re-send any call, watch
12
- for response-shape drift, and export what it captured.
16
+ for response-shape drift, and export what it captured
13
17
 
14
18
  ```bash
15
19
  npm install api-tracer-kit
@@ -17,26 +21,45 @@ npm install api-tracer-kit
17
21
 
18
22
  ## Quick start
19
23
 
24
+ One command wires it into your app — it detects the framework, writes the module
25
+ that configures the tracer, and adds the import to your entry file:
26
+
27
+ ```bash
28
+ npx api-tracer setup
29
+ ```
30
+
31
+ Or do it by hand, which is two lines:
32
+
20
33
  ```ts
21
34
  import { apiTracer } from 'api-tracer-kit';
22
35
 
23
36
  apiTracer.init();
24
37
  ```
25
38
 
26
- That is the whole setup. Every call made afterwards is traced:
39
+ Either way, that is the whole setup. Every call made afterwards is traced:
27
40
 
28
41
  ```ts
29
42
  await fetch('/api/users');
30
43
 
31
44
  apiTracer.getTraces();
32
- // [{ id, request: { url, method, headers, params, body }, response: { status, headers, body },
45
+ // [{ id, request: { url, method, headers, params, body },
46
+ // response: { status, headers, body },
33
47
  // timing: { startedAt, completedAt, duration }, status: 'success' }]
34
48
  ```
35
49
 
36
50
  Nothing happens at import time, so the package is safe to import during SSR, in
37
51
  a build step, or in a test. `init()` is the only thing that patches anything.
38
52
 
39
- Then, optionally, the console:
53
+ **Using axios?** Hand yours over — an axios instance has no global to patch:
54
+
55
+ ```ts
56
+ apiTracer.useAxios(axios);
57
+ ```
58
+
59
+ One call covers the default export and every instance `axios.create()` makes
60
+ afterwards.
61
+
62
+ **Want the console?**
40
63
 
41
64
  ```bash
42
65
  npx api-tracer start
@@ -46,6 +69,22 @@ It reads your source, finds your endpoints, and opens at
46
69
  `http://localhost:4400`. Point the tracer at it with `reportTo: true` and your
47
70
  app's own traffic fills it in as you click around.
48
71
 
72
+ > Configure the tracer in exactly one place. `init()` is idempotent — a second
73
+ > call keeps the first one's configuration and warns about what it ignored.
74
+
75
+ ## Documentation
76
+
77
+ | | |
78
+ | --- | --- |
79
+ | [Getting started](./docs/getting-started.md) | install, first trace, seeing them |
80
+ | [Tracer API](./docs/tracer.md) | every option, the `ApiTrace` model, storage, axios, lifecycle |
81
+ | [The console](./docs/console.md) | CLI, dashboard, replay, drift, coverage, import/export, deployment |
82
+ | [Configuration](./docs/configuration.md) | the config file, scanner presets, custom parsers, sign-in flows |
83
+ | [Frameworks](./docs/frameworks.md) | React, Next.js, Vite, CRA, Node, testing |
84
+ | [Troubleshooting](./docs/troubleshooting.md) | when nothing is being recorded |
85
+ | [Security](./docs/security.md) | redaction, what is stored, what is safe to share |
86
+ | [Architecture](./docs/architecture.md) | how it works inside, and why |
87
+
49
88
  ## Features
50
89
 
51
90
  **Tracing**
@@ -60,24 +99,23 @@ app's own traffic fills it in as you click around.
60
99
  - files in a `FormData` recorded as `<file: scan.pdf, 20418 bytes>`
61
100
  - a unique id per call, and no mixing between concurrent requests
62
101
  - credential redaction in headers and bodies, on by default
63
- - envelope reading, for APIs that answer `200` and put the real verdict in the body
64
- - subscribers, a replaceable storage layer, and a capped in-memory store by default
102
+ - envelope reading, for APIs that answer `200` and put the verdict in the body
103
+ - subscribers, a replaceable storage layer, a capped in-memory store by default
65
104
 
66
105
  **Console**
67
106
 
68
- - an endpoint catalog read straight out of your source — no hand-maintained list
107
+ - an endpoint catalog read straight out of your source — nothing hand-maintained
69
108
  - live recording: your app's traffic lands on the endpoint it belongs to, with
70
109
  the real params, payload and path ids already filled in
71
- - send any endpoint, with a Postman-style tabbed editor (params, headers, body,
72
- capture rules) and a syntax-highlighted JSON editor
73
- - bulk runs, and a full replay of every captured call, with a plan shown before
74
- anything is sent
110
+ - send any endpoint, with a Postman-style tabbed editor and a syntax-highlighted
111
+ JSON editor
112
+ - bulk runs, and a full replay of every captured call, with a plan shown first
75
113
  - response-shape contracts, so a backend that quietly drops a field is caught
76
114
  even though it answered `200`
77
115
  - coverage: which endpoints your app has never exercised
78
116
  - an insight report — coverage, failures, drift, risks, hygiene, auth surface,
79
- latency, inventory and a trend line exportable as Markdown or JSON
80
- - HAR / cURL / Postman export, and HAR / cURL import
117
+ latency, inventory, trend — as Markdown or JSON
118
+ - HAR / cURL / Postman export, HAR / cURL import
81
119
  - `{{variables}}`, captured from responses and chained between calls
82
120
 
83
121
  ## Supported environments
@@ -85,13 +123,13 @@ app's own traffic fills it in as you click around.
85
123
  | | |
86
124
  | --- | --- |
87
125
  | Browsers | ✅ `fetch`, `XMLHttpRequest`, axios |
88
- | Node 18+ | ✅ `fetch` and axios; `XMLHttpRequest` is skipped when absent |
126
+ | Node 18+ | ✅ `fetch` and axios; `XMLHttpRequest` skipped when absent |
89
127
  | React / Next.js / Vite / CRA / webpack | ✅ no plugin, loader, alias or config change |
90
128
  | SSR and build-time execution | ✅ importing does nothing; `init()` no-ops without a runtime |
91
- | TypeScript | ✅ types shipped, `strict`-clean |
129
+ | TypeScript | ✅ types shipped, `strict`-clean, `bundler` and `node16` resolution |
92
130
  | Deno / Bun | ⚠️ untested, but the tracer only uses standard globals |
93
131
 
94
- Nothing in the core imports React or axios. Both are optional peer dependencies
132
+ Nothing in the core imports React or axios. Both are optional peer dependencies,
95
133
  used only by the entry points that need them.
96
134
 
97
135
  ## Entry points
@@ -103,295 +141,19 @@ import { useApiTraces } from 'api-tracer-kit/react'; // hooks
103
141
  import { ApiTracerPanel } from 'api-tracer-kit/ui'; // in-app panel
104
142
  ```
105
143
 
106
- Each is bundled separately, so importing the tracer never pulls in React.
107
-
108
- ## Configuration
109
-
110
- All of it is optional.
111
-
112
- ```ts
113
- apiTracer.init({
114
- // which transports to patch. Default: all of them that exist.
115
- transports: ['fetch', 'xhr'],
116
-
117
- // only trace URLs matching one of these. Strings match by substring.
118
- include: ['/api/'],
119
- exclude: [/\/health$/],
120
-
121
- // header names replaced with "<redacted>"
122
- redactHeaders: ['authorization', 'cookie', 'set-cookie', 'x-api-key'],
123
-
124
- // body and query field names replaced with "<redacted>"
125
- redactFields: /(token|password|secret|api[-_]?key)/i,
126
-
127
- // bodies bigger than this are recorded as omitted rather than kept
128
- maxBodyBytes: 200_000,
129
-
130
- // how many traces to keep; the oldest fall off the end
131
- maxTraces: 500,
132
-
133
- // for APIs that answer 200 and put the verdict in the body
134
- envelope: { codeFields: ['status', 'code'], okField: 'success', failFrom: 400 },
135
-
136
- // stream finished traces to the console; `true` means http://localhost:4400
137
- reportTo: true,
138
-
139
- // replace the in-memory store
140
- storage: myStorage,
141
-
142
- onTrace: (trace) => console.log(trace),
143
- });
144
- ```
145
-
146
- There is no config file, and the consuming project needs no changes to its
147
- webpack, Vite, Babel, TypeScript, Next.js, CRA or ESLint setup.
148
-
149
- ## API reference
150
-
151
- ### `apiTracer`
152
-
153
- | | |
154
- | --- | --- |
155
- | `init(options?)` | installs the interceptors. Idempotent — calling it twice does not double-install. |
156
- | `useAxios(axios)` | traces an axios object and everything `axios.create()` makes from it |
157
- | `getTraces()` | every trace held, oldest first |
158
- | `getTrace(id)` | one trace |
159
- | `subscribe(fn)` | called with each finished trace; returns the unsubscribe function |
160
- | `clear()` | drops every trace |
161
- | `remove(id)` | drops one |
162
- | `destroy()` | restores every patched global; the app behaves exactly as it did before `init()` |
163
- | `isActive` | whether tracing is installed |
164
-
165
- Standalone equivalents are exported too, for callers who prefer functions:
166
- `initApiTracer`, `getApiTraces`, `clearApiTraces`, `subscribeToApiTraces`,
167
- `destroyApiTracer`.
168
-
169
- ### `ApiTrace`
170
-
171
- ```ts
172
- interface ApiTrace {
173
- id: string;
174
- transport: 'fetch' | 'xhr' | 'axios';
175
- status: 'pending' | 'success' | 'error' | 'network-error' | 'cancelled';
176
-
177
- request: {
178
- url: string;
179
- path: string; // origin + pathname, for grouping
180
- method: string;
181
- headers: Record<string, string>;
182
- params: Record<string, unknown>; // parsed query string
183
- body?: unknown; // parsed and redacted
184
- bodyType: 'json' | 'formdata' | 'urlencoded' | 'text' | 'binary' | 'none';
185
- bodyOmitted?: 'too-large' | 'stream' | 'binary' | 'unreadable';
186
- };
187
-
188
- response?: {
189
- status: number;
190
- statusText?: string;
191
- headers: Record<string, string>;
192
- body?: string;
193
- bodyOmitted?: 'too-large' | 'stream' | 'binary' | 'unreadable';
194
- size?: number;
195
- };
196
-
197
- timing: { startedAt: number; completedAt?: number; duration?: number };
198
- error?: { message: string; name?: string; stack?: string };
199
-
200
- envelopeOk?: boolean; // false when a 2xx carried an error code
201
- envelopeCode?: number;
202
- }
203
- ```
204
-
205
- ### Storage
206
-
207
- ```ts
208
- interface TraceStorage {
209
- add(trace: ApiTrace): void;
210
- update(id: string, patch: Partial<ApiTrace>): ApiTrace | undefined;
211
- get(id: string): ApiTrace | undefined;
212
- getAll(): ApiTrace[];
213
- remove(id: string): void;
214
- clear(): void;
215
- }
216
- ```
217
-
218
- The default is `MemoryTraceStorage`, a capped ring — an unbounded store in a
219
- long-lived tab is a memory leak. Nothing is persisted; supply your own storage
220
- if you want it to be.
221
-
222
- ## React
223
-
224
- ```tsx
225
- import { useApiTracer, useApiTraces } from 'api-tracer-kit/react';
226
-
227
- function App() {
228
- useApiTracer(); // init once, StrictMode-safe
229
- const { traces, clear } = useApiTraces();
230
- return <span>{traces.length} calls</span>;
231
- }
232
- ```
233
-
234
- Updates are batched on an animation frame, so a burst of calls on page load
235
- re-renders once rather than once per request.
144
+ Each is bundled separately with `sideEffects: false`, so importing the tracer
145
+ never pulls in React, and an unused import is dropped entirely.
236
146
 
237
- ## The in-app panel
147
+ ## It will not break your API calls
238
148
 
239
- ```tsx
240
- import { ApiTracerPanel } from 'api-tracer-kit/ui';
149
+ Removing the tracer changes nothing. Responses are read through
150
+ `Response.clone()` after the original is handed back; streaming responses are
151
+ never buffered; request-body streams are never consumed; errors are rethrown
152
+ exactly as they arrived; nothing is retried and no request is made twice; XHR is
153
+ observed with `addEventListener` rather than by taking `onload`; and `destroy()`
154
+ restores every patched global.
241
155
 
242
- <ApiTracerPanel enabled={process.env.NODE_ENV !== 'production'} />;
243
- ```
244
-
245
- A floating panel with the call list, per-call detail (request and response
246
- headers, params, bodies, status, duration, errors), search, method and status
247
- filters, sorting, expand/collapse and clear. Styles are inline, so there is no
248
- stylesheet to import and nothing to collide with your app's CSS.
249
-
250
- ## Axios
251
-
252
- axios calls are traced two ways.
253
-
254
- In a browser axios rides on `XMLHttpRequest`, so `apiTracer.init()` alone
255
- already sees them. For richer data — the params object as axios received it, the
256
- config, and Node's `http` adapter, which no global patch can reach — attach it
257
- explicitly:
258
-
259
- ```ts
260
- import axios from 'axios';
261
- import { apiTracer } from 'api-tracer-kit';
262
-
263
- apiTracer.init();
264
- apiTracer.useAxios(axios);
265
- ```
266
-
267
- One call covers the default export **and every instance `axios.create()` makes
268
- afterwards**. That matters in codebases that send some requests through per-page
269
- instances and the rest through the default object — patching only the default
270
- misses half the traffic.
271
-
272
- A call seen by both adapters is recorded once, not twice.
273
-
274
- Why an explicit call at all: an axios instance is a plain object with no global
275
- to patch, so there is no way for the tracer to reach one it was never handed.
276
- This is the smallest integration that works, and it is the only one in the
277
- package.
278
-
279
- ## SSR
280
-
281
- Importing the package evaluates no browser global and installs nothing. `init()`
282
- checks for each runtime capability before patching it, so on a server it simply
283
- finds nothing to patch and returns.
284
-
285
- ```ts
286
- // safe anywhere
287
- import { apiTracer } from 'api-tracer-kit';
288
-
289
- if (typeof window !== 'undefined') apiTracer.init();
290
- ```
291
-
292
- The guard is not required — `init()` is already a no-op without a runtime — but
293
- it makes the intent obvious in a Next.js file that runs on both sides.
294
-
295
- ## Not breaking your API calls
296
-
297
- The tracer is built so that removing it changes nothing. Specifically:
298
-
299
- - responses are read through `Response.clone()`, after the original has been
300
- handed back, so your `.json()` still works
301
- - streaming responses (`text/event-stream`) are never buffered
302
- - binary and non-textual responses are described, not read
303
- - request bodies that are `ReadableStream`s are never consumed
304
- - bodies over `maxBodyBytes` are recorded as omitted rather than copied
305
- - errors are rethrown exactly as they arrived; aborts stay `AbortError`
306
- - `AbortSignal`, redirects, credentials and headers are passed through untouched
307
- - nothing is retried, and no request is ever made twice
308
- - XHR is observed with `addEventListener`, never by taking `onload` or
309
- `onreadystatechange`
310
- - a subscriber that throws is caught, so a bad listener cannot break a request
311
- - `destroy()` restores every patched global and every axios adapter
312
-
313
- ## The console
314
-
315
- ```bash
316
- npx api-tracer scan # read the source, write the endpoint catalog
317
- npx api-tracer serve # the UI, at http://127.0.0.1:4400
318
- npx api-tracer start # both
319
- npx api-tracer report # the insight report, Markdown or JSON
320
- npx api-tracer init # write a config file, if the guesses need help
321
- ```
322
-
323
- ### How it finds your endpoints
324
-
325
- It runs every scanner preset over your codebase and keeps whichever actually
326
- reads it:
327
-
328
- | preset | what it reads |
329
- | --- | --- |
330
- | `service-object` | `const request = { subUrl: '/users' }; return get(request)` |
331
- | `axios-direct` | `axios.get('/users')`, `api.post(\`/users/${id}\`, body)` |
332
- | `fetch-direct` | `fetch('/api/users', { method: 'POST' })` |
333
-
334
- Base URLs and the auth header name are read from your source too, so your
335
- environments stay in sync with the app rather than being copied into a config.
336
-
337
- If none of the presets fit, supply your own in `api-tracer.config.mjs`:
338
-
339
- ```js
340
- export default {
341
- parse: (src, config) => [
342
- { name, method, subUrl, usesParams, usesData, chunk, index },
343
- ],
344
- };
345
- ```
346
-
347
- ### Config file
348
-
349
- Optional. `api-tracer init` writes one filled in with what the scan worked out.
350
- Every field has a default:
351
-
352
- ```js
353
- export default {
354
- name: 'My API',
355
- preset: 'service-object',
356
- sources: ['src'],
357
- baseUrls: { dev: 'https://dev.example.com/api' },
358
- auth: { header: 'Authorization' },
359
- envelope: { codeFields: ['status', 'code'], okField: 'success', failFrom: 400 },
360
- login: { steps: [/* an optional multi-step sign-in chain */] },
361
- };
362
- ```
363
-
364
- ### Environment variables
365
-
366
- | | |
367
- | --- | --- |
368
- | `API_TRACER_DATA` | where captures are kept (default `<project>/.api-tracer`) |
369
- | `PORT` / `HOST` | default `4400` / `127.0.0.1` |
370
- | `BASE_PATH` | serve under a path, e.g. `/api-console`, behind a proxy |
371
- | `ALLOWED_ORIGINS` | comma-separated origins allowed to post recordings |
372
- | `CONSOLE_USER` / `CONSOLE_PASS` | basic auth on everything except `/api/record` |
373
- | `LOCK_RECORDING` / `LOCK_ENV` | stop one person's click changing a shared console |
374
-
375
- ## Security
376
-
377
- The tracer sees whatever your API sees, which on some products means
378
- credentials and personal data. So:
379
-
380
- - `Authorization`, `Cookie`, `Set-Cookie`, `X-API-Key` and the usual token
381
- header names are redacted by default, and the list is configurable
382
- - any field whose **name** looks like a credential (`token`, `password`,
383
- `secret`, `api_key`, `otp`, …) is replaced with `<redacted>`, keeping the
384
- surrounding shape so the sample stays useful
385
- - redaction happens before a trace is stored, so a redacted value is never held
386
- in memory either
387
- - the console redacts again before writing anything to disk
388
- - HAR, cURL and Postman exports carry `<paste your token>` in place of a real
389
- token, so an export is safe to attach to a ticket
390
-
391
- The console's data directory holds captured request and response bodies from a
392
- real environment. Treat it as sensitive: keep it out of version control (it is
393
- `.api-tracer/`, add it to `.gitignore`) and keep a deployed console behind VPN
394
- or SSO.
156
+ The full list is in [Tracer API](./docs/tracer.md#what-the-tracer-will-not-do-to-your-app).
395
157
 
396
158
  ## Limitations
397
159
 
@@ -399,52 +161,23 @@ Worth knowing before you rely on it.
399
161
 
400
162
  - **An axios instance must be handed over.** There is no global to patch. In a
401
163
  browser the XHR adapter catches axios anyway; in Node it cannot.
402
- - **A request body that is a stream is never read**, because reading it would
403
- empty it. It is recorded as `omitted: 'stream'`.
164
+ - **Request bodies that are streams are never read**, because reading one would
165
+ empty it.
404
166
  - **Files in a `FormData` are described, not captured**, so a captured upload
405
167
  cannot be replayed with its file.
406
168
  - **Response bodies over `maxBodyBytes` are omitted**, not truncated — a partial
407
169
  JSON body is more misleading than none.
408
170
  - **`Response.clone()` buffers.** For a large non-streaming response the clone
409
171
  holds a second copy until it is read. The size cap keeps this bounded.
410
- - **The scanner is regex-based.** It reads the three shapes above well and says
172
+ - **The scanner is regex-based.** It reads three common shapes well and says
411
173
  what it could not read; it does not understand a URL assembled from three
412
- variables. Live recording covers what the scan misses — an unexplained call
413
- becomes an endpoint of its own.
174
+ variables. Live recording covers the rest.
414
175
  - **The "unused endpoint" count is a substring count**, so `import * as services`
415
- or a re-export would hide a real usage. Confirm with a grep before deleting.
416
- - **The console is single-user by design**: one token, one selected environment,
417
- one set of results.
418
-
419
- ## Architecture
420
-
421
- ```
422
- api-tracer-kit
423
- ├── src/
424
- │ ├── core/ tracer, data model, redaction, bodies, URLs, envelopes, runtime detection
425
- │ ├── adapters/ fetch, xhr, axios, and the suppression that stops double-counting
426
- │ ├── storage/ the TraceStorage interface and the capped in-memory default
427
- │ ├── transport/ posting finished traces to a console
428
- │ ├── ui/ the React panel (optional entry point)
429
- │ ├── index.ts the public API
430
- │ ├── axios.ts api-tracer-kit/axios
431
- │ └── react.ts api-tracer-kit/react
432
- └── cli/
433
- ├── bin/ the api-tracer command
434
- ├── presets.mjs how to find endpoints in a codebase
435
- ├── config.mjs defaults, and guessing what was not configured
436
- ├── scan.mjs source -> endpoint catalog
437
- ├── server.mjs the console: forwards calls, replays, exports, records
438
- ├── import.mjs HAR and cURL, matched back to the catalog
439
- ├── shape.mjs response shapes and drift diffing
440
- ├── report.mjs the insight report model and its Markdown export
441
- └── web/ the dashboard (no framework, no build step)
442
- ```
443
-
444
- The core is framework-free and knows nothing about any particular application.
445
- The console knows nothing about it either: the catalog supplies the endpoints,
446
- base URLs, auth header and envelope convention, and the config file supplies the
447
- sign-in flow if there is one worth automating.
176
+ would hide a real usage. Confirm with a grep before deleting.
177
+ - **The console is single-user by design**: one token, one environment, one set
178
+ of results.
179
+ - **Redaction is name-based.** A field called `notes` containing personal data is
180
+ not a credential and is not touched. See [Security](./docs/security.md).
448
181
 
449
182
  ## Development
450
183
 
@@ -461,6 +194,8 @@ The example project doubles as an end-to-end check:
461
194
  cd examples/basic && npm install && npm start
462
195
  ```
463
196
 
197
+ See [CONTRIBUTING.md](./CONTRIBUTING.md).
198
+
464
199
  ## Licence
465
200
 
466
201
  MIT
@@ -13,8 +13,19 @@
13
13
  * rather than a blank page.
14
14
  */
15
15
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
16
- import { join, resolve } from 'node:path';
17
- import { loadConfig, guessConfig, defaults, CONFIG_NAMES } from '../config.mjs';
16
+ import { join, resolve, relative, dirname } from 'node:path';
17
+ import { loadConfig, guessConfig, defaults, CONFIG_NAMES, collectFiles } from '../config.mjs';
18
+ import {
19
+ FRAMEWORKS,
20
+ addImport,
21
+ detectFramework,
22
+ ensureGitignore,
23
+ findEntry,
24
+ findExistingInits,
25
+ recorderSource,
26
+ usesAxios,
27
+ usesTypeScript,
28
+ } from '../setup.mjs';
18
29
  import { scanToFile, scanProject } from '../scan.mjs';
19
30
  import { buildReport, toMarkdown } from '../report.mjs';
20
31
 
@@ -148,6 +159,76 @@ async function init() {
148
159
  say('Nothing else is needed: `api-tracer start` works from here.');
149
160
  }
150
161
 
162
+ /* ------------------------------------------------------------------ setup */
163
+
164
+ async function setup() {
165
+ const framework = String(flag('framework', detectFramework(ROOT)));
166
+ if (!FRAMEWORKS[framework]) {
167
+ say(`unknown framework "${framework}"; expected one of: ${Object.keys(FRAMEWORKS).join(', ')}`);
168
+ process.exit(1);
169
+ }
170
+
171
+ const axios = flag('axios') !== undefined ? flag('axios') !== 'false' : usesAxios(ROOT);
172
+ const ts = usesTypeScript(ROOT);
173
+ const out = String(flag('out', `src/apiTracer.${ts ? 'ts' : 'js'}`));
174
+ const entry = flag('entry') ? String(flag('entry')) : findEntry(ROOT, framework);
175
+
176
+ say(`project ${ROOT}`);
177
+ say(`framework ${FRAMEWORKS[framework].label}${flag('framework') ? ' (given)' : ' (detected)'}`);
178
+ say(`axios ${axios ? 'yes, will be attached' : 'no'}`);
179
+
180
+ /*
181
+ * A second init() elsewhere would keep its own configuration and silently
182
+ * ignore this one's, so say so before writing rather than after.
183
+ */
184
+ const existing = findExistingInits(ROOT, collectFiles(ROOT, defaults).map((f) => relative(ROOT, f)));
185
+ const notOurs = existing.filter((f) => f !== out);
186
+ if (notOurs.length) {
187
+ say('');
188
+ say(`Something already calls apiTracer.init():`);
189
+ for (const f of notOurs) say(` ${f}`);
190
+ say('The tracer must be configured in one place -- remove those calls, or this');
191
+ say('file will be the one whose options are ignored.');
192
+ }
193
+
194
+ const target = join(ROOT, out);
195
+ if (existsSync(target) && !flag('force')) {
196
+ say('');
197
+ say(`${out} already exists; pass --force to overwrite it`);
198
+ } else {
199
+ writeFileSync(target, recorderSource({ framework, axios, envelope: flag('envelope') !== 'false' }));
200
+ say('');
201
+ say(`wrote ${out}`);
202
+ }
203
+
204
+ if (!entry) {
205
+ say('');
206
+ say(`Could not find an entry file. Add this line to yours yourself:`);
207
+ say(` import "./${out.replace(/^src\//, '').replace(/\.[jt]sx?$/, '')}";`);
208
+ } else {
209
+ const entryPath = join(ROOT, entry);
210
+ const source = readFileSync(entryPath, 'utf8');
211
+ // a relative specifier from the entry file to the module we just wrote
212
+ let spec = relative(dirname(entryPath), target).replace(/\.[jt]sx?$/, '');
213
+ if (!spec.startsWith('.')) spec = `./${spec}`;
214
+
215
+ const { source: next, added } = addImport(source, spec);
216
+ if (added) {
217
+ writeFileSync(entryPath, next);
218
+ say(`imported ${entry} -> import "${spec}"`);
219
+ } else {
220
+ say(`already imported from ${entry}`);
221
+ }
222
+ }
223
+
224
+ if (ensureGitignore(ROOT)) say('gitignored .api-tracer/');
225
+
226
+ say('');
227
+ say('Next:');
228
+ say(' npx api-tracer start # scan your source and open the console');
229
+ say(' <start your app> # its calls stream in as you click');
230
+ }
231
+
151
232
  /* ----------------------------------------------------------------- report */
152
233
 
153
234
  async function report() {
@@ -223,6 +304,7 @@ const HELP = `api-tracer
223
304
  api-tracer serve run the console (default http://127.0.0.1:4400)
224
305
  api-tracer start scan, then serve
225
306
  api-tracer report build the insight report
307
+ api-tracer setup wire the tracer into this app, in one command
226
308
  api-tracer init write a config file from what the scan guessed
227
309
 
228
310
  Options
@@ -235,11 +317,20 @@ Options
235
317
  --out <file> write the report to a file (default: stdout)
236
318
  --env <name> which base URL the report describes
237
319
 
320
+ setup options
321
+ --framework <name> cra | vite | next | node (default: detected)
322
+ --entry <file> the entry file to import from (default: detected)
323
+ --out <file> where to write the module (default: src/apiTracer.js)
324
+ --axios false do not attach axios
325
+ --envelope false the API reports failure in the status line, not the body
326
+ --force overwrite an existing file
327
+
238
328
  No configuration file is required.
239
329
  `;
240
330
 
241
331
  const commands = {
242
332
  scan,
333
+ setup,
243
334
  init,
244
335
  report,
245
336
  serve,