UNPKG

@intlify/cli

Version:

CLI Tooling for i18n development

597 lines (591 loc) 17.9 kB
import { generateJSON, generateYAML } from '@intlify/bundle-utils'; import path from 'pathe'; import { readFileSync, promises } from 'fs'; import createDebug from 'debug'; import { isString } from '@intlify/shared'; import fg from 'fast-glob'; import diff from 'diff-match-patch'; import { parseJSON } from 'jsonc-eslint-parser'; import { parseYAML } from 'yaml-eslint-parser'; import { cosmiconfig } from 'cosmiconfig'; import { format as format$1 } from 'prettier'; const debug$3 = createDebug("@intlify/cli:utils"); async function exists(path2, isThrow = false) { let ret = false; try { const stat = await promises.stat(path2); ret = stat.isFile(); } catch (e) { if (e.code === "ENOENT") { if (isThrow) { throw e; } } else { throw e; } } return ret; } async function getSourceFiles(input, filter) { const { source, files } = input; const _files = source != null ? await globAsync(source) : [...files || []].map((a) => a.toString()).splice(1); if (filter) { const _filters = Array.isArray(filter) ? filter : [filter]; return _filters.reduce((files2, filter2) => files2.filter(filter2), _files); } else { return _files; } } function globAsync(pattern) { return fg(pattern); } function getSFCBlocks(descriptor, version = 3) { const { template, script, scriptSetup, styles, customBlocks } = descriptor; const blocks = [...styles, ...customBlocks]; template && blocks.push(template); script && blocks.push(script); scriptSetup && blocks.push(scriptSetup); blocks.sort((a, b) => { if (version === 3) { return a.loc.start.offset - b.loc.start.offset; } else if (version === 2 && a.start != null && b.start != null) { return a.start - b.start; } else { return 0; } }); return blocks; } function getSFCContentInfo(block, filepath) { if (block.lang) { return { content: block.content, lang: block.lang, contentPath: filepath }; } else if (block.attrs.lang && isString(block.attrs.lang)) { return { content: block.content, lang: block.attrs.lang, contentPath: filepath }; } else if (block.src || block.attrs.src && isString(block.attrs.src)) { const src = block.src ? block.src : block.attrs.src && isString(block.attrs.src) ? block.attrs.src : ""; if (!src) { throw new Error(`src is empty`); } const parsed = path.parse(filepath); const target = path.resolve(parsed.dir, src); const parsedTarget = path.parse(target); return { content: readFileSync(target, "utf8"), lang: parsedTarget.ext.split(".").pop(), contentPath: target }; } else { return { content: block.content, lang: block.attrs.lang ? block.attrs.lang.toString() : void 0, contentPath: filepath }; } } function getCustomBlockContenType(content) { const [isLoad, type] = loadJson(content); if (isLoad) { return type; } else if (isLoadYaml(content)) { return "yaml"; } else { return "unknwon"; } } function isLoadYaml(content) { try { parseYAML(content); return true; } catch (e) { debug$3("yaml load error", e.message); return false; } } function isLoadJson(content) { try { JSON.parse(content); return true; } catch (e) { debug$3("json load error", e.message); return false; } } function isLoadJson5(content) { try { parseJSON(content, { jsonSyntax: "json5" }); return true; } catch (e) { debug$3("json5 load error", e.message); return false; } } function loadJson(content) { try { if (isLoadJson(content)) { return [true, "json"]; } else if (isLoadJson5(content)) { return [true, "json5"]; } else { return [false, "unknwon"]; } } catch (e) { debug$3("json load error", e.message); return [false, "unknwon"]; } } const ESC = { "<": "&lt;", ">": "&gt;", '"': "&quot;", "&": "&amp;" }; function escapeChar(a) { return ESC[a] || a; } function escape(s) { return s.replace(/[<>"&]/g, escapeChar); } const df = new diff.diff_match_patch(); function hasDiff(newContent, oldContent) { const diffs = df.diff_main(oldContent, newContent, true); if (diffs.length === 0) { return false; } return !!diffs.find( (d) => d[0] === diff.DIFF_DELETE || d[0] === diff.DIFF_INSERT ); } function updateContents(original, contents, offset, block, vue) { const end = vue === 3 ? block.loc.end.offset : vue === 2 && block.end != null ? block.end : -1; if (end === -1) { throw new Error("Not supported error"); } const _contents = contents.concat(original.slice(offset, end)); const _offset = end; return [_contents, _offset]; } function getPosition(block, vue, type) { if (vue === 3) { return [block.loc[type].offset, block.loc[type].column]; } else if (vue === 2 && block[type] != null) { return [block[type], -1]; } else { return [-1, -1]; } } function buildSFCBlockTag(meta) { let tag = `<${meta.type}`; for (const [key, value] of Object.entries(meta.attrs)) { if (value === true) { tag += ` ${key}`; } else { tag += ` ${key}="${escape(meta.attrs[key])}"`; } } tag += ">"; return tag; } async function getPrettierConfig(filepath) { const explorer = cosmiconfig("prettier"); return await explorer.load(filepath); } const _rDefault = (r) => r.default || r; async function getSFCParser(version) { let parser = null; try { if (version === 3) { parser = (await import('@vue/compiler-sfc').then(_rDefault)).parse; } else if (version === 2) { const { parseComponent } = await import('vue-template-compiler').then( _rDefault ); parser = (source) => { const errors = []; const descriptor = parseComponent(source); return { descriptor, errors }; }; } } catch (e) { debug$3("getSFCParser error", e); } return parser; } const SUPPORTED_FORMAT = [".json", ".json5", ".yaml", ".yml"]; const debug$2 = createDebug("@intlify/cli:api:compile"); var CompileErrorCodes = /* @__PURE__ */ ((CompileErrorCodes2) => { CompileErrorCodes2[CompileErrorCodes2["NOT_SUPPORTED_FORMAT"] = 1] = "NOT_SUPPORTED_FORMAT"; CompileErrorCodes2[CompileErrorCodes2["INTERNAL_COMPILE_WARNING"] = 2] = "INTERNAL_COMPILE_WARNING"; CompileErrorCodes2[CompileErrorCodes2["INTERNAL_COMPILE_ERROR"] = 3] = "INTERNAL_COMPILE_ERROR"; return CompileErrorCodes2; })(CompileErrorCodes || {}); const COMPILE_MODE = ["production", "development"]; async function compile(source, output, options = {}) { let ret = true; const targets = await globAsync(source); debug$2("compile: targets", targets); for (const target of targets) { const parsed = path.parse(target); debug$2("parsed", parsed); if (!parsed.ext) { continue; } const filename = `${parsed.name}.js`; const generatePath = path.resolve(output, filename); if (!SUPPORTED_FORMAT.includes(parsed.ext)) { options.onError && options.onError( 1 /* NOT_SUPPORTED_FORMAT */, target, generatePath ); ret = false; continue; } const source2 = await promises.readFile(target, { encoding: "utf-8" }); const generate = /\.json?5/.test(parsed.ext) ? generateJSON : generateYAML; const env = isString(options.mode) && COMPILE_MODE.includes(options.mode) ? options.mode : "production"; debug$2("env", env); let occuredError = false; const { code } = generate(source2, { type: "plain", filename: target, jit: !!options.ast, env, onError: (msg) => { occuredError = true; options.onError && options.onError( 3 /* INTERNAL_COMPILE_ERROR */, target, generatePath, msg ); ret = false; }, onWarn: (msg) => { options.onError && options.onError( 2 /* INTERNAL_COMPILE_WARNING */, target, generatePath, msg ); ret = false; } }); if (!occuredError) { await writeGenerateCode(output, filename, code); options.onCompile && options.onCompile(target, generatePath); } } return ret; } async function writeGenerateCode(target, filename, code) { await promises.mkdir(target, { recursive: true }); const generatePath = path.resolve(target, filename); await promises.writeFile(generatePath, code); return generatePath; } var __defProp$1 = Object.defineProperty; var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$1 = (obj, key, value) => { __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; const debug$1 = createDebug("@intlify/cli:api:annotate"); var AnnotateWarningCodes = /* @__PURE__ */ ((AnnotateWarningCodes2) => { AnnotateWarningCodes2[AnnotateWarningCodes2["NOT_SUPPORTED_TYPE"] = 1] = "NOT_SUPPORTED_TYPE"; AnnotateWarningCodes2[AnnotateWarningCodes2["LANG_MISMATCH_IN_SRC_AND_CONTENT"] = 2] = "LANG_MISMATCH_IN_SRC_AND_CONTENT"; AnnotateWarningCodes2[AnnotateWarningCodes2["LANG_MISMATCH_IN_OPTION_AND_CONTENT"] = 3] = "LANG_MISMATCH_IN_OPTION_AND_CONTENT"; AnnotateWarningCodes2[AnnotateWarningCodes2["LANG_MISMATCH_IN_ATTR_AND_CONTENT"] = 4] = "LANG_MISMATCH_IN_ATTR_AND_CONTENT"; return AnnotateWarningCodes2; })(AnnotateWarningCodes || {}); class SFCAnnotateError extends Error { /** * Constructor * * @param message - The error message * @param filepath - The filepath of the target file at annotate processing */ constructor(message, filepath) { super(message); /** * The filepath of the target file at annotate processing */ __publicField$1(this, "filepath"); this.name = "SFCAnnotateError"; this.filepath = filepath; } } const NOOP_WARN = (code, args, block) => { }; async function annotate(source, filepath, options = {}) { const type = options.type || "i18n"; const force = options.force || false; const attrs = options.attrs || {}; const onWarn = options.onWarn || NOOP_WARN; const vue = options.vue || 3; const parse = await getSFCParser(vue); if (parse == null) { throw new SFCAnnotateError("Not found SFC parser", filepath); } const { descriptor, errors } = parse(source); if (errors.length) { debug$1("parse error", errors); const error = new SyntaxError(String("SFC parse error")); error.erorrs = errors; error.filepath = filepath; throw error; } if (type !== "i18n") { throw new SFCAnnotateError("Not supported error", filepath); } const original = descriptor.source || source; let offset = 0; let diffset = 0; let contents = []; contents = getSFCBlocks(descriptor, vue).reduce((contents2, block) => { debug$1( `start: vue=${vue} type=${block.type}, offset=${offset}, diffset=${diffset}` ); if (block.type !== type) { [contents2, offset] = updateContents( original, contents2, offset, block, vue ); return contents2; } const { content, lang } = getSFCContentInfo(block, filepath); const contentType = getCustomBlockContenType(content); debug$1("content info", block.lang, lang, contentType); if (contentType === "unknwon") { onWarn( 1 /* NOT_SUPPORTED_TYPE */, { type: block.type, actual: contentType }, block ); [contents2, offset] = updateContents( original, contents2, offset, block, vue ); return contents2; } if (block.src) { if (lang !== contentType) { onWarn( 2 /* LANG_MISMATCH_IN_SRC_AND_CONTENT */, { src: lang, content: contentType }, block ); } [contents2, offset] = updateContents( original, contents2, offset, block, vue ); return contents2; } if (block.lang == null) { let lang2 = contentType; if (attrs.lang && attrs.lang !== contentType) { onWarn( 3 /* LANG_MISMATCH_IN_OPTION_AND_CONTENT */, { lang: attrs.lang, content: lang2 }, block ); if (!force) { [contents2, offset] = updateContents( original, contents2, offset, block, vue ); return contents2; } else { lang2 = attrs.lang; } } const [startOffset, startColumn] = getPosition(block, vue, "start"); debug$1( `${block.type} block start: offset=${startOffset}, column=${startColumn}` ); if (startOffset === -1) { throw new SFCAnnotateError( "Invalid block start offset position", filepath ); } const blockTag = buildSFCBlockTag(block); const tagStartOffset = startOffset - blockTag.length; debug$1(`current tag: ${blockTag}`); debug$1(`tag start offset: ${tagStartOffset}`); const annoatedBlockTag = buildSFCBlockTag({ type, attrs: { ...attrs, lang: lang2 } }); debug$1( `annotated tag: ${annoatedBlockTag} (length:${annoatedBlockTag.length})` ); const blockContent = `${annoatedBlockTag}${content}`; debug$1(`content: ${blockContent}`); contents2 = contents2.concat([ original.slice(offset, tagStartOffset), blockContent ]); const [endOffset] = getPosition(block, vue, "end"); if (endOffset === -1) { throw new SFCAnnotateError( "Invalid block end offset position", filepath ); } offset = endOffset; diffset += annoatedBlockTag.length - blockTag.length; return contents2; } else { if (lang !== contentType) { onWarn( 4 /* LANG_MISMATCH_IN_ATTR_AND_CONTENT */, { lang, content: contentType }, block ); } [contents2, offset] = updateContents( original, contents2, offset, block, vue ); return contents2; } }, contents); debug$1(`end: offset=${offset}, diffset=${diffset}`); contents = contents.concat(original.slice(offset, original.length)); return contents.join(""); } var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; const debug = createDebug("@intlify/cli:api:format"); class FormatLangNotFoundError extends Error { /** * Constructor * * @param message - The error message * @param filepath - The filepath of the target file at formatting processing */ constructor(message, filepath) { super(message); /** * The filepath of the target file at formatting processing */ __publicField(this, "filepath"); this.name = "FormatLangNotFoundError"; this.filepath = filepath; } } const DEFAULT_PRETTIER_OPTIONS = { printWidth: 100, tabWidth: 2, jsonRecursiveSort: true, plugins: ["prettier-plugin-sort-json"] }; async function format(source, filepath, options = {}) { const prettierOptions = Object.assign( {}, DEFAULT_PRETTIER_OPTIONS, options.prettier ); const vue = options.vue || 3; const parse = await getSFCParser(vue); if (parse == null) { throw new FormatLangNotFoundError("Not found SFC parser", filepath); } const { descriptor, errors } = parse(source); if (errors.length) { debug("parse error", errors); const error = new SyntaxError(String("SFC parse error")); error.erorrs = errors; error.filepath = filepath; throw error; } const original = descriptor.source || source; let offset = 0; let contents = []; contents = getSFCBlocks(descriptor, vue).reduce((contents2, block) => { debug(`start: type=${block.type}, offset=${offset}`); if (block.type !== "i18n") { [contents2, offset] = updateContents( original, contents2, offset, block, vue ); return contents2; } const { content, lang } = getSFCContentInfo(block, filepath); if (lang == null) { throw new FormatLangNotFoundError("`lang` attr not found", filepath); } const [startOffset, startColumn] = getPosition(block, vue, "start"); debug( `${block.type} block start: offset=${startOffset}, column=${startColumn}` ); if (startOffset === -1) { throw new FormatLangNotFoundError( "Invalid block start offset position", filepath ); } const formatted = format$1( content, Object.assign({}, prettierOptions, { parser: lang }) ); const blockContent = ` ${formatted}`; debug(`content: ${blockContent}`); contents2 = contents2.concat([ original.slice(offset, startOffset), blockContent ]); const [endOffset] = getPosition(block, vue, "end"); if (endOffset === -1) { throw new FormatLangNotFoundError( "Invalid block end offset position", filepath ); } offset = endOffset; return contents2; }, contents); contents = contents.concat(original.slice(offset, original.length)); return contents.join(""); } export { AnnotateWarningCodes as A, CompileErrorCodes as C, DEFAULT_PRETTIER_OPTIONS as D, FormatLangNotFoundError as F, SFCAnnotateError as S, annotate as a, getPrettierConfig as b, compile as c, exists as e, format as f, getSourceFiles as g, hasDiff as h };