Closed20

remixのscaffold repositoryを作る

tmtk75tmtk75
  • remix
  • tailwind
  • chakra-ui ant design
  • prisma
  • biome
  • zod
  • vitest
  • sass
    あたりが入ったscaffold用repositoryを作っておこうと思う。

Dockerfile, composeも追加。

tmtk75tmtk75

https://remix.run/docs/en/main/start/quickstart#quick-start

npx create-remix@latest

何はともあれ。

Need to install the following packages:
  create-remix@2.9.1
Ok to proceed? (y) y

 remix   v2.9.1 💿 Let's build a better website...

   dir   Where should we create your new project?
         ./my-remix-scaffold

      ◼  Using basic template See https://remix.run/guides/templates for more
      ✔  Template copied

   git   Initialize a new git repository?
         Yes

  deps   Install dependencies with npm?
         No
      ◼  Skipping install step. Remember to install dependencies after setup with npm install.

      ✔  Git initialized

  done   That's it!

         Enter your project directory using cd ./my-remix-scaffold
         Check out README.md for development and deploy instructions.

         Join the community at https://rmx.as/discord
tmtk75tmtk75

pnpmを使うようにする。

% git diff
diff --git a/package.json b/package.json
index c11e71e..3b56e79 100644
--- a/package.json
+++ b/package.json
@@ -36,5 +36,6 @@
   },
   "engines": {
     "node": ">=18.0.0"
-  }
-}
\ No newline at end of file
+  },
+  "packageManager": "pnpm@9.0.6"
+}

% corepack enable

% pnpm i
tmtk75tmtk75

biomeをいれる。eslintを削除。.eslintrc.cjsも削除。
https://biomejs.dev

pnpm i -D --save-exact @biomejs/biome
diff --git a/package.json b/package.json
index 7bea9a7..045cebd 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
   "scripts": {
     "build": "remix vite:build",
     "dev": "remix vite:dev",
-    "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
+    "lint": "biome lint --apply ./app",
     "start": "remix-serve ./build/server/index.js",
     "typecheck": "tsc"
   },
@@ -19,18 +19,13 @@
     "react-dom": "^18.2.0"
   },
   "devDependencies": {
+    "@biomejs/biome": "1.7.2",
     "@remix-run/dev": "^2.9.1",
     "@types/react": "^18.2.20",
     "@types/react-dom": "^18.2.7",
     "@typescript-eslint/eslint-plugin": "^6.7.4",
     "@typescript-eslint/parser": "^6.7.4",
     "autoprefixer": "^10.4.19",
-    "eslint": "^8.38.0",
-    "eslint-import-resolver-typescript": "^3.6.1",
-    "eslint-plugin-import": "^2.28.1",
-    "eslint-plugin-jsx-a11y": "^6.7.1",
-    "eslint-plugin-react": "^7.33.2",
-    "eslint-plugin-react-hooks": "^4.6.0",
     "postcss": "^8.4.38",
     "tailwindcss": "^3.4.3",
     "typescript": "^5.1.6",
tmtk75tmtk75

vitestをいれる。失敗するtestコードを足してみて、npx vitestで期待通り失敗することを確認。

$ pnpm i -D @remix-run/testing
$ pnpm i -D vitest
diff --git a/test/sample.test.ts b/test/sample.test.ts
new file mode 100644
index 0000000..af271be
--- /dev/null
+++ b/test/sample.test.ts
@@ -0,0 +1,7 @@
+import { describe, test, expect } from "vitest";
+
+describe("", () => {
+  test("", () => {
+    expect(1).toBe(2);
+  });
+});
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..6c71175
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,13 @@
+import { resolve } from "node:path";
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+  test: {
+    globals: true,
+  },
+  resolve: {
+    alias: {
+      "~": resolve(__dirname, "app"),
+    },
+  },
+});
tmtk75tmtk75

Ant Designを入れてみる。実は初めて使う。
https://ant.design/docs/react/introduce#using-npm-or-yarn-or-pnpm-or-bun

これだけで普通に動いてしまった。見た目も

$ pnpm install antd -D
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
index 4622d1c..8e7bed4 100644
--- a/app/routes/_index.tsx
+++ b/app/routes/_index.tsx
@@ -1,4 +1,5 @@
 import type { MetaFunction } from "@remix-run/node";
+import { DatePicker } from "antd";

 export const meta: MetaFunction = () => {
   return [
@@ -36,6 +37,8 @@ export default function Index() {
           </a>
         </li>
       </ul>
+
+      <DatePicker />
     </div>
   );
 }
tmtk75tmtk75

次はprisma。

https://www.prisma.io/docs/getting-started/quickstart

$ pnpm i -D prisma
$ npx prisma init --datasource-provider sqlite

モデルを足して、生成

+++ b/prisma/schema.prisma
@@ -0,0 +1,27 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+  provider = "prisma-client-js"
+}
+
+datasource db {
+  provider = "sqlite"
+  url      = env("DATABASE_URL")
+}
+
+model User {
+  id    Int     @id @default(autoincrement())
+  email String  @unique
+  name  String?
+  posts Post[]
+}
+
+model Post {
+  id        Int     @id @default(autoincrement())
+  title     String
+  content   String?
+  published Boolean @default(false)
+  author    User    @relation(fields: [authorId], references: [id])
+  authorId  Int
+}
$ npx prisma migrate dev --name init

sqliteで中身を見てみる。テーブル等出来ている。

$ sqlite3 -cmd '.d' prisma/dev.db
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "_prisma_migrations" (
    "id"                    TEXT PRIMARY KEY NOT NULL,
    "checksum"              TEXT NOT NULL,
    "finished_at"           DATETIME,
    "migration_name"        TEXT NOT NULL,
    "logs"                  TEXT,
    "rolled_back_at"        DATETIME,
    "started_at"            DATETIME NOT NULL DEFAULT current_timestamp,
    "applied_steps_count"   INTEGER UNSIGNED NOT NULL DEFAULT 0
);
INSERT INTO _prisma_migrations VALUES('41fe0212-d697-4962-8b96-3d97f8fd2799','ef7338a32149a3d4eeb4465914350049f29d68d453f9de4c865a90b128a4af18',1714695454885,'20240503001734_init',NULL,NULL,1714695454884,1);
CREATE TABLE IF NOT EXISTS "User" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "email" TEXT NOT NULL,
    "name" TEXT
);
CREATE TABLE IF NOT EXISTS "Post" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "title" TEXT NOT NULL,
    "content" TEXT,
    "published" BOOLEAN NOT NULL DEFAULT false,
    "authorId" INTEGER NOT NULL,
    CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
DELETE FROM sqlite_sequence;
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
COMMIT;
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
tmtk75tmtk75

prisma続き。
loader足してfindしてみる。リロードするとdev server起動しているコンソールに表示される。

diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
index 86983ab..9b48d3b 100644
--- a/app/routes/_index.tsx
+++ b/app/routes/_index.tsx
@@ -1,5 +1,7 @@
-import type { MetaFunction } from "@remix-run/node";
+import type { LoaderFunction, MetaFunction } from "@remix-run/node";
 import { DatePicker, Button } from "antd";
+import { PrismaClient } from "@prisma/client";
+const prisma = new PrismaClient();

 export const meta: MetaFunction = () => {
   return [
@@ -8,6 +10,12 @@ export const meta: MetaFunction = () => {
   ];
 };

+export const loader: LoaderFunction = async () => {
+  const users = await prisma.user.findMany();
+  console.debug({ users });
+  return users;
+};
+
tmtk75tmtk75

ちょっと寄り道してsqliteでなくpostgresにしてみる。

compose.ymlを足して

diff --git a/compose.yml b/compose.yml
new file mode 100644
index 0000000..36a958f
--- /dev/null
+++ b/compose.yml
@@ -0,0 +1,36 @@
+version: '3'
+
+services:
+  postgres:
+    image: postgres:14-alpine
+    environment:
+      POSTGRES_USER: "admin"
+      POSTGRES_PASSWORD: "abc123"
+      POSTGRES_DB: "example"
+    ports:
+      - "5432:5432"
+    volumes:
+      - ./pgdata:/var/lib/postgresql/data
+
$ docker compose up
...

接続確認

$ PGPASSWORD=abc123 psql example -h localhost -p 5432 -U admin
psql (16.2, server 14.11)
Type "help" for help.

example=# ^D\q

sqliteのファイルが残っているとmigrateに失敗するので一度消す。

$ rm -rf prisma/migrations

% DATABASE_URL=postgres://admin:abc123@localhost/example  npx prisma migrate dev --name init
...

スキーマが出来ている。

% echo "\d" | PGPASSWORD=abc123 psql example -h localhost -p 5432 -U admin
               List of relations
 Schema |        Name        |   Type   | Owner
--------+--------------------+----------+-------
 public | Post               | table    | admin
 public | Post_id_seq        | sequence | admin
 public | User               | table    | admin
 public | User_id_seq        | sequence | admin
 public | _prisma_migrations | table    | admin
(5 rows)
tmtk75tmtk75

prisma with postgres続き。
.envファイルにDATABASE_URLを定義して再起動。

$ cat .env
DATABASE_URL=postgres://admin:abc123@localhost/example

$ pnpm dev
...
{ users: [] }

レコードはまだないので空。

prisma studioでuserテーブルに適当にレコードを入れてやってリロードすると入れたレコードが見える。

$ npx prisma studio
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Prisma Studio is up on http://localhost:5555
tmtk75tmtk75

localstackも入れておこう。compose.ymlに入れてcompose再起動。

+  localstack:
+    image: localstack/localstack:1.0.0
+    ports:
+      - "0.0.0.0:4566:4566"            # LocalStack Gateway
+      - "0.0.0.0:4510-4559:4510-4559"  # external services port range
+    # https://docs.localstack.cloud/localstack/configuration/
+    environment:
+      - DEBUG=${DEBUG-}
+      - PERSISTENCE=${PERSISTENCE-}
+      - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-}
+      - DOCKER_HOST=unix:///var/run/docker.sock
+      - DEFAULT_REGION=ap-northeast-1
+    volumes:
+      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
+      - "/var/run/docker.sock:/var/run/docker.sock"

bucketを作っておく。

% aws --endpoint-url=http://localhost:4566 s3api create-bucket --bucket my-bucket --region us-east-1
{
    "Location": "/my-bucket"
}
% pnpm i -D @aws-sdk/client-s3
...

bucketをリストするコードをloaderにいれてみる。

diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
index 9b48d3b..cf7e0f5 100644
--- a/app/routes/_index.tsx
+++ b/app/routes/_index.tsx
@@ -2,6 +2,7 @@ import type { LoaderFunction, MetaFunction } from "@remix-run/node";
 import { DatePicker, Button } from "antd";
 import { PrismaClient } from "@prisma/client";
 const prisma = new PrismaClient();
+import { ListBucketsCommand, S3Client } from "@aws-sdk/client-s3";

 export const meta: MetaFunction = () => {
   return [
@@ -11,6 +12,13 @@ export const meta: MetaFunction = () => {
 };

 export const loader: LoaderFunction = async () => {
+  const s3 = new S3Client({
+    region: "us-east-1",
+    endpoint: "http://localhost:4566",
+  });
+  const res = await s3.send(new ListBucketsCommand());
+  console.log(res.Buckets);
tmtk75tmtk75

zodでDATABASE_URLを検証します。
実際はprismaが検証してくれますが、zodのサンプルとしてURLがdocker composeで起動したexampleであることを期待するvalidationをします。

$ pnpm i -D zod
diff --git a/app/env.ts b/app/env.ts
new file mode 100644
index 0000000..12ce634
--- /dev/null
+++ b/app/env.ts
@@ -0,0 +1,10 @@
+import { z } from "zod";
+
+const envSchema = z.object({
+  DATABASE_URL: z
+    .string()
+    .url()
+    .refine((url) => url.endsWith("example")),
+});
+
+export const env = envSchema.parse(process.env);
diff --git a/app/node.d.ts b/app/node.d.ts
new file mode 100644
index 0000000..c7461c3
--- /dev/null
+++ b/app/node.d.ts
@@ -0,0 +1,7 @@
+declare global {
+  namespace NodeJS {
+    interface ProcessEnv {
+      readonly DATABASE_URL: string;
+    }
+  }
+}
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
index cf7e0f5..839a7e9 100644
--- a/app/routes/_index.tsx
+++ b/app/routes/_index.tsx
@@ -3,6 +3,7 @@ import { DatePicker, Button } from "antd";
 import { PrismaClient } from "@prisma/client";
 const prisma = new PrismaClient();
 import { ListBucketsCommand, S3Client } from "@aws-sdk/client-s3";
+import { env } from "~/env";

 export const meta: MetaFunction = () => {
   return [
@@ -21,6 +22,8 @@ export const loader: LoaderFunction = async () => {

   const users = await prisma.user.findMany();
   console.debug({ users });
+
+  env;
   return users;
 };
tmtk75tmtk75

ビルドしてみる。動きますね。

$ pnpm run build && pnpm run start

...
[remix-serve] http://localhost:3000 (http://192.168.2.106:3000)
tmtk75tmtk75

zodのデフォルト値をlocalstackにしておいて、それを使うようにする。

diff --git a/app/env.ts b/app/env.ts
index 12ce634..86f6ff4 100644
--- a/app/env.ts
+++ b/app/env.ts
@@ -5,6 +5,10 @@ const envSchema = z.object({
     .string()
     .url()
     .refine((url) => url.endsWith("example")),
+
+  AWS_REGION: z.string().default("us-east-1"),
+
+  AWS_ENDPOINT_URL: z.string().url().default("http://localhost:4566"),
 });

 export const env = envSchema.parse(process.env);
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
index 839a7e9..148b60f 100644
--- a/app/routes/_index.tsx
+++ b/app/routes/_index.tsx
@@ -14,8 +14,8 @@ export const meta: MetaFunction = () => {

 export const loader: LoaderFunction = async () => {
   const s3 = new S3Client({
-    region: "us-east-1",
-    endpoint: "http://localhost:4566",
+    region: env.AWS_REGION,
+    endpoint: env.AWS_ENDPOINT_URL,
   });
   const res = await s3.send(new ListBucketsCommand());
   console.log(res.Buckets);
tmtk75tmtk75

docker imageも作れるようにしておく。

$ cat Dockerfile
FROM node:20-alpine

COPY package.json pnpm-lock.yaml* tsconfig.json vite.config.ts ./
COPY prisma ./prisma
COPY public ./public
COPY app ./app

RUN corepack enable pnpm && pnpm install
RUN pnpm run build

ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
ENV NODE_ENV "production"

CMD ["pnpm", "run", "start"]

ビルドする。imageでかいな。

$ docker build -t my-app .
...
$ docker image ls
...
my-app       latest    b4d05a946c56   12 minutes ago   615MB
...
$ my_ip=${your_local_ipaddr} # not localhost, 127.0.0.1
$ docker run \
    -e DATABASE_URL=postgres://admin:abc123@${my_ip}/example \
    -e AWS_ENDPOINT_URL=http://${my_ip}:4566 \
    -e AWS_ACCESS_KEY_ID=fake \
    -e AWS_SECRET_ACCESS_KEY=fake \
    -p 3000:3000 my-app

localhost:3000にアクセスしてエラーらしきものが出なければOK

tmtk75tmtk75

my-appをdocker composeで動かす。
compose.my-app.ymlを作る。

version: '3'

services:
  my-app:
    image: my-app:latest
    environment:
      DATABASE_URL: postgres://admin:${PGPASSWORD-abc123}@postgres/example
      AWS_ENDPOINT_URL: http://localstack:4566
      AWS_ACCESS_KEY_ID: fake
      AWS_SECRET_ACCESS_KEY: fake
    ports:
      - "3000:3000"

いったんcomposeを止めて、以下で再起動。

$ docker compose -f compose.yml -f compose.my-app.yml up
...

:3000にアクセスしてcomposeのログに出ればOK。localstackのbucketは一回stopして消えているはずなのでもう一回作ると見える。

$ curl --head localhost:3000
HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
Vary: Accept-Encoding
Date: Fri, 03 May 2024 09:04:47 GMT
Connection: keep-alive
Keep-Alive: timeout=5
$ aws --endpoint-url=http://localhost:4566 s3api create-bucket --bucket my-bucket --region us-east-1
tmtk75tmtk75

UIライブラリにantdを使ってみたが、公式iconがcjs/esm絡みで動かない。
https://github.com/ant-design/ant-design-icons/issues/605

特にこだわりはないので使い慣れたMUIに出戻るかと思ったら、MUIはMUIでremixサポートがまだイマイチの模様[1]
https://github.com/mui/material-ui/issues/39765

さてどうするか。

脚注
  1. まあ後から出てきたフレームワークでうまく動かないのはしょうがないよね。 ↩︎

tmtk75tmtk75

さてどうするか。

chakra-uiにしてみた。

このスクラップは13日前にクローズされました