11import 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 '
33import { safeLoadFront } from 'https://esm.sh/yaml-front-matter'
44import { AlephAPIRequest , AlephAPIResponse } from './api.ts'
55import { EventEmitter } from './events.ts'
@@ -9,7 +9,7 @@ import { Routing } from './router.ts'
99import { colors , ensureDir , path , ServerRequest , Sha1 , walk } from './std.ts'
1010import { compile } from './tsc/compile.ts'
1111import 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'
1313import { cleanCSS , Document , less } from './vendor/mod.ts'
1414import { version } from './version.ts'
1515
@@ -100,6 +100,27 @@ export class Project {
100100 )
101101 }
102102
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+
103124 getModule ( id : string ) : Module | null {
104125 if ( this . #modules. has ( id ) ) {
105126 return this . #modules. get ( id ) !
@@ -159,14 +180,22 @@ export class Project {
159180 if ( this . #modules. has ( moduleID ) ) {
160181 try {
161182 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+ }
166195 } catch ( err ) {
167196 req . respond ( {
168197 status : 500 ,
169- headers : new Headers ( { 'Content-Type' : 'text/plain ; charset=utf-8' } ) ,
198+ headers : new Headers ( { 'Content-Type' : 'application/json ; charset=utf-8' } ) ,
170199 body : JSON . stringify ( { error : { status : 500 , message : err . message } } )
171200 } ) . catch ( err => log . warn ( 'ServerRequest.respond:' , err . message ) )
172201 log . error ( 'callAPI:' , err )
@@ -183,6 +212,11 @@ export class Project {
183212 }
184213
185214 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+
186220 const { baseUrl } = this . config
187221 const mainModule = this . #modules. get ( '/main.js' ) !
188222 const { url, status, head, body } = await this . _renderPage ( loc )
@@ -254,54 +288,103 @@ export class Project {
254288 await this . ready
255289
256290 // lookup output modules
291+ this . #routing. lookup ( path => path . forEach ( r => lookup ( r . module . id ) ) )
257292 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' )
258297
259298 // ensure ouput directory ready
260299 if ( existsDirSync ( outputDir ) ) {
261300 await Deno . remove ( outputDir , { recursive : true } )
262301 }
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+ }
264321
265- // copy public files
322+ // copy public assets
266323 const publicDir = path . join ( this . appRoot , 'public' )
267324 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 ) )
270339 }
271340 }
272341
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+
273349 // write modules
274350 const { sourceMap } = this . config
275351 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 ) !
277353 const saveDir = path . join ( distDir , path . dirname ( sourceFilePath ) )
278354 const name = path . basename ( sourceFilePath ) . replace ( reModuleExt , '' )
279355 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+ }
280368 return Promise . all ( [
281369 writeTextFile ( jsFile , jsContent ) ,
282- sourceMap ? writeTextFile ( jsFile + '.map' , jsSourceMap ) : Promise . resolve ( ) ,
370+ sourceMap && jsSourceMap ? writeTextFile ( jsFile + '.map' , jsSourceMap ) : Promise . resolve ( ) ,
283371 ] )
284372 } ) )
285373
286374 // write static data
287375 if ( this . #modules. has ( '/data.js' ) ) {
288376 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 )
291382 }
292383
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)` ) )
305388
306389 log . info ( `Done in ${ Math . round ( performance . now ( ) - start ) } ms` )
307390 }
@@ -378,8 +461,8 @@ export class Project {
378461 Object . assign ( this . config , { ssr } )
379462 } else if ( util . isPlainObject ( ssr ) ) {
380463 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 ) : [ ]
383466 Object . assign ( this . config , { ssr : { fallback, include, exclude } } )
384467 }
385468 if ( util . isPlainObject ( env ) ) {
@@ -454,20 +537,9 @@ export class Project {
454537 await this . _createMainModule ( )
455538
456539 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- }
468540 log . info ( colors . bold ( ' Config' ) )
469541 if ( this . #modules. has ( '/data.js' ) ) {
470- log . info ( ' ✓' , 'Global Static Data' )
542+ log . info ( ' ✓' , 'App Static Data' )
471543 }
472544 if ( this . #modules. has ( '/app.js' ) ) {
473545 log . info ( ' ✓' , 'Custom App' )
@@ -476,6 +548,20 @@ export class Project {
476548 log . info ( ' ✓' , 'Custom 404 Page' )
477549 }
478550
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+
479565 if ( this . isDev ) {
480566 this . _watch ( )
481567 }
@@ -487,11 +573,12 @@ export class Project {
487573 for await ( const event of w ) {
488574 for ( const p of event . paths ) {
489575 const path = '/' + util . trimPrefix ( util . trimPrefix ( p , this . appRoot ) , '/' )
576+ // handle `api` dir remove directly
490577 const validated = ( ( ) => {
491578 if ( ! reModuleExt . test ( path ) && ! reStyleModuleExt . test ( path ) && ! reMDExt . test ( path ) ) {
492579 return false
493580 }
494- // ignore ' .aleph' and output directories
581+ // ignore ` .aleph` and output directories
495582 if ( path . startsWith ( '/.aleph/' ) || path . startsWith ( this . config . outputDir ) ) {
496583 return false
497584 }
@@ -874,7 +961,7 @@ export class Project {
874961 `MarkdownPage.meta = ${ JSON . stringify ( props , undefined , this . isDev ? 4 : undefined ) } ;` ,
875962 this . isDev && `_s(MarkdownPage, "useRef{ref}\\nuseEffect{}");` ,
876963 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' : '' )
878965 mod . jsSourceMap = ''
879966 mod . hash = ( new Sha1 ) . update ( mod . jsContent ) . hex ( )
880967 } else {
@@ -1125,10 +1212,10 @@ export class Project {
11251212 ] . flat ( ) )
11261213 ret . head = head
11271214 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 === '' ) {
11311216 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` )
11321219 }
11331220 } catch ( err ) {
11341221 ret . status = 500
@@ -1226,3 +1313,13 @@ async function writeTextFile(filepath: string, content: string) {
12261313 await ensureDir ( dir )
12271314 await Deno . writeTextFile ( filepath , content )
12281315}
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