hackmud-script-manager 0.13.0-a60a7a2 → 0.13.0-c461329

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. package/.gitattributes +1 -0
  2. package/.github/workflows/codeql-analysis.yml +39 -0
  3. package/.github/workflows/publish.yml +42 -0
  4. package/.vscode/settings.json +6 -0
  5. package/babel.config.json +6 -0
  6. package/package.json +12 -21
  7. package/rollup.config.js +110 -0
  8. package/scripts/build-package-json.js +36 -0
  9. package/scripts/jsconfig.json +5 -0
  10. package/scripts/version-dev.js +25 -0
  11. package/src/bin/hsm.ts +505 -0
  12. package/src/constants.json +3 -0
  13. package/src/generateTypings.ts +116 -0
  14. package/src/index.ts +19 -0
  15. package/src/modules.d.ts +5 -0
  16. package/src/processScript/index.ts +198 -0
  17. package/src/processScript/minify.ts +529 -0
  18. package/src/processScript/postprocess.ts +38 -0
  19. package/src/processScript/preprocess.ts +146 -0
  20. package/src/processScript/transform.ts +760 -0
  21. package/src/pull.ts +16 -0
  22. package/src/push.ts +314 -0
  23. package/src/syncMacros.ts +52 -0
  24. package/src/test.ts +59 -0
  25. package/src/tsconfig.json +20 -0
  26. package/src/watch.ts +156 -0
  27. package/tsconfig.json +12 -0
  28. package/assert-1b7dada8.js +0 -1
  29. package/bin/hsm.d.ts +0 -2
  30. package/bin/hsm.js +0 -2
  31. package/generateTypings.d.ts +0 -2
  32. package/generateTypings.js +0 -1
  33. package/index.d.ts +0 -15
  34. package/index.js +0 -1
  35. package/processScript/compile.d.ts +0 -17
  36. package/processScript/compile.js +0 -1
  37. package/processScript/index.d.ts +0 -30
  38. package/processScript/index.js +0 -1
  39. package/processScript/minify.d.ts +0 -7
  40. package/processScript/minify.js +0 -1
  41. package/processScript/postProcess.d.ts +0 -2
  42. package/processScript/postProcess.js +0 -1
  43. package/processScript/preProcess.d.ts +0 -15
  44. package/processScript/preProcess.js +0 -1
  45. package/pull.d.ts +0 -9
  46. package/pull.js +0 -1
  47. package/push.d.ts +0 -26
  48. package/push.js +0 -1
  49. package/spliceString-2c6f214f.js +0 -1
  50. package/syncMacros.d.ts +0 -5
  51. package/syncMacros.js +0 -1
  52. package/test.d.ts +0 -6
  53. package/test.js +0 -1
  54. package/watch.d.ts +0 -14
  55. package/watch.js +0 -1
@@ -0,0 +1,529 @@
1
+ import babelGenerator from "@babel/generator"
2
+ import { parse } from "@babel/parser"
3
+ import babelTraverse from "@babel/traverse"
4
+ import t, { Expression } from "@babel/types"
5
+ import { assert, countHackmudCharacters, spliceString } from "@samual/lib"
6
+ import { tokenizer as tokenize, tokTypes as tokenTypes } from "acorn"
7
+ import * as terser from "terser"
8
+
9
+ const { default: generate } = babelGenerator as any as typeof import("@babel/generator")
10
+ const { default: traverse } = babelTraverse as any as typeof import("@babel/traverse")
11
+
12
+ type MinifyOptions = {
13
+ /** 11 a-z 0-9 characters */
14
+ uniqueID: string
15
+
16
+ /** whether to mangle function and class names (defaults to `false`) */
17
+ mangleNames: boolean
18
+ }
19
+
20
+ // TODO when there are more than 3 references to `$G`, place a `let _GLOBAL_0_ = $G` at the top and replace references with `_GLOBAL_0_`
21
+ // TODO move autocomplete stuff outside this function
22
+ // TODO allow not mangling class and function names
23
+
24
+ /**
25
+ * @param code compiled code and/or hackmud compatible code
26
+ * @param autocomplete the comment inserted after the function signature
27
+ * @param options {@link MinifyOptions details}
28
+ */
29
+ export async function minify(code: string, autocomplete: string, {
30
+ uniqueID = "00000000000",
31
+ mangleNames = false
32
+ }: Partial<MinifyOptions> = {}) {
33
+ assert(uniqueID.match(/^\w{11}$/))
34
+
35
+ const jsonValues: any[] = []
36
+ let undefinedIsReferenced = false
37
+
38
+ // remove dead code (so we don't waste chracters quine cheating strings
39
+ // that aren't even used)
40
+ code = (await terser.minify(code, {
41
+ ecma: 2015,
42
+ parse: { bare_returns: true },
43
+ compress: {
44
+ passes: Infinity,
45
+ unsafe: true,
46
+ booleans: false,
47
+ sequences: false
48
+ },
49
+ keep_classnames: !mangleNames,
50
+ keep_fnames: !mangleNames
51
+ })).code || ""
52
+
53
+ let scriptBeforeJSONValueReplacement
54
+
55
+ {
56
+ // BUG when this script is used, the source char count is off
57
+
58
+ const file = await parse(code)
59
+
60
+ traverse(file, {
61
+ MemberExpression({ node: memberExpression }) {
62
+ if (memberExpression.computed)
63
+ return
64
+
65
+ assert(memberExpression.property.type == "Identifier")
66
+
67
+ if (memberExpression.property.name == "prototype") {
68
+ memberExpression.computed = true
69
+ memberExpression.property = t.identifier(`_PROTOTYPE_PROPERTY_${uniqueID}_`)
70
+ } else if (memberExpression.property.name == "__proto__") {
71
+ memberExpression.computed = true
72
+ memberExpression.property = t.identifier(`_PROTO_PROPERTY_${uniqueID}_`)
73
+ }
74
+ }
75
+ })
76
+
77
+ scriptBeforeJSONValueReplacement = (await terser.minify(generate(file!).code, {
78
+ ecma: 2015,
79
+ compress: {
80
+ passes: Infinity,
81
+ unsafe: true,
82
+ unsafe_arrows: true,
83
+ unsafe_comps: true,
84
+ unsafe_symbols: true,
85
+ unsafe_methods: true,
86
+ unsafe_proto: true,
87
+ unsafe_regexp: true,
88
+ unsafe_undefined: true,
89
+ sequences: false
90
+ },
91
+ format: { semicolons: false },
92
+ keep_classnames: !mangleNames,
93
+ keep_fnames: !mangleNames
94
+ })).code!
95
+ .replace(new RegExp(`_PROTOTYPE_PROPERTY_${uniqueID}_`, "g"), `"prototype"`)
96
+ .replace(new RegExp(`_PROTO_PROPERTY_${uniqueID}_`, "g"), `"__proto__"`)
97
+ }
98
+
99
+ let comment: string | null = null
100
+ let hasComment = false
101
+
102
+ {
103
+ const file = await parse(code)
104
+ const promises: Promise<any>[] = []
105
+
106
+ traverse(file, {
107
+ FunctionDeclaration(path) {
108
+ path.traverse({
109
+ Function(path) {
110
+ if (path.parent.type != "CallExpression" && path.parentKey != "callee")
111
+ path.skip()
112
+ },
113
+
114
+ Loop(path) {
115
+ path.skip()
116
+ },
117
+
118
+ ObjectExpression(path) {
119
+ const o: Record<string, unknown> = {}
120
+
121
+ if (parseObjectExpression(path.node, o))
122
+ path.replaceWith(t.identifier(`_JSON_VALUE_${jsonValues.push(o) - 1}_${uniqueID}_`))
123
+ },
124
+
125
+ ArrayExpression(path) {
126
+ const o: unknown[] = []
127
+
128
+ if (parseArrayExpression(path.node, o))
129
+ path.replaceWith(t.identifier(`_JSON_VALUE_${jsonValues.push(o) - 1}_${uniqueID}_`))
130
+ }
131
+ })
132
+
133
+ path.traverse({
134
+ TemplateLiteral(path) {
135
+ const templateLiteral = path.node
136
+ let replacement: babel.Node = t.stringLiteral(templateLiteral.quasis[0].value.cooked!)
137
+
138
+ for (let i = 0; i < templateLiteral.expressions.length; i++) {
139
+ const expression = templateLiteral.expressions[i] as Expression
140
+ const templateElement = templateLiteral.quasis[i + 1]
141
+
142
+ replacement = t.binaryExpression(
143
+ "+",
144
+ replacement,
145
+ expression
146
+ )
147
+
148
+ if (!templateElement.value.cooked)
149
+ continue
150
+
151
+ replacement = t.binaryExpression(
152
+ "+",
153
+ replacement,
154
+ t.stringLiteral(templateElement.value.cooked!)
155
+ )
156
+ }
157
+
158
+ path.replaceWith(replacement)
159
+ },
160
+
161
+ MemberExpression({ node: memberExpression }) {
162
+ if (memberExpression.computed)
163
+ return
164
+
165
+ assert(memberExpression.property.type == "Identifier")
166
+
167
+ if (memberExpression.property.name.length < 3)
168
+ return
169
+
170
+ memberExpression.computed = true
171
+ memberExpression.property = t.stringLiteral(memberExpression.property.name)
172
+ },
173
+
174
+ UnaryExpression(path) {
175
+ if (path.node.operator == "void") {
176
+ if (path.node.argument.type == "NumericLiteral" && !path.node.argument.value) {
177
+ path.replaceWith(t.identifier(`_UNDEFINED_${uniqueID}_`))
178
+ undefinedIsReferenced = true
179
+ }
180
+ } else if (path.node.operator == "-" && path.node.argument.type == "NumericLiteral") {
181
+ const value = -path.node.argument.value
182
+
183
+ promises.push((async () => {
184
+ if ((await minifyNumber(value)).length <= 3)
185
+ return
186
+
187
+ if (path.parentKey == "key" && path.parent.type == "ObjectProperty")
188
+ path.parent.computed = true
189
+
190
+ let jsonValueIndex = jsonValues.indexOf(value)
191
+
192
+ if (jsonValueIndex == -1)
193
+ jsonValueIndex += jsonValues.push(value)
194
+
195
+ path.replaceWith(t.identifier(`_JSON_VALUE_${jsonValueIndex}_${uniqueID}_`))
196
+ })())
197
+
198
+ path.skip()
199
+ }
200
+ },
201
+
202
+ NullLiteral(path) {
203
+ let jsonValueIndex = jsonValues.indexOf(null)
204
+
205
+ if (jsonValueIndex == -1)
206
+ jsonValueIndex += jsonValues.push(null)
207
+
208
+ path.replaceWith(t.identifier(`_JSON_VALUE_${jsonValueIndex}_${uniqueID}_`))
209
+ },
210
+
211
+ BooleanLiteral(path) {
212
+ let jsonValueIndex = jsonValues.indexOf(path.node.value)
213
+
214
+ if (jsonValueIndex == -1)
215
+ jsonValueIndex += jsonValues.push(path.node.value)
216
+
217
+ path.replaceWith(t.identifier(`_JSON_VALUE_${jsonValueIndex}_${uniqueID}_`))
218
+ },
219
+
220
+ NumericLiteral(path) {
221
+ promises.push((async () => {
222
+ if ((await minifyNumber(path.node.value)).length <= 3)
223
+ return
224
+
225
+ if (path.parentKey == "key" && path.parent.type == "ObjectProperty")
226
+ path.parent.computed = true
227
+
228
+ let jsonValueIndex = jsonValues.indexOf(path.node.value)
229
+
230
+ if (jsonValueIndex == -1)
231
+ jsonValueIndex += jsonValues.push(path.node.value)
232
+
233
+ path.replaceWith(t.identifier(`_JSON_VALUE_${jsonValueIndex}_${uniqueID}_`))
234
+ })())
235
+ },
236
+
237
+ StringLiteral(path) {
238
+ if (path.node.value.includes("\u0000") || path.toString().length < 4)
239
+ return
240
+
241
+ if (path.parentKey == "key" && path.parent.type == "ObjectProperty")
242
+ path.parent.computed = true
243
+
244
+ let jsonValueIndex = jsonValues.indexOf(path.node.value)
245
+
246
+ if (jsonValueIndex == -1)
247
+ jsonValueIndex += jsonValues.push(path.node.value)
248
+
249
+ path.replaceWith(t.identifier(`_JSON_VALUE_${jsonValueIndex}_${uniqueID}_`))
250
+ },
251
+
252
+ ObjectProperty({ node }) {
253
+ if (node.computed || node.key.type != "Identifier" || node.key.name.length < 4)
254
+ return
255
+
256
+ let jsonValueIndex = jsonValues.indexOf(node.key.name)
257
+
258
+ if (jsonValueIndex == -1)
259
+ jsonValueIndex += jsonValues.push(node.key.name)
260
+
261
+ node.computed = true
262
+ node.key = t.identifier(`_JSON_VALUE_${jsonValueIndex}_${uniqueID}_`)
263
+ }
264
+ })
265
+
266
+ path.skip()
267
+ }
268
+ })
269
+
270
+ await Promise.all(promises)
271
+
272
+ const [ functionDeclaration ] = file.program.body
273
+
274
+ assert(functionDeclaration.type == "FunctionDeclaration")
275
+
276
+ if (jsonValues.length) {
277
+ hasComment = true
278
+
279
+ if (jsonValues.length == 1) {
280
+ if (typeof jsonValues[0] == "string" && !jsonValues[0].includes("\n") && !jsonValues[0].includes("\t")) {
281
+ const variableDeclaration = t.variableDeclaration(
282
+ "let",
283
+ [
284
+ t.variableDeclarator(
285
+ t.identifier(`_JSON_VALUE_0_${uniqueID}_`),
286
+ t.memberExpression(
287
+ t.taggedTemplateExpression(
288
+ t.memberExpression(
289
+ t.callExpression(t.identifier(`$${uniqueID}$SUBSCRIPT$scripts$quine`), []),
290
+ t.identifier("split")
291
+ ),
292
+ t.templateLiteral([ t.templateElement({ raw: "\t", cooked: "\t" }, true) ], [])
293
+ ),
294
+ t.identifier(`$${uniqueID}$SPLIT_INDEX`),
295
+ true
296
+ )
297
+ )
298
+ ]
299
+ )
300
+
301
+ if (undefinedIsReferenced)
302
+ variableDeclaration.declarations.push(t.variableDeclarator(t.identifier(`_UNDEFINED_${uniqueID}_`)))
303
+
304
+ functionDeclaration.body.body.unshift(variableDeclaration)
305
+
306
+ comment = jsonValues[0]
307
+ } else {
308
+ const variableDeclaration = t.variableDeclaration(
309
+ "let",
310
+ [
311
+ t.variableDeclarator(
312
+ t.identifier(`_JSON_VALUE_0_${uniqueID}_`),
313
+ t.callExpression(
314
+ t.memberExpression(
315
+ t.identifier("JSON"),
316
+ t.identifier("parse")
317
+ ),
318
+ [
319
+ t.memberExpression(
320
+ t.taggedTemplateExpression(
321
+ t.memberExpression(
322
+ t.callExpression(t.identifier(`$${uniqueID}$SUBSCRIPT$scripts$quine`), []),
323
+ t.identifier("split")
324
+ ),
325
+ t.templateLiteral([ t.templateElement({ raw: "\t", cooked: "\t" }, true) ], [])
326
+ ),
327
+ t.identifier(`$${uniqueID}$SPLIT_INDEX`),
328
+ true
329
+ )
330
+ ]
331
+ )
332
+ )
333
+ ]
334
+ )
335
+
336
+ if (undefinedIsReferenced)
337
+ variableDeclaration.declarations.push(t.variableDeclarator(t.identifier(`_UNDEFINED_${uniqueID}_`)))
338
+
339
+ functionDeclaration.body.body.unshift(variableDeclaration)
340
+
341
+ comment = JSON.stringify(jsonValues[0])
342
+ }
343
+ } else {
344
+ const variableDeclaration = t.variableDeclaration(
345
+ "let",
346
+ [
347
+ t.variableDeclarator(
348
+ t.arrayPattern(jsonValues.map((_, i) => t.identifier(`_JSON_VALUE_${i}_${uniqueID}_`))),
349
+ t.callExpression(
350
+ t.memberExpression(
351
+ t.identifier("JSON"),
352
+ t.identifier("parse")
353
+ ),
354
+ [
355
+ t.memberExpression(
356
+ t.taggedTemplateExpression(
357
+ t.memberExpression(
358
+ t.callExpression(t.identifier(`$${uniqueID}$SUBSCRIPT$scripts$quine`), []),
359
+ t.identifier("split")
360
+ ),
361
+ t.templateLiteral([ t.templateElement({ raw: "\t", cooked: "\t" }, true) ], [])
362
+ ),
363
+ t.identifier(`$${uniqueID}$SPLIT_INDEX`),
364
+ true
365
+ )
366
+ ]
367
+ )
368
+ )
369
+ ]
370
+ )
371
+
372
+ if (undefinedIsReferenced)
373
+ variableDeclaration.declarations.push(t.variableDeclarator(t.identifier(`_UNDEFINED_${uniqueID}_`)))
374
+
375
+ functionDeclaration.body.body.unshift(variableDeclaration)
376
+
377
+ comment = JSON.stringify(jsonValues)
378
+ }
379
+ } else if (undefinedIsReferenced) {
380
+ functionDeclaration.body.body.unshift(
381
+ t.variableDeclaration(
382
+ "let",
383
+ [ t.variableDeclarator(t.identifier(`_UNDEFINED_${uniqueID}_`)) ]
384
+ )
385
+ )
386
+ }
387
+
388
+ code = generate(file!).code
389
+ }
390
+
391
+ code = (await terser.minify(code, {
392
+ ecma: 2015,
393
+ compress: {
394
+ passes: Infinity,
395
+ unsafe: true,
396
+ unsafe_arrows: true,
397
+ unsafe_comps: true,
398
+ unsafe_symbols: true,
399
+ unsafe_methods: true,
400
+ unsafe_proto: true,
401
+ unsafe_regexp: true,
402
+ unsafe_undefined: true,
403
+ sequences: false
404
+ },
405
+ format: { semicolons: false },
406
+ keep_classnames: !mangleNames,
407
+ keep_fnames: !mangleNames
408
+ })).code || ""
409
+
410
+
411
+ // this step affects the character count and can't be done after the count comparison
412
+ if (comment != null) {
413
+ code = spliceString(code, `${autocomplete ? `//${autocomplete}\n` : ""}\n//\t${comment}\t\n`, getFunctionBodyStart(code) + 1)
414
+
415
+ for (const [ i, part ] of code.split("\t").entries()) {
416
+ if (part == comment) {
417
+ code = code.replace(`$${uniqueID}$SPLIT_INDEX`, await minifyNumber(i))
418
+ break
419
+ }
420
+ }
421
+ }
422
+
423
+ // if the script has a comment, it's gonna contain `SC$scripts$quine()`
424
+ // which is gonna eventually compile to `#fs.scripts.quine()` which contains
425
+ // an extra character so we have to account for that
426
+ if (countHackmudCharacters(scriptBeforeJSONValueReplacement) <= (countHackmudCharacters(code) + Number(hasComment))) {
427
+ code = scriptBeforeJSONValueReplacement
428
+
429
+ if (autocomplete)
430
+ code = spliceString(code, `//${autocomplete}\n`, getFunctionBodyStart(code) + 1)
431
+ }
432
+
433
+ return code
434
+ }
435
+
436
+ export default minify
437
+
438
+ function parseObjectExpression(node: babel.types.ObjectExpression, o: Record<string, unknown>) {
439
+ if (!node.properties.length)
440
+ return false
441
+
442
+ for (const property of node.properties) {
443
+ if (property.type != "ObjectProperty" || property.computed)
444
+ return false
445
+
446
+ assert(property.key.type == "Identifier" || property.key.type == "NumericLiteral" || property.key.type == "StringLiteral")
447
+
448
+ if (property.value.type == "ArrayExpression") {
449
+ const childArray: unknown[] = []
450
+
451
+ if (parseArrayExpression(property.value, childArray))
452
+ o[property.key.type == "Identifier" ? property.key.name : property.key.value] = childArray
453
+ else
454
+ return false
455
+ } else if (property.value.type == "ObjectExpression") {
456
+ const childObject: Record<string, unknown> = {}
457
+
458
+ if (parseObjectExpression(property.value, childObject))
459
+ o[property.key.type == "Identifier" ? property.key.name : property.key.value] = childObject
460
+ else
461
+ return false
462
+ } else if (property.value.type == "NullLiteral")
463
+ o[property.key.type == "Identifier" ? property.key.name : property.key.value] = null
464
+ else if (property.value.type == "BooleanLiteral" || property.value.type == "NumericLiteral" || property.value.type == "StringLiteral")
465
+ o[property.key.type == "Identifier" ? property.key.name : property.key.value] = property.value.value
466
+ else
467
+ return false
468
+ }
469
+
470
+ return true
471
+ }
472
+
473
+ function parseArrayExpression(node: babel.types.ArrayExpression, o: unknown[]) {
474
+ if (!node.elements.length)
475
+ return false
476
+
477
+ for (const element of node.elements) {
478
+ if (!element)
479
+ return false
480
+
481
+ if (element.type == "ArrayExpression") {
482
+ const childArray: unknown[] = []
483
+
484
+ if (parseArrayExpression(element, childArray))
485
+ childArray.push(childArray)
486
+ else
487
+ return false
488
+ } else if (element.type == "ObjectExpression") {
489
+ const childObject: Record<string, unknown> = {}
490
+
491
+ if (parseObjectExpression(element, childObject))
492
+ o.push(childObject)
493
+ else
494
+ return false
495
+ } else if (element.type == "NullLiteral")
496
+ o.push(null)
497
+ else if (element.type == "BooleanLiteral" || element.type == "NumericLiteral" || element.type == "StringLiteral")
498
+ o.push(element.value)
499
+ else
500
+ return false
501
+ }
502
+
503
+ return true
504
+ }
505
+
506
+ async function minifyNumber(number: number) {
507
+ return (await terser.minify(`$(${number})`, { ecma: 2015 })).code!.match(/\$\((.+)\)/)![1]
508
+ }
509
+
510
+ function getFunctionBodyStart(code: string) {
511
+ const tokens = tokenize(code, { ecmaVersion: 2015 })
512
+
513
+ tokens.getToken() // function
514
+ tokens.getToken() // name
515
+ tokens.getToken() // (
516
+
517
+ let nests = 1
518
+
519
+ while (nests) {
520
+ const token = tokens.getToken()
521
+
522
+ if (token.type == tokenTypes.parenL)
523
+ nests++
524
+ else if (token.type == tokenTypes.parenR)
525
+ nests--
526
+ }
527
+
528
+ return tokens.getToken().start // {
529
+ }
@@ -0,0 +1,38 @@
1
+ import { findMatches, spliceString } from "@samual/lib"
2
+
3
+ export function postprocess(code: string, seclevel: number, uniqueID: string) {
4
+ code = code.replace(/^function\s*\w+\(/, "function(")
5
+
6
+ for (const { index, match } of [ ...findMatches(new RegExp(`\\$${uniqueID}\\$[\\w$]+`, "g"), code) ].reverse()) {
7
+ const [ type, ...args ] = match.slice(13).split("$")
8
+
9
+ switch (type) {
10
+ case "SUBSCRIPT": {
11
+ code = spliceString(code, `#${"nlmhf"[seclevel]}s.${args[0]}.${args[1]}`, index, match.length)
12
+ } break
13
+
14
+ case "DEBUG": {
15
+ code = spliceString(code, `#D`, index, match.length)
16
+ } break
17
+
18
+ case "FMCL": {
19
+ code = spliceString(code, `#FMCL`, index, match.length)
20
+ } break
21
+
22
+ case "GLOBAL": {
23
+ code = spliceString(code, `#G`, index, match.length)
24
+ } break
25
+
26
+ case "DB": {
27
+ code = spliceString(code, `#db.${args[0]}`, index, match.length)
28
+ } break
29
+
30
+ default:
31
+ throw new Error(`unknown preprocessor directive type "${type}"`)
32
+ }
33
+ }
34
+
35
+ return code
36
+ }
37
+
38
+ export default postprocess
@@ -0,0 +1,146 @@
1
+ import { parse } from "@babel/parser"
2
+ import { assert, spliceString } from "@samual/lib"
3
+
4
+ export type PreprocessOptions = {
5
+ /** 11 a-z 0-9 characters */
6
+ uniqueID: string
7
+ }
8
+
9
+ /**
10
+ * @param code source code to be preprocessed
11
+ * @param options {@link PreprocessOptions details}
12
+ */
13
+ export function preprocess(code: string, { uniqueID = "00000000000" }: Partial<PreprocessOptions> = {}) {
14
+ assert(uniqueID.match(/^\w{11}$/))
15
+
16
+ let preScriptComments: string | undefined
17
+ let autocomplete: string | undefined
18
+
19
+ [ , preScriptComments, code, autocomplete ] = code.match(/((?:^\s*\/\/.*\n)*)\s*((?:.+?\/\/\s*(.+?)\s*$)?[^]*)/m)!
20
+
21
+ if (code.match(/(?:SC|DB)\$/))
22
+ throw new Error("SC$ and DB$ are protected and cannot appear in a script")
23
+
24
+ let seclevel: number | undefined
25
+
26
+ for (const line of preScriptComments.split("\n")) {
27
+ let [ , autocompleteMatch, seclevelMatch ] = (line.match(/^\s*\/\/\s*(?:@autocomplete\s*([^\s].*?)|@seclevel\s*([^\s].*?))\s*$/) || []) as [ never, string | undefined, string | undefined ]
28
+
29
+ if (autocompleteMatch)
30
+ autocomplete = autocompleteMatch
31
+ else if (seclevelMatch) {
32
+ if (seclevelMatch.match(/^(?:fullsec|f|4|fs|full)$/i))
33
+ seclevel = 4
34
+ else if (seclevelMatch.match(/^(?:highsec|h|3|hs|high)$/i))
35
+ seclevel = 3
36
+ else if (seclevelMatch.match(/^(?:midsec|m|2|ms|mid)$/i))
37
+ seclevel = 2
38
+ else if (seclevelMatch.match(/^(?:lowsec|l|1|ls|low)$/i))
39
+ seclevel = 1
40
+ else if (seclevelMatch.match(/^(?:nullsec|n|0|ns|null)$/i))
41
+ seclevel = 0
42
+ }
43
+ }
44
+
45
+ // TODO move this over to using the new system for finding subscripts
46
+
47
+ let detectedSeclevel = 4
48
+
49
+ if (code.match(/[#$][n0]s\.[a-z_][a-z_0-9]{0,24}\.[a-z_][a-z_0-9]{0,24}\(/))
50
+ detectedSeclevel = 0
51
+ else if (code.match(/[#$][l1]s\.[a-z_][a-z_0-9]{0,24}\.[a-z_][a-z_0-9]{0,24}\(/))
52
+ detectedSeclevel = 1
53
+ else if (code.match(/[#$][m2]s\.[a-z_][a-z_0-9]{0,24}\.[a-z_][a-z_0-9]{0,24}\(/))
54
+ detectedSeclevel = 2
55
+ else if (code.match(/[#$][h3]s\.[a-z_][a-z_0-9]{0,24}\.[a-z_][a-z_0-9]{0,24}\(/))
56
+ detectedSeclevel = 3
57
+
58
+ const seclevelNames = [ "NULLSEC", "LOWSEC", "MIDSEC", "HIGHSEC", "FULLSEC" ]
59
+
60
+ if (seclevel == undefined)
61
+ seclevel = detectedSeclevel
62
+ else if (detectedSeclevel < seclevel)
63
+ // TODO replace with a warning and build script anyway
64
+ throw new Error(`detected seclevel ${seclevelNames[detectedSeclevel]} is lower than stated seclevel ${seclevelNames[seclevel]}`)
65
+
66
+ const semicolons = code.match(/;/g)?.length ?? 0
67
+ const sourceCode = code
68
+
69
+ code = code.replace(/^function\s*\(/, "export default function (")
70
+
71
+ // TODO I'm not actually doing anything with this yet
72
+ let file
73
+
74
+ while (true) {
75
+ let error
76
+
77
+ try {
78
+ file = parse(code, {
79
+ plugins: [
80
+ "typescript",
81
+ [ "decorators", { decoratorsBeforeExport: true } ],
82
+ "doExpressions",
83
+ "functionBind",
84
+ "functionSent",
85
+ "partialApplication",
86
+ [ "pipelineOperator", { proposal: "hack", topicToken: "%" } ],
87
+ "throwExpressions",
88
+ [ "recordAndTuple", { syntaxType: "hash" } ],
89
+ "classProperties",
90
+ "classPrivateProperties",
91
+ "classPrivateMethods",
92
+ "logicalAssignment",
93
+ "numericSeparator",
94
+ "nullishCoalescingOperator",
95
+ "optionalChaining",
96
+ "optionalCatchBinding",
97
+ "objectRestSpread"
98
+ ],
99
+ sourceType: "module"
100
+ })
101
+ break
102
+ } catch (error_) {
103
+ assert(error_ instanceof SyntaxError)
104
+
105
+ error = error_ as SyntaxError & {
106
+ pos: number
107
+ code: string
108
+ reasonCode: String
109
+ }
110
+ }
111
+
112
+ if (error.code != "BABEL_PARSER_SYNTAX_ERROR" || error.reasonCode != "PrivateInExpectedIn") {
113
+ console.log(code.slice(error.pos).match(/.+/)?.[0])
114
+ throw error
115
+ }
116
+
117
+ const codeSlice = code.slice(error.pos)
118
+
119
+ let match
120
+
121
+ // TODO detect typos and warn e.g. we throw on `#db.ObjectID(` and it makes it look like we don't support it
122
+ if (match = codeSlice.match(/^#[fhmln43210]s\.scripts\.quine\(\)/))
123
+ code = spliceString(code, JSON.stringify(sourceCode), error.pos, match[0].length)
124
+ else if (match = codeSlice.match(/^#[fhmln43210]?s\.([a-z_][a-z_0-9]{0,24})\.([a-z_][a-z_0-9]{0,24})\(/))
125
+ code = spliceString(code, `$${uniqueID}$SUBSCRIPT$${match[1]}$${match[2]}(`, error.pos, match[0].length)
126
+ else if (match = codeSlice.match(/^#D\(/))
127
+ code = spliceString(code, `$${uniqueID}$DEBUG(`, error.pos, match[0].length)
128
+ else if (match = codeSlice.match(/^#FMCL/))
129
+ code = spliceString(code, `$${uniqueID}$FMCL`, error.pos, match[0].length)
130
+ else if (match = codeSlice.match(/^#G/))
131
+ code = spliceString(code, `$${uniqueID}$GLOBAL`, error.pos, match[0].length)
132
+ else if (match = codeSlice.match(/^#db\.(i|r|f|u|u1|us|ObjectId)\(/))
133
+ code = spliceString(code, `$${uniqueID}$DB$${match[1]}(`, error.pos, match[0].length)
134
+ else
135
+ throw error
136
+ }
137
+
138
+ return {
139
+ semicolons,
140
+ autocomplete,
141
+ seclevel,
142
+ code
143
+ }
144
+ }
145
+
146
+ export default preprocess