@xyph3r/rate-limiter 0.1.0 → 0.1.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/README.md +268 -92
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
# `@xyph3r/rate-limiter`
|
|
2
2
|
|
|
3
|
-
Framework-agnostic rate limiting for Node.js and fetch-based runtimes
|
|
3
|
+
Framework-agnostic rate limiting for Node.js and fetch-based runtimes.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- keep the internals structured enough to stay maintainable when requirements change
|
|
5
|
+
This package is for the common cases people actually need:
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
- protect a public API by IP or API key
|
|
8
|
+
- slow down abusive login attempts
|
|
9
|
+
- apply per-tenant or per-user quotas
|
|
10
|
+
- add rate-limit headers to HTTP responses
|
|
11
|
+
- use Redis when you run more than one app instance
|
|
12
|
+
|
|
13
|
+
The package is intentionally small:
|
|
9
14
|
|
|
10
15
|
- `Strategy`: sliding window and token bucket are swappable algorithms
|
|
11
|
-
- `Builder`:
|
|
12
|
-
- `Decorator`:
|
|
13
|
-
- `
|
|
14
|
-
- `Proxy`: short-lived decision caching for hot paths
|
|
16
|
+
- `Builder`: setup stays readable as configuration grows
|
|
17
|
+
- `Decorator`: framework adapters wrap the core without coupling it to Express, Fastify, Hono, or Next
|
|
18
|
+
- `Proxy`: optional short-lived caching reduces repeated checks on hot paths
|
|
15
19
|
|
|
16
20
|
## Install
|
|
17
21
|
|
|
@@ -19,50 +23,69 @@ The package is organized around a few deliberate design choices:
|
|
|
19
23
|
npm install @xyph3r/rate-limiter
|
|
20
24
|
```
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
or
|
|
23
27
|
|
|
24
|
-
```
|
|
25
|
-
|
|
28
|
+
```bash
|
|
29
|
+
bun add @xyph3r/rate-limiter
|
|
30
|
+
```
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
.forSlidingWindow({ limit: 100, windowMs: 60_000 })
|
|
29
|
-
.withMemoryStore()
|
|
30
|
-
.withKeyPrefix("api")
|
|
31
|
-
.build();
|
|
32
|
+
## When to use which algorithm
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
### Sliding window
|
|
35
|
+
|
|
36
|
+
Use sliding window when you want a straightforward limit like:
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
- `100 requests per minute`
|
|
39
|
+
- `5 login attempts per 10 minutes`
|
|
40
|
+
- `30 requests per second`
|
|
41
|
+
|
|
42
|
+
This is usually the default choice for HTTP APIs.
|
|
43
|
+
|
|
44
|
+
### Token bucket
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
Use token bucket when you want bursts to be allowed but still controlled over time.
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
|
|
50
|
+
- allow a short burst of `20` requests, then refill at `5/second`
|
|
51
|
+
- smooth traffic for jobs or background workers
|
|
52
|
+
- tolerate small spikes without rejecting immediately
|
|
53
|
+
|
|
54
|
+
## Production advice
|
|
55
|
+
|
|
56
|
+
- `MemoryStore` is process-local. Use it for local dev, tests, or single-instance apps.
|
|
57
|
+
- `RedisStore` is the right choice for multi-instance deployments.
|
|
58
|
+
- Rate limiting only works as well as your key choice. Pick a stable identity: IP, API key, tenant ID, or user ID.
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
39
61
|
|
|
40
62
|
```ts
|
|
41
|
-
import
|
|
63
|
+
import express from "express";
|
|
64
|
+
import {
|
|
65
|
+
createExpressRateLimit,
|
|
66
|
+
RateLimiterBuilder,
|
|
67
|
+
} from "@xyph3r/rate-limiter";
|
|
68
|
+
|
|
69
|
+
const app = express();
|
|
42
70
|
|
|
43
71
|
const limiter = new RateLimiterBuilder()
|
|
44
72
|
.forSlidingWindow({ limit: 100, windowMs: 60_000 })
|
|
45
73
|
.withMemoryStore()
|
|
74
|
+
.withKeyPrefix("api")
|
|
46
75
|
.build();
|
|
47
76
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Bun.serve({
|
|
51
|
-
fetch: withRateLimit(async () => {
|
|
52
|
-
return Response.json({ ok: true });
|
|
53
|
-
}),
|
|
54
|
-
});
|
|
77
|
+
app.use(createExpressRateLimit(limiter));
|
|
55
78
|
```
|
|
56
79
|
|
|
57
80
|
## Core usage
|
|
58
81
|
|
|
59
|
-
|
|
82
|
+
Use the core API when you want rate-limit decisions outside HTTP middleware.
|
|
60
83
|
|
|
61
84
|
```ts
|
|
62
|
-
import {
|
|
85
|
+
import { RateLimiterBuilder } from "@xyph3r/rate-limiter";
|
|
63
86
|
|
|
64
87
|
const limiter = new RateLimiterBuilder()
|
|
65
|
-
.
|
|
88
|
+
.withMemoryStore()
|
|
66
89
|
.forSlidingWindow({
|
|
67
90
|
limit: 10,
|
|
68
91
|
windowMs: 1_000,
|
|
@@ -76,7 +99,7 @@ if (!decision.allowed) {
|
|
|
76
99
|
}
|
|
77
100
|
```
|
|
78
101
|
|
|
79
|
-
### Token bucket
|
|
102
|
+
### Token bucket example
|
|
80
103
|
|
|
81
104
|
```ts
|
|
82
105
|
import { createRateLimiter } from "@xyph3r/rate-limiter";
|
|
@@ -92,7 +115,7 @@ const limiter = createRateLimiter({
|
|
|
92
115
|
|
|
93
116
|
## Redis-backed usage
|
|
94
117
|
|
|
95
|
-
The package does not force a Redis client.
|
|
118
|
+
The package does not force a Redis client. It ships small executors for the common client shapes.
|
|
96
119
|
|
|
97
120
|
### `node-redis`
|
|
98
121
|
|
|
@@ -104,13 +127,11 @@ import {
|
|
|
104
127
|
RateLimiterBuilder,
|
|
105
128
|
} from "@xyph3r/rate-limiter";
|
|
106
129
|
|
|
107
|
-
const client = createClient();
|
|
130
|
+
const client = createClient({ url: process.env.REDIS_URL });
|
|
108
131
|
await client.connect();
|
|
109
132
|
|
|
110
|
-
const store = new RedisStore(createNodeRedisExecutor(client));
|
|
111
|
-
|
|
112
133
|
const limiter = new RateLimiterBuilder()
|
|
113
|
-
.useStore(
|
|
134
|
+
.useStore(new RedisStore(createNodeRedisExecutor(client)))
|
|
114
135
|
.forSlidingWindow({ limit: 200, windowMs: 60_000 })
|
|
115
136
|
.build();
|
|
116
137
|
```
|
|
@@ -126,114 +147,186 @@ import {
|
|
|
126
147
|
} from "@xyph3r/rate-limiter";
|
|
127
148
|
|
|
128
149
|
const client = new Redis(process.env.REDIS_URL!);
|
|
129
|
-
|
|
150
|
+
|
|
151
|
+
const limiter = new RateLimiterBuilder()
|
|
152
|
+
.useStore(new RedisStore(createIORedisExecutor(client)))
|
|
153
|
+
.forSlidingWindow({ limit: 200, windowMs: 60_000 })
|
|
154
|
+
.build();
|
|
130
155
|
```
|
|
131
156
|
|
|
132
|
-
##
|
|
157
|
+
## Framework usage
|
|
158
|
+
|
|
159
|
+
### Express
|
|
133
160
|
|
|
134
|
-
|
|
161
|
+
Use `createExpressRateLimit()` as middleware.
|
|
135
162
|
|
|
136
163
|
```ts
|
|
137
|
-
import
|
|
164
|
+
import express from "express";
|
|
165
|
+
import {
|
|
166
|
+
createExpressRateLimit,
|
|
167
|
+
RateLimiterBuilder,
|
|
168
|
+
} from "@xyph3r/rate-limiter";
|
|
138
169
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
170
|
+
const app = express();
|
|
171
|
+
|
|
172
|
+
const limiter = new RateLimiterBuilder()
|
|
173
|
+
.withMemoryStore()
|
|
174
|
+
.forSlidingWindow({ limit: 50, windowMs: 60_000 })
|
|
175
|
+
.build();
|
|
176
|
+
|
|
177
|
+
app.use(
|
|
178
|
+
createExpressRateLimit(limiter, {
|
|
179
|
+
key: (request) =>
|
|
180
|
+
request.headers["x-api-key"] as string || request.ip || "anonymous",
|
|
181
|
+
skip: (request) => request.headers["x-internal-call"] === "1",
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
146
184
|
```
|
|
147
185
|
|
|
148
|
-
|
|
186
|
+
Use this for:
|
|
187
|
+
|
|
188
|
+
- public REST APIs
|
|
189
|
+
- login and auth routes
|
|
190
|
+
- per-customer API key limits
|
|
191
|
+
|
|
192
|
+
### Fastify
|
|
193
|
+
|
|
194
|
+
Use `createFastifyRateLimit()` in `preHandler`.
|
|
149
195
|
|
|
150
196
|
```ts
|
|
151
|
-
import
|
|
197
|
+
import Fastify from "fastify";
|
|
152
198
|
import {
|
|
153
|
-
|
|
199
|
+
createFastifyRateLimit,
|
|
154
200
|
RateLimiterBuilder,
|
|
155
201
|
} from "@xyph3r/rate-limiter";
|
|
156
202
|
|
|
157
|
-
const app =
|
|
203
|
+
const app = Fastify();
|
|
158
204
|
|
|
159
205
|
const limiter = new RateLimiterBuilder()
|
|
206
|
+
.withMemoryStore()
|
|
160
207
|
.forTokenBucket({
|
|
161
208
|
capacity: 30,
|
|
162
209
|
refillRate: 10,
|
|
163
210
|
refillIntervalMs: 1_000,
|
|
164
211
|
})
|
|
165
|
-
.withMemoryStore()
|
|
166
212
|
.build();
|
|
167
213
|
|
|
168
|
-
app.
|
|
169
|
-
|
|
170
|
-
|
|
214
|
+
app.addHook(
|
|
215
|
+
"preHandler",
|
|
216
|
+
createFastifyRateLimit(limiter, {
|
|
217
|
+
key: (request) =>
|
|
218
|
+
request.headers["x-api-key"] as string || request.ip || "anonymous",
|
|
171
219
|
}),
|
|
172
220
|
);
|
|
173
221
|
```
|
|
174
222
|
|
|
175
|
-
|
|
223
|
+
### Fetch / Bun / standard `Request` handlers
|
|
224
|
+
|
|
225
|
+
Use `createFetchRateLimit()` when your runtime already uses the standard `Request -> Response` shape.
|
|
176
226
|
|
|
177
227
|
```ts
|
|
178
|
-
import { Hono } from "hono";
|
|
179
228
|
import {
|
|
180
|
-
|
|
229
|
+
createFetchRateLimit,
|
|
181
230
|
RateLimiterBuilder,
|
|
182
231
|
} from "@xyph3r/rate-limiter";
|
|
183
232
|
|
|
184
|
-
const
|
|
233
|
+
const limiter = new RateLimiterBuilder()
|
|
234
|
+
.withMemoryStore()
|
|
235
|
+
.forSlidingWindow({ limit: 100, windowMs: 60_000 })
|
|
236
|
+
.build();
|
|
237
|
+
|
|
238
|
+
const withRateLimit = createFetchRateLimit(limiter, {
|
|
239
|
+
key: (request) => request.headers.get("x-api-key") ?? "anonymous",
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const handler = withRateLimit(async () => {
|
|
243
|
+
return Response.json({ ok: true });
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### Bun example
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import {
|
|
251
|
+
createFetchRateLimit,
|
|
252
|
+
RateLimiterBuilder,
|
|
253
|
+
} from "@xyph3r/rate-limiter";
|
|
185
254
|
|
|
186
255
|
const limiter = new RateLimiterBuilder()
|
|
187
|
-
.forSlidingWindow({ limit: 20, windowMs: 1_000 })
|
|
188
256
|
.withMemoryStore()
|
|
257
|
+
.forSlidingWindow({ limit: 100, windowMs: 60_000 })
|
|
189
258
|
.build();
|
|
190
259
|
|
|
191
|
-
|
|
260
|
+
const withRateLimit = createFetchRateLimit(limiter, {
|
|
261
|
+
key: (request) => request.headers.get("x-forwarded-for") ?? "anonymous",
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
Bun.serve({
|
|
265
|
+
fetch: withRateLimit(async () => Response.json({ ok: true })),
|
|
266
|
+
});
|
|
192
267
|
```
|
|
193
268
|
|
|
194
|
-
|
|
269
|
+
### Hono
|
|
270
|
+
|
|
271
|
+
Use `createHonoRateLimit()` as Hono middleware.
|
|
195
272
|
|
|
196
273
|
```ts
|
|
197
|
-
import
|
|
274
|
+
import { Hono } from "hono";
|
|
198
275
|
import {
|
|
199
|
-
|
|
276
|
+
createHonoRateLimit,
|
|
200
277
|
RateLimiterBuilder,
|
|
201
278
|
} from "@xyph3r/rate-limiter";
|
|
202
279
|
|
|
203
|
-
const app =
|
|
280
|
+
const app = new Hono();
|
|
204
281
|
|
|
205
282
|
const limiter = new RateLimiterBuilder()
|
|
206
|
-
.forSlidingWindow({ limit: 5, windowMs: 1_000 })
|
|
207
283
|
.withMemoryStore()
|
|
284
|
+
.forSlidingWindow({ limit: 20, windowMs: 1_000 })
|
|
208
285
|
.build();
|
|
209
286
|
|
|
210
|
-
app.
|
|
287
|
+
app.use(
|
|
288
|
+
"*",
|
|
289
|
+
createHonoRateLimit(limiter, {
|
|
290
|
+
key: (c) => c.req.header("x-api-key") ?? "anonymous",
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
211
293
|
```
|
|
212
294
|
|
|
213
|
-
|
|
295
|
+
### Next.js App Router
|
|
214
296
|
|
|
215
|
-
|
|
297
|
+
Use `createNextRateLimit()` to wrap the exported route handler.
|
|
216
298
|
|
|
217
299
|
```ts
|
|
218
|
-
import {
|
|
300
|
+
import {
|
|
301
|
+
createNextRateLimit,
|
|
302
|
+
RateLimiterBuilder,
|
|
303
|
+
} from "@xyph3r/rate-limiter";
|
|
219
304
|
|
|
220
305
|
const limiter = new RateLimiterBuilder()
|
|
221
|
-
.forSlidingWindow({ limit: 30, windowMs: 60_000 })
|
|
222
306
|
.withMemoryStore()
|
|
307
|
+
.forSlidingWindow({ limit: 30, windowMs: 60_000 })
|
|
223
308
|
.build();
|
|
224
309
|
|
|
225
|
-
const withRateLimit = createNextRateLimit(limiter
|
|
310
|
+
const withRateLimit = createNextRateLimit(limiter, {
|
|
311
|
+
key: (request) => request.headers.get("x-api-key") ?? "anonymous",
|
|
312
|
+
});
|
|
226
313
|
|
|
227
314
|
export const GET = withRateLimit(async () => {
|
|
228
315
|
return Response.json({ ok: true });
|
|
229
316
|
});
|
|
230
317
|
```
|
|
231
318
|
|
|
232
|
-
You can also
|
|
319
|
+
You can also derive the key from route params or tenant context:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
const withRateLimit = createNextRateLimit(limiter, {
|
|
323
|
+
key: (_request, context: { params: { tenantId: string } }) => context.params.tenantId,
|
|
324
|
+
});
|
|
325
|
+
```
|
|
233
326
|
|
|
234
|
-
|
|
327
|
+
### NestJS
|
|
235
328
|
|
|
236
|
-
|
|
329
|
+
Use `createNestRateLimitGuard()` where a Nest guard fits better than middleware.
|
|
237
330
|
|
|
238
331
|
```ts
|
|
239
332
|
import { TooManyRequestsException } from "@nestjs/common";
|
|
@@ -243,11 +336,12 @@ import {
|
|
|
243
336
|
} from "@xyph3r/rate-limiter";
|
|
244
337
|
|
|
245
338
|
const limiter = new RateLimiterBuilder()
|
|
246
|
-
.forSlidingWindow({ limit: 10, windowMs: 1_000 })
|
|
247
339
|
.withMemoryStore()
|
|
340
|
+
.forSlidingWindow({ limit: 10, windowMs: 1_000 })
|
|
248
341
|
.build();
|
|
249
342
|
|
|
250
343
|
export const RateLimitGuard = createNestRateLimitGuard(limiter, {
|
|
344
|
+
key: (request) => request.headers?.["x-api-key"] as string || request.ip || "anonymous",
|
|
251
345
|
errorFactory: (decision) =>
|
|
252
346
|
new TooManyRequestsException({
|
|
253
347
|
error: "Too many requests",
|
|
@@ -256,6 +350,84 @@ export const RateLimitGuard = createNestRateLimitGuard(limiter, {
|
|
|
256
350
|
});
|
|
257
351
|
```
|
|
258
352
|
|
|
353
|
+
## Choosing the key
|
|
354
|
+
|
|
355
|
+
A good rate-limit key represents the caller you want to control.
|
|
356
|
+
|
|
357
|
+
Good keys:
|
|
358
|
+
|
|
359
|
+
- client IP for anonymous traffic
|
|
360
|
+
- API key
|
|
361
|
+
- authenticated user ID
|
|
362
|
+
- tenant ID
|
|
363
|
+
- machine or worker ID for background endpoints
|
|
364
|
+
|
|
365
|
+
Bad keys:
|
|
366
|
+
|
|
367
|
+
- a random UUID per request
|
|
368
|
+
- request timestamp
|
|
369
|
+
- request path alone, if many callers share it
|
|
370
|
+
|
|
371
|
+
## Cost-based limiting
|
|
372
|
+
|
|
373
|
+
Every adapter and the core API support `cost`.
|
|
374
|
+
|
|
375
|
+
Use this when one operation is more expensive than another.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
const limiter = new RateLimiterBuilder()
|
|
379
|
+
.withMemoryStore()
|
|
380
|
+
.forSlidingWindow({ limit: 100, windowMs: 60_000 })
|
|
381
|
+
.build();
|
|
382
|
+
|
|
383
|
+
await limiter.check("tenant:42", { cost: 5 });
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Example uses:
|
|
387
|
+
|
|
388
|
+
- expensive report generation counts as `5`
|
|
389
|
+
- normal read counts as `1`
|
|
390
|
+
- bulk export counts as `10`
|
|
391
|
+
|
|
392
|
+
## Response headers
|
|
393
|
+
|
|
394
|
+
By default the HTTP adapters set:
|
|
395
|
+
|
|
396
|
+
- `ratelimit-limit`
|
|
397
|
+
- `ratelimit-remaining`
|
|
398
|
+
- `ratelimit-reset`
|
|
399
|
+
- `ratelimit-policy`
|
|
400
|
+
- `retry-after`
|
|
401
|
+
|
|
402
|
+
Disable this with `setHeaders: false` if you need full manual control.
|
|
403
|
+
|
|
404
|
+
## Factory vs Builder
|
|
405
|
+
|
|
406
|
+
Use the Builder when you want readability and progressive configuration.
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
const limiter = new RateLimiterBuilder()
|
|
410
|
+
.withMemoryStore()
|
|
411
|
+
.forSlidingWindow({ limit: 50, windowMs: 10_000 })
|
|
412
|
+
.withKeyPrefix("public-api")
|
|
413
|
+
.withCache(25)
|
|
414
|
+
.build();
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Use `createRateLimiter()` when a plain config object is enough.
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
import { createRateLimiter } from "@xyph3r/rate-limiter";
|
|
421
|
+
|
|
422
|
+
const limiter = createRateLimiter({
|
|
423
|
+
algorithm: "sliding-window",
|
|
424
|
+
limit: 50,
|
|
425
|
+
windowMs: 10_000,
|
|
426
|
+
keyPrefix: "public-api",
|
|
427
|
+
cacheMs: 25,
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
259
431
|
## Public API
|
|
260
432
|
|
|
261
433
|
### `RateLimiterBuilder`
|
|
@@ -265,6 +437,7 @@ export const RateLimitGuard = createNestRateLimitGuard(limiter, {
|
|
|
265
437
|
- `.useStore(store)` / `.withMemoryStore()`
|
|
266
438
|
- `.withKeyPrefix(prefix)`
|
|
267
439
|
- `.withCache(ttlMs)`
|
|
440
|
+
- `.withClock(now)`
|
|
268
441
|
- `.build()`
|
|
269
442
|
|
|
270
443
|
### `RateLimiterLike`
|
|
@@ -274,11 +447,12 @@ export const RateLimitGuard = createNestRateLimitGuard(limiter, {
|
|
|
274
447
|
|
|
275
448
|
### Adapters
|
|
276
449
|
|
|
277
|
-
- `
|
|
278
|
-
- `
|
|
279
|
-
- `
|
|
280
|
-
- `
|
|
281
|
-
- `
|
|
450
|
+
- `createExpressRateLimit()`
|
|
451
|
+
- `createFastifyRateLimit()`
|
|
452
|
+
- `createFetchRateLimit()`
|
|
453
|
+
- `createHonoRateLimit()`
|
|
454
|
+
- `createNextRateLimit()`
|
|
455
|
+
- `createNestRateLimitGuard()`
|
|
282
456
|
|
|
283
457
|
### Decision fields
|
|
284
458
|
|
|
@@ -287,24 +461,26 @@ export const RateLimitGuard = createNestRateLimitGuard(limiter, {
|
|
|
287
461
|
- `used`
|
|
288
462
|
- `remaining`
|
|
289
463
|
- `retryAfterMs`
|
|
464
|
+
- `retryAfterSeconds`
|
|
290
465
|
- `resetAt`
|
|
466
|
+
- `resetAfterMs`
|
|
291
467
|
- `policy`
|
|
292
468
|
|
|
293
469
|
## Notes
|
|
294
470
|
|
|
295
|
-
-
|
|
296
|
-
- Redis
|
|
297
|
-
-
|
|
298
|
-
-
|
|
471
|
+
- Sliding window uses a weighted previous-window approximation, not a timestamp log.
|
|
472
|
+
- Redis checks are atomic because each strategy ships its own Lua program.
|
|
473
|
+
- Bun and Next.js share the fetch adapter shape under the hood.
|
|
474
|
+
- The optional cache proxy is only for very short-lived hot-path reuse. It is not a replacement for Redis.
|
|
299
475
|
|
|
300
476
|
## Publishing
|
|
301
477
|
|
|
302
|
-
The package is set up so `npm publish` builds `dist/` during `prepack` and runs
|
|
478
|
+
The package is set up so `npm publish` builds `dist/` during `prepack` and runs tests during `prepublishOnly`.
|
|
303
479
|
|
|
304
480
|
Release flow:
|
|
305
481
|
|
|
306
|
-
1. Install
|
|
307
|
-
2. Verify
|
|
308
|
-
3. Inspect the
|
|
309
|
-
4. Log in with `npm login` if needed
|
|
310
|
-
5. Publish
|
|
482
|
+
1. Install dependencies with `bun install` or `npm install`.
|
|
483
|
+
2. Verify with `bun run test` and `bun run build`.
|
|
484
|
+
3. Inspect the tarball with `npm pack --dry-run`.
|
|
485
|
+
4. Log in with `npm login` if needed.
|
|
486
|
+
5. Publish with `npm publish --access public --provenance`.
|