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

Commit ed5e56c

Browse files
committed
feat(plugin): add resolve method of loader
1 parent ed52652 commit ed5e56c

File tree

3 files changed

+136
-102
lines changed

3 files changed

+136
-102
lines changed

server/app.ts

Lines changed: 122 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -558,89 +558,23 @@ export class Application implements ServerApplication {
558558
]
559559
}
560560

561-
/** fetch module content */
562-
async fetchModule(url: string): Promise<{ content: Uint8Array, contentType: string | null }> {
563-
if (!util.isLikelyHttpURL(url)) {
564-
const filepath = path.join(this.srcDir, util.trimPrefix(url, 'file://'))
565-
const content = await Deno.readFile(filepath)
566-
return { content, contentType: null }
567-
}
568-
569-
const u = new URL(url)
570-
if (url.startsWith('https://esm.sh/')) {
571-
if (this.isDev && !u.searchParams.has('dev')) {
572-
u.searchParams.set('dev', '')
573-
u.search = u.search.replace('dev=', 'dev')
574-
}
575-
}
576-
577-
const { protocol, hostname, port, pathname, search } = u
578-
const versioned = reFullVersion.test(pathname)
579-
const reload = this.#reloading || !versioned
580-
const isLocalhost = url.startsWith('http://localhost:')
581-
const cacheDir = path.join(
582-
await getDenoDir(),
583-
'deps',
584-
util.trimSuffix(protocol, ':'),
585-
hostname + (port ? '_PORT' + port : '')
586-
)
587-
const hash = createHash('sha256').update(pathname + search).toString()
588-
const contentFile = path.join(cacheDir, hash)
589-
const metaFile = path.join(cacheDir, hash + '.metadata.json')
590-
591-
if (!reload && !isLocalhost && existsFileSync(contentFile) && existsFileSync(metaFile)) {
592-
const [content, meta] = await Promise.all([
593-
Deno.readFile(contentFile),
594-
Deno.readTextFile(metaFile),
595-
])
596-
try {
597-
const { headers } = JSON.parse(meta)
598-
return {
599-
content,
600-
contentType: headers['content-type'] || null
601-
}
602-
} catch (e) { }
561+
async resolveModule(url: string) {
562+
const { content, contentType } = await this.fetchModule(url)
563+
const source = await this.precompile(url, content, contentType)
564+
if (source === null) {
565+
throw new Error(`Unsupported module '${url}'`)
603566
}
567+
return source
568+
}
604569

605-
// download dep when deno cache failed
606-
let err = new Error('Unknown')
607-
for (let i = 0; i < 15; i++) {
608-
if (i === 0) {
609-
if (!isLocalhost) {
610-
log.info('Download', url)
611-
}
612-
} else {
613-
log.debug('Download error:', err)
614-
log.warn(`Download ${url} failed, retrying...`)
615-
}
616-
try {
617-
const resp = await fetch(u.toString())
618-
if (resp.status >= 400) {
619-
return Promise.reject(new Error(resp.statusText))
620-
}
621-
const buffer = await resp.arrayBuffer()
622-
const content = await Deno.readAll(new Deno.Buffer(buffer))
623-
if (!isLocalhost) {
624-
await ensureDir(cacheDir)
625-
Deno.writeFile(contentFile, content)
626-
Deno.writeTextFile(metaFile, JSON.stringify({
627-
headers: Array.from(resp.headers.entries()).reduce((m, [k, v]) => {
628-
m[k] = v
629-
return m
630-
}, {} as Record<string, string>),
631-
url
632-
}, undefined, 2))
633-
}
634-
return {
635-
content,
636-
contentType: resp.headers.get('content-type')
637-
}
638-
} catch (e) {
639-
err = e
640-
}
570+
/** default compiler options */
571+
private get defaultCompileOptions(): TransformOptions {
572+
return {
573+
importMap: this.importMap,
574+
alephPkgUri: getAlephPkgUri(),
575+
reactVersion: defaultReactVersion,
576+
isDev: this.isDev,
641577
}
642-
643-
return Promise.reject(err)
644578
}
645579

646580
/** build the application to a static site(SSG) */
@@ -731,17 +665,107 @@ export class Application implements ServerApplication {
731665
return { code, map }
732666
}
733667

734-
/** default compiler options */
735-
private get defaultCompileOptions(): TransformOptions {
736-
return {
737-
importMap: this.importMap,
738-
alephPkgUri: getAlephPkgUri(),
739-
reactVersion: defaultReactVersion,
740-
isDev: this.isDev,
668+
/** fetch module content */
669+
private async fetchModule(url: string): Promise<{ content: Uint8Array, contentType: string | null }> {
670+
for (const plugin of this.config.plugins) {
671+
if (plugin.type === 'loader' && plugin.test.test(url) && plugin.resolve !== undefined) {
672+
const ret = plugin.resolve(url)
673+
let content: Uint8Array
674+
if (ret instanceof Promise) {
675+
content = (await ret).content
676+
} else {
677+
content = ret.content
678+
}
679+
if (content instanceof Uint8Array) {
680+
return { content, contentType: null }
681+
}
682+
}
683+
}
684+
685+
if (!util.isLikelyHttpURL(url)) {
686+
const filepath = path.join(this.srcDir, util.trimPrefix(url, 'file://'))
687+
const content = await Deno.readFile(filepath)
688+
return { content, contentType: null }
689+
}
690+
691+
const u = new URL(url)
692+
if (url.startsWith('https://esm.sh/')) {
693+
if (this.isDev && !u.searchParams.has('dev')) {
694+
u.searchParams.set('dev', '')
695+
u.search = u.search.replace('dev=', 'dev')
696+
}
741697
}
698+
699+
const { protocol, hostname, port, pathname, search } = u
700+
const versioned = reFullVersion.test(pathname)
701+
const reload = this.#reloading || !versioned
702+
const isLocalhost = url.startsWith('http://localhost:')
703+
const cacheDir = path.join(
704+
await getDenoDir(),
705+
'deps',
706+
util.trimSuffix(protocol, ':'),
707+
hostname + (port ? '_PORT' + port : '')
708+
)
709+
const hash = createHash('sha256').update(pathname + search).toString()
710+
const contentFile = path.join(cacheDir, hash)
711+
const metaFile = path.join(cacheDir, hash + '.metadata.json')
712+
713+
if (!reload && !isLocalhost && existsFileSync(contentFile) && existsFileSync(metaFile)) {
714+
const [content, meta] = await Promise.all([
715+
Deno.readFile(contentFile),
716+
Deno.readTextFile(metaFile),
717+
])
718+
try {
719+
const { headers } = JSON.parse(meta)
720+
return {
721+
content,
722+
contentType: headers['content-type'] || null
723+
}
724+
} catch (e) { }
725+
}
726+
727+
// download dep when deno cache failed
728+
let err = new Error('Unknown')
729+
for (let i = 0; i < 15; i++) {
730+
if (i === 0) {
731+
if (!isLocalhost) {
732+
log.info('Download', url)
733+
}
734+
} else {
735+
log.debug('Download error:', err)
736+
log.warn(`Download ${url} failed, retrying...`)
737+
}
738+
try {
739+
const resp = await fetch(u.toString())
740+
if (resp.status >= 400) {
741+
return Promise.reject(new Error(resp.statusText))
742+
}
743+
const buffer = await resp.arrayBuffer()
744+
const content = await Deno.readAll(new Deno.Buffer(buffer))
745+
if (!isLocalhost) {
746+
await ensureDir(cacheDir)
747+
Deno.writeFile(contentFile, content)
748+
Deno.writeTextFile(metaFile, JSON.stringify({
749+
headers: Array.from(resp.headers.entries()).reduce((m, [k, v]) => {
750+
m[k] = v
751+
return m
752+
}, {} as Record<string, string>),
753+
url
754+
}, undefined, 2))
755+
}
756+
return {
757+
content,
758+
contentType: resp.headers.get('content-type')
759+
}
760+
} catch (e) {
761+
err = e
762+
}
763+
}
764+
765+
return Promise.reject(err)
742766
}
743767

744-
async precompile(
768+
private async precompile(
745769
url: string,
746770
sourceContent: Uint8Array,
747771
contentType: string | null
@@ -1009,16 +1033,6 @@ export class Application implements ServerApplication {
10091033
}
10101034
}
10111035

1012-
private replaceDepHash(jsContent: string, dep: DependencyDescriptor) {
1013-
const s = `.js#${dep.url}@`
1014-
return jsContent.split(s).map((p, i) => {
1015-
if (i > 0 && p.charAt(6) === '"') {
1016-
return dep.hash.slice(0, 6) + p.slice(6)
1017-
}
1018-
return p
1019-
}).join(s)
1020-
}
1021-
10221036
/** create bundle chunks for production. */
10231037
private async bundle() {
10241038
const sharedEntryMods = new Set<string>()
@@ -1176,6 +1190,16 @@ export class Application implements ServerApplication {
11761190
return ssr
11771191
}
11781192

1193+
private replaceDepHash(jsContent: string, dep: DependencyDescriptor) {
1194+
const s = `.js#${dep.url}@`
1195+
return jsContent.split(s).map((p, i) => {
1196+
if (i > 0 && p.charAt(6) === '"') {
1197+
return dep.hash.slice(0, 6) + p.slice(6)
1198+
}
1199+
return p
1200+
}).join(s)
1201+
}
1202+
11791203
/** lookup deps recurively. */
11801204
private lookupDeps(
11811205
url: string,

server/bundler.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ECMA, minify as terser } from 'https://esm.sh/[email protected]'
2-
import { transform } from '../compiler/mod.ts'
2+
import { transform, parseExportNames } from '../compiler/mod.ts'
33
import { colors, ensureDir, path } from '../deps.ts'
44
import { trimModuleExt } from '../framework/core/module.ts'
55
import { defaultReactVersion } from '../shared/constants.ts'
@@ -130,14 +130,13 @@ export class Bundler {
130130
return bundlingFile
131131
}
132132

133-
const { content, contentType } = await this.#app.fetchModule(mod.url)
134-
const source = await this.#app.precompile(mod.url, content, contentType)
133+
const source = await this.#app.resolveModule(mod.url)
135134
if (source === null) {
136135
throw new Error(`Unsupported module '${mod.url}'`)
137136
}
138137

139138
const [sourceCode, sourceType] = source
140-
let { code } = await transform(
139+
let { code, bundleStarExports } = await transform(
141140
mod.url,
142141
sourceCode,
143142
{
@@ -154,6 +153,15 @@ export class Bundler {
154153
}
155154
)
156155

156+
if (bundleStarExports && bundleStarExports.length > 0) {
157+
for (let index = 0; index < bundleStarExports.length; index++) {
158+
const url = bundleStarExports[index]
159+
const [sourceCode, sourceType] = await this.#app.resolveModule(url)
160+
const names = await parseExportNames(url, sourceCode, { sourceType })
161+
code = code.replace(`export const $$star_${index}`, `export const {${names.filter(name => name !== 'default').join(',')}}`)
162+
}
163+
}
164+
157165
// compile deps
158166
for (const dep of mod.deps) {
159167
if (!dep.url.startsWith('#') && !external.includes(dep.url)) {

types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export type LoaderPlugin = {
1414
acceptHMR?: boolean
1515
/** `asPage` allows the loaded module as a page. */
1616
asPage?: boolean
17+
/** `resolve` resolves the module content. */
18+
resolve?(url: string): { content: Uint8Array } | Promise<{ content: Uint8Array }>
1719
/** `transform` transforms the source content. */
1820
transform(source: { url: string, content: Uint8Array, map?: Uint8Array }): LoaderTransformResult | Promise<LoaderTransformResult>
1921
}

0 commit comments

Comments
 (0)