@at-flux/astro-feature-flags 1.0.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/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/dev-toolbar-app.d.mts +16 -0
- package/dist/dev-toolbar-app.d.mts.map +1 -0
- package/dist/dev-toolbar-app.mjs +407 -0
- package/dist/dev-toolbar-app.mjs.map +1 -0
- package/dist/dev-toolbar-flag-icon-BHCQJ53N.mjs +10 -0
- package/dist/dev-toolbar-flag-icon-BHCQJ53N.mjs.map +1 -0
- package/dist/index.d.mts +103 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1107 -0
- package/dist/index.mjs.map +1 -0
- package/dist/runtime.d.mts +115 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +280 -0
- package/dist/runtime.mjs.map +1 -0
- package/docs/assets/element-gating.png +0 -0
- package/docs/assets/page-gating.png +0 -0
- package/docs/assets/toolbar-feature-flags.png +0 -0
- package/docs/how-to/hide-from-sitemaps.md +49 -0
- package/docs/testing.md +40 -0
- package/example/README.md +18 -0
- package/example/astro.config.mjs +26 -0
- package/example/ff.json +26 -0
- package/example/package.json +22 -0
- package/example/pnpm-lock.yaml +4022 -0
- package/example/src/env.d.ts +2 -0
- package/example/src/layouts/Layout.astro +21 -0
- package/example/src/pages/hot/index.astro +23 -0
- package/example/src/pages/hot-dev/index.astro +18 -0
- package/example/src/pages/hot-dev/sub/index.astro +21 -0
- package/example/src/pages/hot-feature-1/index.astro +38 -0
- package/example/src/pages/index.astro +102 -0
- package/example/src/styles/global.css +22 -0
- package/example/tsconfig.json +4 -0
- package/package.json +68 -0
- package/src/badge-layout.ts +146 -0
- package/src/dev-head-inject.ts +26 -0
- package/src/dev-inline-runtimes.ts +397 -0
- package/src/dev-outline-css.ts +449 -0
- package/src/dev-toolbar-app.ts +516 -0
- package/src/dev-toolbar-flag-icon.ts +9 -0
- package/src/index.ts +417 -0
- package/src/inline-script.ts +14 -0
- package/src/production-html-cull.ts +133 -0
- package/src/route-prefix-js.ts +7 -0
- package/src/runtime.ts +575 -0
- package/virtual-astro-feature-flags.d.ts +67 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
import { t as DEV_TOOLBAR_FLAG_ICON_SVG } from "./dev-toolbar-flag-icon-BHCQJ53N.mjs";
|
|
2
|
+
import { longestMatchingRoutePrefix, mergeFlagsWithProcessEnvOverrides, resolveFeatureFlagsByEnvironment, resolveFeatureRuntime, routePathsToPrune, routePatternToPrefix, shouldIncludeRoute, toEnumKey, toToken } from "./runtime.mjs";
|
|
3
|
+
import { readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parse } from "node-html-parser";
|
|
7
|
+
//#region src/badge-layout.ts
|
|
8
|
+
function clampPercent(n, fallback) {
|
|
9
|
+
if (typeof n !== "number" || Number.isNaN(n)) return fallback;
|
|
10
|
+
return Math.min(100, Math.max(0, n));
|
|
11
|
+
}
|
|
12
|
+
function normalizeElementBadgeLayout(input) {
|
|
13
|
+
const v = clampPercent(input?.elementBadgeVerticalShiftPercent ?? 80, 80);
|
|
14
|
+
const anchor = input?.elementBadgeVerticalAnchor === "bottom" ? "bottom" : "top";
|
|
15
|
+
const hp = input?.elementBadgeHorizontalPercent;
|
|
16
|
+
if (hp !== void 0 && hp !== null && typeof hp === "number") return {
|
|
17
|
+
mode: "percent",
|
|
18
|
+
horizontalPercent: clampPercent(hp, 50),
|
|
19
|
+
verticalShiftPercent: v,
|
|
20
|
+
verticalAnchor: anchor
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
mode: "align",
|
|
24
|
+
horizontalAlign: input?.elementBadgeHorizontalAlign === "start" || input?.elementBadgeHorizontalAlign === "center" ? input.elementBadgeHorizontalAlign : "end",
|
|
25
|
+
verticalShiftPercent: v,
|
|
26
|
+
verticalAnchor: anchor
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const INSET = "0.35rem";
|
|
30
|
+
/**
|
|
31
|
+
* `left` / `right` / `top` | `bottom` / `transform` for the element `::before` badge (no trailing newline).
|
|
32
|
+
*/
|
|
33
|
+
function elementBadgePositionBlock(layout) {
|
|
34
|
+
const { verticalShiftPercent: v, verticalAnchor } = layout;
|
|
35
|
+
const y = verticalAnchor === "top" ? `translateY(calc(-1 * ${v}%))` : `translateY(${v}%)`;
|
|
36
|
+
if (layout.mode === "percent") {
|
|
37
|
+
const h = layout.horizontalPercent;
|
|
38
|
+
if (verticalAnchor === "top") return ` left: ${h}%;
|
|
39
|
+
right: auto;
|
|
40
|
+
top: 0;
|
|
41
|
+
bottom: auto;
|
|
42
|
+
transform: translateX(-50%) ${y};`;
|
|
43
|
+
return ` left: ${h}%;
|
|
44
|
+
right: auto;
|
|
45
|
+
bottom: 0;
|
|
46
|
+
top: auto;
|
|
47
|
+
transform: translateX(-50%) ${y};`;
|
|
48
|
+
}
|
|
49
|
+
const { horizontalAlign } = layout;
|
|
50
|
+
if (horizontalAlign === "center") {
|
|
51
|
+
if (verticalAnchor === "top") return ` left: 50%;
|
|
52
|
+
right: auto;
|
|
53
|
+
top: 0;
|
|
54
|
+
bottom: auto;
|
|
55
|
+
transform: translateX(-50%) ${y};`;
|
|
56
|
+
return ` left: 50%;
|
|
57
|
+
right: auto;
|
|
58
|
+
bottom: 0;
|
|
59
|
+
top: auto;
|
|
60
|
+
transform: translateX(-50%) ${y};`;
|
|
61
|
+
}
|
|
62
|
+
if (horizontalAlign === "start") {
|
|
63
|
+
if (verticalAnchor === "top") return ` left: ${INSET};
|
|
64
|
+
right: auto;
|
|
65
|
+
top: 0;
|
|
66
|
+
bottom: auto;
|
|
67
|
+
transform: ${y};`;
|
|
68
|
+
return ` left: ${INSET};
|
|
69
|
+
right: auto;
|
|
70
|
+
bottom: 0;
|
|
71
|
+
top: auto;
|
|
72
|
+
transform: ${y};`;
|
|
73
|
+
}
|
|
74
|
+
if (verticalAnchor === "top") return ` left: auto;
|
|
75
|
+
right: ${INSET};
|
|
76
|
+
top: 0;
|
|
77
|
+
bottom: auto;
|
|
78
|
+
transform: ${y};`;
|
|
79
|
+
return ` left: auto;
|
|
80
|
+
right: ${INSET};
|
|
81
|
+
bottom: 0;
|
|
82
|
+
top: auto;
|
|
83
|
+
transform: ${y};`;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/inline-script.ts
|
|
87
|
+
function compactInlineScript(source) {
|
|
88
|
+
return source.split("\n").map((line) => line.trim()).filter((line, idx, arr) => line.length > 0 || idx > 0 && idx < arr.length - 1).join("\n").trim();
|
|
89
|
+
}
|
|
90
|
+
function inlineInvoke(fn, payload) {
|
|
91
|
+
return compactInlineScript(`(${fn.toString()})(${JSON.stringify(payload)});`);
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/dev-inline-runtimes.ts
|
|
95
|
+
function affHeadInlineRuntime(payload) {
|
|
96
|
+
const toPrefix = (pattern) => {
|
|
97
|
+
let p = String(pattern || "").trim();
|
|
98
|
+
if (p.endsWith("/**")) p = p.slice(0, -3);
|
|
99
|
+
else if (p.endsWith("/*")) p = p.slice(0, -2);
|
|
100
|
+
else if (p.length > 1 && p.endsWith("*")) p = p.slice(0, -1);
|
|
101
|
+
return p.endsWith("/") ? p : `${p}/`;
|
|
102
|
+
};
|
|
103
|
+
try {
|
|
104
|
+
const { featureFlagStyles, routeFlags: RF, flagNameToToken: M } = payload;
|
|
105
|
+
const s = document.createElement("style");
|
|
106
|
+
s.setAttribute("data-astro-feature-flags", "");
|
|
107
|
+
s.textContent = featureFlagStyles;
|
|
108
|
+
(document.head || document.documentElement).appendChild(s);
|
|
109
|
+
const affPath = () => {
|
|
110
|
+
let p = typeof location !== "undefined" && location.pathname ? location.pathname : "/";
|
|
111
|
+
if (!p.endsWith("/")) p += "/";
|
|
112
|
+
return p;
|
|
113
|
+
};
|
|
114
|
+
const affFlagNames = (pathname) => {
|
|
115
|
+
let p = pathname || "/";
|
|
116
|
+
if (!p.endsWith("/")) p += "/";
|
|
117
|
+
const out = [];
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const rp in RF) {
|
|
120
|
+
if (!Object.prototype.hasOwnProperty.call(RF, rp)) continue;
|
|
121
|
+
const route = toPrefix(rp);
|
|
122
|
+
if (!(p === route || p.indexOf(route) === 0)) continue;
|
|
123
|
+
const fns = RF[rp] || [];
|
|
124
|
+
for (let i = 0; i < fns.length; i++) {
|
|
125
|
+
const fn = fns[i];
|
|
126
|
+
if (fn && !seen.has(fn)) {
|
|
127
|
+
seen.add(fn);
|
|
128
|
+
out.push(fn);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
};
|
|
134
|
+
const affTokens = (pathname) => affFlagNames(pathname).map((name) => M[name]).filter(Boolean);
|
|
135
|
+
const affSyncDisabledOverlay = () => {
|
|
136
|
+
const root = document.documentElement;
|
|
137
|
+
if (!document.body) return;
|
|
138
|
+
const names = affFlagNames(affPath());
|
|
139
|
+
const disabled = [];
|
|
140
|
+
for (let i = 0; i < names.length; i++) {
|
|
141
|
+
const tk = M[names[i]];
|
|
142
|
+
if (!tk) continue;
|
|
143
|
+
if (root.getAttribute(`data-ff-enabled-${tk}`) === "off") disabled.push(tk);
|
|
144
|
+
}
|
|
145
|
+
const id = "aff-route-disabled-overlay";
|
|
146
|
+
let el = document.getElementById(id);
|
|
147
|
+
if (!disabled.length) {
|
|
148
|
+
if (el) el.remove();
|
|
149
|
+
root.removeAttribute("data-aff-route-disabled");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
root.setAttribute("data-aff-route-disabled", "");
|
|
153
|
+
if (!el) {
|
|
154
|
+
el = document.createElement("div");
|
|
155
|
+
el.id = id;
|
|
156
|
+
el.className = "aff-route-disabled-overlay";
|
|
157
|
+
const p = document.createElement("p");
|
|
158
|
+
el.appendChild(p);
|
|
159
|
+
document.body.appendChild(el);
|
|
160
|
+
}
|
|
161
|
+
const pEl = el.querySelector("p") || el;
|
|
162
|
+
const flagsText = disabled.join(", ");
|
|
163
|
+
pEl.textContent = `${disabled.length === 1 ? `Page disabled: ${flagsText} flag is disabled.` : `Page disabled: ${flagsText} flags are disabled.`}\nIn production builds, pages with disabled flags like this will not be emitted`;
|
|
164
|
+
};
|
|
165
|
+
const affApply = () => {
|
|
166
|
+
document.documentElement.setAttribute("data-ff-route", affTokens(affPath()).join(" "));
|
|
167
|
+
affSyncDisabledOverlay();
|
|
168
|
+
};
|
|
169
|
+
affApply();
|
|
170
|
+
if (typeof MutationObserver === "function") new MutationObserver((muts) => {
|
|
171
|
+
for (let i = 0; i < muts.length; i++) {
|
|
172
|
+
const a = muts[i] && muts[i].attributeName;
|
|
173
|
+
if (a === "data-ff-route" || a && a.indexOf("data-ff-enabled-") === 0) {
|
|
174
|
+
affSyncDisabledOverlay();
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}).observe(document.documentElement, { attributes: true });
|
|
179
|
+
document.addEventListener("astro:page-load", affApply);
|
|
180
|
+
document.addEventListener("astro:after-swap", affApply);
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
183
|
+
function affDevBootstrapRuntime(payload) {
|
|
184
|
+
const toPrefix = (pattern) => {
|
|
185
|
+
let p = String(pattern || "").trim();
|
|
186
|
+
if (p.endsWith("/**")) p = p.slice(0, -3);
|
|
187
|
+
else if (p.endsWith("/*")) p = p.slice(0, -2);
|
|
188
|
+
else if (p.length > 1 && p.endsWith("*")) p = p.slice(0, -1);
|
|
189
|
+
return p.endsWith("/") ? p : `${p}/`;
|
|
190
|
+
};
|
|
191
|
+
const T = payload.flagTokens;
|
|
192
|
+
const C = payload.colors;
|
|
193
|
+
const OD = payload.outlineDefaults;
|
|
194
|
+
const BD = payload.badgeDefaults;
|
|
195
|
+
const RF = payload.routeFlags;
|
|
196
|
+
const F2T = payload.flagNameToToken;
|
|
197
|
+
const N = payload.namespace;
|
|
198
|
+
const K = `${N}.dev.v1`;
|
|
199
|
+
const root = document.documentElement;
|
|
200
|
+
const isEnabled = (tk) => root.getAttribute(`data-ff-enabled-${tk}`) !== "off";
|
|
201
|
+
const currentTokenColor = (tk, cols) => {
|
|
202
|
+
return root.style.getPropertyValue(`--${N}-c-${tk}`).trim() || cols[tk] || C[tk] || "#94a3b8";
|
|
203
|
+
};
|
|
204
|
+
const unique = (a) => [...new Set(a.filter(Boolean))];
|
|
205
|
+
const comboTokens = (el) => {
|
|
206
|
+
let out = [];
|
|
207
|
+
const raw = (el.getAttribute(`data-${N}`) || "").trim();
|
|
208
|
+
if (raw) out = out.concat(raw.split(/\s+/).filter(Boolean));
|
|
209
|
+
for (let i = 0; i < T.length; i++) {
|
|
210
|
+
const tk = T[i];
|
|
211
|
+
if (el.hasAttribute(`data-${N}-${tk}`)) out.push(tk);
|
|
212
|
+
}
|
|
213
|
+
return unique(out);
|
|
214
|
+
};
|
|
215
|
+
const stripBooleanFlagAttrs = (el) => {
|
|
216
|
+
for (let i = 0; i < T.length; i++) el.removeAttribute(`data-${N}-${T[i]}`);
|
|
217
|
+
};
|
|
218
|
+
const clearComboVisualState = (el) => {
|
|
219
|
+
el.removeAttribute("data-ff-label");
|
|
220
|
+
el.style.removeProperty("--ff-combo-gradient");
|
|
221
|
+
el.style.removeProperty("--ff-combo-gradient-soft");
|
|
222
|
+
el.style.removeProperty("--ff-combo-outline");
|
|
223
|
+
el.style.removeProperty("--ff-combo-text");
|
|
224
|
+
el.style.removeProperty("--ff-combo-badge-border");
|
|
225
|
+
el.style.removeProperty("--ff-combo-badge-gradient");
|
|
226
|
+
};
|
|
227
|
+
const applyCombo = (el, cols) => {
|
|
228
|
+
const allTokens = comboTokens(el);
|
|
229
|
+
const hasCombo = allTokens.length >= 2;
|
|
230
|
+
const hasDisabledInCombo = hasCombo && allTokens.some((tk) => !isEnabled(tk));
|
|
231
|
+
const visual = allTokens.filter((tk) => isEnabled(tk));
|
|
232
|
+
if (hasDisabledInCombo) {
|
|
233
|
+
stripBooleanFlagAttrs(el);
|
|
234
|
+
el.setAttribute(`data-${N}`, allTokens.join(" "));
|
|
235
|
+
clearComboVisualState(el);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (visual.length < 2) {
|
|
239
|
+
clearComboVisualState(el);
|
|
240
|
+
if (hasCombo) {
|
|
241
|
+
stripBooleanFlagAttrs(el);
|
|
242
|
+
if (visual.length === 1) el.setAttribute(`data-${N}`, visual[0]);
|
|
243
|
+
else el.removeAttribute(`data-${N}`);
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
stripBooleanFlagAttrs(el);
|
|
248
|
+
el.setAttribute(`data-${N}`, visual.join(" "));
|
|
249
|
+
el.setAttribute("data-ff-label", visual.join(" | "));
|
|
250
|
+
const strong = visual.map((tk) => currentTokenColor(tk, cols));
|
|
251
|
+
const soft = strong.map((c) => `color-mix(in oklab, white 86%, ${c} 14%)`);
|
|
252
|
+
el.style.setProperty("--ff-combo-gradient", `linear-gradient(90deg, ${strong.join(", ")})`);
|
|
253
|
+
el.style.setProperty("--ff-combo-gradient-soft", `linear-gradient(90deg, ${soft.join(", ")})`);
|
|
254
|
+
el.style.setProperty("--ff-combo-outline", strong[0] || "#64748b");
|
|
255
|
+
el.style.setProperty("--ff-combo-text", strong[0] || "#111827");
|
|
256
|
+
el.style.setProperty("--ff-combo-badge-border", `color-mix(in oklab, ${strong[0] || "#64748b"} 35%, transparent)`);
|
|
257
|
+
el.style.setProperty("--ff-combo-badge-gradient", `linear-gradient(90deg, ${soft.join(", ")})`);
|
|
258
|
+
};
|
|
259
|
+
const routeTokensForPath = (path) => {
|
|
260
|
+
const out = [];
|
|
261
|
+
const seen = /* @__PURE__ */ new Set();
|
|
262
|
+
for (const rp in RF) {
|
|
263
|
+
if (!Object.prototype.hasOwnProperty.call(RF, rp)) continue;
|
|
264
|
+
const cnd = toPrefix(rp);
|
|
265
|
+
if (!(path === cnd || path.indexOf(cnd) === 0)) continue;
|
|
266
|
+
const fns = RF[rp] || [];
|
|
267
|
+
for (let i = 0; i < fns.length; i++) {
|
|
268
|
+
const tk = F2T[fns[i]];
|
|
269
|
+
if (tk && !seen.has(tk)) {
|
|
270
|
+
seen.add(tk);
|
|
271
|
+
out.push(tk);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
};
|
|
277
|
+
const routeLabelFor = (path, toks) => {
|
|
278
|
+
if (!toks.length) return "";
|
|
279
|
+
const byFlag = {};
|
|
280
|
+
for (const rp in RF) {
|
|
281
|
+
if (!Object.prototype.hasOwnProperty.call(RF, rp)) continue;
|
|
282
|
+
const cnd = toPrefix(rp);
|
|
283
|
+
if (!(path === cnd || path.indexOf(cnd) === 0)) continue;
|
|
284
|
+
const fns = RF[rp] || [];
|
|
285
|
+
for (let i = 0; i < fns.length; i++) {
|
|
286
|
+
const tk = F2T[fns[i]];
|
|
287
|
+
if (!tk) continue;
|
|
288
|
+
if (!byFlag[tk]) byFlag[tk] = [];
|
|
289
|
+
byFlag[tk].push(rp);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return `Page: ${toks.map((tk) => `${tk} - ${(byFlag[tk] || []).join(", ") || "matched"}`).join(" | ")}`;
|
|
293
|
+
};
|
|
294
|
+
const affSyncRouteChrome = (path) => {
|
|
295
|
+
if (!document.body) return;
|
|
296
|
+
const routeAll = routeTokensForPath(path);
|
|
297
|
+
if (!routeAll.length) {
|
|
298
|
+
document.getElementById("aff-route-frame")?.remove();
|
|
299
|
+
document.getElementById("aff-route-badge")?.remove();
|
|
300
|
+
root.removeAttribute("data-ff-route");
|
|
301
|
+
root.removeAttribute("data-ff-route-label");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
root.setAttribute("data-ff-route", routeAll.join(" "));
|
|
305
|
+
root.setAttribute("data-ff-route-label", routeLabelFor(path, routeAll));
|
|
306
|
+
if (!document.getElementById("aff-route-frame")) {
|
|
307
|
+
const frame = document.createElement("div");
|
|
308
|
+
frame.id = "aff-route-frame";
|
|
309
|
+
frame.className = "aff-route-frame";
|
|
310
|
+
frame.setAttribute("aria-hidden", "true");
|
|
311
|
+
document.body.appendChild(frame);
|
|
312
|
+
}
|
|
313
|
+
let badge = document.getElementById("aff-route-badge");
|
|
314
|
+
if (!badge) {
|
|
315
|
+
badge = document.createElement("div");
|
|
316
|
+
badge.id = "aff-route-badge";
|
|
317
|
+
badge.className = "aff-route-badge";
|
|
318
|
+
badge.setAttribute("role", "status");
|
|
319
|
+
document.body.appendChild(badge);
|
|
320
|
+
}
|
|
321
|
+
badge.textContent = root.getAttribute("data-ff-route-label") || "";
|
|
322
|
+
badge.setAttribute("aria-label", badge.textContent || "");
|
|
323
|
+
};
|
|
324
|
+
try {
|
|
325
|
+
window.__AFF__ = {
|
|
326
|
+
tokens: T,
|
|
327
|
+
colors: C,
|
|
328
|
+
namespace: N
|
|
329
|
+
};
|
|
330
|
+
const raw = localStorage.getItem(K) || localStorage.getItem("aff.dev.v1");
|
|
331
|
+
const st = raw ? JSON.parse(raw) : {};
|
|
332
|
+
const o = st.outline || st.chrome || {};
|
|
333
|
+
const e = st.enabled || st.render || {};
|
|
334
|
+
const b = st.badge || {};
|
|
335
|
+
const cols = st.colors || {};
|
|
336
|
+
for (let i = 0; i < T.length; i++) {
|
|
337
|
+
const t = T[i];
|
|
338
|
+
const od = OD[t] !== false;
|
|
339
|
+
const bd = BD[t] !== false;
|
|
340
|
+
if (o[t] === false || !od && o[t] !== true) root.setAttribute(`data-ff-outline-${t}`, "off");
|
|
341
|
+
else root.removeAttribute(`data-ff-outline-${t}`);
|
|
342
|
+
if (e[t] === false) root.setAttribute(`data-ff-enabled-${t}`, "off");
|
|
343
|
+
else root.removeAttribute(`data-ff-enabled-${t}`);
|
|
344
|
+
if (b[t] === false || !bd && b[t] !== true) root.setAttribute(`data-ff-badge-${t}`, "off");
|
|
345
|
+
else root.removeAttribute(`data-ff-badge-${t}`);
|
|
346
|
+
const col = cols[t] || C[t];
|
|
347
|
+
const vn = `--${N}-c-${t}`;
|
|
348
|
+
if (col) root.style.setProperty(vn, col);
|
|
349
|
+
else root.style.removeProperty(vn);
|
|
350
|
+
}
|
|
351
|
+
const applyAll = () => {
|
|
352
|
+
const map = /* @__PURE__ */ new Set();
|
|
353
|
+
document.querySelectorAll(`[data-${N}]`).forEach((el) => map.add(el));
|
|
354
|
+
for (let i = 0; i < T.length; i++) document.querySelectorAll(`[data-${N}-${T[i]}]`).forEach((el) => map.add(el));
|
|
355
|
+
map.forEach((el) => applyCombo(el, cols));
|
|
356
|
+
};
|
|
357
|
+
const affAfterNav = () => {
|
|
358
|
+
if (!document.body) return;
|
|
359
|
+
let p = window.location?.pathname || "/";
|
|
360
|
+
if (!p.endsWith("/")) p += "/";
|
|
361
|
+
const toks = routeTokensForPath(p).filter((tk) => isEnabled(tk));
|
|
362
|
+
if (toks.length) {
|
|
363
|
+
const strong = toks.map((tk) => currentTokenColor(tk, cols));
|
|
364
|
+
const soft = strong.map((c) => `color-mix(in oklab, white 86%, ${c} 14%)`);
|
|
365
|
+
root.style.setProperty(`--${N}-route-gradient`, `linear-gradient(90deg, ${soft.join(", ")})`);
|
|
366
|
+
root.style.setProperty("--ff-route-outline-gradient", `linear-gradient(90deg, ${strong.join(", ")})`);
|
|
367
|
+
root.style.setProperty("--ff-route-badge-gradient", `linear-gradient(90deg, ${soft.join(", ")})`);
|
|
368
|
+
root.style.setProperty("--ff-route-badge-border", `color-mix(in oklab, ${strong[0] || "#64748b"} 35%, transparent)`);
|
|
369
|
+
root.style.setProperty("--ff-route-outline", strong[0] || "#64748b");
|
|
370
|
+
root.style.setProperty("--ff-route-text", strong[0] || "#111827");
|
|
371
|
+
} else {
|
|
372
|
+
root.style.removeProperty(`--${N}-route-gradient`);
|
|
373
|
+
root.style.removeProperty("--ff-route-outline-gradient");
|
|
374
|
+
root.style.removeProperty("--ff-route-badge-gradient");
|
|
375
|
+
root.style.removeProperty("--ff-route-badge-border");
|
|
376
|
+
root.style.removeProperty("--ff-route-outline");
|
|
377
|
+
root.style.removeProperty("--ff-route-text");
|
|
378
|
+
}
|
|
379
|
+
applyAll();
|
|
380
|
+
affSyncRouteChrome(p);
|
|
381
|
+
};
|
|
382
|
+
affAfterNav();
|
|
383
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", affAfterNav, { once: true });
|
|
384
|
+
document.addEventListener("astro:page-load", affAfterNav);
|
|
385
|
+
document.addEventListener("astro:after-swap", affAfterNav);
|
|
386
|
+
if (typeof MutationObserver === "function") new MutationObserver((muts) => {
|
|
387
|
+
for (let i = 0; i < muts.length; i++) {
|
|
388
|
+
const a = muts[i]?.attributeName;
|
|
389
|
+
if (!a) continue;
|
|
390
|
+
if (a === "style" || a.indexOf("data-ff-enabled-") === 0 || a.indexOf("data-ff-outline-") === 0 || a.indexOf("data-ff-badge-") === 0) {
|
|
391
|
+
affAfterNav();
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}).observe(root, { attributes: true });
|
|
396
|
+
} catch {}
|
|
397
|
+
}
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/dev-outline-css.ts
|
|
400
|
+
const defaultDevOutlineCssOptions = {
|
|
401
|
+
outlineWidth: "2px",
|
|
402
|
+
outlineColor: "rgb(220 38 38)",
|
|
403
|
+
outlineOffset: "-2px",
|
|
404
|
+
borderRadius: "0.5rem",
|
|
405
|
+
badgeLabelWip: "wip",
|
|
406
|
+
badgeLabelByToken: {},
|
|
407
|
+
hiddenStrategy: "visibility",
|
|
408
|
+
badgeMinInlineSize: "8rem"
|
|
409
|
+
};
|
|
410
|
+
function normalizeCssOptions(css, runtime) {
|
|
411
|
+
const byToken = {
|
|
412
|
+
...runtime.flagColorsByToken,
|
|
413
|
+
...css?.outlineColorByToken ?? {}
|
|
414
|
+
};
|
|
415
|
+
const badgeLabelWip = css?.badgeLabelWip ?? css?.badgeLabelDev ?? defaultDevOutlineCssOptions.badgeLabelWip;
|
|
416
|
+
return {
|
|
417
|
+
...defaultDevOutlineCssOptions,
|
|
418
|
+
...css ?? {},
|
|
419
|
+
outlineColorByToken: byToken,
|
|
420
|
+
badgeLabelWip,
|
|
421
|
+
badgeLabelByToken: {
|
|
422
|
+
...defaultDevOutlineCssOptions.badgeLabelByToken,
|
|
423
|
+
...css?.badgeLabelByToken ?? {}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function colorForToken(token, opts) {
|
|
428
|
+
return opts.outlineColorByToken[token] ?? opts.outlineColor;
|
|
429
|
+
}
|
|
430
|
+
/** CSS variable per token, set on <html> from ff.json + dev-toolbar overrides. */
|
|
431
|
+
function featureColorVar(token, namespace) {
|
|
432
|
+
return `--${toToken(namespace) || "ff"}-c-${token}`;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Dev-only styles: `data-ff="token"` (or space-separated tokens) gates outlines/badges.
|
|
436
|
+
* `html[data-ff-route="<token>"]` (dev) adds a fixed top-right route badge (label only).
|
|
437
|
+
* Toolbar toggles: `data-ff-enabled-*`, `data-ff-outline-*`, `data-ff-badge-*`, `--<namespace>-c-*`.
|
|
438
|
+
*/
|
|
439
|
+
function createFeatureFlagStyles(runtime, css) {
|
|
440
|
+
const nsAttr = `data-${toToken(runtime.namespace) || "ff"}`;
|
|
441
|
+
const selectorsForFlag = (flag) => {
|
|
442
|
+
const token = toToken(flag);
|
|
443
|
+
const valueSelectors = [`[${nsAttr}~="${token}"]`];
|
|
444
|
+
if (flag !== token) valueSelectors.push(`[${nsAttr}~="${flag}"]`);
|
|
445
|
+
const baseSelectors = [...valueSelectors, `[${nsAttr}-${token}]`];
|
|
446
|
+
return {
|
|
447
|
+
token,
|
|
448
|
+
baseSelectors,
|
|
449
|
+
selOutline: baseSelectors.map((s) => `${s}:not([${nsAttr}*=" "])`).join(", "),
|
|
450
|
+
selIs: `:is(${baseSelectors.join(", ")})`
|
|
451
|
+
};
|
|
452
|
+
};
|
|
453
|
+
const opts = normalizeCssOptions(css, runtime);
|
|
454
|
+
const badgePos = elementBadgePositionBlock(normalizeElementBadgeLayout(css));
|
|
455
|
+
const chunks = [];
|
|
456
|
+
for (const flag of Object.keys(runtime.flags)) {
|
|
457
|
+
const { token, selOutline, selIs } = selectorsForFlag(flag);
|
|
458
|
+
/** Combo hosts use `[${nsAttr}*=" "]` + `data-ff-label`; skip per-token ::before so combo rules win. */
|
|
459
|
+
const selIsSingleBadge = `${selIs}:not([${nsAttr}*=" "])`;
|
|
460
|
+
const col = colorForToken(token, opts);
|
|
461
|
+
const v = featureColorVar(token, runtime.namespace);
|
|
462
|
+
const label = opts.badgeLabelByToken[token] ?? (token === "wip" ? opts.badgeLabelWip : token);
|
|
463
|
+
const labelEscaped = String(label).replace(/'/g, "\\'");
|
|
464
|
+
chunks.push(`
|
|
465
|
+
html {
|
|
466
|
+
--aff-outline-c-${token}: transparent;
|
|
467
|
+
}
|
|
468
|
+
html:not([data-ff-outline-${token}="off"]) {
|
|
469
|
+
--aff-outline-c-${token}: var(${v}, ${col});
|
|
470
|
+
}
|
|
471
|
+
${selOutline} {
|
|
472
|
+
position: relative;
|
|
473
|
+
outline: ${opts.outlineWidth} solid var(--aff-outline-c-${token});
|
|
474
|
+
outline-offset: ${opts.outlineOffset};
|
|
475
|
+
border-radius: ${opts.borderRadius};
|
|
476
|
+
}
|
|
477
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}::before {
|
|
478
|
+
box-sizing: border-box;
|
|
479
|
+
position: absolute;
|
|
480
|
+
inset: auto;
|
|
481
|
+
margin: 0;
|
|
482
|
+
width: max-content;
|
|
483
|
+
max-width: min(18rem, calc(100% - 0.75rem));
|
|
484
|
+
pointer-events: auto;
|
|
485
|
+
z-index: 20;
|
|
486
|
+
${badgePos}
|
|
487
|
+
display: block;
|
|
488
|
+
content: '${labelEscaped}';
|
|
489
|
+
border: 1px solid color-mix(in oklab, var(${v}, ${col}) 60%, transparent);
|
|
490
|
+
border-radius: 9999px;
|
|
491
|
+
padding: 0.125rem 0.45rem;
|
|
492
|
+
font-size: 0.625rem;
|
|
493
|
+
line-height: 1.2;
|
|
494
|
+
letter-spacing: 0.02em;
|
|
495
|
+
font-weight: 600;
|
|
496
|
+
color: var(${v}, ${col});
|
|
497
|
+
background: color-mix(in oklab, white 85%, var(${v}, ${col}) 15%);
|
|
498
|
+
white-space: nowrap;
|
|
499
|
+
text-overflow: ellipsis;
|
|
500
|
+
overflow: hidden;
|
|
501
|
+
transition: opacity 0.14s ease, background 0.14s ease, border-color 0.14s ease;
|
|
502
|
+
}
|
|
503
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}:hover::before {
|
|
504
|
+
opacity: 0.16;
|
|
505
|
+
background: color-mix(in oklab, white 94%, var(${v}, ${col}) 6%);
|
|
506
|
+
border-color: color-mix(in oklab, var(${v}, ${col}) 35%, transparent);
|
|
507
|
+
}
|
|
508
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-align="start"]::before {
|
|
509
|
+
left: 0.35rem;
|
|
510
|
+
right: auto;
|
|
511
|
+
transform: translateY(calc(-1 * 80%));
|
|
512
|
+
}
|
|
513
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-align="center"]::before {
|
|
514
|
+
left: 50%;
|
|
515
|
+
right: auto;
|
|
516
|
+
transform: translate(-50%, calc(-1 * 80%));
|
|
517
|
+
}
|
|
518
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-align="end"]::before {
|
|
519
|
+
left: auto;
|
|
520
|
+
right: 0.35rem;
|
|
521
|
+
transform: translateY(calc(-1 * 80%));
|
|
522
|
+
}
|
|
523
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-horizontal]::before {
|
|
524
|
+
left: calc(attr(data-ff-horizontal number, 50) * 1%);
|
|
525
|
+
right: auto;
|
|
526
|
+
transform: translate(-50%, calc(-1 * 80%));
|
|
527
|
+
}
|
|
528
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-horizontal][data-ff-anchor="bottom"]::before {
|
|
529
|
+
transform: translate(-50%, 80%);
|
|
530
|
+
}
|
|
531
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-anchor="bottom"]::before {
|
|
532
|
+
top: auto;
|
|
533
|
+
bottom: 0;
|
|
534
|
+
transform: translateY(80%);
|
|
535
|
+
}
|
|
536
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-anchor="bottom"][data-ff-align="center"]::before {
|
|
537
|
+
transform: translate(-50%, 80%);
|
|
538
|
+
}
|
|
539
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-vertical]::before {
|
|
540
|
+
transform: translateY(calc(attr(data-ff-vertical number, 80) * -1%));
|
|
541
|
+
}
|
|
542
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-horizontal][data-ff-vertical]::before {
|
|
543
|
+
transform: translate(-50%, calc(attr(data-ff-vertical number, 80) * -1%));
|
|
544
|
+
}
|
|
545
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-anchor="bottom"][data-ff-vertical]::before {
|
|
546
|
+
transform: translateY(calc(attr(data-ff-vertical number, 80) * 1%));
|
|
547
|
+
}
|
|
548
|
+
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}[data-ff-anchor="bottom"][data-ff-horizontal][data-ff-vertical]::before {
|
|
549
|
+
transform: translate(-50%, calc(attr(data-ff-vertical number, 80) * 1%));
|
|
550
|
+
}
|
|
551
|
+
html[data-ff-enabled-${token}="off"] ${selIs} {
|
|
552
|
+
display: none !important;
|
|
553
|
+
}
|
|
554
|
+
`);
|
|
555
|
+
}
|
|
556
|
+
chunks.push(`
|
|
557
|
+
[${nsAttr}*=" "] {
|
|
558
|
+
position: relative;
|
|
559
|
+
outline: none !important;
|
|
560
|
+
outline-offset: 0 !important;
|
|
561
|
+
border-radius: ${opts.borderRadius};
|
|
562
|
+
}
|
|
563
|
+
html [${nsAttr}*=" "]::after {
|
|
564
|
+
content: "";
|
|
565
|
+
position: absolute;
|
|
566
|
+
inset: 0;
|
|
567
|
+
border-radius: inherit;
|
|
568
|
+
pointer-events: none;
|
|
569
|
+
z-index: 10;
|
|
570
|
+
box-sizing: border-box;
|
|
571
|
+
padding: ${opts.outlineWidth};
|
|
572
|
+
background: var(--ff-combo-gradient, linear-gradient(90deg, #fecaca, #bfdbfe, #bbf7d0));
|
|
573
|
+
-webkit-mask:
|
|
574
|
+
linear-gradient(#000 0 0) content-box,
|
|
575
|
+
linear-gradient(#000 0 0);
|
|
576
|
+
-webkit-mask-composite: xor;
|
|
577
|
+
mask-composite: exclude;
|
|
578
|
+
}
|
|
579
|
+
html [${nsAttr}*=" "]::before {
|
|
580
|
+
box-sizing: border-box;
|
|
581
|
+
position: absolute;
|
|
582
|
+
inset: auto;
|
|
583
|
+
margin: 0;
|
|
584
|
+
width: max-content;
|
|
585
|
+
max-width: min(18rem, calc(100% - 0.75rem));
|
|
586
|
+
pointer-events: auto;
|
|
587
|
+
z-index: 20;
|
|
588
|
+
${badgePos}
|
|
589
|
+
display: block;
|
|
590
|
+
content: attr(${nsAttr});
|
|
591
|
+
background: var(--ff-combo-badge-gradient, var(--ff-combo-gradient-soft, linear-gradient(90deg, #fff1f2, #eff6ff, #f0fdf4))) !important;
|
|
592
|
+
color: var(--ff-combo-text, #111827) !important;
|
|
593
|
+
border: 1px solid var(--ff-combo-badge-border, color-mix(in oklab, #94a3b8 50%, transparent)) !important;
|
|
594
|
+
border-radius: 9999px;
|
|
595
|
+
padding: 0.125rem 0.45rem;
|
|
596
|
+
font-size: 0.625rem;
|
|
597
|
+
line-height: 1.2;
|
|
598
|
+
letter-spacing: 0.02em;
|
|
599
|
+
font-weight: 600;
|
|
600
|
+
white-space: nowrap;
|
|
601
|
+
text-overflow: ellipsis;
|
|
602
|
+
overflow: hidden;
|
|
603
|
+
transition: opacity 0.14s ease, background 0.14s ease, border-color 0.14s ease;
|
|
604
|
+
}
|
|
605
|
+
html [${nsAttr}*=" "][data-ff-label]::before {
|
|
606
|
+
content: attr(data-ff-label) !important;
|
|
607
|
+
}
|
|
608
|
+
html [${nsAttr}*=" "]:hover::before {
|
|
609
|
+
opacity: 0.2;
|
|
610
|
+
background: linear-gradient(90deg, #fff8f9, #f8fbff, #f8fff9);
|
|
611
|
+
border-color: color-mix(in oklab, #94a3b8 35%, transparent);
|
|
612
|
+
}
|
|
613
|
+
`);
|
|
614
|
+
for (const flag of Object.keys(runtime.flags)) {
|
|
615
|
+
const { token, selIs } = selectorsForFlag(flag);
|
|
616
|
+
chunks.push(`
|
|
617
|
+
html[data-ff-badge-${token}="off"] ${selIs}[${nsAttr}*=" "]::before,
|
|
618
|
+
html[data-ff-badge-${token}="off"] ${selIs}[${nsAttr}*=" "][data-ff-label]::before {
|
|
619
|
+
display: none !important;
|
|
620
|
+
content: none !important;
|
|
621
|
+
}
|
|
622
|
+
html[data-ff-badge-${token}="off"] ${selIs}[${nsAttr}*=" "]:hover::before,
|
|
623
|
+
html[data-ff-badge-${token}="off"] ${selIs}[${nsAttr}*=" "][data-ff-label]:hover::before {
|
|
624
|
+
display: none !important;
|
|
625
|
+
opacity: 1 !important;
|
|
626
|
+
background: none !important;
|
|
627
|
+
border-color: transparent !important;
|
|
628
|
+
}
|
|
629
|
+
html[data-ff-outline-${token}="off"] ${selIs}[${nsAttr}*=" "] {
|
|
630
|
+
outline: none !important;
|
|
631
|
+
outline-offset: 0 !important;
|
|
632
|
+
}
|
|
633
|
+
html[data-ff-outline-${token}="off"] ${selIs}[${nsAttr}*=" "]::after {
|
|
634
|
+
display: none !important;
|
|
635
|
+
}
|
|
636
|
+
`);
|
|
637
|
+
}
|
|
638
|
+
chunks.push(`
|
|
639
|
+
.aff-route-frame {
|
|
640
|
+
position: fixed;
|
|
641
|
+
inset: 0.6rem;
|
|
642
|
+
pointer-events: none;
|
|
643
|
+
z-index: 10040;
|
|
644
|
+
box-sizing: border-box;
|
|
645
|
+
border-radius: 0.5rem;
|
|
646
|
+
overflow: hidden;
|
|
647
|
+
}
|
|
648
|
+
.aff-route-frame::before {
|
|
649
|
+
content: "";
|
|
650
|
+
position: absolute;
|
|
651
|
+
inset: 0;
|
|
652
|
+
border-radius: inherit;
|
|
653
|
+
padding: 2px;
|
|
654
|
+
background: var(--ff-route-outline-gradient, linear-gradient(90deg, #ef4444, #3b82f6, #22c55e));
|
|
655
|
+
-webkit-mask:
|
|
656
|
+
linear-gradient(#000 0 0) content-box,
|
|
657
|
+
linear-gradient(#000 0 0);
|
|
658
|
+
-webkit-mask-composite: xor;
|
|
659
|
+
mask-composite: exclude;
|
|
660
|
+
}
|
|
661
|
+
.aff-route-badge {
|
|
662
|
+
position: fixed;
|
|
663
|
+
top: 0.75rem;
|
|
664
|
+
right: 0.75rem;
|
|
665
|
+
z-index: 10060;
|
|
666
|
+
pointer-events: auto;
|
|
667
|
+
box-sizing: border-box;
|
|
668
|
+
display: inline-flex;
|
|
669
|
+
align-items: center;
|
|
670
|
+
max-width: min(34rem, calc(100vw - 1.5rem));
|
|
671
|
+
white-space: nowrap;
|
|
672
|
+
overflow: hidden;
|
|
673
|
+
text-overflow: ellipsis;
|
|
674
|
+
border: 1px solid var(--ff-route-badge-border, color-mix(in oklab, #94a3b8 55%, transparent));
|
|
675
|
+
border-radius: 9999px;
|
|
676
|
+
padding: 0.15rem 0.5rem;
|
|
677
|
+
font-size: 0.625rem;
|
|
678
|
+
line-height: 1.2;
|
|
679
|
+
letter-spacing: 0.02em;
|
|
680
|
+
font-weight: 600;
|
|
681
|
+
color: var(--ff-route-text, #111827);
|
|
682
|
+
background: var(--ff-route-badge-gradient, linear-gradient(90deg, #fff1f2));
|
|
683
|
+
opacity: 1;
|
|
684
|
+
transition: opacity 0.14s ease, border-color 0.14s ease, background 0.14s ease;
|
|
685
|
+
}
|
|
686
|
+
.aff-route-badge:hover {
|
|
687
|
+
opacity: 0.2;
|
|
688
|
+
border-color: color-mix(in oklab, #94a3b8 35%, transparent);
|
|
689
|
+
}
|
|
690
|
+
.aff-route-disabled-overlay {
|
|
691
|
+
position: fixed;
|
|
692
|
+
inset: 0;
|
|
693
|
+
z-index: 10050;
|
|
694
|
+
box-sizing: border-box;
|
|
695
|
+
display: flex;
|
|
696
|
+
align-items: center;
|
|
697
|
+
justify-content: center;
|
|
698
|
+
padding: clamp(1rem, 4vw, 2.5rem);
|
|
699
|
+
text-align: center;
|
|
700
|
+
font: 600 0.95rem/1.45 system-ui, -apple-system, Segoe UI, sans-serif;
|
|
701
|
+
color: #f8fafc;
|
|
702
|
+
background: rgba(15, 23, 42, 0.52);
|
|
703
|
+
backdrop-filter: blur(2px);
|
|
704
|
+
pointer-events: auto;
|
|
705
|
+
}
|
|
706
|
+
.aff-route-disabled-overlay > p {
|
|
707
|
+
margin: 0;
|
|
708
|
+
max-width: min(44rem, calc(100vw - 2rem));
|
|
709
|
+
white-space: pre-line;
|
|
710
|
+
}
|
|
711
|
+
`);
|
|
712
|
+
for (const flag of Object.keys(runtime.flags)) {
|
|
713
|
+
const token = toToken(flag);
|
|
714
|
+
chunks.push(`
|
|
715
|
+
[data-ff-route~="${token}"][data-ff-outline-${token}="off"] #aff-route-frame {
|
|
716
|
+
display: none !important;
|
|
717
|
+
}
|
|
718
|
+
[data-ff-route~="${token}"][data-ff-badge-${token}="off"] #aff-route-badge {
|
|
719
|
+
display: none !important;
|
|
720
|
+
}
|
|
721
|
+
[data-ff-route~="${token}"][data-ff-enabled-${token}="off"] #aff-route-frame {
|
|
722
|
+
display: none !important;
|
|
723
|
+
}
|
|
724
|
+
[data-ff-route~="${token}"][data-ff-enabled-${token}="off"] #aff-route-badge {
|
|
725
|
+
display: none !important;
|
|
726
|
+
}
|
|
727
|
+
`);
|
|
728
|
+
}
|
|
729
|
+
return "\n" + chunks.join("\n") + "\n";
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* CSS-only hiding for disabled flags. The Astro integration no longer injects this into
|
|
733
|
+
* production builds — static HTML is culled instead. Kept for custom tooling or tests.
|
|
734
|
+
*/
|
|
735
|
+
function createProductionGateStyles(runtime) {
|
|
736
|
+
const nsAttr = `data-${toToken(runtime.namespace) || "ff"}`;
|
|
737
|
+
const chunks = [];
|
|
738
|
+
for (const [flag, enabled] of Object.entries(runtime.flags)) {
|
|
739
|
+
if (enabled) continue;
|
|
740
|
+
const token = toToken(flag);
|
|
741
|
+
const valueSelectors = [`[${nsAttr}~="${token}"]`];
|
|
742
|
+
if (flag !== token) valueSelectors.push(`[${nsAttr}~="${flag}"]`);
|
|
743
|
+
chunks.push(`${[...valueSelectors, `[${nsAttr}-${token}]`].join(", ")} { display: none !important; }`);
|
|
744
|
+
}
|
|
745
|
+
return chunks.length ? `\n${chunks.join("\n")}\n` : "";
|
|
746
|
+
}
|
|
747
|
+
function buildAffDevBootstrapScript(flagTokens, colors, outlineDefaults, badgeDefaults, routeFlags, flagNameToToken, namespace) {
|
|
748
|
+
return inlineInvoke(affDevBootstrapRuntime, {
|
|
749
|
+
flagTokens,
|
|
750
|
+
colors,
|
|
751
|
+
outlineDefaults,
|
|
752
|
+
badgeDefaults,
|
|
753
|
+
routeFlags,
|
|
754
|
+
flagNameToToken,
|
|
755
|
+
namespace: toToken(namespace) || "ff"
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
//#endregion
|
|
759
|
+
//#region src/production-html-cull.ts
|
|
760
|
+
function walkHtmlFiles(dir) {
|
|
761
|
+
const out = [];
|
|
762
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
763
|
+
for (const ent of entries) {
|
|
764
|
+
const p = join(dir, ent.name);
|
|
765
|
+
if (ent.isDirectory()) out.push(...walkHtmlFiles(p));
|
|
766
|
+
else if (ent.isFile() && ent.name.endsWith(".html")) out.push(p);
|
|
767
|
+
}
|
|
768
|
+
return out;
|
|
769
|
+
}
|
|
770
|
+
function namespaceAttrName(namespace) {
|
|
771
|
+
return `data-${toToken(namespace) || "ff"}`;
|
|
772
|
+
}
|
|
773
|
+
function elementFlagSlugs(el, nsAttr, knownTokens) {
|
|
774
|
+
const slugSet = /* @__PURE__ */ new Set();
|
|
775
|
+
const combined = el.getAttribute(nsAttr)?.trim();
|
|
776
|
+
if (combined) for (const part of combined.split(/\s+/).filter(Boolean)) slugSet.add(part);
|
|
777
|
+
for (const tk of knownTokens) if (el.hasAttribute(`${nsAttr}-${tk}`)) slugSet.add(tk);
|
|
778
|
+
return [...slugSet];
|
|
779
|
+
}
|
|
780
|
+
function slugToFlagNames(slug, flagNames) {
|
|
781
|
+
const out = [];
|
|
782
|
+
for (const name of flagNames) if (slug === name || slug === toToken(name)) out.push(name);
|
|
783
|
+
return out;
|
|
784
|
+
}
|
|
785
|
+
function shouldCullElement(el, runtime, nsAttr, knownTokens, flagNames) {
|
|
786
|
+
const slugs = elementFlagSlugs(el, nsAttr, knownTokens);
|
|
787
|
+
if (slugs.length === 0) return false;
|
|
788
|
+
for (const slug of slugs) if (slugToFlagNames(slug, flagNames).length === 0) return false;
|
|
789
|
+
for (const slug of slugs) {
|
|
790
|
+
const names = slugToFlagNames(slug, flagNames);
|
|
791
|
+
for (const name of names) if (!runtime.flags[name]) return true;
|
|
792
|
+
}
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
function elementDepth(el) {
|
|
796
|
+
let d = 0;
|
|
797
|
+
let n = el.parentNode;
|
|
798
|
+
while (n) {
|
|
799
|
+
d++;
|
|
800
|
+
n = n.parentNode;
|
|
801
|
+
}
|
|
802
|
+
return d;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Production static HTML: remove nodes gated by disabled flags (same rules as dev
|
|
806
|
+
* `data-*` / `data-*-<token>`), strip dev-only route attributes on `<html>`, and drop
|
|
807
|
+
* empty `<style>` tags left after `featureFlagStyles` is empty.
|
|
808
|
+
*/
|
|
809
|
+
function cullProductionHtml(html, runtime) {
|
|
810
|
+
const root = parse(html);
|
|
811
|
+
const nsAttr = namespaceAttrName(runtime.namespace);
|
|
812
|
+
const flagNames = Object.keys(runtime.flags);
|
|
813
|
+
const knownTokens = flagNames.map((n) => toToken(n));
|
|
814
|
+
const candidates = root.querySelectorAll("*");
|
|
815
|
+
const toRemove = [];
|
|
816
|
+
for (const el of candidates) if (shouldCullElement(el, runtime, nsAttr, knownTokens, flagNames)) toRemove.push(el);
|
|
817
|
+
toRemove.sort((a, b) => elementDepth(b) - elementDepth(a));
|
|
818
|
+
for (const el of toRemove) el.remove();
|
|
819
|
+
const htmlEl = root.querySelector("html");
|
|
820
|
+
if (htmlEl) {
|
|
821
|
+
htmlEl.removeAttribute("data-ff-route");
|
|
822
|
+
htmlEl.removeAttribute("data-ff-route-label");
|
|
823
|
+
}
|
|
824
|
+
const styles = root.querySelectorAll("style");
|
|
825
|
+
for (const st of styles) if (!(st.textContent ?? "").trim()) st.remove();
|
|
826
|
+
return root.outerHTML;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Walk `outDir` (e.g. Astro `dist/`) and apply {@link cullProductionHtml} to every `.html` file.
|
|
830
|
+
*/
|
|
831
|
+
function applyProductionHtmlCullToDist(outDir, runtime) {
|
|
832
|
+
for (const file of walkHtmlFiles(outDir)) {
|
|
833
|
+
const before = readFileSync(file, "utf8");
|
|
834
|
+
const after = cullProductionHtml(before, runtime);
|
|
835
|
+
if (after !== before) writeFileSync(file, after, "utf8");
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
//#endregion
|
|
839
|
+
//#region src/dev-head-inject.ts
|
|
840
|
+
/**
|
|
841
|
+
* Single `injectScript('head-inline', …)` payload for `astro dev`: dev-only outline CSS,
|
|
842
|
+
* `data-ff-route` on `<html>` (pathname + {@link ResolvedFeatureRuntime.routeFlags}), then
|
|
843
|
+
* the feature-flag dev bootstrap (toolbar state, combo badges, route chrome).
|
|
844
|
+
*/
|
|
845
|
+
function buildAffDevHeadInline(args) {
|
|
846
|
+
const { runtime, featureFlagStyles, affDevBootstrap } = args;
|
|
847
|
+
const flagNameToToken = Object.fromEntries(Object.keys(runtime.flags).map((name) => [name, toToken(name)]));
|
|
848
|
+
return `${inlineInvoke(affHeadInlineRuntime, {
|
|
849
|
+
featureFlagStyles,
|
|
850
|
+
routeFlags: runtime.routeFlags,
|
|
851
|
+
flagNameToToken
|
|
852
|
+
})}\n${affDevBootstrap}`;
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region src/route-prefix-js.ts
|
|
856
|
+
/**
|
|
857
|
+
* Shared JS helper source used by generated browser scripts.
|
|
858
|
+
* Keep this in sync with runtime.routePatternToPrefix semantics.
|
|
859
|
+
*/
|
|
860
|
+
function routePrefixJsHelper(functionName) {
|
|
861
|
+
return `function ${functionName}(pattern){var p=String(pattern||'').trim();if(p.endsWith('/**'))p=p.slice(0,-3);else if(p.endsWith('/*'))p=p.slice(0,-2);else if(p.length>1&&p.endsWith('*'))p=p.slice(0,-1);return p.endsWith('/')?p:p+'/';}`;
|
|
862
|
+
}
|
|
863
|
+
//#endregion
|
|
864
|
+
//#region src/index.ts
|
|
865
|
+
/** Prefer `prod` if present; else first non-`dev` key (sorted); else `"prod"`. */
|
|
866
|
+
function primaryNonDevEnvironmentKey(flagsByEnvironment) {
|
|
867
|
+
const keys = Object.keys(flagsByEnvironment);
|
|
868
|
+
if (keys.includes("prod")) return "prod";
|
|
869
|
+
return keys.filter((k) => k !== "dev").sort()[0] ?? "prod";
|
|
870
|
+
}
|
|
871
|
+
function withDefaultEnvironments(options = {}) {
|
|
872
|
+
const mode = options.mode ?? process.env.NODE_ENV ?? "development";
|
|
873
|
+
const raw = options.environments && typeof options.environments === "object" && !Array.isArray(options.environments) ? { ...options.environments } : {};
|
|
874
|
+
delete raw.dev;
|
|
875
|
+
const environments = {
|
|
876
|
+
dev: { when: mode !== "production" },
|
|
877
|
+
...raw
|
|
878
|
+
};
|
|
879
|
+
if (Object.keys(environments).length < 2) environments.prod = environments.prod ?? {
|
|
880
|
+
when: mode === "production",
|
|
881
|
+
flags: {}
|
|
882
|
+
};
|
|
883
|
+
return {
|
|
884
|
+
...options,
|
|
885
|
+
environments
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function createVirtualModuleSource(runtime, css, flagsByEnvironment = {}, devBootstrap) {
|
|
889
|
+
const flagNames = Object.keys(runtime.flags);
|
|
890
|
+
const flagTokens = flagNames.map((name) => toToken(name));
|
|
891
|
+
const enumEntries = flagNames.map((name) => ` ${toEnumKey(name)}: ${JSON.stringify(name)}`).join(",\n");
|
|
892
|
+
const tokenEntries = flagNames.map((name) => ` ${toEnumKey(name)}: ${JSON.stringify(toToken(name))}`).join(",\n");
|
|
893
|
+
const styles = createFeatureFlagStyles(runtime, css);
|
|
894
|
+
const colorsJson = JSON.stringify(runtime.flagColorsByToken, null, 2);
|
|
895
|
+
const flagNameToToken = JSON.stringify(Object.fromEntries(flagNames.map((name) => [name, toToken(name)])), null, 2);
|
|
896
|
+
const defaultNonDevEnvironment = primaryNonDevEnvironmentKey(flagsByEnvironment);
|
|
897
|
+
const bootstrap = devBootstrap ?? buildAffDevBootstrapScript(flagTokens, { ...runtime.flagColorsByToken }, { ...runtime.flagOutlineDefaultsByToken }, { ...runtime.flagBadgeDefaultsByToken }, { ...runtime.routeFlags }, Object.fromEntries(flagNames.map((name) => [name, toToken(name)])), runtime.namespace);
|
|
898
|
+
return `${routePrefixJsHelper("affRoutePatternToPrefix")}
|
|
899
|
+
|
|
900
|
+
export const FeatureFlag = Object.freeze({
|
|
901
|
+
${enumEntries}
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
export const FeatureToken = Object.freeze({
|
|
905
|
+
${tokenEntries}
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
export const featureFlagTokens = Object.freeze(${JSON.stringify(flagTokens)});
|
|
909
|
+
export const featureFlagColors = Object.freeze(${colorsJson});
|
|
910
|
+
export const featureFlags = Object.freeze(${JSON.stringify(runtime.flags, null, 2)});
|
|
911
|
+
export const featureFlagsByEnvironment = Object.freeze(${JSON.stringify(flagsByEnvironment, null, 2)});
|
|
912
|
+
export const defaultNonDevEnvironment = ${JSON.stringify(defaultNonDevEnvironment)};
|
|
913
|
+
export const featureRouteFlags = Object.freeze(${JSON.stringify(runtime.routeFlags, null, 2)});
|
|
914
|
+
export const featureNamespace = ${JSON.stringify(runtime.namespace)};
|
|
915
|
+
/** True when Astro is running the dev server or dev build (import.meta.env.DEV). Not the same as the reserved \`dev\` environment layer — see \`activeEnvironmentKey\`. */
|
|
916
|
+
export const isAstroDev = import.meta.env.DEV;
|
|
917
|
+
export const activeEnvironmentKey = ${JSON.stringify(runtime.activeEnvironment)};
|
|
918
|
+
|
|
919
|
+
export const affDevBootstrap = import.meta.env.DEV ? ${JSON.stringify(bootstrap)} : '';
|
|
920
|
+
|
|
921
|
+
export function isFeatureEnabled(flag) {
|
|
922
|
+
return Boolean(featureFlags[flag]);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
export function shouldRenderFeature(flag) {
|
|
926
|
+
return isFeatureEnabled(flag);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export function isFeatureRoute(pathname) {
|
|
930
|
+
const normalized = pathname.endsWith('/') ? pathname : pathname + '/';
|
|
931
|
+
return Object.keys(featureRouteFlags).some((route) => {
|
|
932
|
+
const candidate = affRoutePatternToPrefix(route);
|
|
933
|
+
return normalized === candidate || normalized.startsWith(candidate);
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
export function shouldIncludePath(pathname) {
|
|
938
|
+
if (import.meta.env.DEV) return true;
|
|
939
|
+
const normalized = pathname.endsWith('/') ? pathname : pathname + '/';
|
|
940
|
+
for (const [route, flags] of Object.entries(featureRouteFlags)) {
|
|
941
|
+
const candidate = affRoutePatternToPrefix(route);
|
|
942
|
+
if (normalized === candidate || normalized.startsWith(candidate)) {
|
|
943
|
+
return flags.every((flag) => isFeatureEnabled(flag));
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export function flagsForEnvironment(envName) {
|
|
950
|
+
var F = featureFlagsByEnvironment[envName];
|
|
951
|
+
return F != null ? F : null;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
export function isFeatureEnabledForEnvironment(flag, envName) {
|
|
955
|
+
var F = featureFlagsByEnvironment[envName];
|
|
956
|
+
return !!(F && F[flag]);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export function shouldIncludePathForEnvironment(pathname, envName) {
|
|
960
|
+
var F = featureFlagsByEnvironment[envName];
|
|
961
|
+
if (!F) return true;
|
|
962
|
+
const normalized = pathname.endsWith('/') ? pathname : pathname + '/';
|
|
963
|
+
for (const [route, flagList] of Object.entries(featureRouteFlags)) {
|
|
964
|
+
const candidate = affRoutePatternToPrefix(route);
|
|
965
|
+
if (normalized === candidate || normalized.startsWith(candidate)) {
|
|
966
|
+
return flagList.every((flag) => Boolean(F[flag]));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const flagNameToToken = Object.freeze(${flagNameToToken});
|
|
973
|
+
|
|
974
|
+
export function matchedFeatureRoutePrefix(pathname) {
|
|
975
|
+
if (!pathname) return null;
|
|
976
|
+
const normalized = pathname.endsWith('/') ? pathname : pathname + '/';
|
|
977
|
+
let best = null;
|
|
978
|
+
let bestLen = -1;
|
|
979
|
+
for (const routePrefix of Object.keys(featureRouteFlags)) {
|
|
980
|
+
const route = affRoutePatternToPrefix(routePrefix);
|
|
981
|
+
if (normalized === route || normalized.startsWith(route)) {
|
|
982
|
+
if (route.length > bestLen) {
|
|
983
|
+
bestLen = route.length;
|
|
984
|
+
best = routePrefix;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return best;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
export function routeFeatureTokenForPath(pathname) {
|
|
992
|
+
const prefix = matchedFeatureRoutePrefix(pathname);
|
|
993
|
+
if (!prefix) return undefined;
|
|
994
|
+
const flagNames = featureRouteFlags[prefix] || [];
|
|
995
|
+
const first = flagNames[0];
|
|
996
|
+
return first != null ? flagNameToToken[first] : undefined;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
export function routeFeatureTokensForPath(pathname) {
|
|
1000
|
+
if (!pathname) return [];
|
|
1001
|
+
const normalized = pathname.endsWith('/') ? pathname : pathname + '/';
|
|
1002
|
+
const out = [];
|
|
1003
|
+
const seen = new Set();
|
|
1004
|
+
for (const [routePrefix, flagNames] of Object.entries(featureRouteFlags)) {
|
|
1005
|
+
const route = affRoutePatternToPrefix(routePrefix);
|
|
1006
|
+
if (!(normalized === route || normalized.startsWith(route))) continue;
|
|
1007
|
+
for (const name of flagNames || []) {
|
|
1008
|
+
const token = flagNameToToken[name];
|
|
1009
|
+
if (!token || seen.has(token)) continue;
|
|
1010
|
+
seen.add(token);
|
|
1011
|
+
out.push(token);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return out;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
export function routeFeatureMatchesForPath(pathname) {
|
|
1018
|
+
if (!pathname) return [];
|
|
1019
|
+
const normalized = pathname.endsWith('/') ? pathname : pathname + '/';
|
|
1020
|
+
var byFlag = {};
|
|
1021
|
+
for (const [routePattern, flagNames] of Object.entries(featureRouteFlags)) {
|
|
1022
|
+
const candidate = affRoutePatternToPrefix(routePattern);
|
|
1023
|
+
if (!(normalized === candidate || normalized.startsWith(candidate))) continue;
|
|
1024
|
+
for (const flagName of flagNames || []) {
|
|
1025
|
+
const token = flagNameToToken[flagName];
|
|
1026
|
+
if (!token) continue;
|
|
1027
|
+
if (!byFlag[flagName]) byFlag[flagName] = { flag: flagName, token, routes: [] };
|
|
1028
|
+
byFlag[flagName].routes.push(routePattern);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return Object.values(byFlag);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
export const featureFlagStyles = import.meta.env.DEV ? ${JSON.stringify(styles)} : "";
|
|
1035
|
+
`;
|
|
1036
|
+
}
|
|
1037
|
+
function getResolvedFeatures(options = {}) {
|
|
1038
|
+
return resolveFeatureRuntime(withDefaultEnvironments(options));
|
|
1039
|
+
}
|
|
1040
|
+
function featureRouteIncluded(pathname, runtime) {
|
|
1041
|
+
return shouldIncludeRoute({
|
|
1042
|
+
pathname,
|
|
1043
|
+
routeFlags: runtime.routeFlags,
|
|
1044
|
+
flags: runtime.flags,
|
|
1045
|
+
isDev: runtime.isDev
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
function astroFeatureFlags(options = {}) {
|
|
1049
|
+
const { css, staticMinify = true, ...flagOpts } = options;
|
|
1050
|
+
const opts = withDefaultEnvironments(flagOpts);
|
|
1051
|
+
const mode = opts.mode ?? process.env.NODE_ENV ?? "development";
|
|
1052
|
+
const runtime = resolveFeatureRuntime({
|
|
1053
|
+
...opts,
|
|
1054
|
+
mode
|
|
1055
|
+
});
|
|
1056
|
+
return {
|
|
1057
|
+
name: "astro-feature-flags",
|
|
1058
|
+
hooks: {
|
|
1059
|
+
"astro:config:setup": ({ updateConfig, addDevToolbarApp, command, injectScript }) => {
|
|
1060
|
+
const flagNames = Object.keys(runtime.flags);
|
|
1061
|
+
const flagTokens = flagNames.map((name) => toToken(name));
|
|
1062
|
+
const flagsByEnvironment = resolveFeatureFlagsByEnvironment(opts);
|
|
1063
|
+
const flagNameToToken = Object.fromEntries(flagNames.map((name) => [name, toToken(name)]));
|
|
1064
|
+
const bootstrap = buildAffDevBootstrapScript(flagTokens, { ...runtime.flagColorsByToken }, { ...runtime.flagOutlineDefaultsByToken }, { ...runtime.flagBadgeDefaultsByToken }, { ...runtime.routeFlags }, flagNameToToken, runtime.namespace);
|
|
1065
|
+
if (command === "dev" && runtime.isDev && typeof addDevToolbarApp === "function") addDevToolbarApp({
|
|
1066
|
+
id: "astro-feature-flags",
|
|
1067
|
+
name: "Feature flags",
|
|
1068
|
+
icon: DEV_TOOLBAR_FLAG_ICON_SVG,
|
|
1069
|
+
entrypoint: fileURLToPath(new URL("./dev-toolbar-app.mjs", import.meta.url))
|
|
1070
|
+
});
|
|
1071
|
+
if (command === "dev" && runtime.isDev && typeof injectScript === "function") injectScript("head-inline", buildAffDevHeadInline({
|
|
1072
|
+
runtime,
|
|
1073
|
+
featureFlagStyles: createFeatureFlagStyles(runtime, css),
|
|
1074
|
+
affDevBootstrap: bootstrap
|
|
1075
|
+
}));
|
|
1076
|
+
updateConfig({ vite: { plugins: [{
|
|
1077
|
+
name: "astro-feature-flags:virtual-module",
|
|
1078
|
+
resolveId(id) {
|
|
1079
|
+
if (id === "virtual:astro-feature-flags") return "\0virtual:astro-feature-flags";
|
|
1080
|
+
return null;
|
|
1081
|
+
},
|
|
1082
|
+
load(id) {
|
|
1083
|
+
if (id === "\0virtual:astro-feature-flags") return createVirtualModuleSource(runtime, css, flagsByEnvironment, bootstrap);
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
}] } });
|
|
1087
|
+
},
|
|
1088
|
+
"astro:build:done": ({ dir }) => {
|
|
1089
|
+
if (runtime.isDev || !staticMinify) return;
|
|
1090
|
+
const outDir = fileURLToPath(dir);
|
|
1091
|
+
const prunePaths = routePathsToPrune({
|
|
1092
|
+
routeFlags: runtime.routeFlags,
|
|
1093
|
+
flags: runtime.flags
|
|
1094
|
+
});
|
|
1095
|
+
for (const routePath of prunePaths) rmSync(join(outDir, routePath), {
|
|
1096
|
+
recursive: true,
|
|
1097
|
+
force: true
|
|
1098
|
+
});
|
|
1099
|
+
applyProductionHtmlCullToDist(outDir, runtime);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
//#endregion
|
|
1105
|
+
export { applyProductionHtmlCullToDist, createFeatureFlagStyles, createProductionGateStyles, createVirtualModuleSource, cullProductionHtml, astroFeatureFlags as default, elementBadgePositionBlock, featureRouteIncluded, getResolvedFeatures, longestMatchingRoutePrefix, mergeFlagsWithProcessEnvOverrides, normalizeElementBadgeLayout, primaryNonDevEnvironmentKey, resolveFeatureFlagsByEnvironment, routePatternToPrefix };
|
|
1106
|
+
|
|
1107
|
+
//# sourceMappingURL=index.mjs.map
|