diff --git a/apps/docs/src/components/example-block.tsx b/apps/docs/src/components/example-block.tsx index 8ffee6d9..efdca517 100644 --- a/apps/docs/src/components/example-block.tsx +++ b/apps/docs/src/components/example-block.tsx @@ -7,9 +7,9 @@ import { button } from '@luke-ui/react/recipes'; import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'; import type { ComponentType, JSX, ReactNode } from 'react'; import { Suspense, use, useId, useState } from 'react'; -import { encodeCodeHash } from '../lib/playground-hash'; -import { StoryWrapper } from '../lib/story-wrapper'; -import { DocsLink } from './docs-link'; +import { encodeCodeHash } from '../lib/playground-hash.js'; +import { StoryWrapper } from '../lib/story-wrapper.js'; +import { DocsLink } from './docs-link.js'; type ExampleBlockProps = { src: string; @@ -25,11 +25,6 @@ export function ExampleBlock(props: ExampleBlockProps): JSX.Element { ); } -const _modules = import.meta.glob('../examples/*/*.tsx', { - eager: false, - import: 'default', -}); - function ExampleContent({ mode, src, title }: ExampleBlockProps): JSX.Element { const slashIndex = src.indexOf('/'); const component = src.slice(0, slashIndex); @@ -47,6 +42,7 @@ function ExampleContent({ mode, src, title }: ExampleBlockProps): JSX.Element { } const [PreviewComponent, source] = result.data; + const code = source.trim(); return ( @@ -81,7 +77,7 @@ function ExampleContent({ mode, src, title }: ExampleBlockProps): JSX.Element { {showCode ? ( @@ -153,12 +149,19 @@ function ExampleFrame({ actions, ariaLabel, children, title }: ExampleFrameProps ); } +const _modules = import.meta.glob('../examples/*/*.tsx', { + eager: false, + import: 'default', +}); + const _sources = import.meta.glob('../examples/*/*.tsx', { eager: false, import: 'default', query: '?raw', }); +type ExampleLoader = [() => Promise, () => Promise]; + type ExampleTuple = [ComponentType, string]; type ExampleResult = @@ -173,10 +176,7 @@ type ExampleResult = const exampleCache = new Map>(); -function findExample( - component: string, - name: string, -): [() => Promise, () => Promise] | null { +function findExample(component: string, name: string): ExampleLoader | null { const key = `../examples/${component}/${name}.tsx`; const loadModule = _modules[key]; const loadSource = _sources[key]; @@ -194,7 +194,7 @@ function loadExample(component: string, name: string): Promise { const match = findExample(component, name); if (!match) { const promise = Promise.resolve({ - error: new Error(`Example not found: ${component}/${name}`), + error: new Error(`Example not found: ${key}`), ok: false, } satisfies ExampleResult); exampleCache.set(key, promise); diff --git a/apps/docs/src/lib/playground-hash.test.ts b/apps/docs/src/lib/playground-hash.test.ts new file mode 100644 index 00000000..a83a2158 --- /dev/null +++ b/apps/docs/src/lib/playground-hash.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from 'vite-plus/test'; +import { decodeCodeHash, encodeCodeHash } from './playground-hash.js'; + +describe('playground hash helpers', () => { + test('round-trips code through the serialized hash', () => { + const code = 'const demo = 1;'; + const hash = encodeCodeHash(code); + + expect(decodeCodeHash(hash)).toBe(code); + expect(decodeCodeHash(`#${hash}`)).toBe(code); + }); +}); diff --git a/apps/docs/src/lib/playground-hash.ts b/apps/docs/src/lib/playground-hash.ts index 55094693..6dc94fee 100644 --- a/apps/docs/src/lib/playground-hash.ts +++ b/apps/docs/src/lib/playground-hash.ts @@ -5,22 +5,33 @@ import { encodeShape } from './playground-shape'; const { compressToEncodedURIComponent, decompressFromEncodedURIComponent } = lzString; -const CODE_HASH_PREFIX = 'code='; -const SHAPE_PARAM_PREFIX = 'shape='; +const CODE_HASH_PARAM = 'code'; +const SHAPE_PARAM = 'shape'; + +function getHashFragment(hash: string): string { + const value = hash.trim(); + if (!value) return ''; + if (value.startsWith('#')) return value.slice(1); + + return value; +} /** Returns the URL hash value (without the leading `#`) encoding the given code. */ export function encodeCodeHash(code: string): string { - const compressed = compressToEncodedURIComponent(code); - return `${CODE_HASH_PREFIX}${compressed}&${SHAPE_PARAM_PREFIX}${encodeShape(code)}`; + const params = new URLSearchParams({ + [CODE_HASH_PARAM]: compressToEncodedURIComponent(code), + [SHAPE_PARAM]: encodeShape(code), + }); + + return params.toString(); } /** Decodes playground code from a URL hash (with or without the leading `#`). */ export function decodeCodeHash(hash: string): string | null { - const value = hash.startsWith('#') ? hash.slice(1) : hash; - if (!value.startsWith(CODE_HASH_PREFIX)) return null; - const payload = value.slice(CODE_HASH_PREFIX.length); - const paramsStart = payload.indexOf('&'); - const compressed = paramsStart === -1 ? payload : payload.slice(0, paramsStart); + const params = new URLSearchParams(getHashFragment(hash)); + const compressed = params.get(CODE_HASH_PARAM); + if (!compressed) return null; + const code = decompressFromEncodedURIComponent(compressed); return code || null; }