@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.
Files changed (44) hide show
  1. package/README.md +41 -34
  2. package/docs/README.md +63 -0
  3. package/docs/guide/01-getting-started.md +334 -0
  4. package/docs/guide/02-modules.md +254 -0
  5. package/docs/guide/03-providers.md +423 -0
  6. package/docs/guide/04-lifecycle-events.md +423 -0
  7. package/docs/guide/05-components.md +793 -0
  8. package/docs/guide/06-networking.md +614 -0
  9. package/docs/guide/07-macros.md +332 -0
  10. package/docs/guide/08-plugins.md +203 -0
  11. package/docs/guide/09-project-structure.md +392 -0
  12. package/docs/guide/10-migrating-from-v1.md +573 -0
  13. package/docs/guide/11-scopes.md +165 -0
  14. package/docs/guide/12-testing.md +342 -0
  15. package/flamework.build +1 -1
  16. package/out/dependency.d.ts +4 -0
  17. package/out/dependency.luau +4 -0
  18. package/out/index.d.ts +5 -2
  19. package/out/init.luau +11 -2
  20. package/out/lifecycle/lifecyclePlugin.d.ts +13 -1
  21. package/out/lifecycle/lifecyclePlugin.luau +67 -7
  22. package/out/module/module.luau +126 -24
  23. package/out/module/moduleBuilder.d.ts +12 -3
  24. package/out/module/moduleBuilder.luau +23 -1
  25. package/out/module/moduleDefinition.d.ts +6 -0
  26. package/out/module/providerRegistration.d.ts +8 -0
  27. package/out/module/providerRegistration.luau +29 -0
  28. package/out/plugin/pluginDefinition.d.ts +7 -4
  29. package/out/provider.d.ts +19 -0
  30. package/out/provider.luau +8 -0
  31. package/out/reflect.luau +6 -0
  32. package/out/utility/explainUnresolved.d.ts +9 -0
  33. package/out/utility/explainUnresolved.luau +43 -0
  34. package/out/utility/getClassesInPath.d.ts +37 -3
  35. package/out/utility/getClassesInPath.luau +154 -33
  36. package/out/utility/globs.d.ts +2 -2
  37. package/out/utility/globs.luau +3 -3
  38. package/out/utility/leftOut.d.ts +35 -0
  39. package/out/utility/leftOut.luau +171 -0
  40. package/out/utility/moduleClasses.d.ts +9 -0
  41. package/out/utility/moduleClasses.luau +64 -0
  42. package/out/utility/pathRoot.d.ts +20 -1
  43. package/out/utility/pathRoot.luau +80 -4
  44. 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)