Skip to content

Commit

Permalink
chore: migration from scripts-and-assets
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw committed Mar 14, 2024
1 parent a9a3a08 commit 8e592f9
Show file tree
Hide file tree
Showing 39 changed files with 2,864 additions and 2,486 deletions.
3 changes: 0 additions & 3 deletions .eslintignore

This file was deleted.

5 changes: 0 additions & 5 deletions .eslintrc

This file was deleted.

9 changes: 9 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
MIT License

Copyright (c) 2024 Harlan Wilton

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
121 changes: 83 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,103 @@
# Nuxt Scripts and Assets
<h1 align='center'>@nuxt/scripts</h1>

Work in progress for the development of the following modules
- Nuxt Assets - Improved loading options for assets (proxy, inline, etc)
- Nuxt Scripts - useScripts, useStyles composables
- Nuxt Third Parties - Simple optimized wrappers for third parties
- Nuxt Third Party Capital - Wrappers supported by [Third Party Capital](https://github.com/GoogleChromeLabs/third-party-capital)
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![License][license-src]][license-href]
[![Nuxt][nuxt-src]][nuxt-href]

## Nuxt Assets
<p align="center">
Powerful DX improvements for loading third-party scripts in Nuxt.
</p>

Provides `useInlineAsset` and `useProxyAsset` composables to load various resources.
## Features

`useInlineAsset` loads the resource serverside and inlines the response as HTML rather than linking it. This saves any network overhead in fetching the script for the first time but it means it can't be cached by the browser for reloads (good for tiny scripts). It will add a tiny bit of latency to the initial SSR until the script is cached.
All the features from Unhead [useScript](https://unhead.unjs.io/usage/composables/use-script):

`useProxyAsset` loads the resource serverside as well but just acts as a proxy, this can be useful to remove the DNS lookup time of using an alternative domain. This has a caching layer so can potentially provide a faster, closer to the edge download for the end user depending on the sites infrastructure.
- 🦥 Lazy, but fast: `defer`, `fetchpriority: 'low'`, early connections (`preconnect`, `dns-prefetch`)
- ☕ Loading strategies: `idle`, `manual`, `Promise`
- 🪨 Single script instance for your app
- 🎃 Events for SSR scripts: `onload`, `onerror`, etc
- 🪝 Proxy API: call the script functions before it's loaded, noop for SSR, stubbable, etc
- 🇹 Fully typed APIs

Default behavior: if there's an asset strategy then the request will be routed by the Nuxt server (or prerendered for SSG).
Plus Nuxt goodies:

## Nuxt Scripts
- 🕵️ `useTrackingScript` - Load a tracking script while respecting privacy and consent
- 🪵 DevTools integration - see all your loaded scripts with function logs

Provides `useScript` and `useStyles` composables to load scripts and stylesheets.

Check failure on line 28 in README.md

View workflow job for this annotation

GitHub Actions / ci

More than 1 blank line not allowed
`useScript` loads scripts with various options. It uses a trigger and asset strategy options to control how and when the script gets requested.
`useStyles` allows for optimized stylesheet loading out of the box.
## Installation

See [useScript](https://unhead.unjs.io/usage/composables/use-script)
1. Install `@nuxt/scripts` dependency to your project:

## Nuxt Third Parties
```bash
pnpm add -D @nuxt/scripts
#
yarn add -D @nuxt/scripts
#
npm install -D @nuxt/scripts
```

Third Party wrappers with Nuxt support.
In development:
- Cloudflare Analytics
- Cloudflare Turnstile
- Fathom Analytics
- Google Adsense
- Google Recaptcha
2. Add it to your `modules` section in your `nuxt.config`:

## Nuxt Third Party Capital
```ts
export default defineNuxtConfig({
modules: ['@nuxt/scripts']
})
```

Third Party wrappers supported by Nuxt & Third Party Capital. Third Party Capital is a resource that consolidates best practices for loading popular third-parties in a single place.
## Background

Supported wrappers:
- Google Analytics
- Google Tag Manager
- Youtube Embed
- Google Maps JavaScript Api
Loading third-party IIFE scripts using `useHead` composable is easy. However,
things start getting more complicated quickly around SSR, lazy loading, and type safety.

See [Third Party Capital](https://github.com/GoogleChromeLabs/third-party-capital)
Nuxt Scripts was created to solve these issues and more with the goal of making third-party scripts a breeze to use.

## Features
## Usage

### `useScript`

Please see the [useScript](https://unhead.unjs.io/usage/composables/use-script) documentation.

### `useTrackingScript`

This composables is a wrapper around `useScript` that respects privacy and cookie consent.

For the script to load you must provide a `consent` option. This can be promise, ref, or boolean.

```ts
const agreedToCookies = ref(false)
useTrackingScript('https://www.google-analytics.com/analytics.js', {
// will be loaded in when the ref is true
consent: agreedToCookies
})
```

If the user has enabled `DoNotTrack` within their browser, the script will not be loaded, unless
explicitly ignoring.

```ts
const agreedToCookies = ref(false)
useTrackingScript('https://www.google-analytics.com/analytics.js', {
ignoreDoNotTrack: true
})
```


Check failure on line 86 in README.md

View workflow job for this annotation

GitHub Actions / ci

More than 1 blank line not allowed
## License

Licensed under the [MIT license](https://github.com/nuxt/scripts/blob/main/LICENSE.md).


Check failure on line 91 in README.md

View workflow job for this annotation

GitHub Actions / ci

More than 1 blank line not allowed
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/@nuxt/scripts/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-version-href]: https://npmjs.com/package/@nuxt/scripts

- 🌐 Serve scripts from your domain using triggers (`idle`, `manual`, `Promise`) and asset strategies (`inline`, `proxy`)
[npm-downloads-src]: https://img.shields.io/npm/dm/@nuxt/scripts.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-downloads-href]: https://npmjs.com/package/@nuxt/scripts

## Future Features (ideas welcome)
[license-src]: https://img.shields.io/github/license/nuxt/scripts.svg?style=flat&colorA=18181B&colorB=28CF8D
[license-href]: https://github.com/nuxt/scripts/blob/main/LICENSE

- 🔒 Lock down your site with Content Security Policy integration
- Load scripts from nuxt.config with `scripts.globals`
- ?? (ideas welcome)
[nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
[nuxt-href]: https://nuxt.com
134 changes: 134 additions & 0 deletions client/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<script lang="ts" setup>
import { onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client'
import { appFetch, devtools } from '~/composables/rpc'
import { reactive, ref } from '#imports'
import { loadShiki } from '~/composables/shiki'
const scripts = ref({})
await loadShiki()
const scriptSizes = reactive({})
async function getScriptSize(url: string) {
const compressedResponse = await fetch(url, { headers: { 'Accept-Encoding': 'gzip' } })
return getResponseSize(compressedResponse)
}
async function getResponseSize(response) {
const reader = response.body.getReader()
const contentLength = +response.headers.get('Content-Length')
if (contentLength) {
return contentLength
}
else {
let total = 0
while (true) {
const { done, value } = await reader.read()
if (done)
return total
total += value.length
}
}
}
function bytesToSize(bytes: number) {
// be precise to 2 decimal places
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
if (bytes === 0)
return '0 Byte'
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return `${Number.parseFloat((bytes / 1024 ** i).toFixed(2))} ${sizes[i]}`
}
onDevtoolsClientConnected(async (client) => {
appFetch.value = client.host.app.$fetch
devtools.value = client.devtools
client.host.nuxt.hooks.hook('scripts:updated', (ctx) => {
scripts.value = { ...ctx.scripts }
// check if the script size has been set, if not set it
for (const key in ctx.scripts) {
if (!scriptSizes[key]) {
getScriptSize(ctx.scripts[key].src).then((size) => {
scriptSizes[key] = bytesToSize(size)
})
}
}
})
})
function humanFriendlyTimestamp(timestamp: number) {
// use Intl.DateTimeFormat to format the timestamp, we only need the time aspect
return new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: true,
}).format(timestamp)
}
function urlToOrigin(url: string) {
return new URL(url).origin
}
</script>

<template>
<div class="relative n-bg-base flex flex-col">
<div class="flex-row flex p4 h-full" style="min-height: calc(100vh - 64px);">
<main class="mx-auto flex flex-col w-full">
<div v-if="!Object.keys(scripts).length">
<div>No scripts loaded.</div>
</div>
<div class="space-y-3">
<div v-for="(script, id) in scripts" :key="id" class="w-full">
<div class="flex items-center justify-between w-full mb-3">
<div class="flex items-center gap-4">
<a class="text-xl font-bold flex gap-2 items-center font-mono" :title="script.src" target="_blank" :href="script.src">
<img :src="`https://www.google.com/s2/favicons?domain=${urlToOrigin(script.src)}`" class="w-4 h-4 rounded-lg">
<div>{{ script.key }}</div>
</a>
<div class="opacity-70">
{{ script.$script.status }}
</div>
<div v-if="scriptSizes[script.key]">
{{ scriptSizes[script.key] }}
</div>
</div>
<div>
<NButton v-if="script.$script.status === 'awaitingLoad'" @click="script.$script.load()">
Load
</NButton>
<NButton v-else-if="script.$script.status === 'loaded'" @click="script.$script.remove()">
Remove
</NButton>
</div>
</div>
<div class="space-y-2">
<div v-for="(event, key) in script.events" :key="key" class="flex gap-3 text-xs justify-start items-center">
<div class="opacity-40">
{{ humanFriendlyTimestamp(event.at) }}
</div>
<template v-if="event.type === 'status'">
<div v-if="event.status === 'loaded'" class="font-bold px-2 py-[2px] bg-green-50 text-green-700 rounded-lg">
{{ event.status }}
</div>
<div v-else-if="event.status === 'awaitingLoad'" class="font-bold px-2 py-[2px] bg-gray-100 text-gray-700 rounded-lg">
{{ event.status }}
</div>
<div v-else-if="event.status === 'removed' || event.status === 'error'" class="font-bold px-2 py-[2px] bg-red-100 text-red-700 rounded-lg">
{{ event.status }}
</div>
<div v-else-if="event.status === 'loading'" class="font-bold px-2 py-[2px] bg-yellow-100 text-yellow-700 rounded-lg">
{{ event.status }}
</div>
</template>
<template v-else-if="event.type === 'fn-call'">
<OCodeBlock :code="`${event.fn}(${event.args.map(a => JSON.stringify(a, null, 2)).join(', ')})`" lang="javascript" />
</template>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</template>
35 changes: 35 additions & 0 deletions client/components/OCodeBlock.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script setup lang="ts">
import type { BundledLanguage } from 'shiki'
import { computed } from 'vue'
import { renderCodeHighlight } from '../composables/shiki'
const props = withDefaults(
defineProps<{
code: string
lang?: BundledLanguage
lines?: boolean
transformRendered?: (code: string) => string
}>(),
{
lines: false,
},
)
const rendered = computed(() => {
const code = renderCodeHighlight(props.code, props.lang)
return props.transformRendered ? props.transformRendered(code.value || '') : code.value
})
</script>

<template>
<pre
class="n-code-block"
:class="lines ? 'n-code-block-lines' : ''"
v-html="rendered"
/>
</template>

<style>
.n-code-block-lines .shiki code .line::before {
display: none;
}
</style>
7 changes: 7 additions & 0 deletions client/composables/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ref } from 'vue'
import type { $Fetch } from 'nitropack'
import type { NuxtDevtoolsClient } from '@nuxt/devtools-kit/types'

export const devtools = ref<NuxtDevtoolsClient>()

export const appFetch = ref<$Fetch>()
39 changes: 39 additions & 0 deletions client/composables/shiki.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Highlighter, Lang } from 'shiki'
import { getHighlighter } from 'shiki'
import { computed, ref, toValue } from 'vue'
import type { MaybeRef } from '@vueuse/core'
import { devtools } from './rpc'

export const shiki = ref<Highlighter>()

export function loadShiki() {
// Only loading when needed
return getHighlighter({
themes: [
'vitesse-dark',
'vitesse-light',
],
langs: [
'css',
'javascript',
'typescript',
'html',
'vue',
'vue-html',
'bash',
'diff',
],
}).then((i) => {
shiki.value = i
})
}

export function renderCodeHighlight(code: MaybeRef<string>, lang?: Lang) {
return computed(() => {
const colorMode = devtools.value?.colorMode || 'light'
return shiki.value!.codeToHtml(toValue(code), {
lang,
theme: colorMode === 'dark' ? 'vitesse-dark' : 'vitesse-light',
}) || ''
})
}
Loading

0 comments on commit 8e592f9

Please sign in to comment.