@pelag/nts
Version:
Creation and support of the multilingual project Next.js
4 lines (3 loc) ⢠14 kB
JavaScript
;var e=require("commander"),t=require("axios"),r=require("fs-extra"),a=require("path"),o=require("zod"),n=require("os"),s=require("@inquirer/prompts"),i=require("yauzl"),c=require("util"),l=require("stream"),d=require("cross-spawn");const w="https://api.next-translator.com",p="https://next-translator.com",m="nts.config.json",u=".nts",g=".tokens.json",h="translator",f="nts update",y=o.z.object({localesPath:o.z.string().min(1,"Path cannot be empty").refine(e=>!e.includes(".."),'Path should not contain ".."'),projectId:o.z.string().ulid()}),E=o.z.object({login:o.z.string().email("Please enter a valid email address"),password:o.z.string().min(6,"Password must contain at least 6 characters").max(64,"Password must contain at most 64 characters")});class v{static async loadConfig(e){const t=e||this.CONFIG_FILE;if(!await r.pathExists(t))throw new Error(`Configuration file "${t}" not found. Please run "nts init" first.`);try{const e=await r.readJson(t);return y.parse(e)}catch(e){if(e instanceof SyntaxError)throw new Error("Invalid JSON in configuration file");if(e instanceof Error)throw new Error(`Failed to load configuration: ${e.message}`);throw new Error("Failed to load configuration")}}static async saveConfig(e,t){const o=t||this.CONFIG_FILE;try{await r.ensureDir(a.dirname(o)),await r.writeJson(o,e,{spaces:2})}catch(e){if(e instanceof Error)throw new Error(`Failed to save configuration: ${e.message}`);throw new Error("Failed to save configuration")}}}v.CONFIG_FILE=m;class F{constructor(e=w){this.apiBaseUrl=e}async getUserCredentials(){console.log(`š Sign-in: ${p}`);try{const e=await this.promptForLogin();return{login:e,password:await this.promptForPassword()}}catch(e){this.handlePromptError(e),process.exit(1)}}async login(e,r){try{this.validateCredentials(e,r);const a=await t.post(`${this.apiBaseUrl}/auth/login`,{email:e,password:r},{timeout:1e4,headers:{"Content-Type":"application/json"}});return a.data.token||a.data}catch(e){throw this.handleAuthError(e)}}validateCredentials(e,t){try{E.parse({login:e,password:t})}catch(e){throw new Error(`Invalid credentials: ${e.errors[0]?.message||"Validation failed"}`)}}async promptForLogin(){return s.input({message:"Login (email):",default:void 0,validate:e=>this.validateField(E.shape.login,e,"Invalid email")})}async promptForPassword(){return s.password({message:"Password:",mask:"*",validate:e=>this.validateField(E.shape.password,e,"Invalid password")})}validateField(e,t,r){try{return e.parse(t),!0}catch(e){return e.errors[0]?.message||r}}handleAuthError(e){if(t.isAxiosError(e)){const t=e,r=t.response?.status,a=t.response?.data?.message||t.message;switch(r){case 401:return new Error("Invalid email or password");case 400:return new Error(`Bad request: ${a}`);case 403:return new Error("Access forbidden");case 404:return new Error("Authentication service not found");case 429:return new Error("Too many login attempts. Please try again later");case 500:return new Error("Server error. Please try again later");default:return new Error(`Authentication failed: ${a}`)}}return e instanceof Error?e:new Error("Network error during authentication")}handlePromptError(e){e instanceof Error?console.error("ā Error:",e.message):console.error("ā Unknown error occurred while getting credentials")}}const j=a.join(n.homedir(),u),x=a.join(j,g);class k{async ensureTokenDir(){try{await r.ensureDir(j)}catch(e){throw new Error(`Failed to create token directory: ${e instanceof Error?e.message:"Unknown error"}`)}}async loadTokenFile(){if(await this.ensureTokenDir(),!await r.pathExists(x))try{return await r.writeJson(x,{},{spaces:2}),{}}catch(e){throw new Error("Failed to create token file")}try{return await r.readJson(x)}catch(e){throw new Error("Failed to load tokens file")}}async saveToken(e,t){try{const a=await this.loadTokenFile();a[e]=t,await r.writeJson(x,a,{spaces:2})}catch(e){throw console.error("Failed to save token file:",e),new Error("Failed to save token")}}async refreshToken(e){const t=await this.loadTokenFromFile(e),r=new F;if(t){console.log(`š Refreshing token for ${t.login}...`);const a=await s.password({message:`Password for ${t.login}:`,mask:"*"});try{const o=await r.login(t.login,a);return await this.saveToken(e,{token:o,login:t.login}),o}catch(e){console.log("ā Failed to refresh token with saved credentials, requesting new login...")}}const{login:a,password:o}=await r.getUserCredentials(),n=await r.login(a,o);return await this.saveToken(e,{token:n,login:a}),n}async loadTokenFromFile(e){if(!await r.pathExists(x))return null;try{return(await r.readJson(x))[e]??null}catch(e){return null}}async getToken(e){const t=await this.loadTokenFromFile(e);if(t)return t.token;const r=new F,{login:a,password:o}=await r.getUserCredentials(),n=await r.login(a,o);return await this.saveToken(e,{token:n,login:a}),n}}const N=c.promisify(i.fromBuffer),I=c.promisify(l.pipeline);async function $(e,t){const o=await N(e,{lazyEntries:!0});return new Promise((e,n)=>{o.readEntry(),o.on("entry",e=>{if(e.fileName.endsWith("/"))return void o.readEntry();const s="types.ts"===e.fileName,i=e.fileName.endsWith(".json");if(!s&&!i)return void o.readEntry();const c=t.localesPath,l=a.join(c,e.fileName),d=a.dirname(l);r.ensureDirSync(d),o.openReadStream(e,async(e,t)=>{if(!e&&t){try{const e=r.createWriteStream(l);await I(t,e)}catch(e){return void n(e)}o.readEntry()}else n(e??new Error("Failed to open read stream"))})}),o.on("end",()=>{o.close(),e()}),o.on("error",e=>{n(e)})})}async function C(e){try{const r=await v.loadConfig(),a=new k;let o=await a.getToken(r.projectId);try{const t=await P(r.projectId,o);await $(t,r),e?.silent||console.log("\nā
Update completed successfully!")}catch(n){if(!t.isAxiosError(n)||401!==n.response?.status)throw n;{o=await a.refreshToken(r.projectId);const t=await P(r.projectId,o);await $(t,r),e?.silent||console.log("\nā
Update completed successfully!")}}}catch(e){e instanceof Error?console.error(`ā Error: ${e.message}`):console.error("ā Unknown error occurred"),process.exit(1)}}async function P(e,r){const a=await t.post(`${w}/output/zip`,{projectId:e},{headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json"},responseType:"arraybuffer"});return Buffer.from(a.data)}const b=async e=>{try{const r=(await t.get(`${w}/projects/list`,{headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json"}})).data;if(!Array.isArray(r)||0===r.length)throw new Error("No projects available for this user");const a=r.filter(e=>e.allowDownload);if(0===a.length)throw new Error("No projects available for create (forbidden by owner)");const o=await s.select({message:"Select a project:",choices:r.map(e=>({value:e.projectId,name:e.projectName,disabled:!e.allowDownload}))}),n=r.find(e=>e.projectId===o);if(!n)throw new Error("Project not found");return n}catch(e){t.isAxiosError(e)?console.error("ā Network error:",e.response?.data?.message||e.message):e instanceof Error?console.error("ā Error:",e.message):console.error("ā Unknown error"),process.exit(1)}};async function T(e){const{templateName:t,targetDir:o,customFileName:n,variables:s}=e,i=a.join(__dirname,"/templates",t),c=a.join(process.cwd(),o,n),l=a.dirname(c);await r.ensureDir(l);let d=await r.readFile(i,"utf8");var w;s&&(w=s,d=d.replace(/(?<!\{)\{\{(\w+)\}\}(?!\})/g,(e,t)=>t in w?String(w[t]):e)),await r.writeFile(c,d)}async function A(e){const t=a.join(process.cwd(),"src");try{return(await r.stat(t)).isDirectory()}catch(e){return!1}}async function D(){try{const e=a.join(process.cwd(),"package.json");if(await r.pathExists(e)){const t=await r.readJson(e);if({...t.dependencies,...t.devDependencies}.tailwindcss)return!0}return!1}catch(e){return console.warn("Could not determine Tailwind CSS usage, defaulting to regular CSS"),!1}}async function U(){const e=await A(),t=a.join(process.cwd(),e?"src/app":"app");if(!await r.pathExists(t))throw new Error(`App directory not found at: ${t}`);const o=a.join(t,"[lang]");await r.pathExists(o)||await r.ensureDir(o);const n=["favicon.ico"],s=await r.readdir(t,{withFileTypes:!0});for(const e of s){if("[lang]"===e.name)continue;const s=a.join(t,e.name);if(e.isFile()&&n.includes(e.name)){const t=a.join(o,e.name);try{await r.move(s,t)}catch(t){console.error(`Failed to move ${e.name}:`,t)}}else if(e.isFile()&&"globals.css"===e.name)try{await r.remove(s)}catch(t){console.error(`Failed to remove ${e.name}:`,t)}else try{await r.remove(s)}catch(t){console.error(`Failed to remove ${e.name}:`,t)}}const i=await D(),c=i?"globals.tw.txt":"globals.css.txt",l=e?"/src/app/[lang]":"/app/[lang]";if(!i)try{await T({templateName:"page.module.txt",targetDir:l,customFileName:"page.module.css"})}catch(e){console.error("Failed to add page.module.css template:",e)}try{await T({templateName:c,targetDir:l,customFileName:"globals.css"})}catch(e){console.error("Failed to add globals.css template:",e)}}class z{static async isGitInitialized(){return await r.pathExists(a.join(process.cwd(),".git"))}static async addAll(){return new Promise((e,t)=>{const r=d.spawn("git",["add","."],{stdio:"ignore"});r.on("close",r=>{0===r?e():t(new Error(`git add exited with code ${r}`))}),r.on("error",e=>{t(new Error(`Failed to add files to git: ${e.message}`))})})}static async commit(e){return new Promise((t,r)=>{const a=d.spawn("git",["commit","-m",e],{stdio:"ignore"});a.on("close",e=>{0===e?t():r(new Error(`git commit exited with code ${e}`))}),a.on("error",e=>{r(new Error(`Failed to commit: ${e.message}`))})})}static async commitIfGitInitialized(e){await this.isGitInitialized()?(await this.addAll(),await this.commit(e)):console.log("Git is not initialized, skipping commit...")}}async function S(){try{const t=new F,{login:r,password:a}=await t.getUserCredentials(),o=new k,n=await t.login(r,a),i=await b(n);await o.saveToken(i.projectId,{login:r,token:n});const c=await async function({defaultName:e}){try{const r=await s.input({message:"Enter project name:",default:e?(t=e,t.trim().replace(/[\s]+/g,"-").replace(/[<>:"/\\|?*\x00-\x1F()[\]{}@#$%^&+=~`!]/g,"").replace(/\.+$/,"").substring(0,32)):"my-app",validate:e=>!!e.trim()||"Project name is required"});console.log("Run create-next-app...");const a=d("npx",["create-next-app@15",r,"--typescript","--app","--no-src-dir","--use-npm","--import-alias","@/*"],{stdio:"inherit"});return new Promise((e,t)=>{a.on("close",a=>{0===a?e(r):t(new Error(`create-next-app exited with code ${a}`))}),a.on("error",e=>{t(new Error(`Failed to start create-next-app: ${e.message}`))})})}catch(e){e instanceof Error?console.error("ā Error:",e.message):console.error("ā Unknown error"),process.exit(1)}var t}({defaultName:i.projectName});process.chdir(c);const l=await A();console.log("Setup project..."),await T({templateName:"middleware.txt",targetDir:"/",customFileName:"middleware.ts",variables:{PATH:`@/lib/${h}`}}),await T({templateName:"translator.txt",targetDir:l?"src/lib/":"lib/",customFileName:"translator.ts",variables:{defaultLocale:i.base,locales:i.languages.map(e=>`"${e}"`).join(", "),dictionaries:(e=i.languages,`{\n${e.map(e=>` "${e}": () => import("@/locales/${e}.json").then((module) => module.default),`).join("\n")}\n}`)}});const w=y.parse({localesPath:l?"./src/locales":"./locales",typesPath:l?"./src/types":"./types",projectId:i.projectId});await v.saveConfig(w),await async function(){const e=["@formatjs/intl-localematcher","negotiator","server-only"],t=["@types/negotiator","@pelag/nts"];try{const r=d.sync("npm",["install",...e],{stdio:"inherit"});if(0!==r.status)throw new Error(`Failed to install dependencies. Exit code: ${r.status}`);const a=d.sync("npm",["install","--save-dev",...t],{stdio:"inherit"});if(0!==a.status)throw new Error(`Failed to install dev dependencies. Exit code: ${a.status}`)}catch(e){throw console.error("Error installing dependencies:",e),e}}(),await U();const m=await D(),u=l?"/src/app/[lang]":"/app/[lang]";await T({templateName:m?"page.tw.txt":"page.css.txt",targetDir:u,customFileName:"page.tsx",variables:{LANG_CONFIG_PATH:`@/lib/${h}`,PROJECT_ID:i.projectId,FRONTEND_URL:p,UPDATE_COMMAND:f,PROJECT_NAME:i.projectName}}),await T({templateName:"layout.txt",targetDir:u,customFileName:"layout.tsx",variables:{LANG_CONFIG_PATH:`@/lib/${h}`}}),await C({silent:!0}),await z.commitIfGitInitialized("setup by nts"),console.log("ā
Success!")}catch(e){t.isAxiosError(e)?console.error("ā Network error:",e.response?.data?.message||e.message):e instanceof Error?console.error("ā Error:",e.message):console.error("ā Unknown error"),process.exit(1)}var e}const q=new e.Command;q.name("nts").description("CLI tool for translation management").version("1.0.0"),q.command("init").description("Initialize nts configuration").action(async()=>{await async function(){try{if(await r.pathExists(m)&&"y"!==(await s.input({message:`File ${m} already exists. Overwrite? (y/N)`,default:"N"})).toLowerCase())return void console.log("ā Cancelled");const e=new F,{login:t,password:a}=await e.getUserCredentials(),o=new k,n=await e.login(t,a),i=await b(n),c=await s.input({message:"Path to save JSON files:",default:"./locales",validate:e=>{try{return y.shape.localesPath.parse(e),!0}catch(e){if(e&&"object"==typeof e&&"errors"in e){const t=e;return t.errors[0]?.message||"Invalid path"}return"Invalid path format"}}}),l=y.parse({localesPath:c,projectId:i.projectId});await v.saveConfig(l),await o.saveToken(i.projectId,{login:t,token:n}),console.log("ā
Configuration created successfully!"),console.log(`š File: ${m}`)}catch(e){t.isAxiosError(e)?console.error("ā Network error:",e.response?.data?.message||e.message):e instanceof Error?console.error("ā Error:",e.message):console.error("ā Unknown error"),process.exit(1)}}()}),q.command("create").description("Create NextJs project based on web project").action(async()=>{await S()}),q.command("update").description("Update translation JSON files").action(async()=>{await C()}),q.parse();
//# sourceMappingURL=cli.js.map