From 13e391b644101bc9c8be3aada4981f15864a3236 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 15 Feb 2024 20:25:12 +0100 Subject: [PATCH] feat: support watcher --- docs/src/index.ts | 12 ++++ package.json | 1 + pnpm-lock.yaml | 145 ++++++++++++++++++++++++++++++++++++++++++++++ src/automd.ts | 87 +++++++++++++++++++++++----- src/cli.ts | 76 +++++++++++++++++------- src/config.ts | 14 +++++ src/transform.ts | 5 +- 7 files changed, 304 insertions(+), 36 deletions(-) create mode 100644 docs/src/index.ts diff --git a/docs/src/index.ts b/docs/src/index.ts new file mode 100644 index 0000000..31a1c4d --- /dev/null +++ b/docs/src/index.ts @@ -0,0 +1,12 @@ +/** + * Adds two numbers together. + * + * @example + * + * ```js + * add(1, 2); // 3 + * ``` + */ +export function add(a: number, b: number) { + return a + b; +} diff --git a/package.json b/package.json index ddb8f4f..52b0ad7 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "test:types": "tsc --noEmit --skipLibCheck" }, "dependencies": { + "@parcel/watcher": "^2.4.0", "c12": "^1.7.0", "citty": "^0.1.5", "consola": "^3.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd0dc76..6f3f379 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@parcel/watcher': + specifier: ^2.4.0 + version: 2.4.0 c12: specifier: ^1.7.0 version: 1.7.0 @@ -596,6 +599,137 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + /@parcel/watcher-android-arm64@2.4.0: + resolution: {integrity: sha512-+fPtO/GsbYX1LJnCYCaDVT3EOBjvSFdQN9Mrzh9zWAOOfvidPWyScTrHIZHHfJBvlHzNA0Gy0U3NXFA/M7PHUA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@parcel/watcher-darwin-arm64@2.4.0: + resolution: {integrity: sha512-T/At5pansFuQ8VJLRx0C6C87cgfqIYhW2N/kBfLCUvDhCah0EnLLwaD/6MW3ux+rpgkpQAnMELOCTKlbwncwiA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@parcel/watcher-darwin-x64@2.4.0: + resolution: {integrity: sha512-vZMv9jl+szz5YLsSqEGCMSllBl1gU1snfbRL5ysJU03MEa6gkVy9OMcvXV1j4g0++jHEcvzhs3Z3LpeEbVmY6Q==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@parcel/watcher-freebsd-x64@2.4.0: + resolution: {integrity: sha512-dHTRMIplPDT1M0+BkXjtMN+qLtqq24sLDUhmU+UxxLP2TEY2k8GIoqIJiVrGWGomdWsy5IO27aDV1vWyQ6gfHA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@parcel/watcher-linux-arm-glibc@2.4.0: + resolution: {integrity: sha512-9NQXD+qk46RwATNC3/UB7HWurscY18CnAPMTFcI9Y8CTbtm63/eex1SNt+BHFinEQuLBjaZwR2Lp+n7pmEJPpQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@parcel/watcher-linux-arm64-glibc@2.4.0: + resolution: {integrity: sha512-QuJTAQdsd7PFW9jNGaV9Pw+ZMWV9wKThEzzlY3Lhnnwy7iW23qtQFPql8iEaSFMCVI5StNNmONUopk+MFKpiKg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@parcel/watcher-linux-arm64-musl@2.4.0: + resolution: {integrity: sha512-oyN+uA9xcTDo/45bwsd6TFHa7Lc7hKujyMlvwrCLvSckvWogndCEoVYFNfZ6JJ2KNL/6fFiGPcbjp8jJmEh5Ng==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@parcel/watcher-linux-x64-glibc@2.4.0: + resolution: {integrity: sha512-KphV8awJmxU3q52JQvJot0QMu07CIyEjV+2Tb2ZtbucEgqyRcxOBDMsqp1JNq5nuDXtcCC0uHQICeiEz38dPBQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@parcel/watcher-linux-x64-musl@2.4.0: + resolution: {integrity: sha512-7jzcOonpXNWcSijPpKD5IbC6xC7yTibjJw9jviVzZostYLGxbz8LDJLUnLzLzhASPlPGgpeKLtFUMjAAzM+gSA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@parcel/watcher-win32-arm64@2.4.0: + resolution: {integrity: sha512-NOej2lqlq8bQNYhUMnOD0nwvNql8ToQF+1Zhi9ULZoG+XTtJ9hNnCFfyICxoZLXor4bBPTOnzs/aVVoefYnjIg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@parcel/watcher-win32-ia32@2.4.0: + resolution: {integrity: sha512-IO/nM+K2YD/iwjWAfHFMBPz4Zqn6qBDqZxY4j2n9s+4+OuTSRM/y/irksnuqcspom5DjkSeF9d0YbO+qpys+JA==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@parcel/watcher-win32-x64@2.4.0: + resolution: {integrity: sha512-pAUyUVjfFjWaf/pShmJpJmNxZhbMvJASUpdes9jL6bTEJ+gDxPRSpXTIemNyNsb9AtbiGXs9XduP1reThmd+dA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@parcel/watcher@2.4.0: + resolution: {integrity: sha512-XJLGVL0DEclX5pcWa2N9SX1jCGTDd8l972biNooLFtjneuGqodupPQh6XseXIBBeVIMaaJ7bTcs3qGvXwsp4vg==} + engines: {node: '>= 10.0.0'} + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.5 + node-addon-api: 7.1.0 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.4.0 + '@parcel/watcher-darwin-arm64': 2.4.0 + '@parcel/watcher-darwin-x64': 2.4.0 + '@parcel/watcher-freebsd-x64': 2.4.0 + '@parcel/watcher-linux-arm-glibc': 2.4.0 + '@parcel/watcher-linux-arm64-glibc': 2.4.0 + '@parcel/watcher-linux-arm64-musl': 2.4.0 + '@parcel/watcher-linux-x64-glibc': 2.4.0 + '@parcel/watcher-linux-x64-musl': 2.4.0 + '@parcel/watcher-win32-arm64': 2.4.0 + '@parcel/watcher-win32-ia32': 2.4.0 + '@parcel/watcher-win32-x64': 2.4.0 + dev: false + /@rollup/plugin-alias@5.1.0(rollup@3.29.4): resolution: {integrity: sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==} engines: {node: '>=14.0.0'} @@ -1674,6 +1808,12 @@ packages: /destr@2.0.2: resolution: {integrity: sha512-65AlobnZMiCET00KaFFjUefxDX0khFA/E4myqZ7a6Sq1yZtR8+FVIvilVX66vF2uobSumxooYZChiRPCKNqhmg==} + /detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + dev: false + /didyoumean2@6.0.1: resolution: {integrity: sha512-PSy0zQwMg5O+LjT5Mz7vnKC8I7DfWLPF6M7oepqW7WP5mn2CY3hz46xZOa1GJY+KVfyXhdmz6+tdgXwrHlZc5g==} engines: {node: ^16.14.0 || >=18.12.0} @@ -3229,6 +3369,11 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /node-addon-api@7.1.0: + resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==} + engines: {node: ^16 || ^18 || >= 20} + dev: false + /node-fetch-native@1.6.1: resolution: {integrity: sha512-bW9T/uJDPAJB2YNYEpWzE54U5O3MQidXsOyTfnbKYtTtFexRvGzb1waphBN4ZwP6EcIvYYEOwW0b72BpAqydTw==} diff --git a/src/automd.ts b/src/automd.ts index b7d65e4..2e6d3d3 100644 --- a/src/automd.ts +++ b/src/automd.ts @@ -1,16 +1,18 @@ import { existsSync, promises as fsp } from "node:fs"; import { resolve, relative } from "pathe"; +import type { SubscribeCallback } from "@parcel/watcher"; import type { Config, ResolvedConfig } from "./config"; -import { TransformResult, transform } from "./transform"; +import { type TransformResult, transform } from "./transform"; import { loadConfig } from "./config"; export interface AutomdResult extends TransformResult { - _config: ResolvedConfig; input: string; output: string; } -export async function automd(_config: Config = {}): Promise { +export async function automd( + _config: Config = {}, +): Promise<{ results: AutomdResult[]; _config: ResolvedConfig; time: number }> { const config = await loadConfig(_config.dir, _config); let inputFiles = config.input; @@ -20,7 +22,7 @@ export async function automd(_config: Config = {}): Promise { cwd: config.dir, absolute: false, onlyFiles: true, - ignore: ["node_modules", "dist", ".*", ...(config.ignore || [])], + ignore: config.ignore, }); } else { inputFiles = inputFiles @@ -28,32 +30,89 @@ export async function automd(_config: Config = {}): Promise { .filter((i) => existsSync(i)) .map((i) => relative(config.dir, i)); } + const multiFiles = inputFiles.length > 1; - return Promise.all( - inputFiles.map((i) => _automd(i, config, inputFiles.length > 1)), + const cache: ResultCache = new Map(); + + const start = performance.now(); + const results = await Promise.all( + inputFiles.map((i) => _automd(i, config, multiFiles, cache)), ); + const time = Math.round((performance.now() - start) * 1000) / 1000; + + if (config.watch) { + await _watch(inputFiles, config, multiFiles, cache); + } + + return { + _config: config, + time, + results, + }; } -export async function _automd( +// -- internal -- + +type ResultCache = Map; + +async function _automd( relativeInput: string, config: ResolvedConfig, - multi: boolean, + multiFiles: boolean, + cache: ResultCache, ): Promise { const input = resolve(config.dir, relativeInput); const contents = await fsp.readFile(input, "utf8"); - const result = await transform(contents, config); + const cachedResult = await cache.get(input); + if (cachedResult?.contents === contents) { + return cachedResult; + } + + const transformResult = await transform(contents, config); - const output = multi + const output = multiFiles ? resolve(config.dir, config.output || ".", relativeInput) : resolve(config.dir, config.output || relativeInput); - await fsp.writeFile(output, result.contents, "utf8"); + await fsp.writeFile(output, transformResult.contents, "utf8"); - return { - _config: config, + const result: AutomdResult = { input, output, - ...result, + ...transformResult, + }; + cache.set(input, result); + return result; +} + +async function _watch( + inputFiles: string[], + config: ResolvedConfig, + multiFiles: boolean, + cache: ResultCache, +) { + const watcher = await import("@parcel/watcher"); + + const watchCb: SubscribeCallback = async (_err, events) => { + const filesToUpdate = events + .map((e) => relative(config.dir, e.path)) + .filter((p) => inputFiles.includes(p)); + const start = performance.now(); + const results = await Promise.all( + filesToUpdate.map((f) => _automd(f, config, multiFiles, cache)), + ); + const time = performance.now() - start; + if (config.onWatch) { + config.onWatch({ results, time }); + } }; + + const subscription = await watcher.subscribe(config.dir, watchCb, { + ignore: config.ignore, + }); + + process.on("SIGINT", () => { + subscription.unsubscribe(); + }); } diff --git a/src/cli.ts b/src/cli.ts index af48060..73447d2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,9 +3,10 @@ import { relative } from "pathe"; import { defineCommand, runMain } from "citty"; import consola from "consola"; -import { getColor } from "consola/utils"; +import { colorize } from "consola/utils"; import { name, description, version } from "../package.json"; import { AutomdResult, automd } from "./automd"; +import { ResolvedConfig } from "./config"; const main = defineCommand({ meta: { @@ -27,53 +28,82 @@ const main = defineCommand({ description: "name or path the markdown output (defaults to input)", type: "string", }, + watch: { + description: "watch for changes in input files and regenerate output", + type: "boolean", + }, }, async setup({ args }) { - const results = await automd({ + const { + results, + _config: config, + time, + } = await automd({ dir: args.dir, input: args.input, output: args.output, + watch: args.watch, + onWatch: (event) => { + console.clear(); + _printResults(event.results, event.time, config); + }, }); + if (args.watch) { + console.clear(); + consola.info(`Watching for changes in \`${args.input}\``); + } if (results.length === 0) { consola.warn(`No files processed!`); process.exit(1); } - consola.success( - `Automd updated in \`${relative(process.cwd(), results[0]._config.dir)}\``, - ); - _printResults(results); + + _printResults(results, time, config); }, }); runMain(main); -// --- internal utils --- +// -- internal -- const _types = { - updated: { label: "updated", color: getColor("blue") }, - noChanges: { label: "no changes", color: getColor("green") }, - alreadyUpdate: { label: "already up-to-date", color: getColor("gray") }, - issues: { label: "with issues", color: getColor("yellow") }, -}; + updated: { label: "updated", color: "blue" }, + noChanges: { label: "no changes", color: "green" }, + alreadyUpdate: { label: "already up-to-date", color: "gray" }, + issues: { label: "with issues", color: "yellow" }, +} as const; -function _printResults(results: AutomdResult[]) { +function _printResults( + results: AutomdResult[], + time: number, + config: ResolvedConfig, +) { + const rDir = relative(process.cwd(), config.dir); + consola.success( + `Automd updated${rDir ? ` in \`${rDir}\` dir` : ""} ${_formatTime(time)}\n`, + ); for (const res of results) { const type = _getChangeType(res); - const input = relative(res._config.dir, res.input); - const output = relative(res._config.dir, res.output); + const input = relative(config.dir, res.input); + const output = relative(config.dir, res.output); const name = `${input === output ? ` ${input}` : ` ${input} ~> ${output}`}`; - consola.log(type.color(` ─ ${name} ${type.label}`)); + consola.log( + colorize( + type.color, + ` ─ ${name} ${type.label} ${_formatTime(res.time)}`, + ), + ); } const issues = results .filter((res) => res.hasIssues) - .map((res) => _formatIssues(res)); + .map((res) => _formatIssues(res, config)); if (issues.length > 0) { - consola.warn(`Some issues happened during update:`); + consola.warn(`Some issues happened during automd update:`); for (const issue of issues) { - consola.error(issue); + consola.log(issue); } } + consola.log(""); } function _getChangeType(res: AutomdResult) { @@ -86,6 +116,10 @@ function _getChangeType(res: AutomdResult) { return res.hasChanged ? _types.updated : _types.noChanges; } -function _formatIssues(res: AutomdResult) { - return `${_types.issues.color(relative(res._config.dir, res.input))} \n\n ${res.updates.flatMap((u) => u.result.issues).join("\n")}`; +function _formatIssues(res: AutomdResult, config: ResolvedConfig) { + return `${colorize(_types.issues.color, relative(config.dir, res.input))} \n\n ${res.updates.flatMap((u) => u.result.issues).join("\n")}`; +} + +function _formatTime(time: number) { + return colorize("gray", `(${Math.round(time * 100) / 100 + "ms"})`); } diff --git a/src/config.ts b/src/config.ts index 741d528..8012bed 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,6 @@ import { resolve } from "pathe"; import type { Generator } from "./generator"; +import type { AutomdResult } from "./automd"; export interface Config { /** @@ -30,6 +31,16 @@ export interface Config { */ ignore?: string[]; + /** + * Watch for changes in input files and regenerate output + */ + watch?: boolean; + + /** + * Watch callback + */ + onWatch?: (event: { results: AutomdResult[]; time: number }) => void; + /** Custom generators */ generators?: Record; } @@ -79,6 +90,9 @@ export async function loadConfig( name: "automd", dotenv: true, overrides, + defaults: { + ignore: ["node_modules", "dist", ".*"], + }, }); return resolveConfig(config as Config); diff --git a/src/transform.ts b/src/transform.ts index 1ac6f93..16282a8 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -9,12 +9,14 @@ export interface TransformResult { hasIssues: boolean; contents: string; updates: { block: Block; result: GenerateResult }[]; + time: number; } export async function transform( contents: string, _config?: Config, ): Promise { + const start = performance.now(); const config = resolveConfig(_config); const editor = new MagicString(contents); @@ -40,12 +42,13 @@ export async function transform( const hasChanged = editor.hasChanged(); const hasIssues = updates.some((u) => u.result.issues?.length); - + const time = performance.now() - start; return { hasChanged, hasIssues, contents: hasChanged ? editor.toString() : contents, updates, + time, }; }