@@ -7,13 +7,14 @@ import { existsDir, existsFile } from "../lib/fs.ts";
77import { parseHtmlLinks } from "./html.ts" ;
88import log from "../lib/log.ts" ;
99import util from "../lib/util.ts" ;
10- import { DependencyGraph } from "./graph.ts" ;
10+ import type { DependencyGraph } from "./graph.ts" ;
1111import {
1212 builtinModuleExts ,
1313 getAlephPkgUri ,
1414 initModuleLoaders ,
1515 loadImportMap ,
1616 loadJSXConfig ,
17+ restoreUrl ,
1718 toLocalPath ,
1819} from "./helpers.ts" ;
1920import { initRoutes } from "./routing.ts" ;
@@ -33,6 +34,7 @@ export async function build(serverEntry?: string) {
3334 const moduleLoaders = await initModuleLoaders ( importMap ) ;
3435 const config : AlephConfig | undefined = Reflect . get ( globalThis , "__ALEPH_CONFIG" ) ;
3536 const platform = config ?. build ?. platform ?? "deno" ;
37+ const target = config ?. build ?. target ?? "es2015" ;
3638 const outputDir = join ( workingDir , config ?. build ?. outputDir ?? "dist" ) ;
3739
3840 if ( platform === "cloudflare" || platform === "vercel" ) {
@@ -57,6 +59,7 @@ export async function build(serverEntry?: string) {
5759 return [ filename , exportNames ] ;
5860 } ) ) ;
5961 }
62+
6063 const modulesProxyPort = Deno . env . get ( "ALEPH_MODULES_PROXY_PORT" ) ;
6164 const serverEntryCode = [
6265 `import { DependencyGraph } from "${ alephPkgUri } /server/graph.ts";` ,
@@ -162,7 +165,7 @@ export async function build(serverEntry?: string) {
162165 build . onResolve ( { filter : / .* / } , ( args ) => {
163166 let importUrl = args . path ;
164167 if ( importUrl in importMap . imports ) {
165- // since deno deploy doesn't support importMap, we need to resolve the 'react' import
168+ // since deno deploy doesn't support importMap yet , we need to resolve the 'react' import
166169 importUrl = importMap . imports [ importUrl ] ;
167170 }
168171
@@ -230,8 +233,10 @@ export async function build(serverEntry?: string) {
230233 } ] ,
231234 } ) ;
232235
233- // create server_dependency_graph.js
234236 const serverDependencyGraph : DependencyGraph | undefined = Reflect . get ( globalThis , "serverDependencyGraph" ) ;
237+ const clientDependencyGraph : DependencyGraph | undefined = Reflect . get ( globalThis , "clientDependencyGraph" ) ;
238+
239+ // create server_dependency_graph.js
235240 if ( serverDependencyGraph ) {
236241 // deno-lint-ignore no-unused-vars
237242 const modules = serverDependencyGraph . modules . map ( ( { sourceCode, ...ret } ) => ret ) ;
@@ -246,26 +251,26 @@ export async function build(serverEntry?: string) {
246251 if ( await existsFile ( join ( workingDir , "index.html" ) ) ) {
247252 const html = await Deno . readFile ( join ( workingDir , "index.html" ) ) ;
248253 const links = await parseHtmlLinks ( html ) ;
249- for ( const link of links ) {
250- if ( ! util . isLikelyHttpURL ( link ) ) {
251- const ext = extname ( link ) ;
252- if ( ext === ". css" || builtinModuleExts . includes ( ext . slice ( 1 ) ) ) {
253- const specifier = "." + util . cleanPath ( link ) ;
254+ for ( const src of links ) {
255+ if ( ! util . isLikelyHttpURL ( src ) ) {
256+ const ext = extname ( util . splitBy ( src , "?" ) [ 0 ] ) . slice ( 1 ) ;
257+ if ( ext === "css" || builtinModuleExts . includes ( ext ) ) {
258+ const specifier = "." + util . cleanPath ( src ) ;
254259 tasks . push ( specifier ) ;
255260 }
256261 }
257262 }
258263 }
259- tasks . push ( `${ alephPkgUri } /framework/core/style.ts` ) ;
264+
265+ const entryModules = new Set ( tasks ) ;
266+ const allModules = new Set < string > ( ) ;
260267
261268 // transform client modules
262269 const serverHandler : FetchHandler | undefined = Reflect . get ( globalThis , "__ALEPH_SERVER" ) ?. handler ;
263- const clientModules = new Set < string > ( ) ;
264270 if ( serverHandler ) {
265271 while ( tasks . length > 0 ) {
266272 const deps = new Set < string > ( ) ;
267273 await Promise . all ( tasks . map ( async ( specifier ) => {
268- clientModules . add ( specifier ) ;
269274 const url = new URL ( util . isLikelyHttpURL ( specifier ) ? toLocalPath ( specifier ) : specifier , "http://localhost" ) ;
270275 const isCSS = url . pathname . endsWith ( ".css" ) ;
271276 const req = new Request ( url . toString ( ) ) ;
@@ -282,20 +287,151 @@ export async function build(serverEntry?: string) {
282287 ] ) ;
283288 await res . body ?. pipeTo ( file . writable ) ;
284289 if ( ! isCSS ) {
285- const clientDependencyGraph : DependencyGraph | undefined = Reflect . get ( globalThis , "clientDependencyGraph" ) ;
286- clientDependencyGraph ?. get ( specifier ) ?. deps ?. forEach ( ( { specifier } ) => {
290+ clientDependencyGraph ?. get ( specifier ) ?. deps ?. forEach ( ( { specifier, dynamic } ) => {
291+ if ( dynamic ) {
292+ entryModules . add ( specifier ) ;
293+ }
287294 if ( specifier . endsWith ( ".css" ) ) {
288295 deps . add ( specifier + "?module" ) ;
289296 } else {
290297 deps . add ( specifier ) ;
291298 }
292299 } ) ;
300+ } else if ( url . searchParams . has ( "module" ) ) {
301+ deps . add ( `${ alephPkgUri } /framework/core/style.ts` ) ;
293302 }
303+ allModules . add ( specifier ) ;
294304 } ) ) ;
295- tasks = Array . from ( deps ) . filter ( ( specifier ) => ! clientModules . has ( specifier ) ) ;
305+ tasks = Array . from ( deps ) . filter ( ( specifier ) => ! allModules . has ( specifier ) ) ;
296306 }
297307 }
298308
309+ // count client module refs
310+ const refs = new Map < string , Set < string > > ( ) ;
311+ for ( const name of entryModules ) {
312+ clientDependencyGraph ?. walk ( name , ( { specifier } , importer ) => {
313+ if ( importer ) {
314+ let set = refs . get ( specifier ) ;
315+ if ( ! set ) {
316+ set = new Set < string > ( ) ;
317+ refs . set ( specifier , set ) ;
318+ }
319+ set . add ( importer . specifier ) ;
320+ }
321+ } ) ;
322+ }
323+
324+ // hygiene 1
325+ /*
326+ B(1) <-
327+ A <- <- <- D(1+) :: A <- D(1)
328+ C(1) <-
329+ */
330+ refs . forEach ( ( counter , specifier ) => {
331+ if ( counter . size > 1 ) {
332+ const a = Array . from ( counter ) . filter ( ( specifier ) => {
333+ const set = refs . get ( specifier ) ;
334+ if ( set ?. size === 1 ) {
335+ const name = set . values ( ) . next ( ) . value ;
336+ if ( name && counter . has ( name ) ) {
337+ return false ;
338+ }
339+ }
340+ return true ;
341+ } ) ;
342+ refs . set ( specifier , new Set ( a ) ) ;
343+ }
344+ } ) ;
345+
346+ // hygiene 2 (twice)
347+ /*
348+ B(1) <-
349+ A <- C(1) <- E(1+) :: A <- E(1)
350+ D(1) <-
351+ */
352+ for ( let i = 0 ; i < 2 ; i ++ ) {
353+ refs . forEach ( ( counter , specifier ) => {
354+ if ( counter . size > 0 ) {
355+ const a = Array . from ( counter ) ;
356+ if (
357+ a . every ( ( specifier ) => {
358+ const set = refs . get ( specifier ) ;
359+ return set ?. size === 1 ;
360+ } )
361+ ) {
362+ const set = new Set ( a . map ( ( specifier ) => {
363+ const set = refs . get ( specifier ) ;
364+ return set ?. values ( ) . next ( ) . value ;
365+ } ) ) ;
366+ if ( set . size === 1 ) {
367+ refs . set ( specifier , set ) ;
368+ }
369+ }
370+ }
371+ } ) ;
372+ }
373+
374+ // find client modules
375+ const clientModules = new Set < string > ( entryModules ) ;
376+ refs . forEach ( ( counter , specifier ) => {
377+ if ( counter . size > 1 ) {
378+ clientModules . add ( specifier ) ;
379+ }
380+ console . log ( `${ specifier } is referenced by \n - ${ Array . from ( counter ) . join ( "\n - " ) } ` ) ;
381+ } ) ;
382+
383+ // bundle client modules
384+ const bundling = new Set < string > ( ) ;
385+ clientModules . forEach ( ( specifier ) => {
386+ if (
387+ clientDependencyGraph ?. get ( specifier ) ?. deps ?. some ( ( { specifier } ) => ! clientModules . has ( specifier ) ) &&
388+ ! util . splitBy ( specifier , "?" ) [ 0 ] . endsWith ( ".css" )
389+ ) {
390+ bundling . add ( specifier ) ;
391+ }
392+ } ) ;
393+ await Promise . all (
394+ Array . from ( bundling ) . map ( async ( entryPoint ) => {
395+ const url = new URL ( util . isLikelyHttpURL ( entryPoint ) ? toLocalPath ( entryPoint ) : entryPoint , "http://localhost" ) ;
396+ let jsFile = join ( outputDir , url . pathname ) ;
397+ if ( entryPoint . startsWith ( "https://esm.sh/" ) ) {
398+ jsFile += ".js" ;
399+ }
400+ await esbuild ( {
401+ entryPoints : [ jsFile ] ,
402+ outfile : jsFile ,
403+ allowOverwrite : true ,
404+ platform : "browser" ,
405+ format : "esm" ,
406+ target : [ target ] ,
407+ bundle : true ,
408+ minify : true ,
409+ treeShaking : true ,
410+ sourcemap : false ,
411+ plugins : [ {
412+ name : "aleph-esbuild-plugin" ,
413+ setup ( build ) {
414+ build . onResolve ( { filter : / .* / } , ( args ) => {
415+ const path = util . trimPrefix ( args . path , outputDir ) ;
416+ let specifier = "." + path ;
417+ if ( args . path . startsWith ( "/-/" ) ) {
418+ specifier = restoreUrl ( path ) ;
419+ }
420+ if ( clientModules . has ( specifier ) && specifier !== entryPoint ) {
421+ return { path : args . path , external : true } ;
422+ }
423+ let jsFile = join ( outputDir , path ) ;
424+ if ( specifier . startsWith ( "https://esm.sh/" ) ) {
425+ jsFile += ".js" ;
426+ }
427+ return { path : jsFile } ;
428+ } ) ;
429+ } ,
430+ } ] ,
431+ } ) ;
432+ } ) ,
433+ ) ;
434+
299435 // clean up then exit
300436 if ( jsxShimFile ) {
301437 await Deno . remove ( jsxShimFile ) ;
0 commit comments