Skip to content

Commit 2a8a705

Browse files
committed
Implement yarn pnp api
1 parent 4874e98 commit 2a8a705

File tree

3 files changed

+785
-0
lines changed

3 files changed

+785
-0
lines changed

internal/pnp/manifestparser.go

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
package pnp
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/go-json-experiment/json"
9+
10+
"github.com/dlclark/regexp2"
11+
"github.com/microsoft/typescript-go/internal/tspath"
12+
)
13+
14+
type LinkType string
15+
16+
const (
17+
LinkTypeSoft LinkType = "SOFT"
18+
LinkTypeHard LinkType = "HARD"
19+
)
20+
21+
type PackageDependency struct {
22+
Ident string
23+
Reference string // Either the direct reference or alias reference
24+
AliasName string // Empty if not an alias
25+
}
26+
27+
func (pd PackageDependency) IsAlias() bool {
28+
return pd.AliasName != ""
29+
}
30+
31+
type PackageInfo struct {
32+
PackageLocation string `json:"packageLocation"`
33+
PackageDependencies []PackageDependency `json:"packageDependencies,omitempty"`
34+
LinkType LinkType `json:"linkType,omitempty"`
35+
DiscardFromLookup bool `json:"discardFromLookup,omitempty"`
36+
PackagePeers []string `json:"packagePeers,omitempty"`
37+
}
38+
39+
type Locator struct {
40+
Name string `json:"name"`
41+
Reference string `json:"reference"`
42+
}
43+
44+
type FallbackExclusion struct {
45+
Name string `json:"name"`
46+
Entries []string `json:"entries"`
47+
}
48+
49+
type PackageTrieData struct {
50+
ident string
51+
reference string
52+
info *PackageInfo
53+
}
54+
55+
type PackageRegistryTrie struct {
56+
pathSegment string
57+
childrenPathSegments map[string]*PackageRegistryTrie
58+
packageData *PackageTrieData
59+
}
60+
61+
type PnpManifestData struct {
62+
dirPath string
63+
64+
ignorePatternData *regexp2.Regexp
65+
enableTopLevelFallback bool
66+
67+
fallbackPool [][2]string
68+
fallbackExclusionMap map[string]*FallbackExclusion
69+
70+
dependencyTreeRoots []Locator
71+
72+
// Nested maps for package registry (ident -> reference -> PackageInfo)
73+
packageRegistryMap map[string]map[string]*PackageInfo
74+
packageRegistryTrie *PackageRegistryTrie
75+
}
76+
77+
func parseManifestFromPath(fs PnpApiFS, manifestDir string) (*PnpManifestData, error) {
78+
pnpDataString := ""
79+
80+
data, ok := fs.ReadFile(tspath.CombinePaths(manifestDir, ".pnp.data.json"))
81+
if ok {
82+
pnpDataString = data
83+
} else {
84+
pnpScriptString, ok := fs.ReadFile(tspath.CombinePaths(manifestDir, ".pnp.cjs"))
85+
if !ok {
86+
return nil, errors.New("failed to read .pnp.cjs file")
87+
}
88+
89+
manifestRegex := regexp2.MustCompile(`(const[ \r\n]+RAW_RUNTIME_STATE[ \r\n]*=[ \r\n]*|hydrateRuntimeState\(JSON\.parse\()'`, regexp2.None)
90+
matches, err := manifestRegex.FindStringMatch(pnpScriptString)
91+
if err != nil || matches == nil {
92+
return nil, errors.New("We failed to locate the PnP data payload inside its manifest file. Did you manually edit the file?")
93+
}
94+
95+
start := matches.Index + matches.Length
96+
var b strings.Builder
97+
b.Grow(len(pnpScriptString))
98+
for i := start; i < len(pnpScriptString); i++ {
99+
if pnpScriptString[i] == '\'' {
100+
break
101+
}
102+
103+
if pnpScriptString[i] != '\\' {
104+
b.WriteByte(pnpScriptString[i])
105+
}
106+
}
107+
pnpDataString = b.String()
108+
}
109+
110+
return parseManifestFromData(pnpDataString, manifestDir)
111+
}
112+
113+
func parseManifestFromData(pnpDataString string, manifestDir string) (*PnpManifestData, error) {
114+
var rawData map[string]interface{}
115+
if err := json.Unmarshal([]byte(pnpDataString), &rawData); err != nil {
116+
return nil, fmt.Errorf("failed to parse JSON PnP data: %w", err)
117+
}
118+
119+
pnpData, err := parsePnpManifest(rawData, manifestDir)
120+
if err != nil {
121+
return nil, fmt.Errorf("failed to parse PnP data: %w", err)
122+
}
123+
124+
return pnpData, nil
125+
}
126+
127+
// TODO add error handling for corrupted data
128+
func parsePnpManifest(rawData map[string]interface{}, manifestDir string) (*PnpManifestData, error) {
129+
data := &PnpManifestData{dirPath: manifestDir}
130+
131+
if roots, ok := rawData["dependencyTreeRoots"].([]interface{}); ok {
132+
for _, root := range roots {
133+
if rootMap, ok := root.(map[string]interface{}); ok {
134+
data.dependencyTreeRoots = append(data.dependencyTreeRoots, Locator{
135+
Name: getField(rootMap, "name", parseString),
136+
Reference: getField(rootMap, "reference", parseString),
137+
})
138+
}
139+
}
140+
}
141+
142+
ignorePatternData := getField(rawData, "ignorePatternData", parseString)
143+
if ignorePatternData != "" {
144+
ignorePatternDataRegexp, err := regexp2.Compile(ignorePatternData, regexp2.None)
145+
if err != nil {
146+
return nil, fmt.Errorf("failed to compile ignore pattern data: %w", err)
147+
}
148+
149+
data.ignorePatternData = ignorePatternDataRegexp
150+
}
151+
152+
data.enableTopLevelFallback = getField(rawData, "enableTopLevelFallback", parseBool)
153+
154+
data.fallbackPool = getField(rawData, "fallbackPool", parseStringPairs)
155+
156+
data.fallbackExclusionMap = make(map[string]*FallbackExclusion)
157+
158+
if exclusions, ok := rawData["fallbackExclusionList"].([]interface{}); ok {
159+
for _, exclusion := range exclusions {
160+
if exclusionArr, ok := exclusion.([]interface{}); ok && len(exclusionArr) == 2 {
161+
name := parseString(exclusionArr[0])
162+
entries := parseStringArray(exclusionArr[1])
163+
exclusionEntry := &FallbackExclusion{
164+
Name: name,
165+
Entries: entries,
166+
}
167+
data.fallbackExclusionMap[exclusionEntry.Name] = exclusionEntry
168+
}
169+
}
170+
}
171+
172+
data.packageRegistryMap = make(map[string]map[string]*PackageInfo)
173+
174+
if registryData, ok := rawData["packageRegistryData"].([]interface{}); ok {
175+
for _, entry := range registryData {
176+
if entryArr, ok := entry.([]interface{}); ok && len(entryArr) == 2 {
177+
ident := parseString(entryArr[0])
178+
179+
if data.packageRegistryMap[ident] == nil {
180+
data.packageRegistryMap[ident] = make(map[string]*PackageInfo)
181+
}
182+
183+
if versions, ok := entryArr[1].([]interface{}); ok {
184+
for _, version := range versions {
185+
if versionArr, ok := version.([]interface{}); ok && len(versionArr) == 2 {
186+
reference := parseString(versionArr[0])
187+
188+
if infoMap, ok := versionArr[1].(map[string]interface{}); ok {
189+
packageInfo := &PackageInfo{
190+
PackageLocation: getField(infoMap, "packageLocation", parseString),
191+
PackageDependencies: getField(infoMap, "packageDependencies", parsePackageDependencies),
192+
LinkType: LinkType(getField(infoMap, "linkType", parseString)),
193+
DiscardFromLookup: getField(infoMap, "discardFromLookup", parseBool),
194+
PackagePeers: getField(infoMap, "packagePeers", parseStringArray),
195+
}
196+
197+
data.packageRegistryMap[ident][reference] = packageInfo
198+
data.addPackageToTrie(ident, reference, packageInfo)
199+
}
200+
}
201+
}
202+
}
203+
}
204+
}
205+
}
206+
207+
return data, nil
208+
}
209+
210+
func (data *PnpManifestData) addPackageToTrie(ident string, reference string, packageInfo *PackageInfo) {
211+
if data.packageRegistryTrie == nil {
212+
data.packageRegistryTrie = &PackageRegistryTrie{
213+
pathSegment: "",
214+
childrenPathSegments: make(map[string]*PackageRegistryTrie),
215+
packageData: nil,
216+
}
217+
}
218+
219+
packageData := &PackageTrieData{
220+
ident: ident,
221+
reference: reference,
222+
info: packageInfo,
223+
}
224+
225+
packagePath := tspath.RemoveTrailingDirectorySeparator(packageInfo.PackageLocation)
226+
packagePathSegments := strings.Split(packagePath, "/")
227+
228+
currentTrie := data.packageRegistryTrie
229+
230+
for _, segment := range packagePathSegments {
231+
if currentTrie.childrenPathSegments[segment] == nil {
232+
currentTrie.childrenPathSegments[segment] = &PackageRegistryTrie{
233+
pathSegment: segment,
234+
childrenPathSegments: make(map[string]*PackageRegistryTrie),
235+
packageData: nil,
236+
}
237+
}
238+
239+
currentTrie = currentTrie.childrenPathSegments[segment]
240+
}
241+
242+
currentTrie.packageData = packageData
243+
}
244+
245+
// Helper functions for parsing JSON values - following patterns from tsoptions.parseString, etc.
246+
func parseString(value interface{}) string {
247+
if str, ok := value.(string); ok {
248+
return str
249+
}
250+
return ""
251+
}
252+
253+
func parseBool(value interface{}) bool {
254+
if val, ok := value.(bool); ok {
255+
return val
256+
}
257+
return false
258+
}
259+
260+
func parseStringArray(value interface{}) []string {
261+
if arr, ok := value.([]interface{}); ok {
262+
if arr == nil {
263+
return nil
264+
}
265+
result := make([]string, 0, len(arr))
266+
for _, v := range arr {
267+
if str, ok := v.(string); ok {
268+
result = append(result, str)
269+
}
270+
}
271+
return result
272+
}
273+
return nil
274+
}
275+
276+
func parseStringPairs(value interface{}) [][2]string {
277+
var result [][2]string
278+
if arr, ok := value.([]interface{}); ok {
279+
for _, item := range arr {
280+
if pair, ok := item.([]interface{}); ok && len(pair) == 2 {
281+
result = append(result, [2]string{
282+
parseString(pair[0]),
283+
parseString(pair[1]),
284+
})
285+
}
286+
}
287+
}
288+
return result
289+
}
290+
291+
func parsePackageDependencies(value interface{}) []PackageDependency {
292+
var result []PackageDependency
293+
if arr, ok := value.([]interface{}); ok {
294+
for _, item := range arr {
295+
if pair, ok := item.([]interface{}); ok && len(pair) == 2 {
296+
ident := parseString(pair[0])
297+
298+
// Check if second element is string (simple reference) or array (alias)
299+
if str, ok := pair[1].(string); ok {
300+
result = append(result, PackageDependency{
301+
Ident: ident,
302+
Reference: str,
303+
AliasName: "",
304+
})
305+
} else if aliasPair, ok := pair[1].([]interface{}); ok && len(aliasPair) == 2 {
306+
result = append(result, PackageDependency{
307+
Ident: ident,
308+
Reference: parseString(aliasPair[1]),
309+
AliasName: parseString(aliasPair[0]),
310+
})
311+
}
312+
}
313+
}
314+
}
315+
return result
316+
}
317+
318+
func getField[T any](m map[string]interface{}, key string, parser func(interface{}) T) T {
319+
if val, exists := m[key]; exists {
320+
return parser(val)
321+
}
322+
var zero T
323+
return zero
324+
}

0 commit comments

Comments
 (0)