From e6d79bb88ee47b9f56a84371f65a9eaac75c6fa4 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Thu, 11 Oct 2018 16:56:12 +0200 Subject: [PATCH 01/18] Remove pathname (#5428) Same as #5424 --- client/index.js | 7 +- server/document.js | 20 +++--- server/render.js | 7 +- test/integration/production/test/security.js | 68 +++++++++++++++++--- 4 files changed, 75 insertions(+), 27 deletions(-) diff --git a/client/index.js b/client/index.js index 4f7b83b9..8ca23c0d 100644 --- a/client/index.js +++ b/client/index.js @@ -24,7 +24,6 @@ const { props, err, page, - pathname, query, buildId, assetPrefix, @@ -83,7 +82,7 @@ export default async ({ Component = await pageLoader.loadPage(page) if (typeof Component !== 'function') { - 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: "${page}"`) } } catch (error) { // This catches errors like throwing in the top level of a module @@ -92,7 +91,7 @@ export default async ({ await Loadable.preloadReady(dynamicIds || []) - router = createRouter(pathname, query, asPath, { + router = createRouter(page, query, asPath, { initialProps: props, pageLoader, App, @@ -141,7 +140,7 @@ export async function renderError (props) { // Otherwise, we need to call `getInitialProps` on `App` before mounting. const initProps = props.props ? props.props - : await loadGetInitialProps(App, {Component: ErrorComponent, router, ctx: {err, pathname, query, asPath}}) + : await loadGetInitialProps(App, {Component: ErrorComponent, router, ctx: {err, pathname: page, query, asPath}}) await doRender({...props, err, Component: ErrorComponent, props: initProps}) } diff --git a/server/document.js b/server/document.js index cfb38f36..52ab4dc0 100644 --- a/server/document.js +++ b/server/document.js @@ -101,8 +101,8 @@ export class Head extends Component { render () { const { head, styles, assetPrefix, __NEXT_DATA__ } = this.context._documentProps - const { page, pathname, buildId } = __NEXT_DATA__ - const pagePathname = getPagePathname(pathname) + const { page, buildId } = __NEXT_DATA__ + const pagePathname = getPagePathname(page) let children = this.props.children // show a warning if Head contains (only in development) @@ -186,21 +186,21 @@ export class NextScript extends Component { static getInlineScriptSource (documentProps) { const { __NEXT_DATA__ } = documentProps - const { page, pathname } = __NEXT_DATA__ - return `__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)};__NEXT_LOADED_PAGES__=[];__NEXT_REGISTER_PAGE=function(r,f){__NEXT_LOADED_PAGES__.push([r, f])}${page === '/_error' ? `;__NEXT_REGISTER_PAGE(${htmlescape(pathname)},function(){var e = new Error('Page does not exist: ${htmlescape(pathname)}');e.statusCode=404;return {error:e}})`:''}` + const { page } = __NEXT_DATA__ + return `__NEXT_DATA__ = ${htmlescape(__NEXT_DATA__)};__NEXT_LOADED_PAGES__=[];__NEXT_REGISTER_PAGE=function(r,f){__NEXT_LOADED_PAGES__.push([r, f])}` } render () { const { staticMarkup, assetPrefix, devFiles, __NEXT_DATA__ } = this.context._documentProps - const { page, pathname, buildId } = __NEXT_DATA__ - const pagePathname = getPagePathname(pathname) + const { page, buildId } = __NEXT_DATA__ + const pagePathname = getPagePathname(page) return <Fragment> {devFiles ? devFiles.map((file) => <script key={file} src={`${assetPrefix}/_next/${file}`} nonce={this.props.nonce} />) : null} {staticMarkup ? null : <script nonce={this.props.nonce} dangerouslySetInnerHTML={{ __html: NextScript.getInlineScriptSource(this.context._documentProps) }} />} - {page !== '/_error' && <script async id={`__NEXT_PAGE__${pathname}`} src={`${assetPrefix}/_next/static/${buildId}/pages${pagePathname}`} nonce={this.props.nonce} />} + {page !== '/_error' && <script async id={`__NEXT_PAGE__${page}`} src={`${assetPrefix}/_next/static/${buildId}/pages${pagePathname}`} nonce={this.props.nonce} />} <script async id={`__NEXT_PAGE__/_app`} src={`${assetPrefix}/_next/static/${buildId}/pages/_app.js`} nonce={this.props.nonce} /> <script async id={`__NEXT_PAGE__/_error`} src={`${assetPrefix}/_next/static/${buildId}/pages/_error.js`} nonce={this.props.nonce} /> {staticMarkup ? null : this.getDynamicChunks()} @@ -209,10 +209,10 @@ export class NextScript extends Component { } } -function getPagePathname (pathname) { - if (pathname === '/') { +function getPagePathname (page) { + if (page === '/') { return '/index.js' } - return `${pathname}.js` + return `${page}.js` } diff --git a/server/render.js b/server/render.js index 238d75eb..964017b0 100644 --- a/server/render.js +++ b/server/render.js @@ -89,14 +89,14 @@ async function doRender (req, res, pathname, query, { Component = Component.default || Component if (typeof Component !== 'function') { - 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: "${page}"`) } App = App.default || App Document = Document.default || Document const asPath = req.url - const ctx = { err, req, res, pathname, query, asPath } - const router = new Router(pathname, query, asPath) + const ctx = { err, req, res, pathname: page, query, asPath } + const router = new Router(page, query, asPath) const props = await loadGetInitialProps(App, {Component, router, ctx}) const devFiles = buildManifest.devFiles const files = [ @@ -168,7 +168,6 @@ async function doRender (req, res, pathname, query, { __NEXT_DATA__: { props, // The result of getInitialProps page, // The rendered page - pathname, // The requested path 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 assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML diff --git a/test/integration/production/test/security.js b/test/integration/production/test/security.js index 1f83c145..4c5fa07c 100644 --- a/test/integration/production/test/security.js +++ b/test/integration/production/test/security.js @@ -3,9 +3,21 @@ import { readFileSync } from 'fs' import { join } from 'path' -import { renderViaHTTP, waitFor } from 'next-test-utils' +import { renderViaHTTP, getBrowserBodyText, waitFor } from 'next-test-utils' import webdriver from 'next-webdriver' +// Does the same evaluation checking for INJECTED for 15 seconds, triggering every 500ms +async function checkInjected (browser) { + const start = Date.now() + while (Date.now() - start < 15000) { + const bodyText = await getBrowserBodyText(browser) + if (/INJECTED/.test(bodyText)) { + throw new Error('Vulnerable to XSS attacks') + } + await waitFor(500) + } +} + module.exports = (context) => { describe('With Security Related Issues', () => { it('should only access files inside .next directory', async () => { @@ -28,18 +40,56 @@ module.exports = (context) => { }) it('should prevent URI based XSS attacks', async () => { - const browser = await webdriver(context.appPort, '/\',document.body.innerHTML="HACKED",\'') - // Wait 5 secs to make sure we load all the client side JS code - await waitFor(5000) + const browser = await webdriver(context.appPort, '/\',document.body.innerHTML="INJECTED",\'') + await checkInjected(browser) + browser.quit() + }) - const bodyText = await browser - .elementByCss('body').text() + it('should prevent URI based XSS attacks using single quotes', async () => { + const browser = await webdriver(context.appPort, `/'-(document.body.innerHTML='INJECTED')-'`) + await checkInjected(browser) + browser.close() + }) - if (/HACKED/.test(bodyText)) { - throw new Error('Vulnerable to XSS attacks') - } + it('should prevent URI based XSS attacks using double quotes', async () => { + const browser = await webdriver(context.appPort, `/"-(document.body.innerHTML='INJECTED')-"`) + await checkInjected(browser) browser.close() }) + + it('should prevent URI based XSS attacks using semicolons and double quotes', async () => { + const browser = await webdriver(context.appPort, `/;"-(document.body.innerHTML='INJECTED')-"`) + await checkInjected(browser) + + browser.close() + }) + + it('should prevent URI based XSS attacks using semicolons and single quotes', async () => { + const browser = await webdriver(context.appPort, `/;'-(document.body.innerHTML='INJECTED')-'`) + await checkInjected(browser) + + browser.close() + }) + + it('should prevent URI based XSS attacks using src', async () => { + const browser = await webdriver(context.appPort, `/javascript:(document.body.innerHTML='INJECTED')`) + await checkInjected(browser) + + browser.close() + }) + + it('should prevent URI based XSS attacks using querystring', async () => { + const browser = await webdriver(context.appPort, `/?javascript=(document.body.innerHTML='INJECTED')`) + await checkInjected(browser) + + browser.close() + }) + + it('should prevent URI based XSS attacks using querystring and quotes', async () => { + const browser = await webdriver(context.appPort, `/?javascript="(document.body.innerHTML='INJECTED')"`) + await checkInjected(browser) + browser.close() + }) }) } From 45c554a3283dfa7ab2dc007f5f03883c738a4b58 Mon Sep 17 00:00:00 2001 From: Tim Neutkens <timneutkens@me.com> Date: Thu, 11 Oct 2018 17:08:15 +0200 Subject: [PATCH 02/18] 7.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fc3cc078..4d12a126 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "7.0.1", + "version": "7.0.2", "description": "Minimalistic framework for server-rendered React applications", "main": "./dist/server/next.js", "license": "MIT", From ae8a49fe785054d874e1c8dbe861c59fd5fdbb06 Mon Sep 17 00:00:00 2001 From: Muhaimin CS <muhaimincs@gmail.com> Date: Mon, 15 Oct 2018 18:16:05 +0800 Subject: [PATCH 03/18] fix typo (#5451) --- examples/with-apollo-and-redux/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/with-apollo-and-redux/README.md b/examples/with-apollo-and-redux/README.md index 7f44cbd9..f97769fc 100644 --- a/examples/with-apollo-and-redux/README.md +++ b/examples/with-apollo-and-redux/README.md @@ -41,7 +41,7 @@ now ## The idea behind the example This example serves as a conduit if you were using Apollo 1.X with Redux, and are migrating to Apollo 2.x, however, you have chosen not to manage your entire application state within Apollo (`apollo-link-state`). -In 2.0.0, Apollo severs out-of-the-box support for redux in favor of Apollo's state management. This example aims to be an amalgamation of the [`with-apollo`](https://github.com/zeit/next.js/tree/master/examples/with-apollo) and [`with-redux`](https://github.com/zeit/next.js/tree/master/examples/with-redux) examples. +In 2.0.0, Apollo servers out-of-the-box support for redux in favor of Apollo's state management. This example aims to be an amalgamation of the [`with-apollo`](https://github.com/zeit/next.js/tree/master/examples/with-apollo) and [`with-redux`](https://github.com/zeit/next.js/tree/master/examples/with-redux) examples. Note that you can access the redux store like you normally would using `react-redux`'s `connect`. Here's a quick example: From c43975a927cbb8f09a8569adbdafe278252b4c52 Mon Sep 17 00:00:00 2001 From: Justin Stahlman <jstahlman@mac.com> Date: Sun, 2 Dec 2018 08:17:23 -0500 Subject: [PATCH 04/18] Use more recent version of svg plugin (#5788) The demo did not work. Updating to babel-plugin-inline-react-svg v1.0.1 did work. --- examples/svg-components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/svg-components/package.json b/examples/svg-components/package.json index 46e131f1..65d2dc5d 100644 --- a/examples/svg-components/package.json +++ b/examples/svg-components/package.json @@ -12,6 +12,6 @@ "react-dom": "latest" }, "devDependencies": { - "babel-plugin-inline-react-svg": "^0.2.0" + "babel-plugin-inline-react-svg": "^1.0.1" } } From c867b0ce9ce4a42896b6362c6ec43630e398de0a Mon Sep 17 00:00:00 2001 From: Oscar Busk <oscar.busk@gmail.com> Date: Mon, 10 Dec 2018 12:14:29 +0100 Subject: [PATCH 05/18] Fix paths when built on windows (#5795) This PR Fixes #4920 So the problem is that when a next.js application is built on windows, the `pages-manifest.json` file is created with backslashes. If this built application is deployed to a linux hosting enviroment, the server will fail when trying to load the modules. ``` Error: Cannot find module '/user_code/next/server/bundles\pages\index.js ``` My simple solution is to modify the `pages-manifest.json` to always use linux separator (`/`), then also modify `server/require.js` to, when requiring page, replace any separator (`\` or `/`) with current platform-specific file separator (`require('path').sep`). The fix in `server/require.js` would be sufficient, but my opinion is that having some cross-platform consistency is nice. This change was tested by bulding an application in windows and running it in linux and windows, aswell as building an application in linux and running it in linux and windows. The related tests was also run. --- build/webpack/plugins/pages-manifest-plugin.js | 3 ++- test/integration/production/test/index.test.js | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/build/webpack/plugins/pages-manifest-plugin.js b/build/webpack/plugins/pages-manifest-plugin.js index ff580637..7713d783 100644 --- a/build/webpack/plugins/pages-manifest-plugin.js +++ b/build/webpack/plugins/pages-manifest-plugin.js @@ -24,7 +24,8 @@ export default class PagesManifestPlugin { } const {name} = entry - pages[`/${pagePath.replace(/\\/g, '/')}`] = name + // Write filename, replace any backslashes in path (on windows) with forwardslashes for cross-platform consistency. + pages[`/${pagePath.replace(/\\/g, '/')}`] = name.replace(/\\/g, '/') } if (typeof pages['/index'] !== 'undefined') { diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js index 1b78a0d0..0aca7215 100644 --- a/test/integration/production/test/index.test.js +++ b/test/integration/production/test/index.test.js @@ -16,7 +16,7 @@ import fetch from 'node-fetch' import dynamicImportTests from './dynamic' import processEnv from './process-env' import security from './security' -import {BUILD_MANIFEST, REACT_LOADABLE_MANIFEST} from 'next/constants' +import {BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, PAGES_MANIFEST} from 'next/constants' const appDir = join(__dirname, '../') let appPort @@ -276,6 +276,17 @@ describe('Production Usage', () => { expect(serverSideJsBody).toMatch(/404/) }) + it('should not put backslashes in pages-manifest.json', () => { + // Whatever platform you build on, pages-manifest.json should use forward slash (/) + // See: https://github.com/zeit/next.js/issues/4920 + const pagesManifest = require(join('..', '.next', 'server', PAGES_MANIFEST)) + + for (let key of Object.keys(pagesManifest)) { + expect(key).not.toMatch(/\\/) + expect(pagesManifest[key]).not.toMatch(/\\/) + } + }) + dynamicImportTests(context, (p, q) => renderViaHTTP(context.appPort, p, q)) processEnv(context) From cd1d3640a9abfa3df3cadf712fe095aa5326a089 Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz <sheerun@sher.pl> Date: Mon, 10 Dec 2018 23:59:12 +0100 Subject: [PATCH 06/18] Improve with-sentry example (#5727) * Improve with-sentry example * remove nonexisting keys from request and update errorInfo handling * readd query and pathname * read query and params and add pathname and query to client --- examples/with-sentry/README.md | 4 ++ examples/with-sentry/next.config.js | 23 ++++++++++ examples/with-sentry/package.json | 16 ++++--- examples/with-sentry/pages/_app.js | 37 ++++++++++------ examples/with-sentry/pages/index.js | 33 ++++++++++++-- examples/with-sentry/server.js | 66 ++++++++++++++++++++++++++++ examples/with-sentry/utils/sentry.js | 63 ++++++++++++++++++++++++++ 7 files changed, 219 insertions(+), 23 deletions(-) create mode 100644 examples/with-sentry/next.config.js create mode 100644 examples/with-sentry/server.js create mode 100644 examples/with-sentry/utils/sentry.js diff --git a/examples/with-sentry/README.md b/examples/with-sentry/README.md index 235ece55..28fecc99 100644 --- a/examples/with-sentry/README.md +++ b/examples/with-sentry/README.md @@ -47,3 +47,7 @@ This example show you how to add Sentry to catch errors in next.js You will need a Sentry DSN for your project. You can get it from the Settings of your Project, in **Client Keys (DSN)**, and copy the string labeled **DSN (Public)**. Note that if you are using a custom server, there is logging available for common platforms: https://docs.sentry.io/platforms/javascript/express/?platform=node + +You can set SENTRY_DSN in next.config.js + +If you want sentry to show non-minified sources you need to set SENTRY_TOKEN environment variable when starting server. You can find it in project settings under "Security Token" section. diff --git a/examples/with-sentry/next.config.js b/examples/with-sentry/next.config.js new file mode 100644 index 00000000..61b2b70f --- /dev/null +++ b/examples/with-sentry/next.config.js @@ -0,0 +1,23 @@ +const webpack = require('webpack') +const nextSourceMaps = require('@zeit/next-source-maps')() + +const SENTRY_DSN = '' + +module.exports = nextSourceMaps({ + webpack: (config, { dev, isServer, buildId }) => { + if (!dev) { + config.plugins.push( + new webpack.DefinePlugin({ + 'process.env.SENTRY_DSN': JSON.stringify(SENTRY_DSN), + 'process.env.SENTRY_RELEASE': JSON.stringify(buildId) + }) + ) + } + + if (!isServer) { + config.resolve.alias['@sentry/node'] = '@sentry/browser' + } + + return config + } +}) diff --git a/examples/with-sentry/package.json b/examples/with-sentry/package.json index b511f11b..4e8b8aae 100644 --- a/examples/with-sentry/package.json +++ b/examples/with-sentry/package.json @@ -2,15 +2,21 @@ "name": "with-sentry", "version": "1.0.0", "scripts": { - "dev": "next", + "dev": "node server.js", "build": "next build", - "start": "next start" + "start": "NODE_ENV=production node server.js" }, "dependencies": { - "next": "latest", - "@sentry/browser": "^4.0.4", + "@sentry/browser": "^4.3.4", + "@sentry/node": "^4.3.4", + "@zeit/next-source-maps": "0.0.4-canary.0", + "cookie-parser": "^1.4.3", + "express": "^4.16.4", + "js-cookie": "^2.2.0", + "next": "7.0.2", "react": "^16.5.2", - "react-dom": "^16.5.2" + "react-dom": "^16.5.2", + "uuid": "^3.3.2" }, "license": "ISC" } diff --git a/examples/with-sentry/pages/_app.js b/examples/with-sentry/pages/_app.js index 61d194db..7d91e4ca 100644 --- a/examples/with-sentry/pages/_app.js +++ b/examples/with-sentry/pages/_app.js @@ -1,23 +1,32 @@ import App from 'next/app' -import * as Sentry from '@sentry/browser' +import { captureException } from '../utils/sentry' -const SENTRY_PUBLIC_DSN = '' +class MyApp extends App { + // This reports errors before rendering, when fetching initial props + static async getInitialProps (appContext) { + const { Component, ctx } = appContext -export default class MyApp extends App { - constructor (...args) { - super(...args) - Sentry.init({dsn: SENTRY_PUBLIC_DSN}) + let pageProps = {} + + try { + if (Component.getInitialProps) { + pageProps = await Component.getInitialProps(ctx) + } + } catch (e) { + captureException(e, ctx) + throw e // you can also skip re-throwing and set property on pageProps + } + + return { + pageProps + } } + // This reports errors thrown while rendering components componentDidCatch (error, errorInfo) { - Sentry.configureScope(scope => { - Object.keys(errorInfo).forEach(key => { - scope.setExtra(key, errorInfo[key]) - }) - }) - Sentry.captureException(error) - - // This is needed to render errors correctly in development / production + captureException(error, { errorInfo }) super.componentDidCatch(error, errorInfo) } } + +export default MyApp diff --git a/examples/with-sentry/pages/index.js b/examples/with-sentry/pages/index.js index 826020e3..cde8a5ec 100644 --- a/examples/with-sentry/pages/index.js +++ b/examples/with-sentry/pages/index.js @@ -1,23 +1,48 @@ import React from 'react' +import Link from 'next/link' class Index extends React.Component { + static getInitialProps ({ query, req }) { + if (query.raiseError) { + throw new Error('Error in getInitialProps') + } + } + state = { raiseError: false } componentDidUpdate () { - if (this.state.raiseError) { - throw new Error('Houston, we have a problem') + if (this.state.raiseErrorInUpdate) { + throw new Error('Error in componentDidUpdate') } } - raiseError = () => this.setState({ raiseError: true }) + raiseErrorInUpdate = () => this.setState({ raiseErrorInUpdate: '1' }) + raiseErrorInRender = () => this.setState({ raiseErrorInRender: '1' }) render () { + if (this.state.raiseErrorInRender) { + throw new Error('Error in render') + } + return ( <div> <h2>Index page</h2> - <button onClick={this.raiseError}>Click to raise the error</button> + <ul> + <li><a href='#' onClick={this.raiseErrorInRender}>Raise the error in render</a></li> + <li><a href='#' onClick={this.raiseErrorInUpdate}>Raise the error in componentDidUpdate</a></li> + <li> + <Link href={{ pathname: '/', query: { raiseError: '1' } }}> + <a>Raise error in getInitialProps of client-loaded page</a> + </Link> + </li> + <li> + <a href='/?raiseError=1'> + Raise error in getInitialProps of server-loaded page + </a> + </li> + </ul> </div> ) } diff --git a/examples/with-sentry/server.js b/examples/with-sentry/server.js new file mode 100644 index 00000000..7ade81e4 --- /dev/null +++ b/examples/with-sentry/server.js @@ -0,0 +1,66 @@ +const next = require('next') +const express = require('express') +const cookieParser = require('cookie-parser') +const Sentry = require('@sentry/node') +const uuidv4 = require('uuid/v4') +const port = parseInt(process.env.PORT, 10) || 3000 +const dev = process.env.NODE_ENV !== 'production' +const app = next({ dev }) +const handle = app.getRequestHandler() + +require('./utils/sentry') + +app.prepare() + .then(() => { + const server = express() + + // This attaches request information to sentry errors + server.use(Sentry.Handlers.requestHandler()) + + server.use(cookieParser()) + + server.use((req, res, next) => { + const htmlPage = + !req.path.match(/^\/(_next|static)/) && + !req.path.match(/\.(js|map)$/) && + req.accepts('text/html', 'text/css', 'image/png') === 'text/html' + + if (!htmlPage) { + next() + return + } + + if (!req.cookies.sid || req.cookies.sid.length === 0) { + req.cookies.sid = uuidv4() + res.cookie('sid', req.cookies.sid) + } + + next() + }) + + // In production we don't want to serve sourcemaps for anyone + if (!dev) { + const hasSentryToken = !!process.env.SENTRY_TOKEN + server.get(/\.map$/, (req, res, next) => { + if (hasSentryToken && req.headers['x-sentry-token'] !== process.env.SENTRY_TOKEN) { + res + .status(401) + .send( + 'Authentication access token is required to access the source map.' + ) + return + } + next() + }) + } + + server.get('*', (req, res) => handle(req, res)) + + // This handles errors if they are thrown before raching the app + server.use(Sentry.Handlers.errorHandler()) + + server.listen(port, err => { + if (err) throw err + console.log(`> Ready on http://localhost:${port}`) + }) + }) diff --git a/examples/with-sentry/utils/sentry.js b/examples/with-sentry/utils/sentry.js new file mode 100644 index 00000000..458c9019 --- /dev/null +++ b/examples/with-sentry/utils/sentry.js @@ -0,0 +1,63 @@ +const Sentry = require('@sentry/node') +const Cookie = require('js-cookie') + +if (process.env.SENTRY_DSN) { + Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: process.env.SENTRY_RELEASE, + maxBreadcrumbs: 50, + attachStacktrace: true + }) +} + +function captureException (err, { req, res, errorInfo, query, pathname }) { + Sentry.configureScope(scope => { + if (err.message) { + // De-duplication currently doesn't work correctly for SSR / browser errors + // so we force deduplication by error message if it is present + scope.setFingerprint([err.message]) + } + + if (err.statusCode) { + scope.setExtra('statusCode', err.statusCode) + } + + if (res && res.statusCode) { + scope.setExtra('statusCode', res.statusCode) + } + + if (process.browser) { + scope.setTag('ssr', false) + scope.setExtra('query', query) + scope.setExtra('pathname', pathname) + + // On client-side we use js-cookie package to fetch it + const sessionId = Cookie.get('sid') + if (sessionId) { + scope.setUser({ id: sessionId }) + } + } else { + scope.setTag('ssr', true) + scope.setExtra('url', req.url) + scope.setExtra('method', req.method) + scope.setExtra('headers', req.headers) + scope.setExtra('params', req.params) + scope.setExtra('query', req.query) + + // On server-side we take session cookie directly from request + if (req.cookies.sid) { + scope.setUser({ id: req.cookies.sid }) + } + } + + if (errorInfo) { + scope.setExtra('componentStack', errorInfo.componentStack) + } + }) + + Sentry.captureException(err) +} + +module.exports = { + captureException +} From f7477a9e2e74179b6c6a1b8afdc2eb63407e7419 Mon Sep 17 00:00:00 2001 From: Dale Inverarity <dale@diit.ca> Date: Sun, 16 Dec 2018 10:23:18 -0500 Subject: [PATCH 07/18] Adding is `@tailwind components;` (#5897) Without `@tailwind components;` plugins (like container which is added by default) will not work. See: https://github.com/tailwindcss/tailwindcss/issues/446#issuecomment-378792892 for details --- examples/with-tailwindcss/styles/index.css | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/with-tailwindcss/styles/index.css b/examples/with-tailwindcss/styles/index.css index c3b331bb..fbf5970a 100644 --- a/examples/with-tailwindcss/styles/index.css +++ b/examples/with-tailwindcss/styles/index.css @@ -1,6 +1,7 @@ @import "./button.css"; @tailwind preflight; +@tailwind components; @tailwind utilities; .hero { From ab46c45146ca8a1677d9ecacc21522fc60a17397 Mon Sep 17 00:00:00 2001 From: tangye <447084+tangye1234@users.noreply.github.com> Date: Sat, 12 Jan 2019 01:29:59 +0800 Subject: [PATCH 08/18] should not change method to replaceState unless asPath is the same (#6033) original code in `/lib/router/router.js` ``` urlIsNew (pathname, query) { return this.pathname !== pathname || !shallowEquals(query, this.query) } ``` the urlIsNew compare `this.pathname` to an argument `pathname` the invokers: ``` // If asked to change the current URL we should reload the current page // (not location.reload() but reload getInitialProps and other Next.js stuffs) // We also need to set the method = replaceState always // as this should not go into the history (That's how browsers work) if (!this.urlIsNew(asPathname, asQuery)) { method = 'replaceState' } ``` the parameter here is `asPathname` destructured from `asPath` so here is a problem when we reuse a single page rendered in two asPaths pages/a.js ``` <> <Link href='/a'><a>goto a</a></Link> <Link href='/a' as='/b'><a>goto b</a></Link> </> ``` If we navigate to page /a, then click 'goto b', actually the history is replaced, not pushed. It is expected that history could be correctly pushed and popped as long as the browser url is changed. --- lib/router/router.js | 10 ++++++---- test/integration/basic/pages/nav/as-path-pushstate.js | 11 ++++++++++- test/integration/basic/test/client-navigation.js | 5 +++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/router/router.js b/lib/router/router.js index c0e9ff1d..917202e0 100644 --- a/lib/router/router.js +++ b/lib/router/router.js @@ -164,14 +164,14 @@ export default class Router { return true } - const { pathname: asPathname, query: asQuery } = parse(as, true) const { pathname, query } = parse(url, true) // If asked to change the current URL we should reload the current page // (not location.reload() but reload getInitialProps and other Next.js stuffs) // We also need to set the method = replaceState always // as this should not go into the history (That's how browsers work) - if (!this.urlIsNew(asPathname, asQuery)) { + // We should compare the new asPath to the current asPath, not the url + if (!this.urlIsNew(as)) { method = 'replaceState' } @@ -339,8 +339,10 @@ export default class Router { } } - urlIsNew (pathname, query) { - return this.pathname !== pathname || !shallowEquals(query, this.query) + urlIsNew (asPath) { + const { pathname, query } = parse(asPath, true) + const { pathname: curPathname } = parse(this.asPath, true) + return curPathname !== pathname || !shallowEquals(query, this.query) } isShallowRoutingPossible (route) { diff --git a/test/integration/basic/pages/nav/as-path-pushstate.js b/test/integration/basic/pages/nav/as-path-pushstate.js index a5c1482b..a85e0ac2 100644 --- a/test/integration/basic/pages/nav/as-path-pushstate.js +++ b/test/integration/basic/pages/nav/as-path-pushstate.js @@ -9,7 +9,16 @@ export default withRouter(({router: {asPath, query}}) => { <a id='hello'>hello</a> </Link> </div> - + <div> + <Link href='/nav/as-path-pushstate' as='/something/else'> + <a id='else'>else</a> + </Link> + </div> + <div> + <Link href='/nav/as-path-pushstate' as='/nav/as-path-pushstate'> + <a id='hello2'>normal hello</a> + </Link> + </div> {query.something === 'hello' && <Link href='/nav/as-path-pushstate?something=hello' as='/something/same-query'> <a id='same-query'>same query</a> </Link>} diff --git a/test/integration/basic/test/client-navigation.js b/test/integration/basic/test/client-navigation.js index fcdbfa6a..143d49fd 100644 --- a/test/integration/basic/test/client-navigation.js +++ b/test/integration/basic/test/client-navigation.js @@ -648,6 +648,11 @@ export default (context, render) => { await browser.back().waitForElementByCss('#something-hello') const queryThree = JSON.parse(await browser.elementByCss('#router-query').text()) expect(queryThree.something).toBe('hello') + await browser.elementByCss('#else').click().waitForElementByCss('#something-else') + await browser.elementByCss('#hello2').click().waitForElementByCss('#nav-as-path-pushstate') + await browser.back().waitForElementByCss('#something-else') + const queryFour = JSON.parse(await browser.elementByCss('#router-query').text()) + expect(queryFour.something).toBe(undefined) } finally { if (browser) { browser.close() From 155423f26b4fce29f9598627f77d89f8981fec4e Mon Sep 17 00:00:00 2001 From: Tim Neutkens <tim@timneutkens.nl> Date: Sun, 10 Feb 2019 05:32:32 +0100 Subject: [PATCH 09/18] Bring in terser-webpack-plugin (backport #6231 to master) (#6232) * Bring in terser-webpack-plugin (backport #6231 to master) * Use correct path for ignore * Comment out schema --- build/webpack.js | 2 +- .../plugins/terser-webpack-plugin/LICENSE | 20 + .../terser-webpack-plugin/src/TaskRunner.js | 106 +++++ .../plugins/terser-webpack-plugin/src/cjs.js | 3 + .../terser-webpack-plugin/src/index.js | 423 ++++++++++++++++++ .../terser-webpack-plugin/src/minify.js | 186 ++++++++ .../terser-webpack-plugin/src/options.json | 169 +++++++ .../terser-webpack-plugin/src/worker.js | 21 + package.json | 9 +- 9 files changed, 937 insertions(+), 2 deletions(-) create mode 100644 build/webpack/plugins/terser-webpack-plugin/LICENSE create mode 100644 build/webpack/plugins/terser-webpack-plugin/src/TaskRunner.js create mode 100644 build/webpack/plugins/terser-webpack-plugin/src/cjs.js create mode 100644 build/webpack/plugins/terser-webpack-plugin/src/index.js create mode 100644 build/webpack/plugins/terser-webpack-plugin/src/minify.js create mode 100644 build/webpack/plugins/terser-webpack-plugin/src/options.json create mode 100644 build/webpack/plugins/terser-webpack-plugin/src/worker.js diff --git a/build/webpack.js b/build/webpack.js index 764fc6b8..3ddab93b 100644 --- a/build/webpack.js +++ b/build/webpack.js @@ -19,7 +19,7 @@ import ChunkNamesPlugin from './webpack/plugins/chunk-names-plugin' import { ReactLoadablePlugin } from './webpack/plugins/react-loadable-plugin' import {SERVER_DIRECTORY, NEXT_PROJECT_ROOT, NEXT_PROJECT_ROOT_NODE_MODULES, NEXT_PROJECT_ROOT_DIST, DEFAULT_PAGES_DIR, REACT_LOADABLE_MANIFEST, CLIENT_STATIC_FILES_RUNTIME_WEBPACK, CLIENT_STATIC_FILES_RUNTIME_MAIN} from '../lib/constants' import AutoDllPlugin from 'autodll-webpack-plugin' -import TerserPlugin from 'terser-webpack-plugin' +import TerserPlugin from './webpack/plugins/terser-webpack-plugin/src/cjs.js' // The externals config makes sure that // on the server side when modules are diff --git a/build/webpack/plugins/terser-webpack-plugin/LICENSE b/build/webpack/plugins/terser-webpack-plugin/LICENSE new file mode 100644 index 00000000..8c11fc72 --- /dev/null +++ b/build/webpack/plugins/terser-webpack-plugin/LICENSE @@ -0,0 +1,20 @@ +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/build/webpack/plugins/terser-webpack-plugin/src/TaskRunner.js b/build/webpack/plugins/terser-webpack-plugin/src/TaskRunner.js new file mode 100644 index 00000000..82e6664e --- /dev/null +++ b/build/webpack/plugins/terser-webpack-plugin/src/TaskRunner.js @@ -0,0 +1,106 @@ +import os from 'os'; + +import cacache from 'cacache'; +import findCacheDir from 'find-cache-dir'; +import workerFarm from 'worker-farm'; +import serialize from 'serialize-javascript'; + +import minify from './minify'; + +const worker = require.resolve('./worker'); + +export default class TaskRunner { + constructor(options = {}) { + const { cache, parallel } = options; + this.cacheDir = + cache === true ? findCacheDir({ name: 'terser-webpack-plugin' }) : cache; + // In some cases cpus() returns undefined + // https://github.com/nodejs/node/issues/19022 + const cpus = os.cpus() || { length: 1 }; + this.maxConcurrentWorkers = + parallel === true + ? cpus.length - 1 + : Math.min(Number(parallel) || 0, cpus.length - 1); + } + + run(tasks, callback) { + /* istanbul ignore if */ + if (!tasks.length) { + callback(null, []); + return; + } + + if (this.maxConcurrentWorkers > 1) { + const workerOptions = + process.platform === 'win32' + ? { + maxConcurrentWorkers: this.maxConcurrentWorkers, + maxConcurrentCallsPerWorker: 1, + } + : { maxConcurrentWorkers: this.maxConcurrentWorkers }; + this.workers = workerFarm(workerOptions, worker); + this.boundWorkers = (options, cb) => { + try { + this.workers(serialize(options), cb); + } catch (error) { + // worker-farm can fail with ENOMEM or something else + cb(error); + } + }; + } else { + this.boundWorkers = (options, cb) => { + try { + cb(null, minify(options)); + } catch (error) { + cb(error); + } + }; + } + + let toRun = tasks.length; + const results = []; + const step = (index, data) => { + toRun -= 1; + results[index] = data; + + if (!toRun) { + callback(null, results); + } + }; + + tasks.forEach((task, index) => { + const enqueue = () => { + this.boundWorkers(task, (error, data) => { + const result = error ? { error } : data; + const done = () => step(index, result); + + if (this.cacheDir && !result.error) { + cacache + .put( + this.cacheDir, + serialize(task.cacheKeys), + JSON.stringify(data) + ) + .then(done, done); + } else { + done(); + } + }); + }; + + if (this.cacheDir) { + cacache + .get(this.cacheDir, serialize(task.cacheKeys)) + .then(({ data }) => step(index, JSON.parse(data)), enqueue); + } else { + enqueue(); + } + }); + } + + exit() { + if (this.workers) { + workerFarm.end(this.workers); + } + } +} diff --git a/build/webpack/plugins/terser-webpack-plugin/src/cjs.js b/build/webpack/plugins/terser-webpack-plugin/src/cjs.js new file mode 100644 index 00000000..baa7dacb --- /dev/null +++ b/build/webpack/plugins/terser-webpack-plugin/src/cjs.js @@ -0,0 +1,3 @@ +const plugin = require('./index'); + +module.exports = plugin.default; diff --git a/build/webpack/plugins/terser-webpack-plugin/src/index.js b/build/webpack/plugins/terser-webpack-plugin/src/index.js new file mode 100644 index 00000000..5fd42bbd --- /dev/null +++ b/build/webpack/plugins/terser-webpack-plugin/src/index.js @@ -0,0 +1,423 @@ +/* eslint-disable + no-param-reassign +*/ +import crypto from 'crypto'; +import path from 'path'; + +import { SourceMapConsumer } from 'source-map'; +import { SourceMapSource, RawSource, ConcatSource } from 'webpack-sources'; +import RequestShortener from 'webpack/lib/RequestShortener'; +import ModuleFilenameHelpers from 'webpack/lib/ModuleFilenameHelpers'; +// import validateOptions from 'schema-utils'; +import serialize from 'serialize-javascript'; +import terserPackageJson from 'terser/package.json'; + +// import schema from './options.json'; +import TaskRunner from './TaskRunner'; + +const warningRegex = /\[.+:([0-9]+),([0-9]+)\]/; + +class TerserPlugin { + constructor(options = {}) { + // validateOptions(schema, options, 'Terser Plugin'); + + const { + minify, + terserOptions = {}, + test = /\.m?js(\?.*)?$/i, + chunkFilter = () => true, + warningsFilter = () => true, + extractComments = false, + sourceMap = false, + cache = false, + cacheKeys = (defaultCacheKeys) => defaultCacheKeys, + parallel = false, + include, + exclude, + } = options; + + this.options = { + test, + chunkFilter, + warningsFilter, + extractComments, + sourceMap, + cache, + cacheKeys, + parallel, + include, + exclude, + minify, + terserOptions: { + output: { + comments: extractComments + ? false + : /^\**!|@preserve|@license|@cc_on/i, + }, + ...terserOptions, + }, + }; + } + + static isSourceMap(input) { + // All required options for `new SourceMapConsumer(...options)` + // https://github.com/mozilla/source-map#new-sourcemapconsumerrawsourcemap + return Boolean( + input && + input.version && + input.sources && + Array.isArray(input.sources) && + typeof input.mappings === 'string' + ); + } + + static buildSourceMap(inputSourceMap) { + if (!inputSourceMap || !TerserPlugin.isSourceMap(inputSourceMap)) { + return null; + } + + return new SourceMapConsumer(inputSourceMap); + } + + static buildError(err, file, sourceMap, requestShortener) { + // Handling error which should have line, col, filename and message + if (err.line) { + const original = + sourceMap && + sourceMap.originalPositionFor({ + line: err.line, + column: err.col, + }); + + if (original && original.source && requestShortener) { + return new Error( + `${file} from Terser\n${err.message} [${requestShortener.shorten( + original.source + )}:${original.line},${original.column}][${file}:${err.line},${ + err.col + }]` + ); + } + + return new Error( + `${file} from Terser\n${err.message} [${file}:${err.line},${err.col}]` + ); + } else if (err.stack) { + return new Error(`${file} from Terser\n${err.stack}`); + } + + return new Error(`${file} from Terser\n${err.message}`); + } + + static buildWarning( + warning, + file, + sourceMap, + requestShortener, + warningsFilter + ) { + let warningMessage = warning; + let locationMessage = ''; + let source = null; + + if (sourceMap) { + const match = warningRegex.exec(warning); + + if (match) { + const line = +match[1]; + const column = +match[2]; + const original = sourceMap.originalPositionFor({ + line, + column, + }); + + if ( + original && + original.source && + original.source !== file && + requestShortener + ) { + ({ source } = original); + warningMessage = `${warningMessage.replace(warningRegex, '')}`; + + locationMessage = `[${requestShortener.shorten(original.source)}:${ + original.line + },${original.column}]`; + } + } + } + + if (warningsFilter && !warningsFilter(warning, source)) { + return null; + } + + return `Terser Plugin: ${warningMessage}${locationMessage}`; + } + + apply(compiler) { + const buildModuleFn = (moduleArg) => { + // to get detailed location info about errors + moduleArg.useSourceMap = true; + }; + + const optimizeFn = (compilation, chunks, callback) => { + const taskRunner = new TaskRunner({ + cache: this.options.cache, + parallel: this.options.parallel, + }); + + const processedAssets = new WeakSet(); + const tasks = []; + + const { chunkFilter } = this.options; + + Array.from(chunks) + .filter((chunk) => chunkFilter && chunkFilter(chunk)) + .reduce((acc, chunk) => acc.concat(chunk.files || []), []) + .concat(compilation.additionalChunkAssets || []) + .filter(ModuleFilenameHelpers.matchObject.bind(null, this.options)) + .forEach((file) => { + let inputSourceMap; + + const asset = compilation.assets[file]; + + if (processedAssets.has(asset)) { + return; + } + + try { + let input; + + if (this.options.sourceMap && asset.sourceAndMap) { + const { source, map } = asset.sourceAndMap(); + + input = source; + + if (TerserPlugin.isSourceMap(map)) { + inputSourceMap = map; + } else { + inputSourceMap = map; + + compilation.warnings.push( + new Error(`${file} contains invalid source map`) + ); + } + } else { + input = asset.source(); + inputSourceMap = null; + } + + // Handling comment extraction + let commentsFile = false; + + if (this.options.extractComments) { + commentsFile = + this.options.extractComments.filename || `${file}.LICENSE`; + + if (typeof commentsFile === 'function') { + commentsFile = commentsFile(file); + } + } + + const task = { + file, + input, + inputSourceMap, + commentsFile, + extractComments: this.options.extractComments, + terserOptions: this.options.terserOptions, + minify: this.options.minify, + }; + + if (this.options.cache) { + const defaultCacheKeys = { + terser: terserPackageJson.version, + // eslint-disable-next-line global-require + 'terser-webpack-plugin': '1.2.2', + 'terser-webpack-plugin-options': this.options, + hash: crypto + .createHash('md4') + .update(input) + .digest('hex'), + }; + + task.cacheKeys = this.options.cacheKeys(defaultCacheKeys, file); + } + + tasks.push(task); + } catch (error) { + compilation.errors.push( + TerserPlugin.buildError( + error, + file, + TerserPlugin.buildSourceMap(inputSourceMap), + new RequestShortener(compiler.context) + ) + ); + } + }); + + taskRunner.run(tasks, (tasksError, results) => { + if (tasksError) { + compilation.errors.push(tasksError); + + return; + } + + results.forEach((data, index) => { + const { file, input, inputSourceMap, commentsFile } = tasks[index]; + const { error, map, code, warnings } = data; + let { extractedComments } = data; + + let sourceMap = null; + + if (error || (warnings && warnings.length > 0)) { + sourceMap = TerserPlugin.buildSourceMap(inputSourceMap); + } + + // Handling results + // Error case: add errors, and go to next file + if (error) { + compilation.errors.push( + TerserPlugin.buildError( + error, + file, + sourceMap, + new RequestShortener(compiler.context) + ) + ); + + return; + } + + let outputSource; + + if (map) { + outputSource = new SourceMapSource( + code, + file, + JSON.parse(map), + input, + inputSourceMap + ); + } else { + outputSource = new RawSource(code); + } + + // Write extracted comments to commentsFile + if ( + commentsFile && + extractedComments && + extractedComments.length > 0 + ) { + if (commentsFile in compilation.assets) { + const commentsFileSource = compilation.assets[ + commentsFile + ].source(); + + extractedComments = extractedComments.filter( + (comment) => !commentsFileSource.includes(comment) + ); + } + + if (extractedComments.length > 0) { + // Add a banner to the original file + if (this.options.extractComments.banner !== false) { + let banner = + this.options.extractComments.banner || + `For license information please see ${path.posix.basename( + commentsFile + )}`; + + if (typeof banner === 'function') { + banner = banner(commentsFile); + } + + if (banner) { + outputSource = new ConcatSource( + `/*! ${banner} */\n`, + outputSource + ); + } + } + + const commentsSource = new RawSource( + `${extractedComments.join('\n\n')}\n` + ); + + if (commentsFile in compilation.assets) { + // commentsFile already exists, append new comments... + if (compilation.assets[commentsFile] instanceof ConcatSource) { + compilation.assets[commentsFile].add('\n'); + compilation.assets[commentsFile].add(commentsSource); + } else { + compilation.assets[commentsFile] = new ConcatSource( + compilation.assets[commentsFile], + '\n', + commentsSource + ); + } + } else { + compilation.assets[commentsFile] = commentsSource; + } + } + } + + // Updating assets + processedAssets.add((compilation.assets[file] = outputSource)); + + // Handling warnings + if (warnings && warnings.length > 0) { + warnings.forEach((warning) => { + const builtWarning = TerserPlugin.buildWarning( + warning, + file, + sourceMap, + new RequestShortener(compiler.context), + this.options.warningsFilter + ); + + if (builtWarning) { + compilation.warnings.push(builtWarning); + } + }); + } + }); + + taskRunner.exit(); + + callback(); + }); + }; + + const plugin = { name: this.constructor.name }; + + compiler.hooks.compilation.tap(plugin, (compilation) => { + if (this.options.sourceMap) { + compilation.hooks.buildModule.tap(plugin, buildModuleFn); + } + + const { mainTemplate, chunkTemplate } = compilation; + + // Regenerate `contenthash` for minified assets + for (const template of [mainTemplate, chunkTemplate]) { + template.hooks.hashForChunk.tap(plugin, (hash) => { + const data = serialize({ + terser: terserPackageJson.version, + terserOptions: this.options.terserOptions, + }); + + hash.update('TerserPlugin'); + hash.update(data); + }); + } + + compilation.hooks.optimizeChunkAssets.tapAsync( + plugin, + optimizeFn.bind(this, compilation) + ); + }); + } +} + +export default TerserPlugin; diff --git a/build/webpack/plugins/terser-webpack-plugin/src/minify.js b/build/webpack/plugins/terser-webpack-plugin/src/minify.js new file mode 100644 index 00000000..077c5d5a --- /dev/null +++ b/build/webpack/plugins/terser-webpack-plugin/src/minify.js @@ -0,0 +1,186 @@ +/* eslint-disable + arrow-body-style +*/ +import { minify as terserMinify } from 'terser'; + +const buildTerserOptions = ({ + ecma, + warnings, + parse = {}, + compress = {}, + mangle, + module, + output, + toplevel, + nameCache, + ie8, + /* eslint-disable camelcase */ + keep_classnames, + keep_fnames, + /* eslint-enable camelcase */ + safari10, +} = {}) => ({ + ecma, + warnings, + parse: { ...parse }, + compress: typeof compress === 'boolean' ? compress : { ...compress }, + // eslint-disable-next-line no-nested-ternary + mangle: + mangle == null + ? true + : typeof mangle === 'boolean' + ? mangle + : { ...mangle }, + output: { + shebang: true, + comments: false, + beautify: false, + semicolons: true, + ...output, + }, + module, + // Ignoring sourceMap from options + sourceMap: null, + toplevel, + nameCache, + ie8, + keep_classnames, + keep_fnames, + safari10, +}); + +const buildComments = (options, terserOptions, extractedComments) => { + const condition = {}; + const commentsOpts = terserOptions.output.comments; + + // Use /^\**!|@preserve|@license|@cc_on/i RegExp + if (typeof options.extractComments === 'boolean') { + condition.preserve = commentsOpts; + condition.extract = /^\**!|@preserve|@license|@cc_on/i; + } else if ( + typeof options.extractComments === 'string' || + options.extractComments instanceof RegExp + ) { + // extractComments specifies the extract condition and commentsOpts specifies the preserve condition + condition.preserve = commentsOpts; + condition.extract = options.extractComments; + } else if (typeof options.extractComments === 'function') { + condition.preserve = commentsOpts; + condition.extract = options.extractComments; + } else if ( + Object.prototype.hasOwnProperty.call(options.extractComments, 'condition') + ) { + // Extract condition is given in extractComments.condition + condition.preserve = commentsOpts; + condition.extract = options.extractComments.condition; + } else { + // No extract condition is given. Extract comments that match commentsOpts instead of preserving them + condition.preserve = false; + condition.extract = commentsOpts; + } + + // Ensure that both conditions are functions + ['preserve', 'extract'].forEach((key) => { + let regexStr; + let regex; + + switch (typeof condition[key]) { + case 'boolean': + condition[key] = condition[key] ? () => true : () => false; + + break; + case 'function': + break; + case 'string': + if (condition[key] === 'all') { + condition[key] = () => true; + + break; + } + + if (condition[key] === 'some') { + condition[key] = (astNode, comment) => { + return ( + comment.type === 'comment2' && + /^\**!|@preserve|@license|@cc_on/i.test(comment.value) + ); + }; + + break; + } + + regexStr = condition[key]; + + condition[key] = (astNode, comment) => { + return new RegExp(regexStr).test(comment.value); + }; + + break; + default: + regex = condition[key]; + + condition[key] = (astNode, comment) => regex.test(comment.value); + } + }); + + // Redefine the comments function to extract and preserve + // comments according to the two conditions + return (astNode, comment) => { + if (condition.extract(astNode, comment)) { + const commentText = + comment.type === 'comment2' + ? `/*${comment.value}*/` + : `//${comment.value}`; + + // Don't include duplicate comments + if (!extractedComments.includes(commentText)) { + extractedComments.push(commentText); + } + } + + return condition.preserve(astNode, comment); + }; +}; + +const minify = (options) => { + const { + file, + input, + inputSourceMap, + extractComments, + minify: minifyFn, + } = options; + + if (minifyFn) { + return minifyFn({ [file]: input }, inputSourceMap); + } + + // Copy terser options + const terserOptions = buildTerserOptions(options.terserOptions); + + // Add source map data + if (inputSourceMap) { + terserOptions.sourceMap = { + content: inputSourceMap, + }; + } + + const extractedComments = []; + + if (extractComments) { + terserOptions.output.comments = buildComments( + options, + terserOptions, + extractedComments + ); + } + + const { error, map, code, warnings } = terserMinify( + { [file]: input }, + terserOptions + ); + + return { error, map, code, warnings, extractedComments }; +}; + +export default minify; diff --git a/build/webpack/plugins/terser-webpack-plugin/src/options.json b/build/webpack/plugins/terser-webpack-plugin/src/options.json new file mode 100644 index 00000000..f937c2d1 --- /dev/null +++ b/build/webpack/plugins/terser-webpack-plugin/src/options.json @@ -0,0 +1,169 @@ +{ + "additionalProperties": false, + "definitions": { + "file-conditions": { + "anyOf": [ + { + "instanceof": "RegExp" + }, + { + "type": "string" + } + ] + } + }, + "properties": { + "test": { + "anyOf": [ + { + "$ref": "#/definitions/file-conditions" + }, + { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/file-conditions" + } + ] + }, + "type": "array" + } + ] + }, + "include": { + "anyOf": [ + { + "$ref": "#/definitions/file-conditions" + }, + { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/file-conditions" + } + ] + }, + "type": "array" + } + ] + }, + "exclude": { + "anyOf": [ + { + "$ref": "#/definitions/file-conditions" + }, + { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/file-conditions" + } + ] + }, + "type": "array" + } + ] + }, + "chunkFilter": { + "instanceof": "Function" + }, + "cache": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, + "cacheKeys": { + "instanceof": "Function" + }, + "parallel": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + } + ] + }, + "sourceMap": { + "type": "boolean" + }, + "minify": { + "instanceof": "Function" + }, + "terserOptions": { + "additionalProperties": true, + "type": "object" + }, + "extractComments": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + }, + { + "instanceof": "RegExp" + }, + { + "instanceof": "Function" + }, + { + "additionalProperties": false, + "properties": { + "condition": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + }, + { + "instanceof": "RegExp" + }, + { + "instanceof": "Function" + } + ] + }, + "filename": { + "anyOf": [ + { + "type": "string" + }, + { + "instanceof": "Function" + } + ] + }, + "banner": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + }, + { + "instanceof": "Function" + } + ] + } + }, + "type": "object" + } + ] + }, + "warningsFilter": { + "instanceof": "Function" + } + }, + "type": "object" +} diff --git a/build/webpack/plugins/terser-webpack-plugin/src/worker.js b/build/webpack/plugins/terser-webpack-plugin/src/worker.js new file mode 100644 index 00000000..1d8b7aa8 --- /dev/null +++ b/build/webpack/plugins/terser-webpack-plugin/src/worker.js @@ -0,0 +1,21 @@ +import minify from './minify'; + +module.exports = (options, callback) => { + try { + // 'use strict' => this === undefined (Clean Scope) + // Safer for possible security issues, albeit not critical at all here + // eslint-disable-next-line no-new-func, no-param-reassign + options = new Function( + 'exports', + 'require', + 'module', + '__filename', + '__dirname', + `'use strict'\nreturn ${options}` + )(exports, require, module, __filename, __dirname); + + callback(null, minify(options)); + } catch (errors) { + callback(errors); + } +}; diff --git a/package.json b/package.json index 4d12a126..db0410d5 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "standard": { "parser": "babel-eslint", "ignore": [ + "build/webpack/plugins/terser-webpack-plugin/**", "**/node_modules/**", "**/examples/with-ioc/**", "**/examples/with-kea/**", @@ -60,6 +61,13 @@ "bin/*": "standard" }, "dependencies": { + "cacache": "^11.0.2", + "find-cache-dir": "2.0.0", + "schema-utils": "1.0.0", + "serialize-javascript": "1.4.0", + "source-map": "0.6.1", + "terser": "3.16.1", + "worker-farm": "1.5.2", "@babel/core": "7.0.0", "@babel/plugin-proposal-class-properties": "7.0.0", "@babel/plugin-proposal-object-rest-spread": "7.0.0", @@ -104,7 +112,6 @@ "source-map": "0.5.7", "strip-ansi": "3.0.1", "styled-jsx": "3.1.0", - "terser-webpack-plugin": "1.0.2", "unfetch": "3.0.0", "url": "0.11.0", "webpack": "4.20.2", From 7c64336e9d44770ab7fcc0b54643e216bd871bfe Mon Sep 17 00:00:00 2001 From: Tim Neutkens <timneutkens@me.com> Date: Sun, 10 Feb 2019 05:36:32 +0100 Subject: [PATCH 10/18] Revert "should not change method to replaceState unless asPath is the same (#6033)" This reverts commit ab46c45146ca8a1677d9ecacc21522fc60a17397. --- lib/router/router.js | 10 ++++------ test/integration/basic/pages/nav/as-path-pushstate.js | 11 +---------- test/integration/basic/test/client-navigation.js | 5 ----- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/lib/router/router.js b/lib/router/router.js index 917202e0..c0e9ff1d 100644 --- a/lib/router/router.js +++ b/lib/router/router.js @@ -164,14 +164,14 @@ export default class Router { return true } + const { pathname: asPathname, query: asQuery } = parse(as, true) const { pathname, query } = parse(url, true) // If asked to change the current URL we should reload the current page // (not location.reload() but reload getInitialProps and other Next.js stuffs) // We also need to set the method = replaceState always // as this should not go into the history (That's how browsers work) - // We should compare the new asPath to the current asPath, not the url - if (!this.urlIsNew(as)) { + if (!this.urlIsNew(asPathname, asQuery)) { method = 'replaceState' } @@ -339,10 +339,8 @@ export default class Router { } } - urlIsNew (asPath) { - const { pathname, query } = parse(asPath, true) - const { pathname: curPathname } = parse(this.asPath, true) - return curPathname !== pathname || !shallowEquals(query, this.query) + urlIsNew (pathname, query) { + return this.pathname !== pathname || !shallowEquals(query, this.query) } isShallowRoutingPossible (route) { diff --git a/test/integration/basic/pages/nav/as-path-pushstate.js b/test/integration/basic/pages/nav/as-path-pushstate.js index a85e0ac2..a5c1482b 100644 --- a/test/integration/basic/pages/nav/as-path-pushstate.js +++ b/test/integration/basic/pages/nav/as-path-pushstate.js @@ -9,16 +9,7 @@ export default withRouter(({router: {asPath, query}}) => { <a id='hello'>hello</a> </Link> </div> - <div> - <Link href='/nav/as-path-pushstate' as='/something/else'> - <a id='else'>else</a> - </Link> - </div> - <div> - <Link href='/nav/as-path-pushstate' as='/nav/as-path-pushstate'> - <a id='hello2'>normal hello</a> - </Link> - </div> + {query.something === 'hello' && <Link href='/nav/as-path-pushstate?something=hello' as='/something/same-query'> <a id='same-query'>same query</a> </Link>} diff --git a/test/integration/basic/test/client-navigation.js b/test/integration/basic/test/client-navigation.js index 143d49fd..fcdbfa6a 100644 --- a/test/integration/basic/test/client-navigation.js +++ b/test/integration/basic/test/client-navigation.js @@ -648,11 +648,6 @@ export default (context, render) => { await browser.back().waitForElementByCss('#something-hello') const queryThree = JSON.parse(await browser.elementByCss('#router-query').text()) expect(queryThree.something).toBe('hello') - await browser.elementByCss('#else').click().waitForElementByCss('#something-else') - await browser.elementByCss('#hello2').click().waitForElementByCss('#nav-as-path-pushstate') - await browser.back().waitForElementByCss('#something-else') - const queryFour = JSON.parse(await browser.elementByCss('#router-query').text()) - expect(queryFour.something).toBe(undefined) } finally { if (browser) { browser.close() From c07e27f2f12381691962e57cd74220316fb62974 Mon Sep 17 00:00:00 2001 From: Tim Neutkens <timneutkens@me.com> Date: Sun, 10 Feb 2019 05:46:26 +0100 Subject: [PATCH 11/18] 7.0.3 --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index db0410d5..edf2f5ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "7.0.2", + "version": "7.0.3", "description": "Minimalistic framework for server-rendered React applications", "main": "./dist/server/next.js", "license": "MIT", @@ -65,7 +65,7 @@ "find-cache-dir": "2.0.0", "schema-utils": "1.0.0", "serialize-javascript": "1.4.0", - "source-map": "0.6.1", + "source-map": "0.5.7", "terser": "3.16.1", "worker-farm": "1.5.2", "@babel/core": "7.0.0", @@ -109,7 +109,6 @@ "recursive-copy": "2.0.6", "resolve": "1.5.0", "send": "0.16.1", - "source-map": "0.5.7", "strip-ansi": "3.0.1", "styled-jsx": "3.1.0", "unfetch": "3.0.0", From a2bb542f395efa687d2e2301f8ee6ca7b761568e Mon Sep 17 00:00:00 2001 From: Tim Neutkens <timneutkens@me.com> Date: Mon, 11 Feb 2019 08:44:28 +0100 Subject: [PATCH 12/18] Allow publishing the master branch using Lerna --- lerna.json | 2 +- package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 7fb3a08e..da762b20 100644 --- a/lerna.json +++ b/lerna.json @@ -10,7 +10,7 @@ }, "publish": { "npmClient": "npm", - "allowBranch": "canary", + "allowBranch": ["master", "canary"], "registry": "https://registry.npmjs.org/" } }, diff --git a/package.json b/package.json index 429563ce..883f21a8 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "typescript": "lerna run typescript", "prepublish": "lerna run prepublish", "publish-canary": "lerna version prerelease --preid canary --force-publish && release --pre", + "publish-stable": "lerna version --force-publish", "lint-staged": "lint-staged" }, "pre-commit": "lint-staged", From b31819fa50806fe44338c247ee338f928bdd6768 Mon Sep 17 00:00:00 2001 From: Tim Neutkens <timneutkens@me.com> Date: Mon, 11 Feb 2019 08:57:47 +0100 Subject: [PATCH 13/18] Remove build directory --- .../plugins/terser-webpack-plugin/LICENSE | 20 - .../terser-webpack-plugin/src/TaskRunner.js | 106 ----- .../plugins/terser-webpack-plugin/src/cjs.js | 3 - .../terser-webpack-plugin/src/index.js | 423 ------------------ .../terser-webpack-plugin/src/minify.js | 186 -------- .../terser-webpack-plugin/src/options.json | 169 ------- .../terser-webpack-plugin/src/worker.js | 21 - 7 files changed, 928 deletions(-) delete mode 100644 build/webpack/plugins/terser-webpack-plugin/LICENSE delete mode 100644 build/webpack/plugins/terser-webpack-plugin/src/TaskRunner.js delete mode 100644 build/webpack/plugins/terser-webpack-plugin/src/cjs.js delete mode 100644 build/webpack/plugins/terser-webpack-plugin/src/index.js delete mode 100644 build/webpack/plugins/terser-webpack-plugin/src/minify.js delete mode 100644 build/webpack/plugins/terser-webpack-plugin/src/options.json delete mode 100644 build/webpack/plugins/terser-webpack-plugin/src/worker.js diff --git a/build/webpack/plugins/terser-webpack-plugin/LICENSE b/build/webpack/plugins/terser-webpack-plugin/LICENSE deleted file mode 100644 index 8c11fc72..00000000 --- a/build/webpack/plugins/terser-webpack-plugin/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright JS Foundation and other contributors - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/build/webpack/plugins/terser-webpack-plugin/src/TaskRunner.js b/build/webpack/plugins/terser-webpack-plugin/src/TaskRunner.js deleted file mode 100644 index 82e6664e..00000000 --- a/build/webpack/plugins/terser-webpack-plugin/src/TaskRunner.js +++ /dev/null @@ -1,106 +0,0 @@ -import os from 'os'; - -import cacache from 'cacache'; -import findCacheDir from 'find-cache-dir'; -import workerFarm from 'worker-farm'; -import serialize from 'serialize-javascript'; - -import minify from './minify'; - -const worker = require.resolve('./worker'); - -export default class TaskRunner { - constructor(options = {}) { - const { cache, parallel } = options; - this.cacheDir = - cache === true ? findCacheDir({ name: 'terser-webpack-plugin' }) : cache; - // In some cases cpus() returns undefined - // https://github.com/nodejs/node/issues/19022 - const cpus = os.cpus() || { length: 1 }; - this.maxConcurrentWorkers = - parallel === true - ? cpus.length - 1 - : Math.min(Number(parallel) || 0, cpus.length - 1); - } - - run(tasks, callback) { - /* istanbul ignore if */ - if (!tasks.length) { - callback(null, []); - return; - } - - if (this.maxConcurrentWorkers > 1) { - const workerOptions = - process.platform === 'win32' - ? { - maxConcurrentWorkers: this.maxConcurrentWorkers, - maxConcurrentCallsPerWorker: 1, - } - : { maxConcurrentWorkers: this.maxConcurrentWorkers }; - this.workers = workerFarm(workerOptions, worker); - this.boundWorkers = (options, cb) => { - try { - this.workers(serialize(options), cb); - } catch (error) { - // worker-farm can fail with ENOMEM or something else - cb(error); - } - }; - } else { - this.boundWorkers = (options, cb) => { - try { - cb(null, minify(options)); - } catch (error) { - cb(error); - } - }; - } - - let toRun = tasks.length; - const results = []; - const step = (index, data) => { - toRun -= 1; - results[index] = data; - - if (!toRun) { - callback(null, results); - } - }; - - tasks.forEach((task, index) => { - const enqueue = () => { - this.boundWorkers(task, (error, data) => { - const result = error ? { error } : data; - const done = () => step(index, result); - - if (this.cacheDir && !result.error) { - cacache - .put( - this.cacheDir, - serialize(task.cacheKeys), - JSON.stringify(data) - ) - .then(done, done); - } else { - done(); - } - }); - }; - - if (this.cacheDir) { - cacache - .get(this.cacheDir, serialize(task.cacheKeys)) - .then(({ data }) => step(index, JSON.parse(data)), enqueue); - } else { - enqueue(); - } - }); - } - - exit() { - if (this.workers) { - workerFarm.end(this.workers); - } - } -} diff --git a/build/webpack/plugins/terser-webpack-plugin/src/cjs.js b/build/webpack/plugins/terser-webpack-plugin/src/cjs.js deleted file mode 100644 index baa7dacb..00000000 --- a/build/webpack/plugins/terser-webpack-plugin/src/cjs.js +++ /dev/null @@ -1,3 +0,0 @@ -const plugin = require('./index'); - -module.exports = plugin.default; diff --git a/build/webpack/plugins/terser-webpack-plugin/src/index.js b/build/webpack/plugins/terser-webpack-plugin/src/index.js deleted file mode 100644 index 5fd42bbd..00000000 --- a/build/webpack/plugins/terser-webpack-plugin/src/index.js +++ /dev/null @@ -1,423 +0,0 @@ -/* eslint-disable - no-param-reassign -*/ -import crypto from 'crypto'; -import path from 'path'; - -import { SourceMapConsumer } from 'source-map'; -import { SourceMapSource, RawSource, ConcatSource } from 'webpack-sources'; -import RequestShortener from 'webpack/lib/RequestShortener'; -import ModuleFilenameHelpers from 'webpack/lib/ModuleFilenameHelpers'; -// import validateOptions from 'schema-utils'; -import serialize from 'serialize-javascript'; -import terserPackageJson from 'terser/package.json'; - -// import schema from './options.json'; -import TaskRunner from './TaskRunner'; - -const warningRegex = /\[.+:([0-9]+),([0-9]+)\]/; - -class TerserPlugin { - constructor(options = {}) { - // validateOptions(schema, options, 'Terser Plugin'); - - const { - minify, - terserOptions = {}, - test = /\.m?js(\?.*)?$/i, - chunkFilter = () => true, - warningsFilter = () => true, - extractComments = false, - sourceMap = false, - cache = false, - cacheKeys = (defaultCacheKeys) => defaultCacheKeys, - parallel = false, - include, - exclude, - } = options; - - this.options = { - test, - chunkFilter, - warningsFilter, - extractComments, - sourceMap, - cache, - cacheKeys, - parallel, - include, - exclude, - minify, - terserOptions: { - output: { - comments: extractComments - ? false - : /^\**!|@preserve|@license|@cc_on/i, - }, - ...terserOptions, - }, - }; - } - - static isSourceMap(input) { - // All required options for `new SourceMapConsumer(...options)` - // https://github.com/mozilla/source-map#new-sourcemapconsumerrawsourcemap - return Boolean( - input && - input.version && - input.sources && - Array.isArray(input.sources) && - typeof input.mappings === 'string' - ); - } - - static buildSourceMap(inputSourceMap) { - if (!inputSourceMap || !TerserPlugin.isSourceMap(inputSourceMap)) { - return null; - } - - return new SourceMapConsumer(inputSourceMap); - } - - static buildError(err, file, sourceMap, requestShortener) { - // Handling error which should have line, col, filename and message - if (err.line) { - const original = - sourceMap && - sourceMap.originalPositionFor({ - line: err.line, - column: err.col, - }); - - if (original && original.source && requestShortener) { - return new Error( - `${file} from Terser\n${err.message} [${requestShortener.shorten( - original.source - )}:${original.line},${original.column}][${file}:${err.line},${ - err.col - }]` - ); - } - - return new Error( - `${file} from Terser\n${err.message} [${file}:${err.line},${err.col}]` - ); - } else if (err.stack) { - return new Error(`${file} from Terser\n${err.stack}`); - } - - return new Error(`${file} from Terser\n${err.message}`); - } - - static buildWarning( - warning, - file, - sourceMap, - requestShortener, - warningsFilter - ) { - let warningMessage = warning; - let locationMessage = ''; - let source = null; - - if (sourceMap) { - const match = warningRegex.exec(warning); - - if (match) { - const line = +match[1]; - const column = +match[2]; - const original = sourceMap.originalPositionFor({ - line, - column, - }); - - if ( - original && - original.source && - original.source !== file && - requestShortener - ) { - ({ source } = original); - warningMessage = `${warningMessage.replace(warningRegex, '')}`; - - locationMessage = `[${requestShortener.shorten(original.source)}:${ - original.line - },${original.column}]`; - } - } - } - - if (warningsFilter && !warningsFilter(warning, source)) { - return null; - } - - return `Terser Plugin: ${warningMessage}${locationMessage}`; - } - - apply(compiler) { - const buildModuleFn = (moduleArg) => { - // to get detailed location info about errors - moduleArg.useSourceMap = true; - }; - - const optimizeFn = (compilation, chunks, callback) => { - const taskRunner = new TaskRunner({ - cache: this.options.cache, - parallel: this.options.parallel, - }); - - const processedAssets = new WeakSet(); - const tasks = []; - - const { chunkFilter } = this.options; - - Array.from(chunks) - .filter((chunk) => chunkFilter && chunkFilter(chunk)) - .reduce((acc, chunk) => acc.concat(chunk.files || []), []) - .concat(compilation.additionalChunkAssets || []) - .filter(ModuleFilenameHelpers.matchObject.bind(null, this.options)) - .forEach((file) => { - let inputSourceMap; - - const asset = compilation.assets[file]; - - if (processedAssets.has(asset)) { - return; - } - - try { - let input; - - if (this.options.sourceMap && asset.sourceAndMap) { - const { source, map } = asset.sourceAndMap(); - - input = source; - - if (TerserPlugin.isSourceMap(map)) { - inputSourceMap = map; - } else { - inputSourceMap = map; - - compilation.warnings.push( - new Error(`${file} contains invalid source map`) - ); - } - } else { - input = asset.source(); - inputSourceMap = null; - } - - // Handling comment extraction - let commentsFile = false; - - if (this.options.extractComments) { - commentsFile = - this.options.extractComments.filename || `${file}.LICENSE`; - - if (typeof commentsFile === 'function') { - commentsFile = commentsFile(file); - } - } - - const task = { - file, - input, - inputSourceMap, - commentsFile, - extractComments: this.options.extractComments, - terserOptions: this.options.terserOptions, - minify: this.options.minify, - }; - - if (this.options.cache) { - const defaultCacheKeys = { - terser: terserPackageJson.version, - // eslint-disable-next-line global-require - 'terser-webpack-plugin': '1.2.2', - 'terser-webpack-plugin-options': this.options, - hash: crypto - .createHash('md4') - .update(input) - .digest('hex'), - }; - - task.cacheKeys = this.options.cacheKeys(defaultCacheKeys, file); - } - - tasks.push(task); - } catch (error) { - compilation.errors.push( - TerserPlugin.buildError( - error, - file, - TerserPlugin.buildSourceMap(inputSourceMap), - new RequestShortener(compiler.context) - ) - ); - } - }); - - taskRunner.run(tasks, (tasksError, results) => { - if (tasksError) { - compilation.errors.push(tasksError); - - return; - } - - results.forEach((data, index) => { - const { file, input, inputSourceMap, commentsFile } = tasks[index]; - const { error, map, code, warnings } = data; - let { extractedComments } = data; - - let sourceMap = null; - - if (error || (warnings && warnings.length > 0)) { - sourceMap = TerserPlugin.buildSourceMap(inputSourceMap); - } - - // Handling results - // Error case: add errors, and go to next file - if (error) { - compilation.errors.push( - TerserPlugin.buildError( - error, - file, - sourceMap, - new RequestShortener(compiler.context) - ) - ); - - return; - } - - let outputSource; - - if (map) { - outputSource = new SourceMapSource( - code, - file, - JSON.parse(map), - input, - inputSourceMap - ); - } else { - outputSource = new RawSource(code); - } - - // Write extracted comments to commentsFile - if ( - commentsFile && - extractedComments && - extractedComments.length > 0 - ) { - if (commentsFile in compilation.assets) { - const commentsFileSource = compilation.assets[ - commentsFile - ].source(); - - extractedComments = extractedComments.filter( - (comment) => !commentsFileSource.includes(comment) - ); - } - - if (extractedComments.length > 0) { - // Add a banner to the original file - if (this.options.extractComments.banner !== false) { - let banner = - this.options.extractComments.banner || - `For license information please see ${path.posix.basename( - commentsFile - )}`; - - if (typeof banner === 'function') { - banner = banner(commentsFile); - } - - if (banner) { - outputSource = new ConcatSource( - `/*! ${banner} */\n`, - outputSource - ); - } - } - - const commentsSource = new RawSource( - `${extractedComments.join('\n\n')}\n` - ); - - if (commentsFile in compilation.assets) { - // commentsFile already exists, append new comments... - if (compilation.assets[commentsFile] instanceof ConcatSource) { - compilation.assets[commentsFile].add('\n'); - compilation.assets[commentsFile].add(commentsSource); - } else { - compilation.assets[commentsFile] = new ConcatSource( - compilation.assets[commentsFile], - '\n', - commentsSource - ); - } - } else { - compilation.assets[commentsFile] = commentsSource; - } - } - } - - // Updating assets - processedAssets.add((compilation.assets[file] = outputSource)); - - // Handling warnings - if (warnings && warnings.length > 0) { - warnings.forEach((warning) => { - const builtWarning = TerserPlugin.buildWarning( - warning, - file, - sourceMap, - new RequestShortener(compiler.context), - this.options.warningsFilter - ); - - if (builtWarning) { - compilation.warnings.push(builtWarning); - } - }); - } - }); - - taskRunner.exit(); - - callback(); - }); - }; - - const plugin = { name: this.constructor.name }; - - compiler.hooks.compilation.tap(plugin, (compilation) => { - if (this.options.sourceMap) { - compilation.hooks.buildModule.tap(plugin, buildModuleFn); - } - - const { mainTemplate, chunkTemplate } = compilation; - - // Regenerate `contenthash` for minified assets - for (const template of [mainTemplate, chunkTemplate]) { - template.hooks.hashForChunk.tap(plugin, (hash) => { - const data = serialize({ - terser: terserPackageJson.version, - terserOptions: this.options.terserOptions, - }); - - hash.update('TerserPlugin'); - hash.update(data); - }); - } - - compilation.hooks.optimizeChunkAssets.tapAsync( - plugin, - optimizeFn.bind(this, compilation) - ); - }); - } -} - -export default TerserPlugin; diff --git a/build/webpack/plugins/terser-webpack-plugin/src/minify.js b/build/webpack/plugins/terser-webpack-plugin/src/minify.js deleted file mode 100644 index 077c5d5a..00000000 --- a/build/webpack/plugins/terser-webpack-plugin/src/minify.js +++ /dev/null @@ -1,186 +0,0 @@ -/* eslint-disable - arrow-body-style -*/ -import { minify as terserMinify } from 'terser'; - -const buildTerserOptions = ({ - ecma, - warnings, - parse = {}, - compress = {}, - mangle, - module, - output, - toplevel, - nameCache, - ie8, - /* eslint-disable camelcase */ - keep_classnames, - keep_fnames, - /* eslint-enable camelcase */ - safari10, -} = {}) => ({ - ecma, - warnings, - parse: { ...parse }, - compress: typeof compress === 'boolean' ? compress : { ...compress }, - // eslint-disable-next-line no-nested-ternary - mangle: - mangle == null - ? true - : typeof mangle === 'boolean' - ? mangle - : { ...mangle }, - output: { - shebang: true, - comments: false, - beautify: false, - semicolons: true, - ...output, - }, - module, - // Ignoring sourceMap from options - sourceMap: null, - toplevel, - nameCache, - ie8, - keep_classnames, - keep_fnames, - safari10, -}); - -const buildComments = (options, terserOptions, extractedComments) => { - const condition = {}; - const commentsOpts = terserOptions.output.comments; - - // Use /^\**!|@preserve|@license|@cc_on/i RegExp - if (typeof options.extractComments === 'boolean') { - condition.preserve = commentsOpts; - condition.extract = /^\**!|@preserve|@license|@cc_on/i; - } else if ( - typeof options.extractComments === 'string' || - options.extractComments instanceof RegExp - ) { - // extractComments specifies the extract condition and commentsOpts specifies the preserve condition - condition.preserve = commentsOpts; - condition.extract = options.extractComments; - } else if (typeof options.extractComments === 'function') { - condition.preserve = commentsOpts; - condition.extract = options.extractComments; - } else if ( - Object.prototype.hasOwnProperty.call(options.extractComments, 'condition') - ) { - // Extract condition is given in extractComments.condition - condition.preserve = commentsOpts; - condition.extract = options.extractComments.condition; - } else { - // No extract condition is given. Extract comments that match commentsOpts instead of preserving them - condition.preserve = false; - condition.extract = commentsOpts; - } - - // Ensure that both conditions are functions - ['preserve', 'extract'].forEach((key) => { - let regexStr; - let regex; - - switch (typeof condition[key]) { - case 'boolean': - condition[key] = condition[key] ? () => true : () => false; - - break; - case 'function': - break; - case 'string': - if (condition[key] === 'all') { - condition[key] = () => true; - - break; - } - - if (condition[key] === 'some') { - condition[key] = (astNode, comment) => { - return ( - comment.type === 'comment2' && - /^\**!|@preserve|@license|@cc_on/i.test(comment.value) - ); - }; - - break; - } - - regexStr = condition[key]; - - condition[key] = (astNode, comment) => { - return new RegExp(regexStr).test(comment.value); - }; - - break; - default: - regex = condition[key]; - - condition[key] = (astNode, comment) => regex.test(comment.value); - } - }); - - // Redefine the comments function to extract and preserve - // comments according to the two conditions - return (astNode, comment) => { - if (condition.extract(astNode, comment)) { - const commentText = - comment.type === 'comment2' - ? `/*${comment.value}*/` - : `//${comment.value}`; - - // Don't include duplicate comments - if (!extractedComments.includes(commentText)) { - extractedComments.push(commentText); - } - } - - return condition.preserve(astNode, comment); - }; -}; - -const minify = (options) => { - const { - file, - input, - inputSourceMap, - extractComments, - minify: minifyFn, - } = options; - - if (minifyFn) { - return minifyFn({ [file]: input }, inputSourceMap); - } - - // Copy terser options - const terserOptions = buildTerserOptions(options.terserOptions); - - // Add source map data - if (inputSourceMap) { - terserOptions.sourceMap = { - content: inputSourceMap, - }; - } - - const extractedComments = []; - - if (extractComments) { - terserOptions.output.comments = buildComments( - options, - terserOptions, - extractedComments - ); - } - - const { error, map, code, warnings } = terserMinify( - { [file]: input }, - terserOptions - ); - - return { error, map, code, warnings, extractedComments }; -}; - -export default minify; diff --git a/build/webpack/plugins/terser-webpack-plugin/src/options.json b/build/webpack/plugins/terser-webpack-plugin/src/options.json deleted file mode 100644 index f937c2d1..00000000 --- a/build/webpack/plugins/terser-webpack-plugin/src/options.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "additionalProperties": false, - "definitions": { - "file-conditions": { - "anyOf": [ - { - "instanceof": "RegExp" - }, - { - "type": "string" - } - ] - } - }, - "properties": { - "test": { - "anyOf": [ - { - "$ref": "#/definitions/file-conditions" - }, - { - "items": { - "anyOf": [ - { - "$ref": "#/definitions/file-conditions" - } - ] - }, - "type": "array" - } - ] - }, - "include": { - "anyOf": [ - { - "$ref": "#/definitions/file-conditions" - }, - { - "items": { - "anyOf": [ - { - "$ref": "#/definitions/file-conditions" - } - ] - }, - "type": "array" - } - ] - }, - "exclude": { - "anyOf": [ - { - "$ref": "#/definitions/file-conditions" - }, - { - "items": { - "anyOf": [ - { - "$ref": "#/definitions/file-conditions" - } - ] - }, - "type": "array" - } - ] - }, - "chunkFilter": { - "instanceof": "Function" - }, - "cache": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string" - } - ] - }, - "cacheKeys": { - "instanceof": "Function" - }, - "parallel": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "integer" - } - ] - }, - "sourceMap": { - "type": "boolean" - }, - "minify": { - "instanceof": "Function" - }, - "terserOptions": { - "additionalProperties": true, - "type": "object" - }, - "extractComments": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string" - }, - { - "instanceof": "RegExp" - }, - { - "instanceof": "Function" - }, - { - "additionalProperties": false, - "properties": { - "condition": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string" - }, - { - "instanceof": "RegExp" - }, - { - "instanceof": "Function" - } - ] - }, - "filename": { - "anyOf": [ - { - "type": "string" - }, - { - "instanceof": "Function" - } - ] - }, - "banner": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string" - }, - { - "instanceof": "Function" - } - ] - } - }, - "type": "object" - } - ] - }, - "warningsFilter": { - "instanceof": "Function" - } - }, - "type": "object" -} diff --git a/build/webpack/plugins/terser-webpack-plugin/src/worker.js b/build/webpack/plugins/terser-webpack-plugin/src/worker.js deleted file mode 100644 index 1d8b7aa8..00000000 --- a/build/webpack/plugins/terser-webpack-plugin/src/worker.js +++ /dev/null @@ -1,21 +0,0 @@ -import minify from './minify'; - -module.exports = (options, callback) => { - try { - // 'use strict' => this === undefined (Clean Scope) - // Safer for possible security issues, albeit not critical at all here - // eslint-disable-next-line no-new-func, no-param-reassign - options = new Function( - 'exports', - 'require', - 'module', - '__filename', - '__dirname', - `'use strict'\nreturn ${options}` - )(exports, require, module, __filename, __dirname); - - callback(null, minify(options)); - } catch (errors) { - callback(errors); - } -}; From e4f96e65d34f48550185d0e17edd65f427bca536 Mon Sep 17 00:00:00 2001 From: Tim Neutkens <timneutkens@me.com> Date: Mon, 11 Feb 2019 09:02:37 +0100 Subject: [PATCH 14/18] v8.0.0 --- lerna.json | 7 +++++-- packages/next-server/package.json | 2 +- packages/next/package.json | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lerna.json b/lerna.json index da762b20..96a1ba2f 100644 --- a/lerna.json +++ b/lerna.json @@ -10,9 +10,12 @@ }, "publish": { "npmClient": "npm", - "allowBranch": ["master", "canary"], + "allowBranch": [ + "master", + "canary" + ], "registry": "https://registry.npmjs.org/" } }, - "version": "8.0.0-canary.23" + "version": "8.0.0" } diff --git a/packages/next-server/package.json b/packages/next-server/package.json index 0e77e557..c01a54d2 100644 --- a/packages/next-server/package.json +++ b/packages/next-server/package.json @@ -1,6 +1,6 @@ { "name": "next-server", - "version": "8.0.0-canary.23", + "version": "8.0.0", "main": "./index.js", "license": "MIT", "files": [ diff --git a/packages/next/package.json b/packages/next/package.json index 6d27fdc6..d9cd2d00 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "8.0.0-canary.23", + "version": "8.0.0", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -72,7 +72,7 @@ "loader-utils": "1.1.0", "mkdirp-then": "1.2.0", "nanoid": "1.2.1", - "next-server": "8.0.0-canary.23", + "next-server": "8.0.0", "prop-types": "15.6.2", "prop-types-exact": "1.2.0", "react-error-overlay": "4.0.0", From 10f41f5d4702056d829f50c3fb7080db3753edfa Mon Sep 17 00:00:00 2001 From: Truong Hoang Dung <checkraiser11@gmail.com> Date: Wed, 13 Feb 2019 13:08:41 +0700 Subject: [PATCH 15/18] Fix Docs (#6270) Add options to customize webpack config section. --- packages/next/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/README.md b/packages/next/README.md index 0960df74..729dd49b 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -1575,7 +1575,7 @@ Example usage of `defaultLoaders.babel`: // This source was taken from the @zeit/next-mdx plugin source: // https://github.com/zeit/next-plugins/blob/master/packages/next-mdx module.exports = { - webpack: (config, {}) => { + webpack: (config, options) => { config.module.rules.push({ test: /\.mdx/, use: [ From 77a5a6f91a16912971bf750a94b179aed072fcd7 Mon Sep 17 00:00:00 2001 From: Tim Neutkens <timneutkens@me.com> Date: Wed, 13 Feb 2019 07:20:54 +0100 Subject: [PATCH 16/18] v8.0.1-canary.0 --- lerna.json | 2 +- packages/next-server/package.json | 2 +- packages/next/package.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lerna.json b/lerna.json index 96a1ba2f..092c6aae 100644 --- a/lerna.json +++ b/lerna.json @@ -17,5 +17,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "8.0.0" + "version": "8.0.1-canary.0" } diff --git a/packages/next-server/package.json b/packages/next-server/package.json index c01a54d2..2aad2e31 100644 --- a/packages/next-server/package.json +++ b/packages/next-server/package.json @@ -1,6 +1,6 @@ { "name": "next-server", - "version": "8.0.0", + "version": "8.0.1-canary.0", "main": "./index.js", "license": "MIT", "files": [ diff --git a/packages/next/package.json b/packages/next/package.json index d9cd2d00..424ef66c 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "8.0.0", + "version": "8.0.1-canary.0", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -72,7 +72,7 @@ "loader-utils": "1.1.0", "mkdirp-then": "1.2.0", "nanoid": "1.2.1", - "next-server": "8.0.0", + "next-server": "8.0.1-canary.0", "prop-types": "15.6.2", "prop-types-exact": "1.2.0", "react-error-overlay": "4.0.0", From 126eb498675e6f1a27d568672c2a9da49eec09f9 Mon Sep 17 00:00:00 2001 From: Gary Meehan <garymeehan@outlook.com> Date: Wed, 13 Feb 2019 15:53:04 +0000 Subject: [PATCH 17/18] Fix README links (#6284) --- README-zh-CN.md | 8 ++++---- packages/next/README.md | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README-zh-CN.md b/README-zh-CN.md index 2e3a7a37..356cd3c2 100644 --- a/README-zh-CN.md +++ b/README-zh-CN.md @@ -765,7 +765,7 @@ class MyLink extends React.Component { const { router } = this.props router.prefetch('/dynamic') } - + render() { const { router } = this.props return ( @@ -773,7 +773,7 @@ class MyLink extends React.Component { <a onClick={() => setTimeout(() => router.push('/dynamic'), 100)}> A route transition will happen after 100ms </a> - </div> + </div> ) } } @@ -1343,7 +1343,7 @@ module.exports = { ```js // Example next.config.js for adding a loader that depends on babel-loader -// This source was taken from the @zeit/next-mdx plugin source: +// This source was taken from the @zeit/next-mdx plugin source: // https://github.com/zeit/next-plugins/blob/master/packages/next-mdx module.exports = { webpack: (config, {}) => { @@ -1464,7 +1464,7 @@ module.exports = { } ``` -注意:Next.js 运行时将会自动添加前缀,但是对于`/static`是没有效果的,如果你想这些静态资源也能使用 CDN,你需要自己添加前缀。有一个方法可以判断你的环境来加前缀,如 [in this example](https://github.com/zeit/next.js/tree/master/examples/with-universal-configuration)。 +注意:Next.js 运行时将会自动添加前缀,但是对于`/static`是没有效果的,如果你想这些静态资源也能使用 CDN,你需要自己添加前缀。有一个方法可以判断你的环境来加前缀,如 [in this example](https://github.com/zeit/next.js/tree/master/examples/with-universal-configuration-build-time)。 <a id="production-deployment" style="display: none"></a> ## 项目部署 diff --git a/packages/next/README.md b/packages/next/README.md index 729dd49b..a1bb7eb6 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -836,7 +836,7 @@ You can add `prefetch` prop to any `<Link>` and Next.js will prefetch those page ```jsx import Link from 'next/link' -function Header() { +function Header() { return ( <nav> <ul> @@ -870,7 +870,7 @@ Most prefetching needs are addressed by `<Link />`, but we also expose an impera ```jsx import { withRouter } from 'next/router' -function MyLink({ router }) { +function MyLink({ router }) { return ( <div> <a onClick={() => setTimeout(() => router.push('/dynamic'), 100)}> @@ -1082,7 +1082,7 @@ function Home() { <p>HOME PAGE is here!</p> </div> ) -} +} export default Home ``` @@ -1684,10 +1684,10 @@ export default Index > :warning: Note that this option is not available when using `target: 'serverless'` -> :warning: Generally you want to use build-time configuration to provide your configuration. +> :warning: Generally you want to use build-time configuration to provide your configuration. The reason for this is that runtime configuration adds a small rendering / initialization overhead. -The `next/config` module gives your app access to the `publicRuntimeConfig` and `serverRuntimeConfig` stored in your `next.config.js`. +The `next/config` module gives your app access to the `publicRuntimeConfig` and `serverRuntimeConfig` stored in your `next.config.js`. Place any server-only runtime config under a `serverRuntimeConfig` property. @@ -1742,7 +1742,7 @@ module.exports = { } ``` -Note: Next.js will automatically use that prefix in the scripts it loads, but this has no effect whatsoever on `/static`. If you want to serve those assets over the CDN, you'll have to introduce the prefix yourself. One way of introducing a prefix that works inside your components and varies by environment is documented [in this example](https://github.com/zeit/next.js/tree/master/examples/with-universal-configuration). +Note: Next.js will automatically use that prefix in the scripts it loads, but this has no effect whatsoever on `/static`. If you want to serve those assets over the CDN, you'll have to introduce the prefix yourself. One way of introducing a prefix that works inside your components and varies by environment is documented [in this example](https://github.com/zeit/next.js/tree/master/examples/with-universal-configuration-build-time). If your CDN is on a separate domain and you would like assets to be requested using a [CORS aware request](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) you can set a config option for that. From 7dbe837ae4fd974899e8051a6db1039f48feef40 Mon Sep 17 00:00:00 2001 From: Jonathan Reed <jontonsoup4@gmail.com> Date: Wed, 13 Feb 2019 12:53:42 -0600 Subject: [PATCH 18/18] fixes hashed statics readme (#6293) # Description * Fixes incorrect assertion of configuration file in the `with-hashed-statics` README as well as adds link to line for updating --- examples/with-hashed-statics/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/with-hashed-statics/README.md b/examples/with-hashed-statics/README.md index 6fc9f9fd..f4d561f3 100644 --- a/examples/with-hashed-statics/README.md +++ b/examples/with-hashed-statics/README.md @@ -41,4 +41,4 @@ now This example shows how to import images, videos, etc. from `/static` and get the URL with a hash query allowing to use better cache without problems. -This example supports `.svg`, `.png` and `.txt` extensions, but it can be configured to support any possible extension changing the `extensions` array in the `.babelrc` file. \ No newline at end of file +This example supports `.svg`, `.png` and `.txt` extensions, but it can be configured to support any possible extension changing the `extensions` array in the `next.config.js` [file](https://github.com/zeit/next.js/blob/canary/examples/with-hashed-statics/next.config.js#L4).