@davidlee49/launchgate 0.0.0-stage → 0.5.1
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/API.md +417 -0
- package/README.md +409 -3
- package/dist/chunk-YIOEKCH6.js +98 -0
- package/dist/index.d.ts +232 -0
- package/dist/index.js +274 -0
- package/dist/next/index.d.ts +42 -0
- package/dist/next/index.js +102 -0
- package/dist/resolver-B2cstRTq.d.ts +161 -0
- package/package.json +53 -6
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/** What a flag can resolve to. Boolean is the common case, not the only one (Decision 1). */
|
|
2
|
+
type FlagValue = boolean | string | number;
|
|
3
|
+
/**
|
|
4
|
+
* A flag's retirement — the terminal state of its lifecycle (Decision 11).
|
|
5
|
+
*
|
|
6
|
+
* Presence is the retirement: a def carrying this resolves to `value` and **no
|
|
7
|
+
* source is consulted**, not the env kill switch and not a signed override
|
|
8
|
+
* cookie. That ordering is the point. A retirement is a decision someone
|
|
9
|
+
* committed to git, so the only way to reverse it is to revert the commit —
|
|
10
|
+
* never a leftover `FLAG_X=on` secret or a cookie an engineer is still holding.
|
|
11
|
+
*
|
|
12
|
+
* The entry **stays in the registry** after retirement, which is Unleash's
|
|
13
|
+
* archive-rather-than-delete rule and buys the same property: the key cannot be
|
|
14
|
+
* silently reused by a new feature (a duplicate key in the registry literal is
|
|
15
|
+
* a type error), so a stale override can never attach to something it was never
|
|
16
|
+
* about. Delete the entry only once its code is gone.
|
|
17
|
+
*/
|
|
18
|
+
interface Retirement<T extends FlagValue = FlagValue> {
|
|
19
|
+
/**
|
|
20
|
+
* When retirement was decided, `YYYY-MM-DD`. Required, because the clock is
|
|
21
|
+
* the whole point: retired code that nobody deletes is the flag debt this
|
|
22
|
+
* state exists to make visible. `auditRegistry` warns once it is older than
|
|
23
|
+
* the budget.
|
|
24
|
+
*/
|
|
25
|
+
since: string;
|
|
26
|
+
/**
|
|
27
|
+
* What every evaluation returns from now on — and the *disposition record*:
|
|
28
|
+
* Unleash makes you declare an outcome when completing a flag, because the
|
|
29
|
+
* cleanup PR has to know which branch of the code survives. `false` retires a
|
|
30
|
+
* feature outright; `"v1"` on a dynamic-config flag says v1 is what the code
|
|
31
|
+
* keeps.
|
|
32
|
+
*/
|
|
33
|
+
value: T;
|
|
34
|
+
}
|
|
35
|
+
interface FlagDef<T extends FlagValue = FlagValue> {
|
|
36
|
+
/**
|
|
37
|
+
* The value used when no source decides, and when a source throws.
|
|
38
|
+
*
|
|
39
|
+
* Per flag, never global (Decision 3): a work-in-progress gate's safe state is
|
|
40
|
+
* `false`, but a kill switch on a year-old feature's safe state is `true` —
|
|
41
|
+
* failing that one closed would take down working functionality because a
|
|
42
|
+
* database blipped.
|
|
43
|
+
*/
|
|
44
|
+
fallback: T;
|
|
45
|
+
/** Why this flag exists. Greppability plus this line is the whole flag-debt mitigation. */
|
|
46
|
+
description: string;
|
|
47
|
+
/** May a signed visitor cookie force this flag? Default false — see `cookieOverride`. */
|
|
48
|
+
overridable?: boolean;
|
|
49
|
+
/** Per-environment value, consulted by the `envDefault` source. */
|
|
50
|
+
byEnv?: Record<string, T>;
|
|
51
|
+
/**
|
|
52
|
+
* Retired: resolve to `retired.value` and consult no source at all. See
|
|
53
|
+
* `Retirement` — this outranks every source, including the kill switch.
|
|
54
|
+
*/
|
|
55
|
+
retired?: Retirement<T>;
|
|
56
|
+
/**
|
|
57
|
+
* Projects may hang their own fields here — a stage, a tier, an owner — and
|
|
58
|
+
* read them back inside a custom source. The registry keeps their real types
|
|
59
|
+
* (`flags.network.stage` is a `string`); only the erased view a `Source` sees
|
|
60
|
+
* widens to `unknown`.
|
|
61
|
+
*/
|
|
62
|
+
[projectField: string]: unknown;
|
|
63
|
+
}
|
|
64
|
+
type FlagRegistry = Record<string, FlagDef>;
|
|
65
|
+
/**
|
|
66
|
+
* The type a flag resolves to.
|
|
67
|
+
*
|
|
68
|
+
* `fallback: false` infers as the *literal* `false` rather than `boolean`,
|
|
69
|
+
* because `boolean` is itself `true | false` and TypeScript preserves a literal
|
|
70
|
+
* whose contextual type contains it. Left alone, every boolean flag would be
|
|
71
|
+
* unable to hold the opposite value. Strings and numbers do not have this
|
|
72
|
+
* problem (their contextual types are not literal unions), so they keep whatever
|
|
73
|
+
* the author declared — which is what makes `fallback: "v1" as "v1" | "v2"` work.
|
|
74
|
+
*/
|
|
75
|
+
type FlagValueOf<T extends FlagRegistry, K extends keyof T> = T[K]["fallback"] extends boolean ? boolean : T[K]["fallback"];
|
|
76
|
+
/**
|
|
77
|
+
* Data a source may target on. Named for OpenFeature's evaluation context, and
|
|
78
|
+
* `targetingKey` is its field — the CNCF spec every major SDK implements, so
|
|
79
|
+
* this reads as a familiar dialect rather than a private vocabulary.
|
|
80
|
+
*
|
|
81
|
+
* `targetingKey` is **optional** by design, where OpenFeature makes it merely
|
|
82
|
+
* conventional: an anonymous visitor to a static site has none, and a launch
|
|
83
|
+
* gate must still resolve for them.
|
|
84
|
+
*/
|
|
85
|
+
interface EvaluationContext {
|
|
86
|
+
/** Who is being evaluated — a user id, org id, device id. Absent for anonymous visitors. */
|
|
87
|
+
targetingKey?: string;
|
|
88
|
+
/** Reads a request cookie. Supplied by a framework adapter; absent means "no cookies here". */
|
|
89
|
+
cookie?: (name: string) => string | undefined;
|
|
90
|
+
/** Anything else a project-supplied source needs. */
|
|
91
|
+
[key: string]: unknown;
|
|
92
|
+
}
|
|
93
|
+
/** @deprecated Renamed to `EvaluationContext` in 0.3.0 to match OpenFeature. */
|
|
94
|
+
type Context = EvaluationContext;
|
|
95
|
+
/** A source's answer. `undefined` means *undecided — ask the next source* (Decision 2). */
|
|
96
|
+
type Decision = FlagValue | undefined;
|
|
97
|
+
interface FlagMeta {
|
|
98
|
+
key: string;
|
|
99
|
+
def: FlagDef;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* One link in the resolution chain. Sources are typed against `unknown` values
|
|
103
|
+
* rather than the registry's inferred types (Decision 8) — the call site keeps
|
|
104
|
+
* its types, the four built-in sources give theirs up.
|
|
105
|
+
*/
|
|
106
|
+
interface Source {
|
|
107
|
+
/** Reported to `onError` when this source throws. */
|
|
108
|
+
readonly name: string;
|
|
109
|
+
decide(flag: FlagMeta, ctx: Context): Decision | Promise<Decision>;
|
|
110
|
+
}
|
|
111
|
+
interface SourceErrorInfo {
|
|
112
|
+
flag: string;
|
|
113
|
+
source: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Declares a project's flags. Identity at runtime; it exists to infer each flag's
|
|
118
|
+
* value type so `resolve()` is typed at the call site.
|
|
119
|
+
*
|
|
120
|
+
* The registry lives in the project that reads the flags, not in a table and not
|
|
121
|
+
* in library config (Decision 4): flags ship with the code they gate, so deleting
|
|
122
|
+
* one is a pull request and a key nobody reads is visibly dead.
|
|
123
|
+
*/
|
|
124
|
+
declare function defineFlags<T extends FlagRegistry>(flags: T): T;
|
|
125
|
+
interface ResolverOptions<T extends FlagRegistry> {
|
|
126
|
+
flags: T;
|
|
127
|
+
/**
|
|
128
|
+
* Consulted in order; the first definite answer wins. Recommended ordering:
|
|
129
|
+
* `envOverride` (kill switch), `cookieOverride`, project sources (grants,
|
|
130
|
+
* opt-in, entitlement), `envDefault`. The flag's `fallback` is terminal and is
|
|
131
|
+
* not a source, so it cannot be left out.
|
|
132
|
+
*/
|
|
133
|
+
sources: Source[];
|
|
134
|
+
/** Called when a source throws or answers with the wrong type. */
|
|
135
|
+
onError?: (error: unknown, info: SourceErrorInfo) => void;
|
|
136
|
+
}
|
|
137
|
+
/** Every flag in a registry, evaluated. */
|
|
138
|
+
type ResolvedFlags<T extends FlagRegistry> = {
|
|
139
|
+
[K in keyof T]: FlagValueOf<T, K>;
|
|
140
|
+
};
|
|
141
|
+
interface Resolver<T extends FlagRegistry> {
|
|
142
|
+
resolve<K extends keyof T & string>(key: K, ctx?: EvaluationContext): Promise<FlagValueOf<T, K>>;
|
|
143
|
+
/**
|
|
144
|
+
* Evaluate the whole registry at once — LaunchDarkly's `allFlagsState()`
|
|
145
|
+
* pattern: resolve server-side, hand the map to the client, and the UI never
|
|
146
|
+
* flickers because it never had to ask.
|
|
147
|
+
*
|
|
148
|
+
* Resolution is concurrent and shares the context object, so subject-backed
|
|
149
|
+
* sources hit their per-request cache; with `subjectStore({ loadAll })` the
|
|
150
|
+
* whole registry costs one read.
|
|
151
|
+
*
|
|
152
|
+
* Pass the **values** to a client, never the registry — flag keys name
|
|
153
|
+
* unshipped work, and a viewer who can't see a surface shouldn't learn its
|
|
154
|
+
* name from the payload.
|
|
155
|
+
*/
|
|
156
|
+
resolveAll(ctx?: EvaluationContext): Promise<ResolvedFlags<T>>;
|
|
157
|
+
readonly flags: T;
|
|
158
|
+
}
|
|
159
|
+
declare function createResolver<T extends FlagRegistry>(options: ResolverOptions<T>): Resolver<T>;
|
|
160
|
+
|
|
161
|
+
export { type Context as C, type Decision as D, type EvaluationContext as E, type FlagValue as F, type Resolver as R, type Source as S, type FlagMeta as a, type FlagRegistry as b, type FlagValueOf as c, type FlagDef as d, type ResolvedFlags as e, type ResolverOptions as f, type Retirement as g, type SourceErrorInfo as h, createResolver as i, defineFlags as j };
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@davidlee49/launchgate",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "A launch-gate library: hide work in progress, kill a shipped feature, no platform required.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"API.md"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./next": {
|
|
17
|
+
"types": "./dist/next/index.d.ts",
|
|
18
|
+
"import": "./dist/next/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"check": "tsc --noEmit && vitest run && tsup",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"prepublishOnly": "pnpm check"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"tsup": "^8.5.0",
|
|
30
|
+
"typescript": "^5.9.2",
|
|
31
|
+
"vitest": "^3.2.4",
|
|
32
|
+
"next": "^16.0.0",
|
|
33
|
+
"react": "^19.0.0",
|
|
34
|
+
"react-dom": "^19.0.0",
|
|
35
|
+
"@types/react": "^19.0.0"
|
|
36
|
+
},
|
|
37
|
+
"packageManager": "pnpm@10.29.2",
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"next": ">=15"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"next": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/davidlee49/launchgate.git"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|