Skip to content

[Case Study] Perfect TypeScript Integration #589

Open
@arcanis

Description

@arcanis

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:

image


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.

Metadata

Metadata

Assignees

Labels

case studyPackage compatibility report

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions