@dogsbay/dita-publish 0.0.0-stage → 0.2.0-beta.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,14 @@
1
- # Temporary Holding Version
1
+ # @dogsbay/dita-publish
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
+ Publishing DITA through DITA-OT as a dogsbay site. Command: `dogsbay-dita`.
4
+
5
+ ```bash
6
+ npx -y @dogsbay/dita-publish@beta publish-dita <dita-ot-temp-dir> -o site
7
+ npx -y @dogsbay/dita-publish@beta dita dev -d <deliverable>
8
+ ```
9
+
10
+ - `publishDitaTemp` (`src/publish.ts`): DITA-OT's preprocessed temp folder → pages, `nav.yml`, resources. Converts topics with `@dogsbay/format-dita`, through its `./publish-support` hooks.
11
+ - `dogsbay-dita publish-dita` and `dogsbay-dita dita dev`: the commands that used to be in the `dogsbay` CLI. `dita dev` builds and serves the site with the `dogsbay` CLI's `site` commands.
12
+ - `scripts/bundle-publish-dita.mjs` (repo only): the single-file converter the DITA-OT plugin `@dogsbay/dita-ot-plugin` carries.
13
+
14
+ Usually you publish with the DITA-OT plugin (`dita -f dogsbay`), which carries this converter. See "Publishing DITA" in the dogsbay documentation.
package/bin/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/cli.js";
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `dogsbay-dita`: the DITA publishing commands (moved out of the `dogsbay` CLI,
3
+ * whose site commands `dita dev` drives).
4
+ *
5
+ * dogsbay-dita publish-dita <temp-dir> -o <site> DITA-OT temp folder → site project
6
+ * dogsbay-dita dita dev [map] -d <deliverable> live preview through DITA-OT
7
+ *
8
+ * The DITA-OT plugin com.dogsbay.publish runs `publish-dita` from its own
9
+ * single-file bundle (scripts/bundle-publish-dita.mjs), or from this script
10
+ * when given as `dogsbay.converter`, which is what `dita dev` passes.
11
+ */
12
+ import { readFileSync } from "node:fs";
13
+ import { Command } from "commander";
14
+ import { registerPublishDita } from "./commands/publish-dita.js";
15
+ import { registerDitaDev } from "./commands/dita-dev.js";
16
+ const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
17
+ const program = new Command("dogsbay-dita").description("Publish DITA through DITA-OT as a dogsbay site").version(version);
18
+ const cliFail = (error) => {
19
+ console.error(error instanceof Error ? error.message : String(error));
20
+ process.exitCode = 1;
21
+ };
22
+ registerPublishDita(program, cliFail);
23
+ registerDitaDev(program, cliFail);
24
+ await program.parseAsync(process.argv);
@@ -0,0 +1,125 @@
1
+ import type { Command } from "commander";
2
+ import type { SiteDevOptions } from "dogsbay/lib";
3
+ import { type DitaDeliverable } from "../dita/project-file.js";
4
+ export interface DitaDevOptions {
5
+ /**
6
+ * The site project folder the map is published into. Default: the deliverable's
7
+ * output, else `site`.
8
+ */
9
+ output?: string;
10
+ /** DITA-OT installation folder (default: DITA_HOME, else `dita` on the PATH). */
11
+ ditaOt?: string;
12
+ /** A DITA-OT project file (default: the project files found in the current folder). */
13
+ project?: string;
14
+ /** The deliverable to publish: its name, or its context id. */
15
+ deliverable?: string;
16
+ /** DITAVAL files (DITA-OT's --filter); given, they replace the deliverable's. */
17
+ filter?: string[];
18
+ /** DITA-OT and plugin parameters, `name=value`; they override the deliverable's. */
19
+ param?: string[];
20
+ /** Folders or files to watch (default: the project folder, or the map's folder). */
21
+ watch?: string[];
22
+ /** Astro's --host: listen on all addresses. */
23
+ host?: boolean;
24
+ /** Listen on this address instead (Astro's --host <address>). */
25
+ hostAddress?: string;
26
+ /** Print all of DITA-OT's output, not only its errors. */
27
+ verbose?: boolean;
28
+ }
29
+ export declare const PLUGIN_ID = "com.dogsbay.publish";
30
+ /** Where `dita` is, and the DITA-OT installation it belongs to. */
31
+ export interface DitaLocation {
32
+ dita: string;
33
+ home: string;
34
+ }
35
+ /**
36
+ * Find DITA-OT's `dita` command: `--dita-ot`, else `DITA_HOME`, else `dita` on the
37
+ * PATH (its installation is the folder above `bin`, links resolved).
38
+ */
39
+ export declare function findDita(ditaOt: string | undefined, env?: NodeJS.ProcessEnv): DitaLocation;
40
+ /** Refuse a DITA-OT without the dogsbay plugin, saying how to install it. */
41
+ export declare function checkPlugin(home: string): void;
42
+ /** This CLI's script, which DITA-OT's plugin runs for the conversion (`dogsbay.converter`). */
43
+ export declare function cliScript(argv?: readonly string[]): string | undefined;
44
+ /** The transtypes whose output folder is a dogsbay site project. */
45
+ export declare const DOGSBAY_TRANSTYPES: string[];
46
+ /** What a preview publishes: the map, where, with which filters and parameters, and what to watch. */
47
+ export interface PublishPlan {
48
+ /** The deliverable's name, or the map's file name. */
49
+ label: string;
50
+ map: string;
51
+ output: string;
52
+ filters: string[];
53
+ params: Record<string, string>;
54
+ watch: string[];
55
+ /** The deliverable, when one was used. */
56
+ deliverable?: DitaDeliverable;
57
+ }
58
+ /** `-p name=value` options as parameters. */
59
+ export declare function parseParams(params: readonly string[] | undefined): Record<string, string>;
60
+ /**
61
+ * Work out what to publish. With a map, the options say everything (output `site` by
62
+ * default). Otherwise a deliverable from the project files: the one `--deliverable`
63
+ * names, else the only one that publishes a dogsbay site. The deliverable gives the
64
+ * map, output folder, filters and parameters; `-o` and `--filter` replace its own, and
65
+ * `-p` overrides its parameters by name. The transtype is always `dogsbay`, so a
66
+ * deliverable of another transtype needs `-o`, or its output folder would be replaced.
67
+ */
68
+ export declare function planPublish(map: string | undefined, options: DitaDevOptions, cwd?: string): PublishPlan;
69
+ /** The `dita` arguments that publish a plan into its site project, with `dogsbay` whatever the deliverable's transtype. */
70
+ export declare function ditaArgs(plan: Pick<PublishPlan, "map" | "output" | "filters" | "params">, cli: string | undefined): string[];
71
+ /**
72
+ * Whether a change is not a source change: the site project's own files, version
73
+ * control, packages, DITA-OT's temp folder, editor backup and swap files.
74
+ */
75
+ export declare function isIgnored(path: string, output: string, roots?: readonly string[]): boolean;
76
+ /**
77
+ * Runs `publish` after changes settle for `delayMs`, never two at once: a change during
78
+ * a publish runs one more publish after it.
79
+ */
80
+ export declare function createRepublisher(publish: () => Promise<unknown>, delayMs: number): {
81
+ schedule: () => void;
82
+ idle: () => Promise<void>;
83
+ close: () => void;
84
+ };
85
+ /** A publish's outcome: whether the site project was written, and DITA-OT's error lines. */
86
+ export interface PublishResult {
87
+ ok: boolean;
88
+ errors: string[];
89
+ millis: number;
90
+ }
91
+ /** Run `dita` and collect its error lines; print everything with `verbose`. */
92
+ /**
93
+ * An argument for a .bat run through cmd.exe: quoted whole, so `&`, `|`, `<`, `>`, `^` and parentheses
94
+ * stay literal. cmd has no escape for `"` inside quotes and expands `%NAME%` even there, so an argument
95
+ * holding either is refused rather than passed on altered.
96
+ */
97
+ export declare function cmdArgument(arg: string): string;
98
+ export declare function runDita(dita: string, args: string[], cwd: string, verbose: boolean): Promise<{
99
+ code: number;
100
+ errors: string[];
101
+ }>;
102
+ /** The steps `ditaDev` runs, replaceable in tests. */
103
+ export interface DitaDevSteps {
104
+ publish: (args: string[]) => Promise<{
105
+ code: number;
106
+ errors: string[];
107
+ }>;
108
+ scaffold: (output: string) => Promise<void>;
109
+ serve: (output: string, options: SiteDevOptions) => Promise<void>;
110
+ watch: (paths: string[], ignored: (path: string) => boolean, onChange: () => void) => {
111
+ close: () => Promise<void> | void;
112
+ };
113
+ log: (line: string) => void;
114
+ }
115
+ /**
116
+ * Publish the map, serve the site with `site dev`, and publish again on every source
117
+ * change. Resolves when `site dev` ends (it exits the process when Astro stops).
118
+ */
119
+ export declare function ditaDev(map: string | undefined, options: DitaDevOptions, steps?: Partial<DitaDevSteps>): Promise<void>;
120
+ export declare function registerDitaDev(program: Command, fail: (error: unknown) => void): void;
121
+ /** Print a project's deliverables: name, transtype, map, filters, output and parameters. */
122
+ export declare function ditaDeliverables(options: {
123
+ project?: string;
124
+ json?: boolean;
125
+ }, cwd?: string, log?: (line: string) => void): void;
@@ -0,0 +1,450 @@
1
+ /**
2
+ * `dogsbay-dita dita dev <map>` — a live preview of a DITA map as a dogsbay site.
3
+ * (@dogsbay/dita-publish; the `dogsbay` CLI no longer has it.)
4
+ *
5
+ * 1. Publish the map with DITA-OT's `dita` command and the `dogsbay`
6
+ * transtype (plugin com.dogsbay.publish), converting with this CLI.
7
+ * 2. Scaffold the site project when it has none, then run `site dev` on it
8
+ * (package install on the first run, site build, content watcher,
9
+ * `astro dev`).
10
+ * 3. Watch the DITA sources and publish again after a change; `site dev`
11
+ * sees the changed Markdown, rebuilds and the page reloads.
12
+ *
13
+ * DogsBay XML runs the same loop with DITA-OT kept running in the editor
14
+ * (Project > Live Preview); here each publish starts Java, which adds a few
15
+ * seconds. Plan: plans/dita-dev-command.md.
16
+ */
17
+ import { spawn, spawnSync } from "node:child_process";
18
+ import { accessSync, constants, existsSync, realpathSync } from "node:fs";
19
+ import { basename, delimiter, dirname, join, relative, resolve, sep } from "node:path";
20
+ import chokidar from "chokidar";
21
+ import pc from "picocolors";
22
+ import { deliverableOutput, deliverableParams, findDeliverable, findProjectFiles, loadDeliverables, } from "../dita/project-file.js";
23
+ export const PLUGIN_ID = "com.dogsbay.publish";
24
+ const WINDOWS = process.platform === "win32";
25
+ /**
26
+ * Find DITA-OT's `dita` command: `--dita-ot`, else `DITA_HOME`, else `dita` on the
27
+ * PATH (its installation is the folder above `bin`, links resolved).
28
+ */
29
+ export function findDita(ditaOt, env = process.env) {
30
+ const script = WINDOWS ? "dita.bat" : "dita";
31
+ const home = ditaOt ?? env.DITA_HOME;
32
+ if (home) {
33
+ const dita = join(resolve(home), "bin", script);
34
+ if (!existsSync(dita)) {
35
+ throw new Error(`${resolve(home)} is not a DITA-OT installation (no bin/${script})`);
36
+ }
37
+ return { dita, home: resolve(home) };
38
+ }
39
+ const pathKey = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
40
+ for (const folder of (env[pathKey] ?? "").split(delimiter)) {
41
+ if (!folder)
42
+ continue;
43
+ const candidate = join(folder, script);
44
+ try {
45
+ accessSync(candidate, WINDOWS ? constants.F_OK : constants.X_OK);
46
+ }
47
+ catch {
48
+ continue;
49
+ }
50
+ const real = realpathSync(candidate);
51
+ return { dita: candidate, home: dirname(dirname(real)) };
52
+ }
53
+ throw new Error("DITA-OT was not found: pass --dita-ot <folder>, set DITA_HOME, or put DITA-OT's bin folder on the PATH");
54
+ }
55
+ /** Refuse a DITA-OT without the dogsbay plugin, saying how to install it. */
56
+ export function checkPlugin(home) {
57
+ if (!existsSync(join(home, "plugins", PLUGIN_ID))) {
58
+ throw new Error(`The DITA-OT at ${home} does not have the ${PLUGIN_ID} plugin. Install it with:\n` +
59
+ ` npx -y @dogsbay/dita-ot-plugin@beta install ${home}`);
60
+ }
61
+ }
62
+ /** This CLI's script, which DITA-OT's plugin runs for the conversion (`dogsbay.converter`). */
63
+ export function cliScript(argv = process.argv) {
64
+ const script = argv[1];
65
+ if (!script)
66
+ return undefined;
67
+ // A global install, npx and node_modules/.bin run the CLI through a link without an extension
68
+ let real = resolve(script);
69
+ try {
70
+ real = realpathSync(real);
71
+ }
72
+ catch {
73
+ // not a file on disk: judge by the name
74
+ }
75
+ return /\.[cm]?js$/.test(real) ? real : undefined;
76
+ }
77
+ /** The transtypes whose output folder is a dogsbay site project. */
78
+ export const DOGSBAY_TRANSTYPES = ["dogsbay", "dogsbay-site"];
79
+ /** `-p name=value` options as parameters. */
80
+ export function parseParams(params) {
81
+ const out = {};
82
+ for (const p of params ?? []) {
83
+ const eq = p.indexOf("=");
84
+ if (eq <= 0)
85
+ throw new Error(`--param needs name=value, not "${p}"`);
86
+ out[p.slice(0, eq)] = p.slice(eq + 1);
87
+ }
88
+ return out;
89
+ }
90
+ function listNames(deliverables) {
91
+ return deliverables.map((d) => `${d.name} (${d.transtype ?? "no transtype"})`).join(", ");
92
+ }
93
+ /**
94
+ * Work out what to publish. With a map, the options say everything (output `site` by
95
+ * default). Otherwise a deliverable from the project files: the one `--deliverable`
96
+ * names, else the only one that publishes a dogsbay site. The deliverable gives the
97
+ * map, output folder, filters and parameters; `-o` and `--filter` replace its own, and
98
+ * `-p` overrides its parameters by name. The transtype is always `dogsbay`, so a
99
+ * deliverable of another transtype needs `-o`, or its output folder would be replaced.
100
+ */
101
+ export function planPublish(map, options, cwd = process.cwd()) {
102
+ const cliParams = parseParams(options.param);
103
+ const cliFilters = (options.filter ?? []).map((f) => resolve(cwd, f));
104
+ const watchOption = (options.watch ?? []).map((w) => resolve(cwd, w));
105
+ if (map !== undefined) {
106
+ if (options.deliverable)
107
+ throw new Error("Give a map or --deliverable, not both.");
108
+ const mapPath = resolve(cwd, map);
109
+ if (!existsSync(mapPath))
110
+ throw new Error(`No such map: ${map}`);
111
+ return {
112
+ label: basename(mapPath),
113
+ map: mapPath,
114
+ output: resolve(cwd, options.output ?? "site"),
115
+ filters: cliFilters,
116
+ params: cliParams,
117
+ watch: watchOption.length ? watchOption : [dirname(mapPath)],
118
+ };
119
+ }
120
+ // The project folder is where the command runs, as it is the editor's open folder: --project only
121
+ // names the file, so a deliverable without an output publishes to the same out/<name> either way
122
+ const root = cwd;
123
+ if (!options.project && findProjectFiles(root).length === 0) {
124
+ throw new Error(`Give a map, or a deliverable from a DITA-OT project file: none found in ${root} (use --project <file>).`);
125
+ }
126
+ const deliverables = loadDeliverables(root, options.project ? resolve(cwd, options.project) : undefined);
127
+ let d;
128
+ if (options.deliverable) {
129
+ d = findDeliverable(deliverables, options.deliverable);
130
+ if (!d) {
131
+ throw new Error(`No deliverable named "${options.deliverable}". Deliverables: ${listNames(deliverables) || "none"}.`);
132
+ }
133
+ }
134
+ else {
135
+ const sites = deliverables.filter((x) => DOGSBAY_TRANSTYPES.includes(x.transtype ?? ""));
136
+ if (sites.length !== 1) {
137
+ throw new Error((sites.length === 0 ? "No deliverable publishes a dogsbay site" : "Several deliverables publish a dogsbay site") +
138
+ `; name one with --deliverable, or give a map. Deliverables: ${listNames(deliverables) || "none"}.`);
139
+ }
140
+ d = sites[0];
141
+ }
142
+ if (!DOGSBAY_TRANSTYPES.includes(d.transtype ?? "") && !options.output) {
143
+ throw new Error(`The deliverable "${d.name}" publishes ${d.transtype ?? "no transtype"} to ${deliverableOutput(d, root)}; ` +
144
+ "give -o <folder> for the preview's site project, so that output is not replaced.");
145
+ }
146
+ const watch = watchOption.length ? watchOption : [root];
147
+ const mapFolder = dirname(d.map);
148
+ if (!watchOption.length && !mapFolder.startsWith(root + sep) && mapFolder !== root)
149
+ watch.push(mapFolder);
150
+ return {
151
+ label: d.name,
152
+ map: d.map,
153
+ output: options.output ? resolve(cwd, options.output) : deliverableOutput(d, root),
154
+ filters: cliFilters.length ? cliFilters : d.ditavals,
155
+ params: { ...deliverableParams(d), ...cliParams },
156
+ watch,
157
+ deliverable: d,
158
+ };
159
+ }
160
+ /** The `dita` arguments that publish a plan into its site project, with `dogsbay` whatever the deliverable's transtype. */
161
+ export function ditaArgs(plan, cli) {
162
+ const args = ["-i", plan.map, "-f", "dogsbay", "-o", plan.output];
163
+ for (const filter of plan.filters)
164
+ args.push(`--filter=${filter}`);
165
+ // -D takes any property; --name=value only those a plugin declares, so a deliverable's own
166
+ // parameter would fail the publish with "unsupported argument"
167
+ for (const [name, value] of Object.entries(plan.params))
168
+ args.push(`-D${name}=${value}`);
169
+ if (cli && !("dogsbay.converter" in plan.params))
170
+ args.push(`-Ddogsbay.converter=${cli}`);
171
+ return args;
172
+ }
173
+ const IGNORED_FOLDERS = new Set([".git", ".svn", ".hg", "node_modules", ".dogsbay", ".astro", "temp"]);
174
+ /**
175
+ * Whether a change is not a source change: the site project's own files, version
176
+ * control, packages, DITA-OT's temp folder, editor backup and swap files.
177
+ */
178
+ export function isIgnored(path, output, roots = []) {
179
+ const abs = resolve(path);
180
+ const out = resolve(output);
181
+ if (abs === out || abs.startsWith(out + sep))
182
+ return true;
183
+ // Folder names count only inside a watched folder: a project under /home/u/temp must still be watched
184
+ const root = roots.map((r) => resolve(r)).find((r) => abs === r || abs.startsWith(r + sep));
185
+ const inside = root ? relative(root, abs) : abs;
186
+ if (inside.split(sep).some((part) => IGNORED_FOLDERS.has(part)))
187
+ return true;
188
+ const name = basename(abs);
189
+ return name.endsWith("~") || name.endsWith(".swp") || name.endsWith(".swo") || name.startsWith(".#") || name === ".DS_Store";
190
+ }
191
+ /**
192
+ * Runs `publish` after changes settle for `delayMs`, never two at once: a change during
193
+ * a publish runs one more publish after it.
194
+ */
195
+ export function createRepublisher(publish, delayMs) {
196
+ let timer;
197
+ let running;
198
+ let dirty = false;
199
+ let closed = false;
200
+ const run = async () => {
201
+ timer = undefined;
202
+ if (running || closed)
203
+ return;
204
+ while (dirty && !closed) {
205
+ dirty = false;
206
+ running = Promise.resolve(publish()).then(() => undefined, () => undefined);
207
+ await running;
208
+ running = undefined;
209
+ }
210
+ };
211
+ return {
212
+ schedule() {
213
+ if (closed)
214
+ return;
215
+ dirty = true;
216
+ if (timer)
217
+ clearTimeout(timer);
218
+ timer = setTimeout(() => void run(), delayMs);
219
+ },
220
+ async idle() {
221
+ while (timer || running) {
222
+ if (running)
223
+ await running;
224
+ else
225
+ await new Promise((r) => setTimeout(r, 10));
226
+ }
227
+ },
228
+ close() {
229
+ closed = true;
230
+ if (timer)
231
+ clearTimeout(timer);
232
+ timer = undefined;
233
+ },
234
+ };
235
+ }
236
+ const ERROR_LINE = /\[(ERROR|FATAL)\]|^Error:|BUILD FAILED/;
237
+ /** Run `dita` and collect its error lines; print everything with `verbose`. */
238
+ /**
239
+ * An argument for a .bat run through cmd.exe: quoted whole, so `&`, `|`, `<`, `>`, `^` and parentheses
240
+ * stay literal. cmd has no escape for `"` inside quotes and expands `%NAME%` even there, so an argument
241
+ * holding either is refused rather than passed on altered.
242
+ */
243
+ export function cmdArgument(arg) {
244
+ if (/["%]/.test(arg)) {
245
+ throw new Error(`Cannot pass ${JSON.stringify(arg)} to dita.bat: it contains " or %, which cmd.exe would change`);
246
+ }
247
+ return `"${arg}"`;
248
+ }
249
+ /** End a process and, on Windows, everything it started (cmd.exe runs dita.bat, which runs Java). */
250
+ function killTree(child) {
251
+ if (child.exitCode !== null || child.pid === undefined)
252
+ return;
253
+ if (WINDOWS) {
254
+ spawnSync("taskkill", ["/T", "/F", "/PID", String(child.pid)], { stdio: "ignore" });
255
+ }
256
+ else {
257
+ child.kill();
258
+ }
259
+ }
260
+ export function runDita(dita, args, cwd, verbose) {
261
+ return new Promise((resolvePromise) => {
262
+ let child;
263
+ try {
264
+ child = (WINDOWS
265
+ // Node runs a .bat only through the shell, which joins the arguments as they are
266
+ ? spawn(`"${dita}" ${args.map(cmdArgument).join(" ")}`, { cwd, shell: true })
267
+ : spawn(dita, args, { cwd }));
268
+ }
269
+ catch (err) {
270
+ resolvePromise({ code: 1, errors: [err.message] });
271
+ return;
272
+ }
273
+ // DITA-OT's Java process must not outlive this command: not on a normal exit, and not when the
274
+ // command is stopped by a signal before the dev server (which handles signals itself) is running
275
+ const killOnExit = () => killTree(child);
276
+ const onSignal = (signal) => {
277
+ killTree(child);
278
+ process.exit(signal === "SIGINT" ? 130 : 143);
279
+ };
280
+ process.once("exit", killOnExit);
281
+ process.once("SIGINT", onSignal);
282
+ process.once("SIGTERM", onSignal);
283
+ const errors = [];
284
+ let pending = "";
285
+ const onData = (chunk) => {
286
+ pending += chunk.toString("utf-8");
287
+ const lines = pending.split(/\r?\n/);
288
+ pending = lines.pop() ?? "";
289
+ for (const line of lines) {
290
+ if (verbose)
291
+ console.log(pc.dim(`[dita] ${line}`));
292
+ if (ERROR_LINE.test(line))
293
+ errors.push(line.trim());
294
+ }
295
+ };
296
+ child.stdout.on("data", onData);
297
+ child.stderr.on("data", onData);
298
+ child.on("error", (err) => resolvePromise({ code: 1, errors: [`Could not run ${dita}: ${err.message}`] }));
299
+ child.on("close", (code) => {
300
+ process.off("exit", killOnExit);
301
+ process.off("SIGINT", onSignal);
302
+ process.off("SIGTERM", onSignal);
303
+ if (pending && ERROR_LINE.test(pending))
304
+ errors.push(pending.trim());
305
+ resolvePromise({ code: code ?? 1, errors });
306
+ });
307
+ });
308
+ }
309
+ function defaultSteps(location, cwd, verbose) {
310
+ return {
311
+ publish: (args) => location ? runDita(location.dita, args, cwd, verbose) : Promise.reject(new Error("DITA-OT was not located")),
312
+ scaffold: async (output) => (await import("dogsbay/lib")).siteInit(output, { yes: true, scaffoldOnly: true }),
313
+ serve: async (output, options) => (await import("dogsbay/lib")).siteDev(output, options),
314
+ watch: (paths, ignored, onChange) => {
315
+ const watcher = chokidar.watch(paths, {
316
+ ignoreInitial: true,
317
+ ignored: (path) => ignored(path),
318
+ awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 },
319
+ });
320
+ watcher.on("all", () => onChange());
321
+ return watcher;
322
+ },
323
+ log: (line) => console.log(line),
324
+ };
325
+ }
326
+ /**
327
+ * Publish the map, serve the site with `site dev`, and publish again on every source
328
+ * change. Resolves when `site dev` ends (it exits the process when Astro stops).
329
+ */
330
+ export async function ditaDev(map, options, steps) {
331
+ const cwd = process.cwd();
332
+ const plan = planPublish(map, options, cwd);
333
+ const output = plan.output;
334
+ // A replaced publish step (tests) needs no DITA-OT
335
+ const location = steps?.publish ? undefined : findDita(options.ditaOt);
336
+ if (location)
337
+ checkPlugin(location.home);
338
+ const all = { ...defaultSteps(location, cwd, !!options.verbose), ...steps };
339
+ const args = ditaArgs(plan, cliScript());
340
+ const shown = relative(cwd, output) || ".";
341
+ const publish = async () => {
342
+ const started = Date.now();
343
+ const { code, errors } = await all.publish(args);
344
+ const ok = code === 0 && existsSync(join(output, "content", "nav.yml"));
345
+ const millis = Date.now() - started;
346
+ for (const line of errors)
347
+ all.log(pc.red(`[dita] ${line}`));
348
+ if (ok) {
349
+ all.log(pc.green(`[dita] published ${plan.label} to ${shown} in ${(millis / 1000).toFixed(1)} s`) +
350
+ (errors.length ? pc.yellow(` with ${errors.length} error(s)`) : ""));
351
+ }
352
+ else {
353
+ all.log(pc.red(`[dita] publishing failed (exit ${code})`) +
354
+ (options.verbose ? "" : pc.dim("; --verbose shows DITA-OT's output")));
355
+ }
356
+ return { ok, errors, millis };
357
+ };
358
+ if (plan.deliverable) {
359
+ all.log(pc.cyan(`[dita] deliverable ${plan.deliverable.name}: ${relative(cwd, plan.map)}` +
360
+ (plan.filters.length ? `, filters ${plan.filters.map((f) => relative(cwd, f)).join(", ")}` : "") +
361
+ (Object.keys(plan.params).length ? `, ${Object.keys(plan.params).length} parameter(s)` : "")));
362
+ }
363
+ all.log(pc.cyan(`[dita] publishing ${plan.label} with DITA-OT...`));
364
+ const first = await publish();
365
+ if (!first.ok)
366
+ throw new Error("The first publish failed; nothing to serve.");
367
+ if (!existsSync(join(output, "astro", "package.json"))) {
368
+ all.log(pc.cyan("[dita] scaffolding the site"));
369
+ await all.scaffold(output);
370
+ }
371
+ const watched = plan.watch;
372
+ const republisher = createRepublisher(async () => {
373
+ all.log(pc.cyan("[dita] source changed — publishing..."));
374
+ await publish();
375
+ }, 500);
376
+ const watcher = all.watch(watched, (path) => isIgnored(path, output, watched), () => republisher.schedule());
377
+ all.log(pc.cyan(`[dita] watching ${watched.map((p) => relative(cwd, p) || ".").join(", ")} for DITA changes`));
378
+ try {
379
+ await all.serve(output, { host: options.hostAddress ?? options.host });
380
+ }
381
+ finally {
382
+ republisher.close();
383
+ await watcher.close();
384
+ }
385
+ }
386
+ export function registerDitaDev(program, fail) {
387
+ const dita = program.command("dita").description("Work with DITA sources published through DITA-OT");
388
+ dita
389
+ .command("dev")
390
+ .description("Live preview of a DITA map: publish it with DITA-OT (plugin com.dogsbay.publish), serve the site with site dev, and publish again when a source changes")
391
+ .argument("[map]", "The DITA map (or topic) to publish; omit to publish a deliverable")
392
+ .option("-d, --deliverable <name>", "Publish a deliverable from the project file: its map, filters, parameters and output folder")
393
+ .option("--project <file>", "DITA-OT project file (default: project files in the current folder, as DogsBay XML finds them)")
394
+ .option("-o, --output <dir>", "Site project folder (default: the deliverable's output, else site)")
395
+ .option("--dita-ot <dir>", "DITA-OT installation (default: DITA_HOME, else dita on the PATH)")
396
+ .option("--filter <ditaval>", "DITAVAL file (repeatable); replaces the deliverable's filters", (v, prev = []) => [...prev, v])
397
+ .option("-p, --param <name=value>", "DITA-OT or plugin parameter, such as args.gen.task.lbl=YES (repeatable); overrides the deliverable's", (v, prev = []) => [...prev, v])
398
+ .option("--watch <path>", "Folder or file to watch for changes (repeatable; default: the project folder, or the map's folder)", (v, prev = []) => [...prev, v])
399
+ .option("--host", "Listen on all addresses, for a container or remote machine")
400
+ .option("--host-address <address>", "Listen on this address")
401
+ .option("--verbose", "Print all of DITA-OT's output")
402
+ .action(async (map, options) => {
403
+ await ditaDev(map, options).catch(fail);
404
+ });
405
+ dita
406
+ .command("deliverables")
407
+ .description("List the deliverables in DITA-OT project files, as dita dev --deliverable uses them")
408
+ .option("--project <file>", "DITA-OT project file (default: project files in the current folder)")
409
+ .option("--json", "Print JSON")
410
+ .action((options) => {
411
+ try {
412
+ ditaDeliverables(options);
413
+ }
414
+ catch (err) {
415
+ fail(err);
416
+ }
417
+ });
418
+ }
419
+ /** Print a project's deliverables: name, transtype, map, filters, output and parameters. */
420
+ export function ditaDeliverables(options, cwd = process.cwd(), log = console.log) {
421
+ const root = cwd;
422
+ const deliverables = loadDeliverables(root, options.project ? resolve(cwd, options.project) : undefined);
423
+ const rows = deliverables.map((d) => ({
424
+ name: d.name,
425
+ transtype: d.transtype ?? null,
426
+ map: relative(cwd, d.map),
427
+ filters: d.ditavals.map((f) => relative(cwd, f)),
428
+ output: relative(cwd, deliverableOutput(d, root)),
429
+ params: deliverableParams(d),
430
+ site: DOGSBAY_TRANSTYPES.includes(d.transtype ?? ""),
431
+ projectFile: relative(cwd, d.sourceFile),
432
+ }));
433
+ if (options.json) {
434
+ log(JSON.stringify({ deliverables: rows }, null, 2));
435
+ return;
436
+ }
437
+ if (rows.length === 0) {
438
+ log(`No deliverables in ${options.project ?? "the project files in " + root}.`);
439
+ return;
440
+ }
441
+ for (const r of rows) {
442
+ log(`${pc.bold(r.name)} ${r.transtype ?? "(no transtype)"}${r.site ? pc.green(" dogsbay site") : ""}`);
443
+ log(` map ${r.map}`);
444
+ if (r.filters.length)
445
+ log(` filters ${r.filters.join(", ")}`);
446
+ log(` output ${r.output}`);
447
+ for (const [name, value] of Object.entries(r.params))
448
+ log(` param ${name}=${value}`);
449
+ }
450
+ }