Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit c25587d

Browse files
authored
Merge pull request #277 from alephjs/use-esbuild-bundler
Use esbuild bundler
2 parents e99b545 + 67ff0c6 commit c25587d

39 files changed

+382
-295
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ ALEPH_DEV=true deno run -A --unstable --import-map=./import_map.json cli.ts star
3131
ALEPH_DEV=true deno run -A --unstable --import-map=./import_map.json cli.ts build ./examples/hello-world -L debug
3232

3333
# run all tests
34-
deno test -A --unstable --location=http://127.0.0.1 --import-map=./import_map.json
34+
deno test -A --unstable --location=http://localhost --import-map=./import_map.json
3535
```
3636

3737
## Project Structure

bundler/mod.ts

Lines changed: 102 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
import { dim } from 'https://deno.land/std@0.92.0/fmt/colors.ts'
2-
import * as path from 'https://deno.land/std@0.92.0/path/mod.ts'
3-
import { ensureDir } from 'https://deno.land/std@0.92.0/fs/ensure_dir.ts'
1+
import { dim } from 'https://deno.land/std@0.93.0/fmt/colors.ts'
2+
import { basename, dirname, join } from 'https://deno.land/std@0.93.0/path/mod.ts'
3+
import { ensureDir, } from 'https://deno.land/std@0.93.0/fs/ensure_dir.ts'
44
import { parseExportNames, transform } from '../compiler/mod.ts'
55
import { trimModuleExt } from '../framework/core/module.ts'
6-
import { ensureTextFile, existsFileSync, lazyRemove } from '../shared/fs.ts'
6+
import { ensureTextFile, existsDirSync, existsFileSync, lazyRemove } from '../shared/fs.ts'
77
import log from '../shared/log.ts'
88
import util from '../shared/util.ts'
99
import { VERSION } from '../version.ts'
1010
import type { Application, Module } from '../server/app.ts'
11-
import {
12-
clearCompilation,
13-
computeHash,
14-
getAlephPkgUri,
15-
} from '../server/helper.ts'
11+
import { cache } from '../server/cache.ts'
12+
import { computeHash, esbuild, stopEsbuild, getAlephPkgUri } from '../server/helper.ts'
1613

17-
export const bundlerRuntimeCode = (`
14+
const hashShort = 8
15+
const reHashJS = new RegExp(`\\.[0-9a-fx]{${hashShort}}\\.js$`, 'i')
16+
17+
export const bundlerRuntimeCode = `
1818
window.__ALEPH = {
1919
basePath: '/',
2020
pack: {},
@@ -47,22 +47,15 @@ export const bundlerRuntimeCode = (`
4747
})
4848
}
4949
}
50-
`).split('\n')
51-
.map(l => l.trim()
52-
.replaceAll(') {', '){')
53-
.replace(/\s*([,:=|+]{1,2})\s+/g, '$1')
54-
)
55-
.join('')
50+
`
5651

5752
/** The bundler class for aleph server. */
5853
export class Bundler {
5954
#app: Application
60-
#compiledModules: Set<string>
6155
#bundledFiles: Map<string, string>
6256

6357
constructor(app: Application) {
6458
this.#app = app
65-
this.#compiledModules = new Set()
6659
this.#bundledFiles = new Map()
6760
}
6861

@@ -83,21 +76,21 @@ export class Bundler {
8376
}
8477
})
8578

86-
await this.createPolyfillBundle()
87-
await this.createBundleChunk(
79+
await this.bundlePolyfillChunck()
80+
await this.bundleChunk(
8881
'deps',
8982
Array.from(remoteEntries),
9083
[]
9184
)
9285
if (sharedEntries.size > 0) {
93-
await this.createBundleChunk(
86+
await this.bundleChunk(
9487
'shared',
9588
Array.from(sharedEntries),
9689
Array.from(remoteEntries)
9790
)
9891
}
9992
for (const url of entries) {
100-
await this.createBundleChunk(
93+
await this.bundleChunk(
10194
trimModuleExt(url),
10295
[url],
10396
[
@@ -106,7 +99,13 @@ export class Bundler {
10699
].flat()
107100
)
108101
}
102+
103+
// create main.js after all chunks are bundled
109104
await this.createMainJS()
105+
106+
// unlike nodejs, Deno doesn't provide the necessary APIs to allow Deno to
107+
// exit while esbuild's internal child process is still running.
108+
stopEsbuild()
110109
}
111110

112111
getBundledFile(name: string): string | null {
@@ -121,16 +120,16 @@ export class Bundler {
121120

122121
private async copyBundleFile(jsFilename: string) {
123122
const { buildDir, outputDir } = this.#app
124-
const bundleFile = path.join(buildDir, jsFilename)
125-
const saveAs = path.join(outputDir, '_aleph', jsFilename)
126-
await ensureDir(path.dirname(saveAs))
123+
const bundleFile = join(buildDir, jsFilename)
124+
const saveAs = join(outputDir, '_aleph', jsFilename)
125+
await ensureDir(dirname(saveAs))
127126
await Deno.copyFile(bundleFile, saveAs)
128127
}
129128

130129
private async compile(mod: Module, external: string[]): Promise<string> {
131130
const bundlingFile = util.trimSuffix(mod.jsFile, '.js') + '.bundling.js'
132131

133-
if (this.#compiledModules.has(mod.url)) {
132+
if (existsFileSync(bundlingFile)) {
134133
return bundlingFile
135134
}
136135

@@ -180,7 +179,6 @@ export class Bundler {
180179
}
181180

182181
await ensureTextFile(bundlingFile, code)
183-
this.#compiledModules.add(mod.url)
184182

185183
return bundlingFile
186184
}
@@ -194,30 +192,30 @@ export class Bundler {
194192
}, {} as Record<string, string>)
195193
const mainJS = `__ALEPH.bundledFiles=${JSON.stringify(bundledFiles)};` + this.#app.getMainJS(true)
196194
const hash = computeHash(mainJS)
197-
const bundleFilename = `main.bundle.${hash.slice(0, 8)}.js`
198-
const bundleFile = path.join(this.#app.buildDir, bundleFilename)
199-
await Deno.writeTextFile(bundleFile, mainJS)
195+
const bundleFilename = `main.bundle.${hash.slice(0, hashShort)}.js`
196+
const bundleFilePath = join(this.#app.buildDir, bundleFilename)
197+
await Deno.writeTextFile(bundleFilePath, mainJS)
200198
this.#bundledFiles.set('main', bundleFilename)
201199
log.info(` {} main.js ${dim('• ' + util.formatBytes(mainJS.length))}`)
202200
}
203201

204202
/** create polyfill bundle. */
205-
private async createPolyfillBundle() {
203+
private async bundlePolyfillChunck() {
206204
const alephPkgUri = getAlephPkgUri()
207205
const { buildTarget } = this.#app.config
208206
const hash = computeHash(buildTarget + Deno.version.deno + VERSION)
209-
const bundleFilename = `polyfill.bundle.${hash.slice(0, 8)}.js`
210-
const bundleFile = path.join(this.#app.buildDir, bundleFilename)
211-
if (!existsFileSync(bundleFile)) {
207+
const bundleFilename = `polyfill.bundle.${hash.slice(0, hashShort)}.js`
208+
const bundleFilePath = join(this.#app.buildDir, bundleFilename)
209+
if (!existsFileSync(bundleFilePath)) {
212210
const rawPolyfillFile = `${alephPkgUri}/bundler/polyfills/${buildTarget}/mod.ts`
213-
await this._bundle(rawPolyfillFile, bundleFile)
211+
await this.build(rawPolyfillFile, bundleFilePath)
214212
}
215213
this.#bundledFiles.set('polyfill', bundleFilename)
216-
log.info(` {} polyfill.js (${buildTarget.toUpperCase()}) ${dim('• ' + util.formatBytes(Deno.statSync(bundleFile).size))}`)
214+
log.info(` {} polyfill.js (${buildTarget.toUpperCase()}) ${dim('• ' + util.formatBytes(Deno.statSync(bundleFilePath).size))}`)
217215
}
218216

219217
/** create bundle chunk. */
220-
private async createBundleChunk(name: string, entry: string[], external: string[]) {
218+
private async bundleChunk(name: string, entry: string[], external: string[]) {
221219
const entryCode = (await Promise.all(entry.map(async (url, i) => {
222220
let mod = this.#app.getModule(url)
223221
if (mod && mod.jsFile !== '') {
@@ -237,79 +235,87 @@ export class Bundler {
237235
return []
238236
}))).flat().join('\n')
239237
const hash = computeHash(entryCode + VERSION + Deno.version.deno)
240-
const bundleFilename = `${name}.bundle.${hash.slice(0, 8)}.js`
241-
const bundleEntryFile = path.join(this.#app.buildDir, `${name}.bundle.entry.js`)
242-
const bundleFile = path.join(this.#app.buildDir, bundleFilename)
243-
if (!existsFileSync(bundleFile)) {
238+
const bundleFilename = `${name}.bundle.${hash.slice(0, hashShort)}.js`
239+
const bundleEntryFile = join(this.#app.buildDir, `${name}.bundle.entry.js`)
240+
const bundleFilePath = join(this.#app.buildDir, bundleFilename)
241+
if (!existsFileSync(bundleFilePath)) {
244242
await Deno.writeTextFile(bundleEntryFile, entryCode)
245-
await this._bundle(bundleEntryFile, bundleFile)
243+
await this.build(bundleEntryFile, bundleFilePath)
246244
lazyRemove(bundleEntryFile)
247245
}
248246
this.#bundledFiles.set(name, bundleFilename)
249-
log.info(` {} ${name}.js ${dim('• ' + util.formatBytes(Deno.statSync(bundleFile).size))}`)
247+
log.info(` {} ${name}.js ${dim('• ' + util.formatBytes(Deno.statSync(bundleFilePath).size))}`)
250248
}
251249

252250
/** run deno bundle and compress the output using terser. */
253-
private async _bundle(bundleEntryFile: string, bundleFile: string) {
254-
// todo: use Deno.emit()
255-
const p = Deno.run({
256-
cmd: [Deno.execPath(), 'bundle', '--no-check', bundleEntryFile, bundleFile],
257-
stdout: 'null',
258-
stderr: 'piped'
259-
})
260-
const data = await p.stderrOutput()
261-
p.close()
262-
if (!existsFileSync(bundleFile)) {
263-
const msg = (new TextDecoder).decode(data).replaceAll('file://', '').replaceAll(this.#app.buildDir, '/_aleph')
264-
await Deno.stderr.write((new TextEncoder).encode(msg))
265-
Deno.exit(1)
266-
}
251+
private async build(entryFile: string, bundleFile: string) {
252+
const { buildTarget, browserslist } = this.#app.config
267253

268-
// transpile bundle code to `buildTarget`
269-
const { buildTarget } = this.#app.config
270-
271-
let { code } = await transform(
272-
'/bundle.js',
273-
await Deno.readTextFile(bundleFile),
274-
{
275-
transpileOnly: true,
276-
swcOptions: {
277-
target: buildTarget
254+
await clearBuildCache(bundleFile)
255+
await esbuild({
256+
entryPoints: [entryFile],
257+
outfile: bundleFile,
258+
platform: 'browser',
259+
target: [String(buildTarget)].concat(browserslist.map(({ name, version }) => {
260+
return `${name.toLowerCase()}${version}`
261+
})),
262+
bundle: true,
263+
minify: true,
264+
treeShaking: true,
265+
sourcemap: false,
266+
plugins: [{
267+
name: 'http-loader',
268+
setup(build) {
269+
build.onResolve({ filter: /.*/ }, args => {
270+
if (util.isLikelyHttpURL(args.path)) {
271+
return {
272+
path: args.path,
273+
namespace: 'http-module',
274+
}
275+
}
276+
if (args.namespace === 'http-module') {
277+
return {
278+
path: (new URL(args.path, args.importer)).toString(),
279+
namespace: 'http-module',
280+
}
281+
}
282+
const [path] = util.splitBy(util.trimPrefix(args.path, 'file://'), '#')
283+
if (path.startsWith('.')) {
284+
return { path: join(args.resolveDir, path) }
285+
}
286+
return { path }
287+
})
288+
build.onLoad({ filter: /.*/, namespace: 'http-module' }, async args => {
289+
const { content } = await cache(args.path)
290+
return { contents: content }
291+
})
278292
}
279-
}
280-
)
281-
282-
// IIFEify
283-
code = `(() => { ${code} })()`
284-
285-
// minify code
286-
// todo: use swc minify instead (https://github.com/swc-project/swc/pull/1302)
287-
const mini = await minify(code, parseInt(util.trimPrefix(buildTarget, 'es')))
288-
if (mini !== undefined) {
289-
code = mini
290-
}
291-
292-
await clearCompilation(bundleFile)
293-
await Deno.writeTextFile(bundleFile, code)
293+
}],
294+
})
294295
}
295296
}
296297

297-
interface Minify {
298-
(code: string, options: any): Promise<{ code: string }>
298+
export function simpleJSMinify(code: string) {
299+
return code.split('\n').map(l => l.trim()
300+
.replace(/\s*([,:=|+]{1,2})\s+/g, '$1')
301+
.replaceAll(') {', '){')
302+
).join('')
299303
}
300304

301-
let terser: Minify | null = null
305+
async function clearBuildCache(filename: string) {
306+
const dir = dirname(filename)
307+
const hashname = basename(filename)
308+
if (!reHashJS.test(hashname) || !existsDirSync(dir)) {
309+
return
310+
}
302311

303-
async function minify(code: string, ecma: number = 2015) {
304-
if (terser === null) {
305-
const { minify } = await import('https://esm.sh/[email protected]?no-check')
306-
terser = minify as Minify
312+
const jsName = hashname.split('.').slice(0, -2).join('.') + '.js'
313+
for await (const entry of Deno.readDir(dir)) {
314+
if (entry.isFile && reHashJS.test(entry.name)) {
315+
const _jsName = entry.name.split('.').slice(0, -2).join('.') + '.js'
316+
if (_jsName === jsName && hashname !== entry.name) {
317+
await Deno.remove(join(dir, entry.name))
318+
}
319+
}
307320
}
308-
const ret = await terser(code, {
309-
compress: true,
310-
mangle: true,
311-
ecma,
312-
sourceMap: false
313-
})
314-
return ret.code
315321
}

bundler/polyfills/es2015/fetch.ts

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

bundler/polyfills/es2015/mod.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
import './fetch.ts'
1+
import nameShim from 'https://esm.sh/function.prototype.name/shim'
2+
3+
import '../es2016/mod.ts'
4+
5+
nameShim()

bundler/polyfills/es2016/mod.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Array#includes is stage 4, in ES7/ES2016
2+
import includesShim from 'https://esm.sh/array-includes/shim'
3+
4+
import '../es2017/mod.ts'
5+
6+
includesShim()
7+
8+

bundler/polyfills/es2017/mod.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Object.values/Object.entries are stage 4, in ES2017
2+
import valuesShim from 'https://esm.sh/object.values/shim'
3+
import entriesShim from 'https://esm.sh/object.entries/shim'
4+
5+
// String#padStart/String#padEnd are stage 4, in ES2017
6+
import padstartShim from 'https://esm.sh/string.prototype.padstart/shim'
7+
import padendShim from 'https://esm.sh/string.prototype.padend/shim'
8+
9+
// Object.getOwnPropertyDescriptors is stage 4, in ES2017
10+
import getownpropertydescriptorsShim from 'https://esm.sh/object.getownpropertydescriptors/shim'
11+
12+
import '../es2018/mod.ts'
13+
14+
valuesShim()
15+
entriesShim()
16+
padstartShim()
17+
padendShim()
18+
getownpropertydescriptorsShim()

bundler/polyfills/es2018/mod.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import finallyShim from 'https://esm.sh/promise.prototype.finally/shim'
2+
import '../es2019/mod.ts'
3+
4+
if (typeof Promise === 'function') {
5+
finallyShim()
6+
}

bundler/polyfills/es2019/mod.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import flatShim from 'https://esm.sh/array.prototype.flat/shim'
2+
import flatmapShim from 'https://esm.sh/array.prototype.flatmap/shim'
3+
import descriptionShim from 'https://esm.sh/symbol.prototype.description/shim'
4+
import fromentriesShim from 'https://esm.sh/object.fromentries/shim'
5+
import '../es2020/mod.ts'
6+
7+
flatShim()
8+
flatmapShim()
9+
descriptionShim()
10+
fromentriesShim()

bundler/polyfills/es2020/mod.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import matchallShim from 'https://esm.sh/string.prototype.matchall/shim'
2+
import globalthisShim from 'https://esm.sh/globalthis/shim'
3+
import allsettledShim from 'https://esm.sh/promise.allsettled/shim'
4+
5+
matchallShim()
6+
globalthisShim()
7+
allsettledShim()

cli.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { resolve } from 'https://deno.land/std@0.92.0/path/mod.ts'
2-
import { parse } from 'https://deno.land/std@0.92.0/flags/mod.ts'
1+
import { resolve } from 'https://deno.land/std@0.93.0/path/mod.ts'
2+
import { parse } from 'https://deno.land/std@0.93.0/flags/mod.ts'
33
import { existsDirSync } from './shared/fs.ts'
44
import log, { LevelNames } from './shared/log.ts'
55
import util from './shared/util.ts'

0 commit comments

Comments
 (0)