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.
- package/README.md +102 -0
- package/dist/PluginExtension-Bvr6MzG5.d.ts +43 -0
- package/dist/chunk-6WJP7I7R.js +6754 -0
- package/dist/chunk-7ZBFDM3F.js +11 -0
- package/dist/chunk-JSBRDJBE.js +30 -0
- package/dist/chunk-U52VQTCR.js +11 -0
- package/dist/pluginIndex.d.ts +7 -0
- package/dist/pluginIndex.js +43 -0
- package/dist/plugins/astro.d.ts +7 -0
- package/dist/plugins/astro.js +18 -0
- package/dist/plugins/esbuild.d.ts +8 -0
- package/dist/plugins/esbuild.js +11 -0
- package/dist/plugins/farm.d.ts +8 -0
- package/dist/plugins/farm.js +11 -0
- package/dist/plugins/nuxt.d.ts +10 -0
- package/dist/plugins/nuxt.js +28 -0
- package/dist/plugins/rollup.d.ts +8 -0
- package/dist/plugins/rollup.js +11 -0
- package/dist/plugins/rspack.d.ts +7 -0
- package/dist/plugins/rspack.js +11 -0
- package/dist/plugins/vite.d.ts +8 -0
- package/dist/plugins/vite.js +8 -0
- package/dist/plugins/webpack.d.ts +8 -0
- package/dist/plugins/webpack.js +8 -0
- package/dist/weslPluginOptions-DAx0E_JK.d.ts +8 -0
- package/package.json +100 -0
- package/src/BindingLayoutExtension.ts +35 -0
- package/src/LinkExtension.ts +58 -0
- package/src/PluginExtension.ts +26 -0
- package/src/defaultSuffixTypes.d.ts +14 -0
- package/src/pluginIndex.ts +2 -0
- package/src/plugins/astro.ts +13 -0
- package/src/plugins/esbuild.ts +4 -0
- package/src/plugins/farm.ts +4 -0
- package/src/plugins/nuxt.ts +25 -0
- package/src/plugins/rollup.ts +4 -0
- package/src/plugins/rspack.ts +4 -0
- package/src/plugins/vite.ts +4 -0
- package/src/plugins/webpack.ts +4 -0
- package/src/weslPlugin.ts +335 -0
- 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;
|