feat: support standalone Docker runtime and internal CMS API
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
.git
|
||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.log
|
||||||
|
*.tsbuildinfo
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
FROM node:24-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
ARG NEXT_PUBLIC_API_URL=http://localhost:18000
|
||||||
|
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||||
|
RUN node --test tests/*.test.cjs && npm run build
|
||||||
|
|
||||||
|
FROM node:24-alpine AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
NEXT_TELEMETRY_DISABLED=1 \
|
||||||
|
HOSTNAME=0.0.0.0 \
|
||||||
|
PORT=3000
|
||||||
|
COPY --from=build --chown=node:node /app/.next/standalone ./
|
||||||
|
COPY --from=build --chown=node:node /app/.next/static ./.next/static
|
||||||
|
COPY --from=build --chown=node:node /app/public ./public
|
||||||
|
USER node
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
output: "standalone",
|
||||||
images: {
|
images: {
|
||||||
// Wagtail 后端媒体地址(开发环境 + 生产环境国内 OSS/CDN 域名)。
|
// Wagtail 后端媒体地址(开发环境 + 生产环境国内 OSS/CDN 域名)。
|
||||||
// 详见 documents/设计方案分析与完善版.md §2.3。
|
// 详见 documents/设计方案分析与完善版.md §2.3。
|
||||||
|
|||||||
Generated
+49
-3
@@ -281,10 +281,33 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/core": {
|
||||||
|
"version": "1.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
|
||||||
|
"integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.3",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||||
|
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
|
||||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
"integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -2243,6 +2266,29 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.1",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": {
|
"node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
* 详见 documents/设计方案分析与完善版.md §2.7 Headless API 设计规范。
|
* 详见 documents/设计方案分析与完善版.md §2.7 Headless API 设计规范。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
|
const PUBLIC_API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
|
||||||
|
const API_BASE_URL = typeof window === "undefined"
|
||||||
|
? process.env.API_INTERNAL_URL || PUBLIC_API_URL
|
||||||
|
: PUBLIC_API_URL;
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -137,6 +137,21 @@ test("page fetches share the publish cache tag without changing snippet tags", a
|
|||||||
assert.equal(calls[1].next.tags.join(","), "site-settings");
|
assert.equal(calls[1].next.tags.join(","), "site-settings");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("API client separates container internal and browser addresses", async () => {
|
||||||
|
const env = { NEXT_PUBLIC_API_URL: "http://localhost:18000", API_INTERNAL_URL: "http://backend:8000" };
|
||||||
|
for (const browser of [false, true]) {
|
||||||
|
let requestedURL;
|
||||||
|
const globals = {
|
||||||
|
process: { env },
|
||||||
|
fetch: async (url) => { requestedURL = url; return { ok: true, json: async () => ({}) }; },
|
||||||
|
...(browser ? { window: {} } : {}),
|
||||||
|
};
|
||||||
|
const { fetchAPI } = loadModule("services/api-client.ts", {}, globals);
|
||||||
|
await fetchAPI("/api/v2/pages/");
|
||||||
|
assert.equal(requestedURL, `${browser ? env.NEXT_PUBLIC_API_URL : env.API_INTERNAL_URL}/api/v2/pages/`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("authenticated webhook expires shared cache immediately and rejects unauthorized requests", async () => {
|
test("authenticated webhook expires shared cache immediately and rejects unauthorized requests", async () => {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
const { POST } = loadModule("app/api/revalidate/route.ts", {
|
const { POST } = loadModule("app/api/revalidate/route.ts", {
|
||||||
|
|||||||
Reference in New Issue
Block a user