@web-widget/flags-kit
Version:
Flags SDK by Vercel - The feature flags toolkit for Next.js, SvelteKit, and Web Router - Enhanced fork with improved Web Router support
520 lines (513 loc) • 16 kB
JavaScript
import {
deserialize,
memoizeOne,
normalizeOptions,
serialize
} from "./chunk-O6VYPARG.js";
import {
HeadersAdapter,
RequestCookiesAdapter,
decryptOverrides,
internalReportValue,
reportValue,
setSpanAttribute,
trace,
verifyAccess,
version
} from "./chunk-BQ2THYCT.js";
import "./chunk-2UDLZC33.js";
// src/next/index.ts
import { RequestCookies } from "@edge-runtime/cookies";
// src/next/overrides.ts
var memoizedDecrypt = memoizeOne(
(text) => decryptOverrides(text),
(a, b) => a[0] === b[0],
// only the first argument gets compared
{ cachePromiseRejection: true }
);
async function getOverrides(cookie) {
if (typeof cookie === "string" && cookie !== "") {
const cookieOverrides = await memoizedDecrypt(cookie);
return cookieOverrides ?? null;
}
return null;
}
// src/next/precompute.ts
async function evaluate(flags) {
return Promise.all(flags.map((flag2) => flag2()));
}
async function precompute(flags) {
const values = await evaluate(flags);
return serialize2(flags, values);
}
function combine(flags, values) {
return Object.fromEntries(flags.map((flag2, i) => [flag2.key, values[i]]));
}
async function serialize2(flags, values, secret = process.env.FLAGS_SECRET) {
if (!secret) {
throw new Error("flags: Can not serialize due to missing secret");
}
return serialize(combine(flags, values), flags, secret);
}
async function deserialize2(flags, code, secret = process.env.FLAGS_SECRET) {
if (!secret) {
throw new Error("flags: Can not serialize due to missing secret");
}
return deserialize(code, flags, secret);
}
async function getPrecomputed(flagOrFlags, precomputeFlags, code, secret = process.env.FLAGS_SECRET) {
if (!secret) {
throw new Error(
"flags: getPrecomputed was called without a secret. Please set FLAGS_SECRET environment variable."
);
}
const flagSet = await deserialize2(precomputeFlags, code, secret);
if (Array.isArray(flagOrFlags)) {
return flagOrFlags.map((flag2) => flagSet[flag2.key]);
} else {
return flagSet[flagOrFlags.key];
}
}
function* cartesianIterator(items) {
const remainder = items.length > 1 ? cartesianIterator(items.slice(1)) : [[]];
for (let r of remainder)
for (let h of items.at(0))
yield [h, ...r];
}
async function generatePermutations(flags, filter = null, secret = process.env.FLAGS_SECRET) {
if (!secret) {
throw new Error(
"flags: generatePermutations was called without a secret. Please set FLAGS_SECRET environment variable."
);
}
const options = flags.map((flag2) => {
if (!flag2.options)
return [false, true];
return flag2.options.map((option) => option.value);
});
const list = [];
for (const permutation of cartesianIterator(options)) {
const permObject = permutation.reduce(
(acc, value, index) => {
acc[flags[index].key] = value;
return acc;
},
{}
);
if (!filter || filter(permObject))
list.push(permObject);
}
return Promise.all(list.map((values) => serialize(values, flags, secret)));
}
// src/next/is-internal-next-error.ts
var REACT_POSTPONE_TYPE = Symbol.for("react.postpone");
function isPostpone(error) {
return typeof error === "object" && error !== null && "$$typeof" in error && error.$$typeof === REACT_POSTPONE_TYPE;
}
function isInternalNextError(error) {
if (isPostpone(error))
return true;
if (typeof error !== "object" || error === null || !("digest" in error) || typeof error.digest !== "string") {
return false;
}
const errorCode = error.digest.split(";")[0];
return errorCode === "NEXT_REDIRECT" || errorCode === "DYNAMIC_SERVER_USAGE" || errorCode === "BAILOUT_TO_CLIENT_SIDE_RENDERING" || errorCode === "NEXT_NOT_FOUND";
}
// src/next/dedupe.ts
function createCacheNode() {
return {
s: 0 /* UNTERMINATED */,
v: void 0,
o: null,
p: null
};
}
var cacheRegistry = /* @__PURE__ */ new WeakMap();
function dedupe(fn) {
const requestStore = /* @__PURE__ */ new WeakMap();
const dedupedFn = async function(...args) {
const { headers } = await import("next/headers");
const h = await headers();
let cacheNode = requestStore.get(h);
if (!cacheNode) {
cacheNode = createCacheNode();
requestStore.set(h, cacheNode);
}
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (typeof arg === "function" || typeof arg === "object" && arg !== null) {
let objectCache = cacheNode.o;
if (objectCache === null) {
cacheNode.o = objectCache = /* @__PURE__ */ new WeakMap();
}
const objectNode = objectCache.get(arg);
if (objectNode === void 0) {
cacheNode = createCacheNode();
objectCache.set(arg, cacheNode);
} else {
cacheNode = objectNode;
}
} else {
let primitiveCache = cacheNode.p;
if (primitiveCache === null) {
cacheNode.p = primitiveCache = /* @__PURE__ */ new Map();
}
const primitiveNode = primitiveCache.get(arg);
if (primitiveNode === void 0) {
cacheNode = createCacheNode();
primitiveCache.set(arg, cacheNode);
} else {
cacheNode = primitiveNode;
}
}
}
if (cacheNode.s === 1 /* TERMINATED */) {
return cacheNode.v;
}
if (cacheNode.s === 2 /* ERRORED */) {
throw cacheNode.v;
}
try {
const result = fn.apply(this, args);
cacheNode.s = 1 /* TERMINATED */;
cacheNode.v = result;
return result;
} catch (error) {
cacheNode.s = 2 /* ERRORED */;
cacheNode.v = error;
throw error;
}
};
cacheRegistry.set(dedupedFn, requestStore);
return dedupedFn;
}
async function clearDedupeCacheForCurrentRequest(dedupedFn) {
if (typeof dedupedFn !== "function") {
throw new Error("dedupe: not a function");
}
const requestStore = cacheRegistry.get(dedupedFn);
if (!requestStore) {
throw new Error("dedupe: cache not found");
}
const { headers } = await import("next/headers");
const h = await headers();
return requestStore.delete(h);
}
// src/next/create-flags-discovery-endpoint.ts
function createFlagsDiscoveryEndpoint(getApiData, options) {
return async (request) => {
const access = await verifyAccess(
request.headers.get("Authorization"),
options?.secret
);
if (!access)
return Response.json(null, { status: 401 });
const apiData = await getApiData(request);
return new Response(JSON.stringify(apiData), {
headers: {
"x-flags-sdk-version": version,
"content-type": "application/json"
}
});
};
}
// src/next/index.ts
var evaluationCache = /* @__PURE__ */ new WeakMap();
function getCachedValuePromise(headers, flagKey, entitiesKey) {
const map = evaluationCache.get(headers)?.get(flagKey);
if (!map)
return void 0;
return map.get(entitiesKey);
}
function setCachedValuePromise(headers, flagKey, entitiesKey, flagValue) {
const byHeaders = evaluationCache.get(headers);
if (!byHeaders) {
evaluationCache.set(
headers,
/* @__PURE__ */ new Map([[flagKey, /* @__PURE__ */ new Map([[entitiesKey, flagValue]])]])
);
return;
}
const byFlagKey = byHeaders.get(flagKey);
if (!byFlagKey) {
byHeaders.set(flagKey, /* @__PURE__ */ new Map([[entitiesKey, flagValue]]));
return;
}
byFlagKey.set(entitiesKey, flagValue);
}
var transformMap = /* @__PURE__ */ new WeakMap();
var headersMap = /* @__PURE__ */ new WeakMap();
var cookiesMap = /* @__PURE__ */ new WeakMap();
var identifyArgsMap = /* @__PURE__ */ new WeakMap();
function transformToHeaders(incomingHeaders) {
const cached = transformMap.get(incomingHeaders);
if (cached !== void 0)
return cached;
const headers = new Headers();
for (const [key, value] of Object.entries(incomingHeaders)) {
if (Array.isArray(value)) {
value.forEach((item) => headers.append(key, item));
} else if (value !== void 0) {
headers.append(key, value);
}
}
transformMap.set(incomingHeaders, headers);
return headers;
}
function sealHeaders(headers) {
const cached = headersMap.get(headers);
if (cached !== void 0)
return cached;
const sealed = HeadersAdapter.seal(
headers,
"Headers cannot be modified. Read more: https://nextjs.org/docs/app/api-reference/functions/headers"
);
headersMap.set(headers, sealed);
return sealed;
}
function sealCookies(headers) {
const cached = cookiesMap.get(headers);
if (cached !== void 0)
return cached;
const sealed = RequestCookiesAdapter.seal(
new RequestCookies(headers),
"Cookies can only be modified in a Server Action or Route Handler. Read more: https://nextjs.org/docs/app/api-reference/functions/cookies#options"
);
cookiesMap.set(headers, sealed);
return sealed;
}
function isIdentifyFunction(identify) {
return typeof identify === "function";
}
async function getEntities(identify, dedupeCacheKey, readonlyHeaders, readonlyCookies) {
if (!identify)
return void 0;
if (!isIdentifyFunction(identify))
return identify;
const args = identifyArgsMap.get(dedupeCacheKey);
if (args)
return identify(...args);
const nextArgs = [
{ headers: readonlyHeaders, cookies: readonlyCookies }
];
identifyArgsMap.set(dedupeCacheKey, nextArgs);
return identify(...nextArgs);
}
function getDecide(definition) {
return function decide(params) {
if (typeof definition.decide === "function") {
return definition.decide(params);
}
if (typeof definition.adapter?.decide === "function") {
return definition.adapter.decide({ key: definition.key, ...params });
}
throw new Error(`flags: No decide function provided for ${definition.key}`);
};
}
function getIdentify(definition) {
return function identify(params) {
if (typeof definition.identify === "function") {
return definition.identify(params);
}
if (typeof definition.adapter?.identify === "function") {
return definition.adapter.identify(params);
}
return definition.identify;
};
}
function getRun(definition, decide) {
return async function run(options) {
let readonlyHeaders;
let readonlyCookies;
let dedupeCacheKey;
if (options.request) {
const headers = transformToHeaders(options.request.headers);
readonlyHeaders = sealHeaders(headers);
readonlyCookies = sealCookies(headers);
dedupeCacheKey = options.request.headers;
} else {
const { headers, cookies } = await import("next/headers");
const [headersStore, cookiesStore] = await Promise.all([
headers(),
cookies()
]);
readonlyHeaders = headersStore;
readonlyCookies = cookiesStore;
dedupeCacheKey = headersStore;
}
const overrides = await getOverrides(
readonlyCookies.get("vercel-flag-overrides")?.value
);
const entities = await getEntities(
options.identify,
dedupeCacheKey,
readonlyHeaders,
readonlyCookies
);
const entitiesKey = JSON.stringify(entities) ?? "";
const cachedValue = getCachedValuePromise(
readonlyHeaders,
definition.key,
entitiesKey
);
if (cachedValue !== void 0) {
setSpanAttribute("method", "cached");
const value = await cachedValue;
return value;
}
if (overrides && overrides[definition.key] !== void 0) {
setSpanAttribute("method", "override");
const decision2 = overrides[definition.key];
setCachedValuePromise(
readonlyHeaders,
definition.key,
entitiesKey,
Promise.resolve(decision2)
);
internalReportValue(definition.key, decision2, {
reason: "override"
});
return decision2;
}
const decisionPromise = (async () => {
return decide({
// @ts-expect-error TypeScript will not be able to process `getPrecomputed` when added to `Decide`. It is, however, part of the `Adapter` type
defaultValue: definition.defaultValue,
headers: readonlyHeaders,
cookies: readonlyCookies,
entities
});
})().then(
(value) => {
if (value !== void 0)
return value;
if (definition.defaultValue !== void 0)
return definition.defaultValue;
throw new Error(
`flags: Flag "${definition.key}" must have a defaultValue or a decide function that returns a value`
);
},
(error) => {
if (isInternalNextError(error))
throw error;
if (definition.defaultValue !== void 0) {
if (process.env.NODE_ENV === "development") {
console.info(
`flags: Flag "${definition.key}" is falling back to its defaultValue`
);
} else {
console.warn(
`flags: Flag "${definition.key}" is falling back to its defaultValue after catching the following error`,
error
);
}
return definition.defaultValue;
}
console.warn(
`flags: Flag "${definition.key}" could not be evaluated`
);
throw error;
}
);
setCachedValuePromise(
readonlyHeaders,
definition.key,
entitiesKey,
decisionPromise
);
const decision = await decisionPromise;
if (definition.config?.reportValue !== false) {
reportValue(definition.key, decision);
}
return decision;
};
}
function getOrigin(definition) {
if (definition.origin)
return definition.origin;
if (typeof definition.adapter?.origin === "function")
return definition.adapter.origin(definition.key);
return definition.adapter?.origin;
}
function flag(definition) {
const decide = getDecide(definition);
const identify = getIdentify(definition);
const run = getRun(definition, decide);
const origin = getOrigin(definition);
const flag2 = trace(
async (...args) => {
setSpanAttribute("method", "decided");
if (typeof args[0] === "string" && Array.isArray(args[1])) {
const [precomputedCode, precomputedGroup, secret] = args;
if (precomputedCode && precomputedGroup) {
setSpanAttribute("method", "precomputed");
return getPrecomputed(
flag2,
precomputedGroup,
precomputedCode,
secret
);
}
}
if (args[0] && typeof args[0] === "object" && "headers" in args[0]) {
const [request] = args;
return run({ identify, request });
}
return run({ identify, request: void 0 });
},
{
name: "flag",
isVerboseTrace: false,
attributes: { key: definition.key }
}
);
flag2.key = definition.key;
flag2.defaultValue = definition.defaultValue;
flag2.origin = origin;
flag2.options = normalizeOptions(definition.options);
flag2.description = definition.description;
flag2.identify = identify ? trace(identify, {
isVerboseTrace: false,
name: "identify",
attributes: { key: definition.key }
}) : identify;
flag2.decide = trace(decide, {
isVerboseTrace: false,
name: "decide",
attributes: { key: definition.key }
});
flag2.run = trace(run, {
isVerboseTrace: false,
name: "run",
attributes: { key: definition.key }
});
return flag2;
}
function getProviderData(flags) {
const definitions = Object.values(flags).filter((i) => !Array.isArray(i)).reduce((acc, d) => {
acc[d.key] = {
options: d.options,
origin: d.origin,
description: d.description,
defaultValue: d.defaultValue,
declaredInCode: true
};
return acc;
}, {});
return { definitions, hints: [] };
}
export {
clearDedupeCacheForCurrentRequest,
combine,
createFlagsDiscoveryEndpoint,
dedupe,
deserialize2 as deserialize,
evaluate,
flag,
generatePermutations,
getPrecomputed,
getProviderData,
precompute,
serialize2 as serialize
};
//# sourceMappingURL=next.js.map