@react-foundry/plop-pack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +42 -0
  3. package/package.json +38 -0
  4. package/skel/app/.dockerignore +9 -0
  5. package/skel/app/Dockerfile +32 -0
  6. package/skel/app/Makefile.hbs +87 -0
  7. package/skel/app/README.md.hbs +91 -0
  8. package/skel/app/aws-lambda-entry.js +3 -0
  9. package/skel/app/cypress.config.mjs +6 -0
  10. package/skel/app/feat/home.spec.js +5 -0
  11. package/skel/app/gitignore +10 -0
  12. package/skel/app/jest.config.cjs +15 -0
  13. package/skel/app/lambda.Dockerfile +23 -0
  14. package/skel/app/package.json.hbs +61 -0
  15. package/skel/app/plopfile.mjs +5 -0
  16. package/skel/app/public/favicon.ico +0 -0
  17. package/skel/app/react-router.config.ts +13 -0
  18. package/skel/app/src/app/app.scss +0 -0
  19. package/skel/app/src/app/config.ts.hbs +1 -0
  20. package/skel/app/src/app/entry.client.tsx +17 -0
  21. package/skel/app/src/app/entry.server.tsx +85 -0
  22. package/skel/app/src/app/root.tsx +141 -0
  23. package/skel/app/src/app/routes/_index.tsx +23 -0
  24. package/skel/app/src/app/routes.ts +6 -0
  25. package/skel/app/src/server/config.ts +59 -0
  26. package/skel/app/src/server/dev.ts +13 -0
  27. package/skel/app/src/server/httpd.ts +49 -0
  28. package/skel/app/src/server/index.ts +33 -0
  29. package/skel/app/src/server/server-build.d.ts +14 -0
  30. package/skel/app/test.Dockerfile +18 -0
  31. package/skel/app/tsconfig.json +19 -0
  32. package/skel/app/vite.config.server.ts +24 -0
  33. package/skel/app/vite.config.ts +20 -0
  34. package/skel/component/README.md.hbs +66 -0
  35. package/skel/component/assets/Component.scss.hbs +9 -0
  36. package/skel/component/gitignore +5 -0
  37. package/skel/component/jest.config.js.hbs +15 -0
  38. package/skel/component/package.json.hbs +72 -0
  39. package/skel/component/spec/Component.mdx.hbs +19 -0
  40. package/skel/component/spec/Component.stories.tsx.hbs +22 -0
  41. package/skel/component/spec/Component.ts.hbs +28 -0
  42. package/skel/component/src/Component.tsx.hbs +32 -0
  43. package/skel/component/tsconfig.json +14 -0
  44. package/skel/lib/README.md.hbs +51 -0
  45. package/skel/lib/gitignore +5 -0
  46. package/skel/lib/jest.config.js.hbs +15 -0
  47. package/skel/lib/package.json.hbs +44 -0
  48. package/skel/lib/src/index.ts.hbs +6 -0
  49. package/skel/lib/tsconfig.json +14 -0
  50. package/src/action-paths.js +39 -0
  51. package/src/actions/copy.js +27 -0
  52. package/src/actions/merge.js +26 -0
  53. package/src/actions/message.js +3 -0
  54. package/src/actions/shell.js +21 -0
  55. package/src/actions/symlink.js +42 -0
  56. package/src/actions/write.js +20 -0
  57. package/src/extend-generator.js +38 -0
  58. package/src/generators/app.js +140 -0
  59. package/src/generators/component.js +73 -0
  60. package/src/generators/lib.js +54 -0
  61. package/src/helpers/eq.js +3 -0
  62. package/src/helpers/md-title.js +6 -0
  63. package/src/index.js +38 -0
  64. package/src/rel-to-skel.js +9 -0
  65. package/src/relative-path.js +5 -0
  66. package/src/run-plop.js +24 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (C) 2019-2025 Crown Copyright
4
+ Copyright (C) 2019-2026 Daniel A.C. Martin
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
7
+ this software and associated documentation files (the "Software"), to deal in
8
+ the Software without restriction, including without limitation the rights to
9
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10
+ of the Software, and to permit persons to whom the Software is furnished to do
11
+ so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ React Foundry - Plop Pack
2
+ =========================
3
+
4
+ Plop pack with misc tools.
5
+
6
+
7
+ Using this package
8
+ ------------------
9
+
10
+ First install the package into your project:
11
+
12
+ ```shell
13
+ npm install -D @react-foundry/plop-pack
14
+ ```
15
+
16
+ Then use it in your `plopfile.js` as follows:
17
+
18
+ ```js
19
+ module.exports = plop => {
20
+ plop.load('@react-foundry/plop-pack');
21
+
22
+ // ...
23
+ }
24
+ ```
25
+
26
+
27
+ Working on this package
28
+ -----------------------
29
+
30
+ Before working on this package you must install its dependencies using
31
+ the following command:
32
+
33
+ ```shell
34
+ pnpm install
35
+ ```
36
+
37
+
38
+ ### Testing
39
+
40
+ ```shell
41
+ npm test
42
+ ```
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@react-foundry/plop-pack",
3
+ "version": "0.1.0",
4
+ "description": "Plop pack with misc tools.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "files": [
11
+ "/src",
12
+ "/skel"
13
+ ],
14
+ "author": "Daniel A.C. Martin <npm@daniel-martin.co.uk> (http://daniel-martin.co.uk/)",
15
+ "license": "MIT",
16
+ "engines": {
17
+ "node": ">=12.0.0"
18
+ },
19
+ "dependencies": {
20
+ "fs-extra": "^11.3.3",
21
+ "lodash": "^4.17.23",
22
+ "minimist": "^1.2.8",
23
+ "node-plop": "^0.32.3",
24
+ "plop": "^4.0.5",
25
+ "resolve": "^1.22.11",
26
+ "shelljs": "^0.10.0"
27
+ },
28
+ "devDependencies": {
29
+ "handlebars": "4.7.8",
30
+ "jest": "30.2.0",
31
+ "jest-environment-jsdom": "30.2.0"
32
+ },
33
+ "scripts": {
34
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
35
+ "build": "true",
36
+ "clean": "true"
37
+ }
38
+ }
@@ -0,0 +1,9 @@
1
+ .cypress/
2
+ .react-router/
3
+ node_modules/
4
+ pkg/
5
+ spec/
6
+ src/
7
+ README.md
8
+ tsconfig.json
9
+ tsconfig.tsbuildinfo
@@ -0,0 +1,32 @@
1
+ FROM node:24.13.0-alpine
2
+
3
+ RUN apk add --no-cache ca-certificates \
4
+ && apk upgrade --no-cache \
5
+ && addgroup -S app \
6
+ && adduser -S app -G app -u 31337 -h /app/ \
7
+ && chown -R app:app /app/
8
+
9
+ USER app
10
+ WORKDIR /app
11
+ ENV NODE_ENV production
12
+ ENV MODE server
13
+
14
+ COPY package.json /app/
15
+ COPY dist/ /app/dist/
16
+
17
+ USER 31337
18
+ ENV LISTEN_HOST="::" \
19
+ LISTEN_PORT="8080" \
20
+ SSR_ONLY="false" \
21
+ SESSIONS_SECRET="changeme" \
22
+ AUTH_METHOD="none" \
23
+ OIDC_ISSUER="https://keycloak/realms/demo/" \
24
+ OIDC_CLIENT_ID="app" \
25
+ OIDC_CLIENT_SECRET="" \
26
+ OIDC_REDIRECT_URI="https://localhost" \
27
+ AUTH_HEADER_USERNAME="x-auth-username" \
28
+ AUTH_HEADER_GROUPS="x-auth-groups" \
29
+ AUTH_HEADER_ROLES="x-auth-roles"
30
+ EXPOSE ${LISTEN_PORT:-8080}
31
+ HEALTHCHECK CMD wget -q -O /dev/null http://localhost/healthz:${LISTEN_PORT} || exit 1
32
+ CMD ["node", "."]
@@ -0,0 +1,87 @@
1
+ DOCKER_IMAGE ?= {{{pkg 'name'}}}-{{{name}}}
2
+ DOCKER_REGISTRY ?= hub.docker.com
3
+ DOCKER_REPO ?= $(DOCKER_REGISTRY)/$(DOCKER_IMAGE)
4
+ KUBE_CONTEXT ?= {{{pkg 'name'}}}-dev
5
+ KUBE_DEPLOYMENT ?= {{{pkg 'name'}}}-{{{name}}}
6
+ KUBE_CONTAINER ?= app
7
+
8
+ kubectl ?= kubectl --context $(KUBE_CONTEXT)
9
+
10
+ patch_version != jq -r '.version' 'package.json'
11
+ minor_version != echo "$(patch_version)" | awk -F '.' '{print $$1"."$$2}'
12
+ major_version != echo "$(patch_version)" | awk -F '.' '{print $$1}'
13
+ full_version != echo "$(patch_version)-$${BUILD_NUMBER/%/-}$$(git rev-parse --short HEAD)"
14
+
15
+ .PHONY: all aws-lambda build clean deploy deps distclean docker docker-clean docker-push docker-test netlify node-deps test unit-test
16
+
17
+ all: deps test docker
18
+
19
+ clean:
20
+ npm run clean
21
+ rm -rf node_modules/ pkg/
22
+
23
+ distclean: clean docker-clean
24
+
25
+ deps: node-deps
26
+
27
+ node-deps: node_modules/.bin/react-router
28
+
29
+ node_modules/%: package.json
30
+ pnpm install
31
+
32
+ build: dist/server/index.js
33
+
34
+ dist/%: node_modules/.bin/react-router
35
+ npm run build
36
+
37
+ docker: build
38
+ docker build -t '$(DOCKER_IMAGE)' .
39
+
40
+ docker-test:
41
+ docker build -t '$(DOCKER_IMAGE)-test' -f 'test.Dockerfile' .
42
+
43
+ docker-clean:
44
+ docker rmi -f '$(DOCKER_IMAGE)' || true
45
+ docker rmi -f '$(DOCKER_IMAGE)-test' || true
46
+
47
+ docker-push: docker
48
+ docker tag $(DOCKER_IMAGE) '$(DOCKER_REPO):$(full_version)' && docker push '$(DOCKER_REPO):$(full_version)'
49
+ docker tag $(DOCKER_IMAGE) '$(DOCKER_REPO):$(patch_version)' && docker push '$(DOCKER_REPO):$(patch_version)'
50
+ docker tag $(DOCKER_IMAGE) '$(DOCKER_REPO):$(minor_version)' && docker push '$(DOCKER_REPO):$(minor_version)'
51
+ docker tag $(DOCKER_IMAGE) '$(DOCKER_REPO):$(major_version)' && docker push '$(DOCKER_REPO):$(major_version)'
52
+ docker tag $(DOCKER_IMAGE) '$(DOCKER_REPO):latest' && docker push '$(DOCKER_REPO):latest'
53
+
54
+ docker-run:
55
+ docker run --name=docs -p '8080:8080' '$(DOCKER_IMAGE)'
56
+
57
+ aws-lambda: pkg/aws-lambda/{{{name}}}.zip
58
+
59
+ pkg/aws-lambda/%.zip: aws-lambda-entry.js package.json dist/server/index.js
60
+ mkdir -p '$(@D)'
61
+ zip -rv '$(@)' 'dist'
62
+ zip -v '$(@)' 'package.json'
63
+ cp '$(<)' '$(@D)/$(basename $(@F)).js'
64
+ zip -mvj '$(@)' '$(@D)/$(basename $(@F)).js'
65
+
66
+ netlify: pkg/netlify/functions/{{{name}}}.zip pkg/netlify/publish/_redirects
67
+
68
+ pkg/netlify/functions/%.zip: pkg/aws-lambda/%.zip
69
+ mkdir -p '$(@D)'
70
+ cp -a '$(<)' '$(@)'
71
+
72
+ pkg/netlify/publish/_redirects: dist/server/index.js
73
+ mkdir -p '$(@D)'
74
+ cp -a 'dist/app/client/'* '$(@D)'
75
+ echo '/* /.netlify/functions/{{{name}}}/:splat 200' > '$(@)'
76
+
77
+ deploy: docker-push
78
+ $(kubectl) set image 'deployment/$(KUBE_DEPLOYMENT)' '$(KUBE_CONTAINER)=$(DOCKER_REPO):$(full_version)'
79
+ $(kubectl) rollout status 'deployment/$(KUBE_DEPLOYMENT)'
80
+
81
+ functional-test: build
82
+ npm run 'test:functional:ci'
83
+
84
+ unit-test: node-deps
85
+ npm test
86
+
87
+ test: unit-test functional-test
@@ -0,0 +1,91 @@
1
+ {{#mdTitle}}{{{titleCase (pkg 'name')}}} - {{{titleCase name}}}{{/mdTitle}}
2
+
3
+ {{{description}}}
4
+
5
+ ## Welcome to React Router!
6
+
7
+ A modern, production-ready template for building full-stack React applications using React Router.
8
+
9
+ [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
10
+
11
+ ## Features
12
+
13
+ - 🚀 Server-side rendering
14
+ - ⚡️ Hot Module Replacement (HMR)
15
+ - 📦 Asset bundling and optimization
16
+ - 🔄 Data loading and mutations
17
+ - 🔒 TypeScript by default
18
+ - 🎉 TailwindCSS for styling
19
+ - 📖 [React Router docs](https://reactrouter.com/)
20
+
21
+ ## Getting Started
22
+
23
+ ### Installation
24
+
25
+ Install the dependencies:
26
+
27
+ ```bash
28
+ npm install
29
+ ```
30
+
31
+ ### Development
32
+
33
+ Start the development server with HMR:
34
+
35
+ ```bash
36
+ npm run dev
37
+ ```
38
+
39
+ Your application will be available at `http://localhost:5173`.
40
+
41
+ ## Building for Production
42
+
43
+ Create a production build:
44
+
45
+ ```bash
46
+ npm run build
47
+ ```
48
+
49
+ ## Deployment
50
+
51
+ ### Docker Deployment
52
+
53
+ To build and run using Docker:
54
+
55
+ ```bash
56
+ docker build -t my-app .
57
+
58
+ # Run the container
59
+ docker run -p 3000:3000 my-app
60
+ ```
61
+
62
+ The containerized application can be deployed to any platform that supports Docker, including:
63
+
64
+ - AWS ECS
65
+ - Google Cloud Run
66
+ - Azure Container Apps
67
+ - Digital Ocean App Platform
68
+ - Fly.io
69
+ - Railway
70
+
71
+ ### DIY Deployment
72
+
73
+ If you're familiar with deploying Node applications, the built-in app server is production-ready.
74
+
75
+ Make sure to deploy the output of `npm run build`
76
+
77
+ ```
78
+ ├── package.json
79
+ ├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
80
+ ├── build/
81
+ │ ├── client/ # Static assets
82
+ │ └── server/ # Server-side code
83
+ ```
84
+
85
+ ## Styling
86
+
87
+ This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
88
+
89
+ ---
90
+
91
+ Built with ❤️ using React Router.
@@ -0,0 +1,3 @@
1
+ process.env['MODE'] = 'serverless';
2
+ process.env['NODE_ENV'] = 'production';
3
+ export const { handler } = await import('./dist/server/index.js');
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'cypress';
2
+ import projectConfig from '../../cypress.config.mjs';
3
+
4
+ export default defineConfig({
5
+ ...projectConfig
6
+ });
@@ -0,0 +1,5 @@
1
+ describe('The home page', () => {
2
+ it('successfully loads', () => {
3
+ cy.visitReady('/');
4
+ });
5
+ });
@@ -0,0 +1,10 @@
1
+ .cypress/
2
+ .DS_Store
3
+ .netlify/
4
+ .react-router/
5
+ dist/
6
+ node_modules/
7
+ package-lock.json
8
+ pkg/
9
+ pnpm-lock.yaml
10
+ tsconfig.tsbuildinfo
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ const baseConfig = require('../../jest.config.base');
4
+
5
+ const config = {
6
+ ...baseConfig,
7
+ collectCoverageFrom: [
8
+ '<rootDir>/src/**.{ts,tsx}',
9
+ ],
10
+ testMatch: [
11
+ '<rootDir>/spec/**.{ts,tsx}'
12
+ ]
13
+ };
14
+
15
+ module.exports = config;
@@ -0,0 +1,23 @@
1
+ FROM public.ecr.aws/lambda/nodejs:24
2
+
3
+ ENV NODE_ENV production
4
+ ENV MODE serverless
5
+
6
+ COPY package.json ${LAMBDA_TASK_ROOT}/
7
+ COPY dist/ ${LAMBDA_TASK_ROOT}/dist/
8
+ COPY aws-lambda-entry.js ${LAMBDA_TASK_ROOT}/entry.js
9
+
10
+ USER 31337
11
+ ENV LISTEN_HOST="::" \
12
+ LISTEN_PORT="8080" \
13
+ SSR_ONLY="false" \
14
+ SESSIONS_SECRET="changeme" \
15
+ AUTH_METHOD="none" \
16
+ OIDC_ISSUER="https://keycloak/realms/demo/" \
17
+ OIDC_CLIENT_ID="app" \
18
+ OIDC_CLIENT_SECRET="" \
19
+ OIDC_REDIRECT_URI="https://localhost" \
20
+ AUTH_HEADER_USERNAME="x-auth-username" \
21
+ AUTH_HEADER_GROUPS="x-auth-groups" \
22
+ AUTH_HEADER_ROLES="x-auth-roles"
23
+ CMD ["entry.handler"]
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@{{{pkg 'name'}}}/{{{name}}}",
3
+ "version": "{{{pkg 'version'}}}",
4
+ "description": "{{{description}}}",
5
+ "private": true,
6
+ "type": "module",
7
+ "main": "dist/server/index.js",
8
+ "scripts": {
9
+ "start": "node .",
10
+ "start:test": "COOKIES_SECURE=no node .",
11
+ "test": "npm run test:unit",
12
+ "test:ci": "npm test && npm run test:functional:ci",
13
+ "test:functional": "cypress run ${CYPRESS_PROJECT_ID:+--record ${CYPRESS_FLAGS}}",
14
+ "test:functional:ci": "start-server-and-test 'start:test' 'http-get://localhost:8080/readiness' test:functional",
15
+ "test:unit": "jest --passWithNoTests",
16
+ "build": "npm run build:app && npm run build:server",
17
+ "build:app": "react-router build",
18
+ "build:server": "vite -c vite.config.server.ts build",
19
+ "heroku-postbuild": "npm run build",
20
+ "clean": "rm -rf dist/ pkg/ node_modules/.vite/ node_modules/.vite-temp/ .react-router/",
21
+ "dev": "NODE_ENV=development bun --watch --inspect ./src/server/dev.ts",
22
+ "cypress": "cypress open",
23
+ "create": "plop",
24
+ "create:deployment": "plop deployment",
25
+ "create:page": "plop page",
26
+ "typecheck": "react-router typegen && tsc"
27
+ },
28
+ "author": "{{{pkg 'author'}}}",
29
+ "license": "{{{pkg 'license'}}}",
30
+ "dependencies": {
31
+ "@react-foundry/fastify": "{{#if (eq (pkg 'name')'react-foundry')}}workspace:*{{else}}0.1.0{{/if}}",
32
+ "@react-foundry/fastify-react-router": "{{#if (eq (pkg 'name')'react-foundry')}}workspace:*{{else}}0.1.0{{/if}}",
33
+ "@react-foundry/react-router-context": "{{#if (eq (pkg 'name')'react-foundry')}}workspace:*{{else}}0.1.0{{/if}}",
34
+ "@react-foundry/router": "{{#if (eq (pkg 'name')'react-foundry')}}workspace:*{{else}}0.1.0{{/if}}",
35
+ "@mdx-js/rollup": "3.1.1",
36
+ "@react-router/fs-routes": "7.12.0",
37
+ "@react-router/node": "7.12.0",
38
+ "isbot": "5.1.32",
39
+ "react": "19.2.3",
40
+ "react-dom": "19.2.3",
41
+ "react-router": "7.12.0",
42
+ "serverless-http": "4.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@react-foundry/app-plop-pack": "{{#if (eq (pkg 'name')'react-foundry')}}workspace:*{{else}}0.1.0{{/if}}",
46
+ "@react-foundry/vite-html-react": "{{#if (eq (pkg 'name')'react-foundry')}}workspace:*{{else}}0.1.0{{/if}}",
47
+ "@react-router/dev": "7.12.0",
48
+ "@types/mdx": "2.0.13",
49
+ "@types/node": "24.10.9",
50
+ "@types/react": "19.2.8",
51
+ "@types/react-dom": "19.2.3",
52
+ "bun": "1.3.6",
53
+ "cypress": "15.9.0",
54
+ "jest": "30.2.0",
55
+ "jest-environment-jsdom": "30.2.0",
56
+ "start-server-and-test": "2.1.3",
57
+ "ts-jest": "29.4.6",
58
+ "typescript": "5.9.3",
59
+ "vite": "7.3.1"
60
+ }
61
+ }
@@ -0,0 +1,5 @@
1
+ export const plopFunction = async (plop) => {
2
+ await plop.load('@react-foundry/app-plop-pack');
3
+ };
4
+
5
+ export default plopFunction;
Binary file
@@ -0,0 +1,13 @@
1
+ import type { Config } from "@react-router/dev/config";
2
+
3
+ export default {
4
+ appDirectory: 'src/app',
5
+ buildDirectory: 'dist/app',
6
+ future: {
7
+ v8_middleware: true
8
+ },
9
+ routeDiscovery: {
10
+ mode: 'initial'
11
+ },
12
+ ssr: true,
13
+ } satisfies Config;
File without changes
@@ -0,0 +1 @@
1
+ export const siteTitle: string = '{{{ title }}}';
@@ -0,0 +1,17 @@
1
+ import { startTransition, StrictMode } from 'react';
2
+ import { hydrateRoot } from 'react-dom/client';
3
+ import { HydratedRouter } from 'react-router/dom';
4
+
5
+ startTransition(() => {
6
+ hydrateRoot(
7
+ document,
8
+ <StrictMode>
9
+ <HydratedRouter />
10
+ </StrictMode>,
11
+ {
12
+ onRecoverableError: (error, errorInfo) => {
13
+ console.warn(error, 'Component Stack:', errorInfo.componentStack);
14
+ }
15
+ }
16
+ );
17
+ });
@@ -0,0 +1,85 @@
1
+ import { PassThrough } from "node:stream";
2
+
3
+ import type { EntryContext } from "react-router";
4
+ import { createReadableStreamFromReadable } from "@react-router/node";
5
+ import { ServerRouter } from "react-router";
6
+ import { isbot } from "isbot";
7
+ import type { RenderToPipeableStreamOptions } from "react-dom/server";
8
+ import { renderToPipeableStream } from "react-dom/server";
9
+ import type { Request } from "@react-foundry/fastify-react-router";
10
+ import type { RouterContextProvider } from "@react-foundry/react-router-context";
11
+ import { cspNonceContext } from "@react-foundry/react-router-context";
12
+
13
+ export const streamTimeout = 5_000;
14
+ const isServerless = process.env['MODE'] === 'serverless';
15
+
16
+ export default function handleRequest(
17
+ request: Request,
18
+ responseStatusCode: number,
19
+ responseHeaders: Headers,
20
+ routerContext: EntryContext,
21
+ loadContext: RouterContextProvider
22
+ ) {
23
+ const nonce = request.cspNonce || loadContext.cspNonce || loadContext.get(cspNonceContext);
24
+
25
+ return new Promise((resolve, reject) => {
26
+ let shellRendered = false;
27
+ let userAgent = request.headers.get("user-agent");
28
+
29
+ // Ensure requests from bots and SPA Mode renders wait for all content to load before responding
30
+ // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
31
+ let readyOption: keyof RenderToPipeableStreamOptions =
32
+ (userAgent && isbot(userAgent)) || routerContext.isSpaMode || isServerless
33
+ ? "onAllReady"
34
+ : "onShellReady";
35
+
36
+ // Abort the rendering stream after the `streamTimeout` so it has time to
37
+ // flush down the rejected boundaries
38
+ let timeoutId: ReturnType<typeof setTimeout> | undefined = setTimeout(
39
+ () => abort(),
40
+ streamTimeout + 1000,
41
+ );
42
+
43
+ const { pipe, abort } = renderToPipeableStream(
44
+ <ServerRouter context={routerContext} url={request.url} nonce={nonce} />,
45
+ {
46
+ [readyOption]() {
47
+ shellRendered = true;
48
+ const body = new PassThrough({
49
+ final(callback) {
50
+ // Clear the timeout to prevent retaining the closure and memory leak
51
+ clearTimeout(timeoutId);
52
+ timeoutId = undefined;
53
+ callback();
54
+ },
55
+ });
56
+ const stream = createReadableStreamFromReadable(body);
57
+
58
+ responseHeaders.set("Content-Type", "text/html");
59
+
60
+ pipe(body);
61
+
62
+ resolve(
63
+ new Response(stream, {
64
+ headers: responseHeaders,
65
+ status: responseStatusCode,
66
+ }),
67
+ );
68
+ },
69
+ nonce,
70
+ onShellError(error: unknown) {
71
+ reject(error);
72
+ },
73
+ onError(error: unknown) {
74
+ responseStatusCode = 500;
75
+ // Log streaming rendering errors from inside the shell. Don't log
76
+ // errors encountered during initial shell rendering since they'll
77
+ // reject and get logged in handleDocumentRequest.
78
+ if (shellRendered) {
79
+ console.error(error);
80
+ }
81
+ },
82
+ },
83
+ );
84
+ });
85
+ }