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 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
- # Temporary Holding Version
1
+ # part-srv
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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>
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main }
4
+ from '../dist/main.js';
5
+
6
+ process.exitCode =
7
+ await main(
8
+ process.argv.slice(2),
9
+ { cwd: process.cwd(),
10
+ env: process.env,
11
+ stdout: process.stdout,
12
+ stderr: process.stderr });
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
+ }
@@ -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
@@ -0,0 +1,4 @@
1
+ export { createApp } from './app.js';
2
+ export { main } from './main.js';
3
+ export { describeArtefact, listProject } from './project.js';
4
+ export { ARTEFACT_PATH, DATA_PATH } from './view.js';
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>;
@@ -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
+ }
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }