@groma/scanner-javascript 0.2.0 → 0.2.2

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 (3) hide show
  1. package/README.md +5 -4
  2. package/dist/index.js +85 -68
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -13,7 +13,7 @@ bun plugins/scanners/javascript/build.ts
13
13
  From the project being scanned:
14
14
 
15
15
  ```sh
16
- groma scanner add /absolute/path/to/groma/plugins/scanners/javascript/dist/package
16
+ groma plugin add /absolute/path/to/groma/plugins/scanners/javascript/dist/package
17
17
  groma scan
18
18
  ```
19
19
 
@@ -42,10 +42,10 @@ output, such as `jquery-ui.min.js`, and `node_modules`, `dist`, `build` and
42
42
  lists select.
43
43
 
44
44
  A bundle under an ordinary name, such as `vendor.js`, is read like any other
45
- source; a project that keeps it out adds a pattern to `scanners.json`.
45
+ source; a project that keeps it out adds a pattern to `config.json`.
46
46
 
47
47
  Test sources are treated like any other JavaScript source: the defaults name
48
- none, so a project that keeps them out adds a pattern to `scanners.json`.
48
+ none, so a project that keeps them out adds a pattern to `config.json`.
49
49
 
50
50
  TypeScript sources belong to the [TypeScript scanner](../typescript/index.md)
51
51
  and single-file components to the [Vue scanner](../vue/index.md). This scanner
@@ -78,7 +78,8 @@ assignments or `cross-env` may come before the runtime, and runtime options and
78
78
  a `run` or `watch` subcommand before the script; the first remaining word must
79
79
  name the script, so a preloaded module or `bun build` supplies none. An HTML
80
80
  page's script supplies one too, named after the page, or after its folder for
81
- an `index.html`.
81
+ an `index.html`. A `package.json`, `angular.json` or `project.json` that is not
82
+ valid JSON supplies none: the scan continues and reports a warning for that file.
82
83
 
83
84
  ## Source outline
84
85
 
package/dist/index.js CHANGED
@@ -118,13 +118,13 @@ function compare(...values) {
118
118
  }
119
119
  function uniqueBy(values, key, label) {
120
120
  const seen = new Set;
121
- for (const value of values) {
122
- const id = key(value);
121
+ const keyed = values.map((value) => ({ id: key(value), value }));
122
+ for (const { id } of keyed) {
123
123
  if (seen.has(id))
124
124
  throw new Error(`duplicate ${label}: ${id}`);
125
125
  seen.add(id);
126
126
  }
127
- return [...values].sort((left, right) => key(left).localeCompare(key(right)));
127
+ return keyed.sort((left, right) => left.id.localeCompare(right.id)).map((entry) => entry.value);
128
128
  }
129
129
  function createScanObservation(input) {
130
130
  const roots = validateRoots(input.roots);
@@ -135,9 +135,7 @@ function createScanObservation(input) {
135
135
  symbols: [...new Map(file.symbols.map((symbol) => [
136
136
  compare(symbol.id, symbol.kind),
137
137
  symbol
138
- ])).values()].sort((left, right) => {
139
- return compare(left.id, left.kind).localeCompare(compare(right.id, right.kind));
140
- })
138
+ ]))].sort(([left], [right]) => left.localeCompare(right)).map(([, symbol]) => symbol)
141
139
  })), (file) => file.file, "file path");
142
140
  const filePaths = new Set(files.map((file) => file.file));
143
141
  for (const file of files) {
@@ -148,8 +146,6 @@ function createScanObservation(input) {
148
146
  throw new Error(`file references unknown root: ${root}`);
149
147
  }
150
148
  }
151
- for (const diagnostic of input.diagnostics)
152
- diagnosticLocation(diagnostic);
153
149
  return {
154
150
  schemaVersion: 1,
155
151
  scanner: input.scanner,
@@ -159,14 +155,14 @@ function createScanObservation(input) {
159
155
  ...entryPoints(input.entryPoints, filePaths),
160
156
  ...operationEvidence(input, filePaths),
161
157
  ...httpEvidence(input.httpEndpoints, input.httpRequests, input.operations && new Set(input.operations.map((operation) => operation.id))),
162
- diagnostics: [...new Map(input.diagnostics.map((diagnostic) => [
163
- diagnosticKey(diagnostic),
164
- diagnostic
165
- ])).values()].sort((left, right) => {
166
- return diagnosticKey(left).localeCompare(diagnosticKey(right));
167
- })
158
+ diagnostics: normalizeScanDiagnostics(input.diagnostics)
168
159
  };
169
160
  }
161
+ function normalizeScanDiagnostics(diagnostics) {
162
+ for (const diagnostic of diagnostics)
163
+ diagnosticLocation(diagnostic);
164
+ return [...new Map(diagnostics.map((diagnostic) => [diagnosticKey(diagnostic), diagnostic]))].sort(([left], [right]) => left.localeCompare(right)).map(([, diagnostic]) => diagnostic);
165
+ }
170
166
  function entryPoints(input, paths) {
171
167
  if (input === undefined)
172
168
  return {};
@@ -275,10 +271,14 @@ function operationTokens(operation) {
275
271
  const endLine = Number(operation.endLine);
276
272
  if (endLine < startLine)
277
273
  throw new Error("operation.endLine must be at or after startLine");
274
+ const tokens = array(operation.tokens, "operation.tokens");
275
+ tokens.forEach((token) => {
276
+ string(token, "operation.token");
277
+ });
278
278
  return {
279
279
  startLine,
280
280
  endLine,
281
- tokens: array(operation.tokens, "operation.tokens").map((token) => string(token, "operation.token"))
281
+ tokens
282
282
  };
283
283
  }
284
284
  function validateOperationTokens(operation) {
@@ -308,7 +308,7 @@ function sourcePosition(position) {
308
308
  }
309
309
 
310
310
  // plugins/scanners/javascript/src/index.ts
311
- import ts7 from "./typescript.cjs";
311
+ import ts8 from "./typescript.cjs";
312
312
 
313
313
  // plugins/scanners/javascript/src/evidence.ts
314
314
  import ts2 from "./typescript.cjs";
@@ -643,15 +643,15 @@ function topLevelDeclarations(source) {
643
643
 
644
644
  // plugins/scanners/javascript/src/evidence.ts
645
645
  var { executable, operationFields } = typeScriptOperations(ts2);
646
- function javaScriptEvidence(file, source) {
646
+ function javaScriptEvidence(file, syntax) {
647
647
  const operations = [];
648
648
  const invocations = [];
649
649
  const calls = [];
650
650
  const owners = new Map;
651
- const lineOf = (position) => source.getLineAndCharacterOfPosition(position).line + 1;
651
+ const lineOf = (position) => syntax.getLineAndCharacterOfPosition(position).line + 1;
652
652
  let moduleCode;
653
653
  function operation(node) {
654
- const position = node.getStart(source);
654
+ const position = node.getStart(syntax);
655
655
  const recorded = { id: `${file}#${position}`, file, position, ...operationFields(node) };
656
656
  operations.push(recorded);
657
657
  owners.set(node, recorded.id);
@@ -659,13 +659,13 @@ function javaScriptEvidence(file, source) {
659
659
  }
660
660
  function moduleOperation() {
661
661
  if (moduleCode === undefined) {
662
- moduleCode = { id: `${file}#module`, file, name: "(anonymous)", position: source.getStart(source) };
662
+ moduleCode = { id: `${file}#module`, file, name: "(anonymous)", position: syntax.getStart(syntax) };
663
663
  operations.push(moduleCode);
664
664
  }
665
665
  return moduleCode;
666
666
  }
667
667
  function invocation(node, owner) {
668
- const position = node.getStart(source);
668
+ const position = node.getStart(syntax);
669
669
  invocations.push({
670
670
  source: owner.id,
671
671
  targets: [],
@@ -691,8 +691,8 @@ function javaScriptEvidence(file, source) {
691
691
  }
692
692
  return moduleOperation().id;
693
693
  }
694
- source.forEachChild((child) => visit(child, undefined));
695
- const symbols = new Map(topLevelDeclarations(source).map((declaration) => [`${file}#${declaration.name}`, { id: `${file}#${declaration.name}`, ...declaration }]));
694
+ syntax.forEachChild((child) => visit(child, undefined));
695
+ const symbols = new Map(topLevelDeclarations(syntax).map((declaration) => [`${file}#${declaration.name}`, { id: `${file}#${declaration.name}`, ...declaration }]));
696
696
  return { symbols: [...symbols.values()], operations, invocations, calls, operationAt };
697
697
  }
698
698
 
@@ -2288,10 +2288,10 @@ async function httpRequest(context, call) {
2288
2288
  }
2289
2289
 
2290
2290
  // plugins/scanners/javascript/src/http.ts
2291
- function fileChecker(source) {
2291
+ function fileChecker(syntax) {
2292
2292
  const options = { allowJs: true, noLib: true, noResolve: true, types: [], noEmit: true };
2293
2293
  const host = {
2294
- getSourceFile: (name) => name === source.fileName ? source : undefined,
2294
+ getSourceFile: (name) => name === syntax.fileName ? syntax : undefined,
2295
2295
  getDefaultLibFileName: () => "lib.d.ts",
2296
2296
  writeFile: () => {
2297
2297
  return;
@@ -2301,18 +2301,18 @@ function fileChecker(source) {
2301
2301
  useCaseSensitiveFileNames: () => true,
2302
2302
  getNewLine: () => `
2303
2303
  `,
2304
- fileExists: (name) => name === source.fileName,
2304
+ fileExists: (name) => name === syntax.fileName,
2305
2305
  readFile: () => {
2306
2306
  return;
2307
2307
  }
2308
2308
  };
2309
- return ts4.createProgram([source.fileName], options, host).getTypeChecker();
2309
+ return ts4.createProgram([syntax.fileName], options, host).getTypeChecker();
2310
2310
  }
2311
2311
  function classic(node) {
2312
2312
  return node;
2313
2313
  }
2314
- function fileContext(source, evidence) {
2315
- const context = urlContext(ts4, classicChecker(ts4, fileChecker(source)), [source], true);
2314
+ function fileContext({ file, syntax }, evidence) {
2315
+ const context = urlContext(ts4, classicChecker(ts4, fileChecker(syntax)), [syntax], true);
2316
2316
  const operationAt = (node) => evidence.operationAt(classic(node));
2317
2317
  return {
2318
2318
  ...context,
@@ -2323,7 +2323,7 @@ function fileContext(source, evidence) {
2323
2323
  return typeof held === "object" && ts4.isFunctionLike(classic(held.node)) ? operationAt(held.node) : operationAt(registration);
2324
2324
  },
2325
2325
  callerOperation: operationAt,
2326
- file: (node) => node.getSourceFile().fileName
2326
+ file: () => file
2327
2327
  };
2328
2328
  }
2329
2329
  async function javaScriptHttpFacts(source, evidence) {
@@ -2334,7 +2334,7 @@ async function javaScriptHttpFacts(source, evidence) {
2334
2334
  if (request !== undefined)
2335
2335
  httpRequests.push({ operation: evidence.operationAt(call), ...request });
2336
2336
  }
2337
- const placed = await routerEndpoints(context, [source], evidence.calls);
2337
+ const placed = await routerEndpoints(context, [source.syntax], evidence.calls);
2338
2338
  return { httpEndpoints: [...withOrder(placed), ...await serveEndpoints(context, evidence.calls)], httpRequests };
2339
2339
  }
2340
2340
 
@@ -2527,14 +2527,17 @@ async function readJavaScriptOutline(repositoryRoot, references) {
2527
2527
  // plugins/scanners/javascript/src/sources.ts
2528
2528
  import { readFile as readFile2 } from "fs/promises";
2529
2529
  import path2 from "path";
2530
+ import ts6 from "./typescript.cjs";
2530
2531
  var SOURCE = /\.(?:js|mjs|cjs|jsx)$/;
2531
2532
  function javaScriptFiles(files) {
2532
2533
  return files.filter((file) => SOURCE.test(file));
2533
2534
  }
2534
2535
  async function javaScriptSources(root, files) {
2535
2536
  const sources = [];
2536
- for (const file of javaScriptFiles(files))
2537
- sources.push({ file, text: await readFile2(path2.join(root, file), "utf8") });
2537
+ for (const file of javaScriptFiles(files)) {
2538
+ const text = await readFile2(path2.join(root, file), "utf8");
2539
+ sources.push({ file, syntax: ts6.createSourceFile(file, text, ts6.ScriptTarget.Latest, true) });
2540
+ }
2538
2541
  return sources;
2539
2542
  }
2540
2543
 
@@ -2656,20 +2659,32 @@ function reachedFiles(entry, compiled, imports, packages, units) {
2656
2659
  }
2657
2660
  async function declaredEntries(root, declarations, packages, workspaces) {
2658
2661
  const entries = [];
2662
+ const diagnostics = [];
2659
2663
  for (const file of declarations) {
2660
2664
  const text = await readFile3(path4.join(root, file), "utf8");
2661
2665
  const name = path4.posix.basename(file);
2662
- if (name === "package.json") {
2663
- const manifest = JSON.parse(text);
2664
- entries.push(...declaredBins(file, manifest), ...declaredScripts(file, manifest.scripts));
2665
- } else if (name === "angular.json")
2666
- entries.push(...angularEntries(file, text));
2667
- else if (name === "project.json")
2668
- entries.push(...nxEntries(file, text, workspaces));
2669
- else if (file.endsWith(".html"))
2670
- entries.push(...browserEntries(file, text, packages));
2666
+ try {
2667
+ if (name === "package.json") {
2668
+ const manifest = JSON.parse(text);
2669
+ entries.push(...declaredBins(file, manifest), ...declaredScripts(file, manifest.scripts));
2670
+ } else if (name === "angular.json")
2671
+ entries.push(...angularEntries(file, text));
2672
+ else if (name === "project.json")
2673
+ entries.push(...nxEntries(file, text, workspaces));
2674
+ else if (file.endsWith(".html"))
2675
+ entries.push(...browserEntries(file, text, packages));
2676
+ } catch (error) {
2677
+ if (!(error instanceof SyntaxError))
2678
+ throw error;
2679
+ diagnostics.push({
2680
+ severity: "warning",
2681
+ code: "JAVASCRIPT_MANIFEST_INVALID",
2682
+ file,
2683
+ message: `The file is not valid JSON; the entry points it declares are not read. ${error.message}`
2684
+ });
2685
+ }
2671
2686
  }
2672
- return entries;
2687
+ return { entries, diagnostics };
2673
2688
  }
2674
2689
  function sourced(entry, files, outputs = []) {
2675
2690
  const file = sourceOf(entry.file, outputs, files);
@@ -2683,7 +2698,8 @@ async function withJavaScriptEntries(root, observation, inputs, files) {
2683
2698
  const packages = new Set(inventory.filter((file) => path4.posix.basename(file) === "package.json").map((file) => path4.posix.dirname(file)));
2684
2699
  const active = new Set(observation.files.map((file) => packageFor(file.file, packages)));
2685
2700
  const declarations = inventory.filter((file) => path4.posix.basename(file) !== "nx.json" && active.has(packageFor(file, packages)));
2686
- const entries = [...inputs.entries, ...await declaredEntries(root, declarations, packages, workspaces)];
2701
+ const declared = await declaredEntries(root, declarations, packages, workspaces);
2702
+ const entries = [...inputs.entries, ...declared.entries];
2687
2703
  const inventoryFiles = [...observation.files];
2688
2704
  const visible = new Set(inventoryFiles.map((file) => file.file));
2689
2705
  const readable = new Set(files);
@@ -2700,12 +2716,17 @@ async function withJavaScriptEntries(root, observation, inputs, files) {
2700
2716
  }
2701
2717
  return [{ ...entry, files: [...new Set([entry.file, ...members.map((file) => file.file)])] }];
2702
2718
  });
2703
- return createScanObservation({ ...observation, files: inventoryFiles, entryPoints });
2719
+ return createScanObservation({
2720
+ ...observation,
2721
+ files: inventoryFiles,
2722
+ entryPoints,
2723
+ diagnostics: [...observation.diagnostics, ...declared.diagnostics]
2724
+ });
2704
2725
  }
2705
2726
 
2706
2727
  // plugins/scanners/javascript/src/entries.ts
2707
2728
  import path6 from "path";
2708
- import ts6 from "./typescript.cjs";
2729
+ import ts7 from "./typescript.cjs";
2709
2730
 
2710
2731
  // plugins/scanners/entry-points/source.ts
2711
2732
  import path5 from "path";
@@ -2770,37 +2791,34 @@ async function sourceBuildEntries(root, ts, checker, sources) {
2770
2791
 
2771
2792
  // plugins/scanners/javascript/src/entries.ts
2772
2793
  async function javaScriptEntries(root, sources) {
2773
- const files = new Set(sources.map((source) => source.fileName));
2774
- const imports = new Map(sources.map((source) => {
2775
- const targets = ts6.preProcessFile(source.text, true, true).importedFiles.flatMap((imported) => {
2794
+ const files = new Set(sources.map((source) => source.file));
2795
+ const imports = new Map(sources.map(({ file, syntax }) => {
2796
+ const targets = ts7.preProcessFile(syntax.text, true, true).importedFiles.flatMap((imported) => {
2776
2797
  if (!imported.fileName.startsWith("."))
2777
2798
  return [];
2778
- const base = path6.posix.normalize(path6.posix.join(path6.posix.dirname(source.fileName), imported.fileName));
2799
+ const base = path6.posix.normalize(path6.posix.join(path6.posix.dirname(file), imported.fileName));
2779
2800
  const candidates = [base, ...[".js", ".mjs", ".cjs", ".jsx"].flatMap((extension) => [base + extension, `${base}/index${extension}`])];
2780
- return candidates.find((file) => files.has(file)) ?? [];
2801
+ return candidates.find((candidate) => files.has(candidate)) ?? [];
2781
2802
  });
2782
- return [source.fileName, targets];
2803
+ return [file, targets];
2783
2804
  }));
2784
- const entries = (await Promise.all(sources.map((source) => sourceBuildEntries(root, ts6, classicChecker(ts6, fileChecker(source)), [source])))).flat();
2805
+ const entries = (await Promise.all(sources.map(async ({ file, syntax }) => (await sourceBuildEntries(root, ts7, classicChecker(ts7, fileChecker(syntax)), [syntax])).map((entry) => ({ ...entry, declaration: file }))))).flat();
2785
2806
  return { imports, entries };
2786
2807
  }
2787
2808
 
2788
2809
  // plugins/scanners/javascript/src/index.ts
2789
- function parse(source) {
2790
- return ts7.createSourceFile(source.file, source.text, ts7.ScriptTarget.Latest, true);
2791
- }
2792
2810
  var SLOPPY_MODE_ONLY = new Set([1121, 1487, 1488, 1489]);
2793
- function parseWarning(source) {
2794
- const { parseDiagnostics } = source;
2811
+ function parseWarning({ file, syntax }) {
2812
+ const { parseDiagnostics } = syntax;
2795
2813
  const first = parseDiagnostics.find((diagnostic) => !SLOPPY_MODE_ONLY.has(diagnostic.code));
2796
2814
  if (first === undefined)
2797
2815
  return;
2798
2816
  return {
2799
2817
  severity: "warning",
2800
2818
  code: "JAVASCRIPT_SOURCE_INVALID",
2801
- file: source.fileName,
2802
- line: source.getLineAndCharacterOfPosition(first.start).line + 1,
2803
- message: `The file does not parse, so it contributes no evidence: ${ts7.flattenDiagnosticMessageText(first.messageText, " ")}`
2819
+ file,
2820
+ line: syntax.getLineAndCharacterOfPosition(first.start).line + 1,
2821
+ message: `The file does not parse, so it contributes no evidence: ${ts8.flattenDiagnosticMessageText(first.messageText, " ")}`
2804
2822
  };
2805
2823
  }
2806
2824
  var src_default = {
@@ -2815,18 +2833,17 @@ var src_default = {
2815
2833
  const sources = await javaScriptSources(root, files);
2816
2834
  if (!sources.length)
2817
2835
  return;
2818
- const parsed = sources.map(parse);
2819
- const warnings = parsed.map(parseWarning);
2820
- const readable = parsed.filter((_, index) => warnings[index] === undefined);
2836
+ const warnings = sources.map(parseWarning);
2837
+ const readable = sources.filter((_, index) => warnings[index] === undefined);
2821
2838
  const scanned = await Promise.all(readable.map(async (source) => {
2822
- const evidence = javaScriptEvidence(source.fileName, source);
2823
- return { file: source.fileName, evidence, facts: await javaScriptHttpFacts(source, evidence) };
2839
+ const evidence = javaScriptEvidence(source.file, source.syntax);
2840
+ return { file: source.file, evidence, facts: await javaScriptHttpFacts(source, evidence) };
2824
2841
  }));
2825
2842
  const symbols = new Map(scanned.map(({ file, evidence }) => [file, evidence.symbols]));
2826
2843
  return withJavaScriptEntries(root, createScanObservation({
2827
- scanner: { id: "javascript", technology: "javascript", engine: "typescript-sdk", engineVersion: ts7.version },
2844
+ scanner: { id: "javascript", technology: "javascript", engine: "typescript-sdk", engineVersion: ts8.version },
2828
2845
  roots: [{ id: "javascript-source", kind: "source-group", name: path7.basename(root) }],
2829
- files: parsed.map(({ fileName: file }) => ({ file, symbols: symbols.get(file) ?? [], roots: ["javascript-source"] })),
2846
+ files: sources.map(({ file }) => ({ file, symbols: symbols.get(file) ?? [], roots: ["javascript-source"] })),
2830
2847
  operations: scanned.flatMap(({ evidence }) => evidence.operations),
2831
2848
  invocations: scanned.flatMap(({ evidence }) => evidence.invocations),
2832
2849
  httpEndpoints: scanned.flatMap(({ facts }) => facts.httpEndpoints),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groma/scanner-javascript",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "JavaScript source declarations, outlines and operation bodies without project dependencies.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "repository": {
57
57
  "type": "git",
58
- "url": "https://github.com/MrLesk/Groma.md.git"
58
+ "url": "https://github.com/MrLesk/groma.md.git"
59
59
  },
60
60
  "publishConfig": {
61
61
  "access": "public"