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

Commit 0f3d085

Browse files
author
Je
committed
feat: add plugin system
1 parent 34bbc79 commit 0f3d085

File tree

2 files changed

+87
-72
lines changed

2 files changed

+87
-72
lines changed

project.ts

Lines changed: 86 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { EventEmitter } from './events.ts'
77
import { createHtml } from './html.ts'
88
import log from './log.ts'
99
import { getPagePath, RouteModule, Routing } from './routing.ts'
10-
import { colors, ensureDir, path, ServerRequest, Sha1, walk } from './std.ts'
10+
import { colors, ensureDir, fromStreamReader, path, ServerRequest, Sha1, walk } from './std.ts'
1111
import { compile } from './tsc/compile.ts'
1212
import type { AlephEnv, APIHandler, Config, RouterURL } from './types.ts'
1313
import util, { existsDirSync, existsFileSync, hashShort, MB, reHashJs, reHttp, reLocaleID, reMDExt, reModuleExt, reStyleModuleExt } from './util.ts'
@@ -21,13 +21,20 @@ interface Module {
2121
isRemote: boolean
2222
sourceFilePath: string
2323
sourceHash: string
24-
deps: { url: string, hash: string, async?: boolean }[]
24+
deps: ModuleDep[]
2525
jsFile: string
2626
jsContent: string
2727
jsSourceMap: string
2828
hash: string
2929
}
3030

31+
interface ModuleDep {
32+
url: string
33+
hash: string
34+
async?: boolean
35+
external?: boolean
36+
}
37+
3138
interface Renderer {
3239
renderPage: Function
3340
renderHead: Function
@@ -167,9 +174,11 @@ export class Project {
167174
modId = id + '.js'
168175
}
169176
}
177+
if (!this.#modules.has(modId) && modId.endsWith('.js')) {
178+
modId = util.trimSuffix(modId, '.js')
179+
}
170180
if (!this.#modules.has(modId)) {
171181
log.warn(`can't get the module by path '${pathname}(${modId})'`)
172-
console.log(Array.from(this.#modules.keys()))
173182
}
174183
return this.getModule(modId)
175184
}
@@ -843,7 +852,7 @@ export class Project {
843852
}
844853

845854
// todo: force recompile remote modules which URL don't specify version
846-
private async _compile(url: string, options?: { sourceCode?: string, forceCompile?: boolean, forceTarget?: string }) {
855+
private async _compile(url: string, options?: { forceCompile?: boolean, forceTarget?: string }) {
847856
const mod = this._moduleFromURL(url)
848857
if (this.#modules.has(mod.id) && !options?.forceCompile) {
849858
return this.#modules.get(mod.id)!
@@ -870,18 +879,11 @@ export class Project {
870879
}
871880
}
872881

873-
let sourceContent = ''
882+
let sourceContent = new Uint8Array()
874883
let shouldCompile = false
875884
let fsync = false
876885

877-
if (options?.sourceCode) {
878-
const sourceHash = getHash(options.sourceCode, true)
879-
if (mod.sourceHash === '' || mod.sourceHash !== sourceHash) {
880-
mod.sourceHash = sourceHash
881-
sourceContent = options.sourceCode
882-
shouldCompile = true
883-
}
884-
} else if (mod.isRemote) {
886+
if (mod.isRemote) {
885887
let dlUrl = url
886888
const { imports } = this.importMap
887889
for (const importPath in imports) {
@@ -920,8 +922,8 @@ export class Project {
920922
if (resp.status != 200) {
921923
throw new Error(`Download ${url}: ${resp.status} - ${resp.statusText}`)
922924
}
925+
sourceContent = await Deno.readAll(fromStreamReader(resp.body!.getReader()))
923926
mod.sourceHash = getHash(sourceContent)
924-
sourceContent = await resp.text()
925927
shouldCompile = true
926928
} catch (err) {
927929
throw new Error(`Download ${url}: ${err.message}`)
@@ -932,11 +934,10 @@ export class Project {
932934
if (resp.status != 200) {
933935
throw new Error(`${resp.status} - ${resp.statusText}`)
934936
}
935-
const text = await resp.text()
936-
const sourceHash = getHash(text, true)
937-
if (mod.sourceHash !== sourceHash) {
937+
sourceContent = await Deno.readAll(fromStreamReader(resp.body!.getReader()))
938+
const sourceHash = getHash(sourceContent, true)
939+
if (mod.sourceHash === '' || mod.sourceHash !== sourceHash) {
938940
mod.sourceHash = sourceHash
939-
sourceContent = text
940941
shouldCompile = true
941942
}
942943
} catch (err) {
@@ -945,35 +946,32 @@ export class Project {
945946
}
946947
} else {
947948
const filepath = path.join(this.srcDir, url)
948-
try {
949-
const fileinfo = await Deno.stat(filepath)
950-
// 10mb limit
951-
if (fileinfo.size > 10 * (1 << 20)) {
952-
throw new Error(`ignored module '${url}': too large(${(fileinfo.size / (1 << 20)).toFixed(2)}mb)`)
953-
}
954-
} catch (err) {
955-
if (err instanceof Deno.errors.NotFound) {
956-
throw new Error(`module '${url}' not found`)
957-
}
958-
}
959-
const text = await Deno.readTextFile(filepath)
960-
const sourceHash = getHash(text, true)
949+
sourceContent = await Deno.readFile(filepath)
950+
const sourceHash = getHash(sourceContent, true)
961951
if (mod.sourceHash === '' || mod.sourceHash !== sourceHash) {
962952
mod.sourceHash = sourceHash
963-
sourceContent = text
964953
shouldCompile = true
965954
}
966955
}
967956

968957
// compile source code
969958
if (shouldCompile) {
970959
const t = performance.now()
960+
let sourceCode = (new TextDecoder).decode(sourceContent)
961+
for (const plugin of this.config.plugins) {
962+
if (plugin.test.test(url) && plugin.transform) {
963+
const { code, loader = 'js' } = await plugin.transform(sourceContent)
964+
sourceCode = code
965+
mod.loader = loader
966+
break
967+
}
968+
}
971969
mod.deps = []
972970
if (mod.loader === 'css') {
973-
let css: string = sourceContent
971+
let css: string = sourceCode
974972
if (mod.id.endsWith('.less')) {
975973
try {
976-
const output = await less.render(sourceContent || '/* empty content */')
974+
const output = await less.render(sourceCode || '/* empty content */')
977975
css = output.css
978976
} catch (error) {
979977
throw new Error(`less: ${error}`);
@@ -1004,7 +1002,7 @@ export class Project {
10041002
mod.jsSourceMap = '' // todo: sourceMap
10051003
mod.hash = getHash(css)
10061004
} else if (mod.loader === 'markdown') {
1007-
const { __content, ...props } = safeLoadFront(sourceContent)
1005+
const { __content, ...props } = safeLoadFront(sourceCode)
10081006
const html = marked.parse(__content)
10091007
mod.jsContent = [
10101008
this.isDev && `const _s = $RefreshSig$();`,
@@ -1051,7 +1049,7 @@ export class Project {
10511049
return sig
10521050
}
10531051
}
1054-
const { diagnostics, outputText, sourceMapText } = compile(mod.sourceFilePath, sourceContent, compileOptions)
1052+
const { diagnostics, outputText, sourceMapText } = compile(mod.sourceFilePath, sourceCode, compileOptions)
10551053
if (diagnostics && diagnostics.length > 0) {
10561054
throw new Error(`compile ${url}: ${diagnostics.map(d => d.messageText).join('\n')}`)
10571055
}
@@ -1097,7 +1095,7 @@ export class Project {
10971095
this.#modules.set(mod.id, mod)
10981096

10991097
// compile deps
1100-
for (const dep of mod.deps.filter(({ url }) => !url.startsWith('#useDeno.'))) {
1098+
for (const dep of mod.deps.filter(({ url, external }) => !url.startsWith('#useDeno.') && !external)) {
11011099
const depMod = await this._compile(dep.url)
11021100
if (dep.hash !== depMod.hash) {
11031101
dep.hash = depMod.hash
@@ -1184,51 +1182,68 @@ export class Project {
11841182
})
11851183
}
11861184

1187-
private _rewriteImportPath(mod: Module, importPath: string, async?: boolean): string {
1185+
private _rewriteImportPath(importer: Module, importPath: string, async?: boolean): string {
11881186
let rewrittenPath: string
1189-
if (importPath in this.importMap.imports) {
1190-
importPath = this.importMap.imports[importPath]
1191-
}
1192-
if (reHttp.test(importPath)) {
1193-
if (mod.isRemote) {
1194-
rewrittenPath = relativePath(
1195-
path.dirname(mod.url.replace(reHttp, '/-/').replace(/:(\d+)/, '/$1')),
1196-
renameImportUrl(importPath)
1197-
)
1198-
} else {
1199-
rewrittenPath = relativePath(
1200-
path.dirname(mod.url),
1201-
renameImportUrl(importPath)
1202-
)
1187+
let resolveRet: { path: string, external?: boolean } | null = null
1188+
for (const plugin of this.config.plugins) {
1189+
if (plugin.test.test(importPath) && plugin.resolve) {
1190+
resolveRet = plugin.resolve(importPath)
1191+
break
12031192
}
1193+
}
1194+
1195+
// when a plugin resolver returns an external path, do NOT rewrite the `importPath`
1196+
if (resolveRet && resolveRet.external) {
1197+
rewrittenPath = resolveRet.path
12041198
} else {
1205-
if (mod.isRemote) {
1206-
const modUrl = new URL(mod.url)
1207-
let pathname = importPath
1208-
if (!pathname.startsWith('/')) {
1209-
pathname = path.join(path.dirname(modUrl.pathname), importPath)
1199+
if (resolveRet) {
1200+
importPath = resolveRet.path
1201+
}
1202+
if (importPath in this.importMap.imports) {
1203+
importPath = this.importMap.imports[importPath]
1204+
}
1205+
if (reHttp.test(importPath)) {
1206+
if (importer.isRemote) {
1207+
rewrittenPath = relativePath(
1208+
path.dirname(importer.url.replace(reHttp, '/-/').replace(/:(\d+)/, '/$1')),
1209+
renameImportUrl(importPath)
1210+
)
1211+
} else {
1212+
rewrittenPath = relativePath(
1213+
path.dirname(importer.url),
1214+
renameImportUrl(importPath)
1215+
)
12101216
}
1211-
const importUrl = new URL(modUrl.protocol + '//' + modUrl.host + pathname)
1212-
rewrittenPath = relativePath(
1213-
path.dirname(mod.sourceFilePath),
1214-
renameImportUrl(importUrl.toString())
1215-
)
12161217
} else {
1217-
rewrittenPath = importPath.replace(reModuleExt, '') + '.' + 'x'.repeat(hashShort)
1218+
if (importer.isRemote) {
1219+
const modUrl = new URL(importer.url)
1220+
let pathname = importPath
1221+
if (!pathname.startsWith('/')) {
1222+
pathname = path.join(path.dirname(modUrl.pathname), importPath)
1223+
}
1224+
const importUrl = new URL(modUrl.protocol + '//' + modUrl.host + pathname)
1225+
rewrittenPath = relativePath(
1226+
path.dirname(importer.sourceFilePath),
1227+
renameImportUrl(importUrl.toString())
1228+
)
1229+
} else {
1230+
rewrittenPath = importPath.replace(reModuleExt, '') + '.' + 'x'.repeat(hashShort)
1231+
}
12181232
}
12191233
}
1234+
12201235
if (reHttp.test(importPath)) {
1221-
mod.deps.push({ url: importPath, hash: '', async })
1236+
importer.deps.push({ url: importPath, hash: '', async, external: resolveRet?.external })
12221237
} else {
1223-
if (mod.isRemote) {
1224-
const sourceUrl = new URL(mod.url)
1238+
if (importer.isRemote) {
1239+
const sourceUrl = new URL(importer.url)
12251240
let pathname = importPath
12261241
if (!pathname.startsWith('/')) {
12271242
pathname = path.join(path.dirname(sourceUrl.pathname), importPath)
12281243
}
1229-
mod.deps.push({ url: sourceUrl.protocol + '//' + sourceUrl.host + pathname, hash: '', async })
1244+
importer.deps.push({ url: sourceUrl.protocol + '//' + sourceUrl.host + pathname, hash: '', async, external: resolveRet?.external })
12301245
} else {
1231-
mod.deps.push({ url: path.join(path.dirname(mod.url), importPath), hash: '', async })
1246+
importer.deps.push({ url: path.join(path.dirname(importer.url), importPath), hash: '', async, external: resolveRet?.external })
12321247
}
12331248
}
12341249

@@ -1263,8 +1278,8 @@ export class Project {
12631278
host: 'localhost',
12641279
hostname: 'localhost',
12651280
port: '',
1266-
href: 'https://esm.sh' + url.pathname + url.query.toString(),
1267-
origin: 'https://esm.sh',
1281+
href: 'https://localhost' + url.pathname + url.query.toString(),
1282+
origin: 'https://localhost',
12681283
pathname: url.pathname,
12691284
search: url.query.toString(),
12701285
hash: '',
@@ -1449,7 +1464,7 @@ function renameImportUrl(importUrl: string): string {
14491464
return pathname + ext
14501465
}
14511466

1452-
function getHash(content: string, checkVersion = false) {
1467+
function getHash(content: string | Uint8Array, checkVersion = false) {
14531468
const sha1 = new Sha1()
14541469
sha1.update(content)
14551470
if (checkVersion) {

types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface SSROptions {
3333
export interface Plugin {
3434
test: RegExp
3535
resolve?(path: string): { path: string, external?: boolean }
36-
transform?(path: string): { code: string, sourceMap?: string, loader?: 'js' | 'css' | 'text' }
36+
transform?(content: Uint8Array): Promise<{ code: string, sourceMap?: string, loader?: 'js' | 'css' | 'markdown' }>
3737
}
3838

3939
/**

0 commit comments

Comments
 (0)