@chidchanun/bcp 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.
- package/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +241 -0
- package/docs/caching.md +76 -0
- package/docs/configuration.md +97 -0
- package/docs/deployment.md +74 -0
- package/docs/getting-started.md +82 -0
- package/docs/middleware.md +58 -0
- package/docs/releasing.md +299 -0
- package/docs/routing.md +103 -0
- package/docs/security.md +57 -0
- package/package.json +68 -0
- package/packages/bundler/src/client-islands.ts +1457 -0
- package/packages/bundler/src/incremental-context.ts +206 -0
- package/packages/bundler/src/index.ts +1991 -0
- package/packages/bundler/src/module-graph.ts +317 -0
- package/packages/bundler/src/partial-hydration.ts +414 -0
- package/packages/bundler/src/production.ts +974 -0
- package/packages/bundler/src/server-production-middleware.ts +447 -0
- package/packages/bundler/src/server-production.ts +1193 -0
- package/packages/bundler/src/special-files.ts +131 -0
- package/packages/cache/src/index.ts +761 -0
- package/packages/cli/bin/bcp.mjs +93 -0
- package/packages/cli/src/args.ts +305 -0
- package/packages/cli/src/bootstrap.ts +514 -0
- package/packages/cli/src/index.ts +504 -0
- package/packages/cli/src/version.ts +45 -0
- package/packages/client/src/cache.ts +11 -0
- package/packages/client/src/config.ts +18 -0
- package/packages/client/src/error-boundary.tsx +149 -0
- package/packages/client/src/hydration.ts +3 -0
- package/packages/client/src/index.tsx +57 -0
- package/packages/client/src/islands.tsx +315 -0
- package/packages/client/src/metadata.ts +281 -0
- package/packages/client/src/navigation-loading.ts +52 -0
- package/packages/client/src/navigation-state.ts +80 -0
- package/packages/client/src/not-found.ts +34 -0
- package/packages/client/src/persistent-layout-runtime.ts +273 -0
- package/packages/client/src/router-v2.tsx +969 -0
- package/packages/client/src/router.tsx +1 -0
- package/packages/config/src/index.ts +1038 -0
- package/packages/env/src/index.ts +593 -0
- package/packages/router/src/advanced-router.ts +1032 -0
- package/packages/router/src/index.ts +1 -0
- package/packages/server/src/compression.ts +249 -0
- package/packages/server/src/dev-document-metadata.ts +154 -0
- package/packages/server/src/dev-hmr.ts +225 -0
- package/packages/server/src/index.ts +2265 -0
- package/packages/server/src/metadata.ts +478 -0
- package/packages/server/src/middleware-dev-server.ts +260 -0
- package/packages/server/src/middleware-loader.ts +140 -0
- package/packages/server/src/middleware-proxy.ts +516 -0
- package/packages/server/src/middleware.ts +704 -0
- package/packages/server/src/navigation-payload.ts +471 -0
- package/packages/server/src/production-server.ts +1746 -0
- package/packages/server/src/response-cache-proxy.ts +828 -0
- package/packages/server/src/security-proxy.ts +406 -0
- package/packages/server/src/security.ts +451 -0
- package/packages/server/src/standalone-production-runtime-v2.ts +2047 -0
- package/packages/server/src/standalone-production-runtime-v3.ts +250 -0
- package/packages/server/src/standalone-production-runtime-v4.ts +289 -0
- package/packages/server/src/standalone-production-runtime-v5.ts +289 -0
- package/packages/server/src/standalone-production-runtime.ts +1951 -0
- package/packages/server/src/standalone-production-server.ts +6 -0
- package/packages/server/src/static-assets.ts +262 -0
- package/packages/server/src/static-dev-server.ts +854 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AsyncLocalStorage,
|
|
3
|
+
} from "node:async_hooks";
|
|
4
|
+
import {
|
|
5
|
+
createHash,
|
|
6
|
+
} from "node:crypto";
|
|
7
|
+
|
|
8
|
+
export type RevalidateValue =
|
|
9
|
+
| number
|
|
10
|
+
| false;
|
|
11
|
+
|
|
12
|
+
export interface CacheOptions {
|
|
13
|
+
key?: string;
|
|
14
|
+
revalidate?: RevalidateValue;
|
|
15
|
+
tags?: string[];
|
|
16
|
+
paths?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CacheEntryOptions {
|
|
20
|
+
revalidate?: RevalidateValue;
|
|
21
|
+
tags?: string[];
|
|
22
|
+
paths?: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CacheStats {
|
|
26
|
+
entries: number;
|
|
27
|
+
inFlight: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CacheEntry {
|
|
31
|
+
value: unknown;
|
|
32
|
+
expiresAt: number | null;
|
|
33
|
+
tags: Set<string>;
|
|
34
|
+
paths: Set<string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface RequestCacheContext {
|
|
38
|
+
pathname: string | null;
|
|
39
|
+
memo: Map<string, Promise<unknown>>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface CacheRuntimeState {
|
|
43
|
+
persistentCache: Map<string, CacheEntry>;
|
|
44
|
+
inFlight: Map<string, Promise<unknown>>;
|
|
45
|
+
requestStorage: AsyncLocalStorage<RequestCacheContext>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const CACHE_RUNTIME_SYMBOL =
|
|
49
|
+
Symbol.for(
|
|
50
|
+
"bcp.framework.cache.runtime"
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const globalRecord =
|
|
54
|
+
globalThis as typeof globalThis &
|
|
55
|
+
Record<PropertyKey, unknown>;
|
|
56
|
+
|
|
57
|
+
let runtimeState =
|
|
58
|
+
globalRecord[
|
|
59
|
+
CACHE_RUNTIME_SYMBOL
|
|
60
|
+
] as CacheRuntimeState |
|
|
61
|
+
undefined;
|
|
62
|
+
|
|
63
|
+
if (!runtimeState) {
|
|
64
|
+
runtimeState = {
|
|
65
|
+
persistentCache:
|
|
66
|
+
new Map(),
|
|
67
|
+
inFlight:
|
|
68
|
+
new Map(),
|
|
69
|
+
requestStorage:
|
|
70
|
+
new AsyncLocalStorage<RequestCacheContext>(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
globalRecord[
|
|
74
|
+
CACHE_RUNTIME_SYMBOL
|
|
75
|
+
] =
|
|
76
|
+
runtimeState;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const persistentCache =
|
|
80
|
+
runtimeState.persistentCache;
|
|
81
|
+
|
|
82
|
+
const inFlight =
|
|
83
|
+
runtimeState.inFlight;
|
|
84
|
+
|
|
85
|
+
const requestStorage =
|
|
86
|
+
runtimeState.requestStorage;
|
|
87
|
+
|
|
88
|
+
export function enterRequestCacheContext(
|
|
89
|
+
pathname: string
|
|
90
|
+
): void {
|
|
91
|
+
requestStorage.enterWith({
|
|
92
|
+
pathname:
|
|
93
|
+
normalizePath(pathname),
|
|
94
|
+
memo:
|
|
95
|
+
new Map(),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function runWithRequestCacheContext<T>(
|
|
100
|
+
pathname: string,
|
|
101
|
+
callback: () => T
|
|
102
|
+
): T {
|
|
103
|
+
return requestStorage.run(
|
|
104
|
+
{
|
|
105
|
+
pathname:
|
|
106
|
+
normalizePath(pathname),
|
|
107
|
+
memo:
|
|
108
|
+
new Map(),
|
|
109
|
+
},
|
|
110
|
+
callback
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function dedupe<
|
|
115
|
+
Args extends unknown[],
|
|
116
|
+
Result
|
|
117
|
+
>(
|
|
118
|
+
fn: (...args: Args) =>
|
|
119
|
+
Result | Promise<Result>,
|
|
120
|
+
key?: string
|
|
121
|
+
): (...args: Args) => Promise<Result> {
|
|
122
|
+
const namespace =
|
|
123
|
+
key ??
|
|
124
|
+
fn.name ??
|
|
125
|
+
"anonymous";
|
|
126
|
+
|
|
127
|
+
return async (
|
|
128
|
+
...args: Args
|
|
129
|
+
): Promise<Result> => {
|
|
130
|
+
const context =
|
|
131
|
+
ensureRequestContext();
|
|
132
|
+
|
|
133
|
+
const cacheKey =
|
|
134
|
+
`request:${namespace}:${hashArguments(args)}`;
|
|
135
|
+
|
|
136
|
+
const existing =
|
|
137
|
+
context.memo.get(
|
|
138
|
+
cacheKey
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (existing) {
|
|
142
|
+
return await existing as Result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const promise =
|
|
146
|
+
Promise.resolve(
|
|
147
|
+
fn(...args)
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
context.memo.set(
|
|
151
|
+
cacheKey,
|
|
152
|
+
promise
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
return await promise;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
context.memo.delete(
|
|
159
|
+
cacheKey
|
|
160
|
+
);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function cache<
|
|
167
|
+
Args extends unknown[],
|
|
168
|
+
Result
|
|
169
|
+
>(
|
|
170
|
+
fn: (...args: Args) =>
|
|
171
|
+
Result | Promise<Result>,
|
|
172
|
+
options: CacheOptions = {}
|
|
173
|
+
): (...args: Args) => Promise<Result> {
|
|
174
|
+
const namespace =
|
|
175
|
+
options.key ??
|
|
176
|
+
fn.name;
|
|
177
|
+
|
|
178
|
+
if (!namespace) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
"BCP Framework: cache() requires options.key when the wrapped function has no name."
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
validateRevalidate(
|
|
185
|
+
options.revalidate
|
|
186
|
+
);
|
|
187
|
+
validateTags(
|
|
188
|
+
options.tags
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
return async (
|
|
192
|
+
...args: Args
|
|
193
|
+
): Promise<Result> => {
|
|
194
|
+
if (
|
|
195
|
+
options.revalidate === 0
|
|
196
|
+
) {
|
|
197
|
+
return await fn(
|
|
198
|
+
...args
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const cacheKey =
|
|
203
|
+
`data:${namespace}:${hashArguments(args)}`;
|
|
204
|
+
|
|
205
|
+
const existingEntry =
|
|
206
|
+
readCacheEntry<Result>(
|
|
207
|
+
cacheKey
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (existingEntry.hit) {
|
|
211
|
+
return existingEntry.value as Result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const pending =
|
|
215
|
+
inFlight.get(
|
|
216
|
+
cacheKey
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (pending) {
|
|
220
|
+
return await pending as Result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const promise =
|
|
224
|
+
Promise.resolve(
|
|
225
|
+
fn(...args)
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
inFlight.set(
|
|
229
|
+
cacheKey,
|
|
230
|
+
promise
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const value =
|
|
235
|
+
await promise;
|
|
236
|
+
|
|
237
|
+
const currentPath =
|
|
238
|
+
requestStorage
|
|
239
|
+
.getStore()
|
|
240
|
+
?.pathname ??
|
|
241
|
+
null;
|
|
242
|
+
|
|
243
|
+
const paths =
|
|
244
|
+
new Set(
|
|
245
|
+
(
|
|
246
|
+
options.paths ?? []
|
|
247
|
+
).map(
|
|
248
|
+
normalizePath
|
|
249
|
+
)
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if (currentPath) {
|
|
253
|
+
paths.add(
|
|
254
|
+
currentPath
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
setCacheEntry(
|
|
259
|
+
cacheKey,
|
|
260
|
+
value,
|
|
261
|
+
{
|
|
262
|
+
revalidate:
|
|
263
|
+
options.revalidate,
|
|
264
|
+
tags:
|
|
265
|
+
options.tags,
|
|
266
|
+
paths:
|
|
267
|
+
Array.from(
|
|
268
|
+
paths
|
|
269
|
+
),
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return value;
|
|
274
|
+
} finally {
|
|
275
|
+
inFlight.delete(
|
|
276
|
+
cacheKey
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function getCacheEntry<T>(
|
|
283
|
+
key: string
|
|
284
|
+
): T | undefined {
|
|
285
|
+
const result =
|
|
286
|
+
readCacheEntry<T>(
|
|
287
|
+
key
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return result.hit
|
|
291
|
+
? result.value
|
|
292
|
+
: undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function setCacheEntry<T>(
|
|
296
|
+
key: string,
|
|
297
|
+
value: T,
|
|
298
|
+
options: CacheEntryOptions = {}
|
|
299
|
+
): void {
|
|
300
|
+
validateRevalidate(
|
|
301
|
+
options.revalidate
|
|
302
|
+
);
|
|
303
|
+
validateTags(
|
|
304
|
+
options.tags
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (
|
|
308
|
+
options.revalidate === 0
|
|
309
|
+
) {
|
|
310
|
+
persistentCache.delete(
|
|
311
|
+
key
|
|
312
|
+
);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const expiresAt =
|
|
317
|
+
options.revalidate === undefined ||
|
|
318
|
+
options.revalidate === false
|
|
319
|
+
? null
|
|
320
|
+
: Date.now() +
|
|
321
|
+
options.revalidate * 1000;
|
|
322
|
+
|
|
323
|
+
persistentCache.set(
|
|
324
|
+
key,
|
|
325
|
+
{
|
|
326
|
+
value,
|
|
327
|
+
expiresAt,
|
|
328
|
+
tags:
|
|
329
|
+
new Set(
|
|
330
|
+
options.tags ?? []
|
|
331
|
+
),
|
|
332
|
+
paths:
|
|
333
|
+
new Set(
|
|
334
|
+
(
|
|
335
|
+
options.paths ?? []
|
|
336
|
+
).map(
|
|
337
|
+
normalizePath
|
|
338
|
+
)
|
|
339
|
+
),
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function deleteCacheEntry(
|
|
345
|
+
key: string
|
|
346
|
+
): boolean {
|
|
347
|
+
return persistentCache.delete(
|
|
348
|
+
key
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function revalidateTag(
|
|
353
|
+
tag: string
|
|
354
|
+
): number {
|
|
355
|
+
const normalized =
|
|
356
|
+
normalizeTag(tag);
|
|
357
|
+
|
|
358
|
+
let removed = 0;
|
|
359
|
+
|
|
360
|
+
for (
|
|
361
|
+
const [
|
|
362
|
+
key,
|
|
363
|
+
entry,
|
|
364
|
+
]
|
|
365
|
+
of persistentCache
|
|
366
|
+
) {
|
|
367
|
+
if (
|
|
368
|
+
entry.tags.has(
|
|
369
|
+
normalized
|
|
370
|
+
)
|
|
371
|
+
) {
|
|
372
|
+
persistentCache.delete(
|
|
373
|
+
key
|
|
374
|
+
);
|
|
375
|
+
removed++;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return removed;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function revalidatePath(
|
|
383
|
+
pathname: string
|
|
384
|
+
): number {
|
|
385
|
+
const normalized =
|
|
386
|
+
normalizePath(pathname);
|
|
387
|
+
|
|
388
|
+
let removed = 0;
|
|
389
|
+
|
|
390
|
+
for (
|
|
391
|
+
const [
|
|
392
|
+
key,
|
|
393
|
+
entry,
|
|
394
|
+
]
|
|
395
|
+
of persistentCache
|
|
396
|
+
) {
|
|
397
|
+
const matches =
|
|
398
|
+
Array.from(
|
|
399
|
+
entry.paths
|
|
400
|
+
).some(
|
|
401
|
+
(cachedPath) =>
|
|
402
|
+
pathMatches(
|
|
403
|
+
cachedPath,
|
|
404
|
+
normalized
|
|
405
|
+
)
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
if (matches) {
|
|
409
|
+
persistentCache.delete(
|
|
410
|
+
key
|
|
411
|
+
);
|
|
412
|
+
removed++;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return removed;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function clearCache(): void {
|
|
420
|
+
persistentCache.clear();
|
|
421
|
+
inFlight.clear();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function getCacheStats(): CacheStats {
|
|
425
|
+
pruneExpiredEntries();
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
entries:
|
|
429
|
+
persistentCache.size,
|
|
430
|
+
inFlight:
|
|
431
|
+
inFlight.size,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function createRouteCacheKey(
|
|
436
|
+
kind: "page" | "api" | "navigation",
|
|
437
|
+
url: string
|
|
438
|
+
): string {
|
|
439
|
+
return `route:${kind}:${url}`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function normalizeRevalidate(
|
|
443
|
+
value: unknown
|
|
444
|
+
): RevalidateValue | undefined {
|
|
445
|
+
if (
|
|
446
|
+
value === undefined
|
|
447
|
+
) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (
|
|
452
|
+
value === false
|
|
453
|
+
) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (
|
|
458
|
+
typeof value === "number" &&
|
|
459
|
+
Number.isFinite(value) &&
|
|
460
|
+
value >= 0
|
|
461
|
+
) {
|
|
462
|
+
return value;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
throw new Error(
|
|
466
|
+
"BCP Framework: revalidate must be false or a non-negative number of seconds."
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function readCacheEntry<T>(
|
|
471
|
+
key: string
|
|
472
|
+
): {
|
|
473
|
+
hit: boolean;
|
|
474
|
+
value?: T;
|
|
475
|
+
} {
|
|
476
|
+
const entry =
|
|
477
|
+
persistentCache.get(
|
|
478
|
+
key
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
if (!entry) {
|
|
482
|
+
return {
|
|
483
|
+
hit: false,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (
|
|
488
|
+
entry.expiresAt !== null &&
|
|
489
|
+
entry.expiresAt <= Date.now()
|
|
490
|
+
) {
|
|
491
|
+
persistentCache.delete(
|
|
492
|
+
key
|
|
493
|
+
);
|
|
494
|
+
return {
|
|
495
|
+
hit: false,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
hit: true,
|
|
501
|
+
value:
|
|
502
|
+
entry.value as T,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function ensureRequestContext(): RequestCacheContext {
|
|
507
|
+
const existing =
|
|
508
|
+
requestStorage.getStore();
|
|
509
|
+
|
|
510
|
+
if (existing) {
|
|
511
|
+
return existing;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const context:
|
|
515
|
+
RequestCacheContext = {
|
|
516
|
+
pathname: null,
|
|
517
|
+
memo:
|
|
518
|
+
new Map(),
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
requestStorage.enterWith(
|
|
522
|
+
context
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
return context;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function pruneExpiredEntries(): void {
|
|
529
|
+
const now =
|
|
530
|
+
Date.now();
|
|
531
|
+
|
|
532
|
+
for (
|
|
533
|
+
const [
|
|
534
|
+
key,
|
|
535
|
+
entry,
|
|
536
|
+
]
|
|
537
|
+
of persistentCache
|
|
538
|
+
) {
|
|
539
|
+
if (
|
|
540
|
+
entry.expiresAt !== null &&
|
|
541
|
+
entry.expiresAt <= now
|
|
542
|
+
) {
|
|
543
|
+
persistentCache.delete(
|
|
544
|
+
key
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function validateRevalidate(
|
|
551
|
+
value: RevalidateValue | undefined
|
|
552
|
+
): void {
|
|
553
|
+
normalizeRevalidate(
|
|
554
|
+
value
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function validateTags(
|
|
559
|
+
tags: string[] | undefined
|
|
560
|
+
): void {
|
|
561
|
+
for (
|
|
562
|
+
const tag
|
|
563
|
+
of tags ?? []
|
|
564
|
+
) {
|
|
565
|
+
normalizeTag(
|
|
566
|
+
tag
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function normalizeTag(
|
|
572
|
+
value: string
|
|
573
|
+
): string {
|
|
574
|
+
const normalized =
|
|
575
|
+
value.trim();
|
|
576
|
+
|
|
577
|
+
if (!normalized) {
|
|
578
|
+
throw new Error(
|
|
579
|
+
"BCP Framework: cache tags cannot be empty."
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return normalized;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function normalizePath(
|
|
587
|
+
value: string
|
|
588
|
+
): string {
|
|
589
|
+
let pathname =
|
|
590
|
+
value.trim();
|
|
591
|
+
|
|
592
|
+
if (!pathname) {
|
|
593
|
+
return "/";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
if (
|
|
598
|
+
pathname.startsWith(
|
|
599
|
+
"http://"
|
|
600
|
+
) ||
|
|
601
|
+
pathname.startsWith(
|
|
602
|
+
"https://"
|
|
603
|
+
)
|
|
604
|
+
) {
|
|
605
|
+
pathname =
|
|
606
|
+
new URL(
|
|
607
|
+
pathname
|
|
608
|
+
).pathname;
|
|
609
|
+
}
|
|
610
|
+
} catch {
|
|
611
|
+
// Fall back to treating the input as a pathname.
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
pathname =
|
|
615
|
+
pathname.split("?")[0]
|
|
616
|
+
.split("#")[0];
|
|
617
|
+
|
|
618
|
+
if (
|
|
619
|
+
!pathname.startsWith("/")
|
|
620
|
+
) {
|
|
621
|
+
pathname =
|
|
622
|
+
`/${pathname}`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (
|
|
626
|
+
pathname.length > 1 &&
|
|
627
|
+
pathname.endsWith("/")
|
|
628
|
+
) {
|
|
629
|
+
pathname =
|
|
630
|
+
pathname.slice(
|
|
631
|
+
0,
|
|
632
|
+
-1
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return pathname;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function pathMatches(
|
|
640
|
+
cachedPath: string,
|
|
641
|
+
invalidatedPath: string
|
|
642
|
+
): boolean {
|
|
643
|
+
if (
|
|
644
|
+
invalidatedPath === "/"
|
|
645
|
+
) {
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return (
|
|
650
|
+
cachedPath ===
|
|
651
|
+
invalidatedPath ||
|
|
652
|
+
cachedPath.startsWith(
|
|
653
|
+
`${invalidatedPath}/`
|
|
654
|
+
)
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function hashArguments(
|
|
659
|
+
args: unknown[]
|
|
660
|
+
): string {
|
|
661
|
+
return createHash(
|
|
662
|
+
"sha256"
|
|
663
|
+
)
|
|
664
|
+
.update(
|
|
665
|
+
stableSerialize(
|
|
666
|
+
args
|
|
667
|
+
)
|
|
668
|
+
)
|
|
669
|
+
.digest(
|
|
670
|
+
"hex"
|
|
671
|
+
)
|
|
672
|
+
.slice(
|
|
673
|
+
0,
|
|
674
|
+
24
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function stableSerialize(
|
|
679
|
+
value: unknown,
|
|
680
|
+
seen = new WeakSet<object>()
|
|
681
|
+
): string {
|
|
682
|
+
if (
|
|
683
|
+
value === null
|
|
684
|
+
) {
|
|
685
|
+
return "null";
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
switch (
|
|
689
|
+
typeof value
|
|
690
|
+
) {
|
|
691
|
+
case "undefined":
|
|
692
|
+
return "undefined";
|
|
693
|
+
case "string":
|
|
694
|
+
return JSON.stringify(
|
|
695
|
+
value
|
|
696
|
+
);
|
|
697
|
+
case "number":
|
|
698
|
+
case "boolean":
|
|
699
|
+
return String(value);
|
|
700
|
+
case "bigint":
|
|
701
|
+
return `${value.toString()}n`;
|
|
702
|
+
case "symbol":
|
|
703
|
+
case "function":
|
|
704
|
+
throw new Error(
|
|
705
|
+
`BCP Framework: cache keys cannot include ${typeof value} values.`
|
|
706
|
+
);
|
|
707
|
+
case "object":
|
|
708
|
+
break;
|
|
709
|
+
default:
|
|
710
|
+
return String(value);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const object =
|
|
714
|
+
value as object;
|
|
715
|
+
|
|
716
|
+
if (
|
|
717
|
+
seen.has(object)
|
|
718
|
+
) {
|
|
719
|
+
throw new Error(
|
|
720
|
+
"BCP Framework: cache keys cannot include circular values."
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
seen.add(object);
|
|
725
|
+
|
|
726
|
+
try {
|
|
727
|
+
if (
|
|
728
|
+
value instanceof Date
|
|
729
|
+
) {
|
|
730
|
+
return `Date(${value.toISOString()})`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (
|
|
734
|
+
Array.isArray(value)
|
|
735
|
+
) {
|
|
736
|
+
return `[${value.map(
|
|
737
|
+
(item) =>
|
|
738
|
+
stableSerialize(
|
|
739
|
+
item,
|
|
740
|
+
seen
|
|
741
|
+
)
|
|
742
|
+
).join(",")}]`;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const record =
|
|
746
|
+
value as Record<string, unknown>;
|
|
747
|
+
|
|
748
|
+
return `{${Object.keys(record)
|
|
749
|
+
.sort()
|
|
750
|
+
.map(
|
|
751
|
+
(key) =>
|
|
752
|
+
`${JSON.stringify(key)}:${stableSerialize(
|
|
753
|
+
record[key],
|
|
754
|
+
seen
|
|
755
|
+
)}`
|
|
756
|
+
)
|
|
757
|
+
.join(",")}}`;
|
|
758
|
+
} finally {
|
|
759
|
+
seen.delete(object);
|
|
760
|
+
}
|
|
761
|
+
}
|