create-skit 0.1.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/README.md +36 -0
- package/bin/create-skit.mjs +1064 -0
- package/lib/module-application.mjs +281 -0
- package/lib/module-resolver.mjs +179 -0
- package/modules/README.md +22 -0
- package/modules/ai-dx/files/AGENTS.md +116 -0
- package/modules/ai-dx/files/ARCHITECTURE.md +103 -0
- package/modules/ai-dx/module.json +14 -0
- package/modules/ai-dx-claude/files/CLAUDE.md +8 -0
- package/modules/ai-dx-claude/module.json +13 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/auth.mdc +53 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/database.mdc +48 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/env.mdc +43 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/nextjs.mdc +58 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/project.mdc +33 -0
- package/modules/ai-dx-cursor/files/.cursor/rules/testing.mdc +55 -0
- package/modules/ai-dx-cursor/module.json +18 -0
- package/modules/ai-dx-gemini/files/.gemini/GEMINI.md +5 -0
- package/modules/ai-dx-gemini/module.json +13 -0
- package/modules/auth-core/module.json +8 -0
- package/modules/auth-github/module.json +20 -0
- package/modules/billing-polar/module.json +20 -0
- package/modules/billing-stripe/module.json +23 -0
- package/modules/dashboard-shell/files/src/app/globals.css +756 -0
- package/modules/dashboard-shell/files/src/app/settings/page.tsx +67 -0
- package/modules/dashboard-shell/module.json +11 -0
- package/modules/db-pg/module.json +21 -0
- package/modules/db-postgresjs/module.json +21 -0
- package/modules/deploy-docker/files/.dockerignore +19 -0
- package/modules/deploy-docker/files/Dockerfile +25 -0
- package/modules/deploy-docker/module.json +11 -0
- package/modules/email-resend/module.json +21 -0
- package/modules/quality-baseline/module.json +8 -0
- package/modules/testing-baseline/module.json +8 -0
- package/package.json +40 -0
- package/presets/README.md +12 -0
- package/presets/blank.json +67 -0
- package/presets/dashboard.json +67 -0
- package/templates/base-web/.env.example +17 -0
- package/templates/base-web/.github/workflows/ci.yml +34 -0
- package/templates/base-web/.husky/pre-commit +3 -0
- package/templates/base-web/.husky/pre-push +3 -0
- package/templates/base-web/.prettierignore +3 -0
- package/templates/base-web/README.md +42 -0
- package/templates/base-web/drizzle.config.ts +16 -0
- package/templates/base-web/eslint.config.mjs +127 -0
- package/templates/base-web/manifest.json +5 -0
- package/templates/base-web/next-env.d.ts +4 -0
- package/templates/base-web/next.config.ts +5 -0
- package/templates/base-web/package.json +75 -0
- package/templates/base-web/playwright.config.ts +21 -0
- package/templates/base-web/prettier.config.mjs +9 -0
- package/templates/base-web/proxy.ts +23 -0
- package/templates/base-web/src/app/api/auth/[...all]/route.ts +5 -0
- package/templates/base-web/src/app/api/billing/checkout/route.ts +26 -0
- package/templates/base-web/src/app/api/billing/portal/route.ts +25 -0
- package/templates/base-web/src/app/api/email/test/route.ts +28 -0
- package/templates/base-web/src/app/api/webhooks/polar/route.ts +5 -0
- package/templates/base-web/src/app/api/webhooks/stripe/route.ts +5 -0
- package/templates/base-web/src/app/billing/page.tsx +55 -0
- package/templates/base-web/src/app/dashboard/page.tsx +15 -0
- package/templates/base-web/src/app/email/page.tsx +46 -0
- package/templates/base-web/src/app/error.tsx +27 -0
- package/templates/base-web/src/app/globals.css +534 -0
- package/templates/base-web/src/app/layout.tsx +19 -0
- package/templates/base-web/src/app/llms-full.txt/route.ts +158 -0
- package/templates/base-web/src/app/llms.txt/route.ts +59 -0
- package/templates/base-web/src/app/loading.tsx +24 -0
- package/templates/base-web/src/app/not-found.tsx +16 -0
- package/templates/base-web/src/app/page.tsx +5 -0
- package/templates/base-web/src/app/sign-in/page.tsx +14 -0
- package/templates/base-web/src/app/sign-up/page.tsx +14 -0
- package/templates/base-web/src/components/auth/email-auth-form.test.tsx +40 -0
- package/templates/base-web/src/components/auth/email-auth-form.tsx +128 -0
- package/templates/base-web/src/components/auth/sign-out-button.tsx +29 -0
- package/templates/base-web/src/db/index.ts +16 -0
- package/templates/base-web/src/db/schema/auth.ts +4 -0
- package/templates/base-web/src/db/schema/index.ts +2 -0
- package/templates/base-web/src/db/schema/projects.ts +17 -0
- package/templates/base-web/src/db/seeds/index.ts +32 -0
- package/templates/base-web/src/lib/auth-client.ts +5 -0
- package/templates/base-web/src/lib/auth-session.ts +21 -0
- package/templates/base-web/src/lib/auth.ts +23 -0
- package/templates/base-web/src/lib/billing/index.ts +37 -0
- package/templates/base-web/src/lib/billing/providers/polar.ts +80 -0
- package/templates/base-web/src/lib/billing/providers/stripe.ts +77 -0
- package/templates/base-web/src/lib/billing/types.ts +25 -0
- package/templates/base-web/src/lib/email/index.ts +19 -0
- package/templates/base-web/src/lib/email/templates.test.ts +12 -0
- package/templates/base-web/src/lib/email/templates.ts +40 -0
- package/templates/base-web/src/lib/env.ts +83 -0
- package/templates/base-web/tests/e2e/home.spec.ts +8 -0
- package/templates/base-web/tsconfig.json +34 -0
- package/templates/base-web/vitest.config.ts +19 -0
- package/templates/blank/.env.example +16 -0
- package/templates/blank/.github/workflows/ci.yml +34 -0
- package/templates/blank/.husky/pre-commit +3 -0
- package/templates/blank/.husky/pre-push +3 -0
- package/templates/blank/.prettierignore +3 -0
- package/templates/blank/drizzle.config.ts +16 -0
- package/templates/blank/eslint.config.mjs +127 -0
- package/templates/blank/next-env.d.ts +4 -0
- package/templates/blank/next.config.ts +5 -0
- package/templates/blank/package.json +75 -0
- package/templates/blank/playwright.config.ts +21 -0
- package/templates/blank/prettier.config.mjs +9 -0
- package/templates/blank/proxy.ts +28 -0
- package/templates/blank/src/app/api/auth/[...all]/route.ts +5 -0
- package/templates/blank/src/app/api/billing/checkout/route.ts +26 -0
- package/templates/blank/src/app/api/billing/portal/route.ts +25 -0
- package/templates/blank/src/app/api/email/test/route.ts +28 -0
- package/templates/blank/src/app/api/webhooks/polar/route.ts +5 -0
- package/templates/blank/src/app/api/webhooks/stripe/route.ts +5 -0
- package/templates/blank/src/app/billing/page.tsx +70 -0
- package/templates/blank/src/app/email/page.tsx +46 -0
- package/templates/blank/src/app/globals.css +394 -0
- package/templates/blank/src/app/layout.tsx +19 -0
- package/templates/blank/src/app/page.tsx +23 -0
- package/templates/blank/src/app/sign-in/page.tsx +18 -0
- package/templates/blank/src/app/sign-up/page.tsx +18 -0
- package/templates/blank/src/components/auth/email-auth-form.test.tsx +39 -0
- package/templates/blank/src/components/auth/email-auth-form.tsx +109 -0
- package/templates/blank/src/components/auth/sign-out-button.tsx +29 -0
- package/templates/blank/src/db/index.ts +16 -0
- package/templates/blank/src/db/schema/auth.ts +4 -0
- package/templates/blank/src/db/schema/index.ts +2 -0
- package/templates/blank/src/db/schema/projects.ts +17 -0
- package/templates/blank/src/db/seeds/index.ts +28 -0
- package/templates/blank/src/lib/auth-client.ts +5 -0
- package/templates/blank/src/lib/auth-session.ts +11 -0
- package/templates/blank/src/lib/auth.ts +23 -0
- package/templates/blank/src/lib/billing/index.ts +37 -0
- package/templates/blank/src/lib/billing/providers/polar.ts +80 -0
- package/templates/blank/src/lib/billing/providers/stripe.ts +77 -0
- package/templates/blank/src/lib/billing/types.ts +25 -0
- package/templates/blank/src/lib/email/index.ts +19 -0
- package/templates/blank/src/lib/email/templates.test.ts +15 -0
- package/templates/blank/src/lib/email/templates.ts +40 -0
- package/templates/blank/src/lib/env.ts +80 -0
- package/templates/blank/tsconfig.json +34 -0
- package/templates/blank/vitest.config.ts +19 -0
- package/templates/dashboard/.env.example +16 -0
- package/templates/dashboard/.github/workflows/ci.yml +34 -0
- package/templates/dashboard/.husky/pre-commit +3 -0
- package/templates/dashboard/.husky/pre-push +3 -0
- package/templates/dashboard/.prettierignore +3 -0
- package/templates/dashboard/drizzle.config.ts +16 -0
- package/templates/dashboard/eslint.config.mjs +127 -0
- package/templates/dashboard/next-env.d.ts +4 -0
- package/templates/dashboard/next.config.ts +5 -0
- package/templates/dashboard/package.json +75 -0
- package/templates/dashboard/playwright.config.ts +21 -0
- package/templates/dashboard/prettier.config.mjs +9 -0
- package/templates/dashboard/proxy.ts +36 -0
- package/templates/dashboard/src/app/api/auth/[...all]/route.ts +5 -0
- package/templates/dashboard/src/app/api/billing/checkout/route.ts +26 -0
- package/templates/dashboard/src/app/api/billing/portal/route.ts +25 -0
- package/templates/dashboard/src/app/api/email/test/route.ts +28 -0
- package/templates/dashboard/src/app/api/webhooks/polar/route.ts +5 -0
- package/templates/dashboard/src/app/api/webhooks/stripe/route.ts +5 -0
- package/templates/dashboard/src/app/billing/layout.tsx +22 -0
- package/templates/dashboard/src/app/billing/page.tsx +73 -0
- package/templates/dashboard/src/app/dashboard/layout.tsx +22 -0
- package/templates/dashboard/src/app/dashboard/page.tsx +104 -0
- package/templates/dashboard/src/app/email/layout.tsx +22 -0
- package/templates/dashboard/src/app/email/page.tsx +54 -0
- package/templates/dashboard/src/app/globals.css +1357 -0
- package/templates/dashboard/src/app/layout.tsx +25 -0
- package/templates/dashboard/src/app/page.tsx +154 -0
- package/templates/dashboard/src/app/settings/layout.tsx +22 -0
- package/templates/dashboard/src/app/settings/page.tsx +85 -0
- package/templates/dashboard/src/app/sign-in/page.tsx +47 -0
- package/templates/dashboard/src/app/sign-up/page.tsx +47 -0
- package/templates/dashboard/src/components/auth/email-auth-form.test.tsx +39 -0
- package/templates/dashboard/src/components/auth/email-auth-form.tsx +160 -0
- package/templates/dashboard/src/components/auth/sign-out-button.tsx +29 -0
- package/templates/dashboard/src/components/dashboard/shell.tsx +158 -0
- package/templates/dashboard/src/db/index.ts +16 -0
- package/templates/dashboard/src/db/schema/auth.ts +4 -0
- package/templates/dashboard/src/db/schema/index.ts +2 -0
- package/templates/dashboard/src/db/schema/projects.ts +17 -0
- package/templates/dashboard/src/db/seeds/index.ts +28 -0
- package/templates/dashboard/src/lib/auth-client.ts +5 -0
- package/templates/dashboard/src/lib/auth-session.ts +11 -0
- package/templates/dashboard/src/lib/auth.ts +41 -0
- package/templates/dashboard/src/lib/billing/index.ts +37 -0
- package/templates/dashboard/src/lib/billing/providers/polar.ts +80 -0
- package/templates/dashboard/src/lib/billing/providers/stripe.ts +77 -0
- package/templates/dashboard/src/lib/billing/types.ts +25 -0
- package/templates/dashboard/src/lib/email/index.ts +19 -0
- package/templates/dashboard/src/lib/email/templates.test.ts +15 -0
- package/templates/dashboard/src/lib/email/templates.ts +40 -0
- package/templates/dashboard/src/lib/env.ts +88 -0
- package/templates/dashboard/tsconfig.json +34 -0
- package/templates/dashboard/vitest.config.ts +19 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type BillingProviderName = "stripe" | "polar";
|
|
2
|
+
|
|
3
|
+
export type CheckoutSessionInput = {
|
|
4
|
+
customerEmail: string;
|
|
5
|
+
successUrl: string;
|
|
6
|
+
cancelUrl: string;
|
|
7
|
+
userId: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type PortalSessionInput = {
|
|
11
|
+
customerEmail: string;
|
|
12
|
+
returnUrl: string;
|
|
13
|
+
userId: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type WebhookInput = {
|
|
17
|
+
headers: Headers;
|
|
18
|
+
rawBody: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface BillingProvider {
|
|
22
|
+
createCheckoutSession(input: CheckoutSessionInput): Promise<{ url: string }>;
|
|
23
|
+
createCustomerPortalSession(input: PortalSessionInput): Promise<{ url: string }>;
|
|
24
|
+
handleWebhook(input: WebhookInput): Promise<void>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
|
|
3
|
+
__EMAIL_PROVIDER_IMPORTS__
|
|
4
|
+
|
|
5
|
+
__EMAIL_ENV_IMPORT__
|
|
6
|
+
|
|
7
|
+
import type { EmailTemplateResult } from "./templates";
|
|
8
|
+
|
|
9
|
+
__EMAIL_PROVIDER_SETUP__
|
|
10
|
+
|
|
11
|
+
__EMAIL_FROM_ADDRESS_HELPER__
|
|
12
|
+
|
|
13
|
+
export async function sendEmail(__EMAIL_SEND_SIGNATURE__: {
|
|
14
|
+
to: string | string[];
|
|
15
|
+
template: EmailTemplateResult;
|
|
16
|
+
}) {
|
|
17
|
+
void __EMAIL_SEND_SIGNATURE__;
|
|
18
|
+
__EMAIL_SEND_IMPLEMENTATION__
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { __EMAIL_TEMPLATE_TEST_IMPORT__ } from "@/lib/email/templates";
|
|
4
|
+
|
|
5
|
+
describe("__EMAIL_TEMPLATE_TEST_SUITE__", () => {
|
|
6
|
+
it("__EMAIL_TEMPLATE_TEST_CASE__", () => {
|
|
7
|
+
const template = __EMAIL_TEMPLATE_TEST_FACTORY__;
|
|
8
|
+
|
|
9
|
+
expect(template.subject).toContain("__EMAIL_TEMPLATE_EXPECT_SUBJECT__");
|
|
10
|
+
expect(template.text).toContain("__EMAIL_TEMPLATE_EXPECT_TEXT__");
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type EmailTemplateResult = {
|
|
2
|
+
subject: string;
|
|
3
|
+
text: string;
|
|
4
|
+
html: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function createWelcomeEmailTemplate(input: {
|
|
8
|
+
appName: string;
|
|
9
|
+
recipientName?: string | null;
|
|
10
|
+
}): EmailTemplateResult {
|
|
11
|
+
const greeting = input.recipientName ? `Hi ${input.recipientName},` : "Hi there,";
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
subject: `Welcome to ${input.appName}`,
|
|
15
|
+
text: `${greeting}\n\nYour starter app is ready. Auth, billing, and the core project structure are already in place.\n\nNext step: sign in and continue building.\n`,
|
|
16
|
+
html: `<p>${greeting}</p><p>Your starter app is ready. Auth, billing, and the core project structure are already in place.</p><p>Next step: sign in and continue building.</p>`
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createPasswordResetPlaceholderTemplate(input: {
|
|
21
|
+
appName: string;
|
|
22
|
+
resetUrl: string;
|
|
23
|
+
}): EmailTemplateResult {
|
|
24
|
+
return {
|
|
25
|
+
subject: `Reset your ${input.appName} password`,
|
|
26
|
+
text: `Use this link to reset your password: ${input.resetUrl}`,
|
|
27
|
+
html: `<p>Use this link to reset your password:</p><p><a href="${input.resetUrl}">${input.resetUrl}</a></p>`
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createBillingUpdatePlaceholderTemplate(input: {
|
|
32
|
+
appName: string;
|
|
33
|
+
provider: string;
|
|
34
|
+
}): EmailTemplateResult {
|
|
35
|
+
return {
|
|
36
|
+
subject: `${input.appName} billing update`,
|
|
37
|
+
text: `A billing event was received from ${input.provider}. Replace this placeholder with your real billing notification workflow.`,
|
|
38
|
+
html: `<p>A billing event was received from <strong>${input.provider}</strong>.</p><p>Replace this placeholder with your real billing notification workflow.</p>`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
const envSchema = z.object({
|
|
6
|
+
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
|
7
|
+
NEXT_PUBLIC_APP_URL: z.string().url(),
|
|
8
|
+
BETTER_AUTH_URL: z.string().url(),
|
|
9
|
+
DATABASE_URL: z.string().min(1),
|
|
10
|
+
BETTER_AUTH_SECRET: z.string().min(1),
|
|
11
|
+
__AUTH_PROVIDER_ENV_SCHEMA__
|
|
12
|
+
BILLING_PROVIDER: z.enum(["stripe", "polar", "both", "none"]).default("stripe"),
|
|
13
|
+
EMAIL_PROVIDER: z.enum(["resend", "none"]).default("resend"),
|
|
14
|
+
RESEND_API_KEY: z.string().min(1).optional(),
|
|
15
|
+
EMAIL_FROM: z.string().min(1).optional(),
|
|
16
|
+
STRIPE_PRICE_ID: z.string().min(1).optional(),
|
|
17
|
+
STRIPE_SECRET_KEY: z.string().min(1).optional(),
|
|
18
|
+
STRIPE_WEBHOOK_SECRET: z.string().min(1).optional(),
|
|
19
|
+
POLAR_ACCESS_TOKEN: z.string().min(1).optional(),
|
|
20
|
+
POLAR_ORGANIZATION_ID: z.string().min(1).optional(),
|
|
21
|
+
POLAR_PRODUCT_ID: z.string().min(1).optional(),
|
|
22
|
+
POLAR_SERVER: z.enum(["production", "sandbox"]).default("sandbox"),
|
|
23
|
+
POLAR_WEBHOOK_SECRET: z.string().min(1).optional()
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
type Env = z.infer<typeof envSchema>;
|
|
27
|
+
|
|
28
|
+
let cachedEnv: Env | null = null;
|
|
29
|
+
const isBuildRuntime =
|
|
30
|
+
process.env.npm_lifecycle_event === "build" ||
|
|
31
|
+
process.env.NEXT_PHASE === "phase-production-build";
|
|
32
|
+
const isProductionRuntime = process.env.NODE_ENV === "production" && !isBuildRuntime;
|
|
33
|
+
|
|
34
|
+
function readRequiredValue(name: string, fallback: string) {
|
|
35
|
+
const value = process.env[name];
|
|
36
|
+
|
|
37
|
+
if (value) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!isProductionRuntime) {
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseEnv(): Env {
|
|
49
|
+
return envSchema.parse({
|
|
50
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
51
|
+
NEXT_PUBLIC_APP_URL: readRequiredValue("NEXT_PUBLIC_APP_URL", "http://localhost:3000"),
|
|
52
|
+
BETTER_AUTH_URL: readRequiredValue("BETTER_AUTH_URL", "http://localhost:3000"),
|
|
53
|
+
DATABASE_URL: readRequiredValue(
|
|
54
|
+
"DATABASE_URL",
|
|
55
|
+
"postgresql://postgres:postgres@localhost:5432/skit"
|
|
56
|
+
),
|
|
57
|
+
BETTER_AUTH_SECRET: readRequiredValue("BETTER_AUTH_SECRET", "build-placeholder-secret"),
|
|
58
|
+
__AUTH_PROVIDER_ENV_VALUES__
|
|
59
|
+
BILLING_PROVIDER: process.env.BILLING_PROVIDER,
|
|
60
|
+
EMAIL_PROVIDER: process.env.EMAIL_PROVIDER,
|
|
61
|
+
RESEND_API_KEY: process.env.RESEND_API_KEY,
|
|
62
|
+
EMAIL_FROM: process.env.EMAIL_FROM,
|
|
63
|
+
STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID,
|
|
64
|
+
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
|
|
65
|
+
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
|
|
66
|
+
POLAR_ACCESS_TOKEN: process.env.POLAR_ACCESS_TOKEN,
|
|
67
|
+
POLAR_ORGANIZATION_ID: process.env.POLAR_ORGANIZATION_ID,
|
|
68
|
+
POLAR_PRODUCT_ID: process.env.POLAR_PRODUCT_ID,
|
|
69
|
+
POLAR_SERVER: process.env.POLAR_SERVER,
|
|
70
|
+
POLAR_WEBHOOK_SECRET: process.env.POLAR_WEBHOOK_SECRET
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getEnv(): Env {
|
|
75
|
+
cachedEnv ??= parseEnv();
|
|
76
|
+
return cachedEnv;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const env = new Proxy({} as Env, {
|
|
80
|
+
get(_target, property) {
|
|
81
|
+
return getEnv()[property as keyof Env];
|
|
82
|
+
}
|
|
83
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test("__HOME_E2E_TEST_NAME__", async ({ page }) => {
|
|
4
|
+
await page.goto("/");
|
|
5
|
+
|
|
6
|
+
await expect(page.getByRole("heading", { name: /__HOME_E2E_HEADING__/i })).toBeVisible();
|
|
7
|
+
await expect(page.getByRole("link", { name: /__HOME_E2E_LINK__/i })).toBeVisible();
|
|
8
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "es2022"],
|
|
5
|
+
"allowJs": false,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"exactOptionalPropertyTypes": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"module": "esnext",
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"baseUrl": ".",
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
"incremental": true,
|
|
19
|
+
"verbatimModuleSyntax": true,
|
|
20
|
+
"moduleDetection": "force",
|
|
21
|
+
"plugins": [{ "name": "next" }],
|
|
22
|
+
"paths": {
|
|
23
|
+
"@/*": ["./src/*"]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"include": [
|
|
27
|
+
"next-env.d.ts",
|
|
28
|
+
"**/*.ts",
|
|
29
|
+
"**/*.tsx",
|
|
30
|
+
".next/types/**/*.ts",
|
|
31
|
+
".next/dev/types/**/*.ts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": ["node_modules"]
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
import { defineConfig } from "vitest/config";
|
|
5
|
+
|
|
6
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
resolve: {
|
|
10
|
+
alias: {
|
|
11
|
+
"@": path.resolve(dirname, "./src")
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
test: {
|
|
15
|
+
environment: "jsdom",
|
|
16
|
+
globals: true,
|
|
17
|
+
include: ["src/**/*.test.{ts,tsx}"]
|
|
18
|
+
}
|
|
19
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
2
|
+
BETTER_AUTH_URL=http://localhost:3000
|
|
3
|
+
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/__PROJECT_NAME__
|
|
4
|
+
BETTER_AUTH_SECRET=replace-me
|
|
5
|
+
RESEND_API_KEY=re_xxx
|
|
6
|
+
EMAIL_FROM=Panda Starter <onboarding@example.com>
|
|
7
|
+
STRIPE_SECRET_KEY=sk_test_xxx
|
|
8
|
+
STRIPE_PRICE_ID=price_xxx
|
|
9
|
+
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
|
10
|
+
POLAR_ACCESS_TOKEN=polar_pat_xxx
|
|
11
|
+
POLAR_ORGANIZATION_ID=org_xxx
|
|
12
|
+
POLAR_PRODUCT_ID=prod_xxx
|
|
13
|
+
POLAR_SERVER=sandbox
|
|
14
|
+
POLAR_WEBHOOK_SECRET=polar_whsec_xxx
|
|
15
|
+
BILLING_PROVIDER=__BILLING_PROVIDER__
|
|
16
|
+
EMAIL_PROVIDER=__EMAIL_PROVIDER__
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
quality:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Setup Node
|
|
16
|
+
uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: 22
|
|
19
|
+
cache: __CI_CACHE__
|
|
20
|
+
|
|
21
|
+
- name: Install
|
|
22
|
+
run: __CI_INSTALL_COMMAND__
|
|
23
|
+
|
|
24
|
+
- name: Lint
|
|
25
|
+
run: __CI_RUN_LINT__
|
|
26
|
+
|
|
27
|
+
- name: Typecheck
|
|
28
|
+
run: __CI_RUN_TYPECHECK__
|
|
29
|
+
|
|
30
|
+
- name: Unit tests
|
|
31
|
+
run: __CI_RUN_TEST__
|
|
32
|
+
|
|
33
|
+
- name: Build
|
|
34
|
+
run: __CI_RUN_BUILD__
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from "drizzle-kit";
|
|
2
|
+
|
|
3
|
+
if (!process.env.DATABASE_URL) {
|
|
4
|
+
throw new Error("DATABASE_URL is required to use Drizzle.");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
schema: "./src/db/schema/index.ts",
|
|
9
|
+
out: "./src/db/migrations",
|
|
10
|
+
dialect: "postgresql",
|
|
11
|
+
dbCredentials: {
|
|
12
|
+
url: process.env.DATABASE_URL
|
|
13
|
+
},
|
|
14
|
+
verbose: true,
|
|
15
|
+
strict: true
|
|
16
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
5
|
+
import prettier from "eslint-config-prettier/flat";
|
|
6
|
+
import nextTs from "eslint-config-next/typescript";
|
|
7
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
8
|
+
import importPlugin from "eslint-plugin-import";
|
|
9
|
+
import unusedImports from "eslint-plugin-unused-imports";
|
|
10
|
+
import globals from "globals";
|
|
11
|
+
|
|
12
|
+
const SOURCE_FILES = ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"];
|
|
13
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
export default defineConfig([
|
|
16
|
+
...nextVitals,
|
|
17
|
+
...nextTs,
|
|
18
|
+
{
|
|
19
|
+
files: SOURCE_FILES,
|
|
20
|
+
languageOptions: {
|
|
21
|
+
parserOptions: {
|
|
22
|
+
projectService: true,
|
|
23
|
+
tsconfigRootDir: dirname
|
|
24
|
+
},
|
|
25
|
+
globals: {
|
|
26
|
+
...globals.browser,
|
|
27
|
+
...globals.nodeBuiltin
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
plugins: {
|
|
31
|
+
import: importPlugin,
|
|
32
|
+
"unused-imports": unusedImports
|
|
33
|
+
},
|
|
34
|
+
settings: {
|
|
35
|
+
"import/resolver": {
|
|
36
|
+
typescript: true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
rules: {
|
|
40
|
+
"@typescript-eslint/consistent-type-imports": [
|
|
41
|
+
"error",
|
|
42
|
+
{
|
|
43
|
+
prefer: "type-imports",
|
|
44
|
+
fixStyle: "inline-type-imports"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"@typescript-eslint/no-floating-promises": "error",
|
|
48
|
+
"@typescript-eslint/no-misused-promises": [
|
|
49
|
+
"error",
|
|
50
|
+
{
|
|
51
|
+
checksVoidReturn: {
|
|
52
|
+
attributes: false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"@typescript-eslint/no-unused-vars": "off",
|
|
57
|
+
"import/first": "error",
|
|
58
|
+
"import/newline-after-import": "error",
|
|
59
|
+
"import/no-duplicates": "error",
|
|
60
|
+
"import/order": [
|
|
61
|
+
"error",
|
|
62
|
+
{
|
|
63
|
+
groups: [
|
|
64
|
+
"builtin",
|
|
65
|
+
"external",
|
|
66
|
+
"internal",
|
|
67
|
+
["parent", "sibling", "index"],
|
|
68
|
+
"object",
|
|
69
|
+
"type"
|
|
70
|
+
],
|
|
71
|
+
alphabetize: {
|
|
72
|
+
order: "asc",
|
|
73
|
+
caseInsensitive: true
|
|
74
|
+
},
|
|
75
|
+
"newlines-between": "always",
|
|
76
|
+
pathGroups: [
|
|
77
|
+
{
|
|
78
|
+
pattern: "@/**",
|
|
79
|
+
group: "internal",
|
|
80
|
+
position: "before"
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
pathGroupsExcludedImportTypes: ["builtin"]
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"unused-imports/no-unused-imports": "error",
|
|
87
|
+
"unused-imports/no-unused-vars": [
|
|
88
|
+
"warn",
|
|
89
|
+
{
|
|
90
|
+
vars: "all",
|
|
91
|
+
varsIgnorePattern: "^_",
|
|
92
|
+
args: "after-used",
|
|
93
|
+
argsIgnorePattern: "^_"
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
files: SOURCE_FILES,
|
|
100
|
+
ignores: ["drizzle.config.ts", "src/lib/env.ts"],
|
|
101
|
+
rules: {
|
|
102
|
+
"no-restricted-properties": [
|
|
103
|
+
"error",
|
|
104
|
+
{
|
|
105
|
+
object: "process",
|
|
106
|
+
property: "env",
|
|
107
|
+
message: "Use the validated env object from src/lib/env.ts."
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
prettier,
|
|
113
|
+
globalIgnores([
|
|
114
|
+
".next/**",
|
|
115
|
+
"out/**",
|
|
116
|
+
"build/**",
|
|
117
|
+
"coverage/**",
|
|
118
|
+
"drizzle.config.ts",
|
|
119
|
+
"eslint.config.mjs",
|
|
120
|
+
"next.config.ts",
|
|
121
|
+
"playwright.config.ts",
|
|
122
|
+
"prettier.config.mjs",
|
|
123
|
+
"proxy.ts",
|
|
124
|
+
"next-env.d.ts",
|
|
125
|
+
"vitest.config.ts"
|
|
126
|
+
])
|
|
127
|
+
]);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"packageManager": "__PACKAGE_MANAGER__",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "next dev",
|
|
9
|
+
"build": "next build",
|
|
10
|
+
"start": "next start",
|
|
11
|
+
"lint": "eslint . --max-warnings=0",
|
|
12
|
+
"lint:fix": "eslint . --fix",
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"format": "prettier . --write",
|
|
15
|
+
"format:check": "prettier . --check",
|
|
16
|
+
"check": "__CHECK_COMMAND__",
|
|
17
|
+
"prepare": "husky",
|
|
18
|
+
"lint-staged": "lint-staged",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"test:e2e": "playwright test",
|
|
22
|
+
"test:e2e:headed": "playwright test --headed",
|
|
23
|
+
"db:generate": "drizzle-kit generate",
|
|
24
|
+
"db:migrate": "drizzle-kit migrate",
|
|
25
|
+
"db:push": "drizzle-kit push",
|
|
26
|
+
"db:studio": "drizzle-kit studio",
|
|
27
|
+
"db:seed": "tsx src/db/seeds/index.ts",
|
|
28
|
+
"auth:generate": "npx @better-auth/cli@latest generate --config src/lib/auth.ts --output src/db/schema/auth.ts",
|
|
29
|
+
"auth:secret": "npx @better-auth/cli@latest secret"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"better-auth": "1.3.8",
|
|
33
|
+
"drizzle-orm": "0.44.x",
|
|
34
|
+
"next": "16.2.x",
|
|
35
|
+
__DATABASE_DEPENDENCIES__
|
|
36
|
+
"react": "19.2.x",
|
|
37
|
+
"react-dom": "19.2.x",
|
|
38
|
+
__PROVIDER_PACKAGE_DEPENDENCIES__
|
|
39
|
+
"server-only": "^0.0.1",
|
|
40
|
+
"zod": "4.1.5"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@better-auth/cli": "1.3.8",
|
|
44
|
+
"@playwright/test": "1.55.0",
|
|
45
|
+
"@testing-library/react": "16.3.0",
|
|
46
|
+
"@types/node": "^24.0.0",
|
|
47
|
+
__DATABASE_DEV_DEPENDENCIES__
|
|
48
|
+
"@types/react": "^19.0.0",
|
|
49
|
+
"@types/react-dom": "^19.0.0",
|
|
50
|
+
"drizzle-kit": "0.31.x",
|
|
51
|
+
"eslint": "9.35.0",
|
|
52
|
+
"eslint-config-next": "16.2.x",
|
|
53
|
+
"eslint-config-prettier": "10.1.8",
|
|
54
|
+
"eslint-import-resolver-typescript": "^3.10.1",
|
|
55
|
+
"eslint-plugin-import": "2.32.0",
|
|
56
|
+
"eslint-plugin-unused-imports": "4.2.0",
|
|
57
|
+
"globals": "16.3.0",
|
|
58
|
+
"husky": "^9.1.7",
|
|
59
|
+
"jsdom": "26.1.0",
|
|
60
|
+
"lint-staged": "^16.1.6",
|
|
61
|
+
"playwright": "1.55.0",
|
|
62
|
+
"prettier": "3.6.2",
|
|
63
|
+
"tsx": "4.20.x",
|
|
64
|
+
"typescript": "5.9.x",
|
|
65
|
+
"vitest": "3.2.4"
|
|
66
|
+
},
|
|
67
|
+
"lint-staged": {
|
|
68
|
+
"*.{js,jsx,ts,tsx,mjs,cjs}": [
|
|
69
|
+
"eslint --fix"
|
|
70
|
+
],
|
|
71
|
+
"*.{json,md,css,yml,yaml,html}": [
|
|
72
|
+
"prettier --write"
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
testDir: "./tests/e2e",
|
|
5
|
+
fullyParallel: true,
|
|
6
|
+
use: {
|
|
7
|
+
baseURL: "http://127.0.0.1:3000",
|
|
8
|
+
trace: "on-first-retry"
|
|
9
|
+
},
|
|
10
|
+
webServer: {
|
|
11
|
+
command: "npm run dev",
|
|
12
|
+
url: "http://127.0.0.1:3000",
|
|
13
|
+
reuseExistingServer: !process.env.CI
|
|
14
|
+
},
|
|
15
|
+
projects: [
|
|
16
|
+
{
|
|
17
|
+
name: "chromium",
|
|
18
|
+
use: { ...devices["Desktop Chrome"] }
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import type { NextRequest } from "next/server";
|
|
3
|
+
|
|
4
|
+
import { getSessionCookie } from "better-auth/cookies";
|
|
5
|
+
|
|
6
|
+
export function proxy(request: NextRequest) {
|
|
7
|
+
const sessionCookie = getSessionCookie(request);
|
|
8
|
+
const { pathname } = request.nextUrl;
|
|
9
|
+
|
|
10
|
+
if (
|
|
11
|
+
!sessionCookie &&
|
|
12
|
+
(pathname.startsWith("/dashboard") ||
|
|
13
|
+
pathname.startsWith("/billing") ||
|
|
14
|
+
pathname.startsWith("/email"))
|
|
15
|
+
) {
|
|
16
|
+
return NextResponse.redirect(new URL("/sign-in", request.url));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (sessionCookie && (pathname === "/sign-in" || pathname === "/sign-up")) {
|
|
20
|
+
return NextResponse.redirect(new URL("/dashboard", request.url));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return NextResponse.next();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const config = {
|
|
27
|
+
matcher: ["/dashboard/:path*", "/billing/:path*", "/email/:path*", "/sign-in", "/sign-up"]
|
|
28
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { redirect } from "next/navigation";
|
|
2
|
+
|
|
3
|
+
import { getSession } from "@/lib/auth-session";
|
|
4
|
+
import { getBillingProvider, getBillingProviderName } from "@/lib/billing";
|
|
5
|
+
import { env } from "@/lib/env";
|
|
6
|
+
|
|
7
|
+
export async function POST(request: Request) {
|
|
8
|
+
const session = await getSession();
|
|
9
|
+
|
|
10
|
+
if (!session) {
|
|
11
|
+
redirect("/sign-in");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const formData = await request.formData();
|
|
15
|
+
const selected = String(formData.get("provider") ?? "");
|
|
16
|
+
const providerName = getBillingProviderName(selected);
|
|
17
|
+
const provider = getBillingProvider(providerName);
|
|
18
|
+
const checkout = await provider.createCheckoutSession({
|
|
19
|
+
userId: session.user.id,
|
|
20
|
+
customerEmail: session.user.email,
|
|
21
|
+
successUrl: `${env.NEXT_PUBLIC_APP_URL}/billing?success=1&provider=${providerName}`,
|
|
22
|
+
cancelUrl: `${env.NEXT_PUBLIC_APP_URL}/billing?canceled=1&provider=${providerName}`
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
redirect(checkout.url);
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { redirect } from "next/navigation";
|
|
2
|
+
|
|
3
|
+
import { getSession } from "@/lib/auth-session";
|
|
4
|
+
import { getBillingProvider, getBillingProviderName } from "@/lib/billing";
|
|
5
|
+
import { env } from "@/lib/env";
|
|
6
|
+
|
|
7
|
+
export async function POST(request: Request) {
|
|
8
|
+
const session = await getSession();
|
|
9
|
+
|
|
10
|
+
if (!session) {
|
|
11
|
+
redirect("/sign-in");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const formData = await request.formData();
|
|
15
|
+
const selected = String(formData.get("provider") ?? "");
|
|
16
|
+
const providerName = getBillingProviderName(selected);
|
|
17
|
+
const provider = getBillingProvider(providerName);
|
|
18
|
+
const portal = await provider.createCustomerPortalSession({
|
|
19
|
+
userId: session.user.id,
|
|
20
|
+
customerEmail: session.user.email,
|
|
21
|
+
returnUrl: `${env.NEXT_PUBLIC_APP_URL}/billing?provider=${providerName}`
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
redirect(portal.url);
|
|
25
|
+
}
|