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

Commit db301ee

Browse files
author
Je
committed
feat: improve build
1 parent 5c9946a commit db301ee

File tree

4 files changed

+171
-55
lines changed

4 files changed

+171
-55
lines changed

project.ts

Lines changed: 143 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
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'
33
import { safeLoadFront } from 'https://esm.sh/yaml-front-matter'
44
import { AlephAPIRequest, AlephAPIResponse } from './api.ts'
55
import { EventEmitter } from './events.ts'
@@ -9,7 +9,7 @@ import { Routing } from './router.ts'
99
import { colors, ensureDir, path, ServerRequest, Sha1, walk } from './std.ts'
1010
import { compile } from './tsc/compile.ts'
1111
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'
1313
import { cleanCSS, Document, less } from './vendor/mod.ts'
1414
import { 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+
}

router.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class Routing {
2727

2828
get paths() {
2929
const paths: string[] = []
30-
this._lookup(path => { paths.push(path.map(r => r.path).join('')) })
30+
this._lookup(path => { paths.push(path.map(r => r.path).join('')) }, true)
3131
return paths
3232
}
3333

@@ -62,7 +62,7 @@ export class Routing {
6262
newRoute.path = util.trimPrefix(newRoute.path, path)
6363
targetRoutes = route.children || (route.children = [])
6464
}
65-
}, true)
65+
})
6666
if (exists) {
6767
return
6868
}
@@ -88,7 +88,7 @@ export class Routing {
8888
}
8989
return false
9090
}
91-
}, true)
91+
})
9292
}
9393

9494
createRouter(location?: { pathname: string, search?: string }): [RouterURL, Module[]] {
@@ -123,19 +123,23 @@ export class Routing {
123123
params = p
124124
return false
125125
}
126-
})
126+
}, true)
127127

128128
return [{ locale, pathname, pagePath, params, query }, tree]
129129
}
130130

131+
lookup(callback: (path: Route[]) => Boolean | void) {
132+
this._lookup(callback)
133+
}
134+
131135
private _lookup(
132136
callback: (path: Route[]) => Boolean | void,
133-
containsNestIndex = false,
137+
skipNestIndex = false,
134138
__tracing: Route[] = [],
135139
__routes = this._routes
136140
) {
137141
for (const route of __routes) {
138-
if (!containsNestIndex && __tracing.length > 0 && route.path === '/') {
142+
if (skipNestIndex && __tracing.length > 0 && route.path === '/') {
139143
continue
140144
}
141145
if (callback([...__tracing, route]) === false) {
@@ -144,7 +148,7 @@ export class Routing {
144148
}
145149
for (const route of __routes) {
146150
if (route.path !== '/' && route.children?.length) {
147-
if (this._lookup(callback, containsNestIndex, [...__tracing, route], route.children) === false) {
151+
if (this._lookup(callback, skipNestIndex, [...__tracing, route], route.children) === false) {
148152
return false
149153
}
150154
}

types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ export interface AlephRuntime {
77

88
export interface SSROptions {
99
readonly fallback?: string // default is 404.html
10-
readonly include?: string[]
11-
readonly exclude?: string[]
10+
readonly include?: RegExp[]
11+
readonly exclude?: RegExp[]
1212
}
1313

1414
export interface Config {

0 commit comments

Comments
 (0)