Skip to content

Commit ac903a2

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

13 files changed

Lines changed: 380 additions & 93 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,25 @@ 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 reusable installation 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+
`{ packageString, packageName, installPath, packagePath }`. Generated entries
59+
and bundles are written to a separate per-analysis directory, so the returned
60+
installation can be safely reused. Transport failures fall back to a local
61+
install unless `fallbackToLocal` is `false`.
62+
63+
The `package-build-stats/installation` subpath exports `installPackage` and
64+
`disposePackage` for implementing that service. Standalone callers do not need
65+
an installation service.
66+
4867
#### Passing options to the build
4968

5069
```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/config/makeRspackConfig.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import escapeRegex from 'escape-string-regexp'
44
import type { Entry, Configuration } from '@rspack/core'
55
import rspack from '@rspack/core'
66
import { createRequire } from 'node:module'
7+
import path from 'node:path'
78

89
import OxcJsMinimizerRspackPlugin from './OxcJsMinimizerRspackPlugin.js'
910

@@ -17,12 +18,14 @@ type MakeRspackConfigOptions = {
1718
debug?: boolean
1819
minify?: boolean
1920
entry: Entry
21+
dependencyPath?: string
2022
outputPath: string
2123
}
2224

2325
export default function makeRspackConfig({
2426
packageName: _packageName,
2527
entry,
28+
dependencyPath,
2629
externals,
2730
debug: _debug,
2831
minify = true,
@@ -74,7 +77,9 @@ export default function makeRspackConfig({
7477
chunkModules: true,
7578
},
7679
resolve: {
77-
modules: ['node_modules'],
80+
modules: dependencyPath
81+
? [path.join(dependencyPath, 'node_modules'), 'node_modules']
82+
: ['node_modules'],
7883
conditionNames: ['svelte', '...'],
7984
extensions: [
8085
'.web.tsx',

src/getPackageExportSizes.ts

Lines changed: 12 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,30 @@
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 { preparePackage, type PreparedPackage } from './packageInstallation.js'
3816

3917
export async function getAllPackageExports(
4018
packageString: string,
4119
options: InstallPackageOptions = {},
4220
) {
4321
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-
)
22+
let preparedPackage: PreparedPackage | undefined
5023

5124
try {
25+
preparedPackage = await preparePackage(packageString, options, false)
26+
const { packageName, packagePath, installPath } = preparedPackage
5227
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)
5828
const results = await getAllExports(
5929
packageString,
6030
packagePath,
@@ -68,7 +38,7 @@ export async function getAllPackageExports(
6838
Telemetry.packageExports(packageString, startTime, false, err)
6939
throw err
7040
} finally {
71-
await InstallationUtils.cleanupPath(installPath)
41+
await preparedPackage?.cleanup()
7242
}
7343
}
7444

@@ -78,34 +48,17 @@ export async function getPackageExportSizes(
7848
) {
7949
const startTime = performance.now()
8050
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-
)
51+
let preparedPackage: PreparedPackage | undefined
9452

9553
try {
96-
throwIfAborted(options.signal)
9754
const installStart = performance.now()
98-
await installPackage(packageString, installPath, options)
55+
preparedPackage = await preparePackage(packageString, options)
56+
const { packageName, packagePath, installPath, buildPath } = preparedPackage
9957
throwIfAborted(options.signal)
10058
timings.install = performance.now() - installStart
10159
console.log(
10260
`[PERF] [ExportSizes] installPackage: ${timings.install.toFixed(2)}ms`,
10361
)
104-
105-
// The package is installed in node_modules subdirectory
106-
const packagePath =
107-
normalPath || path.join(installPath, 'node_modules', packageName)
108-
10962
const getAllExportsStart = performance.now()
11063
const exportMap = await getAllExports(
11164
packageString,
@@ -152,7 +105,8 @@ export async function getPackageExportSizes(
152105
// oxlint-disable-next-line no-await-in-loop
153106
const chunkDetails = await BuildUtils.buildPackageIgnoringMissingDeps({
154107
name: packageName,
155-
installPath,
108+
installPath: buildPath,
109+
dependencyPath: installPath,
156110
externals,
157111
options: {
158112
customImports: chunk,
@@ -206,6 +160,6 @@ export async function getPackageExportSizes(
206160
Telemetry.packageExportsSizes(packageString, startTime, false, options, err)
207161
throw err
208162
} finally {
209-
await InstallationUtils.cleanupPath(installPath)
163+
await preparedPackage?.cleanup()
210164
}
211165
}

src/getPackageStats.ts

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,12 @@
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 { preparePackage, type PreparedPackage } from './packageInstallation.js'
1915

2016
function getPackageJSONDetails(packageName: string, installPath: string) {
2117
const startTime = performance.now()
@@ -59,29 +55,12 @@ export default async function getPackageStats(
5955
) {
6056
const startTime = performance.now()
6157
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`)
58+
let preparedPackage: PreparedPackage | undefined
7359

7460
try {
75-
throwIfAborted(options.signal)
7661
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-
})
62+
preparedPackage = await preparePackage(packageString, options)
63+
const { packageName, installPath, buildPath } = preparedPackage
8564
throwIfAborted(options.signal)
8665
timings.install = performance.now() - installStart
8766
console.log(`[PERF] installPackage: ${timings.install.toFixed(2)}ms`)
@@ -97,7 +76,8 @@ export default async function getPackageStats(
9776
getPackageJSONDetails(packageName, installPath),
9877
BuildUtils.buildPackageIgnoringMissingDeps({
9978
name: packageName,
100-
installPath,
79+
installPath: buildPath,
80+
dependencyPath: installPath,
10181
externals,
10282
options: {
10383
debug: options.debug,
@@ -145,8 +125,6 @@ export default async function getPackageStats(
145125
)
146126
throw e
147127
} finally {
148-
if (!options.debug) {
149-
await InstallationUtils.cleanupPath(installPath)
150-
}
128+
await preparedPackage?.cleanup(options.debug)
151129
}
152130
}

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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export {
2+
disposePackage,
3+
installPackage,
4+
type PackageInstallation,
5+
} from './packageInstallation.js'

0 commit comments

Comments
 (0)