Repository navigation
Expand file tree
/
Copy pathserve.js
More file actions
121 lines (107 loc) · 4.24 KB
/
Copy pathserve.js
File metadata and controls
121 lines (107 loc) · 4.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/**
* DeepPhe Visualizer v2 — unified production server.
*
* This is the entry point packaged into a self-contained executable with
* @yao-pkg/pkg. A single process:
* 1. serves the built React SPA from the embedded build/ snapshot,
* 2. mounts the read-only piper-files API (src/piper-server/router.js),
* 3. reverse-proxies the DeepPhe data API to a runtime-configurable upstream
* so one binary works against any backend with no CORS.
*
* Runtime configuration (environment variables):
* PORT port to listen on (default 3000)
* DEEPPHE_API_LOCATION upstream DeepPhe data API origin (default http://localhost:3333)
* PIPER_FILES_DIR directory of .piper files (see src/piper-server/config.js)
* PIPER_ACTIVE_FILE active piper filename within PIPER_FILES_DIR
*/
const express = require("express");
const fs = require("fs");
const path = require("path");
const { createProxyMiddleware } = require("http-proxy-middleware");
const { createPiperRouter } = require("./src/piper-server/router");
const PORT = process.env.PORT || 3000;
const DEEPPHE_API_LOCATION = (
process.env.DEEPPHE_API_LOCATION || "http://localhost:3333"
).replace(/\/+$/, "");
const DEEPPHE_API_BASE_PATH = "/v1/deepphe-api";
// build/ is embedded into the pkg snapshot via the "pkg.assets" config. At
// runtime __dirname resolves into that snapshot, so this path works both when
// running from source (node serve.js) and inside the packaged binary.
const BUILD_DIR = path.join(__dirname, "build");
const INDEX_HTML_PATH = path.join(BUILD_DIR, "index.html");
// Read the SPA shell once at startup. Sending it from memory for the fallback
// route is more robust under the read-only pkg snapshot than res.sendFile.
let indexHtml;
try {
indexHtml = fs.readFileSync(INDEX_HTML_PATH, "utf-8");
} catch (error) {
console.error(`Failed to read ${INDEX_HTML_PATH}: ${error.message}`);
console.error("Did you run the web build before packaging? (npm run package:web)");
process.exit(1);
}
const app = express();
// 1. Piper-files API (read-only), mounted at its own /v1/piper-api base.
app.use(createPiperRouter());
// 2. OpenAPI document: fetch it from the upstream and rewrite servers[0].url to
// a relative path so the SPA issues same-origin requests that flow back through
// the reverse proxy below (the client reads servers[0].url to build API URLs).
app.get("/openapi.json", async (req, res) => {
try {
const upstreamUrl = `${DEEPPHE_API_LOCATION}/openapi.json`;
const upstreamRes = await fetch(upstreamUrl, {
headers: { Accept: "application/json" },
});
if (!upstreamRes.ok) {
res.status(502).json({
error: `Upstream DeepPhe API returned ${upstreamRes.status} for /openapi.json`,
});
return;
}
const spec = await upstreamRes.json();
spec.servers = [{ url: DEEPPHE_API_BASE_PATH }];
res.json(spec);
} catch (error) {
res.status(502).json({
error: `Failed to reach upstream DeepPhe API at ${DEEPPHE_API_LOCATION}: ${error.message}`,
});
}
});
// 3. Reverse proxy for the DeepPhe data API. A context filter (not a mount
// path) is used so the full /v1/deepphe-api/... path is preserved when
// forwarded to the upstream origin.
app.use(
createProxyMiddleware(DEEPPHE_API_BASE_PATH, {
target: DEEPPHE_API_LOCATION,
changeOrigin: true,
})
);
// Unified health check.
app.get("/healthz", (req, res) => {
res.json({ status: "ok", upstream: DEEPPHE_API_LOCATION });
});
// 4. Static assets from the build (hashed JS/CSS, favicon, etc.).
app.use(
express.static(BUILD_DIR, {
maxAge: "1y",
etag: true,
setHeaders: (res, filePath) => {
if (filePath.endsWith(".html")) {
res.setHeader("Cache-Control", "no-cache");
}
},
})
);
// 5. SPA fallback: serve index.html for any unmatched GET so client-side
// routes (e.g. /filters) deep-link correctly. Non-GET requests 404.
app.use((req, res) => {
if (req.method !== "GET" && req.method !== "HEAD") {
res.status(404).end();
return;
}
res.setHeader("Cache-Control", "no-cache");
res.type("html").send(indexHtml);
});
app.listen(PORT, () => {
console.log(`DeepPhe Visualizer v2 running on http://localhost:${PORT}`);
console.log(`Proxying DeepPhe data API -> ${DEEPPHE_API_LOCATION}`);
});