diff --git a/contributing.md b/contributing.md index bd3bbfc5ca46b..cbb3eddaa9f7e 100644 --- a/contributing.md +++ b/contributing.md @@ -1,19 +1,46 @@ # Contributing to Next.js -Read about our [Commitment to Open Source](https://vercel.com/oss). +Read about our [Commitment to Open Source](https://vercel.com/oss). To +contribute to [our examples](examples), please see **[Adding +examples](#adding-examples)** below. -1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. -2. Create a new branch: `git checkout -b MY_BRANCH_NAME` -3. Install yarn: `npm install -g yarn` -4. Install the dependencies: `yarn` -5. Run `yarn dev` to build and watch for code changes -6. In a new terminal, run `yarn types` to compile declaration files from TypeScript -7. The development branch is `canary` (this is the branch pull requests should be made against). On a release, the relevant parts of the changes in the `canary` branch are rebased into `master`. +## Developing -> You may need to run `yarn types` again if your types get outdated. +The development branch is `canary`, and this is the branch that all pull +requests should be made against. After publishing a stable release, the changes +in the `canary` branch are rebased into `master`. The changes on the `canary` +branch are published to the `@canary` dist-tag daily. -To contribute to [our examples](examples), take a look at the [“Adding examples” -section](#adding-examples). +To develop locally: + +1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your + own GitHub account and then + [clone](https://help.github.com/articles/cloning-a-repository/) it to your + local device. +2. Create a new branch: + ``` + git checkout -b MY_BRANCH_NAME + ``` +3. Install yarn: + ``` + npm install -g yarn + ``` +4. Install the dependencies with: + ``` + yarn + ``` +5. Start developing and watch for code changes: + ``` + yarn dev + ``` +6. In a new terminal, run `yarn types` to compile declaration files from + TypeScript. + + _Note: You may need to repeat this step if your types get outdated._ + +For instructions on how to build a project with your local version of the CLI, +see **[Developing with your local version of Next.js](#developing-with-your-local-version-of-nextjs)** +below. (Naively linking the binary is not sufficient to develop locally.) ## Building @@ -27,31 +54,20 @@ yarn prepublish If you need to clean the project for any reason, use `yarn clean`. -## Adding warning/error descriptions - -In Next.js we have a system to add helpful links to warnings and errors. - -This allows for the logged message to be short while giving a broader description and instructions on how to solve the warning/error. +## Testing -In general all warnings and errors added should have these links attached. - -Below are the steps to add a new link: - -- Create a new markdown file under the `errors` directory based on `errors/template.md`: `cp errors/template.md errors/.md` -- Add the newly added file to `errors/manifest.json` -- Add the following url to your warning/error: `https://nextjs.org/docs/messages/`. For example to link to `errors/api-routes-static-export.md` you use the url: `https://nextjs.org/docs/messages/api-routes-static-export` - -## To run tests - -Make sure you have `chromedriver` installed for your Chrome version. You can install it with +Make sure you have `chromedriver` installed, and it should match your Chrome version. +You can install it with: +- `apt install chromedriver` on Ubuntu/Debian - `brew install --cask chromedriver` on Mac OS X - `chocolatey install chromedriver` on Windows + - Or manually download the version that matches your installed chrome version (if there's no match, download a version under it, but not above) from the [chromedriver repo](https://chromedriver.storage.googleapis.com/index.html) and add the binary to `/node_modules/.bin` You may also have to [install Rust](https://www.rust-lang.org/tools/install) and build our native packages to see all tests pass locally. We check in binaries for the most common targets and those required for CI so that most people don't have to, but if you do not see a binary for your target in `packages/next/native`, you can build it by running `yarn --cwd packages/next build-native`. If you are working on the Rust code and you need to build the binaries for ci, you can manually trigger [the workflow](https://github.com/vercel/next.js/actions/workflows/build_native.yml) to build and commit with the "Run workflow" button. -Running all tests: +### Running tests ```sh yarn testonly @@ -81,7 +97,7 @@ Running one test in the `production` test suite: yarn testonly --testPathPattern "production" -t "should allow etag header support" ``` -## Running the integration apps +### Running the integration apps Running examples can be done with: @@ -106,15 +122,11 @@ EXAMPLE=./test/integration/basic ) ``` -## Running your own app with locally compiled version of Next.js - -1. Move your app inside of the Next.js monorepo. - -2. Run with `yarn next-with-deps ./app-path-in-monorepo` +## Developing with your local version of Next.js -This will use the version of `next` built inside of the Next.js monorepo and the main `yarn dev` monorepo command can be running to make changes to the local Next.js version at the same time (some changes might require re-running `yarn next-with-deps` to take affect). +There are two options to develop with your local version of the codebase: -or +### Set as local dependency in package.json 1. In your app's `package.json`, replace: @@ -125,7 +137,7 @@ or with: ```json - "next": "file:/packages/next", + "next": "file:/path/to/next.js/packages/next", ``` 2. In your app's root directory, make sure to remove `next` from `node_modules` with: @@ -152,6 +164,42 @@ or yarn install --force ``` +or + +### Develop inside the monorepo + +1. Move your app inside of the Next.js monorepo. + +2. Run with `yarn next-with-deps ./app-path-in-monorepo` + +This will use the version of `next` built inside of the Next.js monorepo and the +main `yarn dev` monorepo command can be running to make changes to the local +Next.js version at the same time (some changes might require re-running `yarn next-with-deps` to take affect). + +## Adding warning/error descriptions + +In Next.js we have a system to add helpful links to warnings and errors. + +This allows for the logged message to be short while giving a broader description and instructions on how to solve the warning/error. + +In general all warnings and errors added should have these links attached. + +Below are the steps to add a new link: + +1. Create a new markdown file under the `errors` directory based on + `errors/template.md`: + + ```shell + cp errors/template.md errors/.md + ``` + +2. Add the newly added file to `errors/manifest.json` +3. Add the following url to your warning/error: + `https://nextjs.org/docs/messages/`. + + For example, to link to `errors/api-routes-static-export.md` you use the url: + `https://nextjs.org/docs/messages/api-routes-static-export` + ## Adding examples When you add an example to the [examples](examples) directory, don’t forget to add a `README.md` file with the following format: diff --git a/docs/advanced-features/dynamic-import.md b/docs/advanced-features/dynamic-import.md index 9bd60726971b5..fd92848067e99 100644 --- a/docs/advanced-features/dynamic-import.md +++ b/docs/advanced-features/dynamic-import.md @@ -156,3 +156,25 @@ function Home() { export default Home ``` + +## With suspense + +Option `suspense` allows you to lazy-load a component, similar to `React.lazy` and `` with React 18. Note that it only works on client-side or server-side with `fallback`. Full SSR support in concurrent mode is still a work-in-progress. + +```jsx +import dynamic from 'next/dynamic' + +const DynamicLazyComponent = dynamic(() => import('../components/hello4'), { + suspense: true, +}) + +function Home() { + return ( +
+ + + +
+ ) +} +``` diff --git a/errors/invalid-dynamic-suspense.md b/errors/invalid-dynamic-suspense.md new file mode 100644 index 0000000000000..11e4d6134d732 --- /dev/null +++ b/errors/invalid-dynamic-suspense.md @@ -0,0 +1,13 @@ +# Invalid Usage of `suspense` Option of `next/dynamic` + +#### Why This Error Occurred + +`` is not allowed under legacy render mode when using React older than v18. + +#### Possible Ways to Fix It + +Remove `suspense: true` option in `next/dynamic` usages. + +### Useful Links + +- [Dynamic Import Suspense Usage](https://nextjs.org/docs/advanced-features/dynamic-import#with-suspense) diff --git a/errors/manifest.json b/errors/manifest.json index fc6a3ebb010eb..5645b9cd13e89 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -151,6 +151,10 @@ "title": "invalid-assetprefix", "path": "/errors/invalid-assetprefix.md" }, + { + "title": "invalid-dynamic-suspense", + "path": "/errors/invalid-dynamic-suspense.md" + }, { "title": "invalid-external-rewrite", "path": "/errors/invalid-external-rewrite.md" diff --git a/packages/next/lib/typescript/writeAppTypeDeclarations.ts b/packages/next/lib/typescript/writeAppTypeDeclarations.ts index 3a8942a3f790f..c6b64dc2b2007 100644 --- a/packages/next/lib/typescript/writeAppTypeDeclarations.ts +++ b/packages/next/lib/typescript/writeAppTypeDeclarations.ts @@ -1,6 +1,7 @@ import { promises as fs } from 'fs' import os from 'os' import path from 'path' +import { fileExists } from '../file-exists' export async function writeAppTypeDeclarations( baseDir: string, @@ -9,19 +10,27 @@ export async function writeAppTypeDeclarations( // Reference `next` types const appTypeDeclarations = path.join(baseDir, 'next-env.d.ts') - await fs.writeFile( - appTypeDeclarations, + const content = '/// ' + - os.EOL + - '/// ' + - os.EOL + - (imageImportsEnabled - ? '/// ' + os.EOL - : '') + - os.EOL + - '// NOTE: This file should not be edited' + - os.EOL + - '// see https://nextjs.org/docs/basic-features/typescript for more information.' + - os.EOL - ) + os.EOL + + '/// ' + + os.EOL + + (imageImportsEnabled + ? '/// ' + os.EOL + : '') + + os.EOL + + '// NOTE: This file should not be edited' + + os.EOL + + '// see https://nextjs.org/docs/basic-features/typescript for more information.' + + os.EOL + + // Avoids a write for read-only filesystems + if ( + (await fileExists(appTypeDeclarations)) && + (await fs.readFile(appTypeDeclarations, 'utf8')) === content + ) { + return + } + + await fs.writeFile(appTypeDeclarations, content) } diff --git a/packages/next/shared/lib/dynamic.tsx b/packages/next/shared/lib/dynamic.tsx index 771e4b5aaa375..8c1f5b3df916d 100644 --- a/packages/next/shared/lib/dynamic.tsx +++ b/packages/next/shared/lib/dynamic.tsx @@ -119,7 +119,7 @@ export default function dynamic

( if (!process.env.__NEXT_REACT_ROOT && suspenseOptions.suspense) { // TODO: add error doc when this feature is stable throw new Error( - `Disallowed suspense option usage with next/dynamic in blocking mode` + `Invalid suspense option usage in next/dynamic. Read more: https://nextjs.org/docs/messages/invalid-dynamic-suspense` ) } } diff --git a/test/integration/react-18/test/index.test.js b/test/integration/react-18/test/index.test.js index a63d78b92372c..1b5fef5360a86 100644 --- a/test/integration/react-18/test/index.test.js +++ b/test/integration/react-18/test/index.test.js @@ -87,7 +87,7 @@ describe('React 18 Support', () => { }) expect(code).toBe(1) expect(stderr).toContain( - 'Disallowed suspense option usage with next/dynamic' + 'Invalid suspense option usage in next/dynamic. Read more: https://nextjs.org/docs/messages/invalid-dynamic-suspense' ) }) }) @@ -130,7 +130,7 @@ describe('Basics', () => { const html = await renderViaHTTP(appPort, '/suspense/unwrapped') unwrappedPage.restore() await killApp(app) - // expect(html).toContain('Disallowed suspense option usage with next/dynamic') + expect(html).toContain( 'A React component suspended while rendering, but no fallback UI was specified' ) diff --git a/test/integration/typescript-app-type-declarations/next-env.d.ts b/test/integration/typescript-app-type-declarations/next-env.d.ts new file mode 100644 index 0000000000000..9bc3dd46b9d9b --- /dev/null +++ b/test/integration/typescript-app-type-declarations/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/test/integration/typescript-app-type-declarations/pages/index.tsx b/test/integration/typescript-app-type-declarations/pages/index.tsx new file mode 100644 index 0000000000000..3618c07655033 --- /dev/null +++ b/test/integration/typescript-app-type-declarations/pages/index.tsx @@ -0,0 +1,3 @@ +export default function Index() { + return

+} diff --git a/test/integration/typescript-app-type-declarations/test/index.test.js b/test/integration/typescript-app-type-declarations/test/index.test.js new file mode 100644 index 0000000000000..ad2ea6a74c031 --- /dev/null +++ b/test/integration/typescript-app-type-declarations/test/index.test.js @@ -0,0 +1,61 @@ +/* eslint-env jest */ + +import { join } from 'path' +import { findPort, launchApp, killApp } from 'next-test-utils' +import { promises as fs } from 'fs' + +jest.setTimeout(1000 * 60 * 2) + +const appDir = join(__dirname, '..') +const appTypeDeclarations = join(appDir, 'next-env.d.ts') + +describe('TypeScript App Type Declarations', () => { + it('should write a new next-env.d.ts if none exist', async () => { + const prevContent = await fs.readFile(appTypeDeclarations, 'utf8') + try { + await fs.unlink(appTypeDeclarations) + const appPort = await findPort() + let app + try { + app = await launchApp(appDir, appPort, {}) + const content = await fs.readFile(appTypeDeclarations, 'utf8') + expect(content).toEqual(prevContent) + } finally { + await killApp(app) + } + } finally { + await fs.writeFile(appTypeDeclarations, prevContent) + } + }) + + it('should overwrite next-env.d.ts if an incorrect one exists', async () => { + const prevContent = await fs.readFile(appTypeDeclarations, 'utf8') + try { + await fs.writeFile(appTypeDeclarations, prevContent + 'modification') + const appPort = await findPort() + let app + try { + app = await launchApp(appDir, appPort, {}) + const content = await fs.readFile(appTypeDeclarations, 'utf8') + expect(content).toEqual(prevContent) + } finally { + await killApp(app) + } + } finally { + await fs.writeFile(appTypeDeclarations, prevContent) + } + }) + + it('should not touch an existing correct next-env.d.ts', async () => { + const prevStat = await fs.stat(appTypeDeclarations) + const appPort = await findPort() + let app + try { + app = await launchApp(appDir, appPort, {}) + const stat = await fs.stat(appTypeDeclarations) + expect(stat.mtime).toEqual(prevStat.mtime) + } finally { + await killApp(app) + } + }) +}) diff --git a/test/integration/typescript-app-type-declarations/tsconfig.json b/test/integration/typescript-app-type-declarations/tsconfig.json new file mode 100644 index 0000000000000..11bbfe8efba7a --- /dev/null +++ b/test/integration/typescript-app-type-declarations/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "esModuleInterop": true, + "module": "esnext", + "jsx": "preserve", + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "incremental": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true + }, + "exclude": ["node_modules"], + "include": ["next-env.d.ts", "components", "pages"] +}