diff --git a/Dockerfile b/Dockerfile index 8604868..6c8323d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,11 +12,15 @@ COPY package.json pnpm-lock.yaml ./ # Install all dependencies (including dev for build) RUN pnpm install --frozen-lockfile -# Copy source code +# Copy source code AND landing page AND build scripts so `pnpm build` +# can produce both the backend dist/ and landing/dist/app.js bundle. COPY src/ src/ +COPY landing/ landing/ +COPY scripts/build-landing.mjs scripts/ COPY tsconfig.json tsconfig.build.json tsup.config.ts ./ -# Build +# `pnpm build` runs tsup (backend -> dist/) and build-landing +# (landing JSX -> landing/dist/app.js, with React + ReactDOM bundled). RUN pnpm build # Stage 2: Production @@ -39,8 +43,11 @@ RUN pnpm install --frozen-lockfile --prod --ignore-scripts # Copy built output from build stage COPY --from=build /app/dist ./dist -# Copy landing page (served at / by @fastify/static) -COPY landing/ ./landing/ +# Copy the landing page WITH its precompiled bundle from the build stage. +# The host-side landing/ doesn't have landing/dist (it's gitignored and only +# materialised during a build), so we pull the whole tree from the build +# stage where the bundle exists at landing/dist/app.js. +COPY --from=build /app/landing ./landing # Copy agent-discovery surface (served at /SKILL.md by the agent-discovery plugin) COPY SKILL.md ./SKILL.md diff --git a/landing/index.html b/landing/index.html index bb1c354..bb6c339 100644 --- a/landing/index.html +++ b/landing/index.html @@ -214,18 +214,10 @@
- - - - - - - - - - - - + + + + diff --git a/package.json b/package.json index 77dcf89..e8d06f3 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ }, "scripts": { "dev": "tsx watch --env-file-if-exists=.env --inspect src/index.ts", - "build": "tsup", + "build": "tsup && pnpm build:landing", + "build:landing": "node scripts/build-landing.mjs", "lint": "eslint src", "lint:fix": "eslint src --fix", "format": "prettier --write \"src/**/*.ts\"", @@ -94,12 +95,17 @@ "ioredis": "^5.10.1", "pino": "^10.3.1", "prom-client": "^15.1.3", + "react": "18.3.1", + "react-dom": "18.3.1", "zod": "^4.4.3" }, "devDependencies": { "@eslint/js": "^9.39.4", "@types/node": "^25.8.0", + "@types/react": "^18.3", + "@types/react-dom": "^18.3", "@vitest/coverage-v8": "^4.0.18", + "esbuild": "^0.28.0", "eslint": "^9.39.4", "eslint-config-airbnb-extended": "^3.1.0", "eslint-plugin-import": "^2.32.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f707789..56d8c0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,12 @@ importers: prom-client: specifier: ^15.1.3 version: 15.1.3 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) zod: specifier: ^4.4.3 version: 4.4.3 @@ -75,9 +81,18 @@ importers: '@types/node': specifier: ^25.8.0 version: 25.8.0 + '@types/react': + specifier: ^18.3 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.3 + version: 18.3.7(@types/react@18.3.28) '@vitest/coverage-v8': specifier: ^4.0.18 version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(tsx@4.22.0)(yaml@2.9.0)) + esbuild: + specifier: ^0.28.0 + version: 0.28.0 eslint: specifier: ^9.39.4 version: 9.39.4 @@ -1329,6 +1344,17 @@ packages: '@types/pg@8.15.6': resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.28': + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} @@ -1869,6 +1895,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -3276,9 +3305,18 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -3397,6 +3435,9 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} @@ -5291,6 +5332,17 @@ snapshots: pg-protocol: 1.11.0 pg-types: 2.2.0 + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react@18.3.28': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + '@types/responselike@1.0.3': dependencies: '@types/node': 25.8.0 @@ -5872,6 +5924,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.2.3: {} + damerau-levenshtein@1.0.8: {} data-view-buffer@1.0.2: @@ -7426,8 +7480,18 @@ snapshots: quick-lru@5.1.1: {} + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + react-is@16.13.1: {} + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -7586,6 +7650,10 @@ snapshots: safe-stable-stringify@2.5.0: {} + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + secure-json-parse@4.1.0: {} semver@6.3.1: {} diff --git a/scripts/build-landing.mjs b/scripts/build-landing.mjs new file mode 100755 index 0000000..f960324 --- /dev/null +++ b/scripts/build-landing.mjs @@ -0,0 +1,105 @@ +#!/usr/bin/env node +// scripts/build-landing.mjs +// Precompile the landing-page JSX so we can drop 'unsafe-eval' from CSP. +// +// Strategy: concatenate the existing 8 .jsx files in the order they were +// loaded via