diff --git a/apps/typegpu-docs/src/examples/react/monkey/index.html b/apps/typegpu-docs/src/examples/react/monkey/index.html new file mode 100644 index 0000000000..974ed97c0a --- /dev/null +++ b/apps/typegpu-docs/src/examples/react/monkey/index.html @@ -0,0 +1 @@ +
diff --git a/apps/typegpu-docs/src/examples/react/monkey/index.tsx b/apps/typegpu-docs/src/examples/react/monkey/index.tsx new file mode 100644 index 0000000000..bac8b33d40 --- /dev/null +++ b/apps/typegpu-docs/src/examples/react/monkey/index.tsx @@ -0,0 +1,86 @@ +import { Canvas, Pass } from '@typegpu/react'; +import tgpu from 'typegpu'; +import * as d from 'typegpu/data'; + +const meshLayout = tgpu.bindGroupLayout({ + modelMatrix: { uniform: d.mat4x4f }, + albedo: { uniform: d.vec3f }, + tint: { uniform: d.vec3f }, +}); + +const vertex = ({ pos }: { pos: d.v3f }) => { + 'use gpu'; + return meshLayout.$.modelMatrix.mul(d.vec4f(pos, 1)); +}; + +const fragment = () => { + 'use gpu'; + return d.vec4f(meshLayout.$.tint, 1); +}; + +export function Monkey({ albedo, pos }: { albedo: d.v3f; pos: d.v3f }) { + // const monkeyMesh = useMonkeyMesh(); + // const modelMatrix = useMemo(() => mat4.translation(pos, d.mat4x4f()), []); + + // Optional bindings + // const bindings = useMemo(() => ([ + // [fooSlot, 123], + // [(cfg: Configurable) => /* ... */], + // // ... + // ]), []); + + return ( + // + // + // {/* things provided after the pipeline is created */} + + // {/* the entries are passed into the shader as automatically created resources */} + // + // {/* monkeyMesh has 'layout' and 'buffer' properties, which fit this component */} + // + // + // +
+ Monkey at {`(${pos.x}, ${pos.y}, ${pos.z})`} +
+ ); +} + +export function App() { + return ( + + + + + {/* ... */} + + + ); +} + +// #region Example controls and cleanup + +import { createRoot } from 'react-dom/client'; + +const reactRoot = createRoot( + document.getElementById('example-app') as HTMLDivElement, +); +reactRoot.render(); + +export function onCleanup() { + setTimeout(() => reactRoot.unmount(), 0); +} + +// #endregion diff --git a/apps/typegpu-docs/src/examples/react/monkey/meta.json b/apps/typegpu-docs/src/examples/react/monkey/meta.json new file mode 100644 index 0000000000..976f106c6c --- /dev/null +++ b/apps/typegpu-docs/src/examples/react/monkey/meta.json @@ -0,0 +1,5 @@ +{ + "title": "React: 3D Monkey", + "category": "react", + "tags": ["experimental"] +} diff --git a/apps/typegpu-docs/src/examples/react/monkey/thumbnail.png b/apps/typegpu-docs/src/examples/react/monkey/thumbnail.png new file mode 100644 index 0000000000..889b2f297b Binary files /dev/null and b/apps/typegpu-docs/src/examples/react/monkey/thumbnail.png differ diff --git a/apps/typegpu-docs/src/examples/react/monkey/use-monkey-mesh.ts b/apps/typegpu-docs/src/examples/react/monkey/use-monkey-mesh.ts new file mode 100644 index 0000000000..8873955537 --- /dev/null +++ b/apps/typegpu-docs/src/examples/react/monkey/use-monkey-mesh.ts @@ -0,0 +1,65 @@ +import { load } from '@loaders.gl/core'; +import { OBJLoader } from '@loaders.gl/obj'; +import * as d from 'typegpu/data'; +import tgpu from 'typegpu'; +import { useRoot, type VertexBufferProps } from '@typegpu/react'; + +const MONKEY_MODEL_PATH = '/TypeGPU/assets/3d-monkey/monkey.obj'; + +const MeshVertexInput = { + modelPosition: d.vec3f, + modelNormal: d.vec3f, + textureUV: d.vec2f, +} as const; + +const meshVertexLayout = tgpu.vertexLayout((n: number) => + d.arrayOf(d.struct(MeshVertexInput), n) +); + +export type MeshVertex = d.WgslArray< + d.WgslStruct<{ + readonly modelPosition: d.Vec3f; + readonly modelNormal: d.Vec3f; + readonly textureUV: d.Vec2f; + }> +>; + +export async function useMonkeyMesh(): Promise> { + const root = useRoot(); + + const modelMesh = await load(MONKEY_MODEL_PATH, OBJLoader); + const polygonCount = modelMesh.attributes.POSITION.value.length / 3; + + const vertexBuffer = root + .createBuffer(meshVertexLayout.schemaForCount(polygonCount)) + .$usage('vertex') + .$name(`model vertices of ${MONKEY_MODEL_PATH}`); + + const modelVertices = []; + for (let i = 0; i < polygonCount; i++) { + modelVertices.push({ + modelPosition: d.vec3f( + modelMesh.attributes.POSITION.value[3 * i], + modelMesh.attributes.POSITION.value[3 * i + 1], + modelMesh.attributes.POSITION.value[3 * i + 2], + ), + modelNormal: d.vec3f( + modelMesh.attributes.NORMAL.value[3 * i], + modelMesh.attributes.NORMAL.value[3 * i + 1], + modelMesh.attributes.NORMAL.value[3 * i + 2], + ), + textureUV: d.vec2f( + modelMesh.attributes.TEXCOORD_0.value[2 * i], + 1 - modelMesh.attributes.TEXCOORD_0.value[2 * i + 1], + ), + }); + } + modelVertices.reverse(); + + vertexBuffer.write(modelVertices); + + return { + layout: meshVertexLayout, + buffer: vertexBuffer, + }; +} \ No newline at end of file diff --git a/packages/typegpu-react/src/components/BindGroup.tsx b/packages/typegpu-react/src/components/BindGroup.tsx new file mode 100644 index 0000000000..2bd0f16f4b --- /dev/null +++ b/packages/typegpu-react/src/components/BindGroup.tsx @@ -0,0 +1,47 @@ +import { useEffect, useMemo } from 'react'; +import type { TgpuBindGroupLayout } from 'typegpu'; +import type { AnyData } from 'typegpu/data'; +import { useCanvas } from '../hooks/use-canvas.ts'; +import { usePass } from '../hooks/use-pass.ts'; + +type Entries> = { + [K in keyof T]: any; // Using 'any' to match the expected value types for buffers, textures, etc. +}; + +interface BindGroupProps> { + /** + * The layout for the bind group. + */ + layout: TgpuBindGroupLayout; + /** + * An object containing the resources to be bound. + * The keys must match the keys in the layout definition. + */ + entries: Entries; +} + +export function BindGroup>({ + layout, + entries, +}: BindGroupProps) { + const { root } = useCanvas(); + const { addDrawCall } = usePass(); + + const bindGroup = useMemo(() => { + // It's important that the values in `entries` are stable (e.g., memoized) + // to avoid recreating the bind group on every render. + return root.createBindGroup(layout, entries); + }, [root, layout, entries]); + + useEffect(() => { + const removeDrawCall = addDrawCall((pass) => { + // The group index is derived from the layout object itself. + pass.setBindGroup(layout.groupIndex, bindGroup); + }); + + return removeDrawCall; + }, [addDrawCall, layout.groupIndex, bindGroup]); + + // This component does not render anything to the DOM. + return null; +} \ No newline at end of file diff --git a/packages/typegpu-react/src/components/Canvas.tsx b/packages/typegpu-react/src/components/Canvas.tsx new file mode 100644 index 0000000000..ca0979cc89 --- /dev/null +++ b/packages/typegpu-react/src/components/Canvas.tsx @@ -0,0 +1,69 @@ +import type React from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { + CanvasContext, + type CanvasContextValue, +} from '../context/canvas-context.ts'; +import { useRoot } from '../hooks/use-root.ts'; + +export function Canvas({ children }: { children: React.ReactNode }) { + const root = useRoot(); + const canvasRef = useRef(null); + const canvasCtxRef = useRef(null); + + const frameCallbacksRef = useRef(new Set<(time: number) => void>()); + + const [contextValue] = useState(() => ({ + get context() { + return canvasCtxRef.current; + }, + addFrameCallback(cb: (time: number) => void) { + frameCallbacksRef.current.add(cb); + return () => frameCallbacksRef.current.delete(cb); + }, + })); + + useEffect(() => { + if (!canvasRef.current) return; + + let disposed = false; + const canvas = canvasRef.current; + const context = canvas.getContext('webgpu'); + if (!context) { + console.error('WebGPU not supported'); + return; + } + + const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + context.configure({ + device: root.device, + format: presentationFormat, + alphaMode: 'premultiplied', + }); + canvasCtxRef.current = context; + + const frame = (time: number) => { + if (disposed) return; + requestAnimationFrame(frame); + + frameCallbacksRef.current.forEach((cb) => { + cb(time); + }); + + root['~unstable'].flush(); + }; + requestAnimationFrame(frame); + + return () => { + disposed = true; + }; + }, [root]); + + return ( + + + {children} + + + ); +} diff --git a/packages/typegpu-react/src/components/Config.tsx b/packages/typegpu-react/src/components/Config.tsx new file mode 100644 index 0000000000..be56e29a30 --- /dev/null +++ b/packages/typegpu-react/src/components/Config.tsx @@ -0,0 +1,39 @@ +import type React from 'react'; +import { useMemo } from 'react'; +import type { Configurable, TgpuRenderPipeline, TgpuSlot } from 'typegpu'; +import { PipelineContext } from '../context/pipeline-context.ts'; +import { useRenderPipeline } from '../hooks/use-render-pipeline.ts'; + +type Binding = + | [slot: TgpuSlot, value: any] + | ((cfg: Configurable) => Configurable); + +interface ConfigProps { + bindings: Binding[]; + children: React.ReactNode; +} + +export function Config({ bindings, children }: ConfigProps) { + const pipeline = useRenderPipeline(); + + const configuredPipeline = useMemo(() => { + if (!pipeline) return null; + return bindings.reduce((p, binding) => { + if (Array.isArray(binding)) { + return p.with(binding[0], binding[1]); + } + return p.pipe(binding); + }, pipeline as Configurable) as TgpuRenderPipeline; + }, [pipeline, bindings]); + + if (!pipeline) { + return <>{children}; + } + + // This re-provides the configured pipeline to children + return ( + + {children} + + ); +} diff --git a/packages/typegpu-react/src/components/Pass.tsx b/packages/typegpu-react/src/components/Pass.tsx new file mode 100644 index 0000000000..d3f313e303 --- /dev/null +++ b/packages/typegpu-react/src/components/Pass.tsx @@ -0,0 +1,80 @@ +import type React from 'react'; +import { useCallback, useEffect, useRef } from 'react'; +import type { RenderPass } from '../../../typegpu/src/core/root/rootTypes.ts'; // TODO: Expose it in typegpu +import { PassContext } from '../context/pass-context.tsx'; +import { useCanvas } from '../hooks/use-canvas.ts'; +import { useRoot } from '../hooks/use-root.ts'; + +export function Pass( + { children }: { children: React.ReactNode; schedule: 'frame' }, +) { + const root = useRoot(); + const ctx = useCanvas(); + const drawCalls = useRef(new Set<(pass: RenderPass) => void>()).current; + const depthTextureRef = useRef(null); + + const addDrawCall = useCallback( + (cb: (pass: RenderPass) => void) => { + drawCalls.add(cb); + return () => drawCalls.delete(cb); + }, + [drawCalls], + ); + + useEffect(() => { + const removeFrameCallback = ctx.addFrameCallback(() => { + const canvas = ctx.context?.canvas as HTMLCanvasElement; + let depthTexture = depthTextureRef.current; + if ( + !depthTexture || + depthTexture.width !== canvas.width || + depthTexture.height !== canvas.height + ) { + depthTexture?.destroy(); + const newDepthTexture = root.device.createTexture({ + size: [canvas.width, canvas.height], + format: 'depth24plus', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + depthTexture = depthTextureRef.current = newDepthTexture; + } + + root['~unstable'].beginRenderPass( + { + colorAttachments: [ + { + view: ctx.context?.getCurrentTexture() + .createView() as GPUTextureView, + clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: depthTexture.createView(), + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }, + }, + (pass) => { + drawCalls.forEach((draw) => { + draw(pass); + }); + }, + ); + }); + + return () => { + removeFrameCallback(); + depthTextureRef.current?.destroy(); + depthTextureRef.current = null; + }; + }, [ctx, root, drawCalls]); + + return ( + + {children} + + ); +} diff --git a/packages/typegpu-react/src/components/RenderPipeline.tsx b/packages/typegpu-react/src/components/RenderPipeline.tsx new file mode 100644 index 0000000000..c7f9ea33ce --- /dev/null +++ b/packages/typegpu-react/src/components/RenderPipeline.tsx @@ -0,0 +1,126 @@ +import type React from 'react'; +import { useEffect, useMemo, useRef } from 'react'; +import tgpu from 'typegpu'; +import type * as d from 'typegpu/data'; +// TODO: Export these types in typegpu +import type { + VertexInConstrained, + VertexOutConstrained, +} from '../../../typegpu/src/core/function/tgpuVertexFn.ts'; +import type { OmitBuiltins } from '../../../typegpu/src/builtin.ts'; +import type { + FragmentInConstrained, + FragmentOutConstrained, +} from '../../../typegpu/src/core/function/tgpuFragmentFn.ts'; +import type { RenderPass } from '../../../typegpu/src/core/root/rootTypes.ts'; +import type { LayoutToAllowedAttribs } from '../../../typegpu/src/core/vertexLayout/vertexAttribute.ts'; +// TODO: +import { usePass } from '../hooks/use-pass.ts'; +import { useCanvas } from '../hooks/use-canvas.ts'; +import { PipelineContext } from '../context/pipeline-context.ts'; +import { useRoot } from '../hooks/use-root.ts'; + +type InferRecord = { [K in keyof T]: d.Infer }; + +interface VertexOptions< + VIn extends VertexInConstrained, + VOut extends VertexOutConstrained, +> { + body: (input: InferRecord) => InferRecord; + in: VIn; + out: VOut; + attributes: LayoutToAllowedAttribs>; +} + +interface FragmentOptions< + FIn extends FragmentInConstrained, + FOut extends FragmentOutConstrained, +> { + body: (input: InferRecord) => d.Infer; + in: FIn; + out: FOut; +} + +interface RenderPipelineProps< + VIn extends VertexInConstrained, + VOut extends VertexOutConstrained, + FIn extends FragmentInConstrained, + FOut extends FragmentOutConstrained, +> { + vertex: VertexOptions; + fragment: FragmentOptions; + vertexCount: number; + instanceCount?: number; + children?: React.ReactNode; +} + +// TODO: Alter this hook when .withVertex and .withFragment will be simplified +export function RenderPipeline< + VIn extends VertexInConstrained, + VOut extends VertexOutConstrained, + FIn extends VertexOutConstrained & FragmentInConstrained, + FOut extends FragmentOutConstrained, +>({ + vertex, + fragment, + vertexCount, + instanceCount, + children, +}: RenderPipelineProps & { + fragmentIn?: OmitBuiltins; +}) { + const root = useRoot(); + const ctx = useCanvas(); + const { addDrawCall } = usePass(); + const drawCommand = useRef<(pass: RenderPass) => void>(() => {}); + + const vertexRef = useRef(vertex.body); + const fragmentRef = useRef(fragment.body); + + const pipeline = useMemo(() => { + const vertexFn = tgpu['~unstable'].vertexFn({ + in: vertex.in, + out: vertex.out, + })(vertexRef.current); + const fragmentFn = tgpu['~unstable'].fragmentFn({ + in: fragment.in, + out: fragment.out, + })( + fragmentRef.current, + ); + + const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + return root['~unstable'] + .withVertex(vertexFn, vertex.attributes) + .withFragment(fragmentFn, { format: presentationFormat }) + .withDepthStencil({ + format: 'depth24plus', + depthWriteEnabled: true, + depthCompare: 'less', + }) + .createPipeline(); + }, [root, vertex, fragment]); + + useEffect(() => { + const removeDrawCall = addDrawCall((pass) => { + pass.setPipeline(pipeline); + // Children will set buffers and bind groups here before drawing. + // This function is captured by the closure and will be executed + // with the latest context from its children. + drawCommand.current(pass); + }); + return removeDrawCall; + }, [addDrawCall, pipeline]); + + // This function will be updated by children (like VertexBuffer, BindGroup) + // to set their resources on the pass. + drawCommand.current = (pass: RenderPass) => { + pass.draw(vertexCount, instanceCount); + }; + + return ( + + {children} + + ); +} diff --git a/packages/typegpu-react/src/components/VertexBuffer.tsx b/packages/typegpu-react/src/components/VertexBuffer.tsx new file mode 100644 index 0000000000..b19242a727 --- /dev/null +++ b/packages/typegpu-react/src/components/VertexBuffer.tsx @@ -0,0 +1,28 @@ +import { useEffect } from 'react'; +import type { + TgpuBuffer, + TgpuVertexLayout, + VertexFlag, +} from 'typegpu'; +import type { Disarray, WgslArray } from 'typegpu/data'; +import { usePass } from '../hooks/use-pass.ts'; + +export type VertexBufferProps ={ + layout: TgpuVertexLayout; + buffer: TgpuBuffer & VertexFlag; +} + +export function VertexBuffer({ + layout, + buffer, +}: VertexBufferProps) { + const { addDrawCall } = usePass(); + + useEffect(() => { + return addDrawCall((pass) => { + pass.setVertexBuffer(layout, buffer); + }); + }, [addDrawCall, layout, buffer]); + + return null; +} \ No newline at end of file diff --git a/packages/typegpu-react/src/components/index.ts b/packages/typegpu-react/src/components/index.ts new file mode 100644 index 0000000000..32117246c3 --- /dev/null +++ b/packages/typegpu-react/src/components/index.ts @@ -0,0 +1,6 @@ +export { BindGroup } from './BindGroup.tsx'; +export { Pass } from './Pass.tsx'; +export { Canvas } from './Canvas.tsx'; +export { RenderPipeline } from './RenderPipeline.tsx'; +export { Config } from './Config.tsx'; +export { VertexBuffer, type VertexBufferProps } from './VertexBuffer.tsx'; diff --git a/packages/typegpu-react/src/context/canvas-context.ts b/packages/typegpu-react/src/context/canvas-context.ts new file mode 100644 index 0000000000..01009c3c87 --- /dev/null +++ b/packages/typegpu-react/src/context/canvas-context.ts @@ -0,0 +1,8 @@ +import { createContext } from 'react'; + +export interface CanvasContextValue { + readonly context: GPUCanvasContext | null; + addFrameCallback: (cb: (time: number) => void) => () => void; +} + +export const CanvasContext = createContext(null); diff --git a/packages/typegpu-react/src/context/pass-context.tsx b/packages/typegpu-react/src/context/pass-context.tsx new file mode 100644 index 0000000000..9e455878c2 --- /dev/null +++ b/packages/typegpu-react/src/context/pass-context.tsx @@ -0,0 +1,8 @@ +import { createContext } from 'react'; +import type { RenderPass } from '../../../typegpu/src/core/root/rootTypes.ts'; // TODO: Expose it in typegpu + +export interface PassContextValue { + addDrawCall: (cb: (pass: RenderPass) => void) => () => void; +} + +export const PassContext = createContext(null); diff --git a/packages/typegpu-react/src/context/pipeline-context.ts b/packages/typegpu-react/src/context/pipeline-context.ts new file mode 100644 index 0000000000..f642d2c3bf --- /dev/null +++ b/packages/typegpu-react/src/context/pipeline-context.ts @@ -0,0 +1,4 @@ +import { createContext } from 'react'; +import type { TgpuRenderPipeline } from 'typegpu'; + +export const PipelineContext = createContext(null); diff --git a/packages/typegpu-react/src/context/root-context.tsx b/packages/typegpu-react/src/context/root-context.tsx new file mode 100644 index 0000000000..dba6308e36 --- /dev/null +++ b/packages/typegpu-react/src/context/root-context.tsx @@ -0,0 +1,29 @@ +import { createContext } from 'react'; +import tgpu, { type TgpuRoot } from 'typegpu'; + +export class RootContext { + #root: TgpuRoot | undefined; + #rootPromise: Promise | undefined; + + initOrGetRoot(): Promise | TgpuRoot { + if (this.#root) { + return this.#root; + } + + if (!this.#rootPromise) { + this.#rootPromise = tgpu.init().then((root) => { + this.#root = root; + return root; + }); + } + + return this.#rootPromise; + } +} + +/** + * Used in case no provider is mounted + */ +export const globalRootContextValue = new RootContext(); + +export const rootContext = createContext(null); \ No newline at end of file diff --git a/packages/typegpu-react/src/hooks/index.ts b/packages/typegpu-react/src/hooks/index.ts new file mode 100644 index 0000000000..5f50074ffe --- /dev/null +++ b/packages/typegpu-react/src/hooks/index.ts @@ -0,0 +1,4 @@ +export { useFrame } from './use-frame.ts'; +export { useRoot } from './use-root.ts'; +export { useUniform } from './use-uniform.ts'; +export { useUniformRef } from './use-uniform-ref.ts'; diff --git a/packages/typegpu-react/src/hooks/use-canvas.ts b/packages/typegpu-react/src/hooks/use-canvas.ts new file mode 100644 index 0000000000..be40f6ac67 --- /dev/null +++ b/packages/typegpu-react/src/hooks/use-canvas.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { CanvasContext } from '../context/canvas-context.ts'; + +export const useCanvas = () => { + const context = useContext(CanvasContext); + if (!context) { + throw new Error('useCanvas must be used within a Canvas component'); + } + return context; +}; diff --git a/packages/typegpu-react/src/use-frame.ts b/packages/typegpu-react/src/hooks/use-frame.ts similarity index 100% rename from packages/typegpu-react/src/use-frame.ts rename to packages/typegpu-react/src/hooks/use-frame.ts diff --git a/packages/typegpu-react/src/hooks/use-pass.ts b/packages/typegpu-react/src/hooks/use-pass.ts new file mode 100644 index 0000000000..e0af752dac --- /dev/null +++ b/packages/typegpu-react/src/hooks/use-pass.ts @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { PassContext, type PassContextValue } from "../context/pass-context.tsx"; + +export function usePass(): PassContextValue { + const context = useContext(PassContext); + if (!context) { + throw new Error('This component must be a child of a Pass component'); + } + return context; +}; diff --git a/packages/typegpu-react/src/hooks/use-render-pipeline.ts b/packages/typegpu-react/src/hooks/use-render-pipeline.ts new file mode 100644 index 0000000000..a38ac98055 --- /dev/null +++ b/packages/typegpu-react/src/hooks/use-render-pipeline.ts @@ -0,0 +1,4 @@ +import { useContext } from 'react'; +import { PipelineContext } from '../context/pipeline-context.ts'; + +export const useRenderPipeline = () => useContext(PipelineContext); diff --git a/packages/typegpu-react/src/hooks/use-root.ts b/packages/typegpu-react/src/hooks/use-root.ts new file mode 100644 index 0000000000..cec7d82ba9 --- /dev/null +++ b/packages/typegpu-react/src/hooks/use-root.ts @@ -0,0 +1,10 @@ +import { use, useContext } from 'react'; +import type { TgpuRoot } from 'typegpu'; +import { globalRootContextValue, rootContext } from '../context/root-context.tsx'; + +export function useRoot(): TgpuRoot { + const context = useContext(rootContext) ?? globalRootContextValue; + + const maybeRoot = context.initOrGetRoot(); + return maybeRoot instanceof Promise ? use(maybeRoot) : maybeRoot; +} diff --git a/packages/typegpu-react/src/use-uniform-value.ts b/packages/typegpu-react/src/hooks/use-uniform-ref.ts similarity index 95% rename from packages/typegpu-react/src/use-uniform-value.ts rename to packages/typegpu-react/src/hooks/use-uniform-ref.ts index 9330c47f36..847ae76595 100644 --- a/packages/typegpu-react/src/use-uniform-value.ts +++ b/packages/typegpu-react/src/hooks/use-uniform-ref.ts @@ -1,7 +1,7 @@ import type * as d from 'typegpu/data'; -import { useRoot } from './root-context.tsx'; import { useEffect, useMemo, useRef, useState } from 'react'; import type { ValidateUniformSchema } from 'typegpu'; +import { useRoot } from './use-root.ts'; interface UniformValue> { schema: TSchema; @@ -19,7 +19,7 @@ function initialValueFromSchema( return schema() as d.Infer; } -export function useUniformValue< +export function useUniformRef< TSchema extends d.AnyWgslData, TValue extends d.Infer, >( diff --git a/packages/typegpu-react/src/hooks/use-uniform.ts b/packages/typegpu-react/src/hooks/use-uniform.ts new file mode 100644 index 0000000000..6a0951563f --- /dev/null +++ b/packages/typegpu-react/src/hooks/use-uniform.ts @@ -0,0 +1,69 @@ +import * as d from 'typegpu/data'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import type { ValidateUniformSchema } from 'typegpu'; +import { useRoot } from './use-root.ts'; + +interface Uniform { + schema: TSchema; + readonly $: d.InferGPU; +} + +export function useUniform< + TSchema extends d.AnyWgslData, + TValue extends d.Infer, +>( + schema: ValidateUniformSchema, + value: TValue, +): Uniform { + const root = useRoot(); + const [uniformBuffer, setUniformBuffer] = useState(() => { + return root.createUniform(schema, value); + }); + const prevSchemaRef = useRef(schema); + const currentSchemaRef = useRef(schema); + const cleanupRef = useRef | null>(null); + + useEffect(() => { + if (cleanupRef.current) { + clearTimeout(cleanupRef.current); + } + + return () => { + cleanupRef.current = setTimeout(() => { + uniformBuffer.buffer.destroy(); + }, 200); + }; + }, [uniformBuffer]); + + useEffect(() => { + if (!d.deepEqual(prevSchemaRef.current as d.AnyData, schema as d.AnyData)) { + uniformBuffer.buffer.destroy(); + setUniformBuffer(root.createUniform(schema, value)); + prevSchemaRef.current = schema; + } else { + uniformBuffer.write(value); + } + }, [schema, value, root, uniformBuffer]); + + if ( + !d.deepEqual(currentSchemaRef.current as d.AnyData, schema as d.AnyData) + ) { + currentSchemaRef.current = schema; + } + + // Using current schema ref instead of schema directly + // to prevent unnecessary re-memoization when schema object + // reference changes but content is structurally equivalent. + // biome-ignore lint/correctness/useExhaustiveDependencies: This value needs to be stable + const uniformValue = useMemo( + () => ({ + schema, + get $() { + return uniformBuffer.$; + }, + }), + [currentSchemaRef.current, uniformBuffer], + ); + + return uniformValue as Uniform; +} \ No newline at end of file diff --git a/packages/typegpu-react/src/index.ts b/packages/typegpu-react/src/index.ts index f92b8da8f8..b404274210 100644 --- a/packages/typegpu-react/src/index.ts +++ b/packages/typegpu-react/src/index.ts @@ -1,3 +1,10 @@ -export { useFrame } from './use-frame.ts'; -export { useRender } from './use-render.ts'; -export { useUniformValue } from './use-uniform-value.ts'; +export { + BindGroup, + Canvas, + Config, + Pass, + RenderPipeline, + VertexBuffer, + type VertexBufferProps, +} from './components/index.ts'; +export { useFrame, useRoot, useUniform, useUniformRef } from './hooks/index.ts'; diff --git a/packages/typegpu-react/src/root-context.tsx b/packages/typegpu-react/src/root-context.tsx deleted file mode 100644 index e0443f3cf7..0000000000 --- a/packages/typegpu-react/src/root-context.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { - createContext, - type ReactNode, - use, - useContext, - useState, -} from 'react'; -import tgpu, { type TgpuRoot } from 'typegpu'; - -class RootContext { - #root: TgpuRoot | undefined; - #rootPromise: Promise | undefined; - - initOrGetRoot(): Promise | TgpuRoot { - if (this.#root) { - return this.#root; - } - - if (!this.#rootPromise) { - this.#rootPromise = tgpu.init().then((root) => { - this.#root = root; - return root; - }); - } - - return this.#rootPromise; - } -} - -/** - * Used in case no provider is mounted - */ -const globalRootContextValue = new RootContext(); - -const rootContext = createContext(null); - -export interface RootProps { - children?: ReactNode | undefined; -} - -export const Root = ({ children }: RootProps) => { - const [ctx] = useState(() => new RootContext()); - - return ( - - {children} - - ); -}; - -export function useRoot(): TgpuRoot { - const context = useContext(rootContext) ?? globalRootContextValue; - - const maybeRoot = context.initOrGetRoot(); - return maybeRoot instanceof Promise ? use(maybeRoot) : maybeRoot; -} diff --git a/packages/typegpu-react/src/use-render.ts b/packages/typegpu-react/src/use-render.ts index e36dd7ee8f..de15be508b 100644 --- a/packages/typegpu-react/src/use-render.ts +++ b/packages/typegpu-react/src/use-render.ts @@ -1,8 +1,8 @@ -import * as d from 'typegpu/data'; -import tgpu from 'typegpu'; -import { useRoot } from './root-context.tsx'; import { useMemo, useRef } from 'react'; -import { useFrame } from './use-frame.ts'; +import tgpu from 'typegpu'; +import * as d from 'typegpu/data'; +import { useRoot } from './hooks/use-root.ts'; +import { useFrame } from './hooks/use-frame.ts'; type InferRecord = { [K in keyof T]: d.Infer; diff --git a/packages/typegpu/src/data/deepEqual.ts b/packages/typegpu/src/data/deepEqual.ts index cb5f31e9a4..bd163497eb 100644 --- a/packages/typegpu/src/data/deepEqual.ts +++ b/packages/typegpu/src/data/deepEqual.ts @@ -1,5 +1,10 @@ import type { AnyAttribute } from './attributes.ts'; -import { isDisarray, isLooseDecorated, isUnstruct } from './dataTypes.ts'; +import { + isDisarray, + isLooseData, + isLooseDecorated, + isUnstruct, +} from './dataTypes.ts'; import type { AnyData } from './dataTypes.ts'; import { isAtomic, @@ -107,8 +112,14 @@ export function deepEqual(a: AnyData, b: AnyData): boolean { return false; } } + + return true; + } + + if (isLooseData(a) && isLooseData(b)) { + // TODO: This is a simplified check. A a more detailed comparison might be necessary. + return JSON.stringify(a) === JSON.stringify(b); } - // All other types have been checked for equality at the start return true; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8e852e0e9..7d0589ee75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,7 +11,7 @@ catalogs: version: 2.6.0 tsdown: specifier: ^0.15.0 - version: 0.15.7 + version: 0.15.11 tsup: specifier: ^8.5.0 version: 8.5.0 @@ -541,7 +541,7 @@ importers: version: 0.1.66 tsdown: specifier: catalog:build - version: 0.15.7(typescript@5.8.3) + version: 0.15.11(typescript@5.8.3) typegpu: specifier: workspace:* version: link:../typegpu @@ -790,6 +790,10 @@ packages: resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} @@ -828,6 +832,10 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -856,6 +864,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -904,6 +917,10 @@ packages: resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -1720,6 +1737,10 @@ packages: resolution: {integrity: sha512-cYxcj5CPn/vo5QSpCZcYzBiLidU5+GlFSqIeNaMgBDtcVRBsBJHZg3pHw999W6nHamFQ1EHuPPByB26tjaJiJw==} engines: {node: '>=6.9.0'} + '@oxc-project/runtime@0.95.0': + resolution: {integrity: sha512-qJS5pNepwMGnafO9ayKGz7rfPQgUBuunHpnP1//9Qa0zK3oT3t1EhT+I+pV9MUA+ZKez//OFqxCxf1vijCKb2Q==} + engines: {node: ^20.19.0 || >=22.12.0} + '@oxc-project/types@0.82.2': resolution: {integrity: sha512-WMGSwd9FsNBs/WfqIOH0h3k1LBdjZJQGYjGnC+vla/fh6HUsu5HzGPerRljiq1hgMQ6gs031YJR12VyP57b/hQ==} @@ -1758,6 +1779,10 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -3768,6 +3793,9 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -4299,6 +4327,9 @@ packages: magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} @@ -5342,13 +5373,13 @@ packages: robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rolldown-plugin-dts@0.16.11: - resolution: {integrity: sha512-9IQDaPvPqTx3RjG2eQCK5GYZITo203BxKunGI80AGYicu1ySFTUyugicAaTZWRzFWh9DSnzkgNeMNbDWBbSs0w==} + rolldown-plugin-dts@0.17.2: + resolution: {integrity: sha512-tbLm7FoDvZAhAY33wJbq0ACw+srToKZ5xFqwn/K4tayGloZPXQHyOEPEYi7whEfTCaMndZWaho9+oiQTlwIe6Q==} engines: {node: '>=20.18.0'} peerDependencies: '@ts-macro/tsc': ^0.3.6 '@typescript/native-preview': '>=7.0.0-dev.20250601.1' - rolldown: ^1.0.0-beta.9 + rolldown: ^1.0.0-beta.44 typescript: ^5.0.0 vue-tsc: ~3.1.0 peerDependenciesMeta: @@ -5623,6 +5654,10 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + tailwindcss-motion@1.1.1: resolution: {integrity: sha512-CeeQAc5o31BuEPMyWdq/786X7QWNeifa+8khfu74Fs8lGkgEwjNYv6dGv+lRFS8FWXV5dp7F3AU9JjBXjiaQfw==} peerDependencies: @@ -5756,8 +5791,8 @@ packages: typescript: optional: true - tsdown@0.15.7: - resolution: {integrity: sha512-uFaVgWAogjOMqjY+CQwrUt3C6wzy6ynt82CIoXymnbS17ipUZ8WDXUceJjkislUahF/BZc5+W44Ue3p2oWtqUg==} + tsdown@0.15.11: + resolution: {integrity: sha512-7k2OglWWt6LzvJKwEf1izbGvETvVfPYRBr9JgEYVRnz/R9LeJSp+B51FUMO46wUeEGtZ1jA3E3PtWWLlq3iygA==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: @@ -5950,6 +5985,11 @@ packages: resolution: {integrity: sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==} engines: {node: '>=18.12.0'} + unrun@0.2.1: + resolution: {integrity: sha512-1HpwmlCKrAOP3jPxFisPR0sYpPuiNtyYKJbmKu9iugIdvCte3DH1uJ1p1DBxUWkxW2pjvkUguJoK9aduK8ak3Q==} + engines: {node: '>=20.19.0'} + hasBin: true + unstorage@1.17.1: resolution: {integrity: sha512-KKGwRTT0iVBCErKemkJCLs7JdxNVfqTPc/85ae1XES0+bsHbc/sFBfVi5kJp156cc51BHinIH2l3k0EZ24vOBQ==} peerDependencies: @@ -6785,6 +6825,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-compilation-targets@7.27.2': dependencies: '@babel/compat-data': 7.28.4 @@ -6821,6 +6869,8 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.28.4': @@ -6844,6 +6894,10 @@ snapshots: dependencies: '@babel/types': 7.28.4 + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 @@ -6903,6 +6957,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@1.0.2': {} '@biomejs/biome@2.2.5': @@ -7601,6 +7660,8 @@ snapshots: '@oxc-project/runtime@0.82.2': {} + '@oxc-project/runtime@0.95.0': {} + '@oxc-project/types@0.82.2': {} '@oxc-project/types@0.95.0': {} @@ -7625,6 +7686,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@pkgr/core@0.2.9': {} + '@polka/url@1.0.0-next.29': {} '@prettier/sync@0.5.5(prettier@3.5.3)': @@ -7992,7 +8055,7 @@ snapshots: estree-walker: 2.0.2 fdir: 6.5.0(picomatch@4.0.3) is-reference: 1.2.1 - magic-string: 0.30.19 + magic-string: 0.30.21 picomatch: 4.0.3 optionalDependencies: rollup: 4.34.8 @@ -8016,7 +8079,7 @@ snapshots: '@rollup/plugin-replace@6.0.2(rollup@4.34.8)': dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.34.8) - magic-string: 0.30.19 + magic-string: 0.30.21 optionalDependencies: rollup: 4.34.8 @@ -8501,7 +8564,7 @@ snapshots: '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.19 + magic-string: 0.30.21 pathe: 2.0.3 '@vitest/spy@3.2.4': @@ -8647,7 +8710,7 @@ snapshots: ast-kit@2.1.3: dependencies: - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 pathe: 2.0.3 astring@1.9.0: {} @@ -9638,7 +9701,7 @@ snapshots: fix-dts-default-cjs-exports@1.0.0: dependencies: - magic-string: 0.30.19 + magic-string: 0.30.21 mlly: 1.7.4 rollup: 4.34.8 @@ -9710,6 +9773,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + github-from-package@0.0.0: optional: true @@ -10304,6 +10371,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: dependencies: '@babel/parser': 7.28.4 @@ -11739,17 +11810,17 @@ snapshots: robust-predicates@3.0.2: {} - rolldown-plugin-dts@0.16.11(rolldown@1.0.0-beta.45)(typescript@5.8.3): + rolldown-plugin-dts@0.17.2(rolldown@1.0.0-beta.45)(typescript@5.8.3): dependencies: - '@babel/generator': 7.28.3 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 ast-kit: 2.1.3 birpc: 2.6.1 debug: 4.4.3 dts-resolver: 2.1.2 - get-tsconfig: 4.10.1 - magic-string: 0.30.19 + get-tsconfig: 4.13.0 + magic-string: 0.30.21 rolldown: 1.0.0-beta.45 optionalDependencies: typescript: 5.8.3 @@ -11801,7 +11872,7 @@ snapshots: rollup-plugin-dts@6.1.1(rollup@4.34.8)(typescript@5.8.3): dependencies: - magic-string: 0.30.19 + magic-string: 0.30.21 rollup: 4.34.8 typescript: 5.8.3 optionalDependencies: @@ -12109,6 +12180,10 @@ snapshots: symbol-tree@3.2.4: {} + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + tailwindcss-motion@1.1.1(tailwindcss@4.1.11): dependencies: tailwindcss: 4.1.11 @@ -12235,7 +12310,7 @@ snapshots: optionalDependencies: typescript: 5.8.3 - tsdown@0.15.7(typescript@5.8.3): + tsdown@0.15.11(typescript@5.8.3): dependencies: ansis: 4.2.0 cac: 6.7.14 @@ -12245,12 +12320,13 @@ snapshots: empathic: 2.0.0 hookable: 5.5.3 rolldown: 1.0.0-beta.45 - rolldown-plugin-dts: 0.16.11(rolldown@1.0.0-beta.45)(typescript@5.8.3) + rolldown-plugin-dts: 0.17.2(rolldown@1.0.0-beta.45)(typescript@5.8.3) semver: 7.7.3 tinyexec: 1.0.1 tinyglobby: 0.2.15 tree-kill: 1.2.2 unconfig: 7.3.3 + unrun: 0.2.1 optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -12362,7 +12438,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.0 hookable: 5.5.3 jiti: 2.6.0 - magic-string: 0.30.19 + magic-string: 0.30.21 mkdist: 2.2.0(typescript@5.8.3) mlly: 1.7.4 pathe: 2.0.3 @@ -12482,6 +12558,12 @@ snapshots: picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 + unrun@0.2.1: + dependencies: + '@oxc-project/runtime': 0.95.0 + rolldown: 1.0.0-beta.45 + synckit: 0.11.11 + unstorage@1.17.1: dependencies: anymatch: 3.1.3 @@ -12642,7 +12724,7 @@ snapshots: chai: 5.2.0 debug: 4.4.3 expect-type: 1.2.1 - magic-string: 0.30.19 + magic-string: 0.30.21 pathe: 2.0.3 picomatch: 4.0.3 std-env: 3.9.0