deploy.sh 1.0.0 → 3.0.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 (161) hide show
  1. package/.claude/settings.local.json +36 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.yml +105 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
  5. package/.github/workflows/ci.yml +29 -0
  6. package/.github/workflows/pages.yml +48 -0
  7. package/.oxfmtrc.json +7 -0
  8. package/.oxlintrc.json +11 -0
  9. package/LICENSE +183 -183
  10. package/README.md +99 -11
  11. package/app/actions/deployments.ts +82 -0
  12. package/app/actions/metrics.ts +13 -0
  13. package/app/root.tsx +60 -0
  14. package/app/routes/dashboard/detail/history.tsx +73 -0
  15. package/app/routes/dashboard/detail/layout.tsx +125 -0
  16. package/app/routes/dashboard/detail/logs.tsx +85 -0
  17. package/app/routes/dashboard/detail/overview.tsx +119 -0
  18. package/app/routes/dashboard/detail/requests.tsx +163 -0
  19. package/app/routes/dashboard/detail/resources.tsx +268 -0
  20. package/app/routes/dashboard/detail/shared.tsx +59 -0
  21. package/app/routes/dashboard/index.tsx +360 -0
  22. package/app/routes/dashboard/layout.tsx +30 -0
  23. package/app/routes/docs/architecture.tsx +155 -0
  24. package/app/routes/docs/cli.tsx +122 -0
  25. package/app/routes/docs/deploying.tsx +105 -0
  26. package/app/routes/docs/index.tsx +104 -0
  27. package/app/routes/docs/layout.tsx +58 -0
  28. package/app/routes/home.tsx +134 -0
  29. package/app/routes/root.client.tsx +46 -0
  30. package/app/routes.ts +21 -0
  31. package/app/styles.css +15 -0
  32. package/app/theme.css +134 -0
  33. package/bin/deploy.js +360 -110
  34. package/docs-site/404.html +33 -0
  35. package/docs-site/home.tsx +130 -0
  36. package/docs-site/index.html +35 -0
  37. package/docs-site/layout.tsx +57 -0
  38. package/docs-site/main.tsx +41 -0
  39. package/docs-site/shell.tsx +34 -0
  40. package/docs-site/styles.css +4 -0
  41. package/drizzle.config.js +8 -0
  42. package/examples/docker/Dockerfile +5 -0
  43. package/examples/docker/server.js +18 -0
  44. package/examples/node/package.json +7 -0
  45. package/examples/node/pnpm-lock.yaml +9 -0
  46. package/examples/node/server.js +12 -0
  47. package/examples/static/index.html +48 -0
  48. package/package.json +41 -55
  49. package/public/favicon.ico +0 -0
  50. package/react-router-vite/entry.browser.tsx +49 -0
  51. package/react-router-vite/entry.rsc.single.tsx +7 -0
  52. package/react-router-vite/entry.rsc.tsx +36 -0
  53. package/react-router-vite/entry.ssr.tsx +29 -0
  54. package/react-router-vite/plugin.ts +114 -0
  55. package/react-router-vite/types.d.ts +11 -0
  56. package/react-router.config.ts +5 -0
  57. package/server/api.test.ts +344 -0
  58. package/server/api.ts +445 -0
  59. package/server/docker.ts +268 -0
  60. package/server/index.ts +17 -0
  61. package/server/metrics-collector.ts +29 -0
  62. package/server/schema.ts +56 -0
  63. package/server/store.test.ts +278 -0
  64. package/server/store.ts +398 -0
  65. package/tsconfig.json +21 -0
  66. package/vite.config.ts +45 -0
  67. package/vite.docs.config.ts +31 -0
  68. package/.eslintignore +0 -5
  69. package/.eslintrc +0 -23
  70. package/.travis.yml +0 -9
  71. package/.tryitout +0 -48
  72. package/CHANGELOG.md +0 -56
  73. package/bin/deploy-delete.js +0 -11
  74. package/bin/deploy-deploy.js +0 -31
  75. package/bin/deploy-list.js +0 -32
  76. package/bin/deploy-login.js +0 -39
  77. package/bin/deploy-logout.js +0 -12
  78. package/bin/deploy-logs.js +0 -19
  79. package/bin/deploy-open.js +0 -19
  80. package/bin/deploy-register.js +0 -40
  81. package/bin/deploy-server.js +0 -5
  82. package/bin/deploy-whoami.js +0 -12
  83. package/docs/code/CLI.html +0 -2901
  84. package/docs/code/Deployment.html +0 -2469
  85. package/docs/code/Request.html +0 -906
  86. package/docs/code/User.html +0 -1219
  87. package/docs/code/classifier.js.html +0 -121
  88. package/docs/code/deploy.js.html +0 -122
  89. package/docs/code/fonts/OpenSans-Bold-webfont.eot +0 -0
  90. package/docs/code/fonts/OpenSans-Bold-webfont.svg +0 -1830
  91. package/docs/code/fonts/OpenSans-Bold-webfont.woff +0 -0
  92. package/docs/code/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  93. package/docs/code/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
  94. package/docs/code/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  95. package/docs/code/fonts/OpenSans-Italic-webfont.eot +0 -0
  96. package/docs/code/fonts/OpenSans-Italic-webfont.svg +0 -1830
  97. package/docs/code/fonts/OpenSans-Italic-webfont.woff +0 -0
  98. package/docs/code/fonts/OpenSans-Light-webfont.eot +0 -0
  99. package/docs/code/fonts/OpenSans-Light-webfont.svg +0 -1831
  100. package/docs/code/fonts/OpenSans-Light-webfont.woff +0 -0
  101. package/docs/code/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  102. package/docs/code/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
  103. package/docs/code/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  104. package/docs/code/fonts/OpenSans-Regular-webfont.eot +0 -0
  105. package/docs/code/fonts/OpenSans-Regular-webfont.svg +0 -1831
  106. package/docs/code/fonts/OpenSans-Regular-webfont.woff +0 -0
  107. package/docs/code/fonts/OpenSans-Semibold-webfont.eot +0 -0
  108. package/docs/code/fonts/OpenSans-Semibold-webfont.svg +0 -1830
  109. package/docs/code/fonts/OpenSans-Semibold-webfont.ttf +0 -0
  110. package/docs/code/fonts/OpenSans-Semibold-webfont.woff +0 -0
  111. package/docs/code/fonts/OpenSans-SemiboldItalic-webfont.eot +0 -0
  112. package/docs/code/fonts/OpenSans-SemiboldItalic-webfont.svg +0 -1830
  113. package/docs/code/fonts/OpenSans-SemiboldItalic-webfont.ttf +0 -0
  114. package/docs/code/fonts/OpenSans-SemiboldItalic-webfont.woff +0 -0
  115. package/docs/code/helpers_cli.js.html +0 -315
  116. package/docs/code/helpers_util.js.html +0 -194
  117. package/docs/code/index.html +0 -66
  118. package/docs/code/models_deployment.js.html +0 -515
  119. package/docs/code/models_request.js.html +0 -158
  120. package/docs/code/models_user.js.html +0 -198
  121. package/docs/code/module-lib_classifier.html +0 -246
  122. package/docs/code/module-lib_deploy.html +0 -350
  123. package/docs/code/module-lib_helpers_util.html +0 -707
  124. package/docs/code/scripts/linenumber.js +0 -25
  125. package/docs/code/scripts/prettify/Apache-License-2.0.txt +0 -202
  126. package/docs/code/scripts/prettify/lang-css.js +0 -2
  127. package/docs/code/scripts/prettify/prettify.js +0 -28
  128. package/docs/code/styles/jsdoc-default.css +0 -692
  129. package/docs/code/styles/prettify-jsdoc.css +0 -111
  130. package/docs/code/styles/prettify-tomorrow.css +0 -132
  131. package/docs/example.gif +0 -0
  132. package/docs/example.mov +0 -0
  133. package/docs/index.html +0 -4463
  134. package/docs/logo.png +0 -0
  135. package/docs/logo.pxm +0 -0
  136. package/docs/logo@2x.png +0 -0
  137. package/docs/main.css +0 -162
  138. package/docs/main.js +0 -53
  139. package/index.js +0 -55
  140. package/jsdoc.json +0 -27
  141. package/lib/classifier.js +0 -61
  142. package/lib/deploy.js +0 -62
  143. package/lib/helpers/cli.js +0 -255
  144. package/lib/helpers/util.js +0 -134
  145. package/lib/models/deployment.js +0 -455
  146. package/lib/models/request.js +0 -98
  147. package/lib/models/user.js +0 -138
  148. package/lib/server.js +0 -165
  149. package/lib/static/not-found.html +0 -30
  150. package/lib/static/page-could-not-load.html +0 -30
  151. package/lib/static/static-server.js +0 -69
  152. package/test/fixtures/docker/Dockerfile +0 -5
  153. package/test/fixtures/docker/index.js +0 -12
  154. package/test/fixtures/node/index.js +0 -8
  155. package/test/fixtures/node/package.json +0 -15
  156. package/test/fixtures/static/index.html +0 -14
  157. package/test/fixtures/static/main.css +0 -7
  158. package/test/fixtures/static/out.gifcd +0 -0
  159. package/test/fixtures/unknown/.gitkeep +0 -0
  160. package/test/lib/classifier.js +0 -51
  161. package/test/lib/helpers/util.js +0 -47
@@ -0,0 +1,35 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="description" content="deploy.sh documentation — self-hosted deployment platform." />
7
+ <link rel="icon" href="/favicon.ico" />
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link
11
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
12
+ rel="stylesheet"
13
+ />
14
+ <title>deploy.sh docs</title>
15
+ <!-- GitHub Pages SPA redirect handler -->
16
+ <script>
17
+ (function (l) {
18
+ if (l.search[1] === '/') {
19
+ var decoded = l.search
20
+ .slice(1)
21
+ .split('&')
22
+ .map(function (s) {
23
+ return s.replace(/~and~/g, '&');
24
+ })
25
+ .join('?');
26
+ window.history.replaceState(null, null, l.pathname.slice(0, -1) + decoded + l.hash);
27
+ }
28
+ })(window.location);
29
+ </script>
30
+ </head>
31
+ <body>
32
+ <div id="root"></div>
33
+ <script type="module" src="./main.tsx"></script>
34
+ </body>
35
+ </html>
@@ -0,0 +1,57 @@
1
+ import { Link, Outlet, useLocation } from 'react-router';
2
+
3
+ const NAV = [
4
+ {
5
+ heading: 'Getting Started',
6
+ links: [
7
+ { to: '/docs', label: 'Introduction' },
8
+ { to: '/docs/deploying', label: 'Deploying Apps' },
9
+ ],
10
+ },
11
+ {
12
+ heading: 'Reference',
13
+ links: [
14
+ { to: '/docs/cli', label: 'CLI' },
15
+ { to: '/docs/architecture', label: 'Architecture' },
16
+ ],
17
+ },
18
+ ];
19
+
20
+ export default function DocsLayout() {
21
+ const { pathname } = useLocation();
22
+
23
+ return (
24
+ <div className="max-w-7xl mx-auto px-6 flex gap-10 py-8">
25
+ <aside className="w-48 shrink-0 max-md:hidden">
26
+ <nav className="sticky top-8">
27
+ {NAV.map((section) => (
28
+ <div key={section.heading}>
29
+ <p className="text-xs font-semibold text-text-tertiary uppercase tracking-wider mb-3">
30
+ {section.heading}
31
+ </p>
32
+ <ul className="flex flex-col gap-1 mb-6 list-none p-0">
33
+ {section.links.map((link) => (
34
+ <li key={link.to}>
35
+ <Link
36
+ to={link.to}
37
+ className={`block text-sm px-2 py-1 rounded-md transition-colors no-underline ${
38
+ pathname === link.to
39
+ ? 'text-text bg-bg-hover'
40
+ : 'text-text-secondary hover:text-text hover:bg-bg-hover'
41
+ }`}
42
+ >
43
+ {link.label}
44
+ </Link>
45
+ </li>
46
+ ))}
47
+ </ul>
48
+ </div>
49
+ ))}
50
+ </nav>
51
+ </aside>
52
+ <main className="flex-1 min-w-0">
53
+ <Outlet />
54
+ </main>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,41 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { createBrowserRouter, Navigate, RouterProvider } from 'react-router';
4
+ import './styles.css';
5
+
6
+ import Shell from './shell';
7
+ import Home from './home';
8
+ import DocsLayout from './layout';
9
+ import GettingStarted from '../app/routes/docs/index';
10
+ import Deploying from '../app/routes/docs/deploying';
11
+ import Cli from '../app/routes/docs/cli';
12
+ import Architecture from '../app/routes/docs/architecture';
13
+
14
+ const router = createBrowserRouter(
15
+ [
16
+ {
17
+ Component: Shell,
18
+ children: [
19
+ { index: true, Component: Home },
20
+ {
21
+ path: 'docs',
22
+ Component: DocsLayout,
23
+ children: [
24
+ { index: true, Component: GettingStarted },
25
+ { path: 'deploying', Component: Deploying },
26
+ { path: 'cli', Component: Cli },
27
+ { path: 'architecture', Component: Architecture },
28
+ ],
29
+ },
30
+ { path: '*', element: <Navigate to="/" replace /> },
31
+ ],
32
+ },
33
+ ],
34
+ { basename: '/deploy.sh' },
35
+ );
36
+
37
+ createRoot(document.getElementById('root')!).render(
38
+ <StrictMode>
39
+ <RouterProvider router={router} />
40
+ </StrictMode>,
41
+ );
@@ -0,0 +1,34 @@
1
+ import { Link, Outlet } from 'react-router';
2
+
3
+ export default function Shell() {
4
+ return (
5
+ <>
6
+ <header className="border-b border-border">
7
+ <div className="max-w-7xl mx-auto px-6 h-14 flex items-center justify-between">
8
+ <div className="flex items-center gap-8">
9
+ <Link to="/" className="text-sm font-semibold tracking-tight text-text no-underline">
10
+ deploy.sh
11
+ </Link>
12
+ <nav className="flex items-center gap-6">
13
+ <Link
14
+ to="/docs"
15
+ className="text-sm text-text-secondary hover:text-text transition-colors no-underline"
16
+ >
17
+ Docs
18
+ </Link>
19
+ <a
20
+ href="https://github.com/gabrielcsapo/deploy.sh"
21
+ className="text-sm text-text-secondary hover:text-text transition-colors no-underline"
22
+ target="_blank"
23
+ rel="noopener noreferrer"
24
+ >
25
+ GitHub
26
+ </a>
27
+ </nav>
28
+ </div>
29
+ </div>
30
+ </header>
31
+ <Outlet />
32
+ </>
33
+ );
34
+ }
@@ -0,0 +1,4 @@
1
+ @import 'tailwindcss';
2
+ @source '../app';
3
+ @plugin '@tailwindcss/typography';
4
+ @import '../app/theme.css';
@@ -0,0 +1,8 @@
1
+ export default {
2
+ schema: './server/schema.js',
3
+ out: './drizzle',
4
+ dialect: 'sqlite',
5
+ dbCredentials: {
6
+ url: '.deploy-data/deploy.db',
7
+ },
8
+ };
@@ -0,0 +1,5 @@
1
+ FROM node:22-alpine
2
+ WORKDIR /app
3
+ COPY server.js .
4
+ EXPOSE 3000
5
+ CMD ["node", "server.js"]
@@ -0,0 +1,18 @@
1
+ import { createServer } from 'node:http';
2
+
3
+ const port = process.env.PORT || 3000;
4
+
5
+ const server = createServer((req, res) => {
6
+ res.writeHead(200, { 'Content-Type': 'application/json' });
7
+ res.end(
8
+ JSON.stringify({
9
+ message: 'Hello from a Docker container!',
10
+ hostname: process.env.HOSTNAME,
11
+ uptime: process.uptime(),
12
+ }),
13
+ );
14
+ });
15
+
16
+ server.listen(port, () => {
17
+ console.log(`Listening on port ${port}`);
18
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "example-node",
3
+ "private": true,
4
+ "scripts": {
5
+ "start": "node server.js"
6
+ }
7
+ }
@@ -0,0 +1,9 @@
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ importers:
8
+
9
+ .: {}
@@ -0,0 +1,12 @@
1
+ import { createServer } from 'node:http';
2
+
3
+ const port = process.env.PORT || 3000;
4
+
5
+ const server = createServer((req, res) => {
6
+ res.writeHead(200, { 'Content-Type': 'application/json' });
7
+ res.end(JSON.stringify({ message: 'Hello from deploy.sh!', uptime: process.uptime() }));
8
+ });
9
+
10
+ server.listen(port, () => {
11
+ console.log(`Listening on port ${port}`);
12
+ });
@@ -0,0 +1,48 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Static Site Example</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+ body {
14
+ font-family:
15
+ system-ui,
16
+ -apple-system,
17
+ sans-serif;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ min-height: 100vh;
22
+ background: #13111c;
23
+ color: #fafafa;
24
+ }
25
+ .card {
26
+ text-align: center;
27
+ padding: 3rem;
28
+ border: 1px solid #2a2540;
29
+ border-radius: 12px;
30
+ background: #1a1726;
31
+ }
32
+ h1 {
33
+ font-size: 1.5rem;
34
+ margin-bottom: 0.5rem;
35
+ }
36
+ p {
37
+ color: #8b82a8;
38
+ font-size: 0.875rem;
39
+ }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ <div class="card">
44
+ <h1>Hello from deploy.sh</h1>
45
+ <p>This is a static site deployed with deploy.sh.</p>
46
+ </div>
47
+ </body>
48
+ </html>
package/package.json CHANGED
@@ -1,66 +1,52 @@
1
1
  {
2
2
  "name": "deploy.sh",
3
- "version": "1.0.0",
4
- "description": "☁️ open source continuous deployment service",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "tap test/index.js test/lib",
8
- "coverage": "tap test/index.js test/lib --coverage --coverage-report=lcov",
9
- "lint": "eslint .",
10
- "start": "./bin/deploy serve",
11
- "pack": "pkg bin/deploy.js -c package.json -o packed/deploy",
12
- "generate-docs": "tryitout && jsdoc -c jsdoc.json"
13
- },
14
- "pkg": {
15
- "scripts": [
16
- "bin/*",
17
- "lib/**/*.js"
18
- ],
19
- "assets": [
20
- "lib/static/*"
21
- ],
22
- "targets": [
23
- "node8-alpine-x64",
24
- "node8-linux-x64",
25
- "node8-macos-x64",
26
- "node8-win-x64"
27
- ]
28
- },
29
- "repository": {
30
- "type": "git",
31
- "url": "git+https://github.com/gabrielcsapo/deploy.sh.git"
32
- },
3
+ "version": "3.0.0",
33
4
  "bin": {
34
5
  "deploy": "./bin/deploy.js"
35
6
  },
36
- "engines": {
37
- "node": ">= 8"
7
+ "type": "module",
8
+ "scripts": {
9
+ "dev": "vite",
10
+ "server": "node server/index.ts",
11
+ "build": "vite build",
12
+ "preview": "vite preview",
13
+ "lint": "oxlint .",
14
+ "format": "oxfmt --write .",
15
+ "format:check": "oxfmt --check .",
16
+ "test": "node --test server/**/*.test.ts",
17
+ "typecheck": "tsc --noEmit",
18
+ "dev:docs": "vite --config vite.docs.config.ts",
19
+ "build:docs": "vite build --config vite.docs.config.ts",
20
+ "preview:docs": "vite preview --config vite.docs.config.ts"
38
21
  },
39
- "homepage": "https://www.gabrielcsapo.com/deploy.sh",
40
22
  "dependencies": {
41
- "body-parser": "^1.17.2",
42
- "commander": "^2.11.0",
43
- "dockerode": "^2.5.0",
44
- "express": "^4.15.4",
45
- "formidable": "^1.1.1",
46
- "inquirer": "^3.2.1",
47
- "moment": "^2.18.1",
48
- "mongoose": "^4.11.6",
49
- "opn": "^5.1.0",
50
- "ora": "^1.3.0",
51
- "request": "^2.81.0",
52
- "tar": "^4.3.0",
53
- "turtler": "^1.0.2",
54
- "update-notifier": "^2.2.0",
55
- "woof": "^0.2.1"
23
+ "@react-router/dev": "7.13.0",
24
+ "better-sqlite3": "^12.6.2",
25
+ "drizzle-orm": "^0.45.1",
26
+ "react": "^19.2.4",
27
+ "react-dom": "^19.2.4",
28
+ "react-router": "7.13.0",
29
+ "vite": "^7.3.1"
56
30
  },
57
31
  "devDependencies": {
58
- "eslint": "^4.4.1",
59
- "jsdoc": "^3.5.4",
60
- "minami": "^1.2.3",
61
- "pkg": "^4.3.0",
62
- "tap": "^10.7.1",
63
- "tape": "^4.8.0",
64
- "tryitout": "^2.0.7"
32
+ "@tailwindcss/typography": "^0.5.19",
33
+ "@tailwindcss/vite": "^4.1.18",
34
+ "@types/better-sqlite3": "^7.6.13",
35
+ "@types/node": "^25.2.2",
36
+ "@types/react": "^19.2.13",
37
+ "@types/react-dom": "^19.2.3",
38
+ "@vitejs/plugin-react": "5.1.3",
39
+ "@vitejs/plugin-rsc": "0.5.19",
40
+ "drizzle-kit": "^0.31.8",
41
+ "oxfmt": "0.28.0",
42
+ "oxlint": "1.43.0",
43
+ "tailwindcss": "^4.1.18",
44
+ "typescript": "^5.9.3"
45
+ },
46
+ "pnpm": {
47
+ "onlyBuiltDependencies": [
48
+ "better-sqlite3",
49
+ "esbuild"
50
+ ]
65
51
  }
66
52
  }
Binary file
@@ -0,0 +1,49 @@
1
+ import {
2
+ createFromReadableStream,
3
+ createTemporaryReferenceSet,
4
+ encodeReply,
5
+ setServerCallback,
6
+ } from '@vitejs/plugin-rsc/browser';
7
+ import { startTransition, StrictMode } from 'react';
8
+ import { hydrateRoot } from 'react-dom/client';
9
+ import { type DataRouter, type unstable_RSCPayload as RSCServerPayload } from 'react-router';
10
+ import {
11
+ unstable_createCallServer as createCallServer,
12
+ unstable_getRSCStream as getRSCStream,
13
+ unstable_RSCHydratedRouter as RSCHydratedRouter,
14
+ } from 'react-router/dom';
15
+
16
+ // Create and set the callServer function to support post-hydration server actions.
17
+ setServerCallback(
18
+ createCallServer({
19
+ createFromReadableStream,
20
+ createTemporaryReferenceSet,
21
+ encodeReply,
22
+ }),
23
+ );
24
+
25
+ // Get and decode the initial server payload
26
+ createFromReadableStream<RSCServerPayload>(getRSCStream()).then((payload) => {
27
+ startTransition(async () => {
28
+ const formState = payload.type === 'render' ? await payload.formState : undefined;
29
+
30
+ hydrateRoot(
31
+ document,
32
+ <StrictMode>
33
+ <RSCHydratedRouter createFromReadableStream={createFromReadableStream} payload={payload} />
34
+ </StrictMode>,
35
+ {
36
+ // @ts-expect-error - no types for this yet
37
+ formState,
38
+ },
39
+ );
40
+ });
41
+ });
42
+
43
+ declare let __reactRouterDataRouter: DataRouter;
44
+
45
+ if (import.meta.hot) {
46
+ import.meta.hot.on('rsc:update', () => {
47
+ __reactRouterDataRouter.revalidate();
48
+ });
49
+ }
@@ -0,0 +1,7 @@
1
+ import { fetchServer } from './entry.rsc';
2
+
3
+ export default async function handler(request: Request) {
4
+ const ssr = await import.meta.viteRsc.loadModule<typeof import('./entry.ssr')>('ssr', 'index');
5
+
6
+ return ssr.default(request, await fetchServer(request));
7
+ }
@@ -0,0 +1,36 @@
1
+ import {
2
+ createTemporaryReferenceSet,
3
+ decodeAction,
4
+ decodeFormState,
5
+ decodeReply,
6
+ loadServerAction,
7
+ renderToReadableStream,
8
+ } from '@vitejs/plugin-rsc/rsc';
9
+ import { unstable_matchRSCServerRequest as matchRSCServerRequest } from 'react-router';
10
+ import routes from 'virtual:react-router-routes';
11
+
12
+ export function fetchServer(request: Request) {
13
+ return matchRSCServerRequest({
14
+ // Provide the React Server touchpoints.
15
+ createTemporaryReferenceSet,
16
+ decodeAction,
17
+ decodeFormState,
18
+ decodeReply,
19
+ loadServerAction,
20
+ // The incoming request.
21
+ request,
22
+ // The app routes.
23
+ routes,
24
+ // Encode the match with the React Server implementation.
25
+ generateResponse(match, options) {
26
+ return new Response(renderToReadableStream(match.payload, options), {
27
+ status: match.statusCode,
28
+ headers: match.headers,
29
+ });
30
+ },
31
+ });
32
+ }
33
+
34
+ if (import.meta.hot) {
35
+ import.meta.hot.accept();
36
+ }
@@ -0,0 +1,29 @@
1
+ import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr';
2
+ import { renderToReadableStream as renderHTMLToReadableStream } from 'react-dom/server.edge';
3
+ import {
4
+ unstable_routeRSCServerRequest as routeRSCServerRequest,
5
+ unstable_RSCStaticRouter as RSCStaticRouter,
6
+ } from 'react-router';
7
+
8
+ export default async function handler(
9
+ request: Request,
10
+ serverResponse: Response,
11
+ ): Promise<Response> {
12
+ const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent('index');
13
+
14
+ return await routeRSCServerRequest({
15
+ request,
16
+ serverResponse,
17
+ createFromReadableStream,
18
+ async renderHTML(getPayload, options) {
19
+ const payload = getPayload();
20
+
21
+ return await renderHTMLToReadableStream(<RSCStaticRouter getPayload={getPayload} />, {
22
+ ...options,
23
+ bootstrapScriptContent,
24
+ signal: request.signal,
25
+ formState: await payload.formState,
26
+ });
27
+ },
28
+ });
29
+ }
@@ -0,0 +1,114 @@
1
+ import assert from 'node:assert/strict';
2
+ import path from 'node:path';
3
+ import type { Config } from '@react-router/dev/config';
4
+ import type { RouteConfigEntry } from '@react-router/dev/routes';
5
+ import { type Plugin, createIdResolver, runnerImport } from 'vite';
6
+
7
+ export function reactRouter(): Plugin[] {
8
+ let idResolver: ReturnType<typeof createIdResolver>;
9
+
10
+ return [
11
+ {
12
+ name: 'deploy:api',
13
+ async configureServer(server) {
14
+ const { apiMiddleware } = await import(/* @vite-ignore */ path.resolve('server/api.ts'));
15
+ server.middlewares.use(apiMiddleware());
16
+ },
17
+ },
18
+ {
19
+ name: 'react-router:config',
20
+ configResolved(config) {
21
+ idResolver = createIdResolver(config);
22
+ },
23
+ resolveId(source) {
24
+ if (source === 'virtual:react-router-routes') {
25
+ return '\0' + source;
26
+ }
27
+ },
28
+ async load(id) {
29
+ if (id === '\0virtual:react-router-routes') {
30
+ const findFile = (id: string) => idResolver(this.environment, id);
31
+ const config = await readReactRouterConfig(findFile);
32
+ this.addWatchFile(config.configFile);
33
+ this.addWatchFile(config.routesFile);
34
+ const code = generateRoutesCode(config);
35
+ return code;
36
+ }
37
+ },
38
+ },
39
+ ];
40
+ }
41
+
42
+ async function readReactRouterConfig(findFile: (id: string) => Promise<string | undefined>) {
43
+ // find react-router.config.ts
44
+ const configFile = await findFile('./react-router.config');
45
+ assert(configFile, "Cannot find 'react-router.config' file");
46
+ const configImport = await runnerImport<{ default: Config }>(configFile!);
47
+ const appDirectory = path.resolve(configImport.module.default.appDirectory ?? 'app');
48
+
49
+ // find routes.ts
50
+ const routesFile = await findFile(path.join(appDirectory, 'routes'));
51
+ assert(routesFile, "Cannot find 'routes' file");
52
+ const routesImport = await runnerImport<{
53
+ default: RouteConfigEntry[];
54
+ }>(routesFile!);
55
+
56
+ // find root.tsx
57
+ const rootFile = await findFile(path.join(appDirectory, 'root'));
58
+ assert(rootFile, "Cannot find 'root' file");
59
+
60
+ const routes: RouteConfigEntry[] = [
61
+ {
62
+ id: 'root',
63
+ path: '',
64
+ file: rootFile!,
65
+ children: routesImport.module.default,
66
+ },
67
+ ];
68
+
69
+ return { configFile, routesFile, appDirectory, routes };
70
+ }
71
+
72
+ // copied from
73
+ // https://github.com/jacob-ebey/parcel-plugin-react-router/blob/9385be813534537dfb0fe640a3e5c5607be3b61d/packages/resolver/src/resolver.ts
74
+
75
+ function generateRoutesCode(config: { appDirectory: string; routes: RouteConfigEntry[] }) {
76
+ let code = 'export default [';
77
+ const closeRouteSymbol = Symbol('CLOSE_ROUTE');
78
+ let stack: Array<typeof closeRouteSymbol | RouteConfigEntry> = [...config.routes];
79
+ while (stack.length > 0) {
80
+ const route = stack.pop();
81
+ if (!route) break;
82
+ if (route === closeRouteSymbol) {
83
+ code += ']},';
84
+ continue;
85
+ }
86
+ code += '{';
87
+ // TODO: route-module transform
88
+ code += `lazy: () => import(${JSON.stringify(path.resolve(config.appDirectory, route.file))}),`;
89
+ code += `id: ${JSON.stringify(route.id || createRouteId(route.file, config.appDirectory))},`;
90
+ if (typeof route.path === 'string') {
91
+ code += `path: ${JSON.stringify(route.path)},`;
92
+ }
93
+ if (route.index) {
94
+ code += `index: true,`;
95
+ }
96
+ if (route.caseSensitive) {
97
+ code += `caseSensitive: true,`;
98
+ }
99
+ if (route.children) {
100
+ code += ['children:['];
101
+ stack.push(closeRouteSymbol);
102
+ stack.push(...[...route.children].reverse());
103
+ } else {
104
+ code += '},';
105
+ }
106
+ }
107
+ code += '];\n';
108
+
109
+ return code;
110
+ }
111
+
112
+ function createRouteId(file: string, appDirectory: string) {
113
+ return path.relative(appDirectory, file).replace(/\\+/, '/').slice(0, -path.extname(file).length);
114
+ }
@@ -0,0 +1,11 @@
1
+ /// <reference types="vite/client" />
2
+ /// <reference types="@vitejs/plugin-rsc/types" />
3
+
4
+ declare module 'react-dom/server.edge' {
5
+ export * from 'react-dom/server';
6
+ }
7
+
8
+ declare module 'virtual:react-router-routes' {
9
+ const routes: any;
10
+ export default routes;
11
+ }
@@ -0,0 +1,5 @@
1
+ import type { Config } from '@react-router/dev/config';
2
+
3
+ export default {
4
+ appDirectory: './app',
5
+ } satisfies Config;