@ladamczyk/outdated 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 +106 -0
- package/bin/cli.js +5 -0
- package/lib/index.cjs +320 -0
- package/lib/index.mjs +319 -0
- package/lib/src/check.d.ts +2 -0
- package/lib/src/check.test.d.ts +1 -0
- package/lib/src/cli.d.ts +2 -0
- package/lib/src/deps.d.ts +17 -0
- package/lib/src/deps.test.d.ts +1 -0
- package/lib/src/format.d.ts +7 -0
- package/lib/src/format.test.d.ts +1 -0
- package/lib/src/index.d.ts +2 -0
- package/lib/src/npm.d.ts +15 -0
- package/lib/src/npm.test.d.ts +1 -0
- package/lib/src/options.d.ts +14 -0
- package/lib/src/options.test.d.ts +1 -0
- package/lib/src/pool.d.ts +1 -0
- package/lib/src/pool.test.d.ts +1 -0
- package/lib/src/readme.test.d.ts +1 -0
- package/lib/src/run.d.ts +11 -0
- package/lib/src/run.test.d.ts +1 -0
- package/lib/src/runner.d.ts +2 -0
- package/lib/src/runner.test.d.ts +1 -0
- package/lib/src/time.d.ts +3 -0
- package/lib/src/time.test.d.ts +1 -0
- package/lib/src/types.d.ts +56 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Łukasz Adamczyk
|
|
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,106 @@
|
|
|
1
|
+
# @ladamczyk/outdated
|
|
2
|
+
|
|
3
|
+
CLI that reports outdated dependencies using the npm CLI.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
outdated [options]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The table mirrors `npm outdated` (`Package`, `Current`, `Wanted`, `Latest`, `Location`, `Depended by`) and adds `Last publish` and `Flags` at the end. It is split into `dependencies`, `devDependencies` and `optionalDependencies` sections (empty ones are omitted); packages keep their `package.json` order and the columns line up across sections. `Wanted` falls back to `Current` for packages `npm outdated` does not list.
|
|
12
|
+
|
|
13
|
+
Flags are coloured: deprecated red, stale yellow, outdated cyan, unknown magenta, blocked blue, ok green; a major version bump shows `Latest` in red. Package names are links to their npmjs.com page in terminals that support hyperlinks. Colours and links turn off automatically when output is piped or `NO_COLOR` is set, and `--json` never contains either.
|
|
14
|
+
|
|
15
|
+
## Options
|
|
16
|
+
|
|
17
|
+
- `--cwd <dir>` — Project folder (default: `.`)
|
|
18
|
+
- `--stale-after <months>` — Months without a release before a package is stale (default: `6`)
|
|
19
|
+
- `--include <types>` — Dependency types to check: `prod`, `dev`, `optional`, comma-separated (default: all)
|
|
20
|
+
- `--ignore <pkgs>` — Comma-separated package names to skip entirely
|
|
21
|
+
- `--concurrency <n>` — Parallel `npm view` lookups (default: `6`)
|
|
22
|
+
- `--json` — Output machine-readable JSON
|
|
23
|
+
- `--only-problems` — Hide ok packages from the table
|
|
24
|
+
- `--fail-on <list>` — Exit 1 when any of `stale`, `deprecated`, `outdated`, `unknown` is found (default: none)
|
|
25
|
+
- `--help, -h` — Display help message
|
|
26
|
+
- `--version, -v` — Display version number
|
|
27
|
+
|
|
28
|
+
## Exit codes
|
|
29
|
+
|
|
30
|
+
- `0` — Success, and no `--fail-on` condition met
|
|
31
|
+
- `1` — A `--fail-on` condition was met
|
|
32
|
+
- `2` — Usage or runtime error (no `package.json`, npm not found, invalid options, `npm outdated` failure)
|
|
33
|
+
|
|
34
|
+
## JSON schema
|
|
35
|
+
|
|
36
|
+
When `--json` is used, the output conforms to the following schema (schemaVersion: 1):
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"schemaVersion": 1,
|
|
41
|
+
"generatedAt": "ISO 8601 timestamp",
|
|
42
|
+
"staleAfterMonths": 6,
|
|
43
|
+
"project": "package.json name, else the directory name",
|
|
44
|
+
"packages": [
|
|
45
|
+
{
|
|
46
|
+
"name": "package name",
|
|
47
|
+
"queriedName": "real package for npm: aliases, else null",
|
|
48
|
+
"type": "prod | dev | optional",
|
|
49
|
+
"specifier": "version specifier from package.json",
|
|
50
|
+
"current": "currently installed version or null",
|
|
51
|
+
"wanted": "npm outdated wanted version or null",
|
|
52
|
+
"latest": "dist-tags.latest or npm outdated latest or null",
|
|
53
|
+
"outdated": true,
|
|
54
|
+
"majorBump": true,
|
|
55
|
+
"deprecated": "full deprecation message or null",
|
|
56
|
+
"lastPublish": "ISO 8601 of dist-tags.latest release date or null",
|
|
57
|
+
"stale": true,
|
|
58
|
+
"unknown": "reason npm view failed or null",
|
|
59
|
+
"latestNode": "engines.node of the latest release, or null",
|
|
60
|
+
"flags": ["deprecated", "stale", "outdated", "unknown", "blocked"]
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"summary": {
|
|
64
|
+
"total": 42,
|
|
65
|
+
"deprecated": 3,
|
|
66
|
+
"stale": 5,
|
|
67
|
+
"outdated": 8,
|
|
68
|
+
"unknown": 2,
|
|
69
|
+
"blocked": 1,
|
|
70
|
+
"ok": 24,
|
|
71
|
+
"skipped": 0
|
|
72
|
+
},
|
|
73
|
+
"skipped": [
|
|
74
|
+
{
|
|
75
|
+
"name": "package name",
|
|
76
|
+
"type": "prod | dev | optional",
|
|
77
|
+
"specifier": "version specifier",
|
|
78
|
+
"reason": "why it was skipped"
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Note that `--only-problems` filters the table output but never filters the JSON `packages` array.
|
|
85
|
+
|
|
86
|
+
## Behaviour notes
|
|
87
|
+
|
|
88
|
+
**Age is a signal, not a verdict.** The `lastPublish` date indicates inactivity, not necessarily abandonment. Regularly maintained packages may have long gaps between releases.
|
|
89
|
+
|
|
90
|
+
**Why `time.modified` is not used.** The npm registry's `time.modified` timestamp changes on metadata edits—deprecation notices, dist-tag moves, owner changes—without a new release. This would hide abandoned packages that changed owners but never shipped again. The `lastPublish` date of `dist-tags.latest` reflects the actual last public version.
|
|
91
|
+
|
|
92
|
+
**Blocked by Node:** `npm outdated` resolves `Latest` with the running Node's `engines` in mind, so a release that needs a newer Node is skipped. `outdated` notices when the registry's latest release differs from what npm settled on, and instead of `ok` it prints `latest needs node <engines.node of that release>` (flag `blocked`, field `latestNode`). It is informational: it is not accepted by `--fail-on`. A package with a deprecated latest release, or one that declares no `engines.node`, is never marked blocked.
|
|
93
|
+
|
|
94
|
+
**Specifiers:** Non-npm specifiers (file:, link:, git:, etc.) and invalid package names are skipped with a reason.
|
|
95
|
+
|
|
96
|
+
**Aliases:** When a dependency uses `npm:` prefix (alias syntax), the lookup queries the real package name.
|
|
97
|
+
|
|
98
|
+
**Ignore:** The `--ignore` flag uses exact-match on the dependency name and omits matching packages entirely from the report.
|
|
99
|
+
|
|
100
|
+
**Timeouts:** A timed-out `npm view` lookup is not killed; the process continues in the background. The timeout only affects the result for that package.
|
|
101
|
+
|
|
102
|
+
**npm stderr:** npm's own error lines may be printed to stderr, inherited from the npm CLI.
|
|
103
|
+
|
|
104
|
+
**Windows behaviour is untested/unverified.**
|
|
105
|
+
|
|
106
|
+
Only the npm CLI is used. Registry, proxy, authentication, and certificate settings are inherited from `.npmrc`.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{EExitCode as e,executeCommand as t}from"@ladamczyk/qoq-utils";import n from"cac";import{basename as r,join as i,resolve as a}from"path";import{readFileSync as o}from"fs";import s from"picocolors";var c=`1.0.0`;const l=[[`prod`,`dependencies`],[`optional`,`optionalDependencies`],[`dev`,`devDependencies`]],u=[`file:`,`link:`,`workspace:`,`git+`,`git:`,`github:`,`http:`,`https:`],d=/^(?:@[A-Za-z0-9~-][A-Za-z0-9._~-]*\/)?[A-Za-z0-9~-][A-Za-z0-9._~-]*$/,f=e=>{try{let t=JSON.parse(o(e,`utf8`));return typeof t==`object`&&t?t:null}catch{return null}},ee=e=>{if(!e.startsWith(`npm:`))return null;let t=e.slice(4),n=t.lastIndexOf(`@`);return n>0?t.slice(0,n):t},p=(e,t,n)=>{for(let[r,i]of l){let a=e[i];if(t.includes(r)&&typeof a==`object`&&a)for(let[e,t]of Object.entries(a))!n.has(e)&&typeof t==`string`&&n.set(e,{type:r,specifier:t})}};function te(e,{include:t,ignore:n}){let a=i(e,`package.json`),o=f(a);if(!o)throw Error(`Cannot read or parse package.json at ${a}`);let s=/* @__PURE__ */ new Map;p(o,t,s);let c=[],l=[];for(let[e,{type:t,specifier:r}]of s){if(n.includes(e))continue;let i=u.find(e=>r.startsWith(e));if(i){l.push({name:e,type:t,specifier:r,reason:`unsupported specifier (${i})`});continue}let a=ee(r);c.push({name:e,type:t,specifier:r,queriedName:a,validName:d.test(a??e)})}return{project:typeof o.name==`string`&&o.name?o.name:r(e),entries:c,skipped:l}}function ne(e,t){let n=f(i(e,`node_modules`,t,`package.json`))?.version;return typeof n==`string`?n:null}const m=e=>typeof e==`object`&&!!e&&!Array.isArray(e),h=e=>typeof e==`string`?e:null,g=e=>JSON.parse(e),re=e=>{let t=Array.isArray(e)?e[0]:e,n=m(t)?t:{};return{current:h(n.current),wanted:h(n.wanted),latest:h(n.latest)}},_=e=>!m(e)||e.code===void 0&&e.summary===void 0?null:[h(e.code),h(e.summary)].filter(Boolean).join(` `),v=e=>{if(e.trim()===``)throw Error(`npm outdated printed empty output`);let t;try{t=g(e)}catch{throw Error(`npm outdated printed invalid JSON`)}if(!m(t))throw Error(`npm outdated printed non-object JSON`);let n=_(t.error);if(n!==null)throw Error(`npm outdated failed: ${n}`);return Object.fromEntries(Object.entries(t).map(([e,t])=>[e,re(t)]))},ie=e=>m(e)&&typeof e.node==`string`&&e.node!==``?e.node:null,y={unknown:`unexpected npm view output`},ae=e=>{let t=e[`dist-tags`],n=m(t)?h(t.latest):null;if(n===null||!m(e.time))return y;let r=h(e.time[n]);if(r===null||Number.isNaN(Date.parse(r)))return{unknown:`no valid publish time for ${n}`};let i=e.deprecated;return{latest:n,lastPublish:r,deprecated:typeof i==`string`&&i!==``?i:null,latestNode:ie(e.engines)}},oe=e=>{let t;try{t=g(e)}catch{return{unknown:`invalid npm view JSON`}}return m(t)?`error`in t?{unknown:_(t.error)??`npm view error`}:ae(t):y};async function se(e,t,n){let r=Array(e.length),i=/* @__PURE__ */ new Set;for(let[a,o]of e.entries()){let e=n(o).then(t=>{r[a]=t,i.delete(e)});i.add(e),i.size===t&&await Promise.race(i)}return await Promise.all(i),r}const ce=String.raw`'\''`,le=e=>process.platform===`win32`?`"${e.replaceAll(`"`,`""`)}"`:`'${e.replaceAll(`'`,ce)}'`,ue=()=>async e=>{if(e.some(e=>/[\0\n\r]/.test(e)))throw Error(`npm argument contains a control character`);let n=await t([`npm`,...e.map(le)].join(` `),[],[`ignore`,`pipe`,`pipe`],!0);return String(n)};function b(e,t){let n=e.getUTCFullYear()*12+e.getUTCMonth()+t,r=Math.floor(n/12),i=n-r*12,a=new Date(Date.UTC(r,i+1,0)).getUTCDate(),o=new Date(e);return o.setUTCFullYear(r,i,Math.min(e.getUTCDate(),a)),o}function de(e,t){let n=new Date(e);if(n>t)return`0d ago`;let r=t.getTime()-n.getTime(),i=Math.floor(r/864e5);if(i<30)return`${i}d ago`;let a=0;for(;b(n,a+1)<=t;)a++;return a<12?`${a}m ago`:`${Math.floor(a/12)}y ago`}const fe=[`prod`,`dev`,`optional`],x=e=>{let t=/^v?(\d+)/.exec(e??``);return t?Number(t[1]):null},S=(e,t)=>{let n=x(e),r=x(t);return n!==null&&r!==null&&r>n},C=async(e,t)=>{let n,r=new Promise((e,r)=>{n=setTimeout(()=>{r(/* @__PURE__ */ Error(`npm view timed out after ${t}ms`))},t)});try{return await Promise.race([e,r])}finally{clearTimeout(n)}},w=async(e,t)=>{if(!e.validName)return{unknown:`invalid package name`};let n=[`view`,e.queriedName??e.name,`time`,`dist-tags`,`deprecated`,`engines`,`--json`,`--prefix`,t.cwd];try{return oe(await C(t.runner(n),t.timeoutMs))}catch(e){return{unknown:e instanceof Error?e.message:String(e)}}},T=e=>e.queriedName===null?e.name:`${e.name}:${e.specifier.replace(/^npm:/,``)}`,E=(e,t,n,r)=>{let i=n?n.latest:r;return t!==null&&t.latestNode!==null&&t.deprecated===null&&e!==null&&i!==null&&i!==e},D=(e,t)=>{let n=[];return e.deprecated!==null&&n.push(`deprecated`),e.stale&&n.push(`stale`),e.outdated&&n.push(`outdated`),e.unknown!==null&&n.push(`unknown`),t&&n.push(`blocked`),n.length>0?n:[`ok`]},O=(e,t,n,r)=>{let i=`latest`in t?t:null,a=n?.current??ne(r.cwd,e.name),o=i?.latest??n?.latest??null,s=i?new Date(i.lastPublish):null,c=n!==void 0,l={name:e.name,queriedName:e.queriedName,type:e.type,specifier:e.specifier,current:a,wanted:n?.wanted??null,latest:o,outdated:c,majorBump:c&&S(a,o),deprecated:i?.deprecated??null,latestNode:i?.latestNode??null,lastPublish:s?s.toISOString():null,stale:s!==null&&s<b(r.now,-r.staleAfterMonths),unknown:`unknown`in t?t.unknown:null};return{...l,flags:D(l,E(o,i,n,a))}},k=(e,t)=>{let n=t=>e.filter(e=>e.flags.includes(t)).length;return{total:e.length,deprecated:n(`deprecated`),stale:n(`stale`),outdated:n(`outdated`),unknown:n(`unknown`),blocked:n(`blocked`),ok:n(`ok`),skipped:t}},A=async(e={})=>{let t=a(e.cwd??process.cwd()),n=e.staleAfterMonths??6,r=e.now??/* @__PURE__ */ new Date,i=e.runner??ue(),{project:o,entries:s,skipped:c}=te(t,{include:e.include??fe,ignore:e.ignore??[]}),l=v(await i([`outdated`,`--json`,`--prefix`,t])),u={cwd:t,runner:i,timeoutMs:e.timeoutMs??3e4,now:r,staleAfterMonths:n,outdated:l},d=await se(s,e.concurrency??6,async e=>O(e,await w(e,u),l[T(e)],u));return{schemaVersion:1,generatedAt:r.toISOString(),staleAfterMonths:n,project:o,packages:d,summary:k(d,c.length),skipped:c.sort((e,t)=>e.name<t.name?-1:Number(e.name>t.name))}},j=[`Package`,`Current`,`Wanted`,`Latest`,`Location`,`Depended by`,`Last publish`,`Flags`],M=/* @__PURE__ */ new Set([1,2,3]),N=[[`prod`,`dependencies`],[`dev`,`devDependencies`],[`optional`,`optionalDependencies`]],P=e=>`${JSON.stringify(e,null,2)}\n`,F=e=>e.length>60?`${e.slice(0,59)}…`:e,I=(e,t)=>t===`deprecated`?`deprecated ${F(e.deprecated??``)}`:t===`blocked`?`latest needs node ${e.latestNode??``}`:t===`unknown`?`unknown ${e.unknown??``}`:t,L=(e,t)=>e.lastPublish===null?`-`:`${e.lastPublish.slice(0,10)} (${de(e.lastPublish,t)})`,R=(e,t,n)=>[e.name,e.current??`-`,e.wanted??e.current??`-`,e.latest??`-`,`node_modules/${e.name}`,t,L(e,n)],z=e=>({deprecated:e.red,stale:e.yellow,outdated:e.cyan,unknown:e.magenta,blocked:e.blue,ok:e.green}),B=(e,t)=>{let n=z(t);return e.flags.map(t=>n[t](I(e,t))).join(` `)},V=(e,t)=>`\u001b]8;;${e}\u001b\\${t}\u001b]8;;\u001b\\`,H=e=>`https://www.npmjs.com/package/${e.queriedName??e.name}`,U=e=>e,W=(e,t)=>e.outdated?e.current===e.wanted?t.yellow:t.red:U,pe=(e,t)=>e.majorBump?t.red:e.outdated?t.magenta:U,me=(e,t,n)=>[n?n=>V(H(e),W(e,t)(n)):W(e,t),U,e.outdated?t.green:U,pe(e,t),t.dim,t.dim,U],G=(e,t,n,r)=>{let i=` `.repeat(Math.max(0,t-e.length));return M.has(n)?i+r(e):r(e)+i},he=(e,t,n,r,i)=>[...me(e,r,i).map((e,r)=>G(t[r]??``,n[r]??0,r,e)),B(e,r)].join(` `).trimEnd(),ge=(e,t,n,r,i)=>{let a=e.map(e=>({pkg:e,cells:R(e,t,n)})),o=j.map((e,t)=>Math.max(e.length,...a.map(e=>e.cells[t]?.length??0))),s=j.map((e,t)=>t===j.length-1?e:G(e,o[t]??0,t,U)),c=[r.underline(r.bold(s.join(` `)))];for(let[e,t]of N){let n=a.filter(t=>t.pkg.type===e);n.length>0&&(c.push(``,r.bold(`${t} (${n.length})`)),c.push(...n.map(e=>he(e.pkg,e.cells,o,r,i))))}return c},_e=(e,t)=>{let n=e.summary;return[`${n.total} packages`,t.red(`${n.deprecated} deprecated`),t.yellow(`${n.stale} stale`),t.cyan(`${n.outdated} outdated`),t.magenta(`${n.unknown} unknown`),t.blue(`${n.blocked} blocked`),t.green(`${n.ok} ok`)].join(` · `)},ve=(e,t)=>{let n=t.color??s.isColorSupported,r=s.createColors(n),i=t.onlyProblems?e.packages.filter(e=>e.flags.length!==1||e.flags[0]!==`ok`):e.packages,a=e.packages.length===0?[`No dependencies found`]:ge(i,e.project,t.now,r,n);if(a.push(``,_e(e,r)),e.skipped.length>0){a.push(``,r.bold(`Skipped:`));let t=Math.max(...e.skipped.map(e=>e.name.length));for(let n of e.skipped)a.push(` ${n.name.padEnd(t)} ${n.specifier} — ${n.reason}`)}return`${a.join(`
|
|
3
|
+
`)}\n`},K=[`prod`,`dev`,`optional`],ye=[`stale`,`deprecated`,`outdated`,`unknown`],be=/* @__PURE__ */ new Set([`--`,`cwd`,`staleAfter`,`stale-after`,`include`,`ignore`,`concurrency`,`json`,`onlyProblems`,`only-problems`,`failOn`,`fail-on`,`h`,`v`,`help`,`version`]),q=e=>Array.isArray(e)?e.map(q).join(`,`):typeof e==`string`||typeof e==`number`||typeof e==`boolean`?String(e):JSON.stringify(e)??`undefined`,J=(e,t,n)=>e[t]??e[n],Y=(e,t,n)=>{if(e===void 0)return n;let r=q(e);if(!/^[1-9]\d*$/.test(r))throw Error(`${t} must be a positive integer, got "${r}"`);return Number(r)},X=(e,t)=>{let n=q(e),r=n.split(`,`);if(typeof e==`boolean`||r.includes(``))throw Error(`${t} must be a comma-separated list without empty entries, got "${n}"`);return r},Z=(e,t,n,r)=>{if(e===void 0)return r;let i=X(e,t),a=i.find(e=>!n.includes(e));if(a!==void 0)throw Error(`${t} has invalid value "${a}"; allowed: ${n.join(`, `)}`);return i},Q=(e,t)=>{if(e===void 0)return!1;if(typeof e!=`boolean`)throw Error(`${t} is a switch and takes no value, got "${q(e)}"`);return e},xe=e=>{if(e!==void 0){if(typeof e!=`string`&&typeof e!=`number`)throw Error(`--cwd must be a single path, got "${q(e)}"`);return{cwd:String(e)}}},Se=e=>{let t=Object.keys(e).find(e=>!be.has(e));if(t!==void 0)throw Error(`Unknown option: ${t}`);let{ignore:n}=e;return{...xe(e.cwd),staleAfterMonths:Y(J(e,`staleAfter`,`stale-after`),`--stale-after`,6),include:Z(e.include,`--include`,K,K),ignore:n===void 0?[]:X(n,`--ignore`),concurrency:Y(e.concurrency,`--concurrency`,6),json:Q(e.json,`--json`),onlyProblems:Q(J(e,`onlyProblems`,`only-problems`),`--only-problems`),failOn:Z(J(e,`failOn`,`fail-on`),`--fail-on`,ye,[])}},Ce=(t,n)=>n.some(e=>t.summary[e]>0)?e.ERROR:e.OK;var $=class extends Error{};const we=()=>n(`outdated`).usage(`[options]`).help(e=>{throw new $(e.map(e=>e.title?`${e.title}:\n${e.body}`:e.body).join(`
|
|
4
|
+
|
|
5
|
+
`))}).option(`-v, --version`,`Display version number`).option(`--cwd <dir>`,`Project folder (default: current directory)`).option(`--stale-after <months>`,`Months without a release before a package is stale (default: 6)`).option(`--include <types>`,`Dependency types to check: prod,dev,optional (default: all)`).option(`--ignore <pkgs>`,`Comma-separated package names to skip`).option(`--concurrency <n>`,`Parallel npm view lookups (default: 6)`).option(`--json`,`Output machine-readable JSON`).option(`--only-problems`,`Hide ok packages from the table`).option(`--fail-on <list>`,`Exit 1 when any of stale,deprecated,outdated,unknown is found`),Te=async(t,n,r)=>{let{options:i}=we().parse([`node`,`outdated`,...t],{run:!1});if(i.version)return{stdout:`${c}\n`,code:e.OK};let a=Se(i),o=await A({cwd:a.cwd,staleAfterMonths:a.staleAfterMonths,include:a.include,ignore:a.ignore,concurrency:a.concurrency,runner:n,now:r});return{stdout:a.json?P(o):ve(o,{onlyProblems:a.onlyProblems,now:r}),code:Ce(o,a.failOn)}},{stdout:Ee,stderr:De,code:Oe}=await(async(t,n={})=>{try{return{...await Te(t,n.runner,n.now??/* @__PURE__ */ new Date),stderr:``}}catch(t){return t instanceof $?{stdout:`${t.message}\n`,stderr:``,code:e.OK}:{stdout:``,stderr:`✖ ${t instanceof Error?t.message:String(t)}\n`,code:e.EXCEPTION}}})(process.argv.slice(2));process.stderr.write(De),process.stdout.write(Ee,()=>process.exit(Oe));
|
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let path = require("path");
|
|
3
|
+
let fs = require("fs");
|
|
4
|
+
let _ladamczyk_qoq_utils = require("@ladamczyk/qoq-utils");
|
|
5
|
+
//#region src/deps.ts
|
|
6
|
+
const FIELDS = [
|
|
7
|
+
["prod", "dependencies"],
|
|
8
|
+
["optional", "optionalDependencies"],
|
|
9
|
+
["dev", "devDependencies"]
|
|
10
|
+
];
|
|
11
|
+
const UNSUPPORTED = [
|
|
12
|
+
"file:",
|
|
13
|
+
"link:",
|
|
14
|
+
"workspace:",
|
|
15
|
+
"git+",
|
|
16
|
+
"git:",
|
|
17
|
+
"github:",
|
|
18
|
+
"http:",
|
|
19
|
+
"https:"
|
|
20
|
+
];
|
|
21
|
+
const NAME_RE = /^(?:@[A-Za-z0-9~-][A-Za-z0-9._~-]*\/)?[A-Za-z0-9~-][A-Za-z0-9._~-]*$/;
|
|
22
|
+
const readJson = (file) => {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse((0, fs.readFileSync)(file, "utf8"));
|
|
25
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const aliasTarget = (specifier) => {
|
|
31
|
+
if (!specifier.startsWith("npm:")) return null;
|
|
32
|
+
const rest = specifier.slice(4);
|
|
33
|
+
const at = rest.lastIndexOf("@");
|
|
34
|
+
return at > 0 ? rest.slice(0, at) : rest;
|
|
35
|
+
};
|
|
36
|
+
const collect = (pkg, include, seen) => {
|
|
37
|
+
for (const [type, field] of FIELDS) {
|
|
38
|
+
const deps = pkg[field];
|
|
39
|
+
if (!include.includes(type) || typeof deps !== "object" || deps === null) continue;
|
|
40
|
+
for (const [name, specifier] of Object.entries(deps)) if (!seen.has(name) && typeof specifier === "string") seen.set(name, {
|
|
41
|
+
type,
|
|
42
|
+
specifier
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function readDeps(cwd, { include, ignore }) {
|
|
47
|
+
const file = (0, path.join)(cwd, "package.json");
|
|
48
|
+
const pkg = readJson(file);
|
|
49
|
+
if (!pkg) throw new Error(`Cannot read or parse package.json at ${file}`);
|
|
50
|
+
const seen = /* @__PURE__ */ new Map();
|
|
51
|
+
collect(pkg, include, seen);
|
|
52
|
+
const entries = [];
|
|
53
|
+
const skipped = [];
|
|
54
|
+
for (const [name, { type, specifier }] of seen) {
|
|
55
|
+
if (ignore.includes(name)) continue;
|
|
56
|
+
const prefix = UNSUPPORTED.find((p) => specifier.startsWith(p));
|
|
57
|
+
if (prefix) {
|
|
58
|
+
skipped.push({
|
|
59
|
+
name,
|
|
60
|
+
type,
|
|
61
|
+
specifier,
|
|
62
|
+
reason: `unsupported specifier (${prefix})`
|
|
63
|
+
});
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const queriedName = aliasTarget(specifier);
|
|
67
|
+
entries.push({
|
|
68
|
+
name,
|
|
69
|
+
type,
|
|
70
|
+
specifier,
|
|
71
|
+
queriedName,
|
|
72
|
+
validName: NAME_RE.test(queriedName ?? name)
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
project: typeof pkg["name"] === "string" && pkg["name"] ? pkg["name"] : (0, path.basename)(cwd),
|
|
77
|
+
entries,
|
|
78
|
+
skipped
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function readInstalledVersion(cwd, name) {
|
|
82
|
+
const version = readJson((0, path.join)(cwd, "node_modules", name, "package.json"))?.["version"];
|
|
83
|
+
return typeof version === "string" ? version : null;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/npm.ts
|
|
87
|
+
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
88
|
+
const str = (v) => typeof v === "string" ? v : null;
|
|
89
|
+
const parseJson = (stdout) => JSON.parse(stdout);
|
|
90
|
+
const toEntry = (value) => {
|
|
91
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
92
|
+
const e = isRecord(raw) ? raw : {};
|
|
93
|
+
return {
|
|
94
|
+
current: str(e.current),
|
|
95
|
+
wanted: str(e.wanted),
|
|
96
|
+
latest: str(e.latest)
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
const errorText = (err) => {
|
|
100
|
+
if (!isRecord(err) || err.code === void 0 && err.summary === void 0) return null;
|
|
101
|
+
return [str(err.code), str(err.summary)].filter(Boolean).join(" ");
|
|
102
|
+
};
|
|
103
|
+
const parseOutdated = (stdout) => {
|
|
104
|
+
if (stdout.trim() === "") throw new Error("npm outdated printed empty output");
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = parseJson(stdout);
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error("npm outdated printed invalid JSON");
|
|
110
|
+
}
|
|
111
|
+
if (!isRecord(parsed)) throw new Error("npm outdated printed non-object JSON");
|
|
112
|
+
const failure = errorText(parsed.error);
|
|
113
|
+
if (failure !== null) throw new Error(`npm outdated failed: ${failure}`);
|
|
114
|
+
return Object.fromEntries(Object.entries(parsed).map(([name, v]) => [name, toEntry(v)]));
|
|
115
|
+
};
|
|
116
|
+
const nodeRange = (engines) => isRecord(engines) && typeof engines.node === "string" && engines.node !== "" ? engines.node : null;
|
|
117
|
+
const UNEXPECTED = { unknown: "unexpected npm view output" };
|
|
118
|
+
const buildView = (parsed) => {
|
|
119
|
+
const tags = parsed["dist-tags"];
|
|
120
|
+
const latest = isRecord(tags) ? str(tags.latest) : null;
|
|
121
|
+
if (latest === null || !isRecord(parsed.time)) return UNEXPECTED;
|
|
122
|
+
const lastPublish = str(parsed.time[latest]);
|
|
123
|
+
if (lastPublish === null || Number.isNaN(Date.parse(lastPublish))) return { unknown: `no valid publish time for ${latest}` };
|
|
124
|
+
const dep = parsed.deprecated;
|
|
125
|
+
return {
|
|
126
|
+
latest,
|
|
127
|
+
lastPublish,
|
|
128
|
+
deprecated: typeof dep === "string" && dep !== "" ? dep : null,
|
|
129
|
+
latestNode: nodeRange(parsed.engines)
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
const parseView = (stdout) => {
|
|
133
|
+
let parsed;
|
|
134
|
+
try {
|
|
135
|
+
parsed = parseJson(stdout);
|
|
136
|
+
} catch {
|
|
137
|
+
return { unknown: "invalid npm view JSON" };
|
|
138
|
+
}
|
|
139
|
+
if (!isRecord(parsed)) return UNEXPECTED;
|
|
140
|
+
if ("error" in parsed) return { unknown: errorText(parsed.error) ?? "npm view error" };
|
|
141
|
+
return buildView(parsed);
|
|
142
|
+
};
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/pool.ts
|
|
145
|
+
async function mapPool(items, limit, fn) {
|
|
146
|
+
const results = new Array(items.length);
|
|
147
|
+
const executing = /* @__PURE__ */ new Set();
|
|
148
|
+
for (const [i, item] of items.entries()) {
|
|
149
|
+
const promise = fn(item).then((res) => {
|
|
150
|
+
results[i] = res;
|
|
151
|
+
executing.delete(promise);
|
|
152
|
+
});
|
|
153
|
+
executing.add(promise);
|
|
154
|
+
if (executing.size === limit) await Promise.race(executing);
|
|
155
|
+
}
|
|
156
|
+
await Promise.all(executing);
|
|
157
|
+
return results;
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/runner.ts
|
|
161
|
+
const SHELL_SINGLE_QUOTE = String.raw`'\''`;
|
|
162
|
+
const quote = (arg) => process.platform === "win32" ? `"${arg.replaceAll("\"", "\"\"")}"` : `'${arg.replaceAll("'", SHELL_SINGLE_QUOTE)}'`;
|
|
163
|
+
const createNpmRunner = () => async (args) => {
|
|
164
|
+
if (args.some((arg) => /[\0\n\r]/.test(arg))) throw new Error("npm argument contains a control character");
|
|
165
|
+
const out = await (0, _ladamczyk_qoq_utils.executeCommand)(["npm", ...args.map(quote)].join(" "), [], [
|
|
166
|
+
"ignore",
|
|
167
|
+
"pipe",
|
|
168
|
+
"pipe"
|
|
169
|
+
], true);
|
|
170
|
+
return String(out);
|
|
171
|
+
};
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/time.ts
|
|
174
|
+
function addMonths(date, months) {
|
|
175
|
+
const total = date.getUTCFullYear() * 12 + date.getUTCMonth() + months;
|
|
176
|
+
const year = Math.floor(total / 12);
|
|
177
|
+
const month = total - year * 12;
|
|
178
|
+
const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
|
|
179
|
+
const result = new Date(date);
|
|
180
|
+
result.setUTCFullYear(year, month, Math.min(date.getUTCDate(), lastDay));
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/check.ts
|
|
185
|
+
const ALL_TYPES = [
|
|
186
|
+
"prod",
|
|
187
|
+
"dev",
|
|
188
|
+
"optional"
|
|
189
|
+
];
|
|
190
|
+
const major = (version) => {
|
|
191
|
+
const match = /^v?(\d+)/.exec(version ?? "");
|
|
192
|
+
return match ? Number(match[1]) : null;
|
|
193
|
+
};
|
|
194
|
+
const isMajorBump = (current, latest) => {
|
|
195
|
+
const from = major(current);
|
|
196
|
+
const to = major(latest);
|
|
197
|
+
return from !== null && to !== null && to > from;
|
|
198
|
+
};
|
|
199
|
+
const withTimeout = async (work, ms) => {
|
|
200
|
+
let timer;
|
|
201
|
+
const timeout = new Promise((_, reject) => {
|
|
202
|
+
timer = setTimeout(() => {
|
|
203
|
+
reject(/* @__PURE__ */ new Error(`npm view timed out after ${ms}ms`));
|
|
204
|
+
}, ms);
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
return await Promise.race([work, timeout]);
|
|
208
|
+
} finally {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const lookup = async (entry, ctx) => {
|
|
213
|
+
if (!entry.validName) return { unknown: "invalid package name" };
|
|
214
|
+
const args = [
|
|
215
|
+
"view",
|
|
216
|
+
entry.queriedName ?? entry.name,
|
|
217
|
+
"time",
|
|
218
|
+
"dist-tags",
|
|
219
|
+
"deprecated",
|
|
220
|
+
"engines",
|
|
221
|
+
"--json",
|
|
222
|
+
"--prefix",
|
|
223
|
+
ctx.cwd
|
|
224
|
+
];
|
|
225
|
+
try {
|
|
226
|
+
return parseView(await withTimeout(ctx.runner(args), ctx.timeoutMs));
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return { unknown: error instanceof Error ? error.message : String(error) };
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const outdatedKey = (entry) => entry.queriedName === null ? entry.name : `${entry.name}:${entry.specifier.replace(/^npm:/, "")}`;
|
|
232
|
+
const isBlocked = (latest, viewed, hit, current) => {
|
|
233
|
+
const npmLatest = hit ? hit.latest : current;
|
|
234
|
+
return viewed !== null && viewed.latestNode !== null && viewed.deprecated === null && latest !== null && npmLatest !== null && npmLatest !== latest;
|
|
235
|
+
};
|
|
236
|
+
const flagsFor = (row, blocked) => {
|
|
237
|
+
const flags = [];
|
|
238
|
+
if (row.deprecated !== null) flags.push("deprecated");
|
|
239
|
+
if (row.stale) flags.push("stale");
|
|
240
|
+
if (row.outdated) flags.push("outdated");
|
|
241
|
+
if (row.unknown !== null) flags.push("unknown");
|
|
242
|
+
if (blocked) flags.push("blocked");
|
|
243
|
+
return flags.length > 0 ? flags : ["ok"];
|
|
244
|
+
};
|
|
245
|
+
const buildRow = (entry, view, hit, ctx) => {
|
|
246
|
+
const viewed = "latest" in view ? view : null;
|
|
247
|
+
const current = hit?.current ?? readInstalledVersion(ctx.cwd, entry.name);
|
|
248
|
+
const latest = viewed?.latest ?? hit?.latest ?? null;
|
|
249
|
+
const published = viewed ? new Date(viewed.lastPublish) : null;
|
|
250
|
+
const outdated = hit !== void 0;
|
|
251
|
+
const partial = {
|
|
252
|
+
name: entry.name,
|
|
253
|
+
queriedName: entry.queriedName,
|
|
254
|
+
type: entry.type,
|
|
255
|
+
specifier: entry.specifier,
|
|
256
|
+
current,
|
|
257
|
+
wanted: hit?.wanted ?? null,
|
|
258
|
+
latest,
|
|
259
|
+
outdated,
|
|
260
|
+
majorBump: outdated && isMajorBump(current, latest),
|
|
261
|
+
deprecated: viewed?.deprecated ?? null,
|
|
262
|
+
latestNode: viewed?.latestNode ?? null,
|
|
263
|
+
lastPublish: published ? published.toISOString() : null,
|
|
264
|
+
stale: published !== null && published < addMonths(ctx.now, -ctx.staleAfterMonths),
|
|
265
|
+
unknown: "unknown" in view ? view.unknown : null
|
|
266
|
+
};
|
|
267
|
+
return {
|
|
268
|
+
...partial,
|
|
269
|
+
flags: flagsFor(partial, isBlocked(latest, viewed, hit, current))
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
const summarise = (packages, skipped) => {
|
|
273
|
+
const count = (flag) => packages.filter((p) => p.flags.includes(flag)).length;
|
|
274
|
+
return {
|
|
275
|
+
total: packages.length,
|
|
276
|
+
deprecated: count("deprecated"),
|
|
277
|
+
stale: count("stale"),
|
|
278
|
+
outdated: count("outdated"),
|
|
279
|
+
unknown: count("unknown"),
|
|
280
|
+
blocked: count("blocked"),
|
|
281
|
+
ok: count("ok"),
|
|
282
|
+
skipped
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
const check = async (options = {}) => {
|
|
286
|
+
const cwd = (0, path.resolve)(options.cwd ?? process.cwd());
|
|
287
|
+
const staleAfterMonths = options.staleAfterMonths ?? 6;
|
|
288
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
289
|
+
const runner = options.runner ?? createNpmRunner();
|
|
290
|
+
const { project, entries, skipped } = readDeps(cwd, {
|
|
291
|
+
include: options.include ?? ALL_TYPES,
|
|
292
|
+
ignore: options.ignore ?? []
|
|
293
|
+
});
|
|
294
|
+
const outdated = parseOutdated(await runner([
|
|
295
|
+
"outdated",
|
|
296
|
+
"--json",
|
|
297
|
+
"--prefix",
|
|
298
|
+
cwd
|
|
299
|
+
]));
|
|
300
|
+
const ctx = {
|
|
301
|
+
cwd,
|
|
302
|
+
runner,
|
|
303
|
+
timeoutMs: options.timeoutMs ?? 3e4,
|
|
304
|
+
now,
|
|
305
|
+
staleAfterMonths,
|
|
306
|
+
outdated
|
|
307
|
+
};
|
|
308
|
+
const packages = await mapPool(entries, options.concurrency ?? 6, async (entry) => buildRow(entry, await lookup(entry, ctx), outdated[outdatedKey(entry)], ctx));
|
|
309
|
+
return {
|
|
310
|
+
schemaVersion: 1,
|
|
311
|
+
generatedAt: now.toISOString(),
|
|
312
|
+
staleAfterMonths,
|
|
313
|
+
project,
|
|
314
|
+
packages,
|
|
315
|
+
summary: summarise(packages, skipped.length),
|
|
316
|
+
skipped: skipped.sort((a, b) => a.name < b.name ? -1 : Number(a.name > b.name))
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
//#endregion
|
|
320
|
+
exports.check = check;
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { basename, join, resolve } from "path";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { executeCommand } from "@ladamczyk/qoq-utils";
|
|
4
|
+
//#region src/deps.ts
|
|
5
|
+
const FIELDS = [
|
|
6
|
+
["prod", "dependencies"],
|
|
7
|
+
["optional", "optionalDependencies"],
|
|
8
|
+
["dev", "devDependencies"]
|
|
9
|
+
];
|
|
10
|
+
const UNSUPPORTED = [
|
|
11
|
+
"file:",
|
|
12
|
+
"link:",
|
|
13
|
+
"workspace:",
|
|
14
|
+
"git+",
|
|
15
|
+
"git:",
|
|
16
|
+
"github:",
|
|
17
|
+
"http:",
|
|
18
|
+
"https:"
|
|
19
|
+
];
|
|
20
|
+
const NAME_RE = /^(?:@[A-Za-z0-9~-][A-Za-z0-9._~-]*\/)?[A-Za-z0-9~-][A-Za-z0-9._~-]*$/;
|
|
21
|
+
const readJson = (file) => {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
24
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const aliasTarget = (specifier) => {
|
|
30
|
+
if (!specifier.startsWith("npm:")) return null;
|
|
31
|
+
const rest = specifier.slice(4);
|
|
32
|
+
const at = rest.lastIndexOf("@");
|
|
33
|
+
return at > 0 ? rest.slice(0, at) : rest;
|
|
34
|
+
};
|
|
35
|
+
const collect = (pkg, include, seen) => {
|
|
36
|
+
for (const [type, field] of FIELDS) {
|
|
37
|
+
const deps = pkg[field];
|
|
38
|
+
if (!include.includes(type) || typeof deps !== "object" || deps === null) continue;
|
|
39
|
+
for (const [name, specifier] of Object.entries(deps)) if (!seen.has(name) && typeof specifier === "string") seen.set(name, {
|
|
40
|
+
type,
|
|
41
|
+
specifier
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function readDeps(cwd, { include, ignore }) {
|
|
46
|
+
const file = join(cwd, "package.json");
|
|
47
|
+
const pkg = readJson(file);
|
|
48
|
+
if (!pkg) throw new Error(`Cannot read or parse package.json at ${file}`);
|
|
49
|
+
const seen = /* @__PURE__ */ new Map();
|
|
50
|
+
collect(pkg, include, seen);
|
|
51
|
+
const entries = [];
|
|
52
|
+
const skipped = [];
|
|
53
|
+
for (const [name, { type, specifier }] of seen) {
|
|
54
|
+
if (ignore.includes(name)) continue;
|
|
55
|
+
const prefix = UNSUPPORTED.find((p) => specifier.startsWith(p));
|
|
56
|
+
if (prefix) {
|
|
57
|
+
skipped.push({
|
|
58
|
+
name,
|
|
59
|
+
type,
|
|
60
|
+
specifier,
|
|
61
|
+
reason: `unsupported specifier (${prefix})`
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const queriedName = aliasTarget(specifier);
|
|
66
|
+
entries.push({
|
|
67
|
+
name,
|
|
68
|
+
type,
|
|
69
|
+
specifier,
|
|
70
|
+
queriedName,
|
|
71
|
+
validName: NAME_RE.test(queriedName ?? name)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
project: typeof pkg["name"] === "string" && pkg["name"] ? pkg["name"] : basename(cwd),
|
|
76
|
+
entries,
|
|
77
|
+
skipped
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function readInstalledVersion(cwd, name) {
|
|
81
|
+
const version = readJson(join(cwd, "node_modules", name, "package.json"))?.["version"];
|
|
82
|
+
return typeof version === "string" ? version : null;
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/npm.ts
|
|
86
|
+
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
87
|
+
const str = (v) => typeof v === "string" ? v : null;
|
|
88
|
+
const parseJson = (stdout) => JSON.parse(stdout);
|
|
89
|
+
const toEntry = (value) => {
|
|
90
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
91
|
+
const e = isRecord(raw) ? raw : {};
|
|
92
|
+
return {
|
|
93
|
+
current: str(e.current),
|
|
94
|
+
wanted: str(e.wanted),
|
|
95
|
+
latest: str(e.latest)
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
const errorText = (err) => {
|
|
99
|
+
if (!isRecord(err) || err.code === void 0 && err.summary === void 0) return null;
|
|
100
|
+
return [str(err.code), str(err.summary)].filter(Boolean).join(" ");
|
|
101
|
+
};
|
|
102
|
+
const parseOutdated = (stdout) => {
|
|
103
|
+
if (stdout.trim() === "") throw new Error("npm outdated printed empty output");
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = parseJson(stdout);
|
|
107
|
+
} catch {
|
|
108
|
+
throw new Error("npm outdated printed invalid JSON");
|
|
109
|
+
}
|
|
110
|
+
if (!isRecord(parsed)) throw new Error("npm outdated printed non-object JSON");
|
|
111
|
+
const failure = errorText(parsed.error);
|
|
112
|
+
if (failure !== null) throw new Error(`npm outdated failed: ${failure}`);
|
|
113
|
+
return Object.fromEntries(Object.entries(parsed).map(([name, v]) => [name, toEntry(v)]));
|
|
114
|
+
};
|
|
115
|
+
const nodeRange = (engines) => isRecord(engines) && typeof engines.node === "string" && engines.node !== "" ? engines.node : null;
|
|
116
|
+
const UNEXPECTED = { unknown: "unexpected npm view output" };
|
|
117
|
+
const buildView = (parsed) => {
|
|
118
|
+
const tags = parsed["dist-tags"];
|
|
119
|
+
const latest = isRecord(tags) ? str(tags.latest) : null;
|
|
120
|
+
if (latest === null || !isRecord(parsed.time)) return UNEXPECTED;
|
|
121
|
+
const lastPublish = str(parsed.time[latest]);
|
|
122
|
+
if (lastPublish === null || Number.isNaN(Date.parse(lastPublish))) return { unknown: `no valid publish time for ${latest}` };
|
|
123
|
+
const dep = parsed.deprecated;
|
|
124
|
+
return {
|
|
125
|
+
latest,
|
|
126
|
+
lastPublish,
|
|
127
|
+
deprecated: typeof dep === "string" && dep !== "" ? dep : null,
|
|
128
|
+
latestNode: nodeRange(parsed.engines)
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
const parseView = (stdout) => {
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = parseJson(stdout);
|
|
135
|
+
} catch {
|
|
136
|
+
return { unknown: "invalid npm view JSON" };
|
|
137
|
+
}
|
|
138
|
+
if (!isRecord(parsed)) return UNEXPECTED;
|
|
139
|
+
if ("error" in parsed) return { unknown: errorText(parsed.error) ?? "npm view error" };
|
|
140
|
+
return buildView(parsed);
|
|
141
|
+
};
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/pool.ts
|
|
144
|
+
async function mapPool(items, limit, fn) {
|
|
145
|
+
const results = new Array(items.length);
|
|
146
|
+
const executing = /* @__PURE__ */ new Set();
|
|
147
|
+
for (const [i, item] of items.entries()) {
|
|
148
|
+
const promise = fn(item).then((res) => {
|
|
149
|
+
results[i] = res;
|
|
150
|
+
executing.delete(promise);
|
|
151
|
+
});
|
|
152
|
+
executing.add(promise);
|
|
153
|
+
if (executing.size === limit) await Promise.race(executing);
|
|
154
|
+
}
|
|
155
|
+
await Promise.all(executing);
|
|
156
|
+
return results;
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/runner.ts
|
|
160
|
+
const SHELL_SINGLE_QUOTE = String.raw`'\''`;
|
|
161
|
+
const quote = (arg) => process.platform === "win32" ? `"${arg.replaceAll("\"", "\"\"")}"` : `'${arg.replaceAll("'", SHELL_SINGLE_QUOTE)}'`;
|
|
162
|
+
const createNpmRunner = () => async (args) => {
|
|
163
|
+
if (args.some((arg) => /[\0\n\r]/.test(arg))) throw new Error("npm argument contains a control character");
|
|
164
|
+
const out = await executeCommand(["npm", ...args.map(quote)].join(" "), [], [
|
|
165
|
+
"ignore",
|
|
166
|
+
"pipe",
|
|
167
|
+
"pipe"
|
|
168
|
+
], true);
|
|
169
|
+
return String(out);
|
|
170
|
+
};
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/time.ts
|
|
173
|
+
function addMonths(date, months) {
|
|
174
|
+
const total = date.getUTCFullYear() * 12 + date.getUTCMonth() + months;
|
|
175
|
+
const year = Math.floor(total / 12);
|
|
176
|
+
const month = total - year * 12;
|
|
177
|
+
const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
|
|
178
|
+
const result = new Date(date);
|
|
179
|
+
result.setUTCFullYear(year, month, Math.min(date.getUTCDate(), lastDay));
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region src/check.ts
|
|
184
|
+
const ALL_TYPES = [
|
|
185
|
+
"prod",
|
|
186
|
+
"dev",
|
|
187
|
+
"optional"
|
|
188
|
+
];
|
|
189
|
+
const major = (version) => {
|
|
190
|
+
const match = /^v?(\d+)/.exec(version ?? "");
|
|
191
|
+
return match ? Number(match[1]) : null;
|
|
192
|
+
};
|
|
193
|
+
const isMajorBump = (current, latest) => {
|
|
194
|
+
const from = major(current);
|
|
195
|
+
const to = major(latest);
|
|
196
|
+
return from !== null && to !== null && to > from;
|
|
197
|
+
};
|
|
198
|
+
const withTimeout = async (work, ms) => {
|
|
199
|
+
let timer;
|
|
200
|
+
const timeout = new Promise((_, reject) => {
|
|
201
|
+
timer = setTimeout(() => {
|
|
202
|
+
reject(/* @__PURE__ */ new Error(`npm view timed out after ${ms}ms`));
|
|
203
|
+
}, ms);
|
|
204
|
+
});
|
|
205
|
+
try {
|
|
206
|
+
return await Promise.race([work, timeout]);
|
|
207
|
+
} finally {
|
|
208
|
+
clearTimeout(timer);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const lookup = async (entry, ctx) => {
|
|
212
|
+
if (!entry.validName) return { unknown: "invalid package name" };
|
|
213
|
+
const args = [
|
|
214
|
+
"view",
|
|
215
|
+
entry.queriedName ?? entry.name,
|
|
216
|
+
"time",
|
|
217
|
+
"dist-tags",
|
|
218
|
+
"deprecated",
|
|
219
|
+
"engines",
|
|
220
|
+
"--json",
|
|
221
|
+
"--prefix",
|
|
222
|
+
ctx.cwd
|
|
223
|
+
];
|
|
224
|
+
try {
|
|
225
|
+
return parseView(await withTimeout(ctx.runner(args), ctx.timeoutMs));
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return { unknown: error instanceof Error ? error.message : String(error) };
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const outdatedKey = (entry) => entry.queriedName === null ? entry.name : `${entry.name}:${entry.specifier.replace(/^npm:/, "")}`;
|
|
231
|
+
const isBlocked = (latest, viewed, hit, current) => {
|
|
232
|
+
const npmLatest = hit ? hit.latest : current;
|
|
233
|
+
return viewed !== null && viewed.latestNode !== null && viewed.deprecated === null && latest !== null && npmLatest !== null && npmLatest !== latest;
|
|
234
|
+
};
|
|
235
|
+
const flagsFor = (row, blocked) => {
|
|
236
|
+
const flags = [];
|
|
237
|
+
if (row.deprecated !== null) flags.push("deprecated");
|
|
238
|
+
if (row.stale) flags.push("stale");
|
|
239
|
+
if (row.outdated) flags.push("outdated");
|
|
240
|
+
if (row.unknown !== null) flags.push("unknown");
|
|
241
|
+
if (blocked) flags.push("blocked");
|
|
242
|
+
return flags.length > 0 ? flags : ["ok"];
|
|
243
|
+
};
|
|
244
|
+
const buildRow = (entry, view, hit, ctx) => {
|
|
245
|
+
const viewed = "latest" in view ? view : null;
|
|
246
|
+
const current = hit?.current ?? readInstalledVersion(ctx.cwd, entry.name);
|
|
247
|
+
const latest = viewed?.latest ?? hit?.latest ?? null;
|
|
248
|
+
const published = viewed ? new Date(viewed.lastPublish) : null;
|
|
249
|
+
const outdated = hit !== void 0;
|
|
250
|
+
const partial = {
|
|
251
|
+
name: entry.name,
|
|
252
|
+
queriedName: entry.queriedName,
|
|
253
|
+
type: entry.type,
|
|
254
|
+
specifier: entry.specifier,
|
|
255
|
+
current,
|
|
256
|
+
wanted: hit?.wanted ?? null,
|
|
257
|
+
latest,
|
|
258
|
+
outdated,
|
|
259
|
+
majorBump: outdated && isMajorBump(current, latest),
|
|
260
|
+
deprecated: viewed?.deprecated ?? null,
|
|
261
|
+
latestNode: viewed?.latestNode ?? null,
|
|
262
|
+
lastPublish: published ? published.toISOString() : null,
|
|
263
|
+
stale: published !== null && published < addMonths(ctx.now, -ctx.staleAfterMonths),
|
|
264
|
+
unknown: "unknown" in view ? view.unknown : null
|
|
265
|
+
};
|
|
266
|
+
return {
|
|
267
|
+
...partial,
|
|
268
|
+
flags: flagsFor(partial, isBlocked(latest, viewed, hit, current))
|
|
269
|
+
};
|
|
270
|
+
};
|
|
271
|
+
const summarise = (packages, skipped) => {
|
|
272
|
+
const count = (flag) => packages.filter((p) => p.flags.includes(flag)).length;
|
|
273
|
+
return {
|
|
274
|
+
total: packages.length,
|
|
275
|
+
deprecated: count("deprecated"),
|
|
276
|
+
stale: count("stale"),
|
|
277
|
+
outdated: count("outdated"),
|
|
278
|
+
unknown: count("unknown"),
|
|
279
|
+
blocked: count("blocked"),
|
|
280
|
+
ok: count("ok"),
|
|
281
|
+
skipped
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
const check = async (options = {}) => {
|
|
285
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
286
|
+
const staleAfterMonths = options.staleAfterMonths ?? 6;
|
|
287
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
288
|
+
const runner = options.runner ?? createNpmRunner();
|
|
289
|
+
const { project, entries, skipped } = readDeps(cwd, {
|
|
290
|
+
include: options.include ?? ALL_TYPES,
|
|
291
|
+
ignore: options.ignore ?? []
|
|
292
|
+
});
|
|
293
|
+
const outdated = parseOutdated(await runner([
|
|
294
|
+
"outdated",
|
|
295
|
+
"--json",
|
|
296
|
+
"--prefix",
|
|
297
|
+
cwd
|
|
298
|
+
]));
|
|
299
|
+
const ctx = {
|
|
300
|
+
cwd,
|
|
301
|
+
runner,
|
|
302
|
+
timeoutMs: options.timeoutMs ?? 3e4,
|
|
303
|
+
now,
|
|
304
|
+
staleAfterMonths,
|
|
305
|
+
outdated
|
|
306
|
+
};
|
|
307
|
+
const packages = await mapPool(entries, options.concurrency ?? 6, async (entry) => buildRow(entry, await lookup(entry, ctx), outdated[outdatedKey(entry)], ctx));
|
|
308
|
+
return {
|
|
309
|
+
schemaVersion: 1,
|
|
310
|
+
generatedAt: now.toISOString(),
|
|
311
|
+
staleAfterMonths,
|
|
312
|
+
project,
|
|
313
|
+
packages,
|
|
314
|
+
summary: summarise(packages, skipped.length),
|
|
315
|
+
skipped: skipped.sort((a, b) => a.name < b.name ? -1 : Number(a.name > b.name))
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
//#endregion
|
|
319
|
+
export { check };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/src/cli.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ISkipped, TDepType } from './types.ts';
|
|
2
|
+
export interface IDepEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
type: TDepType;
|
|
5
|
+
specifier: string;
|
|
6
|
+
queriedName: string | null;
|
|
7
|
+
validName: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function readDeps(cwd: string, { include, ignore }: {
|
|
10
|
+
include: readonly TDepType[];
|
|
11
|
+
ignore: readonly string[];
|
|
12
|
+
}): {
|
|
13
|
+
project: string;
|
|
14
|
+
entries: IDepEntry[];
|
|
15
|
+
skipped: ISkipped[];
|
|
16
|
+
};
|
|
17
|
+
export declare function readInstalledVersion(cwd: string, name: string): string | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ICheckResult } from './types.ts';
|
|
2
|
+
export declare const formatJson: (result: ICheckResult) => string;
|
|
3
|
+
export declare const formatTable: (result: ICheckResult, opts: {
|
|
4
|
+
onlyProblems: boolean;
|
|
5
|
+
now: Date;
|
|
6
|
+
color?: boolean | undefined;
|
|
7
|
+
}) => string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/src/npm.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface IOutdatedEntry {
|
|
2
|
+
current: string | null;
|
|
3
|
+
wanted: string | null;
|
|
4
|
+
latest: string | null;
|
|
5
|
+
}
|
|
6
|
+
export type TViewResult = {
|
|
7
|
+
latest: string;
|
|
8
|
+
lastPublish: string;
|
|
9
|
+
deprecated: string | null;
|
|
10
|
+
latestNode: string | null;
|
|
11
|
+
} | {
|
|
12
|
+
unknown: string;
|
|
13
|
+
};
|
|
14
|
+
export declare const parseOutdated: (stdout: string) => Record<string, IOutdatedEntry>;
|
|
15
|
+
export declare const parseView: (stdout: string) => TViewResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EExitCode } from '@ladamczyk/qoq-utils';
|
|
2
|
+
import type { ICheckResult, TDepType, TProblem } from './types.ts';
|
|
3
|
+
export interface ICliOptions {
|
|
4
|
+
cwd?: string | undefined;
|
|
5
|
+
staleAfterMonths: number;
|
|
6
|
+
include: readonly TDepType[];
|
|
7
|
+
ignore: readonly string[];
|
|
8
|
+
concurrency: number;
|
|
9
|
+
json: boolean;
|
|
10
|
+
onlyProblems: boolean;
|
|
11
|
+
failOn: readonly TProblem[];
|
|
12
|
+
}
|
|
13
|
+
export declare const parseCliOptions: (raw: Record<string, unknown>) => ICliOptions;
|
|
14
|
+
export declare const exitCodeFor: (result: ICheckResult, failOn: readonly TProblem[]) => EExitCode;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function mapPool<T, R>(items: readonly T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/src/run.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EExitCode } from '@ladamczyk/qoq-utils';
|
|
2
|
+
import type { TRunner } from './types.ts';
|
|
3
|
+
export interface IRunResult {
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
code: EExitCode;
|
|
7
|
+
}
|
|
8
|
+
export declare const run: (argv: string[], io?: {
|
|
9
|
+
runner?: TRunner;
|
|
10
|
+
now?: Date;
|
|
11
|
+
}) => Promise<IRunResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type TDepType = 'prod' | 'dev' | 'optional';
|
|
2
|
+
export type TProblem = 'deprecated' | 'stale' | 'outdated' | 'unknown';
|
|
3
|
+
export type TFlag = TProblem | 'blocked' | 'ok';
|
|
4
|
+
export type TRunner = (args: readonly string[]) => Promise<string>;
|
|
5
|
+
export interface ICheckOptions {
|
|
6
|
+
cwd?: string | undefined;
|
|
7
|
+
staleAfterMonths?: number | undefined;
|
|
8
|
+
include?: readonly TDepType[] | undefined;
|
|
9
|
+
ignore?: readonly string[] | undefined;
|
|
10
|
+
concurrency?: number | undefined;
|
|
11
|
+
timeoutMs?: number | undefined;
|
|
12
|
+
runner?: TRunner | undefined;
|
|
13
|
+
now?: Date | undefined;
|
|
14
|
+
}
|
|
15
|
+
export interface IPackageResult {
|
|
16
|
+
name: string;
|
|
17
|
+
queriedName: string | null;
|
|
18
|
+
type: TDepType;
|
|
19
|
+
specifier: string;
|
|
20
|
+
current: string | null;
|
|
21
|
+
wanted: string | null;
|
|
22
|
+
latest: string | null;
|
|
23
|
+
outdated: boolean;
|
|
24
|
+
majorBump: boolean;
|
|
25
|
+
deprecated: string | null;
|
|
26
|
+
latestNode: string | null;
|
|
27
|
+
lastPublish: string | null;
|
|
28
|
+
stale: boolean;
|
|
29
|
+
unknown: string | null;
|
|
30
|
+
flags: TFlag[];
|
|
31
|
+
}
|
|
32
|
+
export interface ISkipped {
|
|
33
|
+
name: string;
|
|
34
|
+
type: TDepType;
|
|
35
|
+
specifier: string;
|
|
36
|
+
reason: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ISummary {
|
|
39
|
+
total: number;
|
|
40
|
+
deprecated: number;
|
|
41
|
+
stale: number;
|
|
42
|
+
outdated: number;
|
|
43
|
+
unknown: number;
|
|
44
|
+
blocked: number;
|
|
45
|
+
ok: number;
|
|
46
|
+
skipped: number;
|
|
47
|
+
}
|
|
48
|
+
export interface ICheckResult {
|
|
49
|
+
schemaVersion: 1;
|
|
50
|
+
generatedAt: string;
|
|
51
|
+
staleAfterMonths: number;
|
|
52
|
+
project: string;
|
|
53
|
+
packages: IPackageResult[];
|
|
54
|
+
summary: ISummary;
|
|
55
|
+
skipped: ISkipped[];
|
|
56
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ladamczyk/outdated",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI that reports outdated dependencies",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"outdated",
|
|
8
|
+
"dependencies",
|
|
9
|
+
"npm"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/ladamczyk-it/outdated",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ladamczyk-it/outdated/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+ssh://git@github.com/ladamczyk-it/outdated.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./lib/src/index.d.ts",
|
|
24
|
+
"import": "./lib/index.mjs",
|
|
25
|
+
"require": "./lib/index.cjs"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"main": "./lib/index.cjs",
|
|
30
|
+
"module": "./lib/index.mjs",
|
|
31
|
+
"types": "./lib/src/index.d.ts",
|
|
32
|
+
"bin": {
|
|
33
|
+
"outdated": "bin/cli.js"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"bin",
|
|
37
|
+
"lib"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "rimraf ./bin ./lib && rolldown -c && tsc --emitDeclarationOnly && chmod +x ./bin/cli.js",
|
|
41
|
+
"dev": "npm run build && node ./bin/cli.js",
|
|
42
|
+
"husky:pre-push": "run-s qoq:check test build",
|
|
43
|
+
"prepare": "husky",
|
|
44
|
+
"publish:full": "run-s husky:pre-push publish:release",
|
|
45
|
+
"publish:release": "npx --package semantic-release --package @semantic-release/changelog --package @semantic-release/git -y semantic-release",
|
|
46
|
+
"qoq:check": "qoq --check",
|
|
47
|
+
"qoq:fix": "qoq --fix",
|
|
48
|
+
"sort:packages:json": "npx -y sort-package-json 'package.json'",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:coverage": "npm run test -- --coverage"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@ladamczyk/qoq-utils": "6.1.10",
|
|
54
|
+
"cac": "7.0.0",
|
|
55
|
+
"picocolors": "1.1.1"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@commitlint/cli": "21.2.3",
|
|
59
|
+
"@commitlint/config-conventional": "21.2.3",
|
|
60
|
+
"@ladamczyk/qoq-cli": "6.1.10",
|
|
61
|
+
"@ladamczyk/qoq-eslint-v9-ts": "6.1.10",
|
|
62
|
+
"@ladamczyk/qoq-jscpd": "6.1.10",
|
|
63
|
+
"@ladamczyk/qoq-knip": "6.1.10",
|
|
64
|
+
"@ladamczyk/qoq-prettier-with-json-sort": "6.1.10",
|
|
65
|
+
"@types/node": "^22",
|
|
66
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
67
|
+
"dotenv": "18.0.6",
|
|
68
|
+
"husky": "9.1.7",
|
|
69
|
+
"npm-run-all2": "9.0.3",
|
|
70
|
+
"rimraf": "6.1.3",
|
|
71
|
+
"rolldown": "1.2.13",
|
|
72
|
+
"typescript": "6.0.3",
|
|
73
|
+
"vitest": "5.0.3"
|
|
74
|
+
},
|
|
75
|
+
"engines": {
|
|
76
|
+
"node": ">=22.22.2"
|
|
77
|
+
},
|
|
78
|
+
"publishConfig": {
|
|
79
|
+
"access": "public"
|
|
80
|
+
}
|
|
81
|
+
}
|