@nuxt/scripts 1.1.1 → 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.
Files changed (56) hide show
  1. package/dist/devtools-client/200.html +1 -1
  2. package/dist/devtools-client/404.html +1 -1
  3. package/dist/devtools-client/_nuxt/{CpUjVpvL.js → BTXw0v4h.js} +1 -1
  4. package/dist/devtools-client/_nuxt/{d96VI30H.js → Bf6N2ObH.js} +1 -1
  5. package/dist/devtools-client/_nuxt/C5Bq9Qgz.js +1 -0
  6. package/dist/devtools-client/_nuxt/{CpodaK4k.js → CSw0kWhI.js} +1 -1
  7. package/dist/devtools-client/_nuxt/DWvCkA0U.js +195 -0
  8. package/dist/devtools-client/_nuxt/{CZLtBO2f.js → TPgAGQc8.js} +1 -1
  9. package/dist/devtools-client/_nuxt/builds/latest.json +1 -1
  10. package/dist/devtools-client/_nuxt/builds/meta/32b705e4-fcb6-4eea-92d5-dbdced5169df.json +1 -0
  11. package/dist/devtools-client/_nuxt/{DkOU4Aaf.js → dvDf1_6X.js} +1 -1
  12. package/dist/devtools-client/_nuxt/entry.VwiedW44.css +1 -0
  13. package/dist/devtools-client/_nuxt/error-404.DBJH6QEN.css +1 -0
  14. package/dist/devtools-client/_nuxt/error-500.BIIPjeaA.css +1 -0
  15. package/dist/devtools-client/docs/index.html +1 -1
  16. package/dist/devtools-client/first-party/index.html +1 -1
  17. package/dist/devtools-client/index.html +1 -1
  18. package/dist/devtools-client/registry/index.html +1 -1
  19. package/dist/module.json +1 -1
  20. package/dist/module.mjs +37 -3
  21. package/dist/registry.mjs +16 -3
  22. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsHeatmapLayer.d.vue.ts +18 -1
  23. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsHeatmapLayer.vue +7 -1
  24. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsHeatmapLayer.vue.d.ts +18 -1
  25. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsStaticMap.d.vue.ts +9 -0
  26. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsStaticMap.vue +6 -5
  27. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsStaticMap.vue.d.ts +9 -0
  28. package/dist/runtime/components/ScriptGravatar.vue +2 -0
  29. package/dist/runtime/components/ScriptLoadingIndicator.vue +1 -1
  30. package/dist/runtime/components/ScriptVimeoPlayer.vue +27 -4
  31. package/dist/runtime/components/ScriptYouTubePlayer.vue +25 -4
  32. package/dist/runtime/composables/useScript.js +4 -3
  33. package/dist/runtime/registry/schemas.d.ts +76 -0
  34. package/dist/runtime/registry/schemas.js +82 -2
  35. package/dist/runtime/registry/speedcurve.d.ts +21 -0
  36. package/dist/runtime/registry/speedcurve.js +95 -0
  37. package/dist/runtime/registry/stripe.js +1 -1
  38. package/dist/runtime/server/instagram-embed.js +37 -10
  39. package/dist/runtime/server/proxy-handler.d.ts +1 -0
  40. package/dist/runtime/server/proxy-handler.js +16 -0
  41. package/dist/runtime/server/utils/instagram-embed.d.ts +1 -0
  42. package/dist/runtime/server/utils/instagram-embed.js +5 -0
  43. package/dist/runtime/types.d.ts +3 -1
  44. package/dist/runtime/utils/after-next-paint.d.ts +1 -0
  45. package/dist/runtime/utils/after-next-paint.js +3 -0
  46. package/dist/stats.d.mts +1 -1
  47. package/dist/stats.d.ts +1 -1
  48. package/dist/stats.mjs +5 -0
  49. package/dist/types-source.mjs +109 -3
  50. package/package.json +9 -4
  51. package/dist/devtools-client/_nuxt/B-0LGYiZ.js +0 -188
  52. package/dist/devtools-client/_nuxt/BHdEFiuu.js +0 -1
  53. package/dist/devtools-client/_nuxt/builds/meta/7cf30d77-8399-4375-9a20-302132331b02.json +0 -1
  54. package/dist/devtools-client/_nuxt/entry.DFx-MLUa.css +0 -1
  55. package/dist/devtools-client/_nuxt/error-404.aORtIbIE.css +0 -1
  56. package/dist/devtools-client/_nuxt/error-500.FkAdJQFV.css +0 -1
@@ -1,4 +1,4 @@
1
- import { any, array, boolean, custom, literal, minLength, number, object, optional, pipe, record, string, union } from "valibot";
1
+ import { any, array, boolean, custom, function_, literal, maxValue, minLength, minValue, number, object, optional, pipe, record, string, union } from "valibot";
2
2
  const consentCategoryValue = union([literal("granted"), literal("denied")]);
3
3
  export const gcmConsentState = object({
4
4
  ad_storage: optional(consentCategoryValue),
@@ -947,6 +947,79 @@ export const InitObjectPropertiesSchema = object({
947
947
  */
948
948
  age: optional(string())
949
949
  });
950
+ export const SpeedCurveOptions = object({
951
+ // -- Composable-only options (consumed by useScriptSpeedCurve, not forwarded to LUX) --
952
+ /**
953
+ * Your SpeedCurve customer ID.
954
+ * @see https://support.speedcurve.com/docs/add-rum-to-your-site
955
+ */
956
+ id: pipe(string(), minLength(1)),
957
+ /**
958
+ * Enable SPA (single-page application) mode.
959
+ * When true, lux.js tracks soft navigations instead of full page loads.
960
+ * @see https://support.speedcurve.com/docs/single-page-applications
961
+ */
962
+ spaMode: optional(boolean()),
963
+ /**
964
+ * Automatically wire Vue Router hooks for SPA tracking when spaMode is true.
965
+ * Set to false to instrument navigations manually.
966
+ * @default true (when spaMode is true)
967
+ */
968
+ autoTrackSpaNavigations: optional(boolean()),
969
+ // -- LUX UserConfig passthrough (forwarded onto window.LUX before lux.js loads) --
970
+ // Property names match upstream LUX UserConfig casing exactly. Keep this list in
971
+ // sync with the LUX_USER_CONFIG_KEYS filter in speedcurve.ts.
972
+ /**
973
+ * Page label shown in the SpeedCurve dashboard.
974
+ * Accepts a static string, a function `(to) => string | false` for per-navigation labels,
975
+ * or `false` to disable labeling entirely. A callback returning `false` skips updating
976
+ * the label for that navigation.
977
+ * @default String(to.name ?? to.path)
978
+ */
979
+ label: optional(union([string(), function_(), literal(false)])),
980
+ /**
981
+ * Sampling rate (0–100). Percentage of sessions that send beacons.
982
+ * Upstream spelling is lowercase — matches LUX UserConfig.
983
+ */
984
+ samplerate: optional(pipe(number(), minValue(0), maxValue(100))),
985
+ /**
986
+ * Send the beacon when the page is hidden (pagehide event).
987
+ * @default true
988
+ */
989
+ sendBeaconOnPageHidden: optional(boolean()),
990
+ /**
991
+ * Track JavaScript errors.
992
+ * @default true
993
+ */
994
+ trackErrors: optional(boolean()),
995
+ /**
996
+ * Maximum number of errors to track per page view.
997
+ * @default 5
998
+ */
999
+ maxErrors: optional(number()),
1000
+ /**
1001
+ * Minimum time (ms) before a beacon can be sent.
1002
+ */
1003
+ minMeasureTime: optional(number()),
1004
+ /**
1005
+ * Maximum time (ms) after which the beacon is sent regardless of load state.
1006
+ * @default 60000
1007
+ */
1008
+ maxMeasureTime: optional(number()),
1009
+ /**
1010
+ * Start a new beacon when the page becomes visible after being hidden.
1011
+ */
1012
+ newBeaconOnPageShow: optional(boolean()),
1013
+ /**
1014
+ * Track pages loaded in background tabs.
1015
+ * @default false
1016
+ */
1017
+ trackHiddenPages: optional(boolean()),
1018
+ /**
1019
+ * Cookie domain for cross-subdomain session tracking.
1020
+ */
1021
+ cookieDomain: optional(string())
1022
+ });
950
1023
  export const SnapTrPixelOptions = object({
951
1024
  /**
952
1025
  * Your Snapchat Pixel ID.
@@ -966,7 +1039,14 @@ export const StripeOptions = object({
966
1039
  * @default true
967
1040
  * @see https://docs.stripe.com/disputes/prevention/advanced-fraud-detection
968
1041
  */
969
- advancedFraudSignals: optional(boolean())
1042
+ advancedFraudSignals: optional(boolean()),
1043
+ /**
1044
+ * The Stripe.js SDK version to load.
1045
+ * Named releases get autocomplete; any future codename is also accepted.
1046
+ * @default 'basil'
1047
+ * @see https://docs.stripe.com/sdks/stripejs-versioning
1048
+ */
1049
+ version: optional(union([literal("v3"), literal("acacia"), literal("basil"), literal("clover"), literal("dahlia"), string()]))
970
1050
  });
971
1051
  export const TikTokPixelOptions = object({
972
1052
  /**
@@ -0,0 +1,21 @@
1
+ import type { LuxGlobal, UserConfig } from '@speedcurve/lux';
2
+ import type { RouteLocationNormalized } from 'vue-router';
3
+ import type { RegistryScriptInput, UseScriptContext } from '#nuxt-scripts/types';
4
+ import { SpeedCurveOptions } from './schemas.js';
5
+ export { SpeedCurveOptions };
6
+ export interface SpeedCurveApi {
7
+ LUX: LuxGlobal;
8
+ }
9
+ declare global {
10
+ interface Window {
11
+ LUX?: LuxGlobal;
12
+ LUX_ae?: ErrorEvent[];
13
+ LUX_al?: PerformanceEntry[];
14
+ }
15
+ }
16
+ export type SpeedCurveInput = Omit<RegistryScriptInput<typeof SpeedCurveOptions>, 'label'> & Omit<UserConfig, 'label'> & {
17
+ label?: string | ((to: RouteLocationNormalized) => string | false) | false;
18
+ };
19
+ export declare function useScriptSpeedCurve<T extends SpeedCurveApi>(_options?: SpeedCurveInput): UseScriptContext<T>;
20
+ export declare function applyConfig(options: SpeedCurveInput): void;
21
+ export declare function installAutoTracker(options?: SpeedCurveInput): void;
@@ -0,0 +1,95 @@
1
+ import { useHead, useNuxtApp, useRouter } from "nuxt/app";
2
+ import { speedcurveLuxSnippet } from "#build/nuxt-scripts-snippets";
3
+ import { useRegistryScript } from "../utils.js";
4
+ import { afterNextPaint } from "../utils/after-next-paint.js";
5
+ import { SpeedCurveOptions } from "./schemas.js";
6
+ export { SpeedCurveOptions };
7
+ const LUX_USER_CONFIG_KEYS = Object.keys(SpeedCurveOptions.entries).filter(
8
+ (k) => k !== "id" && k !== "autoTrackSpaNavigations" && k !== "spaMode"
9
+ );
10
+ let luxWired = false;
11
+ export function useScriptSpeedCurve(_options) {
12
+ return useRegistryScript("speedcurve", (options) => ({
13
+ scriptInput: {
14
+ src: `https://cdn.speedcurve.com/js/lux.js?id=${options.id}`,
15
+ crossorigin: "anonymous"
16
+ },
17
+ schema: import.meta.dev ? SpeedCurveOptions : void 0,
18
+ scriptOptions: {
19
+ trigger: "client",
20
+ use: () => ({ LUX: window.LUX }),
21
+ beforeInit: () => {
22
+ if (!speedcurveLuxSnippet)
23
+ return;
24
+ useHead({
25
+ script: [{
26
+ key: "speedcurve-lux-primer",
27
+ tagPosition: "head",
28
+ tagPriority: "critical",
29
+ innerHTML: speedcurveLuxSnippet
30
+ }]
31
+ });
32
+ }
33
+ },
34
+ clientInit: import.meta.server ? void 0 : () => {
35
+ const input = options;
36
+ applyConfig(input);
37
+ if (options.spaMode && options.autoTrackSpaNavigations !== false)
38
+ installAutoTracker(input);
39
+ }
40
+ }), _options);
41
+ }
42
+ export function applyConfig(options) {
43
+ const lux = window.LUX;
44
+ if (!lux)
45
+ return;
46
+ for (const k of LUX_USER_CONFIG_KEYS) {
47
+ const v = options[k];
48
+ if (v === void 0)
49
+ continue;
50
+ if (k === "label" && typeof v !== "string")
51
+ continue;
52
+ lux[k] = v;
53
+ }
54
+ }
55
+ export function installAutoTracker(options) {
56
+ if (luxWired)
57
+ return;
58
+ luxWired = true;
59
+ const router = useRouter();
60
+ const nuxt = useNuxtApp();
61
+ let pendingInitial = nuxt.payload?.serverRendered === false;
62
+ const label = options?.label === false ? null : typeof options?.label === "function" ? options.label : (to) => String(to.name ?? to.path);
63
+ router.beforeEach((to) => {
64
+ const lux = window.LUX;
65
+ if (!lux)
66
+ return;
67
+ if (pendingInitial) {
68
+ pendingInitial = false;
69
+ if (label) {
70
+ const nextLabel = label(to);
71
+ if (nextLabel !== false)
72
+ lux.label = nextLabel;
73
+ }
74
+ return;
75
+ }
76
+ lux.startSoftNavigation();
77
+ if (label) {
78
+ const nextLabel = label(to);
79
+ if (nextLabel !== false)
80
+ lux.label = nextLabel;
81
+ }
82
+ });
83
+ router.afterEach((_to, _from, failure) => {
84
+ if (!failure)
85
+ return;
86
+ const lux = window.LUX;
87
+ if (!lux)
88
+ return;
89
+ lux.markLoadTime();
90
+ lux.addData("luxNavFailed", "1");
91
+ });
92
+ nuxt.hook("page:finish", () => {
93
+ afterNextPaint(() => window.LUX?.markLoadTime());
94
+ });
95
+ }
@@ -6,7 +6,7 @@ export function useScriptStripe(_options) {
6
6
  return useRegistryScript("stripe", (options) => ({
7
7
  scriptInput: {
8
8
  src: withQuery(
9
- `https://js.stripe.com/basil/stripe.js`,
9
+ `https://js.stripe.com/${options?.version || "basil"}/stripe.js`,
10
10
  typeof options?.advancedFraudSignals === "boolean" && !options?.advancedFraudSignals ? { advancedFraudSignals: false } : {}
11
11
  ),
12
12
  // opt-out of privacy defaults
@@ -1,16 +1,41 @@
1
1
  import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
- import { useRuntimeConfig } from "nitropack/runtime";
2
+ import { defineCachedFunction, useRuntimeConfig } from "nitropack/runtime";
3
+ import { $fetch } from "ofetch";
3
4
  import { ELEMENT_NODE, parse, renderSync, TEXT_NODE, walkSync } from "ultrahtml";
4
5
  import { createCachedJsonFetch } from "./utils/cached-upstream.js";
5
- import { proxyAssetUrl, rewriteUrl, rewriteUrlsInText, RSRC_RE, scopeCss } from "./utils/instagram-embed.js";
6
+ import { isEmbedShell, proxyAssetUrl, rewriteUrl, rewriteUrlsInText, RSRC_RE, scopeCss } from "./utils/instagram-embed.js";
6
7
  import { withSigning } from "./utils/withSigning.js";
7
8
  export { proxyAssetUrl, proxyImageUrl, rewriteUrl, rewriteUrlsInText, scopeCss } from "./utils/instagram-embed.js";
8
9
  const EMBED_INSTAGRAM_SUFFIX_RE = /\/embed\/instagram$/;
9
10
  const SRCSET_SPLIT_RE = /\s+/;
10
- const cachedEmbedFetch = createCachedJsonFetch(
11
- "nuxt-scripts-instagram-embed",
12
- 600,
13
- (url) => url
11
+ const cachedEmbedFetch = defineCachedFunction(
12
+ async (url, headers) => {
13
+ const html = await $fetch(url, { timeout: 1e4, headers });
14
+ if (isEmbedShell(html)) {
15
+ throw createError({
16
+ statusCode: 502,
17
+ statusMessage: "Instagram returned an empty embed shell (post unavailable or upstream rate-limiting)"
18
+ });
19
+ }
20
+ return html;
21
+ },
22
+ {
23
+ // v2 — bump to evict any v1 entries that cached the empty JS shell
24
+ // before the shell-detection / UA fix landed.
25
+ name: "nuxt-scripts-instagram-embed-v2",
26
+ maxAge: 600,
27
+ swr: true,
28
+ staleMaxAge: 600,
29
+ // Vary on headers too — Instagram's response is UA-dependent, so
30
+ // different callers (e.g. unit tests, future UA changes) must not
31
+ // collide on the same key.
32
+ getKey: (url, headers) => {
33
+ const parts = [url];
34
+ for (const [k, v] of Object.entries(headers).sort(([a], [b]) => a.localeCompare(b)))
35
+ parts.push(`${k}=${v}`);
36
+ return parts.join("|");
37
+ }
38
+ }
14
39
  );
15
40
  const cachedCssFetch = createCachedJsonFetch(
16
41
  "nuxt-scripts-instagram-css",
@@ -56,10 +81,12 @@ export default withSigning(defineEventHandler(async (event) => {
56
81
  const cleanUrl = parsedUrl.origin + pathname;
57
82
  const embedUrl = `${cleanUrl}embed/${captions ? "captioned/" : ""}`;
58
83
  const html = await cachedEmbedFetch(embedUrl, {
59
- headers: {
60
- "Accept": "text/html",
61
- "User-Agent": "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
62
- }
84
+ "Accept": "text/html",
85
+ // Meta's own crawler UA. Googlebot's UA is also accepted by Instagram
86
+ // but is IP-verified, so it fails from hosts outside Google's ranges
87
+ // (e.g. Cloudflare/Vercel) and Instagram serves the JS shell instead
88
+ // of the SSR'd post.
89
+ "User-Agent": "facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)"
63
90
  }).catch((error) => {
64
91
  throw createError({
65
92
  statusCode: error.statusCode || 500,
@@ -1,3 +1,4 @@
1
+ export declare const SKIP_REQUEST_HEADERS: Set<string>;
1
2
  /**
2
3
  * Privacy-aware proxy handler for first-party script collection endpoints.
3
4
  * Routes requests to third-party analytics while protecting user privacy.
@@ -13,6 +13,16 @@ import {
13
13
  const COMPRESSION_RE = /gzip|deflate|br|compress|base64/i;
14
14
  const CLIENT_HINT_VERSION_RE = /;v="(\d+)\.[^"]*"/g;
15
15
  const SKIP_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["set-cookie", "transfer-encoding", "content-encoding", "content-length"]);
16
+ export const SKIP_REQUEST_HEADERS = /* @__PURE__ */ new Set([
17
+ "connection",
18
+ "keep-alive",
19
+ "proxy-authenticate",
20
+ "proxy-authorization",
21
+ "te",
22
+ "trailer",
23
+ "transfer-encoding",
24
+ "upgrade"
25
+ ]);
16
26
  function stripQueryFingerprinting(query, privacy) {
17
27
  const stripped = stripPayloadFingerprinting(query, privacy);
18
28
  const params = new URLSearchParams();
@@ -88,12 +98,18 @@ export default defineEventHandler(async (event) => {
88
98
  }
89
99
  }
90
100
  const headers = {};
101
+ const connectionHeaderValue = originalHeaders.connection;
102
+ const connectionNamedHeaders = connectionHeaderValue ? new Set(connectionHeaderValue.split(",").map((h) => h.trim().toLowerCase()).filter(Boolean)) : null;
91
103
  for (const [key, value] of Object.entries(originalHeaders)) {
92
104
  if (!value)
93
105
  continue;
94
106
  const lowerKey = key.toLowerCase();
95
107
  if (lowerKey === "host")
96
108
  continue;
109
+ if (SKIP_REQUEST_HEADERS.has(lowerKey))
110
+ continue;
111
+ if (connectionNamedHeaders?.has(lowerKey))
112
+ continue;
97
113
  if (SENSITIVE_HEADERS.includes(lowerKey))
98
114
  continue;
99
115
  if (lowerKey === "content-length") {
@@ -1,4 +1,5 @@
1
1
  export declare const RSRC_RE: RegExp;
2
+ export declare function isEmbedShell(html: string): boolean;
2
3
  export declare const AMP_RE: RegExp;
3
4
  export declare const SCONTENT_RE: RegExp;
4
5
  export declare const STATIC_CDN_RE: RegExp;
@@ -1,5 +1,10 @@
1
1
  import { buildProxyUrl } from "./proxy-url.js";
2
2
  export const RSRC_RE = /url\(\/rsrc\.php([^)]+)\)/g;
3
+ const SHELL_BODY_RE = /id=["'](?:splash-screen|has-finished-comet-page)["']/;
4
+ const HAS_POST_CONTENT_RE = /\bclass=(["'])[^"']*\b(?:Embed|EmbeddedMedia|EmbeddedMediaImage)\b[^"']*\1/i;
5
+ export function isEmbedShell(html) {
6
+ return SHELL_BODY_RE.test(html) && !HAS_POST_CONTENT_RE.test(html);
7
+ }
3
8
  export const AMP_RE = /&amp;/g;
4
9
  export const SCONTENT_RE = /https:\/\/scontent[^"'\s),]+\.cdninstagram\.com[^"'\s),]+/g;
5
10
  export const STATIC_CDN_RE = /https:\/\/static\.cdninstagram\.com[^"'\s),]+/g;
@@ -35,6 +35,7 @@ import type { RedditPixelInput } from './registry/reddit-pixel.js';
35
35
  import type { RybbitAnalyticsInput } from './registry/rybbit-analytics.js';
36
36
  import type { SegmentInput } from './registry/segment.js';
37
37
  import type { SnapTrPixelInput } from './registry/snapchat-pixel.js';
38
+ import type { SpeedCurveInput } from './registry/speedcurve.js';
38
39
  import type { StripeInput } from './registry/stripe.js';
39
40
  import type { TikTokPixelInput } from './registry/tiktok-pixel.js';
40
41
  import type { UmamiAnalyticsInput } from './registry/umami-analytics.js';
@@ -263,6 +264,7 @@ export interface ScriptRegistry {
263
264
  rybbitAnalytics?: RybbitAnalyticsInput;
264
265
  redditPixel?: RedditPixelInput;
265
266
  segment?: SegmentInput;
267
+ speedcurve?: SpeedCurveInput;
266
268
  stripe?: StripeInput;
267
269
  tiktokPixel?: TikTokPixelInput;
268
270
  xEmbed?: XEmbedInput;
@@ -281,7 +283,7 @@ export interface ScriptRegistry {
281
283
  * Built-in registry script keys — not affected by module augmentation.
282
284
  * Use this to type-check records that must enumerate all built-in scripts (logos, meta, etc.).
283
285
  */
284
- export type BuiltInRegistryScriptKey = 'ahrefsAnalytics' | 'bingUet' | 'blueskyEmbed' | 'calendly' | 'carbonAds' | 'crisp' | 'clarity' | 'cloudflareWebAnalytics' | 'databuddyAnalytics' | 'metaPixel' | 'fathomAnalytics' | 'instagramEmbed' | 'plausibleAnalytics' | 'googleAdsense' | 'googleAnalytics' | 'googleMaps' | 'googleRecaptcha' | 'googleSignIn' | 'lemonSqueezy' | 'googleTagManager' | 'hotjar' | 'intercom' | 'linkedinInsight' | 'paypal' | 'posthog' | 'matomoAnalytics' | 'mixpanelAnalytics' | 'rybbitAnalytics' | 'redditPixel' | 'segment' | 'stripe' | 'tiktokPixel' | 'xEmbed' | 'xPixel' | 'snapchatPixel' | 'youtubePlayer' | 'vercelAnalytics' | 'vimeoPlayer' | 'umamiAnalytics' | 'usercentrics' | 'gravatar' | 'npm';
286
+ export type BuiltInRegistryScriptKey = 'ahrefsAnalytics' | 'bingUet' | 'blueskyEmbed' | 'calendly' | 'carbonAds' | 'crisp' | 'clarity' | 'cloudflareWebAnalytics' | 'databuddyAnalytics' | 'metaPixel' | 'fathomAnalytics' | 'instagramEmbed' | 'plausibleAnalytics' | 'googleAdsense' | 'googleAnalytics' | 'googleMaps' | 'googleRecaptcha' | 'googleSignIn' | 'lemonSqueezy' | 'googleTagManager' | 'hotjar' | 'intercom' | 'linkedinInsight' | 'paypal' | 'posthog' | 'matomoAnalytics' | 'mixpanelAnalytics' | 'rybbitAnalytics' | 'redditPixel' | 'segment' | 'stripe' | 'tiktokPixel' | 'xEmbed' | 'xPixel' | 'snapchatPixel' | 'speedcurve' | 'youtubePlayer' | 'vercelAnalytics' | 'vimeoPlayer' | 'umamiAnalytics' | 'usercentrics' | 'gravatar' | 'npm';
285
287
  /**
286
288
  * Union of all explicit registry script keys (excludes npm pattern).
287
289
  * Includes both built-in and augmented keys.
@@ -0,0 +1 @@
1
+ export declare function afterNextPaint(callback: () => void): void;
@@ -0,0 +1,3 @@
1
+ export function afterNextPaint(callback) {
2
+ requestAnimationFrame(() => requestAnimationFrame(callback));
3
+ }
package/dist/stats.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- type TrackedDataType = 'page-views' | 'events' | 'conversions' | 'user-identity' | 'session-replay' | 'heatmaps' | 'clicks' | 'scrolls' | 'retargeting' | 'audiences' | 'form-submissions' | 'video-engagement' | 'transactions' | 'errors' | 'tag-injection' | 'ab-testing';
1
+ type TrackedDataType = 'page-views' | 'events' | 'conversions' | 'user-identity' | 'session-replay' | 'heatmaps' | 'clicks' | 'scrolls' | 'retargeting' | 'audiences' | 'form-submissions' | 'video-engagement' | 'transactions' | 'errors' | 'tag-injection' | 'ab-testing' | 'performance-timings';
2
2
 
3
3
  interface ScriptPrivacy {
4
4
  ip: boolean;
package/dist/stats.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- type TrackedDataType = 'page-views' | 'events' | 'conversions' | 'user-identity' | 'session-replay' | 'heatmaps' | 'clicks' | 'scrolls' | 'retargeting' | 'audiences' | 'form-submissions' | 'video-engagement' | 'transactions' | 'errors' | 'tag-injection' | 'ab-testing';
1
+ type TrackedDataType = 'page-views' | 'events' | 'conversions' | 'user-identity' | 'session-replay' | 'heatmaps' | 'clicks' | 'scrolls' | 'retargeting' | 'audiences' | 'form-submissions' | 'video-engagement' | 'transactions' | 'errors' | 'tag-injection' | 'ab-testing' | 'performance-timings';
2
2
 
3
3
  interface ScriptPrivacy {
4
4
  ip: boolean;
package/dist/stats.mjs CHANGED
@@ -198,6 +198,11 @@ const scriptMeta = {
198
198
  gravatar: {
199
199
  urls: ["https://secure.gravatar.com/js/gprofiles.js"],
200
200
  trackedData: []
201
+ },
202
+ // Performance monitoring
203
+ speedcurve: {
204
+ urls: ["https://cdn.speedcurve.com/js/lux.js"],
205
+ trackedData: ["page-views", "performance-timings", "errors"]
201
206
  }
202
207
  };
203
208
 
@@ -382,7 +382,7 @@ const types = {
382
382
  {
383
383
  name: "ScriptGoogleMapsHeatmapLayerProps",
384
384
  kind: "interface",
385
- code: "interface ScriptGoogleMapsHeatmapLayerProps {\n /**\n * Configuration options for the heatmap layer visualization.\n * @see https://developers.google.com/maps/documentation/javascript/reference/visualization#HeatmapLayerOptions\n */\n options?: Omit<google.maps.visualization.HeatmapLayerOptions, 'map'>\n}"
385
+ code: "interface ScriptGoogleMapsHeatmapLayerProps {\n /**\n * Configuration options for the heatmap layer visualization.\n * @deprecated Google deprecated the Maps JavaScript API `HeatmapLayer` (May 2025) and removed it in v3.65 (May 2026). Consider [deck.gl HeatmapLayer](https://deck.gl/docs/api-reference/aggregation-layers/heatmap-layer) instead.\n * @see https://developers.google.com/maps/documentation/javascript/reference/visualization#HeatmapLayerOptions\n */\n options?: Omit<HeatmapLayerOptions, 'map'>\n}"
386
386
  },
387
387
  {
388
388
  name: "ScriptGoogleMapsInfoWindowProps",
@@ -965,11 +965,23 @@ const types = {
965
965
  code: "export interface SnapPixelApi {\n snaptr: SnapTrFns & {\n push: SnapTrFns\n loaded: boolean\n version: string\n queue: any[]\n }\n _snaptr: SnapPixelApi['snaptr']\n handleRequest?: SnapTrFns\n}"
966
966
  }
967
967
  ],
968
+ speedcurve: [
969
+ {
970
+ name: "SpeedCurveOptions",
971
+ kind: "const",
972
+ code: "export const SpeedCurveOptions = object({\n // -- Composable-only options (consumed by useScriptSpeedCurve, not forwarded to LUX) --\n /**\n * Your SpeedCurve customer ID.\n * @see https://support.speedcurve.com/docs/add-rum-to-your-site\n */\n id: pipe(string(), minLength(1)),\n /**\n * Enable SPA (single-page application) mode.\n * When true, lux.js tracks soft navigations instead of full page loads.\n * @see https://support.speedcurve.com/docs/single-page-applications\n */\n spaMode: optional(boolean()),\n /**\n * Automatically wire Vue Router hooks for SPA tracking when spaMode is true.\n * Set to false to instrument navigations manually.\n * @default true (when spaMode is true)\n */\n autoTrackSpaNavigations: optional(boolean()),\n\n // -- LUX UserConfig passthrough (forwarded onto window.LUX before lux.js loads) --\n // Property names match upstream LUX UserConfig casing exactly. Keep this list in\n // sync with the LUX_USER_CONFIG_KEYS filter in speedcurve.ts.\n /**\n * Page label shown in the SpeedCurve dashboard.\n * Accepts a static string, a function `(to) => string | false` for per-navigation labels,\n * or `false` to disable labeling entirely. A callback returning `false` skips updating\n * the label for that navigation.\n * @default String(to.name ?? to.path)\n */\n label: optional(union([string(), function_(), literal(false)])),\n /**\n * Sampling rate (0–100). Percentage of sessions that send beacons.\n * Upstream spelling is lowercase — matches LUX UserConfig.\n */\n samplerate: optional(pipe(number(), minValue(0), maxValue(100))),\n /**\n * Send the beacon when the page is hidden (pagehide event).\n * @default true\n */\n sendBeaconOnPageHidden: optional(boolean()),\n /**\n * Track JavaScript errors.\n * @default true\n */\n trackErrors: optional(boolean()),\n /**\n * Maximum number of errors to track per page view.\n * @default 5\n */\n maxErrors: optional(number()),\n /**\n * Minimum time (ms) before a beacon can be sent.\n */\n minMeasureTime: optional(number()),\n /**\n * Maximum time (ms) after which the beacon is sent regardless of load state.\n * @default 60000\n */\n maxMeasureTime: optional(number()),\n /**\n * Start a new beacon when the page becomes visible after being hidden.\n */\n newBeaconOnPageShow: optional(boolean()),\n /**\n * Track pages loaded in background tabs.\n * @default false\n */\n trackHiddenPages: optional(boolean()),\n /**\n * Cookie domain for cross-subdomain session tracking.\n */\n cookieDomain: optional(string()),\n})"
973
+ },
974
+ {
975
+ name: "SpeedCurveApi",
976
+ kind: "interface",
977
+ code: "export interface SpeedCurveApi {\n LUX: LuxGlobal\n}"
978
+ }
979
+ ],
968
980
  stripe: [
969
981
  {
970
982
  name: "StripeOptions",
971
983
  kind: "const",
972
- code: "export const StripeOptions = object({\n /**\n * Whether to load Stripe's advanced fraud detection signals.\n * Set to `false` to opt out.\n * @default true\n * @see https://docs.stripe.com/disputes/prevention/advanced-fraud-detection\n */\n advancedFraudSignals: optional(boolean()),\n})"
984
+ code: "export const StripeOptions = object({\n /**\n * Whether to load Stripe's advanced fraud detection signals.\n * Set to `false` to opt out.\n * @default true\n * @see https://docs.stripe.com/disputes/prevention/advanced-fraud-detection\n */\n advancedFraudSignals: optional(boolean()),\n /**\n * The Stripe.js SDK version to load.\n * Named releases get autocomplete; any future codename is also accepted.\n * @default 'basil'\n * @see https://docs.stripe.com/sdks/stripejs-versioning\n */\n version: optional(union([literal('v3'), literal('acacia'), literal('basil'), literal('clover'), literal('dahlia'), string()])),\n})"
973
985
  },
974
986
  {
975
987
  name: "StripeApi",
@@ -2374,6 +2386,93 @@ const schemaFields = {
2374
2386
  description: "The user's age."
2375
2387
  }
2376
2388
  ],
2389
+ SpeedCurveOptions: [
2390
+ {
2391
+ name: "id",
2392
+ type: "string",
2393
+ required: true,
2394
+ description: "Your SpeedCurve customer ID."
2395
+ },
2396
+ {
2397
+ name: "spaMode",
2398
+ type: "boolean",
2399
+ required: false,
2400
+ description: "Enable SPA (single-page application) mode. When true, lux.js tracks soft navigations instead of full page loads."
2401
+ },
2402
+ {
2403
+ name: "autoTrackSpaNavigations",
2404
+ type: "boolean",
2405
+ required: false,
2406
+ description: "Automatically wire Vue Router hooks for SPA tracking when spaMode is true. Set to false to instrument navigations manually.",
2407
+ defaultValue: "true (when spaMode is true)"
2408
+ },
2409
+ {
2410
+ name: "label",
2411
+ type: "string | Function | false",
2412
+ required: false,
2413
+ description: "Page label shown in the SpeedCurve dashboard. Accepts a static string, a function `(to) => string | false` for per-navigation labels, or `false` to disable labeling entirely. A callback returning `false` skips updating the label for that navigation.",
2414
+ defaultValue: "String(to.name ?? to.path)"
2415
+ },
2416
+ {
2417
+ name: "samplerate",
2418
+ type: "number",
2419
+ required: false,
2420
+ description: "Sampling rate (0–100). Percentage of sessions that send beacons. Upstream spelling is lowercase — matches LUX UserConfig."
2421
+ },
2422
+ {
2423
+ name: "sendBeaconOnPageHidden",
2424
+ type: "boolean",
2425
+ required: false,
2426
+ description: "Send the beacon when the page is hidden (pagehide event).",
2427
+ defaultValue: "true"
2428
+ },
2429
+ {
2430
+ name: "trackErrors",
2431
+ type: "boolean",
2432
+ required: false,
2433
+ description: "Track JavaScript errors.",
2434
+ defaultValue: "true"
2435
+ },
2436
+ {
2437
+ name: "maxErrors",
2438
+ type: "number",
2439
+ required: false,
2440
+ description: "Maximum number of errors to track per page view.",
2441
+ defaultValue: "5"
2442
+ },
2443
+ {
2444
+ name: "minMeasureTime",
2445
+ type: "number",
2446
+ required: false,
2447
+ description: "Minimum time (ms) before a beacon can be sent."
2448
+ },
2449
+ {
2450
+ name: "maxMeasureTime",
2451
+ type: "number",
2452
+ required: false,
2453
+ description: "Maximum time (ms) after which the beacon is sent regardless of load state.",
2454
+ defaultValue: "60000"
2455
+ },
2456
+ {
2457
+ name: "newBeaconOnPageShow",
2458
+ type: "boolean",
2459
+ required: false,
2460
+ description: "Start a new beacon when the page becomes visible after being hidden."
2461
+ },
2462
+ {
2463
+ name: "trackHiddenPages",
2464
+ type: "boolean",
2465
+ required: false,
2466
+ description: "Track pages loaded in background tabs.",
2467
+ defaultValue: "false"
2468
+ },
2469
+ {
2470
+ name: "cookieDomain",
2471
+ type: "string",
2472
+ required: false,
2473
+ description: "Cookie domain for cross-subdomain session tracking."
2474
+ }
2475
+ ],
2377
2476
  SnapTrPixelOptions: [
2378
2477
  {
2379
2478
  name: "id",
@@ -2395,6 +2494,13 @@ const schemaFields = {
2395
2494
  required: false,
2396
2495
  description: "Whether to load Stripe's advanced fraud detection signals. Set to `false` to opt out.",
2397
2496
  defaultValue: "true"
2497
+ },
2498
+ {
2499
+ name: "version",
2500
+ type: "'v3' | 'acacia' | 'basil' | 'clover' | 'dahlia' | string",
2501
+ required: false,
2502
+ description: "The Stripe.js SDK version to load. Named releases get autocomplete; any future codename is also accepted.",
2503
+ defaultValue: "'basil'"
2398
2504
  }
2399
2505
  ],
2400
2506
  TikTokPixelOptions: [
@@ -2747,7 +2853,7 @@ const schemaFields = {
2747
2853
  ScriptGoogleMapsHeatmapLayerProps: [
2748
2854
  {
2749
2855
  name: "options",
2750
- type: "Omit<google.maps.visualization.HeatmapLayerOptions, 'map'>",
2856
+ type: "Omit<HeatmapLayerOptions, 'map'>",
2751
2857
  required: false,
2752
2858
  description: "Configuration options for the heatmap layer visualization."
2753
2859
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxt/scripts",
3
3
  "type": "module",
4
- "version": "1.1.1",
4
+ "version": "1.2.1",
5
5
  "description": "Load third-party scripts with better performance, privacy and DX in Nuxt Apps.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -71,11 +71,12 @@
71
71
  "peerDependencies": {
72
72
  "@googlemaps/markerclusterer": "^2.6.2",
73
73
  "@paypal/paypal-js": "^8.1.2 || ^9.0.0",
74
+ "@speedcurve/lux": "^4.4.3",
74
75
  "@stripe/stripe-js": "^7.0.0 || ^8.0.0 || ^9.0.0",
75
76
  "@types/google.maps": "^3.58.1",
76
77
  "@types/vimeo__player": "^2.18.3",
77
78
  "@types/youtube": "^0.1.0",
78
- "@unhead/vue": "^2.0.3",
79
+ "@unhead/vue": "^2.0.3 || ^3.0.0",
79
80
  "posthog-js": "^1.0.0"
80
81
  },
81
82
  "peerDependenciesMeta": {
@@ -85,6 +86,9 @@
85
86
  "@paypal/paypal-js": {
86
87
  "optional": true
87
88
  },
89
+ "@speedcurve/lux": {
90
+ "optional": true
91
+ },
88
92
  "@stripe/stripe-js": {
89
93
  "optional": true
90
94
  },
@@ -111,7 +115,7 @@
111
115
  "magic-string": "^0.30.21",
112
116
  "ofetch": "^1.5.1",
113
117
  "ohash": "^2.0.11",
114
- "oxc-parser": "^0.132.0",
118
+ "oxc-parser": "^0.134.0",
115
119
  "oxc-walker": "^1.0.0",
116
120
  "pathe": "^2.0.3",
117
121
  "pkg-types": "^2.3.1",
@@ -121,11 +125,12 @@
121
125
  "ultrahtml": "^1.6.0",
122
126
  "unplugin": "^3.0.0",
123
127
  "unstorage": "^1.17.5",
124
- "valibot": "^1.4.0"
128
+ "valibot": "^1.4.1"
125
129
  },
126
130
  "devDependencies": {
127
131
  "@nuxt/kit": "^4.4.6",
128
132
  "@nuxt/module-builder": "^1.0.2",
133
+ "@speedcurve/lux": "^4.4.3",
129
134
  "rollup": "^4.60.4",
130
135
  "unbuild": "^3.6.1",
131
136
  "unimport": "^6.3.0"