Description
What package is covered by this investigations?
TypeScript
Describe the goal of the investigation
We currently support TypeScript through PnPify, but this isn't as good as a real integration - we need to emulate a whole filesystem with all its intricacies, it changes the command line (yarn pnpify tsc
instead of tsc
), disables some of PnP's features (such as semantic errors)...
This investigation's goal is to figure out all the blockers that prevent TypeScript from working with Yarn 2 so that we can provide a clear answer when asked what we would be needing - and potentially open issues and PRs against TypeScript to help this effort.
Investigation report
resolveModuleName
/ resolveTypeReferenceDirective
Those two passes already include hooks that allow to override their typical mechanism - which is what we've actually done in ts-pnp
.
My main wish would be for those hooks to be builtin to TypeScript, as they aren't particularly big. To give you an idea, ts-pnp
is 80 lines big with a redundant logic. The main issue is that it would involve loading the .pnp.js
data in some capacity, and the TS team has historically been reluctant. I think an easy way to solve this is to simply call require('pnpapi')
instead of directly requiring the .pnp.js
file.
I believe this solution would work because since the pnpapi
module only exists when PnP is loaded (so not when running tsc
directly, and neither when running the TS that's embed within VSCode), it means that it'll be up to the user to explicitly request to use a modified environment. In the case of VSCode it means they'll have to explicitly switch to the Workspace version:
fileExists
/ directoryExists
/ readFile
/ realpath
Now that Yarn 2 is keeping the files within zip archives, accessing them from within TS requires some extra work as we would need TS to understand that accessing /foo/bar.zip/index.js
would actually mean accessing /index.js
within the archive /foo/bar.zip
.
This part is likely the one most likely to receive pushback from the TS team, as its implementation can potentially involve adding a large logic to the TS core that may not be useful to all their users. That being said, I think a similar approach to the one detailed above could work as well: since the PnP file generated by Yarn is able to understand those zip paths, it could easily also provide an interface that could be used to implement the functions detailed above. After that, TS could implement fileExists
using something similar to:
ts.effectiveFileExists = p => {
if (!process.versions.pnp)
return ts.fileExists(p);
const pnp = require(`pnpapi`);
if (!pnp.fsExtension)
return ts.fileExists(p);
return pnp.fsExtension.existsSync(p);
};
Similar functions might be readDirectory
/ getFileSize
/ getModifiedTime
, but since they aren't in ModuleResolutionHost
I'm not 100% sure that they really matter in this context.
getAutomaticTypeDirectiveNames
This function is called by createProgram
in order to figure out all the @types
packages that should be loaded. The problem is that getAutomaticTypeDirectiveNames
calls getEffectiveTypeRoots
, which in turns calls getDefaultTypeRoots
, which in turns hardcodes an access to <project>/node_modules/@types
. We never get the chance to change this behaviour to indicate to TS that the @types
are stored in a separate location (or, in our case, multiple separate locations).
A proposal would be to add a getAutomaticTypeRoots(currentDirectory, host)
to the CompilerHost
interface which would be called by getEffectiveTypeRoots
. By default it would just call getDefaultTypeRoots
, but an environment like PnP could override it to instead do something similar to this - eliminating I/O calls and making TS faster in the process:
const pnp = require(`pnpapi`);
const locator = pnp.findPackageLocator(`${currentDirectory}/`);
const {packageDependencies} = pnp.getPackageInformation(locator);
const typeRoots = [];
for (const [name, referencish] of packageDependencies.entries()) {
if (name.startsWith(`@types/`) && referencish !== null) {
const dependencyLocator = pnp.toLocator(name, referencish);
const {packageLocation} = pnp.getPackageInformation(dependencyLocator);
typeRoots.push(packageLocation);
}
}
getExtendsConfigPath
This function makes an hardcoded call to nodeModuleNameResolver
, which prevents the host compiler from kicking in (the rational probably being that at this point the project options haven't been decided yet, and since the resolver takes them in parameter it's unclear how that should work).
I'm not sure what the TS teams would prefer - maybe a compiler host just for this purpose? That would avoid having to specify the project options as nullable, breaking existing resolveModuleName
implementations.