diff --git a/examples/custom-server-express/README.md b/examples/custom-server-express/README.md index c52cad2127c15..2561d25de9530 100644 --- a/examples/custom-server-express/README.md +++ b/examples/custom-server-express/README.md @@ -2,8 +2,12 @@ Most of the time the default Next.js server will be enough but there are times you'll want to run your own server to integrate into an existing application. Next.js provides [a custom server api](https://nextjs.org/docs/advanced-features/custom-server). +The example shows a server that serves the component living in `pages/a.ts` when the route `/b` is requested and `pages/b.ts` when the route `/a` is accessed. This is obviously a non-standard routing strategy. You can see how this custom routing is being made inside `server.ts`. + Because the Next.js server is a Node.js module you can combine it with any other part of the Node.js ecosystem. In this case we are using express. +The example shows how you can use [TypeScript](https://typescriptlang.com) on both the server and the client while using [Nodemon](https://nodemon.io/) to live reload the server code without affecting the Next.js universal code. + ## Deploy your own Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or preview live with [StackBlitz](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/custom-server-express?runScript=dev) diff --git a/examples/custom-server-express/nodemon.json b/examples/custom-server-express/nodemon.json new file mode 100644 index 0000000000000..2ec83dadaad71 --- /dev/null +++ b/examples/custom-server-express/nodemon.json @@ -0,0 +1,5 @@ +{ + "watch": ["server.ts"], + "exec": "ts-node --project tsconfig.server.json server.ts", + "ext": "js ts" +} diff --git a/examples/custom-server-express/package.json b/examples/custom-server-express/package.json index 2df3a736c1d80..9b656b2c8caec 100644 --- a/examples/custom-server-express/package.json +++ b/examples/custom-server-express/package.json @@ -1,15 +1,24 @@ { "private": true, "scripts": { - "dev": "node server.js", - "build": "next build", - "start": "cross-env NODE_ENV=production node server.js" + "dev": "nodemon", + "build": "next build && tsc --project tsconfig.server.json", + "start": "cross-env NODE_ENV=production node dist/server.js" }, "dependencies": { - "cross-env": "^7.0.2", - "express": "^4.17.1", + "cross-env": "^7.0.3", + "express": "^4.18.2", "next": "latest", "react": "^18.2.0", "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/express": "^4.17.14", + "@types/node": "^18.11.7", + "@types/react": "^18.0.24", + "@types/react-dom": "^18.0.8", + "nodemon": "^2.0.20", + "ts-node": "^10.9.1", + "typescript": "^4.8.4" } } diff --git a/examples/custom-server-express/pages/a.js b/examples/custom-server-express/pages/a.tsx similarity index 100% rename from examples/custom-server-express/pages/a.js rename to examples/custom-server-express/pages/a.tsx diff --git a/examples/custom-server-express/pages/b.js b/examples/custom-server-express/pages/b.tsx similarity index 100% rename from examples/custom-server-express/pages/b.js rename to examples/custom-server-express/pages/b.tsx diff --git a/examples/custom-server-express/pages/index.js b/examples/custom-server-express/pages/index.tsx similarity index 100% rename from examples/custom-server-express/pages/index.js rename to examples/custom-server-express/pages/index.tsx diff --git a/examples/custom-server-express/server.js b/examples/custom-server-express/server.ts similarity index 58% rename from examples/custom-server-express/server.js rename to examples/custom-server-express/server.ts index 2e6bc022d993c..dc312ef8a670f 100644 --- a/examples/custom-server-express/server.js +++ b/examples/custom-server-express/server.ts @@ -1,7 +1,8 @@ -const express = require('express') -const next = require('next') +import type { Request, Response } from 'express' +import express from 'express' +import next from 'next' -const port = parseInt(process.env.PORT, 10) || 3000 +const port = parseInt(process.env.PORT || '3000', 10) const dev = process.env.NODE_ENV !== 'production' const app = next({ dev }) const handle = app.getRequestHandler() @@ -9,7 +10,7 @@ const handle = app.getRequestHandler() app.prepare().then(() => { const server = express() - server.all('*', (req, res) => { + server.all('*', (req: Request, res: Response) => { return handle(req, res) }) diff --git a/examples/custom-server-express/tsconfig.json b/examples/custom-server-express/tsconfig.json new file mode 100644 index 0000000000000..99710e857874f --- /dev/null +++ b/examples/custom-server-express/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/custom-server-express/tsconfig.server.json b/examples/custom-server-express/tsconfig.server.json new file mode 100644 index 0000000000000..9902cccc4b4c0 --- /dev/null +++ b/examples/custom-server-express/tsconfig.server.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "dist", + "lib": ["es2019"], + "target": "es2019", + "isolatedModules": false, + "noEmit": false + }, + "include": ["server.ts"] +} diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 51dc98dc76d0c..f3daa62c21abe 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -59,6 +59,10 @@ if (process.env.NEXT_PREBUNDLED_REACT) { overrideBuiltInReactPackages() } +// expose AsyncLocalStorage on global for react usage +const { AsyncLocalStorage } = require('async_hooks') +;(global as any).AsyncLocalStorage = AsyncLocalStorage + export type ROUTER_TYPE = 'pages' | 'app' const RESERVED_PAGE = /^\/(_app|_error|_document|api(\/|$))/ diff --git a/packages/next/client/components/request-async-storage.ts b/packages/next/client/components/request-async-storage.ts index 6b31ab2f371d1..d3d97e8c05ffc 100644 --- a/packages/next/client/components/request-async-storage.ts +++ b/packages/next/client/components/request-async-storage.ts @@ -13,6 +13,8 @@ export interface RequestStore { export let requestAsyncStorage: AsyncLocalStorage | RequestStore = {} as any -if (process.env.NEXT_RUNTIME !== 'edge' && typeof window === 'undefined') { - requestAsyncStorage = new (require('async_hooks').AsyncLocalStorage)() +// @ts-expect-error we provide this on global in +// the edge and node runtime +if (global.AsyncLocalStorage) { + requestAsyncStorage = new (global as any).AsyncLocalStorage() } diff --git a/packages/next/client/components/static-generation-async-storage.ts b/packages/next/client/components/static-generation-async-storage.ts index 36d4034b83b08..39be1e0eaa162 100644 --- a/packages/next/client/components/static-generation-async-storage.ts +++ b/packages/next/client/components/static-generation-async-storage.ts @@ -1,4 +1,19 @@ -export { - staticGenerationAsyncStorage, - StaticGenerationStore, -} from './static-generation-async-storage/storage.js' +import type { AsyncLocalStorage } from 'async_hooks' + +export interface StaticGenerationStore { + inUse?: boolean + pathname?: string + revalidate?: number + fetchRevalidate?: number + isStaticGeneration?: boolean +} + +export let staticGenerationAsyncStorage: + | AsyncLocalStorage + | StaticGenerationStore = {} + +// @ts-expect-error we provide this on global in +// the edge and node runtime +if (global.AsyncLocalStorage) { + staticGenerationAsyncStorage = new (global as any).AsyncLocalStorage() +} diff --git a/packages/next/client/components/static-generation-async-storage/package.json b/packages/next/client/components/static-generation-async-storage/package.json deleted file mode 100644 index e1bbf57be87ee..0000000000000 --- a/packages/next/client/components/static-generation-async-storage/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "browser": { - "./storage.js": "./storage-browser.js" - } -} diff --git a/packages/next/client/components/static-generation-async-storage/storage-browser.ts b/packages/next/client/components/static-generation-async-storage/storage-browser.ts deleted file mode 100644 index 3f9b9fe75d5b6..0000000000000 --- a/packages/next/client/components/static-generation-async-storage/storage-browser.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { AsyncLocalStorage } from 'async_hooks' - -export interface StaticGenerationStore { - inUse?: boolean - pathname?: string - revalidate?: number - fetchRevalidate?: number - isStaticGeneration?: boolean -} - -export let staticGenerationAsyncStorage: - | AsyncLocalStorage - | StaticGenerationStore = {} diff --git a/packages/next/client/components/static-generation-async-storage/storage.ts b/packages/next/client/components/static-generation-async-storage/storage.ts deleted file mode 100644 index f3abd89db0413..0000000000000 --- a/packages/next/client/components/static-generation-async-storage/storage.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { AsyncLocalStorage } from 'async_hooks' - -export interface StaticGenerationStore { - inUse?: boolean - pathname?: string - revalidate?: number - fetchRevalidate?: number - isStaticGeneration?: boolean -} - -export let staticGenerationAsyncStorage: - | AsyncLocalStorage - | StaticGenerationStore = {} - -if (process.env.NEXT_RUNTIME !== 'edge' && typeof window === 'undefined') { - staticGenerationAsyncStorage = - new (require('async_hooks').AsyncLocalStorage)() -} diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index e44dc99e6787e..64e738e5da2e5 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -100,6 +100,10 @@ interface RenderOpts { supportsDynamicHTML?: boolean } +// expose AsyncLocalStorage on global for react usage +const { AsyncLocalStorage } = require('async_hooks') +;(global as any).AsyncLocalStorage = AsyncLocalStorage + export default async function exportPage({ parentSpanId, path, diff --git a/packages/next/server/dev/static-paths-worker.ts b/packages/next/server/dev/static-paths-worker.ts index a6f4b2453b0b8..21368bc9e774a 100644 --- a/packages/next/server/dev/static-paths-worker.ts +++ b/packages/next/server/dev/static-paths-worker.ts @@ -22,6 +22,10 @@ if (process.env.NEXT_PREBUNDLED_REACT) { let workerWasUsed = false +// expose AsyncLocalStorage on global for react usage +const { AsyncLocalStorage } = require('async_hooks') +;(global as any).AsyncLocalStorage = AsyncLocalStorage + // we call getStaticPaths in a separate process to ensure // side-effects aren't relied on in dev that will break // during a production build diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 44974e620d6d6..bc4a2f90eaf76 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -256,11 +256,10 @@ export default class NextNodeServer extends BaseServer { }).catch(() => {}) } - if (this.nextConfig.experimental.appDir) { - // expose AsyncLocalStorage on global for react usage - const { AsyncLocalStorage } = require('async_hooks') - ;(global as any).AsyncLocalStorage = AsyncLocalStorage - } + // expose AsyncLocalStorage on global for react usage + const { AsyncLocalStorage } = require('async_hooks') + ;(global as any).AsyncLocalStorage = AsyncLocalStorage + // ensure options are set when loadConfig isn't called setHttpClientAndAgentOptions(this.nextConfig) } @@ -1775,7 +1774,7 @@ export default class NextNodeServer extends BaseServer { page: page, body: getRequestMeta(params.request, '__NEXT_CLONABLE_BODY'), }, - useCache: false, + useCache: !this.renderOpts.dev, onWarning: params.onWarning, }) @@ -2132,7 +2131,7 @@ export default class NextNodeServer extends BaseServer { }, body: getRequestMeta(params.req, '__NEXT_CLONABLE_BODY'), }, - useCache: false, + useCache: !this.renderOpts.dev, onWarning: params.onWarning, }) diff --git a/packages/next/server/web/sandbox/context.ts b/packages/next/server/web/sandbox/context.ts index da0d334f979c5..6f44c5883cabc 100644 --- a/packages/next/server/web/sandbox/context.ts +++ b/packages/next/server/web/sandbox/context.ts @@ -14,6 +14,7 @@ import { validateURL } from '../utils' import { pick } from '../../../lib/pick' import { fetchInlineAsset } from './fetch-inline-assets' import type { EdgeFunctionDefinition } from '../../../build/webpack/plugins/middleware-plugin' +import { UnwrapPromise } from '../../../lib/coalesced-function' const WEBPACK_HASH_REGEX = /__webpack_require__\.h = function\(\) \{ return "[0-9a-f]+"; \}/g @@ -319,8 +320,15 @@ interface ModuleContextOptions { edgeFunctionEntry: Pick } +const pendingModuleCaches = new Map>() + function getModuleContextShared(options: ModuleContextOptions) { - return createModuleContext(options) + let deferredModuleContext = pendingModuleCaches.get(options.moduleName) + if (!deferredModuleContext) { + deferredModuleContext = createModuleContext(options) + pendingModuleCaches.set(options.moduleName, deferredModuleContext) + } + return deferredModuleContext } /** @@ -335,9 +343,15 @@ export async function getModuleContext(options: ModuleContextOptions): Promise<{ paths: Map warnedEvals: Set }> { - let moduleContext = options.useCache - ? moduleContexts.get(options.moduleName) - : await getModuleContextShared(options) + let moduleContext: + | UnwrapPromise> + | undefined + + if (options.useCache) { + moduleContext = + moduleContexts.get(options.moduleName) || + (await getModuleContextShared(options)) + } if (!moduleContext) { moduleContext = await createModuleContext(options) diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index 6a42e3662120f..77debab26fe63 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -2108,11 +2108,7 @@ export async function compile(task, opts) { ], opts ) - await task.serial([ - 'ncc_react_refresh_utils', - 'ncc_next__react_dev_overlay', - 'copy_package_json', - ]) + await task.serial(['ncc_react_refresh_utils', 'ncc_next__react_dev_overlay']) } export async function bin(task, opts) { @@ -2199,29 +2195,6 @@ export async function nextbuildjest(task, opts) { notify('Compiled build/jest files') } -export async function copy_package_json(task, opts) { - await fs.copy( - join( - __dirname, - 'client/components/static-generation-async-storage/package.json' - ), - join( - __dirname, - 'dist/client/components/static-generation-async-storage/package.json' - ) - ) - await fs.copy( - join( - __dirname, - 'client/components/static-generation-async-storage/package.json' - ), - join( - __dirname, - 'dist/esm/client/components/static-generation-async-storage/package.json' - ) - ) -} - export async function client(task, opts) { await task .source(opts.src || 'client/**/*.+(js|ts|tsx)')