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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +41 -34
  2. package/docs/README.md +63 -0
  3. package/docs/guide/01-getting-started.md +334 -0
  4. package/docs/guide/02-modules.md +254 -0
  5. package/docs/guide/03-providers.md +423 -0
  6. package/docs/guide/04-lifecycle-events.md +423 -0
  7. package/docs/guide/05-components.md +793 -0
  8. package/docs/guide/06-networking.md +614 -0
  9. package/docs/guide/07-macros.md +332 -0
  10. package/docs/guide/08-plugins.md +203 -0
  11. package/docs/guide/09-project-structure.md +392 -0
  12. package/docs/guide/10-migrating-from-v1.md +573 -0
  13. package/docs/guide/11-scopes.md +165 -0
  14. package/docs/guide/12-testing.md +342 -0
  15. package/flamework.build +1 -1
  16. package/out/dependency.d.ts +4 -0
  17. package/out/dependency.luau +4 -0
  18. package/out/index.d.ts +5 -2
  19. package/out/init.luau +11 -2
  20. package/out/lifecycle/lifecyclePlugin.d.ts +13 -1
  21. package/out/lifecycle/lifecyclePlugin.luau +67 -7
  22. package/out/module/module.luau +126 -24
  23. package/out/module/moduleBuilder.d.ts +12 -3
  24. package/out/module/moduleBuilder.luau +23 -1
  25. package/out/module/moduleDefinition.d.ts +6 -0
  26. package/out/module/providerRegistration.d.ts +8 -0
  27. package/out/module/providerRegistration.luau +29 -0
  28. package/out/plugin/pluginDefinition.d.ts +7 -4
  29. package/out/provider.d.ts +19 -0
  30. package/out/provider.luau +8 -0
  31. package/out/reflect.luau +6 -0
  32. package/out/utility/explainUnresolved.d.ts +9 -0
  33. package/out/utility/explainUnresolved.luau +43 -0
  34. package/out/utility/getClassesInPath.d.ts +37 -3
  35. package/out/utility/getClassesInPath.luau +154 -33
  36. package/out/utility/globs.d.ts +2 -2
  37. package/out/utility/globs.luau +3 -3
  38. package/out/utility/leftOut.d.ts +35 -0
  39. package/out/utility/leftOut.luau +171 -0
  40. package/out/utility/moduleClasses.d.ts +9 -0
  41. package/out/utility/moduleClasses.luau +64 -0
  42. package/out/utility/pathRoot.d.ts +20 -1
  43. package/out/utility/pathRoot.luau +80 -4
  44. package/package.json +14 -7
@@ -0,0 +1,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,3 +1,4 @@
1
+ import type { Modding } from "../modding";
1
2
  /**
2
3
  * Requires one ModuleScript through the roblox-ts runtime, as an `import` would, and returns what
3
4
  * it exported. A module that fails to load raises with its full name and how long it took.
@@ -14,10 +15,43 @@ export declare function importModule(moduleScript: ModuleScript): defined | unde
14
15
  */
15
16
  export declare function requireModulesInPath(rbxPath: readonly string[]): Array<defined>;
16
17
  /**
17
- * Requires every ModuleScript at and under the specified Rojo path and returns every exported value
18
- * that carries its own Flamework identifier.
18
+ * Requires every ModuleScript at and under a source folder, in tree order, for what the modules do
19
+ * as they load, and returns what each exported, leaving out the ones that exported nothing. This is
20
+ * v1's `Flamework.addPaths` for a folder that holds no providers: modules that register themselves
21
+ * with a library, such as commands.
22
+ *
23
+ * The path is a source path, as a string literal: `requireModules("src/server/commands")`. The build
24
+ * turns it into the Rojo path the folder ends up at, as it does for `registerProviders`, so the
25
+ * folder has to be in your Rojo project.
26
+ *
27
+ * Each module is required through the roblox-ts module cache, so it runs once, however many times
28
+ * it is required. A folder inside a folder that `registerProviders` registers needs no call:
29
+ * registration already requires every ModuleScript under it.
30
+ *
31
+ * Raises when a module fails to load, and when the folder is not in the place. A missing folder is
32
+ * waited for five seconds, once the place has loaded, and the error names the part that is missing.
33
+ * A folder of the other realm's -- a server folder on a client, a client folder on the server --
34
+ * raises at once, saying so.
35
+ *
36
+ * @metadata macro
37
+ */
38
+ export declare function requireModules<T extends string>(path: T, rbxPath?: Modding.Intrinsic<"path", [T], string[]>): Array<defined>;
39
+ /**
40
+ * Requires every ModuleScript at and under the specified Rojo path and returns every Flamework
41
+ * class they hold, each once: every class carrying its own identifier that a module defined at its
42
+ * top level -- exported or not, as v1 registered every decorated class it required -- then every
43
+ * exported value carrying its own identifier that the module did not define there, such as a
44
+ * re-export of a class from elsewhere.
45
+ *
46
+ * A class declared inside a function is not found unless its module exports it: it is created by
47
+ * every call, so the transformer does not record it against its module. Neither is a class
48
+ * compiled by a transformer older than the one that records classes, other than through its
49
+ * module's exports.
19
50
  *
20
51
  * A module that fails to load raises, as it did in v1: a class that silently fails to register
21
52
  * would otherwise only show up later as an unresolvable dependency, far from the cause.
53
+ *
54
+ * The folder is waited for as {@link resolveRbxPath} waits: `caller`, the registration that gave the
55
+ * path (`registerProviders("src/server/services")`), is named in the warning a slow wait gets.
22
56
  */
23
- export declare function getClassesInPath(rbxPath: readonly string[]): Array<object>;
57
+ export declare function getClassesInPath(rbxPath: readonly string[], caller?: string): Array<object>;
@@ -1,8 +1,34 @@
1
1
  -- Compiled with roblox-ts v3.0.0
2
2
  local TS = _G[script]
3
+ local RunService = TS.import(script, TS.getModule(script, "@rbxts", "services")).RunService
3
4
  local tsImport = TS.import(script, script.Parent, "tsImport").tsImport
4
5
  local Reflect = TS.import(script, script.Parent.Parent, "reflect").Reflect
5
- local resolveRbxPath = TS.import(script, script.Parent, "pathRoot").resolveRbxPath
6
+ local _pathRoot = TS.import(script, script.Parent, "pathRoot")
7
+ local findRbxPath = _pathRoot.findRbxPath
8
+ local getPathRoot = _pathRoot.getPathRoot
9
+ local resolveRbxPath = _pathRoot.resolveRbxPath
10
+ local getModuleClasses = TS.import(script, script.Parent, "moduleClasses").getModuleClasses
11
+ --* How long {@link requireModules} waits for a folder that is not in the place, in seconds.
12
+ local MISSING_FOLDER_TIMEOUT = 5
13
+ --[[
14
+ *
15
+ * Why this realm cannot require a folder under `game` that belongs to the other realm, or nothing
16
+ * when it can: a client sees nothing of the server's containers, and the server has no
17
+ * `PlayerScripts` to take a client folder from.
18
+
19
+ ]]
20
+ local function otherRealmReason(rbxPath)
21
+ if getPathRoot() ~= game then
22
+ return nil
23
+ end
24
+ local service = rbxPath[1]
25
+ if RunService:IsClient() and (service == "ServerScriptService" or service == "ServerStorage") then
26
+ return `the folder is in {service}, which does not replicate to clients. Call requireModules for it on the server`
27
+ end
28
+ if not RunService:IsClient() and service == "StarterPlayer" then
29
+ return "the folder is in StarterPlayer/StarterPlayerScripts, which only a client requires from, through its PlayerScripts. Call requireModules for it on the client"
30
+ end
31
+ end
6
32
  --[[
7
33
  *
8
34
  * Requires one ModuleScript through the roblox-ts runtime, as an `import` would, and returns what
@@ -20,6 +46,44 @@ local function importModule(moduleScript)
20
46
  end
21
47
  return value
22
48
  end
49
+ --[[
50
+ *
51
+ * Requires every ModuleScript at and under an instance, in tree order, handing each to `visit` with
52
+ * what it exported.
53
+
54
+ ]]
55
+ local function loadModulesIn(root, visit)
56
+ if root:IsA("ModuleScript") then
57
+ visit(root, importModule(root))
58
+ end
59
+ for _, instance in root:GetDescendants() do
60
+ if instance:IsA("ModuleScript") then
61
+ visit(instance, importModule(instance))
62
+ end
63
+ end
64
+ end
65
+ --[[
66
+ *
67
+ * Requires every ModuleScript at and under a Rojo path, in tree order, handing each to `visit` with
68
+ * what it exported. `caller` names the call that gave the path, for the warning a slow wait gets.
69
+
70
+ ]]
71
+ local function loadModulesInPath(rbxPath, visit, caller)
72
+ local _rbxPath = rbxPath
73
+ assert(_rbxPath)
74
+ loadModulesIn(resolveRbxPath(rbxPath, caller), visit)
75
+ end
76
+ --* What every ModuleScript at and under an instance exported, leaving out the ones that exported nothing.
77
+ local function requireModulesIn(root)
78
+ local loaded = {}
79
+ loadModulesIn(root, function(_, value)
80
+ if value ~= nil then
81
+ local _value = value
82
+ table.insert(loaded, _value)
83
+ end
84
+ end)
85
+ return loaded
86
+ end
23
87
  --[[
24
88
  *
25
89
  * Requires every ModuleScript at and under the specified Rojo path, in tree order, and returns
@@ -34,58 +98,115 @@ end
34
98
  local function requireModulesInPath(rbxPath)
35
99
  local _rbxPath = rbxPath
36
100
  assert(_rbxPath)
37
- local preloadPath = resolveRbxPath(rbxPath)
38
- local loaded = {}
39
- if preloadPath:IsA("ModuleScript") then
40
- local value = importModule(preloadPath)
41
- if value ~= nil then
42
- table.insert(loaded, value)
43
- end
101
+ return requireModulesIn(resolveRbxPath(rbxPath))
102
+ end
103
+ --[[
104
+ *
105
+ * Requires every ModuleScript at and under a source folder, in tree order, for what the modules do
106
+ * as they load, and returns what each exported, leaving out the ones that exported nothing. This is
107
+ * v1's `Flamework.addPaths` for a folder that holds no providers: modules that register themselves
108
+ * with a library, such as commands.
109
+ *
110
+ * The path is a source path, as a string literal: `requireModules("src/server/commands")`. The build
111
+ * turns it into the Rojo path the folder ends up at, as it does for `registerProviders`, so the
112
+ * folder has to be in your Rojo project.
113
+ *
114
+ * Each module is required through the roblox-ts module cache, so it runs once, however many times
115
+ * it is required. A folder inside a folder that `registerProviders` registers needs no call:
116
+ * registration already requires every ModuleScript under it.
117
+ *
118
+ * Raises when a module fails to load, and when the folder is not in the place. A missing folder is
119
+ * waited for five seconds, once the place has loaded, and the error names the part that is missing.
120
+ * A folder of the other realm's -- a server folder on a client, a client folder on the server --
121
+ * raises at once, saying so.
122
+ *
123
+ * @metadata macro
124
+
125
+ ]]
126
+ local function requireModules(path, rbxPath)
127
+ if rbxPath == nil then
128
+ error(`requireModules("{path}") was called without the folder's Rojo path, which the build fills in. ` .. "Call it directly, with a string literal, in code that the Flamework transformer compiles.", 2)
44
129
  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
130
+ local otherRealm = otherRealmReason(rbxPath)
131
+ if otherRealm ~= nil then
132
+ error(`requireModules("{path}"): {otherRealm}.`, 2)
52
133
  end
53
- return loaded
134
+ local _binding = findRbxPath(rbxPath, MISSING_FOLDER_TIMEOUT)
135
+ local found = _binding.found
136
+ local missing = _binding.missing
137
+ if missing ~= nil then
138
+ error(`requireModules("{path}"): the folder is not in the place. The build put it at {table.concat(rbxPath, "/")}, ` .. `and {found:GetFullName()} has no child named '{missing}' after {MISSING_FOLDER_TIMEOUT} seconds. ` .. "The path is misspelled or differs in case from the folder, or the folder is empty and missing from this " .. "clone, since git keeps no empty folder (the build warns about these where the path is used); or it was " .. "moved or renamed after the build; or the Rojo project the place was built from leaves it out.", 2)
139
+ end
140
+ return requireModulesIn(found)
54
141
  end
55
142
  --[[
56
143
  *
57
- * Requires every ModuleScript at and under the specified Rojo path and returns every exported value
58
- * that carries its own Flamework identifier.
144
+ * Requires every ModuleScript at and under the specified Rojo path and returns every Flamework
145
+ * class they hold, each once: every class carrying its own identifier that a module defined at its
146
+ * top level -- exported or not, as v1 registered every decorated class it required -- then every
147
+ * exported value carrying its own identifier that the module did not define there, such as a
148
+ * re-export of a class from elsewhere.
149
+ *
150
+ * A class declared inside a function is not found unless its module exports it: it is created by
151
+ * every call, so the transformer does not record it against its module. Neither is a class
152
+ * compiled by a transformer older than the one that records classes, other than through its
153
+ * module's exports.
59
154
  *
60
155
  * A module that fails to load raises, as it did in v1: a class that silently fails to register
61
156
  * would otherwise only show up later as an unresolvable dependency, far from the cause.
157
+ *
158
+ * The folder is waited for as {@link resolveRbxPath} waits: `caller`, the registration that gave the
159
+ * path (`registerProviders("src/server/services")`), is named in the warning a slow wait gets.
62
160
 
63
161
  ]]
64
- local function getClassesInPath(rbxPath)
162
+ local function getClassesInPath(rbxPath, caller)
65
163
  local foundClasses = {}
66
- for _, value in requireModulesInPath(rbxPath) do
67
- if not (type(value) == "table") then
68
- continue
164
+ local found = {}
165
+ -- Own metadata only: an undecorated subclass inherits its parent's identifier, and must not be
166
+ -- mistaken for a registered class of its own.
167
+ local add = function(value)
168
+ local _value = value
169
+ local _condition = type(_value) == "table"
170
+ if _condition then
171
+ local _value_1 = value
172
+ _condition = not (found[_value_1] ~= nil)
173
+ if _condition then
174
+ _condition = Reflect.hasOwnMetadata(value, "identifier")
175
+ end
176
+ end
177
+ if _condition then
178
+ local _value_1 = value
179
+ found[_value_1] = true
180
+ local _value_2 = value
181
+ table.insert(foundClasses, _value_2)
182
+ end
183
+ end
184
+ loadModulesInPath(rbxPath, function(moduleScript, value)
185
+ local defined = getModuleClasses(moduleScript)
186
+ if defined ~= nil then
187
+ for _, value in defined do
188
+ add(value)
189
+ end
190
+ end
191
+ local _value = value
192
+ if not (type(_value) == "table") then
193
+ return nil
69
194
  end
70
195
  -- This is an `export =` on a Flamework class.
71
196
  if Reflect.hasOwnMetadata(value, "identifier") then
72
- table.insert(foundClasses, value)
73
- continue
197
+ add(value)
198
+ return nil
74
199
  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
200
+ -- This is an `export` on a Flamework class.
201
+ for _, member in pairs(value) do
202
+ add(member)
83
203
  end
84
- end
204
+ end, caller)
85
205
  return foundClasses
86
206
  end
87
207
  return {
88
208
  importModule = importModule,
89
209
  requireModulesInPath = requireModulesInPath,
210
+ requireModules = requireModules,
90
211
  getClassesInPath = getClassesInPath,
91
212
  }
@@ -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,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;
@@ -0,0 +1,171 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local describeConditions = TS.import(script, script.Parent.Parent, "module", "scopes").describeConditions
4
+ local Reflect = TS.import(script, script.Parent.Parent, "reflect").Reflect
5
+ local getGlobPaths = TS.import(script, script.Parent, "globs").getGlobPaths
6
+ local findModuleClass = TS.import(script, script.Parent, "moduleClasses").findModuleClass
7
+ local getPathRoot = TS.import(script, script.Parent, "pathRoot").getPathRoot
8
+ --[[
9
+ *
10
+ * A path or glob registration that its own scope condition left out. Its folder was never looked up,
11
+ * so nothing under it was loaded, and nothing from it was registered or recorded as inactive: this
12
+ * is what a miss can still say about the classes there.
13
+
14
+ ]]
15
+ --[[
16
+ *
17
+ * Records a registration left out by its own condition. Reads nothing but the path root, which is
18
+ * found from Flamework's own metadata, never from the folder.
19
+
20
+ ]]
21
+ local function leftOutRegistration(call, condition, folder)
22
+ return {
23
+ call = call,
24
+ condition = {
25
+ activeIn = condition.activeIn,
26
+ inactiveIn = condition.inactiveIn,
27
+ },
28
+ path = folder.path,
29
+ glob = folder.glob,
30
+ root = getPathRoot(),
31
+ }
32
+ end
33
+ --[[
34
+ *
35
+ * The tree path of a loaded module below `root`, in the form a compile-time path has: a client's
36
+ * `PlayerScripts` is written as `StarterPlayer/StarterPlayerScripts`, where its content comes from.
37
+ * Nothing when the module is not below `root`.
38
+
39
+ ]]
40
+ local function treePathOf(moduleScript, root)
41
+ local names = {}
42
+ local current = moduleScript
43
+ while current ~= nil and current ~= root do
44
+ local _name = current.Name
45
+ table.insert(names, 1, _name)
46
+ current = current.Parent
47
+ end
48
+ if current == nil then
49
+ return nil
50
+ end
51
+ if root == game and names[1] == "Players" and names[3] == "PlayerScripts" then
52
+ -- ▼ ReadonlyArray.filter ▼
53
+ local _newValue = {}
54
+ local _callback = function(_, index)
55
+ return index >= 3
56
+ end
57
+ local _length = 0
58
+ for _k, _v in names do
59
+ if _callback(_v, _k - 1, names) == true then
60
+ _length += 1
61
+ _newValue[_length] = _v
62
+ end
63
+ end
64
+ -- ▲ ReadonlyArray.filter ▲
65
+ local rest = _newValue
66
+ local _array = { "StarterPlayer", "StarterPlayerScripts" }
67
+ local _length_1 = #_array
68
+ table.move(rest, 1, #rest, _length_1 + 1, _array)
69
+ return _array
70
+ end
71
+ return names
72
+ end
73
+ local function startsWith(path, prefix)
74
+ if #path < #prefix then
75
+ return false
76
+ end
77
+ for i = 0, #prefix - 1 do
78
+ if path[i + 1] ~= prefix[i + 1] then
79
+ return false
80
+ end
81
+ end
82
+ return true
83
+ end
84
+ --* Whether a loaded module is under the folder, or one of the folders, a left-out registration named.
85
+ local function isUnder(moduleScript, registration)
86
+ local modulePath = treePathOf(moduleScript, registration.root)
87
+ if modulePath == nil then
88
+ return false
89
+ end
90
+ if registration.path ~= nil then
91
+ return startsWith(modulePath, registration.path)
92
+ end
93
+ if registration.glob ~= nil then
94
+ -- `globs.json`, not the folders: what the glob matched when the build was compiled.
95
+ local ok, paths = pcall(getGlobPaths, registration.glob)
96
+ if not ok then
97
+ return false
98
+ end
99
+ -- ▼ ReadonlyArray.some ▼
100
+ local _result = false
101
+ local _callback = function(path)
102
+ return startsWith(modulePath, path)
103
+ end
104
+ for _k, _v in paths do
105
+ if _callback(_v, _k - 1, paths) then
106
+ _result = true
107
+ break
108
+ end
109
+ end
110
+ -- ▲ ReadonlyArray.some ▲
111
+ return _result
112
+ end
113
+ return false
114
+ end
115
+ --[[
116
+ *
117
+ * What a miss on `id` can say about the registrations its module or plugin left out by their own
118
+ * condition, without looking at their folders:
119
+ *
120
+ * - the class has loaded some other way, and its module is under one of them: that one, and why;
121
+ * - the class has not loaded: every one of them, since the class may be under any;
122
+ * - nothing when there are none, or when the loaded class is under none of them.
123
+
124
+ ]]
125
+ local function explainLeftOut(id, leftOut)
126
+ if #leftOut == 0 then
127
+ return nil
128
+ end
129
+ local advice = "the build's scopes so that the condition holds, or do not depend on it in this build"
130
+ local found = findModuleClass(function(value)
131
+ return Reflect.getOwnMetadata(value, "identifier") == id
132
+ end)
133
+ if found ~= nil then
134
+ local _binding = found
135
+ local value = _binding[1]
136
+ local moduleScript = _binding[2]
137
+ -- ▼ ReadonlyArray.find ▼
138
+ local _callback = function(registration)
139
+ return isUnder(moduleScript, registration)
140
+ end
141
+ local _result
142
+ for _i, _v in leftOut do
143
+ if _callback(_v, _i - 1, leftOut) == true then
144
+ _result = _v
145
+ break
146
+ end
147
+ end
148
+ -- ▲ ReadonlyArray.find ▲
149
+ local under = _result
150
+ if under == nil then
151
+ return nil
152
+ end
153
+ local where = if typeof(moduleScript) == "Instance" then moduleScript:GetFullName() else tostring(moduleScript)
154
+ return `'{value}' ({where}) is under {under.call}, which is left out by its scope ` .. `({describeConditions({ under.condition })}): nothing under it is registered. Change {advice}`
155
+ end
156
+ -- ▼ ReadonlyArray.map ▼
157
+ local _newValue = table.create(#leftOut)
158
+ local _callback = function(v)
159
+ return `{v.call} ({describeConditions({ v.condition })})`
160
+ end
161
+ for _k, _v in leftOut do
162
+ _newValue[_k] = _callback(_v, _k - 1, leftOut)
163
+ end
164
+ -- ▲ ReadonlyArray.map ▲
165
+ local listed = table.concat(_newValue, ", ")
166
+ return `nothing registers it. Left out by their scope, without loading their folders: {listed}. ` .. `If it is defined under one of them, change {advice}`
167
+ end
168
+ return {
169
+ leftOutRegistration = leftOutRegistration,
170
+ explainLeftOut = explainLeftOut,
171
+ }
@@ -0,0 +1,9 @@
1
+ /** Records a class against the module that defined it. Called by `Reflect.defineMetadata`. */
2
+ export declare function recordModuleClass(moduleScript: unknown, value: object): void;
3
+ /** The classes a module defined as it loaded, in definition order, or none. */
4
+ export declare function getModuleClasses(moduleScript: unknown): ReadonlyArray<object> | undefined;
5
+ /**
6
+ * Every recorded class, with the module that defined it, until the callback answers `true`. For
7
+ * explaining a failed resolution, which is rare: nothing here is indexed for it.
8
+ */
9
+ export declare function findModuleClass(predicate: (value: object) => boolean): [object, unknown] | undefined;
@@ -0,0 +1,64 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ --[[
3
+ *
4
+ * The Flamework classes each module defined as it loaded, by the module's `script`, in the order
5
+ * they were defined: what path registration finds in a module besides what it exports.
6
+ *
7
+ * The transformer records a class with `Reflect.defineMetadata(class, "flamework:module", script)`,
8
+ * and only a class the module creates once per load -- declared at the top level of the file, or
9
+ * of a namespace in it. A class declared inside a function is created by every call and is never
10
+ * recorded, so nothing here grows with calls, and a later path registration never finds a class
11
+ * that belongs to a test case or a factory. Keyed by the ModuleScript rather than read off the
12
+ * class's identifier, which says nothing about where the class came from once ids are short, tiny
13
+ * or obfuscated.
14
+ *
15
+ * Held for good, as the module's own exports are held by the require cache: a module loads once.
16
+
17
+ ]]
18
+ local classesByModule = {}
19
+ --* Records a class against the module that defined it. Called by `Reflect.defineMetadata`.
20
+ local function recordModuleClass(moduleScript, value)
21
+ if moduleScript == nil then
22
+ return nil
23
+ end
24
+ local _moduleScript = moduleScript
25
+ local classes = classesByModule[_moduleScript]
26
+ if classes == nil then
27
+ classes = {}
28
+ local _moduleScript_1 = moduleScript
29
+ local _classes = classes
30
+ classesByModule[_moduleScript_1] = _classes
31
+ end
32
+ local _classes = classes
33
+ local _value = value
34
+ if not (table.find(_classes, _value) ~= nil) then
35
+ local _classes_1 = classes
36
+ local _value_1 = value
37
+ table.insert(_classes_1, _value_1)
38
+ end
39
+ end
40
+ --* The classes a module defined as it loaded, in definition order, or none.
41
+ local function getModuleClasses(moduleScript)
42
+ local _moduleScript = moduleScript
43
+ return classesByModule[_moduleScript]
44
+ end
45
+ --[[
46
+ *
47
+ * Every recorded class, with the module that defined it, until the callback answers `true`. For
48
+ * explaining a failed resolution, which is rare: nothing here is indexed for it.
49
+
50
+ ]]
51
+ local function findModuleClass(predicate)
52
+ for moduleScript, classes in classesByModule do
53
+ for _, value in classes do
54
+ if predicate(value) then
55
+ return { value, moduleScript }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ return {
61
+ recordModuleClass = recordModuleClass,
62
+ getModuleClasses = getModuleClasses,
63
+ findModuleClass = findModuleClass,
64
+ }
@@ -13,5 +13,24 @@ export declare function getPathRoot(): Instance;
13
13
  *
14
14
  * Under `game` the first segment names a service, and `StarterPlayer/StarterPlayerScripts` is
15
15
  * answered from the local player's `PlayerScripts`, which is where that content actually runs.
16
+ *
17
+ * A child that is not there within five seconds (on a client, once the place has loaded) is warned
18
+ * about, naming `caller` -- the call that gave the path, such as `registerProviders("src/shared/components")`
19
+ * -- and the child missing, and then waited for without a limit: content that arrives late still
20
+ * resolves, as it always has. The warning comes once per path: a child further down that is late
21
+ * too is the same wait, and is waited for without a second one.
22
+ */
23
+ export declare function resolveRbxPath(rbxPath: readonly string[], caller?: string): Instance;
24
+ /**
25
+ * Walks a compile-time path as {@link resolveRbxPath} does, but gives up on a child that is not
26
+ * there instead of waiting for it forever. A client still loading the place waits for it to load
27
+ * first, since the place's content arrives as it loads; after that, a missing child is given
28
+ * `timeout` seconds to appear.
29
+ *
30
+ * Returns the instance the path names, or the deepest instance it found and the name missing below
31
+ * it.
16
32
  */
17
- export declare function resolveRbxPath(rbxPath: readonly string[]): Instance;
33
+ export declare function findRbxPath(rbxPath: readonly string[], timeout: number): {
34
+ found: Instance;
35
+ missing?: string;
36
+ };