amp-bind data injection (#6840)
This commit is contained in:
parent
84fbd4b594
commit
b1fdffec75
23 changed files with 385 additions and 31 deletions
|
@ -62,6 +62,7 @@
|
||||||
"@babel/preset-flow": "7.0.0",
|
"@babel/preset-flow": "7.0.0",
|
||||||
"@babel/preset-react": "7.0.0",
|
"@babel/preset-react": "7.0.0",
|
||||||
"@mdx-js/loader": "0.18.0",
|
"@mdx-js/loader": "0.18.0",
|
||||||
|
"@types/string-hash": "1.1.1",
|
||||||
"@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",
|
||||||
|
|
3
packages/next-server/lib/data-manager-context.ts
Normal file
3
packages/next-server/lib/data-manager-context.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export const DataManagerContext: React.Context<any> = React.createContext(null)
|
21
packages/next-server/lib/data-manager.ts
Normal file
21
packages/next-server/lib/data-manager.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export class DataManager {
|
||||||
|
data: Map<string, any>
|
||||||
|
constructor(data?: any) {
|
||||||
|
this.data = new Map(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
return this.data
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: string) {
|
||||||
|
return this.data.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: string, value: any) {
|
||||||
|
this.data.set(key, value)
|
||||||
|
}
|
||||||
|
overwrite(data: any) {
|
||||||
|
this.data = new Map(data)
|
||||||
|
}
|
||||||
|
}
|
3
packages/next-server/lib/router-context.ts
Normal file
3
packages/next-server/lib/router-context.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export const RouterContext: React.Context<any> = React.createContext(null)
|
|
@ -27,6 +27,7 @@ const defaultConfig = {
|
||||||
(Number(process.env.CIRCLE_NODE_TOTAL) ||
|
(Number(process.env.CIRCLE_NODE_TOTAL) ||
|
||||||
(os.cpus() || { length: 1 }).length) - 1
|
(os.cpus() || { length: 1 }).length) - 1
|
||||||
),
|
),
|
||||||
|
ampBindInitData: false,
|
||||||
exportTrailingSlash: true,
|
exportTrailingSlash: true,
|
||||||
profiling: false,
|
profiling: false,
|
||||||
sharedRuntime: false
|
sharedRuntime: false
|
||||||
|
|
|
@ -28,6 +28,8 @@ type ServerConstructor = {
|
||||||
conf?: NextConfig,
|
conf?: NextConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ENDING_IN_JSON_REGEX = /\.json$/
|
||||||
|
|
||||||
export default class Server {
|
export default class Server {
|
||||||
dir: string
|
dir: string
|
||||||
quiet: boolean
|
quiet: boolean
|
||||||
|
@ -36,6 +38,7 @@ export default class Server {
|
||||||
buildId: string
|
buildId: string
|
||||||
renderOpts: {
|
renderOpts: {
|
||||||
ampEnabled: boolean
|
ampEnabled: boolean
|
||||||
|
ampBindInitData: boolean
|
||||||
staticMarkup: boolean
|
staticMarkup: boolean
|
||||||
buildId: string
|
buildId: string
|
||||||
generateEtags: boolean
|
generateEtags: boolean
|
||||||
|
@ -74,6 +77,7 @@ export default class Server {
|
||||||
this.buildId = this.readBuildId()
|
this.buildId = this.readBuildId()
|
||||||
this.renderOpts = {
|
this.renderOpts = {
|
||||||
ampEnabled: this.nextConfig.experimental.amp,
|
ampEnabled: this.nextConfig.experimental.amp,
|
||||||
|
ampBindInitData: this.nextConfig.experimental.ampBindInitData,
|
||||||
staticMarkup,
|
staticMarkup,
|
||||||
buildId: this.buildId,
|
buildId: this.buildId,
|
||||||
generateEtags,
|
generateEtags,
|
||||||
|
@ -260,12 +264,19 @@ export default class Server {
|
||||||
return this.handleRequest(req, res, parsedUrl)
|
return this.handleRequest(req, res, parsedUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDataRequest = ENDING_IN_JSON_REGEX.test(pathname)
|
||||||
|
|
||||||
|
if (isDataRequest) {
|
||||||
|
pathname = pathname.replace(ENDING_IN_JSON_REGEX, '')
|
||||||
|
}
|
||||||
|
|
||||||
if (isBlockedPage(pathname)) {
|
if (isBlockedPage(pathname)) {
|
||||||
return this.render404(req, res, parsedUrl)
|
return this.render404(req, res, parsedUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = await this.renderToHTML(req, res, pathname, query, {
|
const html = await this.renderToHTML(req, res, pathname, query, {
|
||||||
amphtml: query.amp && this.nextConfig.experimental.amp,
|
amphtml: query.amp && this.nextConfig.experimental.amp,
|
||||||
|
dataOnly: isDataRequest,
|
||||||
})
|
})
|
||||||
// Request was ended by the user
|
// Request was ended by the user
|
||||||
if (html === null) {
|
if (html === null) {
|
||||||
|
@ -291,9 +302,10 @@ export default class Server {
|
||||||
res: ServerResponse,
|
res: ServerResponse,
|
||||||
pathname: string,
|
pathname: string,
|
||||||
query: ParsedUrlQuery = {},
|
query: ParsedUrlQuery = {},
|
||||||
{ amphtml, hasAmp }: {
|
{ amphtml, dataOnly, hasAmp }: {
|
||||||
amphtml?: boolean,
|
amphtml?: boolean,
|
||||||
hasAmp?: boolean,
|
hasAmp?: boolean,
|
||||||
|
dataOnly?: boolean,
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
|
@ -303,7 +315,7 @@ export default class Server {
|
||||||
res,
|
res,
|
||||||
pathname,
|
pathname,
|
||||||
query,
|
query,
|
||||||
{ ...this.renderOpts, amphtml, hasAmp },
|
{ ...this.renderOpts, amphtml, hasAmp, dataOnly },
|
||||||
)
|
)
|
||||||
return html
|
return html
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -7,7 +7,11 @@ import mitt, {MittEmitter} from '../lib/mitt';
|
||||||
import { loadGetInitialProps, isResSent } from '../lib/utils'
|
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 { DataManagerContext } from '../lib/data-manager-context'
|
||||||
import {LoadableContext} from '../lib/loadable-context'
|
import {LoadableContext} from '../lib/loadable-context'
|
||||||
|
import { RouterContext } from '../lib/router-context'
|
||||||
|
import { DataManager } from '..//lib/data-manager'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getDynamicImportBundles,
|
getDynamicImportBundles,
|
||||||
Manifest as ReactLoadableManifest,
|
Manifest as ReactLoadableManifest,
|
||||||
|
@ -107,6 +111,7 @@ function render(
|
||||||
|
|
||||||
type RenderOpts = {
|
type RenderOpts = {
|
||||||
ampEnabled: boolean
|
ampEnabled: boolean
|
||||||
|
ampBindInitData: boolean
|
||||||
staticMarkup: boolean
|
staticMarkup: boolean
|
||||||
buildId: string
|
buildId: string
|
||||||
runtimeConfig?: { [key: string]: any }
|
runtimeConfig?: { [key: string]: any }
|
||||||
|
@ -116,7 +121,8 @@ type RenderOpts = {
|
||||||
dev?: boolean
|
dev?: boolean
|
||||||
ampPath?: string
|
ampPath?: string
|
||||||
amphtml?: boolean
|
amphtml?: boolean
|
||||||
hasAmp?: boolean
|
hasAmp?: boolean,
|
||||||
|
dataOnly?: boolean,
|
||||||
buildManifest: BuildManifest
|
buildManifest: BuildManifest
|
||||||
reactLoadableManifest: ReactLoadableManifest
|
reactLoadableManifest: ReactLoadableManifest
|
||||||
Component: React.ComponentType
|
Component: React.ComponentType
|
||||||
|
@ -128,6 +134,7 @@ type RenderOpts = {
|
||||||
function renderDocument(
|
function renderDocument(
|
||||||
Document: React.ComponentType,
|
Document: React.ComponentType,
|
||||||
{
|
{
|
||||||
|
dataManagerData,
|
||||||
ampEnabled = false,
|
ampEnabled = false,
|
||||||
props,
|
props,
|
||||||
docProps,
|
docProps,
|
||||||
|
@ -148,6 +155,7 @@ function renderDocument(
|
||||||
files,
|
files,
|
||||||
dynamicImports,
|
dynamicImports,
|
||||||
}: RenderOpts & {
|
}: RenderOpts & {
|
||||||
|
dataManagerData: any,
|
||||||
props: any
|
props: any
|
||||||
docProps: any
|
docProps: any
|
||||||
pathname: string
|
pathname: string
|
||||||
|
@ -167,6 +175,7 @@ function renderDocument(
|
||||||
<IsAmpContext.Provider value={amphtml}>
|
<IsAmpContext.Provider value={amphtml}>
|
||||||
<Document
|
<Document
|
||||||
__NEXT_DATA__={{
|
__NEXT_DATA__={{
|
||||||
|
dataManager: dataManagerData,
|
||||||
props, // The result of getInitialProps
|
props, // The result of getInitialProps
|
||||||
page: pathname, // The rendered page
|
page: pathname, // The rendered page
|
||||||
query, // querystring parsed / passed by the user
|
query, // querystring parsed / passed by the user
|
||||||
|
@ -175,7 +184,7 @@ function renderDocument(
|
||||||
runtimeConfig, // runtimeConfig if provided, 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`
|
nextExport, // If this is a page exported by `next export`
|
||||||
dynamicIds:
|
dynamicIds:
|
||||||
dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
|
dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
|
||||||
err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
|
err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
|
||||||
}}
|
}}
|
||||||
ampEnabled={ampEnabled}
|
ampEnabled={ampEnabled}
|
||||||
|
@ -205,6 +214,7 @@ export async function renderToHTML(
|
||||||
const {
|
const {
|
||||||
err,
|
err,
|
||||||
dev = false,
|
dev = false,
|
||||||
|
ampBindInitData = false,
|
||||||
staticMarkup = false,
|
staticMarkup = false,
|
||||||
amphtml = false,
|
amphtml = false,
|
||||||
hasAmp = false,
|
hasAmp = false,
|
||||||
|
@ -264,15 +274,79 @@ export async function renderToHTML(
|
||||||
...getPageFiles(buildManifest, '/_app'),
|
...getPageFiles(buildManifest, '/_app'),
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
|
let dataManager: DataManager | undefined
|
||||||
|
if (ampBindInitData) {
|
||||||
|
dataManager = new DataManager()
|
||||||
|
}
|
||||||
|
|
||||||
const reactLoadableModules: string[] = []
|
const reactLoadableModules: string[] = []
|
||||||
const renderPage = (
|
const renderElementToString = staticMarkup
|
||||||
|
? renderToStaticMarkup
|
||||||
|
: renderToString
|
||||||
|
|
||||||
|
let renderPage: (options: ComponentsEnhancer) => { html: string, head: any } | Promise<{ html: string; head: any }>
|
||||||
|
|
||||||
|
if (ampBindInitData) {
|
||||||
|
renderPage = async (
|
||||||
|
options: ComponentsEnhancer = {},
|
||||||
|
): Promise<{ html: string; head: any }> => {
|
||||||
|
if (ctx.err && ErrorDebug) {
|
||||||
|
return render(renderElementToString, <ErrorDebug error={ctx.err} />)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dev && (props.router || props.Component)) {
|
||||||
|
throw new Error(
|
||||||
|
`'router' and 'Component' can not be returned in getInitialProps from _app.js https://err.sh/zeit/next.js/cant-override-next-props.md`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
App: EnhancedApp,
|
||||||
|
Component: EnhancedComponent,
|
||||||
|
} = enhanceComponents(options, App, Component)
|
||||||
|
|
||||||
|
let recursionCount = 0
|
||||||
|
|
||||||
|
const renderTree = async (): Promise<any> => {
|
||||||
|
recursionCount++
|
||||||
|
// This is temporary, we can remove it once the API is finished.
|
||||||
|
if (recursionCount > 100) {
|
||||||
|
throw new Error('Did 100 promise recursions, bailing out to avoid infinite loop.')
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await render(
|
||||||
|
renderElementToString,
|
||||||
|
<RouterContext.Provider value={router}>
|
||||||
|
<DataManagerContext.Provider value={dataManager}>
|
||||||
|
<IsAmpContext.Provider value={amphtml}>
|
||||||
|
<LoadableContext.Provider
|
||||||
|
value={(moduleName) => reactLoadableModules.push(moduleName)}
|
||||||
|
>
|
||||||
|
<EnhancedApp
|
||||||
|
Component={EnhancedComponent}
|
||||||
|
router={router}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</LoadableContext.Provider>
|
||||||
|
</IsAmpContext.Provider>
|
||||||
|
</DataManagerContext.Provider>
|
||||||
|
</RouterContext.Provider>,
|
||||||
|
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
if (typeof err.then !== 'undefined') {
|
||||||
|
await err
|
||||||
|
return await renderTree()
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await renderTree()
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
renderPage = (
|
||||||
options: ComponentsEnhancer = {},
|
options: ComponentsEnhancer = {},
|
||||||
): { html: string; head: any } => {
|
): { html: string; head: any } => {
|
||||||
const renderElementToString = staticMarkup
|
|
||||||
? renderToStaticMarkup
|
|
||||||
: renderToString
|
|
||||||
|
|
||||||
if (ctx.err && ErrorDebug) {
|
if (ctx.err && ErrorDebug) {
|
||||||
return render(renderElementToString, <ErrorDebug error={ctx.err} />)
|
return render(renderElementToString, <ErrorDebug error={ctx.err} />)
|
||||||
}
|
}
|
||||||
|
@ -290,19 +364,22 @@ export async function renderToHTML(
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
renderElementToString,
|
renderElementToString,
|
||||||
<IsAmpContext.Provider value={amphtml}>
|
<RouterContext.Provider value={router}>
|
||||||
<LoadableContext.Provider
|
<IsAmpContext.Provider value={amphtml}>
|
||||||
value={(moduleName) => reactLoadableModules.push(moduleName)}
|
<LoadableContext.Provider
|
||||||
>
|
value={(moduleName) => reactLoadableModules.push(moduleName)}
|
||||||
<EnhancedApp
|
>
|
||||||
Component={EnhancedComponent}
|
<EnhancedApp
|
||||||
router={router}
|
Component={EnhancedComponent}
|
||||||
{...props}
|
router={router}
|
||||||
/>
|
{...props}
|
||||||
</LoadableContext.Provider>
|
/>
|
||||||
</IsAmpContext.Provider>,
|
</LoadableContext.Provider>
|
||||||
|
</IsAmpContext.Provider>
|
||||||
|
</RouterContext.Provider>,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage })
|
const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage })
|
||||||
// the response might be finished on the getInitialProps call
|
// the response might be finished on the getInitialProps call
|
||||||
|
@ -313,8 +390,18 @@ export async function renderToHTML(
|
||||||
]
|
]
|
||||||
const dynamicImportsIds: any = dynamicImports.map((bundle) => bundle.id)
|
const dynamicImportsIds: any = dynamicImports.map((bundle) => bundle.id)
|
||||||
|
|
||||||
|
let dataManagerData = '[]'
|
||||||
|
if (dataManager) {
|
||||||
|
dataManagerData = JSON.stringify([...dataManager.getData()])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderOpts.dataOnly) {
|
||||||
|
return dataManagerData
|
||||||
|
}
|
||||||
|
|
||||||
return renderDocument(Document, {
|
return renderDocument(Document, {
|
||||||
...renderOpts,
|
...renderOpts,
|
||||||
|
dataManagerData,
|
||||||
props,
|
props,
|
||||||
docProps,
|
docProps,
|
||||||
pathname,
|
pathname,
|
||||||
|
|
51
packages/next/build/babel/plugins/next-data.ts
Normal file
51
packages/next/build/babel/plugins/next-data.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import {PluginObj} from '@babel/core'
|
||||||
|
import {NodePath} from '@babel/traverse'
|
||||||
|
import * as BabelTypes from '@babel/types'
|
||||||
|
|
||||||
|
|
||||||
|
export default function ({ types: t }: {types: typeof BabelTypes}): PluginObj {
|
||||||
|
return {
|
||||||
|
visitor: {
|
||||||
|
ImportDeclaration (path: NodePath<BabelTypes.ImportDeclaration>, state) {
|
||||||
|
const source = path.node.source.value
|
||||||
|
if (source !== 'next/data') return
|
||||||
|
|
||||||
|
const createHookSpecifier = path.get('specifiers').find(specifier => {
|
||||||
|
return specifier.isImportSpecifier() && specifier.node.imported.name === 'createHook'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!createHookSpecifier) return
|
||||||
|
|
||||||
|
const bindingName = createHookSpecifier.node.local.name
|
||||||
|
const binding = path.scope.getBinding(bindingName)
|
||||||
|
|
||||||
|
if (!binding) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.referencePaths.forEach(refPath => {
|
||||||
|
let callExpression = refPath.parentPath
|
||||||
|
|
||||||
|
if (!callExpression.isCallExpression()) return
|
||||||
|
|
||||||
|
let args: any = callExpression.get('arguments')
|
||||||
|
|
||||||
|
if (!args[0]) {
|
||||||
|
throw callExpression.buildCodeFrameError('first argument to createHook should be a function')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args[1]) {
|
||||||
|
callExpression.node.arguments.push(t.objectExpression([]))
|
||||||
|
}
|
||||||
|
|
||||||
|
args = callExpression.get('arguments')
|
||||||
|
|
||||||
|
args[1].node.properties.push(t.objectProperty(
|
||||||
|
t.identifier('key'),
|
||||||
|
t.stringLiteral(state.opts.key)
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -225,8 +225,14 @@ export default function getBaseWebpackConfig (dir: string, {dev = false, isServe
|
||||||
...nodePathList // Support for NODE_PATH environment variable
|
...nodePathList // Support for NODE_PATH environment variable
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
// @ts-ignore this is filtered
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
config.experimental.ampBindInitData && !isServer && {
|
||||||
|
test: /\.(js|mjs|jsx)$/,
|
||||||
|
include: [path.join(dir, 'data')],
|
||||||
|
use: 'next-data-loader'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.(js|mjs|jsx)$/,
|
test: /\.(js|mjs|jsx)$/,
|
||||||
include: [dir, /next-server[\\/]dist[\\/]lib/],
|
include: [dir, /next-server[\\/]dist[\\/]lib/],
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import babelLoader from 'babel-loader'
|
import babelLoader from 'babel-loader'
|
||||||
|
import hash from 'string-hash'
|
||||||
|
import { basename } from 'path'
|
||||||
|
|
||||||
module.exports = babelLoader.custom(babel => {
|
module.exports = babelLoader.custom(babel => {
|
||||||
const presetItem = babel.createConfigItem(require('../../babel/preset'), { type: 'preset' })
|
const presetItem = babel.createConfigItem(require('../../babel/preset'), { type: 'preset' })
|
||||||
|
@ -21,6 +23,7 @@ module.exports = babelLoader.custom(babel => {
|
||||||
return { loader, custom }
|
return { loader, custom }
|
||||||
},
|
},
|
||||||
config (cfg, { source, customOptions: { isServer } }) {
|
config (cfg, { source, customOptions: { isServer } }) {
|
||||||
|
const filename = this.resourcePath
|
||||||
const options = Object.assign({}, cfg.options)
|
const options = Object.assign({}, cfg.options)
|
||||||
if (cfg.hasFilesystemConfig()) {
|
if (cfg.hasFilesystemConfig()) {
|
||||||
for (const file of [cfg.babelrc, cfg.config]) {
|
for (const file of [cfg.babelrc, cfg.config]) {
|
||||||
|
@ -36,6 +39,12 @@ module.exports = babelLoader.custom(babel => {
|
||||||
options.presets = [...options.presets, presetItem]
|
options.presets = [...options.presets, presetItem]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isServer && source.indexOf('next/data') !== -1) {
|
||||||
|
const nextDataPlugin = babel.createConfigItem([require('../../babel/plugins/next-data'), { key: basename(filename) + '-' + hash(filename) }], { type: 'plugin' })
|
||||||
|
options.plugins = options.plugins || []
|
||||||
|
options.plugins.push(nextDataPlugin)
|
||||||
|
}
|
||||||
|
|
||||||
// If the file has `module.exports` we have to transpile commonjs because Babel adds `import` statements
|
// If the file has `module.exports` we have to transpile commonjs because Babel adds `import` statements
|
||||||
// That break webpack, since webpack doesn't support combining commonjs and esmodules
|
// That break webpack, since webpack doesn't support combining commonjs and esmodules
|
||||||
if (source.indexOf('module.exports') !== -1) {
|
if (source.indexOf('module.exports') !== -1) {
|
||||||
|
|
13
packages/next/build/webpack/loaders/next-data-loader.ts
Normal file
13
packages/next/build/webpack/loaders/next-data-loader.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import {loader} from 'webpack'
|
||||||
|
import hash from 'string-hash'
|
||||||
|
import {basename} from 'path'
|
||||||
|
const nextDataLoader: loader.Loader = function (source) {
|
||||||
|
const filename = this.resourcePath
|
||||||
|
return `
|
||||||
|
import {createHook} from 'next/data'
|
||||||
|
|
||||||
|
export default createHook(undefined, {key: ${JSON.stringify(basename(filename) + '-' + hash(filename))}})
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default nextDataLoader
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react'
|
import React, { Suspense } from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import HeadManager from './head-manager'
|
import HeadManager from './head-manager'
|
||||||
import { createRouter } from 'next/router'
|
import { createRouter, makePublicRouterInstance } from 'next/router'
|
||||||
import mitt from 'next-server/dist/lib/mitt'
|
import mitt from 'next-server/dist/lib/mitt'
|
||||||
import { loadGetInitialProps, getURL } from 'next-server/dist/lib/utils'
|
import { loadGetInitialProps, getURL } from 'next-server/dist/lib/utils'
|
||||||
import PageLoader from './page-loader'
|
import PageLoader from './page-loader'
|
||||||
|
@ -9,6 +9,9 @@ import * as envConfig from 'next-server/config'
|
||||||
import { ErrorBoundary } from './error-boundary'
|
import { ErrorBoundary } from './error-boundary'
|
||||||
import Loadable from 'next-server/dist/lib/loadable'
|
import Loadable from 'next-server/dist/lib/loadable'
|
||||||
import { HeadManagerContext } from 'next-server/dist/lib/head-manager-context'
|
import { HeadManagerContext } from 'next-server/dist/lib/head-manager-context'
|
||||||
|
import { DataManagerContext } from 'next-server/dist/lib/data-manager-context'
|
||||||
|
import { RouterContext } from 'next-server/dist/lib/router-context'
|
||||||
|
import { DataManager } from 'next-server/dist/lib/data-manager'
|
||||||
|
|
||||||
// Polyfill Promise globally
|
// Polyfill Promise globally
|
||||||
// This is needed because Webpack's dynamic loading(common chunks) code
|
// This is needed because Webpack's dynamic loading(common chunks) code
|
||||||
|
@ -33,6 +36,9 @@ const {
|
||||||
dynamicIds
|
dynamicIds
|
||||||
} = data
|
} = data
|
||||||
|
|
||||||
|
const d = JSON.parse(window.__NEXT_DATA__.dataManager)
|
||||||
|
export const dataManager = new DataManager(d)
|
||||||
|
|
||||||
const prefix = assetPrefix || ''
|
const prefix = assetPrefix || ''
|
||||||
|
|
||||||
// With dynamic assetPrefix it's no longer possible to set assetPrefix at the build time
|
// With dynamic assetPrefix it's no longer possible to set assetPrefix at the build time
|
||||||
|
@ -183,17 +189,25 @@ async function doRender ({ App, Component, props, err }) {
|
||||||
// In development runtime errors are caught by react-error-overlay.
|
// In development runtime errors are caught by react-error-overlay.
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
renderReactElement((
|
renderReactElement((
|
||||||
<HeadManagerContext.Provider value={headManager.updateHead}>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<App {...appProps} />
|
<RouterContext.Provider value={makePublicRouterInstance(router)}>
|
||||||
</HeadManagerContext.Provider>
|
<DataManagerContext.Provider value={dataManager}>
|
||||||
|
<HeadManagerContext.Provider value={headManager.updateHead}>
|
||||||
|
<App {...appProps} />
|
||||||
|
</HeadManagerContext.Provider>
|
||||||
|
</DataManagerContext.Provider>
|
||||||
|
</RouterContext.Provider>
|
||||||
|
</Suspense>
|
||||||
), appContainer)
|
), appContainer)
|
||||||
} else {
|
} else {
|
||||||
// In production we catch runtime errors using componentDidCatch which will trigger renderError.
|
// In production we catch runtime errors using componentDidCatch which will trigger renderError.
|
||||||
renderReactElement((
|
renderReactElement((
|
||||||
<ErrorBoundary fn={(error) => renderError({ App, err: error }).catch(err => console.error('Error rendering page: ', err))}>
|
<ErrorBoundary fn={(error) => renderError({ App, err: error }).catch(err => console.error('Error rendering page: ', err))}>
|
||||||
<HeadManagerContext.Provider value={headManager.updateHead}>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<App {...appProps} />
|
<HeadManagerContext.Provider value={headManager.updateHead}>
|
||||||
</HeadManagerContext.Provider>
|
<App {...appProps} />
|
||||||
|
</HeadManagerContext.Provider>
|
||||||
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
), appContainer)
|
), appContainer)
|
||||||
}
|
}
|
||||||
|
|
1
packages/next/data.js
Normal file
1
packages/next/data.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('./dist/lib/data')
|
34
packages/next/lib/data.ts
Normal file
34
packages/next/lib/data.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import {useContext} from 'react'
|
||||||
|
import { DataManagerContext } from 'next-server/dist/lib/data-manager-context'
|
||||||
|
import { RouterContext } from 'next-server/dist/lib/router-context'
|
||||||
|
import fetch from 'unfetch'
|
||||||
|
|
||||||
|
export function createHook(fetcher: () => Promise<any>, options: {key: string}) {
|
||||||
|
if (!options.key) {
|
||||||
|
throw new Error('key not provided to createHook options.')
|
||||||
|
}
|
||||||
|
return function useData() {
|
||||||
|
const router: import('next-server/lib/router/router').default = useContext(RouterContext)
|
||||||
|
const dataManager: import('next-server/lib/data-manager').DataManager = useContext(DataManagerContext)
|
||||||
|
const existing = dataManager.get(options.key)
|
||||||
|
if (existing && existing.status === 'resolved') {
|
||||||
|
return existing.result
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore webpack optimization
|
||||||
|
if (process.browser) {
|
||||||
|
const res = fetch(router.route === '/' ? 'index.json' : router.route + '.json').then((res: any) => res.json()).then((result: any) => {
|
||||||
|
dataManager.overwrite(result)
|
||||||
|
})
|
||||||
|
throw res
|
||||||
|
} else {
|
||||||
|
const res = fetcher().then((result) => {
|
||||||
|
dataManager.set(options.key, {
|
||||||
|
status: 'resolved',
|
||||||
|
result,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
throw res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,6 +74,7 @@
|
||||||
"recursive-copy": "2.0.6",
|
"recursive-copy": "2.0.6",
|
||||||
"serialize-javascript": "1.6.1",
|
"serialize-javascript": "1.6.1",
|
||||||
"source-map": "0.6.1",
|
"source-map": "0.6.1",
|
||||||
|
"string-hash": "1.1.3",
|
||||||
"strip-ansi": "3.0.1",
|
"strip-ansi": "3.0.1",
|
||||||
"styled-jsx": "3.2.1",
|
"styled-jsx": "3.2.1",
|
||||||
"terser": "3.16.1",
|
"terser": "3.16.1",
|
||||||
|
|
|
@ -15,8 +15,8 @@ export default class Document extends Component {
|
||||||
_devOnlyInvalidateCacheQueryString: PropTypes.string,
|
_devOnlyInvalidateCacheQueryString: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInitialProps({ renderPage }) {
|
static async getInitialProps({ renderPage }) {
|
||||||
const { html, head } = renderPage()
|
const { html, head } = await renderPage()
|
||||||
const styles = flush()
|
const styles = flush()
|
||||||
return { html, head, styles }
|
return { html, head, styles }
|
||||||
}
|
}
|
||||||
|
|
11
packages/next/types/index.d.ts
vendored
11
packages/next/types/index.d.ts
vendored
|
@ -2,6 +2,17 @@ declare module '@babel/plugin-transform-modules-commonjs';
|
||||||
declare module 'next-server/next-config';
|
declare module 'next-server/next-config';
|
||||||
declare module 'next-server/constants';
|
declare module 'next-server/constants';
|
||||||
declare module 'webpack/lib/GraphHelpers';
|
declare module 'webpack/lib/GraphHelpers';
|
||||||
|
declare module 'unfetch'
|
||||||
|
|
||||||
|
declare module 'next-server/dist/lib/data-manager-context' {
|
||||||
|
import * as all from 'next-server/lib/data-manager-context'
|
||||||
|
export = all
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'next-server/dist/lib/router-context' {
|
||||||
|
import * as all from 'next-server/lib/router-context'
|
||||||
|
export = all
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
declare module 'next/dist/compiled/nanoid/index.js' {
|
declare module 'next/dist/compiled/nanoid/index.js' {
|
||||||
|
|
6
test/integration/data/data/get-uptime.js
Normal file
6
test/integration/data/data/get-uptime.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { createHook } from 'next/data'
|
||||||
|
import os from 'os'
|
||||||
|
|
||||||
|
export default createHook(async () => {
|
||||||
|
return os.uptime()
|
||||||
|
})
|
5
test/integration/data/next.config.js
Normal file
5
test/integration/data/next.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
experimental: {
|
||||||
|
ampBindInitData: true
|
||||||
|
}
|
||||||
|
}
|
7
test/integration/data/pages/about.js
Normal file
7
test/integration/data/pages/about.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return <Link href='/'>
|
||||||
|
<a>home</a>
|
||||||
|
</Link>
|
||||||
|
}
|
6
test/integration/data/pages/index.js
Normal file
6
test/integration/data/pages/index.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import getUptime from '../data/get-uptime'
|
||||||
|
|
||||||
|
export default function Index () {
|
||||||
|
const uptime = getUptime()
|
||||||
|
return <h1>The uptime of the server is {uptime}</h1>
|
||||||
|
}
|
52
test/integration/data/test/index.test.js
Normal file
52
test/integration/data/test/index.test.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/* eslint-env jest */
|
||||||
|
/* global jasmine */
|
||||||
|
import { join } from 'path'
|
||||||
|
import webdriver from 'next-webdriver'
|
||||||
|
import {
|
||||||
|
killApp,
|
||||||
|
findPort,
|
||||||
|
launchApp,
|
||||||
|
renderViaHTTP
|
||||||
|
} from 'next-test-utils'
|
||||||
|
|
||||||
|
const appDir = join(__dirname, '../')
|
||||||
|
let appPort
|
||||||
|
let server
|
||||||
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2
|
||||||
|
|
||||||
|
describe('AMP Bind Initial Data', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
appPort = await findPort()
|
||||||
|
server = await launchApp(appDir, appPort)
|
||||||
|
})
|
||||||
|
afterAll(() => killApp(server))
|
||||||
|
|
||||||
|
it('responds with json with .json extension on page', async () => {
|
||||||
|
const data = await renderViaHTTP(appPort, '/index.json')
|
||||||
|
let isJSON = false
|
||||||
|
try {
|
||||||
|
JSON.parse(data)
|
||||||
|
isJSON = true
|
||||||
|
} catch (_) {}
|
||||||
|
expect(isJSON).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the data during SSR', async () => {
|
||||||
|
const html = await renderViaHTTP(appPort, '/')
|
||||||
|
expect(html).toMatch(/The uptime of the server is.*?\d.*?\d/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a page without data', async () => {
|
||||||
|
const html = await renderViaHTTP(appPort, '/about')
|
||||||
|
expect(html).toMatch(/<a.*?home/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('navigates to a page with data correctly', async () => {
|
||||||
|
const browser = await webdriver(appPort, '/about')
|
||||||
|
await browser.elementByCss('a').click()
|
||||||
|
await browser.waitForElementByCss('h1')
|
||||||
|
const h1Text = await browser.elementByCss('h1').text()
|
||||||
|
expect(h1Text).toMatch(/The uptime of the server is.*?\d.*?\d/)
|
||||||
|
await browser.close()
|
||||||
|
})
|
||||||
|
})
|
12
yarn.lock
12
yarn.lock
|
@ -1679,6 +1679,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
|
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
|
||||||
integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
|
integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
|
||||||
|
|
||||||
|
"@types/string-hash@1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/string-hash/-/string-hash-1.1.1.tgz#4c336e61d1e13ce2d3efaaa5910005fd080e106b"
|
||||||
|
integrity sha512-ijt3zdHi2DmZxQpQTmozXszzDo78V4R3EdvX0jFMfnMH2ZzQSmCbaWOMPGXFUYSzSIdStv78HDjg32m5dxc+tA==
|
||||||
|
|
||||||
"@types/strip-ansi@3.0.0":
|
"@types/strip-ansi@3.0.0":
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/strip-ansi/-/strip-ansi-3.0.0.tgz#9b63d453a6b54aa849182207711a08be8eea48ae"
|
resolved "https://registry.yarnpkg.com/@types/strip-ansi/-/strip-ansi-3.0.0.tgz#9b63d453a6b54aa849182207711a08be8eea48ae"
|
||||||
|
@ -8560,7 +8565,12 @@ mute-stream@~0.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
|
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
|
||||||
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
|
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
|
||||||
|
|
||||||
nan@^2.10.0, nan@^2.9.2:
|
nan@^2.10.0:
|
||||||
|
version "2.13.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
|
||||||
|
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
|
||||||
|
|
||||||
|
nan@^2.9.2:
|
||||||
version "2.12.1"
|
version "2.12.1"
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
|
||||||
integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
|
integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
|
||||||
|
|
Loading…
Reference in a new issue