It’s very annoying having to reload forge tunnel while testing the UI;
It doesn’t happen on every project; But if I knew what was different about this one I would have fixed it already.
Any ideas
It’s very annoying having to reload forge tunnel while testing the UI;
It doesn’t happen on every project; But if I knew what was different about this one I would have fixed it already.
Any ideas
Hey Owen,
I haven’t seen that before, have you tried running it in debugging mode to get more information about what is happening?
Cheers,
Mel
Tried
npx forge tunnel --verbose
But I don’t get any more information; Is there a better command to use?
I can confirm that it happens both in Chrome and in Firefox
Hey, I have the same problem here!
Using UI KIT the forge tunnel goes into loop, I have the same import { example } from ‘./example’; logic on few .jsx files.
After I added that forge tunnel went looping.
It would be great if you can take some video recording so that we can know more on how it happens. Thanks a lot
The problem still exists. @KhanhNguyen has this ever been addressed?
Hi all
I’m hitting the same issue: Forge tunnel gets stuck in an endless “Bundling → Reloading code → Running forge lint” loop when starting tunnel and also when editing a resolver function. This makes the dev cycle very slow and frustrating. Same behavior when running with --no-verify.
Environment:
OS: Windows
IDE: IntelliJ IDEA
Node.js: v22.x
@forge/cli: latest
App type: Custom UI (Vite dev server)
Project layout: frontend under ./static/frontend, backend in ./src
Symptoms Forge tunnel repeatedly prints:
=== Bundling code…
✔ Functions bundled.
Reloading code…
=== Running forge lint…
Listening for requests on local port …
No issues found.
…and keeps looping.
I tried ran a file watcher in parallel to see which files change:
npx chokidar-cli ‘**/*’ --verbose
I don’t see any repetitively changing files while the tunnel loops.
I tried also to add .forgeignore and nothing append
Observations:
Even with the watcher showing no file changes, forge tunnel still loops.
The loop also occurs with --no-verify.
Questions:
Is there a known issue with Forge tunnel’s file watching?
Does forge tunnel fully honor .forgeignore for its watch on Windows?
Any recommended flags or configuration to avoid these loops?
I am experiencing the same issue.
Windows, cli@latest, App type: Custom UI (Parcel)
Solved: the endless reload loop is caused by a Windows 8.3 short-name alias in the path where the Forge CLI is installed
Nothing to do with your source code, your imports, or UI Kit vs Custom UI. If the Forge CLI resolves through a path containing an 8.3 alias (e.g. C:\Users\NICOLA~1 instead of C:\Users\Nicolas Dupre), webpack’s watcher marks that path as deleted on every poll and rebundles forever. On my machine the alias came from nvm-windows.
modifiedFiles/removedFiles output anywhere, so forge tunnel --verbose tells you nothing.--no-verify doesn’t help (it only skips lint)..forgeignore doesn’t exist in Atlassian Forge — zero occurrences in the whole @forge/cli package tree. The .forgeignore docs you find by searching belong to an unrelated homonym tool (forge.readthedocs.io). I tried it earlier in this thread; that’s why it did nothing.Save this as forge-watch-spy.cjs anywhere:
const Module = require("module");
const NEEDLE = /~\d(\\|\/|$)/;
const originalLoad = Module._load;
let patched = false;
Module._load = function (request, ...rest) {
const exported = originalLoad.call(this, request, ...rest);
if (!patched && request === "webpack" && exported && exported.Compiler) {
patched = true;
const proto = exported.Compiler.prototype;
const originalWatch = proto.watch;
proto.watch = function (watchOptions, handler) {
this.hooks.watchRun.tap("spy", (c) => {
console.log(`\n[spy] REBUILD modified=${[...(c.modifiedFiles || [])].length} removed=${[...(c.removedFiles || [])].length}`);
for (const f of c.modifiedFiles || []) console.log(` M ${f}`);
for (const f of c.removedFiles || []) console.log(` D ${f}`);
});
const wfs = this.watchFileSystem;
if (wfs && !wfs.__spied) {
wfs.__spied = true;
const orig = wfs.watch.bind(wfs);
wfs.watch = function (files, dirs, missing, ...args) {
for (const [name, set] of Object.entries({ files, dirs, missing })) {
for (const p of set ? [...set] : []) if (NEEDLE.test(String(p))) console.log(` [spy] SUSPECT in ${name}: ${p}`);
}
return orig(files, dirs, missing, ...args);
};
}
return originalWatch.call(this, watchOptions, handler);
};
}
return exported;
};
Then run the tunnel with it preloaded. No changes to your app, no changes to the CLI:
NODE_OPTIONS='--require "C:/path/to/forge-watch-spy.cjs"' forge tunnel
My output, identical on every cycle:
[spy] REBUILD modified=0 removed=1
D C:\Users\NICOLA~1
[spy] SUSPECT in files: C:\Users\NICOLA~1\AppData\Roaming\nvm\v24.11.1\node_modules\@forge\cli\node_modules\typescript\lib\lib.es5.d.ts
... 96 suspects in files, 18 in missing (all ts-loader resolution probes)
modified=0 on every rebuild is the tell: no file of yours ever changed.
settings.txt → root: C:\Users\NICOLA~1\AppData\Roaming\nvm. This happens when your Windows profile name contains a space.nodejs symlink’s target string is the alias: fs.readlinkSync('…\nvm\nodejs') → C:\Users\NICOLA~1\AppData\Roaming\nvm\v24.11.1.fs.realpathSync.native does), so the CLI’s __dirname keeps the alias.ts-loader therefore registers the CLI’s bundled typescript/lib/*.d.ts as webpack file dependencies under the alias path.stat/open (fs.existsSync('C:\Users\NICOLA~1') → true) but never appears in a directory listing — fs.readdirSync('C:\Users') returns Nicolas Dupre, never NICOLA~1. watchpack determines existence from the parent listing, so it reports the whole subtree as removed.compiler.watch({ poll: 1000 }) and no ignored option (@forge/bundler/out/webpack.js), so this repeats every second: phantom deletion → invalidate → Reloading code... → rebundle → phantom deletion → … The visible cadence is ~8 s because lint + bundle take that long.# 1. back up the user PATH first
[Environment]::GetEnvironmentVariable('Path','User') | Set-Content "$env:USERPROFILE\path-user-backup.txt"
# 2. user-scope nvm vars (they override the correct machine-scope ones)
[Environment]::SetEnvironmentVariable('NVM_HOME', 'C:\Users\Nicolas Dupre\AppData\Roaming\nvm', 'User')
[Environment]::SetEnvironmentVariable('NVM_SYMLINK', 'C:\Users\Nicolas Dupre\AppData\Roaming\nvm\nodejs', 'User')
# 3. alias entries in the user PATH
$p = [Environment]::GetEnvironmentVariable('Path','User')
$p = $p -replace [regex]::Escape('C:\Users\NICOLA~1'), 'C:\Users\Nicolas Dupre'
[Environment]::SetEnvironmentVariable('Path', $p, 'User')
# 4. nvm's own config, then recreate the link with a long-name target
$f = 'C:\Users\Nicolas Dupre\AppData\Roaming\nvm\settings.txt'
Copy-Item $f "$f.bak"
(Get-Content $f -Raw) -replace 'NICOLA~1', 'Nicolas Dupre' | Set-Content $f -NoNewline
cmd /c rmdir "C:\Users\Nicolas Dupre\AppData\Roaming\nvm\nodejs"
cmd /c mklink /J "C:\Users\Nicolas Dupre\AppData\Roaming\nvm\nodejs" "C:\Users\Nicolas Dupre\AppData\Roaming\nvm\v24.11.1"
Restart the terminal (and your IDE) afterwards, then verify: readlinkSync returns the long path, env | grep '~1' is empty, and the tunnel sits quietly on Listening for requests....
Instant workaround if you don’t want to touch your environment yet — call the CLI by its real path so the symlink is bypassed entirely:
"/c/Users/Nicolas Dupre/AppData/Roaming/nvm/v24.11.1/forge" tunnel
Any install path with a ~1 component does this, a profile name with a space is the common trigger, but a moved npm prefix or PROGRA~1 would behave the same. Quick check before anything else: env | grep '~1', plus fs.readlinkSync on any symlink in your toolchain.
Two small changes would kill this class of bug:
fs.realpathSync.native() to the paths handed to webpack/ts-loader (it expands 8.3 names; plain realpathSync does not), or set watchOptions.ignored for CLI-internal dependencies. The app’s own bundle has no business watching @forge/cli’s typescript/lib.compiler.modifiedFiles / removedFiles under --verbose. That single line would have turned a multi-hour investigation into a 10-second read.Environment: Forge CLI 13.0.0, Node 24.11.1, Windows 11 Pro, nvm-windows, Custom UI + resolver app.