@oxeo/visual 0.0.0-stage → 0.2.0
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 +99 -2
- package/bin/oxeo-visual.js +2 -0
- package/dist/build.d.ts +23 -0
- package/dist/build.js +58 -0
- package/dist/cli/build-command.d.ts +5 -0
- package/dist/cli/build-command.js +57 -0
- package/dist/cli/dev.d.ts +6 -0
- package/dist/cli/dev.js +274 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +104 -0
- package/dist/cli/open.d.ts +1 -0
- package/dist/cli/open.js +21 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +13 -0
- package/dist/cli/run.d.ts +7 -0
- package/dist/cli/run.js +55 -0
- package/dist/cli/worker.d.ts +20 -0
- package/dist/cli/worker.js +94 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.js +26 -0
- package/dist/extract.d.ts +8 -0
- package/dist/extract.js +300 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +24 -0
- package/dist/layout/cards.d.ts +43 -0
- package/dist/layout/cards.js +121 -0
- package/dist/layout/place.d.ts +40 -0
- package/dist/layout/place.js +197 -0
- package/dist/layout/route.d.ts +30 -0
- package/dist/layout/route.js +192 -0
- package/dist/model.d.ts +60 -0
- package/dist/model.js +2 -0
- package/dist/project.d.ts +14 -0
- package/dist/project.js +11 -0
- package/dist/render/client.js +871 -0
- package/dist/render/html.d.ts +8 -0
- package/dist/render/html.js +211 -0
- package/dist/render/palette.d.ts +4 -0
- package/dist/render/palette.js +66 -0
- package/dist/render/svg.d.ts +20 -0
- package/dist/render/svg.js +222 -0
- package/dist/scheme-meta.d.ts +90 -0
- package/dist/scheme-meta.js +2 -0
- package/dist/text.d.ts +2 -0
- package/dist/text.js +12 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.js +143 -0
- package/package.json +36 -3
package/README.md
CHANGED
|
@@ -1,3 +1,100 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @oxeo/visual
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Интерактивная карта схемы данных oxeo. Модели, поля, embedded-блоки, варианты дискриминатора и связи берутся из
|
|
4
|
+
`DataScheme` и исходников схемы. Подсказки показывают JSDoc, декораторы и типы TS, у json-полей — интерфейс значения.
|
|
5
|
+
Проект задаёт только каркас карты: зоны, пояса, порядок файлов и цвета.
|
|
6
|
+
|
|
7
|
+
## Подключение
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@oxeo/visual": "*",
|
|
13
|
+
"ts-node": "^10.9.0"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"scheme:map": "oxeo-visual dev",
|
|
17
|
+
"scheme:map:build": "oxeo-visual build"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`ts-node` нужен, если конфиг написан на TypeScript: пакет берёт его из проекта, вместе с `tsconfig.json` проекта.
|
|
23
|
+
|
|
24
|
+
## Конфиг
|
|
25
|
+
|
|
26
|
+
`oxeo-visual.config.ts` рядом с `package.json`. Пути считаются от папки конфига.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { defineConfig } from '@oxeo/visual'
|
|
30
|
+
import { dataScheme } from './scheme'
|
|
31
|
+
|
|
32
|
+
export default defineConfig({
|
|
33
|
+
scheme: dataScheme,
|
|
34
|
+
sourceDir: './scheme',
|
|
35
|
+
output: { dir: './scheme', name: 'scheme-map' },
|
|
36
|
+
// Пояса сверху вниз, в каждом — зоны слева направо
|
|
37
|
+
bands: [
|
|
38
|
+
['sales', 'execution'],
|
|
39
|
+
['people', 'dictionaries']
|
|
40
|
+
],
|
|
41
|
+
zones: [
|
|
42
|
+
{ id: 'sales', title: 'Продажи', color: '#e2580b', colorDark: '#fb923c', files: [['sales.ts']] },
|
|
43
|
+
// Ряды файлов зоны сверху вниз
|
|
44
|
+
{
|
|
45
|
+
id: 'execution',
|
|
46
|
+
title: 'Исполнение',
|
|
47
|
+
color: '#0f8f86',
|
|
48
|
+
files: [['projects.ts', 'shifts.ts'], ['timesheet.ts']]
|
|
49
|
+
},
|
|
50
|
+
{ id: 'people', title: 'Люди', color: '#7c3aed', files: [['workers.ts']] },
|
|
51
|
+
{
|
|
52
|
+
id: 'dictionaries',
|
|
53
|
+
title: 'Справочники',
|
|
54
|
+
color: '#a87700',
|
|
55
|
+
files: [['dictionary.ts', 'settings.ts']]
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Поле | Назначение |
|
|
62
|
+
| ---------------- | --------------------------------------------------------------------------- |
|
|
63
|
+
| `scheme` | схема данных проекта |
|
|
64
|
+
| `sourceDir` | папка с классами схемы: файлы, JSDoc, декораторы, типы полей |
|
|
65
|
+
| `bands`, `zones` | каркас карты; файлы вне зон попадают в зону «Без зоны» |
|
|
66
|
+
| `hubMinSources` | с какого числа ссылающихся коллекций модель считается хабом, по умолчанию 7 |
|
|
67
|
+
| `tsconfig` | tsconfig для разрешения типов; по умолчанию ближайший к конфигу |
|
|
68
|
+
| `watch` | дополнительные пути для `dev`; папка конфига отслеживается всегда |
|
|
69
|
+
| `output` | куда `build` пишет `<name>.html` и `<name>.svg` |
|
|
70
|
+
| `spacing` | отступы раскладки, см. `DEFAULT_SPACING` |
|
|
71
|
+
|
|
72
|
+
Описание модели — JSDoc `/** … */` над её классом, описание поля — JSDoc над свойством.
|
|
73
|
+
|
|
74
|
+
## Команды
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
oxeo-visual dev [--config <файл>] [--port <номер>] [--no-open]
|
|
78
|
+
oxeo-visual build [--config <файл>] [--out <папка>]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`dev` поднимает сервер на `127.0.0.1:4870` (если порт занят — следующий свободный) и открывает карту в браузере.
|
|
82
|
+
Пока скрипт работает, изменения в папке конфига и в `watch` пересобирают карту. Страница перезагружается и
|
|
83
|
+
сохраняет масштаб, выделение и поиск, а при ошибке сборки показывает её первую строку. `Ctrl+C` останавливает
|
|
84
|
+
сервер, открытая страница сообщает, что карта больше не обновляется.
|
|
85
|
+
|
|
86
|
+
`build` один раз собирает карту и пишет самодостаточный HTML (без сети работает всё, кроме веб-шрифтов) и
|
|
87
|
+
статический SVG для документации.
|
|
88
|
+
|
|
89
|
+
Каждая сборка идёт в отдельном процессе: декораторы oxeo регистрируют метаданные в глобальном хранилище, и
|
|
90
|
+
повторная загрузка схемы в том же процессе их задвоила бы.
|
|
91
|
+
|
|
92
|
+
## API
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { buildSchemeMap, extractScheme } from '@oxeo/visual'
|
|
96
|
+
|
|
97
|
+
const { html, svg, metrics } = await buildSchemeMap(dataScheme, { sourceDir, tsconfig, config })
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Маршрутизация связей — [libavoid-js](https://github.com/Aksem/libavoid-js) (LGPL-2.1).
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type VisualConfig } from './config';
|
|
2
|
+
import { type RoutingMetrics } from './layout/route';
|
|
3
|
+
import type { SchemeModel } from './model';
|
|
4
|
+
import type { SchemeSource } from './scheme-meta';
|
|
5
|
+
export interface SchemeMapOptions {
|
|
6
|
+
sourceDir: string;
|
|
7
|
+
tsconfig?: string;
|
|
8
|
+
config: VisualConfig;
|
|
9
|
+
}
|
|
10
|
+
export interface SchemeMapResult {
|
|
11
|
+
html: string;
|
|
12
|
+
svg: string;
|
|
13
|
+
model: SchemeModel;
|
|
14
|
+
metrics: {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
cards: number;
|
|
18
|
+
hubs: string[];
|
|
19
|
+
all: RoutingMetrics;
|
|
20
|
+
nohub: RoutingMetrics;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export declare const buildSchemeMap: (dataScheme: SchemeSource, { sourceDir, tsconfig, config }: SchemeMapOptions) => Promise<SchemeMapResult>;
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSchemeMap = void 0;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const extract_1 = require("./extract");
|
|
6
|
+
const cards_1 = require("./layout/cards");
|
|
7
|
+
const place_1 = require("./layout/place");
|
|
8
|
+
const route_1 = require("./layout/route");
|
|
9
|
+
const html_1 = require("./render/html");
|
|
10
|
+
const palette_1 = require("./render/palette");
|
|
11
|
+
const svg_1 = require("./render/svg");
|
|
12
|
+
const text_1 = require("./text");
|
|
13
|
+
const buildSchemeMap = async (dataScheme, { sourceDir, tsconfig, config }) => {
|
|
14
|
+
const model = (0, extract_1.extractScheme)(dataScheme, { sourceDir, tsconfig });
|
|
15
|
+
const spacing = (0, config_1.resolveSpacing)(config);
|
|
16
|
+
const cards = new Map(model.entities.map((e) => [e.key, (0, cards_1.buildCard)(e, spacing)]));
|
|
17
|
+
const placement = (0, place_1.place)(model, cards, config, spacing);
|
|
18
|
+
const sources = new Map();
|
|
19
|
+
for (const r of model.relations) {
|
|
20
|
+
if (r.kind === 'own')
|
|
21
|
+
continue;
|
|
22
|
+
sources.set(r.target, (sources.get(r.target) ?? new Set()).add(r.source));
|
|
23
|
+
}
|
|
24
|
+
const sourcesCount = new Map([...sources].map(([key, set]) => [key, set.size]));
|
|
25
|
+
const hubs = new Set([...sourcesCount].filter(([, n]) => n >= (config.hubMinSources ?? 7)).map(([key]) => key));
|
|
26
|
+
const all = await (0, route_1.routeRelations)(model.relations, placement, cards, spacing);
|
|
27
|
+
const nohub = await (0, route_1.routeRelations)(model.relations.filter((r) => r.kind === 'own' || !hubs.has(r.target)), placement, cards, spacing);
|
|
28
|
+
const layers = { all: all.routes, nohub: nohub.routes };
|
|
29
|
+
const renderOptions = { config, spacing, placement, layers, hubs, sourcesCount };
|
|
30
|
+
const zones = placement.zones.map((z) => z.zone);
|
|
31
|
+
const zoneOf = Object.fromEntries([...placement.cards].map(([key, c]) => [key, c.zone]));
|
|
32
|
+
const data = {
|
|
33
|
+
generatedAt: new Date().toISOString(),
|
|
34
|
+
zones: zones.map((z) => ({ id: z.id, title: z.title, colorVar: (0, palette_1.zoneVar)(z.id) })),
|
|
35
|
+
entities: model.entities,
|
|
36
|
+
types: model.types,
|
|
37
|
+
relations: model.relations,
|
|
38
|
+
zoneOf,
|
|
39
|
+
hubs: [...hubs],
|
|
40
|
+
sources: Object.fromEntries(sourcesCount),
|
|
41
|
+
size: { width: placement.width, height: placement.height }
|
|
42
|
+
};
|
|
43
|
+
const meta = `${(0, text_1.plural)(model.entities.length, ['коллекция', 'коллекции', 'коллекций'])} · ${(0, text_1.plural)(model.relations.length, ['связь', 'связи', 'связей'])} · ${(0, text_1.plural)(model.files.length, ['файл', 'файла', 'файлов'])}`;
|
|
44
|
+
return {
|
|
45
|
+
html: (0, html_1.renderPage)({ zones, mapSvg: (0, svg_1.renderMapSvg)(renderOptions), data, meta }),
|
|
46
|
+
svg: (0, svg_1.renderMapSvg)({ ...renderOptions, standalone: true }),
|
|
47
|
+
model,
|
|
48
|
+
metrics: {
|
|
49
|
+
width: Math.round(placement.width),
|
|
50
|
+
height: Math.round(placement.height),
|
|
51
|
+
cards: cards.size,
|
|
52
|
+
hubs: [...hubs],
|
|
53
|
+
all: all.metrics,
|
|
54
|
+
nohub: nohub.metrics
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
exports.buildSchemeMap = buildSchemeMap;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.buildOnce = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const output_1 = require("./output");
|
|
40
|
+
const run_1 = require("./run");
|
|
41
|
+
const buildOnce = async ({ configPath, out }) => {
|
|
42
|
+
const started = Date.now();
|
|
43
|
+
const message = await (0, run_1.startBuild)(configPath).done;
|
|
44
|
+
if (message.type === 'error') {
|
|
45
|
+
(0, output_1.log)(`сборка не удалась:\n${message.message}`);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const dir = out ? path.resolve(out) : message.settings.output.dir;
|
|
50
|
+
const base = path.join(dir, message.settings.output.name);
|
|
51
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
52
|
+
fs.writeFileSync(`${base}.html`, message.html);
|
|
53
|
+
fs.writeFileSync(`${base}.svg`, message.svg);
|
|
54
|
+
(0, output_1.log)((0, output_1.describeBuild)(message.metrics, Date.now() - started));
|
|
55
|
+
(0, output_1.log)(`${path.relative(process.cwd(), `${base}.html`)}, ${path.relative(process.cwd(), `${base}.svg`)}`);
|
|
56
|
+
};
|
|
57
|
+
exports.buildOnce = buildOnce;
|
package/dist/cli/dev.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.dev = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const http = __importStar(require("http"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const open_1 = require("./open");
|
|
41
|
+
const output_1 = require("./output");
|
|
42
|
+
const run_1 = require("./run");
|
|
43
|
+
const EVENTS_PATH = '/__xeo-visual/events';
|
|
44
|
+
const IGNORED_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage']);
|
|
45
|
+
const WATCHED_FILE = /\.(c|m)?[jt]sx?$|\.json$/;
|
|
46
|
+
const devClient = (build) => `
|
|
47
|
+
<style>
|
|
48
|
+
#xv-dev { position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 100; max-width: min(720px, calc(100vw - 32px)); padding: 8px 14px; border-radius: 6px; font: 500 13px/1.45 system-ui, sans-serif; box-shadow: 0 8px 24px rgb(0 0 0 / 0.18); }
|
|
49
|
+
#xv-dev[data-tone='info'] { background: var(--xv-surface, #fff); color: var(--xv-text, #18181b); border: 1px solid var(--xv-line-strong, #d4d4d8); }
|
|
50
|
+
#xv-dev[data-tone='error'] { background: #b42318; color: #fff; }
|
|
51
|
+
#xv-dev[data-tone='stop'] { background: #3f3f46; color: #fff; }
|
|
52
|
+
</style>
|
|
53
|
+
<script>
|
|
54
|
+
;(() => {
|
|
55
|
+
const BUILD = ${JSON.stringify(build)}
|
|
56
|
+
const bar = document.createElement('div')
|
|
57
|
+
bar.id = 'xv-dev'
|
|
58
|
+
bar.setAttribute('role', 'status')
|
|
59
|
+
bar.hidden = true
|
|
60
|
+
document.body.appendChild(bar)
|
|
61
|
+
const show = (text, tone) => {
|
|
62
|
+
bar.textContent = text
|
|
63
|
+
bar.dataset.tone = tone
|
|
64
|
+
bar.hidden = false
|
|
65
|
+
}
|
|
66
|
+
const reload = () => {
|
|
67
|
+
try {
|
|
68
|
+
if (window.__xv) sessionStorage.setItem('xv-restore', JSON.stringify(window.__xv.state()))
|
|
69
|
+
} catch (e) {}
|
|
70
|
+
location.reload()
|
|
71
|
+
}
|
|
72
|
+
const events = new EventSource(${JSON.stringify(EVENTS_PATH)})
|
|
73
|
+
events.addEventListener('hello', (e) => {
|
|
74
|
+
const { build, error } = JSON.parse(e.data)
|
|
75
|
+
if (build && build !== BUILD) return reload()
|
|
76
|
+
if (error) show('Сборка не удалась: ' + error + ' — подробности в терминале', 'error')
|
|
77
|
+
else bar.hidden = true
|
|
78
|
+
})
|
|
79
|
+
events.addEventListener('building', () => show('Пересобираю карту…', 'info'))
|
|
80
|
+
events.addEventListener('built', (e) => {
|
|
81
|
+
if (JSON.parse(e.data).build !== BUILD) reload()
|
|
82
|
+
})
|
|
83
|
+
events.addEventListener('failed', (e) => {
|
|
84
|
+
show('Сборка не удалась: ' + JSON.parse(e.data).message + ' — подробности в терминале', 'error')
|
|
85
|
+
})
|
|
86
|
+
events.addEventListener('shutdown', () => {
|
|
87
|
+
events.close()
|
|
88
|
+
show('oxeo-visual остановлен: карта больше не обновляется', 'stop')
|
|
89
|
+
})
|
|
90
|
+
events.onerror = () => {
|
|
91
|
+
if (bar.dataset.tone !== 'stop') show('Нет связи с oxeo-visual: сервер остановлен или перезапускается', 'stop')
|
|
92
|
+
}
|
|
93
|
+
})()
|
|
94
|
+
</script>
|
|
95
|
+
`;
|
|
96
|
+
const waitingPage = () => `<!doctype html>
|
|
97
|
+
<meta charset="utf-8" />
|
|
98
|
+
<title>oxeo-visual</title>
|
|
99
|
+
<style>
|
|
100
|
+
body { margin: 0; height: 100vh; display: grid; place-items: center; background: #f6f6f7; color: #63636e; font: 14px/1.5 system-ui, sans-serif; }
|
|
101
|
+
@media (prefers-color-scheme: dark) { body { background: #0c0c0e; color: #a3a3ad; } }
|
|
102
|
+
</style>
|
|
103
|
+
<p>Собираю карту схемы…</p>
|
|
104
|
+
${devClient(null)}`;
|
|
105
|
+
const watchTree = (root, listener) => {
|
|
106
|
+
const handles = [];
|
|
107
|
+
const add = (target) => {
|
|
108
|
+
try {
|
|
109
|
+
handles.push(fs.watch(target, (_event, file) => listener(file ? path.join(target, String(file)) : target)));
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
};
|
|
113
|
+
const visit = (dir) => {
|
|
114
|
+
add(dir);
|
|
115
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
116
|
+
if (entry.isDirectory() && !IGNORED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
117
|
+
visit(path.join(dir, entry.name));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
if (fs.statSync(root).isDirectory())
|
|
122
|
+
visit(root);
|
|
123
|
+
else
|
|
124
|
+
add(root);
|
|
125
|
+
return () => handles.forEach((handle) => handle.close());
|
|
126
|
+
};
|
|
127
|
+
const listen = async (server, port) => {
|
|
128
|
+
for (let candidate = port; candidate < port + 20; candidate++) {
|
|
129
|
+
try {
|
|
130
|
+
await new Promise((resolve, reject) => {
|
|
131
|
+
server.once('error', reject);
|
|
132
|
+
server.listen(candidate, '127.0.0.1', () => {
|
|
133
|
+
server.off('error', reject);
|
|
134
|
+
resolve();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
return candidate;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (error.code !== 'EADDRINUSE')
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`порты ${port}–${port + 19} заняты, укажите другой: --port <номер>`);
|
|
145
|
+
};
|
|
146
|
+
const dev = async ({ configPath, port, open }) => {
|
|
147
|
+
let html = null;
|
|
148
|
+
let svg = null;
|
|
149
|
+
let build = null;
|
|
150
|
+
let error = null;
|
|
151
|
+
let current = null;
|
|
152
|
+
let queued = false;
|
|
153
|
+
let stopping = false;
|
|
154
|
+
const clients = new Set();
|
|
155
|
+
const broadcast = (event, data = {}) => {
|
|
156
|
+
for (const res of clients)
|
|
157
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
158
|
+
};
|
|
159
|
+
const server = http.createServer((req, res) => {
|
|
160
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
161
|
+
if (url.pathname === EVENTS_PATH) {
|
|
162
|
+
res.writeHead(200, {
|
|
163
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
164
|
+
'Cache-Control': 'no-cache',
|
|
165
|
+
Connection: 'keep-alive'
|
|
166
|
+
});
|
|
167
|
+
res.write(`event: hello\ndata: ${JSON.stringify({ build, error: error && (0, output_1.firstLine)(error) })}\n\n`);
|
|
168
|
+
clients.add(res);
|
|
169
|
+
req.on('close', () => clients.delete(res));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (url.pathname === '/' || url.pathname === '/index.html') {
|
|
173
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
174
|
+
res.end(html ? html + devClient(build) : waitingPage());
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (url.pathname.endsWith('.svg') && svg) {
|
|
178
|
+
res.writeHead(200, {
|
|
179
|
+
'Content-Type': 'image/svg+xml; charset=utf-8',
|
|
180
|
+
'Cache-Control': 'no-store'
|
|
181
|
+
});
|
|
182
|
+
res.end(svg);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
186
|
+
res.end('Не найдено');
|
|
187
|
+
});
|
|
188
|
+
const ping = setInterval(() => clients.forEach((res) => res.write(': ping\n\n')), 25_000);
|
|
189
|
+
const stoppers = new Map();
|
|
190
|
+
let timer = null;
|
|
191
|
+
let changed = '';
|
|
192
|
+
const onChange = (file) => {
|
|
193
|
+
if (stopping || !WATCHED_FILE.test(file))
|
|
194
|
+
return;
|
|
195
|
+
changed = file;
|
|
196
|
+
if (timer)
|
|
197
|
+
clearTimeout(timer);
|
|
198
|
+
timer = setTimeout(() => {
|
|
199
|
+
timer = null;
|
|
200
|
+
(0, output_1.log)(`изменение: ${path.relative(process.cwd(), changed) || changed}`);
|
|
201
|
+
void rebuild();
|
|
202
|
+
}, 200);
|
|
203
|
+
};
|
|
204
|
+
const watch = (roots) => {
|
|
205
|
+
const unique = roots.filter((root) => !roots.some((other) => root !== other && root.startsWith(other + path.sep)));
|
|
206
|
+
for (const [root, stop] of stoppers) {
|
|
207
|
+
if (!unique.includes(root)) {
|
|
208
|
+
stop();
|
|
209
|
+
stoppers.delete(root);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const root of unique) {
|
|
213
|
+
if (!stoppers.has(root) && fs.existsSync(root))
|
|
214
|
+
stoppers.set(root, watchTree(root, onChange));
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const rebuild = async () => {
|
|
218
|
+
if (current) {
|
|
219
|
+
queued = true;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const started = Date.now();
|
|
223
|
+
broadcast('building');
|
|
224
|
+
current = (0, run_1.startBuild)(configPath);
|
|
225
|
+
const message = await current.done;
|
|
226
|
+
current = null;
|
|
227
|
+
if (stopping)
|
|
228
|
+
return;
|
|
229
|
+
if (message.type === 'result') {
|
|
230
|
+
html = message.html;
|
|
231
|
+
svg = message.svg;
|
|
232
|
+
error = null;
|
|
233
|
+
build = String(started);
|
|
234
|
+
(0, output_1.log)((0, output_1.describeBuild)(message.metrics, Date.now() - started));
|
|
235
|
+
broadcast('built', { build });
|
|
236
|
+
watch(message.settings.watch);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
error = message.message;
|
|
240
|
+
(0, output_1.log)(`сборка не удалась:\n${message.message}`);
|
|
241
|
+
broadcast('failed', { message: (0, output_1.firstLine)(message.message) });
|
|
242
|
+
}
|
|
243
|
+
if (queued) {
|
|
244
|
+
queued = false;
|
|
245
|
+
void rebuild();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const stop = () => {
|
|
249
|
+
if (stopping)
|
|
250
|
+
return;
|
|
251
|
+
stopping = true;
|
|
252
|
+
broadcast('shutdown');
|
|
253
|
+
clients.forEach((res) => res.end());
|
|
254
|
+
clearInterval(ping);
|
|
255
|
+
if (timer)
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
stoppers.forEach((stopWatching) => stopWatching());
|
|
258
|
+
current?.child.kill();
|
|
259
|
+
server.close(() => process.exit(0));
|
|
260
|
+
setTimeout(() => process.exit(0), 500).unref();
|
|
261
|
+
(0, output_1.log)('остановлен');
|
|
262
|
+
};
|
|
263
|
+
process.on('SIGINT', stop);
|
|
264
|
+
process.on('SIGTERM', stop);
|
|
265
|
+
process.on('SIGHUP', stop);
|
|
266
|
+
const actualPort = await listen(server, port);
|
|
267
|
+
const url = `http://127.0.0.1:${actualPort}`;
|
|
268
|
+
(0, output_1.log)(`карта схемы: ${url} — Ctrl+C, чтобы остановить`);
|
|
269
|
+
watch([path.dirname(configPath)]);
|
|
270
|
+
if (open)
|
|
271
|
+
(0, open_1.openBrowser)(url);
|
|
272
|
+
await rebuild();
|
|
273
|
+
};
|
|
274
|
+
exports.dev = dev;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fs = __importStar(require("fs"));
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const project_1 = require("../project");
|
|
39
|
+
const build_command_1 = require("./build-command");
|
|
40
|
+
const dev_1 = require("./dev");
|
|
41
|
+
const output_1 = require("./output");
|
|
42
|
+
const HELP = `oxeo-visual — карта схемы данных oxeo
|
|
43
|
+
|
|
44
|
+
oxeo-visual dev [--config <файл>] [--port <номер>] [--no-open]
|
|
45
|
+
oxeo-visual build [--config <файл>] [--out <папка>]
|
|
46
|
+
|
|
47
|
+
dev сервер с картой в браузере: пересобирает карту при изменении схемы,
|
|
48
|
+
останавливается вместе со скриптом (Ctrl+C)
|
|
49
|
+
build пишет <name>.html и <name>.svg в output.dir из конфига или в --out
|
|
50
|
+
|
|
51
|
+
Конфиг по умолчанию — ${project_1.CONFIG_FILES.join(', ')} в текущей папке.
|
|
52
|
+
`;
|
|
53
|
+
const DEFAULT_PORT = 4870;
|
|
54
|
+
const parse = (argv) => {
|
|
55
|
+
const [command, ...rest] = argv;
|
|
56
|
+
const flags = new Map();
|
|
57
|
+
for (let i = 0; i < rest.length; i++) {
|
|
58
|
+
const arg = rest[i];
|
|
59
|
+
if (!arg.startsWith('--'))
|
|
60
|
+
continue;
|
|
61
|
+
const [name, inline] = arg.slice(2).split('=', 2);
|
|
62
|
+
if (inline !== undefined)
|
|
63
|
+
flags.set(name, inline);
|
|
64
|
+
else if (rest[i + 1] && !rest[i + 1].startsWith('--'))
|
|
65
|
+
flags.set(name, rest[++i]);
|
|
66
|
+
else
|
|
67
|
+
flags.set(name, true);
|
|
68
|
+
}
|
|
69
|
+
return { command, flags };
|
|
70
|
+
};
|
|
71
|
+
const findConfig = (explicit) => {
|
|
72
|
+
if (explicit) {
|
|
73
|
+
const file = path.resolve(explicit);
|
|
74
|
+
if (!fs.existsSync(file))
|
|
75
|
+
throw new Error(`конфиг не найден: ${file}`);
|
|
76
|
+
return file;
|
|
77
|
+
}
|
|
78
|
+
const found = project_1.CONFIG_FILES.map((name) => path.resolve(name)).find((file) => fs.existsSync(file));
|
|
79
|
+
if (!found)
|
|
80
|
+
throw new Error(`нет ${project_1.CONFIG_FILES[0]} в ${process.cwd()} — создайте его или укажите --config`);
|
|
81
|
+
return found;
|
|
82
|
+
};
|
|
83
|
+
const main = async () => {
|
|
84
|
+
const { command, flags } = parse(process.argv.slice(2));
|
|
85
|
+
const value = (name) => typeof flags.get(name) === 'string' ? flags.get(name) : undefined;
|
|
86
|
+
if (command === 'dev') {
|
|
87
|
+
const port = Number(value('port') ?? process.env.OXEO_VISUAL_PORT ?? DEFAULT_PORT);
|
|
88
|
+
if (!Number.isInteger(port) || port <= 0)
|
|
89
|
+
throw new Error(`неверный порт: ${value('port')}`);
|
|
90
|
+
await (0, dev_1.dev)({ configPath: findConfig(value('config')), port, open: !flags.has('no-open') });
|
|
91
|
+
}
|
|
92
|
+
else if (command === 'build') {
|
|
93
|
+
await (0, build_command_1.buildOnce)({ configPath: findConfig(value('config')), out: value('out') });
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
process.stdout.write(HELP);
|
|
97
|
+
if (command && command !== 'help' && command !== '--help')
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
main().catch((error) => {
|
|
102
|
+
(0, output_1.log)(error instanceof Error ? error.message : String(error));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const openBrowser: (url: string) => void;
|
package/dist/cli/open.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openBrowser = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const output_1 = require("./output");
|
|
6
|
+
const openBrowser = (url) => {
|
|
7
|
+
const [command, args] = process.platform === 'darwin'
|
|
8
|
+
? ['open', [url]]
|
|
9
|
+
: process.platform === 'win32'
|
|
10
|
+
? ['cmd', ['/c', 'start', '""', url]]
|
|
11
|
+
: ['xdg-open', [url]];
|
|
12
|
+
try {
|
|
13
|
+
const child = (0, child_process_1.spawn)(command, args, { stdio: 'ignore', detached: true });
|
|
14
|
+
child.on('error', () => (0, output_1.log)(`откройте в браузере: ${url}`));
|
|
15
|
+
child.unref();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
(0, output_1.log)(`откройте в браузере: ${url}`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
exports.openBrowser = openBrowser;
|