From fc1a95ec132c45965fcc953fb70a5633c45aff53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Wed, 6 Sep 2023 22:03:48 +0200 Subject: [PATCH] feat: upgrade to Remix v2 --- app/entry.server.tsx | 10 +-- app/root.tsx | 4 +- app/routes/_index.tsx | 4 +- app/routes/healthcheck.tsx | 4 +- app/routes/join.tsx | 12 ++-- app/routes/login.tsx | 12 ++-- app/routes/logout.tsx | 5 +- app/routes/notes.$noteId.tsx | 6 +- app/routes/notes.new.tsx | 4 +- app/routes/notes.tsx | 4 +- app/utils.ts | 2 +- cypress/tsconfig.json | 4 +- package.json | 2 +- remix.config.js | 12 +--- remix.init/index.js | 116 +++++------------------------------ remix.init/package.json | 5 +- tsconfig.json | 4 +- 17 files changed, 63 insertions(+), 147 deletions(-) diff --git a/app/entry.server.tsx b/app/entry.server.tsx index ea2e9811..e099bfac 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -7,7 +7,7 @@ import { PassThrough } from "node:stream"; import type { EntryContext } from "@remix-run/node"; -import { Response } from "@remix-run/node"; +import { createReadableStreamFromReadable } from "@remix-run/node"; import { RemixServer } from "@remix-run/react"; import isbot from "isbot"; import { renderToPipeableStream } from "react-dom/server"; @@ -42,7 +42,7 @@ function handleBotRequest( remixContext: EntryContext, ) { return new Promise((resolve, reject) => { - const { pipe, abort } = renderToPipeableStream( + const { abort, pipe } = renderToPipeableStream( { - const { pipe, abort } = renderToPipeableStream( + const { abort, pipe } = renderToPipeableStream( [ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), ]; -export const loader = async ({ request }: LoaderArgs) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { return json({ user: await getUser(request) }); }; diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 65f79afd..9ac99571 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,9 +1,9 @@ -import type { V2_MetaFunction } from "@remix-run/node"; +import type { MetaFunction } from "@remix-run/node"; import { Link } from "@remix-run/react"; import { useOptionalUser } from "~/utils"; -export const meta: V2_MetaFunction = () => [{ title: "Remix Notes" }]; +export const meta: MetaFunction = () => [{ title: "Remix Notes" }]; export default function Index() { const user = useOptionalUser(); diff --git a/app/routes/healthcheck.tsx b/app/routes/healthcheck.tsx index 6c520cea..53168b88 100644 --- a/app/routes/healthcheck.tsx +++ b/app/routes/healthcheck.tsx @@ -1,9 +1,9 @@ // learn more: https://fly.io/docs/reference/configuration/#services-http_checks -import type { LoaderArgs } from "@remix-run/node"; +import type { LoaderFunctionArgs } from "@remix-run/node"; import { prisma } from "~/db.server"; -export const loader = async ({ request }: LoaderArgs) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { const host = request.headers.get("X-Forwarded-Host") ?? request.headers.get("host"); diff --git a/app/routes/join.tsx b/app/routes/join.tsx index 2ca5d882..5b42ad04 100644 --- a/app/routes/join.tsx +++ b/app/routes/join.tsx @@ -1,4 +1,8 @@ -import type { ActionArgs, LoaderArgs, V2_MetaFunction } from "@remix-run/node"; +import type { + ActionFunctionArgs, + LoaderFunctionArgs, + MetaFunction, +} from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Form, Link, useActionData, useSearchParams } from "@remix-run/react"; import { useEffect, useRef } from "react"; @@ -7,13 +11,13 @@ import { createUser, getUserByEmail } from "~/models/user.server"; import { createUserSession, getUserId } from "~/session.server"; import { safeRedirect, validateEmail } from "~/utils"; -export const loader = async ({ request }: LoaderArgs) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { const userId = await getUserId(request); if (userId) return redirect("/"); return json({}); }; -export const action = async ({ request }: ActionArgs) => { +export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const email = formData.get("email"); const password = formData.get("password"); @@ -63,7 +67,7 @@ export const action = async ({ request }: ActionArgs) => { }); }; -export const meta: V2_MetaFunction = () => [{ title: "Sign Up" }]; +export const meta: MetaFunction = () => [{ title: "Sign Up" }]; export default function Join() { const [searchParams] = useSearchParams(); diff --git a/app/routes/login.tsx b/app/routes/login.tsx index 1dbf77ba..aa6250e3 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -1,4 +1,8 @@ -import type { ActionArgs, LoaderArgs, V2_MetaFunction } from "@remix-run/node"; +import type { + ActionFunctionArgs, + LoaderFunctionArgs, + MetaFunction, +} from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Form, Link, useActionData, useSearchParams } from "@remix-run/react"; import { useEffect, useRef } from "react"; @@ -7,13 +11,13 @@ import { verifyLogin } from "~/models/user.server"; import { createUserSession, getUserId } from "~/session.server"; import { safeRedirect, validateEmail } from "~/utils"; -export const loader = async ({ request }: LoaderArgs) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { const userId = await getUserId(request); if (userId) return redirect("/"); return json({}); }; -export const action = async ({ request }: ActionArgs) => { +export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const email = formData.get("email"); const password = formData.get("password"); @@ -58,7 +62,7 @@ export const action = async ({ request }: ActionArgs) => { }); }; -export const meta: V2_MetaFunction = () => [{ title: "Login" }]; +export const meta: MetaFunction = () => [{ title: "Login" }]; export default function LoginPage() { const [searchParams] = useSearchParams(); diff --git a/app/routes/logout.tsx b/app/routes/logout.tsx index 541794d0..1ee2c8a9 100644 --- a/app/routes/logout.tsx +++ b/app/routes/logout.tsx @@ -1,8 +1,9 @@ -import type { ActionArgs } from "@remix-run/node"; +import type { ActionFunctionArgs } from "@remix-run/node"; import { redirect } from "@remix-run/node"; import { logout } from "~/session.server"; -export const action = async ({ request }: ActionArgs) => logout(request); +export const action = async ({ request }: ActionFunctionArgs) => + logout(request); export const loader = async () => redirect("/"); diff --git a/app/routes/notes.$noteId.tsx b/app/routes/notes.$noteId.tsx index 0f83fd86..3edd6ff2 100644 --- a/app/routes/notes.$noteId.tsx +++ b/app/routes/notes.$noteId.tsx @@ -1,4 +1,4 @@ -import type { ActionArgs, LoaderArgs } from "@remix-run/node"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Form, @@ -11,7 +11,7 @@ import invariant from "tiny-invariant"; import { deleteNote, getNote } from "~/models/note.server"; import { requireUserId } from "~/session.server"; -export const loader = async ({ params, request }: LoaderArgs) => { +export const loader = async ({ params, request }: LoaderFunctionArgs) => { const userId = await requireUserId(request); invariant(params.noteId, "noteId not found"); @@ -22,7 +22,7 @@ export const loader = async ({ params, request }: LoaderArgs) => { return json({ note }); }; -export const action = async ({ params, request }: ActionArgs) => { +export const action = async ({ params, request }: ActionFunctionArgs) => { const userId = await requireUserId(request); invariant(params.noteId, "noteId not found"); diff --git a/app/routes/notes.new.tsx b/app/routes/notes.new.tsx index ca11cf0f..48dd52de 100644 --- a/app/routes/notes.new.tsx +++ b/app/routes/notes.new.tsx @@ -1,4 +1,4 @@ -import type { ActionArgs } from "@remix-run/node"; +import type { ActionFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Form, useActionData } from "@remix-run/react"; import { useEffect, useRef } from "react"; @@ -6,7 +6,7 @@ import { useEffect, useRef } from "react"; import { createNote } from "~/models/note.server"; import { requireUserId } from "~/session.server"; -export const action = async ({ request }: ActionArgs) => { +export const action = async ({ request }: ActionFunctionArgs) => { const userId = await requireUserId(request); const formData = await request.formData(); diff --git a/app/routes/notes.tsx b/app/routes/notes.tsx index 17c84a65..ab9d6a47 100644 --- a/app/routes/notes.tsx +++ b/app/routes/notes.tsx @@ -1,4 +1,4 @@ -import type { LoaderArgs } from "@remix-run/node"; +import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { Form, Link, NavLink, Outlet, useLoaderData } from "@remix-run/react"; @@ -6,7 +6,7 @@ import { getNoteListItems } from "~/models/note.server"; import { requireUserId } from "~/session.server"; import { useUser } from "~/utils"; -export const loader = async ({ request }: LoaderArgs) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { const userId = await requireUserId(request); const noteListItems = await getNoteListItems({ userId }); return json({ noteListItems }); diff --git a/app/utils.ts b/app/utils.ts index 0f6a18c1..3a54cc1f 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -41,7 +41,7 @@ export function useMatchesData( () => matchingRoutes.find((route) => route.id === id), [matchingRoutes, id], ); - return route?.data; + return route?.data as Record; } function isUser(user: any): user is User { diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 88e648c5..732bfd9a 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -15,8 +15,8 @@ "types": ["node", "cypress", "@testing-library/cypress"], "esModuleInterop": true, "jsx": "react-jsx", - "moduleResolution": "node", - "target": "es2019", + "moduleResolution": "Bundler", + "target": "ES2020", "strict": true, "skipLibCheck": true, "resolveJsonModule": true, diff --git a/package.json b/package.json index 65a82e4a..799cadaf 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "vitest": "^0.34.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" }, "prisma": { "seed": "ts-node --require tsconfig-paths/register prisma/seed.ts" diff --git a/remix.config.js b/remix.config.js index 5b743123..29582b29 100644 --- a/remix.config.js +++ b/remix.config.js @@ -1,16 +1,6 @@ /** @type {import('@remix-run/dev').AppConfig} */ module.exports = { cacheDirectory: "./node_modules/.cache/remix", - future: { - v2_dev: true, - v2_errorBoundary: true, - v2_headers: true, - v2_meta: true, - v2_normalizeFormMethod: true, - v2_routeConvention: true, - }, - ignoredRouteFiles: ["**/.*", "**/*.test.{js,jsx,ts,tsx}"], - postcss: true, + ignoredRouteFiles: ["**/.*", "**/*.test.{ts,tsx}"], serverModuleFormat: "cjs", - tailwind: true, }; diff --git a/remix.init/index.js b/remix.init/index.js index 09309571..3835309b 100644 --- a/remix.init/index.js +++ b/remix.init/index.js @@ -1,47 +1,22 @@ -const { execSync } = require("child_process"); -const crypto = require("crypto"); -const fs = require("fs/promises"); -const path = require("path"); +const { execSync } = require("node:child_process"); +const crypto = require("node:crypto"); +const fs = require("node:fs/promises"); +const path = require("node:path"); const toml = require("@iarna/toml"); const PackageJson = require("@npmcli/package-json"); const semver = require("semver"); -const YAML = require("yaml"); -const cleanupCypressFiles = ({ fileEntries, isTypeScript, packageManager }) => +const cleanupCypressFiles = ({ fileEntries, packageManager }) => fileEntries.flatMap(([filePath, content]) => { - let newContent = content.replace( + const newContent = content.replace( new RegExp("npx ts-node", "g"), - isTypeScript ? `${packageManager.exec} ts-node` : "node", + `${packageManager.exec} ts-node`, ); - if (!isTypeScript) { - newContent = newContent - .replace(new RegExp("create-user.ts", "g"), "create-user.js") - .replace(new RegExp("delete-user.ts", "g"), "delete-user.js"); - } - return [fs.writeFile(filePath, newContent)]; }); -const cleanupDeployWorkflow = (deployWorkflow, deployWorkflowPath) => { - delete deployWorkflow.jobs.typecheck; - deployWorkflow.jobs.deploy.needs = deployWorkflow.jobs.deploy.needs.filter( - (need) => need !== "typecheck", - ); - - return [fs.writeFile(deployWorkflowPath, YAML.stringify(deployWorkflow))]; -}; - -const cleanupVitestConfig = (vitestConfig, vitestConfigPath) => { - const newVitestConfig = vitestConfig.replace( - "setup-test-env.ts", - "setup-test-env.js", - ); - - return [fs.writeFile(vitestConfigPath, newVitestConfig)]; -}; - const escapeRegExp = (string) => // $& means the whole matched string string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -81,15 +56,6 @@ const getPackageManagerVersion = (packageManager) => const getRandomString = (length) => crypto.randomBytes(length).toString("hex"); -const readFileIfNotTypeScript = ( - isTypeScript, - filePath, - parseFunction = (result) => result, -) => - isTypeScript - ? Promise.resolve() - : fs.readFile(filePath, "utf-8").then(parseFunction); - const removeUnusedDependencies = (dependencies, unusedDependencies) => Object.fromEntries( Object.entries(dependencies).filter( @@ -97,15 +63,12 @@ const removeUnusedDependencies = (dependencies, unusedDependencies) => ), ); -const updatePackageJson = ({ APP_NAME, isTypeScript, packageJson }) => { +const updatePackageJson = ({ APP_NAME, packageJson }) => { const { devDependencies, - prisma: { seed: prismaSeed, ...prisma }, scripts: { "format:repo": _repoFormatScript, "lint:repo": _repoLintScript, - typecheck, - validate, ...scripts }, } = packageJson.content; @@ -115,55 +78,29 @@ const updatePackageJson = ({ APP_NAME, isTypeScript, packageJson }) => { devDependencies: removeUnusedDependencies( devDependencies, // packages that are only used for linting the repo - ["eslint-plugin-markdown", "eslint-plugin-prefer-let"].concat( - isTypeScript ? [] : ["ts-node"], - ), + ["eslint-plugin-markdown", "eslint-plugin-prefer-let"], ), - prisma: isTypeScript - ? { ...prisma, seed: prismaSeed } - : { - ...prisma, - seed: prismaSeed - .replace("ts-node", "node") - .replace("seed.ts", "seed.js"), - }, - scripts: isTypeScript - ? { ...scripts, typecheck, validate } - : { ...scripts, validate: validate.replace(" typecheck", "") }, + scripts, }); }; -const main = async ({ isTypeScript, packageManager, rootDirectory }) => { +const main = async ({ packageManager, rootDirectory }) => { const pm = getPackageManagerCommand(packageManager); - const FILE_EXTENSION = isTypeScript ? "ts" : "js"; const README_PATH = path.join(rootDirectory, "README.md"); const FLY_TOML_PATH = path.join(rootDirectory, "fly.toml"); const EXAMPLE_ENV_PATH = path.join(rootDirectory, ".env.example"); const ENV_PATH = path.join(rootDirectory, ".env"); - const DEPLOY_WORKFLOW_PATH = path.join( - rootDirectory, - ".github", - "workflows", - "deploy.yml", - ); const DOCKERFILE_PATH = path.join(rootDirectory, "Dockerfile"); const CYPRESS_SUPPORT_PATH = path.join(rootDirectory, "cypress", "support"); - const CYPRESS_COMMANDS_PATH = path.join( - CYPRESS_SUPPORT_PATH, - `commands.${FILE_EXTENSION}`, - ); + const CYPRESS_COMMANDS_PATH = path.join(CYPRESS_SUPPORT_PATH, "commands.ts"); const CREATE_USER_COMMAND_PATH = path.join( CYPRESS_SUPPORT_PATH, - `create-user.${FILE_EXTENSION}`, + "create-user.ts", ); const DELETE_USER_COMMAND_PATH = path.join( CYPRESS_SUPPORT_PATH, - `delete-user.${FILE_EXTENSION}`, - ); - const VITEST_CONFIG_PATH = path.join( - rootDirectory, - `vitest.config.${FILE_EXTENSION}`, + "delete-user.ts", ); const REPLACER = "blues-stack-template"; @@ -183,8 +120,6 @@ const main = async ({ isTypeScript, packageManager, rootDirectory }) => { cypressCommands, createUserCommand, deleteUserCommand, - deployWorkflow, - vitestConfig, packageJson, ] = await Promise.all([ fs.readFile(FLY_TOML_PATH, "utf-8"), @@ -194,10 +129,6 @@ const main = async ({ isTypeScript, packageManager, rootDirectory }) => { fs.readFile(CYPRESS_COMMANDS_PATH, "utf-8"), fs.readFile(CREATE_USER_COMMAND_PATH, "utf-8"), fs.readFile(DELETE_USER_COMMAND_PATH, "utf-8"), - readFileIfNotTypeScript(isTypeScript, DEPLOY_WORKFLOW_PATH, (s) => - YAML.parse(s), - ), - readFileIfNotTypeScript(isTypeScript, VITEST_CONFIG_PATH), PackageJson.load(rootDirectory), ]); @@ -231,9 +162,9 @@ const main = async ({ isTypeScript, packageManager, rootDirectory }) => { ) : dockerfile; - updatePackageJson({ APP_NAME, isTypeScript, packageJson }); + updatePackageJson({ APP_NAME, packageJson }); - const fileOperationPromises = [ + await Promise.all([ fs.writeFile(FLY_TOML_PATH, toml.stringify(prodToml)), fs.writeFile(README_PATH, newReadme), fs.writeFile(ENV_PATH, newEnv), @@ -244,7 +175,6 @@ const main = async ({ isTypeScript, packageManager, rootDirectory }) => { [CREATE_USER_COMMAND_PATH, createUserCommand], [DELETE_USER_COMMAND_PATH, deleteUserCommand], ], - isTypeScript, packageManager: pm, }), packageJson.save(), @@ -262,19 +192,7 @@ const main = async ({ isTypeScript, packageManager, rootDirectory }) => { fs.rm(path.join(rootDirectory, ".github", "PULL_REQUEST_TEMPLATE.md")), fs.rm(path.join(rootDirectory, ".eslintrc.repo.js")), fs.rm(path.join(rootDirectory, "LICENSE.md")), - ]; - - if (!isTypeScript) { - fileOperationPromises.push( - ...cleanupDeployWorkflow(deployWorkflow, DEPLOY_WORKFLOW_PATH), - ); - - fileOperationPromises.push( - ...cleanupVitestConfig(vitestConfig, VITEST_CONFIG_PATH), - ); - } - - await Promise.all(fileOperationPromises); + ]); execSync(pm.run("format", "--loglevel warn"), { cwd: rootDirectory, diff --git a/remix.init/package.json b/remix.init/package.json index 5f161b81..7c795c83 100644 --- a/remix.init/package.json +++ b/remix.init/package.json @@ -5,8 +5,7 @@ "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", - "@npmcli/package-json": "^2.0.0", - "semver": "^7.5.4", - "yaml": "^2.3.1" + "@npmcli/package-json": "^5.0.0", + "semver": "^7.5.4" } } diff --git a/tsconfig.json b/tsconfig.json index 9bacef83..c0a761dd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "exclude": ["./cypress", "./cypress.config.ts"], "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2019"], + "lib": ["DOM", "DOM.Iterable", "ES2020"], "types": ["vitest/globals"], "isolatedModules": true, "esModuleInterop": true, @@ -10,7 +10,7 @@ "module": "CommonJS", "moduleResolution": "node", "resolveJsonModule": true, - "target": "ES2019", + "target": "ES2020", "strict": true, "allowJs": true, "forceConsistentCasingInFileNames": true,