build-ts-package.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. #!/usr/bin/env node
  2. import fs from "fs/promises";
  3. import path from "node:path";
  4. import { build } from "tsdown";
  5. import createCLI from "yargs";
  6. import { hideBin } from "yargs/helpers";
  7. /**
  8. * @typedef {import('tsdown').Format} Format
  9. */
  10. /**
  11. * returns the path of the found tsconfig file
  12. * or uses the provided override, instead,
  13. * if it's available
  14. *
  15. * @param {string} cwd
  16. * @param {string | undefined | null} tsconfigOverride
  17. */
  18. async function findTsconfigFile(cwd, tsconfigOverride) {
  19. if (tsconfigOverride) {
  20. const overridePath = path.isAbsolute(tsconfigOverride)
  21. ? tsconfigOverride
  22. : path.join(cwd, tsconfigOverride);
  23. return overridePath;
  24. }
  25. const locations = [
  26. path.join(cwd, "tsconfig.build.json"),
  27. path.join(cwd, "tsconfig.json"),
  28. ];
  29. for (const fp of locations) {
  30. try {
  31. const stat = await fs.stat(fp);
  32. if (stat.isFile()) return fp;
  33. } catch {}
  34. }
  35. return null;
  36. }
  37. /**
  38. * builds a typescript package, using tsdown and its Node-friendly API
  39. * @returns {Promise<void>}
  40. */
  41. export async function buildTsPackage(argv = process.argv) {
  42. const yargs = createCLI(hideBin(argv));
  43. const {
  44. all,
  45. cwd,
  46. noCjs,
  47. noDts,
  48. noEsm,
  49. outDir,
  50. tsconfig: tsconfigOverride,
  51. watch,
  52. } = await yargs
  53. .scriptName("build-ts-package")
  54. .option("all", {
  55. default: false,
  56. description:
  57. "if true, will compile ALL files in your source folder and link them to your package.json. this is only required if you do not have an index.ts or index.tsx entrypoint that exports all of the things you want users to use.",
  58. type: "boolean",
  59. })
  60. .option("cwd", {
  61. default: process.cwd(),
  62. description: "the CWD to use when building",
  63. type: "string",
  64. })
  65. .option("noCjs", {
  66. default: false,
  67. description:
  68. "if true, will not build the CommonJS variant of this package",
  69. type: "boolean",
  70. })
  71. .option("noDts", {
  72. default: false,
  73. description: "if set, will not write typescript typings",
  74. type: "boolean",
  75. })
  76. .option("noEsm", {
  77. default: false,
  78. description: "if true, will not build the ESM variant of this package",
  79. type: "boolean",
  80. })
  81. .option("outDir", {
  82. default: "dist",
  83. description: "the folder where the built files will be written",
  84. type: "string",
  85. })
  86. .option("tsconfig", {
  87. description:
  88. "if provided, will explicitly use this tsconfig.json location instead of searching for a tsconfig.build.json or a plain tsconfig.json",
  89. type: "string",
  90. })
  91. .option("watch", {
  92. default: false,
  93. description:
  94. "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",
  95. type: "boolean",
  96. })
  97. .help().argv;
  98. const outDirPath = path.isAbsolute(outDir) ? outDir : path.join(cwd, outDir);
  99. // ESM Must come before CJS, as those typings and such take precedence
  100. // when dual publishing.
  101. const formats = /** @type {Format[]} */ (
  102. [noEsm ? undefined : "esm", noCjs ? undefined : "cjs"].filter(Boolean)
  103. );
  104. const tsconfig = await findTsconfigFile(cwd, tsconfigOverride);
  105. if (!tsconfig) {
  106. throw new Error(`unable to build ${cwd} because no tsconfig was found`);
  107. }
  108. const pjsonPath = path.join(path.dirname(tsconfig), "package.json");
  109. const numFormats = formats.length;
  110. for (const format of formats) {
  111. console.info(`building ${format} variant in ${cwd}`);
  112. console.info(` tsconfig: ${tsconfig}`);
  113. await build({
  114. clean: false,
  115. cwd,
  116. dts: !noDts,
  117. entry: [
  118. "./src/**/*.ts",
  119. "./src/**/*.tsx",
  120. // ignore all storybook entrypoints
  121. "!./src/**/*.stories.ts",
  122. "!./src/**/*.test.ts",
  123. "!./src/**/*.test.tsx",
  124. "!./src/**/*.spec.ts",
  125. "!./src/**/*.spec.tsx",
  126. "!./src/**/*.stories.tsx",
  127. "!./src/**/*.stories.mdx",
  128. ],
  129. exports:
  130. format === "esm" || numFormats <= 1 ? { all, devExports: true } : false,
  131. // do not attempt to resolve or import CSS, SCSS or SVG files
  132. external: [/\.s?css$/, /\.svg$/],
  133. format,
  134. outDir: path.join(outDirPath, format),
  135. platform: "neutral",
  136. tsconfig,
  137. unbundle: true,
  138. watch,
  139. });
  140. }
  141. if (numFormats > 1) {
  142. // we need to manually set the cjs exports, since tsdown
  143. // isn't yet capable of doing this for us
  144. /** @type {import('type-fest').PackageJson} */
  145. const pjson = JSON.parse(await fs.readFile(pjsonPath, "utf8"));
  146. if (!pjson.publishConfig?.exports) return;
  147. for (const exportKey of Object.keys(pjson.publishConfig.exports)) {
  148. // @ts-expect-error - we can definitely index here, so please be silenced!
  149. const exportPath = String(pjson.publishConfig.exports[exportKey]);
  150. // skip over all package.json files
  151. if (exportPath.includes('package.json')) continue;
  152. // @ts-expect-error - we can definitely index here, so please be silenced!
  153. pjson.publishConfig.exports[exportKey] = {
  154. import: exportPath,
  155. require: exportPath
  156. .replace(path.extname(exportPath), ".cjs")
  157. .replace(`${path.sep}esm${path.sep}`, `${path.sep}cjs${path.sep}`),
  158. types: exportPath.replace(path.extname(exportPath), ".d.ts"),
  159. };
  160. if (pjson.main) {
  161. pjson.main = pjson.main.replace(`${path.sep}esm${path.sep}`, `${path.sep}cjs${path.sep}`);
  162. }
  163. }
  164. await fs.writeFile(pjsonPath, JSON.stringify(pjson, undefined, 2), "utf8");
  165. }
  166. }
  167. await buildTsPackage();