remixのscaffold repositoryを作る
- remix
- tailwind
-
chakra-ui
ant design - prisma
- biome
- zod
- vitest
-
sass
あたりが入ったscaffold用repositoryを作っておこうと思う。
Dockerfile, composeも追加。
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
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
tailwindを入れる。
ガイドどおり。pnpmを使う。
biomeをいれる。eslintを削除。.eslintrc.cjsも削除。
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",
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"),
+ },
+ },
+});
Ant Designを入れてみる。実は初めて使う。
これだけで普通に動いてしまった。見た目も
$ 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>
);
}
次はprisma。
$ 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.
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;
+};
+
ちょっと寄り道して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)
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
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);
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;
};
ビルドしてみる。動きますね。
$ pnpm run build && pnpm run start
...
[remix-serve] http://localhost:3000 (http://192.168.2.106:3000)
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);
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
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
さてどうするか。
chakra-uiにしてみた。
大体できたのでGitHubにpushしておしまい。