rsnext/packages/next/pages/_document.tsx

842 lines
26 KiB
TypeScript
Raw Normal View History

import PropTypes from 'prop-types'
import React, { useContext, Component } from 'react'
import flush from 'styled-jsx/server'
import {
AMP_RENDER_TARGET,
CLIENT_STATIC_FILES_RUNTIME_AMP,
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
CLIENT_STATIC_FILES_RUNTIME_WEBPACK,
} from '../next-server/lib/constants'
import { DocumentContext as DocumentComponentContext } from '../next-server/lib/document-context'
import {
DocumentContext,
DocumentInitialProps,
DocumentProps,
} from '../next-server/lib/utils'
import { cleanAmpPath } from '../next-server/server/utils'
import { htmlEscapeJsonString } from '../server/htmlescape'
2019-04-26 09:37:57 +02:00
export { DocumentContext, DocumentInitialProps, DocumentProps }
2019-04-26 09:37:57 +02:00
export type OriginProps = {
nonce?: string
crossOrigin?: string
}
2019-05-27 03:17:08 +02:00
function dedupe(bundles: any[]): any[] {
const files = new Set()
const kept = []
for (const bundle of bundles) {
if (files.has(bundle.file)) continue
files.add(bundle.file)
kept.push(bundle)
}
return kept
}
function getOptionalModernScriptVariant(path: string): string {
if (process.env.__NEXT_MODERN_BUILD) {
return path.replace(/\.js$/, '.module.js')
}
return path
}
/**
* `Document` component handles the initial `document` markup and renders only on the server side.
* Commonly used for implementing server side rendering for `css-in-js` libraries.
*/
export default class Document<P = {}> extends Component<DocumentProps & P> {
static headTagsMiddleware = process.env.__NEXT_PLUGINS
? import(
// @ts-ignore loader syntax
'next-plugin-loader?middleware=document-head-tags-server!'
)
: () => []
Initial plugins implementation (#9139) * Add initial bit for plugins * Add checks for needed metadata values * Add test * Initial plugins changes * Add handling for _app middleware * Add loading of _document middleware and handling of multiple default export syntaxes * Fix insert order for middleware member expression * Remove early return from middleware plugin from testing * Add tests for current plugin middlewares * Update test plugin package.json * Update handling for class default export * Update to use webpack loader instead of babel plugin, remove redundant middleware naming, and add field for required env for plugins * Add middleware to support material-ui use case and example material-ui plugin * Update tests and remove tests stuff from google analytics plugin * Remove old plugin suite * Add init-server middleware * Exit hard without stack trace when error in collecting plugins * Add on-error-client and on-error-server and update to run init-server with next-start in serverless mode * Update init-client for google analytics plugin * Add example Sentry plugin and update with-sentry-simple * Remove middleware field/folder and use src dir for plugins * Add post-hydration middleware and update material-ui plugin * Put plugins code behind flag * Update chromedriver * Revert "Update chromedriver" This reverts commit 1461e978e677f7da05e29e0415ec614a04bf65f9. * Update lock file * Remove un-needed _app for sentry example * Add auto loading of scoped packages, add plugins config for manually listing plugins, and update to only collect plugins once * Update example plugins * Expose plugins' config * Rename plugin lifecycles and add babel-preset-build * Rename other methods with unstable * Update log when plugin config overrides auto-detecting
2019-11-01 20:13:13 +01:00
/**
2019-08-06 13:23:50 +02:00
* `getInitialProps` hook returns the context object with the addition of `renderPage`.
* `renderPage` callback executes `React` rendering logic synchronously to support server-rendering wrappers
*/
Initial plugins implementation (#9139) * Add initial bit for plugins * Add checks for needed metadata values * Add test * Initial plugins changes * Add handling for _app middleware * Add loading of _document middleware and handling of multiple default export syntaxes * Fix insert order for middleware member expression * Remove early return from middleware plugin from testing * Add tests for current plugin middlewares * Update test plugin package.json * Update handling for class default export * Update to use webpack loader instead of babel plugin, remove redundant middleware naming, and add field for required env for plugins * Add middleware to support material-ui use case and example material-ui plugin * Update tests and remove tests stuff from google analytics plugin * Remove old plugin suite * Add init-server middleware * Exit hard without stack trace when error in collecting plugins * Add on-error-client and on-error-server and update to run init-server with next-start in serverless mode * Update init-client for google analytics plugin * Add example Sentry plugin and update with-sentry-simple * Remove middleware field/folder and use src dir for plugins * Add post-hydration middleware and update material-ui plugin * Put plugins code behind flag * Update chromedriver * Revert "Update chromedriver" This reverts commit 1461e978e677f7da05e29e0415ec614a04bf65f9. * Update lock file * Remove un-needed _app for sentry example * Add auto loading of scoped packages, add plugins config for manually listing plugins, and update to only collect plugins once * Update example plugins * Expose plugins' config * Rename plugin lifecycles and add babel-preset-build * Rename other methods with unstable * Update log when plugin config overrides auto-detecting
2019-11-01 20:13:13 +01:00
static async getInitialProps(
ctx: DocumentContext
): Promise<DocumentInitialProps> {
const enhancers = process.env.__NEXT_PLUGINS
? await import(
// @ts-ignore loader syntax
'next-plugin-loader?middleware=unstable-enhance-app-server!'
2020-05-18 21:24:37 +02:00
).then((mod) => mod.default(ctx))
: []
Initial plugins implementation (#9139) * Add initial bit for plugins * Add checks for needed metadata values * Add test * Initial plugins changes * Add handling for _app middleware * Add loading of _document middleware and handling of multiple default export syntaxes * Fix insert order for middleware member expression * Remove early return from middleware plugin from testing * Add tests for current plugin middlewares * Update test plugin package.json * Update handling for class default export * Update to use webpack loader instead of babel plugin, remove redundant middleware naming, and add field for required env for plugins * Add middleware to support material-ui use case and example material-ui plugin * Update tests and remove tests stuff from google analytics plugin * Remove old plugin suite * Add init-server middleware * Exit hard without stack trace when error in collecting plugins * Add on-error-client and on-error-server and update to run init-server with next-start in serverless mode * Update init-client for google analytics plugin * Add example Sentry plugin and update with-sentry-simple * Remove middleware field/folder and use src dir for plugins * Add post-hydration middleware and update material-ui plugin * Put plugins code behind flag * Update chromedriver * Revert "Update chromedriver" This reverts commit 1461e978e677f7da05e29e0415ec614a04bf65f9. * Update lock file * Remove un-needed _app for sentry example * Add auto loading of scoped packages, add plugins config for manually listing plugins, and update to only collect plugins once * Update example plugins * Expose plugins' config * Rename plugin lifecycles and add babel-preset-build * Rename other methods with unstable * Update log when plugin config overrides auto-detecting
2019-11-01 20:13:13 +01:00
const enhanceApp = (App: any) => {
for (const enhancer of enhancers) {
App = enhancer(App)
}
return (props: any) => <App {...props} />
}
const { html, head } = await ctx.renderPage({ enhanceApp })
const styles = [
...flush(),
...(process.env.__NEXT_PLUGINS
? await import(
// @ts-ignore loader syntax
'next-plugin-loader?middleware=unstable-get-styles-server!'
2020-05-18 21:24:37 +02:00
).then((mod) => mod.default(ctx))
: []),
]
return { html, head, styles }
}
2019-10-14 18:45:56 +02:00
static renderDocument<P>(
DocumentComponent: new () => Document<P>,
2019-10-14 18:45:56 +02:00
props: DocumentProps & P
): React.ReactElement {
return (
<DocumentComponentContext.Provider
value={{
_documentProps: props,
// In dev we invalidate the cache by appending a timestamp to the resource URL.
// This is a workaround to fix https://github.com/vercel/next.js/issues/5860
2019-10-14 18:45:56 +02:00
// TODO: remove this workaround when https://bugs.webkit.org/show_bug.cgi?id=187726 is fixed.
_devOnlyInvalidateCacheQueryString:
process.env.NODE_ENV !== 'production' ? '?ts=' + Date.now() : '',
}}
>
<DocumentComponent {...props} />
2019-10-14 18:45:56 +02:00
</DocumentComponentContext.Provider>
)
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export function Html(
props: React.DetailedHTMLProps<
React.HtmlHTMLAttributes<HTMLHtmlElement>,
HTMLHtmlElement
>
) {
const { inAmpMode } = useContext(DocumentComponentContext)._documentProps
return (
<html
{...props}
amp={inAmpMode ? '' : undefined}
data-ampdevmode={
inAmpMode && process.env.NODE_ENV !== 'production' ? '' : undefined
}
/>
)
}
export class Head extends Component<
OriginProps &
React.DetailedHTMLProps<
React.HTMLAttributes<HTMLHeadElement>,
HTMLHeadElement
>
> {
2019-10-14 18:45:56 +02:00
static contextType = DocumentComponentContext
static propTypes = {
nonce: PropTypes.string,
2019-03-01 20:51:13 +01:00
crossOrigin: PropTypes.string,
}
2019-10-14 18:45:56 +02:00
context!: React.ContextType<typeof DocumentComponentContext>
getCssLinks(): JSX.Element[] | null {
const { assetPrefix, files } = this.context._documentProps
const { _devOnlyInvalidateCacheQueryString } = this.context
const cssFiles =
2020-05-18 21:24:37 +02:00
files && files.length ? files.filter((f) => /\.css$/.test(f)) : []
const cssLinkElements: JSX.Element[] = []
2020-05-18 21:24:37 +02:00
cssFiles.forEach((file) => {
cssLinkElements.push(
<link
key={`${file}-preload`}
nonce={this.props.nonce}
rel="preload"
href={`${assetPrefix}/_next/${encodeURI(
file
)}${_devOnlyInvalidateCacheQueryString}`}
as="style"
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
/>,
<link
key={file}
nonce={this.props.nonce}
rel="stylesheet"
href={`${assetPrefix}/_next/${encodeURI(
file
)}${_devOnlyInvalidateCacheQueryString}`}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
/>
)
})
return cssLinkElements.length === 0 ? null : cssLinkElements
}
2019-03-01 20:51:13 +01:00
getPreloadDynamicChunks() {
const { dynamicImports, assetPrefix } = this.context._documentProps
const { _devOnlyInvalidateCacheQueryString } = this.context
return (
dedupe(dynamicImports)
.map((bundle: any) => {
2019-07-25 18:44:46 +02:00
// `dynamicImports` will contain both `.js` and `.module.js` when the
// feature is enabled. This clause will filter down to the modern
// variants only.
if (!bundle.file.endsWith(getOptionalModernScriptVariant('.js'))) {
return null
}
return (
<link
rel="preload"
key={bundle.file}
href={`${assetPrefix}/_next/${encodeURI(
bundle.file
)}${_devOnlyInvalidateCacheQueryString}`}
as="script"
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
/>
)
})
// Filter out nulled scripts
.filter(Boolean)
)
}
getPreloadMainLinks(): JSX.Element[] | null {
const { assetPrefix, files } = this.context._documentProps
const { _devOnlyInvalidateCacheQueryString } = this.context
const preloadFiles =
files && files.length
? files.filter((file: string) => {
// `dynamicImports` will contain both `.js` and `.module.js` when
// the feature is enabled. This clause will filter down to the
// modern variants only.
return file.endsWith(getOptionalModernScriptVariant('.js'))
})
: []
return !preloadFiles.length
? null
: preloadFiles.map((file: string) => (
<link
key={file}
nonce={this.props.nonce}
rel="preload"
href={`${assetPrefix}/_next/${encodeURI(
file
)}${_devOnlyInvalidateCacheQueryString}`}
as="script"
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
/>
))
}
2019-03-01 20:51:13 +01:00
render() {
const {
styles,
ampPath,
inAmpMode,
2019-03-01 20:51:13 +01:00
assetPrefix,
hybridAmp,
canonicalBase,
2019-03-01 20:51:13 +01:00
__NEXT_DATA__,
dangerousAsPath,
Initial plugins implementation (#9139) * Add initial bit for plugins * Add checks for needed metadata values * Add test * Initial plugins changes * Add handling for _app middleware * Add loading of _document middleware and handling of multiple default export syntaxes * Fix insert order for middleware member expression * Remove early return from middleware plugin from testing * Add tests for current plugin middlewares * Update test plugin package.json * Update handling for class default export * Update to use webpack loader instead of babel plugin, remove redundant middleware naming, and add field for required env for plugins * Add middleware to support material-ui use case and example material-ui plugin * Update tests and remove tests stuff from google analytics plugin * Remove old plugin suite * Add init-server middleware * Exit hard without stack trace when error in collecting plugins * Add on-error-client and on-error-server and update to run init-server with next-start in serverless mode * Update init-client for google analytics plugin * Add example Sentry plugin and update with-sentry-simple * Remove middleware field/folder and use src dir for plugins * Add post-hydration middleware and update material-ui plugin * Put plugins code behind flag * Update chromedriver * Revert "Update chromedriver" This reverts commit 1461e978e677f7da05e29e0415ec614a04bf65f9. * Update lock file * Remove un-needed _app for sentry example * Add auto loading of scoped packages, add plugins config for manually listing plugins, and update to only collect plugins once * Update example plugins * Expose plugins' config * Rename plugin lifecycles and add babel-preset-build * Rename other methods with unstable * Update log when plugin config overrides auto-detecting
2019-11-01 20:13:13 +01:00
headTags,
unstable_runtimeJS,
2019-03-01 20:51:13 +01:00
} = this.context._documentProps
const disableRuntimeJS = unstable_runtimeJS === false
const { _devOnlyInvalidateCacheQueryString } = this.context
const { page, buildId } = __NEXT_DATA__
let { head } = this.context._documentProps
let children = this.props.children
// show a warning if Head contains <title> (only in development)
if (process.env.NODE_ENV !== 'production') {
children = React.Children.map(children, (child: any) => {
const isReactHelmet = child?.props?.['data-react-helmet']
if (child?.type === 'title' && !isReactHelmet) {
2019-03-01 20:51:13 +01:00
console.warn(
"Warning: <title> should not be used in _document.js's <Head>. https://err.sh/next.js/no-document-title"
2019-03-01 20:51:13 +01:00
)
}
return child
})
2019-03-01 20:51:13 +01:00
if (this.props.crossOrigin)
console.warn(
'Warning: `Head` attribute `crossOrigin` is deprecated. https://err.sh/next.js/doc-crossorigin-deprecated'
2019-03-01 20:51:13 +01:00
)
}
let hasAmphtmlRel = false
let hasCanonicalRel = false
// show warning and remove conflicting amp head tags
2020-05-18 21:24:37 +02:00
head = React.Children.map(head || [], (child) => {
if (!child) return child
const { type, props } = child
if (inAmpMode) {
let badProp: string = ''
if (type === 'meta' && props.name === 'viewport') {
badProp = 'name="viewport"'
} else if (type === 'link' && props.rel === 'canonical') {
hasCanonicalRel = true
} else if (type === 'script') {
// only block if
// 1. it has a src and isn't pointing to ampproject's CDN
// 2. it is using dangerouslySetInnerHTML without a type or
// a type of text/javascript
if (
(props.src && props.src.indexOf('ampproject') < -1) ||
(props.dangerouslySetInnerHTML &&
(!props.type || props.type === 'text/javascript'))
) {
badProp = '<script'
2020-05-18 21:24:37 +02:00
Object.keys(props).forEach((prop) => {
badProp += ` ${prop}="${props[prop]}"`
})
badProp += '/>'
}
}
if (badProp) {
console.warn(
`Found conflicting amp tag "${child.type}" with conflicting prop ${badProp} in ${__NEXT_DATA__.page}. https://err.sh/next.js/conflicting-amp-tag`
)
return null
}
} else {
// non-amp mode
if (type === 'link' && props.rel === 'amphtml') {
hasAmphtmlRel = true
}
}
return child
})
// try to parse styles from fragment for backwards compat
const curStyles: React.ReactElement[] = Array.isArray(styles)
? (styles as React.ReactElement[])
: []
if (
inAmpMode &&
styles &&
// @ts-ignore Property 'props' does not exist on type ReactElement
styles.props &&
// @ts-ignore Property 'props' does not exist on type ReactElement
Array.isArray(styles.props.children)
) {
const hasStyles = (el: React.ReactElement) =>
el?.props?.dangerouslySetInnerHTML?.__html
// @ts-ignore Property 'props' does not exist on type ReactElement
styles.props.children.forEach((child: React.ReactElement) => {
if (Array.isArray(child)) {
2020-05-18 21:24:37 +02:00
child.forEach((el) => hasStyles(el) && curStyles.push(el))
} else if (hasStyles(child)) {
curStyles.push(child)
}
})
}
2019-03-01 20:51:13 +01:00
return (
<head {...this.props}>
{this.context._documentProps.isDevelopment && (
<>
<style
data-next-hide-fouc
data-ampdevmode={inAmpMode ? 'true' : undefined}
dangerouslySetInnerHTML={{
__html: `body{display:none}`,
}}
/>
<noscript
data-next-hide-fouc
data-ampdevmode={inAmpMode ? 'true' : undefined}
>
<style
dangerouslySetInnerHTML={{
__html: `body{display:block}`,
}}
/>
</noscript>
</>
)}
2019-03-01 20:51:13 +01:00
{children}
{head}
<meta
name="next-head-count"
content={React.Children.count(head || []).toString()}
/>
{inAmpMode && (
2019-03-01 20:51:13 +01:00
<>
<meta
name="viewport"
content="width=device-width,minimum-scale=1,initial-scale=1"
/>
{!hasCanonicalRel && (
<link
rel="canonical"
href={canonicalBase + cleanAmpPath(dangerousAsPath)}
/>
)}
2019-03-01 20:51:13 +01:00
{/* https://www.ampproject.org/docs/fundamentals/optimize_amp#optimize-the-amp-runtime-loading */}
<link
rel="preload"
as="script"
href="https://cdn.ampproject.org/v0.js"
/>
{/* Add custom styles before AMP styles to prevent accidental overrides */}
{styles && (
<style
amp-custom=""
dangerouslySetInnerHTML={{
__html: curStyles
2020-05-18 21:24:37 +02:00
.map((style) => style.props.dangerouslySetInnerHTML.__html)
.join('')
.replace(/\/\*# sourceMappingURL=.*\*\//g, '')
.replace(/\/\*@ sourceURL=.*?\*\//g, ''),
2019-03-01 20:51:13 +01:00
}}
/>
)}
<style
amp-boilerplate=""
dangerouslySetInnerHTML={{
__html: `body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}`,
}}
/>
<noscript>
<style
amp-boilerplate=""
dangerouslySetInnerHTML={{
__html: `body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}`,
}}
/>
</noscript>
<script async src="https://cdn.ampproject.org/v0.js" />
</>
)}
{!inAmpMode && (
2019-03-01 20:51:13 +01:00
<>
{!hasAmphtmlRel && hybridAmp && (
<link
rel="amphtml"
href={canonicalBase + getAmpPath(ampPath, dangerousAsPath)}
/>
)}
{this.getCssLinks()}
{!disableRuntimeJS && (
2019-03-01 20:51:13 +01:00
<link
rel="preload"
href={
assetPrefix +
getOptionalModernScriptVariant(
encodeURI(`/_next/static/${buildId}/pages/_app.js`)
) +
_devOnlyInvalidateCacheQueryString
}
2019-03-01 20:51:13 +01:00
as="script"
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
2019-03-01 20:51:13 +01:00
/>
)}
{!disableRuntimeJS && page !== '/_error' && (
<link
rel="preload"
href={
assetPrefix +
getOptionalModernScriptVariant(
encodeURI(
`/_next/static/${buildId}/pages${getPageFile(page)}`
)
) +
_devOnlyInvalidateCacheQueryString
}
as="script"
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
/>
)}
{!disableRuntimeJS && this.getPreloadDynamicChunks()}
{!disableRuntimeJS && this.getPreloadMainLinks()}
{this.context._documentProps.isDevelopment && (
// this element is used to mount development styles so the
// ordering matches production
// (by default, style-loader injects at the bottom of <head />)
<noscript id="__next_css__DO_NOT_USE__" />
)}
2019-03-01 20:51:13 +01:00
{styles || null}
</>
)}
Initial plugins implementation (#9139) * Add initial bit for plugins * Add checks for needed metadata values * Add test * Initial plugins changes * Add handling for _app middleware * Add loading of _document middleware and handling of multiple default export syntaxes * Fix insert order for middleware member expression * Remove early return from middleware plugin from testing * Add tests for current plugin middlewares * Update test plugin package.json * Update handling for class default export * Update to use webpack loader instead of babel plugin, remove redundant middleware naming, and add field for required env for plugins * Add middleware to support material-ui use case and example material-ui plugin * Update tests and remove tests stuff from google analytics plugin * Remove old plugin suite * Add init-server middleware * Exit hard without stack trace when error in collecting plugins * Add on-error-client and on-error-server and update to run init-server with next-start in serverless mode * Update init-client for google analytics plugin * Add example Sentry plugin and update with-sentry-simple * Remove middleware field/folder and use src dir for plugins * Add post-hydration middleware and update material-ui plugin * Put plugins code behind flag * Update chromedriver * Revert "Update chromedriver" This reverts commit 1461e978e677f7da05e29e0415ec614a04bf65f9. * Update lock file * Remove un-needed _app for sentry example * Add auto loading of scoped packages, add plugins config for manually listing plugins, and update to only collect plugins once * Update example plugins * Expose plugins' config * Rename plugin lifecycles and add babel-preset-build * Rename other methods with unstable * Update log when plugin config overrides auto-detecting
2019-11-01 20:13:13 +01:00
{React.createElement(React.Fragment, {}, ...(headTags || []))}
2019-03-01 20:51:13 +01:00
</head>
)
}
}
export class Main extends Component {
2019-10-14 18:45:56 +02:00
static contextType = DocumentComponentContext
2019-10-14 18:45:56 +02:00
context!: React.ContextType<typeof DocumentComponentContext>
2019-03-01 20:51:13 +01:00
render() {
const { inAmpMode, html } = this.context._documentProps
if (inAmpMode) return AMP_RENDER_TARGET
return <div id="__next" dangerouslySetInnerHTML={{ __html: html }} />
}
}
2019-04-26 09:37:57 +02:00
export class NextScript extends Component<OriginProps> {
2019-10-14 18:45:56 +02:00
static contextType = DocumentComponentContext
static propTypes = {
nonce: PropTypes.string,
2019-03-01 20:51:13 +01:00
crossOrigin: PropTypes.string,
}
2019-10-14 18:45:56 +02:00
context!: React.ContextType<typeof DocumentComponentContext>
// Source: https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
static safariNomoduleFix =
'!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();'
2019-03-01 20:51:13 +01:00
getDynamicChunks() {
const { dynamicImports, assetPrefix, files } = this.context._documentProps
const { _devOnlyInvalidateCacheQueryString } = this.context
2019-05-27 03:17:08 +02:00
return dedupe(dynamicImports).map((bundle: any) => {
let modernProps = {}
if (process.env.__NEXT_MODERN_BUILD) {
modernProps = /\.module\.js$/.test(bundle.file)
? { type: 'module' }
: { noModule: true }
}
if (!/\.js$/.test(bundle.file) || files.includes(bundle.file)) return null
2019-03-01 20:51:13 +01:00
return (
<script
async
2019-03-01 20:51:13 +01:00
key={bundle.file}
src={`${assetPrefix}/_next/${encodeURI(
2019-03-01 20:51:13 +01:00
bundle.file
)}${_devOnlyInvalidateCacheQueryString}`}
2019-03-01 20:51:13 +01:00
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
{...modernProps}
2019-03-01 20:51:13 +01:00
/>
)
})
}
2019-03-01 20:51:13 +01:00
getScripts() {
const { assetPrefix, files, buildManifest } = this.context._documentProps
const { _devOnlyInvalidateCacheQueryString } = this.context
2020-05-18 21:24:37 +02:00
const normalScripts = files?.filter((file) => file.endsWith('.js'))
const lowPriorityScripts = buildManifest.lowPriorityFiles?.filter((file) =>
file.endsWith('.js')
)
2020-05-18 21:24:37 +02:00
return [...normalScripts, ...lowPriorityScripts].map((file) => {
let modernProps = {}
if (process.env.__NEXT_MODERN_BUILD) {
modernProps = file.endsWith('.module.js')
? { type: 'module' }
: { noModule: true }
}
2019-03-01 20:51:13 +01:00
return (
<script
key={file}
src={`${assetPrefix}/_next/${encodeURI(
file
)}${_devOnlyInvalidateCacheQueryString}`}
2019-03-01 20:51:13 +01:00
nonce={this.props.nonce}
async
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
{...modernProps}
2019-03-01 20:51:13 +01:00
/>
)
})
}
getPolyfillScripts() {
// polyfills.js has to be rendered as nomodule without async
// It also has to be the first script to load
const { assetPrefix, buildManifest } = this.context._documentProps
const { _devOnlyInvalidateCacheQueryString } = this.context
return buildManifest.polyfillFiles
.filter(
2020-05-18 21:24:37 +02:00
(polyfill) =>
polyfill.endsWith('.js') && !/\.module\.js$/.test(polyfill)
)
2020-05-18 21:24:37 +02:00
.map((polyfill) => (
<script
key={polyfill}
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
noModule={true}
src={`${assetPrefix}/_next/${polyfill}${_devOnlyInvalidateCacheQueryString}`}
/>
))
}
static getInlineScriptSource(documentProps: DocumentProps): string {
2019-03-01 20:51:13 +01:00
const { __NEXT_DATA__ } = documentProps
try {
const data = JSON.stringify(__NEXT_DATA__)
return htmlEscapeJsonString(data)
2019-03-01 20:51:13 +01:00
} catch (err) {
if (err.message.indexOf('circular structure')) {
throw new Error(
`Circular structure in "getInitialProps" result of page "${__NEXT_DATA__.page}". https://err.sh/vercel/next.js/circular-structure`
2019-03-01 20:51:13 +01:00
)
}
throw err
}
}
2019-03-01 20:51:13 +01:00
render() {
const {
assetPrefix,
inAmpMode,
buildManifest,
2019-03-01 20:51:13 +01:00
__NEXT_DATA__,
unstable_runtimeJS,
2019-03-01 20:51:13 +01:00
} = this.context._documentProps
const disableRuntimeJS = unstable_runtimeJS === false
const { _devOnlyInvalidateCacheQueryString } = this.context
if (inAmpMode) {
if (process.env.NODE_ENV === 'production') {
return null
}
const AmpDevFiles = [
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
CLIENT_STATIC_FILES_RUNTIME_AMP,
CLIENT_STATIC_FILES_RUNTIME_WEBPACK,
]
return (
<>
{disableRuntimeJS ? null : (
<script
id="__NEXT_DATA__"
type="application/json"
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
dangerouslySetInnerHTML={{
__html: NextScript.getInlineScriptSource(
this.context._documentProps
),
}}
data-ampdevmode
/>
)}
{AmpDevFiles
? AmpDevFiles.map((file) => (
<script
key={file}
src={`${assetPrefix}/_next/${file}${_devOnlyInvalidateCacheQueryString}`}
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
data-ampdevmode
/>
))
: null}
</>
)
}
const { page, buildId } = __NEXT_DATA__
if (process.env.NODE_ENV !== 'production') {
2019-03-01 20:51:13 +01:00
if (this.props.crossOrigin)
console.warn(
'Warning: `NextScript` attribute `crossOrigin` is deprecated. https://err.sh/next.js/doc-crossorigin-deprecated'
2019-03-01 20:51:13 +01:00
)
}
const pageScript = [
<script
async
data-next-page={page}
key={page}
src={
assetPrefix +
encodeURI(`/_next/static/${buildId}/pages${getPageFile(page)}`) +
_devOnlyInvalidateCacheQueryString
}
nonce={this.props.nonce}
crossOrigin={this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN}
{...(process.env.__NEXT_MODERN_BUILD ? { noModule: true } : {})}
/>,
process.env.__NEXT_MODERN_BUILD && (
<script
async
data-next-page={page}
key={`${page}-modern`}
src={
assetPrefix +
getOptionalModernScriptVariant(
encodeURI(`/_next/static/${buildId}/pages${getPageFile(page)}`)
) +
_devOnlyInvalidateCacheQueryString
}
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
type="module"
/>
),
]
const appScript = [
<script
async
data-next-page="/_app"
src={
assetPrefix +
`/_next/static/${buildId}/pages/_app.js` +
_devOnlyInvalidateCacheQueryString
}
key="_app"
nonce={this.props.nonce}
crossOrigin={this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN}
{...(process.env.__NEXT_MODERN_BUILD ? { noModule: true } : {})}
/>,
process.env.__NEXT_MODERN_BUILD && (
<script
async
data-next-page="/_app"
src={
assetPrefix +
`/_next/static/${buildId}/pages/_app.module.js` +
_devOnlyInvalidateCacheQueryString
}
key="_app-modern"
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
type="module"
/>
),
]
2019-03-01 20:51:13 +01:00
return (
<>
{!disableRuntimeJS && buildManifest.devFiles
? buildManifest.devFiles.map(
(file: string) =>
!file.match(/\.js\.map/) && (
<script
key={file}
src={`${assetPrefix}/_next/${encodeURI(
file
)}${_devOnlyInvalidateCacheQueryString}`}
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
/>
)
)
2019-03-01 20:51:13 +01:00
: null}
{disableRuntimeJS ? null : (
2019-03-01 20:51:13 +01:00
<script
id="__NEXT_DATA__"
type="application/json"
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
2019-03-01 20:51:13 +01:00
dangerouslySetInnerHTML={{
__html: NextScript.getInlineScriptSource(
this.context._documentProps
2019-03-01 20:51:13 +01:00
),
}}
/>
)}
{process.env.__NEXT_MODERN_BUILD && !disableRuntimeJS ? (
2019-03-01 20:51:13 +01:00
<script
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
noModule={true}
dangerouslySetInnerHTML={{
__html: NextScript.safariNomoduleFix,
}}
2019-03-01 20:51:13 +01:00
/>
) : null}
{!disableRuntimeJS && this.getPolyfillScripts()}
{!disableRuntimeJS && appScript}
{!disableRuntimeJS && page !== '/_error' && pageScript}
{disableRuntimeJS ? null : this.getDynamicChunks()}
{disableRuntimeJS ? null : this.getScripts()}
2019-03-01 20:51:13 +01:00
</>
)
}
}
function getAmpPath(ampPath: string, asPath: string): string {
return ampPath || `${asPath}${asPath.includes('?') ? '&' : '?'}amp=1`
}
function getPageFile(page: string, buildId?: string): string {
Fix pages/index.js and pages/index/index.js behavior (#13699) Disambiguate between pages/index.js and pages/index/index.js so that they resolve differently. It all started with a bug in pagesmanifest that propagated throughout the codebase. After fixing pagesmanifest I was able to remove a few hacks here and there and more logic is shared now. especially the logic that resolves an entrypoint back into a route path. To sum up what happened: - `getRouteFromEntrypoint` is the inverse operation of `getPageFile` that's under `pages/_document.tsx` - `denormalizePagePath` is the inverse operation of `normalizePagePath`. Everything is refactored in terms of these operations, that makes their behavior uniform and easier to update/patch in a central place. Before there were subtle differences between those that made `index/index.js` hard to handle. Some potential follow up on this PR: - [`hot-reloader`](https://github.com/vercel/next.js/pull/13699/files#diff-6161346d2c5f4b7abc87059d8768c44bR207) still has one place that does very similar behavior to `getRouteFromEntrypoint`. It can probably be rewritten in terms of `getRouteFromEntrypoint`. - There are a few places where `denormalizePagePath(normalizePagePath(...))` is happening. This is a sign that `normalizePagePath` is doing some validation that is independent of its rewriting logic. That should probably be factored out in its own function. after that I should probably investigate whether `normalizePagePath` is even still needed at all. - a lot of code is doing `.replace(/\\/g, '')`. If wanted, that could be replaced with `normalizePathSep`. - It looks to me like some logic that's spread across the project can be centralized in 4 functions - `getRouteFromEntrypoint` (part of this PR) - its inverse `getEntrypointFromRoute` (already exists in `_document.tsx` as `getPageFile`) - `getRouteFromPageFile` - its inverse `getPageFileFromRoute` (already exists as `findPageFile ` in `server/lib/find-page-file.ts`) It could be beneficial to structure the code to keep these fuctionalities close together and name them similarly. - revise `index.amp` handling in pagesmanifest. I left it alone in this PR to keep it scoped, but it may be broken wrt nested index files as well. It might even make sense to reshape the pagesmanifest altogether to handle html/json/amp/... better
2020-06-04 19:32:45 +02:00
const startingUrl =
page === '/'
? '/index'
: /^\/index(\/|$)/.test(page)
? `/index${page}`
: page
return buildId ? `${startingUrl}.${buildId}.js` : `${startingUrl}.js`
}