diff --git a/.changeset/short-coins-sort.md b/.changeset/short-coins-sort.md new file mode 100644 index 000000000..8d4a79f99 --- /dev/null +++ b/.changeset/short-coins-sort.md @@ -0,0 +1,6 @@ +--- +'@vercel/edge-config': patch +'vercel-storage-integration-test-suite': patch +--- + +Refactor edge-config in preparation for Next.js specific entrypoint diff --git a/packages/edge-config/src/edge-config.ts b/packages/edge-config/src/edge-config.ts new file mode 100644 index 000000000..3acc0ecc0 --- /dev/null +++ b/packages/edge-config/src/edge-config.ts @@ -0,0 +1,471 @@ +import { readFile } from '@vercel/edge-config-fs'; +import { name as sdkName, version as sdkVersion } from '../package.json'; +import { + isEmptyKey, + ERRORS, + UnexpectedNetworkError, + parseConnectionString, +} from './utils'; +import type { + Connection, + EdgeConfigClient, + EdgeConfigItems, + EdgeConfigValue, + EmbeddedEdgeConfig, +} from './types'; +import { fetchWithCachedResponse } from './utils/fetch-with-cached-response'; +import { trace } from './utils/tracing'; + +export { setTracerProvider } from './utils/tracing'; + +export { + parseConnectionString, + type EdgeConfigClient, + type EdgeConfigItems, + type EdgeConfigValue, + type EmbeddedEdgeConfig, +}; + +const X_EDGE_CONFIG_SDK_HEADER = + typeof sdkName === 'string' && typeof sdkVersion === 'string' + ? `${sdkName}@${sdkVersion}` + : ''; + +type HeadersRecord = Record; + +const jsonParseCache = new Map(); + +const readFileTraced = trace(readFile, { name: 'readFile' }); +const jsonParseTraced = trace(JSON.parse, { name: 'JSON.parse' }); + +const privateEdgeConfigSymbol = Symbol.for('privateEdgeConfig'); + +const cachedJsonParseTraced = trace( + (edgeConfigId: string, content: string) => { + const cached = jsonParseCache.get(edgeConfigId); + if (cached) return cached; + + const parsed = jsonParseTraced(content) as unknown; + + // freeze the object to avoid mutations of the return value of a "get" call + // from affecting the return value of future "get" calls + jsonParseCache.set(edgeConfigId, Object.freeze(parsed)); + return parsed; + }, + { name: 'cached JSON.parse' }, +); + +/** + * Reads an Edge Config from the local file system. + * This is used at runtime on serverless functions. + */ +const getFileSystemEdgeConfig = trace( + async function getFileSystemEdgeConfig( + connectionType: Connection['type'], + connectionId: Connection['id'], + ): Promise { + // can't optimize non-vercel hosted edge configs + if (connectionType !== 'vercel') return null; + // can't use fs optimizations outside of lambda + if (!process.env.AWS_LAMBDA_FUNCTION_NAME) return null; + + try { + const content = await readFileTraced( + `/opt/edge-config/${connectionId}.json`, + 'utf-8', + ); + + return cachedJsonParseTraced(connectionId, content) as EmbeddedEdgeConfig; + } catch { + return null; + } + }, + { + name: 'getFileSystemEdgeConfig', + }, +); + +/** + * Will return an embedded Edge Config object from memory, + * but only when the `privateEdgeConfigSymbol` is in global scope. + */ +const getPrivateEdgeConfig = trace( + async function getPrivateEdgeConfig( + connectionId: Connection['id'], + ): Promise { + const privateEdgeConfig = Reflect.get( + globalThis, + privateEdgeConfigSymbol, + ) as + | { + get: (id: string) => Promise; + } + | undefined; + + if ( + typeof privateEdgeConfig === 'object' && + typeof privateEdgeConfig.get === 'function' + ) { + return privateEdgeConfig.get(connectionId); + } + + return null; + }, + { + name: 'getPrivateEdgeConfig', + }, +); + +/** + * Reads the Edge Config from a local provider, if available, + * to avoid Network requests. + */ +export async function getLocalEdgeConfig( + connectionType: Connection['type'], + connectionId: Connection['id'], + _fetchCache: EdgeConfigClientOptions['cache'], +): Promise { + const edgeConfig = + (await getPrivateEdgeConfig(connectionId)) || + (await getFileSystemEdgeConfig(connectionType, connectionId)); + + return edgeConfig; +} + +type GetConfigFunction = ( + fetchCache: EdgeConfigClientOptions['cache'], + staleIfError: EdgeConfigClientOptions['staleIfError'], +) => Promise; +const inMemoryEdgeConfigsGetterMap = new Map(); +function getOrCreateGetInMemoryEdgeConfigByConnection( + connectionString: string, +): GetConfigFunction { + const getConfig = inMemoryEdgeConfigsGetterMap.get(connectionString); + if (getConfig) return getConfig; + + const newGetConfig = (() => { + const connection = parseConnectionString(connectionString); + + if (!connection) + throw new Error( + '@vercel/edge-config: Invalid connection string provided', + ); + + const headersRecord: HeadersRecord = { + Authorization: `Bearer ${connection.token}`, + }; + + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- [@vercel/style-guide@5 migration] + if (typeof process !== 'undefined' && process.env.VERCEL_ENV) + headersRecord['x-edge-config-vercel-env'] = process.env.VERCEL_ENV; + + if (X_EDGE_CONFIG_SDK_HEADER) + headersRecord['x-edge-config-sdk'] = X_EDGE_CONFIG_SDK_HEADER; + + // Functions as cache to keep track of the Edge Config. + let embeddedEdgeConfigPromise: Promise | null = + null; + + // Promise that points to the most recent request. + // It'll ensure that subsequent calls won't make another fetch call, + // while one is still on-going. + // Will overwrite `embeddedEdgeConfigPromise` only when resolved. + let latestRequest: Promise | null = null; + + return trace( + ( + fetchCache: EdgeConfigClientOptions['cache'], + staleIfError: EdgeConfigClientOptions['staleIfError'], + ) => { + if (!latestRequest) { + const headers = new Headers(headersRecord); + if (typeof staleIfError === 'number' && staleIfError > 0) { + headers.set('cache-control', `stale-if-error=${staleIfError}`); + } else { + headers.delete('cache-control'); + } + + latestRequest = fetchWithCachedResponse( + `${connection.baseUrl}/items?version=${connection.version}`, + { + headers, + cache: fetchCache, + }, + ).then(async (res) => { + const digest = res.headers.get('x-edge-config-digest'); + let body: EdgeConfigValue | undefined; + + // We ignore all errors here and just proceed. + if (!res.ok) { + await consumeResponseBody(res); + body = res.cachedResponseBody as EdgeConfigValue | undefined; + if (!body) return null; + } else { + body = (await res.json()) as EdgeConfigItems; + } + + return { digest, items: body } as EmbeddedEdgeConfig; + }); + + // Once the request is resolved, we set the proper config to the promise + // such that the next call will return the resolved value. + latestRequest.then( + (resolved) => { + embeddedEdgeConfigPromise = Promise.resolve(resolved); + latestRequest = null; + }, + // Attach a `.catch` handler to this promise so that if it does throw, + // we don't get an unhandled promise rejection event. We unset the + // `latestRequest` so that the next call will make a new request. + () => { + embeddedEdgeConfigPromise = null; + latestRequest = null; + }, + ); + } + + if (!embeddedEdgeConfigPromise) { + // If the `embeddedEdgeConfigPromise` is `null`, it means that there's + // no previous request, so we'll set the `latestRequest` to the current + // request. + embeddedEdgeConfigPromise = latestRequest; + } + + return embeddedEdgeConfigPromise; + }, + { + name: 'getInMemoryEdgeConfig', + }, + ); + })(); + + inMemoryEdgeConfigsGetterMap.set(connectionString, newGetConfig); + + return newGetConfig; +} + +/** + * Returns a function to retrieve the entire Edge Config. + * It'll keep the fetched Edge Config in memory, making subsequent calls fast, + * while revalidating in the background. + */ +export async function getInMemoryEdgeConfig( + connectionString: string, + fetchCache: EdgeConfigClientOptions['cache'], + staleIfError: EdgeConfigClientOptions['staleIfError'], +): Promise { + const getConfig = + getOrCreateGetInMemoryEdgeConfigByConnection(connectionString); + return getConfig(fetchCache, staleIfError); +} + +/** + * Fetches an edge config item from the API + */ +export async function fetchEdgeConfigItem( + baseUrl: string, + key: string, + version: string, + consistentRead: undefined | boolean, + localHeaders: HeadersRecord, + fetchCache: EdgeConfigClientOptions['cache'], +): Promise { + if (isEmptyKey(key)) return undefined; + + const headers = new Headers(localHeaders); + if (consistentRead) { + addConsistentReadHeader(headers); + } + return fetchWithCachedResponse(`${baseUrl}/item/${key}?version=${version}`, { + headers, + cache: fetchCache, + }).then(async (res) => { + if (res.ok) return res.json(); + await consumeResponseBody(res); + + if (res.status === 401) throw new Error(ERRORS.UNAUTHORIZED); + if (res.status === 404) { + // if the x-edge-config-digest header is present, it means + // the edge config exists, but the item does not + if (res.headers.has('x-edge-config-digest')) return undefined; + // if the x-edge-config-digest header is not present, it means + // the edge config itself does not exist + throw new Error(ERRORS.EDGE_CONFIG_NOT_FOUND); + } + if (res.cachedResponseBody !== undefined) + return res.cachedResponseBody as T; + throw new UnexpectedNetworkError(res); + }); +} + +/** + * Determines if a key exists from the API + */ +export async function fetchEdgeConfigHas( + baseUrl: string, + key: string, + version: string, + consistentRead: undefined | boolean, + localHeaders: HeadersRecord, + fetchCache: EdgeConfigClientOptions['cache'], +): Promise { + const headers = new Headers(localHeaders); + if (consistentRead) { + addConsistentReadHeader(headers); + } + // this is a HEAD request anyhow, no need for fetchWithCachedResponse + return fetch(`${baseUrl}/item/${key}?version=${version}`, { + method: 'HEAD', + headers, + cache: fetchCache, + }).then((res) => { + if (res.status === 401) throw new Error(ERRORS.UNAUTHORIZED); + if (res.status === 404) { + // if the x-edge-config-digest header is present, it means + // the edge config exists, but the item does not + if (res.headers.has('x-edge-config-digest')) return false; + // if the x-edge-config-digest header is not present, it means + // the edge config itself does not exist + throw new Error(ERRORS.EDGE_CONFIG_NOT_FOUND); + } + if (res.ok) return true; + throw new UnexpectedNetworkError(res); + }); +} + +/** + * Fetches all or a list of edge config items from the API + */ +export async function fetchAllEdgeConfigItem( + baseUrl: string, + keys: undefined | (keyof T)[], + version: string, + consistentRead: undefined | boolean, + localHeaders: HeadersRecord, + fetchCache: EdgeConfigClientOptions['cache'], +): Promise { + let url = `${baseUrl}/items?version=${version}`; + if (keys) { + if (keys.length === 0) return Promise.resolve({} as T); + + const nonEmptyKeys = keys.filter( + (key) => typeof key === 'string' && !isEmptyKey(key), + ); + if (nonEmptyKeys.length === 0) return Promise.resolve({} as T); + + url += `&${new URLSearchParams( + nonEmptyKeys.map((key) => ['key', key] as [string, string]), + ).toString()}`; + } + + const headers = new Headers(localHeaders); + if (consistentRead) { + addConsistentReadHeader(headers); + } + + return fetchWithCachedResponse(url, { + headers, + cache: fetchCache, + }).then(async (res) => { + if (res.ok) return res.json(); + await consumeResponseBody(res); + + if (res.status === 401) throw new Error(ERRORS.UNAUTHORIZED); + // the /items endpoint never returns 404, so if we get a 404 + // it means the edge config itself did not exist + if (res.status === 404) throw new Error(ERRORS.EDGE_CONFIG_NOT_FOUND); + if (res.cachedResponseBody !== undefined) + return res.cachedResponseBody as T; + throw new UnexpectedNetworkError(res); + }); +} + +/** + * Fetches all or a list of edge config items from the API + */ +export async function fetchEdgeConfigTrace( + baseUrl: string, + version: string, + consistentRead: undefined | boolean, + localHeaders: HeadersRecord, + fetchCache: EdgeConfigClientOptions['cache'], +): Promise { + const headers = new Headers(localHeaders); + if (consistentRead) { + addConsistentReadHeader(headers); + } + + return fetchWithCachedResponse(`${baseUrl}/digest?version=${version}`, { + headers, + cache: fetchCache, + }).then(async (res) => { + if (res.ok) return res.json() as Promise; + await consumeResponseBody(res); + + if (res.cachedResponseBody !== undefined) + return res.cachedResponseBody as string; + throw new UnexpectedNetworkError(res); + }); +} + +/** + * Uses `MAX_SAFE_INTEGER` as minimum updated at timestamp to force + * a request to the origin. + */ +function addConsistentReadHeader(headers: Headers): void { + headers.set('x-edge-config-min-updated-at', `${Number.MAX_SAFE_INTEGER}`); +} + +/** + * This function reads the respone body + * + * Reading the response body serves two purposes + * + * 1) In Node.js it avoids memory leaks + * + * See https://github.com/nodejs/undici/blob/v5.21.2/README.md#garbage-collection + * See https://github.com/node-fetch/node-fetch/issues/83 + * + * 2) In Cloudflare it avoids running into a deadlock. They have a maximum number + * of concurrent fetches (which is documented). Concurrency counts until the + * body of a response is read. It is not uncommon to never read a response body + * (e.g. if you only care about the status code). This can lead to deadlock as + * fetches appear to never resolve. + * + * See https://developers.cloudflare.com/workers/platform/limits/#simultaneous-open-connections + */ +async function consumeResponseBody(res: Response): Promise { + await res.arrayBuffer(); +} + +export interface EdgeConfigClientOptions { + /** + * The stale-if-error response directive indicates that the cache can reuse a + * stale response when an upstream server generates an error, or when the error + * is generated locally - for example due to a connection error. + * + * Any response with a status code of 500, 502, 503, or 504 is considered an error. + * + * Pass a negative number, 0, or false to turn disable stale-if-error semantics. + * + * The time is supplied in seconds. Defaults to one week (`604800`). + */ + staleIfError?: number | false; + /** + * In development, a stale-while-revalidate cache is employed as the default caching strategy. + * + * This cache aims to deliver speedy Edge Config reads during development, though it comes + * at the cost of delayed visibility for updates to Edge Config. Typically, you may need to + * refresh twice to observe these changes as the stale value is replaced. + * + * This cache is not used in preview or production deployments as superior optimisations are applied there. + */ + disableDevelopmentCache?: boolean; + + /** + * Sets a `cache` option on the `fetch` call made by Edge Config. + * + * Unlike Next.js, this defaults to `no-store`, as you most likely want to use Edge Config dynamically. + */ + cache?: 'no-store' | 'force-cache'; +} diff --git a/packages/edge-config/src/index.ts b/packages/edge-config/src/index.ts index f617b064a..e4c072fd3 100644 --- a/packages/edge-config/src/index.ts +++ b/packages/edge-config/src/index.ts @@ -1,25 +1,29 @@ -import { readFile } from '@vercel/edge-config-fs'; import { name as sdkName, version as sdkVersion } from '../package.json'; import { assertIsKey, assertIsKeys, isEmptyKey, - ERRORS, - UnexpectedNetworkError, hasOwnProperty, parseConnectionString, pick, } from './utils'; import type { - Connection, EdgeConfigClient, EdgeConfigItems, EdgeConfigValue, EmbeddedEdgeConfig, EdgeConfigFunctionsOptions, } from './types'; -import { fetchWithCachedResponse } from './utils/fetch-with-cached-response'; import { trace } from './utils/tracing'; +import { + getInMemoryEdgeConfig, + getLocalEdgeConfig, + fetchEdgeConfigItem, + fetchEdgeConfigHas, + fetchAllEdgeConfigItem, + fetchEdgeConfigTrace, + type EdgeConfigClientOptions, +} from './edge-config'; export { setTracerProvider } from './utils/tracing'; @@ -31,253 +35,6 @@ export { type EmbeddedEdgeConfig, }; -const jsonParseCache = new Map(); - -const readFileTraced = trace(readFile, { name: 'readFile' }); -const jsonParseTraced = trace(JSON.parse, { name: 'JSON.parse' }); - -const privateEdgeConfigSymbol = Symbol.for('privateEdgeConfig'); - -const cachedJsonParseTraced = trace( - (edgeConfigId: string, content: string) => { - const cached = jsonParseCache.get(edgeConfigId); - if (cached) return cached; - - const parsed = jsonParseTraced(content) as unknown; - - // freeze the object to avoid mutations of the return value of a "get" call - // from affecting the return value of future "get" calls - jsonParseCache.set(edgeConfigId, Object.freeze(parsed)); - return parsed; - }, - { name: 'cached JSON.parse' }, -); - -/** - * Reads an Edge Config from the local file system. - * This is used at runtime on serverless functions. - */ -const getFileSystemEdgeConfig = trace( - async function getFileSystemEdgeConfig( - connection: Connection, - ): Promise { - // can't optimize non-vercel hosted edge configs - if (connection.type !== 'vercel') return null; - // can't use fs optimizations outside of lambda - if (!process.env.AWS_LAMBDA_FUNCTION_NAME) return null; - - try { - const content = await readFileTraced( - `/opt/edge-config/${connection.id}.json`, - 'utf-8', - ); - - return cachedJsonParseTraced( - connection.id, - content, - ) as EmbeddedEdgeConfig; - } catch { - return null; - } - }, - { - name: 'getFileSystemEdgeConfig', - }, -); - -/** - * Will return an embedded Edge Config object from memory, - * but only when the `privateEdgeConfigSymbol` is in global scope. - */ -const getPrivateEdgeConfig = trace( - async function getPrivateEdgeConfig( - connection: Connection, - ): Promise { - const privateEdgeConfig = Reflect.get( - globalThis, - privateEdgeConfigSymbol, - ) as - | { - get: (id: string) => Promise; - } - | undefined; - - if ( - typeof privateEdgeConfig === 'object' && - typeof privateEdgeConfig.get === 'function' - ) { - return privateEdgeConfig.get(connection.id); - } - - return null; - }, - { - name: 'getPrivateEdgeConfig', - }, -); - -/** - * Returns a function to retrieve the entire Edge Config. - * It'll keep the fetched Edge Config in memory, making subsequent calls fast, - * while revalidating in the background. - */ -function createGetInMemoryEdgeConfig( - shouldUseDevelopmentCache: boolean, - connection: Connection, - headers: Record, - fetchCache: EdgeConfigClientOptions['cache'], -): ( - localOptions?: EdgeConfigFunctionsOptions, -) => Promise { - // Functions as cache to keep track of the Edge Config. - let embeddedEdgeConfigPromise: Promise | null = - null; - - // Promise that points to the most recent request. - // It'll ensure that subsequent calls won't make another fetch call, - // while one is still on-going. - // Will overwrite `embeddedEdgeConfigPromise` only when resolved. - let latestRequest: Promise | null = null; - - return trace( - (localOptions) => { - if (localOptions?.consistentRead || !shouldUseDevelopmentCache) - return Promise.resolve(null); - - if (!latestRequest) { - latestRequest = fetchWithCachedResponse( - `${connection.baseUrl}/items?version=${connection.version}`, - { - headers: new Headers(headers), - cache: fetchCache, - }, - ).then(async (res) => { - const digest = res.headers.get('x-edge-config-digest'); - let body: EdgeConfigValue | undefined; - - // We ignore all errors here and just proceed. - if (!res.ok) { - await consumeResponseBody(res); - body = res.cachedResponseBody as EdgeConfigValue | undefined; - if (!body) return null; - } else { - body = (await res.json()) as EdgeConfigItems; - } - - return { digest, items: body } as EmbeddedEdgeConfig; - }); - - // Once the request is resolved, we set the proper config to the promise - // such that the next call will return the resolved value. - latestRequest.then( - (resolved) => { - embeddedEdgeConfigPromise = Promise.resolve(resolved); - latestRequest = null; - }, - // Attach a `.catch` handler to this promise so that if it does throw, - // we don't get an unhandled promise rejection event. We unset the - // `latestRequest` so that the next call will make a new request. - () => { - embeddedEdgeConfigPromise = null; - latestRequest = null; - }, - ); - } - - if (!embeddedEdgeConfigPromise) { - // If the `embeddedEdgeConfigPromise` is `null`, it means that there's - // no previous request, so we'll set the `latestRequest` to the current - // request. - embeddedEdgeConfigPromise = latestRequest; - } - - return embeddedEdgeConfigPromise; - }, - { - name: 'getInMemoryEdgeConfig', - }, - ); -} - -/** - * Uses `MAX_SAFE_INTEGER` as minimum updated at timestamp to force - * a request to the origin. - */ -function addConsistentReadHeader(headers: Headers): void { - headers.set('x-edge-config-min-updated-at', `${Number.MAX_SAFE_INTEGER}`); -} - -/** - * Reads the Edge Config from a local provider, if available, - * to avoid Network requests. - */ -async function getLocalEdgeConfig( - connection: Connection, - options?: EdgeConfigFunctionsOptions, -): Promise { - if (options?.consistentRead) return null; - - const edgeConfig = - (await getPrivateEdgeConfig(connection)) || - (await getFileSystemEdgeConfig(connection)); - - return edgeConfig; -} - -/** - * This function reads the respone body - * - * Reading the response body serves two purposes - * - * 1) In Node.js it avoids memory leaks - * - * See https://github.com/nodejs/undici/blob/v5.21.2/README.md#garbage-collection - * See https://github.com/node-fetch/node-fetch/issues/83 - * - * 2) In Cloudflare it avoids running into a deadlock. They have a maximum number - * of concurrent fetches (which is documented). Concurrency counts until the - * body of a response is read. It is not uncommon to never read a response body - * (e.g. if you only care about the status code). This can lead to deadlock as - * fetches appear to never resolve. - * - * See https://developers.cloudflare.com/workers/platform/limits/#simultaneous-open-connections - */ -async function consumeResponseBody(res: Response): Promise { - await res.arrayBuffer(); -} - -interface EdgeConfigClientOptions { - /** - * The stale-if-error response directive indicates that the cache can reuse a - * stale response when an upstream server generates an error, or when the error - * is generated locally - for example due to a connection error. - * - * Any response with a status code of 500, 502, 503, or 504 is considered an error. - * - * Pass a negative number, 0, or false to turn disable stale-if-error semantics. - * - * The time is supplied in seconds. Defaults to one week (`604800`). - */ - staleIfError?: number | false; - /** - * In development, a stale-while-revalidate cache is employed as the default caching strategy. - * - * This cache aims to deliver speedy Edge Config reads during development, though it comes - * at the cost of delayed visibility for updates to Edge Config. Typically, you may need to - * refresh twice to observe these changes as the stale value is replaced. - * - * This cache is not used in preview or production deployments as superior optimisations are applied there. - */ - disableDevelopmentCache?: boolean; - - /** - * Sets a `cache` option on the `fetch` call made by Edge Config. - * - * Unlike Next.js, this defaults to `no-store`, as you most likely want to use Edge Config dynamically. - */ - cache?: 'no-store' | 'force-cache'; -} - /** * Create an Edge Config client. * @@ -334,27 +91,33 @@ export const createClient = trace( process.env.NODE_ENV === 'development' && process.env.EDGE_CONFIG_DISABLE_DEVELOPMENT_SWR !== '1'; - const getInMemoryEdgeConfig = createGetInMemoryEdgeConfig( - shouldUseDevelopmentCache, - connection, - headers, - fetchCache, - ); - const api: Omit = { get: trace( async function get( key: string, localOptions?: EdgeConfigFunctionsOptions, ): Promise { - const localEdgeConfig = - (await getInMemoryEdgeConfig(localOptions)) || - (await getLocalEdgeConfig(connection, localOptions)); - assertIsKey(key); - if (isEmptyKey(key)) return undefined; + + let localEdgeConfig: EmbeddedEdgeConfig | null = null; + if (localOptions?.consistentRead) { + // fall through to fetching + } else if (shouldUseDevelopmentCache) { + localEdgeConfig = await getInMemoryEdgeConfig( + connectionString, + fetchCache, + options.staleIfError, + ); + } else { + localEdgeConfig = await getLocalEdgeConfig( + connection.type, + connection.id, + fetchCache, + ); + } if (localEdgeConfig) { + if (isEmptyKey(key)) return undefined; // We need to return a clone of the value so users can't modify // our original value, and so the reference changes. // @@ -362,33 +125,14 @@ export const createClient = trace( return Promise.resolve(localEdgeConfig.items[key] as T); } - const localHeaders = new Headers(headers); - if (localOptions?.consistentRead) - addConsistentReadHeader(localHeaders); - - return fetchWithCachedResponse( - `${baseUrl}/item/${key}?version=${version}`, - { - headers: localHeaders, - cache: fetchCache, - }, - ).then(async (res) => { - if (res.ok) return res.json(); - await consumeResponseBody(res); - - if (res.status === 401) throw new Error(ERRORS.UNAUTHORIZED); - if (res.status === 404) { - // if the x-edge-config-digest header is present, it means - // the edge config exists, but the item does not - if (res.headers.has('x-edge-config-digest')) return undefined; - // if the x-edge-config-digest header is not present, it means - // the edge config itself does not exist - throw new Error(ERRORS.EDGE_CONFIG_NOT_FOUND); - } - if (res.cachedResponseBody !== undefined) - return res.cachedResponseBody as T; - throw new UnexpectedNetworkError(res); - }); + return fetchEdgeConfigItem( + baseUrl, + key, + version, + localOptions?.consistentRead, + headers, + fetchCache, + ); }, { name: 'get', isVerboseTrace: false, attributes: { edgeConfigId } }, ), @@ -397,39 +141,39 @@ export const createClient = trace( key, localOptions?: EdgeConfigFunctionsOptions, ): Promise { - const localEdgeConfig = - (await getInMemoryEdgeConfig(localOptions)) || - (await getLocalEdgeConfig(connection, localOptions)); - assertIsKey(key); if (isEmptyKey(key)) return false; + let localEdgeConfig: EmbeddedEdgeConfig | null = null; + + if (localOptions?.consistentRead) { + // fall through to fetching + } else if (shouldUseDevelopmentCache) { + localEdgeConfig = await getInMemoryEdgeConfig( + connectionString, + fetchCache, + options.staleIfError, + ); + } else { + localEdgeConfig = await getLocalEdgeConfig( + connection.type, + connection.id, + fetchCache, + ); + } + if (localEdgeConfig) { return Promise.resolve(hasOwnProperty(localEdgeConfig.items, key)); } - const localHeaders = new Headers(headers); - if (localOptions?.consistentRead) - addConsistentReadHeader(localHeaders); - - // this is a HEAD request anyhow, no need for fetchWithCachedResponse - return fetch(`${baseUrl}/item/${key}?version=${version}`, { - method: 'HEAD', - headers: localHeaders, - cache: fetchCache, - }).then((res) => { - if (res.status === 401) throw new Error(ERRORS.UNAUTHORIZED); - if (res.status === 404) { - // if the x-edge-config-digest header is present, it means - // the edge config exists, but the item does not - if (res.headers.has('x-edge-config-digest')) return false; - // if the x-edge-config-digest header is not present, it means - // the edge config itself does not exist - throw new Error(ERRORS.EDGE_CONFIG_NOT_FOUND); - } - if (res.ok) return true; - throw new UnexpectedNetworkError(res); - }); + return fetchEdgeConfigHas( + baseUrl, + key, + version, + localOptions?.consistentRead, + headers, + fetchCache, + ); }, { name: 'has', isVerboseTrace: false, attributes: { edgeConfigId } }, ), @@ -438,58 +182,44 @@ export const createClient = trace( keys?: (keyof T)[], localOptions?: EdgeConfigFunctionsOptions, ): Promise { - const localEdgeConfig = - (await getInMemoryEdgeConfig(localOptions)) || - (await getLocalEdgeConfig(connection, localOptions)); + if (keys) { + assertIsKeys(keys); + } + + let localEdgeConfig: EmbeddedEdgeConfig | null = null; + + if (localOptions?.consistentRead) { + // fall through to fetching + } else if (shouldUseDevelopmentCache) { + localEdgeConfig = await getInMemoryEdgeConfig( + connectionString, + fetchCache, + options.staleIfError, + ); + } else { + localEdgeConfig = await getLocalEdgeConfig( + connection.type, + connection.id, + fetchCache, + ); + } if (localEdgeConfig) { if (keys === undefined) { return Promise.resolve(localEdgeConfig.items as T); } - assertIsKeys(keys); return Promise.resolve(pick(localEdgeConfig.items, keys) as T); } - if (Array.isArray(keys)) assertIsKeys(keys); - - const search = Array.isArray(keys) - ? new URLSearchParams( - keys - .filter((key) => typeof key === 'string' && !isEmptyKey(key)) - .map((key) => ['key', key] as [string, string]), - ).toString() - : null; - - // empty search keys array was given, - // so skip the request and return an empty object - if (search === '') return Promise.resolve({} as T); - - const localHeaders = new Headers(headers); - if (localOptions?.consistentRead) - addConsistentReadHeader(localHeaders); - - return fetchWithCachedResponse( - `${baseUrl}/items?version=${version}${ - search === null ? '' : `&${search}` - }`, - { - headers: localHeaders, - cache: fetchCache, - }, - ).then(async (res) => { - if (res.ok) return res.json(); - await consumeResponseBody(res); - - if (res.status === 401) throw new Error(ERRORS.UNAUTHORIZED); - // the /items endpoint never returns 404, so if we get a 404 - // it means the edge config itself did not exist - if (res.status === 404) - throw new Error(ERRORS.EDGE_CONFIG_NOT_FOUND); - if (res.cachedResponseBody !== undefined) - return res.cachedResponseBody as T; - throw new UnexpectedNetworkError(res); - }); + return fetchAllEdgeConfigItem( + baseUrl, + keys, + version, + localOptions?.consistentRead, + headers, + fetchCache, + ); }, { name: 'getAll', isVerboseTrace: false, attributes: { edgeConfigId } }, ), @@ -497,32 +227,35 @@ export const createClient = trace( async function digest( localOptions?: EdgeConfigFunctionsOptions, ): Promise { - const localEdgeConfig = - (await getInMemoryEdgeConfig(localOptions)) || - (await getLocalEdgeConfig(connection, localOptions)); + let localEdgeConfig: EmbeddedEdgeConfig | null = null; + + if (localOptions?.consistentRead) { + // fall through to fetching + } else if (shouldUseDevelopmentCache) { + localEdgeConfig = await getInMemoryEdgeConfig( + connectionString, + fetchCache, + options.staleIfError, + ); + } else { + localEdgeConfig = await getLocalEdgeConfig( + connection.type, + connection.id, + fetchCache, + ); + } if (localEdgeConfig) { return Promise.resolve(localEdgeConfig.digest); } - const localHeaders = new Headers(headers); - if (localOptions?.consistentRead) - addConsistentReadHeader(localHeaders); - - return fetchWithCachedResponse( - `${baseUrl}/digest?version=${version}`, - { - headers: localHeaders, - cache: fetchCache, - }, - ).then(async (res) => { - if (res.ok) return res.json() as Promise; - await consumeResponseBody(res); - - if (res.cachedResponseBody !== undefined) - return res.cachedResponseBody as string; - throw new UnexpectedNetworkError(res); - }); + return fetchEdgeConfigTrace( + baseUrl, + version, + localOptions?.consistentRead, + headers, + fetchCache, + ); }, { name: 'digest', isVerboseTrace: false, attributes: { edgeConfigId } }, ), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16104a3b7..94f75829d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,7 +37,7 @@ importers: version: 0.3.7 ts-jest: specifier: 29.2.6 - version: 29.2.6(@babel/core@7.23.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.2))(jest@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)))(typescript@5.7.3) + version: 29.2.6(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)))(typescript@5.7.3) turbo: specifier: 2.4.4 version: 2.4.4 @@ -92,7 +92,7 @@ importers: version: 29.7.0(bufferutil@4.0.8) ts-jest: specifier: 29.2.6 - version: 29.2.6(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(esbuild@0.25.0)(jest@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)))(typescript@5.7.3) + version: 29.2.6(@babel/core@7.23.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.2))(esbuild@0.25.0)(jest@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)))(typescript@5.7.3) tsconfig: specifier: workspace:* version: link:../../tooling/tsconfig @@ -144,7 +144,7 @@ importers: version: 3.5.2 ts-jest: specifier: 29.2.6 - version: 29.2.6(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(esbuild@0.25.0)(jest@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)))(typescript@5.7.3) + version: 29.2.6(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)))(typescript@5.7.3) tsconfig: specifier: workspace:* version: link:../../tooling/tsconfig @@ -370,8 +370,8 @@ importers: specifier: ^2.1.3 version: 2.1.3 next: - specifier: ^15.2.0 - version: 15.2.0(@babel/core@7.26.0)(@opentelemetry/api@1.7.0)(@playwright/test@1.55.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: 15.2.5 + version: 15.2.5(@babel/core@7.23.9)(@opentelemetry/api@1.7.0)(@playwright/test@1.55.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) postcss: specifier: ^8.5.3 version: 8.5.3 @@ -1422,8 +1422,8 @@ packages: '@neondatabase/serverless@0.10.4': resolution: {integrity: sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA==} - '@next/env@15.2.0': - resolution: {integrity: sha512-eMgJu1RBXxxqqnuRJQh5RozhskoNUDHBFybvi+Z+yK9qzKeG7dadhv/Vp1YooSZmCnegf7JxWuapV77necLZNA==} + '@next/env@15.2.5': + resolution: {integrity: sha512-uWkCf9C8wKTyQjqrNk+BA7eL3LOQdhL+xlmJUf2O85RM4lbzwBwot3Sqv2QGe/RGnc3zysIf1oJdtq9S00pkmQ==} '@next/eslint-plugin-next@14.2.23': resolution: {integrity: sha512-efRC7m39GoiU1fXZRgGySqYbQi6ZyLkuGlvGst7IwkTTczehQTJA/7PoMg4MMjUZvZEGpiSEu+oJBAjPawiC3Q==} @@ -1431,50 +1431,50 @@ packages: '@next/eslint-plugin-next@15.2.0': resolution: {integrity: sha512-jHFUG2OwmAuOASqq253RAEG/5BYcPHn27p1NoWZDCf4OdvdK0yRYWX92YKkL+Mk2s+GyJrmd/GATlL5b2IySpw==} - '@next/swc-darwin-arm64@15.2.0': - resolution: {integrity: sha512-rlp22GZwNJjFCyL7h5wz9vtpBVuCt3ZYjFWpEPBGzG712/uL1bbSkS675rVAUCRZ4hjoTJ26Q7IKhr5DfJrHDA==} + '@next/swc-darwin-arm64@15.2.5': + resolution: {integrity: sha512-4OimvVlFTbgzPdA0kh8A1ih6FN9pQkL4nPXGqemEYgk+e7eQhsst/p35siNNqA49eQA6bvKZ1ASsDtu9gtXuog==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.2.0': - resolution: {integrity: sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA==} + '@next/swc-darwin-x64@15.2.5': + resolution: {integrity: sha512-ohzRaE9YbGt1ctE0um+UGYIDkkOxHV44kEcHzLqQigoRLaiMtZzGrA11AJh2Lu0lv51XeiY1ZkUvkThjkVNBMA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.2.0': - resolution: {integrity: sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ==} + '@next/swc-linux-arm64-gnu@15.2.5': + resolution: {integrity: sha512-FMSdxSUt5bVXqqOoZCc/Seg4LQep9w/fXTazr/EkpXW2Eu4IFI9FD7zBDlID8TJIybmvKk7mhd9s+2XWxz4flA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.2.0': - resolution: {integrity: sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw==} + '@next/swc-linux-arm64-musl@15.2.5': + resolution: {integrity: sha512-4ZNKmuEiW5hRKkGp2HWwZ+JrvK4DQLgf8YDaqtZyn7NYdl0cHfatvlnLFSWUayx9yFAUagIgRGRk8pFxS8Qniw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.2.0': - resolution: {integrity: sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q==} + '@next/swc-linux-x64-gnu@15.2.5': + resolution: {integrity: sha512-bE6lHQ9GXIf3gCDE53u2pTl99RPZW5V1GLHSRMJ5l/oB/MT+cohu9uwnCK7QUph2xIOu2a6+27kL0REa/kqwZw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.2.0': - resolution: {integrity: sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ==} + '@next/swc-linux-x64-musl@15.2.5': + resolution: {integrity: sha512-y7EeQuSkQbTAkCEQnJXm1asRUuGSWAchGJ3c+Qtxh8LVjXleZast8Mn/rL7tZOm7o35QeIpIcid6ufG7EVTTcA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.2.0': - resolution: {integrity: sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw==} + '@next/swc-win32-arm64-msvc@15.2.5': + resolution: {integrity: sha512-gQMz0yA8/dskZM2Xyiq2FRShxSrsJNha40Ob/M2n2+JGRrZ0JwTVjLdvtN6vCxuq4ByhOd4a9qEf60hApNR2gQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.2.0': - resolution: {integrity: sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw==} + '@next/swc-win32-x64-msvc@15.2.5': + resolution: {integrity: sha512-tBDNVUcI7U03+3oMvJ11zrtVin5p0NctiuKmTGyaTIEAVj9Q77xukLXGXRnWxKRIIdFG4OTA2rUVGZDYOwgmAA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -4207,8 +4207,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@15.2.0: - resolution: {integrity: sha512-VaiM7sZYX8KIAHBrRGSFytKknkrexNfGb8GlG6e93JqueCspuGte8i4ybn8z4ww1x3f2uzY4YpTaBEW4/hvsoQ==} + next@15.2.5: + resolution: {integrity: sha512-LlqS8ljc7RWR3riUwxB5+14v7ULAa5EuLUyarD/sFgXPd6Hmmscg8DXcu9hDdh5atybrIDVBrFhjDpRIQo/4pQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -6973,7 +6973,7 @@ snapshots: dependencies: '@types/pg': 8.11.6 - '@next/env@15.2.0': {} + '@next/env@15.2.5': {} '@next/eslint-plugin-next@14.2.23': dependencies: @@ -6984,28 +6984,28 @@ snapshots: dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.2.0': + '@next/swc-darwin-arm64@15.2.5': optional: true - '@next/swc-darwin-x64@15.2.0': + '@next/swc-darwin-x64@15.2.5': optional: true - '@next/swc-linux-arm64-gnu@15.2.0': + '@next/swc-linux-arm64-gnu@15.2.5': optional: true - '@next/swc-linux-arm64-musl@15.2.0': + '@next/swc-linux-arm64-musl@15.2.5': optional: true - '@next/swc-linux-x64-gnu@15.2.0': + '@next/swc-linux-x64-gnu@15.2.5': optional: true - '@next/swc-linux-x64-musl@15.2.0': + '@next/swc-linux-x64-musl@15.2.5': optional: true - '@next/swc-win32-arm64-msvc@15.2.0': + '@next/swc-win32-arm64-msvc@15.2.5': optional: true - '@next/swc-win32-x64-msvc@15.2.0': + '@next/swc-win32-x64-msvc@15.2.5': optional: true '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': @@ -7255,10 +7255,10 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.0 - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.7.3))(eslint@8.56.0)(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@8.25.0(eslint@8.56.0)(typescript@5.7.3))(eslint@8.56.0)(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.7.3) + '@typescript-eslint/parser': 8.25.0(eslint@8.56.0)(typescript@5.7.3) '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/type-utils': 6.21.0(eslint@8.56.0)(typescript@5.7.3) '@typescript-eslint/utils': 6.21.0(eslint@8.56.0)(typescript@5.7.3) @@ -7470,7 +7470,7 @@ snapshots: '@babel/core': 7.23.9 '@babel/eslint-parser': 7.23.10(@babel/core@7.23.9)(eslint@8.56.0) '@rushstack/eslint-patch': 1.7.2 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.7.3))(eslint@8.56.0)(typescript@5.7.3) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@8.25.0(eslint@8.56.0)(typescript@5.7.3))(eslint@8.56.0)(typescript@5.7.3) '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.7.3) eslint-config-prettier: 9.1.0(eslint@8.56.0) eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1) @@ -7986,8 +7986,7 @@ snapshots: caniuse-lite@1.0.30001701: {} - caniuse-lite@1.0.30001745: - optional: true + caniuse-lite@1.0.30001745: {} chalk@2.4.2: dependencies: @@ -8676,7 +8675,7 @@ snapshots: debug: 4.3.7 enhanced-resolve: 5.15.0 eslint: 8.56.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.7.3))(eslint-plugin-import@2.29.1)(eslint@8.56.0))(eslint@8.56.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.7.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) fast-glob: 3.3.2 get-tsconfig: 4.7.2 @@ -8714,7 +8713,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.7.3))(eslint-plugin-import@2.29.1)(eslint@8.56.0))(eslint@8.56.0): + eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -8741,7 +8740,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.7.3))(eslint-plugin-import@2.29.1)(eslint@8.56.0))(eslint@8.56.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) hasown: 2.0.1 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -8792,7 +8791,7 @@ snapshots: '@typescript-eslint/utils': 5.62.0(eslint@8.56.0)(typescript@5.7.3) eslint: 8.56.0 optionalDependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.7.3))(eslint@8.56.0)(typescript@5.7.3) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@8.25.0(eslint@8.56.0)(typescript@5.7.3))(eslint@8.56.0)(typescript@5.7.3) jest: 29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)) transitivePeerDependencies: - supports-color @@ -10568,26 +10567,26 @@ snapshots: natural-compare@1.4.0: {} - next@15.2.0(@babel/core@7.26.0)(@opentelemetry/api@1.7.0)(@playwright/test@1.55.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.2.5(@babel/core@7.23.9)(@opentelemetry/api@1.7.0)(@playwright/test@1.55.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - '@next/env': 15.2.0 + '@next/env': 15.2.5 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 - caniuse-lite: 1.0.30001701 + caniuse-lite: 1.0.30001745 postcss: 8.4.31 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.0.0) + styled-jsx: 5.1.6(@babel/core@7.23.9)(react@19.0.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.2.0 - '@next/swc-darwin-x64': 15.2.0 - '@next/swc-linux-arm64-gnu': 15.2.0 - '@next/swc-linux-arm64-musl': 15.2.0 - '@next/swc-linux-x64-gnu': 15.2.0 - '@next/swc-linux-x64-musl': 15.2.0 - '@next/swc-win32-arm64-msvc': 15.2.0 - '@next/swc-win32-x64-msvc': 15.2.0 + '@next/swc-darwin-arm64': 15.2.5 + '@next/swc-darwin-x64': 15.2.5 + '@next/swc-linux-arm64-gnu': 15.2.5 + '@next/swc-linux-arm64-musl': 15.2.5 + '@next/swc-linux-x64-gnu': 15.2.5 + '@next/swc-linux-x64-musl': 15.2.5 + '@next/swc-win32-arm64-msvc': 15.2.5 + '@next/swc-win32-x64-msvc': 15.2.5 '@opentelemetry/api': 1.7.0 '@playwright/test': 1.55.1 sharp: 0.33.5 @@ -11557,12 +11556,12 @@ snapshots: strip-json-comments@3.1.1: {} - styled-jsx@5.1.6(@babel/core@7.26.0)(react@19.0.0): + styled-jsx@5.1.6(@babel/core@7.23.9)(react@19.0.0): dependencies: client-only: 0.0.1 react: 19.0.0 optionalDependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.23.9 sucrase@3.35.0: dependencies: @@ -11718,7 +11717,7 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.26.0) esbuild: 0.24.2 - ts-jest@29.2.6(@babel/core@7.23.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.2))(jest@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)))(typescript@5.7.3): + ts-jest@29.2.6(@babel/core@7.23.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.2))(esbuild@0.25.0)(jest@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)))(typescript@5.7.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -11736,8 +11735,9 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.23.2) + esbuild: 0.25.0 - ts-jest@29.2.6(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(esbuild@0.25.0)(jest@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)))(typescript@5.7.3): + ts-jest@29.2.6(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@22.13.5)(ts-node@10.9.2(@types/node@22.13.5)(typescript@5.7.3)))(typescript@5.7.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -11755,7 +11755,6 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.26.0) - esbuild: 0.25.0 ts-node@10.9.2(@types/node@22.10.7)(typescript@5.7.3): dependencies: diff --git a/test/next-legacy/next-env.d.ts b/test/next-legacy/next-env.d.ts new file mode 100644 index 000000000..3cd7048ed --- /dev/null +++ b/test/next-legacy/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/test/next/package.json b/test/next/package.json index 5244823b2..ce495da22 100644 --- a/test/next/package.json +++ b/test/next/package.json @@ -28,7 +28,7 @@ "got": "^14.4.6", "kysely": "^0.27.5", "ms": "^2.1.3", - "next": "^15.2.0", + "next": "15.2.5", "postcss": "^8.5.3", "react": "^19.0.0", "react-dom": "^19.0.0",