@groma/scanner-typescript 0.1.0 → 0.1.1

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 (2) hide show
  1. package/package.json +4 -7
  2. package/src/index.js +149 -67
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@groma/scanner-typescript",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "TypeScript source and call evidence using the native TypeScript SDK.",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "license": "MIT",
8
8
  "os": [
9
- "win32",
10
9
  "linux",
11
- "darwin"
10
+ "darwin",
11
+ "win32"
12
12
  ],
13
13
  "cpu": [
14
14
  "arm64",
@@ -43,10 +43,7 @@
43
43
  }
44
44
  ],
45
45
  "compatibility": {
46
- "groma": "^0.3.0",
47
- "technologyVersions": {
48
- "typescript": "5.9.2 || 5.9.3 || 7.0.2"
49
- }
46
+ "groma": "^0.3.0"
50
47
  }
51
48
  }
52
49
  }
package/src/index.js CHANGED
@@ -3803,9 +3803,9 @@ async function listTypeScriptFiles(repositoryRoot, config = defaultTypeScriptSca
3803
3803
  }
3804
3804
 
3805
3805
  // plugins/scanners/typescript/src/scan.ts
3806
- import { existsSync as existsSync2 } from "fs";
3806
+ import { existsSync as existsSync3 } from "fs";
3807
3807
  import { readFile as readFile2 } from "fs/promises";
3808
- import path6 from "path";
3808
+ import path9 from "path";
3809
3809
 
3810
3810
  // node_modules/typescript/lib/version.cjs
3811
3811
  var { version } = require_package();
@@ -3975,7 +3975,7 @@ function sourcePosition(position) {
3975
3975
 
3976
3976
  // plugins/scanners/typescript/src/graph.ts
3977
3977
  import { readFile } from "fs/promises";
3978
- import path5 from "path";
3978
+ import path8 from "path";
3979
3979
 
3980
3980
  // plugins/scanners/typescript/src/naming.ts
3981
3981
  function kebabCase(name) {
@@ -3990,7 +3990,7 @@ function displayName(kebab) {
3990
3990
  }
3991
3991
 
3992
3992
  // plugins/scanners/typescript/src/source-analysis.ts
3993
- import path4 from "path";
3993
+ import path7 from "path";
3994
3994
 
3995
3995
  // node_modules/typescript/dist/enums/checkFlags.js
3996
3996
  var CheckFlags;
@@ -17378,8 +17378,6 @@ function importBindings(source, dependencies) {
17378
17378
  if (!isImportDeclaration(statement) || !isStringLiteral(statement.moduleSpecifier))
17379
17379
  continue;
17380
17380
  const specifier = statement.moduleSpecifier.text;
17381
- if (!specifier.startsWith("."))
17382
- continue;
17383
17381
  const clause = statement.importClause;
17384
17382
  const names = clause ? bindingNames(clause) : [];
17385
17383
  if (names.length === 0 && clause?.phaseModifier !== SyntaxKind.TypeKeyword)
@@ -17433,8 +17431,94 @@ async function usedImportSpecifiers(source, checker) {
17433
17431
  return [...dependencies];
17434
17432
  }
17435
17433
 
17436
- // plugins/scanners/typescript/src/source-operations.ts
17434
+ // plugins/scanners/typescript/src/projects.ts
17435
+ import path4 from "path";
17436
+ import { stat } from "fs/promises";
17437
+
17438
+ // plugins/scanners/projects.ts
17439
+ import { execFile } from "child_process";
17440
+ import { existsSync as existsSync2 } from "fs";
17437
17441
  import path3 from "path";
17442
+ import { promisify } from "util";
17443
+ var execute = promisify(execFile);
17444
+ var excluded = new Set([
17445
+ ".git",
17446
+ "node_modules",
17447
+ "vendor",
17448
+ "target",
17449
+ "dist",
17450
+ "build",
17451
+ "bin",
17452
+ "obj",
17453
+ ".gradle",
17454
+ ".angular",
17455
+ "coverage",
17456
+ "generated",
17457
+ "groma",
17458
+ ".groma"
17459
+ ]);
17460
+ async function projectFiles(root, matches) {
17461
+ const { stdout } = await execute("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { maxBuffer: 64 * 1024 * 1024 });
17462
+ return [...new Set(stdout.split("\x00").filter((file) => file && matches(file) && !file.split("/").some((part) => excluded.has(part)) && existsSync2(path3.join(root, file))))].sort();
17463
+ }
17464
+
17465
+ // plugins/scanners/typescript/src/projects.ts
17466
+ async function typescriptProjects(api, root, files) {
17467
+ const configurations = new Map;
17468
+ async function read(file) {
17469
+ if (configurations.has(file))
17470
+ return;
17471
+ const config = await api.parseConfigFile(file);
17472
+ if (config.errors.length)
17473
+ throw new Error(`${file}: ${config.errors.map((error) => error.text).join(`
17474
+ `)}`);
17475
+ configurations.set(file, config);
17476
+ for (const reference of config.projectReferences ?? []) {
17477
+ const target = (await stat(reference.path)).isDirectory() ? path4.join(reference.path, "tsconfig.json") : reference.path;
17478
+ await read(target);
17479
+ }
17480
+ }
17481
+ for (const file of await projectFiles(root, (file) => path4.posix.basename(file) === "tsconfig.json"))
17482
+ await read(path4.join(root, file));
17483
+ const selected = new Set(files.map((file) => path4.resolve(root, file)));
17484
+ const projects = [...configurations].map(([key, config]) => ({
17485
+ key,
17486
+ config,
17487
+ files: config.fileNames.filter((file) => selected.has(path4.resolve(file)))
17488
+ }));
17489
+ const owners = new Map;
17490
+ for (const project of projects) {
17491
+ for (const file of project.files) {
17492
+ const previous = owners.get(path4.resolve(file));
17493
+ if (!previous || path4.dirname(project.key).split(path4.sep).length >= path4.dirname(previous.key).split(path4.sep).length) {
17494
+ owners.set(path4.resolve(file), project);
17495
+ }
17496
+ }
17497
+ }
17498
+ for (const project of projects)
17499
+ project.files = project.files.filter((file) => owners.get(path4.resolve(file)) === project);
17500
+ const loose = [...selected].filter((file) => !owners.has(file));
17501
+ return [...projects.filter((project) => project.files.length), ...loose.length ? [{ key: "", files: loose }] : []];
17502
+ }
17503
+
17504
+ // plugins/scanners/typescript/src/source-imports.ts
17505
+ import path5 from "path";
17506
+ async function resolvedImports(root, source, specifiers, checker) {
17507
+ const used = new Set(specifiers);
17508
+ const nodes = [];
17509
+ function visit(node) {
17510
+ if (isStringLiteral(node) && used.has(node.text))
17511
+ nodes.push(node);
17512
+ node.forEachChild(visit);
17513
+ }
17514
+ visit(source);
17515
+ const symbols = await checker.getSymbolAtLocation(nodes);
17516
+ const declarations = await Promise.all(symbols.flatMap((symbol) => symbol?.declarations ?? []).map((handle) => handle.resolve()));
17517
+ return [...new Set(declarations.flatMap((declaration) => declaration ? [path5.relative(root, declaration.getSourceFile().fileName).split(path5.sep).join("/")] : []))].sort();
17518
+ }
17519
+
17520
+ // plugins/scanners/typescript/src/source-operations.ts
17521
+ import path6 from "path";
17438
17522
 
17439
17523
  // plugins/scanners/typescript/src/source-tokens.ts
17440
17524
  function lookup(scope, name) {
@@ -17656,7 +17740,7 @@ function executable(node) {
17656
17740
  function location(root, node) {
17657
17741
  const source = node.getSourceFile();
17658
17742
  return {
17659
- file: path3.relative(root, source.fileName).split(path3.sep).join("/"),
17743
+ file: path6.relative(root, source.fileName).split(path6.sep).join("/"),
17660
17744
  line: source.getLineAndCharacterOfPosition(node.getStart()).line + 1,
17661
17745
  position: node.getStart()
17662
17746
  };
@@ -17941,85 +18025,83 @@ async function analyzeSourceFiles(repositoryRoot, paths) {
17941
18025
  return { files: [], operations: [], invocations: [] };
17942
18026
  const api = new API({ cwd: repositoryRoot });
17943
18027
  try {
17944
- const program = await api.createProgram(paths.map((file) => path4.join(repositoryRoot, file)), {
17945
- compilerOptions: {
17946
- noLib: true,
17947
- noResolve: true,
17948
- types: [],
17949
- allowJs: true,
17950
- moduleResolution: ModuleResolutionKind.Bundler,
17951
- module: ModuleKind.Preserve
18028
+ const result = { files: [], operations: [], invocations: [] };
18029
+ for (const project of await typescriptProjects(api, repositoryRoot, paths)) {
18030
+ if (!project.config) {
18031
+ const analyzed = new Set(result.files.map((file) => path7.resolve(repositoryRoot, file.file)));
18032
+ project.files = project.files.filter((file) => !analyzed.has(file));
18033
+ if (!project.files.length)
18034
+ continue;
17952
18035
  }
17953
- });
17954
- const sources = await Promise.all(paths.map(async (file) => {
17955
- const source = await program.getSourceFile(path4.join(repositoryRoot, file));
17956
- if (!source)
17957
- throw new Error(`TypeScript could not parse ${file}`);
17958
- return source;
17959
- }));
17960
- const evidence = await sourceOperations(repositoryRoot, sources, program.getProject().checker);
17961
- const files = await Promise.all(sources.map(async (source, index) => ({
17962
- file: paths[index],
17963
- specifiers: await usedImportSpecifiers(source, program.getProject().checker),
17964
- symbols: exportSymbols(paths[index], source)
17965
- })));
17966
- return { files, ...evidence };
18036
+ const evidence = await analyzeProject(api, repositoryRoot, paths, project);
18037
+ result.files.push(...evidence.files);
18038
+ result.operations.push(...evidence.operations);
18039
+ result.invocations.push(...evidence.invocations);
18040
+ }
18041
+ result.operations = [...new Map(result.operations.map((operation) => [operation.id, operation])).values()];
18042
+ return result;
17967
18043
  } finally {
17968
18044
  await api.close();
17969
18045
  }
17970
18046
  }
18047
+ async function analyzeProject(api, repositoryRoot, paths, project) {
18048
+ const program = await api.createProgram(project.files, {
18049
+ compilerOptions: { ...project.config?.options ?? {
18050
+ noLib: true,
18051
+ types: [],
18052
+ allowJs: true,
18053
+ moduleResolution: ModuleResolutionKind.Bundler,
18054
+ module: ModuleKind.Preserve
18055
+ }, noEmit: true }
18056
+ });
18057
+ const sources = await Promise.all(paths.map((file) => program.getSourceFile(path7.join(repositoryRoot, file))));
18058
+ const selectedProgramSources = sources.filter((source) => source !== undefined);
18059
+ const checker = program.getProject().checker;
18060
+ const evidence = await sourceOperations(repositoryRoot, selectedProgramSources, checker);
18061
+ const files = await Promise.all(selectedProgramSources.map(async (source) => {
18062
+ const file = path7.relative(repositoryRoot, source.fileName).split(path7.sep).join("/");
18063
+ return {
18064
+ file,
18065
+ imports: await resolvedImports(repositoryRoot, source, await usedImportSpecifiers(source, checker), checker),
18066
+ symbols: exportSymbols(file, source)
18067
+ };
18068
+ }));
18069
+ return { files, ...evidence };
18070
+ }
17971
18071
 
17972
18072
  // plugins/scanners/typescript/src/graph.ts
17973
- function resolveSpecifier(fromFile, specifier, files) {
17974
- if (!specifier.startsWith("."))
17975
- return;
17976
- const joined = path5.posix.normalize(path5.posix.join(path5.posix.dirname(fromFile), specifier));
17977
- const candidates = specifier.endsWith(".ts") || specifier.endsWith(".tsx") ? [joined] : [
17978
- joined,
17979
- joined.replace(/\.jsx?$/, ".ts"),
17980
- joined.replace(/\.jsx?$/, ".tsx"),
17981
- `${joined}.ts`,
17982
- `${joined}.tsx`,
17983
- `${joined}.js`,
17984
- `${joined}/index.ts`,
17985
- `${joined}/index.tsx`
17986
- ];
17987
- return candidates.find((candidate) => files.has(candidate));
17988
- }
17989
18073
  function fileStem(file) {
17990
- return path5.posix.basename(file).replace(/\.tsx?$/, "");
18074
+ return path8.posix.basename(file).replace(/\.tsx?$/, "");
17991
18075
  }
17992
18076
  function fileLabel(file) {
17993
18077
  const stem = fileStem(file);
17994
18078
  if (stem === "index") {
17995
- const parent = path5.posix.basename(path5.posix.dirname(file));
18079
+ const parent = path8.posix.basename(path8.posix.dirname(file));
17996
18080
  return displayName(kebabCase(parent === "." || parent === "" ? stem : parent));
17997
18081
  }
17998
18082
  return displayName(kebabCase(stem));
17999
18083
  }
18000
18084
  async function packageName(repositoryRoot) {
18001
18085
  try {
18002
- const source = await readFile(path5.join(repositoryRoot, "package.json"), "utf8");
18086
+ const source = await readFile(path8.join(repositoryRoot, "package.json"), "utf8");
18003
18087
  const name = JSON.parse(source).name;
18004
18088
  if (typeof name === "string" && kebabCase(name) !== "") {
18005
18089
  return displayName(kebabCase(name));
18006
18090
  }
18007
18091
  } catch {}
18008
- return displayName(kebabCase(path5.basename(repositoryRoot)));
18092
+ return displayName(kebabCase(path8.basename(repositoryRoot)));
18009
18093
  }
18010
18094
  async function buildImportGraph(repositoryRoot, config = defaultTypeScriptScannerConfig) {
18011
18095
  const paths = await listTypeScriptFiles(repositoryRoot, config);
18012
18096
  const files = new Set(paths);
18013
18097
  const { files: analyses, operations, invocations } = await analyzeSourceFiles(repositoryRoot, paths);
18014
- const nodes = new Map(analyses.map(({ file, specifiers, symbols }) => [file, {
18015
- file,
18016
- imports: [...new Set(specifiers.flatMap((specifier) => {
18017
- const resolved = resolveSpecifier(file, specifier, files);
18018
- return resolved === undefined ? [] : [resolved];
18019
- }))].sort(),
18020
- importedBy: [],
18021
- symbols
18022
- }]));
18098
+ const nodes = new Map;
18099
+ for (const analysis of analyses) {
18100
+ const node = nodes.get(analysis.file) ?? { file: analysis.file, imports: [], importedBy: [], symbols: [] };
18101
+ node.imports = [...new Set([...node.imports, ...analysis.imports.filter((file) => files.has(file))])].sort();
18102
+ node.symbols = [...new Map([...node.symbols, ...analysis.symbols].map((symbol) => [symbol.id, symbol])).values()];
18103
+ nodes.set(node.file, node);
18104
+ }
18023
18105
  for (const node of nodes.values()) {
18024
18106
  for (const imported of node.imports)
18025
18107
  nodes.get(imported)?.importedBy.push(node.file);
@@ -18036,7 +18118,7 @@ function isPaintFile(file, node) {
18036
18118
  }
18037
18119
  async function packageBins(repositoryRoot) {
18038
18120
  try {
18039
- const source = await readFile2(path6.join(repositoryRoot, "package.json"), "utf8");
18121
+ const source = await readFile2(path9.join(repositoryRoot, "package.json"), "utf8");
18040
18122
  const bin = JSON.parse(source).bin;
18041
18123
  if (typeof bin === "string")
18042
18124
  return [bin.replace(/^\.\//, "")];
@@ -18098,8 +18180,8 @@ function inferScopeFiles(graph, bins) {
18098
18180
  return [...scopes].sort();
18099
18181
  }
18100
18182
  function commonDirectorySegments(left, right) {
18101
- const leftParts = path6.posix.dirname(left).split("/");
18102
- const rightParts = path6.posix.dirname(right).split("/");
18183
+ const leftParts = path9.posix.dirname(left).split("/");
18184
+ const rightParts = path9.posix.dirname(right).split("/");
18103
18185
  let count = 0;
18104
18186
  while (leftParts[count] !== undefined && leftParts[count] === rightParts[count])
18105
18187
  count += 1;
@@ -18149,7 +18231,7 @@ function scopeNames(scopeFiles) {
18149
18231
  for (const file of scopeFiles) {
18150
18232
  let name = fileStem(file) === "cli" ? "Cli" : fileLabel(file);
18151
18233
  if (used.has(name)) {
18152
- name = displayName(kebabCase(`${path6.posix.basename(path6.posix.dirname(file))}-${fileStem(file)}`));
18234
+ name = displayName(kebabCase(`${path9.posix.basename(path9.posix.dirname(file))}-${fileStem(file)}`));
18153
18235
  }
18154
18236
  if (used.has(name))
18155
18237
  name = `${name} ${file}`;
@@ -18179,7 +18261,7 @@ async function scanTypeScriptSource(repositoryRoot, config = defaultTypeScriptSc
18179
18261
  id: "package",
18180
18262
  kind: "package",
18181
18263
  name,
18182
- ...existsSync2(path6.join(repositoryRoot, "package.json")) ? { file: "package.json" } : {}
18264
+ ...existsSync3(path9.join(repositoryRoot, "package.json")) ? { file: "package.json" } : {}
18183
18265
  },
18184
18266
  ...scopeFiles.map((file) => ({
18185
18267
  id: scopeId(file),
@@ -18197,7 +18279,7 @@ async function scanTypeScriptSource(repositoryRoot, config = defaultTypeScriptSc
18197
18279
  }
18198
18280
 
18199
18281
  // plugins/scanners/typescript/src/structure.ts
18200
- import path7 from "path";
18282
+ import path10 from "path";
18201
18283
  function isExported(modifierFlags) {
18202
18284
  return (modifierFlags & ModifierFlags.Export) !== 0;
18203
18285
  }
@@ -18278,7 +18360,7 @@ function declarationsIn(source, symbols) {
18278
18360
  async function readCodeStructure(repositoryRoot, references) {
18279
18361
  if (references.length === 0)
18280
18362
  return [];
18281
- const filenames = references.map((reference) => path7.join(repositoryRoot, reference.file));
18363
+ const filenames = references.map((reference) => path10.join(repositoryRoot, reference.file));
18282
18364
  const api = new API({ cwd: repositoryRoot });
18283
18365
  try {
18284
18366
  const snapshot = await api.updateSnapshot({ openFiles: filenames });
@@ -18303,7 +18385,7 @@ async function readCodeStructure(repositoryRoot, references) {
18303
18385
  var scanner2 = {
18304
18386
  id: "typescript",
18305
18387
  readCodeStructure,
18306
- watch: { include: defaultTypeScriptScannerConfig.globs, exclude: defaultTypeScriptScannerConfig.ignore },
18388
+ watch: { include: [...defaultTypeScriptScannerConfig.globs, "**/tsconfig*.json", "**/package.json"], exclude: defaultTypeScriptScannerConfig.ignore },
18307
18389
  scan: (root) => scanTypeScriptSource(root)
18308
18390
  };
18309
18391
  var src_default = scanner2;