denoland / deno

A modern runtime for JavaScript and TypeScript.
https://deno.com
MIT License
93.92k stars 5.22k forks source link

Support importMap transitivity for local development. #23412

Closed NfNitLoop closed 2 weeks ago

NfNitLoop commented 4 months ago

Feature Request

Deno needs a way to (temporarily?) depend on another local project that uses an import map.

Context

Deno docs have an example of overriding a dependency with a local one for local development:

{
  "imports": {
    "https://deno.land/std@0.177.0/": "https://deno.land/std@0.223.0/"
  },
  "scopes": {
    "https://deno.land/x/example/": {
      "https://deno.land/std@0.177.0/": "./patched/"
    }
  }
}

But I'm pretty sure that only works for dependencies that inline their dependencies in source files (or use the deps.ts pattern).

Let's put aside the "scopes" / override aspect of the above example for now. I don't care about overriding dependencies upstream, or limiting those overrides to a specific scope. I'm mostly concerned with supporting any way to (temporarily or not) depend on another local project that uses import maps.

For example, if I have a bar/main.ts:

#!/usr/bin/env -S deno run
import { add } from "@nfnitloop/foo"

console.log(add(1, "foo"))

with a deno.json including:

   "imports": {
      "@nfnitloop/foo": "../foo/mod.ts"
   }

deno is unable to resolve the transitive dependencies. (i.e.: those defined in ../foo/deno.json.)

> deno info main.ts
local: /Users/codyc/code/deno/test-peers/bar/main.ts
emit: /Users/codyc/Library/Caches/deno/gen/file/Users/codyc/code/deno/test-peers/bar/main.ts.js
type: TypeScript
dependencies: 1 unique
size: 354B

file:///Users/codyc/code/deno/test-peers/bar/main.ts (91B)
└─┬ file:///Users/codyc/code/deno/test-peers/foo/mod.ts (263B)
  └── Relative import path "@std/fmt/colors" not prefixed with / or ./ or ../ and not in import map from "file:///Users/codyc/code/deno/test-peers/foo/mod.ts" (resolve error)
> deno run main.ts
error: Relative import path "@std/fmt/colors" not prefixed with / or ./ or ../ and not in import map from "file:///Users/codyc/code/deno/test-peers/foo/mod.ts"
    at file:///Users/codyc/code/deno/test-peers/foo/mod.ts:1:21

If this worked, it would be an easy way for users to download a dependency and update their code to depend on it locally for testing/development.

The fact that it currently doesn't is one of the reasons I'm still avoiding import maps in my projects and would recommend others do the same.

Insufficient Workaround

The problem is that deno isn't taking into account the local dependency's deno.json , or other locations where an import map might be specified. In the example above, I could "make it work" by re-declaring all of foo's import map inside my own, so that imports are correctly resolved.

But as projects grow, and dependency trees get larger, and deeper, that's not a workable solution. Especially if I only want to do that temporarily to do integration testing between a dependency and my library before pushing my changes up into a PR.

Workspaces

There's also the in-development workspace feature, which can handle some use cases for this. But what I've seen of it so far seems to be targeted more to developing a set of interdependent libraries like those in @std/. There are a couple problems I think that that work won't solve:

  1. From what I've seen, in a workspace all dependencies are pushed to the top level directory of the workspace, so if I wanted to temporarily bring a third-party module into my workspace, I'd still have to manually inline its dependencies into my workspace, if I'm using one, or completely modify my project structure to work as a workspace to be able to patch in a third-party dependency.

  2. If the dependency I want to patch is itself contained in a workspace, we're back to the same issue.

If there were just a general-purpose solution for transitively resolving dependencies in this case, it might be simpler than trying to work around the issue w/ workspaces.

Example from Node

The npm command has npm link to temporarily (sym)link a local dependency into your dependency graph for testing. It does work, but sometimes has unexpected behavior.

There are more complicated cases where you might want something like this in Deno (ex: where a dependency foo is used not just by your own project, but as a common dependency among many of your dependencies, and you need to override it in all cases.)

But it feels like the simpler case of "I need to fix this dependency I'm using" is probably more common, and an easier fix to start with.

Brainstorming Implementation

I see one problem that might be why transitively resolving dependencies via import maps might not be supported -- the same reason that HTTP imports don't automatically use import maps -- there's no standard for where the import map should be found.

JSR.io solves this by having a standard location. Given jsr:@namespace/package/path, tools can know where to find the import map & dependencies for that package.

To do something similar locally, there might need to be an import prefix/syntax that conveys:

  1. This is the project root on the file system
  2. This is the exported path within that root that I want to import. (path, above)
  3. Deno should use the same rules for finding import maps that deno publish does.

Something like: jsr-local:../foo::path

Alternatively, if jsr: always expects to be followed by @ for a namespace, a following . could be taken to mean we're using jsr-style resolution locally, then the above could be written as:

jsr:../foo@local/path

This (ab|re)uses the @version specifier from jsr.io with a version of @local to act as a separator between # 1 and # 2.

dsherret commented 4 months ago

Yeah, there needs to be a solution for this problem.

dsherret commented 3 weeks ago

There is some preliminary work on this here: https://github.com/denoland/deno_config/pull/100

Hoping to get it into Deno 1.46 next week (probably as considered unstable)

dsherret commented 2 weeks ago

We didn't talk about this feature in the 1.46 blog post, but would someone be able to try it out and give some feedback? You just need to add a relative path under the "patch" key like the following to your deno.json and it should use the local path instead (available in Deno 1.46--deno upgrade):

{
  "patch": [
    "../some-package-or-workspace"
  ]
}

It should work even if you specify a workspace. There's a tracking issue for this feature here: https://github.com/denoland/deno/issues/25110