Compare commits

...

7 Commits

Author SHA1 Message Date
0d1fca8dbb Merge branch 'upcoming' 2023-05-11 08:55:25 -04:00
13f443b61c make yargs strict for commands 2023-05-11 08:54:58 -04:00
385c09896a add command upcoming 2023-05-10 17:29:37 -04:00
36e2aade92 Merge branch 'lord3-command-bump' 2023-05-10 16:47:49 -04:00
3f266fa068 add command cut 2023-05-10 16:46:53 -04:00
a326be7211 add more CR padding 2023-05-10 15:33:59 -04:00
8f8a16d025 add command add
fix #2
2023-05-10 15:33:59 -04:00
13 changed files with 355 additions and 123 deletions

View File

@ -6,11 +6,12 @@ project:
releases: releases:
- version: NEXT - version: NEXT
changes: changes:
- port the Perl changelord to JavaScript. - port the core of the Perl changelord to JavaScript.
change_types: change_types:
- title: '' - title: ""
level: minor level: minor
keywords: [''] keywords:
- ""
- title: Features - title: Features
level: minor level: minor
keywords: keywords:

View File

@ -0,0 +1,17 @@
---
to: src/command/<%= name %>.js
---
const handler = async (config) => {
// DO SOMETHING
};
export default {
command: '<%= name %>',
desc : 'TODO',
builder: (yargs) => {
yargs
},
handler,
}

View File

@ -0,0 +1,5 @@
---
message: |
hygen {bold generator new} --name [NAME] --action [ACTION]
hygen {bold generator with-prompt} --name [NAME] --action [ACTION]
---

View File

@ -0,0 +1,18 @@
---
to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs.t
---
---
to: app/hello.js
---
const hello = ```
Hello!
This is your first hygen template.
Learn what it can do here:
https://github.com/jondot/hygen
```
console.log(hello)

View File

@ -0,0 +1,18 @@
---
to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs.t
---
---
to: app/hello.js
---
const hello = ```
Hello!
This is your first prompt based hygen template.
Learn what it can do here:
https://github.com/jondot/hygen
```
console.log(hello)

View File

@ -0,0 +1,14 @@
---
to: _templates/<%= name %>/<%= action || 'new' %>/prompt.js
---
// see types of prompts:
// https://github.com/enquirer/enquirer/tree/master/examples
//
module.exports = [
{
type: 'input',
name: 'message',
message: "What's your message?"
}
]

View File

@ -0,0 +1,4 @@
---
setup: <%= name %>
force: true # this is because mostly, people init into existing folders is safe
---

View File

@ -16,10 +16,13 @@
"author": "Yanick Champoux <yanick@babyl.ca> (http://techblog.babyl.ca/)", "author": "Yanick Champoux <yanick@babyl.ca> (http://techblog.babyl.ca/)",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@yanick/updeep-remeda": "^2.2.0",
"consola": "^3.1.0", "consola": "^3.1.0",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"markdown-utils": "^1.0.0", "markdown-utils": "^1.0.0",
"remeda": "^1.14.0", "remeda": "^1.14.0",
"semver": "^7.5.0",
"simple-git": "^3.18.0",
"yaml": "^2.2.2", "yaml": "^2.2.2",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },

View File

@ -1,29 +1,34 @@
#!/usr/bin/env node #!/usr/bin/env node
import { hideBin } from 'yargs/helpers'; import { hideBin } from "yargs/helpers";
import yargs from 'yargs'; import yargs from "yargs";
import { join } from 'path'; import { join } from "path";
import yaml from 'yaml'; import yaml from "yaml";
import fs from 'fs-extra'; import fs from "fs-extra";
import consola from "consola";
import print from './command/print.js' import print from "./command/print.js";
import init from './command/init.js' import init from "./command/init.js";
import schema from './command/schema.js'; import schema from "./command/schema.js";
import consola from 'consola'; import add from "./command/add.js";
import cut from "./command/cut.js";
import upcoming from "./command/upcoming.js";
consola.raw = (...args) => console.log(...args); consola.raw = (...args) => console.log(...args);
yargs(hideBin(process.argv)) yargs(hideBin(process.argv))
.config({ .config({
consola, consola,
}) })
.default('source', join( process.cwd(), 'CHANGELOG.yml' )) .default("source", join(process.cwd(), "CHANGELOG.yml"))
.describe('source', 'changelog source') .describe("source", "changelog source")
.command({ .command(print)
...print, .command(init)
command: '$0', .command(add)
}) .command(schema)
.command(init) .command(cut)
.command(schema) .command(print)
.command(print).help().parse(); .command(upcoming)
.strictCommands()
.help()
.parse();

51
src/command/add.js Normal file
View File

@ -0,0 +1,51 @@
import fs from "fs-extra";
import yaml from "yaml";
import u from "@yanick/updeep-remeda";
const handler = async (config) => {
if (!config.change)
throw new Error("can't add a change without a description");
const entry = {
desc: config.change.join(" "),
};
if (config.ticket) entry.ticket = config.ticket;
if (config.type) entry.type = config.type;
config.consola.start(`adding '${entry.desc}' to the changelog`);
config.add_to_next(entry);
config.consola.success("done!");
};
export default {
command: "add [change...]",
desc: "add a change to the NEXT release",
builder: (yargs) => {
yargs
.string("ticket")
.string("type")
.middleware((argv) => {
argv.add_to_next = async (entry) => {
const changelog = yaml.parse(await fs.readFile(argv.source, "utf-8"));
let next = changelog.releases.find(u.matches({ version: "NEXT" }));
if (!next) {
next = { version: "NEXT", changes: [] };
changelog.releases.unshift(next);
}
if (Object.keys(entry).length === 1) {
entry = entry.desc;
}
next.changes.push(entry);
return fs.writeFile(argv.source, yaml.stringify(changelog));
};
});
},
handler,
};

68
src/command/cut.js Normal file
View File

@ -0,0 +1,68 @@
import yaml from "yaml";
import fs from "fs-extra";
import u from "@yanick/updeep-remeda";
import semverInc from "semver/functions/inc.js";
import { simpleGit } from "simple-git";
const code_churn = async (previous_version) => {
previous_version = previous_version
? "v" + previous_version
: "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; // empty tree sha1
return simpleGit().diff(["--shortstat", previous_version, "HEAD"]);
};
const handler = async (config) => {
config.consola.start("cutting the next version...");
const changelog = yaml.parse(await fs.readFile(config.source, "utf-8"));
const next = changelog.releases.find(u.matches({ version: "NEXT" }));
if (!next) throw new Error("no changes since last version, aborting");
const previous_version = changelog.releases.find(
({ version }) => version !== "NEXT"
);
next.date = new Date().toISOString().replace(/T.*/, "");
const type_to_level = (type = "") =>
changelog.change_types.find((t) => t.keywords.includes(type)).level;
const bump_level = (types) =>
types.includes("major")
? "major"
: types.includes("minor")
? "minor"
: "patch";
const bumper = bump_level(
next.changes.map(({ type }) => type_to_level(type))
);
if (changelog.project.with_stats) {
next.changes.push({
type: "stats",
desc: "code churn:" + (await code_churn(previous_version)),
});
}
next.version = semverInc(previous_version ?? "0.0.0", bumper);
if (config.dry) {
config.consola.info("running in dry mode, not saving\n", next);
} else {
await fs.writeFile(config.source, yaml.stringify(changelog));
}
config.consola.success(`version ${next.version} is cut!`);
};
export default {
command: "cut",
desc: "cut the next version",
builder: (yargs) => {
yargs.boolean("dry");
},
handler,
};

View File

@ -1,131 +1,135 @@
import * as R from 'remeda'; import * as R from "remeda";
import fs from 'fs-extra'; import fs from "fs-extra";
import yaml from 'yaml'; import yaml from "yaml";
import mkd from 'markdown-utils'; import mkd from "markdown-utils";
const render_header = (source ) => { const render_header = (source) => {
let header = '# Changelog'; let header = "# Changelog";
let name = source.project?.name let name = source.project?.name;
const links = {}; const links = {};
if( source?.project?.homepage) { if (source?.project?.homepage) {
links.homepage = source?.project?.homepage; links.homepage = source?.project?.homepage;
name = `[${name}][homepage]`; name = `[${name}][homepage]`;
} }
if(name) header += ` for ${name}`; if (name) header += ` for ${name}`;
return { header, links }
return { header, links };
}; };
function render_links(links) { function render_links(links) {
return Object.entries(links)
return Object.entries(links).map( .map(([name, url]) => ` [${name}]: ${url}`)
([name,url]) => ` [${name}]: ${url}` .join("\n");
).join( "\n")
} }
const render_change = (config,source)=>(change) => { const render_change = (config, source) => (change) => {
let body = change.desc; let body = change.desc;
let links = {}; let links = {};
return { return {
body, links body,
} links,
};
}; };
export const render_release = (config,source) => ( release ) => { export const render_release = (config, source) => (release) => {
let links = {}; let links = {};
if( typeof release === 'string') return { if (typeof release === "string")
body: release, return {
links body: release,
links,
}; };
if(release.version === 'NEXT' && !config.next) return { if (release.version === "NEXT" && !config.next)
body: '', return {
links, body: "",
links,
}; };
let body = "## " + release.version; let body = "## " + release.version;
if( release.date) { if (release.date) {
body += " " + release.date; body += " " + release.date;
}
body += "\n\n";
const type_map = Object.fromEntries(
source.change_types.flatMap((entry) =>
entry.keywords.map((k) => [k, entry])
)
);
const changes = release.changes.map((change) =>
typeof change === "string" ? { desc: change, type: "" } : change
);
const unknown = R.uniq(
changes
.map(R.prop("type"))
.map((t) => t ?? "")
.filter((type) => !type_map[type])
);
if (unknown.length) {
throw new Error(
"unknown change types encountered: " +
unknown.map((t) => `'${t}'`).join(", ")
);
}
for (const change_type of source.change_types) {
const type_changes = changes.filter(({ type }) =>
change_type.keywords.includes(type)
);
if (type_changes.length === 0) continue;
if (change_type.title) body += "\n" + mkd.h3(change_type.title) + "\n\n";
for (const c of type_changes) {
const res = render_change(config, source)(c);
body += mkd.li(res.body, 1) + "\n";
links = { ...links, ...res.links };
} }
}
body += "\n\n"; return { body, links };
const type_map = Object.fromEntries(source.change_types.flatMap(
entry => entry.keywords.map( k => [ k, entry ])
));
const changes = release.changes.map( change => typeof change === 'string'? { desc: change, type: '' } : change );
const unknown = R.uniq( changes.map(R.prop('type')).map(
t => t ?? ''
).filter( type => !type_map[type]));
if( unknown.length ) {
throw new Error(
"unknown change types encountered: " + unknown.map(t => `'${t}'`).join(', ')
);
}
for ( const change_type of source.change_types ) {
const type_changes = changes.filter(
({type}) => change_type.keywords.includes(type)
);
if( type_changes.length === 0) continue;
if( change_type.title )
body += mkd.h2( change_type.title) + "\n\n";
for( const c of type_changes ) {
const res = render_change(config,source)(c);
body += mkd.li(res.body,1)+"\n";
links = { ...links, ...res.links};
}
}
return { body, links };
}; };
const handler = async (config) => { const handler = async (config) => {
const source = await fs.readFile(config.source,'utf-8').then(yaml.parse); const source = await fs.readFile(config.source, "utf-8").then(yaml.parse);
let output = ''; let output = "";
let {header,links} = render_header(source); let { header, links } = render_header(source);
output += header + "\n\n"; output += header + "\n\n";
for ( const res of source.releases.map( render_release(config,source) )) { for (const res of source.releases.map(render_release(config, source))) {
output += res.body + "\n\n"; output += res.body + "\n\n";
links = {...links,...res.links}; links = { ...links, ...res.links };
} }
output += "\n\n\n\n" + render_links(links);
config.consola.raw(output);
output += "\n\n\n\n" + render_links(links);
config.consola.raw(output);
}; };
export default { export default {
command: 'print', command: "print",
desc : 'render the changelog', desc: "render the changelog",
builder: (yargs) => { builder: (yargs) => {
yargs yargs
//.boolean('json') //.boolean('json')
.boolean('next') .boolean("next")
// .default('json',false) // .default('json',false)
.default('next',true); .default("next", true);
}, },
handler, handler,
} };

24
src/command/upcoming.js Normal file
View File

@ -0,0 +1,24 @@
import u from "@yanick/updeep-remeda";
import fs from "fs-extra";
import yaml from "yaml";
import { render_release } from "./print.js";
const handler = async (config) => {
const source = await fs.readFile(config.source, "utf-8").then(yaml.parse);
const res = render_release(
{ ...config, next: true },
source
)(source.releases.find(u.matches({ version: "NEXT" })));
config.consola.raw("\n" + res.body);
};
export default {
command: "upcoming",
desc: "output the changes in NEXT",
builder: (yargs) => {
yargs;
},
handler,
};