From 5c949e2da5a6e965740247664b16f40155bb3993 Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Mon, 4 Nov 2024 22:05:46 -0500 Subject: [PATCH] feat: use all native node deps for the server --- package.json | 6 +-- pnpm-lock.yaml | 76 +++++-------------------------- server.mjs | 119 ++++++++++++++++++++++++++++++++++++++++++------- vite.config.ts | 73 ++++++++++++++++++++++-------- 4 files changed, 170 insertions(+), 104 deletions(-) diff --git a/package.json b/package.json index 115e381..15b48cf 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "sideEffects": false, "type": "module", "scripts": { - "build": "remix vite:build", + "build": "remix vite:build && vite build", "dev": "remix vite:dev", "typecheck": "tsc" }, @@ -13,7 +13,6 @@ "@dnd-kit/modifiers": "^7.0.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", - "@hono/node-server": "^1.13.5", "@kubernetes/client-node": "^0.22.2", "@primer/octicons-react": "^19.12.0", "@react-aria/toast": "3.0.0-beta.12", @@ -25,15 +24,14 @@ "@uiw/react-codemirror": "^4.23.6", "clsx": "^2.1.1", "dotenv": "^16.4.5", - "hono": "^4.6.9", "isbot": "^5.1.17", + "mime": "^4.0.4", "oauth4webapi": "^2.17.0", "react": "19.0.0-beta-26f2496093-20240514", "react-aria-components": "^1.2.1", "react-codemirror-merge": "^4.23.6", "react-dom": "19.0.0-beta-26f2496093-20240514", "react-error-boundary": "^4.1.2", - "remix-hono": "^0.0.16", "remix-utils": "^7.7.0", "tailwind-merge": "^2.5.4", "tailwindcss-react-aria-components": "^1.1.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03eca9d..258ff0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,9 +28,6 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@19.0.0-beta-26f2496093-20240514) - '@hono/node-server': - specifier: ^1.13.5 - version: 1.13.5(hono@4.6.9) '@kubernetes/client-node': specifier: ^0.22.2 version: 0.22.2 @@ -64,12 +61,12 @@ importers: dotenv: specifier: ^16.4.5 version: 16.4.5 - hono: - specifier: ^4.6.9 - version: 4.6.9 isbot: specifier: ^5.1.17 version: 5.1.17 + mime: + specifier: ^4.0.4 + version: 4.0.4 oauth4webapi: specifier: ^2.17.0 version: 2.17.0 @@ -88,9 +85,6 @@ importers: react-error-boundary: specifier: ^4.1.2 version: 4.1.2(react@19.0.0-beta-26f2496093-20240514) - remix-hono: - specifier: ^0.0.16 - version: 0.0.16(typescript@5.6.3)(zod@3.23.8) remix-utils: specifier: ^7.7.0 version: 7.7.0(@remix-run/node@2.13.1(typescript@5.6.3))(@remix-run/react@2.13.1(react-dom@19.0.0-beta-26f2496093-20240514(react@19.0.0-beta-26f2496093-20240514))(react@19.0.0-beta-26f2496093-20240514)(typescript@5.6.3))(@remix-run/router@1.20.0)(react@19.0.0-beta-26f2496093-20240514)(zod@3.23.8) @@ -666,12 +660,6 @@ packages: '@formatjs/intl-localematcher@0.5.4': resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==} - '@hono/node-server@1.13.5': - resolution: {integrity: sha512-lSo+CFlLqAFB4fX7ePqI9nauEn64wOfJHAfc9duYFTvAG3o416pC0nTGeNjuLHchLedH+XyWda5v79CVx1PIjg==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4 - '@internationalized/date@3.5.6': resolution: {integrity: sha512-jLxQjefH9VI5P9UQuqB6qNKnvFt1Ky1TPIzHGsIlCi7sZZoMR8SdYbBGRvM0y+Jtb+ez4ieBzmiAUcpmPYpyOw==} @@ -2279,10 +2267,6 @@ packages: hast-util-whitespace@2.0.1: resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} - hono@4.6.9: - resolution: {integrity: sha512-p/pN5yZLuZaHzyAOT2nw2/Ud6HhJHYmDNGH6Ck1OWBhPMVeM1r74jbCRwNi0gyFRjjbsGgoHbOyj7mT1PDNbTw==} - engines: {node: '>=16.9.0'} - hosted-git-info@6.1.1: resolution: {integrity: sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -2737,6 +2721,11 @@ packages: engines: {node: '>=4'} hasBin: true + mime@4.0.4: + resolution: {integrity: sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==} + engines: {node: '>=16'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -3084,10 +3073,6 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - pretty-cache-header@1.0.0: - resolution: {integrity: sha512-xtXazslu25CdnGnUkByU1RoOjK55TqwatJkjjJLg5ZAdz2Lngko/mmaUgeET36P2GMlNwh3fdM7FWBO717pNcw==} - engines: {node: '>=12.13'} - pretty-format@24.9.0: resolution: {integrity: sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==} engines: {node: '>= 6'} @@ -3252,23 +3237,6 @@ packages: remark-rehype@10.1.0: resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} - remix-hono@0.0.16: - resolution: {integrity: sha512-IPooI2E0eSFRV9wAgTzpsklSoOyij6Nsk6ANIugGhD9vYlovbYb2BT+siz++sLzWseZSPEtIzx/LBel203jBfQ==} - peerDependencies: - '@remix-run/cloudflare': ^2.0.0 - i18next: ^23.0.0 - remix-i18next: ^6.0.0 - zod: ^3.0.0 - peerDependenciesMeta: - '@remix-run/cloudflare': - optional: true - i18next: - optional: true - remix-i18next: - optional: true - zod: - optional: true - remix-utils@7.7.0: resolution: {integrity: sha512-J8NhP044nrNIam/xOT1L9a4RQ9FSaA2wyrUwmN8ZT+c/+CdAAf70yfaLnvMyKcV5U+8BcURQ/aVbth77sT6jGA==} engines: {node: '>=18.0.0'} @@ -3562,10 +3530,6 @@ packages: through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} - timestring@6.0.0: - resolution: {integrity: sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA==} - engines: {node: '>=8'} - to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -4390,10 +4354,6 @@ snapshots: dependencies: tslib: 2.6.2 - '@hono/node-server@1.13.5(hono@4.6.9)': - dependencies: - hono: 4.6.9 - '@internationalized/date@3.5.6': dependencies: '@swc/helpers': 0.5.11 @@ -6645,8 +6605,6 @@ snapshots: hast-util-whitespace@2.0.1: {} - hono@4.6.9: {} - hosted-git-info@6.1.1: dependencies: lru-cache: 7.18.3 @@ -7230,6 +7188,8 @@ snapshots: mime@1.6.0: {} + mime@4.0.4: {} + mimic-fn@2.1.0: {} minimatch@9.0.5: @@ -7559,10 +7519,6 @@ snapshots: prettier@2.8.8: {} - pretty-cache-header@1.0.0: - dependencies: - timestring: 6.0.0 - pretty-format@24.9.0: dependencies: '@jest/types': 24.9.0 @@ -7833,16 +7789,6 @@ snapshots: mdast-util-to-hast: 12.3.0 unified: 10.1.2 - remix-hono@0.0.16(typescript@5.6.3)(zod@3.23.8): - dependencies: - '@remix-run/server-runtime': 2.13.1(typescript@5.6.3) - hono: 4.6.9 - pretty-cache-header: 1.0.0 - optionalDependencies: - zod: 3.23.8 - transitivePeerDependencies: - - typescript - remix-utils@7.7.0(@remix-run/node@2.13.1(typescript@5.6.3))(@remix-run/react@2.13.1(react-dom@19.0.0-beta-26f2496093-20240514(react@19.0.0-beta-26f2496093-20240514))(react@19.0.0-beta-26f2496093-20240514)(typescript@5.6.3))(@remix-run/router@1.20.0)(react@19.0.0-beta-26f2496093-20240514)(zod@3.23.8): dependencies: type-fest: 4.26.1 @@ -8199,8 +8145,6 @@ snapshots: readable-stream: 2.3.8 xtend: 4.0.2 - timestring@6.0.0: {} - to-fast-properties@2.0.0: {} to-regex-range@5.0.1: diff --git a/server.mjs b/server.mjs index b6e0af6..9173d47 100755 --- a/server.mjs +++ b/server.mjs @@ -1,6 +1,13 @@ -#!/usr/bin/env node +// This is a polyglot entrypoint for Headplane when running in production +// It doesn't use any dependencies aside from @remix-run/node and mime +// During build we bundle the used dependencies into the file so that +// we can only need this file and a Node.js installation to run the server. +// PREFIX is defined globally, see vite.config.ts import { access, constants } from 'node:fs/promises' +import { createReadStream, existsSync, statSync } from 'node:fs' +import { createServer } from 'node:http' +import { join, resolve } from 'node:path' function log(level, message) { const date = new Date().toISOString() @@ -28,25 +35,105 @@ try { process.exit(1) } -const { installGlobals } = await import('@remix-run/node') -const { remix } = await import('remix-hono/handler') -const { serve } = await import('@hono/node-server') -const { Hono } = await import('hono') +const { + createRequestHandler: remixRequestHandler, + createReadableStreamFromReadable, + writeReadableStreamToWritable +} = await import('@remix-run/node') +const { default: mime } = await import('mime') -installGlobals() -const app = new Hono() const port = process.env.PORT || 3000 const host = process.env.HOST || '0.0.0.0' +const buildPath = process.env.BUILD_PATH || './build' -app.use('*', remix({ - build: await import('./build/server/index.js'), - mode: 'production' -})) +// Because this is a dynamic import without an easily discernable path +// we gain the "deoptimization" we want so that Vite doesn't bundle this +const build = await import(resolve(join(buildPath, 'server', 'index.js'))) +const baseDir = resolve(join(buildPath, 'client')) -serve({ - fetch: app.fetch, - hostname: host, - port +const handler = remixRequestHandler(build, 'production') +const http = createServer(async (req, res) => { + const url = new URL(`http://${req.headers.host}${req.url}`) + + // Before we pass any requests to our Remix handler we need to check + // if we can handle a raw file request. This is important for the + // Remix loader to work correctly. + // + // To optimize this, we send them as readable streams in the node + // response and we also set headers for aggressive caching. + if (url.pathname.startsWith(`${PREFIX}assets/`)) { + const filePath = join(baseDir, url.pathname.replace(PREFIX, '')) + const exists = existsSync(filePath) + const stats = statSync(filePath) + + if (exists && stats.isFile()) { + // Build assets are cache-bust friendly so we can cache them heavily + if (req.url.startsWith('/build')) { + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') + } + + // Send the file as a readable stream + const fileStream = createReadStream(filePath) + const type = mime.getType(filePath) + + res.setHeader('Content-Length', stats.size) + res.setHeader('Content-Type', type) + fileStream.pipe(res) + return + } + } + + // Handling the request + const controller = new AbortController() + res.on('close', () => controller.abort()) + + const headers = new Headers() + for (const [key, value] of Object.entries(req.headers)) { + if (!value) continue + + if (Array.isArray(value)) { + for (const v of value) { + headers.append(key, v) + } + + continue + } + + headers.append(key, value) + } + + const remixReq = new Request(url.href, { + headers, + method: req.method, + signal: controller.signal, + + // If we have a body we set a duplex and we load the body + ...(req.method !== 'GET' && req.method !== 'HEAD' ? { + body: createReadableStreamFromReadable(req), + duplex: 'half' + } : {} + ) + }) + + // Pass our request to the Remix handler and get a response + const response = await handler(remixReq, {}) // No context + + // Handle our response and reply + res.statusCode = response.status + res.statusMessage = response.statusText + + for (const [key, value] of response.headers.entries()) { + res.appendHeader(key, value) + } + + if (response.body) { + await writeReadableStreamToWritable(response.body, res) + return + } + + res.end() }) -log('INFO', `Running on ${host}:${port}`) +http.listen(port, host, () => { + log('INFO', `Running on ${host}:${port}`) +}) diff --git a/vite.config.ts b/vite.config.ts index 997fcec..3ab8da1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,22 +6,59 @@ import tsconfigPaths from 'vite-tsconfig-paths' installGlobals() -export default defineConfig(({ isSsrBuild }) => ({ - base: '/admin/', - build: isSsrBuild ? { target: 'ES2022' } : {}, - plugins: [ - remix({ - basename: '/admin/', - }), - tsconfigPaths(), - babel({ - filter: /\.[jt]sx?$/, - babelConfig: { - presets: ['@babel/preset-typescript'], - plugins: [ - ['babel-plugin-react-compiler', {}], - ], +const prefix = process.env.__INTERNAL_PREFIX || '/admin/' +if (!prefix.endsWith('/')) { + throw new Error('Prefix must end with a slash') +} + +export default defineConfig(({ isSsrBuild }) => { + // If we have the Headplane entry we build it as a single + // server.mjs file that is built for production server bundle + // We know the remix invoked command is vite:build + if (!process.argv.includes('vite:build')) { + return { + build: { + minify: false, + target: 'esnext', + rollupOptions: { + input: './server.mjs', + output: { + entryFileNames: 'server.js', + dir: 'build/headplane', + banner: '#!/usr/bin/env node\n', + }, + external: (id) => id.startsWith('node:'), + } }, - }), - ], -})) + define: { + PREFIX: JSON.stringify(prefix), + }, + resolve: { + alias: { + stream: 'node:stream', + crypto: 'node:crypto', + } + } + } + } + + return ({ + base: prefix, + build: isSsrBuild ? { target: 'ES2022' } : {}, + plugins: [ + remix({ + basename: prefix, + }), + tsconfigPaths(), + babel({ + filter: /\.[jt]sx?$/, + babelConfig: { + presets: ['@babel/preset-typescript'], + plugins: [ + ['babel-plugin-react-compiler', {}], + ], + }, + }), + ], + }) +})