From 62e65077269d803627418677f180a77aab2bff53 Mon Sep 17 00:00:00 2001 From: onemen Date: Thu, 18 May 2023 20:33:37 +0300 Subject: [PATCH] Upgrade to Native ESM (#47) --- .eslintrc.js => .eslintrc.cjs | 0 app/components/error-boundary.tsx | 2 +- app/entry.client.tsx | 2 +- app/entry.server.tsx | 2 +- app/root.tsx | 18 ++-- app/routes/_auth+/forgot-password.tsx | 16 ++-- app/routes/_auth+/login.tsx | 10 +- app/routes/_auth+/logout.tsx | 2 +- app/routes/_auth+/onboarding.tsx | 16 ++-- app/routes/_auth+/reset-password.tsx | 12 +-- app/routes/_auth+/signup.tsx | 16 ++-- app/routes/admin+/cache.tsx | 16 ++-- app/routes/admin+/cache_.lru.$cacheKey.ts | 6 +- app/routes/admin+/cache_.sqlite.$cacheKey.ts | 6 +- app/routes/admin+/cache_.sqlite.tsx | 2 +- app/routes/me.tsx | 4 +- app/routes/resources+/delete-image.test.tsx | 8 +- app/routes/resources+/delete-image.tsx | 4 +- app/routes/resources+/delete-note.tsx | 6 +- app/routes/resources+/file.$fileId.tsx | 2 +- app/routes/resources+/healthcheck.tsx | 2 +- app/routes/resources+/image-upload.tsx | 2 +- app/routes/resources+/login.tsx | 12 +-- app/routes/resources+/note-editor.tsx | 6 +- app/routes/settings+/profile.photo.tsx | 16 ++-- app/routes/settings+/profile.tsx | 10 +- app/routes/users+/$username.tsx | 12 +-- .../users+/$username_+/notes.$noteId.tsx | 10 +- .../$username_+/notes.$noteId_.edit.tsx | 4 +- app/routes/users+/$username_+/notes.new.tsx | 2 +- app/routes/users+/$username_+/notes.tsx | 10 +- app/utils/auth.server.ts | 4 +- app/utils/cache.server.ts | 6 +- app/utils/db.server.ts | 2 +- app/utils/encryption.server.test.ts | 2 +- app/utils/forms.tsx | 4 +- app/utils/permissions.server.ts | 4 +- app/utils/user.ts | 2 +- index.js | 8 +- mocks/index.ts | 2 +- mocks/utils.ts | 2 + other/build-server.ts | 10 +- other/start.js | 4 +- other/test-setup/global-setup.ts | 2 +- other/test-setup/matchers.cjs | 5 + other/test-setup/setup-env-vars.ts | 2 +- other/test-setup/setup-test-env.ts | 12 +-- other/test-setup/vitejs-plugin-react.cjs | 2 + package.json | 1 + postcss.config.js | 2 +- prisma/seed.ts | 8 +- remix.config.js | 13 ++- remix.init/index.js | 96 +------------------ remix.init/index.mjs | 94 ++++++++++++++++++ server/index.ts | 37 +++---- tailwind.config.ts | 4 +- tests/e2e/onboarding.test.ts | 2 +- tests/e2e/settings-profile.test.ts | 6 +- tests/playwright-utils.ts | 10 +- tests/vitest-utils.ts | 4 +- tsconfig.json | 10 +- vitest.config.ts | 2 +- 62 files changed, 310 insertions(+), 288 deletions(-) rename .eslintrc.js => .eslintrc.cjs (100%) create mode 100644 other/test-setup/matchers.cjs create mode 100644 other/test-setup/vitejs-plugin-react.cjs create mode 100644 remix.init/index.mjs diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 100% rename from .eslintrc.js rename to .eslintrc.cjs diff --git a/app/components/error-boundary.tsx b/app/components/error-boundary.tsx index 0d2f9ac51..87eab7a68 100644 --- a/app/components/error-boundary.tsx +++ b/app/components/error-boundary.tsx @@ -4,7 +4,7 @@ import { useRouteError, } from '@remix-run/react' import { type ErrorResponse } from '@remix-run/router' -import { getErrorMessage } from '~/utils/misc' +import { getErrorMessage } from '~/utils/misc.ts' type StatusHandler = (info: { error: ErrorResponse diff --git a/app/entry.client.tsx b/app/entry.client.tsx index 562888e43..713cb7c7e 100644 --- a/app/entry.client.tsx +++ b/app/entry.client.tsx @@ -3,7 +3,7 @@ import { startTransition } from 'react' import { hydrateRoot } from 'react-dom/client' if (ENV.MODE === 'development') { - import('~/utils/devtools').then(({ init }) => init()) + import('~/utils/devtools.tsx').then(({ init }) => init()) } startTransition(() => { hydrateRoot(document, ) diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 49809e0ad..0d24a52cc 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -4,7 +4,7 @@ import { type EntryContext, Response } from '@remix-run/node' import { RemixServer } from '@remix-run/react' import isbot from 'isbot' import { renderToPipeableStream } from 'react-dom/server' -import { init, getEnv } from './utils/env.server' +import { init, getEnv } from './utils/env.server.ts' import { getInstanceInfo } from 'litefs-js' const ABORT_DELAY = 5000 diff --git a/app/root.tsx b/app/root.tsx index 4807695d0..33cb85ff4 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,5 +1,5 @@ -import * as Checkbox from '@radix-ui/react-checkbox' -import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import Checkbox from '@radix-ui/react-checkbox/dist/index.js' +import DropdownMenu from '@radix-ui/react-dropdown-menu/dist/index.js' import { cssBundleHref } from '@remix-run/css-bundle' import { json, @@ -20,15 +20,15 @@ import { useLoaderData, useSubmit, } from '@remix-run/react' -import clsx from 'clsx' +import { clsx } from 'clsx' import { useState } from 'react' import tailwindStylesheetUrl from './styles/tailwind.css' -import { authenticator } from './utils/auth.server' -import { prisma } from './utils/db.server' -import { getEnv } from './utils/env.server' -import { ButtonLink } from './utils/forms' -import { getUserImgSrc } from './utils/misc' -import { useUser } from './utils/user' +import { authenticator } from './utils/auth.server.ts' +import { prisma } from './utils/db.server.ts' +import { getEnv } from './utils/env.server.ts' +import { ButtonLink } from './utils/forms.tsx' +import { getUserImgSrc } from './utils/misc.ts' +import { useUser } from './utils/user.ts' export const links: LinksFunction = () => { return [ diff --git a/app/routes/_auth+/forgot-password.tsx b/app/routes/_auth+/forgot-password.tsx index f6b1000e8..c254236d7 100644 --- a/app/routes/_auth+/forgot-password.tsx +++ b/app/routes/_auth+/forgot-password.tsx @@ -8,14 +8,14 @@ import { } from '@remix-run/node' import { Link, useFetcher } from '@remix-run/react' import { z } from 'zod' -import { GeneralErrorBoundary } from '~/components/error-boundary' -import { prisma } from '~/utils/db.server' -import { sendEmail } from '~/utils/email.server' -import { decrypt, encrypt } from '~/utils/encryption.server' -import { Button, ErrorList, Field } from '~/utils/forms' -import { getDomainUrl } from '~/utils/misc.server' -import { commitSession, getSession } from '~/utils/session.server' -import { emailSchema, usernameSchema } from '~/utils/user-validation' +import { GeneralErrorBoundary } from '~/components/error-boundary.tsx' +import { prisma } from '~/utils/db.server.ts' +import { sendEmail } from '~/utils/email.server.ts' +import { decrypt, encrypt } from '~/utils/encryption.server.ts' +import { Button, ErrorList, Field } from '~/utils/forms.tsx' +import { getDomainUrl } from '~/utils/misc.server.ts' +import { commitSession, getSession } from '~/utils/session.server.ts' +import { emailSchema, usernameSchema } from '~/utils/user-validation.ts' export const resetPasswordSessionKey = 'resetPasswordToken' const resetPasswordTokenQueryParam = 'token' diff --git a/app/routes/_auth+/login.tsx b/app/routes/_auth+/login.tsx index b436b631a..7f395d1eb 100644 --- a/app/routes/_auth+/login.tsx +++ b/app/routes/_auth+/login.tsx @@ -4,11 +4,11 @@ import { type V2_MetaFunction, } from '@remix-run/node' import { useLoaderData, useSearchParams } from '@remix-run/react' -import { GeneralErrorBoundary } from '~/components/error-boundary' -import { Spacer } from '~/components/spacer' -import { authenticator } from '~/utils/auth.server' -import { commitSession, getSession } from '~/utils/session.server' -import { InlineLogin } from '../resources+/login' +import { GeneralErrorBoundary } from '~/components/error-boundary.tsx' +import { Spacer } from '~/components/spacer.tsx' +import { authenticator } from '~/utils/auth.server.ts' +import { commitSession, getSession } from '~/utils/session.server.ts' +import { InlineLogin } from '../resources+/login.tsx' export async function loader({ request }: DataFunctionArgs) { await authenticator.isAuthenticated(request, { diff --git a/app/routes/_auth+/logout.tsx b/app/routes/_auth+/logout.tsx index 97163586a..7e3b7e224 100644 --- a/app/routes/_auth+/logout.tsx +++ b/app/routes/_auth+/logout.tsx @@ -1,5 +1,5 @@ import { redirect, type DataFunctionArgs } from '@remix-run/node' -import { authenticator } from '~/utils/auth.server' +import { authenticator } from '~/utils/auth.server.ts' export async function action({ request }: DataFunctionArgs) { await authenticator.logout(request, { redirectTo: '/' }) diff --git a/app/routes/_auth+/onboarding.tsx b/app/routes/_auth+/onboarding.tsx index c45c5e84a..005986d3f 100644 --- a/app/routes/_auth+/onboarding.tsx +++ b/app/routes/_auth+/onboarding.tsx @@ -15,18 +15,18 @@ import { useSearchParams, } from '@remix-run/react' import { z } from 'zod' -import { Spacer } from '~/components/spacer' -import { authenticator, createUser } from '~/utils/auth.server' -import { Button, CheckboxField, ErrorList, Field } from '~/utils/forms' -import { safeRedirect } from '~/utils/misc' -import { commitSession, getSession } from '~/utils/session.server' +import { Spacer } from '~/components/spacer.tsx' +import { authenticator, createUser } from '~/utils/auth.server.ts' +import { Button, CheckboxField, ErrorList, Field } from '~/utils/forms.tsx' +import { safeRedirect } from '~/utils/misc.ts' +import { commitSession, getSession } from '~/utils/session.server.ts' import { nameSchema, passwordSchema, usernameSchema, -} from '~/utils/user-validation' -import { onboardingEmailSessionKey } from './signup' -import { checkboxSchema } from '~/utils/zod-extensions' +} from '~/utils/user-validation.ts' +import { onboardingEmailSessionKey } from './signup.tsx' +import { checkboxSchema } from '~/utils/zod-extensions.ts' const OnboardingFormSchema = z .object({ diff --git a/app/routes/_auth+/reset-password.tsx b/app/routes/_auth+/reset-password.tsx index 04a9575ab..91675c0f3 100644 --- a/app/routes/_auth+/reset-password.tsx +++ b/app/routes/_auth+/reset-password.tsx @@ -12,14 +12,14 @@ import { useNavigation, } from '@remix-run/react' import { z } from 'zod' -import { GeneralErrorBoundary } from '~/components/error-boundary' -import { authenticator, resetUserPassword } from '~/utils/auth.server' -import { Button, ErrorList, Field } from '~/utils/forms' +import { GeneralErrorBoundary } from '~/components/error-boundary.tsx' +import { authenticator, resetUserPassword } from '~/utils/auth.server.ts' +import { Button, ErrorList, Field } from '~/utils/forms.tsx' import { useForm } from '@conform-to/react' import { getFieldsetConstraint, parse } from '@conform-to/zod' -import { commitSession, getSession } from '~/utils/session.server' -import { passwordSchema } from '~/utils/user-validation' -import { resetPasswordSessionKey } from './forgot-password' +import { commitSession, getSession } from '~/utils/session.server.ts' +import { passwordSchema } from '~/utils/user-validation.ts' +import { resetPasswordSessionKey } from './forgot-password.tsx' const ResetPasswordSchema = z .object({ diff --git a/app/routes/_auth+/signup.tsx b/app/routes/_auth+/signup.tsx index 7591c0a51..b1a2ca909 100644 --- a/app/routes/_auth+/signup.tsx +++ b/app/routes/_auth+/signup.tsx @@ -6,16 +6,16 @@ import { } from '@remix-run/node' import { useFetcher } from '@remix-run/react' import { z } from 'zod' -import { GeneralErrorBoundary } from '~/components/error-boundary' -import { prisma } from '~/utils/db.server' -import { sendEmail } from '~/utils/email.server' -import { decrypt, encrypt } from '~/utils/encryption.server' -import { Button, ErrorList, Field } from '~/utils/forms' +import { GeneralErrorBoundary } from '~/components/error-boundary.tsx' +import { prisma } from '~/utils/db.server.ts' +import { sendEmail } from '~/utils/email.server.ts' +import { decrypt, encrypt } from '~/utils/encryption.server.ts' +import { Button, ErrorList, Field } from '~/utils/forms.tsx' import { conform, useForm } from '@conform-to/react' import { getFieldsetConstraint, parse } from '@conform-to/zod' -import { getDomainUrl } from '~/utils/misc.server' -import { commitSession, getSession } from '~/utils/session.server' -import { emailSchema } from '~/utils/user-validation' +import { getDomainUrl } from '~/utils/misc.server.ts' +import { commitSession, getSession } from '~/utils/session.server.ts' +import { emailSchema } from '~/utils/user-validation.ts' export const onboardingEmailSessionKey = 'onboardingToken' const onboardingTokenQueryParam = 'token' diff --git a/app/routes/admin+/cache.tsx b/app/routes/admin+/cache.tsx index 9fc61f685..ac031d432 100644 --- a/app/routes/admin+/cache.tsx +++ b/app/routes/admin+/cache.tsx @@ -7,18 +7,18 @@ import { useSubmit, } from '@remix-run/react' import { getAllInstances, getInstanceInfo } from 'litefs-js' -import { ensureInstance } from 'litefs-js/remix' +import { ensureInstance } from 'litefs-js/remix.js' import invariant from 'tiny-invariant' -import { Spacer } from '~/components/spacer' +import { Spacer } from '~/components/spacer.tsx' import { cache, getAllCacheKeys, lruCache, searchCacheKeys, -} from '~/utils/cache.server' -import { Button, Field } from '~/utils/forms' -import { useDebounce, useDoubleCheck } from '~/utils/misc' -import { requireAdmin } from '~/utils/permissions.server' +} from '~/utils/cache.server.ts' +import { Button, Field } from '~/utils/forms.tsx' +import { useDebounce, useDoubleCheck } from '~/utils/misc.ts' +import { requireAdmin } from '~/utils/permissions.server.ts' export async function loader({ request }: DataFunctionArgs) { await requireAdmin(request) @@ -99,7 +99,7 @@ export default function CacheAdminRoute() {
@@ -112,7 +112,7 @@ export default function CacheAdminRoute() { defaultValue: query, }} /> -
+
{data.cacheKeys.sqlite.length + data.cacheKeys.lru.length} diff --git a/app/routes/admin+/cache_.lru.$cacheKey.ts b/app/routes/admin+/cache_.lru.$cacheKey.ts index 62107854e..8d9a0c94f 100644 --- a/app/routes/admin+/cache_.lru.$cacheKey.ts +++ b/app/routes/admin+/cache_.lru.$cacheKey.ts @@ -2,9 +2,9 @@ import type { DataFunctionArgs } from '@remix-run/node' import { json } from '@remix-run/node' import invariant from 'tiny-invariant' import { getAllInstances, getInstanceInfo } from 'litefs-js' -import { ensureInstance } from 'litefs-js/remix' -import { lruCache } from '~/utils/cache.server' -import { requireAdmin } from '~/utils/permissions.server' +import { ensureInstance } from 'litefs-js/remix.js' +import { lruCache } from '~/utils/cache.server.ts' +import { requireAdmin } from '~/utils/permissions.server.ts' export async function loader({ request, params }: DataFunctionArgs) { await requireAdmin(request) diff --git a/app/routes/admin+/cache_.sqlite.$cacheKey.ts b/app/routes/admin+/cache_.sqlite.$cacheKey.ts index 5e8ea4ca0..aee94d3cd 100644 --- a/app/routes/admin+/cache_.sqlite.$cacheKey.ts +++ b/app/routes/admin+/cache_.sqlite.$cacheKey.ts @@ -2,9 +2,9 @@ import type { DataFunctionArgs } from '@remix-run/node' import { json } from '@remix-run/node' import invariant from 'tiny-invariant' import { getAllInstances, getInstanceInfo } from 'litefs-js' -import { ensureInstance } from 'litefs-js/remix' -import { cache } from '~/utils/cache.server' -import { requireAdmin } from '~/utils/permissions.server' +import { ensureInstance } from 'litefs-js/remix.js' +import { cache } from '~/utils/cache.server.ts' +import { requireAdmin } from '~/utils/permissions.server.ts' export async function loader({ request, params }: DataFunctionArgs) { await requireAdmin(request) diff --git a/app/routes/admin+/cache_.sqlite.tsx b/app/routes/admin+/cache_.sqlite.tsx index a64f4745a..e82782eda 100644 --- a/app/routes/admin+/cache_.sqlite.tsx +++ b/app/routes/admin+/cache_.sqlite.tsx @@ -2,7 +2,7 @@ import type { DataFunctionArgs } from '@remix-run/node' import { json, redirect } from '@remix-run/node' import { z } from 'zod' import { getInstanceInfo, getInternalInstanceDomain } from 'litefs-js' -import { cache } from '~/utils/cache.server' +import { cache } from '~/utils/cache.server.ts' export async function action({ request }: DataFunctionArgs) { const { currentIsPrimary, primaryInstance } = await getInstanceInfo() diff --git a/app/routes/me.tsx b/app/routes/me.tsx index 0086369a5..95039eb81 100644 --- a/app/routes/me.tsx +++ b/app/routes/me.tsx @@ -1,6 +1,6 @@ import { redirect, type DataFunctionArgs } from '@remix-run/node' -import { authenticator, requireUserId } from '~/utils/auth.server' -import { prisma } from '~/utils/db.server' +import { authenticator, requireUserId } from '~/utils/auth.server.ts' +import { prisma } from '~/utils/db.server.ts' export async function loader({ request }: DataFunctionArgs) { const userId = await requireUserId(request) diff --git a/app/routes/resources+/delete-image.test.tsx b/app/routes/resources+/delete-image.test.tsx index 98ee6df02..a174670f2 100644 --- a/app/routes/resources+/delete-image.test.tsx +++ b/app/routes/resources+/delete-image.test.tsx @@ -3,12 +3,12 @@ */ import { faker } from '@faker-js/faker' import fs from 'fs' -import { createPassword, createUser } from 'prisma/seed-utils' -import { BASE_URL, getUserSetCookieHeader } from 'tests/vitest-utils' +import { createPassword, createUser } from 'prisma/seed-utils.ts' +import { BASE_URL, getUserSetCookieHeader } from 'tests/vitest-utils.ts' import invariant from 'tiny-invariant' import { expect } from 'vitest' -import { prisma } from '~/utils/db.server' -import { ROUTE_PATH, action } from './delete-image' +import { prisma } from '~/utils/db.server.ts' +import { ROUTE_PATH, action } from './delete-image.tsx' const RESOURCE_URL = `${BASE_URL}${ROUTE_PATH}` diff --git a/app/routes/resources+/delete-image.tsx b/app/routes/resources+/delete-image.tsx index 6a06901c6..87da90a87 100644 --- a/app/routes/resources+/delete-image.tsx +++ b/app/routes/resources+/delete-image.tsx @@ -1,8 +1,8 @@ import { parse } from '@conform-to/zod' import { json, type DataFunctionArgs } from '@remix-run/node' import { z } from 'zod' -import { requireUserId } from '~/utils/auth.server' -import { prisma } from '~/utils/db.server' +import { requireUserId } from '~/utils/auth.server.ts' +import { prisma } from '~/utils/db.server.ts' export const ROUTE_PATH = '/resources/delete-image' diff --git a/app/routes/resources+/delete-note.tsx b/app/routes/resources+/delete-note.tsx index 5be9b5601..8b8c14e1c 100644 --- a/app/routes/resources+/delete-note.tsx +++ b/app/routes/resources+/delete-note.tsx @@ -1,11 +1,11 @@ import { json, type DataFunctionArgs, redirect } from '@remix-run/node' import { useFetcher } from '@remix-run/react' -import { Button, ErrorList } from '~/utils/forms' +import { Button, ErrorList } from '~/utils/forms.tsx' import { useForm } from '@conform-to/react' import { getFieldsetConstraint, parse } from '@conform-to/zod' import { z } from 'zod' -import { requireUserId } from '~/utils/auth.server' -import { prisma } from '~/utils/db.server' +import { requireUserId } from '~/utils/auth.server.ts' +import { prisma } from '~/utils/db.server.ts' const DeleteFormSchema = z.object({ noteId: z.string(), diff --git a/app/routes/resources+/file.$fileId.tsx b/app/routes/resources+/file.$fileId.tsx index ea8025141..f8bd00cde 100644 --- a/app/routes/resources+/file.$fileId.tsx +++ b/app/routes/resources+/file.$fileId.tsx @@ -1,5 +1,5 @@ import { type DataFunctionArgs } from '@remix-run/node' -import { prisma } from '~/utils/db.server' +import { prisma } from '~/utils/db.server.ts' export async function loader({ params }: DataFunctionArgs) { const image = await prisma.image.findUnique({ diff --git a/app/routes/resources+/healthcheck.tsx b/app/routes/resources+/healthcheck.tsx index 1f5771b61..6b0b48549 100644 --- a/app/routes/resources+/healthcheck.tsx +++ b/app/routes/resources+/healthcheck.tsx @@ -1,7 +1,7 @@ // learn more: https://fly.io/docs/reference/configuration/#services-http_checks import { type DataFunctionArgs } from '@remix-run/node' -import { prisma } from '~/utils/db.server' +import { prisma } from '~/utils/db.server.ts' export async function loader({ request }: DataFunctionArgs) { const host = diff --git a/app/routes/resources+/image-upload.tsx b/app/routes/resources+/image-upload.tsx index fe71b806f..4d4ff83be 100644 --- a/app/routes/resources+/image-upload.tsx +++ b/app/routes/resources+/image-upload.tsx @@ -6,7 +6,7 @@ import { } from '@remix-run/node' import { useFetcher } from '@remix-run/react' import invariant from 'tiny-invariant' -import { prisma } from '~/utils/db.server' +import { prisma } from '~/utils/db.server.ts' const MAX_SIZE = 1024 * 1024 * 5 // 5MB diff --git a/app/routes/resources+/login.tsx b/app/routes/resources+/login.tsx index 49b29ea0c..fc566a564 100644 --- a/app/routes/resources+/login.tsx +++ b/app/routes/resources+/login.tsx @@ -5,12 +5,12 @@ import { Link, useFetcher } from '@remix-run/react' import { AuthorizationError } from 'remix-auth' import { FormStrategy } from 'remix-auth-form' import { z } from 'zod' -import { authenticator } from '~/utils/auth.server' -import { Button, CheckboxField, ErrorList, Field } from '~/utils/forms' -import { safeRedirect } from '~/utils/misc' -import { commitSession, getSession } from '~/utils/session.server' -import { passwordSchema, usernameSchema } from '~/utils/user-validation' -import { checkboxSchema } from '~/utils/zod-extensions' +import { authenticator } from '~/utils/auth.server.ts' +import { Button, CheckboxField, ErrorList, Field } from '~/utils/forms.tsx' +import { safeRedirect } from '~/utils/misc.ts' +import { commitSession, getSession } from '~/utils/session.server.ts' +import { passwordSchema, usernameSchema } from '~/utils/user-validation.ts' +import { checkboxSchema } from '~/utils/zod-extensions.ts' export const LoginFormSchema = z.object({ username: usernameSchema, diff --git a/app/routes/resources+/note-editor.tsx b/app/routes/resources+/note-editor.tsx index 7bbfdae82..e037776c8 100644 --- a/app/routes/resources+/note-editor.tsx +++ b/app/routes/resources+/note-editor.tsx @@ -3,9 +3,9 @@ import { getFieldsetConstraint, parse } from '@conform-to/zod' import { json, redirect, type DataFunctionArgs } from '@remix-run/node' import { useFetcher } from '@remix-run/react' import { z } from 'zod' -import { requireUserId } from '~/utils/auth.server' -import { prisma } from '~/utils/db.server' -import { Button, ErrorList, Field, TextareaField } from '~/utils/forms' +import { requireUserId } from '~/utils/auth.server.ts' +import { prisma } from '~/utils/db.server.ts' +import { Button, ErrorList, Field, TextareaField } from '~/utils/forms.tsx' export const NoteEditorSchema = z.object({ id: z.string().optional(), diff --git a/app/routes/settings+/profile.photo.tsx b/app/routes/settings+/profile.photo.tsx index 5c554d35b..ff21703ae 100644 --- a/app/routes/settings+/profile.photo.tsx +++ b/app/routes/settings+/profile.photo.tsx @@ -1,6 +1,6 @@ import { conform, useForm } from '@conform-to/react' import { getFieldsetConstraint, parse } from '@conform-to/zod' -import * as Dialog from '@radix-ui/react-dialog' +import Dialog from '@radix-ui/react-dialog/dist/index.js' import { type DataFunctionArgs, json, @@ -18,18 +18,18 @@ import { } from '@remix-run/react' import { useState } from 'react' import { z } from 'zod' -import * as deleteImageRoute from '~/routes/resources+/delete-image' -import { authenticator, requireUserId } from '~/utils/auth.server' -import { prisma } from '~/utils/db.server' -import { Button, ErrorList, LabelButton } from '~/utils/forms' -import { getUserImgSrc } from '~/utils/misc' +import * as deleteImageRoute from '~/routes/resources+/delete-image.tsx' +import { authenticator, requireUserId } from '~/utils/auth.server.ts' +import { prisma } from '~/utils/db.server.ts' +import { Button, ErrorList, LabelButton } from '~/utils/forms.tsx' +import { getUserImgSrc } from '~/utils/misc.ts' const MAX_SIZE = 1024 * 1024 * 3 // 3MB -/* +/* The preprocess call is needed because a current bug in @remix-run/web-fetch for more info see the bug (https://github.com/remix-run/web-std-io/pull/28) -and the explanation here: https://conform.guide/file-upload +and the explanation here: https://conform.guide/file-upload */ const PhotoFormSchema = z.object({ photoFile: z.preprocess( diff --git a/app/routes/settings+/profile.tsx b/app/routes/settings+/profile.tsx index 089e680db..7f8fe95f2 100644 --- a/app/routes/settings+/profile.tsx +++ b/app/routes/settings+/profile.tsx @@ -16,16 +16,16 @@ import { getPasswordHash, requireUserId, verifyLogin, -} from '~/utils/auth.server' -import { prisma } from '~/utils/db.server' -import { Button, ErrorList, Field } from '~/utils/forms' -import { getUserImgSrc } from '~/utils/misc' +} from '~/utils/auth.server.ts' +import { prisma } from '~/utils/db.server.ts' +import { Button, ErrorList, Field } from '~/utils/forms.tsx' +import { getUserImgSrc } from '~/utils/misc.ts' import { emailSchema, nameSchema, passwordSchema, usernameSchema, -} from '~/utils/user-validation' +} from '~/utils/user-validation.ts' const ProfileFormSchema = z.object({ name: nameSchema.optional(), diff --git a/app/routes/users+/$username.tsx b/app/routes/users+/$username.tsx index 45f6d2535..c61f9039a 100644 --- a/app/routes/users+/$username.tsx +++ b/app/routes/users+/$username.tsx @@ -5,12 +5,12 @@ import { } from '@remix-run/node' import { useLoaderData } from '@remix-run/react' import invariant from 'tiny-invariant' -import { GeneralErrorBoundary } from '~/components/error-boundary' -import { Spacer } from '~/components/spacer' -import { prisma } from '~/utils/db.server' -import { ButtonLink } from '~/utils/forms' -import { getUserImgSrc } from '~/utils/misc' -import { useOptionalUser } from '~/utils/user' +import { GeneralErrorBoundary } from '~/components/error-boundary.tsx' +import { Spacer } from '~/components/spacer.tsx' +import { prisma } from '~/utils/db.server.ts' +import { ButtonLink } from '~/utils/forms.tsx' +import { getUserImgSrc } from '~/utils/misc.ts' +import { useOptionalUser } from '~/utils/user.ts' export async function loader({ params }: DataFunctionArgs) { invariant(params.username, 'Missing username') diff --git a/app/routes/users+/$username_+/notes.$noteId.tsx b/app/routes/users+/$username_+/notes.$noteId.tsx index 6ea12b2ac..c95661242 100644 --- a/app/routes/users+/$username_+/notes.$noteId.tsx +++ b/app/routes/users+/$username_+/notes.$noteId.tsx @@ -1,10 +1,10 @@ import { json, type DataFunctionArgs } from '@remix-run/node' import { useLoaderData } from '@remix-run/react' -import { GeneralErrorBoundary } from '~/components/error-boundary' -import { DeleteNote } from '~/routes/resources+/delete-note' -import { getUserId } from '~/utils/auth.server' -import { prisma } from '~/utils/db.server' -import { ButtonLink } from '~/utils/forms' +import { GeneralErrorBoundary } from '~/components/error-boundary.tsx' +import { DeleteNote } from '~/routes/resources+/delete-note.tsx' +import { getUserId } from '~/utils/auth.server.ts' +import { prisma } from '~/utils/db.server.ts' +import { ButtonLink } from '~/utils/forms.tsx' export async function loader({ request, params }: DataFunctionArgs) { const userId = await getUserId(request) diff --git a/app/routes/users+/$username_+/notes.$noteId_.edit.tsx b/app/routes/users+/$username_+/notes.$noteId_.edit.tsx index 5a03b7b55..1528d3d8c 100644 --- a/app/routes/users+/$username_+/notes.$noteId_.edit.tsx +++ b/app/routes/users+/$username_+/notes.$noteId_.edit.tsx @@ -1,7 +1,7 @@ import { json, type DataFunctionArgs } from '@remix-run/node' import { useLoaderData } from '@remix-run/react' -import { NoteEditor } from '~/routes/resources+/note-editor' -import { prisma } from '~/utils/db.server' +import { NoteEditor } from '~/routes/resources+/note-editor.tsx' +import { prisma } from '~/utils/db.server.ts' export async function loader({ params }: DataFunctionArgs) { const note = await prisma.note.findUnique({ diff --git a/app/routes/users+/$username_+/notes.new.tsx b/app/routes/users+/$username_+/notes.new.tsx index 60553827b..3d4687935 100644 --- a/app/routes/users+/$username_+/notes.new.tsx +++ b/app/routes/users+/$username_+/notes.new.tsx @@ -1,4 +1,4 @@ -import { NoteEditor } from '~/routes/resources+/note-editor' +import { NoteEditor } from '~/routes/resources+/note-editor.tsx' export default function NewNoteRoute() { return diff --git a/app/routes/users+/$username_+/notes.tsx b/app/routes/users+/$username_+/notes.tsx index 027146fc6..fa41adada 100644 --- a/app/routes/users+/$username_+/notes.tsx +++ b/app/routes/users+/$username_+/notes.tsx @@ -1,10 +1,10 @@ import { useLoaderData, Outlet, NavLink, Link } from '@remix-run/react' import { json, type DataFunctionArgs } from '@remix-run/node' -import { prisma } from '~/utils/db.server' -import clsx from 'clsx' -import { GeneralErrorBoundary } from '~/components/error-boundary' -import { getUserImgSrc } from '~/utils/misc' -import { requireUserId } from '~/utils/auth.server' +import { prisma } from '~/utils/db.server.ts' +import { clsx } from 'clsx' +import { GeneralErrorBoundary } from '~/components/error-boundary.tsx' +import { getUserImgSrc } from '~/utils/misc.ts' +import { requireUserId } from '~/utils/auth.server.ts' export async function loader({ params, request }: DataFunctionArgs) { await requireUserId(request, { redirectTo: null }) diff --git a/app/utils/auth.server.ts b/app/utils/auth.server.ts index 0c1f9a4e9..93207991f 100644 --- a/app/utils/auth.server.ts +++ b/app/utils/auth.server.ts @@ -3,8 +3,8 @@ import bcrypt from 'bcryptjs' import { Authenticator } from 'remix-auth' import { FormStrategy } from 'remix-auth-form' import invariant from 'tiny-invariant' -import { prisma } from '~/utils/db.server' -import { sessionStorage } from './session.server' +import { prisma } from '~/utils/db.server.ts' +import { sessionStorage } from './session.server.ts' export type { User } diff --git a/app/utils/cache.server.ts b/app/utils/cache.server.ts index 9586ed722..82d89d9a4 100644 --- a/app/utils/cache.server.ts +++ b/app/utils/cache.server.ts @@ -13,9 +13,9 @@ import fs from 'fs' import { getInstanceInfo, getInstanceInfoSync } from 'litefs-js' import LRU from 'lru-cache' import { z } from 'zod' -import { updatePrimaryCacheValue } from '~/routes/admin+/cache_.sqlite' -import { cachifiedTimingReporter, type Timings } from './timing.server' -import { singleton } from './singleton.server' +import { updatePrimaryCacheValue } from '~/routes/admin+/cache_.sqlite.tsx' +import { cachifiedTimingReporter, type Timings } from './timing.server.ts' +import { singleton } from './singleton.server.ts' const CACHE_DATABASE_PATH = process.env.CACHE_DATABASE_PATH diff --git a/app/utils/db.server.ts b/app/utils/db.server.ts index f1caa7eaf..cdeeef362 100644 --- a/app/utils/db.server.ts +++ b/app/utils/db.server.ts @@ -1,5 +1,5 @@ import { PrismaClient } from '@prisma/client' -import { singleton } from './singleton.server' +import { singleton } from './singleton.server.ts' const prisma = singleton('prisma', () => new PrismaClient()) prisma.$connect() diff --git a/app/utils/encryption.server.test.ts b/app/utils/encryption.server.test.ts index c6a0b29be..192b67a57 100644 --- a/app/utils/encryption.server.test.ts +++ b/app/utils/encryption.server.test.ts @@ -3,7 +3,7 @@ */ import { test, expect } from 'vitest' import { faker } from '@faker-js/faker' -import { encrypt, decrypt } from './encryption.server' +import { encrypt, decrypt } from './encryption.server.ts' let originalEncryptionSecret: string diff --git a/app/utils/forms.tsx b/app/utils/forms.tsx index 728b8c618..72c5fc225 100644 --- a/app/utils/forms.tsx +++ b/app/utils/forms.tsx @@ -1,6 +1,6 @@ -import * as Checkbox from '@radix-ui/react-checkbox' +import Checkbox from '@radix-ui/react-checkbox/dist/index.js' import { Link } from '@remix-run/react' -import clsx from 'clsx' +import { clsx } from 'clsx' import React, { useId } from 'react' import styles from './forms.module.css' diff --git a/app/utils/permissions.server.ts b/app/utils/permissions.server.ts index 1749b9514..d1f355870 100644 --- a/app/utils/permissions.server.ts +++ b/app/utils/permissions.server.ts @@ -1,6 +1,6 @@ import { json } from '@remix-run/node' -import { requireUserId } from './auth.server' -import { prisma } from './db.server' +import { requireUserId } from './auth.server.ts' +import { prisma } from './db.server.ts' export async function requireUserWithPermission( name: string, diff --git a/app/utils/user.ts b/app/utils/user.ts index 537f4dad4..3733e4265 100644 --- a/app/utils/user.ts +++ b/app/utils/user.ts @@ -1,6 +1,6 @@ import { type SerializeFrom } from '@remix-run/node' import { useRouteLoaderData } from '@remix-run/react' -import { type loader as rootLoader } from '~/root' +import { type loader as rootLoader } from '~/root.tsx' function isUser(user: any): user is SerializeFrom['user'] { return user && typeof user === 'object' && typeof user.id === 'string' diff --git a/index.js b/index.js index d04518549..89752eb22 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,11 @@ -require('dotenv/config') +import 'dotenv/config' if (process.env.MOCKS === 'true') { - require('./mocks') + await import('./mocks/index.ts') } if (process.env.NODE_ENV === 'production') { - require('./server-build') + await import('./server-build/index.js') } else { - require('./server') + await import('./server/index.ts') } diff --git a/mocks/index.ts b/mocks/index.ts index 35888f964..e8814b949 100644 --- a/mocks/index.ts +++ b/mocks/index.ts @@ -1,7 +1,7 @@ import { rest } from 'msw' import { setupServer } from 'msw/node' import closeWithGrace from 'close-with-grace' -import { requiredHeader, writeEmail } from './utils' +import { requiredHeader, writeEmail } from './utils.ts' const handlers = [ process.env.REMIX_DEV_HTTP_ORIGIN diff --git a/mocks/utils.ts b/mocks/utils.ts index 990becfdd..040330148 100644 --- a/mocks/utils.ts +++ b/mocks/utils.ts @@ -1,7 +1,9 @@ import fsExtra from 'fs-extra' import path from 'path' +import { fileURLToPath } from 'url' import { z } from 'zod' +const __dirname = path.dirname(fileURLToPath(import.meta.url)) const fixturesDirPath = path.join(__dirname, `./fixtures`) export async function readFixture(subdir: string, name: string) { diff --git a/other/build-server.ts b/other/build-server.ts index 29e35c58b..462bfc9bf 100644 --- a/other/build-server.ts +++ b/other/build-server.ts @@ -1,8 +1,12 @@ import fsExtra from 'fs-extra' import path from 'path' +import { fileURLToPath } from 'url' import glob from 'glob' -import pkg from '../package.json' +import esbuild from 'esbuild' +const pkg = fsExtra.readJsonSync(path.join(process.cwd(), 'package.json')) + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) const here = (...s: Array) => path.join(__dirname, ...s) const globsafe = (s: string) => s.replace(/\\/g, '/') @@ -25,13 +29,13 @@ for (const file of allFiles) { console.log() console.log('building...') -require('esbuild') +esbuild .build({ entryPoints: glob.sync(globsafe(here('../server/**/*.+(ts|js|tsx|jsx)'))), outdir: here('../server-build'), target: [`node${pkg.engines.node}`], platform: 'node', - format: 'cjs', + format: 'esm', logLevel: 'info', }) .catch((error: unknown) => { diff --git a/other/start.js b/other/start.js index b2aae9363..8003182b6 100644 --- a/other/start.js +++ b/other/start.js @@ -1,5 +1,5 @@ -const { spawn } = require('child_process') -const { getInstanceInfo } = require('litefs-js') +import { spawn } from 'child_process' +import { getInstanceInfo } from 'litefs-js' async function go() { const { currentInstance, currentIsPrimary, primaryInstance } = diff --git a/other/test-setup/global-setup.ts b/other/test-setup/global-setup.ts index ae00236e3..c02b7ef0f 100644 --- a/other/test-setup/global-setup.ts +++ b/other/test-setup/global-setup.ts @@ -1,7 +1,7 @@ import path from 'path' import { execaCommand } from 'execa' import fsExtra from 'fs-extra' -import { BASE_DATABASE_PATH, BASE_DATABASE_URL } from './paths' +import { BASE_DATABASE_PATH, BASE_DATABASE_URL } from './paths.ts' export async function setup() { await fsExtra.ensureDir(path.dirname(BASE_DATABASE_PATH)) diff --git a/other/test-setup/matchers.cjs b/other/test-setup/matchers.cjs new file mode 100644 index 000000000..aa7f43f67 --- /dev/null +++ b/other/test-setup/matchers.cjs @@ -0,0 +1,5 @@ +// matchers types are missing when import as default to ESM module +export { + default as matchers, + TestingLibraryMatchers, +} from '@testing-library/jest-dom/matchers.js' diff --git a/other/test-setup/setup-env-vars.ts b/other/test-setup/setup-env-vars.ts index 6d536bb8e..681bf76e7 100644 --- a/other/test-setup/setup-env-vars.ts +++ b/other/test-setup/setup-env-vars.ts @@ -1,4 +1,4 @@ -import { DATABASE_PATH, DATABASE_URL } from './paths' +import { DATABASE_PATH, DATABASE_URL } from './paths.ts' process.env.DATABASE_PATH = DATABASE_PATH process.env.DATABASE_URL = DATABASE_URL diff --git a/other/test-setup/setup-test-env.ts b/other/test-setup/setup-test-env.ts index 17b85b489..c6a3d7d61 100644 --- a/other/test-setup/setup-test-env.ts +++ b/other/test-setup/setup-test-env.ts @@ -1,13 +1,11 @@ -import './setup-env-vars' +import './setup-env-vars.ts' import { installGlobals } from '@remix-run/node' -import matchers, { - type TestingLibraryMatchers, -} from '@testing-library/jest-dom/matchers' +import { matchers, type TestingLibraryMatchers } from './matchers.cjs' import 'dotenv/config' import fs from 'fs' -import { BASE_DATABASE_PATH, DATABASE_PATH } from './paths' -import { deleteAllData } from './utils' -import { prisma } from '~/utils/db.server' +import { BASE_DATABASE_PATH, DATABASE_PATH } from './paths.ts' +import { deleteAllData } from './utils.ts' +import { prisma } from '~/utils/db.server.ts' declare global { namespace Vi { diff --git a/other/test-setup/vitejs-plugin-react.cjs b/other/test-setup/vitejs-plugin-react.cjs new file mode 100644 index 000000000..ff6626291 --- /dev/null +++ b/other/test-setup/vitejs-plugin-react.cjs @@ -0,0 +1,2 @@ +// react types are missing when import as default to ESM module +export { default as react } from '@vitejs/plugin-react' diff --git a/package.json b/package.json index 478b76af7..e7d44b4de 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "license": "MIT", "epic-stack": true, "author": "Kent C. Dodds (https://kentcdodds.com/)", + "type": "module", "scripts": { "build": "run-s build:*", "build:remix": "remix build", diff --git a/postcss.config.js b/postcss.config.js index ded0ebd84..5ebad5143 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,4 +1,4 @@ -module.exports = { +export default { plugins: { 'tailwindcss/nesting': {}, tailwindcss: {}, diff --git a/prisma/seed.ts b/prisma/seed.ts index b745d90b5..e013c2e5f 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,9 +1,9 @@ import fs from 'fs' import { faker } from '@faker-js/faker' -import { createPassword, createUser } from './seed-utils' -import { prisma } from '~/utils/db.server' -import { deleteAllData } from '../other/test-setup/utils' -import { getPasswordHash } from '~/utils/auth.server' +import { createPassword, createUser } from './seed-utils.ts' +import { prisma } from '~/utils/db.server.ts' +import { deleteAllData } from '../other/test-setup/utils.ts' +import { getPasswordHash } from '~/utils/auth.server.ts' async function seed() { console.log('🌱 Seeding...') diff --git a/remix.config.js b/remix.config.js index c8e2c700c..199c7c709 100644 --- a/remix.config.js +++ b/remix.config.js @@ -1,12 +1,14 @@ -const { flatRoutes } = require('remix-flat-routes') +import { flatRoutes } from 'remix-flat-routes' /** * @type {import('@remix-run/dev').AppConfig} */ -module.exports = { +export default { cacheDirectory: './node_modules/.cache/remix', ignoredRouteFiles: ['**/*'], - serverModuleFormat: 'cjs', + serverModuleFormat: 'esm', + serverPlatform: 'node', + tailwind: true, postcss: true, watchPaths: ['./tailwind.config.ts'], future: { @@ -14,10 +16,7 @@ module.exports = { v2_errorBoundary: true, v2_normalizeFormMethod: true, v2_routeConvention: true, - unstable_dev: { - port: '', // let it choose a random port - appServerPort: process.env.APP_SERVER_PORT || process.env.PORT || 3000, - }, + unstable_dev: true, }, routes: async defineRoutes => { return flatRoutes('routes', defineRoutes, { diff --git a/remix.init/index.js b/remix.init/index.js index d126fa8ad..e5e36c03d 100644 --- a/remix.init/index.js +++ b/remix.init/index.js @@ -1,94 +1,4 @@ -const { execSync } = require('child_process') -const crypto = require('crypto') -const fs = require('fs/promises') -const path = require('path') - -const escapeRegExp = string => - // $& means the whole matched string - string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - -const getRandomString = length => crypto.randomBytes(length).toString('hex') - -const main = async ({ isTypeScript, rootDirectory }) => { - if (!isTypeScript) { - // not throwing an error because the stack trace doesn't do anything to help the user - throw `Sorry, this template only supports TypeScript. Please run the command again and select "TypeScript". Learn more about why in https://github.com/epicweb-dev/epic-stack/blob/main/decisions/01-typescript-only.md` - } - 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 PKG_PATH = path.join(rootDirectory, 'package.json') - - const appNameRegex = escapeRegExp('epic-stack-template') - - const DIR_NAME = path.basename(rootDirectory) - const SUFFIX = getRandomString(2) - - const APP_NAME = (DIR_NAME + '-' + SUFFIX) - // get rid of anything that's not allowed in an app name - .replace(/[^a-zA-Z0-9-_]/g, '-') - - const [flyTomlContent, readme, env, packageJson] = await Promise.all([ - fs.readFile(FLY_TOML_PATH, 'utf-8'), - fs.readFile(README_PATH, 'utf-8'), - fs.readFile(EXAMPLE_ENV_PATH, 'utf-8'), - fs.readFile(PKG_PATH, 'utf-8'), - ]) - - const newEnv = env - .replace(/^SESSION_SECRET=.*$/m, `SESSION_SECRET="${getRandomString(16)}"`) - .replace( - /^ENCRYPTION_SECRET=.*$/m, - `ENCRYPTION_SECRET="${getRandomString(16)}"`, - ) - .replace( - /^INTERNAL_COMMAND_TOKEN=.*$/m, - `INTERNAL_COMMAND_TOKEN="${getRandomString(16)}"`, - ) - - const newFlyTomlContent = flyTomlContent.replace( - new RegExp(appNameRegex, 'g'), - APP_NAME, - ) - - const newReadme = readme.replace(new RegExp(appNameRegex, 'g'), APP_NAME) - - const newPackageJson = packageJson.replace( - new RegExp(appNameRegex, 'g'), - APP_NAME, - ) - - const fileOperationPromises = [ - fs.writeFile(FLY_TOML_PATH, newFlyTomlContent), - fs.writeFile(README_PATH, newReadme), - fs.writeFile(ENV_PATH, newEnv), - fs.writeFile(PKG_PATH, newPackageJson), - fs.copyFile( - path.join(rootDirectory, 'remix.init', 'gitignore'), - path.join(rootDirectory, '.gitignore'), - ), - fs.rm(path.join(rootDirectory, 'LICENSE.md')), - fs.rm(path.join(rootDirectory, 'CONTRIBUTING.md')), - fs.rm(path.join(rootDirectory, 'decisions'), { recursive: true }), - fs.rm(path.join(rootDirectory, 'docs'), { recursive: true }), - ] - - await Promise.all(fileOperationPromises) - - execSync('npm run setup', { cwd: rootDirectory, stdio: 'inherit' }) - - execSync('npm run format --loglevel warn', { - cwd: rootDirectory, - stdio: 'inherit', - }) - - console.log( - `Setup is complete. You're now ready to rock and roll 🐨 - -Start development with \`npm run dev\` - `.trim(), - ) +module.exports = async (...args) => { + const { default: main } = await import('./index.mjs') + await main(...args) } - -module.exports = main diff --git a/remix.init/index.mjs b/remix.init/index.mjs new file mode 100644 index 000000000..e17011a0d --- /dev/null +++ b/remix.init/index.mjs @@ -0,0 +1,94 @@ +import { execSync } from 'child_process' +import crypto from 'crypto' +import fs from 'fs/promises' +import path from 'path' + +const escapeRegExp = string => + // $& means the whole matched string + string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + +const getRandomString = length => crypto.randomBytes(length).toString('hex') + +const main = async ({ isTypeScript, rootDirectory }) => { + if (!isTypeScript) { + // not throwing an error because the stack trace doesn't do anything to help the user + throw `Sorry, this template only supports TypeScript. Please run the command again and select "TypeScript". Learn more about why in https://github.com/epicweb-dev/epic-stack/blob/main/decisions/01-typescript-only.md` + } + 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 PKG_PATH = path.join(rootDirectory, 'package.json') + + const appNameRegex = escapeRegExp('epic-stack-template') + + const DIR_NAME = path.basename(rootDirectory) + const SUFFIX = getRandomString(2) + + const APP_NAME = (DIR_NAME + '-' + SUFFIX) + // get rid of anything that's not allowed in an app name + .replace(/[^a-zA-Z0-9-_]/g, '-') + + const [flyTomlContent, readme, env, packageJson] = await Promise.all([ + fs.readFile(FLY_TOML_PATH, 'utf-8'), + fs.readFile(README_PATH, 'utf-8'), + fs.readFile(EXAMPLE_ENV_PATH, 'utf-8'), + fs.readFile(PKG_PATH, 'utf-8'), + ]) + + const newEnv = env + .replace(/^SESSION_SECRET=.*$/m, `SESSION_SECRET="${getRandomString(16)}"`) + .replace( + /^ENCRYPTION_SECRET=.*$/m, + `ENCRYPTION_SECRET="${getRandomString(16)}"`, + ) + .replace( + /^INTERNAL_COMMAND_TOKEN=.*$/m, + `INTERNAL_COMMAND_TOKEN="${getRandomString(16)}"`, + ) + + const newFlyTomlContent = flyTomlContent.replace( + new RegExp(appNameRegex, 'g'), + APP_NAME, + ) + + const newReadme = readme.replace(new RegExp(appNameRegex, 'g'), APP_NAME) + + const newPackageJson = packageJson.replace( + new RegExp(appNameRegex, 'g'), + APP_NAME, + ) + + const fileOperationPromises = [ + fs.writeFile(FLY_TOML_PATH, newFlyTomlContent), + fs.writeFile(README_PATH, newReadme), + fs.writeFile(ENV_PATH, newEnv), + fs.writeFile(PKG_PATH, newPackageJson), + fs.copyFile( + path.join(rootDirectory, 'remix.init', 'gitignore'), + path.join(rootDirectory, '.gitignore'), + ), + fs.rm(path.join(rootDirectory, 'LICENSE.md')), + fs.rm(path.join(rootDirectory, 'CONTRIBUTING.md')), + fs.rm(path.join(rootDirectory, 'decisions'), { recursive: true }), + fs.rm(path.join(rootDirectory, 'docs'), { recursive: true }), + ] + + await Promise.all(fileOperationPromises) + + execSync('npm run setup', { cwd: rootDirectory, stdio: 'inherit' }) + + execSync('npm run format --loglevel warn', { + cwd: rootDirectory, + stdio: 'inherit', + }) + + console.log( + `Setup is complete. You're now ready to rock and roll 🐨 + +Start development with \`npm run dev\` + `.trim(), + ) +} + +export default main diff --git a/server/index.ts b/server/index.ts index 000499c70..c49e95ac1 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,4 +1,5 @@ import path from 'path' +import { pathToFileURL } from 'url' import express from 'express' import chokidar from 'chokidar' import compression from 'compression' @@ -8,7 +9,8 @@ import closeWithGrace from 'close-with-grace' import { createRequestHandler } from '@remix-run/express' import { broadcastDevReady } from '@remix-run/node' -const BUILD_DIR = path.join(process.cwd(), 'build') +const BUILD_DIR = path.join(process.cwd(), 'build', 'index.js') +const BUILD_DIR_FILE_URL = pathToFileURL(BUILD_DIR).href async function start() { const { default: getPort, portNumbers } = await import('get-port') @@ -30,19 +32,20 @@ async function start() { // more aggressive with this caching. app.use(express.static('public', { maxAge: '1h' })) + morgan.token('url', (req, res) => decodeURIComponent(req.url ?? '')) app.use(morgan('tiny')) app.all( '*', process.env.NODE_ENV === 'development' - ? (req, res, next) => { + ? async (req, res, next) => { return createRequestHandler({ - build: require(BUILD_DIR), + build: await import(BUILD_DIR_FILE_URL), mode: process.env.NODE_ENV, })(req, res, next) } : createRequestHandler({ - build: require(BUILD_DIR), + build: await import(BUILD_DIR_FILE_URL), mode: process.env.NODE_ENV, }), ) @@ -88,7 +91,7 @@ ${chalk.bold('Press Ctrl+C to stop')} ) if (process.env.NODE_ENV === 'development') { - broadcastDevReady(require(BUILD_DIR)) + notifyRemixDevReady() } }) @@ -101,17 +104,19 @@ ${chalk.bold('Press Ctrl+C to stop')} start() +async function notifyRemixDevReady() { + const build = await import(`${BUILD_DIR_FILE_URL}?update=${Date.now()}`) + broadcastDevReady(build) +} + // during dev, we'll keep the build module up to date with the changes if (process.env.NODE_ENV === 'development') { - const watcher = chokidar.watch(BUILD_DIR, { - ignored: ['**/**.map'], - }) - watcher.on('all', () => { - for (const key in require.cache) { - if (key.startsWith(BUILD_DIR)) { - delete require.cache[key] - } - } - broadcastDevReady(require(BUILD_DIR)) - }) + // avoid watching the folder itself, just watch its content + const watcher = chokidar.watch( + `${path.dirname(BUILD_DIR).replace(/\\/g, '/')}/**.*`, + { + ignored: ['**/**.map'], + }, + ) + watcher.on('all', notifyRemixDevReady) } diff --git a/tailwind.config.ts b/tailwind.config.ts index 67249fe7d..94ad3e3ce 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,8 +1,8 @@ import type { Config } from 'tailwindcss' -import defaultTheme from 'tailwindcss/defaultTheme' +import defaultTheme from 'tailwindcss/defaultTheme.js' import tailwindcssRadix from 'tailwindcss-radix' -module.exports = { +export default { content: ['./app/**/*.{ts,tsx,jsx,js}'], darkMode: 'class', theme: { diff --git a/tests/e2e/onboarding.test.ts b/tests/e2e/onboarding.test.ts index c0b88af33..22408f9cd 100644 --- a/tests/e2e/onboarding.test.ts +++ b/tests/e2e/onboarding.test.ts @@ -6,7 +6,7 @@ import { insertNewUser, readEmail, test, -} from '../playwright-utils' +} from '../playwright-utils.ts' const urlRegex = /(?https?:\/\/[^\s$.?#].[^\s]*)/ function extractUrl(text: string) { diff --git a/tests/e2e/settings-profile.test.ts b/tests/e2e/settings-profile.test.ts index 0a11f4a44..db314780a 100644 --- a/tests/e2e/settings-profile.test.ts +++ b/tests/e2e/settings-profile.test.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker' -import { expect, insertNewUser, test } from '../playwright-utils' -import { createUser } from '../../prisma/seed-utils' -import { verifyLogin } from '~/utils/auth.server' +import { expect, insertNewUser, test } from '../playwright-utils.ts' +import { createUser } from '../../prisma/seed-utils.ts' +import { verifyLogin } from '~/utils/auth.server.ts' test('Users can update their basic info', async ({ login, page }) => { await login() diff --git a/tests/playwright-utils.ts b/tests/playwright-utils.ts index 05186f132..45a9a7ed9 100644 --- a/tests/playwright-utils.ts +++ b/tests/playwright-utils.ts @@ -1,15 +1,15 @@ import { test as base, type Page } from '@playwright/test' import { parse } from 'cookie' -import { authenticator, getPasswordHash } from '~/utils/auth.server' -import { prisma } from '~/utils/db.server' -import { commitSession, getSession } from '~/utils/session.server' -import { createUser } from '../prisma/seed-utils' +import { authenticator, getPasswordHash } from '~/utils/auth.server.ts' +import { prisma } from '~/utils/db.server.ts' +import { commitSession, getSession } from '~/utils/session.server.ts' +import { createUser } from '../prisma/seed-utils.ts' export const dataCleanup = { users: new Set(), } -export { readEmail } from '../mocks/utils' +export { readEmail } from '../mocks/utils.ts' export function deleteUserByUsername(username: string) { return prisma.user.delete({ where: { username } }) diff --git a/tests/vitest-utils.ts b/tests/vitest-utils.ts index b572d91c9..0b79367a0 100644 --- a/tests/vitest-utils.ts +++ b/tests/vitest-utils.ts @@ -1,5 +1,5 @@ -import { authenticator } from '~/utils/auth.server' -import { commitSession, getSession } from '~/utils/session.server' +import { authenticator } from '~/utils/auth.server.ts' +import { commitSession, getSession } from '~/utils/session.server.ts' export const BASE_URL = 'https://epicstack.dev' diff --git a/tsconfig.json b/tsconfig.json index c955b79e3..c4db12e5d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,15 +6,15 @@ "./other/test-setup/setup-test-env.ts" ], "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2021"], + "lib": ["DOM", "DOM.Iterable", "ES2022"], "types": ["vitest/globals"], "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", - "module": "CommonJS", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "resolveJsonModule": true, - "target": "ES2019", + "target": "ES2022", "strict": true, "noImplicitAny": true, "allowJs": true, @@ -22,9 +22,11 @@ "baseUrl": ".", "paths": { "~/*": ["./app/*"], + "prisma/*": ["./prisma/*"], "tests/*": ["./tests/*"] }, "skipLibCheck": true, + "allowImportingTsExtensions": true, // Remix takes care of building everything in `remix build`. "noEmit": true diff --git a/vitest.config.ts b/vitest.config.ts index a936c8c47..f20d471c2 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,7 @@ /// /// -import react from '@vitejs/plugin-react' +import { react } from './other/test-setup/vitejs-plugin-react.cjs' import { defineConfig } from 'vite' import tsconfigPaths from 'vite-tsconfig-paths'