@leejpsd/nextjs-cache-handler 0.3.4 → 0.4.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/CHANGELOG.md +42 -0
- package/README.md +10 -3
- package/bin/cli.cjs +10 -0
- package/dist/cache-components/index.cjs +150 -4
- package/dist/cache-components/index.cjs.map +1 -1
- package/dist/cache-components/index.d.cts +1 -1
- package/dist/cache-components/index.d.ts +1 -1
- package/dist/cache-components/index.js +150 -4
- package/dist/cache-components/index.js.map +1 -1
- package/dist/cli/index.cjs +938 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +28 -0
- package/dist/cli/index.d.ts +28 -0
- package/dist/cli/index.js +928 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/incremental/index.cjs +81 -6
- package/dist/incremental/index.cjs.map +1 -1
- package/dist/incremental/index.d.cts +1 -1
- package/dist/incremental/index.d.ts +1 -1
- package/dist/incremental/index.js +81 -6
- package/dist/incremental/index.js.map +1 -1
- package/dist/index.cjs +157 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +157 -8
- package/dist/index.js.map +1 -1
- package/dist/ops/index.d.cts +1 -1
- package/dist/ops/index.d.ts +1 -1
- package/dist/otel/index.d.cts +1 -1
- package/dist/otel/index.d.ts +1 -1
- package/dist/seed/index.cjs +561 -0
- package/dist/seed/index.cjs.map +1 -0
- package/dist/seed/index.d.cts +53 -0
- package/dist/seed/index.d.ts +53 -0
- package/dist/seed/index.js +550 -0
- package/dist/seed/index.js.map +1 -0
- package/dist/shared/client/adapter-ioredis.cjs +40 -1
- package/dist/shared/client/adapter-ioredis.cjs.map +1 -1
- package/dist/shared/client/adapter-ioredis.d.cts +1 -1
- package/dist/shared/client/adapter-ioredis.d.ts +1 -1
- package/dist/shared/client/adapter-ioredis.js +40 -1
- package/dist/shared/client/adapter-ioredis.js.map +1 -1
- package/dist/shared/client/adapter-redis.cjs +34 -1
- package/dist/shared/client/adapter-redis.cjs.map +1 -1
- package/dist/shared/client/adapter-redis.d.cts +1 -1
- package/dist/shared/client/adapter-redis.d.ts +1 -1
- package/dist/shared/client/adapter-redis.js +34 -1
- package/dist/shared/client/adapter-redis.js.map +1 -1
- package/dist/{types-Bl-rkLKX.d.cts → types-C1vJ_sED.d.cts} +25 -1
- package/dist/{types-Bl-rkLKX.d.ts → types-C1vJ_sED.d.ts} +25 -1
- package/package.json +22 -2
- package/setup-instructions/setup.md +31 -9
- package/skills/nextjs-redis-cache/SKILL.md +15 -0
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
import path4 from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import { gzip, gunzip, brotliCompress, brotliDecompress, constants } from 'zlib';
|
|
7
|
+
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __esm = (fn, res) => function __init() {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
};
|
|
13
|
+
var __export = (target, all) => {
|
|
14
|
+
for (var name in all)
|
|
15
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
+
};
|
|
17
|
+
var getFilename, getDirname, __dirname$1;
|
|
18
|
+
var init_esm_shims = __esm({
|
|
19
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
20
|
+
getFilename = () => fileURLToPath(import.meta.url);
|
|
21
|
+
getDirname = () => path4.dirname(getFilename());
|
|
22
|
+
__dirname$1 = /* @__PURE__ */ getDirname();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
function nodeRequire(id) {
|
|
26
|
+
try {
|
|
27
|
+
return packageRequire(id);
|
|
28
|
+
} catch {
|
|
29
|
+
return cwdRequire(id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
var packageRequire, cwdRequire;
|
|
33
|
+
var init_node_require = __esm({
|
|
34
|
+
"src/shared/node-require.ts"() {
|
|
35
|
+
init_esm_shims();
|
|
36
|
+
packageRequire = createRequire(import.meta.url);
|
|
37
|
+
cwdRequire = createRequire(
|
|
38
|
+
path4.join(process.cwd(), "__resolve-anchor__.js")
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// src/shared/client/adapter-redis.ts
|
|
44
|
+
var adapter_redis_exports = {};
|
|
45
|
+
__export(adapter_redis_exports, {
|
|
46
|
+
adaptRedisV5: () => adaptRedisV5,
|
|
47
|
+
createRedisV5Client: () => createRedisV5Client
|
|
48
|
+
});
|
|
49
|
+
function adaptRedisV5(client) {
|
|
50
|
+
const like = client;
|
|
51
|
+
like.dispose = () => {
|
|
52
|
+
try {
|
|
53
|
+
client.destroy();
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
like.subscribe = async (channel, onMessage, onDown) => {
|
|
58
|
+
const sub = client.duplicate();
|
|
59
|
+
const teardown = () => {
|
|
60
|
+
try {
|
|
61
|
+
sub.destroy();
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
let down = false;
|
|
66
|
+
const markDown = () => {
|
|
67
|
+
if (down) return;
|
|
68
|
+
down = true;
|
|
69
|
+
teardown();
|
|
70
|
+
onDown?.();
|
|
71
|
+
};
|
|
72
|
+
sub.on("error", markDown);
|
|
73
|
+
sub.on("end", markDown);
|
|
74
|
+
try {
|
|
75
|
+
await sub.connect();
|
|
76
|
+
await sub.subscribe(channel, (message) => onMessage(message));
|
|
77
|
+
} catch (err) {
|
|
78
|
+
teardown();
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
return async () => teardown();
|
|
82
|
+
};
|
|
83
|
+
return like;
|
|
84
|
+
}
|
|
85
|
+
function createRedisV5Client(config) {
|
|
86
|
+
const { createClient } = nodeRequire("redis");
|
|
87
|
+
const useTls = config.tls ?? config.url.startsWith("rediss://");
|
|
88
|
+
const opts = {
|
|
89
|
+
url: config.url,
|
|
90
|
+
socket: {
|
|
91
|
+
connectTimeout: config.connectTimeout ?? 1500,
|
|
92
|
+
reconnectStrategy: false,
|
|
93
|
+
...useTls ? { tls: true } : {}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
if (config.password !== void 0) opts.password = config.password;
|
|
97
|
+
return createClient(opts);
|
|
98
|
+
}
|
|
99
|
+
var init_adapter_redis = __esm({
|
|
100
|
+
"src/shared/client/adapter-redis.ts"() {
|
|
101
|
+
init_esm_shims();
|
|
102
|
+
init_node_require();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// src/shared/client/adapter-ioredis.ts
|
|
107
|
+
var adapter_ioredis_exports = {};
|
|
108
|
+
__export(adapter_ioredis_exports, {
|
|
109
|
+
adaptCluster: () => adaptCluster,
|
|
110
|
+
adaptIoredis: () => adaptIoredis,
|
|
111
|
+
createIoredisClient: () => createIoredisClient,
|
|
112
|
+
createIoredisCluster: () => createIoredisCluster,
|
|
113
|
+
createIoredisSentinel: () => createIoredisSentinel
|
|
114
|
+
});
|
|
115
|
+
function adapt(client, isClusterClient = false) {
|
|
116
|
+
return {
|
|
117
|
+
get isOpen() {
|
|
118
|
+
return client.status === "ready";
|
|
119
|
+
},
|
|
120
|
+
connect: async () => {
|
|
121
|
+
if (client.status !== "ready" && client.status !== "connecting") {
|
|
122
|
+
await client.connect();
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
get: (k) => client.get(k),
|
|
126
|
+
set: (k, v, opts) => {
|
|
127
|
+
const args = [];
|
|
128
|
+
if (opts?.EX !== void 0) args.push("EX", opts.EX);
|
|
129
|
+
if (opts?.NX) args.push("NX");
|
|
130
|
+
return client.set(k, v, ...args);
|
|
131
|
+
},
|
|
132
|
+
del: async (keys) => {
|
|
133
|
+
const arr = Array.isArray(keys) ? keys : [keys];
|
|
134
|
+
if (arr.length === 0) return 0;
|
|
135
|
+
return client.del(...arr);
|
|
136
|
+
},
|
|
137
|
+
sAdd: async (k, m) => {
|
|
138
|
+
const arr = Array.isArray(m) ? m : [m];
|
|
139
|
+
if (arr.length === 0) return 0;
|
|
140
|
+
return client.sadd(k, ...arr);
|
|
141
|
+
},
|
|
142
|
+
sMembers: (k) => client.smembers(k),
|
|
143
|
+
expire: async (k, s) => {
|
|
144
|
+
const r = await client.expire(k, s);
|
|
145
|
+
return r;
|
|
146
|
+
},
|
|
147
|
+
mGet: async (keys) => {
|
|
148
|
+
if (keys.length === 0) return [];
|
|
149
|
+
return client.mget(...keys);
|
|
150
|
+
},
|
|
151
|
+
eval: (script, opts) => client.eval(
|
|
152
|
+
script,
|
|
153
|
+
opts.keys.length,
|
|
154
|
+
...opts.keys,
|
|
155
|
+
...opts.arguments
|
|
156
|
+
),
|
|
157
|
+
evalSha: (sha, opts) => client.evalsha(
|
|
158
|
+
sha,
|
|
159
|
+
opts.keys.length,
|
|
160
|
+
...opts.keys,
|
|
161
|
+
...opts.arguments
|
|
162
|
+
),
|
|
163
|
+
scriptLoad: async (script) => await client.script("LOAD", script),
|
|
164
|
+
scanIterator: async function* ({ MATCH, COUNT }) {
|
|
165
|
+
if (isClusterClient) {
|
|
166
|
+
const cluster = client;
|
|
167
|
+
for (const node of cluster.nodes("master")) {
|
|
168
|
+
let cursor = "0";
|
|
169
|
+
do {
|
|
170
|
+
const [next, batch] = await node.scan(
|
|
171
|
+
cursor,
|
|
172
|
+
"MATCH",
|
|
173
|
+
MATCH,
|
|
174
|
+
"COUNT",
|
|
175
|
+
COUNT ?? 100
|
|
176
|
+
);
|
|
177
|
+
cursor = next;
|
|
178
|
+
if (batch.length > 0) yield batch;
|
|
179
|
+
} while (cursor !== "0");
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
const redis = client;
|
|
183
|
+
let cursor = "0";
|
|
184
|
+
do {
|
|
185
|
+
const [next, batch] = await redis.scan(
|
|
186
|
+
cursor,
|
|
187
|
+
"MATCH",
|
|
188
|
+
MATCH,
|
|
189
|
+
"COUNT",
|
|
190
|
+
COUNT ?? 100
|
|
191
|
+
);
|
|
192
|
+
cursor = next;
|
|
193
|
+
if (batch.length > 0) yield batch;
|
|
194
|
+
} while (cursor !== "0");
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
ping: () => client.ping(),
|
|
198
|
+
on: (event, listener) => {
|
|
199
|
+
client.on(event, listener);
|
|
200
|
+
return client;
|
|
201
|
+
},
|
|
202
|
+
publish: (channel, message) => client.publish(channel, message),
|
|
203
|
+
...isClusterClient ? {} : {
|
|
204
|
+
subscribe: async (channel, onMessage, onDown) => {
|
|
205
|
+
const sub = client.duplicate();
|
|
206
|
+
let down = false;
|
|
207
|
+
const markDown = () => {
|
|
208
|
+
if (down) return;
|
|
209
|
+
down = true;
|
|
210
|
+
try {
|
|
211
|
+
sub.disconnect();
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
onDown?.();
|
|
215
|
+
};
|
|
216
|
+
sub.on("error", markDown);
|
|
217
|
+
sub.on("end", markDown);
|
|
218
|
+
try {
|
|
219
|
+
if (sub.status !== "ready" && sub.status !== "connecting") {
|
|
220
|
+
await sub.connect();
|
|
221
|
+
}
|
|
222
|
+
await sub.subscribe(channel);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
markDown();
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
sub.on("message", (ch, message) => {
|
|
228
|
+
if (ch === channel) onMessage(message);
|
|
229
|
+
});
|
|
230
|
+
return async () => {
|
|
231
|
+
down = true;
|
|
232
|
+
sub.disconnect();
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
dispose: () => {
|
|
237
|
+
client.disconnect();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function adaptIoredis(client) {
|
|
242
|
+
return adapt(client, false);
|
|
243
|
+
}
|
|
244
|
+
function adaptCluster(cluster) {
|
|
245
|
+
return adapt(cluster, true);
|
|
246
|
+
}
|
|
247
|
+
function createIoredisClient(config) {
|
|
248
|
+
const IORedis = nodeRequire("ioredis");
|
|
249
|
+
const Ctor = IORedis.default ?? IORedis;
|
|
250
|
+
return new Ctor(config.url, {
|
|
251
|
+
password: config.password,
|
|
252
|
+
connectTimeout: config.connectTimeout ?? 1500,
|
|
253
|
+
tls: config.tls ? {} : void 0,
|
|
254
|
+
lazyConnect: true,
|
|
255
|
+
maxRetriesPerRequest: 1,
|
|
256
|
+
// Batch same-tick commands into one RTT (matches redis@5's default
|
|
257
|
+
// behavior) — makes Promise.all fan-outs a single pipeline.
|
|
258
|
+
enableAutoPipelining: true
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function createIoredisSentinel(config) {
|
|
262
|
+
const IORedis = nodeRequire("ioredis");
|
|
263
|
+
const Ctor = IORedis.default ?? IORedis;
|
|
264
|
+
return new Ctor({
|
|
265
|
+
sentinels: config.sentinels,
|
|
266
|
+
name: config.name,
|
|
267
|
+
password: config.password,
|
|
268
|
+
sentinelPassword: config.sentinelPassword,
|
|
269
|
+
connectTimeout: config.connectTimeout ?? 1500,
|
|
270
|
+
tls: config.tls ? {} : void 0,
|
|
271
|
+
lazyConnect: true,
|
|
272
|
+
maxRetriesPerRequest: 1,
|
|
273
|
+
enableAutoPipelining: true
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function createIoredisCluster(config) {
|
|
277
|
+
const IORedis = nodeRequire("ioredis");
|
|
278
|
+
const ClusterCtor = IORedis.Cluster ?? IORedis.default.Cluster;
|
|
279
|
+
return new ClusterCtor(config.nodes, {
|
|
280
|
+
redisOptions: {
|
|
281
|
+
password: config.password,
|
|
282
|
+
tls: config.tls ? {} : void 0
|
|
283
|
+
},
|
|
284
|
+
enableReadyCheck: true,
|
|
285
|
+
lazyConnect: true,
|
|
286
|
+
// Same-slot commands issued in the same tick share one pipeline.
|
|
287
|
+
enableAutoPipelining: true
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
var init_adapter_ioredis = __esm({
|
|
291
|
+
"src/shared/client/adapter-ioredis.ts"() {
|
|
292
|
+
init_esm_shims();
|
|
293
|
+
init_node_require();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// src/cli/index.ts
|
|
298
|
+
init_esm_shims();
|
|
299
|
+
|
|
300
|
+
// src/shared/client/index.ts
|
|
301
|
+
init_esm_shims();
|
|
302
|
+
async function buildClient(source) {
|
|
303
|
+
if (typeof source === "function") {
|
|
304
|
+
return await source();
|
|
305
|
+
}
|
|
306
|
+
switch (source.type) {
|
|
307
|
+
case "redis": {
|
|
308
|
+
const { adaptRedisV5: adaptRedisV52, createRedisV5Client: createRedisV5Client2 } = await Promise.resolve().then(() => (init_adapter_redis(), adapter_redis_exports));
|
|
309
|
+
return adaptRedisV52(createRedisV5Client2(source));
|
|
310
|
+
}
|
|
311
|
+
case "ioredis": {
|
|
312
|
+
const { adaptIoredis: adaptIoredis2, createIoredisClient: createIoredisClient2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
|
|
313
|
+
return adaptIoredis2(createIoredisClient2(source));
|
|
314
|
+
}
|
|
315
|
+
case "cluster": {
|
|
316
|
+
const { adaptCluster: adaptCluster2, createIoredisCluster: createIoredisCluster2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
|
|
317
|
+
return adaptCluster2(createIoredisCluster2(source));
|
|
318
|
+
}
|
|
319
|
+
case "sentinel": {
|
|
320
|
+
const { adaptIoredis: adaptIoredis2, createIoredisSentinel: createIoredisSentinel2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
|
|
321
|
+
return adaptIoredis2(createIoredisSentinel2(source));
|
|
322
|
+
}
|
|
323
|
+
default: {
|
|
324
|
+
const exhaustive = source;
|
|
325
|
+
throw new Error(
|
|
326
|
+
`[next-cache] unknown client config: ${JSON.stringify(exhaustive)}`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/seed/index.ts
|
|
333
|
+
init_esm_shims();
|
|
334
|
+
|
|
335
|
+
// src/shared/compress.ts
|
|
336
|
+
init_esm_shims();
|
|
337
|
+
var gzipAsync = promisify(gzip);
|
|
338
|
+
promisify(gunzip);
|
|
339
|
+
var brotliCompressAsync = promisify(brotliCompress);
|
|
340
|
+
promisify(brotliDecompress);
|
|
341
|
+
var GZIP_MARKER = "__ncgz__:";
|
|
342
|
+
var BROTLI_MARKER = "__ncbr__:";
|
|
343
|
+
var MIN_COMPRESS_BYTES = 1024;
|
|
344
|
+
var BROTLI_OPTS = {
|
|
345
|
+
params: { [constants.BROTLI_PARAM_QUALITY]: 5 }
|
|
346
|
+
};
|
|
347
|
+
async function compressValue(value, algo) {
|
|
348
|
+
if (!algo || Buffer.byteLength(value) < MIN_COMPRESS_BYTES) return value;
|
|
349
|
+
const input = Buffer.from(value);
|
|
350
|
+
if (algo === "gzip") {
|
|
351
|
+
return GZIP_MARKER + (await gzipAsync(input)).toString("base64");
|
|
352
|
+
}
|
|
353
|
+
return BROTLI_MARKER + (await brotliCompressAsync(input, BROTLI_OPTS)).toString("base64");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/shared/namespace.ts
|
|
357
|
+
init_esm_shims();
|
|
358
|
+
var FALLBACK = "unversioned";
|
|
359
|
+
function resolveBuildNamespace(override) {
|
|
360
|
+
if (override !== void 0) {
|
|
361
|
+
return typeof override === "function" ? override() : override;
|
|
362
|
+
}
|
|
363
|
+
return process.env.DEPLOYMENT_VERSION ?? process.env.GIT_HASH ?? process.env.NEXT_DEPLOYMENT_ID ?? FALLBACK;
|
|
364
|
+
}
|
|
365
|
+
function buildKey(prefix, namespace, rest, hashTag = false) {
|
|
366
|
+
const ns = hashTag ? `{${namespace}}` : namespace;
|
|
367
|
+
return `${prefix}${ns}:${rest}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/incremental/serialize.ts
|
|
371
|
+
init_esm_shims();
|
|
372
|
+
function replacer(key, value) {
|
|
373
|
+
const raw = this[key];
|
|
374
|
+
if (Buffer.isBuffer(raw)) {
|
|
375
|
+
const env = {
|
|
376
|
+
__type: "Buffer",
|
|
377
|
+
data: raw.toString("base64")
|
|
378
|
+
};
|
|
379
|
+
return env;
|
|
380
|
+
}
|
|
381
|
+
if (value instanceof Map) {
|
|
382
|
+
const env = {
|
|
383
|
+
__type: "Map",
|
|
384
|
+
entries: Array.from(value.entries())
|
|
385
|
+
};
|
|
386
|
+
return env;
|
|
387
|
+
}
|
|
388
|
+
return value;
|
|
389
|
+
}
|
|
390
|
+
function serializeCacheRecord(record) {
|
|
391
|
+
return JSON.stringify(record, replacer);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/seed/index.ts
|
|
395
|
+
var ONE_YEAR_SEC = 60 * 60 * 24 * 365;
|
|
396
|
+
var MIN_TTL_SEC = 60;
|
|
397
|
+
var DEFAULT_KEY_PREFIX = "next-incremental:";
|
|
398
|
+
function clampTtl(revalidate) {
|
|
399
|
+
if (revalidate === false || revalidate === void 0) return ONE_YEAR_SEC;
|
|
400
|
+
if (typeof revalidate === "number" && revalidate > 0) {
|
|
401
|
+
return Math.min(Math.max(Math.ceil(revalidate), MIN_TTL_SEC), ONE_YEAR_SEC);
|
|
402
|
+
}
|
|
403
|
+
return MIN_TTL_SEC;
|
|
404
|
+
}
|
|
405
|
+
function routeToFileBase(route) {
|
|
406
|
+
return route === "/" ? "/index" : route;
|
|
407
|
+
}
|
|
408
|
+
function readMeta(file) {
|
|
409
|
+
try {
|
|
410
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
411
|
+
} catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async function writeRecord(client, key, value, revalidateSec, opts) {
|
|
416
|
+
const record = {
|
|
417
|
+
lastModified: Date.now(),
|
|
418
|
+
value,
|
|
419
|
+
revalidateSec: clampTtl(revalidateSec)
|
|
420
|
+
};
|
|
421
|
+
const redisKey = buildKey(`${opts.keyPrefix}entry:`, opts.ns, key, opts.hashTag);
|
|
422
|
+
const payload = await compressValue(
|
|
423
|
+
serializeCacheRecord(record),
|
|
424
|
+
opts.compression
|
|
425
|
+
);
|
|
426
|
+
const result = await client.set(redisKey, payload, {
|
|
427
|
+
EX: clampTtl(revalidateSec),
|
|
428
|
+
NX: true
|
|
429
|
+
});
|
|
430
|
+
return result === null ? "skipped" : "written";
|
|
431
|
+
}
|
|
432
|
+
async function seedBuildOutput(options) {
|
|
433
|
+
const dir = path4.resolve(options.dir ?? ".next");
|
|
434
|
+
const keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX;
|
|
435
|
+
const hashTag = options.hashTag ?? false;
|
|
436
|
+
const ns = resolveBuildNamespace(options.buildNamespace);
|
|
437
|
+
const warn = options.logger?.warn ?? (() => {
|
|
438
|
+
});
|
|
439
|
+
const summary = {
|
|
440
|
+
routes: 0,
|
|
441
|
+
pages: 0,
|
|
442
|
+
fetch: 0,
|
|
443
|
+
skippedExisting: 0,
|
|
444
|
+
skippedIncomplete: 0,
|
|
445
|
+
skippedRouteHandlers: 0,
|
|
446
|
+
errors: []
|
|
447
|
+
};
|
|
448
|
+
const manifestPath = path4.join(dir, "prerender-manifest.json");
|
|
449
|
+
if (!fs.existsSync(manifestPath)) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`[next-cache] no prerender-manifest.json in ${dir} \u2014 run \`next build\` first`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
455
|
+
const client = await buildClient(options.client);
|
|
456
|
+
if (!client.isOpen) await client.connect();
|
|
457
|
+
const writeOpts = { keyPrefix, hashTag, ns, ...options.compression ? { compression: options.compression } : {} };
|
|
458
|
+
try {
|
|
459
|
+
for (const [route, info] of Object.entries(manifest.routes ?? {})) {
|
|
460
|
+
try {
|
|
461
|
+
const isPages = typeof info.dataRoute === "string" && info.dataRoute.endsWith(".json");
|
|
462
|
+
const base = routeToFileBase(route);
|
|
463
|
+
if (isPages) {
|
|
464
|
+
const html = path4.join(dir, "server", "pages", `${base.slice(1)}.html`);
|
|
465
|
+
const data = path4.join(dir, "server", "pages", `${base.slice(1)}.json`);
|
|
466
|
+
if (!fs.existsSync(html)) {
|
|
467
|
+
summary.skippedIncomplete += 1;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const value2 = {
|
|
471
|
+
kind: "PAGES",
|
|
472
|
+
html: fs.readFileSync(html, "utf8"),
|
|
473
|
+
pageData: fs.existsSync(data) ? JSON.parse(fs.readFileSync(data, "utf8")) : {},
|
|
474
|
+
headers: void 0,
|
|
475
|
+
status: void 0
|
|
476
|
+
};
|
|
477
|
+
const r2 = await writeRecord(client, route, value2, info.initialRevalidateSeconds, writeOpts);
|
|
478
|
+
if (r2 === "written") summary.pages += 1;
|
|
479
|
+
else summary.skippedExisting += 1;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const htmlPath = path4.join(dir, "server", "app", `${base.slice(1)}.html`);
|
|
483
|
+
const rscPath = path4.join(dir, "server", "app", `${base.slice(1)}.rsc`);
|
|
484
|
+
const metaPath = path4.join(dir, "server", "app", `${base.slice(1)}.meta`);
|
|
485
|
+
if (!fs.existsSync(htmlPath) || !fs.existsSync(metaPath)) {
|
|
486
|
+
if (fs.existsSync(path4.join(dir, "server", "app", `${base.slice(1)}.body`))) {
|
|
487
|
+
summary.skippedRouteHandlers += 1;
|
|
488
|
+
} else {
|
|
489
|
+
summary.skippedIncomplete += 1;
|
|
490
|
+
}
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const meta = readMeta(metaPath) ?? {};
|
|
494
|
+
if (meta.postponed) {
|
|
495
|
+
summary.skippedIncomplete += 1;
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
let segmentData;
|
|
499
|
+
if (Array.isArray(meta.segmentPaths) && meta.segmentPaths.length > 0) {
|
|
500
|
+
const segDir = path4.join(dir, "server", "app", `${base.slice(1)}.segments`);
|
|
501
|
+
segmentData = /* @__PURE__ */ new Map();
|
|
502
|
+
for (const segPath of meta.segmentPaths) {
|
|
503
|
+
const segFile = path4.join(segDir, `${segPath.slice(1)}.segment.rsc`);
|
|
504
|
+
if (fs.existsSync(segFile)) {
|
|
505
|
+
segmentData.set(segPath, fs.readFileSync(segFile));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (segmentData.size !== meta.segmentPaths.length) {
|
|
509
|
+
summary.skippedIncomplete += 1;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const value = {
|
|
514
|
+
kind: "APP_PAGE",
|
|
515
|
+
html: fs.readFileSync(htmlPath, "utf8"),
|
|
516
|
+
rscData: fs.existsSync(rscPath) ? fs.readFileSync(rscPath) : void 0,
|
|
517
|
+
headers: meta.headers,
|
|
518
|
+
status: meta.status
|
|
519
|
+
};
|
|
520
|
+
if (segmentData) value.segmentData = segmentData;
|
|
521
|
+
const r = await writeRecord(client, route, value, info.initialRevalidateSeconds, writeOpts);
|
|
522
|
+
if (r === "written") summary.routes += 1;
|
|
523
|
+
else summary.skippedExisting += 1;
|
|
524
|
+
} catch (err) {
|
|
525
|
+
summary.errors.push(`${route}: ${err.message}`);
|
|
526
|
+
warn("seed: route failed", { route, message: err.message });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const fetchDir = path4.join(dir, "cache", "fetch-cache");
|
|
530
|
+
if (fs.existsSync(fetchDir)) {
|
|
531
|
+
for (const file of fs.readdirSync(fetchDir)) {
|
|
532
|
+
if (file.startsWith(".")) continue;
|
|
533
|
+
try {
|
|
534
|
+
const raw = fs.readFileSync(path4.join(fetchDir, file), "utf8");
|
|
535
|
+
const value = JSON.parse(raw);
|
|
536
|
+
if (value.kind !== "FETCH") continue;
|
|
537
|
+
const r = await writeRecord(client, file, value, value.revalidate, writeOpts);
|
|
538
|
+
if (r === "written") summary.fetch += 1;
|
|
539
|
+
else summary.skippedExisting += 1;
|
|
540
|
+
} catch (err) {
|
|
541
|
+
summary.errors.push(`fetch:${file}: ${err.message}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} finally {
|
|
546
|
+
const c = client;
|
|
547
|
+
try {
|
|
548
|
+
(c.dispose ?? c.destroy ?? c.disconnect)?.call(c);
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return summary;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/cli/index.ts
|
|
556
|
+
var OK = "[ok]";
|
|
557
|
+
var WARN = "[warn]";
|
|
558
|
+
var ACTION = "[action]";
|
|
559
|
+
var ERR = "[error]";
|
|
560
|
+
function readJson(file) {
|
|
561
|
+
try {
|
|
562
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
563
|
+
} catch {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function detectNextMajor(pkg) {
|
|
568
|
+
const deps = {
|
|
569
|
+
...pkg.dependencies,
|
|
570
|
+
...pkg.devDependencies
|
|
571
|
+
};
|
|
572
|
+
const raw = deps["next"] ?? null;
|
|
573
|
+
if (!raw) return { raw: null, major: null, supportsCacheComponents: false };
|
|
574
|
+
const m = raw.match(/(\d+)\.(\d+)/);
|
|
575
|
+
if (!m) return { raw, major: null, supportsCacheComponents: false };
|
|
576
|
+
const major = Number(m[1]);
|
|
577
|
+
const minor = Number(m[2]);
|
|
578
|
+
const supportsCacheComponents = major > 16 ? false : major === 16 && minor >= 1;
|
|
579
|
+
return { raw, major, supportsCacheComponents };
|
|
580
|
+
}
|
|
581
|
+
function detectClient(pkg) {
|
|
582
|
+
const deps = {
|
|
583
|
+
...pkg.dependencies,
|
|
584
|
+
...pkg.devDependencies
|
|
585
|
+
};
|
|
586
|
+
if (deps["redis"]) return "redis";
|
|
587
|
+
if (deps["ioredis"]) return "ioredis";
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
function findNextConfig(cwd) {
|
|
591
|
+
for (const name of ["next.config.ts", "next.config.js", "next.config.mjs", "next.config.cjs"]) {
|
|
592
|
+
const p = path4.join(cwd, name);
|
|
593
|
+
if (fs.existsSync(p)) return p;
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
var RULES_BEGIN = "<!-- nextjs-cache-handler:rules:begin -->";
|
|
598
|
+
var RULES_END = "<!-- nextjs-cache-handler:rules:end -->";
|
|
599
|
+
function packagedFile(rel) {
|
|
600
|
+
const root = path4.resolve(__dirname$1, "..", "..");
|
|
601
|
+
const p = path4.join(root, rel);
|
|
602
|
+
return fs.existsSync(p) ? p : null;
|
|
603
|
+
}
|
|
604
|
+
function shimSource(kind, clientType) {
|
|
605
|
+
const factory = kind === "incremental" ? "createIncrementalCacheHandler" : "createCacheComponentsHandler";
|
|
606
|
+
const subpath = kind === "incremental" ? "incremental" : "cache-components";
|
|
607
|
+
return `// Generated by \`npx nextjs-cache-handler init\`
|
|
608
|
+
const { ${factory} } = require("@leejpsd/nextjs-cache-handler/${subpath}");
|
|
609
|
+
|
|
610
|
+
module.exports = ${factory}({
|
|
611
|
+
client: { type: "${clientType}", url: process.env.REDIS_URL },
|
|
612
|
+
// Per-deploy namespace isolation \u2014 set DEPLOYMENT_VERSION in every runtime
|
|
613
|
+
// environment (Docker: the runner stage), e.g. the git SHA.
|
|
614
|
+
buildNamespace: () => process.env.DEPLOYMENT_VERSION,
|
|
615
|
+
});
|
|
616
|
+
`;
|
|
617
|
+
}
|
|
618
|
+
async function init(ctx) {
|
|
619
|
+
const { cwd, flags, log } = ctx;
|
|
620
|
+
const apply = flags.has("--yes") && !flags.has("--dry-run");
|
|
621
|
+
const pkg = readJson(path4.join(cwd, "package.json"));
|
|
622
|
+
if (!pkg) {
|
|
623
|
+
log(`${ERR} no package.json in ${cwd} \u2014 run inside your Next.js project`);
|
|
624
|
+
return 1;
|
|
625
|
+
}
|
|
626
|
+
const next = detectNextMajor(pkg);
|
|
627
|
+
if (!next.raw) {
|
|
628
|
+
log(`${ERR} "next" is not a dependency here \u2014 not a Next.js project`);
|
|
629
|
+
return 1;
|
|
630
|
+
}
|
|
631
|
+
if (next.major !== null && next.major < 15) {
|
|
632
|
+
log(`${ERR} next@${next.raw} unsupported \u2014 this package needs next >=15.0.0 <17`);
|
|
633
|
+
return 1;
|
|
634
|
+
}
|
|
635
|
+
log(`${OK} next: ${next.raw} \u2192 ${next.supportsCacheComponents ? "BOTH handlers (ISR + 'use cache')" : "ISR handler only"}`);
|
|
636
|
+
const deps = pkg.dependencies;
|
|
637
|
+
if (!deps?.["@leejpsd/nextjs-cache-handler"]) {
|
|
638
|
+
log(`${ACTION} install the package first: npm i @leejpsd/nextjs-cache-handler`);
|
|
639
|
+
} else {
|
|
640
|
+
log(`${OK} @leejpsd/nextjs-cache-handler ${deps["@leejpsd/nextjs-cache-handler"]}`);
|
|
641
|
+
}
|
|
642
|
+
const client = detectClient(pkg);
|
|
643
|
+
if (!client) {
|
|
644
|
+
log(`${ACTION} no Redis client installed \u2014 run: npm i redis (or ioredis for Cluster/Sentinel)`);
|
|
645
|
+
} else {
|
|
646
|
+
log(`${OK} redis client: ${client}`);
|
|
647
|
+
}
|
|
648
|
+
const clientType = client ?? "redis";
|
|
649
|
+
const wanted = [
|
|
650
|
+
["incremental", "cache-incremental.cjs"]
|
|
651
|
+
];
|
|
652
|
+
if (next.supportsCacheComponents) wanted.push(["cache-components", "cache-components.cjs"]);
|
|
653
|
+
for (const [kind, file] of wanted) {
|
|
654
|
+
const target = path4.join(cwd, file);
|
|
655
|
+
if (fs.existsSync(target)) {
|
|
656
|
+
log(`${OK} ${file} already exists \u2014 leaving it untouched`);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (apply) {
|
|
660
|
+
fs.writeFileSync(target, shimSource(kind, clientType));
|
|
661
|
+
log(`${OK} wrote ${file}`);
|
|
662
|
+
} else {
|
|
663
|
+
log(`${ACTION} would create ${file} (re-run with --yes to write)`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const cfg = findNextConfig(cwd);
|
|
667
|
+
const cfgLines = [
|
|
668
|
+
`cacheHandler: require.resolve("./cache-incremental.cjs"),`,
|
|
669
|
+
...next.supportsCacheComponents ? [
|
|
670
|
+
`cacheHandlers: { default: require.resolve("./cache-components.cjs") },`,
|
|
671
|
+
`cacheComponents: true,`
|
|
672
|
+
] : [],
|
|
673
|
+
`cacheMaxMemorySize: 0, // required for multi-instance correctness`
|
|
674
|
+
];
|
|
675
|
+
if (!cfg) {
|
|
676
|
+
log(`${WARN} no next.config found \u2014 create one with:`);
|
|
677
|
+
} else {
|
|
678
|
+
const body = fs.readFileSync(cfg, "utf8");
|
|
679
|
+
if (body.includes("cacheHandler")) {
|
|
680
|
+
log(`${OK} ${path4.basename(cfg)} already references a cacheHandler \u2014 verify it points at the shims above`);
|
|
681
|
+
} else {
|
|
682
|
+
log(`${ACTION} add to ${path4.basename(cfg)}:`);
|
|
683
|
+
}
|
|
684
|
+
if (cfg.endsWith(".mjs")) {
|
|
685
|
+
log(`${WARN} ESM config: add import { createRequire } from "module"; const require = createRequire(import.meta.url);`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
for (const l of cfgLines) log(` ${l}`);
|
|
689
|
+
const envExample = path4.join(cwd, ".env.example");
|
|
690
|
+
const envBlock = `
|
|
691
|
+
# @leejpsd/nextjs-cache-handler
|
|
692
|
+
REDIS_URL=redis://127.0.0.1:6379
|
|
693
|
+
DEPLOYMENT_VERSION=dev
|
|
694
|
+
`;
|
|
695
|
+
if (fs.existsSync(envExample)) {
|
|
696
|
+
const body = fs.readFileSync(envExample, "utf8");
|
|
697
|
+
if (body.includes("REDIS_URL")) {
|
|
698
|
+
log(`${OK} .env.example already mentions REDIS_URL`);
|
|
699
|
+
} else if (apply) {
|
|
700
|
+
fs.appendFileSync(envExample, envBlock);
|
|
701
|
+
log(`${OK} appended REDIS_URL / DEPLOYMENT_VERSION to .env.example`);
|
|
702
|
+
} else {
|
|
703
|
+
log(`${ACTION} would append REDIS_URL / DEPLOYMENT_VERSION to .env.example`);
|
|
704
|
+
}
|
|
705
|
+
} else {
|
|
706
|
+
log(`${ACTION} set REDIS_URL and DEPLOYMENT_VERSION in every runtime environment`);
|
|
707
|
+
}
|
|
708
|
+
const rulesSrc = packagedFile("rules/nextjs-cache-rules.md");
|
|
709
|
+
const ruleTargets = ["CLAUDE.md", "AGENTS.md"].map((f) => path4.join(cwd, f)).filter((p) => fs.existsSync(p));
|
|
710
|
+
if (rulesSrc && ruleTargets.length > 0) {
|
|
711
|
+
const rules = `
|
|
712
|
+
${RULES_BEGIN}
|
|
713
|
+
${fs.readFileSync(rulesSrc, "utf8")}
|
|
714
|
+
${RULES_END}
|
|
715
|
+
`;
|
|
716
|
+
for (const target of ruleTargets) {
|
|
717
|
+
const body = fs.readFileSync(target, "utf8");
|
|
718
|
+
if (body.includes(RULES_BEGIN)) {
|
|
719
|
+
log(`${OK} ${path4.basename(target)} already has the cache rules block`);
|
|
720
|
+
} else if (apply) {
|
|
721
|
+
fs.appendFileSync(target, rules);
|
|
722
|
+
log(`${OK} appended cache rules to ${path4.basename(target)}`);
|
|
723
|
+
} else {
|
|
724
|
+
log(`${ACTION} would append cache rules to ${path4.basename(target)}`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
} else if (ruleTargets.length === 0) {
|
|
728
|
+
log(`${WARN} no CLAUDE.md/AGENTS.md found \u2014 skip rules injection (create one to guide your agent)`);
|
|
729
|
+
}
|
|
730
|
+
const mcpPath = path4.join(cwd, ".mcp.json");
|
|
731
|
+
if (fs.existsSync(mcpPath)) {
|
|
732
|
+
const body = fs.readFileSync(mcpPath, "utf8");
|
|
733
|
+
if (body.includes("nextjs-cache-handler-mcp")) {
|
|
734
|
+
log(`${OK} .mcp.json already registers the cache MCP server`);
|
|
735
|
+
} else {
|
|
736
|
+
log(`${ACTION} .mcp.json exists \u2014 add the "nextjs-cache" server manually (see @leejpsd/nextjs-cache-handler-mcp README)`);
|
|
737
|
+
}
|
|
738
|
+
} else if (apply) {
|
|
739
|
+
fs.writeFileSync(
|
|
740
|
+
mcpPath,
|
|
741
|
+
JSON.stringify(
|
|
742
|
+
{
|
|
743
|
+
mcpServers: {
|
|
744
|
+
"nextjs-cache": {
|
|
745
|
+
command: "npx",
|
|
746
|
+
args: ["-y", "@leejpsd/nextjs-cache-handler-mcp"],
|
|
747
|
+
env: {
|
|
748
|
+
REDIS_URL: "redis://127.0.0.1:6379",
|
|
749
|
+
DEPLOYMENT_VERSION: "dev"
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
null,
|
|
755
|
+
2
|
|
756
|
+
) + "\n"
|
|
757
|
+
);
|
|
758
|
+
log(`${OK} wrote .mcp.json \u2014 your agent now has cache_health/tag_state/... tools (edit REDIS_URL for real environments)`);
|
|
759
|
+
} else {
|
|
760
|
+
log(`${ACTION} would create .mcp.json registering @leejpsd/nextjs-cache-handler-mcp`);
|
|
761
|
+
}
|
|
762
|
+
if (flags.has("--skills")) {
|
|
763
|
+
const skillSrc = packagedFile("skills/nextjs-redis-cache/SKILL.md");
|
|
764
|
+
if (skillSrc) {
|
|
765
|
+
const dest = path4.join(cwd, ".claude", "skills", "nextjs-redis-cache", "SKILL.md");
|
|
766
|
+
if (apply) {
|
|
767
|
+
fs.mkdirSync(path4.dirname(dest), { recursive: true });
|
|
768
|
+
fs.copyFileSync(skillSrc, dest);
|
|
769
|
+
log(`${OK} installed skill to .claude/skills/nextjs-redis-cache/`);
|
|
770
|
+
} else {
|
|
771
|
+
log(`${ACTION} would install skill to .claude/skills/nextjs-redis-cache/`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
log(`${OK} init ${apply ? "complete" : "preview complete \u2014 re-run with --yes to apply"}. Verify with: npx nextjs-cache-handler doctor`);
|
|
776
|
+
return 0;
|
|
777
|
+
}
|
|
778
|
+
async function doctor(ctx) {
|
|
779
|
+
const { args, log } = ctx;
|
|
780
|
+
const urlIdx = args.indexOf("--url");
|
|
781
|
+
const url = urlIdx >= 0 ? args[urlIdx + 1] : process.env.REDIS_URL;
|
|
782
|
+
if (!url) {
|
|
783
|
+
log(`${ERR} no Redis URL \u2014 pass --url <redis-url> or set REDIS_URL`);
|
|
784
|
+
return 1;
|
|
785
|
+
}
|
|
786
|
+
const deployment = process.env.DEPLOYMENT_VERSION;
|
|
787
|
+
log(deployment ? `${OK} DEPLOYMENT_VERSION=${deployment}` : `${WARN} DEPLOYMENT_VERSION not set \u2014 keys will use the "unversioned" namespace`);
|
|
788
|
+
let client;
|
|
789
|
+
try {
|
|
790
|
+
try {
|
|
791
|
+
client = await buildClient({ type: "redis", url });
|
|
792
|
+
} catch (err) {
|
|
793
|
+
if (/Cannot find module/.test(err.message)) {
|
|
794
|
+
client = await buildClient({ type: "ioredis", url });
|
|
795
|
+
} else {
|
|
796
|
+
throw err;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (!client.isOpen) await client.connect();
|
|
800
|
+
} catch (err) {
|
|
801
|
+
log(`${ERR} connect failed: ${err.message}`);
|
|
802
|
+
log(`${ACTION} check the URL/network, or install a client: npm i redis (or ioredis)`);
|
|
803
|
+
return 1;
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
const t0 = Date.now();
|
|
807
|
+
await client.ping();
|
|
808
|
+
log(`${OK} redis PING ${Date.now() - t0}ms (${url.replace(/\/\/[^@]*@/, "//***@")})`);
|
|
809
|
+
const counts = /* @__PURE__ */ new Map();
|
|
810
|
+
for await (const chunk of client.scanIterator({ MATCH: "next-*", COUNT: 500 })) {
|
|
811
|
+
const keys = Array.isArray(chunk) ? chunk : [chunk];
|
|
812
|
+
for (const k of keys) {
|
|
813
|
+
const key = String(k);
|
|
814
|
+
const m = key.match(/^(next-cache|next-incremental):([a-z-]+):/);
|
|
815
|
+
const bucket = m ? `${m[1]}:${m[2]}` : "other";
|
|
816
|
+
counts.set(bucket, (counts.get(bucket) ?? 0) + 1);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (counts.size === 0) {
|
|
820
|
+
log(`${WARN} no next-* keys found \u2014 cache not yet exercised (or a custom keyPrefix is in use)`);
|
|
821
|
+
} else {
|
|
822
|
+
for (const [bucket, n] of [...counts.entries()].sort()) {
|
|
823
|
+
log(`${OK} ${bucket}* \u2014 ${n} keys`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const probeKey = `next-cache:doctor:${Date.now()}`;
|
|
827
|
+
await client.set(probeKey, "ok", { EX: 30 });
|
|
828
|
+
const back = await client.get(probeKey);
|
|
829
|
+
await client.del(probeKey);
|
|
830
|
+
log(back === "ok" ? `${OK} write/read round-trip` : `${ERR} round-trip failed`);
|
|
831
|
+
return back === "ok" ? 0 : 1;
|
|
832
|
+
} finally {
|
|
833
|
+
client.dispose?.();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
async function seed(ctx) {
|
|
837
|
+
const { args, flags, log } = ctx;
|
|
838
|
+
const urlIdx = args.indexOf("--url");
|
|
839
|
+
const url = urlIdx >= 0 ? args[urlIdx + 1] : process.env.REDIS_URL;
|
|
840
|
+
if (!url) {
|
|
841
|
+
log(`${ERR} no Redis URL \u2014 pass --url <redis-url> or set REDIS_URL`);
|
|
842
|
+
return 1;
|
|
843
|
+
}
|
|
844
|
+
const flagVal = (name) => {
|
|
845
|
+
const i = args.indexOf(name);
|
|
846
|
+
return i >= 0 ? args[i + 1] : void 0;
|
|
847
|
+
};
|
|
848
|
+
const dir = flagVal("--dir") ?? ".next";
|
|
849
|
+
const keyPrefix = flagVal("--key-prefix");
|
|
850
|
+
const namespace = flagVal("--namespace");
|
|
851
|
+
const compression = flagVal("--compression");
|
|
852
|
+
const hashTag = flags.has("--hash-tag");
|
|
853
|
+
const ns = namespace ?? process.env.DEPLOYMENT_VERSION ?? process.env.GIT_HASH ?? "unversioned";
|
|
854
|
+
if (ns === "unversioned") {
|
|
855
|
+
log(`${WARN} no namespace \u2014 seeding into "unversioned" (set DEPLOYMENT_VERSION or pass --namespace)`);
|
|
856
|
+
}
|
|
857
|
+
log(`${OK} target keys: ${keyPrefix ?? "next-incremental:"}entry:${hashTag ? `{${ns}}` : ns}:*`);
|
|
858
|
+
const seedOpts = {
|
|
859
|
+
dir,
|
|
860
|
+
buildNamespace: ns,
|
|
861
|
+
...keyPrefix ? { keyPrefix } : {},
|
|
862
|
+
...compression ? { compression } : {},
|
|
863
|
+
...hashTag ? { hashTag: true } : {}
|
|
864
|
+
};
|
|
865
|
+
const report = (summary) => {
|
|
866
|
+
log(`${OK} seeded: ${summary.routes} app routes, ${summary.pages} pages routes, ${summary.fetch} fetch entries`);
|
|
867
|
+
if (summary.skippedExisting > 0)
|
|
868
|
+
log(`${OK} left ${summary.skippedExisting} newer live entries untouched (NX)`);
|
|
869
|
+
if (summary.skippedRouteHandlers > 0)
|
|
870
|
+
log(`${OK} ${summary.skippedRouteHandlers} prerendered route handlers not seeded (by design)`);
|
|
871
|
+
if (summary.skippedIncomplete > 0)
|
|
872
|
+
log(`${WARN} skipped ${summary.skippedIncomplete} routes (incomplete files or PPR resume state)`);
|
|
873
|
+
for (const e of summary.errors.slice(0, 5)) log(`${WARN} ${e}`);
|
|
874
|
+
return summary.errors.length > 0 ? 1 : 0;
|
|
875
|
+
};
|
|
876
|
+
try {
|
|
877
|
+
return report(
|
|
878
|
+
await seedBuildOutput({ client: { type: "redis", url }, ...seedOpts })
|
|
879
|
+
);
|
|
880
|
+
} catch (err) {
|
|
881
|
+
const msg = err.message;
|
|
882
|
+
if (/Cannot find module 'redis'/.test(msg)) {
|
|
883
|
+
try {
|
|
884
|
+
return report(
|
|
885
|
+
await seedBuildOutput({ client: { type: "ioredis", url }, ...seedOpts })
|
|
886
|
+
);
|
|
887
|
+
} catch (err2) {
|
|
888
|
+
log(`${ERR} ${err2.message}`);
|
|
889
|
+
return 1;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
log(`${ERR} ${msg}`);
|
|
893
|
+
return 1;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
async function main(argv = process.argv.slice(2)) {
|
|
897
|
+
const [cmd, ...rest] = argv;
|
|
898
|
+
const ctx = {
|
|
899
|
+
cwd: process.cwd(),
|
|
900
|
+
args: rest,
|
|
901
|
+
flags: new Set(rest.filter((a) => a.startsWith("--"))),
|
|
902
|
+
log: (l) => console.log(l)
|
|
903
|
+
};
|
|
904
|
+
switch (cmd) {
|
|
905
|
+
case "init":
|
|
906
|
+
return init(ctx);
|
|
907
|
+
case "doctor":
|
|
908
|
+
return doctor(ctx);
|
|
909
|
+
case "seed":
|
|
910
|
+
return seed(ctx);
|
|
911
|
+
default:
|
|
912
|
+
console.log(`nextjs-cache-handler <command>
|
|
913
|
+
|
|
914
|
+
Commands:
|
|
915
|
+
init [--yes] [--dry-run] [--skills] wire handlers into this Next.js app
|
|
916
|
+
doctor [--url <redis-url>] connectivity + cache diagnostics
|
|
917
|
+
seed [--dir .next] [--url <u>] [--namespace <ns>] [--key-prefix <p>] [--hash-tag] [--compression gzip|brotli]
|
|
918
|
+
seed prerendered build output into Redis
|
|
919
|
+
|
|
920
|
+
Agent setup instructions:
|
|
921
|
+
https://raw.githubusercontent.com/leejpsd/nextjs-cache-handler/main/setup-instructions/setup.md`);
|
|
922
|
+
return cmd ? 1 : 0;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export { doctor, init, main, seed };
|
|
927
|
+
//# sourceMappingURL=index.js.map
|
|
928
|
+
//# sourceMappingURL=index.js.map
|