@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 ADDED
@@ -0,0 +1,15 @@
1
+ module.exports = {
2
+ "env": {
3
+ "node": true,
4
+ "commonjs": true,
5
+ "es2021": true
6
+ },
7
+ "extends": "eslint:recommended",
8
+ "overrides": [
9
+ ],
10
+ "parserOptions": {
11
+ "ecmaVersion": "latest"
12
+ },
13
+ "rules": {
14
+ }
15
+ }
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.8.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",