@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
@@ -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, `registerProviders("{path}")`), 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)
110
- return self:registerProviderClasses(getClassesInPath(path), options)
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
124
+ return self:registerProviderClasses(getClassesInPath(path, `registerProviders("{_stringPath}")`), 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)
@@ -2,6 +2,7 @@ import type { Modding } from "../modding";
2
2
  import type { PluginDefinition } from "../plugin/pluginDefinition";
3
3
  import { type Module } from "./module";
4
4
  import type { ScopeCondition } from "./scopes";
5
+ import type { LeftOutRegistration } from "../utility/leftOut";
5
6
  /**
6
7
  * Options for one ignition of a module.
7
8
  *
@@ -44,6 +45,11 @@ export interface ModuleState {
44
45
  readonly providers: readonly ModuleProvider[];
45
46
  /** The plugins to set up on ignition, in inclusion order. */
46
47
  readonly plugins: readonly PluginInclusion[];
48
+ /**
49
+ * The path and glob registrations left out by their own scope condition, whose folders were
50
+ * never looked up: what a miss on a class under one of them names.
51
+ */
52
+ readonly leftOut?: readonly LeftOutRegistration[];
47
53
  }
48
54
  export declare class ModuleDefinition {
49
55
  private moduleState;
@@ -20,3 +20,11 @@ export declare function normalizeProviderConfig(config: ProviderConfig): Provide
20
20
  * parent's scope.
21
21
  */
22
22
  export declare function getProviderClassScope(config: ProviderConfig): ScopeCondition;
23
+ /** v1's default, which a provider that does not set `loadOrder` has. */
24
+ export declare const DEFAULT_LOAD_ORDER = 1;
25
+ /**
26
+ * Where a registration sits in its module's startup: its class's `loadOrder`, or the default. A
27
+ * lazy one has none (`undefined`): it starts when it is first resolved, whatever its decorator says.
28
+ * Own metadata, as everywhere else.
29
+ */
30
+ export declare function getProviderLoadOrder(config: ProviderConfig): number | undefined;
@@ -72,9 +72,38 @@ local function getProviderClassScope(config)
72
72
  inactiveIn = decoratorConfig.inactiveIn,
73
73
  }
74
74
  end
75
+ --* v1's default, which a provider that does not set `loadOrder` has.
76
+ local DEFAULT_LOAD_ORDER = 1
77
+ --[[
78
+ *
79
+ * Where a registration sits in its module's startup: its class's `loadOrder`, or the default. A
80
+ * lazy one has none (`undefined`): it starts when it is first resolved, whatever its decorator says.
81
+ * Own metadata, as everywhere else.
82
+
83
+ ]]
84
+ local function getProviderLoadOrder(config)
85
+ if config.type ~= "class" then
86
+ return DEFAULT_LOAD_ORDER
87
+ end
88
+ if config.lazy == true then
89
+ return nil
90
+ end
91
+ local decoratorConfig = Reflect.getOwnMetadata(config.value, "flamework:providerConfig")
92
+ local _result = decoratorConfig
93
+ if _result ~= nil then
94
+ _result = _result.loadOrder
95
+ end
96
+ local _condition = _result
97
+ if _condition == nil then
98
+ _condition = DEFAULT_LOAD_ORDER
99
+ end
100
+ return _condition
101
+ end
75
102
  return {
76
103
  assertIsProviderClass = assertIsProviderClass,
77
104
  getProviderClassId = getProviderClassId,
78
105
  normalizeProviderConfig = normalizeProviderConfig,
79
106
  getProviderClassScope = getProviderClassScope,
107
+ getProviderLoadOrder = getProviderLoadOrder,
108
+ DEFAULT_LOAD_ORDER = DEFAULT_LOAD_ORDER,
80
109
  }
@@ -52,15 +52,18 @@ export interface PluginTarget {
52
52
  */
53
53
  registerClassProvider: (provider: Constructor, options?: ProviderRegistrationOptions) => void;
54
54
  /**
55
- * Registers every exported `@Provider()` class under a source folder, as the module builder's
56
- * `registerProviders` does. This is how a plugin ships a folder of providers. The options apply
57
- * to every class found.
55
+ * Registers every `@Provider()` class the modules under a source folder define, exported or not,
56
+ * as the module builder's `registerProviders` does. This is how a plugin ships a folder of
57
+ * providers. The options apply to every class found; when their scope condition does not hold,
58
+ * the folder is not looked up and nothing under it is required.
58
59
  *
59
60
  * @metadata macro
60
61
  */
61
62
  registerProviders: <T extends string>(path: T, options?: ProviderRegistrationOptions, resolved?: Modding.Intrinsic<"path", [T], string[]>) => void;
62
63
  /**
63
- * Registers every exported `@Provider()` class under every folder a compile-time glob matches.
64
+ * Registers every `@Provider()` class the modules under every folder a compile-time glob matches
65
+ * define, exported or not. As with `registerProviders`, a scope condition that does not hold
66
+ * leaves the folders untouched.
64
67
  *
65
68
  * @metadata macro
66
69
  */
package/out/provider.d.ts CHANGED
@@ -11,6 +11,25 @@ export interface ProviderDecoratorConfig extends ScopeCondition {
11
11
  * Defaults to `false`.
12
12
  */
13
13
  lazy?: boolean;
14
+ /**
15
+ * Orders this provider's `onInit` and `onStart` against the other providers the same ignition
16
+ * constructs, as v1's `loadOrder` did: lower goes first. Defaults to `1`; any finite number,
17
+ * negative and fractional ones included. Providers with the same `loadOrder` keep the order
18
+ * they would have without one.
19
+ *
20
+ * Dependency order still wins. The module constructs its providers in ascending `loadOrder`,
21
+ * each after what its constructor takes, so a provider's dependencies are constructed and
22
+ * initialised before it even when theirs is higher: a low `loadOrder` pulls what the provider
23
+ * needs forward with it. `onInit` runs in that construction order. `onStart` runs in ascending
24
+ * `loadOrder` alone, each on its own thread as always, so a lower one runs up to its first yield
25
+ * before the next is started.
26
+ *
27
+ * Only within one module's ignition: imported modules ignite, and start, before it. Per-frame
28
+ * events (`onTick`, `onPhysics`, `onRender`) stay unordered. A lazy provider is not part of
29
+ * the order: it is initialised and started when it is first resolved, and its `loadOrder` is
30
+ * ignored.
31
+ */
32
+ loadOrder?: number;
14
33
  }
15
34
  /**
16
35
  * Register a class as a provider.
package/out/provider.luau CHANGED
@@ -17,6 +17,14 @@ local Reflect = TS.import(script, script.Parent, "reflect").Reflect
17
17
  ]]
18
18
  local function Provider(config)
19
19
  return function(constructor)
20
+ local _loadOrder = config
21
+ if _loadOrder ~= nil then
22
+ _loadOrder = _loadOrder.loadOrder
23
+ end
24
+ local loadOrder = _loadOrder
25
+ if loadOrder ~= nil and (not (type(loadOrder) == "number") or not (math.abs(loadOrder) < math.huge)) then
26
+ error(`@Provider() on '{constructor}': loadOrder must be a finite number, got {tostring(loadOrder)} ({typeof(loadOrder)})`)
27
+ end
20
28
  Reflect.defineMetadata(constructor, "flamework:provider", true)
21
29
  Reflect.defineMetadata(constructor, "flamework:providerConfig", config or {})
22
30
  end
package/out/reflect.luau CHANGED
@@ -1,6 +1,7 @@
1
1
  -- Compiled with roblox-ts v3.0.0
2
2
  local TS = _G[script]
3
3
  local forgetImplements = TS.import(script, script.Parent, "utility", "implementsCache").forgetImplements
4
+ local recordModuleClass = TS.import(script, script.Parent, "utility", "moduleClasses").recordModuleClass
4
5
  --[[
5
6
  *
6
7
  * Reflection/metadata API
@@ -66,6 +67,11 @@ do
66
67
  if key == "flamework:implements" then
67
68
  forgetImplements(obj)
68
69
  end
70
+ -- The transformer records the module a class was defined in under this key, which is how
71
+ -- path registration finds a class its module does not export.
72
+ if key == "flamework:module" and property == nil then
73
+ recordModuleClass(value, obj)
74
+ end
69
75
  end
70
76
  _container.defineMetadata = defineMetadata
71
77
  --[[
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Why a dependency no module resolved cannot be resolved, when its id names a Flamework class that
3
+ * has been loaded -- one a module defined at its top level, which the module record knows -- with
4
+ * what to do about it. Nothing for any other id: an interface, a type registered nowhere, a class
5
+ * defined inside a function or never required, whose failure keeps the plain message.
6
+ *
7
+ * `origin` is the class whose constructor asked, when one did.
8
+ */
9
+ export declare function explainUnresolvedClass(id: string, origin?: object): string | undefined;