Skip to content

Commit a2a56fd

Browse files
committed
feat: support external installation services
1 parent 3c65912 commit a2a56fd

10 files changed

Lines changed: 402 additions & 86 deletions

.changeset/tidy-lions-install.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'package-build-stats': minor
3+
---
4+
5+
Allow analysis functions to use an optional package installation service while
6+
retaining local installation as the default and fallback.

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@ const results = await getPackageStats('lodash', { client: 'pnpm' })
4545
const results = await getPackageStats('lodash', { client: 'yarn' })
4646
```
4747

48+
Analysis functions install packages locally by default. A service can instead
49+
provide a prepared workspace without changing those APIs:
50+
51+
```js
52+
await getPackageStats('lodash', {
53+
installationService: { url: 'http://127.0.0.1:7003' },
54+
})
55+
```
56+
57+
The service implements `POST /installations` to return
58+
`{ id, packageString, packageName, installPath, packagePath }`, and
59+
`DELETE /installations/:id` to release it. Transport failures fall back to a
60+
local install unless `fallbackToLocal` is `false`.
61+
62+
The `package-build-stats/installation` subpath exports `installPackage`,
63+
`createPackageWorkspace`, and `disposePackage` for implementing that service.
64+
Standalone callers do not need an installation service.
65+
4866
#### Passing options to the build
4967

5068
```js

package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@
1919
],
2020
"main": "build/index.js",
2121
"types": "build/index.d.ts",
22+
"exports": {
23+
".": {
24+
"types": "./build/index.d.ts",
25+
"default": "./build/index.js"
26+
},
27+
"./installation": {
28+
"types": "./build/installation.d.ts",
29+
"default": "./build/installation.js"
30+
},
31+
"./package.json": "./package.json",
32+
"./*": "./*"
33+
},
2234
"scripts": {
2335
"start": "node index.js",
2436
"dev": "DEBUG=bp* node --watch index.js",

src/common.types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
export const packageManagers = ['npm', 'yarn', 'pnpm', 'bun'] as const
22
export type PackageManager = (typeof packageManagers)[number]
33

4+
export type InstallationServiceOptions = {
5+
url: string
6+
fallbackToLocal?: boolean
7+
}
8+
49
type AllOptions = {
510
customImports?: Array<string>
611
splitCustomImports?: boolean
@@ -15,6 +20,7 @@ type AllOptions = {
1520
isLocal?: boolean
1621
installTimeout?: number
1722
signal?: AbortSignal
23+
installationService?: InstallationServiceOptions
1824
}
1925

2026
export type BuildPackageOptions = Pick<
@@ -38,6 +44,7 @@ export type InstallPackageOptions = Pick<
3844
| 'installTimeout'
3945
| 'debug'
4046
| 'signal'
47+
| 'installationService'
4148
>
4249

4350
export type GetPackageStatsOptions = Pick<
@@ -50,6 +57,7 @@ export type GetPackageStatsOptions = Pick<
5057
| 'installTimeout'
5158
| 'minify'
5259
| 'signal'
60+
| 'installationService'
5361
>
5462

5563
export type Externals = {

src/getPackageExportSizes.ts

Lines changed: 13 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,33 @@
11
import Telemetry from './utils/telemetry.utils.js'
22
import { performance } from 'node:perf_hooks'
3-
import path from 'node:path'
43

54
import createDebug from 'debug'
65

76
const debug = createDebug('bp:worker')
87

9-
import {
10-
getExternals,
11-
parsePackageString,
12-
throwIfAborted,
13-
} from './utils/common.utils.js'
8+
import { getExternals, throwIfAborted } from './utils/common.utils.js'
149
import { getAllExports } from './utils/exports.utils.js'
15-
import InstallationUtils from './utils/installation.utils.js'
1610
import BuildUtils from './utils/build.utils.js'
1711
import type {
1812
GetPackageStatsOptions,
1913
InstallPackageOptions,
2014
} from './common.types.js'
21-
22-
async function installPackage(
23-
packageString: string,
24-
installPath: string,
25-
options: InstallPackageOptions,
26-
) {
27-
const { isLocal } = parsePackageString(packageString)
28-
29-
await InstallationUtils.installPackage(packageString, installPath, {
30-
isLocal,
31-
client: options.client,
32-
limitConcurrency: options.limitConcurrency,
33-
networkConcurrency: options.networkConcurrency,
34-
installTimeout: options.installTimeout,
35-
signal: options.signal,
36-
})
37-
}
15+
import {
16+
acquirePackageInstallation,
17+
type PackageInstallationLease,
18+
} from './packageInstallation.js'
3819

3920
export async function getAllPackageExports(
4021
packageString: string,
4122
options: InstallPackageOptions = {},
4223
) {
4324
const startTime = performance.now()
44-
const { name: packageName, normalPath } = parsePackageString(packageString)
45-
const installPath = await InstallationUtils.preparePath(
46-
packageName,
47-
options.client,
48-
options.signal,
49-
)
25+
let installation: PackageInstallationLease | undefined
5026

5127
try {
28+
installation = await acquirePackageInstallation(packageString, options)
29+
const { packageName, packagePath, installPath } = installation
5230
throwIfAborted(options.signal)
53-
await installPackage(packageString, installPath, options)
54-
throwIfAborted(options.signal)
55-
// The package is installed in node_modules subdirectory
56-
const packagePath =
57-
normalPath || path.join(installPath, 'node_modules', packageName)
5831
const results = await getAllExports(
5932
packageString,
6033
packagePath,
@@ -68,7 +41,7 @@ export async function getAllPackageExports(
6841
Telemetry.packageExports(packageString, startTime, false, err)
6942
throw err
7043
} finally {
71-
await InstallationUtils.cleanupPath(installPath)
44+
await installation?.release()
7245
}
7346
}
7447

@@ -78,34 +51,17 @@ export async function getPackageExportSizes(
7851
) {
7952
const startTime = performance.now()
8053
const timings: Record<string, number> = {}
81-
82-
const { name: packageName, normalPath } = parsePackageString(packageString)
83-
84-
const preparePathStart = performance.now()
85-
const installPath = await InstallationUtils.preparePath(
86-
packageName,
87-
options.client,
88-
options.signal,
89-
)
90-
timings.preparePath = performance.now() - preparePathStart
91-
console.log(
92-
`[PERF] [ExportSizes] preparePath: ${timings.preparePath.toFixed(2)}ms`,
93-
)
54+
let installation: PackageInstallationLease | undefined
9455

9556
try {
96-
throwIfAborted(options.signal)
9757
const installStart = performance.now()
98-
await installPackage(packageString, installPath, options)
58+
installation = await acquirePackageInstallation(packageString, options)
59+
const { packageName, packagePath, installPath } = installation
9960
throwIfAborted(options.signal)
10061
timings.install = performance.now() - installStart
10162
console.log(
10263
`[PERF] [ExportSizes] installPackage: ${timings.install.toFixed(2)}ms`,
10364
)
104-
105-
// The package is installed in node_modules subdirectory
106-
const packagePath =
107-
normalPath || path.join(installPath, 'node_modules', packageName)
108-
10965
const getAllExportsStart = performance.now()
11066
const exportMap = await getAllExports(
11167
packageString,
@@ -206,6 +162,6 @@ export async function getPackageExportSizes(
206162
Telemetry.packageExportsSizes(packageString, startTime, false, options, err)
207163
throw err
208164
} finally {
209-
await InstallationUtils.cleanupPath(installPath)
165+
await installation?.release()
210166
}
211167
}

src/getPackageStats.ts

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66
import fs from 'node:fs/promises'
77
import path from 'node:path'
88
import { performance } from 'node:perf_hooks'
9-
import {
10-
getExternals,
11-
parsePackageString,
12-
throwIfAborted,
13-
} from './utils/common.utils.js'
14-
import InstallationUtils from './utils/installation.utils.js'
9+
import { getExternals, throwIfAborted } from './utils/common.utils.js'
1510
import BuildUtils from './utils/build.utils.js'
1611
import { UnexpectedBuildError } from './errors/CustomError.js'
1712
import type { GetPackageStatsOptions } from './common.types.js'
1813
import Telemetry from './utils/telemetry.utils.js'
14+
import {
15+
acquirePackageInstallation,
16+
type PackageInstallationLease,
17+
} from './packageInstallation.js'
1918

2019
function getPackageJSONDetails(packageName: string, installPath: string) {
2120
const startTime = performance.now()
@@ -59,29 +58,12 @@ export default async function getPackageStats(
5958
) {
6059
const startTime = performance.now()
6160
const timings: Record<string, number> = {}
62-
63-
const { name: packageName, isLocal } = parsePackageString(packageString)
64-
65-
const preparePathStart = performance.now()
66-
const installPath = await InstallationUtils.preparePath(
67-
packageName,
68-
options.client,
69-
options.signal,
70-
)
71-
timings.preparePath = performance.now() - preparePathStart
72-
console.log(`[PERF] preparePath: ${timings.preparePath.toFixed(2)}ms`)
61+
let installation: PackageInstallationLease | undefined
7362

7463
try {
75-
throwIfAborted(options.signal)
7664
const installStart = performance.now()
77-
await InstallationUtils.installPackage(packageString, installPath, {
78-
isLocal,
79-
client: options.client,
80-
limitConcurrency: options.limitConcurrency,
81-
networkConcurrency: options.networkConcurrency,
82-
installTimeout: options.installTimeout,
83-
signal: options.signal,
84-
})
65+
installation = await acquirePackageInstallation(packageString, options)
66+
const { packageName, installPath } = installation
8567
throwIfAborted(options.signal)
8668
timings.install = performance.now() - installStart
8769
console.log(`[PERF] installPackage: ${timings.install.toFixed(2)}ms`)
@@ -145,8 +127,6 @@ export default async function getPackageStats(
145127
)
146128
throw e
147129
} finally {
148-
if (!options.debug) {
149-
await InstallationUtils.cleanupPath(installPath)
150-
}
130+
await installation?.release(options.debug)
151131
}
152132
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { default as getPackageStats } from './getPackageStats.js'
2+
export type { InstallationServiceOptions } from './common.types.js'
23
export * from './errors/CustomError.js'
34
export * from './getPackageExportSizes.js'
45
export { emitter as eventQueue } from './utils/telemetry.utils.js'

src/installation.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export {
2+
createPackageWorkspace,
3+
disposePackage,
4+
installPackage,
5+
type PackageInstallation,
6+
} from './packageInstallation.js'

0 commit comments

Comments
 (0)