diff --git a/.gitignore b/.gitignore index 03e178e..0abdb43 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ npm-debug.log* # Compiled files dist +src/**/*.js +tests/**/*.js # Extracted strings strings.json diff --git a/package-lock.json b/package-lock.json index d7dbc38..8738545 100644 --- a/package-lock.json +++ b/package-lock.json @@ -107,6 +107,12 @@ "any-observable": "^0.3.0" } }, + "@types/braces": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.0.tgz", + "integrity": "sha512-TbH79tcyi9FHwbyboOKeRachRq63mSuWYXOflsNO9ZyE5ClQ/JaozNKl+aWUq87qPNsXasXxi2AbgfwIJ+8GQw==", + "dev": true + }, "@types/chai": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.10.tgz", diff --git a/package.json b/package.json index 24cd5fa..a6d863f 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ }, "config": {}, "devDependencies": { + "@types/braces": "^3.0.0", "@types/chai": "^4.2.10", "@types/flat": "^5.0.0", "@types/glob": "^7.1.1", @@ -68,6 +69,7 @@ "@types/mocha": "^7.0.2", "@types/node": "^12.12.29", "@types/yargs": "^15.0.4", + "braces": "^3.0.2", "chai": "^4.2.0", "husky": "^4.2.3", "lint-staged": "^10.0.8", diff --git a/src/cli/cli.ts b/src/cli/cli.ts index e07e065..571ffae 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -1,4 +1,3 @@ -import * as fs from 'fs'; import * as yargs from 'yargs'; import { ExtractTask } from './tasks/extract.task'; @@ -14,9 +13,22 @@ import { NullAsDefaultValuePostProcessor } from '../post-processors/null-as-defa import { PurgeObsoleteKeysPostProcessor } from '../post-processors/purge-obsolete-keys.post-processor'; import { CompilerInterface } from '../compilers/compiler.interface'; import { CompilerFactory } from '../compilers/compiler.factory'; +import { normalizePaths } from '../utils/fs-helpers'; import { donateMessage } from '../utils/donate'; -export const cli = yargs +// First parsing pass to be able to access pattern argument for use input/output arguments +const y = yargs + .option('patterns', { + alias: 'p', + describe: 'Default patterns', + type: 'array', + default: ['/**/*.html', '/**/*.ts'], + hidden: true + }); + +const parsed = y.parse(); + +export const cli = y .usage('Extract strings from files for translation.\nUsage: $0 [options]') .version(require(__dirname + '/../../package.json').version) .alias('version', 'v') @@ -30,19 +42,9 @@ export const cli = yargs normalize: true, required: true }) - .check(options => { - options.input.forEach((dir: string) => { - if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { - throw new Error(`The path you supplied was not found: '${dir}'`); - } - }); - return true; - }) - .option('patterns', { - alias: 'p', - describe: 'Extract strings from the following file patterns', - type: 'array', - default: ['/**/*.html', '/**/*.ts'] + .coerce('input', (input: string[]) => { + const paths = normalizePaths(input, parsed.patterns); + return paths; }) .option('output', { alias: 'o', @@ -51,16 +53,20 @@ export const cli = yargs normalize: true, required: true }) + .coerce('output', (output: string[]) => { + const paths = normalizePaths(output, parsed.patterns); + return paths; + }) .option('format', { alias: 'f', - describe: 'Output format', + describe: 'Format', default: 'json', type: 'string', choices: ['json', 'namespaced-json', 'pot'] }) .option('format-indentation', { alias: 'fi', - describe: 'Output format indentation', + describe: 'Format indentation (JSON/Namedspaced JSON)', default: '\t', type: 'string' }) @@ -71,31 +77,39 @@ export const cli = yargs }) .option('sort', { alias: 's', - describe: 'Sort strings in alphabetical order when saving', + describe: 'Sort strings in alphabetical order', type: 'boolean' }) .option('clean', { alias: 'c', - describe: 'Remove obsolete strings when merging', + describe: 'Remove obsolete strings after merge', type: 'boolean' }) .option('key-as-default-value', { alias: 'k', - describe: 'Use key as default value for translations', + describe: 'Use key as default value', type: 'boolean' }) .option('null-as-default-value', { alias: 'n', - describe: 'Use null as default value for translations', + describe: 'Use null as default value', type: 'boolean' }) + .group(['format', 'format-indentation', 'sort', 'clean'], 'Output') + .group(['key-as-default-value', 'null-as-default-value'], 'Default value (defaults to empty string)') .conflicts('key-as-default-value', 'null-as-default-value') + .example(`$0 -i ./src-a/ -i ./src-b/ -o strings.json`, 'Extract (ts, html) from multiple paths') + .example(`$0 -i './{src-a,src-b}/' -o strings.json`, 'Extract (ts, html) from multiple paths using brace expansion') + .example(`$0 -i ./src/ -o ./i18n/da.json -o ./i18n/en.json`, 'Extract (ts, html) and save to da.json+en.json') + .example(`$0 -i ./src/ -o './i18n/{en,da}.json'`, 'Extract (ts, html) and save to da.json+en.json using brace expansion') + .example(`$0 -i './src/**/*.{ts,tsx,html}' -o strings.json`, 'Extract from ts, tsx and html') + .example(`$0 -i './src/**/!(*.spec).{ts,html}' -o strings.json`, 'Extract from ts, html, excluding files with ".spec" in filename') + .wrap(110) .exitProcess(true) .parse(process.argv); const extractTask = new ExtractTask(cli.input, cli.output, { - replace: cli.replace, - patterns: cli.patterns + replace: cli.replace }); // Parsers diff --git a/src/cli/tasks/extract.task.ts b/src/cli/tasks/extract.task.ts index a0ecf63..2807b99 100644 --- a/src/cli/tasks/extract.task.ts +++ b/src/cli/tasks/extract.task.ts @@ -12,13 +12,11 @@ import * as mkdirp from 'mkdirp'; export interface ExtractTaskOptionsInterface { replace?: boolean; - patterns?: string[]; } export class ExtractTask implements TaskInterface { protected options: ExtractTaskOptionsInterface = { - replace: false, - patterns: [] + replace: false }; protected parsers: ParserInterface[] = []; @@ -100,8 +98,8 @@ export class ExtractTask implements TaskInterface { */ protected extract(): TranslationCollection { let collection: TranslationCollection = new TranslationCollection(); - this.inputs.forEach(dir => { - this.readDir(dir, this.options.patterns).forEach(filePath => { + this.inputs.forEach(pattern => { + this.getFiles(pattern).forEach(filePath => { this.out(dim('- %s'), filePath); const contents: string = fs.readFileSync(filePath, 'utf-8'); this.parsers.forEach(parser => { @@ -138,15 +136,12 @@ export class ExtractTask implements TaskInterface { } /** - * Get all files in dir matching patterns + * Get all files matching pattern */ - protected readDir(dir: string, patterns: string[]): string[] { - return patterns.reduce((results, pattern) => { - return glob - .sync(dir + pattern) - .filter(filePath => fs.statSync(filePath).isFile()) - .concat(results); - }, []); + protected getFiles(pattern: string): string[] { + return glob + .sync(pattern) + .filter(filePath => fs.statSync(filePath).isFile()); } protected out(...args: any[]): void { diff --git a/src/utils/fs-helpers.ts b/src/utils/fs-helpers.ts new file mode 100644 index 0000000..8c392b4 --- /dev/null +++ b/src/utils/fs-helpers.ts @@ -0,0 +1,26 @@ +import * as os from 'os'; +import * as fs from 'fs'; +import * as braces from 'braces'; + +export function normalizeHomeDir(path: string): string { + if (path.substring(0, 1) === '~') { + return `${os.homedir()}/${path.substring(1)}`; + } + return path; +} + +export function expandPattern(pattern: string): string[] { + return braces(pattern, { expand: true }); +} + +export function normalizePaths(patterns: string[], defaultPatterns: string[] = []): string[] { + return patterns.map(pattern => + expandPattern(pattern).map(path => { + path = normalizeHomeDir(path); + if (fs.existsSync(path) && fs.statSync(path).isDirectory()) { + return defaultPatterns.map(defaultPattern => path + defaultPattern); + } + return path; + }).flat() + ).flat(); +}