Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new listr function with add capability #144

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 14 additions & 87 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,30 @@

// --
// Packages

import arg from 'arg';
import chalk from 'chalk';
import { watch, WatchOptions } from 'chokidar';
import getPort from 'get-port';
import getStdin from 'get-stdin';
import Listr from 'listr';
import path from 'path';
import { PackageJson } from '.';
import { Config, defaultConfig } from './lib/config';
import handleListr from './cli/handleListr';
import handleStdin from './cli/handleStdin';
import { CliArgs, cliFlags, Config, defaultConfig } from './lib/config';
import { closeBrowser } from './lib/generate-output';
import { help } from './lib/help';
import { setProcessAndTermTitle } from './lib/helpers';
import { convertMdToPdf } from './lib/md-to-pdf';
import { closeServer, serveDirectory } from './lib/serve-dir';
import { validateNodeVersion } from './lib/validate-node-version';

// --
// Configure CLI Arguments

export const cliFlags = arg({
'--help': Boolean,
'--version': Boolean,
'--basedir': String,
'--watch': Boolean,
'--watch-options': String,
'--stylesheet': [String],
'--css': String,
'--document-title': String,
'--body-class': [String],
'--page-media-type': String,
'--highlight-style': String,
'--marked-options': String,
'--html-pdf-options': String,
'--pdf-options': String,
'--launch-options': String,
'--gray-matter-options': String,
'--port': Number,
'--md-file-encoding': String,
'--stylesheet-encoding': String,
'--as-html': Boolean,
'--config-file': String,
'--devtools': Boolean,

// aliases
'-h': '--help',
'-v': '--version',
'-w': '--watch',
});

// --
// Run

main(cliFlags, defaultConfig).catch((error) => {
console.error(error);
process.exit(1);
});

// --
// Define Main Function

async function main(args: typeof cliFlags, config: Config) {
async function main(args: CliArgs, config: Config) {
setProcessAndTermTitle('md-to-pdf');

if (!validateNodeVersion()) {
Expand Down Expand Up @@ -109,6 +71,10 @@ async function main(args: typeof cliFlags, config: Config) {
}
}

if (args['--watch-timeout']) {
config.watch_timeout = args['--watch-timeout'];
}

/**
* 3. Start the file server.
*/
Expand All @@ -126,51 +92,12 @@ async function main(args: typeof cliFlags, config: Config) {
*/

if (stdin) {
await convertMdToPdf({ content: stdin }, config, args)
.finally(async () => {
await closeBrowser();
await closeServer(server);
})
.catch((error: Error) => {
throw error;
});

return;
await handleStdin(stdin, config, args);
} else {
await handleListr(files, config, args);
}

const getListrTask = (file: string) => ({
title: `generating ${args['--as-html'] ? 'HTML' : 'PDF'} from ${chalk.underline(file)}`,
task: async () => convertMdToPdf({ path: file }, config, args),
});

await new Listr(files.map(getListrTask), { concurrent: true, exitOnError: false })
.run()
.then(async () => {
if (args['--watch']) {
console.log(chalk.bgBlue('\n watching for changes \n'));

const watchOptions = args['--watch-options']
? (JSON.parse(args['--watch-options']) as WatchOptions)
: config.watch_options;

watch(files, watchOptions).on('change', async (file) =>
new Listr([getListrTask(file)], { exitOnError: false }).run().catch(console.error),
);
} else {
await closeBrowser();
await closeServer(server);
}
})
.catch((error: Error) => {
/**
* In watch mode the error needs to be shown immediately because the `main` function's catch handler will never execute.
*
* @todo is this correct or does `main` actually finish and the process is just kept alive because of the file server?
*/
if (args['--watch']) {
return console.error(error);
}

throw error;
});
console.log('Exit.');
closeServer(server);
closeBrowser();
}
136 changes: 136 additions & 0 deletions src/cli/handleListr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import chalk from 'chalk';
import { FSWatcher, watch, WatchOptions } from 'chokidar';
import Listr from 'listr';
import { CliArgs, Config } from '../lib/config';
import { isMdFile } from '../lib/is-md-file';
import { convertMdToPdf } from '../lib/md-to-pdf';

type OutputFileType = 'HTML' | 'PDF';

export default async function handleListr(watchPath: string[], config: Config, args: CliArgs): Promise<void> {
const withWatch = args['--watch'];
const watchOptions = args['--watch-options']
? (JSON.parse(args['--watch-options']) as WatchOptions)
: config.watch_options;
const outputType: OutputFileType = args['--as-html'] ? 'HTML' : 'PDF';

const watcher = watch(watchPath, watchOptions);
if (watchPath.length > 1) {
await legacyListrHandler(watchPath, outputType, watcher, config, args, withWatch);
} else {
await handleSinglePath(watcher, outputType, config, args, withWatch);
}
await watcher.close();
return;
}

const createListrTask = (file: string, outputType: OutputFileType, config: Config, args: CliArgs): Listr.ListrTask => ({
title: `generating ${outputType} from ${chalk.underline(file)}`,
task: () => convertMdToPdf({ path: file }, config, args),
});

const createAndRunListr = async (pathSet: Set<string>, outputType: OutputFileType, config: Config, args: CliArgs) => {
try {
await new Listr(
Array.from(pathSet).map((path) => createListrTask(path, outputType, config, args)),
{ concurrent: true, exitOnError: false },
).run();
} catch (error) {
console.error(error);
}
};

const handleSinglePath = async (
watcher: FSWatcher,
outputType: OutputFileType,
config: Config,
args: CliArgs,
withWatch = false,
) => {
/*
* Run initial watch and listen for 'add' events.
*/
await new Promise<void>(async (resolve) => {
const pathSet: Set<string> = new Set<string>();
watcher.on('add', (path) => {
if (!isMdFile(path)) return;
pathSet.add(path);
});
watcher.on('ready', async () => {
await createAndRunListr(pathSet, outputType, config, args);
resolve();
});
});

if (!withWatch) {
return;
}

/*
* Setup for running with --watch
*/
const pathSet: Set<string> = new Set<string>();
const timeOut = setTimeout(() => {
createAndRunListr(pathSet, outputType, config, args);
pathSet.clear();
}, config.watch_timeout);

const addOrChangeCallback = (path: string) => {
if (!isMdFile(path)) return;

pathSet.add(path);
timeOut.refresh();
};

console.log(chalk.bgBlue(`\n watching for changes with ${config.watch_timeout} ms watch_timeout \n`));
watcher.removeAllListeners();
watcher
.on('add', addOrChangeCallback)
.on('change', addOrChangeCallback)
.on('error', (error) => console.error(error));

/*
* Keep this function open, until the programm gets exit code.
*/
return new Promise<void>(() => null);
};

const legacyListrHandler = async (
files: string[],
outputType: OutputFileType,
watcher: FSWatcher,
config: Config,
args: CliArgs,
withWatch = false,
) => {
return await new Listr(
files.map((path) => createListrTask(path, outputType, config, args)),
{ concurrent: true, exitOnError: false },
)
.run()
.then(async () => {
if (withWatch) {
console.log(chalk.bgBlue('\n watching for changes \n'));

watcher.on('change', async (file) =>
new Listr([createListrTask(file, outputType, config, args)], { exitOnError: false })
.run()
.catch(console.error),
);

return new Promise<void>(() => null);
}
})
.catch((error: Error) => {
/**
* In watch mode the error needs to be shown immediately because the `main` function's catch handler will never execute.
*
* @todo is this correct or does `main` actually finish and the process is just kept alive because of the file server?
*/
if (withWatch) {
return console.error(error);
}

throw error;
});
};
8 changes: 8 additions & 0 deletions src/cli/handleStdin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { CliArgs, Config } from '../lib/config';
import { convertMdToPdf } from '../lib/md-to-pdf';

export default async function handleStdin(stdin: string, config: Config, args: CliArgs) {
await convertMdToPdf({ content: stdin }, config, args).catch((error: Error) => {
throw error;
});
}
53 changes: 53 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,51 @@
import arg from 'arg';
import { WatchOptions } from 'chokidar';
import { GrayMatterOption } from 'gray-matter';
import { marked } from 'marked';
import { resolve } from 'path';
import { FrameAddScriptTagOptions, launch, PDFOptions } from 'puppeteer';

// --
// Configure CLI Arguments
export const cliFlags = arg({
'--help': Boolean,
'--version': Boolean,
'--basedir': String,
'--watch': Boolean,
'--watch-options': String,
'--watch-timeout': Number,
'--stylesheet': [String],
'--css': String,
'--document-title': String,
'--body-class': [String],
'--page-media-type': String,
'--highlight-style': String,
'--marked-options': String,
'--html-pdf-options': String,
'--pdf-options': String,
'--launch-options': String,
'--gray-matter-options': String,
'--port': Number,
'--md-file-encoding': String,
'--stylesheet-encoding': String,
'--as-html': Boolean,
'--config-file': String,
'--devtools': Boolean,

// aliases
'-h': '--help',
'-v': '--version',
'-w': '--watch',
});

/**
* Possible cliFlags
*/
export type CliArgs = typeof cliFlags;

/*
* default Config that can be overwritten.
*/
export const defaultConfig: Config = {
basedir: process.cwd(),
stylesheet: [resolve(__dirname, '..', '..', 'markdown.css')],
Expand Down Expand Up @@ -38,6 +80,7 @@ export const defaultConfig: Config = {
as_html: false,
devtools: false,
marked_extensions: [],
watch_timeout: 250,
};

/**
Expand Down Expand Up @@ -173,6 +216,16 @@ interface BasicConfig {
* @see https://marked.js.org/using_pro#extensions
*/
marked_extensions: marked.MarkedExtension[];

/**
* Timeout (in ms) for delaying the change and add handler in watch mode.
* Adjusts the timeout of the watcher events. If multiple 'add' or 'change' events are fired, before the timout exceeds,
* the timeout gets resetted with each event.
* The watcher collects all events, reduces them to unique events, and passes the tasks to the converter after timeout.
* Shorter timeout could mean higher CPU and Memory load on big amount of events.
* Default: 250 (ms)
*/
watch_timeout: number;
}

export type PuppeteerLaunchOptions = Parameters<typeof launch>[0];
Loading