wgsl-play 0.0.2 → 0.0.4

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.
@@ -1,5 +1,30 @@
1
+ /**
2
+ * Module Discovery Phase
3
+ *
4
+ * Before linking, we run a discovery phase to find and fetch all needed modules.
5
+ * This uses a lightweight binding pass (findUnboundIdents) that walks the scope tree
6
+ * to find module references without fully resolving them.
7
+ *
8
+ * Module paths (e.g., foo::bar::baz) come from import statements or inline qualified
9
+ * references. We categorize them as:
10
+ * - internal: package:: and super:: paths, fetched from local URLs (shaderRoot)
11
+ * - external: other packages, fetched from npm
12
+ *
13
+ * The discovery loop uses FetchingResolver which provides:
14
+ * - Sync resolveModule() for findUnboundIdents compatibility
15
+ * - Async resolveModuleAsync() as prototype for future async BindIdents
16
+ *
17
+ * When wesl gets async BindIdents, the shim loop disappears and BindIdents
18
+ * will call resolveModuleAsync directly.
19
+ */
20
+
1
21
  import type { WeslBundle } from "wesl";
2
- import { findUnboundIdents, npmNameVariations, RecordResolver } from "wesl";
22
+ import {
23
+ fileToModulePath,
24
+ findUnboundIdents,
25
+ npmNameVariations,
26
+ partition,
27
+ } from "wesl";
3
28
  import type { WeslBundleFile } from "./BundleHydrator.ts";
4
29
  import { bundleRegistry, hydrateBundleRegistry } from "./BundleHydrator.ts";
5
30
  import {
@@ -7,40 +32,119 @@ import {
7
32
  fetchBundleFilesFromUrl,
8
33
  lygiaTgzUrl,
9
34
  } from "./BundleLoader.ts";
35
+ import { getConfig, type WgslPlayConfig } from "./Config.ts";
36
+ import { FetchingResolver } from "./FetchingResolver.ts";
10
37
 
11
- /** Shader source with resolved dependency bundles. */
12
- export interface ShaderWithDeps {
13
- source: string;
14
- bundles: WeslBundle[];
38
+ /** Resolved sources ready for the linker. */
39
+ export interface ResolvedSources {
40
+ /** All sources keyed by module path. */
41
+ weslSrc: Record<string, string>;
42
+ libs: WeslBundle[];
43
+ /** Module name for the root/main module. */
44
+ rootModuleName?: string;
15
45
  }
16
46
 
17
- /** Fetch bundles for external dependencies detected in shader source. */
18
- export async function fetchDependenciesForSource(
19
- source: string,
20
- ): Promise<WeslBundle[]> {
21
- const packageNames = detectPackageDeps(source);
22
- return fetchPackages(packageNames);
47
+ const virtualModules = ["constants", "test"];
48
+
49
+ /** Resolve dependencies: internal modules via HTTP, external packages from npm. */
50
+ export async function fetchDependencies(
51
+ rootModuleSource: string,
52
+ configOverrides?: Partial<WgslPlayConfig>,
53
+ currentPath?: string,
54
+ existingSources?: Record<string, string>,
55
+ ): Promise<ResolvedSources> {
56
+ const config = getConfig(configOverrides);
57
+ const rootModuleName = currentPath
58
+ ? urlToModulePath(currentPath, config.shaderRoot)
59
+ : "package::main";
60
+ const initialSources = {
61
+ ...existingSources,
62
+ [rootModuleName]: rootModuleSource,
63
+ };
64
+ const resolverOpts = {
65
+ shaderRoot: config.shaderRoot,
66
+ srcModulePath: currentPath,
67
+ };
68
+ const resolver = new FetchingResolver(initialSources, resolverOpts);
69
+
70
+ const libs: WeslBundle[] = [];
71
+ const fetched = new Set<string>();
72
+
73
+ // Discovery loop (LATER we'll make BindIdents async, and this can go away)
74
+ while (true) {
75
+ findUnboundIdents(resolver);
76
+
77
+ const unresolved = getNonVirtualUnresolved(resolver, fetched);
78
+ if (unresolved.length === 0) break;
79
+
80
+ const [internal, external] = partition(unresolved, isInternal);
81
+ await Promise.all(internal.map(p => resolver.resolveModuleAsync(p)));
82
+
83
+ const newLibs = await fetchExternalBundles(external, fetched);
84
+ libs.push(...newLibs);
85
+ }
86
+
87
+ const weslSrc = extractWeslSrc(resolver);
88
+ return { libs, weslSrc };
23
89
  }
24
90
 
25
- /** Load shader text from a url and recursively fetch imported bundles */
26
- export async function loadShaderFromUrl(url: string): Promise<ShaderWithDeps> {
27
- const source = await fetchShaderSource(url);
28
- const packageNames = detectPackageDeps(source);
29
- const bundles = await fetchPackages(packageNames);
30
- return { source, bundles };
91
+ /** Load shader from URL, resolving all dependencies. */
92
+ export async function loadShaderFromUrl(
93
+ url: string,
94
+ configOverrides?: Partial<WgslPlayConfig>,
95
+ ): Promise<ResolvedSources> {
96
+ const source = await fetchText(url);
97
+ const currentPath = new URL(url, window.location.href).pathname;
98
+ const config = getConfig(configOverrides);
99
+ const rootModuleName = urlToModulePath(currentPath, config.shaderRoot);
100
+ const { weslSrc, libs } = await fetchDependencies(
101
+ source,
102
+ configOverrides,
103
+ currentPath,
104
+ );
105
+ return { weslSrc, libs, rootModuleName };
31
106
  }
32
107
 
33
- /** Detect external package dependencies from shader source. */
34
- function detectPackageDeps(source: string): string[] {
35
- const resolver = new RecordResolver({ "./main.wesl": source });
36
- const unbound = findUnboundIdents(resolver);
37
- const pkgRefs = unbound.filter(p => p[0] !== "constants" && p[0] !== "test"); // LATER make dynamic
38
- const weslPackages = pkgRefs.map(p => p[0]);
39
- return [...new Set(weslPackages)];
108
+ function getNonVirtualUnresolved(
109
+ resolver: FetchingResolver,
110
+ fetched: Set<string>,
111
+ ): string[] {
112
+ return resolver.getUnresolved().filter(p => {
113
+ const pkg = p.split("::")[0];
114
+ return !virtualModules.includes(pkg) && !fetched.has(pkg);
115
+ });
40
116
  }
41
117
 
42
- /** Fetch WESL bundles for packages, auto-fetching dependencies recursively. */
43
- async function fetchPackages(pkgIds: string[]): Promise<WeslBundle[]> {
118
+ function isInternal(modulePath: string): boolean {
119
+ return modulePath.startsWith("package::") || modulePath.startsWith("super::");
120
+ }
121
+
122
+ /** Extract all sources from resolver (including main). */
123
+ function extractWeslSrc(resolver: FetchingResolver): Record<string, string> {
124
+ const weslSrc: Record<string, string> = {};
125
+ for (const [path] of resolver.allModules()) {
126
+ if (resolver.sources[path]) {
127
+ weslSrc[path] = resolver.sources[path];
128
+ }
129
+ }
130
+ return weslSrc;
131
+ }
132
+
133
+ /** Fetch external packages from npm. */
134
+ async function fetchExternalBundles(
135
+ modulePaths: string[],
136
+ fetched: Set<string>,
137
+ ): Promise<WeslBundle[]> {
138
+ const pkgNames = [...new Set(modulePaths.map(p => p.split("::")[0]))];
139
+ const newPkgs = pkgNames.filter(p => !fetched.has(p));
140
+ if (newPkgs.length === 0) return [];
141
+
142
+ for (const p of newPkgs) fetched.add(p);
143
+ return fetchPackagesFromNpm(newPkgs);
144
+ }
145
+
146
+ /** Fetch WESL bundles from npm, auto-fetching dependencies recursively. */
147
+ async function fetchPackagesFromNpm(pkgIds: string[]): Promise<WeslBundle[]> {
44
148
  const loaded = new Set<string>();
45
149
 
46
150
  const promisedBundles = pkgIds.map(id => fetchOnePackage(id, loaded));
@@ -55,7 +159,7 @@ async function fetchOnePackage(
55
159
  pkgId: string,
56
160
  loaded: Set<string>,
57
161
  ): Promise<WeslBundleFile[]> {
58
- if (loaded.has(pkgId)) return []; // already loaded
162
+ if (loaded.has(pkgId)) return [];
59
163
  loaded.add(pkgId);
60
164
 
61
165
  // Special case for lygia - use custom tgz URL (npm package is outdated)
@@ -73,8 +177,15 @@ async function fetchOnePackage(
73
177
  throw new Error(`Package not found: ${pkgId}`);
74
178
  }
75
179
 
76
- /** Fetch shader source from URL. */
77
- async function fetchShaderSource(url: string): Promise<string> {
180
+ /** Convert URL path to module path (e.g., "/shaders/effects/main.wesl" -> "package::effects::main"). */
181
+ function urlToModulePath(urlPath: string, shaderRoot: string): string {
182
+ const cleanRoot = shaderRoot.replace(/\/$/, "");
183
+ const relativePath = urlPath.replace(cleanRoot, "").replace(/^\//, "");
184
+ return fileToModulePath(relativePath, "package", false);
185
+ }
186
+
187
+ /** Fetch text content from URL. */
188
+ async function fetchText(url: string): Promise<string> {
78
189
  const response = await fetch(url);
79
190
  if (!response.ok) {
80
191
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
package/src/Renderer.ts CHANGED
@@ -1,9 +1,15 @@
1
- import type { LinkParams, WeslBundle } from "wesl";
2
- import { requestWeslDevice } from "wesl";
1
+ import type { ModuleResolver } from "wesl";
2
+ import {
3
+ BundleResolver,
4
+ CompositeResolver,
5
+ RecordResolver,
6
+ requestWeslDevice,
7
+ } from "wesl";
3
8
  import {
4
9
  linkAndCreatePipeline,
5
10
  renderFrame,
6
11
  updateRenderUniforms,
12
+ type WeslOptions,
7
13
  } from "wesl-gpu";
8
14
 
9
15
  /** WebGPU state */
@@ -16,6 +22,7 @@ export interface RenderState {
16
22
  pipelineLayout: GPUPipelineLayout;
17
23
  bindGroup: GPUBindGroup;
18
24
  pipeline?: GPURenderPipeline;
25
+ frameCount: number;
19
26
  }
20
27
 
21
28
  /** Animation state */
@@ -25,6 +32,9 @@ export interface PlaybackState {
25
32
  pausedDuration: number;
26
33
  }
27
34
 
35
+ /** Options for linking shaders - re-exports wesl-gpu's WeslOptions */
36
+ export type LinkOptions = WeslOptions;
37
+
28
38
  /** Initialize WebGPU for a canvas element. */
29
39
  export async function initWebGPU(
30
40
  canvas: HTMLCanvasElement,
@@ -68,30 +78,39 @@ export async function initWebGPU(
68
78
  uniformBuffer,
69
79
  pipelineLayout,
70
80
  bindGroup,
81
+ frameCount: 0,
71
82
  };
72
83
  }
73
84
 
74
- export type LinkOptions = Pick<
75
- LinkParams,
76
- "packageName" | "conditions" | "constants"
77
- >;
78
-
79
85
  /** Compile WESL fragment shader and create render pipeline. */
80
86
  export async function createPipeline(
81
87
  state: RenderState,
82
88
  fragmentSource: string,
83
- bundles: WeslBundle[],
84
89
  options?: LinkOptions,
85
90
  ): Promise<void> {
91
+ const { weslSrc, libs = [], conditions, constants } = options ?? {};
92
+ const { packageName, rootModuleName } = options ?? {};
93
+
94
+ // Build resolver from weslSrc/libs if provided
95
+ let resolver: ModuleResolver | undefined;
96
+ if (weslSrc || libs.length > 0) {
97
+ const resolvers: ModuleResolver[] = [];
98
+ if (weslSrc) resolvers.push(new RecordResolver(weslSrc));
99
+ for (const bundle of libs) resolvers.push(new BundleResolver(bundle));
100
+ resolver =
101
+ resolvers.length === 1 ? resolvers[0] : new CompositeResolver(resolvers);
102
+ }
103
+
86
104
  state.pipeline = await linkAndCreatePipeline({
87
105
  device: state.device,
88
106
  fragmentSource,
89
- bundles,
107
+ resolver,
90
108
  format: state.presentationFormat,
91
109
  layout: state.pipelineLayout,
92
- conditions: options?.conditions,
93
- constants: options?.constants,
94
- packageName: options?.packageName,
110
+ conditions,
111
+ constants,
112
+ packageName,
113
+ rootModuleName,
95
114
  });
96
115
  }
97
116
 
@@ -128,11 +147,11 @@ export function startRenderLoop(
128
147
  bindGroup: state.bindGroup,
129
148
  targetView: state.context.getCurrentTexture().createView(),
130
149
  });
150
+ state.frameCount++;
131
151
  animationId = requestAnimationFrame(render);
132
152
  }
133
153
 
134
154
  animationId = requestAnimationFrame(render);
135
-
136
155
  return () => cancelAnimationFrame(animationId);
137
156
  }
138
157
 
package/src/WgslPlay.css CHANGED
@@ -4,6 +4,8 @@
4
4
  }
5
5
 
6
6
  canvas {
7
+ position: absolute;
8
+ inset: 0;
7
9
  width: 100%;
8
10
  height: 100%;
9
11
  display: block;
package/src/WgslPlay.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import type { LinkParams, WeslBundle } from "wesl";
2
+ import { fileToModulePath } from "wesl";
3
+ import type { WgslPlayConfig } from "./Config.ts";
2
4
  import { ErrorOverlay } from "./ErrorOverlay.ts";
3
- import {
4
- fetchDependenciesForSource,
5
- loadShaderFromUrl,
6
- } from "./PackageLoader.ts";
5
+ import { fetchDependencies, loadShaderFromUrl } from "./PackageLoader.ts";
7
6
  import {
8
7
  createPipeline,
9
8
  initWebGPU,
@@ -14,6 +13,8 @@ import {
14
13
  } from "./Renderer.ts";
15
14
  import cssText from "./WgslPlay.css?inline";
16
15
 
16
+ export { defaults, getConfig, resetConfig } from "./Config.ts";
17
+
17
18
  /** Project configuration for multi-file shaders (subset of wesl link() API). */
18
19
  export type WeslProject = Pick<
19
20
  LinkParams,
@@ -49,7 +50,7 @@ let template: HTMLTemplateElement | null = null;
49
50
  * </wgsl-play>
50
51
  */
51
52
  export class WgslPlay extends HTMLElement {
52
- static observedAttributes = ["src"];
53
+ static observedAttributes = ["src", "shader-root"];
53
54
 
54
55
  private canvas: HTMLCanvasElement;
55
56
  private errorOverlay: ErrorOverlay;
@@ -63,10 +64,20 @@ export class WgslPlay extends HTMLElement {
63
64
  pausedDuration: 0,
64
65
  };
65
66
 
66
- private _source = "";
67
+ private _weslSrc: Record<string, string> = {};
68
+ private _rootModuleName = "package::main";
69
+ private _libs?: WeslBundle[];
67
70
  private _linkOptions: LinkOptions = {};
71
+ private _fromFullProject = false;
68
72
  private _initialized = false;
69
73
 
74
+ /** Get config overrides from element attributes. */
75
+ private getConfigOverrides(): Partial<WgslPlayConfig> | undefined {
76
+ const shaderRoot = this.getAttribute("shader-root");
77
+ if (!shaderRoot) return undefined;
78
+ return { shaderRoot };
79
+ }
80
+
70
81
  constructor() {
71
82
  super();
72
83
  const shadow = this.attachShadow({ mode: "open" });
@@ -110,33 +121,57 @@ export class WgslPlay extends HTMLElement {
110
121
  }
111
122
  }
112
123
 
113
- /** Current shader source code. */
124
+ /** Current shader source code (main module). */
114
125
  get source(): string {
115
- return this._source;
126
+ return this._weslSrc[this._rootModuleName] ?? "";
116
127
  }
117
128
 
118
129
  /** Set shader source directly. */
119
130
  set source(value: string) {
120
- this._source = value;
121
- this.compileSource(value);
131
+ this._weslSrc = { [this._rootModuleName]: value };
132
+ this._libs = undefined;
133
+ this._fromFullProject = false;
134
+ this.discoverAndRebuild();
122
135
  }
123
136
 
124
137
  /** Set project configuration (mirrors wesl link() API). */
125
138
  set project(value: WeslProject) {
126
139
  const { weslSrc, rootModuleName, libs } = value;
127
140
  const { packageName, conditions, constants } = value;
128
- if (!weslSrc || !rootModuleName) return;
129
- const mainSource = weslSrc[rootModuleName];
130
- if (!mainSource) return;
131
141
 
132
- this._source = mainSource;
133
- this._linkOptions = { packageName, conditions, constants };
142
+ // Update link options if provided
143
+ if (packageName || conditions || constants) {
144
+ this._linkOptions = { packageName, conditions, constants };
145
+ }
146
+ if (libs) this._libs = libs;
134
147
 
135
- if (libs?.length) {
136
- this.compileWithLibs(mainSource, libs);
137
- } else {
138
- this.compileSource(mainSource);
148
+ if (weslSrc) {
149
+ this.setProjectSources(weslSrc, rootModuleName);
150
+ return;
139
151
  }
152
+
153
+ // Partial update - may need to refetch if conditions changed
154
+ if (Object.keys(this._weslSrc).length === 0) return;
155
+ if (this._fromFullProject) this.rebuildPipeline();
156
+ else this.discoverAndRebuild();
157
+ }
158
+
159
+ /** Set sources from a full project with weslSrc. */
160
+ private setProjectSources(
161
+ weslSrc: Record<string, string>,
162
+ rootModuleName?: string,
163
+ ): void {
164
+ // Convert file paths to module paths if needed (for ?link imports)
165
+ const entries = Object.entries(weslSrc).map(([k, v]) => [
166
+ toModulePath(k),
167
+ v,
168
+ ]);
169
+ this._weslSrc = Object.fromEntries(entries);
170
+ this._rootModuleName = rootModuleName
171
+ ? toModulePath(rootModuleName)
172
+ : "package::main";
173
+ this._fromFullProject = true;
174
+ this.rebuildPipeline();
140
175
  }
141
176
 
142
177
  /** Whether the shader is currently playing. */
@@ -146,10 +181,14 @@ export class WgslPlay extends HTMLElement {
146
181
 
147
182
  /** Current animation time in seconds. */
148
183
  get time(): number {
149
- const currentTime = this.playback.isPlaying
150
- ? performance.now()
151
- : this.playback.startTime + this.playback.pausedDuration;
152
- return (currentTime - this.playback.startTime) / 1000;
184
+ const { isPlaying, startTime, pausedDuration } = this.playback;
185
+ const now = isPlaying ? performance.now() : startTime + pausedDuration;
186
+ return (now - startTime) / 1000;
187
+ }
188
+
189
+ /** Number of frames rendered (for testing/debugging). */
190
+ get frameCount(): number {
191
+ return this.renderState?.frameCount ?? 0;
153
192
  }
154
193
 
155
194
  /** Whether there's a compilation error. */
@@ -164,10 +203,9 @@ export class WgslPlay extends HTMLElement {
164
203
 
165
204
  /** Start playback. */
166
205
  play(): void {
167
- if (this.playback.isPlaying) return;
168
- const pauseTime = this.playback.startTime + this.playback.pausedDuration;
169
- this.playback.startTime =
170
- performance.now() - (pauseTime - this.playback.startTime);
206
+ const { isPlaying, pausedDuration } = this.playback;
207
+ if (isPlaying) return;
208
+ this.playback.startTime = performance.now() - pausedDuration;
171
209
  this.setPlaying(true);
172
210
  }
173
211
 
@@ -194,23 +232,24 @@ export class WgslPlay extends HTMLElement {
194
232
 
195
233
  /** Display error message in overlay. Pass empty string to clear. */
196
234
  showError(message: string): void {
197
- if (message) {
198
- this.errorOverlay.show(message);
199
- this.pause();
200
- } else {
235
+ if (!message) {
201
236
  this.errorOverlay.hide();
237
+ return;
202
238
  }
239
+ this.errorOverlay.show(message);
240
+ this.pause();
203
241
  }
204
242
 
205
- /** Set up WebGPU and load initial shader from src attribute or inline content. */
206
- private async initialize(): Promise<void> {
207
- if (this._initialized) return;
243
+ /** Set up WebGPU and load initial shader. Returns true if successful. */
244
+ private async initialize(): Promise<boolean> {
245
+ if (this._initialized) return !!this.renderState;
208
246
  this._initialized = true;
209
247
 
210
248
  try {
211
249
  this.renderState = await initWebGPU(this.canvas);
212
250
  await this.loadInitialContent();
213
251
  this.stopRenderLoop = startRenderLoop(this.renderState, this.playback);
252
+ return true;
214
253
  } catch (error) {
215
254
  const message = `WebGPU initialization failed: ${error}`;
216
255
  this.errorOverlay.show(message);
@@ -218,18 +257,26 @@ export class WgslPlay extends HTMLElement {
218
257
  this.dispatchEvent(
219
258
  new CustomEvent("init-error", { detail: { message } }),
220
259
  );
260
+ return false;
221
261
  }
222
262
  }
223
263
 
224
- /** Load from src attribute or inline textContent. */
264
+ /** Load from src attribute, script child, or inline textContent. */
225
265
  private async loadInitialContent(): Promise<void> {
226
266
  const src = this.getAttribute("src");
227
- if (src) {
228
- await this.loadFromUrl(src);
229
- } else {
230
- const inlineSource = this.textContent?.trim();
231
- if (inlineSource) await this.compileSource(inlineSource);
232
- }
267
+ if (src) return this.loadFromUrl(src);
268
+
269
+ // Prefer <script type="text/wgsl"> or <script type="text/wesl"> (no HTML escaping needed)
270
+ const script = this.querySelector(
271
+ 'script[type="text/wgsl"], script[type="text/wesl"]',
272
+ );
273
+ const inlineSource =
274
+ script?.textContent?.trim() ?? this.textContent?.trim();
275
+ if (!inlineSource) return;
276
+
277
+ this._weslSrc = { [this._rootModuleName]: inlineSource };
278
+ this._fromFullProject = false;
279
+ await this.discoverAndRebuild();
233
280
  }
234
281
 
235
282
  /** Fetch shader from URL, auto-fetching any imported dependencies. */
@@ -238,45 +285,73 @@ export class WgslPlay extends HTMLElement {
238
285
 
239
286
  try {
240
287
  this.errorOverlay.hide();
241
- const { source, bundles } = await loadShaderFromUrl(url);
242
- this._source = source;
243
- await createPipeline(this.renderState, source, bundles);
288
+ const { weslSrc, libs, rootModuleName } = await loadShaderFromUrl(
289
+ url,
290
+ this.getConfigOverrides(),
291
+ );
292
+ this._weslSrc = weslSrc;
293
+ this._libs = libs;
294
+ this._fromFullProject = false;
295
+ if (rootModuleName) this._rootModuleName = rootModuleName;
296
+
297
+ const mainSource = weslSrc[this._rootModuleName];
298
+ if (!mainSource) return;
299
+
300
+ await createPipeline(this.renderState, mainSource, {
301
+ ...this._linkOptions,
302
+ weslSrc,
303
+ libs,
304
+ rootModuleName: this._rootModuleName,
305
+ });
244
306
  } catch (error) {
245
307
  this.handleCompileError(error);
246
308
  }
247
309
  }
248
310
 
249
- /** Compile source string, auto-fetching any imported dependencies. */
250
- private async compileSource(source: string): Promise<void> {
251
- if (!this.renderState) return;
311
+ /** Rebuild GPU pipeline using stored state. For full projects with all sources. */
312
+ private async rebuildPipeline(): Promise<void> {
313
+ if (!(await this.initialize())) return;
314
+
315
+ const mainSource = this._weslSrc[this._rootModuleName];
316
+ if (!mainSource) return;
252
317
 
253
318
  try {
254
319
  this.errorOverlay.hide();
255
- const bundles = await fetchDependenciesForSource(source);
256
- await createPipeline(
257
- this.renderState,
258
- source,
259
- bundles,
260
- this._linkOptions,
261
- );
320
+ await createPipeline(this.renderState!, mainSource, {
321
+ ...this._linkOptions,
322
+ weslSrc: this._weslSrc,
323
+ libs: this._libs,
324
+ rootModuleName: this._rootModuleName,
325
+ });
262
326
  } catch (error) {
263
327
  this.handleCompileError(error);
264
328
  }
265
329
  }
266
330
 
267
- /** Compile with pre-loaded library bundles (no network fetch for libs). */
268
- private async compileWithLibs(
269
- source: string,
270
- libs: WeslBundle[],
271
- ): Promise<void> {
272
- if (!this.renderState) {
273
- await this.initialize();
274
- }
275
- if (!this.renderState) return;
331
+ /** Discover dependencies and rebuild. For HTTP/inline sources that may need fetching. */
332
+ private async discoverAndRebuild(): Promise<void> {
333
+ if (!(await this.initialize())) return;
334
+
335
+ const mainSource = this._weslSrc[this._rootModuleName];
336
+ if (!mainSource) return;
276
337
 
277
338
  try {
278
339
  this.errorOverlay.hide();
279
- await createPipeline(this.renderState, source, libs, this._linkOptions);
340
+ const { weslSrc, libs } = await fetchDependencies(
341
+ mainSource,
342
+ this.getConfigOverrides(),
343
+ undefined,
344
+ this._weslSrc,
345
+ );
346
+ this._weslSrc = { ...this._weslSrc, ...weslSrc };
347
+ this._libs = [...(this._libs ?? []), ...libs];
348
+
349
+ await createPipeline(this.renderState!, mainSource, {
350
+ ...this._linkOptions,
351
+ weslSrc: this._weslSrc,
352
+ libs: this._libs,
353
+ rootModuleName: this._rootModuleName,
354
+ });
280
355
  } catch (error) {
281
356
  this.handleCompileError(error);
282
357
  }
@@ -306,3 +381,8 @@ function getStyles(): CSSStyleSheet {
306
381
  }
307
382
  return styles;
308
383
  }
384
+
385
+ /** Convert file path to module path (e.g., "effects/main.wesl" -> "package::effects::main"). */
386
+ function toModulePath(filePath: string): string {
387
+ return fileToModulePath(filePath, "package", false);
388
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from "./BundleHydrator.ts";
2
2
  export * from "./BundleLoader.ts";
3
+ export * from "./Config.ts";
4
+ export * from "./HttpPackageLoader.ts";
3
5
  export * from "./PackageLoader.ts";
4
6
  export * from "./WgslPlay.ts";
5
7
 
@@ -9,7 +9,7 @@ import { loadBundlesFromFiles } from "../BundleHydrator.ts";
9
9
 
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = dirname(__filename);
12
- const testPkgDir = join(__dirname, "../../../../test_pkg");
12
+ const testPkgDir = join(__dirname, "../../../test_pkg");
13
13
 
14
14
  function loadBundlesFromTgz(tgzPath: string, packageName: string) {
15
15
  const gzipData = readFileSync(tgzPath);