skymethod / denoflare

Develop, test, and deploy Cloudflare Workers with Deno.
https://denoflare.dev
MIT License
692 stars 33 forks source link

Cannot use npm specifier #69

Open riderx opened 9 months ago

riderx commented 9 months ago

If i change one import in my codebase to the new syntax for npm specifier in Deno. Like below: CleanShot 2024-02-02 at 03 39 50@2x I get this error on the deploy step: CleanShot 2024-02-02 at 03 39 56@2x it seems it's not supported

johnspurlock-skymethod commented 9 months ago

Yep, Deno's npm: specifiers are not supported, as they are not supported by deno bundle - and we need to bundle into a single js for Cloudflare

jollytoad commented 7 months ago

As deno bundle is now deprecated, are there plans to switch to an alternative bundler, eg deno_emit, or esbuild?

johnspurlock-skymethod commented 7 months ago

Yes, deno_emit has never worked well, experimented in denoflare using --bundle backend=module

I imagine esbuild + esbuild_deno_loader will be the most full-featured solution.

I miss Deno.emit, and soon deno bundle : (

riderx commented 7 months ago

I have personally changed completely my stack. Now in deno I use import map so my import look like npm one. I did install only npm and deno compatible packages. And then used hono to have a server who work in both envs the same.

now the only difference is the root file who is different for cloudflare and deno.

That it, no more build step to transform anything.

i believe in the future with winterjs this will become more and more easy.

my code is open source so you see it here: https://github.com/Cap-go/capgo/blob/main/cloudflare_workers/index.ts

it’s deployed in supabase edge (deno) Cloudflare workers and Netlify (nodejs)

jollytoad commented 7 months ago

If it's any help I have a script that uses esbuild and esbuild-deno-loader (to build a service worker for the browser, but that's not so relevant), and it seems to work with both npm: and the new jsr: imports... https://github.com/jollytoad/home/blob/main/scripts/build.ts

(For context, I'm here because, I was curious to see if I could also deploy my site to Cloudflare too using denoflare, but after migrating to npm: (from esm.sh) and jsr:, it's seem not)

Do you think it'd be possible to have a bring-your-own-bundler feature in denoflare?

johnspurlock-skymethod commented 7 months ago

Denoflare supports Cloudflare wasm/text/binary imports with their non-standard syntax using some post-bundling transformations - these would be tricky to do in general, but sure an escape hatch where you could DIY would be interesting and allow for fast experimentation if those aren't used.

It would need to work with the cli, so maybe a pointer to a module that supports a simple bundle interface?

Granitosaurus commented 3 months ago

Is it possible to wrap some features with dynamic imports to build around this? Seems like the bundler sees node: or npm: anywhere in the code and panics prematurely. Now the code has to be patched to remove the references exclusively for this one use case unless I'm missing something?

johnspurlock-skymethod commented 3 months ago

I may have some time today to look into an esbuild-based bundler option. @Granitosaurus do you have a sample worker I could use as a validation test?

jollytoad commented 3 months ago

If it's any help I've just recently got my Deno Deploy site to build for Cloudflare Pages, I had to jump through a fair amount of hoops. It seems Cloudflare doesn't support import.meta.*, so I had to do some code transforms first and then bundle with esbuild. (esbuild doesn't have any option transpile import.meta.url/resolve either)

Transform stuff: https://github.com/jollytoad/home/blob/main/scripts/build_cloudflare.ts#L57 Bundle: https://github.com/jollytoad/home/blob/main/scripts/build_cloudflare.ts#L130

With this, I end up with a bundled _worker.js along with a folder of assets for deploying to Cloudflare Pages.

Granitosaurus commented 3 months ago

hey @johnspurlock-skymethod thanks for the reply. Here's the minimal example that got me stumped:

// mod.ts
export class MyClass {
  constructor() {
    console.log("MyClass constructor");
  }

  async selector() {
    // dynamically import npm: package, in real use it would throw an error if called through unsupported environment like cf
    const cheerio = await import("npm:cheerio");
    return cheerio.load("<h2 class='title'>Hello world</h2>")("h2").text();
  }
}

and when serving through denoflare's index.ts it immediately throws error that npm specifiers have not yet been implemented:

// index.ts
import { MyClass } from "./mod.ts"

export default {
    async fetch(request: Request, env: any) {
        try {
            // Nothing from the package is even used, just imported
            // const foo = new MyClass();
            return new Response("success")
        } catch (e) {
            return new Response(e.message)
        }
    },
}
Full Traceback ``` ❯ : denoflare serve hello-local Compiling https://raw.githubusercontent.com/skymethod/denoflare/v0.6.0/cli-webworker/worker.ts into worker contents... ✅ Granted write access to . Bundled https://raw.githubusercontent.com/skymethod/denoflare/v0.6.0/cli-webworker/worker.ts (process) in 1082ms runScript: index.ts TS0: ⚠ Warning: `deno bundle` is deprecated and will be removed in Deno 2.0. Use an alternative bundler like "deno_emit", "esbuild" or "rollup" instead. Check file:///home/dex/projects/try-denoflare/index.ts error: npm specifiers have not yet been implemented for this subcommand (https://github.com/denoland/deno/issues/15960). Found: npm:/cheerio@1.0.0-rc.12 at undefined: error: Uncaught (in promise) Error: bundle failed throw new Error('bundle failed'); ^ at bundle (https://raw.githubusercontent.com/skymethod/denoflare/v0.6.0/cli/bundle.ts:108:15) at eventLoopTick (ext:core/01_core.js:168:7) at async computeScriptContents (https://raw.githubusercontent.com/skymethod/denoflare/v0.6.0/cli/cli_serve.ts:122:45) at async runScript (https://raw.githubusercontent.com/skymethod/denoflare/v0.6.0/cli/cli_serve.ts:169:40) at async createLocalRequestServer (https://raw.githubusercontent.com/skymethod/denoflare/v0.6.0/cli/cli_serve.ts:177:13) at async Object.serve [as handler] (https://raw.githubusercontent.com/skymethod/denoflare/v0.6.0/cli/cli_serve.ts:194:32) at async CliCommand.routeSubcommand (https://raw.githubusercontent.com/skymethod/denoflare/v0.6.0/cli/cli_command.ts:104:13) at async https://raw.githubusercontent.com/skymethod/denoflare/v0.6.0/cli/cli.ts:41:5 ```

I guess this is Typescript being overly safe here and prematurely breaking but maybe there's a way to tell it to shut up about it?

My goal is to get my package running on cf worker even if some methods that require npm: dependencies are not available and the only way rn it seems is to build out a different version of the package with these methods replaced 🤔

johnspurlock-skymethod commented 3 months ago

@ Granitosaurus thanks - couple of notes on that sample.

  1. Deno has really good compile-time inspection even for dynamic imports these days, even static string templates! You can still fool it by doing string concat tho, so something like
    await import("npm:cheerio" + "");

    will prevent Deno from checking it.

This will still fail at runtime on cloudflare though (no npm:cheerio module)

  1. A library like cheerio works well today even without a new esbuild loader or dynamic imports. Change mod.ts to:
import cheerio from 'https://cdn.skypack.dev/cheerio@1.0.0-rc.12?dts';

export class MyClass {
    constructor() {
      console.log("MyClass constructor");
    }

    async selector() {
      // works in local denoflare serve and when pushed to cf with denoflare push
      return cheerio.load("<h2 class='title'>Hello world</h2>")("h2").text();
    }
  }

running on cloudflare here: https://cheerio.sw.workers.dev/

johnspurlock-skymethod commented 3 months ago

Ok introduced a proof of concept esbuild backend in https://github.com/skymethod/denoflare/commit/827d5b1faebf233f4edd434de7fec7d9f1086483

Install denoflare as of this commit or greater to try it out.

Can use it with a --bundle backend=esbuild option to either denoflare serve or denoflare push.

Imports the esbuild stuff dynamically right now so doesn't bloat denoflare if never used.

Defaults to the native loader (which requires --allow-run), can use the portable (wasm) loader with a --bundle loader=portable option to either denoflare serve or denoflare push.

Since neither esbuild nor the esbuild deno loader does type checking, this backend doesn't do any type checking at all yet, I guess we'll need to shell out to deno check to bring this to the same usefulness level as deno bundle

Granitosaurus commented 3 months ago

hey @johnspurlock-skymethod I've tested the new esbuild backend and it works great with my npm:cheerio dependency:

import * as cheerio from 'npm:cheerio@1.0.0-rc.12'

Thanks for clarifying everything!

johnspurlock-skymethod commented 3 months ago

Great, glad to hear it

Now the esbuild backend is type-checking (via deno check under the hood) by default as of https://github.com/skymethod/denoflare/commit/ea757ffb39ed87a282f22f7cdd88f5b17bc97b1d

Can control the check level using the same option as the other backends: --bundle check=(all|local|none)


Also added two other optional bundle options for specifying the versions of the esbuild module and esbuild-deno-loader used under the hood

--bundle loaderModule=(jsr-spec|url) (defaults to ^0.10.3)

This is the version of the esbuild-deno-loader to use, you can specify a version spec for the canonical module, or provide a full url to a fork etc

--bundle esbuildModule=(version|url) (defaults to 0.23.0)

This is the version of the esbuild module to use, you can specify a version for the deno/x repo, or provide a full url to a fork etc