@flamework-experimental/core 2.0.0-alpha.2 → 2.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,17 @@
1
1
  # Flamework
2
2
 
3
- Flamework is an extensible framework for roblox-ts designed around portable, isolated and testable modules.
3
+ Flamework is an extensible framework for roblox-ts. It is built around modules that are portable,
4
+ isolated and easy to test.
4
5
 
5
6
  ## Documentation
6
7
 
7
- **[docs/](docs/README.md)** -- start there. A ten-part guide that builds up from a working entry
8
- point to plugins and project layout, plus reference material:
8
+ Start with **[docs/](docs/README.md)**. It holds a twelve-part guide, which starts from a working
9
+ entry point and builds up to plugins, project layout, scopes and testing. It also holds reference
10
+ material:
9
11
 
10
12
  | | |
11
13
  |---|---|
12
- | [Guide](docs/README.md#guide) | Getting started, modules, providers, lifecycle events, components, networking, macros, plugins, project structure, migrating from v1. |
14
+ | [Guide](docs/README.md#guide) | Getting started, modules, providers, lifecycle events, components, networking, macros, plugins, project structure, migrating from v1, scopes, testing in the place. |
13
15
  | [Internals](docs/reference/internals.md) | What the transformer does to your code and what the runtime does with the result. |
14
16
  | [Transformer plugins](docs/reference/transformer-plugins.md) | Adding macro types of your own. |
15
17
 
@@ -37,36 +39,36 @@ bun run test:place # the in-place suite in Roblox Studio (tests/place); need
37
39
  | `packages/core` | Modules, dependency injection, plugins and lifecycle events |
38
40
  | `packages/components` | CollectionService components, built on the core plugin system |
39
41
  | `packages/networking` | Remote events and functions |
40
- | `packages/testing` | In-place tests: sections, cleanup, a bindable and a remote to run them, a cloud entry; and `flamework-test`, the CLI that runs them in Roblox Studio on this machine or through Open Cloud (`cli/`) |
42
+ | `packages/testing` | Tests that run inside a place: sections, cleanup, a bindable and a remote that run them, and a cloud entry point. Also `flamework-test` (`cli/`), the CLI that runs them in Roblox Studio on this machine or through Open Cloud |
41
43
  | `packages/transformer` | The roblox-ts transformer |
42
44
  | `packages/transformer-plugin` | Public API for writing transformer plugins |
43
- | `packages/specs` | Runtime specs, compiled by `rbxtsc` and executed under Lune |
45
+ | `packages/specs` | Runtime specs, built by `rbxtsc` and run under Lune |
44
46
 
45
47
  ### Tests
46
48
 
47
- Two suites, both run by `bun run test`:
48
-
49
- - **Transformer tests** (`bun run test:unit`) compile a fixture project with the real `rbxtsc` and
50
- assert on the emitted Luau — guard generation, identifiers, nested macros and the plugin system.
51
- - **Runtime specs** (`bun run test:runtime`) execute compiled `@flamework-experimental/core`, `components` and
52
- `networking` under Lune using the harness in [`tests/runtime`](tests/runtime), which models
53
- roblox-ts's `TS.import` tree over the filesystem and stubs the Roblox API surface Flamework
54
- touches (Instances, attributes, CollectionService, RemoteEvents, Players, signals, `task`,
55
- `Enum`, and a `Heartbeat` pump so `Promise.delay` -- and therefore request timeouts -- runs).
56
- They cover dependency injection, modules, hooks and the per-frame lifecycle events, component
57
- construction, dependencies and streaming, and both halves of networking: events, functions,
58
- middleware and the generated guards.
59
-
60
- They run twice, once as `Server` and once as `Client`, because realm-dependent code paths --
61
- `@Provider`'s metadata, component streaming, and the client/server halves of networking -- differ
62
- between them. Where a spec asserts something realm-specific, running it from both sides is what
63
- proves the two agree: a function receives on `$name` and sends on `@name` from the server and the
64
- mirror image from the client, so the pair of runs pins the wire format down from both ends.
65
-
66
- Specs live in [`packages/specs`](packages/specs) and are compiled by `rbxtsc` like any other
67
- Flamework consumer, so they exercise the transformer and the runtime together.
68
-
69
- A third suite runs against the real engine and is left out of `bun run test`: the
70
- [test place](tests/place/README.md), a small game linked to the packages' builds, whose
71
- `@flamework-experimental/testing` sections `bun run test:place` runs in Roblox Studio, both realms,
72
- under four Rojo projects (see [Testing in Roblox Studio](docs/testing/studio.md)).
49
+ `bun run test` runs two suites:
50
+
51
+ - **Transformer tests** (`bun run test:unit`) build a fixture project with the real `rbxtsc` and
52
+ check the Luau it emits: guard generation, identifiers, nested macros and the plugin system.
53
+ - **Runtime specs** (`bun run test:runtime`) run the built `@flamework-experimental/core`,
54
+ `components` and `networking` packages under Lune. The harness in [`tests/runtime`](tests/runtime)
55
+ models roblox-ts's `TS.import` tree over the filesystem. It also stubs the parts of the Roblox API
56
+ that Flamework uses: Instances, attributes, CollectionService, RemoteEvents, Players, signals,
57
+ `task`, `Enum`, and a `Heartbeat` pump so that `Promise.delay` runs (and with it, request
58
+ timeouts). The specs cover dependency injection, modules, hooks and the per-frame lifecycle
59
+ events; component construction, dependencies and streaming; and both halves of networking:
60
+ events, functions, middleware and the generated guards.
61
+
62
+ The specs run twice, once as `Server` and once as `Client`, because some code paths depend on the
63
+ realm: `@Provider`'s metadata, component streaming, and the client and server halves of
64
+ networking. Running a realm-specific spec from both sides proves that the two sides agree. For
65
+ example, from the server a function receives on `$name` and sends on `@name`, and from the client
66
+ it does the reverse, so the two runs pin down the wire format from both ends.
67
+
68
+ Specs live in [`packages/specs`](packages/specs). `rbxtsc` builds them like any other project that
69
+ uses Flamework, so they test the transformer and the runtime together.
70
+
71
+ A third suite runs against the real engine, and `bun run test` leaves it out. It lives in the
72
+ [test place](tests/place/README.md), a small game linked to the packages' builds.
73
+ `bun run test:place` runs its `@flamework-experimental/testing` sections in Roblox Studio, on both
74
+ realms, under four Rojo projects (see [Testing in Roblox Studio](docs/testing/studio.md)).
package/flamework.build CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "flameworkVersion": "2.0.0-alpha.3",
3
+ "flameworkVersion": "2.0.0-alpha.4",
4
4
  "identifiers": {
5
5
  "@flamework-experimental/core:out/module/module@Module": "$:module/module@Module",
6
6
  "@flamework-experimental/core:out/lifecycle/lifecyclePlugin@LifecycleProvider": "$:lifecycle/lifecyclePlugin@LifecycleProvider",
@@ -10,6 +10,10 @@ import type { Module } from "./module/module";
10
10
  * handed to something outside Flamework. Inside a provider, take a constructor parameter instead:
11
11
  * it declares the dependency where it can be read, and it orders construction.
12
12
  *
13
+ * It resolves what the module registers -- providers, and what plugins provide -- and nothing else:
14
+ * unlike v1, it does not build an unregistered class on demand. A component is never one; the
15
+ * transformer refuses `Dependency<T>()` on a `@Component()` class, which `Components` hands out.
16
+ *
13
17
  * Raises if no module was given and none has been ignited yet, or if the default has since been
14
18
  * extinguished.
15
19
  *
@@ -12,6 +12,10 @@ local getDefaultModule = TS.import(script, script.Parent, "module", "defaultModu
12
12
  * handed to something outside Flamework. Inside a provider, take a constructor parameter instead:
13
13
  * it declares the dependency where it can be read, and it orders construction.
14
14
  *
15
+ * It resolves what the module registers -- providers, and what plugins provide -- and nothing else:
16
+ * unlike v1, it does not build an unregistered class on demand. A component is never one; the
17
+ * transformer refuses `Dependency<T>()` on a `@Component()` class, which `Components` hands out.
18
+ *
15
19
  * Raises if no module was given and none has been ignited yet, or if the default has since been
16
20
  * extinguished.
17
21
  *
package/out/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { HookPriority } from "./module/moduleHooks";
13
13
  export type { Module, ProviderLookup } from "./module/module";
14
14
  export type { IgniteOptions, InjectionContext, ModuleProvider, ModuleState, PluginInclusion, ProviderConfig, ProviderRegistrationOptions, } from "./module/moduleDefinition";
15
15
  export type { HookOptions } from "./module/moduleHooks";
16
- export { describeConditions, holdsCondition, holdsEveryCondition, __setActiveScopes } from "./module/scopes";
16
+ export { describeConditions, holdsCondition, holdsEveryCondition } from "./module/scopes";
17
17
  export type { ScopeCondition } from "./module/scopes";
18
18
  export { PluginDefinition } from "./plugin/pluginDefinition";
19
19
  export { LifecyclePlugin, LifecycleProvider, createLifecyclePlugin } from "./lifecycle/lifecyclePlugin";
@@ -22,7 +22,9 @@ export type { InterfaceConfiguration, InterfaceContext, InterfaceTargetKind, Plu
22
22
  export type { OnExtinguished, OnInit, OnPhysics, OnRender, OnStart, OnTick } from "./lifecycle/lifecycleInterfaces";
23
23
  export { getClassesInPath, importModule, requireModulesInPath } from "./utility/getClassesInPath";
24
24
  export { getClassesInGlob, getGlobPaths } from "./utility/globs";
25
- export { getPathRoot, resolveRbxPath, __setPathRoot } from "./utility/pathRoot";
25
+ export { getPathRoot, resolveRbxPath } from "./utility/pathRoot";
26
+ export { explainLeftOut, leftOutRegistration } from "./utility/leftOut";
27
+ export type { LeftOutRegistration } from "./utility/leftOut";
26
28
  export { getRuntimeConfig } from "./utility/runtimeConfig";
27
29
  export type { ComponentsRuntimeConfig, CoreRuntimeConfig, NetworkingRuntimeConfig, RuntimeConfig, ScopesRuntimeConfig, TestingRuntimeConfig, } from "./utility/runtimeConfig";
28
30
  export type { AbstractConstructor, Constructor } from "./utility/constructors";
package/out/init.luau CHANGED
@@ -16,7 +16,6 @@ local _scopes = TS.import(script, script, "module", "scopes")
16
16
  exports.describeConditions = _scopes.describeConditions
17
17
  exports.holdsCondition = _scopes.holdsCondition
18
18
  exports.holdsEveryCondition = _scopes.holdsEveryCondition
19
- exports.__setActiveScopes = _scopes.__setActiveScopes
20
19
  -- Plugins
21
20
  exports.PluginDefinition = TS.import(script, script, "plugin", "pluginDefinition").PluginDefinition
22
21
  local _lifecyclePlugin = TS.import(script, script, "lifecycle", "lifecyclePlugin")
@@ -35,6 +34,15 @@ exports.getGlobPaths = _globs.getGlobPaths
35
34
  local _pathRoot = TS.import(script, script, "utility", "pathRoot")
36
35
  exports.getPathRoot = _pathRoot.getPathRoot
37
36
  exports.resolveRbxPath = _pathRoot.resolveRbxPath
38
- exports.__setPathRoot = _pathRoot.__setPathRoot
37
+ local _leftOut = TS.import(script, script, "utility", "leftOut")
38
+ exports.explainLeftOut = _leftOut.explainLeftOut
39
+ exports.leftOutRegistration = _leftOut.leftOutRegistration
39
40
  exports.getRuntimeConfig = TS.import(script, script, "utility", "runtimeConfig").getRuntimeConfig
41
+ -- The test harness's hooks: exported for the Luau it reaches through the package entry, and marked
42
+ -- @internal like their declarations so that stripInternal leaves them out of the typings as well
43
+ -- (a re-export of a stripped declaration would not type-check).
44
+ --* @internal
45
+ exports.__setActiveScopes = TS.import(script, script, "module", "scopes").__setActiveScopes
46
+ --* @internal
47
+ exports.__setPathRoot = TS.import(script, script, "utility", "pathRoot").__setPathRoot
40
48
  return exports
@@ -22,8 +22,13 @@ export declare class LifecycleProvider {
22
22
  /** In attachment order, which for providers is dependency order. */
23
23
  private onInit;
24
24
  private initMembers;
25
- /** The providers `postIgnite` starts, in attachment order; `onStart` is every member. */
25
+ /** The providers `start` starts, in attachment order; `onStart` is every member. */
26
26
  private startOrder;
27
+ /**
28
+ * The `loadOrder` of each provider in `startOrder` that has one other than the default, which
29
+ * `start` orders them by. Empty unless a provider sets one, and emptied once they have started.
30
+ */
31
+ private startLoadOrders;
27
32
  onStart: Set<OnStart>;
28
33
  onTick: Set<OnTick>;
29
34
  onPhysics: Set<OnPhysics>;
@@ -228,6 +233,13 @@ export declare class LifecycleProvider {
228
233
  * extinguished while an `onInit` yielded had the providers started against it first.
229
234
  */
230
235
  start(module: Module): void;
236
+ /**
237
+ * A copy of the providers to start, in ascending `loadOrder`: each on its own thread, so a lower
238
+ * one runs up to its first yield before the next is started. Attachment order -- dependency order
239
+ * -- among equals, and unchanged when no provider sets one. Sorted once, at ignition: nothing per
240
+ * frame is ordered.
241
+ */
242
+ private inLoadOrder;
231
243
  extinguished(module: Module): void;
232
244
  private tellExtinguished;
233
245
  }
@@ -10,6 +10,7 @@ local extinguishesBegun = _threadWaits.extinguishesBegun
10
10
  local runsPromiseWork = _threadWaits.runsPromiseWork
11
11
  local threadWaits = _threadWaits.threadWaits
12
12
  local Reflect = TS.import(script, script.Parent.Parent, "reflect").Reflect
13
+ local DEFAULT_LOAD_ORDER = TS.import(script, script.Parent.Parent, "module", "providerRegistration").DEFAULT_LOAD_ORDER
13
14
  local _pluginDefinition = TS.import(script, script.Parent.Parent, "plugin", "pluginDefinition")
14
15
  local LIFECYCLE_SLOT = _pluginDefinition.LIFECYCLE_SLOT
15
16
  local PluginDefinition = _pluginDefinition.PluginDefinition
@@ -144,6 +145,7 @@ do
144
145
  self.onInit = {}
145
146
  self.initMembers = {}
146
147
  self.startOrder = {}
148
+ self.startLoadOrders = {}
147
149
  self.onStart = {}
148
150
  self.onTick = {}
149
151
  self.onPhysics = {}
@@ -636,6 +638,12 @@ do
636
638
  local _startOrder = self.startOrder
637
639
  local _object_1 = object
638
640
  table.insert(_startOrder, _object_1)
641
+ if context.loadOrder ~= nil and context.loadOrder ~= DEFAULT_LOAD_ORDER then
642
+ local _startLoadOrders = self.startLoadOrders
643
+ local _object_2 = object
644
+ local _loadOrder = context.loadOrder
645
+ _startLoadOrders[_object_2] = _loadOrder
646
+ end
639
647
  else
640
648
  self:scheduleLateProvider(object)
641
649
  end
@@ -647,9 +655,12 @@ do
647
655
  local _lateProviders = self.lateProviders
648
656
  local _object_1 = object
649
657
  _lateProviders[_object_1] = nil
650
- local _startOrder = self.startOrder
658
+ local _startLoadOrders = self.startLoadOrders
651
659
  local _object_2 = object
652
- local index = (table.find(_startOrder, _object_2) or 0) - 1
660
+ _startLoadOrders[_object_2] = nil
661
+ local _startOrder = self.startOrder
662
+ local _object_3 = object
663
+ local index = (table.find(_startOrder, _object_3) or 0) - 1
653
664
  if index ~= -1 then
654
665
  table.remove(self.startOrder, index + 1)
655
666
  end
@@ -698,11 +709,7 @@ do
698
709
  end
699
710
  function LifecycleProvider:start(module)
700
711
  -- An `onStart` may extinguish the module; nothing starts or ticks after that.
701
- local _array = {}
702
- local _length = #_array
703
- local _array_1 = self.startOrder
704
- table.move(_array_1, 1, #_array_1, _length + 1, _array)
705
- for _, object in _array do
712
+ for _, object in self:inLoadOrder(self.startOrder) do
706
713
  if self:hasBegunExtinguishing() then
707
714
  return nil
708
715
  end
@@ -741,6 +748,58 @@ do
741
748
  local _module = module
742
749
  _moduleConnections[_module] = connections
743
750
  end
751
+ function LifecycleProvider:inLoadOrder(objects)
752
+ local orders = self.startLoadOrders
753
+ if next(orders) == nil then
754
+ local _array = {}
755
+ local _length = #_array
756
+ table.move(objects, 1, #objects, _length + 1, _array)
757
+ return _array
758
+ end
759
+ self.startLoadOrders = {}
760
+ -- `table.sort` is not stable, so equals are ordered by their position.
761
+ local position = {}
762
+ -- ▼ ReadonlyArray.forEach ▼
763
+ local _callback = function(object, index)
764
+ local _object = object
765
+ local _index = index
766
+ position[_object] = _index
767
+ return position
768
+ end
769
+ for _k, _v in objects do
770
+ _callback(_v, _k - 1, objects)
771
+ end
772
+ -- ▲ ReadonlyArray.forEach ▲
773
+ local _array = {}
774
+ local _length = #_array
775
+ table.move(objects, 1, #objects, _length + 1, _array)
776
+ local sorted = _array
777
+ table.sort(sorted, function(a, b)
778
+ local _a = a
779
+ local _condition = orders[_a]
780
+ if _condition == nil then
781
+ _condition = DEFAULT_LOAD_ORDER
782
+ end
783
+ local orderA = _condition
784
+ local _b = b
785
+ local _condition_1 = orders[_b]
786
+ if _condition_1 == nil then
787
+ _condition_1 = DEFAULT_LOAD_ORDER
788
+ end
789
+ local orderB = _condition_1
790
+ local _result
791
+ if orderA ~= orderB then
792
+ _result = orderA < orderB
793
+ else
794
+ local _a_1 = a
795
+ local _exp = position[_a_1]
796
+ local _b_1 = b
797
+ _result = _exp < position[_b_1]
798
+ end
799
+ return _result
800
+ end)
801
+ return sorted
802
+ end
744
803
  function LifecycleProvider:extinguished(module)
745
804
  local _moduleConnections = self.moduleConnections
746
805
  local _module = module
@@ -821,6 +880,7 @@ do
821
880
  do
822
881
  -- (Flamework) LifecycleProvider metadata
823
882
  Reflect_1.defineMetadata(LifecycleProvider, "identifier", "$:lifecycle/lifecyclePlugin@LifecycleProvider")
883
+ Reflect_1.defineMetadata(LifecycleProvider, "flamework:module", script)
824
884
  Reflect_1.defineMetadata(LifecycleProvider, "flamework:implements", {})
825
885
  Reflect_1.defineMetadata(LifecycleProvider, "flamework:parameters", { "$:lifecycle/lifecyclePlugin@LifecyclePluginOptions" })
826
886
  Reflect_1.defineMetadata(LifecycleProvider, "flamework:dependencies", { {
@@ -5,14 +5,20 @@ local convertConciseDependencyInfo = TS.import(script, script.Parent.Parent, "ut
5
5
  local getClassImplements = TS.import(script, script.Parent.Parent, "utility", "getClassImplements").getClassImplements
6
6
  local getClassesInPath = TS.import(script, script.Parent.Parent, "utility", "getClassesInPath").getClassesInPath
7
7
  local getClassesInGlob = TS.import(script, script.Parent.Parent, "utility", "globs").getClassesInGlob
8
+ local explainUnresolvedClass = TS.import(script, script.Parent.Parent, "utility", "explainUnresolved").explainUnresolvedClass
9
+ local _leftOut = TS.import(script, script.Parent.Parent, "utility", "leftOut")
10
+ local explainLeftOut = _leftOut.explainLeftOut
11
+ local leftOutRegistration = _leftOut.leftOutRegistration
8
12
  local _threadWaits = TS.import(script, script.Parent.Parent, "utility", "threadWaits")
9
13
  local extinguishesBegun = _threadWaits.extinguishesBegun
10
14
  local threadWaits = _threadWaits.threadWaits
11
15
  local clearDefaultModule = TS.import(script, script.Parent, "defaultModule").clearDefaultModule
12
16
  local HookPriority = TS.import(script, script.Parent, "moduleHooks").HookPriority
13
17
  local _providerRegistration = TS.import(script, script.Parent, "providerRegistration")
18
+ local DEFAULT_LOAD_ORDER = _providerRegistration.DEFAULT_LOAD_ORDER
14
19
  local getProviderClassId = _providerRegistration.getProviderClassId
15
20
  local getProviderClassScope = _providerRegistration.getProviderClassScope
21
+ local getProviderLoadOrder = _providerRegistration.getProviderLoadOrder
16
22
  local normalizeProviderConfig = _providerRegistration.normalizeProviderConfig
17
23
  local _scopes = TS.import(script, script.Parent, "scopes")
18
24
  local NO_CONDITION = _scopes.NO_CONDITION
@@ -76,6 +82,12 @@ local function createModuleInstantiation(state, options)
76
82
  local providers = {}
77
83
  --* The registrations left out, by id, with the conditions that were judged: for the error a miss gets.
78
84
  local skipped = {}
85
+ --* The path and glob registrations left out by their own condition, the builder's and the plugins'.
86
+ local _array_2 = {}
87
+ local _length_1 = #_array_2
88
+ local _array_3 = (state.leftOut or {})
89
+ table.move(_array_3, 1, #_array_3, _length_1 + 1, _array_2)
90
+ local leftOut = _array_2
79
91
  --* Modules searched after this one's own providers, in order. Ignited before this one, and extinguished after.
80
92
  local _result = options
81
93
  if _result ~= nil then
@@ -229,6 +241,67 @@ local function createModuleInstantiation(state, options)
229
241
  end
230
242
  providers = active
231
243
  end
244
+ --[[
245
+ *
246
+ * The registrations in ascending `loadOrder`, registration order among equals. The same list
247
+ * when none sets one, which is every module that does not use it.
248
+
249
+ ]]
250
+ local inLoadOrder = function(entries)
251
+ -- ▼ ReadonlyArray.map ▼
252
+ local _newValue = table.create(#entries)
253
+ local _callback = function(entry)
254
+ local _condition_1 = getProviderLoadOrder(entry.config)
255
+ if _condition_1 == nil then
256
+ _condition_1 = DEFAULT_LOAD_ORDER
257
+ end
258
+ return _condition_1
259
+ end
260
+ for _k, _v in entries do
261
+ _newValue[_k] = _callback(_v, _k - 1, entries)
262
+ end
263
+ -- ▲ ReadonlyArray.map ▲
264
+ local orders = _newValue
265
+ -- ▼ ReadonlyArray.every ▼
266
+ local _result_1 = true
267
+ local _callback_1 = function(order)
268
+ return order == DEFAULT_LOAD_ORDER
269
+ end
270
+ for _k, _v in orders do
271
+ if not _callback_1(_v, _k - 1, orders) then
272
+ _result_1 = false
273
+ break
274
+ end
275
+ end
276
+ -- ▲ ReadonlyArray.every ▲
277
+ if _result_1 then
278
+ return entries
279
+ end
280
+ -- `table.sort` is not stable, so equals are ordered by their position.
281
+ -- ▼ ReadonlyArray.map ▼
282
+ local _newValue_1 = table.create(#entries)
283
+ local _callback_2 = function(_, index)
284
+ return index
285
+ end
286
+ for _k, _v in entries do
287
+ _newValue_1[_k] = _callback_2(_v, _k - 1, entries)
288
+ end
289
+ -- ▲ ReadonlyArray.map ▲
290
+ local indices = _newValue_1
291
+ table.sort(indices, function(a, b)
292
+ return if orders[a + 1] ~= orders[b + 1] then orders[a + 1] < orders[b + 1] else a < b
293
+ end)
294
+ -- ▼ ReadonlyArray.map ▼
295
+ local _newValue_2 = table.create(#indices)
296
+ local _callback_3 = function(index)
297
+ return entries[index + 1]
298
+ end
299
+ for _k, _v in indices do
300
+ _newValue_2[_k] = _callback_3(_v, _k - 1, indices)
301
+ end
302
+ -- ▲ ReadonlyArray.map ▲
303
+ return _newValue_2
304
+ end
232
305
  local registerHook = function(phase, callback, priority)
233
306
  local _object = {
234
307
  phase = phase,
@@ -248,11 +321,11 @@ local function createModuleInstantiation(state, options)
248
321
  local _callback = function(hook)
249
322
  return hook.phase == phase
250
323
  end
251
- local _length_1 = 0
324
+ local _length_2 = 0
252
325
  for _k, _v in hooks do
253
326
  if _callback(_v, _k - 1, hooks) == true then
254
- _length_1 += 1
255
- _newValue[_length_1] = _v
327
+ _length_2 += 1
328
+ _newValue[_length_2] = _v
256
329
  end
257
330
  end
258
331
  -- ▲ ReadonlyArray.filter ▲
@@ -311,7 +384,7 @@ local function createModuleInstantiation(state, options)
311
384
  * `dependencies`, what a provider's constructor was given, is handed to the observers with it.
312
385
 
313
386
  ]]
314
- local registerClassInterfaces = function(instance, kind, dependencies)
387
+ local registerClassInterfaces = function(instance, kind, dependencies, loadOrder)
315
388
  local interfaces = getClassImplements(instance)
316
389
  -- How many observers have been told `onAdded`, in the order they were told: what a refusal
317
390
  -- undoes. Counted rather than recorded, since an attachment that goes through -- every one,
@@ -330,6 +403,7 @@ local function createModuleInstantiation(state, options)
330
403
  interfaceId = interfaceId,
331
404
  kind = kind,
332
405
  dependencies = dependencies,
406
+ loadOrder = loadOrder,
333
407
  })
334
408
  end
335
409
  attached += 1
@@ -456,7 +530,7 @@ local function createModuleInstantiation(state, options)
456
530
  -- not be cached either -- the next resolve handed it out with no lifecycle at all, and
457
531
  -- `release` told every observer, the refusing one included, it was removed again.
458
532
  local attached, err = pcall(function()
459
- return registerClassInterfaces(instantiatedProvider, "provider", dependencies)
533
+ return registerClassInterfaces(instantiatedProvider, "provider", dependencies, getProviderLoadOrder(config))
460
534
  end)
461
535
  if not attached then
462
536
  local _id_2 = info.id
@@ -510,6 +584,17 @@ local function createModuleInstantiation(state, options)
510
584
  if inactive ~= nil then
511
585
  error(`module '{state.debugName}' could not resolve dependency '{info.id}': it is registered but inactive ({describeConditions(inactive)}){searched}`)
512
586
  end
587
+ -- A folder registration left out by its own condition registered nothing to be inactive.
588
+ local leftOutReason = explainLeftOut(info.id, leftOut)
589
+ if leftOutReason ~= nil then
590
+ error(`module '{state.debugName}' could not resolve dependency '{info.id}': {leftOutReason}{searched}`)
591
+ end
592
+ -- A class that has been loaded says why it is not here: a component, a provider nothing
593
+ -- registered, a class that is not a provider at all.
594
+ local explanation = explainUnresolvedClass(info.id, requestingOrigin)
595
+ if explanation ~= nil then
596
+ error(`module '{state.debugName}' could not resolve dependency '{info.id}': {explanation}{searched}`)
597
+ end
513
598
  error(`module '{state.debugName}' could not resolve dependency '{info.id}'{searched}`)
514
599
  end
515
600
  return dependency
@@ -658,7 +743,10 @@ local function createModuleInstantiation(state, options)
658
743
  registerClassInterfaces(instance, "provider")
659
744
  unjoinedInstances[instance] = nil
660
745
  end
661
- for _, provider in providers do
746
+ -- In ascending `loadOrder`, each after what its constructor takes, which is the order the
747
+ -- lifecycle plugin runs `onInit` in: a low `loadOrder` goes first and pulls its
748
+ -- dependencies forward with it, and dependency order still wins over `loadOrder`.
749
+ for _, provider in inLoadOrder(providers) do
662
750
  -- Lazy providers are constructed the first time they are resolved instead.
663
751
  if provider.config.type == "class" and provider.config.lazy ~= true then
664
752
  resolveDependency(provider.injectionId)
@@ -708,13 +796,13 @@ local function createModuleInstantiation(state, options)
708
796
  -- extinguishing already, on another thread suspended in a handler that yields, is still
709
797
  -- using this module's providers, so it is waited for: skipped, it had this module released
710
798
  -- under it.
711
- local _array_2 = {}
712
- local _length_1 = #_array_2
799
+ local _array_4 = {}
800
+ local _length_2 = #_array_4
713
801
  for _v in importers do
714
- _length_1 += 1
715
- _array_2[_length_1] = _v
802
+ _length_2 += 1
803
+ _array_4[_length_2] = _v
716
804
  end
717
- for _, importer in _array_2 do
805
+ for _, importer in _array_4 do
718
806
  if not importer.isExtinguished() then
719
807
  importer.extinguish()
720
808
  else
@@ -743,13 +831,13 @@ local function createModuleInstantiation(state, options)
743
831
  end)
744
832
  end
745
833
  -- Copied first: removal callbacks may themselves remove instances.
746
- local _array_2 = {}
747
- local _length_1 = #_array_2
834
+ local _array_4 = {}
835
+ local _length_2 = #_array_4
748
836
  for _v in temporaryInstances do
749
- _length_1 += 1
750
- _array_2[_length_1] = _v
837
+ _length_2 += 1
838
+ _array_4[_length_2] = _v
751
839
  end
752
- for _, temporaryInstance in _array_2 do
840
+ for _, temporaryInstance in _array_4 do
753
841
  guarded("removing an instance", function()
754
842
  return removeClassInstance(temporaryInstance)
755
843
  end)
@@ -907,21 +995,35 @@ local function createModuleInstantiation(state, options)
907
995
  scope = if hasCondition(moduleScope) then moduleScope else nil,
908
996
  isActive = function(...)
909
997
  local conditions = { ... }
910
- local _array_2 = { moduleScope }
911
- local _length_1 = #_array_2
912
- table.move(conditions, 1, #conditions, _length_1 + 1, _array_2)
913
- return holdsEveryCondition(_array_2)
998
+ local _array_4 = { moduleScope }
999
+ local _length_2 = #_array_4
1000
+ table.move(conditions, 1, #conditions, _length_2 + 1, _array_4)
1001
+ return holdsEveryCondition(_array_4)
914
1002
  end,
915
1003
  registerClassProvider = registerClassProvider,
916
- registerProviders = function(_path, registrationOptions, resolved)
1004
+ registerProviders = function(path, registrationOptions, resolved)
917
1005
  local _arg0 = resolved ~= nil
918
1006
  assert(_arg0)
919
- registerProviderClasses(getClassesInPath(resolved), registrationOptions)
1007
+ if holdsCondition(registrationOptions) then
1008
+ registerProviderClasses(getClassesInPath(resolved), registrationOptions)
1009
+ else
1010
+ local _arg0_1 = leftOutRegistration(`registerProviders("{path}")`, registrationOptions, {
1011
+ path = resolved,
1012
+ })
1013
+ table.insert(leftOut, _arg0_1)
1014
+ end
920
1015
  end,
921
- registerProvidersGlob = function(_glob, registrationOptions, resolved)
1016
+ registerProvidersGlob = function(glob, registrationOptions, resolved)
922
1017
  local _arg0 = resolved ~= nil
923
1018
  assert(_arg0)
924
- registerProviderClasses(getClassesInGlob(resolved), registrationOptions)
1019
+ if holdsCondition(registrationOptions) then
1020
+ registerProviderClasses(getClassesInGlob(resolved), registrationOptions)
1021
+ else
1022
+ local _arg0_1 = leftOutRegistration(`registerProvidersGlob("{glob}")`, registrationOptions, {
1023
+ glob = resolved,
1024
+ })
1025
+ table.insert(leftOut, _arg0_1)
1026
+ end
925
1027
  end,
926
1028
  registerProvider = function(config, injectionId)
927
1029
  local _arg0 = injectionId ~= nil
@@ -2,7 +2,7 @@ import { Modding } from "../modding";
2
2
  import { ModuleDefinition, ProviderConfig, type IgniteOptions, type ProviderRegistrationOptions } from "./moduleDefinition";
3
3
  import type { Constructor } from "../utility/constructors";
4
4
  import { type PluginDefinition } from "../plugin/pluginDefinition";
5
- import type { ScopeCondition } from "./scopes";
5
+ import { type ScopeCondition } from "./scopes";
6
6
  type GenericId<T> = string | Modding.Target.Id<T>;
7
7
  export declare class ModuleBuilder {
8
8
  /** A global count of the number of module builders. Used to disambiguate identical module debug names. */
@@ -36,10 +36,16 @@ export declare class ModuleBuilder {
36
36
  /**
37
37
  * Register all providers under the specified path and its descendants.
38
38
  *
39
- * The providers must be exported, and must carry the `@Provider()` decorator themselves: an
40
- * undecorated subclass of a provider is not registered.
39
+ * Every `@Provider()` class the modules there define at their top level is registered, exported
40
+ * or not, as v1 registered every decorated class it required, and so is one they export from
41
+ * elsewhere; each once. A class declared inside a function is registered only if its module
42
+ * exports it. A provider must carry the `@Provider()` decorator itself: an undecorated subclass
43
+ * of a provider is not registered.
41
44
  *
42
45
  * The options apply to every provider found: a scope condition here scopes the whole folder.
46
+ * When it does not hold, the folder is not touched at all -- not looked up, nothing under it
47
+ * required -- so a build can leave the folder out of the place (a release build without its
48
+ * tests, say).
43
49
  *
44
50
  * @metadata macro
45
51
  */
@@ -51,6 +57,9 @@ export declare class ModuleBuilder {
51
57
  * This is the v2 equivalent of v1's `Flamework.addPathsGlob`. Globs can match a large number of
52
58
  * paths, so keep them as specific as possible.
53
59
  *
60
+ * As with `registerProviders`, a scope condition that does not hold leaves every matched folder
61
+ * untouched.
62
+ *
54
63
  * @metadata macro
55
64
  */
56
65
  registerProvidersGlob<T extends string>(_glob: T, options?: ProviderRegistrationOptions, glob?: Modding.Intrinsic<"pathglob", [T], string>): this;
@@ -8,6 +8,8 @@ local LIFECYCLE_SLOT = TS.import(script, script.Parent.Parent, "plugin", "plugin
8
8
  local _providerRegistration = TS.import(script, script.Parent, "providerRegistration")
9
9
  local getProviderClassId = _providerRegistration.getProviderClassId
10
10
  local normalizeProviderConfig = _providerRegistration.normalizeProviderConfig
11
+ local holdsCondition = TS.import(script, script.Parent, "scopes").holdsCondition
12
+ local leftOutRegistration = TS.import(script, script.Parent.Parent, "utility", "leftOut").leftOutRegistration
11
13
  local ModuleBuilder
12
14
  do
13
15
  ModuleBuilder = setmetatable({}, {
@@ -28,6 +30,7 @@ do
28
30
  debugName = "Anonymous",
29
31
  providers = {},
30
32
  plugins = {},
33
+ leftOut = {},
31
34
  }
32
35
  end
33
36
  function ModuleBuilder:includePlugin(plugin, options)
@@ -107,11 +110,30 @@ do
107
110
  function ModuleBuilder:registerProviders(_stringPath, options, path)
108
111
  local _path = path
109
112
  assert(_path)
113
+ -- The active scopes are the ones the build was compiled with, so this is the answer
114
+ -- ignition would give; what it would register is only ever skipped there. Recorded, so that
115
+ -- a miss on a class under the folder can say why it is missing.
116
+ if not holdsCondition(options) then
117
+ local _leftOut = self.module.leftOut
118
+ local _arg0 = leftOutRegistration(`registerProviders("{_stringPath}")`, options, {
119
+ path = path,
120
+ })
121
+ table.insert(_leftOut, _arg0)
122
+ return self
123
+ end
110
124
  return self:registerProviderClasses(getClassesInPath(path), options)
111
125
  end
112
126
  function ModuleBuilder:registerProvidersGlob(_glob, options, glob)
113
127
  local _arg0 = glob ~= nil
114
128
  assert(_arg0)
129
+ if not holdsCondition(options) then
130
+ local _leftOut = self.module.leftOut
131
+ local _arg0_1 = leftOutRegistration(`registerProvidersGlob("{_glob}")`, options, {
132
+ glob = glob,
133
+ })
134
+ table.insert(_leftOut, _arg0_1)
135
+ return self
136
+ end
115
137
  return self:registerProviderClasses(getClassesInGlob(glob), options)
116
138
  end
117
139
  function ModuleBuilder:registerProviderClasses(classes, options)