@ozdao/scriptorium 0.1.3 → 0.1.4
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/README.md +31 -30
- package/bin/scriptorium.js +27 -10
- package/package.json +1 -1
- package/src/server.js +4 -22
- package/src/static-site.js +70 -0
package/README.md
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
# @ozdao/scriptorium
|
|
2
2
|
|
|
3
|
-
Сайт документации из папок markdown на @ozdao/martyrs (Vue 3, rspack
|
|
3
|
+
Сайт документации из папок markdown на @ozdao/martyrs (Vue 3, rspack). Все `.md` репозитория — страницы; `AGENTS.md` — индекс своей папки; адреса без `.md`. Работает как VitePress: `dev` — сервер разработки, `build` — статический сайт в папке `dist/`, деплой — раздать папку.
|
|
4
4
|
|
|
5
5
|
## Раскладка у пользователя
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Сайт живёт в одной папке `documentation/` в корне репозитория документации, команды запускаются из неё. Страницы — все `.md` репозитория (папка над `documentation/`); сама `documentation/` в сайт не входит.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
documentation/
|
|
11
|
-
├── package.json
|
|
12
|
-
├──
|
|
13
|
-
├──
|
|
14
|
-
├──
|
|
15
|
-
├──
|
|
16
|
-
|
|
11
|
+
├── package.json зависимости и скрипты сайта
|
|
12
|
+
├── pnpm-workspace.yaml разрешения на install-скрипты зависимостей (pnpm 11)
|
|
13
|
+
├── node_modules/ единственный node_modules репозитория документации
|
|
14
|
+
├── config.js конфиг сайта (обязателен)
|
|
15
|
+
├── theme/ логотип, блок над страницей, стили (необязательно)
|
|
16
|
+
├── public/ своя статика сайта (favicon)
|
|
17
|
+
├── builds/ сборка клиента и render-бандла (генерируется)
|
|
18
|
+
└── dist/ статический сайт после build (генерируется)
|
|
17
19
|
```
|
|
18
20
|
|
|
19
21
|
Код сайта целиком в пакете (`node_modules/@ozdao/scriptorium`), туда при работе ничего не пишется. В корне репозитория документации `package.json` и `node_modules` нет.
|
|
@@ -27,15 +29,13 @@ documentation/
|
|
|
27
29
|
"name": "docs",
|
|
28
30
|
"private": true,
|
|
29
31
|
"type": "module",
|
|
30
|
-
"packageManager": "pnpm@10.8.0",
|
|
31
32
|
"scripts": {
|
|
32
33
|
"preinstall": "npx only-allow pnpm",
|
|
33
34
|
"dev": "scriptorium dev",
|
|
34
|
-
"build": "scriptorium build"
|
|
35
|
-
"start": "scriptorium start"
|
|
35
|
+
"build": "scriptorium build"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@ozdao/scriptorium": "0.1.
|
|
38
|
+
"@ozdao/scriptorium": "0.1.4",
|
|
39
39
|
"@ozdao/martyrs": "0.2.609",
|
|
40
40
|
"vue": "3.5.42",
|
|
41
41
|
"vue-i18n": "11.4.10",
|
|
@@ -44,9 +44,15 @@ documentation/
|
|
|
44
44
|
}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
`@ozdao/martyrs`, `vue`, `vue-router`, `vue-i18n` — peer-зависимости пакета,
|
|
47
|
+
`@ozdao/martyrs`, `vue`, `vue-router`, `vue-i18n` — peer-зависимости пакета, указывать обязательно: сборка martyrs берёт `vue` из `documentation/node_modules/vue`, а pnpm кладёт наверх только прямые зависимости проекта.
|
|
48
48
|
|
|
49
|
-
`
|
|
49
|
+
`documentation/pnpm-workspace.yaml` — pnpm 11 запускает install-скрипты зависимостей только с разрешения:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
allowBuilds:
|
|
53
|
+
"@ozdao/martyrs": true
|
|
54
|
+
better-sqlite3: true
|
|
55
|
+
```
|
|
50
56
|
|
|
51
57
|
```bash
|
|
52
58
|
cd documentation && pnpm install
|
|
@@ -56,11 +62,12 @@ cd documentation && pnpm install
|
|
|
56
62
|
|
|
57
63
|
```bash
|
|
58
64
|
cd documentation
|
|
59
|
-
pnpm dev #
|
|
60
|
-
pnpm build #
|
|
61
|
-
pnpm start # сервер по сборке
|
|
65
|
+
pnpm dev # разработка с HMR, http://localhost:8200 (порт — PORT)
|
|
66
|
+
pnpm build # статический сайт в dist/
|
|
62
67
|
```
|
|
63
68
|
|
|
69
|
+
`build` собирает клиент и render-бандл в `builds/`, пререндерит каждую страницу в `dist/<адрес>/index.html`, кладёт рядом ассеты, `public/` и `404.html`. Деплой — любой статический сервер с корнем `dist/`; для адресов без расширения — `try_files $uri $uri/index.html =404` (nginx), `404.html` — на неизвестный адрес.
|
|
70
|
+
|
|
64
71
|
Команда запускается только из `documentation/`: там должны лежать `config.js` и `package.json`.
|
|
65
72
|
|
|
66
73
|
## Конфиг
|
|
@@ -74,7 +81,7 @@ export default {
|
|
|
74
81
|
skipped_names: ['node_modules', 'builds'], // папки, пропускаемые на любой глубине
|
|
75
82
|
skipped_paths: [], // пути от корня репозитория; documentation/ пропускается всегда
|
|
76
83
|
nav: [{ text: 'Обзор', link: '/' }],
|
|
77
|
-
sidebar: { priority: ['AGENTS.md'], folders_to_bottom: true, collapse_depth: 2, hidden: [
|
|
84
|
+
sidebar: { priority: ['AGENTS.md'], folders_to_bottom: true, collapse_depth: 2, hidden: [] },
|
|
78
85
|
outline: { level: [2, 3], label: 'На странице' },
|
|
79
86
|
labels: { menu: 'Меню', theme: 'Тема', top: 'Наверх', prev: 'Назад', next: 'Дальше', not_found: 'Страница не найдена', home: 'На главную' },
|
|
80
87
|
}
|
|
@@ -87,27 +94,21 @@ import { Badge, useDocs } from '@ozdao/scriptorium'
|
|
|
87
94
|
const { page, frontmatter, config, pages } = useDocs()
|
|
88
95
|
```
|
|
89
96
|
|
|
90
|
-
`
|
|
97
|
+
Картинки в `.md` — относительными путями от файла; сборка кладёт их в ассеты (png, jpg, webp, avif, svg, gif).
|
|
91
98
|
|
|
92
99
|
## Что генерируется
|
|
93
100
|
|
|
94
|
-
|
|
101
|
+
В `.gitignore` репозитория документации:
|
|
95
102
|
|
|
96
103
|
```
|
|
97
104
|
documentation/node_modules
|
|
98
105
|
documentation/builds
|
|
99
|
-
documentation/
|
|
100
|
-
documentation/public/icon-sprite.svg
|
|
101
|
-
documentation/public/fonts/
|
|
106
|
+
documentation/dist
|
|
102
107
|
documentation/.cache
|
|
103
108
|
documentation/logs
|
|
104
109
|
```
|
|
105
110
|
|
|
106
|
-
-
|
|
107
|
-
- `public/sw.js` — service worker в dev (в prod он лежит в `builds/web/client/`);
|
|
108
|
-
- `public/icon-sprite.svg`, `public/fonts/` — спрайт иконок и шрифты martyrs; у документации своих исходников нет, файлы не появляются;
|
|
109
|
-
- `.cache/martyrs-jit.css` — JIT-утилиты martyrs;
|
|
110
|
-
- `logs/testid-duplicates.txt` — отчёт дублей `data-testid` dev-сборки.
|
|
111
|
+
`.cache/martyrs-jit.css` — JIT-утилиты martyrs; `logs/testid-duplicates.txt` — отчёт dev-сборки.
|
|
111
112
|
|
|
112
113
|
## Разработка пакета
|
|
113
114
|
|
|
@@ -115,6 +116,6 @@ documentation/logs
|
|
|
115
116
|
pnpm install # в корне пакета: pnpm-workspace.yaml разрешает install-скрипты
|
|
116
117
|
```
|
|
117
118
|
|
|
118
|
-
Подключение к репозиторию документации без публикации — `"@ozdao/scriptorium": "link:<путь к пакету>"` в `documentation/package.json
|
|
119
|
+
Подключение к репозиторию документации без публикации — `"@ozdao/scriptorium": "link:<путь к пакету>"` в `documentation/package.json`; peer-зависимости выше там тоже обязательны.
|
|
119
120
|
|
|
120
|
-
Устройство: `bin/scriptorium.js` — CLI
|
|
121
|
+
Устройство: `bin/scriptorium.js` — CLI, конфиги rspack поверх builder'а martyrs (корень приложения = `documentation/`, вход и сервер — файлы пакета), `build`; `src/static-site.js` — пререндер в `dist/`; `src/server.js` — сервер dev; `src/content/` — loader'ы markdown, списка страниц и индекса поиска; `src/components/` — раскладка сайта.
|
package/bin/scriptorium.js
CHANGED
|
@@ -16,10 +16,11 @@ import path from 'node:path';
|
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
import { isMainThread } from 'node:worker_threads';
|
|
18
18
|
|
|
19
|
+
// dev — dev-сервер martyrs с HMR; build — статический сайт в documentation/dist
|
|
20
|
+
// (пререндер всех страниц + ассеты), деплой = раздать папку. Node в проде не нужен.
|
|
19
21
|
const environment_of_command = {
|
|
20
22
|
dev: 'development',
|
|
21
23
|
build: 'production',
|
|
22
|
-
start: 'production',
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
const command = process.argv[2];
|
|
@@ -164,11 +165,12 @@ for (const config of Object.values(configs.ssr)) {
|
|
|
164
165
|
plugin.options.exclude = plugin.options.exclude.filter((part) => part !== 'node_modules');
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
// Service worker
|
|
168
|
-
//
|
|
169
|
-
// <корень приложения>/../public — корень репозитория
|
|
168
|
+
// Service worker: клиент martyrs регистрирует /sw.js на каждой странице. В
|
|
169
|
+
// сборке плагин эмитит sw.js ассетом — он попадает в dist. В dev он писал бы
|
|
170
|
+
// файл в <корень приложения>/../public — корень репозитория документации,
|
|
171
|
+
// поэтому в dev плагин убирается (ошибка регистрации в консоли dev — не беда).
|
|
170
172
|
const is_service_worker = plugin?.constructor === Object && String(plugin.apply).includes('service-worker');
|
|
171
|
-
return !is_service_worker;
|
|
173
|
+
return !(is_service_worker && process.env.NODE_ENV !== 'production');
|
|
172
174
|
});
|
|
173
175
|
}
|
|
174
176
|
|
|
@@ -178,9 +180,24 @@ for (const config of Object.values(configs.ssr)) {
|
|
|
178
180
|
// клиент при гидратации ставил другой адрес.
|
|
179
181
|
configs.ssr.server.output.publicPath = '/';
|
|
180
182
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
if (command === 'build') {
|
|
184
|
+
// Клиент и render-бандл — компиляторами rspack по тем же конфигам, что у dev;
|
|
185
|
+
// затем пререндер страниц в dist. Сборка martyrs (builder/build.js) после
|
|
186
|
+
// компиляции завершает процесс, поэтому компиляторы запускаются здесь.
|
|
187
|
+
const { rspack } = await import('@rspack/core');
|
|
188
|
+
for (const config of [configs.ssr.client, configs.ssr.server]) {
|
|
189
|
+
const compiler = rspack(config);
|
|
190
|
+
const failed = await new Promise((resolve, reject) => compiler.run((error, stats) => {
|
|
191
|
+
if (error) { reject(error); return; }
|
|
192
|
+
if (stats.hasErrors()) console.error(stats.toString({ errors: true, warnings: false, colors: true }));
|
|
193
|
+
compiler.close(() => resolve(stats.hasErrors()));
|
|
194
|
+
}));
|
|
195
|
+
if (failed) process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
const { writeStaticSite } = await import('../src/static-site.js');
|
|
198
|
+
await writeStaticSite(configs.ssr, path.join(docs_dir, 'dist'));
|
|
199
|
+
} else {
|
|
200
|
+
await run(command, configs);
|
|
201
|
+
// Воркер бэкенда в dev (тот же файл, не главный поток) адрес не печатает.
|
|
202
|
+
if (isMainThread) console.log(`Docs: http://localhost:${process.env.PORT}`);
|
|
186
203
|
}
|
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Сервер сайта
|
|
2
|
-
// (builder/start.js), SSR-рендер страниц он же вешает поверх
|
|
1
|
+
// Сервер сайта документации для dev: express + статика. Порт слушает раннер
|
|
2
|
+
// martyrs (builder/start.js внутри dev), SSR-рендер страниц он же вешает поверх
|
|
3
|
+
// этого приложения. В проде сервера нет: build пишет статический сайт в dist.
|
|
3
4
|
// Env и резолв-хуки уже стоят: register выполняет bin/scriptorium.js до этого файла.
|
|
4
5
|
// Грузится в node без сборки, поэтому импорты только относительные: хуки martyrs
|
|
5
6
|
// ведут '@/' в src/ корня приложения (documentation/src), которого нет.
|
|
@@ -9,7 +10,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
10
|
import express from 'express';
|
|
10
11
|
import cookies from 'cookie-parser';
|
|
11
12
|
|
|
12
|
-
import { docs_dir,
|
|
13
|
+
import { docs_dir, loadSiteConfig } from './content/scan.js';
|
|
13
14
|
|
|
14
15
|
// Статика пакета: favicon по умолчанию.
|
|
15
16
|
const package_public = fileURLToPath(new URL('../public', import.meta.url));
|
|
@@ -39,27 +40,8 @@ const createServer = async () => {
|
|
|
39
40
|
app.use(express.static(path.join(docs_dir, 'public')));
|
|
40
41
|
app.use(express.static(package_public));
|
|
41
42
|
|
|
42
|
-
// Файлы репозитория по ссылкам из документов (референсы, логотипы, манифест):
|
|
43
|
-
// как VitePress отдаёт всё из папки исходников. Только медиа и данные, без
|
|
44
|
-
// скрытых папок и node_modules: исходники и конфиги наружу не отдаются.
|
|
45
|
-
app.use((req, res, next) => {
|
|
46
|
-
if (!/\.(?:png|jpe?g|gif|webp|avif|svg|mp4|webm|pdf|json)$/i.test(req.path)) return next();
|
|
47
|
-
if (/\/\.|\/node_modules\//.test(req.path)) return next();
|
|
48
|
-
// Папка самого сайта (конфиг, package.json, lockfile) — не документация.
|
|
49
|
-
if (req.path.startsWith(`/${path.relative(docs_root, docs_dir)}/`)) return next();
|
|
50
|
-
express.static(docs_root, { index: false, dotfiles: 'ignore' })(req, res, next);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
43
|
const server = http.createServer(app);
|
|
54
44
|
|
|
55
|
-
// scriptorium start: SIGTERM (systemd, деплой) — штатный выход с кодом 0,
|
|
56
|
-
// а не 143. В dev этот файл живёт в воркере бэкенда, сигналы туда не
|
|
57
|
-
// приходят: остановку ведёт главный поток (builder/dev.js, сообщение close).
|
|
58
|
-
process.once('SIGTERM', () => {
|
|
59
|
-
server.close(() => process.exit(0));
|
|
60
|
-
server.closeAllConnections();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
45
|
return { app, server };
|
|
64
46
|
};
|
|
65
47
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Статическая сборка сайта — как `vitepress build`: каждая страница
|
|
2
|
+
// пререндерится тем же render-бандлом, что обслуживает dev, и пишется в
|
|
3
|
+
// dist/<адрес>/index.html; рядом — ассеты клиента и public/. Деплой — раздать
|
|
4
|
+
// папку dist любым статическим сервером, node в проде не нужен.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { createAssetResolver } from '@ozdao/martyrs/src/builder/ssr/asset-resolver.js';
|
|
9
|
+
import { renderHtml } from '@ozdao/martyrs/src/builder/ssr/ssr-render-html.js';
|
|
10
|
+
import { createBeastiesProcessor } from '@ozdao/martyrs/src/builder/ssr/beasties-processor.js';
|
|
11
|
+
import { docs_dir, docs_root, loadSiteConfig, scanPages } from './content/scan.js';
|
|
12
|
+
|
|
13
|
+
export async function writeStaticSite({ client, server }, dist_dir) {
|
|
14
|
+
const site_config = await loadSiteConfig();
|
|
15
|
+
const client_dir = client.output.path;
|
|
16
|
+
const server_dir = server.output.path;
|
|
17
|
+
|
|
18
|
+
// Статы клиента → теги <script>/<link> под чанки страницы; render-бандл — по manifest.json.
|
|
19
|
+
const resolver = createAssetResolver(JSON.parse(fs.readFileSync(path.join(client_dir, 'stats.json'), 'utf-8')));
|
|
20
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(server_dir, 'manifest.json'), 'utf-8'));
|
|
21
|
+
const { _renderApp: renderApp } = await import(/* webpackIgnore: true */ pathToFileURL(path.join(server_dir, manifest['main.js'])).href);
|
|
22
|
+
// Критический CSS в <head> страницы, как в проде martyrs.
|
|
23
|
+
const beasties = createBeastiesProcessor(client_dir, { publicPath: client.output.publicPath });
|
|
24
|
+
|
|
25
|
+
fs.rmSync(dist_dir, { recursive: true, force: true });
|
|
26
|
+
fs.mkdirSync(dist_dir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
// Ассеты клиента (без статов сборки) и public/ пользователя — в корень dist.
|
|
29
|
+
for (const entry of fs.readdirSync(client_dir)) {
|
|
30
|
+
if (entry === 'stats.json') continue;
|
|
31
|
+
fs.cpSync(path.join(client_dir, entry), path.join(dist_dir, entry), { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
if (fs.existsSync(path.join(docs_dir, 'public'))) {
|
|
34
|
+
fs.cpSync(path.join(docs_dir, 'public'), dist_dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Страницы сайта плюс одна несуществующая — для 404.html.
|
|
38
|
+
const urls = [...scanPages(docs_root, site_config).map((page) => page.url), '/404'];
|
|
39
|
+
|
|
40
|
+
for (const url of urls) {
|
|
41
|
+
const { html, meta, state, usedModules, loadedModules } = await renderApp({
|
|
42
|
+
url,
|
|
43
|
+
// Тема по умолчанию — та же cookie, по которой рендерер martyrs ставит data-theme.
|
|
44
|
+
cookies: site_config.appearance === 'auto' ? {} : { theme: site_config.appearance },
|
|
45
|
+
headers: { host: 'localhost' },
|
|
46
|
+
languages: [site_config.lang],
|
|
47
|
+
ssrContext: {},
|
|
48
|
+
});
|
|
49
|
+
const modules = loadedModules || usedModules || [];
|
|
50
|
+
const { head, body } = resolver.collect(modules, { criticalCss: true });
|
|
51
|
+
const page_html = await beasties.processHtml(await renderHtml({
|
|
52
|
+
appHtml: html,
|
|
53
|
+
meta,
|
|
54
|
+
head,
|
|
55
|
+
body,
|
|
56
|
+
initialState: JSON.stringify(state),
|
|
57
|
+
loadedModulesJson: JSON.stringify(loadedModules || []),
|
|
58
|
+
}), { url, modules });
|
|
59
|
+
|
|
60
|
+
// '/' → index.html, '/code/core/' и '/code/GLOSSARY' → <путь>/index.html,
|
|
61
|
+
// '/404' → 404.html (его отдаёт статический сервер на любой неизвестный адрес).
|
|
62
|
+
const file = url === '/404'
|
|
63
|
+
? path.join(dist_dir, '404.html')
|
|
64
|
+
: path.join(dist_dir, decodeURI(url).replace(/^\/|\/$/g, ''), 'index.html');
|
|
65
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
66
|
+
fs.writeFileSync(file, page_html);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`✓ ${path.relative(process.cwd(), dist_dir)}: ${urls.length - 1} страниц, 404.html`);
|
|
70
|
+
}
|