@intlify/cli
Version:
CLI Tooling for i18n development
575 lines (566 loc) • 17.1 kB
JavaScript
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import path, { resolve, relative, parse } from 'pathe';
import { promises } from 'fs';
import { isString } from '@intlify/shared';
import { createCoreContext, translate } from '@intlify/core';
import { getNavigatorLanguage } from '@intlify/utils/node';
import createDebug from 'debug';
import { green, red, yellow, bold, yellowBright, white, blue, whiteBright, greenBright, blueBright, redBright } from 'colorette';
import { c as compile, C as CompileErrorCodes, e as exists, g as getSourceFiles, a as annotate, h as hasDiff, S as SFCAnnotateError, A as AnnotateWarningCodes, b as getPrettierConfig, f as format, F as FormatLangNotFoundError } from './shared/cli.b7399166.mjs';
import ignore from 'ignore';
import '@intlify/bundle-utils';
import 'fast-glob';
import 'diff-match-patch';
import 'jsonc-eslint-parser';
import 'yaml-eslint-parser';
import 'cosmiconfig';
import 'prettier';
function isSFCParserError(err) {
return "errors" in err && "filepath" in err;
}
const dirname = path.dirname(new URL(import.meta.url).pathname);
const debug$3 = createDebug("@intlify/cli:i18n");
const DEFAULT_LOCALE = "en-US";
let resources = null;
let context = null;
async function loadI18nResources() {
const dirents = await promises.readdir(path.resolve(dirname, "../locales"), {
withFileTypes: true
});
return dirents.reduce(
async (acc, dir) => {
if (dir.isFile()) {
const data = await promises.readFile(
path.resolve(dirname, "../locales", dir.name),
{ encoding: "utf-8" }
);
const { name } = path.parse(dir.name);
debug$3("load i18n resource", name, data);
const messages = await acc;
messages[name] = JSON.parse(
data
);
}
return acc;
},
Promise.resolve({})
);
}
function getLocale() {
return getNavigatorLanguage() || DEFAULT_LOCALE;
}
function t(key, ...args) {
if (context == null) {
console.error(
"cannot initialize CoreContext with @intlify/core createCoreContext"
);
return key;
}
const ret = Reflect.apply(translate, null, [context, key, ...args]);
return isString(ret) ? ret : key;
}
async function initI18n() {
try {
resources = await loadI18nResources();
context = createCoreContext({
locale: getLocale(),
fallbackLocale: DEFAULT_LOCALE,
fallbackWarn: false,
missingWarn: false,
warnHtmlMessage: false,
fallbackFormat: true,
messages: resources
});
} catch (e) {
debug$3("load i18n resource errors", e.message);
throw e;
}
}
const debug$2 = createDebug("@intlify/cli:compile");
function defineCommand$2() {
const command = "compile";
const aliases = "cp";
const describe = t("compile the i18n resources");
const builder = (args) => {
return args.option("source", {
type: "string",
alias: "s",
describe: t("the i18n resource source path"),
demandOption: true
}).option("output", {
type: "string",
alias: "o",
describe: t("the compiled i18n resource output path")
}).option("mode", {
type: "string",
alias: "m",
default: "production",
describe: t(
"the compiled i18n resource mode, 'production' or 'development' (default: 'production')"
)
}).option("format", {
type: "string",
alias: "f",
default: "function",
describe: t(
"resource compilation format, 'function' or 'ast' (default: 'function')"
)
});
};
const handler = async (args) => {
const output = args.output != null ? path.resolve(process.cwd(), args.output) : process.cwd();
const ret = await compile(args.source, output, {
mode: args.mode,
ast: args.format === "ast",
onCompile: (source, output2) => {
console.log(
green(
t("Success compilation: {source} -> {output}", { source, output: output2 })
)
);
},
onError: (code, source, output2, msg) => {
switch (code) {
case CompileErrorCodes.NOT_SUPPORTED_FORMAT:
const parsed = path.parse(source);
console.warn(
yellow(
t("{source}: Ignore compilation due to not supported '{ext}'", {
ext: parsed.ext
})
)
);
break;
case CompileErrorCodes.INTERNAL_COMPILE_WARNING:
console.log(
yellow(
t("Warning compilation: {source} -> {output}, {msg}", {
source,
output: output2,
msg
})
)
);
break;
case CompileErrorCodes.INTERNAL_COMPILE_ERROR:
console.error(
red(
t("Error compilation: {source} -> {output}, {msg}", {
source,
output: output2,
msg
})
)
);
break;
}
}
});
debug$2("compile: ", ret);
};
return {
command,
aliases,
describe,
builder,
handler
};
}
function typeGuard(o, className) {
return o instanceof className;
}
class RequireError extends Error {
}
function defineFail(userError) {
return (msg, err) => {
if (msg) {
console.error(msg);
console.warn(red(bold(msg)));
process.exit(1);
} else {
if (typeGuard(err, userError)) {
console.warn(yellow(bold(err.message)));
process.exit(0);
} else {
throw err;
}
}
};
}
const DEFAULT_IGNORE_FILENAME = ".intlifyignore";
function checkType(type) {
if (type !== "custom-block") {
throw new RequireError(
t(`'--type' is not supported except for 'custom-block'`)
);
}
}
function checkSource(argsLength, source) {
if (source == null && argsLength === 1) {
throw new RequireError(
t(
`if you don't specify some files at the end of the command, the '\u2014-source' option is required`
)
);
}
}
async function readIgnore(cwd, path) {
if (path == null) {
const defaultIgnorePath = resolve(cwd, DEFAULT_IGNORE_FILENAME);
return await exists(defaultIgnorePath) ? await promises.readFile(defaultIgnorePath, "utf8") : void 0;
} else {
return await exists(path, true) ? await promises.readFile(path, "utf8") : void 0;
}
}
async function getSFCFiles(args = {}) {
const { source, files, cwd, ignore: _ignore } = args;
const _files = await getSourceFiles({ source, files }, [
(file) => {
const parsed = parse(file);
return parsed.ext === ".vue";
}
]);
if (cwd && _ignore) {
return ignore().add(_ignore).filter(_files.map((file) => relative(cwd, file))).map((file) => resolve(cwd, file));
} else {
return _files;
}
}
const debug$1 = createDebug("@intlify/cli:annotate");
function defineCommand$1() {
const command = "annotate";
const aliases = "at";
const describe = t("annotate the attributes");
const builder = (args) => {
return args.option("source", {
type: "string",
alias: "s",
describe: t("the source path")
}).option("type", {
type: "string",
alias: "t",
describe: t("the annotation type")
}).option("force", {
type: "boolean",
alias: "f",
describe: t("forced applying of attributes")
}).option("details", {
type: "boolean",
alias: "V",
describe: t("annotated result detail options")
}).option("attrs", {
type: "array",
alias: "a",
describe: t("the attributes to annotate")
}).option("vue", {
type: "number",
alias: "v",
describe: t("the vue template compiler version"),
default: 3
}).option("ignore", {
type: "string",
alias: "i",
describe: t(
"the ignore configuration path files passed at the end of the options or `--source` option"
)
}).option("dryRun", {
type: "boolean",
alias: "d",
describe: t("target files without annotating")
}).fail(defineFail(RequireError));
};
const handler = async (args) => {
args.type = args.type || "custom-block";
const { source, force, details, attrs, vue, ignore, dryRun } = args;
debug$1("annotate args:", args);
checkType(args.type);
checkSource(args._.length, source);
if (dryRun) {
console.log();
console.log(yellowBright(bold(`${t("dry run mode")}:`)));
console.log();
}
let counter = 0;
let passCounter = 0;
let fineCounter = 0;
let warnCounter = 0;
let forceCounter = 0;
let ignoreCounter = 0;
let errorCounter = 0;
function printStats() {
console.log("");
console.log(
white(bold(t("{count} annotateable files ", { count: counter })))
);
console.log(
green(bold(t("{count} annotated files", { count: fineCounter })))
);
if (details) {
console.log(
white(bold(t("{count} passed files", { count: passCounter })))
);
}
console.log(yellow(t("{count} warned files", { count: warnCounter })));
if (details) {
console.log(yellow(t("{count} forced files", { count: forceCounter })));
console.log(
yellow(t("{count} ignored files", { count: ignoreCounter }))
);
}
console.log(red(t("{count} error files", { count: errorCounter })));
console.log("");
}
let status = "fine";
const onWarn = warnHnadler(() => status = "warn");
const cwd = process.cwd();
const _ignore = await readIgnore(cwd, ignore);
const files = await getSFCFiles({
source,
files: args._,
cwd,
ignore: _ignore
});
for (const file of files) {
const data = await promises.readFile(file, "utf8");
let annotated = null;
try {
status = "none";
annotated = await annotate(data, file, {
type: "i18n",
force,
attrs,
vue,
onWarn
});
if (hasDiff(annotated, data)) {
status = "fine";
}
if (status === "fine") {
fineCounter++;
console.log(green(`${file}: ${t("annotate")}`));
if (!dryRun) {
await promises.writeFile(file, annotated, "utf8");
}
} else if (status === "none") {
passCounter++;
console.log(white(`${file}: ${t("pass annotate")}`));
} else if (status === "warn") {
warnCounter++;
if (force) {
forceCounter++;
console.log(yellow(`${file}: ${t("force annotate")}`));
if (!dryRun) {
await promises.writeFile(file, annotated, "utf8");
}
} else {
ignoreCounter++;
console.log(yellow(`${file}: ${t("ignore annotate")}`));
}
}
} catch (e) {
status = "error";
errorCounter++;
if (isSFCParserError(e)) {
console.error(red(bold(`${e.message} at ${e.filepath}`)));
e.erorrs.forEach((err) => console.error(red(` ${err.message}`)));
} else if (e instanceof SFCAnnotateError) {
console.error(
red(`${e.filepath}: ${t(e.message)}`)
// eslint-disable-line @typescript-eslint/no-explicit-any, @intlify/vue-i18n/no-dynamic-keys
);
} else {
console.error(red(e.message));
}
if (!dryRun) {
throw e;
}
}
counter++;
}
printStats();
};
return {
command,
aliases,
describe,
builder,
handler
};
}
function warnHnadler(cb) {
return (code, args, block) => {
debug$1(`annotate warning: block => ${JSON.stringify(block)}`);
switch (code) {
case AnnotateWarningCodes.NOT_SUPPORTED_TYPE:
console.log(
yellow(t(`Unsupported '{type}' block content type: {actual}`, args))
);
case AnnotateWarningCodes.LANG_MISMATCH_IN_ATTR_AND_CONTENT:
console.log(
yellow(
t(
"Detected lang mismatch in `lang` attr ('{lang}') and block content ('{content}')",
args
)
)
);
case AnnotateWarningCodes.LANG_MISMATCH_IN_OPTION_AND_CONTENT:
console.log(
yellow(
t(
"Detected lang mismatch in `lang` option ('{lang}') and block content ('{content}')",
args
)
)
);
case AnnotateWarningCodes.LANG_MISMATCH_IN_SRC_AND_CONTENT:
console.log(
yellow(
t(
"Detected lang mismatch in block `src` ('{src}') and block content ('{content}')",
args
)
)
);
default:
debug$1(`annotate warning: ${code}`);
cb();
break;
}
};
}
const debug = createDebug("@intlify/cli:format");
function defineCommand() {
const command = "format";
const aliases = "ft";
const describe = t("format for single-file components");
const builder = (args) => {
return args.option("source", {
type: "string",
alias: "s",
describe: t("the source path")
}).option("type", {
type: "string",
alias: "t",
describe: t("the format type")
}).option("prettier", {
type: "string",
alias: "p",
describe: t("the config file path of prettier")
}).option("vue", {
type: "number",
alias: "v",
describe: t("the vue template compiler version"),
default: 3
}).option("ignore", {
type: "string",
alias: "i",
describe: t(
"the ignore configuration path files passed at the end of the options or `--source` option"
)
}).option("dryRun", {
type: "boolean",
alias: "d",
describe: t("target files without formatting")
}).fail(defineFail(RequireError));
};
const handler = async (args) => {
args.type = args.type || "custom-block";
const { source, prettier, ignore, vue, dryRun } = args;
debug("format args:", args);
checkType(args.type);
checkSource(args._.length, source);
const cwd = process.cwd();
const prettierConfig = prettier ? await getPrettierConfig(path.resolve(cwd, prettier)) : { filepath: "", config: {} };
debug("prettier config", prettierConfig);
if (dryRun) {
console.log();
console.log(yellowBright(bold(`${t("dry run mode")}:`)));
console.log();
}
let formattedCounter = 0;
let noChangeCounter = 0;
let errorCounter = 0;
const _ignore = await readIgnore(cwd, ignore);
const files = await getSFCFiles({
source,
files: args._,
cwd,
ignore: _ignore
});
for (const file of files) {
try {
const data = await promises.readFile(file, "utf8");
const formatted = await format(data, file, {
vue,
prettier: prettierConfig?.config
});
if (hasDiff(formatted, data)) {
formattedCounter++;
console.log(green(`${file}: ${t("formatted")}`));
} else {
noChangeCounter++;
console.log(blue(`${file}: ${t("no change")}`));
}
if (!dryRun) {
await promises.writeFile(file, formatted, "utf8");
}
} catch (e) {
errorCounter++;
if (e instanceof FormatLangNotFoundError) {
console.error(
red(`${e.filepath}: ${t(e.message)}`)
// eslint-disable-line @typescript-eslint/no-explicit-any, @intlify/vue-i18n/no-dynamic-keys
);
} else if (isSFCParserError(e)) {
console.error(red(`${e.message} at ${e.filepath}`));
e.erorrs.forEach((err) => console.error(red(` ${err.message}`)));
} else {
console.error(red(e.message));
}
if (!dryRun) {
throw e;
}
}
}
console.log("");
console.log(
whiteBright(bold(t("{count} formattable files", { count: files.length })))
);
console.log(
greenBright(
bold(t("{count} formatted files", { count: formattedCounter }))
)
);
console.log(
blueBright(bold(t("{count} no change files", { count: noChangeCounter })))
);
if (dryRun) {
console.log(
redBright(bold(t("{count} error files", { count: errorCounter })))
);
}
console.log("");
};
return {
command,
aliases,
describe,
builder,
handler
};
}
(async () => {
await initI18n();
yargs(hideBin(process.argv)).scriptName("intlify").usage(t("Usage: $0 <command> [options]")).command(defineCommand$2()).command(defineCommand$1()).command(defineCommand()).demandCommand().help().version().argv;
process.on("uncaughtException", (err) => {
console.error(`uncaught exception: ${err}
`);
process.exit(1);
});
process.on("unhandledRejection", (reason, p) => {
console.error("unhandled rejection at:", p, "reason:", reason);
process.exit(1);
});
})();