UNPKG

bob-the-bundler

Version:
355 lines (354 loc) • 15.5 kB
"use strict"; 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); } })); }