wgsl-edit 0.0.2
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/.turbo/turbo-build.log +19 -0
- package/.turbo/turbo-typecheck.log +5 -0
- package/README.md +168 -0
- package/bin/wgsl-edit +2 -0
- package/demo/index.html +68 -0
- package/demo/main.ts +22 -0
- package/demo/style.css +114 -0
- package/dist/Language.d.mts +30 -0
- package/dist/Language.mjs +127 -0
- package/dist/WgslEdit-D62UKrqG.mjs +649 -0
- package/dist/WgslEdit.d.mts +118 -0
- package/dist/WgslEdit.mjs +4 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +8 -0
- package/package.json +34 -0
- package/site/assets/index-1yVlrenS.js +134 -0
- package/site/assets/index-oyrhrEUu.css +1 -0
- package/site/index.html +68 -0
- package/src/Cli.ts +115 -0
- package/src/Language.ts +204 -0
- package/src/WgslEdit.css +168 -0
- package/src/WgslEdit.ts +783 -0
- package/src/index.ts +8 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +3 -0
- package/tsdown.config.ts +28 -0
- package/vite.config.ts +11 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> wgsl-edit@0.0.1 build /Users/lee/wesl/wesl-js/tools/packages/wgsl-edit
|
|
4
|
+
> tsdown
|
|
5
|
+
|
|
6
|
+
[34mℹ[39m tsdown [2mv0.19.0-beta.3[22m powered by rolldown [2mv1.0.0-beta.58[22m
|
|
7
|
+
[34mℹ[39m config file: [4m/Users/lee/wesl/wesl-js/tools/packages/wgsl-edit/tsdown.config.ts[24m
|
|
8
|
+
[34mℹ[39m entry: [34msrc/index.ts, src/Language.ts, src/WgslEdit.ts[39m
|
|
9
|
+
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
10
|
+
[34mℹ[39m Build start
|
|
11
|
+
[34mℹ[39m [2mdist/[22m[1mLanguage.mjs[22m [2m 4.39 kB[22m [2m│ gzip: 1.58 kB[22m
|
|
12
|
+
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m 0.27 kB[22m [2m│ gzip: 0.18 kB[22m
|
|
13
|
+
[34mℹ[39m [2mdist/[22m[1mWgslEdit.mjs[22m [2m 0.10 kB[22m [2m│ gzip: 0.09 kB[22m
|
|
14
|
+
[34mℹ[39m [2mdist/[22mWgslEdit-D62UKrqG.mjs [2m22.76 kB[22m [2m│ gzip: 6.47 kB[22m
|
|
15
|
+
[34mℹ[39m [2mdist/[22m[32m[1mWgslEdit.d.mts[22m[39m [2m 4.05 kB[22m [2m│ gzip: 1.49 kB[22m
|
|
16
|
+
[34mℹ[39m [2mdist/[22m[32m[1mLanguage.d.mts[22m[39m [2m 1.48 kB[22m [2m│ gzip: 0.68 kB[22m
|
|
17
|
+
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 0.14 kB[22m [2m│ gzip: 0.09 kB[22m
|
|
18
|
+
[34mℹ[39m 7 files, total: 33.20 kB
|
|
19
|
+
[32m✔[39m Build complete in [32m1881ms[39m
|
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# wgsl-edit
|
|
2
|
+
|
|
3
|
+
Web component for editing WESL/WGSL with CodeMirror 6.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<script type="module">import "wgsl-edit";</script>
|
|
9
|
+
|
|
10
|
+
<wgsl-edit></wgsl-edit>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Features syntax highlighting, linting, multi-file tabs, and light/dark themes out of the box.
|
|
14
|
+
|
|
15
|
+
### Inline source
|
|
16
|
+
|
|
17
|
+
Include shader code directly via `<script>` tags:
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<wgsl-edit>
|
|
21
|
+
<script type="text/wesl" data-name="main.wesl">
|
|
22
|
+
import package::utils;
|
|
23
|
+
@fragment fn main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
|
24
|
+
return vec4f(utils::gradient(pos.xy), 1.0);
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
<script type="text/wesl" data-name="utils.wesl">
|
|
28
|
+
fn gradient(uv: vec2f) -> vec3f { return vec3f(uv, 0.5); }
|
|
29
|
+
</script>
|
|
30
|
+
</wgsl-edit>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Multiple `<script>` tags create a multi-file editor with tabs.
|
|
34
|
+
|
|
35
|
+
### With wgsl-play
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<wgsl-edit id="editor" theme="auto">
|
|
39
|
+
<script type="text/wesl">/* shader code */</script>
|
|
40
|
+
</wgsl-edit>
|
|
41
|
+
<wgsl-play source="editor"></wgsl-play>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The play component reads sources from the editor and live-previews the shader.
|
|
45
|
+
|
|
46
|
+
### Programmatic control
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
const editor = document.querySelector("wgsl-edit");
|
|
50
|
+
|
|
51
|
+
editor.source = shaderCode; // set active file content
|
|
52
|
+
editor.sources = { // set all files
|
|
53
|
+
"package::main": mainCode,
|
|
54
|
+
"package::utils": utilsCode,
|
|
55
|
+
};
|
|
56
|
+
editor.addFile("helpers.wesl", code); // add a file
|
|
57
|
+
editor.activeFile = "helpers.wesl"; // switch tabs
|
|
58
|
+
|
|
59
|
+
editor.project = { // load a full project
|
|
60
|
+
weslSrc: { "package::main": code },
|
|
61
|
+
rootModuleName: "package::main",
|
|
62
|
+
conditions: { RED: true },
|
|
63
|
+
packageName: "myshader",
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
### Attributes
|
|
70
|
+
|
|
71
|
+
| Attribute | Values | Default | Description |
|
|
72
|
+
|-----------|--------|---------|-------------|
|
|
73
|
+
| `src` | URL | - | Load source from URL |
|
|
74
|
+
| `theme` | `light` `dark` `auto` | `auto` | Color theme |
|
|
75
|
+
| `readonly` | boolean | `false` | Disable editing |
|
|
76
|
+
| `tabs` | boolean | `true` | Show tab bar |
|
|
77
|
+
| `lint` | `on` `off` | `on` | Real-time WESL validation |
|
|
78
|
+
| `line-numbers` | `true` `false` | `false` | Show line numbers |
|
|
79
|
+
| `shader-root` | string | - | Root path for shader imports |
|
|
80
|
+
|
|
81
|
+
### Properties
|
|
82
|
+
|
|
83
|
+
- `source: string` - Get/set active file content
|
|
84
|
+
- `sources: Record<string, string>` - Get/set all files (keyed by module path)
|
|
85
|
+
- `project: WeslProject` - Set full project (sources, conditions, packageName, etc.)
|
|
86
|
+
- `activeFile: string` - Get/set active file name
|
|
87
|
+
- `fileNames: string[]` - List all file names
|
|
88
|
+
- `theme`, `tabs`, `lint`, `lineNumbers`, `readonly`, `shaderRoot` - Mirror attributes
|
|
89
|
+
|
|
90
|
+
### Methods
|
|
91
|
+
|
|
92
|
+
- `addFile(name, content?)` - Add a new file
|
|
93
|
+
- `removeFile(name)` - Remove a file
|
|
94
|
+
- `renameFile(oldName, newName)` - Rename a file
|
|
95
|
+
|
|
96
|
+
### Events
|
|
97
|
+
|
|
98
|
+
- `change` - `{ source, sources, activeFile }` on any edit
|
|
99
|
+
- `file-change` - `{ action, file }` on add/remove/rename
|
|
100
|
+
|
|
101
|
+
## Using with wesl-plugin
|
|
102
|
+
|
|
103
|
+
For full project support (libraries, conditional compilation, constants),
|
|
104
|
+
use [wesl-plugin](https://github.com/wgsl-tooling-wg/wesl-js/tree/main/tools/packages/wesl-plugin)
|
|
105
|
+
to assemble shaders at build time and pass them to the editor via `project`.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// vite.config.ts
|
|
109
|
+
import { linkBuildExtension } from "wesl-plugin";
|
|
110
|
+
import viteWesl from "wesl-plugin/vite";
|
|
111
|
+
|
|
112
|
+
export default {
|
|
113
|
+
plugins: [viteWesl({ extensions: [linkBuildExtension] })]
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// app.ts
|
|
117
|
+
import shaderConfig from "./shader.wesl?link";
|
|
118
|
+
|
|
119
|
+
const editor = document.querySelector("wgsl-edit");
|
|
120
|
+
editor.project = {
|
|
121
|
+
...shaderConfig,
|
|
122
|
+
conditions: { MOBILE: isMobileGPU },
|
|
123
|
+
constants: { num_lights: 4 }
|
|
124
|
+
};
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The `?link` import provides `weslSrc`, `libs`, `rootModuleName`, and `packageName`.
|
|
128
|
+
The editor's linter uses these to validate imports, conditions, and constants as you type.
|
|
129
|
+
|
|
130
|
+
## Styling
|
|
131
|
+
|
|
132
|
+
```css
|
|
133
|
+
wgsl-edit {
|
|
134
|
+
height: 400px;
|
|
135
|
+
border: 1px solid #444;
|
|
136
|
+
border-radius: 4px;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Bundle Size
|
|
141
|
+
|
|
142
|
+
~136 KB brotli for the full bundle with all dependencies.
|
|
143
|
+
|
|
144
|
+
| Component | Brotli |
|
|
145
|
+
|-----------|--------|
|
|
146
|
+
| CodeMirror (view, state, language, autocomplete, search, commands) | ~104 KB |
|
|
147
|
+
| lezer-wesl (grammar + lezer runtime) | ~26 KB |
|
|
148
|
+
| wesl linker (powers live linting) | ~14 KB |
|
|
149
|
+
| wgsl-edit (web component, theme, CSS) | ~1 KB |
|
|
150
|
+
| future work (tbd) | ~5 KB |
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
## CLI
|
|
154
|
+
|
|
155
|
+
Edit a shader file in the browser with live reload:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
wgsl-edit path/to/shader.wesl
|
|
159
|
+
wgsl-edit shader.wgsl --port 3000 --no-open
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Exports
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import "wgsl-edit"; // auto-registers element
|
|
166
|
+
import { WgslEdit } from "wgsl-edit/element"; // class only
|
|
167
|
+
import { wesl, weslLanguage } from "wgsl-edit/language"; // CodeMirror language
|
|
168
|
+
```
|
package/bin/wgsl-edit
ADDED
package/demo/index.html
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>wgsl-edit test</title>
|
|
7
|
+
<link rel="stylesheet" href="./style.css">
|
|
8
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css" id="prism-light">
|
|
9
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-tomorrow.min.css" id="prism-dark" disabled>
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js" defer></script>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<button id="theme-toggle" aria-label="Toggle theme">
|
|
14
|
+
<svg class="sun" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
15
|
+
<circle cx="12" cy="12" r="5"/>
|
|
16
|
+
<line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/>
|
|
17
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
|
18
|
+
<line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/>
|
|
19
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
|
20
|
+
</svg>
|
|
21
|
+
<svg class="moon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
22
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
23
|
+
</svg>
|
|
24
|
+
</button>
|
|
25
|
+
<div class="titles">
|
|
26
|
+
<h1>wgsl-edit</h1>
|
|
27
|
+
<h1>wgsl-play</h1>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="editor-player">
|
|
30
|
+
<wgsl-edit id="editor" lint-from="player">
|
|
31
|
+
<script type="text/wesl" data-name="main.wesl">
|
|
32
|
+
import package::utils;
|
|
33
|
+
|
|
34
|
+
@group(0) @binding(0) var<uniform> u: test::Uniforms;
|
|
35
|
+
|
|
36
|
+
@fragment
|
|
37
|
+
fn main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
|
38
|
+
let uv = pos.xy / u.resolution;
|
|
39
|
+
return vec4f(utils::gradient(uv, u.time), 1.0);
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
<script type="text/wesl" data-name="utils.wesl">
|
|
43
|
+
fn gradient(uv: vec2f, time: f32) -> vec3f {
|
|
44
|
+
return vec3f(uv, 0.5 + 0.5 * sin(time));
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
</wgsl-edit>
|
|
48
|
+
<wgsl-play id="player" source="editor"></wgsl-play>
|
|
49
|
+
</div>
|
|
50
|
+
<section class="usage">
|
|
51
|
+
<h2>Usage</h2>
|
|
52
|
+
<pre><code class="language-html"><script type="module" src="https://esm.sh/wgsl-edit"></script>
|
|
53
|
+
<script type="module" src="https://esm.sh/wgsl-play"></script>
|
|
54
|
+
|
|
55
|
+
<wgsl-edit id="editor" lint-from="player"></wgsl-edit>
|
|
56
|
+
<wgsl-play id="player" source="editor"></wgsl-play></code></pre>
|
|
57
|
+
|
|
58
|
+
<div class="docs-links">
|
|
59
|
+
<h2>Docs</h2>
|
|
60
|
+
<nav>
|
|
61
|
+
<a href="https://github.com/wgsl-tooling-wg/wesl-js/tree/main/tools/packages/wgsl-edit">wgsl-edit</a>
|
|
62
|
+
<a href="https://github.com/wgsl-tooling-wg/wesl-js/tree/main/tools/packages/wgsl-play">wgsl-play</a>
|
|
63
|
+
</nav>
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
<script type="module" src="./main.ts"></script>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
package/demo/main.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import "wgsl-edit";
|
|
2
|
+
import "wgsl-play";
|
|
3
|
+
import type { WgslEdit } from "wgsl-edit";
|
|
4
|
+
|
|
5
|
+
const editor = document.querySelector("wgsl-edit") as WgslEdit | null;
|
|
6
|
+
|
|
7
|
+
const isDark = () => matchMedia("(prefers-color-scheme: dark)").matches;
|
|
8
|
+
|
|
9
|
+
function applyTheme(dark: boolean) {
|
|
10
|
+
const theme = dark ? "dark" : "light";
|
|
11
|
+
if (editor) editor.theme = theme;
|
|
12
|
+
document.body.className = theme;
|
|
13
|
+
document.getElementById("prism-light")!.toggleAttribute("disabled", dark);
|
|
14
|
+
document.getElementById("prism-dark")!.toggleAttribute("disabled", !dark);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Start with system preference
|
|
18
|
+
applyTheme(isDark());
|
|
19
|
+
|
|
20
|
+
document.getElementById("theme-toggle")!.addEventListener("click", () => {
|
|
21
|
+
applyTheme(document.body.className !== "dark");
|
|
22
|
+
});
|
package/demo/style.css
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
body {
|
|
2
|
+
margin: 0;
|
|
3
|
+
padding: 20px;
|
|
4
|
+
font-family: system-ui, sans-serif;
|
|
5
|
+
transition:
|
|
6
|
+
background 0.2s,
|
|
7
|
+
color 0.2s;
|
|
8
|
+
}
|
|
9
|
+
body.dark {
|
|
10
|
+
background: #1e1e1e;
|
|
11
|
+
color: #ccc;
|
|
12
|
+
}
|
|
13
|
+
body.light {
|
|
14
|
+
background: #fff;
|
|
15
|
+
color: #333;
|
|
16
|
+
}
|
|
17
|
+
.titles {
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: 32px;
|
|
20
|
+
}
|
|
21
|
+
.titles h1:first-child {
|
|
22
|
+
flex: 1;
|
|
23
|
+
text-align: center;
|
|
24
|
+
}
|
|
25
|
+
.titles h1:last-child {
|
|
26
|
+
width: min(60vh, 50%);
|
|
27
|
+
text-align: center;
|
|
28
|
+
}
|
|
29
|
+
h1,
|
|
30
|
+
h2 {
|
|
31
|
+
margin-top: 0;
|
|
32
|
+
font-family: ui-monospace, "Cascadia Code", "Fira Code", Menlo, monospace;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
}
|
|
35
|
+
#theme-toggle {
|
|
36
|
+
position: fixed;
|
|
37
|
+
top: 16px;
|
|
38
|
+
right: 16px;
|
|
39
|
+
background: none;
|
|
40
|
+
border: none;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
padding: 6px;
|
|
43
|
+
border-radius: 8px;
|
|
44
|
+
color: inherit;
|
|
45
|
+
opacity: 0.7;
|
|
46
|
+
transition: opacity 0.2s;
|
|
47
|
+
}
|
|
48
|
+
#theme-toggle:hover {
|
|
49
|
+
opacity: 1;
|
|
50
|
+
}
|
|
51
|
+
#theme-toggle svg {
|
|
52
|
+
display: block;
|
|
53
|
+
}
|
|
54
|
+
#theme-toggle .sun {
|
|
55
|
+
display: none;
|
|
56
|
+
}
|
|
57
|
+
#theme-toggle .moon {
|
|
58
|
+
display: none;
|
|
59
|
+
}
|
|
60
|
+
body.light #theme-toggle .moon {
|
|
61
|
+
display: block;
|
|
62
|
+
}
|
|
63
|
+
body.dark #theme-toggle .sun {
|
|
64
|
+
display: block;
|
|
65
|
+
}
|
|
66
|
+
.editor-player {
|
|
67
|
+
display: flex;
|
|
68
|
+
gap: 32px;
|
|
69
|
+
height: 60vh;
|
|
70
|
+
}
|
|
71
|
+
wgsl-edit {
|
|
72
|
+
flex: 1;
|
|
73
|
+
}
|
|
74
|
+
wgsl-play {
|
|
75
|
+
aspect-ratio: 1;
|
|
76
|
+
width: min(60vh, 50%);
|
|
77
|
+
align-self: start;
|
|
78
|
+
}
|
|
79
|
+
.usage {
|
|
80
|
+
margin-top: 48px;
|
|
81
|
+
}
|
|
82
|
+
.usage pre {
|
|
83
|
+
padding: 16px 20px;
|
|
84
|
+
border-radius: 8px;
|
|
85
|
+
overflow-x: auto;
|
|
86
|
+
font-family: ui-monospace, "Cascadia Code", "Fira Code", Menlo, monospace;
|
|
87
|
+
font-size: 14px;
|
|
88
|
+
line-height: 1.5;
|
|
89
|
+
}
|
|
90
|
+
body.light .usage pre {
|
|
91
|
+
background: #f5f5f5;
|
|
92
|
+
color: #333;
|
|
93
|
+
}
|
|
94
|
+
body.dark .usage pre {
|
|
95
|
+
background: #2a2a2a;
|
|
96
|
+
color: #ccc;
|
|
97
|
+
}
|
|
98
|
+
.docs-links {
|
|
99
|
+
margin-top: 24px;
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: baseline;
|
|
102
|
+
font-size: 18px;
|
|
103
|
+
}
|
|
104
|
+
.docs-links h2 {
|
|
105
|
+
margin: 0;
|
|
106
|
+
}
|
|
107
|
+
.docs-links nav {
|
|
108
|
+
display: flex;
|
|
109
|
+
gap: 40px;
|
|
110
|
+
margin-left: 100px;
|
|
111
|
+
}
|
|
112
|
+
body.dark .docs-links a {
|
|
113
|
+
color: #6cb6ff;
|
|
114
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { LRLanguage, LanguageSupport } from "@codemirror/language";
|
|
2
|
+
import { Diagnostic } from "@codemirror/lint";
|
|
3
|
+
import { Conditions, WeslBundle } from "wesl";
|
|
4
|
+
import * as _codemirror_state0 from "@codemirror/state";
|
|
5
|
+
|
|
6
|
+
//#region src/Language.d.ts
|
|
7
|
+
interface WeslLintConfig {
|
|
8
|
+
/** Get all sources keyed by module path (e.g., "package::main"). */
|
|
9
|
+
getSources: () => Record<string, string>;
|
|
10
|
+
/** Root module to validate (e.g., "package::main"). */
|
|
11
|
+
rootModule: () => string;
|
|
12
|
+
/** Runtime conditions for @if conditional compilation. */
|
|
13
|
+
conditions?: () => Conditions;
|
|
14
|
+
/** Package name alias (enables `import mypkg::foo` alongside `import package::foo`). */
|
|
15
|
+
packageName?: () => string | undefined;
|
|
16
|
+
/** External diagnostics (e.g., GPU compilation errors from wgsl-play). */
|
|
17
|
+
getExternalDiagnostics?: () => Diagnostic[];
|
|
18
|
+
/** Get pre-loaded library bundles. */
|
|
19
|
+
getLibs?: () => WeslBundle[];
|
|
20
|
+
/** Fetch libraries on-demand for unresolved external packages. */
|
|
21
|
+
fetchLibs?: (packageNames: string[]) => Promise<WeslBundle[]>;
|
|
22
|
+
/** Package names to ignore when checking unbound externals (e.g. virtual modules). */
|
|
23
|
+
ignorePackages?: () => string[];
|
|
24
|
+
}
|
|
25
|
+
declare const weslLanguage: LRLanguage;
|
|
26
|
+
declare function wesl(): LanguageSupport;
|
|
27
|
+
/** Create a linter that validates WESL using the canonical parser. */
|
|
28
|
+
declare function createWeslLinter(config: WeslLintConfig): _codemirror_state0.Extension;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { WeslLintConfig, createWeslLinter, wesl, weslLanguage };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { LRLanguage, LanguageSupport } from "@codemirror/language";
|
|
2
|
+
import { linter } from "@codemirror/lint";
|
|
3
|
+
import { parser, weslHighlighting } from "lezer-wesl";
|
|
4
|
+
import { BundleResolver, CompositeResolver, RecordResolver, WeslParseError, bindIdents } from "wesl";
|
|
5
|
+
|
|
6
|
+
//#region src/Language.ts
|
|
7
|
+
const weslLanguage = LRLanguage.define({
|
|
8
|
+
name: "wesl",
|
|
9
|
+
parser: parser.configure({ props: [weslHighlighting] }),
|
|
10
|
+
languageData: { commentTokens: {
|
|
11
|
+
line: "//",
|
|
12
|
+
block: {
|
|
13
|
+
open: "/*",
|
|
14
|
+
close: "*/"
|
|
15
|
+
}
|
|
16
|
+
} }
|
|
17
|
+
});
|
|
18
|
+
function wesl() {
|
|
19
|
+
return new LanguageSupport(weslLanguage);
|
|
20
|
+
}
|
|
21
|
+
/** Create a linter that validates WESL using the canonical parser. */
|
|
22
|
+
function createWeslLinter(config) {
|
|
23
|
+
return linter(async () => lintAndFetch(config), { delay: 300 });
|
|
24
|
+
}
|
|
25
|
+
/** Lint once, fetch missing externals if needed, re-lint with new libs. */
|
|
26
|
+
async function lintAndFetch(config) {
|
|
27
|
+
const libs = config.getLibs?.() ?? [];
|
|
28
|
+
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;
|
|
34
|
+
}
|
|
35
|
+
/** Parse, bind, collect diagnostics and discover missing externals. */
|
|
36
|
+
function lintPass(config, libs, ignored) {
|
|
37
|
+
const sources = config.getSources();
|
|
38
|
+
const rootModule = config.rootModule();
|
|
39
|
+
const diagnostics = [];
|
|
40
|
+
let externals = [];
|
|
41
|
+
try {
|
|
42
|
+
const resolver = buildResolver(sources, libs, config.packageName?.());
|
|
43
|
+
const rootAst = resolver.resolveModule(rootModule);
|
|
44
|
+
if (rootAst) {
|
|
45
|
+
const result = runBind(config, resolver, rootAst);
|
|
46
|
+
diagnostics.push(...unboundDiagnostics(result, rootModule, ignored));
|
|
47
|
+
externals = findMissingPackages(rootAst, result, resolver, ignored, libs);
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
const diag = errorToDiagnostic(e);
|
|
51
|
+
if (diag) diagnostics.push(diag);
|
|
52
|
+
}
|
|
53
|
+
diagnostics.push(...config.getExternalDiagnostics?.() ?? []);
|
|
54
|
+
return {
|
|
55
|
+
diagnostics,
|
|
56
|
+
externals
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Build a resolver from sources and optional libs. */
|
|
60
|
+
function buildResolver(sources, libs, packageName) {
|
|
61
|
+
const record = new RecordResolver(sources, { packageName });
|
|
62
|
+
if (libs.length === 0) return record;
|
|
63
|
+
return new CompositeResolver([record, ...libs.map((b) => new BundleResolver(b))]);
|
|
64
|
+
}
|
|
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
|
+
/** Convert unbound refs to diagnostics, skipping ignored packages. */
|
|
75
|
+
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;
|
|
83
|
+
}
|
|
84
|
+
/** Convert a WESL error to a CodeMirror diagnostic. */
|
|
85
|
+
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
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Find external package names not yet loaded, from unresolved imports and unbound refs. */
|
|
97
|
+
function findMissingPackages(rootAst, result, resolver, ignored, libs) {
|
|
98
|
+
const loaded = new Set(libs.map((b) => b.name));
|
|
99
|
+
const pkgs = [];
|
|
100
|
+
for (const imp of rootAst.imports) {
|
|
101
|
+
const root = imp.segments[0]?.name;
|
|
102
|
+
if (!root || !isExternalRoot(root) || ignored.has(root)) continue;
|
|
103
|
+
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)];
|
|
111
|
+
}
|
|
112
|
+
/** @return true if root is an external package name (not package/super). */
|
|
113
|
+
function isExternalRoot(root) {
|
|
114
|
+
return root !== "package" && root !== "super";
|
|
115
|
+
}
|
|
116
|
+
/** Convert an unbound reference to a CodeMirror diagnostic. */
|
|
117
|
+
function unboundToDiagnostic(ref) {
|
|
118
|
+
return {
|
|
119
|
+
from: ref.start,
|
|
120
|
+
to: ref.end,
|
|
121
|
+
severity: "error",
|
|
122
|
+
message: `unresolved identifier '${ref.path.join("::")}'`
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
//#endregion
|
|
127
|
+
export { createWeslLinter, wesl, weslLanguage };
|