mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
[experimental] Rendering to AMP (#6218)
* Add initial AMP implementation * Implement experimental feature flag * Implement feedback from sbenz * Add next/amp and `useAmp` hook * Use /:path*/amp instead * Add canonical * Add amphtml tag * Add ampEnabled for rel=“amphtml” * Remove extra type
This commit is contained in:
parent
36946f9709
commit
4051ffcb01
|
@ -60,6 +60,7 @@
|
||||||
"@zeit/next-css": "1.0.2-canary.2",
|
"@zeit/next-css": "1.0.2-canary.2",
|
||||||
"@zeit/next-sass": "1.0.2-canary.2",
|
"@zeit/next-sass": "1.0.2-canary.2",
|
||||||
"@zeit/next-typescript": "1.1.2-canary.0",
|
"@zeit/next-typescript": "1.1.2-canary.0",
|
||||||
|
"amphtml-validator": "1.0.23",
|
||||||
"babel-core": "7.0.0-bridge.0",
|
"babel-core": "7.0.0-bridge.0",
|
||||||
"babel-eslint": "9.0.0",
|
"babel-eslint": "9.0.0",
|
||||||
"babel-jest": "23.6.0",
|
"babel-jest": "23.6.0",
|
||||||
|
@ -83,8 +84,8 @@
|
||||||
"node-sass": "4.9.2",
|
"node-sass": "4.9.2",
|
||||||
"pre-commit": "1.2.2",
|
"pre-commit": "1.2.2",
|
||||||
"prettier": "1.15.3",
|
"prettier": "1.15.3",
|
||||||
"react": "16.6.3",
|
"react": "16.8.0",
|
||||||
"react-dom": "16.6.3",
|
"react-dom": "16.8.0",
|
||||||
"release": "5.0.3",
|
"release": "5.0.3",
|
||||||
"request-promise-core": "1.1.1",
|
"request-promise-core": "1.1.1",
|
||||||
"rimraf": "2.6.2",
|
"rimraf": "2.6.2",
|
||||||
|
|
1
packages/next-server/amp.js
Normal file
1
packages/next-server/amp.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('./dist/lib/amp')
|
6
packages/next-server/lib/amp.ts
Normal file
6
packages/next-server/lib/amp.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {IsAmpContext} from './amphtml-context'
|
||||||
|
|
||||||
|
export function useAmp() {
|
||||||
|
return React.useContext(IsAmpContext)
|
||||||
|
}
|
3
packages/next-server/lib/amphtml-context.ts
Normal file
3
packages/next-server/lib/amphtml-context.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export const IsAmpContext: React.Context<any> = React.createContext(false)
|
|
@ -12,7 +12,8 @@
|
||||||
"head.js",
|
"head.js",
|
||||||
"link.js",
|
"link.js",
|
||||||
"router.js",
|
"router.js",
|
||||||
"next-config.js"
|
"next-config.js",
|
||||||
|
"amp.js"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "taskr",
|
"build": "taskr",
|
||||||
|
|
|
@ -21,6 +21,9 @@ const defaultConfig = {
|
||||||
websocketPort: 0,
|
websocketPort: 0,
|
||||||
websocketProxyPath: '/',
|
websocketProxyPath: '/',
|
||||||
websocketProxyPort: null
|
websocketProxyPort: null
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
amp: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +50,12 @@ export default function loadConfig (phase, dir, customConfig) {
|
||||||
if (userConfig.target && !targets.includes(userConfig.target)) {
|
if (userConfig.target && !targets.includes(userConfig.target)) {
|
||||||
throw new Error(`Specified target is invalid. Provided: "${userConfig.target}" should be one of ${targets.join(', ')}`)
|
throw new Error(`Specified target is invalid. Provided: "${userConfig.target}" should be one of ${targets.join(', ')}`)
|
||||||
}
|
}
|
||||||
|
if (userConfig.experimental) {
|
||||||
|
userConfig.experimental = {
|
||||||
|
...defaultConfig.experimental,
|
||||||
|
...userConfig.experimental
|
||||||
|
}
|
||||||
|
}
|
||||||
if (userConfig.onDemandEntries) {
|
if (userConfig.onDemandEntries) {
|
||||||
userConfig.onDemandEntries = {
|
userConfig.onDemandEntries = {
|
||||||
...defaultConfig.onDemandEntries,
|
...defaultConfig.onDemandEntries,
|
||||||
|
|
|
@ -30,6 +30,7 @@ export default class Server {
|
||||||
distDir: string
|
distDir: string
|
||||||
buildId: string
|
buildId: string
|
||||||
renderOpts: {
|
renderOpts: {
|
||||||
|
ampEnabled: boolean,
|
||||||
staticMarkup: boolean,
|
staticMarkup: boolean,
|
||||||
buildId: string,
|
buildId: string,
|
||||||
generateEtags: boolean,
|
generateEtags: boolean,
|
||||||
|
@ -53,6 +54,7 @@ export default class Server {
|
||||||
|
|
||||||
this.buildId = this.readBuildId()
|
this.buildId = this.readBuildId()
|
||||||
this.renderOpts = {
|
this.renderOpts = {
|
||||||
|
ampEnabled: this.nextConfig.experimental.amp,
|
||||||
staticMarkup,
|
staticMarkup,
|
||||||
buildId: this.buildId,
|
buildId: this.buildId,
|
||||||
generateEtags,
|
generateEtags,
|
||||||
|
@ -160,6 +162,29 @@ export default class Server {
|
||||||
]
|
]
|
||||||
|
|
||||||
if (this.nextConfig.useFileSystemPublicRoutes) {
|
if (this.nextConfig.useFileSystemPublicRoutes) {
|
||||||
|
if (this.nextConfig.experimental.amp) {
|
||||||
|
// It's very important to keep this route's param optional.
|
||||||
|
// (but it should support as many params as needed, separated by '/')
|
||||||
|
// Otherwise this will lead to a pretty simple DOS attack.
|
||||||
|
// See more: https://github.com/zeit/next.js/issues/2617
|
||||||
|
routes.push({
|
||||||
|
match: route('/:path*/amp'),
|
||||||
|
fn: async (req, res, params, parsedUrl) => {
|
||||||
|
let pathname
|
||||||
|
if (!params.path) {
|
||||||
|
pathname = '/'
|
||||||
|
} else {
|
||||||
|
pathname = '/' + params.path.join('/')
|
||||||
|
}
|
||||||
|
const { query } = parsedUrl
|
||||||
|
if (!pathname) {
|
||||||
|
throw new Error('pathname is undefined')
|
||||||
|
}
|
||||||
|
await this.renderToAMP(req, res, pathname, query, parsedUrl)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// It's very important to keep this route's param optional.
|
// It's very important to keep this route's param optional.
|
||||||
// (but it should support as many params as needed, separated by '/')
|
// (but it should support as many params as needed, separated by '/')
|
||||||
// Otherwise this will lead to a pretty simple DOS attack.
|
// Otherwise this will lead to a pretty simple DOS attack.
|
||||||
|
@ -229,15 +254,47 @@ export default class Server {
|
||||||
return this.sendHTML(req, res, html)
|
return this.sendHTML(req, res, html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async renderToAMP(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}, parsedUrl?: UrlWithParsedQuery): Promise<void> {
|
||||||
|
if (!this.nextConfig.experimental.amp) {
|
||||||
|
throw new Error('"experimental.amp" is not enabled in "next.config.js"')
|
||||||
|
}
|
||||||
|
const url: any = req.url
|
||||||
|
if (isInternalUrl(url)) {
|
||||||
|
return this.handleRequest(req, res, parsedUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBlockedPage(pathname)) {
|
||||||
|
return this.render404(req, res, parsedUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await this.renderToAMPHTML(req, res, pathname, query)
|
||||||
|
// Request was ended by the user
|
||||||
|
if (html === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.nextConfig.poweredByHeader) {
|
||||||
|
res.setHeader('X-Powered-By', 'Next.js ' + process.env.NEXT_VERSION)
|
||||||
|
}
|
||||||
|
return this.sendHTML(req, res, html)
|
||||||
|
}
|
||||||
|
|
||||||
private async renderToHTMLWithComponents(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}, opts: any) {
|
private async renderToHTMLWithComponents(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}, opts: any) {
|
||||||
const result = await loadComponents(this.distDir, this.buildId, pathname)
|
const result = await loadComponents(this.distDir, this.buildId, pathname)
|
||||||
return renderToHTML(req, res, pathname, query, {...result, ...opts})
|
return renderToHTML(req, res, pathname, query, {...result, ...opts})
|
||||||
}
|
}
|
||||||
|
|
||||||
public async renderToHTML(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}): Promise<string|null> {
|
public async renderToAMPHTML(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}): Promise<string|null> {
|
||||||
|
if (!this.nextConfig.experimental.amp) {
|
||||||
|
throw new Error('"experimental.amp" is not enabled in "next.config.js"')
|
||||||
|
}
|
||||||
|
return this.renderToHTML(req, res, pathname, query, {amphtml: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async renderToHTML(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}, {amphtml}: {amphtml?: boolean} = {}): Promise<string|null> {
|
||||||
try {
|
try {
|
||||||
// To make sure the try/catch is executed
|
// To make sure the try/catch is executed
|
||||||
const html = await this.renderToHTMLWithComponents(req, res, pathname, query, this.renderOpts)
|
const html = await this.renderToHTMLWithComponents(req, res, pathname, query, {...this.renderOpts, amphtml})
|
||||||
return html
|
return html
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {IncomingMessage, ServerResponse} from 'http'
|
import { IncomingMessage, ServerResponse } from 'http'
|
||||||
import { ParsedUrlQuery } from 'querystring'
|
import { ParsedUrlQuery } from 'querystring'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
|
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
|
||||||
|
@ -7,15 +7,26 @@ import { loadGetInitialProps, isResSent } from '../lib/utils'
|
||||||
import Head, { defaultHead } from '../lib/head'
|
import Head, { defaultHead } from '../lib/head'
|
||||||
import Loadable from '../lib/loadable'
|
import Loadable from '../lib/loadable'
|
||||||
import LoadableCapture from '../lib/loadable-capture'
|
import LoadableCapture from '../lib/loadable-capture'
|
||||||
import {getDynamicImportBundles, Manifest as ReactLoadableManifest, ManifestItem} from './get-dynamic-import-bundles'
|
import {
|
||||||
import {getPageFiles, BuildManifest} from './get-page-files'
|
getDynamicImportBundles,
|
||||||
|
Manifest as ReactLoadableManifest,
|
||||||
|
ManifestItem,
|
||||||
|
} from './get-dynamic-import-bundles'
|
||||||
|
import { getPageFiles, BuildManifest } from './get-page-files'
|
||||||
|
import { IsAmpContext } from '../lib/amphtml-context'
|
||||||
|
|
||||||
type Enhancer = (Component: React.ComponentType) => React.ComponentType
|
type Enhancer = (Component: React.ComponentType) => React.ComponentType
|
||||||
type ComponentsEnhancer = {enhanceApp?: Enhancer, enhanceComponent?: Enhancer}|Enhancer
|
type ComponentsEnhancer =
|
||||||
|
| { enhanceApp?: Enhancer; enhanceComponent?: Enhancer }
|
||||||
|
| Enhancer
|
||||||
|
|
||||||
function enhanceComponents(options: ComponentsEnhancer, App: React.ComponentType, Component: React.ComponentType): {
|
function enhanceComponents(
|
||||||
|
options: ComponentsEnhancer,
|
||||||
App: React.ComponentType,
|
App: React.ComponentType,
|
||||||
Component: React.ComponentType,
|
Component: React.ComponentType,
|
||||||
|
): {
|
||||||
|
App: React.ComponentType
|
||||||
|
Component: React.ComponentType,
|
||||||
} {
|
} {
|
||||||
// For backwards compatibility
|
// For backwards compatibility
|
||||||
if (typeof options === 'function') {
|
if (typeof options === 'function') {
|
||||||
|
@ -27,11 +38,16 @@ function enhanceComponents(options: ComponentsEnhancer, App: React.ComponentType
|
||||||
|
|
||||||
return {
|
return {
|
||||||
App: options.enhanceApp ? options.enhanceApp(App) : App,
|
App: options.enhanceApp ? options.enhanceApp(App) : App,
|
||||||
Component: options.enhanceComponent ? options.enhanceComponent(Component) : Component,
|
Component: options.enhanceComponent
|
||||||
|
? options.enhanceComponent(Component)
|
||||||
|
: Component,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(renderElementToString: (element: React.ReactElement<any>) => string, element: React.ReactElement<any>): {html: string, head: any} {
|
function render(
|
||||||
|
renderElementToString: (element: React.ReactElement<any>) => string,
|
||||||
|
element: React.ReactElement<any>,
|
||||||
|
): { html: string; head: any } {
|
||||||
let html
|
let html
|
||||||
let head
|
let head
|
||||||
|
|
||||||
|
@ -45,75 +61,101 @@ function render(renderElementToString: (element: React.ReactElement<any>) => str
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderOpts = {
|
type RenderOpts = {
|
||||||
staticMarkup: boolean,
|
ampEnabled: boolean
|
||||||
buildId: string,
|
staticMarkup: boolean
|
||||||
runtimeConfig?: {[key: string]: any},
|
buildId: string
|
||||||
assetPrefix?: string,
|
runtimeConfig?: { [key: string]: any }
|
||||||
err?: Error|null,
|
assetPrefix?: string
|
||||||
nextExport?: boolean,
|
err?: Error | null
|
||||||
dev?: boolean,
|
nextExport?: boolean
|
||||||
buildManifest: BuildManifest,
|
dev?: boolean
|
||||||
reactLoadableManifest: ReactLoadableManifest,
|
amphtml?: boolean
|
||||||
Component: React.ComponentType,
|
buildManifest: BuildManifest
|
||||||
Document: React.ComponentType,
|
reactLoadableManifest: ReactLoadableManifest
|
||||||
App: React.ComponentType,
|
Component: React.ComponentType
|
||||||
ErrorDebug?: React.ComponentType<{error: Error}>,
|
Document: React.ComponentType
|
||||||
|
App: React.ComponentType
|
||||||
|
ErrorDebug?: React.ComponentType<{ error: Error }>,
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDocument(Document: React.ComponentType, {
|
function renderDocument(
|
||||||
props,
|
Document: React.ComponentType,
|
||||||
docProps,
|
{
|
||||||
pathname,
|
ampEnabled = false,
|
||||||
query,
|
props,
|
||||||
buildId,
|
docProps,
|
||||||
assetPrefix,
|
pathname,
|
||||||
runtimeConfig,
|
asPath,
|
||||||
nextExport,
|
query,
|
||||||
dynamicImportsIds,
|
buildId,
|
||||||
err,
|
assetPrefix,
|
||||||
dev,
|
runtimeConfig,
|
||||||
staticMarkup,
|
nextExport,
|
||||||
devFiles,
|
dynamicImportsIds,
|
||||||
files,
|
err,
|
||||||
dynamicImports,
|
dev,
|
||||||
}: RenderOpts & {
|
amphtml,
|
||||||
props: any,
|
staticMarkup,
|
||||||
docProps: any,
|
devFiles,
|
||||||
pathname: string,
|
files,
|
||||||
query: ParsedUrlQuery,
|
dynamicImports,
|
||||||
dynamicImportsIds: string[],
|
}: RenderOpts & {
|
||||||
dynamicImports: ManifestItem[],
|
props: any
|
||||||
files: string[]
|
docProps: any
|
||||||
devFiles: string[],
|
pathname: string
|
||||||
}): string {
|
asPath: string | undefined
|
||||||
return '<!DOCTYPE html>' + renderToStaticMarkup(
|
query: ParsedUrlQuery
|
||||||
<Document
|
amphtml: boolean
|
||||||
__NEXT_DATA__={{
|
dynamicImportsIds: string[]
|
||||||
props, // The result of getInitialProps
|
dynamicImports: ManifestItem[]
|
||||||
page: pathname, // The rendered page
|
files: string[]
|
||||||
query, // querystring parsed / passed by the user
|
devFiles: string[],
|
||||||
buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
|
},
|
||||||
assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
|
): string {
|
||||||
runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML
|
return (
|
||||||
nextExport, // If this is a page exported by `next export`
|
'<!DOCTYPE html>' +
|
||||||
dynamicIds: dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
|
renderToStaticMarkup(
|
||||||
err: (err) ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
|
<IsAmpContext.Provider value={amphtml}>
|
||||||
}}
|
<Document
|
||||||
staticMarkup={staticMarkup}
|
__NEXT_DATA__={{
|
||||||
devFiles={devFiles}
|
props, // The result of getInitialProps
|
||||||
files={files}
|
page: pathname, // The rendered page
|
||||||
dynamicImports={dynamicImports}
|
query, // querystring parsed / passed by the user
|
||||||
assetPrefix={assetPrefix}
|
buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
|
||||||
{...docProps}
|
assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
|
||||||
/>,
|
runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML
|
||||||
|
nextExport, // If this is a page exported by `next export`
|
||||||
|
dynamicIds:
|
||||||
|
dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
|
||||||
|
err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
|
||||||
|
}}
|
||||||
|
ampEnabled={ampEnabled}
|
||||||
|
asPath={encodeURI(asPath || '')}
|
||||||
|
amphtml={amphtml}
|
||||||
|
staticMarkup={staticMarkup}
|
||||||
|
devFiles={devFiles}
|
||||||
|
files={files}
|
||||||
|
dynamicImports={dynamicImports}
|
||||||
|
assetPrefix={assetPrefix}
|
||||||
|
{...docProps}
|
||||||
|
/>
|
||||||
|
</IsAmpContext.Provider>,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderToHTML(req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery, renderOpts: RenderOpts): Promise<string|null> {
|
export async function renderToHTML(
|
||||||
|
req: IncomingMessage,
|
||||||
|
res: ServerResponse,
|
||||||
|
pathname: string,
|
||||||
|
query: ParsedUrlQuery,
|
||||||
|
renderOpts: RenderOpts,
|
||||||
|
): Promise<string | null> {
|
||||||
const {
|
const {
|
||||||
err,
|
err,
|
||||||
dev = false,
|
dev = false,
|
||||||
staticMarkup = false,
|
staticMarkup = false,
|
||||||
|
amphtml = false,
|
||||||
App,
|
App,
|
||||||
Document,
|
Document,
|
||||||
Component,
|
Component,
|
||||||
|
@ -127,22 +169,28 @@ export async function renderToHTML(req: IncomingMessage, res: ServerResponse, pa
|
||||||
if (dev) {
|
if (dev) {
|
||||||
const { isValidElementType } = require('react-is')
|
const { isValidElementType } = require('react-is')
|
||||||
if (!isValidElementType(Component)) {
|
if (!isValidElementType(Component)) {
|
||||||
throw new Error(`The default export is not a React Component in page: "${pathname}"`)
|
throw new Error(
|
||||||
|
`The default export is not a React Component in page: "${pathname}"`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidElementType(App)) {
|
if (!isValidElementType(App)) {
|
||||||
throw new Error(`The default export is not a React Component in page: "/_app"`)
|
throw new Error(
|
||||||
|
`The default export is not a React Component in page: "/_app"`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidElementType(Document)) {
|
if (!isValidElementType(Document)) {
|
||||||
throw new Error(`The default export is not a React Component in page: "/_document"`)
|
throw new Error(
|
||||||
|
`The default export is not a React Component in page: "/_document"`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const asPath = req.url
|
const asPath = req.url
|
||||||
const ctx = { err, req, res, pathname, query, asPath }
|
const ctx = { err, req, res, pathname, query, asPath }
|
||||||
const router = new Router(pathname, query, asPath)
|
const router = new Router(pathname, query, asPath)
|
||||||
const props = await loadGetInitialProps(App, {Component, router, ctx})
|
const props = await loadGetInitialProps(App, { Component, router, ctx })
|
||||||
|
|
||||||
// the response might be finished on the getInitialProps call
|
// the response might be finished on the getInitialProps call
|
||||||
if (isResSent(res)) return null
|
if (isResSent(res)) return null
|
||||||
|
@ -156,23 +204,35 @@ export async function renderToHTML(req: IncomingMessage, res: ServerResponse, pa
|
||||||
]
|
]
|
||||||
|
|
||||||
const reactLoadableModules: string[] = []
|
const reactLoadableModules: string[] = []
|
||||||
const renderPage = (options: ComponentsEnhancer = {}): {html: string, head: any} => {
|
const renderPage = (
|
||||||
const renderElementToString = staticMarkup ? renderToStaticMarkup : renderToString
|
options: ComponentsEnhancer = {},
|
||||||
|
): { html: string; head: any } => {
|
||||||
|
const renderElementToString = staticMarkup
|
||||||
|
? renderToStaticMarkup
|
||||||
|
: renderToString
|
||||||
|
|
||||||
if (err && ErrorDebug) {
|
if (err && ErrorDebug) {
|
||||||
return render(renderElementToString, <ErrorDebug error={err} />)
|
return render(renderElementToString, <ErrorDebug error={err} />)
|
||||||
}
|
}
|
||||||
|
|
||||||
const {App: EnhancedApp, Component: EnhancedComponent} = enhanceComponents(options, App, Component)
|
const {
|
||||||
|
App: EnhancedApp,
|
||||||
|
Component: EnhancedComponent,
|
||||||
|
} = enhanceComponents(options, App, Component)
|
||||||
|
|
||||||
return render(renderElementToString,
|
return render(
|
||||||
<LoadableCapture report={(moduleName) => reactLoadableModules.push(moduleName)}>
|
renderElementToString,
|
||||||
<EnhancedApp
|
<IsAmpContext.Provider value={amphtml}>
|
||||||
Component={EnhancedComponent}
|
<LoadableCapture
|
||||||
router={router}
|
report={(moduleName) => reactLoadableModules.push(moduleName)}
|
||||||
{...props}
|
>
|
||||||
/>
|
<EnhancedApp
|
||||||
</LoadableCapture>,
|
Component={EnhancedComponent}
|
||||||
|
router={router}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</LoadableCapture>
|
||||||
|
</IsAmpContext.Provider>,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,14 +240,18 @@ export async function renderToHTML(req: IncomingMessage, res: ServerResponse, pa
|
||||||
// the response might be finished on the getInitialProps call
|
// the response might be finished on the getInitialProps call
|
||||||
if (isResSent(res)) return null
|
if (isResSent(res)) return null
|
||||||
|
|
||||||
const dynamicImports = [...getDynamicImportBundles(reactLoadableManifest, reactLoadableModules)]
|
const dynamicImports = [
|
||||||
|
...getDynamicImportBundles(reactLoadableManifest, reactLoadableModules),
|
||||||
|
]
|
||||||
const dynamicImportsIds: any = dynamicImports.map((bundle) => bundle.id)
|
const dynamicImportsIds: any = dynamicImports.map((bundle) => bundle.id)
|
||||||
|
|
||||||
return renderDocument(Document, {
|
return renderDocument(Document, {
|
||||||
...renderOpts,
|
...renderOpts,
|
||||||
props,
|
props,
|
||||||
docProps,
|
docProps,
|
||||||
|
asPath,
|
||||||
pathname,
|
pathname,
|
||||||
|
amphtml,
|
||||||
query,
|
query,
|
||||||
dynamicImportsIds,
|
dynamicImportsIds,
|
||||||
dynamicImports,
|
dynamicImports,
|
||||||
|
@ -201,10 +265,17 @@ function errorToJSON(err: Error): Error {
|
||||||
return { name, message, stack }
|
return { name, message, stack }
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeError(dev: boolean|undefined, err: Error): Error & {statusCode?: number} {
|
function serializeError(
|
||||||
|
dev: boolean | undefined,
|
||||||
|
err: Error,
|
||||||
|
): Error & { statusCode?: number } {
|
||||||
if (dev) {
|
if (dev) {
|
||||||
return errorToJSON(err)
|
return errorToJSON(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { name: 'Internal Server Error.', message: '500 - Internal Server Error.', statusCode: 500 }
|
return {
|
||||||
|
name: 'Internal Server Error.',
|
||||||
|
message: '500 - Internal Server Error.',
|
||||||
|
statusCode: 500,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import pathMatch from './lib/path-match'
|
||||||
|
|
||||||
export const route = pathMatch()
|
export const route = pathMatch()
|
||||||
|
|
||||||
type Params = {[param: string]: string}
|
type Params = {[param: string]: any}
|
||||||
|
|
||||||
export type Route = {
|
export type Route = {
|
||||||
match: (pathname: string|undefined) => false|Params,
|
match: (pathname: string|undefined) => false|Params,
|
||||||
|
|
1
packages/next/amp.js
Normal file
1
packages/next/amp.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('next-server/amp')
|
|
@ -19,7 +19,8 @@
|
||||||
"error.js",
|
"error.js",
|
||||||
"head.js",
|
"head.js",
|
||||||
"link.js",
|
"link.js",
|
||||||
"router.js"
|
"router.js",
|
||||||
|
"amp.js"
|
||||||
],
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"next": "./dist/bin/next"
|
"next": "./dist/bin/next"
|
||||||
|
|
|
@ -4,10 +4,6 @@ import PropTypes from 'prop-types'
|
||||||
import {htmlEscapeJsonString} from '../server/htmlescape'
|
import {htmlEscapeJsonString} from '../server/htmlescape'
|
||||||
import flush from 'styled-jsx/server'
|
import flush from 'styled-jsx/server'
|
||||||
|
|
||||||
const Fragment = React.Fragment || function Fragment ({ children }) {
|
|
||||||
return <div>{children}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Document extends Component {
|
export default class Document extends Component {
|
||||||
static childContextTypes = {
|
static childContextTypes = {
|
||||||
_documentProps: PropTypes.any,
|
_documentProps: PropTypes.any,
|
||||||
|
@ -31,7 +27,7 @@ export default class Document extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return <html>
|
return <html amp={this.props.amphtml ? '' : null}>
|
||||||
<Head />
|
<Head />
|
||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
|
@ -115,10 +111,9 @@ export class Head extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { head, styles, assetPrefix, __NEXT_DATA__ } = this.context._documentProps
|
const { asPath, ampEnabled, head, styles, amphtml, assetPrefix, __NEXT_DATA__ } = this.context._documentProps
|
||||||
const { _devOnlyInvalidateCacheQueryString } = this.context
|
const { _devOnlyInvalidateCacheQueryString } = this.context
|
||||||
const { page, buildId } = __NEXT_DATA__
|
const { page, buildId } = __NEXT_DATA__
|
||||||
const pagePathname = getPagePathname(page)
|
|
||||||
|
|
||||||
let children = this.props.children
|
let children = this.props.children
|
||||||
// show a warning if Head contains <title> (only in development)
|
// show a warning if Head contains <title> (only in development)
|
||||||
|
@ -134,12 +129,26 @@ export class Head extends Component {
|
||||||
return <head {...this.props}>
|
return <head {...this.props}>
|
||||||
{children}
|
{children}
|
||||||
{head}
|
{head}
|
||||||
{page !== '/_error' && <link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages${pagePathname}${_devOnlyInvalidateCacheQueryString}`} as='script' nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />}
|
{amphtml && <>
|
||||||
|
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1"/>
|
||||||
|
<link rel="canonical" href={asPath === '/amp' ? '/' : asPath.replace(/\/amp$/, '')} />
|
||||||
|
{/* 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: styles.map((style) => style.props.dangerouslySetInnerHTML.__html)}} />}
|
||||||
|
<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}}`}}></style>
|
||||||
|
<noscript><style amp-boilerplate="" dangerouslySetInnerHTML={{__html: `body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}`}}></style></noscript>
|
||||||
|
<script async src="https://cdn.ampproject.org/v0.js"></script>
|
||||||
|
</>}
|
||||||
|
{!amphtml && <>
|
||||||
|
{ampEnabled && <link rel="amphtml" href={asPath === '/' ? '/amp' : (asPath.replace(/\/$/, '') + '/amp')} />}
|
||||||
|
{page !== '/_error' && <link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages${getPagePathname(page)}${_devOnlyInvalidateCacheQueryString}`} as='script' nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />}
|
||||||
<link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages/_app.js${_devOnlyInvalidateCacheQueryString}`} as='script' nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />
|
<link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages/_app.js${_devOnlyInvalidateCacheQueryString}`} as='script' nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />
|
||||||
{this.getPreloadDynamicChunks()}
|
{this.getPreloadDynamicChunks()}
|
||||||
{this.getPreloadMainLinks()}
|
{this.getPreloadMainLinks()}
|
||||||
{this.getCssLinks()}
|
{this.getCssLinks()}
|
||||||
{styles || null}
|
{styles || null}
|
||||||
|
</>}
|
||||||
</head>
|
</head>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -221,25 +230,29 @@ export class NextScript extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { staticMarkup, assetPrefix, devFiles, __NEXT_DATA__ } = this.context._documentProps
|
const { staticMarkup, assetPrefix, amphtml, devFiles, __NEXT_DATA__ } = this.context._documentProps
|
||||||
const { _devOnlyInvalidateCacheQueryString } = this.context
|
const { _devOnlyInvalidateCacheQueryString } = this.context
|
||||||
|
|
||||||
|
if(amphtml) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const { page, buildId } = __NEXT_DATA__
|
const { page, buildId } = __NEXT_DATA__
|
||||||
const pagePathname = getPagePathname(page)
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
if (this.props.crossOrigin) console.warn('Warning: `NextScript` attribute `crossOrigin` is deprecated. https://err.sh/next.js/doc-crossorigin-deprecated')
|
if (this.props.crossOrigin) console.warn('Warning: `NextScript` attribute `crossOrigin` is deprecated. https://err.sh/next.js/doc-crossorigin-deprecated')
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Fragment>
|
return <>
|
||||||
{devFiles ? devFiles.map((file) => <script key={file} src={`${assetPrefix}/_next/${file}${_devOnlyInvalidateCacheQueryString}`} nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />) : null}
|
{devFiles ? devFiles.map((file) => <script key={file} src={`${assetPrefix}/_next/${file}${_devOnlyInvalidateCacheQueryString}`} nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />) : null}
|
||||||
{staticMarkup ? null : <script id="__NEXT_DATA__" type="application/json" nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} dangerouslySetInnerHTML={{
|
{staticMarkup ? null : <script id="__NEXT_DATA__" type="application/json" nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} dangerouslySetInnerHTML={{
|
||||||
__html: NextScript.getInlineScriptSource(this.context._documentProps)
|
__html: NextScript.getInlineScriptSource(this.context._documentProps)
|
||||||
}} />}
|
}} />}
|
||||||
{page !== '/_error' && <script async id={`__NEXT_PAGE__${page}`} src={`${assetPrefix}/_next/static/${buildId}/pages${pagePathname}${_devOnlyInvalidateCacheQueryString}`} nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />}
|
{page !== '/_error' && <script async id={`__NEXT_PAGE__${page}`} src={`${assetPrefix}/_next/static/${buildId}/pages${getPagePathname(page)}${_devOnlyInvalidateCacheQueryString}`} nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />}
|
||||||
<script async id={`__NEXT_PAGE__/_app`} src={`${assetPrefix}/_next/static/${buildId}/pages/_app.js${_devOnlyInvalidateCacheQueryString}`} nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />
|
<script async id={`__NEXT_PAGE__/_app`} src={`${assetPrefix}/_next/static/${buildId}/pages/_app.js${_devOnlyInvalidateCacheQueryString}`} nonce={this.props.nonce} crossOrigin={this.props.crossOrigin || process.crossOrigin} />
|
||||||
{staticMarkup ? null : this.getDynamicChunks()}
|
{staticMarkup ? null : this.getDynamicChunks()}
|
||||||
{staticMarkup ? null : this.getScripts()}
|
{staticMarkup ? null : this.getScripts()}
|
||||||
</Fragment>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,7 @@ export default class DevServer extends Server {
|
||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderToHTML (req, res, pathname, query) {
|
async renderToHTML (req, res, pathname, query, options) {
|
||||||
const compilationErr = await this.getCompilationError(pathname)
|
const compilationErr = await this.getCompilationError(pathname)
|
||||||
if (compilationErr) {
|
if (compilationErr) {
|
||||||
res.statusCode = 500
|
res.statusCode = 500
|
||||||
|
@ -109,7 +109,7 @@ export default class DevServer extends Server {
|
||||||
if (!this.quiet) console.error(err)
|
if (!this.quiet) console.error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.renderToHTML(req, res, pathname, query)
|
return super.renderToHTML(req, res, pathname, query, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderErrorToHTML (err, req, res, pathname, query) {
|
async renderErrorToHTML (err, req, res, pathname, query) {
|
||||||
|
|
9
test/integration/amphtml/next.config.js
Normal file
9
test/integration/amphtml/next.config.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = {
|
||||||
|
onDemandEntries: {
|
||||||
|
// Make sure entries are not getting disposed.
|
||||||
|
maxInactiveAge: 1000 * 60 * 60
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
amp: true
|
||||||
|
}
|
||||||
|
}
|
1
test/integration/amphtml/pages/index.js
Normal file
1
test/integration/amphtml/pages/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export default () => 'Hello World'
|
6
test/integration/amphtml/pages/use-amp-hook.js
Normal file
6
test/integration/amphtml/pages/use-amp-hook.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import {useAmp} from 'next/amp'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const isAmp = useAmp()
|
||||||
|
return `Hello ${isAmp ? 'AMP' : 'others'}`
|
||||||
|
}
|
117
test/integration/amphtml/test/index.test.js
Normal file
117
test/integration/amphtml/test/index.test.js
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
/* eslint-env jest */
|
||||||
|
/* global jasmine */
|
||||||
|
import { join } from 'path'
|
||||||
|
import {
|
||||||
|
nextServer,
|
||||||
|
nextBuild,
|
||||||
|
startApp,
|
||||||
|
stopApp,
|
||||||
|
renderViaHTTP
|
||||||
|
} from 'next-test-utils'
|
||||||
|
import cheerio from 'cheerio'
|
||||||
|
import amphtmlValidator from 'amphtml-validator'
|
||||||
|
const appDir = join(__dirname, '../')
|
||||||
|
let appPort
|
||||||
|
let server
|
||||||
|
let app
|
||||||
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5
|
||||||
|
|
||||||
|
const context = {}
|
||||||
|
|
||||||
|
async function validateAMP (html) {
|
||||||
|
const validator = await amphtmlValidator.getInstance()
|
||||||
|
const result = validator.validateString(html)
|
||||||
|
if (result.status !== 'PASS') {
|
||||||
|
for (let ii = 0; ii < result.errors.length; ii++) {
|
||||||
|
const error = result.errors[ii]
|
||||||
|
let msg = 'line ' + error.line + ', col ' + error.col + ': ' + error.message
|
||||||
|
if (error.specUrl !== null) {
|
||||||
|
msg += ' (see ' + error.specUrl + ')'
|
||||||
|
}
|
||||||
|
((error.severity === 'ERROR') ? console.error : console.warn)(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(result.status).toBe('PASS')
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AMP Usage', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await nextBuild(appDir)
|
||||||
|
app = nextServer({
|
||||||
|
dir: join(__dirname, '../'),
|
||||||
|
dev: false,
|
||||||
|
quiet: true
|
||||||
|
})
|
||||||
|
|
||||||
|
server = await startApp(app)
|
||||||
|
context.appPort = appPort = server.address().port
|
||||||
|
})
|
||||||
|
afterAll(() => stopApp(server))
|
||||||
|
|
||||||
|
describe('With basic usage', () => {
|
||||||
|
it('should render the page', async () => {
|
||||||
|
const html = await renderViaHTTP(appPort, '/')
|
||||||
|
expect(html).toMatch(/Hello World/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With basic AMP usage', () => {
|
||||||
|
it('should render the page as valid AMP', async () => {
|
||||||
|
const html = await renderViaHTTP(appPort, '/amp')
|
||||||
|
await validateAMP(html)
|
||||||
|
expect(html).toMatch(/Hello World/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add link preload for amp script', async () => {
|
||||||
|
const html = await renderViaHTTP(appPort, '/amp')
|
||||||
|
await validateAMP(html)
|
||||||
|
const $ = cheerio.load(html)
|
||||||
|
expect($($('link[rel=preload]').toArray().find(i => $(i).attr('href') === 'https://cdn.ampproject.org/v0.js')).attr('href')).toBe('https://cdn.ampproject.org/v0.js')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add custom styles before amp boilerplate styles', async () => {
|
||||||
|
const html = await renderViaHTTP(appPort, '/amp')
|
||||||
|
await validateAMP(html)
|
||||||
|
const $ = cheerio.load(html)
|
||||||
|
const order = []
|
||||||
|
$('style').toArray().forEach((i) => {
|
||||||
|
if ($(i).attr('amp-custom') === '') {
|
||||||
|
order.push('amp-custom')
|
||||||
|
}
|
||||||
|
if ($(i).attr('amp-boilerplate') === '') {
|
||||||
|
order.push('amp-boilerplate')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(order).toEqual(['amp-custom', 'amp-boilerplate', 'amp-boilerplate'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With AMP context', () => {
|
||||||
|
it('should render the normal page that uses the AMP hook', async () => {
|
||||||
|
const html = await renderViaHTTP(appPort, '/use-amp-hook')
|
||||||
|
expect(html).toMatch(/Hello others/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render the AMP page that uses the AMP hook', async () => {
|
||||||
|
const html = await renderViaHTTP(appPort, '/use-amp-hook/amp')
|
||||||
|
await validateAMP(html)
|
||||||
|
expect(html).toMatch(/Hello AMP/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('canonical amphtml', () => {
|
||||||
|
it('should render link rel amphtml', async () => {
|
||||||
|
const html = await renderViaHTTP(appPort, '/use-amp-hook')
|
||||||
|
const $ = cheerio.load(html)
|
||||||
|
expect($('link[rel=amphtml]').first().attr('href')).toBe('/use-amp-hook/amp')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render the AMP page that uses the AMP hook', async () => {
|
||||||
|
const html = await renderViaHTTP(appPort, '/use-amp-hook/amp')
|
||||||
|
const $ = cheerio.load(html)
|
||||||
|
await validateAMP(html)
|
||||||
|
expect($('link[rel=canonical]').first().attr('href')).toBe('/use-amp-hook')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
58
yarn.lock
58
yarn.lock
|
@ -1879,6 +1879,15 @@ amdefine@>=0.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
|
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
|
||||||
integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=
|
integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=
|
||||||
|
|
||||||
|
amphtml-validator@1.0.23:
|
||||||
|
version "1.0.23"
|
||||||
|
resolved "https://registry.yarnpkg.com/amphtml-validator/-/amphtml-validator-1.0.23.tgz#dba0c3854289563c0adaac292cd4d6096ee4d7c8"
|
||||||
|
integrity sha1-26DDhUKJVjwK2qwpLNTWCW7k18g=
|
||||||
|
dependencies:
|
||||||
|
colors "1.1.2"
|
||||||
|
commander "2.9.0"
|
||||||
|
promise "7.1.1"
|
||||||
|
|
||||||
ansi-colors@^3.0.0:
|
ansi-colors@^3.0.0:
|
||||||
version "3.2.1"
|
version "3.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.1.tgz#9638047e4213f3428a11944a7d4b31cba0a3ff95"
|
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.1.tgz#9638047e4213f3428a11944a7d4b31cba0a3ff95"
|
||||||
|
@ -3279,7 +3288,7 @@ color@^3.0.0:
|
||||||
color-convert "^1.9.1"
|
color-convert "^1.9.1"
|
||||||
color-string "^1.5.2"
|
color-string "^1.5.2"
|
||||||
|
|
||||||
colors@~1.1.2:
|
colors@1.1.2, colors@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
|
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
|
||||||
integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM=
|
integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM=
|
||||||
|
@ -3299,6 +3308,13 @@ combined-stream@^1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream "~1.0.0"
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
|
commander@2.9.0:
|
||||||
|
version "2.9.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
|
||||||
|
integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=
|
||||||
|
dependencies:
|
||||||
|
graceful-readlink ">= 1.0.0"
|
||||||
|
|
||||||
commander@^2.12.1, commander@^2.18.0, commander@^2.9.0:
|
commander@^2.12.1, commander@^2.18.0, commander@^2.9.0:
|
||||||
version "2.19.0"
|
version "2.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
|
||||||
|
@ -5776,6 +5792,11 @@ graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4,
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
|
||||||
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
|
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
|
||||||
|
|
||||||
|
"graceful-readlink@>= 1.0.0":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
||||||
|
integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
|
||||||
|
|
||||||
"growl@~> 1.10.0":
|
"growl@~> 1.10.0":
|
||||||
version "1.10.5"
|
version "1.10.5"
|
||||||
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
|
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
|
||||||
|
@ -9637,6 +9658,13 @@ promise-retry@^1.1.1:
|
||||||
err-code "^1.0.0"
|
err-code "^1.0.0"
|
||||||
retry "^0.10.0"
|
retry "^0.10.0"
|
||||||
|
|
||||||
|
promise@7.1.1:
|
||||||
|
version "7.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf"
|
||||||
|
integrity sha1-SJZUxpJha4qlWwck+oCbt9tJxb8=
|
||||||
|
dependencies:
|
||||||
|
asap "~2.0.3"
|
||||||
|
|
||||||
promise@^7.0.1:
|
promise@^7.0.1:
|
||||||
version "7.3.1"
|
version "7.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
|
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
|
||||||
|
@ -9870,15 +9898,15 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
react-dom@16.6.3:
|
react-dom@16.8.0:
|
||||||
version "16.6.3"
|
version "16.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.6.3.tgz#8fa7ba6883c85211b8da2d0efeffc9d3825cccc0"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.0.tgz#18f28d4be3571ed206672a267c66dd083145a9c4"
|
||||||
integrity sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ==
|
integrity sha512-dBzoAGYZpW9Yggp+CzBPC7q1HmWSeRc93DWrwbskmG1eHJWznZB/p0l/Sm+69leIGUS91AXPB/qB3WcPnKx8Sw==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
scheduler "^0.11.2"
|
scheduler "^0.13.0"
|
||||||
|
|
||||||
react-error-overlay@4.0.0:
|
react-error-overlay@4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
|
@ -9890,15 +9918,15 @@ react-is@16.6.3, react-is@^16.3.2:
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.3.tgz#d2d7462fcfcbe6ec0da56ad69047e47e56e7eac0"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.3.tgz#d2d7462fcfcbe6ec0da56ad69047e47e56e7eac0"
|
||||||
integrity sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA==
|
integrity sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA==
|
||||||
|
|
||||||
react@16.6.3:
|
react@16.8.0:
|
||||||
version "16.6.3"
|
version "16.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.8.0.tgz#8533f0e4af818f448a276eae71681d09e8dd970a"
|
||||||
integrity sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==
|
integrity sha512-g+nikW2D48kqgWSPwNo0NH9tIGG3DsQFlrtrQ1kj6W77z5ahyIHG0w8kPpz4Sdj6gyLnz0lEd/xsjOoGge2MYQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
scheduler "^0.11.2"
|
scheduler "^0.13.0"
|
||||||
|
|
||||||
read-cmd-shim@^1.0.1:
|
read-cmd-shim@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
|
@ -10623,10 +10651,10 @@ sax@^1.2.4, sax@~1.2.4:
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||||
|
|
||||||
scheduler@^0.11.2:
|
scheduler@^0.13.0:
|
||||||
version "0.11.3"
|
version "0.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.11.3.tgz#b5769b90cf8b1464f3f3cfcafe8e3cd7555a2d6b"
|
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.0.tgz#e701f62e1b3e78d2bbb264046d4e7260f12184dd"
|
||||||
integrity sha512-i9X9VRRVZDd3xZw10NY5Z2cVMbdYg6gqFecfj79USv1CFN+YrJ3gIPRKf1qlY+Sxly4djoKdfx1T+m9dnRB8kQ==
|
integrity sha512-w7aJnV30jc7OsiZQNPVmBc+HooZuvQZIZIShKutC3tnMFMkcwVN9CZRRSSNw03OnSCKmEkK8usmwcw6dqBaLzw==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
|
|
Loading…
Reference in a new issue