nodejs.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. package buildpacks
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "os"
  7. "strings"
  8. "sync"
  9. "github.com/Masterminds/semver/v3"
  10. "github.com/google/go-github/github"
  11. "github.com/pelletier/go-toml"
  12. )
  13. var (
  14. lts = map[string]int{
  15. "argon": 4,
  16. "boron": 6,
  17. "carbon": 8,
  18. "dubnium": 10,
  19. }
  20. )
  21. type nodejsRuntime struct {
  22. wg sync.WaitGroup
  23. packs map[string]*BuildpackInfo
  24. }
  25. func NewNodeRuntime() Runtime {
  26. return &nodejsRuntime{}
  27. }
  28. // FIXME: should be called once at the top-level somewhere in the backend
  29. func populateNodePacks(client *github.Client) map[string]*BuildpackInfo {
  30. packs := make(map[string]*BuildpackInfo)
  31. repoRelease, _, err := client.Repositories.GetLatestRelease(context.Background(), "paketo-buildpacks", "nodejs")
  32. if err != nil {
  33. fmt.Printf("Error fetching latest release for paketo-buildpacks/nodejs: %v\n", err)
  34. return nil
  35. }
  36. fileContent, _, _, err := client.Repositories.GetContents(
  37. context.Background(), "paketo-buildpacks", "nodejs", "buildpack.toml",
  38. &github.RepositoryContentGetOptions{
  39. Ref: *repoRelease.TagName,
  40. },
  41. )
  42. if err != nil {
  43. fmt.Printf("Error fetching contents of buildpack.toml for paketo-buildpacks/nodejs: %v\n", err)
  44. return nil
  45. }
  46. data, err := fileContent.GetContent()
  47. if err != nil {
  48. fmt.Printf("Error calling GetContent() on buildpack.toml for paketo-buildpacks/nodejs: %v\n", err)
  49. return nil
  50. }
  51. buildpackToml, err := toml.Load(data)
  52. if err != nil {
  53. fmt.Printf("Error while reading buildpack.toml from paketo-buildpacks/nodejs: %v\n", err)
  54. os.Exit(1)
  55. }
  56. order := buildpackToml.Get("order").([]*toml.Tree)
  57. // yarn
  58. packs[yarn] = newBuildpackInfo()
  59. yarnGroup := order[0].GetArray("group").([]*toml.Tree)
  60. for i := 0; i < len(yarnGroup); i++ {
  61. packs[yarn].addPack(
  62. buildpackOrderGroupInfo{
  63. ID: yarnGroup[i].Get("id").(string),
  64. Optional: yarnGroup[i].GetDefault("optional", false).(bool),
  65. Version: yarnGroup[i].Get("version").(string),
  66. },
  67. )
  68. }
  69. packs[yarn].addEnvVar("SSL_CERT_DIR", "")
  70. packs[yarn].addEnvVar("SSL_CERT_FILE", "")
  71. packs[yarn].addEnvVar("BP_NODE_OPTIMIZE_MEMORY", "")
  72. packs[yarn].addEnvVar("BP_NODE_PROJECT_PATH", "")
  73. packs[yarn].addEnvVar("BP_NODE_VERSION", "")
  74. packs[yarn].addEnvVar("BP_NODE_RUN_SCRIPTS", "")
  75. // npm
  76. packs[npm] = newBuildpackInfo()
  77. npmGroup := order[1].GetArray("group").([]*toml.Tree)
  78. for i := 0; i < len(npmGroup); i++ {
  79. packs[npm].addPack(
  80. buildpackOrderGroupInfo{
  81. ID: npmGroup[i].Get("id").(string),
  82. Optional: npmGroup[i].GetDefault("optional", false).(bool),
  83. Version: npmGroup[i].Get("version").(string),
  84. },
  85. )
  86. }
  87. packs[npm].addEnvVar("SSL_CERT_DIR", "")
  88. packs[npm].addEnvVar("SSL_CERT_FILE", "")
  89. packs[npm].addEnvVar("BP_NODE_OPTIMIZE_MEMORY", "")
  90. packs[npm].addEnvVar("BP_NODE_PROJECT_PATH", "")
  91. packs[npm].addEnvVar("BP_NODE_VERSION", "")
  92. packs[npm].addEnvVar("BP_NODE_RUN_SCRIPTS", "")
  93. // no package manager
  94. packs[standalone] = newBuildpackInfo()
  95. standaloneGroup := order[2].GetArray("group").([]*toml.Tree)
  96. for i := 0; i < len(standaloneGroup); i++ {
  97. packs[standalone].addPack(
  98. buildpackOrderGroupInfo{
  99. ID: standaloneGroup[i].Get("id").(string),
  100. Optional: standaloneGroup[i].GetDefault("optional", false).(bool),
  101. Version: standaloneGroup[i].Get("version").(string),
  102. },
  103. )
  104. }
  105. packs[standalone].addEnvVar("SSL_CERT_DIR", "")
  106. packs[standalone].addEnvVar("SSL_CERT_FILE", "")
  107. packs[standalone].addEnvVar("BP_NODE_OPTIMIZE_MEMORY", "")
  108. packs[standalone].addEnvVar("BP_NODE_PROJECT_PATH", "")
  109. packs[standalone].addEnvVar("BP_NODE_VERSION", "")
  110. packs[standalone].addEnvVar("BP_LAUNCHPOINT", "")
  111. packs[standalone].addEnvVar("BP_LIVE_RELOAD_ENABLED", "")
  112. return packs
  113. }
  114. func (runtime *nodejsRuntime) detectYarn(results chan struct {
  115. string
  116. bool
  117. }, directoryContent []*github.RepositoryContent) {
  118. yarnLockFound := false
  119. packageJSONFound := false
  120. for i := 0; i < len(directoryContent); i++ {
  121. name := directoryContent[i].GetName()
  122. if name == "yarn.lock" {
  123. yarnLockFound = true
  124. } else if name == "package.json" {
  125. packageJSONFound = true
  126. }
  127. if yarnLockFound && packageJSONFound {
  128. break
  129. }
  130. }
  131. if yarnLockFound && packageJSONFound {
  132. results <- struct {
  133. string
  134. bool
  135. }{yarn, true}
  136. } else {
  137. results <- struct {
  138. string
  139. bool
  140. }{yarn, false}
  141. }
  142. runtime.wg.Done()
  143. }
  144. func (runtime *nodejsRuntime) detectNPM(results chan struct {
  145. string
  146. bool
  147. }, directoryContent []*github.RepositoryContent) {
  148. packageJSONFound := false
  149. for i := 0; i < len(directoryContent); i++ {
  150. name := directoryContent[i].GetName()
  151. if name == "package.json" {
  152. packageJSONFound = true
  153. break
  154. }
  155. }
  156. if packageJSONFound {
  157. results <- struct {
  158. string
  159. bool
  160. }{npm, true}
  161. } else {
  162. results <- struct {
  163. string
  164. bool
  165. }{npm, false}
  166. }
  167. runtime.wg.Done()
  168. }
  169. func (runtime *nodejsRuntime) detectStandalone(results chan struct {
  170. string
  171. bool
  172. }, directoryContent []*github.RepositoryContent) {
  173. jsFileFound := false
  174. for i := 0; i < len(directoryContent); i++ {
  175. name := directoryContent[i].GetName()
  176. if name == "server.js" || name == "app.js" || name == "main.js" || name == "index.js" {
  177. jsFileFound = true
  178. break
  179. }
  180. }
  181. if jsFileFound {
  182. results <- struct {
  183. string
  184. bool
  185. }{standalone, true}
  186. } else {
  187. results <- struct {
  188. string
  189. bool
  190. }{standalone, false}
  191. }
  192. runtime.wg.Done()
  193. }
  194. // copied directly from https://github.com/paketo-buildpacks/node-engine/blob/main/nvmrc_parser.go
  195. func validateNvmrc(content string) (string, error) {
  196. content = strings.TrimSpace(strings.ToLower(content))
  197. if content == "lts/*" || content == "node" {
  198. return content, nil
  199. }
  200. for key := range lts {
  201. if content == strings.ToLower("lts/"+key) {
  202. return content, nil
  203. }
  204. }
  205. content = strings.TrimPrefix(content, "v")
  206. if _, err := semver.NewConstraint(content); err != nil {
  207. return "", fmt.Errorf("invalid version constraint specified in .nvmrc: %q", content)
  208. }
  209. return content, nil
  210. }
  211. // copied directly from https://github.com/paketo-buildpacks/node-engine/blob/main/nvmrc_parser.go
  212. func formatNvmrcContent(version string) string {
  213. if version == "node" {
  214. return "*"
  215. }
  216. if strings.HasPrefix(version, "lts") {
  217. ltsName := strings.SplitN(version, "/", 2)[1]
  218. if ltsName == "*" {
  219. var maxVersion int
  220. for _, versionValue := range lts {
  221. if maxVersion < versionValue {
  222. maxVersion = versionValue
  223. }
  224. }
  225. return fmt.Sprintf("%d.*", maxVersion)
  226. }
  227. return fmt.Sprintf("%d.*", lts[ltsName])
  228. }
  229. return version
  230. }
  231. // copied directly from https://github.com/paketo-buildpacks/node-engine/blob/main/node_version_parser.go
  232. func validateNodeVersion(content string) (string, error) {
  233. content = strings.TrimSpace(strings.ToLower(content))
  234. content = strings.TrimPrefix(content, "v")
  235. if _, err := semver.NewConstraint(content); err != nil {
  236. return "", fmt.Errorf("invalid version constraint specified in .node-version: %q", content)
  237. }
  238. return content, nil
  239. }
  240. func (runtime *nodejsRuntime) Detect(
  241. client *github.Client,
  242. directoryContent []*github.RepositoryContent,
  243. owner, name, path string,
  244. repoContentOptions github.RepositoryContentGetOptions,
  245. ) *RuntimeResponse {
  246. runtime.packs = populateNodePacks(client)
  247. results := make(chan struct {
  248. string
  249. bool
  250. }, 3)
  251. fmt.Printf("Starting detection for a NodeJS runtime for %s/%s\n", owner, name)
  252. runtime.wg.Add(3)
  253. fmt.Println("Checking for yarn")
  254. go runtime.detectYarn(results, directoryContent)
  255. fmt.Println("Checking for NPM")
  256. go runtime.detectNPM(results, directoryContent)
  257. fmt.Println("Checking for NodeJS standalone")
  258. go runtime.detectStandalone(results, directoryContent)
  259. runtime.wg.Wait()
  260. close(results)
  261. detected := make(map[string]bool)
  262. for result := range results {
  263. detected[result.string] = result.bool
  264. }
  265. if detected[yarn] || detected[npm] {
  266. // it is safe to assume that the project contains a package.json
  267. fmt.Println("package.json file detected")
  268. fileContent, _, _, err := client.Repositories.GetContents(
  269. context.Background(),
  270. owner,
  271. name,
  272. fmt.Sprintf("%s/package.json", path),
  273. &repoContentOptions,
  274. )
  275. if err != nil {
  276. fmt.Printf("Error fetching contents of package.json: %v\n", err)
  277. return nil
  278. }
  279. var packageJSON struct {
  280. Scripts map[string]string `json:"scripts"`
  281. Engines struct {
  282. Node string `json:"node"`
  283. } `json:"engines"`
  284. }
  285. data, err := fileContent.GetContent()
  286. if err != nil {
  287. fmt.Printf("Error calling GetContent() on package.json: %v\n", err)
  288. return nil
  289. }
  290. err = json.NewDecoder(strings.NewReader(data)).Decode(&packageJSON)
  291. if err != nil {
  292. fmt.Printf("Error decoding package.json contents to struct: %v\n", err)
  293. return nil
  294. }
  295. if packageJSON.Engines.Node == "" {
  296. // we should now check for the node engine version in .nvmrc and then .node-version
  297. nvmrcFound := false
  298. nodeVersionFound := false
  299. for i := 0; i < len(directoryContent); i++ {
  300. name := directoryContent[i].GetName()
  301. if name == ".nvmrc" {
  302. nvmrcFound = true
  303. } else if name == ".node-version" {
  304. nodeVersionFound = true
  305. }
  306. }
  307. if nvmrcFound {
  308. // copy exact behavior of https://github.com/paketo-buildpacks/node-engine/blob/main/nvmrc_parser.go
  309. fileContent, _, _, err = client.Repositories.GetContents(
  310. context.Background(),
  311. owner,
  312. name,
  313. fmt.Sprintf("%s/.nvmrc", path),
  314. &repoContentOptions,
  315. )
  316. if err != nil {
  317. fmt.Printf("Error fetching contents of .nvmrc: %v\n", err)
  318. return nil
  319. }
  320. data, err = fileContent.GetContent()
  321. if err != nil {
  322. fmt.Printf("Error calling GetContent() on .nvmrc: %v\n", err)
  323. return nil
  324. }
  325. nvmrcVersion, err := validateNvmrc(data)
  326. if err != nil {
  327. fmt.Printf("Error validating .nvmrc: %v\n", err)
  328. return nil
  329. }
  330. nvmrcVersion = formatNvmrcContent(nvmrcVersion)
  331. if nvmrcVersion != "*" {
  332. packageJSON.Engines.Node = data
  333. }
  334. }
  335. if packageJSON.Engines.Node == "" && nodeVersionFound {
  336. // copy exact behavior of https://github.com/paketo-buildpacks/node-engine/blob/main/node_version_parser.go
  337. fileContent, _, _, err = client.Repositories.GetContents(
  338. context.Background(),
  339. owner,
  340. name,
  341. fmt.Sprintf("%s/.node-version", path),
  342. &repoContentOptions,
  343. )
  344. if err != nil {
  345. fmt.Printf("Error fetching contents of .node-version: %v\n", err)
  346. return nil
  347. }
  348. data, err = fileContent.GetContent()
  349. if err != nil {
  350. fmt.Printf("Error calling GetContent() on .node-version: %v\n", err)
  351. return nil
  352. }
  353. nodeVersion, err := validateNodeVersion(data)
  354. if err != nil {
  355. fmt.Printf("Error validating .node-version: %v\n", err)
  356. return nil
  357. }
  358. if nodeVersion != "" {
  359. packageJSON.Engines.Node = nodeVersion
  360. }
  361. }
  362. }
  363. if packageJSON.Engines.Node == "" {
  364. // use the default node engine version from https://github.com/paketo-buildpacks/node-engine/blob/main/buildpack.toml
  365. packageJSON.Engines.Node = "16.*.*"
  366. }
  367. if detected[yarn] {
  368. fmt.Printf("NodeJS yarn runtime detected for %s/%s\n", owner, name)
  369. return &RuntimeResponse{
  370. Name: "Node.js",
  371. Buildpacks: runtime.packs[yarn],
  372. Runtime: yarn,
  373. Config: map[string]interface{}{
  374. "scripts": packageJSON.Scripts,
  375. "node_engine": packageJSON.Engines.Node,
  376. },
  377. }
  378. } else {
  379. fmt.Printf("NodeJS npm runtime detected for %s/%s\n", owner, name)
  380. return &RuntimeResponse{
  381. Name: "Node.js",
  382. Buildpacks: runtime.packs[npm],
  383. Runtime: npm,
  384. Config: map[string]interface{}{
  385. "scripts": packageJSON.Scripts,
  386. "node_engine": packageJSON.Engines.Node,
  387. },
  388. }
  389. }
  390. } else if detected[standalone] {
  391. fmt.Printf("NodeJS standalone runtime detected for %s/%s\n", owner, name)
  392. return &RuntimeResponse{
  393. Name: "Node.js",
  394. Buildpacks: runtime.packs[standalone],
  395. Runtime: standalone,
  396. }
  397. }
  398. fmt.Printf("No NodeJS runtime detected for %s/%s\n", owner, name)
  399. return nil
  400. }