/* eslint-disable no-console */ const path = require("path"); const fs = require("fs"); const fse = require("fs-extra"); const glob = require("glob"); const yaml = require("js-yaml"); const chalk = require("chalk"); const handlebars = require("handlebars"); const Mustache = require("mustache"); const { version } = require("../lerna.json"); const capitalize = require("@freesewing/utils/capitalize") const repoPath = process.cwd(); const config = { repoPath, defaults: readConfigFile("defaults.yaml"), descriptions: readConfigFile("descriptions.yaml"), keywords: readConfigFile("keywords.yaml"), badges: readConfigFile("badges.yaml"), scripts: readConfigFile("scripts.yaml"), changelog: readConfigFile("changelog.yaml"), changetypes: [ "Added", "Changed", "Deprecated", "Removed", "Fixed", "Security" ], dependencies: readConfigFile("dependencies.yaml", { version }), exceptions: readConfigFile("exceptions.yaml"), templates: { pkg: readTemplateFile("package.dflt.json"), rollup: readTemplateFile("rollup.config.dflt.js"), changelog: readTemplateFile("changelog.dflt.md"), readme: readTemplateFile("readme.dflt.md") } }; const packages = glob.sync("*", { cwd: path.join(config.repoPath, "packages") }); validate(packages, config); reconfigure(packages, config); process.exit(); /** * Reads a template file */ function readTemplateFile(file) { return fs.readFileSync( path.join(repoPath, "config", "templates", file), "utf-8" ); } /** * Reads a pattern example file */ function readExampleFile(file, subdir = false) { return fs.readFileSync( subdir ? path.join( repoPath, "packages", "create-freesewing-pattern", "template", "default", "example", file ) : path.join( repoPath, "packages", "create-freesewing-pattern", "template", "default", "example", subdir, file ), "utf-8" ); } /** * Reads a YAML config file, with Mustache replacements if needed */ function readConfigFile(file, replace = false) { if (replace) return yaml.safeLoad( Mustache.render( fs.readFileSync(path.join(repoPath, "config", file), "utf-8"), replace ) ); return yaml.safeLoad( fs.readFileSync(path.join(repoPath, "config", file), "utf-8") ); } /** * Reads info.md from the package directory * Returns its contents if it exists, or an empty string if not */ function readInfoFile(pkg) { let markup = ""; try { markup = fs.readFileSync( path.join(repoPath, "packages", pkg, "info.md"), "utf-8" ); } catch (err) { return ""; } return markup; } /** * Figure out what sort of package this is. * Returns a string, one of: * - pattern * - plugin * - other */ function packageType(pkg, config) { if (pkg.substring(0, 7) === "plugin-") return "plugin"; if (config.descriptions[pkg].substring(0, 21) === "A FreeSewing pattern ") return "pattern"; return "other"; } /** * Returns an array of keywords for a package */ function keywords(pkg, config, type) { if (typeof config.keywords[pkg] !== "undefined") return config.keywords[pkg]; if (typeof config.keywords[type] !== "undefined") return config.keywords[type]; else { console.log( chalk.redBright.bold("Problem:"), chalk.redBright(`No keywords for package ${pkg} which is of type ${type}`) ); process.exit(); } } /** * Returns an plain object of scripts for a package */ function scripts(pkg, config, type) { let runScripts = {}; for (let key of Object.keys(config.scripts._)) { runScripts[key] = Mustache.render(config.scripts._[key], { name: pkg }); } if (typeof config.scripts._types[type] !== "undefined") { for (let key of Object.keys(config.scripts._types[type])) { runScripts[key] = Mustache.render(config.scripts._types[type][key], { name: pkg }); } } if (typeof config.scripts[pkg] !== "undefined") { for (let key of Object.keys(config.scripts[pkg])) { if (config.scripts[pkg][key] === "!") delete runScripts[key]; else runScripts[key] = Mustache.render(config.scripts[pkg][key], { name: pkg }); } } return runScripts; } /** * Returns an plain object with the of dependencies for a package * section is the key in the dependencies.yaml fine, one of: * * - _ (for dependencies) * - dev (for devDependencies) * - peer (for peerDependencies) * */ function deps(section, pkg, config, type) { let dependencies = {}; if ( typeof config.dependencies._types[type] !== "undefined" && typeof config.dependencies._types[type][section] !== "undefined" ) dependencies = config.dependencies._types[type][section]; if (typeof config.dependencies[pkg] === "undefined") return dependencies; if (typeof config.dependencies[pkg][section] !== "undefined") return { ...dependencies, ...config.dependencies[pkg][section] }; return dependencies; } /** * These merely call deps() for the relevant dependency section */ function dependencies(pkg, config, type) { return deps("_", pkg, config, type); } function devDependencies(pkg, config, type) { return deps("dev", pkg, config, type); } function peerDependencies(pkg, config, type) { return deps("peer", pkg, config, type); } /** * Creates a package.json file for a package */ function packageConfig(pkg, config) { let type = packageType(pkg, config); let pkgConf = {}; // Let's keep these at the top pkgConf.name = fullName(pkg, config); pkgConf.version = version; pkgConf.description = config.descriptions[pkg]; pkgConf = { ...pkgConf, ...JSON.parse(Mustache.render(config.templates.pkg, { name: pkg })) }; pkgConf.keywords = pkgConf.keywords.concat(keywords(pkg, config, type)); pkgConf.scripts = scripts(pkg, config, type); pkgConf.dependencies = dependencies(pkg, config, type); pkgConf.devDependencies = devDependencies(pkg, config, type); pkgConf.peerDependencies = peerDependencies(pkg, config, type); if (typeof config.exceptions.packageJson[pkg] !== "undefined") { pkgConf = { ...pkgConf, ...config.exceptions.packageJson[pkg] }; for (let key of Object.keys(config.exceptions.packageJson[pkg])) { if (config.exceptions.packageJson[pkg][key] === "!") delete pkgConf[key]; } } return pkgConf; } /** * Returns an string with the markup for badges in the readme file */ function badges(pkg, config) { let markup = ""; for (let group of ["_all", "_social"]) { markup += "

"; for (let key of Object.keys(config.badges[group])) { markup += formatBadge( config.badges[group][key], pkg, fullName(pkg, config) ); } markup += "

"; } return markup; } /** * Formats a badge for a readme file */ function formatBadge(badge, name, fullname) { return `${Mustache.render(badge.alt, { name, fullname })} `; } /** * Returns the full (namespaced) name of a package */ function fullName(pkg, config) { if (config.exceptions.noNamespace.indexOf(pkg) !== -1) return pkg; else return `@freesewing/${pkg}`; } /** * Creates a README.md file for a package */ function readme(pkg, config) { let markup = Mustache.render(config.templates.readme, { fullname: fullName(pkg, config), description: config.descriptions[pkg], badges: badges(pkg, config), info: readInfoFile(pkg) }); return markup; } /** * Creates a CHANGELOG.md file for a package */ function changelog(pkg, config) { let markup = Mustache.render(config.templates.changelog, { fullname: pkg === "global" ? "FreeSewing (global)" : fullName(pkg, config), changelog: pkg === "global" ? globalChangelog(config) : packageChangelog(pkg, config) }); return markup; } /** * Generates the global changelog data */ function globalChangelog(config) { let markup = ""; for (let v in config.changelog) { let changes = config.changelog[v]; markup += "\n## " + v; if (v !== "Unreleased") markup += " (" + formatDate(changes.date) + ")"; markup += "\n\n"; for (let pkg of packages) { let changed = false; for (let type of config.changetypes) { if ( typeof changes[type] !== "undefined" && changes[type] !== null && typeof changes[type][pkg] !== "undefined" && changes[type][pkg] !== null ) { if (!changed) changed = ""; changed += "\n#### " + type + "\n\n"; for (let change of changes[type][pkg]) changed += " - " + change + "\n"; } } if (changed) markup += "### " + pkg + "\n" + changed + "\n"; } } return markup; } /** * Generates the changelog data for a package */ function packageChangelog(pkg, config) { let markup = ""; for (let v in config.changelog) { let changes = config.changelog[v]; let changed = false; for (let type of config.changetypes) { if ( typeof changes[type] !== "undefined" && changes[type] !== null && typeof changes[type][pkg] !== "undefined" && changes[type][pkg] !== null ) { if (!changed) changed = ""; changed += "\n### " + type + "\n\n"; for (let change of changes[type][pkg]) changed += " - " + change + "\n"; } } markup += "## " + v; if (v !== "Unreleased") markup += " (" + formatDate(changes.date) + ")"; markup += "\n"; markup += changed ? changed : `\n**Note:** Version bump only for package ${pkg}\n\n\n`; } return markup; } function formatDate(date) { let d = new Date(date), month = "" + (d.getMonth() + 1), day = "" + d.getDate(), year = d.getFullYear(); if (month.length < 2) month = "0" + month; if (day.length < 2) day = "0" + day; return [year, month, day].join("-"); } /** * Make sure we have (at least) a description for each package */ function validate(pkgs, config) { console.log(chalk.blueBright("Validating package descriptions")); for (let pkg of pkgs) { if (typeof config.descriptions[pkg] !== "string") { console.log( chalk.redBright.bold("Problem:"), chalk.redBright(`No description for package ${pkg}`) ); process.exit(); } } console.log(chalk.yellowBright.bold("Looks good")); return true; } /** * Creates and 'example' directory for patterns, * same result as what gets done by create-freesewing-pattern. */ function configurePatternExample(pkg, config) { // Create example dir structure let source = path.join( config.repoPath, "packages", "create-freesewing-pattern", "template", "default", "example" ); let dest = path.join(config.repoPath, "packages", pkg, "example"); fse.ensureDirSync(path.join(dest, "src")); fse.ensureDirSync(path.join(dest, "public")); // Copy files for (let file of [".babelrc", ".env"]) fs.copyFileSync(path.join(source, file), path.join(dest, file)); for (let file of ["index.js", "serviceWorker.js"]) fs.copyFileSync( path.join(source, "src", file), path.join(dest, "src", file) ); fs.copyFileSync( path.join(source, "public", "favicon.ico"), path.join(dest, "public", "favicon.ico") ); // Write templates let replace = { name: pkg, author: "freesewing", yarn: true, language: "en" }; for (let file of ["package.json", "README.md"]) { let template = handlebars.compile( fs.readFileSync(path.join(source, file), "utf-8") ); fs.writeFileSync(path.join(dest, file), template(replace)); } for (let file of ["index.html", "manifest.json"]) { let template = handlebars.compile( fs.readFileSync(path.join(source, "public", file), "utf-8") ); fs.writeFileSync(path.join(dest, "public", file), template(replace)); } let template = handlebars.compile( fs.readFileSync(path.join(source, "src", "App.js"), "utf-8") ); fs.writeFileSync(path.join(dest, "src", "App.js"), template(replace)); } /** * Adds unit tests for patterns */ function configurePatternUnitTests(pkg, config) { // Create tests directory let dest = path.join(config.repoPath, "packages", pkg, "tests"); fse.ensureDirSync(dest) let source = path.join( config.repoPath, "config", "templates", "tests", "patterns" ); // Write templates let replace = { pattern: pkg, Pattern: capitalize(pkg), peerdeps: Object.keys(peerDependencies(pkg, config, 'pattern')).join(' ') }; for (let file of ["shared.test.js"]) { fs.writeFileSync( path.join(dest, file), Mustache.render( fs.readFileSync(path.join(source, file+'.template'), "utf-8"), replace ) ); } // Add workflow file for Github actions fs.writeFileSync( path.join( config.repoPath, '.github', 'workflows', `tests.${pkg}.yml` ), Mustache.render( fs.readFileSync(path.join( config.repoPath, 'config', 'templates', 'workflows', 'tests.pattern.yml' ), "utf-8"), replace ) ); } /** * Puts a package.json, rollup.config.js, README.md, and CHANGELOG.md * into every subdirectory under the packages directory. * Also creates an example dir for pattern packages, and writes * the global CHANGELOG.md. * New: Adds unit tests for patterns */ function reconfigure(pkgs, config) { for (let pkg of pkgs) { console.log(chalk.blueBright(`Reconfiguring ${pkg}`)); let pkgConfig = packageConfig(pkg, config); fs.writeFileSync( path.join(config.repoPath, "packages", pkg, "package.json"), JSON.stringify(pkgConfig, null, 2) + "\n" ); if (config.exceptions.customRollup.indexOf(pkg) === -1) { fs.writeFileSync( path.join(config.repoPath, "packages", pkg, "rollup.config.js"), config.templates.rollup ); } fs.writeFileSync( path.join(config.repoPath, "packages", pkg, "README.md"), readme(pkg, config) ); fs.writeFileSync( path.join(config.repoPath, "packages", pkg, "CHANGELOG.md"), changelog(pkg, config) ); if (packageType(pkg, config) === "pattern") { configurePatternExample(pkg, config); configurePatternUnitTests(pkg, config); } } fs.writeFileSync( path.join(config.repoPath, "CHANGELOG.md"), changelog("global", config) ); console.log(chalk.yellowBright.bold("All done.")); }