aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/fs/package_json.go
blob: 883f7a4ac7df008097cf5e50e79da637b0181c41 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package fs

import (
	"bytes"
	"encoding/json"
	"sync"

	"github.com/vercel/turbo/cli/internal/lockfile"
	"github.com/vercel/turbo/cli/internal/turbopath"
)

// PackageJSON represents NodeJS package.json
type PackageJSON struct {
	Name                 string            `json:"name"`
	Version              string            `json:"version"`
	Scripts              map[string]string `json:"scripts"`
	Dependencies         map[string]string `json:"dependencies"`
	DevDependencies      map[string]string `json:"devDependencies"`
	OptionalDependencies map[string]string `json:"optionalDependencies"`
	PeerDependencies     map[string]string `json:"peerDependencies"`
	PackageManager       string            `json:"packageManager"`
	Os                   []string          `json:"os"`
	Workspaces           Workspaces        `json:"workspaces"`
	Private              bool              `json:"private"`
	// Exact JSON object stored in package.json including unknown fields
	// During marshalling struct fields will take priority over raw fields
	RawJSON map[string]interface{} `json:"-"`

	// relative path from repo root to the package.json file
	PackageJSONPath turbopath.AnchoredSystemPath `json:"-"`
	// relative path from repo root to the package
	Dir                    turbopath.AnchoredSystemPath `json:"-"`
	InternalDeps           []string                     `json:"-"`
	UnresolvedExternalDeps map[string]string            `json:"-"`
	TransitiveDeps         []lockfile.Package           `json:"-"`
	LegacyTurboConfig      *TurboJSON                   `json:"turbo"`
	Mu                     sync.Mutex                   `json:"-"`
	ExternalDepsHash       string                       `json:"-"`
}

type Workspaces []string

type WorkspacesAlt struct {
	Packages []string `json:"packages,omitempty"`
}

func (r *Workspaces) UnmarshalJSON(data []byte) error {
	var tmp = &WorkspacesAlt{}
	if err := json.Unmarshal(data, tmp); err == nil {
		*r = Workspaces(tmp.Packages)
		return nil
	}
	var tempstr = []string{}
	if err := json.Unmarshal(data, &tempstr); err != nil {
		return err
	}
	*r = tempstr
	return nil
}

// ReadPackageJSON returns a struct of package.json
func ReadPackageJSON(path turbopath.AbsoluteSystemPath) (*PackageJSON, error) {
	b, err := path.ReadFile()
	if err != nil {
		return nil, err
	}
	return UnmarshalPackageJSON(b)
}

// UnmarshalPackageJSON decodes a byte slice into a PackageJSON struct
func UnmarshalPackageJSON(data []byte) (*PackageJSON, error) {
	var rawJSON map[string]interface{}
	if err := json.Unmarshal(data, &rawJSON); err != nil {
		return nil, err
	}

	pkgJSON := &PackageJSON{}
	if err := json.Unmarshal(data, &pkgJSON); err != nil {
		return nil, err
	}
	pkgJSON.RawJSON = rawJSON

	return pkgJSON, nil
}

// MarshalPackageJSON Serialize PackageJSON to a slice of bytes
func MarshalPackageJSON(pkgJSON *PackageJSON) ([]byte, error) {
	structuredContent, err := json.Marshal(pkgJSON)
	if err != nil {
		return nil, err
	}
	var structuredFields map[string]interface{}
	if err := json.Unmarshal(structuredContent, &structuredFields); err != nil {
		return nil, err
	}

	fieldsToSerialize := make(map[string]interface{}, len(pkgJSON.RawJSON))

	// copy pkgJSON.RawJSON
	for key, value := range pkgJSON.RawJSON {
		fieldsToSerialize[key] = value
	}

	for key, value := range structuredFields {
		if isEmpty(value) {
			delete(fieldsToSerialize, key)
		} else {
			fieldsToSerialize[key] = value
		}
	}

	var b bytes.Buffer
	encoder := json.NewEncoder(&b)
	encoder.SetEscapeHTML(false)
	encoder.SetIndent("", "  ")
	if err := encoder.Encode(fieldsToSerialize); err != nil {
		return nil, err
	}

	return b.Bytes(), nil
}

func isEmpty(value interface{}) bool {
	if value == nil {
		return true
	}
	switch s := value.(type) {
	case string:
		return s == ""
	case bool:
		return !s
	case []string:
		return len(s) == 0
	case map[string]interface{}:
		return len(s) == 0
	case Workspaces:
		return len(s) == 0
	default:
		// Assume any unknown types aren't empty
		return false
	}
}