Skip to content

Commit d591baf

Browse files
authored
chore: detect build systems (#4725)
* chore: detect lerna & nx * chore: get package name & version * chore: detect rush & turbo * chore: update api to support undefined rootDir * chore: detect lage * chore: detect pants, bazel & back * chore: parallelize build system detection * chore: use config path * chore: add more config files * chore: use type inference * chore: remove redundant types * chore: detect moonrepo * chore: mv & rename package manager test file * chore: remove console.log * chore: fix import path * chore: test for a monorepo setup * chore: test for multiple build systems * chore: detect build system in a try..catch * chore: update snapshots * chore: use absolute path
1 parent b33663b commit d591baf

File tree

5 files changed

+309
-5
lines changed

5 files changed

+309
-5
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { readFileSync } from 'fs'
2+
import path from 'path'
3+
4+
import { findUpSync } from 'find-up'
5+
import { PackageJson } from 'read-pkg'
6+
7+
export type BuildSystem = {
8+
name: string
9+
version?: string | undefined
10+
}
11+
12+
type BuildSystemHandler = (baseDir: string, rootDir?: string) => Promise<BuildSystem | undefined>
13+
14+
export const detectBuildSystems = async (baseDir: string, rootDir?: string): Promise<BuildSystem[]> => {
15+
const buildTools = Object.keys(BUILD_SYSTEMS)
16+
const buildSystems = await Promise.all(buildTools.map(async (tool) => await BUILD_SYSTEMS[tool](baseDir, rootDir)))
17+
18+
return buildSystems.reduce((res, tool) => {
19+
if (tool) {
20+
res.push(tool)
21+
}
22+
23+
return res
24+
}, [] as BuildSystem[])
25+
}
26+
27+
const BUILD_SYSTEMS: Record<string, BuildSystemHandler> = {
28+
nx: async (baseDir, rootDir) => {
29+
const nx = ['nx.json']
30+
const nxConfigPath = lookFor(nx, baseDir, rootDir)
31+
32+
if (nxConfigPath) {
33+
const pkgJson = getPkgJson(nxConfigPath)
34+
const { devDependencies } = pkgJson
35+
36+
return Promise.resolve({
37+
name: 'nx',
38+
version: devDependencies?.nx,
39+
})
40+
}
41+
},
42+
43+
lerna: async (baseDir, rootDir) => {
44+
const lerna = ['lerna.json']
45+
const lernaConfigPath = lookFor(lerna, baseDir, rootDir)
46+
47+
if (lernaConfigPath) {
48+
const pkgJson = getPkgJson(lernaConfigPath)
49+
const { devDependencies } = pkgJson
50+
51+
return Promise.resolve({
52+
name: 'lerna',
53+
version: devDependencies?.lerna,
54+
})
55+
}
56+
},
57+
58+
turbo: async (baseDir, rootDir) => {
59+
const turbo = ['turbo.json']
60+
const turboConfigPath = lookFor(turbo, baseDir, rootDir)
61+
62+
if (turboConfigPath) {
63+
const pkgJson = getPkgJson(turboConfigPath)
64+
const { devDependencies } = pkgJson
65+
66+
return Promise.resolve({
67+
name: 'turbo',
68+
version: devDependencies?.turbo,
69+
})
70+
}
71+
},
72+
73+
rush: async (baseDir, rootDir) => {
74+
const rush = ['rush.json']
75+
const rushConfigPath = lookFor(rush, baseDir, rootDir)
76+
77+
if (rushConfigPath) {
78+
const pkgJson = getPkgJson(rushConfigPath)
79+
const { devDependencies } = pkgJson
80+
81+
return Promise.resolve({
82+
name: 'rush',
83+
version: devDependencies?.rush,
84+
})
85+
}
86+
},
87+
88+
lage: async (baseDir, rootDir) => {
89+
const lage = ['lage.config.js']
90+
const lageConfigPath = lookFor(lage, baseDir, rootDir)
91+
if (lageConfigPath) {
92+
const pkgJson = getPkgJson(lageConfigPath)
93+
const { devDependencies } = pkgJson
94+
95+
return Promise.resolve({
96+
name: 'lage',
97+
version: devDependencies?.lage,
98+
})
99+
}
100+
},
101+
102+
pants: async (baseDir, rootDir) => {
103+
const pants = ['pants.toml']
104+
const pantsConfigPath = lookFor(pants, baseDir, rootDir)
105+
106+
if (pantsConfigPath) {
107+
return Promise.resolve({
108+
name: 'pants',
109+
})
110+
}
111+
},
112+
113+
buck: async (baseDir, rootDir) => {
114+
const buck = ['.buckconfig', 'BUCK']
115+
const buckConfigPath = lookFor(buck, baseDir, rootDir)
116+
117+
if (buckConfigPath) {
118+
return Promise.resolve({
119+
name: 'buck',
120+
})
121+
}
122+
},
123+
124+
gradle: async (baseDir, rootDir) => {
125+
const gradle = ['build.gradle']
126+
const gradleConfigPath = lookFor(gradle, baseDir, rootDir)
127+
128+
if (gradleConfigPath) {
129+
return Promise.resolve({
130+
name: 'gradle',
131+
})
132+
}
133+
},
134+
135+
bazel: async (baseDir, rootDir) => {
136+
const bazel = ['.bazelrc', 'WORKSPACE', 'WORKSPACE.bazel', 'BUILD.bazel']
137+
const bazelConfigPath = lookFor(bazel, baseDir, rootDir)
138+
139+
if (bazelConfigPath) {
140+
return Promise.resolve({
141+
name: 'bazel',
142+
})
143+
}
144+
},
145+
146+
moon: async (baseDir, rootDir) => {
147+
const moon = ['.moon']
148+
const moonConfigPath = lookFor(moon, baseDir, rootDir, 'directory')
149+
150+
if (moonConfigPath) {
151+
const pkgJson = getPkgJson(moonConfigPath)
152+
const { devDependencies } = pkgJson
153+
154+
return Promise.resolve({
155+
name: 'moon',
156+
version: devDependencies?.moon,
157+
})
158+
}
159+
},
160+
}
161+
162+
const lookFor = (
163+
configFile: string[],
164+
baseDir: string,
165+
rootDir?: string,
166+
type?: 'file' | 'directory',
167+
): string | undefined => {
168+
return findUpSync(configFile, { cwd: baseDir, stopAt: rootDir, type: type })
169+
}
170+
171+
const getPkgJson = (configPath: string): PackageJson => {
172+
return JSON.parse(readFileSync(path.join(path.dirname(configPath), 'package.json'), 'utf-8'))
173+
}

packages/build-info/src/get-build-info.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
11
import { listFrameworks } from '@netlify/framework-info'
22

33
import { ContextOptions, getContext } from './context.js'
4+
import { BuildSystem, detectBuildSystems } from './detect-build-system.js'
45
import { detectPackageManager, PkgManagerFields } from './detect-package-manager.js'
56
import { getWorkspaceInfo, WorkspaceInfo } from './workspaces.js'
67

78
export type Info = {
89
jsWorkspaces?: WorkspaceInfo
910
packageManager?: PkgManagerFields
1011
frameworks: unknown[]
12+
buildSystems?: BuildSystem[]
1113
}
1214

1315
export const getBuildInfo = async (opts: ContextOptions) => {
1416
const context = await getContext(opts)
1517
let frameworks: any[] = []
18+
let buildSystems: BuildSystem[] = []
1619

1720
try {
18-
// if the framework detection is crashing we should not crash the build info and package-manager
21+
// if the framework or buildSystem detection is crashing we should not crash the build info and package-manager
1922
// detection
2023
frameworks = await listFrameworks({ projectDir: context.projectDir })
24+
buildSystems = await detectBuildSystems(context.projectDir, context.rootDir)
2125
} catch {
2226
// TODO: build reporting to buildbot see: https://github.com/netlify/pillar-workflow/issues/1001
2327
// noop
2428
}
2529

26-
const info: Info = { frameworks }
30+
const info: Info = { frameworks, buildSystems }
2731

2832
// only if we find a root package.json we know this is a javascript workspace
2933
if (Object.keys(context.rootPackageJson).length > 0) {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import path from 'path'
2+
3+
import { expect, test } from 'vitest'
4+
5+
import { detectBuildSystems } from '../src/detect-build-system.js'
6+
7+
import { mockFileSystem } from './mock-file-system.js'
8+
9+
test('detects nx when nx.json is present', async () => {
10+
const cwd = mockFileSystem({
11+
'package.json': JSON.stringify({ devDependencies: { nx: '^14.7.13' } }),
12+
'nx.json': '',
13+
})
14+
15+
const buildSystems = await detectBuildSystems(cwd)
16+
17+
expect(buildSystems[0]).toEqual({ name: 'nx', version: '^14.7.13' })
18+
})
19+
20+
test('detects lerna when lerna.json is present', async () => {
21+
const cwd = mockFileSystem({
22+
'package.json': JSON.stringify({ devDependencies: { lerna: '^5.5.2' } }),
23+
'lerna.json': '',
24+
})
25+
26+
const buildSystems = await detectBuildSystems(cwd)
27+
expect(buildSystems[0]).toEqual({ name: 'lerna', version: '^5.5.2' })
28+
})
29+
30+
test('detects turbo when turbo.json is present', async () => {
31+
const cwd = mockFileSystem({
32+
'package.json': JSON.stringify({ devDependencies: { turbo: '^1.6.3' } }),
33+
'turbo.json': '',
34+
})
35+
36+
const buildSystems = await detectBuildSystems(cwd)
37+
expect(buildSystems[0]).toEqual({ name: 'turbo', version: '^1.6.3' })
38+
})
39+
40+
test('detects rush when rush.json is present', async () => {
41+
const cwd = mockFileSystem({
42+
'package.json': JSON.stringify({ devDependencies: { rush: '^2.5.3' } }),
43+
'rush.json': '',
44+
})
45+
46+
const buildSystems = await detectBuildSystems(cwd)
47+
expect(buildSystems[0]).toEqual({ name: 'rush', version: '^2.5.3' })
48+
})
49+
50+
test('detects lage when lage.config.json is present', async () => {
51+
const cwd = mockFileSystem({
52+
'package.json': JSON.stringify({ devDependencies: { lage: '^1.5.0' } }),
53+
'lage.config.js': '',
54+
})
55+
56+
const buildSystems = await detectBuildSystems(cwd)
57+
expect(buildSystems[0]).toEqual({ name: 'lage', version: '^1.5.0' })
58+
})
59+
60+
test('detects pants when pants.toml is present', async () => {
61+
const cwd = mockFileSystem({
62+
'pants.toml': '',
63+
})
64+
65+
const buildSystems = await detectBuildSystems(cwd)
66+
expect(buildSystems[0]).toEqual({ name: 'pants' })
67+
})
68+
69+
test('detects buck when .buckconfig is present', async () => {
70+
const cwd = mockFileSystem({
71+
'.buckconfig': '',
72+
})
73+
74+
const buildSystems = await detectBuildSystems(cwd)
75+
expect(buildSystems[0]).toEqual({ name: 'buck' })
76+
})
77+
78+
test('detects gradle when build.gradle is present', async () => {
79+
const cwd = mockFileSystem({
80+
'build.gradle': '',
81+
})
82+
83+
const buildSystems = await detectBuildSystems(cwd)
84+
expect(buildSystems[0]).toEqual({ name: 'gradle' })
85+
})
86+
87+
test('detects bazel when .bazelrc is present', async () => {
88+
const cwd = mockFileSystem({
89+
'.bazelrc': '',
90+
})
91+
92+
const buildSystems = await detectBuildSystems(cwd)
93+
expect(buildSystems[0]).toEqual({ name: 'bazel' })
94+
})
95+
96+
test('detects moonrepo when .moon directory is present', async () => {
97+
const cwd = mockFileSystem({
98+
'package.json': JSON.stringify({ devDependencies: { moon: '^0.5.1' } }),
99+
'.moon/toolchain.yml': '',
100+
})
101+
102+
const buildSystems = await detectBuildSystems(cwd)
103+
expect(buildSystems[0]).toEqual({ name: 'moon', version: '^0.5.1' })
104+
})
105+
106+
test('detects build system in a monorepo setup', async () => {
107+
const cwd = mockFileSystem({
108+
'packages/website/package.json': JSON.stringify({ devDependencies: { turbo: '^1.6.3' } }),
109+
'packages/website/turbo.json': '',
110+
'packages/server/server.js': '',
111+
})
112+
113+
const buildSystems = await detectBuildSystems(path.join(cwd, 'packages/website'), cwd)
114+
expect(buildSystems[0]).toEqual({ name: 'turbo', version: '^1.6.3' })
115+
})
116+
117+
test('detects multiple build systems in a monorepo setup', async () => {
118+
const cwd = mockFileSystem({
119+
'packages/website/package.json': JSON.stringify({ devDependencies: { lerna: '^2.5.3' } }),
120+
'packages/website/lerna.json': '',
121+
'packages/server/server.js': '',
122+
'build.gradle': '',
123+
})
124+
125+
const buildSystems = await detectBuildSystems(path.join(cwd, 'packages/website'), cwd)
126+
expect(buildSystems).toEqual([{ name: 'lerna', version: '^2.5.3' }, { name: 'gradle' }])
127+
})

packages/build-info/src/detect-package-mangaer.test.ts renamed to packages/build-info/tests/detect-package-manager.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import { join } from 'path'
22

33
import { beforeEach, describe, expect, test } from 'vitest'
44

5+
import { detectPackageManager } from '../src/detect-package-manager.js'
56
import { mockFileSystem } from '../tests/mock-file-system.js'
67

7-
import { detectPackageManager } from './detect-package-manager.js'
8-
98
const env = { ...process.env }
109
beforeEach(() => {
1110
// restore process environment variables

packages/build-info/tests/get-build-info.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ describe('Golang', () => {
2929
})
3030
expect(info).toMatchInlineSnapshot(`
3131
{
32+
"buildSystems": [],
3233
"frameworks": [],
3334
}
34-
`)
35+
`)
3536
})
3637
})
3738

0 commit comments

Comments
 (0)