Skip to content

Commit fe8d89d

Browse files
authored
Set up storybook and chromatic to test with onbook (#3038)
* WIP - fresh storybook setup * Tailwind + fonts working * WIP - trying to figure out screenshotting * Test ci/cd as well * Fix ci * Try again * Remove lint from nextjs build * Test env mock * WIP
1 parent ccd2302 commit fe8d89d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+4350
-15
lines changed

.github/workflows/chromatic.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Chromatic
2+
3+
on: push
4+
5+
jobs:
6+
chromatic:
7+
name: Run Chromatic
8+
runs-on: ubuntu-latest
9+
env:
10+
SKIP_ENV_VALIDATION: true
11+
steps:
12+
- name: Checkout code
13+
uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0
16+
17+
- name: Setup Bun
18+
uses: oven-sh/setup-bun@v2
19+
with:
20+
bun-version: 1.3.1
21+
22+
- name: Install dependencies
23+
run: bun install --frozen-lockfile
24+
25+
- name: Run Chromatic
26+
uses: chromaui/action@latest
27+
with:
28+
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
29+
workingDir: apps/web/client
30+
buildScriptName: build-storybook

apps/web/client/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,6 @@ yarn-error.log*
4747

4848
# mastra
4949
.mastra/
50+
51+
*storybook.log
52+
storybook-static

apps/web/client/.storybook/main.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type { StorybookConfig } from "@storybook/nextjs-vite";
2+
3+
import { dirname, join, relative } from "path"
4+
5+
import { fileURLToPath } from "url"
6+
import { existsSync } from "fs"
7+
8+
/**
9+
* This function is used to resolve the absolute path of a package.
10+
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
11+
*/
12+
function getAbsolutePath(value: string): any {
13+
return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`)))
14+
}
15+
16+
/**
17+
* Find the git repository root by walking up the directory tree
18+
*/
19+
function findGitRoot(startPath: string): string | null {
20+
let currentPath = startPath;
21+
22+
while (currentPath !== dirname(currentPath)) {
23+
if (existsSync(join(currentPath, '.git'))) {
24+
return currentPath;
25+
}
26+
currentPath = dirname(currentPath);
27+
}
28+
29+
return null;
30+
}
31+
const config: StorybookConfig = {
32+
"stories": [
33+
"../src/**/*.mdx",
34+
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
35+
],
36+
"addons": [
37+
getAbsolutePath('@chromatic-com/storybook'),
38+
getAbsolutePath('@storybook/addon-docs'),
39+
getAbsolutePath('@storybook/addon-onboarding'),
40+
getAbsolutePath("@storybook/addon-a11y"),
41+
getAbsolutePath("@storybook/addon-vitest")
42+
],
43+
"framework": {
44+
"name": getAbsolutePath("@storybook/nextjs-vite"),
45+
"options": {}
46+
},
47+
"staticDirs": [
48+
"../public"
49+
],
50+
async viteFinal(config) {
51+
const { mergeConfig } = await import('vite');
52+
53+
const __dirname = dirname(fileURLToPath(import.meta.url));
54+
const storybookDir = join(__dirname, '..');
55+
const gitRoot = findGitRoot(storybookDir);
56+
const storybookLocation = gitRoot ? relative(gitRoot, storybookDir) : '';
57+
58+
return mergeConfig(config, {
59+
define: {
60+
'process.env': '{}',
61+
'process': '{"env": {}}',
62+
},
63+
resolve: {
64+
alias: {
65+
'@/utils/supabase/client': fileURLToPath(
66+
new URL('./mocks/supabase-client.ts', import.meta.url)
67+
),
68+
'@/trpc/react': fileURLToPath(
69+
new URL('./mocks/trpc-react.tsx', import.meta.url)
70+
),
71+
'~/trpc/react': fileURLToPath(
72+
new URL('./mocks/trpc-react.tsx', import.meta.url)
73+
),
74+
},
75+
},
76+
plugins: [
77+
{
78+
name: 'onbook-metadata',
79+
configureServer(server) {
80+
// Serve metadata in dev mode
81+
server.middlewares.use((req, res, next) => {
82+
if (req.url === '/onbook-metadata.json') {
83+
res.setHeader('Content-Type', 'application/json');
84+
res.setHeader('Access-Control-Allow-Origin', '*');
85+
res.end(JSON.stringify({ storybookLocation }));
86+
return;
87+
}
88+
next();
89+
});
90+
},
91+
configurePreviewServer(server) {
92+
// Serve metadata in preview/build mode (for Chromatic)
93+
server.middlewares.use((req, res, next) => {
94+
if (req.url === '/onbook-metadata.json') {
95+
res.setHeader('Content-Type', 'application/json');
96+
res.setHeader('Access-Control-Allow-Origin', '*');
97+
res.end(JSON.stringify({ storybookLocation }));
98+
return;
99+
}
100+
next();
101+
});
102+
},
103+
},
104+
],
105+
});
106+
},
107+
};
108+
export default config;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Mock Supabase client utilities for Storybook
3+
*
4+
* This mock prevents the import chain:
5+
* getFileUrlFromStorage → @/env → @t3-oss/env-nextjs → process.cwd()
6+
*
7+
* Stories should use type: 'url' for preview images instead of type: 'storage'
8+
* to avoid needing actual storage path resolution.
9+
*/
10+
11+
export const getFileUrlFromStorage = (bucket: string, path: string) => {
12+
// Return a mock URL - this shouldn't be called if stories use type: 'url'
13+
return `https://example.com/storage/${bucket}/${path}`;
14+
};
15+
16+
// Mock implementation for file info
17+
export const getFileInfoFromStorage = async (bucket: string, path: string) => {
18+
return null;
19+
};
20+
21+
// Mock implementation for upload
22+
export const uploadBlobToStorage = async (
23+
bucket: string,
24+
path: string,
25+
file: Blob,
26+
options: {
27+
upsert?: boolean;
28+
contentType?: string;
29+
cacheControl?: string;
30+
}
31+
) => {
32+
return null;
33+
};
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use client';
2+
3+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4+
import { createTRPCReact, httpBatchLink } from '@trpc/react-query';
5+
import { useState } from 'react';
6+
7+
/**
8+
* Mock tRPC client for Storybook
9+
*
10+
* This mock provides a functional tRPC context without making real API calls.
11+
* It prevents the import chain: AppRouter → subscriptionRouter/usageRouter → @onlook/stripe → dotenv.config()
12+
*
13+
* The mock client uses a dummy HTTP link that will never be called in Storybook
14+
* since we don't actually trigger mutations/queries in stories.
15+
*/
16+
17+
// Create a minimal mock AppRouter type to avoid importing real routers
18+
type MockAppRouter = Record<string, any>;
19+
20+
export const api = createTRPCReact<MockAppRouter>();
21+
22+
// Mock type exports to satisfy TypeScript
23+
export type RouterInputs = Record<string, any>;
24+
export type RouterOutputs = Record<string, any>;
25+
26+
// Create QueryClient singleton for Storybook
27+
let queryClientSingleton: QueryClient | undefined = undefined;
28+
const getQueryClient = () => {
29+
if (!queryClientSingleton) {
30+
queryClientSingleton = new QueryClient({
31+
defaultOptions: {
32+
queries: {
33+
staleTime: 60 * 1000,
34+
retry: false,
35+
},
36+
},
37+
});
38+
}
39+
return queryClientSingleton;
40+
};
41+
42+
export function TRPCReactProvider(props: { children: React.ReactNode }) {
43+
const queryClient = getQueryClient();
44+
45+
const [trpcClient] = useState(() =>
46+
api.createClient({
47+
links: [
48+
httpBatchLink({
49+
url: '/api/trpc',
50+
// This will never actually be called in Storybook
51+
}),
52+
],
53+
}),
54+
);
55+
56+
return (
57+
<QueryClientProvider client={queryClient}>
58+
<api.Provider client={trpcClient} queryClient={queryClient}>
59+
{props.children}
60+
</api.Provider>
61+
</QueryClientProvider>
62+
);
63+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { Preview } from '@storybook/nextjs-vite'
2+
import '@onlook/ui/globals.css'
3+
import { Inter } from 'next/font/google'
4+
import { NextIntlClientProvider } from 'next-intl'
5+
import { ThemeProvider } from 'next-themes'
6+
import messages from '../messages/en.json'
7+
import { TRPCReactProvider } from './mocks/trpc-react'
8+
9+
const inter = Inter({
10+
subsets: ['latin'],
11+
variable: '--font-inter',
12+
})
13+
14+
const preview: Preview = {
15+
decorators: [
16+
(Story) => (
17+
<ThemeProvider
18+
attribute="class"
19+
forcedTheme="dark"
20+
enableSystem
21+
disableTransitionOnChange
22+
>
23+
<TRPCReactProvider>
24+
<NextIntlClientProvider locale="en" messages={messages}>
25+
<div className={inter.variable}>
26+
<Story />
27+
</div>
28+
</NextIntlClientProvider>
29+
</TRPCReactProvider>
30+
</ThemeProvider>
31+
),
32+
],
33+
parameters: {
34+
controls: {
35+
matchers: {
36+
color: /(background|color)$/i,
37+
date: /Date$/i,
38+
},
39+
},
40+
41+
a11y: {
42+
// 'todo' - show a11y violations in the test UI only
43+
// 'error' - fail CI on a11y violations
44+
// 'off' - skip a11y checks entirely
45+
test: 'todo'
46+
}
47+
},
48+
};
49+
50+
export default preview;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
2+
import { setProjectAnnotations } from '@storybook/nextjs-vite';
3+
import * as projectAnnotations from './preview';
4+
5+
// This is an important step to apply the right configuration when testing your stories.
6+
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
7+
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"projectToken": "chpt_549eddb0e603baf",
3+
"buildScriptName": "build-storybook",
4+
"exitZeroOnChanges": true
5+
}

apps/web/client/eslint.config.js

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1+
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
2+
import storybook from "eslint-plugin-storybook";
3+
14
import baseConfig from "@onlook/eslint/base";
25
import nextjsConfig, { restrictEnvAccess } from "@onlook/eslint/nextjs";
36
import reactConfig from "@onlook/eslint/react";
47

58
/** @type {import('typescript-eslint').Config} */
6-
export default [
7-
{
8-
ignores: [".next/**"],
9-
},
10-
...baseConfig,
11-
...reactConfig,
12-
...nextjsConfig,
13-
...restrictEnvAccess,
14-
];
9+
export default [{
10+
ignores: [".next/**"],
11+
}, ...baseConfig, ...reactConfig, ...nextjsConfig, ...restrictEnvAccess, ...storybook.configs["flat/recommended"]];

apps/web/client/next.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import './src/env';
1010
const nextConfig: NextConfig = {
1111
devIndicators: false,
1212
...(process.env.STANDALONE_BUILD === 'true' && { output: 'standalone' }),
13+
eslint: {
14+
// Don't run ESLint during builds - handle it separately in CI
15+
ignoreDuringBuilds: true,
16+
},
1317
};
1418

1519
if (process.env.NODE_ENV === 'development') {

0 commit comments

Comments
 (0)