diff --git a/app/entry.server.tsx b/app/entry.server.tsx index ea2e981..f9edddb 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 94c4f8e..60e8ed3 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, @@ -20,7 +20,7 @@ export const links: LinksFunction = () => [ { rel: "icon", href: "/_static/favicon.ico" }, ]; -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 57c85d9..1f09bf9 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/join.tsx b/app/routes/join.tsx index 2ca5d88..5b42ad0 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 1dbf77b..aa6250e 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 541794d..1ee2c8a 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 0f83fd8..3edd6ff 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 ca11cf0..48dd52d 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 17c84a6..ab9d6a4 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 0f6a18c..3a54cc1 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/support/test-routes/create-user.ts b/cypress/support/test-routes/create-user.ts index 0d4bb27..fcd11fc 100644 --- a/cypress/support/test-routes/create-user.ts +++ b/cypress/support/test-routes/create-user.ts @@ -1,10 +1,10 @@ -import type { ActionArgs } from "@remix-run/node"; +import type { ActionFunctionArgs } from "@remix-run/node"; import { redirect } from "@remix-run/node"; import { createUser } from "~/models/user.server"; import { createUserSession } from "~/session.server"; -export const action = async ({ request }: ActionArgs) => { +export const action = async ({ request }: ActionFunctionArgs) => { if (process.env.NODE_ENV === "production") { console.error( "🚨 🚨 🚨 🚨 🚨 🚨 🚨 🚨 test routes should not be enabled in production 🚨 🚨 🚨 🚨 🚨 🚨 🚨 🚨", diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 88e648c..732bfd9 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 bca2ecb..df4e6f0 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,10 @@ "@architect/architect": "^10.14.0", "@architect/functions": "^7.0.0", "@paralleldrive/cuid2": "^2.2.2", - "@remix-run/architect": "*", - "@remix-run/css-bundle": "*", - "@remix-run/node": "*", - "@remix-run/react": "*", - "@remix-run/server-runtime": "*", + "@remix-run/architect": "^2.0.0-pre.8", + "@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", "bcryptjs": "2.4.3", "isbot": "^3.6.13", "react": "^18.2.0", @@ -38,8 +37,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/architect__functions": "^3.13.7", @@ -75,6 +74,6 @@ "vitest": "^0.34.2" }, "engines": { - "node": ">=14" + "node": ">=18.0.0" } } diff --git a/remix.config.js b/remix.config.js index b2880e6..55a3f68 100644 --- a/remix.config.js +++ b/remix.config.js @@ -1,23 +1,13 @@ -const path = require("path"); +const path = require("node:path"); /** @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}"], publicPath: "/_static/build/", - postcss: true, server: "./server.ts", serverBuildPath: "server/index.js", serverModuleFormat: "cjs", - tailwind: true, routes(defineRoutes) { return defineRoutes((route) => { if (process.env.NODE_ENV === "production") return; diff --git a/remix.init/index.js b/remix.init/index.js index 8009282..9282a9a 100644 --- a/remix.init/index.js +++ b/remix.init/index.js @@ -1,39 +1,12 @@ -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 { toLogicalID } = require("@architect/utils"); const PackageJson = require("@npmcli/package-json"); const inquirer = require("inquirer"); const semver = require("semver"); -const YAML = require("yaml"); - -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 cleanupRemixConfig = (remixConfig, remixConfigPath) => { - const newRemixConfig = remixConfig - .replace("server.ts", "server.js") - .replace("create-user.ts", "create-user.js"); - - return [fs.writeFile(remixConfigPath, newRemixConfig)]; -}; - -const cleanupVitestConfig = (vitestConfig, vitestConfigPath) => { - const newVitestConfig = vitestConfig.replace( - "setup-test-env.ts", - "setup-test-env.js", - ); - - return [fs.writeFile(vitestConfigPath, newVitestConfig)]; -}; const getPackageManagerCommand = (packageManager) => // Inspired by https://github.com/nrwl/nx/blob/bd9b33eaef0393d01f747ea9a2ac5d2ca1fb87c6/packages/nx/src/utils/package-manager.ts#L38-L103 @@ -70,15 +43,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( @@ -86,14 +50,12 @@ const removeUnusedDependencies = (dependencies, unusedDependencies) => ), ); -const updatePackageJson = ({ APP_NAME, isTypeScript, packageJson }) => { +const updatePackageJson = ({ APP_NAME, packageJson }) => { const { devDependencies, scripts: { "format:repo": _repoFormatScript, "lint:repo": _repoLintScript, - typecheck, - validate, ...scripts }, } = packageJson.content; @@ -103,34 +65,17 @@ 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"], ), - scripts: isTypeScript - ? { ...scripts, typecheck, validate } - : { ...scripts, validate: validate.replace(" typecheck", "") }, + scripts, }); }; -const main = async ({ isTypeScript, packageManager, rootDirectory }) => { - const FILE_EXTENSION = isTypeScript ? "ts" : "js"; - +const main = async ({ packageManager, rootDirectory }) => { const APP_ARC_PATH = path.join(rootDirectory, "./app.arc"); const EXAMPLE_ENV_PATH = path.join(rootDirectory, ".env.example"); const ENV_PATH = path.join(rootDirectory, ".env"); const README_PATH = path.join(rootDirectory, "README.md"); - const DEPLOY_WORKFLOW_PATH = path.join( - rootDirectory, - ".github", - "workflows", - "deploy.yml", - ); - const REMIX_CONFIG_PATH = path.join(rootDirectory, "remix.config.js"); - const VITEST_CONFIG_PATH = path.join( - rootDirectory, - `vitest.config.${FILE_EXTENSION}`, - ); const DIR_NAME = path.basename(rootDirectory); const SUFFIX = getRandomString(2); @@ -139,23 +84,10 @@ const main = async ({ isTypeScript, packageManager, rootDirectory }) => { // get rid of anything that's not allowed in an app name .replace(/[^a-zA-Z0-9-_]/g, "-"); - const [ - appArc, - env, - readme, - deployWorkflow, - remixConfig, - vitestConfig, - packageJson, - ] = await Promise.all([ + const [appArc, env, readme, packageJson] = await Promise.all([ fs.readFile(APP_ARC_PATH, "utf-8"), fs.readFile(EXAMPLE_ENV_PATH, "utf-8"), fs.readFile(README_PATH, "utf-8"), - readFileIfNotTypeScript(isTypeScript, DEPLOY_WORKFLOW_PATH, (s) => - YAML.parse(s), - ), - readFileIfNotTypeScript(isTypeScript, REMIX_CONFIG_PATH), - readFileIfNotTypeScript(isTypeScript, VITEST_CONFIG_PATH), PackageJson.load(rootDirectory), ]); @@ -164,7 +96,7 @@ const main = async ({ isTypeScript, packageManager, rootDirectory }) => { `SESSION_SECRET="${getRandomString(16)}"`, ); - updatePackageJson({ APP_NAME, isTypeScript, packageJson }); + updatePackageJson({ APP_NAME, packageJson }); const initInstructions = ` - First run this stack's \`remix.init\` script and commit the changes it makes to your project. @@ -181,7 +113,7 @@ const main = async ({ isTypeScript, packageManager, rootDirectory }) => { .replace(new RegExp("RemixGrungeStack", "g"), toLogicalID(APP_NAME)) .replace(initInstructions, ""); - const fileOperationPromises = [ + await Promise.all([ fs.writeFile( APP_ARC_PATH, appArc.replace("grunge-stack-template", APP_NAME), @@ -203,23 +135,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( - ...cleanupRemixConfig(remixConfig, REMIX_CONFIG_PATH), - ); - - fileOperationPromises.push( - ...cleanupVitestConfig(vitestConfig, VITEST_CONFIG_PATH), - ); - } - - await Promise.all(fileOperationPromises); + ]); await askSetupQuestions({ packageManager, rootDirectory }).catch((error) => { if (error.isTtyError) { diff --git a/remix.init/package.json b/remix.init/package.json index 93ae555..6c2485c 100644 --- a/remix.init/package.json +++ b/remix.init/package.json @@ -5,9 +5,8 @@ "license": "MIT", "dependencies": { "@architect/utils": "^3.1.9", - "@npmcli/package-json": "^2.0.0", + "@npmcli/package-json": "^5.0.0", "inquirer": "^8.2.6", - "semver": "^7.5.4", - "yaml": "^2.3.1" + "semver": "^7.5.4" } } diff --git a/tsconfig.json b/tsconfig.json index 9bacef8..c0a761d 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,