@edgeone/opennextjs-pages 0.2.8 → 0.2.9

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.
@@ -15,6 +15,7 @@ import {
15
15
  convertHeaders,
16
16
  convertTrailingSlash
17
17
  } from "./route-utils.js";
18
+ import { generateTagManifest } from "./tag-manifest.js";
18
19
  function hasAppRouter(appPathRoutesManifest) {
19
20
  return appPathRoutesManifest && Object.keys(appPathRoutesManifest).length > 0;
20
21
  }
@@ -103,6 +104,7 @@ function getServerRoutes(staticRoutes, prerenderManifest, basePath = "") {
103
104
  if (route.namedRegex) {
104
105
  let src = convertNamedRegexToSrc(route.namedRegex, basePath);
105
106
  if (src) {
107
+ src = src.replace(/\(\?:\/\)\?\$$/, "$");
106
108
  routes.push({ src });
107
109
  }
108
110
  } else if (route.regex) {
@@ -111,51 +113,28 @@ function getServerRoutes(staticRoutes, prerenderManifest, basePath = "") {
111
113
  if (basePath && !src.startsWith(`^${basePath}`)) {
112
114
  src = src.replace(/^\^/, `^${basePath}`);
113
115
  }
116
+ src = src.replace(/\(\?:\/\)\?\$$/, "$");
114
117
  routes.push({ src });
115
118
  }
116
119
  }
117
120
  }
118
121
  return routes;
119
122
  }
120
- function getAppRoutes(appPathsManifest, basePath = "", existingPaths = /* @__PURE__ */ new Set()) {
123
+ function getApiRoutes(appPathsManifest, basePath = "") {
121
124
  if (!appPathsManifest) {
122
125
  return [];
123
126
  }
124
127
  const routes = [];
125
- const emitted = /* @__PURE__ */ new Set();
126
128
  for (const routePath of Object.keys(appPathsManifest)) {
127
- if (routePath.startsWith("/_")) {
128
- continue;
129
- }
130
- let pagePath;
131
- if (routePath.endsWith("/route")) {
132
- pagePath = routePath.replace(/\/route$/, "");
133
- } else if (routePath.endsWith("/page")) {
134
- pagePath = routePath.replace(/\/page$/, "") || "/";
135
- } else {
136
- continue;
137
- }
138
- pagePath = pagePath.replace(/\/\([^)]+\)/g, "");
139
- if (!pagePath) pagePath = "/";
140
- if (pagePath.includes("@")) {
141
- continue;
142
- }
143
- if (pagePath.includes("[")) {
144
- continue;
145
- }
146
- if (pagePath === "/") {
147
- continue;
148
- }
149
- if (existingPaths.has(pagePath)) {
150
- continue;
151
- }
152
- if (emitted.has(pagePath)) {
153
- continue;
129
+ if (routePath.includes("/api/") && routePath.endsWith("/route")) {
130
+ const apiPath = routePath.replace(/\/route$/, "");
131
+ if (apiPath.includes("[")) {
132
+ continue;
133
+ }
134
+ const escapedPath = apiPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
135
+ const src = `^${basePath}${escapedPath}$`;
136
+ routes.push({ src });
154
137
  }
155
- emitted.add(pagePath);
156
- const escapedPath = pagePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
157
- const src = `^${basePath}${escapedPath}(?:/)?$`;
158
- routes.push({ src });
159
138
  }
160
139
  return routes;
161
140
  }
@@ -367,10 +346,9 @@ var createRouteMeta = async (ctx) => {
367
346
  if (dataRoutes.length > 0) {
368
347
  routes.push(...getDataRoutes(dataRoutes, basePath));
369
348
  }
370
- const existingPaths = new Set(staticRoutes.map((r) => r.page));
371
- const appRoutes = getAppRoutes(appPathsManifest, basePath, existingPaths);
372
- if (appRoutes.length > 0) {
373
- routes.push(...appRoutes);
349
+ const apiRoutes = getApiRoutes(appPathsManifest, basePath);
350
+ if (apiRoutes.length > 0) {
351
+ routes.push(...apiRoutes);
374
352
  }
375
353
  routes.push({
376
354
  src: `^${basePath}/.*$`
@@ -395,6 +373,10 @@ var createRouteMeta = async (ctx) => {
395
373
  "utf-8"
396
374
  );
397
375
  console.log(`[opennext] Generated ${configFilePath} with ${routes.length} routes`);
376
+ const tagManifest = generateTagManifest(prerenderManifest, ctx.publishDir);
377
+ const tagManifestPath = path.join(serverHandlerDir, "tag-manifest.json");
378
+ fs.writeFileSync(tagManifestPath, JSON.stringify(tagManifest, null, 2), "utf-8");
379
+ console.log(`[opennext] Generated ${tagManifestPath} with ${Object.keys(tagManifest.tags).length} tags`);
398
380
  const middlewareConfig = await getMiddlewareConfig(ctx);
399
381
  updateEdgeFunctionsConfigJson(middlewareConfig);
400
382
  };
@@ -0,0 +1,57 @@
1
+
2
+ var require = await (async () => {
3
+ var { createRequire } = await import("node:module");
4
+ return createRequire(import.meta.url);
5
+ })();
6
+
7
+ import "../esm-chunks/chunk-6BT4RYQJ.js";
8
+
9
+ // src/build/tag-manifest.ts
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ function generateTagManifest(prerenderManifest, publishDir) {
13
+ const tags = {};
14
+ if (!prerenderManifest?.routes) {
15
+ return { version: 1, tags };
16
+ }
17
+ const routes = prerenderManifest.routes;
18
+ for (const [routePath, routeConfig] of Object.entries(routes)) {
19
+ if (typeof routeConfig.initialRevalidateSeconds !== "number") {
20
+ continue;
21
+ }
22
+ const cacheTags = getCacheTagsForRoute(routePath, routeConfig, publishDir);
23
+ if (!cacheTags) {
24
+ continue;
25
+ }
26
+ const tagList = cacheTags.split(",").map((tag) => tag.trim().replace(/^_N_T_/, "")).filter((tag) => tag.length > 0);
27
+ for (const tag of tagList) {
28
+ if (!tags[tag]) {
29
+ tags[tag] = [];
30
+ }
31
+ if (!tags[tag].includes(routePath)) {
32
+ tags[tag].push(routePath);
33
+ }
34
+ }
35
+ }
36
+ return { version: 1, tags };
37
+ }
38
+ function getCacheTagsForRoute(routePath, routeConfig, publishDir) {
39
+ const metaFileName = routePath === "/" ? "index.meta" : `${routePath.slice(1)}.meta`;
40
+ const metaFilePath = path.join(publishDir, "server", "app", metaFileName);
41
+ if (fs.existsSync(metaFilePath)) {
42
+ try {
43
+ const metaContent = fs.readFileSync(metaFilePath, "utf-8");
44
+ const meta = JSON.parse(metaContent);
45
+ const tags = meta?.headers?.["x-next-cache-tags"];
46
+ if (tags) {
47
+ return tags;
48
+ }
49
+ } catch (err) {
50
+ console.warn(`[opennext] Warning: Failed to parse ${metaFilePath}: ${err}`);
51
+ }
52
+ }
53
+ return routeConfig.initialHeaders?.["x-next-cache-tags"] ?? null;
54
+ }
55
+ export {
56
+ generateTagManifest
57
+ };
@@ -99,12 +99,21 @@ var systemLogger = new SystemLogger();
99
99
  var REQUEST_CONTEXT_GLOBAL_KEY = Symbol.for("nf-request-context-async-local-storage");
100
100
  var REQUEST_COUNTER_KEY = Symbol.for("nf-request-counter");
101
101
  var extendedGlobalThis = globalThis;
102
+ function getHostFromRequest(request) {
103
+ if (!request?.headers) return void 0;
104
+ const headers = request.headers;
105
+ if (typeof headers.get === "function") {
106
+ return headers.get("eo-pages-host") || headers.get("host") || void 0;
107
+ }
108
+ return headers["eo-pages-host"] || headers["host"] || void 0;
109
+ }
102
110
  function createRequestContext(request, context) {
103
111
  const backgroundWorkPromises = [];
104
112
  const logger = systemLogger.withLogLevel(LogLevel.Log);
105
113
  return {
106
114
  isBackgroundRevalidation: false,
107
115
  captureServerTiming: false,
116
+ host: getHostFromRequest(request),
108
117
  trackBackgroundWork: (promise) => {
109
118
  if (context?.waitUntil) {
110
119
  context.waitUntil(promise);
@@ -3094,7 +3094,8 @@ function toComputeResponse(res) {
3094
3094
  import { getRunConfig, setRunConfig } from "../config.js";
3095
3095
  import {
3096
3096
  setCacheControlHeaders,
3097
- setCacheStatusHeader
3097
+ setCacheStatusHeader,
3098
+ setCacheTagsHeaders
3098
3099
  } from "../headers.js";
3099
3100
  import { nextResponseProxy } from "../revalidate.js";
3100
3101
  import { setFetchBeforeNextPatchedIt } from "../storage/storage.cjs";
@@ -3235,6 +3236,7 @@ var server_default = async (request, _context, topLevelSpan, requestContext) =>
3235
3236
  topLevelSpan.setAttribute("responseCacheKey", requestContext.responseCacheKey);
3236
3237
  }
3237
3238
  setCacheControlHeaders(response, request, requestContext);
3239
+ setCacheTagsHeaders(response.headers, requestContext);
3238
3240
  setCacheStatusHeader(response.headers, null);
3239
3241
  if (requestContext.isCacheableAppPage && response.status !== 304) {
3240
3242
  const isRSCRequest = request.headers.get("rsc") === "1";
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/run/handlers/tag-manifest.cts
31
+ var tag_manifest_exports = {};
32
+ __export(tag_manifest_exports, {
33
+ expandTagsToPaths: () => expandTagsToPaths
34
+ });
35
+ module.exports = __toCommonJS(tag_manifest_exports);
36
+ var fs = __toESM(require("fs"));
37
+ var path = __toESM(require("path"));
38
+ var cachedManifest = null;
39
+ function loadManifest() {
40
+ if (cachedManifest) {
41
+ return cachedManifest;
42
+ }
43
+ const emptyManifest = { version: 1, tags: {} };
44
+ const candidates = [
45
+ path.resolve(process.cwd(), "tag-manifest.json"),
46
+ path.resolve(__dirname, "..", "..", "tag-manifest.json"),
47
+ // Walk up from __dirname to find tag-manifest.json in SCF bundle root
48
+ ...findAncestorPaths(__dirname, "tag-manifest.json")
49
+ ];
50
+ for (const candidate of candidates) {
51
+ if (fs.existsSync(candidate)) {
52
+ try {
53
+ const content = fs.readFileSync(candidate, "utf-8");
54
+ cachedManifest = JSON.parse(content);
55
+ return cachedManifest;
56
+ } catch {
57
+ }
58
+ }
59
+ }
60
+ cachedManifest = emptyManifest;
61
+ return cachedManifest;
62
+ }
63
+ function findAncestorPaths(startDir, filename) {
64
+ const paths = [];
65
+ let dir = startDir;
66
+ for (let i = 0; i < 10; i++) {
67
+ const parent = path.dirname(dir);
68
+ if (parent === dir) break;
69
+ dir = parent;
70
+ paths.push(path.join(dir, filename));
71
+ }
72
+ return paths;
73
+ }
74
+ function expandTagsToPaths(tags) {
75
+ const manifest = loadManifest();
76
+ const paths = /* @__PURE__ */ new Set();
77
+ for (const tag of tags) {
78
+ if (manifest.tags[tag]) {
79
+ for (const p of manifest.tags[tag]) {
80
+ paths.add(p);
81
+ }
82
+ } else {
83
+ paths.add(tag);
84
+ }
85
+ }
86
+ return Array.from(paths);
87
+ }
88
+ // Annotate the CommonJS export names for ESM import in node:
89
+ 0 && (module.exports = {
90
+ expandTagsToPaths
91
+ });
@@ -28,10 +28,11 @@ module.exports = __toCommonJS(tags_handler_exports);
28
28
 
29
29
  // package.json
30
30
  var name = "@edgeone/opennextjs-pages";
31
- var version = "0.2.7";
31
+ var version = "0.2.9";
32
32
 
33
33
  // src/run/handlers/tags-handler.cts
34
34
  var import_request_context = require("./request-context.cjs");
35
+ var import_tag_manifest = require("./tag-manifest.cjs");
35
36
  var purgeCacheUserAgent = `${name}@${version}`;
36
37
  function isAnyTagStale(tags, timestamp) {
37
38
  return Promise.resolve(false);
@@ -44,24 +45,51 @@ async function purgeEdgeCache(tagOrTags) {
44
45
  return tag.replace(/^_N_T_/, "").replace(/\/page|\/layout$/, "");
45
46
  });
46
47
  if (tags.length === 0) {
47
- return Promise.resolve();
48
+ return;
48
49
  }
49
- (0, import_request_context.getLogger)().debug(`[NextRuntime] Purging CDN cache for: [${tags}.join(', ')]`);
50
- const baseUrl = process.env.IS_MAINLAND === "true" ? "https://pages-api.cloud.tencent.com" : "https://pages-api.edgeone.ai";
51
- const token = process.env.PURGE_TOKEN;
52
- const requestBody = {
53
- path: [...tags]
54
- };
55
- const res = await fetch(`${baseUrl}/eo/purge`, {
56
- method: "POST",
57
- headers: {
58
- "Content-Type": "application/json",
59
- Authorization: `Bearer ${token}`
60
- },
61
- body: JSON.stringify(requestBody)
50
+ const paths = (0, import_tag_manifest.expandTagsToPaths)(tags);
51
+ if (paths.length === 0) {
52
+ return;
53
+ }
54
+ const requestContext = (0, import_request_context.getRequestContext)();
55
+ const host = requestContext?.host || process.env.SITE_HOST;
56
+ if (!host) {
57
+ (0, import_request_context.getLogger)().error(`[NextRuntime] Cannot purge: no host available (request context or SITE_HOST env)`);
58
+ return;
59
+ }
60
+ const fullUrls = paths.map((p) => {
61
+ const normalizedPath = p.startsWith("/") ? p : `/${p}`;
62
+ return `https://${host}${normalizedPath}`;
62
63
  });
63
- const data = await res.json();
64
- if (data?.error) {
64
+ (0, import_request_context.getLogger)().debug(`[NextRuntime] Purging CDN cache for tags [${tags.join(", ")}] \u2192 urls [${fullUrls.join(", ")}]`);
65
+ const purgeEndpoint = process.env.IS_MAINLAND === "true" ? "https://pages-api.cloud.tencent.com/eo/pages/hook/eo_purge" : "https://pages-api.edgeone.ai/eo/pages/hook/eo_purge?site=intl";
66
+ const token = process.env.PURGE_TOKEN;
67
+ try {
68
+ const requestBody = {
69
+ RequestId: `purge-${Date.now()}`,
70
+ Token: token,
71
+ paths: fullUrls
72
+ };
73
+ const res = await fetch(purgeEndpoint, {
74
+ method: "POST",
75
+ headers: {
76
+ "Content-Type": "application/json"
77
+ },
78
+ body: JSON.stringify(requestBody)
79
+ });
80
+ const text = await res.text();
81
+ let data;
82
+ try {
83
+ data = JSON.parse(text);
84
+ } catch {
85
+ (0, import_request_context.getLogger)().error(`[NextRuntime] Purge API returned non-JSON (${res.status}): ${text.slice(0, 200)}`);
86
+ return;
87
+ }
88
+ if (data?.error) {
89
+ (0, import_request_context.getLogger)().error(`[NextRuntime] Purge API error: ${JSON.stringify(data.error)}`);
90
+ }
91
+ } catch (error) {
92
+ (0, import_request_context.getLogger)().withError(error).error(`[NextRuntime] Purging CDN cache for [${fullUrls.join(",")}] failed`);
65
93
  }
66
94
  }
67
95
  async function doRevalidateTagAndPurgeEdgeCache(tags) {
@@ -65,9 +65,14 @@ var setCacheControlHeaders = ({ headers, status }, request, requestContext) => {
65
65
  }
66
66
  };
67
67
  var setCacheTagsHeaders = (headers, requestContext) => {
68
- if (!headers.has("cache-control")) {
68
+ const nextCacheTags = headers.get("x-next-cache-tags");
69
+ if (!nextCacheTags) {
69
70
  return;
70
71
  }
72
+ const tags = nextCacheTags.split(",").map((tag) => tag.trim().replace(/^_N_T_/, "")).filter(Boolean).join(",");
73
+ if (tags) {
74
+ headers.set("cache-tag", tags);
75
+ }
71
76
  };
72
77
  var setCacheStatusHeader = (headers, nextCache) => {
73
78
  if (headers.has("x-nextjs-cache")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgeone/opennextjs-pages",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",