Add AnyClip integration tools and extracted source code
- Add authentication scripts with SubtleCrypto password encryption - Add sourcemap extraction pipeline (update-urls, download-sourcemaps, extract-sources) - Add Playwright API interception script for monetization endpoints - Document two-step auth flow with JWT tokens and dual cookies - Move extracted source from root to anyclip/ directory - Add project configuration (.env.example, .gitignore, CLAUDE.md)
This commit is contained in:
12
anyclip/client/add-base-path.ts
Normal file
12
anyclip/client/add-base-path.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { addPathPrefix } from '../shared/lib/router/utils/add-path-prefix'
|
||||
import { normalizePathTrailingSlash } from './normalize-trailing-slash'
|
||||
|
||||
const basePath = (process.env.__NEXT_ROUTER_BASEPATH as string) || ''
|
||||
|
||||
export function addBasePath(path: string, required?: boolean): string {
|
||||
return normalizePathTrailingSlash(
|
||||
process.env.__NEXT_MANUAL_CLIENT_BASE_PATH && !required
|
||||
? path
|
||||
: addPathPrefix(path, basePath)
|
||||
)
|
||||
}
|
||||
13
anyclip/client/add-locale.ts
Normal file
13
anyclip/client/add-locale.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { addLocale as Fn } from '../shared/lib/router/utils/add-locale'
|
||||
import { normalizePathTrailingSlash } from './normalize-trailing-slash'
|
||||
|
||||
export const addLocale: typeof Fn = (path, ...args) => {
|
||||
if (process.env.__NEXT_I18N_SUPPORT) {
|
||||
return normalizePathTrailingSlash(
|
||||
(
|
||||
require('../shared/lib/router/utils/add-locale') as typeof import('../shared/lib/router/utils/add-locale')
|
||||
).addLocale(path, ...args)
|
||||
)
|
||||
}
|
||||
return path
|
||||
}
|
||||
9
anyclip/client/detect-domain-locale.ts
Normal file
9
anyclip/client/detect-domain-locale.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { detectDomainLocale as Fn } from '../shared/lib/i18n/detect-domain-locale'
|
||||
|
||||
export const detectDomainLocale: typeof Fn = (...args) => {
|
||||
if (process.env.__NEXT_I18N_SUPPORT) {
|
||||
return (
|
||||
require('../shared/lib/i18n/detect-domain-locale') as typeof import('../shared/lib/i18n/detect-domain-locale')
|
||||
).detectDomainLocale(...args)
|
||||
}
|
||||
}
|
||||
35
anyclip/client/get-domain-locale.ts
Normal file
35
anyclip/client/get-domain-locale.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { DomainLocale } from '../server/config'
|
||||
import type { normalizeLocalePath as NormalizeFn } from './normalize-locale-path'
|
||||
import type { detectDomainLocale as DetectFn } from './detect-domain-locale'
|
||||
import { normalizePathTrailingSlash } from './normalize-trailing-slash'
|
||||
|
||||
const basePath = (process.env.__NEXT_ROUTER_BASEPATH as string) || ''
|
||||
|
||||
export function getDomainLocale(
|
||||
path: string,
|
||||
locale?: string | false,
|
||||
locales?: readonly string[],
|
||||
domainLocales?: readonly DomainLocale[]
|
||||
) {
|
||||
if (process.env.__NEXT_I18N_SUPPORT) {
|
||||
const normalizeLocalePath: typeof NormalizeFn = (
|
||||
require('./normalize-locale-path') as typeof import('./normalize-locale-path')
|
||||
).normalizeLocalePath
|
||||
const detectDomainLocale: typeof DetectFn = (
|
||||
require('./detect-domain-locale') as typeof import('./detect-domain-locale')
|
||||
).detectDomainLocale
|
||||
|
||||
const target = locale || normalizeLocalePath(path, locales).detectedLocale
|
||||
const domain = detectDomainLocale(domainLocales, undefined, target)
|
||||
if (domain) {
|
||||
const proto = `http${domain.http ? '' : 's'}://`
|
||||
const finalLocale = target === domain.defaultLocale ? '' : `/${target}`
|
||||
return `${proto}${domain.domain}${normalizePathTrailingSlash(
|
||||
`${basePath}${finalLocale}${path}`
|
||||
)}`
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
7
anyclip/client/has-base-path.ts
Normal file
7
anyclip/client/has-base-path.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { pathHasPrefix } from '../shared/lib/router/utils/path-has-prefix'
|
||||
|
||||
const basePath = (process.env.__NEXT_ROUTER_BASEPATH as string) || ''
|
||||
|
||||
export function hasBasePath(path: string): boolean {
|
||||
return pathHasPrefix(path, basePath)
|
||||
}
|
||||
150
anyclip/client/head-manager.ts
Normal file
150
anyclip/client/head-manager.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { setAttributesFromProps } from './set-attributes-from-props'
|
||||
|
||||
import type { JSX } from 'react'
|
||||
|
||||
function reactElementToDOM({ type, props }: JSX.Element): HTMLElement {
|
||||
const el: HTMLElement = document.createElement(type)
|
||||
setAttributesFromProps(el, props)
|
||||
|
||||
const { children, dangerouslySetInnerHTML } = props
|
||||
if (dangerouslySetInnerHTML) {
|
||||
el.innerHTML = dangerouslySetInnerHTML.__html || ''
|
||||
} else if (children) {
|
||||
el.textContent =
|
||||
typeof children === 'string'
|
||||
? children
|
||||
: Array.isArray(children)
|
||||
? children.join('')
|
||||
: ''
|
||||
}
|
||||
return el
|
||||
}
|
||||
|
||||
/**
|
||||
* When a `nonce` is present on an element, browsers such as Chrome and Firefox strip it out of the
|
||||
* actual HTML attributes for security reasons *when the element is added to the document*. Thus,
|
||||
* given two equivalent elements that have nonces, `Element,isEqualNode()` will return false if one
|
||||
* of those elements gets added to the document. Although the `element.nonce` property will be the
|
||||
* same for both elements, the one that was added to the document will return an empty string for
|
||||
* its nonce HTML attribute value.
|
||||
*
|
||||
* This custom `isEqualNode()` function therefore removes the nonce value from the `newTag` before
|
||||
* comparing it to `oldTag`, restoring it afterwards.
|
||||
*
|
||||
* For more information, see:
|
||||
* https://bugs.chromium.org/p/chromium/issues/detail?id=1211471#c12
|
||||
*/
|
||||
export function isEqualNode(oldTag: Element, newTag: Element) {
|
||||
if (oldTag instanceof HTMLElement && newTag instanceof HTMLElement) {
|
||||
const nonce = newTag.getAttribute('nonce')
|
||||
// Only strip the nonce if `oldTag` has had it stripped. An element's nonce attribute will not
|
||||
// be stripped if there is no content security policy response header that includes a nonce.
|
||||
if (nonce && !oldTag.getAttribute('nonce')) {
|
||||
const cloneTag = newTag.cloneNode(true) as typeof newTag
|
||||
cloneTag.setAttribute('nonce', '')
|
||||
cloneTag.nonce = nonce
|
||||
return nonce === oldTag.nonce && oldTag.isEqualNode(cloneTag)
|
||||
}
|
||||
}
|
||||
|
||||
return oldTag.isEqualNode(newTag)
|
||||
}
|
||||
|
||||
function updateElements(type: string, components: JSX.Element[]) {
|
||||
const headEl = document.querySelector('head')
|
||||
if (!headEl) return
|
||||
|
||||
const oldTags = new Set(headEl.querySelectorAll(`${type}[data-next-head]`))
|
||||
|
||||
if (type === 'meta') {
|
||||
const metaCharset = headEl.querySelector('meta[charset]')
|
||||
if (metaCharset !== null) {
|
||||
oldTags.add(metaCharset)
|
||||
}
|
||||
}
|
||||
|
||||
const newTags: Element[] = []
|
||||
for (let i = 0; i < components.length; i++) {
|
||||
const component = components[i]
|
||||
const newTag = reactElementToDOM(component)
|
||||
newTag.setAttribute('data-next-head', '')
|
||||
|
||||
let isNew = true
|
||||
for (const oldTag of oldTags) {
|
||||
if (isEqualNode(oldTag, newTag)) {
|
||||
oldTags.delete(oldTag)
|
||||
isNew = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
newTags.push(newTag)
|
||||
}
|
||||
}
|
||||
|
||||
for (const oldTag of oldTags) {
|
||||
oldTag.parentNode?.removeChild(oldTag)
|
||||
}
|
||||
|
||||
for (const newTag of newTags) {
|
||||
// meta[charset] must be first element so special case
|
||||
if (
|
||||
newTag.tagName.toLowerCase() === 'meta' &&
|
||||
newTag.getAttribute('charset') !== null
|
||||
) {
|
||||
headEl.prepend(newTag)
|
||||
}
|
||||
headEl.appendChild(newTag)
|
||||
}
|
||||
}
|
||||
|
||||
export default function initHeadManager(): {
|
||||
mountedInstances: Set<unknown>
|
||||
updateHead: (head: JSX.Element[]) => void
|
||||
} {
|
||||
return {
|
||||
mountedInstances: new Set(),
|
||||
updateHead: (head: JSX.Element[]) => {
|
||||
const tags: Record<string, JSX.Element[]> = {}
|
||||
|
||||
head.forEach((h) => {
|
||||
if (
|
||||
// If the font tag is loaded only on client navigation
|
||||
// it won't be inlined. In this case revert to the original behavior
|
||||
h.type === 'link' &&
|
||||
h.props['data-optimized-fonts']
|
||||
) {
|
||||
if (
|
||||
document.querySelector(`style[data-href="${h.props['data-href']}"]`)
|
||||
) {
|
||||
return
|
||||
} else {
|
||||
h.props.href = h.props['data-href']
|
||||
h.props['data-href'] = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const components = tags[h.type] || []
|
||||
components.push(h)
|
||||
tags[h.type] = components
|
||||
})
|
||||
|
||||
const titleComponent = tags.title ? tags.title[0] : null
|
||||
let title = ''
|
||||
if (titleComponent) {
|
||||
const { children } = titleComponent.props
|
||||
title =
|
||||
typeof children === 'string'
|
||||
? children
|
||||
: Array.isArray(children)
|
||||
? children.join('')
|
||||
: ''
|
||||
}
|
||||
if (title !== document.title) document.title = title
|
||||
;['meta', 'base', 'link', 'style', 'script'].forEach((type) => {
|
||||
updateElements(type, tags[type] || [])
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
421
anyclip/client/image-component.tsx
Normal file
421
anyclip/client/image-component.tsx
Normal file
@@ -0,0 +1,421 @@
|
||||
'use client'
|
||||
|
||||
import React, {
|
||||
useRef,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
forwardRef,
|
||||
use,
|
||||
} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import Head from '../shared/lib/head'
|
||||
import { getImgProps } from '../shared/lib/get-img-props'
|
||||
import type {
|
||||
ImageProps,
|
||||
ImgProps,
|
||||
OnLoad,
|
||||
OnLoadingComplete,
|
||||
PlaceholderValue,
|
||||
} from '../shared/lib/get-img-props'
|
||||
import type {
|
||||
ImageConfigComplete,
|
||||
ImageLoaderProps,
|
||||
} from '../shared/lib/image-config'
|
||||
import { imageConfigDefault } from '../shared/lib/image-config'
|
||||
import { ImageConfigContext } from '../shared/lib/image-config-context.shared-runtime'
|
||||
import { warnOnce } from '../shared/lib/utils/warn-once'
|
||||
import { RouterContext } from '../shared/lib/router-context.shared-runtime'
|
||||
|
||||
// This is replaced by webpack alias
|
||||
import defaultLoader from 'next/dist/shared/lib/image-loader'
|
||||
import { useMergedRef } from './use-merged-ref'
|
||||
|
||||
// This is replaced by webpack define plugin
|
||||
const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
;(globalThis as any).__NEXT_IMAGE_IMPORTED = true
|
||||
}
|
||||
|
||||
export type { ImageLoaderProps }
|
||||
export type ImageLoader = (p: ImageLoaderProps) => string
|
||||
|
||||
type ImgElementWithDataProp = HTMLImageElement & {
|
||||
'data-loaded-src': string | undefined
|
||||
}
|
||||
|
||||
type ImageElementProps = ImgProps & {
|
||||
unoptimized: boolean
|
||||
placeholder: PlaceholderValue
|
||||
onLoadRef: React.MutableRefObject<OnLoad | undefined>
|
||||
onLoadingCompleteRef: React.MutableRefObject<OnLoadingComplete | undefined>
|
||||
setBlurComplete: (b: boolean) => void
|
||||
setShowAltText: (b: boolean) => void
|
||||
sizesInput: string | undefined
|
||||
}
|
||||
|
||||
// See https://stackoverflow.com/q/39777833/266535 for why we use this ref
|
||||
// handler instead of the img's onLoad attribute.
|
||||
function handleLoading(
|
||||
img: ImgElementWithDataProp,
|
||||
placeholder: PlaceholderValue,
|
||||
onLoadRef: React.MutableRefObject<OnLoad | undefined>,
|
||||
onLoadingCompleteRef: React.MutableRefObject<OnLoadingComplete | undefined>,
|
||||
setBlurComplete: (b: boolean) => void,
|
||||
unoptimized: boolean,
|
||||
sizesInput: string | undefined
|
||||
) {
|
||||
const src = img?.src
|
||||
if (!img || img['data-loaded-src'] === src) {
|
||||
return
|
||||
}
|
||||
img['data-loaded-src'] = src
|
||||
const p = 'decode' in img ? img.decode() : Promise.resolve()
|
||||
p.catch(() => {}).then(() => {
|
||||
if (!img.parentElement || !img.isConnected) {
|
||||
// Exit early in case of race condition:
|
||||
// - onload() is called
|
||||
// - decode() is called but incomplete
|
||||
// - unmount is called
|
||||
// - decode() completes
|
||||
return
|
||||
}
|
||||
if (placeholder !== 'empty') {
|
||||
setBlurComplete(true)
|
||||
}
|
||||
if (onLoadRef?.current) {
|
||||
// Since we don't have the SyntheticEvent here,
|
||||
// we must create one with the same shape.
|
||||
// See https://reactjs.org/docs/events.html
|
||||
const event = new Event('load')
|
||||
Object.defineProperty(event, 'target', { writable: false, value: img })
|
||||
let prevented = false
|
||||
let stopped = false
|
||||
onLoadRef.current({
|
||||
...event,
|
||||
nativeEvent: event,
|
||||
currentTarget: img,
|
||||
target: img,
|
||||
isDefaultPrevented: () => prevented,
|
||||
isPropagationStopped: () => stopped,
|
||||
persist: () => {},
|
||||
preventDefault: () => {
|
||||
prevented = true
|
||||
event.preventDefault()
|
||||
},
|
||||
stopPropagation: () => {
|
||||
stopped = true
|
||||
event.stopPropagation()
|
||||
},
|
||||
})
|
||||
}
|
||||
if (onLoadingCompleteRef?.current) {
|
||||
onLoadingCompleteRef.current(img)
|
||||
}
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const origSrc = new URL(src, 'http://n').searchParams.get('url') || src
|
||||
if (img.getAttribute('data-nimg') === 'fill') {
|
||||
if (!unoptimized && (!sizesInput || sizesInput === '100vw')) {
|
||||
let widthViewportRatio =
|
||||
img.getBoundingClientRect().width / window.innerWidth
|
||||
if (widthViewportRatio < 0.6) {
|
||||
if (sizesInput === '100vw') {
|
||||
warnOnce(
|
||||
`Image with src "${origSrc}" has "fill" prop and "sizes" prop of "100vw", but image is not rendered at full viewport width. Please adjust "sizes" to improve page performance. Read more: https://nextjs.org/docs/api-reference/next/image#sizes`
|
||||
)
|
||||
} else {
|
||||
warnOnce(
|
||||
`Image with src "${origSrc}" has "fill" but is missing "sizes" prop. Please add it to improve page performance. Read more: https://nextjs.org/docs/api-reference/next/image#sizes`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (img.parentElement) {
|
||||
const { position } = window.getComputedStyle(img.parentElement)
|
||||
const valid = ['absolute', 'fixed', 'relative']
|
||||
if (!valid.includes(position)) {
|
||||
warnOnce(
|
||||
`Image with src "${origSrc}" has "fill" and parent element with invalid "position". Provided "${position}" should be one of ${valid
|
||||
.map(String)
|
||||
.join(',')}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
if (img.height === 0) {
|
||||
warnOnce(
|
||||
`Image with src "${origSrc}" has "fill" and a height value of 0. This is likely because the parent element of the image has not been styled to have a set height.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const heightModified =
|
||||
img.height.toString() !== img.getAttribute('height')
|
||||
const widthModified = img.width.toString() !== img.getAttribute('width')
|
||||
if (
|
||||
(heightModified && !widthModified) ||
|
||||
(!heightModified && widthModified)
|
||||
) {
|
||||
warnOnce(
|
||||
`Image with src "${origSrc}" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio.`
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getDynamicProps(
|
||||
fetchPriority?: string
|
||||
): Record<string, string | undefined> {
|
||||
if (Boolean(use)) {
|
||||
// In React 19.0.0 or newer, we must use camelCase
|
||||
// prop to avoid "Warning: Invalid DOM property".
|
||||
// See https://github.com/facebook/react/pull/25927
|
||||
return { fetchPriority }
|
||||
}
|
||||
// In React 18.2.0 or older, we must use lowercase prop
|
||||
// to avoid "Warning: Invalid DOM property".
|
||||
return { fetchpriority: fetchPriority }
|
||||
}
|
||||
|
||||
const ImageElement = forwardRef<HTMLImageElement | null, ImageElementProps>(
|
||||
(
|
||||
{
|
||||
src,
|
||||
srcSet,
|
||||
sizes,
|
||||
height,
|
||||
width,
|
||||
decoding,
|
||||
className,
|
||||
style,
|
||||
fetchPriority,
|
||||
placeholder,
|
||||
loading,
|
||||
unoptimized,
|
||||
fill,
|
||||
onLoadRef,
|
||||
onLoadingCompleteRef,
|
||||
setBlurComplete,
|
||||
setShowAltText,
|
||||
sizesInput,
|
||||
onLoad,
|
||||
onError,
|
||||
...rest
|
||||
},
|
||||
forwardedRef
|
||||
) => {
|
||||
const ownRef = useCallback(
|
||||
(img: ImgElementWithDataProp | null) => {
|
||||
if (!img) {
|
||||
return
|
||||
}
|
||||
if (onError) {
|
||||
// If the image has an error before react hydrates, then the error is lost.
|
||||
// The workaround is to wait until the image is mounted which is after hydration,
|
||||
// then we set the src again to trigger the error handler (if there was an error).
|
||||
// eslint-disable-next-line no-self-assign
|
||||
img.src = img.src
|
||||
}
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!src) {
|
||||
console.error(`Image is missing required "src" property:`, img)
|
||||
}
|
||||
if (img.getAttribute('alt') === null) {
|
||||
console.error(
|
||||
`Image is missing required "alt" property. Please add Alternative Text to describe the image for screen readers and search engines.`
|
||||
)
|
||||
}
|
||||
}
|
||||
if (img.complete) {
|
||||
handleLoading(
|
||||
img,
|
||||
placeholder,
|
||||
onLoadRef,
|
||||
onLoadingCompleteRef,
|
||||
setBlurComplete,
|
||||
unoptimized,
|
||||
sizesInput
|
||||
)
|
||||
}
|
||||
},
|
||||
[
|
||||
src,
|
||||
placeholder,
|
||||
onLoadRef,
|
||||
onLoadingCompleteRef,
|
||||
setBlurComplete,
|
||||
onError,
|
||||
unoptimized,
|
||||
sizesInput,
|
||||
]
|
||||
)
|
||||
|
||||
const ref = useMergedRef(forwardedRef, ownRef)
|
||||
|
||||
return (
|
||||
<img
|
||||
{...rest}
|
||||
{...getDynamicProps(fetchPriority)}
|
||||
// It's intended to keep `loading` before `src` because React updates
|
||||
// props in order which causes Safari/Firefox to not lazy load properly.
|
||||
// See https://github.com/facebook/react/issues/25883
|
||||
loading={loading}
|
||||
width={width}
|
||||
height={height}
|
||||
decoding={decoding}
|
||||
data-nimg={fill ? 'fill' : '1'}
|
||||
className={className}
|
||||
style={style}
|
||||
// It's intended to keep `src` the last attribute because React updates
|
||||
// attributes in order. If we keep `src` the first one, Safari will
|
||||
// immediately start to fetch `src`, before `sizes` and `srcSet` are even
|
||||
// updated by React. That causes multiple unnecessary requests if `srcSet`
|
||||
// and `sizes` are defined.
|
||||
// This bug cannot be reproduced in Chrome or Firefox.
|
||||
sizes={sizes}
|
||||
srcSet={srcSet}
|
||||
src={src}
|
||||
ref={ref}
|
||||
onLoad={(event) => {
|
||||
const img = event.currentTarget as ImgElementWithDataProp
|
||||
handleLoading(
|
||||
img,
|
||||
placeholder,
|
||||
onLoadRef,
|
||||
onLoadingCompleteRef,
|
||||
setBlurComplete,
|
||||
unoptimized,
|
||||
sizesInput
|
||||
)
|
||||
}}
|
||||
onError={(event) => {
|
||||
// if the real image fails to load, this will ensure "alt" is visible
|
||||
setShowAltText(true)
|
||||
if (placeholder !== 'empty') {
|
||||
// If the real image fails to load, this will still remove the placeholder.
|
||||
setBlurComplete(true)
|
||||
}
|
||||
if (onError) {
|
||||
onError(event)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
function ImagePreload({
|
||||
isAppRouter,
|
||||
imgAttributes,
|
||||
}: {
|
||||
isAppRouter: boolean
|
||||
imgAttributes: ImgProps
|
||||
}) {
|
||||
const opts: ReactDOM.PreloadOptions = {
|
||||
as: 'image',
|
||||
imageSrcSet: imgAttributes.srcSet,
|
||||
imageSizes: imgAttributes.sizes,
|
||||
crossOrigin: imgAttributes.crossOrigin,
|
||||
referrerPolicy: imgAttributes.referrerPolicy,
|
||||
...getDynamicProps(imgAttributes.fetchPriority),
|
||||
}
|
||||
|
||||
if (isAppRouter && ReactDOM.preload) {
|
||||
ReactDOM.preload(imgAttributes.src, opts)
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<link
|
||||
key={
|
||||
'__nimg-' +
|
||||
imgAttributes.src +
|
||||
imgAttributes.srcSet +
|
||||
imgAttributes.sizes
|
||||
}
|
||||
rel="preload"
|
||||
// Note how we omit the `href` attribute, as it would only be relevant
|
||||
// for browsers that do not support `imagesrcset`, and in those cases
|
||||
// it would cause the incorrect image to be preloaded.
|
||||
//
|
||||
// https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset
|
||||
href={imgAttributes.srcSet ? undefined : imgAttributes.src}
|
||||
{...opts}
|
||||
/>
|
||||
</Head>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The `Image` component is used to optimize images.
|
||||
*
|
||||
* Read more: [Next.js docs: `Image`](https://nextjs.org/docs/app/api-reference/components/image)
|
||||
*/
|
||||
export const Image = forwardRef<HTMLImageElement | null, ImageProps>(
|
||||
(props, forwardedRef) => {
|
||||
const pagesRouter = useContext(RouterContext)
|
||||
// We're in the app directory if there is no pages router.
|
||||
const isAppRouter = !pagesRouter
|
||||
|
||||
const configContext = useContext(ImageConfigContext)
|
||||
const config = useMemo(() => {
|
||||
const c = configEnv || configContext || imageConfigDefault
|
||||
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
|
||||
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
|
||||
const qualities = c.qualities?.sort((a, b) => a - b)
|
||||
return { ...c, allSizes, deviceSizes, qualities }
|
||||
}, [configContext])
|
||||
|
||||
const { onLoad, onLoadingComplete } = props
|
||||
const onLoadRef = useRef(onLoad)
|
||||
|
||||
useEffect(() => {
|
||||
onLoadRef.current = onLoad
|
||||
}, [onLoad])
|
||||
|
||||
const onLoadingCompleteRef = useRef(onLoadingComplete)
|
||||
|
||||
useEffect(() => {
|
||||
onLoadingCompleteRef.current = onLoadingComplete
|
||||
}, [onLoadingComplete])
|
||||
|
||||
const [blurComplete, setBlurComplete] = useState(false)
|
||||
const [showAltText, setShowAltText] = useState(false)
|
||||
|
||||
const { props: imgAttributes, meta: imgMeta } = getImgProps(props, {
|
||||
defaultLoader,
|
||||
imgConf: config,
|
||||
blurComplete,
|
||||
showAltText,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
<ImageElement
|
||||
{...imgAttributes}
|
||||
unoptimized={imgMeta.unoptimized}
|
||||
placeholder={imgMeta.placeholder}
|
||||
fill={imgMeta.fill}
|
||||
onLoadRef={onLoadRef}
|
||||
onLoadingCompleteRef={onLoadingCompleteRef}
|
||||
setBlurComplete={setBlurComplete}
|
||||
setShowAltText={setShowAltText}
|
||||
sizesInput={props.sizes}
|
||||
ref={forwardedRef}
|
||||
/>
|
||||
}
|
||||
{imgMeta.priority ? (
|
||||
<ImagePreload
|
||||
isAppRouter={isAppRouter}
|
||||
imgAttributes={imgAttributes}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
||||
1008
anyclip/client/index.tsx
Normal file
1008
anyclip/client/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
717
anyclip/client/link.tsx
Normal file
717
anyclip/client/link.tsx
Normal file
@@ -0,0 +1,717 @@
|
||||
'use client'
|
||||
|
||||
import type {
|
||||
NextRouter,
|
||||
PrefetchOptions as RouterPrefetchOptions,
|
||||
} from '../shared/lib/router/router'
|
||||
|
||||
import React, { createContext, useContext } from 'react'
|
||||
import type { UrlObject } from 'url'
|
||||
import { resolveHref } from './resolve-href'
|
||||
import { isLocalURL } from '../shared/lib/router/utils/is-local-url'
|
||||
import { formatUrl } from '../shared/lib/router/utils/format-url'
|
||||
import { isAbsoluteUrl } from '../shared/lib/utils'
|
||||
import { addLocale } from './add-locale'
|
||||
import { RouterContext } from '../shared/lib/router-context.shared-runtime'
|
||||
import type { AppRouterInstance } from '../shared/lib/app-router-context.shared-runtime'
|
||||
import { useIntersection } from './use-intersection'
|
||||
import { getDomainLocale } from './get-domain-locale'
|
||||
import { addBasePath } from './add-base-path'
|
||||
import { useMergedRef } from './use-merged-ref'
|
||||
import { errorOnce } from '../shared/lib/utils/error-once'
|
||||
|
||||
type Url = string | UrlObject
|
||||
type RequiredKeys<T> = {
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
|
||||
}[keyof T]
|
||||
type OptionalKeys<T> = {
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? K : never
|
||||
}[keyof T]
|
||||
|
||||
type OnNavigateEventHandler = (event: { preventDefault: () => void }) => void
|
||||
|
||||
type InternalLinkProps = {
|
||||
/**
|
||||
* The path or URL to navigate to. It can also be an object.
|
||||
*
|
||||
* @example https://nextjs.org/docs/api-reference/next/link#with-url-object
|
||||
*/
|
||||
href: Url
|
||||
/**
|
||||
* Optional decorator for the path that will be shown in the browser URL bar. Before Next.js 9.5.3 this was used for dynamic routes, check our [previous docs](https://github.com/vercel/next.js/blob/v9.5.2/docs/api-reference/next/link.md#dynamic-routes) to see how it worked. Note: when this path differs from the one provided in `href` the previous `href`/`as` behavior is used as shown in the [previous docs](https://github.com/vercel/next.js/blob/v9.5.2/docs/api-reference/next/link.md#dynamic-routes).
|
||||
*/
|
||||
as?: Url
|
||||
/**
|
||||
* Replace the current `history` state instead of adding a new url into the stack.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
replace?: boolean
|
||||
/**
|
||||
* Whether to override the default scroll behavior
|
||||
*
|
||||
* @example https://nextjs.org/docs/api-reference/next/link#disable-scrolling-to-the-top-of-the-page
|
||||
*
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
scroll?: boolean
|
||||
/**
|
||||
* Update the path of the current page without rerunning [`getStaticProps`](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props), [`getServerSideProps`](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props) or [`getInitialProps`](/docs/pages/api-reference/functions/get-initial-props).
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
shallow?: boolean
|
||||
/**
|
||||
* Forces `Link` to send the `href` property to its child.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
passHref?: boolean
|
||||
/**
|
||||
* Prefetch the page in the background.
|
||||
* Any `<Link />` that is in the viewport (initially or through scroll) will be prefetched.
|
||||
* Prefetch can be disabled by passing `prefetch={false}`. Prefetching is only enabled in production.
|
||||
*
|
||||
* In App Router:
|
||||
* - "auto", null, undefined (default): For statically generated pages, this will prefetch the full React Server Component data. For dynamic pages, this will prefetch up to the nearest route segment with a [`loading.js`](https://nextjs.org/docs/app/api-reference/file-conventions/loading) file. If there is no loading file, it will not fetch the full tree to avoid fetching too much data.
|
||||
* - `true`: This will prefetch the full React Server Component data for all route segments, regardless of whether they contain a segment with `loading.js`.
|
||||
* - `false`: This will not prefetch any data, even on hover.
|
||||
*
|
||||
* In Pages Router:
|
||||
* - `true` (default): The full route & its data will be prefetched.
|
||||
* - `false`: Prefetching will not happen when entering the viewport, but will still happen on hover.
|
||||
* @defaultValue `true` (pages router) or `null` (app router)
|
||||
*/
|
||||
prefetch?: boolean | 'auto' | null | 'unstable_forceStale'
|
||||
/**
|
||||
* The active locale is automatically prepended. `locale` allows for providing a different locale.
|
||||
* When `false` `href` has to include the locale as the default behavior is disabled.
|
||||
* Note: This is only available in the Pages Router.
|
||||
*/
|
||||
locale?: string | false
|
||||
/**
|
||||
* Enable legacy link behavior.
|
||||
* @deprecated This will be removed in v16
|
||||
* @defaultValue `false`
|
||||
* @see https://github.com/vercel/next.js/commit/489e65ed98544e69b0afd7e0cfc3f9f6c2b803b7
|
||||
*/
|
||||
legacyBehavior?: boolean
|
||||
/**
|
||||
* Optional event handler for when the mouse pointer is moved onto Link
|
||||
*/
|
||||
onMouseEnter?: React.MouseEventHandler<HTMLAnchorElement>
|
||||
/**
|
||||
* Optional event handler for when Link is touched.
|
||||
*/
|
||||
onTouchStart?: React.TouchEventHandler<HTMLAnchorElement>
|
||||
/**
|
||||
* Optional event handler for when Link is clicked.
|
||||
*/
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>
|
||||
/**
|
||||
* Optional event handler for when the `<Link>` is navigated.
|
||||
*/
|
||||
onNavigate?: OnNavigateEventHandler
|
||||
}
|
||||
|
||||
// TODO-APP: Include the full set of Anchor props
|
||||
// adding this to the publicly exported type currently breaks existing apps
|
||||
|
||||
// `RouteInferType` is a stub here to avoid breaking `typedRoutes` when the type
|
||||
// isn't generated yet. It will be replaced when type generation runs.
|
||||
// WARNING: This should be an interface to prevent TypeScript from inlining it
|
||||
// in declarations of libraries dependending on Next.js.
|
||||
// Not trivial to reproduce so only convert to an interface when needed.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export interface LinkProps<RouteInferType = any> extends InternalLinkProps {}
|
||||
type LinkPropsRequired = RequiredKeys<LinkProps>
|
||||
type LinkPropsOptional = OptionalKeys<InternalLinkProps>
|
||||
|
||||
const prefetched = new Set<string>()
|
||||
|
||||
type PrefetchOptions = RouterPrefetchOptions & {
|
||||
/**
|
||||
* bypassPrefetchedCheck will bypass the check to see if the `href` has
|
||||
* already been fetched.
|
||||
*/
|
||||
bypassPrefetchedCheck?: boolean
|
||||
}
|
||||
|
||||
function prefetch(
|
||||
router: NextRouter,
|
||||
href: string,
|
||||
as: string,
|
||||
options: PrefetchOptions
|
||||
): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isLocalURL(href)) {
|
||||
return
|
||||
}
|
||||
|
||||
// We should only dedupe requests when experimental.optimisticClientCache is
|
||||
// disabled.
|
||||
if (!options.bypassPrefetchedCheck) {
|
||||
const locale =
|
||||
// Let the link's locale prop override the default router locale.
|
||||
typeof options.locale !== 'undefined'
|
||||
? options.locale
|
||||
: // Otherwise fallback to the router's locale.
|
||||
'locale' in router
|
||||
? router.locale
|
||||
: undefined
|
||||
|
||||
const prefetchedKey = href + '%' + as + '%' + locale
|
||||
|
||||
// If we've already fetched the key, then don't prefetch it again!
|
||||
if (prefetched.has(prefetchedKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Mark this URL as prefetched.
|
||||
prefetched.add(prefetchedKey)
|
||||
}
|
||||
|
||||
// Prefetch the JSON page if asked (only in the client)
|
||||
// We need to handle a prefetch error here since we may be
|
||||
// loading with priority which can reject but we don't
|
||||
// want to force navigation since this is only a prefetch
|
||||
router.prefetch(href, as, options).catch((err) => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// rethrow to show invalid URL errors
|
||||
throw err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function isModifiedEvent(event: React.MouseEvent): boolean {
|
||||
const eventTarget = event.currentTarget as HTMLAnchorElement | SVGAElement
|
||||
const target = eventTarget.getAttribute('target')
|
||||
return (
|
||||
(target && target !== '_self') ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
event.shiftKey ||
|
||||
event.altKey || // triggers resource download
|
||||
(event.nativeEvent && event.nativeEvent.which === 2)
|
||||
)
|
||||
}
|
||||
|
||||
function linkClicked(
|
||||
e: React.MouseEvent,
|
||||
router: NextRouter | AppRouterInstance,
|
||||
href: string,
|
||||
as: string,
|
||||
replace?: boolean,
|
||||
shallow?: boolean,
|
||||
scroll?: boolean,
|
||||
locale?: string | false,
|
||||
onNavigate?: OnNavigateEventHandler
|
||||
): void {
|
||||
const { nodeName } = e.currentTarget
|
||||
|
||||
// anchors inside an svg have a lowercase nodeName
|
||||
const isAnchorNodeName = nodeName.toUpperCase() === 'A'
|
||||
|
||||
if (
|
||||
(isAnchorNodeName && isModifiedEvent(e)) ||
|
||||
e.currentTarget.hasAttribute('download')
|
||||
) {
|
||||
// ignore click for browser’s default behavior
|
||||
return
|
||||
}
|
||||
|
||||
if (!isLocalURL(href)) {
|
||||
if (replace) {
|
||||
// browser default behavior does not replace the history state
|
||||
// so we need to do it manually
|
||||
e.preventDefault()
|
||||
location.replace(href)
|
||||
}
|
||||
|
||||
// ignore click for browser’s default behavior
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
const navigate = () => {
|
||||
if (onNavigate) {
|
||||
let isDefaultPrevented = false
|
||||
|
||||
onNavigate({
|
||||
preventDefault: () => {
|
||||
isDefaultPrevented = true
|
||||
},
|
||||
})
|
||||
|
||||
if (isDefaultPrevented) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If the router is an NextRouter instance it will have `beforePopState`
|
||||
const routerScroll = scroll ?? true
|
||||
if ('beforePopState' in router) {
|
||||
router[replace ? 'replace' : 'push'](href, as, {
|
||||
shallow,
|
||||
locale,
|
||||
scroll: routerScroll,
|
||||
})
|
||||
} else {
|
||||
router[replace ? 'replace' : 'push'](as || href, {
|
||||
scroll: routerScroll,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
navigate()
|
||||
}
|
||||
|
||||
type LinkPropsReal = React.PropsWithChildren<
|
||||
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps> &
|
||||
LinkProps
|
||||
>
|
||||
|
||||
function formatStringOrUrl(urlObjOrString: UrlObject | string): string {
|
||||
if (typeof urlObjOrString === 'string') {
|
||||
return urlObjOrString
|
||||
}
|
||||
|
||||
return formatUrl(urlObjOrString)
|
||||
}
|
||||
|
||||
/**
|
||||
* A React component that extends the HTML `<a>` element to provide [prefetching](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching)
|
||||
* and client-side navigation between routes.
|
||||
*
|
||||
* It is the primary way to navigate between routes in Next.js.
|
||||
*
|
||||
* Read more: [Next.js docs: `<Link>`](https://nextjs.org/docs/app/api-reference/components/link)
|
||||
*/
|
||||
const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
|
||||
function LinkComponent(props, forwardedRef) {
|
||||
let children: React.ReactNode
|
||||
|
||||
const {
|
||||
href: hrefProp,
|
||||
as: asProp,
|
||||
children: childrenProp,
|
||||
prefetch: prefetchProp = null,
|
||||
passHref,
|
||||
replace,
|
||||
shallow,
|
||||
scroll,
|
||||
locale,
|
||||
onClick,
|
||||
onNavigate,
|
||||
onMouseEnter: onMouseEnterProp,
|
||||
onTouchStart: onTouchStartProp,
|
||||
legacyBehavior = false,
|
||||
...restProps
|
||||
} = props
|
||||
|
||||
children = childrenProp
|
||||
|
||||
if (
|
||||
legacyBehavior &&
|
||||
(typeof children === 'string' || typeof children === 'number')
|
||||
) {
|
||||
children = <a>{children}</a>
|
||||
}
|
||||
|
||||
const router = React.useContext(RouterContext)
|
||||
|
||||
const prefetchEnabled = prefetchProp !== false
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
function createPropError(args: {
|
||||
key: string
|
||||
expected: string
|
||||
actual: string
|
||||
}) {
|
||||
return new Error(
|
||||
`Failed prop type: The prop \`${args.key}\` expects a ${args.expected} in \`<Link>\`, but got \`${args.actual}\` instead.` +
|
||||
(typeof window !== 'undefined'
|
||||
? // TODO: Remove this addendum if Owner Stacks are available
|
||||
"\nOpen your browser's console to view the Component stack trace."
|
||||
: '')
|
||||
)
|
||||
}
|
||||
|
||||
// TypeScript trick for type-guarding:
|
||||
const requiredPropsGuard: Record<LinkPropsRequired, true> = {
|
||||
href: true,
|
||||
} as const
|
||||
const requiredProps: LinkPropsRequired[] = Object.keys(
|
||||
requiredPropsGuard
|
||||
) as LinkPropsRequired[]
|
||||
requiredProps.forEach((key: LinkPropsRequired) => {
|
||||
if (key === 'href') {
|
||||
if (
|
||||
props[key] == null ||
|
||||
(typeof props[key] !== 'string' && typeof props[key] !== 'object')
|
||||
) {
|
||||
throw createPropError({
|
||||
key,
|
||||
expected: '`string` or `object`',
|
||||
actual: props[key] === null ? 'null' : typeof props[key],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// TypeScript trick for type-guarding:
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _: never = key
|
||||
}
|
||||
})
|
||||
|
||||
// TypeScript trick for type-guarding:
|
||||
const optionalPropsGuard: Record<LinkPropsOptional, true> = {
|
||||
as: true,
|
||||
replace: true,
|
||||
scroll: true,
|
||||
shallow: true,
|
||||
passHref: true,
|
||||
prefetch: true,
|
||||
locale: true,
|
||||
onClick: true,
|
||||
onMouseEnter: true,
|
||||
onTouchStart: true,
|
||||
legacyBehavior: true,
|
||||
onNavigate: true,
|
||||
} as const
|
||||
const optionalProps: LinkPropsOptional[] = Object.keys(
|
||||
optionalPropsGuard
|
||||
) as LinkPropsOptional[]
|
||||
optionalProps.forEach((key: LinkPropsOptional) => {
|
||||
const valType = typeof props[key]
|
||||
|
||||
if (key === 'as') {
|
||||
if (props[key] && valType !== 'string' && valType !== 'object') {
|
||||
throw createPropError({
|
||||
key,
|
||||
expected: '`string` or `object`',
|
||||
actual: valType,
|
||||
})
|
||||
}
|
||||
} else if (key === 'locale') {
|
||||
if (props[key] && valType !== 'string') {
|
||||
throw createPropError({
|
||||
key,
|
||||
expected: '`string`',
|
||||
actual: valType,
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
key === 'onClick' ||
|
||||
key === 'onMouseEnter' ||
|
||||
key === 'onTouchStart' ||
|
||||
key === 'onNavigate'
|
||||
) {
|
||||
if (props[key] && valType !== 'function') {
|
||||
throw createPropError({
|
||||
key,
|
||||
expected: '`function`',
|
||||
actual: valType,
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
key === 'replace' ||
|
||||
key === 'scroll' ||
|
||||
key === 'shallow' ||
|
||||
key === 'passHref' ||
|
||||
key === 'legacyBehavior'
|
||||
) {
|
||||
if (props[key] != null && valType !== 'boolean') {
|
||||
throw createPropError({
|
||||
key,
|
||||
expected: '`boolean`',
|
||||
actual: valType,
|
||||
})
|
||||
}
|
||||
} else if (key === 'prefetch') {
|
||||
if (
|
||||
props[key] != null &&
|
||||
valType !== 'boolean' &&
|
||||
props[key] !== 'auto'
|
||||
) {
|
||||
throw createPropError({
|
||||
key,
|
||||
expected: '`boolean | "auto"`',
|
||||
actual: valType,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// TypeScript trick for type-guarding:
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _: never = key
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const { href, as } = React.useMemo(() => {
|
||||
if (!router) {
|
||||
const resolvedHref = formatStringOrUrl(hrefProp)
|
||||
return {
|
||||
href: resolvedHref,
|
||||
as: asProp ? formatStringOrUrl(asProp) : resolvedHref,
|
||||
}
|
||||
}
|
||||
|
||||
const [resolvedHref, resolvedAs] = resolveHref(router, hrefProp, true)
|
||||
|
||||
return {
|
||||
href: resolvedHref,
|
||||
as: asProp ? resolveHref(router, asProp) : resolvedAs || resolvedHref,
|
||||
}
|
||||
}, [router, hrefProp, asProp])
|
||||
|
||||
const previousHref = React.useRef<string>(href)
|
||||
const previousAs = React.useRef<string>(as)
|
||||
|
||||
// This will return the first child, if multiple are provided it will throw an error
|
||||
let child: any
|
||||
if (legacyBehavior) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (onClick) {
|
||||
console.warn(
|
||||
`"onClick" was passed to <Link> with \`href\` of \`${hrefProp}\` but "legacyBehavior" was set. The legacy behavior requires onClick be set on the child of next/link`
|
||||
)
|
||||
}
|
||||
if (onMouseEnterProp) {
|
||||
console.warn(
|
||||
`"onMouseEnter" was passed to <Link> with \`href\` of \`${hrefProp}\` but "legacyBehavior" was set. The legacy behavior requires onMouseEnter be set on the child of next/link`
|
||||
)
|
||||
}
|
||||
try {
|
||||
child = React.Children.only(children)
|
||||
} catch (err) {
|
||||
if (!children) {
|
||||
throw new Error(
|
||||
`No children were passed to <Link> with \`href\` of \`${hrefProp}\` but one child is required https://nextjs.org/docs/messages/link-no-children`
|
||||
)
|
||||
}
|
||||
throw new Error(
|
||||
`Multiple children were passed to <Link> with \`href\` of \`${hrefProp}\` but only one child is supported https://nextjs.org/docs/messages/link-multiple-children` +
|
||||
(typeof window !== 'undefined'
|
||||
? " \nOpen your browser's console to view the Component stack trace."
|
||||
: '')
|
||||
)
|
||||
}
|
||||
} else {
|
||||
child = React.Children.only(children)
|
||||
}
|
||||
} else {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if ((children as any)?.type === 'a') {
|
||||
throw new Error(
|
||||
'Invalid <Link> with <a> child. Please remove <a> or use <Link legacyBehavior>.\nLearn more: https://nextjs.org/docs/messages/invalid-new-link-with-extra-anchor'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const childRef: any = legacyBehavior
|
||||
? child && typeof child === 'object' && child.ref
|
||||
: forwardedRef
|
||||
|
||||
const [setIntersectionRef, isVisible, resetVisible] = useIntersection({
|
||||
rootMargin: '200px',
|
||||
})
|
||||
|
||||
const setIntersectionWithResetRef = React.useCallback(
|
||||
(el: Element | null) => {
|
||||
// Before the link getting observed, check if visible state need to be reset
|
||||
if (previousAs.current !== as || previousHref.current !== href) {
|
||||
resetVisible()
|
||||
previousAs.current = as
|
||||
previousHref.current = href
|
||||
}
|
||||
|
||||
setIntersectionRef(el)
|
||||
},
|
||||
[as, href, resetVisible, setIntersectionRef]
|
||||
)
|
||||
|
||||
const setRef = useMergedRef(setIntersectionWithResetRef, childRef)
|
||||
|
||||
// Prefetch the URL if we haven't already and it's visible.
|
||||
React.useEffect(() => {
|
||||
// in dev, we only prefetch on hover to avoid wasting resources as the prefetch will trigger compiling the page.
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!router) {
|
||||
return
|
||||
}
|
||||
|
||||
// If we don't need to prefetch the URL, don't do prefetch.
|
||||
if (!isVisible || !prefetchEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
// Prefetch the URL.
|
||||
prefetch(router, href, as, { locale })
|
||||
}, [as, href, isVisible, locale, prefetchEnabled, router?.locale, router])
|
||||
|
||||
const childProps: {
|
||||
onTouchStart?: React.TouchEventHandler<HTMLAnchorElement>
|
||||
onMouseEnter: React.MouseEventHandler<HTMLAnchorElement>
|
||||
onClick: React.MouseEventHandler<HTMLAnchorElement>
|
||||
href?: string
|
||||
ref?: any
|
||||
} = {
|
||||
ref: setRef,
|
||||
onClick(e) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!e) {
|
||||
throw new Error(
|
||||
`Component rendered inside next/link has to pass click event to "onClick" prop.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!legacyBehavior && typeof onClick === 'function') {
|
||||
onClick(e)
|
||||
}
|
||||
|
||||
if (
|
||||
legacyBehavior &&
|
||||
child.props &&
|
||||
typeof child.props.onClick === 'function'
|
||||
) {
|
||||
child.props.onClick(e)
|
||||
}
|
||||
|
||||
if (!router) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
linkClicked(
|
||||
e,
|
||||
router,
|
||||
href,
|
||||
as,
|
||||
replace,
|
||||
shallow,
|
||||
scroll,
|
||||
locale,
|
||||
onNavigate
|
||||
)
|
||||
},
|
||||
onMouseEnter(e) {
|
||||
if (!legacyBehavior && typeof onMouseEnterProp === 'function') {
|
||||
onMouseEnterProp(e)
|
||||
}
|
||||
|
||||
if (
|
||||
legacyBehavior &&
|
||||
child.props &&
|
||||
typeof child.props.onMouseEnter === 'function'
|
||||
) {
|
||||
child.props.onMouseEnter(e)
|
||||
}
|
||||
|
||||
if (!router) {
|
||||
return
|
||||
}
|
||||
|
||||
prefetch(router, href, as, {
|
||||
locale,
|
||||
priority: true,
|
||||
// @see {https://github.com/vercel/next.js/discussions/40268?sort=top#discussioncomment-3572642}
|
||||
bypassPrefetchedCheck: true,
|
||||
})
|
||||
},
|
||||
onTouchStart: process.env.__NEXT_LINK_NO_TOUCH_START
|
||||
? undefined
|
||||
: function onTouchStart(e) {
|
||||
if (!legacyBehavior && typeof onTouchStartProp === 'function') {
|
||||
onTouchStartProp(e)
|
||||
}
|
||||
|
||||
if (
|
||||
legacyBehavior &&
|
||||
child.props &&
|
||||
typeof child.props.onTouchStart === 'function'
|
||||
) {
|
||||
child.props.onTouchStart(e)
|
||||
}
|
||||
|
||||
if (!router) {
|
||||
return
|
||||
}
|
||||
|
||||
prefetch(router, href, as, {
|
||||
locale,
|
||||
priority: true,
|
||||
// @see {https://github.com/vercel/next.js/discussions/40268?sort=top#discussioncomment-3572642}
|
||||
bypassPrefetchedCheck: true,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// If child is an <a> tag and doesn't have a href attribute, or if the 'passHref' property is
|
||||
// defined, we specify the current 'href', so that repetition is not needed by the user.
|
||||
// If the url is absolute, we can bypass the logic to prepend the domain and locale.
|
||||
if (isAbsoluteUrl(as)) {
|
||||
childProps.href = as
|
||||
} else if (
|
||||
!legacyBehavior ||
|
||||
passHref ||
|
||||
(child.type === 'a' && !('href' in child.props))
|
||||
) {
|
||||
const curLocale = typeof locale !== 'undefined' ? locale : router?.locale
|
||||
|
||||
// we only render domain locales if we are currently on a domain locale
|
||||
// so that locale links are still visitable in development/preview envs
|
||||
const localeDomain =
|
||||
router?.isLocaleDomain &&
|
||||
getDomainLocale(as, curLocale, router?.locales, router?.domainLocales)
|
||||
|
||||
childProps.href =
|
||||
localeDomain ||
|
||||
addBasePath(addLocale(as, curLocale, router?.defaultLocale))
|
||||
}
|
||||
|
||||
if (legacyBehavior) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
errorOnce(
|
||||
'`legacyBehavior` is deprecated and will be removed in a future ' +
|
||||
'release. A codemod is available to upgrade your components:\n\n' +
|
||||
'npx @next/codemod@latest new-link .\n\n' +
|
||||
'Learn more: https://nextjs.org/docs/app/building-your-application/upgrading/codemods#remove-a-tags-from-link-components'
|
||||
)
|
||||
}
|
||||
return React.cloneElement(child, childProps)
|
||||
}
|
||||
|
||||
return (
|
||||
<a {...restProps} {...childProps}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const LinkStatusContext = createContext<{
|
||||
pending: boolean
|
||||
}>({
|
||||
// We do not support link status in the Pages Router, so we always return false
|
||||
pending: false,
|
||||
})
|
||||
|
||||
export const useLinkStatus = () => {
|
||||
// This behaviour is like React's useFormStatus. When the component is not under
|
||||
// a <form> tag, it will get the default value, instead of throwing an error.
|
||||
return useContext(LinkStatusContext)
|
||||
}
|
||||
|
||||
export default Link
|
||||
23
anyclip/client/next.ts
Normal file
23
anyclip/client/next.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import './webpack'
|
||||
import '../lib/require-instrumentation-client'
|
||||
|
||||
import { initialize, hydrate, version, router, emitter } from './'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
next: any
|
||||
}
|
||||
}
|
||||
|
||||
window.next = {
|
||||
version,
|
||||
// router is initialized later so it has to be live-binded
|
||||
get router() {
|
||||
return router
|
||||
},
|
||||
emitter,
|
||||
}
|
||||
|
||||
initialize({})
|
||||
.then(() => hydrate())
|
||||
.catch(console.error)
|
||||
25
anyclip/client/normalize-trailing-slash.ts
Normal file
25
anyclip/client/normalize-trailing-slash.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash'
|
||||
import { parsePath } from '../shared/lib/router/utils/parse-path'
|
||||
|
||||
/**
|
||||
* Normalizes the trailing slash of a path according to the `trailingSlash` option
|
||||
* in `next.config.js`.
|
||||
*/
|
||||
export const normalizePathTrailingSlash = (path: string) => {
|
||||
if (!path.startsWith('/') || process.env.__NEXT_MANUAL_TRAILING_SLASH) {
|
||||
return path
|
||||
}
|
||||
|
||||
const { pathname, query, hash } = parsePath(path)
|
||||
if (process.env.__NEXT_TRAILING_SLASH) {
|
||||
if (/\.[^/]+\/?$/.test(pathname)) {
|
||||
return `${removeTrailingSlash(pathname)}${query}${hash}`
|
||||
} else if (pathname.endsWith('/')) {
|
||||
return `${pathname}${query}${hash}`
|
||||
} else {
|
||||
return `${pathname}/${query}${hash}`
|
||||
}
|
||||
}
|
||||
|
||||
return `${removeTrailingSlash(pathname)}${query}${hash}`
|
||||
}
|
||||
210
anyclip/client/page-loader.ts
Normal file
210
anyclip/client/page-loader.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import type { RouteLoader } from './route-loader'
|
||||
import type { MiddlewareMatcher } from '../build/analysis/get-page-static-info'
|
||||
import { addBasePath } from './add-base-path'
|
||||
import { interpolateAs } from '../shared/lib/router/utils/interpolate-as'
|
||||
import getAssetPathFromRoute from '../shared/lib/router/utils/get-asset-path-from-route'
|
||||
import { addLocale } from './add-locale'
|
||||
import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
|
||||
import { parseRelativeUrl } from '../shared/lib/router/utils/parse-relative-url'
|
||||
import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash'
|
||||
import { createRouteLoader, getClientBuildManifest } from './route-loader'
|
||||
import {
|
||||
DEV_CLIENT_PAGES_MANIFEST,
|
||||
DEV_CLIENT_MIDDLEWARE_MANIFEST,
|
||||
TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST,
|
||||
} from '../shared/lib/constants'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__DEV_MIDDLEWARE_MATCHERS?: MiddlewareMatcher[]
|
||||
__DEV_PAGES_MANIFEST?: { pages: string[] }
|
||||
__SSG_MANIFEST_CB?: () => void
|
||||
__SSG_MANIFEST?: Set<string>
|
||||
}
|
||||
}
|
||||
|
||||
export type StyleSheetTuple = { href: string; text: string }
|
||||
export type GoodPageCache = {
|
||||
page: ComponentType
|
||||
mod: any
|
||||
styleSheets: StyleSheetTuple[]
|
||||
}
|
||||
|
||||
export default class PageLoader {
|
||||
private buildId: string
|
||||
private assetPrefix: string
|
||||
private promisedSsgManifest: Promise<Set<string>>
|
||||
private promisedDevPagesManifest?: Promise<string[]>
|
||||
private promisedMiddlewareMatchers?: Promise<MiddlewareMatcher[]>
|
||||
|
||||
public routeLoader: RouteLoader
|
||||
|
||||
constructor(buildId: string, assetPrefix: string) {
|
||||
this.routeLoader = createRouteLoader(assetPrefix)
|
||||
|
||||
this.buildId = buildId
|
||||
this.assetPrefix = assetPrefix
|
||||
|
||||
this.promisedSsgManifest = new Promise((resolve) => {
|
||||
if (window.__SSG_MANIFEST) {
|
||||
resolve(window.__SSG_MANIFEST)
|
||||
} else {
|
||||
window.__SSG_MANIFEST_CB = () => {
|
||||
resolve(window.__SSG_MANIFEST!)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getPageList() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return getClientBuildManifest().then((manifest) => manifest.sortedPages)
|
||||
} else {
|
||||
if (window.__DEV_PAGES_MANIFEST) {
|
||||
return window.__DEV_PAGES_MANIFEST.pages
|
||||
} else {
|
||||
this.promisedDevPagesManifest ||= fetch(
|
||||
`${this.assetPrefix}/_next/static/development/${DEV_CLIENT_PAGES_MANIFEST}`,
|
||||
{ credentials: 'same-origin' }
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((manifest: { pages: string[] }) => {
|
||||
window.__DEV_PAGES_MANIFEST = manifest
|
||||
return manifest.pages
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(`Failed to fetch devPagesManifest:`, err)
|
||||
throw new Error(
|
||||
`Failed to fetch _devPagesManifest.json. Is something blocking that network request?\n` +
|
||||
'Read more: https://nextjs.org/docs/messages/failed-to-fetch-devpagesmanifest'
|
||||
)
|
||||
})
|
||||
return this.promisedDevPagesManifest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getMiddleware() {
|
||||
// Webpack production
|
||||
if (
|
||||
process.env.NODE_ENV === 'production' &&
|
||||
process.env.__NEXT_MIDDLEWARE_MATCHERS
|
||||
) {
|
||||
const middlewareMatchers = process.env.__NEXT_MIDDLEWARE_MATCHERS
|
||||
window.__MIDDLEWARE_MATCHERS = middlewareMatchers
|
||||
? (middlewareMatchers as any as MiddlewareMatcher[])
|
||||
: undefined
|
||||
return window.__MIDDLEWARE_MATCHERS
|
||||
// Turbopack production
|
||||
} else if (process.env.NODE_ENV === 'production') {
|
||||
if (window.__MIDDLEWARE_MATCHERS) {
|
||||
return window.__MIDDLEWARE_MATCHERS
|
||||
} else {
|
||||
if (!this.promisedMiddlewareMatchers) {
|
||||
// TODO: Decide what should happen when fetching fails instead of asserting
|
||||
// @ts-ignore
|
||||
this.promisedMiddlewareMatchers = fetch(
|
||||
`${this.assetPrefix}/_next/static/${this.buildId}/${TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST}`,
|
||||
{ credentials: 'same-origin' }
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((matchers: MiddlewareMatcher[]) => {
|
||||
window.__MIDDLEWARE_MATCHERS = matchers
|
||||
return matchers
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(`Failed to fetch _devMiddlewareManifest`, err)
|
||||
})
|
||||
}
|
||||
// TODO Remove this assertion as this could be undefined
|
||||
return this.promisedMiddlewareMatchers!
|
||||
}
|
||||
// Development both Turbopack and Webpack
|
||||
} else {
|
||||
if (window.__DEV_MIDDLEWARE_MATCHERS) {
|
||||
return window.__DEV_MIDDLEWARE_MATCHERS
|
||||
} else {
|
||||
if (!this.promisedMiddlewareMatchers) {
|
||||
// TODO: Decide what should happen when fetching fails instead of asserting
|
||||
// @ts-ignore
|
||||
this.promisedMiddlewareMatchers = fetch(
|
||||
`${this.assetPrefix}/_next/static/${this.buildId}/${DEV_CLIENT_MIDDLEWARE_MANIFEST}`,
|
||||
{ credentials: 'same-origin' }
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((matchers: MiddlewareMatcher[]) => {
|
||||
window.__DEV_MIDDLEWARE_MATCHERS = matchers
|
||||
return matchers
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(`Failed to fetch _devMiddlewareManifest`, err)
|
||||
})
|
||||
}
|
||||
// TODO Remove this assertion as this could be undefined
|
||||
return this.promisedMiddlewareMatchers!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDataHref(params: {
|
||||
asPath: string
|
||||
href: string
|
||||
locale?: string | false
|
||||
skipInterpolation?: boolean
|
||||
}): string {
|
||||
const { asPath, href, locale } = params
|
||||
const { pathname: hrefPathname, query, search } = parseRelativeUrl(href)
|
||||
const { pathname: asPathname } = parseRelativeUrl(asPath)
|
||||
const route = removeTrailingSlash(hrefPathname)
|
||||
if (route[0] !== '/') {
|
||||
throw new Error(`Route name should start with a "/", got "${route}"`)
|
||||
}
|
||||
|
||||
const getHrefForSlug = (path: string) => {
|
||||
const dataRoute = getAssetPathFromRoute(
|
||||
removeTrailingSlash(addLocale(path, locale)),
|
||||
'.json'
|
||||
)
|
||||
return addBasePath(
|
||||
`/_next/data/${this.buildId}${dataRoute}${search}`,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
return getHrefForSlug(
|
||||
params.skipInterpolation
|
||||
? asPathname
|
||||
: isDynamicRoute(route)
|
||||
? interpolateAs(hrefPathname, asPathname, query).result
|
||||
: route
|
||||
)
|
||||
}
|
||||
|
||||
_isSsg(
|
||||
/** the route (file-system path) */
|
||||
route: string
|
||||
): Promise<boolean> {
|
||||
return this.promisedSsgManifest.then((manifest) => manifest.has(route))
|
||||
}
|
||||
|
||||
loadPage(route: string): Promise<GoodPageCache> {
|
||||
return this.routeLoader.loadRoute(route).then((res) => {
|
||||
if ('component' in res) {
|
||||
return {
|
||||
page: res.component,
|
||||
mod: res.exports,
|
||||
styleSheets: res.styles.map((o) => ({
|
||||
href: o.href,
|
||||
text: o.content,
|
||||
})),
|
||||
}
|
||||
}
|
||||
throw res.error
|
||||
})
|
||||
}
|
||||
|
||||
prefetch(route: string): Promise<void> {
|
||||
return this.routeLoader.prefetch(route)
|
||||
}
|
||||
}
|
||||
18
anyclip/client/remove-base-path.ts
Normal file
18
anyclip/client/remove-base-path.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { hasBasePath } from './has-base-path'
|
||||
|
||||
const basePath = (process.env.__NEXT_ROUTER_BASEPATH as string) || ''
|
||||
|
||||
export function removeBasePath(path: string): string {
|
||||
if (process.env.__NEXT_MANUAL_CLIENT_BASE_PATH) {
|
||||
if (!hasBasePath(path)) {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// Can't trim the basePath if it has zero length!
|
||||
if (basePath.length === 0) return path
|
||||
|
||||
path = path.slice(basePath.length)
|
||||
if (!path.startsWith('/')) path = `/${path}`
|
||||
return path
|
||||
}
|
||||
18
anyclip/client/remove-locale.ts
Normal file
18
anyclip/client/remove-locale.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { parsePath } from '../shared/lib/router/utils/parse-path'
|
||||
|
||||
export function removeLocale(path: string, locale?: string) {
|
||||
if (process.env.__NEXT_I18N_SUPPORT) {
|
||||
const { pathname } = parsePath(path)
|
||||
const pathLower = pathname.toLowerCase()
|
||||
const localeLower = locale?.toLowerCase()
|
||||
|
||||
return locale &&
|
||||
(pathLower.startsWith(`/${localeLower}/`) ||
|
||||
pathLower === `/${localeLower}`)
|
||||
? `${pathname.length === locale.length + 1 ? `/` : ``}${path.slice(
|
||||
locale.length + 1
|
||||
)}`
|
||||
: path
|
||||
}
|
||||
return path
|
||||
}
|
||||
23
anyclip/client/request-idle-callback.ts
Normal file
23
anyclip/client/request-idle-callback.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const requestIdleCallback =
|
||||
(typeof self !== 'undefined' &&
|
||||
self.requestIdleCallback &&
|
||||
self.requestIdleCallback.bind(window)) ||
|
||||
function (cb: IdleRequestCallback): number {
|
||||
let start = Date.now()
|
||||
return self.setTimeout(function () {
|
||||
cb({
|
||||
didTimeout: false,
|
||||
timeRemaining: function () {
|
||||
return Math.max(0, 50 - (Date.now() - start))
|
||||
},
|
||||
})
|
||||
}, 1)
|
||||
}
|
||||
|
||||
export const cancelIdleCallback =
|
||||
(typeof self !== 'undefined' &&
|
||||
self.cancelIdleCallback &&
|
||||
self.cancelIdleCallback.bind(window)) ||
|
||||
function (id: number) {
|
||||
return clearTimeout(id)
|
||||
}
|
||||
138
anyclip/client/resolve-href.ts
Normal file
138
anyclip/client/resolve-href.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { NextRouter, Url } from '../shared/lib/router/router'
|
||||
|
||||
import { searchParamsToUrlQuery } from '../shared/lib/router/utils/querystring'
|
||||
import { formatWithValidation } from '../shared/lib/router/utils/format-url'
|
||||
import { omit } from '../shared/lib/router/utils/omit'
|
||||
import { normalizeRepeatedSlashes } from '../shared/lib/utils'
|
||||
import { normalizePathTrailingSlash } from './normalize-trailing-slash'
|
||||
import { isLocalURL } from '../shared/lib/router/utils/is-local-url'
|
||||
import { isDynamicRoute } from '../shared/lib/router/utils'
|
||||
import { interpolateAs } from '../shared/lib/router/utils/interpolate-as'
|
||||
import { getRouteRegex } from '../shared/lib/router/utils/route-regex'
|
||||
import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher'
|
||||
|
||||
/**
|
||||
* Resolves a given hyperlink with a certain router state (basePath not included).
|
||||
* Preserves absolute urls.
|
||||
*/
|
||||
export function resolveHref(
|
||||
router: NextRouter,
|
||||
href: Url,
|
||||
resolveAs: true
|
||||
): [string, string] | [string]
|
||||
export function resolveHref(
|
||||
router: NextRouter,
|
||||
href: Url,
|
||||
resolveAs?: false
|
||||
): string
|
||||
export function resolveHref(
|
||||
router: NextRouter,
|
||||
href: Url,
|
||||
resolveAs?: boolean
|
||||
): [string, string] | [string] | string {
|
||||
// we use a dummy base url for relative urls
|
||||
let base: URL
|
||||
let urlAsString = typeof href === 'string' ? href : formatWithValidation(href)
|
||||
|
||||
// repeated slashes and backslashes in the URL are considered
|
||||
// invalid and will never match a Next.js page/file
|
||||
// https://www.rfc-editor.org/rfc/rfc3986.html#section-3.1
|
||||
const urlProtoMatch = urlAsString.match(/^[a-z][a-z0-9+.-]*:\/\//i)
|
||||
const urlAsStringNoProto = urlProtoMatch
|
||||
? urlAsString.slice(urlProtoMatch[0].length)
|
||||
: urlAsString
|
||||
|
||||
const urlParts = urlAsStringNoProto.split('?', 1)
|
||||
|
||||
if ((urlParts[0] || '').match(/(\/\/|\\)/)) {
|
||||
console.error(
|
||||
`Invalid href '${urlAsString}' passed to next/router in page: '${router.pathname}'. Repeated forward-slashes (//) or backslashes \\ are not valid in the href.`
|
||||
)
|
||||
const normalizedUrl = normalizeRepeatedSlashes(urlAsStringNoProto)
|
||||
urlAsString = (urlProtoMatch ? urlProtoMatch[0] : '') + normalizedUrl
|
||||
}
|
||||
|
||||
// Return because it cannot be routed by the Next.js router
|
||||
if (!isLocalURL(urlAsString)) {
|
||||
return (resolveAs ? [urlAsString] : urlAsString) as string
|
||||
}
|
||||
|
||||
try {
|
||||
let baseBase = urlAsString.startsWith('#') ? router.asPath : router.pathname
|
||||
|
||||
// If the provided href is only a query string, it is safer to use the asPath
|
||||
// considering rewrites.
|
||||
if (urlAsString.startsWith('?')) {
|
||||
baseBase = router.asPath
|
||||
|
||||
// However, if is a dynamic route, we need to use the pathname to preserve the
|
||||
// query interpolation and rewrites (router.pathname will look like "/[slug]").
|
||||
if (isDynamicRoute(router.pathname)) {
|
||||
baseBase = router.pathname
|
||||
|
||||
const routeRegex = getRouteRegex(router.pathname)
|
||||
const match = getRouteMatcher(routeRegex)(router.asPath)
|
||||
|
||||
// For dynamic routes, if asPath doesn't match the pathname regex, it is a rewritten path.
|
||||
// In this case, should use asPath to preserve the current URL.
|
||||
if (!match) {
|
||||
baseBase = router.asPath
|
||||
}
|
||||
|
||||
// Note: There is an edge case where the pathname is dynamic, and also a rewrite path to the same segment.
|
||||
// E.g. in "/[slug]" path, rewrite "/foo" -> "/bar"
|
||||
|
||||
// In this case, it will be treated as a non-rewritten path and possibly interpolate the query string.
|
||||
// E.g., "/any?slug=foo" will become the content of "/foo", not rewritten as "/bar"
|
||||
|
||||
// This is currently a trade-off of not resolving rewrite paths on every Router/Link call,
|
||||
// but using a lighter route regex pattern check.
|
||||
}
|
||||
}
|
||||
|
||||
base = new URL(baseBase, 'http://n')
|
||||
} catch (_) {
|
||||
// fallback to / for invalid asPath values e.g. //
|
||||
base = new URL('/', 'http://n')
|
||||
}
|
||||
|
||||
try {
|
||||
const finalUrl = new URL(urlAsString, base)
|
||||
finalUrl.pathname = normalizePathTrailingSlash(finalUrl.pathname)
|
||||
let interpolatedAs = ''
|
||||
|
||||
if (
|
||||
isDynamicRoute(finalUrl.pathname) &&
|
||||
finalUrl.searchParams &&
|
||||
resolveAs
|
||||
) {
|
||||
const query = searchParamsToUrlQuery(finalUrl.searchParams)
|
||||
|
||||
const { result, params } = interpolateAs(
|
||||
finalUrl.pathname,
|
||||
finalUrl.pathname,
|
||||
query
|
||||
)
|
||||
|
||||
if (result) {
|
||||
interpolatedAs = formatWithValidation({
|
||||
pathname: result,
|
||||
hash: finalUrl.hash,
|
||||
query: omit(query, params),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// if the origin didn't change, it means we received a relative href
|
||||
const resolvedHref =
|
||||
finalUrl.origin === base.origin
|
||||
? finalUrl.href.slice(finalUrl.origin.length)
|
||||
: finalUrl.href
|
||||
|
||||
return resolveAs
|
||||
? [resolvedHref, interpolatedAs || resolvedHref]
|
||||
: resolvedHref
|
||||
} catch (_) {
|
||||
return resolveAs ? [urlAsString] : urlAsString
|
||||
}
|
||||
}
|
||||
65
anyclip/client/route-announcer.tsx
Normal file
65
anyclip/client/route-announcer.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
import { useRouter } from './router'
|
||||
|
||||
const nextjsRouteAnnouncerStyles: React.CSSProperties = {
|
||||
border: 0,
|
||||
clip: 'rect(0 0 0 0)',
|
||||
height: '1px',
|
||||
margin: '-1px',
|
||||
overflow: 'hidden',
|
||||
padding: 0,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: '1px',
|
||||
|
||||
// https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe
|
||||
whiteSpace: 'nowrap',
|
||||
wordWrap: 'normal',
|
||||
}
|
||||
|
||||
export const RouteAnnouncer = () => {
|
||||
const { asPath } = useRouter()
|
||||
const [routeAnnouncement, setRouteAnnouncement] = React.useState('')
|
||||
|
||||
// Only announce the path change, but not for the first load because screen
|
||||
// reader will do that automatically.
|
||||
const previouslyLoadedPath = React.useRef(asPath)
|
||||
|
||||
// Every time the path changes, announce the new page’s title following this
|
||||
// priority: first the document title (from head), otherwise the first h1, or
|
||||
// if none of these exist, then the pathname from the URL. This methodology is
|
||||
// inspired by Marcy Sutton’s accessible client routing user testing. More
|
||||
// information can be found here:
|
||||
// https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/
|
||||
React.useEffect(
|
||||
() => {
|
||||
// If the path hasn't change, we do nothing.
|
||||
if (previouslyLoadedPath.current === asPath) return
|
||||
previouslyLoadedPath.current = asPath
|
||||
|
||||
if (document.title) {
|
||||
setRouteAnnouncement(document.title)
|
||||
} else {
|
||||
const pageHeader = document.querySelector('h1')
|
||||
const content = pageHeader?.innerText ?? pageHeader?.textContent
|
||||
|
||||
setRouteAnnouncement(content || asPath)
|
||||
}
|
||||
},
|
||||
// TODO: switch to pathname + query object of dynamic route requirements
|
||||
[asPath]
|
||||
)
|
||||
|
||||
return (
|
||||
<p
|
||||
aria-live="assertive" // Make the announcement immediately.
|
||||
id="__next-route-announcer__"
|
||||
role="alert"
|
||||
style={nextjsRouteAnnouncerStyles}
|
||||
>
|
||||
{routeAnnouncement}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export default RouteAnnouncer
|
||||
453
anyclip/client/route-loader.ts
Normal file
453
anyclip/client/route-loader.ts
Normal file
@@ -0,0 +1,453 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import type { MiddlewareMatcher } from '../build/analysis/get-page-static-info'
|
||||
import getAssetPathFromRoute from '../shared/lib/router/utils/get-asset-path-from-route'
|
||||
import { __unsafeCreateTrustedScriptURL } from './trusted-types'
|
||||
import { requestIdleCallback } from './request-idle-callback'
|
||||
import { getDeploymentIdQueryOrEmptyString } from '../build/deployment-id'
|
||||
import { encodeURIPath } from '../shared/lib/encode-uri-path'
|
||||
|
||||
// 3.8s was arbitrarily chosen as it's what https://web.dev/interactive
|
||||
// considers as "Good" time-to-interactive. We must assume something went
|
||||
// wrong beyond this point, and then fall-back to a full page transition to
|
||||
// show the user something of value.
|
||||
const MS_MAX_IDLE_DELAY = 3800
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__BUILD_MANIFEST?: Record<string, string[]>
|
||||
__BUILD_MANIFEST_CB?: Function
|
||||
__MIDDLEWARE_MATCHERS?: MiddlewareMatcher[]
|
||||
__MIDDLEWARE_MANIFEST_CB?: Function
|
||||
__REACT_LOADABLE_MANIFEST?: any
|
||||
__DYNAMIC_CSS_MANIFEST?: any
|
||||
__RSC_MANIFEST?: any
|
||||
__RSC_SERVER_MANIFEST?: any
|
||||
__NEXT_FONT_MANIFEST?: any
|
||||
__SUBRESOURCE_INTEGRITY_MANIFEST?: string
|
||||
__INTERCEPTION_ROUTE_REWRITE_MANIFEST?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface LoadedEntrypointSuccess {
|
||||
component: ComponentType
|
||||
exports: any
|
||||
}
|
||||
interface LoadedEntrypointFailure {
|
||||
error: unknown
|
||||
}
|
||||
type RouteEntrypoint = LoadedEntrypointSuccess | LoadedEntrypointFailure
|
||||
|
||||
interface RouteStyleSheet {
|
||||
href: string
|
||||
content: string
|
||||
}
|
||||
|
||||
interface LoadedRouteSuccess extends LoadedEntrypointSuccess {
|
||||
styles: RouteStyleSheet[]
|
||||
}
|
||||
interface LoadedRouteFailure {
|
||||
error: unknown
|
||||
}
|
||||
type RouteLoaderEntry = LoadedRouteSuccess | LoadedRouteFailure
|
||||
|
||||
interface Future<V> {
|
||||
resolve: (entrypoint: V) => void
|
||||
future: Promise<V>
|
||||
}
|
||||
function withFuture<T extends object>(
|
||||
key: string,
|
||||
map: Map<string, Future<T> | T>,
|
||||
generator?: () => Promise<T>
|
||||
): Promise<T> {
|
||||
let entry = map.get(key)
|
||||
if (entry) {
|
||||
if ('future' in entry) {
|
||||
return entry.future
|
||||
}
|
||||
return Promise.resolve(entry)
|
||||
}
|
||||
let resolver: (entrypoint: T) => void
|
||||
const prom: Promise<T> = new Promise<T>((resolve) => {
|
||||
resolver = resolve
|
||||
})
|
||||
map.set(key, { resolve: resolver!, future: prom })
|
||||
return generator
|
||||
? generator()
|
||||
.then((value) => {
|
||||
resolver(value)
|
||||
return value
|
||||
})
|
||||
.catch((err) => {
|
||||
map.delete(key)
|
||||
throw err
|
||||
})
|
||||
: prom
|
||||
}
|
||||
|
||||
export interface RouteLoader {
|
||||
whenEntrypoint(route: string): Promise<RouteEntrypoint>
|
||||
onEntrypoint(route: string, execute: () => unknown): void
|
||||
loadRoute(route: string, prefetch?: boolean): Promise<RouteLoaderEntry>
|
||||
prefetch(route: string): Promise<void>
|
||||
}
|
||||
|
||||
const ASSET_LOAD_ERROR = Symbol('ASSET_LOAD_ERROR')
|
||||
// TODO: unexport
|
||||
export function markAssetError(err: Error): Error {
|
||||
return Object.defineProperty(err, ASSET_LOAD_ERROR, {})
|
||||
}
|
||||
|
||||
export function isAssetError(err?: Error): boolean | undefined {
|
||||
return err && ASSET_LOAD_ERROR in err
|
||||
}
|
||||
|
||||
function hasPrefetch(link?: HTMLLinkElement): boolean {
|
||||
try {
|
||||
link = document.createElement('link')
|
||||
return (
|
||||
// detect IE11 since it supports prefetch but isn't detected
|
||||
// with relList.support
|
||||
(!!window.MSInputMethodContext && !!(document as any).documentMode) ||
|
||||
link.relList.supports('prefetch')
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const canPrefetch: boolean = hasPrefetch()
|
||||
|
||||
const getAssetQueryString = () => {
|
||||
return getDeploymentIdQueryOrEmptyString()
|
||||
}
|
||||
|
||||
function prefetchViaDom(
|
||||
href: string,
|
||||
as: string,
|
||||
link?: HTMLLinkElement
|
||||
): Promise<any> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const selector = `
|
||||
link[rel="prefetch"][href^="${href}"],
|
||||
link[rel="preload"][href^="${href}"],
|
||||
script[src^="${href}"]`
|
||||
if (document.querySelector(selector)) {
|
||||
return resolve()
|
||||
}
|
||||
|
||||
link = document.createElement('link')
|
||||
|
||||
// The order of property assignment here is intentional:
|
||||
if (as) link!.as = as
|
||||
link!.rel = `prefetch`
|
||||
link!.crossOrigin = process.env.__NEXT_CROSS_ORIGIN!
|
||||
link!.onload = resolve as any
|
||||
link!.onerror = () =>
|
||||
reject(markAssetError(new Error(`Failed to prefetch: ${href}`)))
|
||||
|
||||
// `href` should always be last:
|
||||
link!.href = href
|
||||
|
||||
document.head.appendChild(link)
|
||||
})
|
||||
}
|
||||
|
||||
function appendScript(
|
||||
src: TrustedScriptURL | string,
|
||||
script?: HTMLScriptElement
|
||||
): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
script = document.createElement('script')
|
||||
|
||||
// The order of property assignment here is intentional.
|
||||
// 1. Setup success/failure hooks in case the browser synchronously
|
||||
// executes when `src` is set.
|
||||
script.onload = resolve
|
||||
script.onerror = () =>
|
||||
reject(markAssetError(new Error(`Failed to load script: ${src}`)))
|
||||
|
||||
// 2. Configure the cross-origin attribute before setting `src` in case the
|
||||
// browser begins to fetch.
|
||||
script.crossOrigin = process.env.__NEXT_CROSS_ORIGIN!
|
||||
|
||||
// 3. Finally, set the source and inject into the DOM in case the child
|
||||
// must be appended for fetching to start.
|
||||
script.src = src as string
|
||||
document.body.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
// We wait for pages to be built in dev before we start the route transition
|
||||
// timeout to prevent an un-necessary hard navigation in development.
|
||||
let devBuildPromise: Promise<void> | undefined
|
||||
|
||||
// Resolve a promise that times out after given amount of milliseconds.
|
||||
function resolvePromiseWithTimeout<T>(
|
||||
p: Promise<T>,
|
||||
ms: number,
|
||||
err: Error
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let cancelled = false
|
||||
|
||||
p.then((r) => {
|
||||
// Resolved, cancel the timeout
|
||||
cancelled = true
|
||||
resolve(r)
|
||||
}).catch(reject)
|
||||
|
||||
// We wrap these checks separately for better dead-code elimination in
|
||||
// production bundles.
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
;(devBuildPromise || Promise.resolve()).then(() => {
|
||||
requestIdleCallback(() =>
|
||||
setTimeout(() => {
|
||||
if (!cancelled) {
|
||||
reject(err)
|
||||
}
|
||||
}, ms)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
requestIdleCallback(() =>
|
||||
setTimeout(() => {
|
||||
if (!cancelled) {
|
||||
reject(err)
|
||||
}
|
||||
}, ms)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: stop exporting or cache the failure
|
||||
// It'd be best to stop exporting this. It's an implementation detail. We're
|
||||
// only exporting it for backwards compatibility with the `page-loader`.
|
||||
// Only cache this response as a last resort if we cannot eliminate all other
|
||||
// code branches that use the Build Manifest Callback and push them through
|
||||
// the Route Loader interface.
|
||||
export function getClientBuildManifest() {
|
||||
if (self.__BUILD_MANIFEST) {
|
||||
return Promise.resolve(self.__BUILD_MANIFEST)
|
||||
}
|
||||
|
||||
const onBuildManifest = new Promise<Record<string, string[]>>((resolve) => {
|
||||
// Mandatory because this is not concurrent safe:
|
||||
const cb = self.__BUILD_MANIFEST_CB
|
||||
self.__BUILD_MANIFEST_CB = () => {
|
||||
resolve(self.__BUILD_MANIFEST!)
|
||||
cb && cb()
|
||||
}
|
||||
})
|
||||
|
||||
return resolvePromiseWithTimeout(
|
||||
onBuildManifest,
|
||||
MS_MAX_IDLE_DELAY,
|
||||
markAssetError(new Error('Failed to load client build manifest'))
|
||||
)
|
||||
}
|
||||
|
||||
interface RouteFiles {
|
||||
scripts: (TrustedScriptURL | string)[]
|
||||
css: string[]
|
||||
}
|
||||
function getFilesForRoute(
|
||||
assetPrefix: string,
|
||||
route: string
|
||||
): Promise<RouteFiles> {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const scriptUrl =
|
||||
assetPrefix +
|
||||
'/_next/static/chunks/pages' +
|
||||
encodeURIPath(getAssetPathFromRoute(route, '.js')) +
|
||||
getAssetQueryString()
|
||||
return Promise.resolve({
|
||||
scripts: [__unsafeCreateTrustedScriptURL(scriptUrl)],
|
||||
// Styles are handled by `style-loader` in development:
|
||||
css: [],
|
||||
})
|
||||
}
|
||||
return getClientBuildManifest().then((manifest) => {
|
||||
if (!(route in manifest)) {
|
||||
throw markAssetError(new Error(`Failed to lookup route: ${route}`))
|
||||
}
|
||||
const allFiles = manifest[route].map(
|
||||
(entry) => assetPrefix + '/_next/' + encodeURIPath(entry)
|
||||
)
|
||||
return {
|
||||
scripts: allFiles
|
||||
.filter((v) => v.endsWith('.js'))
|
||||
.map((v) => __unsafeCreateTrustedScriptURL(v) + getAssetQueryString()),
|
||||
css: allFiles
|
||||
.filter((v) => v.endsWith('.css'))
|
||||
.map((v) => v + getAssetQueryString()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function createRouteLoader(assetPrefix: string): RouteLoader {
|
||||
const entrypoints: Map<string, Future<RouteEntrypoint> | RouteEntrypoint> =
|
||||
new Map()
|
||||
const loadedScripts: Map<string, Promise<unknown>> = new Map()
|
||||
const styleSheets: Map<string, Promise<RouteStyleSheet>> = new Map()
|
||||
const routes: Map<string, Future<RouteLoaderEntry> | RouteLoaderEntry> =
|
||||
new Map()
|
||||
|
||||
function maybeExecuteScript(
|
||||
src: TrustedScriptURL | string
|
||||
): Promise<unknown> {
|
||||
// With HMR we might need to "reload" scripts when they are
|
||||
// disposed and readded. Executing scripts twice has no functional
|
||||
// differences
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
let prom: Promise<unknown> | undefined = loadedScripts.get(src.toString())
|
||||
if (prom) {
|
||||
return prom
|
||||
}
|
||||
|
||||
// Skip executing script if it's already in the DOM:
|
||||
if (document.querySelector(`script[src^="${src}"]`)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
loadedScripts.set(src.toString(), (prom = appendScript(src)))
|
||||
return prom
|
||||
} else {
|
||||
return appendScript(src)
|
||||
}
|
||||
}
|
||||
|
||||
function fetchStyleSheet(href: string): Promise<RouteStyleSheet> {
|
||||
let prom: Promise<RouteStyleSheet> | undefined = styleSheets.get(href)
|
||||
if (prom) {
|
||||
return prom
|
||||
}
|
||||
|
||||
styleSheets.set(
|
||||
href,
|
||||
(prom = fetch(href, { credentials: 'same-origin' })
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load stylesheet: ${href}`)
|
||||
}
|
||||
return res.text().then((text) => ({ href: href, content: text }))
|
||||
})
|
||||
.catch((err) => {
|
||||
throw markAssetError(err)
|
||||
}))
|
||||
)
|
||||
return prom
|
||||
}
|
||||
|
||||
return {
|
||||
whenEntrypoint(route: string) {
|
||||
return withFuture(route, entrypoints)
|
||||
},
|
||||
onEntrypoint(route: string, execute: undefined | (() => unknown)) {
|
||||
;(execute
|
||||
? Promise.resolve()
|
||||
.then(() => execute())
|
||||
.then(
|
||||
(exports: any) => ({
|
||||
component: (exports && exports.default) || exports,
|
||||
exports: exports,
|
||||
}),
|
||||
(err) => ({ error: err })
|
||||
)
|
||||
: Promise.resolve(undefined)
|
||||
).then((input: RouteEntrypoint | undefined) => {
|
||||
const old = entrypoints.get(route)
|
||||
if (old && 'resolve' in old) {
|
||||
if (input) {
|
||||
entrypoints.set(route, input)
|
||||
old.resolve(input)
|
||||
}
|
||||
} else {
|
||||
if (input) {
|
||||
entrypoints.set(route, input)
|
||||
} else {
|
||||
entrypoints.delete(route)
|
||||
}
|
||||
// when this entrypoint has been resolved before
|
||||
// the route is outdated and we want to invalidate
|
||||
// this cache entry
|
||||
routes.delete(route)
|
||||
}
|
||||
})
|
||||
},
|
||||
loadRoute(route: string, prefetch?: boolean) {
|
||||
return withFuture<RouteLoaderEntry>(route, routes, () => {
|
||||
let devBuildPromiseResolve: () => void
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
devBuildPromise = new Promise<void>((resolve) => {
|
||||
devBuildPromiseResolve = resolve
|
||||
})
|
||||
}
|
||||
|
||||
return resolvePromiseWithTimeout(
|
||||
getFilesForRoute(assetPrefix, route)
|
||||
.then(({ scripts, css }) => {
|
||||
return Promise.all([
|
||||
entrypoints.has(route)
|
||||
? []
|
||||
: Promise.all(scripts.map(maybeExecuteScript)),
|
||||
Promise.all(css.map(fetchStyleSheet)),
|
||||
] as const)
|
||||
})
|
||||
.then((res) => {
|
||||
return this.whenEntrypoint(route).then((entrypoint) => ({
|
||||
entrypoint,
|
||||
styles: res[1],
|
||||
}))
|
||||
}),
|
||||
MS_MAX_IDLE_DELAY,
|
||||
markAssetError(new Error(`Route did not complete loading: ${route}`))
|
||||
)
|
||||
.then(({ entrypoint, styles }) => {
|
||||
const res: RouteLoaderEntry = Object.assign<
|
||||
{ styles: RouteStyleSheet[] },
|
||||
RouteEntrypoint
|
||||
>({ styles: styles! }, entrypoint)
|
||||
return 'error' in entrypoint ? entrypoint : res
|
||||
})
|
||||
.catch((err) => {
|
||||
if (prefetch) {
|
||||
// we don't want to cache errors during prefetch
|
||||
throw err
|
||||
}
|
||||
return { error: err }
|
||||
})
|
||||
.finally(() => devBuildPromiseResolve?.())
|
||||
})
|
||||
},
|
||||
prefetch(route: string): Promise<void> {
|
||||
// https://github.com/GoogleChromeLabs/quicklink/blob/453a661fa1fa940e2d2e044452398e38c67a98fb/src/index.mjs#L115-L118
|
||||
// License: Apache 2.0
|
||||
let cn
|
||||
if ((cn = (navigator as any).connection)) {
|
||||
// Don't prefetch if using 2G or if Save-Data is enabled.
|
||||
if (cn.saveData || /2g/.test(cn.effectiveType)) return Promise.resolve()
|
||||
}
|
||||
return getFilesForRoute(assetPrefix, route)
|
||||
.then((output) =>
|
||||
Promise.all(
|
||||
canPrefetch
|
||||
? output.scripts.map((script) =>
|
||||
prefetchViaDom(script.toString(), 'script')
|
||||
)
|
||||
: []
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
requestIdleCallback(() => this.loadRoute(route, true).catch(() => {}))
|
||||
})
|
||||
.catch(
|
||||
// swallow prefetch errors
|
||||
() => {}
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
195
anyclip/client/router.ts
Normal file
195
anyclip/client/router.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/* global window */
|
||||
import React from 'react'
|
||||
import Router from '../shared/lib/router/router'
|
||||
import type { NextRouter } from '../shared/lib/router/router'
|
||||
import { RouterContext } from '../shared/lib/router-context.shared-runtime'
|
||||
import isError from '../lib/is-error'
|
||||
|
||||
type SingletonRouterBase = {
|
||||
router: Router | null
|
||||
readyCallbacks: Array<() => any>
|
||||
ready(cb: () => any): void
|
||||
}
|
||||
|
||||
export { Router }
|
||||
|
||||
export type { NextRouter }
|
||||
|
||||
export type SingletonRouter = SingletonRouterBase & NextRouter
|
||||
|
||||
const singletonRouter: SingletonRouterBase = {
|
||||
router: null, // holds the actual router instance
|
||||
readyCallbacks: [],
|
||||
ready(callback: () => void) {
|
||||
if (this.router) return callback()
|
||||
if (typeof window !== 'undefined') {
|
||||
this.readyCallbacks.push(callback)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Create public properties and methods of the router in the singletonRouter
|
||||
const urlPropertyFields = [
|
||||
'pathname',
|
||||
'route',
|
||||
'query',
|
||||
'asPath',
|
||||
'components',
|
||||
'isFallback',
|
||||
'basePath',
|
||||
'locale',
|
||||
'locales',
|
||||
'defaultLocale',
|
||||
'isReady',
|
||||
'isPreview',
|
||||
'isLocaleDomain',
|
||||
'domainLocales',
|
||||
] as const
|
||||
const routerEvents = [
|
||||
'routeChangeStart',
|
||||
'beforeHistoryChange',
|
||||
'routeChangeComplete',
|
||||
'routeChangeError',
|
||||
'hashChangeStart',
|
||||
'hashChangeComplete',
|
||||
] as const
|
||||
export type RouterEvent = (typeof routerEvents)[number]
|
||||
|
||||
const coreMethodFields = [
|
||||
'push',
|
||||
'replace',
|
||||
'reload',
|
||||
'back',
|
||||
'prefetch',
|
||||
'beforePopState',
|
||||
] as const
|
||||
|
||||
// Events is a static property on the router, the router doesn't have to be initialized to use it
|
||||
Object.defineProperty(singletonRouter, 'events', {
|
||||
get() {
|
||||
return Router.events
|
||||
},
|
||||
})
|
||||
|
||||
function getRouter(): Router {
|
||||
if (!singletonRouter.router) {
|
||||
const message =
|
||||
'No router instance found.\n' +
|
||||
'You should only use "next/router" on the client side of your app.\n'
|
||||
throw new Error(message)
|
||||
}
|
||||
return singletonRouter.router
|
||||
}
|
||||
|
||||
urlPropertyFields.forEach((field) => {
|
||||
// Here we need to use Object.defineProperty because we need to return
|
||||
// the property assigned to the actual router
|
||||
// The value might get changed as we change routes and this is the
|
||||
// proper way to access it
|
||||
Object.defineProperty(singletonRouter, field, {
|
||||
get() {
|
||||
const router = getRouter()
|
||||
return router[field] as string
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
coreMethodFields.forEach((field) => {
|
||||
// We don't really know the types here, so we add them later instead
|
||||
;(singletonRouter as any)[field] = (...args: any[]) => {
|
||||
const router = getRouter() as any
|
||||
return router[field](...args)
|
||||
}
|
||||
})
|
||||
|
||||
routerEvents.forEach((event) => {
|
||||
singletonRouter.ready(() => {
|
||||
Router.events.on(event, (...args) => {
|
||||
const eventField = `on${event.charAt(0).toUpperCase()}${event.substring(
|
||||
1
|
||||
)}`
|
||||
const _singletonRouter = singletonRouter as any
|
||||
if (_singletonRouter[eventField]) {
|
||||
try {
|
||||
_singletonRouter[eventField](...args)
|
||||
} catch (err) {
|
||||
console.error(`Error when running the Router event: ${eventField}`)
|
||||
console.error(
|
||||
isError(err) ? `${err.message}\n${err.stack}` : err + ''
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Export the singletonRouter and this is the public API.
|
||||
export default singletonRouter as SingletonRouter
|
||||
|
||||
// Reexport the withRouter HOC
|
||||
export { default as withRouter } from './with-router'
|
||||
|
||||
/**
|
||||
* This hook gives access the [router object](https://nextjs.org/docs/pages/api-reference/functions/use-router#router-object)
|
||||
* inside the [Pages Router](https://nextjs.org/docs/pages/building-your-application).
|
||||
*
|
||||
* Read more: [Next.js Docs: `useRouter`](https://nextjs.org/docs/pages/api-reference/functions/use-router)
|
||||
*/
|
||||
export function useRouter(): NextRouter {
|
||||
const router = React.useContext(RouterContext)
|
||||
if (!router) {
|
||||
throw new Error(
|
||||
'NextRouter was not mounted. https://nextjs.org/docs/messages/next-router-not-mounted'
|
||||
)
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a router and assign it as the singleton instance.
|
||||
* This is used in client side when we are initializing the app.
|
||||
* This should **not** be used inside the server.
|
||||
* @internal
|
||||
*/
|
||||
export function createRouter(
|
||||
...args: ConstructorParameters<typeof Router>
|
||||
): Router {
|
||||
singletonRouter.router = new Router(...args)
|
||||
singletonRouter.readyCallbacks.forEach((cb) => cb())
|
||||
singletonRouter.readyCallbacks = []
|
||||
|
||||
return singletonRouter.router
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to create the `withRouter` router instance
|
||||
* @internal
|
||||
*/
|
||||
export function makePublicRouterInstance(router: Router): NextRouter {
|
||||
const scopedRouter = router as any
|
||||
const instance = {} as any
|
||||
|
||||
for (const property of urlPropertyFields) {
|
||||
if (typeof scopedRouter[property] === 'object') {
|
||||
instance[property] = Object.assign(
|
||||
Array.isArray(scopedRouter[property]) ? [] : {},
|
||||
scopedRouter[property]
|
||||
) // makes sure query is not stateful
|
||||
continue
|
||||
}
|
||||
|
||||
instance[property] = scopedRouter[property]
|
||||
}
|
||||
|
||||
// Events is a static property on the router, the router doesn't have to be initialized to use it
|
||||
instance.events = Router.events
|
||||
|
||||
coreMethodFields.forEach((field) => {
|
||||
instance[field] = (...args: any[]) => {
|
||||
return scopedRouter[field](...args)
|
||||
}
|
||||
})
|
||||
|
||||
return instance
|
||||
}
|
||||
385
anyclip/client/script.tsx
Normal file
385
anyclip/client/script.tsx
Normal file
@@ -0,0 +1,385 @@
|
||||
'use client'
|
||||
|
||||
import ReactDOM from 'react-dom'
|
||||
import React, { useEffect, useContext, useRef, type JSX } from 'react'
|
||||
import type { ScriptHTMLAttributes } from 'react'
|
||||
import { HeadManagerContext } from '../shared/lib/head-manager-context.shared-runtime'
|
||||
import { setAttributesFromProps } from './set-attributes-from-props'
|
||||
import { requestIdleCallback } from './request-idle-callback'
|
||||
|
||||
const ScriptCache = new Map()
|
||||
const LoadCache = new Set()
|
||||
|
||||
export interface ScriptProps extends ScriptHTMLAttributes<HTMLScriptElement> {
|
||||
strategy?: 'afterInteractive' | 'lazyOnload' | 'beforeInteractive' | 'worker'
|
||||
id?: string
|
||||
onLoad?: (e: any) => void
|
||||
onReady?: () => void | null
|
||||
onError?: (e: any) => void
|
||||
children?: React.ReactNode
|
||||
stylesheets?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `ScriptProps` instead.
|
||||
*/
|
||||
export type Props = ScriptProps
|
||||
|
||||
const insertStylesheets = (stylesheets: string[]) => {
|
||||
// Case 1: Styles for afterInteractive/lazyOnload with appDir injected via handleClientScriptLoad
|
||||
//
|
||||
// Using ReactDOM.preinit to feature detect appDir and inject styles
|
||||
// Stylesheets might have already been loaded if initialized with Script component
|
||||
// Re-inject styles here to handle scripts loaded via handleClientScriptLoad
|
||||
// ReactDOM.preinit handles dedup and ensures the styles are loaded only once
|
||||
if (ReactDOM.preinit) {
|
||||
stylesheets.forEach((stylesheet: string) => {
|
||||
ReactDOM.preinit(stylesheet, { as: 'style' })
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Case 2: Styles for afterInteractive/lazyOnload with pages injected via handleClientScriptLoad
|
||||
//
|
||||
// We use this function to load styles when appdir is not detected
|
||||
// TODO: Use React float APIs to load styles once available for pages dir
|
||||
if (typeof window !== 'undefined') {
|
||||
let head = document.head
|
||||
stylesheets.forEach((stylesheet: string) => {
|
||||
let link = document.createElement('link')
|
||||
|
||||
link.type = 'text/css'
|
||||
link.rel = 'stylesheet'
|
||||
link.href = stylesheet
|
||||
|
||||
head.appendChild(link)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const loadScript = (props: ScriptProps): void => {
|
||||
const {
|
||||
src,
|
||||
id,
|
||||
onLoad = () => {},
|
||||
onReady = null,
|
||||
dangerouslySetInnerHTML,
|
||||
children = '',
|
||||
strategy = 'afterInteractive',
|
||||
onError,
|
||||
stylesheets,
|
||||
} = props
|
||||
|
||||
const cacheKey = id || src
|
||||
|
||||
// Script has already loaded
|
||||
if (cacheKey && LoadCache.has(cacheKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Contents of this script are already loading/loaded
|
||||
if (ScriptCache.has(src)) {
|
||||
LoadCache.add(cacheKey)
|
||||
// It is possible that multiple `next/script` components all have same "src", but has different "onLoad"
|
||||
// This is to make sure the same remote script will only load once, but "onLoad" are executed in order
|
||||
ScriptCache.get(src).then(onLoad, onError)
|
||||
return
|
||||
}
|
||||
|
||||
/** Execute after the script first loaded */
|
||||
const afterLoad = () => {
|
||||
// Run onReady for the first time after load event
|
||||
if (onReady) {
|
||||
onReady()
|
||||
}
|
||||
// add cacheKey to LoadCache when load successfully
|
||||
LoadCache.add(cacheKey)
|
||||
}
|
||||
|
||||
const el = document.createElement('script')
|
||||
|
||||
const loadPromise = new Promise<void>((resolve, reject) => {
|
||||
el.addEventListener('load', function (e) {
|
||||
resolve()
|
||||
if (onLoad) {
|
||||
onLoad.call(this, e)
|
||||
}
|
||||
afterLoad()
|
||||
})
|
||||
el.addEventListener('error', function (e) {
|
||||
reject(e)
|
||||
})
|
||||
}).catch(function (e) {
|
||||
if (onError) {
|
||||
onError(e)
|
||||
}
|
||||
})
|
||||
|
||||
if (dangerouslySetInnerHTML) {
|
||||
// Casting since lib.dom.d.ts doesn't have TrustedHTML yet.
|
||||
el.innerHTML = (dangerouslySetInnerHTML.__html as string) || ''
|
||||
|
||||
afterLoad()
|
||||
} else if (children) {
|
||||
el.textContent =
|
||||
typeof children === 'string'
|
||||
? children
|
||||
: Array.isArray(children)
|
||||
? children.join('')
|
||||
: ''
|
||||
|
||||
afterLoad()
|
||||
} else if (src) {
|
||||
el.src = src
|
||||
// do not add cacheKey into LoadCache for remote script here
|
||||
// cacheKey will be added to LoadCache when it is actually loaded (see loadPromise above)
|
||||
|
||||
ScriptCache.set(src, loadPromise)
|
||||
}
|
||||
|
||||
setAttributesFromProps(el, props)
|
||||
|
||||
if (strategy === 'worker') {
|
||||
el.setAttribute('type', 'text/partytown')
|
||||
}
|
||||
|
||||
el.setAttribute('data-nscript', strategy)
|
||||
|
||||
// Load styles associated with this script
|
||||
if (stylesheets) {
|
||||
insertStylesheets(stylesheets)
|
||||
}
|
||||
|
||||
document.body.appendChild(el)
|
||||
}
|
||||
|
||||
export function handleClientScriptLoad(props: ScriptProps) {
|
||||
const { strategy = 'afterInteractive' } = props
|
||||
if (strategy === 'lazyOnload') {
|
||||
window.addEventListener('load', () => {
|
||||
requestIdleCallback(() => loadScript(props))
|
||||
})
|
||||
} else {
|
||||
loadScript(props)
|
||||
}
|
||||
}
|
||||
|
||||
function loadLazyScript(props: ScriptProps) {
|
||||
if (document.readyState === 'complete') {
|
||||
requestIdleCallback(() => loadScript(props))
|
||||
} else {
|
||||
window.addEventListener('load', () => {
|
||||
requestIdleCallback(() => loadScript(props))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function addBeforeInteractiveToCache() {
|
||||
const scripts = [
|
||||
...document.querySelectorAll('[data-nscript="beforeInteractive"]'),
|
||||
...document.querySelectorAll('[data-nscript="beforePageRender"]'),
|
||||
]
|
||||
scripts.forEach((script) => {
|
||||
const cacheKey = script.id || script.getAttribute('src')
|
||||
LoadCache.add(cacheKey)
|
||||
})
|
||||
}
|
||||
|
||||
export function initScriptLoader(scriptLoaderItems: ScriptProps[]) {
|
||||
scriptLoaderItems.forEach(handleClientScriptLoad)
|
||||
addBeforeInteractiveToCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a third-party scripts in an optimized way.
|
||||
*
|
||||
* Read more: [Next.js Docs: `next/script`](https://nextjs.org/docs/app/api-reference/components/script)
|
||||
*/
|
||||
function Script(props: ScriptProps): JSX.Element | null {
|
||||
const {
|
||||
id,
|
||||
src = '',
|
||||
onLoad = () => {},
|
||||
onReady = null,
|
||||
strategy = 'afterInteractive',
|
||||
onError,
|
||||
stylesheets,
|
||||
...restProps
|
||||
} = props
|
||||
|
||||
// Context is available only during SSR
|
||||
let { updateScripts, scripts, getIsSsr, appDir, nonce } =
|
||||
useContext(HeadManagerContext)
|
||||
|
||||
// if a nonce is explicitly passed to the script tag, favor that over the automatic handling
|
||||
nonce = restProps.nonce || nonce
|
||||
|
||||
/**
|
||||
* - First mount:
|
||||
* 1. The useEffect for onReady executes
|
||||
* 2. hasOnReadyEffectCalled.current is false, but the script hasn't loaded yet (not in LoadCache)
|
||||
* onReady is skipped, set hasOnReadyEffectCalled.current to true
|
||||
* 3. The useEffect for loadScript executes
|
||||
* 4. hasLoadScriptEffectCalled.current is false, loadScript executes
|
||||
* Once the script is loaded, the onLoad and onReady will be called by then
|
||||
* [If strict mode is enabled / is wrapped in <OffScreen /> component]
|
||||
* 5. The useEffect for onReady executes again
|
||||
* 6. hasOnReadyEffectCalled.current is true, so entire effect is skipped
|
||||
* 7. The useEffect for loadScript executes again
|
||||
* 8. hasLoadScriptEffectCalled.current is true, so entire effect is skipped
|
||||
*
|
||||
* - Second mount:
|
||||
* 1. The useEffect for onReady executes
|
||||
* 2. hasOnReadyEffectCalled.current is false, but the script has already loaded (found in LoadCache)
|
||||
* onReady is called, set hasOnReadyEffectCalled.current to true
|
||||
* 3. The useEffect for loadScript executes
|
||||
* 4. The script is already loaded, loadScript bails out
|
||||
* [If strict mode is enabled / is wrapped in <OffScreen /> component]
|
||||
* 5. The useEffect for onReady executes again
|
||||
* 6. hasOnReadyEffectCalled.current is true, so entire effect is skipped
|
||||
* 7. The useEffect for loadScript executes again
|
||||
* 8. hasLoadScriptEffectCalled.current is true, so entire effect is skipped
|
||||
*/
|
||||
const hasOnReadyEffectCalled = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const cacheKey = id || src
|
||||
if (!hasOnReadyEffectCalled.current) {
|
||||
// Run onReady if script has loaded before but component is re-mounted
|
||||
if (onReady && cacheKey && LoadCache.has(cacheKey)) {
|
||||
onReady()
|
||||
}
|
||||
|
||||
hasOnReadyEffectCalled.current = true
|
||||
}
|
||||
}, [onReady, id, src])
|
||||
|
||||
const hasLoadScriptEffectCalled = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasLoadScriptEffectCalled.current) {
|
||||
if (strategy === 'afterInteractive') {
|
||||
loadScript(props)
|
||||
} else if (strategy === 'lazyOnload') {
|
||||
loadLazyScript(props)
|
||||
}
|
||||
|
||||
hasLoadScriptEffectCalled.current = true
|
||||
}
|
||||
}, [props, strategy])
|
||||
|
||||
if (strategy === 'beforeInteractive' || strategy === 'worker') {
|
||||
if (updateScripts) {
|
||||
scripts[strategy] = (scripts[strategy] || []).concat([
|
||||
{
|
||||
id,
|
||||
src,
|
||||
onLoad,
|
||||
onReady,
|
||||
onError,
|
||||
...restProps,
|
||||
nonce,
|
||||
},
|
||||
])
|
||||
updateScripts(scripts)
|
||||
} else if (getIsSsr && getIsSsr()) {
|
||||
// Script has already loaded during SSR
|
||||
LoadCache.add(id || src)
|
||||
} else if (getIsSsr && !getIsSsr()) {
|
||||
loadScript({
|
||||
...props,
|
||||
nonce,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// For the app directory, we need React Float to preload these scripts.
|
||||
if (appDir) {
|
||||
// Injecting stylesheets here handles beforeInteractive and worker scripts correctly
|
||||
// For other strategies injecting here ensures correct stylesheet order
|
||||
// ReactDOM.preinit handles loading the styles in the correct order,
|
||||
// also ensures the stylesheet is loaded only once and in a consistent manner
|
||||
//
|
||||
// Case 1: Styles for beforeInteractive/worker with appDir - handled here
|
||||
// Case 2: Styles for beforeInteractive/worker with pages dir - Not handled yet
|
||||
// Case 3: Styles for afterInteractive/lazyOnload with appDir - handled here
|
||||
// Case 4: Styles for afterInteractive/lazyOnload with pages dir - handled in insertStylesheets function
|
||||
if (stylesheets) {
|
||||
stylesheets.forEach((styleSrc) => {
|
||||
ReactDOM.preinit(styleSrc, { as: 'style' })
|
||||
})
|
||||
}
|
||||
|
||||
// Before interactive scripts need to be loaded by Next.js' runtime instead
|
||||
// of native <script> tags, because they no longer have `defer`.
|
||||
if (strategy === 'beforeInteractive') {
|
||||
if (!src) {
|
||||
// For inlined scripts, we put the content in `children`.
|
||||
if (restProps.dangerouslySetInnerHTML) {
|
||||
// Casting since lib.dom.d.ts doesn't have TrustedHTML yet.
|
||||
restProps.children = restProps.dangerouslySetInnerHTML
|
||||
.__html as string
|
||||
delete restProps.dangerouslySetInnerHTML
|
||||
}
|
||||
|
||||
return (
|
||||
<script
|
||||
nonce={nonce}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(self.__next_s=self.__next_s||[]).push(${JSON.stringify([
|
||||
0,
|
||||
{ ...restProps, id },
|
||||
])})`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
// @ts-ignore
|
||||
ReactDOM.preload(
|
||||
src,
|
||||
restProps.integrity
|
||||
? {
|
||||
as: 'script',
|
||||
integrity: restProps.integrity,
|
||||
nonce,
|
||||
crossOrigin: restProps.crossOrigin,
|
||||
}
|
||||
: { as: 'script', nonce, crossOrigin: restProps.crossOrigin }
|
||||
)
|
||||
return (
|
||||
<script
|
||||
nonce={nonce}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(self.__next_s=self.__next_s||[]).push(${JSON.stringify([
|
||||
src,
|
||||
{ ...restProps, id },
|
||||
])})`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
} else if (strategy === 'afterInteractive') {
|
||||
if (src) {
|
||||
// @ts-ignore
|
||||
ReactDOM.preload(
|
||||
src,
|
||||
restProps.integrity
|
||||
? {
|
||||
as: 'script',
|
||||
integrity: restProps.integrity,
|
||||
nonce,
|
||||
crossOrigin: restProps.crossOrigin,
|
||||
}
|
||||
: { as: 'script', nonce, crossOrigin: restProps.crossOrigin }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
Object.defineProperty(Script, '__nextScript', { value: true })
|
||||
|
||||
export default Script
|
||||
59
anyclip/client/set-attributes-from-props.ts
Normal file
59
anyclip/client/set-attributes-from-props.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
const DOMAttributeNames: Record<string, string> = {
|
||||
acceptCharset: 'accept-charset',
|
||||
className: 'class',
|
||||
htmlFor: 'for',
|
||||
httpEquiv: 'http-equiv',
|
||||
noModule: 'noModule',
|
||||
}
|
||||
|
||||
const ignoreProps = [
|
||||
'onLoad',
|
||||
'onReady',
|
||||
'dangerouslySetInnerHTML',
|
||||
'children',
|
||||
'onError',
|
||||
'strategy',
|
||||
'stylesheets',
|
||||
]
|
||||
|
||||
function isBooleanScriptAttribute(
|
||||
attr: string
|
||||
): attr is 'async' | 'defer' | 'noModule' {
|
||||
return ['async', 'defer', 'noModule'].includes(attr)
|
||||
}
|
||||
|
||||
export function setAttributesFromProps(el: HTMLElement, props: object) {
|
||||
for (const [p, value] of Object.entries(props)) {
|
||||
if (!props.hasOwnProperty(p)) continue
|
||||
if (ignoreProps.includes(p)) continue
|
||||
|
||||
// we don't render undefined props to the DOM
|
||||
if (value === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
const attr = DOMAttributeNames[p] || p.toLowerCase()
|
||||
|
||||
if (el.tagName === 'SCRIPT' && isBooleanScriptAttribute(attr)) {
|
||||
// Correctly assign boolean script attributes
|
||||
// https://github.com/vercel/next.js/pull/20748
|
||||
;(el as HTMLScriptElement)[attr] = !!value
|
||||
} else {
|
||||
el.setAttribute(attr, String(value))
|
||||
}
|
||||
|
||||
// Remove falsy non-zero boolean attributes so they are correctly interpreted
|
||||
// (e.g. if we set them to false, this coerces to the string "false", which the browser interprets as true)
|
||||
if (
|
||||
value === false ||
|
||||
(el.tagName === 'SCRIPT' &&
|
||||
isBooleanScriptAttribute(attr) &&
|
||||
(!value || value === 'false'))
|
||||
) {
|
||||
// Call setAttribute before, as we need to set and unset the attribute to override force async:
|
||||
// https://html.spec.whatwg.org/multipage/scripting.html#script-force-async
|
||||
el.setAttribute(attr, '')
|
||||
el.removeAttribute(attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
37
anyclip/client/trusted-types.ts
Normal file
37
anyclip/client/trusted-types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Stores the Trusted Types Policy. Starts as undefined and can be set to null
|
||||
* if Trusted Types is not supported in the browser.
|
||||
*/
|
||||
let policy: TrustedTypePolicy | null | undefined
|
||||
|
||||
/**
|
||||
* Getter for the Trusted Types Policy. If it is undefined, it is instantiated
|
||||
* here or set to null if Trusted Types is not supported in the browser.
|
||||
*/
|
||||
function getPolicy() {
|
||||
if (typeof policy === 'undefined' && typeof window !== 'undefined') {
|
||||
policy =
|
||||
window.trustedTypes?.createPolicy('nextjs', {
|
||||
createHTML: (input) => input,
|
||||
createScript: (input) => input,
|
||||
createScriptURL: (input) => input,
|
||||
}) || null
|
||||
}
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsafely promote a string to a TrustedScriptURL, falling back to strings
|
||||
* when Trusted Types are not available.
|
||||
* This is a security-sensitive function; any use of this function
|
||||
* must go through security review. In particular, it must be assured that the
|
||||
* provided string will never cause an XSS vulnerability if used in a context
|
||||
* that will cause a browser to load and execute a resource, e.g. when
|
||||
* assigning to script.src.
|
||||
*/
|
||||
export function __unsafeCreateTrustedScriptURL(
|
||||
url: string
|
||||
): TrustedScriptURL | string {
|
||||
return getPolicy()?.createScriptURL(url) || url
|
||||
}
|
||||
137
anyclip/client/use-intersection.tsx
Normal file
137
anyclip/client/use-intersection.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
requestIdleCallback,
|
||||
cancelIdleCallback,
|
||||
} from './request-idle-callback'
|
||||
|
||||
type UseIntersectionObserverInit = Pick<
|
||||
IntersectionObserverInit,
|
||||
'rootMargin' | 'root'
|
||||
>
|
||||
|
||||
type UseIntersection = { disabled?: boolean } & UseIntersectionObserverInit & {
|
||||
rootRef?: React.RefObject<HTMLElement | null> | null
|
||||
}
|
||||
type ObserveCallback = (isVisible: boolean) => void
|
||||
type Identifier = {
|
||||
root: Element | Document | null
|
||||
margin: string
|
||||
}
|
||||
type Observer = {
|
||||
id: Identifier
|
||||
observer: IntersectionObserver
|
||||
elements: Map<Element, ObserveCallback>
|
||||
}
|
||||
|
||||
const hasIntersectionObserver = typeof IntersectionObserver === 'function'
|
||||
|
||||
const observers = new Map<Identifier, Observer>()
|
||||
const idList: Identifier[] = []
|
||||
|
||||
function createObserver(options: UseIntersectionObserverInit): Observer {
|
||||
const id = {
|
||||
root: options.root || null,
|
||||
margin: options.rootMargin || '',
|
||||
}
|
||||
const existing = idList.find(
|
||||
(obj) => obj.root === id.root && obj.margin === id.margin
|
||||
)
|
||||
let instance: Observer | undefined
|
||||
|
||||
if (existing) {
|
||||
instance = observers.get(existing)
|
||||
if (instance) {
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
const elements = new Map<Element, ObserveCallback>()
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const callback = elements.get(entry.target)
|
||||
const isVisible = entry.isIntersecting || entry.intersectionRatio > 0
|
||||
if (callback && isVisible) {
|
||||
callback(isVisible)
|
||||
}
|
||||
})
|
||||
}, options)
|
||||
instance = {
|
||||
id,
|
||||
observer,
|
||||
elements,
|
||||
}
|
||||
|
||||
idList.push(id)
|
||||
observers.set(id, instance)
|
||||
return instance
|
||||
}
|
||||
|
||||
function observe(
|
||||
element: Element,
|
||||
callback: ObserveCallback,
|
||||
options: UseIntersectionObserverInit
|
||||
): () => void {
|
||||
const { id, observer, elements } = createObserver(options)
|
||||
elements.set(element, callback)
|
||||
|
||||
observer.observe(element)
|
||||
return function unobserve(): void {
|
||||
elements.delete(element)
|
||||
observer.unobserve(element)
|
||||
|
||||
// Destroy observer when there's nothing left to watch:
|
||||
if (elements.size === 0) {
|
||||
observer.disconnect()
|
||||
observers.delete(id)
|
||||
const index = idList.findIndex(
|
||||
(obj) => obj.root === id.root && obj.margin === id.margin
|
||||
)
|
||||
if (index > -1) {
|
||||
idList.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useIntersection<T extends Element>({
|
||||
rootRef,
|
||||
rootMargin,
|
||||
disabled,
|
||||
}: UseIntersection): [(element: T | null) => void, boolean, () => void] {
|
||||
const isDisabled: boolean = disabled || !hasIntersectionObserver
|
||||
|
||||
const [visible, setVisible] = useState(false)
|
||||
const elementRef = useRef<T | null>(null)
|
||||
const setElement = useCallback((element: T | null) => {
|
||||
elementRef.current = element
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (hasIntersectionObserver) {
|
||||
if (isDisabled || visible) return
|
||||
|
||||
const element = elementRef.current
|
||||
if (element && element.tagName) {
|
||||
const unobserve = observe(
|
||||
element,
|
||||
(isVisible) => isVisible && setVisible(isVisible),
|
||||
{ root: rootRef?.current, rootMargin }
|
||||
)
|
||||
|
||||
return unobserve
|
||||
}
|
||||
} else {
|
||||
if (!visible) {
|
||||
const idleCallback = requestIdleCallback(() => setVisible(true))
|
||||
return () => cancelIdleCallback(idleCallback)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isDisabled, rootMargin, rootRef, visible, elementRef.current])
|
||||
|
||||
const resetVisible = useCallback(() => {
|
||||
setVisible(false)
|
||||
}, [])
|
||||
|
||||
return [setElement, visible, resetVisible]
|
||||
}
|
||||
67
anyclip/client/use-merged-ref.ts
Normal file
67
anyclip/client/use-merged-ref.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useCallback, useRef, type Ref } from 'react'
|
||||
|
||||
// This is a compatibility hook to support React 18 and 19 refs.
|
||||
// In 19, a cleanup function from refs may be returned.
|
||||
// In 18, returning a cleanup function creates a warning.
|
||||
// Since we take userspace refs, we don't know ahead of time if a cleanup function will be returned.
|
||||
// This implements cleanup functions with the old behavior in 18.
|
||||
// We know refs are always called alternating with `null` and then `T`.
|
||||
// So a call with `null` means we need to call the previous cleanup functions.
|
||||
export function useMergedRef<TElement>(
|
||||
refA: Ref<TElement>,
|
||||
refB: Ref<TElement>
|
||||
): Ref<TElement> {
|
||||
const cleanupA = useRef<(() => void) | null>(null)
|
||||
const cleanupB = useRef<(() => void) | null>(null)
|
||||
|
||||
// NOTE: In theory, we could skip the wrapping if only one of the refs is non-null.
|
||||
// (this happens often if the user doesn't pass a ref to Link/Form/Image)
|
||||
// But this can cause us to leak a cleanup-ref into user code (e.g. via `<Link legacyBehavior>`),
|
||||
// and the user might pass that ref into ref-merging library that doesn't support cleanup refs
|
||||
// (because it hasn't been updated for React 19)
|
||||
// which can then cause things to blow up, because a cleanup-returning ref gets called with `null`.
|
||||
// So in practice, it's safer to be defensive and always wrap the ref, even on React 19.
|
||||
return useCallback(
|
||||
(current: TElement | null): void => {
|
||||
if (current === null) {
|
||||
const cleanupFnA = cleanupA.current
|
||||
if (cleanupFnA) {
|
||||
cleanupA.current = null
|
||||
cleanupFnA()
|
||||
}
|
||||
const cleanupFnB = cleanupB.current
|
||||
if (cleanupFnB) {
|
||||
cleanupB.current = null
|
||||
cleanupFnB()
|
||||
}
|
||||
} else {
|
||||
if (refA) {
|
||||
cleanupA.current = applyRef(refA, current)
|
||||
}
|
||||
if (refB) {
|
||||
cleanupB.current = applyRef(refB, current)
|
||||
}
|
||||
}
|
||||
},
|
||||
[refA, refB]
|
||||
)
|
||||
}
|
||||
|
||||
function applyRef<TElement>(
|
||||
refA: NonNullable<Ref<TElement>>,
|
||||
current: TElement
|
||||
) {
|
||||
if (typeof refA === 'function') {
|
||||
const cleanup = refA(current)
|
||||
if (typeof cleanup === 'function') {
|
||||
return cleanup
|
||||
} else {
|
||||
return () => refA(null)
|
||||
}
|
||||
} else {
|
||||
refA.current = current
|
||||
return () => {
|
||||
refA.current = null
|
||||
}
|
||||
}
|
||||
}
|
||||
39
anyclip/client/webpack.ts
Normal file
39
anyclip/client/webpack.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
declare const __webpack_require__: any
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
declare let __webpack_public_path__: string
|
||||
|
||||
import { getDeploymentIdQueryOrEmptyString } from '../build/deployment-id'
|
||||
|
||||
// If we have a deployment ID, we need to append it to the webpack chunk names
|
||||
// I am keeping the process check explicit so this can be statically optimized
|
||||
if (process.env.NEXT_DEPLOYMENT_ID) {
|
||||
const suffix = getDeploymentIdQueryOrEmptyString()
|
||||
// eslint-disable-next-line no-undef
|
||||
const getChunkScriptFilename = __webpack_require__.u
|
||||
// eslint-disable-next-line no-undef
|
||||
__webpack_require__.u = (...args: any[]) =>
|
||||
// We enode the chunk filename because our static server matches against and encoded
|
||||
// filename path.
|
||||
getChunkScriptFilename(...args) + suffix
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const getChunkCssFilename = __webpack_require__.k
|
||||
// eslint-disable-next-line no-undef
|
||||
__webpack_require__.k = (...args: any[]) =>
|
||||
getChunkCssFilename(...args) + suffix
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const getMiniCssFilename = __webpack_require__.miniCssF
|
||||
// eslint-disable-next-line no-undef
|
||||
__webpack_require__.miniCssF = (...args: any[]) =>
|
||||
getMiniCssFilename(...args) + suffix
|
||||
}
|
||||
|
||||
// Ignore the module ID transform in client.
|
||||
;(self as any).__next_set_public_path__ = (path: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
__webpack_public_path__ = path
|
||||
}
|
||||
|
||||
export {}
|
||||
41
anyclip/client/with-router.tsx
Normal file
41
anyclip/client/with-router.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { type JSX } from 'react'
|
||||
import type {
|
||||
BaseContext,
|
||||
NextComponentType,
|
||||
NextPageContext,
|
||||
} from '../shared/lib/utils'
|
||||
import type { NextRouter } from './router'
|
||||
import { useRouter } from './router'
|
||||
|
||||
export type WithRouterProps = {
|
||||
router: NextRouter
|
||||
}
|
||||
|
||||
export type ExcludeRouterProps<P> = Pick<
|
||||
P,
|
||||
Exclude<keyof P, keyof WithRouterProps>
|
||||
>
|
||||
|
||||
export default function withRouter<
|
||||
P extends WithRouterProps,
|
||||
C extends BaseContext = NextPageContext,
|
||||
>(
|
||||
ComposedComponent: NextComponentType<C, any, P>
|
||||
): React.ComponentType<ExcludeRouterProps<P>> {
|
||||
function WithRouterWrapper(props: any): JSX.Element {
|
||||
return <ComposedComponent router={useRouter()} {...props} />
|
||||
}
|
||||
|
||||
WithRouterWrapper.getInitialProps = ComposedComponent.getInitialProps
|
||||
// This is needed to allow checking for custom getInitialProps in _app
|
||||
;(WithRouterWrapper as any).origGetInitialProps = (
|
||||
ComposedComponent as any
|
||||
).origGetInitialProps
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const name =
|
||||
ComposedComponent.displayName || ComposedComponent.name || 'Unknown'
|
||||
WithRouterWrapper.displayName = `withRouter(${name})`
|
||||
}
|
||||
|
||||
return WithRouterWrapper
|
||||
}
|
||||
Reference in New Issue
Block a user