wgsl-play 0.0.34 → 0.0.36

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.
@@ -154,19 +154,25 @@ async function initWebGPU(canvas, alphaMode = "opaque") {
154
154
  /** Compile WESL fragment shader and create render pipeline. */
155
155
  async function createPipeline(state, fragmentSource, options) {
156
156
  state.device.pushErrorScope("validation");
157
- const pipeline = await linkAndCreatePipeline({
158
- device: state.device,
159
- fragmentSource,
160
- format: state.presentationFormat,
161
- layout: state.pipelineLayout,
162
- ...options
163
- });
164
- const gpuError = await state.device.popErrorScope();
165
- if (gpuError) {
157
+ let gpuError;
158
+ let jsError;
159
+ try {
160
+ state.pipeline = await linkAndCreatePipeline({
161
+ device: state.device,
162
+ fragmentSource,
163
+ format: state.presentationFormat,
164
+ layout: state.pipelineLayout,
165
+ ...options
166
+ });
167
+ } catch (e) {
168
+ jsError = e;
169
+ } finally {
170
+ gpuError = await state.device.popErrorScope();
171
+ }
172
+ if (jsError || gpuError) {
166
173
  state.pipeline = void 0;
167
- throw gpuError;
174
+ throw jsError ?? gpuError;
168
175
  }
169
- state.pipeline = pipeline;
170
176
  }
171
177
  /** Start the render loop. Returns a stop function. */
172
178
  function startRenderLoop(state, playback) {
@@ -38,6 +38,8 @@ declare class WgslPlay extends HTMLElement {
38
38
  private _sourceEl;
39
39
  private _sourceListener;
40
40
  private _fetchLibs;
41
+ private _dirty;
42
+ private _building;
41
43
  private _theme;
42
44
  private _mediaQuery;
43
45
  private _onFullscreenChange;
@@ -93,12 +95,12 @@ declare class WgslPlay extends HTMLElement {
93
95
  private loadInitialContent;
94
96
  /** Connect to a source provider element (e.g., wgsl-edit). */
95
97
  private connectToSource;
96
- /** Fetch shader from URL, auto-fetching any imported dependencies. */
98
+ /** Fetch shader from URL, then trigger a build. */
97
99
  private loadFromUrl;
98
- /** Rebuild GPU pipeline using stored state. For full projects with all sources. */
99
- private rebuildPipeline;
100
- /** Discover dependencies and rebuild. For HTTP/inline sources that may need fetching. */
101
- private discoverAndRebuild;
100
+ /** Mark build as needed. Coalesces rapid requests into a single build. */
101
+ private requestBuild;
102
+ /** Run builds until no longer dirty. Only one instance runs at a time. */
103
+ private runBuild;
102
104
  private handleCompileError;
103
105
  /** Extract source locations from a WESL parse error or GPU compilation error. */
104
106
  private extractLocations;
package/dist/WgslPlay.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as PlaybackControls, c as getConfig, i as startRenderLoop, l as resetConfig, n as createPipeline, o as ErrorOverlay, r as initWebGPU, s as defaults, t as WgslPlay_default } from "./WgslPlay-Ben4VdsC.js";
1
+ import { a as PlaybackControls, c as getConfig, i as startRenderLoop, l as resetConfig, n as createPipeline, o as ErrorOverlay, r as initWebGPU, s as defaults, t as WgslPlay_default } from "./WgslPlay-BRvURGA3.js";
2
2
  import { fetchDependencies, loadShaderFromUrl } from "wesl-fetch";
3
3
  import { WeslParseError, fileToModulePath } from "wesl";
4
4
 
@@ -37,6 +37,8 @@ var WgslPlay = class extends HTMLElement {
37
37
  _sourceEl = null;
38
38
  _sourceListener = null;
39
39
  _fetchLibs = true;
40
+ _dirty = false;
41
+ _building = false;
40
42
  _theme = "auto";
41
43
  _mediaQuery = null;
42
44
  _onFullscreenChange = () => this.controls.setFullscreen(!!document.fullscreenElement);
@@ -117,7 +119,7 @@ var WgslPlay = class extends HTMLElement {
117
119
  this._weslSrc = { [this._rootModuleName]: value };
118
120
  this._libs = void 0;
119
121
  this._fromFullProject = false;
120
- this.discoverAndRebuild();
122
+ this.requestBuild();
121
123
  }
122
124
  /** Conditions for conditional compilation (@if/@elif/@else). */
123
125
  get conditions() {
@@ -129,8 +131,7 @@ var WgslPlay = class extends HTMLElement {
129
131
  conditions: value
130
132
  };
131
133
  if (Object.keys(this._weslSrc).length === 0) return;
132
- if (this._fromFullProject) this.rebuildPipeline();
133
- else this.discoverAndRebuild();
134
+ this.requestBuild();
134
135
  }
135
136
  /** Set project configuration (mirrors wesl link() API). */
136
137
  set project(value) {
@@ -146,8 +147,7 @@ var WgslPlay = class extends HTMLElement {
146
147
  return;
147
148
  }
148
149
  if (Object.keys(this._weslSrc).length === 0) return;
149
- if (this._fromFullProject) this.rebuildPipeline();
150
- else this.discoverAndRebuild();
150
+ this.requestBuild();
151
151
  }
152
152
  /** Set sources from a full project with weslSrc. */
153
153
  setProjectSources(weslSrc, rootModuleName) {
@@ -156,7 +156,7 @@ var WgslPlay = class extends HTMLElement {
156
156
  this._weslSrc = toModulePaths(weslSrc, pkg);
157
157
  this._rootModuleName = fileToModulePath(root, pkg, false);
158
158
  this._fromFullProject = true;
159
- this.rebuildPipeline();
159
+ this.requestBuild();
160
160
  }
161
161
  /** Whether to auto-fetch missing library packages from npm (default: true). */
162
162
  get fetchLibs() {
@@ -248,7 +248,7 @@ var WgslPlay = class extends HTMLElement {
248
248
  try {
249
249
  const alphaMode = this.hasAttribute("transparent") ? "premultiplied" : "opaque";
250
250
  this.renderState = await initWebGPU(this.canvas, alphaMode);
251
- await this.loadInitialContent();
251
+ this.loadInitialContent();
252
252
  this.stopRenderLoop = startRenderLoop(this.renderState, this.playback);
253
253
  this.dispatchEvent(new CustomEvent("ready"));
254
254
  return true;
@@ -261,19 +261,25 @@ var WgslPlay = class extends HTMLElement {
261
261
  }
262
262
  }
263
263
  /** Load from source element, src URL, script child, or inline textContent. */
264
- async loadInitialContent() {
264
+ loadInitialContent() {
265
265
  const sourceId = this.getAttribute("source");
266
- if (sourceId) return this.connectToSource(sourceId);
266
+ if (sourceId) {
267
+ this.connectToSource(sourceId);
268
+ return;
269
+ }
267
270
  const src = this.getAttribute("src");
268
- if (src) return this.loadFromUrl(src);
271
+ if (src) {
272
+ this.loadFromUrl(src);
273
+ return;
274
+ }
269
275
  const inlineSource = this.querySelector("script[type=\"text/wgsl\"], script[type=\"text/wesl\"]")?.textContent?.trim() ?? this.textContent?.trim();
270
276
  if (!inlineSource) return;
271
277
  this._weslSrc = { [this._rootModuleName]: inlineSource };
272
278
  this._fromFullProject = false;
273
- await this.discoverAndRebuild();
279
+ this.requestBuild();
274
280
  }
275
281
  /** Connect to a source provider element (e.g., wgsl-edit). */
276
- async connectToSource(id) {
282
+ connectToSource(id) {
277
283
  const el = document.getElementById(id);
278
284
  if (!el) {
279
285
  console.error(`wgsl-play: source element "${id}" not found`);
@@ -296,96 +302,74 @@ var WgslPlay = class extends HTMLElement {
296
302
  };
297
303
  if (libs) this._libs = libs;
298
304
  this._fromFullProject = false;
299
- await this.discoverAndRebuild();
300
- this._fromFullProject = true;
305
+ this.requestBuild();
301
306
  this._sourceListener = (e) => {
302
307
  const detail = e.detail;
303
308
  const fallback = { [this._rootModuleName]: detail?.source ?? "" };
304
309
  this.project = {
305
310
  weslSrc: detail?.sources ?? fallback,
306
311
  rootModuleName: detail?.rootModuleName,
307
- conditions: detail?.conditions
312
+ conditions: detail?.conditions,
313
+ libs: detail?.libs
308
314
  };
309
315
  };
310
316
  el.addEventListener("change", this._sourceListener);
311
317
  }
312
- /** Fetch shader from URL, auto-fetching any imported dependencies. */
318
+ /** Fetch shader from URL, then trigger a build. */
313
319
  async loadFromUrl(url) {
314
- if (!this.renderState) return;
315
320
  try {
316
- this.errorOverlay.hide();
317
321
  const { weslSrc, libs, rootModuleName } = await loadShaderFromUrl(url, this.getConfigOverrides()?.shaderRoot);
318
322
  this._weslSrc = weslSrc;
319
323
  this._libs = libs;
320
324
  this._fromFullProject = false;
321
325
  if (rootModuleName) this._rootModuleName = rootModuleName;
322
- const mainSource = weslSrc[this._rootModuleName];
323
- if (!mainSource) {
324
- console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(weslSrc));
325
- return;
326
- }
327
- await createPipeline(this.renderState, mainSource, {
328
- ...this._linkOptions,
329
- weslSrc,
330
- libs,
331
- rootModuleName: this._rootModuleName
332
- });
333
- this.dispatchEvent(new CustomEvent("compile-success"));
334
- } catch (error) {
335
- this.handleCompileError(error);
336
- }
337
- }
338
- /** Rebuild GPU pipeline using stored state. For full projects with all sources. */
339
- async rebuildPipeline() {
340
- if (!await this.initialize()) return;
341
- const mainSource = this._weslSrc[this._rootModuleName];
342
- if (!mainSource) {
343
- console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(this._weslSrc));
344
- return;
345
- }
346
- try {
347
- this.errorOverlay.hide();
348
- await createPipeline(this.renderState, mainSource, {
349
- ...this._linkOptions,
350
- weslSrc: this._weslSrc,
351
- libs: this._libs,
352
- rootModuleName: this._rootModuleName
353
- });
354
- this.dispatchEvent(new CustomEvent("compile-success"));
326
+ this.requestBuild();
355
327
  } catch (error) {
356
328
  this.handleCompileError(error);
357
329
  }
358
330
  }
359
- /** Discover dependencies and rebuild. For HTTP/inline sources that may need fetching. */
360
- async discoverAndRebuild() {
361
- if (!await this.initialize()) return;
362
- const mainSource = this._weslSrc[this._rootModuleName];
363
- if (!mainSource) {
364
- console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(this._weslSrc));
365
- return;
366
- }
367
- try {
368
- this.errorOverlay.hide();
369
- const { weslSrc, libs } = await fetchDependencies(mainSource, {
370
- shaderRoot: this.getConfigOverrides()?.shaderRoot,
371
- existingSources: this._weslSrc,
372
- skipExternal: !this._fetchLibs
373
- });
374
- this._weslSrc = {
375
- ...this._weslSrc,
376
- ...weslSrc
377
- };
378
- this._libs = [...this._libs ?? [], ...libs];
379
- await createPipeline(this.renderState, mainSource, {
380
- ...this._linkOptions,
381
- weslSrc: this._weslSrc,
382
- libs: this._libs,
383
- rootModuleName: this._rootModuleName
384
- });
385
- this.dispatchEvent(new CustomEvent("compile-success"));
386
- } catch (error) {
387
- this.handleCompileError(error);
331
+ /** Mark build as needed. Coalesces rapid requests into a single build. */
332
+ requestBuild() {
333
+ this._dirty = true;
334
+ if (!this._building) this.runBuild();
335
+ }
336
+ /** Run builds until no longer dirty. Only one instance runs at a time. */
337
+ async runBuild() {
338
+ this._building = true;
339
+ while (this._dirty) {
340
+ this._dirty = false;
341
+ if (!await this.initialize()) break;
342
+ const mainSource = this._weslSrc[this._rootModuleName];
343
+ if (!mainSource) {
344
+ console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(this._weslSrc));
345
+ continue;
346
+ }
347
+ try {
348
+ this.errorOverlay.hide();
349
+ if (!this._fromFullProject) {
350
+ const { weslSrc, libs } = await fetchDependencies(mainSource, {
351
+ shaderRoot: this.getConfigOverrides()?.shaderRoot,
352
+ existingSources: this._weslSrc,
353
+ skipExternal: !this._fetchLibs
354
+ });
355
+ this._weslSrc = {
356
+ ...this._weslSrc,
357
+ ...weslSrc
358
+ };
359
+ this._libs = dedupLibs(this._libs, libs);
360
+ }
361
+ await createPipeline(this.renderState, mainSource, {
362
+ ...this._linkOptions,
363
+ weslSrc: this._weslSrc,
364
+ libs: this._libs,
365
+ rootModuleName: this._rootModuleName
366
+ });
367
+ if (!this._dirty) this.dispatchEvent(new CustomEvent("compile-success"));
368
+ } catch (error) {
369
+ if (!this._dirty) this.handleCompileError(error);
370
+ }
388
371
  }
372
+ this._building = false;
389
373
  }
390
374
  handleCompileError(error) {
391
375
  const message = error?.message ?? String(error);
@@ -442,6 +426,12 @@ function upgradeProperty(el, prop) {
442
426
  el[prop] = value;
443
427
  }
444
428
  }
429
+ /** Merge new libs, deduplicating by bundle name. */
430
+ function dedupLibs(existing, newLibs) {
431
+ if (!existing || newLibs.length === 0) return [...existing ?? [], ...newLibs];
432
+ const names = new Set(newLibs.map((b) => b.name));
433
+ return [...existing.filter((b) => !names.has(b.name)), ...newLibs];
434
+ }
445
435
  /** Normalize all keys in a weslSrc record to module paths. */
446
436
  function toModulePaths(weslSrc, pkg) {
447
437
  const result = {};
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as getConfig, l as resetConfig, s as defaults } from "./WgslPlay-Ben4VdsC.js";
1
+ import { c as getConfig, l as resetConfig, s as defaults } from "./WgslPlay-BRvURGA3.js";
2
2
  import { WgslPlay } from "./WgslPlay.js";
3
3
 
4
4
  export * from "wesl-fetch"
package/dist/wgsl-play.js CHANGED
@@ -5641,19 +5641,25 @@ async function initWebGPU(canvas, alphaMode = "opaque") {
5641
5641
  /** Compile WESL fragment shader and create render pipeline. */
5642
5642
  async function createPipeline(state, fragmentSource, options) {
5643
5643
  state.device.pushErrorScope("validation");
5644
- const pipeline = await linkAndCreatePipeline({
5645
- device: state.device,
5646
- fragmentSource,
5647
- format: state.presentationFormat,
5648
- layout: state.pipelineLayout,
5649
- ...options
5650
- });
5651
- const gpuError = await state.device.popErrorScope();
5652
- if (gpuError) {
5644
+ let gpuError;
5645
+ let jsError;
5646
+ try {
5647
+ state.pipeline = await linkAndCreatePipeline({
5648
+ device: state.device,
5649
+ fragmentSource,
5650
+ format: state.presentationFormat,
5651
+ layout: state.pipelineLayout,
5652
+ ...options
5653
+ });
5654
+ } catch (e) {
5655
+ jsError = e;
5656
+ } finally {
5657
+ gpuError = await state.device.popErrorScope();
5658
+ }
5659
+ if (jsError || gpuError) {
5653
5660
  state.pipeline = void 0;
5654
- throw gpuError;
5661
+ throw jsError ?? gpuError;
5655
5662
  }
5656
- state.pipeline = pipeline;
5657
5663
  }
5658
5664
  /** Start the render loop. Returns a stop function. */
5659
5665
  function startRenderLoop(state, playback) {
@@ -5722,6 +5728,8 @@ var WgslPlay = class extends HTMLElement {
5722
5728
  _sourceEl = null;
5723
5729
  _sourceListener = null;
5724
5730
  _fetchLibs = true;
5731
+ _dirty = false;
5732
+ _building = false;
5725
5733
  _theme = "auto";
5726
5734
  _mediaQuery = null;
5727
5735
  _onFullscreenChange = () => this.controls.setFullscreen(!!document.fullscreenElement);
@@ -5802,7 +5810,7 @@ var WgslPlay = class extends HTMLElement {
5802
5810
  this._weslSrc = { [this._rootModuleName]: value };
5803
5811
  this._libs = void 0;
5804
5812
  this._fromFullProject = false;
5805
- this.discoverAndRebuild();
5813
+ this.requestBuild();
5806
5814
  }
5807
5815
  /** Conditions for conditional compilation (@if/@elif/@else). */
5808
5816
  get conditions() {
@@ -5814,8 +5822,7 @@ var WgslPlay = class extends HTMLElement {
5814
5822
  conditions: value
5815
5823
  };
5816
5824
  if (Object.keys(this._weslSrc).length === 0) return;
5817
- if (this._fromFullProject) this.rebuildPipeline();
5818
- else this.discoverAndRebuild();
5825
+ this.requestBuild();
5819
5826
  }
5820
5827
  /** Set project configuration (mirrors wesl link() API). */
5821
5828
  set project(value) {
@@ -5831,8 +5838,7 @@ var WgslPlay = class extends HTMLElement {
5831
5838
  return;
5832
5839
  }
5833
5840
  if (Object.keys(this._weslSrc).length === 0) return;
5834
- if (this._fromFullProject) this.rebuildPipeline();
5835
- else this.discoverAndRebuild();
5841
+ this.requestBuild();
5836
5842
  }
5837
5843
  /** Set sources from a full project with weslSrc. */
5838
5844
  setProjectSources(weslSrc, rootModuleName) {
@@ -5841,7 +5847,7 @@ var WgslPlay = class extends HTMLElement {
5841
5847
  this._weslSrc = toModulePaths(weslSrc, pkg);
5842
5848
  this._rootModuleName = fileToModulePath(root, pkg, false);
5843
5849
  this._fromFullProject = true;
5844
- this.rebuildPipeline();
5850
+ this.requestBuild();
5845
5851
  }
5846
5852
  /** Whether to auto-fetch missing library packages from npm (default: true). */
5847
5853
  get fetchLibs() {
@@ -5933,7 +5939,7 @@ var WgslPlay = class extends HTMLElement {
5933
5939
  try {
5934
5940
  const alphaMode = this.hasAttribute("transparent") ? "premultiplied" : "opaque";
5935
5941
  this.renderState = await initWebGPU(this.canvas, alphaMode);
5936
- await this.loadInitialContent();
5942
+ this.loadInitialContent();
5937
5943
  this.stopRenderLoop = startRenderLoop(this.renderState, this.playback);
5938
5944
  this.dispatchEvent(new CustomEvent("ready"));
5939
5945
  return true;
@@ -5946,19 +5952,25 @@ var WgslPlay = class extends HTMLElement {
5946
5952
  }
5947
5953
  }
5948
5954
  /** Load from source element, src URL, script child, or inline textContent. */
5949
- async loadInitialContent() {
5955
+ loadInitialContent() {
5950
5956
  const sourceId = this.getAttribute("source");
5951
- if (sourceId) return this.connectToSource(sourceId);
5957
+ if (sourceId) {
5958
+ this.connectToSource(sourceId);
5959
+ return;
5960
+ }
5952
5961
  const src = this.getAttribute("src");
5953
- if (src) return this.loadFromUrl(src);
5962
+ if (src) {
5963
+ this.loadFromUrl(src);
5964
+ return;
5965
+ }
5954
5966
  const inlineSource = this.querySelector("script[type=\"text/wgsl\"], script[type=\"text/wesl\"]")?.textContent?.trim() ?? this.textContent?.trim();
5955
5967
  if (!inlineSource) return;
5956
5968
  this._weslSrc = { [this._rootModuleName]: inlineSource };
5957
5969
  this._fromFullProject = false;
5958
- await this.discoverAndRebuild();
5970
+ this.requestBuild();
5959
5971
  }
5960
5972
  /** Connect to a source provider element (e.g., wgsl-edit). */
5961
- async connectToSource(id) {
5973
+ connectToSource(id) {
5962
5974
  const el = document.getElementById(id);
5963
5975
  if (!el) {
5964
5976
  console.error(`wgsl-play: source element "${id}" not found`);
@@ -5981,96 +5993,74 @@ var WgslPlay = class extends HTMLElement {
5981
5993
  };
5982
5994
  if (libs) this._libs = libs;
5983
5995
  this._fromFullProject = false;
5984
- await this.discoverAndRebuild();
5985
- this._fromFullProject = true;
5996
+ this.requestBuild();
5986
5997
  this._sourceListener = (e) => {
5987
5998
  const detail = e.detail;
5988
5999
  const fallback = { [this._rootModuleName]: detail?.source ?? "" };
5989
6000
  this.project = {
5990
6001
  weslSrc: detail?.sources ?? fallback,
5991
6002
  rootModuleName: detail?.rootModuleName,
5992
- conditions: detail?.conditions
6003
+ conditions: detail?.conditions,
6004
+ libs: detail?.libs
5993
6005
  };
5994
6006
  };
5995
6007
  el.addEventListener("change", this._sourceListener);
5996
6008
  }
5997
- /** Fetch shader from URL, auto-fetching any imported dependencies. */
6009
+ /** Fetch shader from URL, then trigger a build. */
5998
6010
  async loadFromUrl(url) {
5999
- if (!this.renderState) return;
6000
6011
  try {
6001
- this.errorOverlay.hide();
6002
6012
  const { weslSrc, libs, rootModuleName } = await loadShaderFromUrl(url, this.getConfigOverrides()?.shaderRoot);
6003
6013
  this._weslSrc = weslSrc;
6004
6014
  this._libs = libs;
6005
6015
  this._fromFullProject = false;
6006
6016
  if (rootModuleName) this._rootModuleName = rootModuleName;
6007
- const mainSource = weslSrc[this._rootModuleName];
6008
- if (!mainSource) {
6009
- console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(weslSrc));
6010
- return;
6011
- }
6012
- await createPipeline(this.renderState, mainSource, {
6013
- ...this._linkOptions,
6014
- weslSrc,
6015
- libs,
6016
- rootModuleName: this._rootModuleName
6017
- });
6018
- this.dispatchEvent(new CustomEvent("compile-success"));
6017
+ this.requestBuild();
6019
6018
  } catch (error) {
6020
6019
  this.handleCompileError(error);
6021
6020
  }
6022
6021
  }
6023
- /** Rebuild GPU pipeline using stored state. For full projects with all sources. */
6024
- async rebuildPipeline() {
6025
- if (!await this.initialize()) return;
6026
- const mainSource = this._weslSrc[this._rootModuleName];
6027
- if (!mainSource) {
6028
- console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(this._weslSrc));
6029
- return;
6030
- }
6031
- try {
6032
- this.errorOverlay.hide();
6033
- await createPipeline(this.renderState, mainSource, {
6034
- ...this._linkOptions,
6035
- weslSrc: this._weslSrc,
6036
- libs: this._libs,
6037
- rootModuleName: this._rootModuleName
6038
- });
6039
- this.dispatchEvent(new CustomEvent("compile-success"));
6040
- } catch (error) {
6041
- this.handleCompileError(error);
6042
- }
6043
- }
6044
- /** Discover dependencies and rebuild. For HTTP/inline sources that may need fetching. */
6045
- async discoverAndRebuild() {
6046
- if (!await this.initialize()) return;
6047
- const mainSource = this._weslSrc[this._rootModuleName];
6048
- if (!mainSource) {
6049
- console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(this._weslSrc));
6050
- return;
6051
- }
6052
- try {
6053
- this.errorOverlay.hide();
6054
- const { weslSrc, libs } = await fetchDependencies(mainSource, {
6055
- shaderRoot: this.getConfigOverrides()?.shaderRoot,
6056
- existingSources: this._weslSrc,
6057
- skipExternal: !this._fetchLibs
6058
- });
6059
- this._weslSrc = {
6060
- ...this._weslSrc,
6061
- ...weslSrc
6062
- };
6063
- this._libs = [...this._libs ?? [], ...libs];
6064
- await createPipeline(this.renderState, mainSource, {
6065
- ...this._linkOptions,
6066
- weslSrc: this._weslSrc,
6067
- libs: this._libs,
6068
- rootModuleName: this._rootModuleName
6069
- });
6070
- this.dispatchEvent(new CustomEvent("compile-success"));
6071
- } catch (error) {
6072
- this.handleCompileError(error);
6022
+ /** Mark build as needed. Coalesces rapid requests into a single build. */
6023
+ requestBuild() {
6024
+ this._dirty = true;
6025
+ if (!this._building) this.runBuild();
6026
+ }
6027
+ /** Run builds until no longer dirty. Only one instance runs at a time. */
6028
+ async runBuild() {
6029
+ this._building = true;
6030
+ while (this._dirty) {
6031
+ this._dirty = false;
6032
+ if (!await this.initialize()) break;
6033
+ const mainSource = this._weslSrc[this._rootModuleName];
6034
+ if (!mainSource) {
6035
+ console.warn(`wgsl-play: root module "${this._rootModuleName}" not found in sources:`, Object.keys(this._weslSrc));
6036
+ continue;
6037
+ }
6038
+ try {
6039
+ this.errorOverlay.hide();
6040
+ if (!this._fromFullProject) {
6041
+ const { weslSrc, libs } = await fetchDependencies(mainSource, {
6042
+ shaderRoot: this.getConfigOverrides()?.shaderRoot,
6043
+ existingSources: this._weslSrc,
6044
+ skipExternal: !this._fetchLibs
6045
+ });
6046
+ this._weslSrc = {
6047
+ ...this._weslSrc,
6048
+ ...weslSrc
6049
+ };
6050
+ this._libs = dedupLibs(this._libs, libs);
6051
+ }
6052
+ await createPipeline(this.renderState, mainSource, {
6053
+ ...this._linkOptions,
6054
+ weslSrc: this._weslSrc,
6055
+ libs: this._libs,
6056
+ rootModuleName: this._rootModuleName
6057
+ });
6058
+ if (!this._dirty) this.dispatchEvent(new CustomEvent("compile-success"));
6059
+ } catch (error) {
6060
+ if (!this._dirty) this.handleCompileError(error);
6061
+ }
6073
6062
  }
6063
+ this._building = false;
6074
6064
  }
6075
6065
  handleCompileError(error) {
6076
6066
  const message = error?.message ?? String(error);
@@ -6127,6 +6117,12 @@ function upgradeProperty(el, prop) {
6127
6117
  el[prop] = value;
6128
6118
  }
6129
6119
  }
6120
+ /** Merge new libs, deduplicating by bundle name. */
6121
+ function dedupLibs(existing, newLibs) {
6122
+ if (!existing || newLibs.length === 0) return [...existing ?? [], ...newLibs];
6123
+ const names = new Set(newLibs.map((b) => b.name));
6124
+ return [...existing.filter((b) => !names.has(b.name)), ...newLibs];
6125
+ }
6130
6126
  /** Normalize all keys in a weslSrc record to module paths. */
6131
6127
  function toModulePaths(weslSrc, pkg) {
6132
6128
  const result = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wgsl-play",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -23,8 +23,8 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "wesl": "0.7.23",
26
- "wesl-gpu": "0.1.25",
27
- "wesl-fetch": "0.0.11"
26
+ "wesl-fetch": "0.0.11",
27
+ "wesl-gpu": "0.1.25"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@playwright/test": "^1.53.2",
package/src/Renderer.ts CHANGED
@@ -84,19 +84,25 @@ export async function createPipeline(
84
84
  options?: LinkOptions,
85
85
  ): Promise<void> {
86
86
  state.device.pushErrorScope("validation");
87
- const pipeline = await linkAndCreatePipeline({
88
- device: state.device,
89
- fragmentSource,
90
- format: state.presentationFormat,
91
- layout: state.pipelineLayout,
92
- ...options,
93
- });
94
- const gpuError = await state.device.popErrorScope();
95
- if (gpuError) {
87
+ let gpuError: unknown;
88
+ let jsError: unknown;
89
+ try {
90
+ state.pipeline = await linkAndCreatePipeline({
91
+ device: state.device,
92
+ fragmentSource,
93
+ format: state.presentationFormat,
94
+ layout: state.pipelineLayout,
95
+ ...options,
96
+ });
97
+ } catch (e) {
98
+ jsError = e;
99
+ } finally {
100
+ gpuError = await state.device.popErrorScope();
101
+ }
102
+ if (jsError || gpuError) {
96
103
  state.pipeline = undefined;
97
- throw gpuError;
104
+ throw jsError ?? gpuError;
98
105
  }
99
- state.pipeline = pipeline;
100
106
  }
101
107
 
102
108
  /** Start the render loop. Returns a stop function. */
package/src/WgslPlay.ts CHANGED
@@ -83,6 +83,8 @@ export class WgslPlay extends HTMLElement {
83
83
  private _sourceEl: HTMLElement | null = null;
84
84
  private _sourceListener: ((e: Event) => void) | null = null;
85
85
  private _fetchLibs = true;
86
+ private _dirty = false;
87
+ private _building = false;
86
88
  private _theme: "light" | "dark" | "auto" = "auto";
87
89
  private _mediaQuery: MediaQueryList | null = null;
88
90
  private _onFullscreenChange = () =>
@@ -193,7 +195,7 @@ export class WgslPlay extends HTMLElement {
193
195
  this._weslSrc = { [this._rootModuleName]: value };
194
196
  this._libs = undefined;
195
197
  this._fromFullProject = false;
196
- this.discoverAndRebuild();
198
+ this.requestBuild();
197
199
  }
198
200
 
199
201
  /** Conditions for conditional compilation (@if/@elif/@else). */
@@ -204,8 +206,7 @@ export class WgslPlay extends HTMLElement {
204
206
  set conditions(value: Conditions) {
205
207
  this._linkOptions = { ...this._linkOptions, conditions: value };
206
208
  if (Object.keys(this._weslSrc).length === 0) return;
207
- if (this._fromFullProject) this.rebuildPipeline();
208
- else this.discoverAndRebuild();
209
+ this.requestBuild();
209
210
  }
210
211
 
211
212
  /** Set project configuration (mirrors wesl link() API). */
@@ -230,10 +231,8 @@ export class WgslPlay extends HTMLElement {
230
231
  return;
231
232
  }
232
233
 
233
- // Partial update - may need to refetch if conditions changed
234
234
  if (Object.keys(this._weslSrc).length === 0) return;
235
- if (this._fromFullProject) this.rebuildPipeline();
236
- else this.discoverAndRebuild();
235
+ this.requestBuild();
237
236
  }
238
237
 
239
238
  /** Set sources from a full project with weslSrc. */
@@ -246,7 +245,7 @@ export class WgslPlay extends HTMLElement {
246
245
  this._weslSrc = toModulePaths(weslSrc, pkg);
247
246
  this._rootModuleName = fileToModulePath(root, pkg, false);
248
247
  this._fromFullProject = true;
249
- this.rebuildPipeline();
248
+ this.requestBuild();
250
249
  }
251
250
 
252
251
  /** Whether to auto-fetch missing library packages from npm (default: true). */
@@ -365,7 +364,7 @@ export class WgslPlay extends HTMLElement {
365
364
  ? "premultiplied"
366
365
  : "opaque";
367
366
  this.renderState = await initWebGPU(this.canvas, alphaMode);
368
- await this.loadInitialContent();
367
+ this.loadInitialContent();
369
368
  this.stopRenderLoop = startRenderLoop(this.renderState, this.playback);
370
369
  this.dispatchEvent(new CustomEvent("ready"));
371
370
  return true;
@@ -383,12 +382,18 @@ export class WgslPlay extends HTMLElement {
383
382
  }
384
383
 
385
384
  /** Load from source element, src URL, script child, or inline textContent. */
386
- private async loadInitialContent(): Promise<void> {
385
+ private loadInitialContent(): void {
387
386
  const sourceId = this.getAttribute("source");
388
- if (sourceId) return this.connectToSource(sourceId);
387
+ if (sourceId) {
388
+ this.connectToSource(sourceId);
389
+ return;
390
+ }
389
391
 
390
392
  const src = this.getAttribute("src");
391
- if (src) return this.loadFromUrl(src);
393
+ if (src) {
394
+ this.loadFromUrl(src);
395
+ return;
396
+ }
392
397
 
393
398
  // Prefer <script type="text/wgsl"> or <script type="text/wesl"> (no HTML escaping needed)
394
399
  const script = this.querySelector(
@@ -400,11 +405,11 @@ export class WgslPlay extends HTMLElement {
400
405
 
401
406
  this._weslSrc = { [this._rootModuleName]: inlineSource };
402
407
  this._fromFullProject = false;
403
- await this.discoverAndRebuild();
408
+ this.requestBuild();
404
409
  }
405
410
 
406
411
  /** Connect to a source provider element (e.g., wgsl-edit). */
407
- private async connectToSource(id: string): Promise<void> {
412
+ private connectToSource(id: string): void {
408
413
  const el = document.getElementById(id);
409
414
  if (!el) {
410
415
  console.error(`wgsl-play: source element "${id}" not found`);
@@ -421,7 +426,6 @@ export class WgslPlay extends HTMLElement {
421
426
  };
422
427
 
423
428
  // Load initial sources, conditions, and libs from source element.
424
- // Use discoverAndRebuild (not project setter) so external deps are fetched.
425
429
  const { conditions, rootModuleName, libs } = el as any;
426
430
  const root = rootModuleName ?? "main";
427
431
  this._weslSrc = getSources();
@@ -429,10 +433,9 @@ export class WgslPlay extends HTMLElement {
429
433
  if (conditions) this._linkOptions = { ...this._linkOptions, conditions };
430
434
  if (libs) this._libs = libs;
431
435
  this._fromFullProject = false;
432
- await this.discoverAndRebuild();
433
- this._fromFullProject = true; // fast rebuilds on subsequent edits
436
+ this.requestBuild();
434
437
 
435
- // Listen for changes - rebuild with cached libs
438
+ // Listen for changes - rebuild with libs from source element
436
439
  this._sourceListener = (e: Event) => {
437
440
  const detail = (e as CustomEvent).detail;
438
441
  const fallback = { [this._rootModuleName]: detail?.source ?? "" };
@@ -440,17 +443,15 @@ export class WgslPlay extends HTMLElement {
440
443
  weslSrc: detail?.sources ?? fallback,
441
444
  rootModuleName: detail?.rootModuleName,
442
445
  conditions: detail?.conditions,
446
+ libs: detail?.libs,
443
447
  };
444
448
  };
445
449
  el.addEventListener("change", this._sourceListener);
446
450
  }
447
451
 
448
- /** Fetch shader from URL, auto-fetching any imported dependencies. */
452
+ /** Fetch shader from URL, then trigger a build. */
449
453
  private async loadFromUrl(url: string): Promise<void> {
450
- if (!this.renderState) return;
451
-
452
454
  try {
453
- this.errorOverlay.hide();
454
455
  const { weslSrc, libs, rootModuleName } = await loadShaderFromUrl(
455
456
  url,
456
457
  this.getConfigOverrides()?.shaderRoot,
@@ -459,88 +460,58 @@ export class WgslPlay extends HTMLElement {
459
460
  this._libs = libs;
460
461
  this._fromFullProject = false;
461
462
  if (rootModuleName) this._rootModuleName = rootModuleName;
462
-
463
- const mainSource = weslSrc[this._rootModuleName];
464
- if (!mainSource) {
465
- console.warn(
466
- `wgsl-play: root module "${this._rootModuleName}" not found in sources:`,
467
- Object.keys(weslSrc),
468
- );
469
- return;
470
- }
471
-
472
- await createPipeline(this.renderState, mainSource, {
473
- ...this._linkOptions,
474
- weslSrc,
475
- libs,
476
- rootModuleName: this._rootModuleName,
477
- });
478
- this.dispatchEvent(new CustomEvent("compile-success"));
463
+ this.requestBuild();
479
464
  } catch (error) {
480
465
  this.handleCompileError(error);
481
466
  }
482
467
  }
483
468
 
484
- /** Rebuild GPU pipeline using stored state. For full projects with all sources. */
485
- private async rebuildPipeline(): Promise<void> {
486
- if (!(await this.initialize())) return;
487
-
488
- const mainSource = this._weslSrc[this._rootModuleName];
489
- if (!mainSource) {
490
- console.warn(
491
- `wgsl-play: root module "${this._rootModuleName}" not found in sources:`,
492
- Object.keys(this._weslSrc),
493
- );
494
- return;
495
- }
496
-
497
- try {
498
- this.errorOverlay.hide();
499
- await createPipeline(this.renderState!, mainSource, {
500
- ...this._linkOptions,
501
- weslSrc: this._weslSrc,
502
- libs: this._libs,
503
- rootModuleName: this._rootModuleName,
504
- });
505
- this.dispatchEvent(new CustomEvent("compile-success"));
506
- } catch (error) {
507
- this.handleCompileError(error);
508
- }
469
+ /** Mark build as needed. Coalesces rapid requests into a single build. */
470
+ private requestBuild(): void {
471
+ this._dirty = true;
472
+ if (!this._building) this.runBuild();
509
473
  }
510
474
 
511
- /** Discover dependencies and rebuild. For HTTP/inline sources that may need fetching. */
512
- private async discoverAndRebuild(): Promise<void> {
513
- if (!(await this.initialize())) return;
475
+ /** Run builds until no longer dirty. Only one instance runs at a time. */
476
+ private async runBuild(): Promise<void> {
477
+ this._building = true;
478
+ while (this._dirty) {
479
+ this._dirty = false;
480
+ if (!(await this.initialize())) break;
514
481
 
515
- const mainSource = this._weslSrc[this._rootModuleName];
516
- if (!mainSource) {
517
- console.warn(
518
- `wgsl-play: root module "${this._rootModuleName}" not found in sources:`,
519
- Object.keys(this._weslSrc),
520
- );
521
- return;
522
- }
482
+ const mainSource = this._weslSrc[this._rootModuleName];
483
+ if (!mainSource) {
484
+ console.warn(
485
+ `wgsl-play: root module "${this._rootModuleName}" not found in sources:`,
486
+ Object.keys(this._weslSrc),
487
+ );
488
+ continue;
489
+ }
523
490
 
524
- try {
525
- this.errorOverlay.hide();
526
- const { weslSrc, libs } = await fetchDependencies(mainSource, {
527
- shaderRoot: this.getConfigOverrides()?.shaderRoot,
528
- existingSources: this._weslSrc,
529
- skipExternal: !this._fetchLibs,
530
- });
531
- this._weslSrc = { ...this._weslSrc, ...weslSrc };
532
- this._libs = [...(this._libs ?? []), ...libs];
533
-
534
- await createPipeline(this.renderState!, mainSource, {
535
- ...this._linkOptions,
536
- weslSrc: this._weslSrc,
537
- libs: this._libs,
538
- rootModuleName: this._rootModuleName,
539
- });
540
- this.dispatchEvent(new CustomEvent("compile-success"));
541
- } catch (error) {
542
- this.handleCompileError(error);
491
+ try {
492
+ this.errorOverlay.hide();
493
+ if (!this._fromFullProject) {
494
+ const { weslSrc, libs } = await fetchDependencies(mainSource, {
495
+ shaderRoot: this.getConfigOverrides()?.shaderRoot,
496
+ existingSources: this._weslSrc,
497
+ skipExternal: !this._fetchLibs,
498
+ });
499
+ this._weslSrc = { ...this._weslSrc, ...weslSrc };
500
+ this._libs = dedupLibs(this._libs, libs);
501
+ }
502
+ await createPipeline(this.renderState!, mainSource, {
503
+ ...this._linkOptions,
504
+ weslSrc: this._weslSrc,
505
+ libs: this._libs,
506
+ rootModuleName: this._rootModuleName,
507
+ });
508
+ if (!this._dirty)
509
+ this.dispatchEvent(new CustomEvent("compile-success"));
510
+ } catch (error) {
511
+ if (!this._dirty) this.handleCompileError(error);
512
+ }
543
513
  }
514
+ this._building = false;
544
515
  }
545
516
 
546
517
  private handleCompileError(error: unknown): void {
@@ -616,6 +587,17 @@ function upgradeProperty(el: HTMLElement, prop: string): void {
616
587
  }
617
588
  }
618
589
 
590
+ /** Merge new libs, deduplicating by bundle name. */
591
+ function dedupLibs(
592
+ existing: WeslBundle[] | undefined,
593
+ newLibs: WeslBundle[],
594
+ ): WeslBundle[] {
595
+ if (!existing || newLibs.length === 0)
596
+ return [...(existing ?? []), ...newLibs];
597
+ const names = new Set(newLibs.map(b => b.name));
598
+ return [...existing.filter(b => !names.has(b.name)), ...newLibs];
599
+ }
600
+
619
601
  /** Normalize all keys in a weslSrc record to module paths. */
620
602
  function toModulePaths(
621
603
  weslSrc: Record<string, string>,
@@ -221,20 +221,36 @@ test("connectToSource - external deps are fetched", async ({ page }) => {
221
221
  await expectCanvasSnapshot(page, "#player11", "connect-source-external.png");
222
222
  });
223
223
 
224
+ test("connectToSource - dynamic npm package loading", async ({ page }) => {
225
+ await page.goto("/");
226
+ await waitForFrame(page, "#player12");
227
+
228
+ // Inject code referencing random_wgsl (not in initial content)
229
+ await page.click("#inject-npm12");
230
+ await page.waitForLoadState("networkidle");
231
+ await waitForFrame(page, "#player12");
232
+
233
+ const hasError = await page.evaluate(
234
+ () => (document.querySelector("#player12") as any)?.hasError ?? true,
235
+ );
236
+ expect(hasError).toBe(false);
237
+ await expectCanvasSnapshot(page, "#player12", "connect-dynamic-npm.png");
238
+ });
239
+
224
240
  test("editor.link() with virtualLibs resolves env:: module", async ({
225
241
  page,
226
242
  }) => {
227
243
  await page.goto("/");
228
244
  await waitForWgslPlay(page);
229
245
 
230
- await page.click("#link-btn12");
246
+ await page.click("#link-btn13");
231
247
  await page.waitForFunction(
232
248
  () =>
233
- document.querySelector("#link-output12")?.textContent !==
249
+ document.querySelector("#link-output13")?.textContent !==
234
250
  "Not linked yet",
235
251
  );
236
252
 
237
- const output = await page.textContent("#link-output12");
253
+ const output = await page.textContent("#link-output13");
238
254
  expect(output).toContain("var<uniform>");
239
255
  expect(output).toContain("fs_main");
240
256
  expect(output).not.toContain("Error");