forked from Quenty/NevermoreEngine
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Install.lua
381 lines (314 loc) · 9.63 KB
/
Install.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
-- Nevermore installer script
--
-- Reads Github html and then reifies the structure into Roblox instances.
-- Makes assumptions based upon the name of the files as-to what type it is.
-- Generally follows the rojo standard format for client/server.
--
-- @script Install.lua
-- @author Quenty
-- luacheck: no max line length
local HttpService = game:GetService("HttpService")
local Table = {}
do
local function errorOnIndex(self, index)
error(("Bad index %q"):format(tostring(index)), 2)
end
local READ_ONLY_METATABLE = {
__index = errorOnIndex;
__newindex = errorOnIndex;
}
function Table.readonly(_table)
return setmetatable(_table, READ_ONLY_METATABLE)
end
function Table.merge(orig, new)
local _table = {}
for key, val in pairs(orig) do
_table[key] = val
end
for key, val in pairs(new) do
_table[key] = val
end
return _table
end
end
local Http = {}
do
function Http.getAsync(url)
local response = HttpService:RequestAsync({
Url = url;
Method = "GET";
})
if response.Success then
return response.Body
else
warn(("%d - %q - While retrieving %q"):format(response.StatusCode, response.StatusMessage, url))
return nil
end
end
end
local String = {}
do
--- Escapes a lua pattern
function String.escapeAll(pattern)
-- The following characters escaped: ( ) . % + - * ? [ ^ $
local UNSAFE_CHARACTERS_MATCH = "[%(%)%.%%%+%-%*%?%[%^%$]"
local result = pattern:gsub(UNSAFE_CHARACTERS_MATCH, "%%%1")
return result
end
--- Only escapes the percent
function String.escapePercent(pattern)
local UNSAFE_CHARACTERS_MATCH = "%%"
local result = pattern:gsub(UNSAFE_CHARACTERS_MATCH, "%%%1")
return result
end
-- Takes a very simple HTML set and tries to transform it into a safe pattern to capture
function String.patternFromExample(example, captures)
local pattern = String.escapeAll(example)
for sample, capture in pairs(captures) do
pattern = pattern:gsub(String.escapeAll(sample), String.escapePercent(capture))
end
return pattern
end
function String.endsWith(str, postfix)
return str:sub(-#postfix) == postfix
end
function String.startsWith(str, prefix)
return str:sub(1, #prefix) == prefix
end
function String.withoutPrefix(str, prefix)
if String.startsWith(str, prefix) then
return str:sub(#prefix + 1)
else
return str
end
end
function String.withoutPostfix(str, postfix)
if String.endsWith(str, postfix) then
return str:sub(1, -#postfix - 1)
else
return str
end
end
end
local ENTRY_TYPES = Table.readonly({
Script = "Script";
ModuleScript = "ModuleScript";
LocalScript = "LocalScript";
Folder = "Folder";
Markdown = "Markdown";
})
local EntryUtils = {}
do
function EntryUtils.classifyByName(name)
if String.endsWith(name, ".client.lua") then
return ENTRY_TYPES.LocalScript
elseif String.endsWith(name, ".server.lua") then
return ENTRY_TYPES.Script
elseif String.endsWith(name, ".lua") then
return ENTRY_TYPES.ModuleScript
elseif String.endsWith(name, ".md") then
return ENTRY_TYPES.Markdown
else
return ENTRY_TYPES.Folder
end
end
function EntryUtils.getNameFromClass(name, entryType)
if entryType == ENTRY_TYPES.LocalScript then
return String.withoutPostfix(name, ".client.lua")
elseif entryType == ENTRY_TYPES.Script then
return String.withoutPostfix(name, ".server.lua")
elseif entryType == ENTRY_TYPES.ModuleScript then
return String.withoutPostfix(name, ".lua")
elseif entryType == ENTRY_TYPES.Markdown then
return String.withoutPostfix(name, ".md")
elseif entryType == ENTRY_TYPES.Folder then
return name
else
error("Unknown entryType")
end
end
function EntryUtils.create(className, name, canonicalPath, children)
assert(className)
assert(name)
assert(canonicalPath)
return Table.readonly({
className = className;
name = name;
canonicalPath = canonicalPath;
children = children or {};
properties = {};
})
end
function EntryUtils.mount(parent, entry)
assert(typeof(parent) == "Instance")
assert(type(entry) == "table")
assert(type(entry.name) == "string")
-- No way to mount markdown files
if entry.className == ENTRY_TYPES.Markdown then
return
end
local childrenToForward = {}
local function addChildren(from)
for _, item in pairs(from:GetChildren()) do
table.insert(childrenToForward, item)
end
end
local found
for _, item in pairs(parent:GetChildren()) do
if item.Name == entry.name then
if not found then
found = item
else
warn(("[EntryUtils.mount] - Duplicate of %q")
:format(item:GetFullName()))
addChildren(item)
item:Remove()
end
end
end
if found and (not found:IsA(entry.className)) then
warn(("[EntryUtils.mount] - Changing %q from type %q to type %q")
:format(found:GetFullName(), found.ClassName, entry.className))
addChildren(found)
found:Remove()
found = nil
end
if not found then
found = Instance.new(entry.className)
found.Name = entry.name
end
for property, value in pairs(entry.properties) do
found[property] = value
end
for _, item in pairs(childrenToForward) do
item.Parent = found
end
for _, childEntry in pairs(entry.children) do
EntryUtils.mount(found, childEntry)
end
found.Parent = parent
return found
end
end
local ParseUtils = {}
do
local EMPTY_ITERATOR = coroutine.wrap(function() end)
local CONTENTS_PATTERN = String.patternFromExample([[<span class="css-truncate css-truncate-target d-block width-fit"><a class="js-navigation-open Link--primary" title="Server" data-pjax="#repo-content-pjax-container" href="/Quenty/NevermoreEngine/tree/version2/Modules/Server">Server</a></span>]], {
["\"Server\""] = "\"([^\"]+)\"",
[">Server<"] = ">[^<]+<",
["\"/Quenty/NevermoreEngine/tree/version2/Modules/Server\""] = "\"[^\"]+\"",
[" "] = "%s+"
})
function ParseUtils.parseContents(body, pattern)
assert(pattern)
if not body then
return EMPTY_ITERATOR
end
return body:gmatch(pattern)
end
function ParseUtils.readContentsAsync(url)
local body = Http.getAsync(url)
return ParseUtils.parseContents(body, CONTENTS_PATTERN)
end
function ParseUtils.readEntriesAsync(url, basePath)
assert(url)
assert(basePath)
local entries = {}
for fileName in ParseUtils.readContentsAsync(url) do
local className = EntryUtils.classifyByName(fileName)
local name = EntryUtils.getNameFromClass(fileName, className)
local path = basePath .. "/" .. fileName
table.insert(entries, EntryUtils.create(className, name, path))
end
return entries
end
function ParseUtils.replaceEntryWithTopLevelChild(entry)
if entry.className ~= ENTRY_TYPES.Folder then
return
end
local index = nil
for childIndex, child in pairs(entry.children) do
if child.name == "init" then
if not index then
index = childIndex
else
warn("[ParseUtils.replaceEntryWithTopLevelChild] - Multiple top level children named 'init'. Using first.")
end
end
end
if not index then
return
end
local child = assert(entry.children[index])
table.remove(entry.children, index)
entry.className = child.className
entry.canonicalPath = child.canonicalPath
entry.properties = Table.merge(entry.properties, child.properties)
end
function ParseUtils.shouldRetrieveSource(entry)
return entry.className == ENTRY_TYPES.Script
or entry.className == ENTRY_TYPES.ModuleScript
or entry.className == ENTRY_TYPES.LocalScript
end
function ParseUtils.fillScriptSourcesAsync(baseUrl, entry)
if ParseUtils.shouldRetrieveSource(entry) then
local url = baseUrl .. entry.canonicalPath
print(("Retrieving source %q"):format(url))
local body = Http.getAsync(url)
if not body then
warn(("[ParseUtils.fillScriptSourcesAsync] - Failed to find source of %q at %q")
:format(entry.name, url))
return
end
entry.properties["Source"] = body
end
for _, childEntry in pairs(entry.children) do
-- Recurse!
ParseUtils.fillScriptSourcesAsync(baseUrl, childEntry)
end
end
function ParseUtils.fillFoldersAsync(url, entry)
print(("Retrieving %q"):format(url))
entry.children = ParseUtils.readEntriesAsync(url, entry.canonicalPath)
ParseUtils.replaceEntryWithTopLevelChild(entry)
for _, childEntry in pairs(entry.children) do
if childEntry.className == ENTRY_TYPES.Folder then
-- Recurse!
ParseUtils.fillFoldersAsync(
url .. "/" .. childEntry.name,
childEntry)
end
end
end
function ParseUtils.githubContentFromUrl(url)
if not String.startsWith(url, "https://github.com/") then
error("Not a github URL")
return
end
local stripped = String.withoutPrefix(url, "https://github.com/")
-- also remove random /tree/
-- pattern: username/repository/tree/...
stripped = stripped:gsub("^(%w+/%w+)/tree/(.*)$", "%1/%2")
return "https://raw.githubusercontent.com/" .. stripped
end
end
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
-- Mount loader
do
local url = "https://github.com/Quenty/NevermoreEngine/tree/version2/loader/ReplicatedStorage/Nevermore"
local entry = EntryUtils.create("Folder", "Nevermore", "")
ParseUtils.fillFoldersAsync(url, entry)
ParseUtils.fillScriptSourcesAsync(ParseUtils.githubContentFromUrl(url), entry)
EntryUtils.mount(ReplicatedStorage, entry)
end
-- Mount libraries
do
local url = "https://github.com/Quenty/NevermoreEngine/tree/version2/Modules"
local entry = EntryUtils.create("Folder", "Core", "")
local fullEntry = EntryUtils.create("Folder", "Nevermore", "", { entry })
ParseUtils.fillFoldersAsync(url, entry)
ParseUtils.fillScriptSourcesAsync(ParseUtils.githubContentFromUrl(url), entry)
EntryUtils.mount(ServerScriptService, fullEntry)
end
print("Done installing Nevermore")