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

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 (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