From 6edbf2948df1010110726071d33a91005f95920b Mon Sep 17 00:00:00 2001 From: Daniel Rochetti Date: Tue, 6 Aug 2024 09:09:40 -0700 Subject: [PATCH] feat(client): binary chunk streaming support (#77) * feat(client): binary chunk streaming support * fix: buffer update * fix: audio playing * fix: media resource setup * feat: allow streaming through the proxy * fix: legacy urls env * feat: streaming connection mode * chore: bump client alpha version * feat(proxy): enable response streaming when supported * fix: queue streaming * chore: deprecated endpoint id cleanup * fix: client tests * chore: demo page updates * chore: bump version for release --- .../demo-nextjs-app-router/app/queue/page.tsx | 9 +- libs/client/package.json | 2 +- libs/client/src/function.spec.ts | 13 -- libs/client/src/function.ts | 54 +++++--- libs/client/src/request.ts | 19 ++- libs/client/src/streaming.ts | 116 ++++++++++++---- libs/client/src/utils.spec.ts | 27 +--- libs/client/src/utils.ts | 9 -- libs/proxy/package.json | 2 +- libs/proxy/src/express.ts | 30 ++++- libs/proxy/src/index.ts | 14 +- libs/proxy/src/nextjs.ts | 31 ++++- libs/proxy/src/svelte.ts | 13 +- package-lock.json | 125 +++++++++--------- package.json | 2 +- 15 files changed, 281 insertions(+), 185 deletions(-) diff --git a/apps/demo-nextjs-app-router/app/queue/page.tsx b/apps/demo-nextjs-app-router/app/queue/page.tsx index e979728..944e41f 100644 --- a/apps/demo-nextjs-app-router/app/queue/page.tsx +++ b/apps/demo-nextjs-app-router/app/queue/page.tsx @@ -25,10 +25,15 @@ function Error(props: ErrorProps) { ); } +const DEFAULT_ENDPOINT_ID = 'fal-ai/fast-sdxl'; +const DEFAULT_INPUT = `{ + "prompt": "A beautiful sunset over the ocean" +}`; + export default function Home() { // Input state - const [endpointId, setEndpointId] = useState(''); - const [input, setInput] = useState('{}'); + const [endpointId, setEndpointId] = useState(DEFAULT_ENDPOINT_ID); + const [input, setInput] = useState(DEFAULT_INPUT); // Result state const [loading, setLoading] = useState(false); const [error, setError] = useState(null); diff --git a/libs/client/package.json b/libs/client/package.json index 6e979c5..49deeb9 100644 --- a/libs/client/package.json +++ b/libs/client/package.json @@ -1,7 +1,7 @@ { "name": "@fal-ai/serverless-client", "description": "The fal serverless JS/TS client", - "version": "0.14.0-alpha.3", + "version": "0.14.0", "license": "MIT", "repository": { "type": "git", diff --git a/libs/client/src/function.spec.ts b/libs/client/src/function.spec.ts index 6c47667..a204333 100644 --- a/libs/client/src/function.spec.ts +++ b/libs/client/src/function.spec.ts @@ -1,19 +1,6 @@ -import uuid from 'uuid-random'; import { buildUrl } from './function'; describe('The function test suite', () => { - it('should build the URL with a function UUIDv4', () => { - const id = uuid(); - const url = buildUrl(`12345/${id}`); - expect(url).toMatch(`trigger/12345/${id}`); - }); - - it('should build the URL with a function user-id-app-alias', () => { - const alias = '12345-some-alias'; - const url = buildUrl(alias); - expect(url).toMatch(`fal.run/12345/some-alias`); - }); - it('should build the URL with a function username/app-alias', () => { const alias = 'fal-ai/text-to-image'; const url = buildUrl(alias); diff --git a/libs/client/src/function.ts b/libs/client/src/function.ts index be10835..82d2b99 100644 --- a/libs/client/src/function.ts +++ b/libs/client/src/function.ts @@ -1,14 +1,13 @@ -import { getTemporaryAuthToken } from './auth'; import { dispatchRequest } from './request'; import { storageImpl } from './storage'; -import { FalStream } from './streaming'; +import { FalStream, StreamingConnectionMode } from './streaming'; import { CompletedQueueStatus, EnqueueResult, QueueStatus, RequestLog, } from './types'; -import { ensureAppIdFormat, isUUIDv4, isValidUrl, parseAppId } from './utils'; +import { ensureAppIdFormat, isValidUrl, parseAppId } from './utils'; /** * The function input and other configuration when running @@ -80,7 +79,6 @@ export function buildUrl( Object.keys(params).length > 0 ? `?${new URLSearchParams(params).toString()}` : ''; - const parts = id.split('/'); // if a fal url is passed, just use it if (isValidUrl(id)) { @@ -88,12 +86,6 @@ export function buildUrl( return `${url}${path}${queryParams}`; } - // TODO remove this after some time, fal.run should be preferred - if (parts.length === 2 && isUUIDv4(parts[1])) { - const host = 'gateway.shark.fal.ai'; - return `https://${host}/trigger/${id}/${path}${queryParams}`; - } - const appId = ensureAppIdFormat(id); const subdomain = options.subdomain ? `${options.subdomain}.` : ''; const url = `https://${subdomain}fal.run/${appId}/${path}`; @@ -199,6 +191,12 @@ type QueueSubscribeOptions = { } | { mode: 'streaming'; + + /** + * The connection mode to use for streaming updates. It defaults to `server`. + * Set to `client` if your server proxy doesn't support streaming. + */ + connectionMode?: StreamingConnectionMode; } ); @@ -228,6 +226,14 @@ type QueueStatusOptions = BaseQueueOptions & { logs?: boolean; }; +type QueueStatusStreamOptions = QueueStatusOptions & { + /** + * The connection mode to use for streaming updates. It defaults to `server`. + * Set to `client` if your server proxy doesn't support streaming. + */ + connectionMode?: StreamingConnectionMode; +}; + /** * Represents a request queue with methods for submitting requests, * checking their status, retrieving results, and subscribing to updates. @@ -263,7 +269,7 @@ interface Queue { */ streamStatus( endpointId: string, - options: QueueStatusOptions + options: QueueStatusStreamOptions ): Promise>; /** @@ -340,24 +346,26 @@ export const queue: Queue = { async streamStatus( endpointId: string, - { requestId, logs = false }: QueueStatusOptions + { requestId, logs = false, connectionMode }: QueueStatusStreamOptions ): Promise> { const appId = parseAppId(endpointId); const prefix = appId.namespace ? `${appId.namespace}/` : ''; - const token = await getTemporaryAuthToken(endpointId); + + const queryParams = { + logs: logs ? '1' : '0', + }; + const url = buildUrl(`${prefix}${appId.owner}/${appId.alias}`, { subdomain: 'queue', path: `/requests/${requestId}/status/stream`, + query: queryParams, }); - const queryParams = new URLSearchParams({ - fal_jwt_token: token, - logs: logs ? '1' : '0', - }); - - return new FalStream(`${url}?${queryParams}`, { - input: {}, + return new FalStream(endpointId, { + url, method: 'get', + connectionMode, + queryParams, }); }, @@ -375,6 +383,10 @@ export const queue: Queue = { const status = await queue.streamStatus(endpointId, { requestId, logs: options.logs, + connectionMode: + 'connectionMode' in options + ? (options.connectionMode as StreamingConnectionMode) + : undefined, }); const logs: RequestLog[] = []; if (timeout) { @@ -390,7 +402,7 @@ export const queue: Queue = { ); }, timeout); } - status.on('message', (data: QueueStatus) => { + status.on('data', (data: QueueStatus) => { if (options.onQueueUpdate) { // accumulate logs to match previous polling behavior if ( diff --git a/libs/client/src/request.ts b/libs/client/src/request.ts index 618eb7f..bf94f30 100644 --- a/libs/client/src/request.ts +++ b/libs/client/src/request.ts @@ -1,14 +1,20 @@ import { getConfig } from './config'; +import { ResponseHandler } from './response'; import { getUserAgent, isBrowser } from './runtime'; const isCloudflareWorkers = typeof navigator !== 'undefined' && navigator?.userAgent === 'Cloudflare-Workers'; +type RequestOptions = { + responseHandler?: ResponseHandler; +}; + export async function dispatchRequest( method: string, targetUrl: string, - input: Input + input: Input, + options: RequestOptions & RequestInit = {} ): Promise { const { credentials: credentialsValue, @@ -39,14 +45,21 @@ export async function dispatchRequest( ...userAgent, ...(headers ?? {}), } as HeadersInit; + + const { responseHandler: customResponseHandler, ...requestInit } = options; const response = await fetch(url, { + ...requestInit, method, - headers: requestHeaders, + headers: { + ...requestHeaders, + ...(requestInit.headers ?? {}), + }, ...(!isCloudflareWorkers && { mode: 'cors' }), body: method.toLowerCase() !== 'get' && input ? JSON.stringify(input) : undefined, }); - return await responseHandler(response); + const handleResponse = customResponseHandler ?? responseHandler; + return await handleResponse(response); } diff --git a/libs/client/src/streaming.ts b/libs/client/src/streaming.ts index cd8d76d..bcca90b 100644 --- a/libs/client/src/streaming.ts +++ b/libs/client/src/streaming.ts @@ -2,19 +2,33 @@ import { createParser } from 'eventsource-parser'; import { getTemporaryAuthToken } from './auth'; import { getConfig } from './config'; import { buildUrl } from './function'; +import { dispatchRequest } from './request'; import { ApiError, defaultResponseHandler } from './response'; import { storageImpl } from './storage'; +export type StreamingConnectionMode = 'client' | 'server'; + /** * The stream API options. It requires the API input and also * offers configuration options. */ type StreamOptions = { + /** + * The endpoint URL. If not provided, it will be generated from the + * `endpointId` and the `queryParams`. + */ + readonly url?: string; + /** * The API input payload. */ readonly input?: Input; + /** + * The query parameters to be sent with the request. + */ + readonly queryParams?: Record; + /** * The maximum time interval in milliseconds between stream chunks. Defaults to 15s. */ @@ -30,19 +44,37 @@ type StreamOptions = { * The HTTP method, defaults to `post`; */ readonly method?: 'get' | 'post' | 'put' | 'delete' | string; + + /** + * The content type the client accepts as response. + * By default this is set to `text/event-stream`. + */ + readonly accept?: string; + + /** + * The streaming connection mode. This is used to determine + * whether the streaming will be done from the browser itself (client) + * or through your own server, either when running on NodeJS or when + * using a proxy that supports streaming. + * + * It defaults to `server`. Set to `client` if your server proxy doesn't + * support streaming. + */ + readonly connectionMode?: StreamingConnectionMode; }; const EVENT_STREAM_TIMEOUT = 15 * 1000; -type FalStreamEventType = 'message' | 'error' | 'done'; +type FalStreamEventType = 'data' | 'error' | 'done'; -type EventHandler = (event: any) => void; +type EventHandler = (event: T) => void; /** * The class representing a streaming response. With t */ export class FalStream { // properties + endpointId: string; url: string; options: StreamOptions; @@ -58,8 +90,14 @@ export class FalStream { private abortController = new AbortController(); - constructor(url: string, options: StreamOptions) { - this.url = url; + constructor(endpointId: string, options: StreamOptions) { + this.endpointId = endpointId; + this.url = + options.url ?? + buildUrl(endpointId, { + path: '/stream', + query: options.queryParams, + }); this.options = options; this.donePromise = new Promise((resolve, reject) => { if (this.streamClosed) { @@ -84,20 +122,34 @@ export class FalStream { } private start = async () => { - const { url, options } = this; - const { input, method = 'post' } = options; - const { fetch = global.fetch } = getConfig(); + const { endpointId, options } = this; + const { input, method = 'post', connectionMode = 'server' } = options; try { - const response = await fetch(url, { - method: method.toUpperCase(), + if (connectionMode === 'client') { + // if we are in the browser, we need to get a temporary token + // to authenticate the request + const token = await getTemporaryAuthToken(endpointId); + const { fetch = global.fetch } = getConfig(); + const parsedUrl = new URL(this.url); + parsedUrl.searchParams.set('fal_jwt_token', token); + const response = await fetch(parsedUrl.toString(), { + method: method.toUpperCase(), + headers: { + accept: options.accept ?? 'text/event-stream', + 'content-type': 'application/json', + }, + body: input && method !== 'get' ? JSON.stringify(input) : undefined, + signal: this.abortController.signal, + }); + return await this.handleResponse(response); + } + return await dispatchRequest(method.toUpperCase(), this.url, input, { headers: { - accept: 'text/event-stream', - 'content-type': 'application/json', + accept: options.accept ?? 'text/event-stream', }, - body: input && method !== 'get' ? JSON.stringify(input) : undefined, + responseHandler: this.handleResponse, signal: this.abortController.signal, }); - this.handleResponse(response); } catch (error) { this.handleError(error); } @@ -127,6 +179,25 @@ export class FalStream { ); return; } + + // any response that is not a text/event-stream will be handled as a binary stream + if (response.headers.get('content-type') !== 'text/event-stream') { + const reader = body.getReader(); + const emitRawChunk = () => { + reader.read().then(({ done, value }) => { + if (done) { + this.emit('done', this.currentData); + return; + } + this.currentData = value as Output; + this.emit('data', value); + emitRawChunk(); + }); + }; + emitRawChunk(); + return; + } + const decoder = new TextDecoder('utf-8'); const reader = response.body.getReader(); @@ -138,7 +209,10 @@ export class FalStream { const parsedData = JSON.parse(data); this.buffer.push(parsedData); this.currentData = parsedData; - this.emit('message', parsedData); + this.emit('data', parsedData); + + // also emit 'message'for backwards compatibility + this.emit('message' as any, parsedData); } catch (e) { this.emit('error', e); } @@ -242,27 +316,19 @@ export class FalStream { * object as a result, that can be used to get partial results through either * `AsyncIterator` or through an event listener. * - * @param appId the app id, e.g. `fal-ai/llavav15-13b`. + * @param endpointId the endpoint id, e.g. `fal-ai/llavav15-13b`. * @param options the request options, including the input payload. * @returns the `FalStream` instance. */ export async function stream, Output = any>( - appId: string, + endpointId: string, options: StreamOptions ): Promise> { - const token = await getTemporaryAuthToken(appId); - const url = buildUrl(appId, { path: '/stream' }); - const input = options.input && options.autoUpload !== false ? await storageImpl.transformInput(options.input) : options.input; - - const queryParams = new URLSearchParams({ - fal_jwt_token: token, - }); - - return new FalStream(`${url}?${queryParams}`, { + return new FalStream(endpointId, { ...options, input: input as Input, }); diff --git a/libs/client/src/utils.spec.ts b/libs/client/src/utils.spec.ts index dcd6d58..ec9c3c5 100644 --- a/libs/client/src/utils.spec.ts +++ b/libs/client/src/utils.spec.ts @@ -1,22 +1,6 @@ -import uuid from 'uuid-random'; -import { ensureAppIdFormat, isUUIDv4, parseAppId } from './utils'; +import { ensureAppIdFormat, parseAppId } from './utils'; describe('The utils test suite', () => { - it('should match a valid v4 uuid', () => { - const id = uuid(); - expect(isUUIDv4(id)).toBe(true); - }); - - it('should not match invalid v4 id', () => { - const id = 'e726b886-e2c2-11ed-b5ea-0242ac120002'; - expect(isUUIDv4(id)).toBe(false); - }); - - it('shoud match match a legacy appOwner-appId format', () => { - const id = '12345-abcde-fgh'; - expect(ensureAppIdFormat(id)).toBe('12345/abcde-fgh'); - }); - it('shoud match a current appOwner/appId format', () => { const id = 'fal-ai/fast-sdxl'; expect(ensureAppIdFormat(id)).toBe(id); @@ -32,15 +16,6 @@ describe('The utils test suite', () => { expect(() => ensureAppIdFormat(id)).toThrowError(); }); - it('should parse a legacy app id', () => { - const id = '12345-abcde-fgh'; - const parsed = parseAppId(id); - expect(parsed).toEqual({ - owner: '12345', - alias: 'abcde-fgh', - }); - }); - it('should parse a current app id', () => { const id = 'fal-ai/fast-sdxl'; const parsed = parseAppId(id); diff --git a/libs/client/src/utils.ts b/libs/client/src/utils.ts index dd9117d..e307ba3 100644 --- a/libs/client/src/utils.ts +++ b/libs/client/src/utils.ts @@ -1,12 +1,3 @@ -export function isUUIDv4(id: string): boolean { - return ( - typeof id === 'string' && - id.length === 36 && - id[14] === '4' && - ['8', '9', 'a', 'b'].includes(id[19]) - ); -} - export function ensureAppIdFormat(id: string): string { const parts = id.split('/'); if (parts.length > 1) { diff --git a/libs/proxy/package.json b/libs/proxy/package.json index 2ce4d32..d5d55cb 100644 --- a/libs/proxy/package.json +++ b/libs/proxy/package.json @@ -1,6 +1,6 @@ { "name": "@fal-ai/serverless-proxy", - "version": "0.7.5", + "version": "0.8.0", "license": "MIT", "repository": { "type": "git", diff --git a/libs/proxy/src/express.ts b/libs/proxy/src/express.ts index 5afb765..a49b312 100644 --- a/libs/proxy/src/express.ts +++ b/libs/proxy/src/express.ts @@ -17,11 +17,37 @@ export const handler: RequestHandler = async (request, response, next) => { await handleRequest({ id: 'express', method: request.method, - respondWith: (status, data) => response.status(status).json(data), + getRequestBody: async () => JSON.stringify(request.body), getHeaders: () => request.headers, getHeader: (name) => request.headers[name], sendHeader: (name, value) => response.setHeader(name, value), - getBody: async () => JSON.stringify(request.body), + respondWith: (status, data) => response.status(status).json(data), + sendResponse: async (res) => { + if (res.body instanceof ReadableStream) { + const reader = res.body.getReader(); + const stream = async () => { + const { done, value } = await reader.read(); + if (done) { + response.end(); + return response; + } + response.write(value); + return await stream(); + }; + + return await stream().catch((error) => { + if (!response.headersSent) { + response.status(500).send(error.message); + } else { + response.end(); + } + }); + } + if (res.headers.get('content-type')?.includes('application/json')) { + return response.status(res.status).json(await res.json()); + } + return response.status(res.status).send(await res.text()); + }, }); next(); }; diff --git a/libs/proxy/src/index.ts b/libs/proxy/src/index.ts index c920f75..79d1fad 100644 --- a/libs/proxy/src/index.ts +++ b/libs/proxy/src/index.ts @@ -19,10 +19,11 @@ export interface ProxyBehavior { method: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any respondWith(status: number, data: string | any): ResponseType; + sendResponse(response: Response): Promise; getHeaders(): Record; getHeader(name: string): HeaderValue; sendHeader(name: string, value: string): void; - getBody(): Promise; + getRequestBody(): Promise; resolveApiKey?: () => Promise; } @@ -109,7 +110,7 @@ export async function handleRequest( body: behavior.method?.toUpperCase() === 'GET' ? undefined - : await behavior.getBody(), + : await behavior.getRequestBody(), }); // copy headers from fal to the proxied response @@ -119,12 +120,7 @@ export async function handleRequest( } }); - if (res.headers.get('content-type')?.includes('application/json')) { - const data = await res.json(); - return behavior.respondWith(res.status, data); - } - const data = await res.text(); - return behavior.respondWith(res.status, data); + return behavior.sendResponse(res); } export function fromHeaders( @@ -138,3 +134,5 @@ export function fromHeaders( }); return result; } + +export const responsePassthrough = (res: Response) => Promise.resolve(res); diff --git a/libs/proxy/src/nextjs.ts b/libs/proxy/src/nextjs.ts index 4f941b7..f978f78 100644 --- a/libs/proxy/src/nextjs.ts +++ b/libs/proxy/src/nextjs.ts @@ -1,6 +1,11 @@ import { NextResponse, type NextRequest } from 'next/server'; import type { NextApiHandler } from 'next/types'; -import { DEFAULT_PROXY_ROUTE, fromHeaders, handleRequest } from './index'; +import { + DEFAULT_PROXY_ROUTE, + fromHeaders, + handleRequest, + responsePassthrough, +} from './index'; /** * The default Next API route for the fal.ai client proxy. @@ -11,6 +16,8 @@ export const PROXY_ROUTE = DEFAULT_PROXY_ROUTE; * The Next API route handler for the fal.ai client proxy. * Use it with the /pages router in Next.js. * + * Note: the page routers proxy doesn't support streaming responses. + * * @param request the Next API request object. * @param response the Next API response object. * @returns a promise that resolves when the request is handled. @@ -19,11 +26,17 @@ export const handler: NextApiHandler = async (request, response) => { return handleRequest({ id: 'nextjs-page-router', method: request.method || 'POST', - respondWith: (status, data) => response.status(status).json(data), + getRequestBody: async () => JSON.stringify(request.body), getHeaders: () => request.headers, getHeader: (name) => request.headers[name], sendHeader: (name, value) => response.setHeader(name, value), - getBody: async () => JSON.stringify(request.body), + respondWith: (status, data) => response.status(status).json(data), + sendResponse: async (res) => { + if (res.headers.get('content-type')?.includes('application/json')) { + return response.status(res.status).json(await res.json()); + } + return response.status(res.status).send(await res.text()); + }, }); }; @@ -36,18 +49,22 @@ export const handler: NextApiHandler = async (request, response) => { async function routeHandler(request: NextRequest) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const responseHeaders: Record = {}; + + // check if response if from a streaming request + return await handleRequest({ id: 'nextjs-app-router', method: request.method, + getRequestBody: async () => request.text(), + getHeaders: () => fromHeaders(request.headers), + getHeader: (name) => request.headers.get(name), + sendHeader: (name, value) => (responseHeaders[name] = value), respondWith: (status, data) => NextResponse.json(data, { status, headers: responseHeaders, }), - getHeaders: () => fromHeaders(request.headers), - getHeader: (name) => request.headers.get(name), - sendHeader: (name, value) => (responseHeaders[name] = value), - getBody: async () => request.text(), + sendResponse: responsePassthrough, }); } diff --git a/libs/proxy/src/svelte.ts b/libs/proxy/src/svelte.ts index e820919..56dea0d 100644 --- a/libs/proxy/src/svelte.ts +++ b/libs/proxy/src/svelte.ts @@ -1,5 +1,5 @@ import { type RequestHandler } from '@sveltejs/kit'; -import { fromHeaders, handleRequest } from './index'; +import { fromHeaders, handleRequest, responsePassthrough } from './index'; type RequestHandlerParams = { /** @@ -28,16 +28,17 @@ export const createRequestHandler = ({ return await handleRequest({ id: 'svelte-app-router', method: request.method, + getRequestBody: async () => request.text(), + getHeaders: () => fromHeaders(request.headers), + getHeader: (name) => request.headers.get(name), + sendHeader: (name, value) => (responseHeaders[name] = value), + resolveApiKey: () => Promise.resolve(FAL_KEY), respondWith: (status, data) => new Response(JSON.stringify(data), { status, headers: responseHeaders, }), - getHeaders: () => fromHeaders(request.headers), - getHeader: (name) => request.headers.get(name), - sendHeader: (name, value) => (responseHeaders[name] = value), - getBody: async () => request.text(), - resolveApiKey: () => Promise.resolve(FAL_KEY), + sendResponse: responsePassthrough, }); }; return { diff --git a/package-lock.json b/package-lock.json index 87ad66c..07dc2d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "http-proxy": "^1.18.1", "http-proxy-middleware": "^2.0.6", "js-base64": "^3.7.5", - "next": "^14.0.3", + "next": "^14.2.5", "open": "^10.0.3", "ora": "^8.0.1", "react": "^18.2.0", @@ -4764,9 +4764,9 @@ } }, "node_modules/@next/env": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.3.tgz", - "integrity": "sha512-7xRqh9nMvP5xrW4/+L0jgRRX+HoNRGnfJpD+5Wq6/13j3dsdzxO3BCXn7D3hMqsDb+vjZnJq+vI7+EtgrYZTeA==" + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", + "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==" }, "node_modules/@next/eslint-plugin-next": { "version": "14.0.3", @@ -4798,9 +4798,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.3.tgz", - "integrity": "sha512-64JbSvi3nbbcEtyitNn2LEDS/hcleAFpHdykpcnrstITFlzFgB/bW0ER5/SJJwUPj+ZPY+z3e+1jAfcczRLVGw==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz", + "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==", "cpu": [ "arm64" ], @@ -4813,9 +4813,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.3.tgz", - "integrity": "sha512-RkTf+KbAD0SgYdVn1XzqE/+sIxYGB7NLMZRn9I4Z24afrhUpVJx6L8hsRnIwxz3ERE2NFURNliPjJ2QNfnWicQ==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz", + "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==", "cpu": [ "x64" ], @@ -4828,9 +4828,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.3.tgz", - "integrity": "sha512-3tBWGgz7M9RKLO6sPWC6c4pAw4geujSwQ7q7Si4d6bo0l6cLs4tmO+lnSwFp1Tm3lxwfMk0SgkJT7EdwYSJvcg==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", + "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", "cpu": [ "arm64" ], @@ -4843,9 +4843,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.3.tgz", - "integrity": "sha512-v0v8Kb8j8T23jvVUWZeA2D8+izWspeyeDGNaT2/mTHWp7+37fiNfL8bmBWiOmeumXkacM/AB0XOUQvEbncSnHA==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", + "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", "cpu": [ "arm64" ], @@ -4858,9 +4858,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.3.tgz", - "integrity": "sha512-VM1aE1tJKLBwMGtyBR21yy+STfl0MapMQnNrXkxeyLs0GFv/kZqXS5Jw/TQ3TSUnbv0QPDf/X8sDXuMtSgG6eg==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", + "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", "cpu": [ "x64" ], @@ -4873,9 +4873,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.3.tgz", - "integrity": "sha512-64EnmKy18MYFL5CzLaSuUn561hbO1Gk16jM/KHznYP3iCIfF9e3yULtHaMy0D8zbHfxset9LTOv6cuYKJgcOxg==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", + "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", "cpu": [ "x64" ], @@ -4888,9 +4888,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.3.tgz", - "integrity": "sha512-WRDp8QrmsL1bbGtsh5GqQ/KWulmrnMBgbnb+59qNTW1kVi1nG/2ndZLkcbs2GX7NpFLlToLRMWSQXmPzQm4tog==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", + "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", "cpu": [ "arm64" ], @@ -4903,9 +4903,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.3.tgz", - "integrity": "sha512-EKffQeqCrj+t6qFFhIFTRoqb2QwX1mU7iTOvMyLbYw3QtqTw9sMwjykyiMlZlrfm2a4fA84+/aeW+PMg1MjuTg==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", + "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", "cpu": [ "ia32" ], @@ -4918,9 +4918,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.3.tgz", - "integrity": "sha512-ERhKPSJ1vQrPiwrs15Pjz/rvDHZmkmvbf/BjPN/UCOI++ODftT0GtasDPi0j+y6PPJi5HsXw+dpRaXUaw4vjuQ==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", + "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", "cpu": [ "x64" ], @@ -7616,17 +7616,16 @@ } }, "node_modules/@swc/counter": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", - "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", - "devOptional": true, - "peer": true + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" }, "node_modules/@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", "dependencies": { + "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, @@ -10369,9 +10368,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001558", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001558.tgz", - "integrity": "sha512-/Et7DwLqpjS47JPEcz6VnxU9PwcIdVi0ciLXRWBQdj1XFye68pSQYpV0QtPTfUKWuOaEig+/Vez2l74eDc1tPQ==", + "version": "1.0.30001643", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", + "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==", "funding": [ { "type": "opencollective", @@ -15782,7 +15781,8 @@ "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true }, "node_modules/glob/node_modules/minimatch": { "version": "3.1.2", @@ -20608,17 +20608,17 @@ "dev": true }, "node_modules/next": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/next/-/next-14.0.3.tgz", - "integrity": "sha512-AbYdRNfImBr3XGtvnwOxq8ekVCwbFTv/UJoLwmaX89nk9i051AEY4/HAWzU0YpaTDw8IofUpmuIlvzWF13jxIw==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz", + "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==", "dependencies": { - "@next/env": "14.0.3", - "@swc/helpers": "0.5.2", + "@next/env": "14.2.5", + "@swc/helpers": "0.5.5", "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001406", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", "postcss": "8.4.31", - "styled-jsx": "5.1.1", - "watchpack": "2.4.0" + "styled-jsx": "5.1.1" }, "bin": { "next": "dist/bin/next" @@ -20627,18 +20627,19 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.0.3", - "@next/swc-darwin-x64": "14.0.3", - "@next/swc-linux-arm64-gnu": "14.0.3", - "@next/swc-linux-arm64-musl": "14.0.3", - "@next/swc-linux-x64-gnu": "14.0.3", - "@next/swc-linux-x64-musl": "14.0.3", - "@next/swc-win32-arm64-msvc": "14.0.3", - "@next/swc-win32-ia32-msvc": "14.0.3", - "@next/swc-win32-x64-msvc": "14.0.3" + "@next/swc-darwin-arm64": "14.2.5", + "@next/swc-darwin-x64": "14.2.5", + "@next/swc-linux-arm64-gnu": "14.2.5", + "@next/swc-linux-arm64-musl": "14.2.5", + "@next/swc-linux-x64-gnu": "14.2.5", + "@next/swc-linux-x64-musl": "14.2.5", + "@next/swc-win32-arm64-msvc": "14.2.5", + "@next/swc-win32-ia32-msvc": "14.2.5", + "@next/swc-win32-x64-msvc": "14.2.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -20647,6 +20648,9 @@ "@opentelemetry/api": { "optional": true }, + "@playwright/test": { + "optional": true + }, "sass": { "optional": true } @@ -30163,6 +30167,7 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" diff --git a/package.json b/package.json index 72848b9..7b5074e 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "http-proxy": "^1.18.1", "http-proxy-middleware": "^2.0.6", "js-base64": "^3.7.5", - "next": "^14.0.3", + "next": "^14.2.5", "open": "^10.0.3", "ora": "^8.0.1", "react": "^18.2.0",