Skip to content

Commit

Permalink
fert: prerender from Location resource
Browse files Browse the repository at this point in the history
  • Loading branch information
tsukinoko-kun committed Jul 28, 2024
1 parent ebb5a20 commit 73b4a38
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 128 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ 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.

If you use the Location state (RouterPlugin), all possible locations are prerendered automatically.

```json5
{
scripts: {
dev: "ecs dev",
build: "ecs build", // use --prerender-anchors to prerender all internal anchor links (for HtmlPlugin)
build: "ecs build",
},
}
```
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 --prerender-anchors"
"build": "ecs build"
},
"dependencies": {
"@tsukinoko-kun/ecs.ts": "workspace:*",
Expand Down
119 changes: 75 additions & 44 deletions bin/ecs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,59 +55,75 @@ async function build(args) {

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

const done = new Set(htmlFiles)
const done = new Set(htmlFiles.map((f) => join(viteConfig.build.outDir, f)))

/**
* @param {string} pathname
* @param {string} file
* @param {string|undefined} html
*/
async function render(pathname, html) {
console.log(`render(${pathname})`)
async function render(pathname, file, html) {
done.add(file)

if (!html) {
html = await readFile(file, "utf-8")
}
console.log(`render("${pathname}", "${file}")`)
const url = new URL(pathname, adr)
const dom = new JSDOM(html, {
runScripts: "outside-only",
resources: undefined,
url: adr + pathname,
url: url.href,
pretendToBeVisual: true,
})
// wait for html to be loaded
await new Promise((res) => {
dom.window.onload = res
if (dom.window.document.readyState === "complete") {
res()
}
})
// get all scripts
const scripts = Array.from(dom.window.document.querySelectorAll("script[src]"))
for (const script of scripts) {
const url = new URL(script.src, adr)
let filePath = url.pathname
if (filePath.startsWith(viteConfig.base)) {
filePath = filePath.slice(viteConfig.base.length)
}
const p = join(viteConfig.build.outDir, filePath)
if (!existsSync(p)) {
continue

let open = true
try {
// wait for html to be loaded
await new Promise((res) => {
dom.window.onload = res
if (dom.window.document.readyState === "complete") {
res()
}
})
// get all scripts
const scripts = Array.from(dom.window.document.querySelectorAll("script[src]"))
for (const script of scripts) {
const url = new URL(script.src, adr)
let filePath = url.pathname
if (filePath.startsWith(viteConfig.base)) {
filePath = filePath.slice(viteConfig.base.length)
}
const p = join(viteConfig.build.outDir, filePath)
if (!existsSync(p)) {
continue
}
const content = await readFile(p, "utf-8")
const s = new Script(content, { filename: url.pathname })
s.runInContext(dom.getInternalVMContext())
}
const content = await readFile(p, "utf-8")
const s = new Script(content, { filename: url.pathname })
s.runInContext(dom.getInternalVMContext())
}

await delay(500)
await delay(500)

const newHtml = dom.serialize()
await mkdir(dirname(join(viteConfig.build.outDir, pathname)), { recursive: true })
await writeFile(join(viteConfig.build.outDir, pathname), newHtml)
const newHtml = dom.serialize()
const dir = dirname(file)
await mkdir(dir, { recursive: true })
await writeFile(file, newHtml)

/** @type {Array<string>} */
const routerPaths = Array.from(dom.window.__ecs_debug__(true).location.pathCache.keys())

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) {
open = false

if (routerPaths.length === 0) {
return
}

for (const href of routerPaths) {
try {
const now = new URL(loc)
const targetUrl = new URL(href, loc)
if (targetUrl.origin !== now.origin) {
const targetUrl = new URL(href, url)
if (targetUrl.origin !== url.origin) {
continue
}
let targetPath = targetUrl.pathname
Expand All @@ -117,21 +133,36 @@ async function build(args) {
if (!targetPath.endsWith(".html")) {
targetPath = posixJoin(targetPath, "index.html")
}
if (done.has(targetPath)) {
const file = join(viteConfig.build.outDir, targetPath)
if (done.has(file)) {
continue
}
done.add(targetPath)
await render(targetPath, newHtml)
if (existsSync(file)) {
continue
}
if (targetPath.endsWith("index.html")) {
targetPath = targetPath.slice(0, -10)
}
await render(targetPath, file, newHtml)
} catch {}
}
} else {
dom.window.close()
} finally {
if (open) {
dom.window.close()
}
}
}

// render html files
for (let file of htmlFiles) {
await render(file, await readFile(join(viteConfig.build.outDir, file), "utf8"))
for (const file of htmlFiles) {
let p = file
if (p.endsWith("index.html")) {
p = p.slice(0, -10)
}
if (p === "") {
p = "/"
}
await render(p, join(viteConfig.build.outDir, file), undefined)
}
}

Expand Down
8 changes: 8 additions & 0 deletions lib/builtin/resources/basePath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,12 @@ export class BasePath {
public constructor(basePath: string) {
this.basePath = basePath
}

public toString(): string {
return this.basePath
}

public valueOf(): string {
return this.basePath
}
}
97 changes: 54 additions & 43 deletions lib/builtin/state/location.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Equals } from "../../traits"
import { res } from "../../resource"
import { BasePath } from "../resources"
import { insertDebugObject } from "../../debug"
import { panic } from "../../err"

function trimBasePath(basePath: string, path: string): string {
if (basePath === "/") {
Expand All @@ -15,7 +17,10 @@ function trimBasePath(basePath: string, path: string): string {
return path
}

const pathCache = new Map<string, Location>()
const [pathCache, err] = insertDebugObject("location.pathCache", new Map<string, Location>())
if (err) {
panic(err)
}

export type TrainingSlash = "add" | "trim" | "keep"

Expand All @@ -24,36 +29,38 @@ export class Location implements Equals {
public static trailingSlash: TrainingSlash = "keep"
public path: string

private constructor(path: string) {
private constructor(path: string, trim: boolean = false) {
if (trim) {
if (Location.trimIndexHtml && path.endsWith("/index.html")) {
path = path.slice(0, -10)
}
switch (Location.trailingSlash) {
case "add":
if (!path.endsWith("/")) {
path += "/"
}
break
case "trim":
if (path.endsWith("/")) {
path = path.slice(0, -1)
}
break
}
}

if (path === "") {
path = "/"
}

this.path = path
}

public static windowLocation(): Location {
const pn = window.location.pathname
const pn = Location.trimBasePath(window.location.pathname)
if (pathCache.has(pn)) {
return pathCache.get(pn)!
}
const bp = res(BasePath).basePath
let tp = trimBasePath(bp, pn)
if (Location.trimIndexHtml && tp.endsWith("/index.html")) {
tp = tp.slice(0, -10)
}
switch (Location.trailingSlash) {
case "add":
if (!tp.endsWith("/")) {
tp += "/"
}
break
case "trim":
if (tp.endsWith("/")) {
tp = tp.slice(0, -1)
}
break
}
if (tp === "") {
tp = "/"
}
const l = new Location(tp)
const l = new Location(pn, true)
pathCache.set(pn, l)
return l
}
Expand All @@ -74,35 +81,39 @@ export class Location implements Equals {
* Create a Location from a path, with trimming options applied
*/
public static fromPathTrimmed(path: string): Location {
path = Location.trimBasePath(path)

if (pathCache.has(path)) {
return pathCache.get(path)!
}
const l = new Location(path, true)
pathCache.set(path, l)
return l
}

private static trimBasePath(path: string): string {
const bp = res(BasePath).basePath
let tp = trimBasePath(bp, path)
if (Location.trimIndexHtml && tp.endsWith("/index.html")) {
tp = tp.slice(0, -10)
if (bp === "/") {
return path
}
switch (Location.trailingSlash) {
case "add":
if (!tp.endsWith("/")) {
tp += "/"
}
break
case "trim":
if (tp.endsWith("/")) {
tp = tp.slice(0, -1)
}
break
if (path.startsWith(bp)) {
path = path.slice(bp.length)
}
if (tp === "") {
tp = "/"
if (!path.startsWith("/")) {
path = "/" + path
}
const l = new Location(tp)
pathCache.set(path, l)
return l
return path
}

public equals(other: this): boolean {
return this.path === other.path
}

public toString(): string {
return this.path
}

public valueOf(): string {
return this.path
}
}
Loading

0 comments on commit 73b4a38

Please sign in to comment.