@at-flux/astro-feature-flags 1.0.3 → 1.0.4
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/README.md +13 -12
- package/dist/index.d.mts +74 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +195 -12
- package/dist/index.mjs.map +1 -1
- package/docs/how-to/hide-from-sitemaps.md +56 -10
- package/package.json +1 -1
- package/src/dev-head-inject.ts +2 -0
- package/src/dev-inline-runtimes.ts +29 -1
- package/src/dev-outline-css.ts +42 -5
- package/src/index.ts +49 -3
- package/src/sitemap-prune.ts +171 -0
package/README.md
CHANGED
|
@@ -180,18 +180,19 @@ The dev toolbar changes client-side preview state only.
|
|
|
180
180
|
|
|
181
181
|
### Top-level options
|
|
182
182
|
|
|
183
|
-
| Option | Type | Default | Notes
|
|
184
|
-
| ------------------ | ------------------------------------- | --------------- |
|
|
185
|
-
| `configRoot` | `string` | `process.cwd()` | Resolves relative `jsonConfigPath` values (root + per-environment).
|
|
186
|
-
| `jsonConfigPath` | `string` | unset | Optional **root** JSON file merged after inline config (see merge order for per-environment files).
|
|
187
|
-
| `forceEnvironment` | `string` | unset | Pin the active layer (skips `when` / `AFF_ENVIRONMENT` validation).
|
|
188
|
-
| `mode` | `string` | `NODE_ENV` | Optional advanced override for runtime resolution (mainly tests/tooling). Most apps should omit this and rely on `NODE_ENV` + `environments.when`.
|
|
189
|
-
| `env` | `Record<string, string \| undefined>` | `process.env` | `AFF_FEATURE_*` / `ASTRO_FEATURE_FLAGS` (not applied in `dev` layer).
|
|
190
|
-
| `tokenNamespace` | `string` | `'ff'` | CSS var namespace (`--ff-c-*`).
|
|
191
|
-
| `flags` | `Record<string, FlagConfig>` | `{}` | Flag declarations.
|
|
192
|
-
| `environments` | `Record<string, EnvironmentConfig>` | _(see below)_ | Declare non-`dev` layers only; reserved `dev` is injected. At least one other key; exactly one `when: true` unless forced.
|
|
193
|
-
| `css` | `DevOutlineCssOptions` | defaults | Global badge/outline layout and styling.
|
|
194
|
-
| `staticMinify` | `boolean` | `true` | For static builds: route-prune disabled prefixes + cull gated HTML in `dist/`. Set `false` to keep emitted files untouched.
|
|
183
|
+
| Option | Type | Default | Notes |
|
|
184
|
+
| ------------------ | ------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
185
|
+
| `configRoot` | `string` | `process.cwd()` | Resolves relative `jsonConfigPath` values (root + per-environment). |
|
|
186
|
+
| `jsonConfigPath` | `string` | unset | Optional **root** JSON file merged after inline config (see merge order for per-environment files). |
|
|
187
|
+
| `forceEnvironment` | `string` | unset | Pin the active layer (skips `when` / `AFF_ENVIRONMENT` validation). |
|
|
188
|
+
| `mode` | `string` | `NODE_ENV` | Optional advanced override for runtime resolution (mainly tests/tooling). Most apps should omit this and rely on `NODE_ENV` + `environments.when`. |
|
|
189
|
+
| `env` | `Record<string, string \| undefined>` | `process.env` | `AFF_FEATURE_*` / `ASTRO_FEATURE_FLAGS` (not applied in `dev` layer). |
|
|
190
|
+
| `tokenNamespace` | `string` | `'ff'` | CSS var namespace (`--ff-c-*`). |
|
|
191
|
+
| `flags` | `Record<string, FlagConfig>` | `{}` | Flag declarations. |
|
|
192
|
+
| `environments` | `Record<string, EnvironmentConfig>` | _(see below)_ | Declare non-`dev` layers only; reserved `dev` is injected. At least one other key; exactly one `when: true` unless forced. |
|
|
193
|
+
| `css` | `DevOutlineCssOptions` | defaults | Global badge/outline layout and styling. |
|
|
194
|
+
| `staticMinify` | `boolean` | `true` | For static builds: route-prune disabled prefixes + cull gated HTML in `dist/`. Set `false` to keep emitted files untouched. |
|
|
195
|
+
| `pruneSitemap` | `boolean` | `true` | Drop pruned routes from generated sitemaps in `dist/`. Needs this integration **after** `sitemap()` in `integrations`. See [Hide flagged routes from sitemaps](docs/how-to/hide-from-sitemaps.md). |
|
|
195
196
|
|
|
196
197
|
If you omit `environments`, the integration injects a minimal reserved `dev` plus **`prod`** tied to `NODE_ENV` (or `mode` only if you explicitly override it) so `astroFeatureFlags()` still runs in small demos.
|
|
197
198
|
|
package/dist/index.d.mts
CHANGED
|
@@ -41,6 +41,12 @@ declare function normalizeElementBadgeLayout(input: ElementBadgeLayoutOptions |
|
|
|
41
41
|
declare function elementBadgePositionBlock(layout: NormalizedElementBadgeLayout): string;
|
|
42
42
|
//#endregion
|
|
43
43
|
//#region src/dev-outline-css.d.ts
|
|
44
|
+
/**
|
|
45
|
+
* Cascade layer for the declarations the app must be able to override without
|
|
46
|
+
* writing `!important`. Named rather than anonymous so the order statement in
|
|
47
|
+
* {@link affHeadInlineRuntime} can name it too.
|
|
48
|
+
*/
|
|
49
|
+
declare const DEV_LAYER = "aff-dev";
|
|
44
50
|
type DevOutlineHiddenStrategy = "visibility" | "display";
|
|
45
51
|
interface DevOutlineCssOptions extends ElementBadgeLayoutOptions {
|
|
46
52
|
outlineWidth?: string;
|
|
@@ -65,6 +71,28 @@ interface DevOutlineCssOptions extends ElementBadgeLayoutOptions {
|
|
|
65
71
|
* Dev-only styles: `data-ff="token"` (or space-separated tokens) gates outlines/badges.
|
|
66
72
|
* `html[data-ff-route="<token>"]` (dev) adds a fixed top-right route badge (label only).
|
|
67
73
|
* Toolbar toggles: `data-ff-enabled-*`, `data-ff-outline-*`, `data-ff-badge-*`, `--<namespace>-c-*`.
|
|
74
|
+
*
|
|
75
|
+
* The badge is an absolutely positioned `::before`, so its host has to be a
|
|
76
|
+
* containing block, and the host also wants rounding for the outline to follow.
|
|
77
|
+
* Both of those are the app's business, not ours: a flagged element that the app
|
|
78
|
+
* positions itself, or rounds itself, must keep doing so. Marking a flagged
|
|
79
|
+
* element must not move it.
|
|
80
|
+
*
|
|
81
|
+
* So `position` and `border-radius` are emitted inside `:where()` *and* inside
|
|
82
|
+
* the `aff-dev` cascade layer. `:where()` alone is not enough: an unlayered rule
|
|
83
|
+
* beats a layered one before specificity is ever consulted, so a zero-specificity
|
|
84
|
+
* unlayered rule still overrides Tailwind's `.rounded-full` and `.absolute`,
|
|
85
|
+
* which live in `@layer utilities`. Layering ours is what lets the app win.
|
|
86
|
+
*
|
|
87
|
+
* Layer priority follows the order layers are first declared, so the layer only
|
|
88
|
+
* sorts below the app's own layers if `@layer aff-dev;` appears before them in
|
|
89
|
+
* document order. The sheet opens with that statement for the case where it
|
|
90
|
+
* lands first, and {@link affHeadInlineRuntime} also prepends a style element
|
|
91
|
+
* carrying it as the first child of `<head>`, which is what actually guarantees
|
|
92
|
+
* the order at runtime.
|
|
93
|
+
*
|
|
94
|
+
* Everything else here is dev chrome the app has no opinion about, and stays at
|
|
95
|
+
* its natural specificity.
|
|
68
96
|
*/
|
|
69
97
|
declare function createFeatureFlagStyles(runtime: ResolvedFeatureRuntime, css?: DevOutlineCssOptions): string;
|
|
70
98
|
/**
|
|
@@ -85,11 +113,56 @@ declare function cullProductionHtml(html: string, runtime: ResolvedFeatureRuntim
|
|
|
85
113
|
*/
|
|
86
114
|
declare function applyProductionHtmlCullToDist(outDir: string, runtime: ResolvedFeatureRuntime): void;
|
|
87
115
|
//#endregion
|
|
116
|
+
//#region src/sitemap-prune.d.ts
|
|
117
|
+
/**
|
|
118
|
+
* Drop every `<url>` whose `<loc>` points at a route this runtime prunes.
|
|
119
|
+
*
|
|
120
|
+
* The alternative is asking every site to duplicate the flag decision in its own
|
|
121
|
+
* `sitemap({ filter })`, which is what the docs used to say and what the site this
|
|
122
|
+
* package was written for got subtly wrong: a substring test culled `/blog/about-x/`
|
|
123
|
+
* along with `/about/`. Deciding it here, from the same runtime that deletes the
|
|
124
|
+
* files, means the two answers cannot drift.
|
|
125
|
+
*
|
|
126
|
+
* String surgery rather than an XML parse, so the untouched entries come back
|
|
127
|
+
* byte-identical and the diff of a rebuild stays readable.
|
|
128
|
+
*/
|
|
129
|
+
declare function pruneSitemapXml(xml: string, runtime: ResolvedFeatureRuntime): string;
|
|
130
|
+
/** Number of `<url>` entries left in a urlset. */
|
|
131
|
+
declare function sitemapUrlCount(xml: string): number;
|
|
132
|
+
/**
|
|
133
|
+
* Remove the `<sitemap>` entries of an index that point at files which no longer exist.
|
|
134
|
+
*/
|
|
135
|
+
declare function pruneSitemapIndexXml(xml: string, removedFiles: readonly string[]): string;
|
|
136
|
+
interface SitemapPruneResult {
|
|
137
|
+
/** Sitemap files rewritten with fewer entries. */
|
|
138
|
+
rewritten: string[];
|
|
139
|
+
/** Sitemap files deleted because nothing was left in them. */
|
|
140
|
+
removed: string[];
|
|
141
|
+
/** Whether any sitemap file was found at all. */
|
|
142
|
+
found: boolean;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Walk `outDir` (Astro `dist/`) and take the pruned routes out of every sitemap.
|
|
146
|
+
*
|
|
147
|
+
* Runs in `astro:build:done`, which means this integration has to sit **after**
|
|
148
|
+
* `@astrojs/sitemap` in `integrations` — Astro runs the hook in array order, and a
|
|
149
|
+
* sitemap written after this pass would keep its dead URLs.
|
|
150
|
+
*/
|
|
151
|
+
declare function applySitemapPruneToDist(outDir: string, runtime: ResolvedFeatureRuntime): SitemapPruneResult;
|
|
152
|
+
//#endregion
|
|
88
153
|
//#region src/index.d.ts
|
|
89
154
|
interface AstroFeatureFlagsOptions extends ResolveFeatureRuntimeOptions {
|
|
90
155
|
css?: DevOutlineCssOptions;
|
|
91
156
|
/** When false, keeps static build output untouched (no route pruning / HTML cull). */
|
|
92
157
|
staticMinify?: boolean;
|
|
158
|
+
/**
|
|
159
|
+
* When false, leaves generated sitemaps alone. On by default: a pruned route that is
|
|
160
|
+
* still advertised in `sitemap-0.xml` is a 404 handed to every crawler that reads it.
|
|
161
|
+
*
|
|
162
|
+
* Needs this integration to sit **after** `@astrojs/sitemap` in `integrations`, because
|
|
163
|
+
* Astro runs `astro:build:done` in array order.
|
|
164
|
+
*/
|
|
165
|
+
pruneSitemap?: boolean;
|
|
93
166
|
}
|
|
94
167
|
/** Prefer `prod` if present; else first non-`dev` key (sorted); else `"prod"`. */
|
|
95
168
|
declare function primaryNonDevEnvironmentKey(flagsByEnvironment: Record<string, FeatureFlagMap>): string;
|
|
@@ -98,5 +171,5 @@ declare function getResolvedFeatures(options?: ResolveFeatureRuntimeOptions): Re
|
|
|
98
171
|
declare function featureRouteIncluded(pathname: string, runtime: ResolvedFeatureRuntime): boolean;
|
|
99
172
|
declare function astroFeatureFlags(options?: AstroFeatureFlagsOptions): any;
|
|
100
173
|
//#endregion
|
|
101
|
-
export { AstroFeatureFlagsOptions, type DevOutlineCssOptions, type DevOutlineHiddenStrategy, type ElementBadgeHorizontalAlign, type ElementBadgeLayoutOptions, type ElementBadgeVerticalAnchor, type FeatureFlagMap, type NormalizedElementBadgeLayout, applyProductionHtmlCullToDist, createFeatureFlagStyles, createProductionGateStyles, createVirtualModuleSource, cullProductionHtml, astroFeatureFlags as default, elementBadgePositionBlock, featureRouteIncluded, getResolvedFeatures, longestMatchingRoutePrefix, mergeFlagsWithProcessEnvOverrides, normalizeElementBadgeLayout, primaryNonDevEnvironmentKey, resolveFeatureFlagsByEnvironment, routePatternToPrefix };
|
|
174
|
+
export { AstroFeatureFlagsOptions, DEV_LAYER, type DevOutlineCssOptions, type DevOutlineHiddenStrategy, type ElementBadgeHorizontalAlign, type ElementBadgeLayoutOptions, type ElementBadgeVerticalAnchor, type FeatureFlagMap, type NormalizedElementBadgeLayout, type SitemapPruneResult, applyProductionHtmlCullToDist, applySitemapPruneToDist, createFeatureFlagStyles, createProductionGateStyles, createVirtualModuleSource, cullProductionHtml, astroFeatureFlags as default, elementBadgePositionBlock, featureRouteIncluded, getResolvedFeatures, longestMatchingRoutePrefix, mergeFlagsWithProcessEnvOverrides, normalizeElementBadgeLayout, primaryNonDevEnvironmentKey, pruneSitemapIndexXml, pruneSitemapXml, resolveFeatureFlagsByEnvironment, routePatternToPrefix, sitemapUrlCount };
|
|
102
175
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/badge-layout.ts","../src/dev-outline-css.ts","../src/production-html-cull.ts","../src/index.ts"],"mappings":";;KAAY;;KAGA;;UAGK;;;;;EAKf,8BAA8B;;;;;EAK9B;;;;;EAKA;;EAEA,6BAA6B;;KAGnB;EAEN;EACA,iBAAiB;EACjB;EACA,gBAAgB;;EAGhB;EACA;EACA;EACA,gBAAgB;;iBAQN,4BACd,OAAO,wCACN;;;;iBA+Ba,0BACd,QAAQ
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/badge-layout.ts","../src/dev-outline-css.ts","../src/production-html-cull.ts","../src/sitemap-prune.ts","../src/index.ts"],"mappings":";;KAAY;;KAGA;;UAGK;;;;;EAKf,8BAA8B;;;;;EAK9B;;;;;EAKA;;EAEA,6BAA6B;;KAGnB;EAEN;EACA,iBAAiB;EACjB;EACA,gBAAgB;;EAGhB;EACA;EACA;EACA,gBAAgB;;iBAQN,4BACd,OAAO,wCACN;;;;iBA+Ba,0BACd,QAAQ;;;;;;;;cChEG;KAED;UAEK,6BAA6B;EAC5C;;EAEA;;EAEA,sBAAsB;EACtB;EACA;;EAGA;;EAEA;EACA,oBAAoB;EAEpB,iBAAiB;;;;EAKjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4Fc,wBACd,SAAS,wBACT,MAAM;;;;;iBAiUQ,2BACd,SAAS;;;;;;;;iBCnXK,mBACd,cACA,SAAS;;;;iBAqCK,8BACd,gBACA,SAAS;;;;;;;;;;;;;;;iBC3DK,gBACd,aACA,SAAS;;iBAgBK,gBAAgB;;;;iBAOhB,qBACd,aACA;UAWe;;EAEf;;EAEA;;EAEA;;;;;;;;;iBAUc,wBACd,gBACA,SAAS,yBACR;;;UC3Dc,iCAAiC;EAChD,MAAM;;EAEN;;;;;;;;EAQA;;;iBAIc,4BACd,oBAAoB,eAAe;iBAoCrB,0BACd,SAAS,wBACT,MAAM,sBACN,qBAAoB,eAAe,iBACnC;iBA8Kc,oBACd,UAAS,+BACR;iBAIa,qBACd,kBACA,SAAS;iBAkBa,kBACtB,UAAS"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as DEV_TOOLBAR_FLAG_ICON_SVG } from "./dev-toolbar-flag-icon-MQsIFekv.mjs";
|
|
2
2
|
import { longestMatchingRoutePrefix, mergeFlagsWithProcessEnvOverrides, resolveFeatureFlagsByEnvironment, resolveFeatureRuntime, routePathsToPrune, routePatternToPrefix, shouldIncludeRoute, toEnumKey, toToken } from "./runtime.mjs";
|
|
3
3
|
import { readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { parse } from "node-html-parser";
|
|
7
7
|
//#region src/badge-layout.ts
|
|
@@ -101,8 +101,18 @@ function affHeadInlineRuntime(payload) {
|
|
|
101
101
|
return p.endsWith("/") ? p : `${p}/`;
|
|
102
102
|
};
|
|
103
103
|
try {
|
|
104
|
-
const { featureFlagStyles, routeFlags: RF, flagNameToToken: M } = payload;
|
|
104
|
+
const { featureFlagStyles, routeFlags: RF, flagNameToToken: M, devLayer: L } = payload;
|
|
105
|
+
const ensureLayerOrder = () => {
|
|
106
|
+
if (document.querySelector("style[data-astro-feature-flags-layer]")) return;
|
|
107
|
+
const head = document.head || document.documentElement;
|
|
108
|
+
const s = document.createElement("style");
|
|
109
|
+
s.setAttribute("data-astro-feature-flags-layer", "");
|
|
110
|
+
s.setAttribute("data-astro-transition-persist", "astro-feature-flags-layer");
|
|
111
|
+
s.textContent = `@layer ${L};`;
|
|
112
|
+
head.insertBefore(s, head.firstChild);
|
|
113
|
+
};
|
|
105
114
|
const ensureFeatureFlagStyles = () => {
|
|
115
|
+
ensureLayerOrder();
|
|
106
116
|
if (document.querySelector("style[data-astro-feature-flags]")) return;
|
|
107
117
|
const s = document.createElement("style");
|
|
108
118
|
s.setAttribute("data-astro-feature-flags", "");
|
|
@@ -403,6 +413,12 @@ function affDevBootstrapRuntime(payload) {
|
|
|
403
413
|
}
|
|
404
414
|
//#endregion
|
|
405
415
|
//#region src/dev-outline-css.ts
|
|
416
|
+
/**
|
|
417
|
+
* Cascade layer for the declarations the app must be able to override without
|
|
418
|
+
* writing `!important`. Named rather than anonymous so the order statement in
|
|
419
|
+
* {@link affHeadInlineRuntime} can name it too.
|
|
420
|
+
*/
|
|
421
|
+
const DEV_LAYER = "aff-dev";
|
|
406
422
|
const defaultDevOutlineCssOptions = {
|
|
407
423
|
outlineWidth: "2px",
|
|
408
424
|
outlineColor: "rgb(220 38 38)",
|
|
@@ -441,6 +457,28 @@ function featureColorVar(token, namespace) {
|
|
|
441
457
|
* Dev-only styles: `data-ff="token"` (or space-separated tokens) gates outlines/badges.
|
|
442
458
|
* `html[data-ff-route="<token>"]` (dev) adds a fixed top-right route badge (label only).
|
|
443
459
|
* Toolbar toggles: `data-ff-enabled-*`, `data-ff-outline-*`, `data-ff-badge-*`, `--<namespace>-c-*`.
|
|
460
|
+
*
|
|
461
|
+
* The badge is an absolutely positioned `::before`, so its host has to be a
|
|
462
|
+
* containing block, and the host also wants rounding for the outline to follow.
|
|
463
|
+
* Both of those are the app's business, not ours: a flagged element that the app
|
|
464
|
+
* positions itself, or rounds itself, must keep doing so. Marking a flagged
|
|
465
|
+
* element must not move it.
|
|
466
|
+
*
|
|
467
|
+
* So `position` and `border-radius` are emitted inside `:where()` *and* inside
|
|
468
|
+
* the `aff-dev` cascade layer. `:where()` alone is not enough: an unlayered rule
|
|
469
|
+
* beats a layered one before specificity is ever consulted, so a zero-specificity
|
|
470
|
+
* unlayered rule still overrides Tailwind's `.rounded-full` and `.absolute`,
|
|
471
|
+
* which live in `@layer utilities`. Layering ours is what lets the app win.
|
|
472
|
+
*
|
|
473
|
+
* Layer priority follows the order layers are first declared, so the layer only
|
|
474
|
+
* sorts below the app's own layers if `@layer aff-dev;` appears before them in
|
|
475
|
+
* document order. The sheet opens with that statement for the case where it
|
|
476
|
+
* lands first, and {@link affHeadInlineRuntime} also prepends a style element
|
|
477
|
+
* carrying it as the first child of `<head>`, which is what actually guarantees
|
|
478
|
+
* the order at runtime.
|
|
479
|
+
*
|
|
480
|
+
* Everything else here is dev chrome the app has no opinion about, and stays at
|
|
481
|
+
* its natural specificity.
|
|
444
482
|
*/
|
|
445
483
|
function createFeatureFlagStyles(runtime, css) {
|
|
446
484
|
const nsAttr = `data-${toToken(runtime.namespace) || "ff"}`;
|
|
@@ -474,11 +512,15 @@ html {
|
|
|
474
512
|
html:not([data-ff-outline-${token}="off"]) {
|
|
475
513
|
--aff-outline-c-${token}: var(${v}, ${col});
|
|
476
514
|
}
|
|
515
|
+
@layer ${DEV_LAYER} {
|
|
516
|
+
:where(${selOutline}) {
|
|
517
|
+
position: relative;
|
|
518
|
+
border-radius: ${opts.borderRadius};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
477
521
|
${selOutline} {
|
|
478
|
-
position: relative;
|
|
479
522
|
outline: ${opts.outlineWidth} solid var(--aff-outline-c-${token});
|
|
480
523
|
outline-offset: ${opts.outlineOffset};
|
|
481
|
-
border-radius: ${opts.borderRadius};
|
|
482
524
|
}
|
|
483
525
|
html:not([data-ff-badge-${token}="off"]) ${selIsSingleBadge}::before {
|
|
484
526
|
box-sizing: border-box;
|
|
@@ -560,11 +602,15 @@ html[data-ff-enabled-${token}="off"] ${selIs} {
|
|
|
560
602
|
`);
|
|
561
603
|
}
|
|
562
604
|
chunks.push(`
|
|
605
|
+
@layer ${DEV_LAYER} {
|
|
606
|
+
:where([${nsAttr}*=" "]) {
|
|
607
|
+
position: relative;
|
|
608
|
+
border-radius: ${opts.borderRadius};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
563
611
|
[${nsAttr}*=" "] {
|
|
564
|
-
position: relative;
|
|
565
612
|
outline: none !important;
|
|
566
613
|
outline-offset: 0 !important;
|
|
567
|
-
border-radius: ${opts.borderRadius};
|
|
568
614
|
}
|
|
569
615
|
html [${nsAttr}*=" "]::after {
|
|
570
616
|
content: "";
|
|
@@ -732,7 +778,7 @@ html[data-ff-outline-${token}="off"] ${selIs}[${nsAttr}*=" "]::after {
|
|
|
732
778
|
}
|
|
733
779
|
`);
|
|
734
780
|
}
|
|
735
|
-
return
|
|
781
|
+
return `\n@layer ${DEV_LAYER};\n` + chunks.join("\n") + "\n";
|
|
736
782
|
}
|
|
737
783
|
/**
|
|
738
784
|
* CSS-only hiding for disabled flags. The Astro integration no longer injects this into
|
|
@@ -842,6 +888,134 @@ function applyProductionHtmlCullToDist(outDir, runtime) {
|
|
|
842
888
|
}
|
|
843
889
|
}
|
|
844
890
|
//#endregion
|
|
891
|
+
//#region src/sitemap-prune.ts
|
|
892
|
+
/**
|
|
893
|
+
* `@astrojs/sitemap` names its output `sitemap-0.xml`, `sitemap-1.xml`, … alongside a
|
|
894
|
+
* `sitemap-index.xml`. Other generators write a plain `sitemap.xml`. Match the family.
|
|
895
|
+
*/
|
|
896
|
+
const SITEMAP_FILE = /^sitemap[\w.-]*\.xml$/i;
|
|
897
|
+
function walkSitemapFiles(dir) {
|
|
898
|
+
const out = [];
|
|
899
|
+
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
|
900
|
+
const p = join(dir, ent.name);
|
|
901
|
+
if (ent.isDirectory()) out.push(...walkSitemapFiles(p));
|
|
902
|
+
else if (ent.isFile() && SITEMAP_FILE.test(ent.name)) out.push(p);
|
|
903
|
+
}
|
|
904
|
+
return out;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* `<loc>` is XML, so the five predefined entities and numeric references are all
|
|
908
|
+
* legal in it. `&` is the one that actually turns up (a query string with two
|
|
909
|
+
* parameters), but a path that came through an escaper wholesale can carry the
|
|
910
|
+
* others, and an entity left undecoded turns into a pathname that matches no
|
|
911
|
+
* route and is silently kept.
|
|
912
|
+
*/
|
|
913
|
+
function decodeXmlEntities(value) {
|
|
914
|
+
return value.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number.parseInt(dec, 10))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/g, "&");
|
|
915
|
+
}
|
|
916
|
+
function locPathname(entry) {
|
|
917
|
+
const loc = /<loc>\s*([\s\S]*?)\s*<\/loc>/i.exec(entry)?.[1];
|
|
918
|
+
if (!loc) return null;
|
|
919
|
+
const href = decodeXmlEntities(loc).trim();
|
|
920
|
+
try {
|
|
921
|
+
return new URL(href).pathname;
|
|
922
|
+
} catch {
|
|
923
|
+
return href.startsWith("/") ? href : null;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Drop every `<url>` whose `<loc>` points at a route this runtime prunes.
|
|
928
|
+
*
|
|
929
|
+
* The alternative is asking every site to duplicate the flag decision in its own
|
|
930
|
+
* `sitemap({ filter })`, which is what the docs used to say and what the site this
|
|
931
|
+
* package was written for got subtly wrong: a substring test culled `/blog/about-x/`
|
|
932
|
+
* along with `/about/`. Deciding it here, from the same runtime that deletes the
|
|
933
|
+
* files, means the two answers cannot drift.
|
|
934
|
+
*
|
|
935
|
+
* String surgery rather than an XML parse, so the untouched entries come back
|
|
936
|
+
* byte-identical and the diff of a rebuild stays readable.
|
|
937
|
+
*/
|
|
938
|
+
function pruneSitemapXml(xml, runtime) {
|
|
939
|
+
return xml.replace(/[ \t]*<url>[\s\S]*?<\/url>\s*/gi, (entry) => {
|
|
940
|
+
const pathname = locPathname(entry);
|
|
941
|
+
if (!pathname) return entry;
|
|
942
|
+
return shouldIncludeRoute({
|
|
943
|
+
pathname,
|
|
944
|
+
routeFlags: runtime.routeFlags,
|
|
945
|
+
flags: runtime.flags,
|
|
946
|
+
isDev: false
|
|
947
|
+
}) ? entry : "";
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
/** Number of `<url>` entries left in a urlset. */
|
|
951
|
+
function sitemapUrlCount(xml) {
|
|
952
|
+
return (xml.match(/<url>/gi) ?? []).length;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Remove the `<sitemap>` entries of an index that point at files which no longer exist.
|
|
956
|
+
*/
|
|
957
|
+
function pruneSitemapIndexXml(xml, removedFiles) {
|
|
958
|
+
if (!removedFiles.length) return xml;
|
|
959
|
+
const removed = new Set(removedFiles);
|
|
960
|
+
return xml.replace(/[ \t]*<sitemap>[\s\S]*?<\/sitemap>\s*/gi, (entry) => {
|
|
961
|
+
const pathname = locPathname(entry);
|
|
962
|
+
if (!pathname) return entry;
|
|
963
|
+
return removed.has(basename(pathname)) ? "" : entry;
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Walk `outDir` (Astro `dist/`) and take the pruned routes out of every sitemap.
|
|
968
|
+
*
|
|
969
|
+
* Runs in `astro:build:done`, which means this integration has to sit **after**
|
|
970
|
+
* `@astrojs/sitemap` in `integrations` — Astro runs the hook in array order, and a
|
|
971
|
+
* sitemap written after this pass would keep its dead URLs.
|
|
972
|
+
*/
|
|
973
|
+
function applySitemapPruneToDist(outDir, runtime) {
|
|
974
|
+
const result = {
|
|
975
|
+
rewritten: [],
|
|
976
|
+
removed: [],
|
|
977
|
+
found: false
|
|
978
|
+
};
|
|
979
|
+
let files;
|
|
980
|
+
try {
|
|
981
|
+
files = walkSitemapFiles(outDir);
|
|
982
|
+
} catch {
|
|
983
|
+
return result;
|
|
984
|
+
}
|
|
985
|
+
result.found = files.length > 0;
|
|
986
|
+
const indexes = [];
|
|
987
|
+
for (const file of files) {
|
|
988
|
+
const before = readFileSync(file, "utf8");
|
|
989
|
+
if (!/<urlset[\s>]/i.test(before)) {
|
|
990
|
+
if (/<sitemapindex[\s>]/i.test(before)) indexes.push(file);
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const after = pruneSitemapXml(before, runtime);
|
|
994
|
+
if (after === before) continue;
|
|
995
|
+
if (sitemapUrlCount(after) === 0) {
|
|
996
|
+
rmSync(file, { force: true });
|
|
997
|
+
result.removed.push(file);
|
|
998
|
+
} else {
|
|
999
|
+
writeFileSync(file, after, "utf8");
|
|
1000
|
+
result.rewritten.push(file);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const removedNames = result.removed.map((file) => basename(file));
|
|
1004
|
+
for (const file of indexes) {
|
|
1005
|
+
const before = readFileSync(file, "utf8");
|
|
1006
|
+
const after = pruneSitemapIndexXml(before, removedNames);
|
|
1007
|
+
if (after === before) continue;
|
|
1008
|
+
if (!/<sitemap>/i.test(after)) {
|
|
1009
|
+
rmSync(file, { force: true });
|
|
1010
|
+
result.removed.push(file);
|
|
1011
|
+
} else {
|
|
1012
|
+
writeFileSync(file, after, "utf8");
|
|
1013
|
+
result.rewritten.push(file);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return result;
|
|
1017
|
+
}
|
|
1018
|
+
//#endregion
|
|
845
1019
|
//#region src/dev-head-inject.ts
|
|
846
1020
|
/**
|
|
847
1021
|
* Single `injectScript('head-inline', …)` payload for `astro dev`: dev-only outline CSS,
|
|
@@ -854,7 +1028,8 @@ function buildAffDevHeadInline(args) {
|
|
|
854
1028
|
return `${inlineInvoke(affHeadInlineRuntime, {
|
|
855
1029
|
featureFlagStyles,
|
|
856
1030
|
routeFlags: runtime.routeFlags,
|
|
857
|
-
flagNameToToken
|
|
1031
|
+
flagNameToToken,
|
|
1032
|
+
devLayer: DEV_LAYER
|
|
858
1033
|
})}\n${affDevBootstrap}`;
|
|
859
1034
|
}
|
|
860
1035
|
//#endregion
|
|
@@ -1052,17 +1227,23 @@ function featureRouteIncluded(pathname, runtime) {
|
|
|
1052
1227
|
});
|
|
1053
1228
|
}
|
|
1054
1229
|
function astroFeatureFlags(options = {}) {
|
|
1055
|
-
const { css, staticMinify = true, ...flagOpts } = options;
|
|
1230
|
+
const { css, staticMinify = true, pruneSitemap = true, ...flagOpts } = options;
|
|
1056
1231
|
const opts = withDefaultEnvironments(flagOpts);
|
|
1057
1232
|
const mode = opts.mode ?? process.env.NODE_ENV ?? "development";
|
|
1058
1233
|
const runtime = resolveFeatureRuntime({
|
|
1059
1234
|
...opts,
|
|
1060
1235
|
mode
|
|
1061
1236
|
});
|
|
1237
|
+
/**
|
|
1238
|
+
* Set in `astro:config:setup`, read in `astro:build:done`, so that a sitemap this
|
|
1239
|
+
* pass never saw can be reported as an ordering mistake rather than silently skipped.
|
|
1240
|
+
*/
|
|
1241
|
+
let sitemapIntegrationPresent = false;
|
|
1062
1242
|
return {
|
|
1063
1243
|
name: "astro-feature-flags",
|
|
1064
1244
|
hooks: {
|
|
1065
|
-
"astro:config:setup": ({ updateConfig, addDevToolbarApp, command, injectScript }) => {
|
|
1245
|
+
"astro:config:setup": ({ config, updateConfig, addDevToolbarApp, command, injectScript }) => {
|
|
1246
|
+
sitemapIntegrationPresent = (config?.integrations ?? []).some((integration) => integration?.name === "@astrojs/sitemap");
|
|
1066
1247
|
const flagNames = Object.keys(runtime.flags);
|
|
1067
1248
|
const flagTokens = flagNames.map((name) => toToken(name));
|
|
1068
1249
|
const flagsByEnvironment = resolveFeatureFlagsByEnvironment(opts);
|
|
@@ -1094,7 +1275,7 @@ function astroFeatureFlags(options = {}) {
|
|
|
1094
1275
|
}
|
|
1095
1276
|
}] } });
|
|
1096
1277
|
},
|
|
1097
|
-
"astro:build:done": ({ dir }) => {
|
|
1278
|
+
"astro:build:done": ({ dir, logger }) => {
|
|
1098
1279
|
if (runtime.isDev || !staticMinify) return;
|
|
1099
1280
|
const outDir = fileURLToPath(dir);
|
|
1100
1281
|
const prunePaths = routePathsToPrune({
|
|
@@ -1106,11 +1287,13 @@ function astroFeatureFlags(options = {}) {
|
|
|
1106
1287
|
force: true
|
|
1107
1288
|
});
|
|
1108
1289
|
applyProductionHtmlCullToDist(outDir, runtime);
|
|
1290
|
+
if (!pruneSitemap) return;
|
|
1291
|
+
if (!applySitemapPruneToDist(outDir, runtime).found && sitemapIntegrationPresent && prunePaths.length) logger?.warn("@astrojs/sitemap is configured but no sitemap was on disk yet, so pruned routes may still be listed. Move astroFeatureFlags() after sitemap() in `integrations`.");
|
|
1109
1292
|
}
|
|
1110
1293
|
}
|
|
1111
1294
|
};
|
|
1112
1295
|
}
|
|
1113
1296
|
//#endregion
|
|
1114
|
-
export { applyProductionHtmlCullToDist, createFeatureFlagStyles, createProductionGateStyles, createVirtualModuleSource, cullProductionHtml, astroFeatureFlags as default, elementBadgePositionBlock, featureRouteIncluded, getResolvedFeatures, longestMatchingRoutePrefix, mergeFlagsWithProcessEnvOverrides, normalizeElementBadgeLayout, primaryNonDevEnvironmentKey, resolveFeatureFlagsByEnvironment, routePatternToPrefix };
|
|
1297
|
+
export { DEV_LAYER, applyProductionHtmlCullToDist, applySitemapPruneToDist, createFeatureFlagStyles, createProductionGateStyles, createVirtualModuleSource, cullProductionHtml, astroFeatureFlags as default, elementBadgePositionBlock, featureRouteIncluded, getResolvedFeatures, longestMatchingRoutePrefix, mergeFlagsWithProcessEnvOverrides, normalizeElementBadgeLayout, primaryNonDevEnvironmentKey, pruneSitemapIndexXml, pruneSitemapXml, resolveFeatureFlagsByEnvironment, routePatternToPrefix, sitemapUrlCount };
|
|
1115
1298
|
|
|
1116
1299
|
//# sourceMappingURL=index.mjs.map
|