diff --git a/app/entry.server.tsx b/app/entry.server.tsx index ea2e9811..f9edddbe 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -4,10 +4,9 @@ * For more information, see https://remix.run/docs/en/main/file-conventions/entry.server */ -import { PassThrough } from "node:stream"; +import { PassThrough, Readable } from "node:stream"; import type { EntryContext } from "@remix-run/node"; -import { Response } from "@remix-run/node"; import { RemixServer } from "@remix-run/react"; import isbot from "isbot"; import { renderToPipeableStream } from "react-dom/server"; @@ -55,7 +54,7 @@ function handleBotRequest( responseHeaders.set("Content-Type", "text/html"); resolve( - new Response(body, { + new Response(Readable.toWeb(body) as ReadableStream, { headers: responseHeaders, status: responseStatusCode, }), @@ -97,7 +96,7 @@ function handleBrowserRequest( responseHeaders.set("Content-Type", "text/html"); resolve( - new Response(body, { + new Response(Readable.toWeb(body) as ReadableStream, { headers: responseHeaders, status: responseStatusCode, }), diff --git a/app/root.tsx b/app/root.tsx index fafef271..426fac35 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,5 +1,5 @@ import { cssBundleHref } from "@remix-run/css-bundle"; -import type { LinksFunction, LoaderArgs } from "@remix-run/node"; +import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { Links, @@ -18,7 +18,7 @@ export const links: LinksFunction = () => [ ...(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 83e1f6b8..d266ff72 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 0e067665..49787080 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,14 @@ "scripts": { "build": "remix build", "dev": "remix dev -c \"npm run dev:serve\"", - "dev:serve": "binode --require ./mocks -- @remix-run/serve:remix-serve ./build", + "dev:serve": "binode --require ./mocks -- @remix-run/serve:remix-serve ./build/index.js", "format": "prettier --write .", "format:repo": "npm run format && npm run lint:repo -- --fix", "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .", "lint:repo": "npm run lint -- --config .eslintrc.repo.js", "setup": "prisma generate && prisma migrate deploy && prisma db seed", - "start": "remix-serve build", - "start:mocks": "binode --require ./mocks -- @remix-run/serve:remix-serve build", + "start": "remix-serve ./build/index.js", + "start:mocks": "binode --require ./mocks -- @remix-run/serve:remix-serve ./build/index.js", "test": "vitest", "test:e2e:dev": "start-server-and-test dev http://localhost:3000 \"npx cypress open\"", "pretest:e2e:run": "npm run build", @@ -28,10 +28,10 @@ ], "dependencies": { "@prisma/client": "^4.16.2", - "@remix-run/css-bundle": "*", - "@remix-run/node": "*", - "@remix-run/react": "*", - "@remix-run/serve": "*", + "@remix-run/css-bundle": "^2.0.0-pre.8", + "@remix-run/node": "^2.0.0-pre.8", + "@remix-run/react": "^2.0.0-pre.8", + "@remix-run/serve": "^2.0.0-pre.8", "bcryptjs": "^2.4.3", "isbot": "^3.6.13", "react": "^18.2.0", @@ -40,8 +40,8 @@ }, "devDependencies": { "@faker-js/faker": "^8.0.2", - "@remix-run/dev": "*", - "@remix-run/eslint-config": "*", + "@remix-run/dev": "^2.0.0-pre.8", + "@remix-run/eslint-config": "^2.0.0-pre.8", "@testing-library/cypress": "^9.0.0", "@testing-library/jest-dom": "^5.17.0", "@types/bcryptjs": "^2.4.2", @@ -78,7 +78,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 521944d4..25e92ac4 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 = "indie-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("setup"), { cwd: rootDirectory, stdio: "inherit" }); 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,