@avivbenhamo/polly-kit 0.8.0 → 0.9.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/README.md +12 -2
- package/dist/cli.d.ts +46 -0
- package/dist/cli.js +117 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -71,7 +71,17 @@ body, so a bare `startPolly()` call in `index.tsx` runs *after* `./App` and ever
|
|
|
71
71
|
imports — and any module that fires a request at import time would escape interception,
|
|
72
72
|
silently and only sometimes.
|
|
73
73
|
|
|
74
|
-
Add
|
|
74
|
+
Add the recordings directory to `.gitignore` — a HAR holds complete response bodies:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
npx polly-kit init # adds /recordings, or /apps/web/recordings
|
|
78
|
+
npx polly-kit init --dir fixtures/har # when that is not where you record
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Run it from the directory the app records in. The entry is anchored at the repository root
|
|
82
|
+
and written out in full — `/apps/web/recordings` from a package inside a monorepo, not
|
|
83
|
+
`/recordings`, which would match nothing there and look like it had. An entry already in the
|
|
84
|
+
file, in any spelling git honours, is left alone; running it twice adds one line.
|
|
75
85
|
|
|
76
86
|
## Running it
|
|
77
87
|
|
|
@@ -135,7 +145,7 @@ $env:POLLY_MODE='record'; npm run start:dev
|
|
|
135
145
|
```
|
|
136
146
|
|
|
137
147
|
No Vite plugin and no recording store: `persister-fs` writes the HAR straight to disk.
|
|
138
|
-
|
|
148
|
+
`npx polly-kit init` gitignores the directory.
|
|
139
149
|
|
|
140
150
|
**Use `{kind: 'only'}`, not `{kind: 'all'}`.** In a browser Polly sees the page's own
|
|
141
151
|
requests. In Node the adapters are process-wide: they also see your service talking to
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
export declare function main(argv: string[]): Promise<number>;
|
|
3
|
+
export interface IgnoreOptions {
|
|
4
|
+
/** Where the command was run. The repository is found by walking up from here. */
|
|
5
|
+
cwd?: string;
|
|
6
|
+
/** The recordings directory, as passed to `startPolly`. Default `recordings`. */
|
|
7
|
+
dir?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface IgnoreResult {
|
|
10
|
+
/**
|
|
11
|
+
* `added` — the entry was appended.
|
|
12
|
+
* `covered` — a rule already matching the directory was there; nothing written.
|
|
13
|
+
* `outside` — the directory is not under the repository, so no rule can name it.
|
|
14
|
+
* `root` — the directory *is* the repository, which is not something to ignore.
|
|
15
|
+
*/
|
|
16
|
+
action: 'added' | 'covered' | 'outside' | 'root';
|
|
17
|
+
/** The `.gitignore` written, or the one that would have been. */
|
|
18
|
+
path: string;
|
|
19
|
+
/** The entry, as written or as it would be written. Empty when `outside`. */
|
|
20
|
+
entry: string;
|
|
21
|
+
/** The repository root this was resolved against. */
|
|
22
|
+
root: string;
|
|
23
|
+
/** The recordings directory, absolute. */
|
|
24
|
+
recordingsDir: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Adds the recordings directory to the repository's `.gitignore`.
|
|
28
|
+
*
|
|
29
|
+
* Two things here are deliberate and were the reason for the command.
|
|
30
|
+
*
|
|
31
|
+
* The entry is anchored at the repository root and spelled out in full —
|
|
32
|
+
* `/apps/web/recordings`, not `/recordings` — because the recordings directory
|
|
33
|
+
* is configured in the app's own code and so lives wherever the app does. A bare
|
|
34
|
+
* `/recordings` at the root of a monorepo matches nothing in `apps/web`, and the
|
|
35
|
+
* failure is silent: the file looks like it handled them and the HARs get
|
|
36
|
+
* committed anyway.
|
|
37
|
+
*
|
|
38
|
+
* Coverage is checked against every spelling git would honour, because the entry
|
|
39
|
+
* has been written by hand from the README for longer than this command has
|
|
40
|
+
* existed, and this package's own docs have asked for it as both `/recordings`
|
|
41
|
+
* and `recordings/`. `recordings` and `recordings/` have no leading slash, so git
|
|
42
|
+
* matches them at any depth and they already cover the directory — appending a
|
|
43
|
+
* second rule under one of them is a duplicate somebody then has to delete.
|
|
44
|
+
*
|
|
45
|
+
* Negated (`!recordings`) and commented-out entries are not coverage. Reading
|
|
46
|
+
* either as "already handled" would leave the recordings committed.
|
|
47
|
+
*/
|
|
48
|
+
export declare function ignoreRecordings(options?: IgnoreOptions): IgnoreResult;
|
|
3
49
|
export interface StudioOptions {
|
|
4
50
|
recordingsDir: string;
|
|
5
51
|
port?: number;
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* The
|
|
3
|
+
* The two things that are done to a checkout rather than inside an app: serving
|
|
4
|
+
* the studio with no app running (`studio`), and keeping the recordings out of
|
|
5
|
+
* git (`init`).
|
|
6
|
+
*
|
|
7
|
+
* On `init`, see `ignoreRecordings`. The rest of this is the studio.
|
|
4
8
|
*
|
|
5
9
|
* The mounted form stays the default and is the better one when there *is* a dev
|
|
6
10
|
* server: `app.use('/__polly', pollyUi(...))` — or the Vite plugin, which does
|
|
@@ -18,8 +22,8 @@
|
|
|
18
22
|
* see those.
|
|
19
23
|
*/
|
|
20
24
|
import { createServer } from 'node:http';
|
|
21
|
-
import { existsSync } from 'node:fs';
|
|
22
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
25
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
23
27
|
import { fileURLToPath } from 'node:url';
|
|
24
28
|
import { pollyUi } from './studio/server.js';
|
|
25
29
|
const DEFAULT_PORT = 7788;
|
|
@@ -33,11 +37,16 @@ const PORT_TRIES = 20;
|
|
|
33
37
|
const USAGE = `polly-kit — Polly recordings, in a browser
|
|
34
38
|
|
|
35
39
|
npx polly-kit studio [--dir DIR] [--port PORT]
|
|
40
|
+
npx polly-kit init [--dir DIR]
|
|
36
41
|
|
|
37
42
|
studio Serve the recording editor on 127.0.0.1. Reads and writes the HAR
|
|
38
43
|
files under DIR (default ${DEFAULT_DIR}), the same ones the mounted
|
|
39
44
|
editor edits.
|
|
40
45
|
|
|
46
|
+
init Add DIR to the repository's .gitignore. A HAR holds complete
|
|
47
|
+
request and response bodies, so recordings are not something to
|
|
48
|
+
commit by accident. Safe to run twice.
|
|
49
|
+
|
|
41
50
|
Mounted in a dev server instead — no second port, and it stops when the server
|
|
42
51
|
does — this is the Vite plugin, or app.use('/__polly', pollyUi({recordingsDir})).
|
|
43
52
|
Recordings held in a browser's localStorage are reachable only from the page
|
|
@@ -49,6 +58,24 @@ export async function main(argv) {
|
|
|
49
58
|
process.stdout.write(USAGE);
|
|
50
59
|
return 0;
|
|
51
60
|
}
|
|
61
|
+
if (command === 'init' || command === 'setup') {
|
|
62
|
+
const result = ignoreRecordings({ dir: flag(argv, 'dir') });
|
|
63
|
+
// Every outcome is exit 0. This runs from a README in some trees and from a
|
|
64
|
+
// postinstall in others, and "already done" is the steady state in both.
|
|
65
|
+
if (result.action === 'added') {
|
|
66
|
+
process.stdout.write(`added to ${result.path}: ${result.entry}\n`);
|
|
67
|
+
}
|
|
68
|
+
else if (result.action === 'covered') {
|
|
69
|
+
process.stdout.write(`${result.entry} is already ignored by ${result.path}\n`);
|
|
70
|
+
}
|
|
71
|
+
else if (result.action === 'root') {
|
|
72
|
+
process.stdout.write(`${result.root} is the repository root, not a recordings directory — pass --dir\n`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
process.stdout.write(`${result.recordingsDir} is outside ${result.root} — nothing to ignore there\n`);
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
52
79
|
if (command !== 'studio') {
|
|
53
80
|
process.stderr.write(`unknown command: ${command}\n\n${USAGE}`);
|
|
54
81
|
return 2;
|
|
@@ -79,6 +106,93 @@ export async function main(argv) {
|
|
|
79
106
|
await studio.close();
|
|
80
107
|
return 0;
|
|
81
108
|
}
|
|
109
|
+
const IGNORE_COMMENT = '# @avivbenhamo/polly-kit: recorded traffic. A HAR holds complete request and\n# response bodies, including anything the responses carried.';
|
|
110
|
+
/**
|
|
111
|
+
* Adds the recordings directory to the repository's `.gitignore`.
|
|
112
|
+
*
|
|
113
|
+
* Two things here are deliberate and were the reason for the command.
|
|
114
|
+
*
|
|
115
|
+
* The entry is anchored at the repository root and spelled out in full —
|
|
116
|
+
* `/apps/web/recordings`, not `/recordings` — because the recordings directory
|
|
117
|
+
* is configured in the app's own code and so lives wherever the app does. A bare
|
|
118
|
+
* `/recordings` at the root of a monorepo matches nothing in `apps/web`, and the
|
|
119
|
+
* failure is silent: the file looks like it handled them and the HARs get
|
|
120
|
+
* committed anyway.
|
|
121
|
+
*
|
|
122
|
+
* Coverage is checked against every spelling git would honour, because the entry
|
|
123
|
+
* has been written by hand from the README for longer than this command has
|
|
124
|
+
* existed, and this package's own docs have asked for it as both `/recordings`
|
|
125
|
+
* and `recordings/`. `recordings` and `recordings/` have no leading slash, so git
|
|
126
|
+
* matches them at any depth and they already cover the directory — appending a
|
|
127
|
+
* second rule under one of them is a duplicate somebody then has to delete.
|
|
128
|
+
*
|
|
129
|
+
* Negated (`!recordings`) and commented-out entries are not coverage. Reading
|
|
130
|
+
* either as "already handled" would leave the recordings committed.
|
|
131
|
+
*/
|
|
132
|
+
export function ignoreRecordings(options = {}) {
|
|
133
|
+
const cwd = options.cwd ?? process.cwd();
|
|
134
|
+
const dir = options.dir ?? DEFAULT_DIR;
|
|
135
|
+
const recordingsDir = isAbsolute(dir) ? resolve(dir) : resolve(cwd, dir);
|
|
136
|
+
// No git command is run: `.git` is a directory in a normal checkout and a file
|
|
137
|
+
// in a worktree or submodule, and either one marks the root. Falling back to
|
|
138
|
+
// cwd covers the directory that is not a repository yet.
|
|
139
|
+
const root = gitRoot(cwd) ?? resolve(cwd);
|
|
140
|
+
const path = join(root, '.gitignore');
|
|
141
|
+
const within = relative(root, recordingsDir);
|
|
142
|
+
if (within === '') {
|
|
143
|
+
return { action: 'root', path, entry: '', root, recordingsDir };
|
|
144
|
+
}
|
|
145
|
+
if (within.startsWith('..') || isAbsolute(within)) {
|
|
146
|
+
return { action: 'outside', path, entry: '', root, recordingsDir };
|
|
147
|
+
}
|
|
148
|
+
const entry = `/${within.split(sep).join('/')}`;
|
|
149
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
150
|
+
if (covers(existing, entry)) {
|
|
151
|
+
return { action: 'covered', path, entry, root, recordingsDir };
|
|
152
|
+
}
|
|
153
|
+
// The file's own line ending, not the platform's: git on Windows checks out a
|
|
154
|
+
// CRLF `.gitignore`, and appending LF lines to it shows the whole file as
|
|
155
|
+
// changed in the diff.
|
|
156
|
+
const eol = existing.includes('\r\n') ? '\r\n' : '\n';
|
|
157
|
+
const lead = existing === '' || /(\r?\n){2}$/.test(existing)
|
|
158
|
+
? ''
|
|
159
|
+
: /\r?\n$/.test(existing)
|
|
160
|
+
? eol
|
|
161
|
+
: `${eol}${eol}`;
|
|
162
|
+
const block = `${IGNORE_COMMENT}\n${entry}\n`.replace(/\n/g, eol);
|
|
163
|
+
writeFileSync(path, `${existing}${lead}${block}`, 'utf8');
|
|
164
|
+
return { action: 'added', path, entry, root, recordingsDir };
|
|
165
|
+
}
|
|
166
|
+
/** Nearest ancestor of `from` holding a `.git`, itself included. */
|
|
167
|
+
function gitRoot(from) {
|
|
168
|
+
let at = resolve(from);
|
|
169
|
+
for (;;) {
|
|
170
|
+
if (existsSync(join(at, '.git')))
|
|
171
|
+
return at;
|
|
172
|
+
const up = dirname(at);
|
|
173
|
+
if (up === at)
|
|
174
|
+
return undefined;
|
|
175
|
+
at = up;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Does any rule in `contents` already ignore `entry` (`/a/b/recordings`)? */
|
|
179
|
+
function covers(contents, entry) {
|
|
180
|
+
const wanted = entry.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
181
|
+
const leaf = wanted.slice(wanted.lastIndexOf('/') + 1);
|
|
182
|
+
return contents.split(/\r?\n/).some((raw) => {
|
|
183
|
+
const line = raw.trim();
|
|
184
|
+
if (line === '' || line.startsWith('#') || line.startsWith('!'))
|
|
185
|
+
return false;
|
|
186
|
+
// A leading `**/` is git's explicit spelling of "at any depth", which is
|
|
187
|
+
// what a bare name means anyway.
|
|
188
|
+
const rule = line.replace(/^\*\*\//, '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
189
|
+
// Anchored by a leading slash, or by a slash anywhere inside it: git matches
|
|
190
|
+
// those against the path from the .gitignore's own directory, so it has to be
|
|
191
|
+
// the same path. Otherwise the rule matches at every depth and the last
|
|
192
|
+
// segment of the path is enough.
|
|
193
|
+
return line.startsWith('/') || rule.includes('/') ? rule === wanted : rule === leaf;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
82
196
|
/**
|
|
83
197
|
* The server, without the process around it.
|
|
84
198
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avivbenhamo/polly-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Drop-in record/replay of an app's HTTP traffic with Polly.JS, plus the polly-studio editor. Bring your own host policy.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"dist",
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
|
-
"//bin": "
|
|
15
|
+
"//bin": "`studio` on its own, for a checkout with no dev server to mount it in — the mounted form stays the default — and `init`, which gitignores the recordings directory.",
|
|
16
16
|
"bin": {
|
|
17
17
|
"polly-kit": "./dist/cli.js"
|
|
18
18
|
},
|