@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.mjs","names":[],"sources":["../src/runtime.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { isAbsolute, join } from \"node:path\";\n\nexport type FeatureFlagMap = Record<string, boolean>;\nexport type FeatureRouteMap = Record<string, string[]>;\nexport type FeatureColorMap = Record<string, string>;\nexport type FeatureBoolByTokenMap = Record<string, boolean>;\n\nexport interface FeatureConfig {\n namespace: string;\n flags: FeatureFlagMap;\n routeFlags: FeatureRouteMap;\n colors: FeatureColorMap;\n outlineDefaultsByToken: FeatureBoolByTokenMap;\n badgeDefaultsByToken: FeatureBoolByTokenMap;\n mode: string;\n /** Which `environments` entry was used (reserved `dev` = all flags on, toolbar layer). */\n activeEnvironment: string;\n}\n\n/**\n * Declarative feature flags for Astro. Merge order: inline options → optional root\n * {@link ResolveFeatureRuntimeOptions.jsonConfigPath} → optional per-environment\n * `environments.<name>.jsonConfigPath` (merged when that layer is active).\n *\n * Exactly one `when: true` across environments unless `forceEnvironment` or `AFF_ENVIRONMENT`\n * selects the layer. A reserved **`dev`** environment is required (all flags on at resolve time;\n * toggles are dev-toolbar only) plus at least one other environment (e.g. `prod`).\n */\nexport interface ResolveFeatureRuntimeOptions {\n /**\n * Directory used to resolve relative {@link jsonConfigPath} values (root and per-environment).\n * Defaults to `process.cwd()`.\n */\n configRoot?: string;\n /** Optional main JSON file merged after inline `flags` / `environments`. */\n jsonConfigPath?: string;\n /** Passed through to `ResolvedFeatureRuntime.mode` (e.g. `process.env.NODE_ENV`). */\n mode?: string;\n /** Process env for `AFF_FEATURE_*` and `ASTRO_FEATURE_FLAGS` (skipped when active env is `dev`). */\n env?: Record<string, string | undefined>;\n /**\n * Use this `environments` entry regardless of `when` / `AFF_ENVIRONMENT` (e.g. sitemaps, tests).\n */\n forceEnvironment?: string;\n tokenNamespace?: string;\n flags?: Record<\n string,\n {\n color?: string;\n colour?: string;\n outline?: boolean;\n badge?: boolean;\n routes?: string[];\n }\n >;\n environments?: Record<\n string,\n {\n when?: boolean;\n flags?: Record<string, boolean>;\n /** Optional JSON merged after the root file when this environment is active. */\n jsonConfigPath?: string;\n }\n >;\n}\n\nfunction toBoolean(value: unknown, fallback = false): boolean {\n if (typeof value === \"boolean\") return value;\n if (typeof value !== \"string\") return fallback;\n const normalized = value.trim().toLowerCase();\n if ([\"1\", \"true\", \"yes\", \"on\"].includes(normalized)) return true;\n if ([\"0\", \"false\", \"no\", \"off\"].includes(normalized)) return false;\n return fallback;\n}\n\nfunction readJsonIfExists(pathname: string): Record<string, unknown> {\n if (!existsSync(pathname)) return {};\n return JSON.parse(readFileSync(pathname, \"utf8\")) as Record<string, unknown>;\n}\n\nexport function normalizePath(pathname: string): string {\n if (!pathname) return \"/\";\n return pathname.endsWith(\"/\") ? pathname : `${pathname}/`;\n}\n\n/**\n * Normalizes a route key from `ff.json` for prefix matching.\n * Supports wildcards: `/blog/*`, `/docs/**` → prefix `/blog/`, `/docs/`.\n */\nexport function routePatternToPrefix(pattern: string): string {\n let p = pattern.trim();\n if (p.endsWith(\"/**\")) {\n p = p.slice(0, -3);\n } else if (p.endsWith(\"/*\")) {\n p = p.slice(0, -2);\n } else if (p.endsWith(\"*\") && p.length > 1) {\n p = p.slice(0, -1);\n }\n return normalizePath(p);\n}\n\nexport function toEnumKey(flagName: string): string {\n return flagName\n .replace(/[^a-zA-Z0-9]+/g, \" \")\n .trim()\n .split(/\\s+/)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"\");\n}\n\nexport function toToken(flagName: string): string {\n return flagName\n .replace(/[^a-zA-Z0-9]+/g, \"-\")\n .replace(/([a-z0-9])([A-Z])/g, \"$1-$2\")\n .replace(/([a-zA-Z])([0-9])/g, \"$1-$2\")\n .replace(/([0-9])([a-zA-Z])/g, \"$1-$2\")\n .toLowerCase()\n .replace(/-+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n\nfunction mergeRecords<T extends Record<string, unknown>>(\n base: T,\n ...layers: Partial<T>[]\n): T {\n const out = { ...base };\n for (const layer of layers) {\n for (const [k, v] of Object.entries(layer)) {\n if (v === undefined) continue;\n const cur = out[k as keyof T];\n if (\n v &&\n typeof v === \"object\" &&\n !Array.isArray(v) &&\n cur &&\n typeof cur === \"object\" &&\n !Array.isArray(cur)\n ) {\n (out as Record<string, unknown>)[k] = mergeRecords(\n cur as Record<string, unknown>,\n v as Record<string, unknown>,\n );\n } else {\n (out as Record<string, unknown>)[k] = v;\n }\n }\n }\n return out;\n}\n\nfunction resolvePath(relativeOrAbsolute: string, configRoot: string): string {\n return isAbsolute(relativeOrAbsolute)\n ? relativeOrAbsolute\n : join(configRoot, relativeOrAbsolute);\n}\n\ntype NormalizedFlagDef = {\n color?: string;\n outline: boolean;\n badge: boolean;\n routes: string[];\n};\n\ntype NormalizedEnvEntry = {\n when?: boolean;\n flags: Record<string, boolean>;\n /** Per-environment JSON merged after root `jsonConfigPath` when this layer is active. */\n jsonConfigPath?: string;\n};\n\ntype DeclarativeFeatureConfig = {\n tokenNamespace: string;\n flags: Record<string, NormalizedFlagDef>;\n environments: Record<string, NormalizedEnvEntry>;\n};\n\nfunction normalizeDeclarativeConfig(\n raw: Record<string, unknown>,\n mode: string = process.env.NODE_ENV ?? \"development\",\n): DeclarativeFeatureConfig {\n const tokenNamespaceRaw =\n (typeof raw.tokenNamespace === \"string\" ? raw.tokenNamespace : undefined) ??\n (typeof raw.namespace === \"string\" ? raw.namespace : undefined) ??\n \"ff\";\n\n const flagsRaw =\n raw.flags && typeof raw.flags === \"object\" && !Array.isArray(raw.flags)\n ? (raw.flags as Record<string, unknown>)\n : {};\n const flags: Record<string, NormalizedFlagDef> = {};\n for (const [flagName, maybeDef] of Object.entries(flagsRaw)) {\n if (!maybeDef || typeof maybeDef !== \"object\" || Array.isArray(maybeDef))\n continue;\n const def = maybeDef as Record<string, unknown>;\n const color =\n (typeof def.colour === \"string\" ? def.colour : undefined) ??\n (typeof def.color === \"string\" ? def.color : undefined);\n const outline = typeof def.outline === \"boolean\" ? def.outline : true;\n const badge = typeof def.badge === \"boolean\" ? def.badge : true;\n const routes = Array.isArray(def.routes)\n ? def.routes.filter((v): v is string => typeof v === \"string\")\n : [];\n flags[flagName] = { color, outline, badge, routes };\n }\n\n const envRaw =\n raw.environments &&\n typeof raw.environments === \"object\" &&\n !Array.isArray(raw.environments)\n ? (raw.environments as Record<string, unknown>)\n : {};\n const environments: Record<string, NormalizedEnvEntry> = {};\n for (const [envName, maybeEnv] of Object.entries(envRaw)) {\n if (envName === \"dev\") continue;\n if (!maybeEnv || typeof maybeEnv !== \"object\" || Array.isArray(maybeEnv))\n continue;\n const env = maybeEnv as Record<string, unknown>;\n const envFlagsRaw =\n env.flags && typeof env.flags === \"object\" && !Array.isArray(env.flags)\n ? (env.flags as Record<string, unknown>)\n : {};\n const envFlags: Record<string, boolean> = {};\n for (const [flagName, value] of Object.entries(envFlagsRaw)) {\n envFlags[flagName] = Boolean(value);\n }\n const when = typeof env.when === \"boolean\" ? env.when : undefined;\n const layerJson =\n typeof env.jsonConfigPath === \"string\" ? env.jsonConfigPath : undefined;\n environments[envName] = {\n ...(when !== undefined ? { when } : {}),\n ...(layerJson ? { jsonConfigPath: layerJson } : {}),\n flags: envFlags,\n };\n }\n\n environments.dev = {\n when: mode !== \"production\",\n flags: {},\n };\n\n return {\n tokenNamespace: tokenNamespaceRaw,\n flags,\n environments,\n };\n}\n\nfunction assertEnvironmentShape(environments: Record<string, NormalizedEnvEntry>) {\n const keys = Object.keys(environments);\n if (!environments.dev) {\n throw new Error(\n 'astro-feature-flags: environments must include a reserved \"dev\" entry.',\n );\n }\n if (keys.length < 2) {\n throw new Error(\n \"astro-feature-flags: environments must include `dev` plus at least one other layer (e.g. `prod`).\",\n );\n }\n}\n\nfunction countActiveWhen(environments: Record<string, NormalizedEnvEntry>): number {\n return Object.values(environments).filter((e) => e.when === true).length;\n}\n\nfunction resolveAffEnvironment(\n options: ResolveFeatureRuntimeOptions,\n): string | undefined {\n const proc =\n options.env ?? (process.env as Record<string, string | undefined>);\n return proc.AFF_ENVIRONMENT?.trim();\n}\n\nfunction resolveActiveEnvironmentName(\n normalized: DeclarativeFeatureConfig,\n options: ResolveFeatureRuntimeOptions,\n): string {\n const { environments } = normalized;\n assertEnvironmentShape(environments);\n\n const forced = options.forceEnvironment?.trim();\n if (forced && environments[forced]) return forced;\n\n const aff = resolveAffEnvironment(options);\n if (aff && environments[aff]) return aff;\n\n const n = countActiveWhen(environments);\n if (n !== 1) {\n const activeNames = Object.entries(environments)\n .filter(([, e]) => e.when === true)\n .map(([k]) => k);\n const devWhen = environments.dev?.when;\n const hint =\n n > 1 && environments.dev && devWhen === true\n ? \" The reserved `dev` environment is active for this `mode` while another layer also has `when: true`. Pin one layer with `forceEnvironment` or `AFF_ENVIRONMENT`, or adjust non-dev `when` predicates.\"\n : \"\";\n throw new Error(\n `astro-feature-flags: exactly one environment must have when: true (found ${n}${activeNames.length ? `: ${activeNames.join(\", \")}` : \"\"}).${hint} Set forceEnvironment or AFF_ENVIRONMENT to pick a layer explicitly.`,\n );\n }\n for (const [name, cfg] of Object.entries(environments)) {\n if (cfg.when === true) return name;\n }\n throw new Error(\"astro-feature-flags: internal error resolving environment.\");\n}\n\nfunction readBaseMergedRaw(options: ResolveFeatureRuntimeOptions): Record<string, unknown> {\n const configRoot = options.configRoot ?? process.cwd();\n const {\n tokenNamespace = \"ff\",\n flags = {},\n environments = {},\n } = options;\n const inlineRaw: Record<string, unknown> = {\n tokenNamespace,\n flags,\n environments,\n };\n let merged = { ...inlineRaw };\n if (options.jsonConfigPath) {\n const p = resolvePath(options.jsonConfigPath, configRoot);\n merged = mergeRecords(merged as Record<string, unknown>, readJsonIfExists(p));\n }\n return merged;\n}\n\nfunction mergePerEnvironmentJson(\n baseRaw: Record<string, unknown>,\n envName: string,\n configRoot: string,\n mode: string,\n): Record<string, unknown> {\n if (envName === \"dev\") return baseRaw;\n const normalized = normalizeDeclarativeConfig(baseRaw, mode);\n const entry = normalized.environments[envName];\n const rel = entry?.jsonConfigPath;\n if (!rel) return baseRaw;\n const p = resolvePath(rel, configRoot);\n return mergeRecords(baseRaw, readJsonIfExists(p));\n}\n\nfunction readMergedRawForActiveEnv(\n options: ResolveFeatureRuntimeOptions,\n envName: string,\n): Record<string, unknown> {\n const configRoot = options.configRoot ?? process.cwd();\n const mode = options.mode ?? process.env.NODE_ENV ?? \"development\";\n const base = readBaseMergedRaw(options);\n return mergePerEnvironmentJson(base, envName, configRoot, mode);\n}\n\nexport function loadFeatureConfig(\n options: ResolveFeatureRuntimeOptions = {},\n): FeatureConfig {\n const mode = options.mode ?? process.env.NODE_ENV ?? \"development\";\n const baseRaw = readBaseMergedRaw(options);\n const baseNorm = normalizeDeclarativeConfig(baseRaw, mode);\n const envName = resolveActiveEnvironmentName(baseNorm, options);\n const fullRaw = readMergedRawForActiveEnv(options, envName);\n const normalized = normalizeDeclarativeConfig(fullRaw, mode);\n\n const envFlags = normalized.environments[envName]?.flags ?? {};\n const routeFlags: FeatureRouteMap = {};\n const colors: FeatureColorMap = {};\n const outlineDefaultsByToken: FeatureBoolByTokenMap = {};\n const badgeDefaultsByToken: FeatureBoolByTokenMap = {};\n const resolvedFlags: FeatureFlagMap = {};\n\n for (const [flagName, flagDef] of Object.entries(normalized.flags)) {\n if (envName === \"dev\") {\n resolvedFlags[flagName] = true;\n } else {\n resolvedFlags[flagName] = envFlags[flagName] === true;\n }\n if (flagDef.color) colors[flagName] = flagDef.color;\n const token = toToken(flagName);\n outlineDefaultsByToken[token] = flagDef.outline;\n badgeDefaultsByToken[token] = flagDef.badge;\n for (const route of flagDef.routes) {\n routeFlags[route] = [...(routeFlags[route] ?? []), flagName];\n }\n }\n\n return {\n namespace: normalized.tokenNamespace || \"ff\",\n flags: resolvedFlags,\n routeFlags,\n colors,\n outlineDefaultsByToken,\n badgeDefaultsByToken,\n mode,\n activeEnvironment: envName,\n };\n}\n\n/** Applies `AFF_FEATURE_*` and `ASTRO_FEATURE_FLAGS` on top of a resolved flag map. */\nexport function mergeFlagsWithProcessEnvOverrides(\n flags: FeatureFlagMap,\n env: Record<string, string | undefined> = process.env as Record<\n string,\n string | undefined\n >,\n): FeatureFlagMap {\n const resolvedFlags: FeatureFlagMap = { ...flags };\n for (const [key, value] of Object.entries(resolvedFlags)) {\n const slug = toToken(key).replace(/-/g, \"_\").toUpperCase();\n const envKey = `AFF_FEATURE_${slug}`;\n resolvedFlags[key] = toBoolean(env[envKey], Boolean(value));\n }\n const envMapRaw = env.ASTRO_FEATURE_FLAGS;\n if (envMapRaw) {\n try {\n const envMap = JSON.parse(envMapRaw) as Record<string, unknown>;\n for (const [flag, value] of Object.entries(envMap)) {\n resolvedFlags[flag] = Boolean(value);\n }\n } catch {\n // ignore invalid JSON override\n }\n }\n return resolvedFlags;\n}\n\n/**\n * Resolved flag booleans for each `environments` key, after the same process-env overrides\n * as {@link resolveFeatureRuntime} (overrides skipped for the reserved `dev` layer).\n */\nexport function resolveFeatureFlagsByEnvironment(\n options: ResolveFeatureRuntimeOptions = {},\n): Record<string, FeatureFlagMap> {\n const baseRaw = readBaseMergedRaw(options);\n const mode = options.mode ?? process.env.NODE_ENV ?? \"development\";\n const normalized = normalizeDeclarativeConfig(baseRaw, mode);\n const envKeys = Object.keys(normalized.environments);\n const proc = options.env ?? (process.env as Record<string, string | undefined>);\n const out: Record<string, FeatureFlagMap> = {};\n for (const envName of envKeys) {\n const cfg = loadFeatureConfig({ ...options, forceEnvironment: envName });\n out[envName] =\n envName === \"dev\"\n ? { ...cfg.flags }\n : mergeFlagsWithProcessEnvOverrides({ ...cfg.flags }, proc);\n }\n return out;\n}\n\n/** Normalize colour map keys (flag names or tokens) to CSS tokens. */\nexport function colorsToTokenMap(\n colors: FeatureColorMap,\n): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, value] of Object.entries(colors)) {\n out[toToken(key)] = value;\n }\n return out;\n}\n\nexport interface ResolvedFeatureRuntime {\n namespace: string;\n mode: string;\n /** True when the active environment is the reserved `dev` layer (toolbar + all flags on). */\n isDev: boolean;\n activeEnvironment: string;\n flags: FeatureFlagMap;\n routeFlags: FeatureRouteMap;\n /** Outline/badge/route-frame colour per flag token (CSS colour strings). */\n flagColorsByToken: Record<string, string>;\n /** Default dev-toolbar outline/badge state per flag token. */\n flagOutlineDefaultsByToken: Record<string, boolean>;\n flagBadgeDefaultsByToken: Record<string, boolean>;\n}\n\nexport function resolveFeatureRuntime(\n options: ResolveFeatureRuntimeOptions = {},\n): ResolvedFeatureRuntime {\n const env = options.env ?? (process.env as Record<string, string | undefined>);\n const config = loadFeatureConfig(options);\n const isDevLayer = config.activeEnvironment === \"dev\";\n const resolvedFlags = isDevLayer\n ? { ...config.flags }\n : mergeFlagsWithProcessEnvOverrides(config.flags, env);\n\n const colorByToken = colorsToTokenMap(config.colors);\n\n return {\n namespace: config.namespace,\n mode: config.mode,\n isDev: isDevLayer,\n activeEnvironment: config.activeEnvironment,\n flags: resolvedFlags,\n routeFlags: config.routeFlags,\n flagColorsByToken: colorByToken,\n flagOutlineDefaultsByToken: config.outlineDefaultsByToken,\n flagBadgeDefaultsByToken: config.badgeDefaultsByToken,\n };\n}\n\nexport function isFlagEnabled(flags: FeatureFlagMap, flag: string): boolean {\n return Boolean(flags?.[flag]);\n}\n\nexport function isRouteFlagged(\n pathname: string,\n routeFlags: FeatureRouteMap,\n): boolean {\n const normalized = normalizePath(pathname);\n return Object.keys(routeFlags || {}).some((routePrefix) => {\n const route = routePatternToPrefix(routePrefix);\n return normalized === route || normalized.startsWith(route);\n });\n}\n\nexport function shouldIncludeRoute({\n pathname,\n routeFlags,\n flags,\n isDev,\n}: {\n pathname: string;\n routeFlags: FeatureRouteMap;\n flags: FeatureFlagMap;\n isDev: boolean;\n}): boolean {\n if (isDev) return true;\n const normalized = normalizePath(pathname);\n for (const [routePrefix, flagNames] of Object.entries(routeFlags || {})) {\n const route = routePatternToPrefix(routePrefix);\n if (normalized === route || normalized.startsWith(route)) {\n return (flagNames || []).every((flagName) =>\n isFlagEnabled(flags, flagName),\n );\n }\n }\n return true;\n}\n\nexport function routePathsToPrune({\n routeFlags,\n flags,\n}: {\n routeFlags: FeatureRouteMap;\n flags: FeatureFlagMap;\n}): string[] {\n return Object.entries(routeFlags || {})\n .filter(([, flagNames]) =>\n (flagNames || []).some((flagName) => !isFlagEnabled(flags, flagName)),\n )\n .map(([routePath]) =>\n routePatternToPrefix(routePath).replace(/^\\/+|\\/+$/g, \"\"),\n )\n .filter(Boolean);\n}\n\n/**\n * Longest `routeFlags` key that matches `pathname` (prefix match, trailing slashes normalized).\n */\nexport function longestMatchingRoutePrefix(\n pathname: string,\n routeFlags: FeatureRouteMap,\n): string | null {\n const normalized = normalizePath(pathname);\n let best: string | null = null;\n let bestLen = -1;\n for (const routePrefix of Object.keys(routeFlags || {})) {\n const route = routePatternToPrefix(routePrefix);\n if (normalized === route || normalized.startsWith(route)) {\n if (route.length > bestLen) {\n bestLen = route.length;\n best = routePrefix;\n }\n }\n }\n return best;\n}\n"],"mappings":"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"}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Hide Flagged Routes From Sitemaps
|
|
2
|
+
|
|
3
|
+
Use `getResolvedFeatures()` plus `featureRouteIncluded()` in your sitemap filter so routes disabled by feature flags are excluded.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
// astro.config.mjs
|
|
7
|
+
|
|
8
|
+
import sitemap from "@astrojs/sitemap";
|
|
9
|
+
import astroFeatureFlags, {
|
|
10
|
+
featureRouteIncluded,
|
|
11
|
+
getResolvedFeatures,
|
|
12
|
+
} from "@at-flux/astro-feature-flags";
|
|
13
|
+
|
|
14
|
+
const featureFlagOptions = {
|
|
15
|
+
flags: {
|
|
16
|
+
wip: { colour: "rgb(220 38 38)", routes: ["/blog/*"] },
|
|
17
|
+
},
|
|
18
|
+
environments: {
|
|
19
|
+
prod: {
|
|
20
|
+
when: process.env.NODE_ENV === "production",
|
|
21
|
+
flags: { wip: false },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const featureRuntime = getResolvedFeatures(featureFlagOptions);
|
|
27
|
+
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
integrations: [
|
|
30
|
+
astroFeatureFlags(featureFlagOptions),
|
|
31
|
+
sitemap({
|
|
32
|
+
filter: (page) => {
|
|
33
|
+
try {
|
|
34
|
+
const pathname = new URL(page).pathname;
|
|
35
|
+
return featureRouteIncluded(pathname, featureRuntime);
|
|
36
|
+
} catch {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`featureRuntime` only needs to be declared before the `sitemap` filter uses it.
|
|
46
|
+
|
|
47
|
+
You can still place `astroFeatureFlags(featureFlagOptions)` anywhere in `integrations`; the key is that `featureRouteIncluded(pathname, featureRuntime)` has access to the resolved runtime object.
|
|
48
|
+
|
|
49
|
+
If the sitemap plugin runs with a `mode` where the active layer is not the one you want for URL filtering, pass **`forceEnvironment`** (it wins over **`AFF_ENVIRONMENT`** when both are set), for example `getResolvedFeatures({ ...featureFlagOptions, mode: "development", forceEnvironment: "prod" })`. Behaviour is covered by **`test/sitemap-route-filter.test.ts`** in this package.
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Testing `@at-flux/astro-feature-flags`
|
|
2
|
+
|
|
3
|
+
## Unit and integration tests
|
|
4
|
+
|
|
5
|
+
From the package directory:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm test
|
|
9
|
+
pnpm typecheck
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
From the monorepo root:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm --filter @at-flux/astro-feature-flags test
|
|
16
|
+
pnpm --filter @at-flux/astro-feature-flags exec tsc --noEmit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Slow tests (production builds)
|
|
20
|
+
|
|
21
|
+
`test/build-output.test.ts` runs real Astro production builds and asserts:
|
|
22
|
+
|
|
23
|
+
- **Route pruning:** `pnpm --dir example build` — routes gated by disabled flags are absent under `example/dist/`.
|
|
24
|
+
- **SSR / build-time + declarative gating:** fixture `test/fixtures/ssr-gate-site/` imports `featureFlagStyles` (empty in production). With `hotFeature2` off in prod: **`shouldRenderFeature('hotFeature2')` markup is absent** (compile-time). **`data-ff-*` gated nodes are removed** from emitted static HTML by the integration’s post-build pass (no production gate CSS).
|
|
25
|
+
|
|
26
|
+
These checks are **off by default** in CI because they shell out to a full Astro production build.
|
|
27
|
+
|
|
28
|
+
Enable locally or in a dedicated job:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
ENABLE_SLOW=1 pnpm exec vitest run test/build-output.test.ts
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or from the monorepo root:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd packages/astro-feature-flags && ENABLE_SLOW=1 pnpm test
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Set `ENABLE_SLOW=1` in GitHub Actions (or equivalent) when you want this gate on every PR.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Example
|
|
2
|
+
|
|
3
|
+
Run locally:
|
|
4
|
+
|
|
5
|
+
1. `pnpm install`
|
|
6
|
+
2. `pnpm dev`
|
|
7
|
+
3. open `http://localhost:4321/`
|
|
8
|
+
|
|
9
|
+
Build:
|
|
10
|
+
|
|
11
|
+
- `pnpm build`
|
|
12
|
+
|
|
13
|
+
Pages:
|
|
14
|
+
|
|
15
|
+
- `/` integration overview
|
|
16
|
+
- `/hot-feature-1/` `hotFeature1` route + shorthand
|
|
17
|
+
- `/hot/` `hotFeature2` route + shorthand
|
|
18
|
+
- `/hot-dev/sub/` wildcard nested route + combined `dev` + `hotFeature2` gating
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { defineConfig } from "astro/config";
|
|
4
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
5
|
+
import astroFeatureFlags from "@at-flux/astro-feature-flags";
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
server: { port: 4339 },
|
|
9
|
+
trailingSlash: "always",
|
|
10
|
+
integrations: [
|
|
11
|
+
astroFeatureFlags({
|
|
12
|
+
configRoot: fileURLToPath(new URL(".", import.meta.url)),
|
|
13
|
+
jsonConfigPath: "ff.json",
|
|
14
|
+
// Reserved `dev` is injected. Optional per-layer JSON: `environments.<name>.jsonConfigPath`.
|
|
15
|
+
// Exactly one `when: true` for the selected environment unless you pin with forceEnvironment / AFF_ENVIRONMENT.
|
|
16
|
+
environments: {
|
|
17
|
+
prod: {
|
|
18
|
+
when: process.env.NODE_ENV === "production",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
23
|
+
vite: {
|
|
24
|
+
plugins: [tailwindcss()],
|
|
25
|
+
},
|
|
26
|
+
});
|
package/example/ff.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tokenNamespace": "ff",
|
|
3
|
+
"flags": {
|
|
4
|
+
"wip": {
|
|
5
|
+
"colour": "rgb(220 38 38)",
|
|
6
|
+
"routes": ["/dev/*", "/hot-dev/*"]
|
|
7
|
+
},
|
|
8
|
+
"hotFeature1": {
|
|
9
|
+
"colour": "rgb(37 99 235)",
|
|
10
|
+
"routes": ["/hot-feature-1/*"]
|
|
11
|
+
},
|
|
12
|
+
"hotFeature2": {
|
|
13
|
+
"colour": "rgb(34 197 94)",
|
|
14
|
+
"routes": ["/hot/*", "/hot-dev/*"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"environments": {
|
|
18
|
+
"prod": {
|
|
19
|
+
"flags": {
|
|
20
|
+
"wip": false,
|
|
21
|
+
"hotFeature1": true,
|
|
22
|
+
"hotFeature2": false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "astro-feature-flags-example",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"predev": "pnpm --dir .. build",
|
|
7
|
+
"prebuild": "pnpm --dir .. build",
|
|
8
|
+
"dev": "astro dev",
|
|
9
|
+
"build": "astro build",
|
|
10
|
+
"preview": "astro preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@at-flux/astro-feature-flags": "link:..",
|
|
14
|
+
"astro": "^5.18.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@astrojs/check": "^0.9.8",
|
|
18
|
+
"@tailwindcss/vite": "^4.2.2",
|
|
19
|
+
"tailwindcss": "^4.2.2",
|
|
20
|
+
"typescript": "^5.9.3"
|
|
21
|
+
}
|
|
22
|
+
}
|