bob-the-bundler
Version:
Bob The Bundler!
355 lines (354 loc) • 15.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.validatePackageJson = exports.buildCommand = exports.DIST_DIR = void 0;
const tslib_1 = require("tslib");
const assert = tslib_1.__importStar(require("assert"));
const execa_1 = tslib_1.__importDefault(require("execa"));
const fse = tslib_1.__importStar(require("fs-extra"));
const globby_1 = tslib_1.__importDefault(require("globby"));
const p_limit_1 = tslib_1.__importDefault(require("p-limit"));
const path_1 = require("path");
const lodash_get_1 = tslib_1.__importDefault(require("lodash.get"));
const mkdirp_1 = tslib_1.__importDefault(require("mkdirp"));
const get_root_package_json_1 = require("../utils/get-root-package-json");
const get_workspaces_1 = require("../utils/get-workspaces");
const command_1 = require("../command");
const config_1 = require("../config");
const rewrite_exports_1 = require("../utils/rewrite-exports");
const bootstrap_1 = require("./bootstrap");
const get_workspace_package_paths_1 = require("../utils/get-workspace-package-paths");
exports.DIST_DIR = "dist";
/**
* A list of files that we don't need need within the published package.
* Also known as test files :)
* This list is derived from scouting various of our repositories.
*/
const filesToExcludeFromDist = [
"**/test/**",
"**/tests/**",
"**/__tests__/**",
"**/__testUtils__/**",
"**/*.spec.*",
"**/*.test.*",
"**/dist",
"**/temp",
];
const moduleMappings = {
esm: "es2022",
cjs: "commonjs",
};
function typeScriptCompilerOptions(target) {
return {
module: moduleMappings[target],
sourceMap: false,
inlineSourceMap: false,
};
}
function compilerOptionsToArgs(options) {
const args = [];
for (const [key, value] of Object.entries(options)) {
args.push(`--${key}`, `${value}`);
}
return args;
}
function assertTypeScriptBuildResult(result) {
if (result.exitCode !== 0) {
console.log("TypeScript compiler exited with non-zero exit code.");
console.log(result.stdout);
throw new Error("TypeScript compiler exited with non-zero exit code.");
}
}
async function buildTypeScript(buildPath, options = {}) {
assertTypeScriptBuildResult(await (0, execa_1.default)("npx", [
"tsc",
...compilerOptionsToArgs(typeScriptCompilerOptions("esm")),
...(options.incremental ? ["--incremental"] : []),
"--outDir",
(0, path_1.join)(buildPath, "esm"),
]));
assertTypeScriptBuildResult(await (0, execa_1.default)("npx", [
"tsc",
...compilerOptionsToArgs(typeScriptCompilerOptions("cjs")),
...(options.incremental ? ["--incremental"] : []),
"--outDir",
(0, path_1.join)(buildPath, "cjs"),
]));
}
exports.buildCommand = (0, command_1.createCommand)((api) => {
const { reporter } = api;
return {
command: "build",
describe: "Build",
builder(yargs) {
return yargs.options({
incremental: {
describe: "Better performance by building only packages that had changes.",
type: "boolean",
},
});
},
async handler({ incremental }) {
const cwd = process.cwd();
const rootPackageJSON = await (0, get_root_package_json_1.getRootPackageJSON)(cwd);
const workspaces = (0, get_workspaces_1.getWorkspaces)(rootPackageJSON);
const isSinglePackage = workspaces === null;
if (isSinglePackage) {
const buildPath = (0, path_1.join)(cwd, ".bob");
if (!incremental) {
await fse.remove(buildPath);
}
await buildTypeScript(buildPath, { incremental });
const pkg = await fse.readJSON((0, path_1.resolve)(cwd, "package.json"));
const fullName = pkg.name;
const distPath = (0, path_1.join)(cwd, "dist");
const getBuildPath = (target) => (0, path_1.join)(buildPath, target);
await build({
cwd,
pkg,
fullName,
reporter,
getBuildPath,
distPath,
});
return;
}
const limit = (0, p_limit_1.default)(4);
const workspacePackagePaths = await (0, get_workspace_package_paths_1.getWorkspacePackagePaths)(cwd, workspaces);
const packageInfoList = await Promise.all(workspacePackagePaths.map((packagePath) => limit(async () => {
const cwd = packagePath;
const pkg = await fse.readJSON((0, path_1.resolve)(cwd, "package.json"));
const fullName = pkg.name;
return { packagePath, cwd, pkg, fullName };
})));
const bobBuildPath = (0, path_1.join)(cwd, ".bob");
if (!incremental) {
await fse.remove(bobBuildPath);
}
await buildTypeScript(bobBuildPath, { incremental });
await Promise.all(packageInfoList.map(({ cwd, pkg, fullName }) => limit(async () => {
const getBuildPath = (target) => (0, path_1.join)(cwd.replace("packages", (0, path_1.join)(".bob", target)), "src");
const distPath = (0, path_1.join)(cwd, "dist");
await build({
cwd,
pkg,
fullName,
reporter,
getBuildPath,
distPath,
});
})));
},
};
});
const limit = (0, p_limit_1.default)(20);
async function build({ cwd, pkg, fullName, reporter, getBuildPath, distPath, }) {
var _a, _b, _c;
const config = (0, config_1.getBobConfig)(pkg);
if (config === false || (config === null || config === void 0 ? void 0 : config.build) === false) {
reporter.warn(`Skip build for '${fullName}'`);
return;
}
const declarations = await (0, globby_1.default)("**/*.d.ts", {
cwd: getBuildPath("esm"),
absolute: false,
ignore: filesToExcludeFromDist,
});
const esmFiles = await (0, globby_1.default)("**/*.js", {
cwd: getBuildPath("esm"),
absolute: false,
ignore: filesToExcludeFromDist,
});
// Check whether al esm files are empty, if not - probably a types only build
let emptyEsmFiles = true;
for (const file of esmFiles) {
const src = await fse.readFile((0, path_1.join)(getBuildPath("esm"), file));
if (src.toString().trim() !== "export {};") {
emptyEsmFiles = false;
break;
}
}
// Empty ESM files with existing declarations is a types-only package
const typesOnly = emptyEsmFiles && declarations.length > 0;
validatePackageJson(pkg, {
typesOnly,
includesCommonJS: (_a = config === null || config === void 0 ? void 0 : config.commonjs) !== null && _a !== void 0 ? _a : true,
});
// remove <project>/dist
await fse.remove(distPath);
// Copy type definitions
await fse.ensureDir((0, path_1.join)(distPath, "typings"));
await Promise.all(declarations.map((filePath) => limit(() => fse.copy((0, path_1.join)(getBuildPath("esm"), filePath), (0, path_1.join)(distPath, "typings", filePath)))));
// If ESM files are not empty, copy them to dist/esm
if (!emptyEsmFiles) {
await fse.ensureDir((0, path_1.join)(distPath, "esm"));
await Promise.all(esmFiles.map((filePath) => limit(() => fse.copy((0, path_1.join)(getBuildPath("esm"), filePath), (0, path_1.join)(distPath, "esm", filePath)))));
}
if (!emptyEsmFiles && (config === null || config === void 0 ? void 0 : config.commonjs) === undefined) {
// Transpile ESM to CJS and move CJS to dist/cjs only if there's something to transpile
await fse.ensureDir((0, path_1.join)(distPath, "cjs"));
const cjsFiles = await (0, globby_1.default)("**/*.js", {
cwd: getBuildPath("cjs"),
absolute: false,
ignore: filesToExcludeFromDist,
});
await Promise.all(cjsFiles.map((filePath) => limit(() => fse.copy((0, path_1.join)(getBuildPath("cjs"), filePath), (0, path_1.join)(distPath, "cjs", filePath)))));
// Add package.json to dist/cjs to ensure files are interpreted as commonjs
await fse.writeFile((0, path_1.join)(distPath, "cjs", "package.json"), JSON.stringify({ type: "commonjs" }));
// We need to provide .cjs extension type definitions as well :)
// https://github.com/ardatan/graphql-tools/discussions/4581#discussioncomment-3329673
const declarations = await (0, globby_1.default)("**/*.d.ts", {
cwd: getBuildPath("cjs"),
absolute: false,
ignore: filesToExcludeFromDist,
});
await Promise.all(declarations.map((filePath) => limit(async () => {
const contents = await fse.readFile((0, path_1.join)(getBuildPath("cjs"), filePath), "utf-8");
await fse.writeFile((0, path_1.join)(distPath, "typings", filePath.replace(/\.d\.ts/, ".d.cts")), contents
.replace(/\.js";\n/g, `.cjs";\n`)
.replace(/\.js';\n/g, `.cjs';\n`));
})));
}
// move the package.json to dist
await fse.writeFile((0, path_1.join)(distPath, "package.json"), JSON.stringify(rewritePackageJson(pkg, typesOnly), null, 2));
// move README.md and LICENSE and other specified files
await copyToDist(cwd, ["README.md", "LICENSE", ...((_c = (_b = config === null || config === void 0 ? void 0 : config.build) === null || _b === void 0 ? void 0 : _b.copy) !== null && _c !== void 0 ? _c : [])], distPath);
if (pkg.bin) {
if (globalThis.process.platform === "win32") {
console.warn("Package includes bin files, but cannot set the executable bit on Windows.\n" +
"Please manually set the executable bit on the bin files before publishing.");
}
else {
await Promise.all(Object.values(pkg.bin).map((filePath) => (0, execa_1.default)("chmod", ["+x", (0, path_1.join)(cwd, filePath)])));
}
}
reporter.success(`Built ${pkg.name}`);
}
function rewritePackageJson(pkg, typesOnly) {
const newPkg = {};
const fields = [
"name",
"version",
"description",
"sideEffects",
"peerDependencies",
"dependencies",
"optionalDependencies",
"repository",
"homepage",
"keywords",
"author",
"license",
"engines",
"name",
"main",
"module",
"typings",
"typescript",
"type",
];
fields.forEach((field) => {
if (typeof pkg[field] !== "undefined") {
newPkg[field] = pkg[field];
}
});
const distDirStr = `${exports.DIST_DIR}/`;
if (typesOnly) {
newPkg.main = "";
delete newPkg.module;
delete newPkg.type;
}
else {
newPkg.main = newPkg.main.replace(distDirStr, "");
newPkg.module = newPkg.module.replace(distDirStr, "");
}
newPkg.typings = newPkg.typings.replace(distDirStr, "");
newPkg.typescript = {
definition: newPkg.typescript.definition.replace(distDirStr, ""),
};
if (!typesOnly) {
if (!pkg.exports) {
newPkg.exports = bootstrap_1.presetFields.exports;
}
newPkg.exports = (0, rewrite_exports_1.rewriteExports)(pkg.exports, exports.DIST_DIR);
}
if (pkg.bin) {
newPkg.bin = {};
for (const alias in pkg.bin) {
newPkg.bin[alias] = pkg.bin[alias].replace(distDirStr, "");
}
}
return newPkg;
}
function validatePackageJson(pkg, opts) {
var _a;
function expect(key, expected) {
const received = (0, lodash_get_1.default)(pkg, key);
assert.deepEqual(received, expected, `${pkg.name}: "${key}" equals "${JSON.stringify(received)}"` +
`, should be "${JSON.stringify(expected)}".`);
}
// Type only packages have simpler rules (following the style of https://github.com/DefinitelyTyped/DefinitelyTyped packages)
if (opts.typesOnly) {
expect("main", "");
expect("module", undefined);
expect("typings", bootstrap_1.presetFields.typings);
expect("typescript.definition", bootstrap_1.presetFields.typescript.definition);
expect("exports", undefined);
return;
}
// If the package has NO binary we need to check the exports map.
// a package should either
// 1. have a bin property
// 2. have a exports property
// 3. have an exports and bin property
if (Object.keys((_a = pkg.bin) !== null && _a !== void 0 ? _a : {}).length > 0) {
if (opts.includesCommonJS === true) {
expect("main", bootstrap_1.presetFields.main);
expect("module", bootstrap_1.presetFields.module);
expect("typings", bootstrap_1.presetFields.typings);
expect("typescript.definition", bootstrap_1.presetFields.typescript.definition);
}
else {
expect("main", bootstrap_1.presetFieldsESM.main);
expect("module", bootstrap_1.presetFieldsESM.module);
expect("typings", bootstrap_1.presetFieldsESM.typings);
expect("typescript.definition", bootstrap_1.presetFieldsESM.typescript.definition);
}
}
else if (pkg.main !== undefined ||
pkg.module !== undefined ||
pkg.exports !== undefined ||
pkg.typings !== undefined ||
pkg.typescript !== undefined) {
if (opts.includesCommonJS === true) {
// if there is no bin property, we NEED to check the exports.
expect("main", bootstrap_1.presetFields.main);
expect("module", bootstrap_1.presetFields.module);
expect("typings", bootstrap_1.presetFields.typings);
expect("typescript.definition", bootstrap_1.presetFields.typescript.definition);
// For now we enforce a top level exports property
expect("exports['.'].require", bootstrap_1.presetFields.exports["."].require);
expect("exports['.'].import", bootstrap_1.presetFields.exports["."].import);
expect("exports['.'].default", bootstrap_1.presetFields.exports["."].default);
}
else {
expect("main", bootstrap_1.presetFieldsESM.main);
expect("module", bootstrap_1.presetFieldsESM.module);
expect("typings", bootstrap_1.presetFieldsESM.typings);
expect("typescript.definition", bootstrap_1.presetFieldsESM.typescript.definition);
// For now we enforce a top level exports property
expect("exports['.']", bootstrap_1.presetFieldsESM.exports["."]);
}
}
}
exports.validatePackageJson = validatePackageJson;
async function copyToDist(cwd, files, distDir) {
const allFiles = await (0, globby_1.default)(files, { cwd });
return Promise.all(allFiles.map(async (file) => {
if (await fse.pathExists((0, path_1.join)(cwd, file))) {
const sourcePath = (0, path_1.join)(cwd, file);
const destPath = (0, path_1.join)(distDir, file.replace("src/", ""));
await (0, mkdirp_1.default)((0, path_1.dirname)(destPath));
await fse.copyFile(sourcePath, destPath);
}
}));
}