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.
- package/.gitattributes +1 -0
- package/.github/workflows/codeql-analysis.yml +39 -0
- package/.github/workflows/publish.yml +42 -0
- package/.vscode/settings.json +6 -0
- package/babel.config.json +6 -0
- package/package.json +12 -21
- package/rollup.config.js +110 -0
- package/scripts/build-package-json.js +36 -0
- package/scripts/jsconfig.json +5 -0
- package/scripts/version-dev.js +25 -0
- package/src/bin/hsm.ts +505 -0
- package/src/constants.json +3 -0
- package/src/generateTypings.ts +116 -0
- package/src/index.ts +19 -0
- package/src/modules.d.ts +5 -0
- package/src/processScript/index.ts +198 -0
- package/src/processScript/minify.ts +529 -0
- package/src/processScript/postprocess.ts +38 -0
- package/src/processScript/preprocess.ts +146 -0
- package/src/processScript/transform.ts +760 -0
- package/src/pull.ts +16 -0
- package/src/push.ts +314 -0
- package/src/syncMacros.ts +52 -0
- package/src/test.ts +59 -0
- package/src/tsconfig.json +20 -0
- package/src/watch.ts +156 -0
- package/tsconfig.json +12 -0
- package/assert-1b7dada8.js +0 -1
- package/bin/hsm.d.ts +0 -2
- package/bin/hsm.js +0 -2
- package/generateTypings.d.ts +0 -2
- package/generateTypings.js +0 -1
- package/index.d.ts +0 -15
- package/index.js +0 -1
- package/processScript/compile.d.ts +0 -17
- package/processScript/compile.js +0 -1
- package/processScript/index.d.ts +0 -30
- package/processScript/index.js +0 -1
- package/processScript/minify.d.ts +0 -7
- package/processScript/minify.js +0 -1
- package/processScript/postProcess.d.ts +0 -2
- package/processScript/postProcess.js +0 -1
- package/processScript/preProcess.d.ts +0 -15
- package/processScript/preProcess.js +0 -1
- package/pull.d.ts +0 -9
- package/pull.js +0 -1
- package/push.d.ts +0 -26
- package/push.js +0 -1
- package/spliceString-2c6f214f.js +0 -1
- package/syncMacros.d.ts +0 -5
- package/syncMacros.js +0 -1
- package/test.d.ts +0 -6
- package/test.js +0 -1
- package/watch.d.ts +0 -14
- 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
|