vitest-environment-webgpu-node 1.0.0 → 1.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.
package/CHANGELOG.md CHANGED
@@ -1,13 +1,5 @@
1
- ## [vitest-environment-webgpu-node-v1.0.0](https://github.com/bhouston/vitest-gpu/compare/vitest-environment-webgpu-node-v0.1.0...vitest-environment-webgpu-node-v1.0.0) (2026-09-18)
2
-
3
- ### ⚠ BREAKING CHANGES
4
-
5
- * **screenshot:** the matcher is async and must be awaited; reference paths need an image extension.
6
-
7
- ### Features
8
-
9
- * **screenshot:** accept canvases, images and any image format in toMatchScreenshot ([edaabc4](https://github.com/bhouston/vitest-gpu/commit/edaabc43ac0ab895601565129a8db38121cfa5d3)), closes [#1](https://github.com/bhouston/vitest-gpu/issues/1)
1
+ ## [vitest-environment-webgpu-node-v1.0.2](https://github.com/bhouston/vitest-gpu/compare/vitest-environment-webgpu-node-v1.0.1...vitest-environment-webgpu-node-v1.0.2) (2026-09-21)
10
2
 
11
3
  ### Bug Fixes
12
4
 
13
- * **test:** set testTimeout explicitly on every vitest project ([49858e1](https://github.com/bhouston/vitest-gpu/commit/49858e1e84916e0852add97075666ffeb82c0e7b)), closes [#8](https://github.com/bhouston/vitest-gpu/issues/8)
5
+ * support public types across TypeScript versions ([0f3df06](https://github.com/bhouston/vitest-gpu/commit/0f3df067f28308e0154045304ecbcaf2ba840fcc)), closes [#24](https://github.com/bhouston/vitest-gpu/issues/24)
package/README.md CHANGED
@@ -6,6 +6,10 @@ Real, headless WebGPU inside Vitest, with no browser. Powered by Google's Dawn t
6
6
  The environment puts `navigator.gpu` and every `GPU*` class and constant (`GPUBufferUsage`,
7
7
  `GPUShaderStage`, ...) on the global object before each test file and removes them afterwards.
8
8
 
9
+ Using Jest instead? See the equivalent
10
+ [`jest-environment-webgpu-node`](https://github.com/bhouston/jest-gpu/tree/main/packages/jest-environment-webgpu-node)
11
+ in [jest-gpu](https://github.com/bhouston/jest-gpu).
12
+
9
13
  ## Install
10
14
 
11
15
  ```sh
@@ -24,6 +28,22 @@ export default defineConfig({
24
28
  });
25
29
  ```
26
30
 
31
+ ### TypeScript types
32
+
33
+ The package selects WebGPU declarations for your compiler version. TypeScript 5 receives the bundled
34
+ `@webgpu/types` dependency; TypeScript 6 and later use WebGPU declarations from their DOM library. Include
35
+ the DOM library and keep `skipLibCheck` disabled to catch declaration conflicts:
36
+
37
+ ```json
38
+ {
39
+ "compilerOptions": {
40
+ "lib": ["ES2024", "DOM"],
41
+ "types": ["node"],
42
+ "skipLibCheck": false
43
+ }
44
+ }
45
+ ```
46
+
27
47
  ```ts
28
48
  import { expect, it } from 'vitest';
29
49
 
@@ -55,5 +75,16 @@ await expect(canvas).toMatchScreenshot('scene.png');
55
75
  ```
56
76
 
57
77
  `readPixels()` returns `{ width, height, data }` RGBA8 pixels, ready for
58
- [`vitest-screenshot`](../vitest-screenshot). `asElement()` is the same object typed as an
59
- `HTMLCanvasElement` for library signatures that demand one.
78
+ [`vitest-screenshot`](../vitest-screenshot). Readback supports `rgba8unorm`, `rgba8unorm-srgb`,
79
+ `bgra8unorm`, and `bgra8unorm-srgb`; other canvas formats remain valid for rendering but
80
+ `readPixels()` rejects them with an unsupported-format error. `asElement()` returns the same object and
81
+ defaults to `HTMLCanvasElement` when DOM types are available. You can also request a library-specific type
82
+ explicitly with `canvas.asElement<HTMLCanvasElement>()`.
83
+
84
+ ## Author
85
+
86
+ Created by [Ben Houston](https://ben3d.ca) and sponsored by [Land of Assets](https://landofassets.com).
87
+
88
+ ## License
89
+
90
+ MIT
package/dist/canvas.d.ts CHANGED
@@ -4,6 +4,11 @@ export type RgbaImage = {
4
4
  height: number;
5
5
  data: Uint8Array;
6
6
  };
7
+ type DefaultCanvasElement = typeof globalThis extends {
8
+ HTMLCanvasElement: {
9
+ prototype: infer TElement;
10
+ };
11
+ } ? TElement : HeadlessCanvas;
7
12
  /** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
8
13
  export declare class HeadlessCanvasContext {
9
14
  #private;
@@ -28,8 +33,8 @@ export declare class HeadlessCanvas {
28
33
  getContext(id: 'webgpu'): HeadlessCanvasContext;
29
34
  getContext(id: string): HeadlessCanvasContext | null;
30
35
  readPixels(): Promise<RgbaImage>;
31
- /** This same object, typed as an element for libraries whose signatures demand one. */
32
- asElement(): HTMLCanvasElement;
36
+ /** This same object, cast to the element type a rendering library expects. */
37
+ asElement<TElement = DefaultCanvasElement>(): TElement;
33
38
  getBoundingClientRect(): {
34
39
  x: number;
35
40
  y: number;
@@ -45,4 +50,5 @@ export declare class HeadlessCanvas {
45
50
  dispatchEvent(): boolean;
46
51
  }
47
52
  export declare const createCanvas: (width: number, height: number) => HeadlessCanvas;
53
+ export {};
48
54
  //# sourceMappingURL=canvas.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAO5E,iIAAiI;AACjI,qBAAa,qBAAqB;;IAIpB,QAAQ,CAAC,MAAM,EAAE,cAAc;IAA3C,YAAqB,MAAM,EAAE,cAAc,EAAI;IAE/C,SAAS,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAG9C;IAED,WAAW,IAAI,IAAI,CAGlB;IAED,gBAAgB,IAAI,sBAAsB,GAAG,IAAI,CAEhD;IAED,iBAAiB,IAAI,UAAU,CAY9B;IAED,gFAAgF;IAC1E,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAoBrC;CAMF;AAED,6FAA6F;AAC7F,qBAAa,cAAc;;IAMhB,KAAK;IACL,MAAM;IALf,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IAG5C,YACS,KAAK,SAAM,EACX,MAAM,SAAM,EACjB;IAEJ,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,UAAU,CAAC,EAAE,EAAE,QAAQ,GAAG,qBAAqB,CAAC;IAChD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,CAAC;IAKrD,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAE/B;IAED,uFAAuF;IACvF,SAAS,IAAI,iBAAiB,CAE7B;IAED,qBAAqB,IAAI;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAE1G;IAED,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;IAED,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAExC;IAED,gBAAgB,IAAI,IAAI,CAAG;IAC3B,mBAAmB,IAAI,IAAI,CAAG;IAC9B,aAAa,IAAI,OAAO,CAEvB;CACF;AAED,eAAO,MAAM,YAAY,UAAW,MAAM,UAAU,MAAM,KAAG,cAAmD,CAAC"}
1
+ {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAE5E,KAAK,oBAAoB,GAAG,OAAO,UAAU,SAAS;IACpD,iBAAiB,EAAE;QAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;KAAE,CAAC;CAClD,GACG,QAAQ,GACR,cAAc,CAAC;AAanB,iIAAiI;AACjI,qBAAa,qBAAqB;;IAIpB,QAAQ,CAAC,MAAM,EAAE,cAAc;IAA3C,YAAqB,MAAM,EAAE,cAAc,EAAI;IAE/C,SAAS,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,CAG9C;IAED,WAAW,IAAI,IAAI,CAGlB;IAED,gBAAgB,IAAI,sBAAsB,GAAG,IAAI,CAEhD;IAED,iBAAiB,IAAI,UAAU,CAY9B;IAED,gFAAgF;IAC1E,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAmCrC;CAMF;AAED,6FAA6F;AAC7F,qBAAa,cAAc;;IAMhB,KAAK;IACL,MAAM;IALf,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IAG5C,YACS,KAAK,SAAM,EACX,MAAM,SAAM,EACjB;IAEJ,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,UAAU,CAAC,EAAE,EAAE,QAAQ,GAAG,qBAAqB,CAAC;IAChD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,CAAC;IAKrD,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC,CAE/B;IAED,8EAA8E;IAC9E,SAAS,CAAC,QAAQ,GAAG,oBAAoB,KAAK,QAAQ,CAErD;IAED,qBAAqB,IAAI;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAE1G;IAED,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE9C;IAED,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAExC;IAED,gBAAgB,IAAI,IAAI,CAAG;IAC3B,mBAAmB,IAAI,IAAI,CAAG;IAC9B,aAAa,IAAI,OAAO,CAEvB;CACF;AAED,eAAO,MAAM,YAAY,UAAW,MAAM,UAAU,MAAM,KAAG,cAAmD,CAAC"}
package/dist/canvas.js CHANGED
@@ -2,6 +2,12 @@ const COPY_SRC = 0x01;
2
2
  const RENDER_ATTACHMENT = 0x10;
3
3
  const MAP_READ = 0x01;
4
4
  const COPY_DST = 0x08;
5
+ const READABLE_FORMATS = new Set([
6
+ 'rgba8unorm',
7
+ 'rgba8unorm-srgb',
8
+ 'bgra8unorm',
9
+ 'bgra8unorm-srgb',
10
+ ]);
5
11
  /** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
6
12
  export class HeadlessCanvasContext {
7
13
  canvas;
@@ -38,26 +44,43 @@ export class HeadlessCanvasContext {
38
44
  }
39
45
  /** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
40
46
  async readPixels() {
47
+ const config = this.#config;
48
+ if (!config)
49
+ throw new Error('readPixels() called before configure()');
50
+ const { device, format } = config;
51
+ if (!READABLE_FORMATS.has(format)) {
52
+ throw new Error(`readPixels() does not support canvas format "${format}"; supported formats are rgba8unorm, rgba8unorm-srgb, bgra8unorm, and bgra8unorm-srgb`);
53
+ }
41
54
  const texture = this.getCurrentTexture();
42
- const { device, format } = this.#config;
43
55
  const { width, height } = texture;
44
56
  const bytesPerRow = Math.ceil((width * 4) / 256) * 256;
45
57
  const buffer = device.createBuffer({ size: bytesPerRow * height, usage: MAP_READ | COPY_DST });
46
- const encoder = device.createCommandEncoder();
47
- encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, [width, height]);
48
- device.queue.submit([encoder.finish()]);
49
- await buffer.mapAsync(MAP_READ);
50
- const padded = new Uint8Array(buffer.getMappedRange());
51
- const data = new Uint8Array(width * height * 4);
52
- for (let y = 0; y < height; y++)
53
- data.set(padded.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
54
- buffer.unmap();
55
- buffer.destroy();
56
- if (format.startsWith('bgra')) {
57
- for (let i = 0; i < data.length; i += 4)
58
- [data[i], data[i + 2]] = [data[i + 2], data[i]];
58
+ let mapped = false;
59
+ try {
60
+ const encoder = device.createCommandEncoder();
61
+ encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, [width, height]);
62
+ device.queue.submit([encoder.finish()]);
63
+ await buffer.mapAsync(MAP_READ);
64
+ mapped = true;
65
+ const padded = new Uint8Array(buffer.getMappedRange());
66
+ const data = new Uint8Array(width * height * 4);
67
+ for (let y = 0; y < height; y++)
68
+ data.set(padded.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
69
+ if (format.startsWith('bgra')) {
70
+ for (let i = 0; i < data.length; i += 4)
71
+ [data[i], data[i + 2]] = [data[i + 2], data[i]];
72
+ }
73
+ return { width, height, data };
74
+ }
75
+ finally {
76
+ try {
77
+ if (mapped)
78
+ buffer.unmap();
79
+ }
80
+ finally {
81
+ buffer.destroy();
82
+ }
59
83
  }
60
- return { width, height, data };
61
84
  }
62
85
  #drop() {
63
86
  this.#texture?.destroy();
@@ -87,7 +110,7 @@ export class HeadlessCanvas {
87
110
  readPixels() {
88
111
  return this.#context.readPixels();
89
112
  }
90
- /** This same object, typed as an element for libraries whose signatures demand one. */
113
+ /** This same object, cast to the element type a rendering library expects. */
91
114
  asElement() {
92
115
  return this;
93
116
  }
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,10 +1,18 @@
1
- import type { Environment } from 'vitest/environments';
2
1
  export { createCanvas, HeadlessCanvas, HeadlessCanvasContext, type RgbaImage } from './canvas.js';
3
2
  /** Set under `test.environmentOptions.webgpuNode` in your Vitest config. */
4
3
  export type WebgpuNodeOptions = {
5
4
  /** Dawn options, e.g. `['backend=vulkan', 'enable-dawn-features=allow_unsafe_apis']`. */
6
5
  dawnOptions?: string[];
7
6
  };
8
- declare const _default: Environment;
7
+ declare const _default: {
8
+ name: string;
9
+ transformMode: "ssr";
10
+ viteEnvironment: "ssr";
11
+ setup(global: Record<string, unknown>, { webgpuNode }: {
12
+ webgpuNode?: WebgpuNodeOptions;
13
+ }): {
14
+ teardown(_global?: Record<string, unknown>): void;
15
+ };
16
+ };
9
17
  export default _default;
10
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAIvD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAElG,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,yFAAyF;IACzF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;wBA6Bc,WAAW"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAElG,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,yFAAyF;IACzF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;;;;;kBA0Cc,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAuB;QAAE,UAAU,CAAC,EAAE,iBAAiB,CAAA;KAAE;2BA8BvE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC"}
package/dist/index.js CHANGED
@@ -25,21 +25,52 @@ const domShims = () => {
25
25
  cancelAnimationFrame: clearTimeout,
26
26
  };
27
27
  };
28
+ const restore = (properties) => {
29
+ for (const { target, key, descriptor } of properties.toReversed()) {
30
+ if (descriptor)
31
+ Object.defineProperty(target, key, descriptor);
32
+ else
33
+ Reflect.deleteProperty(target, key);
34
+ }
35
+ };
28
36
  export default {
29
37
  name: 'webgpu-node',
38
+ transformMode: 'ssr',
30
39
  viteEnvironment: 'ssr',
31
40
  setup(global, { webgpuNode = {} }) {
32
41
  const shims = { ...globals, ...domShims() };
33
- const added = Object.keys(shims).filter((key) => !(key in global));
34
- for (const key of added)
35
- global[key] = shims[key];
36
- const navigator = (global.navigator ??= {});
37
- Object.defineProperty(navigator, 'gpu', { value: create(webgpuNode.dawnOptions ?? []), configurable: true });
42
+ const modified = [];
43
+ try {
44
+ for (const key of Object.keys(shims)) {
45
+ if (key in global)
46
+ continue;
47
+ modified.push({ target: global, key, descriptor: Object.getOwnPropertyDescriptor(global, key) });
48
+ global[key] = shims[key];
49
+ }
50
+ if (global.navigator == null) {
51
+ modified.push({
52
+ target: global,
53
+ key: 'navigator',
54
+ descriptor: Object.getOwnPropertyDescriptor(global, 'navigator'),
55
+ });
56
+ global.navigator = {};
57
+ }
58
+ const navigator = global.navigator;
59
+ modified.push({ target: navigator, key: 'gpu', descriptor: Object.getOwnPropertyDescriptor(navigator, 'gpu') });
60
+ Object.defineProperty(navigator, 'gpu', {
61
+ value: create(webgpuNode.dawnOptions ?? []),
62
+ writable: false,
63
+ enumerable: false,
64
+ configurable: true,
65
+ });
66
+ }
67
+ catch (error) {
68
+ restore(modified);
69
+ throw error;
70
+ }
38
71
  return {
39
- teardown(g) {
40
- delete navigator.gpu;
41
- for (const key of added)
42
- delete g[key];
72
+ teardown(_global) {
73
+ restore(modified);
43
74
  },
44
75
  };
45
76
  },
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAkB,MAAM,aAAa,CAAC;AAQlG,oHAAoH;AACpH,MAAM,qBAAqB,GAAG,CAAC,QAAgC,EAAkB,EAAE,CACjF,UAAU,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;AAE5D,MAAM,QAAQ,GAAG,GAA4B,EAAE;IAC7C,MAAM,MAAM,GAAG;QACb,gBAAgB,EAAE,CAAC;QACnB,qBAAqB;QACrB,oBAAoB,EAAE,YAAY;QAClC,gBAAgB,KAAI,CAAC;QACrB,mBAAmB,KAAI,CAAC;KACzB,CAAC;IACF,OAAO;QACL,iBAAiB,EAAE,cAAc;QACjC,QAAQ,EAAE;YACR,aAAa,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9E,eAAe,EAAE,CAAC,GAAW,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7F,gBAAgB,KAAI,CAAC;YACrB,mBAAmB,KAAI,CAAC;SACzB;QACD,MAAM;QACN,IAAI,EAAE,MAAM;QACZ,qBAAqB;QACrB,oBAAoB,EAAE,YAAY;KACnC,CAAC;AACJ,CAAC,CAAC;AAEF,eAA4B;IAC1B,IAAI,EAAE,aAAa;IACnB,eAAe,EAAE,KAAK;IACtB,KAAK,CAAC,MAA+B,EAAE,EAAE,UAAU,GAAG,EAAE,EAAsC;QAC5F,MAAM,KAAK,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,QAAQ,EAAE,EAA6B,CAAC;QACvE,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC;QACnE,KAAK,MAAM,GAAG,IAAI,KAAK;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QAClD,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,SAAS,KAAK,EAAE,CAAkB,CAAC;QAC7D,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,WAAW,IAAI,EAAE,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7G,OAAO;YACL,QAAQ,CAAC,CAA0B;gBACjC,OAAO,SAAS,CAAC,GAAG,CAAC;gBACrB,KAAK,MAAM,GAAG,IAAI,KAAK;oBAAE,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC;YACzC,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/legacy.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /** WebGPU declarations for TypeScript versions whose DOM library predates the WebGPU API. */
2
+ /// <reference types="@webgpu/types" />
3
+
4
+ export { default } from './dist/index.js';
5
+ export * from './dist/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-environment-webgpu-node",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Vitest environment that provides real headless WebGPU (Dawn via the webgpu package) in Node",
5
5
  "keywords": [
6
6
  "dawn",
@@ -22,10 +22,13 @@
22
22
  "directory": "packages/vitest-environment-webgpu-node"
23
23
  },
24
24
  "files": [
25
+ "legacy.d.ts",
25
26
  "dist/**/*.js",
26
27
  "dist/**/*.js.map",
27
28
  "dist/**/*.d.ts",
28
29
  "dist/**/*.d.ts.map",
30
+ "src/**/*.ts",
31
+ "!src/**/*.test.ts",
29
32
  "CHANGELOG.md"
30
33
  ],
31
34
  "type": "module",
@@ -33,6 +36,7 @@
33
36
  "types": "./dist/index.d.ts",
34
37
  "exports": {
35
38
  ".": {
39
+ "types@<6.0": "./legacy.d.ts",
36
40
  "types": "./dist/index.d.ts",
37
41
  "import": "./dist/index.js"
38
42
  }
package/src/canvas.ts ADDED
@@ -0,0 +1,153 @@
1
+ /** RGBA8 pixels, top row first. Feed it to `vitest-screenshot`'s `toMatchScreenshot`. */
2
+ export type RgbaImage = { width: number; height: number; data: Uint8Array };
3
+
4
+ type DefaultCanvasElement = typeof globalThis extends {
5
+ HTMLCanvasElement: { prototype: infer TElement };
6
+ }
7
+ ? TElement
8
+ : HeadlessCanvas;
9
+
10
+ const COPY_SRC = 0x01;
11
+ const RENDER_ATTACHMENT = 0x10;
12
+ const MAP_READ = 0x01;
13
+ const COPY_DST = 0x08;
14
+ const READABLE_FORMATS: ReadonlySet<GPUTextureFormat> = new Set([
15
+ 'rgba8unorm',
16
+ 'rgba8unorm-srgb',
17
+ 'bgra8unorm',
18
+ 'bgra8unorm-srgb',
19
+ ]);
20
+
21
+ /** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
22
+ export class HeadlessCanvasContext {
23
+ #config: GPUCanvasConfiguration | undefined;
24
+ #texture: GPUTexture | undefined;
25
+
26
+ constructor(readonly canvas: HeadlessCanvas) {}
27
+
28
+ configure(config: GPUCanvasConfiguration): void {
29
+ this.#config = config;
30
+ this.#drop();
31
+ }
32
+
33
+ unconfigure(): void {
34
+ this.#config = undefined;
35
+ this.#drop();
36
+ }
37
+
38
+ getConfiguration(): GPUCanvasConfiguration | null {
39
+ return this.#config ?? null;
40
+ }
41
+
42
+ getCurrentTexture(): GPUTexture {
43
+ const config = this.#config;
44
+ if (!config) throw new Error('getCurrentTexture() called before configure()');
45
+ const { width, height } = this.canvas;
46
+ if (this.#texture && (this.#texture.width !== width || this.#texture.height !== height)) this.#drop();
47
+ this.#texture ??= config.device.createTexture({
48
+ size: [width, height],
49
+ format: config.format,
50
+ usage: (config.usage ?? RENDER_ATTACHMENT) | RENDER_ATTACHMENT | COPY_SRC,
51
+ viewFormats: config.viewFormats,
52
+ });
53
+ return this.#texture;
54
+ }
55
+
56
+ /** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
57
+ async readPixels(): Promise<RgbaImage> {
58
+ const config = this.#config;
59
+ if (!config) throw new Error('readPixels() called before configure()');
60
+ const { device, format } = config;
61
+ if (!READABLE_FORMATS.has(format)) {
62
+ throw new Error(
63
+ `readPixels() does not support canvas format "${format}"; supported formats are rgba8unorm, rgba8unorm-srgb, bgra8unorm, and bgra8unorm-srgb`,
64
+ );
65
+ }
66
+ const texture = this.getCurrentTexture();
67
+ const { width, height } = texture;
68
+ const bytesPerRow = Math.ceil((width * 4) / 256) * 256;
69
+ const buffer = device.createBuffer({ size: bytesPerRow * height, usage: MAP_READ | COPY_DST });
70
+ let mapped = false;
71
+ try {
72
+ const encoder = device.createCommandEncoder();
73
+ encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, [width, height]);
74
+ device.queue.submit([encoder.finish()]);
75
+ await buffer.mapAsync(MAP_READ);
76
+ mapped = true;
77
+ const padded = new Uint8Array(buffer.getMappedRange());
78
+ const data = new Uint8Array(width * height * 4);
79
+ for (let y = 0; y < height; y++)
80
+ data.set(padded.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
81
+ if (format.startsWith('bgra')) {
82
+ for (let i = 0; i < data.length; i += 4) [data[i], data[i + 2]] = [data[i + 2]!, data[i]!];
83
+ }
84
+ return { width, height, data };
85
+ } finally {
86
+ try {
87
+ if (mapped) buffer.unmap();
88
+ } finally {
89
+ buffer.destroy();
90
+ }
91
+ }
92
+ }
93
+
94
+ #drop(): void {
95
+ this.#texture?.destroy();
96
+ this.#texture = undefined;
97
+ }
98
+ }
99
+
100
+ /** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */
101
+ export class HeadlessCanvas {
102
+ readonly #context = new HeadlessCanvasContext(this);
103
+ readonly style: Record<string, string> = {};
104
+ readonly #attributes = new Map<string, string>();
105
+
106
+ constructor(
107
+ public width = 300,
108
+ public height = 150,
109
+ ) {}
110
+
111
+ get clientWidth(): number {
112
+ return this.width;
113
+ }
114
+
115
+ get clientHeight(): number {
116
+ return this.height;
117
+ }
118
+
119
+ getContext(id: 'webgpu'): HeadlessCanvasContext;
120
+ getContext(id: string): HeadlessCanvasContext | null;
121
+ getContext(id: string): HeadlessCanvasContext | null {
122
+ return id === 'webgpu' ? this.#context : null;
123
+ }
124
+
125
+ readPixels(): Promise<RgbaImage> {
126
+ return this.#context.readPixels();
127
+ }
128
+
129
+ /** This same object, cast to the element type a rendering library expects. */
130
+ asElement<TElement = DefaultCanvasElement>(): TElement {
131
+ return this as unknown as TElement;
132
+ }
133
+
134
+ getBoundingClientRect(): { x: number; y: number; width: number; height: number; top: number; left: number } {
135
+ return { x: 0, y: 0, top: 0, left: 0, width: this.width, height: this.height };
136
+ }
137
+
138
+ setAttribute(name: string, value: string): void {
139
+ this.#attributes.set(name, value);
140
+ }
141
+
142
+ getAttribute(name: string): string | null {
143
+ return this.#attributes.get(name) ?? null;
144
+ }
145
+
146
+ addEventListener(): void {}
147
+ removeEventListener(): void {}
148
+ dispatchEvent(): boolean {
149
+ return true;
150
+ }
151
+ }
152
+
153
+ export const createCanvas = (width: number, height: number): HeadlessCanvas => new HeadlessCanvas(width, height);
package/src/index.ts ADDED
@@ -0,0 +1,88 @@
1
+ import type { Environment } from 'vitest/environments';
2
+ import { create, globals } from 'webgpu';
3
+ import { HeadlessCanvas } from './canvas.js';
4
+
5
+ export { createCanvas, HeadlessCanvas, HeadlessCanvasContext, type RgbaImage } from './canvas.js';
6
+
7
+ /** Set under `test.environmentOptions.webgpuNode` in your Vitest config. */
8
+ export type WebgpuNodeOptions = {
9
+ /** Dawn options, e.g. `['backend=vulkan', 'enable-dawn-features=allow_unsafe_apis']`. */
10
+ dawnOptions?: string[];
11
+ };
12
+
13
+ /** Just enough DOM for libraries that make their own canvas and drive a frame loop. Only installed when missing. */
14
+ const requestAnimationFrame = (callback: (time: number) => void): NodeJS.Timeout =>
15
+ setTimeout(() => callback(performance.now()), 16).unref();
16
+
17
+ const domShims = (): Record<string, unknown> => {
18
+ const window = {
19
+ devicePixelRatio: 1,
20
+ requestAnimationFrame,
21
+ cancelAnimationFrame: clearTimeout,
22
+ addEventListener() {},
23
+ removeEventListener() {},
24
+ };
25
+ return {
26
+ HTMLCanvasElement: HeadlessCanvas,
27
+ document: {
28
+ createElement: (tag: string) => (tag === 'canvas' ? new HeadlessCanvas() : {}),
29
+ createElementNS: (_ns: string, tag: string) => (tag === 'canvas' ? new HeadlessCanvas() : {}),
30
+ addEventListener() {},
31
+ removeEventListener() {},
32
+ },
33
+ window,
34
+ self: window,
35
+ requestAnimationFrame,
36
+ cancelAnimationFrame: clearTimeout,
37
+ };
38
+ };
39
+
40
+ type SavedProperty = { target: object; key: PropertyKey; descriptor?: PropertyDescriptor };
41
+
42
+ const restore = (properties: SavedProperty[]): void => {
43
+ for (const { target, key, descriptor } of properties.toReversed()) {
44
+ if (descriptor) Object.defineProperty(target, key, descriptor);
45
+ else Reflect.deleteProperty(target, key);
46
+ }
47
+ };
48
+
49
+ export default {
50
+ name: 'webgpu-node',
51
+ transformMode: 'ssr',
52
+ viteEnvironment: 'ssr',
53
+ setup(global: Record<string, unknown>, { webgpuNode = {} }: { webgpuNode?: WebgpuNodeOptions }) {
54
+ const shims = { ...globals, ...domShims() } as Record<string, unknown>;
55
+ const modified: SavedProperty[] = [];
56
+ try {
57
+ for (const key of Object.keys(shims)) {
58
+ if (key in global) continue;
59
+ modified.push({ target: global, key, descriptor: Object.getOwnPropertyDescriptor(global, key) });
60
+ global[key] = shims[key];
61
+ }
62
+ if (global.navigator == null) {
63
+ modified.push({
64
+ target: global,
65
+ key: 'navigator',
66
+ descriptor: Object.getOwnPropertyDescriptor(global, 'navigator'),
67
+ });
68
+ global.navigator = {};
69
+ }
70
+ const navigator = global.navigator as object;
71
+ modified.push({ target: navigator, key: 'gpu', descriptor: Object.getOwnPropertyDescriptor(navigator, 'gpu') });
72
+ Object.defineProperty(navigator, 'gpu', {
73
+ value: create(webgpuNode.dawnOptions ?? []),
74
+ writable: false,
75
+ enumerable: false,
76
+ configurable: true,
77
+ });
78
+ } catch (error) {
79
+ restore(modified);
80
+ throw error;
81
+ }
82
+ return {
83
+ teardown(_global?: Record<string, unknown>) {
84
+ restore(modified);
85
+ },
86
+ };
87
+ },
88
+ } satisfies Environment;