@wildo-ai/saas-website 1.1.4 → 1.1.5
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/dist/esm/astro/blog-post-bridge.d.ts +14 -20
- package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -1
- package/dist/esm/astro/blog-post-bridge.js +4 -2
- package/dist/esm/astro/blog-post-bridge.js.map +1 -1
- package/dist/esm/astro/blog-post-head.renderer.d.ts.map +1 -1
- package/dist/esm/astro/bridge-runtime.d.ts +30 -20
- package/dist/esm/astro/bridge-runtime.d.ts.map +1 -1
- package/dist/esm/astro/bridge-runtime.js +13 -3
- package/dist/esm/astro/bridge-runtime.js.map +1 -1
- package/dist/esm/astro/collect-expected-label-keys.d.ts.map +1 -1
- package/dist/esm/astro/headers-renderer.d.ts.map +1 -1
- package/dist/esm/astro/i18n-routing.helper.d.ts.map +1 -1
- package/dist/esm/astro/label-pack-loader.d.ts.map +1 -1
- package/dist/esm/astro/label-pack-loader.js +5 -0
- package/dist/esm/astro/label-pack-loader.js.map +1 -1
- package/dist/esm/astro/llms-txt-server.d.ts.map +1 -1
- package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -1
- package/dist/esm/astro/robots-renderer.d.ts.map +1 -1
- package/dist/esm/astro/robots-renderer.js.map +1 -1
- package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -1
- package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -1
- package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -1
- package/dist/esm/astro/website-page-head.renderer.js.map +1 -1
- package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -1
- package/dist/esm/astro/website-site-context.d.ts.map +1 -1
- package/dist/esm/astro-island.d.ts.map +1 -1
- package/dist/esm/astro.d.ts.map +1 -1
- package/dist/esm/companion-exports.d.ts.map +1 -1
- package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -1
- package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -1
- package/dist/esm/config/define-website-config.d.ts.map +1 -1
- package/dist/esm/config/index.d.ts.map +1 -1
- package/dist/esm/config/load-website-config.d.ts.map +1 -1
- package/dist/esm/config/load-website-config.js +2 -1
- package/dist/esm/config/load-website-config.js.map +1 -1
- package/dist/esm/config/wildo-website-config.schemas.d.ts +10 -5
- package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -1
- package/dist/esm/config/wildo-website-config.schemas.js +10 -5
- package/dist/esm/config/wildo-website-config.schemas.js.map +1 -1
- package/dist/esm/config-loader.d.ts.map +1 -1
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -1
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +3 -3
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -1
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +13 -9
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -1
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +11 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -1
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +14 -1
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -1
- package/dist/esm/core/consent/WebsiteConsentBanner.d.ts +6 -0
- package/dist/esm/core/consent/WebsiteConsentBanner.d.ts.map +1 -0
- package/dist/esm/core/consent/WebsiteConsentBanner.js +128 -0
- package/dist/esm/core/consent/WebsiteConsentBanner.js.map +1 -0
- package/dist/esm/core/consent/WebsiteConsentContext.d.ts +85 -0
- package/dist/esm/core/consent/WebsiteConsentContext.d.ts.map +1 -0
- package/dist/esm/core/consent/WebsiteConsentContext.js +89 -0
- package/dist/esm/core/consent/WebsiteConsentContext.js.map +1 -0
- package/dist/esm/core/consent/WebsiteConsentGate.d.ts +27 -0
- package/dist/esm/core/consent/WebsiteConsentGate.d.ts.map +1 -0
- package/dist/esm/core/consent/WebsiteConsentGate.js +9 -0
- package/dist/esm/core/consent/WebsiteConsentGate.js.map +1 -0
- package/dist/esm/core/consent/useWebsiteConsent.d.ts +10 -0
- package/dist/esm/core/consent/useWebsiteConsent.d.ts.map +1 -0
- package/dist/esm/core/consent/useWebsiteConsent.js +17 -0
- package/dist/esm/core/consent/useWebsiteConsent.js.map +1 -0
- package/dist/esm/core/consent/website-consent-storage.d.ts +22 -0
- package/dist/esm/core/consent/website-consent-storage.d.ts.map +1 -0
- package/dist/esm/core/consent/website-consent-storage.js +102 -0
- package/dist/esm/core/consent/website-consent-storage.js.map +1 -0
- package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -1
- package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -1
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +8 -1
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -1
- package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -1
- package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -1
- package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -1
- package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -1
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -1
- package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -1
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +1 -2
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -1
- package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -1
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts +33 -10
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts.map +1 -1
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.js +64 -16
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.js.map +1 -1
- package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -1
- package/dist/esm/core/factories/define-website-section.d.ts.map +1 -1
- package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -1
- package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -1
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts +21 -1
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -1
- package/dist/esm/core/layouts/WebsitePageLayout.js +32 -4
- package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -1
- package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -1
- package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -1
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/mdx/BlogPost.d.ts.map +1 -1
- package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -1
- package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -1
- package/dist/esm/mdx/index.d.ts.map +1 -1
- package/dist/esm/mdx.d.ts.map +1 -1
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts +61 -0
- package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js +73 -0
- package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js.map +1 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -1
- package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -1
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -29
- package/src/astro/__tests__/blog-post-bridge.test.tsx +41 -0
- package/src/astro/__tests__/bridge-runtime.test.tsx +25 -0
- package/src/astro/__tests__/label-pack-loader.test.ts +26 -1
- package/src/astro/blog-post-bridge.tsx +18 -22
- package/src/astro/bridge-runtime.tsx +45 -23
- package/src/astro/label-pack-loader.ts +4 -0
- package/src/config/load-website-config.ts +2 -1
- package/src/config/wildo-website-config.schemas.ts +10 -5
- package/src/core/__tests__/WebsitePageLayout.test.tsx +1 -1
- package/src/core/__tests__/website-consent.test.tsx +311 -0
- package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +11 -4
- package/src/core/anonymous-session/inbound-contact-form.schema.ts +14 -9
- package/src/core/anonymous-session/website-anonymous-session-client.ts +15 -1
- package/src/core/consent/WebsiteConsentBanner.tsx +222 -0
- package/src/core/consent/WebsiteConsentContext.tsx +164 -0
- package/src/core/consent/WebsiteConsentGate.tsx +34 -0
- package/src/core/consent/useWebsiteConsent.ts +18 -0
- package/src/core/consent/website-consent-storage.ts +116 -0
- package/src/core/contexts/WebsiteRuntimeContext.tsx +8 -1
- package/src/core/external-providers/frontend-provider-registry.website.ts +1 -2
- package/src/core/external-providers/useWebsiteProviderScripts.ts +72 -17
- package/src/core/layouts/WebsitePageLayout.tsx +66 -3
- package/src/index.ts +7 -0
- package/src/schemas/label-keys/__tests__/scaffolded-consent-label-pack.parity.test.ts +71 -0
- package/src/schemas/label-keys/website-consent-label-keys.schemas.ts +78 -0
- package/dist/esm/core/external-providers/provider-scripts.website.d.ts +0 -68
- package/dist/esm/core/external-providers/provider-scripts.website.d.ts.map +0 -1
- package/dist/esm/core/external-providers/provider-scripts.website.js +0 -99
- package/dist/esm/core/external-providers/provider-scripts.website.js.map +0 -1
- package/src/core/external-providers/__tests__/provider-scripts.website.test.ts +0 -146
- package/src/core/external-providers/provider-scripts.website.ts +0 -126
|
@@ -213,6 +213,12 @@ import type { LabelPack } from '../schemas/validators/label-pack.validator';
|
|
|
213
213
|
* island. NEVER cross the hydration boundary as serialized props.
|
|
214
214
|
*/
|
|
215
215
|
export interface CreateWebsiteBlogPostBridgeIslandDeps {
|
|
216
|
+
/**
|
|
217
|
+
* Browser CODE imported by the consumer bridge, closed over at module load.
|
|
218
|
+
* Never pass executable modules through Astro island props: those are serialized.
|
|
219
|
+
* Config sync emits a stable map, including an empty map when no providers are declared.
|
|
220
|
+
*/
|
|
221
|
+
frontendProviderModules?: GeneratedWebsiteProviderModules;
|
|
216
222
|
/**
|
|
217
223
|
* Per-slug map of compiled MDX bodies (each entry is a React
|
|
218
224
|
* component — the `.default` export of the MDX module). Typically
|
|
@@ -247,6 +253,11 @@ export interface CreateWebsiteBlogPostBridgeIslandDeps {
|
|
|
247
253
|
* serialization boundary.
|
|
248
254
|
*/
|
|
249
255
|
navigation: WebsiteNavigationConfig;
|
|
256
|
+
/**
|
|
257
|
+
* Optional replacement for the framework's visitor consent panel (#534). Closed over for the same
|
|
258
|
+
* reason as `navigation`. See `WebsiteRuntimeContextValue.consentBannerComponent`.
|
|
259
|
+
*/
|
|
260
|
+
consentBannerComponent?: ComponentType;
|
|
250
261
|
/**
|
|
251
262
|
* MDX-tag → React-component map forwarded into `<BlogPost>` and
|
|
252
263
|
* scoped via `<WebsiteMdxProvider>`. Typically a small object
|
|
@@ -304,26 +315,9 @@ export interface WebsiteBlogPostBridgeIslandProps {
|
|
|
304
315
|
skipToMainContentLabelKey: WebsiteLabelKey;
|
|
305
316
|
/** Optional website-facing public providers materialized at build time. */
|
|
306
317
|
frontendProviders?: FrontendProvidersBlock;
|
|
307
|
-
/**
|
|
308
|
-
*
|
|
309
|
-
|
|
310
|
-
* `src/generated/frontend-providers.generated.ts`, written by
|
|
311
|
-
* `wildo config sync`.
|
|
312
|
-
*
|
|
313
|
-
* `frontendProviders` above says WHICH providers this service turned on, in
|
|
314
|
-
* this environment; this says what code the bundle actually contains for
|
|
315
|
-
* them. Both are needed, because a JSON payload cannot carry an SDK loader
|
|
316
|
-
* and a bundle cannot know which environment it is running in.
|
|
317
|
-
*
|
|
318
|
-
* NOT a JSON-serializable island prop like its siblings, and it does not need
|
|
319
|
-
* to be: the bridge module is bundled with the page, so the generated module
|
|
320
|
-
* map is an ordinary import in the same graph rather than something that has
|
|
321
|
-
* to cross the hydration boundary.
|
|
322
|
-
*
|
|
323
|
-
* Omitted is the ordinary case for a website declaring no package-shipped
|
|
324
|
-
* provider: no such file is generated, so there is nothing to import.
|
|
325
|
-
*/
|
|
326
|
-
frontendProviderModules?: GeneratedWebsiteProviderModules;
|
|
318
|
+
/** Product origin for navigation and call-to-action components inside the blog island.
|
|
319
|
+
* Supplied by the Astro page from websiteConfig, just as on ordinary page bridges. */
|
|
320
|
+
appOrigin?: string;
|
|
327
321
|
}
|
|
328
322
|
/**
|
|
329
323
|
* Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blog-post-bridge.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"blog-post-bridge.d.ts","sourceRoot":"","sources":["../../../../src/astro/blog-post-bridge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAa,MAAM,OAAO,CAAC;AAEzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAO9E,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,+DAA+D,CAAC;AACrH,OAAO,EAAY,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAC3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+DAA+D,CAAC;AACzG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iDAAiD,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yDAAyD,CAAC;AACvG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuMG;AAEH;;;GAGG;AACH,MAAM,WAAW,qCAAqC;IACpD;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,+BAA+B,CAAC;IAC1D;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC5D;;;;;OAKG;IACH,UAAU,EAAE,uBAAuB,CAAC;IACpC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,aAAa,CAAC;IACvC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,uBAAuB,CAAC;CACzC;AAED;;;;GAIG;AACH,MAAM,WAAW,gCAAgC;IAC/C;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;OAIG;IACH,aAAa,EAAE,iBAAiB,CAAC;IACjC,2DAA2D;IAC3D,SAAS,EAAE,SAAS,CAAC;IACrB,iEAAiE;IACjE,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;;OAKG;IACH,yBAAyB,EAAE,eAAe,CAAC;IAC3C,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,sBAAsB,CAAC;IAC3C;0FACsF;IACtF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID;;;;;;GAMG;AACH,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,qCAAqC,GAC1C,iBAAiB,CAAC,gCAAgC,CAAC,CAuErD;AAED;;;;;;GAMG;AACH,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAC9B,sDAAsD"}
|
|
@@ -12,7 +12,7 @@ const EMPTY_MDX_COMPONENTS = Object.freeze({});
|
|
|
12
12
|
*/
|
|
13
13
|
export function createWebsiteBlogPostBridgeIsland(deps) {
|
|
14
14
|
const { slugToMdxBody, navigation, mdxComponents = EMPTY_MDX_COMPONENTS, } = deps;
|
|
15
|
-
function WebsiteBlogPostBridgeIsland({ slug, rawFrontmatter, source, locale, defaultLocale, labelPack, designTokens, skipToMainContentLabelKey, frontendProviders,
|
|
15
|
+
function WebsiteBlogPostBridgeIsland({ slug, rawFrontmatter, source, locale, defaultLocale, labelPack, designTokens, skipToMainContentLabelKey, frontendProviders, appOrigin, }) {
|
|
16
16
|
const MdxBody = slugToMdxBody[slug];
|
|
17
17
|
if (MdxBody === undefined) {
|
|
18
18
|
/**
|
|
@@ -39,9 +39,11 @@ export function createWebsiteBlogPostBridgeIsland(deps) {
|
|
|
39
39
|
designTokens,
|
|
40
40
|
navigation,
|
|
41
41
|
skipToMainContentLabelKey,
|
|
42
|
+
appOrigin,
|
|
43
|
+
...(deps.consentBannerComponent !== undefined ? { consentBannerComponent: deps.consentBannerComponent } : {}),
|
|
42
44
|
frontendProviderRegistry: createFrontendWebsiteProviderRegistry({
|
|
43
45
|
entries: frontendProviders,
|
|
44
|
-
modules: frontendProviderModules,
|
|
46
|
+
modules: deps.frontendProviderModules,
|
|
45
47
|
}),
|
|
46
48
|
};
|
|
47
49
|
return (_jsx(WebsiteRuntimeContextProvider, { value: runtimeValue, children: _jsx(BlogPost, { rawFrontmatter: rawFrontmatter, source: source, mdxComponents: mdxComponents, children: _jsx(MdxBody, {}) }) }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blog-post-bridge.js","sourceRoot":"","sources":["../../../src/astro/blog-post-bridge.tsx"],"names":[],"mappings":";AAKA,OAAO,EACL,6BAA6B,GAE9B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,qCAAqC,EAAE,MAAM,+DAA+D,CAAC;AAEtH,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAuU/D,MAAM,oBAAoB,GAA4B,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAExE;;;;;;GAMG;AACH,MAAM,UAAU,iCAAiC,CAC/C,IAA2C;IAE3C,MAAM,EACJ,aAAa,EACb,UAAU,EACV,aAAa,GAAG,oBAAoB,GACrC,GAAG,IAAI,CAAC;IAET,SAAS,2BAA2B,CAAC,EACnC,IAAI,EACJ,cAAc,EACd,MAAM,EACN,MAAM,EACN,aAAa,EACb,SAAS,EACT,YAAY,EACZ,yBAAyB,EACzB,iBAAiB,EACjB,uBAAuB,GACU;QACjC,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B;;;;;;;eAOG;YACH,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YAC7C,MAAM,KAAK,GACT,SAAS,CAAC,MAAM,IAAI,CAAC;gBACnB,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;gBACtB,CAAC,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,SAAS,CAAC,MAAM,SAAS,CAAC;YAC3E,MAAM,IAAI,KAAK,CACb,uDAAuD,IAAI,KAAK;gBAC9D,sDAAsD,KAAK,IAAI;gBAC/D,2EAA2E;gBAC3E,yCAAyC,CAC5C,CAAC;QACJ,CAAC;QAED,MAAM,YAAY,GAA+B;YAC/C,MAAM;YACN,aAAa;YACb,SAAS;YACT,YAAY;YACZ,UAAU;YACV,yBAAyB;YACzB,wBAAwB,EAAE,qCAAqC,CAAC;gBAC9D,OAAO,EAAE,iBAAiB;gBAC1B,OAAO,EAAE,uBAAuB;aACjC,CAAC;SACH,CAAC;QACF,OAAO,CACL,KAAC,6BAA6B,IAAC,KAAK,EAAE,YAAY,YAChD,KAAC,QAAQ,IACP,cAAc,EAAE,cAAc,EAC9B,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,aAAa,YAE5B,KAAC,OAAO,KAAG,GACF,GACmB,CACjC,CAAC;IACJ,CAAC;IACD,2BAA2B,CAAC,WAAW,GAAG,6BAA6B,CAAC;IAExE,OAAO,2BAA2B,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,OAAO,EAAE,kBAAkB,EAAE,CAAC","sourcesContent":["import type { ComponentType, FunctionComponent, ReactNode } from 'react';\n\nimport type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';\nimport type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport {\n WebsiteRuntimeContextProvider,\n type WebsiteRuntimeContextValue,\n} from '../core/contexts/WebsiteRuntimeContext';\nimport { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website';\nimport type { GeneratedWebsiteProviderModules } from '../core/external-providers/frontend-provider-registry.website';\nimport { BlogPost, BLOG_POST_PAGE_REF } from '../mdx/BlogPost';\nimport type { WebsiteMdxComponentsMap } from '../mdx/website-mdx-provider';\nimport type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';\nimport type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';\nimport type { WebsiteNavigationConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport type { LabelPack } from '../schemas/validators/label-pack.validator';\n\n/**\n * @wildo_source:part:start saas.website.blog.bridge facet:layer:astro facet:family:website\n *\n * `createWebsiteBlogPostBridgeIsland(deps)` — factory that returns the\n * React island a consumer's per-locale `*.bridge.tsx` module exports for\n * mounting blog post pages. **Sibling** of `createWebsitePageBridgeIsland`\n * but adapted to the dynamic-routing constraint of the marketing-blog\n * adapter (Phase 7, D-6 = Option A).\n *\n * # Why blog posts cannot reuse the page bridge factory\n *\n * Three structural differences between marketing pages and blog posts\n * forced this dedicated factory shape:\n *\n * 1. **Per-slug body resolution.** A marketing page (`landing`,\n * `pricing`, etc.) has ONE `PageComponent` known at bridge-module\n * load time — the page bridge closes over it. A blog post's body\n * is the compiled MDX module, which varies per slug. Closing the\n * MDX body inside the factory at module load time would force one\n * bridge file per post (defeats Q16=B \"one MDX file per post\").\n * Instead, the factory closes over a `slugToMdxBody: Record<string,\n * ComponentType>` map — typically built from\n * `import.meta.glob('./content/blog/<locale>/*.mdx', { eager: true })`\n * on the consumer side — and the island accepts the active `slug`\n * as a primitive (JSON-serializable) prop.\n *\n * 2. **Layout already wrapped by `<BlogPost>`.** The page bridge wraps\n * its `PageComponent` in `<WebsitePageLayout pageRef={…}>`. The\n * `<BlogPost>` renderer ALREADY wraps its tree in\n * `<WebsitePageLayout pageRef={BLOG_POST_PAGE_REF}>` (the framework\n * reserves the page ref for the entire blog surface — see\n * `BLOG_POST_PAGE_REF` JSDoc in `BlogPost.tsx`). Reusing the page\n * bridge would double-wrap the layout. This factory instead\n * provides the runtime context and yields directly to `<BlogPost>`,\n * which owns the layout boundary itself.\n *\n * 3. **Astro's React-children context-propagation gap.** Astro\n * renders React component children INDEPENDENTLY of the parent\n * component when the boundary is `.astro` ↔ `.tsx`, even on the\n * server side. This means\n * `<BlogPost client:load><Content /></BlogPost>` written in an\n * `.astro` file would NOT propagate the `<WebsiteMdxProvider>`\n * context to the React components embedded inside `<Content />`\n * (the embedded `<CTASection />` would render WITHOUT the engine's\n * MDX provider scope; the `mdxComponents` map would be ignored).\n * Confirmed via the Astro docs, `@astrojs/react` integration\n * readme, and the documented \"Bridging two JSX runtimes\" gotcha.\n *\n * The fix: the entire blog post tree (BlogPost + MDX body +\n * embedded section components) lives inside ONE React island. The\n * MDX module is imported as a React component via Astro's\n * `import.meta.glob`, and the bridge renders it as JSX children\n * of `<BlogPost>` from INSIDE the island — no `.astro` ↔ `.tsx`\n * boundary is crossed during the body render.\n *\n * # Why hydration is required (vs. pure SSR)\n *\n * The `WebsiteRuntimeContextProvider` + `<BlogPost>` tree could in\n * theory render on the server only (no `client:*` directive). But:\n *\n * - Embedded sections (`<CTASection />`) typically have their own\n * interactivity (analytics handlers, A/B variant flippers, etc.)\n * that need React event listeners to attach client-side.\n * - The framework's `useWebsiteLabel(...)` hook chain assumes a live\n * React render tree — a pure-SSR tree would freeze label resolution\n * at build time, but any future client-driven label override (e.g.\n * a debug-mode label inspector) would silently break.\n *\n * The factory therefore returns an island that the consumer's `.astro`\n * mounts with `client:load` (or `client:idle` for posts known to be\n * non-interactive). The hydration cost is bounded by the per-locale MDX\n * glob — see \"Bundle scaling cliff\" below.\n *\n * # Bundle scaling cliff (deferred trade-off)\n *\n * `import.meta.glob({ eager: true })` materializes EVERY post's\n * compiled JS into the per-locale React-island bundle (~5–20 KB per\n * post depending on length and embedded section count). For the\n * Phase 7 dogfood (≤10 posts) this is fine and matches the documented\n * Astro+MDX pattern. Past ~50 posts this becomes a real budget concern.\n * The factory's API is forward-compatible with `{ eager: false }`\n * (lazy loaders): swap the consumer's glob to lazy mode and the\n * `slugToMdxBody[slug]` lookup returns a `() => Promise<{ default:\n * ComponentType }>`; wrap in `React.lazy()` + `<Suspense>` and the\n * factory call site stays unchanged. The migration is a consumer-side\n * change, not an engine API break.\n *\n * # Authoring pattern\n *\n * **Per-locale bridge file** (consumer side, ONE per locale):\n *\n * ```tsx\n * // examples/<app>/website/src/pages-impl/blog-post-en.bridge.tsx\n * import type { ComponentType } from 'react';\n *\n * import { createWebsiteBlogPostBridgeIsland }\n * from '@wildo-ai/saas-website/mdx';\n * import { BLOG_POST_MDX_COMPONENTS }\n * from './blog-post.mdx-components';\n * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';\n *\n * // Static analyzable glob — Vite resolves at build time. The glob\n * // pattern MUST be a single string literal (no concatenation, no\n * // variable interpolation); Vite parses it AST-side. The `as` cast\n * // narrows the unknown record to the per-MDX-file shape Astro's mdx\n * // integration emits (default export = the compiled React component).\n * const rawModules = import.meta.glob('./content/blog/en/*.mdx', {\n * eager: true,\n * }) as Record<string, { default: ComponentType }>;\n *\n * const slugToMdxBody: Record<string, ComponentType> = {};\n * for (const [path, mod] of Object.entries(rawModules)) {\n * // './content/blog/en/why-context.mdx' -> 'why-context'\n * const slug = path.split('/').pop()!.replace(/\\.mdx$/, '');\n * slugToMdxBody[slug] = mod.default;\n * }\n *\n * export const BlogPostEnBridgeIsland =\n * createWebsiteBlogPostBridgeIsland({\n * slugToMdxBody,\n * navigation: WEBSITE_ROOT_CONFIG.navigation,\n * mdxComponents: BLOG_POST_MDX_COMPONENTS,\n * });\n * ```\n *\n * **Per-locale `[slug].astro` shell** (consumer side):\n *\n * ```astro\n * ---\n * import { getCollection } from 'astro:content';\n * import {\n * loadWebsitePageRuntime,\n * renderBlogPostHeadHtml,\n * renderWebsiteDesignTokensCss,\n * } from '@wildo-ai/saas-website/astro';\n * import { parseBlogPostFrontmatter } from '@wildo-ai/saas-website/mdx';\n * import { BlogPostEnBridgeIsland } from '@/pages-impl/blog-post-en.bridge';\n * import { BLOG_POST_PAGE_MANIFEST } from '@/pages-impl/blog-post.page';\n * import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';\n *\n * export async function getStaticPaths() {\n * const posts = await getCollection('blog');\n * const defaultLocale = WEBSITE_SITE_CONTEXT.websiteConfig.defaultLocale;\n * return posts\n * .filter((p) => p.data.locale === defaultLocale)\n * .map((p) => ({ params: { slug: p.data.slug }, props: { entry: p } }));\n * }\n * const { entry } = Astro.props;\n *\n * const runtime = loadWebsitePageRuntime({\n * manifest: BLOG_POST_PAGE_MANIFEST,\n * siteContext: WEBSITE_SITE_CONTEXT,\n * labelPacksDir: './src/i18n',\n * astroLocale: Astro.currentLocale,\n * });\n * const frontmatter = parseBlogPostFrontmatter(entry.data, { source: entry.id });\n * const headHtml = renderBlogPostHeadHtml({\n * siteContext: WEBSITE_SITE_CONTEXT,\n * locale: runtime.locale,\n * routePath: `/blog/${frontmatter.slug}`,\n * frontmatter,\n * });\n * const designTokensCss = renderWebsiteDesignTokensCss(\n * WEBSITE_SITE_CONTEXT.rootConfig.designTokens,\n * );\n * ---\n * <!doctype html>\n * <html lang={runtime.locale}>\n * <head>\n * <meta charset=\"UTF-8\" />\n * <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n * <Fragment set:html={headHtml} />\n * <style set:html={designTokensCss}></style>\n * </head>\n * <body>\n * <BlogPostEnBridgeIsland\n * client:load\n * slug={frontmatter.slug}\n * rawFrontmatter={entry.data}\n * source={entry.id}\n * locale={runtime.locale}\n * defaultLocale={runtime.defaultLocale}\n * labelPack={runtime.labelPack}\n * designTokens={WEBSITE_SITE_CONTEXT.rootConfig.designTokens}\n * skipToMainContentLabelKey={\n * WEBSITE_SITE_CONTEXT.rootConfig.skipToMainContentLabelKey\n * }\n * />\n * </body>\n * </html>\n * ```\n *\n * # Boundary\n *\n * Strictly client-bundle-safe (re-exported from `mdx.ts` — NOT from\n * `astro-island.ts`, because this module hard-imports the MDX adapter\n * whose `@mdx-js/react` peer is optional; walked by the\n * bundle-isolation test). MUST stay clear of `node:*` and SSR-tier\n * specifiers.\n */\n\n/**\n * Module-load-time dependencies — closed over inside the returned\n * island. NEVER cross the hydration boundary as serialized props.\n */\nexport interface CreateWebsiteBlogPostBridgeIslandDeps {\n /**\n * Per-slug map of compiled MDX bodies (each entry is a React\n * component — the `.default` export of the MDX module). Typically\n * built from `import.meta.glob('./content/blog/<locale>/*.mdx',\n * { eager: true })` with the path-key normalized to the slug. Closed\n * over at module-load time so the entire MDX body graph for the\n * locale lands inside ONE React island bundle (D-6 Option A) — this\n * is the structural fix for Astro's React-children context-propagation\n * gap (see file preamble).\n *\n * The map is read by `slugToMdxBody[props.slug]` at island render\n * time. A missing slug throws a clear framework-owned error (rather than\n * rendering an empty body silently); the consumer's `getStaticPaths`\n * is responsible for not emitting routes for slugs absent from the\n * map.\n *\n * **Why `ComponentType<any>` and not a stricter generic**: matches\n * the upstream `@mdx-js/react` convention (`MDXContent` from\n * `mdx/types` is parameterized over an open `MDXProps` shape). MDX\n * compilation hands the body a compiler-managed props object that\n * the type system cannot narrow at the framework boundary; using\n * `<any>` here keeps the consumer's\n * `import.meta.glob(...) as Record<string, { default: ComponentType }>`\n * cast assignable without an additional wrap/unwrap. Same posture\n * as `WebsiteMdxComponentsMap` (intentional symmetry).\n */\n slugToMdxBody: Readonly<Record<string, ComponentType<any>>>;\n /**\n * Site-wide navigation slot bindings (header / footer components).\n * Closed over here for the same reason as the page bridge: both\n * fields are `ComponentType` refs that cannot survive Astro's\n * serialization boundary.\n */\n navigation: WebsiteNavigationConfig;\n /**\n * MDX-tag → React-component map forwarded into `<BlogPost>` and\n * scoped via `<WebsiteMdxProvider>`. Typically a small object\n * literal exporting the consumer-authored business sections that\n * blog posts can embed (`<CTASection />`, etc.). Closed over here\n * (rather than passed as an island prop) because the values are\n * `ComponentType` refs.\n */\n mdxComponents?: WebsiteMdxComponentsMap;\n}\n\n/**\n * Hydration-boundary props — what the consumer's `.astro` page passes\n * to `<BlogPostEnBridgeIsland client:load ...{primitiveProps}/>`.\n * Every field is JSON-serializable.\n */\nexport interface WebsiteBlogPostBridgeIslandProps {\n /**\n * Slug of the post being rendered. Used by the island to look up\n * the matching MDX body in `deps.slugToMdxBody`. Carried as a plain\n * string (not the branded `BlogPostSlug`) because Astro's\n * serialization boundary unwraps brands.\n */\n slug: string;\n /**\n * Raw frontmatter object Astro hands the page from `entry.data`.\n * Forwarded straight to `<BlogPost>` for strict Zod-4 validation.\n * The bridge does NOT re-validate (single validation surface, owned\n * by the renderer).\n */\n rawFrontmatter: unknown;\n /**\n * Diagnostic source identifier echoed into validation errors —\n * typically `entry.id` from Astro's content collection. Optional\n * but recommended for actionable error messages.\n */\n source?: string;\n /** The locale Astro resolved (with default-locale fallback). */\n locale: AvailableLanguage;\n /**\n * The site's default locale. Required for `WebsiteInternalLink`\n * locale-prefix logic (mirrors the page bridge's `defaultLocale`\n * island prop).\n */\n defaultLocale: AvailableLanguage;\n /** Already-loaded + already-shape-validated label pack. */\n labelPack: LabelPack;\n /** Design-token bundle from `WebsiteRootConfig.designTokens`. */\n designTokens: WebsiteDesignTokens;\n /**\n * Fully-composed label key the framework reads to render the\n * \"Skip to main content\" affordance. Mirrors\n * `WebsiteRootConfig.skipToMainContentLabelKey`. Carried as a\n * primitive island prop for the same reason as the page bridge.\n */\n skipToMainContentLabelKey: WebsiteLabelKey;\n /** Optional website-facing public providers materialized at build time. */\n frontendProviders?: FrontendProvidersBlock;\n /**\n * The CODE channel for those providers (#332): the\n * `generatedFrontendProviderModules` export of this website service's own\n * `src/generated/frontend-providers.generated.ts`, written by\n * `wildo config sync`.\n *\n * `frontendProviders` above says WHICH providers this service turned on, in\n * this environment; this says what code the bundle actually contains for\n * them. Both are needed, because a JSON payload cannot carry an SDK loader\n * and a bundle cannot know which environment it is running in.\n *\n * NOT a JSON-serializable island prop like its siblings, and it does not need\n * to be: the bridge module is bundled with the page, so the generated module\n * map is an ordinary import in the same graph rather than something that has\n * to cross the hydration boundary.\n *\n * Omitted is the ordinary case for a website declaring no package-shipped\n * provider: no such file is generated, so there is nothing to import.\n */\n frontendProviderModules?: GeneratedWebsiteProviderModules;\n}\n\nconst EMPTY_MDX_COMPONENTS: WebsiteMdxComponentsMap = Object.freeze({});\n\n/**\n * Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function\n * component — Astro hydrates nothing else here — and the union is not callable, so declaring it\n * cost callers the ability to invoke the island directly (which the bridge suites do, to observe\n * the synchronous throw for an unregistered slug without routing it through React's reconciler\n * error path). `FunctionComponent` is assignable to `ComponentType`, so no consumer narrows.\n */\nexport function createWebsiteBlogPostBridgeIsland(\n deps: CreateWebsiteBlogPostBridgeIslandDeps,\n): FunctionComponent<WebsiteBlogPostBridgeIslandProps> {\n const {\n slugToMdxBody,\n navigation,\n mdxComponents = EMPTY_MDX_COMPONENTS,\n } = deps;\n\n function WebsiteBlogPostBridgeIsland({\n slug,\n rawFrontmatter,\n source,\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n skipToMainContentLabelKey,\n frontendProviders,\n frontendProviderModules,\n }: WebsiteBlogPostBridgeIslandProps): ReactNode {\n const MdxBody = slugToMdxBody[slug];\n if (MdxBody === undefined) {\n /**\n * Loud failure — the consumer's `getStaticPaths` is supposed to\n * only emit routes for slugs present in the MDX glob. A missing\n * lookup means the glob and the collection are out of sync; the\n * operator needs an actionable error string with the offending\n * slug + the keys that ARE present (truncated to keep the build\n * log readable).\n */\n const knownKeys = Object.keys(slugToMdxBody);\n const known =\n knownKeys.length <= 5\n ? knownKeys.join(', ')\n : `${knownKeys.slice(0, 5).join(', ')}, … (${knownKeys.length} total)`;\n throw new Error(\n `[blog-post-bridge] No MDX body registered for slug=\"${slug}\". ` +\n `The bridge factory's slugToMdxBody map only knows: ${known}. ` +\n `Verify the consumer's import.meta.glob('...content/blog/<locale>/*.mdx') ` +\n `pattern matches the source file naming.`,\n );\n }\n\n const runtimeValue: WebsiteRuntimeContextValue = {\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n navigation,\n skipToMainContentLabelKey,\n frontendProviderRegistry: createFrontendWebsiteProviderRegistry({\n entries: frontendProviders,\n modules: frontendProviderModules,\n }),\n };\n return (\n <WebsiteRuntimeContextProvider value={runtimeValue}>\n <BlogPost\n rawFrontmatter={rawFrontmatter}\n source={source}\n mdxComponents={mdxComponents}\n >\n <MdxBody />\n </BlogPost>\n </WebsiteRuntimeContextProvider>\n );\n }\n WebsiteBlogPostBridgeIsland.displayName = 'WebsiteBlogPostBridgeIsland';\n\n return WebsiteBlogPostBridgeIsland;\n}\n\n/**\n * Re-export of the framework-reserved blog-post page ref. Consumers\n * authoring `BLOG_POST_PAGE_MANIFEST` need this constant to satisfy\n * the manifest's `ref` field; re-exporting it from the bridge module\n * means the consumer's per-locale bridge file can grab both symbols\n * from the one `@wildo-ai/saas-website/mdx` import it already has.\n */\nexport { BLOG_POST_PAGE_REF };\n/** @wildo_source:part:end saas.website.blog.bridge */\n"]}
|
|
1
|
+
{"version":3,"file":"blog-post-bridge.js","sourceRoot":"","sources":["../../../../src/astro/blog-post-bridge.tsx"],"names":[],"mappings":";AAKA,OAAO,EACL,6BAA6B,GAE9B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,qCAAqC,EAAE,MAAM,+DAA+D,CAAC;AAEtH,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAiU/D,MAAM,oBAAoB,GAA4B,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAExE;;;;;;GAMG;AACH,MAAM,UAAU,iCAAiC,CAC/C,IAA2C;IAE3C,MAAM,EACJ,aAAa,EACb,UAAU,EACV,aAAa,GAAG,oBAAoB,GACrC,GAAG,IAAI,CAAC;IAET,SAAS,2BAA2B,CAAC,EACnC,IAAI,EACJ,cAAc,EACd,MAAM,EACN,MAAM,EACN,aAAa,EACb,SAAS,EACT,YAAY,EACZ,yBAAyB,EACzB,iBAAiB,EACjB,SAAS,GACwB;QACjC,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B;;;;;;;eAOG;YACH,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YAC7C,MAAM,KAAK,GACT,SAAS,CAAC,MAAM,IAAI,CAAC;gBACnB,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;gBACtB,CAAC,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,SAAS,CAAC,MAAM,SAAS,CAAC;YAC3E,MAAM,IAAI,KAAK,CACb,uDAAuD,IAAI,KAAK;gBAC9D,sDAAsD,KAAK,IAAI;gBAC/D,2EAA2E;gBAC3E,yCAAyC,CAC5C,CAAC;QACJ,CAAC;QAED,MAAM,YAAY,GAA+B;YAC/C,MAAM;YACN,aAAa;YACb,SAAS;YACT,YAAY;YACZ,UAAU;YACV,yBAAyB;YACzB,SAAS;YACT,GAAG,CAAC,IAAI,CAAC,sBAAsB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7G,wBAAwB,EAAE,qCAAqC,CAAC;gBAC9D,OAAO,EAAE,iBAAiB;gBAC1B,OAAO,EAAE,IAAI,CAAC,uBAAuB;aACtC,CAAC;SACH,CAAC;QACF,OAAO,CACL,KAAC,6BAA6B,IAAC,KAAK,EAAE,YAAY,YAChD,KAAC,QAAQ,IACP,cAAc,EAAE,cAAc,EAC9B,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,aAAa,YAE5B,KAAC,OAAO,KAAG,GACF,GACmB,CACjC,CAAC;IACJ,CAAC;IACD,2BAA2B,CAAC,WAAW,GAAG,6BAA6B,CAAC;IAExE,OAAO,2BAA2B,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,OAAO,EAAE,kBAAkB,EAAE,CAAC","sourcesContent":["import type { ComponentType, FunctionComponent, ReactNode } from 'react';\n\nimport type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';\nimport type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport {\n WebsiteRuntimeContextProvider,\n type WebsiteRuntimeContextValue,\n} from '../core/contexts/WebsiteRuntimeContext';\nimport { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website';\nimport type { GeneratedWebsiteProviderModules } from '../core/external-providers/frontend-provider-registry.website';\nimport { BlogPost, BLOG_POST_PAGE_REF } from '../mdx/BlogPost';\nimport type { WebsiteMdxComponentsMap } from '../mdx/website-mdx-provider';\nimport type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';\nimport type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';\nimport type { WebsiteNavigationConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport type { LabelPack } from '../schemas/validators/label-pack.validator';\n\n/**\n * @wildo_source:part:start saas.website.blog.bridge facet:layer:astro facet:family:website\n *\n * `createWebsiteBlogPostBridgeIsland(deps)` — factory that returns the\n * React island a consumer's per-locale `*.bridge.tsx` module exports for\n * mounting blog post pages. **Sibling** of `createWebsitePageBridgeIsland`\n * but adapted to the dynamic-routing constraint of the marketing-blog\n * adapter (Phase 7, D-6 = Option A).\n *\n * # Why blog posts cannot reuse the page bridge factory\n *\n * Three structural differences between marketing pages and blog posts\n * forced this dedicated factory shape:\n *\n * 1. **Per-slug body resolution.** A marketing page (`landing`,\n * `pricing`, etc.) has ONE `PageComponent` known at bridge-module\n * load time — the page bridge closes over it. A blog post's body\n * is the compiled MDX module, which varies per slug. Closing the\n * MDX body inside the factory at module load time would force one\n * bridge file per post (defeats Q16=B \"one MDX file per post\").\n * Instead, the factory closes over a `slugToMdxBody: Record<string,\n * ComponentType>` map — typically built from\n * `import.meta.glob('./content/blog/<locale>/*.mdx', { eager: true })`\n * on the consumer side — and the island accepts the active `slug`\n * as a primitive (JSON-serializable) prop.\n *\n * 2. **Layout already wrapped by `<BlogPost>`.** The page bridge wraps\n * its `PageComponent` in `<WebsitePageLayout pageRef={…}>`. The\n * `<BlogPost>` renderer ALREADY wraps its tree in\n * `<WebsitePageLayout pageRef={BLOG_POST_PAGE_REF}>` (the framework\n * reserves the page ref for the entire blog surface — see\n * `BLOG_POST_PAGE_REF` JSDoc in `BlogPost.tsx`). Reusing the page\n * bridge would double-wrap the layout. This factory instead\n * provides the runtime context and yields directly to `<BlogPost>`,\n * which owns the layout boundary itself.\n *\n * 3. **Astro's React-children context-propagation gap.** Astro\n * renders React component children INDEPENDENTLY of the parent\n * component when the boundary is `.astro` ↔ `.tsx`, even on the\n * server side. This means\n * `<BlogPost client:load><Content /></BlogPost>` written in an\n * `.astro` file would NOT propagate the `<WebsiteMdxProvider>`\n * context to the React components embedded inside `<Content />`\n * (the embedded `<CTASection />` would render WITHOUT the engine's\n * MDX provider scope; the `mdxComponents` map would be ignored).\n * Confirmed via the Astro docs, `@astrojs/react` integration\n * readme, and the documented \"Bridging two JSX runtimes\" gotcha.\n *\n * The fix: the entire blog post tree (BlogPost + MDX body +\n * embedded section components) lives inside ONE React island. The\n * MDX module is imported as a React component via Astro's\n * `import.meta.glob`, and the bridge renders it as JSX children\n * of `<BlogPost>` from INSIDE the island — no `.astro` ↔ `.tsx`\n * boundary is crossed during the body render.\n *\n * # Why hydration is required (vs. pure SSR)\n *\n * The `WebsiteRuntimeContextProvider` + `<BlogPost>` tree could in\n * theory render on the server only (no `client:*` directive). But:\n *\n * - Embedded sections (`<CTASection />`) typically have their own\n * interactivity (analytics handlers, A/B variant flippers, etc.)\n * that need React event listeners to attach client-side.\n * - The framework's `useWebsiteLabel(...)` hook chain assumes a live\n * React render tree — a pure-SSR tree would freeze label resolution\n * at build time, but any future client-driven label override (e.g.\n * a debug-mode label inspector) would silently break.\n *\n * The factory therefore returns an island that the consumer's `.astro`\n * mounts with `client:load` (or `client:idle` for posts known to be\n * non-interactive). The hydration cost is bounded by the per-locale MDX\n * glob — see \"Bundle scaling cliff\" below.\n *\n * # Bundle scaling cliff (deferred trade-off)\n *\n * `import.meta.glob({ eager: true })` materializes EVERY post's\n * compiled JS into the per-locale React-island bundle (~5–20 KB per\n * post depending on length and embedded section count). For the\n * Phase 7 dogfood (≤10 posts) this is fine and matches the documented\n * Astro+MDX pattern. Past ~50 posts this becomes a real budget concern.\n * The factory's API is forward-compatible with `{ eager: false }`\n * (lazy loaders): swap the consumer's glob to lazy mode and the\n * `slugToMdxBody[slug]` lookup returns a `() => Promise<{ default:\n * ComponentType }>`; wrap in `React.lazy()` + `<Suspense>` and the\n * factory call site stays unchanged. The migration is a consumer-side\n * change, not an engine API break.\n *\n * # Authoring pattern\n *\n * **Per-locale bridge file** (consumer side, ONE per locale):\n *\n * ```tsx\n * // examples/<app>/website/src/pages-impl/blog-post-en.bridge.tsx\n * import type { ComponentType } from 'react';\n *\n * import { createWebsiteBlogPostBridgeIsland }\n * from '@wildo-ai/saas-website/mdx';\n * import { BLOG_POST_MDX_COMPONENTS }\n * from './blog-post.mdx-components';\n * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';\n *\n * // Static analyzable glob — Vite resolves at build time. The glob\n * // pattern MUST be a single string literal (no concatenation, no\n * // variable interpolation); Vite parses it AST-side. The `as` cast\n * // narrows the unknown record to the per-MDX-file shape Astro's mdx\n * // integration emits (default export = the compiled React component).\n * const rawModules = import.meta.glob('./content/blog/en/*.mdx', {\n * eager: true,\n * }) as Record<string, { default: ComponentType }>;\n *\n * const slugToMdxBody: Record<string, ComponentType> = {};\n * for (const [path, mod] of Object.entries(rawModules)) {\n * // './content/blog/en/why-context.mdx' -> 'why-context'\n * const slug = path.split('/').pop()!.replace(/\\.mdx$/, '');\n * slugToMdxBody[slug] = mod.default;\n * }\n *\n * export const BlogPostEnBridgeIsland =\n * createWebsiteBlogPostBridgeIsland({\n * slugToMdxBody,\n * navigation: WEBSITE_ROOT_CONFIG.navigation,\n * mdxComponents: BLOG_POST_MDX_COMPONENTS,\n * });\n * ```\n *\n * **Per-locale `[slug].astro` shell** (consumer side):\n *\n * ```astro\n * ---\n * import { getCollection } from 'astro:content';\n * import {\n * loadWebsitePageRuntime,\n * renderBlogPostHeadHtml,\n * renderWebsiteDesignTokensCss,\n * } from '@wildo-ai/saas-website/astro';\n * import { parseBlogPostFrontmatter } from '@wildo-ai/saas-website/mdx';\n * import { BlogPostEnBridgeIsland } from '@/pages-impl/blog-post-en.bridge';\n * import { BLOG_POST_PAGE_MANIFEST } from '@/pages-impl/blog-post.page';\n * import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';\n *\n * export async function getStaticPaths() {\n * const posts = await getCollection('blog');\n * const defaultLocale = WEBSITE_SITE_CONTEXT.websiteConfig.defaultLocale;\n * return posts\n * .filter((p) => p.data.locale === defaultLocale)\n * .map((p) => ({ params: { slug: p.data.slug }, props: { entry: p } }));\n * }\n * const { entry } = Astro.props;\n *\n * const runtime = loadWebsitePageRuntime({\n * manifest: BLOG_POST_PAGE_MANIFEST,\n * siteContext: WEBSITE_SITE_CONTEXT,\n * labelPacksDir: './src/i18n',\n * astroLocale: Astro.currentLocale,\n * });\n * const frontmatter = parseBlogPostFrontmatter(entry.data, { source: entry.id });\n * const headHtml = renderBlogPostHeadHtml({\n * siteContext: WEBSITE_SITE_CONTEXT,\n * locale: runtime.locale,\n * routePath: `/blog/${frontmatter.slug}`,\n * frontmatter,\n * });\n * const designTokensCss = renderWebsiteDesignTokensCss(\n * WEBSITE_SITE_CONTEXT.rootConfig.designTokens,\n * );\n * ---\n * <!doctype html>\n * <html lang={runtime.locale}>\n * <head>\n * <meta charset=\"UTF-8\" />\n * <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n * <Fragment set:html={headHtml} />\n * <style set:html={designTokensCss}></style>\n * </head>\n * <body>\n * <BlogPostEnBridgeIsland\n * client:load\n * slug={frontmatter.slug}\n * rawFrontmatter={entry.data}\n * source={entry.id}\n * locale={runtime.locale}\n * defaultLocale={runtime.defaultLocale}\n * labelPack={runtime.labelPack}\n * designTokens={WEBSITE_SITE_CONTEXT.rootConfig.designTokens}\n * skipToMainContentLabelKey={\n * WEBSITE_SITE_CONTEXT.rootConfig.skipToMainContentLabelKey\n * }\n * />\n * </body>\n * </html>\n * ```\n *\n * # Boundary\n *\n * Strictly client-bundle-safe (re-exported from `mdx.ts` — NOT from\n * `astro-island.ts`, because this module hard-imports the MDX adapter\n * whose `@mdx-js/react` peer is optional; walked by the\n * bundle-isolation test). MUST stay clear of `node:*` and SSR-tier\n * specifiers.\n */\n\n/**\n * Module-load-time dependencies — closed over inside the returned\n * island. NEVER cross the hydration boundary as serialized props.\n */\nexport interface CreateWebsiteBlogPostBridgeIslandDeps {\n /**\n * Browser CODE imported by the consumer bridge, closed over at module load.\n * Never pass executable modules through Astro island props: those are serialized.\n * Config sync emits a stable map, including an empty map when no providers are declared.\n */\n frontendProviderModules?: GeneratedWebsiteProviderModules;\n /**\n * Per-slug map of compiled MDX bodies (each entry is a React\n * component — the `.default` export of the MDX module). Typically\n * built from `import.meta.glob('./content/blog/<locale>/*.mdx',\n * { eager: true })` with the path-key normalized to the slug. Closed\n * over at module-load time so the entire MDX body graph for the\n * locale lands inside ONE React island bundle (D-6 Option A) — this\n * is the structural fix for Astro's React-children context-propagation\n * gap (see file preamble).\n *\n * The map is read by `slugToMdxBody[props.slug]` at island render\n * time. A missing slug throws a clear framework-owned error (rather than\n * rendering an empty body silently); the consumer's `getStaticPaths`\n * is responsible for not emitting routes for slugs absent from the\n * map.\n *\n * **Why `ComponentType<any>` and not a stricter generic**: matches\n * the upstream `@mdx-js/react` convention (`MDXContent` from\n * `mdx/types` is parameterized over an open `MDXProps` shape). MDX\n * compilation hands the body a compiler-managed props object that\n * the type system cannot narrow at the framework boundary; using\n * `<any>` here keeps the consumer's\n * `import.meta.glob(...) as Record<string, { default: ComponentType }>`\n * cast assignable without an additional wrap/unwrap. Same posture\n * as `WebsiteMdxComponentsMap` (intentional symmetry).\n */\n slugToMdxBody: Readonly<Record<string, ComponentType<any>>>;\n /**\n * Site-wide navigation slot bindings (header / footer components).\n * Closed over here for the same reason as the page bridge: both\n * fields are `ComponentType` refs that cannot survive Astro's\n * serialization boundary.\n */\n navigation: WebsiteNavigationConfig;\n /**\n * Optional replacement for the framework's visitor consent panel (#534). Closed over for the same\n * reason as `navigation`. See `WebsiteRuntimeContextValue.consentBannerComponent`.\n */\n consentBannerComponent?: ComponentType;\n /**\n * MDX-tag → React-component map forwarded into `<BlogPost>` and\n * scoped via `<WebsiteMdxProvider>`. Typically a small object\n * literal exporting the consumer-authored business sections that\n * blog posts can embed (`<CTASection />`, etc.). Closed over here\n * (rather than passed as an island prop) because the values are\n * `ComponentType` refs.\n */\n mdxComponents?: WebsiteMdxComponentsMap;\n}\n\n/**\n * Hydration-boundary props — what the consumer's `.astro` page passes\n * to `<BlogPostEnBridgeIsland client:load ...{primitiveProps}/>`.\n * Every field is JSON-serializable.\n */\nexport interface WebsiteBlogPostBridgeIslandProps {\n /**\n * Slug of the post being rendered. Used by the island to look up\n * the matching MDX body in `deps.slugToMdxBody`. Carried as a plain\n * string (not the branded `BlogPostSlug`) because Astro's\n * serialization boundary unwraps brands.\n */\n slug: string;\n /**\n * Raw frontmatter object Astro hands the page from `entry.data`.\n * Forwarded straight to `<BlogPost>` for strict Zod-4 validation.\n * The bridge does NOT re-validate (single validation surface, owned\n * by the renderer).\n */\n rawFrontmatter: unknown;\n /**\n * Diagnostic source identifier echoed into validation errors —\n * typically `entry.id` from Astro's content collection. Optional\n * but recommended for actionable error messages.\n */\n source?: string;\n /** The locale Astro resolved (with default-locale fallback). */\n locale: AvailableLanguage;\n /**\n * The site's default locale. Required for `WebsiteInternalLink`\n * locale-prefix logic (mirrors the page bridge's `defaultLocale`\n * island prop).\n */\n defaultLocale: AvailableLanguage;\n /** Already-loaded + already-shape-validated label pack. */\n labelPack: LabelPack;\n /** Design-token bundle from `WebsiteRootConfig.designTokens`. */\n designTokens: WebsiteDesignTokens;\n /**\n * Fully-composed label key the framework reads to render the\n * \"Skip to main content\" affordance. Mirrors\n * `WebsiteRootConfig.skipToMainContentLabelKey`. Carried as a\n * primitive island prop for the same reason as the page bridge.\n */\n skipToMainContentLabelKey: WebsiteLabelKey;\n /** Optional website-facing public providers materialized at build time. */\n frontendProviders?: FrontendProvidersBlock;\n /** Product origin for navigation and call-to-action components inside the blog island.\n * Supplied by the Astro page from websiteConfig, just as on ordinary page bridges. */\n appOrigin?: string;\n}\n\nconst EMPTY_MDX_COMPONENTS: WebsiteMdxComponentsMap = Object.freeze({});\n\n/**\n * Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function\n * component — Astro hydrates nothing else here — and the union is not callable, so declaring it\n * cost callers the ability to invoke the island directly (which the bridge suites do, to observe\n * the synchronous throw for an unregistered slug without routing it through React's reconciler\n * error path). `FunctionComponent` is assignable to `ComponentType`, so no consumer narrows.\n */\nexport function createWebsiteBlogPostBridgeIsland(\n deps: CreateWebsiteBlogPostBridgeIslandDeps,\n): FunctionComponent<WebsiteBlogPostBridgeIslandProps> {\n const {\n slugToMdxBody,\n navigation,\n mdxComponents = EMPTY_MDX_COMPONENTS,\n } = deps;\n\n function WebsiteBlogPostBridgeIsland({\n slug,\n rawFrontmatter,\n source,\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n skipToMainContentLabelKey,\n frontendProviders,\n appOrigin,\n }: WebsiteBlogPostBridgeIslandProps): ReactNode {\n const MdxBody = slugToMdxBody[slug];\n if (MdxBody === undefined) {\n /**\n * Loud failure — the consumer's `getStaticPaths` is supposed to\n * only emit routes for slugs present in the MDX glob. A missing\n * lookup means the glob and the collection are out of sync; the\n * operator needs an actionable error string with the offending\n * slug + the keys that ARE present (truncated to keep the build\n * log readable).\n */\n const knownKeys = Object.keys(slugToMdxBody);\n const known =\n knownKeys.length <= 5\n ? knownKeys.join(', ')\n : `${knownKeys.slice(0, 5).join(', ')}, … (${knownKeys.length} total)`;\n throw new Error(\n `[blog-post-bridge] No MDX body registered for slug=\"${slug}\". ` +\n `The bridge factory's slugToMdxBody map only knows: ${known}. ` +\n `Verify the consumer's import.meta.glob('...content/blog/<locale>/*.mdx') ` +\n `pattern matches the source file naming.`,\n );\n }\n\n const runtimeValue: WebsiteRuntimeContextValue = {\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n navigation,\n skipToMainContentLabelKey,\n appOrigin,\n ...(deps.consentBannerComponent !== undefined ? { consentBannerComponent: deps.consentBannerComponent } : {}),\n frontendProviderRegistry: createFrontendWebsiteProviderRegistry({\n entries: frontendProviders,\n modules: deps.frontendProviderModules,\n }),\n };\n return (\n <WebsiteRuntimeContextProvider value={runtimeValue}>\n <BlogPost\n rawFrontmatter={rawFrontmatter}\n source={source}\n mdxComponents={mdxComponents}\n >\n <MdxBody />\n </BlogPost>\n </WebsiteRuntimeContextProvider>\n );\n }\n WebsiteBlogPostBridgeIsland.displayName = 'WebsiteBlogPostBridgeIsland';\n\n return WebsiteBlogPostBridgeIsland;\n}\n\n/**\n * Re-export of the framework-reserved blog-post page ref. Consumers\n * authoring `BLOG_POST_PAGE_MANIFEST` need this constant to satisfy\n * the manifest's `ref` field; re-exporting it from the bridge module\n * means the consumer's per-locale bridge file can grab both symbols\n * from the one `@wildo-ai/saas-website/mdx` import it already has.\n */\nexport { BLOG_POST_PAGE_REF };\n/** @wildo_source:part:end saas.website.blog.bridge */\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blog-post-head.renderer.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"blog-post-head.renderer.d.ts","sourceRoot":"","sources":["../../../../src/astro/blog-post-head.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAG9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAChG,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,mEAAmE,CAAC;AAItH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AAEH,MAAM,WAAW,6BAA6B;IAC5C;;;;;;;;;OASG;IACH,WAAW,EAAE,kBAAkB,CAAC;IAChC;;;;OAIG;IACH,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,WAAW,EAAE,mBAAmB,CAAC;IACjC;;;;;;;;;;;;;;;OAeG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,yCAAyC;IACxD,WAAW,EAAE,mBAAmB,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAED,wBAAgB,kCAAkC,CAChD,IAAI,EAAE,yCAAyC,GAC9C,4BAA4B,CAuB9B;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,6BAA6B,GAAG,MAAM,CAkIlF;AACD,6DAA6D"}
|
|
@@ -2,6 +2,7 @@ import type { ComponentType, FunctionComponent } from 'react';
|
|
|
2
2
|
import type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';
|
|
3
3
|
import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
4
4
|
import type { GeneratedWebsiteProviderModules } from '../core/external-providers/frontend-provider-registry.website';
|
|
5
|
+
import type { WebsiteConsentRecorder } from '../core/consent/WebsiteConsentContext';
|
|
5
6
|
import type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';
|
|
6
7
|
import type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';
|
|
7
8
|
import type { WebsiteNavigationConfig } from '../schemas/manifests/website-root-config.shared.schemas';
|
|
@@ -134,6 +135,12 @@ import type { LabelPack } from '../schemas/validators/label-pack.validator';
|
|
|
134
135
|
* island. NEVER cross the hydration boundary as serialized props.
|
|
135
136
|
*/
|
|
136
137
|
export interface CreateWebsitePageBridgeIslandDeps {
|
|
138
|
+
/**
|
|
139
|
+
* Browser CODE imported by the consumer bridge, closed over at module load.
|
|
140
|
+
* Never pass executable modules through Astro island props: those are serialized.
|
|
141
|
+
* Config sync emits a stable map, including an empty map when no providers are declared.
|
|
142
|
+
*/
|
|
143
|
+
frontendProviderModules?: GeneratedWebsiteProviderModules;
|
|
137
144
|
/**
|
|
138
145
|
* The page's React component. Typically the same value that was
|
|
139
146
|
* passed to `defineWebsitePageManifest({ pageComponent })` — the
|
|
@@ -150,6 +157,24 @@ export interface CreateWebsitePageBridgeIslandDeps {
|
|
|
150
157
|
* runtime-safe.
|
|
151
158
|
*/
|
|
152
159
|
PageComponent: ComponentType<any>;
|
|
160
|
+
/**
|
|
161
|
+
* Builds the recorder that persists a VISITOR's consent decision server-side, from the island's
|
|
162
|
+
* runtime `apiOrigin` (#1258).
|
|
163
|
+
*
|
|
164
|
+
* A DEPS entry rather than an island prop, for the reason this whole interface exists: it is a
|
|
165
|
+
* function, and island props are serialized. A factory rather than an instance, because the
|
|
166
|
+
* `apiOrigin` it needs is resolved per environment by the `.astro` page and only reaches the island
|
|
167
|
+
* as a prop.
|
|
168
|
+
*
|
|
169
|
+
* The engine cannot build this itself — a client needs the application's own
|
|
170
|
+
* `frontendServiceName`, which the engine has no way to know. The application already constructs
|
|
171
|
+
* one for its capture forms; passing THAT factory here is what makes a visitor's consent decision,
|
|
172
|
+
* their captured lead and their draft note share a single `uuidRef` and transpose together.
|
|
173
|
+
*
|
|
174
|
+
* Optional. Omitted, a decision lives in the visitor's browser and nowhere else — the behaviour
|
|
175
|
+
* before this existed, and correct for a site with no anonymous-session wiring.
|
|
176
|
+
*/
|
|
177
|
+
consentRecorderFactory?: (apiOrigin: string) => WebsiteConsentRecorder;
|
|
153
178
|
/**
|
|
154
179
|
* The active page's ref. Carried as a primitive (string-branded)
|
|
155
180
|
* value so the inner `WebsitePageLayout` can register the
|
|
@@ -163,6 +188,11 @@ export interface CreateWebsitePageBridgeIslandDeps {
|
|
|
163
188
|
* serialization boundary.
|
|
164
189
|
*/
|
|
165
190
|
navigation: WebsiteNavigationConfig;
|
|
191
|
+
/**
|
|
192
|
+
* Optional replacement for the framework's visitor consent panel (#534). Closed over for the same
|
|
193
|
+
* reason as `navigation`. See `WebsiteRuntimeContextValue.consentBannerComponent`.
|
|
194
|
+
*/
|
|
195
|
+
consentBannerComponent?: ComponentType;
|
|
166
196
|
}
|
|
167
197
|
/**
|
|
168
198
|
* Hydration-boundary props — what the consumer's `.astro` page passes
|
|
@@ -201,26 +231,6 @@ export interface WebsitePageBridgeIslandProps {
|
|
|
201
231
|
skipToMainContentLabelKey: WebsiteLabelKey;
|
|
202
232
|
/** Optional website-facing public providers materialized at build time. */
|
|
203
233
|
frontendProviders?: FrontendProvidersBlock;
|
|
204
|
-
/**
|
|
205
|
-
* The CODE channel for those providers (#332): the
|
|
206
|
-
* `generatedFrontendProviderModules` export of this website service's own
|
|
207
|
-
* `src/generated/frontend-providers.generated.ts`, written by
|
|
208
|
-
* `wildo config sync`.
|
|
209
|
-
*
|
|
210
|
-
* `frontendProviders` above says WHICH providers this service turned on, in
|
|
211
|
-
* this environment; this says what code the bundle actually contains for
|
|
212
|
-
* them. Both are needed, because a JSON payload cannot carry an SDK loader
|
|
213
|
-
* and a bundle cannot know which environment it is running in.
|
|
214
|
-
*
|
|
215
|
-
* NOT a JSON-serializable island prop like its siblings, and it does not need
|
|
216
|
-
* to be: the bridge module is bundled with the page, so the generated module
|
|
217
|
-
* map is an ordinary import in the same graph rather than something that has
|
|
218
|
-
* to cross the hydration boundary.
|
|
219
|
-
*
|
|
220
|
-
* Omitted is the ordinary case for a website declaring no package-shipped
|
|
221
|
-
* provider: no such file is generated, so there is nothing to import.
|
|
222
|
-
*/
|
|
223
|
-
frontendProviderModules?: GeneratedWebsiteProviderModules;
|
|
224
234
|
/**
|
|
225
235
|
* Optional backend API origin (no trailing slash) forwarded into
|
|
226
236
|
* `WebsiteRuntimeContext.apiOrigin` for anonymous-capture sections
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-runtime.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"bridge-runtime.d.ts","sourceRoot":"","sources":["../../../../src/astro/bridge-runtime.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAa,MAAM,OAAO,CAAC;AAEzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAO9E,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,+DAA+D,CAAC;AAErH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AACpF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+DAA+D,CAAC;AACzG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iDAAiD,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yDAAyD,CAAC;AACvG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyHG;AAEH;;;GAGG;AACH,MAAM,WAAW,iCAAiC;IAChD;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,+BAA+B,CAAC;IAC1D;;;;;;;;;;;;;;OAcG;IACH,aAAa,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC;IAClC;;;;;;;;;;;;;;;;OAgBG;IACH,sBAAsB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,sBAAsB,CAAC;IACvE;;;;OAIG;IACH,OAAO,EAAE,cAAc,CAAC;IACxB;;;;;OAKG;IACH,UAAU,EAAE,uBAAuB,CAAC;IACpC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,aAAa,CAAC;CACxC;AAED;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC3C,gEAAgE;IAChE,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;;;;;;OASG;IACH,aAAa,EAAE,iBAAiB,CAAC;IACjC,2DAA2D;IAC3D,SAAS,EAAE,SAAS,CAAC;IACrB,iEAAiE;IACjE,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;;;;;;;OAUG;IACH,yBAAyB,EAAE,eAAe,CAAC;IAC3C,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,sBAAsB,CAAC;IAC3C;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,iCAAiC,GACtC,iBAAiB,CAAC,4BAA4B,CAAC,CAkDjD;AACD,uDAAuD"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
2
3
|
import { WebsiteRuntimeContextProvider, } from '../core/contexts/WebsiteRuntimeContext.js';
|
|
3
4
|
import { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website.js';
|
|
4
5
|
import { WebsitePageLayout } from '../core/layouts/WebsitePageLayout.js';
|
|
@@ -11,7 +12,15 @@ import { WebsitePageLayout } from '../core/layouts/WebsitePageLayout.js';
|
|
|
11
12
|
*/
|
|
12
13
|
export function createWebsitePageBridgeIsland(deps) {
|
|
13
14
|
const { PageComponent, pageRef, navigation } = deps;
|
|
14
|
-
function WebsitePageBridgeIsland({ locale, defaultLocale, labelPack, designTokens, skipToMainContentLabelKey, frontendProviders,
|
|
15
|
+
function WebsitePageBridgeIsland({ locale, defaultLocale, labelPack, designTokens, skipToMainContentLabelKey, frontendProviders, apiOrigin, appOrigin, }) {
|
|
16
|
+
/*
|
|
17
|
+
* Both must be present: the factory is build-time wiring and `apiOrigin` is per-environment, and
|
|
18
|
+
* a page that declares no capture section legitimately has neither. Memoised on `apiOrigin` so a
|
|
19
|
+
* re-render cannot hand the provider a new identity and re-post a decision nobody retook.
|
|
20
|
+
*/
|
|
21
|
+
const consentRecorder = useMemo(() => (deps.consentRecorderFactory !== undefined && apiOrigin !== undefined
|
|
22
|
+
? deps.consentRecorderFactory(apiOrigin)
|
|
23
|
+
: undefined), [apiOrigin]);
|
|
15
24
|
const runtimeValue = {
|
|
16
25
|
locale,
|
|
17
26
|
defaultLocale,
|
|
@@ -21,12 +30,13 @@ export function createWebsitePageBridgeIsland(deps) {
|
|
|
21
30
|
skipToMainContentLabelKey,
|
|
22
31
|
frontendProviderRegistry: createFrontendWebsiteProviderRegistry({
|
|
23
32
|
entries: frontendProviders,
|
|
24
|
-
modules: frontendProviderModules,
|
|
33
|
+
modules: deps.frontendProviderModules,
|
|
25
34
|
}),
|
|
35
|
+
...(deps.consentBannerComponent !== undefined ? { consentBannerComponent: deps.consentBannerComponent } : {}),
|
|
26
36
|
apiOrigin,
|
|
27
37
|
appOrigin,
|
|
28
38
|
};
|
|
29
|
-
return (_jsx(WebsiteRuntimeContextProvider, { value: runtimeValue, children: _jsx(WebsitePageLayout, { pageRef: pageRef, children: _jsx(PageComponent, {}) }) }));
|
|
39
|
+
return (_jsx(WebsiteRuntimeContextProvider, { value: runtimeValue, children: _jsx(WebsitePageLayout, { pageRef: pageRef, ...(consentRecorder !== undefined ? { consentRecorder } : {}), children: _jsx(PageComponent, {}) }) }));
|
|
30
40
|
}
|
|
31
41
|
WebsitePageBridgeIsland.displayName = 'WebsitePageBridgeIsland';
|
|
32
42
|
return WebsitePageBridgeIsland;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-runtime.js","sourceRoot":"","sources":["../../../src/astro/bridge-runtime.tsx"],"names":[],"mappings":";AAKA,OAAO,EACL,6BAA6B,GAE9B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,qCAAqC,EAAE,MAAM,+DAA+D,CAAC;AAEtH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AA4PtE;;;;;;GAMG;AACH,MAAM,UAAU,6BAA6B,CAC3C,IAAuC;IAEvC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAEpD,SAAS,uBAAuB,CAAC,EAC/B,MAAM,EACN,aAAa,EACb,SAAS,EACT,YAAY,EACZ,yBAAyB,EACzB,iBAAiB,EACjB,uBAAuB,EACvB,SAAS,EACT,SAAS,GACoB;QAC7B,MAAM,YAAY,GAA+B;YAC/C,MAAM;YACN,aAAa;YACb,SAAS;YACT,YAAY;YACZ,UAAU;YACV,yBAAyB;YACzB,wBAAwB,EAAE,qCAAqC,CAAC;gBAC9D,OAAO,EAAE,iBAAiB;gBAC1B,OAAO,EAAE,uBAAuB;aACjC,CAAC;YACF,SAAS;YACT,SAAS;SACV,CAAC;QACF,OAAO,CACL,KAAC,6BAA6B,IAAC,KAAK,EAAE,YAAY,YAChD,KAAC,iBAAiB,IAAC,OAAO,EAAE,OAAO,YACjC,KAAC,aAAa,KAAG,GACC,GACU,CACjC,CAAC;IACJ,CAAC;IACD,uBAAuB,CAAC,WAAW,GAAG,yBAAyB,CAAC;IAEhE,OAAO,uBAAuB,CAAC;AACjC,CAAC","sourcesContent":["import type { ComponentType, FunctionComponent, ReactNode } from 'react';\n\nimport type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';\nimport type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport {\n WebsiteRuntimeContextProvider,\n type WebsiteRuntimeContextValue,\n} from '../core/contexts/WebsiteRuntimeContext';\nimport { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website';\nimport type { GeneratedWebsiteProviderModules } from '../core/external-providers/frontend-provider-registry.website';\nimport { WebsitePageLayout } from '../core/layouts/WebsitePageLayout';\nimport type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';\nimport type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';\nimport type { WebsiteNavigationConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport type { WebsitePageRef } from '../schemas/refs/page-ref.schemas';\nimport type { LabelPack } from '../schemas/validators/label-pack.validator';\n\n/**\n * @wildo_source:part:start saas.website.astro.bridge facet:layer:astro facet:family:website\n *\n * `createWebsitePageBridgeIsland(deps)` — factory that returns the\n * React island a consumer's per-page `.astro` file mounts with a\n * `client:idle` (or `client:load`) directive.\n *\n * **The hard architectural constraint this design solves**: Astro\n * serializes React-island props via `devalue`-style JSON to transfer\n * them across the SSR → client-hydration boundary. Function values —\n * including `ComponentType` references — are NOT serializable. Any\n * design that tried to pass `pageComponent`, `headerComponent`, or\n * `footerComponent` AS island props would render correctly server-side\n * (the function ref is in-process during SSR) and then mismatch on\n * first hydration (the client receives `undefined`/broken placeholders\n * for the function-typed fields). The Vitest suite would not catch\n * this because Vitest renders React in a single process — there is no\n * serialization boundary in unit tests.\n *\n * **The factory pattern fixes the constraint by closing over** the\n * `pageComponent`, `pageRef`, and `navigation` (header / footer\n * component refs) at MODULE LOAD TIME on the consumer side. The\n * returned island accepts only PRIMITIVE-VALUED props (`locale`,\n * `labelPack`, `designTokens`) — all JSON-serializable, all safe to\n * cross the hydration boundary. The component refs never appear in\n * the prop signature; they live in the lexical closure that the\n * client-side bundle includes naturally as part of the island's\n * compiled module.\n *\n * **Authoring pattern (per-page bridge module)**:\n *\n * ```tsx\n * // consumer's src/pages-impl/landing.bridge.tsx\n * import { createWebsitePageBridgeIsland } from '@wildo-ai/saas-website/astro-island';\n * import { LandingPage, LANDING_PAGE_MANIFEST } from './landing.page';\n * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';\n *\n * export const LandingBridgeIsland = createWebsitePageBridgeIsland({\n * PageComponent: LandingPage,\n * pageRef: LANDING_PAGE_MANIFEST.ref,\n * navigation: WEBSITE_ROOT_CONFIG.navigation,\n * });\n * ```\n *\n * ```astro\n * ---\n * // consumer's src/pages/index.astro\n * import {\n * loadWebsitePageRuntime,\n * renderWebsitePageHeadHtml,\n * renderWebsiteDesignTokensCss,\n * } from '@wildo-ai/saas-website/astro';\n * import { LandingBridgeIsland } from '../pages-impl/landing.bridge';\n * import { LANDING_PAGE_MANIFEST } from '../pages-impl/landing.page';\n * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';\n *\n * const runtime = loadWebsitePageRuntime({\n * manifest: LANDING_PAGE_MANIFEST,\n * rootConfig: WEBSITE_ROOT_CONFIG,\n * labelPacksDir: './src/i18n',\n * astroLocale: Astro.currentLocale,\n * });\n * const headHtml = renderWebsitePageHeadHtml({\n * manifest: LANDING_PAGE_MANIFEST,\n * rootConfig: WEBSITE_ROOT_CONFIG,\n * locale: runtime.locale,\n * labelPack: runtime.labelPack,\n * });\n * const designTokensCss = renderWebsiteDesignTokensCss(\n * WEBSITE_ROOT_CONFIG.designTokens,\n * );\n * ---\n * <!doctype html>\n * <html lang={runtime.locale}>\n * <head>\n * <Fragment set:html={headHtml} />\n * <style set:html={designTokensCss}></style>\n * </head>\n * <body>\n * <LandingBridgeIsland\n * client:idle\n * locale={runtime.locale}\n * labelPack={runtime.labelPack}\n * designTokens={WEBSITE_ROOT_CONFIG.designTokens}\n * />\n * </body>\n * </html>\n * ```\n *\n * **Why a per-page bridge file** (rather than constructing the island\n * inline in the consumer's `.astro` frontmatter):\n *\n * Astro's `client:*` directive requires the JSX tag to reference a\n * statically-resolvable module export at compile time — that is how\n * Astro knows which module to bundle for client-side hydration. A\n * factory call inside `.astro` frontmatter (`const Island =\n * createWebsitePageBridgeIsland({...}); <Island client:idle/>`) does\n * NOT round-trip through Astro's module-graph analyzer: Astro would\n * see a runtime-constructed component reference and fail to bundle\n * the closure for the client. The per-page bridge file gives Astro a\n * static export to bind the directive to. The cost is one ~6-line\n * file per page; the win is the only architecture that is safe\n * against the H-2 hydration-mismatch failure mode AND uses Astro's\n * native bundling pipeline.\n *\n * **Why `ComponentType` refs in `Deps` and not in `Props`**: this is\n * the entire point of the redesign. `Deps` is consumed at module load\n * time on the consumer side — the closure traps the function refs\n * inside the returned component. `Props` is consumed at island-mount\n * time and crosses Astro's serialization boundary. Keeping the two\n * concerns lexically separate makes the boundary visible in the type\n * signature.\n *\n * **Boundary** (D12 client-bundle discipline): this module ships into\n * the React-island bundle. It MUST stay clear of `node:*` imports,\n * fs / path helpers, and the SSR-tier specifiers\n * (`axios`, `jsonwebtoken`, …). The bundle-isolation test catches\n * regressions both globally (closure-wide allow-list) AND with the\n * targeted Phase-5 negative test that asserts `node:fs` / `node:path`\n * reach this island closure ONLY through `astro.ts`, never through\n * `index.ts` or `companion-exports.ts`.\n */\n\n/**\n * Module-load-time dependencies — closed over inside the returned\n * island. NEVER cross the hydration boundary as serialized props.\n */\nexport interface CreateWebsitePageBridgeIslandDeps {\n /**\n * The page's React component. Typically the same value that was\n * passed to `defineWebsitePageManifest({ pageComponent })` — the\n * authoring convention re-imports it here to keep the closure\n * explicit and the per-page bridge file self-contained.\n *\n * Typed as `ComponentType<any>` to mirror\n * `WebsitePageManifest.pageComponent` and dodge React's `ComponentType`\n * contravariance: a narrower `ComponentType<TProps>` returned by\n * `defineWebsitePageManifest<TProps>` would not be assignable to a\n * `ComponentType<unknown>` slot, blocking every per-page bridge\n * call site. The component is invoked with no props at the\n * `<PageComponent />` JSX call site below, so widening here is\n * runtime-safe.\n */\n PageComponent: ComponentType<any>;\n /**\n * The active page's ref. Carried as a primitive (string-branded)\n * value so the inner `WebsitePageLayout` can register the\n * `WebsitePageContext` without re-resolving it from the manifest.\n */\n pageRef: WebsitePageRef;\n /**\n * Site-wide navigation slot bindings (header / footer components).\n * Closed over here rather than passed as island props because both\n * fields are `ComponentType` refs that cannot survive Astro's\n * serialization boundary.\n */\n navigation: WebsiteNavigationConfig;\n}\n\n/**\n * Hydration-boundary props — what the consumer's `.astro` page passes\n * to `<LandingBridgeIsland client:idle ...{primitiveProps}/>`. Every\n * field is JSON-serializable.\n */\nexport interface WebsitePageBridgeIslandProps {\n /** The locale Astro resolved (with default-locale fallback). */\n locale: AvailableLanguage;\n /**\n * The site's default locale. Carried as an island prop (not closed\n * over in `Deps`) because it is JSON-primitive and consumers\n * already pass `runtime.locale` from the per-locale Astro page\n * frontmatter — passing the matching `runtime.defaultLocale` from\n * the same `loadWebsitePageRuntime(...)` return value keeps the\n * authoring story symmetric. Required for `WebsiteInternalLink`\n * (Phase 6 fix H-β) which needs both locales to compute the\n * correct prefix.\n */\n defaultLocale: AvailableLanguage;\n /** Already-loaded + already-shape-validated label pack. */\n labelPack: LabelPack;\n /** Design-token bundle from `WebsiteRootConfig.designTokens`. */\n designTokens: WebsiteDesignTokens;\n /**\n * Fully-composed label key the framework reads to render the\n * \"Skip to main content\" affordance (Phase 6 third audit fix M-α).\n * Mirrors `WebsiteRootConfig.skipToMainContentLabelKey`. Carried as\n * a JSON-serializable string prop (NOT closed over in `Deps`)\n * because it is a primitive value the consumer already has on the\n * root config in the `.astro` frontmatter — passing it as an island\n * prop keeps the per-page bridge module dependency-free of the\n * site root config (the bridge stays page-local; only the per-page\n * `.astro` page knows about `WEBSITE_ROOT_CONFIG`).\n */\n skipToMainContentLabelKey: WebsiteLabelKey;\n /** Optional website-facing public providers materialized at build time. */\n frontendProviders?: FrontendProvidersBlock;\n /**\n * The CODE channel for those providers (#332): the\n * `generatedFrontendProviderModules` export of this website service's own\n * `src/generated/frontend-providers.generated.ts`, written by\n * `wildo config sync`.\n *\n * `frontendProviders` above says WHICH providers this service turned on, in\n * this environment; this says what code the bundle actually contains for\n * them. Both are needed, because a JSON payload cannot carry an SDK loader\n * and a bundle cannot know which environment it is running in.\n *\n * NOT a JSON-serializable island prop like its siblings, and it does not need\n * to be: the bridge module is bundled with the page, so the generated module\n * map is an ordinary import in the same graph rather than something that has\n * to cross the hydration boundary.\n *\n * Omitted is the ordinary case for a website declaring no package-shipped\n * provider: no such file is generated, so there is nothing to import.\n */\n frontendProviderModules?: GeneratedWebsiteProviderModules;\n /**\n * Optional backend API origin (no trailing slash) forwarded into\n * `WebsiteRuntimeContext.apiOrigin` for anonymous-capture sections\n * (lead / draft-note forms) to construct their\n * `WebsiteAnonymousSessionClient`. A JSON-serializable string, so it\n * crosses the hydration boundary as an island prop; the `.astro` page\n * resolves the per-environment value from\n * `WildoMarketingWebsiteConfig.apiOrigin`. Omitted on pages with no\n * capture section.\n */\n apiOrigin?: string;\n /**\n * The product origin this site's self-serve CTAs link to (\"Start free\",\n * \"Get started\"), forwarded into `WebsiteRuntimeContext.appOrigin` (#493).\n *\n * An island prop for the same reason `apiOrigin` above is one: the `.astro`\n * page resolves it from `WildoMarketingWebsiteConfig.appOrigin`, which is\n * Node-loaded and invisible to a client island. Before this, a site\n * resolved the product origin a SECOND time from a public build-time\n * environment variable — one host authored twice with no mechanical link,\n * and a silent fallback to localhost when the variable was unset.\n *\n * Optional because a page with no CTA needs none; a page that renders one\n * without passing this should fail LOUDLY rather than fall back, which is\n * the consumer's job and the whole point of removing the fallback.\n */\n appOrigin?: string;\n}\n\n/**\n * Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function\n * component — Astro hydrates nothing else here — and the union is not callable, so the wider\n * declaration cost callers the ability to invoke the island directly. Mirrors\n * `createWebsiteBlogPostBridgeIsland`, whose suite depends on that call. `FunctionComponent` is\n * assignable to `ComponentType`, so no consumer narrows.\n */\nexport function createWebsitePageBridgeIsland(\n deps: CreateWebsitePageBridgeIslandDeps,\n): FunctionComponent<WebsitePageBridgeIslandProps> {\n const { PageComponent, pageRef, navigation } = deps;\n\n function WebsitePageBridgeIsland({\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n skipToMainContentLabelKey,\n frontendProviders,\n frontendProviderModules,\n apiOrigin,\n appOrigin,\n }: WebsitePageBridgeIslandProps): ReactNode {\n const runtimeValue: WebsiteRuntimeContextValue = {\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n navigation,\n skipToMainContentLabelKey,\n frontendProviderRegistry: createFrontendWebsiteProviderRegistry({\n entries: frontendProviders,\n modules: frontendProviderModules,\n }),\n apiOrigin,\n appOrigin,\n };\n return (\n <WebsiteRuntimeContextProvider value={runtimeValue}>\n <WebsitePageLayout pageRef={pageRef}>\n <PageComponent />\n </WebsitePageLayout>\n </WebsiteRuntimeContextProvider>\n );\n }\n WebsitePageBridgeIsland.displayName = 'WebsitePageBridgeIsland';\n\n return WebsitePageBridgeIsland;\n}\n/** @wildo_source:part:end saas.website.astro.bridge */\n"]}
|
|
1
|
+
{"version":3,"file":"bridge-runtime.js","sourceRoot":"","sources":["../../../../src/astro/bridge-runtime.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAMhC,OAAO,EACL,6BAA6B,GAE9B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,qCAAqC,EAAE,MAAM,+DAA+D,CAAC;AAEtH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAsQtE;;;;;;GAMG;AACH,MAAM,UAAU,6BAA6B,CAC3C,IAAuC;IAEvC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAEpD,SAAS,uBAAuB,CAAC,EAC/B,MAAM,EACN,aAAa,EACb,SAAS,EACT,YAAY,EACZ,yBAAyB,EACzB,iBAAiB,EACjB,SAAS,EACT,SAAS,GACoB;QAC7B;;;;WAIG;QACH,MAAM,eAAe,GAAG,OAAO,CAC7B,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,sBAAsB,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS;YACzE,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,SAAS,CAAC;YACxC,CAAC,CAAC,SAAS,CAAC,EACd,CAAC,SAAS,CAAC,CACZ,CAAC;QACF,MAAM,YAAY,GAA+B;YAC/C,MAAM;YACN,aAAa;YACb,SAAS;YACT,YAAY;YACZ,UAAU;YACV,yBAAyB;YACzB,wBAAwB,EAAE,qCAAqC,CAAC;gBAC9D,OAAO,EAAE,iBAAiB;gBAC1B,OAAO,EAAE,IAAI,CAAC,uBAAuB;aACtC,CAAC;YACF,GAAG,CAAC,IAAI,CAAC,sBAAsB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7G,SAAS;YACT,SAAS;SACV,CAAC;QACF,OAAO,CACL,KAAC,6BAA6B,IAAC,KAAK,EAAE,YAAY,YAChD,KAAC,iBAAiB,IAAC,OAAO,EAAE,OAAO,KAAM,CAAC,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,YACjG,KAAC,aAAa,KAAG,GACC,GACU,CACjC,CAAC;IACJ,CAAC;IACD,uBAAuB,CAAC,WAAW,GAAG,yBAAyB,CAAC;IAEhE,OAAO,uBAAuB,CAAC;AACjC,CAAC","sourcesContent":["import { useMemo } from 'react';\nimport type { ComponentType, FunctionComponent, ReactNode } from 'react';\n\nimport type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';\nimport type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport {\n WebsiteRuntimeContextProvider,\n type WebsiteRuntimeContextValue,\n} from '../core/contexts/WebsiteRuntimeContext';\nimport { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website';\nimport type { GeneratedWebsiteProviderModules } from '../core/external-providers/frontend-provider-registry.website';\nimport { WebsitePageLayout } from '../core/layouts/WebsitePageLayout';\nimport type { WebsiteConsentRecorder } from '../core/consent/WebsiteConsentContext';\nimport type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';\nimport type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';\nimport type { WebsiteNavigationConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport type { WebsitePageRef } from '../schemas/refs/page-ref.schemas';\nimport type { LabelPack } from '../schemas/validators/label-pack.validator';\n\n/**\n * @wildo_source:part:start saas.website.astro.bridge facet:layer:astro facet:family:website\n *\n * `createWebsitePageBridgeIsland(deps)` — factory that returns the\n * React island a consumer's per-page `.astro` file mounts with a\n * `client:idle` (or `client:load`) directive.\n *\n * **The hard architectural constraint this design solves**: Astro\n * serializes React-island props via `devalue`-style JSON to transfer\n * them across the SSR → client-hydration boundary. Function values —\n * including `ComponentType` references — are NOT serializable. Any\n * design that tried to pass `pageComponent`, `headerComponent`, or\n * `footerComponent` AS island props would render correctly server-side\n * (the function ref is in-process during SSR) and then mismatch on\n * first hydration (the client receives `undefined`/broken placeholders\n * for the function-typed fields). The Vitest suite would not catch\n * this because Vitest renders React in a single process — there is no\n * serialization boundary in unit tests.\n *\n * **The factory pattern fixes the constraint by closing over** the\n * `pageComponent`, `pageRef`, and `navigation` (header / footer\n * component refs) at MODULE LOAD TIME on the consumer side. The\n * returned island accepts only PRIMITIVE-VALUED props (`locale`,\n * `labelPack`, `designTokens`) — all JSON-serializable, all safe to\n * cross the hydration boundary. The component refs never appear in\n * the prop signature; they live in the lexical closure that the\n * client-side bundle includes naturally as part of the island's\n * compiled module.\n *\n * **Authoring pattern (per-page bridge module)**:\n *\n * ```tsx\n * // consumer's src/pages-impl/landing.bridge.tsx\n * import { createWebsitePageBridgeIsland } from '@wildo-ai/saas-website/astro-island';\n * import { LandingPage, LANDING_PAGE_MANIFEST } from './landing.page';\n * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';\n *\n * export const LandingBridgeIsland = createWebsitePageBridgeIsland({\n * PageComponent: LandingPage,\n * pageRef: LANDING_PAGE_MANIFEST.ref,\n * navigation: WEBSITE_ROOT_CONFIG.navigation,\n * });\n * ```\n *\n * ```astro\n * ---\n * // consumer's src/pages/index.astro\n * import {\n * loadWebsitePageRuntime,\n * renderWebsitePageHeadHtml,\n * renderWebsiteDesignTokensCss,\n * } from '@wildo-ai/saas-website/astro';\n * import { LandingBridgeIsland } from '../pages-impl/landing.bridge';\n * import { LANDING_PAGE_MANIFEST } from '../pages-impl/landing.page';\n * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';\n *\n * const runtime = loadWebsitePageRuntime({\n * manifest: LANDING_PAGE_MANIFEST,\n * rootConfig: WEBSITE_ROOT_CONFIG,\n * labelPacksDir: './src/i18n',\n * astroLocale: Astro.currentLocale,\n * });\n * const headHtml = renderWebsitePageHeadHtml({\n * manifest: LANDING_PAGE_MANIFEST,\n * rootConfig: WEBSITE_ROOT_CONFIG,\n * locale: runtime.locale,\n * labelPack: runtime.labelPack,\n * });\n * const designTokensCss = renderWebsiteDesignTokensCss(\n * WEBSITE_ROOT_CONFIG.designTokens,\n * );\n * ---\n * <!doctype html>\n * <html lang={runtime.locale}>\n * <head>\n * <Fragment set:html={headHtml} />\n * <style set:html={designTokensCss}></style>\n * </head>\n * <body>\n * <LandingBridgeIsland\n * client:idle\n * locale={runtime.locale}\n * labelPack={runtime.labelPack}\n * designTokens={WEBSITE_ROOT_CONFIG.designTokens}\n * />\n * </body>\n * </html>\n * ```\n *\n * **Why a per-page bridge file** (rather than constructing the island\n * inline in the consumer's `.astro` frontmatter):\n *\n * Astro's `client:*` directive requires the JSX tag to reference a\n * statically-resolvable module export at compile time — that is how\n * Astro knows which module to bundle for client-side hydration. A\n * factory call inside `.astro` frontmatter (`const Island =\n * createWebsitePageBridgeIsland({...}); <Island client:idle/>`) does\n * NOT round-trip through Astro's module-graph analyzer: Astro would\n * see a runtime-constructed component reference and fail to bundle\n * the closure for the client. The per-page bridge file gives Astro a\n * static export to bind the directive to. The cost is one ~6-line\n * file per page; the win is the only architecture that is safe\n * against the H-2 hydration-mismatch failure mode AND uses Astro's\n * native bundling pipeline.\n *\n * **Why `ComponentType` refs in `Deps` and not in `Props`**: this is\n * the entire point of the redesign. `Deps` is consumed at module load\n * time on the consumer side — the closure traps the function refs\n * inside the returned component. `Props` is consumed at island-mount\n * time and crosses Astro's serialization boundary. Keeping the two\n * concerns lexically separate makes the boundary visible in the type\n * signature.\n *\n * **Boundary** (D12 client-bundle discipline): this module ships into\n * the React-island bundle. It MUST stay clear of `node:*` imports,\n * fs / path helpers, and the SSR-tier specifiers\n * (`axios`, `jsonwebtoken`, …). The bundle-isolation test catches\n * regressions both globally (closure-wide allow-list) AND with the\n * targeted Phase-5 negative test that asserts `node:fs` / `node:path`\n * reach this island closure ONLY through `astro.ts`, never through\n * `index.ts` or `companion-exports.ts`.\n */\n\n/**\n * Module-load-time dependencies — closed over inside the returned\n * island. NEVER cross the hydration boundary as serialized props.\n */\nexport interface CreateWebsitePageBridgeIslandDeps {\n /**\n * Browser CODE imported by the consumer bridge, closed over at module load.\n * Never pass executable modules through Astro island props: those are serialized.\n * Config sync emits a stable map, including an empty map when no providers are declared.\n */\n frontendProviderModules?: GeneratedWebsiteProviderModules;\n /**\n * The page's React component. Typically the same value that was\n * passed to `defineWebsitePageManifest({ pageComponent })` — the\n * authoring convention re-imports it here to keep the closure\n * explicit and the per-page bridge file self-contained.\n *\n * Typed as `ComponentType<any>` to mirror\n * `WebsitePageManifest.pageComponent` and dodge React's `ComponentType`\n * contravariance: a narrower `ComponentType<TProps>` returned by\n * `defineWebsitePageManifest<TProps>` would not be assignable to a\n * `ComponentType<unknown>` slot, blocking every per-page bridge\n * call site. The component is invoked with no props at the\n * `<PageComponent />` JSX call site below, so widening here is\n * runtime-safe.\n */\n PageComponent: ComponentType<any>;\n /**\n * Builds the recorder that persists a VISITOR's consent decision server-side, from the island's\n * runtime `apiOrigin` (#1258).\n *\n * A DEPS entry rather than an island prop, for the reason this whole interface exists: it is a\n * function, and island props are serialized. A factory rather than an instance, because the\n * `apiOrigin` it needs is resolved per environment by the `.astro` page and only reaches the island\n * as a prop.\n *\n * The engine cannot build this itself — a client needs the application's own\n * `frontendServiceName`, which the engine has no way to know. The application already constructs\n * one for its capture forms; passing THAT factory here is what makes a visitor's consent decision,\n * their captured lead and their draft note share a single `uuidRef` and transpose together.\n *\n * Optional. Omitted, a decision lives in the visitor's browser and nowhere else — the behaviour\n * before this existed, and correct for a site with no anonymous-session wiring.\n */\n consentRecorderFactory?: (apiOrigin: string) => WebsiteConsentRecorder;\n /**\n * The active page's ref. Carried as a primitive (string-branded)\n * value so the inner `WebsitePageLayout` can register the\n * `WebsitePageContext` without re-resolving it from the manifest.\n */\n pageRef: WebsitePageRef;\n /**\n * Site-wide navigation slot bindings (header / footer components).\n * Closed over here rather than passed as island props because both\n * fields are `ComponentType` refs that cannot survive Astro's\n * serialization boundary.\n */\n navigation: WebsiteNavigationConfig;\n /**\n * Optional replacement for the framework's visitor consent panel (#534). Closed over for the same\n * reason as `navigation`. See `WebsiteRuntimeContextValue.consentBannerComponent`.\n */\n consentBannerComponent?: ComponentType;\n}\n\n/**\n * Hydration-boundary props — what the consumer's `.astro` page passes\n * to `<LandingBridgeIsland client:idle ...{primitiveProps}/>`. Every\n * field is JSON-serializable.\n */\nexport interface WebsitePageBridgeIslandProps {\n /** The locale Astro resolved (with default-locale fallback). */\n locale: AvailableLanguage;\n /**\n * The site's default locale. Carried as an island prop (not closed\n * over in `Deps`) because it is JSON-primitive and consumers\n * already pass `runtime.locale` from the per-locale Astro page\n * frontmatter — passing the matching `runtime.defaultLocale` from\n * the same `loadWebsitePageRuntime(...)` return value keeps the\n * authoring story symmetric. Required for `WebsiteInternalLink`\n * (Phase 6 fix H-β) which needs both locales to compute the\n * correct prefix.\n */\n defaultLocale: AvailableLanguage;\n /** Already-loaded + already-shape-validated label pack. */\n labelPack: LabelPack;\n /** Design-token bundle from `WebsiteRootConfig.designTokens`. */\n designTokens: WebsiteDesignTokens;\n /**\n * Fully-composed label key the framework reads to render the\n * \"Skip to main content\" affordance (Phase 6 third audit fix M-α).\n * Mirrors `WebsiteRootConfig.skipToMainContentLabelKey`. Carried as\n * a JSON-serializable string prop (NOT closed over in `Deps`)\n * because it is a primitive value the consumer already has on the\n * root config in the `.astro` frontmatter — passing it as an island\n * prop keeps the per-page bridge module dependency-free of the\n * site root config (the bridge stays page-local; only the per-page\n * `.astro` page knows about `WEBSITE_ROOT_CONFIG`).\n */\n skipToMainContentLabelKey: WebsiteLabelKey;\n /** Optional website-facing public providers materialized at build time. */\n frontendProviders?: FrontendProvidersBlock;\n /**\n * Optional backend API origin (no trailing slash) forwarded into\n * `WebsiteRuntimeContext.apiOrigin` for anonymous-capture sections\n * (lead / draft-note forms) to construct their\n * `WebsiteAnonymousSessionClient`. A JSON-serializable string, so it\n * crosses the hydration boundary as an island prop; the `.astro` page\n * resolves the per-environment value from\n * `WildoMarketingWebsiteConfig.apiOrigin`. Omitted on pages with no\n * capture section.\n */\n apiOrigin?: string;\n /**\n * The product origin this site's self-serve CTAs link to (\"Start free\",\n * \"Get started\"), forwarded into `WebsiteRuntimeContext.appOrigin` (#493).\n *\n * An island prop for the same reason `apiOrigin` above is one: the `.astro`\n * page resolves it from `WildoMarketingWebsiteConfig.appOrigin`, which is\n * Node-loaded and invisible to a client island. Before this, a site\n * resolved the product origin a SECOND time from a public build-time\n * environment variable — one host authored twice with no mechanical link,\n * and a silent fallback to localhost when the variable was unset.\n *\n * Optional because a page with no CTA needs none; a page that renders one\n * without passing this should fail LOUDLY rather than fall back, which is\n * the consumer's job and the whole point of removing the fallback.\n */\n appOrigin?: string;\n}\n\n/**\n * Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function\n * component — Astro hydrates nothing else here — and the union is not callable, so the wider\n * declaration cost callers the ability to invoke the island directly. Mirrors\n * `createWebsiteBlogPostBridgeIsland`, whose suite depends on that call. `FunctionComponent` is\n * assignable to `ComponentType`, so no consumer narrows.\n */\nexport function createWebsitePageBridgeIsland(\n deps: CreateWebsitePageBridgeIslandDeps,\n): FunctionComponent<WebsitePageBridgeIslandProps> {\n const { PageComponent, pageRef, navigation } = deps;\n\n function WebsitePageBridgeIsland({\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n skipToMainContentLabelKey,\n frontendProviders,\n apiOrigin,\n appOrigin,\n }: WebsitePageBridgeIslandProps): ReactNode {\n /*\n * Both must be present: the factory is build-time wiring and `apiOrigin` is per-environment, and\n * a page that declares no capture section legitimately has neither. Memoised on `apiOrigin` so a\n * re-render cannot hand the provider a new identity and re-post a decision nobody retook.\n */\n const consentRecorder = useMemo(\n () => (deps.consentRecorderFactory !== undefined && apiOrigin !== undefined\n ? deps.consentRecorderFactory(apiOrigin)\n : undefined),\n [apiOrigin],\n );\n const runtimeValue: WebsiteRuntimeContextValue = {\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n navigation,\n skipToMainContentLabelKey,\n frontendProviderRegistry: createFrontendWebsiteProviderRegistry({\n entries: frontendProviders,\n modules: deps.frontendProviderModules,\n }),\n ...(deps.consentBannerComponent !== undefined ? { consentBannerComponent: deps.consentBannerComponent } : {}),\n apiOrigin,\n appOrigin,\n };\n return (\n <WebsiteRuntimeContextProvider value={runtimeValue}>\n <WebsitePageLayout pageRef={pageRef} {...(consentRecorder !== undefined ? { consentRecorder } : {})}>\n <PageComponent />\n </WebsitePageLayout>\n </WebsiteRuntimeContextProvider>\n );\n }\n WebsitePageBridgeIsland.displayName = 'WebsitePageBridgeIsland';\n\n return WebsitePageBridgeIsland;\n}\n/** @wildo_source:part:end saas.website.astro.bridge */\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collect-expected-label-keys.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"collect-expected-label-keys.d.ts","sourceRoot":"","sources":["../../../../src/astro/collect-expected-label-keys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+DAA+D,CAAC;AAG9G,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,4CAA4C,CAAC;AAEhG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,wBAAgB,oCAAoC,CAClD,QAAQ,EAAE,SAAS,wBAAwB,EAAE,GAC5C,6BAA6B,CAsB/B;AAuBD,oEAAoE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers-renderer.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"headers-renderer.d.ts","sourceRoot":"","sources":["../../../../src/astro/headers-renderer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAIjE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,kBAAkB,GAAG,MAAM,CAapF;AACD,iEAAiE;AAEjE;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE;IAChD,WAAW,EAAE,kBAAkB,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,MAAM,CAgBT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n-routing.helper.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"i18n-routing.helper.d.ts","sourceRoot":"","sources":["../../../../src/astro/i18n-routing.helper.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AACH,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;IACtB,yDAAyD;IACzD,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE;QACP;;;;;;;WAOG;QACH,mBAAmB,EAAE,OAAO,CAAC;QAC7B;;;;WAIG;QACH,uBAAuB,EAAE,OAAO,CAAC;KAClC,CAAC;CACH;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,kBAAkB,GAAG,eAAe,CAsBrF;AACD,4DAA4D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"label-pack-loader.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"label-pack-loader.d.ts","sourceRoot":"","sources":["../../../../src/astro/label-pack-loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAM9E,OAAO,EAEL,KAAK,6BAA6B,EAClC,KAAK,SAAS,EACd,KAAK,yBAAyB,EAC/B,MAAM,4CAA4C,CAAC;AAEpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,SAAS,CA4BpF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,kBAAkB,GAC9B,GAAG,CAAC,iBAAiB,EAAE,SAAS,CAAC,CAMnC;AAED;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,MAAM,EAAE,yBAAyB,CAAC;CACnC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,kBAAkB,EAC/B,wBAAwB,EAAE,6BAA6B,GACtD,SAAS,yBAAyB,EAAE,CAmBtC;AAmCD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,kBAAkB,EAC/B,wBAAwB,EAAE,6BAA6B,GACtD,IAAI,CA6DN;AA+ED,6DAA6D"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import { WEBSITE_CONSENT_LABEL_KEYS } from '../schemas/label-keys/website-consent-label-keys.schemas.js';
|
|
3
4
|
import { validateLabelPackAgainstManifest, } from '../schemas/validators/label-pack.validator.js';
|
|
4
5
|
/**
|
|
5
6
|
* @wildo_source:part:start saas.website.astro.label-loader facet:layer:astro facet:family:website
|
|
@@ -158,6 +159,10 @@ export function loadAndValidateAllLabelPacks(packsDir, siteContext, expectedKeys
|
|
|
158
159
|
function composeEffectiveChromeLabelKeys(rootConfig) {
|
|
159
160
|
const set = new Set(rootConfig.chromeLabelKeys);
|
|
160
161
|
set.add(rootConfig.skipToMainContentLabelKey);
|
|
162
|
+
// The visitor consent panel is framework-owned in the same way (#534): required in every pack, so a site
|
|
163
|
+
// that turns on analytics can never ship it with an unlabelled way to refuse.
|
|
164
|
+
for (const key of WEBSITE_CONSENT_LABEL_KEYS)
|
|
165
|
+
set.add(key);
|
|
161
166
|
return Array.from(set);
|
|
162
167
|
}
|
|
163
168
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"label-pack-loader.js","sourceRoot":"","sources":["../../../../src/astro/label-pack-loader.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';\nimport type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';\nimport type { WebsiteRootConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport {\n validateLabelPackAgainstManifest,\n type ExpectedLabelKeysBySectionRef,\n type LabelPack,\n type LabelPackValidationResult,\n} from '../schemas/validators/label-pack.validator';\n\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.label-loader facet:layer:astro facet:family:website\n *\n * `label-pack-loader.ts` — synchronous, fs-backed loader for\n * marketing-site label packs. Consumed by:\n * - the Astro adapter (`WebsitePageBridge.astro` reads the active\n * locale's pack at request time during SSR / pre-render),\n * - the `wildo website audit` CLI (Phase 7) to validate every\n * locale pack against every manifest at CI time,\n * - any build-time hook that wants to fail fast on a missing or\n * malformed pack.\n *\n * **Convention** (matches the slice plan + the validator's contract):\n *\n * - One file per locale: `<packsDir>/<locale>.json`\n * - JSON object: flat key-value map, `Record<string, string>`\n * - Keys are fully-composed `WebsiteLabelKey`s (`website.<page>.<section>.<slot>`)\n * - Values are the localized strings\n *\n * **Why synchronous I/O**:\n * - The Astro frontmatter executes at build / SSR time in a Node\n * process (no event loop pressure to relieve), and the bridge\n * wants the pack resolved before it emits any HTML — async would\n * force the consumer to `await` inside an Astro frontmatter,\n * which is supported but adds a needless yield-point on a single\n * small JSON file.\n * - The validator is pure and synchronous; making the loader async\n * would add a coloring boundary for no benefit.\n *\n * **Failure modes — all surfaced as thrown errors with actionable\n * messages**:\n * - File missing — wrapped in a `[label-pack-loader] Could not read\n * pack at <path>` error so a typo in the dir / locale is obvious.\n * - JSON parse failure — error includes the file path and the\n * parser's own message.\n * - Top-level shape is not an object, or a value is not a string —\n * error names the offending key + observed type.\n *\n * **Why three exports** (loader, multi-loader, validator-runner):\n * - `loadLabelPack(packsDir, locale)` is the atomic primitive\n * (one file → one validated `LabelPack`). Used by the bridge at\n * request time.\n * - `loadAllLabelPacks(packsDir, siteContext)` is the build-time\n * batch — every locale pack at once. Used by the CLI auditor\n * and by the build-start hook.\n * - `validateAllLabelPacks(packsDir, siteContext, expectedKeysBySectionRef)`\n * is the highest-level entry point: load every pack, run the\n * validator on each, and either return the diagnostic results\n * or throw on the first hard error. The returned shape carries\n * per-locale diagnostics so the caller can format a CLI report.\n *\n * **`siteContext` parameter** — the consumer's composed\n * `WebsiteSiteContext` (see `website-site-context.ts`). The loader\n * reads `siteContext.websiteConfig.locales` for \"which locales does\n * the site ship?\" and `siteContext.rootConfig.{pageManifests,\n * chromeLabelKeys, skipToMainContentLabelKey}` for the manifest\n * registry / chrome-key set the validator runs against.\n */\n\n/**\n * Load and validate a single locale's label pack. Synchronous —\n * suitable for Astro frontmatter execution.\n *\n * @param packsDir Absolute or cwd-relative path to the directory\n * containing per-locale `<locale>.json` files.\n * @param locale The locale to load. Must be a member of\n * `WildoMarketingWebsiteConfig.locales`.\n * @returns A flat `LabelPack` (`Record<string, string>`) ready to\n * feed into `WebsiteRuntimeContextValue.labelPack`.\n * @throws When the file is missing, malformed JSON, not an object,\n * or contains a non-string value.\n */\nexport function loadLabelPack(packsDir: string, locale: AvailableLanguage): LabelPack {\n const filePath = resolve(packsDir, `${locale}.json`);\n\n let raw: string;\n try {\n raw = readFileSync(filePath, 'utf-8');\n } catch (cause) {\n throw new Error(\n `[label-pack-loader] Could not read label pack at ${filePath} ` +\n `(locale=\"${locale}\"). ` +\n `Make sure the directory exists and contains a \"${locale}.json\" file. ` +\n `Underlying error: ${(cause as Error).message}`,\n { cause },\n );\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (cause) {\n throw new Error(\n `[label-pack-loader] Failed to parse JSON in ${filePath} ` +\n `(locale=\"${locale}\"). Underlying parser error: ${(cause as Error).message}`,\n { cause },\n );\n }\n\n return assertLabelPackShape(parsed, filePath, locale);\n}\n\n/**\n * Batch loader — every locale declared on the site at once. The map\n * iteration order matches the declaration order of\n * `siteContext.websiteConfig.locales` so CLI reports come out in a\n * deterministic, authored sequence.\n */\nexport function loadAllLabelPacks(\n packsDir: string,\n siteContext: WebsiteSiteContext,\n): Map<AvailableLanguage, LabelPack> {\n const packs = new Map<AvailableLanguage, LabelPack>();\n for (const entry of siteContext.websiteConfig.locales) {\n packs.set(entry.locale, loadLabelPack(packsDir, entry.locale));\n }\n return packs;\n}\n\n/**\n * Per-locale validation result — the validator's flat output extended\n * with the locale tag so a multi-locale report can flatten + group.\n */\nexport interface LocaleLabelPackValidation {\n locale: AvailableLanguage;\n result: LabelPackValidationResult;\n}\n\n/**\n * Highest-level entry point: load every locale's pack and run the\n * label-pack validator against the manifest registry.\n *\n * **Return shape decision** — this function does NOT throw on\n * `missing` keys; it returns the per-locale results so the caller\n * (the build-start hook OR the CLI auditor OR a debugging\n * subprocess) can decide how to surface the failure (process exit\n * code, formatted CLI output, JSON report, …). A thin `throwIfAny…`\n * helper sits next to it for the common \"fail the build on first\n * missing key\" path.\n */\nexport function loadAndValidateAllLabelPacks(\n packsDir: string,\n siteContext: WebsiteSiteContext,\n expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,\n): readonly LocaleLabelPackValidation[] {\n const { rootConfig } = siteContext;\n const packs = loadAllLabelPacks(packsDir, siteContext);\n const manifests: readonly WebsitePageManifest[] = rootConfig.pageManifests;\n const effectiveChromeKeys = composeEffectiveChromeLabelKeys(rootConfig);\n\n const validations: LocaleLabelPackValidation[] = [];\n for (const [locale, pack] of packs) {\n validations.push({\n locale,\n result: validateLabelPackAgainstManifest(\n manifests,\n expectedKeysBySectionRef,\n pack,\n effectiveChromeKeys,\n ),\n });\n }\n return validations;\n}\n\n/**\n * Compose the chrome-label-key set the validator should treat as\n * \"expected\" — `WebsiteRootConfig.chromeLabelKeys` (consumer-authored)\n * UNION the framework-required `skipToMainContentLabelKey`.\n *\n * Centralized here because:\n * - Both `loadAndValidateAllLabelPacks` (used at build time) and\n * `buildKeyOriginMap` (used to annotate missing-key errors) need\n * the same composed list, and forking the union into two places\n * would be a drift hazard.\n * - The framework owns the skip-link key (it is rendered by\n * `WebsitePageLayout`, not the consumer's chrome), so the\n * consumer should NOT have to remember to also list it in\n * `chromeLabelKeys`. Auto-injection here keeps the contract\n * minimal at the consumer's call site (just declare the key in\n * `WebsiteRootConfig.skipToMainContentLabelKey` once + ship the\n * translation in every locale pack) while still surfacing the key\n * through the validator's \"missing\"/\"orphaned\" diagnostics.\n * - De-duplication is via `Set` semantics so a consumer who\n * defensively lists the key in both places (no-op but harmless)\n * does not produce a duplicate-key warning downstream.\n */\nfunction composeEffectiveChromeLabelKeys(\n rootConfig: WebsiteRootConfig,\n): readonly WebsiteLabelKey[] {\n const set = new Set<WebsiteLabelKey>(rootConfig.chromeLabelKeys);\n set.add(rootConfig.skipToMainContentLabelKey);\n return Array.from(set);\n}\n\n/**\n * Convenience wrapper for the build-start hook: validates every\n * declared locale's pack and throws a SINGLE aggregated error if any\n * locale has a hard failure (`missing` keys or `unmappedSectionRefs`).\n * Orphaned keys are logged via `console.warn` (soft warning per the\n * validator's contract) but do NOT throw.\n *\n * **Why aggregate (not first-fail)**: a multi-locale marketing site\n * with `en` and `fr` packs both missing keys would, under a\n * first-fail policy, force the author to fix `en`, re-run the build,\n * THEN see `fr`'s problem. Aggregating means one build run surfaces\n * every locale's hard errors together — material UX win for the\n * authoring loop. The aggregated message keeps each locale's failure\n * in its own block so the diagnostic stays scannable.\n *\n * **Why throw vs. return**: the build-start hook lives in\n * `astro.config.mjs` (or a `vite-plugin`-style integration) and\n * benefits from the shortest possible code path — a thrown error\n * naturally aborts `astro build` with a non-zero exit code and\n * surfaces the message in the build log. The richer\n * `loadAndValidateAllLabelPacks` is what `wildo website audit` calls\n * when it wants programmatic access to the per-locale results.\n *\n * **Key-origin annotation**: every `missing` key is tagged with the\n * page (and, if section-scoped, the section) that declared it, so the\n * author can grep + open the right `defineWebsiteSection` /\n * `defineWebsitePageManifest` call site without leaving the build log.\n */\nexport function assertLabelPacksValid(\n packsDir: string,\n siteContext: WebsiteSiteContext,\n expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,\n): void {\n const { rootConfig } = siteContext;\n const validations = loadAndValidateAllLabelPacks(\n packsDir,\n siteContext,\n expectedKeysBySectionRef,\n );\n\n /**\n * Build a key → origin map ONCE per assert call (cost is bounded by\n * the total number of declared keys across the whole site, which is\n * typically small for a marketing surface). Enriches the missing-key\n * error with the page/section that committed to each key — the\n * author can grep the ref to find the site authoring call site.\n */\n const keyOrigins = buildKeyOriginMap(\n rootConfig.pageManifests,\n expectedKeysBySectionRef,\n composeEffectiveChromeLabelKeys(rootConfig),\n );\n\n const localeErrorBlocks: string[] = [];\n\n for (const { locale, result } of validations) {\n const blocks: string[] = [];\n if (result.unmappedSectionRefs.length > 0) {\n blocks.push(\n ` unmappedSectionRefs (build-pipeline bug — sections referenced by a ` +\n `manifest but missing from the expected-keys collector): ` +\n `[${(result.unmappedSectionRefs as unknown as readonly string[]).join(', ')}]. ` +\n `Add every referenced section to the array passed to ` +\n `collectExpectedLabelKeysFromSections().`,\n );\n }\n if (result.missing.length > 0) {\n const annotated = (result.missing as unknown as readonly string[]).map(\n (key) => ` \"${key}\" ← ${keyOrigins.get(key) ?? 'unknown origin'}`,\n );\n blocks.push(\n ` missing ${result.missing.length} required key(s) in ${packsDir}/${locale}.json:\\n` +\n annotated.join('\\n'),\n );\n }\n if (blocks.length > 0) {\n localeErrorBlocks.push(`Locale \"${locale}\":\\n${blocks.join('\\n')}`);\n }\n if (result.orphaned.length > 0) {\n console.warn(\n `[label-pack-loader] Locale \"${locale}\": ${result.orphaned.length} ` +\n `orphaned key(s) in ${locale}.json (declared in pack but not consumed ` +\n `by any section or page manifest):\\n ${result.orphaned.join('\\n ')}`,\n );\n }\n }\n\n if (localeErrorBlocks.length > 0) {\n throw new Error(\n `[label-pack-loader] Label pack validation failed for ` +\n `${localeErrorBlocks.length} locale(s):\\n\\n${localeErrorBlocks.join('\\n\\n')}`,\n );\n }\n}\n\n/**\n * Build a `keyString -> \"page <pageRef> meta titleKey\" | \"section <sectionRef> in page <pageRef>\"`\n * lookup so the assert message can name the call site that committed\n * to each missing key. Pure and small (cost is O(totalKeys) per build\n * — bounded by the marketing surface).\n */\nfunction buildKeyOriginMap(\n manifests: readonly WebsitePageManifest[],\n expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,\n chromeLabelKeys: readonly WebsiteLabelKey[],\n): Map<string, string> {\n const origins = new Map<string, string>();\n for (const manifest of manifests) {\n const pageRefStr = manifest.ref as unknown as string;\n const titleKey = manifest.metaLabelKeys.titleKey as unknown as string;\n const descriptionKey = manifest.metaLabelKeys.descriptionKey as unknown as string;\n if (!origins.has(titleKey)) {\n origins.set(titleKey, `page \"${pageRefStr}\" metaLabelKeys.titleKey`);\n }\n if (!origins.has(descriptionKey)) {\n origins.set(descriptionKey, `page \"${pageRefStr}\" metaLabelKeys.descriptionKey`);\n }\n for (const sectionRef of manifest.sectionRefs) {\n const sectionRefStr = sectionRef as unknown as string;\n const sectionKeys = expectedKeysBySectionRef.get(sectionRef);\n if (sectionKeys === undefined) continue;\n for (const sectionKey of sectionKeys) {\n const keyStr = sectionKey as unknown as string;\n if (!origins.has(keyStr)) {\n origins.set(keyStr, `section \"${sectionRefStr}\" in page \"${pageRefStr}\"`);\n }\n }\n }\n }\n for (const chromeKey of chromeLabelKeys) {\n const keyStr = chromeKey as unknown as string;\n if (!origins.has(keyStr)) {\n origins.set(keyStr, `WebsiteRootConfig.chromeLabelKeys`);\n }\n }\n return origins;\n}\n\n/**\n * Defensive shape guard. Surface the offending key + observed type\n * so the consumer can find the malformed entry quickly. Pure\n * validation — no transformation; the returned value is the same\n * object reference passed in (cast to `LabelPack`) so the caller\n * does not pay an unnecessary copy.\n */\nfunction assertLabelPackShape(\n parsed: unknown,\n filePath: string,\n locale: AvailableLanguage,\n): LabelPack {\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(\n `[label-pack-loader] ${filePath} (locale=\"${locale}\") must contain a ` +\n `JSON object at the top level (got ${describeJson(parsed)}).`,\n );\n }\n for (const [key, value] of Object.entries(parsed)) {\n if (typeof value !== 'string') {\n throw new Error(\n `[label-pack-loader] ${filePath} (locale=\"${locale}\"): key \"${key}\" ` +\n `must map to a string (got ${describeJson(value)}).`,\n );\n }\n }\n return parsed as LabelPack;\n}\n\nfunction describeJson(value: unknown): string {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'array';\n return typeof value;\n}\n/** @wildo_source:part:end saas.website.astro.label-loader */\n"]}
|
|
1
|
+
{"version":3,"file":"label-pack-loader.js","sourceRoot":"","sources":["../../../../src/astro/label-pack-loader.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';\nimport { WEBSITE_CONSENT_LABEL_KEYS } from '../schemas/label-keys/website-consent-label-keys.schemas';\nimport type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';\nimport type { WebsiteRootConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport {\n validateLabelPackAgainstManifest,\n type ExpectedLabelKeysBySectionRef,\n type LabelPack,\n type LabelPackValidationResult,\n} from '../schemas/validators/label-pack.validator';\n\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.label-loader facet:layer:astro facet:family:website\n *\n * `label-pack-loader.ts` — synchronous, fs-backed loader for\n * marketing-site label packs. Consumed by:\n * - the Astro adapter (`WebsitePageBridge.astro` reads the active\n * locale's pack at request time during SSR / pre-render),\n * - the `wildo website audit` CLI (Phase 7) to validate every\n * locale pack against every manifest at CI time,\n * - any build-time hook that wants to fail fast on a missing or\n * malformed pack.\n *\n * **Convention** (matches the slice plan + the validator's contract):\n *\n * - One file per locale: `<packsDir>/<locale>.json`\n * - JSON object: flat key-value map, `Record<string, string>`\n * - Keys are fully-composed `WebsiteLabelKey`s (`website.<page>.<section>.<slot>`)\n * - Values are the localized strings\n *\n * **Why synchronous I/O**:\n * - The Astro frontmatter executes at build / SSR time in a Node\n * process (no event loop pressure to relieve), and the bridge\n * wants the pack resolved before it emits any HTML — async would\n * force the consumer to `await` inside an Astro frontmatter,\n * which is supported but adds a needless yield-point on a single\n * small JSON file.\n * - The validator is pure and synchronous; making the loader async\n * would add a coloring boundary for no benefit.\n *\n * **Failure modes — all surfaced as thrown errors with actionable\n * messages**:\n * - File missing — wrapped in a `[label-pack-loader] Could not read\n * pack at <path>` error so a typo in the dir / locale is obvious.\n * - JSON parse failure — error includes the file path and the\n * parser's own message.\n * - Top-level shape is not an object, or a value is not a string —\n * error names the offending key + observed type.\n *\n * **Why three exports** (loader, multi-loader, validator-runner):\n * - `loadLabelPack(packsDir, locale)` is the atomic primitive\n * (one file → one validated `LabelPack`). Used by the bridge at\n * request time.\n * - `loadAllLabelPacks(packsDir, siteContext)` is the build-time\n * batch — every locale pack at once. Used by the CLI auditor\n * and by the build-start hook.\n * - `validateAllLabelPacks(packsDir, siteContext, expectedKeysBySectionRef)`\n * is the highest-level entry point: load every pack, run the\n * validator on each, and either return the diagnostic results\n * or throw on the first hard error. The returned shape carries\n * per-locale diagnostics so the caller can format a CLI report.\n *\n * **`siteContext` parameter** — the consumer's composed\n * `WebsiteSiteContext` (see `website-site-context.ts`). The loader\n * reads `siteContext.websiteConfig.locales` for \"which locales does\n * the site ship?\" and `siteContext.rootConfig.{pageManifests,\n * chromeLabelKeys, skipToMainContentLabelKey}` for the manifest\n * registry / chrome-key set the validator runs against.\n */\n\n/**\n * Load and validate a single locale's label pack. Synchronous —\n * suitable for Astro frontmatter execution.\n *\n * @param packsDir Absolute or cwd-relative path to the directory\n * containing per-locale `<locale>.json` files.\n * @param locale The locale to load. Must be a member of\n * `WildoMarketingWebsiteConfig.locales`.\n * @returns A flat `LabelPack` (`Record<string, string>`) ready to\n * feed into `WebsiteRuntimeContextValue.labelPack`.\n * @throws When the file is missing, malformed JSON, not an object,\n * or contains a non-string value.\n */\nexport function loadLabelPack(packsDir: string, locale: AvailableLanguage): LabelPack {\n const filePath = resolve(packsDir, `${locale}.json`);\n\n let raw: string;\n try {\n raw = readFileSync(filePath, 'utf-8');\n } catch (cause) {\n throw new Error(\n `[label-pack-loader] Could not read label pack at ${filePath} ` +\n `(locale=\"${locale}\"). ` +\n `Make sure the directory exists and contains a \"${locale}.json\" file. ` +\n `Underlying error: ${(cause as Error).message}`,\n { cause },\n );\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (cause) {\n throw new Error(\n `[label-pack-loader] Failed to parse JSON in ${filePath} ` +\n `(locale=\"${locale}\"). Underlying parser error: ${(cause as Error).message}`,\n { cause },\n );\n }\n\n return assertLabelPackShape(parsed, filePath, locale);\n}\n\n/**\n * Batch loader — every locale declared on the site at once. The map\n * iteration order matches the declaration order of\n * `siteContext.websiteConfig.locales` so CLI reports come out in a\n * deterministic, authored sequence.\n */\nexport function loadAllLabelPacks(\n packsDir: string,\n siteContext: WebsiteSiteContext,\n): Map<AvailableLanguage, LabelPack> {\n const packs = new Map<AvailableLanguage, LabelPack>();\n for (const entry of siteContext.websiteConfig.locales) {\n packs.set(entry.locale, loadLabelPack(packsDir, entry.locale));\n }\n return packs;\n}\n\n/**\n * Per-locale validation result — the validator's flat output extended\n * with the locale tag so a multi-locale report can flatten + group.\n */\nexport interface LocaleLabelPackValidation {\n locale: AvailableLanguage;\n result: LabelPackValidationResult;\n}\n\n/**\n * Highest-level entry point: load every locale's pack and run the\n * label-pack validator against the manifest registry.\n *\n * **Return shape decision** — this function does NOT throw on\n * `missing` keys; it returns the per-locale results so the caller\n * (the build-start hook OR the CLI auditor OR a debugging\n * subprocess) can decide how to surface the failure (process exit\n * code, formatted CLI output, JSON report, …). A thin `throwIfAny…`\n * helper sits next to it for the common \"fail the build on first\n * missing key\" path.\n */\nexport function loadAndValidateAllLabelPacks(\n packsDir: string,\n siteContext: WebsiteSiteContext,\n expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,\n): readonly LocaleLabelPackValidation[] {\n const { rootConfig } = siteContext;\n const packs = loadAllLabelPacks(packsDir, siteContext);\n const manifests: readonly WebsitePageManifest[] = rootConfig.pageManifests;\n const effectiveChromeKeys = composeEffectiveChromeLabelKeys(rootConfig);\n\n const validations: LocaleLabelPackValidation[] = [];\n for (const [locale, pack] of packs) {\n validations.push({\n locale,\n result: validateLabelPackAgainstManifest(\n manifests,\n expectedKeysBySectionRef,\n pack,\n effectiveChromeKeys,\n ),\n });\n }\n return validations;\n}\n\n/**\n * Compose the chrome-label-key set the validator should treat as\n * \"expected\" — `WebsiteRootConfig.chromeLabelKeys` (consumer-authored)\n * UNION the framework-required `skipToMainContentLabelKey`.\n *\n * Centralized here because:\n * - Both `loadAndValidateAllLabelPacks` (used at build time) and\n * `buildKeyOriginMap` (used to annotate missing-key errors) need\n * the same composed list, and forking the union into two places\n * would be a drift hazard.\n * - The framework owns the skip-link key (it is rendered by\n * `WebsitePageLayout`, not the consumer's chrome), so the\n * consumer should NOT have to remember to also list it in\n * `chromeLabelKeys`. Auto-injection here keeps the contract\n * minimal at the consumer's call site (just declare the key in\n * `WebsiteRootConfig.skipToMainContentLabelKey` once + ship the\n * translation in every locale pack) while still surfacing the key\n * through the validator's \"missing\"/\"orphaned\" diagnostics.\n * - De-duplication is via `Set` semantics so a consumer who\n * defensively lists the key in both places (no-op but harmless)\n * does not produce a duplicate-key warning downstream.\n */\nfunction composeEffectiveChromeLabelKeys(\n rootConfig: WebsiteRootConfig,\n): readonly WebsiteLabelKey[] {\n const set = new Set<WebsiteLabelKey>(rootConfig.chromeLabelKeys);\n set.add(rootConfig.skipToMainContentLabelKey);\n // The visitor consent panel is framework-owned in the same way (#534): required in every pack, so a site\n // that turns on analytics can never ship it with an unlabelled way to refuse.\n for (const key of WEBSITE_CONSENT_LABEL_KEYS) set.add(key);\n return Array.from(set);\n}\n\n/**\n * Convenience wrapper for the build-start hook: validates every\n * declared locale's pack and throws a SINGLE aggregated error if any\n * locale has a hard failure (`missing` keys or `unmappedSectionRefs`).\n * Orphaned keys are logged via `console.warn` (soft warning per the\n * validator's contract) but do NOT throw.\n *\n * **Why aggregate (not first-fail)**: a multi-locale marketing site\n * with `en` and `fr` packs both missing keys would, under a\n * first-fail policy, force the author to fix `en`, re-run the build,\n * THEN see `fr`'s problem. Aggregating means one build run surfaces\n * every locale's hard errors together — material UX win for the\n * authoring loop. The aggregated message keeps each locale's failure\n * in its own block so the diagnostic stays scannable.\n *\n * **Why throw vs. return**: the build-start hook lives in\n * `astro.config.mjs` (or a `vite-plugin`-style integration) and\n * benefits from the shortest possible code path — a thrown error\n * naturally aborts `astro build` with a non-zero exit code and\n * surfaces the message in the build log. The richer\n * `loadAndValidateAllLabelPacks` is what `wildo website audit` calls\n * when it wants programmatic access to the per-locale results.\n *\n * **Key-origin annotation**: every `missing` key is tagged with the\n * page (and, if section-scoped, the section) that declared it, so the\n * author can grep + open the right `defineWebsiteSection` /\n * `defineWebsitePageManifest` call site without leaving the build log.\n */\nexport function assertLabelPacksValid(\n packsDir: string,\n siteContext: WebsiteSiteContext,\n expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,\n): void {\n const { rootConfig } = siteContext;\n const validations = loadAndValidateAllLabelPacks(\n packsDir,\n siteContext,\n expectedKeysBySectionRef,\n );\n\n /**\n * Build a key → origin map ONCE per assert call (cost is bounded by\n * the total number of declared keys across the whole site, which is\n * typically small for a marketing surface). Enriches the missing-key\n * error with the page/section that committed to each key — the\n * author can grep the ref to find the site authoring call site.\n */\n const keyOrigins = buildKeyOriginMap(\n rootConfig.pageManifests,\n expectedKeysBySectionRef,\n composeEffectiveChromeLabelKeys(rootConfig),\n );\n\n const localeErrorBlocks: string[] = [];\n\n for (const { locale, result } of validations) {\n const blocks: string[] = [];\n if (result.unmappedSectionRefs.length > 0) {\n blocks.push(\n ` unmappedSectionRefs (build-pipeline bug — sections referenced by a ` +\n `manifest but missing from the expected-keys collector): ` +\n `[${(result.unmappedSectionRefs as unknown as readonly string[]).join(', ')}]. ` +\n `Add every referenced section to the array passed to ` +\n `collectExpectedLabelKeysFromSections().`,\n );\n }\n if (result.missing.length > 0) {\n const annotated = (result.missing as unknown as readonly string[]).map(\n (key) => ` \"${key}\" ← ${keyOrigins.get(key) ?? 'unknown origin'}`,\n );\n blocks.push(\n ` missing ${result.missing.length} required key(s) in ${packsDir}/${locale}.json:\\n` +\n annotated.join('\\n'),\n );\n }\n if (blocks.length > 0) {\n localeErrorBlocks.push(`Locale \"${locale}\":\\n${blocks.join('\\n')}`);\n }\n if (result.orphaned.length > 0) {\n console.warn(\n `[label-pack-loader] Locale \"${locale}\": ${result.orphaned.length} ` +\n `orphaned key(s) in ${locale}.json (declared in pack but not consumed ` +\n `by any section or page manifest):\\n ${result.orphaned.join('\\n ')}`,\n );\n }\n }\n\n if (localeErrorBlocks.length > 0) {\n throw new Error(\n `[label-pack-loader] Label pack validation failed for ` +\n `${localeErrorBlocks.length} locale(s):\\n\\n${localeErrorBlocks.join('\\n\\n')}`,\n );\n }\n}\n\n/**\n * Build a `keyString -> \"page <pageRef> meta titleKey\" | \"section <sectionRef> in page <pageRef>\"`\n * lookup so the assert message can name the call site that committed\n * to each missing key. Pure and small (cost is O(totalKeys) per build\n * — bounded by the marketing surface).\n */\nfunction buildKeyOriginMap(\n manifests: readonly WebsitePageManifest[],\n expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,\n chromeLabelKeys: readonly WebsiteLabelKey[],\n): Map<string, string> {\n const origins = new Map<string, string>();\n for (const manifest of manifests) {\n const pageRefStr = manifest.ref as unknown as string;\n const titleKey = manifest.metaLabelKeys.titleKey as unknown as string;\n const descriptionKey = manifest.metaLabelKeys.descriptionKey as unknown as string;\n if (!origins.has(titleKey)) {\n origins.set(titleKey, `page \"${pageRefStr}\" metaLabelKeys.titleKey`);\n }\n if (!origins.has(descriptionKey)) {\n origins.set(descriptionKey, `page \"${pageRefStr}\" metaLabelKeys.descriptionKey`);\n }\n for (const sectionRef of manifest.sectionRefs) {\n const sectionRefStr = sectionRef as unknown as string;\n const sectionKeys = expectedKeysBySectionRef.get(sectionRef);\n if (sectionKeys === undefined) continue;\n for (const sectionKey of sectionKeys) {\n const keyStr = sectionKey as unknown as string;\n if (!origins.has(keyStr)) {\n origins.set(keyStr, `section \"${sectionRefStr}\" in page \"${pageRefStr}\"`);\n }\n }\n }\n }\n for (const chromeKey of chromeLabelKeys) {\n const keyStr = chromeKey as unknown as string;\n if (!origins.has(keyStr)) {\n origins.set(keyStr, `WebsiteRootConfig.chromeLabelKeys`);\n }\n }\n return origins;\n}\n\n/**\n * Defensive shape guard. Surface the offending key + observed type\n * so the consumer can find the malformed entry quickly. Pure\n * validation — no transformation; the returned value is the same\n * object reference passed in (cast to `LabelPack`) so the caller\n * does not pay an unnecessary copy.\n */\nfunction assertLabelPackShape(\n parsed: unknown,\n filePath: string,\n locale: AvailableLanguage,\n): LabelPack {\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(\n `[label-pack-loader] ${filePath} (locale=\"${locale}\") must contain a ` +\n `JSON object at the top level (got ${describeJson(parsed)}).`,\n );\n }\n for (const [key, value] of Object.entries(parsed)) {\n if (typeof value !== 'string') {\n throw new Error(\n `[label-pack-loader] ${filePath} (locale=\"${locale}\"): key \"${key}\" ` +\n `must map to a string (got ${describeJson(value)}).`,\n );\n }\n }\n return parsed as LabelPack;\n}\n\nfunction describeJson(value: unknown): string {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'array';\n return typeof value;\n}\n/** @wildo_source:part:end saas.website.astro.label-loader */\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llms-txt-server.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"llms-txt-server.d.ts","sourceRoot":"","sources":["../../../../src/astro/llms-txt-server.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,kBAAkB,EAC/B,OAAO,GAAE,sBAA2B,GACnC,MAAM,GAAG,IAAI,CAqBf;AACD,gEAAgE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pick-labels-for-page.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"pick-labels-for-page.d.ts","sourceRoot":"","sources":["../../../../src/astro/pick-labels-for-page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iDAAiD,CAAC;AACvF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2DAA2D,CAAC;AACrG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,mBAAmB,EAC7B,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,SAAS,CAkBX;AACD,qEAAqE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"robots-renderer.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"robots-renderer.d.ts","sourceRoot":"","sources":["../../../../src/astro/robots-renderer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,kBAAkB,GAAG,MAAM,CA+FvE;AACD,gEAAgE"}
|