diff --git a/.cspell.json b/.cspell.json index a059376b..886c1cb8 100644 --- a/.cspell.json +++ b/.cspell.json @@ -21,6 +21,9 @@ "**/CHANGELOG.md", "**/LICENSE.md", "**/RELEASE_NOTES.md", + "**/__fixtures__/commits/*.txt", + "**/__fixtures__/tags/*.txt", + "**/scratch.*", ".cspell.json", ".dictionary.txt", ".git/", diff --git a/.dictionary.txt b/.dictionary.txt index a176777a..4054a352 100644 --- a/.dictionary.txt +++ b/.dictionary.txt @@ -14,6 +14,7 @@ gpgsign hmarr iife keyid +ksort larsgw lcov lintstagedrc @@ -25,7 +26,6 @@ nvmrc pathe pkgs preid -shelljs shfmt tscu unstub diff --git a/.dprint.jsonc b/.dprint.jsonc index d69e855b..3aaaa6d3 100644 --- a/.dprint.jsonc +++ b/.dprint.jsonc @@ -9,10 +9,13 @@ "**/*config.*.timestamp*", "**/.temp/", "**/.vercel/", + "**/__fixtures__/commits/*.txt", + "**/__fixtures__/tags/*.txt", "**/__tests__/report.json", "**/coverage/", "**/dist/", "**/node_modules", + "**/scratch.*", "**/tsconfig*temp.json", ".git/", ".husky/_/", @@ -26,7 +29,7 @@ "commands": [ { "command": "node ./dprint/prettier.mjs {{file_path}}", - "exts": ["yaml", "yml"], + "exts": ["json5", "yaml", "yml"], "stdin": true }, { @@ -57,7 +60,7 @@ "incremental": true, "indentWidth": 2, "json": { - "associations": ["**/*.{json5,jsonc,json}"], + "associations": ["**/*.{jsonc,json}"], "array.preferSingleLine": false, "commentLine.forceSpaceAfterSlashes": true, "ignoreNodeCommentText": "dprint-ignore", diff --git a/.eslintignore b/.eslintignore index 704cba1d..7f0839ac 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,9 +6,9 @@ **/*.snap **/*config.*.timestamp* **/.DS_Store -**/.temp/ **/__tests__/report.json **/coverage/ +**/.temp/ **/dist/ **/node_modules/ **/tsconfig*temp.json @@ -25,9 +25,7 @@ yarn.lock !.cspell.json !.dprint.* !.github/ -!.graphqlrc.yml !.lintstagedrc.json !.markdownlint.jsonc -!.prettierrc.json !.vscode/ !.yarnrc.yml diff --git a/.eslintrc.base.cjs b/.eslintrc.base.cjs index c8e03a23..18c93304 100644 --- a/.eslintrc.base.cjs +++ b/.eslintrc.base.cjs @@ -690,14 +690,7 @@ const config = { } ], 'unicorn/import-index': 2, - 'unicorn/import-style': [ - 2, - { - styles: { - shelljs: { default: true } - } - } - ], + 'unicorn/import-style': [2, { styles: {} }], 'unicorn/new-for-builtins': 2, 'unicorn/no-abusive-eslint-disable': 2, 'unicorn/no-array-callback-reference': 0, diff --git a/.github/infrastructure.yml b/.github/infrastructure.yml index 7fbebccb..4d3e166f 100644 --- a/.github/infrastructure.yml +++ b/.github/infrastructure.yml @@ -33,7 +33,7 @@ branches: - context: test (16) - context: test (18) - context: test (19) - - context: test (20) + - context: test (20.6.1) - context: typescript (5.1.6) - context: typescript (5.2.2) - context: typescript (latest) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd513311..1b29c3f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -293,7 +293,7 @@ jobs: fail-fast: false matrix: node-version: - - 20 + - 20.6.1 - 19 - 18 - 16 @@ -322,6 +322,7 @@ jobs: with: cache: yarn cache-dependency-path: yarn.lock + check-latest: true node-version: ${{ matrix.node-version }} - id: cache if: steps.test-files-check.outputs.files_exists == 'true' diff --git a/.nvmrc b/.nvmrc index 1cc433a1..5538e1c3 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.6.0 +20.6.1 diff --git a/__fixtures__/commits/mkbuild.txt b/__fixtures__/commits/mkbuild.txt new file mode 100644 index 00000000..c9de76e5 --- /dev/null +++ b/__fixtures__/commits/mkbuild.txt @@ -0,0 +1,235 @@ +release: 1.0.0-alpha.23 (#361) +-author.email- +unicornware@flexdevelopment.llc +-author.name- +Lex +-body- +Signed-off-by: Lexus Drumgold +-date- +2023-09-09T23:59:31+00:00 +-hash- +a399eae +-sha- +a399eaeff03a88cdb1d59fc1fb42d88fcdc773fe +-tags- +tag: 1.0.0-alpha.23 +-trailers- +Signed-off-by: Lexus Drumgold +--$-- +chore: dprint migration +-author.email- +unicornware@flexdevelopment.llc +-author.name- +Lexus Drumgold +-body- +- prettier 3.0 formatting has degraded in quality for js-like files, but the team refuses to fix it +- prettier can be removed completely once dprint has its own yaml plugin + better json5 support +- prettier markdown formatting was always subpar; it never played nicely with markdownlint +- prettier/prettier#15358 +- prettier/prettier#5715 +- prettier/prettier#11881 +- dprint/dprint#736 +- dprint/dprint-plugin-typescript#432 + +Signed-off-by: Lexus Drumgold + +-date- +2023-09-09T19:00:25-04:00 +-hash- +7f578c9 +-sha- +7f578c90c69d12b283e49b9036c06029585630f8 +-tags- + +-trailers- +Signed-off-by: Lexus Drumgold + +--$-- +build(deps-dev): use @flex-development/nest-commander in lieu of nest-commander +-author.email- +unicornware@flexdevelopment.llc +-author.name- +Lexus Drumgold +-body- +- https://github.com/flex-development/nest-commander/releases/tag/1.0.0-alpha.1 + +Signed-off-by: Lexus Drumgold + +-date- +2023-09-09T06:16:46-04:00 +-hash- +6624182 +-sha- +6624182ccc862dd2a690225372fb825b8c4b6714 +-tags- + +-trailers- +Signed-off-by: Lexus Drumgold + +--$-- +fix(plugins): [`decorators`] decorator check +-author.email- +unicornware@flexdevelopment.llc +-author.name- +Lexus Drumgold +-body- +Signed-off-by: Lexus Drumgold + +-date- +2023-09-09T03:39:03-04:00 +-hash- +978ffe9 +-sha- +978ffe9ec9abfb25868f6ff547cfd45c87c4111a +-tags- + +-trailers- +Signed-off-by: Lexus Drumgold + +--$-- +build(deps): Bump the flex-development group with 3 updates (#330) +-author.email- +49699333+dependabot[bot]@users.noreply.github.com +-author.name- +dependabot[bot] +-body- +Signed-off-by: dependabot[bot] +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +-date- +2023-08-15T04:19:29+00:00 +-hash- +613a69a +-sha- +613a69aae2ce4087e0e6a4eebfdc3b535bcf88b8 +-tags- + +-trailers- +Signed-off-by: dependabot[bot] +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +--$-- +refactor(utils)!: [`analyzeOutputs`] intake `esbuild.Metafile['outputs']` +-author.email- +unicornware@flexdevelopment.llc +-author.name- +Lexus Drumgold +-body- +Signed-off-by: Lexus Drumgold + +-date- +2023-04-07T19:38:09-04:00 +-hash- +f467b4b +-sha- +f467b4bae340a83d126c8dc80b75af0a912d7ff7 +-tags- + +-trailers- +Signed-off-by: Lexus Drumgold + +--$-- +feat!: watch mode +-author.email- +unicornware@flexdevelopment.llc +-author.name- +Lexus Drumgold +-body- +- https://esbuild.github.io/api/#watch +- replaced `esbuilder` with `createContext` to support watch mode (and in the future, serve + live reload) +- started adding cli flags +- overhauled interfaces + +Signed-off-by: Lexus Drumgold + +-date- +2023-03-31T02:50:34-04:00 +-hash- +f1decde +-sha- +f1decde8597173569c69f8df8b013559231ff18b +-tags- + +-trailers- +Signed-off-by: Lexus Drumgold + +--$-- +fix(config): prevent `import.meta.url` from being rewritten +-author.email- +unicornware@flexdevelopment.llc +-author.name- +Lexus Drumgold +-body- +- import.meta.url may be used in build config (e.g. evanw/esbuild#1921) +- https://github.com/unjs/mlly/blob/v0.5.14/src/eval.ts#L17-L38 + +Signed-off-by: Lexus Drumgold + +-date- +2022-09-27T03:23:45-04:00 +-hash- +3e9c7c8 +-sha- +3e9c7c881cbc93156fe31176c7a0090e698dfac6 +-tags- + +-trailers- +Signed-off-by: Lexus Drumgold + +--$-- +feat: file-to-file transpilation (#4) +-author.email- +unicornware@flexdevelopment.llc +-author.name- +Lex +-body- +Signed-off-by: Lexus Drumgold +-date- +2022-09-15T21:22:09-04:00 +-hash- +1601d26 +-sha- +1601d268bce74abafab6a4b0d0e244b35dd1e808 +-tags- + +-trailers- +Signed-off-by: Lexus Drumgold +--$-- +ci: add @dependabot config +-author.email- +unicornware@flexdevelopment.llc +-author.name- +Lexus Drumgold +-body- +Signed-off-by: Lexus Drumgold + +-date- +2022-09-01T00:37:08-04:00 +-hash- +9c58d1c +-sha- +9c58d1ca753406bbba92c729360b9b6e400a6e9a +-tags- + +-trailers- +Signed-off-by: Lexus Drumgold + +--$-- +initial commit +-author.email- +unicornware@flexdevelopment.llc +-author.name- +Lexus Drumgold +-body- +Signed-off-by: Lexus Drumgold + +-date- +2022-09-01T00:34:48-04:00 +-hash- +50385b6 +-sha- +50385b68593d7b07f269631af0ca551539fe22d0 +-tags- + +-trailers- +Signed-off-by: Lexus Drumgold + +--$-- diff --git a/__fixtures__/tags/mkbuild.txt b/__fixtures__/tags/mkbuild.txt new file mode 100644 index 00000000..49a1a7d5 --- /dev/null +++ b/__fixtures__/tags/mkbuild.txt @@ -0,0 +1,422 @@ + + + + + + + + + + + + +tag: 1.0.0-alpha.22 + + + + + + + + + + + + + + +tag: 1.0.0-alpha.21 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +tag: 1.0.0-alpha.20 + + + + + + + + + + +tag: 1.0.0-alpha.19 + + +tag: 1.0.0-alpha.18 + +tag: 1.0.0-alpha.17 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +tag: 1.0.0-alpha.16 + + +tag: 1.0.0-alpha.15 + + + + + + + + + + + +tag: 1.0.0-alpha.14 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +tag: 1.0.0-alpha.13 + +tag: 1.0.0-alpha.12 + + + + + + + + + + + + + + + + + + + + +tag: 1.0.0-alpha.11 + +tag: 1.0.0-alpha.10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +tag: 1.0.0-alpha.9 + + +tag: 1.0.0-alpha.8 + +tag: 1.0.0-alpha.7 + + + + + + + + + + + + + + + + + + + + +tag: 1.0.0-alpha.6 + + + + +tag: 1.0.0-alpha.5 + + +tag: 1.0.0-alpha.4 + + + +tag: 1.0.0-alpha.3 + +tag: 1.0.0-alpha.2 + +tag: 1.0.0-alpha.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/__fixtures__/tags/tutils.txt b/__fixtures__/tags/tutils.txt new file mode 100644 index 00000000..95ee43b1 --- /dev/null +++ b/__fixtures__/tags/tutils.txt @@ -0,0 +1,930 @@ +tag: tutils@6.0.0-alpha.22 + + + + + + + + + + + + + + + + +tag: tutils@6.0.0-alpha.21 + + + + + +tag: tutils@6.0.0-alpha.20 + + + + + + + + + + + + + + + +tag: tutils@6.0.0-alpha.19 + + + + + + + + + + + + + + + +tag: tutils@6.0.0-alpha.18 + +tag: tutils@6.0.0-alpha.17 + + + + + + + +tag: tutils@6.0.0-alpha.16 + + + +tag: tutils@6.0.0-alpha.15 + + + + + + + + + + + + + + + + + + +tag: tutils@6.0.0-alpha.14 + + + + + + + + + + + + + +tag: tutils@6.0.0-alpha.13 + + + + + + + + + + + +tag: tutils@6.0.0-alpha.12 + + + + + + +tag: tutils@6.0.0-alpha.11 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +tag: tutils@6.0.0-alpha.10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +tag: tutils@6.0.0-alpha.9 + + + + + + +tag: tutils@6.0.0-alpha.8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +tag: tutils@6.0.0-alpha.7 + +tag: tutils@6.0.0-alpha.6 + + +tag: tutils@6.0.0-alpha.5 + + + + + +tag: tutils@6.0.0-alpha.4 + +tag: tutils@6.0.0-alpha.3 + +tag: tutils@6.0.0-alpha.2 + + + + +tag: tutils@6.0.0-alpha.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +tag: tutils@5.0.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +tag: tutils@5.0.0 + + + + +tag: tutils@5.0.0-alpha.1 + + + + +tag: tutils@5.0.0-dev.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +tag: tutils@5.0.0-dev.1 + + + + + + +tag: tutils@4.9.0-dev.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +tag: tutils@4.8.0 + +tag: tutils@4.7.0 + +tag: tutils@4.6.0 + + + + + +tag: tutils@4.5.0 + + + + +tag: tutils@4.4.0 + + + + + +tag: tutils@4.3.0 + +tag: tutils@4.2.3 + +tag: tutils@4.2.2 + + + +tag: tutils@4.2.1 + + + + + + + + +tag: tutils@4.2.0 + + + + + + +tag: tutils@4.1.1 + +tag: tutils@4.1.0 + + + + + + + + + + + + +tag: tutils@4.0.3 + +tag: tutils@4.0.2 + + + + + + + + + +tag: tutils@4.0.1 + + + + + + + +tag: tutils@4.0.0 + + + + + + + +tag: tutils@4.0.0-dev.0 + + + + + + + + + + +tag: tutils@3.1.7 + + +tag: tutils@3.1.6 + + +tag: tutils@3.1.5 + + + +tag: tutils@3.1.4 + + +tag: tutils@3.1.3 + + +tag: tutils@3.1.2 + + +tag: tutils@3.1.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +tag: tutils@3.1.0 + + + +tag: tutils@3.0.0 + + + + + + + +tag: tutils@2.0.0 + + + + + + +tag: tutils@1.0.0 + + + + diff --git a/__tests__/interfaces/index.ts b/__tests__/interfaces/index.ts index df90fc31..102a855a 100644 --- a/__tests__/interfaces/index.ts +++ b/__tests__/interfaces/index.ts @@ -1,5 +1,5 @@ /** - * @file Entry Point - Test Environment Interfaces + * @file Entry Point - Test Interfaces * @module tests/interfaces */ diff --git a/__tests__/interfaces/mock.ts b/__tests__/interfaces/mock.ts index 71a9cfea..de2dabb4 100644 --- a/__tests__/interfaces/mock.ts +++ b/__tests__/interfaces/mock.ts @@ -1,5 +1,5 @@ /** - * @file Test Environment Interfaces - Mock + * @file Test Interfaces - Mock * @module tests/interfaces/Mock */ diff --git a/__tests__/interfaces/spy.ts b/__tests__/interfaces/spy.ts index fc8e4f71..a5c652d5 100644 --- a/__tests__/interfaces/spy.ts +++ b/__tests__/interfaces/spy.ts @@ -1,5 +1,5 @@ /** - * @file Test Environment Interfaces - Spy + * @file Test Interfaces - Spy * @module tests/interfaces/Spy */ diff --git a/__tests__/setup/chai.ts b/__tests__/setup/chai.ts index 15db1610..65cebcba 100644 --- a/__tests__/setup/chai.ts +++ b/__tests__/setup/chai.ts @@ -6,12 +6,15 @@ import { JestExtend as extend } from '@vitest/expect' import chaiEach from 'chai-each' +import chaiString from 'chai-string' import { chai } from 'vitest' /** * initialize chai plugins. * * @see https://github.com/jamesthomasonjr/chai-each + * @see https://github.com/onechiporenko/chai-string */ extend(chai, chai.util) chai.use(chaiEach) +chai.use(chaiString) diff --git a/__tests__/setup/serializers/index.ts b/__tests__/setup/serializers/index.ts index 73e9e16e..13ccb88b 100644 --- a/__tests__/setup/serializers/index.ts +++ b/__tests__/setup/serializers/index.ts @@ -4,4 +4,4 @@ * @see https://vitest.dev/guide/snapshot.html#custom-serializer */ -export {} +import './reg-exp-array' diff --git a/__tests__/setup/serializers/reg-exp-array.ts b/__tests__/setup/serializers/reg-exp-array.ts new file mode 100644 index 00000000..bf576cf2 --- /dev/null +++ b/__tests__/setup/serializers/reg-exp-array.ts @@ -0,0 +1,42 @@ +/** + * @file Snapshot Serializers - RegExpArray + * @module tests/setup/serializers/reg-exp-array + */ + +import type { RegExpArray } from '#tests/types' +import { + get, + isArray, + isNumber, + isObjectPlain, + isString, + omit, + type Fn +} from '@flex-development/tutils' + +expect.addSnapshotSerializer({ + /** + * Get a `value` snapshot. + * + * @param {unknown} value - Value to print + * @param {Fn<[unknown], string>} printer - Print function + * @return {string} `value` as printable string + */ + print(value: unknown, printer: Fn<[unknown], string>): string { + return printer(omit(value, ['index', 'input'])) + }, + /** + * Check if `value` is a {@linkcode RegExpArray}. + * + * @param {unknown} value - Value to check + * @return {value is RegExpArray} `true` if `value` is a `RegExpArray` + */ + test(value: unknown): value is RegExpArray { + return ( + isArray(value) && + isObjectPlain(get(value, 'groups')) && + isNumber(get(value, 'index')) && + isString(get(value, 'input')) + ) + } +}) diff --git a/__tests__/types/index.ts b/__tests__/types/index.ts new file mode 100644 index 00000000..b77323fa --- /dev/null +++ b/__tests__/types/index.ts @@ -0,0 +1,6 @@ +/** + * @file Entry Point - Test Type Definitions + * @module tests/types + */ + +export type { default as RegExpArray } from './reg-exp-array' diff --git a/__tests__/types/reg-exp-array.ts b/__tests__/types/reg-exp-array.ts new file mode 100644 index 00000000..9d825433 --- /dev/null +++ b/__tests__/types/reg-exp-array.ts @@ -0,0 +1,11 @@ +/** + * @file Test Type Definitions - RegExpArray + * @module tests/types/RegExpArray + */ + +/** + * A {@linkcode RegExpExecArray} or {@linkcode RegExpMatchArray}. + */ +type RegExpArray = RegExpExecArray | RegExpMatchArray + +export type { RegExpArray as default } diff --git a/build.config.ts b/build.config.ts index ab01bd68..4aba058c 100644 --- a/build.config.ts +++ b/build.config.ts @@ -5,6 +5,7 @@ import { defineBuildConfig, type Config } from '@flex-development/mkbuild' import pathe from '@flex-development/pathe' +import { at } from '@flex-development/tutils' import pkg from './package.json' assert { type: 'json' } import tsconfig from './tsconfig.build.json' assert { type: 'json' } @@ -40,12 +41,10 @@ const config: Config = defineBuildConfig({ '@nestjs/platform-express', '@nestjs/websockets/socket-module', 'cache-manager', - 'node-fetch', - 'rxjs' + 'node-fetch' ], minify: true, name: 'cli', - platform: 'node', source: 'src/cli/index.ts', sourcemap: true, sourcesContent: false @@ -53,9 +52,10 @@ const config: Config = defineBuildConfig({ ], keepNames: true, minifySyntax: true, + platform: 'node', sourceRoot: 'file' + pathe.delimiter + pathe.sep.repeat(2), target: [ - pkg.engines.node.replace(/^\D+/, 'node'), + 'node' + at(/([\d.]+)/.exec(pkg.engines.node), 0, ''), tsconfig.compilerOptions.target ], tsconfig: 'tsconfig.build.json' diff --git a/loader.mjs b/loader.mjs index 91ade9f9..723f3460 100644 --- a/loader.mjs +++ b/loader.mjs @@ -126,7 +126,11 @@ export const load = async (url, context) => { source = code } - return { format: context.format, shortCircuit: true, source } + return { + format: context.format, + shortCircuit: true, + source: tutils.ifelse(context.format === mlly.Format.COMMONJS, null, source) + } } /** diff --git a/package.json b/package.json index 5eb5cf3c..99d5320f 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,6 @@ "@nestjs/common": "10.2.5", "@types/dateformat": "5.0.0", "@types/semver": "7.5.0", - "@types/shelljs": "0.8.12", "class-transformer": "0.5.1", "class-validator": "0.14.0", "consola": "3.2.3", @@ -97,8 +96,7 @@ "dateformat": "5.0.3", "node-emoji": "2.1.0", "reflect-metadata": "0.1.13", - "semver": "7.5.4", - "shelljs": "0.8.5" + "semver": "7.5.4" }, "devDependencies": { "@arethetypeswrong/cli": "0.10.1", @@ -115,6 +113,7 @@ "@nestjs/core": "10.2.5", "@nestjs/testing": "10.2.5", "@types/chai": "4.3.5", + "@types/chai-string": "1.4.3", "@types/eslint": "8.44.2", "@types/is-ci": "3.0.0", "@types/node": "20.5.7", @@ -127,6 +126,7 @@ "@vitest/expect": "0.34.3", "chai": "5.0.0-alpha.1", "chai-each": "0.0.1", + "chai-string": "1.5.0", "cross-env": "7.0.3", "cspell": "7.3.2", "dprint": "0.40.2", @@ -171,7 +171,7 @@ "chai": "5.0.0-alpha.1" }, "engines": { - "node": ">=16.20.0", + "node": ">=16.20.0 <20.6.0 || >20.6.0", "yarn": "4.0.0-rc.50" }, "packageManager": "yarn@4.0.0-rc.50", diff --git a/src/cli/__tests__/program.module.functional.spec.ts b/src/cli/__tests__/program.module.functional.spec.ts index 5c40327c..04466a43 100644 --- a/src/cli/__tests__/program.module.functional.spec.ts +++ b/src/cli/__tests__/program.module.functional.spec.ts @@ -23,7 +23,7 @@ describe('functional:cli/ProgramModule', () => { // Expect expect(process).to.have.property('exitCode', 1) expect(consola.error).toHaveBeenCalledOnce() - expect(consola.error).toHaveBeenCalledWith(e.message) + expect(consola.error).toHaveBeenCalledWith(e) }) }) @@ -54,7 +54,7 @@ describe('functional:cli/ProgramModule', () => { // Expect expect(process).to.have.property('exitCode', e.exitCode) expect(consola.error).toHaveBeenCalledOnce() - expect(consola.error).toHaveBeenCalledWith(e.message) + expect(consola.error).toHaveBeenCalledWith(e) }) }) }) diff --git a/src/cli/index.ts b/src/cli/index.ts index e6c02367..7d827f1a 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -6,9 +6,7 @@ */ import pkg from '#pkg' assert { type: 'json' } -import { - ProgramFactory -} from '@flex-development/nest-commander' +import { ProgramFactory } from '@flex-development/nest-commander' import ProgramModule from './program.module' await (await ProgramFactory.create(ProgramModule, { diff --git a/src/cli/program.module.ts b/src/cli/program.module.ts index 48050dfe..552fd06e 100644 --- a/src/cli/program.module.ts +++ b/src/cli/program.module.ts @@ -4,11 +4,11 @@ */ import pkg from '#pkg' assert { type: 'json' } -import { BumpService, PackageService } from '#src/providers' +import { BumpService, GitService, PackageService } from '#src/providers' import { Program } from '@flex-development/nest-commander' import type { CommanderError } from '@flex-development/nest-commander/commander' import { lowercase } from '@flex-development/tutils' -import { Module } from '@nestjs/common/decorators' +import { Module } from '@nestjs/common' import consola from 'consola' import { BumpCommand } from './commands' @@ -17,7 +17,7 @@ import { BumpCommand } from './commands' * * @class */ -@Module({ providers: [BumpCommand, BumpService, PackageService] }) +@Module({ providers: [BumpCommand, BumpService, GitService, PackageService] }) class ProgramModule { /** * Create a new CLI application module. @@ -39,7 +39,7 @@ class ProgramModule { * @return {void} Nothing when complete */ public static error(e: Error): void { - consola.error(e.message) + consola.error(e) return void (process.exitCode = 1) } diff --git a/src/decorators/__tests__/is-directory.constraint.spec.ts b/src/decorators/__tests__/is-directory.constraint.spec.ts new file mode 100644 index 00000000..2bead919 --- /dev/null +++ b/src/decorators/__tests__/is-directory.constraint.spec.ts @@ -0,0 +1,34 @@ +/** + * @file Unit Tests - IsDirectoryConstraint + * @module grease/decorators/tests/unit/IsDirectoryConstraint + */ + +import { toURL } from '@flex-development/mlly' +import { sep } from '@flex-development/pathe' +import { cast } from '@flex-development/tutils' +import TestSubject from '../is-directory.constraint' + +describe('unit:decorators/IsDirectoryConstraint', () => { + let subject: TestSubject + + beforeAll(() => { + subject = new TestSubject() + }) + + describe('#defaultMessage', () => { + it('should return default validation failure message', () => { + expect(subject.defaultMessage(cast({ value: toURL(sep) }))) + .to.equal('$property must be an absolute directory path') + }) + }) + + describe('#validate', () => { + it('should return false if value is not absolute directory path', () => { + expect(subject.validate(import.meta.url)).to.be.false + }) + + it('should return true if value is absolute directory path', () => { + expect(subject.validate(process.cwd())).to.be.true + }) + }) +}) diff --git a/src/decorators/__tests__/is-directory.functional.spec.ts b/src/decorators/__tests__/is-directory.functional.spec.ts new file mode 100644 index 00000000..d3406707 --- /dev/null +++ b/src/decorators/__tests__/is-directory.functional.spec.ts @@ -0,0 +1,46 @@ +/** + * @file Functional Tests - IsDirectory + * @module grease/decorators/tests/functional/IsDirectory + */ + +import pathe from '@flex-development/pathe' +import { validate } from 'class-validator' +import IsDirectoryConstraint from '../is-directory.constraint' +import TestSubject from '../is-directory.decorator' + +describe('functional:decorators/IsDirectory', () => { + let value: string + + beforeAll(() => { + value = '../../../' + }) + + describe('validation failure', () => { + it('should fail if property is not absolute directory path', async () => { + // Arrange + class Model { + @TestSubject() + public readonly directory: string = value + } + + // Act + Expect + expect((await validate(new Model()))[0]?.constraints).to.eql({ + [IsDirectoryConstraint.options.name]: + 'directory must be an absolute directory path' + }) + }) + }) + + describe('validation success', () => { + it('should pass if property is absolute directory path', async () => { + // Arrange + class Model { + @TestSubject() + public readonly directory: string = pathe.resolve(value) + } + + // Act + Expect + expect(await validate(new Model())).to.be.an('array').that.is.empty + }) + }) +}) diff --git a/src/decorators/index.ts b/src/decorators/index.ts index 63def176..2c34acab 100644 --- a/src/decorators/index.ts +++ b/src/decorators/index.ts @@ -3,6 +3,7 @@ * @module grease/decorators */ +export { default as IsDirectory } from './is-directory.decorator' export { default as IsManifestId } from './is-manifest-id.decorator' export { default as IsNilable } from './is-nilable.decorator' export { default as IsNullable } from './is-nullable.decorator' diff --git a/src/decorators/is-directory.constraint.ts b/src/decorators/is-directory.constraint.ts new file mode 100644 index 00000000..2619fc6e --- /dev/null +++ b/src/decorators/is-directory.constraint.ts @@ -0,0 +1,81 @@ +/** + * @file Decorators - IsDirectoryConstraint + * @module grease/decorators/IsDirectoryConstraint + */ + +import { isDirectory } from '@flex-development/mlly' +import { isAbsolute } from '@flex-development/pathe' +import { + cast, + get, + isString, + template, + type Optional +} from '@flex-development/tutils' +import { + ValidatorConstraint, + buildMessage, + type ValidatorConstraintInterface as IValidatorConstraint, + type ValidationArguments, + type ValidationOptions +} from 'class-validator' + +/** + * Directory path validator. + * + * @class + * @implements {IValidatorConstraint} + */ +@ValidatorConstraint(IsDirectoryConstraint.options) +class IsDirectoryConstraint implements IValidatorConstraint { + /** + * Validator constraint options. + * + * @public + * @static + * @readonly + * @member {{ async: boolean; name: string }} options + */ + public static readonly options: { async: boolean; name: string } = { + async: false, + name: isDirectory.name + } + + /** + * Get the default validation failure message. + * + * @see {@linkcode ValidationArguments} + * + * @public + * + * @param {ValidationArguments} args - Validation arguments + * @return {string} Default validation failure message + */ + public defaultMessage(args: ValidationArguments): string { + /** + * Default message template. + * + * @const {string} tmp + */ + const tmp: string = '{prefix}$property must be an absolute directory path' + + return buildMessage( + prefix => template(tmp, { prefix }), + cast>(get(args, 'constraints.0')) + )() + } + + /** + * Check if `value` is a directory path. + * + * @public + * + * @param {unknown} value - Value to check + * @return {value is string} `true` if value is directory path + */ + public validate(value: unknown): value is string { + return isString(value) && isAbsolute(value) && isDirectory(value) + } +} + +export default IsDirectoryConstraint diff --git a/src/decorators/is-directory.decorator.ts b/src/decorators/is-directory.decorator.ts new file mode 100644 index 00000000..f15f7247 --- /dev/null +++ b/src/decorators/is-directory.decorator.ts @@ -0,0 +1,27 @@ +/** + * @file Decorators - IsDirectory + * @module grease/decorators/IsDirectory + */ + +import type { PropertyDecorator } from '@flex-development/tutils' +import { ValidateBy, type ValidationOptions } from 'class-validator' +import validator from './is-directory.constraint' + +/** + * Check if a property value is a directory path. + * + * @decorator + * + * @param {ValidationOptions} [options] - Validation options + * @return {PropertyDecorator} Property decorator + */ +const IsDirectory = (options: ValidationOptions = {}): PropertyDecorator => { + return ValidateBy({ + async: validator.options.async, + constraints: [options], + name: validator.options.name, + validator + }, options) +} + +export default IsDirectory diff --git a/src/index.ts b/src/index.ts index 1410e81e..7bd21b77 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export * from './decorators' export * from './enums' +export type * from './interfaces' export * from './models' export * from './options' export * from './providers' diff --git a/src/interfaces/__tests__/commit.interface.spec-d.ts b/src/interfaces/__tests__/commit.interface.spec-d.ts new file mode 100644 index 00000000..a573e133 --- /dev/null +++ b/src/interfaces/__tests__/commit.interface.spec-d.ts @@ -0,0 +1,96 @@ +/** + * @file Type Tests - Commit + * @module grease/interfaces/tests/unit-d/Commit + */ + +import type { Author, BreakingChange, Trailer } from '#src/types' +import type { JsonObject, Nullable } from '@flex-development/tutils' +import type TestSubject from '../commit.interface' + +describe('unit-d:interfaces/Commit', () => { + it('should extend JsonObject', () => { + expectTypeOf().toMatchTypeOf() + }) + + it('should match [author: Author]', () => { + expectTypeOf().toHaveProperty('author').toEqualTypeOf() + }) + + it('should match [body: Nullable]', () => { + expectTypeOf() + .toHaveProperty('body') + .toEqualTypeOf>() + }) + + it('should match [breaking: boolean]', () => { + expectTypeOf() + .toHaveProperty('breaking') + .toEqualTypeOf() + }) + + it('should match [breaking_changes: BreakingChange[]]', () => { + expectTypeOf() + .toHaveProperty('breaking_changes') + .toEqualTypeOf() + }) + + it('should match [date: string]', () => { + expectTypeOf().toHaveProperty('date').toEqualTypeOf() + }) + + it('should match [hash: string]', () => { + expectTypeOf().toHaveProperty('hash').toEqualTypeOf() + }) + + it('should match [header: string]', () => { + expectTypeOf().toHaveProperty('header').toEqualTypeOf() + }) + + it('should match [mentions: string[]]', () => { + expectTypeOf() + .toHaveProperty('mentions') + .toEqualTypeOf() + }) + + it('should match [pr: Nullable]', () => { + expectTypeOf() + .toHaveProperty('pr') + .toEqualTypeOf>() + }) + + it('should match [scope: Nullable]', () => { + expectTypeOf() + .toHaveProperty('scope') + .toEqualTypeOf>() + }) + + it('should match [sha: string]', () => { + expectTypeOf().toHaveProperty('sha').toEqualTypeOf() + }) + + it('should match [subject: string]', () => { + expectTypeOf() + .toHaveProperty('subject') + .toEqualTypeOf() + }) + + it('should match [tags: string[]]', () => { + expectTypeOf().toHaveProperty('tags').toEqualTypeOf() + }) + + it('should match [trailers: Trailer[]]', () => { + expectTypeOf() + .toHaveProperty('trailers') + .toEqualTypeOf() + }) + + it('should match [type: string]', () => { + expectTypeOf().toHaveProperty('type').toEqualTypeOf() + }) + + it('should match [version: Nullable]', () => { + expectTypeOf() + .toHaveProperty('version') + .toEqualTypeOf>() + }) +}) diff --git a/src/interfaces/commit.interface.ts b/src/interfaces/commit.interface.ts new file mode 100644 index 00000000..4f9c8294 --- /dev/null +++ b/src/interfaces/commit.interface.ts @@ -0,0 +1,108 @@ +/** + * @file Interfaces - Commit + * @module grease/interfaces/Commit + */ + +import type { Author, BreakingChange, Trailer } from '#src/types' +import type { JsonObject, Nullable } from '@flex-development/tutils' + +/** + * A parsed commit. + * + * @see https://conventionalcommits.org + * + * @extends {JsonObject} + */ +interface Commit extends JsonObject { + /** + * Commit author details. + * + * @see {@linkcode Author} + */ + author: Author + + /** + * Commit body text. + */ + body: Nullable + + /** + * Boolean indicating if commit contains breaking changes. + */ + breaking: boolean + + /** + * Breaking changes noted in {@linkcode subject} and {@linkcode trailers}. + * + * @see {@linkcode BreakingChange} + */ + breaking_changes: BreakingChange[] + + /** + * Commit date in strict ISO 8601 format (`%cI`). + * + * @see https://git-scm.com/docs/pretty-formats/2.21.0 + */ + date: string + + /** + * Abbreviated commit SHA. + */ + hash: string + + /** + * Commit {@linkcode type}, {@linkcode scope}, breaking change indicator, and + * {@linkcode subject}. + */ + header: string + + /** + * Users mentioned in commit message. + */ + mentions: string[] + + /** + * Pull request number if commit {@linkcode subject} includes a pull request + * reference. + */ + pr: Nullable + + /** + * Commit scope. + */ + scope: Nullable + + /** + * Commit SHA. + */ + sha: string + + /** + * Commit subject. + */ + subject: string + + /** + * Tags associated with commit. + */ + tags: string[] + + /** + * Commit message trailers. + * + * @see {@linkcode Trailer} + */ + trailers: Trailer[] + + /** + * Commit type. + */ + type: string + + /** + * Tagged commit version. + */ + version: Nullable +} + +export type { Commit as default } diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts new file mode 100644 index 00000000..eba03450 --- /dev/null +++ b/src/interfaces/index.ts @@ -0,0 +1,6 @@ +/** + * @file Entry Point - Interfaces + * @module grease/interfaces + */ + +export type { default as Commit } from './commit.interface' diff --git a/src/models/__snapshots__/grammar-commit.snap b/src/models/__snapshots__/grammar-commit.snap new file mode 100644 index 00000000..94bbd0ef --- /dev/null +++ b/src/models/__snapshots__/grammar-commit.snap @@ -0,0 +1,392 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`unit:models/CommitGrammar > #field > should match raw commit fields 1`] = ` +[ + { + "0": "-author.email-", + "1": "author.email", + "groups": { + "field": "author.email", + }, + }, + { + "0": "-author.name-", + "1": "author.name", + "groups": { + "field": "author.name", + }, + }, + { + "0": "-body-", + "1": "body", + "groups": { + "field": "body", + }, + }, + { + "0": "-date-", + "1": "date", + "groups": { + "field": "date", + }, + }, + { + "0": "-hash-", + "1": "hash", + "groups": { + "field": "hash", + }, + }, + { + "0": "-sha-", + "1": "sha", + "groups": { + "field": "sha", + }, + }, + { + "0": "-tags-", + "1": "tags", + "groups": { + "field": "tags", + }, + }, + { + "0": "-trailers-", + "1": "trailers", + "groups": { + "field": "trailers", + }, + }, +] +`; + +exports[`unit:models/CommitGrammar > #header > should match raw commit header with [type,breaking,subject,pr] 1`] = ` +{ + "0": "refactor!: statements (#3)", + "1": "refactor", + "2": undefined, + "3": "!", + "4": "statements (#3)", + "5": "3", + "groups": { + "breaking": "!", + "pr": "3", + "scope": undefined, + "subject": "statements (#3)", + "type": "refactor", + }, +} +`; + +exports[`unit:models/CommitGrammar > #header > should match raw commit header with [type,breaking,subject] 1`] = ` +{ + "0": "feat!: watch mode", + "1": "feat", + "2": undefined, + "3": "!", + "4": "watch mode", + "5": undefined, + "groups": { + "breaking": "!", + "pr": undefined, + "scope": undefined, + "subject": "watch mode", + "type": "feat", + }, +} +`; + +exports[`unit:models/CommitGrammar > #header > should match raw commit header with [type,scope,breaking,subject,pr] 1`] = ` +{ + "0": "refactor(utils)!: [getFormat] enforce absolute module id (#3)", + "1": "refactor", + "2": "utils", + "3": "!", + "4": "[getFormat] enforce absolute module id (#3)", + "5": "3", + "groups": { + "breaking": "!", + "pr": "3", + "scope": "utils", + "subject": "[getFormat] enforce absolute module id (#3)", + "type": "refactor", + }, +} +`; + +exports[`unit:models/CommitGrammar > #header > should match raw commit header with [type,scope,breaking,subject] 1`] = ` +{ + "0": "refactor(utils)!: [\`analyzeOutputs\`] intake \`esbuild.Metafile.outputs\`", + "1": "refactor", + "2": "utils", + "3": "!", + "4": "[\`analyzeOutputs\`] intake \`esbuild.Metafile.outputs\`", + "5": undefined, + "groups": { + "breaking": "!", + "pr": undefined, + "scope": "utils", + "subject": "[\`analyzeOutputs\`] intake \`esbuild.Metafile.outputs\`", + "type": "refactor", + }, +} +`; + +exports[`unit:models/CommitGrammar > #header > should match raw commit header with [type,scope,subject,pr] 1`] = ` +{ + "0": "build(deps): Bump the flex-development group with 3 updates (#330)", + "1": "build", + "2": "deps", + "3": undefined, + "4": "Bump the flex-development group with 3 updates (#330)", + "5": "330", + "groups": { + "breaking": undefined, + "pr": "330", + "scope": "deps", + "subject": "Bump the flex-development group with 3 updates (#330)", + "type": "build", + }, +} +`; + +exports[`unit:models/CommitGrammar > #header > should match raw commit header with [type,scope,subject] 1`] = ` +{ + "0": "fix(config): prevent \`import.meta.url\` from being rewritten", + "1": "fix", + "2": "config", + "3": undefined, + "4": "prevent \`import.meta.url\` from being rewritten", + "5": undefined, + "groups": { + "breaking": undefined, + "pr": undefined, + "scope": "config", + "subject": "prevent \`import.meta.url\` from being rewritten", + "type": "fix", + }, +} +`; + +exports[`unit:models/CommitGrammar > #header > should match raw commit header with [type,subject,pr] 1`] = ` +{ + "0": "feat: file-to-file transpilation (#4)", + "1": "feat", + "2": undefined, + "3": undefined, + "4": "file-to-file transpilation (#4)", + "5": "4", + "groups": { + "breaking": undefined, + "pr": "4", + "scope": undefined, + "subject": "file-to-file transpilation (#4)", + "type": "feat", + }, +} +`; + +exports[`unit:models/CommitGrammar > #header > should match raw commit header with [type,subject] 1`] = ` +{ + "0": "ci: add @dependabot config", + "1": "ci", + "2": undefined, + "3": undefined, + "4": "add @dependabot config", + "5": undefined, + "groups": { + "breaking": undefined, + "pr": undefined, + "scope": undefined, + "subject": "add @dependabot config", + "type": "ci", + }, +} +`; + +exports[`unit:models/CommitGrammar > #mention > should match mentions in raw commit 1`] = ` +[ + { + "0": "@dependabot", + "1": "@dependabot", + "2": "dependabot", + "groups": { + "mention": "@dependabot", + "user": "dependabot", + }, + }, +] +`; + +exports[`unit:models/CommitGrammar > #reference > should match references in raw commit 1`] = ` +[ + { + "0": "closes GH-335", + "1": "closes", + "2": undefined, + "3": undefined, + "4": undefined, + "5": "GH-335", + "6": "GH-", + "7": "335", + "groups": { + "action": "closes", + "number": "335", + "owner": undefined, + "prefix": "GH-", + "ref": "GH-335", + "repo": undefined, + "repository": undefined, + }, + }, + { + "0": "prettier/prettier#15358", + "1": undefined, + "2": "prettier/prettier", + "3": "prettier", + "4": "prettier", + "5": "#15358", + "6": "#", + "7": "15358", + "groups": { + "action": undefined, + "number": "15358", + "owner": "prettier", + "prefix": "#", + "ref": "#15358", + "repo": "prettier", + "repository": "prettier/prettier", + }, + }, + { + "0": "prettier/prettier#5715", + "1": undefined, + "2": "prettier/prettier", + "3": "prettier", + "4": "prettier", + "5": "#5715", + "6": "#", + "7": "5715", + "groups": { + "action": undefined, + "number": "5715", + "owner": "prettier", + "prefix": "#", + "ref": "#5715", + "repo": "prettier", + "repository": "prettier/prettier", + }, + }, + { + "0": "prettier/prettier#11881", + "1": undefined, + "2": "prettier/prettier", + "3": "prettier", + "4": "prettier", + "5": "#11881", + "6": "#", + "7": "11881", + "groups": { + "action": undefined, + "number": "11881", + "owner": "prettier", + "prefix": "#", + "ref": "#11881", + "repo": "prettier", + "repository": "prettier/prettier", + }, + }, + { + "0": "dprint/dprint#736", + "1": undefined, + "2": "dprint/dprint", + "3": "dprint", + "4": "dprint", + "5": "#736", + "6": "#", + "7": "736", + "groups": { + "action": undefined, + "number": "736", + "owner": "dprint", + "prefix": "#", + "ref": "#736", + "repo": "dprint", + "repository": "dprint/dprint", + }, + }, + { + "0": "dprint/dprint-plugin-typescript#432", + "1": undefined, + "2": "dprint/dprint-plugin-typescript", + "3": "dprint", + "4": "dprint-plugin-typescript", + "5": "#432", + "6": "#", + "7": "432", + "groups": { + "action": undefined, + "number": "432", + "owner": "dprint", + "prefix": "#", + "ref": "#432", + "repo": "dprint-plugin-typescript", + "repository": "dprint/dprint-plugin-typescript", + }, + }, +] +`; + +exports[`unit:models/CommitGrammar > #trailer > should match trailers in raw commit 1`] = ` +[ + { + "0": "token: This is a very long value, with spaces and + newlines in it.", + "1": "token", + "2": "This is a very long value, with spaces and + newlines in it.", + "groups": { + "token": "token", + "value": "This is a very long value, with spaces and + newlines in it.", + }, + }, + { + "0": "BREAKING CHANGE: \`extends\` key now used for extending other configs", + "1": "BREAKING CHANGE", + "2": "\`extends\` key now used for extending other configs", + "groups": { + "token": "BREAKING CHANGE", + "value": "\`extends\` key now used for extending other configs", + }, + }, + { + "0": "BREAKING-CHANGE: use JavaScript features not available in Node 6.", + "1": "BREAKING-CHANGE", + "2": "use JavaScript features not available in Node 6.", + "groups": { + "token": "BREAKING-CHANGE", + "value": "use JavaScript features not available in Node 6.", + }, + }, + { + "0": "Reviewed-by: Z", + "1": "Reviewed-by", + "2": "Z", + "groups": { + "token": "Reviewed-by", + "value": "Z", + }, + }, + { + "0": "Refs: #123", + "1": "Refs", + "2": "#123", + "groups": { + "token": "Refs", + "value": "#123", + }, + }, +] +`; diff --git a/src/models/__tests__/grammar-commit.spec.ts b/src/models/__tests__/grammar-commit.spec.ts new file mode 100644 index 00000000..2ec1aa45 --- /dev/null +++ b/src/models/__tests__/grammar-commit.spec.ts @@ -0,0 +1,218 @@ +/** + * @file Unit Tests - CommitGrammar + * @module grease/models/tests/unit/CommitGrammar + */ + +import { dedent } from 'ts-dedent' +import TestSubject from '../grammar-commit.model' + +describe('unit:models/CommitGrammar', () => { + let subject: TestSubject + + beforeAll(() => { + subject = new TestSubject() + }) + + describe('#field', () => { + let fields: string + let flags: string + let source: string + + beforeAll(() => { + fields = dedent` + -author.email- + unicornware@flexdevelopment.llc + -author.name- + Lex + -body- + Signed-off-by: Lexus Drumgold + -date- + 2023-09-09T23:59:31+00:00 + -hash- + a399eae + -sha- + a399eaeff03a88cdb1d59fc1fb42d88fcdc773fe + -tags- + tag: 1.0.0-alpha.23 + -trailers- + Signed-off-by: Lexus Drumgold + ` + + flags = subject.field.flags + source = subject.field.source + }) + + it('should match raw commit fields', () => { + // Act + const result = [...fields.matchAll(new RegExp(source, flags + 'g'))] + + // Expect + expect(result).to.not.be.null + expect(result).toMatchSnapshot() + }) + }) + + describe('#header', () => { + it.each<[components: string, header: string]>([ + [ + 'breaking,subject,pr', + 'refactor!: statements (#3)' + ], + [ + 'breaking,subject', + 'feat!: watch mode' + ], + [ + 'scope,breaking,subject,pr', + 'refactor(utils)!: [getFormat] enforce absolute module id (#3)' + ], + [ + 'scope,breaking,subject', + 'refactor(utils)!: [`analyzeOutputs`] intake `esbuild.Metafile.outputs`' + ], + [ + 'scope,subject,pr', + 'build(deps): Bump the flex-development group with 3 updates (#330)' + ], + [ + 'scope,subject', + 'fix(config): prevent `import.meta.url` from being rewritten' + ], + [ + 'subject,pr', + 'feat: file-to-file transpilation (#4)' + ], + [ + 'subject', + 'ci: add @dependabot config' + ] + ])('should match raw commit header with [type,%s]', (_, header: string) => { + // Act + const result = subject.header.exec(header) + + // Expect + expect(result).to.not.be.null + expect(result).toMatchSnapshot() + }) + + it('should not match raw commit header without colon after type', () => { + expect(subject.header.exec('release 1.0.0')).to.be.null + }) + + it('should not match raw commit header without subject', () => { + expect(subject.header.exec('release:')).to.be.null + }) + + it('should not match raw commit header without type', () => { + expect(subject.header.exec('initial commit')).to.be.null + }) + }) + + describe('#mention', () => { + let raw: string + + beforeAll(() => { + raw = dedent` + ci: add @dependabot config + -author.email- + unicornware@flexdevelopment.llc + -author.name- + Lexus Drumgold + -body- + Signed-off-by: Lexus Drumgold + + -date- + 2022-09-01T00:37:08-04:00 + -hash- + 9c58d1c + -sha- + 9c58d1ca753406bbba92c729360b9b6e400a6e9a + -tags- + + -trailers- + Signed-off-by: Lexus Drumgold + + ` + }) + + it('should match mentions in raw commit', () => { + // Act + const result = [...raw.matchAll(subject.mention)] + + // Expect + expect(result).to.not.be.null + expect(result).toMatchSnapshot() + }) + }) + + describe('#reference', () => { + let raw: string + + beforeAll(() => { + raw = dedent` + chore: dprint migration + -author.email- + unicornware@flexdevelopment.llc + -author.name- + Lexus Drumgold + -body- + - prettier 3.0 formatting has degraded in quality for js-like files, but the team refuses to fix it + - prettier can be removed completely once dprint has its own yaml plugin + better json5 support + - prettier markdown formatting was always subpar; it never played nicely with markdownlint + - closes GH-335 + - prettier/prettier#15358 + - prettier/prettier#5715 + - prettier/prettier#11881 + - dprint/dprint#736 + - dprint/dprint-plugin-typescript#432 + + Signed-off-by: Lexus Drumgold + + -date- + 2023-09-09T19:00:25-04:00 + -hash- + 7f578c9 + -sha- + 7f578c90c69d12b283e49b9036c06029585630f8 + -tags- + + -trailers- + Signed-off-by: Lexus Drumgold + + ` + }) + + it('should match references in raw commit', () => { + // Act + const result = [...raw.matchAll(subject.reference(['#', 'gh-']))] + + // Expect + expect(result).to.not.be.null + expect(result).toMatchSnapshot() + }) + }) + + describe('#trailer', () => { + let trailers: string + + beforeAll(() => { + trailers = dedent` + token: This is a very long value, with spaces and + newlines in it. + BREAKING CHANGE: \`extends\` key now used for extending other configs + BREAKING-CHANGE: use JavaScript features not available in Node 6. + Reviewed-by: Z + Refs: #123 + ` + }) + + it('should match trailers in raw commit', () => { + // Act + const result = [...trailers.matchAll(subject.trailer)] + + // Expect + expect(result).to.not.be.null + expect(result).toMatchSnapshot() + }) + }) +}) diff --git a/src/models/grammar-commit.model.ts b/src/models/grammar-commit.model.ts new file mode 100644 index 00000000..bf2e4f3c --- /dev/null +++ b/src/models/grammar-commit.model.ts @@ -0,0 +1,90 @@ +/** + * @file Models - CommitGrammar + * @module grease/models/CommitGrammar + */ + +import { join, template } from '@flex-development/tutils' + +/** + * Commit parser grammar. + * + * @class + */ +class CommitGrammar { + /** + * Regular expression matching fields in a raw commit. + * + * @public + * + * @return {RegExp} Commit log field regex + */ + public get field(): RegExp { + return /^-(?.*?)-(?=\n*)$/m + } + + /** + * Regular expression matching a commit header. + * + * @see https://regex101.com/r/WAJeLp + * @see https://conventionalcommits.org + * + * @public + * + * @return {RegExp} Commit header regex + */ + public get header(): RegExp { + return /^(?[a-z]+)(?:\((?[a-z-]+)\))?(?!)?: +(?(?:.+ \(#(?\d+)\))|.+)/i + } + + /** + * Regular expression matching mentions (e.g. `@unicornware`) in a raw commit. + * + * @see https://regex101.com/r/upbRpj + * + * @public + * + * @return {RegExp} Mention regex + */ + public get mention(): RegExp { + return /\B(?@(?[\w-]{1,38}(?=[^\w-]\B|\s)))/g + } + + /** + * Regular expression matching trailers in a raw commit. + * + * @see https://git-scm.com/docs/git-interpret-trailers + * @see https://regex101.com/r/I56Kgg + * + * @return {RegExp} Git trailer regex + */ + public get trailer(): RegExp { + return /(?<=^|\n)(?[\w -]+?(?=:)): (?[\S\s]+?(?:(?=\n[\w -]+?(?=:))|(?=\n*$)))/g + } + + /** + * Get a regular expression matching issue references in a raw commit. + * + * @see https://regex101.com/r/Thsp1M + * + * @public + * + * @param {ReadonlyArray} [prefixes=['#']] - Issue prefixes + * @return {RegExp} Issue reference regex + */ + public reference(prefixes: readonly string[] = ['#']): RegExp { + /** + * Pattern matching issue references in commit bodies. + * + * @const {string} pattern + */ + const pattern: string = + '(?:(?:(?(?:close|resolve)[ds]?|fix(?:e[ds])?|releases) +)|(?(?[\\da-z](?:-(?=[\\da-z])|[\\da-z]){0,38}(?<=[\\da-z]))\\/(?\\S+)))?(?(?{prefixes})(?\\d+))' + + return new RegExp( + template(pattern, { prefixes: join(prefixes, '|') }), + 'gi' + ) + } +} + +export default CommitGrammar diff --git a/src/models/index.ts b/src/models/index.ts index 2e740e8b..2ea8d5eb 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -3,4 +3,5 @@ * @module grease/models */ +export { default as CommitGrammar } from './grammar-commit.model' export { default as Version } from './version.model' diff --git a/src/options/__snapshots__/commit.options.integration.snap b/src/options/__snapshots__/commit.options.integration.snap new file mode 100644 index 00000000..29c84dd7 --- /dev/null +++ b/src/options/__snapshots__/commit.options.integration.snap @@ -0,0 +1,46 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`integration:options/CommitOptions > validation > should fail validation if schema is invalid 1`] = ` +[ + ValidationError { + "children": [], + "constraints": { + "isString": "from must be a string", + }, + "property": "from", + "value": null, + }, + ValidationError { + "children": [], + "constraints": { + "isArray": "issue_prefixes must be an array", + }, + "property": "issue_prefixes", + "value": "#", + }, + ValidationError { + "children": [], + "constraints": { + "isString": "to must be a string", + }, + "property": "to", + "value": /\\^HEAD/, + }, + ValidationError { + "children": [], + "constraints": { + "isDirectory": "cwd must be an absolute directory path", + }, + "property": "cwd", + "value": "file:///", + }, + ValidationError { + "children": [], + "constraints": { + "isBoolean": "debug must be a boolean value", + }, + "property": "debug", + "value": null, + }, +] +`; diff --git a/src/options/__snapshots__/git-tag.options.integration.snap b/src/options/__snapshots__/git-tag.options.integration.snap new file mode 100644 index 00000000..a12db5fb --- /dev/null +++ b/src/options/__snapshots__/git-tag.options.integration.snap @@ -0,0 +1,38 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`integration:options/GitTagOptions > validation > should fail validation if schema is invalid 1`] = ` +[ + ValidationError { + "children": [], + "constraints": { + "isString": "tagprefix must be a string", + }, + "property": "tagprefix", + "value": /tutils@/, + }, + ValidationError { + "children": [], + "constraints": { + "isBoolean": "unstable must be a boolean value", + }, + "property": "unstable", + "value": 1, + }, + ValidationError { + "children": [], + "constraints": { + "isDirectory": "cwd must be an absolute directory path", + }, + "property": "cwd", + "value": "file:///", + }, + ValidationError { + "children": [], + "constraints": { + "isBoolean": "debug must be a boolean value", + }, + "property": "debug", + "value": null, + }, +] +`; diff --git a/src/options/__snapshots__/git.options.integration.snap b/src/options/__snapshots__/git.options.integration.snap new file mode 100644 index 00000000..c9593156 --- /dev/null +++ b/src/options/__snapshots__/git.options.integration.snap @@ -0,0 +1,22 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`integration:options/GitOptions > validation > should fail validation if schema is invalid 1`] = ` +[ + ValidationError { + "children": [], + "constraints": { + "isDirectory": "cwd must be an absolute directory path", + }, + "property": "cwd", + "value": "file:///", + }, + ValidationError { + "children": [], + "constraints": { + "isBoolean": "debug must be a boolean value", + }, + "property": "debug", + "value": null, + }, +] +`; diff --git a/src/options/__tests__/bump.options.spec.ts b/src/options/__tests__/bump.options.spec.ts index 226203c1..3a81fdfb 100644 --- a/src/options/__tests__/bump.options.spec.ts +++ b/src/options/__tests__/bump.options.spec.ts @@ -3,6 +3,7 @@ * @module grease/options/tests/unit/BumpOptions */ +import { ReleaseType } from '#src/enums' import { DOT } from '@flex-development/tutils' import { pathToFileURL } from 'node:url' import TestSubject from '../bump.options' @@ -10,7 +11,7 @@ import TestSubject from '../bump.options' describe('unit:options/BumpOptions', () => { describe('constructor', () => { let manifest: TestSubject['manifest'] - let preid: Required['preid'] + let preid: TestSubject['preid'] let prestart: TestSubject['prestart'] let release: TestSubject['release'] let silent: TestSubject['silent'] @@ -18,20 +19,13 @@ describe('unit:options/BumpOptions', () => { let write: TestSubject['write'] beforeAll(() => { - manifest = pathToFileURL(DOT).href - preid = 'rc' - prestart = 0 - release = 'premajor' - silent = true - write = true - subject = new TestSubject({ - manifest, - preid, - prestart, - release, - silent, - write + manifest: (manifest = pathToFileURL(DOT).href), + preid: (preid = 'rc'), + prestart: (prestart = 0), + release: (release = ReleaseType.PREMAJOR), + silent: (silent = true), + write: (write = true) }) }) diff --git a/src/options/__tests__/commit.options.integration.spec.ts b/src/options/__tests__/commit.options.integration.spec.ts new file mode 100644 index 00000000..32f71bfb --- /dev/null +++ b/src/options/__tests__/commit.options.integration.spec.ts @@ -0,0 +1,49 @@ +/** + * @file Integration Tests - CommitOptions + * @module grease/options/tests/integration/CommitOptions + */ + +import { toURL } from '@flex-development/mlly' +import { sep } from '@flex-development/pathe' +import { DOT, cast } from '@flex-development/tutils' +import { validate } from 'class-validator' +import TestSubject from '../commit.options' + +describe('integration:options/CommitOptions', () => { + describe('validation', () => { + it('should fail validation if schema is invalid', async () => { + // Arrange + const subject: TestSubject = new TestSubject(cast({ + cwd: toURL(sep), + debug: null, + from: null, + issue_prefixes: '#', + to: /^HEAD/ + })) + + // Act + const errors = await validate(subject, { + skipMissingProperties: false, + stopAtFirstError: false, + validationError: { target: false, value: true } + }) + + // Expect + expect(errors).toMatchSnapshot() + }) + + it('should pass validation if schema is valid', async () => { + // Arrange + const subject: TestSubject = new TestSubject({ + cwd: DOT, + debug: true, + from: 'grease@1.0.0', + issue_prefixes: ['#'], + to: 'grease@2.0.0' + }) + + // Act + Expect + expect(await validate(subject)).to.be.an('array').that.is.empty + }) + }) +}) diff --git a/src/options/__tests__/commit.options.spec-d.ts b/src/options/__tests__/commit.options.spec-d.ts new file mode 100644 index 00000000..7324592c --- /dev/null +++ b/src/options/__tests__/commit.options.spec-d.ts @@ -0,0 +1,27 @@ +/** + * @file Type Tests - CommitOptions + * @module grease/options/tests/unit-d/CommitOptions + */ + +import TestSubject from '../commit.options' +import type GitOptions from '../git.options' + +describe('unit-d:options/CommitOptions', () => { + it('should extend GitOptions', () => { + expectTypeOf().toMatchTypeOf() + }) + + it('should match [from: string]', () => { + expectTypeOf().toHaveProperty('from').toEqualTypeOf() + }) + + it('should match [issue_prefixes: string[]]', () => { + expectTypeOf() + .toHaveProperty('issue_prefixes') + .toEqualTypeOf() + }) + + it('should match [to: string]', () => { + expectTypeOf().toHaveProperty('to').toEqualTypeOf() + }) +}) diff --git a/src/options/__tests__/commit.options.spec.ts b/src/options/__tests__/commit.options.spec.ts new file mode 100644 index 00000000..1a8a7f11 --- /dev/null +++ b/src/options/__tests__/commit.options.spec.ts @@ -0,0 +1,36 @@ +/** + * @file Unit Tests - CommitOptions + * @module grease/options/tests/unit/CommitOptions + */ + +import pkg from '#pkg' assert { type: 'json' } +import TestSubject from '../commit.options' + +describe('unit:options/CommitOptions', () => { + describe('constructor', () => { + let from: TestSubject['from'] + let issue_prefixes: TestSubject['issue_prefixes'] + let subject: TestSubject + let to: TestSubject['to'] + + beforeAll(() => { + subject = new TestSubject({ + from: (from = pkg.tagPrefix + '1.0.0'), + issue_prefixes: (issue_prefixes = ['#']), + to: (to = pkg.tagPrefix + '2.0.0') + }) + }) + + it('should set #from', () => { + expect(subject).to.have.property('from', from) + }) + + it('should set #issue_prefixes', () => { + expect(subject).to.have.deep.property('issue_prefixes', issue_prefixes) + }) + + it('should set #to', () => { + expect(subject).to.have.property('to', to) + }) + }) +}) diff --git a/src/options/__tests__/git-tag.options.integration.spec.ts b/src/options/__tests__/git-tag.options.integration.spec.ts new file mode 100644 index 00000000..ca0c6be2 --- /dev/null +++ b/src/options/__tests__/git-tag.options.integration.spec.ts @@ -0,0 +1,47 @@ +/** + * @file Integration Tests - GitTagOptions + * @module grease/options/tests/integration/GitTagOptions + */ + +import { toURL } from '@flex-development/mlly' +import { sep } from '@flex-development/pathe' +import { DOT, cast } from '@flex-development/tutils' +import { validate } from 'class-validator' +import TestSubject from '../git-tag.options' + +describe('integration:options/GitTagOptions', () => { + describe('validation', () => { + it('should fail validation if schema is invalid', async () => { + // Arrange + const subject: TestSubject = new TestSubject(cast({ + cwd: toURL(sep), + debug: null, + tagprefix: /tutils@/, + unstable: 1 + })) + + // Act + const errors = await validate(subject, { + skipMissingProperties: false, + stopAtFirstError: false, + validationError: { target: false, value: true } + }) + + // Expect + expect(errors).toMatchSnapshot() + }) + + it('should pass validation if schema is valid', async () => { + // Arrange + const subject: TestSubject = new TestSubject({ + cwd: DOT, + debug: true, + tagprefix: 'grease@', + unstable: true + }) + + // Act + Expect + expect(await validate(subject)).to.be.an('array').that.is.empty + }) + }) +}) diff --git a/src/options/__tests__/git-tag.options.spec-d.ts b/src/options/__tests__/git-tag.options.spec-d.ts new file mode 100644 index 00000000..f0e02d50 --- /dev/null +++ b/src/options/__tests__/git-tag.options.spec-d.ts @@ -0,0 +1,25 @@ +/** + * @file Type Tests - GitTagOptions + * @module grease/options/tests/unit-d/GitTagOptions + */ + +import TestSubject from '../git-tag.options' +import type GitOptions from '../git.options' + +describe('unit-d:options/GitTagOptions', () => { + it('should extend GitOptions', () => { + expectTypeOf().toMatchTypeOf() + }) + + it('should match [tagprefix: string]', () => { + expectTypeOf() + .toHaveProperty('tagprefix') + .toEqualTypeOf() + }) + + it('should match [unstable: boolean]', () => { + expectTypeOf() + .toHaveProperty('unstable') + .toEqualTypeOf() + }) +}) diff --git a/src/options/__tests__/git-tag.options.spec.ts b/src/options/__tests__/git-tag.options.spec.ts new file mode 100644 index 00000000..70367d3f --- /dev/null +++ b/src/options/__tests__/git-tag.options.spec.ts @@ -0,0 +1,30 @@ +/** + * @file Unit Tests - GitTagOptions + * @module grease/options/tests/unit/GitTagOptions + */ + +import pkg from '#pkg' assert { type: 'json' } +import TestSubject from '../git-tag.options' + +describe('unit:options/GitTagOptions', () => { + describe('constructor', () => { + let subject: TestSubject + let tagprefix: TestSubject['tagprefix'] + let unstable: TestSubject['unstable'] + + beforeAll(() => { + subject = new TestSubject({ + tagprefix: (tagprefix = pkg.tagPrefix), + unstable: (unstable = false) + }) + }) + + it('should set #tagprefix', () => { + expect(subject).to.have.property('tagprefix', tagprefix) + }) + + it('should set #unstable', () => { + expect(subject).to.have.property('unstable', unstable) + }) + }) +}) diff --git a/src/options/__tests__/git.options.integration.spec.ts b/src/options/__tests__/git.options.integration.spec.ts new file mode 100644 index 00000000..0e9ddf1a --- /dev/null +++ b/src/options/__tests__/git.options.integration.spec.ts @@ -0,0 +1,43 @@ +/** + * @file Integration Tests - GitOptions + * @module grease/options/tests/integration/GitOptions + */ + +import { toURL } from '@flex-development/mlly' +import { sep } from '@flex-development/pathe' +import { DOT, cast } from '@flex-development/tutils' +import { validate } from 'class-validator' +import TestSubject from '../git.options' + +describe('integration:options/GitOptions', () => { + describe('validation', () => { + it('should fail validation if schema is invalid', async () => { + // Arrange + const subject: TestSubject = new TestSubject(cast({ + cwd: toURL(sep), + debug: null + })) + + // Act + const errors = await validate(subject, { + skipMissingProperties: false, + stopAtFirstError: false, + validationError: { target: false, value: true } + }) + + // Expect + expect(errors).toMatchSnapshot() + }) + + it('should pass validation if schema is valid', async () => { + // Arrange + const subject: TestSubject = new TestSubject({ + cwd: DOT, + debug: true + }) + + // Act + Expect + expect(await validate(subject)).to.be.an('array').that.is.empty + }) + }) +}) diff --git a/src/options/__tests__/git.options.spec-d.ts b/src/options/__tests__/git.options.spec-d.ts new file mode 100644 index 00000000..1e9e41c9 --- /dev/null +++ b/src/options/__tests__/git.options.spec-d.ts @@ -0,0 +1,16 @@ +/** + * @file Type Tests - GitOptions + * @module grease/options/tests/unit-d/GitOptions + */ + +import TestSubject from '../git.options' + +describe('unit-d:options/GitOptions', () => { + it('should match [cwd: string]', () => { + expectTypeOf().toHaveProperty('cwd').toEqualTypeOf() + }) + + it('should match [debug: boolean]', () => { + expectTypeOf().toHaveProperty('debug').toEqualTypeOf() + }) +}) diff --git a/src/options/__tests__/git.options.spec.ts b/src/options/__tests__/git.options.spec.ts new file mode 100644 index 00000000..3359de13 --- /dev/null +++ b/src/options/__tests__/git.options.spec.ts @@ -0,0 +1,30 @@ +/** + * @file Unit Tests - GitOptions + * @module grease/options/tests/unit/GitOptions + */ + +import pathe from '@flex-development/pathe' +import TestSubject from '../git.options' + +describe('unit:options/GitOptions', () => { + describe('constructor', () => { + let cwd: TestSubject['cwd'] + let debug: TestSubject['debug'] + let subject: TestSubject + + beforeAll(() => { + subject = new TestSubject({ + cwd: (cwd = pathe.resolve('__fixtures__/pkg/major')), + debug: (debug = true) + }) + }) + + it('should set #cwd', () => { + expect(subject).to.have.property('cwd', cwd) + }) + + it('should set #debug', () => { + expect(subject).to.have.property('debug', debug) + }) + }) +}) diff --git a/src/options/commit.options.ts b/src/options/commit.options.ts new file mode 100644 index 00000000..bb319a9a --- /dev/null +++ b/src/options/commit.options.ts @@ -0,0 +1,67 @@ +/** + * @file Options - CommitOptions + * @module grease/options/CommitOptions + */ + +import { get } from '@flex-development/tutils' +import { IsArray, IsString } from 'class-validator' +import GitOptions from './git.options' + +/** + * Commit retrieval and parsing options. + * + * @class + * @extends {GitOptions} + */ +class CommitOptions extends GitOptions { + /** + * Revision range start. + * + * @default '' + * + * @public + * @instance + * @member {string} from + */ + @IsString() + public from: string + + /** + * Issue reference prefixes. + * + * @default ['#','gh-'] + * + * @public + * @instance + * @member {string[]} issue_prefixes + */ + @IsArray() + @IsString({ each: true }) + public issue_prefixes: string[] + + /** + * Revision range end. + * + * @default 'HEAD' + * + * @public + * @instance + * @member {string} to + */ + @IsString() + public to: string + + /** + * Create a new options object. + * + * @param {Partial?} [opts] - Commit options + */ + constructor(opts?: Partial) { + super(opts) + this.from = get(opts, 'from', '') + this.issue_prefixes = get(opts, 'issue_prefixes', ['#', 'gh-']) + this.to = get(opts, 'to', 'HEAD') + } +} + +export default CommitOptions diff --git a/src/options/git-tag.options.ts b/src/options/git-tag.options.ts new file mode 100644 index 00000000..6c0d7a81 --- /dev/null +++ b/src/options/git-tag.options.ts @@ -0,0 +1,53 @@ +/** + * @file Options - GitTagOptions + * @module grease/options/GitTagOptions + */ + +import { get } from '@flex-development/tutils' +import { IsBoolean, IsString } from 'class-validator' +import GitOptions from './git.options' + +/** + * Git tag retrieval options. + * + * @class + * @extends {GitOptions} + */ +class GitTagOptions extends GitOptions { + /** + * Tag prefix to consider when validating tags. + * + * @default '' + * + * @public + * @instance + * @member {string} tagprefix + */ + @IsString() + public tagprefix: string + + /** + * Include unstable tags. + * + * @default true + * + * @public + * @instance + * @member {boolean} unstable + */ + @IsBoolean() + public unstable: boolean + + /** + * Create a new options object. + * + * @param {Partial?} [opts] - Git tag options + */ + constructor(opts?: Partial) { + super(opts) + this.tagprefix = get(opts, 'tagprefix', '') + this.unstable = get(opts, 'unstable', true) + } +} + +export default GitTagOptions diff --git a/src/options/git.options.ts b/src/options/git.options.ts new file mode 100644 index 00000000..cc74b0c6 --- /dev/null +++ b/src/options/git.options.ts @@ -0,0 +1,59 @@ +/** + * @file Options - GitOptions + * @module grease/options/GitOptions + */ + +import { IsDirectory } from '#src/decorators' +import { DOT, defaults, fallback, ifelse } from '@flex-development/tutils' +import { IsBoolean } from 'class-validator' + +/** + * Git options. + * + * @class + */ +class GitOptions { + /** + * Path to current working directory. + * + * @default process.cwd() + * + * @public + * @instance + * @member {string} cwd + */ + @IsDirectory() + public cwd: string + + /** + * Enable verbose output. + * + * @default false + * + * @public + * @instance + * @member {boolean} debug + */ + @IsBoolean() + public debug: boolean + + /** + * Create a new options object. + * + * @param {Partial?} [opts] - Git options + */ + constructor(opts?: Partial) { + const { + cwd, + debug + } = defaults(fallback(opts, {}), { + cwd: process.cwd(), + debug: false + }) + + this.cwd = ifelse(cwd === DOT, process.cwd(), cwd) + this.debug = debug + } +} + +export default GitOptions diff --git a/src/options/index.ts b/src/options/index.ts index 0cf4aeb2..f10c236e 100644 --- a/src/options/index.ts +++ b/src/options/index.ts @@ -4,3 +4,6 @@ */ export { default as BumpOptions, type BumpOptionsDTO } from './bump.options' +export { default as CommitOptions } from './commit.options' +export { default as GitTagOptions } from './git-tag.options' +export { default as GitOptions } from './git.options' diff --git a/src/providers/__snapshots__/git.service.snap b/src/providers/__snapshots__/git.service.snap new file mode 100644 index 00000000..7f70cfff --- /dev/null +++ b/src/providers/__snapshots__/git.service.snap @@ -0,0 +1,463 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`unit:providers/GitService > #commits > should return parsed commit array 1`] = ` +[ + { + "author": { + "email": "unicornware@flexdevelopment.llc", + "name": "Lex", + }, + "body": "", + "breaking": false, + "breaking_changes": [], + "date": "2023-09-09T23:59:31+00:00", + "hash": "a399eae", + "header": "release: 1.0.0-alpha.23 (#361)", + "mentions": [], + "pr": 361, + "references": [ + { + "action": null, + "number": 361, + "owner": null, + "ref": "#361", + "repo": null, + }, + ], + "scope": null, + "sha": "a399eaeff03a88cdb1d59fc1fb42d88fcdc773fe", + "subject": "1.0.0-alpha.23 (#361)", + "tags": [ + "1.0.0-alpha.23", + ], + "trailers": [ + { + "token": "Signed-off-by", + "value": "Lexus Drumgold ", + }, + ], + "type": "release", + "version": "1.0.0-alpha.23", + }, + { + "author": { + "email": "unicornware@flexdevelopment.llc", + "name": "Lexus Drumgold", + }, + "body": "- prettier 3.0 formatting has degraded in quality for js-like files, but the team refuses to fix it +- prettier can be removed completely once dprint has its own yaml plugin + better json5 support +- prettier markdown formatting was always subpar; it never played nicely with markdownlint +- prettier/prettier#15358 +- prettier/prettier#5715 +- prettier/prettier#11881 +- dprint/dprint#736 +- dprint/dprint-plugin-typescript#432", + "breaking": false, + "breaking_changes": [], + "date": "2023-09-09T19:00:25-04:00", + "hash": "7f578c9", + "header": "chore: dprint migration", + "mentions": [], + "pr": null, + "references": [ + { + "action": null, + "number": 15358, + "owner": "prettier", + "ref": "#15358", + "repo": "prettier", + }, + { + "action": null, + "number": 5715, + "owner": "prettier", + "ref": "#5715", + "repo": "prettier", + }, + { + "action": null, + "number": 11881, + "owner": "prettier", + "ref": "#11881", + "repo": "prettier", + }, + { + "action": null, + "number": 736, + "owner": "dprint", + "ref": "#736", + "repo": "dprint", + }, + { + "action": null, + "number": 432, + "owner": "dprint", + "ref": "#432", + "repo": "dprint-plugin-typescript", + }, + ], + "scope": null, + "sha": "7f578c90c69d12b283e49b9036c06029585630f8", + "subject": "dprint migration", + "tags": [], + "trailers": [ + { + "token": "Signed-off-by", + "value": "Lexus Drumgold ", + }, + ], + "type": "chore", + "version": null, + }, + { + "author": { + "email": "unicornware@flexdevelopment.llc", + "name": "Lexus Drumgold", + }, + "body": "- https://github.com/flex-development/nest-commander/releases/tag/1.0.0-alpha.1", + "breaking": false, + "breaking_changes": [], + "date": "2023-09-09T06:16:46-04:00", + "hash": "6624182", + "header": "build(deps-dev): use @flex-development/nest-commander in lieu of nest-commander", + "mentions": [], + "pr": null, + "references": [], + "scope": "deps-dev", + "sha": "6624182ccc862dd2a690225372fb825b8c4b6714", + "subject": "use @flex-development/nest-commander in lieu of nest-commander", + "tags": [], + "trailers": [ + { + "token": "Signed-off-by", + "value": "Lexus Drumgold ", + }, + ], + "type": "build", + "version": null, + }, + { + "author": { + "email": "unicornware@flexdevelopment.llc", + "name": "Lexus Drumgold", + }, + "body": "", + "breaking": false, + "breaking_changes": [], + "date": "2023-09-09T03:39:03-04:00", + "hash": "978ffe9", + "header": "fix(plugins): [\`decorators\`] decorator check", + "mentions": [], + "pr": null, + "references": [], + "scope": "plugins", + "sha": "978ffe9ec9abfb25868f6ff547cfd45c87c4111a", + "subject": "[\`decorators\`] decorator check", + "tags": [], + "trailers": [ + { + "token": "Signed-off-by", + "value": "Lexus Drumgold ", + }, + ], + "type": "fix", + "version": null, + }, + { + "author": { + "email": "49699333+dependabot[bot]@users.noreply.github.com", + "name": "dependabot[bot]", + }, + "body": "", + "breaking": false, + "breaking_changes": [], + "date": "2023-08-15T04:19:29+00:00", + "hash": "613a69a", + "header": "build(deps): Bump the flex-development group with 3 updates (#330)", + "mentions": [], + "pr": 330, + "references": [ + { + "action": null, + "number": 330, + "owner": null, + "ref": "#330", + "repo": null, + }, + ], + "scope": "deps", + "sha": "613a69aae2ce4087e0e6a4eebfdc3b535bcf88b8", + "subject": "Bump the flex-development group with 3 updates (#330)", + "tags": [], + "trailers": [ + { + "token": "Signed-off-by", + "value": "dependabot[bot] ", + }, + { + "token": "Co-authored-by", + "value": "dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>", + }, + ], + "type": "build", + "version": null, + }, + { + "author": { + "email": "unicornware@flexdevelopment.llc", + "name": "Lexus Drumgold", + }, + "body": "", + "breaking": true, + "breaking_changes": [ + { + "scope": "utils", + "subject": "[\`analyzeOutputs\`] intake \`esbuild.Metafile['outputs']\`", + "type": "refactor", + }, + ], + "date": "2023-04-07T19:38:09-04:00", + "hash": "f467b4b", + "header": "refactor(utils)!: [\`analyzeOutputs\`] intake \`esbuild.Metafile['outputs']\`", + "mentions": [], + "pr": null, + "references": [], + "scope": "utils", + "sha": "f467b4bae340a83d126c8dc80b75af0a912d7ff7", + "subject": "[\`analyzeOutputs\`] intake \`esbuild.Metafile['outputs']\`", + "tags": [], + "trailers": [ + { + "token": "Signed-off-by", + "value": "Lexus Drumgold ", + }, + ], + "type": "refactor", + "version": null, + }, + { + "author": { + "email": "unicornware@flexdevelopment.llc", + "name": "Lexus Drumgold", + }, + "body": "- https://esbuild.github.io/api/#watch +- replaced \`esbuilder\` with \`createContext\` to support watch mode (and in the future, serve + live reload) +- started adding cli flags +- overhauled interfaces", + "breaking": true, + "breaking_changes": [ + { + "scope": null, + "subject": "watch mode", + "type": "feat", + }, + ], + "date": "2023-03-31T02:50:34-04:00", + "hash": "f1decde", + "header": "feat!: watch mode", + "mentions": [], + "pr": null, + "references": [], + "scope": null, + "sha": "f1decde8597173569c69f8df8b013559231ff18b", + "subject": "watch mode", + "tags": [], + "trailers": [ + { + "token": "Signed-off-by", + "value": "Lexus Drumgold ", + }, + ], + "type": "feat", + "version": null, + }, + { + "author": { + "email": "unicornware@flexdevelopment.llc", + "name": "Lexus Drumgold", + }, + "body": "- import.meta.url may be used in build config (e.g. evanw/esbuild#1921) +- https://github.com/unjs/mlly/blob/v0.5.14/src/eval.ts#L17-L38", + "breaking": false, + "breaking_changes": [], + "date": "2022-09-27T03:23:45-04:00", + "hash": "3e9c7c8", + "header": "fix(config): prevent \`import.meta.url\` from being rewritten", + "mentions": [], + "pr": null, + "references": [ + { + "action": null, + "number": 1921, + "owner": "evanw", + "ref": "#1921", + "repo": "esbuild", + }, + ], + "scope": "config", + "sha": "3e9c7c881cbc93156fe31176c7a0090e698dfac6", + "subject": "prevent \`import.meta.url\` from being rewritten", + "tags": [], + "trailers": [ + { + "token": "Signed-off-by", + "value": "Lexus Drumgold ", + }, + ], + "type": "fix", + "version": null, + }, + { + "author": { + "email": "unicornware@flexdevelopment.llc", + "name": "Lex", + }, + "body": "", + "breaking": false, + "breaking_changes": [], + "date": "2022-09-15T21:22:09-04:00", + "hash": "1601d26", + "header": "feat: file-to-file transpilation (#4)", + "mentions": [], + "pr": 4, + "references": [ + { + "action": null, + "number": 4, + "owner": null, + "ref": "#4", + "repo": null, + }, + ], + "scope": null, + "sha": "1601d268bce74abafab6a4b0d0e244b35dd1e808", + "subject": "file-to-file transpilation (#4)", + "tags": [], + "trailers": [ + { + "token": "Signed-off-by", + "value": "Lexus Drumgold ", + }, + ], + "type": "feat", + "version": null, + }, + { + "author": { + "email": "unicornware@flexdevelopment.llc", + "name": "Lexus Drumgold", + }, + "body": "", + "breaking": false, + "breaking_changes": [], + "date": "2022-09-01T00:37:08-04:00", + "hash": "9c58d1c", + "header": "ci: add @dependabot config", + "mentions": [ + "dependabot", + ], + "pr": null, + "references": [], + "scope": null, + "sha": "9c58d1ca753406bbba92c729360b9b6e400a6e9a", + "subject": "add @dependabot config", + "tags": [], + "trailers": [ + { + "token": "Signed-off-by", + "value": "Lexus Drumgold ", + }, + ], + "type": "ci", + "version": null, + }, + { + "author": { + "email": "unicornware@flexdevelopment.llc", + "name": "Lexus Drumgold", + }, + "body": "", + "breaking": false, + "breaking_changes": [], + "date": "2022-09-01T00:34:48-04:00", + "hash": "50385b6", + "header": "initial commit", + "mentions": [], + "pr": null, + "references": [], + "scope": null, + "sha": "50385b68593d7b07f269631af0ca551539fe22d0", + "subject": "", + "tags": [], + "trailers": [ + { + "token": "Signed-off-by", + "value": "Lexus Drumgold ", + }, + ], + "type": "", + "version": null, + }, +] +`; + +exports[`unit:providers/GitService > #tags > should return tags array with unstable tags 1`] = ` +[ + "1.0.0-alpha.22", + "1.0.0-alpha.21", + "1.0.0-alpha.20", + "1.0.0-alpha.19", + "1.0.0-alpha.18", + "1.0.0-alpha.17", + "1.0.0-alpha.16", + "1.0.0-alpha.15", + "1.0.0-alpha.14", + "1.0.0-alpha.13", + "1.0.0-alpha.12", + "1.0.0-alpha.11", + "1.0.0-alpha.10", + "1.0.0-alpha.9", + "1.0.0-alpha.8", + "1.0.0-alpha.7", + "1.0.0-alpha.6", + "1.0.0-alpha.5", + "1.0.0-alpha.4", + "1.0.0-alpha.3", + "1.0.0-alpha.2", + "1.0.0-alpha.1", +] +`; + +exports[`unit:providers/GitService > #tags > should return tags array without unstable tags 1`] = ` +[ + "tutils@5.0.1", + "tutils@5.0.0", + "tutils@4.8.0", + "tutils@4.7.0", + "tutils@4.6.0", + "tutils@4.5.0", + "tutils@4.4.0", + "tutils@4.3.0", + "tutils@4.2.3", + "tutils@4.2.2", + "tutils@4.2.1", + "tutils@4.2.0", + "tutils@4.1.1", + "tutils@4.1.0", + "tutils@4.0.3", + "tutils@4.0.2", + "tutils@4.0.1", + "tutils@4.0.0", + "tutils@3.1.7", + "tutils@3.1.6", + "tutils@3.1.5", + "tutils@3.1.4", + "tutils@3.1.3", + "tutils@3.1.2", + "tutils@3.1.1", + "tutils@3.1.0", + "tutils@3.0.0", + "tutils@2.0.0", + "tutils@1.0.0", +] +`; diff --git a/src/providers/__tests__/git.service.spec.ts b/src/providers/__tests__/git.service.spec.ts new file mode 100644 index 00000000..94f16b9f --- /dev/null +++ b/src/providers/__tests__/git.service.spec.ts @@ -0,0 +1,89 @@ +/** + * @file Unit Tests - GitService + * @module grease/providers/tests/unit/GitService + */ + +import type { EmptyString } from '@flex-development/tutils' +import { Test, type TestingModule } from '@nestjs/testing' +import consola from 'consola' +import fs from 'node:fs/promises' +import TestSubject from '../git.service' + +describe('unit:providers/GitService', () => { + let ref: TestingModule + let subject: TestSubject + + beforeAll(async () => { + consola.mockTypes(() => vi.fn()) + ref = await Test.createTestingModule({ providers: [TestSubject] }).compile() + subject = ref.get(TestSubject) + }) + + describe('#commits', () => { + it('should return parsed commit array', async () => { + // Arrange + vi.spyOn(subject, 'log').mockImplementationOnce(async () => ({ + stderr: '', + stdout: await fs.readFile('__fixtures__/commits/mkbuild.txt', 'utf8') + })) + + // Act + const result = await subject.commits() + + // Expect + expect(result).to.be.an('array').that.is.not.empty + expect(result).toMatchSnapshot() + }) + }) + + describe('#log', () => { + it('should return command output', async () => { + // Act + const result = await subject.log(['HEAD'], { debug: true }) + + // Expect + expect(result).to.have.property('stderr').be.a('string').that.is.empty + expect(result).to.have.property('stdout').startWith('commit') + }) + }) + + describe('#tags', () => { + let stderr: EmptyString + let tagprefix: string + + beforeAll(() => { + stderr = '' + tagprefix = 'tutils@' + }) + + it('should return tags array with unstable tags', async () => { + // Arrange + vi.spyOn(subject, 'log').mockImplementationOnce(async () => ({ + stderr, + stdout: await fs.readFile('__fixtures__/tags/mkbuild.txt', 'utf8') + })) + + // Act + const result = await subject.tags() + + // Expect + expect(result).to.be.an('array').that.is.not.empty + expect(result).toMatchSnapshot() + }) + + it('should return tags array without unstable tags', async () => { + // Arrange + vi.spyOn(subject, 'log').mockImplementationOnce(async () => ({ + stderr, + stdout: await fs.readFile('__fixtures__/tags/tutils.txt', 'utf8') + })) + + // Act + const result = await subject.tags({ tagprefix, unstable: false }) + + // Expect + expect(result).to.be.an('array').that.is.not.empty + expect(result).toMatchSnapshot() + }) + }) +}) diff --git a/src/providers/bump.service.ts b/src/providers/bump.service.ts index 8a925c5a..914385cb 100644 --- a/src/providers/bump.service.ts +++ b/src/providers/bump.service.ts @@ -7,7 +7,7 @@ import { Version } from '#src/models' import { BumpOptions, type BumpOptionsDTO } from '#src/options' import type { PackageJson } from '@flex-development/pkg-types' import { cast, define } from '@flex-development/tutils' -import { Injectable } from '@nestjs/common/decorators' +import { Injectable } from '@nestjs/common' import { validateOrReject } from 'class-validator' import consola from 'consola' import fs from 'node:fs/promises' diff --git a/src/providers/git.service.ts b/src/providers/git.service.ts new file mode 100644 index 00000000..edda40db --- /dev/null +++ b/src/providers/git.service.ts @@ -0,0 +1,330 @@ +/** + * @file Providers - GitService + * @module grease/providers/GitService + */ + +import type { Commit } from '#src/interfaces' +import { CommitGrammar } from '#src/models' +import { CommitOptions, GitTagOptions } from '#src/options' +import type { BreakingChange, CommitLogField, Trailer } from '#src/types' +import { + at, + cast, + construct, + fallback, + fork, + get, + ifelse, + includes, + isNaN, + join, + ksort, + objectify, + pick, + select, + sift, + split, + template, + trim, + trimEnd, + type Construct +} from '@flex-development/tutils' +import { Injectable } from '@nestjs/common' +import consola from 'consola' +import { exec, type ExecOptions } from 'node:child_process' +import util from 'node:util' +import semver from 'semver' + +/** + * Git operations provider. + * + * @see https://git-scm.com/docs + * + * @class + */ +@Injectable() +class GitService { + /** + * Commit log format. + * + * @see https://git-scm.com/docs/git-log#_pretty_formats + * + * @public + * @static + * @readonly + * @member {string} RAW_COMMIT_FORMAT + */ + public static readonly RAW_COMMIT_FORMAT: string = join( + [ + '%s%n', + 'author.email', + '%n%ae%n', + 'author.name', + '%n%an%n', + 'body', + '%n%b%n', + 'date', + '%n%cI%n', + 'hash', + '%n%h%n', + 'sha', + '%n%H%n', + 'tags', + '%n%D%n', + 'trailers', + '%n%(trailers)%n' + ], + '-' + ) + + /** + * Get an array of parsed commits. + * + * @see {@linkcode CommitOptions} + * + * @public + * @async + * + * @param {Partial?} [opts] - Commit options + * @return {Promise} Parsed commit array + */ + public async commits(opts?: Partial): Promise { + const { cwd, debug, from, issue_prefixes, to } = new CommitOptions(opts) + + /** + * String used to separate commit logs. + * + * @const {string} LOG_DELIMITER + */ + const LOG_DELIMITER: string = '--$--' + + /** + * Git command arguments. + * + * @const {string[]} args + */ + const args: string[] = sift([ + '--decorate-refs=refs/tags', + '--decorate=short', + template('--format=\'{format}{delimiter}\'', { + delimiter: LOG_DELIMITER, + format: GitService.RAW_COMMIT_FORMAT + }), + join(sift([from, to]), '..'), + ifelse(cwd === process.cwd(), '', `-- ${cwd}`) + ]) + + /** + * Raw commit logs. + * + * @const {{ stderr: string; stdout: string }} logs + */ + const logs: { stderr: string; stdout: string } = await this.log(args, { + cwd, + debug + }) + + /** + * Array containing parsed commits. + * + * @const {Commit[]} commits + */ + const commits: Commit[] = [] + + /** + * Commit parser grammar. + * + * @const {CommitGrammar} grammar + */ + const grammar: CommitGrammar = new CommitGrammar() + + // parse raw commits + for (let chunk of split(logs.stdout, LOG_DELIMITER + '\n')) { + if (!(chunk = trim(chunk))) continue + + // get commit header and raw fields + const [[header = ''], raw_fields]: [string[], string[]] = fork( + split(chunk, /^(?=-.*?-\n*$)/gm), + raw => !grammar.field.test(raw) + ) + + // extract commit header data + const { + groups: { + breaking = null, + pr = 'null', + scope = null, + subject = '', + type = '' + } = {} + } = pick(grammar.header.exec(header)!, ['groups']) + + /** + * Commit fields object. + * + * @const {Construct>} fields + */ + const fields: Construct> = construct( + cast>( + objectify( + raw_fields, + raw => get(grammar.field.exec(raw)!.groups, 'field', ''), + raw => trim(raw.replace(grammar.field, '')) + ) + ) + ) + + /** + * Tags associated with commit. + * + * @const {string[]} tags + */ + const tags: string[] = select( + split(fields.tags, ','), + tag => !!trim(tag), + tag => trim(tag.replace(/^tag: */, '')) + ) + + /** + * Commit message trailers. + * + * @const {Trailer[]} trailers + */ + const trailers: Trailer[] = select( + [...fields.trailers.matchAll(grammar.trailer)], + null, + match => pick(match.groups!, ['token', 'value']) + ) + + /** + * Breaking changes noted in commit subject and trailers. + * + * @const {BreakingChange[]} breaking_changes + */ + const breaking_changes: BreakingChange[] = select( + trailers, + trailer => /^BREAKING[ -]CHANGE/.test(trailer.token), + trailer => ({ scope: null, subject: trailer.value, type }) + ) + + // add subject to breaking changes if breaking change is in subject + if (breaking && !includes(breaking_changes, subject)) { + breaking_changes.unshift({ scope, subject, type }) + } + + /** + * Parsed commit object. + * + * @var {Commit} commit + */ + const commit: Commit = { + ...fields, + body: trimEnd(fields.body.replace(fields.trailers, '')), + breaking: !!breaking_changes.length, + breaking_changes, + header: header.replace(/\n$/, ''), + mentions: select( + [...chunk.matchAll(grammar.mention)], + null, + match => get(match, 'groups.user')! + ), + pr: fallback(+pr, null, isNaN), + references: select( + [...chunk.matchAll(grammar.reference(issue_prefixes))], + null, + match => ({ + action: get(match, 'groups.action', null), + number: +get(match, 'groups.number', ''), + owner: get(match, 'groups.owner', null), + ref: get(match, 'groups.ref', ''), + repo: get(match, 'groups.repo', null) + }) + ), + scope, + subject, + tags, + trailers, + type, + version: at(tags, 0, null) + } + + commits.push(ksort(commit, { deep: true })) + } + + return commits + } + + /** + * Get commit logs. + * + * @see https://git-scm.com/docs/git-log + * + * @public + * @async + * + * @param {ReadonlyArray} args - `git log` options + * @param {ExecOptions & { debug?: boolean }} [opts={}] - Exec options + * @return {Promise<{ stderr: string; stdout: string }>} Command output + */ + public async log( + args: readonly string[], + opts: ExecOptions & { debug?: boolean } = {} + ): Promise<{ stderr: string; stdout: string }> { + /** + * Command to execute. + * + * @const {string} command + */ + const command: string = join(['git', 'log', ...args], ' ') + + // debug git log command + if (opts.debug) consola.info(`[GitService.${this.log.name}]`, command) + + return util.promisify(exec)(command, { + ...opts, + encoding: 'utf8', + maxBuffer: Number.POSITIVE_INFINITY, + shell: process.env.SHELL + }) + } + + /** + * Get an array containing all git tags in reverse chronological order. + * + * @see {@linkcode GitTagOptions} + * + * @public + * @async + * + * @param {Partial} [opts] - Git tag options + * @return {Promise} Git tags array + */ + public async tags(opts?: Partial): Promise { + const { cwd, debug, tagprefix, unstable } = new GitTagOptions(opts) + + /** + * Raw commit logs. + * + * @const {{ stderr: string; stdout: string }} logs + */ + const logs: { stderr: string; stdout: string } = await this.log([ + '--decorate-refs=refs/tags', + '--decorate=short', + '--format=%D' + ], { cwd, debug, env: process.env }) + + return select( + select( + split(logs.stdout, '\n'), + tag => trim(tag).startsWith(`tag: ${tagprefix}`), + tag => trim(tag).replace(/^tag: */, '') + ), + tag => ( + !!semver.valid(tag = tag.replace(new RegExp(`^${tagprefix}`), '')) && + (unstable ? true : !semver.parse(tag)!.prerelease.length) + ) + ) + } +} + +export default GitService diff --git a/src/providers/index.ts b/src/providers/index.ts index 73426850..23e003d0 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -4,4 +4,5 @@ */ export { default as BumpService } from './bump.service' +export { default as GitService } from './git.service' export { default as PackageService } from './package.service' diff --git a/src/providers/package.service.ts b/src/providers/package.service.ts index 4afa7c39..5aec36ce 100644 --- a/src/providers/package.service.ts +++ b/src/providers/package.service.ts @@ -10,7 +10,7 @@ import { type PackageScope } from '@flex-development/mlly' import { define, DOT, type Nullable } from '@flex-development/tutils' -import { Injectable } from '@nestjs/common/decorators' +import { Injectable } from '@nestjs/common' import semver from 'semver' /** diff --git a/src/types/__tests__/author.spec-d.ts b/src/types/__tests__/author.spec-d.ts new file mode 100644 index 00000000..a7f06518 --- /dev/null +++ b/src/types/__tests__/author.spec-d.ts @@ -0,0 +1,16 @@ +/** + * @file Type Tests - Author + * @module grease/types/tests/unit-d/Author + */ + +import type TestSubject from '../author' + +describe('unit-d:types/Author', () => { + it('should match [email: string]', () => { + expectTypeOf().toHaveProperty('email').toEqualTypeOf() + }) + + it('should match [name: string]', () => { + expectTypeOf().toHaveProperty('name').toEqualTypeOf() + }) +}) diff --git a/src/types/__tests__/breaking-change.spec-d.ts b/src/types/__tests__/breaking-change.spec-d.ts new file mode 100644 index 00000000..d6c27212 --- /dev/null +++ b/src/types/__tests__/breaking-change.spec-d.ts @@ -0,0 +1,25 @@ +/** + * @file Type Tests - BreakingChange + * @module grease/types/tests/unit-d/BreakingChange + */ + +import type { Nullable } from '@flex-development/tutils' +import type TestSubject from '../breaking-change' + +describe('unit-d:types/BreakingChange', () => { + it('should match [scope: Nullable]', () => { + expectTypeOf() + .toHaveProperty('scope') + .toEqualTypeOf>() + }) + + it('should match [subject: string]', () => { + expectTypeOf() + .toHaveProperty('subject') + .toEqualTypeOf() + }) + + it('should match [type: string]', () => { + expectTypeOf().toHaveProperty('type').toEqualTypeOf() + }) +}) diff --git a/src/types/__tests__/commit-log-field.spec-d.ts b/src/types/__tests__/commit-log-field.spec-d.ts new file mode 100644 index 00000000..214383ba --- /dev/null +++ b/src/types/__tests__/commit-log-field.spec-d.ts @@ -0,0 +1,40 @@ +/** + * @file Type Tests - CommitLogField + * @module grease/types/tests/unit-d/CommitLogField + */ + +import type TestSubject from '../commit-log-field' + +describe('unit-d:types/CommitLogField', () => { + it('should extract "author.email"', () => { + expectTypeOf().extract<'author.email'>().not.toBeNever() + }) + + it('should extract "author.name"', () => { + expectTypeOf().extract<'author.name'>().not.toBeNever() + }) + + it('should extract "body"', () => { + expectTypeOf().extract<'body'>().not.toBeNever() + }) + + it('should extract "date"', () => { + expectTypeOf().extract<'date'>().not.toBeNever() + }) + + it('should extract "hash"', () => { + expectTypeOf().extract<'hash'>().not.toBeNever() + }) + + it('should extract "sha"', () => { + expectTypeOf().extract<'sha'>().not.toBeNever() + }) + + it('should extract "tags"', () => { + expectTypeOf().extract<'tags'>().not.toBeNever() + }) + + it('should extract "trailers"', () => { + expectTypeOf().extract<'trailers'>().not.toBeNever() + }) +}) diff --git a/src/types/__tests__/trailer.spec-d.ts b/src/types/__tests__/trailer.spec-d.ts new file mode 100644 index 00000000..b56f4ccd --- /dev/null +++ b/src/types/__tests__/trailer.spec-d.ts @@ -0,0 +1,16 @@ +/** + * @file Type Tests - Trailer + * @module grease/types/tests/unit-d/Trailer + */ + +import type TestSubject from '../trailer' + +describe('unit-d:types/Trailer', () => { + it('should match [token: string]', () => { + expectTypeOf().toHaveProperty('token').toEqualTypeOf() + }) + + it('should match [value: string]', () => { + expectTypeOf().toHaveProperty('value').toEqualTypeOf() + }) +}) diff --git a/src/types/author.ts b/src/types/author.ts new file mode 100644 index 00000000..dc4b0c31 --- /dev/null +++ b/src/types/author.ts @@ -0,0 +1,21 @@ +/** + * @file Type Definitions - Author + * @module grease/types/Author + */ + +/** + * A commit author. + */ +type Author = { + /** + * Commit author email. + */ + email: string + + /** + * Commit author name. + */ + name: string +} + +export type { Author as default } diff --git a/src/types/breaking-change.ts b/src/types/breaking-change.ts new file mode 100644 index 00000000..d66f0a8f --- /dev/null +++ b/src/types/breaking-change.ts @@ -0,0 +1,28 @@ +/** + * @file Type Definitions - BreakingChange + * @module grease/types/BreakingChange + */ + +import type { Nullable } from '@flex-development/tutils' + +/** + * A breaking change noted in a commit subject or trailer. + */ +type BreakingChange = { + /** + * Commit scope if breaking change was noted in a scoped commit message. + */ + scope: Nullable + + /** + * Commit subject or breaking change trailer value. + */ + subject: string + + /** + * Commit type. + */ + type: string +} + +export type { BreakingChange as default } diff --git a/src/types/commit-log-field.ts b/src/types/commit-log-field.ts new file mode 100644 index 00000000..f85efadd --- /dev/null +++ b/src/types/commit-log-field.ts @@ -0,0 +1,18 @@ +/** + * @file Type Definitions - CommitLogField + * @module grease/types/CommitLogField + */ + +/** + * Raw commit field names. + */ +type CommitLogField = + | 'body' + | 'date' + | 'hash' + | 'sha' + | 'tags' + | 'trailers' + | `author.${'email' | 'name'}` + +export type { CommitLogField as default } diff --git a/src/types/index.ts b/src/types/index.ts index f4e71972..302f1d8c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,4 +3,8 @@ * @module grease/types */ +export type { default as Author } from './author' +export type { default as BreakingChange } from './breaking-change' +export type { default as CommitLogField } from './commit-log-field' export type { default as ReleaseVersion } from './release-version' +export type { default as Trailer } from './trailer' diff --git a/src/types/trailer.ts b/src/types/trailer.ts new file mode 100644 index 00000000..b90492cd --- /dev/null +++ b/src/types/trailer.ts @@ -0,0 +1,23 @@ +/** + * @file Type Definitions - Trailer + * @module grease/types/Trailer + */ + +/** + * A trailer line in a commit message. + * + * @see https://git-scm.com/docs/git-interpret-trailers + */ +type Trailer = { + /** + * Trailer token. + */ + token: string + + /** + * Trailer value. + */ + value: string +} + +export type { Trailer as default } diff --git a/yarn.lock b/yarn.lock index a568520b..37dc2182 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1417,6 +1417,7 @@ __metadata: "@nestjs/core": "npm:10.2.5" "@nestjs/testing": "npm:10.2.5" "@types/chai": "npm:4.3.5" + "@types/chai-string": "npm:1.4.3" "@types/dateformat": "npm:5.0.0" "@types/eslint": "npm:8.44.2" "@types/is-ci": "npm:3.0.0" @@ -1424,7 +1425,6 @@ __metadata: "@types/node-notifier": "npm:8.0.2" "@types/prettier": "npm:3.0.0" "@types/semver": "npm:7.5.0" - "@types/shelljs": "npm:0.8.12" "@typescript-eslint/eslint-plugin": "npm:6.6.0" "@typescript-eslint/parser": "npm:6.6.0" "@vates/toggle-scripts": "npm:1.0.0" @@ -1432,6 +1432,7 @@ __metadata: "@vitest/expect": "npm:0.34.3" chai: "npm:5.0.0-alpha.1" chai-each: "npm:0.0.1" + chai-string: "npm:1.5.0" class-transformer: "npm:0.5.1" class-validator: "npm:0.14.0" consola: "npm:3.2.3" @@ -1471,7 +1472,6 @@ __metadata: rxjs: "npm:7.8.1" semver: "npm:7.5.4" sh-syntax: "npm:0.4.1" - shelljs: "npm:0.8.5" trash-cli: "npm:5.0.0" ts-dedent: "npm:2.2.0" typescript: "npm:5.2.2" @@ -2642,6 +2642,15 @@ __metadata: languageName: node linkType: hard +"@types/chai-string@npm:1.4.3": + version: 1.4.3 + resolution: "@types/chai-string@npm:1.4.3" + dependencies: + "@types/chai": "npm:*" + checksum: a1f2a8046ed1b21930b650f3e0387c0cbb930c987e87b996f8595a1d0638c0c86406e487ad876eeca3bdfada84a8f45a81cf8a7308eab48629dfcc8c952e5d78 + languageName: node + linkType: hard + "@types/chai-subset@npm:^1.3.3": version: 1.3.3 resolution: "@types/chai-subset@npm:1.3.3" @@ -2691,16 +2700,6 @@ __metadata: languageName: node linkType: hard -"@types/glob@npm:~7.2.0": - version: 7.2.0 - resolution: "@types/glob@npm:7.2.0" - dependencies: - "@types/minimatch": "npm:*" - "@types/node": "npm:*" - checksum: 6ae717fedfdfdad25f3d5a568323926c64f52ef35897bcac8aca8e19bc50c0bd84630bbd063e5d52078b2137d8e7d3c26eabebd1a2f03ff350fff8a91e79fc19 - languageName: node - linkType: hard - "@types/is-ci@npm:3.0.0": version: 3.0.0 resolution: "@types/is-ci@npm:3.0.0" @@ -2740,13 +2739,6 @@ __metadata: languageName: node linkType: hard -"@types/minimatch@npm:*": - version: 5.1.2 - resolution: "@types/minimatch@npm:5.1.2" - checksum: 94db5060d20df2b80d77b74dd384df3115f01889b5b6c40fa2dfa27cfc03a68fb0ff7c1f2a0366070263eb2e9d6bfd8c87111d4bc3ae93c3f291297c1bf56c85 - languageName: node - linkType: hard - "@types/minimist@npm:^1.2.0, @types/minimist@npm:^1.2.2": version: 1.2.2 resolution: "@types/minimist@npm:1.2.2" @@ -2807,16 +2799,6 @@ __metadata: languageName: node linkType: hard -"@types/shelljs@npm:0.8.12": - version: 0.8.12 - resolution: "@types/shelljs@npm:0.8.12" - dependencies: - "@types/glob": "npm:~7.2.0" - "@types/node": "npm:*" - checksum: c0517e8355614bad2d391270538bd5c7b65d7a771b333cb3ba9fb6321b4b44e1787e99d19cda97ac5d25ec84e2d6b81cb8cc496da5bdae6a725d922de4b44c70 - languageName: node - linkType: hard - "@types/unist@npm:^2, @types/unist@npm:^2.0.2": version: 2.0.8 resolution: "@types/unist@npm:2.0.8" @@ -3643,6 +3625,15 @@ __metadata: languageName: node linkType: hard +"chai-string@npm:1.5.0": + version: 1.5.0 + resolution: "chai-string@npm:1.5.0" + peerDependencies: + chai: ^4.1.2 + checksum: 39b9511525c99b9d378210897caf6352be34976c24f2e773680c2de4173d2071f3010ea0348bf681e2a201564524e28dd5541ebce8a181a455c8aeefe2a7bda3 + languageName: node + linkType: hard + "chai@npm:5.0.0-alpha.1": version: 5.0.0-alpha.1 resolution: "chai@npm:5.0.0-alpha.1" @@ -5645,7 +5636,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.0.0, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4": +"glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -6136,13 +6127,6 @@ __metadata: languageName: node linkType: hard -"interpret@npm:^1.0.0": - version: 1.4.0 - resolution: "interpret@npm:1.4.0" - checksum: 5beec568d3f60543d0f61f2c5969d44dffcb1a372fe5abcdb8013968114d4e4aaac06bc971a4c9f5bd52d150881d8ebad72a8c60686b1361f5f0522f39c0e1a3 - languageName: node - linkType: hard - "ip@npm:^2.0.0": version: 2.0.0 resolution: "ip@npm:2.0.0" @@ -8192,15 +8176,6 @@ __metadata: languageName: node linkType: hard -"rechoir@npm:^0.6.2": - version: 0.6.2 - resolution: "rechoir@npm:0.6.2" - dependencies: - resolve: "npm:^1.1.6" - checksum: fe76bf9c21875ac16e235defedd7cbd34f333c02a92546142b7911a0f7c7059d2e16f441fe6fb9ae203f459c05a31b2bcf26202896d89e390eda7514d5d2702b - languageName: node - linkType: hard - "redent@npm:^3.0.0": version: 3.0.0 resolution: "redent@npm:3.0.0" @@ -8333,7 +8308,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.6, resolve@npm:^1.10.0, resolve@npm:^1.10.1, resolve@npm:^1.22.4": +"resolve@npm:^1.10.0, resolve@npm:^1.10.1, resolve@npm:^1.22.4": version: 1.22.4 resolution: "resolve@npm:1.22.4" dependencies: @@ -8346,7 +8321,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.1.6#optional!builtin, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.10.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": +"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.10.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": version: 1.22.4 resolution: "resolve@patch:resolve@npm%3A1.22.4#optional!builtin::version=1.22.4&hash=c3c19d" dependencies: @@ -8531,19 +8506,6 @@ __metadata: languageName: node linkType: hard -"shelljs@npm:0.8.5": - version: 0.8.5 - resolution: "shelljs@npm:0.8.5" - dependencies: - glob: "npm:^7.0.0" - interpret: "npm:^1.0.0" - rechoir: "npm:^0.6.2" - bin: - shjs: bin/shjs - checksum: f2178274b97b44332bbe9ddb78161137054f55ecf701c7a99db9552cb5478fe279ad5f5131d8a7c2f0730e01ccf0c629d01094143f0541962ce1a3d0243d23f7 - languageName: node - linkType: hard - "shellwords@npm:^0.1.1": version: 0.1.1 resolution: "shellwords@npm:0.1.1"