From 134bcf8d4c2da00b7993d82b2d729f7fc2d87bdb Mon Sep 17 00:00:00 2001 From: Eugene C Date: Sun, 6 Sep 2020 02:02:31 +0300 Subject: [PATCH 1/5] [EXAMPLE] with-cssed (#16735) Adding an example with [cssed](https://github.com/okotoki/cssed). A custom styling solution, which extracts CSS from template literals into separate files. --- examples/with-cssed/.babelrc | 8 +++++ examples/with-cssed/.gitignore | 37 +++++++++++++++++++++++ examples/with-cssed/README.md | 23 +++++++++++++++ examples/with-cssed/lib/theme.js | 2 ++ examples/with-cssed/package.json | 18 ++++++++++++ examples/with-cssed/pages/index.js | 47 ++++++++++++++++++++++++++++++ 6 files changed, 135 insertions(+) create mode 100644 examples/with-cssed/.babelrc create mode 100644 examples/with-cssed/.gitignore create mode 100644 examples/with-cssed/README.md create mode 100644 examples/with-cssed/lib/theme.js create mode 100644 examples/with-cssed/package.json create mode 100644 examples/with-cssed/pages/index.js diff --git a/examples/with-cssed/.babelrc b/examples/with-cssed/.babelrc new file mode 100644 index 0000000000000..00628a7afac18 --- /dev/null +++ b/examples/with-cssed/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": [ + "next/babel" + ], + "plugins": [ + "babel-plugin-macros" + ] +} diff --git a/examples/with-cssed/.gitignore b/examples/with-cssed/.gitignore new file mode 100644 index 0000000000000..f18e7c34e1d62 --- /dev/null +++ b/examples/with-cssed/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +# cssed compilation artifacts +.*.module.css diff --git a/examples/with-cssed/README.md b/examples/with-cssed/README.md new file mode 100644 index 0000000000000..787d92ed42991 --- /dev/null +++ b/examples/with-cssed/README.md @@ -0,0 +1,23 @@ +# Example app with cssed + +This example shows how to use [cssed](https://github.com/okotoki/cssed), a CSS-in-JS library, with Next.js. + +We are creating `div` element with local scoped styles. The styles includes the use of pseudo-selector. + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/vercel/next.js/tree/canary/examples/with-cssed) + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: + +```bash +npx create-next-app --example with-cssed with-cssed-app +# or +yarn create next-app --example with-cssed with-cssed-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/examples/with-cssed/lib/theme.js b/examples/with-cssed/lib/theme.js new file mode 100644 index 0000000000000..368b046f1a382 --- /dev/null +++ b/examples/with-cssed/lib/theme.js @@ -0,0 +1,2 @@ +export const dark = '#333' +export const light = '#ddd' diff --git a/examples/with-cssed/package.json b/examples/with-cssed/package.json new file mode 100644 index 0000000000000..9a6236ea3c0ce --- /dev/null +++ b/examples/with-cssed/package.json @@ -0,0 +1,18 @@ +{ + "name": "with-cssed", + "version": "1.0.0", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@types/react": "^16.9.48", + "babel-plugin-macros": "^2.8.0", + "cssed": "^1.1.2", + "next": "latest", + "react": "^16.13.1", + "react-dom": "^16.13.1" + }, + "license": "MIT" +} diff --git a/examples/with-cssed/pages/index.js b/examples/with-cssed/pages/index.js new file mode 100644 index 0000000000000..0dda7c0f2fb8d --- /dev/null +++ b/examples/with-cssed/pages/index.js @@ -0,0 +1,47 @@ +import { css } from 'cssed/macro' +import Head from 'next/head' +import { useState } from 'react' + +import { dark, light } from '../lib/theme' + +const styles = css` + .box { + height: 200px; + width: 200px; + margin: 0 auto; + margin-top: 40px; + display: flex; + align-items: center; + justify-content: center; + } + + .dark { + background-color: ${dark}; + } + .dark::before { + content: '🌚'; + } + .light { + background-color: ${light}; + } + .light::before { + content: '🌞'; + } +` + +export default function Home() { + const [isDark, setDark] = useState(false) + return ( + <> + + With cssed + +
setDark(!isDark)} + className={styles.box + ' ' + (isDark ? styles.dark : styles.light)} + > + Cssed demo +
+ + ) +} From a7a6aa54ef90c586dcc9274b7366c31873a552ad Mon Sep 17 00:00:00 2001 From: Adityo Pratomo Date: Sun, 6 Sep 2020 06:24:09 +0700 Subject: [PATCH 2/5] update nextjs-auth0 versionfor auth0 example (#16871) While running the current [auth0 example](https://github.com/vercel/next.js/tree/canary/examples/auth0), I bumped into this error while trying to access an API route from a page. ``` { "error": "_lib_auth0__WEBPACK_IMPORTED_MODULE_1__.default.tokenCache is not a function" } ``` After checking the [nextjs-auth0](https://github.com/auth0/nextjs-auth0) repo, I realize that they're using version 0.8.0 of the SDK. Changing the package.json to the appropriate version fixes this error. Signed-off-by: Adityo Pratomo --- examples/auth0/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/auth0/package.json b/examples/auth0/package.json index ef9ceed5c9652..16f771f656d6d 100644 --- a/examples/auth0/package.json +++ b/examples/auth0/package.json @@ -8,7 +8,7 @@ "author": "", "license": "MIT", "dependencies": { - "@auth0/nextjs-auth0": "^0.6.0", + "@auth0/nextjs-auth0": "^0.8.0", "next": "latest", "react": "^16.12.0", "react-dom": "^16.12.0" From b5cf3e4c940f2553f0db6a903363588c01c0c680 Mon Sep 17 00:00:00 2001 From: Luis Alvarez D Date: Sat, 5 Sep 2020 18:45:30 -0500 Subject: [PATCH 3/5] Update dynamic-import docs (#16803) Goals of this PR: - Explain `import()` first without mentioning `next/dynamic`, because `next/dynamic` in our API and **Dynamic Import** is a ES feature. This should avoid a common confusion in our users thinking that one can't be used without the other. - Mention how `next/dynamic` can be used with **Dynamic Imports** to load react components. - Updated example to include fuzzy search using a dynamic import. Potential change: Leave the page to be about `import()` and move `next/dynamic` to the API reference (alongside `next/link`, `next/router`, etc.) Closes https://github.com/vercel/next.js/pull/16299 Closes https://github.com/vercel/next.js/issues/15711 --- docs/advanced-features/dynamic-import.md | 36 +++++++++++++++++++-- examples/with-dynamic-import/package.json | 2 +- examples/with-dynamic-import/pages/index.js | 20 ++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/docs/advanced-features/dynamic-import.md b/docs/advanced-features/dynamic-import.md index b6ef8e5122e2d..5551a084daeee 100644 --- a/docs/advanced-features/dynamic-import.md +++ b/docs/advanced-features/dynamic-import.md @@ -4,17 +4,49 @@ description: Dynamically import JavaScript modules and React Components and spli # Dynamic Import -
+
Examples
-Next.js supports ES2020 [dynamic `import()`](https://github.com/tc39/proposal-dynamic-import) for JavaScript. With it you can import JavaScript modules (inc. React Components) dynamically and work with them. They also work with SSR. +Next.js supports ES2020 [dynamic `import()`](https://github.com/tc39/proposal-dynamic-import) for JavaScript. With it you can import JavaScript modules dynamically and work with them. They also work with SSR. + +In the following example, we implement fuzzy search using `fuse.js` and only load the module dynamically in the browser after the user types in the search input: + +```jsx +import { useState } from 'react' + +const names = ['Tim', 'Joe', 'Bel', 'Max', 'Lee'] + +export default function Page() { + const [results, setResults] = useState() + + return ( +
+ { + const { value } = e.currentTarget + // Dynamically load fuse.js + const Fuse = (await import('fuse.js')).default + const fuse = new Fuse(names) + + setResults(fuse.search(value)) + }} + /> +
Results: {JSON.stringify(results, null, 2)}
+
+ ) +} +``` You can think of dynamic imports as another way to split your code into manageable chunks. +React components can also be imported using dynamic imports, but in this case we use it in conjunction with `next/dynamic` to make sure it works just like any other React Component. Check out the sections below for more details on how it works. + ## Basic usage In the following example, the module `../components/hello` will be dynamically loaded by the page: diff --git a/examples/with-dynamic-import/package.json b/examples/with-dynamic-import/package.json index de6c17c190be3..e10d1179ff957 100644 --- a/examples/with-dynamic-import/package.json +++ b/examples/with-dynamic-import/package.json @@ -8,10 +8,10 @@ "start": "next start" }, "dependencies": { + "fuse.js": "6.4.1", "next": "latest", "react": "^16.13.1", "react-dom": "^16.13.1" }, - "author": "", "license": "MIT" } diff --git a/examples/with-dynamic-import/pages/index.js b/examples/with-dynamic-import/pages/index.js index 2461e6d5b0744..ec04c5878a53a 100644 --- a/examples/with-dynamic-import/pages/index.js +++ b/examples/with-dynamic-import/pages/index.js @@ -18,9 +18,12 @@ const DynamicComponent4 = dynamic(() => import('../components/hello4')) const DynamicComponent5 = dynamic(() => import('../components/hello5')) +const names = ['Tim', 'Joe', 'Bel', 'Max', 'Lee'] + const IndexPage = () => { const [showMore, setShowMore] = useState(false) const [falsyField] = useState(false) + const [results, setResults] = useState() return (
@@ -41,6 +44,23 @@ const IndexPage = () => { {/* Load on demand */} {showMore && } + + {/* Load library on demand */} +
+ { + const { value } = e.currentTarget + // Dynamically load fuse.js + const Fuse = (await import('fuse.js')).default + const fuse = new Fuse(names) + + setResults(fuse.search(value)) + }} + /> +
Results: {JSON.stringify(results, null, 2)}
+
) } From e2cdf215bcc6cacb47ce2cc3345bfb1c24ed96ce Mon Sep 17 00:00:00 2001 From: Mohsen Azimi Date: Sat, 5 Sep 2020 20:08:20 -0400 Subject: [PATCH 4/5] Remove path specific switch statement from http2 example (#16558) Is this switch necessary --- examples/with-http2/server.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/examples/with-http2/server.js b/examples/with-http2/server.js index 5ec52af1a7cee..d4c29d6846941 100644 --- a/examples/with-http2/server.js +++ b/examples/with-http2/server.js @@ -17,19 +17,9 @@ const server = http2.createSecureServer({ app.prepare().then(() => { server.on('error', (err) => console.error(err)) - - // Process the various routes based on `req` - // `/` -> Render index.js - // `/about` -> Render about.js server.on('request', (req, res) => { - switch (req.url) { - case '/about': - return app.render(req, res, '/about', req.query) - default: - return app.render(req, res, '/', req.query) - } + app.render(req, res, req.url || '/', req.query) }) - server.listen(port) console.log(`Listening on HTTPS port ${port}`) From 489cad36bcc95f93ce012712369a83809e91956d Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Sat, 5 Sep 2020 21:36:57 -0500 Subject: [PATCH 5/5] Fix href resolving with trailing slash enabled (#16873) This makes sure to strip the trailing slash before attempting to resolve the `href` against pages/dynamic routes and adds tests ensuring the correct pages are resolved with `trailingSlash: true` enabled. Fixes: https://github.com/vercel/next.js/issues/16872 --- .../next/next-server/lib/router/router.ts | 4 +- .../next.config.js | 3 + .../pages/404.js | 3 + .../pages/[slug].js | 11 +++ .../pages/another.js | 7 ++ .../pages/blog/[slug].js | 11 +++ .../pages/blog/another.js | 7 ++ .../pages/catch-all/[...slug].js | 11 +++ .../pages/catch-all/first.js | 7 ++ .../pages/index.js | 32 ++++++ .../test/index.test.js | 98 +++++++++++++++++++ 11 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 test/integration/trailing-slashes-href-resolving/next.config.js create mode 100644 test/integration/trailing-slashes-href-resolving/pages/404.js create mode 100644 test/integration/trailing-slashes-href-resolving/pages/[slug].js create mode 100644 test/integration/trailing-slashes-href-resolving/pages/another.js create mode 100644 test/integration/trailing-slashes-href-resolving/pages/blog/[slug].js create mode 100644 test/integration/trailing-slashes-href-resolving/pages/blog/another.js create mode 100644 test/integration/trailing-slashes-href-resolving/pages/catch-all/[...slug].js create mode 100644 test/integration/trailing-slashes-href-resolving/pages/catch-all/first.js create mode 100644 test/integration/trailing-slashes-href-resolving/pages/index.js create mode 100644 test/integration/trailing-slashes-href-resolving/test/index.test.js diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 2782c5cb317b9..7d6f93d47d27e 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -922,7 +922,9 @@ export default class Router implements BaseRouter { _resolveHref(parsedHref: UrlObject, pages: string[]) { const { pathname } = parsedHref - const cleanPathname = denormalizePagePath(delBasePath(pathname!)) + const cleanPathname = removePathTrailingSlash( + denormalizePagePath(delBasePath(pathname!)) + ) if (cleanPathname === '/404' || cleanPathname === '/_error') { return parsedHref diff --git a/test/integration/trailing-slashes-href-resolving/next.config.js b/test/integration/trailing-slashes-href-resolving/next.config.js new file mode 100644 index 0000000000000..ce3f975d0eac1 --- /dev/null +++ b/test/integration/trailing-slashes-href-resolving/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + trailingSlash: true, +} diff --git a/test/integration/trailing-slashes-href-resolving/pages/404.js b/test/integration/trailing-slashes-href-resolving/pages/404.js new file mode 100644 index 0000000000000..e16cd81e3bee6 --- /dev/null +++ b/test/integration/trailing-slashes-href-resolving/pages/404.js @@ -0,0 +1,3 @@ +export default function NotFound() { + return
404
+} diff --git a/test/integration/trailing-slashes-href-resolving/pages/[slug].js b/test/integration/trailing-slashes-href-resolving/pages/[slug].js new file mode 100644 index 0000000000000..f873b7ab9104b --- /dev/null +++ b/test/integration/trailing-slashes-href-resolving/pages/[slug].js @@ -0,0 +1,11 @@ +import { useRouter } from 'next/router' + +export default function Page() { + const router = useRouter() + + return ( + <> +

top level slug {router.query.slug}

+ + ) +} diff --git a/test/integration/trailing-slashes-href-resolving/pages/another.js b/test/integration/trailing-slashes-href-resolving/pages/another.js new file mode 100644 index 0000000000000..decf00835a7ca --- /dev/null +++ b/test/integration/trailing-slashes-href-resolving/pages/another.js @@ -0,0 +1,7 @@ +export default function Page() { + return ( + <> +

top level another

+ + ) +} diff --git a/test/integration/trailing-slashes-href-resolving/pages/blog/[slug].js b/test/integration/trailing-slashes-href-resolving/pages/blog/[slug].js new file mode 100644 index 0000000000000..f67b5c66bf71b --- /dev/null +++ b/test/integration/trailing-slashes-href-resolving/pages/blog/[slug].js @@ -0,0 +1,11 @@ +import { useRouter } from 'next/router' + +export default function Page() { + const router = useRouter() + + return ( + <> +

blog slug {router.query.slug}

+ + ) +} diff --git a/test/integration/trailing-slashes-href-resolving/pages/blog/another.js b/test/integration/trailing-slashes-href-resolving/pages/blog/another.js new file mode 100644 index 0000000000000..04416504c581f --- /dev/null +++ b/test/integration/trailing-slashes-href-resolving/pages/blog/another.js @@ -0,0 +1,7 @@ +export default function Page() { + return ( + <> +

blog another

+ + ) +} diff --git a/test/integration/trailing-slashes-href-resolving/pages/catch-all/[...slug].js b/test/integration/trailing-slashes-href-resolving/pages/catch-all/[...slug].js new file mode 100644 index 0000000000000..328deb70ac3f4 --- /dev/null +++ b/test/integration/trailing-slashes-href-resolving/pages/catch-all/[...slug].js @@ -0,0 +1,11 @@ +import { useRouter } from 'next/router' + +export default function Page() { + const router = useRouter() + + return ( + <> +

catch-all slug {router.query.slug?.join('/')}

+ + ) +} diff --git a/test/integration/trailing-slashes-href-resolving/pages/catch-all/first.js b/test/integration/trailing-slashes-href-resolving/pages/catch-all/first.js new file mode 100644 index 0000000000000..ef828deb443e1 --- /dev/null +++ b/test/integration/trailing-slashes-href-resolving/pages/catch-all/first.js @@ -0,0 +1,7 @@ +export default function Page() { + return ( + <> +

catch-all first

+ + ) +} diff --git a/test/integration/trailing-slashes-href-resolving/pages/index.js b/test/integration/trailing-slashes-href-resolving/pages/index.js new file mode 100644 index 0000000000000..46e6e099364b3 --- /dev/null +++ b/test/integration/trailing-slashes-href-resolving/pages/index.js @@ -0,0 +1,32 @@ +import Link from 'next/link' + +export default function Index() { + return ( + <> + + to /blog/another/ + +
+ + to /blog/first-post/ + +
+ + to /catch-all/hello/world/ + +
+ + to /catch-all/first/ + +
+ + to /another/ + +
+ + to /top-level-slug/ + +
+ + ) +} diff --git a/test/integration/trailing-slashes-href-resolving/test/index.test.js b/test/integration/trailing-slashes-href-resolving/test/index.test.js new file mode 100644 index 0000000000000..eb4811538b44f --- /dev/null +++ b/test/integration/trailing-slashes-href-resolving/test/index.test.js @@ -0,0 +1,98 @@ +/* eslint-env jest */ + +import { join } from 'path' +import webdriver from 'next-webdriver' +import { + findPort, + killApp, + launchApp, + nextBuild, + nextStart, +} from 'next-test-utils' + +jest.setTimeout(1000 * 60 * 2) + +let app +let appPort +const appDir = join(__dirname, '../') + +const runTests = () => { + it('should route to /blog/another/ correctly', async () => { + const browser = await webdriver(appPort, '/') + await browser.elementByCss('#to-blog-another').click() + + await browser.waitForElementByCss('#another') + expect(await browser.elementByCss('#another').text()).toBe('blog another') + }) + + it('should route to /blog/first-post/ correctly', async () => { + const browser = await webdriver(appPort, '/') + await browser.elementByCss('#to-blog-post').click() + + await browser.waitForElementByCss('#slug') + expect(await browser.elementByCss('#slug').text()).toBe( + 'blog slug first-post' + ) + }) + + it('should route to /catch-all/hello/world/ correctly', async () => { + const browser = await webdriver(appPort, '/') + await browser.elementByCss('#to-catch-all-item').click() + + await browser.waitForElementByCss('#slug') + expect(await browser.elementByCss('#slug').text()).toBe( + 'catch-all slug hello/world' + ) + }) + + it('should route to /catch-all/first/ correctly', async () => { + const browser = await webdriver(appPort, '/') + await browser.elementByCss('#to-catch-all-first').click() + + await browser.waitForElementByCss('#first') + expect(await browser.elementByCss('#first').text()).toBe('catch-all first') + }) + + it('should route to /another/ correctly', async () => { + const browser = await webdriver(appPort, '/') + await browser.elementByCss('#to-another').click() + + await browser.waitForElementByCss('#another') + expect(await browser.elementByCss('#another').text()).toBe( + 'top level another' + ) + }) + + it('should route to /top-level-slug/ correctly', async () => { + const browser = await webdriver(appPort, '/') + await browser.elementByCss('#to-slug').click() + + await browser.waitForElementByCss('#slug') + expect(await browser.elementByCss('#slug').text()).toBe( + 'top level slug top-level-slug' + ) + }) +} + +describe('href resolving trailing-slash', () => { + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests() + }) + + describe('production mode', () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests() + }) +})