rsnext/packages/next/client/link.js
JJ Kasper a6ddaefe22 Add experimental exportTrailingSlash config (#6664)
By default when `next export`ing a Next.js application we will automatically append a `/` to all urls to be fully compatible with the directory structure being output.

However since most platforms support directory indexes it makes sense to change this default in the future.

This PR adds `exportTrailingSlash` as experimental flag. We'll try this out for a bit on nextjs.org / zeit.co/docs before introducing it as new option.

The default value is `true` as this is the current behavior in stable Next.js.

```
{
  experimental: {
    exportTrailingSlash: false
  }
}
```

⚠️ as with all experimental flags being added this is subject to breaking between canary/stable versions.
2019-03-17 00:54:58 +01:00

183 lines
5.2 KiB
JavaScript

/* global __NEXT_DATA__ */
import { resolve, parse } from 'url'
import React, { Component, Children } from 'react'
import PropTypes from 'prop-types'
import Router, { Router as _Router } from 'next/router'
import { execOnce, formatWithValidation, getLocationOrigin } from 'next-server/dist/lib/utils'
function isLocal (href) {
const url = parse(href, false, true)
const origin = parse(getLocationOrigin(), false, true)
return !url.host ||
(url.protocol === origin.protocol && url.host === origin.host)
}
function memoizedFormatUrl (formatFunc) {
let lastHref = null
let lastAs = null
let lastResult = null
return (href, as) => {
if (href === lastHref && as === lastAs) {
return lastResult
}
const result = formatFunc(href, as)
lastHref = href
lastAs = as
lastResult = result
return result
}
}
function formatUrl (url) {
return url && typeof url === 'object'
? formatWithValidation(url)
: url
}
class Link extends Component {
componentDidMount () {
this.prefetch()
}
componentDidUpdate (prevProps) {
if (JSON.stringify(this.props.href) !== JSON.stringify(prevProps.href)) {
this.prefetch()
}
}
// The function is memoized so that no extra lifecycles are needed
// as per https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html
formatUrls = memoizedFormatUrl((href, asHref) => {
return {
href: formatUrl(href),
as: formatUrl(asHref, true)
}
})
linkClicked = e => {
const { nodeName, target } = e.currentTarget
if (nodeName === 'A' &&
((target && target !== '_self') || e.metaKey || e.ctrlKey || e.shiftKey || (e.nativeEvent && e.nativeEvent.which === 2))) {
// ignore click for new tab / new window behavior
return
}
let { href, as } = this.formatUrls(this.props.href, this.props.as)
if (!isLocal(href)) {
// ignore click if it's outside our scope
return
}
const { pathname } = window.location
href = resolve(pathname, href)
as = as ? resolve(pathname, as) : href
e.preventDefault()
// avoid scroll for urls with anchor refs
let { scroll } = this.props
if (scroll == null) {
scroll = as.indexOf('#') < 0
}
// replace state instead of push if prop is present
Router[this.props.replace ? 'replace' : 'push'](href, as, { shallow: this.props.shallow })
.then((success) => {
if (!success) return
if (scroll) {
window.scrollTo(0, 0)
document.body.focus()
}
})
.catch((err) => {
if (this.props.onError) this.props.onError(err)
})
};
prefetch () {
if (!this.props.prefetch) return
if (typeof window === 'undefined') return
// Prefetch the JSON page if asked (only in the client)
const { pathname } = window.location
const { href: parsedHref } = this.formatUrls(this.props.href, this.props.as)
const href = resolve(pathname, parsedHref)
Router.prefetch(href)
}
render () {
let { children } = this.props
let { href, as } = this.formatUrls(this.props.href, this.props.as)
// Deprecated. Warning shown by propType check. If the childen provided is a string (<Link>example</Link>) we wrap it in an <a> tag
if (typeof children === 'string') {
children = <a>{children}</a>
}
// This will return the first child, if multiple are provided it will throw an error
const child = Children.only(children)
const props = {
onClick: (e) => {
if (child.props && typeof child.props.onClick === 'function') {
child.props.onClick(e)
}
if (!e.defaultPrevented) {
this.linkClicked(e)
}
}
}
// 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 (this.props.passHref || (child.type === 'a' && !('href' in child.props))) {
props.href = as || href
}
// Add the ending slash to the paths. So, we can serve the
// "<page>/index.html" directly.
if (process.env.__NEXT_EXPORT_TRAILING_SLASH) {
if (
props.href &&
typeof __NEXT_DATA__ !== 'undefined' &&
__NEXT_DATA__.nextExport
) {
props.href = _Router._rewriteUrlForNextExport(props.href)
}
}
return React.cloneElement(child, props)
}
}
if (process.env.NODE_ENV === 'development') {
const warn = execOnce(console.error)
// This module gets removed by webpack.IgnorePlugin
const exact = require('prop-types-exact')
Link.propTypes = exact({
href: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
as: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
prefetch: PropTypes.bool,
replace: PropTypes.bool,
shallow: PropTypes.bool,
passHref: PropTypes.bool,
scroll: PropTypes.bool,
children: PropTypes.oneOfType([
PropTypes.element,
(props, propName) => {
const value = props[propName]
if (typeof value === 'string') {
warn(`Warning: You're using a string directly inside <Link>. This usage has been deprecated. Please add an <a> tag as child of <Link>`)
}
return null
}
]).isRequired
})
}
export default Link