package-build-stats 8.2.3 → 8.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/common.types.d.ts +26 -0
- package/build/common.types.js +1 -0
- package/build/config/config.d.ts +4 -0
- package/build/config/config.js +5 -0
- package/build/config/makeRspackConfig.d.ts +11 -0
- package/build/config/makeRspackConfig.js +170 -0
- package/build/errors/CustomError.d.ts +42 -0
- package/build/errors/CustomError.js +69 -0
- package/build/getDependencySizeTree.d.ts +6 -0
- package/build/getDependencySizeTree.js +271 -0
- package/build/getPackageExportSizes.d.ts +18 -0
- package/build/getPackageExportSizes.js +95 -0
- package/build/getPackageStats.d.ts +26 -0
- package/build/getPackageStats.js +98 -0
- package/build/index.d.ts +4 -0
- package/build/index.js +4 -0
- package/build/utils/build.utils.d.ts +51 -0
- package/build/utils/build.utils.js +288 -0
- package/build/utils/common.utils.d.ts +20 -0
- package/build/utils/common.utils.js +112 -0
- package/build/utils/exports.utils.d.ts +26 -0
- package/build/utils/exports.utils.js +192 -0
- package/build/utils/installation.utils.d.ts +9 -0
- package/build/utils/installation.utils.js +203 -0
- package/build/utils/telemetry.utils.d.ts +17 -0
- package/build/utils/telemetry.utils.js +121 -0
- package/package.json +4 -2
- package/src/utils/build.utils.ts +39 -14
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import childProcess from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { builtinModules } from 'module';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
const homeDirectory = os.homedir();
|
|
7
|
+
export const getBuiltInModules = () => builtinModules.flatMap(mod => [mod, 'node:' + mod]);
|
|
8
|
+
export function exec(command, options, timeout) {
|
|
9
|
+
let timerId;
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const child = childProcess.exec(command, options, (error, stdout, stderr) => {
|
|
12
|
+
if (error) {
|
|
13
|
+
reject(stderr);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
resolve(stdout);
|
|
17
|
+
}
|
|
18
|
+
if (timerId) {
|
|
19
|
+
clearTimeout(timerId);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
if (timeout) {
|
|
23
|
+
timerId = setTimeout(() => {
|
|
24
|
+
if (child.pid) {
|
|
25
|
+
process.kill(child.pid);
|
|
26
|
+
}
|
|
27
|
+
reject(`Execution of ${command.substring(0, 40)}... cancelled as it exceeded a timeout of ${timeout} ms`);
|
|
28
|
+
}, timeout * 10);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Gets external peerDeps that shouldn't be a
|
|
34
|
+
* part of the build in a regex format -
|
|
35
|
+
* /(^dep-a$|^dep-a\/|^dep-b$|^dep-b\/)\//
|
|
36
|
+
*/
|
|
37
|
+
export function getExternals(packageName, installPath) {
|
|
38
|
+
const packageJSONPath = path.join(installPath, 'node_modules', packageName, 'package.json');
|
|
39
|
+
const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath, 'utf-8'));
|
|
40
|
+
const dependencies = Object.keys(packageJSON.dependencies || {});
|
|
41
|
+
const peerDependencies = Object.keys(packageJSON.peerDependencies || {});
|
|
42
|
+
// All packages with name same as a built-in node module, but
|
|
43
|
+
// haven't explicitly been added as an npm dependency or aren't the package itself
|
|
44
|
+
// are externals
|
|
45
|
+
const builtInExternals = getBuiltInModules().filter(mod => !dependencies.includes(mod) && mod !== packageName);
|
|
46
|
+
return {
|
|
47
|
+
externalPackages: peerDependencies,
|
|
48
|
+
externalBuiltIns: builtInExternals,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function expandTilde(pathString) {
|
|
52
|
+
return homeDirectory
|
|
53
|
+
? pathString.replace(/^~(?=$|\/|\\)/, homeDirectory)
|
|
54
|
+
: pathString;
|
|
55
|
+
}
|
|
56
|
+
function isLocalPackageString(packageString) {
|
|
57
|
+
const packageJsonPath = path.resolve(packageString, 'package.json');
|
|
58
|
+
try {
|
|
59
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function isScopedPackageString(packageString) {
|
|
68
|
+
return packageString.startsWith('@');
|
|
69
|
+
}
|
|
70
|
+
function parseLocalPackageString(packageString) {
|
|
71
|
+
const fullPath = path.resolve(packageString, 'package.json');
|
|
72
|
+
const packageJSON = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
73
|
+
return {
|
|
74
|
+
name: packageJSON.name,
|
|
75
|
+
version: packageJSON.version,
|
|
76
|
+
scoped: packageJSON.name.startsWith('@'),
|
|
77
|
+
normalPath: packageString,
|
|
78
|
+
isLocal: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function parseScopedPackageString(packageString) {
|
|
82
|
+
const lastAtIndex = packageString.lastIndexOf('@');
|
|
83
|
+
return {
|
|
84
|
+
name: lastAtIndex === 0
|
|
85
|
+
? packageString
|
|
86
|
+
: packageString.substring(0, lastAtIndex),
|
|
87
|
+
version: lastAtIndex === 0 ? null : packageString.substring(lastAtIndex + 1),
|
|
88
|
+
scoped: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function parseUnscopedPackageString(packageString) {
|
|
92
|
+
const lastAtIndex = packageString.lastIndexOf('@');
|
|
93
|
+
return {
|
|
94
|
+
name: lastAtIndex === -1
|
|
95
|
+
? packageString
|
|
96
|
+
: packageString.substring(0, lastAtIndex),
|
|
97
|
+
version: lastAtIndex === -1 ? null : packageString.substring(lastAtIndex + 1),
|
|
98
|
+
scoped: false,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function parsePackageString(packageString) {
|
|
102
|
+
const normalPackageString = expandTilde(packageString);
|
|
103
|
+
if (isLocalPackageString(normalPackageString)) {
|
|
104
|
+
return parseLocalPackageString(normalPackageString);
|
|
105
|
+
}
|
|
106
|
+
else if (isScopedPackageString(normalPackageString)) {
|
|
107
|
+
return parseScopedPackageString(normalPackageString);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
return parseUnscopedPackageString(normalPackageString);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export scanning using oxc-parser and oxc-resolver
|
|
3
|
+
*
|
|
4
|
+
* This implementation follows the pattern from oxc-linter's export.rs
|
|
5
|
+
* using the public npm packages oxc-parser and oxc-resolver.
|
|
6
|
+
*/
|
|
7
|
+
type ResolvedExports = {
|
|
8
|
+
[key: string]: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Get all exports from a package
|
|
12
|
+
*
|
|
13
|
+
* This is the main entry point that matches the API of getAllExports in exports.utils.ts
|
|
14
|
+
*/
|
|
15
|
+
export declare function getAllExports(packageString: string, context: string, lookupPath: string, installPath?: string): Promise<ResolvedExports>;
|
|
16
|
+
/**
|
|
17
|
+
* Get exports details from code (compatibility function)
|
|
18
|
+
*
|
|
19
|
+
* This provides the same API as the existing getExportsDetails for backward compatibility
|
|
20
|
+
* Returns simple string arrays for exports (without moduleRequest info)
|
|
21
|
+
*/
|
|
22
|
+
export declare function getExportsDetails(code: string, filename?: string): {
|
|
23
|
+
exports: string[];
|
|
24
|
+
exportAllLocations: string[];
|
|
25
|
+
};
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export scanning using oxc-parser and oxc-resolver
|
|
3
|
+
*
|
|
4
|
+
* This implementation follows the pattern from oxc-linter's export.rs
|
|
5
|
+
* using the public npm packages oxc-parser and oxc-resolver.
|
|
6
|
+
*/
|
|
7
|
+
import { parseSync } from 'oxc-parser';
|
|
8
|
+
import { ResolverFactory } from 'oxc-resolver';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import Telemetry from './telemetry.utils.js';
|
|
12
|
+
import { performance } from 'perf_hooks';
|
|
13
|
+
// Initialize resolver with ESM-first configuration
|
|
14
|
+
// - main_fields: ["module", "main"] - prioritize ESM entry points
|
|
15
|
+
// - condition_names: ["import", "default", "require"] - ESM-first export conditions
|
|
16
|
+
// NOTE: We intentionally exclude "node" because Node.js conditional exports resolution
|
|
17
|
+
// uses the PACKAGE's exports field order (not our conditionNames order) to determine
|
|
18
|
+
// priority. Packages like Vue have "node" before "import" in their exports, so including
|
|
19
|
+
// "node" would resolve to CJS files instead of ESM. We keep "require" as a fallback for
|
|
20
|
+
// packages that only export via "require" condition.
|
|
21
|
+
// - extensions: all common JS/TS extensions
|
|
22
|
+
// - symlinks: false - keep paths as-is without resolving symlinks (matches enhanced-resolve behavior)
|
|
23
|
+
const resolver = new ResolverFactory({
|
|
24
|
+
extensions: [
|
|
25
|
+
'.mjs',
|
|
26
|
+
'.js',
|
|
27
|
+
'.mts',
|
|
28
|
+
'.ts',
|
|
29
|
+
'.jsx',
|
|
30
|
+
'.tsx',
|
|
31
|
+
'.cjs',
|
|
32
|
+
'.cts',
|
|
33
|
+
'.json',
|
|
34
|
+
],
|
|
35
|
+
mainFields: ['module', 'main'], // ESM-first: prioritize "module" field over "main"
|
|
36
|
+
conditionNames: ['import', 'default', 'require'], // ESM-first: exclude "node" which resolves to CJS
|
|
37
|
+
symlinks: false, // Don't resolve symlinks to match enhanced-resolve behavior
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Extract export information from parsed oxc module
|
|
41
|
+
*/
|
|
42
|
+
function getExportsFromStaticExports(staticExports) {
|
|
43
|
+
const exports = [];
|
|
44
|
+
const exportAllLocations = [];
|
|
45
|
+
staticExports.forEach(staticExport => {
|
|
46
|
+
staticExport.entries.forEach((entry) => {
|
|
47
|
+
var _a, _b;
|
|
48
|
+
// Skip type-only exports (TypeScript type exports)
|
|
49
|
+
if (entry.isType) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Handle different export types based on importName kind
|
|
53
|
+
switch (entry.importName.kind) {
|
|
54
|
+
case 'AllButDefault': // export * from "mod"
|
|
55
|
+
case 'All': // export * as ns from "mod"
|
|
56
|
+
if (entry.moduleRequest) {
|
|
57
|
+
exportAllLocations.push(entry.moduleRequest.value);
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
case 'Name': // export { foo } or export { foo } from "mod"
|
|
61
|
+
case 'None': // export const foo = 1
|
|
62
|
+
// Get the export name
|
|
63
|
+
if (entry.exportName.kind === 'Name' && entry.exportName.name) {
|
|
64
|
+
exports.push({
|
|
65
|
+
name: entry.exportName.name,
|
|
66
|
+
moduleRequest: (_a = entry.moduleRequest) === null || _a === void 0 ? void 0 : _a.value, // Track the source module for re-exports
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else if (entry.exportName.kind === 'Default') {
|
|
70
|
+
exports.push({
|
|
71
|
+
name: 'default',
|
|
72
|
+
moduleRequest: (_b = entry.moduleRequest) === null || _b === void 0 ? void 0 : _b.value,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
return { exports, exportAllLocations };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a module path from a given context
|
|
83
|
+
*/
|
|
84
|
+
async function resolveModule(context, lookupPath) {
|
|
85
|
+
const result = resolver.sync(context, lookupPath);
|
|
86
|
+
if (!result.path) {
|
|
87
|
+
throw new Error(`Cannot resolve module '${lookupPath}' from '${context}'`);
|
|
88
|
+
}
|
|
89
|
+
return result.path;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Recursively walk exports following export * statements
|
|
93
|
+
*
|
|
94
|
+
* This mirrors the walk_exported_recursive function in export.rs
|
|
95
|
+
*/
|
|
96
|
+
async function walkExportsRecursive(context, lookupPath, visited, rootContext, _isRootCall = false) {
|
|
97
|
+
// Use rootContext for calculating relative paths, context for resolution
|
|
98
|
+
const root = rootContext || context;
|
|
99
|
+
const resolvedPath = await resolveModule(context, lookupPath);
|
|
100
|
+
// Avoid circular dependencies
|
|
101
|
+
if (visited.has(resolvedPath)) {
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
visited.add(resolvedPath);
|
|
105
|
+
// Parse the file to get exports
|
|
106
|
+
const code = await fs.readFile(resolvedPath, 'utf8');
|
|
107
|
+
const parseResult = parseSync(resolvedPath, code, {
|
|
108
|
+
sourceType: 'module',
|
|
109
|
+
});
|
|
110
|
+
// Check if file has module syntax
|
|
111
|
+
if (!parseResult.module.hasModuleSyntax) {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
const { exports, exportAllLocations } = getExportsFromStaticExports(parseResult.module.staticExports);
|
|
115
|
+
const resolvedExports = {};
|
|
116
|
+
// Add direct exports from this module, resolving re-exports to their source files
|
|
117
|
+
for (const exp of exports) {
|
|
118
|
+
let sourcePath = resolvedPath;
|
|
119
|
+
// If this is a re-export (export { foo } from './module.js'), resolve to the source file
|
|
120
|
+
if (exp.moduleRequest) {
|
|
121
|
+
try {
|
|
122
|
+
sourcePath = await resolveModule(path.dirname(resolvedPath), exp.moduleRequest);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// If resolution fails, fall back to current file
|
|
126
|
+
sourcePath = resolvedPath;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Use path.relative() to calculate the relative path from root to sourcePath
|
|
130
|
+
// This works correctly since symlinks are not resolved (symlinks: false in resolver config)
|
|
131
|
+
const relativePath = root
|
|
132
|
+
? path.relative(root, sourcePath)
|
|
133
|
+
: path.basename(sourcePath);
|
|
134
|
+
resolvedExports[exp.name] = relativePath;
|
|
135
|
+
}
|
|
136
|
+
// Recursively process export * statements
|
|
137
|
+
const promises = exportAllLocations.map(async (location) => {
|
|
138
|
+
const starExports = await walkExportsRecursive(path.dirname(resolvedPath), location, visited, root);
|
|
139
|
+
// Merge star exports into our exports
|
|
140
|
+
Object.keys(starExports).forEach(expKey => {
|
|
141
|
+
resolvedExports[expKey] = starExports[expKey];
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
await Promise.all(promises);
|
|
145
|
+
return resolvedExports;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Get all exports from a package
|
|
149
|
+
*
|
|
150
|
+
* This is the main entry point that matches the API of getAllExports in exports.utils.ts
|
|
151
|
+
*/
|
|
152
|
+
export async function getAllExports(packageString, context, lookupPath, installPath) {
|
|
153
|
+
const startTime = performance.now();
|
|
154
|
+
const visited = new Set();
|
|
155
|
+
try {
|
|
156
|
+
// Read package.json to get the entry point
|
|
157
|
+
const packageJsonPath = path.join(context, 'package.json');
|
|
158
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
159
|
+
// Prefer module field for ESM, fallback to main, then default
|
|
160
|
+
let entryPoint = packageJson.module || packageJson.main || './index.js';
|
|
161
|
+
// Normalize entry point to start with ./
|
|
162
|
+
if (!entryPoint.startsWith('./') && !entryPoint.startsWith('../')) {
|
|
163
|
+
entryPoint = './' + entryPoint;
|
|
164
|
+
}
|
|
165
|
+
// Resolve the entry point relative to context
|
|
166
|
+
// Pass installPath as rootContext for calculating relative paths
|
|
167
|
+
const results = await walkExportsRecursive(context, entryPoint, visited, installPath, true);
|
|
168
|
+
Telemetry.walkPackageExportsTree(packageString, startTime, true);
|
|
169
|
+
return results;
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
Telemetry.walkPackageExportsTree(packageString, startTime, false, err);
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get exports details from code (compatibility function)
|
|
178
|
+
*
|
|
179
|
+
* This provides the same API as the existing getExportsDetails for backward compatibility
|
|
180
|
+
* Returns simple string arrays for exports (without moduleRequest info)
|
|
181
|
+
*/
|
|
182
|
+
export function getExportsDetails(code, filename = 'module.js') {
|
|
183
|
+
const parseResult = parseSync(filename, code, {
|
|
184
|
+
sourceType: 'module',
|
|
185
|
+
});
|
|
186
|
+
const result = getExportsFromStaticExports(parseResult.module.staticExports);
|
|
187
|
+
// Return simple string array for backward compatibility
|
|
188
|
+
return {
|
|
189
|
+
exports: result.exports.map(exp => exp.name),
|
|
190
|
+
exportAllLocations: result.exportAllLocations,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { InstallPackageOptions } from '../common.types.js';
|
|
2
|
+
declare const InstallationUtils: {
|
|
3
|
+
getInstallPath(packageName: string): string;
|
|
4
|
+
preparePath(packageName: string, clientOption?: "npm" | "yarn" | "pnpm" | "bun" | Array<"npm" | "yarn" | "pnpm" | "bun">): Promise<string>;
|
|
5
|
+
installPackage(packageString: string, installPath: string, installOptions: InstallPackageOptions): Promise<void>;
|
|
6
|
+
_installWithClient(packageString: string, installPath: string, installOptions: InstallPackageOptions, currentClient: "npm" | "yarn" | "pnpm" | "bun"): Promise<void>;
|
|
7
|
+
cleanupPath(installPath: string): Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export default InstallationUtils;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { rimraf } from 'rimraf';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import sanitize from 'sanitize-filename';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import createDebug from 'debug';
|
|
7
|
+
const debug = createDebug('bp:worker');
|
|
8
|
+
import { InstallError, PackageNotFoundError } from '../errors/CustomError.js';
|
|
9
|
+
import { exec } from './common.utils.js';
|
|
10
|
+
import config from '../config/config.js';
|
|
11
|
+
import Telemetry from './telemetry.utils.js';
|
|
12
|
+
import { performance } from 'perf_hooks';
|
|
13
|
+
// When operating on a local directory, force npm to copy directory structure
|
|
14
|
+
// and all dependencies instead of just symlinking files
|
|
15
|
+
const wrapPackCommand = (packagePath) => `$(npm pack --ignore-scripts ${packagePath} | tail -1)`;
|
|
16
|
+
const InstallationUtils = {
|
|
17
|
+
getInstallPath(packageName) {
|
|
18
|
+
const id = randomUUID().slice(0, 8);
|
|
19
|
+
return path.join(config.tmp, 'packages', sanitize(`build-${packageName}-${id}`));
|
|
20
|
+
},
|
|
21
|
+
async preparePath(packageName, clientOption) {
|
|
22
|
+
const startTime = performance.now();
|
|
23
|
+
const installPath = InstallationUtils.getInstallPath(packageName);
|
|
24
|
+
if (process.env.DEBUG_TIMING) {
|
|
25
|
+
console.log(`[TIMING] preparePath.getInstallPath: ${(performance.now() - startTime).toFixed(2)}ms`);
|
|
26
|
+
}
|
|
27
|
+
const step1 = performance.now();
|
|
28
|
+
await fs.mkdir(config.tmp, { recursive: true });
|
|
29
|
+
await fs.mkdir(installPath, { recursive: true });
|
|
30
|
+
if (process.env.DEBUG_TIMING) {
|
|
31
|
+
console.log(`[TIMING] preparePath.mkdir: ${(performance.now() - step1).toFixed(2)}ms`);
|
|
32
|
+
}
|
|
33
|
+
const step2 = performance.now();
|
|
34
|
+
// Check if yarn is being used (either as single value or in array)
|
|
35
|
+
const clients = clientOption
|
|
36
|
+
? Array.isArray(clientOption)
|
|
37
|
+
? clientOption
|
|
38
|
+
: [clientOption]
|
|
39
|
+
: [];
|
|
40
|
+
const isUsingYarn = clients.includes('yarn');
|
|
41
|
+
const packageJson = {
|
|
42
|
+
dependencies: {},
|
|
43
|
+
browserslist: [
|
|
44
|
+
'last 5 Chrome versions',
|
|
45
|
+
'last 5 Firefox versions',
|
|
46
|
+
'Safari >= 9',
|
|
47
|
+
'edge >= 12',
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
// Add packageManager field if yarn is being used (required by corepack)
|
|
51
|
+
if (isUsingYarn) {
|
|
52
|
+
packageJson.packageManager = 'yarn@1.22.22';
|
|
53
|
+
}
|
|
54
|
+
await fs.writeFile(path.join(installPath, 'package.json'), JSON.stringify(packageJson));
|
|
55
|
+
if (process.env.DEBUG_TIMING) {
|
|
56
|
+
console.log(`[TIMING] preparePath.writeFile: ${(performance.now() - step2).toFixed(2)}ms`);
|
|
57
|
+
console.log(`[TIMING] preparePath.total: ${(performance.now() - startTime).toFixed(2)}ms`);
|
|
58
|
+
}
|
|
59
|
+
return installPath;
|
|
60
|
+
},
|
|
61
|
+
async installPackage(packageString, installPath, installOptions) {
|
|
62
|
+
const { client = ['bun', 'npm'], // Default: try bun first, fallback to npm
|
|
63
|
+
limitConcurrency: _limitConcurrency, networkConcurrency: _networkConcurrency, additionalPackages: _additionalPackages = [], isLocal: _isLocal, installTimeout: _installTimeout = 45000, } = installOptions;
|
|
64
|
+
// Normalize client to array
|
|
65
|
+
const clients = Array.isArray(client) ? client : [client];
|
|
66
|
+
// Try each client in order until one succeeds
|
|
67
|
+
let lastError = null;
|
|
68
|
+
for (let i = 0; i < clients.length; i++) {
|
|
69
|
+
const currentClient = clients[i];
|
|
70
|
+
const isLastClient = i === clients.length - 1;
|
|
71
|
+
try {
|
|
72
|
+
await InstallationUtils._installWithClient(packageString, installPath, {
|
|
73
|
+
...installOptions,
|
|
74
|
+
client: currentClient,
|
|
75
|
+
}, currentClient);
|
|
76
|
+
// Success! Log which client was used
|
|
77
|
+
if (installOptions.debug || process.env.DEBUG_TIMING) {
|
|
78
|
+
console.log(`[INSTALL] Successfully installed with ${currentClient}`);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
lastError = error;
|
|
84
|
+
if (!isLastClient) {
|
|
85
|
+
// Try next client
|
|
86
|
+
debug(`Installation with ${currentClient} failed, trying next client...`);
|
|
87
|
+
if (installOptions.debug) {
|
|
88
|
+
console.log(`[INSTALL] ${currentClient} failed, trying ${clients[i + 1]}...`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// All clients failed
|
|
94
|
+
throw lastError;
|
|
95
|
+
},
|
|
96
|
+
async _installWithClient(packageString, installPath, installOptions, currentClient) {
|
|
97
|
+
let flags, command;
|
|
98
|
+
let installStartTime = performance.now();
|
|
99
|
+
const { limitConcurrency, networkConcurrency, additionalPackages = [], isLocal, installTimeout = 45000, } = installOptions;
|
|
100
|
+
if (currentClient === 'yarn') {
|
|
101
|
+
flags = [
|
|
102
|
+
'ignore-flags',
|
|
103
|
+
'ignore-engines',
|
|
104
|
+
'skip-integrity-check',
|
|
105
|
+
'exact',
|
|
106
|
+
'json',
|
|
107
|
+
'no-progress',
|
|
108
|
+
'silent',
|
|
109
|
+
'no-lockfile',
|
|
110
|
+
'no-bin-links',
|
|
111
|
+
'ignore-optional',
|
|
112
|
+
];
|
|
113
|
+
if (limitConcurrency) {
|
|
114
|
+
flags.push('mutex network');
|
|
115
|
+
}
|
|
116
|
+
if (networkConcurrency) {
|
|
117
|
+
flags.push(`network-concurrency ${networkConcurrency}`);
|
|
118
|
+
}
|
|
119
|
+
command = `yarn add ${packageString} ${additionalPackages.join(' ')} --${flags.join(' --')}`;
|
|
120
|
+
}
|
|
121
|
+
else if (currentClient === 'npm') {
|
|
122
|
+
flags = [
|
|
123
|
+
// Setting cache is required for concurrent `npm install`s to work
|
|
124
|
+
`cache=${path.join(config.tmp, 'cache')}`,
|
|
125
|
+
'no-package-lock',
|
|
126
|
+
'no-shrinkwrap',
|
|
127
|
+
'legacy-peer-deps',
|
|
128
|
+
'no-optional',
|
|
129
|
+
'no-bin-links',
|
|
130
|
+
'progress false',
|
|
131
|
+
'loglevel error',
|
|
132
|
+
'ignore-scripts',
|
|
133
|
+
'save-exact',
|
|
134
|
+
'production',
|
|
135
|
+
'json',
|
|
136
|
+
];
|
|
137
|
+
command = `npm install ${isLocal ? wrapPackCommand(packageString) : packageString} ${additionalPackages.join(' ')} --${flags.join(' --')}`;
|
|
138
|
+
}
|
|
139
|
+
else if (currentClient === 'pnpm') {
|
|
140
|
+
flags = ['no-optional', 'loglevel error', 'ignore-scripts', 'save-exact'];
|
|
141
|
+
command = `pnpm add ${packageString} ${additionalPackages.join(' ')} --${[].join(' --')}`;
|
|
142
|
+
}
|
|
143
|
+
else if (currentClient === 'bun') {
|
|
144
|
+
flags = [
|
|
145
|
+
'no-save', // Don't update package.json or save lockfile
|
|
146
|
+
'production', // Don't install devDependencies
|
|
147
|
+
'ignore-scripts', // Skip lifecycle scripts
|
|
148
|
+
'no-progress', // Disable progress bar
|
|
149
|
+
'silent', // Don't log anything
|
|
150
|
+
];
|
|
151
|
+
// Add network concurrency if specified
|
|
152
|
+
if (networkConcurrency) {
|
|
153
|
+
flags.push(`network-concurrency=${networkConcurrency}`);
|
|
154
|
+
}
|
|
155
|
+
command = `bun add ${packageString} ${additionalPackages.join(' ')} --cache-dir=${path.join(config.tmp, 'cache', 'bun')} --${flags.join(' --')}`;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.error('No valid client specified');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
debug('install start %s', packageString);
|
|
162
|
+
try {
|
|
163
|
+
const execStartTime = performance.now();
|
|
164
|
+
await exec(command, {
|
|
165
|
+
cwd: installPath,
|
|
166
|
+
maxBuffer: 1024 * 500,
|
|
167
|
+
}, installTimeout);
|
|
168
|
+
const execDuration = performance.now() - execStartTime;
|
|
169
|
+
if (process.env.DEBUG_TIMING) {
|
|
170
|
+
console.log(`[TIMING] installPackage.exec (${currentClient}): ${execDuration.toFixed(2)}ms`);
|
|
171
|
+
}
|
|
172
|
+
debug('install finish %s', packageString);
|
|
173
|
+
Telemetry.installPackage(packageString, true, installStartTime, {
|
|
174
|
+
...installOptions,
|
|
175
|
+
client: currentClient,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
if (installOptions.debug || process.env.DEBUG_TIMING) {
|
|
180
|
+
console.log(`[INSTALL ERROR] ${currentClient}:`, err);
|
|
181
|
+
}
|
|
182
|
+
Telemetry.installPackage(packageString, false, installStartTime, {
|
|
183
|
+
...installOptions,
|
|
184
|
+
client: currentClient,
|
|
185
|
+
});
|
|
186
|
+
if (typeof err === 'string' && err.includes('code E404')) {
|
|
187
|
+
throw new PackageNotFoundError(err);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
throw new InstallError(err);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
async cleanupPath(installPath) {
|
|
195
|
+
try {
|
|
196
|
+
await rimraf(installPath);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
console.error('cleaning up path ', installPath, ' failed due to ', err);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
export default InstallationUtils;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Emitter, EventType } from 'mitt';
|
|
2
|
+
type Events = Record<EventType, unknown>;
|
|
3
|
+
declare const emitter: Emitter<Events>;
|
|
4
|
+
export { emitter };
|
|
5
|
+
export default class Telemetry {
|
|
6
|
+
static installPackage(packageString: string, isSuccessful: boolean, startTime: number, options: any, error?: any): void;
|
|
7
|
+
static getPackageJSONDetails(packageName: string, isSuccessful: boolean, startTime: number, error?: any): void;
|
|
8
|
+
static buildPackage(packageName: string, isSuccessful: boolean, startTime: number, options: any, error?: any): void;
|
|
9
|
+
static compilePackage(packageName: string, isSuccessful: boolean, startTime: number, options: any, error?: any): void;
|
|
10
|
+
static packageStats(packageString: string, isSuccessful: boolean, startTime: number, options: any, error?: any): void;
|
|
11
|
+
static parseWebpackStats(packageName: string, isSuccessful: boolean, startTime: number, error?: any): void;
|
|
12
|
+
static dependencySizes(packageName: string, startTime: number, isSuccessful: boolean, options: any, error?: any): void;
|
|
13
|
+
static assetsGZIPParseTime(packageName: string, startTime: number): void;
|
|
14
|
+
static walkPackageExportsTree(packageString: string, startTime: number, isSuccessful: boolean, error?: any): void;
|
|
15
|
+
static packageExports(packageString: string, startTime: number, isSuccessful: boolean, error?: any): void;
|
|
16
|
+
static packageExportsSizes(packageString: string, startTime: number, isSuccessful: boolean, options: any, error?: any): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import mitt from 'mitt';
|
|
2
|
+
import { parsePackageString } from './common.utils.js';
|
|
3
|
+
import { performance } from 'perf_hooks';
|
|
4
|
+
import _ from 'lodash';
|
|
5
|
+
import createDebug from 'debug';
|
|
6
|
+
const debug = createDebug('bp-telemetry');
|
|
7
|
+
const emitter = mitt();
|
|
8
|
+
export { emitter };
|
|
9
|
+
emitter.on('*', (type, data) => {
|
|
10
|
+
debug('Telementry Event: %s %o', type, data);
|
|
11
|
+
});
|
|
12
|
+
function errorToObject(error) {
|
|
13
|
+
if (!error)
|
|
14
|
+
return;
|
|
15
|
+
if (error && typeof error === 'object') {
|
|
16
|
+
const errorObject = {};
|
|
17
|
+
Object.getOwnPropertyNames(error).forEach(key => {
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
errorObject[key] =
|
|
20
|
+
typeof error[key] === 'object'
|
|
21
|
+
? errorToObject(error[key])
|
|
22
|
+
: String(error[key]).substring(0, 40);
|
|
23
|
+
});
|
|
24
|
+
return errorObject;
|
|
25
|
+
}
|
|
26
|
+
return { error };
|
|
27
|
+
}
|
|
28
|
+
export default class Telemetry {
|
|
29
|
+
static installPackage(packageString, isSuccessful, startTime, options, error = null) {
|
|
30
|
+
emitter.emit('TASK_PACKAGE_INSTALL', {
|
|
31
|
+
package: parsePackageString(packageString),
|
|
32
|
+
isSuccessful,
|
|
33
|
+
duration: performance.now() - startTime,
|
|
34
|
+
options,
|
|
35
|
+
error: errorToObject(error),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
static getPackageJSONDetails(packageName, isSuccessful, startTime, error = null) {
|
|
39
|
+
emitter.emit('TASK_PACKAGE_JSON_DETAILS', {
|
|
40
|
+
package: { name: packageName },
|
|
41
|
+
isSuccessful,
|
|
42
|
+
duration: performance.now() - startTime,
|
|
43
|
+
error: errorToObject(error),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
static buildPackage(packageName, isSuccessful, startTime, options, error = null) {
|
|
47
|
+
emitter.emit('TASK_PACKAGE_BUILD', {
|
|
48
|
+
package: { name: packageName },
|
|
49
|
+
isSuccessful,
|
|
50
|
+
duration: performance.now() - startTime,
|
|
51
|
+
options: _.omit(options, 'customImports'),
|
|
52
|
+
error: errorToObject(error),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
static compilePackage(packageName, isSuccessful, startTime, options, error = null) {
|
|
56
|
+
emitter.emit('TASK_PACKAGE_COMPILE', {
|
|
57
|
+
packageName,
|
|
58
|
+
isSuccessful,
|
|
59
|
+
duration: performance.now() - startTime,
|
|
60
|
+
options,
|
|
61
|
+
error: errorToObject(error),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
static packageStats(packageString, isSuccessful, startTime, options, error = null) {
|
|
65
|
+
emitter.emit('TASK_PACKAGE_STATS', {
|
|
66
|
+
package: parsePackageString(packageString),
|
|
67
|
+
isSuccessful,
|
|
68
|
+
duration: performance.now() - startTime,
|
|
69
|
+
options,
|
|
70
|
+
error: errorToObject(error),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
static parseWebpackStats(packageName, isSuccessful, startTime, error = null) {
|
|
74
|
+
emitter.emit('TASK_PACKAGE_PARSE_WEBPACK_STATS', {
|
|
75
|
+
package: { name: packageName },
|
|
76
|
+
isSuccessful,
|
|
77
|
+
duration: performance.now() - startTime,
|
|
78
|
+
error: errorToObject(error),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
static dependencySizes(packageName, startTime, isSuccessful, options, error = null) {
|
|
82
|
+
emitter.emit('TASK_PACKAGE_DEPENDENCY_SIZES', {
|
|
83
|
+
package: { name: packageName },
|
|
84
|
+
duration: performance.now() - startTime,
|
|
85
|
+
isSuccessful,
|
|
86
|
+
options,
|
|
87
|
+
error: errorToObject(error),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
static assetsGZIPParseTime(packageName, startTime) {
|
|
91
|
+
emitter.emit('TASK_PACKAGE_ASSETS_GZIP_PARSE_TIME', {
|
|
92
|
+
package: { name: packageName },
|
|
93
|
+
duration: performance.now() - startTime,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
static walkPackageExportsTree(packageString, startTime, isSuccessful, error = null) {
|
|
97
|
+
emitter.emit('TASK_PACKAGE_EXPORTS_TREEWALK', {
|
|
98
|
+
package: parsePackageString(packageString),
|
|
99
|
+
isSuccessful,
|
|
100
|
+
duration: performance.now() - startTime,
|
|
101
|
+
error: errorToObject(error),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
static packageExports(packageString, startTime, isSuccessful, error = null) {
|
|
105
|
+
emitter.emit('TASK_PACKAGE_EXPORTS', {
|
|
106
|
+
package: parsePackageString(packageString),
|
|
107
|
+
isSuccessful,
|
|
108
|
+
duration: performance.now() - startTime,
|
|
109
|
+
error: errorToObject(error),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
static packageExportsSizes(packageString, startTime, isSuccessful, options, error = null) {
|
|
113
|
+
emitter.emit('TASK_PACKAGE_EXPORTS_SIZES', {
|
|
114
|
+
package: parsePackageString(packageString),
|
|
115
|
+
duration: performance.now() - startTime,
|
|
116
|
+
isSuccessful,
|
|
117
|
+
error: errorToObject(error),
|
|
118
|
+
options,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|