mothwing 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +160 -3
- package/bin/mothwing.js +45 -0
- package/package.json +73 -6
- package/src/assets.js +122 -0
- package/src/build.js +104 -0
- package/src/config.js +206 -0
- package/src/csp.js +21 -0
- package/src/data.js +54 -0
- package/src/deps.js +76 -0
- package/src/dev.js +145 -0
- package/src/html.js +67 -0
- package/src/index.js +20 -0
- package/src/log.js +24 -0
- package/src/render.js +162 -0
- package/src/routes.js +125 -0
- package/src/seo.js +105 -0
- package/src/site.js +96 -0
- package/src/template.js +207 -0
- package/types/assets.d.ts +23 -0
- package/types/build.d.ts +34 -0
- package/types/config.d.ts +240 -0
- package/types/csp.d.ts +8 -0
- package/types/data.d.ts +18 -0
- package/types/deps.d.ts +10 -0
- package/types/dev.d.ts +9 -0
- package/types/html.d.ts +12 -0
- package/types/index.d.ts +17 -0
- package/types/log.d.ts +6 -0
- package/types/render.d.ts +42 -0
- package/types/routes.d.ts +39 -0
- package/types/seo.d.ts +16 -0
- package/types/site.d.ts +7 -0
- package/types/template.d.ts +64 -0
package/src/template.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { escapeHtml } from "./html.js"
|
|
4
|
+
import { posix } from "./log.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} attrs ` src="index.js" module`
|
|
8
|
+
* @returns {Record<string, string | true>}
|
|
9
|
+
*/
|
|
10
|
+
export const parseAttrs = (attrs) => Object.fromEntries([...attrs.matchAll(/([\w-]+)(?:="([^"]*)")?/g)].map(([, name, value]) => [name, value ?? true]))
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pastes in the <x-html src="..."/> partials. One on a line of its own is indented like its tag, so partials are
|
|
14
|
+
* written unindented.
|
|
15
|
+
* @param {string} src The source folder, partials are relative to it
|
|
16
|
+
* @param {string} html
|
|
17
|
+
* @param {string} from For errors, the file `html` came from
|
|
18
|
+
* @param {Set<string>} [used] Gets every partial pasted in, relative to src
|
|
19
|
+
* @param {number} [depth]
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function includeHtml(src, html, from, used = new Set(), depth = 0) {
|
|
23
|
+
if (depth > 10) throw new Error(`${from}: <x-html> nested too deep, is a partial including itself?`)
|
|
24
|
+
|
|
25
|
+
return html.replace(/([ \t]*)<x-html((?:\s+[\w-]+(?:="[^"]*")?)*)\s*\/?>/g, (_, space, attrs, offset, page) => {
|
|
26
|
+
const file = parseAttrs(attrs).src
|
|
27
|
+
if (typeof file !== "string") throw new Error(`${from}: <x-html> needs a src="..."`)
|
|
28
|
+
const partial = path.join(src, file.replace(/^\//, ""))
|
|
29
|
+
if (!fs.existsSync(partial)) throw new Error(`${from}: <x-html> can't find ${file}`)
|
|
30
|
+
used.add(posix(path.relative(src, partial)))
|
|
31
|
+
|
|
32
|
+
const content = includeHtml(src, fs.readFileSync(partial, "utf8"), file, used, depth + 1).trimEnd()
|
|
33
|
+
const lineStart = offset === 0 || page[offset - 1] === "\n"
|
|
34
|
+
if (!lineStart) return space + content
|
|
35
|
+
return content.split("\n").map(line => line.trim() ? space + line : "").join("\n")
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {Record<string, any>} values
|
|
41
|
+
* @param {string} name "project.name"
|
|
42
|
+
* @returns {any} undefined when any part of it is missing
|
|
43
|
+
*/
|
|
44
|
+
export const lookup = (values, name) => name.split(".").reduce((value, key) => value?.[key], values)
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* What a page is filled from, and what it used of it.
|
|
48
|
+
* @typedef {object} Context
|
|
49
|
+
* @property {string} from The page, for errors
|
|
50
|
+
* @property {Set<string>} used The first part of every name looked up, "projects" for {{ projects.0.name }}
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {Record<string, any>} values
|
|
55
|
+
* @param {string} name
|
|
56
|
+
* @param {Context} context
|
|
57
|
+
* @returns {any}
|
|
58
|
+
*/
|
|
59
|
+
function find(values, name, context) {
|
|
60
|
+
context.used.add(name.split(".")[0])
|
|
61
|
+
return lookup(values, name)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* {{ name }} is filled with the value as text, {{{ name }}} as HTML, on a line of its own each line indented like it.
|
|
66
|
+
* Names can go into objects: {{ project.name }}.
|
|
67
|
+
* @param {string} html
|
|
68
|
+
* @param {Record<string, any>} values
|
|
69
|
+
* @param {string} from For errors, the page's file
|
|
70
|
+
* @param {Set<string>} [used] Gets the first part of every name filled in
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
export function fillPlaceholders(html, values, from, used = new Set()) {
|
|
74
|
+
/** @param {string} name */
|
|
75
|
+
const value = (name) => {
|
|
76
|
+
const found = find(values, name, { from, used })
|
|
77
|
+
if (found === undefined) throw new Error(`${from}: nothing to fill {{ ${name} }} with`)
|
|
78
|
+
return String(found)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// One pass, so what's filled in is never searched for placeholders itself
|
|
82
|
+
const placeholder = /^([ \t]*)\{\{\{\s*([\w.]+)\s*\}\}\}[ \t]*$|\{\{\{\s*([\w.]+)\s*\}\}\}|\{\{\s*([\w.]+)\s*\}\}/gm
|
|
83
|
+
return html.replace(placeholder, (_, indent, line, inline, text) => {
|
|
84
|
+
if (line) return value(line).split("\n").map(l => l.trim() ? indent + l : "").join("\n")
|
|
85
|
+
return inline ? value(inline) : escapeHtml(value(text))
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A page's HTML split at its <x-each> and <x-if> blocks.
|
|
91
|
+
* @typedef {{ type: "text", text: string }
|
|
92
|
+
* | { type: "x-each" | "x-if", attrs: Record<string, string | true>, dedent: number, children: Node[] }} Node
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
// A block tag on a line of its own takes its line with it, so it leaves no empty line behind. Formatters like
|
|
96
|
+
// Prettier can break a tag before its ">", "</x-if\n >" included
|
|
97
|
+
const BLOCK = /^([ \t]*)<(x-each|x-if)((?:\s+[\w-]+(?:="[^"]*")?)*)\s*>[ \t]*\r?\n|^[ \t]*<\/(x-each|x-if)\s*>[ \t]*(?:\r?\n|$)|<(x-each|x-if)((?:\s+[\w-]+(?:="[^"]*")?)*)\s*>|<\/(x-each|x-if)\s*>/gm
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} text
|
|
101
|
+
* @returns {number} The fewest spaces any of its lines start with
|
|
102
|
+
*/
|
|
103
|
+
const indentOf = (text) => Math.min(...text.split("\n").filter(line => line.trim()).map(line => line.length - line.trimStart().length))
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} html
|
|
107
|
+
* @param {string} from For errors, the page's file
|
|
108
|
+
* @returns {Node[]}
|
|
109
|
+
*/
|
|
110
|
+
export function parseTemplate(html, from) {
|
|
111
|
+
/** @type {{ node: Node & { type: "x-each" | "x-if" }, indent: string | null, bodyStart: number }[]} */
|
|
112
|
+
const open = []
|
|
113
|
+
/** @type {Node[]} */
|
|
114
|
+
const root = []
|
|
115
|
+
const children = () => open.at(-1)?.node.children ?? root
|
|
116
|
+
let last = 0
|
|
117
|
+
|
|
118
|
+
for (const match of html.matchAll(BLOCK)) {
|
|
119
|
+
const [whole, lineIndent, lineOpen, lineAttrs, lineClose, inlineOpen, inlineAttrs, inlineClose] = match
|
|
120
|
+
const start = match.index ?? 0
|
|
121
|
+
if (start > last) children().push({ type: "text", text: html.slice(last, start) })
|
|
122
|
+
last = start + whole.length
|
|
123
|
+
|
|
124
|
+
const opening = lineOpen ?? inlineOpen
|
|
125
|
+
if (opening) {
|
|
126
|
+
const node = { type: /** @type {"x-each" | "x-if"} */ (opening), attrs: parseAttrs(lineAttrs ?? inlineAttrs ?? ""), dedent: 0, children: [] }
|
|
127
|
+
children().push(node)
|
|
128
|
+
open.push({ node, indent: lineOpen ? lineIndent : null, bodyStart: last })
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const closing = lineClose ?? inlineClose
|
|
133
|
+
const block = open.pop()
|
|
134
|
+
if (!block) throw new Error(`${from}: </${closing}> without its <${closing}>`)
|
|
135
|
+
if (block.node.type !== closing) throw new Error(`${from}: </${closing}> where <${block.node.type}> should be closed`)
|
|
136
|
+
// A block on lines of its own is indented a step deeper than where its output goes
|
|
137
|
+
if (block.indent !== null) block.node.dedent = Math.max(0, indentOf(html.slice(block.bodyStart, start)) - block.indent.length)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (open.length) throw new Error(`${from}: <${open[0].node.type}> is never closed`)
|
|
141
|
+
if (last < html.length) root.push({ type: "text", text: html.slice(last) })
|
|
142
|
+
return root
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {string} text
|
|
147
|
+
* @param {number} spaces
|
|
148
|
+
* @returns {string} Every line with up to `spaces` of its indent taken off
|
|
149
|
+
*/
|
|
150
|
+
const dedent = (text, spaces) => spaces ? text.replace(new RegExp(`^[ \\t]{1,${spaces}}`, "gm"), "") : text
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {any} value
|
|
154
|
+
* @returns {boolean} False for nothing, false, 0, "" and empty lists
|
|
155
|
+
*/
|
|
156
|
+
const truthy = (value) => Array.isArray(value) ? value.length > 0 : Boolean(value)
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Fills in the placeholders and runs the blocks:
|
|
160
|
+
* - <x-each items="projects" as="project">...</x-each> once per item, `index="i"` names its position
|
|
161
|
+
* - <x-if value="project.url">...</x-if> only when it's there, `not="..."` only when it isn't
|
|
162
|
+
* @param {Node[]} nodes
|
|
163
|
+
* @param {Record<string, any>} values
|
|
164
|
+
* @param {Context} context
|
|
165
|
+
* @returns {string}
|
|
166
|
+
*/
|
|
167
|
+
export function renderNodes(nodes, values, context) {
|
|
168
|
+
const { from } = context
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {string} tag
|
|
172
|
+
* @param {string} attr
|
|
173
|
+
* @param {string} name What the attribute says, a name to look up
|
|
174
|
+
*/
|
|
175
|
+
const known = (tag, attr, name) => {
|
|
176
|
+
const bare = name.match(/^\{\{\{?\s*([\w.]+)\s*\}?\}\}$/)?.[1]
|
|
177
|
+
if (bare) throw new Error(`${from}: <${tag} ${attr}="${name}"> takes the name on its own: ${attr}="${bare}"`)
|
|
178
|
+
const value = find(values, name, context)
|
|
179
|
+
if (!(name.split(".")[0] in values)) throw new Error(`${from}: <${tag}> has nothing called ${name.split(".")[0]}`)
|
|
180
|
+
return value
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return nodes.map(node => {
|
|
184
|
+
if (node.type === "text") return fillPlaceholders(node.text, values, from, context.used)
|
|
185
|
+
|
|
186
|
+
const { attrs } = node
|
|
187
|
+
if (node.type === "x-each") {
|
|
188
|
+
if (typeof attrs.items !== "string" || typeof attrs.as !== "string") throw new Error(`${from}: <x-each> needs items="..." and as="..."`)
|
|
189
|
+
const items = known("x-each", "items", attrs.items)
|
|
190
|
+
if (!Array.isArray(items)) throw new Error(`${from}: <x-each items="${attrs.items}"> isn't a list`)
|
|
191
|
+
const { as, index } = attrs
|
|
192
|
+
for (const [attr, name] of Object.entries({ as, index })) {
|
|
193
|
+
if (typeof name === "string" && !/^\w+$/.test(name)) throw new Error(`${from}: <x-each ${attr}="${name}"> names what's looped over, a plain name like ${attr}="${attr === "as" ? "project" : "i"}"`)
|
|
194
|
+
}
|
|
195
|
+
return dedent(items.map((item, i) => renderNodes(node.children, {
|
|
196
|
+
...values,
|
|
197
|
+
[as]: item,
|
|
198
|
+
...typeof index === "string" && { [index]: i }
|
|
199
|
+
}, context)).join(""), node.dedent)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const name = attrs.value ?? attrs.not
|
|
203
|
+
if (typeof name !== "string") throw new Error(`${from}: <x-if> needs value="..." or not="..."`)
|
|
204
|
+
const shown = truthy(known("x-if", attrs.value !== undefined ? "value" : "not", name)) === (attrs.value !== undefined)
|
|
205
|
+
return shown ? dedent(renderNodes(node.children, values, context), node.dedent) : ""
|
|
206
|
+
}).join("")
|
|
207
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @param {import("./config.js").Site} site */
|
|
2
|
+
export function clean(site: import("./config.js").Site): Promise<void>;
|
|
3
|
+
/**
|
|
4
|
+
* Copies src as it is into the build, leaving out the excluded folders, pages and Sass.
|
|
5
|
+
* @param {import("./config.js").Site} site
|
|
6
|
+
*/
|
|
7
|
+
export function copyAssets(site: import("./config.js").Site): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* The esbuild builds for the config's js: one per script, and one for all the modules, so they can share chunks.
|
|
10
|
+
* @param {import("./config.js").Site} site
|
|
11
|
+
* @param {import("esbuild").BuildOptions} [overrides]
|
|
12
|
+
* @returns {import("esbuild").BuildOptions[]}
|
|
13
|
+
*/
|
|
14
|
+
export function jsBuilds(site: import("./config.js").Site, overrides?: import("esbuild").BuildOptions): import("esbuild").BuildOptions[];
|
|
15
|
+
/**
|
|
16
|
+
* Bundles the JS, and notes which chunks each file imports, for the modulepreload links.
|
|
17
|
+
* @param {import("./config.js").Site} site
|
|
18
|
+
* @param {import("./render.js").BuildState} state
|
|
19
|
+
*/
|
|
20
|
+
export function buildJS(site: import("./config.js").Site, state: import("./render.js").BuildState): Promise<void>;
|
|
21
|
+
/** @param {import("./config.js").Site} site */
|
|
22
|
+
export function buildSass(site: import("./config.js").Site): Promise<void>;
|
|
23
|
+
export function isExcluded(site: import("./config.js").Site, file: string): boolean;
|
package/types/build.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes a file unless it already has this content. Writing is much slower than reading on Windows, and a dev render
|
|
3
|
+
* of a whole site mostly writes what's already there.
|
|
4
|
+
* @param {string} file
|
|
5
|
+
* @param {string} content
|
|
6
|
+
* @returns {boolean} Whether it was written
|
|
7
|
+
*/
|
|
8
|
+
export function writeIfChanged(file: string, content: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* @param {import("./config.js").Site} site
|
|
11
|
+
* @param {import("./render.js").BuildState} state
|
|
12
|
+
* @param {{ hashed?: boolean, only?: Set<string> }} [options] `hashed` for production: hashed asset links and minified
|
|
13
|
+
* HTML. `only` renders just these pages, relative to the pages folder
|
|
14
|
+
* @returns {Promise<import("./routes.js").Route[]>} The pages rendered
|
|
15
|
+
*/
|
|
16
|
+
export function buildHTML(site: import("./config.js").Site, state: import("./render.js").BuildState, { hashed, only }?: {
|
|
17
|
+
hashed?: boolean;
|
|
18
|
+
only?: Set<string>;
|
|
19
|
+
}): Promise<import("./routes.js").Route[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Renders the pages into an existing build, as dev does after a change. With `only`, just those pages, and the
|
|
22
|
+
* sitemaps are left as they are.
|
|
23
|
+
* @param {import("./config.js").Site} site
|
|
24
|
+
* @param {{ only?: Set<string> }} [options]
|
|
25
|
+
* @returns {Promise<Record<string, string[]>>} What each page rendered is made from, see deps.js
|
|
26
|
+
*/
|
|
27
|
+
export function renderSite(site: import("./config.js").Site, { only }?: {
|
|
28
|
+
only?: Set<string>;
|
|
29
|
+
}): Promise<Record<string, string[]>>;
|
|
30
|
+
/**
|
|
31
|
+
* The production build, from an empty build folder.
|
|
32
|
+
* @param {import("./config.js").Site} site
|
|
33
|
+
*/
|
|
34
|
+
export function build(site: import("./config.js").Site): Promise<void>;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Config} config
|
|
3
|
+
* @param {string} root The config's folder
|
|
4
|
+
* @returns {Site}
|
|
5
|
+
*/
|
|
6
|
+
export function resolveConfig(config: Config, root: string): Site;
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} [root] The folder with mothwing.config.js
|
|
9
|
+
* @returns {Promise<Site>}
|
|
10
|
+
*/
|
|
11
|
+
export function loadConfig(root?: string): Promise<Site>;
|
|
12
|
+
export const CONFIG_FILE: "mothwing.config.js";
|
|
13
|
+
export function defineConfig(config: Config): Config;
|
|
14
|
+
/**
|
|
15
|
+
* What a site's mothwing.config.js default exports. Only `domain` is needed. Paths are relative to `src` unless they
|
|
16
|
+
* say otherwise. The extras (minify to activeLinks) are off until asked for: `true` turns one on as it comes, an object
|
|
17
|
+
* with its own settings.
|
|
18
|
+
*/
|
|
19
|
+
export type Config = {
|
|
20
|
+
/**
|
|
21
|
+
* "example.com", for the canonical URLs, sitemaps and CNAME
|
|
22
|
+
*/
|
|
23
|
+
domain: string;
|
|
24
|
+
/**
|
|
25
|
+
* The source folder, relative to the config. "src"
|
|
26
|
+
*/
|
|
27
|
+
src?: string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* The build folder, relative to the config. "dist"
|
|
30
|
+
*/
|
|
31
|
+
out?: string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* The pages folder. "pages"
|
|
34
|
+
*/
|
|
35
|
+
pages?: string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* The data folder, its files are in every page by their name: projects.json -> {{ projects }}.
|
|
38
|
+
* "data"
|
|
39
|
+
*/
|
|
40
|
+
data?: string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The 404 page, in the pages folder. Kept in the build's root, out of the sitemap. "404.html"
|
|
43
|
+
*/
|
|
44
|
+
notFound?: string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The JS to bundle
|
|
47
|
+
*/
|
|
48
|
+
js?: JsConfig | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Sass to compile, source to output: { "scss/index.scss": "css/index.css" }
|
|
51
|
+
*/
|
|
52
|
+
sass?: Record<string, string> | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Folders left out of the build, on top of pages, partials and data: ["js", "scss"]
|
|
55
|
+
*/
|
|
56
|
+
exclude?: string[] | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Folders the pages read with fs, whose changes render every page again in dev. What
|
|
59
|
+
* the pages import, include or use from the data folder is followed on its own
|
|
60
|
+
*/
|
|
61
|
+
rerender?: string[] | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* For <x-seo>
|
|
64
|
+
*/
|
|
65
|
+
seo?: SeoConfig | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Minify the HTML of production builds
|
|
68
|
+
*/
|
|
69
|
+
minify?: boolean | import("html-minifier-terser").Options | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* A Content-Security-Policy <meta> that allows only the site's own
|
|
72
|
+
* files and inline scripts. An object sets directives over the defaults: { "style-src": "'self' 'unsafe-inline'" }
|
|
73
|
+
*/
|
|
74
|
+
csp?: boolean | Record<string, string> | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* sitemap.xml
|
|
77
|
+
*/
|
|
78
|
+
sitemap?: boolean | SitemapConfig | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* robots.txt
|
|
81
|
+
*/
|
|
82
|
+
robots?: boolean | RobotsConfig | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* A CNAME file with the domain, for GitHub Pages
|
|
85
|
+
*/
|
|
86
|
+
cname?: boolean | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* aria-current on links to the page they're on and to its section
|
|
89
|
+
*/
|
|
90
|
+
activeLinks?: boolean | ActiveLink | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Your own tags: { "x-name": ({ attrs }) => "<b>html</b>" }
|
|
93
|
+
*/
|
|
94
|
+
tags?: Record<string, TagFunction> | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Run on every page's HTML once its placeholders are filled
|
|
97
|
+
*/
|
|
98
|
+
transforms?: Transform[] | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* Run once the pages are written
|
|
101
|
+
*/
|
|
102
|
+
after?: Hook[] | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* esbuild options for every bundle, over mothwing's
|
|
105
|
+
*/
|
|
106
|
+
esbuild?: import("esbuild").BuildOptions | undefined;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Each file is bundled to the same path in the build, js/index.js -> /js/index.js.
|
|
110
|
+
*/
|
|
111
|
+
export type JsConfig = {
|
|
112
|
+
/**
|
|
113
|
+
* For <x-js src="..."/>, bundled on their own
|
|
114
|
+
*/
|
|
115
|
+
scripts?: string[] | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* For <x-js src="..." module/>, ES modules that share their common code in chunks
|
|
118
|
+
*/
|
|
119
|
+
modules?: string[] | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* Where the shared chunks go. A chunks folder next to the first module
|
|
122
|
+
*/
|
|
123
|
+
chunks?: string | undefined;
|
|
124
|
+
};
|
|
125
|
+
export type SeoConfig = {
|
|
126
|
+
/**
|
|
127
|
+
* Added to titles that fit, and the link previews' site name. The domain
|
|
128
|
+
*/
|
|
129
|
+
siteName?: string | undefined;
|
|
130
|
+
/**
|
|
131
|
+
* "en_US"
|
|
132
|
+
*/
|
|
133
|
+
locale?: string | undefined;
|
|
134
|
+
/**
|
|
135
|
+
* The link preview for pages
|
|
136
|
+
* without their own
|
|
137
|
+
*/
|
|
138
|
+
image?: {
|
|
139
|
+
src: string;
|
|
140
|
+
width: number;
|
|
141
|
+
height: number;
|
|
142
|
+
alt: string;
|
|
143
|
+
} | null | undefined;
|
|
144
|
+
/**
|
|
145
|
+
* Titles longer than this leave out the site name, search results cut them off. 60
|
|
146
|
+
*/
|
|
147
|
+
maxTitle?: number | undefined;
|
|
148
|
+
/**
|
|
149
|
+
* Longer descriptions are cut at a word. 160
|
|
150
|
+
*/
|
|
151
|
+
maxDescription?: number | undefined;
|
|
152
|
+
};
|
|
153
|
+
export type SitemapConfig = {
|
|
154
|
+
/**
|
|
155
|
+
* Sections that get a sitemap of their own, the rest share sitemap-pages.xml
|
|
156
|
+
*/
|
|
157
|
+
sections?: string[] | undefined;
|
|
158
|
+
/**
|
|
159
|
+
* Date each page by the last git commit to its files. true
|
|
160
|
+
*/
|
|
161
|
+
dates?: boolean | undefined;
|
|
162
|
+
};
|
|
163
|
+
export type RobotsConfig = {
|
|
164
|
+
/**
|
|
165
|
+
* Crawlers turned away from the whole site, by name
|
|
166
|
+
*/
|
|
167
|
+
block?: string[] | undefined;
|
|
168
|
+
/**
|
|
169
|
+
* Paths kept out of every crawler
|
|
170
|
+
*/
|
|
171
|
+
disallow?: string[] | undefined;
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Returns the HTML a tag turns into. A list is one line each, indented like the tag.
|
|
175
|
+
*/
|
|
176
|
+
export type TagFunction = (tag: {
|
|
177
|
+
attrs: Record<string, string | true>;
|
|
178
|
+
route: import("./routes.js").Route;
|
|
179
|
+
site: Site;
|
|
180
|
+
}) => string | string[];
|
|
181
|
+
/**
|
|
182
|
+
* Returns the HTML a tag turns into. A list is one line each, indented like the tag.
|
|
183
|
+
*/
|
|
184
|
+
export type Transform = (html: string, route: import("./routes.js").Route, site: Site) => string;
|
|
185
|
+
/**
|
|
186
|
+
* Returns the HTML a tag turns into. A list is one line each, indented like the tag.
|
|
187
|
+
*/
|
|
188
|
+
export type Hook = (build: {
|
|
189
|
+
site: Site;
|
|
190
|
+
routes: import("./routes.js").Route[];
|
|
191
|
+
dev: boolean;
|
|
192
|
+
}) => any;
|
|
193
|
+
/**
|
|
194
|
+
* Decides what a link on a page is to it: "page" for the page itself, "section" for a section it's in, false for
|
|
195
|
+
* neither. undefined leaves it to the default.
|
|
196
|
+
*/
|
|
197
|
+
export type ActiveLink = (href: string, route: import("./routes.js").Route) => "page" | "section" | false | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* The config with its defaults filled in, its folders absolute, and each extra either false or its settings.
|
|
200
|
+
*/
|
|
201
|
+
export type Site = {
|
|
202
|
+
domain: string;
|
|
203
|
+
/**
|
|
204
|
+
* "https://example.com"
|
|
205
|
+
*/
|
|
206
|
+
url: string;
|
|
207
|
+
/**
|
|
208
|
+
* The config's folder
|
|
209
|
+
*/
|
|
210
|
+
root: string;
|
|
211
|
+
src: string;
|
|
212
|
+
out: string;
|
|
213
|
+
pages: string;
|
|
214
|
+
/**
|
|
215
|
+
* Relative to src
|
|
216
|
+
*/
|
|
217
|
+
data: string;
|
|
218
|
+
notFound: string;
|
|
219
|
+
js: Required<JsConfig>;
|
|
220
|
+
sass: Record<string, string>;
|
|
221
|
+
/**
|
|
222
|
+
* Folders, with pages, partials and data
|
|
223
|
+
*/
|
|
224
|
+
exclude: string[];
|
|
225
|
+
/**
|
|
226
|
+
* Folders
|
|
227
|
+
*/
|
|
228
|
+
rerender: string[];
|
|
229
|
+
seo: Required<SeoConfig>;
|
|
230
|
+
minify: import("html-minifier-terser").Options | false;
|
|
231
|
+
csp: Record<string, string> | false;
|
|
232
|
+
sitemap: Required<SitemapConfig> | false;
|
|
233
|
+
robots: Required<RobotsConfig> | false;
|
|
234
|
+
cname: boolean;
|
|
235
|
+
activeLinks: ActiveLink | false;
|
|
236
|
+
tags: Record<string, TagFunction>;
|
|
237
|
+
transforms: Transform[];
|
|
238
|
+
after: Hook[];
|
|
239
|
+
esbuild: import("esbuild").BuildOptions;
|
|
240
|
+
};
|
package/types/csp.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds a Content-Security-Policy <meta>, for hosts like GitHub Pages that can't send headers. The inline scripts are
|
|
3
|
+
* allowed by their hash, so it has to run on the finished HTML, after minifying changes them.
|
|
4
|
+
* @param {Record<string, string>} directives
|
|
5
|
+
* @param {string} html The finished page
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
export function withCsp(directives: Record<string, string>, html: string): string;
|
package/types/data.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import("./config.js").Site} site
|
|
3
|
+
* @returns {Promise<SiteData>}
|
|
4
|
+
*/
|
|
5
|
+
export function loadData(site: import("./config.js").Site): Promise<SiteData>;
|
|
6
|
+
/**
|
|
7
|
+
* The site's data folder, for every page.
|
|
8
|
+
*/
|
|
9
|
+
export type SiteData = {
|
|
10
|
+
/**
|
|
11
|
+
* projects.json -> values.projects, a folder is an object of its files
|
|
12
|
+
*/
|
|
13
|
+
values: Record<string, any>;
|
|
14
|
+
/**
|
|
15
|
+
* Each top level name -> its file or folder, relative to src
|
|
16
|
+
*/
|
|
17
|
+
files: Record<string, string>;
|
|
18
|
+
};
|
package/types/deps.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files each page is made from, so dev can render just the pages a change is in: the page, its partials, its
|
|
3
|
+
* .data.js and what that imports, its sources, and the data files it used.
|
|
4
|
+
* @param {import("./config.js").Site} site
|
|
5
|
+
* @param {import("./render.js").BuildState} state
|
|
6
|
+
* @param {import("./routes.js").Route[]} routes
|
|
7
|
+
* @returns {Promise<Record<string, string[]>>} A page's file, relative to the pages folder -> files relative to src
|
|
8
|
+
*/
|
|
9
|
+
export function dependencies(site: import("./config.js").Site, state: import("./render.js").BuildState, routes: import("./routes.js").Route[]): Promise<Record<string, string[]>>;
|
|
10
|
+
export function pagesUsing(deps: Record<string, string[]>, file: string): string[];
|
package/types/dev.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds, serves and rebuilds what changes.
|
|
3
|
+
* @param {import("./config.js").Site} site
|
|
4
|
+
* @param {{ host?: string, port?: number }} [options]
|
|
5
|
+
*/
|
|
6
|
+
export function dev(site: import("./config.js").Site, { host, port }?: {
|
|
7
|
+
host?: string;
|
|
8
|
+
port?: number;
|
|
9
|
+
}): Promise<void>;
|
package/types/html.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** HTML that's safe to put in as-is. */
|
|
2
|
+
export class Html {
|
|
3
|
+
/** @param {string} value */
|
|
4
|
+
constructor(value: string);
|
|
5
|
+
value: string;
|
|
6
|
+
toString(): string;
|
|
7
|
+
}
|
|
8
|
+
export function escapeHtml(text: string): string;
|
|
9
|
+
export function unescapeHtml(html: string): string;
|
|
10
|
+
export function raw(value: string): Html;
|
|
11
|
+
export function slug(name: string): string;
|
|
12
|
+
export function html(strings: TemplateStringsArray, ...values: any[]): Html;
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { clean } from "./assets.js";
|
|
2
|
+
export { dev } from "./dev.js";
|
|
3
|
+
export { log } from "./log.js";
|
|
4
|
+
export type Config = import("./config.js").Config;
|
|
5
|
+
export type Site = import("./config.js").Site;
|
|
6
|
+
export type JsConfig = import("./config.js").JsConfig;
|
|
7
|
+
export type SeoConfig = import("./config.js").SeoConfig;
|
|
8
|
+
export type SitemapConfig = import("./config.js").SitemapConfig;
|
|
9
|
+
export type RobotsConfig = import("./config.js").RobotsConfig;
|
|
10
|
+
export type ActiveLink = import("./config.js").ActiveLink;
|
|
11
|
+
export type TagFunction = import("./config.js").TagFunction;
|
|
12
|
+
export type Transform = import("./config.js").Transform;
|
|
13
|
+
export type Hook = import("./config.js").Hook;
|
|
14
|
+
export type Route = import("./routes.js").Route;
|
|
15
|
+
export { build, renderSite } from "./build.js";
|
|
16
|
+
export { defineConfig, loadConfig, resolveConfig } from "./config.js";
|
|
17
|
+
export { escapeHtml, html, raw, unescapeHtml } from "./html.js";
|
package/types/log.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import("./config.js").Site} site
|
|
3
|
+
* @param {BuildState} state
|
|
4
|
+
* @param {import("./routes.js").Route} route
|
|
5
|
+
* @param {boolean} hashed Add ?v=hash to the assets
|
|
6
|
+
* @returns {string} The finished HTML, before minifying
|
|
7
|
+
*/
|
|
8
|
+
export function renderPage(site: import("./config.js").Site, state: BuildState, route: import("./routes.js").Route, hashed: boolean): string;
|
|
9
|
+
export function buildState(): BuildState;
|
|
10
|
+
/**
|
|
11
|
+
* What one build has worked out so far, shared by its pages.
|
|
12
|
+
*/
|
|
13
|
+
export type BuildState = {
|
|
14
|
+
/**
|
|
15
|
+
* The data folder
|
|
16
|
+
*/
|
|
17
|
+
data: import("./data.js").SiteData;
|
|
18
|
+
/**
|
|
19
|
+
* "/js/index.js" -> "/js/index.js?v=hash"
|
|
20
|
+
*/
|
|
21
|
+
assetUrls: Map<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* A page's file ->
|
|
24
|
+
* its HTML with the partials pasted in, and which those were
|
|
25
|
+
*/
|
|
26
|
+
templates: Map<string, {
|
|
27
|
+
nodes: import("./template.js").Node[];
|
|
28
|
+
partials: Set<string>;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* A page's file -> the names its pages filled in, for what they depend on
|
|
32
|
+
*/
|
|
33
|
+
used: Map<string, Set<string>>;
|
|
34
|
+
/**
|
|
35
|
+
* "/js/gathering.js" -> the chunks it imports up front
|
|
36
|
+
*/
|
|
37
|
+
chunkImports: Map<string, string[]>;
|
|
38
|
+
/**
|
|
39
|
+
* "/js/data/x.json" -> "/js/chunks/x-hash.js"
|
|
40
|
+
*/
|
|
41
|
+
entryChunks: Map<string, string>;
|
|
42
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every page, each folder's index.html first, so the front page leads the sitemap and a list comes before its items.
|
|
3
|
+
* - about.html is one page. Its .data.js, when it has one, default exports `() => values`
|
|
4
|
+
* - [projects.name].html is a page per item of the data folder's projects, by its name, the item is {{ page }}
|
|
5
|
+
* - [name].html is a page per item its .data.js returns, each `{ slug, ...values }`
|
|
6
|
+
* A .data.js can return a `sources` list (relative to src) of the files it's made from, for the sitemap's dates.
|
|
7
|
+
* @param {import("./config.js").Site} site
|
|
8
|
+
* @param {import("./data.js").SiteData} siteData
|
|
9
|
+
* @param {Set<string>} [only] Just these pages, relative to the pages folder
|
|
10
|
+
* @returns {Promise<Route[]>}
|
|
11
|
+
*/
|
|
12
|
+
export function listRoutes(site: import("./config.js").Site, siteData: import("./data.js").SiteData, only?: Set<string>): Promise<Route[]>;
|
|
13
|
+
export function outputUrl(output: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* A page to write. Most come one to a file in the pages folder, a [name].html file gives one per item.
|
|
16
|
+
* Its props can have `files`, more files to write, by their path in the build.
|
|
17
|
+
*/
|
|
18
|
+
export type Route = {
|
|
19
|
+
/**
|
|
20
|
+
* "gathering/[item].html", relative to the pages folder
|
|
21
|
+
*/
|
|
22
|
+
source: string;
|
|
23
|
+
/**
|
|
24
|
+
* Where it goes in the build: "gathering/cedar-log/index.html"
|
|
25
|
+
*/
|
|
26
|
+
output: string;
|
|
27
|
+
/**
|
|
28
|
+
* "/gathering/cedar-log/"
|
|
29
|
+
*/
|
|
30
|
+
url: string;
|
|
31
|
+
/**
|
|
32
|
+
* Its own values, the data folder's are added when it's rendered
|
|
33
|
+
*/
|
|
34
|
+
props: Record<string, any>;
|
|
35
|
+
/**
|
|
36
|
+
* Files its content comes from, relative to src, for the sitemap's lastmod
|
|
37
|
+
*/
|
|
38
|
+
sources: string[];
|
|
39
|
+
};
|