mejora 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/config-De8VvJ6A.mjs +4 -0
- package/dist/index.d.mts +159 -0
- package/dist/index.mjs +1 -0
- package/dist/run.d.mts +1 -0
- package/dist/run.mjs +35 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 mejora
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# mejora
|
|
2
|
+
|
|
3
|
+
Prevent regressions by allowing only improvement.
|
|
4
|
+
|
|
5
|
+
`mejora` runs checks, compares their results to a stored baseline, and fails only when things get worse.
|
|
6
|
+
|
|
7
|
+
If results improve or stay the same, the run passes.
|
|
8
|
+
|
|
9
|
+
## What problem it solves
|
|
10
|
+
|
|
11
|
+
Most tools ask: _“Is this perfect?”_
|
|
12
|
+
`mejora` asks: _“Did this regress?”_
|
|
13
|
+
|
|
14
|
+
This makes it practical for:
|
|
15
|
+
|
|
16
|
+
- large or legacy codebases
|
|
17
|
+
- incremental cleanup
|
|
18
|
+
- CI enforcement without blocking progress
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
1. Each check produces a snapshot, a list of items
|
|
23
|
+
2. The snapshot is compared to a baseline
|
|
24
|
+
3. New items cause failure
|
|
25
|
+
4. Removed items are treated as improvement
|
|
26
|
+
|
|
27
|
+
Baselines are explicit and should be committed to the repo.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm add -D mejora
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> [!NOTE]
|
|
36
|
+
> `mejora` requires Node.js 22.18.0 or later.
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Run checks:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm mejora
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Force the baseline to accept regressions:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm mejora --force
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
JSON output for CI and automation:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pnpm mejora --json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Run only a subset of checks:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pnpm mejora --only "eslint > *"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Skip checks:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pnpm mejora --skip typescript
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
Create one of:
|
|
73
|
+
|
|
74
|
+
- `mejora.config.ts`
|
|
75
|
+
- `mejora.config.js`
|
|
76
|
+
- `mejora.config.mjs`
|
|
77
|
+
- `mejora.config.mts`
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { defineConfig, eslintCheck, typescriptCheck } from "mejora";
|
|
83
|
+
|
|
84
|
+
export default defineConfig({
|
|
85
|
+
checks: {
|
|
86
|
+
"eslint > no-nested-ternary": eslintCheck({
|
|
87
|
+
files: ["src/**/*.{ts,tsx,js,jsx}"],
|
|
88
|
+
overrides: {
|
|
89
|
+
rules: {
|
|
90
|
+
"no-nested-ternary": "error",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
"typescript": typescriptCheck({
|
|
95
|
+
overrides: {
|
|
96
|
+
compilerOptions: {
|
|
97
|
+
noImplicitAny: true,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Each entry in `checks` is an explicit check.
|
|
106
|
+
The object key is the check identifier and is used in the baseline.
|
|
107
|
+
|
|
108
|
+
## Supported checks
|
|
109
|
+
|
|
110
|
+
### ESLint
|
|
111
|
+
|
|
112
|
+
- Snapshot type: items
|
|
113
|
+
- Each lint message is treated as an item
|
|
114
|
+
- Regressions are new lint messages
|
|
115
|
+
|
|
116
|
+
> [!NOTE]
|
|
117
|
+
> `eslint` (^9.34.0) is required as a peer dependency when using the ESLint check
|
|
118
|
+
|
|
119
|
+
### TypeScript
|
|
120
|
+
|
|
121
|
+
- Snapshot type: items
|
|
122
|
+
- Each compiler diagnostic is treated as an item
|
|
123
|
+
- Regressions are new diagnostics
|
|
124
|
+
- Uses the nearest `tsconfig.json` by default, or an explicit one if provided
|
|
125
|
+
|
|
126
|
+
> [!NOTE]
|
|
127
|
+
> `typescript` (^5.0.0) is required as a peer dependency when using the TypeScript check
|
|
128
|
+
|
|
129
|
+
## Snapshot type
|
|
130
|
+
|
|
131
|
+
### Items
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{ "type": "items", "items": ["file.ts:12", "file.ts:45"] }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Fails if new items appear.
|
|
138
|
+
Order does not matter.
|
|
139
|
+
|
|
140
|
+
## Baseline
|
|
141
|
+
|
|
142
|
+
Default location:
|
|
143
|
+
|
|
144
|
+
```txt
|
|
145
|
+
.mejora/baseline.json
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"version": 1,
|
|
153
|
+
"checks": {
|
|
154
|
+
"eslint > no-nested-ternary": {
|
|
155
|
+
"type": "items",
|
|
156
|
+
"items": ["src/a.ts:1"]
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The baseline represents the last accepted state.
|
|
163
|
+
|
|
164
|
+
## Improvements and baseline updates
|
|
165
|
+
|
|
166
|
+
When a run produces fewer items than the baseline, the run passes and the baseline is updated automatically to reflect the improvement.
|
|
167
|
+
|
|
168
|
+
This includes items that no longer exist in the codebase.
|
|
169
|
+
|
|
170
|
+
Improvements are reported separately from regressions so progress is visible.
|
|
171
|
+
|
|
172
|
+
## CI behavior
|
|
173
|
+
|
|
174
|
+
When running in CI, mejora does not write the baseline.
|
|
175
|
+
|
|
176
|
+
Instead, it compares the committed baseline to the expected results from the current codebase.
|
|
177
|
+
|
|
178
|
+
If there is any difference between the committed baseline and the expected results, the run fails.
|
|
179
|
+
|
|
180
|
+
## Force mode
|
|
181
|
+
|
|
182
|
+
`mejora --force` updates the baseline even when regressions are present.
|
|
183
|
+
|
|
184
|
+
## Exit codes
|
|
185
|
+
|
|
186
|
+
- `0` pass or improvement
|
|
187
|
+
- `1` regression detected or baseline out of sync
|
|
188
|
+
- `2` configuration or runtime error
|
|
189
|
+
|
|
190
|
+
## Output
|
|
191
|
+
|
|
192
|
+
- Default output is plain text
|
|
193
|
+
- No prompts
|
|
194
|
+
- No interactive behavior
|
|
195
|
+
- `--json` produces structured, deterministic output
|
|
196
|
+
|
|
197
|
+
## Merge Conflicts
|
|
198
|
+
|
|
199
|
+
mejora automatically resolves conflicts in both `baseline.json` and `baseline.md`:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# After merging branches with baseline changes
|
|
203
|
+
$ git status
|
|
204
|
+
both modified: .mejora/baseline.json
|
|
205
|
+
both modified: .mejora/baseline.md
|
|
206
|
+
|
|
207
|
+
# Just run mejora - both files are auto-resolved
|
|
208
|
+
$ mejora
|
|
209
|
+
Merge conflict detected in baseline, auto-resolving...
|
|
210
|
+
✓ Baseline conflict resolved
|
|
211
|
+
|
|
212
|
+
# Commit the resolved files
|
|
213
|
+
$ git add .mejora/
|
|
214
|
+
$ git commit -m "Merge feature branch"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Inspiration
|
|
218
|
+
|
|
219
|
+
`mejora` is inspired by [betterer](https://phenomnomnominal.github.io/betterer/).
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{createRequire as e}from"node:module";import{relative as t,resolve as n}from"node:path";import{createHash as r}from"node:crypto";import{pathToFileURL as i}from"node:url";var a=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),o=e(import.meta.url);function s(e){let t=JSON.stringify(e??null,(e,t)=>{if(t&&typeof t==`object`&&!Array.isArray(t)){let e=t;return Object.keys(e).toSorted().reduce((t,n)=>({...t,[n]:e[n]}),{})}return t});return r(`sha256`).update(t).digest(`hex`)}const c=({column:e,cwd:n,filePath:r,line:i,ruleId:a})=>`${t(n,r)}:${i}:${e} - ${a}`;async function l(){try{await import(`eslint`)}catch{throw Error(`ESLint check requires eslint but it's not installed.`)}}async function u(e){let{ESLint:t}=await import(`eslint`),n=process.cwd(),r=await new t({cache:!0,cacheLocation:`node_modules/.cache/mejora/eslint/${s(e)}`,concurrency:`auto`,overrideConfig:e.overrides}).lintFiles(e.files),i=[];for(let{filePath:e,messages:t}of r)for(let{column:r,line:a,ruleId:o}of t)if(o){let t=c({column:r,cwd:n,filePath:e,line:a,ruleId:o});i.push(t)}return{items:i.toSorted(),type:`items`}}function d(e){return{type:`eslint`,...e}}async function f(){try{await import(`typescript`)}catch{throw Error(`TypeScript check requires typescript but it's not installed.`)}}const p=({character:e,code:n,cwd:r,fileName:i,line:a,message:o})=>`${t(r,i)}:${a+1}:${e+1} - TS${n}: ${o}`;async function m(e){let{createProgram:t,findConfigFile:r,flattenDiagnosticMessageText:i,getPreEmitDiagnostics:a,parseJsonConfigFileContent:o,readConfigFile:s,sys:c}=await import(`typescript`),l=process.cwd(),u=c.fileExists.bind(c),d=c.readFile.bind(c),f=e.tsconfig?n(e.tsconfig):r(l,u,`tsconfig.json`);if(!f)throw Error(`TypeScript config file not found`);let{config:m,error:h}=s(f,d);if(h){let e=typeof h.messageText==`string`?h.messageText:i(h.messageText,`
|
|
2
|
+
`);throw TypeError(`Failed to read TypeScript config: ${e}`)}let g=o(m,c,process.cwd(),e.overrides?.compilerOptions),_=a(t({options:g.options,rootNames:g.fileNames})),v=[];for(let e of _)if(e.file&&e.start!==void 0){let{character:t,line:n}=e.file.getLineAndCharacterOfPosition(e.start),r=i(e.messageText,`
|
|
3
|
+
`),a=p({character:t,code:e.code,cwd:l,fileName:e.file.fileName,line:n,message:r});v.push(a)}else{let t=i(e.messageText,`
|
|
4
|
+
`),n=`(global) - TS${e.code}: ${t}`;v.push(n)}return{items:v.toSorted(),type:`items`}}function h(e){return{type:`typescript`,...e}}var g=a(((e,t)=>{let n=o(`path`),r=o(`fs`),i=o(`os`),a=o(`url`),s=r.promises.readFile;function c(e,t){return[`package.json`,`.${e}rc.json`,`.${e}rc.js`,`.${e}rc.cjs`,...t?[]:[`.${e}rc.mjs`],`.config/${e}rc`,`.config/${e}rc.json`,`.config/${e}rc.js`,`.config/${e}rc.cjs`,...t?[]:[`.config/${e}rc.mjs`],`${e}.config.js`,`${e}.config.cjs`,...t?[]:[`${e}.config.mjs`]]}function l(e){return n.dirname(e)||n.sep}let u=(e,t)=>JSON.parse(t),d=typeof __webpack_require__==`function`?__non_webpack_require__:o,f=Object.freeze({".js":d,".json":d,".cjs":d,noExt:u});t.exports.defaultLoadersSync=f;let p=async e=>{try{return(await import(a.pathToFileURL(e).href)).default}catch(t){try{return d(e)}catch(e){throw e.code===`ERR_REQUIRE_ESM`||e instanceof SyntaxError&&e.toString().includes(`Cannot use import statement outside a module`)?t:e}}},m=Object.freeze({".js":p,".mjs":p,".cjs":p,".json":u,noExt:u});t.exports.defaultLoaders=m;function h(e,t,r){let a={stopDir:i.homedir(),searchPlaces:c(e,r),ignoreEmptySearchPlaces:!0,cache:!0,transform:e=>e,packageProp:[e],...t,loaders:{...r?f:m,...t.loaders}};return a.searchPlaces.forEach(e=>{let t=n.extname(e)||`noExt`,r=a.loaders[t];if(!r)throw Error(`Missing loader for extension "${e}"`);if(typeof r!=`function`)throw Error(`Loader for extension "${e}" is not a function: Received ${typeof r}.`)}),a}function g(e,t){return typeof e==`string`&&e in t?t[e]:(Array.isArray(e)?e:e.split(`.`)).reduce((e,t)=>e===void 0?e:e[t],t)||null}function _(e){if(!e)throw Error(`load must pass a non-empty string`)}function v(e,t){if(!e)throw Error(`No loader specified for extension "${t}"`);if(typeof e!=`function`)throw Error(`loader is not a function`)}let y=e=>(t,n,r)=>(e&&t.set(n,r),r);t.exports.lilconfig=function(e,t){let{ignoreEmptySearchPlaces:i,loaders:a,packageProp:o,searchPlaces:c,stopDir:u,transform:d,cache:f}=h(e,t??{},!1),p=new Map,m=new Map,b=y(f);return{async search(e=process.cwd()){let t={config:null,filepath:``},m=new Set,h=e;dirLoop:for(;;){if(f){let e=p.get(h);if(e!==void 0){for(let t of m)p.set(t,e);return e}m.add(h)}for(let e of c){let c=n.join(h,e);try{await r.promises.access(c)}catch{continue}let l=String(await s(c)),u=n.extname(e)||`noExt`,d=a[u];if(e===`package.json`){let e=g(o,await d(c,l));if(e!=null){t.config=e,t.filepath=c;break dirLoop}continue}let f=l.trim()===``;if(!(f&&i)){f?(t.isEmpty=!0,t.config=void 0):(v(d,u),t.config=await d(c,l)),t.filepath=c;break dirLoop}}if(h===u||h===l(h))break dirLoop;h=l(h)}let _=t.filepath===``&&t.config===null?d(null):d(t);if(f)for(let e of m)p.set(e,_);return _},async load(e){_(e);let t=n.resolve(process.cwd(),e);if(f&&m.has(t))return m.get(t);let{base:r,ext:c}=n.parse(t),l=c||`noExt`,u=a[l];v(u,l);let p=String(await s(t));if(r===`package.json`)return b(m,t,d({config:g(o,await u(t,p)),filepath:t}));let h={config:null,filepath:t},y=p.trim()===``;return y&&i?b(m,t,d({config:void 0,filepath:t,isEmpty:!0})):(h.config=y?void 0:await u(t,p),b(m,t,d(y?{...h,isEmpty:y,config:void 0}:h)))},clearLoadCache(){f&&m.clear()},clearSearchCache(){f&&p.clear()},clearCaches(){f&&(m.clear(),p.clear())}}},t.exports.lilconfigSync=function(e,t){let{ignoreEmptySearchPlaces:i,loaders:a,packageProp:o,searchPlaces:s,stopDir:c,transform:u,cache:d}=h(e,t??{},!0),f=new Map,p=new Map,m=y(d);return{search(e=process.cwd()){let t={config:null,filepath:``},p=new Set,m=e;dirLoop:for(;;){if(d){let e=f.get(m);if(e!==void 0){for(let t of p)f.set(t,e);return e}p.add(m)}for(let e of s){let s=n.join(m,e);try{r.accessSync(s)}catch{continue}let c=n.extname(e)||`noExt`,l=a[c],u=String(r.readFileSync(s));if(e===`package.json`){let e=g(o,l(s,u));if(e!=null){t.config=e,t.filepath=s;break dirLoop}continue}let d=u.trim()===``;if(!(d&&i)){d?(t.isEmpty=!0,t.config=void 0):(v(l,c),t.config=l(s,u)),t.filepath=s;break dirLoop}}if(m===c||m===l(m))break dirLoop;m=l(m)}let h=t.filepath===``&&t.config===null?u(null):u(t);if(d)for(let e of p)f.set(e,h);return h},load(e){_(e);let t=n.resolve(process.cwd(),e);if(d&&p.has(t))return p.get(t);let{base:s,ext:c}=n.parse(t),l=c||`noExt`,f=a[l];v(f,l);let h=String(r.readFileSync(t));if(s===`package.json`)return u({config:g(o,f(t,h)),filepath:t});let y={config:null,filepath:t},b=h.trim()===``;return b&&i?m(p,t,u({filepath:t,config:void 0,isEmpty:!0})):(y.config=b?void 0:f(t,h),m(p,t,u(b?{...y,isEmpty:b,config:void 0}:y)))},clearLoadCache(){d&&p.clear()},clearSearchCache(){d&&f.clear()},clearCaches(){d&&(p.clear(),f.clear())}}}}))();const _=async e=>{let t=await import(i(e).href);return t&&typeof t==`object`&&`default`in t?t.default:t},v=e=>e,y=async()=>{let e=await(0,g.lilconfig)(`mejora`,{loaders:{".js":_,".mjs":_,".mts":_,".ts":_},searchPlaces:[`mejora.config.ts`,`mejora.config.mts`,`mejora.config.js`,`mejora.config.mjs`]}).search(process.cwd());if(!e?.config)throw Error(`No configuration file found.`);return e.config};export{f as a,l as c,h as i,y as n,d as o,m as r,u as s,v as t};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as eslint0 from "eslint";
|
|
2
|
+
import { Linter } from "eslint";
|
|
3
|
+
import * as typescript0 from "typescript";
|
|
4
|
+
import { CompilerOptions } from "typescript";
|
|
5
|
+
|
|
6
|
+
//#region src/types.d.ts
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Configuration for an ESLint check.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* eslintCheck({
|
|
14
|
+
* files: ["src/**\/*.{ts,tsx}"],
|
|
15
|
+
* overrides: {
|
|
16
|
+
* rules: {
|
|
17
|
+
* "no-nested-ternary": "error",
|
|
18
|
+
* },
|
|
19
|
+
* },
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
interface ESLintCheckConfig {
|
|
24
|
+
/**
|
|
25
|
+
* Glob patterns for files to lint.
|
|
26
|
+
*
|
|
27
|
+
* Passed directly to ESLint's `lintFiles()` method.
|
|
28
|
+
*
|
|
29
|
+
* @example ["src/**\/*.ts", "src/**\/*.tsx"]
|
|
30
|
+
*/
|
|
31
|
+
files: string[];
|
|
32
|
+
/**
|
|
33
|
+
* ESLint configuration to merge with the base config.
|
|
34
|
+
*
|
|
35
|
+
* This is passed to ESLint's `overrideConfig` option and merged with
|
|
36
|
+
* your existing ESLint configuration.
|
|
37
|
+
*
|
|
38
|
+
* Can be a single config object or an array of config objects.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* {
|
|
43
|
+
* rules: {
|
|
44
|
+
* "no-console": "error",
|
|
45
|
+
* },
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
overrides?: Linter.Config | Linter.Config[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Configuration for a TypeScript diagnostics check.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* typescriptCheck({
|
|
57
|
+
* overrides: {
|
|
58
|
+
* compilerOptions: {
|
|
59
|
+
* noImplicitAny: true,
|
|
60
|
+
* },
|
|
61
|
+
* },
|
|
62
|
+
* })
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
interface TypeScriptCheckConfig {
|
|
66
|
+
/**
|
|
67
|
+
* Compiler options to merge with the base tsconfig.
|
|
68
|
+
*
|
|
69
|
+
* These options are merged with (not replacing) the compiler options
|
|
70
|
+
* from your tsconfig file.
|
|
71
|
+
*/
|
|
72
|
+
overrides?: {
|
|
73
|
+
compilerOptions?: CompilerOptions;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Path to a TypeScript config file.
|
|
77
|
+
*
|
|
78
|
+
* If not provided, mejora will search for the nearest `tsconfig.json`
|
|
79
|
+
* starting from the current working directory.
|
|
80
|
+
*
|
|
81
|
+
* @example "tsconfig.strict.json"
|
|
82
|
+
*/
|
|
83
|
+
tsconfig?: string;
|
|
84
|
+
}
|
|
85
|
+
type CheckConfig = (ESLintCheckConfig & {
|
|
86
|
+
type: "eslint";
|
|
87
|
+
}) | (TypeScriptCheckConfig & {
|
|
88
|
+
type: "typescript";
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* mejora configuration.
|
|
92
|
+
*
|
|
93
|
+
* Define checks to run and track for regressions.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* import { defineConfig, eslintCheck, typescriptCheck } from "mejora";
|
|
98
|
+
*
|
|
99
|
+
* export default defineConfig({
|
|
100
|
+
* checks: {
|
|
101
|
+
* "eslint > no-nested-ternary": eslintCheck({
|
|
102
|
+
* files: ["src/**\/*.{ts,tsx}"],
|
|
103
|
+
* overrides: {
|
|
104
|
+
* rules: {
|
|
105
|
+
* "no-nested-ternary": "error",
|
|
106
|
+
* },
|
|
107
|
+
* },
|
|
108
|
+
* }),
|
|
109
|
+
* "typescript": typescriptCheck({
|
|
110
|
+
* overrides: {
|
|
111
|
+
* compilerOptions: {
|
|
112
|
+
* noImplicitAny: true,
|
|
113
|
+
* },
|
|
114
|
+
* },
|
|
115
|
+
* }),
|
|
116
|
+
* },
|
|
117
|
+
* });
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
interface Config {
|
|
121
|
+
/**
|
|
122
|
+
* Check definitions.
|
|
123
|
+
*
|
|
124
|
+
* Each key is a check identifier used in the baseline and output.
|
|
125
|
+
* The identifier can contain any characters.
|
|
126
|
+
*
|
|
127
|
+
* Use `eslintCheck()` and `typescriptCheck()` helpers to create check configs.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* {
|
|
132
|
+
* "eslint > no-console": eslintCheck({ ... }),
|
|
133
|
+
* "typescript": typescriptCheck({ ... }),
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
checks: Record<string, CheckConfig>;
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/checks/eslint.d.ts
|
|
141
|
+
declare function eslintCheck(config: ESLintCheckConfig): {
|
|
142
|
+
files: string[];
|
|
143
|
+
overrides?: eslint0.Linter.Config | eslint0.Linter.Config[];
|
|
144
|
+
type: "eslint";
|
|
145
|
+
};
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/checks/typescript.d.ts
|
|
148
|
+
declare function typescriptCheck(config: TypeScriptCheckConfig): {
|
|
149
|
+
overrides?: {
|
|
150
|
+
compilerOptions?: typescript0.CompilerOptions;
|
|
151
|
+
};
|
|
152
|
+
tsconfig?: string;
|
|
153
|
+
type: "typescript";
|
|
154
|
+
};
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/config.d.ts
|
|
157
|
+
declare const defineConfig: (config: Config) => Config;
|
|
158
|
+
//#endregion
|
|
159
|
+
export { type Config, defineConfig, eslintCheck, typescriptCheck };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as e,o as t,t as n}from"./config-De8VvJ6A.mjs";export{n as defineConfig,t as eslintCheck,e as typescriptCheck};
|
package/dist/run.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/run.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{a as e,c as t,n,r,s as i}from"./config-De8VvJ6A.mjs";import{dirname as a,relative as o}from"node:path";import{inspect as s,parseArgs as c,styleText as l}from"node:util";import{mkdir as u,readFile as d,writeFile as f}from"node:fs/promises";import{env as p}from"node:process";const m=e=>l(`blue`,e),h=e=>l(`bold`,e),g=e=>l(`cyan`,e),_=e=>l(`dim`,e),v=e=>l(`green`,e),y=e=>l(`greenBright`,e),b=e=>l(`red`,e),x=e=>l(`bgRed`,e),S=e=>l(`yellow`,e),C=e=>l(`black`,e);function w(e){if(e<1e3)return`${e}ms`;let t=e/1e3;if(t<60)return t%1==0?`${t}s`:`${t.toFixed(1)}s`;let n=e/6e4;if(n<60)return n%1==0?`${n}m`:`${n.toFixed(1)}m`;let r=e/36e5;return r%1==0?`${r}h`:`${r.toFixed(1)}h`}function T(e){let t=Math.round(e);if(t<1)return _(`<1ms`);let n=w(t);return t<100?y(n):t<1e3?S(n):b(n)}function E(e){let t={checks:e.results.map(e=>({checkId:e.checkId,duration:e.duration,hasImprovement:e.hasImprovement,hasRegression:e.hasRegression,isInitial:e.isInitial,newItems:e.newItems,removedItems:e.removedItems,totalIssues:e.snapshot.items.length||0})),exitCode:e.exitCode,hasImprovement:e.hasImprovement,hasRegression:e.hasRegression,totalDuration:e.totalDuration};return JSON.stringify(t,null,2)}function D(e,t=10){let n=[],r=e.slice(0,t);for(let e of r)n.push(` ${_(e)}`);let i=e.length-t;return i>0&&n.push(` ${_(`... and ${i} more`)}`),n}function ee(e,t){let n=[`${t?``:`
|
|
3
|
+
`}${h(e.checkId)}:`,` Initial baseline created with ${g(e.snapshot.items.length.toString())} issue(s)`];return e.snapshot.items.length>0&&n.push(...D(e.snapshot.items)),e.duration!==void 0&&n.push(` ${_(`Completed in`)} ${T(e.duration)}`),n}function O(e){return e.hasRegression?[` ${b(e.newItems.length.toString())} new issue(s) (regressions):`,...D(e.newItems)]:[]}function k(e){return e.hasImprovement?[` ${y(e.removedItems.length.toString())} issue(s) fixed (improvements):`,...D(e.removedItems)]:[]}function A(e,t){let n=[`${t?``:`
|
|
4
|
+
`}${h(e.checkId)}:`,...O(e),...k(e)];return e.duration!==void 0&&n.push(` ${_(`Completed in`)} ${T(e.duration)}`),n}function j(e,t){return e.isInitial?ee(e,t):e.hasRegression||e.hasImprovement?A(e,t):[]}function M(e){let t=e.results.some(e=>e.isInitial),n=[];return t?n.push(m(`Initial baseline created successfully.`)):e.hasRegression?n.push(`${b(`Regressions detected.`)} Run failed.`):e.hasImprovement?n.push(`${y(`Improvements detected.`)} Baseline updated.`):n.push(y(`All checks passed.`)),e.totalDuration!==void 0&&n.push(`${_(`Completed in`)} ${T(e.totalDuration)}`),n.join(`
|
|
5
|
+
`)}function N(e){let t=[];for(let n=0;n<e.results.length;n++){let r=e.results[n];t.push(...j(r,n===0))}return t.length>0&&t.push(``),t.push(M(e)),t.join(`
|
|
6
|
+
`)}const P=e=>e in p&&p[e]!==`0`&&p[e]!==`false`;var F=P(`CI`)||P(`CONTINUOUS_INTEGRATION`);function I(e,t){let n=e;for(let e=0;e<t&&n.includes(`}`);e++){let e=n.lastIndexOf(`}`);n=n.slice(0,e)+n.slice(e+1)}return n}function L(e,t){return`${e}\n${` }`.repeat(t)}`}function R(e){let t=0,n=0;for(let r of e)r===`{`&&t++,r===`}`&&n++;if(t===n)return e;let r=Math.abs(t-n);return n>t?I(e,r):L(e,r)}function z(e){let t=e.trim();return R(t.endsWith(`,`)?t.slice(0,-1):t)}function B(e){return`{
|
|
7
|
+
"version": 1,
|
|
8
|
+
${e}
|
|
9
|
+
}`}function V(e){if(e.checks)return e;let t={};for(let[n,r]of Object.entries(e))n!==`version`&&(t[n]=r);return{checks:t,version:1}}function H(e){try{let t=B(z(e));return V(JSON.parse(t))}catch(e){let t=e instanceof Error?e.message:String(e);throw Error(`Failed to parse baseline during conflict resolution: ${t}`,{cause:e})}}function U(e){let t=[...e.matchAll(/<<<<<<< .*\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> .*$/gm)];if(t.length===0)throw Error(`Could not parse conflict markers in baseline`);return t.map(([,e=``,t=``])=>({ours:e,theirs:t}))}function te(e){let t=new Set;for(let n of e)for(let e of Object.keys(n.checks))t.add(e);return t}function W(e,t){let n=new Set;for(let r of e){let e=r.checks[t];if(e?.items)for(let t of e.items)n.add(t)}return[...n].toSorted()}function G(e){let t={checks:{},version:1},n=te(e);for(let r of n)t.checks[r]={items:W(e,r),type:`items`};return t}function K(e){let t=U(e),n=[];for(let{ours:e,theirs:r}of t)n.push(H(e),H(r));return G(n)}function q(e,t){let n=`# Mejora Baseline
|
|
10
|
+
|
|
11
|
+
`,r=Object.entries(e.checks);for(let e=0;e<r.length;e++){let[i,{items:a=[]}]=r[e],s=e===r.length-1;if(n+=`## ${i}\n\n`,a.length===0)n+=`No issues
|
|
12
|
+
`;else for(let e of a){let[r,...i]=e.split(` - `),a=r?.split(`:`),s=a?.[0],c=a?.[1];if(s){let e=o(t,s),a=c?`${e}#L${c}`:e,l=i.length>0?` - ${i.join(` - `)}`:``;n+=`- [${r}](${a})${l}\n`}}s||(n+=`
|
|
13
|
+
`)}return n}const J=(e,t)=>{if(!t)return!1;let n=e.items??[],r=t.items??[];if(n.length!==r.length)return!1;let i=n.toSorted(),a=r.toSorted();return i.every((e,t)=>e===a[t])};function Y(e){let t=[e.message];if(e.stack){let n=e.stack.split(`
|
|
14
|
+
`).slice(1).map(e=>_(e.trim())).join(`
|
|
15
|
+
`);t.push(n)}return t.join(`
|
|
16
|
+
`)}function X(...e){return e.map(e=>typeof e==`string`?e:e instanceof Error?Y(e):s(e,{colors:!1,depth:10})).join(` `)}const Z={error:(...e)=>{console.error(x(C(` ERROR `)),X(...e))},log:(...e)=>{console.log(X(...e))},start:(...e)=>{console.log(g(`◐`),X(...e))},success:(...e)=>{console.log(v(`✔`),X(...e))}};var Q=class e{baselinePath;constructor(e=`.mejora/baseline.json`){this.baselinePath=e}static create(e){return{checks:e,version:1}}static getEntry(e,t){return e?.checks[t]}static update(t,n,r){let i=t??e.create({}),a=i.checks[n];return J(r,a)?i:{...i,checks:{...i.checks,[n]:r}}}async load(){try{let e=await d(this.baselinePath,`utf8`);if(e.includes(`<<<<<<<`)){Z.start(`Merge conflict detected in baseline, auto-resolving...`);let t=K(e);return await this.save(t,!0),Z.success(`Baseline conflict resolved`),t}return JSON.parse(e)}catch(e){if(e.code===`ENOENT`)return null;throw e}}async save(e,t=!1){if(F&&!t)return;let n=`${JSON.stringify(e,null,2)}\n`,r=this.baselinePath.replace(`.json`,`.md`),i=q(e,a(this.baselinePath));await u(a(this.baselinePath),{recursive:!0}),await Promise.all([f(this.baselinePath,n,`utf8`),f(r,i,`utf8`)])}};function ne(){return{hasImprovement:!1,hasRegression:!1,isInitial:!0,newItems:[],removedItems:[]}}function re(e,t){return{hasImprovement:t.length>0,hasRegression:e.length>0,isInitial:!1,newItems:e.toSorted(),removedItems:t.toSorted()}}function ie(e,t){let n=[];for(let r of e)t.has(r)||n.push(r);return n}function ae(e,t){let n=[];for(let r of t)e.has(r)||n.push(r);return n}function oe(e,t){let n=new Set(e.items),r=new Set(t.items);return re(ie(n,r),ae(n,r))}function se(e,t){return t?oe(e,t):ne()}var ce=class n{baselineManager;constructor(e){this.baselineManager=new Q(e)}static filterChecks(e,t){let r={...e};if(t.only){let e=n.resolveRegex(t.only,`--only`);r=Object.fromEntries(Object.entries(r).filter(([t])=>e.test(t)))}if(t.skip){let e=n.resolveRegex(t.skip,`--skip`);r=Object.fromEntries(Object.entries(r).filter(([t])=>!e.test(t)))}return r}static resolveRegex(e,t){try{return new RegExp(e)}catch{throw Error(`Invalid regex pattern for ${t}: "${e}"`)}}static async runCheck(n){return n.type===`eslint`?(await t(),i(n)):(await e(),r(n))}async run(e,t={}){let r=performance.now(),i=await this.baselineManager.load(),a=[],o=!1,s=!1,c=!1,l=i,u=n.filterChecks(e.checks,t);for(let[e,d]of Object.entries(u))try{let r=performance.now(),u=await n.runCheck(d),f=performance.now()-r,p=Q.getEntry(i,e),m=se(u,p),h={baseline:p,checkId:e,duration:f,hasImprovement:m.hasImprovement,hasRegression:m.hasRegression,isInitial:m.isInitial,newItems:m.newItems,removedItems:m.removedItems,snapshot:u};a.push(h),m.hasRegression&&(o=!0),m.hasImprovement&&(s=!0),m.isInitial&&(c=!0),(m.hasImprovement||t.force||m.isInitial)&&(l=Q.update(l,e,{items:u.items,type:u.type}))}catch(t){return Z.error(`Error running check "${e}":`,t),{exitCode:2,hasImprovement:!1,hasRegression:!0,results:a,totalDuration:performance.now()-r}}l&&l!==i&&(!o||t.force||c)&&await this.baselineManager.save(l,t.force);let d=0;o&&!t.force&&(d=1);let f=performance.now()-r;return{exitCode:d,hasImprovement:s,hasRegression:o,results:a,totalDuration:f}}};const{values:$}=c({allowPositionals:!1,options:{force:{default:!1,short:`f`,type:`boolean`},help:{short:`h`,type:`boolean`},json:{default:!1,type:`boolean`},only:{type:`string`},skip:{type:`string`}},strict:!0});$.help&&(Z.log(`
|
|
17
|
+
mejora - Prevent regressions by allowing only improvement
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
mejora [options]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
-f, --force Update baseline even with regressions
|
|
24
|
+
--json Output results as JSON
|
|
25
|
+
--only <pattern> Run only checks matching pattern (e.g., "eslint > *")
|
|
26
|
+
--skip <pattern> Skip checks matching pattern
|
|
27
|
+
-h, --help Show this help message
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
mejora
|
|
31
|
+
mejora --force
|
|
32
|
+
mejora --json
|
|
33
|
+
mejora --only "eslint > *"
|
|
34
|
+
mejora --skip typescript
|
|
35
|
+
`),process.exit(0));try{let e=new ce,t=await n(),r=await e.run(t,{force:$.force,json:$.json,only:$.only,skip:$.skip});$.json?Z.log(E(r)):Z.log(N(r)),process.exit(r.exitCode)}catch(e){e instanceof Error?Z.error(e.message):Z.error(e),process.exit(2)}export{};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mejora",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/jimmy-guzman/mejora.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./dist/index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.mjs",
|
|
17
|
+
"types": "./dist/index.d.mts",
|
|
18
|
+
"bin": {
|
|
19
|
+
"mejora": "./dist/run.mjs"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"eslint": "^9.34.0",
|
|
26
|
+
"typescript": "^5.0.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"eslint": {
|
|
30
|
+
"optional": true
|
|
31
|
+
},
|
|
32
|
+
"typescript": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">= 22.18.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|