Skip to content

Commit

Permalink
feat: added prerender for anchor tags
Browse files Browse the repository at this point in the history
  • Loading branch information
tsukinoko-kun committed Jul 27, 2024
1 parent 143cf63 commit 3799b06
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 31 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ Just keep in mind that ECS.ts is written in TypeScript and is not shipping any J
You can use a `vite.config.ts` file to configure the build process.
By default, ECS.ts expects a `src` folder with an `index.html` as entry point.

```json
```json5
{
"scripts": {
"dev": "ecs dev",
"build": "ecs build"
}
scripts: {
dev: "ecs dev",
build: "ecs build", // use --prerender-anchors to prerender all internal anchor links (for HtmlPlugin)
},
}
```

Expand Down
2 changes: 1 addition & 1 deletion apps/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"source": "src/index.html",
"scripts": {
"dev": "ecs dev",
"build": "ecs build"
"build": "ecs build --prerender-anchors"
},
"dependencies": {
"@tsukinoko-kun/ecs.ts": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<title>ECS Test</title>
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<link href="style.css" rel="stylesheet" />
<link href="./style.css" rel="stylesheet" />
</head>
<body>
<main id="app"></main>
Expand Down
84 changes: 62 additions & 22 deletions bin/ecs.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { join, relative, resolve } from "node:path"
import { dirname, join, relative } from "node:path"
import { join as posixJoin } from "node:path/posix"
import { existsSync } from "node:fs"
import { build as viteBuild, createServer as viteDev } from "vite"
import { readdir, readFile, writeFile } from "node:fs/promises"
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"
import { JSDOM } from "jsdom"
import { Script } from "node:vm"

Expand All @@ -19,18 +20,19 @@ async function main(args) {

switch (args[0]) {
case "build":
await build()
await build(args.slice(1))
break
case "dev":
await dev()
await dev(args.slice(1))
break
default:
console.error(`Unknown command: ${args[0]}`)
process.exit(1)
}
}

async function build() {
/** @param {string[]} args */
async function build(args) {
const viteConfig = await getViteConfig()

// build
Expand All @@ -44,23 +46,27 @@ async function build() {
const files = await readdir(dir, { withFileTypes: true })
for (const file of files) {
if (file.isDirectory()) {
dirs.push(resolve(dir, file.name))
dirs.push(join(dir, file.name))
} else if (file.isFile() && file.name.endsWith(".html")) {
htmlFiles.push(relative(viteConfig.build.outDir, resolve(dir, file.name)))
htmlFiles.push(relative(viteConfig.build.outDir, join(dir, file.name)))
}
}
}

const adr = `http://localhost${viteConfig.base}`

// render html files
for (let file of htmlFiles) {
console.log("Rendering", file)
const p = resolve(viteConfig.build.outDir, file)
const dom = new JSDOM(await readFile(p, "utf-8"), {
const done = new Set(htmlFiles)

/**
* @param {string} pathname
* @param {string|undefined} html
*/
async function render(pathname, html) {
console.log(`render(${pathname})`)
const dom = new JSDOM(html, {
runScripts: "outside-only",
resources: undefined,
url: adr + file,
url: adr + pathname,
pretendToBeVisual: true,
})
// wait for html to be loaded
Expand Down Expand Up @@ -89,14 +95,48 @@ async function build() {

await delay(500)

const html = dom.serialize()
console.log("Writing", file, html)
await writeFile(resolve(viteConfig.build.outDir, file), html)
dom.window.close()
const newHtml = dom.serialize()
await mkdir(dirname(join(viteConfig.build.outDir, pathname)), { recursive: true })
await writeFile(join(viteConfig.build.outDir, pathname), newHtml)

if (args.includes("--prerender-anchors")) {
const hrefs = Array.from(dom.window.document.querySelectorAll("a[href]")).map((a) => a.href)
const loc = dom.window.location.href
dom.window.close()
for (const href of hrefs) {
try {
const now = new URL(loc)
const targetUrl = new URL(href, loc)
if (targetUrl.origin !== now.origin) {
continue
}
let targetPath = targetUrl.pathname
if (targetPath.startsWith(viteConfig.base)) {
targetPath = targetPath.slice(viteConfig.base.length)
}
if (!targetPath.endsWith(".html")) {
targetPath = posixJoin(targetPath, "index.html")
}
if (done.has(targetPath)) {
continue
}
done.add(targetPath)
await render(targetPath, newHtml)
} catch {}
}
} else {
dom.window.close()
}
}

// render html files
for (let file of htmlFiles) {
await render(file, await readFile(join(viteConfig.build.outDir, file), "utf8"))
}
}

async function dev() {
/** @param {string[]} args */
async function dev(args) {
const viteConfig = await getViteConfig()
const server = await viteDev(viteConfig)
await server.listen()
Expand All @@ -107,15 +147,15 @@ async function dev() {

async function getViteConfig() {
const def = {
root: resolve(__dirname, "./src"),
root: join(__dirname, "./src"),
base: "/",
build: {
outDir: resolve(__dirname, "./dist"),
outDir: join(__dirname, "./dist"),
emptyOutDir: true,
},
}

const viteConfigPath = resolve(__dirname, "vite.config.js")
const viteConfigPath = join(__dirname, "vite.config.js")
if (existsSync(viteConfigPath)) {
const config = await import(viteConfigPath)

Expand All @@ -130,7 +170,7 @@ async function getViteConfig() {
}

function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
return new Promise((res) => setTimeout(res, ms))
}

await main(process.argv.slice(2))
7 changes: 5 additions & 2 deletions lib/builtin/plugins/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { HtmlRoot } from "../resources"
import { Last, Startup, Update } from "../../schedule"
import { cleanupHtmlInteraction, htmlInteraction, renderHtmlRoot } from "../systems"
import { Commands } from "../../commands"
import { res } from "../../resource"

export function HtmlPlugin(rootSelector: string): Plugin
export function HtmlPlugin(rootElement: Element): Plugin
export function HtmlPlugin(root: string | Element): Plugin {
return (app) => {
Commands.insertResource(new HtmlRoot(root))
const rootElement = res(HtmlRoot).element
rootElement.innerHTML = ""
app.addSystem(Update, renderHtmlRoot)
app.addSystem(Startup, htmlInteraction)
app.addSystem(Last, cleanupHtmlInteraction)
.addSystem(Startup, htmlInteraction)
.addSystem(Last, cleanupHtmlInteraction)
}
}

0 comments on commit 3799b06

Please sign in to comment.