diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts
index d4e3106196e3c..d3b2d2551b896 100644
--- a/packages/next/build/webpack-config.ts
+++ b/packages/next/build/webpack-config.ts
@@ -217,6 +217,22 @@ export default async function getBaseWebpackConfig(
? config.typescript?.ignoreDevErrors
: config.typescript?.ignoreBuildErrors
+ let jsConfig
+ // jsconfig is a subset of tsconfig
+ if (useTypeScript) {
+ jsConfig = require(tsConfigPath)
+ }
+
+ const jsConfigPath = path.join(dir, 'jsconfig.json')
+ if (!useTypeScript && (await fileExists(jsConfigPath))) {
+ jsConfig = require(jsConfigPath)
+ }
+
+ let resolvedBaseUrl
+ if (jsConfig?.compilerOptions?.baseUrl) {
+ resolvedBaseUrl = path.resolve(dir, jsConfig.compilerOptions.baseUrl)
+ }
+
const resolveConfig = {
// Disable .mjs for node_modules bundling
extensions: isServer
@@ -889,6 +905,11 @@ export default async function getBaseWebpackConfig(
].filter((Boolean as any) as ExcludesFalse),
}
+ // Support tsconfig and jsconfig baseUrl
+ if (resolvedBaseUrl) {
+ webpackConfig.resolve?.modules?.push(resolvedBaseUrl)
+ }
+
webpackConfig = await buildConfiguration(webpackConfig, {
rootDirectory: dir,
customAppFile,
diff --git a/test/integration/jsconfig-baseurl/components/world.js b/test/integration/jsconfig-baseurl/components/world.js
new file mode 100644
index 0000000000000..5a93b9838685c
--- /dev/null
+++ b/test/integration/jsconfig-baseurl/components/world.js
@@ -0,0 +1,5 @@
+import React from 'react'
+
+export function World() {
+ return <>World>
+}
diff --git a/test/integration/jsconfig-baseurl/jsconfig.json b/test/integration/jsconfig-baseurl/jsconfig.json
new file mode 100644
index 0000000000000..36aa1a4dc28f1
--- /dev/null
+++ b/test/integration/jsconfig-baseurl/jsconfig.json
@@ -0,0 +1,5 @@
+{
+ "compilerOptions": {
+ "baseUrl": "."
+ }
+}
diff --git a/test/integration/jsconfig-baseurl/next.config.js b/test/integration/jsconfig-baseurl/next.config.js
new file mode 100644
index 0000000000000..cc17cf48c578f
--- /dev/null
+++ b/test/integration/jsconfig-baseurl/next.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ onDemandEntries: {
+ // Make sure entries are not getting disposed.
+ maxInactiveAge: 1000 * 60 * 60,
+ },
+}
diff --git a/test/integration/jsconfig-baseurl/pages/hello.js b/test/integration/jsconfig-baseurl/pages/hello.js
new file mode 100644
index 0000000000000..430fdc247a864
--- /dev/null
+++ b/test/integration/jsconfig-baseurl/pages/hello.js
@@ -0,0 +1,9 @@
+import React from 'react'
+import { World } from 'components/world'
+export default function HelloPage() {
+ return (
+
+
+
+ )
+}
diff --git a/test/integration/jsconfig-baseurl/test/index.test.js b/test/integration/jsconfig-baseurl/test/index.test.js
new file mode 100644
index 0000000000000..ec516f7766ac8
--- /dev/null
+++ b/test/integration/jsconfig-baseurl/test/index.test.js
@@ -0,0 +1,31 @@
+/* eslint-env jest */
+/* global jasmine */
+import { join } from 'path'
+import cheerio from 'cheerio'
+import { renderViaHTTP, findPort, launchApp, killApp } from 'next-test-utils'
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2
+
+const appDir = join(__dirname, '..')
+let appPort
+let app
+
+async function get$(path, query) {
+ const html = await renderViaHTTP(appPort, path, query)
+ return cheerio.load(html)
+}
+
+describe('TypeScript Features', () => {
+ describe('default behavior', () => {
+ beforeAll(async () => {
+ appPort = await findPort()
+ app = await launchApp(appDir, appPort, {})
+ })
+ afterAll(() => killApp(app))
+
+ it('should render the page', async () => {
+ const $ = await get$('/hello')
+ expect($('body').text()).toMatch(/World/)
+ })
+ })
+})
diff --git a/test/integration/typescript-baseurl/components/world.tsx b/test/integration/typescript-baseurl/components/world.tsx
new file mode 100644
index 0000000000000..d7d1f66258c77
--- /dev/null
+++ b/test/integration/typescript-baseurl/components/world.tsx
@@ -0,0 +1,5 @@
+import React from 'react'
+
+export function World(): JSX.Element {
+ return <>World>
+}
diff --git a/test/integration/typescript-baseurl/next.config.js b/test/integration/typescript-baseurl/next.config.js
new file mode 100644
index 0000000000000..cc17cf48c578f
--- /dev/null
+++ b/test/integration/typescript-baseurl/next.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ onDemandEntries: {
+ // Make sure entries are not getting disposed.
+ maxInactiveAge: 1000 * 60 * 60,
+ },
+}
diff --git a/test/integration/typescript-baseurl/pages/hello.tsx b/test/integration/typescript-baseurl/pages/hello.tsx
new file mode 100644
index 0000000000000..1129fe708833b
--- /dev/null
+++ b/test/integration/typescript-baseurl/pages/hello.tsx
@@ -0,0 +1,9 @@
+import React from 'react'
+import { World } from 'components/world'
+export default function HelloPage(): JSX.Element {
+ return (
+
+
+
+ )
+}
diff --git a/test/integration/typescript-baseurl/test/index.test.js b/test/integration/typescript-baseurl/test/index.test.js
new file mode 100644
index 0000000000000..ec516f7766ac8
--- /dev/null
+++ b/test/integration/typescript-baseurl/test/index.test.js
@@ -0,0 +1,31 @@
+/* eslint-env jest */
+/* global jasmine */
+import { join } from 'path'
+import cheerio from 'cheerio'
+import { renderViaHTTP, findPort, launchApp, killApp } from 'next-test-utils'
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2
+
+const appDir = join(__dirname, '..')
+let appPort
+let app
+
+async function get$(path, query) {
+ const html = await renderViaHTTP(appPort, path, query)
+ return cheerio.load(html)
+}
+
+describe('TypeScript Features', () => {
+ describe('default behavior', () => {
+ beforeAll(async () => {
+ appPort = await findPort()
+ app = await launchApp(appDir, appPort, {})
+ })
+ afterAll(() => killApp(app))
+
+ it('should render the page', async () => {
+ const $ = await get$('/hello')
+ expect($('body').text()).toMatch(/World/)
+ })
+ })
+})
diff --git a/test/integration/typescript-baseurl/tsconfig.json b/test/integration/typescript-baseurl/tsconfig.json
new file mode 100644
index 0000000000000..dce2837cb1bcc
--- /dev/null
+++ b/test/integration/typescript-baseurl/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "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,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true
+ },
+ "exclude": ["node_modules"],
+ "include": ["next-env.d.ts", "components", "pages"]
+}