From 24d6d1bb7a770d9298675d74a365b14147a2f229 Mon Sep 17 00:00:00 2001 From: Long Ho Date: Sun, 3 Jan 2021 22:36:19 -0500 Subject: [PATCH] feat(@formatjs/cli): support .vue SFC files --- package.json | 2 + packages/cli/BUILD | 2 + packages/cli/package.json | 2 + packages/cli/src/cli.ts | 6 ++ packages/cli/src/extract.ts | 49 +++-------- packages/cli/src/parse_script.ts | 46 +++++++++++ packages/cli/src/vue_extractor.ts | 59 +++++++++++++ .../__snapshots__/integration.test.ts.snap | 13 +++ .../cli/tests/extract/integration.test.ts | 8 ++ packages/cli/tests/extract/vue/comp.vue | 17 ++++ .../cli/tests/extract/vue_extractor.test.ts | 50 +++++++++++ website/docs/tooling/cli.md | 6 +- yarn.lock | 82 ++++++++++++++++++- 13 files changed, 302 insertions(+), 40 deletions(-) create mode 100644 packages/cli/src/parse_script.ts create mode 100644 packages/cli/src/vue_extractor.ts create mode 100644 packages/cli/tests/extract/vue/comp.vue create mode 100644 packages/cli/tests/extract/vue_extractor.test.ts diff --git a/package.json b/package.json index 32f96f71a4..4c6a06714a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,8 @@ "@typescript-eslint/eslint-plugin": "^4.0.0", "@typescript-eslint/parser": "^4.11.0", "@typescript-eslint/typescript-estree": "^4.11.0", + "@vue/compiler-core": "^3.0.5", + "@vue/compiler-sfc": "^3.0.5", "@vue/test-utils": "2.0.0-beta.13", "benchmark": "^2.1.4", "chalk": "^4.0.0", diff --git a/packages/cli/BUILD b/packages/cli/BUILD index c61287cfb2..7dcce43271 100644 --- a/packages/cli/BUILD +++ b/packages/cli/BUILD @@ -43,6 +43,7 @@ SRC_DEPS = [ "@npm//chalk", "@npm//json-stable-stringify", "@npm//@types/json-stable-stringify", + "@npm//@vue/compiler-sfc", ] ts_compile( @@ -57,6 +58,7 @@ jest_test( srcs = [ "package.json", "tests/extract/unit.test.ts", + "tests/extract/vue_extractor.test.ts", "tests/extract/__snapshots__/unit.test.ts.snap", ] + SRCS, deps = [ diff --git a/packages/cli/package.json b/packages/cli/package.json index 8f2b0d040c..307dba715c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -36,6 +36,8 @@ "@types/lodash": "^4.14.150", "@types/loud-rejection": "^2.0.0", "@types/node": "14", + "@vue/compiler-core": "^3.0.0", + "@vue/compiler-sfc": "^3.0.5", "chalk": "^4.0.0", "commander": "^6.1.0", "fast-glob": "^3.2.4", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 8da6358e57..443f709929 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -79,6 +79,11 @@ is imported from \`moduleSourceName\` to make sure variable alias works. This option does not do that so it's less safe.`, (val: string) => val.split(',') ) + .option( + '--additional-function-names ', + `Additional function names to extract messages from, e.g: \`['$t']\`.`, + (val: string) => val.split(',') + ) .option( '--extract-from-format-message-call', `Opt-in to extract from \`intl.formatMessage\` call with the same restrictions, e.g: has @@ -124,6 +129,7 @@ to be called with object literal such as \`intl.formatMessage({ id: 'foo', defau extractSourceLocation: cmdObj.extractSourceLocation, removeDefaultMessage: cmdObj.removeDefaultMessage, additionalComponentNames: cmdObj.additionalComponentNames, + additionalFunctionNames: cmdObj.additionalFunctionNames, extractFromFormatMessageCall: cmdObj.extractFromFormatMessageCall, throws: cmdObj.throws, pragma: cmdObj.pragma, diff --git a/packages/cli/src/extract.ts b/packages/cli/src/extract.ts index 54fa59c7b4..443411cc51 100644 --- a/packages/cli/src/extract.ts +++ b/packages/cli/src/extract.ts @@ -2,14 +2,14 @@ import {warn, getStdinAsString} from './console_utils'; import {readFile, outputFile} from 'fs-extra'; import { interpolateName, - transform, Opts, MessageDescriptor, } from '@formatjs/ts-transformer'; -import ts from 'typescript'; + import {resolveBuiltinFormatter, Formatter} from './formatters'; import stringify from 'json-stable-stringify'; - +import {parseFile} from './vue_extractor'; +import {parseScript} from './parse_script'; export interface ExtractionResult> { /** * List of extracted messages @@ -93,6 +93,10 @@ function processFile( opts = { ...opts, + additionalComponentNames: [ + '$formatMessage', + ...(opts.additionalComponentNames || []), + ], onMsgExtracted(_, msgs) { if (opts.extractSourceLocation) { msgs = msgs.map(msg => ({ @@ -125,39 +129,12 @@ function processFile( ), }; } - let output; - try { - output = ts.transpileModule(source, { - compilerOptions: { - allowJs: true, - target: ts.ScriptTarget.ESNext, - noEmit: true, - experimentalDecorators: true, - }, - reportDiagnostics: true, - fileName: fn, - transformers: { - before: [transform(opts)], - }, - }); - } catch (e) { - e.message = `Error processing file ${fn} -${e.message || ''}`; - throw e; - } - if (output.diagnostics) { - const errs = output.diagnostics.filter( - d => d.category === ts.DiagnosticCategory.Error - ); - if (errs.length) { - throw new Error( - ts.formatDiagnosticsWithColorAndContext(errs, { - getCanonicalFileName: fileName => fileName, - getCurrentDirectory: () => process.cwd(), - getNewLine: () => ts.sys.newLine, - }) - ); - } + + const scriptParseFn = parseScript(opts, fn); + if (fn.endsWith('.vue')) { + parseFile(source, fn, scriptParseFn); + } else { + scriptParseFn(source); } if (meta) { diff --git a/packages/cli/src/parse_script.ts b/packages/cli/src/parse_script.ts new file mode 100644 index 0000000000..bbac881093 --- /dev/null +++ b/packages/cli/src/parse_script.ts @@ -0,0 +1,46 @@ +import {Opts, transform} from '@formatjs/ts-transformer'; +import ts from 'typescript'; + +/** + * Invoid TypeScript module transpilation with our TS transformer + * @param opts Formatjs TS Transformer opt + * @param fn filename + */ +export function parseScript(opts: Opts, fn?: string) { + return (source: string) => { + let output; + try { + output = ts.transpileModule(source, { + compilerOptions: { + allowJs: true, + target: ts.ScriptTarget.ESNext, + noEmit: true, + experimentalDecorators: true, + }, + reportDiagnostics: true, + fileName: fn, + transformers: { + before: [transform(opts)], + }, + }); + } catch (e) { + e.message = `Error processing file ${fn} +${e.message || ''}`; + throw e; + } + if (output.diagnostics) { + const errs = output.diagnostics.filter( + d => d.category === ts.DiagnosticCategory.Error + ); + if (errs.length) { + throw new Error( + ts.formatDiagnosticsWithColorAndContext(errs, { + getCanonicalFileName: fileName => fileName, + getCurrentDirectory: () => process.cwd(), + getNewLine: () => ts.sys.newLine, + }) + ); + } + } + }; +} diff --git a/packages/cli/src/vue_extractor.ts b/packages/cli/src/vue_extractor.ts new file mode 100644 index 0000000000..f6eb8f0e9d --- /dev/null +++ b/packages/cli/src/vue_extractor.ts @@ -0,0 +1,59 @@ +import {parse} from '@vue/compiler-sfc'; +import { + TemplateChildNode, + NodeTypes, + SimpleExpressionNode, +} from '@vue/compiler-core'; + +export type ScriptParseFn = (source: string) => void; + +function walk(node: TemplateChildNode, visitor: (node: any) => void) { + if ( + node.type === NodeTypes.TEXT || + node.type === NodeTypes.COMMENT || + node.type === NodeTypes.IF || + node.type === NodeTypes.TEXT_CALL + ) { + return; + } + visitor(node); + + if (node.type === NodeTypes.INTERPOLATION) { + visitor(node.content); + } else { + node.children.forEach((n: any) => walk(n, visitor)); + } +} + +function templateSimpleExpressionNodeVisitor(parseScriptFn: ScriptParseFn) { + return (n: any) => { + if (n.type !== NodeTypes.SIMPLE_EXPRESSION) { + return; + } + + const {content} = n as SimpleExpressionNode; + parseScriptFn(content); + }; +} + +export function parseFile( + source: string, + filename: string, + parseScriptFn: ScriptParseFn +): any { + const {descriptor, errors} = parse(source, { + filename, + }); + if (errors.length) { + throw errors[0]; + } + const {script, template} = descriptor; + + if (template) { + walk(template.ast, templateSimpleExpressionNodeVisitor(parseScriptFn)); + } + + if (script) { + parseScriptFn(script.content); + } +} diff --git a/packages/cli/tests/extract/__snapshots__/integration.test.ts.snap b/packages/cli/tests/extract/__snapshots__/integration.test.ts.snap index bca26d56a0..8e226c9012 100644 --- a/packages/cli/tests/extract/__snapshots__/integration.test.ts.snap +++ b/packages/cli/tests/extract/__snapshots__/integration.test.ts.snap @@ -666,3 +666,16 @@ Object { ", } `; + +exports[`vue 1`] = ` +Object { + "1ebd4": Object { + "defaultMessage": "in script", + "description": "in script desc", + }, + "f6d14": Object { + "defaultMessage": "in template", + "description": "in template desc", + }, +} +`; diff --git a/packages/cli/tests/extract/integration.test.ts b/packages/cli/tests/extract/integration.test.ts index 90c04a5b2e..e8acde4656 100644 --- a/packages/cli/tests/extract/integration.test.ts +++ b/packages/cli/tests/extract/integration.test.ts @@ -196,3 +196,11 @@ test('invalid syntax should throw', async () => { ); }).rejects.toThrowError('TS1005'); }, 20000); + +test('vue', async () => { + const {stdout} = await exec( + `${BIN_PATH} extract '${join(__dirname, 'vue/*.vue')}'` + ); + + expect(JSON.parse(stdout)).toMatchSnapshot(); +}); diff --git a/packages/cli/tests/extract/vue/comp.vue b/packages/cli/tests/extract/vue/comp.vue new file mode 100644 index 0000000000..b451c98afa --- /dev/null +++ b/packages/cli/tests/extract/vue/comp.vue @@ -0,0 +1,17 @@ + + + diff --git a/packages/cli/tests/extract/vue_extractor.test.ts b/packages/cli/tests/extract/vue_extractor.test.ts new file mode 100644 index 0000000000..70f79fcb09 --- /dev/null +++ b/packages/cli/tests/extract/vue_extractor.test.ts @@ -0,0 +1,50 @@ +import {MessageDescriptor} from '@formatjs/ts-transformer'; +import {parseScript} from '../../src/parse_script'; +import {parseFile} from '../../src/vue_extractor'; + +test('vue_extractor', function () { + let messages: MessageDescriptor[] = []; + + parseFile( + ` + + + + `, + 'comp.vue', + parseScript({ + additionalFunctionNames: ['$formatMessage'], + extractFromFormatMessageCall: true, + onMsgExtracted(_, msgs) { + messages = messages.concat(msgs); + }, + overrideIdFn: '[contenthash:5]', + }) + ); + expect(messages).toEqual([ + { + defaultMessage: 'in template', + description: 'in template desc', + id: 'f6d14', + }, + { + defaultMessage: 'in script', + description: 'in script desc', + id: '1ebd4', + }, + ]); +}); diff --git a/website/docs/tooling/cli.md b/website/docs/tooling/cli.md index 9218a32733..5c025533fe 100644 --- a/website/docs/tooling/cli.md +++ b/website/docs/tooling/cli.md @@ -92,7 +92,7 @@ formatjs extract --help For example: ```sh -formatjs extract "src/**/*.{ts,tsx}" --out-file lang.json +formatjs extract "src/**/*.{ts,tsx,vue}" --out-file lang.json ``` :::caution @@ -130,6 +130,10 @@ Whether the metadata about the location of the message in the source file should Additional component names to extract messages from, e.g: `['FormattedFooBarMessage']`. **NOTE**: By default we check for the fact that `FormattedMessage` is imported from `moduleSourceName` to make sure variable alias works. This option does not do that so it's less safe. +### `--additional-function-names [comma-separated-names]` + +Additional function names to extract messages from, e.g: `['$t']`. + ### `--extract-from-format-message-call` Opt-in to extract from `intl.formatMessage` call with the same restrictions, e.g: has to be called with object literal such as `intl.formatMessage({ id: 'foo', defaultMessage: 'bar', description: 'baz'})` (default: `false`) diff --git a/yarn.lock b/yarn.lock index 3a1e9e225f..98d28d0d51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3718,7 +3718,7 @@ "@typescript-eslint/types" "4.11.1" eslint-visitor-keys "^2.0.0" -"@vue/compiler-core@3.0.5": +"@vue/compiler-core@3.0.5", "@vue/compiler-core@^3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.5.tgz#a6e54cabe9536e74c6513acd2649f311af1d43ac" integrity sha512-iFXwk2gmU/GGwN4hpBwDWWMLvpkIejf/AybcFtlQ5V1ur+5jwfBaV0Y1RXoR6ePfBPJixtKZ3PmN+M+HgMAtfQ== @@ -3737,6 +3737,36 @@ "@vue/compiler-core" "3.0.5" "@vue/shared" "3.0.5" +"@vue/compiler-sfc@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.0.5.tgz#3ae08e60244a72faf9598361874fb7bdb5b1d37c" + integrity sha512-uOAC4X0Gx3SQ9YvDC7YMpbDvoCmPvP0afVhJoxRotDdJ+r8VO3q4hFf/2f7U62k4Vkdftp6DVni8QixrfYzs+w== + dependencies: + "@babel/parser" "^7.12.0" + "@babel/types" "^7.12.0" + "@vue/compiler-core" "3.0.5" + "@vue/compiler-dom" "3.0.5" + "@vue/compiler-ssr" "3.0.5" + "@vue/shared" "3.0.5" + consolidate "^0.16.0" + estree-walker "^2.0.1" + hash-sum "^2.0.0" + lru-cache "^5.1.1" + magic-string "^0.25.7" + merge-source-map "^1.1.0" + postcss "^7.0.32" + postcss-modules "^3.2.2" + postcss-selector-parser "^6.0.4" + source-map "^0.6.1" + +"@vue/compiler-ssr@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.0.5.tgz#7661ad891a0be948726c7f7ad1e425253c587b83" + integrity sha512-Wm//Kuxa1DpgjE4P9W0coZr8wklOfJ35Jtq61CbU+t601CpPTK4+FL2QDBItaG7aoUUDCWL5nnxMkuaOgzTBKg== + dependencies: + "@vue/compiler-dom" "3.0.5" + "@vue/shared" "3.0.5" + "@vue/reactivity@3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.0.5.tgz#e3789e4d523d845f9ae0b4d770e2b45594742fd2" @@ -4862,7 +4892,7 @@ blob@0.0.5: resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== -bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5, bluebird@^3.7.1: +bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5, bluebird@^3.7.1, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -6091,6 +6121,13 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= +consolidate@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16" + integrity sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ== + dependencies: + bluebird "^3.7.2" + constant-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1" @@ -8806,6 +8843,13 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" +generic-names@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-2.0.1.tgz#f8a378ead2ccaa7a34f0317b05554832ae41b872" + integrity sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ== + dependencies: + loader-utils "^1.1.0" + genfun@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" @@ -9402,6 +9446,11 @@ hash-base@^3.0.0: readable-stream "^3.6.0" safe-buffer "^5.2.0" +hash-sum@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-2.0.0.tgz#81d01bb5de8ea4a214ad5d6ead1b523460b0b45a" + integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg== + hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" @@ -12309,6 +12358,13 @@ merge-source-map@1.0.4: dependencies: source-map "^0.5.6" +merge-source-map@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== + dependencies: + source-map "^0.6.1" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -14331,6 +14387,21 @@ postcss-modules-values@^3.0.0: icss-utils "^4.0.0" postcss "^7.0.6" +postcss-modules@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-3.2.2.tgz#ee390de0f9f18e761e1778dfb9be26685c02c51f" + integrity sha512-JQ8IAqHELxC0N6tyCg2UF40pACY5oiL6UpiqqcIFRWqgDYO8B0jnxzoQ0EOpPrWXvcpu6BSbQU/3vSiq7w8Nhw== + dependencies: + generic-names "^2.0.1" + icss-replace-symbols "^1.1.0" + lodash.camelcase "^4.3.0" + postcss "^7.0.32" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^3.0.2" + postcss-modules-scope "^2.2.0" + postcss-modules-values "^3.0.0" + string-hash "^1.1.1" + postcss-nesting@^7.0.0: version "7.0.1" resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.1.tgz#b50ad7b7f0173e5b5e3880c3501344703e04c052" @@ -14579,7 +14650,7 @@ postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.0.4" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== @@ -16952,6 +17023,11 @@ string-argv@0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== +string-hash@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" + integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs= + string-length@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1"