From 83a76c2f7cd38fbd8025860c67f78b74880fa067 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 13 Feb 2024 17:29:21 +1100 Subject: [PATCH 1/5] First POC of how an app-level SB could function --- .gitignore | 2 ++ .storybook/main.ts | 2 +- src/app/StorybookLayout.tsx | 44 ++++++++++++++++++++++++++++ src/app/builds/[number]/page.tsx | 21 ++++++++++++++ src/app/layout.tsx | 5 +++- src/app/sb-manager/page.tsx | 26 +++++++++++++++++ src/data/__mock__/getBuild.ts | 6 ++++ src/data/getBuild.ts | 3 ++ src/middleware.ts | 48 +++++++++++++++++++++++++++++++ src/mock.ts | 40 ++++++++++++++++++++++++++ src/page-stories/build.stories.ts | 35 ++++++++++++++++++++++ tsconfig.json | 2 +- 12 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 src/app/StorybookLayout.tsx create mode 100644 src/app/builds/[number]/page.tsx create mode 100644 src/app/sb-manager/page.tsx create mode 100644 src/data/__mock__/getBuild.ts create mode 100644 src/data/getBuild.ts create mode 100644 src/middleware.ts create mode 100644 src/mock.ts create mode 100644 src/page-stories/build.stories.ts diff --git a/.gitignore b/.gitignore index fd3dbb5..2d6fcba 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +src/app/(sb) \ No newline at end of file diff --git a/.storybook/main.ts b/.storybook/main.ts index 429a3ad..dd924cb 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,7 +1,7 @@ import type { StorybookConfig } from '@storybook/nextjs-server'; const config: StorybookConfig = { - stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + stories: ['../src/page-stories/*.stories.@(js|jsx|mjs|ts|tsx)'], addons: [ '@storybook/addon-onboarding', '@storybook/addon-links', diff --git a/src/app/StorybookLayout.tsx b/src/app/StorybookLayout.tsx new file mode 100644 index 0000000..650d97d --- /dev/null +++ b/src/app/StorybookLayout.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { composeStory } from '@storybook/react'; + +import * as stories from '../page-stories/build.stories'; +import { cookies, headers } from 'next/headers'; +import { Mock } from '../mock'; + +const map = { + 'pages-build--build-seven-desc': { + csf: stories, + key: 'BuildSevenDesc', + }, + 'pages-build--build-six': { + csf: stories, + key: 'BuildSix', + }, +}; + +export const StorybookLayout = ({ children }) => { + const storyCookie = cookies().get('__storyId__'); + + console.log('StorybookLayout'); + console.log({ storyCookie }); + if (storyCookie) { + const storyId = storyCookie.value; + if (!storyId) throw new Error('no storyId'); + if (!(storyId in map)) throw new Error(`unknown storyId: '${storyId}`); + + const data = map[storyId as keyof typeof map]; + + const { args } = composeStory( + // @ts-expect-error fix types + data.csf[data.key], + data.csf.default, + {}, + data.key + ); + + console.log('Setting mock to', args.$mock); + Mock.set(args.$mock); + } + + return children; +}; diff --git a/src/app/builds/[number]/page.tsx b/src/app/builds/[number]/page.tsx new file mode 100644 index 0000000..804adb5 --- /dev/null +++ b/src/app/builds/[number]/page.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import { getBuild } from '@/getBuild'; + +export default async function Build({ + params: { number }, + searchParams: { sort = 'asc' }, +}: { + params: { number: string }; + searchParams: { [key: string]: string | string[] | undefined }; +}) { + console.log('Build'); + return ( +
+
+        Build page, build number: {number}, sort: {sort} Build data:{' '}
+        {JSON.stringify(await getBuild())}
+      
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4ff5d3c..f0f46e5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; +import { StorybookLayout } from './StorybookLayout'; // import "./globals.css"; const inter = Inter({ subsets: ['latin'] }); @@ -16,7 +17,9 @@ export default function RootLayout({ }>) { return ( - {children} + + {children} + ); } diff --git a/src/app/sb-manager/page.tsx b/src/app/sb-manager/page.tsx new file mode 100644 index 0000000..5fd1ce5 --- /dev/null +++ b/src/app/sb-manager/page.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Link from 'next/link'; +import * as stories from '../../page-stories/build.stories'; + +const map = { + 'pages-build--build-seven-desc': { + csf: stories, + key: 'BuildSevenDesc', + }, + 'pages-build--build-six': { + csf: stories, + key: 'BuildSix', + }, +}; + +export default function Page() { + return ( + + ); +} diff --git a/src/data/__mock__/getBuild.ts b/src/data/__mock__/getBuild.ts new file mode 100644 index 0000000..a23e8be --- /dev/null +++ b/src/data/__mock__/getBuild.ts @@ -0,0 +1,6 @@ +import { Mock } from '../../mock'; +import * as module from '../getBuild'; + +const { getBuild } = Mock.module(module, { getBuild: () => Mock.get()?.build }); + +export { getBuild }; diff --git a/src/data/getBuild.ts b/src/data/getBuild.ts new file mode 100644 index 0000000..d8974ab --- /dev/null +++ b/src/data/getBuild.ts @@ -0,0 +1,3 @@ +export async function getBuild(id: number) { + throw new Error('Do some server stuff'); +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..c6d2227 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { composeStory } from '@storybook/react'; + +import * as stories from './page-stories/build.stories'; + +const map = { + 'pages-build--build-seven-desc': { + csf: stories, + key: 'BuildSevenDesc', + }, + 'pages-build--build-six': { + csf: stories, + key: 'BuildSix', + }, +}; + +export function middleware(request: NextRequest) { + const storyId = request.url.split('/').at(-1); + + if (!storyId) throw new Error('no storyId'); + if (!(storyId in map)) throw new Error(`unknown storyId: '${storyId}`); + + const data = map[storyId as keyof typeof map]; + + const { args } = composeStory( + // @ts-expect-error fix types + data.csf[data.key], + data.csf.default, + {}, + data.key + ); + + // TODO compose stories doesn't handle URLs (we could use parameters temporarily?) + const { url } = data.csf.default; + + // TODO make this bit generic + const newUrl = + url.replace('[number]', (args.$url?.number || '').toString()) + + (args.$url.sort ? `?sort=${args.$url.sort}` : ''); + + const response = NextResponse.redirect(new URL(newUrl, request.url)); + response.cookies.set('__storyId__', storyId); + return response; +} + +export const config = { + matcher: '/storybook-redirect/:path*', +}; diff --git a/src/mock.ts b/src/mock.ts new file mode 100644 index 0000000..8ae4475 --- /dev/null +++ b/src/mock.ts @@ -0,0 +1,40 @@ +// @ts-expect-error wrong react version +import React, { cache } from 'react'; + +const requestId = cache(() => Math.random()); +type Exports = Record; + +export const Mock = { + cache: {} as Record, + storybookRequests: {} as Record, + set(data: any) { + const id = requestId(); + console.log('Mock.set', { id, data }); + this.cache[id] = data; + this.storybookRequests[id] = true; + }, + get() { + const id = requestId(); + const data = this.cache[id]; + console.log('Mock.get', { id, data }); + return data; + }, + fn(original: any, mock: any) { + return async (...args: any) => { + // Important -- we need to do this because otherwise the page renders *before* the layout + await new Promise((r) => setTimeout(r, 0)); + + const id = requestId(); + const isStorybook = !!this.storybookRequests[id]; + console.log(`Checking ${id}, using SB: ${isStorybook}`); + return isStorybook ? mock(...args) : original(...args); + }; + }, + module(original: Exports, mocks: Exports) { + const result = { ...original }; + Object.keys(mocks).forEach((key) => { + result[key] = Mock.fn(original[key], mocks[key]); + }); + return result; + }, +}; diff --git a/src/page-stories/build.stories.ts b/src/page-stories/build.stories.ts new file mode 100644 index 0000000..323e39b --- /dev/null +++ b/src/page-stories/build.stories.ts @@ -0,0 +1,35 @@ +const meta = { + title: 'Pages/Build', + url: '/builds/[number]', +}; + +export default meta; + +export const BuildSix = { + args: { + $url: { + number: 6, + }, + $mock: { + build: { + number: 6, + status: 'ACCEPTED', + }, + }, + }, +}; + +export const BuildSevenDesc = { + args: { + $url: { + number: 7, + sort: 'desc', + }, + $mock: { + build: { + number: 7, + status: 'IN_PROGRESS', + }, + }, + }, +}; diff --git a/tsconfig.json b/tsconfig.json index 7b28589..24087d0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,7 @@ } ], "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/data/__mock__/*", "./src/data/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], From 0bda6655b265a5fe6b3e12d38727ae288f7a50a2 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 15 Feb 2024 17:14:49 +1100 Subject: [PATCH 2/5] Get data directly in mock lib, rather than indirectly via layout --- src/app/StorybookLayout.tsx | 42 +-------------- src/app/sb-manager/page.tsx | 19 ++----- src/data/__mock__/getBuild.ts | 8 ++- src/middleware.ts | 18 ++----- src/mock.ts | 90 ++++++++++++++++++------------- src/page-stories/build.stories.ts | 4 +- src/storyIndex.ts | 16 ++++++ 7 files changed, 84 insertions(+), 113 deletions(-) create mode 100644 src/storyIndex.ts diff --git a/src/app/StorybookLayout.tsx b/src/app/StorybookLayout.tsx index 650d97d..2cf12b9 100644 --- a/src/app/StorybookLayout.tsx +++ b/src/app/StorybookLayout.tsx @@ -1,44 +1,4 @@ -import React from 'react'; -import { composeStory } from '@storybook/react'; - -import * as stories from '../page-stories/build.stories'; -import { cookies, headers } from 'next/headers'; -import { Mock } from '../mock'; - -const map = { - 'pages-build--build-seven-desc': { - csf: stories, - key: 'BuildSevenDesc', - }, - 'pages-build--build-six': { - csf: stories, - key: 'BuildSix', - }, -}; - export const StorybookLayout = ({ children }) => { - const storyCookie = cookies().get('__storyId__'); - - console.log('StorybookLayout'); - console.log({ storyCookie }); - if (storyCookie) { - const storyId = storyCookie.value; - if (!storyId) throw new Error('no storyId'); - if (!(storyId in map)) throw new Error(`unknown storyId: '${storyId}`); - - const data = map[storyId as keyof typeof map]; - - const { args } = composeStory( - // @ts-expect-error fix types - data.csf[data.key], - data.csf.default, - {}, - data.key - ); - - console.log('Setting mock to', args.$mock); - Mock.set(args.$mock); - } - + // TODO - render SB UI return children; }; diff --git a/src/app/sb-manager/page.tsx b/src/app/sb-manager/page.tsx index 5fd1ce5..1e7089f 100644 --- a/src/app/sb-manager/page.tsx +++ b/src/app/sb-manager/page.tsx @@ -1,24 +1,15 @@ import React from 'react'; import Link from 'next/link'; -import * as stories from '../../page-stories/build.stories'; - -const map = { - 'pages-build--build-seven-desc': { - csf: stories, - key: 'BuildSevenDesc', - }, - 'pages-build--build-six': { - csf: stories, - key: 'BuildSix', - }, -}; +import { storyIndex } from '../../storyIndex'; export default function Page() { return (
    - {Object.entries(map).map(([id, { key }]) => ( + {Object.entries(storyIndex).map(([id, { title, name }]) => (
  • - {key} + + {title}: {name} +
  • ))}
diff --git a/src/data/__mock__/getBuild.ts b/src/data/__mock__/getBuild.ts index a23e8be..6f56e6a 100644 --- a/src/data/__mock__/getBuild.ts +++ b/src/data/__mock__/getBuild.ts @@ -1,6 +1,4 @@ -import { Mock } from '../../mock'; -import * as module from '../getBuild'; +import { mockFn } from '../../mock'; +import { getBuild as getBuildOriginal } from '../getBuild'; -const { getBuild } = Mock.module(module, { getBuild: () => Mock.get()?.build }); - -export { getBuild }; +export const getBuild = mockFn(getBuildOriginal, 'getBuild'); diff --git a/src/middleware.ts b/src/middleware.ts index c6d2227..9bfb335 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,26 +1,14 @@ import { NextRequest, NextResponse } from 'next/server'; import { composeStory } from '@storybook/react'; - -import * as stories from './page-stories/build.stories'; - -const map = { - 'pages-build--build-seven-desc': { - csf: stories, - key: 'BuildSevenDesc', - }, - 'pages-build--build-six': { - csf: stories, - key: 'BuildSix', - }, -}; +import { storyIndex } from './storyIndex'; export function middleware(request: NextRequest) { const storyId = request.url.split('/').at(-1); if (!storyId) throw new Error('no storyId'); - if (!(storyId in map)) throw new Error(`unknown storyId: '${storyId}`); + if (!(storyId in storyIndex)) throw new Error(`unknown storyId: '${storyId}`); - const data = map[storyId as keyof typeof map]; + const data = storyIndex[storyId as keyof typeof storyIndex]; const { args } = composeStory( // @ts-expect-error fix types diff --git a/src/mock.ts b/src/mock.ts index 8ae4475..9153814 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -1,40 +1,58 @@ +import { cookies } from 'next/headers'; // @ts-expect-error wrong react version -import React, { cache } from 'react'; +import { cache } from 'react'; +import { storyIndex } from './storyIndex'; +import { composeStory } from '@storybook/react'; + +const getStoryMockData = cache(() => { + const storyId = cookies().get('__storyId__')?.value; + console.log(`Getting mock data for '${storyId}'`); + + if (!storyId) return null; + if (!(storyId in storyIndex)) throw new Error(`unknown storyId: '${storyId}`); + + const data = storyIndex[storyId as keyof typeof storyIndex]; + + const { args } = composeStory( + // @ts-expect-error fix types + data.csf[data.key], + data.csf.default, + {}, + data.key + ); + + return args.$mock; +}); -const requestId = cache(() => Math.random()); type Exports = Record; -export const Mock = { - cache: {} as Record, - storybookRequests: {} as Record, - set(data: any) { - const id = requestId(); - console.log('Mock.set', { id, data }); - this.cache[id] = data; - this.storybookRequests[id] = true; - }, - get() { - const id = requestId(); - const data = this.cache[id]; - console.log('Mock.get', { id, data }); - return data; - }, - fn(original: any, mock: any) { - return async (...args: any) => { - // Important -- we need to do this because otherwise the page renders *before* the layout - await new Promise((r) => setTimeout(r, 0)); - - const id = requestId(); - const isStorybook = !!this.storybookRequests[id]; - console.log(`Checking ${id}, using SB: ${isStorybook}`); - return isStorybook ? mock(...args) : original(...args); - }; - }, - module(original: Exports, mocks: Exports) { - const result = { ...original }; - Object.keys(mocks).forEach((key) => { - result[key] = Mock.fn(original[key], mocks[key]); - }); - return result; - }, -}; +export function mockFn any>( + original: TFunction, + mockKey: string +) { + return (...args: Parameters) => { + const data = getStoryMockData(); + + if (data[mockKey]) { + return data[mockKey]; + } + + return original(...args); + }; +} + +export function mockModule(original: Exports, mockKey: string) { + return Object.fromEntries( + Object.entries(original).map(([exportName, exportValue]) => { + if (!(exportValue instanceof Function)) { + console.warn( + `We don't currently handle non-functional exports for ${exportName} export of ${mockKey} mock, returning original value` + ); + return [exportName, exportValue]; + } + + // TODO: should we instead assume the mock is an object + return [exportName, mockFn(exportValue, `${mockKey}.${exportName}`)]; + }) + ); +} diff --git a/src/page-stories/build.stories.ts b/src/page-stories/build.stories.ts index 323e39b..4bc746a 100644 --- a/src/page-stories/build.stories.ts +++ b/src/page-stories/build.stories.ts @@ -11,7 +11,7 @@ export const BuildSix = { number: 6, }, $mock: { - build: { + getBuild: { number: 6, status: 'ACCEPTED', }, @@ -26,7 +26,7 @@ export const BuildSevenDesc = { sort: 'desc', }, $mock: { - build: { + getBuild: { number: 7, status: 'IN_PROGRESS', }, diff --git a/src/storyIndex.ts b/src/storyIndex.ts new file mode 100644 index 0000000..cc43040 --- /dev/null +++ b/src/storyIndex.ts @@ -0,0 +1,16 @@ +import * as stories from './page-stories/build.stories'; + +export const storyIndex = { + 'pages-build--build-six': { + title: 'Pages / Build', + name: 'Build Six', + csf: stories, + key: 'BuildSix', + }, + 'pages-build--build-seven-desc': { + title: 'Pages / Build', + name: 'Build Seven Desc', + csf: stories, + key: 'BuildSevenDesc', + }, +}; From 6c1583bf8bc22c744fd080b421c2790f9e3a193b Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 15 Feb 2024 17:25:57 +1100 Subject: [PATCH 3/5] Made manager into a modal --- src/app/StorybookLayout.tsx | 4 ---- src/app/StorybookModal.tsx | 27 +++++++++++++++++++++++++++ src/app/layout.tsx | 6 ++++-- src/app/sb-manager/page.tsx | 17 ----------------- 4 files changed, 31 insertions(+), 23 deletions(-) delete mode 100644 src/app/StorybookLayout.tsx create mode 100644 src/app/StorybookModal.tsx delete mode 100644 src/app/sb-manager/page.tsx diff --git a/src/app/StorybookLayout.tsx b/src/app/StorybookLayout.tsx deleted file mode 100644 index 2cf12b9..0000000 --- a/src/app/StorybookLayout.tsx +++ /dev/null @@ -1,4 +0,0 @@ -export const StorybookLayout = ({ children }) => { - // TODO - render SB UI - return children; -}; diff --git a/src/app/StorybookModal.tsx b/src/app/StorybookModal.tsx new file mode 100644 index 0000000..f346d80 --- /dev/null +++ b/src/app/StorybookModal.tsx @@ -0,0 +1,27 @@ +import Link from 'next/link'; +import { storyIndex } from '../storyIndex'; + +export function StorybookModal({}) { + return ( +
+
    + {Object.entries(storyIndex).map(([id, { title, name }]) => ( +
  • + + {title}: {name} + +
  • + ))} +
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f0f46e5..bc3a630 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; -import { StorybookLayout } from './StorybookLayout'; +import { StorybookModal } from './StorybookModal'; // import "./globals.css"; const inter = Inter({ subsets: ['latin'] }); @@ -18,7 +18,9 @@ export default function RootLayout({ return ( - {children} + + + {children} ); diff --git a/src/app/sb-manager/page.tsx b/src/app/sb-manager/page.tsx deleted file mode 100644 index 1e7089f..0000000 --- a/src/app/sb-manager/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import Link from 'next/link'; -import { storyIndex } from '../../storyIndex'; - -export default function Page() { - return ( -
    - {Object.entries(storyIndex).map(([id, { title, name }]) => ( -
  • - - {title}: {name} - -
  • - ))} -
- ); -} From cc36d587371eac6a7aa6c347442efde6901dd857 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 15 Feb 2024 18:27:01 +1100 Subject: [PATCH 4/5] Add super basic "save story" functionality --- src/app/StorybookModal.tsx | 14 ++++++++++++ src/app/builds/[number]/page.tsx | 2 +- src/data/getBuild.ts | 8 +++++-- src/middleware.ts | 34 +++++++++++++++++++++++----- src/mock.ts | 38 ++++++++++++++++++++++++++------ 5 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/app/StorybookModal.tsx b/src/app/StorybookModal.tsx index f346d80..bdb7fd2 100644 --- a/src/app/StorybookModal.tsx +++ b/src/app/StorybookModal.tsx @@ -1,7 +1,14 @@ import Link from 'next/link'; import { storyIndex } from '../storyIndex'; +import { getLastRequestMockData } from '../mock'; export function StorybookModal({}) { + async function saveStory() { + 'use server'; + + console.log(getLastRequestMockData()); + } + return (
))} + +
  • +
    + Save current route + +
    +
  • ); diff --git a/src/app/builds/[number]/page.tsx b/src/app/builds/[number]/page.tsx index 804adb5..d6f3d04 100644 --- a/src/app/builds/[number]/page.tsx +++ b/src/app/builds/[number]/page.tsx @@ -14,7 +14,7 @@ export default async function Build({
             Build page, build number: {number}, sort: {sort} Build data:{' '}
    -        {JSON.stringify(await getBuild())}
    +        {JSON.stringify(await getBuild(number))}
           
    ); diff --git a/src/data/getBuild.ts b/src/data/getBuild.ts index d8974ab..ff09e0b 100644 --- a/src/data/getBuild.ts +++ b/src/data/getBuild.ts @@ -1,3 +1,7 @@ -export async function getBuild(id: number) { - throw new Error('Do some server stuff'); +export async function getBuild(number: number) { + return { + number, + name: `Build #${number}`, + status: 'PASSED', + }; } diff --git a/src/middleware.ts b/src/middleware.ts index 9bfb335..a465ef3 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -2,7 +2,10 @@ import { NextRequest, NextResponse } from 'next/server'; import { composeStory } from '@storybook/react'; import { storyIndex } from './storyIndex'; -export function middleware(request: NextRequest) { +const sessionIdCookieName = '__storybookSessionId__'; +const storyCookieName = '__storyId__'; + +export function storyMiddleware(request: NextRequest) { const storyId = request.url.split('/').at(-1); if (!storyId) throw new Error('no storyId'); @@ -27,10 +30,31 @@ export function middleware(request: NextRequest) { (args.$url.sort ? `?sort=${args.$url.sort}` : ''); const response = NextResponse.redirect(new URL(newUrl, request.url)); - response.cookies.set('__storyId__', storyId); + response.cookies.set(storyCookieName, storyId); return response; } -export const config = { - matcher: '/storybook-redirect/:path*', -}; +export function middleware(request: NextRequest) { + if (request.nextUrl.pathname.startsWith('/storybook-redirect/')) { + return storyMiddleware(request); + } + + const sessionCookie = request.cookies.get(sessionIdCookieName); + const sessionId = sessionCookie?.value || Math.random().toString(); + + // Clone the request headers and set a new header `x-hello-from-middleware1` + const requestHeaders = new Headers(request.headers); + requestHeaders.set(sessionIdCookieName, sessionId); + + const response = NextResponse.next({ + request: { + // New request headers + headers: requestHeaders, + }, + }); + + if (!sessionCookie) { + response.cookies.set(sessionIdCookieName, sessionId); + } + return response; +} diff --git a/src/mock.ts b/src/mock.ts index 9153814..3de1cb2 100644 --- a/src/mock.ts +++ b/src/mock.ts @@ -1,4 +1,4 @@ -import { cookies } from 'next/headers'; +import { cookies, headers } from 'next/headers'; // @ts-expect-error wrong react version import { cache } from 'react'; import { storyIndex } from './storyIndex'; @@ -24,20 +24,44 @@ const getStoryMockData = cache(() => { return args.$mock; }); -type Exports = Record; +type MockData = { [key: string]: any }; +const lastRequestMockDataPerSession: Record = {}; + +const getSessionId = () => { + const sessionId = headers().get('__storybookSessionId__'); + if (!sessionId) throw new Error('Unknown sessionId'); + + return sessionId; +}; + +export function getLastRequestMockData(): MockData | void { + return lastRequestMockDataPerSession[getSessionId()]; +} + +const getRequestMockData = cache(() => ({}) as MockData); +const setSessionMockDataKey = (key: string, data: any) => { + const sessionId = getSessionId(); + const requestMockData = getRequestMockData(); + requestMockData[key] = data; + lastRequestMockDataPerSession[sessionId] = requestMockData; +}; + +type Exports = Record; export function mockFn any>( original: TFunction, mockKey: string ) { - return (...args: Parameters) => { - const data = getStoryMockData(); + return async (...args: Parameters) => { + const mockData = getStoryMockData(); - if (data[mockKey]) { - return data[mockKey]; + if (mockData?.[mockKey]) { + return mockData[mockKey]; } - return original(...args); + const data = await original(...args); + setSessionMockDataKey(mockKey, data); + return data; }; } From 587935f3ef734eecccbf65bb64512e47329cbb20 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 15 Feb 2024 22:42:53 +1100 Subject: [PATCH 5/5] Have a go at writing story files --- src/app/StoryForm.tsx | 13 ++++++++ src/app/StorybookModal.tsx | 51 ++++++++++++++++++++++++++++---- src/app/builds/[number]/page.tsx | 1 - 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 src/app/StoryForm.tsx diff --git a/src/app/StoryForm.tsx b/src/app/StoryForm.tsx new file mode 100644 index 0000000..7a29d0d --- /dev/null +++ b/src/app/StoryForm.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { usePathname, useSearchParams } from 'next/navigation'; + +export const StoryForm = ({ saveStory }: { saveStory: (url: string) => Promise }) => { + const url = `${usePathname()}?${useSearchParams()}`; + return ( +
    + Save current route + +
    + ); +}; diff --git a/src/app/StorybookModal.tsx b/src/app/StorybookModal.tsx index bdb7fd2..3785ed0 100644 --- a/src/app/StorybookModal.tsx +++ b/src/app/StorybookModal.tsx @@ -1,12 +1,54 @@ import Link from 'next/link'; import { storyIndex } from '../storyIndex'; import { getLastRequestMockData } from '../mock'; +import { StoryForm } from './StoryForm'; +import { URL } from 'url'; +import { readFile, writeFile } from 'fs/promises'; export function StorybookModal({}) { - async function saveStory() { + async function saveStory(url: string) { 'use server'; - console.log(getLastRequestMockData()); + const $mock = getLastRequestMockData(); + + // TODO this code is obviously super custom + const { pathname, searchParams } = new URL(url, 'https://whatever.com'); + const sort = searchParams.get('sort'); + const $url = { + number: pathname.split('/').at(-1), + ...(sort && { sort }), + }; + + const title = 'Pages / Build'; + const name = 'New Build'; + const exportName = 'NewBuild'; + const id = 'pages-build--new-build'; + + const story = ` + export const ${exportName} = { + args: { + $url: ${JSON.stringify($url)}, + $mock: ${JSON.stringify($mock)}, + }, + }; + `; + + const csfFile = './src/page-stories/build.stories.ts'; + const csfContents = (await readFile(csfFile)).toString('utf-8'); + await writeFile(csfFile, `${csfContents}\n\n${story}`); + + const indexEntry = ` + '${id}': { + title: '${title}', + name: '${name}', + csf: stories, + key: '${exportName}', + }, + `; + + const indexFile = './src/storyIndex.ts'; + const indexContents = (await readFile(indexFile)).toString('utf8'); + await writeFile(indexFile, indexContents.replace('};', `${indexEntry}\n};`)); } return ( @@ -30,10 +72,7 @@ export function StorybookModal({}) { ))}
  • -
    - Save current route - -
    +
  • diff --git a/src/app/builds/[number]/page.tsx b/src/app/builds/[number]/page.tsx index d6f3d04..27c9a2b 100644 --- a/src/app/builds/[number]/page.tsx +++ b/src/app/builds/[number]/page.tsx @@ -9,7 +9,6 @@ export default async function Build({ params: { number: string }; searchParams: { [key: string]: string | string[] | undefined }; }) { - console.log('Build'); return (