Skip to content

Commit a8b91db

Browse files
committed
[infra] Abstract tsdown based build-new command
This is done in a way so that we could potentially remove/replace tsdown in future with a new bundler (if required). Also, this doesn't change our current `code-infra build` command with flags.
1 parent 8414c83 commit a8b91db

File tree

11 files changed

+1371
-238
lines changed

11 files changed

+1371
-238
lines changed

packages/code-infra/bin/code-infra.mjs

Lines changed: 0 additions & 2 deletions
This file was deleted.

packages/code-infra/package.json

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"./babel-config": "./src/babel-config.mjs"
1818
},
1919
"bin": {
20-
"code-infra": "./bin/code-infra.mjs"
20+
"code-infra": "./src/bin/code-infra.mjs"
2121
},
2222
"scripts": {
2323
"typescript": "tsc -p tsconfig.json",
@@ -70,8 +70,14 @@
7070
"peerDependencies": {
7171
"eslint": "^9.0.0",
7272
"prettier": "^3.5.3",
73+
"tsdown": "^0.14.2",
7374
"typescript": "^5.0.0"
7475
},
76+
"peerDependenciesMeta": {
77+
"tsdown": {
78+
"optional": true
79+
}
80+
},
7581
"devDependencies": {
7682
"@types/babel__core": "^7.20.5",
7783
"@types/babel__preset-env": "^7.10.0",
@@ -86,13 +92,6 @@
8692
"prettier": "^3.6.2",
8793
"typescript-eslint": "^8.40.0"
8894
},
89-
"files": [
90-
"bin",
91-
"build",
92-
"src",
93-
"README.md",
94-
"LICENSE"
95-
],
9695
"publishConfig": {
9796
"access": "public"
9897
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env node
2+
import { run } from '../cli/index.mjs';
3+
4+
run();
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { builtinModules } from 'node:module';
2+
import * as fs from 'node:fs/promises';
3+
import { build as tsdown } from 'tsdown';
4+
import {
5+
getTsConfigPath,
6+
getVersionEnvVariables,
7+
processExportsToEntry,
8+
writePkgJson,
9+
} from '../utils/build.mjs';
10+
11+
/**
12+
* @typedef {import('../cli/cmdBuildNew.mjs').BaseArgs} Args
13+
*/
14+
15+
/**
16+
* @typedef {import('../cli/cmdBuildNew.mjs').PackageJson} PackageJson
17+
*/
18+
19+
/**
20+
* @param {Args} args
21+
* @param {PackageJson} pkgJson
22+
* @returns {Promise<void>}
23+
*/
24+
export async function build(args, pkgJson) {
25+
const cwd = process.cwd();
26+
27+
const outDir = /** @type {any} */ (pkgJson.publishConfig)?.directory;
28+
await fs.rm(outDir, {
29+
recursive: true,
30+
force: true,
31+
});
32+
33+
const [exportEntries, , binEntries] = await processExportsToEntry(
34+
pkgJson.exports ?? {},
35+
pkgJson.bin ?? {},
36+
{ cwd },
37+
);
38+
const externals = new Set([
39+
...Object.keys(pkgJson.dependencies || {}),
40+
...Object.keys(pkgJson.peerDependencies || {}),
41+
]);
42+
/**
43+
* @type {(string|RegExp)[]}
44+
*/
45+
const externalsArray = Array.from(externals);
46+
externalsArray.push(new RegExp(`^(${externalsArray.join('|')})/`));
47+
externalsArray.push(/^node:/);
48+
externalsArray.push(...builtinModules);
49+
50+
const tsconfigPath = await getTsConfigPath(cwd);
51+
const bannerText = `/**
52+
* ${pkgJson.name} v${pkgJson.version}
53+
*
54+
* @license ${pkgJson.license ?? 'MIT'}
55+
* This source code is licensed under the ${pkgJson.license} license found in the
56+
* LICENSE file in the root directory of this source tree.
57+
*/
58+
`;
59+
60+
/**
61+
* @type {import('tsdown').Options}
62+
*/
63+
const baseOptions = {
64+
watch: args.watch || false,
65+
config: false,
66+
outDir,
67+
unbundle: true,
68+
clean: false,
69+
skipNodeModulesBundle: true,
70+
external: externalsArray,
71+
platform: 'neutral',
72+
ignoreWatch: ['**/node_modules/**', '**/dist/**', '**/build/**'],
73+
env: {
74+
...getVersionEnvVariables(pkgJson.version ?? ''),
75+
},
76+
loader: {
77+
'.js': 'jsx',
78+
},
79+
logLevel: args.verbose ? 'info' : 'silent',
80+
tsconfig: tsconfigPath ?? undefined,
81+
sourcemap: false,
82+
banner: {
83+
js: bannerText,
84+
dts: bannerText,
85+
css: bannerText,
86+
},
87+
minify: 'dce-only',
88+
};
89+
90+
/**
91+
* @type {Promise<void>[]}
92+
*/
93+
const promises = [];
94+
95+
/**
96+
* @type {Record<string, {paths: string[]; isBin?: boolean}>}
97+
*/
98+
const outChunks = {};
99+
100+
if (Object.keys(exportEntries).length > 0) {
101+
args.bundle.forEach((format) => {
102+
promises.push(
103+
tsdown({
104+
...baseOptions,
105+
entry: exportEntries,
106+
format,
107+
dts: tsconfigPath
108+
? {
109+
cwd,
110+
tsconfig: tsconfigPath,
111+
emitJs: false,
112+
compilerOptions: {
113+
jsx: 'react-jsx',
114+
outDir,
115+
},
116+
}
117+
: false,
118+
outputOptions: {
119+
plugins: [
120+
{
121+
name: `get-output-chunks-${format}`,
122+
writeBundle(_ctx, chunks) {
123+
Object.entries(chunks).forEach(([fileName, chunk]) => {
124+
if (chunk.type !== 'chunk' || !chunk.isEntry) {
125+
return;
126+
}
127+
const chunkName = chunk.name.endsWith('.d')
128+
? chunk.name.slice(0, -2)
129+
: chunk.name;
130+
outChunks[chunkName] = outChunks[chunkName] || { paths: [] };
131+
outChunks[chunkName].paths.push(fileName);
132+
});
133+
},
134+
},
135+
],
136+
},
137+
}),
138+
);
139+
});
140+
}
141+
142+
if (Object.keys(binEntries).length > 0) {
143+
const format = pkgJson.type === 'module' ? 'esm' : 'cjs';
144+
promises.push(
145+
tsdown({
146+
...baseOptions,
147+
tsconfig: undefined,
148+
entry: binEntries,
149+
format,
150+
platform: 'node',
151+
outputOptions: {
152+
plugins: [
153+
{
154+
name: 'get-output-chunks-bin',
155+
writeBundle(_ctx, chunks) {
156+
Object.entries(chunks).forEach(([fileName, chunk]) => {
157+
if (chunk.type !== 'chunk' || !chunk.isEntry) {
158+
return;
159+
}
160+
outChunks[chunk.name] = outChunks[chunk.name] || { paths: [], isBin: true };
161+
outChunks[chunk.name].paths.push(fileName);
162+
});
163+
},
164+
},
165+
],
166+
},
167+
}),
168+
);
169+
}
170+
await Promise.all(promises);
171+
await writePkgJson(pkgJson, outChunks, {
172+
usePkgType: true,
173+
});
174+
}

packages/code-infra/src/cli/cmdBuild.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
308308
} = args;
309309

310310
const cwd = process.cwd();
311+
performance.mark('build-start');
311312
const pkgJsonPath = path.join(cwd, 'package.json');
312313
const packageJson = JSON.parse(await fs.readFile(pkgJsonPath, { encoding: 'utf8' }));
313314
const buildDirBase = packageJson.publishConfig?.directory;
@@ -443,5 +444,8 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
443444
outputDir: buildDir,
444445
addTypes: buildTypes,
445446
});
447+
performance.mark('build-end');
448+
const measure = performance.measure('build', 'build-start', 'build-end');
449+
console.log(`✅ Built "${packageJson.name}" in ${(measure.duration / 1000).toFixed(3)}s.`);
446450
},
447451
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/* eslint-disable no-console */
2+
import * as fs from 'node:fs/promises';
3+
import * as path from 'node:path';
4+
import { validatePkgJson } from '../utils/build.mjs';
5+
6+
/**
7+
* @typedef {import('./cmdBuild.mjs').Args & {watch?: boolean}} BaseArgs
8+
*/
9+
10+
/**
11+
* @typedef {BaseArgs & { bundler: 'tsdown' }} Args
12+
*/
13+
14+
/**
15+
* @typedef {Partial<import('../../package.json')>} PackageJson
16+
*/
17+
18+
/**
19+
* @type {import('../utils/build.mjs').BundleType[]}
20+
*/
21+
const validBundles = ['esm', 'cjs'];
22+
23+
/**
24+
* @type {Args['bundler'][]}
25+
*/
26+
const validBundlers = ['tsdown'];
27+
28+
export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
29+
command: 'build-new',
30+
describe: 'Builds the package for publishing.',
31+
builder(yargs) {
32+
return yargs
33+
.option('bundler', {
34+
demandOption: true,
35+
type: 'string',
36+
choices: validBundlers,
37+
description: 'The bundler to use for building the package.',
38+
default: 'tsdown',
39+
})
40+
.option('bundle', {
41+
array: true,
42+
demandOption: true,
43+
type: 'string',
44+
choices: validBundles,
45+
description: 'Bundles to output',
46+
default: ['esm', 'cjs'],
47+
})
48+
.option('hasLargeFiles', {
49+
type: 'boolean',
50+
default: false,
51+
describe: 'Set to `true` if you know you are transpiling large files.',
52+
})
53+
.option('skipBundlePackageJson', {
54+
type: 'boolean',
55+
default: false,
56+
describe:
57+
"Set to `true` if you don't want to generate a package.json file in the bundle output.",
58+
})
59+
.option('cjsOutDir', {
60+
default: '.',
61+
type: 'string',
62+
description: 'The directory to output the cjs files to.',
63+
})
64+
.option('verbose', {
65+
type: 'boolean',
66+
default: false,
67+
description: 'Enable verbose logging.',
68+
})
69+
.option('buildTypes', {
70+
type: 'boolean',
71+
default: true,
72+
description: 'Whether to build types for the package.',
73+
})
74+
.option('skipTsc', {
75+
type: 'boolean',
76+
default: false,
77+
description: 'Skip running TypeScript compiler (tsc) for building types.',
78+
})
79+
.option('ignore', {
80+
type: 'string',
81+
array: true,
82+
description: 'Extra globs to be ignored by Babel.',
83+
default: [],
84+
})
85+
.option('skipBabelRuntimeCheck', {
86+
type: 'boolean',
87+
default: false,
88+
description: 'Skip checking for Babel runtime dependencies in the package.',
89+
})
90+
.option('skipPackageJson', {
91+
type: 'boolean',
92+
default: false,
93+
description: 'Skip generating the package.json file in the bundle output.',
94+
})
95+
.option('watch', {
96+
type: 'boolean',
97+
default: false,
98+
description: 'Watch files for changes and rebuild automatically.',
99+
});
100+
},
101+
async handler({ bundler, ...args }) {
102+
const cwd = process.cwd();
103+
performance.mark('build-start');
104+
const pkgJson = JSON.parse(await fs.readFile(path.join(cwd, 'package.json'), 'utf8'));
105+
106+
if (!bundler) {
107+
throw new Error('No bundler specified');
108+
}
109+
console.log(`⚒️ Building ${pkgJson.name} using 📦 "${bundler}"`);
110+
validatePkgJson(pkgJson);
111+
112+
switch (bundler) {
113+
case 'tsdown':
114+
default:
115+
await import('../bundlers/tsdown.mjs').then(({ build }) => build(args, pkgJson));
116+
break;
117+
}
118+
performance.mark('build-end');
119+
const measure = performance.measure('build', 'build-start', 'build-end');
120+
console.log(`✅ Built "${pkgJson.name}" in ${(measure.duration / 1000).toFixed(3)}s.`);
121+
},
122+
});

0 commit comments

Comments
 (0)