@0x0501/mystack 0.0.0-stage → 0.1.0-bootstrap.0
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/LICENSE +21 -0
- package/README.md +68 -2
- package/assets/auth.sqlite.schema.ts +479 -0
- package/assets/bun-sqlite.test.ts +46 -0
- package/assets/d1.test.ts +41 -0
- package/assets/drizzle.config.ts +2 -0
- package/assets/generate-auth-schema.mjs +25 -0
- package/assets/platform.sqlite.schema.ts +46 -0
- package/assets/polar-checkout.ts +10 -0
- package/assets/polar-route.ts +19 -0
- package/assets/polar.test.ts +30 -0
- package/assets/polar.ts +12 -0
- package/assets/purchase.bun-sqlite.ts +38 -0
- package/assets/purchase.sqlite.ts +39 -0
- package/assets/sqlite-migrations/0000_thin_phalanx.sql +295 -0
- package/assets/sqlite-migrations/meta/0000_snapshot.json +2058 -0
- package/assets/sqlite-migrations/meta/_journal.json +13 -0
- package/assets/turso.test.ts +40 -0
- package/package.json +44 -5
- package/src/adapters/database.mjs +448 -0
- package/src/adapters/deployment.mjs +101 -0
- package/src/adapters/email.mjs +75 -0
- package/src/adapters/payments.mjs +270 -0
- package/src/catalog.mjs +128 -0
- package/src/cli.mjs +294 -0
- package/src/generate.mjs +183 -0
- package/src/template.mjs +93 -0
- package/template/.env.docker +16 -0
- package/template/.env.example +67 -0
- package/template/DESIGN.md +58 -0
- package/template/LICENSE +21 -0
- package/template/biome.json +46 -0
- package/template/docker-compose.yml +47 -0
- package/template/docs/project-guide.md +61 -0
- package/template/drizzle/0000_create-app-role.sql +54 -0
- package/template/drizzle/0001_add-rls-context.sql +21 -0
- package/template/drizzle/0002_platform_tables.sql +320 -0
- package/template/drizzle/meta/0002_snapshot.json +2125 -0
- package/template/drizzle/meta/_journal.json +27 -0
- package/template/drizzle.config.ts +25 -0
- package/template/e2e/authenticated-ui.spec.ts +40 -0
- package/template/e2e/headless-design.spec.ts +66 -0
- package/template/e2e/navigation-loading.spec.ts +70 -0
- package/template/e2e/theme-responsive.spec.ts +260 -0
- package/template/gitignore +24 -0
- package/template/messages/de.json +212 -0
- package/template/messages/en.json +212 -0
- package/template/package.json +101 -0
- package/template/paraglide.config.ts +49 -0
- package/template/playwright.config.ts +58 -0
- package/template/project.inlang/settings.json +12 -0
- package/template/public/drizzle.svg +1 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/logo192.png +0 -0
- package/template/public/logo512.png +0 -0
- package/template/public/manifest.json +25 -0
- package/template/public/robots.txt +3 -0
- package/template/scripts/check-client-bundle.ts +150 -0
- package/template/scripts/check-server-fns.ts +290 -0
- package/template/scripts/check-sql-raw.ts +50 -0
- package/template/scripts/compile-paraglide.ts +12 -0
- package/template/scripts/postgres/README.md +76 -0
- package/template/scripts/postgres/create-app-role.sh +60 -0
- package/template/scripts/seed-local.ts +331 -0
- package/template/scripts/validate-prod-env.ts +53 -0
- package/template/src/__tests__/README.md +9 -0
- package/template/src/__tests__/account-security.test.ts +664 -0
- package/template/src/__tests__/admin-http-surface.test.ts +311 -0
- package/template/src/__tests__/admin-peer-protection.test.ts +178 -0
- package/template/src/__tests__/auth-harness.ts +104 -0
- package/template/src/__tests__/auth-rate-limit.test.ts +43 -0
- package/template/src/__tests__/auth-redirect.test.ts +120 -0
- package/template/src/__tests__/body-limit.test.ts +47 -0
- package/template/src/__tests__/creem.test.ts +62 -0
- package/template/src/__tests__/demo-checkout.test.ts +141 -0
- package/template/src/__tests__/email-locale.test.ts +110 -0
- package/template/src/__tests__/global-setup.ts +80 -0
- package/template/src/__tests__/input-rules.test.ts +0 -0
- package/template/src/__tests__/message-parity.test.ts +38 -0
- package/template/src/__tests__/navigation-loading.test.tsx +252 -0
- package/template/src/__tests__/nowpayments.test.ts +106 -0
- package/template/src/__tests__/paraglide-url-contract.test.ts +87 -0
- package/template/src/__tests__/purchase-fulfillment.test.ts +204 -0
- package/template/src/__tests__/query-unauthorized-redirect.test.ts +95 -0
- package/template/src/__tests__/rls-policy.test.ts +120 -0
- package/template/src/__tests__/same-page-locale-href.test.ts +47 -0
- package/template/src/__tests__/search-params-oauth.test.ts +57 -0
- package/template/src/__tests__/security-headers.test.ts +12 -0
- package/template/src/__tests__/server-fn-error-contract.test.ts +173 -0
- package/template/src/__tests__/stripe.test.ts +84 -0
- package/template/src/__tests__/test-db.ts +32 -0
- package/template/src/__tests__/test-rls-db.ts +23 -0
- package/template/src/__tests__/user-update-guard.test.ts +109 -0
- package/template/src/components/LocaleSwitcher.tsx +79 -0
- package/template/src/components/ThemeToggle.tsx +47 -0
- package/template/src/components/auth/social.tsx +90 -0
- package/template/src/components/auth/turnstile.tsx +67 -0
- package/template/src/components/root/navigation-progress.tsx +32 -0
- package/template/src/components/root/not-found.tsx +20 -0
- package/template/src/components/root/pending.tsx +9 -0
- package/template/src/components/root/root-document.tsx +37 -0
- package/template/src/components/root/root-error.tsx +34 -0
- package/template/src/components/shells.tsx +201 -0
- package/template/src/components/ui/styles.ts +9 -0
- package/template/src/db/auth.schema.ts +455 -0
- package/template/src/db/helper.ts +38 -0
- package/template/src/db/index.ts +15 -0
- package/template/src/db/platform.schema.ts +122 -0
- package/template/src/db/schema.ts +2 -0
- package/template/src/env.ts +65 -0
- package/template/src/hooks/use-theme.ts +32 -0
- package/template/src/integrations/better-auth/header-user.tsx +36 -0
- package/template/src/integrations/better-auth/session-query.ts +11 -0
- package/template/src/integrations/react-email/PasswordOtpEmail.tsx +62 -0
- package/template/src/integrations/react-email/ResetPasswordEmail.tsx +64 -0
- package/template/src/integrations/react-email/VerifyEmail.tsx +72 -0
- package/template/src/integrations/react-email/theme.ts +75 -0
- package/template/src/integrations/tanstack-query/devtools.tsx +6 -0
- package/template/src/integrations/tanstack-query/root-provider.tsx +60 -0
- package/template/src/lib/auth-client.ts +27 -0
- package/template/src/lib/auth-paths.ts +94 -0
- package/template/src/lib/auth-plugins/existing-email-signup.ts +86 -0
- package/template/src/lib/auth-plugins/field-rules.ts +84 -0
- package/template/src/lib/auth-plugins/social-two-factor.ts +194 -0
- package/template/src/lib/auth-timing.ts +9 -0
- package/template/src/lib/auth.ts +279 -0
- package/template/src/lib/captcha.ts +11 -0
- package/template/src/lib/email.tsx +162 -0
- package/template/src/lib/github.ts +18 -0
- package/template/src/lib/locale-cookie.ts +12 -0
- package/template/src/lib/localized-pages.ts +25 -0
- package/template/src/lib/oauth-metadata.ts +19 -0
- package/template/src/lib/permissions.ts +29 -0
- package/template/src/lib/security-headers.ts +20 -0
- package/template/src/lib/site.ts +59 -0
- package/template/src/lib/theme.ts +42 -0
- package/template/src/lib/user-update-guard.ts +35 -0
- package/template/src/middlewares/admin.ts +46 -0
- package/template/src/middlewares/auth-rate-limit.ts +37 -0
- package/template/src/middlewares/better-auth.ts +15 -0
- package/template/src/middlewares/body-limit.ts +62 -0
- package/template/src/middlewares/database.ts +13 -0
- package/template/src/middlewares/protected.ts +45 -0
- package/template/src/routeTree.gen.ts +536 -0
- package/template/src/router.tsx +53 -0
- package/template/src/routes/[.]well-known/oauth-authorization-server/api/auth.ts +18 -0
- package/template/src/routes/[.]well-known/oauth-protected-resource/index.ts +32 -0
- package/template/src/routes/__root.tsx +175 -0
- package/template/src/routes/admin/audit.tsx +74 -0
- package/template/src/routes/admin/index.tsx +7 -0
- package/template/src/routes/admin/oauth-apps.tsx +164 -0
- package/template/src/routes/admin/route.tsx +27 -0
- package/template/src/routes/admin/users.tsx +215 -0
- package/template/src/routes/api/auth/$.ts +14 -0
- package/template/src/routes/api/auth/[.]well-known/openid-configuration.ts +15 -0
- package/template/src/routes/api/webhooks/creem.ts +69 -0
- package/template/src/routes/api/webhooks/nowpayments.ts +62 -0
- package/template/src/routes/api/webhooks/stripe.ts +74 -0
- package/template/src/routes/dashboard/account.tsx +430 -0
- package/template/src/routes/dashboard/index.tsx +128 -0
- package/template/src/routes/dashboard/route.tsx +17 -0
- package/template/src/routes/forgot-password.tsx +65 -0
- package/template/src/routes/index.tsx +85 -0
- package/template/src/routes/oauth/consent.tsx +77 -0
- package/template/src/routes/reset-password.tsx +78 -0
- package/template/src/routes/sign-in.tsx +174 -0
- package/template/src/routes/sign-up.tsx +135 -0
- package/template/src/routes/two-factor.tsx +74 -0
- package/template/src/server/README.md +3 -0
- package/template/src/server/account-security.ts +163 -0
- package/template/src/server/admin-audit.ts +37 -0
- package/template/src/server/admin-users.ts +112 -0
- package/template/src/server/auth.ts +44 -0
- package/template/src/server/demo-checkout.ts +67 -0
- package/template/src/server/oauth-clients.ts +83 -0
- package/template/src/server/payment-toggles.ts +39 -0
- package/template/src/server.ts +20 -0
- package/template/src/services/README.md +3 -0
- package/template/src/services/account-security.ts +893 -0
- package/template/src/services/admin-security.ts +164 -0
- package/template/src/services/admin-users.ts +181 -0
- package/template/src/services/auth-rate-limit.ts +32 -0
- package/template/src/services/creem.ts +83 -0
- package/template/src/services/demo-checkout.ts +100 -0
- package/template/src/services/nowpayments.ts +154 -0
- package/template/src/services/oauth-client.ts +85 -0
- package/template/src/services/oauth-clients.ts +15 -0
- package/template/src/services/payment-providers.ts +157 -0
- package/template/src/services/payment-toggles.ts +48 -0
- package/template/src/services/purchase.ts +233 -0
- package/template/src/services/stripe.ts +106 -0
- package/template/src/start.ts +35 -0
- package/template/src/styles/layout.css +93 -0
- package/template/src/styles/theme.css +84 -0
- package/template/src/styles/ui.css +233 -0
- package/template/src/styles.css +90 -0
- package/template/src/utils/api-error.ts +129 -0
- package/template/src/utils/auth-errors.ts +157 -0
- package/template/src/utils/auth-redirect.ts +61 -0
- package/template/src/utils/body-limit.ts +84 -0
- package/template/src/utils/cn.ts +6 -0
- package/template/src/utils/input-rules.ts +172 -0
- package/template/src/utils/locale-paths.ts +18 -0
- package/template/src/utils/same-page-locale-href.ts +20 -0
- package/template/src/utils/search-params.ts +71 -0
- package/template/tsconfig.json +30 -0
- package/template/tsr.config.json +6 -0
- package/template/vite.config.ts +24 -0
- package/template/vitest.config.ts +26 -0
- package/template/worker-configuration.d.ts +14660 -0
- package/template/wrangler.jsonc +39 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { afterAll, beforeAll, expect, test } from "vitest";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { createClient } from "@libsql/client";
|
|
7
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
8
|
+
import * as schema from "@/db/schema";
|
|
9
|
+
import { purchase, purchaseDelivery, session, user } from "@/db/schema";
|
|
10
|
+
import { revokeUserCredentials } from "@/services/admin-security";
|
|
11
|
+
import { createPendingPurchase, markPurchasePaid } from "@/services/purchase";
|
|
12
|
+
import { eq } from "drizzle-orm";
|
|
13
|
+
const temporary = mkdtempSync(join(tmpdir(), "mystack-db-"));
|
|
14
|
+
const db = drizzle(createClient({ url: `file:${join(temporary, "test.db")}` }), { schema });
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
await db.$client.executeMultiple(await readFile("drizzle/0000_thin_phalanx.sql", "utf8"));
|
|
17
|
+
await db.insert(user).values({ id: "test-user", email: "test@example.com", name: "Test", emailVerified: true });
|
|
18
|
+
});
|
|
19
|
+
afterAll(() => { db.$client.close(); rmSync(temporary, { recursive: true, force: true }); });
|
|
20
|
+
test("concurrent confirmations deliver a purchase once", async () => {
|
|
21
|
+
const { purchaseId } = await createPendingPurchase(db, { userId: "test-user", provider: "stripe", externalId: "checkout-1", amount: 500 });
|
|
22
|
+
const outcomes = await Promise.all([1, 2].map(() => markPurchasePaid(db, { provider: "stripe", externalId: "checkout-1", reportedAmount: 500 })));
|
|
23
|
+
expect(outcomes.map((outcome) => outcome.status).sort()).toEqual(["already_paid", "fulfilled"]);
|
|
24
|
+
expect((await db.select().from(purchase).where(eq(purchase.id, purchaseId)))[0].status).toBe("paid");
|
|
25
|
+
expect(await db.select().from(purchaseDelivery).where(eq(purchaseDelivery.purchaseId, purchaseId))).toHaveLength(1);
|
|
26
|
+
});
|
|
27
|
+
test("failed delivery insertion rolls back the paid flip and permits retry", async () => {
|
|
28
|
+
const { purchaseId } = await createPendingPurchase(db, { userId: "test-user", provider: "stripe", externalId: "checkout-2", amount: 500 });
|
|
29
|
+
await db.$client.execute("CREATE TRIGGER reject_delivery BEFORE INSERT ON purchase_delivery BEGIN SELECT RAISE(ABORT, 'reject'); END");
|
|
30
|
+
try { await expect(markPurchasePaid(db, { provider: "stripe", externalId: "checkout-2" })).rejects.toThrow(); }
|
|
31
|
+
finally { await db.$client.execute("DROP TRIGGER reject_delivery"); }
|
|
32
|
+
expect((await db.select().from(purchase).where(eq(purchase.id, purchaseId)))[0].status).toBe("pending");
|
|
33
|
+
expect((await markPurchasePaid(db, { provider: "stripe", externalId: "checkout-2" })).status).toBe("fulfilled");
|
|
34
|
+
});
|
|
35
|
+
test("credential revocation clears sessions and records token revocation time", async () => {
|
|
36
|
+
await db.insert(session).values({ id: "test-session", userId: "test-user", token: "test-token", expiresAt: new Date(Date.now() + 60_000) });
|
|
37
|
+
await revokeUserCredentials(db, "test-user");
|
|
38
|
+
expect(await db.select().from(session)).toEqual([]);
|
|
39
|
+
expect((await db.select().from(user).where(eq(user.id, "test-user")))[0].tokensRevokedAt).toBeInstanceOf(Date);
|
|
40
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@0x0501/mystack",
|
|
3
|
+
"version": "0.1.0-bootstrap.0",
|
|
4
|
+
"description": "Create a configured TanStack Start application",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/0x0501/tanstack-starter.git",
|
|
9
|
+
"directory": "packages/mystack"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/0x0501/tanstack-starter/tree/main/packages/mystack#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/0x0501/tanstack-starter/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22.12.0"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"mystack": "src/cli.mjs"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"template",
|
|
25
|
+
"assets",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"template:prepare": "node scripts/prepare-template.mjs",
|
|
31
|
+
"prepack": "npm run template:prepare",
|
|
32
|
+
"test": "bun run --cwd ../.. test:cli",
|
|
33
|
+
"test:generated": "node scripts/verify-generated.mjs",
|
|
34
|
+
"test:packed": "node scripts/verify-packed.mjs"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@biomejs/biome": "2.5.15",
|
|
38
|
+
"@clack/prompts": "^1.8.1",
|
|
39
|
+
"@tanstack/router-generator": "1.167.38"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"registry": "https://registry.npmjs.org"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { isPostgres } from "../catalog.mjs";
|
|
5
|
+
import { replace, text } from "../template.mjs";
|
|
6
|
+
|
|
7
|
+
export async function configureDatabase(context) {
|
|
8
|
+
const { files, selection, pkg } = context;
|
|
9
|
+
// Schema generation must not instantiate an empty auth adapter at app startup.
|
|
10
|
+
replace(
|
|
11
|
+
files,
|
|
12
|
+
"src/lib/auth.ts",
|
|
13
|
+
"// For better-auth schema generation only.\nexport const auth = createAuth({} as unknown as Database);",
|
|
14
|
+
"export type Auth = ReturnType<typeof createAuth>;",
|
|
15
|
+
);
|
|
16
|
+
replace(
|
|
17
|
+
files,
|
|
18
|
+
"src/lib/auth-client.ts",
|
|
19
|
+
"import type { auth }",
|
|
20
|
+
"import type { Auth }",
|
|
21
|
+
);
|
|
22
|
+
replace(
|
|
23
|
+
files,
|
|
24
|
+
"src/lib/auth-client.ts",
|
|
25
|
+
"inferAdditionalFields<typeof auth>()",
|
|
26
|
+
"inferAdditionalFields<Auth>()",
|
|
27
|
+
);
|
|
28
|
+
files.set(
|
|
29
|
+
"scripts/generate-auth-schema.mjs",
|
|
30
|
+
await readFile(
|
|
31
|
+
new URL("../../assets/generate-auth-schema.mjs", import.meta.url),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
pkg.scripts["auth:generate"] =
|
|
35
|
+
"bun run paraglide:compile && bun scripts/generate-auth-schema.mjs";
|
|
36
|
+
if (isPostgres(selection.database)) {
|
|
37
|
+
if (selection.database === "pg") {
|
|
38
|
+
files.set(
|
|
39
|
+
"src/db/index.ts",
|
|
40
|
+
`import { drizzle } from "drizzle-orm/node-postgres";
|
|
41
|
+
import { Pool } from "pg";
|
|
42
|
+
import * as schema from "./schema";
|
|
43
|
+
export function createDB(connectionString: string) {
|
|
44
|
+
return drizzle(new Pool({ connectionString }), { schema });
|
|
45
|
+
}
|
|
46
|
+
export type Database = ReturnType<typeof createDB>;
|
|
47
|
+
export type DatabaseTxCallback = Parameters<Database["transaction"]>[0];
|
|
48
|
+
export type DatabaseTx = Parameters<DatabaseTxCallback>[0];
|
|
49
|
+
`,
|
|
50
|
+
);
|
|
51
|
+
files.set(
|
|
52
|
+
"src/middlewares/database.ts",
|
|
53
|
+
`import { createMiddleware } from "@tanstack/react-start";
|
|
54
|
+
import { createDB } from "@/db";
|
|
55
|
+
import { env } from "@/env";
|
|
56
|
+
let database: ReturnType<typeof createDB> | undefined;
|
|
57
|
+
export const databaseMiddleware = createMiddleware().server(({ next }) => {
|
|
58
|
+
database ??= createDB(env.DATABASE_URL);
|
|
59
|
+
return next({ context: { db: database } });
|
|
60
|
+
});
|
|
61
|
+
`,
|
|
62
|
+
);
|
|
63
|
+
replace(
|
|
64
|
+
files,
|
|
65
|
+
"src/env.ts",
|
|
66
|
+
"\t\t\tAPP_ORIGIN: z.url(),",
|
|
67
|
+
"\t\t\tDATABASE_URL: z.string().min(1),\n\t\t\tAPP_ORIGIN: z.url(),",
|
|
68
|
+
);
|
|
69
|
+
files.set(
|
|
70
|
+
".env.example",
|
|
71
|
+
`${text(files, ".env.example")}\n# Runtime app role; never use DATABASE_MIGRATION_URL here.\nDATABASE_URL=postgresql://starter_app:starter_app_dev_password@localhost:5432/starter\n`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
files.set(
|
|
77
|
+
"src/services/purchase.ts",
|
|
78
|
+
await readFile(
|
|
79
|
+
new URL(
|
|
80
|
+
selection.database === "bun-sqlite"
|
|
81
|
+
? "../../assets/purchase.bun-sqlite.ts"
|
|
82
|
+
: "../../assets/purchase.sqlite.ts",
|
|
83
|
+
import.meta.url,
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
delete pkg.scripts["db:push"];
|
|
88
|
+
delete pkg.scripts["db:studio"];
|
|
89
|
+
delete pkg.dependencies.pg;
|
|
90
|
+
delete pkg.devDependencies["@types/pg"];
|
|
91
|
+
delete pkg.devDependencies["@testcontainers/postgresql"];
|
|
92
|
+
for (const path of files.keys()) {
|
|
93
|
+
if (
|
|
94
|
+
path.startsWith("drizzle/") ||
|
|
95
|
+
path.startsWith("scripts/postgres/") ||
|
|
96
|
+
path === ".env.docker" ||
|
|
97
|
+
path.includes("compose")
|
|
98
|
+
)
|
|
99
|
+
files.delete(path);
|
|
100
|
+
}
|
|
101
|
+
for (const command of [
|
|
102
|
+
"db:up",
|
|
103
|
+
"db:down",
|
|
104
|
+
"db:reset",
|
|
105
|
+
"db:seed",
|
|
106
|
+
"seed:local",
|
|
107
|
+
"db:pull",
|
|
108
|
+
])
|
|
109
|
+
delete pkg.scripts[command];
|
|
110
|
+
files.delete("scripts/seed-local.ts");
|
|
111
|
+
files.set(
|
|
112
|
+
"src/db/auth.schema.ts",
|
|
113
|
+
await readFile(
|
|
114
|
+
new URL("../../assets/auth.sqlite.schema.ts", import.meta.url),
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
files.set(
|
|
118
|
+
"src/db/platform.schema.ts",
|
|
119
|
+
(
|
|
120
|
+
await readFile(
|
|
121
|
+
new URL("../../assets/platform.sqlite.schema.ts", import.meta.url),
|
|
122
|
+
"utf8",
|
|
123
|
+
)
|
|
124
|
+
).replace("./auth.sqlite.schema", "./auth.schema"),
|
|
125
|
+
);
|
|
126
|
+
replace(
|
|
127
|
+
files,
|
|
128
|
+
"src/lib/auth.ts",
|
|
129
|
+
'provider: "pg"',
|
|
130
|
+
'provider: "sqlite",\n\t\t\ttransaction: false',
|
|
131
|
+
);
|
|
132
|
+
// SQLite has no PG session GUCs. All user queries carry explicit ownership
|
|
133
|
+
// predicates; admin writes are guarded by admin middleware.
|
|
134
|
+
files.set(
|
|
135
|
+
"src/db/helper.ts",
|
|
136
|
+
`import type { Database } from ".";
|
|
137
|
+
export async function withDatabaseUser<T>(db: Database, _userId: string, fn: (db: Database) => T | Promise<T>): Promise<T> { return fn(db); }
|
|
138
|
+
export async function withDatabaseService<T>(db: Database, fn: (db: Database) => T | Promise<T>): Promise<T> { return fn(db); }
|
|
139
|
+
`,
|
|
140
|
+
);
|
|
141
|
+
for (const [path, contents] of files) {
|
|
142
|
+
if (!path.endsWith(".ts") && !path.endsWith(".tsx")) continue;
|
|
143
|
+
files.set(
|
|
144
|
+
path,
|
|
145
|
+
contents
|
|
146
|
+
.toString()
|
|
147
|
+
.replaceAll("withRlsUser", "withDatabaseUser")
|
|
148
|
+
.replaceAll("withRlsService", "withDatabaseService"),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
replace(files, "src/services/admin-users.ts", "ilike", "like");
|
|
152
|
+
replace(
|
|
153
|
+
files,
|
|
154
|
+
"src/services/admin-users.ts",
|
|
155
|
+
"ilike(user.email, q), ilike(user.name, q)",
|
|
156
|
+
"like(user.email, q), like(user.name, q)",
|
|
157
|
+
);
|
|
158
|
+
// Database integration tests need a dialect-specific harness, not the PG one.
|
|
159
|
+
for (const [path, content] of files) {
|
|
160
|
+
if (!path.startsWith("src/__tests__/")) continue;
|
|
161
|
+
if (
|
|
162
|
+
/\/(global-setup|test-db|test-rls-db|auth-harness|rls-policy\.test)/.test(
|
|
163
|
+
path,
|
|
164
|
+
) ||
|
|
165
|
+
/from ["']\.\/(test-db|test-rls-db|auth-harness)/.test(content.toString())
|
|
166
|
+
)
|
|
167
|
+
files.delete(path);
|
|
168
|
+
}
|
|
169
|
+
replace(
|
|
170
|
+
files,
|
|
171
|
+
"vitest.config.ts",
|
|
172
|
+
'\t\tglobalSetup: ["./src/__tests__/global-setup.ts"],\n',
|
|
173
|
+
"",
|
|
174
|
+
);
|
|
175
|
+
files.set(
|
|
176
|
+
".env.example",
|
|
177
|
+
text(files, ".env.example").replace(
|
|
178
|
+
/# —————————————————— Database[\s\S]*?(?=# ——————————————————)/,
|
|
179
|
+
"",
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
const service = text(files, "src/services/admin-security.ts");
|
|
183
|
+
const start = service.indexOf("\tawait db.transaction(async (tx) => {");
|
|
184
|
+
const end = service.indexOf("\n\t});", start);
|
|
185
|
+
if (start < 0 || end < 0)
|
|
186
|
+
throw new Error("Credential revocation anchor changed.");
|
|
187
|
+
files.set(
|
|
188
|
+
"src/services/admin-security.ts",
|
|
189
|
+
service.slice(0, start) +
|
|
190
|
+
(selection.database === "bun-sqlite"
|
|
191
|
+
? `\t// Bun SQLite transactions must stay synchronous.
|
|
192
|
+
db.transaction((tx) => {
|
|
193
|
+
tx.delete(oauthAccessToken).where(eq(oauthAccessToken.userId, userId)).run();
|
|
194
|
+
tx.delete(oauthRefreshToken).where(eq(oauthRefreshToken.userId, userId)).run();
|
|
195
|
+
tx.delete(session).where(eq(session.userId, userId)).run();
|
|
196
|
+
tx.update(user).set({ tokensRevokedAt: new Date() }).where(eq(user.id, userId)).run();
|
|
197
|
+
});`
|
|
198
|
+
: `\tawait db.batch([
|
|
199
|
+
db.delete(oauthAccessToken).where(eq(oauthAccessToken.userId, userId)),
|
|
200
|
+
db.delete(oauthRefreshToken).where(eq(oauthRefreshToken.userId, userId)),
|
|
201
|
+
db.delete(session).where(eq(session.userId, userId)),
|
|
202
|
+
db.update(user).set({ tokensRevokedAt: new Date() }).where(eq(user.id, userId)),
|
|
203
|
+
]);`) +
|
|
204
|
+
service.slice(end + "\n\t});".length),
|
|
205
|
+
);
|
|
206
|
+
if (
|
|
207
|
+
selection.database === "turso-cloud" ||
|
|
208
|
+
selection.database === "turso-local"
|
|
209
|
+
) {
|
|
210
|
+
await configureTurso(context);
|
|
211
|
+
} else if (selection.database === "bun-sqlite") {
|
|
212
|
+
await configureBunSQLite(context);
|
|
213
|
+
} else {
|
|
214
|
+
files.set(
|
|
215
|
+
"src/db/index.ts",
|
|
216
|
+
`import { drizzle } from "drizzle-orm/d1";
|
|
217
|
+
import * as schema from "./schema";
|
|
218
|
+
export function createDB(binding: D1Database) { return drizzle(binding, { schema }); }
|
|
219
|
+
export type Database = ReturnType<typeof createDB>;
|
|
220
|
+
`,
|
|
221
|
+
);
|
|
222
|
+
replace(files, "src/middlewares/database.ts", "env.HYPERDRIVE", "env.DB");
|
|
223
|
+
replace(
|
|
224
|
+
files,
|
|
225
|
+
"worker-configuration.d.ts",
|
|
226
|
+
"interface __BaseEnv_Env {",
|
|
227
|
+
"interface __BaseEnv_Env {\n\tDB: D1Database;",
|
|
228
|
+
);
|
|
229
|
+
files.set(
|
|
230
|
+
"drizzle.config.ts",
|
|
231
|
+
`import { defineConfig } from "drizzle-kit";
|
|
232
|
+
export default defineConfig({ out: "./drizzle", schema: "./src/db/schema.ts", dialect: "sqlite" });
|
|
233
|
+
`,
|
|
234
|
+
);
|
|
235
|
+
pkg.scripts["db:migrate"] = "wrangler d1 migrations apply DB --local";
|
|
236
|
+
pkg.scripts["db:migrate:remote"] =
|
|
237
|
+
"wrangler d1 migrations apply DB --remote";
|
|
238
|
+
delete pkg.scripts["db:push"];
|
|
239
|
+
delete pkg.scripts["db:studio"];
|
|
240
|
+
files.set(
|
|
241
|
+
"src/__tests__/database-adapter.test.ts",
|
|
242
|
+
await readFile(new URL("../../assets/d1.test.ts", import.meta.url)),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (selection.deployment === "cloudflare") {
|
|
246
|
+
replace(
|
|
247
|
+
files,
|
|
248
|
+
"worker-configuration.d.ts",
|
|
249
|
+
"\tHYPERDRIVE: Hyperdrive;\n",
|
|
250
|
+
"",
|
|
251
|
+
);
|
|
252
|
+
files.set(
|
|
253
|
+
"wrangler.jsonc",
|
|
254
|
+
text(files, "wrangler.jsonc").replace(
|
|
255
|
+
/\t"hyperdrive": \[[\s\S]*?\n\t\],\n/,
|
|
256
|
+
selection.database === "d1"
|
|
257
|
+
? `\t"d1_databases": [{ "binding": "DB", "database_name": "${context.name}", "database_id": "00000000-0000-0000-0000-000000000000", "migrations_dir": "drizzle" }],\n`
|
|
258
|
+
: "",
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
// Bundle checked-in migrations generated by drizzle-kit for the SQLite schema.
|
|
263
|
+
const { readdir } = await import("node:fs/promises");
|
|
264
|
+
const migrationRoot = new URL(
|
|
265
|
+
"../../assets/sqlite-migrations/",
|
|
266
|
+
import.meta.url,
|
|
267
|
+
);
|
|
268
|
+
for (const entry of await readdir(migrationRoot, {
|
|
269
|
+
recursive: true,
|
|
270
|
+
withFileTypes: true,
|
|
271
|
+
})) {
|
|
272
|
+
if (!entry.isFile()) continue;
|
|
273
|
+
const path = join(entry.parentPath, entry.name);
|
|
274
|
+
const name = relative(fileURLToPath(migrationRoot), path);
|
|
275
|
+
files.set(`drizzle/${name}`, await readFile(path));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function configureTurso({ files, pkg, selection }) {
|
|
280
|
+
const cloud = selection.database === "turso-cloud";
|
|
281
|
+
pkg.dependencies["@libsql/client"] = "^0.17.0";
|
|
282
|
+
files.set(
|
|
283
|
+
"src/db/index.ts",
|
|
284
|
+
`import { createClient } from "${selection.deployment === "cloudflare" ? "@libsql/client/web" : "@libsql/client"}";
|
|
285
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
286
|
+
import * as schema from "./schema";
|
|
287
|
+
export function createDB(url: string${cloud ? ", authToken: string" : ""}) {
|
|
288
|
+
return drizzle(createClient({ url${cloud ? ", authToken" : ""} }), { schema });
|
|
289
|
+
}
|
|
290
|
+
export type Database = ReturnType<typeof createDB>;
|
|
291
|
+
`,
|
|
292
|
+
);
|
|
293
|
+
files.set(
|
|
294
|
+
"src/middlewares/database.ts",
|
|
295
|
+
`import { createMiddleware } from "@tanstack/react-start";
|
|
296
|
+
import { createDB } from "@/db";
|
|
297
|
+
import { env } from "@/env";
|
|
298
|
+
${selection.deployment === "cloudflare" ? "" : "let database: ReturnType<typeof createDB> | undefined;"}
|
|
299
|
+
export const databaseMiddleware = createMiddleware().server(({ next }) => {
|
|
300
|
+
${selection.deployment === "cloudflare" ? "const database =" : "database ??="} createDB(env.TURSO_DATABASE_URL${cloud ? ", env.TURSO_AUTH_TOKEN" : ""});
|
|
301
|
+
return next({ context: { db: database } });
|
|
302
|
+
});
|
|
303
|
+
`,
|
|
304
|
+
);
|
|
305
|
+
replace(
|
|
306
|
+
files,
|
|
307
|
+
"src/env.ts",
|
|
308
|
+
"\t\t\tAPP_ORIGIN: z.url(),",
|
|
309
|
+
`${
|
|
310
|
+
cloud
|
|
311
|
+
? '\t\t\tTURSO_DATABASE_URL: z.string().regex(/^(libsql|https):\\/\\//, "Use a remote Turso Cloud URL"),\n\t\t\tTURSO_AUTH_TOKEN: z.string().min(1),'
|
|
312
|
+
: '\t\t\tTURSO_DATABASE_URL: z.string().startsWith("file:"),'
|
|
313
|
+
}\n\t\t\tAPP_ORIGIN: z.url(),`,
|
|
314
|
+
);
|
|
315
|
+
files.set(
|
|
316
|
+
".env.example",
|
|
317
|
+
`${text(files, ".env.example")}\n${
|
|
318
|
+
cloud
|
|
319
|
+
? "# Turso Cloud: copy the URL and token from your Turso database.\nTURSO_DATABASE_URL=libsql://your-database.turso.io\nTURSO_AUTH_TOKEN=replace-with-your-turso-auth-token"
|
|
320
|
+
: "# Turso Local: a local libSQL file; no cloud credentials required.\nTURSO_DATABASE_URL=file:local.db"
|
|
321
|
+
}\n`,
|
|
322
|
+
);
|
|
323
|
+
files.set(
|
|
324
|
+
"drizzle.config.ts",
|
|
325
|
+
`import { config } from "dotenv";
|
|
326
|
+
import { defineConfig } from "drizzle-kit";
|
|
327
|
+
config({ path: ".env" }); config({ path: ".env.local", override: true });
|
|
328
|
+
export default defineConfig({ out: "./drizzle", schema: "./src/db/schema.ts", dialect: "turso", dbCredentials: { url: process.env.TURSO_DATABASE_URL ?? "${cloud ? "" : "file:local.db"}"${cloud ? ", authToken: process.env.TURSO_AUTH_TOKEN" : ""} } });
|
|
329
|
+
`,
|
|
330
|
+
);
|
|
331
|
+
files.set(
|
|
332
|
+
"src/__tests__/database-adapter.test.ts",
|
|
333
|
+
await readFile(new URL("../../assets/turso.test.ts", import.meta.url)),
|
|
334
|
+
);
|
|
335
|
+
pkg.scripts["db:migrate"] = "bun scripts/migrate-turso.ts";
|
|
336
|
+
files.set(
|
|
337
|
+
"scripts/migrate-turso.ts",
|
|
338
|
+
`import { createClient } from "@libsql/client";
|
|
339
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
340
|
+
import { migrate } from "drizzle-orm/libsql/migrator";
|
|
341
|
+
const url = process.env.TURSO_DATABASE_URL;
|
|
342
|
+
if (!url${cloud ? " || !/^(libsql|https):\\/\\//.test(url) || !process.env.TURSO_AUTH_TOKEN" : ' || !url.startsWith("file:")'}) throw new Error("${cloud ? "Set a remote TURSO_DATABASE_URL and TURSO_AUTH_TOKEN" : "Set a file: TURSO_DATABASE_URL"} in .env before running migrations.");
|
|
343
|
+
const client = createClient({ url${cloud ? ", authToken: process.env.TURSO_AUTH_TOKEN" : ""} });
|
|
344
|
+
try { await migrate(drizzle(client), { migrationsFolder: "./drizzle" }); }
|
|
345
|
+
finally { client.close(); }
|
|
346
|
+
`,
|
|
347
|
+
);
|
|
348
|
+
if (cloud) {
|
|
349
|
+
replace(
|
|
350
|
+
files,
|
|
351
|
+
"scripts/validate-prod-env.ts",
|
|
352
|
+
'\t"APP_ORIGIN",',
|
|
353
|
+
'\t"TURSO_DATABASE_URL",\n\t"TURSO_AUTH_TOKEN",\n\t"APP_ORIGIN",',
|
|
354
|
+
);
|
|
355
|
+
replace(
|
|
356
|
+
files,
|
|
357
|
+
"scripts/validate-prod-env.ts",
|
|
358
|
+
"\tBETTER_AUTH_SECRET:",
|
|
359
|
+
'\tTURSO_AUTH_TOKEN: "replace-with-your-turso-auth-token",\n\tTURSO_DATABASE_URL: "libsql://your-database.turso.io",\n\tBETTER_AUTH_SECRET:',
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
files.set(
|
|
363
|
+
".gitignore",
|
|
364
|
+
`${text(files, ".gitignore")}\n*.db\n*.db-shm\n*.db-wal\n`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function configureBunSQLite({ files, pkg }) {
|
|
369
|
+
pkg.scripts.dev = "bun --bun vite dev --port 3000";
|
|
370
|
+
files.set(
|
|
371
|
+
"src/db/index.ts",
|
|
372
|
+
`import { Database as SQLite } from "bun:sqlite";
|
|
373
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
374
|
+
import * as schema from "./schema";
|
|
375
|
+
export function createDB(filename: string) {
|
|
376
|
+
const client = new SQLite(filename, { create: true, strict: true });
|
|
377
|
+
client.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;");
|
|
378
|
+
return drizzle(client, { schema });
|
|
379
|
+
}
|
|
380
|
+
export type Database = ReturnType<typeof createDB>;
|
|
381
|
+
`,
|
|
382
|
+
);
|
|
383
|
+
files.set(
|
|
384
|
+
"src/middlewares/database.ts",
|
|
385
|
+
`import { createMiddleware } from "@tanstack/react-start";
|
|
386
|
+
import { createDB } from "@/db";
|
|
387
|
+
import { env } from "@/env";
|
|
388
|
+
let database: ReturnType<typeof createDB> | undefined;
|
|
389
|
+
export const databaseMiddleware = createMiddleware().server(({ next }) => {
|
|
390
|
+
database ??= createDB(env.SQLITE_DATABASE_PATH);
|
|
391
|
+
return next({ context: { db: database } });
|
|
392
|
+
});
|
|
393
|
+
`,
|
|
394
|
+
);
|
|
395
|
+
replace(
|
|
396
|
+
files,
|
|
397
|
+
"src/env.ts",
|
|
398
|
+
"\t\t\tAPP_ORIGIN: z.url(),",
|
|
399
|
+
"\t\t\tSQLITE_DATABASE_PATH: z.string().min(1),\n\t\t\tAPP_ORIGIN: z.url(),",
|
|
400
|
+
);
|
|
401
|
+
files.set(
|
|
402
|
+
".env.example",
|
|
403
|
+
`${text(files, ".env.example")}\n# Bun native SQLite file. Keep this file on persistent storage.\nSQLITE_DATABASE_PATH=local.db\n`,
|
|
404
|
+
);
|
|
405
|
+
files.set(
|
|
406
|
+
"drizzle.config.ts",
|
|
407
|
+
`import { config } from "dotenv";
|
|
408
|
+
import { defineConfig } from "drizzle-kit";
|
|
409
|
+
config({ path: ".env" }); config({ path: ".env.local", override: true });
|
|
410
|
+
export default defineConfig({ out: "./drizzle", schema: "./src/db/schema.ts", dialect: "sqlite", dbCredentials: { url: process.env.SQLITE_DATABASE_PATH ?? "local.db" } });
|
|
411
|
+
`,
|
|
412
|
+
);
|
|
413
|
+
pkg.scripts["db:migrate"] = "bun scripts/migrate-bun-sqlite.ts";
|
|
414
|
+
files.set(
|
|
415
|
+
"scripts/migrate-bun-sqlite.ts",
|
|
416
|
+
`import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
|
417
|
+
import { createDB } from "../src/db";
|
|
418
|
+
const filename = process.env.SQLITE_DATABASE_PATH;
|
|
419
|
+
if (!filename) throw new Error("Set SQLITE_DATABASE_PATH in .env before running migrations.");
|
|
420
|
+
const db = createDB(filename);
|
|
421
|
+
try { migrate(db, { migrationsFolder: "./drizzle" }); }
|
|
422
|
+
finally { db.$client.close(); }
|
|
423
|
+
`,
|
|
424
|
+
);
|
|
425
|
+
replace(
|
|
426
|
+
files,
|
|
427
|
+
"vite.config.ts",
|
|
428
|
+
"\tresolve: { tsconfigPaths: true },",
|
|
429
|
+
'\tresolve: { tsconfigPaths: true },\n\tssr: { external: ["bun:sqlite"] },',
|
|
430
|
+
);
|
|
431
|
+
files.set(
|
|
432
|
+
"src/__tests__/database-adapter.test.ts",
|
|
433
|
+
await readFile(new URL("../../assets/bun-sqlite.test.ts", import.meta.url)),
|
|
434
|
+
);
|
|
435
|
+
replace(
|
|
436
|
+
files,
|
|
437
|
+
"vitest.config.ts",
|
|
438
|
+
"\t\ttestTimeout: 30_000,",
|
|
439
|
+
'\t\texclude: ["src/__tests__/database-adapter.test.ts"],\n\t\ttestTimeout: 30_000,',
|
|
440
|
+
);
|
|
441
|
+
for (const command of ["test", "test:unit"])
|
|
442
|
+
pkg.scripts[command] +=
|
|
443
|
+
" && bun test src/__tests__/database-adapter.test.ts";
|
|
444
|
+
files.set(
|
|
445
|
+
".gitignore",
|
|
446
|
+
`${text(files, ".gitignore")}\n*.db\n*.db-shm\n*.db-wal\n`,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { replace, text } from "../template.mjs";
|
|
2
|
+
|
|
3
|
+
export async function configureDeployment({ files, pkg, selection, name }) {
|
|
4
|
+
if (selection.deployment === "cloudflare") {
|
|
5
|
+
replace(
|
|
6
|
+
files,
|
|
7
|
+
"wrangler.jsonc",
|
|
8
|
+
'"name": "tanstack-starter"',
|
|
9
|
+
`"name": "${name}"`,
|
|
10
|
+
);
|
|
11
|
+
pkg.scripts["cf:typegen"] = "wrangler types";
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
files.delete("wrangler.jsonc");
|
|
15
|
+
files.delete("worker-configuration.d.ts");
|
|
16
|
+
delete pkg.dependencies["@cloudflare/vite-plugin"];
|
|
17
|
+
delete pkg.devDependencies.wrangler;
|
|
18
|
+
replace(
|
|
19
|
+
files,
|
|
20
|
+
"src/lib/auth.ts",
|
|
21
|
+
'import { waitUntil } from "cloudflare:workers";\n',
|
|
22
|
+
"",
|
|
23
|
+
);
|
|
24
|
+
// Server runtimes keep promises alive without Workers waitUntil.
|
|
25
|
+
replace(
|
|
26
|
+
files,
|
|
27
|
+
"src/lib/auth.ts",
|
|
28
|
+
"\t\tadvanced: {\n\t\t\tbackgroundTasks: {\n\t\t\t\thandler: (promise) => {\n\t\t\t\t\twaitUntil(promise);\n\t\t\t\t},\n\t\t\t},\n\t\t},\n",
|
|
29
|
+
"",
|
|
30
|
+
);
|
|
31
|
+
files.set(
|
|
32
|
+
"src/middlewares/auth-rate-limit.ts",
|
|
33
|
+
`import { createMiddleware } from "@tanstack/react-start";
|
|
34
|
+
// Better Auth's built-in per-process limiter handles server deployments.
|
|
35
|
+
export const authRateLimitMiddleware = createMiddleware().server(({ next }) => next());
|
|
36
|
+
`,
|
|
37
|
+
);
|
|
38
|
+
replace(
|
|
39
|
+
files,
|
|
40
|
+
"src/lib/auth.ts",
|
|
41
|
+
"\t\ttelemetry: { enabled: false },",
|
|
42
|
+
'\t\ttelemetry: { enabled: false },\n\t\trateLimit: { enabled: true, storage: "memory" },',
|
|
43
|
+
);
|
|
44
|
+
let vite = text(files, "vite.config.ts")
|
|
45
|
+
.replace('import { cloudflare } from "@cloudflare/vite-plugin";\n', "")
|
|
46
|
+
.replace('\t\tcloudflare({ viteEnvironment: { name: "ssr" } }),\n', "");
|
|
47
|
+
if (selection.deployment === "nitro") {
|
|
48
|
+
pkg.devDependencies.nitro = "3.0.260903-beta";
|
|
49
|
+
vite = `import { nitro } from "nitro/vite";\n${vite}`.replace(
|
|
50
|
+
"\t\ttanstackStart(),",
|
|
51
|
+
'\t\ttanstackStart(),\n\t\tnitro({ preset: "node-server" }),',
|
|
52
|
+
);
|
|
53
|
+
pkg.scripts.start = "node .output/server/index.mjs";
|
|
54
|
+
pkg.scripts.preview = "node .output/server/index.mjs";
|
|
55
|
+
pkg.scripts.deploy =
|
|
56
|
+
"bun run validate-prod-env && bun run build && bun run check:client-bundle";
|
|
57
|
+
replace(
|
|
58
|
+
files,
|
|
59
|
+
"scripts/check-client-bundle.ts",
|
|
60
|
+
'join(ROOT, "dist", "client")',
|
|
61
|
+
'join(ROOT, ".output", "public")',
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
pkg.devDependencies["@types/bun"] = "^1.3.0";
|
|
65
|
+
files.set("serve.ts", bunServer);
|
|
66
|
+
pkg.scripts.start = "bun run serve.ts";
|
|
67
|
+
pkg.scripts.preview = "bun run serve.ts";
|
|
68
|
+
pkg.scripts.deploy =
|
|
69
|
+
"bun run validate-prod-env && bun run build && bun run check:client-bundle";
|
|
70
|
+
}
|
|
71
|
+
files.set("vite.config.ts", vite);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const bunServer = `import { resolve, sep } from "node:path";
|
|
75
|
+
const entry = "./dist/server/server.js";
|
|
76
|
+
const { default: handler } = await import(entry);
|
|
77
|
+
|
|
78
|
+
const publicDirectory = resolve("dist/client");
|
|
79
|
+
Bun.serve({
|
|
80
|
+
port: Number(process.env.PORT ?? 3000),
|
|
81
|
+
hostname: process.env.HOST ?? "0.0.0.0",
|
|
82
|
+
async fetch(request) {
|
|
83
|
+
const url = new URL(request.url);
|
|
84
|
+
let pathname: string;
|
|
85
|
+
try { pathname = decodeURIComponent(url.pathname); }
|
|
86
|
+
catch { return new Response("Bad request", { status: 400 }); }
|
|
87
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
88
|
+
const path = resolve(publicDirectory, "." + pathname);
|
|
89
|
+
if (path.startsWith(publicDirectory + sep)) {
|
|
90
|
+
const file = Bun.file(path);
|
|
91
|
+
if (await file.exists()) {
|
|
92
|
+
const headers = new Headers({ "Content-Type": file.type });
|
|
93
|
+
if (pathname.startsWith("/assets/")) headers.set("Cache-Control", "public, max-age=31536000, immutable");
|
|
94
|
+
return new Response(request.method === "HEAD" ? null : file, { headers });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return handler.fetch(request);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
`;
|