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

Commit 685d837

Browse files
committed
Use esbuild bundler
1 parent 1d58b17 commit 685d837

File tree

9 files changed

+174
-169
lines changed

9 files changed

+174
-169
lines changed

bundler/mod.ts

Lines changed: 53 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import { dim } from 'https://deno.land/[email protected]/fmt/colors.ts'
2-
import * as path from 'https://deno.land/[email protected]/path/mod.ts'
2+
import { dirname, join } from 'https://deno.land/[email protected]/path/mod.ts'
33
import { ensureDir } from 'https://deno.land/[email protected]/fs/ensure_dir.ts'
4+
// @deno-types="https://deno.land/x/[email protected]/mod.d.ts"
5+
import { build } from 'https://deno.land/x/[email protected]/mod.js'
46
import { parseExportNames, transform } from '../compiler/mod.ts'
57
import { trimModuleExt } from '../framework/core/module.ts'
68
import { ensureTextFile, existsFileSync, lazyRemove } from '../shared/fs.ts'
79
import log from '../shared/log.ts'
810
import util from '../shared/util.ts'
911
import { VERSION } from '../version.ts'
1012
import type { Application, Module } from '../server/app.ts'
11-
import {
12-
clearCompilation,
13-
computeHash,
14-
getAlephPkgUri,
15-
} from '../server/helper.ts'
13+
import { computeHash, getAlephPkgUri } from '../server/helper.ts'
14+
import { cache } from '../server/cache.ts'
1615

1716
export const bundlerRuntimeCode = (`
1817
window.__ALEPH = {
@@ -121,9 +120,9 @@ 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

@@ -195,7 +194,7 @@ export class Bundler {
195194
const mainJS = `__ALEPH.bundledFiles=${JSON.stringify(bundledFiles)};` + this.#app.getMainJS(true)
196195
const hash = computeHash(mainJS)
197196
const bundleFilename = `main.bundle.${hash.slice(0, 8)}.js`
198-
const bundleFile = path.join(this.#app.buildDir, bundleFilename)
197+
const bundleFile = join(this.#app.buildDir, bundleFilename)
199198
await Deno.writeTextFile(bundleFile, mainJS)
200199
this.#bundledFiles.set('main', bundleFilename)
201200
log.info(` {} main.js ${dim('• ' + util.formatBytes(mainJS.length))}`)
@@ -207,7 +206,7 @@ export class Bundler {
207206
const { buildTarget } = this.#app.config
208207
const hash = computeHash(buildTarget + Deno.version.deno + VERSION)
209208
const bundleFilename = `polyfill.bundle.${hash.slice(0, 8)}.js`
210-
const bundleFile = path.join(this.#app.buildDir, bundleFilename)
209+
const bundleFile = join(this.#app.buildDir, bundleFilename)
211210
if (!existsFileSync(bundleFile)) {
212211
const rawPolyfillFile = `${alephPkgUri}/bundler/polyfills/${buildTarget}/mod.ts`
213212
await this._bundle(rawPolyfillFile, bundleFile)
@@ -238,8 +237,8 @@ export class Bundler {
238237
}))).flat().join('\n')
239238
const hash = computeHash(entryCode + VERSION + Deno.version.deno)
240239
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)
240+
const bundleEntryFile = join(this.#app.buildDir, `${name}.bundle.entry.js`)
241+
const bundleFile = join(this.#app.buildDir, bundleFilename)
243242
if (!existsFileSync(bundleFile)) {
244243
await Deno.writeTextFile(bundleEntryFile, entryCode)
245244
await this._bundle(bundleEntryFile, bundleFile)
@@ -250,66 +249,51 @@ export class Bundler {
250249
}
251250

252251
/** 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-
}
267-
268-
// transpile bundle code to `buildTarget`
269-
const { buildTarget } = this.#app.config
252+
private async _bundle(entryFile: string, bundleFile: string) {
253+
const { buildTarget, browserslist } = this.#app.config
270254

271-
let { code } = await transform(
272-
'/bundle.js',
273-
await Deno.readTextFile(bundleFile),
274-
{
275-
transpileOnly: true,
276-
swcOptions: {
277-
target: buildTarget
255+
await clearBundle(bundleFile)
256+
await build({
257+
entryPoints: [entryFile],
258+
outfile: bundleFile,
259+
platform: 'browser',
260+
target: [String(buildTarget)].concat(browserslist.map(({ name, version }) => {
261+
return `${name.toLowerCase()}${version}`
262+
})),
263+
bundle: true,
264+
minify: true,
265+
plugins: [{
266+
name: 'http-loader',
267+
setup(build) {
268+
build.onResolve({ filter: /.*/ }, async (args) => {
269+
if (util.isLikelyHttpURL(args.path)) {
270+
return {
271+
path: args.path,
272+
namespace: 'http-module',
273+
}
274+
}
275+
if (args.namespace === 'http-module') {
276+
return {
277+
path: (new URL(args.path, args.importer)).toString(),
278+
namespace: 'http-module',
279+
}
280+
}
281+
const [path] = util.splitBy(util.trimPrefix(args.path, 'file://'), '#')
282+
if (path.startsWith('.')) {
283+
return { path: join(args.resolveDir, path) }
284+
}
285+
return { path }
286+
})
287+
build.onLoad({ filter: /.*/, namespace: 'http-module' }, async (args) => {
288+
const { content } = await cache(args.path)
289+
return { contents: content }
290+
})
278291
}
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)
292+
}],
293+
})
294294
}
295295
}
296296

297-
interface Minify {
298-
(code: string, options: any): Promise<{ code: string }>
299-
}
297+
async function clearBundle(filename: string) {
300298

301-
let terser: Minify | null = null
302-
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
307-
}
308-
const ret = await terser(code, {
309-
compress: true,
310-
mangle: true,
311-
ecma,
312-
sourceMap: false
313-
})
314-
return ret.code
315299
}

cli/dev.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { getFlag, parsePortNumber } from '../server/helper.ts'
21
import { Application } from '../server/app.ts'
32
import { serve } from '../server/stdserver.ts'
3+
import { getFlag, parsePortNumber } from '../shared/flags.ts'
44

55
export const helpMessage = `
66
Usage:

cli/start.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { getFlag, parsePortNumber } from '../server/helper.ts'
21
import { Application } from '../server/app.ts'
32
import { serve } from '../server/stdserver.ts'
3+
import { getFlag, parsePortNumber } from '../shared/flags.ts'
44
import log from '../shared/log.ts'
55

66
export const helpMessage = `

server/app.ts

Lines changed: 4 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { bold, dim } from 'https://deno.land/[email protected]/fmt/colors.ts'
22
import { ensureDir } from 'https://deno.land/[email protected]/fs/ensure_dir.ts'
33
import { walk } from 'https://deno.land/[email protected]/fs/walk.ts'
4-
import { createHash } from 'https://deno.land/[email protected]/hash/mod.ts'
54
import {
65
basename,
76
dirname,
@@ -40,13 +39,13 @@ import {
4039
loadAndUpgradeImportMap,
4140
RequiredConfig
4241
} from './config.ts'
42+
import { cache } from './cache.ts'
4343
import { CSSProcessor } from './css.ts'
4444
import {
4545
checkAlephDev,
4646
computeHash,
4747
formatBytesWithColor,
4848
getAlephPkgUri,
49-
getDenoDir,
5049
getRelativePath,
5150
isLoaderPlugin,
5251
reFullVersion,
@@ -880,79 +879,16 @@ export class Application implements ServerApplication {
880879
return { content: new Uint8Array(), contentType: 'text/css' }
881880
}
882881

883-
const u = new URL(url)
884882
if (url.startsWith('https://esm.sh/')) {
883+
const u = new URL(url)
885884
if (this.isDev && !u.searchParams.has('dev')) {
886885
u.searchParams.set('dev', '')
887886
u.search = u.search.replace('dev=', 'dev')
887+
url = u.toString()
888888
}
889889
}
890890

891-
const { protocol, hostname, port, pathname, search } = u
892-
const isLocalhost = hostname === 'localhost' || hostname === '0.0.0.0' || hostname === '172.0.0.1'
893-
const versioned = reFullVersion.test(pathname)
894-
const reload = this.#reloading || !versioned
895-
const cacheDir = join(
896-
await getDenoDir(),
897-
'deps',
898-
util.trimSuffix(protocol, ':'),
899-
hostname + (port ? '_PORT' + port : '')
900-
)
901-
const hash = createHash('sha256').update(pathname + search).toString()
902-
const contentFile = join(cacheDir, hash)
903-
const metaFile = join(cacheDir, hash + '.metadata.json')
904-
905-
if (!reload && !isLocalhost && existsFileSync(contentFile) && existsFileSync(metaFile)) {
906-
const [content, meta] = await Promise.all([
907-
Deno.readFile(contentFile),
908-
Deno.readTextFile(metaFile),
909-
])
910-
try {
911-
const { headers } = JSON.parse(meta)
912-
return {
913-
content,
914-
contentType: headers['content-type'] || null
915-
}
916-
} catch (e) { }
917-
}
918-
919-
// download dep when deno cache failed
920-
let err = new Error('Unknown')
921-
for (let i = 0; i < 10; i++) {
922-
if (i === 0) {
923-
if (!isLocalhost) {
924-
log.info('Download', url)
925-
}
926-
} else {
927-
log.debug('Download error:', err)
928-
log.warn(`Download ${url} failed, retrying...`)
929-
}
930-
try {
931-
const resp = await fetch(u.toString())
932-
if (resp.status >= 400) {
933-
return Promise.reject(new Error(resp.statusText))
934-
}
935-
const buffer = await resp.arrayBuffer()
936-
const content = await Deno.readAll(new Deno.Buffer(buffer))
937-
if (!isLocalhost) {
938-
await ensureDir(cacheDir)
939-
Deno.writeFile(contentFile, content)
940-
const headers: Record<string, string> = {}
941-
resp.headers.forEach((val, key) => {
942-
headers[key] = val
943-
})
944-
Deno.writeTextFile(metaFile, JSON.stringify({ headers, url }, undefined, 2))
945-
}
946-
return {
947-
content,
948-
contentType: resp.headers.get('content-type')
949-
}
950-
} catch (e) {
951-
err = e
952-
}
953-
}
954-
955-
return Promise.reject(err)
891+
return await cache(url, { forceRefresh: this.#reloading, retryTimes: 10 })
956892
}
957893

958894
private async precompile(

server/cache.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { ensureDir } from 'https://deno.land/[email protected]/fs/ensure_dir.ts'
2+
import { createHash } from 'https://deno.land/[email protected]/hash/mod.ts'
3+
import { join } from 'https://deno.land/[email protected]/path/mod.ts'
4+
import { existsFileSync } from '../shared/fs.ts'
5+
import util from '../shared/util.ts'
6+
import log from '../shared/log.ts'
7+
import { getDenoDir, reFullVersion } from './helper.ts'
8+
9+
/** download and cache remote content */
10+
export async function cache(url: string, options?: { forceRefresh?: boolean, retryTimes?: number }) {
11+
const u = new URL(url)
12+
const { protocol, hostname, port, pathname, search } = u
13+
const isLocalhost = hostname === 'localhost' || hostname === '0.0.0.0' || hostname === '172.0.0.1'
14+
const versioned = reFullVersion.test(pathname)
15+
const reload = !!options?.forceRefresh || !versioned
16+
const cacheDir = join(
17+
await getDenoDir(),
18+
'deps',
19+
util.trimSuffix(protocol, ':'),
20+
hostname + (port ? '_PORT' + port : '')
21+
)
22+
const hash = createHash('sha256').update(pathname + search).toString()
23+
const contentFile = join(cacheDir, hash)
24+
const metaFile = join(cacheDir, hash + '.metadata.json')
25+
26+
if (!reload && !isLocalhost && existsFileSync(contentFile) && existsFileSync(metaFile)) {
27+
const [content, meta] = await Promise.all([
28+
Deno.readFile(contentFile),
29+
Deno.readTextFile(metaFile),
30+
])
31+
try {
32+
const { headers = {} } = JSON.parse(meta)
33+
return {
34+
content,
35+
contentType: headers['Content-Type'] || null
36+
}
37+
} catch (e) { }
38+
}
39+
40+
const retryTimes = options?.retryTimes || 3
41+
let err = new Error('Unknown')
42+
for (let i = 0; i < retryTimes; i++) {
43+
if (i === 0) {
44+
if (!isLocalhost) {
45+
log.info('Download', url)
46+
}
47+
} else {
48+
log.debug('Download error:', err)
49+
log.warn(`Download ${url} failed, retrying...`)
50+
}
51+
try {
52+
const resp = await fetch(u.toString())
53+
if (resp.status >= 400) {
54+
return Promise.reject(new Error(resp.statusText))
55+
}
56+
const buffer = await resp.arrayBuffer()
57+
const content = new Uint8Array(buffer)
58+
if (!isLocalhost) {
59+
const headers: Record<string, string> = {}
60+
resp.headers.forEach((val, key) => {
61+
headers[key] = val
62+
})
63+
await ensureDir(cacheDir)
64+
Deno.writeFile(contentFile, content)
65+
Deno.writeTextFile(metaFile, JSON.stringify({ headers, url }, undefined, 2))
66+
}
67+
return {
68+
content,
69+
contentType: resp.headers.get('Content-Type')
70+
}
71+
} catch (e) {
72+
err = e
73+
}
74+
}
75+
76+
return Promise.reject(err)
77+
}

server/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface RequiredConfig extends Required<Omit<Config, 'css'>> {
1717
export const defaultConfig: Readonly<RequiredConfig> = {
1818
framework: 'react',
1919
buildTarget: 'es2015',
20+
browserslist: [],
2021
basePath: '/',
2122
srcDir: '/',
2223
outputDir: '/dist',
@@ -70,6 +71,7 @@ export async function loadConfig(workingDir: string): Promise<Config> {
7071
outputDir,
7172
basePath,
7273
buildTarget,
74+
browserslist,
7375
defaultLocale,
7476
locales,
7577
ssr,
@@ -100,6 +102,9 @@ export async function loadConfig(workingDir: string): Promise<Config> {
100102
if (isBuildTarget(buildTarget)) {
101103
config.buildTarget = buildTarget
102104
}
105+
if (util.isNEArray(browserslist)) {
106+
config.browserslist = browserslist
107+
}
103108
if (isLocaleID(defaultLocale)) {
104109
config.defaultLocale = defaultLocale
105110
}

0 commit comments

Comments
 (0)