wesl-plugin 0.6.0-pre10

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.
Files changed (41) hide show
  1. package/README.md +102 -0
  2. package/dist/PluginExtension-Bvr6MzG5.d.ts +43 -0
  3. package/dist/chunk-6WJP7I7R.js +6754 -0
  4. package/dist/chunk-7ZBFDM3F.js +11 -0
  5. package/dist/chunk-JSBRDJBE.js +30 -0
  6. package/dist/chunk-U52VQTCR.js +11 -0
  7. package/dist/pluginIndex.d.ts +7 -0
  8. package/dist/pluginIndex.js +43 -0
  9. package/dist/plugins/astro.d.ts +7 -0
  10. package/dist/plugins/astro.js +18 -0
  11. package/dist/plugins/esbuild.d.ts +8 -0
  12. package/dist/plugins/esbuild.js +11 -0
  13. package/dist/plugins/farm.d.ts +8 -0
  14. package/dist/plugins/farm.js +11 -0
  15. package/dist/plugins/nuxt.d.ts +10 -0
  16. package/dist/plugins/nuxt.js +28 -0
  17. package/dist/plugins/rollup.d.ts +8 -0
  18. package/dist/plugins/rollup.js +11 -0
  19. package/dist/plugins/rspack.d.ts +7 -0
  20. package/dist/plugins/rspack.js +11 -0
  21. package/dist/plugins/vite.d.ts +8 -0
  22. package/dist/plugins/vite.js +8 -0
  23. package/dist/plugins/webpack.d.ts +8 -0
  24. package/dist/plugins/webpack.js +8 -0
  25. package/dist/weslPluginOptions-DAx0E_JK.d.ts +8 -0
  26. package/package.json +100 -0
  27. package/src/BindingLayoutExtension.ts +35 -0
  28. package/src/LinkExtension.ts +58 -0
  29. package/src/PluginExtension.ts +26 -0
  30. package/src/defaultSuffixTypes.d.ts +14 -0
  31. package/src/pluginIndex.ts +2 -0
  32. package/src/plugins/astro.ts +13 -0
  33. package/src/plugins/esbuild.ts +4 -0
  34. package/src/plugins/farm.ts +4 -0
  35. package/src/plugins/nuxt.ts +25 -0
  36. package/src/plugins/rollup.ts +4 -0
  37. package/src/plugins/rspack.ts +4 -0
  38. package/src/plugins/vite.ts +4 -0
  39. package/src/plugins/webpack.ts +4 -0
  40. package/src/weslPlugin.ts +335 -0
  41. package/src/weslPluginOptions.ts +6 -0
@@ -0,0 +1,335 @@
1
+ import { glob } from "glob";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import toml from "toml";
5
+ import type {
6
+ ExternalIdResult,
7
+ Thenable,
8
+ TransformResult,
9
+ UnpluginBuildContext,
10
+ UnpluginContext,
11
+ UnpluginContextMeta,
12
+ UnpluginOptions
13
+ } from "unplugin";
14
+ import { createUnplugin } from "unplugin";
15
+ import { parsedRegistry, ParsedRegistry, parseIntoRegistry } from "wesl";
16
+ import { PluginExtension, PluginExtensionApi } from "./PluginExtension.js";
17
+ import type { WeslPluginOptions } from "./weslPluginOptions.js";
18
+
19
+ /** loaded (or synthesized) info from .toml */
20
+ export interface WeslToml {
21
+ /** glob search strings to find .wesl/.wgsl files. Relative to the toml directory. */
22
+ weslFiles: string[];
23
+
24
+ /** base directory for wesl files. Relative to the toml directory. */
25
+ weslRoot: string;
26
+
27
+ /** names of directly referenced wesl shader packages (e.g. npm package names) */
28
+ dependencies?: string[];
29
+ }
30
+
31
+ export interface WeslTomlInfo {
32
+ /** The path to the toml file, relative to the cwd, undefined if no toml file */
33
+ tomlFile: string | undefined;
34
+
35
+ /** The path to the directory that contains the toml.
36
+ * Relative to the cwd. Paths inside the toml are relative to this. */
37
+ tomlDir: string;
38
+
39
+ /** The wesl root, relative to the cwd.
40
+ * This lets us correctly do `path.resolve(resolvedWeslRoot, someShaderFile)` */
41
+ resolvedWeslRoot: string;
42
+
43
+ /** The underlying toml file */
44
+ toml: WeslToml;
45
+ }
46
+
47
+ /** internal cache used by the plugin to avoid reloading files
48
+ * The assumption is that the plugin is used for a single wesl.toml and set of shaders
49
+ * (a plugin instance supports only one shader project)
50
+ */
51
+ interface PluginCache {
52
+ registry?: ParsedRegistry;
53
+ weslToml?: WeslTomlInfo;
54
+ }
55
+
56
+ /** some types from unplugin */
57
+ type Resolver = (
58
+ this: UnpluginBuildContext & UnpluginContext,
59
+ id: string,
60
+ importer: string | undefined,
61
+ options: {
62
+ isEntry: boolean;
63
+ },
64
+ ) => Thenable<string | ExternalIdResult | null | undefined>;
65
+
66
+ type Loader = (
67
+ this: UnpluginBuildContext & UnpluginContext,
68
+ id: string,
69
+ ) => Thenable<TransformResult>;
70
+
71
+ /** convenient state for local functions */
72
+ interface PluginContext {
73
+ cache: PluginCache;
74
+ options: WeslPluginOptions;
75
+ meta: UnpluginContextMeta;
76
+ }
77
+
78
+ /**
79
+ * A bundler plugin for processing WESL files.
80
+ *
81
+ * The plugin works by reading the wesl.toml file and possibly package.json
82
+ *
83
+ * The plugin is triggered by imports to special virtual module urls
84
+ * two urls suffixes are supported:
85
+ * 1. `import "./shaders/bar.wesl?reflect"` - produces a javascript file for binding struct reflection
86
+ * 2. `import "./shaders/bar.wesl?link"` - produces a javascript file for preconstructed link functions
87
+ */
88
+ export function weslPlugin(
89
+ options: WeslPluginOptions = {},
90
+ meta: UnpluginContextMeta,
91
+ ): UnpluginOptions {
92
+ const cache: PluginCache = {};
93
+ const context: PluginContext = { cache, meta, options };
94
+
95
+ return {
96
+ name: "wesl-plugin",
97
+ resolveId: buildResolver(options),
98
+ load: buildLoader(context),
99
+ watchChange(id, change) {
100
+ if (id.endsWith("wesl.toml")) {
101
+ // The cache is shared for multiple imports
102
+ cache.weslToml = undefined;
103
+ cache.registry = undefined;
104
+ } else {
105
+ cache.registry = undefined;
106
+ }
107
+ },
108
+ };
109
+ }
110
+
111
+ function pluginNames(options: WeslPluginOptions): string[] {
112
+ return options.extensions?.map(p => p.extensionName) ?? [];
113
+ }
114
+
115
+ function pluginsByName(
116
+ options: WeslPluginOptions,
117
+ ): Record<string, PluginExtension> {
118
+ const entries = options.extensions?.map(p => [p.extensionName, p]) ?? [];
119
+ return Object.fromEntries(entries);
120
+ }
121
+
122
+ const pluginSuffix = /(?<baseId>.*\.w[eg]sl)\?(?<pluginName>[\w_-]+)/;
123
+
124
+ /** build plugin entry for 'resolverId'
125
+ * to validate our virtual import modules (with ?reflect or ?link suffixes) */
126
+ function buildResolver(options: WeslPluginOptions): Resolver {
127
+ const suffixes = pluginNames(options);
128
+ return resolver;
129
+
130
+ function resolver(
131
+ this: UnpluginBuildContext & UnpluginContext,
132
+ id: string,
133
+ ): string | null {
134
+ const matched = pluginSuffixMatch(id, suffixes);
135
+ return matched ? id : null;
136
+ }
137
+ }
138
+ interface PluginMatch {
139
+ baseId: string;
140
+ pluginName: string;
141
+ }
142
+
143
+ function pluginSuffixMatch(id: string, suffixes: string[]): PluginMatch | null {
144
+ const suffixMatch = id.match(pluginSuffix);
145
+ const pluginName = suffixMatch?.groups?.pluginName;
146
+ if (!pluginName || !suffixes.includes(pluginName)) return null;
147
+ return { pluginName, baseId: suffixMatch.groups!.baseId };
148
+ }
149
+
150
+ function buildApi(
151
+ context: PluginContext,
152
+ unpluginCtx: UnpluginBuildContext & UnpluginContext,
153
+ ): PluginExtensionApi {
154
+ return {
155
+ weslToml: async () => getWeslToml(context, unpluginCtx),
156
+ weslSrc: async () => loadWesl(context, unpluginCtx),
157
+ weslRegistry: async () => getRegistry(context, unpluginCtx),
158
+ weslMain: makeGetWeslMain(context, unpluginCtx),
159
+ };
160
+ }
161
+
162
+ /** build plugin function for serving a javascript module in response to
163
+ * an import of of our virtual import modules. */
164
+ function buildLoader(context: PluginContext): Loader {
165
+ const { options } = context;
166
+ const suffixes = pluginNames(options);
167
+ const pluginsMap = pluginsByName(options);
168
+ return loader;
169
+
170
+ async function loader(
171
+ this: UnpluginBuildContext & UnpluginContext,
172
+ id: string,
173
+ ) {
174
+ const matched = pluginSuffixMatch(id, suffixes);
175
+ if (matched) {
176
+ const buildPluginApi = buildApi(context, this);
177
+ const plugin = pluginsMap[matched.pluginName];
178
+ return await plugin.emitFn(matched.baseId, buildPluginApi);
179
+ }
180
+
181
+ return null;
182
+ }
183
+ }
184
+ export const defaultTomlMessage = `no wesl.toml found: assuming .wesl files are in ./shaders`;
185
+
186
+ /** load the wesl.toml */
187
+ async function getWeslToml(
188
+ context: PluginContext,
189
+ unpluginCtx: UnpluginBuildContext & UnpluginContext,
190
+ ): Promise<WeslTomlInfo> {
191
+ const { cache } = context;
192
+ if (cache.weslToml) return cache.weslToml;
193
+
194
+ // find the wesl.toml file if it exists
195
+ const specifiedToml = context.options.weslToml;
196
+ let tomlFile: string | undefined;
197
+ if (specifiedToml) {
198
+ fs.access(specifiedToml);
199
+ tomlFile = specifiedToml;
200
+ } else {
201
+ tomlFile = await fs
202
+ .access("wesl.toml")
203
+ .then(() => "wesl.toml")
204
+ .catch(() => {
205
+ return undefined;
206
+ });
207
+ }
208
+
209
+ // load the toml contents
210
+ let parsedToml: WeslToml;
211
+ let tomlDir: string;
212
+ if (tomlFile) {
213
+ unpluginCtx.addWatchFile(tomlFile); // The cache gets cleared by the watchChange hook
214
+ const tomlString = await fs.readFile(tomlFile, "utf-8");
215
+ parsedToml = toml.parse(tomlString) as WeslToml;
216
+ const tomlDirAbsolute = path.dirname(tomlFile);
217
+ tomlDir = path.relative(process.cwd(), tomlDirAbsolute);
218
+ } else {
219
+ console.log(defaultTomlMessage);
220
+ parsedToml = { weslFiles: ["shaders/**/*.w[eg]sl"], weslRoot: "shaders" };
221
+ tomlDir = ".";
222
+ }
223
+
224
+ const tomlToWeslRoot = path.resolve(tomlDir, parsedToml.weslRoot);
225
+ const resolvedWeslRoot = path.relative(process.cwd(), tomlToWeslRoot);
226
+ cache.weslToml = { tomlFile, tomlDir, resolvedWeslRoot, toml: parsedToml };
227
+ return cache.weslToml;
228
+ }
229
+
230
+ /** load and parse all the wesl files into a ParsedRegistry */
231
+ async function getRegistry(
232
+ context: PluginContext,
233
+ unpluginCtx: UnpluginBuildContext & UnpluginContext,
234
+ ): Promise<ParsedRegistry> {
235
+ const { cache } = context;
236
+ let { registry } = cache;
237
+ if (registry) return registry;
238
+
239
+ // load wesl files into registry
240
+ const loaded = await loadWesl(context, unpluginCtx);
241
+ const { resolvedWeslRoot } = await getWeslToml(context, unpluginCtx);
242
+
243
+ registry = parsedRegistry();
244
+ parseIntoRegistry(loaded, registry);
245
+
246
+ // The paths are relative to the weslRoot, but vite needs actual filesystem paths
247
+ const fullPaths = Object.keys(loaded).map(p =>
248
+ path.resolve(resolvedWeslRoot, p),
249
+ );
250
+
251
+ // trigger clearing cache on shader file change
252
+ fullPaths.forEach(f => {
253
+ unpluginCtx.addWatchFile(f);
254
+ });
255
+
256
+ cache.registry = registry;
257
+ return registry;
258
+ }
259
+
260
+ function makeGetWeslMain(
261
+ context: PluginContext,
262
+ unpluginContext: UnpluginBuildContext & UnpluginContext,
263
+ ): (baseId: string) => Promise<string> {
264
+ return getWeslMain;
265
+
266
+ /**
267
+ * @param baseId is the absolute path to the shader file
268
+ * @return the / separated path to the shader file, relative to the weslRoot
269
+ */
270
+ async function getWeslMain(baseId: string): Promise<string> {
271
+ const { resolvedWeslRoot } = await getWeslToml(context, unpluginContext);
272
+ await fs.access(baseId); // if file doesn't exist, report now when the user problem is clear.
273
+
274
+ const absRoot = path.join(process.cwd(), resolvedWeslRoot);
275
+ const weslRootToMain = path.relative(absRoot, baseId);
276
+ return toUnixPath(weslRootToMain);
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Load the wesl files referenced in the wesl.toml file
282
+ *
283
+ * @return a record of wesl files with
284
+ * keys as wesl file paths, and
285
+ * values as wesl file contents.
286
+ */
287
+ async function loadWesl(
288
+ context: PluginContext,
289
+ unpluginCtx: UnpluginBuildContext & UnpluginContext,
290
+ ): Promise<Record<string, string>> {
291
+ const {
292
+ toml: { weslFiles },
293
+ resolvedWeslRoot,
294
+ tomlDir,
295
+ } = await getWeslToml(context, unpluginCtx);
296
+ const futureFiles = weslFiles.map(g =>
297
+ glob(g, { cwd: tomlDir, absolute: true }),
298
+ );
299
+ const files = (await Promise.all(futureFiles)).flat();
300
+
301
+ // trigger rebuild on shader file change
302
+ files.forEach(f => unpluginCtx.addWatchFile(f));
303
+
304
+ return await loadFiles(files, resolvedWeslRoot);
305
+ }
306
+
307
+ /** load a set of shader files, converting to paths relative to the weslRoot directory */
308
+ async function loadFiles(
309
+ files: string[],
310
+ weslRoot: string,
311
+ ): Promise<Record<string, string>> {
312
+ const loaded: [string, string][] = [];
313
+
314
+ for (const fullPath of files) {
315
+ const data = await fs.readFile(fullPath, "utf-8");
316
+ const relativePath = path.relative(weslRoot, fullPath);
317
+ loaded.push([toUnixPath(relativePath), data]);
318
+ }
319
+ return Object.fromEntries(loaded);
320
+ }
321
+
322
+ function toUnixPath(p: string): string {
323
+ if (path.sep !== "/") {
324
+ return p.replaceAll(path.sep, "/");
325
+ } else {
326
+ return p;
327
+ }
328
+ }
329
+
330
+ export const unplugin = createUnplugin(
331
+ (options: WeslPluginOptions, meta: UnpluginContextMeta) => {
332
+ return weslPlugin(options, meta);
333
+ },
334
+ );
335
+ export default unplugin;
@@ -0,0 +1,6 @@
1
+ import { PluginExtension } from "./PluginExtension.ts";
2
+
3
+ export interface WeslPluginOptions {
4
+ weslToml?: string;
5
+ extensions?: PluginExtension[];
6
+ }