@appthreat/atom 0.8.0 → 0.9.0
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/.eslintrc.js +15 -0
- package/astgen.js +475 -0
- package/package.json +13 -3
- package/plugins/atom-1.0.0/lib/io.appthreat.atom-1.0.0.jar +0 -0
package/.eslintrc.js
ADDED
package/astgen.js
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const yargs = require("yargs");
|
|
5
|
+
const { hideBin } = require("yargs/helpers");
|
|
6
|
+
const babelParser = require("@babel/parser");
|
|
7
|
+
const tsc = require("typescript");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
|
|
10
|
+
const ASTGEN_VERSION = "3.1.0";
|
|
11
|
+
|
|
12
|
+
const IGNORE_DIRS = [
|
|
13
|
+
"node_modules",
|
|
14
|
+
"venv",
|
|
15
|
+
"docs",
|
|
16
|
+
"test",
|
|
17
|
+
"tests",
|
|
18
|
+
"e2e",
|
|
19
|
+
"e2e-beta",
|
|
20
|
+
"examples",
|
|
21
|
+
"cypress",
|
|
22
|
+
"jest-cache",
|
|
23
|
+
"eslint-rules",
|
|
24
|
+
"codemods",
|
|
25
|
+
"flow-typed",
|
|
26
|
+
"i18n",
|
|
27
|
+
"vendor",
|
|
28
|
+
"www",
|
|
29
|
+
"dist",
|
|
30
|
+
"build"
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const IGNORE_FILE_PATTERN = new RegExp(
|
|
34
|
+
"(conf|test|spec|\\.d)\\.(js|ts|jsx|tsx)$",
|
|
35
|
+
"i"
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const getAllFiles = (dir, extn, files, result, regex) => {
|
|
39
|
+
files = files || fs.readdirSync(dir);
|
|
40
|
+
result = result || [];
|
|
41
|
+
regex = regex || new RegExp(`\\${extn}$`);
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < files.length; i++) {
|
|
44
|
+
const file = files[i];
|
|
45
|
+
if (
|
|
46
|
+
file.startsWith(".") ||
|
|
47
|
+
file.startsWith("__") ||
|
|
48
|
+
IGNORE_FILE_PATTERN.test(file)
|
|
49
|
+
) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const fileWithDir = path.join(dir, file);
|
|
53
|
+
if (fs.statSync(fileWithDir).isDirectory()) {
|
|
54
|
+
// Ignore directories
|
|
55
|
+
const dirName = path.basename(fileWithDir);
|
|
56
|
+
if (
|
|
57
|
+
dirName.startsWith(".") ||
|
|
58
|
+
dirName.startsWith("__") ||
|
|
59
|
+
IGNORE_DIRS.includes(dirName.toLowerCase())
|
|
60
|
+
) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
result = getAllFiles(
|
|
65
|
+
fileWithDir,
|
|
66
|
+
extn,
|
|
67
|
+
fs.readdirSync(fileWithDir),
|
|
68
|
+
result,
|
|
69
|
+
regex
|
|
70
|
+
);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// ignore
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
if (regex.test(fileWithDir)) {
|
|
76
|
+
result.push(fileWithDir);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const babelParserOptions = {
|
|
84
|
+
sourceType: "unambiguous",
|
|
85
|
+
allowImportExportEverywhere: true,
|
|
86
|
+
allowAwaitOutsideFunction: true,
|
|
87
|
+
allowNewTargetOutsideFunction: true,
|
|
88
|
+
allowReturnOutsideFunction: true,
|
|
89
|
+
allowSuperOutsideMethod: true,
|
|
90
|
+
allowUndeclaredExports: true,
|
|
91
|
+
errorRecovery: true,
|
|
92
|
+
plugins: [
|
|
93
|
+
"optionalChaining",
|
|
94
|
+
"classProperties",
|
|
95
|
+
"decorators-legacy",
|
|
96
|
+
"exportDefaultFrom",
|
|
97
|
+
"doExpressions",
|
|
98
|
+
"numericSeparator",
|
|
99
|
+
"dynamicImport",
|
|
100
|
+
"jsx",
|
|
101
|
+
"typescript"
|
|
102
|
+
]
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const babelSafeParserOptions = {
|
|
106
|
+
sourceType: "module",
|
|
107
|
+
allowImportExportEverywhere: true,
|
|
108
|
+
allowAwaitOutsideFunction: true,
|
|
109
|
+
allowReturnOutsideFunction: true,
|
|
110
|
+
errorRecovery: true,
|
|
111
|
+
plugins: [
|
|
112
|
+
"optionalChaining",
|
|
113
|
+
"classProperties",
|
|
114
|
+
"decorators-legacy",
|
|
115
|
+
"exportDefaultFrom",
|
|
116
|
+
"doExpressions",
|
|
117
|
+
"numericSeparator",
|
|
118
|
+
"dynamicImport",
|
|
119
|
+
"typescript"
|
|
120
|
+
]
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Return paths to all (j|tsx?) files.
|
|
125
|
+
*/
|
|
126
|
+
const getAllSrcJSAndTSFiles = (src) =>
|
|
127
|
+
Promise.all([
|
|
128
|
+
getAllFiles(src, ".js"),
|
|
129
|
+
getAllFiles(src, ".jsx"),
|
|
130
|
+
getAllFiles(src, ".cjs"),
|
|
131
|
+
getAllFiles(src, ".mjs"),
|
|
132
|
+
getAllFiles(src, ".ts"),
|
|
133
|
+
getAllFiles(src, ".tsx")
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Convert a single JS/TS file to AST
|
|
138
|
+
*/
|
|
139
|
+
const fileToJsAst = (file) => {
|
|
140
|
+
try {
|
|
141
|
+
return babelParser.parse(
|
|
142
|
+
fs.readFileSync(file, "utf-8"),
|
|
143
|
+
babelParserOptions
|
|
144
|
+
);
|
|
145
|
+
} catch {
|
|
146
|
+
return babelParser.parse(
|
|
147
|
+
fs.readFileSync(file, "utf-8"),
|
|
148
|
+
babelSafeParserOptions
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Convert a single JS/TS code snippet to AST
|
|
155
|
+
*/
|
|
156
|
+
const codeToJsAst = (code) => {
|
|
157
|
+
try {
|
|
158
|
+
return babelParser.parse(code, babelParserOptions);
|
|
159
|
+
} catch {
|
|
160
|
+
return babelParser.parse(code, babelSafeParserOptions);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const vueCleaningRegex = /<\/*script.*>|<style[\s\S]*style>|<\/*br>/gi;
|
|
165
|
+
const vueTemplateRegex = /(<template.*>)([\s\S]*)(<\/template>)/gi;
|
|
166
|
+
const vueCommentRegex = /<!--[\s\S]*?-->/gi;
|
|
167
|
+
const vueBindRegex = /(:\[)([\s\S]*?)(\])/gi;
|
|
168
|
+
const vuePropRegex = /\s([.:@])([a-zA-Z]*?=)/gi;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Convert a single vue file to AST
|
|
172
|
+
*/
|
|
173
|
+
const toVueAst = (file) => {
|
|
174
|
+
const code = fs.readFileSync(file, "utf-8");
|
|
175
|
+
const cleanedCode = code
|
|
176
|
+
.replace(vueCommentRegex, function (match) {
|
|
177
|
+
return match.replaceAll(/\S/g, " ");
|
|
178
|
+
})
|
|
179
|
+
.replace(vueCleaningRegex, function (match) {
|
|
180
|
+
return match.replaceAll(/\S/g, " ").substring(1) + ";";
|
|
181
|
+
})
|
|
182
|
+
.replace(vueBindRegex, function (match, grA, grB, grC) {
|
|
183
|
+
return grA.replaceAll(/\S/g, " ") + grB + grC.replaceAll(/\S/g, " ");
|
|
184
|
+
})
|
|
185
|
+
.replace(vuePropRegex, function (match, grA, grB) {
|
|
186
|
+
return " " + grA.replace(/[.:@]/g, " ") + grB;
|
|
187
|
+
})
|
|
188
|
+
.replace(vueTemplateRegex, function (match, grA, grB, grC) {
|
|
189
|
+
return grA + grB.replaceAll("{{", "{ ").replaceAll("}}", " }") + grC;
|
|
190
|
+
});
|
|
191
|
+
return codeToJsAst(cleanedCode);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
function createTsc(srcFiles) {
|
|
195
|
+
try {
|
|
196
|
+
const program = tsc.createProgram(srcFiles, {
|
|
197
|
+
target: tsc.ScriptTarget.ES2020,
|
|
198
|
+
module: tsc.ModuleKind.CommonJS,
|
|
199
|
+
allowJs: true,
|
|
200
|
+
allowUnreachableCode: true,
|
|
201
|
+
allowUnusedLabels: true,
|
|
202
|
+
alwaysStrict: false,
|
|
203
|
+
emitDecoratorMetadata: true,
|
|
204
|
+
exactOptionalPropertyTypes: true,
|
|
205
|
+
experimentalDecorators: false,
|
|
206
|
+
ignoreDeprecations: true,
|
|
207
|
+
noStrictGenericChecks: true,
|
|
208
|
+
noUncheckedIndexedAccess: false,
|
|
209
|
+
noPropertyAccessFromIndexSignature: false,
|
|
210
|
+
removeComments: true
|
|
211
|
+
});
|
|
212
|
+
const typeChecker = program.getTypeChecker();
|
|
213
|
+
const seenTypes = new Map();
|
|
214
|
+
|
|
215
|
+
const safeTypeToString = (node) => {
|
|
216
|
+
try {
|
|
217
|
+
return typeChecker.typeToString(
|
|
218
|
+
node,
|
|
219
|
+
tsc.TypeFormatFlags.NoTruncation | tsc.TypeFormatFlags.InTypeAlias
|
|
220
|
+
);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
return "any";
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const safeTypeWithContextToString = (node, context) => {
|
|
227
|
+
try {
|
|
228
|
+
return typeChecker.typeToString(
|
|
229
|
+
node,
|
|
230
|
+
context,
|
|
231
|
+
tsc.TypeFormatFlags.NoTruncation | tsc.TypeFormatFlags.InTypeAlias
|
|
232
|
+
);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
return "any";
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const addType = (node) => {
|
|
239
|
+
let typeStr;
|
|
240
|
+
if (
|
|
241
|
+
tsc.isSetAccessor(node) ||
|
|
242
|
+
tsc.isGetAccessor(node) ||
|
|
243
|
+
tsc.isConstructSignatureDeclaration(node) ||
|
|
244
|
+
tsc.isMethodDeclaration(node) ||
|
|
245
|
+
tsc.isFunctionDeclaration(node) ||
|
|
246
|
+
tsc.isConstructorDeclaration(node)
|
|
247
|
+
) {
|
|
248
|
+
const signature = typeChecker.getSignatureFromDeclaration(node);
|
|
249
|
+
const returnType = typeChecker.getReturnTypeOfSignature(signature);
|
|
250
|
+
typeStr = safeTypeToString(returnType);
|
|
251
|
+
} else if (tsc.isFunctionLike(node)) {
|
|
252
|
+
const funcType = typeChecker.getTypeAtLocation(node);
|
|
253
|
+
const funcSignature = typeChecker.getSignaturesOfType(
|
|
254
|
+
funcType,
|
|
255
|
+
tsc.SignatureKind.Call
|
|
256
|
+
)[0];
|
|
257
|
+
if (funcSignature) {
|
|
258
|
+
typeStr = safeTypeToString(funcSignature.getReturnType());
|
|
259
|
+
} else {
|
|
260
|
+
typeStr = safeTypeWithContextToString(
|
|
261
|
+
typeChecker.getTypeAtLocation(node),
|
|
262
|
+
node
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
typeStr = safeTypeWithContextToString(
|
|
267
|
+
typeChecker.getTypeAtLocation(node),
|
|
268
|
+
node
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (!["any", "unknown", "any[]", "unknown[]"].includes(typeStr))
|
|
272
|
+
seenTypes.set(node.getStart(), typeStr);
|
|
273
|
+
tsc.forEachChild(node, addType);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
program: program,
|
|
278
|
+
typeChecker: typeChecker,
|
|
279
|
+
addType: addType,
|
|
280
|
+
seenTypes: seenTypes
|
|
281
|
+
};
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.warn("Retrieving types", err.message);
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Generate AST for JavaScript or TypeScript
|
|
290
|
+
*/
|
|
291
|
+
const createJSAst = async (options) => {
|
|
292
|
+
try {
|
|
293
|
+
const promiseMap = await getAllSrcJSAndTSFiles(options.src);
|
|
294
|
+
const srcFiles = promiseMap.flatMap((d) => d);
|
|
295
|
+
let ts;
|
|
296
|
+
if (options.tsTypes) {
|
|
297
|
+
ts = createTsc(srcFiles);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const file of srcFiles) {
|
|
301
|
+
try {
|
|
302
|
+
const ast = fileToJsAst(file);
|
|
303
|
+
writeAstFile(file, ast, options);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.error(file, err.message);
|
|
306
|
+
}
|
|
307
|
+
if (ts) {
|
|
308
|
+
try {
|
|
309
|
+
const tsAst = ts.program.getSourceFile(file);
|
|
310
|
+
tsc.forEachChild(tsAst, ts.addType);
|
|
311
|
+
writeTypesFile(file, ts.seenTypes, options);
|
|
312
|
+
ts.seenTypes.clear();
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.warn("Retrieving types", file, ":", err.message);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch (err) {
|
|
319
|
+
console.error(err);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Generate AST for .vue files
|
|
325
|
+
*/
|
|
326
|
+
const createVueAst = async (options) => {
|
|
327
|
+
const srcFiles = getAllFiles(options.src, ".vue");
|
|
328
|
+
for (const file of srcFiles) {
|
|
329
|
+
try {
|
|
330
|
+
const ast = toVueAst(file);
|
|
331
|
+
if (ast) {
|
|
332
|
+
writeAstFile(file, ast, options);
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.error(file, err.message);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Deal with cyclic reference in json
|
|
342
|
+
*/
|
|
343
|
+
const getCircularReplacer = () => {
|
|
344
|
+
const seen = new WeakSet();
|
|
345
|
+
return (key, value) => {
|
|
346
|
+
if (typeof value === "object" && value !== null) {
|
|
347
|
+
if (seen.has(value)) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
seen.add(value);
|
|
351
|
+
}
|
|
352
|
+
return value;
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Write AST data to a json file
|
|
358
|
+
*/
|
|
359
|
+
const writeAstFile = (file, ast, options) => {
|
|
360
|
+
const relativePath = file.replace(new RegExp("^" + options.src + "/"), "");
|
|
361
|
+
const outAstFile = path.join(options.output, relativePath + ".json");
|
|
362
|
+
const data = {
|
|
363
|
+
fullName: file,
|
|
364
|
+
relativeName: relativePath,
|
|
365
|
+
ast: ast
|
|
366
|
+
};
|
|
367
|
+
fs.mkdirSync(path.dirname(outAstFile), { recursive: true });
|
|
368
|
+
fs.writeFileSync(
|
|
369
|
+
outAstFile,
|
|
370
|
+
JSON.stringify(data, getCircularReplacer(), " ")
|
|
371
|
+
);
|
|
372
|
+
console.log("Converted AST for", relativePath, "to", outAstFile);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const writeTypesFile = (file, seenTypes, options) => {
|
|
376
|
+
const relativePath = file.replace(new RegExp("^" + options.src + "/"), "");
|
|
377
|
+
const outTypeFile = path.join(options.output, relativePath + ".typemap");
|
|
378
|
+
fs.mkdirSync(path.dirname(outTypeFile), { recursive: true });
|
|
379
|
+
fs.writeFileSync(
|
|
380
|
+
outTypeFile,
|
|
381
|
+
JSON.stringify(Object.fromEntries(seenTypes), undefined, " ")
|
|
382
|
+
);
|
|
383
|
+
console.log("Converted types for", relativePath, "to", outTypeFile);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const createXAst = async (options) => {
|
|
387
|
+
const src_dir = options.src;
|
|
388
|
+
try {
|
|
389
|
+
fs.accessSync(src_dir, fs.constants.R_OK);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error(src_dir, "is invalid");
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
if (
|
|
395
|
+
fs.existsSync(path.join(src_dir, "package.json")) ||
|
|
396
|
+
fs.existsSync(path.join(src_dir, "rush.json"))
|
|
397
|
+
) {
|
|
398
|
+
return await createJSAst(options);
|
|
399
|
+
}
|
|
400
|
+
console.error(src_dir, "unknown project type");
|
|
401
|
+
process.exit(1);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Method to start the ast generation process
|
|
406
|
+
*
|
|
407
|
+
* @args options CLI arguments
|
|
408
|
+
*/
|
|
409
|
+
const start = async (options) => {
|
|
410
|
+
let { type } = options;
|
|
411
|
+
if (!type) {
|
|
412
|
+
type = "";
|
|
413
|
+
}
|
|
414
|
+
type = type.toLowerCase();
|
|
415
|
+
switch (type) {
|
|
416
|
+
case "nodejs":
|
|
417
|
+
case "js":
|
|
418
|
+
case "javascript":
|
|
419
|
+
case "typescript":
|
|
420
|
+
case "ts":
|
|
421
|
+
return await createJSAst(options);
|
|
422
|
+
case "vue":
|
|
423
|
+
return await createVueAst(options);
|
|
424
|
+
default:
|
|
425
|
+
return await createXAst(options);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
async function main(argvs) {
|
|
430
|
+
const args = yargs(hideBin(argvs))
|
|
431
|
+
.option("src", {
|
|
432
|
+
alias: "i",
|
|
433
|
+
default: ".",
|
|
434
|
+
description: "Source directory"
|
|
435
|
+
})
|
|
436
|
+
.option("output", {
|
|
437
|
+
alias: "o",
|
|
438
|
+
default: "ast_out",
|
|
439
|
+
description: "Output directory for generated AST json files"
|
|
440
|
+
})
|
|
441
|
+
.option("type", {
|
|
442
|
+
alias: "t",
|
|
443
|
+
description: "Project type. Default auto-detect"
|
|
444
|
+
})
|
|
445
|
+
.option("recurse", {
|
|
446
|
+
alias: "r",
|
|
447
|
+
default: true,
|
|
448
|
+
type: "boolean",
|
|
449
|
+
description: "Recurse mode suitable for mono-repos"
|
|
450
|
+
})
|
|
451
|
+
.option("tsTypes", {
|
|
452
|
+
default: true,
|
|
453
|
+
type: "boolean",
|
|
454
|
+
description: "Generate type mappings using the Typescript Compiler API"
|
|
455
|
+
})
|
|
456
|
+
.version(ASTGEN_VERSION)
|
|
457
|
+
.help("h").argv;
|
|
458
|
+
|
|
459
|
+
if (args.version) {
|
|
460
|
+
console.log(ASTGEN_VERSION);
|
|
461
|
+
process.exit(0);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
if (args.output === "ast_out") {
|
|
466
|
+
args.output = path.join(args.src, args.output);
|
|
467
|
+
}
|
|
468
|
+
await start(args);
|
|
469
|
+
} catch (e) {
|
|
470
|
+
console.error(e);
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
main(process.argv);
|
package/package.json
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appthreat/atom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Create atom (⚛) representation for your application, packages and libraries",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"pretty": "prettier --write *.js --trailing-comma=none"
|
|
7
|
+
"pretty": "prettier --write *.js --trailing-comma=none",
|
|
8
|
+
"lint": "eslint *.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@babel/parser": "^7.22.5",
|
|
12
|
+
"typescript": "^5.1.3",
|
|
13
|
+
"yargs": "^17.7.2"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"eslint": "^8.42.0"
|
|
8
17
|
},
|
|
9
18
|
"bin": {
|
|
10
|
-
"atom": "./index.js"
|
|
19
|
+
"atom": "./index.js",
|
|
20
|
+
"astgen": "./astgen.js"
|
|
11
21
|
},
|
|
12
22
|
"repository": {
|
|
13
23
|
"type": "git",
|
|
Binary file
|