Skip to content

Conversation

gun-yu
Copy link

@gun-yu gun-yu commented Oct 11, 2025

This PR adds support for PnP resolution.

#1875 in the previous discussion, I have internalized pnp-go into the repository.

Please note that the PR has become quite large due to the inclusion of the PnP source and test code. I apologize for the length and appreciate your understanding.

what is pnp

Yarn Plug’n’Play (PnP) is a dependency resolution system that removes the need for a traditional node_modules folder.
describe at #460

how to support

add pnp resolution config in host

type host struct {
	orchestrator *Orchestrator
	host         compiler.CompilerHost
	// Caches that last only for build cycle and then cleared out
	extendedConfigCache tsc.ExtendedConfigCache
	sourceFiles         parseCache[ast.SourceFileParseOptions, *ast.SourceFile]
	configTimes         collections.SyncMap[tspath.Path, time.Duration]

	// caches that stay as long as they are needed
	resolvedReferences parseCache[tspath.Path, *tsoptions.ParsedCommandLine]
	mTimes             *collections.SyncMap[tspath.Path, time.Time]
	resolvedReferences  parseCache[tspath.Path, *tsoptions.ParsedCommandLine]
	mTimes              *collections.SyncMap[tspath.Path, time.Time]
	pnpResolutionConfig *pnp.ResolutionConfig
}

add pnp branch in resolver.go

// resolveTypeReferenceDirective
// skip typeRoots because PnP knows exactly where each @types is located.
	if r.resolver.pnpResolutionConfig != nil {
		if resolvedFromNearestNodeModulesDirectory := r.loadModuleFromNearestNodeModulesDirectory(true /*typesScopeOnly*/); !resolvedFromNearestNodeModulesDirectory.shouldContinueSearching() {
			return r.createResolvedTypeReferenceDirective(resolvedFromNearestNodeModulesDirectory, true /*primary*/)
		}
	} else {
		if len(typeRoots) > 0 {
			if r.tracer != nil {
				r.tracer.write(diagnostics.Resolving_with_primary_search_path_0.Format(strings.Join(typeRoots, ", ")))
			}
// loadModuleFromNearestNodeModulesDirectory
if r.resolver.pnpResolutionConfig != nil {
			if result := r.loadModuleFromPNP(priorityExtensions, typesScopeOnly); !result.shouldContinueSearching() {
				return result
			}
		} else {
			if result := r.loadModuleFromNearestNodeModulesDirectoryWorker(priorityExtensions, mode, typesScopeOnly); !result.shouldContinueSearching() {
				return result
			}
		}

logger.Log(fmt.Sprintf("ATA:: Installed typings %v", packageNames))
var installedTypingFiles []string
resolver := module.NewResolver(ti.host, &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, "", "")
resolver := module.NewResolver(ti.host, &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, "", "", nil)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: add pnp support later

@gun-yu gun-yu changed the title Feat/add pnp resolver support pnp resolver Oct 11, 2025
packageName, rest := ParsePackageName(moduleName)
packageDirectory := tspath.CombinePaths(nodeModulesDirectory, packageName)

func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensions, candidate string, packageDirectory string, rest string, nodeModulesDirectoryExists bool) *resolved {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I change this function's interface

}

// need fixtures to be yarn install and make global cache
func TestGlobalCache(t *testing.T) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These test cases were taken from pnp-rs, but since they are relatively complex to run and maintain.
I think it would be fine to remove them if they’re not considered necessary.
Please let me know your thoughts!

@gun-yu
Copy link
Author

gun-yu commented Oct 12, 2025

@microsoft-github-policy-service agree

pnpResolutionConfig := TryGetPnpResolutionConfig(currentDirectory)

if pnpResolutionConfig != nil {
fs = pnpvfs.From(fs)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, this way I wouldn’t be able to take advantage of the cached VFS, so I’m thinking of moving its location instead.

@jakebailey
Copy link
Member

You said some of this comes from pnp-rs. Is it a pure port? What is the license of that code? Do you own it? (You signed the CLA, but that may not be enough if it's not yours.)

@gun-yu
Copy link
Author

gun-yu commented Oct 13, 2025

@jakebailey
The original source code, pnp-rs, is licensed under the BSD 2-Clause License. As the ported code is derived from that source, it should also fall under the BSD 2-Clause License — I missed that part, thank you for pointing it out.

As far as I understand, this means I need to include the original author’s license and copyright notice in the comments.

Would this cause any issues? If everything is fine, I’ll go ahead and add the appropriate BSD 2-Clause License notice.

@jakebailey
Copy link
Member

We could pull it, but would need to explicitly declare that dependence in some extra metadata to generate NOTICE.txt (something we have avoided entirely by ensuring all code was written by us from spec or tests, but it's not really different than other deps).

But you definitely need to declare that there are files derived from code under a different license.

@gun-yu
Copy link
Author

gun-yu commented Oct 13, 2025

@jakebailey
Sorry for the late reply. I just want to confirm my understanding—would it be sufficient to directly add a notice for the BSD 2-Clause License of pnp-rs in the NOTICE.txt file?

@jakebailey
Copy link
Member

No, the files containing the code need to have headers with said license. For example, these tests:

// Copyright 2018 Ulf Adams

@gun-yu
Copy link
Author

gun-yu commented Oct 14, 2025

Thank you. I’ve added the license notice as suggested. @jakebailey
b368c1c

Copy link
Member

@jakebailey jakebailey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A "few" comments; my overall worry is that the code style is not so much similar to our code, but maybe that's okay.

(My reviewing it is not really saying yes or no to the PR, we haven't discussed it)

"strings"
)

func NormalizePath(original string) string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think we don't need this sort of thing; the tspath package already defines the way we typically normalize paths, which is already forward slashed and I think compatible with accessing zips.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

305fd49
I replaced normalizePath with tspath.normalizePath.

Comment on lines 8 to 13
var (
reWindowsPath = regexp.MustCompile(`^([a-zA-Z]:.*)$`)
reUNCWindowsPath = regexp.MustCompile(`^[\/\\][\/\\](\.[\/\\])?(.*)$`)
rePortablePath = regexp.MustCompile(`^\/([a-zA-Z]:.*)$`)
reUNCPortablePath = regexp.MustCompile(`^\/unc\/(\.dot\/)?(.*)$`)
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using regexes is definitely not going to be a good idea; these are notoriously slow. I would hope we could avoid needing any of this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

305fd49
I removed this file as normalizePath has been removed.

}

func fromPortablePath(s string) string {
if runtime.GOOS != "windows" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on runtime.GOOS is a bad idea; all of our other path handling is platform independent.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

305fd49
I removed this file as normalizePath has been removed.

Comment on lines +298 to +299
files := make([]string, 0)
dirs := make([]string, 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make([]string, 0) is just an allocate-y version of var files []string; don't intentionally make an empty slice unless nil vs non-nil matters.

module github.com/microsoft/typescript-go

go 1.25
go 1.25.1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please undo this.

return "", false
}

var rePNP = regexp.MustCompile(`(?s)(const[\ \r\n]+RAW_RUNTIME_STATE[\ \r\n]*=[\ \r\n]*|hydrateRuntimeState\(JSON\.parse\()'`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scares me a bit; because of the regex, but also because this reads JS files? Does the spec really require such a thing?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! esbuild uses the JS AST for parsing. Since I was primarily focused on porting pnp-rs, I initially brought the code over as-is, but as you mentioned in another comment, there are definitely some risky areas. I believe the TypeScript Go codebase also includes a JS parser, so I’ll try using that for parsing instead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the official spec, in a Node.js environment, pnp.cjs patches Node’s module resolution, so it’s executed first to override the default behavior. In Go, however, this part needs to be implemented manually.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately, parsing pnp.cjs is necessary to read the metadata, so this part seems unavoidable.
If we parse it using an AST like esbuild does, it might be a bit more reliable — would that be okay?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes more sense to me. Though in general I really thought that PnP had been specified to just emit a plain JSON file...

}

func (r *RegexDef) compile() (*regexp.Regexp, error) {
return regexp.Compile(r.Source)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fail because regex is re2 syntax, not ECMAScript. You'd have to use regexp2 in ECMAScript mode.

That being said, is this really ever used? Does the PnP spec really require regex parsing? (How do esbuild and so on do this without pulling on totally different regex implementations?)

Copy link
Author

@gun-yu gun-yu Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback!
The original ignorePatternData looks like this:

"ignorePatternData": "(^(?:\\\\.yarn\\\\/sdks(?:\\\\/(?!\\\\.{1,2}(?:\\\\/|$))(?:(?:(?!(?:^|\\\\/)\\\\.{1,2}(?:\\\\/|$)).)*?)|$))$)",

Esbuild also compiles and uses the regex in the same way. Since this is part of the Yarn PnP spec, it seems unavoidable to use a regular expression here.

For now, I’ve optimized this commit to avoid the inefficiency of compiling the regex on every use. f0ee55b

@gun-yu
Copy link
Author

gun-yu commented Oct 16, 2025

@jakebailey Thank you for the thoughtful review! I know feedback like this takes a lot of time, and I truly appreciate it. Reading your comments made me realize how unfamiliar I still am with the TypeScript Go codebase—I’m sorry I didn’t reference the existing code more. Consistent code style is especially important in open source, so I’m happy to adopt all of your suggestions. I’ll start by addressing your comments.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants