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 +4 -9
- package/dist/GpuValidator-BZSULsmE.js +48 -0
- package/dist/GpuValidator-D88-VTq9.js +48 -0
- package/dist/Language.d.ts +5 -1
- package/dist/Language.js +57 -52
- package/dist/WgslEdit-ByXfb3R9.js +805 -0
- package/dist/WgslEdit.d.ts +11 -18
- package/dist/WgslEdit.js +3 -762
- package/dist/index.js +2 -4
- package/dist/wgsl-edit.js +171 -282
- package/package.json +9 -7
- package/src/GpuValidator.ts +65 -0
- package/src/Language.ts +79 -66
- package/src/WgslEdit.ts +123 -79
- package/src/test/WgslEdit.e2e.ts +70 -0
- package/dist/WgslEdit-BiLVs-hz.js +0 -5
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
|
|
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` -
|
|
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` - `
|
|
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/
|
|
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 };
|
package/dist/Language.d.ts
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 =
|
|
46
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
100
|
-
|
|
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 ||
|
|
113
|
+
if (!root || skip(root)) return false;
|
|
103
114
|
const modPath = imp.segments.map((s) => s.name).join("::");
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
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 };
|