diff --git a/.gitignore b/.gitignore index 89c5664..639e566 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ /.react-router/ /build/ /docker/build/ + +# Amplify build output +/.amplify-hosting/ diff --git a/amplify.yml b/amplify.yml new file mode 100644 index 0000000..26ab8ee --- /dev/null +++ b/amplify.yml @@ -0,0 +1,19 @@ +version: 1 +frontend: + phases: + preBuild: + commands: + - nvm use 22 + - corepack enable + - corepack prepare pnpm@10.28.2 --activate + - pnpm install --frozen-lockfile + build: + commands: + - env | grep -e SUPABASE_URL >> .env + - env | grep -e SUPABASE_PUBLISHABLE_KEY >> .env + - pnpm run build + - bash bin/postbuild.sh + artifacts: + baseDirectory: .amplify-hosting + files: + - '**/*' diff --git a/app/api/resource-revisions/methods.ts b/app/api/resource-revisions/methods.ts index a703e6d..0596649 100644 --- a/app/api/resource-revisions/methods.ts +++ b/app/api/resource-revisions/methods.ts @@ -1,20 +1,20 @@ import type { SupabaseClient } from "@supabase/supabase-js"; import type { - ResourceRevision, - RevisionStatus, + ResourceEdit, + ResourceEditReviewStatus, } from "~/types/ResourceRevision"; -const TABLE_NAME = "resource_revisions"; +const TABLE_NAME = "resource_edits"; export const getResourceRevisionAPI = (client: SupabaseClient) => { const table = client.from(TABLE_NAME); return { - getList: async (params: { status?: RevisionStatus } = {}) => { + getList: async (params: { status?: ResourceEditReviewStatus } = {}) => { let query = table.select("*").order("date_created", { ascending: false }); if (params.status) { - query = query.eq("status", params.status); + query = query.eq("review_status", params.status); } const { data, error } = await query; @@ -23,13 +23,13 @@ export const getResourceRevisionAPI = (client: SupabaseClient) => { throw error; } - return (data ?? []) as ResourceRevision[]; + return (data ?? []) as ResourceEdit[]; }, getById: async (id: number) => { const { data, error } = await table .select("*") .eq("id", id) - .single(); + .single(); if (error) { throw error; @@ -37,12 +37,12 @@ export const getResourceRevisionAPI = (client: SupabaseClient) => { return data; }, - updateStatus: async (id: number, status: RevisionStatus) => { + updateStatus: async (id: number, status: ResourceEditReviewStatus) => { const { data, error } = await table .update({ status }) .eq("id", id) .select() - .single(); + .single(); if (error) { throw error; @@ -50,12 +50,12 @@ export const getResourceRevisionAPI = (client: SupabaseClient) => { return data; }, - updateFields: async (id: number, values: Partial) => { + updateFields: async (id: number, values: Partial) => { const { data, error } = await table .update(values) .eq("id", id) .select() - .single(); + .single(); if (error) { throw error; diff --git a/app/api/types.ts b/app/api/types.ts index c123452..899ef5e 100644 --- a/app/api/types.ts +++ b/app/api/types.ts @@ -14,8 +14,8 @@ type PaginatedResult = { export type GetModelAPI = (client: SupabaseClient) => { getList: (params: Params) => Promise>; - getById: (id: string) => Promise; + getById: (id: number) => Promise; create: (values: Entity) => Promise; - updateById: (id: string, values: Partial) => Promise; - delete: (id: string) => Promise; + updateById: (id: number, values: Partial) => Promise; + delete: (id: number) => Promise; }; diff --git a/app/routes/authenticated/dashboard.tsx b/app/routes/authenticated/dashboard.tsx index 845c394..2e8c899 100644 --- a/app/routes/authenticated/dashboard.tsx +++ b/app/routes/authenticated/dashboard.tsx @@ -40,7 +40,7 @@ export const loader: LoaderFunction = async ({ request }) => { const creator = revision.creator || "Unknown"; submitterCounts.set(creator, (submitterCounts.get(creator) ?? 0) + 1); - if (revision.status === "PENDING") { + if (revision.review_status === "PENDING") { pendingCount += 1; outstandingByType.set( revision.resource_type, @@ -218,7 +218,7 @@ const Dashboard = () => { variant="outlined" sx={{ p: 2.5, flex: 1, bgcolor: "action.hover" }} > - + Top approvers diff --git a/app/routes/authenticated/reviews/detail.tsx b/app/routes/authenticated/reviews/detail.tsx index d6c7acf..3a02b85 100644 --- a/app/routes/authenticated/reviews/detail.tsx +++ b/app/routes/authenticated/reviews/detail.tsx @@ -45,7 +45,7 @@ import type { ResourceType, WaterTag, } from "~/types/ResourceEntry"; -import type { ResourceRevision } from "~/types/ResourceRevision"; +import type { ResourceEdit } from "~/types/ResourceRevision"; import { statusChipColor } from "~/utils/chipColors"; export const middleware = [authMiddleware]; @@ -129,7 +129,7 @@ type EditableValues = { bathroom: { tags: BathroomTag[] }; }; -const toEditableValues = (revision: ResourceRevision): EditableValues => ({ +const toEditableValues = (revision: ResourceEdit): EditableValues => ({ name: revision.name ?? "", resource_type: revision.resource_type, entry_type: revision.entry_type ?? "", @@ -174,7 +174,7 @@ export const loader: LoaderFunction = async ({ request, params }) => { let resource: ResourceEntry | null = null; try { const resourceAPI = getResourceEntryAPI(client); - resource = await resourceAPI.getById(String(revision.mapped_resources)); + resource = await resourceAPI.getById(revision.mapped_resource); } catch { resource = null; } @@ -224,7 +224,7 @@ export const action: ActionFunction = async ({ request, params }) => { const ReviewDetail = () => { const { revision, resource } = useLoaderData<{ - revision: ResourceRevision; + revision: ResourceEdit; resource: ResourceEntry | null; }>(); const actionData = useActionData<{ message?: string }>(); @@ -238,7 +238,7 @@ const ReviewDetail = () => { setValues(toEditableValues(revision)); }, [revision]); - const isPending = revision.status === "PENDING"; + const isPending = revision.review_status === "PENDING"; const isSaving = fetcher.state !== "idle"; const setField = ( @@ -267,7 +267,7 @@ const ReviewDetail = () => { ) => setValues((v) => ({ ...v, bathroom: { ...v.bathroom, [key]: value } })); const handleSave = () => { - const payload: Partial = { + const payload: Partial = { name: values.name || null, resource_type: values.resource_type, entry_type: values.entry_type || null, @@ -335,9 +335,9 @@ const ReviewDetail = () => { Submitted{" "} @@ -352,8 +352,8 @@ const ReviewDetail = () => { {!resource && ( - The resource this revision targets (#{revision.mapped_resources}) - could not be found — it may have been deleted. + The resource this revision targets (#{revision.mapped_resource}) could + not be found — it may have been deleted. )} diff --git a/app/routes/authenticated/reviews/index.tsx b/app/routes/authenticated/reviews/index.tsx index 3c13900..5a58d49 100644 --- a/app/routes/authenticated/reviews/index.tsx +++ b/app/routes/authenticated/reviews/index.tsx @@ -22,7 +22,7 @@ import { type LoaderFunction, useLoaderData, useNavigate } from "react-router"; import { getDatabaseClient } from "~/api/client.server"; import { getResourceRevisionAPI } from "~/api/resource-revisions/methods"; import { authMiddleware } from "~/middleware/auth"; -import type { ResourceRevision } from "~/types/ResourceRevision"; +import type { ResourceEdit } from "~/types/ResourceRevision"; import { resourceTypeChipColor, resourceTypeChipIcon, @@ -35,7 +35,7 @@ export const loader: LoaderFunction = async ({ request }) => { const revisionAPI = getResourceRevisionAPI(client); const revisions = await revisionAPI.getList({ status: "PENDING" }); - const resourceIds = [...new Set(revisions.map((r) => r.mapped_resources))]; + const resourceIds = [...new Set(revisions.map((r) => r.mapped_resource))]; const resourceNames = new Map(); if (resourceIds.length > 0) { @@ -59,7 +59,7 @@ export const loader: LoaderFunction = async ({ request }) => { }; }; -type RowData = ResourceRevision & { resourceLabel: string }; +type RowData = ResourceEdit & { resourceLabel: string }; const columns: ColumnDef[] = [ { @@ -96,7 +96,7 @@ const columns: ColumnDef[] = [ const ReviewsQueue = () => { const { revisions, resourceNames } = useLoaderData<{ - revisions: ResourceRevision[]; + revisions: ResourceEdit[]; resourceNames: Record; }>(); const navigate = useNavigate(); @@ -109,8 +109,8 @@ const ReviewsQueue = () => { revisions.map((revision) => ({ ...revision, resourceLabel: - resourceNames[revision.mapped_resources] || - `Resource #${revision.mapped_resources}`, + resourceNames[revision.mapped_resource] || + `Resource #${revision.mapped_resource}`, })), [revisions, resourceNames], ); diff --git a/app/types/ResourceRevision.ts b/app/types/ResourceRevision.ts index cd79fa6..549aec1 100644 --- a/app/types/ResourceRevision.ts +++ b/app/types/ResourceRevision.ts @@ -1,29 +1,14 @@ import type { ResourceEntry } from "~/types/ResourceEntry"; -/** - * Review status of a `resource_revisions` row. Note this table reuses the - * `status` column for review state rather than the resource's operational - * status (`ResourceStatus`) — the two are unrelated despite the shared - * column name. - */ -export type RevisionStatus = "PENDING" | "APPROVED" | "REJECTED"; +export type ResourceEditReviewStatus = "PENDING" | "APPROVED" | "REJECTED"; -/** - * A proposed edit to an existing resource, staged in `resource_revisions`. - * - * Shape mirrors `ResourceEntry` (it's a full proposed snapshot of the - * resource) plus: - * - `mapped_resources`: FK to `resources.id` — the resource this revision - * proposes changes to. Every revision maps to an existing resource; this - * table has no concept of a brand-new, not-yet-existing resource. - * - `mapped_resource`: a second, unconstrained (no FK) int column that - * exists on the table but isn't used by this feature. - * - `status`: review state (`PENDING` / `APPROVED` / `REJECTED`), not the - * resource's operational status. - */ -export type ResourceRevision = Omit & { - id: number; +export type ResourceEdit = { + id?: number; mapped_resource: number; - mapped_resources: number; - status: RevisionStatus; -}; + review_status: ResourceEditReviewStatus; + submitted_by?: string; + submitted_at?: Date | string; + reviewed_by?: string; + reviewed_at?: Date | string; + review_notes?: string; +} & Omit; diff --git a/app/utils/chipColors.tsx b/app/utils/chipColors.tsx index 9509c68..956b6e5 100644 --- a/app/utils/chipColors.tsx +++ b/app/utils/chipColors.tsx @@ -2,7 +2,7 @@ import { LocalDining, Park, WaterDrop, Wc } from "@mui/icons-material"; import type { ChipProps } from "@mui/material"; import type { ReactElement } from "react"; import type { ResourceType } from "~/types/ResourceEntry"; -import type { RevisionStatus } from "~/types/ResourceRevision"; +import type { ResourceEditReviewStatus } from "~/types/ResourceRevision"; export const resourceTypeChipColor = ( type: ResourceType | string, @@ -72,7 +72,7 @@ export const resourceTypeChipIcon = ( }; export const statusChipColor = ( - status: RevisionStatus | string, + status: ResourceEditReviewStatus | string, ): ChipProps["color"] => { switch (status) { case "PENDING": diff --git a/bin/postbuild.sh b/bin/postbuild.sh new file mode 100755 index 0000000..3b463e2 --- /dev/null +++ b/bin/postbuild.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -euo pipefail + +rm -rf .amplify-hosting + +mkdir -p .amplify-hosting/compute/default/build +mkdir -p .amplify-hosting/static + +# Server entry point and server-side build +cp server.js .amplify-hosting/compute/default/index.js +cp package.json .amplify-hosting/compute/default/package.json +cp pnpm-lock.yaml .amplify-hosting/compute/default/pnpm-lock.yaml +cp pnpm-workspace.yaml .amplify-hosting/compute/default/pnpm-workspace.yaml +cp -r build/server .amplify-hosting/compute/default/build/server + +# Copy env vars baked in by the Amplify build phase so the runtime +# process can load them via process.loadEnvFile (see server.js) +if [ -f .env ]; then + cp .env .amplify-hosting/compute/default/.env +fi + +# Install production dependencies directly in the compute bundle so pnpm +# creates a self-contained node_modules/.pnpm virtual store there. +# Copying pnpm's node_modules from the build root is not safe because its +# symlinks target the global content-addressable store which won't exist +# in the Lambda runtime. +cd .amplify-hosting/compute/default +pnpm install --prod --frozen-lockfile +cd - + +# Static client assets served by CloudFront/S3 via the deployment manifest +cp -r build/client/. .amplify-hosting/static/ + +# Amplify deployment manifest +cp deploy-manifest.json .amplify-hosting/deploy-manifest.json diff --git a/deploy-manifest.json b/deploy-manifest.json new file mode 100644 index 0000000..21f30da --- /dev/null +++ b/deploy-manifest.json @@ -0,0 +1,38 @@ +{ + "version": 1, + "framework": { "name": "react-router", "version": "1.0.0" }, + "routes": [ + { + "path": "/assets/*", + "target": { + "kind": "Static", + "cacheControl": "public, max-age=31536000, immutable" + } + }, + { + "path": "/*.*", + "target": { + "kind": "Static", + "cacheControl": "public, max-age=3600" + }, + "fallback": { + "kind": "Compute", + "src": "default" + } + }, + { + "path": "/*", + "target": { + "kind": "Compute", + "src": "default" + } + } + ], + "computeResources": [ + { + "name": "default", + "runtime": "nodejs22.x", + "entrypoint": "index.js" + } + ] +} diff --git a/package.json b/package.json index 798bbe5..837a91a 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,14 @@ "@emotion/styled": "^11.14.1", "@mui/icons-material": "^7.3.8", "@mui/material": "^7.3.8", + "@react-router/express": "^7.13.0", "@react-router/node": "^7.13.0", "@react-router/serve": "^7.13.0", "@supabase/ssr": "^0.8.0", "@supabase/supabase-js": "^2.96.0", "@tanstack/react-table": "^8.21.3", "composable-functions": "^5.0.0", + "express": "^5.2.1", "isbot": "^5.1.35", "react": "^19.2.4", "react-dom": "^19.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9890669..8ff0d9c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@mui/material': specifier: ^7.3.8 version: 7.3.8(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@react-router/express': + specifier: ^7.13.0 + version: 7.13.0(express@5.2.1)(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) '@react-router/node': specifier: ^7.13.0 version: 7.13.0(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) @@ -38,6 +41,9 @@ importers: composable-functions: specifier: ^5.0.0 version: 5.0.0 + express: + specifier: ^5.2.1 + version: 5.2.1 isbot: specifier: ^5.1.35 version: 5.1.35 @@ -871,6 +877,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -896,6 +906,10 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -953,10 +967,18 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -966,6 +988,10 @@ packages: cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -1081,6 +1107,10 @@ packages: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -1097,6 +1127,10 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} @@ -1108,6 +1142,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1162,6 +1200,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1180,6 +1222,9 @@ packages: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + isbot@5.1.35: resolution: {integrity: sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg==} engines: {node: '>=18'} @@ -1353,9 +1398,17 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -1372,6 +1425,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -1400,6 +1457,10 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -1423,6 +1484,9 @@ packages: resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} engines: {node: '>= 0.8'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + p-map@7.0.4: resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} engines: {node: '>=18'} @@ -1445,6 +1509,9 @@ packages: path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -1485,6 +1552,10 @@ packages: resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} engines: {node: '>=0.6'} + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -1493,6 +1564,10 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -1561,6 +1636,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -1586,10 +1665,18 @@ packages: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serve-static@1.16.3: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -1600,6 +1687,10 @@ packages: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + side-channel-map@1.0.1: resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} engines: {node: '>= 0.4'} @@ -1612,6 +1703,10 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1663,6 +1758,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1750,6 +1849,9 @@ packages: yaml: optional: true + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -2326,6 +2428,14 @@ snapshots: optionalDependencies: typescript: 5.9.3 + '@react-router/express@7.13.0(express@5.2.1)(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': + dependencies: + '@react-router/node': 7.13.0(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + express: 5.2.1 + react-router: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + optionalDependencies: + typescript: 5.9.3 + '@react-router/node@7.13.0(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': dependencies: '@mjackson/node-fetch-server': 0.2.0 @@ -2509,6 +2619,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + arg@5.0.2: {} array-flatten@1.1.1: {} @@ -2551,6 +2666,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.19 @@ -2609,14 +2738,20 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.1.0: {} + content-type@1.0.5: {} + content-type@2.0.0: {} + convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} + cookie@0.7.2: {} cookie@1.1.1: {} @@ -2756,6 +2891,39 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.8: {} fdir@6.5.0(picomatch@4.0.3): @@ -2774,12 +2942,25 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-root@1.1.0: {} forwarded@0.2.0: {} fresh@0.5.2: {} + fresh@2.0.0: {} + fsevents@2.3.3: optional: true @@ -2835,6 +3016,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -2850,6 +3035,8 @@ snapshots: dependencies: hasown: 2.0.2 + is-promise@4.0.0: {} + isbot@5.1.35: {} jiti@2.6.1: @@ -2972,8 +3159,12 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.1: {} + merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + methods@1.1.2: {} mime-db@1.52.0: {} @@ -2984,6 +3175,10 @@ snapshots: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} morgan@1.10.1: @@ -3006,6 +3201,8 @@ snapshots: negotiator@0.6.4: {} + negotiator@1.0.0: {} + node-releases@2.0.27: {} object-assign@4.1.1: {} @@ -3022,6 +3219,10 @@ snapshots: on-headers@1.1.0: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + p-map@7.0.4: {} parent-module@1.0.1: @@ -3041,6 +3242,8 @@ snapshots: path-to-regexp@0.1.12: {} + path-to-regexp@8.4.2: {} + path-type@4.0.0: {} pathe@1.1.2: {} @@ -3080,6 +3283,11 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + range-parser@1.2.1: {} raw-body@2.5.3: @@ -3089,6 +3297,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -3172,6 +3387,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -3202,6 +3427,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serve-static@1.16.3: dependencies: encodeurl: 2.0.0 @@ -3211,6 +3452,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + set-cookie-parser@2.7.2: {} setprototypeof@1.2.0: {} @@ -3220,6 +3470,11 @@ snapshots: es-errors: 1.3.0 object-inspect: 1.13.4 + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-map@1.0.1: dependencies: call-bound: 1.0.4 @@ -3243,6 +3498,14 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -3278,6 +3541,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + typescript@5.9.3: {} undici-types@6.21.0: {} @@ -3344,6 +3613,8 @@ snapshots: jiti: 2.6.1 lightningcss: 1.30.2 + wrappy@1.0.2: {} + ws@8.19.0: {} yallist@3.1.1: {} diff --git a/server.js b/server.js new file mode 100644 index 0000000..9eaee34 --- /dev/null +++ b/server.js @@ -0,0 +1,25 @@ +import { createRequestHandler } from "@react-router/express"; +import express from "express"; + +// Load env vars written by the Amplify build phase. Silently skipped +// in environments where the file doesn't exist (e.g. local dev with +// vars already in the shell). +try { + process.loadEnvFile(".env"); +} catch { + // no-op +} + +const app = express(); + +app.disable("x-powered-by"); + +app.all( + "*splat", + createRequestHandler({ build: () => import("./build/server/index.js") }), +); + +const port = process.env.PORT || 3000; +app.listen(port, () => { + console.log(`Server listening on port ${port}`); +});