Skip to main content
Module

x/aleph/server/aleph.ts

The Full-stack Framework in Deno.
Very Popular
Go to Latest
File
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616
import { dim } from 'https://deno.land/std@0.106.0/fmt/colors.ts'import { indexOf, copy, equals } from 'https://deno.land/std@0.106.0/bytes/mod.ts'import { ensureDir } from 'https://deno.land/std@0.106.0/fs/ensure_dir.ts'import { walk } from 'https://deno.land/std@0.106.0/fs/walk.ts'import { createHash } from 'https://deno.land/std@0.106.0/hash/mod.ts'import { basename, dirname, extname, join, resolve } from 'https://deno.land/std@0.106.0/path/mod.ts'import { Bundler, bundlerRuntimeCode, simpleJSMinify } from '../bundler/mod.ts'import type { TransformOptions, TransformResult, ModuleSource } from '../compiler/mod.ts'import { wasmChecksum, parseExportNames, SourceType, fastTransform, transform, stripSsrCode } from '../compiler/mod.ts'import { EventEmitter } from '../framework/core/events.ts'import { builtinModuleExts, toPagePath, trimBuiltinModuleExts } from '../framework/core/module.ts'import { Routing } from '../framework/core/routing.ts'import { frameworks } from '../framework/mod.ts'import { cssLoader } from '../plugins/css.ts'import { ensureTextFile, existsDir, existsFile, findFile, lazyRemove } from '../shared/fs.ts'import log, { Measure } from '../shared/log.ts'import util from '../shared/util.ts'import type { APIHandler, Aleph as IAleph, DependencyDescriptor, ImportMap, LoadInput, LoadOutput, Module, RouterURL, ResolveResult, TransformInput, TransformOutput, SSRData, RenderOutput } from '../types.d.ts'import { VERSION } from '../version.ts'import { Analyzer } from './analyzer.ts'import { cache } from './cache.ts'import type { RequiredConfig } from './config.ts'import { defaultConfig, fixConfig, getDefaultImportMap, loadConfig, loadImportMap } from './config.ts'import { checkAlephDev, checkDenoVersion, clearBuildCache, computeHash, decoder, encoder, getAlephPkgUri, getSourceType, isLocalUrl, moduleExclude, toLocalPath, toRelativePath,} from './helper.ts'import { getContentType } from './mime.ts'import { buildHtml, Renderer } from './renderer.ts'
type CompileOptions = { source?: ModuleSource, forceRefresh?: boolean, ignoreDeps?: boolean, httpExternal?: boolean virtual?: boolean}
type ResolveListener = { test: RegExp, resolve(specifier: string): ResolveResult,}
type LoadListener = { test: RegExp, load(input: LoadInput): Promise<LoadOutput> | LoadOutput,}
type TransformListener = { test: RegExp | 'hmr' | 'main', transform(input: TransformInput): TransformOutput | void | Promise<TransformOutput | void>,}
type RenderListener = (input: RenderOutput & { path: string }) => void | Promise<void>
/** The class for Aleph server runtime. */export class Aleph implements IAleph { #config: RequiredConfig #importMap: ImportMap #ready: Promise<void> #mode: 'development' | 'production' #workingDir: string #buildDir: string #modules: Map<string, Module> = new Map() #appModule: Module | null = null #pageRouting: Routing = new Routing() #apiRouting: Routing = new Routing() #analyzer: Analyzer = new Analyzer(this) #bundler: Bundler = new Bundler(this) #renderer: Renderer = new Renderer(this) #fsWatchListeners: Array<EventEmitter> = [] #resolverListeners: Array<ResolveListener> = [] #loadListeners: Array<LoadListener> = [] #transformListeners: Array<TransformListener> = [] #renderListeners: Array<RenderListener> = [] #dists: Set<string> = new Set() #reloading = false
constructor( workingDir = '.', mode: 'development' | 'production' = 'production', reload = false ) { checkDenoVersion() checkAlephDev() this.#mode = mode this.#workingDir = resolve(workingDir) this.#buildDir = join(this.#workingDir, '.aleph', mode) this.#config = { ...defaultConfig() } this.#importMap = { imports: {}, scopes: {} } this.#ready = Deno.env.get('DENO_TESTING') ? Promise.resolve() : this.init(reload) }
/** initiate runtime */ private async init(reload: boolean) { const ms = new Measure()
let [importMapFile, configFile] = await Promise.all([ findFile(this.#workingDir, ['import_map', 'import-map', 'importmap', 'importMap'].map(name => `${name}.json`)), findFile(this.#workingDir, ['ts', 'js', 'mjs', 'json'].map(ext => `aleph.config.${ext}`)) ]) if (importMapFile) { Object.assign(this.#importMap, await loadImportMap(importMapFile)) } else { Object.assign(this.#importMap, getDefaultImportMap()) } if (configFile) { Object.assign(this.#config, await loadConfig(configFile)) this.#pageRouting = new Routing(this.#config) }
await fixConfig(this.#workingDir, this.#config, this.#importMap) ms.stop('load config')
Deno.env.set('ALEPH_ENV', this.#mode) Deno.env.set('ALEPH_FRAMEWORK', this.#config.framework) Deno.env.set('ALEPH_WORKING_DIR', this.#workingDir) Deno.env.set('ALEPH_VERSION', VERSION)
const alephPkgUri = getAlephPkgUri() const srcDir = join(this.#workingDir, this.#config.srcDir) const apiDir = join(srcDir, 'api') const pagesDir = join(srcDir, 'pages') const manifestFile = join(this.#buildDir, 'build.manifest.json') const importMapString = JSON.stringify(this.#importMap) const pluginNames = this.#config.plugins.map(({ name }) => name).join(',')
let shouldRebuild = !await existsFile(manifestFile) let shouldUpdateManifest = shouldRebuild if (!shouldRebuild) { try { const v = JSON.parse(await Deno.readTextFile(manifestFile)) const confirmClean = (msg: string): boolean => { shouldUpdateManifest = true // in 'prodcution' mode, we don't ask user to clean build cache if (!this.isDev) { return true } return confirm(msg) } shouldRebuild = ( typeof v !== 'object' || v === null || v.compiler !== wasmChecksum || (v.importMap !== importMapString && confirmClean('The import-maps has been changed, clean build cache?')) || (v.plugins !== pluginNames && confirmClean('The plugin list has been updated, clean build cache?')) ) if (shouldRebuild) { shouldUpdateManifest = true } } catch (e) { } }
this.#reloading = reload if (reload || shouldRebuild) { if (await existsDir(this.#buildDir)) { await Deno.remove(this.#buildDir, { recursive: true }) } await ensureDir(this.#buildDir) }
if (shouldUpdateManifest) { ensureTextFile(manifestFile, JSON.stringify({ aleph: VERSION, deno: Deno.version.deno, compiler: wasmChecksum, importMap: importMapString, plugins: pluginNames, }, undefined, 2)) }
// load .env.* files for await (const { path: p, } of walk(this.workingDir, { match: [/(^|\/|\\)\.env(\.|$)/i], maxDepth: 1 })) { const text = await Deno.readTextFile(p) text.split('\n').forEach(line => { let [key, value] = util.splitBy(line, '=') key = key.trim() if (key) { Deno.env.set(key, value.trim()) } }) log.info('load env from', basename(p)) }
ms.stop(`init env`)
// apply plugins await Promise.all( this.#config.plugins.map(async plugin => { await plugin.setup(this) }) )
ms.stop('apply plugins')
const mwsFile = await findFile(this.#workingDir, ['ts', 'js', 'mjs'].map(ext => `${this.#config.srcDir}/api/_middlewares.${ext}`)) if (mwsFile) { const mwMod = await this.compile(`/api/${basename(mwsFile)}`, { httpExternal: true }) const { default: _middlewares } = await import('file://' + join(this.#buildDir, mwMod.jsFile)) const middlewares = Array.isArray(_middlewares) ? _middlewares.filter(fn => util.isFunction(fn)) : [] this.#config.server.middlewares.push(...middlewares) ms.stop(`load API middlewares (${middlewares.length}) from 'api/${basename(mwsFile)}'`) }
// init framework await frameworks[this.#config.framework].init(this) // compile and import framework renderer if (this.#config.ssr) { const mod = await this.compile(`${getAlephPkgUri()}/framework/${this.#config.framework}/renderer.ts`) const { render } = await this.importModule(mod) if (util.isFunction(render)) { this.#renderer.setFrameworkRenderer({ render }) } } ms.stop(`init ${this.#config.framework} framework`)
const appFile = await findFile(srcDir, builtinModuleExts.map(ext => `app.${ext}`)) const modules: string[] = [] const moduleWalkOptions = { includeDirs: false, skip: moduleExclude }
// pre-compile framework modules modules.push(`${alephPkgUri}/framework/${this.#config.framework}/bootstrap.ts`) if (this.isDev) { modules.push(`${alephPkgUri}/framework/core/hmr.ts`) modules.push(`${alephPkgUri}/framework/core/nomodule.ts`) } if (appFile) { modules.push(`/${basename(appFile)}`) }
// create API routing if (await existsDir(apiDir)) { for await (const { path: p } of walk(apiDir, { ...moduleWalkOptions, exts: builtinModuleExts })) { const specifier = util.cleanPath('/api/' + util.trimPrefix(p, apiDir)) if (!specifier.startsWith('/api/_middlewares.')) { this.#apiRouting.update(...this.createRouteUpdate(specifier)) } } }
// create Page routing if (await existsDir(pagesDir)) { for await (const { path: p } of walk(pagesDir, moduleWalkOptions)) { const specifier = util.cleanPath('/pages/' + util.trimPrefix(p, pagesDir)) if (this.isPageModule(specifier)) { this.#pageRouting.update(...this.createRouteUpdate(specifier)) if (!this.isDev) { modules.push(specifier) } } } }
// wait all compilation tasks are done await Promise.all(modules.map(specifier => this.compile(specifier)))
// bundle if (!this.isDev) { await this.bundle() }
// end reload if (reload) { this.#reloading = false }
ms.stop('init project')
if (this.isDev) { this.watch() } }
/** watch file changes, re-compile modules, and send HMR signal to client. */ private async watch() { const srcDir = join(this.#workingDir, this.#config.srcDir) const w = Deno.watchFs(srcDir, { recursive: true }) log.info('Start watching code changes...') for await (const event of w) { for (const path of event.paths) { const specifier = util.cleanPath(util.trimPrefix(path, srcDir)) if (this.isScopedModule(specifier)) { util.debounceById( specifier, () => this.watchHandler(path, specifier), 50 ) } } } }
private async watchHandler(path: string, specifier: string): Promise<void> { if (await existsFile(path)) { if (this.#modules.has(specifier)) { try { const prevModule = this.#modules.get(specifier)! const module = await this.compile(specifier, { forceRefresh: true, ignoreDeps: true, httpExternal: prevModule.httpExternal }) const refreshPage = ( this.#config.ssr && ( (module.denoHooks !== undefined && JSON.stringify(prevModule.denoHooks) !== JSON.stringify(module.denoHooks)) || (module.ssrPropsFn !== undefined && prevModule.ssrPropsFn !== module.ssrPropsFn) ) ) const hmrable = this.isHMRable(specifier) if (hmrable) { this.#fsWatchListeners.forEach(e => { e.emit('modify-' + module.specifier, { refreshPage: refreshPage || undefined }) }) } this.applyCompilationSideEffect(module, () => { if (!hmrable && this.isHMRable(specifier)) { log.debug(`compilation side-effect: ${specifier} ${dim('<-')} ${module.specifier}(${module.sourceHash.substr(0, 6)})`) this.#fsWatchListeners.forEach(e => { e.emit('modify-' + specifier, { refreshPage: refreshPage || undefined }) }) } this.clearSSRCache(specifier) }) this.clearSSRCache(specifier) log.debug('modify', specifier) } catch (err) { log.error(`compile(${specifier}):`, err.message) } } else { let routePath: string | undefined = undefined let isIndex: boolean | undefined = undefined let emitHMR = false if (this.isPageModule(specifier)) { emitHMR = true this.#pageRouting.lookup(routes => { routes.forEach(({ module }) => { if (module === specifier) { emitHMR = false return false // break loop } }) }) if (emitHMR) { const [_routePath, _specifier, _isIndex] = this.createRouteUpdate(specifier) routePath = _routePath specifier = _specifier isIndex = _isIndex this.#pageRouting.update(routePath, specifier, isIndex) } } else if (specifier.startsWith('/api/') && !specifier.startsWith('/api/_middlewares.')) { let routeExists = false this.#apiRouting.lookup(routes => { routes.forEach(({ module }) => { if (module === specifier) { routeExists = true return false // break loop } }) }) if (!routeExists) { this.#apiRouting.update(...this.createRouteUpdate(specifier)) } } if (trimBuiltinModuleExts(specifier) === '/app') { await this.compile(specifier) emitHMR = true } if (emitHMR) { this.#fsWatchListeners.forEach(e => { e.emit('add', { specifier, routePath, isIndex }) }) log.debug('add', specifier) } } } else { if (this.#modules.has(specifier)) { this.#modules.delete(specifier) } if (trimBuiltinModuleExts(specifier) === '/app') { this.#fsWatchListeners.forEach(e => e.emit('remove', specifier)) } else if (this.isPageModule(specifier)) { this.#pageRouting.removeRouteByModule(specifier) this.#fsWatchListeners.forEach(e => e.emit('remove', specifier)) } else if (specifier.startsWith('/api/')) { this.#apiRouting.removeRouteByModule(specifier) } this.clearSSRCache(specifier) log.debug('remove', specifier) } }
/** check the file whether it is a scoped module. */ private isScopedModule(specifier: string) { if (moduleExclude.some(r => r.test(specifier))) { return false }
// is compiled module if (this.#modules.has(specifier)) { return true }
// is page module by plugin if (this.isPageModule(specifier)) { return true }
// is api or app module for (const ext of builtinModuleExts) { if ( specifier.endsWith('.' + ext) && ( specifier.startsWith('/api/') || util.trimSuffix(specifier, '.' + ext) === '/app' ) ) { return true } }
return false }
get mode() { return this.#mode }
get isDev() { return this.#mode === 'development' }
get workingDir() { return this.#workingDir }
get buildDir() { return this.#buildDir }
get config() { return this.#config }
get importMap() { return this.#importMap }
get ready() { return this.#ready }
get transformListeners() { return this.#transformListeners }
/** get the module by given specifier. */ getModule(specifier: string): Module | null { if (specifier === 'app') { return this.#appModule } if (this.#modules.has(specifier)) { return this.#modules.get(specifier)! } return null }
/** get the first module in the modules map where predicate is true, and null otherwise. */ findModule(predicate: (module: Module) => boolean): Module | null { for (const specifier of this.#modules.keys()) { const module = this.#modules.get(specifier)! if (predicate(module)) { return module } } return null }
/** get api route by the given location. */ async getAPIRoute(location: { pathname: string, search?: string }): Promise<[RouterURL, APIHandler] | null> { const router = this.#apiRouting.createRouter(location) if (router !== null) { const [url, nestedModules] = router if (url.routePath !== '') { const specifier = nestedModules.pop()! const filepath = join(this.#workingDir, this.#config.srcDir, util.trimPrefix(specifier, 'file://')) const qs = this.isDev ? '?mtime=' + (await Deno.lstat(filepath)).mtime?.getTime() : '' const { handler } = await import(`file://${filepath}${qs}`) return [url, handler] } } return null }
onResolve(test: RegExp, callback: (specifier: string) => ResolveResult): void { this.#resolverListeners.push({ test, resolve: callback }) }
onLoad(test: RegExp, callback: (input: LoadInput) => LoadOutput | Promise<LoadOutput>): void { this.#loadListeners.push({ test, load: callback }) }
onTransform(test: RegExp | 'hmr' | 'main', callback: (input: TransformOutput & { module: Module }) => TransformOutput | Promise<TransformOutput>): void { this.#transformListeners.push({ test, transform: callback }) }
onRender(callback: (input: RenderOutput & { path: string }) => void | Promise<void>): void { this.#renderListeners.push(callback) }
/** add a module by given path and optional source code. */ async addModule(specifier: string, sourceCode: string, forceRefresh?: boolean): Promise<Module> { let sourceType = getSourceType(specifier) if (sourceType === SourceType.Unknown) { throw new Error("addModule: unknown source type") } const source = { code: sourceCode, type: sourceType, } const module = await this.compile(specifier, { source, forceRefresh, }) if (specifier.startsWith('pages/') || specifier.startsWith('api/')) { specifier = '/' + specifier } if (specifier.startsWith('/pages/') && this.isPageModule(specifier)) { this.#pageRouting.update(...this.createRouteUpdate(specifier)) } else if (specifier.startsWith('/api/') && !specifier.startsWith('/api/_middlewares.')) { this.#apiRouting.update(...this.createRouteUpdate(specifier)) } Object.assign(module, { source }) return module }
/** add a dist. */ async addDist(path: string, content: Uint8Array): Promise<void> { const pathname = util.cleanPath(path) const savePath = join(this.#buildDir, pathname) if (!await existsFile(savePath)) { const saveDir = dirname(savePath) await ensureDir(saveDir) await clearBuildCache(savePath, extname(savePath).slice(1)) await Deno.writeFile(savePath, content) } this.#dists.add(pathname) }
/** get ssr data by the given location(page), return `null` if no data defined */ async getSSRData(loc: { pathname: string, search?: string }): Promise<Record<string, SSRData> | null> { const [router, nestedModules] = this.#pageRouting.createRouter(loc) const { routePath } = router if (routePath === '' || !this.isSSRable(router.pathname)) { return null }
// pre-compile modules to check ssr options await Promise.all( nestedModules .filter(specifier => !this.#modules.has(specifier)) .map(specifier => this.compile(specifier)) )
if (!this.#isDataRoute(nestedModules)) { return null }
const path = loc.pathname + (loc.search || '') const [_, data] = await this.#renderer.cache(routePath, path, async () => { return await this.#renderPage(router, nestedModules) }) return data }
/* check whether the route has data by givan nested modules */ #isDataRoute(nestedModules: string[]) { const pageModule = this.getModule(nestedModules[nestedModules.length - 1]) if (pageModule && pageModule.ssrPropsFn) { return true } for (const specifier of ['app', ...nestedModules]) { const mod = this.getModule(specifier) if (mod) { if (mod.denoHooks?.length) { return true } let ok = false this.lookupDeps(mod.specifier, dep => { const depMod = this.getModule(dep.specifier) if (depMod?.denoHooks?.length) { ok = true return false // break loop } }) if (ok) { return } } } return false }
/** render page to HTML by the given location */ async renderPage(loc: { pathname: string, search?: string }): Promise<[number, string]> { const [router, nestedModules] = this.#pageRouting.createRouter(loc) const { routePath } = router const path = loc.pathname + (loc.search || '')
if (!this.isSSRable(loc.pathname)) { const [html] = await this.#renderer.cache('-', 'spa-index-html', async () => { return [await this.createSPAIndexHtml(), null] }) return [200, html] }
if (routePath === '') { const [html] = await this.#renderer.cache('404', path, async () => { const [_, nestedModules] = this.#pageRouting.createRouter({ pathname: '/404' }) return await this.#renderPage(router, nestedModules.slice(0, 1)) }) return [404, html] }
const [html] = await this.#renderer.cache(routePath, path, async () => { return await this.#renderPage(router, nestedModules) }) return [200, html] }
async #renderPage(url: RouterURL, nestedModules: string[]): Promise<[string, Record<string, SSRData> | null]> { let [html, data] = await this.#renderer.renderPage(url, nestedModules) for (const callback of this.#renderListeners) { await callback({ path: url.toString(), html, data }) } return [buildHtml(html, !this.isDev), data] }
/** create a fs watcher. */ createFSWatcher(): EventEmitter { const e = new EventEmitter() this.#fsWatchListeners.push(e) return e }
/** remove the fs watcher. */ removeFSWatcher(e: EventEmitter) { e.removeAllListeners() const index = this.#fsWatchListeners.indexOf(e) if (index > -1) { this.#fsWatchListeners.splice(index, 1) } }
/** create main bootstrap script in javascript. */ async createMainJS(bundleMode = false): Promise<string> { const alephPkgUri = getAlephPkgUri() const alephPkgPath = alephPkgUri.replace('https://', '').replace('http://localhost:', 'http_localhost_') const { framework, basePath: basePath, i18n: { defaultLocale } } = this.#config const { routes } = this.#pageRouting const config: Record<string, any> = { basePath, appModule: this.#appModule?.specifier, routes, renderMode: this.#config.ssr ? 'ssr' : 'spa', defaultLocale, locales: [], rewrites: this.#config.server.rewrites, }
let code: string if (bundleMode) { config.dataRoutes = this.#pageRouting.paths.filter(pathname => { const [_, nestedModules] = this.#pageRouting.createRouter({ pathname }) return this.#isDataRoute(nestedModules) }) code = [ `__ALEPH__.basePath = ${JSON.stringify(basePath)};`, `__ALEPH__.pack["${alephPkgUri}/framework/${framework}/bootstrap.ts"].default(${JSON.stringify(config)});` ].join('') } else { code = [ `import bootstrap from "./-/${alephPkgPath}/framework/${framework}/bootstrap.js";`, this.isDev && `import { connect } from "./-/${alephPkgPath}/framework/core/hmr.js";`, this.isDev && `connect(${JSON.stringify(basePath)});`, `bootstrap(${JSON.stringify(config, undefined, this.isDev ? 2 : undefined)});` ].filter(Boolean).join('\n') } for (const { test, transform } of this.#transformListeners) { if (test === 'main') { let ret = await transform({ module: { specifier: '/main.js', deps: [], sourceHash: '', jsFile: '' }, code, }) if (util.isFilledString(ret?.code)) { code = ret!.code } } } return code }
/** create the index html for SPA mode. */ private async createSPAIndexHtml(): Promise<string> { let html = { lang: this.#config.i18n.defaultLocale, headElements: [], scripts: this.getScripts(), body: '<div id="__aleph"></div>', bodyAttrs: {}, } for (const callback of this.#renderListeners) { await callback({ path: 'spa-index-html', html, data: null }) } return buildHtml(html, !this.isDev) }
/** get scripts for html output */ getScripts(entryFile?: string) { const { framework } = this.#config const basePath = util.trimSuffix(this.#config.basePath, '/') const alephPkgPath = getAlephPkgUri().replace('https://', '').replace('http://localhost:', 'http_localhost_') const syncChunks = this.#bundler.getSyncChunks()
if (this.isDev) { const preload: string[] = [ `/framework/core/module.js`, `/framework/core/events.js`, `/framework/core/routing.js`, `/framework/core/hmr.js`, `/framework/${framework}/bootstrap.js`, `/shared/util.js`, ].map(p => `${basePath}/_aleph/-/${alephPkgPath}${p}`)
if (this.#appModule) { preload.push(`${basePath}/_aleph/app.js`) }
if (entryFile) { preload.push(`${basePath}/_aleph${entryFile}`) }
return [ ...preload.map(src => ({ src, type: 'module', preload: true })), { src: `${basePath}/_aleph/main.js`, type: 'module' }, { src: `${basePath}/_aleph/-/${alephPkgPath}/nomodule.js`, nomodule: true }, ] }
return [ simpleJSMinify(bundlerRuntimeCode), ...syncChunks.map(filename => ({ src: `${basePath}/_aleph/${filename}` })) ] }
computeModuleHash(module: Module) { const hasher = createHash('md5').update(module.sourceHash) this.lookupDeps(module.specifier, dep => { const depMod = this.getModule(dep.specifier) if (depMod) { hasher.update(depMod.sourceHash) } }) return hasher.toString() }
/** parse the export names of the module. */ async parseModuleExportNames(specifier: string): Promise<string[]> { const { content, contentType } = await this.fetchModule(specifier) const sourceType = getSourceType(specifier, contentType || undefined) if (sourceType === SourceType.Unknown || sourceType === SourceType.CSS) { return [] } const code = decoder.decode(content) const names = await parseExportNames(specifier, code, { sourceType }) return (await Promise.all(names.map(async name => { if (name.startsWith('{') && name.endsWith('}')) { let dep = name.slice(1, -1) if (util.isLikelyHttpURL(specifier)) { const url = new URL(specifier) if (dep.startsWith('/')) { dep = url.protocol + '//' + url.host + dep } else { dep = url.protocol + '//' + url.host + join(url.pathname, dep) } } return await this.parseModuleExportNames(dep) } return name }))).flat() }
/** common compiler options */ get commonCompilerOptions(): TransformOptions { return { alephPkgUri: getAlephPkgUri(), workingDir: this.#workingDir, importMap: this.#importMap, inlineStylePreprocess: async (key: string, type: string, tpl: string) => { if (type !== 'css') { for (const { test, load } of this.#loadListeners) { if (test.test(`.${type}`)) { const { code, type: codeType } = await load({ specifier: key, data: encoder.encode(tpl) }) if (codeType === 'css') { type = 'css' tpl = code break } } } } const { code } = await cssLoader({ specifier: key, data: encoder.encode(tpl) }, this) return code }, isDev: this.isDev, react: this.#config.react, } }
analyze() { this.#analyzer.reset() this.#pageRouting.lookup(routes => { routes.forEach(({ module: specifier }) => { const module = this.getModule(specifier) if (module) { this.#analyzer.addEntry(module) } }) }) return this.#analyzer.entries }
/** build the application to a static site(SSG) */ async build() { const start = performance.now()
// wait for app ready await this.#ready
const outputDir = join(this.#workingDir, this.#config.build.outputDir) const distDir = join(outputDir, '_aleph')
// clean previous build if (await existsDir(outputDir)) { for await (const entry of Deno.readDir(outputDir)) { await Deno.remove(join(outputDir, entry.name), { recursive: entry.isDirectory }) } }
// copy bundle dist await this.#bundler.copyDist()
// ssg await this.ssg()
// copy public assets const publicDir = join(this.#workingDir, 'public') if (await existsDir(publicDir)) { for await (const { path: p } of walk(publicDir, { includeDirs: false, skip: [/(^|\/|\\)\./] })) { const rp = util.trimPrefix(p, publicDir) const fp = join(outputDir, rp) await ensureDir(dirname(fp)) await Deno.copyFile(p, fp) } }
// copy custom dist files if (this.#dists.size > 0) { Promise.all(Array.from(this.#dists.values()).map(async path => { const src = join(this.#buildDir, path) if (await existsFile(src)) { const dest = join(distDir, path) await ensureDir(dirname(dest)) return Deno.copyFile(src, dest) } })) }
log.info(`Done in ${Math.round(performance.now() - start)}ms`) }
private createRouteUpdate(specifier: string): [string, string, boolean | undefined] { const isBuiltinModuleType = builtinModuleExts.some(ext => specifier.endsWith('.' + ext)) let routePath = isBuiltinModuleType ? toPagePath(specifier) : util.trimSuffix(specifier, '/pages') let isIndex: boolean | undefined = undefined
if (!isBuiltinModuleType) { for (const { test, resolve } of this.#resolverListeners) { if (test.test(specifier)) { const { specifier: _specifier, asPage } = resolve(specifier) if (asPage) { const { path: pagePath, isIndex: _isIndex } = asPage if (util.isFilledString(pagePath)) { routePath = pagePath if (_specifier) { specifier = _specifier } if (_isIndex) { isIndex = true } break } } } } } else if (routePath !== '/') { for (const ext of builtinModuleExts) { if (specifier.endsWith(`/index.${ext}`)) { isIndex = true break } } }
return [routePath, specifier, isIndex] }
async importModule<T = any>(module: Module): Promise<T> { const path = join(this.#buildDir, module.jsFile) const hash = this.computeModuleHash(module) if (await existsFile(path)) { return await import(`file://${path}#${(hash).slice(0, 6)}`) } throw new Error(`import ${module.specifier}: file not found: ${path}`) }
async getModuleJS(module: Module, injectHMRCode = false): Promise<Uint8Array | null> { const { specifier, jsFile, jsBuffer } = module if (!jsBuffer) { const cacheFile = join(this.#buildDir, jsFile) if (await existsFile(cacheFile)) { module.jsBuffer = await Deno.readFile(cacheFile) log.debug(`load '${jsFile}'` + dim(' • ' + util.formatBytes(module.jsBuffer.length))) } }
if (!module.jsBuffer) { return null }
if (!injectHMRCode || !this.isHMRable(specifier)) { return module.jsBuffer }
let code = decoder.decode(module.jsBuffer) if (module.denoHooks?.length || module.ssrPropsFn || module.ssgPathsFn) { if ('csrCode' in module) { code = (module as any).csrCode } else { [code] = util.splitBy(code, '\n//# sourceMappingURL=', true) const { code: csrCode } = await stripSsrCode(specifier, code, { sourceMap: true, swcOptions: { sourceType: SourceType.JS } }) // cache csr code Object.assign(module, { csrCode }) code = csrCode // todo: merge source map } } for (const { test, transform } of this.#transformListeners) { if (test === 'hmr') { const { jsBuffer, ready, ...rest } = module const ret = await transform({ module: structuredClone(rest), code }) if (util.isFilledString(ret?.code)) { code = ret!.code } // todo: merge source map } } return encoder.encode([ `import.meta.hot = $createHotContext(${JSON.stringify(specifier)});`, '', code, '', 'import.meta.hot.accept();' ].join('\n')) }
/** fetch module source by the specifier. */ async fetchModule(specifier: string): Promise<{ content: Uint8Array, contentType: string | null }> { if (!util.isLikelyHttpURL(specifier)) { const filepath = join(this.#workingDir, this.#config.srcDir, util.trimPrefix(specifier, 'file://')) if (await existsFile(filepath)) { const content = await Deno.readFile(filepath) return { content, contentType: getContentType(filepath) } } else { return Promise.reject(new Error(`No such file: ${util.trimPrefix(filepath, this.#workingDir + '/')}`)) } }
// append `dev` query for development mode if (this.isDev && specifier.startsWith('https://esm.sh/')) { const u = new URL(specifier) if (!u.searchParams.has('dev')) { u.searchParams.set('dev', '') u.search = u.search.replace('dev=', 'dev') specifier = u.toString() } }
return await cache(specifier, { forceRefresh: this.#reloading, retryTimes: 10 }) }
resolveImport({ jsFile, sourceHash }: Module, importer: string, bundleMode?: boolean, timeStamp?: boolean): string { const relPath = toRelativePath( dirname(toLocalPath(importer)), jsFile ) if (bundleMode) { return util.trimSuffix(relPath, '.js') + '.bundling.js' } let hash = '#' + sourceHash.slice(0, 8) if (timeStamp) { hash += '-' + Date.now() } return relPath + hash }
async resolveModuleSource(specifier: string, data?: any): Promise<ModuleSource> { let sourceCode: string = '' let sourceType: SourceType = SourceType.Unknown let sourceMap: string | null = null let loader = this.#loadListeners.find(l => l.test.test(specifier))
if (loader) { const { code, type = 'js', map } = await loader.load({ specifier, data }) switch (type) { case 'js': sourceType = SourceType.JS break case 'jsx': sourceType = SourceType.JSX break case 'ts': sourceType = SourceType.TS break case 'tsx': sourceType = SourceType.TSX break case 'css': sourceType = SourceType.CSS break } sourceCode = code sourceMap = map || null } else { const source = await this.fetchModule(specifier) sourceType = getSourceType(specifier, source.contentType || undefined) if (sourceType !== SourceType.Unknown) { sourceCode = decoder.decode(source.content) } }
return { code: sourceCode, type: sourceType, map: sourceMap ? sourceMap : undefined } }
/** compile the module by given specifier */ async compile(specifier: string, options: CompileOptions = {}) { const [module, source] = await this.initModule(specifier, options) if (!module.external) { await this.transpileModule(module, source, options.ignoreDeps) } return module }
/** init the module by given specifier, don't transpile the code when the returned `source` is equal to null */ private async initModule( specifier: string, { source: customSource, forceRefresh, httpExternal, virtual }: CompileOptions = {} ): Promise<[Module, ModuleSource | null]> { let external = false let data: any = null
if (customSource === undefined) { for (const { test, resolve } of this.#resolverListeners) { if (test.test(specifier)) { const ret = resolve(specifier) if (ret.specifier) { specifier = ret.specifier } external = Boolean(ret.external) data = ret.data break } } }
if (external) { return [{ specifier, deps: [], external, sourceHash: '', jsFile: '', ready: Promise.resolve() }, null] }
let mod = this.#modules.get(specifier) if (mod && !forceRefresh && !(!httpExternal && mod.httpExternal)) { await mod.ready return [mod, null] }
const isRemote = util.isLikelyHttpURL(specifier) && !isLocalUrl(specifier) const localPath = toLocalPath(specifier) const jsFile = trimBuiltinModuleExts(localPath) + '.js' const cacheFile = join(this.#buildDir, jsFile) const metaFile = cacheFile.slice(0, -3) + '.meta.json' const isNew = !mod
let defer = (err?: Error) => { } let source: ModuleSource | null = null mod = { specifier, deps: [], sourceHash: '', httpExternal, jsFile, ready: new Promise((resolve) => { defer = (err?: Error) => { if (err) { if (isNew) { this.#modules.delete(specifier) } log.error(err.message) // todo: send error to client } resolve() } }) }
this.#modules.set(specifier, mod) if (trimBuiltinModuleExts(specifier) === '/app') { this.#appModule = mod }
if (!forceRefresh && await existsFile(metaFile)) { try { const meta = JSON.parse(await Deno.readTextFile(metaFile)) if (meta.specifier === specifier && util.isFilledString(meta.sourceHash) && util.isArray(meta.deps)) { Object.assign(mod, meta) } else { log.warn(`removing invalid metadata of '${basename(specifier)}'`) Deno.remove(metaFile) } } catch (e) { } }
if (virtual) { defer() return [mod, null] }
if (!isRemote || this.#reloading || mod.sourceHash === '' || !await existsFile(cacheFile)) { try { const src = customSource || await this.resolveModuleSource(specifier, data) const sourceHash = computeHash(src.code) if (mod.sourceHash === '' || mod.sourceHash !== sourceHash) { mod.sourceHash = sourceHash source = src } } catch (err) { defer(err) return [mod, null] } }
defer() return [mod, source] }
private async transpileModule( module: Module, source: ModuleSource | null, ignoreDeps = false, __tracing: Set<string> = new Set() ): Promise<void> { const { specifier, jsFile, httpExternal } = module
// ensure the module only be transppiled once in current compilation context, // to avoid dead-loop caused by cicular imports if (__tracing.has(specifier)) { return } __tracing.add(specifier)
if (source) { if (source.type === SourceType.Unknown) { log.error(`Unsupported module '${specifier}'`) return }
const ms = new Measure()
if (source.type === SourceType.CSS) { const { code, map } = await cssLoader({ specifier, data: source.code }, this) source.code = code source.map = map source.type = SourceType.JS module.isStyle = true }
let ret: TransformResult // use `fastTransform` when the module is remote non-jsx module in `development` mode if (this.isDev && util.isLikelyHttpURL(specifier) && source.type !== SourceType.JSX && source.type !== SourceType.TSX) { ret = await fastTransform(specifier, source, { react: this.#config.react }) } else { ret = await transform(specifier, source.code, { ...this.commonCompilerOptions, sourceMap: this.isDev, swcOptions: { sourceType: source.type }, httpExternal }) }
const { code, deps = [], denoHooks, ssrPropsFn, ssgPathsFn, starExports, jsxStaticClassNames, map } = ret
let jsCode = code let sourceMap = map
// in production(bundle) mode we need to replace the star export with names if (!this.isDev && starExports && starExports.length > 0) { for (let index = 0; index < starExports.length; index++) { const exportSpecifier = starExports[index] const names = await this.parseModuleExportNames(exportSpecifier) jsCode = jsCode.replace( `export * from "[${exportSpecifier}]:`, `export {${names.filter(name => name !== 'default').join(',')}} from "` ) } }
// revert external imports if (deps.length > 0 && this.#resolverListeners.length > 0) { deps.forEach(({ specifier }) => { if (specifier !== module.specifier && util.isLikelyHttpURL(specifier)) { let external = false for (const { test, resolve } of this.#resolverListeners) { if (test.test(specifier)) { const ret = resolve(specifier) if (ret.specifier) { specifier = ret.specifier } external = Boolean(ret.external) break } } if (external) { const importSpecifier = toRelativePath( dirname(toLocalPath(module.specifier)), toLocalPath(specifier) ) jsCode.replaceAll(`"${importSpecifier}"`, `"${specifier}"`) } } }) }
Object.assign(module, { deps, ssrPropsFn, ssgPathsFn, jsxStaticClassNames }) if (util.isFilledArray(denoHooks)) { module.denoHooks = denoHooks.map(id => util.trimPrefix(id, 'useDeno-')) if (!this.#config.ssr) { log.error(`'useDeno' hook in SPA mode is illegal: ${specifier}`) } }
let extraDeps: DependencyDescriptor[] = [] for (const { test, transform } of this.#transformListeners) { if (test instanceof RegExp && test.test(specifier)) { const { jsBuffer, ready, ...rest } = module const ret = await transform({ module: { ...structuredClone(rest) }, code: jsCode, map: sourceMap }) if (util.isFilledString(ret?.code)) { jsCode = ret!.code } if (util.isFilledString(ret?.map)) { sourceMap = ret!.map } if (Array.isArray(ret?.extraDeps)) { extraDeps.push(...ret!.extraDeps) } } }
// add source mapping url if (sourceMap) { jsCode += `\n//# sourceMappingURL=${basename(jsFile)}.map` }
module.jsBuffer = encoder.encode(jsCode) module.deps = deps.filter(({ specifier }) => specifier !== module.specifier).map(({ specifier, resolved, isDynamic }) => { const dep: DependencyDescriptor = { specifier, } if (isDynamic) { dep.isDynamic = true } if (specifier.startsWith('/')) { const mark = encoder.encode(resolved) const idx = indexOf(module.jsBuffer!, mark) if (idx > 0) { dep.hashLoc = idx + mark.length - 6 } } return dep }).concat(extraDeps)
ms.stop(`transpile '${specifier}'`)
await this.cacheModule(module, sourceMap) }
if (module.deps.length > 0) { let fsync = false await Promise.all(module.deps.map(async ({ specifier, hashLoc, virtual }) => { let depModule: Module | null = null if (ignoreDeps || virtual) { depModule = this.getModule(specifier) if (depModule == null && virtual) { const [mod] = await this.initModule(specifier, { virtual: true }) depModule = mod } } if (depModule === null) { const [mod, src] = await this.initModule(specifier, { httpExternal }) if (!mod.external) { await this.transpileModule(mod, src, false, __tracing) } depModule = mod } if (depModule) { if (hashLoc !== undefined) { const hash = this.computeModuleHash(depModule) if (await this.replaceDepHash(module, hashLoc, hash)) { fsync = true } } } else { log.error(`transpile '${module.specifier}': missing dependency module '${specifier}'`) } })) if (fsync) { await this.cacheModule(module) } } }
/** apply compilation side-effect caused by updating dependency graph. */ private async applyCompilationSideEffect(by: Module, callback: (mod: Module) => void, __tracing = new Set<string>()) { if (__tracing.has(by.specifier)) { return } __tracing.add(by.specifier)
let hash: string | null = null for (const mod of this.#modules.values()) { const { deps } = mod if (deps.length > 0) { let fsync = false for (const dep of deps) { const { specifier, hashLoc } = dep if (specifier === by.specifier && hashLoc !== undefined) { if (hash == null) { hash = this.computeModuleHash(by) } if (await this.replaceDepHash(mod, hashLoc, hash)) { fsync = true } } } if (fsync) { callback(mod) this.applyCompilationSideEffect(mod, callback) this.cacheModule(mod) } } } }
/** replace dep hash in the `jsBuffer` and remove `csrCode` cache if it exits */ private async replaceDepHash(module: Module, hashLoc: number, hash: string) { const hashData = encoder.encode(hash.substr(0, 6)) const jsBuffer = await this.getModuleJS(module) if (jsBuffer && !equals(hashData, jsBuffer.slice(hashLoc, hashLoc + 6))) { copy(hashData, jsBuffer, hashLoc) if ('csrCode' in module) { Reflect.deleteProperty(module, 'csrCode') } return true } return false }
private clearSSRCache(specifier: string) { if (trimBuiltinModuleExts(specifier) === '/app') { this.#renderer.clearCache() } else if (this.isPageModule(specifier)) { const [routePath] = this.createRouteUpdate(specifier) this.#renderer.clearCache(routePath) } }
private async cacheModule(module: Module, sourceMap?: string) { const { jsBuffer, jsFile, ready, ...rest } = module if (jsBuffer) { const cacheFile = join(this.#buildDir, jsFile) const metaFile = cacheFile.slice(0, -3) + '.meta.json' await ensureDir(dirname(cacheFile)) await Promise.all([ Deno.writeFile(cacheFile, jsBuffer), Deno.writeTextFile(metaFile, JSON.stringify({ ...rest }, undefined, 2)), sourceMap ? Deno.writeTextFile(`${cacheFile}.map`, sourceMap) : Promise.resolve(), lazyRemove(cacheFile.slice(0, -3) + '.bundling.js'), ]) } }
/** create bundled chunks for production. */ private async bundle() { const entries = this.analyze() await this.#bundler.bundle(entries) }
/** render all pages in routing. */ private async ssg() { const { ssr } = this.#config const outputDir = join(this.#workingDir, this.#config.build.outputDir)
if (ssr === false) { const html = await this.createSPAIndexHtml() await ensureTextFile(join(outputDir, 'index.html'), html) await ensureTextFile(join(outputDir, '404.html'), html) // todo: 500 page return }
// lookup pages const paths: Set<{ pathname: string, search?: string }> = new Set(this.#pageRouting.paths.map(pathname => ({ pathname }))) const locales = this.#config.i18n.locales.filter(l => l !== this.#config.i18n.defaultLocale) for (const specifier of this.#modules.keys()) { const module = this.#modules.get(specifier)! if (module.ssgPathsFn) { const { ssr } = await this.importModule(module) let ssrPaths = ssr.paths if (util.isFunction(ssrPaths)) { ssrPaths = ssrPaths() if (ssrPaths instanceof Promise) { ssrPaths = await ssrPaths } } if (util.isFilledArray(ssrPaths)) { ssrPaths.forEach(path => { if (util.isFilledString(path)) { const parts = path.split('?') const pathname = util.cleanPath(parts.shift()!) const search = parts.length > 0 ? '?' + (new URLSearchParams('?' + parts.join('?'))).toString() : undefined const [router, nestedModules] = this.#pageRouting.createRouter({ pathname, search }) if (router.routePath !== '' && nestedModules.pop() === specifier) { paths.add({ pathname, search }) } else { log.warn(`Invalid SSG path '${path}'`) } } }) } } }
// render route pages await Promise.all(Array.from(paths).map(loc => ([loc, ...locales.map(locale => ({ ...loc, pathname: locale + loc.pathname }))])).flat().map(async ({ pathname, search }) => { if (this.isSSRable(pathname)) { const [router, nestedModules] = this.#pageRouting.createRouter({ pathname, search }) if (router.routePath !== '') { const href = router.toString() const [html, data] = await this.#renderPage(router, nestedModules) await ensureTextFile(join(outputDir, pathname, 'index.html' + (search || '')), html) if (data) { const dataFile = join( outputDir, `_aleph/data/${util.btoaUrl(href)}.json` ) await ensureTextFile(dataFile, JSON.stringify(data)) } log.debug('SSR', href, dim('• ' + util.formatBytes(html.length))) } } }))
// render 404 page { const [router, nestedModules] = this.#pageRouting.createRouter({ pathname: '/404' }) if (nestedModules.length > 0) { await this.compile(nestedModules[0]) } const [html] = await this.#renderPage(router, nestedModules.slice(0, 1)) await ensureTextFile(join(outputDir, '404.html'), html) } }
/** check the module whether it is page. */ private isPageModule(specifier: string): boolean { if (!specifier.startsWith('/pages/')) { return false } if (builtinModuleExts.some(ext => specifier.endsWith('.' + ext))) { return true }
return this.#resolverListeners.some(({ test, resolve }) => test.test(specifier) && !!resolve(specifier).asPage) }
/** check the module whether it is hmrable. */ private isHMRable(specifier: string): boolean { if (util.isLikelyHttpURL(specifier)) { return false }
for (const ext of builtinModuleExts) { if (specifier.endsWith('.' + ext)) { return ( specifier.startsWith('/pages/') || specifier.startsWith('/components/') || util.trimSuffix(specifier, '.' + ext) === '/app' ) } }
const mod = this.#modules.get(specifier) if (mod && mod.isStyle) { return true }
return this.#resolverListeners.some(({ test, resolve }) => ( test.test(specifier) && this.acceptHMR(resolve(specifier)) )) }
/** check the page whether it supports SSR. */ private isSSRable(pathname: string): boolean { const { ssr } = this.#config if (util.isPlainObject(ssr)) { if (ssr.include) { for (let r of ssr.include) { if (!r.test(pathname)) { return false } } } if (ssr.exclude) { for (let r of ssr.exclude) { if (r.test(pathname)) { return false } } } return true } return ssr }
private acceptHMR(ret: ResolveResult): boolean { return ret.acceptHMR || !!ret.asPage }
/** lookup app deps recurively. */ lookupDeps( specifier: string, callback: (dep: DependencyDescriptor) => false | void, __tracing: Set<string> = new Set() ) { const mod = this.getModule(specifier) if (mod === null) { return } if (__tracing.has(specifier)) { return } __tracing.add(specifier) for (const dep of mod.deps) { if (callback(dep) === false) { return false } } for (const { specifier } of mod.deps) { if ((this.lookupDeps(specifier, callback, __tracing)) === false) { return false } } }}