wesl-tooling 0.6.9 → 0.6.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,30 +1,30 @@
1
1
  {
2
2
  "name": "wesl-tooling",
3
- "version": "0.6.9",
3
+ "version": "0.6.12",
4
4
  "type": "module",
5
5
  "files": [
6
- "dist"
6
+ "src"
7
7
  ],
8
- "main": "./dist/index.js",
9
- "module": "./dist/index.js",
10
- "types": "./dist/index.d.ts",
8
+ "repository": "github:wgsl-tooling-wg/wesl-js",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./src/index.ts"
12
+ }
13
+ },
11
14
  "dependencies": {
12
- "glob": "^11.0.2",
15
+ "glob": "^11.0.3",
13
16
  "import-meta-resolve": "^4.1.0",
14
- "thimbleberry": "^0.2.10"
17
+ "toml": "^3.0.0"
15
18
  },
16
19
  "devDependencies": {
17
- "dependent_package": "x",
18
- "tsdown": "^0.11.12"
20
+ "dependent_package": "x"
19
21
  },
20
22
  "peerDependencies": {
21
- "wesl": "^0.6.9"
23
+ "wesl": "^0.6.49"
22
24
  },
23
25
  "scripts": {
24
- "build": "tsdown",
25
- "dev": "tsdown --watch",
26
- "typecheck": "tsgo",
27
- "test": "FORCE_COLOR=1 vitest",
28
- "test:once": "vitest run"
26
+ "test": "cross-env FORCE_COLOR=1 vitest",
27
+ "test:once": "vitest run",
28
+ "typecheck": "tsgo"
29
29
  }
30
30
  }
@@ -0,0 +1,110 @@
1
+ import * as fs from "node:fs";
2
+ import type { ModuleResolver, WeslAST } from "wesl";
3
+ import { moduleToRelativePath, normalizeDebugRoot, parseSrcModule } from "wesl";
4
+
5
+ /**
6
+ * Loads WESL modules from the filesystem on demand with caching.
7
+ *
8
+ * Resolves module paths like `package::foo::bar` to filesystem paths
9
+ * like `baseDir/foo/bar.wesl` or `baseDir/foo/bar.wgsl`.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const resolver = new FileModuleResolver("./shaders", "my-package");
14
+ * const ast = resolver.resolveModule("package::utils::helper");
15
+ * ```
16
+ */
17
+ export class FileModuleResolver implements ModuleResolver {
18
+ /** Cached parsed ASTs to avoid re-parsing the same module */
19
+ readonly astCache = new Map<string, WeslAST>();
20
+
21
+ /** Root directory containing shader source files */
22
+ readonly baseDir: string;
23
+
24
+ /** Package name that this resolver handles (in addition to generic "package") */
25
+ readonly packageName: string;
26
+
27
+ /** Optional root path for debug file paths (for browser-clickable errors) */
28
+ readonly debugWeslRoot?: string;
29
+
30
+ /**
31
+ * @param baseDir - Root directory containing shader source files
32
+ * @param packageName - Package name to resolve (defaults to "package")
33
+ * @param debugWeslRoot - Optional root path for debug file paths. If provided, error messages
34
+ * will show paths relative to this root (e.g., "shaders/foo.wesl") instead of absolute
35
+ * filesystem paths. This is needed for clickable errors in browser dev tools.
36
+ */
37
+ constructor(
38
+ baseDir: string,
39
+ packageName = "package",
40
+ debugWeslRoot?: string,
41
+ ) {
42
+ this.baseDir = baseDir;
43
+ this.packageName = packageName;
44
+ this.debugWeslRoot = debugWeslRoot;
45
+ }
46
+
47
+ /**
48
+ * Resolves and parses a module by its import path.
49
+ *
50
+ * Returns cached AST if available, otherwise loads from filesystem,
51
+ * parses, caches, and returns the AST. Returns undefined if module
52
+ * cannot be found.
53
+ *
54
+ * @param modulePath - Module path like "package::foo::bar"
55
+ * @returns Parsed AST or undefined if module not found
56
+ */
57
+ resolveModule(modulePath: string): WeslAST | undefined {
58
+ const cached = this.astCache.get(modulePath);
59
+ if (cached) return cached;
60
+
61
+ const sourceFile = this.tryExtensions(modulePath);
62
+ if (!sourceFile) return undefined;
63
+
64
+ const debugFilePath = this.debugWeslRoot
65
+ ? this.modulePathToDebugPath(modulePath)
66
+ : sourceFile.filePath;
67
+ const ast = parseSrcModule({
68
+ modulePath,
69
+ debugFilePath,
70
+ src: sourceFile.source,
71
+ });
72
+ this.astCache.set(modulePath, ast);
73
+ return ast;
74
+ }
75
+
76
+ /** Try .wesl first, then .wgsl */
77
+ private tryExtensions(modulePath: string) {
78
+ const basePath = this.moduleToFilePath(modulePath);
79
+ if (!basePath) return undefined;
80
+
81
+ for (const ext of [".wesl", ".wgsl"]) {
82
+ const filePath = basePath + ext;
83
+ const source = this.loadSource(filePath);
84
+ if (source) return { filePath, source };
85
+ }
86
+ return undefined;
87
+ }
88
+
89
+ private loadSource(filePath: string): string | undefined {
90
+ try {
91
+ return fs.readFileSync(filePath, "utf8");
92
+ } catch {
93
+ return undefined;
94
+ }
95
+ }
96
+
97
+ /** Convert module path (package::foo::bar) to filesystem path (baseDir/foo/bar) */
98
+ private moduleToFilePath(modulePath: string): string | undefined {
99
+ const relativePath = moduleToRelativePath(modulePath, this.packageName);
100
+ if (!relativePath) return undefined;
101
+ return `${this.baseDir}/${relativePath}`;
102
+ }
103
+
104
+ /** Convert module path to debug path for error messages */
105
+ private modulePathToDebugPath(modulePath: string): string {
106
+ const relative = moduleToRelativePath(modulePath, this.packageName) ?? "";
107
+ const root = normalizeDebugRoot(this.debugWeslRoot);
108
+ return root + relative + ".wesl";
109
+ }
110
+ }
@@ -0,0 +1,62 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { glob } from "glob";
4
+ import { findWeslToml } from "./LoadWeslToml.ts";
5
+
6
+ /**
7
+ * Load the wesl/wgsl shader sources.
8
+ *
9
+ * If baseDir or srcGlob are not provided, this function will attempt to read
10
+ * configuration from wesl.toml in the projectDir. If no wesl.toml exists,
11
+ * default values will be used.
12
+ *
13
+ * @param projectDir The project directory (typically cwd or directory containing package.json)
14
+ * @param baseDir Optional base directory for shaders (overrides wesl.toml if provided)
15
+ * @param srcGlob Optional glob pattern for shader files (overrides wesl.toml if provided)
16
+ */
17
+ export async function loadModules(
18
+ projectDir: string,
19
+ baseDir?: string,
20
+ srcGlob?: string,
21
+ ): Promise<Record<string, string>> {
22
+ // If baseDir or srcGlob not provided, load from wesl.toml
23
+ let resolvedBaseDir: string;
24
+ let resolvedSrcGlob: string;
25
+
26
+ if (!baseDir || !srcGlob) {
27
+ const tomlInfo = await findWeslToml(projectDir);
28
+ resolvedBaseDir = baseDir ?? tomlInfo.resolvedRoot;
29
+ resolvedSrcGlob = srcGlob ?? tomlInfo.toml.include[0]; // Use first glob pattern
30
+ } else {
31
+ resolvedBaseDir = baseDir;
32
+ resolvedSrcGlob = srcGlob;
33
+ }
34
+
35
+ const foundFiles = await glob(`${resolvedSrcGlob}`, {
36
+ cwd: projectDir,
37
+ ignore: "node_modules/**",
38
+ });
39
+ const shaderFiles = foundFiles.map(f => path.resolve(projectDir, f));
40
+ const promisedSrcs = shaderFiles.map(f =>
41
+ fs.readFile(f, { encoding: "utf8" }),
42
+ );
43
+ const src = await Promise.all(promisedSrcs);
44
+ if (src.length === 0) {
45
+ throw new Error(`no WGSL/WESL files found in ${resolvedSrcGlob}`);
46
+ }
47
+ const baseDirAbs = path.resolve(projectDir, resolvedBaseDir);
48
+ const relativePaths = shaderFiles.map(p =>
49
+ path.relative(baseDirAbs, path.resolve(p)),
50
+ );
51
+
52
+ // Normalize Windows paths and line endings
53
+ const normalPaths = relativePaths.map(p => p.replace(/\\/g, "/"));
54
+ const normalSrc = src.map(s => s.replace(/\r\n/g, "\n"));
55
+
56
+ const moduleEntries = zip(normalPaths, normalSrc);
57
+ return Object.fromEntries(moduleEntries);
58
+ }
59
+
60
+ export function zip<A, B>(as: A[], bs: B[]): [A, B][] {
61
+ return as.map((a, i) => [a, bs[i]]);
62
+ }
@@ -0,0 +1,104 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import toml from "toml";
4
+
5
+ /** Configuration from wesl.toml */
6
+ export interface WeslToml {
7
+ /** WESL edition (e.g. "unstable_2025") */
8
+ edition: string;
9
+
10
+ /** glob patterns to find .wesl/.wgsl files. Relative to the toml directory. */
11
+ include: string[];
12
+
13
+ /** base directory for wesl files. Relative to the toml directory. */
14
+ root: string;
15
+
16
+ /** glob patterns to exclude directories. */
17
+ exclude?: string[];
18
+
19
+ /** package manager ("npm" or "cargo") */
20
+ "package-manager"?: string;
21
+
22
+ /** names of directly referenced wesl shader packages (e.g. npm package names), or "auto" for auto-detection */
23
+ dependencies?: string[] | string;
24
+ }
25
+
26
+ /** Information about the loaded wesl.toml file and its location */
27
+ export interface WeslTomlInfo {
28
+ /** The path to the toml file, relative to the cwd, undefined if no toml file */
29
+ tomlFile: string | undefined;
30
+
31
+ /** The absolute path to the directory that contains the toml.
32
+ * Paths inside the toml are relative to this. */
33
+ tomlDir: string;
34
+
35
+ /** The wesl root, relative to the projectDir.
36
+ * This lets loadModules do `path.resolve(projectDir, resolvedRoot)` */
37
+ resolvedRoot: string;
38
+
39
+ /** The underlying toml file */
40
+ toml: WeslToml;
41
+ }
42
+
43
+ /** Default configuration when no wesl.toml is found */
44
+ export const defaultWeslToml: WeslToml = {
45
+ edition: "unstable_2025",
46
+ include: ["shaders/**/*.w[eg]sl"],
47
+ root: "shaders",
48
+ dependencies: "auto",
49
+ };
50
+
51
+ /**
52
+ * Load and parse a wesl.toml file from the fs.
53
+ * Provide default values for any required WeslToml fields.
54
+ */
55
+ export async function loadWeslToml(tomlFile: string): Promise<WeslToml> {
56
+ const tomlString = await fs.readFile(tomlFile, "utf-8");
57
+ const parsed = toml.parse(tomlString) as WeslToml;
58
+ const weslToml = { ...defaultWeslToml, ...parsed };
59
+ return weslToml;
60
+ }
61
+
62
+ /**
63
+ * Find and load the wesl.toml file, or use defaults if not found
64
+ *
65
+ * @param projectDir The directory to search for wesl.toml (typically cwd or project root)
66
+ * @param specifiedToml Optional explicit path to a toml file
67
+ * @returns Information about the loaded TOML configuration
68
+ */
69
+ export async function findWeslToml(
70
+ projectDir: string,
71
+ specifiedToml?: string,
72
+ ): Promise<WeslTomlInfo> {
73
+ // find the wesl.toml file if it exists
74
+ let tomlFile: string | undefined;
75
+ if (specifiedToml) {
76
+ await fs.access(specifiedToml);
77
+ tomlFile = specifiedToml;
78
+ } else {
79
+ const tomlPath = path.join(projectDir, "wesl.toml");
80
+ tomlFile = await fs
81
+ .access(tomlPath)
82
+ .then(() => tomlPath)
83
+ .catch(() => {
84
+ return undefined;
85
+ });
86
+ }
87
+
88
+ // load the toml contents
89
+ let parsedToml: WeslToml;
90
+ let tomlDir: string;
91
+ if (tomlFile) {
92
+ parsedToml = await loadWeslToml(tomlFile);
93
+ tomlDir = path.dirname(tomlFile);
94
+ } else {
95
+ parsedToml = defaultWeslToml;
96
+ tomlDir = projectDir;
97
+ }
98
+
99
+ const tomlToWeslRoot = path.resolve(tomlDir, parsedToml.root);
100
+ const projectDirAbs = path.resolve(projectDir);
101
+ const resolvedRoot = path.relative(projectDirAbs, tomlToWeslRoot);
102
+
103
+ return { tomlFile, tomlDir, resolvedRoot, toml: parsedToml };
104
+ }
@@ -0,0 +1,59 @@
1
+ import { resolve } from "import-meta-resolve";
2
+ import { npmNameVariations } from "wesl";
3
+
4
+ /** Find longest resolvable npm subpath from WESL module path segments.
5
+ *
6
+ * A WESL statement containing a WESL module path like 'import foo__bar::baz::elem;' references
7
+ * an npm package, an export within that package, a module within the WeslBundle,
8
+ * and an element within the WESL module.
9
+ * This function returns the npm package and export portion from the module path.
10
+ * The return value is usable to dynamically import the corresponding weslBundle.js file.
11
+ *
12
+ * Translation from a WESL module path to an npm package path involves:
13
+ * - Mapping WESL package names to their npm counterparts (e.g., 'foo__bar' -> '@foo/bar')
14
+ * - Probing to find the longest valid export subpath within the package
15
+ * - package.json allows export subpaths, so 'mypkg::gpu' could be 'mypkg/gpu' or just 'mypkg' in npm
16
+ * - Probing to handle variations in package naming
17
+ * - foo_bar could be foo-bar in npm
18
+ *
19
+ * Note that the resolution is based on package.json.
20
+ * The resolved file itself may not exist yet. (e.g. dist/weslBundle.js may not have been built yet)
21
+ *
22
+ * @param mPath - Module path segments
23
+ * @param importerURL - Base URL for resolution (e.g., 'file:///path/to/project/')
24
+ * @returns Longest resolvable subpath (e.g., 'foo/bar/baz' or 'foo')
25
+ */
26
+ export function npmResolveWESL(
27
+ mPath: string[],
28
+ importerURL: string,
29
+ ): string | undefined {
30
+ // Try longest subpaths first
31
+ for (const subPath of exportSubpaths(mPath)) {
32
+ // Try npm name variations to handle sanitized package names
33
+ for (const npmPath of npmNameVariations(subPath)) {
34
+ if (tryResolve(npmPath, importerURL)) {
35
+ return npmPath;
36
+ }
37
+ }
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ /** Try Node.js module resolution.
43
+ * @return undefined if unresolvable. */
44
+ function tryResolve(path: string, importerURL: string): string | undefined {
45
+ try {
46
+ return resolve(path, importerURL);
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ }
51
+
52
+ /** Yield possible export subpaths from module path, longest first.
53
+ * Drops the last segment (element name) and iterates down. */
54
+ function* exportSubpaths(mPath: string[]): Generator<string> {
55
+ const longest = mPath.length - 1;
56
+ for (let i = longest; i >= 0; i--) {
57
+ yield mPath.slice(0, i).join("/");
58
+ }
59
+ }
@@ -0,0 +1,104 @@
1
+ import { pathToFileURL } from "node:url";
2
+ import { resolve } from "import-meta-resolve";
3
+ import type { WeslBundle } from "wesl";
4
+ import {
5
+ filterMap,
6
+ findUnboundIdents,
7
+ RecordResolver,
8
+ WeslParseError,
9
+ } from "wesl";
10
+ import { npmResolveWESL } from "./NpmResolver.ts";
11
+
12
+ /**
13
+ * Find package dependencies in WESL source files.
14
+ *
15
+ * Parses sources and partially binds identifiers to reveal unresolved package
16
+ * references. Returns the longest resolvable npm subpath for each dependency.
17
+ *
18
+ * For example, 'foo::bar::baz' could resolve to:
19
+ * - 'foo/bar' (package foo, export './bar' bundle)
20
+ * - 'foo' (package foo, default export)
21
+ *
22
+ * @param weslSrc - Record of WESL source files by path
23
+ * @param projectDir - Project directory for resolving package imports
24
+ * @returns Dependency paths in npm format (e.g., 'foo/bar', 'foo')
25
+ */
26
+ export function parseDependencies(
27
+ weslSrc: Record<string, string>,
28
+ projectDir: string,
29
+ ): string[] {
30
+ let resolver: RecordResolver;
31
+ try {
32
+ resolver = new RecordResolver(weslSrc);
33
+ } catch (e: any) {
34
+ if (e.cause instanceof WeslParseError) {
35
+ console.error(e.message, "\n");
36
+ return [];
37
+ }
38
+ throw e;
39
+ }
40
+
41
+ const unbound = findUnboundIdents(resolver);
42
+ if (!unbound) return [];
43
+
44
+ // Filter: skip builtins (1 segment) and linker virtuals ('constants')
45
+ const pkgRefs = unbound.filter(
46
+ modulePath => modulePath.length > 1 && modulePath[0] !== "constants",
47
+ );
48
+ if (pkgRefs.length === 0) return [];
49
+
50
+ const projectURL = projectDirURL(projectDir);
51
+ const deps = filterMap(pkgRefs, mPath => npmResolveWESL(mPath, projectURL));
52
+ const uniqueDeps = [...new Set(deps)];
53
+
54
+ return uniqueDeps;
55
+ }
56
+
57
+ /**
58
+ * Load WeslBundle instances referenced by WESL sources.
59
+ *
60
+ * Parses sources to find external module references, then dynamically imports
61
+ * the corresponding weslBundle.js files.
62
+ *
63
+ * @param weslSrc - Record of WESL source files by path
64
+ * @param projectDir - Project directory for resolving imports
65
+ * @param packageName - Optional current package name
66
+ * @param includeCurrentPackage - Include current package in results (default: false)
67
+ * @returns Loaded WeslBundle instances
68
+ */
69
+ export async function dependencyBundles(
70
+ weslSrc: Record<string, string>,
71
+ projectDir: string,
72
+ packageName?: string,
73
+ includeCurrentPackage = false,
74
+ ): Promise<WeslBundle[]> {
75
+ const deps = parseDependencies(weslSrc, projectDir);
76
+ const filteredDeps = includeCurrentPackage
77
+ ? deps
78
+ : otherPackages(deps, packageName);
79
+ const projectURL = projectDirURL(projectDir);
80
+ const bundles = filteredDeps.map(async dep => {
81
+ const url = resolve(dep, projectURL);
82
+ const module = await import(url);
83
+ return module.default;
84
+ });
85
+
86
+ return await Promise.all(bundles);
87
+ }
88
+
89
+ /** Exclude current package from dependency list. */
90
+ function otherPackages(deps: string[], packageName?: string): string[] {
91
+ if (!packageName) return deps;
92
+ return deps.filter(
93
+ dep => dep !== packageName && !dep.startsWith(`${packageName}/`),
94
+ );
95
+ }
96
+
97
+ /** Normalize project directory to file:// URL with trailing slash. */
98
+ function projectDirURL(projectDir: string): string {
99
+ if (projectDir.startsWith("file://")) {
100
+ return projectDir.endsWith("/") ? projectDir : `${projectDir}/`;
101
+ }
102
+ const fileUrl = pathToFileURL(projectDir).href;
103
+ return fileUrl.endsWith("/") ? fileUrl : `${fileUrl}/`;
104
+ }
@@ -0,0 +1,64 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+
5
+ /**
6
+ * Resolves a project directory by searching upward for package.json or wesl.toml.
7
+ *
8
+ * @param startPath - Optional starting path (file:// URL or filesystem path).
9
+ * If a file URL is provided, uses its directory.
10
+ * If omitted or falsy, defaults to process.cwd().
11
+ * @returns file:// URL string pointing to the project directory
12
+ * (the first ancestor containing package.json or wesl.toml, or the start directory)
13
+ */
14
+ export async function resolveProjectDir(startPath?: string): Promise<string> {
15
+ let dir: string;
16
+
17
+ if (!startPath) {
18
+ dir = process.cwd();
19
+ } else if (startPath.startsWith("file://")) {
20
+ // Convert file:// URL to path, then get its directory
21
+ const fsPath = fileURLToPath(startPath);
22
+ dir = (await isFile(fsPath)) ? path.dirname(fsPath) : fsPath;
23
+ } else {
24
+ // Treat as filesystem path
25
+ dir = (await isFile(startPath)) ? path.dirname(startPath) : startPath;
26
+ }
27
+
28
+ // Search upward for package.json or wesl.toml
29
+ let current = path.resolve(dir);
30
+ while (true) {
31
+ const hasPkgJson = await fileExists(path.join(current, "package.json"));
32
+ const hasWeslToml = await fileExists(path.join(current, "wesl.toml"));
33
+
34
+ if (hasPkgJson || hasWeslToml) {
35
+ return pathToFileURL(current).href;
36
+ }
37
+
38
+ const parent = path.dirname(current);
39
+ if (parent === current) {
40
+ // Reached filesystem root without finding package.json or wesl.toml
41
+ // Return the original dir as file:// URL
42
+ return pathToFileURL(dir).href;
43
+ }
44
+ current = parent;
45
+ }
46
+ }
47
+
48
+ async function fileExists(filePath: string): Promise<boolean> {
49
+ try {
50
+ await fs.access(filePath);
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ async function isFile(fsPath: string): Promise<boolean> {
58
+ try {
59
+ const stat = await fs.stat(fsPath);
60
+ return stat.isFile();
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
package/src/Version.ts ADDED
@@ -0,0 +1,22 @@
1
+ /** Read package.json from a directory.
2
+ * @param projectDir - file:// URL string to directory containing package.json
3
+ * @returns the parsed package.json contents */
4
+ export async function readPackageJson(
5
+ projectDir: string,
6
+ ): Promise<Record<string, any>> {
7
+ const baseUrl = projectDir.endsWith("/") ? projectDir : `${projectDir}/`;
8
+ const pkgJsonPath = new URL("package.json", baseUrl);
9
+ const pkgModule = await import(pkgJsonPath.href, { with: { type: "json" } });
10
+ return pkgModule.default;
11
+ }
12
+
13
+ /**
14
+ * @param projectDir - file:// URL string to directory containing package.json
15
+ * @returns the 'version' field from the package.json in the `projectDir`
16
+ */
17
+ export async function versionFromPackageJson(
18
+ projectDir: string,
19
+ ): Promise<string> {
20
+ const pkg = await readPackageJson(projectDir);
21
+ return pkg.version;
22
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./FileModuleResolver.ts";
2
+ export * from "./LoadModules.ts";
3
+ export * from "./LoadWeslToml.ts";
4
+ export * from "./ParseDependencies.ts";
5
+ export * from "./ResolveProjectDir.ts";
6
+ export * from "./Version.ts";
package/dist/index.d.ts DELETED
@@ -1,118 +0,0 @@
1
- import { VirtualLibraryFn, WeslBundle } from "wesl";
2
- import { WgslElementType, WgslElementType as WgslElementType$1 } from "thimbleberry";
3
-
4
- //#region src/ParseDependencies.d.ts
5
- /**
6
- * Find the wesl package dependencies in a set of WESL files
7
- * (for packaging WESL files into a library)
8
- *
9
- * Parse the WESL files and partially bind the identifiers,
10
- * returning any identifiers that are not succesfully bound.
11
- * Those identifiers are the package dependencies.
12
- *
13
- * The dependency might be a default export bundle or
14
- * a named export bundle. e.g. for 'foo::bar::baz', it could be
15
- * . package foo, export '.' bundle, module bar
16
- * . package foo, export './bar' bundle, element baz
17
- * . package foo, export './bar/baz' bundle, module lib.wesl, element baz
18
- * To distinguish these, we node resolve the longest path we can.
19
- */
20
- /**
21
- * Find the wesl package dependencies in a set of WESL files
22
- * (for packaging WESL files into a library)
23
- *
24
- * Parse the WESL files and partially bind the identifiers,
25
- * returning any identifiers that are not succesfully bound.
26
- * Those identifiers are the package dependencies.
27
- *
28
- * The dependency might be a default export bundle or
29
- * a named export bundle. e.g. for 'foo::bar::baz', it could be
30
- * . package foo, export '.' bundle, module bar
31
- * . package foo, export './bar' bundle, element baz
32
- * . package foo, export './bar/baz' bundle, module lib.wesl, element baz
33
- * To distinguish these, we node resolve the longest path we can.
34
- */
35
- declare function parseDependencies(weslSrc: Record<string, string>, projectDir: string): string[];
36
- /** @return WeslBundle instances referenced by wesl sources
37
- *
38
- * Parse the WESL files to find references to external WESL modules,
39
- * and then load those modules (weslBundle.js files) using node dynamic imports.
40
- */
41
- declare function dependencyBundles(weslSrc: Record<string, string>, projectDir: string): Promise<WeslBundle[]>;
42
-
43
- //#endregion
44
- //#region src/SimpleComputeShader.d.ts
45
- interface CompileShaderParams {
46
- /** The project directory, used for resolving dependencies. */
47
- projectDir: string;
48
- /** The GPUDevice to use for shader compilation. */
49
- device: GPUDevice;
50
- /** The WGSL/WESL shader source code. */
51
- src: string;
52
- /** Optional conditions for shader compilation. */
53
- conditions?: Record<string, boolean>;
54
- /** Optional virtual libraries to include in the shader. */
55
- virtualLibs?: Record<string, VirtualLibraryFn>;
56
- }
57
- /**
58
- * Compiles a single WESL shader source string into a GPUShaderModule for testing
59
- * with automatic package detection.
60
- *
61
- * Parses the shader source to find references to wesl packages, and
62
- * then searches installed npm packages to find the appropriate npm package
63
- * bundle to include in the link.
64
- *
65
- * @param projectDir - The project directory, used for resolving dependencies.
66
- * @param device - The GPUDevice to use for shader compilation.
67
- * @param src - The WESL shader source code.
68
- * @returns A Promise that resolves to the compiled GPUShaderModule.
69
- */
70
- declare function compileShader(params: CompileShaderParams): Promise<GPUShaderModule>;
71
- /**
72
- * Transpiles and runs a simple compute shader on the GPU for testing.
73
- *
74
- * A storage buffer is available for the shader to write test results.
75
- * `test::results[0]` is the first element of the buffer in wesl.
76
- * After execution the storage buffer is copied back to the CPU and returned
77
- * for test validation.
78
- *
79
- * Shader libraries mentioned in the shader source are attached automatically
80
- * if they are in node_modules.
81
- *
82
- * @param module - The compiled GPUShaderModule containing the compute shader.
83
- * The shader is invoked once.
84
- * @param resultFormat - format for interpreting the result buffer data. (default u32)
85
- * @returns storage result array (typically four numbers if the buffer format is u32 or f32)
86
- */
87
- declare function testComputeShader(projectDir: string, gpu: GPU, src: string, resultFormat: WgslElementType$1, conditions?: Record<string, boolean>): Promise<number[]>;
88
- /**
89
- * Transpiles and runs a simple compute shader on the GPU for testing.
90
- *
91
- * a 16 byte storage buffer is available for the shader at `@group(0) @binding(0)`.
92
- * Compute shaders can write test results into the buffer.
93
- * After execution the storage buffer is copied back to the CPU and returned
94
- * for test validation.
95
- *
96
- * Shader libraries mentioned in the shader source are attached automatically
97
- * if they are in node_modules.
98
- *
99
- * @param module - The compiled GPUShaderModule containing the compute shader.
100
- * The shader is invoked once.
101
- * @param resultFormat - format for interpreting the result buffer data. (default u32)
102
- * @returns storage result array
103
- */
104
- declare function runSimpleComputePipeline(device: GPUDevice, module: GPUShaderModule, resultFormat?: WgslElementType$1): Promise<number[]>;
105
-
106
- //#endregion
107
- //#region src/Version.d.ts
108
- /** @returns the version from the package.json in the provided directory */
109
- declare function versionFromPackageJson(projectDir: string): Promise<string>;
110
-
111
- //#endregion
112
- //#region src/LoadModules.d.ts
113
- /** load the wesl/wgsl shader sources */
114
- declare function loadModules(projectDir: string, baseDir: string, srcGlob: string): Promise<Record<string, string>>;
115
- declare function zip<A, B>(as: A[], bs: B[]): [A, B][];
116
-
117
- //#endregion
118
- export { CompileShaderParams, WgslElementType, compileShader, dependencyBundles, loadModules, parseDependencies, runSimpleComputePipeline, testComputeShader, versionFromPackageJson, zip };
package/dist/index.js DELETED
@@ -1,235 +0,0 @@
1
- import path from "node:path";
2
- import { pathToFileURL } from "node:url";
3
- import { resolve } from "import-meta-resolve";
4
- import { WeslParseError, filterMap, findUnboundIdents, link, parseIntoRegistry, parsedRegistry, requestWeslDevice } from "wesl";
5
- import { copyBuffer, elementStride } from "thimbleberry";
6
- import fs from "node:fs/promises";
7
- import { glob } from "glob";
8
-
9
- //#region src/ParseDependencies.ts
10
- /**
11
- * Find the wesl package dependencies in a set of WESL files
12
- * (for packaging WESL files into a library)
13
- *
14
- * Parse the WESL files and partially bind the identifiers,
15
- * returning any identifiers that are not succesfully bound.
16
- * Those identifiers are the package dependencies.
17
- *
18
- * The dependency might be a default export bundle or
19
- * a named export bundle. e.g. for 'foo::bar::baz', it could be
20
- * . package foo, export '.' bundle, module bar
21
- * . package foo, export './bar' bundle, element baz
22
- * . package foo, export './bar/baz' bundle, module lib.wesl, element baz
23
- * To distinguish these, we node resolve the longest path we can.
24
- */
25
- function parseDependencies(weslSrc, projectDir) {
26
- const registry = parsedRegistry();
27
- try {
28
- parseIntoRegistry(weslSrc, registry);
29
- } catch (e) {
30
- if (e.cause instanceof WeslParseError) console.error(e.message, "\n");
31
- else throw e;
32
- }
33
- const unbound = findUnboundIdents(registry);
34
- if (!unbound) return [];
35
- const pkgRefs = unbound.filter((modulePath) => modulePath.length > 1);
36
- if (pkgRefs.length === 0) return [];
37
- const fullProjectDir = path.resolve(path.join(projectDir, "foo"));
38
- const projectURL = pathToFileURL(fullProjectDir).href;
39
- const deps = filterMap(pkgRefs, (mPath) => unboundToDependency(mPath, projectURL));
40
- const uniqueDeps = [...new Set(deps)];
41
- return uniqueDeps;
42
- }
43
- /**
44
- * Find the longest resolvable npm subpath from a module path.
45
- *
46
- * @param mPath module path, e.g. ['foo', 'bar', 'baz', 'elem']
47
- * @param importerURL URL of the importer, e.g. 'file:///path/to/project/foo/bar/baz.wesl' (doesn't need to be a real file)
48
- * @returns longest resolvable subpath of mPath, e.g. 'foo/bar/baz' or 'foo/bar'
49
- */
50
- function unboundToDependency(mPath, importerURL) {
51
- return exportSubpaths(mPath).find((subPath) => tryResolve(subPath, importerURL));
52
- }
53
- /** Try to resolve a path using node's resolve algorithm.
54
- * @return the resolved path */
55
- function tryResolve(path$1, importerURL) {
56
- try {
57
- return resolve(path$1, importerURL);
58
- } catch {
59
- return void 0;
60
- }
61
- }
62
- /**
63
- * Yield possible export entry subpaths from module path
64
- * longest subpath first.
65
- */
66
- function* exportSubpaths(mPath) {
67
- const longest = mPath.length - 1;
68
- for (let i = longest; i >= 0; i--) {
69
- const subPath = mPath.slice(0, i).join("/");
70
- yield subPath;
71
- }
72
- }
73
- /** @return WeslBundle instances referenced by wesl sources
74
- *
75
- * Parse the WESL files to find references to external WESL modules,
76
- * and then load those modules (weslBundle.js files) using node dynamic imports.
77
- */
78
- async function dependencyBundles(weslSrc, projectDir) {
79
- const deps = parseDependencies(weslSrc, projectDir);
80
- const bundles = deps.map(async (dep) => {
81
- const url = resolve(dep, projectDir);
82
- const module = await import(url);
83
- return module.default;
84
- });
85
- return await Promise.all(bundles);
86
- }
87
-
88
- //#endregion
89
- //#region src/SimpleComputeShader.ts
90
- const resultBufferSize = 16;
91
- /**
92
- * Compiles a single WESL shader source string into a GPUShaderModule for testing
93
- * with automatic package detection.
94
- *
95
- * Parses the shader source to find references to wesl packages, and
96
- * then searches installed npm packages to find the appropriate npm package
97
- * bundle to include in the link.
98
- *
99
- * @param projectDir - The project directory, used for resolving dependencies.
100
- * @param device - The GPUDevice to use for shader compilation.
101
- * @param src - The WESL shader source code.
102
- * @returns A Promise that resolves to the compiled GPUShaderModule.
103
- */
104
- async function compileShader(params) {
105
- const { projectDir, device, src, conditions, virtualLibs } = params;
106
- const weslSrc = { main: src };
107
- const libs = await dependencyBundles(weslSrc, projectDir);
108
- const linked = await link({
109
- weslSrc,
110
- libs,
111
- virtualLibs,
112
- conditions
113
- });
114
- return device.createShaderModule({ code: linked.dest });
115
- }
116
- /**
117
- * Transpiles and runs a simple compute shader on the GPU for testing.
118
- *
119
- * A storage buffer is available for the shader to write test results.
120
- * `test::results[0]` is the first element of the buffer in wesl.
121
- * After execution the storage buffer is copied back to the CPU and returned
122
- * for test validation.
123
- *
124
- * Shader libraries mentioned in the shader source are attached automatically
125
- * if they are in node_modules.
126
- *
127
- * @param module - The compiled GPUShaderModule containing the compute shader.
128
- * The shader is invoked once.
129
- * @param resultFormat - format for interpreting the result buffer data. (default u32)
130
- * @returns storage result array (typically four numbers if the buffer format is u32 or f32)
131
- */
132
- async function testComputeShader(projectDir, gpu, src, resultFormat, conditions = {}) {
133
- const adapter = await gpu.requestAdapter();
134
- const device = await requestWeslDevice(adapter);
135
- try {
136
- const arraySize = resultBufferSize / elementStride(resultFormat);
137
- const arrayType = `array<${resultFormat}, ${arraySize}>`;
138
- const virtualLibs = { test: () => `@group(0) @binding(0) var <storage, read_write> results: ${arrayType};` };
139
- const params = {
140
- projectDir,
141
- device,
142
- src,
143
- conditions,
144
- virtualLibs
145
- };
146
- const module = await compileShader(params);
147
- const result = await runSimpleComputePipeline(device, module, resultFormat);
148
- return result;
149
- } finally {
150
- device.destroy();
151
- }
152
- }
153
- /**
154
- * Transpiles and runs a simple compute shader on the GPU for testing.
155
- *
156
- * a 16 byte storage buffer is available for the shader at `@group(0) @binding(0)`.
157
- * Compute shaders can write test results into the buffer.
158
- * After execution the storage buffer is copied back to the CPU and returned
159
- * for test validation.
160
- *
161
- * Shader libraries mentioned in the shader source are attached automatically
162
- * if they are in node_modules.
163
- *
164
- * @param module - The compiled GPUShaderModule containing the compute shader.
165
- * The shader is invoked once.
166
- * @param resultFormat - format for interpreting the result buffer data. (default u32)
167
- * @returns storage result array
168
- */
169
- async function runSimpleComputePipeline(device, module, resultFormat) {
170
- const bgLayout = device.createBindGroupLayout({ entries: [{
171
- binding: 0,
172
- visibility: GPUShaderStage.COMPUTE,
173
- buffer: { type: "storage" }
174
- }] });
175
- const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bgLayout] });
176
- const pipeline = device.createComputePipeline({
177
- layout: pipelineLayout,
178
- compute: { module }
179
- });
180
- const storageBuffer = device.createBuffer({
181
- label: "storage",
182
- size: resultBufferSize,
183
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
184
- });
185
- const bindGroup = device.createBindGroup({
186
- layout: bgLayout,
187
- entries: [{
188
- binding: 0,
189
- resource: { buffer: storageBuffer }
190
- }]
191
- });
192
- const commands = device.createCommandEncoder();
193
- const pass = commands.beginComputePass();
194
- pass.setPipeline(pipeline);
195
- pass.setBindGroup(0, bindGroup);
196
- pass.dispatchWorkgroups(1);
197
- pass.end();
198
- device.queue.submit([commands.finish()]);
199
- const data = await copyBuffer(device, storageBuffer, resultFormat);
200
- return data;
201
- }
202
-
203
- //#endregion
204
- //#region src/Version.ts
205
- /** @returns the version from the package.json in the provided directory */
206
- async function versionFromPackageJson(projectDir) {
207
- const pkgJsonPath = new URL("./package.json", projectDir);
208
- const pkgModule = await import(pkgJsonPath.href, { with: { type: "json" } });
209
- const version = pkgModule.default.version;
210
- return version;
211
- }
212
-
213
- //#endregion
214
- //#region src/LoadModules.ts
215
- /** load the wesl/wgsl shader sources */
216
- async function loadModules(projectDir, baseDir, srcGlob) {
217
- const foundFiles = await glob(`${srcGlob}`, {
218
- cwd: projectDir,
219
- ignore: "node_modules/**"
220
- });
221
- const shaderFiles = foundFiles.map((f) => path.resolve(projectDir, f));
222
- const promisedSrcs = shaderFiles.map((f) => fs.readFile(f, { encoding: "utf8" }));
223
- const src = await Promise.all(promisedSrcs);
224
- if (src.length === 0) throw new Error(`no WGSL/WESL files found in ${srcGlob}`);
225
- const baseDirAbs = path.resolve(projectDir, baseDir);
226
- const relativePaths = shaderFiles.map((p) => path.relative(baseDirAbs, path.resolve(p)));
227
- const moduleEntries = zip(relativePaths, src);
228
- return Object.fromEntries(moduleEntries);
229
- }
230
- function zip(as, bs) {
231
- return as.map((a, i) => [a, bs[i]]);
232
- }
233
-
234
- //#endregion
235
- export { compileShader, dependencyBundles, loadModules, parseDependencies, runSimpleComputePipeline, testComputeShader, versionFromPackageJson, zip };