Skip to content

Build fails with compatibilityVersion: 4 and TypeScript templates added via addTemplate #28995

Closed as not planned

Description

@dulnan

Environment

Nuxt project info:                                                                                                                                                                                                                                                                                                                                                                                                              09:23:49

------------------------------
- Operating System: Linux
- Node Version:     v20.17.0
- Nuxt Version:     3.13.1
- CLI Version:      3.13.1
- Nitro Version:    2.9.7
- Package Manager:  npm@10.7.0
- Builder:          -
- User Config:      -
- Runtime Modules:  -
- Build Modules:    -
------------------------------

Reproduction

This example repo shows the issue: https://github.com/dulnan/nuxt4-server-options-reproduction

  • npm run dev works
  • npm run dev:build does not work

Describe the bug

Setting compatibilityVersion: 4 breaks almost all modules of mine (issues were created for dulnan/nuxt-multi-cache#74 and dulnan/nuxt-graphql-middleware#37, but 4 other modules are affected too). What they all have in common is a way for module users to provide runtime configuration in the form of functions. As there is no built-in way to easily do that, I stitch this together using templates. This has always been a bit of a pain, but for the last 1.5 years or so it has worked fairly okay.

Now with Nuxt 4 and the new compatibilityVersion option, this approach does not seem to work anymore. I have created a basic reproduction here: https://github.com/dulnan/nuxt4-server-options-reproduction

In essence:

This works when running npm run dev, however npm run dev:build fails with the following error:

[plugin inject] playground/node_modules/.cache/nuxt/.nuxt/example.serverOptions.ts: rollup-plugin-inject: failed to parse /run/media/dulnan/Storage/development/nuxt4-server-options/playground/node_modules/.cache/nuxt/.nuxt/example.serverOptions.ts. Consider restricting the plugin to particular files via options.include

 ERROR  RollupError: playground/node_modules/.cache/nuxt/.nuxt/example.serverOptions.ts (6:12): Expected ',', got '{' (Note that you need plugins to import files that are not JavaScript)                                                                                                                                                                                                                                nitro 09:22:21


4: 
5: // Import type from the module.
6: import type { ExampleModuleFunctionType } from '../../../../../src/types'
               ^
7: 
8: export const foobar: ExampleModuleFunctionType = () => {


 ERROR  playground/node_modules/.cache/nuxt/.nuxt/example.serverOptions.ts (6:12): Expected ',', got '{' (Note that you need plugins to import files that are not JavaScript)                                                                                                                                                                                                                                                   09:22:21

  at getRollupError (node_modules/rollup/dist/es/shared/parseAst.js:392:41)
  at ParseError.initialise (node_modules/rollup/dist/es/shared/node-entry.js:11495:28)
  at convertNode (node_modules/rollup/dist/es/shared/node-entry.js:13193:10)
  at convertProgram (node_modules/rollup/dist/es/shared/node-entry.js:12537:12)
  at Module.setSource (node_modules/rollup/dist/es/shared/node-entry.js:14356:24)
  at async ModuleLoader.addModuleSource (node_modules/rollup/dist/es/shared/node-entry.js:19047:13) 

The exact same code works when removing compatibilityVersion: 4 and the playground builds just fine.

So I'm not sure if this is actually a bug or if what I'm doing is (and has been) wrong anyway. I'm very much open to adapting a different approach. However, not being able to use TypeScript templates kind of sounds like a bug, so I decided to file this issue.

Additional context

No response

Logs

No response

Activity

  1. github-actions commented on Sep 14, 2024

    @github-actions
    Contributor

    Stackblitz link for the reproduction: Stackblitz

  2. vernaillen commented on Sep 14, 2024

    @vernaillen

    This error started appearing after upgrading to Nuxt 3.13.1, in which this new build cache feature was introduced: #28726

    When not in compatibilityVersion 4, the buildCache is disabled by default, and then the error does not occur.
    The error appears when enabling the buildCache or when enabling compatibilityVersion 4.

    I also noticed that the error does not occur when there is no build cache yet from a previous build.
    To be able to build and deploy on vercel I'm using this build command as a workaround:

    rm -rf .nuxt && npx nuxi cleanup && nuxt build
    
  3. danielroe commented on Sep 14, 2024

    @danielroe
    Member

    Your code is assuming a relative path from the Nuxt build directory:

    // Import the user's server options file.
    import serverOptions from './../server/myModule.serverOptions'

    https://github.com/dulnan/nuxt4-server-options-reproduction/blob/main/src/module.ts#L34

    There are a couple of problems with this:

    1. if the user has a different server directory, this will not work (this is configurable in nuxt.options.serverDir
    2. if the nuxt buildDir is not at the top level, next to server/, this will not work (this is configurable in nuxt.options.buildDir)

    The reason this happens after turning on compatibilityVersion: 4 is not because buildCache is turned on, but rather because the Nuxt production buildDir is located within node_modules/.cache/nuxt. It is also possible for users to configure this even without compatibilityVersion: 4 and your module should ideally handle both cases.

    However, the fix is simple. Instead of using a hard-coded relative path, you can compute the path relative to nuxt.options.buildDir and nuxt.options.serverDir... Or, you could use an absolute path or even a Nuxt alias.

    I hope this helps 🙏

    Let me know if there's anything else I can help with - and thank you for your work on those modules ❤️

  4. dulnan commented on Sep 21, 2024

    @dulnan
    Author

    Thank you very much for the explanation! I gave it a try with computing the correct relative path using the relative util. That did not work unfortunately.

    I have reduced the example to the absolute minimum, with no complex relative imports at all. I am now generating 2 templates, the existing serverOptions file and a file that exports the type. I'm no longer importing anything from the project's serverDir.

    import {
      defineNuxtModule,
      createResolver,
      addTemplate,
      addServerHandler,
    } from '@nuxt/kit'
    
    // Module options TypeScript interface definition
    export interface ModuleOptions {}
    
    export default defineNuxtModule<ModuleOptions>({
      meta: {
        name: 'my-module',
        configKey: 'myModule',
      },
      // Default configuration options of the Nuxt module
      defaults: {},
      setup(_options, nuxt) {
        // Resolves paths relative to the module.
        const moduleResolver = createResolver(import.meta.url)
    
        // Directly generate the types in the same folder as the serverOptions file.
        addTemplate({
          write: true,
          filename: 'example.types.ts',
          getContents: function () {
            return `
    export type ExampleModuleFunctionType = () => string
    `
          },
        })
    
        const template = addTemplate({
          write: true,
          filename: 'example.serverOptions.ts',
          getContents: function () {
            return `
    // Import type from file in same directory.
    import type { ExampleModuleFunctionType } from './example.types'
    
    export const foobar: ExampleModuleFunctionType = () => {
      return 'Default Value'
    }`
          },
        })
    
        nuxt.options.alias['#my-module-server-options'] = template.dst
        nuxt.options.build.transpile.push(template.dst)
    
        // Add an example server handler.
        addServerHandler({
          handler: moduleResolver.resolve('./runtime/server/test'),
          route: '/api/test',
        })
      },
    })

    This is the generated file that imports the type from a file in the same folder:

    // Import type from file in same directory.
    import type { ExampleModuleFunctionType } from './example.types'
    
    export const foobar: ExampleModuleFunctionType = () => {
      return 'Default Value'
    }

    And the error is still the same:

     ERROR  RollupError: playground/node_modules/.cache/nuxt/.nuxt/example.serverOptions.ts (3:12): Expected ',', got '{' (Note that you need plugins to import files that are not JavaScript)                                                                                                                                                                                                                                nitro 06:57:45
    
    
    1: 
    2: // Import type from file in same directory.
    3: import type { ExampleModuleFunctionType } from './example.types'
                   ^
    4: 
    5: export const foobar: ExampleModuleFunctionType = () => {
    
    
     ERROR  playground/node_modules/.cache/nuxt/.nuxt/example.serverOptions.ts (3:12): Expected ',', got '{' (Note that you need plugins to import files that are not JavaScript)                                                                                                                                                                                                                                                   06:57:45
    
      at getRollupError (node_modules/rollup/dist/es/shared/parseAst.js:392:41)
      at ParseError.initialise (node_modules/rollup/dist/es/shared/node-entry.js:11495:28)
      at convertNode (node_modules/rollup/dist/es/shared/node-entry.js:13193:10)
      at convertProgram (node_modules/rollup/dist/es/shared/node-entry.js:12537:12)
      at Module.setSource (node_modules/rollup/dist/es/shared/node-entry.js:14356:24)
      at async ModuleLoader.addModuleSource (node_modules/rollup/dist/es/shared/node-entry.js:19047:13) 
    
  5. frasza commented on Sep 23, 2024

    @frasza
    Contributor

    Might not be related, but I found out that my CI docker builds are also failing due to compat 4 option -> #29098

  6. dulnan commented on Oct 19, 2024

    @dulnan
    Author

    After some more digging I think that generating a .ts file with addTemplate is likely something that just happened to work previously, but isn't really supported. I looked at various modules and the Nuxt repository itself and haven't found anyone else doing this. The workaround (or likely "proper" way anyway) is to split each template into two, one .mjs and one .d.ts file. Basically only generate non-TS files that can be properly bundled. I will update my modules accordingly.

  7. danielroe commented on Nov 4, 2024

    @danielroe
    Member

    I think the issue here is likely that we don't transpile the buildDir by default (if it's in node_modules, which it is for compatibilityVersion: 4).

    This should be fixed in Nuxt.

  8. added
    🐛 bugSomething isn't working as expected
    🔨 p3-minorMinor issue, should be fixed
    and removed on Nov 4, 2024
  9. dulnan commented on Mar 21, 2025

    @dulnan
    Author

    Just ran into this problem again, this time because I'm adding a template with the .ts extension where the source is provided by a third party library. There is no easy way for me to split this into two templates with separate .js and .d.ts files. I tried pushing the path of the generated template to nuxt.options.build.transpile, however that did not work. Is there a potential workaround I could do until it's fixed?

  10. 7 remaining items

  11. Gwynerva commented on May 12, 2026

    @Gwynerva
    Contributor

    @danielroe With addTemplate and write: true there is a way to write full TypeScript template with runtime code and types together and use both types and runtime code in Nuxt App.

    // In module setup
    
    addTemplate({
      write: true,
      filename: 'my-module/template.ts
      getContents: () => `export const myArr = [1, 2, 3] as const;`
    })
    
    // In Nuxt App
    
    import { myArr } from '#build/my-module/template';
    
    myArr; // correctly infers type as [1,2,3]
    
    // On server side
    
    import { myArr } from '#build/my-module/template'; // errors "#build" is not accessible from server-side
    import { myArr } from '#my-module-alias/template': // works fine in dev mode, breaks when building if file contains TS code

    However, the server side has no access to templates added via addTemplate. Attempting to bypass this by creating an alias to such a template throws an error during the build — Rolldown complains about the TS code (although it works fine in dev mode). Additionally, addServerTemplate has no write property and does not seem to be detected by the project's TypeScript system, resulting in the error: "Cannot find module ... or its corresponding type declarations."

    Is support for TypeScript templates on the server side planned?

    It is really annoying to manually split everything into .mjs and .d.ts files, and it requires a lot of extra effort to make Nitro pick up these types...

  12. danielroe commented on May 12, 2026

    @danielroe
    Member

    @Gwynerva you expose types to your nitro + nuxt apps like this:

    addTypeTemplate({
      filename: 'my-module/template.d.ts',
      getContents: () => `export const myArr = [1, 2, 3] as const;`
    }, { nitro: true, nuxt: true })

    does that work for you?

  13. Gwynerva commented on May 12, 2026

    @Gwynerva
    Contributor

    @danielroe

    In my module setup I have:

      addServerTemplate({
        filename: '#my-module/template.mjs',
        getContents() {
          return `export const test = [1,2,3];`;
        },
      });
    
      addTypeTemplate(
        {
          filename: '#my-module/template.d.ts',
          getContents: () => `export const myArr = [1, 2, 3] as const;`,
        },
        { nuxt: true, nitro: true },
      );

    In server plugin I have:

    import { bootTheiServer } from './boot/process';
    import { test } from '#my-module/template.mjs';
    
    export default defineNitroPlugin(async (nitroApp) => {
      console.log(test);
      THEI_SERVER.console.log('Server plugin started.');
      await bootTheiServer();
    });

    It still errors of missing types but at least the console log works in dev mode.
    Changing the extension to .ts or .js or none at all breaks server and errors "unknown module, treating as external" so the docs there should be corrected as well: https://nuxt.com/docs/4.x/api/kit/templates#creating-a-virtual-file-for-nitro

    I tried to fix all this like a week ago and figured out you need to manually configure tsConfig for types to work which is even worse: 3 actions needed to make TypeScript work on Nitro side. I dropped this idea and tried to trick Nitro with global Nuxt alias pointing to addTemplate file. Works in dev but breaks of TS code when building sadly.

    So no it does not work correctly, no TS support and it still requires painful manual splitting of code with duplicating content.

  14. danielroe commented on May 12, 2026

    @danielroe
    Member

    I tried to fix all this like a week ago and figured out you need to manually configure tsConfig for types to work which is even worse: 3 actions needed to make TypeScript work on Nitro side.

    how did you manually configure tsConfig? what do you mean by this?

  15. Gwynerva commented on May 12, 2026

    @Gwynerva
    Contributor

    Can't remember right now, it was Claude doing this stuff, but I think it added additional include for .d.ts with types for Nitro server tsconfig or something like that.

  16. danielroe commented on May 12, 2026

    @danielroe
    Member

    that is exactly what passing the second option: { nitro: true } does. (assuming you have the version of nuxt/kit that the feature is implemented in)

  17. Gwynerva commented on May 12, 2026

    @Gwynerva
    Contributor

    Well I tried exactly this right now and it fails. I can create a new issue if you want like in 10 minutes.
    Also worth noting that I use layers setup "base project" + ".playground" so maybe this breaks it.
    I am using lastest Nuxt.

  18. Gwynerva commented on May 12, 2026

    @Gwynerva
    Contributor

    @danielroe I was able to reproduce it on clean latest Nuxt version without any layers involved: #35064

  19. danielroe commented on May 12, 2026

    @danielroe
    Member

    @Gwynerva I had another look and realised there was an issue with the content of the type template in the example I provided earlier.

    I've provided a better example in the linked issue, which works.

  20. fukcrz commented on May 28, 2026

    @fukcrz

    @danielroe

    I can still reproduce a similar issue with Nuxt 4.4.5 / Nitro 2.13.4.

    The root cause seems to be that Nuxt moves the production buildDir under node_modules/.cache/nuxt/.nuxt when .nuxt already exists.

    In Nuxt 4.4.5, the related source is in:

    @nuxt/kit/dist/index.mjs

    The relevant logic is:

    const defaultBuildDir = join(nuxtConfig.rootDir, ".nuxt")
    if (
      !opts.overrides?._prepare &&
      !nuxtConfig.dev &&
      !nuxtConfig.buildDir &&
      existsSync(defaultBuildDir)
    ) {
      nuxtConfig.buildDir = join(nuxtConfig.rootDir, "node_modules/.cache/nuxt/.nuxt")
    }

    Then .ts templates generated by addTemplate also live under node_modules, and Nitro/Rollup seems to parse them as plain JS instead of transforming them as TS.

    The related Nitro source seems to be in:

    nitropack/dist/rollup/index.mjs

    where the esbuild transform excludes node_modules:

    const EXCLUDE_REGEXP = /node_modules/

    Example error:

    RollupError: node_modules/.cache/nuxt/.nuxt/resources.ts (1:12): Expected ',', got '{'
    1: import type { Permission } from "../../../../../auth/shared/utils/access"
                   ^

    A temporary workaround is to explicitly set:

    export default defineNuxtConfig({
      buildDir: ".nuxt",
    })

    With this, #build stays in .nuxt, and the TS template parse error disappears.

    Leaving this here in case it helps clarify the failure mode. If this should be tracked as a separate Nuxt/Nitro issue, I can open one with a minimal reproduction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions