Skip to content

Commit d13229c

Browse files
authored
feat: support multiple output files (#1785)
Don't try to force the output target to be `dist/index.min.js` if there are multiple `build.config.entryPoints` entries. Also allow specifying `build.bundleSizeMax` as a map to allow setting a max bundle size for each output file.
1 parent 2d5d48e commit d13229c

File tree

13 files changed

+1122
-848
lines changed

13 files changed

+1122
-848
lines changed

package-lock.json

Lines changed: 922 additions & 797 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/build/index.js

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ const defaults = merge.bind({
2626
* @param {GlobalOptions & BuildOptions} argv
2727
*/
2828
const build = async (argv) => {
29-
const outfile = path.join(paths.dist, 'index.min.js')
3029
const globalName = pascalCase(pkg.name)
3130
const umdPre = `(function (root, factory) {(typeof module === 'object' && module.exports) ? module.exports = factory() : root.${globalName} = factory()}(typeof self !== 'undefined' ? self : this, function () {`
3231
const umdPost = `return ${globalName}}));`
@@ -36,32 +35,47 @@ const build = async (argv) => {
3635
entryPoint = fromRoot('dist', 'src', 'index.js')
3736
}
3837

39-
const result = await esbuild.build(defaults(
40-
{
41-
entryPoints: [entryPoint],
42-
bundle: true,
43-
format: 'iife',
44-
conditions: ['production'],
45-
sourcemap: argv.bundlesize,
46-
minify: true,
47-
globalName,
48-
banner: { js: umdPre },
49-
footer: { js: umdPost },
50-
metafile: argv.bundlesize,
51-
outfile,
52-
define: {
53-
global: 'globalThis',
54-
'process.env.NODE_ENV': '"production"'
55-
}
56-
},
57-
argv.fileConfig.build.config
58-
))
38+
/** @type {esbuild.BuildOptions} */
39+
const config = defaults({
40+
bundle: true,
41+
format: 'iife',
42+
conditions: ['production'],
43+
sourcemap: true,
44+
minify: true,
45+
globalName,
46+
banner: { js: umdPre },
47+
footer: { js: umdPost },
48+
metafile: true,
49+
define: {
50+
global: 'globalThis',
51+
'process.env.NODE_ENV': '"production"'
52+
}
53+
}, argv.fileConfig.build.config)
54+
55+
// use default single-file build
56+
if (config.entryPoints == null) {
57+
config.entryPoints = [entryPoint]
58+
config.outfile = path.join(paths.dist, 'index.min.js')
59+
}
60+
61+
const entryPoints = Array.isArray(config.entryPoints) ? config.entryPoints : Object.keys(config.entryPoints)
62+
63+
// support multi-output-file build
64+
if (entryPoints.length > 1) {
65+
delete config.outfile
66+
67+
if (config.outdir == null) {
68+
config.outdir = 'dist'
69+
}
70+
}
71+
72+
const result = await esbuild.build(config)
5973

60-
if (result.metafile) {
74+
if (result.metafile && argv.bundlesize) {
6175
fs.writeJSONSync(path.join(paths.dist, 'stats.json'), result.metafile)
6276
}
6377

64-
return outfile
78+
return result.metafile?.outputs
6579
}
6680

6781
const tasks = new Listr([
@@ -88,24 +102,41 @@ const tasks = new Listr([
88102
* @param {Task} task
89103
*/
90104
task: async (ctx, task) => {
91-
const outfile = await build(ctx)
105+
const outputs = await build(ctx)
92106

93-
if (ctx.bundlesize) {
94-
const gzip = await gzipSize(outfile)
95-
const maxSize = bytes(ctx.bundlesizeMax)
107+
if (ctx.bundlesize && outputs != null) {
108+
task.output = 'Use https://esbuild.github.io/analyze/ to load "./dist/stats.json".'
96109

97-
if (maxSize == null) {
98-
throw new Error(`Could not parse bytes from "${ctx.bundlesizeMax}"`)
99-
}
110+
const maxSizes = typeof ctx.bundlesizeMax === 'string'
111+
? {
112+
'dist/index.min.js': ctx.bundlesizeMax
113+
}
114+
: ctx.bundlesizeMax
100115

101-
const diff = gzip - maxSize
116+
for (const file of Object.keys(outputs)) {
117+
if (file.endsWith('.map')) {
118+
continue
119+
}
102120

103-
task.output = 'Use https://esbuild.github.io/analyze/ to load "./dist/stats.json".'
121+
if (maxSizes[file] == null) {
122+
task.output = `Size for ${file} missing from bundlesizeMax in .aegir.js`
123+
continue
124+
}
125+
126+
const gzip = await gzipSize(file)
127+
const maxSize = bytes(maxSizes[file])
128+
129+
if (maxSize == null) {
130+
throw new Error(`Could not parse bytes from "${ctx.bundlesizeMax}"`)
131+
}
132+
133+
const diff = gzip - maxSize
104134

105-
if (diff > 0) {
106-
throw new Error(`${bytes(gzip)} (▲${bytes(diff)} / ${bytes(maxSize)})`)
107-
} else {
108-
task.output = `${bytes(gzip)} (▼${bytes(diff)} / ${bytes(maxSize)})`
135+
if (diff > 0) {
136+
throw new Error(`${file} ${bytes(gzip)} (▲${bytes(diff)} / ${bytes(maxSize)})`)
137+
} else {
138+
task.output = `${file} ${bytes(gzip)} (▼${bytes(diff)} / ${bytes(maxSize)})`
139+
}
109140
}
110141
}
111142
}

src/test/browser.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ export default async (argv, execaOptions) => {
3030
const files = argv.files.length > 0
3131
? argv.files
3232
: [
33-
'test/**/*.spec.{js,cjs,mjs}',
34-
'test/browser.{js,cjs,mjs}',
35-
'dist/test/**/*.spec.{js,cjs,mjs}',
36-
'dist/test/browser.{js,cjs,mjs}'
33+
'test/**/*.spec.*js',
34+
'test/browser.*js',
35+
'dist/test/**/*.spec.*js',
36+
'dist/test/browser.*js'
3737
]
3838

3939
// before hook

src/test/electron.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ export default async (argv, execaOptions) => {
2323
const files = argv.files.length > 0
2424
? [...argv.files]
2525
: [
26-
'test/**/*.spec.{js,mjs,cjs}',
27-
'dist/test/**/*.spec.{js,cjs,mjs}'
26+
'test/**/*.spec.*js',
27+
'dist/test/**/*.spec.*js'
2828
]
2929
const grep = argv.grep ? ['--grep', argv.grep] : []
3030
const progress = argv.progress ? ['--reporter=progress'] : []

src/test/node.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ export default async function testNode (argv, execaOptions) {
3434
const files = argv.files.length > 0
3535
? argv.files
3636
: [
37-
'test/node.{js,cjs,mjs}',
38-
'test/**/*.spec.{js,cjs,mjs}',
39-
'dist/test/node.{js,cjs,mjs}',
40-
'dist/test/**/*.spec.{js,cjs,mjs}'
37+
'test/node.*js',
38+
'test/**/*.spec.*js',
39+
'dist/test/node.*js',
40+
'dist/test/**/*.spec.*js'
4141
]
4242

4343
const args = [

src/test/react-native.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ export default async (argv, execaOptions) => {
2929
const files = argv.files.length > 0
3030
? argv.files
3131
: [
32-
'test/**/*.spec.{js,ts,cjs,mjs}',
33-
'test/browser.{js,ts,cjs,mjs}'
32+
'test/**/*.spec.*js',
33+
'test/**/*.spec.ts',
34+
'test/browser.*js',
35+
'test/browser.ts'
3436
]
3537

3638
// before hook

src/types.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,19 @@ interface BuildOptions {
121121
*/
122122
bundlesize: boolean
123123
/**
124-
* Max threshold for the bundle size.
125-
*/
126-
bundlesizeMax: string
124+
* Max threshold for the bundle size(s). Either a single string for when only
125+
* `dist/index.min.js` is built or an object of key/value pairs, e.g.
126+
*
127+
* ```json
128+
* {
129+
* "bundlesizeMax": {
130+
* "dist/index.min.js": "50KB",
131+
* "dist/worker.min.js": "80KB"
132+
* }
133+
* }
134+
* ```
135+
*/
136+
bundlesizeMax: string | Record<string, string>
127137
/**
128138
* Build the Typescript type declarations.
129139
*/

test/build.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,23 @@ describe('build', () => {
4646
expect(fs.existsSync(join(projectDir, 'dist', 'index.min.js'))).to.be.true()
4747
})
4848
})
49+
50+
describe('multiple output files', () => {
51+
let projectDir = ''
52+
53+
before(async () => {
54+
projectDir = await setUpProject('a-multiple-output-project')
55+
})
56+
57+
it('should build a typescript project with multiple outputs', async function () {
58+
this.timeout(120 * 1000) // slow ci is slow
59+
60+
await execa(bin, ['build'], {
61+
cwd: projectDir
62+
})
63+
64+
expect(fs.existsSync(join(projectDir, 'dist', 'index.js'))).to.be.true()
65+
expect(fs.existsSync(join(projectDir, 'dist', 'sw.js'))).to.be.true()
66+
})
67+
})
4968
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// @ts-check
2+
3+
/** @type {import('../../../..').PartialOptions} */
4+
const options = {
5+
build: {
6+
config: {
7+
entryPoints: ['./src/index.js', './src/sw.js']
8+
},
9+
bundlesizeMax: {
10+
'dist/index.js': '400KB',
11+
'dist/sw.js': '400KB'
12+
}
13+
}
14+
}
15+
16+
export default options
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "a-ts-project",
3+
"version": "1.0.0",
4+
"description": "",
5+
"homepage": "https://github.com/ipfs/aegir#readme",
6+
"exports": {
7+
".": {
8+
"import": "./dist/src/index.js"
9+
}
10+
},
11+
"type": "module",
12+
"types": "./dist/src/index.d.ts",
13+
"scripts": {
14+
"build": "aegir build",
15+
"test": "aegir test"
16+
},
17+
"author": "",
18+
"license": "ISC",
19+
"eslintConfig": {
20+
"extends": "ipfs",
21+
"parserOptions": {
22+
"sourceType": "module"
23+
}
24+
}
25+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function main (): string {
2+
return 'hello world!'
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function main (): string {
2+
return 'hello world from sw!'
3+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
// project options
5+
"outDir": "dist",
6+
"allowJs": true,
7+
"checkJs": true,
8+
"target": "ES2020",
9+
"module": "ES2020",
10+
"lib": ["ES2021", "ES2021.Promise", "ES2021.String", "ES2020.BigInt", "DOM", "DOM.Iterable"],
11+
"noEmit": false,
12+
"noEmitOnError": true,
13+
"emitDeclarationOnly": false,
14+
"declaration": true,
15+
"declarationMap": true,
16+
"incremental": true,
17+
"composite": true,
18+
"isolatedModules": true,
19+
"removeComments": false,
20+
"sourceMap": true,
21+
// module resolution
22+
"esModuleInterop": true,
23+
"moduleResolution": "node",
24+
// linter checks
25+
"noImplicitReturns": false,
26+
"noFallthroughCasesInSwitch": true,
27+
"noUnusedLocals": true,
28+
"noUnusedParameters": false,
29+
// advanced
30+
"verbatimModuleSyntax": true,
31+
"forceConsistentCasingInFileNames": true,
32+
"skipLibCheck": true,
33+
"stripInternal": true,
34+
"resolveJsonModule": true
35+
},
36+
"include": [
37+
"src",
38+
"test"
39+
]
40+
}

0 commit comments

Comments
 (0)