The Wayback Machine - https://web.archive.org/web/20210304121931/https://github.com/microsoft/TypeScript/pull/35206
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

Native support for PnP #35206

Open
wants to merge 1 commit into
base: master
from
Open

Native support for PnP #35206

wants to merge 1 commit into from

Conversation

@arcanis
Copy link

@arcanis arcanis commented Nov 19, 2019

  • Code is up-to-date with the master branch
  • You've successfully run gulp runtests locally
  • There is an associated issue in the Backlog milestone (required):
    Although not in the Backlog milestone, this PR is in reference to #28289
  • There are new or updated unit tests validating the change:
    Not yet because it's just a PoC for now and I wanted to hear the opinion of the TS team first

What is PnP? (you can skip if you already know)

It's an alternate install strategy where instead of installing the files within the node_modules, we instead tell Node to directly reference them from a flat location on the disk. The resulting installs are more space-efficient and faster, but aren't compatible with programs that reimplement the node_modules traversal algorithm.

Because it involves a different resolution algorithm than the default Node one it can be seen as a relatively high-risk endeavour. Conscious of that, we've made significant progress during this past year to remove obstacles one by one. We welcomed new community members, helped many project to fix their package definitions, improved our implementation to provide more detailed errors, and thanks to everyone's collaboration the remaining blockers are only a handful. My point here is: PnP isn't just a phase - just like Yarn itself didn't end up just a phase.

PnP and TypeScript

We've discussed a bit with TS not so long ago, and from my understanding some tools might appear in the future to make it easier to implement this kind of logic outside of the core. Given that Yarn will likely be the prime consumer, I figured it could be interesting to try and see how PnP support could be implemented natively - to get a better idea of the tradeoffs involved, etc. A basic analysis yielded yarnpkg/berry#589, and I decided to spent a couple of hours trying to actually make it work.

This diff makes TS able to query the PnP runtime to obtain the location of the file dependencies. Because it doesn't require files that haven't been executed already (cf isPnpAvailable) I believe it should also address the security concerns initially raised by the TS team. I tested it with the Berry repository itself and it typechecked everything correctly:

git clone [email protected]:yarnpkg/berry berry
git clone [email protected]:arcanis/typescript arca-ts
(cd arca-ts && git checkout mael/pnp && yarn && yarn gulp)
(cd berry && yarn node ../arca-ts/built/local/tsc.js --noEmit -p .)

The one missing part is watch support; adding or removing packages requires restarting the server. This part seemed fairly different from the module resolution, so I thought I should first get your opinion on this PR as it is before going any further 😊

return typeRoots;
}
const nodeModulesAtTypes = combinePaths("node_modules", "@types");

function getPnpTypeRoots(currentDirectory: string) {

This comment has been minimized.

@arcanis

arcanis Nov 19, 2019
Author

Under PnP environments, the packages are installed like this:

/project/.yarn/cache/@types-foo-1.0.0.zip/node_modules/@types/foo/package.json
/project/.yarn/cache/@types-bar-1.0.0.zip/node_modules/@types/bar/package.json
/project/.yarn/cache/@types-baz-1.0.0.zip/node_modules/@types/baz/package.json

This function obtains the location of all @types dependencies, then retrieve their parent directories to build up to a list of N type roots which each contain a single package (instead of a single type roots that contains N packages as you'd get with typical installs).

const searchResult = loadModuleFromNearestNodeModulesDirectory(Extensions.DtsOnly, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState, /*cache*/ undefined, /*redirectedReference*/ undefined);
const searchResult = isPnpAvailable()
? tryLoadModuleUsingPnpResolution(Extensions.DtsOnly, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState)
: loadModuleFromNearestNodeModulesDirectory(Extensions.DtsOnly, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState, /*cache*/ undefined, /*redirectedReference*/ undefined);

This comment has been minimized.

@arcanis

arcanis Nov 19, 2019
Author

This only affects projects using PnP projects (because of the isPnpAvailable check).

The reason I went with a branch rather than a fallback is because I'm worried of potential resolution corruptions that would happen if for example someone installs a project with Yarn 1 (which would generate a node_modules) then Yarn 2. In this situation, a fallback could get passing results that would fail on pure Yarn 2 systems (because of hoisting).

}

function getPnpApi() {
return require("pnpapi");

This comment has been minimized.

@arcanis

arcanis Nov 19, 2019
Author

The pnpapi module is a builtin module that's only available within the PnP runtime. It always points to the PnP runtime currently active (so by extension, it doesn't execute anything from the disk - similar to if you were calling require('module'), for example).

This comment has been minimized.

@weswigham

weswigham Nov 21, 2019
Member

How would a PnP runtime be active? Neither the tsc executable we ship nor vscode's language service would do such a thing; so this entrypoint is for.... what kind of usage?

This comment has been minimized.

@arcanis

arcanis Nov 21, 2019
Author

The runtime is active when running TypeScript through yarn. Running yarn run tsc setups NODE_OPTIONS to first load the loader before pursuing the execution.

For VSCode, we use typescript.tssdk to point to a tsserver that firsts setups the hook and then require the real tsserver. In both cases, those paths require explicit user interaction.

This comment has been minimized.

@weswigham

weswigham Nov 21, 2019
Member

I'm going to go out on a limb and say that this kind of ad-hoc cooperative plugin model (least of all, one that explicitly ties the executed plugin to specifically pnp-related runtime code, and not "arbitrary runtime-provided host behavior" like the ChakraHost hook provides) isn't something we'd be interested in compared with the very explicit model @rbuckton has been working on, but we'll see I guess.

This comment has been minimized.

@arcanis

arcanis Nov 22, 2019
Author

One thing I'd like to mention is that the PnP API is specified in a way that leaves open various implementations. I'm fairly sure the node_modules resolution could be described by the exact same API, for example.

So in a sense (and that's definitely not an accident; that's part of the reasons why I made it an API rather than say a JSON file), PnP can be seen as a plugin interface in itself, separate from the way Yarn implements it.

This comment has been minimized.

@arcanis

arcanis Nov 26, 2019
Author

I understand. At the same time this API has now been there for more than a year now and I think we've proved time and again during this period that we're committed to keep developing it - putting special care in our documentation, providing context to our neighbours, providing direct contributions, etc.

PnP will stay - even once the loaders are ready (and I don't expect them to be for a very long time, if ever when it comes to commonjs packages) it will only change how the runtime is injected into the environment. PnP itself will keep the same API, with the pnpapi builtin etc. Adding support for it now doesn't preclude you from supporting more complex schemes down the road, and only unlocks use cases that people care strongly about at what appears a minimal cost - especially considering that me or one of my fellow contributors will always be available to help maintain it.

And of course, it would also simply be a nice and appreciated way to support our team. We believe the PnP resolution will play a large part in Yarn in the future, and TS having builtin support would help us a lot by removing the single main blocker that we can't fix by ourselves. Succeeding alone¹ is hard, hence why we ask for your help.


¹ I'm overdramatising a bit here - we've worked with many projects already, and for example Webpack 5 (and Gatsby / Next / CRA / Jest / ...) will support PnP out of the box. Still, TS is now central enough to many workflows - including ours - that its lack of support adds a significant weight on us.

This comment has been minimized.

@weswigham

weswigham Nov 26, 2019
Member

I'm not saying we don't want to enable this kind of thing, I'm just saying we have little interest in one-off PnP-specific things, when a more complete approach would render it redundant and allow for more use cases to be filled, and that this model, specifically, follows a pattern that we don't consider particularly usable (we should know, it is similar in setup to our existing "language service plugin" architecture that doesn't layer plugins easily, isn't easy to develop for, isn't particularly easy to use, isn't (well) documented, and and was rushed out to support a high pressure request from angular). We've had requests for custom resolver behavior for approximately forever, and would like to do right by those, and not repeat the mistakes we've already made in a different context.

This comment has been minimized.

@alexpomsft

alexpomsft Nov 26, 2019

+1 to solving the bigger custom resolver problem in a good way, that supports multiple resolvers playing nicely with each other. For example, we would like to be able to build a resolver to solve React Native file extension resolution and chain it together with a yarn pnp resolver.

This comment has been minimized.

@arcanis

arcanis Nov 26, 2019
Author

If there's a way to do that, now or in the next months, I'm all for it 👍

My anxiety is mostly that as you mentioned, there have been similar requests for a long time. Plugin systems are notoriously hard to get right, and you understandably want to be sure it won't bite you in the future. That's fine - but meanwhile, we're still left in a state where we have to rely on very creative solutions (a virtual filesystem!) where a simple integration would appear to work fine - within the constraint of our runtime, admittedly.

Anyway, I understand what you're saying and your roof, your rules. I'll be eagerly waiting for progress on #16607 then 🙂

This comment has been minimized.

@Ore4444

Ore4444 Jan 26, 2020

wait, isn't pnpapi just an implementation detail of the compiler? The logic could all be conditional to presence of 'pnpapi' module. When npm would settle on its approach, supported could be added without breaking changes, no?

@sgurenkov
Copy link

@sgurenkov sgurenkov commented Nov 21, 2019

Hopefully we can get support from the TS core team for this one
Lacking native support in TS is the only thing that stops us from migrating to PnP

@arcanis
Copy link
Author

@arcanis arcanis commented Mar 10, 2020

@weswigham fyi the patch-on-install approach we've been following has worked fine so far (kudos to your team btw - even when we see bugs, they aren't that hard to locate and fix).

That being said, the architecture isn't as clean as it could be since we have to prioritize small diffs over sound design (hence why isPnpAvailable is duplicated in three files, for example).

All that to say that I'm still interested to get this PR merged if there's any interest on your side 🙂

@arcanis arcanis changed the title PoC native support for PnP Native support for PnP Mar 10, 2020
@arcanis
Copy link
Author

@arcanis arcanis commented Mar 11, 2020

In fact we didn't have to wait long since the patch doesn't apply cleanly on TS 3.5 ...

@arcanis
Copy link
Author

@arcanis arcanis commented Mar 29, 2020

And TS 3.9 seems to be incompatible as well, which will require another copy of the patch.

@pauldraper
Copy link

@pauldraper pauldraper commented Jun 1, 2020

I'm just saying we have little interest in one-off PnP-specific things, when a more complete approach would render it redundant and allow for more use cases to be filled....We've had requests for custom resolver behavior for approximately forever, and would like to do right by those

+1 to solving the bigger custom resolver problem in a good way, that supports multiple resolvers playing nicely with each other.

But that is PnP: a standardized resolver API, developed by one of the major JavaScript dependency managers and already supported by dozens of the industries most common tools (tsc being a conspicuous exception).

It's being delivered here on a silver platter by @arcanis, but tsc wants(?) to introduce its own standardized resolver API to the mix that is adopted(?) by other tools like Node.js and webpack? And it will have some capability that pnpapi somehow lacks?

How many years will it take for tsc to implement its own "more complete approach," given that it has already been requested "approximately forever."

The NIH is reaching extreme levels.

@typescript-bot
Copy link
Collaborator

@typescript-bot typescript-bot commented Jun 15, 2020

It looks like you've sent a pull request to update our 'lib' files. These files aren't meant to be edited by hand, as they consist of last-known good states of the compiler and are generated from 'src'. Unless this is necessary, consider closing the pull request and sending a separate PR to update 'src'.

@pauldraper
Copy link

@pauldraper pauldraper commented Jul 13, 2020

We've had requests for custom resolver behavior for approximately forever, and would like to do right by those

@weswigham any progress on those requests?

@weswigham
Copy link
Member

@weswigham weswigham commented Jul 13, 2020

We've redone our node factory API for 4.0, which was one of the blockers. We'll possibly see more progress once 4.0 ships.

@arcanis arcanis force-pushed the arcanis:mael/pnp branch from 149d42c to 6ce39c2 Jul 21, 2020
@pauldraper
Copy link

@pauldraper pauldraper commented Jul 22, 2020

We'll possibly see more progress

Is that progress be merging this "moduleResolution": "pnp" PR? Or something else?

Right now, people are patching typescript in postinstall (yarn does this automatically) to get this working; it works, though it's pretty ugly.

Can it possibly be an experimental feature, like --experimentalDecorators was?

@macabeus
Copy link

@macabeus macabeus commented Sep 9, 2020

Any news about that?

@frank-dspeed
Copy link

@frank-dspeed frank-dspeed commented Sep 9, 2020

@macabeus yes i have some news on that typescript needs a resolver plugin interface something that it can call to integrate that there exists already some code that goes into that direction you can expect such integration in the next 5 years i think. If some one puts priority on that it is do able in maybe 1-2 month with 2 people but at present no one did something in that direction.

I also want such features but i am a single person i would need to focus on that. So it has low priority from my side. but if you get me 3 JS coders that want to do it i can lead them into the right direction and do reviews and that.

@arcanis
Copy link
Author

@arcanis arcanis commented Sep 9, 2020

@frank-dspeed I'm sorry but I think your project is quite different from what I'm aiming for in this PR. The goal here is to add support for the PnP resolution in TS itself, so anything tied to a specific bundler isn't an option.

@macabeus No news regarding this PR itself. I still maintain it, and it's still automatically applied when installing TS with Yarn 2, but we haven't heard anything from the TS team for some time. I still hope it'll be merged someday, but I have no control over that.

@SagnikPradhan
Copy link

@SagnikPradhan SagnikPradhan commented Sep 9, 2020

What happened about typescript's very own resolver API?

@frank-dspeed
Copy link

@frank-dspeed frank-dspeed commented Sep 9, 2020

@SagnikPradhan @arcanis there is a new resolve api inside typescript you can use it already to switch between typescript and node resolve via tsconfig this needs to get expanded.

@zkochan
Copy link

@zkochan zkochan commented Sep 28, 2020

Just to underscore the importance of PnP support by TypeScript. pnpm will also have an option to install dependencies using PnP, so soon two Node.js package managers will support PnP.

@arcanis
Copy link
Author

@arcanis arcanis commented Sep 29, 2020

@frank-dspeed Again, this PR is purely to discuss about this PR, not alternative strategies (unless the TS team is the one suggesting them, for obvious reasons). As you can guess a significant amount of work has already been spent evaluating the different options, and there's a reason why it ended up being implemented this way.

Also note that many people are subscribed to this issue - please let's keep the discussion focused.

@llooz
Copy link

@llooz llooz commented Oct 12, 2020

@arcanis we love this patch and Yarn berry, we have an issue when trying to use a custom typeRoots config.
Is there a way to add another typeRoot and keep the pnp default @types import?
Maybe a special value pnp/@types or something like that

@arcanis
Copy link
Author

@arcanis arcanis commented Oct 12, 2020

@weswigham Forgive me for testing out the water again, but pnpm has shipped with PnP in 5.9. Would your team be open to consider that perhaps PnP would make for an acceptable resolver API to support? I'm aware it may not satisfy all your checkboxes, but it's used in production in many projects by now, and despite my best efforts I'm afraid TS support will always be imperfect until your team collaborates directly with us (which we're super open to do, on our side).

@weswigham
Copy link
Member

@weswigham weswigham commented Oct 12, 2020

We're still not willing to call out to arbitrary JS during our execution without a proper plugin model ready, and the "pnp API" is still arbitrary JS code and not a manifest we can directly consume. ❤️

@arcanis
Copy link
Author

@arcanis arcanis commented Oct 12, 2020

the "PnP API" is still arbitrary JS code and not a manifest we can directly consume

Note that the PnP implementation shipped by both Yarn and pnpm supports generating an additional static JSON file (.pnp.data.json) containing the data structures used by the API.

These data structures can be hydrated into a runtime API by either implementing your own logic, or adding a dependency on the @yarnpkg/pnp package (via the makeRuntimeApi method), which you could bundle with TS to remove the dependency from the published artifact. If arbitrary JS code is a problem, there are ways to not have to do it!

@arcanis arcanis force-pushed the arcanis:mael/pnp branch from 58a3860 to 6dbdd2f Oct 23, 2020
@octogonz
Copy link

@octogonz octogonz commented Oct 28, 2020

We're still not willing to call out to arbitrary JS during our execution without a proper plugin model ready

@arcanis I have always been curious why Plug'n'Play is a plugin: Most programming languages besides JavaScript have one standard way of resolving modules. For example, given a Java binary or some Java source code, an analysis tool can easily determine module mappings -- without executing arbitrary scripts that might get stuck in an infinite loop, or leak memory, or maliciously mine bitcoins or whatever.

NPM/Node.js actually demonstrated that many different JavaScript scenarios can all be addressed by a single universal package system and resolver algorithm. Unfortunately the node_modules design was not very efficient or correct, but it was very successful. So if, with hindsight, Yarn and PNPM are now seeking to replace node_modules with a more industrial strength resolver, why not just propose a really great design, and everyone can adopt that?

The flexibility of Plug'n'Play seems to be a disadvantage for adoption: Caches can't be sure what inputs might invalidate the cache. Package authors can't be sure which implementations they should test with. And here the TypeScript maintainers are hesitant about loading arbitrary code into their engine. All those problems would disappear if NPM resolution used a standardized algorithm and some standardized config files -- data instead of code. Actually solving the problem instead of outsourcing the solution to plugin code.

I'm very new to Plug'n'Play though, so sorry if these questions are misguided. :-)

@0rvar
Copy link

@0rvar 0rvar commented Oct 28, 2020

@octogonz Java is a bad example in this case. Try googling "classpath issue" or "jar hell". Not really inspiring.

I agree with many of your other points though, I'd like to hear more motivation for having an executable format for the pnp file.

@arcanis
Copy link
Author

@arcanis arcanis commented Oct 28, 2020

So if, with hindsight, Yarn and PNPM are now seeking to replace node_modules with a more industrial strength resolver, why not just propose a really great design, and everyone can adopt that?

Because as always, the question isn't to have a great design but rather to gather support around it. When you consider that even now the only package manager in Node is controlled by a for-profit and that it takes crazy significant efforts to improve even that, I can't imagine the amount of work that a proposal such as Plug'n'Play would require - and how much of my family time it would consume.

So of course in an ideal world PnP should be implemented in Node, and I'd be open to help with that (or even do most of the work), but it likely won't happen until the Node TSC comes to us with a firm Intent to Implement so that I know it won't be all for nothing. In the meantime, the best I can do is to document the standard and API the best I can and work with third-party project to make "PnP as a plugin" a reality. I think it worked very well so far, even though there's still this kind of PR every once in a while.

I'd like to hear more motivation for having an executable format for the pnp file.

This point in particular got discussed a few times already (for example here), but I think the TS team demonstrated since then why the executable format is just fine: the data format actually got implemented in May 2019 - a year and a half ago - but it didn't do anything to bring PnP closer from TS. Given that it was literally its only purpose, it shows that we should optimize the format for the convenience of our team rather than for theoretical use cases.

@0rvar
Copy link

@0rvar 0rvar commented Oct 28, 2020

@arcanis well, you did not make the data format the default, just opt-in. Until its the default, how could it change anything for TS? TS would still need to execute the pnp js for projects that have not opted for a static data pnp file.

@andreialecu
Copy link

@andreialecu andreialecu commented Oct 28, 2020

@0rvar users who want native TS+PnP support could just opt-in to the data format. The default can be changed later.

@0rvar
Copy link

@0rvar 0rvar commented Oct 28, 2020

@andreialecu Then I guess the PR should change to only work with the data pnp file.

@andreialecu
Copy link

@andreialecu andreialecu commented Oct 28, 2020

If the TS team would commit with a firm Intent to Implement, I'm sure a PR that uses the data format will easily come along. But like @arcanis said:

the data format actually got implemented in May 2019 - a year and a half ago - but it didn't do anything to bring PnP closer from TS. Given that it was literally its only purpose, it shows that we should optimize the format for the convenience of our team rather than for theoretical use cases.

@octogonz
Copy link

@octogonz octogonz commented Oct 28, 2020

@arcanis What if you separated your work into two parts, maybe even with different names:

  • "SmartRequire" [<-- insert better name here]: The proposed spec, which describes a .pnp.data.json data file format, plus some simple psuedocode for how to resolve modules. (Something like All together... from the classic algorithm.)
  • "Plug'n'Play": The temporary hack to make this work in Node.js. In other words, Yarn's .pnp.js is now just a reference implementation of the SmartRequire spec.

If the spec stabilizes and achieves widespread adoption, then one day we won't need Plug'n'Play any more, because Node.js will implement SmartRequire natively.

This would achieve a couple things:

  1. It would clarify that Plug'n'Play is step towards a end goal, not an invitation for every random tool to roll its own unique require() semantics. I personally never realized this, despite being pretty familiar with the package manager scene.
  2. It would give projects like TypeScript a narrowly-defined spec to implement. The important thing for TypeScript to support is the .pnp.data.json data file format. They can code their own implementation (which they always do, because they never bundle NPM dependencies). This avoids any need to eval .pnp.js or even care how Node.js implements SmartRequire.

the best I can do is to document the standard and API the best I can and work with third-party project to make "PnP as a plugin" a reality

The API seems like a third thing. It's not the Plug'n'Play hack. But it's also not something TypeScript would need to implement in order to resolve a module. It seems more like a handy but optional utility for querying the .pnp.data.json data structures.

@arcanis
Copy link
Author

@arcanis arcanis commented Oct 28, 2020

I appreciate suggestions, but this thread is already long and many people are subscribed - it's not the right place to discuss strategy 🙂

Can you repost that on the yarnpkg/berry repository, so that we can keep this PR focused on feedback and updates about this specific implementation? I wouldn't want it to be closed for being off topic 😉

@typescript-bot
Copy link
Collaborator

@typescript-bot typescript-bot commented Dec 9, 2020

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

@pauldraper
Copy link

@pauldraper pauldraper commented Dec 10, 2020

Bot, the reference to #28289 (still open) is in the very first post: #35206 (comment)

@arcanis arcanis force-pushed the arcanis:mael/pnp branch 2 times, most recently from a24be51 to 98889d3 Feb 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet