diff --git a/.dockerignore b/.dockerignore index 65332bc..85679dd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,10 +9,8 @@ Dockerfile node_modules /.cache /build -/public/build .env -/public/generated /local *.local.* *.db -coverage \ No newline at end of file +coverage diff --git a/.eslintignore b/.eslintignore index 6038637..71f5f6d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,9 +1,7 @@ node_modules build -public/build -public/generated # Intentional ignore *.ignored/ -*.ignored.* \ No newline at end of file +*.ignored.* diff --git a/.gitignore b/.gitignore index ee9a608..4859264 100644 --- a/.gitignore +++ b/.gitignore @@ -5,14 +5,11 @@ node_modules /.cache /build -/public/build .env -/public/generated - /local *.local.* *.db -coverage \ No newline at end of file +coverage diff --git a/.prettierignore b/.prettierignore index 0f98c21..365e115 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,13 +1,10 @@ node_modules /build -/public/build -/public/generated -/server-build .env pnpm-lock.yaml coverage -drizzle/meta \ No newline at end of file +drizzle/meta diff --git a/app/root.tsx b/app/root.tsx index e1b0274..8e17b4e 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,5 +1,4 @@ -import { cssBundleHref } from "@remix-run/css-bundle"; -import appStylesheet from "#app/styles/app.css"; +import appStylesheet from "#app/styles/app.css?url"; import { json } from "@remix-run/node"; import type { MetaFunction, @@ -8,7 +7,6 @@ import type { } from "@remix-run/node"; import { Links, - LiveReload, Meta, Outlet, Scripts, @@ -33,7 +31,6 @@ import { csrf } from "./utils/csrf.server.ts"; import { AuthenticityTokenProvider } from "remix-utils/csrf/react"; export const links: LinksFunction = () => [ - ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), { rel: "stylesheet", href: appStylesheet }, { rel: "manifest", href: "/site.webmanifest" }, ...faviconLinks, @@ -126,13 +123,12 @@ function Document({ /> - ); } -export function App() { +function App() { const data = useLoaderData(); const nonce = useNonce(); const env = data.env; diff --git a/app/utils/blog.server.ts b/app/utils/blog.server.ts index 1875cc2..3df432c 100644 --- a/app/utils/blog.server.ts +++ b/app/utils/blog.server.ts @@ -11,10 +11,10 @@ import { import matter from "gray-matter"; import { fileURLToPath } from "url"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __filename = fileURLToPath(import.meta.url); // build/server/index.js +const __dirname = dirname(__filename); // build/server/ -const rootPath = resolve(__dirname, "../"); +const rootPath = resolve(__dirname, "../../"); const contentDirPath = resolve(rootPath, "./content"); const blogDirPath = resolve(contentDirPath, "./blog"); diff --git a/app/utils/compile-mdx.server.ts b/app/utils/compile-mdx.server.ts index 79dc590..8f3f442 100644 --- a/app/utils/compile-mdx.server.ts +++ b/app/utils/compile-mdx.server.ts @@ -10,11 +10,11 @@ import { cachified } from "cachified"; import { lruCache } from "./cache.server.ts"; import { fileURLToPath } from "url"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __filename = fileURLToPath(import.meta.url); // build/server/index.js +const __dirname = dirname(__filename); // build/server/ -const root = resolve(__dirname, "../"); -const publicDir = resolve(root, "./public"); +const rootPath = resolve(__dirname, "../../"); +const assetsDirPath = resolve(rootPath, "./build/client/assets/"); export interface BundleMdx { source: string; @@ -50,8 +50,8 @@ export const compileMdx = async ({ source, files }: BundleMdx) => { ".jpeg": "file", ".gif": "file", }; - options.outdir = resolve(publicDir, "./generated/assets"); - options.publicPath = "/generated/assets"; + options.outdir = resolve(assetsDirPath, "./blog/"); + options.publicPath = "/assets/blog/"; options.write = true; return options; diff --git a/package.json b/package.json index a53125c..eb00617 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "sideEffects": false, "type": "module", "scripts": { - "build": "remix build", - "dev": "remix dev --manual -c \"node server.js\"", + "dev": "node ./server.js", + "build": "remix vite:build", "start": "cross-env NODE_ENV=production node ./server.js", "typecheck": "tsc", "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .", @@ -103,6 +103,7 @@ "tailwindcss-animate": "1.0.7", "tsx": "4.7.1", "typescript": "5.3.3", + "vite": "5.1.5", "vite-tsconfig-paths": "4.3.1", "vitest": "1.3.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cc1280..0065f09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -189,7 +189,7 @@ devDependencies: version: 8.4.1 '@remix-run/dev': specifier: 2.8.0 - version: 2.8.0(@remix-run/serve@2.8.0)(@types/node@20.11.24)(typescript@5.3.3) + version: 2.8.0(@remix-run/serve@2.8.0)(@types/node@20.11.24)(typescript@5.3.3)(vite@5.1.5) '@remix-run/eslint-config': specifier: 2.8.0 version: 2.8.0(eslint@8.57.0)(react@18.2.0)(typescript@5.3.3) @@ -268,9 +268,12 @@ devDependencies: typescript: specifier: 5.3.3 version: 5.3.3 + vite: + specifier: 5.1.5 + version: 5.1.5(@types/node@20.11.24) vite-tsconfig-paths: specifier: 4.3.1 - version: 4.3.1(typescript@5.3.3) + version: 4.3.1(typescript@5.3.3)(vite@5.1.5) vitest: specifier: 1.3.1 version: 1.3.1(@types/node@20.11.24)(jsdom@24.0.0) @@ -2803,7 +2806,7 @@ packages: engines: {node: '>=18.0.0'} dev: false - /@remix-run/dev@2.8.0(@remix-run/serve@2.8.0)(@types/node@20.11.24)(typescript@5.3.3): + /@remix-run/dev@2.8.0(@remix-run/serve@2.8.0)(@types/node@20.11.24)(typescript@5.3.3)(vite@5.1.5): resolution: {integrity: sha512-kZtmK/7vKk7QV8CGCyC9Or3wP7EwL4rOJS9vObmTRAPv8mLyznR8bJxeNVWA7ICnCGejF8s2X3abVJrkEMiFlg==} engines: {node: '>=18.0.0'} hasBin: true @@ -2876,6 +2879,7 @@ packages: tar-fs: 2.1.1 tsconfig-paths: 4.2.0 typescript: 5.3.3 + vite: 5.1.5(@types/node@20.11.24) ws: 7.5.9 transitivePeerDependencies: - '@types/node' @@ -3270,7 +3274,7 @@ packages: /@types/acorn@4.0.6: resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 /@types/aria-query@5.0.1: resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} @@ -3317,7 +3321,6 @@ packages: /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - dev: true /@types/express-serve-static-core@4.17.35: resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} @@ -5539,7 +5542,7 @@ packages: /estree-util-attach-comments@3.0.0: resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 dev: false /estree-util-build-jsx@2.2.2: @@ -6236,7 +6239,7 @@ packages: /hast-util-to-estree@3.1.0: resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 '@types/estree-jsx': 1.0.0 '@types/hast': 3.0.0 comma-separated-tokens: 2.0.3 @@ -6614,7 +6617,7 @@ packages: /is-reference@3.0.1: resolution: {integrity: sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==} dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} @@ -7495,7 +7498,7 @@ packages: /micromark-extension-mdx-expression@3.0.0: resolution: {integrity: sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==} dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 devlop: 1.1.0 micromark-factory-mdx-expression: 2.0.1 micromark-factory-space: 2.0.0 @@ -7523,7 +7526,7 @@ packages: resolution: {integrity: sha512-uvhhss8OGuzR4/N17L1JwvmJIpPhAd8oByMawEKx6NVdBCbesjH4t+vjEp3ZXft9DwvlKSD07fCeI44/N0Vf2w==} dependencies: '@types/acorn': 4.0.6 - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 micromark-factory-mdx-expression: 2.0.1 @@ -7562,7 +7565,7 @@ packages: /micromark-extension-mdxjs-esm@3.0.0: resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 devlop: 1.1.0 micromark-core-commonmark: 2.0.0 micromark-util-character: 2.0.1 @@ -7649,7 +7652,7 @@ packages: /micromark-factory-mdx-expression@2.0.1: resolution: {integrity: sha512-F0ccWIUHRLRrYp5TC9ZYXmZo+p2AM13ggbsW4T0b5CRKP8KHVRB8t4pwtBgTxtjRmwrK0Irwm7vs2JOZabHZfg==} dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 devlop: 1.1.0 micromark-util-character: 2.0.1 micromark-util-events-to-acorn: 2.0.2 @@ -7821,7 +7824,7 @@ packages: resolution: {integrity: sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==} dependencies: '@types/acorn': 4.0.6 - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 '@types/unist': 3.0.0 devlop: 1.1.0 estree-util-visit: 2.0.0 @@ -10494,7 +10497,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.1.1(@types/node@20.11.24) + vite: 5.1.5(@types/node@20.11.24) transitivePeerDependencies: - '@types/node' - less @@ -10506,7 +10509,7 @@ packages: - terser dev: true - /vite-tsconfig-paths@4.3.1(typescript@5.3.3): + /vite-tsconfig-paths@4.3.1(typescript@5.3.3)(vite@5.1.5): resolution: {integrity: sha512-cfgJwcGOsIxXOLU/nELPny2/LUD/lcf1IbfyeKTv2bsupVbTH/xpFtdQlBmIP1GEK2CjjLxYhFfB+QODFAx5aw==} peerDependencies: vite: '*' @@ -10517,6 +10520,7 @@ packages: debug: 4.3.4 globrex: 0.1.2 tsconfck: 3.0.2(typescript@5.3.3) + vite: 5.1.5(@types/node@20.11.24) transitivePeerDependencies: - supports-color - typescript @@ -10555,8 +10559,8 @@ packages: fsevents: 2.3.3 dev: true - /vite@5.1.1(@types/node@20.11.24): - resolution: {integrity: sha512-wclpAgY3F1tR7t9LL5CcHC41YPkQIpKUGeIuT8MdNwNZr6OqOTLs7JX5vIHAtzqLWXts0T+GDrh9pN2arneKqg==} + /vite@5.1.5(@types/node@20.11.24): + resolution: {integrity: sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -10635,7 +10639,7 @@ packages: strip-literal: 2.0.0 tinybench: 2.6.0 tinypool: 0.8.2 - vite: 5.1.1(@types/node@20.11.24) + vite: 5.1.5(@types/node@20.11.24) vite-node: 1.3.1(@types/node@20.11.24) why-is-node-running: 2.2.2 transitivePeerDependencies: diff --git a/remix.config.js b/remix.config.js deleted file mode 100644 index 381f722..0000000 --- a/remix.config.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @type {import('@remix-run/dev').AppConfig} */ -export default { - serverModuleFormat: "esm", - watchPaths: ["./tailwind.config.ts", "./content"], -}; diff --git a/server.js b/server.js index 18269e1..65713a4 100644 --- a/server.js +++ b/server.js @@ -1,8 +1,7 @@ import "dotenv/config"; -import * as fs from "node:fs"; import crypto from "crypto"; import { createRequestHandler } from "@remix-run/express"; -import { broadcastDevReady, installGlobals } from "@remix-run/node"; +import { installGlobals } from "@remix-run/node"; import compression from "compression"; import express from "express"; import helmet from "helmet"; @@ -12,16 +11,19 @@ import getPort, { portNumbers } from "get-port"; import chalk from "chalk"; import { printUrls } from "./server-utils.js"; +const MODE = process.env.NODE_ENV; + sourceMapSupport.install(); installGlobals(); -const MODE = process.env.NODE_ENV; - -const BUILD_PATH = "./build/index.js"; -/** - * @type { import('@remix-run/node').ServerBuild | Promise } - */ -let build = await import(BUILD_PATH); +const viteDevServer = + MODE === "production" + ? undefined + : await import("vite").then((vite) => + vite.createServer({ + server: { middlewareMode: true }, + }), + ); const app = express(); @@ -30,16 +32,6 @@ app.use(compression()); // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header app.disable("x-powered-by"); -// Remix fingerprints its assets so we can cache forever. -app.use( - "/build", - express.static("public/build", { immutable: true, maxAge: "1y" }), -); - -// Everything else (like favicon.ico) is cached for an hour. You may want to be -// more aggressive with this caching. -app.use(express.static("public", { maxAge: "1h" })); - app.use(morgan("tiny")); app.use((_, res, next) => { @@ -77,18 +69,29 @@ app.use( }), ); -function getRequestHandler(build) { - function getLoadContext(_, res) { - return { cspNonce: res.locals.cspNonce }; - } - return createRequestHandler({ build, mode: MODE, getLoadContext }); +// handle asset requests +if (viteDevServer) { + app.use(viteDevServer.middlewares); +} else { + app.use( + "/assets", + express.static("build/client/assets", { + immutable: true, + maxAge: "1y", + }), + ); } +app.use(express.static("build/client", { maxAge: "1h" })); +// handle SSR requests app.all( "*", - MODE === "development" - ? await createDevRequestHandler() - : getRequestHandler(build), + createRequestHandler({ + getLoadContext: (_, res) => ({ cspNonce: res.locals.cspNonce }), + build: viteDevServer + ? () => viteDevServer.ssrLoadModule("virtual:remix/server-build") + : await import("./build/server/index.js"), + }), ); const desiredPort = Number(process.env.PORT || 3000); @@ -113,29 +116,4 @@ const server = app.listen(portToUse, async () => { printUrls(portUsed); console.log(chalk.bold("Press Ctrl+C to stop")); - - if (MODE === "development") { - broadcastDevReady(build); - } }); - -async function createDevRequestHandler() { - const chokidar = await import("chokidar"); - const watcher = chokidar.watch(BUILD_PATH, { ignoreInitial: true }); - - watcher.on("all", async () => { - // 1. purge require cache && load updated server build - const stat = fs.statSync(BUILD_PATH); - build = import(BUILD_PATH + "?t=" + stat.mtimeMs); - // 2. tell dev server that this app server is now ready - broadcastDevReady(await build); - }); - - return async (req, res, next) => { - try { - return getRequestHandler(await build)(req, res, next); - } catch (error) { - next(error); - } - }; -} diff --git a/tsconfig.json b/tsconfig.json index 9c26ae1..e30880c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", - "module": "ES2022", + "module": "ESNext", "target": "ES2022", "moduleResolution": "bundler", "resolveJsonModule": true, diff --git a/types/remix.env.d.ts b/types/env.d.ts similarity index 50% rename from types/remix.env.d.ts rename to types/env.d.ts index dcf8c45..8d2f951 100644 --- a/types/remix.env.d.ts +++ b/types/env.d.ts @@ -1,2 +1,2 @@ -/// +/// /// diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..279b45f --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,12 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + tsconfigPaths(), + remix({ + serverModuleFormat: "esm", + }), + ], +});