denoland / deploy_feedback

For reporting issues with Deno Deploy
https://deno.com/deploy
74 stars 5 forks source link

"True" (non-statically-analyzable) dynamic imports #433

Open lionel-rowe opened 1 year ago

lionel-rowe commented 1 year ago

What problem are you trying to solve?

Separate tracking issue for non-statically-analyzable dynamic imports, as the fix for #1 Dynamic imports only solves statically-analyzable use cases.

Among other use cases, this would be useful for framework authors wishing to skip build steps entirely; without this feature, at a minimum, a file containing literal paths for routes/components has to be generated.

Example (currently possible in Deno, but not on Deno Deploy):

import { serve } from 'https://deno.land/std@0.190.0/http/server.ts'
import { resolve } from 'https://deno.land/std@0.190.0/path/mod.ts'
import html from 'https://deno.land/x/htm@0.2.1/mod.ts'

serve(async (req: Request) => {
    try {
        const { default: route } = await import(
            resolve('./routes', `.${new URL(req.url).pathname}.tsx`),
        )

        return html({ body: route({ req }) })
    } catch (e) {
        if (e.code === 'ERR_MODULE_NOT_FOUND') {
            return new Response(null, { status: 404 })
        } else throw e
    }
})

Describe the solution you'd like

Describe alternatives you've considered

A partial solution could involve only supporting js/json files, with transpilation not supported. However, my guess is that wouldn't support most use cases, as most developers using Deno primarily author in TypeScript.

Documentation, Adoption, Migration Strategy

No response

vfssantos commented 1 year ago

For anyone looking for a workaround to this issue in Deno Deploy, check out https://github.com/ayoreis/import.

With it, you can dynamically import non-statically-analyzable modules; import code from string and even a code string that imports another module dynamically inside it.

import { importString, importModule } from "https://deno.land/x/import/mod.ts";

let { default: renderer } = await importString(`
    const renderer = async ()=>{

      const { render } = await modules.importModule('https://deno.land/x/mustache_ts/mustache.ts');

      const template = '{{foo}}, {{bar}}!'
      const view = {
          foo: 'Hello',
          bar: 'World!'
      }
      const output = render(template, view)
      return output;
    };
    export default renderer;
  `,
    { modules: { importModule } },
)
  console.log(await renderer()) // expected: "Hello, World!"
lowlighter commented 1 year ago

Another workaround inspired by a nice trick someone posted on the discord

Assuming you want to achieve something similar to this:

const {mod} = await import(`./routes/${url.pathname}`)

You can make something that has a similar behavior to the --include flags from deno compile (i.e. include additional modules you know you may use later ahead of time) by doing this:

(async () => await import("./routes/a.ts"));
(async () => await import("./routes/b.ts"));
(async () => await import("./routes/c.ts"));

Basically these are async functions which are not called (so not executed meaning they are no-op) but the analyzer will still resolve these imports during deployment, making them loadable dynamically later on

If you want to automate a bit things, you can even generate these definitions using something like an expandGlob("**/*.ts") and run this as a build step or pre-commit

This alternative has the advantage of being somewhat "native" as it uses deno deploy own resolution/loading system

revgum commented 9 months ago

Thanks for the comment @lowlighter .. I wrote a simple shell script, run it as a step in Github Actions to find all of the files and append the await import lines to the code which is performing the dynamic imports. Deno Deploy static analysis sees the imports, the files are kept, and the dynamic imports are working! 🚀

lionel-rowe commented 2 weeks ago

As things stand, importing from dynamically created data: URIs does work, including if they import other (statically analyzable) packages, e.g.

// Import in a statically analyzable way
import('npm:<some-npm-package>@1.2.3')

// then, later on...
await import(
    `data:text/javascript;base64,${
        encodeBase64(`
            import * as mod from 'npm:<some-npm-package>@1.2.3'
            console.log(mod)
            console.log(${Math.random()})
        `)
    }`
)

But it doesn't work if you try to dedupe the package version by un-hard-coding it, even if it's a simple string declared with const in the same file:

const PACKAGE_VERSION = 'npm:<some-npm-package>@1.2.3'
// next line fails
import(PACKAGE_VERSION)

await import(
    `data:text/javascript;base64,${
        encodeBase64(`
            import * as mod from '${PACKAGE_VERSION}'
            console.log(mod)
        `)
    }`
)

Not sure if it'd be feasible to enable relatively simple cases like this as an intermediate step toward full dynamic imports, but I have a use case where it'd be useful.