Skip to content

Commit

Permalink
feat!: convert module config globals to object (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw committed Jul 5, 2024
1 parent dc79087 commit 7423527
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 82 deletions.
6 changes: 3 additions & 3 deletions docs/content/docs/1.guides/1.script-triggers.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ useScriptMetaPixel({
```ts [Global Script]
export default defineNuxtConfig({
scripts: {
globals: [
['https://example.com/script.js', {
globals: {
myScript: ['https://example.com/script.js', {
// load however you like!
trigger: 'onNuxtReady'
}]
]
}
}
})
```
Expand Down
120 changes: 75 additions & 45 deletions docs/content/docs/1.guides/4.global.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,43 +10,103 @@ While the `app.head` method in Nuxt Config allows for loading global scripts, it
```ts
export default defineNuxtConfig({
head: {
script: [
{
src: 'https://analytics.com/tracker.js',
async: true,
defer: true,
}
]
script: [ { src: 'https://analytics.com/tracker.js', async: true } ]
}
})
```

A simpler method is now available: directly input the script URL into `scripts.globals`. You can also include optional settings to tailor the script loading process with specific optimizations in mind.

## How it Works
You may consider using global scripts when:
- The script isn't a supported [Registry Script](/docs/api/use-script#registry-script).
- You don't care about interacting with the API provided by the third-party script (e.g. you don't need to use `gtag` from Google Analytics).
- You are interacting with the API provided by the third-party script, but you don't care about type safety.

Otherwise, it's recommended to use [useScript](/docs/api/use-script) to load scripts in a safer way.

## Usage

The `globals` key supports strings, objects and arrays.

**Example: Load a script using just the src**

```ts
export default defineNuxtConfig({
scripts: {
globals: {
myScript: 'https://analytics.com/tracker.js',
}
}
})
```

**Example: Load a script while providing extra script attributes**

```ts
export default defineNuxtConfig({
scripts: {
globals: {
myScript: {
src: 'https://example.com/script.js',
integrity: 'sha256-abc123',
}
}
}
})
```


You can optionally provide the script as an array which allows you to provide [Script Options](/docs/api/use-script#NuxtUseScriptOptions).

```ts
export default defineNuxtConfig({
scripts: {
globals: {
myScript: [
{ src: 'https://example.com/script.js' },
// load the script as part of the hydration process instead of on idle
{ trigger: 'client' }
]
}
}
})
```

The `globals` configuration will be used to create a virtual Nuxt plugin that loads in the script using the `useScript` composable. This allows for the script to be loaded in with best practices in mind.
### Accessing a global script

This also means you can access your script anywhere in your Nuxt app without triggering the script to be loaded again.
All Nuxt Scripts are registered on the `$scripts` Nuxt App property.

For example, if we're loading in a tracking script we can use the `useScript` composable to access the underlying API.
For scripts registered through nuxt.config, type autocompletion is available.

```vue
<script setup lang="ts">
const { $scripts } = useNuxtApp()
$scripts.myScript // { $script, instance }
</script>
```

## How it Works

The `globals` configuration will be used to create a virtual Nuxt plugin that loads in the script using the `useScript` composable.

As `useScript` is being used under the hood, it's important to understand the defaults and behavior of the [useScript](/api/use-script) composable..

::code-group

```ts [nuxt.config.ts]
export default defineNuxtConfig({
scripts: {
globals: [
'https://analytics.com/tracker.js',
]
globals: {
tracker: 'https://analytics.com/tracker.js',
}
}
})
```

```vue [components/Tracking.vue]
<script setup lang="ts">
// This will not trigger the script to be loaded again because of the nuxt.config global script
const { track, $script } = useScript<{ track: (e: string) => void }>('https://analytics.com/tracker.js')
const { track, $script } = useNuxtApp().$scripts.tracker
$script.then(() => {
console.log('Script loaded')
Expand All @@ -65,33 +125,3 @@ function trackCustom() {
```

::

## Usage

To load scripts globally, use a configuration-based approach by specifying them in your `nuxt.config.ts`.

```ts
export default defineNuxtConfig({
scripts: {
globals: [
'https://example.com/script.js',
]
}
})
```

You can optionally provide the script as an array which allows you to provide [Script Options](/docs/api/use-script#NuxtUseScriptOptions).

```ts
export default defineNuxtConfig({
scripts: {
globals: [
[
{ src: 'https://example.com/script.js' },
// load the script as part of the hydration process instead of on idle
{ trigger: 'client' }
]
]
}
})
```
11 changes: 8 additions & 3 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export interface ModuleOptions {
/**
* Register scripts that should be loaded globally on all pages.
*/
globals?: (NuxtUseScriptInput | [NuxtUseScriptInput, NuxtUseScriptOptionsSerializable])[]
globals?: Record<string, NuxtUseScriptInput | [NuxtUseScriptInput, NuxtUseScriptOptionsSerializable]>
/** Configure the way scripts assets are exposed */
assets?: {
/**
Expand Down Expand Up @@ -123,7 +123,11 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.alias['#nuxt-scripts'] = resolve('./runtime/types')
nuxt.options.alias['#nuxt-scripts-utils'] = resolve('./runtime/utils')
nuxt.options.runtimeConfig['nuxt-scripts'] = { version }
nuxt.options.runtimeConfig.public['nuxt-scripts'] = { defaultScriptOptions: config.defaultScriptOptions }
nuxt.options.runtimeConfig.public['nuxt-scripts'] = {
// expose for devtools
version: nuxt.options.dev ? version : undefined,
defaultScriptOptions: config.defaultScriptOptions,
}
addImportsDir([
resolve('./runtime/composables'),
// auto-imports aren't working without this for some reason
Expand Down Expand Up @@ -160,6 +164,7 @@ export default defineNuxtModule<ModuleOptions>({
let types = `
declare module '#app' {
interface NuxtApp {
$scripts: Record<${[...Object.keys(config.globals || {}), ...Object.keys(config.registry || {})].map(k => `'${k}'`).join(' | ')}, Pick<(import('#nuxt-scripts').NuxtAppScript), '$script'> & Record<string, any>>
_scripts: Record<string, (import('#nuxt-scripts').NuxtAppScript)>
}
interface RuntimeNuxtHooks {
Expand All @@ -184,7 +189,7 @@ ${newScripts.map((i) => {
return types
})

if (config.globals?.length || Object.keys(config.registry || {}).length) {
if (Object.keys(config.globals || {}).length || Object.keys(config.registry || {}).length) {
// create a virtual plugin
addPluginTemplate({
filename: `modules/${name!.replace('/', '-')}.mjs`,
Expand Down
29 changes: 21 additions & 8 deletions src/templates.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { hash } from 'ohash'
import type { ModuleOptions } from './module'
import { logger } from './logger'
import type { RegistryScript } from '#nuxt-scripts'

export function templatePlugin(config: Partial<ModuleOptions>, registry: Required<RegistryScript>[]) {
if (Array.isArray(config.globals)) {
// convert to object
config.globals = Object.fromEntries(config.globals.map(i => [hash(i), i]))
logger.warn('The `globals` array option is deprecated, please convert to an object.')
}
const imports = ['useScript', 'defineNuxtPlugin']
const inits = []
// for global scripts, we can initialise them script away
Expand All @@ -13,14 +20,20 @@ export function templatePlugin(config: Partial<ModuleOptions>, registry: Require
const args = (typeof c !== 'object' ? {} : c) || {}
if (c === 'mock')
args.scriptOptions = { trigger: 'manual', skipValidation: true }
inits.push(` ${importDefinition.import.name}(${JSON.stringify(args)})`)
inits.push(`const ${k} = ${importDefinition.import.name}(${JSON.stringify(args)})`)
}
}
for (const [k, c] of Object.entries(config.globals || {})) {
if (typeof c === 'string') {
inits.push(`const ${k} = useScript(${JSON.stringify({ src: c, key: k })}, { use: () => ({ ${k}: window.${k} }) })`)
}
else if (Array.isArray(c) && c.length === 2) {
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} } }) )`)
}
else {
inits.push(`const ${k} = useScript(${JSON.stringify({ key: k, ...c })}, { use: () => ({ ${k}: window.${k} }) })`)
}
}
const useScriptStatements = (config.globals || []).map(g => typeof g === 'string'
? ` useScript("${g.toString()}")`
: Array.isArray(g) && g.length === 2
? ` useScript(${JSON.stringify(g[0])}, ${JSON.stringify(g[1])} })`
: ` useScript(${JSON.stringify(g)})`)
return [
`import { ${imports.join(', ')} } from '#imports'`,
'',
Expand All @@ -29,8 +42,8 @@ export function templatePlugin(config: Partial<ModuleOptions>, registry: Require
` env: { islands: false },`,
` parallel: true,`,
` setup() {`,
...useScriptStatements,
...inits,
...inits.map(i => ` ${i}`),
` return { provide: { $scripts: { ${[...Object.keys(config.globals || {}), ...Object.keys(config.registry || {})].join(', ')} } } }`,
` }`,
`})`,
].join('\n')
Expand Down
48 changes: 25 additions & 23 deletions test/unit/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('template plugin file', () => {
// global
it('empty global', async () => {
const res = templatePlugin({
globals: [],
globals: {},
registry: {},
}, [])
expect(res).toMatchInlineSnapshot(`
Expand All @@ -16,36 +16,37 @@ describe('template plugin file', () => {
env: { islands: false },
parallel: true,
setup() {
return { provide: { $scripts: { } } }
}
})"
`)
})
it('string global', async () => {
const res = templatePlugin({
globals: [
'https://js.stripe.com/v3/',
],
globals: {
stripe: 'https://js.stripe.com/v3/',
},
}, [])
expect(res).toContain('useScript("https://js.stripe.com/v3/")')
expect(res).toContain('const stripe = useScript({"src":"https://js.stripe.com/v3/","key":"stripe"}, { use: () => ({ stripe: window.stripe }) })')
})
it('object global', async () => {
const res = templatePlugin({
globals: [
{
globals: {
stripe: {
async: true,
src: 'https://js.stripe.com/v3/',
key: 'stripe',
defer: true,
referrerpolicy: 'no-referrer',
},
],
},
}, [])
expect(res).toContain('useScript({"async":true,"src":"https://js.stripe.com/v3/","key":"stripe","defer":true,"referrerpolicy":"no-referrer"})')
expect(res).toContain('const stripe = useScript({"key":"stripe","async":true,"src":"https://js.stripe.com/v3/","defer":true,"referrerpolicy":"no-referrer"}, { use: () => ({ stripe: window.stripe }) })')
})
it('array global', async () => {
const res = templatePlugin({
globals: [
[
globals: {
stripe: [
{
async: true,
src: 'https://js.stripe.com/v3/',
Expand All @@ -58,29 +59,29 @@ describe('template plugin file', () => {
mode: 'client',
},
],
],
},
}, [])
expect(res).toContain('useScript({"async":true,"src":"https://js.stripe.com/v3/","key":"stripe","defer":true,"referrerpolicy":"no-referrer"}, {"trigger":"onNuxtReady","mode":"client"} })')
expect(res).toContain('const stripe = useScript({"key":"stripe","async":true,"src":"https://js.stripe.com/v3/","defer":true,"referrerpolicy":"no-referrer"}, { ...{"trigger":"onNuxtReady","mode":"client"}, use: () => ({ stripe: window.stripe } }) )')
})
it('mixing global', async () => {
const res = templatePlugin({
globals: [
'https://js.stripe.com/v3/',
{
globals: {
stripe1: 'https://js.stripe.com/v3/',
stripe2: {
async: true,
src: 'https://js.stripe.com/v3/',
key: 'stripe',
defer: true,
referrerpolicy: 'no-referrer',
},
[
stripe3: [
'https://js.stripe.com/v3/',
{
trigger: 'onNuxtReady',
mode: 'client',
},
],
],
},
}, [])
expect(res).toMatchInlineSnapshot(`
"import { useScript, defineNuxtPlugin } from '#imports'
Expand All @@ -90,17 +91,18 @@ describe('template plugin file', () => {
env: { islands: false },
parallel: true,
setup() {
useScript("https://js.stripe.com/v3/")
useScript({"async":true,"src":"https://js.stripe.com/v3/","key":"stripe","defer":true,"referrerpolicy":"no-referrer"})
useScript("https://js.stripe.com/v3/", {"trigger":"onNuxtReady","mode":"client"} })
const stripe1 = useScript({"src":"https://js.stripe.com/v3/","key":"stripe1"}, { use: () => ({ stripe1: window.stripe1 }) })
const stripe2 = useScript({"key":"stripe","async":true,"src":"https://js.stripe.com/v3/","defer":true,"referrerpolicy":"no-referrer"}, { use: () => ({ stripe2: window.stripe2 }) })
const stripe3 = useScript({"key":"stripe3","src":"https://js.stripe.com/v3/"}, { ...{"trigger":"onNuxtReady","mode":"client"}, use: () => ({ stripe3: window.stripe3 } }) )
return { provide: { $scripts: { stripe1, stripe2, stripe3 } } }
}
})"
`)
})
// registry
it('registry object', async () => {
const res = templatePlugin({
globals: [],
globals: {},
registry: {
stripe: {
id: 'test',
Expand All @@ -117,7 +119,7 @@ describe('template plugin file', () => {
})
it('registry array', async () => {
const res = templatePlugin({
globals: [],
globals: {},
registry: {
stripe: [
{
Expand Down

0 comments on commit 7423527

Please sign in to comment.