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/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" 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 +
+ + ) +} 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)}
+
) } 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}`) diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index dc788f042a366..f77c2519a800c 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -951,7 +951,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() + }) +})