frontity
Version:
Frontity cli and entry point to other packages
362 lines (361 loc) • 14 kB
JavaScript
;
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",
".ds_store",
];
const faviconUrl = "https://frontity.org/wp-content/plugins/frontity-favicon/favicon.ico";
/**
* 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 = (0, ramda_1.mergeRight)(defaultOptions, passedOptions);
// Normalize and validate `name` option.
options.name = options.name.replace(/[\s_-]+/g, "-").toLowerCase();
if (!(0, 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 (0, fs_extra_1.pathExists)(path);
if (dirExisted) {
// Check if the directory is a new repo.
const dirContent = await (0, 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 (0, 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 (0, 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 (0, 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 = (0, path_1.resolve)(path, "package.json");
const fileData = `${JSON.stringify(packageJson, null, 2)}${os_1.EOL}`;
await (0, 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 (0, fs_extra_1.readFile)((0, path_1.resolve)(__dirname, "../../templates/README.md"), {
encoding: "utf8",
});
const filePath = (0, path_1.resolve)(path, "README.md");
const fileData = fileTemplate.replace(/\$name\$/g, name);
await (0, 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 (0, fs_extra_1.readFile)((0, path_1.resolve)(__dirname, `../../templates/settings-${extension}-template`), { encoding: "utf8" });
const filePath = (0, path_1.resolve)(path, `frontity.settings.${extension}`);
const fileData = fileTemplate.replace(/\$([\w-]+)\$/g, (_match, key) => {
if (key === "settings")
return JSON.stringify(frontitySettings, null, 2);
if (key === "theme")
return theme;
});
await (0, 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 (0, fs_extra_1.readFile)((0, path_1.resolve)(__dirname, "../../templates/tsconfig.json"), {
encoding: "utf8",
});
const filePath = (0, path_1.resolve)(path, "tsconfig.json");
await (0, 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 = (0, path_1.resolve)(path, "./package.json");
const packageJson = JSON.parse(await (0, fs_extra_1.readFile)(packageJsonPath, { encoding: "utf8" }));
const themePath = (0, path_1.resolve)(path, packageJson.dependencies[theme]);
await (0, fs_extra_1.ensureDir)(themePath);
if (!(0, utils_1.isThemeNameValid)(theme))
throw new Error("The name of the theme is not a valid npm package name.");
await (0, util_1.promisify)(child_process_1.exec)(`npm pack ${theme}`, { cwd: themePath });
const tarball = (await (0, fs_extra_1.readdir)(themePath)).find((file) => /\.tgz$/.test(file));
const tarballPath = (0, path_1.resolve)(themePath, tarball);
await (0, tar_1.extract)({ cwd: themePath, file: tarballPath, strip: 1 });
await (0, 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 (0, 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 (0, node_fetch_1.default)(faviconUrl);
const fileStream = (0, fs_extra_1.createWriteStream)((0, 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 {
(0, child_process_1.execSync)("git init", { cwd: path, stdio: "ignore" });
(0, child_process_1.execSync)("git add .", { cwd: path, stdio: "ignore" });
(0, 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 (0, fs_extra_1.remove)((0, 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 (0, fs_extra_1.readFile)((0, path_1.resolve)(__dirname, "../../templates/gitignore-template"), {
encoding: "utf8",
});
const gitignorePath = (0, path_1.resolve)(path, ".gitignore");
const gitignoreExists = await (0, 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 (0, fs_extra_1.readFile)(gitignorePath, {
encoding: "utf8",
});
// Append if there's already a `.gitignore` file
(0, fs_extra_1.appendFileSync)(gitignorePath, "node_modules\nbuild");
// Return a "cleanup" function
return async () => {
await (0, fs_extra_1.writeFile)(gitignorePath, backupGitignore);
};
}
else {
await (0, fs_extra_1.writeFile)(gitignorePath, fileTemplate);
// Return a "cleanup" function
return async () => {
// Remove the .gitignore file
await (0, 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 (0, fs_extra_1.readdir)(path);
const removableContent = content
.filter((item) => !allowedExistingContent.includes(item.toLowerCase()))
.map((item) => (0, path_1.resolve)(path, item));
for (const content of removableContent)
await (0, fs_extra_1.remove)(content);
}
else {
await (0, 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: string): boolean =>
// /^[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 () => {
throw new Error("Frontity newsletter is currently disabled");
};
exports.subscribe = subscribe;