Skip to content

Commit 33ef2e7

Browse files
authored
fix(typegpu-docs): Cross file import types (#1142)
1 parent 6fa173b commit 33ef2e7

File tree

7 files changed

+104
-107
lines changed

7 files changed

+104
-107
lines changed

apps/typegpu-docs/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
2222
"@radix-ui/react-slider": "^1.2.0",
2323
"@stackblitz/sdk": "^1.11.0",
2424
"@std/yaml": "npm:@jsr/std__yaml@^1.0.5",
25-
"@typegpu/noise": "workspace:*",
2625
"@typegpu/color": "workspace:*",
26+
"@typegpu/noise": "workspace:*",
2727
"@types/dom-mediacapture-transform": "^0.1.9",
2828
"@types/react": "^19.0.10",
2929
"@types/react-dom": "^19.0.4",
30-
"astro": "^5.3.1",
3130
"arktype": "catalog:",
31+
"astro": "^5.3.1",
3232
"classnames": "^2.5.1",
3333
"expressive-code-twoslash": "^0.4.0",
3434
"jotai": "^2.8.4",
@@ -37,6 +37,7 @@
3737
"lz-string": "^1.5.0",
3838
"monaco-editor": "^0.52.2",
3939
"motion": "^12.4.5",
40+
"pathe": "^2.0.3",
4041
"react": "^19.0.0",
4142
"react-dom": "^19.0.0",
4243
"remeda": "^2.21.2",

apps/typegpu-docs/src/components/CodeEditor.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Editor, {
77
import type { editor } from 'monaco-editor';
88
import { entries, filter, fromEntries, isTruthy, map, pipe } from 'remeda';
99
import { SANDBOX_MODULES } from '../utils/examples/sandboxModules';
10+
import type { ExampleSrcFile } from '../utils/examples/types';
1011
import { tsCompilerOptions } from '../utils/liveEditor/embeddedTypeScript';
1112

1213
function handleEditorWillMount(monaco: Monaco) {
@@ -56,7 +57,7 @@ function handleEditorOnMount(editor: editor.IStandaloneCodeEditor) {
5657
}
5758

5859
type Props = {
59-
code: string;
60+
file: ExampleSrcFile;
6061
shown: boolean;
6162
};
6263

@@ -67,7 +68,7 @@ const createCodeEditorComponent =
6768
onMount?: OnMount,
6869
) =>
6970
(props: Props) => {
70-
const { code, shown } = props;
71+
const { file, shown } = props;
7172

7273
return (
7374
<div
@@ -77,7 +78,8 @@ const createCodeEditorComponent =
7778
>
7879
<Editor
7980
defaultLanguage={language}
80-
value={code}
81+
value={file.content}
82+
path={file.path}
8183
beforeMount={beforeMount}
8284
onMount={onMount}
8385
options={{

apps/typegpu-docs/src/components/ExampleView.tsx

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ type Props = {
2323

2424
function useExample(
2525
tsImport: () => Promise<unknown>,
26-
htmlCode: string,
2726
setSnackbarText: (text: string | undefined) => void,
2827
) {
2928
const exampleRef = useRef<ExampleState | null>(null);
@@ -62,35 +61,35 @@ function useExample(
6261
exampleRef.current?.dispose();
6362
cancelled = true;
6463
};
65-
}, [htmlCode, setSnackbarText, setExampleControlParams]);
64+
}, [setSnackbarText, setExampleControlParams]);
6665
}
6766

6867
export function ExampleView({ example }: Props) {
69-
const { tsCodes, tsImport, htmlCode } = example;
68+
const { tsFiles, tsImport, htmlFile } = example;
7069

7170
const [snackbarText, setSnackbarText] = useAtom(currentSnackbarAtom);
72-
const [currentFile, setCurrentFile] = useState<string>('index.ts');
71+
const [currentFilePath, setCurrentFilePath] = useState<string>('index.ts');
7372

7473
const codeEditorShowing = useAtomValue(codeEditorShownAtom);
7574
const codeEditorMobileShowing = useAtomValue(codeEditorShownMobileAtom);
7675
const exampleHtmlRef = useRef<HTMLDivElement>(null);
7776

78-
const codeFiles = Object.keys(tsCodes);
77+
const filePaths = tsFiles.map((file) => file.path);
7978
const editorTabsList = [
8079
'index.ts',
81-
...codeFiles.filter((name) => name !== 'index.ts'),
80+
...filePaths.filter((name) => name !== 'index.ts'),
8281
'index.html',
8382
];
8483

8584
useEffect(() => {
8685
if (!exampleHtmlRef.current) {
8786
return;
8887
}
89-
exampleHtmlRef.current.innerHTML = htmlCode;
90-
}, [htmlCode]);
88+
exampleHtmlRef.current.innerHTML = htmlFile.content;
89+
}, [htmlFile]);
9190

92-
useExample(tsImport, htmlCode, setSnackbarText); // live example
93-
useResizableCanvas(exampleHtmlRef, htmlCode);
91+
useExample(tsImport, setSnackbarText); // live example
92+
useResizableCanvas(exampleHtmlRef);
9493

9594
return (
9695
<>
@@ -140,10 +139,10 @@ export function ExampleView({ example }: Props) {
140139
<button
141140
key={fileName}
142141
type="button"
143-
onClick={() => setCurrentFile(fileName)}
142+
onClick={() => setCurrentFilePath(fileName)}
144143
className={cs(
145144
'px-4 rounded-t-lg rounded-b-none text-nowrap',
146-
currentFile === fileName
145+
currentFilePath === fileName
147146
? 'bg-gradient-to-br from-gradient-purple to-gradient-blue text-white hover:from-gradient-purple-dark hover:to-gradient-blue-dark'
148147
: 'bg-white border-tameplum-100 border-2 hover:bg-tameplum-20',
149148
)}
@@ -155,15 +154,15 @@ export function ExampleView({ example }: Props) {
155154
</div>
156155

157156
<HtmlCodeEditor
158-
shown={currentFile === 'index.html'}
159-
code={htmlCode}
157+
shown={currentFilePath === 'index.html'}
158+
file={htmlFile}
160159
/>
161160

162-
{Object.entries(tsCodes).map(([key, value]) => (
161+
{tsFiles.map((file) => (
163162
<TsCodeEditor
164-
shown={key === currentFile}
165-
code={value}
166-
key={key}
163+
key={file.path}
164+
shown={file.path === currentFilePath}
165+
file={file}
167166
/>
168167
))}
169168
</div>
@@ -194,11 +193,7 @@ function GPUUnsupportedPanel() {
194193
);
195194
}
196195

197-
function useResizableCanvas(
198-
exampleHtmlRef: RefObject<HTMLDivElement | null>,
199-
htmlCode: string,
200-
) {
201-
// biome-ignore lint/correctness/useExhaustiveDependencies: should be run on every htmlCode and tsCode change
196+
function useResizableCanvas(exampleHtmlRef: RefObject<HTMLDivElement | null>) {
202197
useEffect(() => {
203198
const canvases = exampleHtmlRef.current?.querySelectorAll('canvas') as
204199
| HTMLCanvasElement[]
@@ -261,5 +256,5 @@ function useResizableCanvas(
261256
observer.disconnect();
262257
}
263258
};
264-
}, [exampleHtmlRef, htmlCode]);
259+
}, [exampleHtmlRef]);
265260
}

apps/typegpu-docs/src/components/stackblitz/openInStackBlitz.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ if (pnpmWorkspaceYaml instanceof type.errors) {
1919
}
2020

2121
export const openInStackBlitz = (example: Example) => {
22-
const tsFiles = Object.entries(example.tsCodes).reduce(
23-
(acc, [fileName, code]) => {
24-
acc[`src/${fileName}`] = code.replaceAll(
22+
const tsFiles = example.tsFiles.reduce(
23+
(acc, file) => {
24+
acc[`src/${file.path}`] = file.content.replaceAll(
2525
'/TypeGPU',
2626
'https://docs.swmansion.com/TypeGPU',
2727
);
@@ -46,7 +46,7 @@ export const openInStackBlitz = (example: Example) => {
4646
<title>${example.metadata.title}</title>
4747
</head>
4848
<body>
49-
${example.htmlCode}
49+
${example.htmlFile.content}
5050
<script type="module" src="/index.ts"></script>
5151
</body>
5252
</html>`,

apps/typegpu-docs/src/utils/examples/exampleContent.ts

Lines changed: 62 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,105 @@
1-
import { entries, filter, fromEntries, groupBy, map, pipe } from 'remeda';
2-
import type { Example, ExampleMetadata } from './types';
1+
import pathe from 'pathe';
2+
import * as R from 'remeda';
3+
import type { Example, ExampleMetadata, ExampleSrcFile } from './types';
34

4-
function pathPipe(path: string): string {
5-
return pipe(
5+
const contentExamplesPath = '../../content/examples/';
6+
7+
function pathToExampleKey(path: string): string {
8+
return R.pipe(
69
path,
7-
(p) => p.replace(/^..\/..\/content\/examples\//, ''), // removing parent folder
8-
(p) => p.replace(/\/[^\/]*$/, ''), // removing leaf file names (e.g. meta.json, index.ts)
9-
(p) => p.replace(/\//, '--'), // replacing path separators with '--'
10+
(p) => pathe.relative(contentExamplesPath, p), // removing parent folder
11+
(p) => p.split('/'), // splitting into segments
12+
([category, name]) => `${category}--${name}`,
1013
);
1114
}
1215

13-
function pathToExampleKey<T>(record: Record<string, T>): Record<string, T> {
14-
return pipe(
16+
function globToExampleFiles(
17+
record: Record<string, string>,
18+
): Record<string, ExampleSrcFile[]> {
19+
return R.pipe(
1520
record,
16-
entries(),
17-
map(([path, value]) => [pathPipe(path), value] as const),
18-
fromEntries(),
19-
);
20-
}
21-
22-
function pathToExampleFilesMap<T>(
23-
record: Record<string, T>,
24-
): Record<string, Record<string, T>> {
25-
const groups: Record<string, Record<string, T>> = {};
21+
R.mapValues((content, key): ExampleSrcFile => {
22+
const pathRelToExamples = pathe.relative(contentExamplesPath, key);
23+
const categoryDir = pathRelToExamples.split('/')[0];
24+
const exampleDir = pathRelToExamples.split('/')[1];
25+
const examplePath = pathe.join(
26+
contentExamplesPath,
27+
categoryDir,
28+
exampleDir,
29+
);
2630

27-
for (const [path, value] of Object.entries(record)) {
28-
const groupKey = pathPipe(path);
29-
30-
const fileNameMatch = path.match(/\/([^\/]+\.ts)$/);
31-
const fileName = fileNameMatch ? fileNameMatch[1] : path;
32-
33-
if (!groups[groupKey]) {
34-
groups[groupKey] = {};
35-
}
36-
groups[groupKey][fileName] = value;
37-
}
38-
return groups;
31+
return {
32+
exampleKey: pathToExampleKey(key),
33+
path: pathe.relative(examplePath, key),
34+
content,
35+
};
36+
}),
37+
R.values(),
38+
R.groupBy(R.prop('exampleKey')),
39+
);
3940
}
4041

41-
const metaFiles: Record<string, ExampleMetadata> = pathToExampleKey(
42+
const metaFiles = R.pipe(
4243
import.meta.glob('../../content/examples/**/meta.json', {
4344
eager: true,
4445
import: 'default',
45-
}),
46+
}) as Record<string, ExampleMetadata>,
47+
R.mapKeys(pathToExampleKey),
4648
);
4749

48-
const readonlyTsFiles: Record<
49-
string,
50-
Record<string, string>
51-
> = pathToExampleFilesMap(
50+
const readonlyTsFiles = R.pipe(
5251
import.meta.glob('../../content/examples/**/*.ts', {
5352
query: 'raw',
5453
eager: true,
5554
import: 'default',
56-
}),
55+
}) as Record<string, string>,
56+
globToExampleFiles,
5757
);
5858

59-
const tsFilesImportFunctions: Record<string, () => Promise<unknown>> = pipe(
60-
import.meta.glob('../../content/examples/**/index.ts'),
61-
entries(),
62-
map(
63-
([key, value]) =>
64-
[pathPipe(key), value] satisfies [string, () => Promise<unknown>],
65-
),
66-
fromEntries(),
59+
const tsFilesImportFunctions = R.pipe(
60+
import.meta.glob('../../content/examples/**/index.ts') as Record<
61+
string,
62+
() => Promise<unknown>
63+
>,
64+
R.mapKeys(pathToExampleKey),
6765
);
6866

69-
const htmlFiles: Record<string, string> = pathToExampleKey(
67+
const htmlFiles = R.pipe(
7068
import.meta.glob('../../content/examples/**/index.html', {
7169
query: 'raw',
7270
eager: true,
7371
import: 'default',
74-
}),
72+
}) as Record<string, string>,
73+
globToExampleFiles,
7574
);
7675

77-
export const examples = pipe(
76+
export const examples = R.pipe(
7877
metaFiles,
79-
entries(),
80-
map(
81-
([key, value]) =>
82-
[
78+
R.mapValues(
79+
(value, key) =>
80+
({
8381
key,
84-
{
85-
key,
86-
metadata: value,
87-
tsCodes: readonlyTsFiles[key] ?? {},
88-
tsImport: tsFilesImportFunctions[key],
89-
htmlCode: htmlFiles[key] ?? '',
90-
},
91-
] satisfies [string, Example],
82+
metadata: value,
83+
tsFiles: readonlyTsFiles[key] ?? [],
84+
tsImport: tsFilesImportFunctions[key],
85+
htmlFile: htmlFiles[key][0] ?? '',
86+
}) satisfies Example,
9287
),
93-
fromEntries(),
9488
);
9589

96-
export const examplesStable = pipe(
90+
export const examplesStable = R.pipe(
9791
examples,
98-
entries(),
99-
filter(([_, example]) => !example.metadata.tags?.includes('experimental')),
100-
filter(([_, example]) =>
92+
R.entries(),
93+
R.filter(([_, example]) => !example.metadata.tags?.includes('experimental')),
94+
R.filter(([_, example]) =>
10195
example.metadata.tags?.includes('camera')
10296
? typeof MediaStreamTrackProcessor === 'undefined'
10397
: true,
10498
),
105-
fromEntries(),
99+
R.fromEntries(),
106100
);
107101

108-
export const examplesByCategory = groupBy(
102+
export const examplesByCategory = R.groupBy(
109103
Object.values(examples),
110104
(example) => example.metadata.category,
111105
);

apps/typegpu-docs/src/utils/examples/types.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ export const exampleCategories = [
1616
{ key: 'tests', label: 'Tests' },
1717
];
1818

19+
export type ExampleSrcFile = {
20+
exampleKey: string;
21+
path: string;
22+
content: string;
23+
};
24+
1925
export type Example = {
2026
key: string;
21-
tsCodes: Record<string, string>;
27+
tsFiles: ExampleSrcFile[];
2228
tsImport: () => Promise<unknown>;
23-
htmlCode: string;
29+
htmlFile: ExampleSrcFile;
2430
metadata: ExampleMetadata;
2531
};
26-
27-
export type Module = {
28-
default: string;
29-
};

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)