/* 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.load( Mustache.render(fs.readFileSync(path.join(repoPath, 'config', file), 'utf-8'), replace) ) return yaml.load(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] } } if (config.exceptions.namedExports.indexOf(pkg) !== -1) { pkgConf.rollup.exports = 'named' } 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 log = {} let version let markup = '' for (let v in config.changelog) { version = v 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' } } if (v !== 'Unreleased' && changed) { markup += '\n## ' + v markup += ' (' + formatDate(changes.date) + ')' markup += '\n' markup += changed } } markup += '\n\nThis is the **initial release**, and the start of this change log.\n' if (version === '2.0.0') markup += ` > Prior to version 2, FreeSewing was not a JavaScript project. > As such, that history is out of scope for this change log. ` 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', 'freesewing', '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', 'layout.css']) 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, version, author: 'freesewing', yarn: true, language: 'en' } for (let file of ['package.json', 'README.md', 'netlify.toml']) { 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', 'layout.css']) { 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 peerdeps = peerDependencies(pkg, config, 'pattern') let replace = { version, pattern: pkg, Pattern: capitalize(pkg), peerdeps: Object.keys(peerdeps) .map((dep) => dep + '@' + peerdeps[dep]) .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.')) }