Skip to content

Commit

Permalink
Merge pull request #2 from huang-julien/chore/types
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw authored Aug 29, 2023
2 parents d0838ad + 6e0f4f6 commit daf936a
Show file tree
Hide file tree
Showing 10 changed files with 1,062 additions and 219 deletions.
1,198 changes: 1,008 additions & 190 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
packages:
- "playground"
2 changes: 1 addition & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default defineNuxtModule<ModuleOptions>({
proxyTtl: 60 * 60 * 24
}

const customScriptNames = Object.keys(options.global || {}).map(m => `'${m.name}'`).join(' | ')
const customScriptNames = Object.values(options.globals || {}).map(m => `'${m.name}'`).join(' | ')
if (customScriptNames) {
// extend types with global names
addTypeTemplate({
Expand Down
19 changes: 13 additions & 6 deletions src/runtime/composables/useScript.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {UseScriptInput, UseScriptReturn, UseScriptStatus} from "../types";
import { UseScriptInput, UseScriptReturn, UseScriptStatus } from "../types";
import { ref, toValue, computed } from 'vue'
import {useRequestEvent, injectHead, useNuxtApp } from '#imports'
import type {Head, RuntimeMode} from '@unhead/schema'
Expand All @@ -18,12 +18,12 @@ import { hash } from 'ohash'
* - innerHTML
*/

export function useScript(input: UseScriptInput): Promise<UseScriptReturn> {
const error = ref('')
export function useScript(input: UseScriptInput): Promise<UseScriptReturn>|UseScriptReturn {
const error = ref<Error | null>(null)

const resolvedInput = toValue(input)

const presets = input.presets || []
const presets = resolvedInput.presets || []
delete input.presets

const status = ref<UseScriptStatus>('awaitingLoad')
Expand All @@ -41,7 +41,9 @@ export function useScript(input: UseScriptInput): Promise<UseScriptReturn> {
}

const mode: RuntimeMode = input.mode || 'all'
delete input.mode
if(input.mode) {
delete input.mode
}

const requestEvent = process.server ? useRequestEvent() : null

Expand All @@ -56,7 +58,10 @@ export function useScript(input: UseScriptInput): Promise<UseScriptReturn> {
for (const preset of presets) {
preset.setup?.(scriptLoadCtx)
}

async function transform(input: Head) {
if(!input.script) return { script: [] }

const script = input.script[0] as ScriptBase
for (const preset of presets) {
if (await preset.transform?.(script, scriptLoadCtx) === false) {
Expand Down Expand Up @@ -86,15 +91,17 @@ export function useScript(input: UseScriptInput): Promise<UseScriptReturn> {
status.value = 'loaded'
}
})

scriptLoadCtx.loadPromise
.then(() => {
status.value = 'loading'
if (process.server && mode === 'client')
return
if (process.client && mode === 'server')
return
head.push({script: [input]}, {mode, transform})
head.push({ script: [input] }, { mode, transform: transform as (input: unknown) => unknown })
})
.catch(e => error.value = e)

return { status, error, loaded: computed(() => status.value === 'loaded' )}
}
8 changes: 6 additions & 2 deletions src/runtime/presets/inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ import {defineScriptPreset} from "../util";
export const ScriptPresetInline = () => defineScriptPreset({
name: 'inline-external-script',
async transform(script, { status, requestEvent }) {
if (!script.src) {
console.warn('[nuxt-script]: script has no src')
return
}
const scriptProps = await $fetch('/api/__nuxt_script__/inline', {
accept: 'application/javascript',
query: {
src: encodeURIComponent(script.src),
integrity: encodeURIComponent(script.integrity),
integrity: script.integrity ? encodeURIComponent(script.integrity) : undefined,
}
}).catch((error) => {
status.value = 'error'
}) as RawScript
Object.assign(script, scriptProps)
if (script.integrity) {
if (process.server)
if (process.server && requestEvent)
// we need to use a CSP header for inline scripts
setHeader(requestEvent, process.dev ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy', `base-uri 'self'; script-src 'self' '${script.integrity}'`)
delete script.integrity
Expand Down
8 changes: 5 additions & 3 deletions src/runtime/presets/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { withQuery } from 'ufo'
export const ScriptPresetProxy = () => defineScriptPreset({
name: 'proxy',
transform(script) {
script.src = withQuery(`/api/__nuxt_script__/proxy`, {
src: encodeURIComponent(script.src),
})
if(script.src) {
script.src = withQuery(`/api/__nuxt_script__/proxy`, {
src: encodeURIComponent(script.src),
})
}
}
})
7 changes: 4 additions & 3 deletions src/runtime/server/api/inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,30 @@ import {useCachedScript} from "../util";
import { createConsola } from 'consola'

export default defineEventHandler(async (e) => {
// eslint-disable-next-line prefer-const
let { src, ttl, purge, integrity } = getQuery(e)

if (typeof src !== 'string')
return

src = decodeURIComponent(src)
integrity = decodeURIComponent(integrity as string)

const { script, cacheResult, cacheExpires } = await useCachedScript(src,
const cachedScript= await useCachedScript(src,
{
ttl: Number(ttl),
purge: Boolean(purge)
}
)

if (!script) {
if (!cachedScript || !cachedScript.script) {
// error
const error = createError({
status: 400,
statusText: `The script ${src} was not found.`,
})
return sendError(e, error)
}
const { script, cacheResult, cacheExpires } = cachedScript

const logger = createConsola()

Expand Down
9 changes: 6 additions & 3 deletions src/runtime/server/api/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import { defineEventHandler, getQuery, setHeader, proxyRequest } from 'h3'
import {useCachedScript} from "../util";
import { useCachedScript } from "../util";

export default defineEventHandler(async (e) => {
// eslint-disable-next-line prefer-const
let { src, ttl, purge } = getQuery(e)

if (typeof src !== 'string')
return

src = decodeURIComponent(src)

const { script, cacheResult, cacheExpires, ttl: _ttl } = await useCachedScript(src,
const cachedScript = await useCachedScript(src,
{
ttl: Number(ttl),
purge: Boolean(purge)
}
)

if (!script) {
if (!cachedScript || !cachedScript.script) {
return proxyRequest(e, src)
}

const { script, cacheResult, cacheExpires, ttl: _ttl } = cachedScript

setHeader(e, 'x-nuxt-script-proxy-cache', cacheResult)
setHeader(e, 'x-nuxt-script-proxy-cache-ttl', cacheExpires.toString())
setHeader(e, 'content-type', 'text/javascript; charset=utf-8')
Expand Down
16 changes: 11 additions & 5 deletions src/runtime/server/util.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import {prefixStorage} from "unstorage";
import {useRuntimeConfig, useStorage} from "#imports";
import {prefixStorage} from "unstorage"
import { useRuntimeConfig } from "#imports"
import {sha256base64} from 'ohash'
import { Script } from '@unhead/schema'
import type { Storage } from 'unstorage'

interface CachedScript {
value: Script,
expiresAt: number
}

export async function useCachedScript(src: string, options?: { ttl?: number; purge?: boolean }) {
const key = src as string
const ttl = options?.ttl || useRuntimeConfig()['nuxt-script'].proxyTtl || 0

const ttl = options?.ttl || useRuntimeConfig()['nuxt-script']?.proxyTtl || 0

const useCache = true
const cache = prefixStorage(useStorage(), `/cache/nuxt-script/proxy`) as Storage<{ value: Script; expiresAt: number }>
Expand All @@ -15,7 +21,7 @@ export async function useCachedScript(src: string, options?: { ttl?: number; pur
// cache will invalidate if the options change
let script: Script | false = false
if (useCache && await cache.hasItem(key)) {
const { value, expiresAt } = await cache.getItem(key)
const { value, expiresAt } = (await cache.getItem<CachedScript>(key))!
if (expiresAt > Date.now()) {
if (options?.purge) {
cacheResult = 'PURGE'
Expand Down Expand Up @@ -43,7 +49,7 @@ export async function useCachedScript(src: string, options?: { ttl?: number; pur
}
script = {
innerHTML: result._data,
integrity: `sha256-${sha256base64(result._data)}`
integrity: `sha256-${sha256base64(result._data as string)}`
}
if (useCache) {
await cache.setItem(key, { value: script, expiresAt: Date.now() + ttl })
Expand Down
12 changes: 6 additions & 6 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import type {ComputedRef, Ref} from "vue";
import type { ScriptBase } from 'zhead'
import type { Script } from '@unhead/vue'
import type { Script, RuntimeMode } from '@unhead/schema'
import {H3Event} from "h3";

export interface ScriptLoadContext {
status: Ref<UseScriptStatus>
error: Ref<string>
requestEvent: H3Event
error: Ref<Error|null>
requestEvent: H3Event | null
loadPromise: Promise<void>
mode: 'all' | 'client' | 'server'
}

export interface ScriptPreset {
name: string
transform: (script: ScriptBase, ctx: ScriptLoadContext) => Promise<void | false> | void | false
transform?: (script: ScriptBase, ctx: ScriptLoadContext) => Promise<void | false> | void | false
setup?: (ctx: ScriptLoadContext) => void
}

export type UseScriptInput = { presets?: ScriptPreset[] } & Script
export type UseScriptInput = Script<{ presets?: ScriptPreset[], mode?: RuntimeMode }>

export type UseScriptStatus = 'awaitingLoad' | 'loading' | 'loaded' | 'error'

export interface UseScriptReturn {
loaded: ComputedRef<boolean>
status: Ref<UseScriptStatus>
error: Ref<string>
error: Ref<Error|null>
}

0 comments on commit daf936a

Please sign in to comment.