diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 6122d41a77bad..875f542af8742 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -90,7 +90,7 @@ import { printTreeView, getCssFilePaths, } from './utils' -import getBaseWebpackConfig from './webpack-config' +import getBaseWebpackConfig, { hasCustomSvgLoader } from './webpack-config' import { PagesManifest } from './webpack/plugins/pages-manifest-plugin' import { writeBuildId } from './write-build-id' import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' @@ -190,65 +190,8 @@ export default async function build( telemetry.record(events) ) - const ignoreTypeScriptErrors = Boolean(config.typescript.ignoreBuildErrors) - const typeCheckStart = process.hrtime() - const typeCheckingSpinner = createSpinner({ - prefixText: `${Log.prefixes.info} ${ - ignoreTypeScriptErrors - ? 'Skipping validation of types' - : 'Checking validity of types' - }`, - }) - - const verifyResult = await nextBuildSpan - .traceChild('verify-typescript-setup') - .traceAsyncFn(() => - verifyTypeScriptSetup( - dir, - pagesDir, - !ignoreTypeScriptErrors, - config, - cacheDir - ) - ) - - const typeCheckEnd = process.hrtime(typeCheckStart) - - if (!ignoreTypeScriptErrors) { - telemetry.record( - eventTypeCheckCompleted({ - durationInSeconds: typeCheckEnd[0], - typescriptVersion: verifyResult.version, - inputFilesCount: verifyResult.result?.inputFilesCount, - totalFilesCount: verifyResult.result?.totalFilesCount, - incremental: verifyResult.result?.incremental, - }) - ) - } - - if (typeCheckingSpinner) { - typeCheckingSpinner.stopAndPersist() - } - - const ignoreESLint = Boolean(config.eslint.ignoreDuringBuilds) - const eslintCacheDir = path.join(cacheDir, 'eslint/') - if (!ignoreESLint && runLint) { - await nextBuildSpan - .traceChild('verify-and-lint') - .traceAsyncFn(async () => { - await verifyAndLint( - dir, - eslintCacheDir, - config.eslint?.dirs, - config.experimental.cpus, - config.experimental.workerThreads, - telemetry - ) - }) - } - - const buildSpinner = createSpinner({ - prefixText: `${Log.prefixes.info} Creating an optimized production build`, + const configCheckSpinner = createSpinner({ + prefixText: `${Log.prefixes.info} Checking build configuration`, }) const isLikeServerless = isTargetLikeServerless(target) @@ -595,7 +538,72 @@ export default async function build( ) } + if (configCheckSpinner) { + configCheckSpinner.stopAndPersist() + } + + const ignoreTypeScriptErrors = Boolean(config.typescript.ignoreBuildErrors) + const typeCheckStart = process.hrtime() + const typeCheckingSpinner = createSpinner({ + prefixText: `${Log.prefixes.info} ${ + ignoreTypeScriptErrors + ? 'Skipping validation of types' + : 'Checking validity of types' + }`, + }) + + const verifyResult = await nextBuildSpan + .traceChild('verify-typescript-setup') + .traceAsyncFn(() => + verifyTypeScriptSetup( + dir, + pagesDir, + !ignoreTypeScriptErrors, + config, + hasCustomSvgLoader(clientConfig), + cacheDir + ) + ) + + const typeCheckEnd = process.hrtime(typeCheckStart) + + if (!ignoreTypeScriptErrors) { + telemetry.record( + eventTypeCheckCompleted({ + durationInSeconds: typeCheckEnd[0], + typescriptVersion: verifyResult.version, + inputFilesCount: verifyResult.result?.inputFilesCount, + totalFilesCount: verifyResult.result?.totalFilesCount, + incremental: verifyResult.result?.incremental, + }) + ) + } + + if (typeCheckingSpinner) { + typeCheckingSpinner.stopAndPersist() + } + + const ignoreESLint = Boolean(config.eslint.ignoreDuringBuilds) + const eslintCacheDir = path.join(cacheDir, 'eslint/') + if (!ignoreESLint && runLint) { + await nextBuildSpan + .traceChild('verify-and-lint') + .traceAsyncFn(async () => { + await verifyAndLint( + dir, + eslintCacheDir, + config.eslint?.dirs, + config.experimental.cpus, + config.experimental.workerThreads, + telemetry + ) + }) + } + const webpackBuildStart = process.hrtime() + const buildSpinner = createSpinner({ + prefixText: `${Log.prefixes.info} Creating an optimized production build`, + }) let result: CompilerResult = { warnings: [], errors: [] } // We run client and server compilation separately to optimize for memory usage diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index e769d254880e9..fc07e93857d10 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -178,6 +178,20 @@ export function attachReactRefresh( } } +export function hasCustomSvgLoader(webpackConfig: webpack.Configuration) { + const rules = webpackConfig.module?.rules || [] + + const hasCustomSvg = rules.some( + (rule) => + rule.loader !== 'next-image-loader' && + 'test' in rule && + rule.test instanceof RegExp && + rule.test.test('.svg') + ) + + return hasCustomSvg +} + export const NODE_RESOLVE_OPTIONS = { dependencyType: 'commonjs', modules: ['node_modules'], @@ -1552,13 +1566,7 @@ export default async function getBaseWebpackConfig( if (!config.images.disableStaticImages) { const rules = webpackConfig.module?.rules || [] - const hasCustomSvg = rules.some( - (rule) => - rule.loader !== 'next-image-loader' && - 'test' in rule && - rule.test instanceof RegExp && - rule.test.test('.svg') - ) + const hasCustomSvg = hasCustomSvgLoader(webpackConfig) const nextImageRule = rules.find( (rule) => rule.loader === 'next-image-loader' ) diff --git a/packages/next/image-types/global-svg.d.ts b/packages/next/image-types/global-svg.d.ts new file mode 100644 index 0000000000000..038a3adade743 --- /dev/null +++ b/packages/next/image-types/global-svg.d.ts @@ -0,0 +1,8 @@ +// this file is conditionally added/removed to next-env.d.ts +// if the static image import handling is enabled and there is no custom SVG loader + +declare module '*.svg' { + const content: StaticImageData + + export default content +} diff --git a/packages/next/image-types/global.d.ts b/packages/next/image-types/global.d.ts index 1a1c9642b8b36..436bc666561ce 100644 --- a/packages/next/image-types/global.d.ts +++ b/packages/next/image-types/global.d.ts @@ -14,17 +14,6 @@ declare module '*.png' { export default content } -declare module '*.svg' { - /** - * Use `any` to avoid conflicts with - * `@svgr/webpack` plugin or - * `babel-plugin-inline-react-svg` plugin. - */ - const content: any - - export default content -} - declare module '*.jpg' { const content: StaticImageData diff --git a/packages/next/lib/typescript/writeAppTypeDeclarations.ts b/packages/next/lib/typescript/writeAppTypeDeclarations.ts index 89f036814966f..8035b1f4eb7d1 100644 --- a/packages/next/lib/typescript/writeAppTypeDeclarations.ts +++ b/packages/next/lib/typescript/writeAppTypeDeclarations.ts @@ -4,7 +4,8 @@ import { promises as fs } from 'fs' export async function writeAppTypeDeclarations( baseDir: string, - imageImportsEnabled: boolean + imageImportsEnabled: boolean, + hasCustomSvgLoader: boolean ): Promise { // Reference `next` types const appTypeDeclarations = path.join(baseDir, 'next-env.d.ts') @@ -35,6 +36,9 @@ export async function writeAppTypeDeclarations( (imageImportsEnabled ? '/// ' + eol : '') + + (imageImportsEnabled && !hasCustomSvgLoader + ? '/// ' + eol + : '') + eol + '// NOTE: This file should not be edited' + eol + diff --git a/packages/next/lib/verifyTypeScriptSetup.ts b/packages/next/lib/verifyTypeScriptSetup.ts index b35a982ef2c3d..f55a5a2cc9b74 100644 --- a/packages/next/lib/verifyTypeScriptSetup.ts +++ b/packages/next/lib/verifyTypeScriptSetup.ts @@ -27,6 +27,7 @@ export async function verifyTypeScriptSetup( pagesDir: string, typeCheckPreflight: boolean, config: NextConfigComplete, + hasCustomSvgLoader: boolean, cacheDir?: string ): Promise<{ result?: TypeCheckResult; version: string | null }> { const tsConfigPath = path.join(dir, config.typescript.tsconfigPath) @@ -63,7 +64,11 @@ export async function verifyTypeScriptSetup( await writeConfigurationDefaults(ts, tsConfigPath, intent.firstTimeSetup) // Write out the necessary `next-env.d.ts` file to correctly register // Next.js' types: - await writeAppTypeDeclarations(dir, !config.images.disableStaticImages) + await writeAppTypeDeclarations( + dir, + !config.images.disableStaticImages, + hasCustomSvgLoader + ) let result if (typeCheckPreflight) { diff --git a/packages/next/package.json b/packages/next/package.json index 94378e3bc2bdd..f0b724c883abc 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -44,7 +44,8 @@ "amp.d.ts", "types/index.d.ts", "types/global.d.ts", - "image-types/global.d.ts" + "image-types/global.d.ts", + "image-types/global-svg.d.ts" ], "bin": { "next": "./dist/bin/next" diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 21b182d6af859..cfda541e9c2cc 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -11,7 +11,9 @@ import { finalizeEntrypoint, } from '../../build/entries' import { watchCompilers } from '../../build/output' -import getBaseWebpackConfig from '../../build/webpack-config' +import getBaseWebpackConfig, { + hasCustomSvgLoader, +} from '../../build/webpack-config' import { API_ROUTE, MIDDLEWARE_ROUTE } from '../../lib/constants' import { recursiveDelete } from '../../lib/recursive-delete' import { BLOCKED_PAGES } from '../../shared/lib/constants' @@ -34,6 +36,7 @@ import { DecodeError } from '../../shared/lib/utils' import { Span, trace } from '../../trace' import isError from '../../lib/is-error' import ws from 'next/dist/compiled/ws' +import { verifyTypeScriptSetup } from '../../lib/verifyTypeScriptSetup' const wsServer = new ws.Server({ noServer: true }) @@ -400,6 +403,14 @@ export default class HotReloader { const configs = await this.getWebpackConfig(startSpan) + await verifyTypeScriptSetup( + this.dir, + this.pagesDir!, + false, + this.config, + hasCustomSvgLoader(configs[0]) + ) + for (const config of configs) { const defaultEntry = config.entry config.entry = async (...args) => { diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 5912772b10a58..32a23a4d04eda 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -14,7 +14,6 @@ import { PUBLIC_DIR_MIDDLEWARE_CONFLICT } from '../../lib/constants' import { fileExists } from '../../lib/file-exists' import { findPagesDir } from '../../lib/find-pages-dir' import loadCustomRoutes, { CustomRoutes } from '../../lib/load-custom-routes' -import { verifyTypeScriptSetup } from '../../lib/verifyTypeScriptSetup' import { PHASE_DEVELOPMENT_SERVER, CLIENT_STATIC_FILES_PATH, @@ -320,12 +319,6 @@ export default class DevServer extends Server { async prepare(): Promise { setGlobal('distDir', this.distDir) setGlobal('phase', PHASE_DEVELOPMENT_SERVER) - await verifyTypeScriptSetup( - this.dir, - this.pagesDir!, - false, - this.nextConfig - ) this.customRoutes = await loadCustomRoutes(this.nextConfig) diff --git a/test/integration/image-component/svgo-webpack/additional.d.ts b/test/integration/image-component/svgo-webpack/additional.d.ts new file mode 100644 index 0000000000000..f2ee9a5baf837 --- /dev/null +++ b/test/integration/image-component/svgo-webpack/additional.d.ts @@ -0,0 +1,5 @@ +declare module '*.svg' { + import React from 'react' + const SVG: React.FC> + export default SVG +} diff --git a/test/unit/write-app-declarations.test.ts b/test/unit/write-app-declarations.test.ts index e9f115421d953..f665c09bdc50d 100644 --- a/test/unit/write-app-declarations.test.ts +++ b/test/unit/write-app-declarations.test.ts @@ -7,6 +7,7 @@ import { writeAppTypeDeclarations } from 'next/dist/lib/typescript/writeAppTypeD const fixtureDir = join(__dirname, 'fixtures/app-declarations') const declarationFile = join(fixtureDir, 'next-env.d.ts') const imageImportsEnabled = false +const hasCustomSvgLoader = false describe('find config', () => { afterEach(() => fs.remove(declarationFile)) @@ -21,6 +22,9 @@ describe('find config', () => { (imageImportsEnabled ? '/// ' + eol : '') + + (imageImportsEnabled && !hasCustomSvgLoader + ? '/// ' + eol + : '') + eol + '// NOTE: This file should not be edited' + eol + @@ -30,7 +34,11 @@ describe('find config', () => { await fs.ensureDir(fixtureDir) await fs.writeFile(declarationFile, content) - await writeAppTypeDeclarations(fixtureDir, imageImportsEnabled) + await writeAppTypeDeclarations( + fixtureDir, + imageImportsEnabled, + hasCustomSvgLoader + ) expect(await fs.readFile(declarationFile, 'utf8')).toBe(content) }) @@ -44,6 +52,9 @@ describe('find config', () => { (imageImportsEnabled ? '/// ' + eol : '') + + (imageImportsEnabled && !hasCustomSvgLoader + ? '/// ' + eol + : '') + eol + '// NOTE: This file should not be edited' + eol + @@ -53,7 +64,11 @@ describe('find config', () => { await fs.ensureDir(fixtureDir) await fs.writeFile(declarationFile, content) - await writeAppTypeDeclarations(fixtureDir, imageImportsEnabled) + await writeAppTypeDeclarations( + fixtureDir, + imageImportsEnabled, + hasCustomSvgLoader + ) expect(await fs.readFile(declarationFile, 'utf8')).toBe(content) }) @@ -67,6 +82,9 @@ describe('find config', () => { (imageImportsEnabled ? '/// ' + eol : '') + + (imageImportsEnabled && !hasCustomSvgLoader + ? '/// ' + eol + : '') + eol + '// NOTE: This file should not be edited' + eol + @@ -74,7 +92,11 @@ describe('find config', () => { eol await fs.ensureDir(fixtureDir) - await writeAppTypeDeclarations(fixtureDir, imageImportsEnabled) + await writeAppTypeDeclarations( + fixtureDir, + imageImportsEnabled, + hasCustomSvgLoader + ) expect(await fs.readFile(declarationFile, 'utf8')).toBe(content) }) })