Build fails with `compatibilityVersion: 4` and TypeScript templates added via `addTemplate`
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: [email protected]
- Builder: -
- User Config: -
- Runtime Modules: -
- Build Modules: -
------------------------------
Reproduction
This example repo shows the issue: https://github.com/dulnan/nuxt4-server-options-reproduction
npm run devworksnpm run dev:builddoes not work
Describe the bug
Setting compatibilityVersion: 4 breaks almost all modules of mine (issues were created for https://github.com/dulnan/nuxt-multi-cache/issues/74 and https://github.com/dulnan/nuxt-graphql-middleware/issues/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:
- Users can provide runtime "server options" by creating a file in
~/server/myModule.serverOptions.ts - Using
addTemplatethe module creates a "compiled"example.serverOptions.tsfile that imports the user-provided file and exports functions with default implementations - A server handler provided by the module imports the compiled template and calls a method on it
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
Stackblitz link for the reproduction: Stackblitz
This error started appearing after upgrading to Nuxt 3.13.1, in which this new build cache feature was introduced: https://github.com/nuxt/nuxt/pull/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
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:
- if the user has a different server directory, this will not work (this is configurable in
nuxt.options.serverDir - if the nuxt
buildDiris not at the top level, next toserver/, this will not work (this is configurable innuxt.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 ❤️
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)
Might not be related, but I found out that my CI docker builds are also failing due to compat 4 option -> #29098
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.
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.
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?
~~After some trial and error I found something that seems to work, though I'm not quite sure why.~~
const nitroExternals = ['#my-alias']
nuxt.options.nitro.externals = nuxt.options.nitro.externals || {}
nuxt.options.nitro.externals.inline =
nuxt.options.nitro.externals.inline || []
nuxt.options.nitro.externals.inline.push(...nitroExternals)
const rollupConfig = nuxt.options.nitro.rollupConfig || {}
if (!rollupConfig.external) {
rollupConfig.external = nitroExternals
} else if (typeof rollupConfig.external === 'function') {
const originalFn = rollupConfig.external
rollupConfig.external = (source, importer, isResolved) => {
if (originalFn(source, importer, isResolved)) {
return true
}
return nitroExternals.includes(source)
}
} else if (Array.isArray(rollupConfig.external)) {
rollupConfig.external.push(...nitroExternals)
} else {
rollupConfig.external = [rollupConfig.external, ...nitroExternals]
}
nuxt.options.nitro.rollupConfig = rollupConfig
Edit: It does not work, it only makes the build work. Makes sense in hindsight, since it now treats the alias as an external. I would have thought that also adding it to externals.inline would make it work, but that's not the case.
I have the same issue. I created a small minimal repo for reproduction: https://github.com/GaborTorma/nuxt-module-template-error
Stackblitz: https://stackblitz.com/~/github.com/GaborTorma/nuxt-module-template-error?file=playground/nuxt.config.ts
If compatibilityVersion: 4 and not dev mode, the buildDir moved to node_modules/.cache/nuxt/.nuxt
If switched back to compatibilityVersion: 3 there are no build error, because the buildDir is .nuxt. Doesn't changed to node_modules/.cache/nuxt/.nuxt
Workaround: if fixed the buildDir: './.nuxt' in nuxt.config.ts there are no build error, because buildDir not in node_modules
@GaborTorma you should use addServerTemplate if you are adding code into the nitro build.