wesl-plugin 0.6.47 → 0.6.49

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/README.md CHANGED
@@ -1,109 +1,135 @@
1
- # **WESL Plugin**
1
+ # WESL Plugin
2
2
 
3
3
  [![NPM Version](https://img.shields.io/npm/v/wesl-plugin)](https://www.npmjs.com/package/wesl-plugin)
4
4
  [![Static Badge](https://img.shields.io/badge/Read%20the%20-Docs-blue)](https://wesl-lang.dev/)
5
5
 
6
- > The wesl-plugin handles `.wesl` and `.wgsl` files
7
- in JavaScript bundlers to make using webgpu shaders more convenient.
6
+ Bundler plugin for importing `.wesl` and `.wgsl` shader files in JavaScript/TypeScript.
8
7
 
9
- Plugin features are accessed from JavaScript and TypeScript with `import` statements:
8
+ ## Install
10
9
 
11
- ```ts
12
- import linkConfig from "./shaders/app.wesl?link";
13
10
  ```
14
-
15
- ```ts
16
- import linkConfig from "./shaders/app.wesl?static";
11
+ npm install wesl wesl-plugin
17
12
  ```
18
13
 
19
- Each plugin ?feature is enabled by its own wesl-plugin [extension](#extensions).
14
+ ## Quick Start
20
15
 
21
- ## Install
16
+ ### Build-time linking (?static)
17
+
18
+ Link shaders at build time for the smallest application bundle size.
19
+
20
+ ```ts
21
+ // vite.config.ts
22
+ import { staticBuildExtension } from "wesl-plugin";
23
+ import viteWesl from "wesl-plugin/vite";
22
24
 
25
+ export default {
26
+ plugins: [viteWesl({ extensions: [staticBuildExtension] })]
27
+ };
23
28
  ```
24
- npm install wesl
25
- npm install wesl-plugin
29
+
30
+ ```ts
31
+ // app.ts
32
+ import wgsl from "./shaders/main.wesl?static";
33
+
34
+ const module = device.createShaderModule({ code: wgsl });
26
35
  ```
27
36
 
28
- ### Vite Configuration
37
+ ### Runtime linking (?link)
29
38
 
30
- Add the wesl-plugin along with any selected extensions to `vite.config.ts`:
39
+ Link shaders at runtime when you need dynamic conditions or constants:
31
40
 
32
41
  ```ts
33
- import { UserConfig } from "vite";
34
- import weslPlugin from "wesl-plugin/vite";
42
+ // vite.config.ts
35
43
  import { linkBuildExtension } from "wesl-plugin";
44
+ import viteWesl from "wesl-plugin/vite";
36
45
 
37
- const config: UserConfig = {
38
- plugins: [ weslPlugin({ extensions: [linkBuildExtension] }) ],
46
+ export default {
47
+ plugins: [viteWesl({ extensions: [linkBuildExtension] })]
39
48
  };
49
+ ```
50
+
51
+ ```ts
52
+ // app.ts
53
+ import { link } from "wesl";
54
+ import shaderConfig from "./shaders/main.wesl?link";
55
+
56
+ const linked = await link({
57
+ ...shaderConfig,
58
+ conditions: { MOBILE: isMobileGPU },
59
+ constants: { num_lights: 4 }
60
+ });
40
61
 
41
- export default config;
62
+ const module = linked.createShaderModule(device, {});
42
63
  ```
43
64
 
44
- In your JavaScript or TypeScript program you can then import
45
- wesl or wgsl shaders with a `?link` suffix and link them into WGSL at runtime.
65
+ ## Other Bundlers
46
66
 
47
67
  ```ts
48
- import linkConfig from "./shaders/app.wesl?link";
49
-
50
- function makeShaders() {
51
- const vertShader = await link({
52
- ...linkConfig,
53
- rootModuleName: "myVerts.wesl",
54
- conditions: {mobileGPU: true}
55
- });
56
- const computeShader = await link({
57
- ...linkConfig,
58
- rootModuleName: "myCompute.wesl",
59
- constants: {num_lights: 1}
60
- });
61
- }
62
-
68
+ import viteWesl from "wesl-plugin/vite";
69
+ import esbuildWesl from "wesl-plugin/esbuild";
70
+ import rollupWesl from "wesl-plugin/rollup";
71
+ import webpackWesl from "wesl-plugin/webpack";
72
+ // Also: nuxt, farm, rspack, astro
63
73
  ```
64
74
 
65
- ### Other Bundlers
75
+ ## Extensions
76
+
77
+ Extensions enable different import suffixes:
78
+
79
+ | Extension | Suffix | Output | Use Case |
80
+ |-----------|--------|--------|----------|
81
+ | `staticBuildExtension` | `?static` | WGSL string | Build-time linking, simplest |
82
+ | `linkBuildExtension` | `?link` | LinkParams object | Runtime conditions/constants |
66
83
 
67
- The wesl-plugin is available for many popular bundlers:
84
+ ### Combining Extensions
68
85
 
69
- ``` ts
70
- import weslPlugin from "wesl-plugin/esbuild";
71
- import weslPlugin from "wesl-plugin/rollup";
72
- import weslPlugin from "wesl-plugin/webpack";
73
- import weslPlugin from "wesl-plugin/nuxt";
74
- import weslPlugin from "wesl-plugin/farm";
75
- import weslPlugin from "wesl-plugin/rpack";
76
- // etc.
86
+ ```ts
87
+ import { staticBuildExtension, linkBuildExtension } from "wesl-plugin";
88
+ import viteWesl from "wesl-plugin/vite";
89
+
90
+ export default {
91
+ plugins: [viteWesl({
92
+ extensions: [staticBuildExtension, linkBuildExtension]
93
+ })]
94
+ };
77
95
  ```
78
96
 
79
- ## Extensions
97
+ ### Conditions in Import Path
80
98
 
81
- - **LinkExtension** - import `?link` in JavaScript/TypeScript programs to conveniently assemble shader files and libraries for linking at runtime.
82
- Reads the `wesl.toml` file to find local shader files and libraries,
83
- Returns a `LinkParams` object ready to use for runtime linking.
99
+ For `?static`, you can specify conditions directly in the import:
84
100
 
85
- - **StaticExtension** - import `?static` in JavaScript/TypeScript programs to
86
- link shader files at build time.
87
- Reads the `wesl.toml` file to find local shader files and libraries,
88
- Returns a wgsl string ready to use for `createShaderModule`.
101
+ ```ts
102
+ import wgsl from "./app.wesl MOBILE=true DEBUG=false ?static";
103
+ ```
104
+
105
+ ## Configuration (wesl.toml)
106
+
107
+ The plugin reads `wesl.toml` to find shader files and dependencies:
89
108
 
90
- ### Prototype Extensions
109
+ ```toml
110
+ weslFiles = ["shaders/**/*.wesl"]
111
+ weslRoot = "shaders"
112
+ dependencies = ["auto"] # Auto-detect from package.json
113
+ ```
91
114
 
92
- - **SimpleReReflectExtension** - (_demo for extension writers_) import `?simple_reflect` to
93
- translate some wgsl `struct` elements into JavaScript and TypeScript.
94
- Demonstrates to wesl-plugin extension authors how to connect
95
- to the wesl-plugin, how to produce JavaScript, and how to produce TypeScript.
96
- - **BindingLayoutExtension** - (_prototype_) import `?bindingLayout` to collect JavaScript
97
- `BindingGroupLayout` objects.
98
- Works in concert with the `bindingStructsPlugin` to translate a proposed new WGSL
99
- feature for defining binding group layouts in shaders [#4957](https://github.com/gpuweb/gpuweb/issues/4957).
115
+ ## Prototype Extensions
100
116
 
101
- ## Developing a wesl-plugin extension
117
+ - **SimpleReflectExtension** - Demo for extension authors showing how to generate JS/TS from shader structs
118
+ - **BindingLayoutExtension** - Prototype for generating `BindGroupLayout` objects from shaders
102
119
 
103
- To add a new extension to the wesl-plugin:
120
+ ## Writing Custom Extensions
104
121
 
105
- - Pick an import suffix (e.g. `?myExtension`).
106
- - Implement a function that returns a JavaScript string.
107
- - Extensions have access to wgsl/wesl sources, a parsed abstract syntax tree for the sources, etc.
122
+ ```ts
123
+ import type { PluginExtension } from "wesl-plugin";
124
+
125
+ const myExtension: PluginExtension = {
126
+ extensionName: "myfeature", // enables ?myfeature imports
127
+ emitFn: async (shaderPath, api, conditions) => {
128
+ const sources = await api.weslSrc();
129
+ // Return JavaScript code as a string
130
+ return `export default ${JSON.stringify(sources)};`;
131
+ }
132
+ };
133
+ ```
108
134
 
109
- See [PluginExtension.ts](https://github.com/wgsl-tooling-wg/wesl-js/blob/master/tools/packages/wesl-plugin/src/PluginExtension.ts) for details.
135
+ See [PluginExtension.ts](https://github.com/wgsl-tooling-wg/wesl-js/blob/main/tools/packages/wesl-plugin/src/PluginExtension.ts) for the full API.
@@ -9880,11 +9880,14 @@ function weslPlugin(options, meta) {
9880
9880
  meta,
9881
9881
  options
9882
9882
  };
9883
+ const log = options.debug ? debugLog : noopLog;
9884
+ log("init", { extensions: options.extensions?.map((e) => e.extensionName) });
9883
9885
  return {
9884
9886
  name: "wesl-plugin",
9885
- resolveId: buildResolver(options, context),
9886
- load: buildLoader(context),
9887
+ resolveId: buildResolver(options, context, log),
9888
+ load: buildLoader(context, log),
9887
9889
  watchChange(id, _change) {
9890
+ log("watchChange", { id });
9888
9891
  if (id.endsWith("wesl.toml")) {
9889
9892
  cache.weslToml = void 0;
9890
9893
  cache.registry = void 0;
@@ -9905,15 +9908,18 @@ function pluginsByName(options) {
9905
9908
  * or
9906
9909
  * foo/bar.wesl COND=false ?static
9907
9910
  *
9911
+ * Bundlers may add extra query params (e.g. Vite adds ?import for dynamic imports,
9912
+ * ?t=123 for cache busting), so we capture the full query and search within it.
9913
+ *
9908
9914
  * someday it'd be nice to support import attributes like:
9909
9915
  * import "foo.bar.wesl?static" with { COND: false};
9910
9916
  * (but that doesn't seem supported to be supported in the the bundler plugins yet)
9911
9917
  */
9912
- const pluginMatch = /(^^)?(?<baseId>.*\.w[eg]sl)(?<cond>(\s*\w+(=\w+)?\s*)*)\?(?<pluginName>[\w_-]+)$/;
9918
+ const pluginMatch = /(^^)?(?<baseId>.*\.w[eg]sl)(?<cond>(\s*\w+(=\w+)?\s*)*)\?(?<query>.+)$/;
9913
9919
  const resolvedPrefix = "^^";
9914
9920
  /** build plugin entry for 'resolverId'
9915
9921
  * to validate our javascript virtual module imports (with e.g. ?static or ?link suffixes) */
9916
- function buildResolver(options, context) {
9922
+ function buildResolver(options, context, log) {
9917
9923
  const suffixes = pluginNames(options);
9918
9924
  return resolver;
9919
9925
  /**
@@ -9927,39 +9933,58 @@ function buildResolver(options, context) {
9927
9933
  if (id.startsWith(resolvedPrefix)) return id;
9928
9934
  if (id === context.weslToml) return id;
9929
9935
  const matched = pluginSuffixMatch(id, suffixes);
9936
+ log("resolveId", {
9937
+ id,
9938
+ matched: !!matched,
9939
+ suffixes
9940
+ });
9930
9941
  if (matched) {
9931
9942
  const { importParams, baseId, pluginName } = matched;
9932
9943
  const importerDir = path.dirname(importer);
9933
- return resolvedPrefix + path.join(importerDir, baseId) + importParams + "?" + pluginName;
9944
+ const result = resolvedPrefix + path.join(importerDir, baseId) + importParams + "?" + pluginName;
9945
+ log("resolveId resolved", { result });
9946
+ return result;
9934
9947
  }
9935
9948
  return matched ? id : null;
9936
9949
  }
9937
9950
  }
9951
+ /** Find matching plugin suffix in query string (handles ?import&static, ?t=123&static, etc.) */
9938
9952
  function pluginSuffixMatch(id, suffixes) {
9939
- const suffixMatch = id.match(pluginMatch);
9940
- const pluginName = suffixMatch?.groups?.pluginName;
9941
- if (!pluginName || !suffixes.includes(pluginName)) return null;
9953
+ const match$1 = id.match(pluginMatch);
9954
+ const query = match$1?.groups?.query;
9955
+ if (!query) return null;
9956
+ const segments = query.split("&");
9957
+ const pluginName = suffixes.find((s) => segments.includes(s));
9958
+ if (!pluginName) return null;
9942
9959
  return {
9943
9960
  pluginName,
9944
- baseId: suffixMatch.groups.baseId,
9945
- importParams: suffixMatch.groups?.cond
9961
+ baseId: match$1.groups.baseId,
9962
+ importParams: match$1.groups?.cond
9946
9963
  };
9947
9964
  }
9948
9965
  /** build plugin function for serving a javascript module in response to
9949
9966
  * an import of of our virtual import modules. */
9950
- function buildLoader(context) {
9967
+ function buildLoader(context, log) {
9951
9968
  const { options } = context;
9952
9969
  const suffixes = pluginNames(options);
9953
9970
  const pluginsMap = pluginsByName(options);
9954
9971
  return loader;
9955
9972
  async function loader(id) {
9956
9973
  const matched = pluginSuffixMatch(id, suffixes);
9974
+ log("load", {
9975
+ id,
9976
+ matched: matched?.pluginName ?? null
9977
+ });
9957
9978
  if (matched) {
9958
9979
  const buildPluginApi = buildApi(context, this);
9959
9980
  const plugin = pluginsMap[matched.pluginName];
9960
9981
  const { baseId, importParams } = matched;
9961
9982
  const conditions = importParamsToConditions(importParams);
9962
9983
  const shaderPath = baseId.startsWith(resolvedPrefix) ? baseId.slice(2) : baseId;
9984
+ log("load emitting", {
9985
+ shaderPath,
9986
+ conditions
9987
+ });
9963
9988
  return await plugin.emitFn(shaderPath, buildPluginApi, conditions);
9964
9989
  }
9965
9990
  return null;
@@ -9981,6 +10006,13 @@ function importParamsToConditions(importParams) {
9981
10006
  });
9982
10007
  return Object.fromEntries(condEntries);
9983
10008
  }
10009
+ function fmtDebugData(data) {
10010
+ return data ? " " + JSON.stringify(data) : "";
10011
+ }
10012
+ function debugLog(msg, data) {
10013
+ console.error(`[wesl-plugin] ${msg}${fmtDebugData(data)}`);
10014
+ }
10015
+ function noopLog() {}
9984
10016
  const unplugin = createUnplugin((options, meta) => {
9985
10017
  return weslPlugin(options, meta);
9986
10018
  });
@@ -4,6 +4,8 @@ import { n as PluginExtension } from "./PluginExtension-DlhUTOLC.js";
4
4
  interface WeslPluginOptions {
5
5
  weslToml?: string;
6
6
  extensions?: PluginExtension[];
7
+ /** Log plugin activity to stderr for debugging */
8
+ debug?: boolean;
7
9
  }
8
10
  //#endregion
9
11
  export { WeslPluginOptions as t };
@@ -17,7 +17,7 @@ async function emitLinkJs(baseId, api) {
17
17
  const autoDeps = await api.weslDependencies();
18
18
  const sanitizedDeps = autoDeps.map((dep) => dep.replaceAll("/", "_"));
19
19
  const bundleImports = autoDeps.map((p, i) => `import ${sanitizedDeps[i]} from "${p}";`).join("\n");
20
- const paramsName = `link${path.basename(rootModuleName)}Config`;
20
+ const paramsName = `link${path.basename(rootModuleName).replace(/\W/g, "_")}Config`;
21
21
  const linkParams = {
22
22
  rootModuleName,
23
23
  weslSrc,
@@ -1,5 +1,5 @@
1
1
  import "../PluginExtension-DlhUTOLC.js";
2
- import { t as WeslPluginOptions } from "../WeslPluginOptions-BXvD7dWh.js";
2
+ import { t as WeslPluginOptions } from "../WeslPluginOptions-BljqDSO5.js";
3
3
 
4
4
  //#region src/plugins/astro.d.ts
5
5
  declare const _default: (options: WeslPluginOptions) => any;
@@ -1,4 +1,4 @@
1
- import { t as WeslPlugin_default } from "../WeslPlugin-BhkB-CW-.js";
1
+ import { t as WeslPlugin_default } from "../WeslPlugin-JYxf2uLM.js";
2
2
  import "../import-meta-resolve-CUFqnZwT.js";
3
3
 
4
4
  //#region src/plugins/astro.ts
@@ -1,5 +1,5 @@
1
1
  import "../PluginExtension-DlhUTOLC.js";
2
- import { t as WeslPluginOptions } from "../WeslPluginOptions-BXvD7dWh.js";
2
+ import { t as WeslPluginOptions } from "../WeslPluginOptions-BljqDSO5.js";
3
3
  import * as esbuild0 from "esbuild";
4
4
 
5
5
  //#region src/plugins/esbuild.d.ts
@@ -1,4 +1,4 @@
1
- import { n as weslPlugin } from "../WeslPlugin-BhkB-CW-.js";
1
+ import { n as weslPlugin } from "../WeslPlugin-JYxf2uLM.js";
2
2
  import "../import-meta-resolve-CUFqnZwT.js";
3
3
  import { createEsbuildPlugin } from "unplugin";
4
4
 
@@ -1,5 +1,5 @@
1
1
  import "../PluginExtension-DlhUTOLC.js";
2
- import { t as WeslPluginOptions } from "../WeslPluginOptions-BXvD7dWh.js";
2
+ import { t as WeslPluginOptions } from "../WeslPluginOptions-BljqDSO5.js";
3
3
  import * as _farmfe_core0 from "@farmfe/core";
4
4
 
5
5
  //#region src/plugins/farm.d.ts
@@ -1,4 +1,4 @@
1
- import { n as weslPlugin } from "../WeslPlugin-BhkB-CW-.js";
1
+ import { n as weslPlugin } from "../WeslPlugin-JYxf2uLM.js";
2
2
  import "../import-meta-resolve-CUFqnZwT.js";
3
3
  import { createFarmPlugin } from "unplugin";
4
4
 
@@ -1,5 +1,5 @@
1
1
  import "../PluginExtension-DlhUTOLC.js";
2
- import { t as WeslPluginOptions } from "../WeslPluginOptions-BXvD7dWh.js";
2
+ import { t as WeslPluginOptions } from "../WeslPluginOptions-BljqDSO5.js";
3
3
  import * as _nuxt_schema0 from "@nuxt/schema";
4
4
 
5
5
  //#region src/plugins/nuxt.d.ts
@@ -1,7 +1,7 @@
1
- import "../WeslPlugin-BhkB-CW-.js";
1
+ import "../WeslPlugin-JYxf2uLM.js";
2
2
  import "../import-meta-resolve-CUFqnZwT.js";
3
- import { t as vite_default } from "../vite-BqdMYd8l.js";
4
- import { t as webpack_default } from "../webpack-D-jacRMO.js";
3
+ import { t as vite_default } from "../vite-CE8Yhwk-.js";
4
+ import { t as webpack_default } from "../webpack-C7YoLSyP.js";
5
5
  import { addVitePlugin, addWebpackPlugin, defineNuxtModule } from "@nuxt/kit";
6
6
  import "@nuxt/schema";
7
7
 
@@ -1,5 +1,5 @@
1
1
  import "../PluginExtension-DlhUTOLC.js";
2
- import { t as WeslPluginOptions } from "../WeslPluginOptions-BXvD7dWh.js";
2
+ import { t as WeslPluginOptions } from "../WeslPluginOptions-BljqDSO5.js";
3
3
  import * as rollup0 from "rollup";
4
4
 
5
5
  //#region src/plugins/rollup.d.ts
@@ -1,4 +1,4 @@
1
- import { n as weslPlugin } from "../WeslPlugin-BhkB-CW-.js";
1
+ import { n as weslPlugin } from "../WeslPlugin-JYxf2uLM.js";
2
2
  import "../import-meta-resolve-CUFqnZwT.js";
3
3
  import { createRollupPlugin } from "unplugin";
4
4
 
@@ -1,5 +1,5 @@
1
1
  import "../PluginExtension-DlhUTOLC.js";
2
- import { t as WeslPluginOptions } from "../WeslPluginOptions-BXvD7dWh.js";
2
+ import { t as WeslPluginOptions } from "../WeslPluginOptions-BljqDSO5.js";
3
3
 
4
4
  //#region src/plugins/rspack.d.ts
5
5
  declare const _default: (options: WeslPluginOptions) => RspackPluginInstance;
@@ -1,4 +1,4 @@
1
- import { n as weslPlugin } from "../WeslPlugin-BhkB-CW-.js";
1
+ import { n as weslPlugin } from "../WeslPlugin-JYxf2uLM.js";
2
2
  import "../import-meta-resolve-CUFqnZwT.js";
3
3
  import { createRspackPlugin } from "unplugin";
4
4
 
@@ -1,5 +1,5 @@
1
1
  import "../PluginExtension-DlhUTOLC.js";
2
- import { t as WeslPluginOptions } from "../WeslPluginOptions-BXvD7dWh.js";
2
+ import { t as WeslPluginOptions } from "../WeslPluginOptions-BljqDSO5.js";
3
3
  import * as vite0 from "vite";
4
4
 
5
5
  //#region src/plugins/vite.d.ts
@@ -1,5 +1,5 @@
1
- import "../WeslPlugin-BhkB-CW-.js";
1
+ import "../WeslPlugin-JYxf2uLM.js";
2
2
  import "../import-meta-resolve-CUFqnZwT.js";
3
- import { t as vite_default } from "../vite-BqdMYd8l.js";
3
+ import { t as vite_default } from "../vite-CE8Yhwk-.js";
4
4
 
5
5
  export { vite_default as default };
@@ -1,5 +1,5 @@
1
1
  import "../PluginExtension-DlhUTOLC.js";
2
- import { t as WeslPluginOptions } from "../WeslPluginOptions-BXvD7dWh.js";
2
+ import { t as WeslPluginOptions } from "../WeslPluginOptions-BljqDSO5.js";
3
3
  import * as webpack0 from "webpack";
4
4
 
5
5
  //#region src/plugins/webpack.d.ts
@@ -1,5 +1,5 @@
1
- import "../WeslPlugin-BhkB-CW-.js";
1
+ import "../WeslPlugin-JYxf2uLM.js";
2
2
  import "../import-meta-resolve-CUFqnZwT.js";
3
- import { t as webpack_default } from "../webpack-D-jacRMO.js";
3
+ import { t as webpack_default } from "../webpack-C7YoLSyP.js";
4
4
 
5
5
  export { webpack_default as default };
@@ -1,4 +1,4 @@
1
- import { n as weslPlugin } from "./WeslPlugin-BhkB-CW-.js";
1
+ import { n as weslPlugin } from "./WeslPlugin-JYxf2uLM.js";
2
2
  import { createVitePlugin } from "unplugin";
3
3
 
4
4
  //#region src/plugins/vite.ts
@@ -1,4 +1,4 @@
1
- import { n as weslPlugin } from "./WeslPlugin-BhkB-CW-.js";
1
+ import { n as weslPlugin } from "./WeslPlugin-JYxf2uLM.js";
2
2
  import { createWebpackPlugin } from "unplugin";
3
3
 
4
4
  //#region src/plugins/webpack.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wesl-plugin",
3
3
  "description": "",
4
- "version": "0.6.47",
4
+ "version": "0.6.49",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "src",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "unplugin": "^2.3.5",
28
- "wesl": "0.6.47"
28
+ "wesl": "0.6.49"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@nuxt/kit": "^3.17.6",
package/src/WeslPlugin.ts CHANGED
@@ -50,6 +50,8 @@ export interface PluginContext {
50
50
  weslToml?: string;
51
51
  }
52
52
 
53
+ type DebugLog = (msg: string, data?: Record<string, unknown>) => void;
54
+
53
55
  /**
54
56
  * A bundler plugin for processing WESL files.
55
57
  *
@@ -66,12 +68,16 @@ export function weslPlugin(
66
68
  ): UnpluginOptions {
67
69
  const cache: PluginCache = {};
68
70
  const context: PluginContext = { cache, meta, options };
71
+ const log = options.debug ? debugLog : noopLog;
72
+
73
+ log("init", { extensions: options.extensions?.map(e => e.extensionName) });
69
74
 
70
75
  return {
71
76
  name: "wesl-plugin",
72
- resolveId: buildResolver(options, context),
73
- load: buildLoader(context),
77
+ resolveId: buildResolver(options, context, log),
78
+ load: buildLoader(context, log),
74
79
  watchChange(id, _change) {
80
+ log("watchChange", { id });
75
81
  if (id.endsWith("wesl.toml")) {
76
82
  // The cache is shared for multiple imports
77
83
  cache.weslToml = undefined;
@@ -100,12 +106,15 @@ function pluginsByName(
100
106
  * or
101
107
  * foo/bar.wesl COND=false ?static
102
108
  *
109
+ * Bundlers may add extra query params (e.g. Vite adds ?import for dynamic imports,
110
+ * ?t=123 for cache busting), so we capture the full query and search within it.
111
+ *
103
112
  * someday it'd be nice to support import attributes like:
104
113
  * import "foo.bar.wesl?static" with { COND: false};
105
114
  * (but that doesn't seem supported to be supported in the the bundler plugins yet)
106
115
  */
107
116
  const pluginMatch =
108
- /(^^)?(?<baseId>.*\.w[eg]sl)(?<cond>(\s*\w+(=\w+)?\s*)*)\?(?<pluginName>[\w_-]+)$/;
117
+ /(^^)?(?<baseId>.*\.w[eg]sl)(?<cond>(\s*\w+(=\w+)?\s*)*)\?(?<query>.+)$/;
109
118
 
110
119
  const resolvedPrefix = "^^";
111
120
 
@@ -114,6 +123,7 @@ const resolvedPrefix = "^^";
114
123
  function buildResolver(
115
124
  options: WeslPluginOptions,
116
125
  context: PluginContext,
126
+ log: DebugLog,
117
127
  ): Resolver {
118
128
  const suffixes = pluginNames(options);
119
129
  return resolver;
@@ -141,6 +151,7 @@ function buildResolver(
141
151
  return id;
142
152
  }
143
153
  const matched = pluginSuffixMatch(id, suffixes);
154
+ log("resolveId", { id, matched: !!matched, suffixes });
144
155
  if (matched) {
145
156
  const { importParams, baseId, pluginName } = matched;
146
157
 
@@ -149,6 +160,7 @@ function buildResolver(
149
160
  const pathToShader = path.join(importerDir, baseId);
150
161
  const result =
151
162
  resolvedPrefix + pathToShader + importParams + "?" + pluginName;
163
+ log("resolveId resolved", { result });
152
164
  return result;
153
165
  }
154
166
  return matched ? id : null; // this case doesn't happen AFAIK
@@ -161,20 +173,27 @@ interface PluginMatch {
161
173
  pluginName: string;
162
174
  }
163
175
 
176
+ /** Find matching plugin suffix in query string (handles ?import&static, ?t=123&static, etc.) */
164
177
  function pluginSuffixMatch(id: string, suffixes: string[]): PluginMatch | null {
165
- const suffixMatch = id.match(pluginMatch);
166
- const pluginName = suffixMatch?.groups?.pluginName;
167
- if (!pluginName || !suffixes.includes(pluginName)) return null;
178
+ const match = id.match(pluginMatch);
179
+ const query = match?.groups?.query;
180
+ if (!query) return null;
181
+
182
+ // Query params are &-separated; find one that matches a configured suffix
183
+ const segments = query.split("&");
184
+ const pluginName = suffixes.find(s => segments.includes(s));
185
+ if (!pluginName) return null;
186
+
168
187
  return {
169
188
  pluginName,
170
- baseId: suffixMatch.groups!.baseId,
171
- importParams: suffixMatch.groups?.cond,
189
+ baseId: match.groups!.baseId,
190
+ importParams: match.groups?.cond,
172
191
  };
173
192
  }
174
193
 
175
194
  /** build plugin function for serving a javascript module in response to
176
195
  * an import of of our virtual import modules. */
177
- function buildLoader(context: PluginContext): Loader {
196
+ function buildLoader(context: PluginContext, log: DebugLog): Loader {
178
197
  const { options } = context;
179
198
  const suffixes = pluginNames(options);
180
199
  const pluginsMap = pluginsByName(options);
@@ -185,6 +204,7 @@ function buildLoader(context: PluginContext): Loader {
185
204
  id: string,
186
205
  ) {
187
206
  const matched = pluginSuffixMatch(id, suffixes);
207
+ log("load", { id, matched: matched?.pluginName ?? null });
188
208
  if (matched) {
189
209
  const buildPluginApi = buildApi(context, this);
190
210
  const plugin = pluginsMap[matched.pluginName];
@@ -194,6 +214,7 @@ function buildLoader(context: PluginContext): Loader {
194
214
  ? baseId.slice(resolvedPrefix.length)
195
215
  : baseId;
196
216
 
217
+ log("load emitting", { shaderPath, conditions });
197
218
  return await plugin.emitFn(shaderPath, buildPluginApi, conditions);
198
219
  }
199
220
 
@@ -226,6 +247,16 @@ function importParamsToConditions(
226
247
  return conditions;
227
248
  }
228
249
 
250
+ function fmtDebugData(data?: Record<string, unknown>): string {
251
+ return data ? " " + JSON.stringify(data) : "";
252
+ }
253
+
254
+ function debugLog(msg: string, data?: Record<string, unknown>): void {
255
+ console.error(`[wesl-plugin] ${msg}${fmtDebugData(data)}`);
256
+ }
257
+
258
+ function noopLog(): void {}
259
+
229
260
  export const unplugin = createUnplugin(
230
261
  (options: WeslPluginOptions, meta: UnpluginContextMeta) => {
231
262
  return weslPlugin(options, meta);
@@ -3,4 +3,7 @@ import type { PluginExtension } from "./PluginExtension.ts";
3
3
  export interface WeslPluginOptions {
4
4
  weslToml?: string;
5
5
  extensions?: PluginExtension[];
6
+
7
+ /** Log plugin activity to stderr for debugging */
8
+ debug?: boolean;
6
9
  }
@@ -32,7 +32,7 @@ async function emitLinkJs(
32
32
  .map((p, i) => `import ${sanitizedDeps[i]} from "${p}";`)
33
33
  .join("\n");
34
34
 
35
- const rootName = path.basename(rootModuleName);
35
+ const rootName = path.basename(rootModuleName).replace(/\W/g, "_");
36
36
  const paramsName = `link${rootName}Config`;
37
37
 
38
38
  const linkParams: LinkParams = {