Skip to content

Introduce package-root:// canonical uris #4616

Open

Description

@dcharkes

Currently, we only have a way to refer to Dart libraries in a canonical way (after we have a packages config) for libraries inside the lib/ directory.

This means we have to use relative imports in test/, bin/, tool/, samples/, examples/, and friends.

This means we have a way to uniquely point to public definitions inside a library e.g. package:my_package/src/foo.dart::class::Foo::method::bar, but no way to do the same for private definitions. (Useful for dart-lang/native#2891.)

Also it means we don't have a standardized way to point to samples or other data files in our tooling:

Should we consider standardizing on on a format that all our compilers and tooling agree on?

  • package-file:<package-name>/<file-path-from-package-root> or
  • package-private:<package-name>/<file-path-from-package-root>

And start allowing that format in Dart import statements?

(For some initial discussion see dart-lang/native#2891)

Thanks for your input @jensjoha @eernstg @jonasfj @davidmorgan @johnniwinther @srawlins!

Activity

  1. davidmorgan commented on Jan 23, 2026

    @davidmorgan
    Contributor

    I like the idea of officially adopting a scheme, FWIW my current favorite bikeshed color is package-internal ;) avoiding "package private" because it already means "under lib/src"

    Re: import statements

    • Is it allowed for a source under lib to import a non-lib from its own package; and/or is there a warning issued
    • Is it allowed for a source in one package to import a non-lib from another package? That add access to a lot of previously-inaccessible code :)

    I guess @lrhn probably has some thoughts on this one :)

  2. lrhn commented on Jan 23, 2026

    @lrhn
    Member

    It's not impossible. We already have the root of the Pub package in package_config.json.
    I'd prefer not repeating the mistake of package: URIs, and put the package name in the authority.
    Maybe pub://packageName/path/to/file. Or pub-pkg: as scheme. No need to be long.

    This is a reference to the Pub package, not the Dart package. The language doesn't know about Pub packages, it only knows that package:... URIs can (sometimes) be resolved to the content of a Dart package, which is only the code in the lib/ directory of the Pub package. Everything around that is historically Pub-specific, and only recently recognized at all (and only for language versioning).

    Maybe we can generally ensure that files use the pub: URI as their identity when they are inside a Pub package, like we try to make references to lib/ files use package: URIs. That would avoid having a file: URI as base when using relative paths.
    Relative paths will still work inside the same package, and you can use a rooted path as relative to the current pub package's root, like import '/test/test_helper.dart';. If the containing file's URI is pub://my_pkg/tool/test/my_test.dart, then /test/test_helper.dart will automatically resolve to pub://my_pkg/test/test_helper.dart, using the completely normal URI resolution rules.
    (I had to hack the Uri class to preserve the package name in package:package_name/... from being treated as part of the path, while people are still doing packageUri.path.split('/').first to get the package name. Ugh.)

    As @davidmorgan says, there may need to be some limits on which URIs you can use from where.
    And maybe they need to be "you can't refer to other packages at all", or we might completely break dependencies!
    (So the answers to those two questions are probably "no" and "no".)

    The current separation is:

    • Code inside lib/ (and bin/?) depend only on non-dev-dependencies.
    • Code depending on another package inherits the other package's non-dev-dependencies transitively.
    • So referencing any code inside lib/ of the other package is safe, all its dependencies are available in your own package resolution config.
    • Code outside of lib/ can depend on dev-dependencies. Dev-dependencies are never inherited.

    If you can do import 'pub://other_pkg/tool/helper.dart';, then that library should be compiled in your package configuration, which may not include the dev-dependencies of other_pkg. And helper.dart is allowed to refer to such.

    If you do dart run other_pkg:binexeI think it runs in that code in its own package configuration.
    That should be fine, you're not mixin code from different configurations in the same program.

    Importing non-lib/ source from another package is probably completely and utterly unsafe, and can't be allowed.
    (I tried coming up with some kind of new dependency relation, and a way for an author to opt-in to non-lib/ code
    being available outside of the package, but none of it would be better than just having an extra package for that code, where the shared code is in lib/.)

    So, as @eernstg said in the other issue, these are URIs for files "not intended to be imported by dart libraries in other packages".
    (I'm not sure this needs to be a language issue, if it's mainly tooling.)

    Doesn't stop other uses of the URI, like tools referring to specific files in specific packages. (Referring to files is a source concept and only makes sense while compiling, so it's probably mainly for tools.)

    We can also allow files inside a pub package to refer to itself using pub://myname/path/file.dart in imports/exports/parts, but not to any other package. And we can try to canonicalize the URIs of entry points or other externally referenced libraries to such URIs when compiling inside a package config where they have such a Pub-package URI. Then you can treat the package root as the path root for imports.

    And start allowing that format in Dart import statements?

    So only to a very limited degree (self-package only). But then we should prefer and enforce that URI format, so you can depend on it.

    package:my_package/src/foo.dart::class::Foo::method::bar

    Nit: Please use package:my_package/src/foo.dart#class::Foo::method::bar.

    The part before the fragment identifies the file, the fragment part is not part of the path or file name, and has a meaning internal to the file.

    (I don't care what the format is after the #, it can also use / and # if it wants to, ...foo.dart#class/Foo#method/bar, just that it's separated from what is actually a path.)

  3. jakemac53 commented on Jan 30, 2026

    @jakemac53
    Contributor

    We already have a name for the place we want to resolve to, it is right in the package config, the "rootUri". Given that, I would like to propose the URI scheme package-root. That is what this is pointing to, the true "root" of the package (which may or may not also be the package URI root).

    I do like the idea of using the authority for the package name, so something like package-root://foo/lib/foo.dart.

    This is not a pub specific concept at all, it is baked right into the package config, so I do not think pub should be anywhere in the name. Internally we use the same package layout, because it just makes sense, even though there are no pubspecs for internal Dart packages. But it is irrelevant as this is a package config concept now not a pub concept.

    Separately, I am interested in being able to distinguish between two identical URIs which actually resolve to different places, because multiple package config files exist within one context. This is a common occurrence for IDE environments and also command line AI environments, or really any development environment at all outside of google3 or pub workspaces. This is relevant not only to this proposal but also package: URIs. They are really only canonical when put in the context of a package config. So, I would like to use either the host or query portions of the URI to point at the package config which should be used to resolve a given URI.

  4. rrousselGit commented on Jan 30, 2026

    @rrousselGit

    Adding to that, build_runner sends tools an Uri of the format asset:package/path.dart

    And since imports are of the format import 'uri', we could make a case for import 'asset:package_name/bin/main.dart';

  5. jakemac53 commented on Jan 30, 2026

    @jakemac53
    Contributor

    I don't think we should use asset: as a general scheme as I think it is a little too generic. I kind of regret choosing that name originally, I just chose it because build_runner calls all files "assets", but that is sort of build_runner specific.

    I do like that its short.

    And since imports are of the format import 'uri', we could make a case for import 'asset:package_name/bin/main.dart';

    I do not think these URIs should be valid in imports, if you want people to be able to import something put it under lib, that is what it's for. This would effectively make altering even your tests or examples a breaking change.

  6. rrousselGit commented on Jan 30, 2026

    @rrousselGit

    It'd be like lib vs lib/src

    You could import it, but it'd be considered private

  7. jakemac53 commented on Jan 30, 2026

    @jakemac53
    Contributor

    I really don't see a need for importing these files, and I do not want anybody to be able to import these from my packages, or tbh even to be able to try and reference them at runtime at all.

    These URIs in general should only be a development time internal detail for certain tools, so they can have a canonical way to speak about files that are outside of lib. Similar to multi-root-scheme (and actually, could be a replacement for that potentially).

    Note that build_runner even does not allow access to these files in general, unless the package opts in to exposing them.

  8. dcharkes commented on Jan 30, 2026

    @dcharkes
    ContributorAuthor

    +1 I don't think we should use the asset: scheme, it's very confusing in the context of Flutter where we have assets in the pubspec.

    I really don't see a need for importing these files, and I do not want anybody to be able to import these from my packages, or tbh even to be able to try and reference them at runtime at all.

    +1 I want a canonical way to refer to things, and for that way to be compatible across tools.

    So, as @eernstg said in the other issue, these are URIs for files "not intended to be imported by dart libraries in other packages".
    (I'm not sure this needs to be a language issue, if it's mainly tooling.)

    We look to the language team for standardization 😉 🤷‍♂️

    For reasons such as this! 👇 🙏

    Nit: Please use package:my_package/src/foo.dart#class::Foo::method::bar.

    The part before the fragment identifies the file, the fragment part is not part of the path or file name, and has a meaning internal to the file.

    @jakemac53, but not surfaced in this thread yet:

    package-root://<package-name>@<version>/<package-root-relative-path>

    I love the addition of @version. To make it compliant with the URI spec, we should put version before the @ and use the "authority": package-root://<version>@<package-name>/<package-root-relative-path>.

    If you do dart run other_pkg:binexeI think it runs in that code in its own package configuration.

    Correct. @jonasfj @sigurdm and I have been toying withdart run other_pkg@<version>:binexe. (Which does not follow URI conventions either. <version> in such command can be arbitrary JSON...)

  9. lrhn commented on Jan 30, 2026

    @lrhn
    Member

    Putting the "version" into the package reference does make it Pub-specific, and may warrant a pub in the scheme name.
    (Using package_name@version is tricky, because if parsed by the standard URI rules, that makes the package_name the user-info, and the version the host. Sadly, can't use //package_name:version/, because the "port" after : can only be a single number. Swapping them, as suggested, would be better, but only if the version is simple enough to fit into the authority to begin with.)

    Everything else has only referred to package info available in a package_config.json, which means one version of each package.

    Putting anything into the authority part of a package: URI is a bad idea. The format has no / at the front of the path, but a URI with an authority must start a non-empty path with /. So that's a no-go.

    If we want to refer to the package-config file in the URI, a query is probably better:
    package:foo/path/to/file.dart?config=file:///absolute/path/package_config.json

    The query is part of the resource identifier, a fragment is not, so

    • package:foo/path/to/file.dart?config=file:///absolute/path/package_config.json
    • package:foo/path/to/file.dart?config=file:///absolute/other/path/package_config.json

    refers to two different resources, whereas

    • package:foo/path/to/file.dart#config=file:///absolute/path/package_config.json
    • package:foo/path/to/file.dart#config=file:///absolute/other/path/package_config.json

    refers to the same resource (and whoever reads that resource may know what the fragment identifier means in relation to the content).
    In standard URI interpretation at least. Any scheme can define what everything after it means, like data: and email: do, but you won't get any help from the Uri class if you don't follow the standard rules.

    So use a query for that. It's also not easy to cram a full URI into the authority section of a URI, even if we wanted to. You'll have to escape everything, making it much harder to read.
    And a query will also work the same for a package-root: URI. So, that could work:

    • package:package_name/path/to/file.dart?config=file:///absolute/path/package_config.json
    • package-root://package_name/test/some_test.dart?config=file:///absolute/path/package_config.json

    If no specific config is given, it should occur in a context where a config is given. (And you can't give one in Dart imports, or where a config is otherwise implied!)

    We could use package: for both, and say that if there is an authority, that's the package name and the path is root-relative, and if not, the first part segment is the package name and the rest is a lib/-relative path.
    I just think it will confuse existing code that works with package: URIs, which there is far too much of. (Nobody should parse Package URIs themselves, and do var packageName = uri.pathSegments.first;. There should be abstractions for that. Alas, a little too late.)
    So a new scheme is safer.

    If we want to specify a version, then package-root://pkgname/path?version=1.2.25 could work. It doesn't work together with a config=, and whoever interprets the URI needs to understand what the query means.

  10. lrhn commented on Feb 1, 2026

    @lrhn
    Member

    If we want this, it could be integrated into package:package_config. That package already has the package config loading and resolution built in.

    To support a ?config=... parameter, it probably has to provide a "context" object where it can cache package configurations, so it doesn't have to load a package-config file every time it resolves such a URI. And it should be a user-controlled context that can be cleared if a config file changes, which isn't something the package wants to be detecting itself. (So probably whoever creates the context can provide a callback to be notified every time a new configuration is added, and be given a way to invalidate a cached configuration.)

    No version parameter, that's outside the scope of package configurations.

  11. dcharkes commented on Feb 2, 2026

    @dcharkes
    ContributorAuthor

    Putting the "version" into the package reference does make it Pub-specific, and may warrant a pub in the scheme name.

    I like this distinction. Using a different scheme for whether pub is needed or not. package-root:// requires a package config, and doesn't support asking for a different version. And, pub:// could support versions.

    It seems we are converging to package-root as scheme, I'll update the proposal title.

  12. changed the title [-]Introduce `package-file:` or `package-private:` canonical (library) uris[/-] [+]Introduce `package-root://` canonical uris[/+] on Feb 2, 2026
  13. jakemac53 commented on Feb 2, 2026

    @jakemac53
    Contributor

    The ?config=<path> query param is anyways more accurate, even in the pub world you actually have a mixture of pub dependencies and other dependencies, so the version alone isn't actually enough. If you instead say to resolve a URI with a specific package config, that is unambiguous.

    The only thing I don't love about it is in my case I plan to expose these URIs to AI agents as resources, and in that world shorter URIs are better - they are both surfaced to users sometimes and also consume tokens.

    But, I think it makes sense, and it can in any case be optional so we can choose to use it or not.

  14. lrhn commented on May 26, 2026

    @lrhn
    Member

    Just came by this thread again. A config=... could be allowed to omit the .dart_tool/package_config.json, which isn't much, but it is some. Pointing to a directory would trigger the "find config for directory" logic, which would find .dart_tool/package_config.json as the first thing. (Or check the parent directory, but if brevity is the goal, might as well have the parent directory directly in the URL.)

  15. jakemac53 commented on May 26, 2026

    @jakemac53
    Contributor

    Note that for the MCP server I ended up using package-root: (without the //). This was just to retain consistency with package: for people and agents.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions