1
1
import marked from 'https://esm.sh/marked'
2
- import { minify } from 'https://esm.sh/terser'
2
+ import { minify } from 'https://esm.sh/terser@5.3.2 '
3
3
import { safeLoadFront } from 'https://esm.sh/yaml-front-matter'
4
4
import { AlephAPIRequest , AlephAPIResponse } from './api.ts'
5
5
import { EventEmitter } from './events.ts'
@@ -9,7 +9,7 @@ import { Routing } from './router.ts'
9
9
import { colors , ensureDir , path , ServerRequest , Sha1 , walk } from './std.ts'
10
10
import { compile } from './tsc/compile.ts'
11
11
import type { AlephRuntime , APIHandle , Config , RouterURL } from './types.ts'
12
- import util , { existsDirSync , existsFileSync , hashShort , reHashJs , reHttp , reLocaleID , reMDExt , reModuleExt , reStyleModuleExt } from './util.ts'
12
+ import util , { existsDirSync , existsFileSync , hashShort , MB , reHashJs , reHttp , reLocaleID , reMDExt , reModuleExt , reStyleModuleExt } from './util.ts'
13
13
import { cleanCSS , Document , less } from './vendor/mod.ts'
14
14
import { version } from './version.ts'
15
15
@@ -100,6 +100,27 @@ export class Project {
100
100
)
101
101
}
102
102
103
+ isSSRable ( pathname : string ) : boolean {
104
+ const { ssr } = this . config
105
+ if ( util . isPlainObject ( ssr ) ) {
106
+ if ( ssr . include ) {
107
+ for ( let r of ssr . include ) {
108
+ if ( ! r . test ( pathname ) ) {
109
+ return false
110
+ }
111
+ }
112
+ }
113
+ if ( ssr . exclude ) {
114
+ for ( let r of ssr . exclude ) {
115
+ if ( r . test ( pathname ) ) {
116
+ return false
117
+ }
118
+ }
119
+ }
120
+ }
121
+ return true
122
+ }
123
+
103
124
getModule ( id : string ) : Module | null {
104
125
if ( this . #modules. has ( id ) ) {
105
126
return this . #modules. get ( id ) !
@@ -159,14 +180,22 @@ export class Project {
159
180
if ( this . #modules. has ( moduleID ) ) {
160
181
try {
161
182
const { default : handle } = await import ( 'file://' + this . #modules. get ( moduleID ) ! . jsFile )
162
- await handle (
163
- new AlephAPIRequest ( req , url ) ,
164
- new AlephAPIResponse ( req )
165
- )
183
+ if ( util . isFunction ( handle ) ) {
184
+ await handle (
185
+ new AlephAPIRequest ( req , url ) ,
186
+ new AlephAPIResponse ( req )
187
+ )
188
+ } else {
189
+ req . respond ( {
190
+ status : 500 ,
191
+ headers : new Headers ( { 'Content-Type' : 'application/json; charset=utf-8' } ) ,
192
+ body : JSON . stringify ( { error : { status : 404 , message : "handle not found" } } )
193
+ } ) . catch ( err => log . warn ( 'ServerRequest.respond:' , err . message ) )
194
+ }
166
195
} catch ( err ) {
167
196
req . respond ( {
168
197
status : 500 ,
169
- headers : new Headers ( { 'Content-Type' : 'text/plain ; charset=utf-8' } ) ,
198
+ headers : new Headers ( { 'Content-Type' : 'application/json ; charset=utf-8' } ) ,
170
199
body : JSON . stringify ( { error : { status : 500 , message : err . message } } )
171
200
} ) . catch ( err => log . warn ( 'ServerRequest.respond:' , err . message ) )
172
201
log . error ( 'callAPI:' , err )
@@ -183,6 +212,11 @@ export class Project {
183
212
}
184
213
185
214
async getPageHtml ( loc : { pathname : string , search ?: string } ) : Promise < [ number , string ] > {
215
+ if ( ! this . isSSRable ( loc . pathname ) ) {
216
+ const [ url ] = this . #routing. createRouter ( loc )
217
+ return [ url . pagePath === '' ? 404 : 200 , this . getDefaultIndexHtml ( ) ]
218
+ }
219
+
186
220
const { baseUrl } = this . config
187
221
const mainModule = this . #modules. get ( '/main.js' ) !
188
222
const { url, status, head, body } = await this . _renderPage ( loc )
@@ -254,54 +288,103 @@ export class Project {
254
288
await this . ready
255
289
256
290
// lookup output modules
291
+ this . #routing. lookup ( path => path . forEach ( r => lookup ( r . module . id ) ) )
257
292
lookup ( '/main.js' )
293
+ lookup ( '/404.js' )
294
+ lookup ( '/app.js' )
295
+ lookup ( '//deno.land/x/aleph/nomodule.ts' )
296
+ lookup ( '//deno.land/x/aleph/tsc/tslib.js' )
258
297
259
298
// ensure ouput directory ready
260
299
if ( existsDirSync ( outputDir ) ) {
261
300
await Deno . remove ( outputDir , { recursive : true } )
262
301
}
263
- await Promise . all ( [ outputDir , distDir ] . map ( dir => ensureDir ( dir ) ) )
302
+ await ensureDir ( outputDir )
303
+ await ensureDir ( distDir )
304
+
305
+ const { ssr } = this . config
306
+ if ( ssr ) {
307
+ log . info ( colors . bold ( ' Pages (SSG)' ) )
308
+ for ( const pathname of this . #routing. paths ) {
309
+ if ( this . isSSRable ( pathname ) ) {
310
+ const [ _ , html ] = await this . getPageHtml ( { pathname } )
311
+ const htmlFile = path . join ( outputDir , pathname , 'index.html' )
312
+ await writeTextFile ( htmlFile , html )
313
+ log . info ( ' ○' , pathname , colors . dim ( '• ' + util . bytesString ( html . length ) ) )
314
+ }
315
+ }
316
+ const fbHtmlFile = path . join ( outputDir , util . isPlainObject ( ssr ) && ssr . fallback ? ssr . fallback : '404.html' )
317
+ await writeTextFile ( fbHtmlFile , this . getDefaultIndexHtml ( ) )
318
+ } else {
319
+ await writeTextFile ( path . join ( outputDir , 'index.html' ) , this . getDefaultIndexHtml ( ) )
320
+ }
264
321
265
- // copy public files
322
+ // copy public assets
266
323
const publicDir = path . join ( this . appRoot , 'public' )
267
324
if ( existsDirSync ( publicDir ) ) {
268
- for await ( const { path : p } of walk ( publicDir , { includeDirs : false } ) ) {
269
- await Deno . copyFile ( p , path . join ( outputDir , util . trimPrefix ( p , publicDir ) ) )
325
+ log . info ( colors . bold ( ' Public Assets' ) )
326
+ for await ( const { path : p } of walk ( publicDir , { includeDirs : false , skip : [ / \/ \. [ ^ \/ ] + ( $ | \/ ) / ] } ) ) {
327
+ const rp = util . trimPrefix ( p , publicDir )
328
+ const fp = path . join ( outputDir , rp )
329
+ const fi = await Deno . lstat ( p )
330
+ await ensureDir ( path . dirname ( fp ) )
331
+ await Deno . copyFile ( p , fp )
332
+ let sizeColorful = colors . dim
333
+ if ( fi . size > 10 * MB ) {
334
+ sizeColorful = colors . red
335
+ } else if ( fi . size > MB ) {
336
+ sizeColorful = colors . yellow
337
+ }
338
+ log . info ( ' ✹' , rp , colors . dim ( '•' ) , getColorfulBytesString ( fi . size ) )
270
339
}
271
340
}
272
341
342
+ let deps = 0
343
+ let depsBytes = 0
344
+ let modules = 0
345
+ let modulesBytes = 0
346
+ let styles = 0
347
+ let stylesBytes = 0
348
+
273
349
// write modules
274
350
const { sourceMap } = this . config
275
351
await Promise . all ( Array . from ( outputModules ) . map ( ( moduleID ) => {
276
- const { sourceFilePath, isRemote, jsContent, jsSourceMap, hash } = this . #modules. get ( moduleID ) !
352
+ const { sourceFilePath, sourceType , isRemote, jsContent, jsSourceMap, hash } = this . #modules. get ( moduleID ) !
277
353
const saveDir = path . join ( distDir , path . dirname ( sourceFilePath ) )
278
354
const name = path . basename ( sourceFilePath ) . replace ( reModuleExt , '' )
279
355
const jsFile = path . join ( saveDir , name + ( isRemote ? '' : '.' + hash . slice ( 0 , hashShort ) ) ) + '.js'
356
+ if ( isRemote ) {
357
+ deps ++
358
+ depsBytes += jsContent . length
359
+ } else {
360
+ if ( sourceType === 'css' || sourceType === 'less' ) {
361
+ styles ++
362
+ stylesBytes += jsContent . length
363
+ } else {
364
+ modules ++
365
+ modulesBytes += jsContent . length
366
+ }
367
+ }
280
368
return Promise . all ( [
281
369
writeTextFile ( jsFile , jsContent ) ,
282
- sourceMap ? writeTextFile ( jsFile + '.map' , jsSourceMap ) : Promise . resolve ( ) ,
370
+ sourceMap && jsSourceMap ? writeTextFile ( jsFile + '.map' , jsSourceMap ) : Promise . resolve ( ) ,
283
371
] )
284
372
} ) )
285
373
286
374
// write static data
287
375
if ( this . #modules. has ( '/data.js' ) ) {
288
376
const { hash } = this . #modules. get ( '/data.js' ) !
289
- const data = this . getStaticData ( )
290
- await writeTextFile ( path . join ( distDir , `data.${ hash . slice ( 0 , hashShort ) } .js` ) , `export default ${ JSON . stringify ( data ) } ` )
377
+ const data = await this . getStaticData ( )
378
+ const jsContent = `export default ${ JSON . stringify ( data ) } `
379
+ modules ++
380
+ modulesBytes += jsContent . length
381
+ await writeTextFile ( path . join ( distDir , `data.${ hash . slice ( 0 , hashShort ) } .js` ) , jsContent )
291
382
}
292
383
293
- const { ssr } = this . config
294
- if ( ssr ) {
295
- for ( const pathname of this . #routing. paths ) {
296
- const [ _ , html ] = await this . getPageHtml ( { pathname } )
297
- const htmlFile = path . join ( outputDir , pathname , 'index.html' )
298
- await writeTextFile ( htmlFile , html )
299
- }
300
- const fbHtmlFile = path . join ( outputDir , util . isPlainObject ( ssr ) && ssr . fallback ? ssr . fallback : '404.html' )
301
- await writeTextFile ( fbHtmlFile , this . getDefaultIndexHtml ( ) )
302
- } else {
303
- await writeTextFile ( path . join ( outputDir , 'index.html' ) , this . getDefaultIndexHtml ( ) )
304
- }
384
+ log . info ( colors . bold ( ' Modules' ) )
385
+ log . info ( ' ▲' , colors . bold ( deps . toString ( ) ) , 'deps' , colors . dim ( `• ${ util . bytesString ( depsBytes ) } (mini, uncompress)` ) )
386
+ log . info ( ' ▲' , colors . bold ( modules . toString ( ) ) , 'modules' , colors . dim ( `• ${ util . bytesString ( modulesBytes ) } (mini, uncompress)` ) )
387
+ log . info ( ' ▲' , colors . bold ( styles . toString ( ) ) , 'styles' , colors . dim ( `• ${ util . bytesString ( stylesBytes ) } (mini, uncompress)` ) )
305
388
306
389
log . info ( `Done in ${ Math . round ( performance . now ( ) - start ) } ms` )
307
390
}
@@ -378,8 +461,8 @@ export class Project {
378
461
Object . assign ( this . config , { ssr } )
379
462
} else if ( util . isPlainObject ( ssr ) ) {
380
463
const fallback = util . isNEString ( ssr . fallback ) ? util . ensureExt ( ssr . fallback , '.html' ) : '404.html'
381
- const include = util . isArray ( ssr . include ) ? ssr . include : [ ]
382
- const exclude = util . isArray ( ssr . exclude ) ? ssr . exclude : [ ]
464
+ const include = util . isArray ( ssr . include ) ? ssr . include . map ( v => util . isNEString ( v ) ? new RegExp ( v ) : v ) . filter ( v => v instanceof RegExp ) : [ ]
465
+ const exclude = util . isArray ( ssr . exclude ) ? ssr . exclude . map ( v => util . isNEString ( v ) ? new RegExp ( v ) : v ) . filter ( v => v instanceof RegExp ) : [ ]
383
466
Object . assign ( this . config , { ssr : { fallback, include, exclude } } )
384
467
}
385
468
if ( util . isPlainObject ( env ) ) {
@@ -454,20 +537,9 @@ export class Project {
454
537
await this . _createMainModule ( )
455
538
456
539
log . info ( colors . bold ( 'Aleph.js' ) )
457
- log . info ( colors . bold ( ' Pages' ) )
458
- for ( const path of this . #routing. paths ) {
459
- const isIndex = path == '/'
460
- log . info ( ' ○' , path , isIndex ? colors . dim ( '(index)' ) : '' )
461
- }
462
- if ( this . #apiRouting. paths . length > 0 ) {
463
- log . info ( colors . bold ( ' APIs' ) )
464
- }
465
- for ( const path of this . #apiRouting. paths ) {
466
- log . info ( ' λ' , path )
467
- }
468
540
log . info ( colors . bold ( ' Config' ) )
469
541
if ( this . #modules. has ( '/data.js' ) ) {
470
- log . info ( ' ✓' , 'Global Static Data' )
542
+ log . info ( ' ✓' , 'App Static Data' )
471
543
}
472
544
if ( this . #modules. has ( '/app.js' ) ) {
473
545
log . info ( ' ✓' , 'Custom App' )
@@ -476,6 +548,20 @@ export class Project {
476
548
log . info ( ' ✓' , 'Custom 404 Page' )
477
549
}
478
550
551
+ if ( this . isDev ) {
552
+ if ( this . #apiRouting. paths . length > 0 ) {
553
+ log . info ( colors . bold ( ' APIs' ) )
554
+ }
555
+ for ( const path of this . #apiRouting. paths ) {
556
+ log . info ( ' λ' , path )
557
+ }
558
+ log . info ( colors . bold ( ' Pages' ) )
559
+ for ( const path of this . #routing. paths ) {
560
+ const isIndex = path == '/'
561
+ log . info ( ' ○' , path , isIndex ? colors . dim ( '(index)' ) : '' )
562
+ }
563
+ }
564
+
479
565
if ( this . isDev ) {
480
566
this . _watch ( )
481
567
}
@@ -487,11 +573,12 @@ export class Project {
487
573
for await ( const event of w ) {
488
574
for ( const p of event . paths ) {
489
575
const path = '/' + util . trimPrefix ( util . trimPrefix ( p , this . appRoot ) , '/' )
576
+ // handle `api` dir remove directly
490
577
const validated = ( ( ) => {
491
578
if ( ! reModuleExt . test ( path ) && ! reStyleModuleExt . test ( path ) && ! reMDExt . test ( path ) ) {
492
579
return false
493
580
}
494
- // ignore ' .aleph' and output directories
581
+ // ignore ` .aleph` and output directories
495
582
if ( path . startsWith ( '/.aleph/' ) || path . startsWith ( this . config . outputDir ) ) {
496
583
return false
497
584
}
@@ -874,7 +961,7 @@ export class Project {
874
961
`MarkdownPage.meta = ${ JSON . stringify ( props , undefined , this . isDev ? 4 : undefined ) } ;` ,
875
962
this . isDev && `_s(MarkdownPage, "useRef{ref}\\nuseEffect{}");` ,
876
963
this . isDev && `$RefreshReg$(MarkdownPage, "MarkdownPage");` ,
877
- ] . filter ( Boolean ) . map ( l => this . isDev ? String ( l ) . trim ( ) : l ) . join ( this . isDev ? '\n' : '' )
964
+ ] . filter ( Boolean ) . map ( l => ! this . isDev ? String ( l ) . trim ( ) : l ) . join ( this . isDev ? '\n' : '' )
878
965
mod . jsSourceMap = ''
879
966
mod . hash = ( new Sha1 ) . update ( mod . jsContent ) . hex ( )
880
967
} else {
@@ -1125,10 +1212,10 @@ export class Project {
1125
1212
] . flat ( ) )
1126
1213
ret . head = head
1127
1214
ret . body = `<main>${ html } </main>`
1128
- if ( url . pagePath !== '' ) {
1129
- log . debug ( `render page '${ url . pagePath } ' in ${ Math . round ( performance . now ( ) - start ) } ms` )
1130
- } else {
1215
+ if ( url . pagePath === '' ) {
1131
1216
log . warn ( `page '${ url . pathname } ' not found` )
1217
+ } else if ( this . isDev ) {
1218
+ log . debug ( `render page '${ url . pagePath } ' in ${ Math . round ( performance . now ( ) - start ) } ms` )
1132
1219
}
1133
1220
} catch ( err ) {
1134
1221
ret . status = 500
@@ -1226,3 +1313,13 @@ async function writeTextFile(filepath: string, content: string) {
1226
1313
await ensureDir ( dir )
1227
1314
await Deno . writeTextFile ( filepath , content )
1228
1315
}
1316
+
1317
+ function getColorfulBytesString ( bytes : number ) {
1318
+ let cf = colors . dim
1319
+ if ( bytes > 10 * MB ) {
1320
+ cf = colors . red
1321
+ } else if ( bytes > MB ) {
1322
+ cf = colors . yellow
1323
+ }
1324
+ return cf ( util . bytesString ( bytes ) )
1325
+ }
0 commit comments