diff --git a/apps/site/docs/en/cli.md b/apps/site/docs/en/cli.md index 24ada3b76..e6328669a 100644 --- a/apps/site/docs/en/cli.md +++ b/apps/site/docs/en/cli.md @@ -1,6 +1,6 @@ # Command Line Tools -`@midscene/cli` is the command line version of Midscene. It is suitable for executing very simple tasks or experiencing the basics of Midscene. +`@midscene/cli` is the command line version of Midscene. It is suitable for executing very simple tasks. For example, you can write a one-line npm script to check if the build result can launch normally, or extract some data from the web page and write the result into a JSON file. :::info Demo Project you can check the demo project of command line tools here: [https://github.com/web-infra-dev/midscene-example/blob/main/command-line](https://github.com/web-infra-dev/midscene-example/blob/main/command-line) @@ -21,13 +21,7 @@ export OPENAI_API_KEY="sk-abcdefghijklmnopqrstuvwxyz" ## Examples -Use headed mode (i.e. visible browser) to visit bing.com and search for 'weather today' - -```bash -npx @midscene/cli --headed --url https://wwww.bing.com --action "type 'weather today', hit enter" --sleep 3000 -``` - -visit github status page and save the status to ./status.json +Visit github status page and save the status to `./status.json` ```bash npx @midscene/cli --url https://www.githubstatus.com/ \ @@ -35,6 +29,18 @@ npx @midscene/cli --url https://www.githubstatus.com/ \ --query '{name: string, status: string}[], service status of github page' ``` +Serve the `./dist` path statically and check if the `index.html` can launch normally + +```bash +npx @midscene/cli --serve ./dist --url index.html --assert 'page title is "My App"' +``` + +Use headed mode (i.e. visible browser) to visit bing.com and search for 'weather today' + +```bash +npx @midscene/cli --headed --url https://wwww.bing.com --action "type 'weather today', hit enter" --sleep 3000 +``` + Or you may install @midscene/cli globally before calling ```bash @@ -77,3 +83,4 @@ Actions (the order matters, can be used multiple times): 1. Always put options before any action param. 2. The order of action parameters matters. For example, `--action "some action" --query "some data"` means that the action is taken first, followed by a query. 3. If you have some more complex requirements, such as loop operations, using the SDK version (instead of this cli) is an easier way to achieve them. +4. Midscene CLI reads the `.env` file by dotenv in the current working directory, allowing you to place some configuration in it. diff --git a/apps/site/docs/zh/cli.md b/apps/site/docs/zh/cli.md index 130564537..f160c6344 100644 --- a/apps/site/docs/zh/cli.md +++ b/apps/site/docs/zh/cli.md @@ -1,6 +1,6 @@ # 命令行工具 -`@midscene/cli` 是 Midscene 的命令行版本。它适合执行简单的任务或体验 Midscene 的基础功能。 +`@midscene/cli` 是 Midscene 的命令行版本。它非常适合执行简单的任务。比如你可以用它校验编译后的产物是否能正常启动,或者是从某些页面中提取信息并写入一个 JSON 文件。 :::info 样例项目 你可以在这里看到使用命令行工具的样例项目:[https://github.com/web-infra-dev/midscene-example/blob/main/command-line](https://github.com/web-infra-dev/midscene-example/blob/main/command-line) @@ -25,16 +25,26 @@ export OPENAI_API_KEY="sk-abcdefghijklmnopqrstuvwxyz" ## 示例 -```bash -# headed 模式(即可见浏览器)访问 baidu.com 并搜索“天气” -npx @midscene/cli --headed --url https://www.baidu.com --action "输入 '天气', 敲回车" --sleep 3000 +访问 Github 状态页面并将状态保存到 `./status.json` -# 访问 Github 状态页面并将状态保存到 ./status.json +```bash npx @midscene/cli --url https://www.githubstatus.com/ \ --query-output status.json \ --query '{serviceName: string, status: string}[], github 页面的服务状态,返回服务名称' ``` +为 `./dist` 目录启动静态服务,并检查 `index.html` 能否正常启动 + +```bash +npx @midscene/cli --serve ./dist --url index.html --assert '页面标题是 "My App"' +``` + +用 headed 模式(即可见浏览器)访问 baidu.com 并搜索“天气” + +```bash +npx @midscene/cli --headed --url https://www.baidu.com --action "输入 '天气', 敲回车" --sleep 3000 +``` + 你也可以先全局安装 @midscene/cli 再调用 ```bash @@ -76,3 +86,4 @@ Actions (参数顺序很重要,可以支持多次使用): 1. Options 参数(任务信息)应始终放在 Actions 参数之前。 2. Actions 参数的顺序很重要。例如,`--action "某操作" --query "某数据"` 表示先执行操作,然后再查询。 3. 如果有更复杂的需求,比如循环操作,使用 SDK 版本(而不是这个命令行工具)会更合适。 +4. Midscene Cli 会用 dotenv 读取当前路径下的 `.env` 配置文件,你可以将环境配置放在其中 \ No newline at end of file diff --git a/package.json b/package.json index 319bb36f6..d90b1d75b 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "e2e": "nx run @midscene/web:e2e --verbose", "e2e:cache": "nx run @midscene/web:e2e:cache --verbose", "e2e:report": "nx run @midscene/web:e2e:report --verbose", - "test:ai:all": "npm run e2e && npm run e2e:cache && npm run e2e:report && npm run test:ai", + "e2e:visualizer": "nx run @midscene/visualizer:e2e --verbose", + "test:ai:all": "npm run e2e && npm run e2e:cache && npm run e2e:report && npm run test:ai && npm run e2e:visualizer", "prepare": "pnpm run build && simple-git-hooks", "check-dependency-version": "check-dependency-version-consistency .", "lint": "npx biome check . --diagnostic-level=warn --no-errors-on-unmatched --fix", diff --git a/packages/cli/bin/midscene b/packages/cli/bin/midscene index 5a87901fa..8e95a710b 100755 --- a/packages/cli/bin/midscene +++ b/packages/cli/bin/midscene @@ -2,8 +2,4 @@ require('../dist/lib/help.js'); -if (process.argv.indexOf('playground') !== -1) { - require('../dist/lib/playground.js'); -} else { - require('../dist/lib/index.js'); -} \ No newline at end of file +require('../dist/lib/index.js'); diff --git a/packages/cli/modern.config.ts b/packages/cli/modern.config.ts index 8030338e0..74d213951 100644 --- a/packages/cli/modern.config.ts +++ b/packages/cli/modern.config.ts @@ -8,9 +8,7 @@ export default defineConfig({ input: { index: 'src/index.ts', help: 'src/help.ts', - playground: 'src/playground.ts', }, - // input: ['src/utils.ts', 'src/index.ts', 'src/image/index.ts'], externals: ['node:buffer'], target: 'es6', }, diff --git a/packages/cli/package.json b/packages/cli/package.json index f2a2e2cb9..44a201a08 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -22,12 +22,16 @@ }, "dependencies": { "@midscene/web": "workspace:*", + "dotenv": "16.4.5", + "http-server": "14.1.1", + "minimist": "1.2.5", "ora-classic": "5.4.2", "puppeteer": "23.0.2", "yargs": "17.7.2" }, "devDependencies": { "@modern-js/module-tools": "2.60.6", + "@types/minimist": "1.2.5", "@types/node": "^18.0.0", "@types/yargs": "17.0.32", "execa": "9.3.0", diff --git a/packages/cli/src/args.ts b/packages/cli/src/args.ts index 75b843255..85244ac64 100644 --- a/packages/cli/src/args.ts +++ b/packages/cli/src/args.ts @@ -1,12 +1,29 @@ -export type ArgumentValueType = string | boolean | number; +import type minimist from 'minimist'; -export interface Argument { +export type ArgumentValueType = string | boolean | number; +export function findOnlyItemInArgs( + args: minimist.ParsedArgs, + name: string, +): string | boolean | number | undefined { + const found = args[name]; + if (found === undefined) { + return false; + } + + if (Array.isArray(found) && found.length > 1) { + throw new Error(`Multiple values found for ${name}`); + } + + return found; +} + +export interface OrderedArgumentItem { name: string; value: ArgumentValueType; } -export function parse(args: string[]): Argument[] { - const orderedArgs: Argument[] = []; +export function orderMattersParse(args: string[]): OrderedArgumentItem[] { + const orderedArgs: OrderedArgumentItem[] = []; args.forEach((arg, index) => { if (arg.startsWith('--')) { const key = arg.substring(2); @@ -24,19 +41,3 @@ export function parse(args: string[]): Argument[] { return orderedArgs; } - -export function findOnlyItemInArgs( - args: Argument[], - name: string, -): ArgumentValueType { - const found = args.filter((arg) => arg.name === name); - if (found.length === 0) { - return false; - } - - if (found.length > 1) { - throw new Error(`Multiple values found for ${name}`); - } - - return found[0].value; -} diff --git a/packages/cli/src/help.ts b/packages/cli/src/help.ts index 78b9c1032..cb5b58073 100644 --- a/packages/cli/src/help.ts +++ b/packages/cli/src/help.ts @@ -8,7 +8,8 @@ if (process.argv.indexOf('--help') !== -1) { Usage: midscene [options] [actions] Options: - --url The URL to visit, required + --serve Serve the local path as a static server, optional + --url The URL to visit, required. If --serve is provided, provide the path to the file to visit --user-agent The user agent to use, optional --viewport-width The width of the viewport, optional --viewport-height The height of the viewport, optional @@ -34,6 +35,9 @@ if (process.argv.indexOf('--help') !== -1) { --query-output status.json \\ --query '{name: string, status: string}[], service status of github page' + # serve the current directory and visit the index.html file + midscene --serve . --url "index.html" --assert "the content title is 'My App'" + Examples with Chinese Prompts # headed 模式(即可见浏览器)访问 baidu.com 并搜索“天气” midscene --headed --url "https://www.baidu.com" --action "在搜索框输入 '天气', 敲回车" --wait-for 界面上出现了天气信息 @@ -42,10 +46,6 @@ if (process.argv.indexOf('--help') !== -1) { midscene --url "https://www.githubstatus.com/" \\ --query-output status.json \\ --query '{serviceName: string, status: string}[], github 页面的服务状态,返回服务名称' - - - To launch a playground server, run the following command: - midscene playground `); process.exit(0); } else if (process.argv.indexOf('--version') !== -1) { diff --git a/packages/cli/src/http-server.d.ts b/packages/cli/src/http-server.d.ts new file mode 100644 index 000000000..32a0eeb1f --- /dev/null +++ b/packages/cli/src/http-server.d.ts @@ -0,0 +1,6 @@ +declare module 'http-server' { + export function createServer(options: http.ServerOptions): { + server: http.Server; + listen: (port: number, host: string, callback: () => void) => void; + }; +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 42f4ba973..318f563c9 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,28 +1,38 @@ import assert from 'node:assert'; import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; import { PuppeteerAgent } from '@midscene/web/puppeteer'; +import { createServer } from 'http-server'; +import minimist from 'minimist'; import ora from 'ora-classic'; import puppeteer from 'puppeteer'; -import { type ArgumentValueType, findOnlyItemInArgs, parse } from './args'; +import { findOnlyItemInArgs, orderMattersParse } from './args'; +import 'dotenv/config'; let spinner: ora.Ora | undefined; -const stepString = (name: string, param?: any) => { - let paramStr; - if (typeof param === 'object') { - paramStr = JSON.stringify(param, null, 2); - } else if (name === 'sleep') { - paramStr = `${param}ms`; - } else { - paramStr = param; +const stepString = (name: string, param?: any, line2?: string) => { + const paramToString = (data: any) => { + if (name === 'sleep') { + return `${data}ms`; + } + if (typeof data === 'object') { + return JSON.stringify(data, null, 2); + } + return String(data); + }; + + let paramStr = paramToString(param); + if (line2) { + paramStr = `${paramStr}\n ${line2}`; } return `${name}\n ${paramStr ? `${paramStr}` : ''}`; }; -const printStep = (name: string, param?: any) => { +const printStep = (name: string, param?: any, line2?: string) => { if (spinner) { spinner.stop(); } - console.log(`- ${stepString(name, param)}`); + console.log(`- ${stepString(name, param, line2)}`); }; const updateSpin = (text: string) => { @@ -35,8 +45,23 @@ const updateSpin = (text: string) => { } }; +const launchServer = async ( + dir: string, +): Promise> => { + // https://github.com/http-party/http-server/blob/master/bin/http-server + return new Promise((resolve, reject) => { + const server = createServer({ + root: dir, + }); + server.listen(0, '127.0.0.1', () => { + resolve(server); + }); + }); +}; + const preferenceArgs = { url: 'url', + serve: 'serve', headed: 'headed', viewportWidth: 'viewport-width', viewportHeight: 'viewport-height', @@ -61,8 +86,7 @@ const defaultUA = const welcome = '\nWelcome to @midscene/cli\n'; console.log(welcome); -const args = parse(process.argv); - +const args = minimist(process.argv); if (findOnlyItemInArgs(args, 'version')) { const versionFromPkgJson = require('../package.json').version; console.log(`@midscene/cli version ${versionFromPkgJson}`); @@ -70,45 +94,84 @@ if (findOnlyItemInArgs(args, 'version')) { } // check each arg is either in the preferenceArgs or actionArgs -args.forEach((arg) => { +Object.keys(args).forEach((arg) => { + if (arg === '_') return; assert( - Object.values(preferenceArgs).includes(arg.name) || - Object.values(actionArgs).includes(arg.name), - `Unknown argument: ${arg.name}`, + Object.values(preferenceArgs).includes(arg) || + Object.values(actionArgs).includes(arg), + `Unknown argument: ${arg}`, ); }); -// prepare the viewport config -const preferHeaded = findOnlyItemInArgs(args, preferenceArgs.headed); -const userExpectWidth = findOnlyItemInArgs(args, preferenceArgs.viewportWidth); -const userExpectHeight = findOnlyItemInArgs( - args, - preferenceArgs.viewportHeight, -); -const userExpectDpr = findOnlyItemInArgs(args, preferenceArgs.viewportScale); -const defaultDpr = process.platform === 'darwin' ? 2 : 1; -const viewportConfig = { - width: typeof userExpectWidth === 'number' ? userExpectWidth : 1280, - height: typeof userExpectHeight === 'number' ? userExpectHeight : 1280, - deviceScaleFactor: - typeof userExpectDpr === 'number' ? userExpectDpr : defaultDpr, -}; -const url = findOnlyItemInArgs(args, preferenceArgs.url); -assert(url, 'URL is required'); -assert(typeof url === 'string', 'URL must be a string'); - -const preferredUA = findOnlyItemInArgs(args, preferenceArgs.useragent); -const ua = typeof preferredUA === 'string' ? preferredUA : defaultUA; - -printStep(preferenceArgs.url, url); -printStep(preferenceArgs.useragent, ua); -printStep('viewport', JSON.stringify(viewportConfig)); -if (preferHeaded) { - printStep(preferenceArgs.headed, 'true'); -} - Promise.resolve( (async () => { + // prepare the static server + const staticServerConfig = findOnlyItemInArgs(args, 'serve'); + let staticServerUrl: string | undefined; + if (staticServerConfig) { + const serverDir = + typeof staticServerConfig === 'string' + ? staticServerConfig + : process.cwd(); + const finalServerDir = resolve(process.cwd(), serverDir); + + const staticServerResult = await launchServer(finalServerDir); + const server = staticServerResult.server; + const serverAddress = server.address(); + staticServerUrl = `http://${serverAddress?.address}:${serverAddress?.port}`; + printStep('static server', finalServerDir, staticServerUrl); + } + + // prepare the viewport config + const preferHeaded = findOnlyItemInArgs(args, preferenceArgs.headed); + const userExpectWidth = findOnlyItemInArgs( + args, + preferenceArgs.viewportWidth, + ); + const userExpectHeight = findOnlyItemInArgs( + args, + preferenceArgs.viewportHeight, + ); + const userExpectDpr = findOnlyItemInArgs( + args, + preferenceArgs.viewportScale, + ); + const defaultDpr = process.platform === 'darwin' ? 2 : 1; + const viewportConfig = { + width: typeof userExpectWidth === 'number' ? userExpectWidth : 1280, + height: typeof userExpectHeight === 'number' ? userExpectHeight : 1280, + deviceScaleFactor: + typeof userExpectDpr === 'number' ? userExpectDpr : defaultDpr, + }; + const url = findOnlyItemInArgs(args, preferenceArgs.url) as + | string + | undefined; + let urlToVisit: string | undefined; + if (staticServerUrl) { + if (typeof url !== 'undefined') { + if (url.startsWith('/')) { + urlToVisit = `${staticServerUrl}${url}`; + } else { + urlToVisit = `${staticServerUrl}/${url}`; + } + } + } else { + urlToVisit = url; + } + assert(urlToVisit, 'URL is required'); + assert(typeof urlToVisit === 'string', 'URL must be a string'); + + const preferredUA = findOnlyItemInArgs(args, preferenceArgs.useragent); + const ua = typeof preferredUA === 'string' ? preferredUA : defaultUA; + + printStep(preferenceArgs.url, urlToVisit); + printStep(preferenceArgs.useragent, ua); + printStep('viewport', JSON.stringify(viewportConfig)); + if (preferHeaded) { + printStep(preferenceArgs.headed, 'true'); + } + + // launch the browser updateSpin(stepString('launch', 'puppeteer')); const browser = await puppeteer.launch({ headless: !preferHeaded, @@ -120,24 +183,27 @@ Promise.resolve( let errorWhenRunning: Error | undefined; let argName: string; - let argValue: ArgumentValueType; + let argValue: string | boolean | number | undefined; let agent: PuppeteerAgent | undefined; try { - updateSpin(stepString('launch', url)); - await page.goto(url); - updateSpin(stepString('waitForNetworkIdle', url)); + updateSpin(stepString('launch', urlToVisit)); + await page.goto(urlToVisit); + updateSpin(stepString('waitForNetworkIdle', urlToVisit)); await page.waitForNetworkIdle(); - printStep('launched', url); + printStep('launched', urlToVisit); agent = new PuppeteerAgent(page, { autoPrintReportMsg: false, }); + const orderedArgs = orderMattersParse(process.argv); + let index = 0; let outputPath: string | undefined; let actionStarted = false; - while (index <= args.length - 1) { - const arg = args[index]; + + while (index <= orderedArgs.length - 1) { + const arg = orderedArgs[index]; argName = arg.name; argValue = arg.value; updateSpin(stepString(argName, String(argValue))); diff --git a/packages/cli/src/playground.ts b/packages/cli/src/playground.ts deleted file mode 100644 index 37c07a5d1..000000000 --- a/packages/cli/src/playground.ts +++ /dev/null @@ -1,10 +0,0 @@ -// import { PlaygroundServer } from '@midscene/web/playground'; - -// const server = new PlaygroundServer(); -// Promise.resolve() -// .then(() => server.launch()) -// .then(() => { -// console.log( -// `Midscene playground server is running on http://localhost:${server.port}`, -// ); -// }); diff --git a/packages/cli/tests/args.test.ts b/packages/cli/tests/args.test.ts index 34372cf9a..1bf94d3bd 100644 --- a/packages/cli/tests/args.test.ts +++ b/packages/cli/tests/args.test.ts @@ -1,34 +1,16 @@ -import { findOnlyItemInArgs, parse } from '@/args'; +import { findOnlyItemInArgs, orderMattersParse } from '@/args'; import { describe, expect, test } from 'vitest'; describe('args', () => { test('should parse arguments', async () => { - const input = [ - '--url', - 'https://example.com', - '--width', - '500', - '--action', - 'click', - '--assert', - 'this is an assertion', - '--query-output', - 'output.json', - '--query', - 'title', - '--query', - 'content', - '--prefer-cache', - ]; - - const result = parse(input); - - expect(result).toMatchSnapshot(); - - expect(findOnlyItemInArgs(result, 'url')).toBe('https://example.com'); - expect(findOnlyItemInArgs(result, 'prefer-cache')).toBe(true); + expect( + findOnlyItemInArgs({ url: 'https://example.com', _: [] }, 'url'), + ).toBe('https://example.com'); expect(() => { - findOnlyItemInArgs(result, 'query'); + findOnlyItemInArgs( + { url: 'https://example.com', _: [], query: [1, 2] }, + 'query', + ); }).toThrowError('Multiple values found for query'); }); @@ -39,9 +21,13 @@ describe('args', () => { '--url', 'https://example.com', '--action', + '--sleep', + '20', + '--sleep', + '10', ]; - const result = parse(input); + const result = orderMattersParse(input); expect(result).toEqual([ { @@ -52,6 +38,14 @@ describe('args', () => { name: 'action', value: true, }, + { + name: 'sleep', + value: 20, + }, + { + name: 'sleep', + value: 10, + }, ]); }); }); diff --git a/packages/cli/tests/bin.test.ts b/packages/cli/tests/bin.test.ts index fe1c2df19..6d21290f6 100644 --- a/packages/cli/tests/bin.test.ts +++ b/packages/cli/tests/bin.test.ts @@ -36,4 +36,17 @@ describe.skipIf(process.platform !== 'darwin')('bin', () => { expect(existsSync(randomFileName)).toBeTruthy(); unlinkSync(randomFileName); }); + + test('serve', async () => { + const params = [ + '--serve', + './tests/server_root', + '--url', + 'index.html', + '--assert', + 'the content title is "My App"', + ]; + const { failed } = await execa(cliBin, params); + expect(failed).toBe(false); + }); }); diff --git a/packages/cli/tests/server_root/index.html b/packages/cli/tests/server_root/index.html new file mode 100644 index 000000000..3116c23e0 --- /dev/null +++ b/packages/cli/tests/server_root/index.html @@ -0,0 +1,2 @@ +

My App

+

This is a test page

\ No newline at end of file diff --git a/packages/visualizer/.gitignore b/packages/visualizer/.gitignore index 3fdb66692..d56693605 100644 --- a/packages/visualizer/.gitignore +++ b/packages/visualizer/.gitignore @@ -2,3 +2,7 @@ unpacked-extension/lib/ unpacked-extension/scripts/ unpacked-extension/pages/ + +# Midscene.js dump files +midscene_run/report +midscene_run/dump diff --git a/packages/visualizer/package.json b/packages/visualizer/package.json index 594c8c57c..ba58a5d31 100644 --- a/packages/visualizer/package.json +++ b/packages/visualizer/package.json @@ -20,7 +20,8 @@ "build:watch": "modern build -w", "serve": "http-server ./dist/ -p 3000", "new": "modern new", - "upgrade": "modern upgrade" + "upgrade": "modern upgrade", + "e2e": "./scripts/check-html.sh" }, "devDependencies": { "@ant-design/icons": "5.3.7", diff --git a/packages/visualizer/scripts/check-html.sh b/packages/visualizer/scripts/check-html.sh new file mode 100755 index 000000000..d6461308b --- /dev/null +++ b/packages/visualizer/scripts/check-html.sh @@ -0,0 +1,6 @@ +#! /bin/bash + +node ../cli/bin/midscene --serve ./dist/report --url demo.html \ + --action "Click the 'Insight / Locate' on Left" \ + --sleep 300 \ + --assert "There is a 'Open in Playground' button on the page" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd7598b92..390035301 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,15 @@ importers: '@midscene/web': specifier: workspace:* version: link:../web-integration + dotenv: + specifier: 16.4.5 + version: 16.4.5 + http-server: + specifier: 14.1.1 + version: 14.1.1 + minimist: + specifier: 1.2.5 + version: 1.2.5 ora-classic: specifier: 5.4.2 version: 5.4.2 @@ -97,6 +106,9 @@ importers: '@modern-js/module-tools': specifier: 2.60.6 version: 2.60.6(typescript@5.0.4) + '@types/minimist': + specifier: 1.2.5 + version: 1.2.5 '@types/node': specifier: ^18.0.0 version: 18.19.62 @@ -3498,9 +3510,6 @@ packages: '@types/node@22.8.5': resolution: {integrity: sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==} - '@types/node@22.9.0': - resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} - '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -13992,7 +14001,7 @@ snapshots: '@types/conventional-commits-parser@5.0.0': dependencies: - '@types/node': 22.9.0 + '@types/node': 18.19.62 optional: true '@types/cors@2.8.12': {} @@ -14142,11 +14151,6 @@ snapshots: dependencies: undici-types: 6.19.8 - '@types/node@22.9.0': - dependencies: - undici-types: 6.19.8 - optional: true - '@types/normalize-package-data@2.4.4': {} '@types/parse-json@4.0.2': {}