@flamework-experimental/core 2.0.0-alpha.2 → 2.0.0-alpha.4
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 +41 -34
- package/docs/README.md +63 -0
- package/docs/guide/01-getting-started.md +334 -0
- package/docs/guide/02-modules.md +254 -0
- package/docs/guide/03-providers.md +423 -0
- package/docs/guide/04-lifecycle-events.md +423 -0
- package/docs/guide/05-components.md +793 -0
- package/docs/guide/06-networking.md +614 -0
- package/docs/guide/07-macros.md +332 -0
- package/docs/guide/08-plugins.md +203 -0
- package/docs/guide/09-project-structure.md +392 -0
- package/docs/guide/10-migrating-from-v1.md +573 -0
- package/docs/guide/11-scopes.md +165 -0
- package/docs/guide/12-testing.md +342 -0
- package/flamework.build +1 -1
- package/out/dependency.d.ts +4 -0
- package/out/dependency.luau +4 -0
- package/out/index.d.ts +5 -2
- package/out/init.luau +11 -2
- package/out/lifecycle/lifecyclePlugin.d.ts +13 -1
- package/out/lifecycle/lifecyclePlugin.luau +67 -7
- package/out/module/module.luau +126 -24
- package/out/module/moduleBuilder.d.ts +12 -3
- package/out/module/moduleBuilder.luau +23 -1
- package/out/module/moduleDefinition.d.ts +6 -0
- package/out/module/providerRegistration.d.ts +8 -0
- package/out/module/providerRegistration.luau +29 -0
- package/out/plugin/pluginDefinition.d.ts +7 -4
- package/out/provider.d.ts +19 -0
- package/out/provider.luau +8 -0
- package/out/reflect.luau +6 -0
- package/out/utility/explainUnresolved.d.ts +9 -0
- package/out/utility/explainUnresolved.luau +43 -0
- package/out/utility/getClassesInPath.d.ts +37 -3
- package/out/utility/getClassesInPath.luau +154 -33
- package/out/utility/globs.d.ts +2 -2
- package/out/utility/globs.luau +3 -3
- package/out/utility/leftOut.d.ts +35 -0
- package/out/utility/leftOut.luau +171 -0
- package/out/utility/moduleClasses.d.ts +9 -0
- package/out/utility/moduleClasses.luau +64 -0
- package/out/utility/pathRoot.d.ts +20 -1
- package/out/utility/pathRoot.luau +80 -4
- package/package.json +14 -7
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# 9. Project structure
|
|
2
|
+
|
|
3
|
+
Nothing here is enforced. Flamework only needs the folders you register from to be mapped in your
|
|
4
|
+
Rojo project. A class is found in the file that defines it, exported or not. This page shows what
|
|
5
|
+
tends to work.
|
|
6
|
+
|
|
7
|
+
## A layout that scales
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
server/
|
|
12
|
+
runtime.server.ts entry point: builds and ignites the server module
|
|
13
|
+
services/ @Provider classes, registered by path
|
|
14
|
+
economy.ts
|
|
15
|
+
matchmaking.ts
|
|
16
|
+
components/ server-side components
|
|
17
|
+
client/
|
|
18
|
+
runtime.client.ts entry point: builds and ignites the client module
|
|
19
|
+
controllers/ @Provider classes, registered by path
|
|
20
|
+
components/ client-side components
|
|
21
|
+
shared/
|
|
22
|
+
network.ts Networking.createEvent / createFunction
|
|
23
|
+
components/ components that exist on both realms
|
|
24
|
+
plugins/ plugins both entry points include
|
|
25
|
+
types/
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The entry points are the only files that know how everything fits together:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// src/server/runtime.server.ts
|
|
32
|
+
import { ComponentPlugin } from "@flamework-experimental/components";
|
|
33
|
+
import { Flamework } from "@flamework-experimental/core";
|
|
34
|
+
|
|
35
|
+
Flamework.createModule()
|
|
36
|
+
.includePlugin(ComponentPlugin.fromPath("src/shared/components"))
|
|
37
|
+
.includePlugin(ComponentPlugin.fromPath("src/server/components"))
|
|
38
|
+
.registerProviders("src/server/services")
|
|
39
|
+
.ignite();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// src/client/runtime.client.ts
|
|
44
|
+
Flamework.createModule()
|
|
45
|
+
.includePlugin(ComponentPlugin.fromPath("src/shared/components"))
|
|
46
|
+
.includePlugin(ComponentPlugin.fromPath("src/client/components"))
|
|
47
|
+
.registerProviders("src/client/controllers")
|
|
48
|
+
.ignite();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`services` and `controllers` are only folder names: v2 has no `@Service`/`@Controller` distinction.
|
|
52
|
+
Keeping the folders separate is what keeps server code off the client.
|
|
53
|
+
|
|
54
|
+
Register only the folders you have. A registered folder that does not exist is not in the place,
|
|
55
|
+
and the call waits for it at runtime, warning after five seconds with its own name. One that holds
|
|
56
|
+
no module yet (a new game's `shared/components`, say) is copied into the place empty and registers
|
|
57
|
+
nothing, but git keeps no empty folder, so a fresh clone has no such folder and waits the same way.
|
|
58
|
+
The build warns about both where the path is written
|
|
59
|
+
([Getting started › Rojo](01-getting-started.md#rojo)). Add the `fromPath` line with the first
|
|
60
|
+
component, or leave a module in the folder. `plugins/` and `types/` are not registered, so they need
|
|
61
|
+
nothing.
|
|
62
|
+
|
|
63
|
+
Each entry point includes two `ComponentPlugin`s. They share the module's one `Components`, so a
|
|
64
|
+
server component can link to a shared one ([Components](05-components.md)).
|
|
65
|
+
|
|
66
|
+
## One module per realm
|
|
67
|
+
|
|
68
|
+
**One module per realm** is right for every game. Everything is in one container, anything can
|
|
69
|
+
inject anything, and you don't have to think about it again. What varies is what goes *into* the
|
|
70
|
+
module:
|
|
71
|
+
|
|
72
|
+
| Situation | Shape |
|
|
73
|
+
|---|---|
|
|
74
|
+
| Shared code both realms need | A plugin in `shared/plugins`, included by both entry points. |
|
|
75
|
+
| A library you publish | A plugin; its setup registers the library's providers. |
|
|
76
|
+
| Tests | Build the definition once, ignite per case, extinguish after. |
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// src/shared/plugins/core.ts
|
|
80
|
+
export const CorePlugin = Flamework.createPlugin("Core", (target) => {
|
|
81
|
+
target.registerProviders("src/shared/services");
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// both entry points
|
|
87
|
+
.includePlugin(CorePlugin)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Each realm ignites its own module. That is correct: the server and the client are different
|
|
91
|
+
processes.
|
|
92
|
+
|
|
93
|
+
## Where things go
|
|
94
|
+
|
|
95
|
+
| Thing | Where | Why |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `createEvent` / `createFunction` | `shared/` | Both realms import the same object; its identity is generated from the callsite. |
|
|
98
|
+
| Components used on both realms | `shared/components` | Registered by both entry points. |
|
|
99
|
+
| Components for one realm | `<realm>/components` | Same tag, different class per realm, is a normal pattern. |
|
|
100
|
+
| Interfaces for plugin dispatch | `shared/` | Both the plugin and the implementers need them. |
|
|
101
|
+
| Plugins | `shared/plugins` | Not `shared/services`: path registration requires everything under the folder, and a plugin built with `ComponentPlugin.fromPath` registers its folder as a side effect. |
|
|
102
|
+
|
|
103
|
+
The last row matters because `registerProviders("src/shared/services")` requires **every**
|
|
104
|
+
ModuleScript under that folder. Whatever a file there does when it loads happens during
|
|
105
|
+
registration.
|
|
106
|
+
|
|
107
|
+
## Configuration
|
|
108
|
+
|
|
109
|
+
Every Flamework package reads one file, `flamework.config.json`, next to `tsconfig.json`. Each
|
|
110
|
+
package has its own section in the file. The only entry `tsconfig.json` needs is `transform`:
|
|
111
|
+
|
|
112
|
+
```jsonc
|
|
113
|
+
// flamework.config.json
|
|
114
|
+
{
|
|
115
|
+
"$schema": "./node_modules/@flamework-experimental/transformer/flamework.config.schema.json",
|
|
116
|
+
"transformer": {
|
|
117
|
+
"obfuscation": false,
|
|
118
|
+
"idGenerationMode": "short",
|
|
119
|
+
"optimizations": { "guardGenerationDedupLimit": 5 },
|
|
120
|
+
"plugins": []
|
|
121
|
+
},
|
|
122
|
+
"core": { "profiling": true },
|
|
123
|
+
"networking": { "serialization": true },
|
|
124
|
+
"components": { "warningTimeout": 5, "attributeWarningTimeout": 5, "streamingMode": "Contextual", "watchRenames": false },
|
|
125
|
+
"scopes": { "active": "${FLAMEWORK_SCOPES:-}" }
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
| Section | Key | Effect |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `transformer` | `hashPrefix` | Prefix for generated ids. Defaults to the package name in a package, and to none in a game. A game needs none: every package id starts with its package's prefix and a colon (`$c:components@Components`, or the package's name for a package of someone else's), which none of a game's own ids starts with, so they cannot collide. A game may still set one, to mark its ids; the ids change with it, so change it with a plain build. It cannot start with `$`, which Flamework's own packages use. |
|
|
132
|
+
| | `obfuscation` | Obfuscates identifiers: random remote names, shuffled metadata, short ids, all different on every plain build. Game projects only; see [obfuscation](#obfuscation). |
|
|
133
|
+
| | `idGenerationMode` | `"full"`, `"short"`, `"tiny"` or `"obfuscated"`. Defaults to `"obfuscated"` with obfuscation on, else `"full"`. Only shorten in a game. |
|
|
134
|
+
| | `plugins` | Transformer plugins; see [transformer plugins](https://github.com/Velover/ExperimentalFlameworkV2/blob/HEAD/docs/reference/transformer-plugins.md). |
|
|
135
|
+
| | `salt`, `noSemanticDiagnostics`, `optimizations` | Hash salt, skipping semantic diagnostics, [guard deduplication](#guard-deduplication). |
|
|
136
|
+
| `core` | `profiling` | Default for `LifecyclePlugin` profiling; `createLifecyclePlugin({ profiling })` overrides it per module. |
|
|
137
|
+
| `networking` | `serialization` | Serializes every event and function payload into a buffer with code generated at compile time; see [Networking](06-networking.md#serialization). |
|
|
138
|
+
| `components` | `warningTimeout`, `attributeWarningTimeout`, `streamingMode`, `watchRenames` | Defaults for components that do not set their own. |
|
|
139
|
+
| `scopes` | `active` | The scopes this build is compiled with; see [Scopes](11-scopes.md). |
|
|
140
|
+
| `testing` | `activeIn`, `inactiveIn`, `enabled`, `autoRun`, `timeout`, `entry` | Tests in the place: the scopes under which the plugin attaches (`["testing"]` by default) and an override, whether tests run at start, the timeout per test, and `entry`, the ModuleScript a cloud task ignites the game from (a cloud task runs none of the place's Scripts; Studio needs no entry). See [Testing in the place](12-testing.md). |
|
|
141
|
+
| `cloud` | `testingUniverseId`, `testingPlaceId`, `apiKey`, `originalPlace` | The testing place that `flamework-test` publishes to and runs in for a cloud run, and a copy of the original place to lay the build over (Studio runs use it too). Read by that CLI only, never compiled in. See [Running the tests](https://github.com/Velover/ExperimentalFlameworkV2/blob/HEAD/docs/testing/place.md). |
|
|
142
|
+
|
|
143
|
+
The transformer looks for the file in the tsconfig's directory, then in each parent folder up to the
|
|
144
|
+
package root. So a repository with several places can share one file at the root, and a place can
|
|
145
|
+
still have its own file, which is used instead.
|
|
146
|
+
|
|
147
|
+
Comments and trailing commas are allowed. Unknown keys are rejected, with their name in the error.
|
|
148
|
+
|
|
149
|
+
The `$schema` line gives your editor every option, with its description and its default, and checks
|
|
150
|
+
what you write. You don't have to add it yourself. When a game builds, the transformer adds the line
|
|
151
|
+
if the file has none. If the game has no `flamework.config.json` yet, it creates one next to
|
|
152
|
+
`tsconfig.json` with just that line, but only when `tsconfig.json` is at the package root. A place
|
|
153
|
+
in a subfolder gets no file of its own, because that file would hide a shared one you add at the
|
|
154
|
+
root later. The transformer does this once, when `rbxtsc` starts, not on a watcher's rebuilds. It
|
|
155
|
+
never changes a `$schema` that is already there, and never touches a package's config file.
|
|
156
|
+
|
|
157
|
+
The tsconfig entry takes only `transform` and, if you need it, `configFile`. To use a different
|
|
158
|
+
name or location for the file, set `"configFile": "config/flamework.json"` on the entry. Any other
|
|
159
|
+
key on the entry fails the build, and the error names the key. For a transformer option it also
|
|
160
|
+
names the file to move it to. For any other key it says to remove it. (Keys that the plugin loader
|
|
161
|
+
reads itself, such as `import`, are allowed.)
|
|
162
|
+
|
|
163
|
+
For a game project, the transformer copies the runtime sections (`core`, `networking`,
|
|
164
|
+
`components`, `scopes` and `testing`) into
|
|
165
|
+
`include/flamework/config.json`, and the packages read them through `getRuntimeConfig()` from
|
|
166
|
+
`@flamework-experimental/core`. `cloud` is not one of them and never reaches the place. A package (a
|
|
167
|
+
project with a scoped name) gets no such file: its defaults come from the game that uses it.
|
|
168
|
+
|
|
169
|
+
**Do not set `idGenerationMode` or `obfuscation` in a published package.** Ids have to be stable,
|
|
170
|
+
and must not collide, in every game that uses the package.
|
|
171
|
+
|
|
172
|
+
### Obfuscation
|
|
173
|
+
|
|
174
|
+
With `obfuscation` on, every generated name changes with every plain build: a fresh run of `rbxtsc`,
|
|
175
|
+
not a watcher's rebuild or an incremental build (both explained below). That includes class ids,
|
|
176
|
+
hashed strings, and the callsite uuids that name every remote. A name mapped in one release is
|
|
177
|
+
useless against the next. That is the point: a cheat cannot carry a map of your remotes from one
|
|
178
|
+
version to another.
|
|
179
|
+
|
|
180
|
+
Ids declared in a package are the exception. Obfuscation hashes your game's own ids: its classes and
|
|
181
|
+
its own interfaces. A package's ids keep the names the package was published with. That covers
|
|
182
|
+
Flamework's own (`OnStart`, `OnInit`, `OnTick`, `Components` and the rest) and those of any other
|
|
183
|
+
package built with Flamework. The reason: a package is built before your game, and its compiled code
|
|
184
|
+
compares those exact strings at runtime. For example, core's lifecycle plugin looks for
|
|
185
|
+
`$:lifecycle/lifecycleInterfaces@OnStart`. Your game's build cannot rename them. Renaming them at
|
|
186
|
+
runtime would ship the list of new names to the client, next to the package's own readable code, so
|
|
187
|
+
it would hide nothing. Flamework v1 worked the same way. A readable package id only reveals which
|
|
188
|
+
Flamework events and package types a class uses.
|
|
189
|
+
|
|
190
|
+
The names come from a hash salt and a build seed, both kept in `flamework.build`. A plain `rbxtsc`
|
|
191
|
+
recreates that file, and with it the salt and the seed. A running `rbxtsc -w` keeps reading the
|
|
192
|
+
file it started with. So the names stay the same while the watcher runs, and every rebuild agrees
|
|
193
|
+
with the files it did not recompile.
|
|
194
|
+
|
|
195
|
+
Two things keep names the same across builds, and the transformer warns about both:
|
|
196
|
+
|
|
197
|
+
- **`transformer.salt`**, which fixes the hash the class ids come from. Leave it unset with
|
|
198
|
+
obfuscation on.
|
|
199
|
+
- **An incremental build** (`incremental`, with a `tsBuildInfoFile` or TypeScript's default,
|
|
200
|
+
which is `tsconfig.tsbuildinfo` beside `tsconfig.json` when `rootDir` is `src`). It reuses the
|
|
201
|
+
previous `flamework.build`, so that the files it does not recompile still match. Delete the
|
|
202
|
+
tsbuildinfo before a release build; the warning names it.
|
|
203
|
+
|
|
204
|
+
Without obfuscation the names are stable across builds, which is what you want while debugging.
|
|
205
|
+
|
|
206
|
+
### Values from the environment
|
|
207
|
+
|
|
208
|
+
Any string in the file can use environment variables:
|
|
209
|
+
|
|
210
|
+
- `${NAME}` is the variable's value.
|
|
211
|
+
- `${NAME:-fallback}` uses the fallback when the variable is not set.
|
|
212
|
+
- `$$` writes a literal dollar sign.
|
|
213
|
+
|
|
214
|
+
The variables come from `.env` and `.env.local` next to `flamework.config.json`, and from the
|
|
215
|
+
process environment. A shell variable wins over `.env.local`, which wins over `.env`. Commit `.env`
|
|
216
|
+
with the defaults, and git-ignore `.env.local` for personal overrides. `.env.local` is read by every
|
|
217
|
+
build on your machine, the ones you ship included, so a scope that adds test or debug code does not
|
|
218
|
+
belong there when you build a place to publish; give it to the one build that needs it instead
|
|
219
|
+
([Testing in the place › Setting up](12-testing.md#setting-up)).
|
|
220
|
+
|
|
221
|
+
A variable is always a string. Where the file expects a boolean, a number or a list, the string is
|
|
222
|
+
converted: `"obfuscation": "${OBFUSCATE:-false}"` becomes a boolean, and
|
|
223
|
+
`"active": "${FLAMEWORK_SCOPES:-}"` is split on commas into a list (an empty value gives an empty
|
|
224
|
+
list). A variable that is not set and has no fallback fails the build, with an error naming the
|
|
225
|
+
variable and the key that used it.
|
|
226
|
+
|
|
227
|
+
```ini
|
|
228
|
+
# .env
|
|
229
|
+
FLAMEWORK_SCOPES=
|
|
230
|
+
OBFUSCATE=false
|
|
231
|
+
|
|
232
|
+
# .env.local (ignored by git)
|
|
233
|
+
FLAMEWORK_SCOPES=components,collections
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`Flamework.env("NAME", fallback?)` reads the same environment, and inlines the value into code as a
|
|
237
|
+
string literal when you build. Its type is `string | undefined` on its own, and `string` with a
|
|
238
|
+
fallback. Use it for deployment values, never for secrets: the value is written into the emitted
|
|
239
|
+
Luau. See [Macros](07-macros.md#what-you-already-used).
|
|
240
|
+
|
|
241
|
+
### Watching
|
|
242
|
+
|
|
243
|
+
The file and the environment are read once, when `rbxtsc` starts. A watcher keeps what it read for
|
|
244
|
+
as long as it runs. The reason: a watcher only recompiles the files that changed, but much of the
|
|
245
|
+
config is compiled into every file (ids, serialization codecs, `Flamework.env` values). Taking up a
|
|
246
|
+
change halfway would leave the output disagreeing with itself.
|
|
247
|
+
|
|
248
|
+
Under `rbxtsc -w`, a change to `flamework.config.json`, `.env` or `.env.local` is noticed on the
|
|
249
|
+
next rebuild and reported: `flamework.config.json or .env changed since the watcher started`. The
|
|
250
|
+
watcher keeps using the values it started with until you restart it. A plain build reads everything
|
|
251
|
+
fresh. Changing `idGenerationMode` or `obfuscation` also regenerates every identifier, which the
|
|
252
|
+
next plain build does on its own.
|
|
253
|
+
|
|
254
|
+
## What to commit
|
|
255
|
+
|
|
256
|
+
What a game commits and what it ignores. The build creates `flamework.config.json` once, when it is
|
|
257
|
+
missing, and that file is yours from then on: commit it. Everything else the build writes is
|
|
258
|
+
written again by every plain build (`rbxtsc`), so none of it is committed:
|
|
259
|
+
|
|
260
|
+
| File | Commit | Why |
|
|
261
|
+
|---|---|---|
|
|
262
|
+
| `flamework.config.json` | yes | Your settings. The `$schema` line in it is a relative path into `node_modules`, the same on every machine, so it goes with the file. |
|
|
263
|
+
| `.env` | yes | The defaults the config reads, with nothing secret in it. Leave build switches such as `FLAMEWORK_SCOPES` empty here: every plain build, the release one included, reads this file. |
|
|
264
|
+
| `.env.local` | no | Your own overrides, and secrets such as `ROBLOX_API_KEY`. It wins over `.env`. |
|
|
265
|
+
| `flamework.build` | no | The ids, the hash salt and the build seed. Every plain build writes it anew; only an incremental build or a watcher reads the old one. |
|
|
266
|
+
| `include/`, `include/flamework/` | no | roblox-ts copies its runtime into `include/` on every build, and the transformer writes `include/flamework/` (paths, globs, the runtime config) on every build of a game. roblox-ts's template already ignores `/include`. |
|
|
267
|
+
| `out/`, `*.tsbuildinfo` | no | The compiled Luau, and an incremental build's record. |
|
|
268
|
+
| Place files the build makes (`rojo build -o place.rbxl`, and the `place.patched.rbxl` or `place.<project>.rbxl` that `flamework-test` writes beside it) | no | Rebuilt from the sources. Ignore `/*.rbxl` at the root rather than every `*.rbxl`, so a place you keep in a folder on purpose, such as a test place saved from Studio, can still be committed. |
|
|
269
|
+
| `*.rbxl.lock`, `*.rbxlx.lock` | no | Studio's lock beside a place it has open. `flamework-test` removes the lock of a window it ends; one left by a Studio closed any other way stays behind. |
|
|
270
|
+
| `build/` | no | `flamework-test` records the version it published in `build/version.json`: `cloud publish` does, and so `cloud test` and `test --cloud`. |
|
|
271
|
+
|
|
272
|
+
```gitignore
|
|
273
|
+
/node_modules
|
|
274
|
+
/out
|
|
275
|
+
/include
|
|
276
|
+
/flamework.build
|
|
277
|
+
*.tsbuildinfo
|
|
278
|
+
.env.local
|
|
279
|
+
/*.rbxl
|
|
280
|
+
/*.rbxlx
|
|
281
|
+
*.rbxl.lock
|
|
282
|
+
*.rbxlx.lock
|
|
283
|
+
/build
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Testing
|
|
287
|
+
|
|
288
|
+
For tests that run inside the place (in Studio, in a live server, or in an Open Cloud task), use
|
|
289
|
+
[`@flamework-experimental/testing`](12-testing.md): sections of tests loaded by a plugin, run
|
|
290
|
+
through a bindable, with cleanup that always runs.
|
|
291
|
+
|
|
292
|
+
This section covers the other kind. A module is a container you can build fresh, so you can test
|
|
293
|
+
Flamework code without a mocking framework that works through injection:
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
const definition = Flamework.createModule()
|
|
297
|
+
.registerClassProvider(Shop)
|
|
298
|
+
// swap the real implementation for a fake under the same id
|
|
299
|
+
.registerProvider<Storage>({ type: "function", callback: () => new FakeStorage() })
|
|
300
|
+
.build();
|
|
301
|
+
|
|
302
|
+
const module = definition.ignite();
|
|
303
|
+
const shop = module.resolveDependency<Shop>();
|
|
304
|
+
|
|
305
|
+
// ...assert...
|
|
306
|
+
|
|
307
|
+
module.extinguish();
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Ignite the *definition* once per test case, not the module: a `Module` cannot be ignited again.
|
|
311
|
+
|
|
312
|
+
For scenarios that run inside a place, such as a test rig or a debug world, keep them in their own
|
|
313
|
+
folder and tie them to a [scope](11-scopes.md), so they only exist in builds that ask for them.
|
|
314
|
+
Give them a module of their own that [imports](02-modules.md#importing-a-module) the game's module:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
if (Flamework.isScopeActive("components")) {
|
|
318
|
+
Flamework.createModule()
|
|
319
|
+
.registerProviders("src/server/Testing/components")
|
|
320
|
+
.includePlugin(ComponentPlugin.fromPath("src/server/Testing/components"))
|
|
321
|
+
.ignite({ activeIn: ["components"], imports: [game] });
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
A provider in that folder takes the game's providers in its constructor like any other.
|
|
326
|
+
`game.extinguish()` takes the rig down first.
|
|
327
|
+
|
|
328
|
+
For networking, `predict` runs a receiving handler locally, guards and middleware included, without
|
|
329
|
+
a remote:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
events.setReady.predict(player, true);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Flamework's own runtime specs use this shape; see [`packages/specs`](https://github.com/Velover/ExperimentalFlameworkV2/tree/HEAD/packages/specs) for a
|
|
336
|
+
worked example.
|
|
337
|
+
|
|
338
|
+
## Studio plugins and models
|
|
339
|
+
|
|
340
|
+
A place is a `DataModel` tree, so a registered path starts at a service.
|
|
341
|
+
`registerProviders("src/server/services")` becomes `ServerScriptService/TS/services`, and the
|
|
342
|
+
runtime walks there from `game`. A Studio plugin or a model is a tree of its own: a `Folder` at the
|
|
343
|
+
top of `default.project.json`, with nothing above it. Its paths are relative to that root instead.
|
|
344
|
+
|
|
345
|
+
Nothing changes in what you write. It works like this:
|
|
346
|
+
|
|
347
|
+
1. The transformer emits every path relative to the tree's root.
|
|
348
|
+
2. It records in `include/flamework/paths.json` how far below that root the include folder sits.
|
|
349
|
+
3. The first time a path is resolved, the runtime climbs up from the include folder to find the
|
|
350
|
+
root.
|
|
351
|
+
|
|
352
|
+
So a Studio plugin registers folders and globs exactly as a place does, and `@Provider` has no
|
|
353
|
+
notion of a realm to get in the way. For a plugin of your own that resolves paths by hand,
|
|
354
|
+
`getPathRoot()` from `@flamework-experimental/core` returns that root instance, and
|
|
355
|
+
`resolveRbxPath(path)` walks a compile-time path from it.
|
|
356
|
+
|
|
357
|
+
The only requirement is the usual one: the include directory has to be in the Rojo tree, as it is
|
|
358
|
+
in every roblox-ts template.
|
|
359
|
+
|
|
360
|
+
## Caveats
|
|
361
|
+
|
|
362
|
+
- **Overlapping registration paths raise.** `registerProviders("src/server")` and
|
|
363
|
+
`registerProviders("src/server/services")` both find the same classes.
|
|
364
|
+
- **Path registration requires every ModuleScript under the path**, so import side effects run at
|
|
365
|
+
ignition. A registration whose own scope condition does not hold skips its folder instead
|
|
366
|
+
([Scopes](11-scopes.md)).
|
|
367
|
+
- **Every registered folder must be mapped in Rojo**, or the build fails with
|
|
368
|
+
`Could not find Rojo data`. It must also exist under its exact name and hold a module, or the
|
|
369
|
+
build warns: a folder the place lacks is waited for at runtime, and an empty one registers
|
|
370
|
+
nothing.
|
|
371
|
+
- **`ModuleDefinition`s in a registered folder get built as a side effect.** Keep them out of
|
|
372
|
+
`services`.
|
|
373
|
+
- **The entry point should be the only file that ignites.** A second `ignite()` elsewhere builds a
|
|
374
|
+
second, unrelated container, and dependencies will not resolve across them.
|
|
375
|
+
|
|
376
|
+
## Guard deduplication
|
|
377
|
+
|
|
378
|
+
Large guards can repeat the same nested type many times. Set
|
|
379
|
+
`"optimizations": { "guardGenerationDedupLimit": N }` in the transformer options, and any object or
|
|
380
|
+
union type that occurs at least `N` times inside one generated guard is emitted once, as a local,
|
|
381
|
+
and referenced from there. This shrinks the output and the work `t` does per check.
|
|
382
|
+
|
|
383
|
+
Guards with more than two members always use `t.unionList`, `t.intersectionList` and
|
|
384
|
+
`t.literalList`, so there is no argument limit to hit.
|
|
385
|
+
|
|
386
|
+
When your project resolves a different `@rbxts/t` than `@flamework-experimental/core` does,
|
|
387
|
+
generated guards import `t` through `@flamework-experimental/core/out/prelude`. That way they run
|
|
388
|
+
against the version core was built with.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
Previous: [Plugins](08-plugins.md) · Next: [Migrating from v1](10-migrating-from-v1.md)
|