diff --git a/docs/content/docs/1.guides/2.bundling.md b/docs/content/docs/1.guides/2.bundling.md index 154f032a..a36795b3 100644 --- a/docs/content/docs/1.guides/2.bundling.md +++ b/docs/content/docs/1.guides/2.bundling.md @@ -72,9 +72,12 @@ useScript('https://example.com/script.js', { ``` ```ts [Registry Script] -// Registry script must support bundling -useScriptGoogleAnalytics('https://example.com/script.js', { - bundle: true, +// Registry script bundling using scriptOptions +useScriptGoogleAnalytics({ + id: 'GA_MEASUREMENT_ID', + scriptOptions: { + bundle: true + } }) ``` :: @@ -93,15 +96,130 @@ export default defineNuxtConfig({ }) ``` -### Limitations of Bundling +### Build-time vs Runtime Behavior + +Understanding when bundling happens and how it affects runtime behavior is crucial for effective usage. + +#### Build-time Processing + +Bundling occurs during the build phase through static code analysis: + +```ts +// ✅ Bundled at build-time (static values) +useScript('https://example.com/script.js', { bundle: true }) + +// ❌ Cannot be bundled (dynamic values) +const scriptUrl = computed(() => getScriptUrl()) +useScript(scriptUrl, { bundle: dynamic.value }) +``` + +#### Runtime Behavior -While many scripts can be bundled, there are exceptions you need to be aware of. +At runtime, bundled scripts behave differently: -For instance, certain scripts: -- Require tracking all user interactions for security reasons, like fraud detection (e.g., Stripe). -- Must be served directly from their original source to function properly (e.g., Fathom Analytics). +```ts +// Original code +useScript('https://example.com/script.js', { bundle: true }) -Scripts from known registries are pre-configured to either allow or disallow bundling. For your own scripts, you'll need to decide whether bundling is appropriate on a case-by-case basis. +// After build transformation +useScript('/_scripts/abc123.js', {}) +``` + +**Important**: Once bundled, you lose access to the original URL at runtime. If you need the original URL for tracking or analytics, store it separately. + +#### Static URL Requirements + +For bundling to work, the transformer requires **completely static values**: + +::code-group + +```ts [✅ Valid for Bundling] +// Static string literals +useScript('https://cdn.example.com/lib.js', { bundle: true }) + +// Static template literals (no variables) +useScript(`https://cdn.example.com/lib.js`, { bundle: true }) + +// Constants defined at module level +const SCRIPT_URL = 'https://cdn.example.com/lib.js' +useScript(SCRIPT_URL, { bundle: true }) +``` + +```ts [❌ Cannot be Bundled] +// Runtime variables +const url = getScriptUrl() +useScript(url, { bundle: true }) + +// Computed values +const scriptUrl = computed(() => `https://cdn.example.com/${version.value}.js`) +useScript(scriptUrl, { bundle: true }) + +// Environment variables at runtime +useScript(process.env.SCRIPT_URL, { bundle: true }) + +// Props or reactive values +useScript(props.scriptUrl, { bundle: true }) +``` + +:: + +#### Manual Injection Patterns + +When automatic bundling isn't possible, you can manually inject bundled scripts: + +```ts [Manual Bundling Workaround] +// 1. Bundle during build with static URL +const staticScript = useScript('https://cdn.example.com/static.js', { + bundle: true, + trigger: 'manual' // Don't auto-load +}) + +// 2. Conditionally load based on runtime logic +function loadScript() { + if (shouldLoadScript.value) { + staticScript.load() + } +} + +// 3. Alternative: Use multiple static configurations +const scriptVariants = { + dev: useScript('https://cdn.example.com/dev.js', { bundle: true, trigger: 'manual' }), + prod: useScript('https://cdn.example.com/prod.js', { bundle: true, trigger: 'manual' }) +} + +// Load appropriate variant +const currentScript = computed(() => + isDev ? scriptVariants.dev : scriptVariants.prod +) +``` + +#### Working with Dynamic URLs + +For truly dynamic scenarios, consider these patterns: + +```ts [Dynamic URL Strategies] +// Option 1: Pre-bundle known variants +const analytics = { + google: useScript('https://www.googletagmanager.com/gtag/js', { bundle: true }), + plausible: useScript('https://plausible.io/js/script.js', { bundle: true }) +} + +// Option 2: Fallback to runtime loading +function loadDynamicScript(url: string) { + // This won't be bundled, but will work at runtime + return useScript(url, { + bundle: false, // Explicitly disable + trigger: 'manual' + }) +} + +// Option 3: Use server-side bundling +// Store script content in your bundle and inject manually +const { $script } = useNuxtApp() +$script.add({ + innerHTML: await $fetch('/api/dynamic-script-content'), +}) +``` ### Change Asset Behavior diff --git a/test/unit/transform.test.ts b/test/unit/transform.test.ts index 51c29a58..c42b1bad 100644 --- a/test/unit/transform.test.ts +++ b/test/unit/transform.test.ts @@ -147,6 +147,70 @@ describe('nuxtScriptTransformer', () => { expect(code).toMatchInlineSnapshot(`"const instance = useScriptFathomAnalytics({ src: '/_scripts/custom.js.js' }, )"`) }) + it('registry script with scriptOptions.bundle - correct usage', async () => { + vi.mocked(hash).mockImplementationOnce(() => 'analytics') + const code = await transform( + `const instance = useScriptGoogleAnalytics({ + id: 'GA_MEASUREMENT_ID', + scriptOptions: { + bundle: true + } + })`, + { + defaultBundle: false, + scripts: [ + { + scriptBundling() { + return 'https://www.googletagmanager.com/gtag/js' + }, + import: { + name: 'useScriptGoogleAnalytics', + from: '', + }, + }, + ], + }, + ) + expect(code).toMatchInlineSnapshot(` + "const instance = useScriptGoogleAnalytics({ scriptInput: { src: '/_scripts/analytics.js' }, + id: 'GA_MEASUREMENT_ID', + scriptOptions: { + bundle: true + } + })" + `) + }) + + it('registry script with top-level bundle also transforms', async () => { + vi.mocked(hash).mockImplementationOnce(() => 'gtag/js') + const code = await transform( + `const instance = useScriptGoogleAnalytics({ + id: 'GA_MEASUREMENT_ID' + }, { + bundle: true + })`, + { + defaultBundle: false, + scripts: [ + { + scriptBundling() { + return 'https://www.googletagmanager.com/gtag/js' + }, + import: { + name: 'useScriptGoogleAnalytics', + from: '', + }, + }, + ], + }, + ) + expect(code).toMatchInlineSnapshot(` + "const instance = useScriptGoogleAnalytics({ scriptInput: { src: '/_scripts/gtag/js.js' }, + id: 'GA_MEASUREMENT_ID' + }, )" + `) + }) + it('static src integration is transformed - opt-in', async () => { const code = await transform( `const instance = useScriptFathomAnalytics({ site: '123' }, { bundle: true, })`,