@nuxt/scripts 0.13.2 → 1.0.0-beta.2

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 (98) hide show
  1. package/README.md +15 -0
  2. package/dist/client/200.html +1 -1
  3. package/dist/client/404.html +1 -1
  4. package/dist/client/_nuxt/B66N9HCo.js +1 -0
  5. package/dist/client/_nuxt/B8XOar-X.js +162 -0
  6. package/dist/client/_nuxt/{Bje-0OHL.js → DfLgoB--.js} +1 -1
  7. package/dist/client/_nuxt/DvH517bE.js +1 -0
  8. package/dist/client/_nuxt/builds/latest.json +1 -1
  9. package/dist/client/_nuxt/builds/meta/133a46c5-a5c1-4a63-87d1-037947a5bcdb.json +1 -0
  10. package/dist/client/_nuxt/entry.D45OuV0w.css +1 -0
  11. package/dist/client/_nuxt/error-404.B57D-jUQ.css +1 -0
  12. package/dist/client/_nuxt/error-500.DTHUW7BI.css +1 -0
  13. package/dist/client/index.html +1 -1
  14. package/dist/module.d.mts +106 -4
  15. package/dist/module.json +1 -1
  16. package/dist/module.mjs +705 -173
  17. package/dist/registry.mjs +63 -0
  18. package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.d.vue.ts +29 -1
  19. package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.vue +35 -10
  20. package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.vue.d.ts +29 -1
  21. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsMarkerClusterer.d.vue.ts +20 -8
  22. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsMarkerClusterer.vue +2 -2
  23. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsMarkerClusterer.vue.d.ts +20 -8
  24. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsPinElement.vue +7 -1
  25. package/dist/runtime/components/ScriptCrisp.d.vue.ts +1 -1
  26. package/dist/runtime/components/ScriptCrisp.vue.d.ts +1 -1
  27. package/dist/runtime/components/ScriptInstagramEmbed.d.vue.ts +53 -0
  28. package/dist/runtime/components/ScriptInstagramEmbed.vue +38 -0
  29. package/dist/runtime/components/ScriptInstagramEmbed.vue.d.ts +53 -0
  30. package/dist/runtime/components/ScriptIntercom.d.vue.ts +1 -1
  31. package/dist/runtime/components/ScriptIntercom.vue.d.ts +1 -1
  32. package/dist/runtime/components/ScriptVimeoPlayer.d.vue.ts +2 -2
  33. package/dist/runtime/components/ScriptVimeoPlayer.vue.d.ts +2 -2
  34. package/dist/runtime/components/ScriptXEmbed.d.vue.ts +82 -0
  35. package/dist/runtime/components/ScriptXEmbed.vue +76 -0
  36. package/dist/runtime/components/ScriptXEmbed.vue.d.ts +82 -0
  37. package/dist/runtime/components/ScriptYouTubePlayer.d.vue.ts +12 -1
  38. package/dist/runtime/components/ScriptYouTubePlayer.vue +41 -16
  39. package/dist/runtime/components/ScriptYouTubePlayer.vue.d.ts +12 -1
  40. package/dist/runtime/composables/useScript.js +34 -3
  41. package/dist/runtime/composables/useScriptTriggerServiceWorker.d.ts +7 -0
  42. package/dist/runtime/composables/useScriptTriggerServiceWorker.js +39 -0
  43. package/dist/runtime/npm-script-stub.d.ts +20 -0
  44. package/dist/runtime/npm-script-stub.js +73 -0
  45. package/dist/runtime/plugins/sw-register.client.d.ts +2 -0
  46. package/dist/runtime/plugins/sw-register.client.js +12 -0
  47. package/dist/runtime/registry/google-recaptcha.d.ts +27 -0
  48. package/dist/runtime/registry/google-recaptcha.js +45 -0
  49. package/dist/runtime/registry/google-sign-in.d.ts +84 -0
  50. package/dist/runtime/registry/google-sign-in.js +50 -0
  51. package/dist/runtime/registry/google-tag-manager.d.ts +3 -1
  52. package/dist/runtime/registry/google-tag-manager.js +15 -5
  53. package/dist/runtime/registry/instagram-embed.d.ts +23 -0
  54. package/dist/runtime/registry/instagram-embed.js +22 -0
  55. package/dist/runtime/registry/lemon-squeezy.d.ts +0 -1
  56. package/dist/runtime/registry/matomo-analytics.js +1 -1
  57. package/dist/runtime/registry/plausible-analytics.js +8 -6
  58. package/dist/runtime/registry/posthog.d.ts +26 -0
  59. package/dist/runtime/registry/posthog.js +92 -0
  60. package/dist/runtime/registry/rybbit-analytics.js +38 -8
  61. package/dist/runtime/registry/tiktok-pixel.d.ts +44 -0
  62. package/dist/runtime/registry/tiktok-pixel.js +44 -0
  63. package/dist/runtime/registry/x-embed.d.ts +77 -0
  64. package/dist/runtime/registry/x-embed.js +41 -0
  65. package/dist/runtime/server/google-static-maps-proxy.d.ts +2 -0
  66. package/dist/runtime/server/google-static-maps-proxy.js +54 -0
  67. package/dist/runtime/server/instagram-embed-asset.d.ts +2 -0
  68. package/dist/runtime/server/instagram-embed-asset.js +42 -0
  69. package/dist/runtime/server/instagram-embed-image.d.ts +2 -0
  70. package/dist/runtime/server/instagram-embed-image.js +54 -0
  71. package/dist/runtime/server/instagram-embed.d.ts +2 -0
  72. package/dist/runtime/server/instagram-embed.js +91 -0
  73. package/dist/runtime/server/proxy-handler.d.ts +6 -0
  74. package/dist/runtime/server/proxy-handler.js +230 -0
  75. package/dist/runtime/server/sw-handler.d.ts +2 -0
  76. package/dist/runtime/server/sw-handler.js +25 -0
  77. package/dist/runtime/server/utils/privacy.d.ts +97 -0
  78. package/dist/runtime/server/utils/privacy.js +268 -0
  79. package/dist/runtime/server/x-embed-image.d.ts +2 -0
  80. package/dist/runtime/server/x-embed-image.js +53 -0
  81. package/dist/runtime/server/x-embed.d.ts +49 -0
  82. package/dist/runtime/server/x-embed.js +31 -0
  83. package/dist/runtime/sw/proxy-sw.template.d.ts +1 -0
  84. package/dist/runtime/sw/proxy-sw.template.js +54 -0
  85. package/dist/runtime/types.d.ts +42 -1
  86. package/dist/runtime/utils/pure.d.ts +13 -0
  87. package/dist/runtime/utils/pure.js +67 -0
  88. package/dist/runtime/utils.d.ts +3 -2
  89. package/dist/runtime/utils.js +11 -1
  90. package/dist/types.d.mts +1 -1
  91. package/package.json +39 -32
  92. package/dist/client/_nuxt/DMut0W-e.js +0 -162
  93. package/dist/client/_nuxt/builds/meta/5e0206fe-a683-423c-8d59-2596d0b16fee.json +0 -1
  94. package/dist/client/_nuxt/entry.BjfcJo5q.css +0 -1
  95. package/dist/client/_nuxt/error-404.B0ZhSNwd.css +0 -1
  96. package/dist/client/_nuxt/error-500.D4MdgPaC.css +0 -1
  97. package/dist/client/_nuxt/iNmKC7TZ.js +0 -1
  98. package/dist/client/_nuxt/rttsH3SL.js +0 -1
package/dist/module.mjs CHANGED
@@ -1,11 +1,13 @@
1
- import { useNuxt, extendViteConfig, useLogger, addDevServerHandler, tryUseNuxt, logger as logger$1, defineNuxtModule, createResolver, addImports, addComponentsDir, addTemplate, addTypeTemplate, addPluginTemplate, addBuildPlugin, hasNuxtModule } from '@nuxt/kit';
1
+ import { useNuxt, extendViteConfig, useLogger, addDevServerHandler, extendRouteRules, tryUseNuxt, logger as logger$1, addTypeTemplate, defineNuxtModule, createResolver, hasNuxtModule, addImports, addComponentsDir, addTemplate, addServerHandler, addPluginTemplate, addBuildPlugin } from '@nuxt/kit';
2
+ import { existsSync, readFileSync } from 'node:fs';
2
3
  import { defu } from 'defu';
3
4
  import { resolvePackageJSON, readPackageJSON } from 'pkg-types';
4
- import { existsSync } from 'node:fs';
5
+ import { addCustomTab } from '@nuxt/devtools-kit';
6
+ import { createHash } from 'node:crypto';
5
7
  import fsp from 'node:fs/promises';
6
8
  import { createUnplugin } from 'unplugin';
7
9
  import MagicString from 'magic-string';
8
- import { asyncWalk, walk } from 'estree-walker';
10
+ import { parseAndWalk } from 'oxc-walker';
9
11
  import { joinURL, parseURL, parseQuery, hasProtocol } from 'ufo';
10
12
  import { hash } from 'ohash';
11
13
  import { join, resolve, relative } from 'pathe';
@@ -14,6 +16,7 @@ import { fetch, $fetch } from 'ofetch';
14
16
  import { lazyEventHandler, eventHandler, createError } from 'h3';
15
17
  import { createStorage } from 'unstorage';
16
18
  import fsDriver from 'unstorage/drivers/fs-lite';
19
+ import { rewriteScriptUrls } from '../dist/runtime/utils/pure.js';
17
20
  import { pathToFileURL } from 'node:url';
18
21
  import { isCI, provider } from 'std-env';
19
22
  import { registry } from './registry.mjs';
@@ -44,20 +47,18 @@ async function setupDevToolsUI(options, resolve, nuxt = useNuxt()) {
44
47
  };
45
48
  });
46
49
  }
47
- nuxt.hook("devtools:customTabs", (tabs) => {
48
- tabs.push({
49
- // unique identifier
50
- name: "nuxt-scripts",
51
- // title to display in the tab
52
- title: "Scripts",
53
- // any icon from Iconify, or a URL to an image
54
- icon: "carbon:script",
55
- // iframe view
56
- view: {
57
- type: "iframe",
58
- src: DEVTOOLS_UI_ROUTE
59
- }
60
- });
50
+ addCustomTab({
51
+ // unique identifier
52
+ name: "nuxt-scripts",
53
+ // title to display in the tab
54
+ title: "Scripts",
55
+ // any icon from Iconify, or a URL to an image
56
+ icon: "carbon:script",
57
+ // iframe view
58
+ view: {
59
+ type: "iframe",
60
+ src: DEVTOOLS_UI_ROUTE
61
+ }
61
62
  });
62
63
  }
63
64
 
@@ -85,6 +86,9 @@ function setupPublicAssetStrategy(options = {}) {
85
86
  const scriptDescriptor = renderedScript.get(join(assetsBaseURL, event.path.slice(1)));
86
87
  if (!scriptDescriptor || scriptDescriptor instanceof Error)
87
88
  throw createError({ statusCode: 404 });
89
+ if (scriptDescriptor.content) {
90
+ return scriptDescriptor.content;
91
+ }
88
92
  const key = `bundle:${filename}`;
89
93
  let res = await storage.getItemRaw(key);
90
94
  if (!res) {
@@ -96,31 +100,204 @@ function setupPublicAssetStrategy(options = {}) {
96
100
  })
97
101
  });
98
102
  if (nuxt.options.dev) {
99
- nuxt.options.routeRules ||= {};
100
- nuxt.options.routeRules[joinURL(assetsBaseURL, "**")] = {
103
+ extendRouteRules(joinURL(assetsBaseURL, "**"), {
101
104
  cache: {
102
105
  maxAge: ONE_YEAR_IN_SECONDS
103
106
  }
104
- };
107
+ });
105
108
  }
106
- nuxt.options.nitro.publicAssets ||= [];
107
109
  const cacheDir = join(nuxt.options.buildDir, "cache", "scripts");
108
- nuxt.options.nitro.publicAssets.push();
109
- nuxt.options.nitro = defu(nuxt.options.nitro, {
110
- publicAssets: [{
110
+ nuxt.hook("nitro:config", (nitroConfig) => {
111
+ nitroConfig.publicAssets ||= [];
112
+ nitroConfig.publicAssets.push({
111
113
  dir: cacheDir,
112
114
  maxAge: ONE_YEAR_IN_SECONDS,
113
115
  baseURL: assetsBaseURL
114
- }],
115
- prerender: {
116
- ignore: [assetsBaseURL]
117
- }
116
+ });
117
+ nitroConfig.prerender ||= {};
118
+ nitroConfig.prerender.ignore ||= [];
119
+ nitroConfig.prerender.ignore.push(assetsBaseURL);
118
120
  });
119
121
  return {
120
122
  renderedScript
121
123
  };
122
124
  }
123
125
 
126
+ function buildProxyConfig(collectPrefix) {
127
+ return {
128
+ googleAnalytics: {
129
+ rewrite: [
130
+ // Modern gtag.js uses www.google.com/g/collect
131
+ { from: "www.google.com/g/collect", to: `${collectPrefix}/ga/g/collect` },
132
+ // Older gtag.js constructs URLs dynamically: "https://"+(subdomain)+".google-analytics.com/g/collect"
133
+ // We need to catch the suffix pattern with leading dot
134
+ { from: ".google-analytics.com/g/collect", to: `${collectPrefix}/ga/g/collect` },
135
+ { from: ".analytics.google.com/g/collect", to: `${collectPrefix}/ga/g/collect` },
136
+ // Full domain patterns for static URLs
137
+ { from: "www.google-analytics.com/g/collect", to: `${collectPrefix}/ga/g/collect` },
138
+ { from: "analytics.google.com/g/collect", to: `${collectPrefix}/ga/g/collect` },
139
+ // Legacy endpoints still used by some scripts
140
+ { from: "www.google-analytics.com", to: `${collectPrefix}/ga` },
141
+ { from: "analytics.google.com", to: `${collectPrefix}/ga` },
142
+ // DoubleClick tracking (used by GA4 for ads/conversions)
143
+ { from: "stats.g.doubleclick.net/g/collect", to: `${collectPrefix}/ga/g/collect` },
144
+ { from: "stats.g.doubleclick.net", to: `${collectPrefix}/ga-dc` },
145
+ // Google Ads/Syndication tracking
146
+ { from: "pagead2.googlesyndication.com", to: `${collectPrefix}/ga-syn` },
147
+ { from: "www.googleadservices.com", to: `${collectPrefix}/ga-ads` },
148
+ { from: "googleads.g.doubleclick.net", to: `${collectPrefix}/ga-gads` }
149
+ ],
150
+ routes: {
151
+ [`${collectPrefix}/ga/**`]: { proxy: "https://www.google-analytics.com/**" },
152
+ [`${collectPrefix}/ga-dc/**`]: { proxy: "https://stats.g.doubleclick.net/**" },
153
+ [`${collectPrefix}/ga-syn/**`]: { proxy: "https://pagead2.googlesyndication.com/**" },
154
+ [`${collectPrefix}/ga-ads/**`]: { proxy: "https://www.googleadservices.com/**" },
155
+ [`${collectPrefix}/ga-gads/**`]: { proxy: "https://googleads.g.doubleclick.net/**" }
156
+ }
157
+ },
158
+ googleTagManager: {
159
+ rewrite: [
160
+ { from: "www.googletagmanager.com", to: `${collectPrefix}/gtm` }
161
+ ],
162
+ routes: {
163
+ [`${collectPrefix}/gtm/**`]: { proxy: "https://www.googletagmanager.com/**" }
164
+ }
165
+ },
166
+ metaPixel: {
167
+ rewrite: [
168
+ // SDK script loading
169
+ { from: "connect.facebook.net", to: `${collectPrefix}/meta` },
170
+ // Tracking pixel endpoint (www and non-www)
171
+ { from: "www.facebook.com/tr", to: `${collectPrefix}/meta-tr` },
172
+ { from: "facebook.com/tr", to: `${collectPrefix}/meta-tr` },
173
+ // Additional Meta tracking domains
174
+ { from: "pixel.facebook.com", to: `${collectPrefix}/meta-px` },
175
+ { from: "www.facebook.com/plugins", to: `${collectPrefix}/meta-plugins` }
176
+ ],
177
+ routes: {
178
+ [`${collectPrefix}/meta/**`]: { proxy: "https://connect.facebook.net/**" },
179
+ [`${collectPrefix}/meta-tr/**`]: { proxy: "https://www.facebook.com/tr/**" },
180
+ [`${collectPrefix}/meta-px/**`]: { proxy: "https://pixel.facebook.com/**" },
181
+ [`${collectPrefix}/meta-plugins/**`]: { proxy: "https://www.facebook.com/plugins/**" }
182
+ }
183
+ },
184
+ tiktokPixel: {
185
+ rewrite: [
186
+ { from: "analytics.tiktok.com", to: `${collectPrefix}/tiktok` }
187
+ ],
188
+ routes: {
189
+ [`${collectPrefix}/tiktok/**`]: { proxy: "https://analytics.tiktok.com/**" }
190
+ }
191
+ },
192
+ segment: {
193
+ rewrite: [
194
+ { from: "api.segment.io", to: `${collectPrefix}/segment` },
195
+ { from: "cdn.segment.com", to: `${collectPrefix}/segment-cdn` }
196
+ ],
197
+ routes: {
198
+ [`${collectPrefix}/segment/**`]: { proxy: "https://api.segment.io/**" },
199
+ [`${collectPrefix}/segment-cdn/**`]: { proxy: "https://cdn.segment.com/**" }
200
+ }
201
+ },
202
+ xPixel: {
203
+ rewrite: [
204
+ { from: "analytics.twitter.com", to: `${collectPrefix}/x` },
205
+ { from: "t.co", to: `${collectPrefix}/x-t` }
206
+ ],
207
+ routes: {
208
+ [`${collectPrefix}/x/**`]: { proxy: "https://analytics.twitter.com/**" },
209
+ [`${collectPrefix}/x-t/**`]: { proxy: "https://t.co/**" }
210
+ }
211
+ },
212
+ snapchatPixel: {
213
+ rewrite: [
214
+ { from: "tr.snapchat.com", to: `${collectPrefix}/snap` }
215
+ ],
216
+ routes: {
217
+ [`${collectPrefix}/snap/**`]: { proxy: "https://tr.snapchat.com/**" }
218
+ }
219
+ },
220
+ redditPixel: {
221
+ rewrite: [
222
+ { from: "alb.reddit.com", to: `${collectPrefix}/reddit` }
223
+ ],
224
+ routes: {
225
+ [`${collectPrefix}/reddit/**`]: { proxy: "https://alb.reddit.com/**" }
226
+ }
227
+ },
228
+ clarity: {
229
+ rewrite: [
230
+ // Main clarity domain
231
+ { from: "www.clarity.ms", to: `${collectPrefix}/clarity` },
232
+ // Script loader (the actual SDK is loaded from here)
233
+ { from: "scripts.clarity.ms", to: `${collectPrefix}/clarity-scripts` },
234
+ // Data collection endpoint
235
+ { from: "d.clarity.ms", to: `${collectPrefix}/clarity-data` },
236
+ // Event collection endpoint
237
+ { from: "e.clarity.ms", to: `${collectPrefix}/clarity-events` }
238
+ ],
239
+ routes: {
240
+ [`${collectPrefix}/clarity/**`]: { proxy: "https://www.clarity.ms/**" },
241
+ [`${collectPrefix}/clarity-scripts/**`]: { proxy: "https://scripts.clarity.ms/**" },
242
+ [`${collectPrefix}/clarity-data/**`]: { proxy: "https://d.clarity.ms/**" },
243
+ [`${collectPrefix}/clarity-events/**`]: { proxy: "https://e.clarity.ms/**" }
244
+ }
245
+ },
246
+ hotjar: {
247
+ rewrite: [
248
+ // Static assets
249
+ { from: "static.hotjar.com", to: `${collectPrefix}/hotjar` },
250
+ // Script loader (bootstrap script loads the main SDK from here)
251
+ { from: "script.hotjar.com", to: `${collectPrefix}/hotjar-script` },
252
+ // Configuration/variables
253
+ { from: "vars.hotjar.com", to: `${collectPrefix}/hotjar-vars` },
254
+ // Data ingestion endpoint
255
+ { from: "in.hotjar.com", to: `${collectPrefix}/hotjar-in` },
256
+ // Video capture
257
+ { from: "vc.hotjar.com", to: `${collectPrefix}/hotjar-vc` },
258
+ // Metrics/telemetry
259
+ { from: "metrics.hotjar.io", to: `${collectPrefix}/hotjar-metrics` },
260
+ // Insights (ContentSquare integration)
261
+ { from: "insights.hotjar.com", to: `${collectPrefix}/hotjar-insights` }
262
+ ],
263
+ routes: {
264
+ [`${collectPrefix}/hotjar/**`]: { proxy: "https://static.hotjar.com/**" },
265
+ [`${collectPrefix}/hotjar-script/**`]: { proxy: "https://script.hotjar.com/**" },
266
+ [`${collectPrefix}/hotjar-vars/**`]: { proxy: "https://vars.hotjar.com/**" },
267
+ [`${collectPrefix}/hotjar-in/**`]: { proxy: "https://in.hotjar.com/**" },
268
+ [`${collectPrefix}/hotjar-vc/**`]: { proxy: "https://vc.hotjar.com/**" },
269
+ [`${collectPrefix}/hotjar-metrics/**`]: { proxy: "https://metrics.hotjar.io/**" },
270
+ [`${collectPrefix}/hotjar-insights/**`]: { proxy: "https://insights.hotjar.com/**" }
271
+ }
272
+ }
273
+ };
274
+ }
275
+ function getProxyConfig(key, collectPrefix) {
276
+ const configs = buildProxyConfig(collectPrefix);
277
+ return configs[key];
278
+ }
279
+ function getAllProxyConfigs(collectPrefix) {
280
+ return buildProxyConfig(collectPrefix);
281
+ }
282
+ function getSWInterceptRules(collectPrefix) {
283
+ const configs = buildProxyConfig(collectPrefix);
284
+ const rules = [];
285
+ for (const config of Object.values(configs)) {
286
+ if (!config.routes)
287
+ continue;
288
+ for (const [localPath, { proxy }] of Object.entries(config.routes)) {
289
+ const match = proxy.match(/^https?:\/\/([^/]+)(\/.*)?\/\*\*$/);
290
+ if (match?.[1]) {
291
+ const domain = match[1];
292
+ const pathPrefix = match[2] || "";
293
+ const target = localPath.replace(/\/\*\*$/, "");
294
+ rules.push({ pattern: domain, pathPrefix, target });
295
+ }
296
+ }
297
+ }
298
+ return rules;
299
+ }
300
+
124
301
  function isVue(id, opts = {}) {
125
302
  const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href));
126
303
  if (id.endsWith(".vue") && !search) {
@@ -149,6 +326,10 @@ function isJS(id) {
149
326
  }
150
327
 
151
328
  const SEVEN_DAYS_IN_MS = 7 * 24 * 60 * 60 * 1e3;
329
+ function calculateIntegrity(content, algorithm = "sha384") {
330
+ const hash = createHash(algorithm).update(content).digest("base64");
331
+ return `${algorithm}-${hash}`;
332
+ }
152
333
  async function isCacheExpired(storage, filename, cacheMaxAge = SEVEN_DAYS_IN_MS) {
153
334
  const metaKey = `bundle-meta:${filename}`;
154
335
  const meta = await storage.getItem(metaKey);
@@ -173,7 +354,7 @@ function normalizeScriptData(src, assetsBaseURL = "/_scripts") {
173
354
  return { url: src };
174
355
  }
175
356
  async function downloadScript(opts, renderedScript, fetchOptions, cacheMaxAge) {
176
- const { src, url, filename, forceDownload } = opts;
357
+ const { src, url, filename, forceDownload, integrity, proxyRewrites } = opts;
177
358
  if (src === url || !filename) {
178
359
  return;
179
360
  }
@@ -181,16 +362,19 @@ async function downloadScript(opts, renderedScript, fetchOptions, cacheMaxAge) {
181
362
  const scriptContent = renderedScript.get(src);
182
363
  let res = scriptContent instanceof Error ? void 0 : scriptContent?.content;
183
364
  if (!res) {
184
- const cacheKey = `bundle:${filename}`;
365
+ const proxyRewritesHash = proxyRewrites?.length ? `-${hash(proxyRewrites)}` : "";
366
+ const cacheKey = proxyRewrites?.length ? `bundle-proxy:${filename.replace(".js", `${proxyRewritesHash}.js`)}` : `bundle:${filename}`;
185
367
  const shouldUseCache = !forceDownload && await storage.hasItem(cacheKey) && !await isCacheExpired(storage, filename, cacheMaxAge);
186
368
  if (shouldUseCache) {
187
- const res2 = await storage.getItemRaw(cacheKey);
369
+ const cachedContent = await storage.getItemRaw(cacheKey);
370
+ const meta = await storage.getItem(`bundle-meta:${filename}`);
188
371
  renderedScript.set(url, {
189
- content: res2,
190
- size: res2.length / 1024,
372
+ content: cachedContent,
373
+ size: cachedContent.length / 1024,
191
374
  encoding: "utf-8",
192
375
  src,
193
- filename
376
+ filename,
377
+ integrity: meta?.integrity
194
378
  });
195
379
  return;
196
380
  }
@@ -206,19 +390,29 @@ async function downloadScript(opts, renderedScript, fetchOptions, cacheMaxAge) {
206
390
  return Buffer.from(r._data || await r.arrayBuffer());
207
391
  });
208
392
  await storage.setItemRaw(`bundle:${filename}`, res);
393
+ if (proxyRewrites?.length && res) {
394
+ const content = res.toString("utf-8");
395
+ const rewritten = rewriteScriptUrls(content, proxyRewrites);
396
+ res = Buffer.from(rewritten, "utf-8");
397
+ logger.debug(`Rewrote ${proxyRewrites.length} URL patterns in ${filename}`);
398
+ }
399
+ const integrityHash = integrity && res ? calculateIntegrity(res, integrity === true ? "sha384" : integrity) : void 0;
400
+ await storage.setItemRaw(cacheKey, res);
209
401
  await storage.setItem(`bundle-meta:${filename}`, {
210
402
  timestamp: Date.now(),
211
403
  src,
212
- filename
404
+ filename,
405
+ integrity: integrityHash
213
406
  });
214
407
  size = size || res.length / 1024;
215
- logger.info(`Downloading script ${colors.gray(`${src} \u2192 ${filename} (${size.toFixed(2)} kB ${encoding})`)}`);
408
+ logger.info(`Downloading script ${colors.gray(`${src} \u2192 ${filename} (${size.toFixed(2)} kB ${encoding})${integrityHash ? ` [${integrityHash.slice(0, 15)}...]` : ""}`)}`);
216
409
  renderedScript.set(url, {
217
410
  content: res,
218
411
  size,
219
412
  encoding,
220
413
  src,
221
- filename
414
+ filename,
415
+ integrity: integrityHash
222
416
  });
223
417
  }
224
418
  }
@@ -252,16 +446,21 @@ function NuxtScriptBundleTransformer(options = {
252
446
  return createUnplugin(() => {
253
447
  return {
254
448
  name: "nuxt:scripts:bundler-transformer",
255
- transformInclude(id) {
256
- return isVue(id, { type: ["template", "script"] }) || isJS(id);
257
- },
258
- async transform(code, id) {
259
- if (!code.includes("useScript"))
260
- return;
261
- const ast = this.parse(code);
262
- const s = new MagicString(code);
263
- await asyncWalk(ast, {
264
- async enter(_node) {
449
+ transform: {
450
+ filter: {
451
+ id: {
452
+ include: [/\.vue/, /\.[cm]?[jt]sx?$/],
453
+ exclude: [/\.(?:test|spec)\./]
454
+ }
455
+ },
456
+ async handler(code, id) {
457
+ if (!isVue(id, { type: ["template", "script"] }) && !isJS(id))
458
+ return;
459
+ if (!code.includes("useScript"))
460
+ return;
461
+ const s = new MagicString(code);
462
+ const deferredOps = [];
463
+ parseAndWalk(code, id, function(_node) {
265
464
  const calleeName = _node.callee?.name;
266
465
  if (!calleeName)
267
466
  return;
@@ -271,6 +470,11 @@ function NuxtScriptBundleTransformer(options = {
271
470
  const node = _node;
272
471
  let scriptSrcNode;
273
472
  let src;
473
+ let registryKey;
474
+ if (fnName !== "useScript") {
475
+ const baseName = fnName.replace(/^useScript/, "");
476
+ registryKey = baseName.length > 0 ? baseName.charAt(0).toLowerCase() + baseName.slice(1) : void 0;
477
+ }
274
478
  if (fnName === "useScript") {
275
479
  if (node.arguments[0]?.type === "Literal") {
276
480
  scriptSrcNode = node.arguments[0];
@@ -287,9 +491,7 @@ function NuxtScriptBundleTransformer(options = {
287
491
  }
288
492
  if (!registryNode.scriptBundling && !registryNode.src)
289
493
  return;
290
- const baseName = fnName.replace(/^useScript/, "");
291
- const registryKey = baseName.length > 0 ? baseName.charAt(0).toLowerCase() + baseName.slice(1) : "";
292
- const registryConfig = options.registryConfig?.[registryKey] || {};
494
+ const registryConfig = options.registryConfig?.[registryKey || ""] || {};
293
495
  const fnArg0 = {};
294
496
  if (node.arguments[0]?.type === "ObjectExpression") {
295
497
  const optionsNode = node.arguments[0];
@@ -309,6 +511,8 @@ function NuxtScriptBundleTransformer(options = {
309
511
  src = registryNode.scriptBundling && registryNode.scriptBundling(mergedOptions);
310
512
  if (src === false)
311
513
  return;
514
+ if (!src && registryNode.src)
515
+ src = registryNode.src;
312
516
  }
313
517
  }
314
518
  if (!scriptSrcNode && !src) {
@@ -323,8 +527,7 @@ function NuxtScriptBundleTransformer(options = {
323
527
  if (bundleProperty && bundleProperty.value.type === "Literal") {
324
528
  const bundleValue = bundleProperty.value.value;
325
529
  if (bundleValue === true || bundleValue === "force" || String(bundleValue) === "true") {
326
- const valueNode = bundleProperty.value;
327
- s.overwrite(valueNode.start, valueNode.end, `'unsupported'`);
530
+ s.overwrite(bundleProperty.value.start, bundleProperty.value.end, `'unsupported'`);
328
531
  }
329
532
  }
330
533
  }
@@ -341,8 +544,7 @@ function NuxtScriptBundleTransformer(options = {
341
544
  (p) => (p.key?.name === "bundle" || p.key?.value === "bundle") && p.type === "Property"
342
545
  );
343
546
  if (bundleProperty && bundleProperty.value.type === "Literal") {
344
- const value = bundleProperty.value;
345
- const bundleValue = value.value;
547
+ const bundleValue = bundleProperty.value.value;
346
548
  if (bundleValue !== true && bundleValue !== "force" && String(bundleValue) !== "true") {
347
549
  canBundle = false;
348
550
  return;
@@ -370,67 +572,104 @@ function NuxtScriptBundleTransformer(options = {
370
572
  canBundle = bundleValue === true || bundleValue === "force" || String(bundleValue) === "true";
371
573
  forceDownload = bundleValue === "force";
372
574
  }
575
+ const firstPartyOption = scriptOptions?.value.properties?.find((prop) => {
576
+ return prop.type === "Property" && prop.key?.name === "firstParty" && prop.value.type === "Literal";
577
+ });
578
+ let firstPartyOptOut = firstPartyOption?.value.value === false;
579
+ if (!firstPartyOptOut && node.arguments[1]?.type === "ObjectExpression") {
580
+ const secondArgFirstPartyProp = node.arguments[1].properties.find(
581
+ (p) => p.type === "Property" && p.key?.name === "firstParty" && p.value.type === "Literal"
582
+ );
583
+ firstPartyOptOut = secondArgFirstPartyProp?.value.value === false;
584
+ }
585
+ if (!firstPartyOptOut && node.arguments[0]?.type === "ObjectExpression") {
586
+ const firstArgFirstPartyProp = node.arguments[0].properties.find(
587
+ (p) => p.type === "Property" && p.key?.name === "firstParty" && p.value.type === "Literal"
588
+ );
589
+ firstPartyOptOut = firstArgFirstPartyProp?.value.value === false;
590
+ }
373
591
  if (canBundle) {
374
592
  const { url: _url, filename } = normalizeScriptData(src, options.assetsBaseURL);
375
- let url = _url;
376
- try {
377
- await downloadScript({ src, url, filename, forceDownload }, renderedScript, options.fetchOptions, options.cacheMaxAge);
378
- } catch (e) {
379
- if (options.fallbackOnSrcOnBundleFail) {
380
- logger.warn(`[Nuxt Scripts: Bundle Transformer] Failed to bundle ${src}. Fallback to remote loading.`);
381
- url = src;
382
- } else {
383
- const errorMessage = e?.message || "Unknown error";
384
- if (errorMessage.includes("timeout") || errorMessage.includes("network") || errorMessage.includes("ENOTFOUND")) {
385
- logger.error(`[Nuxt Scripts: Bundle Transformer] Network issue while bundling ${src}: ${errorMessage}`);
386
- logger.error(`[Nuxt Scripts: Bundle Transformer] Tip: Set 'fallbackOnSrcOnBundleFail: true' in module options or disable bundling in Docker environments`);
593
+ const script = options.scripts?.find((s2) => s2.import.name === fnName);
594
+ const proxyConfigKey = script?.proxy !== false ? script?.proxy || registryKey : void 0;
595
+ const proxyRewrites = options.firstPartyEnabled && !firstPartyOptOut && proxyConfigKey && options.firstPartyCollectPrefix ? getProxyConfig(proxyConfigKey, options.firstPartyCollectPrefix)?.rewrite : void 0;
596
+ deferredOps.push(async () => {
597
+ let url = _url;
598
+ try {
599
+ await downloadScript({ src, url, filename, forceDownload, proxyRewrites, integrity: options.integrity }, renderedScript, options.fetchOptions, options.cacheMaxAge);
600
+ } catch (e) {
601
+ if (options.fallbackOnSrcOnBundleFail) {
602
+ logger.warn(`[Nuxt Scripts: Bundle Transformer] Failed to bundle ${src}. Fallback to remote loading.`);
603
+ url = src;
604
+ } else {
605
+ const errorMessage = e?.message || "Unknown error";
606
+ if (errorMessage.includes("timeout") || errorMessage.includes("network") || errorMessage.includes("ENOTFOUND") || errorMessage.includes("certificate")) {
607
+ logger.error(`[Nuxt Scripts: Bundle Transformer] Network issue while bundling ${src}: ${errorMessage}`);
608
+ logger.error(`[Nuxt Scripts: Bundle Transformer] Tip: Set 'fallbackOnSrcOnBundleFail: true' in module options or disable bundling in Docker environments`);
609
+ }
610
+ throw e;
387
611
  }
388
- throw e;
389
612
  }
390
- }
391
- if (src === url) {
392
- if (src && src.startsWith("/"))
393
- logger.warn(`[Nuxt Scripts: Bundle Transformer] Relative scripts are already bundled. Skipping bundling for \`${src}\`.`);
394
- else
395
- logger.warn(`[Nuxt Scripts: Bundle Transformer] Failed to bundle ${src}.`);
396
- }
397
- if (scriptSrcNode) {
398
- s.overwrite(scriptSrcNode.start, scriptSrcNode.end, `'${url}'`);
399
- } else {
400
- if (node.arguments[0]) {
401
- const optionsNode = node.arguments[0];
402
- const scriptInputProperty = optionsNode.properties.find(
403
- (p) => p.key?.name === "scriptInput" || p.key?.value === "scriptInput"
404
- );
405
- if (scriptInputProperty) {
406
- const scriptInput = scriptInputProperty.value;
407
- if (scriptInput.type === "ObjectExpression") {
408
- const srcProperty = scriptInput.properties.find(
409
- (p) => p.key?.name === "src" || p.key?.value === "src"
410
- );
411
- if (srcProperty)
412
- s.overwrite(srcProperty.value.start, srcProperty.value.end, `'${url}'`);
413
- else
414
- s.appendRight(scriptInput.end, `, src: '${url}'`);
415
- }
613
+ if (src === url) {
614
+ if (src && src.startsWith("/"))
615
+ logger.warn(`[Nuxt Scripts: Bundle Transformer] Relative scripts are already bundled. Skipping bundling for \`${src}\`.`);
616
+ else
617
+ logger.warn(`[Nuxt Scripts: Bundle Transformer] Failed to bundle ${src}.`);
618
+ }
619
+ const scriptMeta = renderedScript.get(url);
620
+ const integrityHash = scriptMeta instanceof Error ? void 0 : scriptMeta?.integrity;
621
+ if (scriptSrcNode) {
622
+ if (integrityHash && fnName === "useScript" && node.arguments[0]?.type === "Literal") {
623
+ s.overwrite(scriptSrcNode.start, scriptSrcNode.end, `{ src: '${url}', integrity: '${integrityHash}', crossorigin: 'anonymous' }`);
624
+ } else if (integrityHash && fnName === "useScript" && node.arguments[0]?.type === "ObjectExpression") {
625
+ s.overwrite(scriptSrcNode.start, scriptSrcNode.end, `'${url}'`);
626
+ s.appendLeft(node.arguments[0].end - 1, `, integrity: '${integrityHash}', crossorigin: 'anonymous'`);
416
627
  } else {
417
- s.appendRight(node.arguments[0].start + 1, ` scriptInput: { src: '${url}' }, `);
628
+ s.overwrite(scriptSrcNode.start, scriptSrcNode.end, `'${url}'`);
418
629
  }
419
630
  } else {
420
- s.appendRight(node.callee.end, `({ scriptInput: { src: '${url}' } })`);
631
+ const integrityProps = integrityHash ? `, integrity: '${integrityHash}', crossorigin: 'anonymous'` : "";
632
+ if (node.arguments[0]) {
633
+ const optionsNode = node.arguments[0];
634
+ const scriptInputProperty = optionsNode.properties.find(
635
+ (p) => p.key?.name === "scriptInput" || p.key?.value === "scriptInput"
636
+ );
637
+ if (scriptInputProperty) {
638
+ const scriptInput = scriptInputProperty.value;
639
+ if (scriptInput.type === "ObjectExpression") {
640
+ const srcProperty = scriptInput.properties.find(
641
+ (p) => p.key?.name === "src" || p.key?.value === "src"
642
+ );
643
+ if (srcProperty) {
644
+ s.overwrite(srcProperty.value.start, srcProperty.value.end, `'${url}'`);
645
+ if (integrityHash)
646
+ s.appendLeft(scriptInput.end - 1, integrityProps);
647
+ } else {
648
+ s.appendRight(scriptInput.end - 1, `, src: '${url}'${integrityProps}`);
649
+ }
650
+ }
651
+ } else {
652
+ s.appendRight(node.arguments[0].start + 1, ` scriptInput: { src: '${url}'${integrityProps} }, `);
653
+ }
654
+ } else {
655
+ s.appendRight(node.callee.end, `({ scriptInput: { src: '${url}'${integrityProps} } })`);
656
+ }
421
657
  }
422
- }
658
+ });
423
659
  }
424
660
  }
425
661
  }
426
662
  }
663
+ });
664
+ for (const op of deferredOps) {
665
+ await op();
666
+ }
667
+ if (s.hasChanged()) {
668
+ return {
669
+ code: s.toString(),
670
+ map: s.generateMap({ includeContent: true, source: id })
671
+ };
427
672
  }
428
- });
429
- if (s.hasChanged()) {
430
- return {
431
- code: s.toString(),
432
- map: s.generateMap({ includeContent: true, source: id })
433
- };
434
673
  }
435
674
  }
436
675
  };
@@ -481,17 +720,18 @@ function NuxtScriptsCheckScripts() {
481
720
  return createUnplugin(() => {
482
721
  return {
483
722
  name: "nuxt-scripts:check-scripts",
484
- transformInclude(id) {
485
- return isVue(id, { type: ["script"] });
486
- },
487
- async transform(code) {
488
- if (!code.includes("useScript"))
489
- return;
490
- const ast = this.parse(code);
491
- let nameNode;
492
- let errorNode;
493
- walk(ast, {
494
- enter(_node) {
723
+ transform: {
724
+ filter: {
725
+ id: /\.vue/
726
+ },
727
+ handler(code, id) {
728
+ if (!isVue(id, { type: ["script"] }))
729
+ return;
730
+ if (!code.includes("useScript"))
731
+ return;
732
+ let nameNode;
733
+ let errorNode;
734
+ parseAndWalk(code, id, function(_node) {
495
735
  if (_node.type === "VariableDeclaration" && _node.declarations?.[0]?.id?.type === "ObjectPattern") {
496
736
  const objPattern = _node.declarations[0]?.id;
497
737
  for (const property of objPattern.properties) {
@@ -521,19 +761,58 @@ function NuxtScriptsCheckScripts() {
521
761
  }
522
762
  }
523
763
  }
764
+ });
765
+ if (errorNode) {
766
+ return this.error(new Error("You can't use a top-level await on $script as it will never resolve."));
524
767
  }
525
- });
526
- if (errorNode) {
527
- return this.error(new Error("You can't use a top-level await on $script as it will never resolve."));
528
768
  }
529
769
  }
530
770
  };
531
771
  });
532
772
  }
533
773
 
774
+ function registerTypeTemplates({ nuxt, config, newScripts }) {
775
+ addTypeTemplate({
776
+ filename: "types/nuxt-scripts-augments.d.ts",
777
+ getContents: () => {
778
+ const typesPath = relative(
779
+ resolve(nuxt.options.rootDir, nuxt.options.buildDir, "types"),
780
+ resolve("runtime/types")
781
+ );
782
+ let augments = `// Generated by @nuxt/scripts
783
+ declare module '#app' {
784
+ interface NuxtApp {
785
+ $scripts: Record<${[...Object.keys(config.globals || {}), ...Object.keys(config.registry || {})].map((k) => `'${k}'`).concat(["string"]).join(" | ")}, import('#nuxt-scripts/types').UseScriptContext<any> | undefined>
786
+ _scripts: Record<string, import('#nuxt-scripts/types').NuxtDevToolsScriptInstance>
787
+ }
788
+ interface RuntimeNuxtHooks {
789
+ 'scripts:updated': (ctx: { scripts: Record<string, import('#nuxt-scripts/types').NuxtDevToolsScriptInstance> }) => void | Promise<void>
790
+ }
791
+ }
792
+ `;
793
+ if (newScripts.length) {
794
+ augments += `
795
+ declare module '#nuxt-scripts/types' {
796
+ type _NuxtScriptOptions = Omit<import('${typesPath}').NuxtUseScriptOptions, 'use' | 'beforeInit'>
797
+ interface ScriptRegistry {
798
+ ${newScripts.map((i) => {
799
+ const key = i.import.name.replace("useScript", "");
800
+ const keyLcFirst = key.substring(0, 1).toLowerCase() + key.substring(1);
801
+ return ` ${keyLcFirst}?: import('${i.import.from}').${key}Input | [import('${i.import.from}').${key}Input, _NuxtScriptOptions]`;
802
+ }).join("\n")}
803
+ }
804
+ }
805
+ `;
806
+ }
807
+ return `${augments}
808
+ export {}`;
809
+ }
810
+ }, { nuxt: true });
811
+ }
534
812
  function templateTriggerResolver(defaultScriptOptions) {
535
813
  const needsIdleTimeout = defaultScriptOptions?.trigger && typeof defaultScriptOptions.trigger === "object" && "idleTimeout" in defaultScriptOptions.trigger;
536
814
  const needsInteraction = defaultScriptOptions?.trigger && typeof defaultScriptOptions.trigger === "object" && "interaction" in defaultScriptOptions.trigger;
815
+ const needsServiceWorker = defaultScriptOptions?.trigger && typeof defaultScriptOptions.trigger === "object" && "serviceWorker" in defaultScriptOptions.trigger;
537
816
  const imports = [];
538
817
  if (needsIdleTimeout) {
539
818
  imports.push(`import { useScriptTriggerIdleTimeout } from '#nuxt-scripts/composables/useScriptTriggerIdleTimeout'`);
@@ -541,11 +820,15 @@ function templateTriggerResolver(defaultScriptOptions) {
541
820
  if (needsInteraction) {
542
821
  imports.push(`import { useScriptTriggerInteraction } from '#nuxt-scripts/composables/useScriptTriggerInteraction'`);
543
822
  }
823
+ if (needsServiceWorker) {
824
+ imports.push(`import { useScriptTriggerServiceWorker } from '#nuxt-scripts/composables/useScriptTriggerServiceWorker'`);
825
+ }
544
826
  return [
545
827
  ...imports,
546
828
  `export function resolveTrigger(trigger) {`,
547
829
  needsIdleTimeout ? ` if ('idleTimeout' in trigger) return useScriptTriggerIdleTimeout({ timeout: trigger.idleTimeout })` : "",
548
830
  needsInteraction ? ` if ('interaction' in trigger) return useScriptTriggerInteraction({ events: trigger.interaction })` : "",
831
+ needsServiceWorker ? ` if ('serviceWorker' in trigger) return useScriptTriggerServiceWorker()` : "",
549
832
  ` return null`,
550
833
  `}`
551
834
  ].filter(Boolean).join("\n");
@@ -562,6 +845,9 @@ function resolveTriggerForTemplate(trigger) {
562
845
  if ("interaction" in trigger) {
563
846
  return `useScriptTriggerInteraction({ events: ${JSON.stringify(trigger.interaction)} })`;
564
847
  }
848
+ if ("serviceWorker" in trigger) {
849
+ return `useScriptTriggerServiceWorker()`;
850
+ }
565
851
  }
566
852
  return null;
567
853
  }
@@ -572,27 +858,34 @@ function templatePlugin(config, registry) {
572
858
  }
573
859
  const imports = [];
574
860
  const inits = [];
861
+ const resolvedRegistryKeys = [];
575
862
  let needsIdleTimeoutImport = false;
576
863
  let needsInteractionImport = false;
864
+ let needsServiceWorkerImport = false;
577
865
  for (const [k, c] of Object.entries(config.registry || {})) {
578
- const importDefinition = registry.find((i) => i.import.name === `useScript${k.substring(0, 1).toUpperCase() + k.substring(1)}`);
866
+ const importDefinition = registry.find((i) => i.proxy === k || i.import.name === `useScript${k.substring(0, 1).toUpperCase() + k.substring(1)}`);
579
867
  if (importDefinition) {
868
+ resolvedRegistryKeys.push(k);
580
869
  imports.unshift(`import { ${importDefinition.import.name} } from '${importDefinition.import.from}'`);
581
- const args = (typeof c !== "object" ? {} : c) || {};
582
870
  if (c === "mock") {
583
- args.scriptOptions = { trigger: "manual", skipValidation: true };
584
- } else if (Array.isArray(c) && c.length === 2 && c[1]?.trigger) {
585
- const triggerResolved = resolveTriggerForTemplate(c[1].trigger);
871
+ inits.push(`const ${k} = ${importDefinition.import.name}({ scriptOptions: { trigger: 'manual', skipValidation: true } })`);
872
+ } else if (Array.isArray(c) && c.length === 2) {
873
+ const input = c[0] || {};
874
+ const scriptOptions = { ...c[1] };
875
+ const triggerResolved = resolveTriggerForTemplate(scriptOptions?.trigger);
586
876
  if (triggerResolved) {
587
- args.scriptOptions = { ...c[1] };
588
- if (args.scriptOptions) {
589
- args.scriptOptions.trigger = `__TRIGGER_${triggerResolved}__`;
590
- }
877
+ scriptOptions.trigger = "__TRIGGER_PLACEHOLDER__";
591
878
  if (triggerResolved.includes("useScriptTriggerIdleTimeout")) needsIdleTimeoutImport = true;
592
879
  if (triggerResolved.includes("useScriptTriggerInteraction")) needsInteractionImport = true;
880
+ if (triggerResolved.includes("useScriptTriggerServiceWorker")) needsServiceWorkerImport = true;
593
881
  }
882
+ const args = { ...input, scriptOptions };
883
+ const argsJson = triggerResolved ? JSON.stringify(args).replace(/"__TRIGGER_PLACEHOLDER__"/g, triggerResolved) : JSON.stringify(args);
884
+ inits.push(`const ${k} = ${importDefinition.import.name}(${argsJson})`);
885
+ } else {
886
+ const args = (typeof c !== "object" ? {} : c) || {};
887
+ inits.push(`const ${k} = ${importDefinition.import.name}(${JSON.stringify(args)})`);
594
888
  }
595
- inits.push(`const ${k} = ${importDefinition.import.name}(${JSON.stringify(args).replace(/"__TRIGGER_(.*?)__"/g, "$1")})`);
596
889
  }
597
890
  }
598
891
  for (const [k, c] of Object.entries(config.globals || {})) {
@@ -604,8 +897,10 @@ function templatePlugin(config, registry) {
604
897
  if (triggerResolved) {
605
898
  if (triggerResolved.includes("useScriptTriggerIdleTimeout")) needsIdleTimeoutImport = true;
606
899
  if (triggerResolved.includes("useScriptTriggerInteraction")) needsInteractionImport = true;
607
- const resolvedOptions = { ...options, trigger: `__TRIGGER_${triggerResolved}__` };
608
- inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...typeof c[0] === "string" ? { src: c[0] } : c[0] })}, { ...${JSON.stringify(resolvedOptions).replace(/"__TRIGGER_(.*?)__"/g, "$1")}, use: () => ({ ${k}: window.${k} }) })`);
900
+ if (triggerResolved.includes("useScriptTriggerServiceWorker")) needsServiceWorkerImport = true;
901
+ const resolvedOptions = { ...options, trigger: "__TRIGGER_PLACEHOLDER__" };
902
+ const optionsJson = JSON.stringify(resolvedOptions).replace(/"__TRIGGER_PLACEHOLDER__"/g, triggerResolved);
903
+ inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...typeof c[0] === "string" ? { src: c[0] } : c[0] })}, { ...${optionsJson}, use: () => ({ ${k}: window.${k} }) })`);
609
904
  } else {
610
905
  inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...typeof c[0] === "string" ? { src: c[0] } : c[0] })}, { ...${JSON.stringify(c[1])}, use: () => ({ ${k}: window.${k} }) })`);
611
906
  }
@@ -614,8 +909,10 @@ function templatePlugin(config, registry) {
614
909
  if (triggerResolved) {
615
910
  if (triggerResolved.includes("useScriptTriggerIdleTimeout")) needsIdleTimeoutImport = true;
616
911
  if (triggerResolved.includes("useScriptTriggerInteraction")) needsInteractionImport = true;
617
- const resolvedOptions = { ...c, trigger: `__TRIGGER_${triggerResolved}__` };
618
- inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...resolvedOptions }).replace(/"__TRIGGER_(.*?)__"/g, "$1")}, { use: () => ({ ${k}: window.${k} }) })`);
912
+ if (triggerResolved.includes("useScriptTriggerServiceWorker")) needsServiceWorkerImport = true;
913
+ const resolvedOptions = { ...c, trigger: "__TRIGGER_PLACEHOLDER__" };
914
+ const argsJson = JSON.stringify({ key: k, ...resolvedOptions }).replace(/"__TRIGGER_PLACEHOLDER__"/g, triggerResolved);
915
+ inits.push(`const ${k} = useScript(${argsJson}, { use: () => ({ ${k}: window.${k} }) })`);
619
916
  } else {
620
917
  inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...c })}, { use: () => ({ ${k}: window.${k} }) })`);
621
918
  }
@@ -628,6 +925,9 @@ function templatePlugin(config, registry) {
628
925
  if (needsInteractionImport) {
629
926
  triggerImports.push(`import { useScriptTriggerInteraction } from '#nuxt-scripts/composables/useScriptTriggerInteraction'`);
630
927
  }
928
+ if (needsServiceWorkerImport) {
929
+ triggerImports.push(`import { useScriptTriggerServiceWorker } from '#nuxt-scripts/composables/useScriptTriggerServiceWorker'`);
930
+ }
631
931
  return [
632
932
  `import { useScript } from '#nuxt-scripts/composables/useScript'`,
633
933
  `import { defineNuxtPlugin } from 'nuxt/app'`,
@@ -640,12 +940,26 @@ function templatePlugin(config, registry) {
640
940
  ` parallel: true,`,
641
941
  ` setup() {`,
642
942
  ...inits.map((i) => ` ${i}`),
643
- ` return { provide: { $scripts: { ${[...Object.keys(config.globals || {}), ...Object.keys(config.registry || {})].join(", ")} } } }`,
943
+ ` return { provide: { $scripts: { ${[...Object.keys(config.globals || {}), ...resolvedRegistryKeys].join(", ")} } } }`,
644
944
  ` }`,
645
945
  `})`
646
946
  ].join("\n");
647
947
  }
648
948
 
949
+ const PARTYTOWN_FORWARDS = {
950
+ googleAnalytics: ["dataLayer.push", "gtag"],
951
+ plausible: ["plausible"],
952
+ fathom: ["fathom", "fathom.trackEvent", "fathom.trackPageview"],
953
+ umami: ["umami", "umami.track"],
954
+ matomo: ["_paq.push"],
955
+ segment: ["analytics", "analytics.track", "analytics.page", "analytics.identify"],
956
+ metaPixel: ["fbq"],
957
+ xPixel: ["twq"],
958
+ tiktokPixel: ["ttq.track", "ttq.page", "ttq.identify"],
959
+ snapchatPixel: ["snaptr"],
960
+ redditPixel: ["rdt"],
961
+ cloudflareWebAnalytics: ["__cfBeacon"]
962
+ };
649
963
  const module$1 = defineNuxtModule({
650
964
  meta: {
651
965
  name: "@nuxt/scripts",
@@ -655,6 +969,7 @@ const module$1 = defineNuxtModule({
655
969
  }
656
970
  },
657
971
  defaults: {
972
+ firstParty: true,
658
973
  defaultScriptOptions: {
659
974
  trigger: "onNuxtReady"
660
975
  },
@@ -668,6 +983,10 @@ const module$1 = defineNuxtModule({
668
983
  // Configures the maximum time (in milliseconds) allowed for each fetch attempt.
669
984
  }
670
985
  },
986
+ googleStaticMapsProxy: {
987
+ enabled: false,
988
+ cacheMaxAge: 3600
989
+ },
671
990
  enabled: true,
672
991
  debug: false
673
992
  },
@@ -687,11 +1006,18 @@ const module$1 = defineNuxtModule({
687
1006
  if (unheadVersion?.startsWith("1")) {
688
1007
  logger.error(`Nuxt Scripts requires Unhead >= 2, you are using v${unheadVersion}. Please run \`nuxi upgrade --clean\` to upgrade...`);
689
1008
  }
690
- nuxt.options.runtimeConfig["nuxt-scripts"] = { version };
1009
+ nuxt.options.runtimeConfig["nuxt-scripts"] = {
1010
+ version,
1011
+ // Private proxy config with API key (server-side only)
1012
+ googleStaticMapsProxy: config.googleStaticMapsProxy?.enabled ? { apiKey: nuxt.options.runtimeConfig.public.scripts?.googleMaps?.apiKey } : void 0,
1013
+ swTemplate: readFileSync(await resolvePath("./runtime/sw/proxy-sw.template.js"), "utf-8")
1014
+ };
691
1015
  nuxt.options.runtimeConfig.public["nuxt-scripts"] = {
692
1016
  // expose for devtools
693
1017
  version: nuxt.options.dev ? version : void 0,
694
- defaultScriptOptions: config.defaultScriptOptions
1018
+ defaultScriptOptions: config.defaultScriptOptions,
1019
+ // Only expose enabled and cacheMaxAge to client, not apiKey
1020
+ googleStaticMapsProxy: config.googleStaticMapsProxy?.enabled ? { enabled: true, cacheMaxAge: config.googleStaticMapsProxy.cacheMaxAge } : void 0
695
1021
  };
696
1022
  if (config.registry) {
697
1023
  nuxt.options.runtimeConfig.public = nuxt.options.runtimeConfig.public || {};
@@ -700,13 +1026,57 @@ const module$1 = defineNuxtModule({
700
1026
  config.registry
701
1027
  );
702
1028
  }
1029
+ if (config.defaultScriptOptions?.bundle !== void 0) {
1030
+ logger.warn(
1031
+ "`scripts.defaultScriptOptions.bundle` is deprecated. Use `scripts.firstParty: true` instead. First-party mode is now enabled by default."
1032
+ );
1033
+ }
1034
+ const staticPresets = ["static", "github-pages", "cloudflare-pages-static"];
1035
+ const preset = process.env.NITRO_PRESET || "";
1036
+ const isStaticPreset = staticPresets.includes(preset);
1037
+ const firstPartyEnabled = !!config.firstParty;
1038
+ const firstPartyPrefix = typeof config.firstParty === "object" ? config.firstParty.prefix : void 0;
1039
+ const firstPartyCollectPrefix = typeof config.firstParty === "object" ? config.firstParty.collectPrefix || "/_proxy" : "/_proxy";
1040
+ const firstPartyPrivacy = typeof config.firstParty === "object" ? config.firstParty.privacy ?? "anonymize" : "anonymize";
1041
+ const assetsPrefix = firstPartyPrefix || config.assets?.prefix || "/_scripts";
1042
+ if (config.partytown?.length) {
1043
+ config.registry = config.registry || {};
1044
+ const requiredForwards = [];
1045
+ for (const scriptKey of config.partytown) {
1046
+ const forwards = PARTYTOWN_FORWARDS[scriptKey];
1047
+ if (forwards) {
1048
+ requiredForwards.push(...forwards);
1049
+ } else if (import.meta.dev) {
1050
+ logger.warn(`[partytown] "${scriptKey}" has no known Partytown forwards configured. It may not work correctly or may require manual forward configuration.`);
1051
+ }
1052
+ const reg = config.registry;
1053
+ const existing = reg[scriptKey];
1054
+ if (Array.isArray(existing)) {
1055
+ existing[1] = { ...existing[1], partytown: true };
1056
+ } else if (existing && typeof existing === "object" && existing !== true && existing !== "mock") {
1057
+ reg[scriptKey] = [existing, { partytown: true }];
1058
+ } else if (existing === true || existing === "mock") {
1059
+ reg[scriptKey] = [{}, { partytown: true }];
1060
+ } else {
1061
+ reg[scriptKey] = [{}, { partytown: true }];
1062
+ }
1063
+ }
1064
+ if (requiredForwards.length && hasNuxtModule("@nuxtjs/partytown")) {
1065
+ const partytownConfig = nuxt.options.partytown || {};
1066
+ const existingForwards = partytownConfig.forward || [];
1067
+ const newForwards = [.../* @__PURE__ */ new Set([...existingForwards, ...requiredForwards])];
1068
+ nuxt.options.partytown = { ...partytownConfig, forward: newForwards };
1069
+ logger.info(`[partytown] Auto-configured forwards: ${requiredForwards.join(", ")}`);
1070
+ }
1071
+ }
703
1072
  const composables = [
704
1073
  "useScript",
705
1074
  "useScriptEventPage",
706
1075
  "useScriptTriggerConsent",
707
1076
  "useScriptTriggerElement",
708
1077
  "useScriptTriggerIdleTimeout",
709
- "useScriptTriggerInteraction"
1078
+ "useScriptTriggerInteraction",
1079
+ "useScriptTriggerServiceWorker"
710
1080
  ];
711
1081
  for (const composable of composables) {
712
1082
  addImports({
@@ -726,6 +1096,104 @@ const module$1 = defineNuxtModule({
726
1096
  return templateTriggerResolver(config.defaultScriptOptions);
727
1097
  }
728
1098
  });
1099
+ const swHandlerPath = await resolvePath("./runtime/server/sw-handler");
1100
+ logger.debug("[nuxt-scripts] First-party config:", { firstPartyEnabled, firstPartyPrivacy, firstPartyCollectPrefix });
1101
+ if (firstPartyEnabled && !nuxt.options.dev) {
1102
+ const swPath = "/_nuxt-scripts-sw.js";
1103
+ const swRules = getSWInterceptRules(firstPartyCollectPrefix);
1104
+ addServerHandler({
1105
+ route: swPath,
1106
+ handler: swHandlerPath
1107
+ });
1108
+ addPluginTemplate({
1109
+ filename: "nuxt-scripts-sw-register.client.mjs",
1110
+ getContents() {
1111
+ return `import { defineNuxtPlugin } from 'nuxt/app'
1112
+
1113
+ export default defineNuxtPlugin({
1114
+ name: 'nuxt-scripts:sw-register',
1115
+ enforce: 'pre',
1116
+ async setup() {
1117
+ if (!('serviceWorker' in navigator)) return;
1118
+
1119
+ try {
1120
+ const reg = await navigator.serviceWorker.register('${swPath}', { scope: '/' });
1121
+
1122
+ // Wait for SW to be active and controlling this page
1123
+ if (!navigator.serviceWorker.controller) {
1124
+ await new Promise((resolve) => {
1125
+ const onControllerChange = () => {
1126
+ navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
1127
+ resolve();
1128
+ };
1129
+ navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
1130
+
1131
+ // Fallback timeout
1132
+ setTimeout(resolve, 2000);
1133
+ });
1134
+ }
1135
+ } catch (err) {
1136
+ console.warn('[nuxt-scripts] SW registration failed:', err);
1137
+ }
1138
+ },
1139
+ })
1140
+ `;
1141
+ }
1142
+ });
1143
+ addPluginTemplate({
1144
+ filename: "nuxt-scripts-beacon-intercept.client.mjs",
1145
+ getContents() {
1146
+ const rulesJson = JSON.stringify(swRules);
1147
+ return `export default defineNuxtPlugin({
1148
+ name: 'nuxt-scripts:beacon-intercept',
1149
+ enforce: 'pre',
1150
+ setup() {
1151
+ if (typeof navigator === 'undefined' || !navigator.sendBeacon) return;
1152
+
1153
+ const rules = ${rulesJson};
1154
+ const originalBeacon = navigator.sendBeacon.bind(navigator);
1155
+
1156
+ navigator.sendBeacon = (url, data) => {
1157
+ try {
1158
+ const parsed = new URL(url, window.location.origin);
1159
+
1160
+ // Check if this URL matches any of our proxy rules
1161
+ for (const rule of rules) {
1162
+ if (parsed.hostname === rule.pattern || parsed.hostname.endsWith('.' + rule.pattern)) {
1163
+ // Check path prefix if specified
1164
+ if (rule.pathPrefix && !parsed.pathname.startsWith(rule.pathPrefix)) {
1165
+ continue;
1166
+ }
1167
+
1168
+ // Rewrite to proxy: strip pathPrefix from original, prepend target
1169
+ const pathWithoutPrefix = rule.pathPrefix
1170
+ ? parsed.pathname.slice(rule.pathPrefix.length)
1171
+ : parsed.pathname;
1172
+ const separator = pathWithoutPrefix.startsWith('/') ? '' : '/';
1173
+ const proxyUrl = rule.target + separator + pathWithoutPrefix + parsed.search;
1174
+
1175
+ return originalBeacon(proxyUrl, data);
1176
+ }
1177
+ }
1178
+ } catch (e) {
1179
+ // URL parsing failed, pass through
1180
+ }
1181
+
1182
+ return originalBeacon(url, data);
1183
+ };
1184
+ },
1185
+ })
1186
+ `;
1187
+ }
1188
+ });
1189
+ nuxt.options.runtimeConfig.public["nuxt-scripts-sw"] = { path: swPath };
1190
+ const proxyHandlerPath = await resolvePath("./runtime/server/proxy-handler");
1191
+ logger.debug("[nuxt-scripts] Registering proxy handler:", `${firstPartyCollectPrefix}/**`, "->", proxyHandlerPath);
1192
+ addServerHandler({
1193
+ route: `${firstPartyCollectPrefix}/**`,
1194
+ handler: proxyHandlerPath
1195
+ });
1196
+ }
729
1197
  const scripts = await registry(resolvePath);
730
1198
  for (const script of scripts) {
731
1199
  if (script.import?.name) {
@@ -743,42 +1211,7 @@ const module$1 = defineNuxtModule({
743
1211
  }
744
1212
  const registryScriptsWithImport = registryScripts.filter((i) => !!i.import?.name);
745
1213
  const newScripts = registryScriptsWithImport.filter((i) => !scripts.some((r) => r.import?.name === i.import.name));
746
- addTypeTemplate({
747
- filename: "module/nuxt-scripts.d.ts",
748
- getContents: (data) => {
749
- const typesPath = relative(resolve(data.nuxt.options.rootDir, data.nuxt.options.buildDir, "module"), resolve("runtime/types"));
750
- let types = `
751
- declare module '#app' {
752
- interface NuxtApp {
753
- $scripts: Record<${[...Object.keys(config.globals || {}), ...Object.keys(config.registry || {})].map((k) => `'${k}'`).concat(["string"]).join(" | ")}, (import('#nuxt-scripts/types').UseScriptContext<any>)>
754
- _scripts: Record<string, (import('#nuxt-scripts/types').UseScriptContext<any>)>
755
- }
756
- interface RuntimeNuxtHooks {
757
- 'scripts:updated': (ctx: { scripts: Record<string, (import('#nuxt-scripts/types').UseScriptContext<any>)> }) => void | Promise<void>
758
- }
759
- }
760
- `;
761
- if (newScripts.length) {
762
- types = `${types}
763
- declare module '#nuxt-scripts/types' {
764
- type NuxtUseScriptOptions = Omit<import('${typesPath}').NuxtUseScriptOptions, 'use' | 'beforeInit'>
765
- interface ScriptRegistry {
766
- ${newScripts.map((i) => {
767
- const key = i.import?.name.replace("useScript", "");
768
- const keyLcFirst = key.substring(0, 1).toLowerCase() + key.substring(1);
769
- return ` ${keyLcFirst}?: import('${i.import?.from}').${key}Input | [import('${i.import?.from}').${key}Input, NuxtUseScriptOptions]`;
770
- }).join("\n")}
771
- }
772
- }`;
773
- return types;
774
- }
775
- return `${types}
776
- export {}`;
777
- }
778
- }, {
779
- nuxt: true,
780
- node: true
781
- });
1214
+ registerTypeTemplates({ nuxt, config, newScripts });
782
1215
  if (Object.keys(config.globals || {}).length || Object.keys(config.registry || {}).length) {
783
1216
  addPluginTemplate({
784
1217
  filename: `modules/${name.replace("/", "-")}/plugin.mjs`,
@@ -788,6 +1221,75 @@ export {}`;
788
1221
  });
789
1222
  }
790
1223
  const { renderedScript } = setupPublicAssetStrategy(config.assets);
1224
+ if (firstPartyEnabled) {
1225
+ const proxyConfigs = getAllProxyConfigs(firstPartyCollectPrefix);
1226
+ const registryKeys = Object.keys(config.registry || {});
1227
+ const neededRoutes = {};
1228
+ const unsupportedScripts = [];
1229
+ for (const key of registryKeys) {
1230
+ const script = registryScriptsWithImport.find((s) => s.import.name.toLowerCase() === `usescript${key.toLowerCase()}`);
1231
+ const proxyKey = script?.proxy || void 0;
1232
+ if (proxyKey) {
1233
+ const proxyConfig = proxyConfigs[proxyKey];
1234
+ if (proxyConfig?.routes) {
1235
+ Object.assign(neededRoutes, proxyConfig.routes);
1236
+ } else {
1237
+ unsupportedScripts.push(key);
1238
+ }
1239
+ }
1240
+ }
1241
+ if (unsupportedScripts.length && nuxt.options.dev) {
1242
+ logger.warn(
1243
+ `First-party mode is enabled but these scripts don't support it yet: ${unsupportedScripts.join(", ")}.
1244
+ They will load directly from third-party servers. Request support at https://github.com/nuxt/scripts/issues`
1245
+ );
1246
+ }
1247
+ const flatRoutes = {};
1248
+ for (const [path, config2] of Object.entries(neededRoutes)) {
1249
+ flatRoutes[path] = config2.proxy;
1250
+ }
1251
+ const allRewrites = [];
1252
+ for (const key of registryKeys) {
1253
+ const script = registryScriptsWithImport.find((s) => s.import.name.toLowerCase() === `usescript${key.toLowerCase()}`);
1254
+ const proxyKey = script?.proxy !== false ? script?.proxy || key : void 0;
1255
+ if (proxyKey) {
1256
+ const proxyConfig = proxyConfigs[proxyKey];
1257
+ if (proxyConfig?.rewrite) {
1258
+ allRewrites.push(...proxyConfig.rewrite);
1259
+ }
1260
+ }
1261
+ }
1262
+ nuxt.options.runtimeConfig["nuxt-scripts-proxy"] = {
1263
+ routes: flatRoutes,
1264
+ privacy: firstPartyPrivacy,
1265
+ rewrites: allRewrites
1266
+ };
1267
+ if (Object.keys(neededRoutes).length) {
1268
+ if (nuxt.options.dev) {
1269
+ const routeCount = Object.keys(neededRoutes).length;
1270
+ const scriptsCount = registryKeys.length;
1271
+ logger.success(`First-party mode enabled for ${scriptsCount} script(s), ${routeCount} proxy route(s) configured (privacy: ${firstPartyPrivacy})`);
1272
+ if (logger.level >= 4) {
1273
+ for (const [path, config2] of Object.entries(neededRoutes)) {
1274
+ logger.debug(` ${path} \u2192 ${config2.proxy}`);
1275
+ }
1276
+ }
1277
+ }
1278
+ }
1279
+ if (isStaticPreset) {
1280
+ logger.warn(
1281
+ `First-party collection endpoints require a server runtime (detected: ${preset || "static"}).
1282
+ Scripts will be bundled, but collection requests will not be proxied.
1283
+
1284
+ Options:
1285
+ 1. Configure platform rewrites (Vercel, Netlify, Cloudflare)
1286
+ 2. Switch to server-rendered mode (ssr: true)
1287
+ 3. Disable with firstParty: false
1288
+
1289
+ See: https://scripts.nuxt.com/docs/guides/first-party#static-hosting`
1290
+ );
1291
+ }
1292
+ }
791
1293
  const moduleInstallPromises = /* @__PURE__ */ new Map();
792
1294
  addBuildPlugin(NuxtScriptsCheckScripts(), {
793
1295
  dev: true
@@ -795,15 +1297,18 @@ export {}`;
795
1297
  addBuildPlugin(NuxtScriptBundleTransformer({
796
1298
  scripts: registryScriptsWithImport,
797
1299
  registryConfig: nuxt.options.runtimeConfig.public.scripts,
798
- defaultBundle: config.defaultScriptOptions?.bundle,
1300
+ defaultBundle: firstPartyEnabled || config.defaultScriptOptions?.bundle,
1301
+ firstPartyEnabled,
1302
+ firstPartyCollectPrefix,
799
1303
  moduleDetected(module) {
800
1304
  if (nuxt.options.dev && module !== "@nuxt/scripts" && !moduleInstallPromises.has(module) && !hasNuxtModule(module))
801
1305
  moduleInstallPromises.set(module, () => installNuxtModule(module));
802
1306
  },
803
- assetsBaseURL: config.assets?.prefix,
1307
+ assetsBaseURL: assetsPrefix,
804
1308
  fallbackOnSrcOnBundleFail: config.assets?.fallbackOnSrcOnBundleFail,
805
1309
  fetchOptions: config.assets?.fetchOptions,
806
1310
  cacheMaxAge: config.assets?.cacheMaxAge,
1311
+ integrity: config.assets?.integrity,
807
1312
  renderedScript
808
1313
  }));
809
1314
  nuxt.hooks.hook("build:done", async () => {
@@ -812,8 +1317,35 @@ export {}`;
812
1317
  await p?.();
813
1318
  });
814
1319
  });
815
- if (nuxt.options.dev)
1320
+ if (config.googleStaticMapsProxy?.enabled) {
1321
+ addServerHandler({
1322
+ route: "/_scripts/google-static-maps-proxy",
1323
+ handler: await resolvePath("./runtime/server/google-static-maps-proxy")
1324
+ });
1325
+ }
1326
+ addServerHandler({
1327
+ route: "/api/_scripts/x-embed",
1328
+ handler: await resolvePath("./runtime/server/x-embed")
1329
+ });
1330
+ addServerHandler({
1331
+ route: "/api/_scripts/x-embed-image",
1332
+ handler: await resolvePath("./runtime/server/x-embed-image")
1333
+ });
1334
+ addServerHandler({
1335
+ route: "/api/_scripts/instagram-embed",
1336
+ handler: await resolvePath("./runtime/server/instagram-embed")
1337
+ });
1338
+ addServerHandler({
1339
+ route: "/api/_scripts/instagram-embed-image",
1340
+ handler: await resolvePath("./runtime/server/instagram-embed-image")
1341
+ });
1342
+ addServerHandler({
1343
+ route: "/api/_scripts/instagram-embed-asset",
1344
+ handler: await resolvePath("./runtime/server/instagram-embed-asset")
1345
+ });
1346
+ if (nuxt.options.dev) {
816
1347
  setupDevToolsUI(config, resolvePath);
1348
+ }
817
1349
  }
818
1350
  });
819
1351