@transifex/cli
Advanced tools
Comparing version 2.1.2 to 2.2.0-alpha.0
@@ -1,1 +0,1 @@ | ||
{"version":"2.1.2","commands":{"invalidate":{"id":"invalidate","description":"invalidate and refresh CDS cache\nContent for delivery is cached in CDS and refreshed automatically every hour.\nThis command triggers a refresh of cached content on the fly.\n\nBy default, invalidation does not remove existing cached content, but\nstarts the process of updating with latest translations from Transifex.\n\nPassing the --purge option, cached content will be forced to be deleted,\nhowever use that with caution, as it may introduce downtime of\ntranslation delivery to the apps until fresh content is cached in the CDS.\n\nTo invalidate translations some environment variables must be set:\nTRANSIFEX_TOKEN=<Transifex Native Project Token>\nTRANSIFEX_SECRET=<Transifex Native Project Secret>\n(optional) TRANSIFEX_CDS_HOST=<CDS HOST>\n\nor passed as --token=<TOKEN> --secret=<SECRET> parameters\n\nDefault CDS Host is https://cds.svc.transifex.net\n\nExamples:\ntxjs-cli invalidate\ntxjs-cli invalidate --purge\ntxjs-cli invalidate --token=mytoken --secret=mysecret\nTRANSIFEX_TOKEN=mytoken TRANSIFEX_SECRET=mysecret txjs-cli invalidate\n","pluginName":"@transifex/cli","pluginType":"core","aliases":[],"flags":{"purge":{"name":"purge","type":"boolean","description":"force delete CDS cached content","allowNo":false},"token":{"name":"token","type":"option","description":"native project public token","default":""},"secret":{"name":"secret","type":"option","description":"native project secret","default":""},"cds-host":{"name":"cds-host","type":"option","description":"CDS host URL","default":""}},"args":[]},"push":{"id":"push","description":"detect and push source content to Transifex\nParse .js, .ts, .jsx, .tsx and .html files and detect phrases marked for\ntranslation by Transifex Native toolkit for Javascript and\nupload them to Transifex for translation.\n\nTo push content some environment variables must be set:\nTRANSIFEX_TOKEN=<Transifex Native Project Token>\nTRANSIFEX_SECRET=<Transifex Native Project Secret>\n(optional) TRANSIFEX_CDS_HOST=<CDS HOST>\n\nor passed as --token=<TOKEN> --secret=<SECRET> parameters\n\nDefault CDS Host is https://cds.svc.transifex.net\n\nExamples:\ntxjs-cli push -v\ntxjs-cli push src/\ntxjs-cli push /home/repo/src\ntxjs-cli push \"*.js\"\ntxjs-cli push --dry-run\ntxjs-cli push --no-wait\ntxjs-cli push --key-generator=hash\ntxjs-cli push --append-tags=\"master,release:2.5\"\ntxjs-cli push --with-tags-only=\"home,error\"\ntxjs-cli push --without-tags-only=\"custom\"\ntxjs-cli push --token=mytoken --secret=mysecret\nTRANSIFEX_TOKEN=mytoken TRANSIFEX_SECRET=mysecret txjs-cli push\n","pluginName":"@transifex/cli","pluginType":"core","aliases":[],"flags":{"dry-run":{"name":"dry-run","type":"boolean","description":"dry run, do not push to Transifex","allowNo":false},"verbose":{"name":"verbose","type":"boolean","char":"v","description":"verbose output","allowNo":false},"purge":{"name":"purge","type":"boolean","description":"purge content on Transifex","allowNo":false},"no-wait":{"name":"no-wait","type":"boolean","description":"disable polling for upload results","allowNo":false},"token":{"name":"token","type":"option","description":"native project public token","default":""},"secret":{"name":"secret","type":"option","description":"native project secret","default":""},"append-tags":{"name":"append-tags","type":"option","description":"append tags to strings","default":""},"with-tags-only":{"name":"with-tags-only","type":"option","description":"push strings with specific tags","default":""},"without-tags-only":{"name":"without-tags-only","type":"option","description":"push strings without specific tags","default":""},"cds-host":{"name":"cds-host","type":"option","description":"CDS host URL","default":""},"key-generator":{"name":"key-generator","type":"option","description":"use hashed or source based keys","options":["source","hash"],"default":"source"}},"args":[{"name":"pattern","description":"file pattern to scan for strings","required":false,"default":"**/*.{js,jsx,ts,tsx}"}]}}} | ||
{"version":"2.2.0-alpha.0","commands":{"invalidate":{"id":"invalidate","description":"invalidate and refresh CDS cache\nContent for delivery is cached in CDS and refreshed automatically every hour.\nThis command triggers a refresh of cached content on the fly.\n\nBy default, invalidation does not remove existing cached content, but\nstarts the process of updating with latest translations from Transifex.\n\nPassing the --purge option, cached content will be forced to be deleted,\nhowever use that with caution, as it may introduce downtime of\ntranslation delivery to the apps until fresh content is cached in the CDS.\n\nTo invalidate translations some environment variables must be set:\nTRANSIFEX_TOKEN=<Transifex Native Project Token>\nTRANSIFEX_SECRET=<Transifex Native Project Secret>\n(optional) TRANSIFEX_CDS_HOST=<CDS HOST>\n\nor passed as --token=<TOKEN> --secret=<SECRET> parameters\n\nDefault CDS Host is https://cds.svc.transifex.net\n\nExamples:\ntxjs-cli invalidate\ntxjs-cli invalidate --purge\ntxjs-cli invalidate --token=mytoken --secret=mysecret\nTRANSIFEX_TOKEN=mytoken TRANSIFEX_SECRET=mysecret txjs-cli invalidate\n","pluginName":"@transifex/cli","pluginType":"core","aliases":[],"flags":{"purge":{"name":"purge","type":"boolean","description":"force delete CDS cached content","allowNo":false},"token":{"name":"token","type":"option","description":"native project public token","default":""},"secret":{"name":"secret","type":"option","description":"native project secret","default":""},"cds-host":{"name":"cds-host","type":"option","description":"CDS host URL","default":""}},"args":[]},"push":{"id":"push","description":"detect and push source content to Transifex\nParse .js, .ts, .jsx, .tsx and .html files and detect phrases marked for\ntranslation by Transifex Native toolkit for Javascript and\nupload them to Transifex for translation.\n\nTo push content some environment variables must be set:\nTRANSIFEX_TOKEN=<Transifex Native Project Token>\nTRANSIFEX_SECRET=<Transifex Native Project Secret>\n(optional) TRANSIFEX_CDS_HOST=<CDS HOST>\n\nor passed as --token=<TOKEN> --secret=<SECRET> parameters\n\nDefault CDS Host is https://cds.svc.transifex.net\n\nExamples:\ntxjs-cli push -v\ntxjs-cli push src/\ntxjs-cli push /home/repo/src\ntxjs-cli push \"*.js\"\ntxjs-cli push --dry-run\ntxjs-cli push --no-wait\ntxjs-cli push --key-generator=hash\ntxjs-cli push --append-tags=\"master,release:2.5\"\ntxjs-cli push --with-tags-only=\"home,error\"\ntxjs-cli push --without-tags-only=\"custom\"\ntxjs-cli push --token=mytoken --secret=mysecret\nTRANSIFEX_TOKEN=mytoken TRANSIFEX_SECRET=mysecret txjs-cli push\n","pluginName":"@transifex/cli","pluginType":"core","aliases":[],"flags":{"dry-run":{"name":"dry-run","type":"boolean","description":"dry run, do not push to Transifex","allowNo":false},"verbose":{"name":"verbose","type":"boolean","char":"v","description":"verbose output","allowNo":false},"purge":{"name":"purge","type":"boolean","description":"purge content on Transifex","allowNo":false},"no-wait":{"name":"no-wait","type":"boolean","description":"disable polling for upload results","allowNo":false},"token":{"name":"token","type":"option","description":"native project public token","default":""},"secret":{"name":"secret","type":"option","description":"native project secret","default":""},"append-tags":{"name":"append-tags","type":"option","description":"append tags to strings","default":""},"with-tags-only":{"name":"with-tags-only","type":"option","description":"push strings with specific tags","default":""},"without-tags-only":{"name":"without-tags-only","type":"option","description":"push strings without specific tags","default":""},"cds-host":{"name":"cds-host","type":"option","description":"CDS host URL","default":""},"key-generator":{"name":"key-generator","type":"option","description":"use hashed or source based keys","options":["source","hash"],"default":"source"}},"args":[{"name":"pattern","description":"file pattern to scan for strings","required":false,"default":"**/*.{js,jsx,ts,tsx}"}]}}} |
{ | ||
"name": "@transifex/cli", | ||
"description": "Transifex Native CLI", | ||
"version": "2.1.2", | ||
"version": "2.2.0-alpha.0", | ||
"author": "Transifex", | ||
@@ -30,3 +30,3 @@ "keywords": [ | ||
"@oclif/plugin-help": "^3.2.1", | ||
"@transifex/native": "^2.1.2", | ||
"@transifex/native": "^2.2.0-alpha.0", | ||
"angular-html-parser": "^1.7.1", | ||
@@ -38,3 +38,4 @@ "axios": "^0.24.0", | ||
"lodash": "^4.17.21", | ||
"shelljs": "^0.8.4" | ||
"shelljs": "^0.8.4", | ||
"vue-template-compiler": "^2.6.14" | ||
}, | ||
@@ -41,0 +42,0 @@ "devDependencies": { |
@@ -85,3 +85,3 @@ # Transifex Native CLI | ||
ARGUMENTS | ||
PATTERN [default: **/*.{js,jsx,ts,tsx}] file pattern to scan for strings | ||
PATTERN [default: **/*.{js,jsx,ts,tsx,vue}] file pattern to scan for strings | ||
@@ -102,3 +102,3 @@ OPTIONS | ||
DESCRIPTION | ||
Parse .js, .ts, .jsx, .tsx and .html files and detect phrases marked for | ||
Parse .js, .ts, .jsx, .tsx, .html and .vue files and detect phrases marked for | ||
translation by Transifex Native toolkit for Javascript and | ||
@@ -105,0 +105,0 @@ upload them to Transifex for translation. |
/* eslint no-underscore-dangle: 0 */ | ||
const ngHtmlParser = require('angular-html-parser'); | ||
const fs = require('fs'); | ||
const babelParser = require('@babel/parser'); | ||
const babelTraverse = require('@babel/traverse').default; | ||
const _ = require('lodash'); | ||
const path = require('path'); | ||
const { generateKey, generateHashedKey } = require('@transifex/native'); | ||
const { mergePayload } = require('./merge'); | ||
const { stringToArray, mergeArrays } = require('./utils'); | ||
const { parseHTMLTemplateFile } = require('./parsers/angularHTML'); | ||
const { babelExtractPhrases } = require('./parsers/babel'); | ||
const { extractVuePhrases } = require('./parsers/vue'); | ||
/** | ||
* Create an extraction payload | ||
* | ||
* @param {String} string | ||
* @param {Object} params | ||
* @param {String} params._context | ||
* @param {String} params._comment | ||
* @param {Number} params._charlimit | ||
* @param {Number} params._tags | ||
* @param {String} occurence | ||
* @param {Object} options | ||
*/ | ||
function createPayload(string, params, occurence, options = {}) { | ||
return { | ||
string, | ||
key: options.useHashedKeys | ||
? generateHashedKey(string, params) | ||
: generateKey(string, params), | ||
meta: _.omitBy({ | ||
context: stringToArray(params._context || params.context), | ||
developer_comment: params._comment || params.comment, | ||
character_limit: params._charlimit || params.charlimit | ||
? parseInt(params._charlimit || params.charlimit, 10) | ||
: undefined, | ||
tags: mergeArrays(stringToArray(params._tags || params.tags), options.appendTags), | ||
occurrences: [occurence], | ||
}, _.isNil), | ||
}; | ||
} | ||
/** | ||
* Check if payload coming from createPayload is valid based on tag filters | ||
* | ||
* @param {Object} payload | ||
* @param {String[]} options.filterWithTags | ||
* @param {String[]} options.filterWithoutTags | ||
* @returns {Boolean} | ||
*/ | ||
function isPayloadValid(payload, options = {}) { | ||
const { filterWithTags, filterWithoutTags } = options; | ||
let isValid = true; | ||
_.each(filterWithTags, (tag) => { | ||
if (!_.includes(payload.meta.tags, tag)) { | ||
isValid = false; | ||
} | ||
}); | ||
_.each(filterWithoutTags, (tag) => { | ||
if (_.includes(payload.meta.tags, tag)) { | ||
isValid = false; | ||
} | ||
}); | ||
return isValid; | ||
} | ||
/** | ||
* Check if callee is a valid Transifex Native function | ||
* | ||
* @param {*} node | ||
* @returns {Boolean} | ||
*/ | ||
function isTransifexCall(node) { | ||
const { callee } = node; | ||
if (!callee) return false; | ||
if (_.includes(['t', 'useT'], callee.name)) { return true; } | ||
if (!callee.object || !callee.property) return false; | ||
if (callee.property.name === 'translate') return true; | ||
return false; | ||
} | ||
/** | ||
* Global regexp to find use of TranslatePipe. | ||
*/ | ||
const pipeRegexpG = /{{\s*?['|"]([\s\S]+?)['|"]\s*?\|\s*?translate\s*?:?({[\s\S]*?})?\s*?}}/gi; | ||
/** | ||
* Regexp to find use of TranslatePipe and match with capture groups. | ||
*/ | ||
const pipeRegexp = /{{\s*?['|"]([\s\S]+?)['|"]\s*?\|\s*?translate\s*?:?({[\s\S]*?})?\s*?}}/i; | ||
/** | ||
* Regexp to find use of TranslatePipe in Attributes; | ||
*/ | ||
const pipeBindingRegexp = /'([\s\S]+?)'\s*?\|\s*?translate\s*?:?({[\s\S]*?})?/i; | ||
/** | ||
* Loosely parses string (from HTML) to an object. | ||
* | ||
* According to Mozilla a bit better than eval(). | ||
* | ||
* @param {str} obj | ||
* @returns {*} | ||
*/ | ||
function looseJsonParse(obj) { | ||
let parsed; | ||
try { | ||
// eslint-disable-next-line no-new-func | ||
parsed = Function(`"use strict";return (${obj})`)(); | ||
} catch (err) { | ||
parsed = {}; | ||
} | ||
return parsed; | ||
} | ||
/** | ||
* Parse an HTML file and detects T/UT tags and usage of TranslatePipe | ||
* | ||
* @param {Object} HASHES | ||
* @param {String} filename | ||
* @param {String} relativeFile | ||
* @param {String[]} appendTags | ||
* @param {Object} options | ||
* @returns void | ||
*/ | ||
function parseHTMLTemplateFile(HASHES, filename, relativeFile, options) { | ||
const TXComponents = []; | ||
const TXTemplateStrings = []; | ||
function parseTemplateText(text) { | ||
const textStr = _.trim(String(text)); | ||
if (textStr.length) { | ||
const results = String(textStr).match(pipeRegexpG); | ||
if (results && results.length) { | ||
_.each(results, (result) => { | ||
const lineResult = result.match(pipeRegexp); | ||
if (lineResult) { | ||
const string = lineResult[1]; | ||
const paramsStr = lineResult[2]; | ||
const params = looseJsonParse(paramsStr) || {}; | ||
if (string && params) { | ||
TXTemplateStrings.push({ | ||
string, | ||
params, | ||
}); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
function parseTemplateBindingText(text) { | ||
const textStr = _.trim(String(text)); | ||
if (textStr.length) { | ||
const result = textStr.match(pipeBindingRegexp); | ||
if (result) { | ||
const string = result[1]; | ||
const paramsStr = result[2]; | ||
const params = looseJsonParse(paramsStr) || {}; | ||
if (string && params) { | ||
TXTemplateStrings.push({ | ||
string, | ||
params, | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
function parseTemplateNode(children) { | ||
if (children) { | ||
_.each(children, (child) => { | ||
if (child.name === 'T' || child.name === 'UT') { | ||
TXComponents.push(child); | ||
} else if (child.type === 'text') { | ||
parseTemplateText(child.value); | ||
} else if (child.attrs && child.attrs.length > 0) { | ||
const attributes = child.attrs.filter((a) => a.value.includes('translate')); | ||
_.each(attributes, (attr) => { | ||
parseTemplateBindingText(attr.value); | ||
}); | ||
} | ||
parseTemplateNode(child.children); | ||
}); | ||
} | ||
} | ||
const data = fs.readFileSync(filename, 'utf8'); | ||
const { rootNodes, errors } = ngHtmlParser.parse(data); | ||
if (errors.length) return; | ||
parseTemplateNode(rootNodes); | ||
_.each(TXComponents, (txcmp) => { | ||
let string = ''; | ||
let key = ''; | ||
const params = {}; | ||
if (txcmp.attrs) { | ||
_.each(txcmp.attrs, (attribute) => { | ||
if (attribute.name === 'str') { | ||
string = attribute.value; | ||
} else if (attribute.name === 'key') { | ||
key = attribute.value; | ||
} else { | ||
params[attribute.name] = attribute.value; | ||
} | ||
}); | ||
} | ||
if (string) { | ||
const partial = createPayload(string, params, relativeFile, options); | ||
if (!isPayloadValid(partial, options)) return; | ||
mergePayload(HASHES, { | ||
[key || partial.key]: { | ||
string: partial.string, | ||
meta: partial.meta, | ||
}, | ||
}); | ||
} | ||
}); | ||
_.each(TXTemplateStrings, (txStr) => { | ||
let key = ''; | ||
if (txStr.params.key) { | ||
key = txStr.params.key; | ||
} | ||
const partial = createPayload(txStr.string, txStr.params, relativeFile, options); | ||
if (!isPayloadValid(partial, options)) return; | ||
mergePayload(HASHES, { | ||
[key || partial.key]: { | ||
string: partial.string, | ||
meta: partial.meta, | ||
}, | ||
}); | ||
}); | ||
} | ||
function _parse(source) { | ||
try { | ||
return babelParser.parse( | ||
source, | ||
{ | ||
sourceType: 'unambiguous', | ||
plugins: [ | ||
'decorators-legacy', | ||
'classProperties', | ||
'jsx', | ||
'typescript', | ||
], | ||
}, | ||
); | ||
} catch (e) { | ||
return babelParser.parse( | ||
source, | ||
{ | ||
sourceType: 'unambiguous', | ||
plugins: [ | ||
'decorators-legacy', | ||
'jsx', | ||
'flow', | ||
], | ||
}, | ||
); | ||
} | ||
} | ||
/** | ||
* Find value bound to some identifier with passed name. | ||
* | ||
* @param {Object} scope AST Scope to use for lookup. | ||
* @param {String} name Name of the identifier. | ||
* @returns {String?} declared value or null. | ||
*/ | ||
function findIdentifierValue(scope, name) { | ||
if (!scope) return null; | ||
if (scope.bindings[name]) { | ||
const binding = scope.bindings[name]; | ||
if (binding.kind !== 'const') return null; | ||
const { node } = binding.path; | ||
if (node.type === 'VariableDeclarator' && node.init) { | ||
// eslint-disable-next-line no-use-before-define | ||
return findDeclaredValue(scope, node.init); | ||
} | ||
} | ||
if (scope.path.parentPath) { | ||
return findIdentifierValue(scope.path.parentPath.scope, name); | ||
} | ||
return null; | ||
} | ||
/** | ||
* Find declared value bound to identifier defined in init. | ||
* | ||
* @param {Object} scope AST Scope to use for lookup. | ||
* @param {Object} init AST Node to work with. | ||
* @returns {String?} declared value or null. | ||
*/ | ||
function findDeclaredValue(scope, init) { | ||
if (!init) return null; | ||
if (init.type === 'StringLiteral') { | ||
return init.value; | ||
} | ||
if (init.type === 'NumericLiteral') { | ||
return init.value; | ||
} | ||
if (init.type === 'JSXExpressionContainer') { | ||
return findDeclaredValue(scope, init.expression); | ||
} | ||
if (init.type === 'Identifier') { | ||
return findIdentifierValue(scope, init.name); | ||
} | ||
if (init.type === 'BinaryExpression' && init.operator === '+') { | ||
const left = findDeclaredValue(scope, init.left); | ||
const right = findDeclaredValue(scope, init.right); | ||
if (_.isString(left) && _.isString(right)) { | ||
return left + right; | ||
} | ||
} | ||
if (init.type === 'TemplateLiteral') { | ||
const expressions = init.expressions.map((node) => findDeclaredValue(scope, node)); | ||
if (expressions.includes(null)) return null; | ||
const elements = init.quasis.flatMap( | ||
({ tail, value }, i) => (tail ? value.raw : [value.raw, expressions[i]]), | ||
); | ||
return elements.join(''); | ||
} | ||
return null; | ||
} | ||
/** | ||
* Parse file and extract phrases using AST | ||
@@ -376,118 +25,8 @@ * | ||
const source = fs.readFileSync(file, 'utf8'); | ||
if (path.extname(file) !== '.html') { | ||
const ast = _parse(source); | ||
babelTraverse(ast, { | ||
// T / UT functions | ||
CallExpression({ node, scope }) { | ||
// Check if node is a Transifex function | ||
if (!isTransifexCall(node)) return; | ||
if (_.isEmpty(node.arguments)) return; | ||
// Try to find the value of first argument | ||
const string = findDeclaredValue(scope, node.arguments[0]); | ||
// Verify that at least the string is passed to the function | ||
if (!_.isString(string)) return; | ||
// Extract function parameters | ||
const params = {}; | ||
if ( | ||
node.arguments[1] | ||
&& node.arguments[1].type === 'ObjectExpression' | ||
) { | ||
_.each(node.arguments[1].properties, (prop) => { | ||
// get only string on number params | ||
const value = findDeclaredValue(scope, prop.value); | ||
if (_.isString(value) || _.isNumber(value)) { | ||
params[prop.key.name] = value; | ||
} | ||
}); | ||
} | ||
const partial = createPayload(string, params, relativeFile, options); | ||
if (!isPayloadValid(partial, options)) return; | ||
mergePayload(HASHES, { | ||
[partial.key]: { | ||
string: partial.string, | ||
meta: partial.meta, | ||
}, | ||
}); | ||
}, | ||
// Decorator | ||
Decorator({ node }) { | ||
const elem = node.expression; | ||
if (!elem || !elem.arguments || !elem.arguments.length) return; | ||
if (!node.expression.callee.name === 'T') return; | ||
let string = ''; | ||
let key = ''; | ||
const params = {}; | ||
_.each(node.expression.arguments, (arg) => { | ||
if (arg.type === 'StringLiteral') { | ||
string = arg.value; | ||
} else if (arg.type === 'ObjectExpression') { | ||
_.each(arg.properties, (prop) => { | ||
if (prop.key.name === '_key') { | ||
key = prop.value.value; | ||
} else { | ||
params[prop.key.name] = prop.value.value; | ||
} | ||
}); | ||
} | ||
}); | ||
if (string) { | ||
const partial = createPayload(string, params, relativeFile, options); | ||
if (!isPayloadValid(partial, options)) return; | ||
mergePayload(HASHES, { | ||
[key || partial.key]: { | ||
string: partial.string, | ||
meta: partial.meta, | ||
}, | ||
}); | ||
} | ||
}, | ||
// React component | ||
JSXElement({ node, scope }) { | ||
const elem = node.openingElement; | ||
if (!elem || !elem.name) return; | ||
if (elem.name.name !== 'T' && elem.name.name !== 'UT') return; | ||
let string; | ||
const params = {}; | ||
_.each(elem.attributes, (attr) => { | ||
const property = attr.name && attr.name.name; | ||
if (!property || !attr.value) return; | ||
const attrValue = findDeclaredValue(scope, attr.value); | ||
if (!attrValue) return; | ||
if (property === '_str') { | ||
string = attrValue; | ||
return; | ||
} | ||
params[property] = attrValue; | ||
}); | ||
if (!string) return; | ||
const partial = createPayload(string, params, relativeFile, options); | ||
if (!isPayloadValid(partial, options)) return; | ||
mergePayload(HASHES, { | ||
[partial.key]: { | ||
string: partial.string, | ||
meta: partial.meta, | ||
}, | ||
}); | ||
}, | ||
}); | ||
} else if (path.extname(file) === '.html') { | ||
if (path.extname(file) === '.html') { | ||
parseHTMLTemplateFile(HASHES, file, relativeFile, options); | ||
} else if (path.extname(file) === '.vue') { | ||
extractVuePhrases(HASHES, source, relativeFile, options); | ||
} else if (path.extname(file) !== '.html') { | ||
babelExtractPhrases(HASHES, source, relativeFile, options); | ||
} | ||
@@ -494,0 +33,0 @@ |
@@ -41,3 +41,3 @@ /* eslint-disable no-await-in-loop */ | ||
if (isFolder(filePattern)) { | ||
filePattern = path.join(filePattern, '**/*.{js,jsx,ts,tsx,html}'); | ||
filePattern = path.join(filePattern, '**/*.{js,jsx,ts,tsx,html,vue}'); | ||
} | ||
@@ -44,0 +44,0 @@ |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
46362
17
1119
15
1
10
+ Addedde-indent@1.0.2(transitive)
+ Addedhe@1.2.0(transitive)
+ Addedvue-template-compiler@2.7.16(transitive)