const fs = require("fs") const { execSync } = require("child_process") const postcssPlugin = require("esbuild-postcss") // Resolve version at build time via git describe (tag + hash), fallback to env var or "dev" let gitHash = "dev" try { gitHash = process.env.GIT_HASH || execSync("git describe --tags --always --dirty").toString().trim() } catch (_) { // .git not available (e.g. Docker build without .git context) } // Generate buildInfo module that can be imported by frontend code function writeBuildInfo() { const info = { gitHash, buildTime: new Date().toISOString(), } fs.writeFileSync( __dirname + "/frontend/src/lib/buildInfo.ts", `// AUTO-GENERATED by esbuild.config.js \u2013 do not edit\nexport const gitHash = ${JSON.stringify(info.gitHash)}\nexport const buildTime = ${JSON.stringify(info.buildTime)}\n` ) // Write same buildInfo for backend hooks (X-Build-Time / X-Release headers) fs.writeFileSync( __dirname + "/api/hooks/lib/buildInfo.js", `// AUTO-GENERATED by esbuild.config.js \u2013 do not edit\nmodule.exports = { gitHash: ${JSON.stringify(info.gitHash)}, buildTime: ${JSON.stringify(info.buildTime)} }\n` ) } // NOTE: writeBuildInfo() is NOT called here at top-level. // It is called by esbuild-wrapper.js before each build. // This prevents the server build from overwriting the frontend's timestamp // (which would cause a version mismatch and spurious auto-reload). const resolvePlugin = { name: "resolvePlugin", setup(build) { let path = require("path") // url in css does not resolve via esbuild-svelte correctly build.onResolve({ filter: /.*/, namespace: "fakecss" }, (args) => { // console.log(args) if (args.path.match(/^\./)) return { path: path.dirname(args.importer) + "/" + args.path } // return { path: path.join(args.resolveDir, "public", args.path) } }) }, } // When MOCK is disabled, replace the mock module with a no-op stub so // esbuild can tree-shake all mock data out of the production bundle. const mockPlugin = { name: "mockPlugin", setup(build) { if (process.env.MOCK !== "1") { build.onResolve({ filter: /\/mock$/ }, (args) => { if (args.importer.includes("api.ts") || args.importer.includes("api.js")) { return { path: "mock-noop", namespace: "mock-noop" } } }) build.onLoad({ filter: /.*/, namespace: "mock-noop" }, () => ({ contents: "export function mockApiRequest() { return null }", loader: "ts", })) } }, } ////////////////////////// esbuild-svelte const sveltePlugin = require("esbuild-svelte") const frontendDir = "./frontend" const distDir = frontendDir + "/dist" // console.log("copy public dir...") // const copydir = require("copy-dir") // copydir.sync(__dirname + "/public", __dirname + "/" + distDir) /*copydir.sync( __dirname + "/public/index.html", __dirname + "/" + distDir + "/template.html" )*/ const svelteConfig = require("./svelte.config") const esbuildSvelte = sveltePlugin({ compilerOptions: { css: "external", dev: (process.argv?.length > 2 ? process.argv[2] : "build") !== "build", }, preprocess: svelteConfig.preprocess, cache: true, filterWarnings: (warning) => { // filter out a11y if (warning.code.match(/^a11y/)) return false return true }, }) const options = { logLevel: "info", color: true, entryPoints: ["./frontend/src/index.ts"], outfile: distDir + "/index.mjs", metafile: true, format: "esm", minify: process.argv[2] == "build", bundle: true, splitting: false, define: { __MOCK__: process.env.MOCK === "1" ? "true" : "false", }, plugins: [esbuildSvelte, postcssPlugin(), resolvePlugin, mockPlugin], loader: { ".woff2": "file", ".woff": "file", ".eot": "file", ".svg": "file", ".ttf": "file", }, sourcemap: true, target: ["es2022", "chrome100", "firefox100", "safari15", "edge100"], } const bsMiddleware = [] if (process.argv[2] == "start") { const { createProxyMiddleware } = require("http-proxy-middleware") const dotEnv = fs.readFileSync(__dirname + "/.env", "utf8") const TIBI_NAMESPACE = dotEnv.match(/TIBI_NAMESPACE=(.*)/)[1] const apiBase = process.env.API_BASE || "http://localhost:8080/api/v1/_/" + TIBI_NAMESPACE bsMiddleware.push( createProxyMiddleware({ pathFilter: "/api", target: apiBase, pathRewrite: { "^/api": "" }, changeOrigin: true, logLevel: "debug", }) ) // Sentry tunnel proxy: forwards /_s requests to Sentry ingest to avoid ad-blockers const https = require("https") const http = require("http") bsMiddleware.push(function (req, res, next) { if (req.url !== "/_s" || req.method !== "POST") return next() let body = [] req.on("data", (chunk) => body.push(chunk)) req.on("end", () => { const payload = Buffer.concat(body).toString() const firstLine = payload.split("\n")[0] let envelope try { envelope = JSON.parse(firstLine) } catch { res.writeHead(400) res.end() return } const dsn = envelope.dsn || "" let sentryUrl try { sentryUrl = new URL(dsn) } catch { res.writeHead(400) res.end() return } const projectId = sentryUrl.pathname.replace(/\//g, "") const ingestUrl = `${sentryUrl.protocol}//${sentryUrl.host}/api/${projectId}/envelope/` const transport = ingestUrl.startsWith("https") ? https : http const proxyReq = transport.request( ingestUrl, { method: "POST", headers: { "Content-Type": "application/x-sentry-envelope" }, }, (proxyRes) => { res.writeHead(proxyRes.statusCode || 200) proxyRes.pipe(res) } ) proxyReq.on("error", () => { res.writeHead(502) res.end() }) proxyReq.write(payload) proxyReq.end() }) }) if (process.env.SSR) { bsMiddleware.push( createProxyMiddleware({ pathFilter: function (path, req) { return !path.match(/\./) }, target: apiBase, changeOrigin: true, logLevel: "debug", pathRewrite: function (path, req) { return "/ssr?url=" + encodeURIComponent(path) }, }) ) } } module.exports = { sveltePlugin: sveltePlugin, resolvePlugin: resolvePlugin, writeBuildInfo: writeBuildInfo, options: options, distDir, watch: { path: [__dirname + "/" + frontendDir + "/src"], }, serve: { onRequest(args) { console.log(args) }, }, browserSync: { server: { baseDir: frontendDir, middleware: [ require("morgan")("dev"), ...bsMiddleware, require("connect-history-api-fallback")({ index: "/spa.html", // Allow dots in URL paths (e.g. /version-3.7.1) to fall through to SPA, // but only if they don't look like actual file requests. rewrites: [ { from: /\.(js|css|mjs|map|woff2?|ttf|eot|svg|png|jpe?g|gif|webp|ico|json|pdf)(\?.*)?$/i, to: function (context) { return context.parsedUrl.pathname }, }, { from: /./, to: "/spa.html", }, ], // verbose: true, }), ], }, open: false, // logLevel: "debug", ghostMode: false, }, }