未验证 提交 e5111745 编写于 作者: J JJ Kasper 提交者: GitHub

Replace `.amp.js` with `withAmp(Comp)` (#7009)

* Add WithAmp to enable AMP support for
pages instead of .amp.js

* Update handling for exporting AMP

* Fix ampPath in export for / path and
revert isAmp logic to handle right

* Update amphtml test suite

* Add handling for noDirtyAmp during
export and update amp-export test suite

* Update serverless and export-default-map
test suites

* Update require-page tests
上级 cc09478a
import Head from 'next/head'
import { useAmp } from 'next/amp'
import Byline from '../components/Byline'
export default () => {
const isAmp = useAmp()
return (
<div>
<Head>
<title>The Cat</title>
</Head>
<h1>The Cat</h1>
<Byline author='Meow Meow Fuzzyface' />
<p>
<a href={isAmp ? '/cat' : '/cat?amp=1'}>
{isAmp ? 'View Non-AMP' : 'View AMP'} Version
</a>
</p>
<p className='caption'>Woooooooooooof</p>
<p>
Wafer donut candy soufflé{' '}
<a href={isAmp ? '/?amp=1' : '/'}>lemon drops</a> icing. Marzipan gummi
bears pie danish lollipop pudding powder gummi bears sweet. Pie sweet
roll sweet roll topping chocolate bar dragée pudding chocolate cake.
Croissant sweet chocolate bar cheesecake candy canes. Tootsie roll icing
macaroon bonbon cupcake apple pie candy canes biscuit candy canes.
Jujubes jelly liquorice toffee gingerbread. Candy tootsie roll macaroon
chocolate bar icing sugar plum pie. Icing gummies chocolate bar
chocolate marzipan bonbon cookie chocolate tart. Caramels danish halvah
croissant. Cheesecake cookie tootsie roll ice cream. Powder dessert
carrot cake muffin tiramisu lemon drops liquorice topping brownie.
Soufflé chocolate cake croissant cupcake jelly.
</p>
<p>
Muffin gummies dessert cheesecake candy canes. Candy canes danish cotton
candy tart dessert powder bear claw marshmallow. Muffin chocolate
marshmallow danish. Chocolate bar biscuit cake tiramisu. Topping sweet
brownie jujubes powder marzipan. Croissant wafer bonbon chupa chups cake
cake marzipan caramels jujubes. Cupcake cheesecake sweet roll
marshmallow lollipop danish jujubes jelly icing. Apple pie chupa chups
lollipop jelly-o cheesecake jelly beans cake dessert. Tootsie roll
tootsie roll bonbon pastry croissant gummi bears cake cake. Fruitcake
sugar plum halvah gingerbread cookie pastry chupa chups wafer lemon
drops. Marshmallow liquorice oat cake lollipop. Lemon drops oat cake
halvah liquorice danish powder cupcake soufflé. Cake tart topping
jelly-o tart sugar plum. Chocolate bar cookie wafer tootsie roll candy
cotton candy toffee pie donut.
</p>
<p>
Ice cream lollipop marshmallow tiramisu jujubes croissant. Bear claw
lemon drops marzipan candy bonbon cupcake powder. Candy canes cheesecake
bear claw pastry cake donut jujubes. Icing tart jelly-o soufflé bonbon
apple pie. Cheesecake pie chupa chups toffee powder. Bonbon lemon drops
carrot cake pudding candy halvah cheesecake lollipop cupcake. Pudding
marshmallow fruitcake. Gummi bears bonbon chupa chups lemon drops. Wafer
dessert gummies gummi bears biscuit donut tiramisu gummi bears brownie.
Tootsie roll liquorice bonbon cookie. Sesame snaps chocolate bar cake
croissant chupa chups cheesecake gingerbread tiramisu jelly. Cheesecake
ice cream muffin lollipop gummies. Sesame snaps jelly beans sweet bear
claw tart.
</p>
<p>
Sweet topping chupa chups chocolate cake jelly-o liquorice danish.
Pastry jelly beans apple pie dessert pastry lemon drops marzipan
gummies. Jelly beans macaroon bear claw cotton candy. Toffee sweet
lollipop toffee oat cake. Jelly-o oat cake fruitcake chocolate bar
sweet. Lemon drops gummies chocolate cake lollipop bear claw croissant
danish icing. Chocolate bar donut brownie chocolate cake lemon drops
chocolate bar. Cake fruitcake pudding chocolate apple pie. Brownie
tiramisu chocolate macaroon lemon drops wafer soufflé jujubes icing.
Cheesecake tiramisu cake macaroon tart lollipop donut. Gummi bears
dragée pudding bear claw. Muffin cake cupcake candy canes. Soufflé candy
canes biscuit. Macaroon gummies danish.
</p>
<p>
Cupcake cupcake tart. Cotton candy danish candy canes oat cake ice cream
candy canes powder wafer. Chocolate sesame snaps oat cake dragée
cheesecake. Sesame snaps marshmallow topping liquorice cookie
marshmallow. Liquorice pudding chocolate bar. Cake powder brownie
fruitcake. Carrot cake dessert marzipan sugar plum cupcake cheesecake
pastry. Apple pie macaroon ice cream fruitcake apple pie cookie. Tootsie
roll ice cream oat cake cheesecake donut cheesecake bear claw. Sesame
snaps marzipan jelly beans chocolate tootsie roll. Chocolate bar donut
dragée ice cream biscuit. Pie candy canes muffin candy canes ice cream
tiramisu.
</p>
</div>
)
}
import Head from 'next/head'
import { useAmp } from 'next/amp'
import { useAmp, withAmp } from 'next/amp'
import Byline from '../components/Byline'
export default () => {
export default withAmp(() => {
const isAmp = useAmp()
return (
......@@ -87,4 +87,4 @@ export default () => {
</p>
</div>
)
}
}, { hybrid: true })
import Head from 'next/head'
import { useAmp } from 'next/amp'
import Byline from '../../components/Byline'
export default () => {
const isAmp = useAmp()
return (
<div>
<Head>
<title>The Duck</title>
</Head>
<h1>The Duck</h1>
<Byline author='Meow Meow Fuzzyface' />
<p>
<a href={isAmp ? '/duck' : '/duck?amp=1'}>
{isAmp ? 'View Non-AMP' : 'View AMP'} Version
</a>
</p>
<p className='caption'>Woooooooooooof</p>
<p>
Wafer donut candy soufflé{' '}
<a href={isAmp ? '/?amp=1' : '/'}>lemon drops</a> icing. Marzipan gummi
bears pie danish lollipop pudding powder gummi bears sweet. Pie sweet
roll sweet roll topping chocolate bar dragée pudding chocolate cake.
Croissant sweet chocolate bar cheesecake candy canes. Tootsie roll icing
macaroon bonbon cupcake apple pie candy canes biscuit candy canes.
Jujubes jelly liquorice toffee gingerbread. Candy tootsie roll macaroon
chocolate bar icing sugar plum pie. Icing gummies chocolate bar
chocolate marzipan bonbon cookie chocolate tart. Caramels danish halvah
croissant. Cheesecake cookie tootsie roll ice cream. Powder dessert
carrot cake muffin tiramisu lemon drops liquorice topping brownie.
Soufflé chocolate cake croissant cupcake jelly.
</p>
<p>
Muffin gummies dessert cheesecake candy canes. Candy canes danish cotton
candy tart dessert powder bear claw marshmallow. Muffin chocolate
marshmallow danish. Chocolate bar biscuit cake tiramisu. Topping sweet
brownie jujubes powder marzipan. Croissant wafer bonbon chupa chups cake
cake marzipan caramels jujubes. Cupcake cheesecake sweet roll
marshmallow lollipop danish jujubes jelly icing. Apple pie chupa chups
lollipop jelly-o cheesecake jelly beans cake dessert. Tootsie roll
tootsie roll bonbon pastry croissant gummi bears cake cake. Fruitcake
sugar plum halvah gingerbread cookie pastry chupa chups wafer lemon
drops. Marshmallow liquorice oat cake lollipop. Lemon drops oat cake
halvah liquorice danish powder cupcake soufflé. Cake tart topping
jelly-o tart sugar plum. Chocolate bar cookie wafer tootsie roll candy
cotton candy toffee pie donut.
</p>
<p>
Ice cream lollipop marshmallow tiramisu jujubes croissant. Bear claw
lemon drops marzipan candy bonbon cupcake powder. Candy canes cheesecake
bear claw pastry cake donut jujubes. Icing tart jelly-o soufflé bonbon
apple pie. Cheesecake pie chupa chups toffee powder. Bonbon lemon drops
carrot cake pudding candy halvah cheesecake lollipop cupcake. Pudding
marshmallow fruitcake. Gummi bears bonbon chupa chups lemon drops. Wafer
dessert gummies gummi bears biscuit donut tiramisu gummi bears brownie.
Tootsie roll liquorice bonbon cookie. Sesame snaps chocolate bar cake
croissant chupa chups cheesecake gingerbread tiramisu jelly. Cheesecake
ice cream muffin lollipop gummies. Sesame snaps jelly beans sweet bear
claw tart.
</p>
<p>
Sweet topping chupa chups chocolate cake jelly-o liquorice danish.
Pastry jelly beans apple pie dessert pastry lemon drops marzipan
gummies. Jelly beans macaroon bear claw cotton candy. Toffee sweet
lollipop toffee oat cake. Jelly-o oat cake fruitcake chocolate bar
sweet. Lemon drops gummies chocolate cake lollipop bear claw croissant
danish icing. Chocolate bar donut brownie chocolate cake lemon drops
chocolate bar. Cake fruitcake pudding chocolate apple pie. Brownie
tiramisu chocolate macaroon lemon drops wafer soufflé jujubes icing.
Cheesecake tiramisu cake macaroon tart lollipop donut. Gummi bears
dragée pudding bear claw. Muffin cake cupcake candy canes. Soufflé candy
canes biscuit. Macaroon gummies danish.
</p>
<p>
Cupcake cupcake tart. Cotton candy danish candy canes oat cake ice cream
candy canes powder wafer. Chocolate sesame snaps oat cake dragée
cheesecake. Sesame snaps marshmallow topping liquorice cookie
marshmallow. Liquorice pudding chocolate bar. Cake powder brownie
fruitcake. Carrot cake dessert marzipan sugar plum cupcake cheesecake
pastry. Apple pie macaroon ice cream fruitcake apple pie cookie. Tootsie
roll ice cream oat cake cheesecake donut cheesecake bear claw. Sesame
snaps marzipan jelly beans chocolate tootsie roll. Chocolate bar donut
dragée ice cream biscuit. Pie candy canes muffin candy canes ice cream
tiramisu.
</p>
</div>
)
}
import Head from 'next/head'
import { useAmp } from 'next/amp'
import { useAmp, withAmp } from 'next/amp'
import Layout from '../components/Layout'
import Byline from '../components/Byline'
export default () => {
export default withAmp(() => {
const isAmp = useAmp()
return (
......@@ -219,4 +219,4 @@ export default () => {
`}</style>
</Layout>
)
}
})
export default () => (
<p>Hello AMP!</p>
<p>I'm just a normal old page, no AMP for me</p>
)
import React from 'react'
import {IsAmpContext} from './amphtml-context'
import {AmpModeContext} from './amphtml-context'
export function isAmp({
enabled= false,
hybrid= false,
hasQuery= false,
} = {}) {
return enabled && (!hybrid || (hybrid && hasQuery))
}
export function useAmp() {
return React.useContext(IsAmpContext)
const ampMode = React.useContext(AmpModeContext)
// un-comment below to not be considered AMP in dirty mode
return isAmp(ampMode) // && ampMode.hasQuery
}
export function withAmp(
Component: any,
{ hybrid = false } = {},
): any {
function WithAmpWrapper(props= {}) {
const ampMode = React.useContext(AmpModeContext)
ampMode.enabled = true
ampMode.hybrid = hybrid
return React.createElement(Component, props)
}
WithAmpWrapper.getInitialProps = Component.getInitialProps
return WithAmpWrapper
}
import * as React from 'react'
export const IsAmpContext: React.Context<any> = React.createContext(false)
export const AmpModeContext: React.Context<any> = React.createContext({})
import React from "react";
import withSideEffect from "./side-effect";
import {IsAmpContext} from './amphtml-context';
import {AmpModeContext} from './amphtml-context';
import { HeadManagerContext } from "./head-manager-context";
import { isAmp } from './amp'
type WithIsAmp = {
isAmp?: boolean;
......@@ -128,21 +129,21 @@ const Effect = withSideEffect<WithIsAmp>();
function Head({ children }: { children: React.ReactNode }) {
return (
<IsAmpContext.Consumer>
{(isAmp) => (
<AmpModeContext.Consumer>
{(ampMode) => (
<HeadManagerContext.Consumer>
{(updateHead) => (
<Effect
reduceComponentsToState={reduceComponents}
handleStateChange={updateHead}
isAmp={isAmp}
isAmp={isAmp(ampMode)}
>
{children}
</Effect>
)}
</HeadManagerContext.Consumer>
)}
</IsAmpContext.Consumer>
</AmpModeContext.Consumer>
);
}
......
......@@ -21,7 +21,6 @@ const defaultConfig = {
pagesBufferLength: 2
},
experimental: {
amp: false,
noDirtyAmp: false,
cpus: Math.max(
1,
......
import { normalizePagePath } from './normalize-page-path'
import { tryAmp } from './require'
export type BuildManifest = {
devFiles: string[],
......@@ -10,11 +9,7 @@ export type BuildManifest = {
export function getPageFiles(buildManifest: BuildManifest, page: string): string[] {
const normalizedPage = normalizePagePath(page)
let files = buildManifest.pages[normalizedPage]
if (!files) {
page = tryAmp(buildManifest.pages, normalizedPage)
files = buildManifest.pages[page]
}
const files = buildManifest.pages[normalizedPage]
if (!files) {
// tslint:disable-next-line
......
import {BUILD_MANIFEST, CLIENT_STATIC_FILES_PATH, REACT_LOADABLE_MANIFEST, SERVER_DIRECTORY} from 'next-server/constants';
import { join } from 'path';
import { PagePathOptions, requirePage } from './require';
import { requirePage } from './require';
function interopDefault(mod: any) {
return mod.default || mod
}
export async function loadComponents(distDir: string, buildId: string, pathname: string, opts?: PagePathOptions) {
export async function loadComponents(distDir: string, buildId: string, pathname: string) {
const documentPath = join(distDir, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH, buildId, 'pages', '_document')
const appPath = join(distDir, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH, buildId, 'pages', '_app')
const [buildManifest, reactLoadableManifest, pageData, Document, App] = await Promise.all([
const [buildManifest, reactLoadableManifest, Component, Document, App] = await Promise.all([
require(join(distDir, BUILD_MANIFEST)),
require(join(distDir, REACT_LOADABLE_MANIFEST)),
requirePage(pathname, distDir, opts),
interopDefault(requirePage(pathname, distDir)),
interopDefault(require(documentPath)),
interopDefault(require(appPath)),
])
const Component = interopDefault(pageData.mod)
const { hasAmp } = pageData
return {buildManifest, reactLoadableManifest, Component, Document, App, hasAmp}
return {buildManifest, reactLoadableManifest, Component, Document, App}
}
......@@ -37,7 +37,6 @@ export default class Server {
distDir: string
buildId: string
renderOpts: {
ampEnabled: boolean
noDirtyAmp: boolean
ampBindInitData: boolean
staticMarkup: boolean
......@@ -77,7 +76,6 @@ export default class Server {
this.buildId = this.readBuildId()
this.renderOpts = {
ampEnabled: this.nextConfig.experimental.amp,
noDirtyAmp: this.nextConfig.experimental.noDirtyAmp,
ampBindInitData: this.nextConfig.experimental.ampBindInitData,
staticMarkup,
......@@ -271,7 +269,6 @@ export default class Server {
}
const html = await this.renderToHTML(req, res, pathname, query, {
amphtml: query.amp && this.nextConfig.experimental.amp,
dataOnly: this.renderOpts.ampBindInitData && Boolean(query.dataOnly) || (req.headers && (req.headers.accept || '').indexOf('application/amp.bind+json') !== -1),
})
// Request was ended by the user
......@@ -289,8 +286,8 @@ export default class Server {
query: ParsedUrlQuery = {},
opts: any,
) {
const result = await loadComponents(this.distDir, this.buildId, pathname, opts)
return renderToHTML(req, res, pathname, query, { ...result, ...opts, hasAmp: result.hasAmp })
const result = await loadComponents(this.distDir, this.buildId, pathname)
return renderToHTML(req, res, pathname, query, { ...result, ...opts })
}
public async renderToHTML(
......
......@@ -13,15 +13,15 @@ import { RequestContext } from '../lib/request-context'
import {LoadableContext} from '../lib/loadable-context'
import { RouterContext } from '../lib/router-context'
import { DataManager } from '../lib/data-manager'
import {
ManifestItem,
getDynamicImportBundles,
Manifest as ReactLoadableManifest,
ManifestItem,
} from './get-dynamic-import-bundles'
import { getPageFiles, BuildManifest } from './get-page-files'
import { IsAmpContext } from '../lib/amphtml-context'
import { AmpModeContext } from '../lib/amphtml-context'
import optimizeAmp from './optimize-amp'
import { isAmp } from '../lib/amp';
type Enhancer = (Component: React.ComponentType) => React.ComponentType
type ComponentsEnhancer =
......@@ -113,7 +113,6 @@ function render(
}
type RenderOpts = {
ampEnabled: boolean
noDirtyAmp: boolean
ampBindInitData: boolean
staticMarkup: boolean
......@@ -134,13 +133,13 @@ type RenderOpts = {
Document: React.ComponentType
App: React.ComponentType
ErrorDebug?: React.ComponentType<{ error: Error }>,
ampValidator?: (html: string, pathname: string) => Promise<void>,
}
function renderDocument(
Document: React.ComponentType,
{
dataManagerData,
ampEnabled = false,
props,
docProps,
pathname,
......@@ -178,34 +177,31 @@ function renderDocument(
return (
'<!DOCTYPE html>' +
renderToStaticMarkup(
<IsAmpContext.Provider value={amphtml}>
<Document
__NEXT_DATA__={{
dataManager: dataManagerData,
props, // The result of getInitialProps
page: pathname, // The rendered page
query, // querystring parsed / passed by the user
buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
dynamicBuildId, // Specifies if the buildId should by dynamically fetched
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}
ampPath={ampPath}
amphtml={amphtml}
hasAmp={hasAmp}
staticMarkup={staticMarkup}
devFiles={devFiles}
files={files}
dynamicImports={dynamicImports}
assetPrefix={assetPrefix}
{...docProps}
/>
</IsAmpContext.Provider>,
<Document
__NEXT_DATA__={{
dataManager: dataManagerData,
props, // The result of getInitialProps
page: pathname, // The rendered page
query, // querystring parsed / passed by the user
buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
dynamicBuildId, // Specifies if the buildId should by dynamically fetched
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
}}
ampPath={ampPath}
amphtml={amphtml}
hasAmp={hasAmp}
staticMarkup={staticMarkup}
devFiles={devFiles}
files={files}
dynamicImports={dynamicImports}
assetPrefix={assetPrefix}
{...docProps}
/>,
)
)
}
......@@ -224,8 +220,6 @@ export async function renderToHTML(
ampBindInitData = false,
staticMarkup = false,
noDirtyAmp = false,
amphtml = false,
hasAmp = false,
ampPath = '',
App,
Document,
......@@ -306,6 +300,11 @@ export async function renderToHTML(
let renderPage: (options: ComponentsEnhancer) => { html: string, head: any } | Promise<{ html: string; head: any }>
const ampMode = {
enabled: false,
hasQuery: Boolean(query.amp),
}
if (ampBindInitData) {
renderPage = async (
options: ComponentsEnhancer = {},
......@@ -321,7 +320,7 @@ export async function renderToHTML(
const Application = () => <RequestContext.Provider value={req}>
<RouterContext.Provider value={router}>
<DataManagerContext.Provider value={dataManager}>
<IsAmpContext.Provider value={amphtml}>
<AmpModeContext.Provider value={ampMode}>
<LoadableContext.Provider
value={(moduleName) => reactLoadableModules.push(moduleName)}
>
......@@ -331,7 +330,7 @@ export async function renderToHTML(
{...props}
/>
</LoadableContext.Provider>
</IsAmpContext.Provider>
</AmpModeContext.Provider>
</DataManagerContext.Provider>
</RouterContext.Provider>
</RequestContext.Provider>
......@@ -378,7 +377,7 @@ export async function renderToHTML(
renderElementToString,
<RequestContext.Provider value={req}>
<RouterContext.Provider value={router}>
<IsAmpContext.Provider value={amphtml}>
<AmpModeContext.Provider value={ampMode}>
<LoadableContext.Provider
value={(moduleName) => reactLoadableModules.push(moduleName)}
>
......@@ -388,7 +387,7 @@ export async function renderToHTML(
{...props}
/>
</LoadableContext.Provider>
</IsAmpContext.Provider>
</AmpModeContext.Provider>
</RouterContext.Provider>
</RequestContext.Provider>,
)
......@@ -412,6 +411,11 @@ export async function renderToHTML(
...getDynamicImportBundles(reactLoadableManifest, reactLoadableModules),
]
const dynamicImportsIds: any = dynamicImports.map((bundle) => bundle.id)
const amphtml = isAmp(ampMode)
const hasAmp = !amphtml && ampMode.enabled
// update renderOpts so export knows it's AMP
renderOpts.amphtml = amphtml
renderOpts.hasAmp = hasAmp
let html = renderDocument(Document, {
...renderOpts,
......@@ -429,8 +433,13 @@ export async function renderToHTML(
devFiles,
})
if (!dev && amphtml && html) {
if (amphtml && html) {
html = await optimizeAmp(html, { amphtml, noDirtyAmp, query })
// don't validate dirty AMP
if (renderOpts.ampValidator && query.amp) {
await renderOpts.ampValidator(html, pathname)
}
}
return html
}
......
import {join} from 'path'
import { isAmpFile } from './utils'
import {PAGES_MANIFEST, SERVER_DIRECTORY} from 'next-server/constants'
import { normalizePagePath } from './normalize-page-path'
export type PagePathOptions = {
amphtml?: boolean,
}
export function pageNotFoundError(page: string): Error {
const err: any = new Error(`Cannot find module for page: ${page}`)
err.code = 'ENOENT'
return err
}
export const tryAmp = (manifest: any, page: string) => {
page = page === '/' ? '/index' : page
const hasAmp = manifest[page + '.amp']
if (hasAmp) {
page += '.amp'
} else if (manifest[page + '/index.amp']) {
page += '/index.amp'
}
return page
}
export function getPagePath(page: string, distDir: string, opts: PagePathOptions = {}): string {
export function getPagePath(page: string, distDir: string): string {
const serverBuildPath = join(distDir, SERVER_DIRECTORY)
const pagesManifest = require(join(serverBuildPath, PAGES_MANIFEST))
try {
page = normalizePagePath(page)
if (opts.amphtml || !pagesManifest[page]) {
page = tryAmp(pagesManifest, page)
// Force .amp to show 404 if set
const isAmp = page.endsWith('.amp')
if (opts.amphtml && !isAmp) {
page += '.amp'
}
opts.amphtml = opts.amphtml || isAmp
}
page = page === '/' ? '/index' : page
} catch (err) {
// tslint:disable-next-line
console.error(err)
......@@ -53,20 +28,7 @@ export function getPagePath(page: string, distDir: string, opts: PagePathOptions
return join(serverBuildPath, pagesManifest[page])
}
export function requirePage(page: string, distDir: string, opts: PagePathOptions = {}): any {
const pagePath = getPagePath(page, distDir, opts)
const isAmp = isAmpFile(pagePath)
let hasAmp = false
if (!isAmp) {
try {
hasAmp = isAmpFile(getPagePath(page, distDir, { amphtml: true }))
} catch (_) {}
}
opts.amphtml = opts.amphtml || isAmp
return {
hasAmp,
mod: require(pagePath),
}
export function requirePage(page: string, distDir: string): any {
const pagePath = getPagePath(page, distDir)
return require(pagePath)
}
......@@ -24,15 +24,3 @@ export function cleanAmpPath(pathname: string): string {
.replace(/\.amp$/, '')
.replace(/\index$/, '')
}
export function isAmpPath(pathname: string): boolean {
return (pathname || '').endsWith('.amp')
}
export function isAmpFile(pathname: string): boolean {
if (isAmpPath(pathname)) return true
pathname = pathname || ''
const parts = pathname.split('.')
parts.pop() // remove extension
return isAmpPath(parts.join('.'))
}
......@@ -43,7 +43,6 @@ export function createEntrypoints(pages: PagesMapping, target: 'server'|'serverl
distDir: DOT_NEXT_ALIAS,
assetPrefix: config.assetPrefix,
generateEtags: config.generateEtags,
ampEnabled: config.experimental.amp,
ampBindInitData: config.experimental.ampBindInitData,
dynamicBuildId
}
......
......@@ -11,7 +11,6 @@ export type ServerlessLoaderQuery = {
absoluteDocumentPath: string,
absoluteErrorPath: string,
assetPrefix: string,
ampEnabled: boolean | string,
ampBindInitData: boolean | string,
generateEtags: string
dynamicBuildId?: string | boolean
......@@ -23,7 +22,6 @@ const nextServerlessLoader: loader.Loader = function () {
absolutePagePath,
page,
assetPrefix,
ampEnabled,
ampBindInitData,
absoluteAppPath,
absoluteDocumentPath,
......@@ -52,7 +50,6 @@ const nextServerlessLoader: loader.Loader = function () {
buildId: "__NEXT_REPLACE__BUILD_ID__",
dynamicBuildId: ${dynamicBuildId === true || dynamicBuildId === 'true'},
assetPrefix: "${assetPrefix}",
ampEnabled: ${ampEnabled === true || ampEnabled === 'true'},
ampBindInitData: ${ampBindInitData === true || ampBindInitData === 'true'}
}
const parsedUrl = parse(req.url, true)
......@@ -61,7 +58,6 @@ const nextServerlessLoader: loader.Loader = function () {
const result = await renderToHTML(req, res, "${page}", parsedUrl.query, Object.assign(
{
Component,
amphtml: options.ampEnabled && (parsedUrl.query.amp || ${page.endsWith('.amp')}),
dataOnly: req.headers && (req.headers.accept || '').indexOf('application/amp.bind+json') !== -1,
},
options,
......
......@@ -5,8 +5,6 @@ import mkdirpModule from 'mkdirp'
import { resolve, join } from 'path'
import { existsSync, readFileSync } from 'fs'
import loadConfig from 'next-server/next-config'
import { tryAmp } from 'next-server/dist/server/require'
import { cleanAmpPath } from 'next-server/dist/server/utils'
import { PHASE_EXPORT, SERVER_DIRECTORY, PAGES_MANIFEST, CONFIG_FILE, BUILD_ID_FILE, CLIENT_STATIC_FILES_PATH } from 'next-server/constants'
import createProgress from 'tty-aware-progress'
import { promisify } from 'util'
......@@ -55,39 +53,6 @@ export default async function (dir, options, configuration) {
defaultPathMap[page] = { page }
}
Object.keys(defaultPathMap).forEach(path => {
const isAmp = path.indexOf('.amp') > -1
if (isAmp) {
defaultPathMap[path].query = { amphtml: true }
const nonAmp = cleanAmpPath(path).replace(/\/$/, '') || '/'
if (!defaultPathMap[nonAmp]) {
if (!nextConfig.experimental.noDirtyAmp) {
// dirty optimized
defaultPathMap[nonAmp] = {
...defaultPathMap[path],
query: { ...defaultPathMap[path].query }
}
defaultPathMap[nonAmp].query.ampOnly = true
defaultPathMap[nonAmp].query.ampPath = path
// clean optimized
defaultPathMap[path].query.amp = 1
} else {
// dirty optimizing is disabled
defaultPathMap[path].query.amp = 1
defaultPathMap[path].query.ampOnly = true
}
} else {
defaultPathMap[path].query.amp = 1
}
} else {
const ampPath = tryAmp(defaultPathMap, path)
if (ampPath !== path) {
defaultPathMap[path].query = { hasAmp: true, ampPath: ampPath.replace(/(?<!^)\/index\.amp$/, '.amp') }
}
}
})
// Initialize the output directory
const outDir = options.outdir
await recursiveDelete(join(outDir))
......@@ -130,7 +95,7 @@ export default async function (dir, options, configuration) {
dev: false,
staticMarkup: false,
hotReloader: null,
ampEnabled: nextConfig.experimental.amp
noDirtyAmp: nextConfig.experimental.noDirtyAmp
}
const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
......
import mkdirpModule from 'mkdirp'
import { promisify } from 'util'
import { extname, join, dirname, sep } from 'path'
import { cleanAmpPath } from 'next-server/dist/server/utils'
import { renderToHTML } from 'next-server/dist/server/render'
import { writeFile } from 'fs'
import { writeFile, access } from 'fs'
import Sema from 'async-sema'
import AmpHtmlValidator from 'amphtml-validator'
import { loadComponents } from 'next-server/dist/server/load-components'
const envConfig = require('next-server/config')
const mkdirp = promisify(mkdirpModule)
const writeFileP = promisify(writeFile)
const accessP = promisify(access)
global.__NEXT_DATA__ = {
nextExport: true
......@@ -24,6 +25,7 @@ process.on(
exportPathMap,
outDir,
renderOpts,
noDirtyAmp,
serverRuntimeConfig,
concurrency
}) => {
......@@ -31,9 +33,8 @@ process.on(
try {
const work = async path => {
await sema.acquire()
const ampPath = `${path === '/' ? '/index' : path}.amp`
const { page, query = {} } = exportPathMap[path]
const ampOpts = { amphtml: query.amphtml, hasAmp: query.hasAmp, ampPath: query.ampPath }
const ampOnly = query.ampOnly
delete query.ampOnly
delete query.hasAmp
delete query.ampPath
......@@ -46,14 +47,6 @@ process.on(
publicRuntimeConfig: renderOpts.runtimeConfig
})
if (ampOnly) {
path = cleanAmpPath(path)
ampOpts.ampPath = path + '.amp'
}
// replace /docs/index.amp with /docs.amp
path = path.replace(/(?<!^)\/index\.amp$/, '.amp')
let htmlFilename = `${path}${sep}index.html`
const pageExt = extname(page)
const pathExt = extname(path)
......@@ -70,9 +63,10 @@ process.on(
await mkdirp(baseDir)
const components = await loadComponents(distDir, buildId, page)
const html = await renderToHTML(req, res, page, query, { ...components, ...renderOpts, ...ampOpts })
const curRenderOpts = { ...components, ...renderOpts, ampPath }
const html = await renderToHTML(req, res, page, query, curRenderOpts)
if (ampOpts.amphtml && query.amp) {
const validateAmp = async (html, page) => {
const validator = await AmpHtmlValidator.getInstance()
const result = validator.validateString(html)
const errors = result.errors.filter(e => e.severity === 'ERROR')
......@@ -92,13 +86,38 @@ process.on(
}
}
await new Promise((resolve, reject) =>
writeFile(
htmlFilepath,
html,
'utf8',
err => (err ? reject(err) : resolve())
)
if (curRenderOpts.amphtml && query.amp) {
await validateAmp(html, path)
}
if (
(curRenderOpts.amphtml && !query.amp && !noDirtyAmp) ||
curRenderOpts.hasAmp
) {
// we need to render a clean AMP version
const ampHtmlFilename = `${ampPath}${sep}index.html`
const ampBaseDir = join(outDir, dirname(ampHtmlFilename))
const ampHtmlFilepath = join(outDir, ampHtmlFilename)
try {
await accessP(ampHtmlFilepath)
} catch (_) {
// make sure it doesn't exist from manual mapping
const ampHtml = await renderToHTML(req, res, page, { ...query, amp: 1 }, curRenderOpts)
await validateAmp(ampHtml, page + '?amp=1')
await mkdirp(ampBaseDir)
await writeFileP(
ampHtmlFilepath,
ampHtml,
'utf8'
)
}
}
await writeFileP(
htmlFilepath,
html,
'utf8'
)
process.send({ type: 'progress' })
sema.release()
......
......@@ -148,7 +148,6 @@ export class Head extends Component {
render() {
const {
ampEnabled,
styles,
amphtml,
hasAmp,
......@@ -251,7 +250,7 @@ export class Head extends Component {
)}
{!amphtml && (
<>
{ampEnabled && hasAmp && <link rel="amphtml" href={ampPath ? ampPath : `${page}?amp=1`} />}
{hasAmp && <link rel="amphtml" href={ampPath ? ampPath : `${page}?amp=1`} />}
{page !== '/_error' && (
<link
rel="preload"
......
......@@ -391,12 +391,12 @@ export default class HotReloader {
this.webpackHotMiddleware.publish({ action, data: args })
}
async ensurePage (page, amp, ampEnabled) {
async ensurePage (page) {
// Make sure we don't re-build or dispose prebuilt pages
if (page !== '/_error' && BLOCKED_PAGES.indexOf(page) !== -1) {
return
}
return this.onDemandEntries.ensurePage(page, amp, ampEnabled)
return this.onDemandEntries.ensurePage(page)
}
}
......
import { join } from 'path'
import {isWriteable} from '../../build/is-writeable'
export async function findPageFile(rootDir: string, normalizedPagePath: string, pageExtensions: string[], amp: boolean, ampEnabled: boolean): Promise<string|null> {
if (ampEnabled) {
// Add falling back to .amp.js extension
if (!amp) pageExtensions = pageExtensions.concat(pageExtensions.map((ext) => 'amp.' + ext))
}
for (let extension of pageExtensions) {
if (amp) extension = 'amp.' + extension
export async function findPageFile(rootDir: string, normalizedPagePath: string, pageExtensions: string[]): Promise<string|null> {
for (const extension of pageExtensions) {
const relativePagePath = `${normalizedPagePath}.${extension}`
const pagePath = join(rootDir, relativePagePath)
......
......@@ -22,6 +22,18 @@ export default class DevServer extends Server {
this.devReady = new Promise(resolve => {
this.setDevReady = resolve
})
this.renderOpts.ampValidator = (html, pathname) => {
return AmpHtmlValidator.getInstance().then(validator => {
const result = validator.validateString(html)
ampValidation(
pathname,
result.errors
.filter(e => e.severity === 'ERROR')
.filter(e => this._filterAmpDevelopmentScript(html, e)),
result.errors.filter(e => e.severity !== 'ERROR')
)
})
}
}
currentPhase () {
......@@ -130,10 +142,7 @@ export default class DevServer extends Server {
// In dev mode we use on demand entries to compile the page before rendering
try {
const result = await this.hotReloader.ensurePage(pathname, options.amphtml, this.nextConfig.experimental.amp)
pathname = result.pathname
options.amphtml = options.amphtml || result.isAmp
options.hasAmp = result.hasAmp
await this.hotReloader.ensurePage(pathname)
} catch (err) {
if (err.code === 'ENOENT') {
res.statusCode = 404
......@@ -142,18 +151,6 @@ export default class DevServer extends Server {
if (!this.quiet) console.error(err)
}
const html = await super.renderToHTML(req, res, pathname, query, options)
if (options.amphtml && pathname !== '/_error') {
await AmpHtmlValidator.getInstance().then(validator => {
const result = validator.validateString(html)
ampValidation(
pathname,
result.errors
.filter(e => e.severity === 'ERROR')
.filter(e => this._filterAmpDevelopmentScript(html, e)),
result.errors.filter(e => e.severity !== 'ERROR')
)
})
}
return html
}
......
......@@ -206,7 +206,7 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
})
},
async ensurePage (page, amp, ampEnabled) {
async ensurePage (page) {
await this.waitUntilReloaded()
let normalizedPagePath
try {
......@@ -216,8 +216,7 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
throw pageNotFoundError(normalizedPagePath)
}
let pagePath = await findPageFile(pagesDir, normalizedPagePath, pageExtensions, amp, ampEnabled)
const isAmp = pagePath && pageExtensions.some(ext => pagePath.endsWith('amp.' + ext))
let pagePath = await findPageFile(pagesDir, normalizedPagePath, pageExtensions)
// Default the /_error route to the Next.js provided default page
if (page === '/_error' && pagePath === null) {
......@@ -235,13 +234,8 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
const absolutePagePath = pagePath.startsWith('next/dist/pages') ? require.resolve(pagePath) : join(pagesDir, pagePath)
page = posix.normalize(pageUrl)
const result = {
isAmp,
pathname: page,
hasAmp: !isAmp && await findPageFile(pagesDir, normalizedPagePath, pageExtensions, !isAmp, ampEnabled)
}
await new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
// Makes sure the page that is being kept in on-demand-entries matches the webpack output
const normalizedPage = normalizePage(page)
const entryInfo = entries[normalizedPage]
......@@ -270,8 +264,6 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
resolve()
}
})
return result
},
middleware () {
......
module.exports = {
// exportPathMap
experimental: {
amp: true
}
}
export default () => (
import { withAmp } from 'next/amp'
export default withAmp(() => (
<div>
{/* I show a warning since the amp-video script isn't added */}
<amp-video src='/cats.mp4' height={400} width={800} />
</div>
)
))
export default () => (
import { withAmp } from 'next/amp'
export default withAmp(() => (
<div>
{/* I throw an error since <amp-img/> should be used instead */}
<img src='/dog.gif' height={400} width={800} />
{/* I show a warning since the amp-video script isn't added */}
<amp-video src='/cats.mp4' height={400} width={800} />
</div>
)
))
export default () => (
import { withAmp } from 'next/amp'
export default withAmp(() => (
<div>
{/* I throw an error since <amp-img/> should be used instead */}
<img src='/dog.gif' height={400} width={800} />
</div>
)
))
import { withAmp } from 'next/amp'
export default withAmp(() => (
<p>Hello AMP!</p>
))
import { withAmp } from 'next/amp'
export default withAmp(() => (
<p>Hello again AMP!</p>
))
export default () => (
import { withAmp } from 'next/amp'
export default withAmp(() => (
'I am a hybrid AMP page'
)
), { hybrid: true })
......@@ -35,7 +35,7 @@ describe('AMP Validation on Export', () => {
})
it('should disable dirty AMP with noDirtyAmp set', async () => {
nextConfig.replace(`amp: true`, `amp: true,noDirtyAmp: true`)
nextConfig.replace(`// exportPathMap`, `experimental: { noDirtyAmp: true } `)
await nextExport(appDir, { outdir: outDir })
const ampOnly = ['first', 'second', 'third']
await Promise.all(ampOnly.map(async page => {
......@@ -50,7 +50,7 @@ describe('AMP Validation on Export', () => {
nextConfig.replace('// exportPathMap',
`exportPathMap: function(defaultMap) {
return {
'/cat': defaultMap['/cat.amp'],
'/cat': { page: '/cat', query: { amp: 1 } },
}
},`)
......@@ -67,7 +67,7 @@ describe('AMP Validation on Export', () => {
nextConfig.replace('// exportPathMap',
`exportPathMap: function(defaultMap) {
return {
'/dog': defaultMap['/dog.amp'],
'/dog': { page: '/dog', query: { amp: 1 }},
}
},`)
......@@ -84,7 +84,7 @@ describe('AMP Validation on Export', () => {
nextConfig.replace('// exportPathMap',
`exportPathMap: function(defaultMap) {
return {
'/dog-cat': defaultMap['/dog-cat.amp'],
'/dog-cat': { page: '/dog-cat', query: { amp: 1 } },
}
},`)
......
import Head from 'next/head'
import { withAmp } from 'next/amp'
const date = new Date().toJSON()
export default () => (
export default withAmp(() => (
<>
<Head>
<script
......@@ -22,4 +23,4 @@ export default () => (
{date}
</amp-timeago>
</>
)
))
import Head from 'next/head'
import { withAmp } from 'next/amp'
export default () => (
export default withAmp(() => (
<amp-layout className='abc' layout='responsive' width='1' height='1'>
<Head>
<meta name='viewport' content='something :p' />
</Head>
<span>Hello World</span>
</amp-layout>
)
))
import Head from 'next/head'
import { withAmp } from 'next/amp'
export default () => (
export default withAmp(() => (
<div>
<Head>
<script src='/im-not-allowed.js' type='text/javascript' />
......@@ -10,4 +11,4 @@ export default () => (
</Head>
<p>We only allow AMP scripts now</p>
</div>
)
))
export default () => (
import { withAmp } from 'next/amp'
export default withAmp(() => (
<amp-layout className='abc' layout='responsive' width='1' height='1'>
<span>Hello World</span>
</amp-layout>
)
), { hybrid: true })
import { useAmp } from 'next/amp'
import { useAmp, withAmp } from 'next/amp'
export default () => {
export default withAmp(() => {
const isAmp = useAmp()
return `Hello ${isAmp ? 'AMP' : 'others'}`
}
}, { hybrid: true })
export default () => <div>Only AMP for me...</div>
import { withAmp } from 'next/amp'
export default withAmp(() => <div>Only AMP for me...</div>)
import Foo from '../components/Foo'
import Bar from '../components/Bar'
import { withAmp } from 'next/amp'
export default () => (
export default withAmp(() => (
<div>
<Foo />
<Bar />
......@@ -11,4 +12,4 @@ export default () => (
}
`}</style>
</div>
)
))
import { useAmp } from 'next/amp'
import { useAmp, withAmp } from 'next/amp'
export default () => {
export default withAmp(() => {
const isAmp = useAmp()
return `Hello ${isAmp ? 'AMP' : 'others'}`
}
}, { hybrid: true })
module.exports = {
experimental: {
amp: true
}
}
import { useAmp } from 'next/amp'
import { useAmp, withAmp } from 'next/amp'
export default () => (
export default withAmp(() => (
<p>I'm an {useAmp() ? 'AMP' : 'normal'} page</p>
)
), { hybrid: true })
export default () => (
import { withAmp } from 'next/amp'
export default withAmp(() => (
<p>Simple hybrid amp/non-amp page</p>
)
), { hybrid: true })
import { useAmp } from 'next/amp'
import { useAmp, withAmp } from 'next/amp'
export default () => (
export default withAmp(() => (
<p>I'm an {useAmp() ? 'AMP' : 'normal'} page</p>
)
), { hybrid: true })
export default () => (
import { withAmp } from 'next/amp'
export default withAmp(() => (
<p>I am an AMP only page</p>
)
))
export default () => (
import { withAmp } from 'next/amp'
export default withAmp(() => (
<p>I'm an AMP page</p>
)
))
......@@ -4,8 +4,5 @@ module.exports = {
// Make sure entries are not getting disposed.
maxInactiveAge: 1000 * 60 * 60
},
experimental: {
amp: true
},
lambdas: true
}
import { withAmp } from 'next/amp'
export default withAmp(() => `Hi Im an AMP page!`)
export default () => `Hi Im an AMP page!`
......@@ -23,7 +23,7 @@ module.exports = function start (port = 0) {
require('./.next/serverless/pages/dynamic-two.js').render(req, res)
})
app.get('/amp', (req, res) => {
require('./.next/serverless/pages/some.amp.js').render(req, res)
require('./.next/serverless/pages/some-amp.js').render(req, res)
})
app.get('/404', (req, res) => {
require('./.next/serverless/pages/_error.js').render(req, res)
......
......@@ -59,17 +59,17 @@ describe('getPagePath', () => {
describe('requirePage', () => {
it('Should require /index.js when using /', async () => {
const page = (await requirePage('/', distDir)).mod
const page = (await requirePage('/', distDir))
expect(page.test).toBe('hello')
})
it('Should require /index.js when using /index', async () => {
const page = (await requirePage('/index', distDir)).mod
const page = (await requirePage('/index', distDir))
expect(page.test).toBe('hello')
})
it('Should require /world.js when using /world', async () => {
const page = (await requirePage('/world', distDir)).mod
const page = (await requirePage('/world', distDir))
expect(page.test).toBe('world')
})
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册