UNPKG

@vtex/fsp-cli

Version:

A VTEX CLI

687 lines (676 loc) 20.3 kB
// src/commands/dev.ts import { Args, Command } from "@oclif/core"; import { start } from "@vtex/fsp-local"; // src/modules.ts import { readFileSync } from "fs"; import path from "path"; import { loadConfig } from "@vtex/fsp-config"; var moduleCliMap = { checkout: "@vtex/checkout", discovery: "@faststore/cli", "sales-app": "@vtex/sales-app" }; var moduleCLIPathMap = { "@vtex/checkout": "@vtex/checkout/cli", "@faststore/cli": "@faststore/cli", "@vtex/sales-app": "@vtex/sales-app/cli" }; var availableModules = Object.keys(moduleCliMap); async function loadModules(account, moduleFilterFn = () => true) { const { stores } = await loadConfig(); const accountConfigs = stores[account]; if (!accountConfigs) { const availableAccounts = Object.keys(stores).join(", "); throw new Error( `Could not find account "${account}". Found accounts: ${availableAccounts}` ); } const modules = Object.keys(accountConfigs).filter(moduleFilterFn).map((module) => { const moduleConfig = accountConfigs[module]; const cli = moduleConfig?.cli ?? moduleCliMap[module]; if (!cli) { throw new Error("CLI not found! Provide a valid module or a CLI"); } return { ...accountConfigs[module], cli }; }); return await Promise.all(modules.map((module) => loadModule(module))); } async function loadModule(module) { const foundDirectory = path.join(process.cwd(), module.path); if (!foundDirectory) { throw new Error("Module not found"); } const loadedCli = await load(module); return { ...module, loadedCli }; } async function load(module) { let rootPackageJson = {}; try { rootPackageJson = JSON.parse( readFileSync(path.join(process.cwd(), "package.json")).toString() ); } catch { throw new Error("Could not find package.json"); } if (!rootPackageJson.devDependencies?.[module.cli]) { throw new Error( `You must add ${module.cli} to your devDependencies and install it` ); } try { const importedModule = await import(moduleCLIPathMap[module.cli]); const isEsm = !!importedModule.__esModule; return isEsm ? importedModule.default : importedModule; } catch { throw new Error(`Could not import module ${module.cli}`); } } // src/commands/dev.ts var Dev = class _Dev extends Command { static args = { account: Args.string({ required: true, description: "Store name key to be considered. It must match the keys on faststore.json, otherwise the first one found will be used." }) }; static description = "Runs the development server for the local storefront environment."; static examples = ["<%= config.bin %> <%= command.id %>"]; static flags = {}; async run() { this.log("Running faststore dev \u{1F680}"); const { args: { account } } = await this.parse(_Dev); const modules = await loadModules(account); try { await this.runPreTasks({ modules, account }); await this.runMainTasks({ modules, account }); await this.runPostTasks({ modules, account }); await start(account); } catch (error) { this.logToStderr("Something went wrong."); console.log(error); } } async runMainTasks({ modules, account }) { try { await Promise.all( modules.map(({ loadedCli, path: path6, port }) => { const { dev } = loadedCli.commands; return dev.run([account, path6, port?.toString() ?? ""]); }) ); } catch (error) { console.log(error); } } async runPostTasks({ modules }) { try { await Promise.all( modules.map(({ loadedCli }) => { const postDev = loadedCli.hooks?.postDev; if (postDev) return postDev(); return Promise.resolve(); }) ); } catch (error) { console.log(error); } } async runPreTasks({ modules }) { try { await Promise.all( modules.map(({ loadedCli }) => { const preDev = loadedCli.hooks?.preDev; if (preDev) return preDev(); return Promise.resolve(); }) ); } catch (error) { this.logToStderr("Something went wrong."); console.log(error); } } }; // src/commands/build.ts import { Args as Args2, Command as Command2 } from "@oclif/core"; var Build = class _Build extends Command2 { static args = { account: Args2.string({ required: true, description: "Store name key to be considered. It must match the keys on faststore.json, otherwise the first one found will be used." }), moduleList: Args2.string({ required: false, description: "Modules to build. Separated by comma (,)" }) }; static description = "Initiates the build process for the storefront project."; static examples = ["<%= config.bin %> <%= command.id %>"]; static flags = {}; async run() { this.log("Running faststore build \u{1F680}"); const { args: { account, moduleList } } = await this.parse(_Build); const modules = await loadModules( account, moduleList ? (currentModule) => moduleList.split(",").includes(currentModule) : () => true ); try { await this.runPreTasks({ modules, account }); await this.runMainTasks({ modules, account }); await this.runPostTasks({ modules, account }); } catch (error) { this.logToStderr("Something went wrong."); console.log(error); } } async runMainTasks({ modules, account }) { try { await Promise.all( modules.map(({ loadedCli, path: path6 }) => { const { build } = loadedCli.commands; return build.run([account, path6]); }) ); } catch (error) { console.log(error); } } async runPostTasks({ modules }) { try { await Promise.all( modules.map(({ loadedCli }) => { const postBuild = loadedCli.hooks?.postBuild; if (postBuild) return postBuild(); return Promise.resolve(); }) ); } catch (error) { console.log(error); } } async runPreTasks({ modules }) { try { await Promise.all( modules.map(({ loadedCli }) => { const preBuild = loadedCli.hooks?.preBuild; if (preBuild) return preBuild(); return Promise.resolve(); }) ); } catch (error) { this.logToStderr("Something went wrong."); console.log(error); } } }; // src/commands/create.ts import { writeFile } from "fs/promises"; import path2 from "path"; import { cwd } from "process"; import { input, select } from "@inquirer/prompts"; import { Args as Args3, Command as Command3 } from "@oclif/core"; import { loadConfig as loadConfig2 } from "@vtex/fsp-config"; var Create = class _Create extends Command3 { static ACCOUNT_PROMPT = "What is the account name?"; static MODULE_PROMPT = "Which module do you want to initialize?"; static PATH_PROMPT = (moduleName) => { return `What should be the path to initialize ${moduleName}?`; }; static args = { account: Args3.string({ required: false, description: "Name of the account to be initialized" }), moduleName: Args3.string({ required: false, description: "Name of the module to be initialized" }), path: Args3.string({ required: false, description: "Path of where to initialize the module" }) }; static description = "Add a new faststore module on the monorepo."; static examples = ["<%= config.bin %> <%= command.id %>"]; static flags = {}; async run() { const { args } = await this.parse(_Create); let account = args.account; if (!account) { account = await input({ message: _Create.ACCOUNT_PROMPT }); } let moduleName = args.moduleName; if (!moduleName) { moduleName = await select({ message: _Create.MODULE_PROMPT, choices: availableModules.map((module) => ({ name: module, value: module })) }); } let modulePath = args.path; if (!modulePath) { modulePath = await input({ message: _Create.PATH_PROMPT(moduleName), default: `./packages/${moduleName}` }); } const currentConfig = await loadConfig2(); currentConfig.stores = currentConfig.stores ?? {}; if (currentConfig.stores?.[account]?.[moduleName]) { this.error(`${moduleName} has already been initialized for ${account}.`, { code: "Already Initialized Module", exit: 1 }); } const accountConfig = currentConfig.stores[account] ?? {}; const hasModules = Object.keys(accountConfig).length > 0; let modulePort = 3001; if (hasModules) { let largestPort = modulePort; for (const module of Object.keys(accountConfig)) { const port = accountConfig[module].port; if (port && port > largestPort) { largestPort = port; } } modulePort = largestPort + 1; } currentConfig.stores[account] = { ...accountConfig, [moduleName]: { path: modulePath, port: modulePort } }; this.log(`Initializing ${moduleName} for ${account} in ${modulePath}`); const cli = await load({ cli: moduleCliMap[moduleName], path: modulePath }); await cli.commands.create.run([modulePath]); await writeFile( path2.join(cwd(), "faststore.json"), `${JSON.stringify(currentConfig, void 0, 2)} ` ); } }; // src/commands/serve.ts import { Args as Args4, Command as Command4 } from "@oclif/core"; var Serve = class _Serve extends Command4 { static args = { account: Args4.string({ required: true, description: "Store name key to be considered. It must match the keys on faststore.json, otherwise the first one found will be used." }) }; static description = "Runs the local server for the storefront environment."; static examples = ["<%= config.bin %> <%= command.id %>"]; static flags = {}; async run() { this.log("Running faststore serve \u{1F680}"); const { args: { account } } = await this.parse(_Serve); const modules = await loadModules(account); try { await this.runPreTasks({ modules, account }); await this.runMainTasks({ modules, account }); await this.runPostTasks({ modules, account }); } catch (error) { this.logToStderr("Something went wrong."); console.log(error); } } async runMainTasks({ modules, account }) { try { await Promise.all( modules.map(({ loadedCli, path: path6, port }) => { const { serve } = loadedCli.commands; return serve.run([account, path6, port?.toString() ?? ""]); }) ); } catch (error) { console.log(error); } } async runPostTasks({ modules }) { try { await Promise.all( modules.map(({ loadedCli }) => { const postServe = loadedCli.hooks?.postServe; if (postServe) return postServe(); return Promise.resolve(); }) ); } catch (error) { console.log(error); } } async runPreTasks({ modules }) { try { await Promise.all( modules.map(({ loadedCli }) => { const preServe = loadedCli.hooks?.preServe; if (preServe) return preServe(); return Promise.resolve(); }) ); } catch (error) { this.logToStderr("Something went wrong."); console.log(error); } } }; // src/commands/init.ts import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, renameSync, unlinkSync, writeFileSync } from "fs"; import path5 from "path"; import { cwd as cwd2 } from "process"; import { fileURLToPath } from "url"; import { checkbox, input as input2 } from "@inquirer/prompts"; import { Command as Command5, Flags } from "@oclif/core"; import { loadConfig as loadConfig3 } from "@vtex/fsp-config"; import merge from "deepmerge"; import Handlebars from "handlebars"; import ora from "ora"; import * as prettier from "prettier"; // src/utils/copy-file.ts import { cpSync, existsSync, mkdirSync } from "fs"; import path3 from "path"; function copyFile(sourceDir, destDir, file, rename) { if (!existsSync(destDir)) { mkdirSync(destDir, { recursive: true }); } const sourceFile = path3.join(sourceDir, file); const destFile = path3.join(destDir, rename ?? file); try { cpSync(sourceFile, destFile, { recursive: true }); } catch (e) { console.error(e); } } // src/utils/get-file-info.ts import path4 from "path"; function getFileInfo(file) { const extension = path4.extname(file); const name = path4.basename(file, extension); return { name, extension }; } // src/utils/npm-registry.ts async function getPackageLatestVersion(packageName) { try { const request = await fetch(`https://registry.npmjs.org/${packageName}`); const { "dist-tags": distTags } = await request.json(); return distTags.latest; } catch { return "latest"; } } // src/commands/init.ts var Init = class _Init extends Command5 { static args = {}; static description = "Initialize a new FastStore monorepo project from scratch."; static examples = ["<%= config.bin %> <%= command.id %>"]; static flags = { "from-discovery": Flags.boolean({ required: false, description: "Migrates the current faststore discovery to the monorepo structure" }) }; async run() { const currentConfig = await loadConfig3(); if (currentConfig?.stores) { this.error("Already initialized"); } const { flags } = await this.parse(_Init); if (flags["from-discovery"]) { await this.migrate(); } else { this.freshStart(); } } /** * Starts a store fresh */ async freshStart() { const appName = await input2({ message: "What is the application name?", default: "faststore-app" }); if (appName.length === 0) { this.error("App name is required"); } await this.execTemplate({ templateName: "default", destination: path5.join(process.cwd(), appName), aditionalDependencies: ["@biomejs/biome", "turbo"], optionalTemplateData: { package: { name: appName } } }); } /** * Use a template from the available templates */ async execTemplate(props) { const { templateName, destination, aditionalDependencies = [], optionalTemplateData = {} } = props; try { this.log("Copying template files"); const devDependencies = await this.fetchDevDependencies( aditionalDependencies ); const templateData = merge( { package: { name: "faststore-monorepo", devDependencies } }, optionalTemplateData ); Handlebars.registerHelper("json", (context) => { return JSON.stringify(context, void 0, 2); }); const dirname = path5.dirname(fileURLToPath(import.meta.url)); const templatePath = path5.join(dirname, "../src/templates", templateName); const templateDirectory = readdirSync(templatePath); for (const file of templateDirectory) { const fileInfo = getFileInfo(file); const filePath = path5.join(templatePath, file); if (fileInfo.extension !== ".hbs") { copyFile(templatePath, destination, file); continue; } const parserOptions = { ".json": "json", ".js": "babel", ".ts": "babel-ts" }; const fileBuffer = readFileSync2(filePath); const handlebarsTemplate = Handlebars.compile(fileBuffer.toString()); let textContent = handlebarsTemplate(templateData); const newFileExtension = path5.extname(fileInfo.name); const parser = parserOptions[newFileExtension]; if (parser) { try { textContent = await prettier.format(textContent, { semi: false, singleQuote: true, trailingComma: "es5", parser }); } catch (e) { console.error(e); } } writeFileSync(path5.join(destination, fileInfo.name), textContent); } this.log("All files copied"); } catch (e) { this.log("Could not copy files"); } } async fetchDevDependencies(additionalDeps = []) { const devDependencies = {}; const spinner = ora("Fetching dependencies"); try { spinner.start(); const allClis = Object.values(moduleCliMap); const dependenciesToFetch = [ "@vtex/fsp-cli", ...allClis, ...additionalDeps ]; const appsVersion = await Promise.all( dependenciesToFetch.map(getPackageLatestVersion) ); dependenciesToFetch.forEach((dependency, index) => { devDependencies[dependency] = appsVersion[index]; }); return devDependencies; } catch { spinner.fail("Could not fetch dependencies"); return devDependencies; } finally { spinner.succeed("All dependencies fetched"); } } /** * Migrates an existent store to the monorepo struture */ async migrate() { this.log("\u26A1 Starting the migration of your store"); const sourceDir = cwd2(); const destDir = path5.join(cwd2(), "packages/discovery"); const files = readdirSync(sourceDir); if (files.length === 0) { return; } const filesToIgnore = { ".git": true, ".github": true, "yarn.lock": true }; const mandatoryFiles = { src: true, "vtex.env": true, "vercel.json": true, packages: true, public: true, "next-env.d.ts": true, "package.json": true, "faststore.config.js": true, "discovery.config.js": true, ".gitignore": true }; const fileRenames = { "faststore.config.js": "discovery.config.js" }; const filesToDelete = { "vercel.json": true }; const optionalFilesToIgnore = await checkbox({ message: "Choose the files that should remain on the repository", choices: files.filter( (file) => !filesToIgnore[file] && !mandatoryFiles[file] ).map((file) => ({ name: file, value: file })) }); for (const file of optionalFilesToIgnore) { filesToIgnore[file] = true; } let accountName = ""; for (const file of files) { if (filesToIgnore[file]) { continue; } if (filesToDelete[file]) { try { unlinkSync(path5.join(sourceDir, file)); this.log(`\u2705 ${file} deleted`); } catch { this.log(`\u274C Could not delete ${file}`); } continue; } if (file === "package.json") { const { name = "" } = JSON.parse( readFileSync2(file).toString() ); accountName = String(name).replace(".store", ""); } this.moveFile( sourceDir, destDir, file, fileRenames[file] ); } await this.execTemplate({ templateName: "from-discovery", destination: sourceDir, aditionalDependencies: ["turbo"], optionalTemplateData: { accountName } }); this.log("\u{1F984} Store migrated. You can install your packages with yarn."); } /** * Move a file between two directories. * It handles the creation of the destDir case its needed. * @param sourceDir Path of the source directory * @param destDir Path of the destination directory * @param file Name of the file */ moveFile(sourceDir, destDir, file, rename) { if (!existsSync2(destDir)) { mkdirSync2(destDir, { recursive: true }); } const sourceFile = path5.join(sourceDir, file); const destFile = path5.join(destDir, rename ?? file); try { renameSync(sourceFile, destFile); this.log(`\u2705 Moved: ${file}`); } catch { this.log(`\u274C Could not move the file: ${file}`); } } }; // src/index.ts var COMMANDS = { dev: Dev, serve: Serve, create: Create, build: Build, init: Init }; export { COMMANDS }; //# sourceMappingURL=index.js.map