asljs-part-srv 0.0.0-stage → 0.1.2
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 +79 -2
- package/bin/part-srv.js +12 -0
- package/dist/app.d.ts +10 -0
- package/dist/app.js +81 -0
- package/dist/browser/locations.d.ts +6 -0
- package/dist/browser/locations.js +13 -0
- package/dist/browser/plugin.d.ts +16 -0
- package/dist/browser/plugin.js +263 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/main.d.ts +15 -0
- package/dist/main.js +49 -0
- package/dist/project.d.ts +96 -0
- package/dist/project.js +134 -0
- package/dist/view.d.ts +26 -0
- package/dist/view.js +43 -0
- package/docs/part-srv.md +143 -0
- package/package.json +85 -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,80 @@
|
|
|
1
|
-
#
|
|
1
|
+
# part-srv
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Part of [Alexandrite Software Library][#1] - a set of high-quality, performant
|
|
4
|
+
> JavaScript libraries and tools for everyday use.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
`asljs-part-srv` is the `part-srv` web server: the files of projects in an
|
|
9
|
+
editor in the browser, as a tree of folders, each opened as a tab of its
|
|
10
|
+
details - path, size, modification time - and the artefact definitions that
|
|
11
|
+
match it. Each `part.json` of the working directory, else of its folders, is a
|
|
12
|
+
project, chosen in a selector that finds it by its title. It reads a project on
|
|
13
|
+
every request and never serves the content of a file.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g asljs-part-srv
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
part-srv
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
serves the projects of the working folder at the address it prints, until
|
|
28
|
+
stopped with Ctrl+C; `part-srv --working-dir ~/work` serves those of `~/work`.
|
|
29
|
+
Each project takes its root, definitions and state folder from its own
|
|
30
|
+
`part.json` only.
|
|
31
|
+
|
|
32
|
+
As a library:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { NullLoggerProvider } from 'asljs-logging';
|
|
36
|
+
import { serve } from 'asljs-appbase';
|
|
37
|
+
import { createApp } from 'asljs-part-srv';
|
|
38
|
+
|
|
39
|
+
const app = await createApp(
|
|
40
|
+
{ cwd: process.cwd(),
|
|
41
|
+
base: '/',
|
|
42
|
+
env: process.env,
|
|
43
|
+
loggerProvider: new NullLoggerProvider() });
|
|
44
|
+
|
|
45
|
+
const server = await serve(app.handler, { port: 8080 });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`createApp(context)` is the application as an editor plugin, an `EditorApp` of
|
|
49
|
+
`asljs-appbase`, served at `context.base`, with a project for each `part.json`
|
|
50
|
+
found from `context.cwd`. `listProject(project)` gives the files and the other
|
|
51
|
+
artefacts the Artefacts panel lists, and `describeArtefact(project, location)`
|
|
52
|
+
the details a tab shows; the server answers them as JSON at `DATA_PATH` and
|
|
53
|
+
`ARTEFACT_PATH` of each project, and the plugin renders them in the browser.
|
|
54
|
+
`main(args, environment)` is the `part-srv` command.
|
|
55
|
+
|
|
56
|
+
## Further reading
|
|
57
|
+
|
|
58
|
+
- [part-srv][DOC] - the command, the projects, the editor and the data the
|
|
59
|
+
server answers.
|
|
60
|
+
- [Requirements][REQ] - what the server guarantees, as rq requirements.
|
|
61
|
+
|
|
62
|
+
## Related packages
|
|
63
|
+
|
|
64
|
+
- [asljs-part][PRT] - definitions, artefacts and plugins, as a library.
|
|
65
|
+
- [asljs-part-cli][CLI] - the `part` command line.
|
|
66
|
+
- [asljs-ui-editor][EDT] - the editor and its plugins.
|
|
67
|
+
- [appbase][MDC] - the document server behind `part-srv`.
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
See [LICENSE][LIC] for details.
|
|
72
|
+
|
|
73
|
+
[#1]: https://github.com/AlexandriteSoftware/asljs
|
|
74
|
+
[CLI]: ../part-cli/README.md
|
|
75
|
+
[DOC]: docs/part-srv.md
|
|
76
|
+
[EDT]: ../../../ui/editor/README.md
|
|
77
|
+
[LIC]: LICENSE.md
|
|
78
|
+
[MDC]: ../../../libs/appbase/README.md
|
|
79
|
+
[PRT]: ../part/README.md
|
|
80
|
+
[REQ]: <dev/rq/R32 part-srv server.md>
|
package/bin/part-srv.js
ADDED
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type EditorApp, type EditorAppContext } from 'asljs-appbase';
|
|
2
|
+
/**
|
|
3
|
+
* The part application as an editor plugin: its projects, by `part.json`.
|
|
4
|
+
* Each `part.json` `findAppProjects` finds from the working directory is a
|
|
5
|
+
* project. A project's root, definitions and state folder come from its own
|
|
6
|
+
* `part.json` only, else the default, when it is first requested; no option
|
|
7
|
+
* or environment variable sets them, since one value cannot suit every
|
|
8
|
+
* project.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createApp(context: EditorAppContext): Promise<EditorApp>;
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { configPath, configStrings, createEditorApp, findAppProjects, findBrowserModules, stateFolder } from 'asljs-appbase';
|
|
2
|
+
import { CONFIG_FILE, resolveDefinitionSource } from 'asljs-part';
|
|
3
|
+
import { ICONS } from 'asljs-ui-editor';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { createProjectHandler } from './view.js';
|
|
7
|
+
/**
|
|
8
|
+
* The style of an artefact's details: the tab, the details with the preview
|
|
9
|
+
* of a file on their right and an action bar along its bottom, as the graph
|
|
10
|
+
* of `rq-srv` has them; a scrolling view of the details, their table of the
|
|
11
|
+
* location, path, size and modification time, and their list of
|
|
12
|
+
* definitions; and the file's content in the preview.
|
|
13
|
+
*/
|
|
14
|
+
const STYLE = `.part-document { height: 100%; display: flex; flex-direction: column; background: #fff; }
|
|
15
|
+
.part-stage { flex: 1; min-height: 0; display: flex; }
|
|
16
|
+
.part-view { flex: 1; min-width: 0; box-sizing: border-box; overflow: auto; padding: 0.75rem 1rem; line-height: 1.5; }
|
|
17
|
+
asljs-diagram-preview { width: min(40rem, 45vw); border-left: 1px solid #ddd; background: #fff; }
|
|
18
|
+
.asljs-diagram-preview-splitter { background: transparent; transition: background 0.15s; }
|
|
19
|
+
.asljs-diagram-preview-splitter:hover, .asljs-diagram-preview-splitter:focus-visible { background: #1a73e8; outline: none; }
|
|
20
|
+
.asljs-diagram-preview-placeholder, .asljs-diagram-preview-body > .asljs-plugin-note { margin: 1rem; color: #757575; }
|
|
21
|
+
.part-actionbar { display: flex; align-items: center; justify-content: flex-end; padding: 2px 8px; background: #f3f4f6; border-top: 1px solid #ddd; }
|
|
22
|
+
.asljs-diagram-preview-tabs { gap: 2px; }
|
|
23
|
+
.asljs-diagram-preview-tab { border: 0; border-radius: 4px; background: none; padding: 3px 10px; font: inherit; color: #555; cursor: pointer; }
|
|
24
|
+
.asljs-diagram-preview-tab:hover:not(:disabled) { background: #e5e7eb; }
|
|
25
|
+
.asljs-diagram-preview-tab[aria-selected="true"] { background: #dbe7fd; color: #1a73e8; }
|
|
26
|
+
.asljs-diagram-preview-toggle { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 4px; background: none; color: #555; cursor: pointer; }
|
|
27
|
+
.asljs-diagram-preview-toggle:hover:not(:disabled) { background: #e5e7eb; }
|
|
28
|
+
.asljs-diagram-preview-toggle[aria-expanded="true"] { color: #1a73e8; }
|
|
29
|
+
.part-content { flex: 1; min-height: 0; overflow: auto; margin: 0; padding: 0.75rem 1rem; box-sizing: border-box; }
|
|
30
|
+
.part-text { font-family: ui-monospace, monospace; font-size: 0.85rem; line-height: 1.45; white-space: pre; }
|
|
31
|
+
.part-image { max-width: 100%; }
|
|
32
|
+
.part-details { border-collapse: collapse; margin: 0 0 1rem; }
|
|
33
|
+
.part-details th { text-align: left; color: #555; font-weight: normal; padding: 2px 1rem 2px 0; vertical-align: top; }
|
|
34
|
+
.part-details td { padding: 2px 0; word-break: break-all; }
|
|
35
|
+
.part-definitions { padding-left: 1.25rem; }
|
|
36
|
+
.part-definitions li { margin: 0 0 0.6rem; }
|
|
37
|
+
.part-note { color: #757575; }`;
|
|
38
|
+
/**
|
|
39
|
+
* The part application as an editor plugin: its projects, by `part.json`.
|
|
40
|
+
* Each `part.json` `findAppProjects` finds from the working directory is a
|
|
41
|
+
* project. A project's root, definitions and state folder come from its own
|
|
42
|
+
* `part.json` only, else the default, when it is first requested; no option
|
|
43
|
+
* or environment variable sets them, since one value cannot suit every
|
|
44
|
+
* project.
|
|
45
|
+
*/
|
|
46
|
+
export async function createApp(context) {
|
|
47
|
+
const projects = await findAppProjects(context, CONFIG_FILE, { required: true });
|
|
48
|
+
return createEditorApp({
|
|
49
|
+
id: 'part',
|
|
50
|
+
title: 'Part',
|
|
51
|
+
icon: ICONS.document,
|
|
52
|
+
base: context.base,
|
|
53
|
+
projects,
|
|
54
|
+
project: project => createProjectHandler(resolveProject(project, context)),
|
|
55
|
+
plugin: fileURLToPath(new URL('./browser/plugin.js', import.meta.url)),
|
|
56
|
+
// The page loads the plugin's packages, and what they import, from
|
|
57
|
+
// the server.
|
|
58
|
+
modules: findBrowserModules(['asljs-ui-diagram',
|
|
59
|
+
'asljs-ui-editor',
|
|
60
|
+
'asljs-ui-markdown'], path.dirname(fileURLToPath(import.meta.url))),
|
|
61
|
+
style: STYLE
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The project part reads in an application project: `project`,
|
|
66
|
+
* `definitions` and `stateDir` of its `part.json`, each path resolved
|
|
67
|
+
* against the file's folder; the root defaults to the project's folder, the
|
|
68
|
+
* state folder to `.part` in the root (`stateFolder`). A definition source
|
|
69
|
+
* may be a package specifier, not a path, so `configPaths` does not apply.
|
|
70
|
+
*/
|
|
71
|
+
function resolveProject(project, context) {
|
|
72
|
+
const { config } = project;
|
|
73
|
+
const root = path.normalize(configPath(config, 'project')
|
|
74
|
+
?? project.folder);
|
|
75
|
+
return { root,
|
|
76
|
+
definitions: (configStrings(config, 'definitions')
|
|
77
|
+
?? [])
|
|
78
|
+
.map(source => resolveDefinitionSource(source, config.folder)),
|
|
79
|
+
stateDir: stateFolder(root, 'part', config),
|
|
80
|
+
loggerProvider: context.loggerProvider };
|
|
81
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The segments of a location, for `pathTree`: its scheme with its colon, as
|
|
3
|
+
* `file:`, then each segment of its path, anything between `/` or `\`. A
|
|
4
|
+
* location without a scheme, a relative path, is a `file:` one.
|
|
5
|
+
*/
|
|
6
|
+
export declare function locationSegments(location: string): string[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The segments of a location, for `pathTree`: its scheme with its colon, as
|
|
3
|
+
* `file:`, then each segment of its path, anything between `/` or `\`. A
|
|
4
|
+
* location without a scheme, a relative path, is a `file:` one.
|
|
5
|
+
*/
|
|
6
|
+
export function locationSegments(location) {
|
|
7
|
+
const scheme = /^[a-z][a-z0-9+.-]*:/i.exec(location)?.[0];
|
|
8
|
+
return [scheme ?? 'file:',
|
|
9
|
+
...location
|
|
10
|
+
.slice(scheme?.length ?? 0)
|
|
11
|
+
.split(/[/\\]/)
|
|
12
|
+
.filter(segment => segment !== '')];
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type EditorPluginFactory } from 'asljs-ui-editor';
|
|
2
|
+
/**
|
|
3
|
+
* The plugin of a project, the browser module of `asljs-ui-editor` the
|
|
4
|
+
* server serves. It loads the project's files and other artefacts from
|
|
5
|
+
* `_/data.json` once, when it is made. Its Artefacts operation is a
|
|
6
|
+
* `pathTree` of their locations, grouped by `locationSegments`, its groups
|
|
7
|
+
* folded. Choosing an entry opens its details as a document whose id is
|
|
8
|
+
* `part:<location>`, rendered here from `_/artefact.json`, with the preview
|
|
9
|
+
* of a file on their right, `previewOf`, in an `asljs-diagram-preview`
|
|
10
|
+
* whose state, shared by the tabs, this browser keeps. Its search finds
|
|
11
|
+
* artefacts by location (`searchMatcher`), at most `MAX_RESULTS` of them: a
|
|
12
|
+
* file by its path, with or without `file:`, and another artefact by its
|
|
13
|
+
* location. Every URL is relative to the project's `base`.
|
|
14
|
+
*/
|
|
15
|
+
declare const createPlugin: EditorPluginFactory;
|
|
16
|
+
export default createPlugin;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { observable } from 'asljs-observable';
|
|
2
|
+
import { createPreviewModel } from 'asljs-ui-diagram';
|
|
3
|
+
import { element, fetchJson, ICONS, note, pathHref, pathTree, projectPath, searchMatcher, tree } from 'asljs-ui-editor';
|
|
4
|
+
import { markdownDocument } from 'asljs-ui-markdown';
|
|
5
|
+
import { locationSegments } from './locations.js';
|
|
6
|
+
/**
|
|
7
|
+
* How many matches a search shows.
|
|
8
|
+
*/
|
|
9
|
+
const MAX_RESULTS = 200;
|
|
10
|
+
/**
|
|
11
|
+
* Where the browser keeps the state of the details' preview.
|
|
12
|
+
*/
|
|
13
|
+
const LAYOUT_KEY = 'part-view-layout';
|
|
14
|
+
/**
|
|
15
|
+
* The largest file the preview shows, in bytes.
|
|
16
|
+
*/
|
|
17
|
+
const PREVIEW_LIMIT = 1024 * 1024;
|
|
18
|
+
/**
|
|
19
|
+
* The plugin of a project, the browser module of `asljs-ui-editor` the
|
|
20
|
+
* server serves. It loads the project's files and other artefacts from
|
|
21
|
+
* `_/data.json` once, when it is made. Its Artefacts operation is a
|
|
22
|
+
* `pathTree` of their locations, grouped by `locationSegments`, its groups
|
|
23
|
+
* folded. Choosing an entry opens its details as a document whose id is
|
|
24
|
+
* `part:<location>`, rendered here from `_/artefact.json`, with the preview
|
|
25
|
+
* of a file on their right, `previewOf`, in an `asljs-diagram-preview`
|
|
26
|
+
* whose state, shared by the tabs, this browser keeps. Its search finds
|
|
27
|
+
* artefacts by location (`searchMatcher`), at most `MAX_RESULTS` of them: a
|
|
28
|
+
* file by its path, with or without `file:`, and another artefact by its
|
|
29
|
+
* location. Every URL is relative to the project's `base`.
|
|
30
|
+
*/
|
|
31
|
+
const createPlugin = async ({ id, base, model }) => {
|
|
32
|
+
const { files, others } = await fetchJson(base, '_/data.json');
|
|
33
|
+
const documentOf = (location, title) => ({ id: `${id}:${location}`,
|
|
34
|
+
title,
|
|
35
|
+
data: { location } });
|
|
36
|
+
const artefactsPanel = () => {
|
|
37
|
+
const locations = [...files.map(file => `file:${file}`),
|
|
38
|
+
...others.map(other => other.location)];
|
|
39
|
+
if (locations.length === 0) {
|
|
40
|
+
return note('The project has no artefacts.');
|
|
41
|
+
}
|
|
42
|
+
return tree(model, pathTree(locations, { segments: locationSegments,
|
|
43
|
+
document: location => documentOf(location, locationSegments(location).at(-1)),
|
|
44
|
+
open: false }));
|
|
45
|
+
};
|
|
46
|
+
// The preview of the details: whether it is shown, its mode and its
|
|
47
|
+
// width, kept in this browser.
|
|
48
|
+
let stored = {};
|
|
49
|
+
try {
|
|
50
|
+
stored =
|
|
51
|
+
JSON.parse(localStorage.getItem(LAYOUT_KEY) ?? '{}');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Without storage, the preview starts open.
|
|
55
|
+
}
|
|
56
|
+
const preview = createPreviewModel(stored.preview
|
|
57
|
+
?? { open: true,
|
|
58
|
+
mode: 'view' });
|
|
59
|
+
preview.on('change', () => {
|
|
60
|
+
try {
|
|
61
|
+
localStorage.setItem(LAYOUT_KEY, JSON.stringify({ preview: { open: preview.open,
|
|
62
|
+
mode: preview.mode,
|
|
63
|
+
width: preview.width } }));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Without storage, the preview lasts until the page is reloaded.
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// A link of a previewed markdown file to another file of the project
|
|
70
|
+
// opens that file's details.
|
|
71
|
+
const openLink = (url) => {
|
|
72
|
+
const relative = projectPath(base, url);
|
|
73
|
+
const file = relative === null
|
|
74
|
+
? undefined
|
|
75
|
+
: files.find(candidate => candidate.replaceAll('\\', '/')
|
|
76
|
+
=== relative);
|
|
77
|
+
if (file === undefined) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
model.open(documentOf(`file:${file}`, locationSegments(`file:${file}`).at(-1)));
|
|
81
|
+
return true;
|
|
82
|
+
};
|
|
83
|
+
const renderDocument = (opened) => {
|
|
84
|
+
const { location } = opened.data;
|
|
85
|
+
const view = element('div', 'part-document');
|
|
86
|
+
const details = element('div', 'part-view');
|
|
87
|
+
// The panel shows the file of the details, once they are read: its
|
|
88
|
+
// selection is the file's path, and none for another artefact.
|
|
89
|
+
const shown = observable({ selection: [] });
|
|
90
|
+
let file = null;
|
|
91
|
+
const modes = [{ id: 'view',
|
|
92
|
+
title: 'View',
|
|
93
|
+
isEnabled: selection => selection.length === 1
|
|
94
|
+
&& file !== null,
|
|
95
|
+
render: async () => await previewOf(base, file, openLink) }];
|
|
96
|
+
const panel = document.createElement('asljs-diagram-preview');
|
|
97
|
+
panel.placeholder = '';
|
|
98
|
+
panel.modes = modes;
|
|
99
|
+
panel.model = shown;
|
|
100
|
+
panel.preview = preview;
|
|
101
|
+
const stage = element('div', 'part-stage');
|
|
102
|
+
stage.append(details, panel);
|
|
103
|
+
const tabs = document.createElement('asljs-diagram-preview-tabs');
|
|
104
|
+
tabs.modes = modes;
|
|
105
|
+
tabs.preview = preview;
|
|
106
|
+
const bar = element('div', 'part-actionbar');
|
|
107
|
+
bar.append(tabs);
|
|
108
|
+
view.append(stage, bar);
|
|
109
|
+
void fetchJson(base, `_/artefact.json?location=${encodeURIComponent(location)}`)
|
|
110
|
+
.then((read) => {
|
|
111
|
+
details.append(...detailsView(read));
|
|
112
|
+
if (read.path === null) {
|
|
113
|
+
panel.placeholder =
|
|
114
|
+
'Only a file has a preview.';
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
file = read;
|
|
118
|
+
}
|
|
119
|
+
shown.selection =
|
|
120
|
+
[location];
|
|
121
|
+
}, (error) => details.append(note(error instanceof Error
|
|
122
|
+
? error.message
|
|
123
|
+
: String(error))));
|
|
124
|
+
return view;
|
|
125
|
+
};
|
|
126
|
+
return { id,
|
|
127
|
+
title: 'Part',
|
|
128
|
+
icon: ICONS.document,
|
|
129
|
+
operations: [{ id: 'artefacts',
|
|
130
|
+
title: 'Artefacts',
|
|
131
|
+
icon: ICONS.document,
|
|
132
|
+
render: artefactsPanel }],
|
|
133
|
+
renderDocument,
|
|
134
|
+
documentOf: location => documentOf(location, location
|
|
135
|
+
.split(/[\\/]/)
|
|
136
|
+
.at(-1)),
|
|
137
|
+
search: async (query) => {
|
|
138
|
+
const matches = searchMatcher(query);
|
|
139
|
+
return [...files
|
|
140
|
+
.filter(file => matches(`file:${file}`))
|
|
141
|
+
.map(file => ({ document: documentOf(`file:${file}`, file),
|
|
142
|
+
title: file })),
|
|
143
|
+
...others
|
|
144
|
+
.filter(other => matches(other.location))
|
|
145
|
+
.map(other => ({ document: documentOf(other.location, other.location),
|
|
146
|
+
title: other.location,
|
|
147
|
+
detail: other.name }))]
|
|
148
|
+
.slice(0, MAX_RESULTS);
|
|
149
|
+
} };
|
|
150
|
+
};
|
|
151
|
+
export default createPlugin;
|
|
152
|
+
/**
|
|
153
|
+
* The details of an artefact: its name, a table of its location and, for a
|
|
154
|
+
* file, its path, size and modification time, and the definitions that
|
|
155
|
+
* match it.
|
|
156
|
+
*/
|
|
157
|
+
function detailsView(details) {
|
|
158
|
+
const table = element('table', 'part-details');
|
|
159
|
+
table.append(row('Location', text(details.location)));
|
|
160
|
+
if (details.path !== null) {
|
|
161
|
+
table.append(row('Path', text(details.path)));
|
|
162
|
+
}
|
|
163
|
+
if (details.size !== null) {
|
|
164
|
+
table.append(row('Size', text(`${details.size} bytes`)));
|
|
165
|
+
}
|
|
166
|
+
if (details.modified !== null) {
|
|
167
|
+
const time = element('time', undefined, details.modified);
|
|
168
|
+
time.dateTime = details.modified;
|
|
169
|
+
table.append(row('Modified', time));
|
|
170
|
+
}
|
|
171
|
+
return [element('h1', undefined, details.name),
|
|
172
|
+
table,
|
|
173
|
+
element('h2', undefined, 'Definitions'),
|
|
174
|
+
definitionsView(details.definitions)];
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The preview of a file, read from the server as it is: an image as an image,
|
|
178
|
+
* a markdown file rendered, `openLink` opening its links to other files, and
|
|
179
|
+
* other text as it is; a note for a binary file, one over `PREVIEW_LIMIT`,
|
|
180
|
+
* and one the server does not serve.
|
|
181
|
+
*/
|
|
182
|
+
async function previewOf(base, file, openLink) {
|
|
183
|
+
if ((file.size
|
|
184
|
+
?? 0)
|
|
185
|
+
> PREVIEW_LIMIT) {
|
|
186
|
+
return note(`The file is too large to preview, ${file.size} bytes.`);
|
|
187
|
+
}
|
|
188
|
+
const url = new URL(pathHref(base, file.path), location.href);
|
|
189
|
+
try {
|
|
190
|
+
const response = await fetch(url);
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
return note(`The file has no preview: ${response.status} ${response.statusText}`
|
|
193
|
+
.trim());
|
|
194
|
+
}
|
|
195
|
+
if ((response.headers.get('content-type') ?? '')
|
|
196
|
+
.startsWith('image/')) {
|
|
197
|
+
const image = element('img', 'part-image');
|
|
198
|
+
image.src = url.href;
|
|
199
|
+
image.alt = file.name;
|
|
200
|
+
const frame = element('div', 'part-content');
|
|
201
|
+
frame.append(image);
|
|
202
|
+
return frame;
|
|
203
|
+
}
|
|
204
|
+
let text;
|
|
205
|
+
try {
|
|
206
|
+
text =
|
|
207
|
+
new TextDecoder('utf-8', { fatal: true })
|
|
208
|
+
.decode(await response.arrayBuffer());
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
text = '\0';
|
|
212
|
+
}
|
|
213
|
+
if (text.includes('\0')) {
|
|
214
|
+
return note('A binary file has no preview.');
|
|
215
|
+
}
|
|
216
|
+
if (/\.md$/i.test(file.path)) {
|
|
217
|
+
return markdownDocument({ url,
|
|
218
|
+
load: async () => ({ body: text }),
|
|
219
|
+
openLink });
|
|
220
|
+
}
|
|
221
|
+
const code = element('pre', 'part-content part-text');
|
|
222
|
+
code.append(element('code', undefined, text));
|
|
223
|
+
return code;
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
return note(error instanceof Error
|
|
227
|
+
? error.message
|
|
228
|
+
: String(error));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function row(label, value) {
|
|
232
|
+
const made = element('tr');
|
|
233
|
+
const cell = element('td');
|
|
234
|
+
cell.append(value);
|
|
235
|
+
made.append(element('th', undefined, label), cell);
|
|
236
|
+
return made;
|
|
237
|
+
}
|
|
238
|
+
function text(value) {
|
|
239
|
+
return document.createTextNode(value);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The definitions that match an artefact, each with where it comes from and
|
|
243
|
+
* its description, or a note that none does.
|
|
244
|
+
*/
|
|
245
|
+
function definitionsView(definitions) {
|
|
246
|
+
if (definitions.length === 0) {
|
|
247
|
+
return note('No artefact definition matches it.');
|
|
248
|
+
}
|
|
249
|
+
const list = element('ul', 'part-definitions');
|
|
250
|
+
for (const definition of definitions) {
|
|
251
|
+
const origin = [definition.source,
|
|
252
|
+
definition.document]
|
|
253
|
+
.filter((part) => part !== null)
|
|
254
|
+
.join(', ');
|
|
255
|
+
const item = element('li');
|
|
256
|
+
item.append(element('strong', undefined, definition.name), ' ', element('span', 'part-note', `(${origin})`));
|
|
257
|
+
if (definition.description !== '') {
|
|
258
|
+
item.append(element('br'), definition.description);
|
|
259
|
+
}
|
|
260
|
+
list.append(item);
|
|
261
|
+
}
|
|
262
|
+
return list;
|
|
263
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createApp } from './app.js';
|
|
2
|
+
export { main } from './main.js';
|
|
3
|
+
export { describeArtefact, listProject, type ArtefactDetails, type MatchingDefinition, type Project, type ProjectEntries } from './project.js';
|
|
4
|
+
export { ARTEFACT_PATH, DATA_PATH } from './view.js';
|
package/dist/index.js
ADDED
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type CommandIo } from 'asljs-appbase';
|
|
2
|
+
/**
|
|
3
|
+
* Run the `part-srv` command: serve the projects in a browser until SIGINT or
|
|
4
|
+
* SIGTERM, then return 0; return 1, with the message on standard error, when
|
|
5
|
+
* the arguments are wrong or the server cannot start.
|
|
6
|
+
*
|
|
7
|
+
* Each `part.json` of the working folder, `--working-dir` else the working
|
|
8
|
+
* directory, else of its folders, is a project (`createApp`), its root,
|
|
9
|
+
* definitions and state folder from that file only. The port, the host and
|
|
10
|
+
* logging come from the options, else the nearest `part.json` of the working
|
|
11
|
+
* folder or a parent: `port` and `host`, and, below the `PART_LOG_`
|
|
12
|
+
* variables, the logging keys of `docs/Logging.md` at the repository root
|
|
13
|
+
* (`runMain`). A path of `part.json` resolves against its folder.
|
|
14
|
+
*/
|
|
15
|
+
export declare function main(args: string[], io?: CommandIo): Promise<number>;
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { commandLine, configPort, configString, loadConfig, packageVersion, parsePort, runMain, serveApp } from 'asljs-appbase';
|
|
2
|
+
import { CONFIG_FILE } from 'asljs-part';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createApp } from './app.js';
|
|
6
|
+
/**
|
|
7
|
+
* Run the `part-srv` command: serve the projects in a browser until SIGINT or
|
|
8
|
+
* SIGTERM, then return 0; return 1, with the message on standard error, when
|
|
9
|
+
* the arguments are wrong or the server cannot start.
|
|
10
|
+
*
|
|
11
|
+
* Each `part.json` of the working folder, `--working-dir` else the working
|
|
12
|
+
* directory, else of its folders, is a project (`createApp`), its root,
|
|
13
|
+
* definitions and state folder from that file only. The port, the host and
|
|
14
|
+
* logging come from the options, else the nearest `part.json` of the working
|
|
15
|
+
* folder or a parent: `port` and `host`, and, below the `PART_LOG_`
|
|
16
|
+
* variables, the logging keys of `docs/Logging.md` at the repository root
|
|
17
|
+
* (`runMain`). A path of `part.json` resolves against its folder.
|
|
18
|
+
*/
|
|
19
|
+
export async function main(args, io = { cwd: process.cwd(),
|
|
20
|
+
env: process.env,
|
|
21
|
+
stdout: process.stdout,
|
|
22
|
+
stderr: process.stderr }) {
|
|
23
|
+
return await runMain(args, io, { prefix: 'PART_LOG_',
|
|
24
|
+
configFile: CONFIG_FILE }, async (loggerProvider) => {
|
|
25
|
+
const cli = commandLine(new Command('part-srv'), io, { workingDir: 'The folder the projects are looked for in; defaults to the current directory' });
|
|
26
|
+
cli
|
|
27
|
+
.description('Serve the projects of part.json files in a browser: the files of each as a tree, each with its details and the artefact definitions that match it.')
|
|
28
|
+
.version(packageVersion(import.meta.url), '--version')
|
|
29
|
+
.allowExcessArguments(false)
|
|
30
|
+
.option('--port <port>', 'Port to listen on, exactly; 0 picks a free one. By default port of part.json, else the first free port from 3000 on')
|
|
31
|
+
.option('--host <host>', 'Address to listen on. Defaults to host of part.json, then 127.0.0.1')
|
|
32
|
+
.action(async (options) => {
|
|
33
|
+
const cwd = path.resolve(io.cwd, options.workingDir ?? '.');
|
|
34
|
+
const config = await loadConfig(cwd, CONFIG_FILE);
|
|
35
|
+
await serveApp(await createApp({ cwd,
|
|
36
|
+
base: '/',
|
|
37
|
+
env: io.env,
|
|
38
|
+
loggerProvider }), { io,
|
|
39
|
+
cwd,
|
|
40
|
+
port: parsePort(options.port)
|
|
41
|
+
?? configPort(config),
|
|
42
|
+
host: options.host
|
|
43
|
+
?? configString(config, 'host'),
|
|
44
|
+
loggerProvider });
|
|
45
|
+
});
|
|
46
|
+
await cli.parseAsync(args, { from: 'user' });
|
|
47
|
+
return 0;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { LoggerProvider } from 'asljs-logging';
|
|
2
|
+
/**
|
|
3
|
+
* The project part-srv shows, and how to read it.
|
|
4
|
+
*/
|
|
5
|
+
export interface Project {
|
|
6
|
+
/**
|
|
7
|
+
* Absolute path of the project root.
|
|
8
|
+
*/
|
|
9
|
+
root: string;
|
|
10
|
+
/**
|
|
11
|
+
* Definition sources, as `providersFactory` takes them.
|
|
12
|
+
*/
|
|
13
|
+
definitions: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* The folder part keeps its caches in; `.part` under the root when absent.
|
|
16
|
+
*/
|
|
17
|
+
stateDir?: string;
|
|
18
|
+
loggerProvider: LoggerProvider;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What the Files panel lists: the files of the project, and the artefacts
|
|
22
|
+
* that are not files.
|
|
23
|
+
*/
|
|
24
|
+
export interface ProjectEntries {
|
|
25
|
+
/**
|
|
26
|
+
* Project-relative POSIX paths, sorted: every file `.gitignore` does not
|
|
27
|
+
* leave out, outside `node_modules` and dot folders, and every file a
|
|
28
|
+
* definition covers.
|
|
29
|
+
*/
|
|
30
|
+
files: string[];
|
|
31
|
+
/**
|
|
32
|
+
* The artefacts that are not files, e.g. `npm:` or `git:` ones, sorted by
|
|
33
|
+
* location.
|
|
34
|
+
*/
|
|
35
|
+
others: {
|
|
36
|
+
location: string;
|
|
37
|
+
name: string;
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A definition that matches an artefact.
|
|
42
|
+
*/
|
|
43
|
+
export interface MatchingDefinition {
|
|
44
|
+
name: string;
|
|
45
|
+
/**
|
|
46
|
+
* The first paragraph of its description.
|
|
47
|
+
*/
|
|
48
|
+
description: string;
|
|
49
|
+
/**
|
|
50
|
+
* Where it comes from: `markdown`, or the name of the plugin that gives it.
|
|
51
|
+
*/
|
|
52
|
+
source: string;
|
|
53
|
+
/**
|
|
54
|
+
* Project-relative path of its definition document, or the absolute path
|
|
55
|
+
* of one outside the project; `null` for a definition a plugin gives
|
|
56
|
+
* without a document.
|
|
57
|
+
*/
|
|
58
|
+
document: string | null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* What part-srv shows of a file or another artefact.
|
|
62
|
+
*/
|
|
63
|
+
export interface ArtefactDetails {
|
|
64
|
+
location: string;
|
|
65
|
+
name: string;
|
|
66
|
+
/**
|
|
67
|
+
* Project-relative POSIX path of a file; `null` for another artefact.
|
|
68
|
+
*/
|
|
69
|
+
path: string | null;
|
|
70
|
+
/**
|
|
71
|
+
* The size of a file in bytes; `null` for another artefact.
|
|
72
|
+
*/
|
|
73
|
+
size: number | null;
|
|
74
|
+
/**
|
|
75
|
+
* When a file was last modified, as an ISO 8601 UTC time; `null` for
|
|
76
|
+
* another artefact.
|
|
77
|
+
*/
|
|
78
|
+
modified: string | null;
|
|
79
|
+
/**
|
|
80
|
+
* The definitions that match it, in the order part reports them; none for
|
|
81
|
+
* a file no definition covers.
|
|
82
|
+
*/
|
|
83
|
+
definitions: MatchingDefinition[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The files and other artefacts of the project.
|
|
87
|
+
*/
|
|
88
|
+
export declare function listProject(project: Project): Promise<ProjectEntries>;
|
|
89
|
+
/**
|
|
90
|
+
* The details of a file or another artefact, by its location: a `file:`
|
|
91
|
+
* location, or the path of a file, gives the file, whatever definitions
|
|
92
|
+
* match it; another location gives the artefact of that location. Throws
|
|
93
|
+
* for a file that does not exist or is outside the project, and for another
|
|
94
|
+
* location no artefact has.
|
|
95
|
+
*/
|
|
96
|
+
export declare function describeArtefact(project: Project, location: string): Promise<ArtefactDetails>;
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { toDisplayPath } from 'asljs-appbase';
|
|
2
|
+
import { isInside } from 'asljs-locator';
|
|
3
|
+
import { FILE_SCHEME, providersFactory } from 'asljs-part';
|
|
4
|
+
import { stat } from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
/**
|
|
7
|
+
* The files and other artefacts of the project.
|
|
8
|
+
*/
|
|
9
|
+
export async function listProject(project) {
|
|
10
|
+
return await withProviders(project, async (providers) => {
|
|
11
|
+
const files = new Set((await providers.locationResolver.resolve(project.root, { pattern: '/**/*',
|
|
12
|
+
exclude: ['/**/node_modules/**',
|
|
13
|
+
'/**/.*/**'],
|
|
14
|
+
filters: [{ name: 'GitIgnore' }] }))
|
|
15
|
+
.map(file => toDisplayPath(project.root, file)));
|
|
16
|
+
const others = [];
|
|
17
|
+
for (const artefact of await providers.artefactProvider.getArtefacts()) {
|
|
18
|
+
if (artefact.location.startsWith(FILE_SCHEME)) {
|
|
19
|
+
files.add(artefact.location.slice(FILE_SCHEME.length));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
others.push({ location: artefact.location,
|
|
23
|
+
name: artefact.name });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { files: [...files].sort(),
|
|
27
|
+
others: others.sort((left, right) => left.location.localeCompare(right.location)) };
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The details of a file or another artefact, by its location: a `file:`
|
|
32
|
+
* location, or the path of a file, gives the file, whatever definitions
|
|
33
|
+
* match it; another location gives the artefact of that location. Throws
|
|
34
|
+
* for a file that does not exist or is outside the project, and for another
|
|
35
|
+
* location no artefact has.
|
|
36
|
+
*/
|
|
37
|
+
export async function describeArtefact(project, location) {
|
|
38
|
+
const filePath = fileLocationPath(location);
|
|
39
|
+
return await withProviders(project, async (providers) => {
|
|
40
|
+
const artefact = await providers.artefactProvider.tryGetArtefact(fileLocationOf(location, filePath));
|
|
41
|
+
const definitions = await matchingDefinitions(project, providers, artefact?.definitions ?? []);
|
|
42
|
+
if (filePath !== null) {
|
|
43
|
+
return await describeFile(project, filePath, artefact?.name, definitions);
|
|
44
|
+
}
|
|
45
|
+
if (artefact === null) {
|
|
46
|
+
throw new Error(`No artefact at ${location}`);
|
|
47
|
+
}
|
|
48
|
+
return { location: artefact.location,
|
|
49
|
+
name: artefact.name,
|
|
50
|
+
path: null,
|
|
51
|
+
size: null,
|
|
52
|
+
modified: null,
|
|
53
|
+
definitions };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The project-relative path of a file location, or of a location without a
|
|
58
|
+
* scheme; `null` for any other location.
|
|
59
|
+
*/
|
|
60
|
+
function fileLocationPath(location) {
|
|
61
|
+
if (location.startsWith(FILE_SCHEME)) {
|
|
62
|
+
return location.slice(FILE_SCHEME.length);
|
|
63
|
+
}
|
|
64
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(location)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return location;
|
|
68
|
+
}
|
|
69
|
+
function fileLocationOf(location, filePath) {
|
|
70
|
+
if (filePath === null) {
|
|
71
|
+
return location;
|
|
72
|
+
}
|
|
73
|
+
return `${FILE_SCHEME}${filePath}`;
|
|
74
|
+
}
|
|
75
|
+
async function describeFile(project, filePath, name, definitions) {
|
|
76
|
+
const absolute = path.resolve(project.root, filePath);
|
|
77
|
+
const relative = toDisplayPath(project.root, absolute);
|
|
78
|
+
const stats = isInside(project.root, absolute, { orSame: false })
|
|
79
|
+
&& await stat(absolute).catch(() => null);
|
|
80
|
+
if (!stats
|
|
81
|
+
|| !stats.isFile()) {
|
|
82
|
+
throw new Error(`No file at ${filePath}`);
|
|
83
|
+
}
|
|
84
|
+
return { location: `${FILE_SCHEME}${relative}`,
|
|
85
|
+
name: name ?? path.posix.basename(relative),
|
|
86
|
+
path: relative,
|
|
87
|
+
size: stats.size,
|
|
88
|
+
modified: stats.mtime.toISOString(),
|
|
89
|
+
definitions };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The definitions of the names, in their order.
|
|
93
|
+
*/
|
|
94
|
+
async function matchingDefinitions(project, providers, names) {
|
|
95
|
+
if (names.length === 0) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const definitions = await providers.artefactDefinitionProvider
|
|
99
|
+
.getDefinitions();
|
|
100
|
+
return names
|
|
101
|
+
.map(name => definitions.find(definition => definition.name === name))
|
|
102
|
+
.filter((definition) => definition !== undefined)
|
|
103
|
+
.map(definition => ({ name: definition.name,
|
|
104
|
+
description: firstParagraph(definition.description),
|
|
105
|
+
source: definition.source,
|
|
106
|
+
document: documentOf(project.root, definition.path) }));
|
|
107
|
+
}
|
|
108
|
+
function documentOf(root, document) {
|
|
109
|
+
if (document === undefined) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (!isInside(root, document, { orSame: false })) {
|
|
113
|
+
return document;
|
|
114
|
+
}
|
|
115
|
+
return toDisplayPath(root, document);
|
|
116
|
+
}
|
|
117
|
+
function firstParagraph(text) {
|
|
118
|
+
return (text.trim().split(/\n\s*\n/)[0] ?? '')
|
|
119
|
+
.replace(/\s+/g, ' ')
|
|
120
|
+
.trim();
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Runs an action with providers made for it, and closes the content store
|
|
124
|
+
* after it, so that each request sees the project as it is.
|
|
125
|
+
*/
|
|
126
|
+
async function withProviders(project, action) {
|
|
127
|
+
const providers = providersFactory(project.loggerProvider, project.root, project.definitions, { stateFolder: project.stateDir });
|
|
128
|
+
try {
|
|
129
|
+
return await action(providers);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
providers.contentStore.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dist/view.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type RequestHandler } from 'asljs-appbase';
|
|
2
|
+
import { type Project } from './project.js';
|
|
3
|
+
/**
|
|
4
|
+
* The details of an artefact, `describeArtefact`, by its `location` in the
|
|
5
|
+
* query, as JSON, relative to the project's path.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ARTEFACT_PATH = "/_/artefact.json";
|
|
8
|
+
/**
|
|
9
|
+
* The files and other artefacts of the project, `listProject`, as JSON,
|
|
10
|
+
* relative to the project's path.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DATA_PATH = "/_/data.json";
|
|
13
|
+
/**
|
|
14
|
+
* The handler of a project, relative to where it is mounted. It answers
|
|
15
|
+
* data, which the plugin renders: `DATA_PATH` the files and the other
|
|
16
|
+
* artefacts of the project, `ARTEFACT_PATH` the details of an artefact, or
|
|
17
|
+
* 404 with `{ error }` saying why there is none, and a file of the project
|
|
18
|
+
* root by its path, as it is, for the preview, unless `isServed` refuses it.
|
|
19
|
+
* The project is read on every request.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createProjectHandler(project: Project): RequestHandler;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a file of the project is served, by its path relative to the
|
|
24
|
+
* root: not one in a dot folder, a dot file or one under `node_modules`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isServed(relative: string): boolean;
|
package/dist/view.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHandler, jsonReply, messageOf } from 'asljs-appbase';
|
|
2
|
+
import { describeArtefact, listProject } from './project.js';
|
|
3
|
+
/**
|
|
4
|
+
* The details of an artefact, `describeArtefact`, by its `location` in the
|
|
5
|
+
* query, as JSON, relative to the project's path.
|
|
6
|
+
*/
|
|
7
|
+
export const ARTEFACT_PATH = '/_/artefact.json';
|
|
8
|
+
/**
|
|
9
|
+
* The files and other artefacts of the project, `listProject`, as JSON,
|
|
10
|
+
* relative to the project's path.
|
|
11
|
+
*/
|
|
12
|
+
export const DATA_PATH = '/_/data.json';
|
|
13
|
+
/**
|
|
14
|
+
* The handler of a project, relative to where it is mounted. It answers
|
|
15
|
+
* data, which the plugin renders: `DATA_PATH` the files and the other
|
|
16
|
+
* artefacts of the project, `ARTEFACT_PATH` the details of an artefact, or
|
|
17
|
+
* 404 with `{ error }` saying why there is none, and a file of the project
|
|
18
|
+
* root by its path, as it is, for the preview, unless `isServed` refuses it.
|
|
19
|
+
* The project is read on every request.
|
|
20
|
+
*/
|
|
21
|
+
export function createProjectHandler(project) {
|
|
22
|
+
return createHandler({ folder: project.root,
|
|
23
|
+
json: { [DATA_PATH]: async () => await listProject(project),
|
|
24
|
+
[ARTEFACT_PATH]: async (url) => {
|
|
25
|
+
try {
|
|
26
|
+
return await describeArtefact(project, url.searchParams.get('location') ?? '');
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return jsonReply(404, { error: `${messageOf(error)}.` });
|
|
30
|
+
}
|
|
31
|
+
} },
|
|
32
|
+
allow: isServed });
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether a file of the project is served, by its path relative to the
|
|
36
|
+
* root: not one in a dot folder, a dot file or one under `node_modules`.
|
|
37
|
+
*/
|
|
38
|
+
export function isServed(relative) {
|
|
39
|
+
return !relative
|
|
40
|
+
.split('/')
|
|
41
|
+
.some(segment => segment.startsWith('.')
|
|
42
|
+
|| segment === 'node_modules');
|
|
43
|
+
}
|
package/docs/part-srv.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# part-srv
|
|
2
|
+
|
|
3
|
+
Starts a web server that shows the files of projects and the artefact
|
|
4
|
+
definitions that match each, and runs until stopped, by SIGINT (Ctrl+C) or
|
|
5
|
+
SIGTERM.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
part-srv [--working-dir <folder>] [--port <port>] [--host <host>]
|
|
9
|
+
[--loglevel <level>] [--logfile <target>] [--logformat <format>]
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The server listens on `127.0.0.1` and prints the folder of its projects and its
|
|
13
|
+
address when it starts.
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
part-srv --working-dir ~/work --port 8080
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Projects
|
|
20
|
+
|
|
21
|
+
A project is a folder with its own `part.json`, looked for in the working
|
|
22
|
+
folder, `--working-dir` or else the working directory:
|
|
23
|
+
|
|
24
|
+
- When the working folder has a `part.json`, it is the one project.
|
|
25
|
+
- Otherwise, every folder below it that has a `part.json` is a project, by its
|
|
26
|
+
path from the working folder, leaving out what `.gitignore` ignores, `.git`
|
|
27
|
+
and `node_modules`.
|
|
28
|
+
- With none, the working folder is the one project, with the nearest `part.json`
|
|
29
|
+
of a parent.
|
|
30
|
+
- With no `part.json` there either, `part-srv` stops with an error, since a
|
|
31
|
+
project's `part.json` says which artefacts it supports; `factory-srv` serves
|
|
32
|
+
its other applications.
|
|
33
|
+
|
|
34
|
+
The root, the definitions and the state folder of each project come from its own
|
|
35
|
+
`part.json` only, `project`, `definitions` and `stateDir`, its paths resolved
|
|
36
|
+
against its folder; see [Options][OPT] for the keys. Without `project`, the root
|
|
37
|
+
is the project's folder; without `definitions`, no definition matches anything;
|
|
38
|
+
without `stateDir`, the state folder is `.part` under the project. No option or
|
|
39
|
+
environment variable of the server sets them, since one value cannot suit every
|
|
40
|
+
project: `PART_PROJECT`, `PART_DEFINITIONS` and `PART_STATE_DIR` are read by
|
|
41
|
+
`part` only. They are read when the project is first opened: an invalid key of
|
|
42
|
+
one project's `part.json` fails the requests of that project only.
|
|
43
|
+
|
|
44
|
+
## Pages
|
|
45
|
+
|
|
46
|
+
The paths are relative to where the server serves `part-srv`, `/` alone; a
|
|
47
|
+
server that shows several applications serves it at `/part/`.
|
|
48
|
+
|
|
49
|
+
- `/` - an editor filling the window, built with [`asljs-editor`][ED] of
|
|
50
|
+
`asljs-ui-editor`, whose only plugin is part. Down the left is a column of
|
|
51
|
+
operations, and beside it the open details as tabs. Pressing the operation's
|
|
52
|
+
icon opens its panel next to the column; the button at the bottom of the
|
|
53
|
+
column hides and shows the panel. Dragging the panel's inner edge resizes it,
|
|
54
|
+
and the browser remembers the width, and the project shown, for the next
|
|
55
|
+
visit. With more than one project, a selector heads the panel, its filter box
|
|
56
|
+
finding a project by its title; choosing another project shows its artefacts,
|
|
57
|
+
and the details of the one before stay open.
|
|
58
|
+
- **Artefacts** - the artefacts of the project as one tree of their locations,
|
|
59
|
+
folded at first: grouped by scheme, such as `file:`, `npm:` or `git:`, then
|
|
60
|
+
by each segment of the path, anything between `/` or `\`, as `npm:` >
|
|
61
|
+
`apps` > `kb` > `package.json#dependencies` > `asljs-part`; a location
|
|
62
|
+
without a scheme is a `file:` one. In each group its groups come first, then
|
|
63
|
+
its artefacts, by name. The files are every file `.gitignore` does not leave
|
|
64
|
+
out, outside `node_modules` and dot folders, and every file a definition
|
|
65
|
+
covers.
|
|
66
|
+
- **Search** - the shared search of the editor: the files and other artefacts
|
|
67
|
+
whose location holds the text, regardless of case unless Match case is
|
|
68
|
+
checked, or matches it as a regular expression: a file by its path, with or
|
|
69
|
+
without `file:`. At most 200 are listed.
|
|
70
|
+
|
|
71
|
+
Choosing a file or an artefact opens its details as a tab, or shows its tab
|
|
72
|
+
when it is open; the active one is marked in the list. The tab is a preview,
|
|
73
|
+
with its title in italics, which the next one chosen replaces, until it is
|
|
74
|
+
double clicked. A tab closes with its ×, a middle click, or Delete on the tab.
|
|
75
|
+
The details are rendered in the browser from `_/artefact.json`: the artefact's
|
|
76
|
+
name and location; for a file, its path, its size and when it was last
|
|
77
|
+
modified, in UTC; the artefact definitions that match it, each with where it
|
|
78
|
+
comes from - `markdown` or a plugin, and its definition document - and the
|
|
79
|
+
first paragraph of its description; a file no definition matches says so. A
|
|
80
|
+
file that does not exist, a path outside the project and an unknown location
|
|
81
|
+
give a note that says so.
|
|
82
|
+
|
|
83
|
+
The details have a preview of their file on their right, as the graph of
|
|
84
|
+
`rq-srv` has of its node: an `asljs-diagram-preview` of `asljs-ui-diagram`,
|
|
85
|
+
read from the server as the file is. An image is shown, a markdown file is
|
|
86
|
+
rendered - a link to another file of the project opens that file's details -
|
|
87
|
+
and other text is shown as it is. A binary file, one over 1 MiB, one the
|
|
88
|
+
server does not serve, and an artefact that is not a file have a note instead.
|
|
89
|
+
The View tab at the right of the action bar under the details shows it, and
|
|
90
|
+
the button after it shows and hides it; dragging its left edge resizes it, and
|
|
91
|
+
the browser remembers its width and whether it is shown, for every tab.
|
|
92
|
+
- `/_/app.json` - the application and its projects, each with the path it is
|
|
93
|
+
served at; `/_/plugin/plugin.js` - the plugin the editor loads, a browser
|
|
94
|
+
module, with the modules beside it under `/_/plugin/`.
|
|
95
|
+
- `/p/<project>/` - a project, by its path from the working folder, `~` for the
|
|
96
|
+
working folder itself. It answers data only, which the plugin renders:
|
|
97
|
+
- `/p/<project>/_/data.json` - `{ files, others }`, the files and the other
|
|
98
|
+
artefacts of the project, which the plugin loads once, when the project is
|
|
99
|
+
shown;
|
|
100
|
+
- `/p/<project>/_/artefact.json?location=<location>` - the details of a file
|
|
101
|
+
or another artefact: `{ location, name, path, size, modified, definitions
|
|
102
|
+
}`, `path`, `size` and `modified` `null` for an artefact that is not a file,
|
|
103
|
+
and each definition `{ name, description, source, document }`. A location is
|
|
104
|
+
a `file:` location, a path relative to the project root, or the location of
|
|
105
|
+
another artefact. When there is none, the answer is 404 with `{ error }`,
|
|
106
|
+
the message the details tab shows.
|
|
107
|
+
- `/p/<project>/<path>` - a file of the project root, by its path, as it is,
|
|
108
|
+
for the preview: a `.md` file as `text/markdown`. A file in a dot folder, a
|
|
109
|
+
dot file and a file under `node_modules` are not found.
|
|
110
|
+
|
|
111
|
+
The server renders nothing: it answers the details and the files as they are,
|
|
112
|
+
and the preview is made in the browser. The project is read on every request, so
|
|
113
|
+
a reload shows the files as they are. The page loads `asljs-ui-editor`,
|
|
114
|
+
`asljs-ui-diagram`, `asljs-ui-markdown` and what they import from the server,
|
|
115
|
+
under `/modules/`, so it needs no network.
|
|
116
|
+
|
|
117
|
+
## Options
|
|
118
|
+
|
|
119
|
+
- `--working-dir <folder>` - the folder the projects are looked for in, resolved
|
|
120
|
+
against the working directory; the working directory by default.
|
|
121
|
+
- `--port <port>` - the port, exactly: one in use is an error, and 0 picks any
|
|
122
|
+
free one. Without it, `port` of `part.json`, else the first free port from
|
|
123
|
+
3000 on, up to 3099.
|
|
124
|
+
- `--host <host>` - the address to listen on; `host` of `part.json`, else
|
|
125
|
+
`127.0.0.1`.
|
|
126
|
+
- `--loglevel`, `--logfile`, `--logformat` - logging, silent by default; they
|
|
127
|
+
take precedence over `PART_LOG_LEVEL`, `PART_LOG_FILE` and `PART_LOG_FORMAT`,
|
|
128
|
+
which take precedence over `logLevel`, `logFile` and `logFormat` of
|
|
129
|
+
`part.json`. See [Logging][LG].
|
|
130
|
+
- `--version`, `--help` - print the version of `asljs-part-srv`, or the help.
|
|
131
|
+
|
|
132
|
+
The `part.json` of `port`, `host` and logging is the nearest one in the working
|
|
133
|
+
folder or a parent of it; that of a project's settings is the project's, see
|
|
134
|
+
[Projects][PRJ]. A path of the file resolves against its folder; see
|
|
135
|
+
[Configuration][CFG] and the keys in [Options][OPT]. A `part.json` that cannot
|
|
136
|
+
be read, or a wrong `port`, `host` or logging key, stops the server with a
|
|
137
|
+
message naming the file.
|
|
138
|
+
|
|
139
|
+
[CFG]: ../../../../docs/Configuration.md
|
|
140
|
+
[ED]: ../../../../ui/editor/docs/Editor.md
|
|
141
|
+
[LG]: ../../../../docs/Logging.md
|
|
142
|
+
[OPT]: ../../part-cli/docs/Options.md
|
|
143
|
+
[PRJ]: #projects
|
package/package.json
CHANGED
|
@@ -1,6 +1,87 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "asljs-part-srv",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "The part-srv web server: a project's files in an editor, each with its details and the artefact definitions that match it.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"asljs": {
|
|
7
|
+
"type": "app"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"bin": {
|
|
12
|
+
"part-srv": "bin/part-srv.js"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin/**",
|
|
22
|
+
"dist/**",
|
|
23
|
+
"docs/**",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE.md",
|
|
26
|
+
"!dist/*.tsbuildinfo"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"flint": "toolkit flint",
|
|
30
|
+
"clean:build": "node ../../../scripts/cleanBuildOutDir.js",
|
|
31
|
+
"clean:dist": "node ../../../scripts/cleanDistOutDir.js",
|
|
32
|
+
"compile:build": "npm run codegen --if-present && tsc -p tsconfig.build.json",
|
|
33
|
+
"compile:dist": "npm run codegen --if-present && tsc -p tsconfig.dist.json",
|
|
34
|
+
"build:dist": "node ../../../scripts/buildDistDeps.js && npm run compile:dist",
|
|
35
|
+
"typecheck": "npm run codegen --if-present && tsc -p tsconfig.build.json --noEmit",
|
|
36
|
+
"guard:clean-git": "toolkit ensure-clean-working-directory",
|
|
37
|
+
"prepack": "npm run clean:build && npm run clean:dist && npm run build:dist && npm run compile:build && npm run test && npm run guard:clean-git",
|
|
38
|
+
"postpublish": "toolkit tag-release-revision",
|
|
39
|
+
"test": "node --test --enable-source-maps \"build/**/*.test.js\"",
|
|
40
|
+
"test:watch": "node --watch --test \"build/**/*.test.js\"",
|
|
41
|
+
"release:patch": "toolkit release-patch",
|
|
42
|
+
"build": "npm run clean:build && npm run flint && npm run typecheck && npm run compile:build && npm run test"
|
|
43
|
+
},
|
|
44
|
+
"distDependencies": [
|
|
45
|
+
"asljs-appbase",
|
|
46
|
+
"asljs-locator",
|
|
47
|
+
"asljs-logging",
|
|
48
|
+
"asljs-observable",
|
|
49
|
+
"asljs-part",
|
|
50
|
+
"asljs-testing",
|
|
51
|
+
"asljs-tmpdir",
|
|
52
|
+
"asljs-ui-diagram",
|
|
53
|
+
"asljs-ui-editor",
|
|
54
|
+
"asljs-ui-markdown"
|
|
55
|
+
],
|
|
56
|
+
"keywords": [
|
|
57
|
+
"artefacts",
|
|
58
|
+
"project",
|
|
59
|
+
"server"
|
|
60
|
+
],
|
|
61
|
+
"homepage": "https://github.com/AlexandriteSoftware/asljs#readme",
|
|
62
|
+
"bugs": {
|
|
63
|
+
"url": "https://github.com/AlexandriteSoftware/asljs/issues"
|
|
64
|
+
},
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "git+https://github.com/AlexandriteSoftware/asljs.git"
|
|
68
|
+
},
|
|
69
|
+
"author": "\"Alex Netkachov\" <alex.netkachov@gmail.com>",
|
|
70
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"asljs-locator": "^0.1.1",
|
|
73
|
+
"asljs-logging": "^0.1.1",
|
|
74
|
+
"asljs-appbase": "^0.1.3",
|
|
75
|
+
"asljs-observable": "^0.5.6",
|
|
76
|
+
"asljs-part": "^0.3.2",
|
|
77
|
+
"asljs-ui-diagram": "^0.1.2",
|
|
78
|
+
"asljs-ui-editor": "^0.1.1",
|
|
79
|
+
"asljs-ui-markdown": "^0.1.1",
|
|
80
|
+
"commander": "^15.0.0"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"asljs-testing": "^0.1.0",
|
|
84
|
+
"asljs-tmpdir": "^0.1.13",
|
|
85
|
+
"asljs-toolkit": "^0.1.0"
|
|
86
|
+
}
|
|
87
|
+
}
|