UNPKG

frontity

Version:

Frontity cli and entry point to other packages

362 lines (361 loc) 14 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.subscribe = exports.revertProgress = exports.createGitignore = exports.initializeGit = exports.downloadFavicon = exports.installDependencies = exports.cloneStarterTheme = exports.createTsConfig = exports.createFrontitySettings = exports.createReadme = exports.createPackageJson = exports.ensureProjectDir = exports.normalizeOptions = void 0; const os_1 = require("os"); const path_1 = require("path"); const child_process_1 = require("child_process"); const util_1 = require("util"); const fs_extra_1 = require("fs-extra"); const tar_1 = require("tar"); const node_fetch_1 = __importDefault(require("node-fetch")); const ramda_1 = require("ramda"); const utils_1 = require("../utils"); const allowedExistingContent = ["readme.md", "license", ".git", ".gitignore"]; const faviconUrl = "https://favicon.frontity.org/"; /** * This function normalizes and validates options. * * @param defaultOptions - The default options. Defined in {@link * CreateCommandOptions}. * @param passedOptions - The options from the user. Defined in {@link * CreateCommandOptions}. * * @returns The final options, normalized. Defined in {@link * CreateCommandOptions}. */ const normalizeOptions = (defaultOptions, passedOptions) => { const options = ramda_1.mergeRight(defaultOptions, passedOptions); // Normalize and validate `name` option. options.name = options.name.replace(/[\s_-]+/g, "-").toLowerCase(); if (!utils_1.isPackageNameValid(options.name)) throw new Error("The name of the package is not valid. Please enter a valid one (only letters and dashes)."); return options; }; exports.normalizeOptions = normalizeOptions; /** * This function ensures the path exists and checks if it's empty or it's a new * repo. Also returns a boolean indicating if the directory existed already. * * @param path - The path where the project will be installed. * * @returns A promise that resolves once the check has been made. */ const ensureProjectDir = async (path) => { const dirExisted = await fs_extra_1.pathExists(path); if (dirExisted) { // Check if the directory is a new repo. const dirContent = await fs_extra_1.readdir(path); const notAllowedContent = dirContent.filter((content) => !allowedExistingContent.includes(content.toLowerCase())); // If it's not, throw. if (notAllowedContent.length) { throw new Error("The directory passed to `create` function is not empty"); } } else { await fs_extra_1.ensureDir(path); } return dirExisted; }; exports.ensureProjectDir = ensureProjectDir; /** * Create a `package.json` file. * * @param name - The name of the project. * @param theme - The theme that will be cloned and installed locally. * @param path - The path where the file will be created. * @param typescript - Indicates if the typescript flag is active. */ const createPackageJson = async (name, theme, path, typescript) => { const packages = [ "frontity", "@frontity/core", "@frontity/wp-source", "@frontity/tiny-router", "@frontity/html2react", ]; // Add Frontity packages to the dependencies. const dependencies = (await Promise.all(packages.map(async (pkg) => { // Get the version of each package. const version = await utils_1.fetchPackageVersion(pkg); return [pkg, `^${version}`]; }))).reduce((final, current) => { // Reduce the packages into a dependecies object. final[current[0]] = current[1]; return final; }, {}); // Add the starter theme to the dependencies. const themeName = (theme.match(/\/?([\w-]+)$/) || ["", ""])[1]; dependencies[theme] = `./packages/${themeName}`; const packageJson = { name, version: "1.0.0", private: true, description: "Frontity project", keywords: ["frontity"], engines: { node: ">=10.0.0", npm: ">=6.0.0", }, scripts: { dev: "frontity dev", build: "frontity build", serve: "frontity serve", }, prettier: {}, dependencies, }; // If the typescript flag is active, add the needed devDependencies. if (typescript) { const devPackages = ["@types/react", "@types/node-fetch"]; const devDependencies = (await Promise.all(devPackages.map(async (pkg) => { // Get the version of each package. const version = await utils_1.fetchPackageVersion(pkg); return [pkg, `^${version}`]; }))).reduce((final, current) => { // Reduce the packages into a dependecies object. final[current[0]] = current[1]; return final; }, {}); packageJson.devDependencies = devDependencies; } const filePath = path_1.resolve(path, "package.json"); const fileData = `${JSON.stringify(packageJson, null, 2)}${os_1.EOL}`; await fs_extra_1.writeFile(filePath, fileData); }; exports.createPackageJson = createPackageJson; /** * Create a `README.md` file. * * @param name - The name of the project. * @param path - The path where the file will be created. */ const createReadme = async (name, path) => { const fileTemplate = await fs_extra_1.readFile(path_1.resolve(__dirname, "../../templates/README.md"), { encoding: "utf8", }); const filePath = path_1.resolve(path, "README.md"); const fileData = fileTemplate.replace(/\$name\$/g, name); await fs_extra_1.writeFile(filePath, fileData); }; exports.createReadme = createReadme; /** * Create a `frontity.settings` file. * * @param extension - The extension of the file, either `.js` or `.ts`. * @param name - The name of the project. * @param path - The path where the file will be created. * @param theme - The theme installed in the project. */ const createFrontitySettings = async (extension, name, path, theme) => { const frontitySettings = { name, state: { frontity: { url: "https://test.frontity.org", title: "Test Frontity Blog", description: "WordPress installation for Frontity development", }, }, packages: [ { name: theme, state: { theme: { menu: [ ["Home", "/"], ["Nature", "/category/nature/"], ["Travel", "/category/travel/"], ["Japan", "/tag/japan/"], ["About Us", "/about-us/"], ], featured: { showOnList: false, showOnPost: false, }, }, }, }, { name: "@frontity/wp-source", state: { source: { url: "https://test.frontity.org", }, }, }, "@frontity/tiny-router", "@frontity/html2react", ], }; const fileTemplate = await fs_extra_1.readFile(path_1.resolve(__dirname, `../../templates/settings-${extension}-template`), { encoding: "utf8" }); const filePath = path_1.resolve(path, `frontity.settings.${extension}`); const fileData = fileTemplate.replace(/\$([\w-]+)\$/g, (_match, key) => { if (key === "settings") return JSON.stringify(frontitySettings, null, 2); }); await fs_extra_1.writeFile(filePath, fileData); }; exports.createFrontitySettings = createFrontitySettings; /** * Create a `tsconfig.json` file. * * @param path - The path where the file will be created. */ const createTsConfig = async (path) => { const fileTemplate = await fs_extra_1.readFile(path_1.resolve(__dirname, "../../templates/tsconfig.json"), { encoding: "utf8", }); const filePath = path_1.resolve(path, "tsconfig.json"); await fs_extra_1.writeFile(filePath, fileTemplate); }; exports.createTsConfig = createTsConfig; /** * Clone the starter theme. * * @param theme - The name of the theme. * @param path - The path where it needs to be installed. */ const cloneStarterTheme = async (theme, path) => { const packageJsonPath = path_1.resolve(path, "./package.json"); const packageJson = JSON.parse(await fs_extra_1.readFile(packageJsonPath, { encoding: "utf8" })); const themePath = path_1.resolve(path, packageJson.dependencies[theme]); await fs_extra_1.ensureDir(themePath); if (!utils_1.isThemeNameValid(theme)) throw new Error("The name of the theme is not a valid npm package name."); await util_1.promisify(child_process_1.exec)(`npm pack ${theme}`, { cwd: themePath }); const tarball = (await fs_extra_1.readdir(themePath)).find((file) => /\.tgz$/.test(file)); const tarballPath = path_1.resolve(themePath, tarball); await tar_1.extract({ cwd: themePath, file: tarballPath, strip: 1 }); await fs_extra_1.remove(tarballPath); }; exports.cloneStarterTheme = cloneStarterTheme; /** * Install the Frontity packages. * * @param path - The location where `npm install` should be run. */ const installDependencies = async (path) => { await util_1.promisify(child_process_1.exec)("npm install", { cwd: path }); }; exports.installDependencies = installDependencies; /** * Downlaod the favicon file. * * @param path - The path where the favicon should be downloaded. */ const downloadFavicon = async (path) => { const response = await node_fetch_1.default(faviconUrl); const fileStream = fs_extra_1.createWriteStream(path_1.resolve(path, "favicon.ico")); response.body.pipe(fileStream); await new Promise((resolve) => fileStream.on("finish", resolve)); }; exports.downloadFavicon = downloadFavicon; /** * Initializes a new git repository. * * @param path - The path where git should be initialized. */ const initializeGit = async (path) => { try { child_process_1.execSync("git init", { cwd: path, stdio: "ignore" }); child_process_1.execSync("git add .", { cwd: path, stdio: "ignore" }); child_process_1.execSync('git commit -m "Initialized with Frontity"', { cwd: path, stdio: "ignore", }); } catch (e) { // If there is any issue we want to revert to "pre-git" state await fs_extra_1.remove(path_1.resolve(path, ".git")); // Rethrow the error so that it can be caught by the `create` command. throw e; } }; exports.initializeGit = initializeGit; /** * Creates a .gitignore file. * * @param path - The path where .gitignore file should be created. * @returns - A promise which resolves with a cleanup function which * should be called if any subsequent step fails. */ const createGitignore = async (path) => { const fileTemplate = await fs_extra_1.readFile(path_1.resolve(__dirname, "../../templates/gitignore-template"), { encoding: "utf8", }); const gitignorePath = path_1.resolve(path, ".gitignore"); const gitignoreExists = await fs_extra_1.pathExists(gitignorePath); if (gitignoreExists) { // Keep the existing .gitignore in memory in case we need to revert to it later. const backupGitignore = await fs_extra_1.readFile(gitignorePath, { encoding: "utf8", }); // Append if there's already a `.gitignore` file fs_extra_1.appendFileSync(gitignorePath, "node_modules\nbuild"); // Return a "cleanup" function return async () => { await fs_extra_1.writeFile(gitignorePath, backupGitignore); }; } else { await fs_extra_1.writeFile(gitignorePath, fileTemplate); // Return a "cleanup" function return async () => { // Remove the .gitignore file await fs_extra_1.remove(gitignorePath); }; } }; exports.createGitignore = createGitignore; /** * Remove the files and directories created with `frontity create` in case there * was a problem and we need to revert everything. * * @param dirExisted - If the directory existed already. * @param path - The path of the direcotry. */ const revertProgress = async (dirExisted, path) => { if (dirExisted) { const content = await fs_extra_1.readdir(path); const removableContent = content .filter((item) => !allowedExistingContent.includes(item.toLowerCase())) .map((item) => path_1.resolve(path, item)); for (const content of removableContent) await fs_extra_1.remove(content); } else { await fs_extra_1.remove(path); } }; exports.revertProgress = revertProgress; /** * Check if an email is valid or not. * * @param email - The email to be checked. * * @returns True or false depending if the email is valid. */ const isEmailValid = (email) => /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,63}$/i.test(email); /** * Subscribe an email to the newsletter service. * * @param email - The email to be subscribed. * * @returns The response of the subscription. */ const subscribe = async (email) => { if (!isEmailValid(email)) throw new Error("Email not valid. Please enter a valid email."); return node_fetch_1.default("https://n8n.frontity.org/webhook/62923334-59a4-484c-a9c2-632814b94225", { method: "POST", body: JSON.stringify({ event: "frontity-subscribe", email: email.toLowerCase(), }), headers: { "Content-Type": "application/json" }, }); }; exports.subscribe = subscribe;