| 📦 Turbo JS 91999d1 k33g 12h ago | 1 | # JavaScript tools — explanation |
| 2 | |
| 3 | ## What is this about? |
| 4 | |
| 5 | A **JavaScript** menu whose commands come from a TOML file, each run where the tool asked — a popup, a terminal window, or an editing window — and the open files re-read afterwards. This page is about why each of those is the way it is. |
| 6 | |
| 7 | ## Why the output has three places to go, and a popup by default |
| 8 | |
| 9 | The first version of this mechanism, in Turbo Go, put every command in a terminal window, and it was the wrong default for most of them. |
| 10 | |
| 11 | A terminal is the right answer when the program is *interactive or long*: `node main.js` on a script that reads `process.stdin` has to be answerable, and `npm start` on a project whose start script is an HTTP server has to be interruptible with `Ctrl-C`. Neither is true of `node --test`, which prints its report and ends. Giving that a whole window — one you then have to close, on a desktop where windows overlap and are numbered — is more ceremony than the result deserves. |
| 12 | |
| 13 | A popup is the right answer for a command you run, read and dismiss. It is modal, which is a real cost and is named in the [how-to](../how-to/run-node-commands.md): an `npm install` you did not expect to be slow — it is fetching half of the registry — holds the editor until it finishes or you press Escape. That cost was accepted on purpose, because the alternative — a dialog appearing unbidden three seconds later — swallows whatever was being typed at the moment it arrives. |
| 14 | |
| 15 | So the popup **opens immediately and fills in**. You see progress, nothing surprises you, and Escape both closes it and stops the command, which is the only way to interrupt something whose output is not in a terminal. |
| 16 | |
| 17 | An editing window is the right answer for output you are going to work through: a long test report, or the JSON `npm ls --json` prints. It is an ordinary buffer, so `Ctrl-F` searches it and `Save as` keeps it. It is filled once the command has ended rather than as it goes, because a buffer growing under the cursor while you search it is the opposite of what that mode is for. |
| 18 | |
| 19 | None of those three is right for everything, which is why `output` is in the file rather than in the code. `Run` is the worked example: it says `terminal`, and the comment beside it says why. So does `Start`, because the thing behind `npm start` is nearly always a server. |
| 20 | |
| 21 | ## Why Install comes first |
| 22 | |
| 23 | In Turbo Golo's starter file the first tool runs the script, because for a scripting language with no dependencies "what does it print?" is the question asked most often. A Node project has dependencies before it has anything else: a fresh clone does nothing — not run, not test, not lint — until `npm install` has written `node_modules/`, and every other command in the menu assumes it has. So the first line of the JavaScript menu installs, and the rest follow in the order a project is usually worked on: format, lint, test, run. |
| 24 | |
| 25 | ## Why there is no Build |
| 26 | |
| 27 | JavaScript has no compile step, and a menu item that pretended otherwise would run something. Turbo MoonBit's first tool is `moon check` and Turbo Rust's third is `cargo build`, because those languages have a compiler with an opinion. Node reads the source. The nearest thing — `node --check main.js`, which parses a file without running it — is real and cheap, and it is left to the project to add, because the language server already marks every syntax error in the gutter as you type, which is the same check earlier. |
| 28 | |
| 29 | A project with a bundler or a TypeScript step has a build, and it is one line in the file: `npx vite build`, `npx tsc`. The starter file cannot know which, so it names none. |
| 30 | |
| 31 | ## Why a terminal window is still there |
| 32 | |
| 33 | The editor already had one — a real pseudo-terminal with a VT emulator, built for the `F8` windows — so `output = "terminal"` costs one field on its options and buys colours, paging, `Ctrl-C`, keyboard input and scrollback for nothing, because they are the same mechanisms every other terminal uses. Node's own tools use those colours: `node --test` draws green ticks and red crosses, and `npm install` draws a progress bar. |
| 34 | |
| 35 | The window stays after the command exits, which is the point: the output is what you asked for, and a window that vanished with it would be useless. |
| 36 | |
| 37 | ## Why the exit code is always in the title |
| 38 | |
| 39 | `npx prettier --write .` on a project already formatted prints nothing at all, and so does `npx eslint .` on a clean one. A popup with an empty body and a neutral title is indistinguishable from one whose command has not started, and the reader is left guessing at the one thing they wanted to know. |
| 40 | |
| 41 | So the title carries the verdict — `— ok` or `— exit 1` — and an empty body says `(no output)` once the command has ended. While it is still running the body stays blank, because "(no output)" is a verdict and a running command has not reached one. |
| 42 | |
| 43 | ## Why the commands are in a file |
| 44 | |
| 45 | Six commands hardwired into the editor would have answered the request. They would also have been wrong within a week. |
| 46 | |
| 47 | Every command in the starter file goes through `node`, `npm` or `npx`, so none of them needs anything set up beyond Node itself. That is a defensible default and it is nobody's universal answer. A project on pnpm wants `pnpm install` and `pnpm test`, one on Bun wants `bun run`, one on Deno neither. A project with one entry point wants `node server.js` without being asked which script. One that tests with Vitest wants `npx vitest run` and never `node --test`. One that runs under Docker wants `docker compose up`. None of that is knowable from here, and all of it is one line in a file. |
| 48 | |
| 49 | So the six are **defaults, not code**: they are the contents of the starter file that **JavaScript ▸ Create tools file** writes, and changing one is editing a file rather than rebuilding an editor. The file is read every time the menu opens, for the same reason the Snippets menu is: an edit should take effect at once, and the file is often open in the window behind the menu. |
| 50 | |
| 51 | Commands go to `sh -c` — `cmd.exe /S /C` on Windows — rather than being split into an argv here. The file is the user's own, so pipes, globs and `&&` are features rather than hazards, and one entry can be `npx eslint . && node --test`. Splitting an argv would mean inventing quoting rules for a string somebody wrote by hand. |
| 52 | |
| 53 | ## Why there is no user-level tools file |
| 54 | |
| 55 | Snippets are read from two files — yours and the project's — because your snippets are your habits and should follow you between projects. |
| 56 | |
| 57 | Tools are not like that. They belong to a project's own toolchain: a global tools file would offer `npm install` in a repository that has never heard of Node, and a project on pnpm would get `npm start` in its menu beside its own commands. The file is per-project, and that is the whole of the rule. |
| 58 | |
| 59 | ## Why a tool may name its own menu |
| 60 | |
| 61 | A menu called **JavaScript** holding `docker compose up` is a lie about what the menu is. The first tools file anybody writes outgrows JavaScript, because a project's commands are not all about the language it is written in: containers, databases, deploys, a `Makefile` target somebody added in 2019. |
| 62 | |
| 63 | Two shapes were considered. A **fixed second menu** called Tools — everything JavaScript in JavaScript, everything else in Tools — is one key in the format and no naming problem at all, but it only moves the lie: a Tools menu holding `docker compose up`, `psql`, and a deploy script is just as undifferentiated, and the moment there are ten entries nobody can find one. And a **second file**, `menus.toml`, keeps the tools file simple at the cost of two files that have to agree about which tools exist. |
| 64 | |
| 65 | So the menu is a **free-form name on the tool**, in the one file: `menu = "Docker"`. A name nothing else uses creates the menu; leaving the key out means JavaScript. There is no list of allowed names, because a list would be a list of somebody else's projects. |
| 66 | |
| 67 | JavaScript itself stays fixed on the bar rather than becoming just another name from the file. **JavaScript ▸ Create tools file** has to be reachable in a project that has no tools file at all — which is exactly the project that needs it — and a menu that only exists once the file exists cannot offer to write the file. |
| 68 | |
| 69 | ## Why the hot key is not the file's to choose |
| 70 | |
| 71 | The author of a tools file cannot know which letters are free. They can see `File`, `Edit`, `Search`, `Run`, `Code`, `Options`, `Window`, `Snippets`, `Agent`, `JavaScript` and `Help` on the bar, but only by counting the underlines, and a project shared between people would then depend on nobody adding a menu that collides. |
| 72 | |
| 73 | Collisions here are **silent**, which is what makes them worth designing against. The bar answers the first menu whose hot key matches; a second menu claiming the same letter is not an error and draws normally — it simply never opens. That trap has already been sprung once in this family: `Snippets` and `Search` both wanted `S`, `Snippets` was the unreachable one, and every test passed. The fix then was to move Snippets to `N` by hand. Letting a file name menus makes that a permanent hazard rather than a one-off mistake, so the assignment is done by the editor: the first letter of the name nothing else claims. |
| 74 | |
| 75 | Tildes written into the name are honoured **when the letter is free**, and quietly overridden when it is not. Refusing the file instead was the alternative, and it is worse: the clash depends on which menus exist, so a tools file that worked would break the day an editor release added a menu — as one did when the Agent menu took `A`. Between a menu on a letter you did not ask for and a menu you cannot open, the first is the smaller loss. |
| 76 | |
| 77 | When every letter of a name is taken, the menu gets no hot key at all. `F10`, the arrow keys and the mouse still reach it, and the alternative — reaching for a letter that is not in the name — would put an underline under nothing. |
| 78 | |
| 79 | ## Why the bar is rebuilt from a stat |
| 80 | |
| 81 | `Menu.OnOpen` refills a menu's items just before it drops down, which is how the JavaScript and Snippets menus follow their files without a restart. It cannot help here: the *set* of menus is part of the bar, not part of any one menu, and adding `menu = "Docker"` to the file should put Docker on the bar. |
| 82 | |
| 83 | Reading and parsing the file on every turn of the event loop would do it, and would also be work done for nothing on every keystroke of a file nobody has edited. So the bar carries the size and modification time of the tools file it was built from, and one `stat` per turn decides whether to rebuild. Editing the file in the window in front of you, saving it, and watching the bar change is the case this is for. |
| 84 | |
| 85 | ## Why open files are re-read, and only some of them |
| 86 | |
| 87 | `npx prettier --write .` rewrites the file in front of you. `npm install` writes `node_modules/` and `package-lock.json` into the directory. `npx eslint --fix .` rewrites whatever it can. Without anything further, the editor would sit on a stale copy of a file another command changed, and the next `F2` would write your copy back over the command's work — undoing the formatting you just asked for. |
| 88 | |
| 89 | So when a command finishes, the editor re-reads every open file. The interesting part is which ones it refuses to touch. |
| 90 | |
| 91 | **A file with unsaved changes is left alone**, and the status bar says how many were skipped. Reloading it would throw away work the user has not saved, which no amount of convenience justifies. And the conflict is genuine: the command and the unsaved edit disagree about what the file should say, and the editor is not in a position to decide. Naming it and stopping is the honest outcome. |
| 92 | |
| 93 | Two smaller decisions inside that: |
| 94 | |
| 95 | - **The cursor stays where it was**, clamped into whatever the file now holds. |
| 96 | - **The undo history is discarded.** Undoing back past a reload would restore text the file no longer has, which is worse than not being able to undo at all. |
| 97 | |
| 98 | ## Why the reload happens on the event loop |
| 99 | |
| 100 | The command's exit is noticed on the goroutine reading the terminal, which may not touch a buffer or the desktop. So it sets a flag, and the reload runs at the top of the next turn of the event loop. |
| 101 | |
| 102 | This is the fourth thing in the library built that way — the language-server announcement, the terminal redraws, the autosave deadline, and now this. The rule they share is worth stating once more: **the wake-up may be lost, so the state must not be.** `PostEvent` drops what does not fit in its queue, so anything that depends on a message arriving is a bug waiting for a busy moment. A flag the loop checks for itself cannot go missing. |
| 103 | |
| 104 | ## Why a command can ask for a value, and why it asks in double braces |
| 105 | |
| 106 | `node` needs a script. A Node project has no single entry point the editor could know: `package.json` may name a `main`, a `bin`, a `start` script or none of them, and the file being edited is as often a module as a program. A tool that cannot ask is a tool that has to be edited before each use, which is not a tool. |
| 107 | |
| 108 | So a `{{label}}` in a command is a value the editor asks for first, in a box titled after the tool. **`Run` uses it**, which is deliberate: a feature demonstrated in the file everybody gets is a feature people find, and one described only in a comment is not. The other five need no value, because `npm` and `npx` read `package.json` and know what to do — which is the difference between a language with a manifest and one without, and the reason Turbo Golo's starter file asks six times where this one asks once. |
| 109 | |
| 110 | **Single braces were the obvious spelling and are wrong.** `awk '{print $1}'` and `find . -exec rm {} +` are ordinary things to put in a tools file, and reading the first as a placeholder turns a working command into a box asking for "print $1". Double braces collide with almost nothing — and nothing in a shell command, where JavaScript's own `${…}` never appears. |
| 111 | |
| 112 | **The value is quoted by default**, because the alternative fails silently. A path with a space in it, substituted raw, becomes two arguments and the command reports something about a file that does not exist. Quoting makes that case work and makes the other case — "put these three flags on the end" — impossible, so `...` inside the braces asks for the value verbatim. Two behaviours, both documented, rather than one that is wrong half the time. |
| 113 | |
| 114 | **Nothing is remembered on disk.** The box starts from what was typed last time, for the session. Writing it into the project's own directory was considered and rejected: that directory holds what the project decided, and the script somebody ran while chasing one bug is not that. |
| 115 | |
| 116 | **A file that cannot be parsed is refused when it is read**, not when the tool is chosen. An unclosed `{{` reaching the shell is a command failing with braces in it, which names neither the tool nor the file; refusing at load names both. That is the same rule an unknown `output` value already follows. |
| 117 | |
| 118 | **The dialog is refused when it will not fit.** A tool asking for more values than the terminal has rows would give a box whose OK button is below the bottom of the screen — answerable only by Escape, which cancels. Saying "this asks for twelve values and nine fit" is worse than nothing only if you would rather find out by trying. |
| 119 | |
| 120 | ## How it relates to the rest |
| 121 | |
| 122 | - Every key of the file and every rule: [JavaScript tools reference](../reference/javascript-tools.md) |
| 123 | - Using it: [How to run Node commands from the editor](../how-to/run-node-commands.md) |
| 124 | - The windows `output = "terminal"` uses, and why they are real terminals: [Terminal windows](terminal-windows.md) |
| 125 | - The other menu built from a file: [Snippets](snippets.md) |