Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
9 changes: 2 additions & 7 deletions apps/web/src/lib/server/shader-detail.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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)
})
108 changes: 108 additions & 0 deletions apps/web/src/lib/server/shader-source.ts
Original file line number Diff line number Diff line change
@@ -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<ShaderEntry[]> {
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<Record<string, unknown>>
}
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<ShaderDetail> {
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<string, unknown>

const compatibility = bundle.compatibility as Record<string, unknown>
const capabilityProfile = bundle.capabilityProfile as Record<string, unknown>
const provenance = bundle.provenance as Record<string, unknown>
const attribution = (provenance.attribution as Record<string, unknown>) ?? {}
const uniformsFull = bundle.uniformsFull as ShaderDetail['uniforms']
const recipesRecord = (bundle.recipes as Record<string, Record<string, unknown>>) ?? {}

// Convert recipes from Record<target, bundle> 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))
}
38 changes: 21 additions & 17 deletions apps/web/src/lib/server/shaders.ts
Original file line number Diff line number Diff line change
@@ -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
},
)
4 changes: 3 additions & 1 deletion apps/web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
3 changes: 3 additions & 0 deletions apps/web/wrangler.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down