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:
2026-01-21 10:36:51 +08:00
parent d4fe4800e6
commit e32d475aa9
3463 changed files with 184648 additions and 64341 deletions

View 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)
)
}

View 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
}

View 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)
}
}

View 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
}
}

View 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)
}

View 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] || [])
})
},
}
}

View 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

File diff suppressed because it is too large Load Diff

717
anyclip/client/link.tsx Normal file
View 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 browsers 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 browsers 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
View 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)

View 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}`
}

View 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)
}
}

View 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
}

View 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
}

View 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)
}

View 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
}
}

View 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 pages 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 Suttons 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

View 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
View 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
View 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

View 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)
}
}
}

View 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
}

View 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]
}

View 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
View 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 {}

View 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
}