-
-
Notifications
You must be signed in to change notification settings - Fork 0
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's package.json#targets #1
Comments
Thanks for clarifying @jamiebuilds that makes a lot of sense.... so maybe these are actually very compatible proposals.... by thinking of (where {
"name": "package-name",
"entries": {
"module": "dist/module/index.js",
"browser": "dist/browser/index.js",
"default": "dist/main/index.js"
},
"targets": {
"default": {
"node": ["^4.0.0"]
},
"module": {
"node": ["^8.0.0"]
},
"browser": {
"browsers": ["> 1%", "not dead"]
}
}
} In this model, The convergence I am hoping to be able to see with exports is the following: {
"exports": {
"./feature": "./dist/features/feature.js"
}
} allowing branching just like we do for mains via: {
"exports": {
"./feature": {
"module": "dist/module/feature.js",
"browser": "dist/browser/feature.js",
"default": "dist/main/feature.js"
}
}
} which also seems like it could remain compatible with targets as well. If this really does work out that would be very exciting, because it means we can have our cake and eat it, in that we have the simplicity of what users currently use, while also remaining fully compatible with the fine-grained definitions you've worked out! There's still a bit to discuss though certainly.... I would be interested to hear your thoughts further. |
One thing I'm worried about is that if Using your example before, I think it's more likely that people will write this (in order to remain compatible with the existing ecosystem): {
"name": "package-name",
"main": "dist/main/index.js",
"module": "dist/module/index.js",
"browser": "dist/browser/index.js",
"entries": {
"module": "dist/module/index.js",
"browser": "dist/browser/index.js",
"default": "dist/main/index.js"
},
"targets": {
"default": {...},
"module": {...},
"browser": {...}
}
} At which point it's kinda confusing that But if you do align the key names, then it also feels like a lot of extra work that users won't necessarily understand what they are getting from it: {
"name": "package-name",
"main": "dist/main/index.js",
"module": "dist/module/index.js",
"browser": "dist/browser/index.js",
"entries": {
"module": "dist/module/index.js",
"browser": "dist/browser/index.js",
"default": "dist/main/index.js"
},
"targets": {
"default": {...},
"module": {...},
"browser": {...}
}
} So while I think that these proposal can definitely be introduced separately without conflicting with one another (Yes they are both mutually additive imo). I think we should try for something more unified so that users 1. Don't feel like they are being forced to do a ton of extra work and 2. Understand everything that they are getting from any additional work they are putting in. |
That's a good point. The major win of the I'm fine with users not using The major feature I want though really is to be able to share the meaning of these names, just like you have in targets, with both aliases and exports: {
"name": "package-name",
"main": "dist/main/index.js",
"module": "dist/module/index.js",
"browser": "dist/browser/index.js",
"targets": {
"default": {...},
"module": {...},
"browser": {...}
},
"exports": {
"./feature": {
"module": "./dist/feature.module.js",
"browser": "./dist/feature.browser.js",
"default": "./dist/feature.default.js"
}
},
"aliases": {
"local": {
"module": "./local/alias.module.js",
"browser": "./local/alias.browser.js",
"default": "./local/alias.default.js"
}
}
}
The overall goal is simply to get the same benefits we have with the browser field, but in a scalable way that scales with these names. |
I absolutely agree with that. I much prefer the separate In terms of having a "default" I think we need to consider carefully what we're implying. Because in terms of the "default" {
"name": "package-name",
"main": "dist/main/index.js", // "main" is the default if you are in a commonjs env (generally falling back to fs resolution, or another field if you've configured a bundler to do so)
"module": "dist/module/index.js", // "module" is the default if you are in a esm env (generally falling back to "main")
"browser": "dist/browser/index.js" // "browser" is the default if you are requesting for a browser bundle. (generally falling back to either "module" or "main")
} However, you certainly could have a "defaults" in terms of the configuration for all of your entry fields. {
"name": "package-name",
"main": "dist/main/index.js",
"module": "dist/module/index.js",
"browser": "dist/browser/index.js",
"targets": {
"default": {...}, // `package.json#main/module` uses this config (because they aren't configured separately)
"browser": {...} // But `package.json#browser` uses this config
}
} However, in terms of this {
"name": "package-name",
"main": "dist/main/index.js",
"module": "dist/module/index.js",
"browser": "dist/browser/index.js",
"engines": { "node": ">=4.0.0" }, // default targets config
"browserslist": "...", // default targets config
"targets": {
"main": {
// "node" -- defaults to `package.json#engines.node`
"browsers": false // override to turn off default
},
"module": {
"node": ">=12.0.0", // override to new value
"browsers": "..." // override to new value
},
"browser": {
"node": false // override to turn off default
// "browsers" -- defaults to `package.json#browserslist
}
}
} Counter Argument: People may be confused that they have to set Alternative {
"targets": {
"defaults": {
"node": ">4"
"browsers": "..."
},
"main": {...},
"module": {...},
"browser": {...}
}
} |
One of the underlying ideas of these "environment names" is that you could pass a list of truthy environment names to the bundler, eg - But the concept of Then when selecting which option to use, out of say: {
"exports": {
"./feature": {
"module": "./dist/feature.module.js",
"browser": "./dist/feature.browser.js",
"default": "./dist/feature.default.js"
}
}
} the conditions get checked in object order from top to bottom. The first truthy environment is then selected. So if none match, we get the So I can appreciate that Re setting Composition of conditions is also supported in this model (as in this proposal) - {
"exports": {
"./feature": {
"module|browser": "./dist/feature.module.js",
"default": "./dist/feature.default.js"
}
}
} would only select |
Okay, in that case I would question that "main" isn't a better name for the "always truthy" case. It's somewhat already understood to have that meaning, and I think it's more important to align with But going along with "default" I would still expect {
"main": "...", // is default
"module": "...",
"browser": "...",
"defaultEntryField": "main",
"targets": {
"main": {...}, // is default
"module": {...},
"browser": {...},
},
"exports": {
"./feature": {
"main": "...", // is default
"module": "...",
"browser": "..."
}
}
} |
Now that you mention it I could actually get behind renaming Note though that If we're in some vague agreement on the overall model, I'd be interested to hear how we might manage the balance between control over |
(is the expectation that |
What's the reasoning for selecting them in that order. Not that I'm opposed to adding order to keys in JSON objects (That's a huge part of the design of
Yes, it's also part of our strategy to know which installed packages need additional transpiling and which ones we're okay just using. If we can't find an entry point to a module that meets our requirements, we'll select one that is "closest" and compile it down further. (This is a big problem within existing tools today, I've seen a lot of not-enough-transpiled code end up in production that came from |
Seems to me like the order shouldn't be configurable directly. The tool should choose the entry/target that satisfies the maximum number of constraints. First, it filters the set of entries to those that match the target environment (node, browser, electron, etc.). If multiple entries still match, sort based on target information (e.g. engines, syntax support, etc.). If none match, then fall back to the "default"/"main" entry. Tools might also want to find the "closest" entry to what they need rather than an exact match. For example, Parcel will transpile modules futher if the desired target is "lower" than the resolved module. |
I guess I'm trying to define the behaviour here for when there is no If it isn't the case that an environment fulfills multiple conditions - say with the mains we have currently where But if we add a new environment condition like Maybe we won't define an Thinking about the simplest possible case: {
"exports": {
"./x": {
"node": "./index.js",
"esmodule": "./index.esm.js"
}
}
} How would Node.js know which one to pick if not through ordering? Or do we need Node.js to understand I'd like to find a way that targets can act additively, as if there were a standard default Where the rules would be the sort of generic: {
"targets": {
"main": {
"node": ["*"],
"browser": ["*"]
},
"node": { "node": ["*"] },
"browser": { "browsers": ["*"] }
}
} does that make sense as an approach? Would you require targets to be present to be able to do differential aliases in Parcel, or handle this another way without precedence? |
Very interesting. Out of interest, and to understand targets better, how would you define three main targets for main entry points in Node.js 4, one for Node.js 5 - 9 and another for Node.js 10+ in the package.json? |
Right now with multiple entry points defined in a
Since we were talking about having the names be mirrored in Then we can come back later with resolvePackageEntry(pkg, { mainFields: ["browser", "module", "main"]) {
"main": "...",
"module": "...",
"browser": "...", // selected because "browser" was first in array
"exports": {
"./feature": {
"main": "...",
"module": "..." // selected because "browser" doesn't exist and "module" was second in array
}
}
} |
Edit: Corrected the Electron rule.
Right, but in environments like electron, the rule is just
This certainly seems a sensible tooling choice to generalize the decision process. If the resolver is free to handle the precedence decision itself, my concern with that approach is that the point of specifying something is to attempt to avoid ambiguity. A good spec should provide well-defined behaviours down to the inputs. If precedence is not obviously defined that could be a concern for predictability. This also brings up the question as to whether we should try and define these conditions. For consistency between tools I was hoping to maintain central definitions / meanings that try to accurately describe what the condition refers to. Eg I was imagining a central website / form where these names could be registered along with their meanings to avoid conflicts, but tools would, like with MIME / content-type be free to do their own thing too. Parcel could redefine the definitions entirely through targets, but perhaps this would provide a useful default? I was also hoping to define combining condition names with the I'm open to reconsidering the precedence question though certainly - perhaps each name could be associated with a precedence in the spec itself? |
If we allow names to have precedence associated with them in the resolver or spec, then combined precedence if we also introduce Then the default precedence is probably something like (where each row is equal precedence, and environments / tools would only enable the ones that apply):
|
(And to clarify too, I'm not tied to |
Another question - does Parcel support a |
No, it treats I wouldn't be against a |
This discussion has been really productive, thank you both again. I've been thinking about how to consolidate this discussion into a new proposal that might work more flexibly than entries. Would it be worth combining targets into their definitions with a new type of array fallback form: {
"entries": [{
"target": "./main-browser-modern.js",
"browsers": ["> 1%", "not dead"]
}, {
"target": "./main-browser-legacy.js"
"browsers": ["> 1%", "not dead"]
}, {
"target": "./node-all.js",
"engines": {
"node": ">=4.x",
"electron": ">=2.x"
},
}]
} where just like array fallbacks in import maps, the first match is taken from left to right. I've always steered clear of this type of a complex construct due to the difficulty of specifying the conditions, but perhaps we could find a way to sketch out the space simply in a spec between tools? Again, the above would generalize to "exports" and "imports" in package.json too. Would value your thoughts on that - and if it sounds sensible I can set up a new proposal to investigate this. |
I've gone ahead and created a new Package Targets Proposal based out of these discussions, and very similar to the Parcel 2 definition at https://github.com/guybedford/proposal-pkg-targets. It would be great to move further discussions there. |
Also, @devongovett @jamiebuilds @jkrems @bterlson would you be ok for me to list you as contributors? Since the ideas are mostly yours and from our discussions. I just didn't want to do so without consent though. |
Sure, happy to be involved (and be listed as a contributor)! |
Sure
I think this is the major divergence of what we're trying to do here. Consider all the different things those field names are accounting for:
If we explode that out into a matrix of all the different combinations of "Browser/Node/Electron environment that supports/doesn't support modules and ES5/ES2015/ES2016" Then you get into the problem of what do those mean over time. If we're using a very generic label like "browser" we could say that means Chrome 75, Safari 12, etc. But then 5 years down the line no one cares about those versions anymore, what do we do? Change the definition? Now every package using the old definition is out of date. My opinion is that we should throw in the towel trying to make Then it doesn't matter what fields a package author defines. They could have a Taking that into consideration, I would also like a central place for defining what different field names in {
"targets": {
"hissingbubblegumslide": {
"browsers": ["Chrome >= 75", ...], // https://package-json-target-types.org/spec#browsers
"modules": "esm", // https://package-json-target-types.org/spec#modules
"node": ">=4.0.0", // https://package-json-target-types.org/spec#node
"electron": "..." // https://package-json-target-types.org/spec#electron
...
}
}
} |
Note that the Further slicing on that doesn't look so bad either. There has been some discussion in guybedford/proposal-pkg-targets#2 of yearly target environments to capture this sort of thing. Under such a model this might become something like - {
"main": "index-legacy.js",
"browser": {
"featureset2019": "./index-2019-browser.js",
"main": "./index-legacy-browser.js"
},
} Because Electron is a browser it should match the above as well. If we wanted to match both Node and browser, we wouldn't even need to specify the "browser" and could have gone straight to the featureset slice: {
"main": "index-legacy.js",
"featureset2019": "./index-2019.js"
}
The definitions are very carefully designed not to change over time. That is one of the major requirements of the default definitions.
I'm all for the Parcel targets proposal, but I think having sane defaults helps the major case. The browser field has been vitally important in the past to mean any browser. I just want to work out how to continue to use the "browser" field as we expand into "exports" and "imports" in the package.json, and think about other environments too. I don't think anyone is complaining about the current meanings of these fields, but rather looking to extend them. What I like about the Parcel approach is that exactly does provide an extension point. But having defaults that back it if you have no
The proposal as it stands is exactly designed not to preclude that. I'm all for the freedom of the hissingbubblegumslide target! |
Opening this issue after our discussion on how to align this proposal and the work that Parcel is doing to specify what Parcel 2 has proposed with
package.json#targets
.At a high-level
package.json#targets
looks like this:For background, our motivations in this design were to solve for the following problems:
package.json
fields are entry points to the package?package.json
entry field actually means (in terms of the environment it is supposed to be executed in)?We also wanted to make sure that we designed something that:
Basically what we ended up designing was a way of storing metadata about what existing entry fields existed and what they meant to the package author. We designed
package.json#targets
to not care when package authors added newpackage.json
entry fields, and not to care when the ecosystem defined a new type of environment that an entry point could be targeting.For starters, if you wanted to know what entry fields existed in
package.json
you could dokeys(package.json#targets)
:This keeps us compatible with tools that really want the entry fields to be in the top-level of the
package.json
object, and we weren't replacing them at all, just describing them.The
package.json#targets
values then start describing the environment they are intending to support. So from the context in which I am requesting the entry point for a package, I can say "I want X package for Y environment"For the above
package.json#targets
, if I'm looking for an entry that can target"environment-y"
I can easily figure out thatentry-b
is the only entry that supports that environment.This is not intended to be a strictly defined system, it's meant to support whatever the ecosystem may come up with next-- Just tell us what it is and we'll do our best to make it work from the tooling side.
There is more information about this proposal in the Parcel 2 RFC
The text was updated successfully, but these errors were encountered: