diff --git a/.dockerignore b/.dockerignore index 71cdb8b9d..74fa836a6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,6 +4,4 @@ Dockerfile .gitignore .DS_Store node_modules -.idea -.env -.env.* +.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 43e127e35..78419e652 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,6 @@ FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . -COPY docker/middleware.ts ./src ARG DATABASE_TYPE ARG BASE_PATH @@ -42,7 +41,7 @@ RUN set -x \ && apk add --no-cache curl # Script dependencies -RUN pnpm add npm-run-all dotenv chalk semver prisma@6.16.0 @prisma/adapter-pg@6.16.0 +RUN pnpm add npm-run-all dotenv prisma@6.8.2 # Permissions for prisma RUN chown -R nextjs:nodejs node_modules/.pnpm/ @@ -50,13 +49,15 @@ RUN chown -R nextjs:nodejs node_modules/.pnpm/ COPY --from=builder --chown=nextjs:nodejs /app/public ./public COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/scripts ./scripts -COPY --from=builder /app/generated ./generated # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +# Custom routes +RUN mv ./.next/routes-manifest.json ./.next/routes-manifest-orig.json + USER nextjs EXPOSE 3000 diff --git a/docker/middleware.ts b/docker/middleware.ts deleted file mode 100644 index ae143140d..000000000 --- a/docker/middleware.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export const config = { - matcher: '/:path*', -}; - -const TRACKER_NAME = '/script.js'; -const COLLECT_ENDPOINT = '/api/send'; - -const apiHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Headers': '*', - 'Access-Control-Allow-Methods': 'GET, DELETE, POST, PUT', - 'Access-Control-Max-Age': process.env.CORS_MAX_AGE || '86400', - 'Cache-Control': 'no-cache', -}; - -const trackerHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Cache-Control': 'public, max-age=86400, must-revalidate', -}; - -function customCollectEndpoint(request: NextRequest) { - const collectEndpoint = process.env.COLLECT_API_ENDPOINT; - - if (collectEndpoint) { - const url = request.nextUrl.clone(); - - if (url.pathname.endsWith(collectEndpoint)) { - url.pathname = COLLECT_ENDPOINT; - return NextResponse.rewrite(url, { headers: apiHeaders }); - } - } -} - -function customScriptName(request: NextRequest) { - const scriptName = process.env.TRACKER_SCRIPT_NAME; - - if (scriptName) { - const url = request.nextUrl.clone(); - const names = scriptName.split(',').map(name => name.trim().replace(/^\/+/, '')); - - if (names.find(name => url.pathname.endsWith(name))) { - url.pathname = TRACKER_NAME; - return NextResponse.rewrite(url, { headers: trackerHeaders }); - } - } -} - -function customScriptUrl(request: NextRequest) { - const scriptUrl = process.env.TRACKER_SCRIPT_URL; - - if (scriptUrl && request.nextUrl.pathname.endsWith(TRACKER_NAME)) { - return NextResponse.rewrite(scriptUrl, { headers: trackerHeaders }); - } -} - -export default function middleware(req: NextRequest) { - const fns = [customCollectEndpoint, customScriptName, customScriptUrl]; - - for (const fn of fns) { - const res = fn(req); - if (res) { - return res; - } - } - - return NextResponse.next(); -} diff --git a/package.json b/package.json index d2325de54..8e35c620a 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app", "start": "next start", "build-docker": "npm-run-all build-db build-tracker build-geo build-app", - "start-docker": "npm-run-all check-db update-tracker start-server", + "start-docker": "npm-run-all check-db update-tracker set-routes-manifest start-server", "start-env": "node scripts/start-env.js", "start-server": "node server.js", "build-app": "next build --turbo", @@ -28,6 +28,7 @@ "build-db": "npm-run-all build-db-client build-prisma-client", "build-db-schema": "prisma db pull", "build-db-client": "prisma generate", + "set-routes-manifest": "node scripts/set-routes-manifest.js", "update-tracker": "node scripts/update-tracker.js", "update-db": "prisma migrate deploy", "check-db": "node scripts/check-db.js", diff --git a/scripts/set-routes-manifest.js b/scripts/set-routes-manifest.js new file mode 100644 index 000000000..392c7b743 --- /dev/null +++ b/scripts/set-routes-manifest.js @@ -0,0 +1,81 @@ +/* eslint-disable no-console */ +import 'dotenv/config'; +import fs from 'node:fs'; +import path from 'node:path'; +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); + +const routesManifestPath = path.resolve(process.cwd(), '.next/routes-manifest.json'); +const originalPath = path.resolve(process.cwd(), '.next/routes-manifest-orig.json'); +const originalManifest = require(originalPath); + +const basePath = originalManifest.basePath; + +const API_PATH = basePath + '/api/:path*'; +const TRACKER_SCRIPT = basePath + '/script.js'; + +const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT; +const trackerScriptName = process.env.TRACKER_SCRIPT_NAME; + +const headers = []; +const rewrites = []; + +if (collectApiEndpoint) { + const apiRoute = originalManifest.headers.find(route => route.source === API_PATH); + const routeRegex = new RegExp(apiRoute.regex); + + const normalizedSource = basePath + collectApiEndpoint; + + rewrites.push({ + source: normalizedSource, + destination: basePath + '/api/send', + }); + + if (!routeRegex.test(normalizedSource)) { + headers.push({ + source: normalizedSource, + headers: apiRoute.headers, + }); + } +} + +if (trackerScriptName) { + const trackerRoute = originalManifest.headers.find(route => route.source === TRACKER_SCRIPT); + + const names = trackerScriptName?.split(',').map(name => name.trim()); + + if (names) { + names.forEach(name => { + const normalizedSource = `${basePath}/${name.replace(/^\/+/, '')}`; + + rewrites.push({ + source: normalizedSource, + destination: TRACKER_SCRIPT, + }); + + headers.push({ + source: normalizedSource, + headers: trackerRoute.headers, + }); + }); + } +} + +const routesManifest = { ...originalManifest }; + +if (rewrites.length !== 0) { + const { buildCustomRoute } = require('next/dist/lib/build-custom-route'); + + const builtHeaders = headers.map(header => buildCustomRoute('header', header)); + const builtRewrites = rewrites.map(rewrite => buildCustomRoute('rewrite', rewrite)); + + routesManifest.headers = [...originalManifest.headers, ...builtHeaders]; + routesManifest.rewrites = [...builtRewrites, ...originalManifest.rewrites]; + + console.log('Using updated Next.js routes manifest'); +} else { + console.log('Using original Next.js routes manifest'); +} + +fs.writeFileSync(routesManifestPath, JSON.stringify(routesManifest, null, 2)); diff --git a/src/components/input/FilterBar.tsx b/src/components/input/FilterBar.tsx index b81f77e30..96ad51b4f 100644 --- a/src/components/input/FilterBar.tsx +++ b/src/components/input/FilterBar.tsx @@ -25,14 +25,13 @@ export function FilterBar({ websiteId }: { websiteId: string }) { const { formatValue } = useFormat(); const { router, - pathname, updateParams, replaceParams, query: { segment, cohort }, } = useNavigation(); const { filters, operatorLabels } = useFilters(); const { data, isLoading } = useWebsiteSegmentQuery(websiteId, segment || cohort); - const canSaveSegment = filters.length > 0 && !segment && !cohort && !pathname.includes('/share'); + const canSaveSegment = filters.length > 0 && !segment && !cohort; const handleCloseFilter = (param: string) => { router.push(updateParams({ [param]: undefined }));