@taprootio/rollup-plugin-taproot
Version:
Simple static site generation
264 lines (226 loc) • 7.85 kB
text/typescript
import { OutputAsset, OutputBundle, OutputChunk, Plugin } from "rollup"
import { TaprootPluginOptions } from "./models/TaprootPluginOptions"
import { Template } from "./models/TaprootTemplateParser"
import readdir from "recursive-readdir"
import fs from "fs"
import path from "path"
import { Page } from "./models/TaprootPageRenderer"
import { BuildHead } from "./head-builder"
import { cloneable } from "./helpers.ts/cloneable"
interface FileElements {
scripts: string
links: string
}
const determinePath = (fileName: string, canonical: string): string => {
let emitPath: string
if (canonical) {
emitPath = canonical
} else {
emitPath = fileName.split(".")[0]
}
if (emitPath.endsWith("index")) {
emitPath = emitPath.substring(0, emitPath.length - 5)
}
if (emitPath.endsWith("/")) {
emitPath = emitPath.substring(0, emitPath.length - 1)
}
return emitPath
}
const buildTagPath = (tagRoot: string, tag: string): string =>
`${tagRoot ?? "/tag/"}${encodeURI(tag.toLowerCase().replace(" ", "-"))}`
const getElements = (
files: Record<string, (OutputChunk | OutputAsset)[]>
): FileElements => {
// https://github.com/rollup/plugins/blob/master/packages/html/src/index.ts
// TODO: Set these from options...
const publicPath = "/"
// TODO: Allow specifying attributes
const attrs = ""
const scripts = (files.js || [])
.map(({ fileName }) => {
// const attrs = makeHtmlAttributes(attributes.script);
return `<script src="${publicPath}${fileName}"${attrs} type="module"></script>`
})
.join("\n")
const links = (files.css || [])
.map(({ fileName }) => {
// const attrs = makeHtmlAttributes(attributes.link);
return `<link href="${publicPath}${fileName}" rel="stylesheet"${attrs}>`
})
.join("\n")
return {
scripts,
links,
}
}
const getFiles = (
bundle: OutputBundle
): Record<string, (OutputChunk | OutputAsset)[]> => {
const files = Object.values(bundle).filter(
(file) => file.type === "chunk" || file.type === "asset"
)
const result = {} as ReturnType<typeof getFiles>
for (const file of files) {
const { fileName } = file
const extension = path.extname(fileName).substring(1)
result[extension] = (result[extension] || []).concat(file)
}
return result
}
const Taproot = (options: TaprootPluginOptions): Plugin => {
const templates = new Map<String, Template>()
let pagesToRender: Map<string, Page>
return {
name: "rollup-plugin-taproot",
async buildStart(_options) {
pagesToRender = new Map<string, Page>()
const templateFiles = await readdir(options.TemplatesPath)
for (const templateFile of templateFiles) {
for (const templateParser of options.TemplateParsers) {
if (!templateParser.FileMatcher.test(templateFile)) {
continue
}
const source = fs.readFileSync(templateFile).toString()
const templateKey = path.basename(templateFile).split(".")[0]
const compiled = templateParser.CompileTemplate(source)
templates.set(templateKey, compiled)
}
}
console.log("Reading pages...")
const pageFiles = await readdir(options.PagesPath)
for (const pageFile of pageFiles) {
for (const pageRenderer of options.PageRenderers) {
if (!pageRenderer.FileMatcher.test(pageFile)) {
continue
}
const source = fs.readFileSync(pageFile).toString()
const fileName = pageFile.substring(
pageFile.indexOf(`${options.PagesPath.substring(1)}/`) +
options.PagesPath.length
)
// Clone it so any changes made here do not affect cached
// data in the renderer.
const rendered = cloneable.deepCopy(pageRenderer.Render(source))
rendered.Data.Canonical = determinePath(
fileName,
rendered.Data.Canonical ?? ""
)
pagesToRender.set(fileName, rendered)
}
}
const tags = new Map<string, Array<Page>>()
for (const page of pagesToRender.values()) {
for (const tag of page.Data.Tags ?? []) {
if (!tags.has(tag)) {
tags.set(tag, [])
}
tags.get(tag)?.push(page)
}
}
for (const tag of tags.keys()) {
const pages = tags.get(tag) ?? []
const contents = `
<ul class="articles">
${pages
.map(
(page) => `
<li>
<article>
<h2 slot="page-title">${page.Data.Title}</h2>
${
page.Data.Description
? `<p slot="page-description">${page.Data.Description}</p>`
: ""
}
<a href="/${page.Data.Canonical}">Read More</a>
</article>
</li>`
)
.join("\n")}
</ul>`
const page: Page = {
Contents: contents,
Data: {
Title: tag,
Description: `Pages tagged with '${tag}'`,
Canonical: determinePath(
`${buildTagPath(options.TagRoot, tag).substring(1)}`,
""
),
PageType: "website",
},
}
pagesToRender.set(buildTagPath(options.TagRoot, tag), page)
}
},
generateBundle(_options, bundle) {
const assets = getFiles(bundle)
const elements = getElements(assets)
const currentYear = new Date().getFullYear().toString()
for (const fileName of pagesToRender.keys()) {
const taprootPage = pagesToRender.get(fileName) as Page
console.log(JSON.stringify(taprootPage.Data))
const outPath = taprootPage.Data.Canonical ?? ""
const author = taprootPage.Data.Author
? options.Authors.get(taprootPage.Data.Author)
: undefined
const head = [
BuildHead({
Author: author,
DateModified: taprootPage.Data.DateModified,
DatePublished: taprootPage.Data.DatePublished,
Publisher: options.Publisher,
SocialImage: {
Height: 100,
Width: 100,
MediaType: "image/png",
Url: `${options.SiteRootUrl}${
outPath ? `${outPath}/` : ""
}social_image.png}`,
},
Title: taprootPage.Data.Title,
Canonical: `${options.SiteRootUrl}${outPath}`,
CharSet: options.CharSet ?? "UTF-8",
Description: taprootPage.Data.Description,
PageType: taprootPage.Data.PageType,
}),
elements.links,
elements.scripts,
].join("\n")
const template = templates.has(taprootPage.Data.Template ?? "")
? (taprootPage.Data.Template as string)
: "default"
const rendered = templates.get(template)?.Render({
Author: author,
Canonical: taprootPage.Data.Canonical,
Contents: taprootPage.Contents,
CSSVars: taprootPage.Data.CSSVars,
CurrentYear: currentYear,
DateModified: taprootPage.Data.DateModified,
DatePublished: taprootPage.Data.DatePublished,
Description: taprootPage.Data.Description,
Head: head,
HidePageHead: taprootPage.Data.HidePageHead,
PageType: taprootPage.Data.PageType,
SiteName: options.Publisher.Name,
Tags:
taprootPage.Data.Tags?.map((tag) => {
return {
Tag: tag,
Url: buildTagPath(options.TagRoot, tag),
}
}) ?? [],
Title: taprootPage.Data.Title,
}) as string
const htmlPath = `${outPath ? `${outPath}/` : ""}index.html`
console.log(`Emitting file ${htmlPath}`)
this.emitFile({
type: "asset",
fileName: htmlPath,
source: rendered,
})
}
},
}
}
export { Taproot }