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.
@@ -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;
@@ -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;
@@ -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
+ };
@@ -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>;
@@ -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;
@@ -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,6 @@
1
+ /**
2
+ * @param {string} type Short tag, like "HTML"
3
+ * @param {string} message
4
+ */
5
+ export function log(type: string, message: string): void;
6
+ export function posix(file: string): string;
@@ -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
+ };