@flamework-experimental/core 2.0.0-alpha.1 → 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.
Files changed (38) hide show
  1. package/README.md +35 -27
  2. package/flamework.build +3 -3
  3. package/out/dependency.d.ts +4 -0
  4. package/out/dependency.luau +4 -0
  5. package/out/flamework.luau +8 -0
  6. package/out/index.d.ts +4 -2
  7. package/out/init.luau +10 -2
  8. package/out/lifecycle/lifecyclePlugin.d.ts +175 -2
  9. package/out/lifecycle/lifecyclePlugin.luau +636 -102
  10. package/out/module/module.luau +337 -41
  11. package/out/module/moduleBuilder.d.ts +12 -3
  12. package/out/module/moduleBuilder.luau +22 -0
  13. package/out/module/moduleDefinition.d.ts +6 -0
  14. package/out/module/providerRegistration.d.ts +8 -0
  15. package/out/module/providerRegistration.luau +29 -0
  16. package/out/plugin/pluginDefinition.d.ts +14 -4
  17. package/out/provider.d.ts +19 -0
  18. package/out/provider.luau +8 -0
  19. package/out/reflect.luau +17 -0
  20. package/out/utility/explainUnresolved.d.ts +9 -0
  21. package/out/utility/explainUnresolved.luau +43 -0
  22. package/out/utility/getClassImplements.d.ts +9 -2
  23. package/out/utility/getClassImplements.luau +89 -6
  24. package/out/utility/getClassesInPath.d.ts +10 -2
  25. package/out/utility/getClassesInPath.luau +70 -31
  26. package/out/utility/globs.d.ts +2 -2
  27. package/out/utility/globs.luau +3 -3
  28. package/out/utility/implementsCache.d.ts +16 -0
  29. package/out/utility/implementsCache.luau +35 -0
  30. package/out/utility/leftOut.d.ts +35 -0
  31. package/out/utility/leftOut.luau +171 -0
  32. package/out/utility/moduleClasses.d.ts +9 -0
  33. package/out/utility/moduleClasses.luau +64 -0
  34. package/out/utility/recycleThread.d.ts +12 -0
  35. package/out/utility/recycleThread.luau +29 -10
  36. package/out/utility/threadWaits.d.ts +36 -0
  37. package/out/utility/threadWaits.luau +81 -0
  38. package/package.json +1 -1
@@ -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
  */
@@ -94,6 +97,13 @@ export interface PluginTarget {
94
97
  onPreIgnite: (callback: (module: Module) => void, options?: HookOptions) => void;
95
98
  /** Runs after every provider has been constructed. */
96
99
  onPostIgnite: (callback: (module: Module) => void, options?: HookOptions) => void;
100
+ /**
101
+ * Runs once ignition has completed: the module is ignited, and its imports count it among
102
+ * their importers. The lifecycle plugin starts the providers here. Nothing can fail the
103
+ * ignition any more, so a hook that raises is warned about and the ones after it still run;
104
+ * once the module has been extinguished -- by an `onStart`, say -- the rest do not run.
105
+ */
106
+ onIgnited: (callback: (module: Module) => void, options?: HookOptions) => void;
97
107
  /** Runs when the module extinguishes, before its providers are released. */
98
108
  onExtinguished: (callback: (module: Module) => void, options?: HookOptions) => void;
99
109
  /**
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,4 +1,7 @@
1
1
  -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local forgetImplements = TS.import(script, script.Parent, "utility", "implementsCache").forgetImplements
4
+ local recordModuleClass = TS.import(script, script.Parent, "utility", "moduleClasses").recordModuleClass
2
5
  --[[
3
6
  *
4
7
  * Reflection/metadata API
@@ -60,6 +63,15 @@ do
60
63
  local _key = key
61
64
  local _value = value
62
65
  metadata[_key] = _value
66
+ -- What `getClassImplements` keeps per class is built from this key.
67
+ if key == "flamework:implements" then
68
+ forgetImplements(obj)
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
63
75
  end
64
76
  _container.defineMetadata = defineMetadata
65
77
  --[[
@@ -72,6 +84,7 @@ do
72
84
  for key, value in pairs(list) do
73
85
  metadata[key] = value
74
86
  end
87
+ forgetImplements(obj)
75
88
  end
76
89
  _container.defineMetadataBatch = defineMetadataBatch
77
90
  --[[
@@ -86,6 +99,9 @@ do
86
99
  local _key = key
87
100
  _result[_key] = nil
88
101
  end
102
+ if key == "flamework:implements" then
103
+ forgetImplements(obj)
104
+ end
89
105
  end
90
106
  _container.deleteMetadata = deleteMetadata
91
107
  --[[
@@ -302,6 +318,7 @@ do
302
318
  local function resetObject(object)
303
319
  local _object = object
304
320
  metadata[_object] = nil
321
+ forgetImplements(object)
305
322
  end
306
323
  _container.resetObject = resetObject
307
324
  end
@@ -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;
@@ -0,0 +1,43 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local Reflect = TS.import(script, script.Parent.Parent, "reflect").Reflect
4
+ local findModuleClass = TS.import(script, script.Parent, "moduleClasses").findModuleClass
5
+ local function describeModule(moduleScript)
6
+ local _moduleScript = moduleScript
7
+ return if typeof(_moduleScript) == "Instance" then moduleScript:GetFullName() else tostring(moduleScript)
8
+ end
9
+ --[[
10
+ *
11
+ * Why a dependency no module resolved cannot be resolved, when its id names a Flamework class that
12
+ * has been loaded -- one a module defined at its top level, which the module record knows -- with
13
+ * what to do about it. Nothing for any other id: an interface, a type registered nowhere, a class
14
+ * defined inside a function or never required, whose failure keeps the plain message.
15
+ *
16
+ * `origin` is the class whose constructor asked, when one did.
17
+
18
+ ]]
19
+ local function explainUnresolvedClass(id, origin)
20
+ local found = findModuleClass(function(value)
21
+ return Reflect.getOwnMetadata(value, "identifier") == id
22
+ end)
23
+ if found == nil then
24
+ return nil
25
+ end
26
+ local _binding = found
27
+ local value = _binding[1]
28
+ local moduleScript = _binding[2]
29
+ local where = describeModule(moduleScript)
30
+ if Reflect.hasOwnMetadata(value, "flamework:provider") then
31
+ return `'{value}' ({where}) is a @Provider() that nothing in this module registers or provides: ` .. "add its folder to registerProviders, register it with registerClassProvider, include the plugin that provides it, " .. "or import a module that has it"
32
+ end
33
+ if Reflect.hasOwnMetadata(value, "flamework:component") then
34
+ if origin ~= nil and Reflect.hasOwnMetadata(origin, "flamework:component") then
35
+ return `'{value}' ({where}) is a component that no ComponentPlugin of this module registers, ` .. `so the component '{origin}' cannot take it: register it in one`
36
+ end
37
+ return `'{value}' ({where}) is a component (@Component), not a provider: a module never constructs a component, ` .. "so Dependency<T>() and constructor injection cannot reach one. Make it a @Provider(), or get it from " .. "Components on the instance it is attached to (components.getComponent<T>(instance))"
38
+ end
39
+ return `'{value}' ({where}) is not a provider: it carries no @Provider() decorator. ` .. "Decorate it with @Provider(), or build it with module.createClassInstance"
40
+ end
41
+ return {
42
+ explainUnresolvedClass = explainUnresolvedClass,
43
+ }
@@ -1,7 +1,14 @@
1
1
  /**
2
- * The interfaces a class implements, own and inherited, each once: the transformer writes every
2
+ * The interfaces an object implements, own and inherited, each once: the transformer writes every
3
3
  * class's own heritage clause, so a subclass that re-declares an interface its parent implements
4
4
  * carries the id twice up the chain, and attached to it twice -- the lifecycle's ordered lists ran
5
5
  * `onInit` and `onStart` twice for it.
6
+ *
7
+ * Walked once per class and kept: this runs on every attach and detach, and behind
8
+ * `Flamework.implements`. An instance, which implements nothing of its own, answers its class's
9
+ * list; so does a class asked directly. An object that implements something of its own -- a
10
+ * `listen` proxy, a plain object given the metadata -- or whose `__index` is not a table is walked
11
+ * every time, as before, rather than kept per object. The list is shared and frozen: callers must
12
+ * not change it.
6
13
  */
7
- export declare function getClassImplements(constructor: object): string[];
14
+ export declare function getClassImplements(object: object): ReadonlyArray<string>;
@@ -1,18 +1,26 @@
1
1
  -- Compiled with roblox-ts v3.0.0
2
2
  local TS = _G[script]
3
3
  local Reflect = TS.import(script, script.Parent.Parent, "reflect").Reflect
4
+ local implementsCache = TS.import(script, script.Parent, "implementsCache").implementsCache
5
+ local IMPLEMENTS = "flamework:implements"
6
+ local EMPTY = table.freeze({})
7
+ --* Where `Reflect.getMetadatas` goes next from an object: its metatable's `__index`, its class.
8
+ local function getParent(object)
9
+ local metatable = getmetatable(object)
10
+ if metatable ~= nil and type(metatable) == "table" then
11
+ return rawget(metatable, "__index")
12
+ end
13
+ end
4
14
  --[[
5
15
  *
6
- * The interfaces a class implements, own and inherited, each once: the transformer writes every
7
- * class's own heritage clause, so a subclass that re-declares an interface its parent implements
8
- * carries the id twice up the chain, and attached to it twice -- the lifecycle's ordered lists ran
9
- * `onInit` and `onStart` twice for it.
16
+ * The walk as `Reflect.getMetadatas` makes it, for an object whose list is not cached: every
17
+ * `flamework:implements` list from the object up its chain, each id once, first seen first.
10
18
 
11
19
  ]]
12
- local function getClassImplements(constructor)
20
+ local function collect(object)
13
21
  local classImplements = {}
14
22
  local seen = {}
15
- for _, implementList in Reflect.getMetadatas(constructor, "flamework:implements") do
23
+ for _, implementList in Reflect.getMetadatas(object, IMPLEMENTS) do
16
24
  for _1, implementId in implementList do
17
25
  if not (seen[implementId] ~= nil) then
18
26
  seen[implementId] = true
@@ -22,6 +30,81 @@ local function getClassImplements(constructor)
22
30
  end
23
31
  return classImplements
24
32
  end
33
+ --[[
34
+ *
35
+ * A class's list, built once: its own ids, then those of the class above it that it does not
36
+ * re-declare, which is the order and the ids `collect` finds. One that declares nothing of its own
37
+ * shares the list above it.
38
+
39
+ ]]
40
+ local function getCachedImplements(object)
41
+ local _byClass = implementsCache.byClass
42
+ local _object = object
43
+ local cached = _byClass[_object]
44
+ if cached ~= nil then
45
+ return cached
46
+ end
47
+ local parent = getParent(object)
48
+ local inherited = if parent ~= nil then getCachedImplements(parent) else EMPTY
49
+ local list = inherited
50
+ local own = Reflect.getOwnMetadata(object, IMPLEMENTS)
51
+ if own ~= nil then
52
+ local merged = {}
53
+ for _, implementId in own do
54
+ if not (table.find(merged, implementId) ~= nil) then
55
+ table.insert(merged, implementId)
56
+ end
57
+ end
58
+ for _, implementId in inherited do
59
+ if not (table.find(merged, implementId) ~= nil) then
60
+ table.insert(merged, implementId)
61
+ end
62
+ end
63
+ list = table.freeze(merged)
64
+ end
65
+ local _byClass_1 = implementsCache.byClass
66
+ local _object_1 = object
67
+ local _list = list
68
+ _byClass_1[_object_1] = _list
69
+ return list
70
+ end
71
+ --[[
72
+ *
73
+ * The interfaces an object implements, own and inherited, each once: the transformer writes every
74
+ * class's own heritage clause, so a subclass that re-declares an interface its parent implements
75
+ * carries the id twice up the chain, and attached to it twice -- the lifecycle's ordered lists ran
76
+ * `onInit` and `onStart` twice for it.
77
+ *
78
+ * Walked once per class and kept: this runs on every attach and detach, and behind
79
+ * `Flamework.implements`. An instance, which implements nothing of its own, answers its class's
80
+ * list; so does a class asked directly. An object that implements something of its own -- a
81
+ * `listen` proxy, a plain object given the metadata -- or whose `__index` is not a table is walked
82
+ * every time, as before, rather than kept per object. The list is shared and frozen: callers must
83
+ * not change it.
84
+
85
+ ]]
86
+ local function getClassImplements(object)
87
+ if Reflect.getOwnMetadata(object, IMPLEMENTS) == nil then
88
+ local parent = getParent(object)
89
+ if parent == nil then
90
+ return EMPTY
91
+ end
92
+ if type(parent) == "table" then
93
+ return getCachedImplements(parent)
94
+ end
95
+ else
96
+ local _object = object
97
+ local _condition = type(_object) == "table"
98
+ if _condition then
99
+ _condition = rawget(object, "__index") == object
100
+ end
101
+ if _condition then
102
+ -- A class: a roblox-ts class is its own instances' `__index`.
103
+ return getCachedImplements(object)
104
+ end
105
+ end
106
+ return collect(object)
107
+ end
25
108
  return {
26
109
  getClassImplements = getClassImplements,
27
110
  }
@@ -14,8 +14,16 @@ export declare function importModule(moduleScript: ModuleScript): defined | unde
14
14
  */
15
15
  export declare function requireModulesInPath(rbxPath: readonly string[]): Array<defined>;
16
16
  /**
17
- * Requires every ModuleScript at and under the specified Rojo path and returns every exported value
18
- * that carries its own Flamework identifier.
17
+ * Requires every ModuleScript at and under the specified Rojo path and returns every Flamework
18
+ * class they hold, each once: every class carrying its own identifier that a module defined at its
19
+ * top level -- exported or not, as v1 registered every decorated class it required -- then every
20
+ * exported value carrying its own identifier that the module did not define there, such as a
21
+ * re-export of a class from elsewhere.
22
+ *
23
+ * A class declared inside a function is not found unless its module exports it: it is created by
24
+ * every call, so the transformer does not record it against its module. Neither is a class
25
+ * compiled by a transformer older than the one that records classes, other than through its
26
+ * module's exports.
19
27
  *
20
28
  * A module that fails to load raises, as it did in v1: a class that silently fails to register
21
29
  * would otherwise only show up later as an unresolvable dependency, far from the cause.
@@ -3,6 +3,7 @@ local TS = _G[script]
3
3
  local tsImport = TS.import(script, script.Parent, "tsImport").tsImport
4
4
  local Reflect = TS.import(script, script.Parent.Parent, "reflect").Reflect
5
5
  local resolveRbxPath = TS.import(script, script.Parent, "pathRoot").resolveRbxPath
6
+ local getModuleClasses = TS.import(script, script.Parent, "moduleClasses").getModuleClasses
6
7
  --[[
7
8
  *
8
9
  * Requires one ModuleScript through the roblox-ts runtime, as an `import` would, and returns what
@@ -20,6 +21,25 @@ local function importModule(moduleScript)
20
21
  end
21
22
  return value
22
23
  end
24
+ --[[
25
+ *
26
+ * Requires every ModuleScript at and under a Rojo path, in tree order, handing each to `visit` with
27
+ * what it exported.
28
+
29
+ ]]
30
+ local function loadModulesInPath(rbxPath, visit)
31
+ local _rbxPath = rbxPath
32
+ assert(_rbxPath)
33
+ local preloadPath = resolveRbxPath(rbxPath)
34
+ if preloadPath:IsA("ModuleScript") then
35
+ visit(preloadPath, importModule(preloadPath))
36
+ end
37
+ for _, instance in preloadPath:GetDescendants() do
38
+ if instance:IsA("ModuleScript") then
39
+ visit(instance, importModule(instance))
40
+ end
41
+ end
42
+ end
23
43
  --[[
24
44
  *
25
45
  * Requires every ModuleScript at and under the specified Rojo path, in tree order, and returns
@@ -32,30 +52,27 @@ end
32
52
 
33
53
  ]]
34
54
  local function requireModulesInPath(rbxPath)
35
- local _rbxPath = rbxPath
36
- assert(_rbxPath)
37
- local preloadPath = resolveRbxPath(rbxPath)
38
55
  local loaded = {}
39
- if preloadPath:IsA("ModuleScript") then
40
- local value = importModule(preloadPath)
56
+ loadModulesInPath(rbxPath, function(_, value)
41
57
  if value ~= nil then
42
- table.insert(loaded, value)
58
+ local _value = value
59
+ table.insert(loaded, _value)
43
60
  end
44
- end
45
- for _, instance in preloadPath:GetDescendants() do
46
- if instance:IsA("ModuleScript") then
47
- local value = importModule(instance)
48
- if value ~= nil then
49
- table.insert(loaded, value)
50
- end
51
- end
52
- end
61
+ end)
53
62
  return loaded
54
63
  end
55
64
  --[[
56
65
  *
57
- * Requires every ModuleScript at and under the specified Rojo path and returns every exported value
58
- * that carries its own Flamework identifier.
66
+ * Requires every ModuleScript at and under the specified Rojo path and returns every Flamework
67
+ * class they hold, each once: every class carrying its own identifier that a module defined at its
68
+ * top level -- exported or not, as v1 registered every decorated class it required -- then every
69
+ * exported value carrying its own identifier that the module did not define there, such as a
70
+ * re-export of a class from elsewhere.
71
+ *
72
+ * A class declared inside a function is not found unless its module exports it: it is created by
73
+ * every call, so the transformer does not record it against its module. Neither is a class
74
+ * compiled by a transformer older than the one that records classes, other than through its
75
+ * module's exports.
59
76
  *
60
77
  * A module that fails to load raises, as it did in v1: a class that silently fails to register
61
78
  * would otherwise only show up later as an unresolvable dependency, far from the cause.
@@ -63,25 +80,47 @@ end
63
80
  ]]
64
81
  local function getClassesInPath(rbxPath)
65
82
  local foundClasses = {}
66
- for _, value in requireModulesInPath(rbxPath) do
67
- if not (type(value) == "table") then
68
- continue
83
+ local found = {}
84
+ -- Own metadata only: an undecorated subclass inherits its parent's identifier, and must not be
85
+ -- mistaken for a registered class of its own.
86
+ local add = function(value)
87
+ local _value = value
88
+ local _condition = type(_value) == "table"
89
+ if _condition then
90
+ local _value_1 = value
91
+ _condition = not (found[_value_1] ~= nil)
92
+ if _condition then
93
+ _condition = Reflect.hasOwnMetadata(value, "identifier")
94
+ end
95
+ end
96
+ if _condition then
97
+ local _value_1 = value
98
+ found[_value_1] = true
99
+ local _value_2 = value
100
+ table.insert(foundClasses, _value_2)
101
+ end
102
+ end
103
+ loadModulesInPath(rbxPath, function(moduleScript, value)
104
+ local defined = getModuleClasses(moduleScript)
105
+ if defined ~= nil then
106
+ for _, value in defined do
107
+ add(value)
108
+ end
109
+ end
110
+ local _value = value
111
+ if not (type(_value) == "table") then
112
+ return nil
69
113
  end
70
114
  -- This is an `export =` on a Flamework class.
71
115
  if Reflect.hasOwnMetadata(value, "identifier") then
72
- table.insert(foundClasses, value)
73
- continue
116
+ add(value)
117
+ return nil
74
118
  end
75
- for _1, member in pairs(value) do
76
- -- This is an `export` on a Flamework class.
77
- --
78
- -- Own metadata only: an undecorated subclass inherits its parent's identifier, and
79
- -- must not be mistaken for a registered class of its own.
80
- if type(member) == "table" and Reflect.hasOwnMetadata(member, "identifier") then
81
- table.insert(foundClasses, member)
82
- end
119
+ -- This is an `export` on a Flamework class.
120
+ for _, member in pairs(value) do
121
+ add(member)
83
122
  end
84
- end
123
+ end)
85
124
  return foundClasses
86
125
  end
87
126
  return {
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export declare function getGlobPaths(glob: string): string[][];
5
5
  /**
6
- * Requires every ModuleScript under every path the glob matched and returns the exported classes,
7
- * each at most once.
6
+ * Requires every ModuleScript under every path the glob matched and returns the Flamework classes
7
+ * they hold, exported or not, each at most once (see {@link getClassesInPath}).
8
8
  */
9
9
  export declare function getClassesInGlob(glob: string): Array<object>;
@@ -37,14 +37,14 @@ local function getGlobPaths(glob)
37
37
  end
38
38
  local paths = _result
39
39
  if paths == nil then
40
- error(`Flamework has no paths for the glob '{glob}'. ` .. "Globs are resolved at compile time into include/flamework/globs.json, which only game projects emit; " .. "make sure the include directory is part of your Rojo project and that the glob matched at least one file.", 0)
40
+ error(`Flamework has no paths for the glob '{glob}'. ` .. "A game's globs are resolved at compile time into include/flamework/globs.json, and this one is not there: " .. "the include folder is not in your Rojo project, the glob is used inside a package (a package's globs " .. "are not resolved), the string was not produced by a glob macro, or globs.json is from another build. " .. "A glob that matched no files does not raise: it resolves to no paths, and the build warns about it.", 0)
41
41
  end
42
42
  return paths
43
43
  end
44
44
  --[[
45
45
  *
46
- * Requires every ModuleScript under every path the glob matched and returns the exported classes,
47
- * each at most once.
46
+ * Requires every ModuleScript under every path the glob matched and returns the Flamework classes
47
+ * they hold, exported or not, each at most once (see {@link getClassesInPath}).
48
48
 
49
49
  ]]
50
50
  local function getClassesInGlob(glob)
@@ -0,0 +1,16 @@
1
+ /**
2
+ * What each class implements, own and inherited, as `getClassImplements` found it, by the class --
3
+ * and by every class above it, which it walked on the way. Its own module, apart from
4
+ * `getClassImplements`, so that `Reflect`, which that one reads through, can reach it too.
5
+ */
6
+ export declare const implementsCache: {
7
+ byClass: WeakMap<object, readonly string[]>;
8
+ };
9
+ /**
10
+ * Forgets every list once an object whose `flamework:implements` metadata one was built from has
11
+ * it changed: the lists of the classes below it were built from it too. The transformer writes a
12
+ * class's metadata as the class is defined, before anything can ask, so this does not happen in a
13
+ * game; it keeps a later change seen as it was before the lists were cached. What `listen` gives
14
+ * its proxies changes nothing here: an object that implements something of its own is not cached.
15
+ */
16
+ export declare function forgetImplements(object: object): void;
@@ -0,0 +1,35 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ --[[
3
+ *
4
+ * What each class implements, own and inherited, as `getClassImplements` found it, by the class --
5
+ * and by every class above it, which it walked on the way. Its own module, apart from
6
+ * `getClassImplements`, so that `Reflect`, which that one reads through, can reach it too.
7
+
8
+ ]]
9
+ local implementsCache = {
10
+ byClass = setmetatable({}, {
11
+ __mode = "k",
12
+ }),
13
+ }
14
+ --[[
15
+ *
16
+ * Forgets every list once an object whose `flamework:implements` metadata one was built from has
17
+ * it changed: the lists of the classes below it were built from it too. The transformer writes a
18
+ * class's metadata as the class is defined, before anything can ask, so this does not happen in a
19
+ * game; it keeps a later change seen as it was before the lists were cached. What `listen` gives
20
+ * its proxies changes nothing here: an object that implements something of its own is not cached.
21
+
22
+ ]]
23
+ local function forgetImplements(object)
24
+ local _byClass = implementsCache.byClass
25
+ local _object = object
26
+ if _byClass[_object] ~= nil then
27
+ implementsCache.byClass = setmetatable({}, {
28
+ __mode = "k",
29
+ })
30
+ end
31
+ end
32
+ return {
33
+ forgetImplements = forgetImplements,
34
+ implementsCache = implementsCache,
35
+ }
@@ -0,0 +1,35 @@
1
+ import { type ScopeCondition } from "../module/scopes";
2
+ /**
3
+ * A path or glob registration that its own scope condition left out. Its folder was never looked up,
4
+ * so nothing under it was loaded, and nothing from it was registered or recorded as inactive: this
5
+ * is what a miss can still say about the classes there.
6
+ */
7
+ export interface LeftOutRegistration {
8
+ /** The call as written, for messages: `registerProviders("src/server/debug")`. */
9
+ readonly call: string;
10
+ /** The registration's own condition, which did not hold. */
11
+ readonly condition: ScopeCondition;
12
+ /** The folder's tree path, for a path registration. */
13
+ readonly path?: readonly string[];
14
+ /** The glob as the runtime keys it, for a glob registration. */
15
+ readonly glob?: string;
16
+ /** What `path` was relative to when the registration was made. */
17
+ readonly root: Instance;
18
+ }
19
+ /**
20
+ * Records a registration left out by its own condition. Reads nothing but the path root, which is
21
+ * found from Flamework's own metadata, never from the folder.
22
+ */
23
+ export declare function leftOutRegistration(call: string, condition: ScopeCondition, folder: {
24
+ path?: readonly string[];
25
+ glob?: string;
26
+ }): LeftOutRegistration;
27
+ /**
28
+ * What a miss on `id` can say about the registrations its module or plugin left out by their own
29
+ * condition, without looking at their folders:
30
+ *
31
+ * - the class has loaded some other way, and its module is under one of them: that one, and why;
32
+ * - the class has not loaded: every one of them, since the class may be under any;
33
+ * - nothing when there are none, or when the loaded class is under none of them.
34
+ */
35
+ export declare function explainLeftOut(id: string, leftOut: ReadonlyArray<LeftOutRegistration>): string | undefined;