wesl-plugin 0.6.70 → 0.6.71

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
@@ -96,10 +96,10 @@ export default {
96
96
 
97
97
  ### Conditions in Import Path
98
98
 
99
- For `?static`, you can specify conditions directly in the import:
99
+ For `?static`, you can specify conditions as query parameters:
100
100
 
101
101
  ```ts
102
- import wgsl from "./app.wesl MOBILE=true DEBUG=false ?static";
102
+ import wgsl from "./app.wesl?MOBILE=true&DEBUG=false&static";
103
103
  ```
104
104
 
105
105
  ## Configuration (wesl.toml)
@@ -124,7 +124,7 @@ import type { PluginExtension } from "wesl-plugin";
124
124
 
125
125
  const myExtension: PluginExtension = {
126
126
  extensionName: "myfeature", // enables ?myfeature imports
127
- emitFn: async (shaderPath, api, conditions) => {
127
+ emitFn: async (shaderPath, api, conditions, options) => {
128
128
  const sources = await api.weslSrc();
129
129
  // Return JavaScript code as a string
130
130
  return `export default ${JSON.stringify(sources)};`;
@@ -31,14 +31,16 @@ interface WeslTomlInfo {
31
31
  }
32
32
  //#endregion
33
33
  //#region src/PluginExtension.d.ts
34
- /** function type required for for emit extensions */
34
+ /** function type required for emit extensions */
35
35
  type ExtensionEmitFn = (/** absolute path to the shader to which the extension is attached */
36
36
 
37
37
  shaderPath: string, /** support functions available to plugin extensions */
38
38
 
39
39
  pluginApi: PluginExtensionApi, /** static conditions specified on the js import */
40
40
 
41
- conditions?: Record<string, boolean>) => Promise<string>;
41
+ conditions?: Record<string, boolean>, /** plugin-level options from query params (e.g., { include: "all" }) */
42
+
43
+ options?: Record<string, string>) => Promise<string>;
42
44
  /** an extension that runs inside the wesl-js build plugin */
43
45
  interface PluginExtension extends WeslJsPlugin {
44
46
  /** javascript imports with this suffix will trigger the plugin */
@@ -47,6 +49,10 @@ interface PluginExtension extends WeslJsPlugin {
47
49
  * e.g. import myPluginJs from "./foo.wesl?myPlugin"; */
48
50
  emitFn: ExtensionEmitFn;
49
51
  }
52
+ interface ProjectSources {
53
+ weslSrc: Record<string, string>;
54
+ dependencies: string[];
55
+ }
50
56
  /** api supplied to plugin extensions */
51
57
  interface PluginExtensionApi {
52
58
  weslToml: () => Promise<WeslTomlInfo>;
@@ -54,6 +60,12 @@ interface PluginExtensionApi {
54
60
  weslRegistry: () => Promise<BatchModuleResolver>;
55
61
  weslMain: (baseId: string) => Promise<string>;
56
62
  weslDependencies: () => Promise<string[]>;
63
+ /** weslRoot relative to tomlDir, with forward slashes. */
64
+ debugWeslRoot: () => Promise<string>;
65
+ /** Get weslSrc scoped to modules reachable from a root, plus their deps. */
66
+ scopedProject: (rootModuleName: string) => Promise<ProjectSources>;
67
+ /** Fetch project sources, either all or scoped to reachable modules. */
68
+ fetchProject: (rootModuleName: string, options?: Record<string, string>) => Promise<ProjectSources>;
57
69
  }
58
70
  //#endregion
59
- export { PluginExtension as n, PluginExtensionApi as r, ExtensionEmitFn as t };
71
+ export { ProjectSources as i, PluginExtension as n, PluginExtensionApi as r, ExtensionEmitFn as t };
@@ -14,12 +14,9 @@ const linkBuildExtension = {
14
14
  emitFn: emitLinkJs
15
15
  };
16
16
  /** Emit a JavaScript LinkParams structure, ready for linking at runtime. */
17
- async function emitLinkJs(baseId, api) {
18
- const { resolvedRoot, tomlDir } = await api.weslToml();
19
- const weslSrc = await api.weslSrc();
17
+ async function emitLinkJs(baseId, api, _conditions, options) {
20
18
  const rootModuleName = noSuffix(await api.weslMain(baseId));
21
- const debugWeslRoot = path.relative(tomlDir, resolvedRoot).replaceAll(path.sep, "/");
22
- const autoDeps = await api.weslDependencies();
19
+ const [{ weslSrc, dependencies: autoDeps }, debugWeslRoot] = await Promise.all([api.fetchProject(rootModuleName, options), api.debugWeslRoot()]);
23
20
  const sanitizedDeps = autoDeps.map((dep) => dep.replaceAll("/", "_"));
24
21
  const bundleImports = autoDeps.map((p, i) => `import ${sanitizedDeps[i]} from "${p}";`).join("\n");
25
22
  const paramsName = `link${path.basename(rootModuleName).replace(/\W/g, "_")}Config`;
@@ -1301,34 +1298,28 @@ function resolve(specifier, parent) {
1301
1298
 
1302
1299
  //#endregion
1303
1300
  //#region src/extensions/StaticExtension.ts
1304
- /**
1305
- * a wesl-js ?static build extension that statically links from the root file
1306
- * and emits a JavaScript file containing the linked wgsl string.
1307
- *
1308
- * use it like this:
1309
- * import wgsl from "./shaders/app.wesl?static";
1310
- *
1311
- * or with conditions, like this:
1312
- * import wgsl from "../shaders/foo/app.wesl MOBILE=true FUN SAFE=false ?static";
1313
- */
1301
+ /** Build extension for ?static imports: links WESL at build time, emits WGSL string. */
1314
1302
  const staticBuildExtension = {
1315
1303
  extensionName: "static",
1316
1304
  emitFn: emitStaticJs
1317
1305
  };
1318
- /** Emit a JavaScript file containing the wgsl string */
1319
- async function emitStaticJs(baseId, api, conditions) {
1306
+ /** Emit a JS module exporting the statically linked WGSL string. */
1307
+ async function emitStaticJs(baseId, api, conditions, _options) {
1320
1308
  const { resolvedRoot, tomlDir } = await api.weslToml();
1321
1309
  const parentModule = url.pathToFileURL(path.join(tomlDir, "wesl.toml")).toString();
1322
- const futureLibs = (await api.weslDependencies()).map((d) => resolve(d, parentModule)).map((f) => import(f));
1323
- const libs = (await Promise.all(futureLibs)).map((m) => m.default);
1324
- return `
1325
- export const wgsl = \`${(await link({
1326
- weslSrc: await api.weslSrc(),
1327
- rootModuleName: noSuffix(await api.weslMain(baseId)),
1310
+ const rootModuleName = noSuffix(await api.weslMain(baseId));
1311
+ const [weslSrc, dependencies] = await Promise.all([api.weslSrc(), api.weslDependencies()]);
1312
+ const libFileUrls = dependencies.map((d) => resolve(d, parentModule));
1313
+ const libs = (await Promise.all(libFileUrls.map((f) => import(f)))).map((m) => m.default);
1314
+ const { dest: wgsl } = await link({
1315
+ weslSrc,
1316
+ rootModuleName,
1328
1317
  debugWeslRoot: path.relative(tomlDir, resolvedRoot).replaceAll(path.sep, "/"),
1329
1318
  libs,
1330
1319
  conditions
1331
- })).dest}\`;
1320
+ });
1321
+ return `
1322
+ export const wgsl = \`${wgsl}\`;
1332
1323
  export default wgsl;
1333
1324
  `;
1334
1325
  }
@@ -1,10 +1,10 @@
1
- import { n as resolve, r as linkBuildExtension, t as staticBuildExtension } from "./StaticExtension-Chl5KPpw.mjs";
1
+ import { n as resolve, r as linkBuildExtension, t as staticBuildExtension } from "./StaticExtension-Q8HMuFJa.mjs";
2
2
  import path, { posix, win32 } from "node:path";
3
- import { RecordResolver, WeslParseError, filterMap, findUnboundIdents, npmNameVariations } from "wesl";
3
+ import { RecordResolver, WeslParseError, discoverModules, fileToModulePath, filterMap, findUnboundIdents, npmNameVariations } from "wesl";
4
+ import fs, { lstat, readdir, readlink, realpath } from "node:fs/promises";
4
5
  import { fileURLToPath, pathToFileURL } from "node:url";
5
6
  import * as xi from "node:fs";
6
7
  import { createUnplugin } from "unplugin";
7
- import fs$1, { lstat, readdir, readlink, realpath } from "node:fs/promises";
8
8
  import { lstatSync, readdir as readdir$1, readdirSync, readlinkSync, realpathSync as realpathSync$1 } from "fs";
9
9
  import { EventEmitter } from "node:events";
10
10
  import Pe from "node:stream";
@@ -6947,7 +6947,7 @@ const defaultWeslToml = {
6947
6947
  * Provide default values for any required WeslToml fields.
6948
6948
  */
6949
6949
  async function loadWeslToml(tomlFile) {
6950
- const tomlString = await fs$1.readFile(tomlFile, "utf-8");
6950
+ const tomlString = await fs.readFile(tomlFile, "utf-8");
6951
6951
  const parsed = import_toml.default.parse(tomlString);
6952
6952
  return {
6953
6953
  ...defaultWeslToml,
@@ -6964,11 +6964,11 @@ async function loadWeslToml(tomlFile) {
6964
6964
  async function findWeslToml(projectDir, specifiedToml) {
6965
6965
  let tomlFile;
6966
6966
  if (specifiedToml) {
6967
- await fs$1.access(specifiedToml);
6967
+ await fs.access(specifiedToml);
6968
6968
  tomlFile = specifiedToml;
6969
6969
  } else {
6970
6970
  const tomlPath = path.join(projectDir, "wesl.toml");
6971
- tomlFile = await fs$1.access(tomlPath).then(() => tomlPath).catch(() => {});
6971
+ tomlFile = await fs.access(tomlPath).then(() => tomlPath).catch(() => {});
6972
6972
  }
6973
6973
  let parsedToml;
6974
6974
  let tomlDir;
@@ -7047,7 +7047,7 @@ function* exportSubpaths(mPath) {
7047
7047
  *
7048
7048
  * @param weslSrc - Record of WESL source files by path
7049
7049
  * @param projectDir - Project directory for resolving package imports
7050
- * @param virtualLibNames - Virtual lib names to exclude (e.g., ['test', 'constants'])
7050
+ * @param virtualLibNames - Virtual lib names to exclude (e.g., ['env', 'constants'])
7051
7051
  * @returns Dependency paths in npm format (e.g., 'foo/bar', 'foo')
7052
7052
  */
7053
7053
  function parseDependencies(weslSrc, projectDir, virtualLibNames = []) {
@@ -7063,6 +7063,10 @@ function parseDependencies(weslSrc, projectDir, virtualLibNames = []) {
7063
7063
  }
7064
7064
  const unbound = findUnboundIdents(resolver);
7065
7065
  if (!unbound) return [];
7066
+ return resolvePkgDeps(unbound, projectDir, virtualLibNames);
7067
+ }
7068
+ /** Resolve pre-computed unbound refs to npm dependency paths. */
7069
+ function resolvePkgDeps(unbound, projectDir, virtualLibNames = []) {
7066
7070
  const excludeRoots = new Set(["constants", ...virtualLibNames]);
7067
7071
  const pkgRefs = unbound.filter((modulePath) => modulePath.length > 1 && !excludeRoots.has(modulePath[0]));
7068
7072
  if (pkgRefs.length === 0) return [];
@@ -7079,29 +7083,51 @@ function projectDirURL(projectDir) {
7079
7083
 
7080
7084
  //#endregion
7081
7085
  //#region src/PluginApi.ts
7086
+ /** Construct the API surface available to plugin extensions. */
7082
7087
  function buildApi(context, unpluginCtx) {
7083
- return {
7088
+ const api = {
7084
7089
  weslToml: async () => getWeslToml(context, unpluginCtx),
7085
7090
  weslSrc: async () => loadWesl(context, unpluginCtx),
7086
7091
  weslRegistry: async () => getRegistry(context, unpluginCtx),
7087
7092
  weslMain: makeGetWeslMain(context, unpluginCtx),
7088
- weslDependencies: async () => findDependencies(context, unpluginCtx)
7093
+ weslDependencies: async () => findDependencies(context, unpluginCtx),
7094
+ debugWeslRoot: async () => getDebugWeslRoot(context, unpluginCtx),
7095
+ scopedProject: (rootModuleName) => getScopedProject(rootModuleName, context, unpluginCtx),
7096
+ fetchProject: (rootModuleName, options) => fetchProject(api, rootModuleName, options)
7097
+ };
7098
+ return api;
7099
+ }
7100
+ /** Get weslSrc scoped to modules reachable from root, plus their deps. */
7101
+ async function getScopedProject(rootModuleName, context, unpluginCtx) {
7102
+ const fullSrc = await loadWesl(context, unpluginCtx);
7103
+ const { toml, tomlDir: projectDir } = await getWeslToml(context, unpluginCtx);
7104
+ const { weslSrc, unbound } = discoverModules(fullSrc, new RecordResolver(fullSrc), fileToModulePath(rootModuleName, "package", false));
7105
+ return {
7106
+ weslSrc,
7107
+ dependencies: resolveDepsFromUnbound(toml.dependencies, unbound, projectDir)
7089
7108
  };
7090
7109
  }
7091
- /** load the wesl.toml */
7110
+ /** Resolve dependencies using pre-computed unbound refs (avoids re-parsing). */
7111
+ function resolveDepsFromUnbound(dependencies, unbound, projectDir) {
7112
+ const depsArray = Array.isArray(dependencies) ? dependencies : [dependencies ?? "auto"];
7113
+ if (!depsArray.includes("auto")) return depsArray;
7114
+ const base = depsArray.filter((dep) => dep !== "auto");
7115
+ const discovered = resolvePkgDeps(unbound, projectDir);
7116
+ return [...new Set([...base, ...discovered])];
7117
+ }
7118
+ /** Load and cache the wesl.toml configuration. */
7092
7119
  async function getWeslToml(context, unpluginCtx) {
7093
7120
  const { cache } = context;
7094
7121
  if (cache.weslToml) return cache.weslToml;
7095
- const specifiedToml = context.options.weslToml;
7096
- const tomlInfo = await findWeslToml(process.cwd(), specifiedToml);
7122
+ const tomlInfo = await findWeslToml(process.cwd(), context.options.weslToml);
7097
7123
  if (tomlInfo.tomlFile) {
7098
7124
  unpluginCtx.addWatchFile(tomlInfo.tomlFile);
7099
7125
  context.weslToml = tomlInfo.tomlFile;
7100
7126
  }
7101
7127
  cache.weslToml = tomlInfo;
7102
- return cache.weslToml;
7128
+ return tomlInfo;
7103
7129
  }
7104
- /** load and parse all the wesl files into a ParsedRegistry */
7130
+ /** Load all wesl files and return a cached RecordResolver. */
7105
7131
  async function getRegistry(context, unpluginCtx) {
7106
7132
  const { cache } = context;
7107
7133
  let { registry } = cache;
@@ -7109,26 +7135,42 @@ async function getRegistry(context, unpluginCtx) {
7109
7135
  const loaded = await loadWesl(context, unpluginCtx);
7110
7136
  const { resolvedRoot } = await getWeslToml(context, unpluginCtx);
7111
7137
  registry = new RecordResolver(loaded);
7112
- Object.keys(loaded).map((p) => path.resolve(resolvedRoot, p)).forEach((f) => {
7113
- unpluginCtx.addWatchFile(f);
7114
- });
7138
+ const fullPaths = Object.keys(loaded).map((p) => path.resolve(resolvedRoot, p));
7139
+ for (const f of fullPaths) unpluginCtx.addWatchFile(f);
7115
7140
  cache.registry = registry;
7116
7141
  return registry;
7117
7142
  }
7118
- /** if the dependency list includes "auto", fill in the missing dependencies
7119
- * by parsing the source files to find references to packages
7120
- * @return the list of dependencies with "auto" replaced by the found dependencies
7121
- */
7143
+ /** Compute weslRoot relative to tomlDir, with forward slashes. */
7144
+ async function getDebugWeslRoot(context, unpluginCtx) {
7145
+ const { resolvedRoot, tomlDir } = await getWeslToml(context, unpluginCtx);
7146
+ return toUnixPath(path.relative(tomlDir, resolvedRoot));
7147
+ }
7148
+ /** Fetch project sources, either all or scoped to reachable modules. */
7149
+ async function fetchProject(api, rootModuleName, options) {
7150
+ if (options?.include === "all") {
7151
+ const [weslSrc, dependencies] = await Promise.all([api.weslSrc(), api.weslDependencies()]);
7152
+ return {
7153
+ weslSrc,
7154
+ dependencies
7155
+ };
7156
+ }
7157
+ return api.scopedProject(rootModuleName);
7158
+ }
7159
+ /** Find dependencies, resolving "auto" entries by parsing source files. */
7122
7160
  async function findDependencies(context, unpluginCtx) {
7123
7161
  const { toml, tomlDir: projectDir } = await getWeslToml(context, unpluginCtx);
7124
7162
  const weslSrc = await loadWesl(context, unpluginCtx);
7125
- const { dependencies = "auto" } = toml;
7126
- const depsArray = Array.isArray(dependencies) ? dependencies : [dependencies];
7163
+ return resolveDeps(toml.dependencies, weslSrc, projectDir);
7164
+ }
7165
+ /** Resolve the dependency list, replacing "auto" entries with discovered deps. */
7166
+ function resolveDeps(dependencies, weslSrc, projectDir) {
7167
+ const depsArray = Array.isArray(dependencies) ? dependencies : [dependencies ?? "auto"];
7127
7168
  if (!depsArray.includes("auto")) return depsArray;
7128
7169
  const base = depsArray.filter((dep) => dep !== "auto");
7129
- const deps = parseDependencies(weslSrc, projectDir);
7130
- return [...new Set([...base, ...deps])];
7170
+ const discovered = parseDependencies(weslSrc, projectDir);
7171
+ return [...new Set([...base, ...discovered])];
7131
7172
  }
7173
+ /** @return a function that resolves a shader path to a weslRoot-relative module path. */
7132
7174
  function makeGetWeslMain(context, unpluginContext) {
7133
7175
  return getWeslMain;
7134
7176
  /**
@@ -7137,58 +7179,40 @@ function makeGetWeslMain(context, unpluginContext) {
7137
7179
  */
7138
7180
  async function getWeslMain(shaderPath) {
7139
7181
  const { resolvedRoot } = await getWeslToml(context, unpluginContext);
7140
- await fs$1.access(shaderPath);
7182
+ await fs.access(shaderPath);
7141
7183
  const absRoot = path.join(process.cwd(), resolvedRoot);
7142
7184
  return toUnixPath(path.relative(absRoot, shaderPath));
7143
7185
  }
7144
7186
  }
7145
- /**
7146
- * Load the wesl files referenced in the wesl.toml file
7147
- *
7148
- * @return a record of wesl files with
7149
- * keys as wesl file paths, and
7150
- * values as wesl file contents.
7151
- */
7187
+ /** Load wesl files referenced in wesl.toml as a path-to-contents record. */
7152
7188
  async function loadWesl(context, unpluginCtx) {
7153
- const { toml: { include }, resolvedRoot, tomlDir } = await getWeslToml(context, unpluginCtx);
7189
+ const tomlInfo = await getWeslToml(context, unpluginCtx);
7190
+ const { resolvedRoot, tomlDir } = tomlInfo;
7191
+ const { include } = tomlInfo.toml;
7154
7192
  const futureFiles = include.map((g) => Ze(g, {
7155
7193
  cwd: tomlDir,
7156
7194
  absolute: true
7157
7195
  }));
7158
7196
  const files = (await Promise.all(futureFiles)).flat();
7159
- files.forEach((f) => {
7160
- unpluginCtx.addWatchFile(f);
7161
- });
7197
+ for (const f of files) unpluginCtx.addWatchFile(f);
7162
7198
  return await loadFiles(files, resolvedRoot);
7163
7199
  }
7164
- /** load a set of shader files, converting to paths relative to the weslRoot directory */
7200
+ /** Load shader files, returning paths relative to weslRoot. */
7165
7201
  async function loadFiles(files, weslRoot) {
7166
- const loaded = [];
7167
- for (const fullPath of files) {
7168
- const normalized = (await fs$1.readFile(fullPath, "utf-8")).replace(/\r\n/g, "\n");
7169
- const relativePath = path.relative(weslRoot, fullPath);
7170
- loaded.push([toUnixPath(relativePath), normalized]);
7171
- }
7172
- return Object.fromEntries(loaded);
7202
+ const entries = await Promise.all(files.map(async (fullPath) => {
7203
+ const normalized = (await fs.readFile(fullPath, "utf-8")).replace(/\r\n/g, "\n");
7204
+ return [toUnixPath(path.relative(weslRoot, fullPath)), normalized];
7205
+ }));
7206
+ return Object.fromEntries(entries);
7173
7207
  }
7174
7208
  function toUnixPath(p) {
7175
- if (path.sep !== "/") return p.replaceAll(path.sep, "/");
7176
- else return p;
7209
+ return path.sep !== "/" ? p.replaceAll(path.sep, "/") : p;
7177
7210
  }
7178
7211
 
7179
7212
  //#endregion
7180
7213
  //#region src/WeslPlugin.ts
7181
- /**
7182
- * A bundler plugin for processing WESL files.
7183
- *
7184
- * The plugin works by reading the wesl.toml file and possibly package.json
7185
- *
7186
- * The plugin is triggered by imports to special virtual module urls
7187
- * two urls suffixes are supported:
7188
- * 1. `import "./shaders/bar.wesl?reflect"` - produces a javascript file for binding struct reflection
7189
- * 2. `import "./shaders/bar.wesl?link"` - produces a javascript file for preconstructed link functions
7190
- */
7191
7214
  const builtinExtensions = [staticBuildExtension, linkBuildExtension];
7215
+ /** Bundler plugin for WESL files, triggered by ?link or ?static import suffixes. */
7192
7216
  function weslPlugin(options, meta) {
7193
7217
  const o = options ?? {};
7194
7218
  const extensions = [...builtinExtensions, ...o.extensions ?? []];
@@ -7210,10 +7234,8 @@ function weslPlugin(options, meta) {
7210
7234
  load: buildLoader(context, log),
7211
7235
  watchChange(id, _change) {
7212
7236
  log("watchChange", { id });
7213
- if (id.endsWith("wesl.toml")) {
7214
- cache.weslToml = void 0;
7215
- cache.registry = void 0;
7216
- } else cache.registry = void 0;
7237
+ if (id.endsWith("wesl.toml")) cache.weslToml = void 0;
7238
+ cache.registry = void 0;
7217
7239
  }
7218
7240
  };
7219
7241
  }
@@ -7224,110 +7246,91 @@ function pluginsByName(options) {
7224
7246
  const entries = options.extensions?.map((p) => [p.extensionName, p]) ?? [];
7225
7247
  return Object.fromEntries(entries);
7226
7248
  }
7227
- /** wesl plugins match import statements of the form:
7228
- *
7229
- * foo/bar.wesl?link
7230
- * or
7231
- * foo/bar.wesl COND=false ?static
7232
- *
7233
- * Bundlers may add extra query params (e.g. Vite adds ?import for dynamic imports,
7234
- * ?t=123 for cache busting), so we capture the full query and search within it.
7235
- *
7236
- * someday it'd be nice to support import attributes like:
7237
- * import "foo.bar.wesl?static" with { COND: false};
7238
- * (but that doesn't seem supported to be supported in the the bundler plugins yet)
7239
- */
7240
- const pluginMatch = /(^^)?(?<baseId>.*\.w[eg]sl)(?<cond>(\s*\w+(=\w+)?\s*)*)\?(?<query>.+)$/;
7249
+ /** Match .wesl/.wgsl imports with query params. Bundlers may append extra params. */
7250
+ const pluginMatch = /(^^)?(?<baseId>.*\.w[eg]sl)\?(?<query>.+)$/;
7241
7251
  const resolvedPrefix = "^^";
7242
- /** build plugin entry for 'resolverId'
7243
- * to validate our javascript virtual module imports (with e.g. ?static or ?link suffixes) */
7252
+ /** Reserved query param names (not treated as conditions). */
7253
+ const reservedParams = new Set(["include"]);
7254
+ /** Parse query string into plugin name, conditions, and options. */
7255
+ function parsePluginQuery(query, suffixes) {
7256
+ const segments = query.split("&");
7257
+ const pluginName = suffixes.find((s) => segments.includes(s));
7258
+ if (!pluginName) return null;
7259
+ const isBundlerParam = (s) => s === "import" || /^t=\d+/.test(s);
7260
+ const userSegments = segments.filter((s) => s !== pluginName && !isBundlerParam(s));
7261
+ const conditions = {};
7262
+ const options = {};
7263
+ for (const seg of userSegments) {
7264
+ const eqIdx = seg.indexOf("=");
7265
+ if (eqIdx === -1) conditions[seg] = true;
7266
+ else {
7267
+ const key = seg.slice(0, eqIdx);
7268
+ const val = seg.slice(eqIdx + 1);
7269
+ if (reservedParams.has(key)) options[key] = val;
7270
+ else conditions[key] = val !== "false";
7271
+ }
7272
+ }
7273
+ const hasConds = Object.keys(conditions).length > 0;
7274
+ const hasOpts = Object.keys(options).length > 0;
7275
+ return {
7276
+ pluginName,
7277
+ conditions: hasConds ? conditions : void 0,
7278
+ options: hasOpts ? options : void 0
7279
+ };
7280
+ }
7281
+ /** Build the resolveId hook for virtual module imports (?static, ?link, etc). */
7244
7282
  function buildResolver(options, context, log) {
7245
7283
  const suffixes = pluginNames(options);
7246
7284
  return resolver;
7247
- /**
7248
- * For imports with conditions, vite won't resolve the module-path part of the js import
7249
- * so we do it here.
7250
- *
7251
- * To avoid recirculating on resolve(), we rewrite the resolution id to start with ^^
7252
- * The loader will drop the prefix.
7253
- */
7254
7285
  function resolver(id, importer) {
7255
7286
  if (id.startsWith(resolvedPrefix)) return id;
7256
7287
  if (id === context.weslToml) return id;
7257
- const matched = pluginSuffixMatch(id, suffixes);
7288
+ const match = id.match(pluginMatch);
7289
+ const query = match?.groups?.query;
7290
+ if (!query) return null;
7291
+ const parsed = parsePluginQuery(query, suffixes);
7258
7292
  log("resolveId", {
7259
7293
  id,
7260
- matched: !!matched,
7294
+ matched: !!parsed,
7261
7295
  suffixes
7262
7296
  });
7263
- if (matched) {
7264
- const { importParams, baseId, pluginName } = matched;
7265
- const importerDir = path.dirname(importer);
7266
- const result = resolvedPrefix + path.join(importerDir, baseId) + importParams + "?" + pluginName;
7267
- log("resolveId resolved", { result });
7268
- return result;
7269
- }
7270
- return matched ? id : null;
7297
+ if (!parsed) return null;
7298
+ const baseId = match.groups.baseId;
7299
+ const importerDir = path.dirname(importer);
7300
+ const result = resolvedPrefix + path.join(importerDir, baseId) + "?" + query;
7301
+ log("resolveId resolved", { result });
7302
+ return result;
7271
7303
  }
7272
7304
  }
7273
- /** Find matching plugin suffix in query string (handles ?import&static, ?t=123&static, etc.) */
7274
- function pluginSuffixMatch(id, suffixes) {
7275
- const match = id.match(pluginMatch);
7276
- const query = match?.groups?.query;
7277
- if (!query) return null;
7278
- const segments = query.split("&");
7279
- const pluginName = suffixes.find((s) => segments.includes(s));
7280
- if (!pluginName) return null;
7281
- return {
7282
- pluginName,
7283
- baseId: match.groups.baseId,
7284
- importParams: match.groups?.cond
7285
- };
7286
- }
7287
- /** build plugin function for serving a javascript module in response to
7288
- * an import of of our virtual import modules. */
7305
+ /** Build the load hook that emits JS for virtual module imports. */
7289
7306
  function buildLoader(context, log) {
7290
7307
  const { options } = context;
7291
7308
  const suffixes = pluginNames(options);
7292
7309
  const pluginsMap = pluginsByName(options);
7293
7310
  return loader;
7294
7311
  async function loader(id) {
7295
- const matched = pluginSuffixMatch(id, suffixes);
7312
+ const match = id.match(pluginMatch);
7313
+ const query = match?.groups?.query;
7314
+ if (!query) return null;
7315
+ const parsed = parsePluginQuery(query, suffixes);
7296
7316
  log("load", {
7297
7317
  id,
7298
- matched: matched?.pluginName ?? null
7318
+ matched: parsed?.pluginName ?? null
7299
7319
  });
7300
- if (matched) {
7301
- const buildPluginApi = buildApi(context, this);
7302
- const plugin = pluginsMap[matched.pluginName];
7303
- const { baseId, importParams } = matched;
7304
- const conditions = importParamsToConditions(importParams);
7305
- const shaderPath = baseId.startsWith(resolvedPrefix) ? baseId.slice(2) : baseId;
7306
- log("load emitting", {
7307
- shaderPath,
7308
- conditions
7309
- });
7310
- return await plugin.emitFn(shaderPath, buildPluginApi, conditions);
7311
- }
7312
- return null;
7320
+ if (!parsed) return null;
7321
+ const buildPluginApi = buildApi(context, this);
7322
+ const plugin = pluginsMap[parsed.pluginName];
7323
+ const rawPath = match.groups.baseId;
7324
+ const shaderPath = rawPath.startsWith(resolvedPrefix) ? rawPath.slice(2) : rawPath;
7325
+ const { conditions, options: opts } = parsed;
7326
+ log("load emitting", {
7327
+ shaderPath,
7328
+ conditions,
7329
+ options: opts
7330
+ });
7331
+ return await plugin.emitFn(shaderPath, buildPluginApi, conditions, opts);
7313
7332
  }
7314
7333
  }
7315
- /**
7316
- * Convert an import parameters string to a Conditions record.
7317
- *
7318
- * Import parameters are key=value pairs separated by spaces.
7319
- * Values may be "true" or "false" or missing (default to true)
7320
- * e.g. ' MOBILE=true FUN SAFE=false '
7321
- */
7322
- function importParamsToConditions(importParams) {
7323
- if (!importParams) return void 0;
7324
- const condEntries = importParams.trim().split(/\s+/).map((p) => {
7325
- const [cond, value] = p.trim().split("=");
7326
- if (value === void 0 || value === "true") return [cond, true];
7327
- else return [cond, false];
7328
- });
7329
- return Object.fromEntries(condEntries);
7330
- }
7331
7334
  function fmtDebugData(data) {
7332
7335
  return data ? " " + JSON.stringify(data) : "";
7333
7336
  }
@@ -7335,9 +7338,7 @@ function debugLog(msg, data) {
7335
7338
  console.error(`[wesl-plugin] ${msg}${fmtDebugData(data)}`);
7336
7339
  }
7337
7340
  function noopLog() {}
7338
- const unplugin = createUnplugin((options, meta) => {
7339
- return weslPlugin(options, meta);
7340
- });
7341
+ const unplugin = createUnplugin(weslPlugin);
7341
7342
 
7342
7343
  //#endregion
7343
7344
  export { weslPlugin as n, unplugin as t };
@@ -1,4 +1,4 @@
1
- import { n as PluginExtension } from "./PluginExtension-nH8vi0nm.mjs";
1
+ import { n as PluginExtension } from "./PluginExtension-CHxUB0-8.mjs";
2
2
 
3
3
  //#region src/WeslPluginOptions.d.ts
4
4
  interface WeslPluginOptions {
@@ -1,19 +1,18 @@
1
- import { n as PluginExtension, r as PluginExtensionApi, t as ExtensionEmitFn } from "./PluginExtension-nH8vi0nm.mjs";
1
+ import { i as ProjectSources, n as PluginExtension, r as PluginExtensionApi, t as ExtensionEmitFn } from "./PluginExtension-CHxUB0-8.mjs";
2
2
 
3
3
  //#region src/extensions/LinkExtension.d.ts
4
4
  declare const linkBuildExtension: PluginExtension;
5
5
  //#endregion
6
+ //#region src/extensions/ReflectExtension.d.ts
7
+ interface SimpleReflectOptions {
8
+ /** directory to contain the .d.ts files or undefined to not write .d.ts files */
9
+ typesDir?: string;
10
+ }
11
+ /** wesl-js build extension to reflect wgsl structs into js and .d.ts files. */
12
+ declare function simpleReflect(options?: SimpleReflectOptions): PluginExtension;
13
+ //#endregion
6
14
  //#region src/extensions/StaticExtension.d.ts
7
- /**
8
- * a wesl-js ?static build extension that statically links from the root file
9
- * and emits a JavaScript file containing the linked wgsl string.
10
- *
11
- * use it like this:
12
- * import wgsl from "./shaders/app.wesl?static";
13
- *
14
- * or with conditions, like this:
15
- * import wgsl from "../shaders/foo/app.wesl MOBILE=true FUN SAFE=false ?static";
16
- */
15
+ /** Build extension for ?static imports: links WESL at build time, emits WGSL string. */
17
16
  declare const staticBuildExtension: PluginExtension;
18
17
  //#endregion
19
- export { ExtensionEmitFn, PluginExtension, PluginExtensionApi, linkBuildExtension, staticBuildExtension };
18
+ export { ExtensionEmitFn, PluginExtension, PluginExtensionApi, ProjectSources, SimpleReflectOptions, linkBuildExtension, simpleReflect, staticBuildExtension };
@@ -1,3 +1,32 @@
1
- import { r as linkBuildExtension, t as staticBuildExtension } from "./StaticExtension-Chl5KPpw.mjs";
1
+ import { r as linkBuildExtension, t as staticBuildExtension } from "./StaticExtension-Q8HMuFJa.mjs";
2
+ import fs from "node:fs/promises";
3
+ import { originalTypeName, weslStructs, wgslTypeToTs } from "wesl-reflect";
2
4
 
3
- export { linkBuildExtension, staticBuildExtension };
5
+ //#region src/extensions/ReflectExtension.ts
6
+ /** wesl-js build extension to reflect wgsl structs into js and .d.ts files. */
7
+ function simpleReflect(options = {}) {
8
+ const { typesDir = "./src/types" } = options;
9
+ return {
10
+ extensionName: "simple_reflect",
11
+ emitFn: async (_baseId, api) => {
12
+ const astStructs = [...(await api.weslRegistry()).allModules()].flatMap(([, module]) => module.moduleElem.contents.filter((e) => e.kind === "struct"));
13
+ const jsStructs = weslStructs(astStructs);
14
+ if (typesDir) await writeTypes(astStructs, typesDir);
15
+ return `export const structs = ${JSON.stringify(jsStructs, null, 2)};`;
16
+ }
17
+ };
18
+ }
19
+ /** Write .d.ts file with TypeScript interfaces for reflected structs. */
20
+ async function writeTypes(structs, typesDir) {
21
+ const tsdInterfaces = structs.map((s) => {
22
+ return `interface ${s.name.ident.originalName} {\n${s.members.map((m) => {
23
+ const tsType = wgslTypeToTs(originalTypeName(m.typeRef));
24
+ return ` ${m.name.name}: ${tsType};`;
25
+ }).join("\n")}\n}`;
26
+ });
27
+ await fs.mkdir(typesDir, { recursive: true });
28
+ await fs.writeFile(`${typesDir}/reflectTypes.d.ts`, tsdInterfaces.join("\n"));
29
+ }
30
+
31
+ //#endregion
32
+ export { linkBuildExtension, simpleReflect, staticBuildExtension };
@@ -1,4 +1,4 @@
1
- import { t as WeslPluginOptions } from "../WeslPluginOptions-DWIrd183.mjs";
1
+ import { t as WeslPluginOptions } from "../WeslPluginOptions-kd-CXSl9.mjs";
2
2
 
3
3
  //#region src/plugins/astro.d.ts
4
4
  declare const _default: (options: WeslPluginOptions) => any;
@@ -1,5 +1,5 @@
1
- import { t as unplugin } from "../WeslPlugin-BKQJvQve.mjs";
2
- import "../StaticExtension-Chl5KPpw.mjs";
1
+ import { t as unplugin } from "../WeslPlugin-CajtfXHH.mjs";
2
+ import "../StaticExtension-Q8HMuFJa.mjs";
3
3
 
4
4
  //#region src/plugins/astro.ts
5
5
  var astro_default = (options) => ({