asljs-diagram-cli 0.0.0-stage → 0.1.1
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.md +51 -0
- package/README.md +84 -2
- package/bin/diagram.js +8 -0
- package/dist/cli.d.ts +26 -0
- package/dist/cli.js +80 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/open.d.ts +2 -0
- package/dist/open.js +20 -0
- package/dist/page.d.ts +6 -0
- package/dist/page.js +161 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +63 -0
- package/docs/Diagram CLI.md +78 -0
- package/package.json +66 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Permissive No-Derivatives License 1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexandrite Software Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software (the "Software") to:
|
|
7
|
+
|
|
8
|
+
1. Use the Software for any purpose, including commercial purposes.
|
|
9
|
+
2. Copy and redistribute the Software, including as part of another application,
|
|
10
|
+
library, or package.
|
|
11
|
+
3. Modify the Software for private or internal use.
|
|
12
|
+
|
|
13
|
+
Subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
## 1. No Distribution of Modified Versions
|
|
16
|
+
|
|
17
|
+
Modified versions of the Software, or substantial portions thereof, must not be
|
|
18
|
+
distributed without prior written permission from the copyright holder.
|
|
19
|
+
|
|
20
|
+
Routine build transformations, including minification, transpilation, and
|
|
21
|
+
bundling, are permitted for distribution, provided they do not intentionally
|
|
22
|
+
alter the Software's functionality.
|
|
23
|
+
|
|
24
|
+
## 2. Licence and Copyright Preservation
|
|
25
|
+
|
|
26
|
+
All distributed copies of the Software must retain this copyright notice and
|
|
27
|
+
licence.
|
|
28
|
+
|
|
29
|
+
The Software itself, including copies incorporated into other applications,
|
|
30
|
+
libraries, or packages, remains subject to this licence.
|
|
31
|
+
|
|
32
|
+
The Software must not be sublicensed or redistributed under a different licence.
|
|
33
|
+
|
|
34
|
+
## 3. Use in Other Software
|
|
35
|
+
|
|
36
|
+
Applications and libraries that merely use, import, link to, or bundle the
|
|
37
|
+
Software are not considered modified versions of the Software and may be
|
|
38
|
+
distributed under any licence.
|
|
39
|
+
|
|
40
|
+
Such use, incorporation, or bundling does not grant permission to relicense the
|
|
41
|
+
Software or distribute modified versions of it.
|
|
42
|
+
|
|
43
|
+
### 4. No Warranty
|
|
44
|
+
|
|
45
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
46
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
47
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
48
|
+
|
|
49
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR
|
|
50
|
+
OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR ITS
|
|
51
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,85 @@
|
|
|
1
|
-
#
|
|
1
|
+
# diagram-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Part of [Alexandrite Software Library][#1] - a set of high-quality and
|
|
4
|
+
> performant JavaScript libraries for everyday use.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
`asljs-diagram-cli` is the `diagram` command. `diagram view <path>` reads a
|
|
9
|
+
diagram file, a graph of nodes and links or a diagram of visual elements, and
|
|
10
|
+
shows it in the browser with [asljs-ui-diagram][LIB]: pan, zoom, select, move
|
|
11
|
+
and resize, and for a graph, add, link and delete nodes.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
diagram view build/ts-classes.diagram.json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
Showing build/ts-classes.diagram.json at http://127.0.0.1:52144/
|
|
19
|
+
Press Ctrl+C to stop.
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Scope
|
|
23
|
+
|
|
24
|
+
- **Files.** A JSON document in the `asljs-ui-diagram` format: `"type": "graph"`
|
|
25
|
+
with `nodes` and `links`, or `"type": "diagram"` with `elements`. The file is
|
|
26
|
+
checked before anything is served, and an error names the invalid value.
|
|
27
|
+
- **Local and in memory.** The command serves the page on `127.0.0.1` and opens
|
|
28
|
+
the default browser. Edits stay in the page; the file is read again on each
|
|
29
|
+
reload and never written.
|
|
30
|
+
- **Generated diagrams.** `toolkit build-ts-files-diagram` writes the import
|
|
31
|
+
graph of TypeScript sources in this format, and `toolkit
|
|
32
|
+
build-ts-class-diagram` their class diagram.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install --global asljs-diagram-cli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
NPM Package: [asljs-diagram-cli][NPM]
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# A free port, and the browser opens
|
|
46
|
+
diagram view graph.json
|
|
47
|
+
|
|
48
|
+
# A fixed port, without opening the browser
|
|
49
|
+
diagram view graph.json --port 8080 --no-open
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A minimal graph file:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{ "type": "graph",
|
|
56
|
+
"nodes": [ { "id": "a", "label": "Start", "x": 0, "y": 0 },
|
|
57
|
+
{ "id": "b", "label": "End", "x": 220, "y": 0 } ],
|
|
58
|
+
"links": [ { "from": "a", "to": "b" } ] }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Further reading
|
|
62
|
+
|
|
63
|
+
- [diagram view][DOC] - the options, what the page does, and the errors.
|
|
64
|
+
- [Diagram][FMT] - the document format, in `asljs-ui-diagram`.
|
|
65
|
+
|
|
66
|
+
Questions and bugs: [asljs/issues][ISS].
|
|
67
|
+
|
|
68
|
+
## Related packages
|
|
69
|
+
|
|
70
|
+
- [ui-diagram][LIB] - the diagram component and models the page uses.
|
|
71
|
+
- [toolkit][TLK] - `build-ts-files-diagram` and `build-ts-class-diagram` turn
|
|
72
|
+
`.structure.json` files into diagram files.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT License. See [LICENSE][LIC] for details.
|
|
77
|
+
|
|
78
|
+
[#1]: https://github.com/AlexandriteSoftware/asljs
|
|
79
|
+
[DOC]: <docs/Diagram CLI.md>
|
|
80
|
+
[FMT]: ../../ui/diagram/docs/Diagram.md#documents
|
|
81
|
+
[ISS]: https://github.com/AlexandriteSoftware/asljs/issues
|
|
82
|
+
[LIB]: ../../ui/diagram/README.md
|
|
83
|
+
[LIC]: LICENSE.md
|
|
84
|
+
[NPM]: https://www.npmjs.com/package/asljs-diagram-cli
|
|
85
|
+
[TLK]: ../toolkit/README.md
|
package/bin/diagram.js
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type CommandIo } from 'asljs-appbase';
|
|
2
|
+
import { type Viewer } from './server.js';
|
|
3
|
+
/**
|
|
4
|
+
* The configuration file, looked for in the current directory and its
|
|
5
|
+
* parents.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CONFIG_FILE = "diagram.json";
|
|
8
|
+
/**
|
|
9
|
+
* What the command runs with: the folder `diagram.json` is looked for from,
|
|
10
|
+
* the environment, the output, and how it opens and waits; replaced in tests.
|
|
11
|
+
*/
|
|
12
|
+
export interface CliIo extends CommandIo {
|
|
13
|
+
/** Opens the viewer's address; the default opens the browser. */
|
|
14
|
+
open?: (url: string) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Resolves when the viewer should stop; the default waits for Ctrl+C or
|
|
17
|
+
* SIGTERM.
|
|
18
|
+
*/
|
|
19
|
+
untilStopped?: (viewer: Viewer) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Runs the `diagram` command line, with a logger provider made from
|
|
23
|
+
* `--loglevel`, `--logfile`, `--logformat`, the `DIAGRAM_LOG_` variables and
|
|
24
|
+
* `diagram.json`, and returns the exit code.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runCli(args: string[], io?: CliIo): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { commandLine, configBoolean, configPort, configString, loadConfig, packageVersion, parsePort, runMain } from 'asljs-appbase';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { openInBrowser } from './open.js';
|
|
4
|
+
import { startViewer } from './server.js';
|
|
5
|
+
/**
|
|
6
|
+
* The configuration file, looked for in the current directory and its
|
|
7
|
+
* parents.
|
|
8
|
+
*/
|
|
9
|
+
export const CONFIG_FILE = 'diagram.json';
|
|
10
|
+
/**
|
|
11
|
+
* Runs the `diagram` command line, with a logger provider made from
|
|
12
|
+
* `--loglevel`, `--logfile`, `--logformat`, the `DIAGRAM_LOG_` variables and
|
|
13
|
+
* `diagram.json`, and returns the exit code.
|
|
14
|
+
*/
|
|
15
|
+
export async function runCli(args, io = { cwd: process.cwd(),
|
|
16
|
+
env: process.env,
|
|
17
|
+
stdout: process.stdout,
|
|
18
|
+
stderr: process.stderr }) {
|
|
19
|
+
return await runMain(args, io, { prefix: 'DIAGRAM_LOG_',
|
|
20
|
+
configFile: CONFIG_FILE }, async (loggerProvider) => {
|
|
21
|
+
const cli = createCli(io, loggerProvider);
|
|
22
|
+
if (args.length === 0) {
|
|
23
|
+
cli.outputHelp();
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
await cli.parseAsync(args, { from: 'user' });
|
|
27
|
+
return 0;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function createCli(io, loggerProvider) {
|
|
31
|
+
const cli = commandLine(new Command('diagram'), io);
|
|
32
|
+
cli
|
|
33
|
+
.description('Shows diagram files in the browser.')
|
|
34
|
+
.helpCommand(false);
|
|
35
|
+
cli.command('view')
|
|
36
|
+
.description('Serve a diagram file, a graph or a diagram of visual elements, and open it in the browser.')
|
|
37
|
+
.argument('<path>', 'the diagram JSON file')
|
|
38
|
+
.option('--port <port>', 'the port to listen on; 0, the default, picks a free one')
|
|
39
|
+
.option('--host <host>', 'the address to listen on; 127.0.0.1 by default')
|
|
40
|
+
.option('--no-open', 'do not open the browser')
|
|
41
|
+
.action(async (file, options, command) => {
|
|
42
|
+
const config = await loadConfig(io.cwd, CONFIG_FILE);
|
|
43
|
+
const port = parsePort(options.port)
|
|
44
|
+
?? configPort(config)
|
|
45
|
+
?? 0;
|
|
46
|
+
const host = options.host
|
|
47
|
+
?? configString(config, 'host')
|
|
48
|
+
?? '127.0.0.1';
|
|
49
|
+
const open = command.getOptionValueSource('open') === 'cli'
|
|
50
|
+
? options.open
|
|
51
|
+
: configBoolean(config, 'open')
|
|
52
|
+
?? true;
|
|
53
|
+
const viewer = await startViewer({ file,
|
|
54
|
+
port,
|
|
55
|
+
host,
|
|
56
|
+
logger: loggerProvider.getLogger('diagram') });
|
|
57
|
+
io.stdout.write(`Showing ${file} at ${viewer.url}\nPress Ctrl+C to stop.\n`);
|
|
58
|
+
if (open) {
|
|
59
|
+
(io.open ?? openInBrowser)(viewer.url);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
await (io.untilStopped ?? untilSignal)(viewer);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await viewer.close();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
cli.command('version')
|
|
69
|
+
.description('Print the version.')
|
|
70
|
+
.action(() => {
|
|
71
|
+
io.stdout.write(`${packageVersion(import.meta.url)}\n`);
|
|
72
|
+
});
|
|
73
|
+
return cli;
|
|
74
|
+
}
|
|
75
|
+
function untilSignal() {
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
process.once('SIGINT', () => resolve());
|
|
78
|
+
process.once('SIGTERM', () => resolve());
|
|
79
|
+
});
|
|
80
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { CONFIG_FILE, runCli } from './cli.js';
|
|
2
|
+
export type { CliIo } from './cli.js';
|
|
3
|
+
export { createImportMap, findBrowserModules } from 'asljs-appbase';
|
|
4
|
+
export type { BrowserModule } from 'asljs-appbase';
|
|
5
|
+
export { readDocument, startViewer } from './server.js';
|
|
6
|
+
export type { Viewer, ViewerOptions } from './server.js';
|
package/dist/index.js
ADDED
package/dist/open.d.ts
ADDED
package/dist/open.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
/** Opens `url` in the default browser, without waiting for it. */
|
|
3
|
+
export function openInBrowser(url, platform = process.platform) {
|
|
4
|
+
const [command, args] = platform === 'win32'
|
|
5
|
+
? ['cmd',
|
|
6
|
+
['/c',
|
|
7
|
+
'start',
|
|
8
|
+
'""',
|
|
9
|
+
url]]
|
|
10
|
+
: platform === 'darwin'
|
|
11
|
+
? ['open',
|
|
12
|
+
[url]]
|
|
13
|
+
: ['xdg-open',
|
|
14
|
+
[url]];
|
|
15
|
+
const child = spawn(command, args, { detached: true,
|
|
16
|
+
stdio: 'ignore',
|
|
17
|
+
windowsVerbatimArguments: platform === 'win32' });
|
|
18
|
+
child.on('error', () => { });
|
|
19
|
+
child.unref();
|
|
20
|
+
}
|
package/dist/page.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The viewer page: a toolbar, the diagram and a status line. It loads the
|
|
3
|
+
* document from `/document`, creates its model, and fits it on first show. For
|
|
4
|
+
* a graph, the toolbar adds, links and deletes nodes.
|
|
5
|
+
*/
|
|
6
|
+
export declare function renderPage(title: string, importMap: object): string;
|
package/dist/page.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { escapeHtml, page, scriptJson } from 'asljs-appbase';
|
|
2
|
+
/**
|
|
3
|
+
* The viewer page: a toolbar, the diagram and a status line. It loads the
|
|
4
|
+
* document from `/document`, creates its model, and fits it on first show. For
|
|
5
|
+
* a graph, the toolbar adds, links and deletes nodes.
|
|
6
|
+
*/
|
|
7
|
+
export function renderPage(title, importMap) {
|
|
8
|
+
// The import map goes first in the body, before the module script.
|
|
9
|
+
return page(title, `<script type="importmap">${scriptJson(importMap)}</script>
|
|
10
|
+
<header>
|
|
11
|
+
<h1>${escapeHtml(title)}</h1>
|
|
12
|
+
<asljs-diagram-zoom id="zoom"></asljs-diagram-zoom>
|
|
13
|
+
<asljs-diagram-search id="search"></asljs-diagram-search>
|
|
14
|
+
<span class="graph-only" hidden>
|
|
15
|
+
<button type="button" id="add-node">Add node</button>
|
|
16
|
+
<button type="button" id="add-link" title="Link the two selected nodes, first to second">Link</button>
|
|
17
|
+
</span>
|
|
18
|
+
<button type="button" id="delete" title="Delete the selection (Delete key)">Delete</button>
|
|
19
|
+
</header>
|
|
20
|
+
<asljs-diagram id="diagram"></asljs-diagram>
|
|
21
|
+
<footer>
|
|
22
|
+
<span id="status"></span>
|
|
23
|
+
<span id="error"></span>
|
|
24
|
+
</footer>
|
|
25
|
+
<script type="module">
|
|
26
|
+
import { createModelFromDocument,
|
|
27
|
+
isNode,
|
|
28
|
+
parseDiagramDocument } from 'asljs-ui-diagram';
|
|
29
|
+
|
|
30
|
+
const diagram = document.querySelector('#diagram');
|
|
31
|
+
const status = document.querySelector('#status');
|
|
32
|
+
const error = document.querySelector('#error');
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch('document');
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error((await response.json()).error);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const data = parseDiagramDocument(await response.json());
|
|
42
|
+
const model = createModelFromDocument(data);
|
|
43
|
+
|
|
44
|
+
diagram.model = model;
|
|
45
|
+
document.querySelector('#zoom').viewport = model.viewport;
|
|
46
|
+
document.querySelector('#search').model = model;
|
|
47
|
+
window.diagramModel = model;
|
|
48
|
+
|
|
49
|
+
const isGraph = data.type === 'graph';
|
|
50
|
+
|
|
51
|
+
for (const element of document.querySelectorAll('.graph-only')) {
|
|
52
|
+
element.hidden = !isGraph;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const showStatus = () =>
|
|
56
|
+
{
|
|
57
|
+
const elements = model.diagram.elements;
|
|
58
|
+
const nodes = elements.filter(isNode).length;
|
|
59
|
+
|
|
60
|
+
status.textContent =
|
|
61
|
+
(isGraph
|
|
62
|
+
? nodes + ' nodes, ' + (elements.length - nodes) + ' links'
|
|
63
|
+
: elements.length + ' elements')
|
|
64
|
+
+ (model.selection.length > 0
|
|
65
|
+
? '; selected: ' + model.selection.join(', ')
|
|
66
|
+
: '');
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
model.on('change', showStatus);
|
|
70
|
+
model.diagram.elements.on('change', showStatus);
|
|
71
|
+
showStatus();
|
|
72
|
+
|
|
73
|
+
document.querySelector('#delete').addEventListener('click', () =>
|
|
74
|
+
model.deleteElements([ ...model.selection ]));
|
|
75
|
+
|
|
76
|
+
document.querySelector('#add-node').addEventListener('click', () =>
|
|
77
|
+
{
|
|
78
|
+
const viewport = model.viewport;
|
|
79
|
+
let index = 1;
|
|
80
|
+
|
|
81
|
+
while (model.find('node-' + index)) {
|
|
82
|
+
index++;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const id = 'node-' + index;
|
|
86
|
+
|
|
87
|
+
model.addNode(
|
|
88
|
+
{ id,
|
|
89
|
+
x: (viewport.width / 2 - viewport.x) / viewport.zoom - 60,
|
|
90
|
+
y: (viewport.height / 2 - viewport.y) / viewport.zoom - 24 });
|
|
91
|
+
|
|
92
|
+
model.select([ id ]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
document.querySelector('#add-link').addEventListener('click', () =>
|
|
96
|
+
{
|
|
97
|
+
const ids = model.selection.filter(id => isNode(model.find(id)));
|
|
98
|
+
|
|
99
|
+
if (ids.length !== 2) {
|
|
100
|
+
error.textContent = 'Select two nodes to link, the first one first.';
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
error.textContent = '';
|
|
105
|
+
model.select([ model.addLink({ from: ids[0], to: ids[1] }) ]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!data.viewport) {
|
|
109
|
+
requestAnimationFrame(() => model.viewport.fit());
|
|
110
|
+
}
|
|
111
|
+
} catch (failure) {
|
|
112
|
+
error.textContent = String(failure?.message ?? failure);
|
|
113
|
+
}
|
|
114
|
+
</script>`, `html, body { height: 100%; margin: 0; }
|
|
115
|
+
body {
|
|
116
|
+
max-width: none;
|
|
117
|
+
padding: 0;
|
|
118
|
+
line-height: normal;
|
|
119
|
+
display: grid;
|
|
120
|
+
grid-template-rows: auto 1fr auto;
|
|
121
|
+
font: 14px system-ui, sans-serif;
|
|
122
|
+
color: #1f2937;
|
|
123
|
+
}
|
|
124
|
+
header, footer {
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-wrap: wrap;
|
|
127
|
+
gap: 8px;
|
|
128
|
+
align-items: center;
|
|
129
|
+
padding: 6px 12px;
|
|
130
|
+
background: #f3f4f6;
|
|
131
|
+
}
|
|
132
|
+
header h1 { font-size: 15px; margin: 0 8px 0 0; }
|
|
133
|
+
asljs-diagram {
|
|
134
|
+
min-height: 0;
|
|
135
|
+
background: #ffffff;
|
|
136
|
+
border-block: 1px solid #d1d5db;
|
|
137
|
+
outline: none;
|
|
138
|
+
}
|
|
139
|
+
.asljs-diagram-zoom { display: flex; gap: 4px; align-items: center; }
|
|
140
|
+
.zoom-level { min-width: 4ch; text-align: right; }
|
|
141
|
+
.asljs-select-lookup { position: relative; display: inline-block; }
|
|
142
|
+
.asljs-select-lookup [data-role="menu"] {
|
|
143
|
+
position: absolute;
|
|
144
|
+
z-index: 10;
|
|
145
|
+
top: 100%;
|
|
146
|
+
left: 0;
|
|
147
|
+
min-width: 240px;
|
|
148
|
+
padding: 4px;
|
|
149
|
+
background: #ffffff;
|
|
150
|
+
border: 1px solid #d1d5db;
|
|
151
|
+
box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
|
|
152
|
+
}
|
|
153
|
+
.asljs-select-lookup [data-role="menu"][hidden] { display: none; }
|
|
154
|
+
.asljs-select-lookup [data-role="filter"] { box-sizing: border-box; width: 100%; }
|
|
155
|
+
.asljs-select-lookup [data-role="results"] { list-style: none; margin: 4px 0 0; padding: 0; }
|
|
156
|
+
.asljs-select-lookup-item { display: block; padding: 2px 6px; color: inherit; text-decoration: none; }
|
|
157
|
+
.asljs-select-lookup-item:hover, .asljs-select-lookup-active { background: #e5edff; }
|
|
158
|
+
.asljs-select-lookup-hint { color: #6b7280; }
|
|
159
|
+
.graph-only[hidden] { display: none; }
|
|
160
|
+
#error { color: #b91c1c; white-space: pre-wrap; }`);
|
|
161
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type BrowserModule, type ListenOptions } from 'asljs-appbase';
|
|
2
|
+
export interface ViewerOptions {
|
|
3
|
+
/** The diagram file to show. */
|
|
4
|
+
file: string;
|
|
5
|
+
/** Default 0: any free port. */
|
|
6
|
+
port?: number;
|
|
7
|
+
/** Default `127.0.0.1`. */
|
|
8
|
+
host?: string;
|
|
9
|
+
/** The packages the page loads; found from `asljs-ui-diagram` by default. */
|
|
10
|
+
modules?: BrowserModule[];
|
|
11
|
+
/** Where the server logs; nothing is logged by default. */
|
|
12
|
+
logger?: ListenOptions['logger'];
|
|
13
|
+
}
|
|
14
|
+
export interface Viewer {
|
|
15
|
+
url: string;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export { MODULES_PREFIX } from 'asljs-appbase';
|
|
19
|
+
/**
|
|
20
|
+
* Reads and checks the diagram file, then serves the viewer page, the file as
|
|
21
|
+
* `/document`, and the packages the page imports. The file is read again on
|
|
22
|
+
* every request for it, so a reload shows its current content.
|
|
23
|
+
*/
|
|
24
|
+
export declare function startViewer(options: ViewerOptions): Promise<Viewer>;
|
|
25
|
+
/** Reads a diagram file and checks it; the error names the file. */
|
|
26
|
+
export declare function readDocument(file: string): Promise<unknown>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHandler, createImportMap, findBrowserModules, jsonReply, MODULES_PREFIX, serve, serverUrl } from 'asljs-appbase';
|
|
2
|
+
import { parseDiagramDocument } from 'asljs-ui-diagram';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { renderPage } from './page.js';
|
|
7
|
+
export { MODULES_PREFIX } from 'asljs-appbase';
|
|
8
|
+
/**
|
|
9
|
+
* Reads and checks the diagram file, then serves the viewer page, the file as
|
|
10
|
+
* `/document`, and the packages the page imports. The file is read again on
|
|
11
|
+
* every request for it, so a reload shows its current content.
|
|
12
|
+
*/
|
|
13
|
+
export async function startViewer(options) {
|
|
14
|
+
const file = path.resolve(options.file);
|
|
15
|
+
await readDocument(file);
|
|
16
|
+
const modules = options.modules
|
|
17
|
+
?? findBrowserModules('asljs-ui-diagram', path.dirname(fileURLToPath(import.meta.url)));
|
|
18
|
+
const page = renderPage(path.basename(file), createImportMap(modules, MODULES_PREFIX));
|
|
19
|
+
// Nothing but the page, the document and the scripts of the packages.
|
|
20
|
+
const server = await serve(createHandler({ folder: path.dirname(file),
|
|
21
|
+
index: async () => page,
|
|
22
|
+
json: { '/document': async () => {
|
|
23
|
+
try {
|
|
24
|
+
return await readDocument(file);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return jsonReply(422, { error: error.message });
|
|
28
|
+
}
|
|
29
|
+
} },
|
|
30
|
+
modules,
|
|
31
|
+
methods: ['GET',
|
|
32
|
+
'HEAD'],
|
|
33
|
+
moduleTypes: ['.js',
|
|
34
|
+
'.mjs',
|
|
35
|
+
'.map'],
|
|
36
|
+
allow: () => false }), { port: options.port ?? 0,
|
|
37
|
+
host: options.host,
|
|
38
|
+
logger: options.logger });
|
|
39
|
+
return { url: serverUrl(server),
|
|
40
|
+
close: () => new Promise((resolve, reject) => {
|
|
41
|
+
server.closeAllConnections();
|
|
42
|
+
server.close(error => error
|
|
43
|
+
? reject(error)
|
|
44
|
+
: resolve());
|
|
45
|
+
}) };
|
|
46
|
+
}
|
|
47
|
+
/** Reads a diagram file and checks it; the error names the file. */
|
|
48
|
+
export async function readDocument(file) {
|
|
49
|
+
let text;
|
|
50
|
+
try {
|
|
51
|
+
text =
|
|
52
|
+
await readFile(file, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
throw new Error(`Cannot read ${file}: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return parseDiagramDocument(JSON.parse(text));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw new Error(`${file}: ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Diagram CLI
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
What the `diagram` command does: `diagram view`, its options, the page it
|
|
6
|
+
serves, and its errors.
|
|
7
|
+
|
|
8
|
+
## Logging
|
|
9
|
+
|
|
10
|
+
Every command also takes `--loglevel <level>`, `--logfile <target>` and
|
|
11
|
+
`--logformat <format>`, or `DIAGRAM_LOG_LEVEL`, `DIAGRAM_LOG_FILE` and
|
|
12
|
+
`DIAGRAM_LOG_FORMAT`, or `logLevel`, `logFile` and `logFormat` of the nearest
|
|
13
|
+
`diagram.json`, in that order. Nothing is logged without them. At `information`
|
|
14
|
+
the log has the address the server listens on, at `trace` each request, and at
|
|
15
|
+
`error` a request that failed.
|
|
16
|
+
|
|
17
|
+
## diagram view
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
diagram view <path> [--port <port>] [--host <host>] [--no-open]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `<path>` - the diagram file, JSON in the `asljs-ui-diagram` document format.
|
|
24
|
+
- `--port` - the port to listen on; 0, the default, picks a free one.
|
|
25
|
+
- `--host` - the address to listen on, `127.0.0.1` by default.
|
|
26
|
+
- `--no-open` - print the address without opening the browser.
|
|
27
|
+
|
|
28
|
+
The options can also be set in `diagram.json`, the nearest one in the current
|
|
29
|
+
directory or a parent, as the [configuration][CFG] describes:
|
|
30
|
+
|
|
31
|
+
- `host` - a string, for `--host`.
|
|
32
|
+
- `port` - an integer from 0 to 65535, for `--port`.
|
|
33
|
+
- `open` - `true` or `false`; `false` is `--no-open`.
|
|
34
|
+
|
|
35
|
+
An option beats the key: `--no-open` keeps the browser closed with `"open":
|
|
36
|
+
true`. A value of the wrong type ends the command with exit code 1 and a message
|
|
37
|
+
naming the file and the key, such as `diagram.json: "port" must be a port
|
|
38
|
+
number.`
|
|
39
|
+
|
|
40
|
+
The command reads and checks the file first. A file that cannot be read, is not
|
|
41
|
+
JSON, or does not match the format ends the command with exit code 1 and a
|
|
42
|
+
message naming the file and the first invalid value, such as `graph.json:
|
|
43
|
+
nodes[2].x: expected a number`, before anything is served.
|
|
44
|
+
|
|
45
|
+
It then serves, until Ctrl+C or SIGTERM:
|
|
46
|
+
|
|
47
|
+
- `/` - the page.
|
|
48
|
+
- `/document` - the file, read and checked again on every request, so reloading
|
|
49
|
+
the page shows the file's current content. A file that has become invalid is
|
|
50
|
+
answered with status 422 and `{ "error": <the message> }`.
|
|
51
|
+
- `/modules/<package>/<path>` - the JavaScript of `asljs-ui-diagram` and the
|
|
52
|
+
packages it depends on, `asljs-ui-select-lookup` and lit among them, found the
|
|
53
|
+
way Node finds them from the installed `asljs-ui-diagram`. The page loads them
|
|
54
|
+
through an import map. Only `.js`, `.mjs` and `.map` files inside a package's
|
|
55
|
+
folder are served.
|
|
56
|
+
|
|
57
|
+
Anything else is a 404, and a method other than `GET` and `HEAD` a 405.
|
|
58
|
+
|
|
59
|
+
## The page
|
|
60
|
+
|
|
61
|
+
The page shows the file name, the zoom control, the diagram and a status line
|
|
62
|
+
with the number of nodes and links, or of elements, and the selection.
|
|
63
|
+
|
|
64
|
+
- Without a `viewport` in the file, the diagram is fitted to the window when it
|
|
65
|
+
first shows.
|
|
66
|
+
- The wheel zooms, dragging the background pans, pressing an element selects it,
|
|
67
|
+
dragging moves the selection, the corner handles resize, and Delete or the
|
|
68
|
+
Delete button deletes the selection with its links.
|
|
69
|
+
- For a graph, Add node adds a node in the middle of the window and selects it,
|
|
70
|
+
and Link links the two selected nodes, from the one selected first.
|
|
71
|
+
|
|
72
|
+
Edits change the page's model only; nothing is written to the file.
|
|
73
|
+
|
|
74
|
+
## diagram version
|
|
75
|
+
|
|
76
|
+
Prints the version of `asljs-diagram-cli`.
|
|
77
|
+
|
|
78
|
+
[CFG]: ../../../docs/Configuration.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "asljs-diagram-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "The diagram command: shows a diagram file, such as a graph, in the browser with asljs-diagram.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"asljs": {
|
|
7
|
+
"type": "app"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/**",
|
|
19
|
+
"bin/**",
|
|
20
|
+
"docs/**",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE.md"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"diagram": "bin/diagram.js"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"flint": "toolkit flint",
|
|
29
|
+
"clean": "toolkit clean",
|
|
30
|
+
"build": "npm run codegen --if-present && tsc -p tsconfig.build.json",
|
|
31
|
+
"build:dist": "npm run codegen --if-present && tsc -p tsconfig.dist.json",
|
|
32
|
+
"typecheck": "npm run codegen --if-present && tsc -p tsconfig.build.json --noEmit",
|
|
33
|
+
"guard:clean-git": "toolkit ensure-clean-working-directory",
|
|
34
|
+
"prepack": "npm run clean && npm run build:dist && npm run build && npm run test && npm run guard:clean-git",
|
|
35
|
+
"postpublish": "toolkit tag-release-revision",
|
|
36
|
+
"test": "node --test --enable-source-maps \"build/**/*.test.js\"",
|
|
37
|
+
"test:watch": "node --watch --test \"build/**/*.test.js\"",
|
|
38
|
+
"release:patch": "toolkit release-patch",
|
|
39
|
+
"all": "npm run clean && npm run build:dist && npm run flint && npm run typecheck && npm run build && npm run test"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"diagram",
|
|
43
|
+
"graph",
|
|
44
|
+
"viewer",
|
|
45
|
+
"cli"
|
|
46
|
+
],
|
|
47
|
+
"homepage": "https://github.com/AlexandriteSoftware/asljs#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/AlexandriteSoftware/asljs/issues"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/AlexandriteSoftware/asljs.git"
|
|
54
|
+
},
|
|
55
|
+
"author": "\"Alex Netkachov\" <alex.netkachov@gmail.com>",
|
|
56
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"asljs-logging": "^0.1.2",
|
|
59
|
+
"asljs-appbase": "^0.1.3",
|
|
60
|
+
"asljs-ui-diagram": "^0.1.1",
|
|
61
|
+
"commander": "^15.0.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"asljs-testing": "^0.1.0",
|
|
65
|
+
"asljs-tmpdir": "^0.1.13",
|
|
66
|
+
"asljs-toolkit": "^0.1.0"
|
|
67
|
+
}
|
|
68
|
+
}
|