Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Design a basic npm packaging format #56

Open
mighdoll opened this issue Oct 10, 2024 · 18 comments
Open

Design a basic npm packaging format #56

mighdoll opened this issue Oct 10, 2024 · 18 comments
Milestone

Comments

@mighdoll
Copy link
Contributor

Let's see if we can come up with a basic npm packaging format for simple WESL libraries so that JavaScript and TypeScript MVP users can get a taste of using stateless WESL libraries.

The packaging format doesn't need to be stable for long term and libaries should be labeled with an unstable version. See #5.

Here's one possible approach:

  • A library (manually) creates a file that includes WESL files as strings, along with a package.json entry:

     dist/wesl.ts:
     
     export default package = {
       packageName: "myPkg",
       edition: "wesl_unstable_1",
       weslFiles: {
         "./lib.wesl": "fn bar() {}",
       },			
     }
     package.json:
     {
       "name": "myPkg", 
       "exports": {
         "./wesl": {"module" : "./dist/wesl.ts" }
       }
     }	
  • The user (manually) imports and passes the library to the linker
     import pkgWESL from "myPkg/wesl"; 
     
     linker.link(appWESL, { libs: [pkgWgsl] });

See also packaging and earlier gdoc for related discussion.

@mighdoll mighdoll added the mvp label Oct 10, 2024
@mighdoll
Copy link
Contributor Author

mighdoll commented Nov 5, 2024

There's a prototype implementation here.

Some questions:

  • should we also include the original wgsl/wesl source files in the package for e.g. a vite bundler to use?
  • how should we package libraries with multiple parts? multiple exports entries? multiple bundles?

@stefnotch
Copy link
Collaborator

My immediate reactions to the questions would be

* should we also include the original wgsl/wesl source files in the package for e.g. a vite bundler to use?

No, vite can just as well use the Javascript object that contains everything.

* how should we package libraries with multiple parts? multiple exports entries? multiple bundles?

I think it doesn't matter, since tree-shaking should work either way.

@mighdoll
Copy link
Contributor Author

mighdoll commented Nov 5, 2024

* how should we package libraries with multiple parts? multiple exports entries? multiple bundles?

I think it doesn't matter, since tree-shaking should work either way.

The linkers will keep unused shader code from WebGPU, I agree.

But w/o further changes to spit things up, I think the javascript bundle will include all the shaders from the library. At least in the case of runtime linking w/o a build time support through e.g. a vite plugin..

@mighdoll mighdoll added this to the M1 milestone Nov 18, 2024
@mighdoll mighdoll removed the mvp label Nov 18, 2024
@iwoplaza
Copy link

Great initiative! This might have already been resolved, but I'll share my initial thoughts. Let me know in case I missed something.

creates a file that includes WESL files as strings

If we instead parse the source WESL files on the side of the library author, before publishing, we can:

  • store the WESL code in a intermediate format that will not require the library consumer to ship a WGSL parser into their app (for linking).
  • Use the host language's import functionality to perform linking for us.
  • Mirror the WESL file structure in the generated .js files, so that linking between WESL files can also happen automatically by just importing from a relative path.
  • Expose a minimal library to the library consumer that resolves the intermediate format into a spec-compliant WGSL code string.
  • Couple the metadata with the resources, instead of providing them in separate trees. That way, types can get properly inferred by libraries like TypeGPU, or any other TypeScript library.

Example of what a type-friendly format could look like

Transform a WGSL file at prepublish time:

// The style of imports can of-course change in WESL.
use example-wgsl-utility::{ Gradient };

pub fn red_to_blue_gradient() -> Gradient {
  var result: Gradient;
  result.from = vec3f(1., 0., 0.);
  result.to = vec3f(0., 0., 1.);
  return result;
}:

Into this:

// This lets the types propagate from deep in the
// dependency tree straight up to the library consumer
import { Gradient } from 'example-wgsl-utility';

// Injecting only once, when one or more functions are defined
/**
 * @template {unknown[]} TArgs
 * @template TReturn
 * @typedef {object} WgslFn
 * @prop {'function'} kind
 * @prop {T} argTypes
 */
const fn = /**@type{<TArgs extends unknown[], TReturn>(argTypes: TArgs, returnType: TReturn, body: string)=>WgslFn<TArgs,TReturn>}*/ (
  (argTypes, returnType, body) => ({
    kind: 'function',
    argTypes,
    returnType,
    body,
  })
);

export const red_to_blue_gradient = fn([], Gradient, '() -> Gradient {
  var result: Gradient;
  result.from = vec3f(1., 0., 0.);
  result.to = vec3f(0., 0., 1.);
  return result;
}'};

Example: Using the library to retrieve just WGSL

import * as myLib from 'my-library';

const rawWGSL = linker.link(appWGSL, { libs: [myLib] });

Example: Using the library with TypeGPU

import { red_to_blue_gradient } from 'my-library';

const mainFrag = tgpu.fragmentFn({}, {}).does(() => {
  // inferred by TypeScript to return: WgslStruct<{ from: Vec3f, to: Vec3f }>
  const grad = red_to_blue_gradient();
  return vec4f(grad.from, 1.0);
});

@stefnotch
Copy link
Collaborator

One thing that this issue is still missing is "how does a language server get the .wesl code".

There are two major approaches

  1. Separate bundles for "language server wesl" and "consumer wesl".
  2. Combine them, and keep the format simple enough for a language server.

@iwoplaza
Copy link

If we'd like to support click to go to definition as well as IntelliSense, maybe we should ship the source .wesl/.wgsl files along with the consumer wesl. That way, when going to definition in host-land, it would take the developer to the consumer wesl, and when going to definition in wesl-land, it would take them to the source wesl.

@iwoplaza
Copy link

In the context of my proposal, the "consumer wesl" would be WGSL snippets that are wrapped in easy to interpret JS that can be consumed both by the TS type system as well as JS at runtime.

@iwoplaza
Copy link

On another note, how would transitive dependencies be handled in the current system? For the given example:

app-code
|- module_a
|  |- module_c
|- module_b

Lets say module_a has a dependency on module_c, both being WESL modules.
Would the linker have to include all 3 when linking?:

linker.link(appWESL, { libs: [moduleA, moduleB, moduleC] });

If so, that means that if a module gains a direct dependency, then the module consumer has to also become a direct dependant of that dependency. If we instead leverage the host language's imports for "consumer wesl", then the transitive dependency problem solves itself.

@mighdoll
Copy link
Contributor Author

Lets say module_a has a dependency on module_c, both being WESL modules. Would the linker have to include all 3 when linking?:

Yes, currently. And also yes, it'd be great to fix things so that users didn't have to specify libraries manually when calling the linker. See vite plugin for WESL for some vague hopes that a plugin could help.

@mighdoll
Copy link
Contributor Author

If we instead parse the source WESL files on the side of the library author, before publishing, we can:

I think using a pre-parsed version of WGSL at runtime makes a lot of sense. Re-parsing to link at runtime takes time and code space. I expect that the parsing will eventually be more than 150K LOC/sec on a laptop, and that the parser part of the linker will be less than 10kb. I bet a pre-parsed version could save most of that time and code space.

If we had a build tool that converts project and library WESL to a pre-parsed form, the pre-parsed form would be more free to evolve and optimize. I think that might be wiser than trying to stabilize a pre-parsed form for long term support..

Or perhaps there ought to be optional fields in the library format. Packages could include WESL source as a baseline, but also include version locked pre-parsed WESL source, or shader-slang source, etc.

  • Use the host language's import functionality to perform linking for us.
  • Mirror the WESL file structure in the generated .js files, so that linking between WESL files can also happen automatically by just importing from a relative path.
  • Couple the metadata with the resources, instead of providing them in separate trees. That way, types can get properly inferred by libraries like TypeGPU, or any other TypeScript library.

I really like the ability to control linking from TypeScript, and also that exposing TypeScript types for WGSL elements so that interop between shaders and TypeScript is safer.

I think the interop problem is probably the easier one of the two, e.g. sharing types for uniforms by injected them from TypeScript or reflecting them from WGSL. I know you've been thinking about that too. Maybe we should start with that?

For linking control (i.e. controlling how shaders get assembled), I think some will prefer to control things from WESL and some will prefer to control things from TypeScript. Let's talk about how we can support both camps!

See #51 too.

  • Expose a minimal library to the library consumer that resolves the intermediate format into a spec-compliant WGSL code string.

I'm not sure if we can use WGSL as the pre-parsed form for shaders. It'd be nice! But wouldn't that preclude the WESL features like conditions or planned features like generics? Those features can trigger different WGSL code specialization at runtime. Maybe we could eventually design some kind of WGSL+annotations as the pre-parsed form..

@iwoplaza
Copy link

If we had a build tool that converts project and library WESL to a pre-parsed form, the pre-parsed form would be more free to evolve and optimize. I think that might be wiser than trying to stabilize a pre-parsed form for long term support..

Definitely 🚀. Each host language would most likely have to have its own pre-parsed form that can be properly ingested. For TypeScript, I believe embedding types into packages at build-time, and publishing the result would be the only optimal solution. Otherwise we would be tasking ourselves with traversing the dependency tree recursively, and providing the types by means of code-gen, which is not the best DX. We'll still need codegen for the app WESL, but JS runtimes can differ in where they store libraries (Node: node_modules, Deno: a global cache).

@iwoplaza
Copy link

I think the interop problem is probably the easier one of the two, ... Maybe we should start with that?

I'm working on a POC of that since yesterday, I'll share it once I have something to share 🙌

@iwoplaza
Copy link

Let's talk about how we can support both camps!

From what I can understand, dynamic links (ones determined by the host code) are explicitly defined in WESL source code via @link statements, right? If so, we can use host language's imports for static linking, and leave the dynamic links for runtime linking.

@mighdoll
Copy link
Contributor Author

Let's talk about how we can support both camps!

From what I can understand, dynamic links (ones determined by the host code) are explicitly defined in WESL source code via @link statements, right? If so, we can use host language's imports for static linking, and leave the dynamic links for runtime linking.

The @link design sketch is just a sketch. The field is pretty open for design ideas on how this ought to work.

@iwoplaza
Copy link

iwoplaza commented Nov 22, 2024

I'm not sure if we can use WGSL as the pre-parsed form for shaders

Let me clarify, the result of linking would be WGSL, the input (as in the app code and lib code) would be the pre-parsed format.

@mighdoll
Copy link
Contributor Author

flowchart LR
%% Nodes
    A(".wesl files")
    B("TypeScript Types")
    C("Preparsed Shaders")
    D("wgsl string")
    T>Type Tool]
    O>Opt Tool]
    P>Packager Tool]
    R>"App Bundler (opt)"]
    N[npm package]
    L>Linker]
    I[/IDE\]

%% Edge connections between nodes
    A --> R --> L --> D
    A --> P --> N --> R 
    A --> T --> B -.-> R
    A --> O --> C -.-> R 
    
%% styles
    classDef today fill:#7cbded;
    linkStyle default stroke-width:2;
    linkStyle 0,1,2,3,4,5 stroke:#7cbded,stroke-width:2;
    class A,D,P,N,L,R,I today;
Loading

Today we go from .wesl files directly to the linker (in blue).

I think we're discussing two new data structures.
(They're both together in the sample you provide, but I'm separating them here for clarity.)

  • TypeScript types describing the WESL.
  • An optimized format for shader code.

Many design questions that will be fun to work out together:

  • What specific use cases do we have in mind for these?
  • Which use cases are the most important and most achievable to try first?
  • What tools are used to contruct things and who runs them when?
  • How does generated type info feed back into the IDE?
  • What goes into an npm package?

@iwoplaza
Copy link

Exciting!
I see TypeGPU nowadays as an alternative syntax for WGSL that represents resources like structs, bind groups, buffers (etc) in a form that TypeScript can infer types from, without additional tooling (codegen, language server, extensions). I believe a properly constructed intermediate format of pre-parsed WESL could fulfill the same purpose, and be consumable by TypeScript frameworks/libs for WebGPU. I'll come back with a working prototype 🫡.

@iwoplaza
Copy link

Below is a repo containing an example module along with an example app using that module. I used our learnings from TypeGPU to solve the following problems:

  • Including transitive dependencies when linking.
  • Avoiding name clashes between WGSL definitions (functions, structs, etc.)
  • Providing type information in a form that the host language's language server can consume.
  • Showing how that type information can be used to create useful utilities, either within TypeScript libraries, or by the end users themselves with a hard dependency on those libraries (e.g., TypeGPU).

https://github.com/iwoplaza/wesl-package-example

To see the full IDE experience, it would be best to clone the repo, run the local setup described in the README, and hover over the interesting bits in the example app. Excited to discuss these problems and solutions further! 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

No branches or pull requests

3 participants