diff --git a/packages/nuxt/src/app/composables/script-stubs.ts b/packages/nuxt/src/app/composables/script-stubs.ts new file mode 100644 index 0000000000..6fcaf114b2 --- /dev/null +++ b/packages/nuxt/src/app/composables/script-stubs.ts @@ -0,0 +1,102 @@ +import type { UseScriptInput } from '@unhead/vue' +import { createError } from './error' + +function renderStubMessage (name: string) { + const message = `\`${name}\` is provided by @nuxt/scripts. Check your console to install it or run 'npx nuxi@latest module add @nuxt/scripts' to install it.` + if (import.meta.client) { + throw createError({ + fatal: true, + statusCode: 500, + statusMessage: message, + }) + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScript> (input: UseScriptInput, options?: Record) { + renderStubMessage('useScript') +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useElementScriptTrigger (...args: unknown[]) { + renderStubMessage('useElementScriptTrigger') +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useConsentScriptTrigger (...args: unknown[]) { + renderStubMessage('useConsentScriptTrigger') +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useAnalyticsPageEvent (...args: unknown[]) { + renderStubMessage('useAnalyticsPageEvent') +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptGoogleAnalytics (...args: unknown[]) { + renderStubMessage('useScriptGoogleAnalytics') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptPlausibleAnalytics (...args: unknown[]) { + renderStubMessage('useScriptPlausibleAnalytics') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptCloudflareWebAnalytics (...args: unknown[]) { + renderStubMessage('useScriptCloudflareWebAnalytics') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptFathomAnalytics (...args: unknown[]) { + renderStubMessage('useScriptFathomAnalytics') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptMatomoAnalytics (...args: unknown[]) { + renderStubMessage('useScriptMatomoAnalytics') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptGoogleTagManager (...args: unknown[]) { + renderStubMessage('useScriptGoogleTagManager') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptSegment (...args: unknown[]) { + renderStubMessage('useScriptSegment') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptFacebookPixel (...args: unknown[]) { + renderStubMessage('useScriptFacebookPixel') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptXPixel (...args: unknown[]) { + renderStubMessage('useScriptXPixel') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptIntercom (...args: unknown[]) { + renderStubMessage('useScriptIntercom') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptHotjar (...args: unknown[]) { + renderStubMessage('useScriptHotjar') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptStripe (...args: unknown[]) { + renderStubMessage('useScriptStripe') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptLemonSqueezy (...args: unknown[]) { + renderStubMessage('useScriptLemonSqueezy') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptVimeoPlayer (...args: unknown[]) { + renderStubMessage('useScriptVimeoPlayer') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptYouTubeIframe (...args: unknown[]) { + renderStubMessage('useScriptYouTubeIframe') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptGoogleMaps (...args: unknown[]) { + renderStubMessage('useScriptGoogleMaps') +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function useScriptNpm (...args: unknown[]) { + renderStubMessage('useScriptNpm') +} diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 39e47a4e06..31ead96d88 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -20,6 +20,7 @@ import importsModule from '../imports/module' import { distDir, pkgDir } from '../dirs' import { version } from '../../package.json' +import { scriptsStubsPreset } from '../imports/presets' import { ImportProtectionPlugin, nuxtImportProtections } from './plugins/import-protection' import type { UnctxTransformPluginOptions } from './plugins/unctx' import { UnctxTransformPlugin } from './plugins/unctx' @@ -125,6 +126,14 @@ async function initNuxt (nuxt: Nuxt) { } }) + // Prompt to install `@nuxt/scripts` if user has configured it + // @ts-expect-error scripts types are not present as the module is not installed + if (nuxt.options.scripts) { + if (!nuxt.options._modules.some(m => m === '@nuxt/scripts' || m === '@nuxt/scripts-nightly')) { + await import('../core/features').then(({ installNuxtModule }) => installNuxtModule('@nuxt/scripts')) + } + } + // Add plugin normalization plugin addBuildPlugin(RemovePluginMetadataPlugin(nuxt)) @@ -550,6 +559,12 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise { } } + if (!options._modules.some(m => m === '@nuxt/scripts' || m === '@nuxt/scripts-nightly')) { + options.imports = defu(options.imports, { + presets: [scriptsStubsPreset], + }) + } + // Nuxt Webpack Builder is currently opt-in if (options.builder === '@nuxt/webpack-builder') { if (!await import('./features').then(r => r.ensurePackageInstalled('@nuxt/webpack-builder', { diff --git a/packages/nuxt/src/imports/presets.ts b/packages/nuxt/src/imports/presets.ts index 7949ed6149..8387737409 100644 --- a/packages/nuxt/src/imports/presets.ts +++ b/packages/nuxt/src/imports/presets.ts @@ -111,6 +111,33 @@ const granularAppPresets: InlinePreset[] = [ }, ] +export const scriptsStubsPreset = { + imports: [ + 'useConsentScriptTrigger', + 'useAnalyticsPageEvent', + 'useElementScriptTrigger', + 'useScript', + 'useScriptGoogleAnalytics', + 'useScriptPlausibleAnalytics', + 'useScriptCloudflareWebAnalytics', + 'useScriptFathomAnalytics', + 'useScriptMatomoAnalytics', + 'useScriptGoogleTagManager', + 'useScriptSegment', + 'useScriptFacebookPixel', + 'useScriptXPixel', + 'useScriptIntercom', + 'useScriptHotjar', + 'useScriptStripe', + 'useScriptLemonSqueezy', + 'useScriptVimeoPlayer', + 'useScriptYouTubeIframe', + 'useScriptGoogleMaps', + 'useScriptNpm', + ], + from: '#app/composables/script-stubs', +} satisfies InlinePreset + // This is a separate preset as we'll swap these out for import from `vue-router` itself in `pages` module const routerPreset = defineUnimportPreset({ imports: ['onBeforeRouteLeave', 'onBeforeRouteUpdate'], diff --git a/packages/nuxt/src/imports/transform.ts b/packages/nuxt/src/imports/transform.ts index 19f7d8865f..0edeb96e11 100644 --- a/packages/nuxt/src/imports/transform.ts +++ b/packages/nuxt/src/imports/transform.ts @@ -1,6 +1,7 @@ import { createUnplugin } from 'unplugin' import type { Unimport } from 'unimport' import { normalize } from 'pathe' +import { tryUseNuxt } from '@nuxt/kit' import type { ImportsOptions } from 'nuxt/schema' import { isJS, isVue } from '../core/utils' @@ -37,7 +38,11 @@ export const TransformPlugin = createUnplugin(({ ctx, options, sourcemap }: { ct return } - const { s } = await ctx.injectImports(code, id, { autoImport: options.autoImport && !isNodeModule }) + const { s, imports } = await ctx.injectImports(code, id, { autoImport: options.autoImport && !isNodeModule }) + if (imports.some(i => i.from === '#app/composables/script-stubs') && tryUseNuxt()?.options.test === false) { + import('../core/features').then(({ installNuxtModule }) => installNuxtModule('@nuxt/scripts')) + } + if (s.hasChanged()) { return { code: s.toString(),