@@ -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'