📝 add VitePress site and deployment workflow

This commit is contained in:
Nanaloveyuki
2026-06-14 16:00:00 +08:00
parent 265cd69ea9
commit 637962a934
15 changed files with 5528 additions and 1 deletions
+61
View File
@@ -0,0 +1,61 @@
name: Docs
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: docs-pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Build docs site
env:
DOCS_BASE: /
run: npm run docs:build
- name: Configure Pages
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/configure-pages@v5
- name: Upload Pages artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
deploy:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+7 -1
View File
@@ -16,9 +16,15 @@ desktop.ini
*.log
*.tmp
*.bak
node_modules/
# Dev Documentations
docs/dev/*
# Docs site build artifacts
docs/.vitepress/cache/
docs/.vitepress/dist/
docs/.vitepress/generated/
# vibecoding
AGENTS/
AGENTS/
+142
View File
@@ -0,0 +1,142 @@
import { basename } from 'node:path'
import { createMoonbitLanguageRegistration } from 'moonbit-syntax-highlighter'
import { defineConfig, type DefaultTheme } from 'vitepress'
import { docsData } from './generated/docs-data.mjs'
type ApiDocEntry = {
file: string
text: string
group: string
category: string
}
const repoSlug = process.env.GITHUB_REPOSITORY?.split('/')[1] ?? 'BitLogger'
const docsBase = process.env.DOCS_BASE ?? (process.env.GITHUB_ACTIONS ? `/${repoSlug}/` : '/')
const repository = 'https://github.com/Nanaloveyuki/BitLogger'
function splitWords(input: string): string[] {
return input
.split(/[^A-Za-z0-9]+/)
.map(part => part.trim())
.filter(Boolean)
}
function formatWord(word: string): string {
const lower = word.toLowerCase()
if (lower === 'api') return 'API'
if (lower === 'json') return 'JSON'
if (lower === 'js') return 'JS'
if (lower === 'wasm') return 'WASM'
if (lower === 'llvm') return 'LLVM'
return lower.charAt(0).toUpperCase() + lower.slice(1)
}
function humanize(input: string): string {
const words = splitWords(input)
if (words.length === 0) return input
return words.map(formatWord).join(' ')
}
function readApiDocs(): ApiDocEntry[] {
return docsData.api.entries.map(entry => ({
file: entry.file,
text: entry.title,
group: entry.group,
category: entry.category,
}))
}
function buildApiSidebar(): DefaultTheme.SidebarItem[] {
const entries = readApiDocs()
const sidebar: DefaultTheme.SidebarItem[] = [{ text: 'Overview', link: '/api/' }]
const groups = new Map<string, Map<string, DefaultTheme.SidebarItem[]>>()
for (const entry of entries) {
if (entry.file === 'index.md') continue
const group = groups.get(entry.group) ?? new Map<string, DefaultTheme.SidebarItem[]>()
const categoryItems = group.get(entry.category) ?? []
categoryItems.push({
text: entry.text,
link: `/api/${basename(entry.file, '.md')}`,
})
group.set(entry.category, categoryItems)
groups.set(entry.group, group)
}
if (groups.size === 1 && groups.has('api')) {
const categories = groups.get('api')!
for (const category of [...categories.keys()].sort((a, b) => humanize(a).localeCompare(humanize(b)))) {
sidebar.push({
text: humanize(category),
collapsed: true,
items: categories.get(category)!,
})
}
return sidebar
}
for (const group of [...groups.keys()].sort((a, b) => humanize(a).localeCompare(humanize(b)))) {
const categories = groups.get(group)!
sidebar.push({
text: humanize(group),
collapsed: true,
items: [...categories.keys()]
.sort((a, b) => humanize(a).localeCompare(humanize(b)))
.map(category => ({
text: humanize(category),
collapsed: true,
items: categories.get(category)!,
})),
})
}
return sidebar
}
function buildChangesSidebar(): DefaultTheme.SidebarItem[] {
return [
{ text: 'Overview', link: '/changes/' },
{
text: 'Versions',
items: docsData.changes.map(item => ({ text: item.version, link: item.link })),
},
]
}
export default defineConfig({
title: 'BitLogger',
description: 'Structured logging library docs for MoonBit.',
base: docsBase,
cleanUrls: true,
srcExclude: ['dev/**'],
lastUpdated: true,
markdown: {
languages: [createMoonbitLanguageRegistration()],
},
themeConfig: {
siteTitle: 'BitLogger',
nav: [
{ text: 'Home', link: '/' },
{ text: 'API', link: '/api/' },
{ text: 'Changes', link: '/changes/' },
{ text: 'Mooncake', link: 'https://mooncakes.io/docs/Nanaloveyuki/BitLogger' },
],
search: {
provider: 'local',
},
socialLinks: [{ icon: 'github', link: repository }],
editLink: {
pattern: `${repository}/edit/main/docs/:path`,
text: 'Edit this page on GitHub',
},
sidebar: {
'/api/': buildApiSidebar(),
'/changes/': buildChangesSidebar(),
},
footer: {
message: 'Published from the repository docs folder with VitePress.',
copyright: 'MIT',
},
},
})
@@ -0,0 +1,219 @@
<script setup lang="ts">
import { computed, shallowRef } from 'vue'
import { withBase } from 'vitepress'
import { docsData } from '../../generated/docs-data.mjs'
const selectedGroup = shallowRef('all')
const query = shallowRef('')
const groups = computed(() => docsData.api.groups)
const filteredGroups = computed(() => {
const keyword = query.value.trim().toLowerCase()
return groups.value
.filter(group => selectedGroup.value === 'all' || group.id === selectedGroup.value)
.map(group => ({
...group,
categories: group.categories
.map(category => ({
...category,
entries: category.entries.filter(entry => {
if (!keyword) return true
const haystack = [entry.title, entry.description, entry.name, ...entry.keywords]
.join(' ')
.toLowerCase()
return haystack.includes(keyword)
}),
}))
.filter(category => category.entries.length > 0),
}))
.filter(group => group.categories.length > 0)
})
const resultCount = computed(() =>
filteredGroups.value.reduce(
(total, group) => total + group.categories.reduce((sum, category) => sum + category.entries.length, 0),
0,
),
)
</script>
<template>
<section class="api-shell">
<div class="api-toolbar">
<label class="api-search">
<span>Find API</span>
<input v-model="query" type="search" placeholder="Search logger, sink, config, async...">
</label>
<label class="api-filter">
<span>Doc Group</span>
<select v-model="selectedGroup">
<option value="all">All groups</option>
<option v-for="group in groups" :key="group.id" :value="group.id">
{{ group.label }}
</option>
</select>
</label>
</div>
<div class="api-summary">
<strong>{{ resultCount }}</strong>
<span>matching API pages across {{ filteredGroups.length }} group views</span>
</div>
<div class="api-groups">
<article v-for="group in filteredGroups" :key="group.id" class="api-group-card">
<header class="api-group-header">
<div>
<p class="api-group-kicker">{{ group.label }}</p>
<h3>{{ group.entryCount }} APIs in this group</h3>
</div>
</header>
<div class="api-category-grid">
<section v-for="category in group.categories" :key="category.id" class="api-category-card">
<h4>{{ category.label }}</h4>
<p>{{ category.entries.length }} entries</p>
<ul>
<li v-for="entry in category.entries.slice(0, 8)" :key="entry.slug">
<a :href="withBase(entry.link)">{{ entry.title }}</a>
</li>
</ul>
<a v-if="category.entries.length > 8" class="api-more" :href="withBase('/api/')">
Browse {{ category.entries.length - 8 }} more in sidebar
</a>
</section>
</div>
</article>
</div>
</section>
</template>
<style scoped>
.api-shell {
margin: 1.5rem 0 2rem;
}
.api-toolbar {
display: grid;
gap: 0.9rem;
margin-bottom: 1rem;
}
.api-search,
.api-filter {
display: grid;
gap: 0.4rem;
}
.api-search span,
.api-filter span {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #9b4d28;
font-weight: 700;
}
.api-search input,
.api-filter select {
width: 100%;
border-radius: 14px;
border: 1px solid var(--vp-c-divider);
background: rgba(255, 255, 255, 0.9);
padding: 0.85rem 0.95rem;
font: inherit;
}
.api-summary {
display: flex;
align-items: baseline;
gap: 0.65rem;
margin-bottom: 1rem;
}
.api-summary strong {
font-size: 1.8rem;
}
.api-summary span {
color: var(--vp-c-text-2);
}
.api-groups {
display: grid;
gap: 1rem;
}
.api-group-card {
border: 1px solid var(--vp-c-divider);
border-radius: 28px;
padding: 1rem;
background: linear-gradient(180deg, rgba(255, 251, 247, 0.96), rgba(246, 242, 236, 0.96));
}
.api-group-header h3,
.api-category-card h4,
.api-category-card p {
margin: 0;
}
.api-group-kicker {
margin: 0 0 0.25rem;
color: #9b4d28;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.75rem;
font-weight: 700;
}
.api-category-grid {
display: grid;
gap: 0.85rem;
margin-top: 0.9rem;
}
.api-category-card {
border-radius: 20px;
background: rgba(255, 255, 255, 0.88);
border: 1px solid rgba(155, 77, 40, 0.12);
padding: 0.95rem;
}
.api-category-card p {
margin-top: 0.25rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.api-category-card ul {
list-style: none;
padding: 0;
margin: 0.85rem 0 0;
display: grid;
gap: 0.45rem;
}
.api-category-card a {
text-decoration: none;
}
.api-more {
display: inline-block;
margin-top: 0.8rem;
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
@media (min-width: 820px) {
.api-toolbar {
grid-template-columns: minmax(0, 2fr) minmax(220px, 0.8fr);
}
.api-category-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
@@ -0,0 +1,191 @@
<script setup lang="ts">
import { computed } from 'vue'
import { withBase } from 'vitepress'
import { docsData } from '../../generated/docs-data.mjs'
const topCategories = computed(() => docsData.api.categories.slice(0, 6))
const recentChanges = computed(() => docsData.changes.slice(0, 4))
const apiCount = computed(() => docsData.api.entries.filter(entry => entry.slug !== 'index').length)
const groupCount = computed(() => docsData.api.groups.length)
</script>
<template>
<section class="hub-grid">
<article class="hub-panel hub-panel-accent">
<p class="hub-eyebrow">Docs Hub</p>
<h2 class="hub-title">Start from use case, not from filenames.</h2>
<p class="hub-copy">
Browse builders, runtime helpers, presets, sync logging, and async logging from one place.
</p>
<div class="hub-stats">
<div>
<strong>{{ apiCount }}</strong>
<span>API pages</span>
</div>
<div>
<strong>{{ groupCount }}</strong>
<span>doc groups</span>
</div>
<div>
<strong>{{ docsData.changes.length }}</strong>
<span>release notes</span>
</div>
</div>
</article>
<article class="hub-panel">
<p class="hub-eyebrow">Popular Areas</p>
<ul class="hub-chip-list">
<li v-for="category in topCategories" :key="category.id">
<a class="hub-chip" :href="withBase('/api/')">
<span>{{ category.label }}</span>
<small>{{ category.entryCount }}</small>
</a>
</li>
</ul>
</article>
<article class="hub-panel">
<p class="hub-eyebrow">Release Trail</p>
<ul class="hub-link-list">
<li v-for="item in recentChanges" :key="item.version">
<a :href="withBase(item.link)">Version {{ item.version }}</a>
</li>
</ul>
</article>
</section>
</template>
<style scoped>
.hub-grid {
display: grid;
gap: 1rem;
margin: 1.5rem 0 2rem;
}
.hub-panel {
border: 1px solid var(--vp-c-divider);
border-radius: 24px;
padding: 1.25rem;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(247, 245, 239, 0.95));
box-shadow: 0 16px 40px rgba(62, 46, 31, 0.08);
}
.hub-panel-accent {
background:
radial-gradient(circle at top right, rgba(191, 68, 32, 0.16), transparent 34%),
linear-gradient(180deg, rgba(255, 250, 244, 0.98), rgba(247, 241, 232, 0.98));
}
.hub-eyebrow {
margin: 0 0 0.5rem;
color: #9b4d28;
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.75rem;
font-weight: 700;
}
.hub-title {
margin: 0;
font-size: 1.55rem;
line-height: 1.15;
}
.hub-copy {
margin: 0.85rem 0 0;
color: var(--vp-c-text-2);
}
.hub-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.75rem;
margin-top: 1.15rem;
}
.hub-stats strong,
.hub-stats span {
display: block;
}
.hub-stats strong {
font-size: 1.6rem;
}
.hub-stats span {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.hub-chip-list,
.hub-link-list {
list-style: none;
padding: 0;
margin: 0;
}
.hub-chip-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.7rem;
}
.hub-chip {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 56px;
gap: 0.8rem;
padding: 0.7rem 0.9rem;
border-radius: 999px;
text-decoration: none;
color: inherit;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(155, 77, 40, 0.14);
box-sizing: border-box;
}
.hub-chip span {
min-width: 0;
}
.hub-chip small {
flex: 0 0 auto;
color: var(--vp-c-text-2);
}
@media (max-width: 640px) {
.hub-chip-list {
grid-template-columns: 1fr;
}
}
.hub-link-list {
display: grid;
gap: 0.5rem;
}
.hub-link-list a {
color: var(--vp-c-brand-1);
text-decoration: none;
}
@media (min-width: 860px) {
.hub-grid {
grid-template-columns: 1.35fr 1fr;
}
.hub-panel-accent {
grid-row: span 2;
}
}
@media (max-width: 640px) {
.hub-stats {
grid-template-columns: 1fr;
}
}
</style>
+37
View File
@@ -0,0 +1,37 @@
:root {
--vp-c-brand-1: #b34a24;
--vp-c-brand-2: #92381a;
--vp-c-brand-3: #d06d39;
--vp-c-brand-soft: rgba(176, 74, 36, 0.14);
--vp-home-hero-name-color: #6b2f17;
--vp-home-hero-text-color: #27170f;
--vp-home-hero-tagline-color: #5f5249;
--vp-font-family-base: "Segoe UI", "PingFang SC", "Hiragino Sans GB", sans-serif;
--vp-font-family-mono: "Cascadia Mono", "JetBrains Mono", monospace;
}
.VPContent {
background:
radial-gradient(circle at top right, rgba(208, 109, 57, 0.08), transparent 28%),
radial-gradient(circle at left 20%, rgba(179, 74, 36, 0.06), transparent 22%);
}
.VPHomeHero .name,
.VPDoc h1,
.VPDoc h2,
.VPDoc h3 {
letter-spacing: -0.02em;
}
.VPHomeHero .image-container {
display: none;
}
.vp-doc a {
text-underline-offset: 0.18em;
}
.vp-doc .custom-block,
.vp-doc div[class*='language-'] {
border-radius: 18px;
}
+17
View File
@@ -0,0 +1,17 @@
import DefaultTheme from 'vitepress/theme'
import type { Theme } from 'vitepress'
import ApiOverview from './components/ApiOverview.vue'
import HomeDocHub from './components/HomeDocHub.vue'
import './custom.css'
const theme: Theme = {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('ApiOverview', ApiOverview)
app.component('HomeDocHub', HomeDocHub)
},
}
export default theme
+25
View File
@@ -0,0 +1,25 @@
## BitLogger Update Changes
version 0.5.3
### Docs
- docs: expand `docs/api` coverage for sync and async facade aliases, builders, config models, state models, and runtime/file helper surfaces
- docs: clarify facade layering for `ConfiguredLogger`, `ApplicationLogger`, `LibraryLogger`, `ApplicationAsyncLogger`, `ApplicationTextAsyncLogger`, and the library async/text facades
- docs: refresh sync and async target-verification notes so published support claims stay aligned with the current validation matrix
### Test
- test: cover sync facade composition and parse/build contracts for configured, application, and library logger paths
- test: cover async facade helper-surface and unwrap contracts for application, library, parsed, and text-console builder paths
- test: keep sync and async package lines validated across `native`, `js`, and `wasm-gc` after the facade contract sweep
### Runtime
- fix: correct closed async backlog accounting so pending and dropped counts remain consistent after close and shutdown paths
- fix: hide internal file backend wrapper details from the public surface while preserving runtime file-helper behavior
### Notes
- this release is a contract-locking follow-up focused on making the published API reference match the live facade behavior more directly
- facade-oriented sync and async entry paths now have stronger direct tests for alias-preserved helpers, narrowing boundaries, and unwrap recovery behavior
+12
View File
@@ -0,0 +1,12 @@
## Release Notes
Versioned BitLogger change summaries.
- [0.5.3](./0.5.3.md)
- [0.5.2](./0.5.2.md)
- [0.5.1](./0.5.1.md)
- [0.5.0](./0.5.0.md)
- [0.4.1](./0.4.1.md)
- [0.4.0](./0.4.0.md)
- [0.3.0](./0.3.0.md)
- [0.2.0](./0.2.0.md)
+34
View File
@@ -0,0 +1,34 @@
---
layout: home
hero:
name: BitLogger
text: Structured logging for MoonBit
tagline: API reference, examples, and release notes for BitLogger.
actions:
- theme: brand
text: Open API Reference
link: /api/
- theme: alt
text: Read Release Notes
link: /changes/
- theme: alt
text: Mooncake Package
link: https://mooncakes.io/docs/Nanaloveyuki/BitLogger
features:
- title: Structured logging
details: Levels, targets, message fields, and configurable text or JSON output.
- title: Sync and async paths
details: API reference covers both the main package and the async package surface.
- title: Config and runtime helpers
details: Browse builders, presets, file helpers, queue helpers, and verification notes quickly.
---
## Start Here
<HomeDocHub />
- Use [API Reference](./api/index.md) when you want the canonical one-file-per-public-API docs.
- Use [Release Notes](./changes/index.md) when you want version-scoped changes and publish-facing summaries.
- Use [Chinese README](https://github.com/Nanaloveyuki/BitLogger/blob/main/README.md) or [English README](./README-en.md) when you want a shorter project overview first.
+2837
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "bitlogger-docs-site",
"private": true,
"scripts": {
"prepare": "node ./scripts/generate-docs-data.mjs",
"dev": "node ./scripts/generate-docs-data.mjs && node ./node_modules/vitepress/dist/node/cli.js dev docs",
"build": "node ./scripts/generate-docs-data.mjs && node ./node_modules/vitepress/dist/node/cli.js build docs",
"preview": "node ./node_modules/vitepress/dist/node/cli.js preview docs",
"docs:prepare": "node ./scripts/generate-docs-data.mjs",
"docs:dev": "node ./scripts/generate-docs-data.mjs && node ./node_modules/vitepress/dist/node/cli.js dev docs",
"docs:build": "node ./scripts/generate-docs-data.mjs && node ./node_modules/vitepress/dist/node/cli.js build docs",
"docs:preview": "node ./node_modules/vitepress/dist/node/cli.js preview docs"
},
"devDependencies": {
"gray-matter": "^4.0.3",
"moonbit-syntax-highlighter": "^0.1.1",
"vitepress": "^1.6.4"
}
}
+1803
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
allowBuilds:
esbuild: true
minimumReleaseAgeExclude:
- moonbit-syntax-highlighter@0.1.1
+120
View File
@@ -0,0 +1,120 @@
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
import { basename, dirname, extname, resolve } from 'node:path'
import matter from 'gray-matter'
const root = process.cwd()
const docsDir = resolve(root, 'docs')
const apiDir = resolve(docsDir, 'api')
const changesDir = resolve(docsDir, 'changes')
const outFile = resolve(docsDir, '.vitepress', 'generated', 'docs-data.mjs')
function splitWords(input) {
return input
.split(/[^A-Za-z0-9]+/)
.map(part => part.trim())
.filter(Boolean)
}
function formatWord(word) {
const lower = word.toLowerCase()
if (lower === 'api') return 'API'
if (lower === 'json') return 'JSON'
if (lower === 'js') return 'JS'
if (lower === 'wasm') return 'WASM'
if (lower === 'llvm') return 'LLVM'
return lower.charAt(0).toUpperCase() + lower.slice(1)
}
function humanize(input) {
const words = splitWords(input)
if (words.length === 0) return input
return words.map(formatWord).join(' ')
}
function extractTitle(source, fallback) {
const heading = source.match(/^##\s+(.+)$/m)?.[1]?.trim()
return humanize(heading ?? fallback)
}
function readApiDocs() {
const entries = readdirSync(apiDir, { withFileTypes: true })
.filter(entry => entry.isFile())
.filter(entry => extname(entry.name) === '.md')
.filter(entry => !entry.name.startsWith('.'))
.map(entry => {
const file = entry.name
const slug = basename(file, '.md')
const source = readFileSync(resolve(apiDir, file), 'utf8')
const parsed = matter(source)
const data = parsed.data
const keywords = Array.isArray(data['key-word']) ? data['key-word'].map(String) : []
return {
file,
slug,
title: extractTitle(parsed.content, data.name ?? slug),
name: String(data.name ?? slug),
group: String(data.group ?? 'misc'),
category: String(data.category ?? 'misc'),
description: String(data.description ?? ''),
updateTime: data['update-time'] == null ? '' : String(data['update-time']),
keywords,
link: `/api/${slug}`,
}
})
.sort((left, right) => left.title.localeCompare(right.title))
const grouped = [...new Set(entries.map(entry => entry.group))]
.sort((left, right) => humanize(left).localeCompare(humanize(right)))
.map(group => {
const items = entries.filter(entry => entry.group === group)
const categories = [...new Set(items.map(entry => entry.category))]
.sort((left, right) => humanize(left).localeCompare(humanize(right)))
.map(category => ({
id: category,
label: humanize(category),
entries: items.filter(entry => entry.category === category),
}))
return {
id: group,
label: humanize(group),
entryCount: items.length,
categories,
}
})
const categorySummaries = [...new Set(entries.map(entry => entry.category))]
.sort((left, right) => humanize(left).localeCompare(humanize(right)))
.map(category => ({
id: category,
label: humanize(category),
entryCount: entries.filter(entry => entry.category === category).length,
}))
return {
entries,
groups: grouped,
categories: categorySummaries,
}
}
function readChanges() {
return readdirSync(changesDir, { withFileTypes: true })
.filter(entry => entry.isFile())
.filter(entry => extname(entry.name) === '.md')
.filter(entry => entry.name !== 'index.md')
.map(entry => basename(entry.name, '.md'))
.sort((left, right) => right.localeCompare(left, undefined, { numeric: true }))
.map(version => ({
version,
link: `/changes/${version}`,
}))
}
const api = readApiDocs()
const changes = readChanges()
const output = `export const docsData = ${JSON.stringify({ api, changes }, null, 2)}\n`
mkdirSync(dirname(outFile), { recursive: true })
writeFileSync(outFile, output)