mothwing 0.0.0-stage → 0.0.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AlexFlipnote
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,160 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # mothwing
2
+ A small static site builder for pages as light as a moth's wing. Plain HTML in, plain HTML out, with hashed asset links and modulepreloads for code split JS. The rest is there when you ask for it: SEO tags, a Content-Security-Policy that allows only your own inline scripts, sitemaps dated from git, robots.txt and minified HTML.
3
+
4
+ It was born as the build script of [ffxiv.watch](https://ffxiv.watch), and got tweaked over time until I was a little too proud of it to keep it there. So it branched out into its own thing, for any site that wants the same.
5
+
6
+ ## What it isn't
7
+ mothwing isn't a framework. There are no components, and nothing runs in the browser that you didn't write. If you want those, Astro and SvelteKit are great.
8
+
9
+ I did try them, and others, before making my own. They're good at what they do, but I wanted full control of the output: every byte of HTML exactly as I wrote it, and as few dependencies as possible between my files and the finished pages. mothwing is for when you want the same.
10
+
11
+ ## Getting started
12
+ ```sh
13
+ npm install --save-dev mothwing
14
+ npx mothwing dev # serves on http://localhost:8080, a change renders just the pages it's in
15
+ npx mothwing build # production build into dist/
16
+ ```
17
+
18
+ A site is a `mothwing.config.js` and a `src/` folder. The build mirrors `src/`, so `src/images/logo.png` is `/images/logo.png`:
19
+
20
+ ```
21
+ mothwing.config.js
22
+ src/
23
+ pages/ every page, the folders are the URLs: about.html -> /about/
24
+ partials/ bits of HTML the pages share
25
+ data/ JSON and JS the pages are filled from
26
+ js/ bundled by esbuild
27
+ ... everything else is copied as it is
28
+ ```
29
+
30
+ ```js
31
+ import { defineConfig } from "mothwing"
32
+
33
+ export default defineConfig({
34
+ domain: "example.com",
35
+ js: { scripts: ["js/index.js"] },
36
+ exclude: ["js"]
37
+ })
38
+ ```
39
+
40
+ `defineConfig` does nothing but give your editor the config's types, so every option autocompletes and a typo is underlined. Without the import, a JSDoc comment does the same:
41
+
42
+ ```js
43
+ /** @type {import("mothwing").Config} */
44
+ export default { ... }
45
+ ```
46
+
47
+ An option mothwing doesn't know fails the build either way.
48
+
49
+ ## Data
50
+ Every file in `src/data/` is in every page by its name. `projects.json` is `{{ projects }}`, a folder is an object of its files. A `.js` file's default export is its value, called first when it's a function, for data worked out at build time:
51
+
52
+ ```js
53
+ // src/data/site.js
54
+ export default () => ({ year: new Date().getFullYear() })
55
+ ```
56
+
57
+ ```html
58
+ <h1>{{ title }}</h1> <!-- as text, escaped -->
59
+ {{{ intro }}} <!-- as HTML, every line indented like the placeholder -->
60
+ <footer>{{ site.year }}</footer> <!-- into objects with dots -->
61
+ ```
62
+
63
+ ## Loops
64
+ ```html
65
+ <ul>
66
+ <x-each items="projects" as="project">
67
+ <li>
68
+ <x-if value="project.url"><a href="{{ project.url }}">{{ project.name }}</a></x-if>
69
+ <x-if not="project.url">{{ project.name }}</x-if>
70
+ </li>
71
+ </x-each>
72
+ </ul>
73
+ ```
74
+
75
+ `<x-each>` repeats its content per item, `index="i"` names the item's position. `<x-if value="...">` shows its content when the value is there and not empty, `not="..."` when it isn't. On lines of their own, they leave no empty lines behind and their content keeps the indent it would have by hand.
76
+
77
+ `items`, `value` and `not` take a name as it is, `items="projects"`, not a placeholder. The rest of the tags' attributes can have placeholders, `<x-seo title="{{ page.name }}"/>`.
78
+
79
+ Every tag is ordinary HTML to an editor or formatter. Templates formatted by Prettier build to the same pages.
80
+
81
+ ## Pages
82
+ `src/pages/about.html` is `/about/`, `src/pages/blog/index.html` is `/blog/`.
83
+
84
+ `[projects.id].html` makes a page per item of `projects` in the data folder, with its `id` as the URL: `src/pages/projects/[projects.id].html` is `/projects/hexclock/` for `{ "id": "hexclock" }`. The item is `{{ page }}`, and the list links to the pages by the same field, `/projects/{{ project.id }}/`.
85
+
86
+ ### .data.js
87
+ For values that take more than the data folder, a page can have a `.data.js` next to it, whose default export returns them. Its values come before the data folder's:
88
+
89
+ ```js
90
+ // src/pages/index.data.js
91
+ export default () => ({ updated: new Date().toISOString() })
92
+ ```
93
+
94
+ `[name].html` with a `.data.js` makes a page per item that returns, each with a `slug`. `posts/[post].html` with a slug of `first` becomes `/posts/first/`. Besides its values, an item can have:
95
+
96
+ | Key | |
97
+ | --- | --- |
98
+ | `slug` | The page's URL part |
99
+ | `sources` | The files its content comes from, relative to `src/`, which the sitemap dates the page by |
100
+ | `files` | More files to write, by their path in the build: `{ "rows/list.html": "..." }` |
101
+
102
+ For building HTML in a `.data.js`, `mothwing/html` has an `html` tagged template, which escapes what goes into it unless it's HTML already, and `slug()`.
103
+
104
+ ## Tags
105
+ Paths in tags start at `src/`, the same as the URLs in the build.
106
+
107
+ | Tag | Does |
108
+ | --- | ---- |
109
+ | `<x-html src="partials/nav.html"/>` | Pastes in a partial, indented like the tag |
110
+ | `<x-js src="/js/index.js"/>` | `<script>` with a `?v=hash` |
111
+ | `<x-js src="/js/app.js" module/>` | One of your `modules`, with a modulepreload for each chunk it imports. `preload="/js/data/x.js"` adds one for a file it `import()`s later |
112
+ | `<x-css src="/css/index.css"/>` | Stylesheet `<link>` with a `?v=hash` |
113
+ | `<x-seo title="..." description="..."/>` | Title, description, canonical URL, link preview tags. `image`, `image-alt` and `image-size="WxH"` for its own preview, `section` and `name` for breadcrumbs in search results |
114
+
115
+ A tag mothwing doesn't know, or one missing what it needs, fails the build with the page it's on.
116
+
117
+ ## Config
118
+ Only `domain` is needed. Folders are relative to `src/`.
119
+
120
+ | Option | Default | |
121
+ | --- | --- | --- |
122
+ | `src`, `out` | `"src"`, `"dist"` | Relative to the config |
123
+ | `pages` | `"pages"` | |
124
+ | `data` | `"data"` | Its files are in every page, it isn't copied to the build |
125
+ | `notFound` | `"404.html"` | Kept in the build's root, out of the sitemap |
126
+ | `js` | | `scripts` are bundled on their own, `modules` together, sharing their common code in chunks. `chunks` sets the chunks' folder, next to the modules by default |
127
+ | `sass` | `{}` | Sass to compile, `{ "scss/index.scss": "css/index.css" }`, needs `sass` installed |
128
+ | `exclude` | `[]` | Folders left out of the build, like the JS sources: `["js"]`. Pages and partials always are |
129
+ | `rerender` | `[]` | Folders your `.data.js` reads with `fs`, whose changes render every page in dev. What pages import, include or use from the data folder is followed on its own |
130
+ | `seo` | | For `<x-seo>`: `siteName`, `locale`, `image` (the default preview), `maxTitle`, `maxDescription` |
131
+ | `esbuild` | `{}` | esbuild options for every bundle |
132
+
133
+ ### Extras
134
+ Off until you turn them on. `true` turns one on as it comes, an object changes how it works.
135
+
136
+ | Option | Turned on | Object |
137
+ | --- | --- | --- |
138
+ | `minify` | Minifies the HTML of production builds | html-minifier-terser options |
139
+ | `csp` | A `<meta>` Content-Security-Policy: only your own files, and your inline scripts by their hash | Directives over the defaults, `{ "style-src": "'self' 'unsafe-inline'" }` |
140
+ | `sitemap` | `sitemap.xml`, each page dated by the last commit to its files | `sections` that get a sitemap each, `dates: false` to leave git out |
141
+ | `robots` | `robots.txt` that allows everything, pointing at the sitemap | `block` crawlers by name, `disallow` paths |
142
+ | `cname` | `CNAME` with the domain, for GitHub Pages | |
143
+ | `activeLinks` | `aria-current="page"` on links to the page they're on, `"true"` on links to its section | A function `(href, route)` returning `"page"`, `"section"` or `false`, `undefined` keeps the default |
144
+
145
+ ## Making it yours
146
+ For what's only your site's:
147
+
148
+ ```js
149
+ export default defineConfig({
150
+ domain: "example.com",
151
+ // <x-emoji name="coffee"/>
152
+ tags: {
153
+ "x-emoji": ({ attrs }) => `<img src="/emoji/${attrs.name}.png" alt="">`
154
+ },
155
+ // Every page, after its placeholders are filled
156
+ transforms: [(html, route) => html.replace("<body>", `<body data-page="${route.url}">`)],
157
+ // Once the pages are written, `dev` is true on dev renders
158
+ after: [async ({ site, routes, dev }) => { /* draw preview images, ... */ }]
159
+ })
160
+ ```
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import { build, clean, dev, loadConfig, renderSite } from "../src/index.js"
3
+
4
+ const USAGE = `Usage: mothwing <command> [--root <dir>]
5
+
6
+ Commands:
7
+ build Build the site for production
8
+ dev Serve it and rebuild on changes, takes --host and --port
9
+ clean Remove the build folder
10
+ html Render only the pages, into an existing build, --pages a.html,b.html for just those`
11
+
12
+ /**
13
+ * @param {string} name
14
+ * @returns {string | undefined}
15
+ */
16
+ const flag = (name) => {
17
+ const i = process.argv.indexOf(`--${name}`)
18
+ return i > -1 ? process.argv[i + 1] : undefined
19
+ }
20
+
21
+ /** @type {Record<string, (site: import("../src/index.js").Site) => Promise<void>>} */
22
+ const commands = {
23
+ build: (site) => build(site),
24
+ dev: (site) => dev(site, { host: flag("host"), port: Number(flag("port")) || undefined }),
25
+ clean: (site) => clean(site),
26
+ html: async (site) => {
27
+ const pages = flag("pages")
28
+ const deps = await renderSite(site, { only: pages ? new Set(pages.split(",")) : undefined })
29
+ // Run by dev, which keeps track of what the pages are made from
30
+ process.send?.({ deps })
31
+ }
32
+ }
33
+ const command = commands[process.argv[2]]
34
+
35
+ if (!command) {
36
+ console.log(USAGE)
37
+ process.exit(process.argv[2] ? 1 : 0)
38
+ }
39
+
40
+ try {
41
+ await command(await loadConfig(flag("root")))
42
+ } catch (err) {
43
+ console.error(`mothwing: ${/** @type {Error} */ (err).message}`)
44
+ process.exit(1)
45
+ }
package/package.json CHANGED
@@ -1,6 +1,73 @@
1
- {
2
- "name": "mothwing",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "mothwing",
3
+ "version": "0.0.2",
4
+ "description": "A small static site builder for pages as light as a moth's wing",
5
+ "keywords": [
6
+ "static-site-generator",
7
+ "ssg",
8
+ "html",
9
+ "esbuild",
10
+ "csp",
11
+ "sitemap"
12
+ ],
13
+ "author": "AlexFlipnote",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/AlexFlipnote/mothwing.git"
18
+ },
19
+ "homepage": "https://github.com/AlexFlipnote/mothwing#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/AlexFlipnote/mothwing/issues"
22
+ },
23
+ "type": "module",
24
+ "bin": {
25
+ "mothwing": "bin/mothwing.js"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./types/index.d.ts",
30
+ "default": "./src/index.js"
31
+ },
32
+ "./html": {
33
+ "types": "./types/html.d.ts",
34
+ "default": "./src/html.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "bin",
39
+ "src",
40
+ "types"
41
+ ],
42
+ "engines": {
43
+ "node": ">=20.11"
44
+ },
45
+ "scripts": {
46
+ "test": "node --test test/*.test.js",
47
+ "lint": "eslint .",
48
+ "types": "tsc src/index.js src/html.js --allowJs --declaration --emitDeclarationOnly --module nodenext --moduleResolution nodenext --target es2022 --strict --types node --skipLibCheck --outDir types",
49
+ "prepack": "npm run types",
50
+ "typecheck": "tsc -p jsconfig.json"
51
+ },
52
+ "dependencies": {
53
+ "@types/html-minifier-terser": "^7.0.2",
54
+ "esbuild": "0.28.1",
55
+ "html-minifier-terser": "7.2.0"
56
+ },
57
+ "peerDependencies": {
58
+ "sass": "^1.93.3"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "sass": {
62
+ "optional": true
63
+ }
64
+ },
65
+ "devDependencies": {
66
+ "@eslint/js": "^9.39.5",
67
+ "@types/node": "^22.20.5",
68
+ "eslint": "^9.39.5",
69
+ "globals": "^16.5.0",
70
+ "sass": "^1.105.2",
71
+ "typescript": "^6.0.3"
72
+ }
73
+ }
package/src/assets.js ADDED
@@ -0,0 +1,122 @@
1
+ import esbuild from "esbuild"
2
+ import fs from "fs"
3
+ import path from "path"
4
+ import { log, posix } from "./log.js"
5
+
6
+ /**
7
+ * @param {import("./config.js").Site} site
8
+ * @param {string} file Absolute path
9
+ * @returns {boolean} In one of the config's exclude folders
10
+ */
11
+ export const isExcluded = (site, file) => {
12
+ const relative = posix(path.relative(site.src, file))
13
+ return site.exclude.some(dir => relative === dir || relative.startsWith(`${dir}/`))
14
+ }
15
+
16
+ /** @param {import("./config.js").Site} site */
17
+ export async function clean(site) {
18
+ log("CLEAN", "Removing build folder...")
19
+ // Retries, as the dev server can be writing a file into it right then
20
+ await fs.promises.rm(site.out, { recursive: true, force: true, maxRetries: 5 })
21
+ log("CLEAN", "Done")
22
+ }
23
+
24
+ /**
25
+ * Copies src as it is into the build, leaving out the excluded folders, pages and Sass.
26
+ * @param {import("./config.js").Site} site
27
+ */
28
+ export async function copyAssets(site) {
29
+ log("ASSET", "Copying assets...")
30
+ fs.cpSync(site.src, site.out, {
31
+ recursive: true,
32
+ filter: (file) => !isExcluded(site, file) && !/\.(html|scss|sass)$/.test(file)
33
+ })
34
+ log("ASSET", "Done copying assets")
35
+ }
36
+
37
+ /**
38
+ * @param {string[]} files "js/a.js", "js/pages/b.js"
39
+ * @returns {string} The folder they're all in, "js"
40
+ */
41
+ function commonFolder(files) {
42
+ const [first = [], ...rest] = files.map(file => path.posix.dirname(file).split("/"))
43
+ let depth = first.length
44
+ for (const parts of rest) {
45
+ while (depth && parts.slice(0, depth).join("/") !== first.slice(0, depth).join("/")) depth--
46
+ }
47
+ return first.slice(0, depth).join("/").replace(/^\.$/, "")
48
+ }
49
+
50
+ /**
51
+ * The esbuild builds for the config's js: one per script, and one for all the modules, so they can share chunks.
52
+ * @param {import("./config.js").Site} site
53
+ * @param {import("esbuild").BuildOptions} [overrides]
54
+ * @returns {import("esbuild").BuildOptions[]}
55
+ */
56
+ export function jsBuilds(site, overrides = {}) {
57
+ /** @param {import("esbuild").BuildOptions} build */
58
+ const options = (build) => ({
59
+ bundle: true,
60
+ minify: true,
61
+ sourcemap: false,
62
+ target: ["es2020"],
63
+ ...site.esbuild,
64
+ ...build,
65
+ ...overrides
66
+ })
67
+ const { scripts, modules, chunks } = site.js
68
+ // From the modules' own folder, as esbuild's chunk hashes depend on the paths the build is laid out with
69
+ const base = commonFolder(modules)
70
+
71
+ return [
72
+ ...scripts.map(file => options({ entryPoints: [path.join(site.src, file)], outfile: path.join(site.out, file), format: "iife" })),
73
+ ...modules.length
74
+ ? [options({
75
+ entryPoints: modules.map(file => path.join(site.src, file)),
76
+ outbase: path.join(site.src, base),
77
+ outdir: path.join(site.out, base),
78
+ format: "esm",
79
+ splitting: true,
80
+ chunkNames: `${path.posix.relative(base, chunks) || "."}/[name]-[hash]`
81
+ })]
82
+ : []
83
+ ]
84
+ }
85
+
86
+ /**
87
+ * Bundles the JS, and notes which chunks each file imports, for the modulepreload links.
88
+ * @param {import("./config.js").Site} site
89
+ * @param {import("./render.js").BuildState} state
90
+ */
91
+ export async function buildJS(site, state) {
92
+ const builds = jsBuilds(site, { metafile: true })
93
+ if (!builds.length) return
94
+ log("JS", "Building JS...")
95
+ const results = await Promise.all(builds.map(build => esbuild.build(build)))
96
+
97
+ /** @param {string} file */
98
+ const url = (file) => "/" + posix(path.relative(site.out, path.resolve(file)))
99
+ for (const [file, output] of results.flatMap(r => Object.entries(r.metafile?.outputs ?? {}))) {
100
+ const imports = output.imports.filter(i => i.kind === "import-statement").map(i => url(i.path))
101
+ if (imports.length) state.chunkImports.set(url(file), imports)
102
+ if (output.entryPoint) state.entryChunks.set("/" + posix(path.relative(site.src, output.entryPoint)), url(file))
103
+ }
104
+ log("JS", "Done building the JS")
105
+ }
106
+
107
+ /** @param {import("./config.js").Site} site */
108
+ export async function buildSass(site) {
109
+ const files = Object.entries(site.sass)
110
+ if (!files.length) return
111
+ log("CSS", "Converting SCSS to CSS")
112
+ const sass = await import("sass").catch(() => {
113
+ throw new Error("The config has sass to compile, which needs it installed: npm install sass")
114
+ })
115
+ for (const [from, to] of files) {
116
+ const result = sass.compile(path.join(site.src, from), { style: "compressed" })
117
+ const outputFile = path.join(site.out, to)
118
+ fs.mkdirSync(path.dirname(outputFile), { recursive: true })
119
+ fs.writeFileSync(outputFile, result.css)
120
+ }
121
+ log("CSS", "Done building the CSS")
122
+ }
package/src/build.js ADDED
@@ -0,0 +1,104 @@
1
+ import fs from "fs"
2
+ import { minify } from "html-minifier-terser"
3
+ import path from "path"
4
+ import { buildJS, buildSass, clean, copyAssets } from "./assets.js"
5
+ import { withCsp } from "./csp.js"
6
+ import { loadData } from "./data.js"
7
+ import { dependencies } from "./deps.js"
8
+ import { log } from "./log.js"
9
+ import { buildState, renderPage } from "./render.js"
10
+ import { listRoutes } from "./routes.js"
11
+ import { writeSiteFiles } from "./site.js"
12
+
13
+ /**
14
+ * Writes a file unless it already has this content. Writing is much slower than reading on Windows, and a dev render
15
+ * of a whole site mostly writes what's already there.
16
+ * @param {string} file
17
+ * @param {string} content
18
+ * @returns {boolean} Whether it was written
19
+ */
20
+ export function writeIfChanged(file, content) {
21
+ try {
22
+ if (fs.readFileSync(file, "utf8") === content) return false
23
+ } catch {
24
+ fs.mkdirSync(path.dirname(file), { recursive: true })
25
+ }
26
+ fs.writeFileSync(file, content)
27
+ return true
28
+ }
29
+
30
+ /**
31
+ * @param {import("./config.js").Site} site
32
+ * @param {import("./render.js").BuildState} state
33
+ * @param {{ hashed?: boolean, only?: Set<string> }} [options] `hashed` for production: hashed asset links and minified
34
+ * HTML. `only` renders just these pages, relative to the pages folder
35
+ * @returns {Promise<import("./routes.js").Route[]>} The pages rendered
36
+ */
37
+ export async function buildHTML(site, state, { hashed = true, only } = {}) {
38
+ log("HTML", only ? `Rendering ${[...only].join(", ")}...` : "Rendering pages...")
39
+ state.assetUrls.clear()
40
+ state.templates.clear()
41
+ state.used.clear()
42
+ state.data = await loadData(site)
43
+
44
+ const routes = await listRoutes(site, state.data, only)
45
+ let written = 0
46
+ for (const route of routes) {
47
+ const html = renderPage(site, state, route, hashed)
48
+ const finished = hashed && site.minify ? await minify(html, site.minify) : html
49
+ if (writeIfChanged(path.join(site.out, route.output), site.csp ? withCsp(site.csp, finished) : finished)) written++
50
+
51
+ for (const [file, content] of Object.entries(route.props.files ?? {})) writeIfChanged(path.join(site.out, file), content)
52
+ }
53
+
54
+ // A page that used the data folder is dated by it too
55
+ for (const route of routes) {
56
+ if (route.props.page) continue
57
+ for (const name of state.used.get(route.source) ?? []) {
58
+ const file = state.data.files[name]
59
+ if (file && !(name in route.props) && !route.sources.includes(file)) route.sources.push(file)
60
+ }
61
+ }
62
+ log("HTML", `Done rendering ${routes.length} pages${written < routes.length ? `, ${written} changed` : ""}`)
63
+ return routes
64
+ }
65
+
66
+ /**
67
+ * @param {import("./config.js").Site} site
68
+ * @param {import("./routes.js").Route[]} routes
69
+ * @param {boolean} dev
70
+ */
71
+ async function runAfter(site, routes, dev) {
72
+ for (const hook of site.after) await hook({ site, routes, dev })
73
+ }
74
+
75
+ /**
76
+ * Renders the pages into an existing build, as dev does after a change. With `only`, just those pages, and the
77
+ * sitemaps are left as they are.
78
+ * @param {import("./config.js").Site} site
79
+ * @param {{ only?: Set<string> }} [options]
80
+ * @returns {Promise<Record<string, string[]>>} What each page rendered is made from, see deps.js
81
+ */
82
+ export async function renderSite(site, { only } = {}) {
83
+ const state = buildState()
84
+ const routes = await buildHTML(site, state, { hashed: false, only })
85
+ if (!only) await writeSiteFiles(site, routes, false)
86
+ await runAfter(site, routes, true)
87
+ return dependencies(site, state, routes)
88
+ }
89
+
90
+ /**
91
+ * The production build, from an empty build folder.
92
+ * @param {import("./config.js").Site} site
93
+ */
94
+ export async function build(site) {
95
+ const state = buildState()
96
+ await clean(site)
97
+ await copyAssets(site)
98
+ await buildJS(site, state)
99
+ await buildSass(site)
100
+ // After JS and CSS, since the ?v=hash is made from the built files
101
+ const routes = await buildHTML(site, state)
102
+ await writeSiteFiles(site, routes)
103
+ await runAfter(site, routes, false)
104
+ }