@isikk/core 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/configError-BtEWY-oE.d.ts +79 -0
- package/dist/hooks/index.cjs +18 -10
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +18 -10
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +7 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/next/config/browser.d.ts +6 -4
- package/dist/next/config/browser.js +63 -6
- package/dist/next/config/browser.js.map +1 -1
- package/dist/next/config/index.d.ts +7 -13
- package/dist/next/config/index.js +66 -9
- package/dist/next/config/index.js.map +1 -1
- package/dist/next/middleware/index.cjs.map +1 -1
- package/dist/next/middleware/index.js.map +1 -1
- package/dist/node/index.cjs +4 -3
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.js +4 -3
- package/dist/node/index.js.map +1 -1
- package/package.json +6 -2
- package/dist/shared-By0kkXDs.d.ts +0 -31
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/next/middleware.ts","../../../src/functions/index.ts"],"sourcesContent":["import { NextResponse } from 'next/server'\nimport type { NextRequest } from 'next/server'\n\nimport { isPathMatched } from '../functions'\n\nexport const DEFAULT_EXEMPT_PATTERNS: RegExp[] = [\n /^\\/_next/,\n /^\\/\\.well-known/,\n /^\\/apple-icon\\.png$/,\n /^\\/favicon\\.ico$/,\n /^\\/icon\\.png$/,\n /^\\/icon\\.svg$/,\n /^\\/manifest\\.json$/,\n /^\\/robots\\.txt$/,\n /^\\/sitemap\\.xml$/,\n]\n\nexport function runProxyIfPathMatches(pattern: RegExp, exemptPatterns: RegExp[] = DEFAULT_EXEMPT_PATTERNS) {\n return function (handler: (request: NextRequest) => Promise<NextResponse | void>) {\n return async function (request: NextRequest): Promise<NextResponse | void> {\n if (isPathMatched(request.nextUrl.pathname, pattern, exemptPatterns)) {\n return await handler(request)\n }\n return undefined\n }\n }\n}\n\n/**\n * Redirects to a copy of `request`'s URL with every empty-string query param value removed\n * (`?tag=&sort=name` becomes `?sort=name`), or returns `undefined` if there was nothing to strip.\n * Preserves repeated keys (`?tag=a&tag=b` stays `?tag=a&tag=b`) - rebuilds the query string\n * directly from `URLSearchParams` entries rather than round-tripping through a plain object,\n * which would silently collapse repeats down to the last value.\n */\nexport function stripEmptyQueryParams(request: NextRequest): NextResponse | undefined {\n const url = request.nextUrl.clone()\n const cleaned = new URLSearchParams()\n let changed = false\n\n for (const [key, value] of url.searchParams.entries()) {\n if (value === '') {\n changed = true\n continue\n }\n cleaned.append(key, value)\n }\n\n if (!changed) {\n return undefined\n }\n\n url.search = cleaned.toString()\n return NextResponse.redirect(url)\n}\n\n// Next.js 16 deprecated the `middleware.ts`/`middleware` file convention in favor of\n// `proxy.ts`/`proxy` (middleware.ts still works today for edge-runtime use cases, but is\n// deprecated and defaults to being phased out). This alias exists so code written against either\n// naming keeps working - the wrapped handler's shape (NextRequest in, NextResponse|void out)\n// hasn't changed between the two, only what Next.js calls the file/export that uses it.\nexport const runMiddlewareIfPathMatches = runProxyIfPathMatches\n","export function withAttributes<Fn extends (...args: never[]) => unknown, Attrs extends Record<string, unknown>>(\n fn: Fn,\n attributes: Attrs\n): Fn & Attrs {\n for (const key of Object.keys(attributes)) {\n // Object.defineProperty (unlike a plain `fn[key] = value` assignment) always creates/\n // overwrites an own property, even when key is \"__proto__\" - a plain assignment would instead\n // be intercepted by Object.prototype's special __proto__ accessor and reassign fn's own\n // prototype instead of setting a property on it.\n Object.defineProperty(fn, key, {\n value: attributes[key],\n writable: true,\n configurable: true,\n enumerable: true,\n })\n }\n return fn as Fn & Attrs\n}\n\nexport function makeCallable<T, R>(originalCallable: (arg: T) => R) {\n return (data: T) => () => originalCallable(data)\n}\n\nexport function getLazyValue<T>(input: T | (() => T)): T {\n if (typeof input === 'function') {\n return (input as () => T)()\n }\n return input\n}\n\nexport async function getLazyValueAsync<T>(input: T | (() => Promise<T>) | (() => T)): Promise<T> {\n if (typeof input === 'function') {\n const result = (input as () => Promise<T>)()\n if (result instanceof Promise) {\n return await result\n }\n return result as unknown as T\n }\n return input\n}\n\nexport function suppress<T, ERT>(\n exceptions: Array<new (message?: string) => Error>,\n fn: () => T,\n onError?: (error: unknown) => ERT\n): T | ERT | undefined {\n try {\n return fn()\n } catch (error) {\n if (exceptions.some((exception) => error instanceof exception)) {\n return onError ? onError(error) : undefined\n }\n throw error\n }\n}\n\nexport function preventDefault<E extends Event, R>(callable: (event: E) => R): (event: E) => Promise<Awaited<R>> {\n return async function (event: E): Promise<Awaited<R>> {\n event.preventDefault()\n return await callable(event)\n }\n}\n\nexport function isPathMatched(pathname: string, pattern: RegExp, exemptPatterns: RegExp[] = []): boolean {\n if (exemptPatterns.some((exempt) => exempt.test(pathname))) {\n return false\n }\n return pattern.test(pathname)\n}\n\nexport function raises(error: Error): (...args: unknown[]) => never {\n return () => {\n throw error\n }\n}\n\nexport function cloned<Fn extends (...args: never[]) => unknown>(fn: Fn): Fn {\n return ((...args: Parameters<Fn>) => fn(...args)) as Fn\n}\n\nexport function enabledIf<R>(condition: boolean | (() => boolean), options: { ifNotEnabledReturnValue: R }) {\n const enabled = typeof condition === 'function' ? condition() : condition\n\n return function <Fn extends (...args: never[]) => R>(fn: Fn): Fn {\n if (!enabled) {\n return cloned((() => options.ifNotEnabledReturnValue) as Fn)\n }\n return cloned(fn)\n }\n}\n\nexport function transformExceptions<E extends Error>(\n exceptionTypes: Array<new (...args: never[]) => E>,\n transform: (error: E) => Error,\n options: { keepOriginal?: boolean } = {}\n) {\n const { keepOriginal = true } = options\n\n return function <Fn extends (...args: never[]) => unknown>(fn: Fn): Fn {\n return ((...args: Parameters<Fn>) => {\n try {\n return fn(...args)\n } catch (error) {\n if (!exceptionTypes.some((ExceptionType) => error instanceof ExceptionType)) {\n throw error\n }\n const newError = transform(error as E)\n if (keepOriginal) {\n newError.cause = error\n }\n throw newError\n }\n }) as Fn\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA6B;;;
|
|
1
|
+
{"version":3,"sources":["../../../src/next/middleware.ts","../../../src/functions/index.ts"],"sourcesContent":["import { NextResponse } from 'next/server'\nimport type { NextRequest } from 'next/server'\n\nimport { isPathMatched } from '../functions'\n\nexport const DEFAULT_EXEMPT_PATTERNS: RegExp[] = [\n /^\\/_next/,\n /^\\/\\.well-known/,\n /^\\/apple-icon\\.png$/,\n /^\\/favicon\\.ico$/,\n /^\\/icon\\.png$/,\n /^\\/icon\\.svg$/,\n /^\\/manifest\\.json$/,\n /^\\/robots\\.txt$/,\n /^\\/sitemap\\.xml$/,\n]\n\nexport function runProxyIfPathMatches(pattern: RegExp, exemptPatterns: RegExp[] = DEFAULT_EXEMPT_PATTERNS) {\n return function (handler: (request: NextRequest) => Promise<NextResponse | void>) {\n return async function (request: NextRequest): Promise<NextResponse | void> {\n if (isPathMatched(request.nextUrl.pathname, pattern, exemptPatterns)) {\n return await handler(request)\n }\n return undefined\n }\n }\n}\n\n/**\n * Redirects to a copy of `request`'s URL with every empty-string query param value removed\n * (`?tag=&sort=name` becomes `?sort=name`), or returns `undefined` if there was nothing to strip.\n * Preserves repeated keys (`?tag=a&tag=b` stays `?tag=a&tag=b`) - rebuilds the query string\n * directly from `URLSearchParams` entries rather than round-tripping through a plain object,\n * which would silently collapse repeats down to the last value.\n */\nexport function stripEmptyQueryParams(request: NextRequest): NextResponse | undefined {\n const url = request.nextUrl.clone()\n const cleaned = new URLSearchParams()\n let changed = false\n\n for (const [key, value] of url.searchParams.entries()) {\n if (value === '') {\n changed = true\n continue\n }\n cleaned.append(key, value)\n }\n\n if (!changed) {\n return undefined\n }\n\n url.search = cleaned.toString()\n return NextResponse.redirect(url)\n}\n\n// Next.js 16 deprecated the `middleware.ts`/`middleware` file convention in favor of\n// `proxy.ts`/`proxy` (middleware.ts still works today for edge-runtime use cases, but is\n// deprecated and defaults to being phased out). This alias exists so code written against either\n// naming keeps working - the wrapped handler's shape (NextRequest in, NextResponse|void out)\n// hasn't changed between the two, only what Next.js calls the file/export that uses it.\nexport const runMiddlewareIfPathMatches = runProxyIfPathMatches\n","export function withAttributes<Fn extends (...args: never[]) => unknown, Attrs extends Record<string, unknown>>(\n fn: Fn,\n attributes: Attrs\n): Fn & Attrs {\n for (const key of Object.keys(attributes)) {\n // Object.defineProperty (unlike a plain `fn[key] = value` assignment) always creates/\n // overwrites an own property, even when key is \"__proto__\" - a plain assignment would instead\n // be intercepted by Object.prototype's special __proto__ accessor and reassign fn's own\n // prototype instead of setting a property on it.\n Object.defineProperty(fn, key, {\n value: attributes[key],\n writable: true,\n configurable: true,\n enumerable: true,\n })\n }\n return fn as Fn & Attrs\n}\n\nexport function makeCallable<T, R>(originalCallable: (arg: T) => R) {\n return (data: T) => () => originalCallable(data)\n}\n\nexport function getLazyValue<T>(input: T | (() => T)): T {\n if (typeof input === 'function') {\n return (input as () => T)()\n }\n return input\n}\n\nexport async function getLazyValueAsync<T>(input: T | (() => Promise<T>) | (() => T)): Promise<T> {\n if (typeof input === 'function') {\n const result = (input as () => Promise<T>)()\n // Stryker disable next-line ConditionalExpression,BlockStatement: equivalent mutant. An async\n // function auto-adopts a returned thenable through the same resolution algorithm `await`\n // uses, so `return result` here resolves to the same value as `return await result` - the\n // only difference is an extra microtask tick, which isn't part of this function's contract.\n if (result instanceof Promise) {\n return await result\n }\n return result as unknown as T\n }\n return input\n}\n\nexport function suppress<T, ERT>(\n exceptions: Array<new (message?: string) => Error>,\n fn: () => T,\n onError?: (error: unknown) => ERT\n): T | ERT | undefined {\n try {\n return fn()\n } catch (error) {\n if (exceptions.some((exception) => error instanceof exception)) {\n return onError ? onError(error) : undefined\n }\n throw error\n }\n}\n\nexport function preventDefault<E extends Event, R>(callable: (event: E) => R): (event: E) => Promise<Awaited<R>> {\n return async function (event: E): Promise<Awaited<R>> {\n event.preventDefault()\n return await callable(event)\n }\n}\n\nexport function isPathMatched(pathname: string, pattern: RegExp, exemptPatterns: RegExp[] = []): boolean {\n if (exemptPatterns.some((exempt) => exempt.test(pathname))) {\n return false\n }\n return pattern.test(pathname)\n}\n\nexport function raises(error: Error): (...args: unknown[]) => never {\n return () => {\n throw error\n }\n}\n\nexport function cloned<Fn extends (...args: never[]) => unknown>(fn: Fn): Fn {\n return ((...args: Parameters<Fn>) => fn(...args)) as Fn\n}\n\nexport function enabledIf<R>(condition: boolean | (() => boolean), options: { ifNotEnabledReturnValue: R }) {\n const enabled = typeof condition === 'function' ? condition() : condition\n\n return function <Fn extends (...args: never[]) => R>(fn: Fn): Fn {\n if (!enabled) {\n return cloned((() => options.ifNotEnabledReturnValue) as Fn)\n }\n return cloned(fn)\n }\n}\n\nexport function transformExceptions<E extends Error>(\n exceptionTypes: Array<new (...args: never[]) => E>,\n transform: (error: E) => Error,\n options: { keepOriginal?: boolean } = {}\n) {\n const { keepOriginal = true } = options\n\n return function <Fn extends (...args: never[]) => unknown>(fn: Fn): Fn {\n return ((...args: Parameters<Fn>) => {\n try {\n return fn(...args)\n } catch (error) {\n if (!exceptionTypes.some((ExceptionType) => error instanceof ExceptionType)) {\n throw error\n }\n const newError = transform(error as E)\n if (keepOriginal) {\n newError.cause = error\n }\n throw newError\n }\n }) as Fn\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA6B;;;ACmEtB,SAAS,cAAc,UAAkB,SAAiB,iBAA2B,CAAC,GAAY;AACvG,MAAI,eAAe,KAAK,CAAC,WAAW,OAAO,KAAK,QAAQ,CAAC,GAAG;AAC1D,WAAO;AAAA,EACT;AACA,SAAO,QAAQ,KAAK,QAAQ;AAC9B;;;ADnEO,IAAM,0BAAoC;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,sBAAsB,SAAiB,iBAA2B,yBAAyB;AACzG,SAAO,SAAU,SAAiE;AAChF,WAAO,eAAgB,SAAoD;AACzE,UAAI,cAAc,QAAQ,QAAQ,UAAU,SAAS,cAAc,GAAG;AACpE,eAAO,MAAM,QAAQ,OAAO;AAAA,MAC9B;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;AASO,SAAS,sBAAsB,SAAgD;AACpF,QAAM,MAAM,QAAQ,QAAQ,MAAM;AAClC,QAAM,UAAU,IAAI,gBAAgB;AACpC,MAAI,UAAU;AAEd,aAAW,CAAC,KAAK,KAAK,KAAK,IAAI,aAAa,QAAQ,GAAG;AACrD,QAAI,UAAU,IAAI;AAChB,gBAAU;AACV;AAAA,IACF;AACA,YAAQ,OAAO,KAAK,KAAK;AAAA,EAC3B;AAEA,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,QAAQ,SAAS;AAC9B,SAAO,2BAAa,SAAS,GAAG;AAClC;AAOO,IAAM,6BAA6B;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/next/middleware.ts","../../../src/functions/index.ts"],"sourcesContent":["import { NextResponse } from 'next/server'\nimport type { NextRequest } from 'next/server'\n\nimport { isPathMatched } from '../functions'\n\nexport const DEFAULT_EXEMPT_PATTERNS: RegExp[] = [\n /^\\/_next/,\n /^\\/\\.well-known/,\n /^\\/apple-icon\\.png$/,\n /^\\/favicon\\.ico$/,\n /^\\/icon\\.png$/,\n /^\\/icon\\.svg$/,\n /^\\/manifest\\.json$/,\n /^\\/robots\\.txt$/,\n /^\\/sitemap\\.xml$/,\n]\n\nexport function runProxyIfPathMatches(pattern: RegExp, exemptPatterns: RegExp[] = DEFAULT_EXEMPT_PATTERNS) {\n return function (handler: (request: NextRequest) => Promise<NextResponse | void>) {\n return async function (request: NextRequest): Promise<NextResponse | void> {\n if (isPathMatched(request.nextUrl.pathname, pattern, exemptPatterns)) {\n return await handler(request)\n }\n return undefined\n }\n }\n}\n\n/**\n * Redirects to a copy of `request`'s URL with every empty-string query param value removed\n * (`?tag=&sort=name` becomes `?sort=name`), or returns `undefined` if there was nothing to strip.\n * Preserves repeated keys (`?tag=a&tag=b` stays `?tag=a&tag=b`) - rebuilds the query string\n * directly from `URLSearchParams` entries rather than round-tripping through a plain object,\n * which would silently collapse repeats down to the last value.\n */\nexport function stripEmptyQueryParams(request: NextRequest): NextResponse | undefined {\n const url = request.nextUrl.clone()\n const cleaned = new URLSearchParams()\n let changed = false\n\n for (const [key, value] of url.searchParams.entries()) {\n if (value === '') {\n changed = true\n continue\n }\n cleaned.append(key, value)\n }\n\n if (!changed) {\n return undefined\n }\n\n url.search = cleaned.toString()\n return NextResponse.redirect(url)\n}\n\n// Next.js 16 deprecated the `middleware.ts`/`middleware` file convention in favor of\n// `proxy.ts`/`proxy` (middleware.ts still works today for edge-runtime use cases, but is\n// deprecated and defaults to being phased out). This alias exists so code written against either\n// naming keeps working - the wrapped handler's shape (NextRequest in, NextResponse|void out)\n// hasn't changed between the two, only what Next.js calls the file/export that uses it.\nexport const runMiddlewareIfPathMatches = runProxyIfPathMatches\n","export function withAttributes<Fn extends (...args: never[]) => unknown, Attrs extends Record<string, unknown>>(\n fn: Fn,\n attributes: Attrs\n): Fn & Attrs {\n for (const key of Object.keys(attributes)) {\n // Object.defineProperty (unlike a plain `fn[key] = value` assignment) always creates/\n // overwrites an own property, even when key is \"__proto__\" - a plain assignment would instead\n // be intercepted by Object.prototype's special __proto__ accessor and reassign fn's own\n // prototype instead of setting a property on it.\n Object.defineProperty(fn, key, {\n value: attributes[key],\n writable: true,\n configurable: true,\n enumerable: true,\n })\n }\n return fn as Fn & Attrs\n}\n\nexport function makeCallable<T, R>(originalCallable: (arg: T) => R) {\n return (data: T) => () => originalCallable(data)\n}\n\nexport function getLazyValue<T>(input: T | (() => T)): T {\n if (typeof input === 'function') {\n return (input as () => T)()\n }\n return input\n}\n\nexport async function getLazyValueAsync<T>(input: T | (() => Promise<T>) | (() => T)): Promise<T> {\n if (typeof input === 'function') {\n const result = (input as () => Promise<T>)()\n if (result instanceof Promise) {\n return await result\n }\n return result as unknown as T\n }\n return input\n}\n\nexport function suppress<T, ERT>(\n exceptions: Array<new (message?: string) => Error>,\n fn: () => T,\n onError?: (error: unknown) => ERT\n): T | ERT | undefined {\n try {\n return fn()\n } catch (error) {\n if (exceptions.some((exception) => error instanceof exception)) {\n return onError ? onError(error) : undefined\n }\n throw error\n }\n}\n\nexport function preventDefault<E extends Event, R>(callable: (event: E) => R): (event: E) => Promise<Awaited<R>> {\n return async function (event: E): Promise<Awaited<R>> {\n event.preventDefault()\n return await callable(event)\n }\n}\n\nexport function isPathMatched(pathname: string, pattern: RegExp, exemptPatterns: RegExp[] = []): boolean {\n if (exemptPatterns.some((exempt) => exempt.test(pathname))) {\n return false\n }\n return pattern.test(pathname)\n}\n\nexport function raises(error: Error): (...args: unknown[]) => never {\n return () => {\n throw error\n }\n}\n\nexport function cloned<Fn extends (...args: never[]) => unknown>(fn: Fn): Fn {\n return ((...args: Parameters<Fn>) => fn(...args)) as Fn\n}\n\nexport function enabledIf<R>(condition: boolean | (() => boolean), options: { ifNotEnabledReturnValue: R }) {\n const enabled = typeof condition === 'function' ? condition() : condition\n\n return function <Fn extends (...args: never[]) => R>(fn: Fn): Fn {\n if (!enabled) {\n return cloned((() => options.ifNotEnabledReturnValue) as Fn)\n }\n return cloned(fn)\n }\n}\n\nexport function transformExceptions<E extends Error>(\n exceptionTypes: Array<new (...args: never[]) => E>,\n transform: (error: E) => Error,\n options: { keepOriginal?: boolean } = {}\n) {\n const { keepOriginal = true } = options\n\n return function <Fn extends (...args: never[]) => unknown>(fn: Fn): Fn {\n return ((...args: Parameters<Fn>) => {\n try {\n return fn(...args)\n } catch (error) {\n if (!exceptionTypes.some((ExceptionType) => error instanceof ExceptionType)) {\n throw error\n }\n const newError = transform(error as E)\n if (keepOriginal) {\n newError.cause = error\n }\n throw newError\n }\n }) as Fn\n }\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;;;
|
|
1
|
+
{"version":3,"sources":["../../../src/next/middleware.ts","../../../src/functions/index.ts"],"sourcesContent":["import { NextResponse } from 'next/server'\nimport type { NextRequest } from 'next/server'\n\nimport { isPathMatched } from '../functions'\n\nexport const DEFAULT_EXEMPT_PATTERNS: RegExp[] = [\n /^\\/_next/,\n /^\\/\\.well-known/,\n /^\\/apple-icon\\.png$/,\n /^\\/favicon\\.ico$/,\n /^\\/icon\\.png$/,\n /^\\/icon\\.svg$/,\n /^\\/manifest\\.json$/,\n /^\\/robots\\.txt$/,\n /^\\/sitemap\\.xml$/,\n]\n\nexport function runProxyIfPathMatches(pattern: RegExp, exemptPatterns: RegExp[] = DEFAULT_EXEMPT_PATTERNS) {\n return function (handler: (request: NextRequest) => Promise<NextResponse | void>) {\n return async function (request: NextRequest): Promise<NextResponse | void> {\n if (isPathMatched(request.nextUrl.pathname, pattern, exemptPatterns)) {\n return await handler(request)\n }\n return undefined\n }\n }\n}\n\n/**\n * Redirects to a copy of `request`'s URL with every empty-string query param value removed\n * (`?tag=&sort=name` becomes `?sort=name`), or returns `undefined` if there was nothing to strip.\n * Preserves repeated keys (`?tag=a&tag=b` stays `?tag=a&tag=b`) - rebuilds the query string\n * directly from `URLSearchParams` entries rather than round-tripping through a plain object,\n * which would silently collapse repeats down to the last value.\n */\nexport function stripEmptyQueryParams(request: NextRequest): NextResponse | undefined {\n const url = request.nextUrl.clone()\n const cleaned = new URLSearchParams()\n let changed = false\n\n for (const [key, value] of url.searchParams.entries()) {\n if (value === '') {\n changed = true\n continue\n }\n cleaned.append(key, value)\n }\n\n if (!changed) {\n return undefined\n }\n\n url.search = cleaned.toString()\n return NextResponse.redirect(url)\n}\n\n// Next.js 16 deprecated the `middleware.ts`/`middleware` file convention in favor of\n// `proxy.ts`/`proxy` (middleware.ts still works today for edge-runtime use cases, but is\n// deprecated and defaults to being phased out). This alias exists so code written against either\n// naming keeps working - the wrapped handler's shape (NextRequest in, NextResponse|void out)\n// hasn't changed between the two, only what Next.js calls the file/export that uses it.\nexport const runMiddlewareIfPathMatches = runProxyIfPathMatches\n","export function withAttributes<Fn extends (...args: never[]) => unknown, Attrs extends Record<string, unknown>>(\n fn: Fn,\n attributes: Attrs\n): Fn & Attrs {\n for (const key of Object.keys(attributes)) {\n // Object.defineProperty (unlike a plain `fn[key] = value` assignment) always creates/\n // overwrites an own property, even when key is \"__proto__\" - a plain assignment would instead\n // be intercepted by Object.prototype's special __proto__ accessor and reassign fn's own\n // prototype instead of setting a property on it.\n Object.defineProperty(fn, key, {\n value: attributes[key],\n writable: true,\n configurable: true,\n enumerable: true,\n })\n }\n return fn as Fn & Attrs\n}\n\nexport function makeCallable<T, R>(originalCallable: (arg: T) => R) {\n return (data: T) => () => originalCallable(data)\n}\n\nexport function getLazyValue<T>(input: T | (() => T)): T {\n if (typeof input === 'function') {\n return (input as () => T)()\n }\n return input\n}\n\nexport async function getLazyValueAsync<T>(input: T | (() => Promise<T>) | (() => T)): Promise<T> {\n if (typeof input === 'function') {\n const result = (input as () => Promise<T>)()\n // Stryker disable next-line ConditionalExpression,BlockStatement: equivalent mutant. An async\n // function auto-adopts a returned thenable through the same resolution algorithm `await`\n // uses, so `return result` here resolves to the same value as `return await result` - the\n // only difference is an extra microtask tick, which isn't part of this function's contract.\n if (result instanceof Promise) {\n return await result\n }\n return result as unknown as T\n }\n return input\n}\n\nexport function suppress<T, ERT>(\n exceptions: Array<new (message?: string) => Error>,\n fn: () => T,\n onError?: (error: unknown) => ERT\n): T | ERT | undefined {\n try {\n return fn()\n } catch (error) {\n if (exceptions.some((exception) => error instanceof exception)) {\n return onError ? onError(error) : undefined\n }\n throw error\n }\n}\n\nexport function preventDefault<E extends Event, R>(callable: (event: E) => R): (event: E) => Promise<Awaited<R>> {\n return async function (event: E): Promise<Awaited<R>> {\n event.preventDefault()\n return await callable(event)\n }\n}\n\nexport function isPathMatched(pathname: string, pattern: RegExp, exemptPatterns: RegExp[] = []): boolean {\n if (exemptPatterns.some((exempt) => exempt.test(pathname))) {\n return false\n }\n return pattern.test(pathname)\n}\n\nexport function raises(error: Error): (...args: unknown[]) => never {\n return () => {\n throw error\n }\n}\n\nexport function cloned<Fn extends (...args: never[]) => unknown>(fn: Fn): Fn {\n return ((...args: Parameters<Fn>) => fn(...args)) as Fn\n}\n\nexport function enabledIf<R>(condition: boolean | (() => boolean), options: { ifNotEnabledReturnValue: R }) {\n const enabled = typeof condition === 'function' ? condition() : condition\n\n return function <Fn extends (...args: never[]) => R>(fn: Fn): Fn {\n if (!enabled) {\n return cloned((() => options.ifNotEnabledReturnValue) as Fn)\n }\n return cloned(fn)\n }\n}\n\nexport function transformExceptions<E extends Error>(\n exceptionTypes: Array<new (...args: never[]) => E>,\n transform: (error: E) => Error,\n options: { keepOriginal?: boolean } = {}\n) {\n const { keepOriginal = true } = options\n\n return function <Fn extends (...args: never[]) => unknown>(fn: Fn): Fn {\n return ((...args: Parameters<Fn>) => {\n try {\n return fn(...args)\n } catch (error) {\n if (!exceptionTypes.some((ExceptionType) => error instanceof ExceptionType)) {\n throw error\n }\n const newError = transform(error as E)\n if (keepOriginal) {\n newError.cause = error\n }\n throw newError\n }\n }) as Fn\n }\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;;;ACmEtB,SAAS,cAAc,UAAkB,SAAiB,iBAA2B,CAAC,GAAY;AACvG,MAAI,eAAe,KAAK,CAAC,WAAW,OAAO,KAAK,QAAQ,CAAC,GAAG;AAC1D,WAAO;AAAA,EACT;AACA,SAAO,QAAQ,KAAK,QAAQ;AAC9B;;;ADnEO,IAAM,0BAAoC;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,sBAAsB,SAAiB,iBAA2B,yBAAyB;AACzG,SAAO,SAAU,SAAiE;AAChF,WAAO,eAAgB,SAAoD;AACzE,UAAI,cAAc,QAAQ,QAAQ,UAAU,SAAS,cAAc,GAAG;AACpE,eAAO,MAAM,QAAQ,OAAO;AAAA,MAC9B;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;AASO,SAAS,sBAAsB,SAAgD;AACpF,QAAM,MAAM,QAAQ,QAAQ,MAAM;AAClC,QAAM,UAAU,IAAI,gBAAgB;AACpC,MAAI,UAAU;AAEd,aAAW,CAAC,KAAK,KAAK,KAAK,IAAI,aAAa,QAAQ,GAAG;AACrD,QAAI,UAAU,IAAI;AAChB,gBAAU;AACV;AAAA,IACF;AACA,YAAQ,OAAO,KAAK,KAAK;AAAA,EAC3B;AAEA,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,QAAQ,SAAS;AAC9B,SAAO,aAAa,SAAS,GAAG;AAClC;AAOO,IAAM,6BAA6B;","names":[]}
|
package/dist/node/index.cjs
CHANGED
|
@@ -157,6 +157,9 @@ function namespacesOverlap(a, b) {
|
|
|
157
157
|
function describeNamespace(namespace) {
|
|
158
158
|
return namespace.prefix === "" ? "no prefix" : `prefix ${JSON.stringify(namespace.prefix)}`;
|
|
159
159
|
}
|
|
160
|
+
function sameNamespace(a, b) {
|
|
161
|
+
return a.kind === b.kind && a.prefix === b.prefix && a.sep === b.sep;
|
|
162
|
+
}
|
|
160
163
|
function claimConfigNamespace(claim) {
|
|
161
164
|
const registry2 = getRegistry();
|
|
162
165
|
for (const existing of registry2) {
|
|
@@ -164,9 +167,7 @@ function claimConfigNamespace(claim) {
|
|
|
164
167
|
return `${CALL_NAME[claim.kind]} was called with ${describeNamespace(claim)}, but ${CALL_NAME[existing.kind]} already claimed ${describeNamespace(existing)} - they would read the same environment variable namespace, so a key added to the public schema can resolve to a server-only value and be serialized into the browser. Give one of them a prefix the other does not use.`;
|
|
165
168
|
}
|
|
166
169
|
}
|
|
167
|
-
const alreadyClaimed = registry2.some(
|
|
168
|
-
(existing) => existing.kind === claim.kind && existing.prefix === claim.prefix && existing.sep === claim.sep
|
|
169
|
-
);
|
|
170
|
+
const alreadyClaimed = registry2.some((existing) => sameNamespace(existing, claim));
|
|
170
171
|
if (!alreadyClaimed) {
|
|
171
172
|
registry2.push(claim);
|
|
172
173
|
}
|
package/dist/node/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/node/index.ts","../../src/node/casters.ts","../../src/node/configError.ts","../../src/node/configCore.ts","../../src/node/configRegistry.ts","../../src/node/config.ts","../../src/node/contextLocal.ts","../../src/node/getFileAsString.ts"],"sourcesContent":["export * from './casters'\nexport * from './config'\nexport * from './contextLocal'\nexport * from './getFileAsString'\n","export type Caster<T> = ((value: string) => T) & { missingDefault?: T; errorDefault?: T }\n\nexport function caster<T>(fn: (value: string) => T) {\n return function (options: { missingDefault?: T; errorDefault?: T } = {}): Caster<T> {\n const clone = ((value: string) => fn(value)) as Caster<T>\n if ('missingDefault' in options) {\n clone.missingDefault = options.missingDefault\n }\n if ('errorDefault' in options) {\n clone.errorDefault = options.errorDefault\n }\n return clone\n }\n}\n\n// JS's Number()/parseInt()/parseFloat() don't throw on unparseable input the way Python's int()/\n// float() do (parseInt('123abc') silently returns 123, Number('') silently returns 0) - these\n// helpers add back the \"either it's a clean, fully-parsed number or it throws\" contract the\n// missingDefault/errorDefault fallback system above depends on.\nfunction parseStrictInteger(value: string): number {\n if (value.trim() === '' || !Number.isInteger(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into an integer.`)\n }\n return Number(value)\n}\n\nfunction parseStrictFloat(value: string): number {\n if (value.trim() === '' || Number.isNaN(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a float.`)\n }\n return Number(value)\n}\n\nexport const string = caster((value: string) => value)\n\nexport const integer = caster(parseStrictInteger)\n\nexport const float = caster(parseStrictFloat)\n\nexport const boolean = caster((value: string) => {\n const truthy = ['true', 'True', '1']\n const falsy = ['false', 'False', '0']\n if (truthy.includes(value)) {\n return true\n }\n if (falsy.includes(value)) {\n return false\n }\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a boolean.`)\n})\n\nexport const commaSeparatedList = caster((value: string) => value.split(','))\n\nexport const commaSeparatedIntList = caster((value: string) => value.split(',').map(parseStrictInteger))\n\nexport const commaSeparatedFloatList = caster((value: string) => value.split(',').map(parseStrictFloat))\n","/**\n * Isolated in its own module - with no `process.env` access anywhere in it - so that the browser\n * half of `@isikk/core/next/config` can throw the same error type without importing\n * anything that reads the environment. Keeping the split structural means the guarantee holds\n * because of what the file contains, not because a bundler happened to tree-shake it away.\n */\nexport class ConfigError extends Error {}\n","import type { Caster } from './casters'\nimport { ConfigError } from './configError'\n\nexport type ConfigSchema = { [key: string]: Caster<unknown> | ConfigSchema }\n\nexport type InferConfig<S> = {\n [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends ConfigSchema ? InferConfig<S[K]> : never\n}\n\nexport interface ConfigOptions {\n /** Prepended to every environment variable name this call reads, joined with `sep`. */\n prefix?: string\n /** Joins the prefix and the nested key path into a variable name. Defaults to `\"__\"`. */\n sep?: string\n}\n\nfunction isCaster(value: unknown): value is Caster<unknown> {\n return typeof value === 'function'\n}\n\nfunction environmentKey(prefix: string | undefined, path: string[], sep: string): string {\n return [...(prefix ? [prefix] : []), ...path].join(sep)\n}\n\nfunction readLeaf<T>(leafCaster: Caster<T>, key: string): T {\n const rawValue = process.env[key]\n\n if (rawValue === undefined) {\n if ('missingDefault' in leafCaster) {\n return leafCaster.missingDefault as T\n }\n throw new ConfigError(\n `Environment variable ${key} not found. Please set it or provide a missingDefault to your caster.`\n )\n }\n\n try {\n return leafCaster(rawValue)\n } catch (error) {\n if ('errorDefault' in leafCaster) {\n return leafCaster.errorDefault as T\n }\n throw new ConfigError(\n `Error while parsing ${key}=${JSON.stringify(rawValue)}: ${error instanceof Error ? error.message : String(error)}. ` +\n 'Please check the value and the caster, or provide an errorDefault to your caster.'\n )\n }\n}\n\n/**\n * Walks the schema and reads every leaf out of `process.env`. Shared by `config()` and by the\n * server half of `publicConfig()` so the two agree on variable naming and on the\n * `missingDefault`/`errorDefault` fallback rules by construction rather than by duplication.\n * Claims no namespace of its own - that is the caller's job, and the two callers claim different\n * kinds.\n */\nexport function buildConfig<S extends ConfigSchema>(\n schema: S,\n prefix: string | undefined,\n sep: string,\n path: string[] = []\n): InferConfig<S> {\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(schema)) {\n const keyPath = [...path, key]\n result[key] = isCaster(value)\n ? readLeaf(value, environmentKey(prefix, keyPath, sep))\n : buildConfig(value, prefix, sep, keyPath)\n }\n return result as InferConfig<S>\n}\n","/**\n * Process-wide record of which environment variable namespace each config call has claimed, so a\n * server-only `config()` and a browser-visible `publicConfig()` can be stopped from reading the\n * same one. That overlap is the mistake worth catching: with a shared namespace, a key pasted\n * into the public schema by accident resolves to the real server value and gets serialized into\n * the HTML of every page, silently. With disjoint namespaces it resolves to nothing and throws.\n *\n * Kept on `globalThis` under a `Symbol.for` key rather than in module scope because\n * `@isikk/core/node` and `@isikk/core/next/config` are separate tsup entries built\n * with `splitting: false` - a module-scoped registry would be duplicated into each bundle, giving\n * the two sides one registry each and so nothing to ever collide in.\n *\n * Claims are keyed by kind, so any number of `config()` calls (or any number of `publicConfig()`\n * calls) may share a namespace - two server reads of the same variable are harmless. Only a\n * server/public overlap is a conflict, which is also why re-registration is always safe: Next\n * evaluates the same module once per bundler layer (RSC, SSR, edge) and again on every Fast\n * Refresh, and every one of those repeats is the same kind claiming the same namespace.\n */\nexport type ConfigKind = 'server' | 'public'\n\nexport interface ConfigNamespace {\n kind: ConfigKind\n /** Empty string means \"no prefix\" - the root of the environment. */\n prefix: string\n sep: string\n}\n\nconst REGISTRY_KEY = Symbol.for('@isikk/core/config-namespace-registry')\n\nconst CALL_NAME: Record<ConfigKind, string> = {\n server: 'config()',\n public: 'publicConfig()',\n}\n\nfunction getRegistry(): ConfigNamespace[] {\n const host = globalThis as unknown as Record<symbol, ConfigNamespace[] | undefined>\n const existing = host[REGISTRY_KEY]\n if (existing) {\n return existing\n }\n const created: ConfigNamespace[] = []\n host[REGISTRY_KEY] = created\n return created\n}\n\n/**\n * Two namespaces overlap when one can produce an environment variable name the other can also\n * produce. Identical prefixes always overlap. A prefix nested under another at a separator\n * boundary overlaps too (`APP` and `APP__PUBLIC` both reach `APP__PUBLIC__TOKEN`).\n *\n * An absent prefix is deliberately treated as disjoint from every non-empty one rather than as\n * the root that technically contains them all: unprefixed server config alongside prefixed public\n * config is the most natural setup there is, and the only way it actually collides is a server\n * schema with a top-level key named exactly like the public prefix. Rejecting the whole shape to\n * catch that would cost far more than it buys - docs/next/config.md says so out loud.\n */\nfunction namespacesOverlap(a: ConfigNamespace, b: ConfigNamespace): boolean {\n if (a.prefix === b.prefix) {\n return true\n }\n if (a.prefix === '' || b.prefix === '') {\n return false\n }\n return a.prefix.startsWith(`${b.prefix}${b.sep}`) || b.prefix.startsWith(`${a.prefix}${a.sep}`)\n}\n\nfunction describeNamespace(namespace: ConfigNamespace): string {\n return namespace.prefix === '' ? 'no prefix' : `prefix ${JSON.stringify(namespace.prefix)}`\n}\n\n/**\n * Records `claim`, returning `null` when it is allowed or an explanatory message when it overlaps\n * a namespace already claimed by the other kind. Returns the message instead of throwing so each\n * entry point can throw its own bundled copy of `ConfigError`, keeping `instanceof` working\n * against the class imported from the same entry point the call came from.\n */\nexport function claimConfigNamespace(claim: ConfigNamespace): string | null {\n const registry = getRegistry()\n\n for (const existing of registry) {\n if (existing.kind !== claim.kind && namespacesOverlap(existing, claim)) {\n return (\n `${CALL_NAME[claim.kind]} was called with ${describeNamespace(claim)}, but ` +\n `${CALL_NAME[existing.kind]} already claimed ${describeNamespace(existing)} - they would read ` +\n 'the same environment variable namespace, so a key added to the public schema can resolve to ' +\n 'a server-only value and be serialized into the browser. Give one of them a prefix the other ' +\n 'does not use.'\n )\n }\n }\n\n const alreadyClaimed = registry.some(\n (existing) => existing.kind === claim.kind && existing.prefix === claim.prefix && existing.sep === claim.sep\n )\n if (!alreadyClaimed) {\n registry.push(claim)\n }\n\n return null\n}\n\n/** Test-only reset. Deliberately not re-exported from any of the package's public entry points. */\nexport function resetConfigNamespaces(): void {\n getRegistry().length = 0\n}\n","import { type ConfigOptions, type ConfigSchema, type InferConfig, buildConfig } from './configCore'\nimport { ConfigError } from './configError'\nimport { claimConfigNamespace } from './configRegistry'\n\nexport { ConfigError } from './configError'\nexport type { ConfigOptions, ConfigSchema, InferConfig } from './configCore'\n\n/**\n * Builds a typed config object by reading and casting environment variables against a schema,\n * throwing `ConfigError` when a required variable is missing or a value doesn't parse (unless the\n * caster for that key was given a `missingDefault`/`errorDefault`).\n *\n * Values read here are server-only: nothing in this module serializes them anywhere. Claims its\n * prefix as a server namespace, so a `publicConfig()` call that would read the same variable\n * names throws instead of quietly publishing them - see `@isikk/core/next/config`.\n */\nexport function config<S extends ConfigSchema>(schema: S, options: ConfigOptions = {}): InferConfig<S> {\n const { prefix, sep = '__' } = options\n\n const conflict = claimConfigNamespace({ kind: 'server', prefix: prefix ?? '', sep })\n if (conflict) {\n throw new ConfigError(conflict)\n }\n\n return buildConfig(schema, prefix, sep)\n}\n","import { AsyncLocalStorage } from 'node:async_hooks'\n\nconst registry = new Map<string, AsyncLocalStorage<unknown>>()\n\nexport function contextLocal<T>(name: string): AsyncLocalStorage<T> {\n let storage = registry.get(name)\n if (!storage) {\n storage = new AsyncLocalStorage<T>()\n registry.set(name, storage)\n }\n return storage as AsyncLocalStorage<T>\n}\n","import fs from 'fs/promises'\nimport path from 'path'\n\nexport async function getFileAsString(filename: string): Promise<string> {\n try {\n const filePath = path.join(process.cwd(), filename)\n return await fs.readFile(filePath, 'utf8')\n } catch (error) {\n console.error(`Error reading file ${filename}:`, error)\n return `Error reading file: ${error instanceof Error ? error.message : String(error)}`\n }\n}\n"],"mappings":"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","names":["path","registry","path","fs"]}
|
|
1
|
+
{"version":3,"sources":["../../src/node/index.ts","../../src/node/casters.ts","../../src/node/configError.ts","../../src/node/configCore.ts","../../src/node/configRegistry.ts","../../src/node/config.ts","../../src/node/contextLocal.ts","../../src/node/getFileAsString.ts"],"sourcesContent":["export * from './casters'\nexport * from './config'\nexport * from './contextLocal'\nexport * from './getFileAsString'\n","export type Caster<T> = ((value: string) => T) & { missingDefault?: T; errorDefault?: T }\n\nexport function caster<T>(fn: (value: string) => T) {\n return function (options: { missingDefault?: T; errorDefault?: T } = {}): Caster<T> {\n const clone = ((value: string) => fn(value)) as Caster<T>\n if ('missingDefault' in options) {\n clone.missingDefault = options.missingDefault\n }\n if ('errorDefault' in options) {\n clone.errorDefault = options.errorDefault\n }\n return clone\n }\n}\n\n// JS's Number()/parseInt()/parseFloat() don't throw on unparseable input the way Python's int()/\n// float() do (parseInt('123abc') silently returns 123, Number('') silently returns 0) - these\n// helpers add back the \"either it's a clean, fully-parsed number or it throws\" contract the\n// missingDefault/errorDefault fallback system above depends on.\nfunction parseStrictInteger(value: string): number {\n if (value.trim() === '' || !Number.isInteger(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into an integer.`)\n }\n return Number(value)\n}\n\nfunction parseStrictFloat(value: string): number {\n if (value.trim() === '' || Number.isNaN(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a float.`)\n }\n return Number(value)\n}\n\nexport const string = caster((value: string) => value)\n\nexport const integer = caster(parseStrictInteger)\n\nexport const float = caster(parseStrictFloat)\n\nexport const boolean = caster((value: string) => {\n const truthy = ['true', 'True', '1']\n const falsy = ['false', 'False', '0']\n if (truthy.includes(value)) {\n return true\n }\n if (falsy.includes(value)) {\n return false\n }\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a boolean.`)\n})\n\nexport const commaSeparatedList = caster((value: string) => value.split(','))\n\nexport const commaSeparatedIntList = caster((value: string) => value.split(',').map(parseStrictInteger))\n\nexport const commaSeparatedFloatList = caster((value: string) => value.split(',').map(parseStrictFloat))\n","/**\n * Isolated in its own module - with no `process.env` access anywhere in it - so that the browser\n * half of `@isikk/core/next/config` can throw the same error type without importing\n * anything that reads the environment. Keeping the split structural means the guarantee holds\n * because of what the file contains, not because a bundler happened to tree-shake it away.\n */\nexport class ConfigError extends Error {}\n","import type { Caster } from './casters'\nimport { ConfigError } from './configError'\n\nexport type ConfigSchema = { [key: string]: Caster<unknown> | ConfigSchema }\n\nexport type InferConfig<S> = {\n [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends ConfigSchema ? InferConfig<S[K]> : never\n}\n\nexport interface ConfigOptions {\n /** Prepended to every environment variable name this call reads, joined with `sep`. */\n prefix?: string\n /** Joins the prefix and the nested key path into a variable name. Defaults to `\"__\"`. */\n sep?: string\n}\n\nfunction isCaster(value: unknown): value is Caster<unknown> {\n return typeof value === 'function'\n}\n\nfunction environmentKey(prefix: string | undefined, path: string[], sep: string): string {\n return [...(prefix ? [prefix] : []), ...path].join(sep)\n}\n\nfunction readLeaf<T>(leafCaster: Caster<T>, key: string): T {\n const rawValue = process.env[key]\n\n if (rawValue === undefined) {\n if ('missingDefault' in leafCaster) {\n return leafCaster.missingDefault as T\n }\n throw new ConfigError(\n `Environment variable ${key} not found. Please set it or provide a missingDefault to your caster.`\n )\n }\n\n try {\n return leafCaster(rawValue)\n } catch (error) {\n if ('errorDefault' in leafCaster) {\n return leafCaster.errorDefault as T\n }\n throw new ConfigError(\n `Error while parsing ${key}=${JSON.stringify(rawValue)}: ${error instanceof Error ? error.message : String(error)}. ` +\n 'Please check the value and the caster, or provide an errorDefault to your caster.'\n )\n }\n}\n\n/**\n * Walks the schema and reads every leaf out of `process.env`. Shared by `config()` and by the\n * server half of `publicConfig()` so the two agree on variable naming and on the\n * `missingDefault`/`errorDefault` fallback rules by construction rather than by duplication.\n * Claims no namespace of its own - that is the caller's job, and the two callers claim different\n * kinds.\n */\nexport function buildConfig<S extends ConfigSchema>(\n schema: S,\n prefix: string | undefined,\n sep: string,\n path: string[] = []\n): InferConfig<S> {\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(schema)) {\n const keyPath = [...path, key]\n result[key] = isCaster(value)\n ? readLeaf(value, environmentKey(prefix, keyPath, sep))\n : buildConfig(value, prefix, sep, keyPath)\n }\n return result as InferConfig<S>\n}\n","/**\n * Process-wide record of which environment variable namespace each config call has claimed, so a\n * server-only `config()` and a browser-visible `publicConfig()` can be stopped from reading the\n * same one. That overlap is the mistake worth catching: with a shared namespace, a key pasted\n * into the public schema by accident resolves to the real server value and gets serialized into\n * the HTML of every page, silently. With disjoint namespaces it resolves to nothing and throws.\n *\n * Kept on `globalThis` under a `Symbol.for` key rather than in module scope because\n * `@isikk/core/node` and `@isikk/core/next/config` are separate tsup entries built\n * with `splitting: false` - a module-scoped registry would be duplicated into each bundle, giving\n * the two sides one registry each and so nothing to ever collide in.\n *\n * Claims are keyed by kind, so any number of `config()` calls (or any number of `publicConfig()`\n * calls) may share a namespace - two server reads of the same variable are harmless. Only a\n * server/public overlap is a conflict, which is also why re-registration is always safe: Next\n * evaluates the same module once per bundler layer (RSC, SSR, edge) and again on every Fast\n * Refresh, and every one of those repeats is the same kind claiming the same namespace.\n */\nexport type ConfigKind = 'server' | 'public'\n\nexport interface ConfigNamespace {\n kind: ConfigKind\n /** Empty string means \"no prefix\" - the root of the environment. */\n prefix: string\n sep: string\n}\n\n// Stryker disable next-line StringLiteral: equivalent as far as this module's own behavior goes -\n// any distinct key works identically for read/write here. The specific, namespaced string only\n// matters for avoiding a collision with unrelated code that also stashes state on `globalThis`\n// via `Symbol.for`, which isn't something a test *of this module* can observe or verify.\nconst REGISTRY_KEY = Symbol.for('@isikk/core/config-namespace-registry')\n\nconst CALL_NAME: Record<ConfigKind, string> = {\n server: 'config()',\n public: 'publicConfig()',\n}\n\nfunction getRegistry(): ConfigNamespace[] {\n const host = globalThis as unknown as Record<symbol, ConfigNamespace[] | undefined>\n const existing = host[REGISTRY_KEY]\n if (existing) {\n return existing\n }\n const created: ConfigNamespace[] = []\n host[REGISTRY_KEY] = created\n return created\n}\n\n/**\n * Two namespaces overlap when one can produce an environment variable name the other can also\n * produce. Identical prefixes always overlap. A prefix nested under another at a separator\n * boundary overlaps too (`APP` and `APP__PUBLIC` both reach `APP__PUBLIC__TOKEN`).\n *\n * An absent prefix is deliberately treated as disjoint from every non-empty one rather than as\n * the root that technically contains them all: unprefixed server config alongside prefixed public\n * config is the most natural setup there is, and the only way it actually collides is a server\n * schema with a top-level key named exactly like the public prefix. Rejecting the whole shape to\n * catch that would cost far more than it buys - docs/next/config.md says so out loud.\n */\nfunction namespacesOverlap(a: ConfigNamespace, b: ConfigNamespace): boolean {\n if (a.prefix === b.prefix) {\n return true\n }\n if (a.prefix === '' || b.prefix === '') {\n return false\n }\n return a.prefix.startsWith(`${b.prefix}${b.sep}`) || b.prefix.startsWith(`${a.prefix}${a.sep}`)\n}\n\nfunction describeNamespace(namespace: ConfigNamespace): string {\n return namespace.prefix === '' ? 'no prefix' : `prefix ${JSON.stringify(namespace.prefix)}`\n}\n\nfunction sameNamespace(a: ConfigNamespace, b: ConfigNamespace): boolean {\n // Stryker disable next-line ConditionalExpression: equivalent mutant on the `kind` comparison\n // specifically. Every caller of this function only ever compares entries the earlier conflict\n // check in claimConfigNamespace has already let through - and that check has already returned\n // for any existing entry of a *different* kind whose prefix overlaps claim's, and an equal\n // prefix always overlaps (namespacesOverlap's first check) - so an existing entry with a\n // matching prefix reaching here is guaranteed to already be the same kind. Checking `kind`\n // again can't change it.\n return a.kind === b.kind && a.prefix === b.prefix && a.sep === b.sep\n}\n\n/**\n * Records `claim`, returning `null` when it is allowed or an explanatory message when it overlaps\n * a namespace already claimed by the other kind. Returns the message instead of throwing so each\n * entry point can throw its own bundled copy of `ConfigError`, keeping `instanceof` working\n * against the class imported from the same entry point the call came from.\n */\nexport function claimConfigNamespace(claim: ConfigNamespace): string | null {\n const registry = getRegistry()\n\n for (const existing of registry) {\n if (existing.kind !== claim.kind && namespacesOverlap(existing, claim)) {\n return (\n `${CALL_NAME[claim.kind]} was called with ${describeNamespace(claim)}, but ` +\n `${CALL_NAME[existing.kind]} already claimed ${describeNamespace(existing)} - they would read ` +\n 'the same environment variable namespace, so a key added to the public schema can resolve to ' +\n 'a server-only value and be serialized into the browser. Give one of them a prefix the other ' +\n 'does not use.'\n )\n }\n }\n\n const alreadyClaimed = registry.some((existing) => sameNamespace(existing, claim))\n if (!alreadyClaimed) {\n registry.push(claim)\n }\n\n return null\n}\n\n/** Test-only reset. Deliberately not re-exported from any of the package's public entry points. */\nexport function resetConfigNamespaces(): void {\n getRegistry().length = 0\n}\n\n/**\n * Test-only: number of currently-registered claims, so the growth-prevention in\n * `claimConfigNamespace` (a repeated identical claim - e.g. from Next Fast Refresh re-evaluating\n * the same `config()` call - must not grow the registry) is verifiable without exposing the\n * registry's contents. Deliberately not re-exported from any of the package's public entry points.\n */\nexport function configNamespaceCount(): number {\n return getRegistry().length\n}\n\n/**\n * Test-only: removes the registry from `globalThis` entirely, so the next call that touches it\n * re-creates it from scratch - lets a test observe the freshly-created registry's initial value\n * without duplicating the `Symbol.for` key string. Deliberately not re-exported from any of the\n * package's public entry points.\n */\nexport function deleteConfigNamespaceRegistry(): void {\n delete (globalThis as unknown as Record<symbol, unknown>)[REGISTRY_KEY]\n}\n","import { type ConfigOptions, type ConfigSchema, type InferConfig, buildConfig } from './configCore'\nimport { ConfigError } from './configError'\nimport { claimConfigNamespace } from './configRegistry'\n\nexport { ConfigError } from './configError'\nexport type { ConfigOptions, ConfigSchema, InferConfig } from './configCore'\n\n/**\n * Builds a typed config object by reading and casting environment variables against a schema,\n * throwing `ConfigError` when a required variable is missing or a value doesn't parse (unless the\n * caster for that key was given a `missingDefault`/`errorDefault`).\n *\n * Values read here are server-only: nothing in this module serializes them anywhere. Claims its\n * prefix as a server namespace, so a `publicConfig()` call that would read the same variable\n * names throws instead of quietly publishing them - see `@isikk/core/next/config`.\n */\nexport function config<S extends ConfigSchema>(schema: S, options: ConfigOptions = {}): InferConfig<S> {\n const { prefix, sep = '__' } = options\n\n const conflict = claimConfigNamespace({ kind: 'server', prefix: prefix ?? '', sep })\n if (conflict) {\n throw new ConfigError(conflict)\n }\n\n return buildConfig(schema, prefix, sep)\n}\n","import { AsyncLocalStorage } from 'node:async_hooks'\n\nconst registry = new Map<string, AsyncLocalStorage<unknown>>()\n\nexport function contextLocal<T>(name: string): AsyncLocalStorage<T> {\n let storage = registry.get(name)\n if (!storage) {\n storage = new AsyncLocalStorage<T>()\n registry.set(name, storage)\n }\n return storage as AsyncLocalStorage<T>\n}\n","import fs from 'fs/promises'\nimport path from 'path'\n\nexport async function getFileAsString(filename: string): Promise<string> {\n try {\n const filePath = path.join(process.cwd(), filename)\n return await fs.readFile(filePath, 'utf8')\n } catch (error) {\n console.error(`Error reading file ${filename}:`, error)\n return `Error reading file: ${error instanceof Error ? error.message : String(error)}`\n }\n}\n"],"mappings":"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","names":["path","registry","path","fs"]}
|
package/dist/node/index.js
CHANGED
|
@@ -110,6 +110,9 @@ function namespacesOverlap(a, b) {
|
|
|
110
110
|
function describeNamespace(namespace) {
|
|
111
111
|
return namespace.prefix === "" ? "no prefix" : `prefix ${JSON.stringify(namespace.prefix)}`;
|
|
112
112
|
}
|
|
113
|
+
function sameNamespace(a, b) {
|
|
114
|
+
return a.kind === b.kind && a.prefix === b.prefix && a.sep === b.sep;
|
|
115
|
+
}
|
|
113
116
|
function claimConfigNamespace(claim) {
|
|
114
117
|
const registry2 = getRegistry();
|
|
115
118
|
for (const existing of registry2) {
|
|
@@ -117,9 +120,7 @@ function claimConfigNamespace(claim) {
|
|
|
117
120
|
return `${CALL_NAME[claim.kind]} was called with ${describeNamespace(claim)}, but ${CALL_NAME[existing.kind]} already claimed ${describeNamespace(existing)} - they would read the same environment variable namespace, so a key added to the public schema can resolve to a server-only value and be serialized into the browser. Give one of them a prefix the other does not use.`;
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
|
-
const alreadyClaimed = registry2.some(
|
|
121
|
-
(existing) => existing.kind === claim.kind && existing.prefix === claim.prefix && existing.sep === claim.sep
|
|
122
|
-
);
|
|
123
|
+
const alreadyClaimed = registry2.some((existing) => sameNamespace(existing, claim));
|
|
123
124
|
if (!alreadyClaimed) {
|
|
124
125
|
registry2.push(claim);
|
|
125
126
|
}
|
package/dist/node/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/node/casters.ts","../../src/node/configError.ts","../../src/node/configCore.ts","../../src/node/configRegistry.ts","../../src/node/config.ts","../../src/node/contextLocal.ts","../../src/node/getFileAsString.ts"],"sourcesContent":["export type Caster<T> = ((value: string) => T) & { missingDefault?: T; errorDefault?: T }\n\nexport function caster<T>(fn: (value: string) => T) {\n return function (options: { missingDefault?: T; errorDefault?: T } = {}): Caster<T> {\n const clone = ((value: string) => fn(value)) as Caster<T>\n if ('missingDefault' in options) {\n clone.missingDefault = options.missingDefault\n }\n if ('errorDefault' in options) {\n clone.errorDefault = options.errorDefault\n }\n return clone\n }\n}\n\n// JS's Number()/parseInt()/parseFloat() don't throw on unparseable input the way Python's int()/\n// float() do (parseInt('123abc') silently returns 123, Number('') silently returns 0) - these\n// helpers add back the \"either it's a clean, fully-parsed number or it throws\" contract the\n// missingDefault/errorDefault fallback system above depends on.\nfunction parseStrictInteger(value: string): number {\n if (value.trim() === '' || !Number.isInteger(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into an integer.`)\n }\n return Number(value)\n}\n\nfunction parseStrictFloat(value: string): number {\n if (value.trim() === '' || Number.isNaN(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a float.`)\n }\n return Number(value)\n}\n\nexport const string = caster((value: string) => value)\n\nexport const integer = caster(parseStrictInteger)\n\nexport const float = caster(parseStrictFloat)\n\nexport const boolean = caster((value: string) => {\n const truthy = ['true', 'True', '1']\n const falsy = ['false', 'False', '0']\n if (truthy.includes(value)) {\n return true\n }\n if (falsy.includes(value)) {\n return false\n }\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a boolean.`)\n})\n\nexport const commaSeparatedList = caster((value: string) => value.split(','))\n\nexport const commaSeparatedIntList = caster((value: string) => value.split(',').map(parseStrictInteger))\n\nexport const commaSeparatedFloatList = caster((value: string) => value.split(',').map(parseStrictFloat))\n","/**\n * Isolated in its own module - with no `process.env` access anywhere in it - so that the browser\n * half of `@isikk/core/next/config` can throw the same error type without importing\n * anything that reads the environment. Keeping the split structural means the guarantee holds\n * because of what the file contains, not because a bundler happened to tree-shake it away.\n */\nexport class ConfigError extends Error {}\n","import type { Caster } from './casters'\nimport { ConfigError } from './configError'\n\nexport type ConfigSchema = { [key: string]: Caster<unknown> | ConfigSchema }\n\nexport type InferConfig<S> = {\n [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends ConfigSchema ? InferConfig<S[K]> : never\n}\n\nexport interface ConfigOptions {\n /** Prepended to every environment variable name this call reads, joined with `sep`. */\n prefix?: string\n /** Joins the prefix and the nested key path into a variable name. Defaults to `\"__\"`. */\n sep?: string\n}\n\nfunction isCaster(value: unknown): value is Caster<unknown> {\n return typeof value === 'function'\n}\n\nfunction environmentKey(prefix: string | undefined, path: string[], sep: string): string {\n return [...(prefix ? [prefix] : []), ...path].join(sep)\n}\n\nfunction readLeaf<T>(leafCaster: Caster<T>, key: string): T {\n const rawValue = process.env[key]\n\n if (rawValue === undefined) {\n if ('missingDefault' in leafCaster) {\n return leafCaster.missingDefault as T\n }\n throw new ConfigError(\n `Environment variable ${key} not found. Please set it or provide a missingDefault to your caster.`\n )\n }\n\n try {\n return leafCaster(rawValue)\n } catch (error) {\n if ('errorDefault' in leafCaster) {\n return leafCaster.errorDefault as T\n }\n throw new ConfigError(\n `Error while parsing ${key}=${JSON.stringify(rawValue)}: ${error instanceof Error ? error.message : String(error)}. ` +\n 'Please check the value and the caster, or provide an errorDefault to your caster.'\n )\n }\n}\n\n/**\n * Walks the schema and reads every leaf out of `process.env`. Shared by `config()` and by the\n * server half of `publicConfig()` so the two agree on variable naming and on the\n * `missingDefault`/`errorDefault` fallback rules by construction rather than by duplication.\n * Claims no namespace of its own - that is the caller's job, and the two callers claim different\n * kinds.\n */\nexport function buildConfig<S extends ConfigSchema>(\n schema: S,\n prefix: string | undefined,\n sep: string,\n path: string[] = []\n): InferConfig<S> {\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(schema)) {\n const keyPath = [...path, key]\n result[key] = isCaster(value)\n ? readLeaf(value, environmentKey(prefix, keyPath, sep))\n : buildConfig(value, prefix, sep, keyPath)\n }\n return result as InferConfig<S>\n}\n","/**\n * Process-wide record of which environment variable namespace each config call has claimed, so a\n * server-only `config()` and a browser-visible `publicConfig()` can be stopped from reading the\n * same one. That overlap is the mistake worth catching: with a shared namespace, a key pasted\n * into the public schema by accident resolves to the real server value and gets serialized into\n * the HTML of every page, silently. With disjoint namespaces it resolves to nothing and throws.\n *\n * Kept on `globalThis` under a `Symbol.for` key rather than in module scope because\n * `@isikk/core/node` and `@isikk/core/next/config` are separate tsup entries built\n * with `splitting: false` - a module-scoped registry would be duplicated into each bundle, giving\n * the two sides one registry each and so nothing to ever collide in.\n *\n * Claims are keyed by kind, so any number of `config()` calls (or any number of `publicConfig()`\n * calls) may share a namespace - two server reads of the same variable are harmless. Only a\n * server/public overlap is a conflict, which is also why re-registration is always safe: Next\n * evaluates the same module once per bundler layer (RSC, SSR, edge) and again on every Fast\n * Refresh, and every one of those repeats is the same kind claiming the same namespace.\n */\nexport type ConfigKind = 'server' | 'public'\n\nexport interface ConfigNamespace {\n kind: ConfigKind\n /** Empty string means \"no prefix\" - the root of the environment. */\n prefix: string\n sep: string\n}\n\nconst REGISTRY_KEY = Symbol.for('@isikk/core/config-namespace-registry')\n\nconst CALL_NAME: Record<ConfigKind, string> = {\n server: 'config()',\n public: 'publicConfig()',\n}\n\nfunction getRegistry(): ConfigNamespace[] {\n const host = globalThis as unknown as Record<symbol, ConfigNamespace[] | undefined>\n const existing = host[REGISTRY_KEY]\n if (existing) {\n return existing\n }\n const created: ConfigNamespace[] = []\n host[REGISTRY_KEY] = created\n return created\n}\n\n/**\n * Two namespaces overlap when one can produce an environment variable name the other can also\n * produce. Identical prefixes always overlap. A prefix nested under another at a separator\n * boundary overlaps too (`APP` and `APP__PUBLIC` both reach `APP__PUBLIC__TOKEN`).\n *\n * An absent prefix is deliberately treated as disjoint from every non-empty one rather than as\n * the root that technically contains them all: unprefixed server config alongside prefixed public\n * config is the most natural setup there is, and the only way it actually collides is a server\n * schema with a top-level key named exactly like the public prefix. Rejecting the whole shape to\n * catch that would cost far more than it buys - docs/next/config.md says so out loud.\n */\nfunction namespacesOverlap(a: ConfigNamespace, b: ConfigNamespace): boolean {\n if (a.prefix === b.prefix) {\n return true\n }\n if (a.prefix === '' || b.prefix === '') {\n return false\n }\n return a.prefix.startsWith(`${b.prefix}${b.sep}`) || b.prefix.startsWith(`${a.prefix}${a.sep}`)\n}\n\nfunction describeNamespace(namespace: ConfigNamespace): string {\n return namespace.prefix === '' ? 'no prefix' : `prefix ${JSON.stringify(namespace.prefix)}`\n}\n\n/**\n * Records `claim`, returning `null` when it is allowed or an explanatory message when it overlaps\n * a namespace already claimed by the other kind. Returns the message instead of throwing so each\n * entry point can throw its own bundled copy of `ConfigError`, keeping `instanceof` working\n * against the class imported from the same entry point the call came from.\n */\nexport function claimConfigNamespace(claim: ConfigNamespace): string | null {\n const registry = getRegistry()\n\n for (const existing of registry) {\n if (existing.kind !== claim.kind && namespacesOverlap(existing, claim)) {\n return (\n `${CALL_NAME[claim.kind]} was called with ${describeNamespace(claim)}, but ` +\n `${CALL_NAME[existing.kind]} already claimed ${describeNamespace(existing)} - they would read ` +\n 'the same environment variable namespace, so a key added to the public schema can resolve to ' +\n 'a server-only value and be serialized into the browser. Give one of them a prefix the other ' +\n 'does not use.'\n )\n }\n }\n\n const alreadyClaimed = registry.some(\n (existing) => existing.kind === claim.kind && existing.prefix === claim.prefix && existing.sep === claim.sep\n )\n if (!alreadyClaimed) {\n registry.push(claim)\n }\n\n return null\n}\n\n/** Test-only reset. Deliberately not re-exported from any of the package's public entry points. */\nexport function resetConfigNamespaces(): void {\n getRegistry().length = 0\n}\n","import { type ConfigOptions, type ConfigSchema, type InferConfig, buildConfig } from './configCore'\nimport { ConfigError } from './configError'\nimport { claimConfigNamespace } from './configRegistry'\n\nexport { ConfigError } from './configError'\nexport type { ConfigOptions, ConfigSchema, InferConfig } from './configCore'\n\n/**\n * Builds a typed config object by reading and casting environment variables against a schema,\n * throwing `ConfigError` when a required variable is missing or a value doesn't parse (unless the\n * caster for that key was given a `missingDefault`/`errorDefault`).\n *\n * Values read here are server-only: nothing in this module serializes them anywhere. Claims its\n * prefix as a server namespace, so a `publicConfig()` call that would read the same variable\n * names throws instead of quietly publishing them - see `@isikk/core/next/config`.\n */\nexport function config<S extends ConfigSchema>(schema: S, options: ConfigOptions = {}): InferConfig<S> {\n const { prefix, sep = '__' } = options\n\n const conflict = claimConfigNamespace({ kind: 'server', prefix: prefix ?? '', sep })\n if (conflict) {\n throw new ConfigError(conflict)\n }\n\n return buildConfig(schema, prefix, sep)\n}\n","import { AsyncLocalStorage } from 'node:async_hooks'\n\nconst registry = new Map<string, AsyncLocalStorage<unknown>>()\n\nexport function contextLocal<T>(name: string): AsyncLocalStorage<T> {\n let storage = registry.get(name)\n if (!storage) {\n storage = new AsyncLocalStorage<T>()\n registry.set(name, storage)\n }\n return storage as AsyncLocalStorage<T>\n}\n","import fs from 'fs/promises'\nimport path from 'path'\n\nexport async function getFileAsString(filename: string): Promise<string> {\n try {\n const filePath = path.join(process.cwd(), filename)\n return await fs.readFile(filePath, 'utf8')\n } catch (error) {\n console.error(`Error reading file ${filename}:`, error)\n return `Error reading file: ${error instanceof Error ? error.message : String(error)}`\n }\n}\n"],"mappings":"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","names":["path","registry"]}
|
|
1
|
+
{"version":3,"sources":["../../src/node/casters.ts","../../src/node/configError.ts","../../src/node/configCore.ts","../../src/node/configRegistry.ts","../../src/node/config.ts","../../src/node/contextLocal.ts","../../src/node/getFileAsString.ts"],"sourcesContent":["export type Caster<T> = ((value: string) => T) & { missingDefault?: T; errorDefault?: T }\n\nexport function caster<T>(fn: (value: string) => T) {\n return function (options: { missingDefault?: T; errorDefault?: T } = {}): Caster<T> {\n const clone = ((value: string) => fn(value)) as Caster<T>\n if ('missingDefault' in options) {\n clone.missingDefault = options.missingDefault\n }\n if ('errorDefault' in options) {\n clone.errorDefault = options.errorDefault\n }\n return clone\n }\n}\n\n// JS's Number()/parseInt()/parseFloat() don't throw on unparseable input the way Python's int()/\n// float() do (parseInt('123abc') silently returns 123, Number('') silently returns 0) - these\n// helpers add back the \"either it's a clean, fully-parsed number or it throws\" contract the\n// missingDefault/errorDefault fallback system above depends on.\nfunction parseStrictInteger(value: string): number {\n if (value.trim() === '' || !Number.isInteger(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into an integer.`)\n }\n return Number(value)\n}\n\nfunction parseStrictFloat(value: string): number {\n if (value.trim() === '' || Number.isNaN(Number(value))) {\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a float.`)\n }\n return Number(value)\n}\n\nexport const string = caster((value: string) => value)\n\nexport const integer = caster(parseStrictInteger)\n\nexport const float = caster(parseStrictFloat)\n\nexport const boolean = caster((value: string) => {\n const truthy = ['true', 'True', '1']\n const falsy = ['false', 'False', '0']\n if (truthy.includes(value)) {\n return true\n }\n if (falsy.includes(value)) {\n return false\n }\n throw new Error(`Value ${JSON.stringify(value)} can not be parsed into a boolean.`)\n})\n\nexport const commaSeparatedList = caster((value: string) => value.split(','))\n\nexport const commaSeparatedIntList = caster((value: string) => value.split(',').map(parseStrictInteger))\n\nexport const commaSeparatedFloatList = caster((value: string) => value.split(',').map(parseStrictFloat))\n","/**\n * Isolated in its own module - with no `process.env` access anywhere in it - so that the browser\n * half of `@isikk/core/next/config` can throw the same error type without importing\n * anything that reads the environment. Keeping the split structural means the guarantee holds\n * because of what the file contains, not because a bundler happened to tree-shake it away.\n */\nexport class ConfigError extends Error {}\n","import type { Caster } from './casters'\nimport { ConfigError } from './configError'\n\nexport type ConfigSchema = { [key: string]: Caster<unknown> | ConfigSchema }\n\nexport type InferConfig<S> = {\n [K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends ConfigSchema ? InferConfig<S[K]> : never\n}\n\nexport interface ConfigOptions {\n /** Prepended to every environment variable name this call reads, joined with `sep`. */\n prefix?: string\n /** Joins the prefix and the nested key path into a variable name. Defaults to `\"__\"`. */\n sep?: string\n}\n\nfunction isCaster(value: unknown): value is Caster<unknown> {\n return typeof value === 'function'\n}\n\nfunction environmentKey(prefix: string | undefined, path: string[], sep: string): string {\n return [...(prefix ? [prefix] : []), ...path].join(sep)\n}\n\nfunction readLeaf<T>(leafCaster: Caster<T>, key: string): T {\n const rawValue = process.env[key]\n\n if (rawValue === undefined) {\n if ('missingDefault' in leafCaster) {\n return leafCaster.missingDefault as T\n }\n throw new ConfigError(\n `Environment variable ${key} not found. Please set it or provide a missingDefault to your caster.`\n )\n }\n\n try {\n return leafCaster(rawValue)\n } catch (error) {\n if ('errorDefault' in leafCaster) {\n return leafCaster.errorDefault as T\n }\n throw new ConfigError(\n `Error while parsing ${key}=${JSON.stringify(rawValue)}: ${error instanceof Error ? error.message : String(error)}. ` +\n 'Please check the value and the caster, or provide an errorDefault to your caster.'\n )\n }\n}\n\n/**\n * Walks the schema and reads every leaf out of `process.env`. Shared by `config()` and by the\n * server half of `publicConfig()` so the two agree on variable naming and on the\n * `missingDefault`/`errorDefault` fallback rules by construction rather than by duplication.\n * Claims no namespace of its own - that is the caller's job, and the two callers claim different\n * kinds.\n */\nexport function buildConfig<S extends ConfigSchema>(\n schema: S,\n prefix: string | undefined,\n sep: string,\n path: string[] = []\n): InferConfig<S> {\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(schema)) {\n const keyPath = [...path, key]\n result[key] = isCaster(value)\n ? readLeaf(value, environmentKey(prefix, keyPath, sep))\n : buildConfig(value, prefix, sep, keyPath)\n }\n return result as InferConfig<S>\n}\n","/**\n * Process-wide record of which environment variable namespace each config call has claimed, so a\n * server-only `config()` and a browser-visible `publicConfig()` can be stopped from reading the\n * same one. That overlap is the mistake worth catching: with a shared namespace, a key pasted\n * into the public schema by accident resolves to the real server value and gets serialized into\n * the HTML of every page, silently. With disjoint namespaces it resolves to nothing and throws.\n *\n * Kept on `globalThis` under a `Symbol.for` key rather than in module scope because\n * `@isikk/core/node` and `@isikk/core/next/config` are separate tsup entries built\n * with `splitting: false` - a module-scoped registry would be duplicated into each bundle, giving\n * the two sides one registry each and so nothing to ever collide in.\n *\n * Claims are keyed by kind, so any number of `config()` calls (or any number of `publicConfig()`\n * calls) may share a namespace - two server reads of the same variable are harmless. Only a\n * server/public overlap is a conflict, which is also why re-registration is always safe: Next\n * evaluates the same module once per bundler layer (RSC, SSR, edge) and again on every Fast\n * Refresh, and every one of those repeats is the same kind claiming the same namespace.\n */\nexport type ConfigKind = 'server' | 'public'\n\nexport interface ConfigNamespace {\n kind: ConfigKind\n /** Empty string means \"no prefix\" - the root of the environment. */\n prefix: string\n sep: string\n}\n\n// Stryker disable next-line StringLiteral: equivalent as far as this module's own behavior goes -\n// any distinct key works identically for read/write here. The specific, namespaced string only\n// matters for avoiding a collision with unrelated code that also stashes state on `globalThis`\n// via `Symbol.for`, which isn't something a test *of this module* can observe or verify.\nconst REGISTRY_KEY = Symbol.for('@isikk/core/config-namespace-registry')\n\nconst CALL_NAME: Record<ConfigKind, string> = {\n server: 'config()',\n public: 'publicConfig()',\n}\n\nfunction getRegistry(): ConfigNamespace[] {\n const host = globalThis as unknown as Record<symbol, ConfigNamespace[] | undefined>\n const existing = host[REGISTRY_KEY]\n if (existing) {\n return existing\n }\n const created: ConfigNamespace[] = []\n host[REGISTRY_KEY] = created\n return created\n}\n\n/**\n * Two namespaces overlap when one can produce an environment variable name the other can also\n * produce. Identical prefixes always overlap. A prefix nested under another at a separator\n * boundary overlaps too (`APP` and `APP__PUBLIC` both reach `APP__PUBLIC__TOKEN`).\n *\n * An absent prefix is deliberately treated as disjoint from every non-empty one rather than as\n * the root that technically contains them all: unprefixed server config alongside prefixed public\n * config is the most natural setup there is, and the only way it actually collides is a server\n * schema with a top-level key named exactly like the public prefix. Rejecting the whole shape to\n * catch that would cost far more than it buys - docs/next/config.md says so out loud.\n */\nfunction namespacesOverlap(a: ConfigNamespace, b: ConfigNamespace): boolean {\n if (a.prefix === b.prefix) {\n return true\n }\n if (a.prefix === '' || b.prefix === '') {\n return false\n }\n return a.prefix.startsWith(`${b.prefix}${b.sep}`) || b.prefix.startsWith(`${a.prefix}${a.sep}`)\n}\n\nfunction describeNamespace(namespace: ConfigNamespace): string {\n return namespace.prefix === '' ? 'no prefix' : `prefix ${JSON.stringify(namespace.prefix)}`\n}\n\nfunction sameNamespace(a: ConfigNamespace, b: ConfigNamespace): boolean {\n // Stryker disable next-line ConditionalExpression: equivalent mutant on the `kind` comparison\n // specifically. Every caller of this function only ever compares entries the earlier conflict\n // check in claimConfigNamespace has already let through - and that check has already returned\n // for any existing entry of a *different* kind whose prefix overlaps claim's, and an equal\n // prefix always overlaps (namespacesOverlap's first check) - so an existing entry with a\n // matching prefix reaching here is guaranteed to already be the same kind. Checking `kind`\n // again can't change it.\n return a.kind === b.kind && a.prefix === b.prefix && a.sep === b.sep\n}\n\n/**\n * Records `claim`, returning `null` when it is allowed or an explanatory message when it overlaps\n * a namespace already claimed by the other kind. Returns the message instead of throwing so each\n * entry point can throw its own bundled copy of `ConfigError`, keeping `instanceof` working\n * against the class imported from the same entry point the call came from.\n */\nexport function claimConfigNamespace(claim: ConfigNamespace): string | null {\n const registry = getRegistry()\n\n for (const existing of registry) {\n if (existing.kind !== claim.kind && namespacesOverlap(existing, claim)) {\n return (\n `${CALL_NAME[claim.kind]} was called with ${describeNamespace(claim)}, but ` +\n `${CALL_NAME[existing.kind]} already claimed ${describeNamespace(existing)} - they would read ` +\n 'the same environment variable namespace, so a key added to the public schema can resolve to ' +\n 'a server-only value and be serialized into the browser. Give one of them a prefix the other ' +\n 'does not use.'\n )\n }\n }\n\n const alreadyClaimed = registry.some((existing) => sameNamespace(existing, claim))\n if (!alreadyClaimed) {\n registry.push(claim)\n }\n\n return null\n}\n\n/** Test-only reset. Deliberately not re-exported from any of the package's public entry points. */\nexport function resetConfigNamespaces(): void {\n getRegistry().length = 0\n}\n\n/**\n * Test-only: number of currently-registered claims, so the growth-prevention in\n * `claimConfigNamespace` (a repeated identical claim - e.g. from Next Fast Refresh re-evaluating\n * the same `config()` call - must not grow the registry) is verifiable without exposing the\n * registry's contents. Deliberately not re-exported from any of the package's public entry points.\n */\nexport function configNamespaceCount(): number {\n return getRegistry().length\n}\n\n/**\n * Test-only: removes the registry from `globalThis` entirely, so the next call that touches it\n * re-creates it from scratch - lets a test observe the freshly-created registry's initial value\n * without duplicating the `Symbol.for` key string. Deliberately not re-exported from any of the\n * package's public entry points.\n */\nexport function deleteConfigNamespaceRegistry(): void {\n delete (globalThis as unknown as Record<symbol, unknown>)[REGISTRY_KEY]\n}\n","import { type ConfigOptions, type ConfigSchema, type InferConfig, buildConfig } from './configCore'\nimport { ConfigError } from './configError'\nimport { claimConfigNamespace } from './configRegistry'\n\nexport { ConfigError } from './configError'\nexport type { ConfigOptions, ConfigSchema, InferConfig } from './configCore'\n\n/**\n * Builds a typed config object by reading and casting environment variables against a schema,\n * throwing `ConfigError` when a required variable is missing or a value doesn't parse (unless the\n * caster for that key was given a `missingDefault`/`errorDefault`).\n *\n * Values read here are server-only: nothing in this module serializes them anywhere. Claims its\n * prefix as a server namespace, so a `publicConfig()` call that would read the same variable\n * names throws instead of quietly publishing them - see `@isikk/core/next/config`.\n */\nexport function config<S extends ConfigSchema>(schema: S, options: ConfigOptions = {}): InferConfig<S> {\n const { prefix, sep = '__' } = options\n\n const conflict = claimConfigNamespace({ kind: 'server', prefix: prefix ?? '', sep })\n if (conflict) {\n throw new ConfigError(conflict)\n }\n\n return buildConfig(schema, prefix, sep)\n}\n","import { AsyncLocalStorage } from 'node:async_hooks'\n\nconst registry = new Map<string, AsyncLocalStorage<unknown>>()\n\nexport function contextLocal<T>(name: string): AsyncLocalStorage<T> {\n let storage = registry.get(name)\n if (!storage) {\n storage = new AsyncLocalStorage<T>()\n registry.set(name, storage)\n }\n return storage as AsyncLocalStorage<T>\n}\n","import fs from 'fs/promises'\nimport path from 'path'\n\nexport async function getFileAsString(filename: string): Promise<string> {\n try {\n const filePath = path.join(process.cwd(), filename)\n return await fs.readFile(filePath, 'utf8')\n } catch (error) {\n console.error(`Error reading file ${filename}:`, error)\n return `Error reading file: ${error instanceof Error ? error.message : String(error)}`\n }\n}\n"],"mappings":"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","names":["path","registry"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isikk/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Everyday TypeScript utilities.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -81,7 +81,9 @@
|
|
|
81
81
|
"lint:prettier": "prettier --check .",
|
|
82
82
|
"lint": "npm run lint:eslint && npm run lint:types && npm run lint:prettier",
|
|
83
83
|
"fix": "eslint . --fix && prettier --write .",
|
|
84
|
-
"prepare": "husky"
|
|
84
|
+
"prepare": "husky",
|
|
85
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
86
|
+
"test:mutation": "stryker run"
|
|
85
87
|
},
|
|
86
88
|
"lint-staged": {
|
|
87
89
|
"*.{ts,tsx}": [
|
|
@@ -118,6 +120,8 @@
|
|
|
118
120
|
"@eslint/eslintrc": "^3.3.6",
|
|
119
121
|
"@eslint/js": "^9.9.1",
|
|
120
122
|
"@fast-check/vitest": "^0.4.1",
|
|
123
|
+
"@stryker-mutator/core": "^9.6.1",
|
|
124
|
+
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
121
125
|
"@testing-library/dom": "^10.4.1",
|
|
122
126
|
"@testing-library/react": "^16.3.2",
|
|
123
127
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
type Caster<T> = ((value: string) => T) & {
|
|
4
|
-
missingDefault?: T;
|
|
5
|
-
errorDefault?: T;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
type ConfigSchema = {
|
|
9
|
-
[key: string]: Caster<unknown> | ConfigSchema;
|
|
10
|
-
};
|
|
11
|
-
type InferConfig<S> = {
|
|
12
|
-
[K in keyof S]: S[K] extends Caster<infer T> ? T : S[K] extends ConfigSchema ? InferConfig<S[K]> : never;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
interface PublicConfigOptions {
|
|
16
|
-
/** Prepended to every environment variable name this call reads, joined with `sep`. */
|
|
17
|
-
prefix?: string;
|
|
18
|
-
/** Joins the prefix and the nested key path into a variable name. Defaults to `"__"`. */
|
|
19
|
-
sep?: string;
|
|
20
|
-
}
|
|
21
|
-
interface PublicConfigScriptProps {
|
|
22
|
-
/** Forwarded to the injected `<script>` so a CSP with a per-request nonce keeps working. */
|
|
23
|
-
nonce?: string;
|
|
24
|
-
}
|
|
25
|
-
type PublicConfigScriptComponent = (props: PublicConfigScriptProps) => ReactNode | Promise<ReactNode>;
|
|
26
|
-
interface PublicConfig<S extends ConfigSchema> {
|
|
27
|
-
CONFIG: InferConfig<S>;
|
|
28
|
-
PublicConfigScript: PublicConfigScriptComponent;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export type { ConfigSchema as C, InferConfig as I, PublicConfigOptions as P, PublicConfig as a, PublicConfigScriptComponent as b, PublicConfigScriptProps as c };
|