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

Commit 2d87682

Browse files
committed
feat: add markdown plugin
1 parent c94b003 commit 2d87682

File tree

11 files changed

+154
-36
lines changed

11 files changed

+154
-36
lines changed

framework/core/hmr.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,15 @@ socket.addEventListener('message', ({ data }: { data?: string }) => {
7070
url,
7171
updateUrl,
7272
pagePath,
73-
isIndexModule,
73+
isIndex,
7474
useDeno,
7575
} = JSON.parse(data)
7676
switch (type) {
7777
case 'add':
7878
events.emit('add-module', {
7979
url,
8080
pagePath,
81-
isIndexModule,
81+
isIndex,
8282
useDeno,
8383
})
8484
break

framework/core/routing.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ export class Routing {
6565
})
6666
}
6767

68-
update(path: string, moduleUrl: string, options: { isIndexModule?: boolean, useDeno?: boolean } = {}) {
69-
const { isIndexModule, ...rest } = options
68+
update(path: string, moduleUrl: string, options: { isIndex?: boolean, useDeno?: boolean } = {}) {
69+
const { isIndex, ...rest } = options
7070
const newRoute: Route = {
71-
path: path === '/' ? path : path + (options.isIndexModule ? '/' : ''),
71+
path: path === '/' ? path : util.trimSuffix(path, '/') + (options.isIndex ? '/' : ''),
7272
module: { url: moduleUrl, ...rest }
7373
}
7474
const dirtyRoutes: Set<Route[]> = new Set()

framework/core/routing_test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Deno.test(`routing`, () => {
2121
routing.update(
2222
'/blog',
2323
'/pages/blog/index.tsx',
24-
{ isIndexModule: true }
24+
{ isIndex: true }
2525
)
2626
routing.update(
2727
'/blog/[slug]',
@@ -30,7 +30,7 @@ Deno.test(`routing`, () => {
3030
routing.update(
3131
'/user',
3232
'/pages/user/index.tsx',
33-
{ isIndexModule: true }
33+
{ isIndex: true }
3434
)
3535
routing.update(
3636
'/user/[...all]',
@@ -63,7 +63,7 @@ Deno.test(`routing`, () => {
6363
routing.update(
6464
'/',
6565
'/pages/index.tsx',
66-
{ isIndexModule: true }
66+
{ isIndex: true }
6767
)
6868

6969
assertEquals(routing.paths, [

framework/react/router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export default function Router({
8282
useEffect(() => {
8383
const isDev = !('__ALEPH' in window)
8484
const { baseURL } = routing
85-
const onAddModule = async (mod: RouteModule & { pagePath?: string, isIndexModule?: boolean }) => {
85+
const onAddModule = async (mod: RouteModule & { pagePath?: string, isIndex?: boolean }) => {
8686
switch (mod.url) {
8787
case '/404.js': {
8888
const { default: Component } = await importModule(baseURL, mod.url, true)

plugins/css.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ export default (options?: Options): LoaderPlugin => {
2929
acceptHMR: true,
3030
async resolve(url: string) {
3131
if (util.isLikelyHttpURL(url)) {
32-
return Promise.resolve(new Uint8Array())
32+
return new Uint8Array()
3333
}
34-
return Deno.readFile(join(Deno.cwd(), url))
34+
return await Deno.readFile(join(Deno.cwd(), url))
3535
},
3636
async transform({ url, content }) {
3737
if (util.isLikelyHttpURL(url)) {

plugins/markdown.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { LoaderPlugin } from '../types.ts'
2+
import marked from 'https://esm.sh/[email protected]'
3+
import { safeLoadFront } from 'https://esm.sh/[email protected]'
4+
import util from '../shared/util.ts'
5+
6+
const decoder = new TextDecoder()
7+
8+
export default (): LoaderPlugin => {
9+
return {
10+
name: 'markdown-loader',
11+
type: 'loader',
12+
test: /\.(md|markdown)$/i,
13+
acceptHMR: true,
14+
asPage: true,
15+
pagePathResolve: (url) => {
16+
let path = util.trimPrefix(url.replace(/\.(md|markdown)$/i, ''), '/pages')
17+
let isIndex = path.endsWith('/index')
18+
if (isIndex) {
19+
path = util.trimSuffix(path, '/index')
20+
if (path === '') {
21+
path = '/'
22+
}
23+
}
24+
return { path, isIndex }
25+
},
26+
transform: ({ content }) => {
27+
const { __content, ...meta } = safeLoadFront(decoder.decode(content))
28+
const html = marked.parse(__content)
29+
30+
return {
31+
code: [
32+
`import React, { useEffect, useRef } from "https://esm.sh/react";`,
33+
`import { redirect } from "https://deno.land/x/aleph/mod.ts";`,
34+
`export default function MarkdownPage() {`,
35+
` const ref = useRef(null);`,
36+
``,
37+
` useEffect(() => {`,
38+
` const anchors = [];`,
39+
` const onClick = e => {`,
40+
` e.preventDefault();`,
41+
` redirect(e.currentTarget.getAttribute("href"));`,
42+
` };`,
43+
` if (ref.current) {`,
44+
` ref.current.querySelectorAll("a").forEach(a => {`,
45+
` const href = a.getAttribute("href");`,
46+
` if (href && !/^[a-z0-9]+:/i.test(href)) {`,
47+
` a.addEventListener("click", onClick, false);`,
48+
` anchors.push(a);`,
49+
` }`,
50+
` });`,
51+
` }`,
52+
` return () => anchors.forEach(a => a.removeEventListener("click", onClick));`,
53+
` }, []);`,
54+
``,
55+
` return React.createElement("div", {`,
56+
` className: "markdown-page",`,
57+
` ref,`,
58+
` dangerouslySetInnerHTML: { __html: ${JSON.stringify(html)} }`,
59+
` });`,
60+
`}`,
61+
`MarkdownPage.meta = ${JSON.stringify(meta, undefined, 2)};`,
62+
].join('\n')
63+
}
64+
}
65+
}
66+
}

plugins/markdown_test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { assertEquals } from 'https://deno.land/[email protected]/testing/asserts.ts'
2+
import markdownLoader from './markdown.ts'
3+
4+
Deno.test('markdown loader', async () => {
5+
const loader = markdownLoader()
6+
const { code } = await loader.transform!({
7+
url: '/test.md',
8+
content: (new TextEncoder).encode([
9+
'---',
10+
'url: https://alephjs.org',
11+
'---',
12+
'',
13+
'# Aleph.js',
14+
'The Full-stack Framework in Deno.'
15+
].join('\n')),
16+
})
17+
assertEquals(loader.test.test('/test.md'), true)
18+
assertEquals(loader.test.test('/test.markdown'), true)
19+
assertEquals(loader.acceptHMR, true)
20+
assertEquals(loader.asPage, true)
21+
assertEquals(loader.pagePathResolve!('/pages/docs/get-started.md'), { path: '/docs/get-started', isIndex: false })
22+
assertEquals(loader.pagePathResolve!('/pages/docs/index.md'), { path: '/docs', isIndex: true })
23+
assertEquals(code.includes('{ __html: "<h1 id=\\"alephjs\\">Aleph.js</h1>\\n<p>The Full-stack Framework in Deno.</p>\\n" }'), true)
24+
assertEquals(code.includes('MarkdownPage.meta = {\n "url": "https://alephjs.org"\n}'), true)
25+
})
26+
27+

plugins/sass.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import type { LoaderPlugin } from '../types.ts'
33

44
type Sass = { renderSync(options: Options): Result }
55

6-
export default (opts?: Options): LoaderPlugin => {
7-
const decoder = new TextDecoder()
6+
const decoder = new TextDecoder()
87

8+
export default (opts?: Options): LoaderPlugin => {
99
let sass: Sass | null = null
1010

1111
return {

server/app.ts

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -274,11 +274,12 @@ export class Application implements ServerApplication {
274274
log.info(type, url)
275275
this.compile(url, { forceCompile: true }).then(mod => {
276276
const hmrable = this.isHMRable(mod.url)
277-
const update = ({ url }: Module) => {
277+
const applyEffect = (url: string) => {
278278
if (trimModuleExt(url) === '/app') {
279279
this.#renderer.clearCache()
280280
} else if (url.startsWith('/pages/')) {
281-
this.#renderer.clearCache(toPagePath(url))
281+
const [pagePath] = this.createRouteUpdate(url)
282+
this.#renderer.clearCache(pagePath)
282283
this.#pageRouting.update(...this.createRouteUpdate(url))
283284
} else if (url.startsWith('/api/')) {
284285
this.#apiRouting.update(...this.createRouteUpdate(url))
@@ -287,12 +288,12 @@ export class Application implements ServerApplication {
287288
if (hmrable) {
288289
let pagePath: string | undefined = undefined
289290
let useDeno: boolean | undefined = undefined
290-
let isIndexModule: boolean | undefined = undefined
291+
let isIndex: boolean | undefined = undefined
291292
if (mod.url.startsWith('/pages/')) {
292293
const [path, _, options] = this.createRouteUpdate(mod.url)
293294
pagePath = path
294295
useDeno = options.useDeno
295-
isIndexModule = options.isIndexModule
296+
isIndex = options.isIndex
296297
} else {
297298
if (['/app', '/404'].includes(trimModuleExt(mod.url))) {
298299
this.lookupDeps(mod.url, dep => {
@@ -305,17 +306,17 @@ export class Application implements ServerApplication {
305306
}
306307
if (type === 'add') {
307308
this.#fsWatchListeners.forEach(e => {
308-
e.emit('add', { url: mod.url, pagePath, isIndexModule, useDeno })
309+
e.emit('add', { url: mod.url, pagePath, isIndex, useDeno })
309310
})
310311
} else {
311312
this.#fsWatchListeners.forEach(e => {
312313
e.emit('modify-' + mod.url, { useDeno })
313314
})
314315
}
315316
}
316-
update(mod)
317+
applyEffect(mod.url)
317318
this.applyCompilationSideEffect(url, (mod) => {
318-
update(mod)
319+
applyEffect(mod.url)
319320
if (!hmrable && this.isHMRable(mod.url)) {
320321
this.#fsWatchListeners.forEach(w => w.emit('modify-' + mod.url))
321322
}
@@ -596,7 +597,7 @@ export class Application implements ServerApplication {
596597
}
597598

598599
/** get ssr html scripts */
599-
getSSRHTMLScripts(pagePath?: string) {
600+
getSSRHTMLScripts(entryFile?: string) {
600601
const { framework } = this.config
601602
const baseUrl = util.trimSuffix(this.config.baseUrl, '/')
602603
const alephPkgPath = getAlephPkgUri().replace('https://', '').replace('http://localhost:', 'http_localhost_')
@@ -623,8 +624,8 @@ export class Application implements ServerApplication {
623624
}
624625
})
625626

626-
if (pagePath) {
627-
preload.push(`${baseUrl}/_aleph/pages/${pagePath.replace(/\/$/, '/index')}.js`)
627+
if (entryFile) {
628+
preload.push(`${baseUrl}/_aleph${entryFile}`)
628629
}
629630

630631
return [
@@ -636,7 +637,7 @@ export class Application implements ServerApplication {
636637

637638
return [
638639
bundlerRuntimeCode,
639-
...['polyfill', 'deps', 'shared', 'main', pagePath ? '/pages' + pagePath.replace(/\/$/, '/index') : '']
640+
...['polyfill', 'deps', 'shared', 'main', entryFile ? util.trimSuffix(entryFile, '.js') : '']
640641
.filter(name => name !== "" && this.#bundler.getBundledFile(name) !== null)
641642
.map(name => ({
642643
src: `${baseUrl}/_aleph/${this.#bundler.getBundledFile(name)}`
@@ -717,10 +718,12 @@ export class Application implements ServerApplication {
717718
return dir
718719
}
719720

720-
private createRouteUpdate(url: string): [string, string, { isIndexModule?: boolean, useDeno?: boolean }] {
721-
let pathPath = toPagePath(url)
721+
private createRouteUpdate(url: string): [string, string, { isIndex?: boolean, useDeno?: boolean }] {
722+
const isBuiltinModule = moduleExts.some(ext => url.endsWith('.' + ext))
723+
let pagePath = isBuiltinModule ? toPagePath(url) : util.trimSuffix(url, '/pages')
722724
let useDeno: boolean | undefined = undefined
723-
let isIndexModule: boolean | undefined = undefined
725+
let isIndex: boolean | undefined = undefined
726+
724727
if (this.config.ssr !== false) {
725728
this.lookupDeps(url, dep => {
726729
if (dep.url.startsWith('#useDeno-')) {
@@ -729,15 +732,31 @@ export class Application implements ServerApplication {
729732
}
730733
})
731734
}
732-
if (pathPath !== '/') {
735+
736+
if (!isBuiltinModule) {
737+
for (const plugin of this.config.plugins) {
738+
if (plugin.type === 'loader' && plugin.test.test(url) && plugin.pagePathResolve) {
739+
const { path, isIndex: _isIndex } = plugin.pagePathResolve(url)
740+
if (!util.isNEString(path)) {
741+
throw new Error(`bad pagePathResolve result of '${plugin.name}' plugin`)
742+
}
743+
pagePath = path
744+
if (!!_isIndex) {
745+
isIndex = true
746+
}
747+
break
748+
}
749+
}
750+
} else if (pagePath !== '/') {
733751
for (const ext of moduleExts) {
734752
if (url.endsWith('/index.' + ext)) {
735-
isIndexModule = true
753+
isIndex = true
736754
break
737755
}
738756
}
739757
}
740-
return [pathPath, url, { isIndexModule, useDeno }]
758+
759+
return [pagePath, url, { isIndex, useDeno }]
741760
}
742761

743762
/** apply loaders recurively. */
@@ -766,6 +785,7 @@ export class Application implements ServerApplication {
766785
}
767786
}
768787
}
788+
769789
return { code, map }
770790
}
771791

@@ -1149,11 +1169,11 @@ export class Application implements ServerApplication {
11491169
{ url, hash }
11501170
)
11511171
await Deno.writeTextFile(mod.jsFile, jsContent)
1172+
dep.hash = hash
11521173
mod.hash = computeHash(mod.sourceHash + mod.deps.map(({ hash }) => hash).join(''))
11531174
callback(mod)
11541175
log.debug('compilation side-effect:', mod.url, dim('<-'), url)
11551176
this.applyCompilationSideEffect(mod.url, callback)
1156-
break
11571177
}
11581178
}
11591179
}

server/ssr.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createBlankRouterURL, RouteModule } from '../framework/core/routing.ts'
2+
import { dirname, basename } from 'https://deno.land/[email protected]/path/mod.ts'
23
import log from '../shared/log.ts'
34
import util from '../shared/util.ts'
45
import type { RouterURL } from '../types.ts'
@@ -96,20 +97,24 @@ export class Renderer {
9697
const isDev = this.#app.isDev
9798
const appModule = this.#app.findModuleByName('app')
9899
const { default: App } = appModule ? await import(`file://${appModule.jsFile}#${appModule.hash.slice(0, 6)}`) : {} as any
99-
const imports = nestedModules
100+
101+
let entryFile = ''
102+
const nestedPageComponents = await Promise.all(nestedModules
100103
.filter(({ url }) => this.#app.getModule(url) !== null)
101104
.map(async ({ url }) => {
102105
const { jsFile, hash } = this.#app.getModule(url)!
103106
const { default: Component } = await import(`file://${jsFile}#${hash.slice(0, 6)}`)
107+
entryFile = dirname(url) + '/' + basename(jsFile)
104108
return {
105109
url,
106110
Component
107111
}
108112
})
113+
)
109114
const { head, body, data, scripts } = await this.#renderer.render(
110115
url,
111116
App,
112-
await Promise.all(imports)
117+
nestedPageComponents
113118
)
114119

115120
if (isDev) {
@@ -126,9 +131,9 @@ export class Renderer {
126131
type: 'application/json',
127132
innerText: JSON.stringify(data, undefined, isDev ? 2 : 0),
128133
} : '',
129-
...this.#app.getSSRHTMLScripts(url.pagePath),
134+
...this.#app.getSSRHTMLScripts(entryFile),
130135
...scripts.map((script: Record<string, any>) => {
131-
if (script.innerText && !this.#app.isDev) {
136+
if (script.innerText && !isDev) {
132137
return { ...script, innerText: script.innerText }
133138
}
134139
return script

0 commit comments

Comments
 (0)