impacted 0.0.5 → 0.0.8

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/README.md CHANGED
@@ -9,18 +9,21 @@ A userland implementation of [predictive test selection for Node.js test runner]
9
9
  ## Usage
10
10
 
11
11
  ```bash
12
- # Run only impacted tests
12
+ # Run only impacted tests (--since gets changed files from git diff)
13
+ node --test $(npx impacted --since main)
14
+
15
+ # Pipe changed files from stdin
13
16
  node --test $(git diff --name-only main | npx impacted)
14
17
 
15
18
  # Works with any test runner
16
- vitest $(git diff --name-only main | npx impacted)
17
- jest $(git diff --name-only main | npx impacted)
19
+ vitest $(npx impacted --since main)
20
+ jest $(npx impacted --since main)
18
21
 
19
22
  # Custom test pattern
20
- git diff --name-only main | npx impacted -p "src/**/*.spec.js"
23
+ npx impacted --since main -p "src/**/*.spec.js"
21
24
 
22
25
  # Multiple patterns
23
- git diff --name-only main | npx impacted -p "test/**/*.test.js" -p "test/**/*.spec.js"
26
+ npx impacted --since main -p "test/**/*.test.js" -p "test/**/*.spec.js"
24
27
  ```
25
28
 
26
29
  ## GitHub Action
@@ -33,7 +36,7 @@ git diff --name-only main | npx impacted -p "test/**/*.test.js" -p "test/**/*.sp
33
36
  - uses: sozua/impacted@v1
34
37
  id: impacted
35
38
  with:
36
- pattern: '**/*.{test,spec}.{js,mjs,cjs,jsx}' # default
39
+ pattern: '**/*.{test,spec}.{js,mjs,cjs,jsx,ts,mts,cts,tsx}' # default
37
40
 
38
41
  - name: Run impacted tests
39
42
  if: steps.impacted.outputs.has-impacted == 'true'
@@ -54,15 +57,37 @@ const tests = await findImpacted({
54
57
  });
55
58
  ```
56
59
 
60
+ ### `node:test` `run()` integration
61
+
62
+ ```javascript
63
+ import { run } from 'node:test';
64
+ import { findImpacted } from 'impacted';
65
+
66
+ const files = await findImpacted({
67
+ changedFiles: ['src/utils.js'],
68
+ testFiles: 'test/**/*.test.js',
69
+ });
70
+
71
+ run({ files });
72
+ ```
73
+
74
+ See [examples/05-node-test-run](./examples/05-node-test-run) for a full working example.
75
+
76
+ ## TypeScript
77
+
78
+ TypeScript files (`.ts`, `.mts`, `.cts`, `.tsx`) are supported out of the box on Node.js >= 22.7. Type stripping is handled via `node:module.stripTypeScriptTypes()` — no additional dependencies required.
79
+
80
+ Follows Node.js core's TypeScript philosophy: explicit extensions, no `tsconfig.json`, no path aliases.
81
+
57
82
  ## Limitations
58
83
 
59
- - JavaScript only (`.js`, `.mjs`, `.cjs`, `.jsx`) — no TypeScript yet
60
84
  - Static analysis only — dynamic `require(variable)` not supported
61
85
  - Local files only — `node_modules` changes won't trigger tests
86
+ - TypeScript support requires Node.js >= 22.7 (JS analysis works on Node.js >= 18)
62
87
 
63
88
  ## Requirements
64
89
 
65
- - Node.js >= 18
90
+ - Node.js >= 18 (TypeScript support requires >= 22.7)
66
91
 
67
92
  ## License
68
93
 
package/bin/impacted.js CHANGED
@@ -1,35 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { execSync } from 'node:child_process';
3
4
  import { findImpacted } from '../src/index.js';
4
5
 
5
6
  const args = process.argv.slice(2);
6
7
 
7
8
  // Parse -p/--pattern flags (supports multiple: -p "*.test.js" -p "*.spec.js")
8
9
  const patterns = [];
10
+ let since = null;
9
11
  for (let i = 0; i < args.length; i++) {
10
12
  if ((args[i] === '-p' || args[i] === '--pattern') && args[i + 1]) {
11
13
  patterns.push(args[++i]);
14
+ } else if (args[i] === '--since' && args[i + 1]) {
15
+ since = args[++i];
12
16
  }
13
17
  }
14
- const pattern = patterns.length > 0 ? patterns : '**/*.{test,spec}.{js,mjs,cjs,jsx}';
15
-
16
- // Read changed files from stdin
17
- let input = '';
18
- process.stdin.setEncoding('utf8');
19
-
20
- process.stdin.on('readable', () => {
21
- let chunk;
22
- while ((chunk = process.stdin.read()) !== null) {
23
- input += chunk;
24
- }
25
- });
26
-
27
- process.stdin.on('end', async () => {
28
- const changedFiles = input
29
- .split('\n')
30
- .map((line) => line.trim())
31
- .filter((line) => line.length > 0);
18
+ const pattern = patterns.length > 0 ? patterns : '**/*.{test,spec}.{js,mjs,cjs,jsx,ts,mts,cts,tsx}';
32
19
 
20
+ async function run(changedFiles) {
33
21
  if (changedFiles.length === 0) {
34
22
  process.exit(0);
35
23
  }
@@ -42,4 +30,41 @@ process.stdin.on('end', async () => {
42
30
  for (const file of impacted) {
43
31
  console.log(file);
44
32
  }
45
- });
33
+ }
34
+
35
+ // --since <ref>: get changed files from git diff
36
+ if (since) {
37
+ let output;
38
+ try {
39
+ output = execSync(`git diff --name-only ${since}`, { encoding: 'utf8' });
40
+ } catch {
41
+ process.exit(1);
42
+ }
43
+
44
+ const changedFiles = output
45
+ .split('\n')
46
+ .map((line) => line.trim())
47
+ .filter((line) => line.length > 0);
48
+
49
+ await run(changedFiles);
50
+ } else {
51
+ // Read changed files from stdin
52
+ let input = '';
53
+ process.stdin.setEncoding('utf8');
54
+
55
+ process.stdin.on('readable', () => {
56
+ let chunk;
57
+ while ((chunk = process.stdin.read()) !== null) {
58
+ input += chunk;
59
+ }
60
+ });
61
+
62
+ process.stdin.on('end', async () => {
63
+ const changedFiles = input
64
+ .split('\n')
65
+ .map((line) => line.trim())
66
+ .filter((line) => line.length > 0);
67
+
68
+ await run(changedFiles);
69
+ });
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impacted",
3
- "version": "0.0.5",
3
+ "version": "0.0.8",
4
4
  "description": "Find test files impacted by code changes using static dependency analysis",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -0,0 +1,3 @@
1
+ export const JS_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.jsx']);
2
+ export const TS_EXTENSIONS = new Set(['.ts', '.mts', '.cts', '.tsx']);
3
+ export const SUPPORTED_EXTENSIONS = new Set([...JS_EXTENSIONS, ...TS_EXTENSIONS]);
package/src/graph.js CHANGED
@@ -2,8 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { extname } from 'node:path';
3
3
  import { parseImports } from './parser.js';
4
4
  import { resolveSpecifier } from './resolver.js';
5
-
6
- const SUPPORTED_EXTENSIONS = ['.js', '.mjs', '.cjs', '.jsx'];
5
+ import { SUPPORTED_EXTENSIONS, TS_EXTENSIONS } from './constants.js';
7
6
 
8
7
  /** @typedef {import('./cache.js').ImportCache} ImportCache */
9
8
 
@@ -13,7 +12,7 @@ const SUPPORTED_EXTENSIONS = ['.js', '.mjs', '.cjs', '.jsx'];
13
12
  * @returns {boolean}
14
13
  */
15
14
  function isSupportedFile(filePath) {
16
- return SUPPORTED_EXTENSIONS.includes(extname(filePath));
15
+ return SUPPORTED_EXTENSIONS.has(extname(filePath));
17
16
  }
18
17
 
19
18
  /**
@@ -28,7 +27,8 @@ export function extractImports(filePath) {
28
27
  } catch {
29
28
  return [];
30
29
  }
31
- return parseImports(source);
30
+ const typescript = TS_EXTENSIONS.has(extname(filePath));
31
+ return parseImports(source, { typescript });
32
32
  }
33
33
 
34
34
  const DEFAULT_EXCLUDE_PATHS = ['node_modules', 'dist'];
package/src/index.d.ts CHANGED
@@ -52,8 +52,13 @@ export function invertGraph(
52
52
  graph: Map<string, Set<string>>,
53
53
  ): Map<string, Set<string>>;
54
54
 
55
+ export interface ParseImportsOptions {
56
+ /** Strip TypeScript syntax before parsing (requires Node >= 22.7) */
57
+ typescript?: boolean;
58
+ }
59
+
55
60
  /** Extract import/require specifiers from source code */
56
- export function parseImports(source: string): string[];
61
+ export function parseImports(source: string, options?: ParseImportsOptions): string[];
57
62
 
58
63
  /** Resolve an import specifier to an absolute file path */
59
64
  export function resolveSpecifier(
package/src/parser.js CHANGED
@@ -1,13 +1,34 @@
1
1
  import * as acorn from 'acorn';
2
2
  import * as walk from 'acorn-walk';
3
+ import * as nodeModule from 'node:module';
3
4
 
4
5
  /**
5
6
  * Extract all import/require specifiers from source code
6
7
  * Handles ESM (import/export) and CJS (require) patterns
7
8
  * @param {string} source - The source code
9
+ * @param {Object} [options]
10
+ * @param {boolean} [options.typescript=false] - Strip TypeScript syntax before parsing (requires Node >= 22.7)
8
11
  * @returns {string[]} Array of import specifiers
9
12
  */
10
- export function parseImports(source) {
13
+ export function parseImports(source, { typescript = false } = {}) {
14
+ if (typescript) {
15
+ if (!nodeModule.stripTypeScriptTypes) {
16
+ if (!parseImports._tsWarned) {
17
+ parseImports._tsWarned = true;
18
+ process.emitWarning(
19
+ 'TypeScript support requires Node.js >= 22.7',
20
+ 'UnsupportedWarning',
21
+ );
22
+ }
23
+ return [];
24
+ }
25
+ try {
26
+ source = nodeModule.stripTypeScriptTypes(source);
27
+ } catch {
28
+ return [];
29
+ }
30
+ }
31
+
11
32
  const imports = [];
12
33
 
13
34
  let ast;
package/src/resolver.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { builtinModules } from 'node:module';
3
- import { readFileSync } from 'node:fs';
4
- import { dirname, join, resolve } from 'node:path';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { dirname, extname, join, resolve } from 'node:path';
5
+ import { TS_EXTENSIONS } from './constants.js';
5
6
 
6
7
  /**
7
8
  * Find the nearest package.json from a given path
@@ -111,6 +112,14 @@ export function resolveSpecifier(specifier, parentPath) {
111
112
  const require = createRequire(parentPath);
112
113
  return require.resolve(specifier);
113
114
  } catch {
115
+ // require.resolve doesn't handle TS extensions
116
+ // For relative specifiers with explicit TS extensions, try manual resolution
117
+ if (specifier.startsWith('.') && TS_EXTENSIONS.has(extname(specifier))) {
118
+ const resolved = resolve(dirname(parentPath), specifier);
119
+ if (existsSync(resolved)) {
120
+ return resolved;
121
+ }
122
+ }
114
123
  return null;
115
124
  }
116
125
  }