UNPKG

@slimio/generator

Version:
366 lines (316 loc) 13.2 kB
#!/usr/bin/env node /* eslint-disable require-atomic-updates */ "use strict"; // Require Node.js Dependencies const { readFile, writeFile, unlink, readdir, copyFile, mkdir, rmdir } = require("fs").promises; const { join } = require("path"); const { performance } = require("perf_hooks"); // Require Third-party Dependencies const spawn = require("cross-spawn"); const qoa = require("qoa"); const Registry = require("@slimio/npm-registry"); const manifest = require("@slimio/manifest"); const Spinner = require("@slimio/async-cli-spinner"); const { gray, yellow, cyan, green, white, underline, red } = require("kleur"); const { downloadNodeFile, extract, constants: { File } } = require("@slimio/nodejs-downloader"); const { validate, CONSTANTS } = require("@slimio/validate-addon-name"); const { taggedString } = require("@slimio/utils"); const ms = require("ms"); // Require Internal Dependencies const DEFAULT_PKG = require("../template/package.json"); const { transfertFiles, filterPackageName, cppTemplate, upperCase } = require("../src/utils"); // CONSTANTS const FILE_INDENTATION = 4; const ROOT_DIR = join(__dirname, ".."); const TEMPLATE_DIR = join(ROOT_DIR, "template"); const DEFAULT_FILES_DIR = join(TEMPLATE_DIR, "defaultFiles"); const DEFAULT_FILES_INCLUDE = join(TEMPLATE_DIR, "include"); const DEFAULT_FILES_TEST = join(TEMPLATE_DIR, "test"); const { GEN_QUESTIONS, MODULES_QUESTIONS } = require("../src/questions.json"); const { DEV_DEPENDENCIES, NAPI_DEPENDENCIES } = require("../src/dependencies.json"); const TEST_SCRIPTS = { ava: taggedString`cross-env psp && ${0} ava --verbose`, japa: taggedString`cross-env psp && ${0} node test/test.js`, jest: taggedString`cross-env psp && jest --coverage` }; // Vars Spinner.DEFAULT_SPINNER = "dots"; /** * @async * @function downloadNAPIHeader * @description Download and extract NAPI Headers * @param {!string} dest include directory absolute path * @returns {Promise<void>} */ async function downloadNAPIHeader(dest) { const tarFile = await downloadNodeFile(File.Headers, { dest }); /** @type {string} */ let headerDir; try { headerDir = await extract(tarFile); } finally { await unlink(tarFile); } try { const [nodeVerDir] = await readdir(headerDir); const nodeDir = join(headerDir, nodeVerDir, "include", "node"); await Promise.allSettled([ copyFile(join(nodeDir, "napi.h"), join(dest, "napi.h")), copyFile(join(nodeDir, "napi-inl.h"), join(dest, "napi-inl.h")), copyFile(join(nodeDir, "node_api.h"), join(dest, "node_api.h")), copyFile(join(nodeDir, "node_api_types.h"), join(dest, "node_api_types.h")) ]); } finally { await rmdir(headerDir, { recursive: true }); } } /** * @async * @function generateTest * @param {!string} libName * @returns {Promise<void>} */ async function generateTest(libName) { const testPath = join(process.cwd(), "test"); await mkdir(testPath, { recursive: true }); const buf = await readFile(join(DEFAULT_FILES_TEST, `${libName}.js`)); await writeFile(join(testPath, "test.js"), buf.toString()); } /** * @async * @function getQueriesResponse * @returns {Promise<object>} */ async function getQueriesResponse() { const response = {}; let skipNext = false; for (const row of GEN_QUESTIONS) { if (skipNext) { skipNext = false; continue; } if (Reflect.has(row, "description")) { console.log(`\n ${yellow().bold("> note:")} ${gray().bold(row.description)}\n`); delete row.description; } row.query = underline().white().bold(row.query); if (row.type === "interactive") { row.symbol = "->"; } let ret; while (true) { ret = await qoa.prompt([row]); if (row.handle === "projectname") { ret.projectname = filterPackageName(ret.projectname); if (ret.projectname.length <= 1 || ret.projectname.length > 214) { console.log(red().bold("The project name must be of length 2<>214")); continue; } } if (row.handle === "testfw" && ret.testfw === "jest") { skipNext = true; response.covpackage = null; } break ; } Object.assign(response, ret); console.log(gray().bold("----------------------------")); } return response; } /** * @async * @function main * @description Main Generator CLI * @returns {Promise<void>} */ async function main() { const cwd = process.cwd(); if (cwd === ROOT_DIR || cwd === __dirname) { console.log(red().bold("Cannot execute at the root of the project")); process.exit(0); } console.log(gray().bold(`\n > Executing generator at ${yellow().bold(cwd)}\n`)); // Prompt all questions const response = await getQueriesResponse(); const projectName = response.projectname; // Check the addon package name if (response.type === "Addon" && !validate(projectName)) { console.log(red().bold(`The addon name not matching expected regex ${CONSTANTS.VALIDATE_REGEX}`)); process.exit(0); } console.log(gray().bold(`\n > Start configuring project ${cyan().bold(projectName)}\n`)); // Create initial package.json && write default projects files spawn.sync("npm", ["init", "-y"]); await transfertFiles(DEFAULT_FILES_DIR, cwd); await mkdir(join(cwd, "src"), { recursive: true }); DEFAULT_PKG.keywords.push("SlimIO", projectName); DEV_DEPENDENCIES.push(response.testfw); let coveragePrefix; switch (response.covpackage) { case "nyc": { DEV_DEPENDENCIES.push("nyc"); coveragePrefix = "nyc --reporter=lcov"; DEFAULT_PKG.scripts.report = "nyc report --reporter=html"; break; } case "c8": { DEV_DEPENDENCIES.push("c8"); coveragePrefix = "c8 -r=\"html\""; break; } default: // do nothing } DEFAULT_PKG.scripts.test = TEST_SCRIPTS[response.testfw](coveragePrefix); // Create .env file if (response.type === "Service" || response.env || response.covpackage === "c8") { DEV_DEPENDENCIES.push("dotenv"); const envData = response.covpackage === "c8" ? `NODE_V8_COVERAGE="${join(cwd, "coverage")}"\n` : ""; await writeFile(join(cwd, ".env"), envData); } // Create .npmrc file if (response.type === "Service" || response.npmrc) { const npmrcData = "package-lock=false"; await writeFile(join(cwd, ".npmrc"), npmrcData); } // If this is a NAPI project if (response.type === "NAPI") { // Push devDependencies for NAPI project DEV_DEPENDENCIES.push("node-gyp", "prebuildify", "cross-env"); // Update DEFAULT_PKG Scripts DEFAULT_PKG.scripts.prebuilds = "prebuildify --napi"; DEFAULT_PKG.scripts.build = "cross-env node-gyp configure && node-gyp build"; // Download include files const spinner = new Spinner({ prefixText: white().bold("Setup & configure N-API files") }); spinner.start("Downloading N-API Header"); try { const includeDir = join(cwd, "include"); const start = performance.now(); await mkdir(join(cwd, "include"), { recursive: true }); await downloadNAPIHeader(includeDir); await transfertFiles(DEFAULT_FILES_INCLUDE, includeDir); const buf = await readFile(join(TEMPLATE_DIR, "binding.gyp")); const gyp = JSON.parse(buf.toString()); gyp.targets[0].target_name = projectName; gyp.targets[0].sources = [`${projectName}.cpp`]; // Create .cpp file at the root of the project await Promise.all([ writeFile(join(cwd, `${projectName}.cpp`), cppTemplate(projectName)), writeFile(join(cwd, "binding.gyp"), JSON.stringify(gyp, null, FILE_INDENTATION)) ]); const executeTimeMs = green().bold(`${(performance.now() - start).toFixed(2)}ms`); spinner.succeed(`Done in ${executeTimeMs}`); } catch (err) { console.log(err); spinner.failed(err.message); } } // If the project is a binary project if (response.type === "CLI" || response.binary) { await mkdir(join(cwd, "bin"), { recursive: true }); await writeFile(join(cwd, "bin", "index.js"), "#!/usr/bin/env node"); const { binName } = await qoa.input({ query: white().bold("What is the name of the binary command ?"), handle: "binName" }); console.log(gray().bold("----------------------------\n")); DEFAULT_PKG.bin = { [binName]: "./bin/index.js" }; DEFAULT_PKG.husky.hooks["pre-push"] = "cross-env eslint bin/index.js && npm test"; } // Handle Package.json { const npmRegistry = new Registry(); const cwdPackage = join(cwd, "package.json"); const buf = await readFile(cwdPackage); const pkg = JSON.parse(buf.toString()); pkg.name = `@slimio/${projectName}`; pkg.version = response.version; pkg.description = response.projectdesc; pkg.dependencies = {}; pkg.devDependencies = {}; // Search for Dependencies if NAPI if (response.type === "NAPI") { const Packages = await Promise.all( NAPI_DEPENDENCIES.map((pkgName) => npmRegistry.package(pkgName)) ); for (const Pkg of Packages) { pkg.dependencies[Pkg.name] = `^${Pkg.lastVersion}`; } } // Search for DevDependencies const spinner = new Spinner({ prefixText: white().bold("Seeking latest package(s) version") }).start("Fetching..."); try { const start = performance.now(); const Packages = await Promise.all( DEV_DEPENDENCIES.map((pkgName) => npmRegistry.package(pkgName)) ); for (const Pkg of Packages) { pkg.devDependencies[Pkg.name] = `^${Pkg.lastVersion}`; } const executeTimeMs = green().bold(`${(performance.now() - start).toFixed(2)}ms`); spinner.succeed(`Fetched all latest versions in ${executeTimeMs}`); } catch (err) { spinner.failed(err.message); } await generateTest(response.testfw); await writeFile(cwdPackage, JSON.stringify(Object.assign(pkg, DEFAULT_PKG), null, FILE_INDENTATION)); } // Handle README.md const ReadmeSpinner = new Spinner({ prefixText: white().bold("Setup README.md") }).start("Read file"); try { const start = performance.now(); const buf = await readFile(join(TEMPLATE_DIR, "README.md")); const MDTemplate = response.type === "Addon" ? "addon.md" : "default.md"; const gettingStarted = await readFile(join(TEMPLATE_DIR, "readme", MDTemplate), "utf-8"); ReadmeSpinner.text = "Replacing inner variables..."; const finalReadme = buf.toString() .replace(/\${getting_started}/gm, gettingStarted) .replace(/\${lower-name}/gm, projectName.toLocaleLowerCase()) .replace(/\${title}/gm, upperCase(projectName)) .replace(/\${version}/gm, response.version) .replace(/\${package}/gm, `@slimio/${projectName}`) .replace(/\${desc}/gm, `${response.projectdesc}`); ReadmeSpinner.text = "Writing file to disk!"; await writeFile(join(cwd, "README.md"), finalReadme); const executeTimeMs = green().bold(`${(performance.now() - start).toFixed(2)}ms`); ReadmeSpinner.succeed(`Done in ${executeTimeMs}`); } catch (err) { ReadmeSpinner.failed(err.message); } // Write Manifest manifest.create({ name: projectName, version: response.version, type: response.type }, void 0, true); if (!response.binary) { await writeFile("index.js", "\"use strict\";\n"); } // Installation of nodes modules console.log(""); const { modules } = await qoa.prompt([MODULES_QUESTIONS]); if (modules) { const spinner = new Spinner().start(white().bold(`Running '${cyan().bold("npm install")}' on node_modules ...`)); try { const start = performance.now(); const child = spawn("npm", ["install"]); await new Promise((resolve, reject) => { child.once("close", resolve); child.once("error", reject); }); const executeTimeMs = green().bold(`${ms(performance.now() - start)}`); spinner.succeed(white().bold(`Packages installed in ${executeTimeMs}`)); } catch (err) { spinner.failed(red().bold(err.message)); } } console.log(gray().bold("\n > Done with no errors...\n\n")); } main().catch(console.error);