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 +33 -8
- package/bin/impacted.js +44 -19
- package/package.json +1 -1
- package/src/constants.js +3 -0
- package/src/graph.js +4 -4
- package/src/index.d.ts +6 -1
- package/src/parser.js +22 -1
- package/src/resolver.js +11 -2
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 $(
|
|
17
|
-
jest $(
|
|
19
|
+
vitest $(npx impacted --since main)
|
|
20
|
+
jest $(npx impacted --since main)
|
|
18
21
|
|
|
19
22
|
# Custom test pattern
|
|
20
|
-
|
|
23
|
+
npx impacted --since main -p "src/**/*.spec.js"
|
|
21
24
|
|
|
22
25
|
# Multiple patterns
|
|
23
|
-
|
|
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
package/src/constants.js
ADDED
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.
|
|
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
|
-
|
|
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
|
}
|