@wildo-ai/saas-website 1.1.5 → 1.1.7
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 +9 -0
- package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -1
- package/dist/esm/astro/blog-post-bridge.js +10 -5
- 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 +25 -6
- package/dist/esm/astro/bridge-runtime.d.ts.map +1 -1
- package/dist/esm/astro/bridge-runtime.js +10 -6
- 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 +4 -4
- package/dist/esm/astro/label-pack-loader.d.ts.map +1 -1
- package/dist/esm/astro/label-pack-loader.js +9 -4
- 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 +9 -8
- package/dist/esm/astro/robots-renderer.d.ts.map +1 -1
- package/dist/esm/astro/robots-renderer.js +15 -15
- 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 +1 -1
- package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -1
- package/dist/esm/astro/website-page-head.renderer.js +2 -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 +5 -2
- package/dist/esm/astro/website-site-context.d.ts.map +1 -1
- package/dist/esm/astro/website-site-context.js.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 +8 -15
- package/dist/esm/companion-exports.d.ts.map +1 -1
- package/dist/esm/companion-exports.js +8 -15
- package/dist/esm/companion-exports.js.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 +3 -2
- package/dist/esm/config/define-website-config.d.ts.map +1 -1
- package/dist/esm/config/define-website-config.js +10 -10
- package/dist/esm/config/define-website-config.js.map +1 -1
- package/dist/esm/config/index.d.ts.map +1 -1
- package/dist/esm/config/load-website-config.d.ts +6 -0
- package/dist/esm/config/load-website-config.d.ts.map +1 -1
- package/dist/esm/config/load-website-config.js +18 -9
- package/dist/esm/config/load-website-config.js.map +1 -1
- package/dist/esm/config/load-website-pricing-catalog.d.ts +13 -0
- package/dist/esm/config/load-website-pricing-catalog.d.ts.map +1 -0
- package/dist/esm/config/load-website-pricing-catalog.js +34 -0
- package/dist/esm/config/load-website-pricing-catalog.js.map +1 -0
- package/dist/esm/config/load-website-site-context.d.ts +53 -0
- package/dist/esm/config/load-website-site-context.d.ts.map +1 -0
- package/dist/esm/config/load-website-site-context.js +98 -0
- package/dist/esm/config/load-website-site-context.js.map +1 -0
- package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -1
- package/dist/esm/config-loader.d.ts +2 -0
- package/dist/esm/config-loader.d.ts.map +1 -1
- package/dist/esm/config-loader.js +2 -0
- package/dist/esm/config-loader.js.map +1 -1
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +26 -6
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -1
- package/dist/esm/core/anonymous-session/InboundContactForm.js +51 -33
- package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -1
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +11 -0
- 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 +15 -4
- 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 +14 -2
- 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 +34 -4
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -1
- package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts +36 -0
- package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/website-submission-challenge.js +69 -0
- package/dist/esm/core/anonymous-session/website-submission-challenge.js.map +1 -0
- package/dist/esm/core/consent/WebsiteConsentBanner.d.ts.map +1 -1
- package/dist/esm/core/consent/WebsiteConsentBanner.js +11 -10
- package/dist/esm/core/consent/WebsiteConsentBanner.js.map +1 -1
- package/dist/esm/core/consent/WebsiteConsentContext.d.ts.map +1 -1
- package/dist/esm/core/consent/WebsiteConsentGate.d.ts.map +1 -1
- package/dist/esm/core/consent/useWebsiteConsent.d.ts.map +1 -1
- package/dist/esm/core/consent/website-consent-storage.d.ts.map +1 -1
- package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -1
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +25 -5
- 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/useWebsitePage.d.ts.map +1 -1
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +18 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -1
- package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -2
- package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -1
- package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -1
- package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts +57 -0
- package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts.map +1 -0
- package/dist/esm/core/external-providers/WebsiteProviderComponent.js +56 -0
- package/dist/esm/core/external-providers/WebsiteProviderComponent.js.map +1 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -1
- package/dist/esm/core/external-providers/provider-component-registry.website.d.ts +63 -0
- package/dist/esm/core/external-providers/provider-component-registry.website.d.ts.map +1 -0
- package/dist/esm/core/external-providers/provider-component-registry.website.js +68 -0
- package/dist/esm/core/external-providers/provider-component-registry.website.js.map +1 -0
- package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts +36 -0
- package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts.map +1 -0
- package/dist/esm/core/external-providers/useWebsiteProviderComponent.js +59 -0
- package/dist/esm/core/external-providers/useWebsiteProviderComponent.js.map +1 -0
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts +15 -5
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts.map +1 -1
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.js +39 -24
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.js.map +1 -1
- package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts +11 -0
- package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts.map +1 -0
- package/dist/esm/core/external-providers/website-provider-sdk-activations.js +14 -0
- package/dist/esm/core/external-providers/website-provider-sdk-activations.js.map +1 -0
- package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -1
- package/dist/esm/core/factories/define-website-section.d.ts +3 -3
- package/dist/esm/core/factories/define-website-section.d.ts.map +1 -1
- package/dist/esm/core/factories/define-website-section.js +3 -3
- package/dist/esm/core/factories/define-website-section.js.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.map +1 -1
- package/dist/esm/core/layouts/WebsitePageLayout.js +10 -7
- 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 +30 -65
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +30 -65
- 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 +35 -6
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +39 -5
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -1
- package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts +32 -0
- package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js +36 -0
- package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js.map +1 -0
- 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 +9 -8
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +8 -7
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.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-definition.shared.schemas.d.ts +14 -7
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +14 -7
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -1
- package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts +57 -0
- package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts.map +1 -0
- package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js +76 -0
- package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js.map +1 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +23 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -1
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +20 -2
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -1
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +7 -5
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.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 +5 -5
- package/src/__tests__/bundle-isolation.test.ts +19 -22
- package/src/astro/__tests__/blog-post-bridge.test.tsx +18 -2
- package/src/astro/__tests__/bridge-runtime.test.tsx +21 -3
- package/src/astro/__tests__/label-pack-loader.test.ts +7 -4
- package/src/astro/blog-post-bridge.tsx +19 -5
- package/src/astro/bridge-runtime.tsx +35 -11
- package/src/astro/label-pack-loader.ts +8 -4
- package/src/astro/robots-renderer.ts +15 -15
- package/src/astro/website-page-head.renderer.ts +2 -2
- package/src/astro/website-site-context.ts +5 -2
- package/src/companion-exports.ts +8 -15
- package/src/config/__tests__/website-build-origins.test.ts +52 -0
- package/src/config/define-website-config.ts +10 -10
- package/src/config/load-website-config.ts +25 -14
- package/src/config/load-website-pricing-catalog.ts +52 -0
- package/src/config/load-website-site-context.ts +145 -0
- package/src/config-loader.ts +2 -0
- package/src/core/__tests__/contexts.test.tsx +20 -1
- package/src/core/__tests__/website-consent.test.tsx +2 -1
- package/src/core/anonymous-session/InboundContactForm.tsx +100 -45
- package/src/core/anonymous-session/__tests__/InboundContactForm.labels.test.tsx +108 -0
- package/src/core/anonymous-session/__tests__/InboundContactForm.submission-challenge.test.tsx +139 -0
- package/src/core/anonymous-session/inbound-contact-form.schema.ts +16 -4
- package/src/core/anonymous-session/website-anonymous-session-client.ts +39 -4
- package/src/core/anonymous-session/website-submission-challenge.ts +99 -0
- package/src/core/consent/WebsiteConsentBanner.tsx +11 -10
- package/src/core/contexts/WebsiteRuntimeContext.tsx +25 -5
- package/src/core/contexts/useWebsiteRuntime.ts +33 -2
- package/src/core/external-providers/WebsiteProviderComponent.tsx +153 -0
- package/src/core/external-providers/__tests__/frontend-provider-registry.website.test.ts +4 -0
- package/src/core/external-providers/__tests__/website-provider-component.test.tsx +238 -0
- package/src/core/external-providers/__tests__/website-provider-sdks.test.tsx +72 -0
- package/src/core/external-providers/provider-component-registry.website.ts +115 -0
- package/src/core/external-providers/useWebsiteProviderComponent.ts +76 -0
- package/src/core/external-providers/useWebsiteProviderScripts.ts +56 -37
- package/src/core/external-providers/website-provider-sdk-activations.ts +23 -0
- package/src/core/factories/define-website-section.ts +3 -3
- package/src/core/layouts/WebsitePageLayout.tsx +11 -6
- package/src/index.ts +31 -65
- package/src/schemas/__tests__/design-token-css-property.test.ts +72 -0
- package/src/schemas/__tests__/pricing-offer-structured-data.test.ts +122 -0
- package/src/schemas/__tests__/structured-data-reconciliation.test.ts +35 -0
- package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +45 -5
- package/src/schemas/label-keys/__tests__/scaffolded-consent-label-pack.parity.test.ts +12 -0
- package/src/schemas/label-keys/website-provider-component-label-keys.schemas.ts +40 -0
- package/src/schemas/manifests/website-root-config.shared.schemas.ts +10 -9
- package/src/schemas/sections/website-section-definition.shared.schemas.ts +14 -7
- package/src/schemas/structured-data/pricing-offer-structured-data.shared.ts +144 -0
- package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +46 -1
- package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +7 -5
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading provider-contributed components on the website (#1703) — the website's counterpart of the
|
|
3
|
+
* application's `useProviderComponentChoices` / `useProviderComponent`, over the same registry and
|
|
4
|
+
* the same status vocabulary (`ProviderComponentStatus`).
|
|
5
|
+
*
|
|
6
|
+
* Two differences from the application, both about WHO answers:
|
|
7
|
+
*
|
|
8
|
+
* - the consent grant is the VISITOR's decision from `WebsiteConsentContext`, not a signed-in
|
|
9
|
+
* person's stored preference;
|
|
10
|
+
* - nothing starts before that decision has been read from storage (`isResolved`). The server render
|
|
11
|
+
* and the first client render read no storage, and asking the registry then would report a
|
|
12
|
+
* consent-required state to a visitor who already agreed, flashing the refusal on every page.
|
|
13
|
+
*
|
|
14
|
+
* Neither hook picks a provider: when several providers offer a component for one capability,
|
|
15
|
+
* choosing is the page's job.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useEffect, useState } from 'react';
|
|
19
|
+
import {
|
|
20
|
+
idleProviderComponentLoadState,
|
|
21
|
+
startProviderComponentLoad,
|
|
22
|
+
type ProviderComponentLoadState,
|
|
23
|
+
} from '@wildo-ai/external-connectors-public/website';
|
|
24
|
+
import type { ProviderCapability } from '@wildo-ai/saas-models/public-runtime';
|
|
25
|
+
|
|
26
|
+
import { useWebsiteConsent } from '../consent/useWebsiteConsent';
|
|
27
|
+
import { useWebsiteRuntime } from '../contexts/useWebsiteRuntime';
|
|
28
|
+
import type { WebsiteProviderComponentType } from './provider-component-registry.website';
|
|
29
|
+
|
|
30
|
+
/** What `useWebsiteProviderComponent` returns: the shared load state, holding a React component. */
|
|
31
|
+
export type WebsiteProviderComponentState = ProviderComponentLoadState<WebsiteProviderComponentType>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The providers that are BOTH live for this website service and carry a component for this
|
|
35
|
+
* capability, in the order the service declared them. Synchronous, so a page can render its choice
|
|
36
|
+
* on the first frame.
|
|
37
|
+
*/
|
|
38
|
+
export function useWebsiteProviderComponentChoices(capability: ProviderCapability): ReadonlyArray<string> {
|
|
39
|
+
const { providerComponentRegistry } = useWebsiteRuntime();
|
|
40
|
+
return providerComponentRegistry?.listProviders(capability) ?? [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Loads one provider's component for a capability, once the visitor's consent decision is known.
|
|
45
|
+
*
|
|
46
|
+
* Pass `undefined` for the ref while nothing is chosen. The load runs in an effect, so it never runs
|
|
47
|
+
* during the Astro server render; `CONSENT_REQUIRED` means nothing of the provider was downloaded,
|
|
48
|
+
* and granting the missing purposes through the consent panel loads it without a re-mount.
|
|
49
|
+
*/
|
|
50
|
+
export function useWebsiteProviderComponent(
|
|
51
|
+
capability: ProviderCapability,
|
|
52
|
+
providerRef: string | undefined,
|
|
53
|
+
): WebsiteProviderComponentState {
|
|
54
|
+
const { providerComponentRegistry } = useWebsiteRuntime();
|
|
55
|
+
const consent = useWebsiteConsent();
|
|
56
|
+
const grantedKey = [...consent.grantedPurposes].sort().join(',');
|
|
57
|
+
const [state, setState] = useState<WebsiteProviderComponentState>(() => idleProviderComponentLoadState<WebsiteProviderComponentType>());
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!consent.isResolved) {
|
|
61
|
+
setState(idleProviderComponentLoadState<WebsiteProviderComponentType>());
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return startProviderComponentLoad({
|
|
65
|
+
registry: providerComponentRegistry,
|
|
66
|
+
capability,
|
|
67
|
+
providerRef,
|
|
68
|
+
grantedConsentPurposes: consent.grantedPurposes,
|
|
69
|
+
onState: setState,
|
|
70
|
+
});
|
|
71
|
+
// `grantedPurposes` is read through `grantedKey`: keyed by the granted SET, not the array's identity.
|
|
72
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
+
}, [capability, providerRef, providerComponentRegistry, consent.isResolved, grantedKey]);
|
|
74
|
+
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
@@ -31,16 +31,18 @@
|
|
|
31
31
|
* Revisit if a provider ever needs to run before first paint.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import { useEffect } from 'react';
|
|
34
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
35
35
|
import {
|
|
36
36
|
activateFrontendProviderSdks,
|
|
37
|
-
|
|
37
|
+
collectProviderScriptsWithoutComponents,
|
|
38
38
|
consentPurposesRequiredByProvider,
|
|
39
|
+
createFrontendProviderSdkActivationLane,
|
|
39
40
|
injectProviderScripts,
|
|
40
|
-
type ActivatedFrontendProviderSdks,
|
|
41
41
|
} from '@wildo-ai/external-connectors-public/website';
|
|
42
42
|
import { AppFrontendType, type ConsentPurpose } from '@wildo-ai/saas-models/public-runtime';
|
|
43
43
|
import type { FrontendWebsiteProviderRegistry } from './frontend-provider-registry.website';
|
|
44
|
+
import type { WebsiteProviderComponentRegistry } from './provider-component-registry.website';
|
|
45
|
+
import type { WebsiteProviderSdkActivation } from './website-provider-sdk-activations';
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
48
|
* A stable key for a granted-purpose set, so an effect re-runs when the SET changes and not when a new
|
|
@@ -50,20 +52,28 @@ function grantedKey(grantedConsentPurposes: readonly ConsentPurpose[]): string {
|
|
|
50
52
|
return [...grantedConsentPurposes].sort().join(',');
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Injects the `<script>` tags of the live providers that rest on NO consent purpose and render no component.
|
|
57
|
+
*
|
|
58
|
+
* No consent input, deliberately (#1704): an executed `<script>` cannot be stopped, so a provider that rests on a
|
|
59
|
+
* purpose must ship as a stoppable SDK (started on the grant and stopped on withdrawal by
|
|
60
|
+
* {@link useWebsiteProviderSdks}); a consent-based module declaring `scripts` is refused at projection and skipped by
|
|
61
|
+
* the collector. No grant could change what this injects.
|
|
62
|
+
*/
|
|
53
63
|
export function useWebsiteProviderScripts(
|
|
54
64
|
registry: FrontendWebsiteProviderRegistry | undefined,
|
|
55
65
|
/**
|
|
56
|
-
* The
|
|
57
|
-
*
|
|
58
|
-
*
|
|
66
|
+
* The website's provider-component registry (#1703). A provider with a registered component loads its
|
|
67
|
+
* scripts WITH that component, when a page first shows it — so an address lookup's vendor loader is
|
|
68
|
+
* not downloaded on every page of the site, only on the one with the form. This hook injects the rest.
|
|
59
69
|
*/
|
|
60
|
-
|
|
70
|
+
componentRegistry?: WebsiteProviderComponentRegistry,
|
|
61
71
|
): void {
|
|
62
|
-
const key = grantedKey(grantedConsentPurposes);
|
|
63
72
|
useEffect(() => {
|
|
64
73
|
if (typeof document === 'undefined') return;
|
|
65
74
|
|
|
66
|
-
const
|
|
75
|
+
const withComponents = [...(componentRegistry?.providersWithComponents() ?? [])].map((providerRef) => ({ providerRef }));
|
|
76
|
+
const scripts = collectProviderScriptsWithoutComponents(registry?.modules.values() ?? [], withComponents);
|
|
67
77
|
if (scripts.length === 0) return;
|
|
68
78
|
|
|
69
79
|
injectProviderScripts(scripts, document);
|
|
@@ -71,8 +81,7 @@ export function useWebsiteProviderScripts(
|
|
|
71
81
|
// work immediately; removing the element undoes neither, so a cleanup that
|
|
72
82
|
// removed it would only make the next mount inject a SECOND copy of an
|
|
73
83
|
// already-running script. Idempotence by `src` is the real guard.
|
|
74
|
-
|
|
75
|
-
}, [registry, key]);
|
|
84
|
+
}, [registry, componentRegistry]);
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
/**
|
|
@@ -91,16 +100,20 @@ export function useWebsiteProviderSdks(
|
|
|
91
100
|
* is withdrawn — which is the only way a withdrawal stops a running analytics client.
|
|
92
101
|
*/
|
|
93
102
|
grantedConsentPurposes: readonly ConsentPurpose[],
|
|
94
|
-
):
|
|
95
|
-
useProviderSdkGroup(registry, false, []);
|
|
96
|
-
useProviderSdkGroup(registry, true, grantedConsentPurposes);
|
|
103
|
+
): ReadonlyArray<WebsiteProviderSdkActivation> {
|
|
104
|
+
const consentFree = useProviderSdkGroup(registry, false, []);
|
|
105
|
+
const consentBased = useProviderSdkGroup(registry, true, grantedConsentPurposes);
|
|
106
|
+
// The providers RUNNING on this page, both groups, published by the page layout (#1735): a form reads the captcha
|
|
107
|
+
// vendor's handle from here. Emptied the moment a group stops, so nothing holds a handle past a withdrawal.
|
|
108
|
+
return useMemo(() => [...consentFree, ...consentBased], [consentFree, consentBased]);
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
function useProviderSdkGroup(
|
|
100
112
|
registry: FrontendWebsiteProviderRegistry | undefined,
|
|
101
113
|
consentBased: boolean,
|
|
102
114
|
grantedConsentPurposes: readonly ConsentPurpose[],
|
|
103
|
-
):
|
|
115
|
+
): ReadonlyArray<WebsiteProviderSdkActivation> {
|
|
116
|
+
const [running, setRunning] = useState<ReadonlyArray<WebsiteProviderSdkActivation>>([]);
|
|
104
117
|
// Keyed by the grants this group's providers actually rest on, so changing an unrelated purpose does
|
|
105
118
|
// not stop and restart a running analytics client.
|
|
106
119
|
const relevant = registry === undefined
|
|
@@ -110,6 +123,8 @@ function useProviderSdkGroup(
|
|
|
110
123
|
.flatMap((module) => consentPurposesRequiredByProvider(module))
|
|
111
124
|
.filter((purpose) => grantedConsentPurposes.includes(purpose));
|
|
112
125
|
const key = grantedKey(relevant);
|
|
126
|
+
// One lane per group for the page's lifetime: a start waits for the previous stop.
|
|
127
|
+
const lane = useRef(createFrontendProviderSdkActivationLane());
|
|
113
128
|
useEffect(() => {
|
|
114
129
|
if (registry === undefined) return;
|
|
115
130
|
const modules = [...registry.modules.values()].filter(
|
|
@@ -117,31 +132,35 @@ function useProviderSdkGroup(
|
|
|
117
132
|
);
|
|
118
133
|
if (modules.length === 0) return;
|
|
119
134
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
// Through the group's lane, so a start never overlaps the previous stop of the same page-global vendor (#1704): a
|
|
136
|
+
// visitor who grants, withdraws and grants again while the bundle downloads must not have the first run's stop
|
|
137
|
+
// switch off the second run's client. The stop also covers an unmount mid-download.
|
|
138
|
+
const stop = lane.current.start(
|
|
139
|
+
() => activateFrontendProviderSdks({
|
|
140
|
+
surface: AppFrontendType.STATIC_WEBSITE,
|
|
141
|
+
modules,
|
|
142
|
+
grantedConsentPurposes,
|
|
143
|
+
onDiagnostic: (diagnostic) => {
|
|
144
|
+
// The website runtime has no logger service, and a provider that cannot
|
|
145
|
+
// start is a developer-facing fact that must not be silent.
|
|
146
|
+
// eslint-disable-next-line no-console
|
|
147
|
+
console.warn(`[wildo-provider-sdk] ${diagnostic.kind}: ${diagnostic.message}`);
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
(result) => {
|
|
151
|
+
setRunning(modules.flatMap((module) => {
|
|
152
|
+
const activation = result.activations.get(module.metadata.ref);
|
|
153
|
+
return activation === undefined
|
|
154
|
+
? []
|
|
155
|
+
: [{ providerRef: module.metadata.ref, providerCapabilities: [...module.providerCapabilities], activation }];
|
|
156
|
+
}));
|
|
132
157
|
},
|
|
133
|
-
|
|
134
|
-
activated = result;
|
|
135
|
-
// Activation is asynchronous and unmount is not: if the surface went away
|
|
136
|
-
// while a vendor bundle was still downloading, stop what we just started
|
|
137
|
-
// rather than leaving a client nobody can reach.
|
|
138
|
-
if (unmounted) void result.deactivateAll();
|
|
139
|
-
});
|
|
140
|
-
|
|
158
|
+
);
|
|
141
159
|
return () => {
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
setRunning([]);
|
|
161
|
+
stop();
|
|
144
162
|
};
|
|
145
163
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- keyed by the granted SET, see grantedKey.
|
|
146
164
|
}, [registry, consentBased, key]);
|
|
165
|
+
return running;
|
|
147
166
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provider SDKs RUNNING on this website page (#1735), published by the page layout.
|
|
3
|
+
*
|
|
4
|
+
* A section reaches a running vendor through here rather than through a global — the contact form asks the consented
|
|
5
|
+
* captcha vendor for a token this way. The list is emptied the moment a provider is stopped (a consent withdrawal, a
|
|
6
|
+
* navigation), so a consumer that reads it at the moment it needs a handle never holds one past a withdrawal.
|
|
7
|
+
*/
|
|
8
|
+
import { createContext, useContext } from 'react';
|
|
9
|
+
import type { ProviderFrontendSdkActivation } from '@wildo-ai/external-connectors-public/website';
|
|
10
|
+
|
|
11
|
+
/** One running provider: its ref, the capabilities it serves, and its activation (whose `handle` is the vendor's client). */
|
|
12
|
+
export interface WebsiteProviderSdkActivation {
|
|
13
|
+
readonly providerRef: string;
|
|
14
|
+
readonly providerCapabilities: ReadonlyArray<string>;
|
|
15
|
+
readonly activation: ProviderFrontendSdkActivation;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const WebsiteProviderSdkActivationsContext = createContext<ReadonlyArray<WebsiteProviderSdkActivation>>([]);
|
|
19
|
+
|
|
20
|
+
/** The provider SDKs running on this page, in declaration order. Empty outside a page layout and before any start. */
|
|
21
|
+
export function useWebsiteProviderSdkActivations(): ReadonlyArray<WebsiteProviderSdkActivation> {
|
|
22
|
+
return useContext(WebsiteProviderSdkActivationsContext);
|
|
23
|
+
}
|
|
@@ -71,9 +71,9 @@ export interface TypedWebsiteSectionDefinition<TProps>
|
|
|
71
71
|
* **Why a factory instead of constructing the literal directly**:
|
|
72
72
|
* - Consumers don't have to remember to parse refs / keys
|
|
73
73
|
* themselves before assembling the literal.
|
|
74
|
-
* - The schema parse provides a single audited validation chokepoint
|
|
75
|
-
*
|
|
76
|
-
* without re-entering the React graph.
|
|
74
|
+
* - The schema parse provides a single audited validation chokepoint,
|
|
75
|
+
* and the parsed `expectedLabelKeys` are what `wildo website audit`
|
|
76
|
+
* checks every locale pack against, without re-entering the React graph.
|
|
77
77
|
* - The factory is the documented authoring API the
|
|
78
78
|
* `website_factory_authoring` skill targets — keeping the
|
|
79
79
|
* authoring surface narrow lets the framework evolve the
|
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
useWebsiteProviderScripts,
|
|
8
8
|
useWebsiteProviderSdks,
|
|
9
9
|
} from '../external-providers/useWebsiteProviderScripts';
|
|
10
|
+
import { WebsiteProviderSdkActivationsContext } from '../external-providers/website-provider-sdk-activations';
|
|
10
11
|
import type { WebsitePageContextValue } from '../contexts/WebsitePageContext';
|
|
11
12
|
import { useWebsiteRuntime } from '../contexts/useWebsiteRuntime';
|
|
13
|
+
import { websiteDesignTokenCssVar } from '../../schemas/design-tokens/website-design-tokens.shared.schemas';
|
|
12
14
|
import { useWebsiteLabelByKey } from '../hooks/useWebsiteLabel';
|
|
13
15
|
import { WebsiteConsentProvider, WebsiteConsentStatus, type WebsiteConsentRecorder } from '../consent/WebsiteConsentContext';
|
|
14
16
|
import { useWebsiteConsent } from '../consent/useWebsiteConsent';
|
|
@@ -100,10 +102,10 @@ const SKIP_LINK_FOCUS_CSS = `
|
|
|
100
102
|
overflow: visible !important;
|
|
101
103
|
clip: auto !important;
|
|
102
104
|
white-space: normal !important;
|
|
103
|
-
background:
|
|
104
|
-
color:
|
|
105
|
-
border: 2px solid
|
|
106
|
-
border-radius:
|
|
105
|
+
background: ${websiteDesignTokenCssVar('colors', 'background', '#fff')} !important;
|
|
106
|
+
color: ${websiteDesignTokenCssVar('colors', 'foreground', '#000')} !important;
|
|
107
|
+
border: 2px solid ${websiteDesignTokenCssVar('colors', 'primary', '#000')} !important;
|
|
108
|
+
border-radius: ${websiteDesignTokenCssVar('radii', 'md', '0.375rem')} !important;
|
|
107
109
|
z-index: 9999 !important;
|
|
108
110
|
text-decoration: none !important;
|
|
109
111
|
}
|
|
@@ -281,8 +283,9 @@ function WebsitePageLayoutContent({
|
|
|
281
283
|
//
|
|
282
284
|
// Consent-based providers start only once their purpose is granted (#534). Before the stored decision is
|
|
283
285
|
// read, nothing is granted — the static HTML and the first render start nothing optional.
|
|
284
|
-
useWebsiteProviderScripts(runtime.frontendProviderRegistry,
|
|
285
|
-
|
|
286
|
+
useWebsiteProviderScripts(runtime.frontendProviderRegistry, runtime.providerComponentRegistry);
|
|
287
|
+
// The running SDKs are published to the page (#1735): the contact form asks the consented captcha vendor for a token.
|
|
288
|
+
const runningProviderSdks = useWebsiteProviderSdks(runtime.frontendProviderRegistry, consent.grantedPurposes);
|
|
286
289
|
|
|
287
290
|
const providerRootAttributes = useMemo<Record<string, string>>(() => {
|
|
288
291
|
const attrs: Record<string, string> = {};
|
|
@@ -330,6 +333,7 @@ function WebsitePageLayoutContent({
|
|
|
330
333
|
|
|
331
334
|
return (
|
|
332
335
|
<WebsitePageContextProvider value={pageContextValue}>
|
|
336
|
+
<WebsiteProviderSdkActivationsContext.Provider value={runningProviderSdks}>
|
|
333
337
|
<div
|
|
334
338
|
data-website-page-ref={safePageRef as unknown as string}
|
|
335
339
|
className={className}
|
|
@@ -357,6 +361,7 @@ function WebsitePageLayoutContent({
|
|
|
357
361
|
{resolvedFooter !== null ? <footer>{resolvedFooter}</footer> : null}
|
|
358
362
|
{renderConsentSurface(consent, runtime.consentBannerComponent)}
|
|
359
363
|
</div>
|
|
364
|
+
</WebsiteProviderSdkActivationsContext.Provider>
|
|
360
365
|
</WebsitePageContextProvider>
|
|
361
366
|
);
|
|
362
367
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,75 +11,33 @@
|
|
|
11
11
|
* accompanying SKILLs in `framework-knowledge`.
|
|
12
12
|
*
|
|
13
13
|
* @wildo-boundary
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* This barrel re-exports the React-side surface: `schemas/`, `core/`,
|
|
15
|
+
* `components/`. The other entries are separate subpaths with their own
|
|
16
|
+
* audiences — `/astro` (Node, Astro frontmatter), `/astro-island` (the
|
|
17
|
+
* bridge factory), `/config` and `/config-loader` (Node), `/mdx`,
|
|
18
|
+
* `/companion` (React-free). Per directory:
|
|
17
19
|
* - `schemas/` : NO React, NO Astro, NO `~wildo-frontend-lib/**`
|
|
18
|
-
* - `
|
|
19
|
-
* - `
|
|
20
|
-
* - `
|
|
20
|
+
* - `core/`, `components/`: MAY use React; NO `~wildo-frontend-lib/**` (Q17=B)
|
|
21
|
+
* - `astro/` : MAY use Astro and Node; NO React component runtime imports
|
|
22
|
+
* - `config/` : Node-side loaders; never reachable from a React-island entry
|
|
21
23
|
* - `mdx/` : MAY use `@mdx-js/*`
|
|
22
|
-
* - `integration/` : MAY use React; MAY import the
|
|
23
|
-
* `@wildo-ai/saas-models/public-runtime` SUBPATH,
|
|
24
|
-
* the `@wildo-ai/presets-components-models/public-runtime`
|
|
25
|
-
* SUBPATH, `@wildo-ai/zod-decorators`,
|
|
26
|
-
* plus the SSR / Astro-middleware tier
|
|
27
|
-
* (`axios`, `jsonwebtoken`, `socket.io-client`,
|
|
28
|
-
* `reflect-metadata`) — D12 revised
|
|
29
|
-
* 2026-04-19 (second same-day loosening);
|
|
30
|
-
* the ROOT barrels of `@wildo-ai/saas-models`
|
|
31
|
-
* and `@wildo-ai/presets-components-models`
|
|
32
|
-
* are FORBIDDEN (they boot the decorator
|
|
33
|
-
* framework / pull the heavy registry);
|
|
34
|
-
* the saas-app DI / backend stack
|
|
35
|
-
* (`inversify`, `mongoose`, `express`,
|
|
36
|
-
* `pino`, `helmet`, `argon2`, `bcrypt`,
|
|
37
|
-
* the `@wildo-ai/saas-frontend-lib` /
|
|
38
|
-
* `@wildo-ai/saas-backend-lib` /
|
|
39
|
-
* `@wildo-ai/external-connectors-models`
|
|
40
|
-
* root + subpath surfaces) is FORBIDDEN;
|
|
41
|
-
* MUST NOT import `~wildo-frontend-lib/**`
|
|
42
|
-
* (Q17=B / D16)
|
|
43
24
|
*
|
|
44
|
-
* D12 (client-bundle import discipline
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* internal seam for symbols not (yet) re-exported by saas-models.
|
|
52
|
-
* - `@wildo-ai/zod-decorators` is allowed to import (first revision
|
|
53
|
-
* 2026-04-19). The package itself is side-effect-free; only an
|
|
54
|
-
* explicit `initZodDecorators()` call mutates `z.ZodType.prototype`,
|
|
55
|
-
* which neither saas-website nor the saas-models /public-runtime
|
|
56
|
-
* barrel does.
|
|
57
|
-
* - `axios`, `jsonwebtoken`, `socket.io-client`, `reflect-metadata` are
|
|
58
|
-
* allowed for SSR / Astro-middleware code paths (second same-day
|
|
59
|
-
* revision 2026-04-19). The website engine needs them to provide
|
|
60
|
-
* middleware utilities (HTTP proxying to the SaaS backend, JWT
|
|
61
|
-
* verification of cross-frontend handoff codes, real-time push,
|
|
62
|
-
* decorator-metadata reads). They are SSR-tier — discipline
|
|
63
|
-
* (and a future strict/loose entry split for React islands vs
|
|
64
|
-
* middleware) keeps them out of the React-island bundle.
|
|
25
|
+
* D12 (client-bundle import discipline): everything reachable from a
|
|
26
|
+
* React-island entry ships to the browser, so every shared model atom
|
|
27
|
+
* comes from a side-effect-free `/public-runtime` subpath —
|
|
28
|
+
* `@wildo-ai/saas-models/public-runtime` first,
|
|
29
|
+
* `@wildo-ai/presets-components-models/public-runtime` for symbols it does
|
|
30
|
+
* not re-export yet — and `@wildo-ai/zod-decorators` is allowed because
|
|
31
|
+
* only an explicit `initZodDecorators()` mutates Zod's prototype.
|
|
65
32
|
*
|
|
66
|
-
* The ROOT
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* `@wildo-ai/
|
|
71
|
-
* (`
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* These boundaries are enforced by review + the bundle-isolation test
|
|
76
|
-
* above until Phase 8 promotes them to a programmatic ESLint check
|
|
77
|
-
* (cf. `saas-website-slice.md`
|
|
78
|
-
* §"DEFERRED to Phase 8 — boundary lint mechanism design").
|
|
79
|
-
*
|
|
80
|
-
* Phase 1+2 status: schemas + runtime contexts/hooks + layout
|
|
81
|
-
* primitives + L1 placeholders + factories are all exported. Phase 5
|
|
82
|
-
* adds the Astro adapter; Phase 5.7 the `integration/` layer.
|
|
33
|
+
* The ROOT barrels of `@wildo-ai/saas-models` and
|
|
34
|
+
* `@wildo-ai/presets-components-models` are forbidden everywhere in this
|
|
35
|
+
* package (they boot the decorator framework and pull the full schema
|
|
36
|
+
* graph), as are the saas-app DI runtime (`inversify`,
|
|
37
|
+
* `@wildo-ai/saas-frontend-lib`, `@wildo-ai/saas-backend-lib`) and
|
|
38
|
+
* pure-backend libs (`mongoose`, `express`, `pino`, `helmet`, `argon2`,
|
|
39
|
+
* `bcrypt`). Enforced by `src/__tests__/bundle-isolation.test.ts`, which
|
|
40
|
+
* walks every entry's import graph against an allow-list.
|
|
83
41
|
*
|
|
84
42
|
* D18 (L1 boundary, Phase 2): the L1 primitives (`WebsiteHeading`,
|
|
85
43
|
* `WebsiteText`, `WebsiteButton`, `WebsiteLink`, `WebsiteImage`) are
|
|
@@ -96,10 +54,12 @@ export * from './schemas/refs/page-ref.schemas';
|
|
|
96
54
|
export * from './schemas/refs/section-ref.schemas';
|
|
97
55
|
export * from './schemas/label-keys/website-label-key.schemas';
|
|
98
56
|
export * from './schemas/label-keys/website-consent-label-keys.schemas';
|
|
57
|
+
export * from './schemas/label-keys/website-provider-component-label-keys.schemas';
|
|
99
58
|
export * from './schemas/sections/website-section-category.shared';
|
|
100
59
|
export * from './schemas/sections/website-section-definition.shared.schemas';
|
|
101
60
|
export * from './schemas/structured-data/website-structured-data.shared.schemas';
|
|
102
61
|
export * from './schemas/structured-data/structured-data-reconciliation.shared';
|
|
62
|
+
export * from './schemas/structured-data/pricing-offer-structured-data.shared';
|
|
103
63
|
export * from './schemas/design-tokens/website-design-tokens.shared.schemas';
|
|
104
64
|
export * from './schemas/manifests/website-page-manifest.shared.schemas';
|
|
105
65
|
export * from './schemas/manifests/website-root-config.shared.schemas';
|
|
@@ -125,9 +85,15 @@ export * from './core/consent/useWebsiteConsent';
|
|
|
125
85
|
export * from './core/consent/WebsiteConsentGate';
|
|
126
86
|
export * from './core/consent/WebsiteConsentBanner';
|
|
127
87
|
|
|
88
|
+
export * from './core/external-providers/provider-component-registry.website';
|
|
89
|
+
export * from './core/external-providers/useWebsiteProviderComponent';
|
|
90
|
+
export * from './core/external-providers/WebsiteProviderComponent';
|
|
91
|
+
export * from './core/external-providers/website-provider-sdk-activations';
|
|
92
|
+
|
|
128
93
|
export * from './core/anonymous-session/website-anonymous-session-client';
|
|
129
94
|
export * from './core/anonymous-session/inbound-contact-form.schema';
|
|
130
95
|
export * from './core/anonymous-session/InboundContactForm';
|
|
96
|
+
export * from './core/anonymous-session/website-submission-challenge';
|
|
131
97
|
|
|
132
98
|
export * from './components/low-level/WebsiteHeading';
|
|
133
99
|
export * from './components/low-level/WebsiteText';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { renderWebsiteDesignTokensCss } from '../../astro/website-page-head.renderer';
|
|
7
|
+
import {
|
|
8
|
+
WebsiteDesignTokensSchema,
|
|
9
|
+
websiteDesignTokenCssProperty,
|
|
10
|
+
websiteDesignTokenCssVar,
|
|
11
|
+
type WebsiteDesignTokens,
|
|
12
|
+
} from '../design-tokens/website-design-tokens.shared.schemas';
|
|
13
|
+
|
|
14
|
+
const TOKENS: WebsiteDesignTokens = WebsiteDesignTokensSchema.parse({
|
|
15
|
+
colors: {
|
|
16
|
+
background: '#fff',
|
|
17
|
+
foreground: '#111',
|
|
18
|
+
primary: '#222',
|
|
19
|
+
primaryForeground: '#fff',
|
|
20
|
+
accent: '#333',
|
|
21
|
+
accentForeground: '#fff',
|
|
22
|
+
surface: '#eee',
|
|
23
|
+
surfaceForeground: '#444',
|
|
24
|
+
border: '#ccc',
|
|
25
|
+
muted: '#ddd',
|
|
26
|
+
mutedForeground: '#555',
|
|
27
|
+
},
|
|
28
|
+
radii: { none: '0', sm: '0.25rem', md: '0.5rem', lg: '0.75rem', xl: '1rem', full: '9999px' },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const SOURCE_ROOT = join(__dirname, '..', '..');
|
|
32
|
+
|
|
33
|
+
function sourceFiles(dir: string): string[] {
|
|
34
|
+
return readdirSync(dir).flatMap((entry) => {
|
|
35
|
+
const full = join(dir, entry);
|
|
36
|
+
if (statSync(full).isDirectory()) return entry === '__tests__' ? [] : sourceFiles(full);
|
|
37
|
+
return /\.(ts|tsx)$/.test(entry) ? [full] : [];
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('design-token CSS property naming', () => {
|
|
42
|
+
it('the renderer emits every token under the name the readers reference', () => {
|
|
43
|
+
const css = renderWebsiteDesignTokensCss(TOKENS);
|
|
44
|
+
for (const [group, values] of Object.entries(TOKENS)) {
|
|
45
|
+
for (const token of Object.keys(values)) {
|
|
46
|
+
expect(css).toContain(`${websiteDesignTokenCssProperty(group, token)}:`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// The key is verbatim: the reader the consent banner used to be spelled `surface-foreground`.
|
|
50
|
+
expect(css).toContain('--website-colors-surfaceForeground: #444;');
|
|
51
|
+
expect(websiteDesignTokenCssVar('colors', 'surfaceForeground', '#000')).toBe('var(--website-colors-surfaceForeground, #000)');
|
|
52
|
+
expect(websiteDesignTokenCssVar('radii', 'md')).toBe('var(--website-radii-md)');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('no engine source spells a token custom property by hand', () => {
|
|
56
|
+
// Positive control: the walk reaches the component that used to carry the defect.
|
|
57
|
+
const files = sourceFiles(SOURCE_ROOT);
|
|
58
|
+
expect(files.some((file) => file.endsWith('WebsiteConsentBanner.tsx'))).toBe(true);
|
|
59
|
+
|
|
60
|
+
const offenders = files
|
|
61
|
+
.filter((file) => !file.endsWith('website-design-tokens.shared.schemas.ts'))
|
|
62
|
+
.flatMap((file) =>
|
|
63
|
+
readFileSync(file, 'utf8')
|
|
64
|
+
.split('\n')
|
|
65
|
+
.map((line, index) => ({ line, index }))
|
|
66
|
+
// A `var(--website-…)` in code, not in a comment explaining the naming rule.
|
|
67
|
+
.filter(({ line }) => /var\(--website-/.test(line) && !/^\s*(\*|\/\/)/.test(line))
|
|
68
|
+
.map(({ index }) => `${relative(SOURCE_ROOT, file)}:${index + 1}`),
|
|
69
|
+
);
|
|
70
|
+
expect(offenders).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
});
|