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.
@@ -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
+ ℹ tsdown v0.19.0-beta.3 powered by rolldown v1.0.0-beta.58
7
+ ℹ config file: /Users/lee/wesl/wesl-js/tools/packages/wgsl-edit/tsdown.config.ts
8
+ ℹ entry: src/index.ts, src/Language.ts, src/WgslEdit.ts
9
+ ℹ tsconfig: tsconfig.json
10
+ ℹ Build start
11
+ ℹ dist/Language.mjs  4.39 kB │ gzip: 1.58 kB
12
+ ℹ dist/index.mjs  0.27 kB │ gzip: 0.18 kB
13
+ ℹ dist/WgslEdit.mjs  0.10 kB │ gzip: 0.09 kB
14
+ ℹ dist/WgslEdit-D62UKrqG.mjs 22.76 kB │ gzip: 6.47 kB
15
+ ℹ dist/WgslEdit.d.mts  4.05 kB │ gzip: 1.49 kB
16
+ ℹ dist/Language.d.mts  1.48 kB │ gzip: 0.68 kB
17
+ ℹ dist/index.d.mts  0.14 kB │ gzip: 0.09 kB
18
+ ℹ 7 files, total: 33.20 kB
19
+ ✔ Build complete in 1881ms
@@ -0,0 +1,5 @@
1
+
2
+ 
3
+ > wgsl-edit@0.0.1 typecheck /Users/lee/wesl/wesl-js/tools/packages/wgsl-edit
4
+ > tsgo
5
+
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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node --experimental-strip-types
2
+ import "../src/Cli.ts";
@@ -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">&lt;script type="module" src="https://esm.sh/wgsl-edit"&gt;&lt;/script&gt;
53
+ &lt;script type="module" src="https://esm.sh/wgsl-play"&gt;&lt;/script&gt;
54
+
55
+ &lt;wgsl-edit id="editor" lint-from="player"&gt;&lt;/wgsl-edit&gt;
56
+ &lt;wgsl-play id="player" source="editor"&gt;&lt;/wgsl-play&gt;</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 };