Files
metabase/bin/generate-license-disclaimer.js
Fraser 355a5fc29c Use bun package manager (#68170)
* Use bun in package.json

* Update workflows

* Update more workflows

* Kick ci

* Fix yaml

* Fix type errors

* Compat with snyk

* Fix jest resolutions

* Add another esm package

* Fix aliases and next js missing deps

* Update next js embed tests

* Try fix embed test config

* Fix

* Fix requires in e2e support

* Use bun not yarn in embed bin

* Revert config changes

* Use bun in more scripts and use prepare-release action

* Replace more yarn usages

* Remove overrides from bunfig move to package json

* Revert e2e support changes

* Add back comment

* [ci skip] comment typo

* Update bun lock

* Update to reference bun

* Update to reference bun

* Update license disclaimer

* Tidy tests

* Update to use bun in workflows

* Change aliases

* Update codemirrort selector

* Tidy license generation

* Remove .only

* Fix yarn references

* Checkout .github for release scripts

* Update lockfile

* Re-add package

* update release bunlock

* fix checkout check

---------

Co-authored-by: Ryan Laurie <iethree@gmail.com>
2026-02-10 16:27:57 +00:00

227 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
// Usage: node bin/generate-license-disclaimer.js [output-path]
// If output-path is not provided, writes to resources/license-frontend-third-party.txt
const fs = require("fs");
const path = require("path");
const OUTPUT_FILE = path.join(
__dirname,
"..",
"resources",
"license-frontend-third-party.txt",
);
const BUN_MODULES_DIR = path.join(__dirname, "..", "node_modules", ".bun");
const LICENSE_FILE_NAMES = [
"LICENSE",
"LICENSE.md",
"LICENSE.txt",
"license",
"license.md",
"license.txt",
"LICENCE",
"LICENCE.md",
"LICENCE.txt",
"COPYING",
"COPYING.md",
"COPYING.txt",
];
function normalizeRepoUrl(url) {
if (!url) {
return undefined;
}
return url
.replace(/^git\+/, "")
.replace(/\.git$/, "")
.replace(/^git:\/\//, "https://");
}
/**
* Parse a bun package folder name like "@scope+pkg@1.0.0" or "lodash@4.17.21"
* Returns { name, version } or undefined if invalid.
*/
function parseBunFolderName(folderName) {
const lastAtIndex = folderName.lastIndexOf("@");
if (lastAtIndex <= 0) {
return undefined;
}
const name = folderName.slice(0, lastAtIndex).replace(/\+/g, "/");
const version = folderName.slice(lastAtIndex + 1);
return { name, version };
}
/**
* Scan node_modules/.bun/ and return array of packages with their paths.
*/
function scanBunPackages(bunModulesDir) {
if (!fs.existsSync(bunModulesDir)) {
throw new Error(`Bun modules directory not found: ${bunModulesDir}`);
}
const folders = fs.readdirSync(bunModulesDir);
const packages = [];
for (const folder of folders) {
const parsed = parseBunFolderName(folder);
if (!parsed) {
continue;
}
const packagePath = path.join(
bunModulesDir,
folder,
"node_modules",
parsed.name,
);
if (!fs.existsSync(packagePath)) {
continue;
}
packages.push({
name: parsed.name,
version: parsed.version,
path: packagePath,
});
}
return packages;
}
function findLicenseFile(packagePath) {
for (const name of LICENSE_FILE_NAMES) {
const licensePath = path.join(packagePath, name);
if (fs.existsSync(licensePath)) {
return fs.readFileSync(licensePath, "utf-8").trim();
}
}
return undefined;
}
function getPackageInfo(packagePath) {
const pkgJsonPath = path.join(packagePath, "package.json");
if (!fs.existsSync(pkgJsonPath)) {
return undefined;
}
try {
return JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
} catch {
return undefined;
}
}
/**
* Generate disclaimer text from package list.
*
* @param {Array} packages - Array of {name, version, path}
* @param {function} getPackageDetails - Callback (pkg) => {licenseText, repoUrl, homepage}
* @returns {string} The disclaimer text
*/
function generateDisclaimerText(packages, getPackageDetails) {
const enriched = packages.map((pkg) => {
const details = getPackageDetails(pkg);
return {
...pkg,
licenseText:
details.licenseText ||
`License: See ${details.homepage || pkg.name} package`,
repoUrl: details.repoUrl,
};
});
const groups = new Map();
for (const pkg of enriched) {
const normalizedRepo = normalizeRepoUrl(pkg.repoUrl);
const key = `${normalizedRepo || pkg.name}|||${pkg.licenseText}`;
if (!groups.has(key)) {
groups.set(key, {
packages: [],
licenseText: pkg.licenseText,
repoUrl: pkg.repoUrl,
});
}
groups.get(key).packages.push(pkg);
}
const sortedGroups = Array.from(groups.values()).sort((a, b) => {
const aName = a.packages[0].name.toLowerCase();
const bName = b.packages[0].name.toLowerCase();
return aName.localeCompare(bName);
});
const lines = [
"THE FOLLOWING SETS FORTH ATTRIBUTION NOTICES FOR THIRD PARTY SOFTWARE THAT MAY BE CONTAINED IN PORTIONS OF THE METABASE PRODUCT.",
"",
];
for (const group of sortedGroups) {
group.packages.sort((a, b) => a.name.localeCompare(b.name));
const uniqueNames = [...new Set(group.packages.map((p) => p.name))];
const packageList = uniqueNames.join(", ");
let sourceInfo = "";
if (group.repoUrl) {
if (uniqueNames.length > 1) {
const urlParts = uniqueNames.map(
(name) => `${group.repoUrl} (${name})`,
);
sourceInfo = `A copy of the source code may be downloaded from ${urlParts.join(", ")}.`;
} else {
sourceInfo = `A copy of the source code may be downloaded from ${group.repoUrl}.`;
}
}
lines.push("-----");
lines.push("");
lines.push(
`The following software may be included in this product: ${packageList}.${sourceInfo ? " " + sourceInfo : ""} This software contains the following license and notice below:`,
);
lines.push("");
lines.push(group.licenseText);
lines.push("");
}
return lines.join("\n");
}
if (require.main === module) {
// Allow overriding output path via command line argument (useful for tests)
const outputPath = process.argv[2] || OUTPUT_FILE;
console.log("Scanning bun packages...");
const packages = scanBunPackages(BUN_MODULES_DIR);
console.log(`Found ${packages.length} packages`);
const output = generateDisclaimerText(packages, (pkg) => {
const pkgJson = getPackageInfo(pkg.path) || {};
const licenseFile = findLicenseFile(pkg.path);
// Fall back to license field from package.json if no LICENSE file found
const licenseText = licenseFile || pkgJson.license || undefined;
const repo = pkgJson.repository;
const repoUrl =
(typeof repo === "string" ? repo : repo?.url) ||
pkgJson.homepage ||
undefined;
return {
licenseText,
repoUrl: repoUrl,
homepage: pkgJson.homepage,
};
});
fs.writeFileSync(outputPath, output);
console.log(`Generated ${outputPath}`);
}
module.exports = {
generateDisclaimerText,
parseBunFolderName,
scanBunPackages,
};