@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 CHANGED
@@ -1,19 +1,21 @@
1
1
  # @ozdao/scriptorium
2
2
 
3
- Сайт документации из папок markdown на @ozdao/martyrs (Vue 3, rspack, SSR). Все `.md` репозитория — страницы; `AGENTS.md` — индекс своей папки; адреса без `.md`.
3
+ Сайт документации из папок markdown на @ozdao/martyrs (Vue 3, rspack). Все `.md` репозитория — страницы; `AGENTS.md` — индекс своей папки; адреса без `.md`. Работает как VitePress: `dev` — сервер разработки, `build` — статический сайт в папке `dist/`, деплой — раздать папку.
4
4
 
5
5
  ## Раскладка у пользователя
6
6
 
7
- Как у VitePress: сайт живёт в одной папке `documentation/` в корне репозитория документации, команды запускаются из неё. Страницы — все `.md` репозитория (папка над `documentation/`); сама `documentation/` в сайт не входит.
7
+ Сайт живёт в одной папке `documentation/` в корне репозитория документации, команды запускаются из неё. Страницы — все `.md` репозитория (папка над `documentation/`); сама `documentation/` в сайт не входит.
8
8
 
9
9
  ```
10
10
  documentation/
11
- ├── package.json зависимости и скрипты сайта
12
- ├── node_modules/ единственный node_modules репозитория документации
13
- ├── config.js конфиг сайта (обязателен)
14
- ├── theme/ логотип, блок над страницей, стили (необязательно)
15
- ├── public/ статика сайта: своё (favicon) + генерируемое сборкой
16
- └── builds/ сборка
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.0",
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-зависимости пакета, и указывать их обязательно. Сборка martyrs берёт `vue` из `documentation/node_modules/vue`, а шаблон service worker — из `documentation/node_modules/@ozdao/martyrs`. pnpm кладёт наверх `node_modules` только прямые зависимости проекта. Без явного указания этих пакетов там нет, и сборка падает.
47
+ `@ozdao/martyrs`, `vue`, `vue-router`, `vue-i18n` — peer-зависимости пакета, указывать обязательно: сборка martyrs берёт `vue` из `documentation/node_modules/vue`, а pnpm кладёт наверх только прямые зависимости проекта.
48
48
 
49
- `packageManager: pnpm@10.8.0` нужен, как в шаблоне create-martyrs: pnpm 11 отказывается ставить git-зависимость martyrs (`uWebSockets.js`, `blockExoticSubdeps`) и передаёт установку pnpm 10.
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 # разработка, http://localhost:8200 (порт — PORT)
60
- pnpm build # сборка в builds/
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: ['templates'] },
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
- `documentation/public/` отдаётся статикой раньше статики пакета: свой `favicon/` перекрывает умолчание.
97
+ Картинки в `.md` — относительными путями от файла; сборка кладёт их в ассеты (png, jpg, webp, avif, svg, gif).
91
98
 
92
99
  ## Что генерируется
93
100
 
94
- Сборка martyrs пишет в `documentation/`. В `.gitignore` репозитория документации:
101
+ В `.gitignore` репозитория документации:
95
102
 
96
103
  ```
97
104
  documentation/node_modules
98
105
  documentation/builds
99
- documentation/public/sw.js
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
- - `builds/` — клиентская и серверная сборка;
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`. При `link:` pnpm не ставит зависимости пакета в проект, поэтому peer-зависимости выше там тоже обязательны.
119
+ Подключение к репозиторию документации без публикации — `"@ozdao/scriptorium": "link:<путь к пакету>"` в `documentation/package.json`; peer-зависимости выше там тоже обязательны.
119
120
 
120
- Устройство: `bin/scriptorium.js` — CLI и сборка конфигов rspack поверх builder'а martyrs (корень приложения = `documentation/`, вход и сервер — файлы пакета); `src/` — приложение сайта (клиент, `server.js`, loader'ы markdown, списка страниц и индекса поиска).
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/` — раскладка сайта.
@@ -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
- // notifications martyrs, которого здесь нет. Плагин в dev писал бы sw.js в
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
- await run(command, configs);
182
-
183
- // Воркер бэкенда в dev (тот же файл, не главный поток) и сборка адрес не печатают.
184
- if (isMainThread && command !== 'build') {
185
- console.log(`Docs: http://localhost:${process.env.PORT}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ozdao/scriptorium",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Documentation site generator on @ozdao/martyrs: markdown folders to an SSR site",
5
5
  "author": "OZ DAO <hello@ozdao.com>",
6
6
  "license": "GPL-3.0-or-later",
package/src/server.js CHANGED
@@ -1,5 +1,6 @@
1
- // Сервер сайта документации: express + статика. Порт слушает раннер martyrs
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, docs_root, loadSiteConfig } from './content/scan.js';
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
+ }