diff --git a/packages/next/navigation-types/compat/navigation.d.ts b/packages/next/navigation-types/compat/navigation.d.ts new file mode 100644 index 0000000000000..56075531f0fd3 --- /dev/null +++ b/packages/next/navigation-types/compat/navigation.d.ts @@ -0,0 +1,22 @@ +declare module 'next/navigation' { + /** + * Get a read-only URLSearchParams object. For example searchParams.get('foo') would return 'bar' when ?foo=bar + * Learn more about URLSearchParams here: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams + * + * If used from `pages/`, the hook may return `null` when the router is not + * ready. + */ + export function useSearchParams(): ReadonlyURLSearchParams | null + + /** + * Get the current pathname. For example, if the URL is + * https://example.com/foo?bar=baz, the pathname would be /foo. + * + * If the hook is accessed from `pages/`, the pathname may be `null` when the + * router is not ready. + */ + export function usePathname(): string | null + + // Re-export the types for next/navigation. + export * from 'next/dist/client/components/navigation' +} diff --git a/packages/next/navigation-types/navigation.d.ts b/packages/next/navigation-types/navigation.d.ts new file mode 100644 index 0000000000000..7ce329d465580 --- /dev/null +++ b/packages/next/navigation-types/navigation.d.ts @@ -0,0 +1,16 @@ +declare module 'next/navigation' { + /** + * Get a read-only URLSearchParams object. For example searchParams.get('foo') would return 'bar' when ?foo=bar + * Learn more about URLSearchParams here: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams + */ + export function useSearchParams(): ReadonlyURLSearchParams + + /** + * Get the current pathname. For example, if the URL is + * https://example.com/foo?bar=baz, the pathname would be /foo. + */ + export function usePathname(): string + + // Re-export the types for next/navigation. + export * from 'next/dist/client/components/navigation' +} diff --git a/packages/next/package.json b/packages/next/package.json index 4071ae932bd2b..b89e706472eda 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -64,7 +64,7 @@ "release": "taskr release", "build": "pnpm release && pnpm types", "prepublishOnly": "cd ../../ && turbo run build", - "types": "tsc --declaration --emitDeclarationOnly --declarationDir dist", + "types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist", "typescript": "tsec --noEmit", "ncc-compiled": "ncc cache clean && taskr ncc", "test-pack": "cd ../../ && pnpm test-pack next" diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 53095020ca5da..66506ab13aecd 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -167,7 +167,8 @@ function verifyTypeScriptSetup( disableStaticImages: boolean, cacheDir: string | undefined, enableWorkerThreads: boolean | undefined, - isAppDirEnabled: boolean + isAppDirEnabled: boolean, + hasPagesDir: boolean ) { const typeCheckWorker = new JestWorker( require.resolve('../lib/verifyTypeScriptSetup'), @@ -193,6 +194,7 @@ function verifyTypeScriptSetup( disableStaticImages, cacheDir, isAppDirEnabled, + hasPagesDir, }) .then((result) => { typeCheckWorker.end() @@ -415,7 +417,8 @@ export default async function build( config.images.disableStaticImages, cacheDir, config.experimental.workerThreads, - isAppDirEnabled + isAppDirEnabled, + !!pagesDir ).then((resolved) => { const checkEnd = process.hrtime(typeCheckStart) return [resolved, checkEnd] as const diff --git a/packages/next/src/client/components/navigation.ts b/packages/next/src/client/components/navigation.ts index 575016c9577d5..0837e991ec52e 100644 --- a/packages/next/src/client/components/navigation.ts +++ b/packages/next/src/client/components/navigation.ts @@ -68,13 +68,21 @@ class ReadonlyURLSearchParams { /** * Get a read-only URLSearchParams object. For example searchParams.get('foo') would return 'bar' when ?foo=bar * Learn more about URLSearchParams here: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams + * + * @internal - re-exported in `next-env.d.ts`. */ -export function useSearchParams() { +export function useSearchParams(): ReadonlyURLSearchParams | null { clientHookInServerComponentError('useSearchParams') const searchParams = useContext(SearchParamsContext) const readonlySearchParams = useMemo(() => { - return new ReadonlyURLSearchParams(searchParams || new URLSearchParams()) + if (!searchParams) { + // When the router is not ready in pages, we won't have the search params + // available. + return null + } + + return new ReadonlyURLSearchParams(searchParams) }, [searchParams]) if (typeof window === 'undefined') { @@ -87,15 +95,13 @@ export function useSearchParams() { } } - if (!searchParams) { - throw new Error('invariant expected search params to be mounted') - } - return readonlySearchParams } /** * Get the current pathname. For example usePathname() on /dashboard?foo=bar would return "/dashboard" + * + * @internal - re-exported in `next-env.d.ts`. */ export function usePathname(): string | null { clientHookInServerComponentError('usePathname') diff --git a/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts b/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts index edd3e861b237e..b5d88f067b5eb 100644 --- a/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts +++ b/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts @@ -2,10 +2,17 @@ import os from 'os' import path from 'path' import { promises as fs } from 'fs' -export async function writeAppTypeDeclarations( - baseDir: string, +export async function writeAppTypeDeclarations({ + baseDir, + imageImportsEnabled, + hasPagesDir, + isAppDirEnabled, +}: { + baseDir: string imageImportsEnabled: boolean -): Promise { + hasPagesDir: boolean + isAppDirEnabled: boolean +}): Promise { // Reference `next` types const appTypeDeclarations = path.join(baseDir, 'next-env.d.ts') @@ -25,19 +32,42 @@ export async function writeAppTypeDeclarations( eol = '\n' } } - } catch (err) {} - - const content = - '/// ' + - eol + - (imageImportsEnabled - ? '/// ' + eol - : '') + - eol + - '// NOTE: This file should not be edited' + - eol + - '// see https://nextjs.org/docs/basic-features/typescript for more information.' + - eol + } catch {} + + /** + * "Triple-slash directives" used to create typings files for Next.js projects + * using Typescript . + * + * @see https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html + */ + const directives: string[] = [ + // Include the core Next.js typings. + '/// ', + ] + + if (imageImportsEnabled) { + directives.push('/// ') + } + + if (isAppDirEnabled) { + if (hasPagesDir) { + directives.push( + '/// ' + ) + } else { + directives.push( + '/// ' + ) + } + } + + // Push the notice in. + directives.push( + '// NOTE: This file should not be edited', + '// see https://nextjs.org/docs/basic-features/typescript for more information.' + ) + + const content = directives.join(eol) + eol // Avoids an un-necessary write on read-only fs if (currentContent === content) { diff --git a/packages/next/src/lib/typescript/writeConfigurationDefaults.ts b/packages/next/src/lib/typescript/writeConfigurationDefaults.ts index 92ff58591dbbf..fc3336e20e72e 100644 --- a/packages/next/src/lib/typescript/writeConfigurationDefaults.ts +++ b/packages/next/src/lib/typescript/writeConfigurationDefaults.ts @@ -226,6 +226,19 @@ export async function writeConfigurationDefaults( ) } } + + // If `strict` is set to `false` or `strictNullChecks` is set to `false`, + // then set `strictNullChecks` to `true`. + if ( + isAppDirEnabled && + !userTsConfig.compilerOptions.strict && + !('strictNullChecks' in userTsConfig.compilerOptions) + ) { + userTsConfig.compilerOptions.strictNullChecks = true + suggestedActions.push( + chalk.cyan('strictNullChecks') + ' was set to ' + chalk.bold(`true`) + ) + } } } diff --git a/packages/next/src/lib/verifyTypeScriptSetup.ts b/packages/next/src/lib/verifyTypeScriptSetup.ts index aefc72cea7028..12461e26ec0a8 100644 --- a/packages/next/src/lib/verifyTypeScriptSetup.ts +++ b/packages/next/src/lib/verifyTypeScriptSetup.ts @@ -46,6 +46,7 @@ export async function verifyTypeScriptSetup({ typeCheckPreflight, disableStaticImages, isAppDirEnabled, + hasPagesDir, }: { dir: string distDir: string @@ -55,6 +56,7 @@ export async function verifyTypeScriptSetup({ typeCheckPreflight: boolean disableStaticImages: boolean isAppDirEnabled: boolean + hasPagesDir: boolean }): Promise<{ result?: TypeCheckResult; version: string | null }> { const resolvedTsConfigPath = path.join(dir, tsconfigPath) @@ -126,7 +128,12 @@ export async function verifyTypeScriptSetup({ ) // Write out the necessary `next-env.d.ts` file to correctly register // Next.js' types: - await writeAppTypeDeclarations(dir, !disableStaticImages) + await writeAppTypeDeclarations({ + baseDir: dir, + imageImportsEnabled: !disableStaticImages, + hasPagesDir, + isAppDirEnabled, + }) if (isAppDirEnabled && !isCI) { await writeVscodeConfigurations(dir, tsPath) diff --git a/packages/next/src/server/dev/next-dev-server.ts b/packages/next/src/server/dev/next-dev-server.ts index ef526697c0a87..1db31504bb3b4 100644 --- a/packages/next/src/server/dev/next-dev-server.ts +++ b/packages/next/src/server/dev/next-dev-server.ts @@ -687,6 +687,7 @@ export default class DevServer extends Server { tsconfigPath: this.nextConfig.typescript.tsconfigPath, disableStaticImages: this.nextConfig.images.disableStaticImages, isAppDirEnabled: !!this.appDir, + hasPagesDir: !!this.pagesDir, }) if (verifyResult.version) { diff --git a/packages/next/tsconfig.json b/packages/next/tsconfig.json index 0cbb6fd2199e9..19a34efa7da69 100644 --- a/packages/next/tsconfig.json +++ b/packages/next/tsconfig.json @@ -16,6 +16,8 @@ "image-types/global.d.ts", "compat/*.d.ts", "legacy/*.d.ts", - "types/compiled.d.ts" + "types/compiled.d.ts", + "navigation-types/*.d.ts", + "navigation-types/compat/*.d.ts" ] }