wgsl-edit 0.0.23 → 0.0.25

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
@@ -38,7 +38,7 @@ Multiple `<script>` tags create a multi-file editor with tabs.
38
38
  <wgsl-edit id="editor" theme="auto">
39
39
  <script type="text/wesl">/* shader code */</script>
40
40
  </wgsl-edit>
41
- <wgsl-play source="editor"></wgsl-play>
41
+ <wgsl-play from="editor"></wgsl-play>
42
42
  ```
43
43
 
44
44
  The play component reads sources from the editor and live-previews the shader.
@@ -49,10 +49,6 @@ The play component reads sources from the editor and live-previews the shader.
49
49
  const editor = document.querySelector("wgsl-edit");
50
50
 
51
51
  editor.source = shaderCode; // set active file content
52
- editor.sources = { // set all files
53
- "package::main": mainCode,
54
- "package::utils": utilsCode,
55
- };
56
52
  editor.addFile("helpers.wesl", code); // add a file
57
53
  editor.activeFile = "helpers.wesl"; // switch tabs
58
54
 
@@ -82,9 +78,8 @@ editor.project = { // load a full project
82
78
  ### Properties
83
79
 
84
80
  - `source: string` - Get/set active file content
85
- - `sources: Record<string, string>` - Get/set all files (keyed by module path)
86
81
  - `conditions: Record<string, boolean>` - Get/set conditions for conditional compilation (`@if`/`@elif`/`@else`)
87
- - `project: WeslProject` - Set full project (sources, conditions, packageName, etc.)
82
+ - `project: WeslProject` - Get/set full project (weslSrc, conditions, constants, packageName, libs)
88
83
  - `activeFile: string` - Get/set active file name
89
84
  - `fileNames: string[]` - List all file names
90
85
  - `theme`, `tabs`, `lint`, `lineNumbers`, `readonly`, `shaderRoot`, `fetchLibs` - Mirror attributes
@@ -98,13 +93,13 @@ editor.project = { // load a full project
98
93
 
99
94
  ### Events
100
95
 
101
- - `change` - `{ source, sources, activeFile, conditions }` on edit or conditions change
96
+ - `change` - `WeslProject` detail on edit or conditions change
102
97
  - `file-change` - `{ action, file }` on add/remove/rename
103
98
 
104
99
  ## Using with wesl-plugin
105
100
 
106
101
  For full project support (libraries, conditional compilation, constants),
107
- use [wesl-plugin](https://github.com/wgsl-tooling-wg/wesl-js/tree/main/tools/packages/wesl-plugin)
102
+ use [wesl-plugin](https://github.com/wgsl-tooling-wg/wesl-js/tree/main/packages/wesl-plugin)
108
103
  to assemble shaders at build time and pass them to the editor via `project`.
109
104
 
110
105
  ```typescript
@@ -0,0 +1,48 @@
1
+ //#region src/GpuValidator.ts
2
+ /** Lazy-loaded GPU device singleton for shader validation. */
3
+ let device = null;
4
+ let initPromise = null;
5
+ let warned = false;
6
+ /** Get or initialize the shared GPU device, returning null if WebGPU is unavailable. */
7
+ async function getDevice() {
8
+ if (device) return device;
9
+ if (initPromise) return initPromise;
10
+ if (typeof navigator === "undefined" || !navigator.gpu) {
11
+ if (!warned) console.warn("wgsl-edit: WebGPU unavailable, GPU lint disabled");
12
+ warned = true;
13
+ return null;
14
+ }
15
+ initPromise = (async () => {
16
+ try {
17
+ const adapter = await navigator.gpu.requestAdapter();
18
+ if (!adapter) {
19
+ console.warn("wgsl-edit: no GPU adapter, GPU lint disabled");
20
+ return null;
21
+ }
22
+ const dev = await adapter.requestDevice();
23
+ dev.lost.then(() => {
24
+ device = null;
25
+ initPromise = null;
26
+ });
27
+ device = dev;
28
+ return dev;
29
+ } catch (e) {
30
+ console.warn("wgsl-edit: GPU device request failed", e);
31
+ return null;
32
+ }
33
+ })();
34
+ return initPromise;
35
+ }
36
+ /** Validate WGSL code via WebGPU createShaderModule + getCompilationInfo. */
37
+ async function validateWgsl(code) {
38
+ const dev = await getDevice();
39
+ if (!dev) return [];
40
+ return (await dev.createShaderModule({ code }).getCompilationInfo()).messages.filter((m) => m.type !== "info").map((m) => ({
41
+ offset: m.offset,
42
+ length: m.length,
43
+ message: m.message,
44
+ severity: m.type
45
+ }));
46
+ }
47
+ //#endregion
48
+ export { validateWgsl };
@@ -0,0 +1,48 @@
1
+ //#region src/GpuValidator.ts
2
+ /** Lazy-loaded GPU device singleton for shader validation. */
3
+ let device = null;
4
+ let initPromise = null;
5
+ let warned = false;
6
+ /** Get or initialize the shared GPU device, returning null if WebGPU is unavailable. */
7
+ async function getDevice() {
8
+ if (device) return device;
9
+ if (initPromise) return initPromise;
10
+ if (typeof navigator === "undefined" || !navigator.gpu) {
11
+ if (!warned) console.warn("wgsl-edit: WebGPU unavailable, GPU lint disabled");
12
+ warned = true;
13
+ return null;
14
+ }
15
+ initPromise = (async () => {
16
+ try {
17
+ const adapter = await navigator.gpu.requestAdapter();
18
+ if (!adapter) {
19
+ console.warn("wgsl-edit: no GPU adapter, GPU lint disabled");
20
+ return null;
21
+ }
22
+ const dev = await adapter.requestDevice();
23
+ dev.lost.then(() => {
24
+ device = null;
25
+ initPromise = null;
26
+ });
27
+ device = dev;
28
+ return dev;
29
+ } catch (e) {
30
+ console.warn("wgsl-edit: GPU device request failed", e);
31
+ return null;
32
+ }
33
+ })();
34
+ return initPromise;
35
+ }
36
+ /** Validate WGSL code via WebGPU createShaderModule + getCompilationInfo. */
37
+ async function validateWgsl(code) {
38
+ const dev = await getDevice();
39
+ if (!dev) return [];
40
+ return (await dev.createShaderModule({ code }).getCompilationInfo()).messages.filter((m) => m.type !== "info").map((m) => ({
41
+ offset: m.offset,
42
+ length: m.length,
43
+ message: m.message,
44
+ severity: m.type
45
+ }));
46
+ }
47
+ //#endregion
48
+ export { validateWgsl };
@@ -21,10 +21,14 @@ interface WeslLintConfig {
21
21
  fetchLibs?: (packageNames: string[]) => Promise<WeslBundle[]>;
22
22
  /** Package names to ignore when checking unbound externals (e.g. virtual modules). */
23
23
  ignorePackages?: () => string[];
24
+ /** GPU validation of linked WGSL. Called after WESL lint passes with no errors. */
25
+ gpuValidate?: () => Promise<Diagnostic[]>;
24
26
  }
25
27
  declare const weslLanguage: LRLanguage;
26
28
  declare function wesl(): LanguageSupport;
27
29
  /** Create a linter that validates WESL using the canonical parser. */
28
30
  declare function createWeslLinter(config: WeslLintConfig): _codemirror_state0.Extension;
31
+ /** Lint once, fetch missing externals if needed, re-lint with new libs. @internal */
32
+ declare function lintAndFetch(config: WeslLintConfig): Promise<Diagnostic[]>;
29
33
  //#endregion
30
- export { WeslLintConfig, createWeslLinter, wesl, weslLanguage };
34
+ export { WeslLintConfig, createWeslLinter, lintAndFetch, wesl, weslLanguage };
package/dist/Language.js CHANGED
@@ -21,15 +21,33 @@ function wesl() {
21
21
  function createWeslLinter(config) {
22
22
  return linter(async () => lintAndFetch(config), { delay: 300 });
23
23
  }
24
- /** Lint once, fetch missing externals if needed, re-lint with new libs. */
24
+ /** Lint once, fetch missing externals if needed, re-lint with new libs. @internal */
25
25
  async function lintAndFetch(config) {
26
26
  const libs = config.getLibs?.() ?? [];
27
27
  const ignored = new Set(config.ignorePackages?.() ?? []);
28
- const { diagnostics, externals } = lintPass(config, libs, ignored);
29
- if (!config.fetchLibs || !externals.length) return diagnostics;
30
- const newLibs = await config.fetchLibs(externals);
31
- if (!newLibs.length) return diagnostics;
32
- return lintPass(config, [...libs, ...newLibs], ignored).diagnostics;
28
+ let result = lintPass(config, libs, ignored);
29
+ let { diagnostics, externals, weslErrorCount } = result;
30
+ if (config.fetchLibs && externals.length) {
31
+ const newLibs = await config.fetchLibs(externals);
32
+ if (newLibs.length) {
33
+ result = lintPass(config, [...libs, ...newLibs], ignored);
34
+ ({diagnostics, weslErrorCount} = result);
35
+ }
36
+ }
37
+ if (weslErrorCount === 0 && config.gpuValidate) try {
38
+ const gpuDiags = await config.gpuValidate();
39
+ diagnostics.push(...gpuDiags);
40
+ } catch (e) {
41
+ const msg = e instanceof Error ? e.message : "unknown error";
42
+ diagnostics.push({
43
+ from: 0,
44
+ to: 0,
45
+ severity: "warning",
46
+ message: `GPU validation skipped: ${msg}`,
47
+ source: "WebGPU"
48
+ });
49
+ }
50
+ return diagnostics;
33
51
  }
34
52
  /** Parse, bind, collect diagnostics and discover missing externals. */
35
53
  function lintPass(config, libs, ignored) {
@@ -37,82 +55,71 @@ function lintPass(config, libs, ignored) {
37
55
  const rootModule = config.rootModule();
38
56
  const diagnostics = [];
39
57
  let externals = [];
58
+ let weslErrorCount = 0;
40
59
  try {
41
60
  const resolver = buildResolver(sources, libs, config.packageName?.());
42
61
  const rootAst = resolver.resolveModule(rootModule);
43
62
  if (rootAst) {
44
- const result = runBind(config, resolver, rootAst);
45
- diagnostics.push(...unboundDiagnostics(result, rootModule, ignored));
63
+ const result = bindIdents({
64
+ resolver,
65
+ rootAst,
66
+ conditions: config.conditions?.(),
67
+ accumulateUnbound: true
68
+ });
69
+ const unbound = unboundDiagnostics(result, rootModule, ignored);
70
+ diagnostics.push(...unbound);
71
+ weslErrorCount = unbound.length;
46
72
  externals = findMissingPackages(rootAst, result, resolver, ignored, libs);
47
73
  }
48
74
  } catch (e) {
49
75
  const diag = errorToDiagnostic(e);
50
- if (diag) diagnostics.push(diag);
76
+ if (diag) {
77
+ diagnostics.push(diag);
78
+ weslErrorCount++;
79
+ }
51
80
  }
52
81
  diagnostics.push(...config.getExternalDiagnostics?.() ?? []);
53
82
  return {
54
83
  diagnostics,
55
- externals
84
+ externals,
85
+ weslErrorCount
56
86
  };
57
87
  }
58
- /** Build a resolver from sources and optional libs. */
59
88
  function buildResolver(sources, libs, packageName) {
60
89
  const record = new RecordResolver(sources, { packageName });
61
90
  if (libs.length === 0) return record;
62
91
  return new CompositeResolver([record, ...libs.map((b) => new BundleResolver(b))]);
63
92
  }
64
- /** Parse and bind identifiers starting from a root module. */
65
- function runBind(config, resolver, rootAst) {
66
- return bindIdents({
67
- resolver,
68
- rootAst,
69
- conditions: config.conditions?.(),
70
- accumulateUnbound: true
71
- });
72
- }
73
93
  /** Convert unbound refs to diagnostics, skipping ignored packages. */
74
94
  function unboundDiagnostics(result, rootModule, ignored) {
75
- const diagnostics = [];
76
- for (const ref of result.unbound ?? []) {
77
- if (ref.srcModule.modulePath !== rootModule) continue;
78
- if (ref.path.length > 1 && ignored.has(ref.path[0])) continue;
79
- diagnostics.push(unboundToDiagnostic(ref));
80
- }
81
- return diagnostics;
95
+ return (result.unbound ?? []).filter((ref) => ref.srcModule.modulePath === rootModule && !(ref.path.length > 1 && ignored.has(ref.path[0]))).map(unboundToDiagnostic);
82
96
  }
83
- /** Convert a WESL error to a CodeMirror diagnostic. */
84
97
  function errorToDiagnostic(e) {
85
- if (e instanceof WeslParseError) {
86
- const [from, to] = e.span;
87
- return {
88
- from,
89
- to,
90
- severity: "error",
91
- message: e.cause?.message ?? e.message
92
- };
93
- }
98
+ if (!(e instanceof WeslParseError)) return void 0;
99
+ const [from, to] = e.span;
100
+ return {
101
+ from,
102
+ to,
103
+ severity: "error",
104
+ message: e.cause?.message ?? e.message
105
+ };
94
106
  }
95
107
  /** Find external package names not yet loaded, from unresolved imports and unbound refs. */
96
108
  function findMissingPackages(rootAst, result, resolver, ignored, libs) {
97
109
  const loaded = new Set(libs.map((b) => b.name));
98
- const pkgs = [];
99
- for (const imp of rootAst.imports) {
110
+ const skip = (r) => !isExternalRoot(r) || ignored.has(r) || loaded.has(r);
111
+ const fromImports = rootAst.imports.filter((imp) => {
100
112
  const root = imp.segments[0]?.name;
101
- if (!root || !isExternalRoot(root) || ignored.has(root) || loaded.has(root)) continue;
113
+ if (!root || skip(root)) return false;
102
114
  const modPath = imp.segments.map((s) => s.name).join("::");
103
- if (!resolver.resolveModule(modPath)) pkgs.push(root);
104
- }
105
- for (const ref of result.unbound ?? []) {
106
- const root = ref.path[0];
107
- if (ref.path.length > 1 && isExternalRoot(root) && !ignored.has(root) && !loaded.has(root)) pkgs.push(root);
108
- }
109
- return [...new Set(pkgs)];
115
+ return !resolver.resolveModule(modPath);
116
+ }).map((imp) => imp.segments[0].name);
117
+ const fromUnbound = (result.unbound ?? []).filter((ref) => ref.path.length > 1 && !skip(ref.path[0])).map((ref) => ref.path[0]);
118
+ return [...new Set([...fromImports, ...fromUnbound])];
110
119
  }
111
- /** @return true if root is an external package name (not package/super). */
112
120
  function isExternalRoot(root) {
113
121
  return root !== "package" && root !== "super";
114
122
  }
115
- /** Convert an unbound reference to a CodeMirror diagnostic. */
116
123
  function unboundToDiagnostic(ref) {
117
124
  return {
118
125
  from: ref.start,
@@ -122,4 +129,4 @@ function unboundToDiagnostic(ref) {
122
129
  };
123
130
  }
124
131
  //#endregion
125
- export { createWeslLinter, wesl, weslLanguage };
132
+ export { createWeslLinter, lintAndFetch, wesl, weslLanguage };
@@ -46,7 +46,8 @@ var WgslEdit = class extends HTMLElement {
46
46
  "lint",
47
47
  "lint-from",
48
48
  "line-numbers",
49
- "fetch-libs"
49
+ "fetch-libs",
50
+ "gpu-lint"
50
51
  ];
51
52
  editorView = null;
52
53
  editorContainer;
@@ -65,8 +66,10 @@ var WgslEdit = class extends HTMLElement {
65
66
  _rootModuleName;
66
67
  _tabs = true;
67
68
  _lint = "on";
69
+ _gpuLint = true;
68
70
  _fetchLibs = true;
69
71
  _conditions = {};
72
+ _constants;
70
73
  _packageName;
71
74
  _libs = [];
72
75
  _ignorePackages = ["constants", "env"];
@@ -123,6 +126,9 @@ var WgslEdit = class extends HTMLElement {
123
126
  } else if (name === "fetch-libs") {
124
127
  this._fetchLibs = value !== "false";
125
128
  this.updateLint();
129
+ } else if (name === "gpu-lint") {
130
+ this._gpuLint = value !== "off";
131
+ this.updateLint();
126
132
  }
127
133
  }
128
134
  /** Conditions for conditional compilation (@if/@elif/@else). */
@@ -177,10 +183,21 @@ var WgslEdit = class extends HTMLElement {
177
183
  if (firstKey) this.switchToFile(toTabName(firstKey));
178
184
  this.renderTabs();
179
185
  }
180
- /** Load a full project config (sources, conditions, packageName, etc.). */
186
+ get project() {
187
+ return {
188
+ weslSrc: this.sources,
189
+ rootModuleName: this._rootModuleName,
190
+ conditions: this._conditions,
191
+ constants: this._constants,
192
+ libs: this._libs,
193
+ packageName: this._packageName
194
+ };
195
+ }
181
196
  set project(value) {
182
- const { weslSrc, rootModuleName, conditions, packageName, libs } = value;
197
+ const { weslSrc, rootModuleName, conditions } = value;
198
+ const { constants, packageName, libs } = value;
183
199
  if (conditions !== void 0) this._conditions = conditions;
200
+ if (constants !== void 0) this._constants = constants;
184
201
  if (packageName !== void 0) this._packageName = packageName;
185
202
  if (libs !== void 0) this._libs = libs;
186
203
  if (rootModuleName !== void 0) this._rootModuleName = rootModuleName;
@@ -193,18 +210,22 @@ var WgslEdit = class extends HTMLElement {
193
210
  }
194
211
  /** Link/compile WESL sources into WGSL. Returns the compiled WGSL string. */
195
212
  async link(options) {
196
- const pkg = this._packageName ?? "package";
197
- const rootModuleName = this._rootModuleName ?? fileToModulePath(this._activeFile, pkg, false);
198
213
  return (await link({
214
+ ...this.linkParams(),
215
+ ...options
216
+ })).dest;
217
+ }
218
+ linkParams() {
219
+ const pkg = this._packageName ?? "package";
220
+ return {
199
221
  weslSrc: this.sources,
200
- rootModuleName,
222
+ rootModuleName: this._rootModuleName ?? fileToModulePath(this._activeFile, pkg, false),
201
223
  conditions: this._conditions,
224
+ constants: this._constants,
202
225
  libs: this._libs,
203
- packageName: pkg,
204
- ...options
205
- })).dest;
226
+ packageName: pkg
227
+ };
206
228
  }
207
- /** Library bundles for linking (set via project). */
208
229
  get libs() {
209
230
  return this._libs;
210
231
  }
@@ -216,19 +237,15 @@ var WgslEdit = class extends HTMLElement {
216
237
  this._rootModuleName = value;
217
238
  this.dispatchChange();
218
239
  }
219
- /** Currently active file name (selected tab). */
220
240
  get activeFile() {
221
241
  return this._activeFile;
222
242
  }
223
- /** Switch to a file by name. */
224
243
  set activeFile(name) {
225
244
  this.switchToFile(name);
226
245
  }
227
- /** List of file names in order. */
228
246
  get fileNames() {
229
247
  return Array.from(this._files.keys());
230
248
  }
231
- /** Tab bar visibility. */
232
249
  get tabs() {
233
250
  return this._tabs;
234
251
  }
@@ -253,6 +270,15 @@ var WgslEdit = class extends HTMLElement {
253
270
  if (value) this.setAttribute("line-numbers", "true");
254
271
  else this.removeAttribute("line-numbers");
255
272
  }
273
+ /** GPU validation of linked WGSL (default: true). Set to false to disable. */
274
+ get gpuLint() {
275
+ return this._gpuLint;
276
+ }
277
+ set gpuLint(value) {
278
+ this._gpuLint = value;
279
+ if (!value) this.setAttribute("gpu-lint", "off");
280
+ else this.removeAttribute("gpu-lint");
281
+ }
256
282
  /** Whether to auto-fetch missing library packages from npm (default: true). */
257
283
  get fetchLibs() {
258
284
  return this._fetchLibs;
@@ -262,7 +288,6 @@ var WgslEdit = class extends HTMLElement {
262
288
  if (value) this.removeAttribute("fetch-libs");
263
289
  else this.setAttribute("fetch-libs", "false");
264
290
  }
265
- /** Whether the editor is currently loading content. */
266
291
  get loading() {
267
292
  return this.snackbar.classList.contains("visible");
268
293
  }
@@ -303,7 +328,6 @@ var WgslEdit = class extends HTMLElement {
303
328
  if (value) this.setAttribute("shader-root", value);
304
329
  else this.removeAttribute("shader-root");
305
330
  }
306
- /** Add a new file. */
307
331
  addFile(name, content = "") {
308
332
  if (this._files.has(name)) return;
309
333
  this._files.set(name, { doc: Text.of(content.split("\n")) });
@@ -311,7 +335,6 @@ var WgslEdit = class extends HTMLElement {
311
335
  this.renderTabs();
312
336
  this.dispatchFileChange("add", name);
313
337
  }
314
- /** Remove a file. */
315
338
  removeFile(name) {
316
339
  if (!this._files.has(name) || this._files.size <= 1) return;
317
340
  this._files.delete(name);
@@ -322,7 +345,6 @@ var WgslEdit = class extends HTMLElement {
322
345
  this.renderTabs();
323
346
  this.dispatchFileChange("remove", name);
324
347
  }
325
- /** Rename a file. */
326
348
  renameFile(oldName, newName) {
327
349
  const state = this._files.get(oldName);
328
350
  if (!state || this._files.has(newName)) return;
@@ -353,32 +375,23 @@ var WgslEdit = class extends HTMLElement {
353
375
  }
354
376
  this.renderTabs();
355
377
  }
356
- /** Save current editor state to the active file. */
357
378
  saveCurrentFileState() {
358
- if (!this.editorView || !this._activeFile) return;
359
- const state = this._files.get(this._activeFile);
360
- if (state) {
361
- state.doc = this.editorView.state.doc;
362
- state.selection = this.editorView.state.selection;
363
- state.scrollPos = this.editorView.scrollDOM.scrollTop;
364
- }
379
+ const view = this.editorView;
380
+ const state = this._activeFile ? this._files.get(this._activeFile) : void 0;
381
+ if (!view || !state) return;
382
+ state.doc = view.state.doc;
383
+ state.selection = view.state.selection;
384
+ state.scrollPos = view.scrollDOM.scrollTop;
365
385
  }
366
386
  dispatchChange() {
367
- const { source, sources, conditions, _rootModuleName: rootModuleName, libs } = this;
368
- const detail = {
369
- source,
370
- sources,
371
- rootModuleName,
372
- conditions,
373
- libs
374
- };
375
- this.dispatchEvent(new CustomEvent("change", { detail }));
387
+ this.dispatchEvent(new CustomEvent("change", { detail: this.project }));
376
388
  }
377
389
  dispatchFileChange(action, file) {
378
- this.dispatchEvent(new CustomEvent("file-change", { detail: {
390
+ const detail = {
379
391
  action,
380
392
  file
381
- } }));
393
+ };
394
+ this.dispatchEvent(new CustomEvent("file-change", { detail }));
382
395
  }
383
396
  initEditor() {
384
397
  this.readInitialAttributes();
@@ -393,10 +406,11 @@ var WgslEdit = class extends HTMLElement {
393
406
  this._files.set("main.wesl", { doc: Text.of(initialDoc.split("\n")) });
394
407
  this._activeFile = "main.wesl";
395
408
  }
409
+ const extensions = this.buildExtensions();
396
410
  this.editorView = new EditorView({
397
411
  state: EditorState.create({
398
412
  doc: initialDoc,
399
- extensions: this.buildExtensions()
413
+ extensions
400
414
  }),
401
415
  parent: this.editorContainer
402
416
  });
@@ -471,11 +485,13 @@ var WgslEdit = class extends HTMLElement {
471
485
  this.editorView?.dispatch({ effects: this.themeCompartment.reconfigure(this.resolveTheme()) });
472
486
  }
473
487
  updateReadonly() {
474
- this.editorView?.dispatch({ effects: this.readonlyCompartment.reconfigure(EditorState.readOnly.of(this.readonly)) });
488
+ const ext = EditorState.readOnly.of(this.readonly);
489
+ this.editorView?.dispatch({ effects: this.readonlyCompartment.reconfigure(ext) });
475
490
  this.renderTabs();
476
491
  }
477
492
  resolveLint() {
478
493
  if (this._lint === "off") return [];
494
+ const useGpuLint = this._gpuLint && !this._lintFromEl;
479
495
  return createWeslLinter({
480
496
  getSources: () => this.sources,
481
497
  rootModule: () => fileToModulePath(this._activeFile, this._packageName ?? "package", false),
@@ -484,12 +500,22 @@ var WgslEdit = class extends HTMLElement {
484
500
  getExternalDiagnostics: () => this._externalDiagnostics,
485
501
  getLibs: () => this._libs,
486
502
  fetchLibs: this._fetchLibs ? (pkgs) => this.fetchLibsOnDemand(pkgs) : void 0,
487
- ignorePackages: () => this._ignorePackages
503
+ ignorePackages: () => this._ignorePackages,
504
+ gpuValidate: useGpuLint ? () => this.gpuValidate() : void 0
488
505
  });
489
506
  }
507
+ /** Link WESL->WGSL and validate via WebGPU, returning CodeMirror diagnostics. */
508
+ async gpuValidate() {
509
+ const { validateWgsl } = await import("./GpuValidator-BZSULsmE.js");
510
+ const params = this.linkParams();
511
+ const linked = await link(params);
512
+ const messages = await validateWgsl(linked.dest);
513
+ const pkg = params.packageName ?? "package";
514
+ return mapGpuDiagnostics(messages, linked, this._activeFile, pkg);
515
+ }
490
516
  /** Fetch missing library packages, deduplicating in-flight requests. */
491
- async fetchLibsOnDemand(packageNames) {
492
- const needed = packageNames.filter((n) => !this._fetchedPkgs.has(n) && !this._fetchingPkgs.has(n));
517
+ async fetchLibsOnDemand(names) {
518
+ const needed = names.filter((n) => !this._fetchedPkgs.has(n) && !this._fetchingPkgs.has(n));
493
519
  if (needed.length === 0) return [];
494
520
  for (const n of needed) this._fetchingPkgs.add(n);
495
521
  this.showSnack(`Loading ${needed.join(", ")}…`);
@@ -516,18 +542,20 @@ var WgslEdit = class extends HTMLElement {
516
542
  }
517
543
  /** Listen for compile-error/compile-success events from a lint source element. */
518
544
  connectLintSource(id) {
545
+ const hadExternal = !!this._lintFromEl;
519
546
  if (this._lintFromEl) {
520
547
  this._lintFromEl.removeEventListener("compile-error", this._boundCompileError);
521
548
  this._lintFromEl.removeEventListener("compile-success", this._boundCompileSuccess);
522
549
  this._lintFromEl = null;
523
550
  }
524
551
  this._externalDiagnostics = [];
525
- if (!id) return;
526
- const el = document.getElementById(id);
527
- if (!el) return;
528
- this._lintFromEl = el;
529
- el.addEventListener("compile-error", this._boundCompileError);
530
- el.addEventListener("compile-success", this._boundCompileSuccess);
552
+ const el = id ? document.getElementById(id) : null;
553
+ if (el) {
554
+ this._lintFromEl = el;
555
+ el.addEventListener("compile-error", this._boundCompileError);
556
+ el.addEventListener("compile-success", this._boundCompileSuccess);
557
+ }
558
+ if (hadExternal !== !!this._lintFromEl) this.updateLint();
531
559
  }
532
560
  onCompileError(e) {
533
561
  const detail = e.detail;
@@ -560,7 +588,8 @@ var WgslEdit = class extends HTMLElement {
560
588
  return this._lineNumbers ? [] : EditorView.theme({ ".cm-gutters": { display: "none" } });
561
589
  }
562
590
  updateLineNumbers() {
563
- this.editorView?.dispatch({ effects: this.lineNumbersCompartment.reconfigure(this.resolveLineNumbers()) });
591
+ const ext = this.resolveLineNumbers();
592
+ this.editorView?.dispatch({ effects: this.lineNumbersCompartment.reconfigure(ext) });
564
593
  }
565
594
  /** Parse script tags into _files. Supports single or multi-file via data-name. */
566
595
  parseInlineContent() {
@@ -576,7 +605,6 @@ var WgslEdit = class extends HTMLElement {
576
605
  this._files.set(name, { doc: Text.of(content.split("\n")) });
577
606
  }
578
607
  }
579
- /** Render tab bar based on files and visibility mode. */
580
608
  renderTabs() {
581
609
  this.tabBar.style.display = this._tabs ? "flex" : "none";
582
610
  if (!this._tabs) return;
@@ -584,7 +612,6 @@ var WgslEdit = class extends HTMLElement {
584
612
  for (const name of this._files.keys()) this.tabBar.appendChild(this.createTab(name));
585
613
  if (!this.readonly) this.tabBar.appendChild(this.createAddButton());
586
614
  }
587
- /** Create a tab button for a file. */
588
615
  createTab(name) {
589
616
  const tab = document.createElement("button");
590
617
  tab.className = "tab" + (name === this._activeFile ? " active" : "");
@@ -608,7 +635,6 @@ var WgslEdit = class extends HTMLElement {
608
635
  tab.addEventListener("click", () => this.switchToFile(name));
609
636
  return tab;
610
637
  }
611
- /** Create the "+" button for adding new files. */
612
638
  createAddButton() {
613
639
  const btn = document.createElement("button");
614
640
  btn.className = "tab-add";
@@ -621,7 +647,6 @@ var WgslEdit = class extends HTMLElement {
621
647
  });
622
648
  return btn;
623
649
  }
624
- /** Start inline rename of a tab. */
625
650
  startRenameTab(tab, nameSpan, oldName) {
626
651
  const input = document.createElement("input");
627
652
  input.className = "tab-rename";
@@ -738,6 +763,24 @@ function weslColors(c) {
738
763
  fontStyle: "normal"
739
764
  } }));
740
765
  }
766
+ /** Map GPU validation messages back to source positions via the source map. */
767
+ function mapGpuDiagnostics(messages, linked, activeFile, pkg) {
768
+ const { sourceMap } = linked;
769
+ const active = fileToModulePath(activeFile, pkg, false);
770
+ return messages.flatMap((msg) => {
771
+ const srcPos = sourceMap.destToSrc(msg.offset);
772
+ if ((srcPos.src.path ? fileToModulePath(srcPos.src.path, pkg, false) : null) !== active) return [];
773
+ const endPos = sourceMap.destToSrc(msg.offset + msg.length);
774
+ const from = srcPos.position;
775
+ return {
776
+ from,
777
+ to: endPos.position > from ? endPos.position : from + 1,
778
+ severity: msg.severity,
779
+ message: msg.message,
780
+ source: "WebGPU"
781
+ };
782
+ });
783
+ }
741
784
  function getStyles() {
742
785
  if (!cachedStyleSheet) {
743
786
  cachedStyleSheet = new CSSStyleSheet();