asljs-appbase 0.0.0-stage → 0.1.5

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.
@@ -0,0 +1,268 @@
1
+ import { loadConfig } from 'asljs-cmdbase';
2
+ import path from 'node:path';
3
+ import { createImportMap, MODULES_PREFIX } from './browser-modules.js';
4
+ import { listFiles } from './files.js';
5
+ import { createHandler, mountHandlers, page, scriptJson, send, serve, serverUrl, untilStopped } from './server.js';
6
+ /**
7
+ * The path an application serves its plugin's folder at.
8
+ */
9
+ const PLUGIN_PREFIX = '/_/plugin/';
10
+ /**
11
+ * The projects of an application in `folder`, by its configuration file
12
+ * `name`, e.g. `kb.json`: the folder itself when it has the file; else every
13
+ * folder below it that has one, by id; else the folder itself, with the
14
+ * nearest file of a parent, or with none (`loadConfig`). The files are
15
+ * `files`, the files of `folder` as `listFiles` gives them, so that a server
16
+ * of several applications lists them once; `listFiles(folder)` when absent.
17
+ *
18
+ * With `required`, a project needs the file: when the folder, the folders
19
+ * below it and those above it have none, it throws, for an application whose
20
+ * projects cannot be told without it, e.g. which artefacts a part project
21
+ * supports.
22
+ */
23
+ export async function findProjects(folder, name, files, options = {}) {
24
+ const root = path.resolve(folder);
25
+ const listed = files
26
+ ?? await listFiles(root);
27
+ if (listed.includes(name)) {
28
+ return [await toProject(root, root, name)];
29
+ }
30
+ const suffix = `/${name}`;
31
+ const folders = listed
32
+ .filter(file => file.endsWith(suffix))
33
+ .map(file => path.join(root, file.slice(0, -suffix.length)));
34
+ if (folders.length > 0) {
35
+ return (await Promise.all(folders.map(found => toProject(root, found, name))))
36
+ .sort((a, b) => a.id.localeCompare(b.id));
37
+ }
38
+ const project = await toProject(root, root, name);
39
+ if (options.required === true
40
+ && project.config.file === null) {
41
+ throw new Error(`No ${name} in ${root}, in a folder below it, or in a folder above it.`);
42
+ }
43
+ return [project];
44
+ }
45
+ /**
46
+ * The projects of an application in the folders of `context` by its
47
+ * configuration file `name`: `findProjects` of `cwd` and `files` alone, or of
48
+ * each of `folders` when there are several. Then the id of a project starts
49
+ * with the name of its folder, `docs/notes` for `notes` of `docs`, the name
50
+ * alone for the folder itself, and a name that two folders share gets `-2`,
51
+ * `-3` and so on after the first. With `required`, a folder without the file
52
+ * has no project, and an error is thrown only when no folder has one.
53
+ */
54
+ export async function findAppProjects(context, name, options = {}) {
55
+ const folders = context.folders
56
+ ?? [{ cwd: context.cwd,
57
+ files: context.files }];
58
+ if (folders.length === 1) {
59
+ return await findProjects(folders[0].cwd, name, folders[0].files, options);
60
+ }
61
+ const labels = folderLabels(folders.map(folder => folder.cwd));
62
+ const projects = [];
63
+ for (const [index, folder] of folders.entries()) {
64
+ for (const project of await findProjects(folder.cwd, name, folder.files)) {
65
+ if (options.required === true
66
+ && project.config.file === null) {
67
+ continue;
68
+ }
69
+ const id = project.id === '.'
70
+ ? labels[index]
71
+ : `${labels[index]}/${project.id}`;
72
+ projects.push({ ...project,
73
+ id,
74
+ title: id });
75
+ }
76
+ }
77
+ if (projects.length === 0) {
78
+ throw new Error(`No ${name} in ${folders
79
+ .map(folder => path.resolve(folder.cwd))
80
+ .join(', ')}, in a folder below them, or in a folder above them.`);
81
+ }
82
+ return projects;
83
+ }
84
+ /**
85
+ * The name of each folder, by its base name, `-2`, `-3` and so on added to a
86
+ * name an earlier folder has.
87
+ */
88
+ function folderLabels(folders) {
89
+ const labels = [];
90
+ for (const folder of folders) {
91
+ const resolved = path.resolve(folder);
92
+ const base = path.basename(resolved)
93
+ || resolved;
94
+ let label = base;
95
+ for (let n = 2; labels.includes(label); n++) {
96
+ label = `${base}-${n}`;
97
+ }
98
+ labels.push(label);
99
+ }
100
+ return labels;
101
+ }
102
+ async function toProject(root, folder, name) {
103
+ const id = path.relative(root, folder)
104
+ .split(path.sep)
105
+ .join('/')
106
+ || '.';
107
+ return { id,
108
+ title: id === '.'
109
+ ? path.basename(root)
110
+ : id,
111
+ folder,
112
+ config: await loadConfig(folder, name) };
113
+ }
114
+ /**
115
+ * The segment of a project in its path: its id, `~` for `.`.
116
+ */
117
+ export function projectSegment(id) {
118
+ return id === '.'
119
+ ? '~'
120
+ : encodeURIComponent(id);
121
+ }
122
+ /**
123
+ * An application served as an editor plugin; see `EditorApp`.
124
+ */
125
+ export function createEditorApp(options) {
126
+ const base = options.base ?? '/';
127
+ const handlers = new Map();
128
+ const closers = [];
129
+ const modules = createHandler({ folder: '/',
130
+ modules: options.modules,
131
+ index: async () => '',
132
+ allow: () => false });
133
+ const style = options.style ?? '';
134
+ const pluginFiles = createHandler({ folder: path.dirname(options.plugin),
135
+ methods: ['GET',
136
+ 'HEAD'],
137
+ allow: relative => /\.(?:m?js|map)$/.test(relative) });
138
+ const descriptor = () => ({ id: options.id,
139
+ title: options.title,
140
+ icon: options.icon,
141
+ plugin: `${base}${PLUGIN_PREFIX.slice(1)}${encodeURIComponent(path.basename(options.plugin))}`,
142
+ projects: options.projects.map(project => ({ id: project.id,
143
+ title: project.title,
144
+ base: `${base}p/${projectSegment(project.id)}/` })) });
145
+ const handler = async (request, response, url) => {
146
+ if (url.pathname === '/') {
147
+ send(response, 200, 'text/html; charset=utf-8', editorPage({ title: options.title,
148
+ apps: [`${base}_/app.json`],
149
+ modules: options.modules,
150
+ style }));
151
+ return;
152
+ }
153
+ if (url.pathname === '/_/app.json') {
154
+ send(response, 200, 'application/json; charset=utf-8', JSON.stringify(descriptor()));
155
+ return;
156
+ }
157
+ if (url.pathname.startsWith(PLUGIN_PREFIX)) {
158
+ const inner = new URL(url);
159
+ inner.pathname =
160
+ url.pathname.slice(PLUGIN_PREFIX.length - 1);
161
+ await pluginFiles(request, response, inner);
162
+ return;
163
+ }
164
+ if (url.pathname.startsWith(MODULES_PREFIX)) {
165
+ await modules(request, response, url);
166
+ return;
167
+ }
168
+ const match = /^\/p\/([^/]+)(\/.*)?$/.exec(url.pathname);
169
+ const id = match === null
170
+ ? undefined
171
+ : match[1] === '~'
172
+ ? '.'
173
+ : decodeURIComponent(match[1]);
174
+ const project = options.projects.find(candidate => candidate.id === id);
175
+ if (match === null
176
+ || project === undefined) {
177
+ send(response, 404, 'text/plain; charset=utf-8', `Not found: ${url.pathname}`);
178
+ return;
179
+ }
180
+ if (match[2] === undefined) {
181
+ response.writeHead(302, { location: `${base}p/${match[1]}/${url.search}` });
182
+ response.end();
183
+ return;
184
+ }
185
+ let made = handlers.get(project.id);
186
+ if (made === undefined) {
187
+ made =
188
+ Promise.resolve(options.project(project, { base: `${base}p/${match[1]}/`,
189
+ onClose: action => closers.push(action) }));
190
+ handlers.set(project.id, made);
191
+ }
192
+ const inner = new URL(url);
193
+ inner.pathname = match[2];
194
+ await (await made)(request, response, inner);
195
+ };
196
+ return { id: options.id,
197
+ title: options.title,
198
+ base,
199
+ style,
200
+ modules: options.modules,
201
+ handler,
202
+ close: async () => {
203
+ for (const close of closers.splice(0)) {
204
+ await close();
205
+ }
206
+ } };
207
+ }
208
+ /**
209
+ * A handler for several applications, each mounted at `/<id>/`, which is the
210
+ * `base` it was made with: `/` is the editor with every application as a
211
+ * plugin, and `/modules/...` the modules of them all.
212
+ */
213
+ export function combineEditorApps(title, apps) {
214
+ const modules = [...new Map(apps
215
+ .flatMap(app => app.modules)
216
+ .map(module => [module.name,
217
+ module]))
218
+ .values()];
219
+ const files = createHandler({ folder: '/',
220
+ modules,
221
+ index: async () => '',
222
+ allow: () => false });
223
+ const style = apps
224
+ .map(app => app.style)
225
+ .join('\n');
226
+ return mountHandlers(Object.fromEntries(apps.map(app => [app.id,
227
+ app.handler])), async (request, response, url) => {
228
+ if (url.pathname === '/') {
229
+ send(response, 200, 'text/html; charset=utf-8', editorPage({ title,
230
+ apps: apps.map(app => `${app.base}_/app.json`),
231
+ modules,
232
+ style }));
233
+ return;
234
+ }
235
+ await files(request, response, url);
236
+ });
237
+ }
238
+ /**
239
+ * An editor page whose plugins are the applications of `apps`, the URLs of
240
+ * their `/_/app.json`; see `mountEditorApps` of `asljs-ui-editor`.
241
+ */
242
+ export function editorPage(options) {
243
+ return page(options.title, `<script type="importmap">${scriptJson(createImportMap(options.modules, MODULES_PREFIX))}</script>
244
+ <asljs-editor id="editor"></asljs-editor>
245
+ <script type="module">
246
+ import { mountEditorApps } from 'asljs-ui-editor';
247
+ await mountEditorApps(
248
+ document.getElementById('editor'),
249
+ ${scriptJson(options.apps)},
250
+ { storageKey: ${scriptJson(`asljs-editor:${options.title}`)} });
251
+ </script>`, options.style ?? '');
252
+ }
253
+ /**
254
+ * Serves an editor application, as `<app>-srv` does: prints where it serves
255
+ * `cwd`, serves until SIGINT or SIGTERM, then closes the application.
256
+ */
257
+ export async function serveApp(app, options) {
258
+ const server = await serve(app.handler, { port: options.port,
259
+ host: options.host,
260
+ logger: options.loggerProvider.getLogger(`${app.id}.srv`) });
261
+ options.io.stdout.write(`Serving ${options.cwd} at ${serverUrl(server)}\n`);
262
+ try {
263
+ await untilStopped(server);
264
+ }
265
+ finally {
266
+ await app.close();
267
+ }
268
+ }
@@ -0,0 +1,25 @@
1
+ export interface FindOptions {
2
+ /**
3
+ * Names of folders to skip besides those starting with `.` and
4
+ * `node_modules`, e.g. `Archive`.
5
+ */
6
+ skip?: readonly string[];
7
+ }
8
+ /**
9
+ * Every markdown file of a folder and its subfolders, sorted, by absolute
10
+ * path, as the `LocationResolver` of `asljs-locator` finds them: skipping
11
+ * folders whose name starts with `.`, `node_modules`, and the `skip` names.
12
+ */
13
+ export declare function findMarkdownFiles(folder: string, options?: FindOptions): Promise<string[]>;
14
+ /**
15
+ * Every file of a folder and its subfolders, relative to it with `/`
16
+ * separators, sorted, as the `LocationResolver` of `asljs-locator` finds them:
17
+ * what `.gitignore` files ignore is left out, and so are `.git` and
18
+ * `node_modules`. One list serves every search of the folder's files, such as
19
+ * each application's `findProjects`.
20
+ */
21
+ export declare function listFiles(folder: string): Promise<string[]>;
22
+ /**
23
+ * A path relative to `folder`, with `/` separators.
24
+ */
25
+ export declare function toDisplayPath(folder: string, file: string): string;
package/dist/files.js ADDED
@@ -0,0 +1,48 @@
1
+ import { LocationResolver } from 'asljs-locator';
2
+ import { NullLogger } from 'asljs-logging';
3
+ import path from 'node:path';
4
+ /**
5
+ * Every markdown file of a folder and its subfolders, sorted, by absolute
6
+ * path, as the `LocationResolver` of `asljs-locator` finds them: skipping
7
+ * folders whose name starts with `.`, `node_modules`, and the `skip` names.
8
+ */
9
+ export async function findMarkdownFiles(folder, options = {}) {
10
+ const root = path.resolve(folder);
11
+ return (await new LocationResolver(new NullLogger(), root)
12
+ .resolve(root,
13
+ // Any case of the extension, as `.MD`.
14
+ { patterns: ['**/*.md',
15
+ '**/*.MD',
16
+ '**/*.Md',
17
+ '**/*.mD'],
18
+ exclude: ['**/.*/**',
19
+ '**/node_modules/**',
20
+ ...(options.skip ?? []).map(name => `**/${name}/**`)] }))
21
+ .sort();
22
+ }
23
+ /**
24
+ * Every file of a folder and its subfolders, relative to it with `/`
25
+ * separators, sorted, as the `LocationResolver` of `asljs-locator` finds them:
26
+ * what `.gitignore` files ignore is left out, and so are `.git` and
27
+ * `node_modules`. One list serves every search of the folder's files, such as
28
+ * each application's `findProjects`.
29
+ */
30
+ export async function listFiles(folder) {
31
+ const root = path.resolve(folder);
32
+ const files = await new LocationResolver(new NullLogger(), root)
33
+ .resolve(root, { pattern: '**/*',
34
+ exclude: ['.git/**',
35
+ '**/node_modules/**'],
36
+ filters: [{ name: 'GitIgnore' }] });
37
+ return files
38
+ .map(file => toDisplayPath(root, file))
39
+ .sort();
40
+ }
41
+ /**
42
+ * A path relative to `folder`, with `/` separators.
43
+ */
44
+ export function toDisplayPath(folder, file) {
45
+ return path.relative(folder, file)
46
+ .split(path.sep)
47
+ .join('/');
48
+ }
@@ -0,0 +1,8 @@
1
+ export { createImportMap, findBrowserModules, findModuleFile, MODULES_PREFIX, type BrowserModule } from './browser-modules.js';
2
+ export { commandLine, configBoolean, configLogging, configPath, configPaths, configPort, configString, configStrings, loadCommandLineConfig, loadConfig, messageOf, packageVersion, parsePort, readOptionValue, runMain, workingFolder, type CommandIo, type Config } from 'asljs-cmdbase';
3
+ export { combineEditorApps, createEditorApp, editorPage, findAppProjects, findProjects, projectSegment, serveApp, type EditorApp, type EditorAppContext, type EditorAppDescriptor, type EditorAppOptions, type Project, type ProjectContext, type WorkingFolder } from './editor-app.js';
4
+ export { findMarkdownFiles, listFiles, toDisplayPath, type FindOptions } from './files.js';
5
+ export { findSectionHeading, formatUrl, getSection, parseMarkdown, plainText, splitLocalUrl, type MarkdownNode } from './markdown.js';
6
+ export { findConfig, postProcess, takeWritten, writeMarkdown, type PostProcessIo, type ToolConfig } from './post-process.js';
7
+ export { createHandler, DEFAULT_PORT, escapeHtml, jsonReply, mountHandlers, page, scriptJson, send, serve, serverUrl, startServer, untilStopped, versionConflict, type HandlerOptions, type JsonReply, type ListenOptions, type PostReply, type RequestHandler, type ServerOptions } from './server.js';
8
+ export { stateFolder, type GivenStateFolder } from './state-folder.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { createImportMap, findBrowserModules, findModuleFile, MODULES_PREFIX } from './browser-modules.js';
2
+ export { commandLine, configBoolean, configLogging, configPath, configPaths, configPort, configString, configStrings, loadCommandLineConfig, loadConfig, messageOf, packageVersion, parsePort, readOptionValue, runMain, workingFolder } from 'asljs-cmdbase';
3
+ export { combineEditorApps, createEditorApp, editorPage, findAppProjects, findProjects, projectSegment, serveApp } from './editor-app.js';
4
+ export { findMarkdownFiles, listFiles, toDisplayPath } from './files.js';
5
+ export { findSectionHeading, formatUrl, getSection, parseMarkdown, plainText, splitLocalUrl } from './markdown.js';
6
+ export { findConfig, postProcess, takeWritten, writeMarkdown } from './post-process.js';
7
+ export { createHandler, DEFAULT_PORT, escapeHtml, jsonReply, mountHandlers, page, scriptJson, send, serve, serverUrl, startServer, untilStopped, versionConflict } from './server.js';
8
+ export { stateFolder } from './state-folder.js';
@@ -0,0 +1,27 @@
1
+ import { type Heading, type Root, type RootContent } from 'mdast';
2
+ export type MarkdownNode = Root | RootContent;
3
+ export declare function parseMarkdown(text: string): Root;
4
+ /**
5
+ * The level 2 heading with the given text.
6
+ */
7
+ export declare function findSectionHeading(root: Root, name: string): Heading | null;
8
+ /**
9
+ * The nodes between a level 2 heading and the next heading of level 1 or 2,
10
+ * or `null` when there is no such heading.
11
+ */
12
+ export declare function getSection(root: Root, name: string): RootContent[] | null;
13
+ export declare function plainText(node: MarkdownNode): string;
14
+ /**
15
+ * Splits a link target that points at a local file into the path, with
16
+ * percent-encoding decoded, and the `#` fragment; `null` for a target with a
17
+ * scheme, a protocol-relative, root-relative or fragment-only target.
18
+ */
19
+ export declare function splitLocalUrl(url: string): {
20
+ path: string;
21
+ fragment: string;
22
+ } | null;
23
+ /**
24
+ * A link target in the form written by `rq`: in angle brackets when it has
25
+ * spaces or parentheses.
26
+ */
27
+ export declare function formatUrl(url: string): string;
@@ -0,0 +1,84 @@
1
+ import remarkParse from 'remark-parse';
2
+ import { unified } from 'unified';
3
+ export function parseMarkdown(text) {
4
+ return unified()
5
+ .use(remarkParse)
6
+ .parse(text);
7
+ }
8
+ /**
9
+ * The level 2 heading with the given text.
10
+ */
11
+ export function findSectionHeading(root, name) {
12
+ return root.children.find((node) => node.type === 'heading'
13
+ && node.depth === 2
14
+ && plainText(node) === name)
15
+ ?? null;
16
+ }
17
+ /**
18
+ * The nodes between a level 2 heading and the next heading of level 1 or 2,
19
+ * or `null` when there is no such heading.
20
+ */
21
+ export function getSection(root, name) {
22
+ const heading = findSectionHeading(root, name);
23
+ if (!heading) {
24
+ return null;
25
+ }
26
+ const nodes = [];
27
+ for (const node of root.children.slice(root.children.indexOf(heading) + 1)) {
28
+ if (node.type === 'heading'
29
+ && node.depth <= 2) {
30
+ break;
31
+ }
32
+ nodes.push(node);
33
+ }
34
+ return nodes;
35
+ }
36
+ export function plainText(node) {
37
+ if ('value' in node) {
38
+ return node.value;
39
+ }
40
+ if ('children' in node) {
41
+ return node.children
42
+ .map(child => plainText(child))
43
+ .join('');
44
+ }
45
+ return '';
46
+ }
47
+ /**
48
+ * Splits a link target that points at a local file into the path, with
49
+ * percent-encoding decoded, and the `#` fragment; `null` for a target with a
50
+ * scheme, a protocol-relative, root-relative or fragment-only target.
51
+ */
52
+ export function splitLocalUrl(url) {
53
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url)
54
+ || url.startsWith('/')
55
+ || url.startsWith('#')
56
+ || url === '') {
57
+ return null;
58
+ }
59
+ const hash = url.indexOf('#');
60
+ const encoded = hash < 0
61
+ ? url
62
+ : url.slice(0, hash);
63
+ let decoded;
64
+ try {
65
+ decoded =
66
+ decodeURIComponent(encoded);
67
+ }
68
+ catch {
69
+ decoded = encoded;
70
+ }
71
+ return { path: decoded,
72
+ fragment: hash < 0
73
+ ? ''
74
+ : url.slice(hash) };
75
+ }
76
+ /**
77
+ * A link target in the form written by `rq`: in angle brackets when it has
78
+ * spaces or parentheses.
79
+ */
80
+ export function formatUrl(url) {
81
+ return /[\s()]/.test(url)
82
+ ? `<${url}>`
83
+ : url;
84
+ }
@@ -0,0 +1,50 @@
1
+ import { type Logger } from 'asljs-logging';
2
+ /**
3
+ * What a tool's configuration file, e.g. `rq.json`, says.
4
+ */
5
+ export interface ToolConfig {
6
+ /**
7
+ * A command line run, in the folder of the configuration file, with the
8
+ * markdown files a command wrote as arguments, e.g. a project's formatter
9
+ * and linter.
10
+ */
11
+ markdownPostProcessing?: string;
12
+ }
13
+ /**
14
+ * Where `postProcess` reports a failure.
15
+ */
16
+ export interface PostProcessIo {
17
+ cwd: string;
18
+ stderr: {
19
+ write(text: string): unknown;
20
+ };
21
+ /**
22
+ * Where the command line and its exit code are logged; nothing is logged
23
+ * when absent.
24
+ */
25
+ logger?: Logger;
26
+ }
27
+ /**
28
+ * Writes a markdown file and remembers it for `postProcess`.
29
+ */
30
+ export declare function writeMarkdown(file: string, text: string): Promise<void>;
31
+ /**
32
+ * The markdown files written since the last call, and forgets them.
33
+ */
34
+ export declare function takeWritten(): string[];
35
+ /**
36
+ * The nearest configuration file named `name`, e.g. `rq.json`, of `folder` or
37
+ * a parent, and its folder; `null` when there is none.
38
+ */
39
+ export declare function findConfig(folder: string, name: string): Promise<{
40
+ folder: string;
41
+ config: ToolConfig;
42
+ } | null>;
43
+ /**
44
+ * Runs the `markdownPostProcessing` command of the nearest configuration file
45
+ * named `config`, e.g. `rq.json`, with the markdown files a command wrote and
46
+ * that still exist, relative to the folder of the configuration file, where it
47
+ * runs. Returns 1 and reports the output when the command fails, 0 otherwise
48
+ * or when there is nothing to do.
49
+ */
50
+ export declare function postProcess(io: PostProcessIo, files: readonly string[], config: string): Promise<number>;
@@ -0,0 +1,84 @@
1
+ import { runCommand } from 'asljs-agents';
2
+ import { loadConfig } from 'asljs-cmdbase';
3
+ import { NullLogger } from 'asljs-logging';
4
+ import { stat, writeFile } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ const written = new Set();
7
+ /**
8
+ * Writes a markdown file and remembers it for `postProcess`.
9
+ */
10
+ export async function writeMarkdown(file, text) {
11
+ await writeFile(file, text, 'utf8');
12
+ written.add(path.resolve(file));
13
+ }
14
+ /**
15
+ * The markdown files written since the last call, and forgets them.
16
+ */
17
+ export function takeWritten() {
18
+ const files = [...written];
19
+ written.clear();
20
+ return files;
21
+ }
22
+ /**
23
+ * The nearest configuration file named `name`, e.g. `rq.json`, of `folder` or
24
+ * a parent, and its folder; `null` when there is none.
25
+ */
26
+ export async function findConfig(folder, name) {
27
+ const config = await loadConfig(folder, name);
28
+ if (config.folder === null) {
29
+ return null;
30
+ }
31
+ const command = config.values.markdownPostProcessing;
32
+ if (command !== undefined
33
+ && typeof command
34
+ !== 'string') {
35
+ throw new Error(`${config.file}: "markdownPostProcessing" must be a command line.`);
36
+ }
37
+ return { folder: config.folder,
38
+ config: config.values };
39
+ }
40
+ /**
41
+ * Runs the `markdownPostProcessing` command of the nearest configuration file
42
+ * named `config`, e.g. `rq.json`, with the markdown files a command wrote and
43
+ * that still exist, relative to the folder of the configuration file, where it
44
+ * runs. Returns 1 and reports the output when the command fails, 0 otherwise
45
+ * or when there is nothing to do.
46
+ */
47
+ export async function postProcess(io, files, config) {
48
+ const existing = [];
49
+ for (const file of files) {
50
+ if ((await stat(file).catch(() => null))?.isFile()) {
51
+ existing.push(file);
52
+ }
53
+ }
54
+ if (existing.length === 0) {
55
+ return 0;
56
+ }
57
+ const found = await findConfig(io.cwd, config);
58
+ const command = found?.config.markdownPostProcessing?.trim();
59
+ if (found === null
60
+ || command === undefined
61
+ || command === '') {
62
+ return 0;
63
+ }
64
+ const line = [command,
65
+ ...existing.map(file => JSON.stringify(path.relative(found.folder, file)
66
+ .split(path.sep)
67
+ .join('/')))]
68
+ .join(' ');
69
+ const logger = io.logger ?? new NullLogger();
70
+ logger.debug({ command: line,
71
+ cwd: found.folder }, 'post-processing');
72
+ const run = await runCommand(line, found.folder)
73
+ .catch((error) => ({ code: -1,
74
+ stdout: '',
75
+ stderr: error instanceof Error
76
+ ? error.message
77
+ : String(error) }));
78
+ logger.debug({ code: run.code }, 'post-processing exited');
79
+ if (run.code === 0) {
80
+ return 0;
81
+ }
82
+ io.stderr.write(`Post-processing failed: ${command} exited with code ${run.code}\n${`${run.stdout}${run.stderr}`.trimEnd()}\n`);
83
+ return 1;
84
+ }