mirror of
https://github.com/microsoft/playwright.git
synced 2026-09-14 14:08:10 +08:00
244 lines
8.4 KiB
JavaScript
244 lines
8.4 KiB
JavaScript
/**
|
|
* Copyright (c) Microsoft Corporation.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
// @ts-check
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ROOT = path.join(__dirname, '..', '..');
|
|
const BUILTIN_SET = new Set(require('module').builtinModules);
|
|
|
|
/** @type {Map<string, { license: string, repository?: string }>} */
|
|
const _licenseCache = new Map();
|
|
|
|
/**
|
|
* @param {*} pkg
|
|
* @returns {string}
|
|
*/
|
|
function normalizeRepositoryUrlToGitHub(pkg) {
|
|
let url = typeof pkg.repository === 'object' ? pkg.repository.url : pkg.repository;
|
|
if (typeof url !== 'string')
|
|
throw new Error(`Malformed repository for ${pkg.name}@${pkg.version}: ${JSON.stringify(pkg.repository)}`);
|
|
url = url.replace(/^git@github\.com:/, 'https://github.com/');
|
|
url = url.replace(/^git:\/\/git@github\.com/, 'https://github.com');
|
|
url = url.replace(/^git\+ssh:\/\/git@github\.com/, 'https://github.com');
|
|
url = url.replace(/^git\+https:\/\//, 'https://');
|
|
url = url.replace(/^http:\/\//, 'https://');
|
|
url = url.replace(/^git:\/\//, 'https://');
|
|
url = url.replace(/^github:/, 'https://github.com/');
|
|
url = url.replace(/\.git$/, '');
|
|
if (url.match(/^[\w-.]+\/[\w-.]+$/))
|
|
url = 'https://github.com/' + url;
|
|
if (!url.match(/^https:\/\/github\.com\/([\w-.]+\/)+[\w-.]+$/))
|
|
throw new Error(`Malformed repository for ${pkg.name}@${pkg.version}: ${JSON.stringify(pkg.repository)}`);
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* @param {string} dir
|
|
* @returns {string | undefined}
|
|
*/
|
|
function readLicenseText(dir) {
|
|
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
const patterns = [/^LICENSE$/i, /^LICENSE-\w+$/i];
|
|
for (const pattern of patterns) {
|
|
for (const file of files) {
|
|
if (!file.isFile())
|
|
continue;
|
|
if (pattern.test(path.basename(file.name, path.extname(file.name))))
|
|
return fs.readFileSync(path.join(dir, file.name), 'utf8').trim();
|
|
}
|
|
}
|
|
// Note: this list ensures we do not accidentally miss a license for some package.
|
|
const knownPackagesWithoutLicense = ['proxy-agent-negotiate'];
|
|
if (!knownPackagesWithoutLicense.some(pkg => dir.endsWith(path.sep + pkg)))
|
|
throw new Error(`Cannot locate license file for ${dir}`);
|
|
}
|
|
|
|
/**
|
|
* @param {string} dir
|
|
* @returns {{ license: string, repository?: string }}
|
|
*/
|
|
function licenseInfoForDir(dir) {
|
|
let info = _licenseCache.get(dir);
|
|
if (!info) {
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
let license = readLicenseText(dir);
|
|
if (!license) {
|
|
if (!pkg.license)
|
|
throw new Error(`Cannot determine license for ${pkg.name}@${pkg.version} at ${dir}`);
|
|
license = `(no license text found; declared licenses: ${pkg.license})`;
|
|
}
|
|
info = {
|
|
repository: normalizeRepositoryUrlToGitHub(pkg),
|
|
license,
|
|
};
|
|
_licenseCache.set(dir, info);
|
|
}
|
|
return info;
|
|
}
|
|
|
|
/** Extract the owning npm package directory from an input path like
|
|
* "node_modules/foo/src/index.js" or
|
|
* "packages/x/node_modules/@scope/foo/lib/x.js". */
|
|
function packageDirForInput(inputPath) {
|
|
const segments = inputPath.split('/');
|
|
const nmIdx = segments.lastIndexOf('node_modules');
|
|
if (nmIdx === -1)
|
|
return null;
|
|
const next = segments[nmIdx + 1];
|
|
if (!next)
|
|
return null;
|
|
const pkgSegs = next.startsWith('@')
|
|
? [next, segments[nmIdx + 2]]
|
|
: [next];
|
|
if (pkgSegs.some(s => !s))
|
|
return null;
|
|
return path.join(ROOT, ...segments.slice(0, nmIdx), 'node_modules', ...pkgSegs);
|
|
}
|
|
|
|
/** Read "name@version" from a package directory, or null. */
|
|
function packageKeyForDir(pkgDir) {
|
|
try {
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'));
|
|
if (!pkg.name || !pkg.version)
|
|
return null;
|
|
return `${pkg.name}@${pkg.version}`;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Given one bundle's metafile output, return the deduped sorted list of inlined
|
|
* npm packages as { key: "name@version", dir } entries. */
|
|
function inlinedPackages(outInfo) {
|
|
const byKey = new Map();
|
|
for (const inputPath of Object.keys(outInfo.inputs)) {
|
|
if (inputPath.startsWith('(disabled):'))
|
|
continue;
|
|
const dir = packageDirForInput(inputPath);
|
|
if (!dir)
|
|
continue;
|
|
const key = packageKeyForDir(dir);
|
|
if (key && !byKey.has(key))
|
|
byKey.set(key, dir);
|
|
}
|
|
return [...byKey.keys()].sort().map(key => ({ key, dir: byKey.get(key) }));
|
|
}
|
|
|
|
function fmtKB(bytes) {
|
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
}
|
|
|
|
/** Write the .js.txt report (inlined files + externals + sizes). */
|
|
function writeBundleReport(result, outFile, outInfo) {
|
|
const inputEntries = Object.entries(outInfo.inputs)
|
|
.filter(([p]) => !p.startsWith('(disabled):'))
|
|
.map(([p, info]) => ({ path: p, bytes: info.bytesInOutput || 0 }))
|
|
.sort((a, b) => a.path.localeCompare(b.path));
|
|
|
|
const externals = new Set();
|
|
for (const { path: inFile } of inputEntries) {
|
|
const meta = result.metafile.inputs[inFile];
|
|
if (!meta)
|
|
continue;
|
|
for (const imp of meta.imports || []) {
|
|
if (!imp.external)
|
|
continue;
|
|
if (imp.path.startsWith('node:'))
|
|
continue;
|
|
if (BUILTIN_SET.has(imp.path))
|
|
continue;
|
|
externals.add(imp.path);
|
|
}
|
|
}
|
|
const sortedExternals = [...externals].sort();
|
|
const maxBytes = Math.max(0, ...inputEntries.map(e => e.bytes));
|
|
const bytesColWidth = fmtKB(maxBytes).length;
|
|
|
|
const lines = [];
|
|
lines.push(`# ${path.relative(ROOT, outFile)}`);
|
|
lines.push(`# total: ${fmtKB(outInfo.bytes)}`);
|
|
lines.push('');
|
|
lines.push(`## Inlined (${inputEntries.length})`);
|
|
for (const { path: f, bytes } of inputEntries)
|
|
lines.push(` ${fmtKB(bytes).padStart(bytesColWidth)} ${f}`);
|
|
lines.push('');
|
|
lines.push(`## External (${sortedExternals.length})`);
|
|
for (const e of sortedExternals)
|
|
lines.push(` ${e}`);
|
|
lines.push('');
|
|
|
|
fs.writeFileSync(outFile + '.txt', lines.join('\n'));
|
|
return { inputCount: inputEntries.length, externalCount: sortedExternals.length };
|
|
}
|
|
|
|
/** Write the .js.LICENSE sidecar. No-op for bundles with no inlined
|
|
* third-party packages. */
|
|
function writeBundleLicenses(outFile, outInfo) {
|
|
const packages = inlinedPackages(outInfo);
|
|
if (packages.length === 0)
|
|
return 0;
|
|
|
|
const lines = [];
|
|
lines.push(`${path.relative(ROOT, outFile)}`);
|
|
lines.push('');
|
|
lines.push('THIRD-PARTY SOFTWARE NOTICES AND INFORMATION');
|
|
lines.push('');
|
|
lines.push('The following npm packages are inlined into this bundle.');
|
|
lines.push('');
|
|
for (const { key, dir } of packages) {
|
|
const info = licenseInfoForDir(dir);
|
|
const repo = info.repository ? info.repository : '';
|
|
lines.push(`- ${key}${repo ? ` (${repo})` : ''}`);
|
|
}
|
|
for (const { key, dir } of packages) {
|
|
const info = licenseInfoForDir(dir);
|
|
lines.push('');
|
|
lines.push(`%% ${key} NOTICES AND INFORMATION BEGIN HERE`);
|
|
lines.push('=========================================');
|
|
lines.push(info.license);
|
|
lines.push('=========================================');
|
|
lines.push(`END OF ${key} NOTICES AND INFORMATION`);
|
|
}
|
|
lines.push('');
|
|
lines.push('SUMMARY');
|
|
lines.push('=========================================');
|
|
lines.push(`Total Packages: ${packages.length}`);
|
|
lines.push('=========================================');
|
|
|
|
fs.writeFileSync(outFile + '.LICENSE', lines.join('\n'));
|
|
return packages.length;
|
|
}
|
|
|
|
/** Top-level entry called by EsbuildStep after each bundled build. */
|
|
function writeReports(result) {
|
|
if (!result.metafile)
|
|
return;
|
|
for (const [outFile, outInfo] of Object.entries(result.metafile.outputs)) {
|
|
if (outFile.endsWith('.map'))
|
|
continue;
|
|
const { inputCount, externalCount } = writeBundleReport(result, outFile, outInfo);
|
|
const licCount = writeBundleLicenses(outFile, outInfo);
|
|
const rel = path.relative(ROOT, outFile);
|
|
const licFragment = licCount ? `, ${licCount} licenses` : '';
|
|
console.log(` bundle: ${rel} (${inputCount} files, ${externalCount} external${licFragment}, ${fmtKB(outInfo.bytes)})`);
|
|
}
|
|
}
|
|
|
|
module.exports = { writeReports };
|