@typetorch/framework 0.5.5 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,21 @@ It ships **inside every artifact**, so each generation runs its own fresh copy a
9
9
  code. It needs the TypeTorch kernel in the place (`@typetorch/kernel`), which calls your `Server/boot` and
10
10
  `Client/boot` modules.
11
11
 
12
+ ### How it fits with the other TypeTorch packages
13
+
14
+ | Package | Role | Where it runs |
15
+ |---|---|---|
16
+ | `@typetorch/kernel` | Baked into the place. Picks the branch, loads the payload, calls `boot(kernel)`, swaps generations, owns the stable remotes, `persist`, roles, settings and the health window. | the place (server and client) |
17
+ | `@typetorch/framework` (this) | Modules, DI, lifecycle, troves, network, hot assets, analytics and the dev menu, on top of the kernel API (`src/kernel.ts` types it). | inside every payload |
18
+ | `@typetorch/transformer` | roblox-ts compiler plugin: constructor dependency ids, network guards, `Dependency<T>()` / `Lazy<T>` / `TypeTorch.module<T>()` ids and user macros. Its `runtime-kit/` is copied here as `src/reflection/`. | your compile (Node) |
19
+ | `@typetorch/cli` (`typetorch`, alias `tt`) | Builds the payload (`typetorch build`), deploys and hot-swaps it, rolls back, syncs hot assets, writes the signed settings (`liveConfig`, analytics, experiments), migrates Flamework games. | your machine / CI |
20
+ | `@typetorch/dev-server` | `remote-claude`: the dev menu's Claude tab and Logs > Upload talk to it. | a dev's machine |
21
+
22
+ The framework never talks to the CLI directly: the CLI builds and signs payloads and settings, the kernel loads them,
23
+ and the framework reads what the kernel hands it. Features that need a newer kernel check `TypeTorch.features` and say
24
+ "needs kernel x.y.z" on older ones; the dev menu flags kernels older than `LATEST_KERNEL` (`src/devtools/health.ts`,
25
+ now 0.5.3) with a Migrate issue.
26
+
12
27
  ## Install
13
28
 
14
29
  ```sh
@@ -84,7 +99,7 @@ mapping.
84
99
  ```ts
85
100
  // src/server/boot.ts
86
101
  import { startServer, type ServerKernel } from "@typetorch/framework";
87
- import { BUILD } from "../shared/build";
102
+ import { BUILD } from "../shared/build"; // written by `typetorch build`
88
103
  export function boot(kernel: ServerKernel) {
89
104
  return startServer(kernel, { modules: [script.Parent!.FindFirstChild("services")!], build: BUILD });
90
105
  }
@@ -96,6 +111,10 @@ interface ServerToClient { coins: { changed(total: number): void } }
96
111
  export const network = createNetwork<ClientToServer, ServerToClient>();
97
112
 
98
113
  // src/server/services/coin.service.ts
114
+ import { Module, Service, type OnStart } from "@typetorch/framework";
115
+ import { network } from "../../shared/net";
116
+ import { ScoreService } from "./score.service";
117
+
99
118
  @Service()
100
119
  export class CoinService extends Module implements OnStart {
101
120
  constructor(private readonly score: ScoreService) { // injected
@@ -103,13 +122,22 @@ export class CoinService extends Module implements OnStart {
103
122
  }
104
123
  onStart() {
105
124
  this.trove.add(network.server.coins.collect.on((player, coinId) => this.score.add(player, 1)));
125
+ this.trove.add(network.server.coins.balance.handle((player) => [this.score.get(player)]));
106
126
  }
107
127
  }
108
128
  ```
109
129
 
130
+ `startClient(kernel, options)` is the same for `src/client/boot.ts` with your controllers folder. `StartOptions`:
131
+ `modules` (folders whose ModuleScripts are required, recursively, so their decorators register), `build` (the
132
+ generated build info, shown in the dev menu) and `devtools` (default `true`; `false` leaves the dev menu out). Both
133
+ return the generation's stop function, which the kernel calls on a swap.
134
+
110
135
  - **Modules:** `@Service()` (server) and `@Controller()` (client) classes extending `Module`. Constructor parameters
111
136
  are other modules, injected by type. Lifecycle: `OnInit` (sequential, dependencies first), `OnStart` (spawned),
112
137
  `OnStop` (reverse order), `OnTick`, `OnPhysics`, `OnRender`, `OnPlayerAdded` (replays players already in the server).
138
+ `@Service({ loadOrder: -1 })` breaks ties between modules with no dependency relation (lower first, default 0).
139
+ `this.ctx` has the realm, artifact, branch, channel, generation, build info, the raw kernel, `persist` and
140
+ `playerState`. `observePlayers(trove, (player, playerTrove) => ...)` is `OnPlayerAdded` for code outside a module.
113
141
  - **Modules outside the constructor:** `Dependency<T>()` (or `TypeTorch.module<T>()`) returns the running module of
114
142
  type T from anywhere: a method, a plain class a module built, a command handler. `TypeTorch.tryModule<T>()` returns
115
143
  undefined instead of throwing. They work once every module is constructed (from onInit on); in a constructor, a
@@ -161,6 +189,11 @@ export class CoinService extends Module implements OnStart {
161
189
  last minute's average, its slowest second and the physics FPS (`59.9 avg, 52.0 min, physics 60`; the warning colour
162
190
  under 50 on average). Older kernels show `needs kernel 0.4.2`. The same numbers ride on fleet heartbeats (the
163
191
  explorer's fleet table).
192
+ - **Devtools reach devs only (kernel 0.5.6+):** the kernel leaves `devtools` off the shared client tree of a framework
193
+ with `devtools/on-demand.ts`. `runtime/devtools-loader.ts` then clones the folder, minus the server-only modules, into
194
+ each dev's PlayerGui (join, each generation, dev status changes), and that client copies it into its own tree and
195
+ starts it. On older kernels the folder stays in the tree as before. A failure costs a dev the menu, with a warning, and
196
+ never the generation.
164
197
  - **Dev-only tools (framework 0.4.1, `devtools/access.ts`):** Claude, Logs > Upload, Dex edits on the server, Network
165
198
  packet blocking and the hot-swap sound work under dev rules (private, reserved and Studio servers on a dev branch)
166
199
  for every dev, and on a public server an owner switched to a dev branch (Server > Branch) for owners only. Everywhere
@@ -198,10 +231,19 @@ export class CoinService extends Module implements OnStart {
198
231
  - **Owners and devs:** two roles (no admins: an older kernel's or typetorch.json's "admin" counts as a dev). The Manage
199
232
  group (Players, Servers, Bans) is for owners only.
200
233
  - **Remote Claude** (a dev-only tool: dev rules, or owners on a public server an owner switched to a dev branch):
201
- while `typetorch remote-claude` (`@typetorch/dev-server`) runs on a dev's machine, allowlisted devs prompt Claude
202
- Code from the Claude tab. Each dev pairs once by pasting the pairing code printed by
234
+ while `typetorch remote-claude` (alias `typetorch dev`; it runs `@typetorch/dev-server`) runs on a dev's machine,
235
+ allowlisted devs prompt Claude Code from the Claude tab. The allowlist is `--users`; without it the dev-server uses
236
+ the owners in the game's `typetorch.json`. Each dev pairs once by pasting the pairing code printed by
203
237
  typetorch-dev-server; the game server keeps the session URL and the tokens in memory and never sends them to clients.
204
- No Roblox secret is needed.
238
+ No Roblox secret is needed. (The Studio plugin's Claude tab, with Apply / Deploy to dev / Discard, is a separate
239
+ client of the same dev-server, started by `typetorch serve --claude`; it doesn't go through the framework.)
240
+ - **Remote debug (framework 0.5.0, kernel 0.5.0, `devtools/remote-debug.ts`):** owners can read a fleet server from
241
+ the backend's explorer (`/servers/<JobId>`). The kernel polls the backend, checks the op, the rate and that the
242
+ caller is an owner, then hands the framework's ops to the handler installed through `kernel.onRemoteDebug`. Read-only,
243
+ with the dev menu's own readers and limits: `modules`, `state` (Modules > State queries, rate-limited per caller),
244
+ `assets` (the last hot-asset sync), `network` (per-remote counters, no packet capture), `dex.children` (8 nodes, 200
245
+ rows each at most) and `dex.props`. There are no edit ops. Older kernels don't have the hook, so nothing is
246
+ installed. It is part of the devtools server, so `devtools: false` leaves it out too.
205
247
 
206
248
  ## Runtime API: `TypeTorch`
207
249
 
@@ -358,10 +400,15 @@ rebuild(shop.get());
358
400
  - **AssetSync** is a server built-in. It runs on every generation start, before any module loads (top-level code
359
401
  included). For each manifest key:
360
402
  1. it keeps the live copy that already has the manifest's version;
361
- 2. else it adopts the place's own copy, if its `TypeTorchAssetHash` matches (or if the manifest's optional
362
- `placeVersion` is this server's place version);
363
- 3. else it runs `InsertService:LoadAssetVersion(ver)` (all loads in parallel), strips any scripts, places the copy
364
- at its path (creating missing Folders), tags it, then destroys the copy it replaces.
403
+ 2. else it adopts the place's own copy, if its `TypeTorchAssetHash` matches, or if the place holds exactly the
404
+ copies the manifest was exported from: the manifest's optional `placeVersion` is this server's place version, or
405
+ the place's backup build (`ServerStorage.TypeTorchBackup`, attributes `HotAssetsPlaceVersion` /
406
+ `HotAssetsSavedAs`) vouches that this place version is a save of that one which changed nothing outside the
407
+ kernel slots (the CLI's backup refresh);
408
+ 3. else it runs `InsertService:LoadAssetVersion(ver)` (all loads in parallel), strips any scripts, gives back the
409
+ CollectionService tags the place's copy has (uploaded versions arrive without them, so a game that finds its UI
410
+ by tag keeps working), places the copy at its path (creating missing Folders), tags it, then destroys the copy it
411
+ replaces.
365
412
 
366
413
  It holds the start for at most 8 s. Loads still running after that keep going, swap in when done and fire
367
414
  `changed`. A failed load keeps the old copy (on a new server, the place's copy) and shows under Server > Status >
@@ -373,7 +420,8 @@ rebuild(shop.get());
373
420
  `TypeTorchAssetPath` (stamped by AssetSync) is the live one. So a template cloned into PlayerGui never shows up in
374
421
  `get()`, and AssetSync never destroys it.
375
422
  - **Dev menu:** Modules > Assets (Server | Client) lists each key's source (kept, baked, loaded, failed), version,
376
- load time and last error.
423
+ load time and last error. With analytics on, each generation also sends one `tech` / `assets` row (the place match
424
+ and the counts per source; `src/analytics/SCHEMA.md`).
377
425
 
378
426
  ## Analytics: `AnalyticsEngine`
379
427
 
@@ -420,7 +468,9 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
420
468
  - **Experiments:** `experiment(...)` is deterministic per player and name (they keep their variant in every session),
421
469
  may yield briefly the first time (until the player's id loads), and stamps the variant on the player's later events.
422
470
  The settings turn one off (`active: false`: everyone gets the first variant), sets `weights` or forces a
423
- `variant`, live.
471
+ `variant`, live. `players` (framework 0.5.3+) forces a variant for single players, keyed by UserId or pid, over
472
+ everything else (QA, support); a client only ever gets its own entry. The CLI writes these with `typetorch
473
+ experiment weights | settle | stop | resume | clear | force | unforce`.
424
474
  - **First-ever session:** for new players (a share of them, `recordShare`), the client records character and camera
425
475
  about 10 times a second, every input (never while a TextBox or the chat has focus; text boxes only say which box was
426
476
  used), buttons pressed and hovered, screens, prompts and deaths, from the join until 60 s after the first input.
@@ -493,8 +543,11 @@ bun install
493
543
  bun run build # rbxtsc --type package -> out/
494
544
  ```
495
545
 
496
- - **Runtime of the transformer:** `src/reflection/` (`Reflect`, `Modding`, `t`) is a copy of
497
- `@typetorch/transformer`'s `runtime-kit/`; keep the two identical.
546
+ - **Runtime of the transformer:** `src/reflection/reflect.ts` and `modding.ts` are copies of
547
+ `@typetorch/transformer`'s `runtime-kit/` (the source of truth); keep them identical. The framework compiles itself
548
+ without the transformer (only `rbxts-transform-debug`): it declares macros, which expand in the game's compile.
549
+ - **Versions:** bump `src/version.ts` (`FRAMEWORK_VERSION`, shown in uploaded log headers) with package.json, and
550
+ `LATEST_KERNEL` in `src/devtools/health.ts` when the framework starts relying on a newer kernel.
498
551
  - **Tests (Lune, offline):** `scripts/test-*.luau`; each file's header has its command (Lune is pinned in the
499
552
  kernel's and the template's `rokit.toml`). `scripts/test-generations.luau` takes a game's built payload:
500
553
  `cd ../template && bun run payload && lune run ../framework/scripts/test-generations.luau build/payload.rbxm` boots
@@ -522,13 +575,37 @@ bun run build # rbxtsc --type package -> out/
522
575
  retries, a NetFail streak and its one log line a minute, a swap with a request in flight, shutdown; the recorder,
523
576
  screens, batching). `scripts/test-net-health.luau` checks the dev menu's Fleet API / Analytics status lines and
524
577
  Attention issues.
525
- - **Publishing:** `npm publish` runs `prepublishOnly` (clean + build). The package ships only `out/` (no
526
- `.tsbuildinfo`), `README.md` and `LICENSE`; check with `bun pm pack --dry-run`.
527
-
578
+ The others, each on the compiled `out/`: `test-hot-assets` (manifest parser, AssetSync decisions incl. the backup
579
+ vouch, live-copy rules), `test-messaging` (messaging and the server list), `test-settings` (`liveConfig` and the
580
+ settings lines), `test-detached` (`runDetached`), `test-dev-access` (the dev-only tools rule and every gate through
581
+ it), `test-remote-debug` (the remote debug ops and their limits), `test-net-inspect` (packet capture),
582
+ `test-explorer-core` (Dex), `test-toolbox-sanitize` (the Creator Store sanitizer) and `test-sha256` (the pairing-code
583
+ fingerprint). `node scripts/check-self-calls.mjs out` flags compiled functions that call themselves on their first
584
+ lines (after `bun run build`).
585
+ - **Package contents:** only `out/` (no `.tsbuildinfo`), `README.md` and `LICENSE`; check with `bun pm pack --dry-run`.
528
586
  - **Explorer class icons:** `assets/class-icons.png` (kept locally, not in the repo: it is Roblox's own texture) is
529
587
  the client's `content/textures/ClassImages.PNG` (a 2352x16 strip) repacked into a 32-column grid, because live
530
588
  clients downscale textures wider than 1024 px:
531
589
  `ffmpeg -i ClassImages.PNG -vf "untile=147x1,tile=32x5:color=0x00000000" class-icons.png`. It is uploaded as image
532
590
  `rbxassetid://97389585475400`; `scripts/gen-explorer-icons.ts` generates the index table.
533
591
 
592
+ ## Release
593
+
594
+ Publishing is tag-only, from CI (`.github/workflows/publish.yml`), with npm trusted publishing (OIDC, provenance):
595
+
596
+ ```sh
597
+ # first commit src/version.ts set to the new version (npm version needs a clean tree)
598
+ npm version 0.5.6 -m "release %s"
599
+ git push origin main --follow-tags
600
+ ```
601
+
602
+ The workflow runs on a `v*` tag, checks that the tag matches package.json and that the version isn't on npm yet, then
603
+ `npm publish --provenance` (which runs `prepublishOnly`: a clean `rbxtsc --type package` into `out/`) and creates the
604
+ GitHub release with generated notes. A prerelease version (`x.y.z-next.0`) goes to the `next` dist-tag and a GitHub
605
+ prerelease; anything else to `latest`. Trusted publishing is set on npmjs.com (@typetorch/framework > Settings >
606
+ Trusted publishing: this repo, workflow `publish.yml`, environment `npm`); an `NPM_TOKEN` repo secret is the fallback.
607
+ Don't run `npm publish` by hand. Games pick the new version up with `bun update @typetorch/framework` and their next
608
+ deploy, since the framework ships inside the payload.
609
+
610
+
534
611
  MIT licensed.
@@ -5,4 +5,9 @@ export interface DevtoolsClient {
5
5
  /** Call once the client generation is up (all modules started). */
6
6
  readonly started: () => void;
7
7
  }
8
+ /**
9
+ * Starts the dev menu for this client generation. Everything it creates or connects lives in `trove`, so a swap
10
+ * removes it; the open state, tab, sub-tab, window rectangle and options survive the swap through the kernel persist
11
+ * store.
12
+ */
8
13
  export declare function startDevtoolsClient(kernel: ClientKernel, dispatcher: ClientDispatcher, trove: Trove): DevtoolsClient;
@@ -22,6 +22,10 @@ local showCliNote = _build_actions.showCliNote
22
22
  local switchedText = _build_actions.switchedText
23
23
  local switchOffered = _build_actions.switchOffered
24
24
  local viewerIsOwner = _build_actions.viewerIsOwner
25
+ local _dev_gui = TS.import(script, script.Parent.Parent, "runtime", "dev-gui")
26
+ local DEV_GUI_GENERATION = _dev_gui.DEV_GUI_GENERATION
27
+ local DEV_GUI_NAME = _dev_gui.DEV_GUI_NAME
28
+ local sweepStaleMenus = _dev_gui.sweepStaleMenus
25
29
  local _ui = TS.import(script, script.Parent.Parent, "ui")
26
30
  local bump = _ui.bump
27
31
  local popIn = _ui.popIn
@@ -422,7 +426,7 @@ local function every(trove, interval, callback)
422
426
  while true do
423
427
  local ok, err = pcall(callback)
424
428
  if not ok then
425
- warn("[src/devtools/client.ts:367]", `[devtools] refresh failed: {err}`)
429
+ warn("[src/devtools/client.ts:368]", `[devtools] refresh failed: {err}`)
426
430
  end
427
431
  task.wait(interval)
428
432
  end
@@ -534,36 +538,7 @@ end
534
538
  * store.
535
539
 
536
540
  ]]
537
- local DEV_GUI_NAME = "TypeTorchDev"
538
- --* The client generation that built a dev menu gui (an attribute on the ScreenGui).
539
- local DEV_GUI_GENERATION = "TypeTorchGeneration"
540
- --[[
541
- *
542
- * Destroys dev menus an older generation left behind. Its trove should have removed them, but a generation whose stop
543
- * was cut short (a throwing cleanup, before framework 0.5.2) left its menu on screen, dead, over the new one.
544
- * Menus without the attribute come from a framework before 0.5.2: always older, since this generation hasn't built one.
545
-
546
- ]]
547
- local function sweepStaleMenus(playerGui, generation)
548
- for _, child in playerGui:GetChildren() do
549
- if child.Name ~= DEV_GUI_NAME or not child:IsA("ScreenGui") then
550
- continue
551
- end
552
- local built = child:GetAttribute(DEV_GUI_GENERATION)
553
- if type(built) == "number" and built >= generation then
554
- continue
555
- end
556
- pcall(function()
557
- return child:Destroy()
558
- end)
559
- end
560
- end
561
541
  local function startDevtoolsClient(kernel, dispatcher, trove)
562
- -- Before anything else, so a leaked menu goes away even for players who never get the dev menu.
563
- local startGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
564
- if startGui then
565
- sweepStaleMenus(startGui, kernel.generation)
566
- end
567
542
  local state = kernel:persist(PERSIST_KEY, function()
568
543
  return {
569
544
  open = false,
@@ -2547,7 +2522,7 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
2547
2522
  })
2548
2523
  end)
2549
2524
  if not ok then
2550
- warn("[src/devtools/client.ts:1768]", `[devtools] {key} tab failed: {err}`)
2525
+ warn("[src/devtools/client.ts:1748]", `[devtools] {key} tab failed: {err}`)
2551
2526
  page:text(`This tab failed: {err}`, COLORS.bad)
2552
2527
  end
2553
2528
  end
@@ -3154,7 +3129,7 @@ local function startDevtoolsClient(kernel, dispatcher, trove)
3154
3129
  return playReloadSound()
3155
3130
  end)
3156
3131
  if not ok then
3157
- warn("[src/devtools/client.ts:2189]", `[devtools] reload sound failed: {err}`)
3132
+ warn("[src/devtools/client.ts:2169]", `[devtools] reload sound failed: {err}`)
3158
3133
  end
3159
3134
  end))
3160
3135
  end,
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The kernel (0.5.6+) looks for this module by name: its presence says this framework's `devtools` folder can stay off
3
+ * the shared client tree, because runtime/devtools-loader.ts hands each dev a copy. Older frameworks don't have it, and
4
+ * keep the folder where their static imports need it.
5
+ */
6
+ export declare const DEVTOOLS_ON_DEMAND = true;
@@ -0,0 +1,12 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ --[[
3
+ *
4
+ * The kernel (0.5.6+) looks for this module by name: its presence says this framework's `devtools` folder can stay off
5
+ * the shared client tree, because runtime/devtools-loader.ts hands each dev a copy. Older frameworks don't have it, and
6
+ * keep the folder where their static imports need it.
7
+
8
+ ]]
9
+ local DEVTOOLS_ON_DEMAND = true
10
+ return {
11
+ DEVTOOLS_ON_DEMAND = DEVTOOLS_ON_DEMAND,
12
+ }
@@ -0,0 +1,8 @@
1
+ /** The dev menu's ScreenGui name, and the attribute with the generation that built it. */
2
+ export declare const DEV_GUI_NAME = "TypeTorchDev";
3
+ export declare const DEV_GUI_GENERATION = "TypeTorchGeneration";
4
+ /**
5
+ * Destroys menus an older generation left behind, so a leaked menu goes away. Lives outside devtools: a player who has
6
+ * since lost dev access gets no devtools, and still needs this.
7
+ */
8
+ export declare function sweepStaleMenus(playerGui: Instance, generation: number): void;
@@ -0,0 +1,29 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ --* The dev menu's ScreenGui name, and the attribute with the generation that built it.
3
+ local DEV_GUI_NAME = "TypeTorchDev"
4
+ local DEV_GUI_GENERATION = "TypeTorchGeneration"
5
+ --[[
6
+ *
7
+ * Destroys menus an older generation left behind, so a leaked menu goes away. Lives outside devtools: a player who has
8
+ * since lost dev access gets no devtools, and still needs this.
9
+
10
+ ]]
11
+ local function sweepStaleMenus(playerGui, generation)
12
+ for _, child in playerGui:GetChildren() do
13
+ if child.Name ~= DEV_GUI_NAME or not child:IsA("ScreenGui") then
14
+ continue
15
+ end
16
+ local built = child:GetAttribute(DEV_GUI_GENERATION)
17
+ if type(built) == "number" and built >= generation then
18
+ continue
19
+ end
20
+ pcall(function()
21
+ return child:Destroy()
22
+ end)
23
+ end
24
+ end
25
+ return {
26
+ sweepStaleMenus = sweepStaleMenus,
27
+ DEV_GUI_NAME = DEV_GUI_NAME,
28
+ DEV_GUI_GENERATION = DEV_GUI_GENERATION,
29
+ }
@@ -0,0 +1,6 @@
1
+ import type { Trove } from "@rbxts/trove";
2
+ import type { ClientKernel, ServerKernel } from "../kernel";
3
+ import type { ClientDispatcher, ServerDispatcher } from "../net/runtime";
4
+ export declare function loadDevtoolsServer(kernel: ServerKernel, dispatcher: ServerDispatcher, trove: Trove): void;
5
+ /** Starts the client's devtools now or when its copy arrives. Returns the "every module has started" hook. */
6
+ export declare function loadDevtoolsClient(kernel: ClientKernel, dispatcher: ClientDispatcher, trove: Trove): () => void;
@@ -0,0 +1,227 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local Players = TS.import(script, TS.getModule(script, "@rbxts", "services")).Players
4
+ local TypeTorch = TS.import(script, script.Parent.Parent, "typetorch").TypeTorch
5
+ local sweepStaleMenus = TS.import(script, script.Parent, "dev-gui").sweepStaleMenus
6
+ --[[
7
+ *
8
+ * Loads the devtools, and only for those who get them. Kernel 0.5.6+ leaves `devtools` off the shared client tree of a
9
+ * framework that has devtools/on-demand.ts, and sets ON_DEMAND_ATTRIBUTE on the generation's trees. Then the server
10
+ * clones the folder into each dev's PlayerGui (which replicates to its owner only), and that dev's client copies it
11
+ * back to its place in its own tree, so the compiled relative imports and the module cache work as usual. On an older
12
+ * kernel the attribute is missing and the folder is in the tree as before.
13
+ *
14
+ * The devtools are never imported statically outside this file's dynamic imports: TS.import waits on a missing child
15
+ * forever. A failure here costs a dev the menu, with a warning, and never the generation.
16
+
17
+ ]]
18
+ local ON_DEMAND_ATTRIBUTE = "DevtoolsOnDemand"
19
+ local COPY_NAME = "TypeTorchDevtools"
20
+ local COPY_GENERATION = "Generation"
21
+ local COPY_INSTANCES = "Instances"
22
+ --* Devtools modules only servers require: left out of a dev's copy. scripts/check-devtools-split.mjs checks the list.
23
+ local SERVER_ONLY = { "access", "admin-server", "artifact-notes", "claude", "explorer-server", "network-server", "remote-debug", "server", "sha256", "toolbox-sanitize", "toolbox-server" }
24
+ --* Seconds a dev's client waits for its copy to finish replicating.
25
+ local REPLICATION_TIMEOUT = 60
26
+ local frameworkRoot = function()
27
+ return script.Parent.Parent
28
+ end
29
+ --* This generation's tree: the same name on the server and the client (kernel: `<artifact>#<n>`).
30
+ local generationTree = function()
31
+ local _result = script:FindFirstAncestor("include")
32
+ if _result ~= nil then
33
+ _result = _result.Parent
34
+ end
35
+ return _result
36
+ end
37
+ local function shareWithDevs(devtools, generation, trove)
38
+ local copies = {}
39
+ local give = function(player)
40
+ local ok, problem = pcall(function()
41
+ local _player = player
42
+ local _condition = copies[_player] ~= nil
43
+ if not _condition then
44
+ _condition = not TypeTorch:isDev(player)
45
+ end
46
+ if _condition then
47
+ return nil
48
+ end
49
+ local playerGui = player:WaitForChild("PlayerGui", 30)
50
+ if not playerGui then
51
+ error("no PlayerGui after 30 s")
52
+ end
53
+ local _player_1 = player
54
+ local _condition_1 = copies[_player_1] ~= nil
55
+ if not _condition_1 then
56
+ _condition_1 = player.Parent == nil
57
+ end
58
+ if _condition_1 then
59
+ return nil
60
+ end
61
+ local copy = devtools:Clone()
62
+ for _, name in SERVER_ONLY do
63
+ local _result = copy:FindFirstChild(name)
64
+ if _result ~= nil then
65
+ _result:Destroy()
66
+ end
67
+ end
68
+ copy.Name = COPY_NAME
69
+ copy:SetAttribute(COPY_GENERATION, generation)
70
+ copy:SetAttribute(COPY_INSTANCES, #copy:GetDescendants())
71
+ local _player_2 = player
72
+ copies[_player_2] = copy
73
+ trove:add(copy)
74
+ copy.Parent = playerGui
75
+ end)
76
+ if not ok then
77
+ warn(`[TypeTorch] no devtools for {player.Name}: {problem}`)
78
+ end
79
+ end
80
+ trove:connect(Players.PlayerAdded, function(player)
81
+ return give(player)
82
+ end)
83
+ trove:connect(Players.PlayerRemoving, function(player)
84
+ local _player = player
85
+ -- ▼ Map.delete ▼
86
+ local _valueExisted = copies[_player] ~= nil
87
+ copies[_player] = nil
88
+ -- ▲ Map.delete ▲
89
+ return _valueExisted
90
+ end)
91
+ trove:add(TypeTorch:onPlayerDevChanged(function(player, info)
92
+ if info.dev then
93
+ give(player)
94
+ return nil
95
+ end
96
+ local _player = player
97
+ local copy = copies[_player]
98
+ if copy then
99
+ local _player_1 = player
100
+ copies[_player_1] = nil
101
+ trove:remove(copy)
102
+ end
103
+ end))
104
+ for _, player in Players:GetPlayers() do
105
+ trove:add(task.spawn(give, player))
106
+ end
107
+ end
108
+ local function loadDevtoolsServer(kernel, dispatcher, trove)
109
+ local ok, problem = pcall(function()
110
+ local _binding = TS.Promise.new(function(resolve)
111
+ resolve(TS.import(script, script.Parent.Parent, "devtools", "server"))
112
+ end):expect()
113
+ local startDevtoolsServer = _binding.startDevtoolsServer
114
+ startDevtoolsServer(kernel, dispatcher, trove:extend())
115
+ local tree = generationTree()
116
+ local devtools = frameworkRoot():FindFirstChild("devtools")
117
+ local _result = tree
118
+ if _result ~= nil then
119
+ _result = _result:GetAttribute(ON_DEMAND_ATTRIBUTE)
120
+ end
121
+ local _condition = _result == true
122
+ if _condition then
123
+ _condition = devtools
124
+ end
125
+ if _condition then
126
+ shareWithDevs(devtools, tree.Name, trove)
127
+ end
128
+ end)
129
+ if not ok then
130
+ warn(`[TypeTorch] devtools failed to start, the game goes on without them: {problem}`)
131
+ end
132
+ end
133
+ --* Starts the client's devtools now or when its copy arrives. Returns the "every module has started" hook.
134
+ local function loadDevtoolsClient(kernel, dispatcher, trove)
135
+ local playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
136
+ if playerGui then
137
+ sweepStaleMenus(playerGui, kernel.generation)
138
+ end
139
+ local alive = true
140
+ trove:add(function()
141
+ alive = false
142
+ end)
143
+ local onStarted
144
+ local modulesStarted = false
145
+ local runStarted = function()
146
+ if not onStarted or not modulesStarted then
147
+ return nil
148
+ end
149
+ local ok, problem = pcall(onStarted)
150
+ if not ok then
151
+ warn(`[TypeTorch] devtools: {problem}`)
152
+ end
153
+ end
154
+ local begin = function()
155
+ local ok, result = pcall(function()
156
+ local _binding = TS.Promise.new(function(resolve)
157
+ resolve(TS.import(script, script.Parent.Parent, "devtools", "client"))
158
+ end):expect()
159
+ local startDevtoolsClient = _binding.startDevtoolsClient
160
+ return startDevtoolsClient(kernel, dispatcher, trove:extend())
161
+ end)
162
+ if not ok then
163
+ warn(`[TypeTorch] devtools failed to start, the game goes on without them: {result}`)
164
+ return nil
165
+ end
166
+ onStarted = result.started
167
+ runStarted()
168
+ end
169
+ local tree = generationTree()
170
+ local _result = tree
171
+ if _result ~= nil then
172
+ _result = _result:GetAttribute(ON_DEMAND_ATTRIBUTE)
173
+ end
174
+ local _condition = _result ~= true
175
+ if not _condition then
176
+ _condition = frameworkRoot():FindFirstChild("devtools")
177
+ end
178
+ if _condition then
179
+ begin()
180
+ else
181
+ local taken = false
182
+ local take = function(copy)
183
+ if taken or copy.Name ~= COPY_NAME or copy:GetAttribute(COPY_GENERATION) ~= tree.Name then
184
+ return nil
185
+ end
186
+ taken = true
187
+ local expected = copy:GetAttribute(COPY_INSTANCES)
188
+ local deadline = os.clock() + REPLICATION_TIMEOUT
189
+ while alive and type(expected) == "number" and #copy:GetDescendants() < expected do
190
+ if os.clock() > deadline then
191
+ warn("[TypeTorch] the devtools copy didn't finish replicating: no dev menu until the next generation")
192
+ return nil
193
+ end
194
+ task.wait(0.1)
195
+ end
196
+ if not alive then
197
+ return nil
198
+ end
199
+ -- A clone, so a retry of this generation on a fresh tree (kernel client) still finds the original.
200
+ local ok, problem = pcall(function()
201
+ local placed = copy:Clone()
202
+ placed.Name = "devtools"
203
+ placed.Parent = frameworkRoot()
204
+ end)
205
+ if ok then
206
+ begin()
207
+ else
208
+ warn(`[TypeTorch] devtools copy failed: {problem}`)
209
+ end
210
+ end
211
+ trove:add(task.spawn(function()
212
+ local gui = Players.LocalPlayer:WaitForChild("PlayerGui")
213
+ trove:connect(gui.ChildAdded, take)
214
+ for _, child in gui:GetChildren() do
215
+ take(child)
216
+ end
217
+ end))
218
+ end
219
+ return function()
220
+ modulesStarted = true
221
+ runStarted()
222
+ end
223
+ end
224
+ return {
225
+ loadDevtoolsServer = loadDevtoolsServer,
226
+ loadDevtoolsClient = loadDevtoolsClient,
227
+ }
@@ -6,8 +6,6 @@ local RunService = _services.RunService
6
6
  local Trove = TS.import(script, TS.getModule(script, "@rbxts", "trove").out).Trove
7
7
  local bindHotAssets = TS.import(script, script.Parent.Parent, "assets", "hot-asset").bindHotAssets
8
8
  local syncHotAssets = TS.import(script, script.Parent.Parent, "assets", "sync").syncHotAssets
9
- local startDevtoolsClient = TS.import(script, script.Parent.Parent, "devtools", "client").startDevtoolsClient
10
- local startDevtoolsServer = TS.import(script, script.Parent.Parent, "devtools", "server").startDevtoolsServer
11
9
  local _runtime = TS.import(script, script.Parent.Parent, "net", "runtime")
12
10
  local ClientDispatcher = _runtime.ClientDispatcher
13
11
  local ServerDispatcher = _runtime.ServerDispatcher
@@ -22,6 +20,9 @@ local startedTypeTorch = _typetorch.startedTypeTorch
22
20
  local swapOutTypeTorch = _typetorch.swapOutTypeTorch
23
21
  local TypeTorch = _typetorch.TypeTorch
24
22
  local unbindTypeTorch = _typetorch.unbindTypeTorch
23
+ local _devtools_loader = TS.import(script, script.Parent, "devtools-loader")
24
+ local loadDevtoolsClient = _devtools_loader.loadDevtoolsClient
25
+ local loadDevtoolsServer = _devtools_loader.loadDevtoolsServer
25
26
  local _dependency = TS.import(script, script.Parent, "dependency")
26
27
  local addModuleInstance = _dependency.addModuleInstance
27
28
  local createLazy = _dependency.createLazy
@@ -251,7 +252,7 @@ local function start(realm, kernel, options)
251
252
  return dispatcher:forget(player)
252
253
  end)
253
254
  if options.devtools ~= false then
254
- startDevtoolsServer(serverKernel, dispatcher, root:extend())
255
+ loadDevtoolsServer(serverKernel, dispatcher, root:extend())
255
256
  end
256
257
  stopNetwork = function()
257
258
  return setServerDispatcher(nil)
@@ -271,7 +272,7 @@ local function start(realm, kernel, options)
271
272
  -- The server queues what it sends this player until this (0.3.0).
272
273
  dispatcher:hello()
273
274
  if options.devtools ~= false then
274
- onStarted = startDevtoolsClient(clientKernel, dispatcher, root:extend()).started
275
+ onStarted = loadDevtoolsClient(clientKernel, dispatcher, root:extend())
275
276
  end
276
277
  stopNetwork = function()
277
278
  dispatcher:stop()
@@ -308,7 +309,7 @@ local function start(realm, kernel, options)
308
309
  return (running.instance):onStop()
309
310
  end)
310
311
  if not ok then
311
- warn("[src/runtime/start.ts:206]", `{running.name}.onStop threw: {err}`)
312
+ warn("[src/runtime/start.ts:205]", `{running.name}.onStop threw: {err}`)
312
313
  end
313
314
  end
314
315
  -- Object by object, each pcalled: a cleanup that throws (e.g. trove.remove inside a cleanup) can't skip the
@@ -461,7 +462,7 @@ local function start(realm, kernel, options)
461
462
  _result()
462
463
  end
463
464
  startedTypeTorch()
464
- print("[src/runtime/start.ts:305]", `TypeTorch {realm} started {#runningModules} modules in {math.floor((os.clock() - startedAt) * 1000)} ms (artifact {kernel.artifact.id}, generation {kernel.generation})`)
465
+ print("[src/runtime/start.ts:304]", `TypeTorch {realm} started {#runningModules} modules in {math.floor((os.clock() - startedAt) * 1000)} ms (artifact {kernel.artifact.id}, generation {kernel.generation})`)
465
466
  return function(info)
466
467
  -- TypeTorch.onSwapOut first, while every module still runs (so they can save into persist).
467
468
  stopStep("onSwapOut", function()
package/out/servers.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { Trove } from "@rbxts/trove";
2
- import { RollCall } from "./devtools/roll-call";
2
+ import { RollCall } from "./roll-call";
3
3
  import type { Channel, ServerKernel, ServerType } from "./kernel";
4
4
  /**
5
5
  * The universe's live servers for game code: `TypeTorch.servers()` and `TypeTorch.setServerInfo()` (server only;
6
- * plans/19 item 19). Built on the framework's roll call (devtools/roll-call.ts; the same one Manage > Servers uses): no
6
+ * plans/19 item 19). Built on the framework's roll call (roll-call.ts; the same one Manage > Servers uses): no
7
7
  * MemoryStore, no heartbeat writes.
8
8
  *
9
9
  * - One roll call per generation, owned here and shared with the dev menu. Every server answers asks with its row: the
package/out/servers.luau CHANGED
@@ -4,7 +4,7 @@ local _services = TS.import(script, TS.getModule(script, "@rbxts", "services"))
4
4
  local HttpService = _services.HttpService
5
5
  local MessagingService = _services.MessagingService
6
6
  local Players = _services.Players
7
- local _roll_call = TS.import(script, script.Parent, "devtools", "roll-call")
7
+ local _roll_call = TS.import(script, script.Parent, "roll-call")
8
8
  local isJobId = _roll_call.isJobId
9
9
  local ROLL_CALL_TOPIC = _roll_call.ROLL_CALL_TOPIC
10
10
  local RollCall = _roll_call.RollCall
@@ -16,7 +16,7 @@ local Relay = TS.import(script, script.Parent, "runtime", "relay").Relay
16
16
  --[[
17
17
  *
18
18
  * The universe's live servers for game code: `TypeTorch.servers()` and `TypeTorch.setServerInfo()` (server only;
19
- * plans/19 item 19). Built on the framework's roll call (devtools/roll-call.ts; the same one Manage > Servers uses): no
19
+ * plans/19 item 19). Built on the framework's roll call (roll-call.ts; the same one Manage > Servers uses): no
20
20
  * MemoryStore, no heartbeat writes.
21
21
  *
22
22
  * - One roll call per generation, owned here and shared with the dev menu. Every server answers asks with its row: the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typetorch/framework",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "TypeTorch framework for roblox-ts: hot-swappable modules (dependency injection, lifecycle, troves), guarded networking, hot assets and the in-game dev menu. Ships inside every TypeTorch artifact.",
5
5
  "license": "MIT",
6
6
  "author": "TypeTorch",
File without changes
File without changes