Next.js プロジェクトを作成し、tRPC, prisma を順に追加していきたいと思います。
完全に個人的メモです。とりあえず tRPC を使うとフロントエンドとバックエンドで安全な型解決ができます。
T3 Stack なプロジェクトを読み解く必要があるのだが、tRPC も prisma も良く分かってないし、Next.jsも見慣れていないため、ディレクトリ構成やファイルの関係性が分かっていないことが発端です。
Next.js
Reactと比較し
- クライアントPCに依存せずレンダリング可能
- デフォルトSSRのため(Next.js 13)
- ルーティング設計が用意
- ディレクトリ構成がそのまま反映されるため
- SEO対策
- ページ読み込みが早い
- 検索エンジンがクロールしやすい
レンダリング
- SSG(Static Site Generation)
- ビルド時に必要なHTMLが生成される
- APIもこのタイミングで実行されデータを引っ張ってくる
- レンダリング済のHTMLが返却されるため高速
- 更新頻度が少ないページ
- ブログの記事
- 利用規約
- ヘルプ
- etc
- SSR(Server Side Rendering)
- サーバーリクエストのタイミングで、サーバー側でHTMLをレンダリング
- クライアントPC、ブラウザに依存せずレンダリング可能
- 更新頻度が高いページ
- プロフィール
- お知らせ
- etc
- ISR(Incremental Static Regeneration)
- SSG と SSR のハイブリッド
- キャッシュにより更新を制御
# SSG
// This request should be cached until manually invalidated.
// Similar to `getStaticProps`.
// `force-cache` is the default and can be omitted.
fetch(URL, { cache: 'force-cache' });
# SSR
// This request should be refetched on every request.
// Similar to `getServerSideProps`.
fetch(URL, { cache: 'no-store' });
# ISR
// This request should be cached with a lifetime of 10 seconds.
// Similar to `getStaticProps` with the `revalidate` option.
fetch(URL, { next: { revalidate: 10 } });
Next.js プロジェクト作成
npx create-next-app@latest tutorial --ts
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
tRPC
必要なモジュールをインストール
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
プロジェクトルートにtRPC backend用のディレクトリ(server)を作成し、trpc.ts にてtRPCサーバーのインスタンスを生成します
import { initTRPC } from '@trpc/server';
// tRPCサーバーの生成
// アプリ内でインスタンス生成は一度のみ
const t = initTRPC.create();
export const router = t.router;
export const procedure = t.procedure;
続いて router を定義していきます
// 入力値のバリデーション用
import { z } from "zod";
import { router, procedure } from "../trpc";
export const appRouter = router({
// ここにAPIを定義していく
})
// フロントエンドで型を参照できるようになる
export type AppRouter = typeof appRouter
procedure.input()に記載したinputのプロパティが自動的にVSCodeで補完されます。
tRPCの前に、Next.jsにおけるAPI実装方法
Next.jsではAPIを実装する機能が提供されておりv13.3以前ではPageRouterが標準だったが、最新ではAppRouteとなっている
pages/api/user.js => http://localhost:3000/api/user
Router Handlersはappディレクトリ内でのみ利用することができます。
app/api/user/route.js => http://localhost:3000/api/user
import type { NextApiRequest, NextApiResponse } from "next";
// フロントエンドで型解決できるように明示的に定義&exportが必要となる!
export type ResponseUserData = {
message: string;
};
export async function GET(request: Request) {
return Response.json({ message: "Next.js GET user sample" });
}
APIリクエスト (curl)
curl http://localhost:3000/api/user -w '\n'
{"message":"Next.js GET user sample"}
APIリクエスト (frontend)
"use client";
import axios from "axios";
// フロントエンド側でAPIレスポンスの型解決が必要になる!
import { ResponseUserData } from "./api/user/route";
export default function Home() {
const getData = async () => {
// ジェネリクスでレスポンスの型を指定する必要がある!
const response = await axios.get<ResponseUserData>("/api/user");
console.log(response.data.message);
};
return (
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={getData}
>
getDate
</button>
);
}
tRPCをNext.js APIとして追加
// trpcNext は tRPC ライブラリの Next.js アダプター
// このアダプターで Next.js の API ルートに tRPC の機能が統合される
// express を使う場合は Express Adapter がある(https://trpc.io/docs/server/adapters/express)
import * as trpcNext from "@trpc/server/adapters/next";
import { appRouter } from "../../../../../server/routers/_app";
import { NextRequest } from "next/server";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
const handler = (req: NextRequest) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: () => ({}),
});
export { handler as GET, handler as POST };
API実装
import { z } from "zod";
import { router, procedure } from "../trpc";
// 入力値のバリデーション用スキーマ
const getTaskInput = z.object({
id: z.number(),
});
export const appRouter = router({
// getTask APIの定義
getTask: procedure.query((request) => {
console.log(request);
const task = {
name: `sample task`,
};
return { task };
}),
});
// フロントエンドで型を参照できるようになる
export type AppRouter = typeof appRouter;
呼出し側
const getTask = async () => {
const response = await axios.get("/api/trpc/getTask");
console.log(response);
};
tanstak を使ったtRPCの呼出し
React Queryを使った tRPCクライアントを作成
import { createTRPCReact } from "@trpc/react-query";
import { type AppRouter } from "../../../server/routers/_app";
// tRPC と React Query を統合するためのクライアントインスタンスを作成
// React コンポーネント内で tRPC APIを直接呼び出すためのフックやユーティリティが提供される
export const clientApi = createTRPCReact<AppRouter>({});
プロバイダ作成
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import React, { useState } from "react";
import { clientApi } from "./client-api";
export default function Provider({ children }: { children: React.ReactNode }) {
// 状態管理&キャッシュのため
// React Query によるデータフェッチ、キャッシュ、同期、および状態更新のためのオブジェクト
// これによりアプリ内でデータのキャッシュと状態管理が行われる
// データ取得に関連する多くの最適化が自動的に適用される
const [queryClient] = useState(() => new QueryClient({}));
const [trpcClient] = useState(() =>
// tRPC クライアント生成
// サーバー側の tRPC APIとのインターフェースとなる
clientApi.createClient({
links: [
// 複数のリクエストを単一のHTTPリクエストにまとめる
httpBatchLink({
url: "http://localhost:3000/api/trpc",
}),
],
})
);
return (
// clientAPI.Provider により、任意のコンポーネントから設定された tRPC クライアントを通じてサーバー側の tRPC APIとの通信が可能となる
// QueryClientProvider により、任意のコンポーネントが React Query の機能(データフェッチ、キャッシュの更新、データの再取得など)を利用可能となる
<clientApi.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</clientApi.Provider>
);
}
プロバイダ適用
import React from "react";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Provider from "./utils/provider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<Provider>{children}</Provider>
</body>
</html>
);
}
APIリクエスト
"use client";
import { clientApi } from "@/app/utils/client-api";
const Task = () => {
const task = clientApi.getTask.useQuery();
return <div>{task.data?.task.name}</div>;
};
export default Task;
補完も使える!
バックエンドでレスポンスを変更したら、型補完も追随されているのが分かる
getTask: procedure.query((request) => {
console.log(request);
const task = {
name: `sample task`,
done: false // プロパティ追加
};
return { task };
}),
ディレクトリ構成
|--server //------------------------tRPC backend
| |--routers
| | |--_app.ts // router(API)定義
| |--trpc.ts // tRPCサーバーインスタンス生成
|--src //------------------------frontend
| |--app
| | |--api //----------------------Next.js API
| | | |--route.ts // ディレクトリ+"route" ファイルが各APIエンドポイントのパスとなる
| | | |--trpc // /api/trpc エンドポイント
| | | | |--[trpc]
| | | | | |--route.ts
| | | |--user // /api/user エンドポイント
| | | | |--route.ts
| | |--components
| | | |--task.tsx
| | |--favicon.ico
| | |--globals.css
| | |--layout.tsx
| | |--page.tsx
| | |--utils // tRPCクライアントの生成と準備
| | | |--client-api.ts // tRPC と React Query を統合するためのクライアントインスタンスを作成
| | | |--provider.tsx // tRPCクライアント(tRPC backend側とのインターフェース)作成
inputパラメーターの検証と取得を追加
"use client";
import { clientApi } from "@/app/utils/client-api";
const Task = () => {
const task = clientApi.getTask.useQuery({ id: "1" });
return <div>{task.data?.task.name}</div>;
};
export default Task;
上記の通り、id: "1" をパラメーターに渡した場合の、tRPC backend側のログを確認すると以下の通りとなっています。
{
ctx: {},
type: 'query',
path: 'getTask',
rawInput: { id: '1' },
meta: undefined,
input: { id: '1' },
next: [Function: next]
}
tRPC backend 側は以下のように .input で入力値を検証し、 { input } にて分割代入してアクセスできます。また画像のように補完も効きます。
import { z } from "zod";
import { router, procedure } from "../trpc";
// 入力値のバリデーション用スキーマ
const getTaskInput = z.object({
id: z.string().max(3),
});
export const appRouter = router({
// getTask APIの定義
getTask: procedure.input(getTaskInput).query(({ input }) => {
const task = {
name: `sample task ${input.id}`,
done: false,
};
return { task };
}),
});
// フロントエンドで型を参照できるようになる
export type AppRouter = typeof appRouter;
Prisma
複数の .env ファイルを切り替えたい場合は dotenv-cli を使う
https://www.prisma.io/docs/orm/more/development-environment/environment-variables/using-multiple-env-files
npm i dotenv-cli --save-de
"scripts": {
"local:studio": "dotenv -e .env.local -- npx prisma studio"
},
モジュールインストール
npm install prisma --save-dev
prisma初期化
npx prisma init
✔ Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.
Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver, mongodb or cockroachdb.
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.
More information in our documentation:
https://pris.ly/d/getting-started
┌────────────────────────────────────────────────────────────────┐
│ Developing real-time features? │
│ Prisma Pulse lets you respond instantly to database changes. │
│ https://pris.ly/cli/pulse │
└────────────────────────────────────────────────────────────────┘
prisma/schema.prisma, .env が作成される。
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
postgresqlをDockerで起動
version: "3"
services:
db:
image: postgres:14
container_name: postgres_prisma
ports:
- 5432:5432
volumes:
- db-store:/var/lib/postgresql/data
environment:
POSTGRES_USER: "user"
POSTGRES_PASSWORD: "user"
volumes:
db-store:
.env の DATABASE_URL を修正
DATABASE_URL="postgresql://user:user@localhost:5432/mydb?schema=public"
Taskテーブル用のModelを作成
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Task model
model tasks {
id Int @id @default(autoincrement())
name String
done Boolean
}
migrateを実行しテーブルを作成する
npx prisma migrate dev
実行結果
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "mydb", schema "public" at "localhost:5432"
PostgreSQL database mydb created at localhost:5432
? Enter a name for the new migration: › init
Applying migration `20240507063021_init`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ 20240507063021_init/
└─ migration.sql
Your database is now in sync with your schema.
Running generate... (Use --skip-generate to skip the generators)
added 1 package, and audited 386 packages in 6s
141 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
✔ Generated Prisma Client (v5.13.0) to ./node_modules/@prisma/client in 49ms
migration 結果を確認
prisma/migrations/yyyymmddhhmmss_[name]/migration.sql が作成されます.
Modelに定義した内容相当のSQLが出力されていることが分かります.
-- CreateTable
CREATE TABLE "tasks" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"done" BOOLEAN NOT NULL,
CONSTRAINT "tasks_pkey" PRIMARY KEY ("id")
);
prisma studo で確認
npx prisma studio
http://localhost:5555 にアクセスすると以下のように確認できます.
tasksテーブルが生成されていることが分かります.
tasksテーブルの中身.
Prisma Clientを使った操作
実際にprisma clientを使って、データの登録と取得をしてみます.
clientのインストール
npm install @prisma/client
prisma generateの実行
これにより schema が読み込まれ Prisma Clientのコードが自動生成されます.
schema を変更した際は、 prisma generate を実行してコードを自動生成する必要があります.
デフォルトでは node_modules/.prisma/client に生成されます.
npx prisma generate
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
✔ Generated Prisma Client (v5.13.0) to ./node_modules/@prisma/client in 48ms
Start using Prisma Client in Node.js (See: https://pris.ly/d/client)
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
or start using Prisma Client at the edge (See: https://pris.ly/d/accelerate)
import { PrismaClient } from '@prisma/client/edge'
const prisma = new PrismaClient()
See other ways of importing Prisma Client: http://pris.ly/d/importing-client
┌────────────────────────────────────────────────────────────────┐
│ Supercharge your Prisma Client with global database caching, │
│ scalable connection pooling and real-time database events. │
│ Explore Prisma Accelerate: https://pris.ly/cli/-accelerate │
│ Explore Prisma Pulse: https://pris.ly/cli/-pulse │
└────────────────────────────────────────────────────────────────┘
prisma generate を実行していないと以下のようなエラーが出ます.
Error: @prisma/client did not initialize yet. Please run "prisma generate" and try to import it again.
schema.prisma で定義した model の通りに補完される
Prisma Client を使ったDB登録処理(addTask)
import { z } from "zod";
import { router, procedure } from "../trpc";
import { PrismaClient } from "@prisma/client";
// prisma clientの準備
const prisma = new PrismaClient();
// 入力値のバリデーション用スキーマ
const getTaskInput = z.object({
id: z.string().max(3),
});
// addTask APIのバリデーションスキーマ
const addTaskInput = z.object({
name: z.string().max(10),
done: z.optional(z.boolean()),
});
export const appRouter = router({
// getTask APIの定義
getTask: procedure.input(getTaskInput).query(({ input }) => {
const task = {
name: `sample task ${input.id}`,
done: false,
};
return { task };
}),
// 登録処理
addTask: procedure.input(addTaskInput).mutation(async ({ input }) => {
const newTask = await prisma.tasks.create({
data: {
name: "test",
done: false,
},
});
console.log(`newTask: ${newTask.id}`);
return { newTask };
}),
});
// フロントエンドで型を参照できるようになる
export type AppRouter = typeof appRouter;
クライアント側の実装
"use client";
import { clientApi } from "@/app/utils/client-api";
const Task = () => {
const { mutate, isLoading, isSuccess, isError, error, data } =
clientApi.addTask.useMutation({
onSuccess: () => {
console.log("success!!");
},
});
const handleAdd = () => {
mutate(
{ name: "add task 2", done: false },
{
onSuccess: (data) => {
console.log(`mutate onSuccess: ${data.newTask.id}`);
},
}
);
};
return (
<div>
<button onClick={() => handleAdd()}>add</button>
<div>
{isLoading
? `loading...`
: isSuccess
? `${data.newTask.id} : ${data.newTask.name} / ${data.newTask.done}`
: `ng`}
</div>
</div>
);
};
export default Task;
登録成功時の onSuccess 内.
補完が効いていることがわかります
Prisma Studioでも登録されていることが確認できます
ディレクトリ構成
|--.env // 設定ファイル(DATABASE_URLを定義)
|--prisma //------------------------prisma
| |--migrations // npx prisma migrate の実行結果
| | |--20240507063021_init // migrate 実行結果 hhhhmmddhhmmss_[name]
| | | |--migration.sql
| |--schema.prisma // スキーマ
|--server //------------------------tRPC backend
| |--routers
| | |--_app.ts // router(API)定義
| |--trpc.ts // tRPCサーバーインスタンス生成
|--src //------------------------frontend
| |--app
| | |--api //----------------------Next.js API
| | | |--route.ts // ディレクトリ+"route" ファイルが各APIエンドポイントのパスとなる
| | | |--trpc // /api/trpc エンドポイント
| | | | |--[trpc]
| | | | | |--route.ts
| | | |--user // /api/user エンドポイント
| | | | |--route.ts
| | |--components
| | | |--task.tsx
| | |--favicon.ico
| | |--globals.css
| | |--layout.tsx
| | |--page.tsx
| | |--utils // tRPCクライアントの生成と準備
| | | |--client-api.ts // tRPC と React Query を統合するためのクライアントインスタンスを作成
| | | |--provider.tsx // tRPCクライアント(tRPC backend側とのインターフェース)作成
T3 Stack
t3 stack アプリひな形作成
NextAuth のみ今回は省略
別途、まずは手動で NextAuth を追加してみたいと思います
npx create-t3-app sample-t3
Need to install the following packages:
create-t3-app@7.32.1
Ok to proceed? (y) y
___ ___ ___ __ _____ ___ _____ ____ __ ___ ___
/ __| _ \ __| / \_ _| __| |_ _|__ / / \ | _ \ _ \
| (__| / _| / /\ \| | | _| | | |_ \ / /\ \| _/ _/
\___|_|_\___|_/‾‾\_\_| |___| |_| |___/ /_/‾‾\_\_| |_|
│
◇ Will you be using TypeScript or JavaScript?
│ TypeScript
│
◇ Will you be using Tailwind CSS for styling?
│ Yes
│
◇ Would you like to use tRPC?
│ Yes
│
◇ What authentication provider would you like to use?
│ None
│
◇ What database ORM would you like to use?
│ Prisma
│
◇ Would you like to use Next.js App Router?
│ Yes
│
◇ What database provider would you like to use?
│ PostgreSQL
│
◇ Should we initialize a Git repository and stage the changes?
│ Yes
│
◇ Should we run 'npm install' for you?
│ Yes
│
◇ What import alias would you like to use?
│ ~/
Using: npm
✔ sample-t3 scaffolded successfully!
Adding boilerplate...
✔ Successfully setup boilerplate for prisma
✔ Successfully setup boilerplate for tailwind
✔ Successfully setup boilerplate for trpc
✔ Successfully setup boilerplate for dbContainer
✔ Successfully setup boilerplate for envVariables
✔ Successfully setup boilerplate for eslint
Installing dependencies...
✔ Successfully installed dependencies!
Initializing Git...
✔ Successfully initialized and staged git
Next steps:
cd sample-t3
Start up a database, if needed using './start-database.sh'
npm run db:push
npm run dev
git commit -m "initial commit"
ローカルで起動する場合
出力された手順にある通りですが、
- ./start-database.sh
- postgresql のDockerコンテナを起動してくれます
- .env も自動で生成してくれるので、
DB_PASSWORD
に適宜パスワードを設定します - t3 では npx prisma init は済の状態になります
- npm run db:push
- prisma db push が実行されます
- ローカル用にサクッとスキーマとテーブルを同期させたいときに使うもの?
- 本番ではマイグレーション履歴を残すために prisma migrate を使う
- npm run dev でアプリを起動するのは同じ
Dockerコンテナでアプリを実行したい
- NextjsのコンテナとPostgresqlのコンテナを用意
- アプリをコンテナ内でビルド&起動
- こちらに関しては公式サンプルDockerファイルがあります
FROM node:18-alpine
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json package-lock.json* ./
# Omit --production flag for TypeScript devDependencies
RUN npm ci
COPY .env .
COPY postcss.config.cjs .
COPY prettier.config.js .
COPY tailwind.config.ts .
COPY src ./src
COPY public ./public
COPY next.config.js .
COPY tsconfig.json .
COPY prisma ./prisma
# Environment variables must be present at build time
# https://github.com/vercel/next.js/discussions/14030
ARG ENV_VARIABLE
ENV ENV_VARIABLE=${ENV_VARIABLE}
ARG NEXT_PUBLIC_ENV_VARIABLE
ENV NEXT_PUBLIC_ENV_VARIABLE=${NEXT_PUBLIC_ENV_VARIABLE}
ARG DATABASE_URL
ENV DATABASE_URL=${DATABASE_URL}
# 初回のみスキーマと同期させる必要がある
RUN npx prisma migrate dev --name init
# build前にgenerateが必要
RUN npx prisma generate
# Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry
# Uncomment the following line to disable telemetry at build time
# ENV NEXT_TELEMETRY_DISABLED 1
# Note: Don't expose ports here, Compose will handle that for us
# Build Next.js based on the preferred package manager
RUN npm run build
# Start Next.js based on the preferred package manager
CMD ["npm", "run","start"]
version: "3"
services:
next-app:
container_name: next-app
build:
context: .
dockerfile: prod.withoutMulti.Dockerfile
args:
DATABASE_URL: ${DATABASE_URL}
ENV_VARIABLE: ${ENV_VARIABLE}
NEXT_PUBLIC_ENV_VARIABLE: ${NEXT_PUBLIC_ENV_VARIABLE}
restart: always
ports:
- 3001:3000
- 5551:5555
networks:
- my_network
# Add more containers below (nginx, postgres, etc.)
db:
image: postgres:14
container_name: postgres_prisma
ports:
- 5432:5432
environment:
POSTGRES_USER: "user"
POSTGRES_PASSWORD: "user"
networks:
- my_network
# Define a network, which allows containers to communicate
# with each other, by using their container name as a hostname
networks:
my_network:
external: true
VS Codeでデバッグ
試行錯誤したが、とりあえず以下の設定で動いたということだけ...
{
"version": "0.2.0",
"configurations": [
{
"name": "debug full stack",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev",
}
]
}
ブレイクポイントを貼り、Chromeでアプリを起動すると止まります。
Refineを使っている場合は以下でできるっぽい
{
"version": "0.2.0",
"configurations": [
{
"name": "debug full stack",
"program": "${workspaceFolder}/node_modules/.bin/refine",
"type": "node",
"request": "launch",
"args": ["dev"],
"cwd": "${workspaceFolder}",
}
]
}