ship-create 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ship-create",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Scaffold a new project the SHIP Method way — Structure, Human Flow, Instruction, Publish. No git clone, no API key, just one command.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,32 @@
1
+ # Pick exactly one provider. This is a setup-time choice, not runtime-switchable.
2
+ DB_PROVIDER=supabase # supabase | neon | cloudflare-d1 | postgres
3
+
4
+ # --- supabase | postgres | neon (Postgres-family — all use DATABASE_URL) ---
5
+ # Supabase: Project Settings -> Database -> Connection string (use the "Transaction pooler" URI for serverless)
6
+ # Neon: Project Dashboard -> Connection Details -> select "Pooled connection"
7
+ # Plain Postgres: your host's connection string, e.g. postgres://user:pass@host:5432/dbname
8
+ DATABASE_URL=
9
+
10
+ # --- cloudflare-d1 only ---
11
+ # D1 has no connection string at runtime — the binding comes from wrangler.toml
12
+ # and the Cloudflare Workers runtime, not an env var. The vars below are only
13
+ # used by `drizzle-kit` migration commands run from your local machine.
14
+ CLOUDFLARE_ACCOUNT_ID=
15
+ CLOUDFLARE_D1_DATABASE_ID=
16
+ CLOUDFLARE_API_TOKEN=
17
+
18
+ # IMPORTANT: DB_PROVIDER=cloudflare-d1 requires deploying this app to
19
+ # Cloudflare Workers/Pages via @opennextjs/cloudflare. It will NOT work on
20
+ # Vercel. See 13-TECH-STACK/DB_PROVIDER_GUIDE.md before picking this option.
21
+
22
+ # --- Auth.js (required regardless of DB_PROVIDER) ---
23
+ AUTH_SECRET=
24
+ # Add at least one provider's keys here once chosen, e.g.:
25
+ # AUTH_GITHUB_ID=
26
+ # AUTH_GITHUB_SECRET=
27
+
28
+ # --- Storage (optional — only needed once you wire up file uploads) ---
29
+ STORAGE_PROVIDER=supabase # supabase | cloudflare-r2 | vercel-blob (only supabase is implemented)
30
+ SUPABASE_URL=
31
+ SUPABASE_SERVICE_ROLE_KEY=
32
+ SUPABASE_STORAGE_BUCKET=uploads
@@ -1,15 +1,16 @@
1
1
  # SHIP Starter Kit
2
2
 
3
- This is the code foundation for **The SHIP Method OS** — a Next.js 14 (App
3
+ This is the code foundation for **The SHIP Method OS** — a Next.js 16 (App
4
4
  Router) + TypeScript + Tailwind CSS starter that gives every downstream agent
5
5
  a consistent, working UI shell to build on top of.
6
6
 
7
- It is currently **mock-data only**. There is no backend wired up — no
8
- Supabase, no auth, no payments. Every list, table, and metric you see is
9
- sourced from `lib/mock-data.ts`. That's intentional: the goal of this layer
10
- is a correct, consistent shared foundation (design tokens, primitives,
11
- layout) that other agents can build real features on without re-deciding
12
- button styles or color values per screen.
7
+ It is currently **mock-data only**. Backend code (`lib/db/`, `lib/auth.ts`,
8
+ `lib/storage.ts`) is included but has no live database connection configured
9
+ by default. Every list, table, and metric you see is sourced from
10
+ `lib/mock-data.ts`. That's intentional: the goal of this layer is a correct,
11
+ consistent shared foundation (design tokens, primitives, layout) that other
12
+ agents can build real features on without re-deciding button styles or color
13
+ values per screen.
13
14
 
14
15
  ## Running it
15
16
 
@@ -51,14 +52,23 @@ shared primitives in `components/ui/` and the `cn()` helper in
51
52
 
52
53
  ## Next step: real data
53
54
 
54
- When it's time to move off mock data, replace the contents of
55
- `lib/mock-data.ts` (or the call sites that import from it) with real
56
- Supabase queries. Before doing that, read:
55
+ This kit ships with a real, pluggable backend in `lib/db/`, `lib/auth.ts`,
56
+ and `lib/storage.ts` but it has no live database connection configured
57
+ yet. To turn it on:
57
58
 
58
- - `../13-TECH-STACK/TECH_STACK.md` the chosen stack and why.
59
- - `../03-INSTRUCTION/DATABASE_SPEC.md` the database schema this app
60
- should query against.
59
+ 1. Copy `.env.example` to `.env` and pick a `DB_PROVIDER`: `supabase`,
60
+ `neon`, `cloudflare-d1`, or `postgres`. Read
61
+ `../13-TECH-STACK/DB_PROVIDER_GUIDE.md` first if you're unsure which —
62
+ it covers free-tier limits and which providers can deploy where.
63
+ 2. Fill in that provider's connection details in `.env` (see the comments
64
+ in `.env.example`).
65
+ 3. Run `npx drizzle-kit generate` then apply the generated migration to
66
+ create the `user`/`account`/`session`/`verificationToken` tables.
67
+ 4. Add at least one Auth.js provider (OAuth or credentials) to the empty
68
+ `providers: []` array in `lib/auth.ts` — auth won't work until you do.
69
+ 5. Replace the contents of `lib/mock-data.ts` (or its call sites) with
70
+ real queries against `getDb()` from `lib/db`.
61
71
 
62
- Keep the typed interfaces (`MockUser`, `MockMemberContent`, `MockMetric`)
63
- as the contract — swap the data source, not the shape, unless the database
64
- spec requires otherwise.
72
+ Before writing your own tables, also read
73
+ `../03-INSTRUCTION/DATABASE_SPEC.md` for the schema-design template this
74
+ app's data model should follow.
@@ -0,0 +1,27 @@
1
+ import { defineConfig } from "drizzle-kit"
2
+
3
+ const provider = process.env.DB_PROVIDER
4
+
5
+ const config =
6
+ provider === "cloudflare-d1"
7
+ ? defineConfig({
8
+ dialect: "sqlite",
9
+ schema: "./lib/db/schema.sqlite.ts",
10
+ out: "./drizzle/sqlite",
11
+ driver: "d1-http",
12
+ dbCredentials: {
13
+ accountId: process.env.CLOUDFLARE_ACCOUNT_ID ?? "",
14
+ databaseId: process.env.CLOUDFLARE_D1_DATABASE_ID ?? "",
15
+ token: process.env.CLOUDFLARE_API_TOKEN ?? "",
16
+ },
17
+ })
18
+ : defineConfig({
19
+ dialect: "postgresql",
20
+ schema: "./lib/db/schema.pg.ts",
21
+ out: "./drizzle/postgres",
22
+ dbCredentials: {
23
+ url: process.env.DATABASE_URL ?? "",
24
+ },
25
+ })
26
+
27
+ export default config
@@ -0,0 +1,30 @@
1
+ import NextAuth from "next-auth"
2
+ import { DrizzleAdapter } from "@auth/drizzle-adapter"
3
+ import { getDb } from "./db"
4
+ import * as pgSchema from "./db/schema.pg"
5
+ import * as sqliteSchema from "./db/schema.sqlite"
6
+
7
+ const provider = process.env.DB_PROVIDER
8
+ const schema = (provider === "cloudflare-d1" ? sqliteSchema : pgSchema) as any
9
+
10
+ // D1's binding only exists inside a Cloudflare request context, so the
11
+ // adapter is built per-request via NextAuth's config-function form instead
12
+ // of once at module scope (which is enough for every other provider).
13
+ export const { handlers, signIn, signOut, auth } = NextAuth(async () => {
14
+ if (provider === "cloudflare-d1") {
15
+ const { getCloudflareContext } = await import("@opennextjs/cloudflare")
16
+ const { env } = getCloudflareContext()
17
+ return {
18
+ adapter: DrizzleAdapter(getDb((env as { DB: unknown }).DB), schema),
19
+ // No providers configured yet — add at least one (OAuth or
20
+ // credentials) before this auth setup is functional.
21
+ providers: [],
22
+ session: { strategy: "database" },
23
+ }
24
+ }
25
+ return {
26
+ adapter: DrizzleAdapter(getDb(), schema),
27
+ providers: [],
28
+ session: { strategy: "database" },
29
+ }
30
+ })
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
2
+ import { getDb } from "./index"
3
+
4
+ describe("getDb", () => {
5
+ afterEach(() => {
6
+ vi.unstubAllEnvs()
7
+ })
8
+
9
+ it("throws when DB_PROVIDER is missing", () => {
10
+ vi.stubEnv("DB_PROVIDER", "")
11
+ expect(() => getDb()).toThrow(/DB_PROVIDER/)
12
+ })
13
+
14
+ it("throws when DB_PROVIDER is invalid", () => {
15
+ vi.stubEnv("DB_PROVIDER", "mongodb")
16
+ expect(() => getDb()).toThrow(/DB_PROVIDER/)
17
+ })
18
+
19
+ it("throws when DB_PROVIDER=postgres and DATABASE_URL is missing", () => {
20
+ vi.stubEnv("DB_PROVIDER", "postgres")
21
+ vi.stubEnv("DATABASE_URL", "")
22
+ expect(() => getDb()).toThrow(/DATABASE_URL/)
23
+ })
24
+
25
+ it("constructs a client for DB_PROVIDER=postgres given a connection string", () => {
26
+ vi.stubEnv("DB_PROVIDER", "postgres")
27
+ vi.stubEnv("DATABASE_URL", "postgres://user:pass@localhost:5432/db")
28
+ expect(() => getDb()).not.toThrow()
29
+ })
30
+
31
+ it("constructs a client for DB_PROVIDER=supabase given a connection string", () => {
32
+ vi.stubEnv("DB_PROVIDER", "supabase")
33
+ vi.stubEnv("DATABASE_URL", "postgres://user:pass@localhost:5432/db")
34
+ expect(() => getDb()).not.toThrow()
35
+ })
36
+
37
+ it("constructs a client for DB_PROVIDER=neon given a connection string", () => {
38
+ vi.stubEnv("DB_PROVIDER", "neon")
39
+ vi.stubEnv("DATABASE_URL", "postgres://user:pass@neon.example.com/db")
40
+ expect(() => getDb()).not.toThrow()
41
+ })
42
+
43
+ it("throws when DB_PROVIDER=cloudflare-d1 and no binding is passed", () => {
44
+ vi.stubEnv("DB_PROVIDER", "cloudflare-d1")
45
+ expect(() => getDb()).toThrow(/d1Binding/)
46
+ })
47
+
48
+ it("constructs a client for DB_PROVIDER=cloudflare-d1 given a binding", () => {
49
+ vi.stubEnv("DB_PROVIDER", "cloudflare-d1")
50
+ const fakeBinding = {} as never
51
+ expect(() => getDb(fakeBinding)).not.toThrow()
52
+ })
53
+ })
@@ -0,0 +1,64 @@
1
+ import { drizzle as drizzlePg } from "drizzle-orm/postgres-js"
2
+ import postgres from "postgres"
3
+ import { drizzle as drizzleNeon } from "drizzle-orm/neon-http"
4
+ import { neon } from "@neondatabase/serverless"
5
+ import { drizzle as drizzleD1 } from "drizzle-orm/d1"
6
+ import * as pgSchema from "./schema.pg"
7
+ import * as sqliteSchema from "./schema.sqlite"
8
+
9
+ export type DbProvider = "supabase" | "neon" | "cloudflare-d1" | "postgres"
10
+
11
+ function getProvider(): DbProvider {
12
+ const provider = process.env.DB_PROVIDER
13
+ if (
14
+ provider !== "supabase" &&
15
+ provider !== "neon" &&
16
+ provider !== "cloudflare-d1" &&
17
+ provider !== "postgres"
18
+ ) {
19
+ throw new Error(
20
+ `Invalid or missing DB_PROVIDER env var: "${provider}". Must be one of: supabase, neon, cloudflare-d1, postgres. See .env.example.`
21
+ )
22
+ }
23
+ return provider
24
+ }
25
+
26
+ // D1's binding only exists inside a Cloudflare request context (there is no
27
+ // connection string for it), so callers on that path must pass it in.
28
+ export function getDb(d1Binding?: unknown) {
29
+ const provider = getProvider()
30
+
31
+ switch (provider) {
32
+ case "supabase":
33
+ case "postgres": {
34
+ const connectionString = process.env.DATABASE_URL
35
+ if (!connectionString) {
36
+ throw new Error(
37
+ `DATABASE_URL is required when DB_PROVIDER is "${provider}". See .env.example.`
38
+ )
39
+ }
40
+ const client = postgres(connectionString)
41
+ return drizzlePg(client, { schema: pgSchema })
42
+ }
43
+ case "neon": {
44
+ const connectionString = process.env.DATABASE_URL
45
+ if (!connectionString) {
46
+ throw new Error(
47
+ 'DATABASE_URL is required when DB_PROVIDER is "neon". See .env.example.'
48
+ )
49
+ }
50
+ const client = neon(connectionString)
51
+ return drizzleNeon(client, { schema: pgSchema })
52
+ }
53
+ case "cloudflare-d1": {
54
+ if (!d1Binding) {
55
+ throw new Error(
56
+ 'DB_PROVIDER is "cloudflare-d1" but no d1Binding was passed to getDb(). ' +
57
+ "D1's binding comes from the Cloudflare runtime context (e.g. getCloudflareContext().env.DB " +
58
+ "from @opennextjs/cloudflare) — it cannot be constructed from an env var alone."
59
+ )
60
+ }
61
+ return drizzleD1(d1Binding as never, { schema: sqliteSchema })
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,58 @@
1
+ import { integer, timestamp, pgTable, primaryKey, text } from "drizzle-orm/pg-core"
2
+ import type { AdapterAccountType } from "next-auth/adapters"
3
+
4
+ export const users = pgTable("user", {
5
+ id: text("id")
6
+ .primaryKey()
7
+ .$defaultFn(() => crypto.randomUUID()),
8
+ name: text("name"),
9
+ email: text("email").unique(),
10
+ emailVerified: timestamp("emailVerified", { mode: "date" }),
11
+ image: text("image"),
12
+ })
13
+
14
+ export const accounts = pgTable(
15
+ "account",
16
+ {
17
+ userId: text("userId")
18
+ .notNull()
19
+ .references(() => users.id, { onDelete: "cascade" }),
20
+ type: text("type").$type<AdapterAccountType>().notNull(),
21
+ provider: text("provider").notNull(),
22
+ providerAccountId: text("providerAccountId").notNull(),
23
+ refresh_token: text("refresh_token"),
24
+ access_token: text("access_token"),
25
+ expires_at: integer("expires_at"),
26
+ token_type: text("token_type"),
27
+ scope: text("scope"),
28
+ id_token: text("id_token"),
29
+ session_state: text("session_state"),
30
+ },
31
+ (account) => ({
32
+ compoundKey: primaryKey({
33
+ columns: [account.provider, account.providerAccountId],
34
+ }),
35
+ })
36
+ )
37
+
38
+ export const sessions = pgTable("session", {
39
+ sessionToken: text("sessionToken").primaryKey(),
40
+ userId: text("userId")
41
+ .notNull()
42
+ .references(() => users.id, { onDelete: "cascade" }),
43
+ expires: timestamp("expires", { mode: "date" }).notNull(),
44
+ })
45
+
46
+ export const verificationTokens = pgTable(
47
+ "verificationToken",
48
+ {
49
+ identifier: text("identifier").notNull(),
50
+ token: text("token").notNull(),
51
+ expires: timestamp("expires", { mode: "date" }).notNull(),
52
+ },
53
+ (verificationToken) => ({
54
+ compositePk: primaryKey({
55
+ columns: [verificationToken.identifier, verificationToken.token],
56
+ }),
57
+ })
58
+ )
@@ -0,0 +1,58 @@
1
+ import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"
2
+ import type { AdapterAccountType } from "next-auth/adapters"
3
+
4
+ export const users = sqliteTable("user", {
5
+ id: text("id")
6
+ .primaryKey()
7
+ .$defaultFn(() => crypto.randomUUID()),
8
+ name: text("name"),
9
+ email: text("email").unique(),
10
+ emailVerified: integer("emailVerified", { mode: "timestamp_ms" }),
11
+ image: text("image"),
12
+ })
13
+
14
+ export const accounts = sqliteTable(
15
+ "account",
16
+ {
17
+ userId: text("userId")
18
+ .notNull()
19
+ .references(() => users.id, { onDelete: "cascade" }),
20
+ type: text("type").$type<AdapterAccountType>().notNull(),
21
+ provider: text("provider").notNull(),
22
+ providerAccountId: text("providerAccountId").notNull(),
23
+ refresh_token: text("refresh_token"),
24
+ access_token: text("access_token"),
25
+ expires_at: integer("expires_at"),
26
+ token_type: text("token_type"),
27
+ scope: text("scope"),
28
+ id_token: text("id_token"),
29
+ session_state: text("session_state"),
30
+ },
31
+ (account) => ({
32
+ compoundKey: primaryKey({
33
+ columns: [account.provider, account.providerAccountId],
34
+ }),
35
+ })
36
+ )
37
+
38
+ export const sessions = sqliteTable("session", {
39
+ sessionToken: text("sessionToken").primaryKey(),
40
+ userId: text("userId")
41
+ .notNull()
42
+ .references(() => users.id, { onDelete: "cascade" }),
43
+ expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
44
+ })
45
+
46
+ export const verificationTokens = sqliteTable(
47
+ "verificationToken",
48
+ {
49
+ identifier: text("identifier").notNull(),
50
+ token: text("token").notNull(),
51
+ expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
52
+ },
53
+ (verificationToken) => ({
54
+ compositePk: primaryKey({
55
+ columns: [verificationToken.identifier, verificationToken.token],
56
+ }),
57
+ })
58
+ )
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect, afterEach, vi } from "vitest"
2
+ import { createStorageClient } from "./storage"
3
+
4
+ describe("createStorageClient", () => {
5
+ afterEach(() => {
6
+ vi.unstubAllEnvs()
7
+ })
8
+
9
+ it("throws when STORAGE_PROVIDER=supabase and SUPABASE_URL is missing", () => {
10
+ vi.stubEnv("STORAGE_PROVIDER", "supabase")
11
+ vi.stubEnv("SUPABASE_URL", "")
12
+ vi.stubEnv("SUPABASE_SERVICE_ROLE_KEY", "")
13
+ expect(() => createStorageClient()).toThrow(/SUPABASE_URL/)
14
+ })
15
+
16
+ it("constructs a client when STORAGE_PROVIDER=supabase with credentials set", () => {
17
+ vi.stubEnv("STORAGE_PROVIDER", "supabase")
18
+ vi.stubEnv("SUPABASE_URL", "https://example.supabase.co")
19
+ vi.stubEnv("SUPABASE_SERVICE_ROLE_KEY", "fake-key")
20
+ expect(() => createStorageClient()).not.toThrow()
21
+ })
22
+
23
+ it("throws a clear not-implemented error for cloudflare-r2", () => {
24
+ vi.stubEnv("STORAGE_PROVIDER", "cloudflare-r2")
25
+ expect(() => createStorageClient()).toThrow(/not implemented/)
26
+ })
27
+
28
+ it("throws a clear not-implemented error for vercel-blob", () => {
29
+ vi.stubEnv("STORAGE_PROVIDER", "vercel-blob")
30
+ expect(() => createStorageClient()).toThrow(/not implemented/)
31
+ })
32
+ })
@@ -0,0 +1,62 @@
1
+ import { createClient } from "@supabase/supabase-js"
2
+
3
+ export interface StorageClient {
4
+ upload(
5
+ path: string,
6
+ file: Blob | Buffer,
7
+ contentType: string
8
+ ): Promise<{ path: string; publicUrl: string }>
9
+ getPublicUrl(path: string): string
10
+ remove(path: string): Promise<void>
11
+ }
12
+
13
+ class SupabaseStorageClient implements StorageClient {
14
+ private client: ReturnType<typeof createClient>
15
+ private bucket: string
16
+
17
+ constructor(supabaseUrl: string, supabaseKey: string, bucket: string) {
18
+ this.client = createClient(supabaseUrl, supabaseKey)
19
+ this.bucket = bucket
20
+ }
21
+
22
+ async upload(path: string, file: Blob | Buffer, contentType: string) {
23
+ const { data, error } = await this.client.storage
24
+ .from(this.bucket)
25
+ .upload(path, file, { contentType, upsert: true })
26
+ if (error) throw error
27
+ return { path: data.path, publicUrl: this.getPublicUrl(data.path) }
28
+ }
29
+
30
+ getPublicUrl(path: string): string {
31
+ const { data } = this.client.storage.from(this.bucket).getPublicUrl(path)
32
+ return data.publicUrl
33
+ }
34
+
35
+ async remove(path: string): Promise<void> {
36
+ const { error } = await this.client.storage.from(this.bucket).remove([path])
37
+ if (error) throw error
38
+ }
39
+ }
40
+
41
+ export type StorageProvider = "supabase" | "cloudflare-r2" | "vercel-blob"
42
+
43
+ export function createStorageClient(): StorageClient {
44
+ const storageProvider = (process.env.STORAGE_PROVIDER || "supabase") as StorageProvider
45
+
46
+ if (storageProvider === "supabase") {
47
+ const url = process.env.SUPABASE_URL
48
+ const key = process.env.SUPABASE_SERVICE_ROLE_KEY
49
+ const bucket = process.env.SUPABASE_STORAGE_BUCKET || "uploads"
50
+ if (!url || !key) {
51
+ throw new Error(
52
+ "SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required when STORAGE_PROVIDER=supabase. See .env.example."
53
+ )
54
+ }
55
+ return new SupabaseStorageClient(url, key, bucket)
56
+ }
57
+
58
+ throw new Error(
59
+ `STORAGE_PROVIDER="${storageProvider}" is not implemented yet — only "supabase" is wired up. ` +
60
+ "See 13-TECH-STACK/DB_PROVIDER_GUIDE.md for how to add Cloudflare R2 or Vercel Blob."
61
+ )
62
+ }
@@ -6,18 +6,25 @@
6
6
  "dev": "next dev",
7
7
  "build": "next build",
8
8
  "start": "next start",
9
- "lint": "next lint"
9
+ "lint": "next lint",
10
+ "test": "vitest run"
10
11
  },
11
12
  "dependencies": {
13
+ "@auth/drizzle-adapter": "^1.7.4",
14
+ "@neondatabase/serverless": "^0.10.3",
12
15
  "@radix-ui/react-avatar": "^1.1.0",
13
16
  "@radix-ui/react-dialog": "^1.1.1",
14
17
  "@radix-ui/react-dropdown-menu": "^2.1.1",
15
18
  "@radix-ui/react-slot": "^1.1.0",
16
19
  "@radix-ui/react-tabs": "^1.1.0",
20
+ "@supabase/supabase-js": "^2.46.1",
17
21
  "class-variance-authority": "^0.7.0",
18
22
  "clsx": "^2.1.1",
23
+ "drizzle-orm": "^0.36.0",
19
24
  "lucide-react": "^0.439.0",
20
- "next": "^16.2.9",
25
+ "next": "^15.3.0",
26
+ "next-auth": "5.0.0-beta.25",
27
+ "postgres": "^3.4.5",
21
28
  "react": "18.3.1",
22
29
  "react-dom": "18.3.1",
23
30
  "tailwind-merge": "^2.5.2"
@@ -27,10 +34,12 @@
27
34
  "@types/react": "^18.3.4",
28
35
  "@types/react-dom": "^18.3.0",
29
36
  "autoprefixer": "^10.4.20",
37
+ "drizzle-kit": "^0.28.1",
30
38
  "eslint": "^8.57.0",
31
- "eslint-config-next": "14.2.5",
39
+ "eslint-config-next": "^15.3.0",
32
40
  "postcss": "^8.4.41",
33
41
  "tailwindcss": "^3.4.10",
34
- "typescript": "^5.5.4"
42
+ "typescript": "^5.5.4",
43
+ "vitest": "^2.1.5"
35
44
  }
36
45
  }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vitest/config"
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "node",
6
+ },
7
+ })