diff --git a/apps/typegpu-docs/src/components/CodeEditor.tsx b/apps/typegpu-docs/src/components/CodeEditor.tsx index 90ab90aa57..ac61628042 100644 --- a/apps/typegpu-docs/src/components/CodeEditor.tsx +++ b/apps/typegpu-docs/src/components/CodeEditor.tsx @@ -4,49 +4,57 @@ import Editor, { type OnMount, } from '@monaco-editor/react'; import type { editor } from 'monaco-editor'; +import { getDefaultStore } from 'jotai'; import { entries, filter, fromEntries, isTruthy, map, pipe } from 'remeda'; -import { SANDBOX_MODULES } from '../utils/examples/sandboxModules.ts'; +import { sandboxModulesAtom } from '../utils/examples/sandboxModules.ts'; import type { ExampleSrcFile } from '../utils/examples/types.ts'; import { tsCompilerOptions } from '../utils/liveEditor/embeddedTypeScript.ts'; function handleEditorWillMount(monaco: Monaco) { + // NOTE: This is not recommended, as the default store is not always the one you want, + // but in this particular case, it's totally fine. + const store = getDefaultStore(); const tsDefaults = monaco?.languages.typescript.typescriptDefaults; - const reroutes = pipe( - entries(SANDBOX_MODULES), - map(([key, moduleDef]) => { - if ('reroute' in moduleDef.typeDef) { - return [key, [moduleDef.typeDef.reroute]] as [string, string[]]; - } - return null; - }), - filter(isTruthy), - fromEntries(), - ); + (async () => { + const SANDBOX_MODULES = await store.get(sandboxModulesAtom); - for (const [moduleKey, moduleDef] of entries(SANDBOX_MODULES)) { - if ('content' in moduleDef.typeDef) { - tsDefaults.addExtraLib( - moduleDef.typeDef.content, - moduleDef.typeDef.filename, - ); + const reroutes = pipe( + entries(SANDBOX_MODULES), + map(([key, moduleDef]) => { + if ('reroute' in moduleDef.typeDef) { + return [key, [moduleDef.typeDef.reroute]] as [string, string[]]; + } + return null; + }), + filter(isTruthy), + fromEntries(), + ); - if ( - moduleDef.typeDef.filename && - moduleDef.typeDef.filename !== moduleKey // the redirect is not a no-op - ) { - reroutes[moduleKey] = [ - ...(reroutes[moduleKey] ?? []), + for (const [moduleKey, moduleDef] of entries(SANDBOX_MODULES)) { + if ('content' in moduleDef.typeDef) { + tsDefaults.addExtraLib( + moduleDef.typeDef.content, moduleDef.typeDef.filename, - ]; + ); + + if ( + moduleDef.typeDef.filename && + moduleDef.typeDef.filename !== moduleKey // the redirect is not a no-op + ) { + reroutes[moduleKey] = [ + ...(reroutes[moduleKey] ?? []), + moduleDef.typeDef.filename, + ]; + } } } - } - tsDefaults.setCompilerOptions({ - ...tsCompilerOptions, - paths: reroutes, - }); + tsDefaults.setCompilerOptions({ + ...tsCompilerOptions, + paths: reroutes, + }); + })(); } function handleEditorOnMount(editor: editor.IStandaloneCodeEditor) { diff --git a/apps/typegpu-docs/src/components/ExampleView.tsx b/apps/typegpu-docs/src/components/ExampleView.tsx index bc3611bdcc..5a7550429d 100644 --- a/apps/typegpu-docs/src/components/ExampleView.tsx +++ b/apps/typegpu-docs/src/components/ExampleView.tsx @@ -67,7 +67,8 @@ function useExample( } export function ExampleView({ example }: Props) { - const { tsFiles, tsImport, htmlFile } = example; + const { tsImport, contentAtom } = example; + const { htmlFile, tsFiles } = useAtomValue(contentAtom); const [snackbarText, setSnackbarText] = useAtom(currentSnackbarAtom); const [currentFilePath, setCurrentFilePath] = useState('index.ts'); @@ -88,7 +89,7 @@ export function ExampleView({ example }: Props) { return; } exampleHtmlRef.current.innerHTML = htmlFile.content; - }, [htmlFile]); + }, [htmlFile.content]); useExample(tsImport, setSnackbarText); // live example useResizableCanvas(exampleHtmlRef); diff --git a/apps/typegpu-docs/src/components/translator/lib/editorConfig.ts b/apps/typegpu-docs/src/components/translator/lib/editorConfig.ts index 90a8c54646..52929b8500 100644 --- a/apps/typegpu-docs/src/components/translator/lib/editorConfig.ts +++ b/apps/typegpu-docs/src/components/translator/lib/editorConfig.ts @@ -1,7 +1,8 @@ import { entries, filter, fromEntries, isTruthy, map, pipe } from 'remeda'; import type { Monaco } from '@monaco-editor/react'; -import { SANDBOX_MODULES } from '../../../utils/examples/sandboxModules.ts'; +import { sandboxModulesAtom } from '../../../utils/examples/sandboxModules.ts'; import { tsCompilerOptions } from '../../../utils/liveEditor/embeddedTypeScript.ts'; +import { getDefaultStore } from 'jotai'; export const LANGUAGE_MAP: Record = { wgsl: 'wgsl', @@ -38,41 +39,48 @@ export const readOnlyEditorOptions = { }; export function setupMonacoEditor(monaco: Monaco) { + // NOTE: This is not recommended, as the default store is not always the one you want, + // but in this particular case, it's totally fine. + const store = getDefaultStore(); const tsDefaults = monaco?.languages.typescript.typescriptDefaults; - const reroutes = pipe( - entries(SANDBOX_MODULES), - map(([key, moduleDef]) => { - if ('reroute' in moduleDef.typeDef) { - return [key, [moduleDef.typeDef.reroute]] as [string, string[]]; - } - return null; - }), - filter(isTruthy), - fromEntries(), - ); + (async () => { + const SANDBOX_MODULES = await store.get(sandboxModulesAtom); - for (const [moduleKey, moduleDef] of entries(SANDBOX_MODULES)) { - if ('content' in moduleDef.typeDef) { - tsDefaults.addExtraLib( - moduleDef.typeDef.content, - moduleDef.typeDef.filename, - ); + const reroutes = pipe( + entries(SANDBOX_MODULES), + map(([key, moduleDef]) => { + if ('reroute' in moduleDef.typeDef) { + return [key, [moduleDef.typeDef.reroute]] as [string, string[]]; + } + return null; + }), + filter(isTruthy), + fromEntries(), + ); - if ( - moduleDef.typeDef.filename && - moduleDef.typeDef.filename !== moduleKey - ) { - reroutes[moduleKey] = [ - ...(reroutes[moduleKey] ?? []), + for (const [moduleKey, moduleDef] of entries(SANDBOX_MODULES)) { + if ('content' in moduleDef.typeDef) { + tsDefaults.addExtraLib( + moduleDef.typeDef.content, moduleDef.typeDef.filename, - ]; + ); + + if ( + moduleDef.typeDef.filename && + moduleDef.typeDef.filename !== moduleKey + ) { + reroutes[moduleKey] = [ + ...(reroutes[moduleKey] ?? []), + moduleDef.typeDef.filename, + ]; + } } } - } - tsDefaults.setCompilerOptions({ - ...tsCompilerOptions, - paths: reroutes, + tsDefaults.setCompilerOptions({ + ...tsCompilerOptions, + paths: reroutes, + }); }); } diff --git a/apps/typegpu-docs/src/examples/exampleContent.ts b/apps/typegpu-docs/src/examples/exampleContent.ts index 058dc998ba..f1d90b8dc6 100644 --- a/apps/typegpu-docs/src/examples/exampleContent.ts +++ b/apps/typegpu-docs/src/examples/exampleContent.ts @@ -1,5 +1,6 @@ import pathe from 'pathe'; import * as R from 'remeda'; +import { atom } from 'jotai'; import type { Example, ExampleMetadata, @@ -46,20 +47,27 @@ function pathToExampleKey(path: string): string { } function globToExampleFiles( - record: Record, -): Record { + record: Record Promise>, +): Record< + string, + ({ exampleKey: string; file: () => Promise })[] +> { return R.pipe( record, - R.mapValues((content, key): ExampleSrcFile => { + R.mapValues((content, key) => { const pathRelToExamples = pathe.relative('./', key); const categoryDir = pathRelToExamples.split('/')[0]; const exampleDir = pathRelToExamples.split('/')[1]; const examplePath = pathe.join(categoryDir, exampleDir); + const exampleKey = pathToExampleKey(key); return { - exampleKey: pathToExampleKey(key), - path: pathe.relative(examplePath, key), - content, + exampleKey, + file: async () => ({ + exampleKey, + path: pathe.relative(examplePath, key), + content: await content(), + }), }; }), R.values(), @@ -76,37 +84,32 @@ const metaFiles = R.pipe( ); const readonlyTsFiles = R.pipe( - import.meta.glob('./**/*.ts', { + import.meta.glob('./**/*.ts', { query: 'raw', - eager: true, import: 'default', - }) as Record, + }), globToExampleFiles, ); const tsFilesImportFunctions = R.pipe( - import.meta.glob('./**/index.ts') as Record< - string, - () => Promise - >, + import.meta.glob('./**/index.ts'), R.mapKeys(pathToExampleKey), ); const htmlFiles = R.pipe( - import.meta.glob('./**/index.html', { + import.meta.glob('./**/index.html', { query: 'raw', - eager: true, import: 'default', - }) as Record, + }), globToExampleFiles, ); const thumbnailFiles = R.pipe( - import.meta.glob('./**/thumbnail.png', { + import.meta.glob('./**/thumbnail.png', { eager: true, import: 'default', query: 'w=512;1024', - }) as Record, + }), R.mapKeys(pathToExampleKey), R.mapValues(( value, @@ -127,9 +130,18 @@ export const examples = R.pipe( ({ key, metadata: value, - tsFiles: readonlyTsFiles[key] ?? [], + contentAtom: atom(async () => { + const [htmlFile, ...tsFiles] = await Promise.all([ + htmlFiles[key]?.[0].file(), + ...readonlyTsFiles[key].map((file) => file.file()), + ]); + + return { + htmlFile, + tsFiles, + }; + }), tsImport: () => noCacheImport(tsFilesImportFunctions[key]), - htmlFile: htmlFiles[key]?.[0] ?? '', thumbnails: thumbnailFiles[key], }) satisfies Example ), diff --git a/apps/typegpu-docs/src/utils/examples/sandboxModules.ts b/apps/typegpu-docs/src/utils/examples/sandboxModules.ts index a87d10e72c..8023515c79 100644 --- a/apps/typegpu-docs/src/utils/examples/sandboxModules.ts +++ b/apps/typegpu-docs/src/utils/examples/sandboxModules.ts @@ -1,7 +1,4 @@ -import { entries, fromEntries, map, pipe } from 'remeda'; - -import dtsWebGPU from '@webgpu/types/dist/index.d.ts?raw'; -import dtsWgpuMatrix from 'wgpu-matrix/dist/3.x/wgpu-matrix.d.ts?raw'; +import { atom } from 'jotai'; interface SandboxModuleDefinition { typeDef: @@ -13,14 +10,14 @@ interface SandboxModuleDefinition { | undefined; } -function srcFileToModule( - [filepath, content]: [string, string], +async function srcFileToModule( + [filepath, sourceImport]: [string, () => Promise], baseUrl: string, -): [moduleKey: string, moduleDef: SandboxModuleDefinition] { +): Promise<[moduleKey: string, moduleDef: SandboxModuleDefinition]> { const filename = filepath.replace(baseUrl, ''); const def = { filename, - content, + content: await sourceImport(), }; return [ @@ -32,10 +29,10 @@ function srcFileToModule( ] as const; } -function dtsFileToModule( - [filepath, content]: [string, string], +async function dtsFileToModule( + [filepath, sourceImport]: [string, () => Promise], baseUrl: string, -): [moduleKey: string, moduleDef: SandboxModuleDefinition] { +): Promise<[moduleKey: string, moduleDef: SandboxModuleDefinition]> { const filename = filepath.replace(baseUrl, ''); return [ @@ -43,106 +40,119 @@ function dtsFileToModule( { typeDef: { filename, - content, + content: await sourceImport(), }, }, ] as const; } -const allPackagesSrcFiles = pipe( - entries( - import.meta.glob([ - '../../../../../packages/*/src/**/*.ts', - '../../../../../packages/*/package.json', - ], { - query: 'raw', - eager: true, - import: 'default', - }) as Record, - ), - map((dtsFile) => srcFileToModule(dtsFile, '../../../../../packages/')), - fromEntries(), -); +const allPackagesSrcImports = import.meta.glob([ + '../../../../../packages/*/src/**/*.ts', + '../../../../../packages/*/package.json', +], { + query: 'raw', + import: 'default', +}); -const threeModules = pipe( - entries( - import.meta.glob( - '../../../node_modules/@types/three/**/*.d.ts', - { - query: 'raw', - eager: true, - import: 'default', - }, - ) as Record, - ), - map((dtsFile) => dtsFileToModule(dtsFile, '../../../node_modules/')), - fromEntries(), +const threeImports = import.meta.glob( + '../../../node_modules/@types/three/**/*.d.ts', + { + query: 'raw', + import: 'default', + }, ); -const mediacaptureModules = pipe( - entries( - import.meta.glob( - '../../../node_modules/@types/dom-mediacapture-transform/**/*.d.ts', - { - query: 'raw', - eager: true, - import: 'default', - }, - ) as Record, - ), - map((dtsFile) => dtsFileToModule(dtsFile, '../../../node_modules/')), - fromEntries(), +const mediacaptureImports = import.meta.glob( + '../../../node_modules/@types/dom-mediacapture-transform/**/*.d.ts', + { + query: 'raw', + import: 'default', + }, ); -export const SANDBOX_MODULES: Record = { - ...allPackagesSrcFiles, - ...threeModules, - ...mediacaptureModules, +// Using an async atom so that the async computation is cached +export const sandboxModulesAtom = atom(async () => { + const [dtsWebGPU, dtsWgpuMatrix] = await Promise.all([ + import('@webgpu/types/dist/index.d.ts?raw'), + import('wgpu-matrix/dist/3.x/wgpu-matrix.d.ts?raw'), + ]); - '@webgpu/types': { - typeDef: { content: dtsWebGPU }, - }, - 'wgpu-matrix': { - typeDef: { filename: 'wgpu-matrix.d.ts', content: dtsWgpuMatrix }, - }, - tinyest: { - import: { reroute: 'tinyest/src/index.ts' }, - typeDef: { reroute: 'tinyest/src/index.ts' }, - }, - typegpu: { - import: { reroute: 'typegpu/src/index.ts' }, - typeDef: { reroute: 'typegpu/src/index.ts' }, - }, - 'typegpu/data': { - import: { reroute: 'typegpu/src/data/index.ts' }, - typeDef: { reroute: 'typegpu/src/data/index.ts' }, - }, - 'typegpu/std': { - import: { reroute: 'typegpu/src/std/index.ts' }, - typeDef: { reroute: 'typegpu/src/std/index.ts' }, - }, + const allPackagesSrcFiles = Object.fromEntries( + await Promise.all( + Object.entries(allPackagesSrcImports).map((dtsFile) => + srcFileToModule(dtsFile, '../../../../../packages/') + ), + ), + ); - // Three.js, for examples of @typegpu/three - 'three': { - typeDef: { reroute: '@types/three/build/three.module.d.ts' }, - }, - 'three/webgpu': { - typeDef: { reroute: '@types/three/build/three.webgpu.d.ts' }, - }, - 'three/tsl': { - typeDef: { reroute: '@types/three/build/three.tsl.d.ts' }, - }, + const threeModules = Object.fromEntries( + await Promise.all( + Object.entries(threeImports).map((dtsFile) => + dtsFileToModule(dtsFile, '../../../node_modules/') + ), + ), + ); - // Utility modules - '@typegpu/noise': { - import: { reroute: 'typegpu-noise/src/index.ts' }, - typeDef: { reroute: 'typegpu-noise/src/index.ts' }, - }, - '@typegpu/color': { - import: { reroute: 'typegpu-color/src/index.ts' }, - typeDef: { reroute: 'typegpu-color/src/index.ts' }, - }, - '@typegpu/three': { - typeDef: { reroute: 'typegpu-three/src/index.ts' }, - }, -}; + const mediacaptureModules = Object.fromEntries( + await Promise.all( + Object.entries(mediacaptureImports).map((dtsFile) => + dtsFileToModule(dtsFile, '../../../node_modules/') + ), + ), + ); + + const SANDBOX_MODULES: Record = { + ...allPackagesSrcFiles, + ...threeModules, + ...mediacaptureModules, + + '@webgpu/types': { + typeDef: { content: dtsWebGPU.default }, + }, + 'wgpu-matrix': { + typeDef: { filename: 'wgpu-matrix.d.ts', content: dtsWgpuMatrix.default }, + }, + tinyest: { + import: { reroute: 'tinyest/src/index.ts' }, + typeDef: { reroute: 'tinyest/src/index.ts' }, + }, + typegpu: { + import: { reroute: 'typegpu/src/index.ts' }, + typeDef: { reroute: 'typegpu/src/index.ts' }, + }, + 'typegpu/data': { + import: { reroute: 'typegpu/src/data/index.ts' }, + typeDef: { reroute: 'typegpu/src/data/index.ts' }, + }, + 'typegpu/std': { + import: { reroute: 'typegpu/src/std/index.ts' }, + typeDef: { reroute: 'typegpu/src/std/index.ts' }, + }, + + // Three.js, for examples of @typegpu/three + 'three': { + typeDef: { reroute: '@types/three/build/three.module.d.ts' }, + }, + 'three/webgpu': { + typeDef: { reroute: '@types/three/build/three.webgpu.d.ts' }, + }, + 'three/tsl': { + typeDef: { reroute: '@types/three/build/three.tsl.d.ts' }, + }, + + // Utility modules + '@typegpu/noise': { + import: { reroute: 'typegpu-noise/src/index.ts' }, + typeDef: { reroute: 'typegpu-noise/src/index.ts' }, + }, + '@typegpu/color': { + import: { reroute: 'typegpu-color/src/index.ts' }, + typeDef: { reroute: 'typegpu-color/src/index.ts' }, + }, + '@typegpu/three': { + typeDef: { reroute: 'typegpu-three/src/index.ts' }, + }, + }; + + return SANDBOX_MODULES; +}); diff --git a/apps/typegpu-docs/src/utils/examples/types.ts b/apps/typegpu-docs/src/utils/examples/types.ts index 6c92bfc2bc..5768187c9a 100644 --- a/apps/typegpu-docs/src/utils/examples/types.ts +++ b/apps/typegpu-docs/src/utils/examples/types.ts @@ -1,4 +1,5 @@ import { type } from 'arktype'; +import type { Atom } from 'jotai'; export type ExampleMetadata = typeof ExampleMetadata.infer; export const ExampleMetadata = type({ @@ -32,9 +33,13 @@ export interface ThumbnailPair { export type Example = { key: string; - tsFiles: ExampleSrcFile[]; + contentAtom: Atom< + Promise<{ + htmlFile: ExampleSrcFile; + tsFiles: ExampleSrcFile[]; + }> + >; tsImport: () => Promise; - htmlFile: ExampleSrcFile; metadata: ExampleMetadata; thumbnails?: ThumbnailPair; };