From 4fe3c436244335fcb7ade90786eecf001314c7db Mon Sep 17 00:00:00 2001 From: Kim Biesbjerg Date: Mon, 16 Sep 2019 16:40:37 +0200 Subject: [PATCH] - (chore) update packages - (refactor) use tsquery for querying AST - (feat) autodetect usage of marker function and remove --marker cli argument - (bugfix) extract strings when TranslateService is declared directly as a class parameter. Closes https://github.com/biesbjerg/ngx-translate-extract/issues/83 - (bugfix) handle split strings: marker('hello ' + 'world') is now extracted as a single string: 'hello world' --- README.md | 4 +- package-lock.json | 151 +++++++++++------- package.json | 20 +-- src/cli/cli.ts | 20 +-- src/cli/tasks/extract.task.ts | 9 +- src/index.ts | 3 +- src/parsers/abstract-ast.parser.ts | 48 ------ src/parsers/function.parser.ts | 60 ------- src/parsers/marker.parser.ts | 34 ++++ src/parsers/service.parser.ts | 143 +++-------------- src/utils/ast-helpers.ts | 135 ++++++++++++++++ ...n.parser.spec.ts => marker.parser.spec.ts} | 8 +- tests/parsers/service.parser.spec.ts | 36 +++++ 13 files changed, 346 insertions(+), 325 deletions(-) delete mode 100644 src/parsers/abstract-ast.parser.ts delete mode 100644 src/parsers/function.parser.ts create mode 100644 src/parsers/marker.parser.ts create mode 100644 src/utils/ast-helpers.ts rename tests/parsers/{function.parser.spec.ts => marker.parser.spec.ts} (80%) diff --git a/README.md b/README.md index 405bdf28..2f269672 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ If you want to use spaces instead, you can do the following: `ngx-translate-extract --input ./src --output ./src/i18n/en.json --format-indentation ' '` ## Mark strings for extraction using a marker function -If, for some reason, you want to extract strings not passed directly to `TranslateService`'s `get()` or `instant()` methods, you can wrap them in a custom marker function to let `ngx-translate-extract` know you want to extract them. +If, for some reason, you want to extract strings not passed directly to `TranslateService`'s `get()` or `instant()` methods, you can wrap them in a marker function to let `ngx-translate-extract` know you want to extract them. Install marker function: `npm install @biesbjerg/ngx-translate-extract-marker` @@ -94,8 +94,6 @@ Options: --output, -o Paths where you would like to save extracted strings. You can use path expansion, glob patterns and multiple paths [array] [required] - --marker, -m Extract strings passed to a marker function - [string] [default: false] --format, -f Output format [string] [choices: "json", "namespaced-json", "pot"] [default: "json"] --format-indentation, --fi Output format indentation [string] [default: " "] diff --git a/package-lock.json b/package-lock.json index cb003373..6450e9b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "@biesbjerg/ngx-translate-extract", - "version": "3.0.4", + "version": "4.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { "@angular/compiler": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-8.2.2.tgz", - "integrity": "sha512-UMhOQehvi9u1r4u48Ymwm5JkdOKoH057ImCo26WqRqJBUgA44xwmUsKLFAmSg1JqzWCO5pBDyA3RaNBscD8ZzQ==", + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-8.2.6.tgz", + "integrity": "sha512-NdTY2n0XNRmKixbKDWB++9tEDLFwN0/Bp/1lXJ4qF/8V5Wju6IJ/UZZKjR5C4uiKtF17T3GzubhXgghumt5UVA==", "requires": { "tslib": "^1.9.0" } @@ -32,10 +32,18 @@ "js-tokens": "^4.0.0" } }, + "@phenomnomnominal/tsquery": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-3.0.0.tgz", + "integrity": "sha512-SW8lKitBHWJ9fAYkJ9kJivuctwNYCh3BUxLdH0+XiR1GPBiu+7qiZzh8p8jqlj1LgVC1TbvfNFroaEsmYlL8Iw==", + "requires": { + "esquery": "^1.0.1" + } + }, "@types/chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-zw8UvoBEImn392tLjxoavuonblX/4Yb9ha4KBU10FirCfwgzhKO0dvyJSF9ByxV1xK1r2AgnAi/tvQaLgxQqxA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.2.tgz", + "integrity": "sha512-8V2aCcPM3WLuJvJpF6N60uUvdZb7NHjpjQlLk1QmZbTP4XZET/FX0c3TJ3K7qt4L98FS1Pifa8BVofTVuJFWVA==", "dev": true }, "@types/events": { @@ -83,9 +91,9 @@ "dev": true }, "@types/node": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.8.tgz", - "integrity": "sha1-JeTdgEtjDJFq5nEjPm1x9s4YEko=", + "version": "12.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.5.tgz", + "integrity": "sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w==", "dev": true }, "@types/yargs": { @@ -143,9 +151,12 @@ "dev": true }, "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz", + "integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==", + "requires": { + "type-fest": "^0.5.2" + } }, "ansi-regex": { "version": "3.0.0", @@ -162,9 +173,9 @@ } }, "arg": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", - "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.1.tgz", + "integrity": "sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw==", "dev": true }, "argparse": { @@ -482,6 +493,19 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "requires": { + "estraverse": "^4.0.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -550,9 +574,9 @@ } }, "gettext-parser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-4.0.1.tgz", - "integrity": "sha512-ny1f9saN1xnhto5UzDOp7djJy7NbK6ebDAmOFXwp0DDu5KkQ5u3WF6giFU3BXHVqkS+3bxjXS1AmSUbX64fblA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-4.0.2.tgz", + "integrity": "sha512-JPCBpGzm01te+nTenJwWqKDzixYPY4pInedixpcMl4GPEJeia/cH2TJCh32IggDrrLYrzqA8OitXZLpBdrx4Gg==", "requires": { "content-type": "^1.0.4", "encoding": "^0.1.12", @@ -1147,9 +1171,9 @@ "dev": true }, "source-map-support": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", - "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -1173,18 +1197,11 @@ } }, "string_decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", - "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "requires": { - "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } + "safe-buffer": "~5.2.0" } }, "strip-ansi": { @@ -1217,18 +1234,26 @@ } }, "supports-hyperlinks": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz", - "integrity": "sha512-HHi5kVSefKaJkGYXbDuKbUGRVxqnWGn3J2e39CYcNJEfWciGq2zYtOhXLTlvrOZW1QU7VX67w7fMmWafHX9Pfw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.0.0.tgz", + "integrity": "sha512-bFhn0MQ8qefLyJ3K7PpHiPUTuTVPWw6RXfaMeV6xgJLXtBbszyboz1bvGTVv4R0YpQm2DqlXXn0fFHhxUHVE5w==", "requires": { - "has-flag": "^2.0.0", - "supports-color": "^5.0.0" + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" }, "dependencies": { "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.0.0.tgz", + "integrity": "sha512-WRt32iTpYEZWYOpcetGm0NPeSvaebccx7hhS/5M6sAiqnhedtFCHFxkjzZlJvFNCPowiKSFGiZk5USQDFy83vQ==", + "requires": { + "has-flag": "^4.0.0" + } } } }, @@ -1238,18 +1263,18 @@ "integrity": "sha512-I42EWhJ+2aeNQawGx1VtpO0DFI9YcfuvAMNIdKyf/6sRbHJ4P+ZQ/zIT87tE+ln1ymAGcCJds4dolfSAS0AcNg==" }, "terminal-link": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-1.3.0.tgz", - "integrity": "sha512-nFaWG/gs3brGi3opgWU2+dyFGbQ7tueSRYOBOD8URdDXCbAGqDEZzuskCc+okCClYcJFDPwn8e2mbv4FqAnWFA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.0.0.tgz", + "integrity": "sha512-rdBAY35jUvVapqCuhehjenLbYY73cVgRQ6podD6u9EDBomBBHjCOtmq2InPgPpTysOIOsQ5PdBzwSC/sKjv6ew==", "requires": { - "ansi-escapes": "^3.2.0", - "supports-hyperlinks": "^1.0.1" + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" } }, "ts-node": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.3.0.tgz", - "integrity": "sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.4.1.tgz", + "integrity": "sha512-5LpRN+mTiCs7lI5EtbXmF/HfMeCjzt7DH9CZwtkr6SywStrNQC723wG+aOWFiLNn7zT3kD/RnFqi3ZUfr4l5Qw==", "dev": true, "requires": { "arg": "^4.1.0", @@ -1273,16 +1298,16 @@ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, "tslint": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.19.0.tgz", - "integrity": "sha512-1LwwtBxfRJZnUvoS9c0uj8XQtAnyhWr9KlNvDIdB+oXyT+VpsOAaEhEgKi1HrZ8rq0ki/AAnbGSv4KM6/AfVZw==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.0.tgz", + "integrity": "sha512-2vqIvkMHbnx8acMogAERQ/IuINOq6DFqgF8/VDvhEkBqQh/x6SP0Y+OHnKth9/ZcHQSroOZwUQSN18v8KKF0/g==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "builtin-modules": "^1.1.1", "chalk": "^2.3.0", "commander": "^2.12.1", - "diff": "^3.2.0", + "diff": "^4.0.1", "glob": "^7.1.1", "js-yaml": "^3.13.1", "minimatch": "^3.0.4", @@ -1291,6 +1316,14 @@ "semver": "^5.3.0", "tslib": "^1.8.0", "tsutils": "^2.29.0" + }, + "dependencies": { + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + } } }, "tslint-eslint-rules": { @@ -1342,9 +1375,9 @@ "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==" }, "typescript": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", - "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==" + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz", + "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==" }, "util": { "version": "0.10.3", @@ -1636,9 +1669,9 @@ } }, "yn": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.0.tgz", - "integrity": "sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true } } diff --git a/package.json b/package.json index 5e929322..f4e404dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@biesbjerg/ngx-translate-extract", - "version": "3.0.5", + "version": "4.0.0", "description": "Extract strings from projects using ngx-translate", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -40,36 +40,38 @@ }, "homepage": "https://github.com/biesbjerg/ngx-translate-extract", "engines": { - "node": ">=4.3.2" + "node": ">=8" }, "config": {}, "devDependencies": { - "@types/chai": "^4.2.0", + "@types/chai": "^4.2.2", "@types/flat": "^0.0.28", "@types/glob": "^7.1.1", "@types/mkdirp": "^0.5.2", "@types/mocha": "^5.2.7", + "@types/node": "^12.7.5", "@types/yargs": "^13.0.2", "chai": "^4.2.0", "mocha": "^6.2.0", - "ts-node": "^8.3.0", - "tslint": "^5.19.0", + "ts-node": "^8.4.1", + "tslint": "^5.20.0", "tslint-eslint-rules": "^5.4.0" }, "bundledDependencies": [ "flat" ], "dependencies": { - "@angular/compiler": "^8.2.2", + "@angular/compiler": "^8.2.6", + "@phenomnomnominal/tsquery": "^3.0.0", "boxen": "^4.1.0", "colorette": "^1.1.0", "flat": "github:lenchvolodymyr/flat#ffe77ef", - "gettext-parser": "^4.0.1", + "gettext-parser": "^4.0.2", "glob": "^7.1.4", "mkdirp": "^0.5.1", "path": "^0.12.7", - "terminal-link": "^1.3.0", - "typescript": "^3.5.3", + "terminal-link": "^2.0.0", + "typescript": "^3.6.3", "yargs": "^14.0.0" } } diff --git a/src/cli/cli.ts b/src/cli/cli.ts index a0cd694b..d6c890fb 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -6,7 +6,7 @@ import { ParserInterface } from '../parsers/parser.interface'; import { PipeParser } from '../parsers/pipe.parser'; import { DirectiveParser } from '../parsers/directive.parser'; import { ServiceParser } from '../parsers/service.parser'; -import { FunctionParser } from '../parsers/function.parser'; +import { MarkerParser } from '../parsers/marker.parser'; import { PostProcessorInterface } from '../post-processors/post-processor.interface'; import { SortByKeyPostProcessor } from '../post-processors/sort-by-key.post-processor'; import { KeyAsDefaultValuePostProcessor } from '../post-processors/key-as-default-value.post-processor'; @@ -29,7 +29,7 @@ export const cli = yargs normalize: true }) .check(options => { - options.input.forEach((dir: string) => { + (options.input as unknown as string[]).forEach((dir: string) => { if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { throw new Error(`The path you supplied was not found: '${dir}'`); } @@ -50,12 +50,6 @@ export const cli = yargs normalize: true, required: true }) - .option('marker', { - alias: 'm', - describe: 'Extract strings passed to a marker function', - default: false, - type: 'string' - }) .option('format', { alias: 'f', describe: 'Output format', @@ -96,7 +90,7 @@ export const cli = yargs .exitProcess(true) .parse(process.argv); -const extractTask = new ExtractTask(cli.input, cli.output, { +const extractTask = new ExtractTask(cli.input as unknown as string[], cli.output, { replace: cli.replace, patterns: cli.patterns }); @@ -105,13 +99,9 @@ const extractTask = new ExtractTask(cli.input, cli.output, { const parsers: ParserInterface[] = [ new PipeParser(), new DirectiveParser(), - new ServiceParser() + new ServiceParser(), + new MarkerParser() ]; -if (cli.marker) { - parsers.push(new FunctionParser({ - identifier: cli.marker - })); -} extractTask.setParsers(parsers); // Post processors diff --git a/src/cli/tasks/extract.task.ts b/src/cli/tasks/extract.task.ts index b120a525..e7a2219c 100644 --- a/src/cli/tasks/extract.task.ts +++ b/src/cli/tasks/extract.task.ts @@ -100,17 +100,20 @@ export class ExtractTask implements TaskInterface { * Extract strings from specified input dirs using configured parsers */ protected extract(): TranslationCollection { - let extracted: TranslationCollection = new TranslationCollection(); + let collection: TranslationCollection = new TranslationCollection(); this.inputs.forEach(dir => { this.readDir(dir, this.options.patterns).forEach(path => { this.out(dim('- %s'), path); const contents: string = fs.readFileSync(path, 'utf-8'); this.parsers.forEach(parser => { - extracted = extracted.union(parser.extract(contents, path)); + const extracted = parser.extract(contents, path); + if (extracted) { + collection = collection.union(extracted); + } }); }); }); - return extracted; + return collection; } /** diff --git a/src/index.ts b/src/index.ts index 34bc206e..3e8133bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,11 +6,10 @@ export * from './cli/tasks/task.interface'; export * from './cli/tasks/extract.task'; export * from './parsers/parser.interface'; -export * from './parsers/abstract-ast.parser'; export * from './parsers/directive.parser'; export * from './parsers/pipe.parser'; export * from './parsers/service.parser'; -export * from './parsers/function.parser'; +export * from './parsers/marker.parser'; export * from './compilers/compiler.interface'; export * from './compilers/compiler.factory'; diff --git a/src/parsers/abstract-ast.parser.ts b/src/parsers/abstract-ast.parser.ts deleted file mode 100644 index c01a1cb8..00000000 --- a/src/parsers/abstract-ast.parser.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - createSourceFile, - SourceFile, - CallExpression, - Node, - SyntaxKind, - StringLiteral, - NoSubstitutionTemplateLiteral -} from 'typescript'; - -export abstract class AbstractAstParser { - - protected sourceFile: SourceFile; - - protected createSourceFile(path: string, contents: string): SourceFile { - return createSourceFile(path, contents, null, /*setParentNodes */ false); - } - - /** - * Get strings from function call's first argument - */ - protected getStringLiterals(callNode: CallExpression): string[] { - if (!callNode.arguments.length) { - return[]; - } - - const firstArg = callNode.arguments[0]; - - return this.findNodes(firstArg, [ - SyntaxKind.StringLiteral, - SyntaxKind.NoSubstitutionTemplateLiteral - ]) - .map((node: StringLiteral | NoSubstitutionTemplateLiteral) => node.text); - } - - /** - * Find all child nodes of a kind - */ - protected findNodes(node: Node, kinds: SyntaxKind[]): Node[] { - const childrenNodes: Node[] = node.getChildren(this.sourceFile); - const initialValue: Node[] = kinds.includes(node.kind) ? [node] : []; - - return childrenNodes.reduce((result: Node[], childNode: Node) => { - return result.concat(this.findNodes(childNode, kinds)); - }, initialValue); - } - -} diff --git a/src/parsers/function.parser.ts b/src/parsers/function.parser.ts deleted file mode 100644 index 7b9eed43..00000000 --- a/src/parsers/function.parser.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Node, CallExpression, SyntaxKind, Identifier } from 'typescript'; - -import { ParserInterface } from './parser.interface'; -import { AbstractAstParser } from './abstract-ast.parser'; -import { TranslationCollection } from '../utils/translation.collection'; - -export class FunctionParser extends AbstractAstParser implements ParserInterface { - - protected functionIdentifier: string = 'marker'; - - public constructor(options?: any) { - super(); - if (options && typeof options.identifier !== 'undefined') { - this.functionIdentifier = options.identifier; - } - } - - public extract(template: string, path: string): TranslationCollection { - let collection: TranslationCollection = new TranslationCollection(); - - this.sourceFile = this.createSourceFile(path, template); - - const callNodes = this.findCallNodes(); - callNodes.forEach(callNode => { - const keys: string[] = this.getStringLiterals(callNode); - if (keys && keys.length) { - collection = collection.addKeys(keys); - } - }); - return collection; - } - - /** - * Find all calls to marker function - */ - protected findCallNodes(node?: Node): CallExpression[] { - if (!node) { - node = this.sourceFile; - } - - let callNodes = this.findNodes(node, [SyntaxKind.CallExpression]) as CallExpression[]; - callNodes = callNodes - .filter(callNode => { - // Only call expressions with arguments - if (callNode.arguments.length < 1) { - return false; - } - - const identifier = (callNode.getChildAt(0) as Identifier).text; - if (identifier !== this.functionIdentifier) { - return false; - } - - return true; - }); - - return callNodes; - } - -} diff --git a/src/parsers/marker.parser.ts b/src/parsers/marker.parser.ts new file mode 100644 index 00000000..9ec5d328 --- /dev/null +++ b/src/parsers/marker.parser.ts @@ -0,0 +1,34 @@ +import { tsquery } from '@phenomnomnominal/tsquery'; + +import { ParserInterface } from './parser.interface'; +import { TranslationCollection } from '../utils/translation.collection'; +import { getNamedImportAlias, findFunctionCallExpressions, getStringsFromExpression } from '../utils/ast-helpers'; + +const MARKER_PACKAGE_MODULE_NAME = '@biesbjerg/ngx-translate-extract-marker'; +const MARKER_PACKAGE_IMPORT_NAME = 'marker'; + +export class MarkerParser implements ParserInterface { + + public extract(contents: string, filePath: string): TranslationCollection { + const sourceFile = tsquery.ast(contents, filePath); + + const markerFnName = getNamedImportAlias(sourceFile, MARKER_PACKAGE_MODULE_NAME, MARKER_PACKAGE_IMPORT_NAME); + if (!markerFnName) { + return; + } + + let collection: TranslationCollection = new TranslationCollection(); + + const callNodes = findFunctionCallExpressions(sourceFile, markerFnName); + callNodes.forEach(callNode => { + const [firstArgNode] = callNode.arguments; + if (!firstArgNode) { + return; + } + const strings = getStringsFromExpression(firstArgNode); + collection = collection.addKeys(strings); + }); + return collection; + } + +} diff --git a/src/parsers/service.parser.ts b/src/parsers/service.parser.ts index 99ebd150..9e2f06cc 100644 --- a/src/parsers/service.parser.ts +++ b/src/parsers/service.parser.ts @@ -1,142 +1,41 @@ -import { - SourceFile, - Node, - ConstructorDeclaration, - Identifier, - TypeReferenceNode, - ClassDeclaration, - SyntaxKind, - CallExpression, - PropertyAccessExpression, - isPropertyAccessExpression -} from 'typescript'; +import { tsquery } from '@phenomnomnominal/tsquery'; import { ParserInterface } from './parser.interface'; -import { AbstractAstParser } from './abstract-ast.parser'; import { TranslationCollection } from '../utils/translation.collection'; +import { findClasses, findClassPropertyByType, findMethodCallExpression, getStringsFromExpression } from '../utils/ast-helpers'; -export class ServiceParser extends AbstractAstParser implements ParserInterface { +const TRANSLATE_SERVICE_TYPE_REFERENCE = 'TranslateService'; +const TRANSLATE_SERVICE_METHOD_NAMES = ['get', 'instant', 'stream']; - protected sourceFile: SourceFile; +export class ServiceParser implements ParserInterface { + + public extract(source: string, filePath: string): TranslationCollection { + const sourceFile = tsquery.ast(source, filePath); + + const classNodes = findClasses(sourceFile); + if (!classNodes) { + return; + } - public extract(template: string, path: string): TranslationCollection { let collection: TranslationCollection = new TranslationCollection(); - this.sourceFile = this.createSourceFile(path, template); - const classNodes = this.findClassNodes(this.sourceFile); classNodes.forEach(classNode => { - const constructorNode = this.findConstructorNode(classNode); - if (!constructorNode) { - return; - } - - const propertyName: string = this.findTranslateServicePropertyName(constructorNode); - if (!propertyName) { + const propName: string = findClassPropertyByType(classNode, TRANSLATE_SERVICE_TYPE_REFERENCE); + if (!propName) { return; } - const callNodes = this.findCallNodes(classNode, propertyName); + const callNodes = findMethodCallExpression(classNode, propName, TRANSLATE_SERVICE_METHOD_NAMES); callNodes.forEach(callNode => { - const keys: string[] = this.getStringLiterals(callNode); - if (keys && keys.length) { - collection = collection.addKeys(keys); + const [firstArgNode] = callNode.arguments; + if (!firstArgNode) { + return; } + const strings = getStringsFromExpression(firstArgNode); + collection = collection.addKeys(strings); }); }); - return collection; } - /** - * Detect what the TranslateService instance property - * is called by inspecting constructor arguments - */ - protected findTranslateServicePropertyName(constructorNode: ConstructorDeclaration): string { - if (!constructorNode) { - return null; - } - - const result = constructorNode.parameters.find(parameter => { - // Skip if visibility modifier is not present (we want it set as an instance property) - if (!parameter.modifiers) { - return false; - } - - // Parameter has no type - if (!parameter.type) { - return false; - } - - // Make sure className is of the correct type - const parameterType: Identifier = (parameter.type as TypeReferenceNode).typeName as Identifier; - if (!parameterType) { - return false; - } - const className: string = parameterType.text; - if (className !== 'TranslateService') { - return false; - } - - return true; - }); - - if (result) { - return (result.name as Identifier).text; - } - } - - /** - * Find class nodes - */ - protected findClassNodes(node: Node): ClassDeclaration[] { - return this.findNodes(node, [SyntaxKind.ClassDeclaration]) as ClassDeclaration[]; - } - - /** - * Find constructor - */ - protected findConstructorNode(node: ClassDeclaration): ConstructorDeclaration { - const constructorNodes = this.findNodes(node, [SyntaxKind.Constructor]) as ConstructorDeclaration[]; - if (constructorNodes) { - return constructorNodes[0]; - } - } - - /** - * Find all calls to TranslateService methods - */ - protected findCallNodes(node: Node, propertyIdentifier: string): CallExpression[] { - let callNodes = this.findNodes(node, [SyntaxKind.CallExpression]) as CallExpression[]; - callNodes = callNodes - .filter(callNode => { - // Only call expressions with arguments - if (callNode.arguments.length < 1) { - return false; - } - - const propAccess = callNode.getChildAt(0).getChildAt(0) as PropertyAccessExpression; - if (!propAccess || !isPropertyAccessExpression(propAccess)) { - return false; - } - if (!propAccess.getFirstToken() || propAccess.getFirstToken().kind !== SyntaxKind.ThisKeyword) { - return false; - } - if (propAccess.name.text !== propertyIdentifier) { - return false; - } - - const methodAccess = callNode.getChildAt(0) as PropertyAccessExpression; - if (!methodAccess || methodAccess.kind !== SyntaxKind.PropertyAccessExpression) { - return false; - } - if (!methodAccess.name || (methodAccess.name.text !== 'get' && methodAccess.name.text !== 'instant' && methodAccess.name.text !== 'stream')) { - return false; - } - - return true; - }); - - return callNodes; - } - } diff --git a/src/utils/ast-helpers.ts b/src/utils/ast-helpers.ts new file mode 100644 index 00000000..78a7adec --- /dev/null +++ b/src/utils/ast-helpers.ts @@ -0,0 +1,135 @@ +import { tsquery } from '@phenomnomnominal/tsquery'; +import { + Node, + NamedImports, + Identifier, + ClassDeclaration, + CallExpression, + isStringLiteralLike, + isArrayLiteralExpression, + Expression, + isBinaryExpression, + SyntaxKind, + isConditionalExpression, + PropertyAccessExpression +} from 'typescript'; + +export function getNamedImports(node: Node, moduleName: string): NamedImports[] { + const query = `ImportDeclaration[moduleSpecifier.text="${moduleName}"] NamedImports`; + return tsquery(node, query); +} + +export function getNamedImportAlias(node: Node, moduleName: string, importName: string): string | null { + const [namedImportNode] = getNamedImports(node, moduleName); + if (!namedImportNode) { + return; + } + + const query = `ImportSpecifier:has(Identifier[name="${importName}"]) > Identifier`; + const identifiers = tsquery(namedImportNode, query); + if (identifiers.length === 1) { + return identifiers[0].text; + } + if (identifiers.length > 1) { + return identifiers[identifiers.length - 1].text; + } + return null; +} + +export function findClasses(node: Node): ClassDeclaration[] { + const query = 'ClassDeclaration'; + return tsquery(node, query); +} + +export function findClassPropertyByType(node: ClassDeclaration, type: string): string | null { + return findClassPropertyConstructorParameterByType(node, type) || findClassPropertyDeclarationByType(node, type); +} + +export function findClassPropertyConstructorParameterByType(node: ClassDeclaration, type: string): string | null { + const query = `Constructor Parameter:has(TypeReference > Identifier[name="${type}"]):has(PublicKeyword,ProtectedKeyword,PrivateKeyword) > Identifier`; + const [result] = tsquery(node, query); + if (result) { + return result.text; + } + return null; +} + +export function findClassPropertyDeclarationByType(node: ClassDeclaration, type: string): string | null { + const query = `PropertyDeclaration:has(TypeReference > Identifier[name="${type}"]) > Identifier`; + const [result] = tsquery(node, query); + if (result) { + return result.text; + } + return null; +} + +export function findFunctionCallExpressions(node: Node, fnName: string | string[]): CallExpression[] { + if (Array.isArray(fnName)) { + fnName = fnName.join('|'); + } + const query = `CallExpression:has(Identifier[name="${fnName}"]):not(:has(PropertyAccessExpression))`; + const nodes = tsquery(node, query); + return nodes; +} + +export function findMethodCallExpression(node: Node, prop: string, fnName: string | string[]): CallExpression[] { + if (Array.isArray(fnName)) { + fnName = fnName.join('|'); + } + const query = `CallExpression > PropertyAccessExpression:has(Identifier[name=/^(${fnName})$/]):has(PropertyAccessExpression:has(Identifier[name="${prop}"]):has(ThisKeyword))`; + let nodes = tsquery(node, query).map(node => node.parent as CallExpression); + return nodes; +} + +export function getStringsFromExpression(expression: Expression): string[] { + if (isStringLiteralLike(expression)) { + return [expression.text]; + } + + if (isArrayLiteralExpression(expression)) { + return expression.elements.reduce((result: string[], element: Expression) => { + const strings = this.getStringsFromExpression(element); + return [ + ...result, + ...strings + ]; + }, []); + } + + if (isBinaryExpression(expression)) { + const [left] = this.getStringsFromExpression(expression.left); + const [right] = this.getStringsFromExpression(expression.right); + + if (expression.operatorToken.kind === SyntaxKind.PlusToken) { + if (typeof left === 'string' && typeof right === 'string') { + return [left + right]; + } + } + + if (expression.operatorToken.kind === SyntaxKind.BarBarToken) { + const result = []; + if (typeof left === 'string') { + result.push(left); + } + if (typeof right === 'string') { + result.push(right); + } + return result; + } + } + + if (isConditionalExpression(expression)) { + const [whenTrue] = this.getStringsFromExpression(expression.whenTrue); + const [whenFalse] = this.getStringsFromExpression(expression.whenFalse); + + const result = []; + if (typeof whenTrue === 'string') { + result.push(whenTrue); + } + if (typeof whenFalse === 'string') { + result.push(whenFalse); + } + return result; + } + return []; +} diff --git a/tests/parsers/function.parser.spec.ts b/tests/parsers/marker.parser.spec.ts similarity index 80% rename from tests/parsers/function.parser.spec.ts rename to tests/parsers/marker.parser.spec.ts index ae32bd11..3e0042a8 100644 --- a/tests/parsers/function.parser.spec.ts +++ b/tests/parsers/marker.parser.spec.ts @@ -1,15 +1,15 @@ import { expect } from 'chai'; -import { FunctionParser } from '../../src/parsers/function.parser'; +import { MarkerParser } from '../../src/parsers/marker.parser'; -describe('FunctionParser', () => { +describe('MarkerParser', () => { const componentFilename: string = 'test.component.ts'; - let parser: FunctionParser; + let parser: MarkerParser; beforeEach(() => { - parser = new FunctionParser(); + parser = new MarkerParser(); }); diff --git a/tests/parsers/service.parser.spec.ts b/tests/parsers/service.parser.spec.ts index 2c5f27a2..a3027d83 100644 --- a/tests/parsers/service.parser.spec.ts +++ b/tests/parsers/service.parser.spec.ts @@ -224,6 +224,7 @@ describe('ServiceParser', () => { console.log(translations[variable]); }); } + } `; const keys = parser.extract(contents, componentFilename).keys(); expect(keys).to.deep.equal(['yes']); @@ -263,4 +264,39 @@ describe('ServiceParser', () => { expect(keys).to.deep.equal(['Extract me!', 'Hello!']); }); + it('should extract strings when TranslateService is declared as a property', () => { + const contents = ` + export class MyComponent { + protected translateService: TranslateService; + public constructor() { + this.translateService = new TranslateService(); + } + public test() { + this.translateService.instant('Hello World'); + } + `; + const keys = parser.extract(contents, componentFilename).keys(); + expect(keys).to.deep.equal(['Hello World']); + }); + + it('should extract strings passed to TranslateServices methods only', () => { + const contents = ` + export class AppComponent implements OnInit { + constructor(protected config: Config, protected translateService: TranslateService) {} + + public ngOnInit(): void { + this.localizeBackButton(); + } + + protected localizeBackButton(): void { + this.translateService.onLangChange.subscribe((event: LangChangeEvent) => { + this.config.set('backButtonText', this.translateService.instant('Back')); + }); + } + } + `; + const keys = parser.extract(contents, componentFilename).keys(); + expect(keys).to.deep.equal(['Back']); + }); + });