Skip to content

Conversation

@vinnymac
Copy link

Summary

In a local development environment I am working in, I experience an issue where one of the developer dependencies replaces the global URL. I am not sure who is specifically to blame there, but I did find that just changing the environment-node script to stop using URL allows me to use dropflow without any issues.

I don't believe this will cause any problems, so I figured I'd share my solution. If you'd rather not include this change, I can always just keep this as an internal patch on top of dropflow in my project.

The stack trace for the error looks like:

⨯ TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string or an instance of Buffer or URL. Received an instance of URL
    at Object.openSync (node:fs:563:5)
    at Object.readFileSync (node:fs:446:35)
    at Object.value (webpack-internal:///(rsc)/../../node_modules/dropflow/dist/src/api-wasm-locator-node.js:10:53)

Let me know your thoughts 🙇🏼

@chearon
Copy link
Owner

chearon commented Mar 27, 2025

Hmm seems harmless enough, although it would be good to know what's doing it so we can document it in a comment. Can you grep node_modules for global.URL/function URL/class URL/etc?

Also, what about changing this to simply import {URL} from 'node:url'; at the top?

@vinnymac
Copy link
Author

I can try to do more research. I know jsdom replaces URL, but I haven’t found evidence of it in use (yet).

Also, what about changing this to simply import {URL} from 'node:url'; at the top?

Funny you should mention that. That’s the very first thing I tried. It did not work, however when I did, import {URL as B} from 'node:url'; and used B instead, it worked just fine.

@vinnymac
Copy link
Author

vinnymac commented Mar 27, 2025

I did some more searching and I believe this issue is related to the use of NextJS and Webpack. There are some past discussions on the topic here: vercel/next.js#55523

EDIT: An interesting detail from those findings is that they said the simpler fixes didn't work in production, as it looks like it does some naive static analysis to extract and package the correct files when building. With that in mind, I think a solution like the one I have here might work, although they mentioned in the blog post at the end that they had to use process.cwd() instead of import.meta.url. Not sure if that is a deal breaker for you in this project, but thought it was worth mentioning.

@chearon
Copy link
Owner

chearon commented Mar 28, 2025

Next.js is bundling dropflow for node and modifying the URL so it points to the bundled location - is that correct? I wish we could just wrap the new URL in fileURLToPath, but I think that would have the same problem. import.meta.url must be a real URL since you're passing it to fileURLToPath.

I'm cool with this change, just needs a comment so I don't forget!

I also don't think you'd have the same problem in later versions of Node. I tried executing readFileSync with a non-URL instance in node 23 and it worked (below). Node 18 didn't work though.

> fs.readFileSync( {
...   href: 'file:///Users/caleb/Desktop/Screenshot%202025-03-27%20at%202.50.46%E2%80%AFPM.png',
...   origin: 'null',
...   protocol: 'file:',
...   username: '',
...   password: '',
...   host: '',
...   hostname: '',
...   port: '',
...   pathname: '/Users/caleb/Desktop/Screenshot%202025-03-27%20at%202.50.46%E2%80%AFPM.png',
...   search: '',
... searchParams: {},
... hash: ''
... })
<Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 02 cf 00 00 00 3c 08 06 00 00 00 99 eb 57 29 00 00 01 5e 69 43 43 50 49 43 43 20 50 72 6f 66 69 ... 12493 more bytes>

@vinnymac
Copy link
Author

vinnymac commented Mar 28, 2025

I am now confident this PR should not be merged as-is 😅

It took me some massaging, but eventually I jumped through all the hoops to get dropflow functioning on Vercel. Most of these issues I attribute to Vercel's platform and NextJS, and not dropflow. Although perhaps one thing could've been made clearer here.

To summarize the changes I had to make:

  • Replace canvas with @napi-rs/canvas so that I could fit dropflow within the tiny 50MB payload limit
  • Update my NextJS config to set @napi-rs/canvas as an external dependency, but also set canvas as an external dependency because there are references to canvas within the dropflow project that somehow make it try to include it as a dependency even though it's not an actual dependency of either dropflow or my project.
  • Patch dropflow to support loading fonts for both canvas and @napi-rs/canvas
  • Patch the node environment script in dropflow to use process.cwd() as the blog post described, and apparently I described a similar issue here many months ago that I forgot about 😓
  • Use the experimental.outputFileTracingIncludes Next config option so that @vercel/nft actually includes the dropflow.wasm files in the serverless functions.

After all of that, I was successfully able to use dropflow in Vercel. Here is the patch I made to dropflow for context:

Expand to see changes

Dropflow Changes

diff --git a/dist/src/api-wasm-locator-node.js b/dist/src/api-wasm-locator-node.js
index 2212d683cc57c3282034e76f36524c840ec438b9..40ce2944fb8d749e2e736753eba1f2f3b8c4de6a 100644
--- a/dist/src/api-wasm-locator-node.js
+++ b/dist/src/api-wasm-locator-node.js
@@ -1,7 +1,14 @@
+import process from 'node:process';
 import fs from 'node:fs';
+import path from 'node:path';
 export const locatorFunction = {
     value: async () => {
-        return fs.readFileSync(new URL('../dropflow.wasm', import.meta.url));
+        return fs.readFileSync(
+            path.join(
+                process.cwd(),
+                '../../node_modules/dropflow/dist/dropflow.wasm'
+            )
+        );
     }
 };
 export default function setBundleLocator(fn) {
diff --git a/dist/src/backend-node.js b/dist/src/backend-node.js
index c58bf7d5ea715d98b21162375fc2fd8e1fe92b2c..c4d6d24782fa64645ee2cd73aa5ccc4aa3186190 100644
--- a/dist/src/backend-node.js
+++ b/dist/src/backend-node.js
@@ -1,15 +1,23 @@
-import { fileURLToPath } from 'url';
+import { fileURLToPath } from "node:url";
 const alreadyRegistered = new Set();
+
 try {
-    var canvas = await import('canvas');
-}
-catch (e) {
-}
+  var canvas = await import("canvas");
+} catch (e) {}
+try {
+  var napiRsCanvas = await import("@napi-rs/canvas");
+} catch (e) {}
+
 export function registerPaintFont(match, buffer, url) {
-    const filename = fileURLToPath(url);
-    if (canvas?.registerFont && !alreadyRegistered.has(filename)) {
-        const descriptor = match.toCssDescriptor();
-        canvas.registerFont(filename, descriptor);
-        alreadyRegistered.add(filename);
+  const filename = fileURLToPath(url);
+  if (!alreadyRegistered.has(filename)) {
+    const descriptor = match.toCssDescriptor();
+    if (canvas?.registerFont) {
+      canvas.registerFont(filename, descriptor);
+    }
+    if (napiRsCanvas?.GlobalFonts) {
+      napiRsCanvas.GlobalFonts.registerFromPath(filename, descriptor.family);
     }
+    alreadyRegistered.add(filename);
+  }
 }

I think the Napi changes should be relatively safe to make, although I am left wondering if there is a much better way to solve these problems. The api-wasm-locator-node changes are especially problematic, as they are relative to the monorepo I am working on, and wouldn't be safe to merge into this project.

I tried executing readFileSync with a non-URL instance in node 23 and it worked (below). Node 18 didn't work though.

Yea, my environments are all running on NodeJS 22, and one of the first things I tested was NodeJS 20, which gave me the same error in the original post above. It would be great if NodeJS 23 made these problems completely disappear.

One thing I have been considering to solve this is to do something like this:

export const locatorFunction = {
    value: async () => {
        if (process.env.NEXT_RUNTIME) {
          return fs.readFileSync(
              path.join(
                  process.cwd(),
                  '../../node_modules/dropflow/dist/dropflow.wasm'
              )
          );
        }
        
        // existing code goes here
    }
};

But I think we would still need a way to find the root node modules folder to avoid the monorepo issue of ../../ here.

EDIT: I will experiment further with https://nextjs.org/docs/pages/api-reference/config/next-config-js/output and see if I can get this working without project specific modifications to dropflow. If I can't get import.meta.url working, I'll probably use the NEXT_RUNTIME env with process.cwd.

@chearon
Copy link
Owner

chearon commented Mar 29, 2025

I'm actually working on @napi-rs/canvas support today. I made the environment files you changed in this PR for that reason, but I kept getting blocked by other work that needed to happen first.

The concept of "environments" is to have flexible support for different canvases and ways to retrieve files. So when I'm done, you'll be able to write your own locator that uses process.cwd() and provide a hook to register fonts with @napi-rs/canvas using the public API.

I guess you would still need to make canvas an external dependency since dropflow does import it to provide those hooks by default for convenience.

I'll post an update when I'm done!

@chearon chearon force-pushed the master branch 2 times, most recently from f33e526 to 7bf7610 Compare March 30, 2025 20:10
@vinnymac
Copy link
Author

vinnymac commented Apr 1, 2025

That's awesome to hear, the changes to webpack for canvas and napi were minimal, so that's a fair trade off.

The API change you're proposing is exactly what I was hoping for, looking forward to being able to hook into dropflow 🎉

For now I am unblocked using the solution I shared above, haven't found a way to workaround Vercel's limitations yet regarding process.cwd(). However the outputTracingRoot configuration did allow me to do away with the relative path for the root of the monorepo.

@vinnymac vinnymac changed the title Fix environments with fake URL Fix Dropflow for Vercel serverless environments Apr 1, 2025
@chearon
Copy link
Owner

chearon commented Apr 11, 2025

@vinnymac I have documentation for environments now, which includes some samples for @napi-rs/canvas. I'm releasing 0.5.0 in a sec. Can you let me know if it works for you so we can call it resolved? I appreciate the report!

@vinnymac
Copy link
Author

Thanks for letting me know, I will try to integrate this update into my work and see how it goes. Although at a glance reviewing the documentation it looks like exactly what I would need 👍🏼

@vinnymac
Copy link
Author

vinnymac commented Apr 11, 2025

It appears to work well for my needs. Only thing I am a little confused about is when I await fonts.ready it never appears to be ready and hangs indefinitely. But if I just await the load methods it works fine:

This works:

flow.fonts.add(font1).add(font2);
await Promise.all([font1.load(), font2.load()]);

This does not:

flow.fonts.add(font1).add(font2);
font1.load(); // adding await here and to the line below also does not help
font2.load();
await flow.fonts.ready;

But other than that, this worked without a hitch 👍🏼

EDIT: I think the issue I described above may just be due to the fact that I'm using an ArrayBuffer, which is loaded immediately.

@chearon
Copy link
Owner

chearon commented Apr 12, 2025

Hmm are there any other steps to it? I tried writing a test for that but it passes, and so does every combination I can think of:

    it('resolves FontFaceSet.ready when buffers are sync loaded', async function () {
      const f1 = new FontFace('f1', arrayBuffer());
      const f2 = new FontFace('f2', arrayBuffer());
      f1.load();
      f2.load();
      await flow.fonts.ready;
    });

But yeah, the .load()s there are unnecessary, and so is the await.

@vinnymac
Copy link
Author

Not quite sure why that happens in my case. But removing load and ready work just fine. I'll go ahead and close this. Appreciate your help, as the solution is now very robust and flexible. The only modifications I have to make to get dropflow working in Vercel are to externalize canvas libs and configure output tracing for the wasm file, which is much cleaner ✨

@vinnymac vinnymac closed this Apr 12, 2025
@vinnymac vinnymac deleted the vt/fix-url branch April 12, 2025 16:11
@chearon
Copy link
Owner

chearon commented Apr 12, 2025

Great! Let me know if you run into anything else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants