@jwiedeman/gtm-kit-astro 1.1.6 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -319,6 +319,182 @@ Page views are automatically tracked on:
319
319
 
320
320
  ---
321
321
 
322
+ ## Island Architecture Compatibility
323
+
324
+ GTM Kit is fully compatible with Astro's [island architecture](https://docs.astro.build/en/concepts/islands/) (partial hydration). Here's how it works:
325
+
326
+ ### How GTM Integrates with Islands
327
+
328
+ ```
329
+ ┌─────────────────────────────────────────────────────────┐
330
+ │ Static HTML Shell │
331
+ │ ┌─────────────────────────────────────────────────┐ │
332
+ │ │ <head> │ │
333
+ │ │ <GtmHead /> ← Injects GTM script (static) │ │
334
+ │ └─────────────────────────────────────────────────┘ │
335
+ │ ┌─────────────────────────────────────────────────┐ │
336
+ │ │ <body> │ │
337
+ │ │ ┌───────────────┐ ┌───────────────────┐ │ │
338
+ │ │ │ Static Content│ │ 🏝️ Interactive │ │ │
339
+ │ │ │ │ │ Island │ │ │
340
+ │ │ │ │ │ │ │ │
341
+ │ │ │ │ │ push({ event }) │→ dataLayer
342
+ │ │ └───────────────┘ └───────────────────┘ │ │
343
+ │ └─────────────────────────────────────────────────┘ │
344
+ └─────────────────────────────────────────────────────────┘
345
+ ```
346
+
347
+ ### Key Concepts
348
+
349
+ 1. **GTM loads globally**: The `<GtmHead />` component injects GTM scripts into the static HTML shell. This means GTM is available to all islands without re-initialization.
350
+
351
+ 2. **Islands can push events**: Any hydrated island (React, Vue, Svelte, etc.) can import and use the `push()` function to send events.
352
+
353
+ 3. **No hydration mismatch**: Since GTM initialization happens in a `<script>` tag (not a component), there's no hydration mismatch concern.
354
+
355
+ 4. **View Transitions work**: Page tracking integrates seamlessly with Astro's View Transitions for SPA-like navigation.
356
+
357
+ ### Using GTM in Framework Islands
358
+
359
+ #### React Island
360
+
361
+ ```tsx
362
+ // src/components/AddToCart.tsx
363
+ import { push } from '@jwiedeman/gtm-kit-astro';
364
+
365
+ export default function AddToCart({ product }) {
366
+ const handleClick = () => {
367
+ push({
368
+ event: 'add_to_cart',
369
+ ecommerce: {
370
+ items: [
371
+ {
372
+ item_id: product.id,
373
+ item_name: product.name,
374
+ price: product.price
375
+ }
376
+ ]
377
+ }
378
+ });
379
+ };
380
+
381
+ return <button onClick={handleClick}>Add to Cart</button>;
382
+ }
383
+ ```
384
+
385
+ ```astro
386
+ ---
387
+ // src/pages/product.astro
388
+ import { GtmHead } from '@jwiedeman/gtm-kit-astro/components';
389
+ import AddToCart from '../components/AddToCart';
390
+ ---
391
+ <html>
392
+ <head>
393
+ <GtmHead containers="GTM-XXXXXX" enablePageTracking />
394
+ </head>
395
+ <body>
396
+ <h1>Product Page</h1>
397
+ <!-- Static content -->
398
+ <p>Product description...</p>
399
+
400
+ <!-- Interactive island -->
401
+ <AddToCart client:load product={{ id: 'SKU-001', name: 'Widget', price: 29.99 }} />
402
+ </body>
403
+ </html>
404
+ ```
405
+
406
+ #### Vue Island
407
+
408
+ ```vue
409
+ <!-- src/components/NewsletterForm.vue -->
410
+ <script setup>
411
+ import { push } from '@jwiedeman/gtm-kit-astro';
412
+
413
+ const handleSubmit = () => {
414
+ push({ event: 'newsletter_signup', method: 'footer_form' });
415
+ };
416
+ </script>
417
+
418
+ <template>
419
+ <form @submit.prevent="handleSubmit">
420
+ <input type="email" placeholder="Email" />
421
+ <button type="submit">Subscribe</button>
422
+ </form>
423
+ </template>
424
+ ```
425
+
426
+ #### Svelte Island
427
+
428
+ ```svelte
429
+ <!-- src/components/VideoPlayer.svelte -->
430
+ <script>
431
+ import { push } from '@jwiedeman/gtm-kit-astro';
432
+
433
+ export let videoId;
434
+
435
+ function onPlay() {
436
+ push({ event: 'video_start', video_id: videoId });
437
+ }
438
+ </script>
439
+
440
+ <video on:play={onPlay}>
441
+ <source src="/videos/{videoId}.mp4" />
442
+ </video>
443
+ ```
444
+
445
+ ### Hydration Directive Impact
446
+
447
+ Different Astro hydration directives affect when your island can push events:
448
+
449
+ | Directive | When GTM Events Work |
450
+ | ---------------- | ------------------------------------------------ |
451
+ | `client:load` | Immediately after page load |
452
+ | `client:idle` | After page is interactive (requestIdleCallback) |
453
+ | `client:visible` | When island scrolls into viewport |
454
+ | `client:media` | When media query matches |
455
+ | `client:only` | Immediately (no server render, client-side only) |
456
+
457
+ ### Best Practices
458
+
459
+ 1. **Initialize once in layout**: Place `<GtmHead />` in your base layout, not in individual pages.
460
+
461
+ 2. **Don't re-initialize in islands**: The `push()` function works without re-initializing GTM.
462
+
463
+ 3. **Use `client:load` for critical tracking**: For e-commerce or important events, use `client:load` to ensure tracking starts immediately.
464
+
465
+ 4. **Defer non-critical islands**: Use `client:idle` or `client:visible` for analytics-only components to improve page performance.
466
+
467
+ 5. **Handle edge cases**: If an island might render before GTM loads, events are safely queued.
468
+
469
+ ### Static Site Generation (SSG)
470
+
471
+ GTM Kit works perfectly with Astro's static output:
472
+
473
+ ```javascript
474
+ // astro.config.mjs
475
+ export default defineConfig({
476
+ output: 'static' // Default - fully static
477
+ });
478
+ ```
479
+
480
+ All GTM initialization happens client-side, so pre-rendered pages work correctly.
481
+
482
+ ### Server-Side Rendering (SSR)
483
+
484
+ For SSR/hybrid mode, GTM Kit also works seamlessly:
485
+
486
+ ```javascript
487
+ // astro.config.mjs
488
+ export default defineConfig({
489
+ output: 'server', // or 'hybrid'
490
+ adapter: node() // or any adapter
491
+ });
492
+ ```
493
+
494
+ The `<GtmHead />` component generates static script tags that work identically in both modes.
495
+
496
+ ---
497
+
322
498
  ## Requirements
323
499
 
324
500
  - Astro 3.0+, 4.0+, or 5.0+
@@ -2,30 +2,109 @@
2
2
 
3
3
  var gtmKit = require('@jwiedeman/gtm-kit');
4
4
 
5
- var w=r=>/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(r);var C=r=>Object.fromEntries(Object.entries(r).filter(([,e])=>e!=null)),x=r=>{let{containers:e,host:o=gtmKit.DEFAULT_GTM_HOST,defaultQueryParams:c,scriptAttributes:i,dataLayerName:g=gtmKit.DEFAULT_DATA_LAYER_NAME}=r,n=gtmKit.normalizeContainers(e);if(!n.length)throw new Error("At least one GTM container is required.");return n.map(t=>{if(!t.id)throw new Error("Container id is required.");let p={...c,...t.queryParams},l=gtmKit.buildGtmScriptUrl(o,t.id,p,g),{async:a,defer:u,nonce:s,...d}=i!=null?i:{};return {id:t.id,src:l,async:a!=null?a:!0,defer:u,nonce:s,attributes:C(d)}})},E=r=>{let{containers:e,host:o=gtmKit.DEFAULT_GTM_HOST,defaultQueryParams:c,iframeAttributes:i,dataLayerName:g=gtmKit.DEFAULT_DATA_LAYER_NAME}=r,n=gtmKit.normalizeContainers(e);if(!n.length)throw new Error("At least one GTM container is required.");return n.map(t=>{if(!t.id)throw new Error("Container id is required.");let p={...c,...t.queryParams},l=gtmKit.buildGtmNoscriptUrl(o,t.id,p,g),a={...gtmKit.DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,...i},u=Object.fromEntries(Object.entries(a).filter(([,s])=>s!=null).map(([s,d])=>[s,String(d)]));return {id:t.id,src:l,attributes:u}})},h=(r=gtmKit.DEFAULT_DATA_LAYER_NAME)=>{if(!w(r))throw new Error(`Invalid dataLayerName: "${r}". Must be a valid JavaScript identifier (letters, digits, $, _ only, cannot start with a digit).`);return `window.${r}=window.${r}||[];`};
5
+ // src/components/helpers.ts
6
+ var isValidJsIdentifier = (value) => {
7
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);
8
+ };
9
+ var filterNullish = (obj) => Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null));
10
+ var generateScriptTags = (config) => {
11
+ const {
12
+ containers,
13
+ host = gtmKit.DEFAULT_GTM_HOST,
14
+ defaultQueryParams,
15
+ scriptAttributes,
16
+ dataLayerName = gtmKit.DEFAULT_DATA_LAYER_NAME
17
+ } = config;
18
+ const normalized = gtmKit.normalizeContainers(containers);
19
+ if (!normalized.length) {
20
+ throw new Error("At least one GTM container is required.");
21
+ }
22
+ return normalized.map((container) => {
23
+ if (!container.id) {
24
+ throw new Error("Container id is required.");
25
+ }
26
+ const params = {
27
+ ...defaultQueryParams,
28
+ ...container.queryParams
29
+ };
30
+ const src = gtmKit.buildGtmScriptUrl(host, container.id, params, dataLayerName);
31
+ const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes != null ? scriptAttributes : {};
32
+ return {
33
+ id: container.id,
34
+ src,
35
+ async: asyncAttr != null ? asyncAttr : true,
36
+ defer,
37
+ nonce,
38
+ attributes: filterNullish(restAttributes)
39
+ };
40
+ });
41
+ };
42
+ var generateNoscriptTags = (config) => {
43
+ const {
44
+ containers,
45
+ host = gtmKit.DEFAULT_GTM_HOST,
46
+ defaultQueryParams,
47
+ iframeAttributes,
48
+ dataLayerName = gtmKit.DEFAULT_DATA_LAYER_NAME
49
+ } = config;
50
+ const normalized = gtmKit.normalizeContainers(containers);
51
+ if (!normalized.length) {
52
+ throw new Error("At least one GTM container is required.");
53
+ }
54
+ return normalized.map((container) => {
55
+ if (!container.id) {
56
+ throw new Error("Container id is required.");
57
+ }
58
+ const params = {
59
+ ...defaultQueryParams,
60
+ ...container.queryParams
61
+ };
62
+ const src = gtmKit.buildGtmNoscriptUrl(host, container.id, params, dataLayerName);
63
+ const mergedAttributes = {
64
+ ...gtmKit.DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,
65
+ ...iframeAttributes
66
+ };
67
+ const attributes = Object.fromEntries(
68
+ Object.entries(mergedAttributes).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])
69
+ );
70
+ return {
71
+ id: container.id,
72
+ src,
73
+ attributes
74
+ };
75
+ });
76
+ };
77
+ var generateDataLayerScript = (dataLayerName = gtmKit.DEFAULT_DATA_LAYER_NAME) => {
78
+ if (!isValidJsIdentifier(dataLayerName)) {
79
+ throw new Error(
80
+ `Invalid dataLayerName: "${dataLayerName}". Must be a valid JavaScript identifier (letters, digits, $, _ only, cannot start with a digit).`
81
+ );
82
+ }
83
+ return `window.${dataLayerName}=window.${dataLayerName}||[];`;
84
+ };
6
85
 
7
86
  Object.defineProperty(exports, 'DEFAULT_GTM_HOST', {
8
- enumerable: true,
9
- get: function () { return gtmKit.DEFAULT_GTM_HOST; }
87
+ enumerable: true,
88
+ get: function () { return gtmKit.DEFAULT_GTM_HOST; }
10
89
  });
11
90
  Object.defineProperty(exports, 'buildNoscriptUrl', {
12
- enumerable: true,
13
- get: function () { return gtmKit.buildGtmNoscriptUrl; }
91
+ enumerable: true,
92
+ get: function () { return gtmKit.buildGtmNoscriptUrl; }
14
93
  });
15
94
  Object.defineProperty(exports, 'buildScriptUrl', {
16
- enumerable: true,
17
- get: function () { return gtmKit.buildGtmScriptUrl; }
95
+ enumerable: true,
96
+ get: function () { return gtmKit.buildGtmScriptUrl; }
18
97
  });
19
98
  Object.defineProperty(exports, 'normalizeContainer', {
20
- enumerable: true,
21
- get: function () { return gtmKit.normalizeContainer; }
99
+ enumerable: true,
100
+ get: function () { return gtmKit.normalizeContainer; }
22
101
  });
23
102
  Object.defineProperty(exports, 'normalizeContainers', {
24
- enumerable: true,
25
- get: function () { return gtmKit.normalizeContainers; }
103
+ enumerable: true,
104
+ get: function () { return gtmKit.normalizeContainers; }
26
105
  });
27
- exports.generateDataLayerScript = h;
28
- exports.generateNoscriptTags = E;
29
- exports.generateScriptTags = x;
106
+ exports.generateDataLayerScript = generateDataLayerScript;
107
+ exports.generateNoscriptTags = generateNoscriptTags;
108
+ exports.generateScriptTags = generateScriptTags;
30
109
  //# sourceMappingURL=out.js.map
31
110
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/helpers.ts","../../src/components/index.ts"],"names":["DEFAULT_DATA_LAYER_NAME","DEFAULT_GTM_HOST","DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES","normalizeContainer","normalizeContainers","buildGtmScriptUrl","buildGtmNoscriptUrl","isValidJsIdentifier","value","filterNullish","obj","v","generateScriptTags","config","containers","host","defaultQueryParams","scriptAttributes","dataLayerName","normalized","container","params","src","asyncAttr","defer","nonce","restAttributes","generateNoscriptTags","iframeAttributes","mergedAttributes","attributes","k","generateDataLayerScript"],"mappings":"AAAA,OACE,2BAAAA,EACA,oBAAAC,EACA,sCAAAC,EACA,sBAAAC,EACA,uBAAAC,EACA,qBAAAC,EACA,uBAAAC,MACK,qBAiBA,IAAMC,EAAuBC,GAE3B,6BAA6B,KAAKA,CAAK,EAqBhD,IAAMC,EAAoDC,GACxD,OAAO,YAAY,OAAO,QAAQA,CAAG,EAAE,OAAO,CAAC,CAAC,CAAEC,CAAC,IAAMA,GAAK,IAAI,CAAC,EAuBxDC,EAAsBC,GAA6C,CAC9E,GAAM,CACJ,WAAAC,EACA,KAAAC,EAAOd,EACP,mBAAAe,EACA,iBAAAC,EACA,cAAAC,EAAgBlB,CAClB,EAAIa,EAEEM,EAAaf,EAAoBU,CAAU,EAEjD,GAAI,CAACK,EAAW,OACd,MAAM,IAAI,MAAM,yCAAyC,EAG3D,OAAOA,EAAW,IAAKC,GAAc,CACnC,GAAI,CAACA,EAAU,GACb,MAAM,IAAI,MAAM,2BAA2B,EAG7C,IAAMC,EAAS,CACb,GAAGL,EACH,GAAGI,EAAU,WACf,EAEME,EAAMjB,EAAkBU,EAAMK,EAAU,GAAIC,EAAQH,CAAa,EACjE,CAAE,MAAOK,EAAW,MAAAC,EAAO,MAAAC,EAAO,GAAGC,CAAe,EAAIT,GAAA,KAAAA,EAAoB,CAAC,EAEnF,MAAO,CACL,GAAIG,EAAU,GACd,IAAAE,EACA,MAAOC,GAAA,KAAAA,EAAa,GACpB,MAAAC,EACA,MAAAC,EACA,WAAYhB,EAAciB,CAAc,CAC1C,CACF,CAAC,CACH,EAYaC,EACXd,GAGsB,CACtB,GAAM,CACJ,WAAAC,EACA,KAAAC,EAAOd,EACP,mBAAAe,EACA,iBAAAY,EACA,cAAAV,EAAgBlB,CAClB,EAAIa,EAEEM,EAAaf,EAAoBU,CAAU,EAEjD,GAAI,CAACK,EAAW,OACd,MAAM,IAAI,MAAM,yCAAyC,EAG3D,OAAOA,EAAW,IAAKC,GAAc,CACnC,GAAI,CAACA,EAAU,GACb,MAAM,IAAI,MAAM,2BAA2B,EAG7C,IAAMC,EAAS,CACb,GAAGL,EACH,GAAGI,EAAU,WACf,EAEME,EAAMhB,EAAoBS,EAAMK,EAAU,GAAIC,EAAQH,CAAa,EACnEW,EAAmB,CACvB,GAAG3B,EACH,GAAG0B,CACL,EAGME,EAAa,OAAO,YACxB,OAAO,QAAQD,CAAgB,EAC5B,OAAO,CAAC,CAAC,CAAElB,CAAC,IAAMA,GAAK,IAAI,EAC3B,IAAI,CAAC,CAACoB,EAAGpB,CAAC,IAAM,CAACoB,EAAG,OAAOpB,CAAC,CAAC,CAAC,CACnC,EAEA,MAAO,CACL,GAAIS,EAAU,GACd,IAAAE,EACA,WAAAQ,CACF,CACF,CAAC,CACH,EAMaE,EAA0B,CAACd,EAAwBlB,IAAoC,CAClG,GAAI,CAACO,EAAoBW,CAAa,EACpC,MAAM,IAAI,MACR,2BAA2BA,CAAa,mGAC1C,EAEF,MAAO,UAAUA,CAAa,WAAWA,CAAa,OACxD,ECnLA,OACE,uBAAAd,EACA,sBAAAD,EACqB,qBAArBE,EACuB,uBAAvBC,MACK","sourcesContent":["import {\n DEFAULT_DATA_LAYER_NAME,\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl,\n buildGtmNoscriptUrl\n} from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput, ScriptAttributes } from '@jwiedeman/gtm-kit';\n\n// Re-export for convenience\nexport {\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n};\n\n/**\n * Validate that a string is a valid JavaScript identifier.\n * This prevents XSS attacks through dataLayerName injection.\n */\nexport const isValidJsIdentifier = (value: string): boolean => {\n // Must be a valid JS identifier: starts with letter/$/_, followed by letters/digits/$/_\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);\n};\n\n/**\n * Escape a string for safe use in JavaScript string literals.\n * Prevents XSS when interpolating user-provided values into inline scripts.\n */\nexport const escapeJsString = (value: string): string => {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/'/g, \"\\\\'\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/</g, '\\\\x3c')\n .replace(/>/g, '\\\\x3e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n};\n\n/** Filter out null/undefined values from an object */\nconst filterNullish = <T extends Record<string, unknown>>(obj: T): T =>\n Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null)) as T;\n\nexport interface GtmScriptConfig {\n containers: ContainerConfigInput | ContainerConfigInput[];\n host?: string;\n defaultQueryParams?: Record<string, string | number | boolean>;\n scriptAttributes?: ScriptAttributes;\n dataLayerName?: string;\n}\n\nexport interface ScriptTagData {\n id: string;\n src: string;\n async: boolean;\n defer?: boolean;\n nonce?: string;\n attributes: Record<string, string | boolean>;\n}\n\n/**\n * Generate script tag data for GTM containers.\n * Used by Astro components to render script tags.\n */\nexport const generateScriptTags = (config: GtmScriptConfig): ScriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n scriptAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmScriptUrl(host, container.id, params, dataLayerName);\n const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes ?? {};\n\n return {\n id: container.id,\n src,\n async: asyncAttr ?? true,\n defer,\n nonce,\n attributes: filterNullish(restAttributes) as Record<string, string | boolean>\n };\n });\n};\n\nexport interface NoscriptTagData {\n id: string;\n src: string;\n attributes: Record<string, string>;\n}\n\n/**\n * Generate noscript iframe data for GTM containers.\n * Used by Astro components to render noscript fallbacks.\n */\nexport const generateNoscriptTags = (\n config: Omit<GtmScriptConfig, 'scriptAttributes'> & {\n iframeAttributes?: Record<string, string | number | boolean>;\n }\n): NoscriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n iframeAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmNoscriptUrl(host, container.id, params, dataLayerName);\n const mergedAttributes = {\n ...DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n ...iframeAttributes\n };\n\n // Filter nullish values and convert to strings\n const attributes = Object.fromEntries(\n Object.entries(mergedAttributes)\n .filter(([, v]) => v != null)\n .map(([k, v]) => [k, String(v)])\n ) as Record<string, string>;\n\n return {\n id: container.id,\n src,\n attributes\n };\n });\n};\n\n/**\n * Generate the dataLayer initialization script.\n * @throws {Error} If dataLayerName is not a valid JavaScript identifier\n */\nexport const generateDataLayerScript = (dataLayerName: string = DEFAULT_DATA_LAYER_NAME): string => {\n if (!isValidJsIdentifier(dataLayerName)) {\n throw new Error(\n `Invalid dataLayerName: \"${dataLayerName}\". Must be a valid JavaScript identifier (letters, digits, $, _ only, cannot start with a digit).`\n );\n }\n return `window.${dataLayerName}=window.${dataLayerName}||[];`;\n};\n","export { generateScriptTags, generateNoscriptTags, generateDataLayerScript, DEFAULT_GTM_HOST } from './helpers';\n\n// Re-export URL utilities from core for backwards compatibility\nexport {\n normalizeContainers,\n normalizeContainer,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n} from '@jwiedeman/gtm-kit';\n\nexport type { GtmScriptConfig, ScriptTagData, NoscriptTagData } from './helpers';\n"]}
1
+ {"version":3,"sources":["../../src/components/helpers.ts","../../src/components/index.ts"],"names":["normalizeContainers","normalizeContainer","buildGtmScriptUrl","buildGtmNoscriptUrl"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAiBA,IAAM,sBAAsB,CAAC,UAA2B;AAE7D,SAAO,6BAA6B,KAAK,KAAK;AAChD;AAoBA,IAAM,gBAAgB,CAAoC,QACxD,OAAO,YAAY,OAAO,QAAQ,GAAG,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,KAAK,IAAI,CAAC;AAuB9D,IAAM,qBAAqB,CAAC,WAA6C;AAC9E,QAAM;AAAA,IACJ;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,EAClB,IAAI;AAEJ,QAAM,aAAa,oBAAoB,UAAU;AAEjD,MAAI,CAAC,WAAW,QAAQ;AACtB,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAEA,SAAO,WAAW,IAAI,CAAC,cAAc;AACnC,QAAI,CAAC,UAAU,IAAI;AACjB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,UAAM,SAAS;AAAA,MACb,GAAG;AAAA,MACH,GAAG,UAAU;AAAA,IACf;AAEA,UAAM,MAAM,kBAAkB,MAAM,UAAU,IAAI,QAAQ,aAAa;AACvE,UAAM,EAAE,OAAO,WAAW,OAAO,OAAO,GAAG,eAAe,IAAI,8CAAoB,CAAC;AAEnF,WAAO;AAAA,MACL,IAAI,UAAU;AAAA,MACd;AAAA,MACA,OAAO,gCAAa;AAAA,MACpB;AAAA,MACA;AAAA,MACA,YAAY,cAAc,cAAc;AAAA,IAC1C;AAAA,EACF,CAAC;AACH;AAYO,IAAM,uBAAuB,CAClC,WAGsB;AACtB,QAAM;AAAA,IACJ;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,EAClB,IAAI;AAEJ,QAAM,aAAa,oBAAoB,UAAU;AAEjD,MAAI,CAAC,WAAW,QAAQ;AACtB,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAEA,SAAO,WAAW,IAAI,CAAC,cAAc;AACnC,QAAI,CAAC,UAAU,IAAI;AACjB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,UAAM,SAAS;AAAA,MACb,GAAG;AAAA,MACH,GAAG,UAAU;AAAA,IACf;AAEA,UAAM,MAAM,oBAAoB,MAAM,UAAU,IAAI,QAAQ,aAAa;AACzE,UAAM,mBAAmB;AAAA,MACvB,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAGA,UAAM,aAAa,OAAO;AAAA,MACxB,OAAO,QAAQ,gBAAgB,EAC5B,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,KAAK,IAAI,EAC3B,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;AAAA,IACnC;AAEA,WAAO;AAAA,MACL,IAAI,UAAU;AAAA,MACd;AAAA,MACA;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAMO,IAAM,0BAA0B,CAAC,gBAAwB,4BAAoC;AAClG,MAAI,CAAC,oBAAoB,aAAa,GAAG;AACvC,UAAM,IAAI;AAAA,MACR,2BAA2B,aAAa;AAAA,IAC1C;AAAA,EACF;AACA,SAAO,UAAU,aAAa,WAAW,aAAa;AACxD;;;ACnLA;AAAA,EACE,uBAAAA;AAAA,EACA,sBAAAC;AAAA,EACqB,qBAArBC;AAAA,EACuB,uBAAvBC;AAAA,OACK","sourcesContent":["import {\n DEFAULT_DATA_LAYER_NAME,\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl,\n buildGtmNoscriptUrl\n} from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput, ScriptAttributes } from '@jwiedeman/gtm-kit';\n\n// Re-export for convenience\nexport {\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n};\n\n/**\n * Validate that a string is a valid JavaScript identifier.\n * This prevents XSS attacks through dataLayerName injection.\n */\nexport const isValidJsIdentifier = (value: string): boolean => {\n // Must be a valid JS identifier: starts with letter/$/_, followed by letters/digits/$/_\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);\n};\n\n/**\n * Escape a string for safe use in JavaScript string literals.\n * Prevents XSS when interpolating user-provided values into inline scripts.\n */\nexport const escapeJsString = (value: string): string => {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/'/g, \"\\\\'\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/</g, '\\\\x3c')\n .replace(/>/g, '\\\\x3e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n};\n\n/** Filter out null/undefined values from an object */\nconst filterNullish = <T extends Record<string, unknown>>(obj: T): T =>\n Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null)) as T;\n\nexport interface GtmScriptConfig {\n containers: ContainerConfigInput | ContainerConfigInput[];\n host?: string;\n defaultQueryParams?: Record<string, string | number | boolean>;\n scriptAttributes?: ScriptAttributes;\n dataLayerName?: string;\n}\n\nexport interface ScriptTagData {\n id: string;\n src: string;\n async: boolean;\n defer?: boolean;\n nonce?: string;\n attributes: Record<string, string | boolean>;\n}\n\n/**\n * Generate script tag data for GTM containers.\n * Used by Astro components to render script tags.\n */\nexport const generateScriptTags = (config: GtmScriptConfig): ScriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n scriptAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmScriptUrl(host, container.id, params, dataLayerName);\n const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes ?? {};\n\n return {\n id: container.id,\n src,\n async: asyncAttr ?? true,\n defer,\n nonce,\n attributes: filterNullish(restAttributes) as Record<string, string | boolean>\n };\n });\n};\n\nexport interface NoscriptTagData {\n id: string;\n src: string;\n attributes: Record<string, string>;\n}\n\n/**\n * Generate noscript iframe data for GTM containers.\n * Used by Astro components to render noscript fallbacks.\n */\nexport const generateNoscriptTags = (\n config: Omit<GtmScriptConfig, 'scriptAttributes'> & {\n iframeAttributes?: Record<string, string | number | boolean>;\n }\n): NoscriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n iframeAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmNoscriptUrl(host, container.id, params, dataLayerName);\n const mergedAttributes = {\n ...DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n ...iframeAttributes\n };\n\n // Filter nullish values and convert to strings\n const attributes = Object.fromEntries(\n Object.entries(mergedAttributes)\n .filter(([, v]) => v != null)\n .map(([k, v]) => [k, String(v)])\n ) as Record<string, string>;\n\n return {\n id: container.id,\n src,\n attributes\n };\n });\n};\n\n/**\n * Generate the dataLayer initialization script.\n * @throws {Error} If dataLayerName is not a valid JavaScript identifier\n */\nexport const generateDataLayerScript = (dataLayerName: string = DEFAULT_DATA_LAYER_NAME): string => {\n if (!isValidJsIdentifier(dataLayerName)) {\n throw new Error(\n `Invalid dataLayerName: \"${dataLayerName}\". Must be a valid JavaScript identifier (letters, digits, $, _ only, cannot start with a digit).`\n );\n }\n return `window.${dataLayerName}=window.${dataLayerName}||[];`;\n};\n","export { generateScriptTags, generateNoscriptTags, generateDataLayerScript, DEFAULT_GTM_HOST } from './helpers';\n\n// Re-export URL utilities from core for backwards compatibility\nexport {\n normalizeContainers,\n normalizeContainer,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n} from '@jwiedeman/gtm-kit';\n\nexport type { GtmScriptConfig, ScriptTagData, NoscriptTagData } from './helpers';\n"]}
@@ -1,8 +1,87 @@
1
1
  import { normalizeContainers, buildGtmScriptUrl, buildGtmNoscriptUrl, DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES, DEFAULT_DATA_LAYER_NAME, DEFAULT_GTM_HOST } from '@jwiedeman/gtm-kit';
2
2
  export { DEFAULT_GTM_HOST, buildGtmNoscriptUrl as buildNoscriptUrl, buildGtmScriptUrl as buildScriptUrl, normalizeContainer, normalizeContainers } from '@jwiedeman/gtm-kit';
3
3
 
4
- var w=r=>/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(r);var C=r=>Object.fromEntries(Object.entries(r).filter(([,e])=>e!=null)),x=r=>{let{containers:e,host:o=DEFAULT_GTM_HOST,defaultQueryParams:c,scriptAttributes:i,dataLayerName:g=DEFAULT_DATA_LAYER_NAME}=r,n=normalizeContainers(e);if(!n.length)throw new Error("At least one GTM container is required.");return n.map(t=>{if(!t.id)throw new Error("Container id is required.");let p={...c,...t.queryParams},l=buildGtmScriptUrl(o,t.id,p,g),{async:a,defer:u,nonce:s,...d}=i!=null?i:{};return {id:t.id,src:l,async:a!=null?a:!0,defer:u,nonce:s,attributes:C(d)}})},E=r=>{let{containers:e,host:o=DEFAULT_GTM_HOST,defaultQueryParams:c,iframeAttributes:i,dataLayerName:g=DEFAULT_DATA_LAYER_NAME}=r,n=normalizeContainers(e);if(!n.length)throw new Error("At least one GTM container is required.");return n.map(t=>{if(!t.id)throw new Error("Container id is required.");let p={...c,...t.queryParams},l=buildGtmNoscriptUrl(o,t.id,p,g),a={...DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,...i},u=Object.fromEntries(Object.entries(a).filter(([,s])=>s!=null).map(([s,d])=>[s,String(d)]));return {id:t.id,src:l,attributes:u}})},h=(r=DEFAULT_DATA_LAYER_NAME)=>{if(!w(r))throw new Error(`Invalid dataLayerName: "${r}". Must be a valid JavaScript identifier (letters, digits, $, _ only, cannot start with a digit).`);return `window.${r}=window.${r}||[];`};
4
+ // src/components/helpers.ts
5
+ var isValidJsIdentifier = (value) => {
6
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);
7
+ };
8
+ var filterNullish = (obj) => Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null));
9
+ var generateScriptTags = (config) => {
10
+ const {
11
+ containers,
12
+ host = DEFAULT_GTM_HOST,
13
+ defaultQueryParams,
14
+ scriptAttributes,
15
+ dataLayerName = DEFAULT_DATA_LAYER_NAME
16
+ } = config;
17
+ const normalized = normalizeContainers(containers);
18
+ if (!normalized.length) {
19
+ throw new Error("At least one GTM container is required.");
20
+ }
21
+ return normalized.map((container) => {
22
+ if (!container.id) {
23
+ throw new Error("Container id is required.");
24
+ }
25
+ const params = {
26
+ ...defaultQueryParams,
27
+ ...container.queryParams
28
+ };
29
+ const src = buildGtmScriptUrl(host, container.id, params, dataLayerName);
30
+ const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes != null ? scriptAttributes : {};
31
+ return {
32
+ id: container.id,
33
+ src,
34
+ async: asyncAttr != null ? asyncAttr : true,
35
+ defer,
36
+ nonce,
37
+ attributes: filterNullish(restAttributes)
38
+ };
39
+ });
40
+ };
41
+ var generateNoscriptTags = (config) => {
42
+ const {
43
+ containers,
44
+ host = DEFAULT_GTM_HOST,
45
+ defaultQueryParams,
46
+ iframeAttributes,
47
+ dataLayerName = DEFAULT_DATA_LAYER_NAME
48
+ } = config;
49
+ const normalized = normalizeContainers(containers);
50
+ if (!normalized.length) {
51
+ throw new Error("At least one GTM container is required.");
52
+ }
53
+ return normalized.map((container) => {
54
+ if (!container.id) {
55
+ throw new Error("Container id is required.");
56
+ }
57
+ const params = {
58
+ ...defaultQueryParams,
59
+ ...container.queryParams
60
+ };
61
+ const src = buildGtmNoscriptUrl(host, container.id, params, dataLayerName);
62
+ const mergedAttributes = {
63
+ ...DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,
64
+ ...iframeAttributes
65
+ };
66
+ const attributes = Object.fromEntries(
67
+ Object.entries(mergedAttributes).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])
68
+ );
69
+ return {
70
+ id: container.id,
71
+ src,
72
+ attributes
73
+ };
74
+ });
75
+ };
76
+ var generateDataLayerScript = (dataLayerName = DEFAULT_DATA_LAYER_NAME) => {
77
+ if (!isValidJsIdentifier(dataLayerName)) {
78
+ throw new Error(
79
+ `Invalid dataLayerName: "${dataLayerName}". Must be a valid JavaScript identifier (letters, digits, $, _ only, cannot start with a digit).`
80
+ );
81
+ }
82
+ return `window.${dataLayerName}=window.${dataLayerName}||[];`;
83
+ };
5
84
 
6
- export { h as generateDataLayerScript, E as generateNoscriptTags, x as generateScriptTags };
85
+ export { generateDataLayerScript, generateNoscriptTags, generateScriptTags };
7
86
  //# sourceMappingURL=out.js.map
8
87
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/helpers.ts","../../src/components/index.ts"],"names":["DEFAULT_DATA_LAYER_NAME","DEFAULT_GTM_HOST","DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES","normalizeContainer","normalizeContainers","buildGtmScriptUrl","buildGtmNoscriptUrl","isValidJsIdentifier","value","filterNullish","obj","v","generateScriptTags","config","containers","host","defaultQueryParams","scriptAttributes","dataLayerName","normalized","container","params","src","asyncAttr","defer","nonce","restAttributes","generateNoscriptTags","iframeAttributes","mergedAttributes","attributes","k","generateDataLayerScript"],"mappings":"AAAA,OACE,2BAAAA,EACA,oBAAAC,EACA,sCAAAC,EACA,sBAAAC,EACA,uBAAAC,EACA,qBAAAC,EACA,uBAAAC,MACK,qBAiBA,IAAMC,EAAuBC,GAE3B,6BAA6B,KAAKA,CAAK,EAqBhD,IAAMC,EAAoDC,GACxD,OAAO,YAAY,OAAO,QAAQA,CAAG,EAAE,OAAO,CAAC,CAAC,CAAEC,CAAC,IAAMA,GAAK,IAAI,CAAC,EAuBxDC,EAAsBC,GAA6C,CAC9E,GAAM,CACJ,WAAAC,EACA,KAAAC,EAAOd,EACP,mBAAAe,EACA,iBAAAC,EACA,cAAAC,EAAgBlB,CAClB,EAAIa,EAEEM,EAAaf,EAAoBU,CAAU,EAEjD,GAAI,CAACK,EAAW,OACd,MAAM,IAAI,MAAM,yCAAyC,EAG3D,OAAOA,EAAW,IAAKC,GAAc,CACnC,GAAI,CAACA,EAAU,GACb,MAAM,IAAI,MAAM,2BAA2B,EAG7C,IAAMC,EAAS,CACb,GAAGL,EACH,GAAGI,EAAU,WACf,EAEME,EAAMjB,EAAkBU,EAAMK,EAAU,GAAIC,EAAQH,CAAa,EACjE,CAAE,MAAOK,EAAW,MAAAC,EAAO,MAAAC,EAAO,GAAGC,CAAe,EAAIT,GAAA,KAAAA,EAAoB,CAAC,EAEnF,MAAO,CACL,GAAIG,EAAU,GACd,IAAAE,EACA,MAAOC,GAAA,KAAAA,EAAa,GACpB,MAAAC,EACA,MAAAC,EACA,WAAYhB,EAAciB,CAAc,CAC1C,CACF,CAAC,CACH,EAYaC,EACXd,GAGsB,CACtB,GAAM,CACJ,WAAAC,EACA,KAAAC,EAAOd,EACP,mBAAAe,EACA,iBAAAY,EACA,cAAAV,EAAgBlB,CAClB,EAAIa,EAEEM,EAAaf,EAAoBU,CAAU,EAEjD,GAAI,CAACK,EAAW,OACd,MAAM,IAAI,MAAM,yCAAyC,EAG3D,OAAOA,EAAW,IAAKC,GAAc,CACnC,GAAI,CAACA,EAAU,GACb,MAAM,IAAI,MAAM,2BAA2B,EAG7C,IAAMC,EAAS,CACb,GAAGL,EACH,GAAGI,EAAU,WACf,EAEME,EAAMhB,EAAoBS,EAAMK,EAAU,GAAIC,EAAQH,CAAa,EACnEW,EAAmB,CACvB,GAAG3B,EACH,GAAG0B,CACL,EAGME,EAAa,OAAO,YACxB,OAAO,QAAQD,CAAgB,EAC5B,OAAO,CAAC,CAAC,CAAElB,CAAC,IAAMA,GAAK,IAAI,EAC3B,IAAI,CAAC,CAACoB,EAAGpB,CAAC,IAAM,CAACoB,EAAG,OAAOpB,CAAC,CAAC,CAAC,CACnC,EAEA,MAAO,CACL,GAAIS,EAAU,GACd,IAAAE,EACA,WAAAQ,CACF,CACF,CAAC,CACH,EAMaE,EAA0B,CAACd,EAAwBlB,IAAoC,CAClG,GAAI,CAACO,EAAoBW,CAAa,EACpC,MAAM,IAAI,MACR,2BAA2BA,CAAa,mGAC1C,EAEF,MAAO,UAAUA,CAAa,WAAWA,CAAa,OACxD,ECnLA,OACE,uBAAAd,EACA,sBAAAD,EACqB,qBAArBE,EACuB,uBAAvBC,MACK","sourcesContent":["import {\n DEFAULT_DATA_LAYER_NAME,\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl,\n buildGtmNoscriptUrl\n} from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput, ScriptAttributes } from '@jwiedeman/gtm-kit';\n\n// Re-export for convenience\nexport {\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n};\n\n/**\n * Validate that a string is a valid JavaScript identifier.\n * This prevents XSS attacks through dataLayerName injection.\n */\nexport const isValidJsIdentifier = (value: string): boolean => {\n // Must be a valid JS identifier: starts with letter/$/_, followed by letters/digits/$/_\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);\n};\n\n/**\n * Escape a string for safe use in JavaScript string literals.\n * Prevents XSS when interpolating user-provided values into inline scripts.\n */\nexport const escapeJsString = (value: string): string => {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/'/g, \"\\\\'\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/</g, '\\\\x3c')\n .replace(/>/g, '\\\\x3e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n};\n\n/** Filter out null/undefined values from an object */\nconst filterNullish = <T extends Record<string, unknown>>(obj: T): T =>\n Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null)) as T;\n\nexport interface GtmScriptConfig {\n containers: ContainerConfigInput | ContainerConfigInput[];\n host?: string;\n defaultQueryParams?: Record<string, string | number | boolean>;\n scriptAttributes?: ScriptAttributes;\n dataLayerName?: string;\n}\n\nexport interface ScriptTagData {\n id: string;\n src: string;\n async: boolean;\n defer?: boolean;\n nonce?: string;\n attributes: Record<string, string | boolean>;\n}\n\n/**\n * Generate script tag data for GTM containers.\n * Used by Astro components to render script tags.\n */\nexport const generateScriptTags = (config: GtmScriptConfig): ScriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n scriptAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmScriptUrl(host, container.id, params, dataLayerName);\n const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes ?? {};\n\n return {\n id: container.id,\n src,\n async: asyncAttr ?? true,\n defer,\n nonce,\n attributes: filterNullish(restAttributes) as Record<string, string | boolean>\n };\n });\n};\n\nexport interface NoscriptTagData {\n id: string;\n src: string;\n attributes: Record<string, string>;\n}\n\n/**\n * Generate noscript iframe data for GTM containers.\n * Used by Astro components to render noscript fallbacks.\n */\nexport const generateNoscriptTags = (\n config: Omit<GtmScriptConfig, 'scriptAttributes'> & {\n iframeAttributes?: Record<string, string | number | boolean>;\n }\n): NoscriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n iframeAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmNoscriptUrl(host, container.id, params, dataLayerName);\n const mergedAttributes = {\n ...DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n ...iframeAttributes\n };\n\n // Filter nullish values and convert to strings\n const attributes = Object.fromEntries(\n Object.entries(mergedAttributes)\n .filter(([, v]) => v != null)\n .map(([k, v]) => [k, String(v)])\n ) as Record<string, string>;\n\n return {\n id: container.id,\n src,\n attributes\n };\n });\n};\n\n/**\n * Generate the dataLayer initialization script.\n * @throws {Error} If dataLayerName is not a valid JavaScript identifier\n */\nexport const generateDataLayerScript = (dataLayerName: string = DEFAULT_DATA_LAYER_NAME): string => {\n if (!isValidJsIdentifier(dataLayerName)) {\n throw new Error(\n `Invalid dataLayerName: \"${dataLayerName}\". Must be a valid JavaScript identifier (letters, digits, $, _ only, cannot start with a digit).`\n );\n }\n return `window.${dataLayerName}=window.${dataLayerName}||[];`;\n};\n","export { generateScriptTags, generateNoscriptTags, generateDataLayerScript, DEFAULT_GTM_HOST } from './helpers';\n\n// Re-export URL utilities from core for backwards compatibility\nexport {\n normalizeContainers,\n normalizeContainer,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n} from '@jwiedeman/gtm-kit';\n\nexport type { GtmScriptConfig, ScriptTagData, NoscriptTagData } from './helpers';\n"]}
1
+ {"version":3,"sources":["../../src/components/helpers.ts","../../src/components/index.ts"],"names":["normalizeContainers","normalizeContainer","buildGtmScriptUrl","buildGtmNoscriptUrl"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAiBA,IAAM,sBAAsB,CAAC,UAA2B;AAE7D,SAAO,6BAA6B,KAAK,KAAK;AAChD;AAoBA,IAAM,gBAAgB,CAAoC,QACxD,OAAO,YAAY,OAAO,QAAQ,GAAG,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,KAAK,IAAI,CAAC;AAuB9D,IAAM,qBAAqB,CAAC,WAA6C;AAC9E,QAAM;AAAA,IACJ;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,EAClB,IAAI;AAEJ,QAAM,aAAa,oBAAoB,UAAU;AAEjD,MAAI,CAAC,WAAW,QAAQ;AACtB,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAEA,SAAO,WAAW,IAAI,CAAC,cAAc;AACnC,QAAI,CAAC,UAAU,IAAI;AACjB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,UAAM,SAAS;AAAA,MACb,GAAG;AAAA,MACH,GAAG,UAAU;AAAA,IACf;AAEA,UAAM,MAAM,kBAAkB,MAAM,UAAU,IAAI,QAAQ,aAAa;AACvE,UAAM,EAAE,OAAO,WAAW,OAAO,OAAO,GAAG,eAAe,IAAI,8CAAoB,CAAC;AAEnF,WAAO;AAAA,MACL,IAAI,UAAU;AAAA,MACd;AAAA,MACA,OAAO,gCAAa;AAAA,MACpB;AAAA,MACA;AAAA,MACA,YAAY,cAAc,cAAc;AAAA,IAC1C;AAAA,EACF,CAAC;AACH;AAYO,IAAM,uBAAuB,CAClC,WAGsB;AACtB,QAAM;AAAA,IACJ;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,EAClB,IAAI;AAEJ,QAAM,aAAa,oBAAoB,UAAU;AAEjD,MAAI,CAAC,WAAW,QAAQ;AACtB,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAEA,SAAO,WAAW,IAAI,CAAC,cAAc;AACnC,QAAI,CAAC,UAAU,IAAI;AACjB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,UAAM,SAAS;AAAA,MACb,GAAG;AAAA,MACH,GAAG,UAAU;AAAA,IACf;AAEA,UAAM,MAAM,oBAAoB,MAAM,UAAU,IAAI,QAAQ,aAAa;AACzE,UAAM,mBAAmB;AAAA,MACvB,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAGA,UAAM,aAAa,OAAO;AAAA,MACxB,OAAO,QAAQ,gBAAgB,EAC5B,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,KAAK,IAAI,EAC3B,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;AAAA,IACnC;AAEA,WAAO;AAAA,MACL,IAAI,UAAU;AAAA,MACd;AAAA,MACA;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAMO,IAAM,0BAA0B,CAAC,gBAAwB,4BAAoC;AAClG,MAAI,CAAC,oBAAoB,aAAa,GAAG;AACvC,UAAM,IAAI;AAAA,MACR,2BAA2B,aAAa;AAAA,IAC1C;AAAA,EACF;AACA,SAAO,UAAU,aAAa,WAAW,aAAa;AACxD;;;ACnLA;AAAA,EACE,uBAAAA;AAAA,EACA,sBAAAC;AAAA,EACqB,qBAArBC;AAAA,EACuB,uBAAvBC;AAAA,OACK","sourcesContent":["import {\n DEFAULT_DATA_LAYER_NAME,\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl,\n buildGtmNoscriptUrl\n} from '@jwiedeman/gtm-kit';\nimport type { ContainerConfigInput, ScriptAttributes } from '@jwiedeman/gtm-kit';\n\n// Re-export for convenience\nexport {\n DEFAULT_GTM_HOST,\n DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n normalizeContainer,\n normalizeContainers,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n};\n\n/**\n * Validate that a string is a valid JavaScript identifier.\n * This prevents XSS attacks through dataLayerName injection.\n */\nexport const isValidJsIdentifier = (value: string): boolean => {\n // Must be a valid JS identifier: starts with letter/$/_, followed by letters/digits/$/_\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);\n};\n\n/**\n * Escape a string for safe use in JavaScript string literals.\n * Prevents XSS when interpolating user-provided values into inline scripts.\n */\nexport const escapeJsString = (value: string): string => {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/'/g, \"\\\\'\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/</g, '\\\\x3c')\n .replace(/>/g, '\\\\x3e')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n};\n\n/** Filter out null/undefined values from an object */\nconst filterNullish = <T extends Record<string, unknown>>(obj: T): T =>\n Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null)) as T;\n\nexport interface GtmScriptConfig {\n containers: ContainerConfigInput | ContainerConfigInput[];\n host?: string;\n defaultQueryParams?: Record<string, string | number | boolean>;\n scriptAttributes?: ScriptAttributes;\n dataLayerName?: string;\n}\n\nexport interface ScriptTagData {\n id: string;\n src: string;\n async: boolean;\n defer?: boolean;\n nonce?: string;\n attributes: Record<string, string | boolean>;\n}\n\n/**\n * Generate script tag data for GTM containers.\n * Used by Astro components to render script tags.\n */\nexport const generateScriptTags = (config: GtmScriptConfig): ScriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n scriptAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmScriptUrl(host, container.id, params, dataLayerName);\n const { async: asyncAttr, defer, nonce, ...restAttributes } = scriptAttributes ?? {};\n\n return {\n id: container.id,\n src,\n async: asyncAttr ?? true,\n defer,\n nonce,\n attributes: filterNullish(restAttributes) as Record<string, string | boolean>\n };\n });\n};\n\nexport interface NoscriptTagData {\n id: string;\n src: string;\n attributes: Record<string, string>;\n}\n\n/**\n * Generate noscript iframe data for GTM containers.\n * Used by Astro components to render noscript fallbacks.\n */\nexport const generateNoscriptTags = (\n config: Omit<GtmScriptConfig, 'scriptAttributes'> & {\n iframeAttributes?: Record<string, string | number | boolean>;\n }\n): NoscriptTagData[] => {\n const {\n containers,\n host = DEFAULT_GTM_HOST,\n defaultQueryParams,\n iframeAttributes,\n dataLayerName = DEFAULT_DATA_LAYER_NAME\n } = config;\n\n const normalized = normalizeContainers(containers);\n\n if (!normalized.length) {\n throw new Error('At least one GTM container is required.');\n }\n\n return normalized.map((container) => {\n if (!container.id) {\n throw new Error('Container id is required.');\n }\n\n const params = {\n ...defaultQueryParams,\n ...container.queryParams\n };\n\n const src = buildGtmNoscriptUrl(host, container.id, params, dataLayerName);\n const mergedAttributes = {\n ...DEFAULT_NOSCRIPT_IFRAME_ATTRIBUTES,\n ...iframeAttributes\n };\n\n // Filter nullish values and convert to strings\n const attributes = Object.fromEntries(\n Object.entries(mergedAttributes)\n .filter(([, v]) => v != null)\n .map(([k, v]) => [k, String(v)])\n ) as Record<string, string>;\n\n return {\n id: container.id,\n src,\n attributes\n };\n });\n};\n\n/**\n * Generate the dataLayer initialization script.\n * @throws {Error} If dataLayerName is not a valid JavaScript identifier\n */\nexport const generateDataLayerScript = (dataLayerName: string = DEFAULT_DATA_LAYER_NAME): string => {\n if (!isValidJsIdentifier(dataLayerName)) {\n throw new Error(\n `Invalid dataLayerName: \"${dataLayerName}\". Must be a valid JavaScript identifier (letters, digits, $, _ only, cannot start with a digit).`\n );\n }\n return `window.${dataLayerName}=window.${dataLayerName}||[];`;\n};\n","export { generateScriptTags, generateNoscriptTags, generateDataLayerScript, DEFAULT_GTM_HOST } from './helpers';\n\n// Re-export URL utilities from core for backwards compatibility\nexport {\n normalizeContainers,\n normalizeContainer,\n buildGtmScriptUrl as buildScriptUrl,\n buildGtmNoscriptUrl as buildNoscriptUrl\n} from '@jwiedeman/gtm-kit';\n\nexport type { GtmScriptConfig, ScriptTagData, NoscriptTagData } from './helpers';\n"]}