-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: migration from scripts-and-assets
- Loading branch information
Showing
39 changed files
with
2,864 additions
and
2,486 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
||
`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 | ||
}) | ||
``` | ||
|
||
|
||
## License | ||
|
||
Licensed under the [MIT license](https://github.com/nuxt/scripts/blob/main/LICENSE.md). | ||
|
||
|
||
<!-- 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}) || '' | ||
}) | ||
} |
Oops, something went wrong.