From a13334ceba850bfd50818d66551877867b86d655 Mon Sep 17 00:00:00 2001 From: Jeroen Visser Date: Mon, 30 Aug 2021 16:05:20 +0200 Subject: [PATCH] feat: Add debounce option to compile and extract CLI (#1101) --- docs/ref/cli.rst | 14 ++++- packages/cli/src/lingui-compile.ts | 82 +++++++++++++++++++----------- packages/cli/src/lingui-extract.ts | 52 +++++++++++++------ 3 files changed, 100 insertions(+), 48 deletions(-) diff --git a/docs/ref/cli.rst b/docs/ref/cli.rst index 72836fff6..0fdeb0da5 100644 --- a/docs/ref/cli.rst +++ b/docs/ref/cli.rst @@ -44,7 +44,7 @@ Commands ``extract`` ----------- -.. lingui-cli:: extract [files...] [--clean] [--overwrite] [--format ] [--locale ] [--convert-from ] [--verbose] [--watch] +.. lingui-cli:: extract [files...] [--clean] [--overwrite] [--format ] [--locale ] [--convert-from ] [--verbose] [--watch [--debounce ]] This command extracts messages from source files and creates a message catalog for each language using the following steps: @@ -109,6 +109,11 @@ Watches only for changes in files in paths defined in config file or in the comm Remember to use this only in development as this command do not cleans obsolete translations. +.. lingui-cli-option:: --debounce + +Debounce, when used with ``--debounce ``, delays extraction for ```` milliseconds, +bundling multiple file changes together. + ``extract-template`` -------------------- @@ -123,7 +128,7 @@ Prints additional information. ``compile`` ----------- -.. lingui-cli:: compile [--strict] [--format ] [--verbose] [--namespace ] [--watch] +.. lingui-cli:: compile [--strict] [--format ] [--verbose] [--namespace ] [--watch [--debounce ]] This command compiles message catalogs in :conf:`localeDir` and outputs minified Javascript files. Each message is replaced with a function @@ -158,3 +163,8 @@ Generates a {compiledFile}.d.ts and the compiled file is generated using the ext Watch mode. Watches only for changes in locale files in your defined locale catalogs. For ex. ``locales\en\messages.po`` + +.. lingui-cli-option:: --debounce + +Debounce, when used with ``--debounce ``, delays compilation for ```` milliseconds, +to avoid compiling multiple times for subsequent file changes. diff --git a/packages/cli/src/lingui-compile.ts b/packages/cli/src/lingui-compile.ts index f9f4de007..bcbd7dfde 100644 --- a/packages/cli/src/lingui-compile.ts +++ b/packages/cli/src/lingui-compile.ts @@ -10,7 +10,7 @@ import { getConfig, LinguiConfig } from "@lingui/conf" import { getCatalogs } from "./api/catalog" import { createCompiledCatalog } from "./api/compile" import { helpRun } from "./api/help" -import { getFormat } from "./api"; +import { getFormat } from "./api" const noMessages: (catalogs: Object[]) => boolean = R.pipe( R.map(R.isEmpty), @@ -50,19 +50,13 @@ function command(config: LinguiConfig, options) { } catalogs.forEach((catalog) => { - const messages = catalog.getTranslations( - locale, - { - fallbackLocales: config.fallbackLocales, - sourceLocale: config.sourceLocale, - } - ) + const messages = catalog.getTranslations(locale, { + fallbackLocales: config.fallbackLocales, + sourceLocale: config.sourceLocale, + }) if (!options.allowEmpty) { - const missingMsgIds = R.pipe( - R.pickBy(R.isNil), - R.keys, - )(messages) + const missingMsgIds = R.pipe(R.pickBy(R.isNil), R.keys)(messages) if (missingMsgIds.length > 0) { console.error( @@ -77,7 +71,9 @@ function command(config: LinguiConfig, options) { console.error(chalk.red("Missing translations:")) missingMsgIds.forEach((msgId) => console.log(msgId)) } else { - console.error(chalk.red(`Missing ${missingMsgIds.length} translation(s)`)) + console.error( + chalk.red(`Missing ${missingMsgIds.length} translation(s)`) + ) } console.error() process.exit(1) @@ -87,7 +83,9 @@ function command(config: LinguiConfig, options) { if (doMerge) { mergedCatalogs = { ...mergedCatalogs, ...messages } } else { - const namespace = options.typescript ? "ts" : options.namespace || config.compileNamespace + const namespace = options.typescript + ? "ts" + : options.namespace || config.compileNamespace const compiledCatalog = createCompiledCatalog(locale, messages, { strict: false, namespace, @@ -135,6 +133,10 @@ if (require.main === module) { "Specify namespace for compiled bundle. Ex: cjs(default) -> module.exports, es -> export, window.test -> window.test" ) .option("--watch", "Enables Watch Mode") + .option( + "--debounce ", + "Debounces compilation for given amount of milliseconds" + ) .on("--help", function () { console.log("\n Examples:\n") console.log( @@ -159,42 +161,60 @@ if (require.main === module) { config.format = program.format } - const compile = () => command(config, { - verbose: program.watch || program.verbose || false, - allowEmpty: !program.strict, - typescript: program.typescript || config.compileNamespace === "ts" || false, - namespace: program.namespace, // we want this to be undefined if user does not specify so default can be used - }) + const compile = () => + command(config, { + verbose: program.watch || program.verbose || false, + allowEmpty: !program.strict, + typescript: + program.typescript || config.compileNamespace === "ts" || false, + namespace: program.namespace, // we want this to be undefined if user does not specify so default can be used + }) + + let debounceTimer: NodeJS.Timer + const dispatchCompile = () => { + // Skip debouncing if not enabled + if (!program.debounce) return compile() + + // CLear the previous timer if there is any, and schedule the next + debounceTimer && clearTimeout(debounceTimer) + debounceTimer = setTimeout(() => compile(), program.debounce) + } // Check if Watch Mode is enabled if (program.watch) { const NAME = "{name}" const LOCALE = "{locale}" - console.info(chalk.bold("Initializing Watch Mode...")) + console.info(chalk.bold("Initializing Watch Mode...")) - const catalogs = getCatalogs(config); - let paths = []; - const catalogExtension = getFormat(config.format).catalogExtension; + const catalogs = getCatalogs(config) + let paths = [] + const catalogExtension = getFormat(config.format).catalogExtension config.locales.forEach((locale) => { catalogs.forEach((catalog) => { - paths.push(`${catalog.path.replace(LOCALE, locale).replace(NAME, "*")}${catalogExtension}`) - }) + paths.push( + `${catalog.path + .replace(LOCALE, locale) + .replace(NAME, "*")}${catalogExtension}` + ) + }) }) const watcher = chokidar.watch(paths, { persistent: true, - }); + }) const onReady = () => { console.info(chalk.green.bold("Watcher is ready!")) - watcher.on('add', () => compile()).on('change', () => compile()); - }; + watcher + .on("add", () => dispatchCompile()) + .on("change", () => dispatchCompile()) + } - watcher.on('ready', () => onReady()); + watcher.on("ready", () => onReady()) } else { - const results = compile(); + const results = compile() if (!results) { process.exit(1) diff --git a/packages/cli/src/lingui-extract.ts b/packages/cli/src/lingui-extract.ts index b88eb9ecc..7eb4ca9b4 100644 --- a/packages/cli/src/lingui-extract.ts +++ b/packages/cli/src/lingui-extract.ts @@ -1,5 +1,5 @@ import chalk from "chalk" -import chokidar from "chokidar" +import chokidar, { watch } from "chokidar" import program from "commander" import { getConfig, LinguiConfig } from "@lingui/conf" @@ -37,7 +37,7 @@ export default function command( options.verbose && console.error("Extracting messages from source files…") const catalogs = getCatalogs(config) - const catalogStats: { [path: string]: AllCatalogsType } = {} + const catalogStats: { [path: string]: AllCatalogsType } = {} catalogs.forEach((catalog) => { catalog.make({ ...options, @@ -65,7 +65,7 @@ export default function command( `(use "${chalk.yellow( helpRun("compile") )}" to compile catalogs for production)` - ) + ) } return true @@ -77,6 +77,10 @@ if (require.main === module) { .option("--locale ", "Only extract the specified locale") .option("--overwrite", "Overwrite translations for source locale") .option("--clean", "Remove obsolete translations") + .option( + "--debounce ", + "Debounces extraction for given amount of milliseconds" + ) .option("--verbose", "Verbose output") .option( "--convert-from ", @@ -148,39 +152,57 @@ if (require.main === module) { }) } + const changedPaths = new Set() + let debounceTimer: NodeJS.Timer + const dispatchExtract = (filePath?: string[]) => { + // Skip debouncing if not enabled + if (!program.debounce) return extract(filePath) + + filePath?.forEach((path) => changedPaths.add(path)) + + // CLear the previous timer if there is any, and schedule the next + debounceTimer && clearTimeout(debounceTimer) + debounceTimer = setTimeout(() => { + const filePath = [...changedPaths] + changedPaths.clear() + + extract(filePath) + }, program.debounce) + } + // Check if Watch Mode is enabled if (program.watch) { console.info(chalk.bold("Initializing Watch Mode...")) const catalogs = getCatalogs(config) - let paths = []; - let ignored = []; + let paths = [] + let ignored = [] catalogs.forEach((catalog) => { - paths.push(...catalog.include); - ignored.push(...catalog.exclude); + paths.push(...catalog.include) + ignored.push(...catalog.exclude) }) const watcher = chokidar.watch(paths, { - ignored: ['/(^|[\/\\])\../', ...ignored], + ignored: ["/(^|[/\\])../", ...ignored], persistent: true, - }); + }) const onReady = () => { console.info(chalk.green.bold("Watcher is ready!")) watcher - .on('add', (path) => extract([path])) - .on('change', (path) => extract([path])); - }; + .on("add", (path) => dispatchExtract([path])) + .on("change", (path) => dispatchExtract([path])) + } - watcher.on('ready', () => onReady()); + watcher.on("ready", () => onReady()) } else if (program.args) { // this behaviour occurs when we extract files by his name // for ex: lingui extract src/app, this will extract only files included in src/app - const result = extract(program.args); + const result = extract(program.args) if (!result) process.exit(1) } else { - const result = extract(); + const result = extract() if (!result) process.exit(1) } }