/* 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 `
`;
}
/**
* 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)
};
for (let file of ["config.test.js"]) {
let template = handlebars.compile(
fs.readFileSync(path.join(source, file), "utf-8")
);
fs.writeFileSync(path.join(dest, file), template(replace));
}
// Add workflow file for Github actions
let template = handlebars.compile(
fs.readFileSync(path.join(
config.repoPath,
'config',
'templates',
'workflows',
'tests.pattern.yml'
), "utf-8")
);
fs.writeFileSync(path.join(
config.repoPath,
'.github',
'workflows',
`tests.${pkg}.yml`
), template(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."));
}