Skip to content

Commit dbbc188

Browse files
committed
feat(tests): mostly bypass npm when installing deps to fixtures
This is still slower than it should be but let's see what happens.
1 parent 4518549 commit dbbc188

File tree

12 files changed

+1308
-494
lines changed

12 files changed

+1308
-494
lines changed

package-lock.json

Lines changed: 743 additions & 422 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"test:e2e": "npm run test:e2e --workspaces"
1414
},
1515
"workspaces": [
16-
"packages/*"
16+
"packages/*",
17+
"utilities/*"
1718
],
1819
"devDependencies": {
1920
"@commitlint/cli": "^17.8.1",
@@ -34,4 +35,4 @@
3435
"lint-staged": {
3536
"*.{ts,js,mjs}": "npm run lint"
3637
}
37-
}
38+
}

packages/cli/src/testing/fixture-sandbox.ts

Lines changed: 21 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Debug from 'debug'
66

77
const debug = Debug('checkly:cli:testing:fixture-sandbox')
88

9-
import { detectNearestPackageJson, detectPackageManager, PackageManager } from '../services/check-parser/package-files/package-manager'
9+
import { detectNearestPackageJson } from '../services/check-parser/package-files/package-manager'
1010

1111
export interface CreateFixtureSandboxOptions {
1212
/**
@@ -24,13 +24,6 @@ export interface CreateFixtureSandboxOptions {
2424
*/
2525
root?: string
2626

27-
/**
28-
* The package manager used to manage the fixture.
29-
*
30-
* If not provided, the package manager is detected automatically.
31-
*/
32-
packageManager?: PackageManager
33-
3427
/**
3528
* Whether to install packages using the package manager.
3629
*
@@ -48,34 +41,34 @@ export interface CreateFixtureSandboxOptions {
4841
}
4942

5043
interface FixtureSandboxOptions {
51-
packageManager: PackageManager
5244
root: string
5345
}
5446

5547
export class FixtureSandbox {
5648
#root: string
57-
#packageManager: PackageManager
5849

59-
private constructor ({ root, packageManager }: FixtureSandboxOptions) {
50+
static installer: any
51+
52+
private constructor ({ root }: FixtureSandboxOptions) {
6053
this.#root = root
61-
this.#packageManager = packageManager
6254
}
6355

6456
get root (): string {
6557
return this.#root
6658
}
6759

68-
get packageManager (): PackageManager {
69-
return this.#packageManager
70-
}
71-
7260
static async create (options: CreateFixtureSandboxOptions): Promise<FixtureSandbox> {
73-
const { execa } = await import('execa')
61+
// @ts-expect-error Not a TypeScript module.
62+
const { createInstaller, fileHashSha256 } = await import('dependency-layer-installer')
63+
64+
const installer = FixtureSandbox.installer ?? await createInstaller({
65+
archiveDir: path.join(tmpdir(), 'fixture-sandbox-installer-archive'),
66+
extractDir: path.join(tmpdir(), 'fixture-sandbox-installer-extracted'),
67+
})
7468

7569
const {
7670
source,
7771
root: maybeRoot,
78-
packageManager: maybePackageManager,
7972
installPackages = true,
8073
injectPackedSelf = true,
8174
} = options
@@ -92,41 +85,7 @@ export class FixtureSandbox {
9285
recursive: true,
9386
})
9487

95-
const packageManager = maybePackageManager
96-
?? await detectPackageManager(root)
97-
98-
debug(`Detected package manager ${packageManager.name}`)
99-
10088
if (installPackages) {
101-
const { executable, args, unsafeDisplayCommand } = packageManager.installCommand()
102-
103-
debug(`Installing packages via ${unsafeDisplayCommand}`)
104-
105-
await execa(executable, args, {
106-
cwd: root,
107-
})
108-
}
109-
110-
if (installPackages && injectPackedSelf) {
111-
debug('Injecting containing package')
112-
113-
const lockfile = packageManager.representativeLockfile
114-
115-
// Take a backup of the original package.json so that we can restore
116-
// it later.
117-
await fs.cp(
118-
path.join(root, 'package.json'),
119-
path.join(root, 'package.json.backup'),
120-
)
121-
122-
// Same for the lockfile.
123-
if (lockfile) {
124-
await fs.cp(
125-
path.join(root, lockfile),
126-
path.join(root, `${lockfile}.backup`),
127-
)
128-
}
129-
13089
const packageJson = await detectNearestPackageJson(__dirname)
13190

13291
const sourcePath = path.join(
@@ -137,30 +96,22 @@ export class FixtureSandbox {
13796
// Make sure the archive exists.
13897
await fs.access(sourcePath, fs.constants.R_OK)
13998

140-
const { executable, args } = packageManager.installCommand()
99+
await installer.install(root, async (plan: any) => {
100+
await plan.installDependencies()
141101

142-
await execa(executable, [...args, '--save-dev', `file:${sourcePath}`], {
143-
cwd: root,
144-
})
102+
if (injectPackedSelf) {
103+
await plan.injectPackedDependency(packageJson.name, {
104+
file: sourcePath,
105+
key: await fileHashSha256(packageJson.meta.filePath),
106+
})
107+
}
145108

146-
// Restore original package.json.
147-
await fs.rename(
148-
path.join(root, 'package.json.backup'),
149-
path.join(root, 'package.json'),
150-
)
151-
152-
// Restore original lockfile.
153-
if (lockfile) {
154-
await fs.rename(
155-
path.join(root, `${lockfile}.backup`),
156-
path.join(root, lockfile),
157-
)
158-
}
109+
await plan.updateLockfile()
110+
})
159111
}
160112

161113
return new FixtureSandbox({
162114
root,
163-
packageManager,
164115
})
165116
}
166117

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createLayer } from '../create.mjs'
2+
3+
if (process.argv.length < 3) {
4+
throw new Error('Missing required argument PACKAGE[@VERSION=latest]')
5+
}
6+
7+
const input = {
8+
pkg: process.argv[2],
9+
outputDir: process.env['OUTDIR'] ?? process.cwd(),
10+
outputFile: process.env['OUTFILE'],
11+
onlyDeps: process.env['ONLY_DEPENDENCIES'] === '1',
12+
}
13+
14+
try {
15+
// eslint-disable-next-line no-console
16+
console.log(await createLayer(input))
17+
} catch (err) {
18+
// eslint-disable-next-line no-console
19+
console.error(err)
20+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import fs from 'node:fs/promises'
2+
import path from 'node:path'
3+
import os from 'node:os'
4+
5+
import * as tar from 'tar'
6+
import Debug from 'debug'
7+
8+
import { fetchLatestVersion, readPackageJson, splitPkg } from './util.mjs'
9+
import { npm } from './npm.mjs'
10+
11+
const debug = Debug('checkly:utility:dependency-layer-installer:create')
12+
13+
export async function createLayer ({ pkg, outputDir, outputFile, onlyDeps }) {
14+
const [packageName, requestedVersion = 'latest'] = splitPkg(pkg)
15+
16+
const version = requestedVersion === 'latest'
17+
? await fetchLatestVersion(packageName)
18+
: requestedVersion
19+
20+
debug('Creating build directory...')
21+
const buildDir = await fs.mkdtemp(path.join(os.tmpdir(), 'make-npm-install-layer-'))
22+
debug(`Build directory: ${buildDir}`)
23+
24+
const buildPath = (...paths) => path.join(buildDir, ...paths)
25+
const sourcePath = (...paths) => buildPath('node_modules', ...paths)
26+
const targetPath = (...paths) => buildPath('new_node_modules', ...paths)
27+
28+
try {
29+
debug(`Installing ${packageName}@${version}...`)
30+
await npm({
31+
cwd: buildPath(),
32+
args: ['install', '--ignore-scripts', `${packageName}@${version}`],
33+
})
34+
debug(`Installation complete`)
35+
36+
const pkgPath = packageName.split('/')
37+
38+
debug(`Reading package.json...`)
39+
const {
40+
version: installedVersion,
41+
bin,
42+
} = await readPackageJson(sourcePath(...pkgPath))
43+
44+
debug(`Installed version: ${installedVersion}`)
45+
46+
debug('Creating target path...')
47+
await fs.mkdir(targetPath(), {
48+
recursive: true,
49+
})
50+
51+
if (bin !== undefined) {
52+
debug(`Shuffling binaries...`)
53+
await fs.mkdir(targetPath('.bin'), {
54+
recursive: true,
55+
})
56+
57+
for (const executable of Object.keys(bin)) {
58+
debug(`Moving binary: ${executable}`)
59+
await fs.rename(
60+
sourcePath('.bin', executable),
61+
targetPath('.bin', executable),
62+
)
63+
}
64+
}
65+
66+
debug(`Removing cruft...`)
67+
await fs.rm(sourcePath('.package-lock.json'), {
68+
force: true,
69+
})
70+
71+
debug(`Preparing target package path...`)
72+
await fs.mkdir(targetPath(...pkgPath), {
73+
recursive: true,
74+
})
75+
76+
if (!onlyDeps) {
77+
debug(`Moving source package to target...`)
78+
await fs.rename(sourcePath(...pkgPath), targetPath(...pkgPath))
79+
} else {
80+
debug(`Removing source package...`)
81+
await fs.rm(sourcePath(...pkgPath), {
82+
recursive: true,
83+
})
84+
}
85+
86+
if (pkgPath.length > 1) {
87+
try {
88+
await fs.rmdir(pkgPath[0])
89+
} catch {
90+
// It has other contents, ignore.
91+
}
92+
}
93+
94+
debug(`Moving dependencies to target package...`)
95+
await fs.rename(sourcePath(), targetPath(...pkgPath, 'node_modules'))
96+
97+
debug(`Renaming target to source...`)
98+
await fs.rename(targetPath(), sourcePath())
99+
100+
const defaultOutputFile = `node_modules_layer_${pkgPath.join('_')}-v${installedVersion}.tgz`
101+
102+
const outputFilePath = path.resolve(outputDir, outputFile ?? defaultOutputFile)
103+
104+
debug('Creating archive...')
105+
await tar.create({
106+
cwd: buildPath(),
107+
gzip: true,
108+
file: outputFilePath,
109+
}, [
110+
'node_modules',
111+
])
112+
113+
const stat = await fs.stat(outputFilePath)
114+
115+
return {
116+
layerFile: outputFilePath,
117+
size: stat.size,
118+
version: installedVersion,
119+
}
120+
} finally {
121+
debug('Cleaning up build path...')
122+
await fs.rm(buildDir, {
123+
recursive: true,
124+
})
125+
}
126+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import fs from 'node:fs/promises'
2+
3+
import * as tar from 'tar'
4+
import Debug from 'debug'
5+
6+
const debug = Debug('checkly:utility:dependency-layer-installer:extract')
7+
8+
export async function extractLayer ({ layerFile, extractDir }) {
9+
await fs.mkdir(extractDir, {
10+
recursive: true,
11+
})
12+
13+
debug(`Extracting ${layerFile} to ${extractDir}...`)
14+
await tar.extract({
15+
file: layerFile,
16+
cwd: extractDir,
17+
})
18+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import fs from 'node:fs'
2+
import { pipeline } from 'node:stream/promises'
3+
import crypto from 'node:crypto'
4+
5+
export async function fileHash (filePath, hash, { limit = 12 } = {}) {
6+
const stream = fs.createReadStream(filePath)
7+
await pipeline(stream, hash)
8+
const digest = hash.digest('hex')
9+
return digest.slice(0, limit)
10+
}
11+
12+
export async function fileHashSha256 (filePath, { limit = 12 } = {}) {
13+
return await fileHash(filePath, crypto.createHash('sha256'), { limit })
14+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { createInstaller } from './installer.mjs'
2+
export { fileHash, fileHashSha256 } from './hash.mjs'

0 commit comments

Comments
 (0)