mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(cli): add cli command to setup custom ui local debugging proxy (#6365)
* feat(cli): add proxy * refactor(cli): polish code per comments * refactor(cli): polish code * refactor(cli): support serving static files * chore: add changeset * refactor: polish code * refactor(cli): polish code * refactor(cli): make json parse safer
This commit is contained in:
parent
3f014eb573
commit
2d0502a427
7 changed files with 369 additions and 28 deletions
34
.changeset/slow-buses-rhyme.md
Normal file
34
.changeset/slow-buses-rhyme.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
"@logto/cli": minor
|
||||
---
|
||||
|
||||
add new cli command to setup proxy for developing and debugging custom ui locally
|
||||
|
||||
This command will establish a proxy tunnel between the following 3 entities together: your Logto cloud auth services, your application, and your custom sign-in UI.
|
||||
|
||||
Assuming you have a custom sign-in page running on `http://localhost:4000`.
|
||||
Then you can execute the command this way:
|
||||
|
||||
```bash
|
||||
npm cli proxy --endpoint https://<tenant-id>.logto.app --port 9000 --experience-uri http://localhost:4000
|
||||
```
|
||||
|
||||
Or if you don't have your custom UI pages hosted on a dev server, you can use the `--experience-path` option to specify the path to your static files:
|
||||
|
||||
```bash
|
||||
npm cli proxy --endpoint https://<tenant-id>.logto.app --port 9000 --experience-path /path/to/your/custom/ui
|
||||
```
|
||||
|
||||
This command also works if you have enabled custom domain in your Logto tenant. E.g.:
|
||||
|
||||
```bash
|
||||
npm cli proxy --endpoint https://your-custom-domain.com --port 9000 --experience-path /path/to/your/custom/ui
|
||||
```
|
||||
|
||||
This should set up the proxy and it will be running on your local machine at `http://localhost:9000/`.
|
||||
|
||||
Finally, run your application and set its Logto endpoint to the proxy address `http://localhost:9000/` instead.
|
||||
|
||||
If all set up correctly, when you click the "sign-in" button in your application, you should be navigated to your custom sign-in page instead of Logto's built-in UI, along with valid session (cookies) that allows you to further interact with Logto experience API.
|
||||
|
||||
Happy coding!
|
|
@ -56,7 +56,9 @@
|
|||
"dotenv": "^16.4.5",
|
||||
"got": "^14.0.0",
|
||||
"hpagent": "^1.2.0",
|
||||
"http-proxy-middleware": "^3.0.0",
|
||||
"inquirer": "^9.0.0",
|
||||
"mime": "^4.0.4",
|
||||
"nanoid": "^5.0.1",
|
||||
"ora": "^8.0.1",
|
||||
"p-limit": "^6.0.0",
|
||||
|
|
106
packages/cli/src/commands/proxy/index.ts
Normal file
106
packages/cli/src/commands/proxy/index.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import http from 'node:http';
|
||||
|
||||
import { isValidUrl } from '@logto/core-kit';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import chalk from 'chalk';
|
||||
import type { CommandModule } from 'yargs';
|
||||
|
||||
import { consoleLog } from '../../utils.js';
|
||||
|
||||
import { type ProxyCommandArgs } from './types.js';
|
||||
import {
|
||||
checkExperienceInput,
|
||||
createLogtoResponseHandler,
|
||||
createProxy,
|
||||
createStaticFileProxy,
|
||||
isLogtoRequestPath,
|
||||
} from './utils.js';
|
||||
|
||||
const proxy: CommandModule<unknown, ProxyCommandArgs> = {
|
||||
command: ['proxy'],
|
||||
describe: 'Command for Logto proxy',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.options({
|
||||
'experience-uri': {
|
||||
alias: ['x'],
|
||||
describe: 'The URI of your custom sign-in experience page.',
|
||||
type: 'string',
|
||||
},
|
||||
'experience-path': {
|
||||
alias: ['xp'],
|
||||
describe: 'The local folder path of your custom sign-in experience assets.',
|
||||
type: 'string',
|
||||
},
|
||||
endpoint: {
|
||||
alias: 'ep',
|
||||
describe:
|
||||
'Logto endpoint URI, which can be found in Logto Console. E.g.: https://<tenant-id>.logto.app/',
|
||||
type: 'string',
|
||||
},
|
||||
port: {
|
||||
alias: 'p',
|
||||
describe: 'The port number where the proxy server will be running on. Defaults to 9000.',
|
||||
type: 'number',
|
||||
default: 9000,
|
||||
},
|
||||
verbose: {
|
||||
alias: 'v',
|
||||
describe: 'Show verbose output.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
.global('e'),
|
||||
handler: async ({ 'experience-uri': url, 'experience-path': path, endpoint, port, verbose }) => {
|
||||
checkExperienceInput(url, path);
|
||||
|
||||
if (!endpoint || !isValidUrl(endpoint)) {
|
||||
consoleLog.fatal('A valid Logto endpoint URI must be provided.');
|
||||
}
|
||||
const logtoEndpointUrl = new URL(endpoint);
|
||||
const proxyUrl = new URL(`http://localhost:${port}`);
|
||||
|
||||
const proxyLogtoRequest = createProxy(
|
||||
logtoEndpointUrl.href,
|
||||
async (proxyResponse, request, response) =>
|
||||
createLogtoResponseHandler({
|
||||
proxyResponse,
|
||||
request,
|
||||
response,
|
||||
logtoEndpointUrl,
|
||||
proxyUrl,
|
||||
verbose,
|
||||
})
|
||||
);
|
||||
const proxyExperienceServerRequest = conditional(url && createProxy(url));
|
||||
const proxyExperienceStaticFileRequest = conditional(path && createStaticFileProxy(path));
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
if (verbose) {
|
||||
consoleLog.info(`Incoming request: ${chalk.blue(request.method, request.url)}`);
|
||||
}
|
||||
|
||||
// Proxy the requests to Logto endpoint
|
||||
if (isLogtoRequestPath(request.url)) {
|
||||
void proxyLogtoRequest(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (proxyExperienceServerRequest) {
|
||||
void proxyExperienceServerRequest(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (proxyExperienceStaticFileRequest) {
|
||||
void proxyExperienceStaticFileRequest(request, response);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port, () => {
|
||||
consoleLog.info(`Proxy server is running on ${chalk.blue(proxyUrl.href)}`);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default proxy;
|
18
packages/cli/src/commands/proxy/types.ts
Normal file
18
packages/cli/src/commands/proxy/types.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import type * as http from 'node:http';
|
||||
|
||||
export type ProxyCommandArgs = {
|
||||
'experience-uri'?: string;
|
||||
'experience-path'?: string;
|
||||
endpoint?: string;
|
||||
port: number;
|
||||
verbose: boolean;
|
||||
};
|
||||
|
||||
export type ProxyResponseHandler = {
|
||||
proxyResponse: http.IncomingMessage;
|
||||
request: http.IncomingMessage;
|
||||
response: http.ServerResponse;
|
||||
logtoEndpointUrl: URL;
|
||||
proxyUrl: URL;
|
||||
verbose: boolean;
|
||||
};
|
157
packages/cli/src/commands/proxy/utils.ts
Normal file
157
packages/cli/src/commands/proxy/utils.ts
Normal file
|
@ -0,0 +1,157 @@
|
|||
import { existsSync } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import type http from 'node:http';
|
||||
import path from 'node:path';
|
||||
|
||||
import { isValidUrl } from '@logto/core-kit';
|
||||
import { conditional, trySafe } from '@silverhand/essentials';
|
||||
import chalk from 'chalk';
|
||||
import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware';
|
||||
import { type OnProxyEvent } from 'http-proxy-middleware/dist/types.js';
|
||||
import mime from 'mime';
|
||||
|
||||
import { consoleLog } from '../../utils.js';
|
||||
|
||||
import { type ProxyResponseHandler } from './types.js';
|
||||
|
||||
export const createProxy = (targetUrl: string, onProxyResponse?: OnProxyEvent['proxyRes']) => {
|
||||
const hasResponseHandler = Boolean(onProxyResponse);
|
||||
return createProxyMiddleware({
|
||||
target: targetUrl,
|
||||
changeOrigin: true,
|
||||
selfHandleResponse: hasResponseHandler,
|
||||
...conditional(
|
||||
hasResponseHandler && {
|
||||
on: {
|
||||
proxyRes: onProxyResponse,
|
||||
error: (error) => {
|
||||
consoleLog.error(chalk.red(error));
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const index = 'index.html';
|
||||
const indexContentType = 'text/html; charset=utf-8';
|
||||
const noCache = 'no-cache, no-store, must-revalidate';
|
||||
const maxAgeSevenDays = 'max-age=604_800_000';
|
||||
|
||||
export const createStaticFileProxy =
|
||||
(staticPath: string) => async (request: http.IncomingMessage, response: http.ServerResponse) => {
|
||||
if (!request.url) {
|
||||
response.writeHead(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === 'HEAD' || request.method === 'GET') {
|
||||
const fallBackToIndex = !isFileAssetPath(request.url);
|
||||
const requestPath = path.join(staticPath, fallBackToIndex ? index : request.url);
|
||||
try {
|
||||
const content = await fs.readFile(requestPath, 'utf8');
|
||||
response.setHeader('cache-control', fallBackToIndex ? noCache : maxAgeSevenDays);
|
||||
response.setHeader('content-type', getMimeType(request.url));
|
||||
response.writeHead(200);
|
||||
response.end(content);
|
||||
} catch (error: unknown) {
|
||||
consoleLog.error(chalk.red(error));
|
||||
response.setHeader('content-type', getMimeType(request.url));
|
||||
response.writeHead(existsSync(request.url) ? 500 : 404);
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Intercept the response from Logto endpoint and replace Logto endpoint URLs in the response with the
|
||||
* proxy URL. The string replace happens in the following cases:
|
||||
* - The response is a redirect response, and the `location` property in response header may contain Logto
|
||||
* endpoint URI.
|
||||
* - The response body is JSON, which consists of properties such as `**_endpoint` and `redirectTo`. These
|
||||
* properties may contain Logto endpoint URI.
|
||||
* - The response is HTML content that contains a form. The form action URL may contain Logto endpoint URI.
|
||||
*
|
||||
* Note: the `issuer` and `jwks_uri` properties in the `/oidc/.well-known` response should not be replaced,
|
||||
* even they also contain the Logto endpoint URI.
|
||||
*/
|
||||
export const createLogtoResponseHandler = async ({
|
||||
proxyResponse,
|
||||
request,
|
||||
response,
|
||||
logtoEndpointUrl,
|
||||
proxyUrl,
|
||||
verbose,
|
||||
}: ProxyResponseHandler) => {
|
||||
const { location } = proxyResponse.headers;
|
||||
if (location) {
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
proxyResponse.headers.location = location.replace(logtoEndpointUrl.href, proxyUrl.href);
|
||||
}
|
||||
|
||||
void responseInterceptor(async (responseBuffer, proxyResponse) => {
|
||||
const responseBody = responseBuffer.toString();
|
||||
if (verbose) {
|
||||
consoleLog.info(`Response received: ${chalk.green(responseBody)}`);
|
||||
}
|
||||
|
||||
if (proxyResponse.headers['content-type']?.includes('text/html')) {
|
||||
return responseBody.replace(`action="${logtoEndpointUrl.href}`, `action="${proxyUrl.href}`);
|
||||
}
|
||||
|
||||
if (proxyResponse.headers['content-type']?.includes('application/json')) {
|
||||
const jsonData = trySafe<unknown>(() => JSON.parse(responseBody));
|
||||
|
||||
if (jsonData && typeof jsonData === 'object') {
|
||||
const updatedEntries: Array<[string, unknown]> = Object.entries(jsonData).map(
|
||||
([key, value]) => {
|
||||
if ((key === 'redirectTo' || key.endsWith('_endpoint')) && typeof value === 'string') {
|
||||
return [key, value.replace(logtoEndpointUrl.href, proxyUrl.href)];
|
||||
}
|
||||
return [key, value];
|
||||
}
|
||||
);
|
||||
|
||||
return JSON.stringify(Object.fromEntries(updatedEntries));
|
||||
}
|
||||
}
|
||||
return responseBody;
|
||||
})(proxyResponse, request, response);
|
||||
};
|
||||
|
||||
export const checkExperienceInput = (url?: string, staticPath?: string) => {
|
||||
if (staticPath && url) {
|
||||
consoleLog.fatal('Only one of the experience URI or path can be provided.');
|
||||
}
|
||||
if (!staticPath && !url) {
|
||||
consoleLog.fatal('Either a sign-in experience URI or local path must be provided.');
|
||||
}
|
||||
if (url && !isValidUrl(url)) {
|
||||
consoleLog.fatal(
|
||||
'A valid sign-in experience URI must be provided. E.g.: http://localhost:4000'
|
||||
);
|
||||
}
|
||||
if (staticPath && !existsSync(path.join(staticPath, index))) {
|
||||
consoleLog.fatal('The provided path does not contain a valid index.html file.');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the request path is a Logto request path.
|
||||
* @example isLogtoRequestPath('/oidc/.well-known/openid-configuration') // true
|
||||
* @example isLogtoRequestPath('/oidc/auth') // true
|
||||
* @example isLogtoRequestPath('/api/interaction/submit') // true
|
||||
* @example isLogtoRequestPath('/consent') // true
|
||||
*/
|
||||
export const isLogtoRequestPath = (requestPath?: string) =>
|
||||
['/oidc/', '/api/'].some((path) => requestPath?.startsWith(path)) || requestPath === '/consent';
|
||||
|
||||
const isFileAssetPath = (url: string) => url.split('/').at(-1)?.includes('.');
|
||||
|
||||
const getMimeType = (requestPath: string) => {
|
||||
const fallBackToIndex = !isFileAssetPath(requestPath);
|
||||
if (fallBackToIndex) {
|
||||
return indexContentType;
|
||||
}
|
||||
return mime.getType(requestPath) ?? 'application/octet-stream';
|
||||
};
|
|
@ -6,6 +6,7 @@ import { hideBin } from 'yargs/helpers';
|
|||
import connector from './commands/connector/index.js';
|
||||
import database from './commands/database/index.js';
|
||||
import install from './commands/install/index.js';
|
||||
import proxy from './commands/proxy/index.js';
|
||||
import translate from './commands/translate/index.js';
|
||||
import { packageJson } from './package-json.js';
|
||||
import { cliConfig, ConfigKey, consoleLog } from './utils.js';
|
||||
|
@ -48,6 +49,7 @@ void yargs(hideBin(process.argv))
|
|||
.command(database)
|
||||
.command(connector)
|
||||
.command(translate)
|
||||
.command(proxy)
|
||||
.demandCommand(1)
|
||||
.showHelpOnFail(false, `Specify ${chalk.green('--help')} for available options`)
|
||||
.strict()
|
||||
|
|
|
@ -130,9 +130,15 @@ importers:
|
|||
hpagent:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
http-proxy-middleware:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
inquirer:
|
||||
specifier: ^9.0.0
|
||||
version: 9.1.4
|
||||
mime:
|
||||
specifier: ^4.0.4
|
||||
version: 4.0.4
|
||||
nanoid:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1
|
||||
|
@ -5942,6 +5948,9 @@ packages:
|
|||
'@types/http-errors@1.8.2':
|
||||
resolution: {integrity: sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==}
|
||||
|
||||
'@types/http-proxy@1.17.14':
|
||||
resolution: {integrity: sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==}
|
||||
|
||||
'@types/inquirer@9.0.3':
|
||||
resolution: {integrity: sha512-CzNkWqQftcmk2jaCWdBTf9Sm7xSw4rkI1zpU/Udw3HX5//adEZUIm9STtoRP1qgWj0CWQtJ9UTvqmO2NNjhMJw==}
|
||||
|
||||
|
@ -8574,6 +8583,10 @@ packages:
|
|||
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
http-proxy-middleware@3.0.0:
|
||||
resolution: {integrity: sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
http-proxy@1.18.1:
|
||||
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
@ -8910,6 +8923,10 @@ packages:
|
|||
resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-plain-obj@3.0.0:
|
||||
resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
is-plain-obj@4.1.0:
|
||||
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -10049,6 +10066,11 @@ packages:
|
|||
engines: {node: '>=10.0.0'}
|
||||
hasBin: true
|
||||
|
||||
mime@4.0.4:
|
||||
resolution: {integrity: sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
mimic-fn@2.1.0:
|
||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||
engines: {node: '>=6'}
|
||||
|
@ -15815,6 +15837,10 @@ snapshots:
|
|||
|
||||
'@types/http-errors@1.8.2': {}
|
||||
|
||||
'@types/http-proxy@1.17.14':
|
||||
dependencies:
|
||||
'@types/node': 20.12.7
|
||||
|
||||
'@types/inquirer@9.0.3':
|
||||
dependencies:
|
||||
'@types/through': 0.0.30
|
||||
|
@ -16660,7 +16686,7 @@ snapshots:
|
|||
|
||||
axios@1.6.7:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.6
|
||||
follow-redirects: 1.15.6(debug@4.3.5)
|
||||
form-data: 4.0.0
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
|
@ -16668,7 +16694,7 @@ snapshots:
|
|||
|
||||
axios@1.7.2:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.6
|
||||
follow-redirects: 1.15.6(debug@4.3.5)
|
||||
form-data: 4.0.0
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
|
@ -18520,7 +18546,9 @@ snapshots:
|
|||
|
||||
flatted@3.3.1: {}
|
||||
|
||||
follow-redirects@1.15.6: {}
|
||||
follow-redirects@1.15.6(debug@4.3.5):
|
||||
optionalDependencies:
|
||||
debug: 4.3.5
|
||||
|
||||
for-each@0.3.3:
|
||||
dependencies:
|
||||
|
@ -19031,10 +19059,21 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
http-proxy@1.18.1:
|
||||
http-proxy-middleware@3.0.0:
|
||||
dependencies:
|
||||
'@types/http-proxy': 1.17.14
|
||||
debug: 4.3.5
|
||||
http-proxy: 1.18.1(debug@4.3.5)
|
||||
is-glob: 4.0.3
|
||||
is-plain-obj: 3.0.0
|
||||
micromatch: 4.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
http-proxy@1.18.1(debug@4.3.5):
|
||||
dependencies:
|
||||
eventemitter3: 4.0.7
|
||||
follow-redirects: 1.15.6
|
||||
follow-redirects: 1.15.6(debug@4.3.5)
|
||||
requires-port: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
@ -19340,6 +19379,8 @@ snapshots:
|
|||
|
||||
is-plain-obj@1.1.0: {}
|
||||
|
||||
is-plain-obj@3.0.0: {}
|
||||
|
||||
is-plain-obj@4.1.0: {}
|
||||
|
||||
is-plain-object@5.0.0: {}
|
||||
|
@ -20213,7 +20254,7 @@ snapshots:
|
|||
|
||||
koa-proxies@0.12.4(koa@2.15.3):
|
||||
dependencies:
|
||||
http-proxy: 1.18.1
|
||||
http-proxy: 1.18.1(debug@4.3.5)
|
||||
koa: 2.15.3
|
||||
path-match: 1.2.4
|
||||
uuid: 8.3.2
|
||||
|
@ -21243,6 +21284,8 @@ snapshots:
|
|||
|
||||
mime@3.0.0: {}
|
||||
|
||||
mime@4.0.4: {}
|
||||
|
||||
mimic-fn@2.1.0: {}
|
||||
|
||||
mimic-fn@4.0.0: {}
|
||||
|
@ -21923,7 +21966,7 @@ snapshots:
|
|||
yaml: 2.4.5
|
||||
optionalDependencies:
|
||||
postcss: 8.4.39
|
||||
ts-node: 10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.5.3)
|
||||
ts-node: 10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.5.3)
|
||||
|
||||
postcss-media-query-parser@0.2.3: {}
|
||||
|
||||
|
@ -23568,27 +23611,6 @@ snapshots:
|
|||
'@swc/core': 1.3.52(@swc/helpers@0.5.1)
|
||||
optional: true
|
||||
|
||||
ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.12.7)(typescript@5.5.3):
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
'@tsconfig/node10': 1.0.9
|
||||
'@tsconfig/node12': 1.0.11
|
||||
'@tsconfig/node14': 1.0.3
|
||||
'@tsconfig/node16': 1.0.4
|
||||
'@types/node': 20.12.7
|
||||
acorn: 8.11.3
|
||||
acorn-walk: 8.3.2
|
||||
arg: 4.1.3
|
||||
create-require: 1.1.1
|
||||
diff: 4.0.2
|
||||
make-error: 1.3.6
|
||||
typescript: 5.5.3
|
||||
v8-compile-cache-lib: 3.0.1
|
||||
yn: 3.1.1
|
||||
optionalDependencies:
|
||||
'@swc/core': 1.3.52(@swc/helpers@0.5.1)
|
||||
optional: true
|
||||
|
||||
tsconfig-paths@3.15.0:
|
||||
dependencies:
|
||||
'@types/json5': 0.0.29
|
||||
|
|
Loading…
Reference in a new issue