@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 +92 -15
- package/out/devtools/client.d.ts +5 -0
- package/out/devtools/client.luau +7 -32
- package/out/devtools/on-demand.d.ts +6 -0
- package/out/devtools/on-demand.luau +12 -0
- package/out/runtime/dev-gui.d.ts +8 -0
- package/out/runtime/dev-gui.luau +29 -0
- package/out/runtime/devtools-loader.d.ts +6 -0
- package/out/runtime/devtools-loader.luau +227 -0
- package/out/runtime/start.luau +7 -6
- package/out/servers.d.ts +2 -2
- package/out/servers.luau +2 -2
- package/package.json +1 -1
- /package/out/{devtools/roll-call.d.ts → roll-call.d.ts} +0 -0
- /package/out/{devtools/roll-call.luau → roll-call.luau} +0 -0
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,
|
|
202
|
-
Code from the Claude tab.
|
|
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
|
|
362
|
-
`placeVersion` is this server's place version
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
497
|
-
`@typetorch/transformer`'s `runtime-kit
|
|
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
|
-
|
|
526
|
-
|
|
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.
|
package/out/devtools/client.d.ts
CHANGED
|
@@ -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;
|
package/out/devtools/client.luau
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
+
}
|
package/out/runtime/start.luau
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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:
|
|
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:
|
|
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 "./
|
|
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 (
|
|
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, "
|
|
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 (
|
|
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.
|
|
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
|