Environment
- @sidebase/nuxt-auth 1.1.1
- Nuxt 4.4.8
- vue-tsc 3.3.5
- typescript 6.0.3
Reproduction
- Use nuxt-auth in a Nuxt 4 project with
vue-tsc and incremental builds (incremental: true, which is the Nuxt default).
- Run
nuxt prepare, then run vue-tsc --noEmit once to build the incremental cache.
- Run
nuxt typecheck. It rewrites .nuxt/types/imports.d.ts with the refreshHandler import missing the .ts, which differs from the dev/prepare output.
- The whole program is rechecked from cold instead of reusing the cache.
- Run
nuxt dev again and the line flips back, so the next type check is cold again.
Describe the bug
What happens
The module registers the _refreshHandler auto-import using the destination path of the generated template, and that path still carries the .ts extension.
src/module.ts:
const generatedRefreshHandlerPath = addTemplate({
filename: './refreshHandler.ts',
// ...
}).dst // ends with ".../refreshHandler.ts"
addImports([{
name: '_refreshHandler',
from: generatedRefreshHandlerPath, // registered WITH the .ts extension
}])
Because the from path carries the extension, the generated .nuxt/types/imports.d.ts is not stable across Nuxt commands. The line comes out one of two ways depending on which command generated .nuxt:
- After
nuxt dev or nuxt prepare:
const _refreshHandler: typeof import('../refreshHandler.ts')._refreshHandler
- After
nuxt typecheck:
const _refreshHandler: typeof import('../refreshHandler')._refreshHandler
Why this is a problem
imports.d.ts is a global declaration. Almost every file in an app depends on it through auto-imports. When that one line flips, TypeScript treats the global declaration as changed and throws away the whole incremental build cache (tsconfig.tsbuildinfo), so the next vue-tsc run checks every file from scratch.
In normal use you switch between nuxt dev and nuxt typecheck all the time, so the line flips back and forth and the cache is invalidated on almost every type check.
The size of the hit scales with the project. On a large Nuxt 4 app (several thousand first-party TypeScript and Vue files) a warm vue-tsc run takes about 20 seconds, while a nuxt typecheck that follows a nuxt dev or nuxt prepare runs cold and takes a few minutes. Running the same command twice in a row stays warm. The cold run hits when you go from dev to typecheck, which happens constantly during normal work.
Suggested fix
Register the auto-import without the extension. The module already does this for the custom-handler path just above, which strips .ts with TS_ENDS_RE, so the same treatment for the generated path is consistent:
addImports([{
name: '_refreshHandler',
from: generatedRefreshHandlerPath.replace(TS_ENDS_RE, ''),
}])
With the extension stripped, nuxt dev, nuxt prepare, and nuxt typecheck all generate the identical import('../refreshHandler') line. The global declaration stops changing between commands, so the cache survives. Resolution is fine without the extension: nuxt typecheck was already emitting and type-checking that exact form.
Additional context
Measured impact
I ran a controlled benchmark of the fix: two phases, unpatched and patched, each from a clean install, differing only by this one-line change.
The benchmark reproduces the real workflow. Each measured sample does a dev-lane check (nuxt prepare + vue-tsc, the state nuxt dev or the editor leaves behind) and then runs nuxt typecheck, so it crosses from dev to typecheck.
The dev to typecheck time:
| run |
unpatched |
patched |
| 1 |
151 s |
21 s |
| 2 |
149 s |
21 s |
| average |
150 s (cold) |
21 s (warm) |
That is roughly 7 times faster. I also logged the generated import line at each step. Unpatched it was import('../refreshHandler.ts') in the dev state and import('../refreshHandler') after typecheck; patched it stayed import('../refreshHandler') the whole time.
To rule out other causes I ran loops that never cross from dev to typecheck: typechecks in a row, and typechecks after editing a widely imported file. Those were the same in both phases, cold once then warm, and a steady cascade cost for the edits. So the patch only moves the dev-to-typecheck case.
Full benchmark log (anonymized)
============================================================
[19:44:51] START node=v24.11.0 pnpm=10.20.0
[19:44:51] Loops per phase: nochange=4 edit=4 devswitch=2
============================================================
[19:44:51] SETUP [BASELINE-unpatched]: clean + pnpm i + codegen
[19:44:52] RUN [BASELINE-unpatched] pnpm i
[19:44:59] -> 7s ok
[19:44:59] RUN [BASELINE-unpatched] codegen
[19:45:07] -> 8s ok
============================================================
[19:45:07] PHASE: BASELINE-unpatched
[19:45:07] refreshHandler import line (after setup): const _refreshHandler: typeof import('../refreshHandler.ts')._refreshHandler
[19:45:07] RUN [BASELINE-unpatched] nochange typecheck #1
[19:46:42] -> 95s ok
[19:46:42] RUN [BASELINE-unpatched] nochange typecheck #2
[19:49:01] -> 139s ok
[19:49:01] RUN [BASELINE-unpatched] nochange typecheck #3
[19:49:23] -> 22s ok
[19:49:23] RUN [BASELINE-unpatched] nochange typecheck #4
[19:49:46] -> 23s ok
[19:49:46] RUN [BASELINE-unpatched] edit typecheck #1
[19:51:17] -> 91s ok
[19:51:17] RUN [BASELINE-unpatched] edit typecheck #2
[19:52:49] -> 92s ok
[19:52:49] RUN [BASELINE-unpatched] edit typecheck #3
[19:54:22] -> 93s ok
[19:54:22] RUN [BASELINE-unpatched] edit typecheck #4
[19:56:07] -> 105s ok
[19:56:07] RUN [BASELINE-unpatched] dev-lane (prepare+vue-tsc) #1
[19:57:45] -> 98s ok
[19:57:45] dev-lane imports line: const _refreshHandler: typeof import('../refreshHandler.ts')._refreshHandler
[19:57:45] RUN [BASELINE-unpatched] dev->typecheck #1
[20:00:16] -> 151s ok
[20:00:16] RUN [BASELINE-unpatched] dev-lane (prepare+vue-tsc) #2
[20:02:46] -> 150s ok
[20:02:46] dev-lane imports line: const _refreshHandler: typeof import('../refreshHandler.ts')._refreshHandler
[20:02:46] RUN [BASELINE-unpatched] dev->typecheck #2
[20:05:15] -> 149s ok
[20:05:15] imports line after phase: const _refreshHandler: typeof import('../refreshHandler')._refreshHandler
============================================================
[20:05:15] APPLY PATCH @sidebase/nuxt-auth@1.1.1
============================================================
[20:05:15] SETUP [PATCHED]: clean + pnpm i + codegen
[20:05:16] RUN [PATCHED] pnpm i
[20:05:29] -> 13s ok
[20:05:29] RUN [PATCHED] codegen
[20:05:44] -> 15s ok
============================================================
[20:05:44] PHASE: PATCHED
[20:05:44] refreshHandler import line (after setup): const _refreshHandler: typeof import('../refreshHandler')._refreshHandler
[20:05:44] RUN [PATCHED] nochange typecheck #1
[20:07:22] -> 98s ok
[20:07:22] RUN [PATCHED] nochange typecheck #2
[20:07:46] -> 24s ok
[20:07:46] RUN [PATCHED] nochange typecheck #3
[20:08:09] -> 23s ok
[20:08:09] RUN [PATCHED] nochange typecheck #4
[20:08:32] -> 23s ok
[20:08:32] RUN [PATCHED] edit typecheck #1
[20:10:05] -> 93s ok
[20:10:05] RUN [PATCHED] edit typecheck #2
[20:11:37] -> 92s ok
[20:11:37] RUN [PATCHED] edit typecheck #3
[20:13:13] -> 96s ok
[20:13:13] RUN [PATCHED] edit typecheck #4
[20:14:50] -> 97s ok
[20:14:50] RUN [PATCHED] dev-lane (prepare+vue-tsc) #1
[20:16:27] -> 97s ok
[20:16:27] dev-lane imports line: const _refreshHandler: typeof import('../refreshHandler')._refreshHandler
[20:16:27] RUN [PATCHED] dev->typecheck #1
[20:16:48] -> 21s ok
[20:16:48] RUN [PATCHED] dev-lane (prepare+vue-tsc) #2
[20:17:10] -> 22s ok
[20:17:10] dev-lane imports line: const _refreshHandler: typeof import('../refreshHandler')._refreshHandler
[20:17:10] RUN [PATCHED] dev->typecheck #2
[20:17:31] -> 21s ok
[20:17:31] imports line after phase: const _refreshHandler: typeof import('../refreshHandler')._refreshHandler
============================================================
[20:17:31] SUMMARY (all runs)
============================================================
run seconds exit
[BASELINE-unpatched] pnpm i 7 0
[BASELINE-unpatched] codegen 8 0
[BASELINE-unpatched] nochange typecheck #1 95 0
[BASELINE-unpatched] nochange typecheck #2 139 0
[BASELINE-unpatched] nochange typecheck #3 22 0
[BASELINE-unpatched] nochange typecheck #4 23 0
[BASELINE-unpatched] edit typecheck #1 91 0
[BASELINE-unpatched] edit typecheck #2 92 0
[BASELINE-unpatched] edit typecheck #3 93 0
[BASELINE-unpatched] edit typecheck #4 105 0
[BASELINE-unpatched] dev-lane (prepare+vue-tsc) #1 98 0
[BASELINE-unpatched] dev->typecheck #1 151 0
[BASELINE-unpatched] dev-lane (prepare+vue-tsc) #2 150 0
[BASELINE-unpatched] dev->typecheck #2 149 0
[PATCHED] pnpm i 13 0
[PATCHED] codegen 15 0
[PATCHED] nochange typecheck #1 98 0
[PATCHED] nochange typecheck #2 24 0
[PATCHED] nochange typecheck #3 23 0
[PATCHED] nochange typecheck #4 23 0
[PATCHED] edit typecheck #1 93 0
[PATCHED] edit typecheck #2 92 0
[PATCHED] edit typecheck #3 96 0
[PATCHED] edit typecheck #4 97 0
[PATCHED] dev-lane (prepare+vue-tsc) #1 97 0
[PATCHED] dev->typecheck #1 21 0
[PATCHED] dev-lane (prepare+vue-tsc) #2 22 0
[PATCHED] dev->typecheck #2 21 0
============================================================
[20:17:31] VALIDATION (loop C: dev -> typecheck, the scenario the patch fixes)
[20:17:31] baseline dev->typecheck: 151 149 (avg 150s)
[20:17:31] patched dev->typecheck: 21 21 (avg 21s)
============================================================
[20:17:31] RESULT: PASS
[20:17:31] Unpatched is cold (~150s) on every dev->typecheck; patched stays warm (~21s).
[20:17:31] The patch keeps the vue-tsc incremental cache alive across the dev->typecheck switch.
============================================================
[20:17:31] DONE. Patch is currently APPLIED. To revert: pnpm patch-remove "@sidebase/nuxt-auth@1.1.1" && pnpm install
Environment
Reproduction
vue-tscand incremental builds (incremental: true, which is the Nuxt default).nuxt prepare, then runvue-tsc --noEmitonce to build the incremental cache.nuxt typecheck. It rewrites.nuxt/types/imports.d.tswith therefreshHandlerimport missing the.ts, which differs from the dev/prepare output.nuxt devagain and the line flips back, so the next type check is cold again.Describe the bug
What happens
The module registers the
_refreshHandlerauto-import using the destination path of the generated template, and that path still carries the.tsextension.src/module.ts:Because the
frompath carries the extension, the generated.nuxt/types/imports.d.tsis not stable across Nuxt commands. The line comes out one of two ways depending on which command generated.nuxt:nuxt devornuxt prepare:nuxt typecheck:Why this is a problem
imports.d.tsis a global declaration. Almost every file in an app depends on it through auto-imports. When that one line flips, TypeScript treats the global declaration as changed and throws away the whole incremental build cache (tsconfig.tsbuildinfo), so the nextvue-tscrun checks every file from scratch.In normal use you switch between
nuxt devandnuxt typecheckall the time, so the line flips back and forth and the cache is invalidated on almost every type check.The size of the hit scales with the project. On a large Nuxt 4 app (several thousand first-party TypeScript and Vue files) a warm
vue-tscrun takes about 20 seconds, while anuxt typecheckthat follows anuxt devornuxt prepareruns cold and takes a few minutes. Running the same command twice in a row stays warm. The cold run hits when you go from dev to typecheck, which happens constantly during normal work.Suggested fix
Register the auto-import without the extension. The module already does this for the custom-handler path just above, which strips
.tswithTS_ENDS_RE, so the same treatment for the generated path is consistent:With the extension stripped,
nuxt dev,nuxt prepare, andnuxt typecheckall generate the identicalimport('../refreshHandler')line. The global declaration stops changing between commands, so the cache survives. Resolution is fine without the extension:nuxt typecheckwas already emitting and type-checking that exact form.Additional context
Measured impact
I ran a controlled benchmark of the fix: two phases, unpatched and patched, each from a clean install, differing only by this one-line change.
The benchmark reproduces the real workflow. Each measured sample does a dev-lane check (
nuxt prepare+vue-tsc, the statenuxt devor the editor leaves behind) and then runsnuxt typecheck, so it crosses from dev to typecheck.The dev to typecheck time:
That is roughly 7 times faster. I also logged the generated import line at each step. Unpatched it was
import('../refreshHandler.ts')in the dev state andimport('../refreshHandler')after typecheck; patched it stayedimport('../refreshHandler')the whole time.To rule out other causes I ran loops that never cross from dev to typecheck: typechecks in a row, and typechecks after editing a widely imported file. Those were the same in both phases, cold once then warm, and a steady cascade cost for the edits. So the patch only moves the dev-to-typecheck case.
Full benchmark log (anonymized)