Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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.config.ts #5318

Closed
virzak opened this issue Sep 28, 2018 · 70 comments · Fixed by #25240
Closed

next.config.ts #5318

virzak opened this issue Sep 28, 2018 · 70 comments · Fixed by #25240
Labels
Webpack Related to Webpack with Next.js.

Comments

@virzak
Copy link

virzak commented Sep 28, 2018

Feature request

Is it possible to use next.config.ts instead of next.config.js?
Currently none of the typescript examples use typescript in the config file. Is it even possible?

@resir014

@timneutkens
Copy link
Member

If you want to do something like this it's your own responsibility to compile next.config.ts to next.config.js. We're not planning to transpile next.config.js.

@resir014
Copy link
Contributor

resir014 commented Sep 28, 2018

@virzak Yeah, not without much effort. It will require runtime transpilation w/ ts-node, and it adds unnecessary complexity to Next.js.

You can however, use // @ts-check to type check your next.config.js just like you would in Flowtype.

@ManAnRuck
Copy link

any changes here since nextJs Core is running with typescript?

@kachkaev
Copy link
Contributor

kachkaev commented Oct 10, 2019

Any chance this issue could be reconsidered? It was closed over a year ago and a lot has changed since then, TS support is now part of the core.

A specific reason I would like to have next.config.ts is because I want to set publicRuntimeConfig: { appInfo: generateAppInfo() }. The generateAppInfo() function sits in helpers.ts and returns a data structure that is type-checked against the AppInfo interface. The contents include enabled feature switches, git commit hash, sentry id and other things a launched app instance would need.

The same generateAppInfo() function is used in Jest mocks, so moving its implementation out of a ts file right into next.config.js would not be possible. Seems like I now need to extract generateAppInfo() into a separate js file, thus losing type checking against AppInfo interface 🤔

@timneutkens
Copy link
Member

Still the same as #5318 (comment)

Compiling it would slow down bootup and make config loading significantly more complex compared to the benefits.

@kachkaev
Copy link
Contributor

I believe the main performance problem is production-related. What if next.config.ts was compiled into .next directory along with other code, which would remove a need to pass it via babel or ts-node in production? Boot time should remain the same if not slightly better given that there will be scope for build-time optimisations.

@timneutkens
Copy link
Member

timneutkens commented Oct 10, 2019

It's both development and production. Note that this file is a config file and generally shouldn't have anything complex in there.

@jwarkentin
Copy link

It should just build and cache the config like everything else so it doesn't slow bootup performance. Plus, I would be shocked if it was a significant performance impact on bootup compared to the benefits. It's frustrating not being able to use a consistent language throughout my entire project and include the same linting tools or esnext syntax.

Note that this file is a config file and generally shouldn't have anything complex in there.

Umm, maybe for super simple use cases? I don't know. I work on big complex apps that require a lot of customization and tweaking. Data on this would be interesting. They certainly felt it was worthwhile to transpile preact.config.js and I haven't seen any noticeable performance issues with that. Not sure why this would be any different.

One particularly useful tool for modifying configs imperatively like this that loses a lot of benefits without transpiling TS is Ramda. That's my FP toolbelt for everything because it has awesome TS support for dynamic type inference when currying.

@reaktivo
Copy link

reaktivo commented Dec 6, 2019

Is supporting a transpiled next.config.ts still not being considered? Our specific use case is simply that we have multiple functions a services directory that are fully typed. The services consume our APIs and are fully typed. Currently we have to duplicate the service, one typed and one untyped, since we also need them to consume our APIs and generate our exportPathMap.

@timneutkens
Copy link
Member

Generating a list of urls to export will be covered by getStaticPaths in #9524

@lsm
Copy link

lsm commented Jan 19, 2020

You probably already have the following 2 packages installed if you are using typescript with nextjs:
@babel/core
@babel/preset-typescript

Then you can use a function like the one below to require typescript file on the fly:

function requireTypescript(path) {
  const fileContent = require('fs').readFileSync(path, 'utf8')
  const compiled = require('@babel/core').transform(
    fileContent,
    {
      filename: path,
      presets: [ '@babel/preset-typescript' ],
    },
  )
  return eval(compiled.code)
}

Then use it in your next.config.js like this:

const myModule = requireTypescript('./path/to/mymodule.ts')

Note: you will need to inject some path information if your ts file is not in the same folder of next.config.js and you have relative import inside the ts file you are requiring. So, keep the target ts file simple.

@timneutkens
Copy link
Member

Note about #5318 (comment)

  • Slows down bootup when using next start
  • Slows down bootup in development

@Janpot
Copy link
Contributor

Janpot commented Jan 19, 2020

But if you decided to use typescript in your project, didn't you already trade in some performance for type safety? How come the performance impact is not seen as problematic when it happens on every change of a file, but is suddenly so insurmountable when it's once on boot? And If you cache it a bit smart it's not even happening on every boot.

If the concern is that people would blame next.js for slow bootup instead of typescript, maybe a log could be added like

> compiled next.config.ts (1700ms)

That would make clear to people which impact the use of which tool has for them.

@timneutkens
Copy link
Member

@Janpot because it's cached inside Next.js and not with this method. On top of that it affects production boot if you use next start, as it loads next.config.js. Next.js doesn't compile typescript on production boot.

@Janpot
Copy link
Contributor

Janpot commented Jan 19, 2020

Yes, I agree, not with this method. But if next were to hypothetically implement this feature, I presume it wouldn't be too hard to add cache. Since next.js already has infrastructure to compile and cache typescript. And for production boot it could precompile when running next build? All of that would make the performance impact very minimal.

@jerrygreen
Copy link
Contributor

jerrygreen commented Jul 18, 2020

I used typescript ever since it's supported in nextjs and was thinking that it could be cool that next.config.js to be next.config.ts. But aside type checking, I never needed config to be a typescript file, so this issue didn't bother me too much. But now I wanted to import a ts helper script in my next.config.js (to generate paths for exportPathMap), which is used in several places, but turns out I can't use ts here (obviously..?). So now I'm concerning again that next.config.js should be actually next.config.ts, so ts imports supported too, so I came here...

I see the workaround suggested by @lsm here, but this requires some overwhelming customization with babel etc just for this one file??

Either babel, or a separate tsconfig and some script running tsc to emit js, so I could import this helper file as js. Which isn't any better...

But then I see that author doesn't want to add support for "just this one file" too, and I'm starting to see some irony.

🤔🤔🤔🤔🤔🤦‍♂️

P.S. I don't know what I'll do with all this yet, but wanted to at least share my feelings
P.P.S. I've seen getStaticPaths but it's not the same as exportPathMap, I need path mapping

UPD. on a second glance, @babel/core and @babel/preset-typescript are already part of nextjs, so I tried the code suggested by @lsm but I'm getting SyntaxError: Cannot use import statement outside a module

UPD2. ok, I gave up: I changed this ts helper script to js, and required/imported it where I needed (in next.config.js, and in a page, used it in getStaticProps since it uses fs lib)

@dpwolfe
Copy link

dpwolfe commented Sep 10, 2020

@jerrygreen Would moving that useful helper as TS into an npm package that publishes as JS have been an option for you?

@beppek
Copy link

beppek commented Sep 15, 2020

I've got the same issue as @jerrygreen. Was considering making the code an npm package, but there's too much that will be project-specific so I'd need to publish a new package for each project. I'll have to give up on this for now and just duplicate some of this code in js.

However, a better solution to my use case would be to have the option to run scripts during the build cycle, similar to how Gatsby does it in gatsby-node.ts.

@jerrygreen
Copy link
Contributor

@dpwolfe yeah, I agree with @beppek, this thing appears to be a bit too project specific. In my case I have files in pages folder that start with a date (shorten timestamp), something like 20200917-hello-world, and I want it to be accessible via /hello-world path. So I had to write function that goes through file system, applies regular expression, and generates required paths for exportPathMap, creating an object consisting of entries like /blog/posts/20200917-hello-world, -> /hello-world. Seems to be too project specific, so I don't see actual reasons to publish it into a package. Btw I kinda lied about getStaticProps - it turned out I don't need that. Custom exportPathMap is enough for that need. Still, weird that this helper had to be written in js, because if I write it in ts, then I wouldn't be able to import it from next.config.js... I probably should indeed move this helper into a separate package, write it in ts, yet compile it into js, so it will be possible to use it within next.config.js but just for personal need, barely anyone would need that.

@oste
Copy link
Contributor

oste commented Nov 7, 2020

I am running into eslint errors that need to be suppressed like this

/* eslint-disable @typescript-eslint/no-var-requires */
const withFonts = require('next-fonts');

This should be reopened or at least something added to the typescript documentation to mention that this is a known issue. It would add polish to document this upfront or even better allow next.config.ts.

@marceloverdijk
Copy link

Just like having getStaticPaths in the /pages folder it would be nice if Next would have some special folder or something with a function to execute on startup, just outside next.config.js.
This way it would not matter if it js or ts , similar as it doesn't matter for the pages.

I'm having the same use case where I want to run some TypeScript code during build time; e.g. generating a sitemap.json or a search-index.json.

@MrStLouis
Copy link

Is there a way to type the config at least? I looked through the source and couldn't find any interface for the config options

@heshamz
Copy link

heshamz commented Nov 29, 2020

I found a sample at https://github.com/ant-design/antd-mobile-samples/tree/master/web-ssr-ts, which uses next.config.ts.
I hope that could be helpful.

@eric-burel
Copy link
Contributor

@devinrhode2 Is there any official reference for "@types"? Where does it come from? Googled it but I couldn't find the official documentation.

@devinrhode2
Copy link

devinrhode2 commented Dec 29, 2021

Wow, there's actually very good jsdoc support, we can use more than just @type: https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html

@devinrhode2
Copy link

devinrhode2 commented Jan 16, 2022

@floroz

However, the jsdoc solution does not solve the problem when you have to import a utility or function from a .ts file into the next.config.js

You can convert said .ts utility to jsdoc+js, and then const foo = require('foo') inside of next.config.js

As an example, I did this so that only 1 file in our app repo references process.env.*: https://gist.github.com/devinrhode2/e033cf8ec4653cfb70d34389db86cb72

@floroz
Copy link

floroz commented Jan 17, 2022

@floroz

However, the jsdoc solution does not solve the problem when you have to import a utility or function from a .ts file into the next.config.js

You can convert said .ts utility to jsdoc+js, and then const foo = require('foo') inside of next.config.js

As an example, I did this so that only 1 file in our app repo references process.env.*: https://gist.github.com/devinrhode2/e033cf8ec4653cfb70d34389db86cb72

Thank you for the suggestion.

However, converting files from TS back to JS is not really a viable solution for many projects (full migration from JS to TS is a often goal of many codebases).

The workaround I used in the past it's to execute these tasks separately via ts-node if their output is not required by Next.js, or if it is, saving the output to a shared cache on disk that can be read when starting the Next dev/build process.

@tushar-singh
Copy link

I ended up using tsc to compile before each run.
If you're using something like turborepo you can probably extract it out into a module that only builds when you make changes

"scripts": {
  "next-config": "tsc next.config.ts --skipLibCheck --module commonjs --target esnext --esModuleInterop true --allowSyntheticDefaultImports true --moduleResolution node",
  "start": "npm run next-config && next start"

for more options it's probably better to create a separate tsconfig.next.json and then tsc --project tsconfig.next.json

@stefee
Copy link
Contributor

stefee commented Feb 3, 2022

Next.js bundles SWC now so it seems to me like it might as well look for a next.config.ts and compile it at build time, it should be pretty fast?

@Merott
Copy link

Merott commented Feb 19, 2022

Latest little piece of advice on this: using esbuild in predev and prebuild steps adds less than 100ms of additional transpilation time (in my case).

I find this minor latency entirely acceptable for the tradeoff of being able to type check our configuration. NextJs has moved on significantly from the days when next.config.js, "shouldn't have anything complex in there". In my use case, we have complex logic regarding domain-based localisation, headers, rewrites/redirects, and so on. Our configuration essentially is source code, and needs to be treated as such.

package.json

"scripts": {
  "predev": "yarn build:config",
  "dev": "next dev",
  "prebuild": "yarn build:config",
  "build": "next build",
  "build:config": "esbuild next.config.ts --bundle --outfile=next.config.js --platform=node --external:pnpapi --target=es2020 --minify",
}

Thanks @isaachinman!

Building on that, the latest esbuild supports wildcard for node_modules to treat all packages as external and avoid bundling them into next.config.js:

esbuild next.config.ts --bundle --outfile=next.config.js --platform=node --external:./node_modules/* --target=es2020 --minify

@vjpr
Copy link

vjpr commented Mar 2, 2022

Since my previous comment I now use SWC (no babel) and Next allows .mjs configs.

Here is the config loading code:

const path = await findUp(CONFIG_FILES, { cwd: dir })
// If config file was found
if (path?.length) {
configFileName = basename(path)
let userConfigModule: any
try {
// `import()` expects url-encoded strings, so the path must be properly
// escaped and (especially on Windows) absolute paths must pe prefixed
// with the `file://` protocol
if (process.env.__NEXT_TEST_MODE === 'jest') {
// dynamic import does not currently work inside of vm which
// jest relies on so we fall back to require for this case
// https://github.com/nodejs/node/issues/35889
userConfigModule = require(path)
} else {
userConfigModule = await import(pathToFileURL(path).href)
}

export const CONFIG_FILES = ['next.config.js', 'next.config.mjs']

You can use cjs config or mjs config.

cjs

Use the same approach as my comment and use @swr/register instead.

mjs

NOTE: @swr/register does not work with esm.

Use a shell script to run next using the experimental loader api.

Something like:

#!/bin/sh

node \
--no-warnings \
--experimental-loader=<some-esm-loader> \
"$@"

@yunsii
Copy link
Contributor

yunsii commented Mar 20, 2022

As a TypeScript enthusiast, I implemented the next.config.ts configuration by: (support auto restart next server on change of next.config.ts)

  • concurrently
  • esno
  • chokidar
  • esbuild

how:

// package.json
{
  "scripts": {
    "build:next-config": "esno ./scripts/nextConfig/buildNextConfig.ts",
    "watch:next-config": "esno ./scripts/nextConfig/watchNextConfig.ts",
    "predev": "npm run build:next-config",
    "dev": "concurrently 'next dev' 'npm run watch:next-config'",
    "prebuild": "npm run build:next-config",
    "build": "next build && esno ./scripts/generate-sitemap.ts",
  },
}
// scripts/nextConfig/utils.ts
import chokidar from 'chokidar'
import esbuild from 'esbuild'

export const nextConfigTsPath = `${process.cwd()}/next.config.ts`
export const outputPath = `${process.cwd()}/next.config.mjs`

export function buildNextConfig() {
  esbuild.buildSync({
    entryPoints: [nextConfigTsPath],
    outfile: outputPath,
    format: 'esm',
    platform: 'node',
    target: 'es2020',
  })
}

export function watchNextConfig() {
  chokidar.watch(nextConfigTsPath).on('change', (event, path) => {
    buildNextConfig()
  })
}

// scripts/nextConfig/buildNextConfig.ts
import { buildNextConfig } from './utils'

buildNextConfig()

// scripts/nextConfig/watchNextConfig.ts
import { watchNextConfig } from './utils'

watchNextConfig()

More details refer to my repo: tailwind-nextjs-typescript-starter-blog

@timneutkens timneutkens added kind: story Webpack Related to Webpack with Next.js. labels Mar 31, 2022
@timneutkens
Copy link
Member

timneutkens commented Mar 31, 2022

I'm going to re-open this feature request given that we've been reworking the compiler to be faster which unlocks this feature among others. There will have to be limitations to what can be done though given that next.config.ts can't be loaded in production. It's currently loaded on-demand with next start. To solve compiling it without breaking import/require() all configuration would have to be read on next build. next start would leverage the configuration values provided during next build, overall that's not a problem and would solve inconsistencies with outputStandalone.

@timneutkens timneutkens reopened this Mar 31, 2022
@yunsii
Copy link
Contributor

yunsii commented Mar 31, 2022

Unfortunately, I find that next.js not auto-restart on next.config.mjs updated, so maybe nodemon is better for now _(:з」∠)_

@devinrhode2
Copy link

@timneutkens in what ways could import/require() break?

@devinrhode2
Copy link

devinrhode2 commented Apr 1, 2022

I believe the greatest benefit here is that we can have a 100% TS codebase, while sharing code between next.config.ts and things in our src tree. (JSDoc type annotations were fun to learn but I'm more than happy to throw them away)

That being said, compiling next.config.ts means there's likely other imported TS files that need to be compiled. Likely the shared code will compile very quickly, so if it's compiled twice, once for next.config.ts compilation+read, and again for full TS project compilation, I think that's ok from a performance perspective.

@devinrhode2
Copy link

I don't know who will be implementing this, but if it's easy to secretly/experimentally allow .mjs/.mts (top level await) I can imagine many situations where it'd be very powerful :)

@timneutkens
Copy link
Member

@timneutkens in what ways could import/require() break?

Entirely depends on what other trade-offs are applied but the clear one to make this work would be "next.config.js is only loaded during compilation (next dev / next build) instead of also at runtime (next start) that way the file could be bundled in the same way pages are bundled and output into .next, allowing for import/require() to work.

@KATT
Copy link
Contributor

KATT commented Apr 4, 2022

I've tried the babel approach before, but my favorite workaround right now is to simply add a // @ts-check at the top of next.config.js - unsure why @resir014 got so many downvotes on that. 😅


Edit:

We even infer our publicRuntimeConfig from next.config.js

// utils/publicConfig.ts
import getConfig from 'next/config';
import type * as config from '../next.config';

/**
 * Infers the type from the `publicRuntime` in `next.config.js`
 */
type PublicConfig = typeof config.publicRuntimeConfig;

const nextConfig = getConfig();

export const publicConfig = nextConfig.publicRuntimeConfig as PublicConfig;

@eric-burel
Copy link
Contributor

eric-burel commented Apr 4, 2022

I've tried the babel approach before, but my favorite workaround right now is to simply add a // @ts-check at the top of next.config.js - unsure why @resir014 got so many downvotes on that. 😅

Small note on having .js but TS checks on it: in TS you often need the const keyword to cast enums, otherwise TS will complain that its a "string" and not "ListOfPossibleFoobars = "foo" | "bar". I thought this would be right to use @types directive in .js file (eg Docusaurus does that for sidebar config) but it doesn't work that well.

@ristomatti
Copy link

After embarrassing amount of shameless brute forcing, I managed to hack together a JSDoc based solution to go with next@^12.1.4 and webpack@^5.41.1.

I hope this saves at least someone the many hours I'll never be getting back. Please let me know if there's a way to simplify this seemingly over complicated mess. 🙏

Disclaimer: Some pieces of the puzzle might be missing and only the relevant bits are included.

package.json

{
  "dependencies": {
    "next": "^12.1.4",
    ...
  },
  "devDependencies": {
    "@tsconfig/next": "^1.0.2",
    "typescript": "^4.5.4",
    "webpack": "^5.41.1",
    ...
  }
}

tsconfig.json

{
  "extends": "@tsconfig/next/tsconfig.json",
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "*": ["../types/*"],
      ...
      "webpack": ["../node_modules/webpack/types"]
    },
  ...
  },
  "include": [
    "next-env.d.ts",
    "types/*.d.ts",
    "next.config.js",
    "src"
  ],
  "exclude": ["node_modules"]
}

types/webpack.d.ts

export * from 'webpack';

next.config.js

/// @ts-check
const webpack = require('webpack');

/** @type {import('next').NextConfig} nextConfig */
const nextConfig = {
  // ...
  /** @param {import('webpack').Configuration} config */
  webpack(config) {
    // ...
    return config;
  }
};

module.exports = nextConfig;

@ristomatti
Copy link

@devinrhode2 Have you tried if your @ts-check based config still works with Next ~12.1.x and TS ~4.5.4?

I'm curious since I had set ours in a similar fashion and just noticed it had broken at some point between two Next.js and TS upgrades and moving the source files under src. The only way I was able to get the actual Webpack types to override the practically useless ones that got included from node_modules/next/dist/compiled/webpack/webpack.d.ts was the above combination of a path mapping, a custom webpack.d.ts and explicit @type definition at the point of usage.

One of my failed attempts was this disaster. please note the comment within the snippet.

/**
 * @typedef {import('next').NextConfig} NextConfig
 * @typedef {import('webpack').Configuration} WebpackConfig
 * @typedef {import('next/dist/server/config-shared').WebpackConfigContext} WebpackConfigContext
 * @typedef {(config: WebpackConfig, context: WebpackConfigContext) => any} NextWebpackConfig
 */

/** @type {Omit<NextConfig, 'webpack'>} nextConfig */
const nextConfig = {
  /** @type {NextWebpackConfig} */
  webpack(config, context) {
    // depending on what I tried elsewhere in the configuration, I only got either
    // config or context to have the intended type but not at the sime time
  }
}

Among various other approaches, I tried both specifically including node_modules/webpack/types.d.ts and/or excluding node_modules/next/dist/compiled/webpack/webpack.d.ts within tsconfig.json but no matter what the latter ones came up. In retrospect, I should rather have spent the time learning how to properly debug TypeScript's module resolution than trial and error. 🤦

My apologies for spamming the people watching this in hopes of an official TS solution. Personally I just find this type of approach is the best workaround at this point, given it works with fast refresh. I'm hoping someone might still find this useful.

@devinrhode2
Copy link

One thing I will say, is you need to Pick<> certain properties out of the NextConfig type. It really shouldn't be necessary, I personally see it as a bug. If you don't Pick certain properties out of the type, which reflect which properties you have in your config object, you don't get any type errors for unknown properties, like WebPak (i.e. a typo) or doesNotExist: true

@ristomatti
Copy link

ristomatti commented Apr 6, 2022

That is a good point. I tried using Omit to drop webpack from NextConfig but maybe it was actually the fact NextConfig extends Record<string, any that caused the any value. I'll need to focus on some real work for a change but I'll definitely need to try this at a suitable moment.

The downside of using Pick would be losing the discoverability of new settings etc... but then again, this could be solved simply by building the config from smaller parts 🤔. E.g.

/** @type {Configuration} webpack */
const webpack = { ... };

/** @type {NextConfig} nextConfig */
const nextConfig = {
  ...webpack
};

It's ridiculous how you stop seeing the simplest solutions while getting frustrated and begin to think the issue is more obscure than what it (very often) actually is!

Update 4.6.2022:
It turned out this was actually a difference between how WebStorm and VSCode resolve types. For some reason WebStorm required node_modules/webpack to be specifically added as a project library for it to be detected correctly. So in the end this was enough for me:

/** @type {import('next').NextConfig}*/
const nextConfig = {
  // ...
  /** @param {import('webpack').Configuration} config */
  webpack(config) { /*.. */ }
}

@devinrhode2
Copy link

Well, bottom line is I think next should make the type for NextConfig stricter. @timneutkens any idea why it allows unknown keys?

@raulfdm
Copy link
Contributor

raulfdm commented Apr 6, 2022

Just an addition, I'm seeing that many people want next.config.ts only for getting type inference, which is totally valid but we could use the @type annotation to import the types indeed.

Though my case for having that is duplicated is importing pieces of ts code to compose my configuration. Basically I have both next-mdx-remote (for rendering markdown pages consumed from a CMS) and @next/mdx for static pages.

Because they must have the exact same configuration, I needed to copy and past my TS config implementation inside my next.config.mjs file, which generates a duplication and every single change or addition to my markdown workflow I (or someone from my team) needs to remember that there's 2 places to update.

If my codebase were fully in javascript, I could import easily that configuration and having a single source of truth. However because it's TS based, I don't have this option.

@stefee
Copy link
Contributor

stefee commented Apr 6, 2022

Can I suggest someone create a Discussions thread for this? There’s a fair amount of chatter here but the use case has been made quite clear already, so I’d prefer if this thread is kept to just updates regarding the implementation.

@vercel vercel locked and limited conversation to collaborators Apr 7, 2022
@balazsorban44 balazsorban44 converted this issue into discussion #35969 Apr 7, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
Webpack Related to Webpack with Next.js.
Projects
None yet
Development

Successfully merging a pull request may close this issue.