Closed jlarmstrongiv closed 8 months ago
Here’s a reproducible example astro-vips.zip
I tried using vite with another popular wasm library ( https://www.npmjs.com/package/@sqlite.org/sqlite-wasm ) and it did build and bundle correctly with
optimizeDeps: {
exclude: ["@sqlite.org/sqlite-wasm"],
},
Anyway, if you have suggestions on how to get wasm-vips
working, please let me know!
EDIT: I’m going to try again tomorrow inside a plain astro component and see if that works any better
This is due to https://github.com/vitejs/vite/issues/7015. Fortunately, there's a workaround for this that seems to work:
However, this would mean that the locateFile
handler on the incoming module would have to be overridden to provide support for nested workers, for example when using wasm-vips in a web worker (see: https://github.com/kleisauke/wasm-vips/issues/15#issuecomment-1110770951).
Thank you @kleisauke! Unfortunately, I’ve tried disabling the nested plugins, and it broke the dev server. It builds just fine though.
Can you describe:
cannot override the locateFile handler on the incoming module
I’m getting that Nested workers are disabled
error.
I am using vips in a worker—is that allowed, or does vips spawn it’s own web worker?
Or is the way I’m importing the workers the problem?
// error
const vipsWorker = new Worker(
new URL("../workers/vips.worker", import.meta.url),
{ type: "module", name: "comlink.worker" },
);
// error
import vipsWorkerUrl from "../workers/vips.worker?worker&url";
const vipsWorker = new Worker(
vipsWorkerUrl,
{ type: "module", name: "comlink.worker" },
);
import type { PluginOption } from "vite";
// https://github.com/vitejs/vite/blob/ec7ee22cf15bed05a6c55693ecbac27cfd615118/packages/vite/src/node/plugins/workerImportMetaUrl.ts#L127-L128
const workerImportMetaUrlRE =
/\bnew\s+(?:Worker|SharedWorker)\s*\(\s*(new\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*\))/g;
// https://github.com/vitejs/vite/issues/7015
// https://github.com/vitejs/vite/issues/14499#issuecomment-1740267849
export const disableNestedWorkers: PluginOption = {
enforce: "pre",
name: "Disable nested workers",
transform(code) {
if (
code.includes("new Worker") &&
code.includes("new URL") &&
code.includes("import.meta.url")
) {
return {
code: code.replaceAll(
workerImportMetaUrlRE,
`((() => { throw new Error('Nested workers are disabled') })()`,
),
// (Disable nested workers plugin) Sourcemap is likely to be incorrect: a plugin (Disable nested workers) was used to transform files, but didn't generate a sourcemap for the transformation. Consult the plugin documentation for help
// - https://github.com/withastro/astro/pull/6817
// - https://github.com/withastro/astro/pull/6817/files#diff-2daffa3917247b6251d31ca5525312790f033402a8d48d6616ec7dcf37b78ef6R86
map: { mappings: "" },
};
}
},
};
Apologies for the confusion; I mixed it up. You'll need to override the locateFile
handler to support nested workers, I clarified my previous post.
I am using vips in a worker—is that allowed, or does vips spawn it’s own web worker?
Using wasm-vips in a worker is allowed. Given that Emscripten's pthreads integration also uses web workers, it can sometimes be tricky to set this up with different module bundlers, as you've noticed.
So, for example, if you initiate wasm-vips in a web worker and not overriding the locateFile
handler, this happens:
graph TD
A["vips-es6.js #40;bundled#41;"] -- "new Worker#40;new URL#40;'vips-es6.worker.js', import.meta.url#41;, {type: 'module'}#41;" --> vips-es6.worker.js
vips-es6.worker.js -- "import#40;'./vips-es6.js'#41; " --> B["vips-es6.js #40;pre-processed#41;"]
B["vips-es6.js #40;pre-processed#41;"] --> C["throw new Error#40;'Nested workers are disabled'#41;"]
But by overriding that handler, this happens:
graph TD
A["vips-es6.js #40;bundled#41;"] -- "new Worker#40;locateFile#40;'vips-es6.worker.js'#41;, {type: 'module'}#41;" --> vips-es6.worker.js
vips-es6.worker.js -- "import#40;'./vips-es6.js'#41; " --> B["vips-es6.js #40;pre-processed#41;"]
B["vips-es6.js #40;pre-processed#41;"] -- "new Worker#40;locateFile#40;'vips-es6.worker.js'#41;, {type: 'module'}#41;" --> vips-es6.worker.js
I’m really sorry @kleisauke but do you have a full example using vips in a worker? I feel like I’m missing something important. Am I supposed to have a vips.js
or vips-es6.js
file? Do I import that from node_modules
? What goes in it?
When using importScripts
from the example, I get the error:
Uncaught TypeError: Failed to execute 'importScripts' on 'WorkerGlobalScope': Module scripts don't support importScripts().
at vips.worker.ts:15:6
But, when I switch to classic
, I get the error:
Uncaught SyntaxError: Cannot use import statement outside a module (at vips.worker.ts?type=module&worker_file:1:1)
And I need the workers to support ES Modules, so I feel like solving the first error is the way to go.
I also tried await import("./vips.js");
, but get the error:
[vite] Internal server error: Failed to resolve import "./vips.js" from "../../core/src/astro/workers/vips.worker.ts?type=module&worker_file". Does the file exist?
Please note that I cannot use top-level await with comlink, so I put the await import("./vips.js");
inside the getVips
function.
My worker file looks like:
/// <reference lib="webworker" />
import { expose } from "comlink";
import onetime from "onetime";
// import Vips from "wasm-vips";
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
declare type VipsConstructor = typeof import("wasm-vips");
if (!(typeof importScripts === "function")) {
// determine environment https://stackoverflow.com/a/23619712
throw new TypeError(`[example.worker]: ENVIRONMENT_IS_WORKER is false`);
}
importScripts("./vips.js");
// libvips
// - https://www.npmjs.com/package/wasm-vips
// - https://wasm-vips.kleisauke.nl/playground/
const getVips = onetime(
async () =>
// @ts-expect-error Cannot find name 'Vips'.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
Vips({
// @ts-expect-error Parameter 'fileName' implicitly has an 'any' type.
locateFile: (fileName, scriptDirectory) =>
`${scriptDirectory}${fileName}`,
mainScriptUrlOrBlob: "./vips.js",
}) as Promise<VipsConstructor>,
);
async function getImageUrl(): Promise<string> {
const vips = await getVips();
// #C83658 as CIELAB triple
const start = [46.479, 58.976, 15.052];
// #D8E74F as CIELAB triple
const stop = [88.12, -23.952, 69.178];
// Makes a lut which is a smooth gradient from start colour to stop colour,
// with start and stop in CIELAB
// let lut = vips.Image.identity() / 255;
let lut = vips.Image.identity().divide(255);
// lut = lut * stop + (1 - lut) * start;
lut = lut.multiply(stop).add(lut.multiply(-1).add(1).multiply(start));
lut = lut.colourspace(vips.Interpretation.srgb /* 'srgb' */, {
source_space: vips.Interpretation.lab, // 'lab'
});
const buffer = await fetch("/assets/images/owl.jpg").then(async (resp) =>
resp.arrayBuffer(),
);
let im = vips.Image.newFromBuffer(buffer);
if (im.hasAlpha()) {
// Separate alpha channel
const withoutAlpha = im.extractBand(0, { n: im.bands - 1 });
const alpha = im.extractBand(im.bands - 1);
im = withoutAlpha
.colourspace(vips.Interpretation.b_w /* 'b-w' */)
.maplut(lut)
.bandjoin(alpha);
} else {
im = im.colourspace(vips.Interpretation.b_w /* 'b-w' */).maplut(lut);
}
// Finally, write the result to a blob
const t0 = performance.now();
const outBuffer = im.writeToBuffer(".jpg");
const t1 = performance.now();
console.log(`Call to writeToBuffer took ${t1 - t0} milliseconds.`);
const blob = new Blob([outBuffer], { type: "image/jpeg" });
const blobURL = URL.createObjectURL(blob);
return blobURL;
}
const workerInterface = {
getImageUrl,
};
expose(workerInterface);
export type ExposedInterface = typeof workerInterface;
And I’m importing it in this Astro file:
---
// Usage
// <VipsWorker transition:persist />
---
<script>
import { wrap } from "comlink";
import type { ExposedInterface } from "../workers/vips.worker";
import vipsWorkerUrl from "../workers/vips.worker?worker&url";
// use newer syntax https://v3.vitejs.dev/guide/features.html#import-with-constructors
const vipsWorker = new Worker(
// new URL("../workers/vips.worker", import.meta.url),
vipsWorkerUrl,
{ type: "module", name: "comlink.worker" },
);
const vips = wrap<ExposedInterface>(vipsWorker);
// @ts-expect-error Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
globalThis.vips = vips;
console.log("vipsWorker", await vips.getImageUrl());
</script>
do you have a full example using vips in a worker?
Here's a full example of using wasm-vips in a ES6 web worker with vanilla JavaScript: https://github.com/kleisauke/wasm-vips/issues/15#issuecomment-1825631671.
I tried to fix your code example above, but it didn't work. I tried this patch:
and modified the package.json
to include:
"overrides": {
"astro": {
"vite": "^5.0.2"
}
}
(this ensures issue https://github.com/vitejs/vite/issues/13367 is not masking issue https://github.com/vitejs/vite/issues/7015)
This will hang indefinitely during astro build
, so it seems that the above mentioned workaround is not effective when wasm-vips was imported in a web worker.
There's not much I can do for this, please subscribe to https://github.com/vitejs/vite/issues/7015 for updates related to this.
Thank you for trying @kleisauke ! I really appreciate your help and recommendations trying to solve this bug. I’ve followed the issue and look forward to when I can try wasm-vips
. If you find any workarounds, please let me know and I’ll give them a try.
Thinking about this further, you might be able to patch vips-es6.js
in a similar way as the Vite plugin does. Here's a minimal working example:
astro-vips.zip
Hope this helps.
Thanks @kleisauke! That worked 🎉 I had to find and fix a few paths for the monorepo, but after studying your example with Vips
, cleanup
, and patch-package
, I was able to get it working 🎉 thank you so much for helping debug vite and providing those extremely helpful examples
Actually, @kleisauke, I spoke too soon. I am running into CORS
errors, even with workaroundCors: true,
Access to script at 'https://typefoundriesdirect.dev/assets/vips-es6-b7b0be19.js' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
I was able to get it to work with workaroundCors: true,
and mainScriptUrlOrBlob: "https://cdn.jsdelivr.net/npm/wasm-vips@0.0.7/lib/vips-es6.js",
, but I would love to be able to serve the file locally.
Is the cors error due to loading the worker as a blob, which causes the origin header to be set to null
which breaks cors?
I am running into
CORS
errors, even withworkaroundCors: true,
Unfortunately, workaroundCors: true
is no-op for ES6 modules, see: https://github.com/kleisauke/wasm-vips/issues/12#issuecomment-1099401544.
Is the cors error due to loading the worker as a blob, which causes the origin header to be set to
null
which breaks cors?
I think this is indeed the issue. It seems that vips-es6.worker.js
is being inlined since its size is smaller than 4 KiB. Fortunately, you could set build.assetsInlineLimit
to 0
to disable inlining altogether.
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -10,7 +10,8 @@ export default defineConfig({
},
vite: {
build: {
- target: 'esnext'
+ target: 'esnext',
+ assetsInlineLimit: 0
},
optimizeDeps: {
esbuildOptions: {
@kleisauke disabling all asset inlining has a pretty big performance impact. Is there another way with vite.worker.rollupOptions.external
or vite.build.rollupOptions.external
?
I’ve tried some of those rollup options, but I haven’t quite gotten it to work 🤔
I couldn't get it work with those Rollup options either. Hopefully sometime in the future this can be controlled with the ?no-inline
or ?inline=false
query suffixes.
The only two options I can think of to fix this are:
Access-Control-Allow-Origin: *
HTTP header on vips-es6.js
;vips-es6.js
, vips.wasm
and vips-es6.worker.js
in the public
directory.You’re right, the easiest solution is that mainScriptUrlOrBlob
as vips-es6.js
needs to be loaded from the public directory or a cdn
@jlarmstrongiv Which solution did you end up finding? I'd love to check it out.
I'm currently trying to get wasm-vips to run in a worker as well, and while https://github.com/kleisauke/wasm-vips/issues/15#issuecomment-1110770951 and https://github.com/kleisauke/wasm-vips/issues/58#issuecomment-1822629048 are really helpful for trying to understand the problem better, I have yet to get it working on my project (see https://github.com/swissspidy/media-experiments/pull/260)
Might be special because I'm using @shopify/web-worker to load code in a blob worker. I need to load wasm-vips in there or from a CDN (using jsdelivr right now) as I otherwise don't have control to add CORS header.
Anyway, having more examples of wasm-vips in a web worker would be really helpful to me.
Edit: I got it working now! The solution was to load https://cdn.jsdelivr.net/npm/wasm-vips@0.0.7/lib/vips.worker.js
into a blob and then in the locateFile
callback return a blob URL for it.
@swissspidy I tried @shopify/web-worker and it didn’t work for me, presumably because of its webpack and babel dependencies
This library contains three parts that must be used together:
- The public API of the package provided by @shopify/web-worker
- A babel plugin provided by @shopify/web-worker/babel that identifies uses of createWorkerFactory that need to be processed
- A webpack plugin provided by @shopify/web-worker/webpack that outputs the worker as a dedicated chunk
Did you get it working with a vite-based project?
I‘m not using Vite unfortunately, just React in this case.
Commit https://github.com/vitejs/vite/commit/4d1342ebe0969cbcfc9c6d7fc5347f85df07df7f introduces a new callback functionality to build.assetsInlineLimit
, enabling users to selectively opt-in or opt-out for inlining. This enhancement will be available in an upcoming version of Vite, alowing you to do this:
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -10,7 +10,9 @@ export default defineConfig({
},
vite: {
build: {
- target: 'esnext'
+ target: 'esnext',
+ assetsInlineLimit: (filePath) =>
+ filePath.endsWith('.worker.js') ? false : undefined,
},
optimizeDeps: {
esbuildOptions: {
I'll close this as "won't fix" for now, as there's not much I can do for this. Please subscribe to https://github.com/vitejs/vite/issues/7015 for updates related to this.
Issue https://github.com/vitejs/vite/issues/7015 is now fixed via PR https://github.com/vitejs/vite/pull/16103, which is included in Vite v5.1.6. This means that the previously mentioned workarounds are no longer needed. :tada:
Here's a minimal working example: astro-vips.zip
@kleisauke fantastic news 🎉 thank you so much for the update and example
@kleisauke it seems that the vite worker no longer works? It appears that vite is attempting to bundle @vite/client
with overlay.ts
, which fails to due the webworker not having a dom. Other workers like sqlite3
and resvg
work fine. Any idea why wasm-vips
would be breaking?
I tried the older workaround, but that doesn’t seem to work either. Did something change in vite?
I also tried the workaround in https://github.com/vitejs/vite/issues/9879, but those didn’t work either
@jlarmstrongiv This looks like a regression introduced in PR https://github.com/vitejs/vite/pull/15852, I recommend downgrading Vite to v5.1.7 for now.
$ npm install vite@5.1.7
Fixed in vite@5.2.8
Hey @jlarmstrongiv would you mind setting up an example repository using vite and wasm-vips ? I cannot get this to work.
@pepijnolivier the only thing missing from this example is upgrading all dependencies and adding this option to the config:
vite: {
optimizeDeps: {
exclude: ["wasm-vips"],
},
},
In dev mode
While dev mode starts to work with
This causes errors in build
I would love to use
wasm-vips
, I just need to figure out how to import and use it