api_nodejs.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. package buildpacks
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "strings"
  7. "sync"
  8. "github.com/Masterminds/semver/v3"
  9. "github.com/google/go-github/github"
  10. )
  11. var (
  12. lts = map[string]int{
  13. "argon": 4,
  14. "boron": 6,
  15. "carbon": 8,
  16. "dubnium": 10,
  17. }
  18. )
  19. type apiNodeRuntime struct {
  20. ghClient *github.Client
  21. wg sync.WaitGroup
  22. }
  23. func NewAPINodeRuntime(client *github.Client) *apiNodeRuntime {
  24. return &apiNodeRuntime{
  25. ghClient: client,
  26. }
  27. }
  28. func (runtime *apiNodeRuntime) detectYarn(results chan struct {
  29. string
  30. bool
  31. }, directoryContent []*github.RepositoryContent) {
  32. yarnLockFound := false
  33. packageJSONFound := false
  34. for i := 0; i < len(directoryContent); i++ {
  35. name := directoryContent[i].GetName()
  36. if name == "yarn.lock" {
  37. yarnLockFound = true
  38. } else if name == "package.json" {
  39. packageJSONFound = true
  40. }
  41. if yarnLockFound && packageJSONFound {
  42. break
  43. }
  44. }
  45. if yarnLockFound && packageJSONFound {
  46. results <- struct {
  47. string
  48. bool
  49. }{yarn, true}
  50. } else {
  51. results <- struct {
  52. string
  53. bool
  54. }{yarn, false}
  55. }
  56. runtime.wg.Done()
  57. }
  58. func (runtime *apiNodeRuntime) detectNPM(results chan struct {
  59. string
  60. bool
  61. }, directoryContent []*github.RepositoryContent) {
  62. packageJSONFound := false
  63. for i := 0; i < len(directoryContent); i++ {
  64. name := directoryContent[i].GetName()
  65. if name == "package.json" {
  66. packageJSONFound = true
  67. break
  68. }
  69. }
  70. if packageJSONFound {
  71. results <- struct {
  72. string
  73. bool
  74. }{npm, true}
  75. } else {
  76. results <- struct {
  77. string
  78. bool
  79. }{npm, false}
  80. }
  81. runtime.wg.Done()
  82. }
  83. func (runtime *apiNodeRuntime) detectStandalone(results chan struct {
  84. string
  85. bool
  86. }, directoryContent []*github.RepositoryContent) {
  87. jsFileFound := false
  88. for i := 0; i < len(directoryContent); i++ {
  89. name := directoryContent[i].GetName()
  90. if name == "server.js" || name == "app.js" || name == "main.js" || name == "index.js" {
  91. jsFileFound = true
  92. break
  93. }
  94. }
  95. if jsFileFound {
  96. results <- struct {
  97. string
  98. bool
  99. }{standalone, true}
  100. } else {
  101. results <- struct {
  102. string
  103. bool
  104. }{standalone, false}
  105. }
  106. runtime.wg.Done()
  107. }
  108. // copied directly from https://github.com/paketo-buildpacks/node-engine/blob/main/nvmrc_parser.go
  109. func validateNvmrc(content string) (string, error) {
  110. content = strings.TrimSpace(strings.ToLower(content))
  111. if content == "lts/*" || content == "node" {
  112. return content, nil
  113. }
  114. for key := range lts {
  115. if content == strings.ToLower("lts/"+key) {
  116. return content, nil
  117. }
  118. }
  119. content = strings.TrimPrefix(content, "v")
  120. if _, err := semver.NewConstraint(content); err != nil {
  121. return "", fmt.Errorf("invalid version constraint specified in .nvmrc: %q", content)
  122. }
  123. return content, nil
  124. }
  125. // copied directly from https://github.com/paketo-buildpacks/node-engine/blob/main/nvmrc_parser.go
  126. func formatNvmrcContent(version string) string {
  127. if version == "node" {
  128. return "*"
  129. }
  130. if strings.HasPrefix(version, "lts") {
  131. ltsName := strings.SplitN(version, "/", 2)[1]
  132. if ltsName == "*" {
  133. var maxVersion int
  134. for _, versionValue := range lts {
  135. if maxVersion < versionValue {
  136. maxVersion = versionValue
  137. }
  138. }
  139. return fmt.Sprintf("%d.*", maxVersion)
  140. }
  141. return fmt.Sprintf("%d.*", lts[ltsName])
  142. }
  143. return version
  144. }
  145. // copied directly from https://github.com/paketo-buildpacks/node-engine/blob/main/node_version_parser.go
  146. func validateNodeVersion(content string) (string, error) {
  147. content = strings.TrimSpace(strings.ToLower(content))
  148. content = strings.TrimPrefix(content, "v")
  149. if _, err := semver.NewConstraint(content); err != nil {
  150. return "", fmt.Errorf("invalid version constraint specified in .node-version: %q", content)
  151. }
  152. return content, nil
  153. }
  154. func (runtime *apiNodeRuntime) Detect(
  155. directoryContent []*github.RepositoryContent,
  156. owner, name, path string,
  157. repoContentOptions github.RepositoryContentGetOptions,
  158. ) map[string]interface{} {
  159. results := make(chan struct {
  160. string
  161. bool
  162. }, 3)
  163. fmt.Printf("Starting detection for a NodeJS runtime for %s/%s\n", owner, name)
  164. runtime.wg.Add(3)
  165. fmt.Println("Checking for yarn")
  166. go runtime.detectYarn(results, directoryContent)
  167. fmt.Println("Checking for NPM")
  168. go runtime.detectNPM(results, directoryContent)
  169. fmt.Println("Checking for NodeJS standalone")
  170. go runtime.detectStandalone(results, directoryContent)
  171. runtime.wg.Wait()
  172. close(results)
  173. atLeastOne := false
  174. detected := make(map[string]bool)
  175. for result := range results {
  176. if result.bool {
  177. atLeastOne = true
  178. }
  179. detected[result.string] = result.bool
  180. }
  181. if atLeastOne {
  182. if detected[yarn] || detected[npm] {
  183. // it is safe to assume that the project contains a package.json
  184. fmt.Println("package.json file detected")
  185. fileContent, _, _, err := runtime.ghClient.Repositories.GetContents(
  186. context.Background(),
  187. owner,
  188. name,
  189. fmt.Sprintf("%s/package.json", path),
  190. &repoContentOptions,
  191. )
  192. if err != nil {
  193. fmt.Printf("Error fetching contents of package.json: %v\n", err)
  194. return nil
  195. }
  196. var packageJSON struct {
  197. Scripts map[string]string `json:"scripts"`
  198. Engines struct {
  199. Node string `json:"node"`
  200. } `json:"engines"`
  201. }
  202. data, err := fileContent.GetContent()
  203. if err != nil {
  204. fmt.Printf("Error calling GetContent() on package.json: %v\n", err)
  205. return nil
  206. }
  207. err = json.NewDecoder(strings.NewReader(data)).Decode(&packageJSON)
  208. if err != nil {
  209. fmt.Printf("Error decoding package.json contents to struct: %v\n", err)
  210. return nil
  211. }
  212. if packageJSON.Engines.Node == "" {
  213. // we should now check for the node engine version in .nvmrc and then .node-version
  214. nvmrcFound := false
  215. nodeVersionFound := false
  216. for i := 0; i < len(directoryContent); i++ {
  217. name := directoryContent[i].GetName()
  218. if name == ".nvmrc" {
  219. nvmrcFound = true
  220. } else if name == ".node-version" {
  221. nodeVersionFound = true
  222. }
  223. }
  224. if nvmrcFound {
  225. // copy exact behavior of https://github.com/paketo-buildpacks/node-engine/blob/main/nvmrc_parser.go
  226. fileContent, _, _, err = runtime.ghClient.Repositories.GetContents(
  227. context.Background(),
  228. owner,
  229. name,
  230. fmt.Sprintf("%s/.nvmrc", path),
  231. &repoContentOptions,
  232. )
  233. if err != nil {
  234. fmt.Printf("Error fetching contents of .nvmrc: %v\n", err)
  235. return nil
  236. }
  237. data, err = fileContent.GetContent()
  238. if err != nil {
  239. fmt.Printf("Error calling GetContent() on .nvmrc: %v\n", err)
  240. return nil
  241. }
  242. nvmrcVersion, err := validateNvmrc(data)
  243. if err != nil {
  244. fmt.Printf("Error validating .nvmrc: %v\n", err)
  245. return nil
  246. }
  247. nvmrcVersion = formatNvmrcContent(nvmrcVersion)
  248. if nvmrcVersion != "*" {
  249. packageJSON.Engines.Node = data
  250. }
  251. }
  252. if packageJSON.Engines.Node == "" && nodeVersionFound {
  253. // copy exact behavior of https://github.com/paketo-buildpacks/node-engine/blob/main/node_version_parser.go
  254. fileContent, _, _, err = runtime.ghClient.Repositories.GetContents(
  255. context.Background(),
  256. owner,
  257. name,
  258. fmt.Sprintf("%s/.node-version", path),
  259. &repoContentOptions,
  260. )
  261. if err != nil {
  262. fmt.Printf("Error fetching contents of .node-version: %v\n", err)
  263. return nil
  264. }
  265. data, err = fileContent.GetContent()
  266. if err != nil {
  267. fmt.Printf("Error calling GetContent() on .node-version: %v\n", err)
  268. return nil
  269. }
  270. nodeVersion, err := validateNodeVersion(data)
  271. if err != nil {
  272. fmt.Printf("Error validating .node-version: %v\n", err)
  273. return nil
  274. }
  275. if nodeVersion != "" {
  276. packageJSON.Engines.Node = nodeVersion
  277. }
  278. }
  279. }
  280. if packageJSON.Engines.Node == "" {
  281. // use the default node engine version from https://github.com/paketo-buildpacks/node-engine/blob/main/buildpack.toml
  282. packageJSON.Engines.Node = "16.*.*"
  283. }
  284. if detected[yarn] {
  285. fmt.Printf("NodeJS yarn runtime detected for %s/%s\n", owner, name)
  286. return map[string]interface{}{
  287. "runtime": yarn, "scripts": packageJSON.Scripts, "node_engine": packageJSON.Engines.Node,
  288. }
  289. } else {
  290. fmt.Printf("NodeJS npm runtime detected for %s/%s\n", owner, name)
  291. return map[string]interface{}{
  292. "runtime": npm, "scripts": packageJSON.Scripts, "node_engine": packageJSON.Engines.Node,
  293. }
  294. }
  295. }
  296. fmt.Printf("NodeJS standalone runtime detected for %s/%s\n", owner, name)
  297. return map[string]interface{}{"runtime": "node-standalone"}
  298. }
  299. fmt.Printf("No NodeJS runtime detected for %s/%s\n", owner, name)
  300. return nil
  301. }