Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 127 additions & 9 deletions docs/content/docs/1.guides/2.bundling.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})
```
::
Expand All @@ -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

Expand Down
64 changes: 64 additions & 0 deletions test/unit/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, })`,
Expand Down
Loading