gorsee 0.1.3 → 0.2.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 +11 -2
- package/src/cli/cmd-deploy.ts +141 -0
- package/src/cli/cmd-docs.ts +145 -0
- package/src/cli/cmd-test.ts +121 -0
- package/src/cli/cmd-upgrade.ts +135 -0
- package/src/cli/index.ts +23 -0
- package/src/deploy/cloudflare.ts +109 -0
- package/src/deploy/fly.ts +79 -0
- package/src/deploy/index.ts +31 -0
- package/src/deploy/netlify.ts +77 -0
- package/src/deploy/vercel.ts +94 -0
- package/src/index.ts +1 -0
- package/src/plugins/drizzle.ts +84 -0
- package/src/plugins/index.ts +86 -0
- package/src/plugins/lucia.ts +111 -0
- package/src/plugins/prisma.ts +85 -0
- package/src/plugins/resend.ts +78 -0
- package/src/plugins/s3.ts +102 -0
- package/src/plugins/stripe.ts +133 -0
- package/src/plugins/tailwind.ts +92 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Prisma integration plugin -- zero external dependencies
|
|
2
|
+
|
|
3
|
+
import type { MiddlewareFn, Context } from "../server/middleware.ts"
|
|
4
|
+
import type { GorseePlugin } from "./index.ts"
|
|
5
|
+
import { definePlugin } from "./index.ts"
|
|
6
|
+
|
|
7
|
+
export interface PrismaPluginConfig {
|
|
8
|
+
schemaPath?: string
|
|
9
|
+
datasourceUrl?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let prismaClient: unknown = null
|
|
13
|
+
|
|
14
|
+
/** Returns the current Prisma client (available after setup) */
|
|
15
|
+
export function getPrisma<T = unknown>(): T {
|
|
16
|
+
if (!prismaClient) {
|
|
17
|
+
throw new Error("Prisma not initialized. Did you register prismaPlugin?")
|
|
18
|
+
}
|
|
19
|
+
return prismaClient as T
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Middleware that attaches prisma to ctx.locals.prisma */
|
|
23
|
+
export function prismaMiddleware(client: unknown): MiddlewareFn {
|
|
24
|
+
return async (ctx: Context, next) => {
|
|
25
|
+
ctx.locals.prisma = client
|
|
26
|
+
return next()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Generates a basic schema.prisma content string */
|
|
31
|
+
export function generatePrismaSchema(config: PrismaPluginConfig): string {
|
|
32
|
+
const url = config.datasourceUrl ?? "file:./dev.db"
|
|
33
|
+
const provider = url.startsWith("file:") ? "sqlite"
|
|
34
|
+
: url.startsWith("postgres") ? "postgresql"
|
|
35
|
+
: "mysql"
|
|
36
|
+
|
|
37
|
+
return `generator client {
|
|
38
|
+
provider = "prisma-client-js"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
datasource db {
|
|
42
|
+
provider = "${provider}"
|
|
43
|
+
url = "${url}"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Add your models below
|
|
47
|
+
// model User {
|
|
48
|
+
// id Int @id @default(autoincrement())
|
|
49
|
+
// email String @unique
|
|
50
|
+
// name String?
|
|
51
|
+
// }
|
|
52
|
+
`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Creates a Prisma integration plugin */
|
|
56
|
+
export function prismaPlugin(config: PrismaPluginConfig = {}): GorseePlugin {
|
|
57
|
+
return definePlugin({
|
|
58
|
+
name: "gorsee-prisma",
|
|
59
|
+
|
|
60
|
+
async setup(app) {
|
|
61
|
+
try {
|
|
62
|
+
// Dynamic import -- PrismaClient must be generated by user
|
|
63
|
+
const { PrismaClient } = await import("@prisma/client" as string)
|
|
64
|
+
prismaClient = new PrismaClient({
|
|
65
|
+
datasourceUrl: config.datasourceUrl,
|
|
66
|
+
})
|
|
67
|
+
} catch {
|
|
68
|
+
// If @prisma/client not available, create a placeholder
|
|
69
|
+
prismaClient = {
|
|
70
|
+
_placeholder: true,
|
|
71
|
+
_message: "Run `bunx prisma generate` to create the client",
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
app.addMiddleware(prismaMiddleware(prismaClient))
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async teardown() {
|
|
79
|
+
if (prismaClient && typeof (prismaClient as any).$disconnect === "function") {
|
|
80
|
+
await (prismaClient as any).$disconnect()
|
|
81
|
+
}
|
|
82
|
+
prismaClient = null
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Resend email plugin -- uses native fetch, no SDK dependency
|
|
2
|
+
|
|
3
|
+
import type { GorseePlugin } from "./index.ts"
|
|
4
|
+
import { definePlugin } from "./index.ts"
|
|
5
|
+
|
|
6
|
+
export interface ResendPluginConfig {
|
|
7
|
+
apiKey: string
|
|
8
|
+
from?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SendEmailOptions {
|
|
12
|
+
to: string | string[]
|
|
13
|
+
subject: string
|
|
14
|
+
html?: string
|
|
15
|
+
text?: string
|
|
16
|
+
from?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Mailer {
|
|
20
|
+
send(options: SendEmailOptions): Promise<{ id: string }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const RESEND_API = "https://api.resend.com"
|
|
24
|
+
|
|
25
|
+
let mailerInstance: Mailer | null = null
|
|
26
|
+
|
|
27
|
+
/** Returns the mailer instance (available after setup) */
|
|
28
|
+
export function getMailer(): Mailer {
|
|
29
|
+
if (!mailerInstance) {
|
|
30
|
+
throw new Error("Resend not initialized. Did you register resendPlugin?")
|
|
31
|
+
}
|
|
32
|
+
return mailerInstance
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createMailer(config: ResendPluginConfig): Mailer {
|
|
36
|
+
return {
|
|
37
|
+
async send(options: SendEmailOptions): Promise<{ id: string }> {
|
|
38
|
+
const body = {
|
|
39
|
+
from: options.from ?? config.from ?? "onboarding@resend.dev",
|
|
40
|
+
to: Array.isArray(options.to) ? options.to : [options.to],
|
|
41
|
+
subject: options.subject,
|
|
42
|
+
...(options.html ? { html: options.html } : {}),
|
|
43
|
+
...(options.text ? { text: options.text } : {}),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const res = await fetch(`${RESEND_API}/emails`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const err = await res.text()
|
|
57
|
+
throw new Error(`Resend API error (${res.status}): ${err}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (await res.json()) as { id: string }
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Creates a Resend email plugin */
|
|
66
|
+
export function resendPlugin(config: ResendPluginConfig): GorseePlugin {
|
|
67
|
+
return definePlugin({
|
|
68
|
+
name: "gorsee-resend",
|
|
69
|
+
|
|
70
|
+
async setup() {
|
|
71
|
+
mailerInstance = createMailer(config)
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async teardown() {
|
|
75
|
+
mailerInstance = null
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// S3-compatible object storage plugin -- uses native fetch, no AWS SDK
|
|
2
|
+
|
|
3
|
+
import type { GorseePlugin } from "./index.ts"
|
|
4
|
+
import { definePlugin } from "./index.ts"
|
|
5
|
+
|
|
6
|
+
export interface S3PluginConfig {
|
|
7
|
+
bucket: string
|
|
8
|
+
region?: string
|
|
9
|
+
endpoint?: string
|
|
10
|
+
accessKeyId?: string
|
|
11
|
+
secretAccessKey?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface StorageClient {
|
|
15
|
+
upload(key: string, body: ArrayBuffer | ReadableStream | string, contentType?: string): Promise<string>
|
|
16
|
+
download(key: string): Promise<Response>
|
|
17
|
+
delete(key: string): Promise<void>
|
|
18
|
+
list(prefix?: string): Promise<string[]>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let storageClient: StorageClient | null = null
|
|
22
|
+
|
|
23
|
+
/** Returns the storage client (available after setup) */
|
|
24
|
+
export function getStorage(): StorageClient {
|
|
25
|
+
if (!storageClient) {
|
|
26
|
+
throw new Error("S3 not initialized. Did you register s3Plugin?")
|
|
27
|
+
}
|
|
28
|
+
return storageClient
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildEndpoint(config: S3PluginConfig): string {
|
|
32
|
+
if (config.endpoint) return config.endpoint.replace(/\/$/, "")
|
|
33
|
+
const region = config.region ?? "us-east-1"
|
|
34
|
+
return `https://${config.bucket}.s3.${region}.amazonaws.com`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createStorageClient(config: S3PluginConfig): StorageClient {
|
|
38
|
+
const baseUrl = buildEndpoint(config)
|
|
39
|
+
const headers: Record<string, string> = {}
|
|
40
|
+
|
|
41
|
+
// Basic auth headers (simplified -- production should use AWS Signature V4)
|
|
42
|
+
if (config.accessKeyId) {
|
|
43
|
+
headers["x-amz-access-key"] = config.accessKeyId
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
async upload(key: string, body: ArrayBuffer | ReadableStream | string, contentType?: string) {
|
|
48
|
+
const url = `${baseUrl}/${encodeURIComponent(key)}`
|
|
49
|
+
const ct = contentType ?? "application/octet-stream"
|
|
50
|
+
const fetchBody: BodyInit = body instanceof ArrayBuffer
|
|
51
|
+
? new Blob([body], { type: ct })
|
|
52
|
+
: body
|
|
53
|
+
const res = await fetch(url, {
|
|
54
|
+
method: "PUT",
|
|
55
|
+
headers: { ...headers, "Content-Type": ct },
|
|
56
|
+
body: fetchBody,
|
|
57
|
+
})
|
|
58
|
+
if (!res.ok) throw new Error(`S3 upload failed: ${res.status} ${res.statusText}`)
|
|
59
|
+
return url
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
async download(key) {
|
|
63
|
+
const url = `${baseUrl}/${encodeURIComponent(key)}`
|
|
64
|
+
const res = await fetch(url, { headers })
|
|
65
|
+
if (!res.ok) throw new Error(`S3 download failed: ${res.status} ${res.statusText}`)
|
|
66
|
+
return res
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async delete(key) {
|
|
70
|
+
const url = `${baseUrl}/${encodeURIComponent(key)}`
|
|
71
|
+
const res = await fetch(url, { method: "DELETE", headers })
|
|
72
|
+
if (!res.ok) throw new Error(`S3 delete failed: ${res.status} ${res.statusText}`)
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async list(prefix) {
|
|
76
|
+
const params = prefix ? `?list-type=2&prefix=${encodeURIComponent(prefix)}` : "?list-type=2"
|
|
77
|
+
const res = await fetch(`${baseUrl}${params}`, { headers })
|
|
78
|
+
if (!res.ok) throw new Error(`S3 list failed: ${res.status} ${res.statusText}`)
|
|
79
|
+
const xml = await res.text()
|
|
80
|
+
const keys: string[] = []
|
|
81
|
+
const regex = /<Key>([^<]+)<\/Key>/g
|
|
82
|
+
let match: RegExpExecArray | null
|
|
83
|
+
while ((match = regex.exec(xml)) !== null) keys.push(match[1]!)
|
|
84
|
+
return keys
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Creates an S3-compatible storage plugin */
|
|
90
|
+
export function s3Plugin(config: S3PluginConfig): GorseePlugin {
|
|
91
|
+
return definePlugin({
|
|
92
|
+
name: "gorsee-s3",
|
|
93
|
+
|
|
94
|
+
async setup() {
|
|
95
|
+
storageClient = createStorageClient(config)
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
async teardown() {
|
|
99
|
+
storageClient = null
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Stripe payments plugin -- uses native fetch, no stripe SDK dependency
|
|
2
|
+
|
|
3
|
+
import type { MiddlewareFn, Context } from "../server/middleware.ts"
|
|
4
|
+
import type { GorseePlugin } from "./index.ts"
|
|
5
|
+
import { definePlugin } from "./index.ts"
|
|
6
|
+
|
|
7
|
+
export interface StripePluginConfig {
|
|
8
|
+
secretKey: string
|
|
9
|
+
webhookSecret?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CheckoutSessionOptions {
|
|
13
|
+
lineItems: Array<{ price: string; quantity: number }>
|
|
14
|
+
mode?: "payment" | "subscription"
|
|
15
|
+
successUrl: string
|
|
16
|
+
cancelUrl: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StripeEvent {
|
|
20
|
+
id: string
|
|
21
|
+
type: string
|
|
22
|
+
data: { object: Record<string, unknown> }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StripeClient {
|
|
26
|
+
createCheckoutSession(options: CheckoutSessionOptions): Promise<{ url: string; id: string }>
|
|
27
|
+
verifyWebhook(request: Request): Promise<StripeEvent>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const STRIPE_API = "https://api.stripe.com/v1"
|
|
31
|
+
|
|
32
|
+
let stripeClient: StripeClient | null = null
|
|
33
|
+
|
|
34
|
+
/** Returns the Stripe client (available after setup) */
|
|
35
|
+
export function getStripe(): StripeClient {
|
|
36
|
+
if (!stripeClient) {
|
|
37
|
+
throw new Error("Stripe not initialized. Did you register stripePlugin?")
|
|
38
|
+
}
|
|
39
|
+
return stripeClient
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createStripeClient(config: StripePluginConfig): StripeClient {
|
|
43
|
+
const authHeader = `Basic ${btoa(config.secretKey + ":")}`
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
async createCheckoutSession(options) {
|
|
47
|
+
const params = new URLSearchParams()
|
|
48
|
+
params.set("mode", options.mode ?? "payment")
|
|
49
|
+
params.set("success_url", options.successUrl)
|
|
50
|
+
params.set("cancel_url", options.cancelUrl)
|
|
51
|
+
options.lineItems.forEach((item, i) => {
|
|
52
|
+
params.set(`line_items[${i}][price]`, item.price)
|
|
53
|
+
params.set(`line_items[${i}][quantity]`, String(item.quantity))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const res = await fetch(`${STRIPE_API}/checkout/sessions`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { Authorization: authHeader, "Content-Type": "application/x-www-form-urlencoded" },
|
|
59
|
+
body: params.toString(),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
const err = await res.text()
|
|
64
|
+
throw new Error(`Stripe API error (${res.status}): ${err}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const data = (await res.json()) as { url: string; id: string }
|
|
68
|
+
return { url: data.url, id: data.id }
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async verifyWebhook(request: Request) {
|
|
72
|
+
const body = await request.text()
|
|
73
|
+
const sig = request.headers.get("stripe-signature") ?? ""
|
|
74
|
+
|
|
75
|
+
if (!config.webhookSecret) {
|
|
76
|
+
throw new Error("Webhook secret not configured")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Verify signature using HMAC-SHA256
|
|
80
|
+
const encoder = new TextEncoder()
|
|
81
|
+
const timestamp = sig.split(",").find((s) => s.startsWith("t="))?.slice(2) ?? ""
|
|
82
|
+
const v1Sig = sig.split(",").find((s) => s.startsWith("v1="))?.slice(3) ?? ""
|
|
83
|
+
const payload = `${timestamp}.${body}`
|
|
84
|
+
|
|
85
|
+
const key = await crypto.subtle.importKey(
|
|
86
|
+
"raw",
|
|
87
|
+
encoder.encode(config.webhookSecret),
|
|
88
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
89
|
+
false,
|
|
90
|
+
["sign"],
|
|
91
|
+
)
|
|
92
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload))
|
|
93
|
+
const expected = Array.from(new Uint8Array(signature))
|
|
94
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
95
|
+
.join("")
|
|
96
|
+
|
|
97
|
+
if (expected !== v1Sig) {
|
|
98
|
+
throw new Error("Invalid webhook signature")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return JSON.parse(body) as StripeEvent
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Creates a Stripe payments plugin */
|
|
107
|
+
export function stripePlugin(config: StripePluginConfig): GorseePlugin {
|
|
108
|
+
return definePlugin({
|
|
109
|
+
name: "gorsee-stripe",
|
|
110
|
+
|
|
111
|
+
async setup(app) {
|
|
112
|
+
stripeClient = createStripeClient(config)
|
|
113
|
+
|
|
114
|
+
// Register webhook route if webhook secret is configured
|
|
115
|
+
if (config.webhookSecret) {
|
|
116
|
+
app.addRoute("/api/stripe/webhook", async (ctx) => {
|
|
117
|
+
try {
|
|
118
|
+
const event = await stripeClient!.verifyWebhook(ctx.request)
|
|
119
|
+
return new Response(JSON.stringify({ received: true, type: event.type }), {
|
|
120
|
+
headers: { "Content-Type": "application/json" },
|
|
121
|
+
})
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return new Response(JSON.stringify({ error: String(err) }), { status: 400 })
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async teardown() {
|
|
130
|
+
stripeClient = null
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Tailwind CSS integration plugin -- build pipeline hook
|
|
2
|
+
|
|
3
|
+
import type { GorseePlugin } from "./index.ts"
|
|
4
|
+
import { definePlugin } from "./index.ts"
|
|
5
|
+
|
|
6
|
+
export interface TailwindPluginConfig {
|
|
7
|
+
configPath?: string
|
|
8
|
+
inputCSS?: string
|
|
9
|
+
outputCSS?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Generates tailwind.config.ts content */
|
|
13
|
+
export function generateTailwindConfig(options?: {
|
|
14
|
+
content?: string[]
|
|
15
|
+
theme?: Record<string, unknown>
|
|
16
|
+
}): string {
|
|
17
|
+
const content = options?.content ?? [
|
|
18
|
+
"./routes/**/*.{tsx,ts}",
|
|
19
|
+
"./components/**/*.{tsx,ts}",
|
|
20
|
+
]
|
|
21
|
+
const themeStr = options?.theme
|
|
22
|
+
? JSON.stringify(options.theme, null, 4)
|
|
23
|
+
: "{}"
|
|
24
|
+
|
|
25
|
+
return `/** @type {import('tailwindcss').Config} */
|
|
26
|
+
export default {
|
|
27
|
+
content: ${JSON.stringify(content, null, 4)},
|
|
28
|
+
theme: {
|
|
29
|
+
extend: ${themeStr},
|
|
30
|
+
},
|
|
31
|
+
plugins: [],
|
|
32
|
+
}
|
|
33
|
+
`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Generates base CSS with @tailwind directives */
|
|
37
|
+
export function generateTailwindCSS(): string {
|
|
38
|
+
return `@tailwind base;
|
|
39
|
+
@tailwind components;
|
|
40
|
+
@tailwind utilities;
|
|
41
|
+
`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Creates a Tailwind CSS integration plugin */
|
|
45
|
+
export function tailwindPlugin(config: TailwindPluginConfig = {}): GorseePlugin {
|
|
46
|
+
const inputCSS = config.inputCSS ?? "./styles/globals.css"
|
|
47
|
+
const outputCSS = config.outputCSS ?? "./.gorsee/client/tailwind.css"
|
|
48
|
+
|
|
49
|
+
return definePlugin({
|
|
50
|
+
name: "gorsee-tailwind",
|
|
51
|
+
|
|
52
|
+
async setup() {
|
|
53
|
+
// Generate tailwind.config.ts if it doesn't exist
|
|
54
|
+
const configPath = config.configPath ?? "./tailwind.config.ts"
|
|
55
|
+
const file = Bun.file(configPath)
|
|
56
|
+
if (!(await file.exists())) {
|
|
57
|
+
await Bun.write(configPath, generateTailwindConfig())
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
buildPlugins() {
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
name: "gorsee-tailwind-transform",
|
|
65
|
+
setup(build) {
|
|
66
|
+
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
|
67
|
+
const source = await Bun.file(args.path).text()
|
|
68
|
+
|
|
69
|
+
// If file contains @tailwind directives, process it
|
|
70
|
+
if (source.includes("@tailwind")) {
|
|
71
|
+
try {
|
|
72
|
+
const proc = Bun.spawn(
|
|
73
|
+
["bunx", "tailwindcss", "-i", args.path, "-o", outputCSS, "--minify"],
|
|
74
|
+
{ stdin: "inherit", stdout: "pipe", stderr: "pipe" },
|
|
75
|
+
)
|
|
76
|
+
await proc.exited
|
|
77
|
+
const processed = await Bun.file(outputCSS).text()
|
|
78
|
+
return { contents: processed, loader: "css" }
|
|
79
|
+
} catch {
|
|
80
|
+
// Fallback: return raw CSS if tailwindcss CLI not available
|
|
81
|
+
return { contents: source, loader: "css" }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return undefined
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
}
|