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

Commit 87c0533

Browse files
author
Je
committed
feat: add Scripts component
1 parent 25b8eeb commit 87c0533

File tree

5 files changed

+98
-61
lines changed

5 files changed

+98
-61
lines changed

head.ts

Lines changed: 76 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,69 @@
1+
import unescape from 'https://esm.sh/lodash/unescape?no-check'
12
import React, { Children, createElement, isValidElement, PropsWithChildren, ReactElement, ReactNode, useEffect } from 'https://esm.sh/react'
23
import type { AlephEnv } from './types.ts'
34
import util, { hashShort } from './util.ts'
45

6+
const serverHeadElements: Map<string, { type: string, props: Record<string, any> }> = new Map()
7+
const serverScriptsElements: Map<string, { type: string, props: Record<string, any> }> = new Map()
8+
const serverStyles: Map<string, { css: string, asLink: boolean }> = new Map()
9+
10+
export async function renderHead(styles?: { url: string, hash: string, async?: boolean }[]) {
11+
const { __buildMode, __buildTarget } = (window as any).ALEPH.ENV as AlephEnv
12+
const tags: string[] = []
13+
serverHeadElements.forEach(({ type, props }) => {
14+
if (type === 'title') {
15+
if (util.isNEString(props.children)) {
16+
tags.push(`<title ssr>${props.children}</title>`)
17+
} else if (util.isNEArray(props.children)) {
18+
tags.push(`<title ssr>${props.children.join('')}</title>`)
19+
}
20+
} else {
21+
const attrs = Object.keys(props)
22+
.filter(key => key !== 'children')
23+
.map(key => ` ${key}=${JSON.stringify(props[key])}`)
24+
.join('')
25+
if (util.isNEString(props.children)) {
26+
tags.push(`<${type}${attrs} ssr>${props.children}</${type}>`)
27+
} else if (util.isNEArray(props.children)) {
28+
tags.push(`<${type}${attrs} ssr>${props.children.join('')}</${type}>`)
29+
} else {
30+
tags.push(`<${type}${attrs} ssr />`)
31+
}
32+
}
33+
})
34+
await Promise.all(styles?.filter(({ async }) => !!async).map(({ url, hash }) => {
35+
return import('file://' + util.cleanPath(`${Deno.cwd()}/.aleph/${__buildMode}.${__buildTarget}/${url}.${hash.slice(0, hashShort)}.js`))
36+
}) || [])
37+
styles?.forEach(({ url }) => {
38+
if (serverStyles.has(url)) {
39+
const { css, asLink } = serverStyles.get(url)!
40+
if (asLink) {
41+
tags.push(`<link rel="stylesheet" href="${css}" data-module-id=${JSON.stringify(url)} />`)
42+
} else {
43+
tags.push(`<style type="text/css" data-module-id=${JSON.stringify(url)}>${css}</style>`)
44+
}
45+
}
46+
})
47+
serverHeadElements.clear()
48+
return tags
49+
}
50+
51+
export async function renderScripts() {
52+
const scripts: Record<string, any>[] = []
53+
serverScriptsElements.forEach(({ props }) => {
54+
const { children, ...attrs } = props
55+
if (util.isNEString(children)) {
56+
scripts.push({ ...attrs, innerText: unescape(children).trim() })
57+
} else if (util.isNEArray(children)) {
58+
scripts.push({ ...attrs, innerText: unescape(children.join('')).trim() })
59+
} else {
60+
scripts.push(props)
61+
}
62+
})
63+
serverScriptsElements.clear()
64+
return scripts
65+
}
66+
567
export default function Head({ children }: PropsWithChildren<{}>) {
668
if (window.Deno) {
769
parse(children).forEach(({ type, props }, key) => serverHeadElements.set(key, { type, props }))
@@ -55,6 +117,20 @@ export default function Head({ children }: PropsWithChildren<{}>) {
55117
return null
56118
}
57119

120+
export function Scripts({ children }: PropsWithChildren<{}>) {
121+
if (window.Deno) {
122+
parse(children).forEach(({ type, props }, key) => {
123+
if (type === 'script') {
124+
serverScriptsElements.set(key, { type, props })
125+
}
126+
})
127+
}
128+
129+
// todo: insert page scripts in browser
130+
131+
return null
132+
}
133+
58134
interface SEOProps {
59135
title: string
60136
description: string
@@ -108,50 +184,6 @@ export function Viewport(props: ViewportProps) {
108184
)
109185
}
110186

111-
const serverHeadElements: Map<string, { type: string, props: Record<string, any> }> = new Map()
112-
const serverStyles: Map<string, { css: string, asLink: boolean }> = new Map()
113-
114-
export async function renderHead(styles?: { url: string, hash: string, async?: boolean }[]) {
115-
const { __buildMode, __buildTarget } = (window as any).ALEPH.ENV as AlephEnv
116-
const tags: string[] = []
117-
serverHeadElements.forEach(({ type, props }) => {
118-
if (type === 'title') {
119-
if (util.isNEString(props.children)) {
120-
tags.push(`<title ssr>${props.children}</title>`)
121-
} else if (util.isNEArray(props.children)) {
122-
tags.push(`<title ssr>${props.children.join('')}</title>`)
123-
}
124-
} else {
125-
const attrs = Object.keys(props)
126-
.filter(key => key !== 'children')
127-
.map(key => ` ${key}=${JSON.stringify(props[key])}`)
128-
.join('')
129-
if (util.isNEString(props.children)) {
130-
tags.push(`<${type}${attrs} ssr>${props.children}</${type}>`)
131-
} else if (util.isNEArray(props.children)) {
132-
tags.push(`<${type}${attrs} ssr>${props.children.join('')}</${type}>`)
133-
} else {
134-
tags.push(`<${type}${attrs} ssr />`)
135-
}
136-
}
137-
})
138-
await Promise.all(styles?.filter(({ async }) => !!async).map(({ url, hash }) => {
139-
return import('file://' + util.cleanPath(`${Deno.cwd()}/.aleph/${__buildMode}.${__buildTarget}/${url}.${hash.slice(0, hashShort)}.js`))
140-
}) || [])
141-
styles?.forEach(({ url }) => {
142-
if (serverStyles.has(url)) {
143-
const { css, asLink } = serverStyles.get(url)!
144-
if (asLink) {
145-
tags.push(`<link rel="stylesheet" href="${css}" data-module-id=${JSON.stringify(url)} />`)
146-
} else {
147-
tags.push(`<style type="text/css" data-module-id=${JSON.stringify(url)}>${css}</style>`)
148-
}
149-
}
150-
})
151-
serverHeadElements.clear()
152-
return tags
153-
}
154-
155187
export function applyCSS(id: string, css: string, asLink: boolean = false) {
156188
if (window.Deno) {
157189
serverStyles.set(id, { css, asLink })

mod.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { redirect } from './aleph.ts'
22
export * from './context.ts'
3-
export { default as Head, SEO, Viewport } from './head.ts'
3+
export { default as Head, Scripts, SEO, Viewport } from './head.ts'
44
export * from './hooks.ts'
55
export { default as Import } from './import.ts'
66
export { default as Link, NavLink } from './link.ts'
7+

project.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ interface Module {
3131
interface Renderer {
3232
renderPage: Function
3333
renderHead: Function
34+
renderScripts: Function
3435
}
3536

3637
interface RenderResult {
3738
url: RouterURL
3839
status: number
3940
head: string[]
41+
scripts: Record<string, any>[]
4042
body: string
4143
data: Record<string, string> | null
4244
}
@@ -52,7 +54,7 @@ export class Project {
5254
#routing: Routing = new Routing()
5355
#apiRouting: Routing = new Routing()
5456
#fsWatchListeners: Array<EventEmitter> = []
55-
#renderer: Renderer = { renderPage: () => void 0, renderHead: () => void 0 }
57+
#renderer: Renderer = { renderPage: () => void 0, renderHead: () => void 0, renderScripts: () => void 0 }
5658
#rendered: Map<string, Map<string, RenderResult>> = new Map()
5759
#postcssPlugins: Record<string, AcceptedPlugin> = {}
5860

@@ -76,7 +78,6 @@ export class Project {
7678
plugins: [],
7779
postcss: {
7880
plugins: [
79-
'postcss-flexbugs-fixes',
8081
'autoprefixer'
8182
]
8283
}
@@ -238,14 +239,15 @@ export class Project {
238239

239240
const { baseUrl } = this.config
240241
const mainModule = this.#modules.get('/main.js')!
241-
const { url, status, head, body, data } = await this._renderPage(loc)
242+
const { url, status, head, scripts, body, data } = await this._renderPage(loc)
242243
const html = createHtml({
243244
lang: url.locale,
244245
head: head,
245246
scripts: [
246247
data ? { type: 'application/json', innerText: JSON.stringify(data), id: 'ssr-data' } : '',
247248
{ src: path.join(baseUrl, `/_aleph/main.${mainModule.hash.slice(0, hashShort)}.js`), type: 'module' },
248249
{ src: path.join(baseUrl, `/_aleph/-/deno.land/x/aleph/nomodule.js${this.isDev ? '?dev' : ''}`), nomodule: true },
250+
...scripts
249251
],
250252
body,
251253
minify: !this.isDev
@@ -339,7 +341,7 @@ export class Project {
339341

340342
// write 404 page
341343
const { baseUrl } = this.config
342-
const { url, head, body, data } = await this._render404Page()
344+
const { url, head, scripts, body, data } = await this._render404Page()
343345
const mainModule = this.#modules.get('/main.js')!
344346
const e404PageHtml = createHtml({
345347
lang: url.locale,
@@ -348,6 +350,7 @@ export class Project {
348350
data ? { type: 'application/json', innerText: JSON.stringify(data), id: 'ssr-data' } : '',
349351
{ src: path.join(baseUrl, `/_aleph/main.${mainModule.hash.slice(0, hashShort)}.js`), type: 'module' },
350352
{ src: path.join(baseUrl, `/_aleph/-/deno.land/x/aleph/nomodule.js${this.isDev ? '?dev' : ''}`), nomodule: true },
353+
...scripts
351354
],
352355
body,
353356
minify: !this.isDev
@@ -521,7 +524,7 @@ export class Project {
521524
} else {
522525
name = p.name
523526
}
524-
const { default: Plugin } = await import(`https://esm.sh/${name}[email protected]`)
527+
const { default: Plugin } = await import(`https://esm.sh/${name}[email protected]&no-check`)
525528
this.#postcssPlugins[name] = Plugin
526529
})
527530
}
@@ -598,8 +601,8 @@ export class Project {
598601
await this._compile('https://deno.land/x/aleph/renderer.ts', { forceTarget: 'es2020' })
599602
await this._createMainModule()
600603

601-
const { renderPage, renderHead } = await import('file://' + this.#modules.get('//deno.land/x/aleph/renderer.js')!.jsFile)
602-
this.#renderer = { renderPage, renderHead }
604+
const { renderPage, renderHead, renderScripts } = await import('file://' + this.#modules.get('//deno.land/x/aleph/renderer.js')!.jsFile)
605+
this.#renderer = { renderPage, renderHead, renderScripts }
603606

604607
log.info(colors.bold('Aleph.js'))
605608
if ('__file' in this.config) {
@@ -885,10 +888,10 @@ export class Project {
885888
break
886889
}
887890
}
888-
if (/^https?:\/\/[0-9a-z\.\-]+\/react(@[0-9a-z\.\-]+)?\/?$/i.test(url)) {
891+
if (/^https?:\/\/[0-9a-z\.\-]+\/react(@[0-9a-z\.\-]+)?\/?$/i.test(dlUrl)) {
889892
dlUrl = this.config.reactUrl
890893
}
891-
if (/^https?:\/\/[0-9a-z\.\-]+\/react\-dom(@[0-9a-z\.\-]+)?(\/server)?\/?$/i.test(url)) {
894+
if (/^https?:\/\/[0-9a-z\.\-]+\/react\-dom(@[0-9a-z\.\-]+)?(\/server)?\/?$/i.test(dlUrl)) {
892895
dlUrl = this.config.reactDomUrl
893896
if (/\/server\/?$/i.test(url)) {
894897
dlUrl += '/server'
@@ -966,7 +969,6 @@ export class Project {
966969
let css: string = sourceContent
967970
if (mod.id.endsWith('.less')) {
968971
try {
969-
// todo: sourceMap
970972
const output = await less.render(sourceContent || '/* empty content */')
971973
css = output.css
972974
} catch (error) {
@@ -983,7 +985,7 @@ export class Project {
983985
})
984986
css = (await postcss(plugins).process(css).async()).content
985987
if (this.isDev) {
986-
css = String(css).trim()
988+
css = css.trim()
987989
} else {
988990
const output = cleanCSS.minify(css)
989991
css = output.styles
@@ -995,7 +997,7 @@ export class Project {
995997
))};`,
996998
`applyCSS(${JSON.stringify(url)}, ${JSON.stringify(this.isDev ? `\n${css}\n` : css)});`,
997999
].join(this.isDev ? '\n' : '')
998-
mod.jsSourceMap = ''
1000+
mod.jsSourceMap = '' // todo: sourceMap
9991001
mod.hash = getHash(css)
10001002
} else if (mod.loader === 'markdown') {
10011003
const { __content, ...props } = safeLoadFront(sourceContent)
@@ -1250,7 +1252,7 @@ export class Project {
12501252
this.#rendered.set(url.pagePath, new Map())
12511253
}
12521254
}
1253-
const ret: RenderResult = { url, status: url.pagePath === '' ? 404 : 200, head: [], body: '<main></main>', data: null }
1255+
const ret: RenderResult = { url, status: url.pagePath === '' ? 404 : 200, head: [], scripts: [], body: '<main></main>', data: null }
12541256
Object.assign(window, {
12551257
location: {
12561258
protocol: 'http:',
@@ -1292,6 +1294,7 @@ export class Project {
12921294
...pageModuleTree.map(({ id }) => this._lookupAsyncDeps(id).filter(({ url }) => reStyleModuleExt.test(url)))
12931295
].flat())
12941296
ret.head = head
1297+
ret.scripts = await this.#renderer.renderScripts()
12951298
ret.body = `<main>${html}</main>`
12961299
ret.data = data
12971300
this.#rendered.get(url.pagePath)!.set(key, ret)
@@ -1308,7 +1311,7 @@ export class Project {
13081311
}
13091312

13101313
private async _render404Page(url: RouterURL = { locale: this.config.defaultLocale, pagePath: '', pathname: '/', params: {}, query: new URLSearchParams() }) {
1311-
const ret: RenderResult = { url, status: 404, head: [], body: '<main></main>', data: null }
1314+
const ret: RenderResult = { url, status: 404, head: [], scripts: [], body: '<main></main>', data: null }
13121315
try {
13131316
const e404Module = this.#modules.get('/404.js')
13141317
const { default: E404 } = e404Module ? await import('file://' + e404Module.jsFile) : {} as any
@@ -1317,6 +1320,7 @@ export class Project {
13171320
e404Module ? this._lookupAsyncDeps(e404Module.id).filter(({ url }) => reStyleModuleExt.test(url)) : []
13181321
].flat())
13191322
ret.head = head
1323+
ret.scripts = await this.#renderer.renderScripts()
13201324
ret.body = `<main>${html}</main>`
13211325
ret.data = data
13221326
} catch (err) {

renderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { createPageProps } from './routing.ts'
77
import type { RouterURL } from './types.ts'
88
import util from './util.ts'
99

10-
export { renderHead } from './head.ts'
10+
export { renderHead, renderScripts } from './head.ts'
1111

1212
export async function renderPage(
1313
url: RouterURL,

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' | 'json' | 'css' }
36+
transform?(path: string): { code: string, sourceMap?: string, loader?: 'js' | 'json' | 'css' | 'markdown' }
3737
}
3838

3939
/**

0 commit comments

Comments
 (0)