@intlify/cli
Version:
CLI Tooling for i18n development
597 lines (591 loc) • 17.9 kB
JavaScript
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 = {
"<": "<",
">": ">",
'"': """,
"&": "&"
};
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 };