diff --git a/README.md b/README.md index 5558879d..a19d956a 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,50 @@ In addition, we also recommend the following Quasar Framework tutorials: * [Reusable Vue.js Components](doc/COMPONENTS.md) +### Translations +To keep translation files consistent and updated please run **i18n:extract** command before each commit to the GIT repo. + + yarn run i18n:extract + +That CLI command will collect all new translation keys from the JS source code, and will place those keys into all translation files in a proper format. + +Example of the JS code with translations: + +```javascript +const someOptions = { + label: this.$t('Remove message'), + message: this.$t('The {email} will be removed. Are you sure?', { email: this.email }) +} +``` +**Important**: We are trying to avoid usage of the dynamic keys (example below) because it's very difficult to detect and collect automatically. + +Example (anti-pattern): +```javascript +function getTranslatedMessage (weatherState) { + return this.$t('Tooday is ' + weatherState) +} +``` +Try to avoid such code and use text messages with substitution variables (like `email` in example above) or if there are only a couple similar messages, you can use a map object to convert some exact state to exact translation message. +But if it's really impossible to do, and you have to use dynamic keys, you should place such keys to the English translation file manually and execute `i18n:extract` which will do all the rest. + +For example, for the code above, you would need to place next lines into `en.json`: +```JSON +{ + ... + "Today is sunny": "", + "Today is windy": "", + "Today is cloudy": "" +} +``` + +**Note**: if you want to see information about missed or possible unused translation keys you could run **i18n:extract-report** command. It will just display detailed report without any modifications in the language files. + +Keep in mind that some of "Unused translations" keys might be dynamic translation keys which cannot be detected in source code automatically and were added manually. + + yarn run i18n:extract-report + + + ### Add a new page In order to add a new page you need to go along the following steps: diff --git a/bin/vue-i18n-extract/extract.sh b/bin/vue-i18n-extract/extract.sh new file mode 100644 index 00000000..3b4a9d42 --- /dev/null +++ b/bin/vue-i18n-extract/extract.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +if [ "$1" == "report" ]; then + OTHER_PARAMS=() +else + OTHER_PARAMS=(--add --json-sort-keys --json-indent-characters "1,tab" --no-detailed-report) +fi + +node ./bin/vue-i18n-extract/vue-i18n-extract.js report -v './src/**/*.?(js|vue)' -l './src/i18n/*.json' \ + --no-dot-notation --add-value-for-languages en --main-language-to-sync-keys en "${OTHER_PARAMS[@]}" diff --git a/bin/vue-i18n-extract/readme.txt b/bin/vue-i18n-extract/readme.txt new file mode 100644 index 00000000..df902e0d --- /dev/null +++ b/bin/vue-i18n-extract/readme.txt @@ -0,0 +1 @@ +Codebase of the tool is vue-i18n-extract:v1.1.11 package \ No newline at end of file diff --git a/bin/vue-i18n-extract/vue-i18n-extract.js b/bin/vue-i18n-extract/vue-i18n-extract.js new file mode 100644 index 00000000..a140563d --- /dev/null +++ b/bin/vue-i18n-extract/vue-i18n-extract.js @@ -0,0 +1,77 @@ +#!/usr/bin/env node +// vim: set filetype=javascript: + /* eslint-disable */ +'use strict'; +const program = require('commander'); +const { reportCommand } = require('./vue-i18n-extract.umd.js'); + +function increaseDynamic(dummyValue, previous) { + return previous + 1; +} + +program + .command('report', { isDefault: true }) + .description('Create a report from a glob of your Vue.js source files and your language files.') + .requiredOption( + '-v, --vueFiles ', + 'The Vue.js file(s) you want to extract i18n strings from. It can be a path to a folder or to a file. It accepts glob patterns. (ex. *, ?, (pattern|pattern|pattern)', + ) + .requiredOption( + '-l, --languageFiles ', + 'The language file(s) you want to compare your Vue.js file(s) to. It can be a path to a folder or to a file. It accepts glob patterns (ex. *, ?, (pattern|pattern|pattern) ', + ) + .option( + '-o, --output ', + 'Use if you want to create a json file out of your report. (ex. -o output.json)', + ) + .option( + '-a, --add', + 'Use if you want to add missing keys into your json language files.', + ) + .option( + '-d, --dynamic', + 'Use if you want to ignore dynamic keys false-positive. Use it 2 times to get dynamic keys report', + increaseDynamic, + 0 + ) + .option( + '--no-dot-notation', + 'Use if your language keys are flat, contains dots and you do not use a dot character as a separator for nested key structure' + ) + .option( + '--add-value-for-languages ', + 'Duplicate missing key text to it`s value for specified languages. (Pass them as coma-separated parameter value)', + function splitLanguages(value= '') { + return value.split(','); + } + ) + .option( + '--main-language-to-sync-keys ', + 'Use it if you want to check and synchronize all translation keys from some lang file to all others. Mainly it should be English lang file' + ) + .option( + '--json-sort-keys', + 'Use if you want to resort language JSON file`s keys alphabetically', + false + ) + .option( + '--json-indent-characters ', + 'You can specify indentation characters for lines in language JSON files. It will work if JSON language file will be updated, for example because of (--add) option', + function checkValue(value) { + let [amount, character] = value.split(','); + amount = parseInt(amount, 10) + if (isNaN(amount) || amount <= 0 || !['space', 'tab'].includes(character)) { + console.error(`Unknown or incorrect value format for "--json-indent-characters" option: "${value}"`); + process.exit(1) + } + return value + }, + '2,space' + ) + .option( + '--no-detailed-report', + 'Use if you do not want to see detailed list of the keys output on the screen' + ) + .action(reportCommand); + +program.parseAsync(process.argv); diff --git a/bin/vue-i18n-extract/vue-i18n-extract.umd.js b/bin/vue-i18n-extract/vue-i18n-extract.umd.js new file mode 100644 index 00000000..6e63843c --- /dev/null +++ b/bin/vue-i18n-extract/vue-i18n-extract.umd.js @@ -0,0 +1,466 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('path'), require('is-valid-glob'), require('glob'), require('fs'), require('dot-object'), require('js-yaml')) : + typeof define === 'function' && define.amd ? define(['exports', 'path', 'is-valid-glob', 'glob', 'fs', 'dot-object', 'js-yaml'], factory) : + (global = global || self, factory(global.vueI18NExtract = {}, global.path, global.isValidGlob, global.glob, global.fs, global.dotObject, global.jsYaml)); +}(this, (function (exports, path, isValidGlob, glob, fs, dot, yaml) { + path = path && Object.prototype.hasOwnProperty.call(path, 'default') ? path['default'] : path; + isValidGlob = isValidGlob && Object.prototype.hasOwnProperty.call(isValidGlob, 'default') ? isValidGlob['default'] : isValidGlob; + glob = glob && Object.prototype.hasOwnProperty.call(glob, 'default') ? glob['default'] : glob; + fs = fs && Object.prototype.hasOwnProperty.call(fs, 'default') ? fs['default'] : fs; + dot = dot && Object.prototype.hasOwnProperty.call(dot, 'default') ? dot['default'] : dot; + yaml = yaml && Object.prototype.hasOwnProperty.call(yaml, 'default') ? yaml['default'] : yaml; + + function _extends() { + _extends = Object.assign || function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; + }; + + return _extends.apply(this, arguments); + } + + function readVueFiles(src) { + if (!isValidGlob(src)) { + throw new Error(`vueFiles isn't a valid glob pattern.`); + } + + const targetFiles = glob.sync(src); + + if (targetFiles.length === 0) { + throw new Error('vueFiles glob has no files.'); + } + + return targetFiles.map(f => { + const fileName = f.replace(process.cwd(), ''); + return { + fileName, + path: f, + content: fs.readFileSync(f, 'utf8') + }; + }); + } + + function* getMatches(file, regExp, captureGroup = 1) { + while (true) { + const match = regExp.exec(file.content); + + if (match === null) { + break; + } + + const line = (file.content.substring(0, match.index).match(/\n/g) || []).length + 1; + yield { + path: match[captureGroup], + line, + file: file.fileName + }; + } + } + /** + * Extracts translation keys from methods such as `$t` and `$tc`. + * + * - **regexp pattern**: (?:[$ .]tc?)\( + * + * **description**: Matches the sequence t( or tc(, optionally with either “$”, “.” or “ ” in front of it. + * + * - **regexp pattern**: (["'`]) + * + * **description**: 1. capturing group. Matches either “"”, “'”, or “`”. + * + * - **regexp pattern**: ((?:[^\\]|\\.)*?) + * + * **description**: 2. capturing group. Matches anything except a backslash + * *or* matches any backslash followed by any character (e.g. “\"”, “\`”, “\t”, etc.) + * + * - **regexp pattern**: \1 + * + * **description**: matches whatever was matched by capturing group 1 (e.g. the starting string character) + * + * @param file a file object + * @returns a list of translation keys found in `file`. + */ + + + function extractMethodMatches(file) { + const methodRegExp = /(?:[$ .]tc?)\(\s*?(["'`])((?:[^\\]|\\.)*?)\1/g; + return [...getMatches(file, methodRegExp, 2)]; + } + + function extractComponentMatches(file) { + const componentRegExp = /(?: { + const methodMatches = extractMethodMatches(file); + const componentMatches = extractComponentMatches(file); + const directiveMatches = extractDirectiveMatches(file); + return [...accumulator, ...methodMatches, ...componentMatches, ...directiveMatches]; + }, []); + } + + function parseVueFiles(vueFilesPath) { + const filesList = readVueFiles(vueFilesPath); + return extractI18nItemsFromVueFiles(filesList); + } + + function readLangFiles(src) { + if (!isValidGlob(src)) { + throw new Error(`languageFiles isn't a valid glob pattern.`); + } + + const targetFiles = glob.sync(src); + + if (targetFiles.length === 0) { + throw new Error('languageFiles glob has no files.'); + } + + return targetFiles.map(f => { + const langPath = path.resolve(process.cwd(), f); + const extension = langPath.substring(langPath.lastIndexOf('.')).toLowerCase(); + const isJSON = extension === '.json'; + const isYAML = extension === '.yaml' || extension === '.yml'; + let langObj; + + if (isJSON) { + langObj = JSON.parse(fs.readFileSync(langPath, 'utf8')); + } else if (isYAML) { + langObj = yaml.safeLoad(fs.readFileSync(langPath, 'utf8')); + } else { + langObj = eval(fs.readFileSync(langPath, 'utf8')); + } + + const fileName = f.replace(process.cwd(), ''); + return { + fileName, + path: f, + content: JSON.stringify(langObj) + }; + }); + } + + function extractI18nItemsFromLanguageFiles(languageFiles, missingKeysOptions) { + return languageFiles.reduce((accumulator, file) => { + const language = file.fileName.substring(file.fileName.lastIndexOf('/') + 1, file.fileName.lastIndexOf('.')); + + if (!accumulator[language]) { + accumulator[language] = []; + } + + const fileContent = JSON.parse(file.content); + const flattenedObject = dot.dot(fileContent); + Object.keys(flattenedObject).forEach((key, index) => { + var _accumulator$language; + + (_accumulator$language = accumulator[language]) == null ? void 0 : _accumulator$language.push({ + line: index, + path: key, + file: file.fileName, + translated: ((missingKeysOptions.dotNotation ? dot.pick(key, fileContent) : fileContent[key]) || '').trim().length > 0 + }); + }); + return accumulator; + }, {}); + } + + function sortedJSONStringify(obj, indent = 2) { + function flattenEntries([key, value]) { + return typeof value !== 'object' ? [[key, value]] : [[key, value], ...Object.entries(value).flatMap(flattenEntries)]; + } + + const allEntries = Object.entries(obj).flatMap(flattenEntries); + const sorted = allEntries.map(entry => entry[0]).sort(); + return JSON.stringify(obj, sorted, indent); + } + + function writeMissingToLanguage(resolvedLanguageFiles, missingKeys, missingKeysOptions, outputOptions) { + const languageFiles = readLangFiles(resolvedLanguageFiles); + languageFiles.forEach(languageFile => { + const languageFileContent = JSON.parse(languageFile.content); + missingKeys.forEach(item => { + if (item.language && languageFile.fileName.includes(item.language) || !item.language) { + const keyValue = item.language && missingKeysOptions.addValueForLanguages.includes(item.language) ? item.path : ''; + + if (missingKeysOptions.dotNotation) { + dot.str(item.path, keyValue, languageFileContent); + } else { + languageFileContent[item.path] = keyValue; + } + } + }); + const fileExtension = languageFile.fileName.substring(languageFile.fileName.lastIndexOf('.') + 1); + const filePath = path.resolve(process.cwd(), languageFile.fileName); + const stringifiedContent = outputOptions.sortKeys ? sortedJSONStringify(languageFileContent, outputOptions.indentationString) : JSON.stringify(languageFileContent, null, outputOptions.indentationString || 2); + + if (fileExtension === 'json') { + fs.writeFileSync(filePath, stringifiedContent); + } else if (fileExtension === 'js') { + const jsFile = `export default ${stringifiedContent}; \n`; + fs.writeFileSync(filePath, jsFile); + } else if (fileExtension === 'yaml' || fileExtension === 'yml') { + const yamlFile = yaml.safeDump(languageFileContent); + fs.writeFileSync(filePath, yamlFile); + } + }); + } + function parseLanguageFiles(languageFilesPath, missingKeysOptions) { + const filesList = readLangFiles(languageFilesPath); + return extractI18nItemsFromLanguageFiles(filesList, missingKeysOptions); + } + + (function (VueI18NExtractReportTypes) { + VueI18NExtractReportTypes[VueI18NExtractReportTypes["None"] = 0] = "None"; + VueI18NExtractReportTypes[VueI18NExtractReportTypes["Missing"] = 1] = "Missing"; + VueI18NExtractReportTypes[VueI18NExtractReportTypes["Unused"] = 2] = "Unused"; + VueI18NExtractReportTypes[VueI18NExtractReportTypes["Dynamic"] = 4] = "Dynamic"; + VueI18NExtractReportTypes[VueI18NExtractReportTypes["All"] = 7] = "All"; + })(exports.VueI18NExtractReportTypes || (exports.VueI18NExtractReportTypes = {})); + + const mightBeUsedDynamically = function (languageItem, dynamicKeys) { + return dynamicKeys.some(dynamicKey => languageItem.path.includes(dynamicKey.path)); + }; + + function extractI18NReport(parsedVueFiles, parsedLanguageFiles, reportType = exports.VueI18NExtractReportTypes.Missing + exports.VueI18NExtractReportTypes.Unused) { + const missingKeys = []; + const unusedKeys = []; + const dynamicKeys = []; + const dynamicReportEnabled = reportType & exports.VueI18NExtractReportTypes.Dynamic; + Object.keys(parsedLanguageFiles).forEach(language => { + let languageItems = parsedLanguageFiles[language]; + parsedVueFiles.forEach(vueItem => { + const usedByVueItem = function (languageItem) { + return languageItem.path === vueItem.path || languageItem.path.startsWith(vueItem.path + '.'); + }; + + if (dynamicReportEnabled && (vueItem.path.includes('${') || vueItem.path.endsWith('.'))) { + dynamicKeys.push(_extends({}, vueItem, { + language + })); + return; + } + + if (!parsedLanguageFiles[language].some(usedByVueItem)) { + missingKeys.push(_extends({}, vueItem, { + language + })); + } + + languageItems = languageItems.filter(languageItem => dynamicReportEnabled ? !mightBeUsedDynamically(languageItem, dynamicKeys) && !usedByVueItem(languageItem) : !usedByVueItem(languageItem)); + }); + unusedKeys.push(...languageItems.map(item => _extends({}, item, { + language + }))); + }); + let extracts = {}; + + if (reportType & exports.VueI18NExtractReportTypes.Missing) { + extracts = Object.assign(extracts, { + missingKeys + }); + } + + if (reportType & exports.VueI18NExtractReportTypes.Unused) { + extracts = Object.assign(extracts, { + unusedKeys + }); + } + + if (dynamicReportEnabled) { + extracts = Object.assign(extracts, { + dynamicKeys + }); + } + + return extracts; + } + function extractI18NLangFilesSynchronizationReport(parsedLanguageFiles, missingKeysOptions) { + const missingKeys = []; + + if (missingKeysOptions.mainLanguageToSyncKeys) { + const otherLanguages = _extends({}, parsedLanguageFiles); + + delete otherLanguages[missingKeysOptions.mainLanguageToSyncKeys]; + const mainLanguageKeys = parsedLanguageFiles[missingKeysOptions.mainLanguageToSyncKeys] || []; + mainLanguageKeys.forEach(({ + path + }) => { + Object.entries(otherLanguages).forEach(([lang, iItems]) => { + if (!iItems.some(iItem => iItem.path === path)) { + missingKeys.push({ + path, + language: lang + }); + } + }); + }); + } // detect empty translations for some languages (mainly for English) + + + Object.entries(parsedLanguageFiles).forEach(([lang, iItems]) => { + if (missingKeysOptions.addValueForLanguages.includes(lang)) { + iItems.filter(({ + translated + }) => !translated).forEach(({ + path + }) => { + missingKeys.push({ + path, + language: lang + }); + }); + } + }); + return { + missingKeys + }; + } + async function writeReportToFile(report, writePath) { + const reportString = JSON.stringify(report); + return new Promise((resolve, reject) => { + fs.writeFile(writePath, reportString, err => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); + } + + function createI18NReport(vueFiles, languageFiles, command, missingKeysOptions) { + const resolvedVueFiles = path.resolve(process.cwd(), vueFiles); + const resolvedLanguageFiles = path.resolve(process.cwd(), languageFiles); + const parsedVueFiles = parseVueFiles(resolvedVueFiles); + const parsedLanguageFiles = parseLanguageFiles(resolvedLanguageFiles, missingKeysOptions); + const reportType = command.dynamic ? exports.VueI18NExtractReportTypes.All : exports.VueI18NExtractReportTypes.Missing + exports.VueI18NExtractReportTypes.Unused; + const report = extractI18NReport(parsedVueFiles, parsedLanguageFiles, reportType); + const syncReport = extractI18NLangFilesSynchronizationReport(parsedLanguageFiles, missingKeysOptions); // @ts-ignore + + report.missingKeys = [...report.missingKeys, ...syncReport.missingKeys]; + return report; + } + + function getStatisticsForKeys(keysData = []) { + const result = { + uniqueKeys: {}, + keysNumbers: {} + }; + result.uniqueKeys = keysData.reduce((accumulator, currentValue) => { + const langStatistics = accumulator[currentValue.language || '?'] = accumulator[currentValue.language || '?'] || {}; + langStatistics[currentValue.path] = (langStatistics[currentValue.path] || 0) + 1; + return accumulator; + }, { + 'en': {} + }); + result.keysNumbers = Object.keys(result.uniqueKeys).reduce((acc, language) => { + acc[language] = Object.keys(result.uniqueKeys[language]).length; + return acc; + }, {}); + return result; + } + + async function reportCommand(command) { + const { + vueFiles, + languageFiles, + output, + add, + dynamic, + dotNotation, + addValueForLanguages, + mainLanguageToSyncKeys, + jsonSortKeys, + jsonIndentCharacters, + detailedReport + } = command; + const outputOptions = { + sortKeys: jsonSortKeys, + indentationString: ((indentationConfig = '2,space') => { + const charactersMap = { + 'space': ' ', + 'tab': '\t' + }; + const [charactersAmount, character] = indentationConfig.split(','); + return ''.padEnd(Number(charactersAmount), charactersMap[character]); + })(jsonIndentCharacters) + }; + const missingKeysOptions = { + dotNotation, + addValueForLanguages: addValueForLanguages instanceof Array ? addValueForLanguages : addValueForLanguages ? [addValueForLanguages] : [], + mainLanguageToSyncKeys + }; + const report = createI18NReport(vueFiles, languageFiles, command, missingKeysOptions); + + if (detailedReport) { + if (report.missingKeys) console.info('missing keys: '), console.table(report.missingKeys); + if (report.unusedKeys) console.info('unused keys: '), console.table(report.unusedKeys); + if (report.dynamicKeys && dynamic && dynamic > 1) console.info('dynamic detected keys: '), console.table(report.dynamicKeys); + } + + if (output) { + await writeReportToFile(report, path.resolve(process.cwd(), output)); + console.log(`The report has been has been saved to ${output}`); + } + + const summaryReport = _extends({ + 'Missing keys': getStatisticsForKeys(report.missingKeys).keysNumbers, + 'Unused keys': getStatisticsForKeys(report.unusedKeys).keysNumbers + }, dynamic ? { + 'Dynamic keys': getStatisticsForKeys(report.dynamicKeys).keysNumbers + } : {}); + + console.info('\nSummary report:'); + console.table(summaryReport); + + if (add && report.missingKeys && report.missingKeys.length > 0) { + const resolvedLanguageFiles = path.resolve(process.cwd(), languageFiles); + writeMissingToLanguage(resolvedLanguageFiles, report.missingKeys, missingKeysOptions, outputOptions); + console.log('The missing keys have been added to your languages files'); + } + } + + var report = { + __proto__: null, + createI18NReport: createI18NReport, + reportCommand: reportCommand, + readVueFiles: readVueFiles, + parseVueFiles: parseVueFiles, + writeMissingToLanguage: writeMissingToLanguage, + parseLanguageFiles: parseLanguageFiles, + get VueI18NExtractReportTypes () { return exports.VueI18NExtractReportTypes; }, + extractI18NReport: extractI18NReport, + extractI18NLangFilesSynchronizationReport: extractI18NLangFilesSynchronizationReport, + writeReportToFile: writeReportToFile + }; + + var index = _extends({}, report); + + exports.createI18NReport = createI18NReport; + exports.default = index; + exports.extractI18NLangFilesSynchronizationReport = extractI18NLangFilesSynchronizationReport; + exports.extractI18NReport = extractI18NReport; + exports.parseLanguageFiles = parseLanguageFiles; + exports.parseVueFiles = parseVueFiles; + exports.readVueFiles = readVueFiles; + exports.reportCommand = reportCommand; + exports.writeMissingToLanguage = writeMissingToLanguage; + exports.writeReportToFile = writeReportToFile; + +}))); +//# sourceMappingURL=vue-i18n-extract.umd.js.map diff --git a/package.json b/package.json index b7238863..58b9bdb0 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "test:unit:watchAll": "jest --watchAll", "serve:test:coverage": "quasar serve test/jest/coverage/lcov-report/ --port 8788", "concurrently:dev:jest": "concurrently \"quasar dev\" \"jest --watch\"", - "new:store": "quasar new store" + "new:store": "quasar new store", + "i18n:extract": "sh ./bin/vue-i18n-extract/extract.sh", + "i18n:extract-report": "sh ./bin/vue-i18n-extract/extract.sh report" }, "dependencies": { "@quasar/extras": "^1.9.10", @@ -48,6 +50,8 @@ "@quasar/quasar-app-extension-testing": "^1.0.0", "@quasar/quasar-app-extension-testing-unit-jest": "^1.0.1", "babel-eslint": "^10.0.1", + "commander": "6.2.1", + "dot-object": "2.1.4", "eslint": "^6.8.0", "eslint-config-standard": "^14.1.0", "eslint-loader": "^3.0.3", @@ -57,6 +61,9 @@ "eslint-plugin-standard": "^4.0.0", "eslint-plugin-vue": "^6.1.2", "generate-password": "^1.5.1", + "glob": "7.1.6", + "is-valid-glob": "1.0.0", + "js-yaml": "3.14.1", "parseuri": "^0.0.6", "uuid": "8.3.1", "vue-wait": "1.4.8" diff --git a/yarn.lock b/yarn.lock index 4a8c7cac..d8433f96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3377,12 +3377,17 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +commander@6.2.1: + version "6.2.1" + resolved "https://npm-registry.sipwise.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.6.0, commander@^2.8.1: version "2.20.3" resolved "https://npm-registry.sipwise.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^4.1.1: +commander@^4.0.0, commander@^4.1.1: version "4.1.1" resolved "https://npm-registry.sipwise.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== @@ -4338,6 +4343,14 @@ dot-case@^3.0.3: no-case "^3.0.3" tslib "^1.10.0" +dot-object@2.1.4: + version "2.1.4" + resolved "https://npm-registry.sipwise.com/dot-object/-/dot-object-2.1.4.tgz#c6c54e9fca510b4d0ea4d65acf33726963843b5f" + integrity sha512-7FXnyyCLFawNYJ+NhkqyP9Wd2yzuo+7n9pGiYpkmXCTYa8Ci2U0eUNDVg5OuO5Pm6aFXI2SWN8/N/w7SJWu1WA== + dependencies: + commander "^4.0.0" + glob "^7.1.5" + dot-prop@5.3.0: version "5.3.0" resolved "https://npm-registry.sipwise.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -5570,7 +5583,7 @@ glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0: dependencies: is-glob "^4.0.1" -glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.1: +glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.5, glob@^7.1.6, glob@~7.1.1: version "7.1.6" resolved "https://npm-registry.sipwise.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -6624,6 +6637,11 @@ is-utf8@^0.2.0, is-utf8@~0.2.0: resolved "https://npm-registry.sipwise.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= +is-valid-glob@1.0.0: + version "1.0.0" + resolved "https://npm-registry.sipwise.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" + integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= + is-whitespace@^0.3.0: version "0.3.0" resolved "https://npm-registry.sipwise.com/is-whitespace/-/is-whitespace-0.3.0.tgz#1639ecb1be036aec69a54cbb401cfbed7114ab7f" @@ -7164,6 +7182,14 @@ js-tokens@^3.0.2: resolved "https://npm-registry.sipwise.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= +js-yaml@3.14.1: + version "3.14.1" + resolved "https://npm-registry.sipwise.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + js-yaml@^3.13.1, js-yaml@^3.8.1: version "3.14.0" resolved "https://npm-registry.sipwise.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"