From 3b11d468a840a9a8b6974de47b439ad4d91a0412 Mon Sep 17 00:00:00 2001 From: Daniel Rochetti Date: Tue, 2 May 2023 09:18:25 -0700 Subject: [PATCH] feat(client): add function alias support (#7) * feat(client): add function alias support * fix(client): username in aliased url --- .gitignore | 1 + README.md | 4 +- apps/demo-app/pages/index.tsx | 104 ++++++----- apps/demo-app/services/generateImage.ts | 10 +- apps/demo-app/services/getJoke.ts | 18 ++ libs/client/src/config.ts | 4 +- libs/client/src/function.spec.ts | 28 +++ libs/client/src/function.ts | 84 ++++++--- libs/client/src/index.ts | 4 +- libs/client/src/utils.spec.ts | 14 ++ libs/client/src/utils.ts | 8 + package-lock.json | 231 ++++++++++++++++++++++++ 12 files changed, 430 insertions(+), 80 deletions(-) create mode 100644 apps/demo-app/services/getJoke.ts create mode 100644 libs/client/src/function.spec.ts create mode 100644 libs/client/src/utils.spec.ts create mode 100644 libs/client/src/utils.ts diff --git a/.gitignore b/.gitignore index ac31baf..944c89d 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ Thumbs.db # Next.js .next +*.local diff --git a/README.md b/README.md index ceb1909..d72537d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # The fal-serverless JS Client -![NPM client](https://img.shields.io/npm/v/@fal-ai/serverless-client?color=purple&label=client) +![NPM client](https://img.shields.io/npm/v/@fal-ai/serverless-client?color=%237527D7&label=client) ![Build](https://img.shields.io/github/actions/workflow/status/fal-ai/serverless-js/build.yml) ![License](https://img.shields.io/github/license/fal-ai/serverless-js) @@ -22,7 +22,7 @@ It also handle platform differences, so it work seamlessly across different JS r > **Note** > -> Make sure you followed the [fal-serverless getting started]() so you get your credentials and register your functions. +> Make sure you followed the [fal-serverless getting started](https://docs.fal.ai/fal-serverless/quickstart) so you get your credentials and register your functions. 1. First you need to configure your credentials: diff --git a/apps/demo-app/pages/index.tsx b/apps/demo-app/pages/index.tsx index 0f4f80b..181a3e4 100644 --- a/apps/demo-app/pages/index.tsx +++ b/apps/demo-app/pages/index.tsx @@ -1,57 +1,69 @@ -import styles from './index.module.css'; -import * as fal from '@fal-ai/serverless-client'; - -fal.config({ - credentials: { - userId: '', - keyId: '', - keySecret: '', - }, -}); +import { getJoke } from '../services/getJoke'; export async function getServerSideProps(context) { - console.log('About to call a fal serverless function from NodeJS'); - const result = await fal.run( - 'e300f60b-4a7c-44cd-871d-bea588ef43d6/jokes/add', - { - input: { - joke: 'fal serverless is cool, so the joke is on you!', + try { + const result = await getJoke(); + return { + props: { + ...result, }, - } + }; + } catch (error) { + return { + props: { + error: error.message, + }, + }; + } +} + +function Error(props) { + if (!props.error) { + return null; + } + return ( +
+ Error {props.error} +
); - console.log(result); - const random = await fal.run( - 'e300f60b-4a7c-44cd-871d-bea588ef43d6/jokes/get', - { - method: 'get', - } - ); - console.log(random); - return { - props: { - random, - result, - }, - }; } export function Index(props) { + const handleClick = async (e) => { + e.preventDefault(); + try { + const joke = await getJoke(); + console.log(joke); + } catch (e) { + console.log(e); + } + }; return ( -
-

- Hello fal serverless -

-

- This page can access fal serverless functions when - it's rendering. -

-

- Added joke with success?{' '} - {props.result.success.toString()} -

-

- Joke {props.random.joke} -

+
+
+

+ Hello fal-serverless +

+

+ This page can access fal-serverless functions when + it's rendering. +

+ + + + +

+ Here's a joke: {props.joke} +

+
); } diff --git a/apps/demo-app/services/generateImage.ts b/apps/demo-app/services/generateImage.ts index 54d5935..2ed4e27 100644 --- a/apps/demo-app/services/generateImage.ts +++ b/apps/demo-app/services/generateImage.ts @@ -18,12 +18,10 @@ fal.config({ export async function generateImage( input: GenerateImageInput ): Promise { - const result = await fal.run( - 'a51c0ca0-9011-4ff0-8dc1-2ac0b42a9fd0/generate', - { - input, - } - ); + const result = await fal.run('a51c0ca0-9011-4ff0-8dc1-2ac0b42a9fd0', { + path: '/generate', + input, + }); const data = result['raw_data']; return `data:image/jpg;base64,${data}`; } diff --git a/apps/demo-app/services/getJoke.ts b/apps/demo-app/services/getJoke.ts new file mode 100644 index 0000000..e4afad2 --- /dev/null +++ b/apps/demo-app/services/getJoke.ts @@ -0,0 +1,18 @@ +import * as fal from '@fal-ai/serverless-client'; + +fal.config({ + host: 'gateway.alpha.fal.ai', + credentials: { + userId: process.env.FAL_USER_ID || '', + keyId: process.env.FAL_KEY_ID || '', + keySecret: process.env.FAL_KEY_SECRET || '', + }, +}); + +export type GetJokeInput = { + language?: string; +}; + +export function getJoke(input?: GetJokeInput): Promise<{ joke: string }> { + return fal.run('fastapi_get_joke', { input }); +} diff --git a/libs/client/src/config.ts b/libs/client/src/config.ts index 891db65..403ae69 100644 --- a/libs/client/src/config.ts +++ b/libs/client/src/config.ts @@ -12,7 +12,7 @@ export type Config = { export type RequiredConfig = Required; const DEFAULT_CONFIG: Partial = { - host: 'https://gateway.shark.fal.ai', + host: 'gateway.shark.fal.ai', }; let configuration: RequiredConfig | undefined = undefined; @@ -33,7 +33,7 @@ export function config(config: Config) { */ export function getConfig(): RequiredConfig { if (typeof configuration === 'undefined') { - throw new Error('You must configure fal serverless first.'); + throw new Error('You must configure fal-serverless first.'); } return configuration; } diff --git a/libs/client/src/function.spec.ts b/libs/client/src/function.spec.ts new file mode 100644 index 0000000..2cbd87e --- /dev/null +++ b/libs/client/src/function.spec.ts @@ -0,0 +1,28 @@ +import { randomUUID } from 'crypto'; +import { config, getConfig } from './config'; +import { buildUrl } from './function'; + +config({ + host: 'gateway.alpha.fal.ai', + credentials: { + userId: 'github|123456', + keyId: 'a91ff3ca-71bc-4c8c-b400-859f6cbe804d', + keySecret: '0123456789abcdfeghijklmnopqrstuv', + }, +}); + +describe('The function test suite', () => { + it('should build the URL with a function UUIDv4', () => { + const { credentials } = getConfig(); + const id = randomUUID(); + const url = buildUrl(id); + expect(url).toMatch(`trigger/${credentials.userId}/${id}`); + }); + + it('should build the URL with a function alias', () => { + const { host } = getConfig(); + const alias = 'some-alias'; + const url = buildUrl(alias); + expect(url).toMatch(`${alias}.${host}`); + }); +}); diff --git a/libs/client/src/function.ts b/libs/client/src/function.ts index 091ee73..d0a5721 100644 --- a/libs/client/src/function.ts +++ b/libs/client/src/function.ts @@ -1,12 +1,22 @@ import fetch from 'cross-fetch'; import { getConfig } from './config'; import { getUserAgent, isBrowser } from './runtime'; +import { isUUIDv4 } from './utils'; /** * The function input and other configuration when running * the function, such as the HTTP method to use. */ type RunOptions = { + /** + * The path to the function, if any. Defaults to `/`. + */ + readonly path?: string; + + /** + * The function input. It will be submitted either as query params + * or the body payload, depending on the `method`. + */ readonly input?: Input; /** @@ -15,6 +25,35 @@ type RunOptions = { readonly method?: 'get' | 'post' | 'put' | 'delete'; }; +/** + * Builds the final url to run the function based on its `id` or alias and + * a the options from `RunOptions`. + * + * @private + * @param id the function id or alias + * @param options the run options + * @returns the final url to run the function + */ +export function buildUrl( + id: string, + options: RunOptions = {} +): string { + const { credentials, host } = getConfig(); + const method = (options.method ?? 'post').toLowerCase(); + const path = options.path ?? ''; + const params = + method === 'get' ? new URLSearchParams(options.input ?? {}) : undefined; + let queryParams = ''; + if (params) { + queryParams = `?${params.toString()}`; + } + if (isUUIDv4(id)) { + return `https://${host}/trigger/${credentials.userId}/${id}/${path}${queryParams}`; + } + const userId = credentials.userId.replace(/github\|/g, ''); + return `https://${userId}-${id}.${host}/${path}${queryParams}`; +} + /** * Runs a fal serverless function identified by its `id`. * TODO: expand documentation and provide examples @@ -24,31 +63,32 @@ type RunOptions = { */ export async function run( id: string, - options?: RunOptions + options: RunOptions = {} ): Promise { - const { credentials, host } = getConfig(); + const { credentials } = getConfig(); const method = (options.method ?? 'post').toLowerCase(); - const params = - method === 'get' ? new URLSearchParams(options.input ?? {}).toString() : ''; const userAgent = isBrowser ? {} : { 'User-Agent': getUserAgent() }; - const response = await fetch( - `${host}/trigger/${credentials.userId}/${id}${params}`, - { - method, - headers: { - 'X-Fal-Key-Id': credentials.keyId, - 'X-Fal-Key-Secret': credentials.keySecret, - Accept: 'application/json', - 'Content-Type': 'application/json', - ...userAgent, - }, - mode: 'cors', - body: - method !== 'get' && options.input - ? JSON.stringify(options.input) - : null, - } - ); + const response = await fetch(buildUrl(id, options), { + method, + headers: { + 'X-Fal-Key-Id': credentials.keyId, + 'X-Fal-Key-Secret': credentials.keySecret, + 'Content-Type': 'application/json', + ...userAgent, + }, + mode: 'cors', + body: + method !== 'get' && options.input + ? JSON.stringify(options.input) + : undefined, + }); + + const { status, statusText } = response; + if (status < 200 || status >= 300) { + // TODO better error type so handlers can differentiate + throw new Error(statusText); + } + // TODO move this elsewhere so it can be reused by websocket impl too const contentType = response.headers.get('Content-Type'); if (contentType?.includes('application/json')) { diff --git a/libs/client/src/index.ts b/libs/client/src/index.ts index 2db699a..094aed8 100644 --- a/libs/client/src/index.ts +++ b/libs/client/src/index.ts @@ -1,4 +1,4 @@ -export type { Credentials } from './config'; export { config } from './config'; -export type { FunctionExecution, ProgressEvent } from './function'; +export type { Credentials } from './config'; export { run } from './function'; +export type { FunctionExecution, ProgressEvent } from './function'; diff --git a/libs/client/src/utils.spec.ts b/libs/client/src/utils.spec.ts new file mode 100644 index 0000000..efc37e5 --- /dev/null +++ b/libs/client/src/utils.spec.ts @@ -0,0 +1,14 @@ +import { randomUUID } from 'crypto'; +import { isUUIDv4 } from './utils'; + +describe('The utils test suite', () => { + it('should match a valid v4 uuid', () => { + const id = randomUUID(); + expect(isUUIDv4(id)).toBe(true); + }); + + it('should not match invalid v4 id', () => { + const id = 'e726b886-e2c2-11ed-b5ea-0242ac120002'; + expect(isUUIDv4(id)).toBe(false); + }); +}); diff --git a/libs/client/src/utils.ts b/libs/client/src/utils.ts new file mode 100644 index 0000000..177860a --- /dev/null +++ b/libs/client/src/utils.ts @@ -0,0 +1,8 @@ +export function isUUIDv4(id: string): boolean { + return ( + typeof id === 'string' && + id.length === 36 && + id[14] === '4' && + ['8', '9', 'a', 'b'].includes(id[19]) + ); +} diff --git a/package-lock.json b/package-lock.json index 39f3796..0dc8a11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3179,6 +3179,126 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@next/swc-android-arm-eabi": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.1.1.tgz", + "integrity": "sha512-qnFCx1kT3JTWhWve4VkeWuZiyjG0b5T6J2iWuin74lORCupdrNukxkq9Pm+Z7PsatxuwVJMhjUoYz7H4cWzx2A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-android-arm64": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.1.1.tgz", + "integrity": "sha512-eCiZhTzjySubNqUnNkQCjU3Fh+ep3C6b5DCM5FKzsTH/3Gr/4Y7EiaPZKILbvnXmhWtKPIdcY6Zjx51t4VeTfA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.1.tgz", + "integrity": "sha512-9zRJSSIwER5tu9ADDkPw5rIZ+Np44HTXpYMr0rkM656IvssowPxmhK0rTreC1gpUCYwFsRbxarUJnJsTWiutPg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.1.tgz", + "integrity": "sha512-qWr9qEn5nrnlhB0rtjSdR00RRZEtxg4EGvicIipqZWEyayPxhUu6NwKiG8wZiYZCLfJ5KWr66PGSNeDMGlNaiA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-freebsd-x64": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.1.1.tgz", + "integrity": "sha512-UwP4w/NcQ7V/VJEj3tGVszgb4pyUCt3lzJfUhjDMUmQbzG9LDvgiZgAGMYH6L21MoyAATJQPDGiAMWAPKsmumA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm-gnueabihf": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.1.1.tgz", + "integrity": "sha512-CnsxmKHco9sosBs1XcvCXP845Db+Wx1G0qouV5+Gr+HT/ZlDYEWKoHVDgnJXLVEQzq4FmHddBNGbXvgqM1Gfkg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.1.1.tgz", + "integrity": "sha512-JfDq1eri5Dif+VDpTkONRd083780nsMCOKoFG87wA0sa4xL8LGcXIBAkUGIC1uVy9SMsr2scA9CySLD/i+Oqiw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.1.1.tgz", + "integrity": "sha512-GA67ZbDq2AW0CY07zzGt07M5b5Yaq5qUpFIoW3UFfjOPgb0Sqf3DAW7GtFMK1sF4ROHsRDMGQ9rnT0VM2dVfKA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-linux-x64-gnu": { "version": "13.1.1", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.1.1.tgz", @@ -3209,6 +3329,51 @@ "node": ">= 10" } }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.1.1.tgz", + "integrity": "sha512-pzUHOGrbgfGgPlOMx9xk3QdPJoRPU+om84hqVoe6u+E0RdwOG0Ho/2UxCgDqmvpUrMab1Deltlt6RqcXFpnigQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.1.1.tgz", + "integrity": "sha512-WeX8kVS46aobM9a7Xr/kEPcrTyiwJqQv/tbw6nhJ4fH9xNZ+cEcyPoQkwPo570dCOLz3Zo9S2q0E6lJ/EAUOBg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.1.1.tgz", + "integrity": "sha512-mVF0/3/5QAc5EGVnb8ll31nNvf3BWpPY4pBb84tk+BfQglWLqc5AC9q1Ht/YMWiEgs8ALNKEQ3GQnbY0bJF2Gg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -29665,6 +29830,54 @@ } } }, + "@next/swc-android-arm-eabi": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.1.1.tgz", + "integrity": "sha512-qnFCx1kT3JTWhWve4VkeWuZiyjG0b5T6J2iWuin74lORCupdrNukxkq9Pm+Z7PsatxuwVJMhjUoYz7H4cWzx2A==", + "optional": true + }, + "@next/swc-android-arm64": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.1.1.tgz", + "integrity": "sha512-eCiZhTzjySubNqUnNkQCjU3Fh+ep3C6b5DCM5FKzsTH/3Gr/4Y7EiaPZKILbvnXmhWtKPIdcY6Zjx51t4VeTfA==", + "optional": true + }, + "@next/swc-darwin-arm64": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.1.tgz", + "integrity": "sha512-9zRJSSIwER5tu9ADDkPw5rIZ+Np44HTXpYMr0rkM656IvssowPxmhK0rTreC1gpUCYwFsRbxarUJnJsTWiutPg==", + "optional": true + }, + "@next/swc-darwin-x64": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.1.tgz", + "integrity": "sha512-qWr9qEn5nrnlhB0rtjSdR00RRZEtxg4EGvicIipqZWEyayPxhUu6NwKiG8wZiYZCLfJ5KWr66PGSNeDMGlNaiA==", + "optional": true + }, + "@next/swc-freebsd-x64": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.1.1.tgz", + "integrity": "sha512-UwP4w/NcQ7V/VJEj3tGVszgb4pyUCt3lzJfUhjDMUmQbzG9LDvgiZgAGMYH6L21MoyAATJQPDGiAMWAPKsmumA==", + "optional": true + }, + "@next/swc-linux-arm-gnueabihf": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.1.1.tgz", + "integrity": "sha512-CnsxmKHco9sosBs1XcvCXP845Db+Wx1G0qouV5+Gr+HT/ZlDYEWKoHVDgnJXLVEQzq4FmHddBNGbXvgqM1Gfkg==", + "optional": true + }, + "@next/swc-linux-arm64-gnu": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.1.1.tgz", + "integrity": "sha512-JfDq1eri5Dif+VDpTkONRd083780nsMCOKoFG87wA0sa4xL8LGcXIBAkUGIC1uVy9SMsr2scA9CySLD/i+Oqiw==", + "optional": true + }, + "@next/swc-linux-arm64-musl": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.1.1.tgz", + "integrity": "sha512-GA67ZbDq2AW0CY07zzGt07M5b5Yaq5qUpFIoW3UFfjOPgb0Sqf3DAW7GtFMK1sF4ROHsRDMGQ9rnT0VM2dVfKA==", + "optional": true + }, "@next/swc-linux-x64-gnu": { "version": "13.1.1", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.1.1.tgz", @@ -29677,6 +29890,24 @@ "integrity": "sha512-CM9xnAQNIZ8zf/igbIT/i3xWbQZYaF397H+JroF5VMOCUleElaMdQLL5riJml8wUfPoN3dtfn2s4peSr3azz/g==", "optional": true }, + "@next/swc-win32-arm64-msvc": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.1.1.tgz", + "integrity": "sha512-pzUHOGrbgfGgPlOMx9xk3QdPJoRPU+om84hqVoe6u+E0RdwOG0Ho/2UxCgDqmvpUrMab1Deltlt6RqcXFpnigQ==", + "optional": true + }, + "@next/swc-win32-ia32-msvc": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.1.1.tgz", + "integrity": "sha512-WeX8kVS46aobM9a7Xr/kEPcrTyiwJqQv/tbw6nhJ4fH9xNZ+cEcyPoQkwPo570dCOLz3Zo9S2q0E6lJ/EAUOBg==", + "optional": true + }, + "@next/swc-win32-x64-msvc": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.1.1.tgz", + "integrity": "sha512-mVF0/3/5QAc5EGVnb8ll31nNvf3BWpPY4pBb84tk+BfQglWLqc5AC9q1Ht/YMWiEgs8ALNKEQ3GQnbY0bJF2Gg==", + "optional": true + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",