From de5266a6e267c07c90bda3cacc90b8bf00ae8513 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Thu, 18 Sep 2025 09:37:09 +1000 Subject: [PATCH 1/2] docs: significantly improve bundling documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive documentation covering build-time vs runtime behavior, static URL requirements, manual injection patterns, and limitations with workarounds. - Add Build-time vs Runtime Behavior section explaining bundling process - Add Static URL Requirements with clear examples of valid/invalid patterns - Add Manual Injection Patterns for dynamic scenarios - Add Working with Dynamic URLs with multiple strategies - Expand Limitations and Workarounds section with technical details - Include practical code examples for common use cases This addresses Phase 1 of Issue #466 - Bundle Documentation improvements. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/content/docs/1.guides/2.bundling.md | 174 ++++++++++++++++++++++- 1 file changed, 170 insertions(+), 4 deletions(-) diff --git a/docs/content/docs/1.guides/2.bundling.md b/docs/content/docs/1.guides/2.bundling.md index 154f032a..5ddfa0be 100644 --- a/docs/content/docs/1.guides/2.bundling.md +++ b/docs/content/docs/1.guides/2.bundling.md @@ -93,13 +93,179 @@ 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 + +At runtime, bundled scripts behave differently: + +```ts +// Original code +useScript('https://example.com/script.js', { bundle: true }) + +// 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'), +}) +``` + +### Limitations and Workarounds While many scripts can be bundled, there are exceptions you need to be aware of. -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). +#### Technical Limitations + +**Scripts that cannot be bundled:** +- Scripts with dynamic URLs determined at runtime +- Scripts requiring specific domain origins for CORS +- Scripts that self-modify or inject additional scripts dynamically +- Scripts requiring real-time updates without cache invalidation + +**Registry-specific limitations:** +- 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) +- Some analytics scripts that rely on referrer headers from their original domain + +#### Workarounds for Common Issues + +```ts [Common Bundling Workarounds] +// Issue: Need original URL for analytics +const originalUrl = 'https://cdn.example.com/analytics.js' +const script = useScript(originalUrl, { bundle: true }) + +// Store original URL separately for tracking +const trackingData = { + originalUrl, + bundledUrl: script.src // Available after bundling +} + +// Issue: Version-specific scripts +// Pre-bundle multiple versions +const versions = ['1.0.0', '1.1.0', '1.2.0'] +const bundledVersions = Object.fromEntries( + versions.map(v => [ + v, + useScript(`https://cdn.example.com/lib-${v}.js`, { + bundle: true, + trigger: 'manual' + }) + ]) +) + +// Load specific version at runtime +function loadVersion(version: string) { + bundledVersions[version]?.load() +} +``` 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. From 4f9720925d8bcc71fee72c87fae5e1a03cb47bfe Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Mon, 22 Sep 2025 13:22:02 +1000 Subject: [PATCH 2/2] chore: clean up --- docs/content/docs/1.guides/2.bundling.md | 60 +++------------------- test/unit/transform.test.ts | 64 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 54 deletions(-) diff --git a/docs/content/docs/1.guides/2.bundling.md b/docs/content/docs/1.guides/2.bundling.md index 5ddfa0be..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 + } }) ``` :: @@ -218,57 +221,6 @@ $script.add({ }) ``` -### Limitations and Workarounds - -While many scripts can be bundled, there are exceptions you need to be aware of. - -#### Technical Limitations - -**Scripts that cannot be bundled:** -- Scripts with dynamic URLs determined at runtime -- Scripts requiring specific domain origins for CORS -- Scripts that self-modify or inject additional scripts dynamically -- Scripts requiring real-time updates without cache invalidation - -**Registry-specific limitations:** -- 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) -- Some analytics scripts that rely on referrer headers from their original domain - -#### Workarounds for Common Issues - -```ts [Common Bundling Workarounds] -// Issue: Need original URL for analytics -const originalUrl = 'https://cdn.example.com/analytics.js' -const script = useScript(originalUrl, { bundle: true }) - -// Store original URL separately for tracking -const trackingData = { - originalUrl, - bundledUrl: script.src // Available after bundling -} - -// Issue: Version-specific scripts -// Pre-bundle multiple versions -const versions = ['1.0.0', '1.1.0', '1.2.0'] -const bundledVersions = Object.fromEntries( - versions.map(v => [ - v, - useScript(`https://cdn.example.com/lib-${v}.js`, { - bundle: true, - trigger: 'manual' - }) - ]) -) - -// Load specific version at runtime -function loadVersion(version: string) { - bundledVersions[version]?.load() -} -``` - -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. - ### Change Asset Behavior Use the `assets` option in your configuration to customize how scripts are bundled, such as changing the output directory for the bundled scripts. 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, })`,