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

Commit a2781ff

Browse files
committed
Implement the smart esm bundling
1 parent 6d8140a commit a2781ff

File tree

6 files changed

+192
-37
lines changed

6 files changed

+192
-37
lines changed

server/build.ts

Lines changed: 150 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import { existsDir, existsFile } from "../lib/fs.ts";
77
import { parseHtmlLinks } from "./html.ts";
88
import log from "../lib/log.ts";
99
import util from "../lib/util.ts";
10-
import { DependencyGraph } from "./graph.ts";
10+
import type { DependencyGraph } from "./graph.ts";
1111
import {
1212
builtinModuleExts,
1313
getAlephPkgUri,
1414
initModuleLoaders,
1515
loadImportMap,
1616
loadJSXConfig,
17+
restoreUrl,
1718
toLocalPath,
1819
} from "./helpers.ts";
1920
import { 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);

server/graph.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,44 @@ export class DependencyGraph {
101101
}
102102
}
103103

104-
walk(specifier: string, callback: (mod: Module) => void) {
105-
this.#walk(specifier, callback);
104+
shallowWalk(specifier: string, callback: (mod: Module) => void) {
105+
this.#shallowWalk(specifier, callback);
106106
}
107107

108-
#walk(specifier: string, callback: (mod: Module) => void, _set = new Set<string>()) {
108+
#shallowWalk(
109+
specifier: string,
110+
callback: (mod: Module) => void,
111+
_set = new Set<string>(),
112+
) {
109113
if (this.#modules.has(specifier)) {
110114
const mod = this.#modules.get(specifier)!;
111115
callback(mod);
112116
_set.add(specifier);
113117
mod.deps?.forEach((dep) => {
114118
if (!_set.has(dep.specifier)) {
115-
this.#walk(dep.specifier, callback, _set);
119+
this.#shallowWalk(dep.specifier, callback, _set);
120+
}
121+
});
122+
}
123+
}
124+
125+
walk(specifier: string, callback: (mod: Module, importer?: Module) => void) {
126+
this.#walk(specifier, callback);
127+
}
128+
129+
#walk(
130+
specifier: string,
131+
callback: (mod: Module, importer?: Module) => void,
132+
importer?: Module,
133+
_path: string[] = [],
134+
) {
135+
if (this.#modules.has(specifier)) {
136+
const mod = this.#modules.get(specifier)!;
137+
callback(mod, importer);
138+
_path.push(specifier);
139+
mod.deps?.forEach((dep) => {
140+
if (!_path.includes(dep.specifier)) {
141+
this.#walk(dep.specifier, callback, mod, [..._path]);
116142
}
117143
});
118144
}

server/mod.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Routes } from "../lib/route.ts";
66
import util from "../lib/util.ts";
77
import { VERSION } from "../version.ts";
88
import { errorHtml } from "./error.ts";
9+
import { DependencyGraph } from "./graph.ts";
910
import { getDeploymentId, initModuleLoaders, loadImportMap, loadJSXConfig } from "./helpers.ts";
1011
import { type HTMLRewriterHandlers, loadAndFixIndexHtml } from "./html.ts";
1112
import renderer, { type SSR } from "./renderer.ts";
@@ -357,12 +358,14 @@ export const serve = (options: ServerOptions = {}) => {
357358
Reflect.deleteProperty(globalThis, "__ALEPH_INDEX_HTML");
358359
Reflect.deleteProperty(globalThis, "__UNO_GENERATOR");
359360

360-
// inject global `__ALEPH_CONFIG`
361+
// inject global objects
361362
Reflect.set(globalThis, "__ALEPH_CONFIG", Object.assign({}, config));
363+
Reflect.set(globalThis, "clientDependencyGraph", new DependencyGraph());
362364

363365
const { hostname, port = 8080, certFile, keyFile, signal } = options;
364366
if (Deno.env.get("ALEPH_CLI")) {
365367
Reflect.set(globalThis, "__ALEPH_SERVER", { hostname, port, certFile, keyFile, handler, signal });
368+
Reflect.set(globalThis, "serverDependencyGraph", new DependencyGraph());
366369
} else {
367370
if (certFile && keyFile) {
368371
serveTls(handler, { hostname, port, certFile, keyFile, signal });

server/proxy_modules.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getContentType } from "../lib/mime.ts";
55
import { serveDir } from "../lib/serve.ts";
66
import util from "../lib/util.ts";
77
import { bundleCSS } from "./bundle_css.ts";
8-
import { DependencyGraph } from "./graph.ts";
8+
import type { DependencyGraph } from "./graph.ts";
99
import { builtinModuleExts } from "./helpers.ts";
1010
import type { ImportMap, ModuleLoader, ModuleLoaderContent, ModuleLoaderEnv } from "./types.ts";
1111

@@ -105,9 +105,6 @@ type ProxyModulesOptions = {
105105
/** serve app modules to support module loader that allows you import Non-JavaScript modules like `.css/.vue/.svelet/...` */
106106
export function proxyModules(port: number, options: ProxyModulesOptions) {
107107
return new Promise<void>((resolve, reject) => {
108-
if (!Reflect.has(globalThis, "serverDependencyGraph")) {
109-
Reflect.set(globalThis, "serverDependencyGraph", new DependencyGraph());
110-
}
111108
serveDir({
112109
port,
113110
signal: options.signal,

server/renderer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ export default {
8989
}
9090
};
9191
for (const { filename } of routeModules) {
92-
serverDependencyGraph.walk(filename, lookupModuleStyle);
92+
serverDependencyGraph.shallowWalk(filename, lookupModuleStyle);
9393
}
9494
for (const serverEntry of builtinModuleExts.map((ext) => `./server.${ext}`)) {
9595
if (serverDependencyGraph.get(serverEntry)) {
96-
serverDependencyGraph.walk(serverEntry, lookupModuleStyle);
96+
serverDependencyGraph.shallowWalk(serverEntry, lookupModuleStyle);
9797
break;
9898
}
9999
}

0 commit comments

Comments
 (0)