Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

next/navigation Typescript support for pages/ #45919

Merged
merged 5 commits into from
Feb 15, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/next/navigation-types/compat/navigation.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { ReadonlyURLSearchParams } from 'next/navigation'

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
ijjk marked this conversation as resolved.
Show resolved Hide resolved
ijjk marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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'
}
18 changes: 18 additions & 0 deletions packages/next/navigation-types/navigation.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ReadonlyURLSearchParams } from 'next/navigation'

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
ijjk marked this conversation as resolved.
Show resolved Hide resolved
ijjk marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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'
}
7 changes: 5 additions & 2 deletions packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,14 @@
"types/global.d.ts",
"types/compiled.d.ts",
"image-types/global.d.ts",
"navigation-types/navigation.d.ts",
"navigation-types/compat/navigation.d.ts",
"font",
"navigation.js",
"navigation.d.ts",
"headers.js",
"headers.d.ts"
"headers.d.ts",
"navigation-types"
],
"bin": {
"next": "./dist/bin/next"
Expand All @@ -64,7 +67,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"
Expand Down
7 changes: 5 additions & 2 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -193,6 +194,7 @@ function verifyTypeScriptSetup(
disableStaticImages,
cacheDir,
isAppDirEnabled,
hasPagesDir,
})
.then((result) => {
typeCheckWorker.end()
Expand Down Expand Up @@ -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
Expand Down
20 changes: 13 additions & 7 deletions packages/next/src/client/components/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function readonlyURLSearchParamsError() {
return new Error('ReadonlyURLSearchParams cannot be modified')
}

class ReadonlyURLSearchParams {
export class ReadonlyURLSearchParams {
[INTERNAL_URLSEARCHPARAMS_INSTANCE]: URLSearchParams

entries: URLSearchParams['entries']
Expand Down Expand Up @@ -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') {
Expand All @@ -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')
Expand Down
63 changes: 47 additions & 16 deletions packages/next/src/lib/typescript/writeAppTypeDeclarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
hasPagesDir: boolean
isAppDirEnabled: boolean
}): Promise<void> {
// Reference `next` types
const appTypeDeclarations = path.join(baseDir, 'next-env.d.ts')

Expand All @@ -25,19 +32,43 @@ export async function writeAppTypeDeclarations(
eol = '\n'
}
}
} catch (err) {}

const content =
'/// <reference types="next" />' +
eol +
(imageImportsEnabled
? '/// <reference types="next/image-types/global" />' + 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.
'/// <reference types="next" />',
]

if (imageImportsEnabled) {
directives.push('/// <reference types="next/image-types/global" />')
}

if (isAppDirEnabled) {
if (hasPagesDir) {
directives.push(
'/// <reference types="next/navigation-types/compat/navigation" />'
)
} else {
directives.push(
'/// <reference types="next/navigation-types/navigation" />'
)
}
}

// 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) {
Expand Down
17 changes: 16 additions & 1 deletion packages/next/src/lib/typescript/writeConfigurationDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ export async function writeConfigurationDefaults(
tsConfigPath: string,
isFirstTimeSetup: boolean,
isAppDirEnabled: boolean,
distDir: string
distDir: string,
hasPagesDir: boolean
): Promise<void> {
if (isFirstTimeSetup) {
await fs.writeFile(tsConfigPath, '{}' + os.EOL)
Expand Down Expand Up @@ -226,6 +227,20 @@ export async function writeConfigurationDefaults(
)
}
}

// If `strict` is set to `false` or `strictNullChecks` is set to `false`,
// then set `strictNullChecks` to `true`.
if (
hasPagesDir &&
isAppDirEnabled &&
!userTsConfig.compilerOptions.strict &&
!('strictNullChecks' in userTsConfig.compilerOptions)
) {
userTsConfig.compilerOptions.strictNullChecks = true
suggestedActions.push(
chalk.cyan('strictNullChecks') + ' was set to ' + chalk.bold(`true`)
)
}
}
}

Expand Down
12 changes: 10 additions & 2 deletions packages/next/src/lib/verifyTypeScriptSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export async function verifyTypeScriptSetup({
typeCheckPreflight,
disableStaticImages,
isAppDirEnabled,
hasPagesDir,
}: {
dir: string
distDir: string
Expand All @@ -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)

Expand Down Expand Up @@ -122,11 +124,17 @@ export async function verifyTypeScriptSetup({
resolvedTsConfigPath,
intent.firstTimeSetup,
isAppDirEnabled,
distDir
distDir,
hasPagesDir
)
// 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)
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/dev/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion packages/next/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
51 changes: 45 additions & 6 deletions test/unit/write-app-declarations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const declarationFile = join(fixtureDir, 'next-env.d.ts')
const imageImportsEnabled = false

describe('find config', () => {
beforeEach(async () => {
await fs.ensureDir(fixtureDir)
})
afterEach(() => fs.remove(declarationFile))

it('should preserve CRLF EOL', async () => {
Expand All @@ -25,10 +28,14 @@ describe('find config', () => {
'// see https://nextjs.org/docs/basic-features/typescript for more information.' +
eol

await fs.ensureDir(fixtureDir)
await fs.writeFile(declarationFile, content)

await writeAppTypeDeclarations(fixtureDir, imageImportsEnabled)
await writeAppTypeDeclarations({
baseDir: fixtureDir,
imageImportsEnabled,
hasPagesDir: false,
isAppDirEnabled: false,
})
expect(await fs.readFile(declarationFile, 'utf8')).toBe(content)
})

Expand All @@ -46,10 +53,14 @@ describe('find config', () => {
'// see https://nextjs.org/docs/basic-features/typescript for more information.' +
eol

await fs.ensureDir(fixtureDir)
await fs.writeFile(declarationFile, content)

await writeAppTypeDeclarations(fixtureDir, imageImportsEnabled)
await writeAppTypeDeclarations({
baseDir: fixtureDir,
imageImportsEnabled,
hasPagesDir: false,
isAppDirEnabled: false,
})
expect(await fs.readFile(declarationFile, 'utf8')).toBe(content)
})

Expand All @@ -67,8 +78,36 @@ describe('find config', () => {
'// see https://nextjs.org/docs/basic-features/typescript for more information.' +
eol

await fs.ensureDir(fixtureDir)
await writeAppTypeDeclarations(fixtureDir, imageImportsEnabled)
await writeAppTypeDeclarations({
baseDir: fixtureDir,
imageImportsEnabled,
hasPagesDir: false,
isAppDirEnabled: false,
})
expect(await fs.readFile(declarationFile, 'utf8')).toBe(content)
})

it('should include navigation types if app directory is enabled', async () => {
await writeAppTypeDeclarations({
baseDir: fixtureDir,
imageImportsEnabled,
hasPagesDir: false,
isAppDirEnabled: true,
})

await expect(fs.readFile(declarationFile, 'utf8')).resolves.toContain(
'next/navigation-types/navigation'
)

await writeAppTypeDeclarations({
baseDir: fixtureDir,
imageImportsEnabled,
hasPagesDir: true,
isAppDirEnabled: true,
})

await expect(fs.readFile(declarationFile, 'utf8')).resolves.toContain(
'next/navigation-types/compat/navigation'
)
})
})