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'
4
4
import { parseExportNames , transform } from '../compiler/mod.ts'
5
5
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'
7
7
import log from '../shared/log.ts'
8
8
import util from '../shared/util.ts'
9
9
import { VERSION } from '../version.ts'
10
10
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'
16
13
17
- export const bundlerRuntimeCode = ( `
14
+ const hashShort = 8
15
+ const reHashJS = new RegExp ( `\\.[0-9a-fx]{${ hashShort } }\\.js$` , 'i' )
16
+
17
+ export const bundlerRuntimeCode = `
18
18
window.__ALEPH = {
19
19
basePath: '/',
20
20
pack: {},
@@ -47,22 +47,15 @@ export const bundlerRuntimeCode = (`
47
47
})
48
48
}
49
49
}
50
- ` ) . split ( '\n' )
51
- . map ( l => l . trim ( )
52
- . replaceAll ( ') {' , '){' )
53
- . replace ( / \s * ( [ , : = | + ] { 1 , 2 } ) \s + / g, '$1' )
54
- )
55
- . join ( '' )
50
+ `
56
51
57
52
/** The bundler class for aleph server. */
58
53
export class Bundler {
59
54
#app: Application
60
- #compiledModules: Set < string >
61
55
#bundledFiles: Map < string , string >
62
56
63
57
constructor ( app : Application ) {
64
58
this . #app = app
65
- this . #compiledModules = new Set ( )
66
59
this . #bundledFiles = new Map ( )
67
60
}
68
61
@@ -83,21 +76,21 @@ export class Bundler {
83
76
}
84
77
} )
85
78
86
- await this . createPolyfillBundle ( )
87
- await this . createBundleChunk (
79
+ await this . bundlePolyfillChunck ( )
80
+ await this . bundleChunk (
88
81
'deps' ,
89
82
Array . from ( remoteEntries ) ,
90
83
[ ]
91
84
)
92
85
if ( sharedEntries . size > 0 ) {
93
- await this . createBundleChunk (
86
+ await this . bundleChunk (
94
87
'shared' ,
95
88
Array . from ( sharedEntries ) ,
96
89
Array . from ( remoteEntries )
97
90
)
98
91
}
99
92
for ( const url of entries ) {
100
- await this . createBundleChunk (
93
+ await this . bundleChunk (
101
94
trimModuleExt ( url ) ,
102
95
[ url ] ,
103
96
[
@@ -106,7 +99,13 @@ export class Bundler {
106
99
] . flat ( )
107
100
)
108
101
}
102
+
103
+ // create main.js after all chunks are bundled
109
104
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 ( )
110
109
}
111
110
112
111
getBundledFile ( name : string ) : string | null {
@@ -121,16 +120,16 @@ export class Bundler {
121
120
122
121
private async copyBundleFile ( jsFilename : string ) {
123
122
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 ) )
127
126
await Deno . copyFile ( bundleFile , saveAs )
128
127
}
129
128
130
129
private async compile ( mod : Module , external : string [ ] ) : Promise < string > {
131
130
const bundlingFile = util . trimSuffix ( mod . jsFile , '.js' ) + '.bundling.js'
132
131
133
- if ( this . #compiledModules . has ( mod . url ) ) {
132
+ if ( existsFileSync ( bundlingFile ) ) {
134
133
return bundlingFile
135
134
}
136
135
@@ -180,7 +179,6 @@ export class Bundler {
180
179
}
181
180
182
181
await ensureTextFile ( bundlingFile , code )
183
- this . #compiledModules. add ( mod . url )
184
182
185
183
return bundlingFile
186
184
}
@@ -194,30 +192,30 @@ export class Bundler {
194
192
} , { } as Record < string , string > )
195
193
const mainJS = `__ALEPH.bundledFiles=${ JSON . stringify ( bundledFiles ) } ;` + this . #app. getMainJS ( true )
196
194
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 )
200
198
this . #bundledFiles. set ( 'main' , bundleFilename )
201
199
log . info ( ` {} main.js ${ dim ( '• ' + util . formatBytes ( mainJS . length ) ) } ` )
202
200
}
203
201
204
202
/** create polyfill bundle. */
205
- private async createPolyfillBundle ( ) {
203
+ private async bundlePolyfillChunck ( ) {
206
204
const alephPkgUri = getAlephPkgUri ( )
207
205
const { buildTarget } = this . #app. config
208
206
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 ) ) {
212
210
const rawPolyfillFile = `${ alephPkgUri } /bundler/polyfills/${ buildTarget } /mod.ts`
213
- await this . _bundle ( rawPolyfillFile , bundleFile )
211
+ await this . build ( rawPolyfillFile , bundleFilePath )
214
212
}
215
213
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 ) ) } ` )
217
215
}
218
216
219
217
/** create bundle chunk. */
220
- private async createBundleChunk ( name : string , entry : string [ ] , external : string [ ] ) {
218
+ private async bundleChunk ( name : string , entry : string [ ] , external : string [ ] ) {
221
219
const entryCode = ( await Promise . all ( entry . map ( async ( url , i ) => {
222
220
let mod = this . #app. getModule ( url )
223
221
if ( mod && mod . jsFile !== '' ) {
@@ -237,79 +235,87 @@ export class Bundler {
237
235
return [ ]
238
236
} ) ) ) . flat ( ) . join ( '\n' )
239
237
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 ) ) {
244
242
await Deno . writeTextFile ( bundleEntryFile , entryCode )
245
- await this . _bundle ( bundleEntryFile , bundleFile )
243
+ await this . build ( bundleEntryFile , bundleFilePath )
246
244
lazyRemove ( bundleEntryFile )
247
245
}
248
246
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 ) ) } ` )
250
248
}
251
249
252
250
/** 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
267
253
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
+ } )
278
292
}
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
+ } )
294
295
}
295
296
}
296
297
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 ( '' )
299
303
}
300
304
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
+ }
302
311
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
+ }
307
320
}
308
- const ret = await terser ( code , {
309
- compress : true ,
310
- mangle : true ,
311
- ecma,
312
- sourceMap : false
313
- } )
314
- return ret . code
315
321
}
0 commit comments