wgsl-edit 0.0.22 → 0.0.24

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
@@ -2,7 +2,6 @@ import { LRLanguage, LanguageSupport } from "@codemirror/language";
2
2
  import { linter } from "@codemirror/lint";
3
3
  import { parser, weslHighlighting } from "lezer-wesl";
4
4
  import { BundleResolver, CompositeResolver, RecordResolver, WeslParseError, bindIdents } from "wesl";
5
-
6
5
  //#region src/Language.ts
7
6
  const weslLanguage = LRLanguage.define({
8
7
  name: "wesl",
@@ -22,15 +21,33 @@ function wesl() {
22
21
  function createWeslLinter(config) {
23
22
  return linter(async () => lintAndFetch(config), { delay: 300 });
24
23
  }
25
- /** 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 */
26
25
  async function lintAndFetch(config) {
27
26
  const libs = config.getLibs?.() ?? [];
28
27
  const ignored = new Set(config.ignorePackages?.() ?? []);
29
- const { diagnostics, externals } = lintPass(config, libs, ignored);
30
- if (!config.fetchLibs || !externals.length) return diagnostics;
31
- const newLibs = await config.fetchLibs(externals);
32
- if (!newLibs.length) return diagnostics;
33
- 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;
34
51
  }
35
52
  /** Parse, bind, collect diagnostics and discover missing externals. */
36
53
  function lintPass(config, libs, ignored) {
@@ -38,82 +55,71 @@ function lintPass(config, libs, ignored) {
38
55
  const rootModule = config.rootModule();
39
56
  const diagnostics = [];
40
57
  let externals = [];
58
+ let weslErrorCount = 0;
41
59
  try {
42
60
  const resolver = buildResolver(sources, libs, config.packageName?.());
43
61
  const rootAst = resolver.resolveModule(rootModule);
44
62
  if (rootAst) {
45
- const result = runBind(config, resolver, rootAst);
46
- 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;
47
72
  externals = findMissingPackages(rootAst, result, resolver, ignored, libs);
48
73
  }
49
74
  } catch (e) {
50
75
  const diag = errorToDiagnostic(e);
51
- if (diag) diagnostics.push(diag);
76
+ if (diag) {
77
+ diagnostics.push(diag);
78
+ weslErrorCount++;
79
+ }
52
80
  }
53
81
  diagnostics.push(...config.getExternalDiagnostics?.() ?? []);
54
82
  return {
55
83
  diagnostics,
56
- externals
84
+ externals,
85
+ weslErrorCount
57
86
  };
58
87
  }
59
- /** Build a resolver from sources and optional libs. */
60
88
  function buildResolver(sources, libs, packageName) {
61
89
  const record = new RecordResolver(sources, { packageName });
62
90
  if (libs.length === 0) return record;
63
91
  return new CompositeResolver([record, ...libs.map((b) => new BundleResolver(b))]);
64
92
  }
65
- /** Parse and bind identifiers starting from a root module. */
66
- function runBind(config, resolver, rootAst) {
67
- return bindIdents({
68
- resolver,
69
- rootAst,
70
- conditions: config.conditions?.(),
71
- accumulateUnbound: true
72
- });
73
- }
74
93
  /** Convert unbound refs to diagnostics, skipping ignored packages. */
75
94
  function unboundDiagnostics(result, rootModule, ignored) {
76
- const diagnostics = [];
77
- for (const ref of result.unbound ?? []) {
78
- if (ref.srcModule.modulePath !== rootModule) continue;
79
- if (ref.path.length > 1 && ignored.has(ref.path[0])) continue;
80
- diagnostics.push(unboundToDiagnostic(ref));
81
- }
82
- return diagnostics;
95
+ return (result.unbound ?? []).filter((ref) => ref.srcModule.modulePath === rootModule && !(ref.path.length > 1 && ignored.has(ref.path[0]))).map(unboundToDiagnostic);
83
96
  }
84
- /** Convert a WESL error to a CodeMirror diagnostic. */
85
97
  function errorToDiagnostic(e) {
86
- if (e instanceof WeslParseError) {
87
- const [from, to] = e.span;
88
- return {
89
- from,
90
- to,
91
- severity: "error",
92
- message: e.cause?.message ?? e.message
93
- };
94
- }
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
+ };
95
106
  }
96
107
  /** Find external package names not yet loaded, from unresolved imports and unbound refs. */
97
108
  function findMissingPackages(rootAst, result, resolver, ignored, libs) {
98
109
  const loaded = new Set(libs.map((b) => b.name));
99
- const pkgs = [];
100
- 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) => {
101
112
  const root = imp.segments[0]?.name;
102
- if (!root || !isExternalRoot(root) || ignored.has(root) || loaded.has(root)) continue;
113
+ if (!root || skip(root)) return false;
103
114
  const modPath = imp.segments.map((s) => s.name).join("::");
104
- if (!resolver.resolveModule(modPath)) pkgs.push(root);
105
- }
106
- for (const ref of result.unbound ?? []) {
107
- const root = ref.path[0];
108
- if (ref.path.length > 1 && isExternalRoot(root) && !ignored.has(root) && !loaded.has(root)) pkgs.push(root);
109
- }
110
- 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])];
111
119
  }
112
- /** @return true if root is an external package name (not package/super). */
113
120
  function isExternalRoot(root) {
114
121
  return root !== "package" && root !== "super";
115
122
  }
116
- /** Convert an unbound reference to a CodeMirror diagnostic. */
117
123
  function unboundToDiagnostic(ref) {
118
124
  return {
119
125
  from: ref.start,
@@ -122,6 +128,5 @@ function unboundToDiagnostic(ref) {
122
128
  message: `unresolved identifier '${ref.path.join("::")}'`
123
129
  };
124
130
  }
125
-
126
131
  //#endregion
127
- export { createWeslLinter, wesl, weslLanguage };
132
+ export { createWeslLinter, lintAndFetch, wesl, weslLanguage };