diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ff562dc..f5b787e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -74,6 +74,36 @@ jobs: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} command: pages deploy dist/registry --project-name=shaderbase-registry + deploy-web: + needs: [check, deploy-registry] + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.8 + + - name: Install Dependencies + run: bun install --frozen-lockfile + + - name: Build Web App for Cloudflare Pages + run: cd apps/web && NITRO_PRESET=cloudflare-pages bun run build + + - name: Deploy to Cloudflare Pages + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + command: pages deploy apps/web/.output/public --project-name=shaderbase-web + publish-cli: needs: check if: github.ref == 'refs/heads/master' && github.event_name == 'push' diff --git a/apps/web/src/lib/server/shader-detail.ts b/apps/web/src/lib/server/shader-detail.ts index 0b19de7..ac3a1ae 100644 --- a/apps/web/src/lib/server/shader-detail.ts +++ b/apps/web/src/lib/server/shader-detail.ts @@ -1,5 +1,5 @@ import { createServerFn } from '@tanstack/solid-start' -import { loadShaderDetail } from './load-shader-detail.ts' +import { getShaderDetailFromSource } from './shader-source.ts' export type { ShaderDetail, @@ -10,10 +10,5 @@ export type { export const getShaderDetail = createServerFn({ method: 'GET' }) .inputValidator((input: { name: string }) => input) .handler(async ({ data }) => { - const { join, resolve } = await import('node:path') - - const repoRoot = resolve(process.cwd(), '../..') - const shaderDir = join(repoRoot, 'shaders', data.name) - - return loadShaderDetail(shaderDir) + return getShaderDetailFromSource(data.name) }) diff --git a/apps/web/src/lib/server/shader-source.ts b/apps/web/src/lib/server/shader-source.ts new file mode 100644 index 0000000..a02f284 --- /dev/null +++ b/apps/web/src/lib/server/shader-source.ts @@ -0,0 +1,108 @@ +import type { ShaderEntry } from './list-shaders.ts' +import type { ShaderDetail, ShaderDetailRecipe } from './load-shader-detail.ts' + +/** + * Environment-aware shader data source. + * + * When REGISTRY_URL is set (Cloudflare Pages production), fetches from the + * registry CDN. Otherwise falls back to the local filesystem (local dev). + */ +const REGISTRY_URL = process.env.REGISTRY_URL || '' + +export async function listShadersFromSource(): Promise { + if (REGISTRY_URL) { + const res = await fetch(`${REGISTRY_URL}/index.json`) + if (!res.ok) throw new Error(`Failed to fetch registry index: ${res.status}`) + const index = (await res.json()) as { + shaders: Array> + } + return index.shaders.map((s) => ({ + name: s.name as string, + displayName: s.displayName as string, + summary: s.summary as string, + category: s.category as string, + sourceKind: s.sourceKind as string, + tags: s.tags as string[], + pipeline: s.pipeline as string, + stage: s.stage as string, + renderers: s.renderers as string[], + environments: s.environments as string[], + })) + } + + // Fallback: filesystem (local dev) + const { join, resolve } = await import('node:path') + const { listShadersFromDisk } = await import('./list-shaders.ts') + const repoRoot = resolve(process.cwd(), '../..') + return listShadersFromDisk(join(repoRoot, 'shaders')) +} + +export async function getShaderDetailFromSource(name: string): Promise { + if (REGISTRY_URL) { + const res = await fetch(`${REGISTRY_URL}/shaders/${name}.json`) + if (!res.ok) throw new Error(`Shader "${name}" not found`) + const bundle = (await res.json()) as Record + + const compatibility = bundle.compatibility as Record + const capabilityProfile = bundle.capabilityProfile as Record + const provenance = bundle.provenance as Record + const attribution = (provenance.attribution as Record) ?? {} + const uniformsFull = bundle.uniformsFull as ShaderDetail['uniforms'] + const recipesRecord = (bundle.recipes as Record>) ?? {} + + // Convert recipes from Record to ShaderDetailRecipe[] + const recipes: ShaderDetailRecipe[] = Object.entries(recipesRecord).map( + ([target, r]) => ({ + target, + code: r.code as string, + exportName: r.exportName as string, + summary: r.summary as string, + placeholders: (r.placeholders as ShaderDetailRecipe['placeholders']) ?? [], + requirements: (r.requirements as string[]) ?? [], + }), + ) + + return { + name: bundle.name as string, + displayName: bundle.displayName as string, + version: bundle.version as string, + summary: bundle.summary as string, + description: bundle.description as string, + author: bundle.author as ShaderDetail['author'], + license: bundle.license as string, + tags: bundle.tags as string[], + category: bundle.category as string, + pipeline: capabilityProfile.pipeline as string, + stage: capabilityProfile.stage as string, + requires: (capabilityProfile.requires as string[]) ?? [], + capabilityOutputs: (capabilityProfile.outputs as string[]) ?? [], + threeRange: compatibility.three as string, + renderers: compatibility.renderers as string[], + material: compatibility.material as string, + environments: compatibility.environments as string[], + uniforms: uniformsFull, + inputs: bundle.inputs as ShaderDetail['inputs'], + outputs: bundle.outputs as ShaderDetail['outputs'], + vertexSource: bundle.vertexSource as string, + fragmentSource: bundle.fragmentSource as string, + recipes, + // previewSvg is not available in the registry bundle + previewSvg: null, + provenance: { + sourceKind: provenance.sourceKind as string, + sources: (provenance.sources as ShaderDetail['provenance']['sources']) ?? [], + attribution: { + summary: attribution.summary as string, + requiredNotice: attribution.requiredNotice as string | undefined, + }, + notes: provenance.notes as string | undefined, + }, + } + } + + // Fallback: filesystem (local dev) + const { join, resolve } = await import('node:path') + const { loadShaderDetail } = await import('./load-shader-detail.ts') + const repoRoot = resolve(process.cwd(), '../..') + return loadShaderDetail(join(repoRoot, 'shaders', name)) +} diff --git a/apps/web/src/lib/server/shaders.ts b/apps/web/src/lib/server/shaders.ts index 9191eae..6d636db 100644 --- a/apps/web/src/lib/server/shaders.ts +++ b/apps/web/src/lib/server/shaders.ts @@ -1,27 +1,31 @@ import { createServerFn } from '@tanstack/solid-start' -import { listShadersFromDisk } from './list-shaders.ts' +import { listShadersFromSource } from './shader-source.ts' export type { ShaderEntry } from './list-shaders.ts' export const listShaders = createServerFn({ method: 'GET' }).handler( async () => { - const { join, resolve } = await import('node:path') + const shaders = await listShadersFromSource() - const { getAllShaderRatings } = await import('./reviews-db') - - const repoRoot = resolve(process.cwd(), '../..') - const shadersRoot = join(repoRoot, 'shaders') - - const shaders = await listShadersFromDisk(shadersRoot) - const ratings = getAllShaderRatings() - - return shaders.map((shader) => { - const rating = ratings[shader.name] - return { - ...shader, - averageRating: rating?.average, - reviewCount: rating?.count, + // Reviews use node:sqlite which is unavailable on Cloudflare. + // Only attempt to load ratings when running locally (no REGISTRY_URL). + if (!process.env.REGISTRY_URL) { + try { + const { getAllShaderRatings } = await import('./reviews-db') + const ratings = getAllShaderRatings() + return shaders.map((shader) => { + const rating = ratings[shader.name] + return { + ...shader, + averageRating: rating?.average, + reviewCount: rating?.count, + } + }) + } catch { + /* reviews unavailable */ } - }) + } + + return shaders }, ) diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 6c2d629..1664615 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -11,7 +11,9 @@ import { nitro } from 'nitro/vite' export default defineConfig({ plugins: [ devtools(), - nitro(), + nitro({ + preset: process.env.NITRO_PRESET || 'node-server', + }), // this is the plugin that enables path aliases viteTsConfigPaths({ projects: ['./tsconfig.json'], diff --git a/apps/web/wrangler.toml b/apps/web/wrangler.toml index 953c801..37b635f 100644 --- a/apps/web/wrangler.toml +++ b/apps/web/wrangler.toml @@ -1,6 +1,9 @@ name = "shaderbase-web" compatibility_date = "2026-03-01" +[vars] +REGISTRY_URL = "https://shaderbase-registry.pages.dev" + [[d1_databases]] binding = "DB" database_name = "shaderbase-reviews"