build-ts-package.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. #!/usr/bin/env node
  2. import fs from "fs-extra";
  3. import path from "node:path";
  4. import createCLI from "yargs";
  5. import { hideBin } from "yargs/helpers";
  6. import { findTsconfigFile } from "./find-tsconfig-file.js";
  7. import { execAsync } from "./exec-async.js";
  8. import { generateTsconfigs } from "./generate-tsconfigs.js";
  9. import { Logger } from "./logger.js";
  10. import chalk from "chalk";
  11. import { compileTs } from "./compile-ts.js";
  12. import { injectExtraExports } from "./inject-extra-exports.js";
  13. /**
  14. * builds a typescript package, using tsdown and its Node-friendly API
  15. * @returns {Promise<void>}
  16. */
  17. export async function buildTsPackage(argv = process.argv) {
  18. const yargs = createCLI(hideBin(argv));
  19. const {
  20. clean,
  21. cwd: absOrRelativeCwd,
  22. generateTsconfig,
  23. noCjs,
  24. noDts,
  25. noEsm,
  26. outDir,
  27. tsconfig: tsconfigOverride,
  28. watch,
  29. } = await yargs
  30. .scriptName("build-ts-package")
  31. .option("clean", {
  32. default: false,
  33. description:
  34. "if set, will clean out the build dirs before compiling anything",
  35. type: "boolean",
  36. })
  37. .option("cwd", {
  38. default: process.cwd(),
  39. description: "the CWD to use when building",
  40. type: "string",
  41. })
  42. .option("generateTsconfig", {
  43. default: false,
  44. description:
  45. "if set, will NOT build, but instead, will generate reasonable default TSConfig files that will work with dual publishing, and in most other use cases, as well",
  46. type: "boolean",
  47. })
  48. .option("noCjs", {
  49. default: false,
  50. description:
  51. "if true, will not build the CommonJS variant of this package",
  52. type: "boolean",
  53. })
  54. .option("noDts", {
  55. default: false,
  56. description: "if set, will not write typescript typings",
  57. type: "boolean",
  58. })
  59. .option("noEsm", {
  60. default: false,
  61. description: "if true, will not build the ESM variant of this package",
  62. type: "boolean",
  63. })
  64. .option("outDir", {
  65. default: "dist",
  66. description: "the folder where the built files will be written",
  67. type: "string",
  68. })
  69. .option("tsconfig", {
  70. description:
  71. "if provided, will explicitly use this tsconfig.json location instead of searching for a tsconfig.build.json or a plain tsconfig.json",
  72. type: "string",
  73. })
  74. .option("watch", {
  75. default: false,
  76. description:
  77. "if set, will automatically watch for any changes to this library and rebuild, making it easier for you to consume changes in the monorepo while doing local development",
  78. type: "boolean",
  79. })
  80. .help().argv;
  81. const cwd = path.isAbsolute(absOrRelativeCwd)
  82. ? absOrRelativeCwd
  83. : path.resolve(absOrRelativeCwd);
  84. if (generateTsconfig) {
  85. return generateTsconfigs(cwd);
  86. }
  87. const outDirPath = path.isAbsolute(outDir) ? outDir : path.join(cwd, outDir);
  88. if (clean) await fs.remove(outDirPath);
  89. // ESM Must come before CJS, as those typings and such take precedence
  90. // when dual publishing.
  91. /** @type {any[]} */
  92. const formats = [noEsm ? undefined : "esm", noCjs ? undefined : "cjs"].filter(
  93. Boolean,
  94. );
  95. const tsconfig = await findTsconfigFile(cwd, tsconfigOverride);
  96. if (!tsconfig) {
  97. throw new Error(`unable to build ${cwd} because no tsconfig was found`);
  98. }
  99. const pjsonPath = path.join(path.dirname(tsconfig), "package.json");
  100. const numFormats = formats.length;
  101. const pjson = JSON.parse(await fs.readFile(pjsonPath, "utf8"));
  102. // always freshly reset the exports and let the tool take over
  103. pjson.exports = {};
  104. // let the exports define how this module should be treated
  105. delete pjson.type;
  106. Logger.info("building package", chalk.magenta(pjson.name));
  107. for (const format of formats) {
  108. try {
  109. Logger.info("building", chalk.magenta(format), "variant in", cwd);
  110. Logger.info(" tsconfig", chalk.magenta(tsconfig));
  111. const outDir =
  112. numFormats <= 1 ? outDirPath : path.join(outDirPath, format);
  113. const getConfigCmd = `pnpm tsc --project ${tsconfig} --showConfig`;
  114. const finalConfig = JSON.parse(
  115. await execAsync(getConfigCmd, { cwd, stdio: "pipe", verbose: true }),
  116. );
  117. /** @type {string[]} */
  118. const tscFoundFiles = Array.isArray(finalConfig.files)
  119. ? finalConfig.files
  120. : [];
  121. const absoluteBuiltFiles = await compileTs({
  122. cwd,
  123. entryPoints: tscFoundFiles,
  124. format,
  125. noDts,
  126. outDir,
  127. tsconfig,
  128. watch,
  129. });
  130. const builtFiles = absoluteBuiltFiles
  131. .map((fp) => {
  132. const relPath = path.relative(outDir, fp);
  133. if (numFormats <= 1) return `.${path.sep}${relPath}`;
  134. return `.${path.sep}${path.join(format, relPath)}`;
  135. })
  136. .sort();
  137. const indexFile = builtFiles.find((fp) => {
  138. const r = /^\.(\/|\\)((cjs|esm)(\/|\\))?index\.(c|m)?js$/;
  139. return r.test(fp);
  140. });
  141. if (indexFile) {
  142. const fixedIndexFile = `./${path.join(path.basename(outDirPath), indexFile)}`;
  143. Logger.info("index file detected");
  144. if (format === "cjs" || numFormats <= 1) {
  145. // we use the legacy type of typing exports for the top-level
  146. // typings
  147. pjson.types = fixedIndexFile.replace(
  148. path.extname(indexFile),
  149. ".d.ts",
  150. );
  151. }
  152. if (format === "esm") {
  153. pjson.module = fixedIndexFile;
  154. } else {
  155. pjson.main = fixedIndexFile;
  156. }
  157. }
  158. const exports =
  159. Array.isArray(pjson.exports) || typeof pjson.exports === "string"
  160. ? {}
  161. : (pjson.exports ?? {});
  162. const outDirBasename = path.basename(outDirPath);
  163. for (const fp of builtFiles) {
  164. const fpWithNoExt = fp
  165. .replace(/(\.d)?\.(c|m)?(js|ts)$/, "")
  166. .replaceAll(/\\/g, "/");
  167. const key = fpWithNoExt
  168. .replace(/(\/|\\)?index$/, "")
  169. .replace(/^\.(\/|\\)(cjs|esm)/, ".")
  170. .replaceAll(/\\/g, "/");
  171. const fpWithBasename = `./${path
  172. .join(outDirBasename, fp)
  173. .replaceAll(/\\/g, "/")}`;
  174. // Ensure key object exists
  175. const tempExports = exports[key] ?? {};
  176. // Pick target (default / import / require)
  177. const target =
  178. numFormats <= 1
  179. ? tempExports
  180. : format === "cjs"
  181. ? (tempExports.require ??= {})
  182. : (tempExports.import ??= {});
  183. // Assign default JS entry
  184. target.default = fpWithBasename;
  185. // Assign type definitions if applicable
  186. if (!noDts && fp.endsWith(".d.ts")) {
  187. target.types = fpWithBasename;
  188. }
  189. exports[key] = tempExports;
  190. }
  191. pjson.exports = exports;
  192. if (format === "esm") {
  193. await fs.writeFile(
  194. path.join(outDir, "package.json"),
  195. '{ "type": "module" }',
  196. "utf8",
  197. );
  198. }
  199. Logger.info(chalk.green(`${pjson.name} - ${format} has been built!`));
  200. } catch (error) {
  201. Logger.error(
  202. "**building",
  203. pjson.name,
  204. chalk.underline(format),
  205. "variant has failed**",
  206. );
  207. throw error;
  208. }
  209. }
  210. pjson.exports["./package.json"] = "./package.json";
  211. const injected = injectExtraExports(pjson);
  212. await fs.writeFile(pjsonPath, JSON.stringify(injected, null, 2), "utf8");
  213. }
  214. await buildTsPackage();