@wildo-ai/saas-website 1.1.3 → 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.
Files changed (101) hide show
  1. package/dist/esm/astro/blog-post-bridge.d.ts +14 -20
  2. package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -1
  3. package/dist/esm/astro/blog-post-bridge.js +4 -2
  4. package/dist/esm/astro/blog-post-bridge.js.map +1 -1
  5. package/dist/esm/astro/bridge-runtime.d.ts +30 -20
  6. package/dist/esm/astro/bridge-runtime.d.ts.map +1 -1
  7. package/dist/esm/astro/bridge-runtime.js +13 -3
  8. package/dist/esm/astro/bridge-runtime.js.map +1 -1
  9. package/dist/esm/astro/label-pack-loader.d.ts.map +1 -1
  10. package/dist/esm/astro/label-pack-loader.js +5 -0
  11. package/dist/esm/astro/label-pack-loader.js.map +1 -1
  12. package/dist/esm/config/load-website-config.d.ts.map +1 -1
  13. package/dist/esm/config/load-website-config.js +2 -1
  14. package/dist/esm/config/load-website-config.js.map +1 -1
  15. package/dist/esm/config/wildo-website-config.schemas.d.ts +10 -5
  16. package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -1
  17. package/dist/esm/config/wildo-website-config.schemas.js +10 -5
  18. package/dist/esm/config/wildo-website-config.schemas.js.map +1 -1
  19. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +3 -3
  20. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -1
  21. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +13 -9
  22. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -1
  23. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +11 -0
  24. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -1
  25. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +14 -1
  26. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -1
  27. package/dist/esm/core/consent/WebsiteConsentBanner.d.ts +6 -0
  28. package/dist/esm/core/consent/WebsiteConsentBanner.d.ts.map +1 -0
  29. package/dist/esm/core/consent/WebsiteConsentBanner.js +128 -0
  30. package/dist/esm/core/consent/WebsiteConsentBanner.js.map +1 -0
  31. package/dist/esm/core/consent/WebsiteConsentContext.d.ts +85 -0
  32. package/dist/esm/core/consent/WebsiteConsentContext.d.ts.map +1 -0
  33. package/dist/esm/core/consent/WebsiteConsentContext.js +89 -0
  34. package/dist/esm/core/consent/WebsiteConsentContext.js.map +1 -0
  35. package/dist/esm/core/consent/WebsiteConsentGate.d.ts +27 -0
  36. package/dist/esm/core/consent/WebsiteConsentGate.d.ts.map +1 -0
  37. package/dist/esm/core/consent/WebsiteConsentGate.js +9 -0
  38. package/dist/esm/core/consent/WebsiteConsentGate.js.map +1 -0
  39. package/dist/esm/core/consent/useWebsiteConsent.d.ts +10 -0
  40. package/dist/esm/core/consent/useWebsiteConsent.d.ts.map +1 -0
  41. package/dist/esm/core/consent/useWebsiteConsent.js +17 -0
  42. package/dist/esm/core/consent/useWebsiteConsent.js.map +1 -0
  43. package/dist/esm/core/consent/website-consent-storage.d.ts +22 -0
  44. package/dist/esm/core/consent/website-consent-storage.d.ts.map +1 -0
  45. package/dist/esm/core/consent/website-consent-storage.js +102 -0
  46. package/dist/esm/core/consent/website-consent-storage.js.map +1 -0
  47. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +8 -1
  48. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -1
  49. package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -1
  50. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +1 -2
  51. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -1
  52. package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -1
  53. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts +33 -10
  54. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts.map +1 -1
  55. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js +64 -16
  56. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js.map +1 -1
  57. package/dist/esm/core/layouts/WebsitePageLayout.d.ts +21 -1
  58. package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -1
  59. package/dist/esm/core/layouts/WebsitePageLayout.js +32 -4
  60. package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -1
  61. package/dist/esm/index.d.ts +6 -0
  62. package/dist/esm/index.d.ts.map +1 -1
  63. package/dist/esm/index.js +6 -0
  64. package/dist/esm/index.js.map +1 -1
  65. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts +61 -0
  66. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts.map +1 -0
  67. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js +73 -0
  68. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js.map +1 -0
  69. package/dist/tsconfig.build.tsbuildinfo +1 -1
  70. package/package.json +6 -29
  71. package/src/astro/__tests__/blog-post-bridge.test.tsx +41 -0
  72. package/src/astro/__tests__/bridge-runtime.test.tsx +25 -0
  73. package/src/astro/__tests__/label-pack-loader.test.ts +26 -1
  74. package/src/astro/blog-post-bridge.tsx +18 -22
  75. package/src/astro/bridge-runtime.tsx +45 -23
  76. package/src/astro/label-pack-loader.ts +4 -0
  77. package/src/config/load-website-config.ts +2 -1
  78. package/src/config/wildo-website-config.schemas.ts +10 -5
  79. package/src/core/__tests__/WebsitePageLayout.test.tsx +1 -1
  80. package/src/core/__tests__/website-consent.test.tsx +311 -0
  81. package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +11 -4
  82. package/src/core/anonymous-session/inbound-contact-form.schema.ts +14 -9
  83. package/src/core/anonymous-session/website-anonymous-session-client.ts +15 -1
  84. package/src/core/consent/WebsiteConsentBanner.tsx +222 -0
  85. package/src/core/consent/WebsiteConsentContext.tsx +164 -0
  86. package/src/core/consent/WebsiteConsentGate.tsx +34 -0
  87. package/src/core/consent/useWebsiteConsent.ts +18 -0
  88. package/src/core/consent/website-consent-storage.ts +116 -0
  89. package/src/core/contexts/WebsiteRuntimeContext.tsx +8 -1
  90. package/src/core/external-providers/frontend-provider-registry.website.ts +1 -2
  91. package/src/core/external-providers/useWebsiteProviderScripts.ts +72 -17
  92. package/src/core/layouts/WebsitePageLayout.tsx +66 -3
  93. package/src/index.ts +7 -0
  94. package/src/schemas/label-keys/__tests__/scaffolded-consent-label-pack.parity.test.ts +71 -0
  95. package/src/schemas/label-keys/website-consent-label-keys.schemas.ts +78 -0
  96. package/dist/esm/core/external-providers/provider-scripts.website.d.ts +0 -68
  97. package/dist/esm/core/external-providers/provider-scripts.website.d.ts.map +0 -1
  98. package/dist/esm/core/external-providers/provider-scripts.website.js +0 -99
  99. package/dist/esm/core/external-providers/provider-scripts.website.js.map +0 -1
  100. package/src/core/external-providers/__tests__/provider-scripts.website.test.ts +0 -146
  101. package/src/core/external-providers/provider-scripts.website.ts +0 -126
@@ -0,0 +1,71 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ import { WEBSITE_CONSENT_LABEL_KEYS } from '../website-consent-label-keys.schemas';
8
+
9
+ /**
10
+ * The `add-website` scaffold must author EVERY framework-owned consent key.
11
+ *
12
+ * ## The failure this exists to stop, measured 2026-09-20
13
+ *
14
+ * `WEBSITE_CONSENT_LABEL_KEYS` is what a locale pack must carry, and a pack missing one fails the
15
+ * site's build rather than showing a visitor a raw key — that refusal is deliberate, and its own
16
+ * declaration says why: *"a site that adds analytics tomorrow must not ship it with an unlabelled —
17
+ * or absent — way to refuse."*
18
+ *
19
+ * The key set then GREW by four (the always-active row, and one explanation per purpose). The
20
+ * dogfood site's packs were updated by hand and the SCAFFOLD was not, so every newly composed
21
+ * website would have shipped four keys short — a scaffold that cannot build, produced by a change
22
+ * two packages away, with nothing between the two but somebody remembering.
23
+ *
24
+ * ## Why a parity test rather than a shared constant
25
+ *
26
+ * The template is Handlebars, so no TypeScript can import it and no type can relate the two. That
27
+ * leaves the same choice this repository has made before for exactly this shape
28
+ * (`scaffolded-resource-operations.parity.test.ts`): a COPY of a fact, with a test holding the copy
29
+ * equal to its source. What makes it worth having is the direction of the failure — a missing key is
30
+ * invisible until somebody composes a site, and the person who composes it is not the person who
31
+ * added the key.
32
+ *
33
+ * It asserts EQUALITY, not containment, in both directions: a key the template authors and the
34
+ * framework no longer asks for is a label nobody renders, which is the defect `#54` started from.
35
+ */
36
+ const TEMPLATE_RELATIVE_PATH = 'templates/saas/scenarios/add-website/files/website/src/i18n/en.json.hbs';
37
+
38
+ function findRepositoryRoot(): string {
39
+ let directory = dirname(fileURLToPath(import.meta.url));
40
+ while (!existsSync(join(directory, TEMPLATE_RELATIVE_PATH))) {
41
+ const parent = dirname(directory);
42
+ if (parent === directory) throw new Error(`Could not locate ${TEMPLATE_RELATIVE_PATH} above ${fileURLToPath(import.meta.url)}.`);
43
+ directory = parent;
44
+ }
45
+ return directory;
46
+ }
47
+
48
+ describe('the add-website scaffold’s locale pack', () => {
49
+ it('authors exactly the consent keys the framework requires of every site', () => {
50
+ const template = readFileSync(join(findRepositoryRoot(), TEMPLATE_RELATIVE_PATH), 'utf8');
51
+ const authored = [...template.matchAll(/"(website\.chrome\.consent\.[^"]+)"\s*:/gu)].map((match) => match[1]!);
52
+
53
+ /*
54
+ * Guards the guard: a regex that matched nothing would report perfect agreement with an empty
55
+ * template, which is the shape `--self-test` exists for everywhere else in this repository.
56
+ */
57
+ expect(authored.length, 'the template no longer authors consent keys this test can read').toBeGreaterThan(0);
58
+
59
+ const required = WEBSITE_CONSENT_LABEL_KEYS.map((key) => key as unknown as string);
60
+ expect([...authored].sort()).toEqual([...required].sort());
61
+ });
62
+
63
+ it('gives every authored consent key a non-empty value', () => {
64
+ // A key present with an empty string satisfies a pack validator and shows a visitor nothing —
65
+ // the same outcome as the missing key, arrived at through the check rather than around it.
66
+ const template = readFileSync(join(findRepositoryRoot(), TEMPLATE_RELATIVE_PATH), 'utf8');
67
+ const entries = [...template.matchAll(/"(website\.chrome\.consent\.[^"]+)"\s*:\s*"([^"]*)"/gu)];
68
+ expect(entries.length).toBe(WEBSITE_CONSENT_LABEL_KEYS.length);
69
+ for (const [, key, value] of entries) expect(value!.trim(), `${key!} is authored empty`).not.toBe('');
70
+ });
71
+ });
@@ -0,0 +1,78 @@
1
+ import { ConsentPurpose, CONSENT_PURPOSES } from '@wildo-ai/saas-models/public-runtime';
2
+
3
+ import { WebsiteLabelKeySchema, type WebsiteLabelKey } from './website-label-key.schemas';
4
+
5
+ /**
6
+ * The label keys the framework's visitor consent surface reads (#534).
7
+ *
8
+ * FRAMEWORK-OWNED, like the skip link's key: the consent banner is rendered by `WebsitePageLayout`, not by
9
+ * the site's chrome, so the site does not list these in `chromeLabelKeys`.
10
+ * `composeEffectiveChromeLabelKeys` unions them into the label-pack validator's expected set, so a locale
11
+ * pack missing one fails the build rather than showing a visitor a raw key.
12
+ *
13
+ * They are required of every site, including one that runs no consent-based provider today: the words are
14
+ * cheap, and a site that adds analytics tomorrow must not ship it with an unlabelled — or absent — way to
15
+ * refuse.
16
+ *
17
+ * Purpose names are keyed by the `ConsentPurpose` VALUE and derived from the vocabulary, so a purpose added
18
+ * to it is a missing label in every site's pack at the next build, not a purpose the banner cannot name.
19
+ */
20
+ export enum WebsiteConsentLabelSlot {
21
+ /** Heading of the consent panel. */
22
+ TITLE = 'title',
23
+ /** What the panel is for: optional purposes are off until agreed, each chosen separately, changeable later. */
24
+ DESCRIPTION = 'description',
25
+ /** Agree to every purpose listed. */
26
+ ACCEPT_ALL = 'accept-all',
27
+ /** Agree to none of them — recorded as a choice, so the panel does not return. */
28
+ DECLINE_ALL = 'decline-all',
29
+ /** Save exactly the purposes ticked. */
30
+ SAVE = 'save',
31
+ /** Re-open the panel to change a choice already made. */
32
+ MANAGE = 'manage',
33
+ /**
34
+ * Name of the row covering what runs whatever the visitor chooses.
35
+ *
36
+ * NOT a `ConsentPurpose`, and it must never become one: that vocabulary is what a visitor DECIDES,
37
+ * and this is the thing they are not being asked about. It is shown because a panel listing only
38
+ * the optional purposes cannot answer the question a visitor opened it to answer — what is
39
+ * happening on this site — which is why every specialized consent platform states it.
40
+ */
41
+ ESSENTIAL_TITLE = 'essential-title',
42
+ /** What the always-active row covers, and why its control is checked and cannot be changed. */
43
+ ESSENTIAL_DESCRIPTION = 'essential-description',
44
+ }
45
+
46
+ const CONSENT_LABEL_KEY_PREFIX = 'website.chrome.consent';
47
+
48
+ /** The fully-composed key for one panel slot. */
49
+ export function websiteConsentLabelKey(slot: WebsiteConsentLabelSlot): WebsiteLabelKey {
50
+ return WebsiteLabelKeySchema.parse(`${CONSENT_LABEL_KEY_PREFIX}.${slot}`);
51
+ }
52
+
53
+ /** The fully-composed key naming one consent purpose. */
54
+ export function websiteConsentPurposeLabelKey(purpose: ConsentPurpose): WebsiteLabelKey {
55
+ return WebsiteLabelKeySchema.parse(`${CONSENT_LABEL_KEY_PREFIX}.purpose-${purpose}`);
56
+ }
57
+
58
+ /**
59
+ * The key EXPLAINING one consent purpose — what agreeing to it means for this visitor.
60
+ *
61
+ * The name above answers *what is it called*; this answers *what will happen to me*, and a choice
62
+ * made without the second is not an informed one. "Analytics" beside a tick box is a category, not
63
+ * something a visitor can weigh.
64
+ *
65
+ * Derived from the vocabulary exactly as the name is, so a purpose added to `ConsentPurpose` is a
66
+ * missing label in every site's pack at the next build rather than a row that renders a name with no
67
+ * explanation beneath it.
68
+ */
69
+ export function websiteConsentPurposeDescriptionLabelKey(purpose: ConsentPurpose): WebsiteLabelKey {
70
+ return WebsiteLabelKeySchema.parse(`${CONSENT_LABEL_KEY_PREFIX}.purpose-${purpose}-description`);
71
+ }
72
+
73
+ /** Every framework-owned consent key — what a locale pack must carry. Total over both vocabularies. */
74
+ export const WEBSITE_CONSENT_LABEL_KEYS: readonly WebsiteLabelKey[] = Object.freeze([
75
+ ...Object.values(WebsiteConsentLabelSlot).map(websiteConsentLabelKey),
76
+ ...CONSENT_PURPOSES.map(websiteConsentPurposeLabelKey),
77
+ ...CONSENT_PURPOSES.map(websiteConsentPurposeDescriptionLabelKey),
78
+ ]);
@@ -1,68 +0,0 @@
1
- /**
2
- * Loading what a website provider actually declared, instead of naming it.
3
- *
4
- * `FRONTEND_SDK` was a label on this surface. The layout resolved the analytics,
5
- * error-monitoring and captcha provider refs and stamped them into `data-*`
6
- * attributes on the page root; no consumer loaded anything. A provider could be
7
- * declared, enabled, hydrated — and do nothing at all.
8
- *
9
- * This is the consumer for the DATA half of a provider's executable surface: the
10
- * `scripts` it declares (#332). It serves the case a dynamic `import()` cannot,
11
- * and it is the dominant case on a marketing site: a vendor that ships a
12
- * global-installing `<script>` tag rather than an npm module (reCAPTCHA, tag
13
- * managers, chat widgets, and the CDN distributions of most analytics SDKs).
14
- *
15
- * The npm half — `sdk.activate`, a dynamic import inside the provider package —
16
- * needs the CODE channel, which needs this package to depend on
17
- * `@wildo-ai/external-connectors-public`. That dependency is not added here; see
18
- * the note in the S4a ledger. Nothing below anticipates it: a provider that
19
- * declares both gets its scripts from here and its SDK from there.
20
- *
21
- * ## Client-side injection, and the alternative that was rejected
22
- *
23
- * This is an Astro static site, so the "obvious" home for a third-party tag is
24
- * the built HTML `<head>`. That was rejected for now: the set of live providers
25
- * is per SERVICE and per ENVIRONMENT, resolved from the materialized artifact,
26
- * while the built HTML is one artifact shared across deploys of the same build.
27
- * Baking the tags in would make the build environment-specific — the same class
28
- * of coupling the DATA channel exists to avoid. Injecting from the client keeps
29
- * one build correct everywhere, at the cost of the tag arriving after hydration.
30
- * Revisit if a provider ever needs to run before first paint.
31
- */
32
- import type { ProviderFrontendScriptDeclaration } from '@wildo-ai/saas-models/public-runtime';
33
- import type { FrontendWebsiteProviderRegistry } from './frontend-provider-registry.website';
34
- /**
35
- * Marks a `<script>` this module owns, so a re-render, a client-side route
36
- * change or a second layout mount finds it instead of appending a duplicate.
37
- * A vendor tag loaded twice initialises twice — two analytics clients, two sets
38
- * of events — which is the failure mode this attribute exists to prevent.
39
- */
40
- export declare const PROVIDER_SCRIPT_MARKER_ATTRIBUTE = "data-wildo-provider-script";
41
- export interface DeclaredProviderScript {
42
- readonly providerRef: string;
43
- readonly declaration: ProviderFrontendScriptDeclaration;
44
- }
45
- /**
46
- * Every script declared by the providers live on this surface, in registry
47
- * order, with the declaring ref kept beside each one so a failure can name it.
48
- */
49
- export declare function collectDeclaredProviderScripts(registry: FrontendWebsiteProviderRegistry | undefined): ReadonlyArray<DeclaredProviderScript>;
50
- export interface InjectProviderScriptsResult {
51
- /** Refs whose script this call added. */
52
- readonly injected: ReadonlyArray<string>;
53
- /** Refs whose script was already present, so nothing was added. */
54
- readonly alreadyPresent: ReadonlyArray<string>;
55
- }
56
- /**
57
- * Adds each declared script to the document, once.
58
- *
59
- * Idempotent by `src`: a declaration whose element is already in the document is
60
- * skipped. Callers may therefore run this on every mount without tracking state.
61
- *
62
- * `async` defaults to TRUE when the declaration says nothing. A third-party tag
63
- * that blocks parsing is the single most common way an analytics provider costs
64
- * a marketing site its paint metrics, and a provider author who has not thought
65
- * about it should get the safe answer rather than the blocking one.
66
- */
67
- export declare function injectProviderScripts(scripts: ReadonlyArray<DeclaredProviderScript>, targetDocument: Document): InjectProviderScriptsResult;
68
- //# sourceMappingURL=provider-scripts.website.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"provider-scripts.website.d.ts","sourceRoot":"","sources":["../../../../../src/core/external-providers/provider-scripts.website.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,sCAAsC,CAAC;AAC9F,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,sCAAsC,CAAC;AAE5F;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,+BAA+B,CAAC;AAE7E,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,iCAAiC,CAAC;CACzD;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,+BAA+B,GAAG,SAAS,GACpD,aAAa,CAAC,sBAAsB,CAAC,CAUvC;AAED,MAAM,WAAW,2BAA2B;IAC1C,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACzC,mEAAmE;IACnE,QAAQ,CAAC,cAAc,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAC9C,cAAc,EAAE,QAAQ,GACvB,2BAA2B,CAsC7B"}
@@ -1,99 +0,0 @@
1
- /**
2
- * Loading what a website provider actually declared, instead of naming it.
3
- *
4
- * `FRONTEND_SDK` was a label on this surface. The layout resolved the analytics,
5
- * error-monitoring and captcha provider refs and stamped them into `data-*`
6
- * attributes on the page root; no consumer loaded anything. A provider could be
7
- * declared, enabled, hydrated — and do nothing at all.
8
- *
9
- * This is the consumer for the DATA half of a provider's executable surface: the
10
- * `scripts` it declares (#332). It serves the case a dynamic `import()` cannot,
11
- * and it is the dominant case on a marketing site: a vendor that ships a
12
- * global-installing `<script>` tag rather than an npm module (reCAPTCHA, tag
13
- * managers, chat widgets, and the CDN distributions of most analytics SDKs).
14
- *
15
- * The npm half — `sdk.activate`, a dynamic import inside the provider package —
16
- * needs the CODE channel, which needs this package to depend on
17
- * `@wildo-ai/external-connectors-public`. That dependency is not added here; see
18
- * the note in the S4a ledger. Nothing below anticipates it: a provider that
19
- * declares both gets its scripts from here and its SDK from there.
20
- *
21
- * ## Client-side injection, and the alternative that was rejected
22
- *
23
- * This is an Astro static site, so the "obvious" home for a third-party tag is
24
- * the built HTML `<head>`. That was rejected for now: the set of live providers
25
- * is per SERVICE and per ENVIRONMENT, resolved from the materialized artifact,
26
- * while the built HTML is one artifact shared across deploys of the same build.
27
- * Baking the tags in would make the build environment-specific — the same class
28
- * of coupling the DATA channel exists to avoid. Injecting from the client keeps
29
- * one build correct everywhere, at the cost of the tag arriving after hydration.
30
- * Revisit if a provider ever needs to run before first paint.
31
- */
32
- /**
33
- * Marks a `<script>` this module owns, so a re-render, a client-side route
34
- * change or a second layout mount finds it instead of appending a duplicate.
35
- * A vendor tag loaded twice initialises twice — two analytics clients, two sets
36
- * of events — which is the failure mode this attribute exists to prevent.
37
- */
38
- export const PROVIDER_SCRIPT_MARKER_ATTRIBUTE = 'data-wildo-provider-script';
39
- /**
40
- * Every script declared by the providers live on this surface, in registry
41
- * order, with the declaring ref kept beside each one so a failure can name it.
42
- */
43
- export function collectDeclaredProviderScripts(registry) {
44
- if (registry === undefined)
45
- return [];
46
- const collected = [];
47
- for (const provider of registry.modules.values()) {
48
- for (const declaration of provider.scripts ?? []) {
49
- collected.push({ providerRef: provider.metadata.ref, declaration });
50
- }
51
- }
52
- return collected;
53
- }
54
- /**
55
- * Adds each declared script to the document, once.
56
- *
57
- * Idempotent by `src`: a declaration whose element is already in the document is
58
- * skipped. Callers may therefore run this on every mount without tracking state.
59
- *
60
- * `async` defaults to TRUE when the declaration says nothing. A third-party tag
61
- * that blocks parsing is the single most common way an analytics provider costs
62
- * a marketing site its paint metrics, and a provider author who has not thought
63
- * about it should get the safe answer rather than the blocking one.
64
- */
65
- export function injectProviderScripts(scripts, targetDocument) {
66
- const injected = [];
67
- const alreadyPresent = [];
68
- for (const { providerRef, declaration } of scripts) {
69
- // Matched by reading the attribute rather than by a selector carrying the
70
- // URL: a `src` can contain characters a CSS selector would have to escape,
71
- // and an escaping bug here would show up as a duplicated vendor tag — two
72
- // analytics clients, two sets of events — rather than as an error.
73
- const existing = [
74
- ...targetDocument.querySelectorAll(`script[${PROVIDER_SCRIPT_MARKER_ATTRIBUTE}]`),
75
- ].some((element) => element.getAttribute('src') === declaration.src);
76
- if (existing) {
77
- alreadyPresent.push(providerRef);
78
- continue;
79
- }
80
- const element = targetDocument.createElement('script');
81
- element.src = declaration.src;
82
- element.setAttribute(PROVIDER_SCRIPT_MARKER_ATTRIBUTE, providerRef);
83
- if (declaration.integrity !== undefined) {
84
- element.integrity = declaration.integrity;
85
- }
86
- if (declaration.crossOrigin !== undefined) {
87
- element.crossOrigin = declaration.crossOrigin;
88
- }
89
- // Both flags are set explicitly rather than left to the element's defaults:
90
- // a dynamically created script is `async` by default, so `defer: true` alone
91
- // would be silently ignored without the paired `async = false`.
92
- element.async = declaration.async ?? declaration.defer !== true;
93
- element.defer = declaration.defer ?? false;
94
- targetDocument.head.appendChild(element);
95
- injected.push(providerRef);
96
- }
97
- return { injected, alreadyPresent };
98
- }
99
- //# sourceMappingURL=provider-scripts.website.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"provider-scripts.website.js","sourceRoot":"","sources":["../../../../../src/core/external-providers/provider-scripts.website.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,4BAA4B,CAAC;AAO7E;;;GAGG;AACH,MAAM,UAAU,8BAA8B,CAC5C,QAAqD;IAErD,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAEtC,MAAM,SAAS,GAA6B,EAAE,CAAC;IAC/C,KAAK,MAAM,QAAQ,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QACjD,KAAK,MAAM,WAAW,IAAI,QAAQ,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;YACjD,SAAS,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,EAAE,WAAW,EAAE,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AASD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAA8C,EAC9C,cAAwB;IAExB,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,cAAc,GAAa,EAAE,CAAC;IAEpC,KAAK,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,IAAI,OAAO,EAAE,CAAC;QACnD,0EAA0E;QAC1E,2EAA2E;QAC3E,0EAA0E;QAC1E,mEAAmE;QACnE,MAAM,QAAQ,GAAG;YACf,GAAG,cAAc,CAAC,gBAAgB,CAAC,UAAU,gCAAgC,GAAG,CAAC;SAClF,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,WAAW,CAAC,GAAG,CAAC,CAAC;QACrE,IAAI,QAAQ,EAAE,CAAC;YACb,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YACjC,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,cAAc,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QACvD,OAAO,CAAC,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC;QAC9B,OAAO,CAAC,YAAY,CAAC,gCAAgC,EAAE,WAAW,CAAC,CAAC;QAEpE,IAAI,WAAW,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YACxC,OAAO,CAAC,SAAS,GAAG,WAAW,CAAC,SAAS,CAAC;QAC5C,CAAC;QACD,IAAI,WAAW,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YAC1C,OAAO,CAAC,WAAW,GAAG,WAAW,CAAC,WAAW,CAAC;QAChD,CAAC;QACD,4EAA4E;QAC5E,6EAA6E;QAC7E,gEAAgE;QAChE,OAAO,CAAC,KAAK,GAAG,WAAW,CAAC,KAAK,IAAI,WAAW,CAAC,KAAK,KAAK,IAAI,CAAC;QAChE,OAAO,CAAC,KAAK,GAAG,WAAW,CAAC,KAAK,IAAI,KAAK,CAAC;QAE3C,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACzC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;AACtC,CAAC","sourcesContent":["/**\n * Loading what a website provider actually declared, instead of naming it.\n *\n * `FRONTEND_SDK` was a label on this surface. The layout resolved the analytics,\n * error-monitoring and captcha provider refs and stamped them into `data-*`\n * attributes on the page root; no consumer loaded anything. A provider could be\n * declared, enabled, hydrated — and do nothing at all.\n *\n * This is the consumer for the DATA half of a provider's executable surface: the\n * `scripts` it declares (#332). It serves the case a dynamic `import()` cannot,\n * and it is the dominant case on a marketing site: a vendor that ships a\n * global-installing `<script>` tag rather than an npm module (reCAPTCHA, tag\n * managers, chat widgets, and the CDN distributions of most analytics SDKs).\n *\n * The npm half — `sdk.activate`, a dynamic import inside the provider package —\n * needs the CODE channel, which needs this package to depend on\n * `@wildo-ai/external-connectors-public`. That dependency is not added here; see\n * the note in the S4a ledger. Nothing below anticipates it: a provider that\n * declares both gets its scripts from here and its SDK from there.\n *\n * ## Client-side injection, and the alternative that was rejected\n *\n * This is an Astro static site, so the \"obvious\" home for a third-party tag is\n * the built HTML `<head>`. That was rejected for now: the set of live providers\n * is per SERVICE and per ENVIRONMENT, resolved from the materialized artifact,\n * while the built HTML is one artifact shared across deploys of the same build.\n * Baking the tags in would make the build environment-specific — the same class\n * of coupling the DATA channel exists to avoid. Injecting from the client keeps\n * one build correct everywhere, at the cost of the tag arriving after hydration.\n * Revisit if a provider ever needs to run before first paint.\n */\n\nimport type { ProviderFrontendScriptDeclaration } from '@wildo-ai/saas-models/public-runtime';\nimport type { FrontendWebsiteProviderRegistry } from './frontend-provider-registry.website';\n\n/**\n * Marks a `<script>` this module owns, so a re-render, a client-side route\n * change or a second layout mount finds it instead of appending a duplicate.\n * A vendor tag loaded twice initialises twice — two analytics clients, two sets\n * of events — which is the failure mode this attribute exists to prevent.\n */\nexport const PROVIDER_SCRIPT_MARKER_ATTRIBUTE = 'data-wildo-provider-script';\n\nexport interface DeclaredProviderScript {\n readonly providerRef: string;\n readonly declaration: ProviderFrontendScriptDeclaration;\n}\n\n/**\n * Every script declared by the providers live on this surface, in registry\n * order, with the declaring ref kept beside each one so a failure can name it.\n */\nexport function collectDeclaredProviderScripts(\n registry: FrontendWebsiteProviderRegistry | undefined,\n): ReadonlyArray<DeclaredProviderScript> {\n if (registry === undefined) return [];\n\n const collected: DeclaredProviderScript[] = [];\n for (const provider of registry.modules.values()) {\n for (const declaration of provider.scripts ?? []) {\n collected.push({ providerRef: provider.metadata.ref, declaration });\n }\n }\n return collected;\n}\n\nexport interface InjectProviderScriptsResult {\n /** Refs whose script this call added. */\n readonly injected: ReadonlyArray<string>;\n /** Refs whose script was already present, so nothing was added. */\n readonly alreadyPresent: ReadonlyArray<string>;\n}\n\n/**\n * Adds each declared script to the document, once.\n *\n * Idempotent by `src`: a declaration whose element is already in the document is\n * skipped. Callers may therefore run this on every mount without tracking state.\n *\n * `async` defaults to TRUE when the declaration says nothing. A third-party tag\n * that blocks parsing is the single most common way an analytics provider costs\n * a marketing site its paint metrics, and a provider author who has not thought\n * about it should get the safe answer rather than the blocking one.\n */\nexport function injectProviderScripts(\n scripts: ReadonlyArray<DeclaredProviderScript>,\n targetDocument: Document,\n): InjectProviderScriptsResult {\n const injected: string[] = [];\n const alreadyPresent: string[] = [];\n\n for (const { providerRef, declaration } of scripts) {\n // Matched by reading the attribute rather than by a selector carrying the\n // URL: a `src` can contain characters a CSS selector would have to escape,\n // and an escaping bug here would show up as a duplicated vendor tag — two\n // analytics clients, two sets of events — rather than as an error.\n const existing = [\n ...targetDocument.querySelectorAll(`script[${PROVIDER_SCRIPT_MARKER_ATTRIBUTE}]`),\n ].some((element) => element.getAttribute('src') === declaration.src);\n if (existing) {\n alreadyPresent.push(providerRef);\n continue;\n }\n\n const element = targetDocument.createElement('script');\n element.src = declaration.src;\n element.setAttribute(PROVIDER_SCRIPT_MARKER_ATTRIBUTE, providerRef);\n\n if (declaration.integrity !== undefined) {\n element.integrity = declaration.integrity;\n }\n if (declaration.crossOrigin !== undefined) {\n element.crossOrigin = declaration.crossOrigin;\n }\n // Both flags are set explicitly rather than left to the element's defaults:\n // a dynamically created script is `async` by default, so `defer: true` alone\n // would be silently ignored without the paired `async = false`.\n element.async = declaration.async ?? declaration.defer !== true;\n element.defer = declaration.defer ?? false;\n\n targetDocument.head.appendChild(element);\n injected.push(providerRef);\n }\n\n return { injected, alreadyPresent };\n}\n"]}
@@ -1,146 +0,0 @@
1
- import { beforeEach, describe, expect, it } from 'vitest';
2
- import {
3
- ENGINE_PROVIDER_ORIGIN,
4
- ProviderFrontendScriptCrossOrigin,
5
- ProviderTier,
6
- defineProviderMetadata,
7
- type FrontendProvidersBlock,
8
- } from '@wildo-ai/saas-models/public-runtime';
9
- import { createFrontendWebsiteProviderRegistry } from '../frontend-provider-registry.website';
10
- import {
11
- PROVIDER_SCRIPT_MARKER_ATTRIBUTE,
12
- collectDeclaredProviderScripts,
13
- injectProviderScripts,
14
- } from '../provider-scripts.website';
15
-
16
- function registryWith(scriptsByRef: Record<string, unknown[]>): ReturnType<typeof createFrontendWebsiteProviderRegistry> {
17
- const providers = Object.fromEntries(
18
- Object.entries(scriptsByRef).map(([ref, scripts]) => [
19
- ref,
20
- {
21
- metadata: defineProviderMetadata({
22
- ref,
23
- packageName: `@wildo-ai/provider-${ref}`,
24
- tier: ProviderTier.CATALOGUE,
25
- origin: ENGINE_PROVIDER_ORIGIN,
26
- }),
27
- providerCapabilities: [],
28
- protocols: ['FRONTEND_SDK'],
29
- scripts,
30
- },
31
- ]),
32
- );
33
- return createFrontendWebsiteProviderRegistry({ entries: { providers } as unknown as FrontendProvidersBlock });
34
- }
35
-
36
- function injectedScripts(): HTMLScriptElement[] {
37
- return [...document.querySelectorAll<HTMLScriptElement>(`script[${PROVIDER_SCRIPT_MARKER_ATTRIBUTE}]`)];
38
- }
39
-
40
- describe('website provider scripts', () => {
41
- beforeEach(() => {
42
- document.head.innerHTML = '';
43
- });
44
-
45
- it('collects every declared script with the ref that declared it', () => {
46
- const collected = collectDeclaredProviderScripts(
47
- registryWith({
48
- analytics: [{ src: 'https://cdn.analytics.test/a.js' }],
49
- captcha: [{ src: 'https://captcha.test/api.js' }, { src: 'https://captcha.test/extra.js' }],
50
- }),
51
- );
52
-
53
- expect(collected.map((entry) => `${entry.providerRef}:${entry.declaration.src}`)).toEqual([
54
- 'analytics:https://cdn.analytics.test/a.js',
55
- 'captcha:https://captcha.test/api.js',
56
- 'captcha:https://captcha.test/extra.js',
57
- ]);
58
- });
59
-
60
- it('collects nothing when no registry is hydrated at all', () => {
61
- expect(collectDeclaredProviderScripts(undefined)).toEqual([]);
62
- });
63
-
64
- it('injects a declared script into the document head, carrying its SRI and crossorigin', () => {
65
- const result = injectProviderScripts(
66
- collectDeclaredProviderScripts(
67
- registryWith({
68
- analytics: [
69
- {
70
- src: 'https://cdn.analytics.test/a.js',
71
- integrity: 'sha384-abc',
72
- crossOrigin: ProviderFrontendScriptCrossOrigin.ANONYMOUS,
73
- },
74
- ],
75
- }),
76
- ),
77
- document,
78
- );
79
-
80
- const [element] = injectedScripts();
81
- expect(result.injected).toEqual(['analytics']);
82
- expect(element.src).toBe('https://cdn.analytics.test/a.js');
83
- expect(element.integrity).toBe('sha384-abc');
84
- expect(element.crossOrigin).toBe('anonymous');
85
- expect(element.getAttribute(PROVIDER_SCRIPT_MARKER_ATTRIBUTE)).toBe('analytics');
86
- });
87
-
88
- it('defaults to async, because a blocking third-party tag is how a marketing site loses its paint', () => {
89
- injectProviderScripts(
90
- collectDeclaredProviderScripts(registryWith({ analytics: [{ src: 'https://cdn.analytics.test/a.js' }] })),
91
- document,
92
- );
93
-
94
- expect(injectedScripts()[0].async).toBe(true);
95
- expect(injectedScripts()[0].defer).toBe(false);
96
- });
97
-
98
- it('honours defer by turning async OFF, which a dynamically created script does not do on its own', () => {
99
- // A script element created by script is async by default, so `defer: true`
100
- // alone would be silently ignored — the exact shape of a declaration that
101
- // looks respected and is not.
102
- injectProviderScripts(
103
- collectDeclaredProviderScripts(
104
- registryWith({ analytics: [{ src: 'https://cdn.analytics.test/a.js', defer: true }] }),
105
- ),
106
- document,
107
- );
108
-
109
- expect(injectedScripts()[0].async).toBe(false);
110
- expect(injectedScripts()[0].defer).toBe(true);
111
- });
112
-
113
- it('is idempotent: a second call adds nothing, so a remount cannot start a vendor twice', () => {
114
- const scripts = collectDeclaredProviderScripts(
115
- registryWith({ analytics: [{ src: 'https://cdn.analytics.test/a.js' }] }),
116
- );
117
-
118
- injectProviderScripts(scripts, document);
119
- const second = injectProviderScripts(scripts, document);
120
-
121
- expect(injectedScripts()).toHaveLength(1);
122
- expect(second.injected).toEqual([]);
123
- expect(second.alreadyPresent).toEqual(['analytics']);
124
- });
125
-
126
- it('matches an existing script by its src even when the URL carries query characters', () => {
127
- const scripts = collectDeclaredProviderScripts(
128
- registryWith({ captcha: [{ src: 'https://captcha.test/api.js?render=abc&hl=fr' }] }),
129
- );
130
-
131
- injectProviderScripts(scripts, document);
132
- injectProviderScripts(scripts, document);
133
-
134
- expect(injectedScripts()).toHaveLength(1);
135
- });
136
-
137
- it('adds nothing for a provider that declares no scripts', () => {
138
- const result = injectProviderScripts(
139
- collectDeclaredProviderScripts(registryWith({ analytics: [] })),
140
- document,
141
- );
142
-
143
- expect(result.injected).toEqual([]);
144
- expect(injectedScripts()).toHaveLength(0);
145
- });
146
- });
@@ -1,126 +0,0 @@
1
- /**
2
- * Loading what a website provider actually declared, instead of naming it.
3
- *
4
- * `FRONTEND_SDK` was a label on this surface. The layout resolved the analytics,
5
- * error-monitoring and captcha provider refs and stamped them into `data-*`
6
- * attributes on the page root; no consumer loaded anything. A provider could be
7
- * declared, enabled, hydrated — and do nothing at all.
8
- *
9
- * This is the consumer for the DATA half of a provider's executable surface: the
10
- * `scripts` it declares (#332). It serves the case a dynamic `import()` cannot,
11
- * and it is the dominant case on a marketing site: a vendor that ships a
12
- * global-installing `<script>` tag rather than an npm module (reCAPTCHA, tag
13
- * managers, chat widgets, and the CDN distributions of most analytics SDKs).
14
- *
15
- * The npm half — `sdk.activate`, a dynamic import inside the provider package —
16
- * needs the CODE channel, which needs this package to depend on
17
- * `@wildo-ai/external-connectors-public`. That dependency is not added here; see
18
- * the note in the S4a ledger. Nothing below anticipates it: a provider that
19
- * declares both gets its scripts from here and its SDK from there.
20
- *
21
- * ## Client-side injection, and the alternative that was rejected
22
- *
23
- * This is an Astro static site, so the "obvious" home for a third-party tag is
24
- * the built HTML `<head>`. That was rejected for now: the set of live providers
25
- * is per SERVICE and per ENVIRONMENT, resolved from the materialized artifact,
26
- * while the built HTML is one artifact shared across deploys of the same build.
27
- * Baking the tags in would make the build environment-specific — the same class
28
- * of coupling the DATA channel exists to avoid. Injecting from the client keeps
29
- * one build correct everywhere, at the cost of the tag arriving after hydration.
30
- * Revisit if a provider ever needs to run before first paint.
31
- */
32
-
33
- import type { ProviderFrontendScriptDeclaration } from '@wildo-ai/saas-models/public-runtime';
34
- import type { FrontendWebsiteProviderRegistry } from './frontend-provider-registry.website';
35
-
36
- /**
37
- * Marks a `<script>` this module owns, so a re-render, a client-side route
38
- * change or a second layout mount finds it instead of appending a duplicate.
39
- * A vendor tag loaded twice initialises twice — two analytics clients, two sets
40
- * of events — which is the failure mode this attribute exists to prevent.
41
- */
42
- export const PROVIDER_SCRIPT_MARKER_ATTRIBUTE = 'data-wildo-provider-script';
43
-
44
- export interface DeclaredProviderScript {
45
- readonly providerRef: string;
46
- readonly declaration: ProviderFrontendScriptDeclaration;
47
- }
48
-
49
- /**
50
- * Every script declared by the providers live on this surface, in registry
51
- * order, with the declaring ref kept beside each one so a failure can name it.
52
- */
53
- export function collectDeclaredProviderScripts(
54
- registry: FrontendWebsiteProviderRegistry | undefined,
55
- ): ReadonlyArray<DeclaredProviderScript> {
56
- if (registry === undefined) return [];
57
-
58
- const collected: DeclaredProviderScript[] = [];
59
- for (const provider of registry.modules.values()) {
60
- for (const declaration of provider.scripts ?? []) {
61
- collected.push({ providerRef: provider.metadata.ref, declaration });
62
- }
63
- }
64
- return collected;
65
- }
66
-
67
- export interface InjectProviderScriptsResult {
68
- /** Refs whose script this call added. */
69
- readonly injected: ReadonlyArray<string>;
70
- /** Refs whose script was already present, so nothing was added. */
71
- readonly alreadyPresent: ReadonlyArray<string>;
72
- }
73
-
74
- /**
75
- * Adds each declared script to the document, once.
76
- *
77
- * Idempotent by `src`: a declaration whose element is already in the document is
78
- * skipped. Callers may therefore run this on every mount without tracking state.
79
- *
80
- * `async` defaults to TRUE when the declaration says nothing. A third-party tag
81
- * that blocks parsing is the single most common way an analytics provider costs
82
- * a marketing site its paint metrics, and a provider author who has not thought
83
- * about it should get the safe answer rather than the blocking one.
84
- */
85
- export function injectProviderScripts(
86
- scripts: ReadonlyArray<DeclaredProviderScript>,
87
- targetDocument: Document,
88
- ): InjectProviderScriptsResult {
89
- const injected: string[] = [];
90
- const alreadyPresent: string[] = [];
91
-
92
- for (const { providerRef, declaration } of scripts) {
93
- // Matched by reading the attribute rather than by a selector carrying the
94
- // URL: a `src` can contain characters a CSS selector would have to escape,
95
- // and an escaping bug here would show up as a duplicated vendor tag — two
96
- // analytics clients, two sets of events — rather than as an error.
97
- const existing = [
98
- ...targetDocument.querySelectorAll(`script[${PROVIDER_SCRIPT_MARKER_ATTRIBUTE}]`),
99
- ].some((element) => element.getAttribute('src') === declaration.src);
100
- if (existing) {
101
- alreadyPresent.push(providerRef);
102
- continue;
103
- }
104
-
105
- const element = targetDocument.createElement('script');
106
- element.src = declaration.src;
107
- element.setAttribute(PROVIDER_SCRIPT_MARKER_ATTRIBUTE, providerRef);
108
-
109
- if (declaration.integrity !== undefined) {
110
- element.integrity = declaration.integrity;
111
- }
112
- if (declaration.crossOrigin !== undefined) {
113
- element.crossOrigin = declaration.crossOrigin;
114
- }
115
- // Both flags are set explicitly rather than left to the element's defaults:
116
- // a dynamically created script is `async` by default, so `defer: true` alone
117
- // would be silently ignored without the paired `async = false`.
118
- element.async = declaration.async ?? declaration.defer !== true;
119
- element.defer = declaration.defer ?? false;
120
-
121
- targetDocument.head.appendChild(element);
122
- injected.push(providerRef);
123
- }
124
-
125
- return { injected, alreadyPresent };
126
- }