Skip to content

Commit 2ae52e9

Browse files
committed
Implement yarn pnp api
1 parent 179ff49 commit 2ae52e9

File tree

2 files changed

+354
-0
lines changed

2 files changed

+354
-0
lines changed

internal/pnp/pnp.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package pnp
2+
3+
func InitPnpApi(fs PnpApiFS, filePath string) *PnpApi {
4+
pnpApi := &PnpApi{fs: fs, url: filePath}
5+
6+
manifestData, err := pnpApi.findClosestPnpManifest()
7+
if err == nil {
8+
pnpApi.manifest = manifestData
9+
return pnpApi
10+
}
11+
12+
return nil
13+
}

internal/pnp/pnpapi.go

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
package pnp
2+
3+
/*
4+
* Yarn Plug'n'Play (generally referred to as Yarn PnP) is the default installation strategy in modern releases of Yarn.
5+
* Yarn PnP generates a single Node.js loader file in place of the typical node_modules folder.
6+
* This loader file, named .pnp.cjs, contains all information about your project's dependency tree, informing your tools as to
7+
* the location of the packages on the disk and letting them know how to resolve require and import calls.
8+
*
9+
* The full specification is available at https://yarnpkg.com/advanced/pnp-spec
10+
*/
11+
12+
import (
13+
"errors"
14+
"fmt"
15+
"strings"
16+
17+
"github.com/microsoft/typescript-go/internal/tspath"
18+
)
19+
20+
type PnpApi struct {
21+
fs PnpApiFS
22+
url string
23+
manifest *PnpManifestData
24+
}
25+
26+
// FS abstraction used by the PnpApi to access the file system
27+
// We can't use the vfs.FS interface because it creates an import cycle: core -> pnp -> vfs -> core
28+
type PnpApiFS interface {
29+
FileExists(path string) bool
30+
ReadFile(path string) (contents string, ok bool)
31+
}
32+
33+
func (p *PnpApi) RefreshManifest() error {
34+
var newData *PnpManifestData
35+
var err error
36+
37+
if p.manifest == nil {
38+
newData, err = p.findClosestPnpManifest()
39+
} else {
40+
newData, err = parseManifestFromPath(p.fs, p.manifest.dirPath)
41+
}
42+
43+
if err != nil {
44+
return err
45+
}
46+
47+
p.manifest = newData
48+
return nil
49+
}
50+
51+
func (p *PnpApi) ResolveToUnqualified(specifier string, parentPath string) (string, error) {
52+
if p.manifest == nil {
53+
panic("ResolveToUnqualified called with no PnP manifest available")
54+
}
55+
56+
ident, modulePath, err := p.ParseBareIdentifier(specifier)
57+
if err != nil {
58+
// Skipping resolution
59+
return "", nil
60+
}
61+
62+
parentLocator, err := p.FindLocator(parentPath)
63+
if err != nil || parentLocator == nil {
64+
// Skipping resolution
65+
return "", nil
66+
}
67+
68+
parentPkg := p.GetPackage(parentLocator)
69+
70+
var referenceOrAlias *PackageDependency
71+
for _, dep := range parentPkg.PackageDependencies {
72+
if dep.Ident == ident {
73+
referenceOrAlias = &dep
74+
break
75+
}
76+
}
77+
78+
// If not found, try fallback if enabled
79+
if referenceOrAlias == nil {
80+
if p.manifest.enableTopLevelFallback {
81+
excluded := false
82+
if exclusion, ok := p.manifest.fallbackExclusionMap[parentLocator.Name]; ok {
83+
for _, entry := range exclusion.Entries {
84+
if entry == parentLocator.Reference {
85+
excluded = true
86+
break
87+
}
88+
}
89+
}
90+
if !excluded {
91+
fallback := p.ResolveViaFallback(ident)
92+
if fallback != nil {
93+
referenceOrAlias = fallback
94+
}
95+
}
96+
}
97+
}
98+
99+
// undeclared dependency
100+
if referenceOrAlias == nil {
101+
if parentLocator.Name == "" {
102+
return "", fmt.Errorf("Your application tried to access %s, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: %s\nRequired by: %s", ident, ident, parentPath)
103+
}
104+
return "", fmt.Errorf("%s tried to access %s, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound.\n\nRequired package: %s\nRequired by: %s", parentLocator.Name, ident, ident, parentPath)
105+
}
106+
107+
// unfulfilled peer dependency
108+
if !referenceOrAlias.IsAlias() && referenceOrAlias.Reference == "" {
109+
if parentLocator.Name == "" {
110+
return "", fmt.Errorf("Your application tried to access %s (a peer dependency); this isn't allowed as there is no ancestor to satisfy the requirement. Use a devDependency if needed.\n\nRequired package: %s\nRequired by: %s", ident, ident, parentPath)
111+
}
112+
return "", fmt.Errorf("%s tried to access %s (a peer dependency) but it isn't provided by its ancestors/your application; this makes the require call ambiguous and unsound.\n\nRequired package: %s\nRequired by: %s", parentLocator.Name, ident, ident, parentPath)
113+
}
114+
115+
var dependencyPkg *PackageInfo
116+
if referenceOrAlias.IsAlias() {
117+
dependencyPkg = p.GetPackage(&Locator{Name: referenceOrAlias.AliasName, Reference: referenceOrAlias.Reference})
118+
} else {
119+
dependencyPkg = p.GetPackage(&Locator{Name: referenceOrAlias.Ident, Reference: referenceOrAlias.Reference})
120+
}
121+
122+
return tspath.ResolvePath(p.manifest.dirPath, dependencyPkg.PackageLocation, modulePath), nil
123+
}
124+
125+
func (p *PnpApi) findClosestPnpManifest() (*PnpManifestData, error) {
126+
directoryPath := tspath.GetNormalizedAbsolutePath(p.url, "/")
127+
128+
for {
129+
pnpPath := tspath.CombinePaths(directoryPath, ".pnp.cjs")
130+
if p.fs.FileExists(pnpPath) {
131+
return parseManifestFromPath(p.fs, directoryPath)
132+
}
133+
134+
if tspath.IsDiskPathRoot(directoryPath) {
135+
return nil, errors.New("no PnP manifest found")
136+
}
137+
138+
directoryPath = tspath.GetDirectoryPath(directoryPath)
139+
}
140+
}
141+
142+
func (p *PnpApi) GetPackage(locator *Locator) *PackageInfo {
143+
packageRegistryMap := p.manifest.packageRegistryMap
144+
packageInfo, ok := packageRegistryMap[locator.Name][locator.Reference]
145+
if !ok {
146+
panic(locator.Name + " should have an entry in the package registry")
147+
}
148+
149+
return packageInfo
150+
}
151+
152+
func (p *PnpApi) FindLocator(parentPath string) (*Locator, error) {
153+
relativePath := tspath.GetRelativePathFromDirectory(p.manifest.dirPath, parentPath,
154+
tspath.ComparePathsOptions{UseCaseSensitiveFileNames: true})
155+
156+
if p.manifest.ignorePatternData != nil {
157+
match, err := p.manifest.ignorePatternData.MatchString(relativePath)
158+
if err != nil {
159+
return nil, err
160+
}
161+
162+
if match {
163+
return nil, nil
164+
}
165+
}
166+
167+
var relativePathWithDot string
168+
if strings.HasPrefix(relativePath, "../") {
169+
relativePathWithDot = relativePath
170+
} else {
171+
relativePathWithDot = "./" + relativePath
172+
}
173+
174+
var bestLength int
175+
var bestLocator *Locator
176+
pathSegments := strings.Split(relativePathWithDot, "/")
177+
currentTrie := p.manifest.packageRegistryTrie
178+
179+
// Go down the trie, looking for the latest defined packageInfo that matches the path
180+
for index, segment := range pathSegments {
181+
currentTrie = currentTrie.childrenPathSegments[segment]
182+
183+
if currentTrie == nil || currentTrie.childrenPathSegments == nil {
184+
break
185+
}
186+
187+
if currentTrie.packageData != nil && index >= bestLength {
188+
bestLength = index
189+
bestLocator = &Locator{Name: currentTrie.packageData.ident, Reference: currentTrie.packageData.reference}
190+
}
191+
}
192+
193+
if bestLocator == nil {
194+
return nil, fmt.Errorf("no package found for path %s", relativePath)
195+
}
196+
197+
return bestLocator, nil
198+
}
199+
200+
func (p *PnpApi) ResolveViaFallback(name string) *PackageDependency {
201+
topLevelPkg := p.GetPackage(&Locator{Name: "", Reference: ""})
202+
203+
if topLevelPkg != nil {
204+
for _, dep := range topLevelPkg.PackageDependencies {
205+
if dep.Ident == name {
206+
return &dep
207+
}
208+
}
209+
}
210+
211+
for _, dep := range p.manifest.fallbackPool {
212+
if dep[0] == name {
213+
return &PackageDependency{
214+
Ident: dep[0],
215+
Reference: dep[1],
216+
AliasName: "",
217+
}
218+
}
219+
}
220+
221+
return nil
222+
}
223+
224+
func (p *PnpApi) ParseBareIdentifier(specifier string) (ident string, modulePath string, err error) {
225+
if len(specifier) == 0 {
226+
return "", "", fmt.Errorf("Empty specifier: %s", specifier)
227+
}
228+
229+
firstSlash := strings.Index(specifier, "/")
230+
231+
if specifier[0] == '@' {
232+
if firstSlash == -1 {
233+
return "", "", fmt.Errorf("Invalid specifier: %s", specifier)
234+
}
235+
236+
secondSlash := strings.Index(specifier[firstSlash+1:], "/")
237+
238+
if secondSlash == -1 {
239+
ident = specifier
240+
} else {
241+
ident = specifier[:firstSlash+1+secondSlash]
242+
}
243+
} else {
244+
firstSlash := strings.Index(specifier, "/")
245+
246+
if firstSlash == -1 {
247+
ident = specifier
248+
} else {
249+
ident = specifier[:firstSlash]
250+
}
251+
}
252+
253+
modulePath = specifier[len(ident):]
254+
255+
return ident, modulePath, nil
256+
}
257+
258+
func (p *PnpApi) GetPnpTypeRoots(currentDirectory string) []string {
259+
if p.manifest == nil {
260+
return []string{}
261+
}
262+
263+
currentDirectory = tspath.NormalizePath(currentDirectory)
264+
265+
currentPackage, err := p.FindLocator(currentDirectory)
266+
if err != nil {
267+
return []string{}
268+
}
269+
270+
if currentPackage == nil {
271+
return []string{}
272+
}
273+
274+
packageDependencies := p.GetPackage(currentPackage).PackageDependencies
275+
276+
typeRoots := []string{}
277+
for _, dep := range packageDependencies {
278+
if strings.HasPrefix(dep.Ident, "@types/") && dep.Reference != "" {
279+
packageInfo := p.GetPackage(&Locator{Name: dep.Ident, Reference: dep.Reference})
280+
typeRoots = append(typeRoots, tspath.GetDirectoryPath(
281+
tspath.ResolvePath(p.manifest.dirPath, packageInfo.PackageLocation),
282+
))
283+
}
284+
}
285+
286+
return typeRoots
287+
}
288+
289+
func (p *PnpApi) IsImportable(fromFileName string, toFileName string) bool {
290+
fromLocator, errFromLocator := p.FindLocator(fromFileName)
291+
toLocator, errToLocator := p.FindLocator(toFileName)
292+
293+
if fromLocator == nil || toLocator == nil || errFromLocator != nil || errToLocator != nil {
294+
return false
295+
}
296+
297+
fromInfo := p.GetPackage(fromLocator)
298+
for _, dep := range fromInfo.PackageDependencies {
299+
if dep.Reference == toLocator.Reference {
300+
if dep.IsAlias() && dep.AliasName == toLocator.Name {
301+
return true
302+
}
303+
304+
if dep.Ident == toLocator.Name {
305+
return true
306+
}
307+
}
308+
}
309+
310+
return false
311+
}
312+
313+
func (p *PnpApi) GetPackageLocationAbsolutePath(packageInfo *PackageInfo) string {
314+
if packageInfo == nil {
315+
return ""
316+
}
317+
318+
packageLocation := packageInfo.PackageLocation
319+
return tspath.ResolvePath(p.manifest.dirPath, packageLocation)
320+
}
321+
322+
func (p *PnpApi) IsInPnpModule(fromFileName string, toFileName string) bool {
323+
fromLocator, _ := p.FindLocator(fromFileName)
324+
toLocator, _ := p.FindLocator(toFileName)
325+
// The targeted filename is in a pnp module different from the requesting filename
326+
return fromLocator != nil && toLocator != nil && fromLocator.Name != toLocator.Name
327+
}
328+
329+
func (p *PnpApi) AppendPnpTypeRoots(nmTypes []string, baseDir string, nmFromConfig bool) ([]string, bool) {
330+
pnpTypes := p.GetPnpTypeRoots(baseDir)
331+
332+
if len(nmTypes) > 0 {
333+
return append(nmTypes, pnpTypes...), nmFromConfig
334+
}
335+
336+
if len(pnpTypes) > 0 {
337+
return pnpTypes, false
338+
}
339+
340+
return nil, false
341+
}

0 commit comments

Comments
 (0)