hackmud-script-manager 0.12.0-9309192 → 0.12.0-c276bb2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. package/bin/hsm.js +55 -53
  2. package/index.js +10 -857
  3. package/package.json +11 -7
  4. package/shared.js +902 -0
  5. package/lib.js +0 -71
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hackmud-script-manager",
3
- "version": "0.12.0-9309192",
3
+ "version": "0.12.0-c276bb2",
4
4
  "description": "Script manager for game hackmud, with minification, TypeScript support, and player script type definition generation.",
5
5
  "keywords": [
6
6
  "api",
@@ -23,7 +23,7 @@
23
23
  "author": "Samual Norman",
24
24
  "files": [
25
25
  "index.d.ts",
26
- "lib.js"
26
+ "shared.js"
27
27
  ],
28
28
  "main": "index.js",
29
29
  "bin": {
@@ -34,9 +34,8 @@
34
34
  "url": "https://github.com/samualtnorman/hackmud-script-manager.git"
35
35
  },
36
36
  "scripts": {
37
- "build": "tsc",
38
- "dev": "tsc --watch",
39
- "test": "tsc --noEmit"
37
+ "build": "rollup -c",
38
+ "dev": "rollup -cw"
40
39
  },
41
40
  "dependencies": {
42
41
  "acorn": "8.x",
@@ -49,15 +48,20 @@
49
48
  "typescript": "^4.4.0-beta"
50
49
  },
51
50
  "devDependencies": {
51
+ "@rollup/plugin-typescript": "^8.2.5",
52
52
  "@types/escodegen": "^0.0.7",
53
53
  "@types/esprima": "^4.0.3",
54
54
  "@types/esquery": "^1.0.2",
55
55
  "@types/node": "12.x",
56
56
  "@types/semver": "7.x",
57
- "semver": "7.x"
57
+ "rollup": "^2.56.3",
58
+ "rollup-plugin-preserve-shebang": "^1.0.1",
59
+ "semver": "7.x",
60
+ "tslib": "^2.3.1"
58
61
  },
59
62
  "engines": {
60
63
  "node": ">=12"
61
64
  },
62
- "types": "index.d.ts"
65
+ "types": "index.d.ts",
66
+ "type": "module"
63
67
  }
package/shared.js ADDED
@@ -0,0 +1,902 @@
1
+ import { tokenizer, tokTypes } from 'acorn';
2
+ import { watch as watch$1 } from 'chokidar';
3
+ import { generate } from 'escodegen';
4
+ import { parseScript } from 'esprima';
5
+ import { query } from 'esquery';
6
+ import fs from 'fs';
7
+ import { dirname, resolve, extname, basename } from 'path';
8
+ import { minify } from 'terser';
9
+ import typescript from 'typescript';
10
+
11
+ const { writeFile: writeFile$1, mkdir: makeDirectory, copyFile } = fs.promises;
12
+ function writeFilePersist(path, data, options) {
13
+ return writeFile$1(path, data, options).catch(async (error) => {
14
+ if (error.code != "ENOENT")
15
+ throw error;
16
+ await makeDirectory(dirname(path), { recursive: true });
17
+ await writeFile$1(path, data, options);
18
+ });
19
+ }
20
+ function copyFilePersist(src, dest, flags) {
21
+ return copyFile(src, dest, flags).catch(async (error) => {
22
+ if (error.code != "ENOENT")
23
+ throw error;
24
+ await makeDirectory(dirname(dest), { recursive: true });
25
+ await copyFile(src, dest, flags);
26
+ });
27
+ }
28
+ function hackmudLength(script) {
29
+ return script.replace(/\/\/.*/g, "").replace(/[ \t\n\r\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000]/g, "").length;
30
+ }
31
+ function positionToLineNumber(position, script) {
32
+ let totalCharacters = 0;
33
+ for (const [lineNumber, line] of script.split("\n").entries()) {
34
+ totalCharacters += line.length + 1;
35
+ if (position < totalCharacters)
36
+ return lineNumber;
37
+ }
38
+ throw new Error("unreachable");
39
+ }
40
+ function stringSplice(original, replacement, start, end = start) {
41
+ return original.slice(0, start) + replacement + original.slice(end);
42
+ }
43
+ class DynamicMap extends Map {
44
+ constructor(fallbackHandler) {
45
+ super();
46
+ this.fallbackHandler = fallbackHandler;
47
+ }
48
+ get(key) {
49
+ if (super.has(key))
50
+ return super.get(key);
51
+ const value = this.fallbackHandler(key);
52
+ super.set(key, value);
53
+ return value;
54
+ }
55
+ }
56
+ function clearObject(object) {
57
+ for (const propertyName of Object.getOwnPropertyNames(object)) {
58
+ // @ts-ignore
59
+ delete object[propertyName];
60
+ }
61
+ for (const propertySymbol of Object.getOwnPropertySymbols(object)) {
62
+ // @ts-ignore
63
+ delete object[propertyName];
64
+ }
65
+ return object;
66
+ }
67
+
68
+ const { readFile: readFile, readdir: readDirectory, stat: getFileStatus, writeFile: writeFile } = fs.promises;
69
+ const supportedExtensions = [".js", ".ts"];
70
+ // TODO `clean()` function that delete all scripts in hackmud directory #70
71
+ // TODO optional argument (defaults to false) for `clean()` that makes it only remove scripts without a source file #70
72
+ /**
73
+ * Push a specific or all scripts to a specific or all users.
74
+ * In source directory, scripts in folders will override scripts with same name for user with folder name.
75
+ *
76
+ * e.g. foo/bar.js overrides other bar.js script just for user foo.
77
+ *
78
+ * @param srcDir path to folder containing source files
79
+ * @param hackmudDir path to hackmud directory
80
+ * @param users users to push to (pushes to all if empty)
81
+ * @param scripts scripts to push from (pushes from all if empty)
82
+ * @param onPush function that's called when a script has been pushed
83
+ */
84
+ function push(srcDir, hackmudDir, users, scripts, onPush) {
85
+ return new Promise(async (resolve$1) => {
86
+ const infoAll = [];
87
+ const files = await readDirectory(srcDir, { withFileTypes: true });
88
+ const skips = new Map();
89
+ const promises = [];
90
+ for (const dir of files) {
91
+ const user = dir.name;
92
+ if (dir.isDirectory() && (!users.length || users.includes(user))) {
93
+ promises.push(readDirectory(resolve(srcDir, user), { withFileTypes: true }).then(files => {
94
+ for (const file of files) {
95
+ const extension = extname(file.name);
96
+ const name = basename(file.name, extension);
97
+ if (supportedExtensions.includes(extension) && file.isFile() && (!scripts.length || scripts.includes(name))) {
98
+ let skip = skips.get(name);
99
+ if (skip)
100
+ skip.push(user);
101
+ else
102
+ skips.set(name, [user]);
103
+ readFile(resolve(srcDir, user, file.name), { encoding: "utf-8" }).then(async (code) => {
104
+ let error = null;
105
+ const { srcLength, script: minCode } = await processScript(code).catch(reason => {
106
+ error = reason;
107
+ return {
108
+ srcLength: 0,
109
+ script: ""
110
+ };
111
+ });
112
+ const info = {
113
+ file: `${user}/${file.name}`,
114
+ users: [user],
115
+ minLength: 0,
116
+ error,
117
+ srcLength
118
+ };
119
+ infoAll.push(info);
120
+ if (!error) {
121
+ if (minCode) {
122
+ info.minLength = hackmudLength(minCode);
123
+ await writeFilePersist(resolve(hackmudDir, user, "scripts", `${name}.js`), minCode);
124
+ }
125
+ else
126
+ info.error = new Error("processed script was empty");
127
+ }
128
+ onPush === null || onPush === void 0 ? void 0 : onPush(info);
129
+ });
130
+ }
131
+ }
132
+ }));
133
+ }
134
+ }
135
+ if (!users.length) {
136
+ users = (await readDirectory(hackmudDir, { withFileTypes: true }))
137
+ .filter(a => a.isFile() && extname(a.name) == ".key")
138
+ .map(a => basename(a.name, ".key"));
139
+ }
140
+ Promise.all(promises).then(() => {
141
+ const promises = [];
142
+ for (const file of files) {
143
+ if (file.isFile()) {
144
+ const extension = extname(file.name);
145
+ if (supportedExtensions.includes(extension)) {
146
+ const name = basename(file.name, extension);
147
+ if (!scripts.length || scripts.includes(name)) {
148
+ promises.push(readFile(resolve(srcDir, file.name), { encoding: "utf-8" }).then(async (code) => {
149
+ let error = null;
150
+ const { script: minCode, srcLength } = await processScript(code).catch(reason => {
151
+ error = reason;
152
+ return {
153
+ script: "",
154
+ srcLength: 0
155
+ };
156
+ });
157
+ const info = {
158
+ file: file.name,
159
+ users: [],
160
+ minLength: 0,
161
+ error,
162
+ srcLength
163
+ };
164
+ infoAll.push(info);
165
+ if (!error) {
166
+ if (minCode) {
167
+ info.minLength = hackmudLength(minCode);
168
+ const skip = skips.get(name) || [];
169
+ const promises = [];
170
+ for (const user of users) {
171
+ if (!skip.includes(user)) {
172
+ info.users.push(user);
173
+ promises.push(writeFilePersist(resolve(hackmudDir, user, "scripts", `${name}.js`), minCode));
174
+ }
175
+ }
176
+ }
177
+ else
178
+ info.error = new Error("processed script was empty");
179
+ }
180
+ if (onPush)
181
+ Promise.all(promises).then(() => onPush(info));
182
+ }));
183
+ }
184
+ }
185
+ }
186
+ }
187
+ Promise.all(promises).then(() => resolve$1(infoAll));
188
+ });
189
+ });
190
+ }
191
+ /**
192
+ * Watches target file or folder for updates and builds and pushes updated file.
193
+ *
194
+ * @param srcDir path to folder containing source files
195
+ * @param hackmudDir path to hackmud directory
196
+ * @param users users to push to (pushes to all if empty)
197
+ * @param scripts scripts to push from (pushes from all if empty)
198
+ * @param onPush function that's called after each script has been built and written
199
+ */
200
+ function watch(srcDir, hackmudDir, users, scripts, onPush, { genTypes } = {}) {
201
+ const watcher = watch$1("", { depth: 1, cwd: srcDir, awaitWriteFinish: { stabilityThreshold: 100 } }).on("change", async (path) => {
202
+ const extension = extname(path);
203
+ if (supportedExtensions.includes(extension)) {
204
+ const name = basename(path, extension);
205
+ const fileName = basename(path);
206
+ if (path == fileName) {
207
+ if (!scripts.length || scripts.includes(name)) {
208
+ const sourceCode = await readFile(resolve(srcDir, path), { encoding: "utf-8" });
209
+ const skips = new Map();
210
+ const promisesSkips = [];
211
+ for (const dir of await readDirectory(srcDir, { withFileTypes: true })) {
212
+ if (!dir.isDirectory())
213
+ continue;
214
+ promisesSkips.push(readDirectory(resolve(srcDir, dir.name), { withFileTypes: true }).then(files => {
215
+ for (const file of files) {
216
+ if (!file.isFile())
217
+ continue;
218
+ const fileExtension = extname(file.name);
219
+ if (!supportedExtensions.includes(fileExtension))
220
+ continue;
221
+ const name = basename(file.name, fileExtension);
222
+ const skip = skips.get(name);
223
+ if (skip)
224
+ skip.push(dir.name);
225
+ else
226
+ skips.set(name, [dir.name]);
227
+ }
228
+ }));
229
+ }
230
+ await Promise.all(promisesSkips);
231
+ let error = null;
232
+ const { script, srcLength } = await processScript(sourceCode).catch(reason => {
233
+ error = reason;
234
+ return {
235
+ script: "",
236
+ srcLength: 0
237
+ };
238
+ });
239
+ const info = {
240
+ file: path,
241
+ users: [],
242
+ minLength: 0,
243
+ error,
244
+ srcLength
245
+ };
246
+ const promises = [];
247
+ if (!error) {
248
+ if (script) {
249
+ const skip = skips.get(name) || [];
250
+ info.minLength = hackmudLength(script);
251
+ if (!users.length) {
252
+ users = (await readDirectory(hackmudDir, { withFileTypes: true }))
253
+ .filter(a => a.isFile() && extname(a.name) == ".key")
254
+ .map(a => basename(a.name, ".key"));
255
+ }
256
+ for (const user of users) {
257
+ if (skip.includes(user))
258
+ continue;
259
+ info.users.push(user);
260
+ promises.push(writeFilePersist(resolve(hackmudDir, user, "scripts", `${name}.js`), script));
261
+ }
262
+ }
263
+ else
264
+ info.error = new Error("processed script was empty");
265
+ }
266
+ if (onPush) {
267
+ await Promise.all(promises);
268
+ onPush(info);
269
+ }
270
+ }
271
+ }
272
+ else {
273
+ const user = basename(resolve(path, ".."));
274
+ if ((!users.length || users.includes(user)) && (!scripts.length || scripts.includes(name))) {
275
+ const sourceCode = await readFile(resolve(srcDir, path), { encoding: "utf-8" });
276
+ let error = null;
277
+ const { script, srcLength } = await processScript(sourceCode).catch(reason => {
278
+ error = reason;
279
+ return {
280
+ script: "",
281
+ srcLength: 0
282
+ };
283
+ });
284
+ const info = {
285
+ file: path,
286
+ users: [user],
287
+ minLength: 0,
288
+ error,
289
+ srcLength
290
+ };
291
+ if (!error) {
292
+ if (script) {
293
+ info.minLength = hackmudLength(script);
294
+ await writeFilePersist(resolve(hackmudDir, user, "scripts", `${name}.js`), script);
295
+ }
296
+ else
297
+ info.error = new Error("processed script was empty");
298
+ }
299
+ onPush === null || onPush === void 0 ? void 0 : onPush(info);
300
+ }
301
+ }
302
+ }
303
+ });
304
+ if (genTypes) {
305
+ generateTypings(srcDir, resolve(srcDir, genTypes), hackmudDir);
306
+ watcher.on("add", () => generateTypings(srcDir, resolve(srcDir, genTypes), hackmudDir));
307
+ watcher.on("unlink", () => generateTypings(srcDir, resolve(srcDir, genTypes), hackmudDir));
308
+ }
309
+ }
310
+ /**
311
+ * Copies script from hackmud to local source folder.
312
+ *
313
+ * @param sourceFolderPath path to folder containing source files
314
+ * @param hackmudPath path to hackmud directory
315
+ * @param script script to pull in `user.name` format
316
+ */
317
+ async function pull(sourceFolderPath, hackmudPath, script) {
318
+ const [user, name] = script.split(".");
319
+ await copyFilePersist(resolve(hackmudPath, user, "scripts", `${name}.js`), resolve(sourceFolderPath, user, `${name}.js`));
320
+ }
321
+ async function syncMacros(hackmudPath) {
322
+ const files = await readDirectory(hackmudPath, { withFileTypes: true });
323
+ const macros = new Map();
324
+ const users = [];
325
+ for (const file of files) {
326
+ if (!file.isFile())
327
+ continue;
328
+ switch (extname(file.name)) {
329
+ case ".macros":
330
+ {
331
+ const lines = (await readFile(resolve(hackmudPath, file.name), { encoding: "utf-8" })).split("\n");
332
+ const date = (await getFileStatus(resolve(hackmudPath, file.name))).mtime;
333
+ for (let i = 0; i < lines.length / 2 - 1; i++) {
334
+ const macroName = lines[i * 2];
335
+ const curMacro = macros.get(macroName);
336
+ if (!curMacro || date > curMacro.date)
337
+ macros.set(macroName, { date, macro: lines[i * 2 + 1] });
338
+ }
339
+ }
340
+ break;
341
+ case ".key":
342
+ {
343
+ users.push(basename(file.name, ".key"));
344
+ }
345
+ break;
346
+ }
347
+ }
348
+ let macroFile = "";
349
+ let macrosSynced = 0;
350
+ for (const [name, { macro }] of [...macros].sort(([a], [b]) => (a > b) - (a < b))) {
351
+ if (macro[0] != macro[0].toLowerCase())
352
+ continue;
353
+ macroFile += `${name}\n${macro}\n`;
354
+ macrosSynced++;
355
+ }
356
+ for (const user of users)
357
+ writeFile(resolve(hackmudPath, user + ".macros"), macroFile);
358
+ return { macrosSynced, usersSynced: users.length };
359
+ }
360
+ async function test(srcPath) {
361
+ const promises = [];
362
+ const errors = [];
363
+ for (const dirent of await readDirectory(srcPath, { withFileTypes: true })) {
364
+ if (dirent.isDirectory()) {
365
+ promises.push(readDirectory(resolve(srcPath, dirent.name), { withFileTypes: true }).then(files => {
366
+ const promises = [];
367
+ for (const file of files) {
368
+ if (!file.isFile() || !supportedExtensions.includes(extname(file.name)))
369
+ continue;
370
+ promises.push(readFile(resolve(srcPath, dirent.name, file.name), { encoding: "utf-8" })
371
+ .then(processScript)
372
+ .then(({ warnings }) => errors.push(...warnings.map(({ message, line }) => ({
373
+ file: `${dirent.name}/${file.name}`,
374
+ message, line
375
+ })))));
376
+ }
377
+ return Promise.all(promises);
378
+ }));
379
+ }
380
+ else if (dirent.isFile() && supportedExtensions.includes(extname(dirent.name))) {
381
+ promises.push(readFile(resolve(srcPath, dirent.name), { encoding: "utf-8" })
382
+ .then(processScript)
383
+ .then(({ warnings }) => errors.push(...warnings.map(({ message, line }) => ({
384
+ file: dirent.name,
385
+ message, line
386
+ })))));
387
+ }
388
+ }
389
+ await Promise.all(promises);
390
+ return errors;
391
+ }
392
+ async function generateTypings(srcDir, target, hackmudPath) {
393
+ const users = new Set();
394
+ if (hackmudPath) {
395
+ for (const dirent of await readDirectory(hackmudPath, { withFileTypes: true })) {
396
+ if (dirent.isFile() && extname(dirent.name) == ".key")
397
+ users.add(basename(dirent.name, ".key"));
398
+ }
399
+ }
400
+ const wildScripts = [];
401
+ const wildAnyScripts = [];
402
+ const allScripts = {};
403
+ const allAnyScripts = {};
404
+ for (const dirent of await readDirectory(srcDir, { withFileTypes: true })) {
405
+ if (dirent.isFile()) {
406
+ if (extname(dirent.name) == ".ts")
407
+ wildScripts.push(basename(dirent.name, ".ts"));
408
+ else if (extname(dirent.name) == ".js")
409
+ wildAnyScripts.push(basename(dirent.name, ".js"));
410
+ }
411
+ else if (dirent.isDirectory()) {
412
+ const scripts = allScripts[dirent.name] = [];
413
+ const anyScripts = allAnyScripts[dirent.name] = [];
414
+ users.add(dirent.name);
415
+ for (const file of await readDirectory(resolve(srcDir, dirent.name), { withFileTypes: true })) {
416
+ if (file.isFile()) {
417
+ if (extname(file.name) == ".ts")
418
+ scripts.push(basename(file.name, ".ts"));
419
+ else if (extname(file.name) == ".js")
420
+ anyScripts.push(basename(file.name, ".js"));
421
+ }
422
+ }
423
+ }
424
+ }
425
+ let o = "";
426
+ for (const script of wildScripts)
427
+ o += `import { script as $${script}$ } from "./src/${script}"\n`;
428
+ o += "\n";
429
+ for (const user in allScripts) {
430
+ const scripts = allScripts[user];
431
+ for (const script of scripts)
432
+ o += `import { script as $${user}$${script}$ } from "./src/${user}/${script}"\n`;
433
+ }
434
+ // TODO detect security level and generate apropriate code
435
+ // TODO accurate function signatures
436
+ // currently I lose the generic-ness of my functions when I wrap them
437
+ // just regexing isn't enough and it looks like I'm going to need to parse the files in TypeScript to extract the signature
438
+ o += `
439
+ type ArrayRemoveFirst<A> = A extends [ infer FirstItem, ...infer Rest ] ? Rest : never
440
+
441
+ type Subscript<T extends (...args: any) => any> =
442
+ (...args: ArrayRemoveFirst<Parameters<T>>) => ReturnType<T> | ScriptFailure
443
+
444
+ type WildFullsec = Record<string, () => ScriptFailure> & {
445
+ `;
446
+ for (const script of wildScripts)
447
+ o += `\t${script}: Subscript<typeof $${script}$>\n`;
448
+ for (const script of wildAnyScripts)
449
+ o += `\t${script}: (...args: any) => any\n`;
450
+ o += "}\n\ndeclare global {\n\tinterface PlayerFullsec {";
451
+ let lastWasMultiLine = true;
452
+ for (const user of users) {
453
+ const scripts = allScripts[user];
454
+ const anyScripts = allAnyScripts[user];
455
+ if ((scripts && scripts.length) || (anyScripts && anyScripts.length)) {
456
+ lastWasMultiLine = true;
457
+ o += `\n\t\t${user}: WildFullsec & {\n`;
458
+ for (const script of scripts)
459
+ o += `\t\t\t${script}: Subscript<typeof $${user}$${script}$>\n`;
460
+ for (const script of anyScripts)
461
+ o += `\t\t\t${script}: (...args: any) => any\n`;
462
+ o += "\t\t}";
463
+ }
464
+ else {
465
+ if (lastWasMultiLine) {
466
+ o += "\n";
467
+ lastWasMultiLine = false;
468
+ }
469
+ o += `\t\t${user}: WildFullsec`;
470
+ }
471
+ o += "\n";
472
+ }
473
+ o += "\t}\n}\n";
474
+ await writeFile(target, o);
475
+ }
476
+ /**
477
+ * Minifies a given script
478
+ *
479
+ * @param script JavaScript or TypeScript code
480
+ */
481
+ async function processScript(script) {
482
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
483
+ let preScriptComments;
484
+ let autocomplete;
485
+ [, preScriptComments, script, autocomplete] = script.match(/((?:^\s*\/\/.*\n)*)\s*((?:.+?\/\/\s*(.+?)\s*$)?[^]*)/m);
486
+ if (!script)
487
+ throw new Error("script was empty");
488
+ if (script.match(/(?:SC|DB)\$/))
489
+ throw new Error("SC$ and DB$ are protected and cannot appear in a script");
490
+ let seclevel;
491
+ for (const line of preScriptComments.split("\n")) {
492
+ let [, autocompleteMatch, seclevelMatch] = (line.match(/^\s*\/\/\s*(?:@autocomplete\s*([^\s].*?)|@seclevel\s*([^\s].*?))\s*$/) || []);
493
+ if (autocompleteMatch)
494
+ autocomplete = autocompleteMatch;
495
+ else if (seclevelMatch) {
496
+ if (seclevelMatch.match(/^(?:fullsec|f|4|fs|full)$/i))
497
+ seclevel = 4;
498
+ else if (seclevelMatch.match(/^(?:highsec|h|3|hs|high)$/i))
499
+ seclevel = 3;
500
+ else if (seclevelMatch.match(/^(?:midsec|m|2|ms|mid)$/i))
501
+ seclevel = 2;
502
+ else if (seclevelMatch.match(/^(?:lowsec|l|1|ls|low)$/i))
503
+ seclevel = 1;
504
+ else if (seclevelMatch.match(/^(?:nullsec|n|0|ns|null)$/i))
505
+ seclevel = 0;
506
+ }
507
+ }
508
+ let detectedSeclevel;
509
+ if (script.match(/[#$][n0]s\.[a-z_][a-z_0-9]{0,24}\.[a-z_][a-z_0-9]{0,24}\(/))
510
+ detectedSeclevel = 0;
511
+ else if (script.match(/[#$][l1]s\.[a-z_][a-z_0-9]{0,24}\.[a-z_][a-z_0-9]{0,24}\(/))
512
+ detectedSeclevel = 1;
513
+ else if (script.match(/[#$][m2]s\.[a-z_][a-z_0-9]{0,24}\.[a-z_][a-z_0-9]{0,24}\(/))
514
+ detectedSeclevel = 2;
515
+ else if (script.match(/[#$][h3]s\.[a-z_][a-z_0-9]{0,24}\.[a-z_][a-z_0-9]{0,24}\(/))
516
+ detectedSeclevel = 3;
517
+ else if (script.match(/[#$][f4]s\.[a-z_][a-z_0-9]{0,24}\.[a-z_][a-z_0-9]{0,24}\(/))
518
+ detectedSeclevel = 4;
519
+ const seclevelNames = ["NULLSEC", "LOWSEC", "MIDSEC", "HIGHSEC", "FULLSEC"];
520
+ if (seclevel == undefined)
521
+ seclevel = (_a = seclevel !== null && seclevel !== void 0 ? seclevel : detectedSeclevel) !== null && _a !== void 0 ? _a : 0;
522
+ else if (detectedSeclevel != undefined && seclevel > detectedSeclevel)
523
+ throw new Error(`detected seclevel of ${seclevelNames[detectedSeclevel]} is lower than the provided seclevel of ${seclevelNames[seclevel]}`);
524
+ const semicolons = (_c = (_b = script.match(/;/g)) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0;
525
+ script = script
526
+ .replace(/#[fhmln43210]s\.scripts\.quine\(\)/g, JSON.stringify(script))
527
+ .replace(/[#$][fhmln43210]?s\.([a-z_][a-z_0-9]{0,24})\.([a-z_][a-z_0-9]{0,24})\(/g, "SC$$$1$$$2(")
528
+ .replace(/^function\s*\(/, "function script(")
529
+ .replace(/#D\(/g, "$D(")
530
+ .replace(/#FMCL/g, "$FMCL")
531
+ .replace(/#G/g, "$G")
532
+ .replace(/[#$]db\./g, "DB$");
533
+ // typescript compilation, this runs on regular javascript too to convert
534
+ // any post es2015 syntax into es2015 syntax
535
+ const { outputText, diagnostics = [] } = typescript.transpileModule(script, {
536
+ compilerOptions: { target: typescript.ScriptTarget.ES2015 },
537
+ reportDiagnostics: true
538
+ });
539
+ const warnings = diagnostics.map(({ messageText, start }) => ({
540
+ message: typeof messageText == "string" ? messageText : messageText.messageText,
541
+ line: positionToLineNumber(start, script)
542
+ }));
543
+ script = outputText.replace(/^export /, "");
544
+ await writeFile("./test.json", JSON.stringify(parseScript(script), null, "\t"));
545
+ const ast = parseScript(script);
546
+ for (const node of query(ast, "ClassBody > MethodDefinition[kind=constructor] > FunctionExpression > BlockStatement")) {
547
+ node.body.unshift({
548
+ type: "VariableDeclaration",
549
+ declarations: [
550
+ {
551
+ type: "VariableDeclarator",
552
+ id: {
553
+ type: "Identifier",
554
+ name: "__THIS__"
555
+ }
556
+ }
557
+ ],
558
+ kind: "let"
559
+ });
560
+ }
561
+ for (const node of query(ast, "ClassBody > MethodDefinition[kind=constructor] > FunctionExpression > BlockStatement !CallExpression > Super")) {
562
+ const newNode = {
563
+ type: "AssignmentExpression",
564
+ operator: "=",
565
+ left: {
566
+ type: "Identifier",
567
+ name: "__THIS__"
568
+ },
569
+ right: { ...node }
570
+ };
571
+ Object.assign(clearObject(node), newNode);
572
+ }
573
+ for (const node of query(ast, "ClassBody > MethodDefinition > FunctionExpression > BlockStatement !ThisExpression")) {
574
+ const newNode = {
575
+ type: "Identifier",
576
+ name: "__THIS__"
577
+ };
578
+ Object.assign(clearObject(node), newNode);
579
+ }
580
+ for (const node of query(ast, "ClassBody > MethodDefinition[kind=method] > FunctionExpression > BlockStatement")) {
581
+ node.body.unshift({
582
+ type: "VariableDeclaration",
583
+ declarations: [{
584
+ type: "VariableDeclarator",
585
+ id: {
586
+ type: "Identifier",
587
+ name: "__THIS__"
588
+ },
589
+ init: {
590
+ type: "CallExpression",
591
+ callee: {
592
+ type: "MemberExpression",
593
+ computed: false,
594
+ object: {
595
+ type: "Super"
596
+ },
597
+ property: {
598
+ type: "Identifier",
599
+ name: "valueOf"
600
+ },
601
+ optional: false
602
+ },
603
+ arguments: [],
604
+ optional: false
605
+ }
606
+ }],
607
+ "kind": "let"
608
+ });
609
+ }
610
+ script = generate(ast);
611
+ // the typescript inserts semicolons where they weren't already so we take
612
+ // all semicolons out of the count and add the number of semicolons in the
613
+ // source to make things fair
614
+ let srcLength = hackmudLength(script.replace(/^function\s*\w+\(/, "function("))
615
+ - ((_e = (_d = script.match(/;/g)) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0)
616
+ + semicolons
617
+ + ((_g = (_f = script.match(/SC\$[a-zA-Z_][a-zA-Z0-9_]*\$[a-zA-Z_][a-zA-Z0-9_]*\(/g)) === null || _f === void 0 ? void 0 : _f.length) !== null && _g !== void 0 ? _g : 0)
618
+ + ((_j = (_h = script.match(/DB\$/g)) === null || _h === void 0 ? void 0 : _h.length) !== null && _j !== void 0 ? _j : 0);
619
+ // remove dead code (so we don't waste chracters quine cheating strings
620
+ // that aren't even used)
621
+ script = (await minify(script, {
622
+ ecma: 2015,
623
+ parse: { bare_returns: true },
624
+ compress: { booleans: false }
625
+ })).code || "";
626
+ let blockStatementIndex;
627
+ if (script.startsWith("function "))
628
+ blockStatementIndex = getFunctionBodyStart(script);
629
+ else {
630
+ script = `function script(context, args) {\n${script}\n}`;
631
+ blockStatementIndex = 31;
632
+ srcLength += 24;
633
+ }
634
+ let scriptBeforeJSONValueReplacement = (await minify(script, {
635
+ ecma: 2015,
636
+ compress: {
637
+ passes: Infinity,
638
+ unsafe: true,
639
+ unsafe_arrows: true,
640
+ unsafe_comps: true,
641
+ unsafe_symbols: true,
642
+ unsafe_methods: true,
643
+ unsafe_proto: true,
644
+ unsafe_regexp: true,
645
+ unsafe_undefined: true
646
+ },
647
+ format: { semicolons: false }
648
+ })).code || "";
649
+ {
650
+ const tokens = [...tokenizer(scriptBeforeJSONValueReplacement, { ecmaVersion: 2015 })].reverse().values();
651
+ for (const token of tokens) {
652
+ // we can't replace any tokens before the block statement or we'll break stuff
653
+ if (token.start < blockStatementIndex)
654
+ break;
655
+ switch (token.type) {
656
+ case tokTypes.name:
657
+ {
658
+ if (token.value != "prototype" && token.value != "__proto__")
659
+ break;
660
+ const tokenBefore = tokens.next().value;
661
+ if (tokenBefore.type != tokTypes.dot)
662
+ break;
663
+ srcLength += 3;
664
+ scriptBeforeJSONValueReplacement = stringSplice(scriptBeforeJSONValueReplacement, `["${token.value}"]`, tokenBefore.start, token.end);
665
+ }
666
+ break;
667
+ case tokTypes._const:
668
+ {
669
+ scriptBeforeJSONValueReplacement = stringSplice(scriptBeforeJSONValueReplacement, "let", token.start, token.end);
670
+ }
671
+ break;
672
+ case tokTypes._this:
673
+ throw new Error('"this" keyword is not supported in hackmud');
674
+ }
675
+ }
676
+ }
677
+ const jsonValues = [];
678
+ let undefinedIsReferenced = false;
679
+ // we iterate through the tokens backwards so that substring replacements
680
+ // don't affect future replacements since a part of the string could be
681
+ // replaced with a string of a different length which messes up indexes
682
+ const tokens = [...tokenizer(script, { ecmaVersion: 2015 })].reverse().values();
683
+ let templateToRightOfPlaceholder = false;
684
+ for (const token of tokens) {
685
+ // we can't replace any tokens before the block statement or we'll break stuff
686
+ if (token.start < blockStatementIndex)
687
+ break;
688
+ switch (token.type) {
689
+ case tokTypes.backQuote:
690
+ {
691
+ const templateToken = tokens.next().value;
692
+ if (tokens.next().value.type == tokTypes.backQuote)
693
+ throw new Error("tagged templates not supported yet");
694
+ // no point in concatenating an empty string
695
+ if (templateToken.value == "") {
696
+ script = stringSplice(script, "))", templateToken.start - 1, token.end);
697
+ break;
698
+ }
699
+ let jsonValueIndex = jsonValues.indexOf(templateToken.value);
700
+ if (jsonValueIndex == -1)
701
+ jsonValueIndex += jsonValues.push(templateToken.value);
702
+ script = stringSplice(script, `)+_JSON_VALUE_${jsonValueIndex}_)`, templateToken.start - 1, token.end);
703
+ }
704
+ break;
705
+ case tokTypes.template:
706
+ {
707
+ if (tokens.next().value.type == tokTypes.backQuote) {
708
+ if (tokens.next().value.type == tokTypes.name)
709
+ throw new Error("tagged templates not supported yet");
710
+ // there *is* a point in concatenating an empty string at the
711
+ // start because foo + bar is not the same thing as "" + foo + bar
712
+ // ...but foo + "<template>" + bar *is* the same thing as "" + foo + "<template>" + bar
713
+ // so we just need to check if there's a template to the right of the placeholder and skip that case
714
+ if (token.value == "" && templateToRightOfPlaceholder) {
715
+ templateToRightOfPlaceholder = false;
716
+ script = stringSplice(script, "((", token.start - 1, token.end + 2);
717
+ break;
718
+ }
719
+ templateToRightOfPlaceholder = false;
720
+ let jsonValueIndex = jsonValues.indexOf(token.value);
721
+ if (jsonValueIndex == -1)
722
+ jsonValueIndex += jsonValues.push(token.value);
723
+ script = stringSplice(script, `(_JSON_VALUE_${jsonValueIndex}_+(`, token.start - 1, token.end + 2);
724
+ break;
725
+ }
726
+ // no point in concatenating an empty string
727
+ if (token.value == "") {
728
+ templateToRightOfPlaceholder = false;
729
+ script = stringSplice(script, ")+(", token.start - 1, token.end + 2);
730
+ break;
731
+ }
732
+ templateToRightOfPlaceholder = true;
733
+ let jsonValueIndex = jsonValues.indexOf(token.value);
734
+ if (jsonValueIndex == -1)
735
+ jsonValueIndex += jsonValues.push(token.value);
736
+ script = stringSplice(script, `)+_JSON_VALUE_${jsonValueIndex}_+(`, token.start - 1, token.end + 2);
737
+ }
738
+ break;
739
+ case tokTypes.name:
740
+ {
741
+ if (token.value.length < 3)
742
+ break;
743
+ const tokenBefore = tokens.next().value;
744
+ if (tokenBefore.type == tokTypes.dot) {
745
+ let jsonValueIndex = jsonValues.indexOf(token.value);
746
+ if (jsonValueIndex == -1)
747
+ jsonValueIndex += jsonValues.push(token.value);
748
+ script = stringSplice(script, `[_JSON_VALUE_${jsonValueIndex}_]`, tokenBefore.start, token.end);
749
+ break;
750
+ }
751
+ if (token.value == "undefined") {
752
+ script = stringSplice(script, " _UNDEFINED_ ", token.start, token.end);
753
+ undefinedIsReferenced = true;
754
+ }
755
+ }
756
+ break;
757
+ case tokTypes._null:
758
+ {
759
+ let jsonValueIndex = jsonValues.indexOf(null);
760
+ if (jsonValueIndex == -1)
761
+ jsonValueIndex += jsonValues.push(null);
762
+ script = stringSplice(script, ` _JSON_VALUE_${jsonValueIndex}_ `, token.start, token.end);
763
+ }
764
+ break;
765
+ case tokTypes._true:
766
+ {
767
+ let jsonValueIndex = jsonValues.indexOf(true);
768
+ if (jsonValueIndex == -1)
769
+ jsonValueIndex += jsonValues.push(true);
770
+ script = stringSplice(script, ` _JSON_VALUE_${jsonValueIndex}_ `, token.start, token.end);
771
+ }
772
+ break;
773
+ case tokTypes._false:
774
+ {
775
+ let jsonValueIndex = jsonValues.indexOf(false);
776
+ if (jsonValueIndex == -1)
777
+ jsonValueIndex += jsonValues.push(false);
778
+ script = stringSplice(script, ` _JSON_VALUE_${jsonValueIndex}_ `, token.start, token.end);
779
+ }
780
+ break;
781
+ case tokTypes.num:
782
+ {
783
+ if (token.value == 0) {
784
+ const tokenBefore = tokens.next().value;
785
+ if (tokenBefore.type == tokTypes._void) {
786
+ script = stringSplice(script, " _UNDEFINED_ ", tokenBefore.start, token.end);
787
+ undefinedIsReferenced = true;
788
+ }
789
+ // may as well break here since we're gonna break anyway
790
+ break;
791
+ }
792
+ if (token.value < 10)
793
+ break;
794
+ let jsonValueIndex = jsonValues.indexOf(token.value);
795
+ if (jsonValueIndex == -1)
796
+ jsonValueIndex += jsonValues.push(token.value);
797
+ script = stringSplice(script, ` _JSON_VALUE_${jsonValueIndex}_ `, token.start, token.end);
798
+ }
799
+ break;
800
+ case tokTypes.string:
801
+ {
802
+ if (token.value.includes("\u0000"))
803
+ break;
804
+ let jsonValueIndex = jsonValues.indexOf(token.value);
805
+ if (jsonValueIndex == -1)
806
+ jsonValueIndex += jsonValues.push(token.value);
807
+ script = stringSplice(script, ` _JSON_VALUE_${jsonValueIndex}_ `, token.start, token.end);
808
+ }
809
+ break;
810
+ case tokTypes._const:
811
+ {
812
+ script = stringSplice(script, "let", token.start, token.end);
813
+ }
814
+ break;
815
+ case tokTypes._this:
816
+ throw new Error('"this" keyword is not supported in hackmud');
817
+ }
818
+ }
819
+ let comment = null;
820
+ let hasComment = false;
821
+ if (jsonValues.length) {
822
+ hasComment = true;
823
+ if (jsonValues.length == 1) {
824
+ if (typeof jsonValues[0] == "string" && !jsonValues[0].includes("\n") && !jsonValues[0].includes("\t")) {
825
+ script = stringSplice(script, `\nlet _JSON_VALUE_0_ = SC$scripts$quine().split\`\t\`[_SPLIT_INDEX_]${undefinedIsReferenced ? ", _UNDEFINED_" : ""}\n`, blockStatementIndex + 1);
826
+ comment = jsonValues[0];
827
+ }
828
+ else {
829
+ script = stringSplice(script, `\nlet _JSON_VALUE_0_ = JSON.parse(SC$scripts$quine().split\`\t\`[_SPLIT_INDEX_])${undefinedIsReferenced ? ", _UNDEFINED_" : ""}\n`, blockStatementIndex + 1);
830
+ comment = JSON.stringify(jsonValues[0]);
831
+ }
832
+ }
833
+ else {
834
+ script = stringSplice(script, `\nlet [ ${jsonValues.map((_, i) => `_JSON_VALUE_${i}_`).join(", ")} ] = JSON.parse(SC$scripts$quine().split\`\t\`[_SPLIT_INDEX_])${undefinedIsReferenced ? ", _UNDEFINED_" : ""}\n`, blockStatementIndex + 1);
835
+ comment = JSON.stringify(jsonValues);
836
+ }
837
+ }
838
+ else
839
+ script = script.replace(/_UNDEFINED_/g, "void 0");
840
+ script = (await minify(script, {
841
+ ecma: 2015,
842
+ compress: {
843
+ passes: Infinity,
844
+ unsafe: true,
845
+ unsafe_arrows: true,
846
+ unsafe_comps: true,
847
+ unsafe_symbols: true,
848
+ unsafe_methods: true,
849
+ unsafe_proto: true,
850
+ unsafe_regexp: true,
851
+ unsafe_undefined: true
852
+ },
853
+ format: { semicolons: false }
854
+ })).code || "";
855
+ // this step affects the chracter count and can't be done after the count comparison
856
+ if (comment != null) {
857
+ script = stringSplice(script, `${autocomplete ? `//${autocomplete}\n` : ""}\n//\t${comment}\t\n`, getFunctionBodyStart(script) + 1);
858
+ for (const [i, part] of script.split("\t").entries()) {
859
+ if (part != comment)
860
+ continue;
861
+ script = script.replace("_SPLIT_INDEX_", (await minify(`$(${i})`, { ecma: 2015 })).code.match(/\$\((.+)\)/)[1]);
862
+ break;
863
+ }
864
+ }
865
+ // if the script has a comment, it's gonna contain `SC$scripts$quine()`
866
+ // which is gonna eventually compile to `#fs.scripts.quine()` which contains
867
+ // an extra character so we have to account for that
868
+ if (hackmudLength(scriptBeforeJSONValueReplacement) <= (hackmudLength(script) + Number(hasComment))) {
869
+ script = scriptBeforeJSONValueReplacement;
870
+ if (autocomplete)
871
+ script = stringSplice(script, `//${autocomplete}\n`, getFunctionBodyStart(script) + 1);
872
+ }
873
+ script = script
874
+ .replace(/^function\s*\w+\(/, "function(")
875
+ .replace(/SC\$([a-zA-Z_][a-zA-Z0-9_]*)\$([a-zA-Z_][a-zA-Z0-9_]*)\(/g, `#${"nlmhf"[seclevel]}s.$1.$2(`)
876
+ .replace(/\$D\(/g, "#D(")
877
+ .replace(/\$FMCL/g, "#FMCL")
878
+ .replace(/\$G/g, "#G")
879
+ .replace(/DB\$/g, "#db.");
880
+ return {
881
+ srcLength,
882
+ script,
883
+ warnings
884
+ };
885
+ }
886
+ function getFunctionBodyStart(code) {
887
+ const tokens = tokenizer(code, { ecmaVersion: 2015 });
888
+ tokens.getToken(); // function
889
+ tokens.getToken(); // name
890
+ tokens.getToken(); // (
891
+ let nests = 1;
892
+ while (nests) {
893
+ const token = tokens.getToken();
894
+ if (token.type == tokTypes.parenL)
895
+ nests++;
896
+ else if (token.type == tokTypes.parenR)
897
+ nests--;
898
+ }
899
+ return tokens.getToken().start; // {
900
+ }
901
+
902
+ export { DynamicMap as D, syncMacros as a, pull as b, watch as c, push as d, getFunctionBodyStart as e, generateTypings as g, hackmudLength as h, processScript as p, supportedExtensions as s, test as t, writeFilePersist as w };