impacted 0.0.3 → 0.0.5

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
@@ -1,145 +1,64 @@
1
1
  # impacted
2
2
 
3
- Find test files impacted by code changes using static dependency analysis.
3
+ It takes a list of changed files, builds a dependency graph from your test files by statically analyzing their imports, and returns only the test files that depend on the changed files.
4
4
 
5
- [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
- [![npm version](https://img.shields.io/npm/v/impacted)](https://www.npmjs.com/package/impacted)
7
-
8
- ## About
9
-
10
- `impacted` analyzes your codebase's import graph to determine which test files are affected by a set of changed files. Run only the tests that matter instead of your entire test suite.
11
-
12
- This project is a userland implementation of [predictive test selection for Node.js test runner](https://github.com/nodejs/node/issues/54173).
13
-
14
- ## Features
15
-
16
- - Static analysis of ESM and CommonJS (import, require, dynamic import, re-exports)
17
- - Supports Node.js subpath imports (`#imports`)
18
- - Transitive dependency tracking
19
- - Optional caching for faster repeated runs
20
- - CLI and programmatic API
21
- - GitHub Action for CI integration
5
+ A userland implementation of [predictive test selection for Node.js test runner](https://github.com/nodejs/node/issues/54173).
22
6
 
23
- ## Installation
24
-
25
- ```bash
26
- npm install impacted
27
- ```
7
+ [![npm version](https://img.shields.io/npm/v/impacted)](https://www.npmjs.com/package/impacted)
28
8
 
29
9
  ## Usage
30
10
 
31
- ### CLI
32
-
33
- Pipe changed files to `impacted`:
34
-
35
11
  ```bash
36
- git diff --name-only HEAD~1 | npx impacted
37
- ```
38
-
39
- With a custom test pattern:
40
-
41
- ```bash
42
- git diff --name-only main | npx impacted -p "src/**/*.spec.js"
43
- ```
44
-
45
- Use with any test runner:
46
-
47
- ```bash
48
- # Node.js test runner
12
+ # Run only impacted tests
49
13
  node --test $(git diff --name-only main | npx impacted)
50
14
 
51
- # Vitest
15
+ # Works with any test runner
52
16
  vitest $(git diff --name-only main | npx impacted)
53
-
54
- # Jest
55
17
  jest $(git diff --name-only main | npx impacted)
18
+
19
+ # Custom test pattern
20
+ git diff --name-only main | npx impacted -p "src/**/*.spec.js"
21
+
22
+ # Multiple patterns
23
+ git diff --name-only main | npx impacted -p "test/**/*.test.js" -p "test/**/*.spec.js"
56
24
  ```
57
25
 
58
- ### GitHub Action
26
+ ## GitHub Action
59
27
 
60
28
  ```yaml
29
+ - uses: actions/checkout@v4
30
+ with:
31
+ fetch-depth: 0
32
+
61
33
  - uses: sozua/impacted@v1
62
34
  id: impacted
63
35
  with:
64
- pattern: '**/*.test.js'
36
+ pattern: '**/*.{test,spec}.{js,mjs,cjs,jsx}' # default
65
37
 
66
38
  - name: Run impacted tests
67
39
  if: steps.impacted.outputs.has-impacted == 'true'
68
40
  run: node --test ${{ steps.impacted.outputs.files }}
69
41
  ```
70
42
 
71
- #### Inputs
72
-
73
- | Input | Description | Default |
74
- |-------|-------------|---------|
75
- | `base` | Base ref to compare against | PR base or `HEAD~1` |
76
- | `head` | Head ref | `HEAD` |
77
- | `pattern` | Glob pattern for test files | `**/*.test.js` |
78
- | `working-directory` | Working directory | `.` |
79
-
80
- #### Outputs
43
+ See [action.yml](https://github.com/sozua/impacted/blob/main/action.yml) for all inputs and outputs.
81
44
 
82
- | Output | Description |
83
- |--------|-------------|
84
- | `files` | Space-separated list of impacted test files |
85
- | `files-json` | JSON array of impacted test files |
86
- | `count` | Number of impacted test files |
87
- | `has-impacted` | Whether any tests are impacted (`true`/`false`) |
88
-
89
- ### Programmatic API
45
+ ## Programmatic API
90
46
 
91
47
  ```javascript
92
48
  import { findImpacted } from 'impacted';
93
49
 
94
- const tests = await findImpacted({
95
- changedFiles: ['src/utils.js', 'src/parser.js'],
96
- testFiles: 'test/**/*.test.js',
97
- });
98
-
99
- console.log(tests);
100
- // ['/project/test/utils.test.js', '/project/test/parser.test.js']
101
- ```
102
-
103
- #### With caching
104
-
105
- ```javascript
106
50
  const tests = await findImpacted({
107
51
  changedFiles: ['src/utils.js'],
108
52
  testFiles: 'test/**/*.test.js',
109
- cacheFile: '.impacted-cache.json',
53
+ cacheFile: '.impacted-cache.json', // optional
110
54
  });
111
55
  ```
112
56
 
113
- #### Options
114
-
115
- | Option | Type | Description |
116
- |--------|------|-------------|
117
- | `changedFiles` | `string[]` | Files that changed (relative or absolute) |
118
- | `testFiles` | `string \| string[]` | Glob pattern(s) or explicit file list |
119
- | `cwd` | `string` | Working directory (default: `process.cwd()`) |
120
- | `excludePaths` | `string[]` | Paths to exclude (default: `['node_modules', 'dist']`) |
121
- | `cacheFile` | `string` | Path to persist cache between runs |
122
-
123
- ## How it works
124
-
125
- 1. Build a dependency graph starting from your test files
126
- 2. Invert the graph to map each file to its dependents
127
- 3. Walk the inverted graph from changed files to find impacted tests
128
-
129
- ```
130
- src/utils.js (changed)
131
- ↓ imported by
132
- src/parser.js
133
- ↓ imported by
134
- test/parser.test.js (impacted)
135
- ```
136
-
137
- ## Known limitations
57
+ ## Limitations
138
58
 
139
- - **JavaScript only** - Supports `.js`, `.mjs`, `.cjs`, and `.jsx`. TypeScript files are not yet supported.
140
- - **Static analysis** - Cannot detect dynamic imports with variables (e.g., `require(getModuleName())`). Only string literals are resolved.
141
- - **Local files only** - Dependencies in `node_modules` are excluded from the graph. Changes to dependencies won't trigger impacted tests.
142
- - **No TypeScript path aliases** - Only Node.js subpath imports (`#imports` in package.json) are supported, not `tsconfig.json` paths.
59
+ - JavaScript only (`.js`, `.mjs`, `.cjs`, `.jsx`) no TypeScript yet
60
+ - Static analysis only dynamic `require(variable)` not supported
61
+ - Local files only `node_modules` changes won't trigger tests
143
62
 
144
63
  ## Requirements
145
64
 
package/bin/impacted.js CHANGED
@@ -4,12 +4,14 @@ import { findImpacted } from '../src/index.js';
4
4
 
5
5
  const args = process.argv.slice(2);
6
6
 
7
- // Parse -p/--pattern flag
8
- let pattern = '**/*.test.js';
9
- const patternIndex = args.findIndex((arg) => arg === '-p' || arg === '--pattern');
10
- if (patternIndex !== -1 && args[patternIndex + 1]) {
11
- pattern = args[patternIndex + 1];
7
+ // Parse -p/--pattern flags (supports multiple: -p "*.test.js" -p "*.spec.js")
8
+ const patterns = [];
9
+ for (let i = 0; i < args.length; i++) {
10
+ if ((args[i] === '-p' || args[i] === '--pattern') && args[i + 1]) {
11
+ patterns.push(args[++i]);
12
+ }
12
13
  }
14
+ const pattern = patterns.length > 0 ? patterns : '**/*.{test,spec}.{js,mjs,cjs,jsx}';
13
15
 
14
16
  // Read changed files from stdin
15
17
  let input = '';
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "impacted",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Find test files impacted by code changes using static dependency analysis",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
7
8
  "exports": {
8
- ".": "./src/index.js"
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "default": "./src/index.js"
12
+ }
9
13
  },
10
14
  "bin": {
11
15
  "impacted": "./bin/impacted.js"
@@ -15,7 +19,8 @@
15
19
  "bin"
16
20
  ],
17
21
  "scripts": {
18
- "test": "node --test"
22
+ "test": "node --test",
23
+ "test:examples": "node --test examples/test.js"
19
24
  },
20
25
  "keywords": [
21
26
  "test",
@@ -44,6 +49,7 @@
44
49
  "dependencies": {
45
50
  "acorn": "^8.14.0",
46
51
  "acorn-walk": "^8.3.4",
47
- "fast-glob": "^3.3.3"
52
+ "fast-glob": "^3.3.3",
53
+ "is-glob": "^4.0.3"
48
54
  }
49
55
  }
package/src/index.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ export interface FindImpactedOptions {
2
+ /** Files that changed (relative or absolute) */
3
+ changedFiles: string[];
4
+ /** Test file glob pattern(s) or explicit file list */
5
+ testFiles: string | string[];
6
+ /** Working directory (default: process.cwd()) */
7
+ cwd?: string;
8
+ /** Paths to exclude from graph (default: ['node_modules', 'dist']) */
9
+ excludePaths?: string[];
10
+ /** Cache instance for import extraction */
11
+ cache?: ImportCache;
12
+ /** Path to persist cache (creates cache automatically) */
13
+ cacheFile?: string;
14
+ }
15
+
16
+ export interface GraphOptions {
17
+ /** Paths to exclude from graph (default: ['node_modules', 'dist']) */
18
+ excludePaths?: string[];
19
+ /** Cache instance for import extraction */
20
+ cache?: ImportCache;
21
+ }
22
+
23
+ export interface CacheOptions {
24
+ /** Path to persist cache */
25
+ cacheFile?: string;
26
+ }
27
+
28
+ export interface CacheStats {
29
+ hits: number;
30
+ misses: number;
31
+ size: number;
32
+ }
33
+
34
+ /** Find test files impacted by code changes */
35
+ export function findImpacted(options: FindImpactedOptions): Promise<string[]>;
36
+
37
+ /** Build a dependency graph starting from entry files */
38
+ export function buildDependencyGraph(
39
+ entryFiles: string[],
40
+ options?: GraphOptions,
41
+ ): Map<string, Set<string>>;
42
+
43
+ /** Find test files impacted by changed files given a dependency graph */
44
+ export function findImpactedTests(
45
+ changedFiles: string[],
46
+ testFiles: string[],
47
+ graph: Map<string, Set<string>>,
48
+ ): string[];
49
+
50
+ /** Invert a dependency graph (file → deps becomes dep → dependents) */
51
+ export function invertGraph(
52
+ graph: Map<string, Set<string>>,
53
+ ): Map<string, Set<string>>;
54
+
55
+ /** Extract import/require specifiers from source code */
56
+ export function parseImports(source: string): string[];
57
+
58
+ /** Resolve an import specifier to an absolute file path */
59
+ export function resolveSpecifier(
60
+ specifier: string,
61
+ parentPath: string,
62
+ ): string | null;
63
+
64
+ /** Create a cache instance */
65
+ export function createCache(options?: CacheOptions): ImportCache;
66
+
67
+ /** Cache for import extraction results with mtime-based invalidation */
68
+ export class ImportCache {
69
+ constructor(options?: CacheOptions);
70
+ /** Get cached imports for a file, or null if not cached/stale */
71
+ get(filePath: string): string[] | null;
72
+ /** Store imports for a file */
73
+ set(filePath: string, imports: string[]): void;
74
+ /** Get cache statistics */
75
+ stats(): CacheStats;
76
+ /** Persist cache to disk */
77
+ save(): void;
78
+ /** Clear all cached data */
79
+ clear(): void;
80
+ }
package/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { resolve } from 'node:path';
2
2
  import fg from 'fast-glob';
3
+ import isGlob from 'is-glob';
3
4
  import { getImpactedTests } from './graph.js';
4
5
  import { createCache, ImportCache } from './cache.js';
5
6
 
@@ -50,7 +51,7 @@ export async function findImpacted(options) {
50
51
 
51
52
  // Resolve test files - either glob or explicit list
52
53
  let absoluteTestFiles;
53
- if (typeof testFiles === 'string' || (Array.isArray(testFiles) && testFiles.some((t) => t.includes('*')))) {
54
+ if (typeof testFiles === 'string' || (Array.isArray(testFiles) && testFiles.some((t) => isGlob(t)))) {
54
55
  // It's a glob pattern
55
56
  const patterns = Array.isArray(testFiles) ? testFiles : [testFiles];
56
57
  absoluteTestFiles = await fg(patterns, { cwd, absolute: true });