Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parcel 2: Specific resolvers per file type #3492

Closed
devongovett opened this issue Sep 4, 2019 · 9 comments · Fixed by #6696
Closed

Parcel 2: Specific resolvers per file type #3492

devongovett opened this issue Sep 4, 2019 · 9 comments · Fixed by #6696

Comments

@devongovett
Copy link
Member

The way Parcel currently resolves all files is the same across all file types, whether that be JS, CSS, HTML, or anything else, but sometimes it makes sense for there to be differences. For Parcel 2, we are considering making resolvers configurable per file type. Something like this:

"resolvers": {
  "*.{js,mjs,jsm,jsx,es6,ts,tsx,coffee}": ["@parcel/resolver-path", "@parcel/resolver-alias", "@parcel/resolver-node"],
  "*.{css,styl,stylus,sss,sass,scss,less}": ["@parcel/resolver-url", "@parcel/resolver-path", "@parcel/resolver-alias", "@parcel/resolver-node"],
  "*.{htm,html}": ["@parcel/resolver-url", "@parcel/resolver-path", "@parcel/resolver-alias"]
}

These would run as pipelines, similar to other plugin types (e.g. transformers, optimizers, etc.). Each resolver would run, and the result if any would be passed to the next resolver and so on. Allowing resolvers to be configured this way would allow different languages to resolve slightly differently, and for users to adjust smaller aspects of the resolution more easily than rewriting the entire default resolver plugin.

The proposed resolver plugins above are the following:

  • @parcel/resolver-url - resolves URL specifiers to file paths, e.g. from CSS and HTML.
  • @parcel/resolver-path - resolves file path specifiers to absolute, e.g. tilde, relative, etc.
  • @parcel/resolver-alias - resolves aliases defined in package.json
  • @parcel/resolver-node - resolves node_modules.

The actual resolution algorithms for each language are described in the following sections.

JavaScript

The JS resolution algorithm is a modified version of the Node resolution algorithm. It also supports Parcel's tilde and absolute paths, aliases, and a few other main fields like source, browser and module. This is essentially the same as how Parcel 1 resolved modules.

One change from Parcel 1 is to begin treating ES module import specifiers are URLs rather than file paths. This means resolving with URL semantics rather than platform specific path semantics, and allowing things like query strings and percent escaping. See also #3477.

Some examples:

import 'foo'; // node_modules/foo
import 'foo/bar'; // node_modules/foo/bar.js
import './foo.js'; // ./foo.js
import './foo'; // ./foo.js
import '~/foo'; // <nearest dir with package.json>/foo.js
import '/foo'; // <projectRoot>/foo.js

Extensionless specifiers are allowed, but only for the following extensions: '.js', '.jsx', '.mjs', '.json', '.ts', '.tsx', '.coffee'. This is different from Parcel 1, which allowed importing any extension supported by Parcel without specifying it. This was slow since Parcel needed to check for every possible extension as it searched for files. Also in Parcel 2 there is no definitive list of extensions that are supported, as we use globs to match files instead, and I don't think it was really ever expected to be able to import non-JS without specifying the extension anyway. It is mostly for compatibility with the Node CommonJS ecosystem.

There are a few exceptions, where resolution is treated differently. These are for JS features that produce dependencies other than ES modules and CommonJS, which are generally treated as URLs. Examples include workers, service workers, etc.

new Worker('./foo') // ./foo.js
new Worker('foo') // ./foo.js || node_modules/foo

The main difference with these specifiers is that they are always treated as URLs, and bare specifiers are resolved as relative by default but fall back to node_modules. See #670. I'm still debating whether extensionless specifiers should be supported here. We could support them only for relative paths and not bare specifiers, or as above at all times. Would like feedback here.

CSS

The resolution algorithm for CSS is similar to the JS one, but has some additional restrictions. It still allows relative, tilde, and absolute path specifiers, but all specifiers are treated as URLs. This means that in addition to allowing query params, percent encoding, etc., relative URLs are allowed without prefixing with ./. Unfortunately, this conflicts with node_modules resolution, but that's less expected than in JS. For CSS, we will first check whether a relative path exists for a bare specifier, and if not, check if a node_modules path exists.

Some examples:

@import "foo"; /* node_modules/foo */
@import "foo/bar.css"; /* node_modules/foo/bar.css */
@import "foo.css"; /* ./foo.css || node_modules/foo.css */
@import "./foo"; /* ERROR - extensionless imports not allowed */
@import "./foo.css"; /* ./foo.css */
@import "~/foo.css"; /* <nearest dir with package.json>/foo.js */
@import "/foo.css"; /* <projectRoot>/foo.js */

Extensionless imports are not allowed (unlike JS, unlike Parcel 1), and directory index imports are also not allowed (e.g. no foo/index.css). This more closely matches the CSS spec, which treats imports are URLs.

When resolving from node_modules, the package.json#style field is considered as an entry point. Other entry fields are mostly JS specific, so are not supported for CSS.

HTML

The resolution algorithm for HTML is much simpler than for JS and CSS. node_modules resolution is not generally needed or expected in HTML, so it is not supported by default. That just leaves relative, tilde, and absolute paths, and URLs.

Some examples:

<a href="foo.html"></a> <!-- ./foo.html -->
<script src="foo.js"></script> <!-- ./foo.js -->
<script src="./foo.js"></script> <!-- ./foo.js -->
<script src="~/foo.js"></script> <!-- <nearest dir with package.json>/foo.js -->
<script src="/foo.js"></script> <!-- <projectRoot>/foo.js -->
<script src="./foo"></script> <!-- ERROR - extensionless -->
<script src="foo"></script> <!-- ERROR - node_modules resolution is not allowed -->

Extensionless imports and directory index imports are not supported. All specifiers are treated as URLs, and are resolved with URL semantics. Bare specifiers are treated as relative URLs, just like normal HTML.

Other languages

Other languages generally use their own import semantics and are handled by the individual transformer plugins. A few have Parcel specific extensions:

  • SASS, LESS, Stylus - these languages are extended to allow node_modules resolution, along with absolute and tilde paths (the CSS resolution path described above). If the Parcel resolver doesn't find anything, then the transformer falls back to the default resolver for those languages.
@devongovett
Copy link
Member Author

devongovett commented Sep 7, 2019

Now thinking of possibly adjusting this slightly to use an npm:/node:/package: protocol to load modules from node_modules where specifiers are URLs. This has the advantage of not being ambiguous, so we don't need to try the relative path with fallback. It's also clearer to the reader of the code, and more compatible with other tools and the spec.

new Worker('foo.js') // relative -> ./foo.js
new Worker('npm:foo') // node_modules/foo
@import "foo.css"; /* relative -> ./foo.css */
@import "npm:foo"; /* node_modules/foo */
@import "npm:foo/bar.css"; /* node_modules/foo/bar.css */
<script src="foo.js"></script> <!-- ./foo.js -->
<script src="npm:foo"></script> <!-- node_modules/foo -->

@devongovett
Copy link
Member Author

Also probably dropping extensionless import and index file support for Worker, service worker, etc. (JS dependencies other than ESM and CommonJS). This is also more compatible with the way those APIs would work natively in browsers. CSS and HTML also would not support those features.

@zanona
Copy link

zanona commented Jun 20, 2020

Allowing users to customise the meaning for tilde and backslash would be awesome.
In fact, the typescript resolution example under https://parcel2-docs.now.sh/features/module-resolution/#typescript-~-resolution already shows ~ being used to refer to files inside the src directory, which is the sensible setup for my use case. So, unless purposely adding an empty package.json file inside src, parcel resolver would still point to the ../src, in this case.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "~*": ["./src/*"]
    }
  }

@mischnic
Copy link
Member

Allowing users to customise the meaning for tilde and backslash would be awesome.

That is possible even with the current architecture

@zanona
Copy link

zanona commented Jun 20, 2020

@micopc strange, does it require additional setup though? I couldn't find it documented, and by only declaring it on tsconfig.json didn't seem to be enough? I guess we could use babel-plugin-module-resolve instead.

@mischnic
Copy link
Member

I meant: it is already possible to create a resolver plugin that uses tsconfig (without the changes to the plugin architecture as describes in this issue).

@fregante
Copy link
Contributor

node_modules resolution is not generally needed or expected in HTML

I don't understand this point. <script src="lodash"> isn't logically different than require('lodash'). In both cases I'm trying to load a script file. The only difference is that a non-type=module <script> either adds globals or has no exports.

Likewise <link rel="stylesheet" href="normalize.css"> would also be expected to work.

@devongovett
Copy link
Member Author

The problem is script and link elements in the browser resolve bare specifiers (without './ or / prefixes) as relative paths, not as npm modules. Parcel generally tries to follow existing browser standards, and makes deviations explicit. That's why we are thinking of using the npm: prefix for explicitly referencing node_modules in URL specifiers.

@fregante
Copy link
Contributor

Makes sense. Following the nomenclature of @parcel/resolver-node, node: would probably more correct.

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

Successfully merging a pull request may close this issue.

4 participants