@edgeone/nuxt-pages 1.1.0 → 1.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.
@@ -8,8 +8,8 @@ import {
8
8
  cleanupEdgeoneModule,
9
9
  injectEdgeoneModule,
10
10
  removeTempModule
11
- } from "../../esm-chunks/chunk-ZR3DYEBK.js";
12
- import "../../esm-chunks/chunk-VCJ5U4PV.js";
11
+ } from "../../esm-chunks/chunk-PHIWUHZJ.js";
12
+ import "../../esm-chunks/chunk-VPL326TW.js";
13
13
  import "../../esm-chunks/chunk-6BT4RYQJ.js";
14
14
  export {
15
15
  cleanupEdgeoneModule,
@@ -7,7 +7,7 @@
7
7
  import {
8
8
  createServerHandler,
9
9
  patchNitroHandler
10
- } from "../../esm-chunks/chunk-PJWA566S.js";
10
+ } from "../../esm-chunks/chunk-4B7OVONM.js";
11
11
  import "../../esm-chunks/chunk-K5VT7O2H.js";
12
12
  import "../../esm-chunks/chunk-6BT4RYQJ.js";
13
13
  export {
@@ -7,9 +7,9 @@
7
7
  import {
8
8
  PluginContext,
9
9
  SERVER_HANDLER_NAME
10
- } from "../esm-chunks/chunk-VDBASNGL.js";
11
- import "../esm-chunks/chunk-AZLY3JO2.js";
12
- import "../esm-chunks/chunk-VCJ5U4PV.js";
10
+ } from "../esm-chunks/chunk-VLRIK4OQ.js";
11
+ import "../esm-chunks/chunk-ODJXW6KT.js";
12
+ import "../esm-chunks/chunk-VPL326TW.js";
13
13
  import "../esm-chunks/chunk-6BT4RYQJ.js";
14
14
  export {
15
15
  PluginContext,
@@ -8,9 +8,10 @@ import {
8
8
  createNuxtApiRoutesMeta,
9
9
  createNuxtPagesRouteMeta,
10
10
  createNuxtRoutesMeta
11
- } from "../esm-chunks/chunk-3HA3DZ3G.js";
12
- import "../esm-chunks/chunk-AZLY3JO2.js";
13
- import "../esm-chunks/chunk-VCJ5U4PV.js";
11
+ } from "../esm-chunks/chunk-D5KM3RWE.js";
12
+ import "../esm-chunks/chunk-ODJXW6KT.js";
13
+ import "../esm-chunks/chunk-QUFJCWGJ.js";
14
+ import "../esm-chunks/chunk-VPL326TW.js";
14
15
  import "../esm-chunks/chunk-6BT4RYQJ.js";
15
16
  export {
16
17
  createNuxtApiRoutesMeta,
@@ -0,0 +1,15 @@
1
+
2
+ var require = await (async () => {
3
+ var { createRequire } = await import("node:module");
4
+ return createRequire(import.meta.url);
5
+ })();
6
+
7
+ import {
8
+ buildTagManifest,
9
+ extractCacheTags
10
+ } from "../esm-chunks/chunk-QUFJCWGJ.js";
11
+ import "../esm-chunks/chunk-6BT4RYQJ.js";
12
+ export {
13
+ buildTagManifest,
14
+ extractCacheTags
15
+ };
@@ -0,0 +1,215 @@
1
+ /**
2
+ * ISR (Incremental Static Regeneration) runtime for the EdgeOne Nuxt adapter.
3
+ *
4
+ * Extracted from the handler template so the handler stays thin. This module is
5
+ * copied next to handler.js in the server-handler directory at build time and
6
+ * imported from there; it reads the build-time manifests (isr-manifest.json /
7
+ * tag-manifest.json) that sit in the same directory.
8
+ *
9
+ * ISR on EdgeOne is a "CDN cache + purge" model:
10
+ * - applyIsr() tags cacheable responses with `eo-cdn-cache-control` /
11
+ * `cache-tag` so the edge caches + revalidates them.
12
+ * - handleRevalidate() purges the edge cache on demand (POST endpoint).
13
+ */
14
+ import { resolve, dirname } from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import { readFileSync, existsSync } from 'fs';
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+
20
+ const ISR_SWR_WINDOW = 31536000; // 1 year stale-while-revalidate window
21
+ export const REVALIDATE_ENDPOINT = '/__edgeone_revalidate';
22
+
23
+ let _isrRoutes = null; // [{ re: RegExp, revalidate, tags }]
24
+ function getIsrRoutes() {
25
+ if (_isrRoutes) return _isrRoutes;
26
+ _isrRoutes = [];
27
+ try {
28
+ const p = resolve(__dirname, 'isr-manifest.json');
29
+ if (existsSync(p)) {
30
+ const manifest = JSON.parse(readFileSync(p, 'utf-8'));
31
+ for (const r of manifest?.routes || []) {
32
+ if (!r?.src) continue;
33
+ try {
34
+ _isrRoutes.push({ re: new RegExp(r.src), revalidate: r.revalidate, tags: r.tags || [] });
35
+ } catch { /* skip invalid pattern */ }
36
+ }
37
+ }
38
+ } catch (err) {
39
+ console.warn('[nuxt-pages] Failed to load isr-manifest.json:', err?.message || err);
40
+ }
41
+ return _isrRoutes;
42
+ }
43
+
44
+ let _tagManifest = null; // { tag: [paths] }
45
+ function getTagManifest() {
46
+ if (_tagManifest) return _tagManifest;
47
+ _tagManifest = { version: 1, tags: {} };
48
+ try {
49
+ const p = resolve(__dirname, 'tag-manifest.json');
50
+ if (existsSync(p)) {
51
+ _tagManifest = JSON.parse(readFileSync(p, 'utf-8'));
52
+ }
53
+ } catch (err) {
54
+ console.warn('[nuxt-pages] Failed to load tag-manifest.json:', err?.message || err);
55
+ }
56
+ return _tagManifest;
57
+ }
58
+
59
+ /** Find the first ISR route whose pattern matches the given pathname. */
60
+ function matchIsrRoute(pathname) {
61
+ for (const route of getIsrRoutes()) {
62
+ if (route.re.test(pathname)) return route;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ /** Set EdgeOne CDN cache-control for an ISR response (time-based revalidation). */
68
+ function setIsrCacheHeaders(headers, route) {
69
+ const revalidate = typeof route.revalidate === 'number' ? route.revalidate : ISR_SWR_WINDOW;
70
+ headers.set(
71
+ 'eo-cdn-cache-control',
72
+ `s-maxage=${revalidate}, stale-while-revalidate=${ISR_SWR_WINDOW}, durable`,
73
+ );
74
+ // Keep the browser from caching a stale copy; the edge holds the shared cache.
75
+ if (!headers.has('cache-control')) {
76
+ headers.set('cache-control', 'public, max-age=0, must-revalidate');
77
+ }
78
+ if (Array.isArray(route.tags) && route.tags.length > 0) {
79
+ headers.set('cache-tag', route.tags.join(','));
80
+ }
81
+ }
82
+
83
+ /** Real user-facing host. EdgeOne rewrites Host to the origin, real host is in `eo-pages-host`. */
84
+ function getRealHost(headers) {
85
+ const get = (k) => {
86
+ if (headers && typeof headers.get === 'function') return headers.get(k) || '';
87
+ return (headers && (headers[k] || headers[k.toLowerCase()])) || '';
88
+ };
89
+ return get('eo-pages-host') || get('host') || process.env.SITE_HOST || '';
90
+ }
91
+
92
+ /** Expand cache tags to concrete paths using the tag manifest (unknown tag -> treated as a path). */
93
+ function expandTagsToPaths(tags) {
94
+ const manifest = getTagManifest();
95
+ const paths = new Set();
96
+ for (const tag of tags) {
97
+ const mapped = manifest?.tags?.[tag];
98
+ if (Array.isArray(mapped) && mapped.length) {
99
+ for (const p of mapped) paths.add(p);
100
+ } else {
101
+ paths.add(tag); // revalidatePath scenario
102
+ }
103
+ }
104
+ return Array.from(paths);
105
+ }
106
+
107
+ /** Purge the EdgeOne CDN cache for a set of paths on the given host. */
108
+ async function purgeEdgeCache(paths, host) {
109
+ if (!paths || paths.length === 0) return { purged: 0 };
110
+ if (!host) {
111
+ console.error('[nuxt-pages] Cannot purge: no host (eo-pages-host header or SITE_HOST env)');
112
+ return { purged: 0, error: 'no-host' };
113
+ }
114
+
115
+ // Only concrete paths can be purged as URLs. Route patterns (containing
116
+ // dynamic segments like `**`, `:id`, `[id]`) can't be expanded to a single
117
+ // URL here — skip them and warn.
118
+ const isConcrete = (p) => typeof p === 'string' && !/[*:\[\]]/.test(p);
119
+ const skipped = paths.filter((p) => !isConcrete(p));
120
+ if (skipped.length) {
121
+ console.warn(
122
+ '[nuxt-pages] Skipped non-concrete purge target(s) — pass explicit paths to revalidate dynamic routes:',
123
+ skipped,
124
+ );
125
+ }
126
+
127
+ const fullUrls = paths
128
+ .filter(isConcrete)
129
+ .map((p) => `https://${host}${p.startsWith('/') ? p : `/${p}`}`);
130
+
131
+ if (fullUrls.length === 0) return { purged: 0, skipped };
132
+
133
+ const endpoint = process.env.IS_MAINLAND === 'true'
134
+ ? 'https://pages-api.cloud.tencent.com/eo/pages/hook/eo_purge'
135
+ : 'https://pages-api.edgeone.ai/eo/pages/hook/eo_purge?site=intl';
136
+
137
+ try {
138
+ const res = await fetch(endpoint, {
139
+ method: 'POST',
140
+ headers: { 'Content-Type': 'application/json' },
141
+ body: JSON.stringify({
142
+ RequestId: `purge-${Date.now()}`,
143
+ Token: process.env.PURGE_TOKEN,
144
+ paths: fullUrls,
145
+ }),
146
+ });
147
+ const text = await res.text();
148
+ let data;
149
+ try { data = JSON.parse(text); } catch {
150
+ console.error(`[nuxt-pages] Purge API non-JSON (${res.status}): ${text.slice(0, 200)}`);
151
+ return { purged: 0, error: 'bad-response' };
152
+ }
153
+ if (data?.error) {
154
+ console.error('[nuxt-pages] Purge API error:', JSON.stringify(data.error));
155
+ return { purged: 0, error: data.error };
156
+ }
157
+ return { purged: fullUrls.length, urls: fullUrls };
158
+ } catch (error) {
159
+ console.error('[nuxt-pages] Purge request failed:', error?.message || error);
160
+ return { purged: 0, error: String(error?.message || error) };
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Tag a cacheable response with the ISR CDN cache directives, if its path
166
+ * matches an ISR route. Mutates `headers` in place. Skips personalized
167
+ * (Set-Cookie) or explicitly non-shared (no-store/private) responses.
168
+ */
169
+ export function applyIsr(headers, { method, status, pathname }) {
170
+ if (method !== 'GET' && method !== 'HEAD') return;
171
+ if (status !== 200) return;
172
+ if (headers.has('set-cookie')) return;
173
+ if (/no-store|private/i.test(headers.get('cache-control') || '')) return;
174
+
175
+ // Nuxt client navigation fetches `<route>/_payload.json`; match it to the
176
+ // page route so the payload is cached with the same window.
177
+ const matchPath = pathname.replace(/\/_payload\.json$/, '') || '/';
178
+ const route = matchIsrRoute(matchPath);
179
+ if (route) setIsrCacheHeaders(headers, route);
180
+ }
181
+
182
+ /**
183
+ * On-demand revalidation endpoint handler. POST /__edgeone_revalidate
184
+ * Body: { token, tags?: string[], paths?: string[] }
185
+ * Purges the CDN cache for the resolved paths so the next request regenerates.
186
+ */
187
+ export async function handleRevalidate(req, headers, body) {
188
+ const token = process.env.PURGE_TOKEN;
189
+ // Secure by default: without a configured token the endpoint is disabled.
190
+ if (!token) {
191
+ return new Response(JSON.stringify({ revalidated: false, error: 'purge-not-configured' }), {
192
+ status: 403, headers: { 'Content-Type': 'application/json' },
193
+ });
194
+ }
195
+ let payload = {};
196
+ try { payload = body ? JSON.parse(body) : {}; } catch { /* ignore */ }
197
+
198
+ const provided = payload.token
199
+ || (headers && typeof headers.get === 'function' ? headers.get('x-revalidate-token') : headers?.['x-revalidate-token']);
200
+ if (provided !== token) {
201
+ return new Response(JSON.stringify({ revalidated: false, error: 'invalid-token' }), {
202
+ status: 401, headers: { 'Content-Type': 'application/json' },
203
+ });
204
+ }
205
+
206
+ const tags = Array.isArray(payload.tags) ? payload.tags : [];
207
+ const explicitPaths = Array.isArray(payload.paths) ? payload.paths : [];
208
+ const paths = Array.from(new Set([...expandTagsToPaths(tags), ...explicitPaths]));
209
+
210
+ const result = await purgeEdgeCache(paths, getRealHost(headers));
211
+ return new Response(JSON.stringify({ revalidated: !result.error, ...result }), {
212
+ status: result.error ? 500 : 200,
213
+ headers: { 'Content-Type': 'application/json' },
214
+ });
215
+ }
@@ -2,6 +2,7 @@ import { resolve, dirname } from 'path';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { readFileSync, existsSync, statSync } from 'fs';
4
4
  import { extname } from 'path';
5
+ import { REVALIDATE_ENDPOINT, applyIsr, handleRevalidate } from './isr-runtime.js';
5
6
 
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = dirname(__filename);
@@ -172,6 +173,13 @@ export default async function handler(req, res, context) {
172
173
  const headers = req.headers || new Headers();
173
174
  const body = await getBody(req);
174
175
 
176
+ const pathname = url.split('?')[0];
177
+
178
+ // On-demand ISR revalidation endpoint (purges CDN cache).
179
+ if (pathname === REVALIDATE_ENDPOINT && method === 'POST') {
180
+ return await handleRevalidate(req, headers, body);
181
+ }
182
+
175
183
  // First try to handle static assets
176
184
  if (method === 'GET') {
177
185
  const staticResponse = handleStaticFile(url);
@@ -221,8 +229,13 @@ export default async function handler(req, res, context) {
221
229
  }
222
230
 
223
231
  // console.log('responseHeaders.getSetCookie() --->', responseHeaders.getSetCookie());
232
+ const status = response.status || response.statusCode;
233
+
234
+ // ISR: tag cacheable responses with EdgeOne CDN cache directives.
235
+ applyIsr(responseHeaders, { method, status, pathname });
236
+
224
237
  return new Response(response.body, {
225
- status: response.status || response.statusCode,
238
+ status,
226
239
  headers: responseHeaders
227
240
  });
228
241
  } catch (nitroError) {
@@ -92,6 +92,12 @@ var getHandlerFile = async (ctx) => {
92
92
  var writeHandlerFile = async (ctx) => {
93
93
  const handler = await getHandlerFile(ctx);
94
94
  await writeFile(join(ctx.serverHandlerRootDir, `handler.js`), handler);
95
+ const templatesDir = join(ctx.pluginDir, "dist/build/templates");
96
+ await cp(
97
+ join(templatesDir, "isr-runtime.js"),
98
+ join(ctx.serverHandlerRootDir, "isr-runtime.js"),
99
+ { force: true }
100
+ );
95
101
  };
96
102
  var createServerHandler = async (ctx) => {
97
103
  await mkdir(join(ctx.serverHandlerRuntimeModulesDir), { recursive: true });
@@ -7,13 +7,19 @@
7
7
  import {
8
8
  getHandlersArrayWithAST,
9
9
  getRouteRulesWithAST
10
- } from "./chunk-AZLY3JO2.js";
10
+ } from "./chunk-ODJXW6KT.js";
11
+ import {
12
+ buildTagManifest,
13
+ extractCacheTags
14
+ } from "./chunk-QUFJCWGJ.js";
11
15
 
12
16
  // src/build/routes.ts
13
17
  import * as fs from "fs";
14
18
  import * as path from "path";
15
19
  var CONFIG_VERSION = 3;
16
20
  var CONFIG_FILE_NAME = "config.json";
21
+ var ISR_MANIFEST_FILE_NAME = "isr-manifest.json";
22
+ var TAG_MANIFEST_FILE_NAME = "tag-manifest.json";
17
23
  function escapeRegExp(str) {
18
24
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
25
  }
@@ -37,6 +43,14 @@ function writeConfig(config) {
37
43
  const configPath = getConfigPath();
38
44
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
39
45
  }
46
+ function writeIsrManifest(manifest) {
47
+ const p = path.join(ensureServerHandlerDir(), ISR_MANIFEST_FILE_NAME);
48
+ fs.writeFileSync(p, JSON.stringify(manifest, null, 2), "utf-8");
49
+ }
50
+ function writeTagManifest(manifest) {
51
+ const p = path.join(ensureServerHandlerDir(), TAG_MANIFEST_FILE_NAME);
52
+ fs.writeFileSync(p, JSON.stringify(manifest, null, 2), "utf-8");
53
+ }
40
54
  function normalizeHeaders(headers) {
41
55
  if (!headers || typeof headers !== "object") return null;
42
56
  const out = {};
@@ -240,12 +254,51 @@ async function buildExplicitSsrAndSwrRoutes(ctx) {
240
254
  tmp.sort((a, b) => b.score - a.score);
241
255
  return tmp.map((x) => x.rule);
242
256
  }
257
+ async function buildIsrEntries(ctx) {
258
+ const routesManifest = await ctx.getRoutesManifest();
259
+ const manifestRoutes = Array.isArray(routesManifest?.routes) ? routesManifest.routes : [];
260
+ const routeRules = await getRouteRulesFromBuildOutput(ctx) || {};
261
+ const seen = /* @__PURE__ */ new Set();
262
+ const scored = [];
263
+ for (const r of manifestRoutes) {
264
+ const p = typeof r?.path === "string" ? r.path : "";
265
+ if (!p) continue;
266
+ const swr = r?.swr;
267
+ const isIsr = swr === true || typeof swr === "number" && swr > 0;
268
+ if (!isIsr || r?.prerender === true) continue;
269
+ const normalized = normalizeRoutePath(p);
270
+ if (normalized.startsWith("/api")) continue;
271
+ const rule = nuxtRoutePathToRouteRule(normalized);
272
+ const src = rule?.src;
273
+ if (!src || seen.has(src)) continue;
274
+ seen.add(src);
275
+ const tags = extractCacheTags(routeRules?.[p]?.headers ?? routeRules?.[normalized]?.headers);
276
+ const score = (isCatchAllLikeRoutePath(normalized) ? -1e4 : 0) + normalized.split("/").length;
277
+ scored.push({
278
+ score,
279
+ entry: {
280
+ src,
281
+ path: normalized,
282
+ revalidate: typeof swr === "number" ? swr : false,
283
+ tags
284
+ }
285
+ });
286
+ }
287
+ scored.sort((a, b) => b.score - a.score);
288
+ return scored.map((x) => x.entry);
289
+ }
243
290
  var createNuxtRoutesMeta = async (ctx) => {
244
291
  const routes = await buildStage1Routes(ctx);
245
292
  routes.push(...await buildExplicitApiRoutes(ctx));
246
293
  routes.push(...await buildExplicitSsrAndSwrRoutes(ctx));
247
294
  routes.push({ src: "^/(.*)$" });
248
295
  writeConfig({ version: CONFIG_VERSION, routes });
296
+ const isrEntries = await buildIsrEntries(ctx);
297
+ writeIsrManifest({ version: 1, routes: isrEntries });
298
+ writeTagManifest(buildTagManifest(isrEntries));
299
+ console.log(
300
+ `[nuxt-pages] Generated ISR manifest with ${isrEntries.length} route(s) and ${Object.keys(buildTagManifest(isrEntries).tags).length} cache tag(s)`
301
+ );
249
302
  };
250
303
  var createNuxtPagesRouteMeta = async (ctx) => {
251
304
  await createNuxtRoutesMeta(ctx);
@@ -6,7 +6,7 @@
6
6
 
7
7
  import {
8
8
  require_lib
9
- } from "./chunk-VCJ5U4PV.js";
9
+ } from "./chunk-VPL326TW.js";
10
10
  import {
11
11
  __toESM
12
12
  } from "./chunk-6BT4RYQJ.js";
@@ -6,7 +6,7 @@
6
6
 
7
7
  import {
8
8
  require_lib
9
- } from "./chunk-VCJ5U4PV.js";
9
+ } from "./chunk-VPL326TW.js";
10
10
  import {
11
11
  __toESM
12
12
  } from "./chunk-6BT4RYQJ.js";
@@ -0,0 +1,38 @@
1
+
2
+ var require = await (async () => {
3
+ var { createRequire } = await import("node:module");
4
+ return createRequire(import.meta.url);
5
+ })();
6
+
7
+
8
+ // src/build/tag-manifest.ts
9
+ function extractCacheTags(headers) {
10
+ if (!headers || typeof headers !== "object") return [];
11
+ let raw;
12
+ for (const [k, v] of Object.entries(headers)) {
13
+ if (typeof k === "string" && k.toLowerCase() === "cache-tag") {
14
+ raw = v;
15
+ break;
16
+ }
17
+ }
18
+ if (raw == null) return [];
19
+ const parts = Array.isArray(raw) ? raw : String(raw).split(",");
20
+ return parts.map((t) => String(t).trim()).filter((t) => t.length > 0);
21
+ }
22
+ function buildTagManifest(entries) {
23
+ const tags = {};
24
+ for (const entry of entries) {
25
+ for (const tag of entry.tags) {
26
+ if (!tags[tag]) tags[tag] = [];
27
+ if (!tags[tag].includes(entry.path)) {
28
+ tags[tag].push(entry.path);
29
+ }
30
+ }
31
+ }
32
+ return { version: 1, tags };
33
+ }
34
+
35
+ export {
36
+ extractCacheTags,
37
+ buildTagManifest
38
+ };
@@ -9,7 +9,7 @@ import {
9
9
  getPrerenderRoutesWithAST,
10
10
  getRouteRulesWithAST,
11
11
  getRoutesArrayWithAST
12
- } from "./chunk-AZLY3JO2.js";
12
+ } from "./chunk-ODJXW6KT.js";
13
13
 
14
14
  // src/build/plugin-context.ts
15
15
  import { existsSync, readFileSync } from "node:fs";