UNPKG

@intlify/cli

Version:

CLI Tooling for i18n development

575 lines (566 loc) 17.1 kB
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); }); })();