8000 Support the new watch mode in AVA 6 · avajs/typescript@2b6fb25 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2b6fb25

Browse files
committed
Support the new watch mode in AVA 6
1 parent e0a5cf0 commit 2b6fb25

File tree

4 files changed

+402
-30
lines changed

4 files changed

+402
-30
lines changed

index.js

Lines changed: 145 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,14 @@ const configProperties = {
6868
},
6969
};
7070

71+
const changeInterpretations = Object.freeze(Object.assign(Object.create(null), {
72+
unspecified: 0,
73+
ignoreCompiled: 1,
74+
waitForOutOfBandCompilation: 2,
75+
}));
76+
7177
export default function typescriptProvider({negotiateProtocol}) {
72-
const protocol = negotiateProtocol(['ava-3.2'], {version: pkg.version});
78+
const protocol = negotiateProtocol(['ava-6', 'ava-3.2'], {version: pkg.version});
7379
if (protocol === null) {
7480
return;
7581
}
@@ -94,7 +100,145 @@ export default function typescriptProvider({negotiateProtocol}) {
94100
]);
95101
const testFileExtension = new RegExp(`\\.(${extensions.map(ext => escapeStringRegexp(ext)).join('|')})$`);
96102

103+
const watchMode = protocol.identifier === 'ava-3.2'
104+
? {
105+
ignoreChange(filePath) {
106+
if (!testFileExtension.test(filePath)) {
107+
return false;
108+
}
109+
110+
return rewritePaths.some(([from]) => filePath.startsWith(from));
111+
},
112+
113+
resolveTestFile(testfile) { // Used under AVA 3.2 protocol by legacy watcher implementation.
114+
if (!testFileExtension.test(testfile)) {
115+
return testfile;
116+
}
117+
118+
const rewrite = rewritePaths.find(([from]) => testfile.startsWith(from));
119+
if (rewrite === undefined) {
120+
return testfile;
121+
}
122+
123+
const [from, to] = rewrite;
124+
let newExtension = '.js';
125+
if (testfile.endsWith('.cts')) {
126+
newExtension = '.cjs';
127+
} else if (testfile.endsWith('.mts')) {
128+
newExtension = '.mjs';
129+
}
130+
131+
return `${to}${testfile.slice(from.length)}`.replace(testFileExtension, newExtension);
132+
},
133+
}
134+
: {
135+
changeInterpretations,
136+
interpretChange(filePath) {
137+
if (config.compile === false) {
138+
for (const [from] of rewritePaths) {
139+
if (testFileExtension.test(filePath) && filePath.startsWith(from)) {
140+
return changeInterpretations.waitForOutOfBandCompilation;
141+
}
142+
}
143+
}
144+
145+
if (config.compile === 'tsc') {
146+
for (const [, to] of rewritePaths) {
147+
if (filePath.startsWith(to)) {
148+
return changeInterpretations.ignoreCompiled;
149+
}
150+
}
151+
}
152+
153+
return changeInterpretations.unspecified;
154+
},
155+
156+
resolvePossibleOutOfBandCompilationSources(filePath) {
157+
if (config.compile !== false) {
158+
return null;
159+
}
160+
161+
// Only recognize .cjs, .mjs and .js files.
162+
if (!/\.(c|m)?js$/.test(filePath)) {
163+
return null;
164+
}
165+
166+
for (const [from, to] of rewritePaths) {
167+
if (!filePath.startsWith(to)) {
168+
continue;
169+
}
170+
171+
const rewritten = `${from}${filePath.slice(to.length)}`;
172+
const possibleExtensions = [];
173+
174+
if (filePath.endsWith('.cjs')) {
175+
if (extensions.includes('cjs')) {
176+
possibleExtensions.push({replace: /\.cjs$/, extension: 'cjs'});
177+
}
178+
179+
if (extensions.includes('cts')) {
180+
possibleExtensions.push({replace: /\.cjs$/, extension: 'cts'});
181+
}
182+
183+
if (possibleExtensions.length === 0) {
184+
return null;
185+
}
186+
}
187+
188+
if (filePath.endsWith('.mjs')) {
189+
if (extensions.includes('mjs')) {
190+
possibleExtensions.push({replace: /\.mjs$/, extension: 'mjs'});
191+
}
192+
193+
if (extensions.includes('mts')) {
194+
possibleExtensions.push({replace: /\.mjs$/, extension: 'mts'});
195+
}
196+
197+
if (possibleExtensions.length === 0) {
198+
return null;
199+
}
200+
}
201+
202+
if (filePath.endsWith('.js')) {
203+
if (extensions.includes('js')) {
204+
possibleExtensions.push({replace: /\.js$/, extension: 'js'});
205+
}
206+
207+
if (extensions.includes('ts')) {
208+
possibleExtensions.push({replace: /\.js$/, extension: 'ts'});
209+
}
210+
211+
if (extensions.includes('tsx')) {
212+
possibleExtensions.push({replace: /\.js$/, extension: 'tsx'});
213+
}
214+
215+
if (possibleExtensions.length === 0) {
216+
return null;
217+
}
218+
}
219+
220+
const possibleDeletedFiles = [];
221+
for (const {replace, extension} of possibleExtensions) {
222+
const possibleFilePath = rewritten.replace(replace, `.${extension}`);
223+
224+
// Pick the first file path that exists.
225+
if (fs.existsSync(possibleFilePath)) {
226+
return [possibleFilePath];
227+
}
228+
229+
possibleDeletedFiles.push(possibleFilePath);
230+
}
231+
232+
return possibleDeletedFiles;
233+
}
234+
235+
return null;
236+
},
237+
};
238+
97239
return {
240+
...watchMode,
241+
98242
async compile() {
99243
if (compile === 'tsc') {
100244
await compileTypeScript(protocol.projectDir);
@@ -110,35 +254,6 @@ export default function typescriptProvider({negotiateProtocol}) {
110254
return [...extensions];
111255
},
112256

113-
ignoreChange(filePath) {
114-
if (!testFileExtension.test(filePath)) {
115-
return false;
116-
}
117-
118-
return rewritePaths.some(([from]) => filePath.startsWith(from));
119-
},
120-
121-
resolveTestFile(testfile) { // Used under AVA 3.2 protocol by legacy watcher implementation.
122-
if (!testFileExtension.test(testfile)) {
123-
return testfile;
124-
}
125-
126-
const rewrite = rewritePaths.find(([from]) => testfile.startsWith(from));
127-
if (rewrite === undefined) {
128-
return testfile;
129-
}
130-
131-
const [from, to] = rewrite;
132-
let newExtension = '.js';
133-
if (testfile.endsWith('.cts')) {
134-
newExtension = '.cjs';
135-
} else if (testfile.endsWith('.mts')) {
136-
newExtension = '.mjs';
137-
}
138-
139-
return `${to}${testfile.slice(from.length)}`.replace(testFileExtension, newExtension);
140-
},
141-
142257
updateGlobs({filePatterns, ignoredByWatcherPatterns}) {
143258
return {
144259
filePatterns: [

test/protocol-ava-6.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import {fileURLToPath} from 'node:url';
4+
import test from 'ava';
5+
import createProviderMacro from './_with-provider.js';
6+
7+
const projectDir = path.dirname(fileURLToPath(import.meta.url));
8+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
9+
const withProvider = createProviderMacro('ava-6', '5.3.0');
10+
11+
const validateConfig = (t, provider, config) => {
12+
const error = t.throws(() => provider.main({config}));
13+
error.message = error.message.replace(`v${pkg.version}`, 'v${pkg.version}'); // eslint-disable-line no-template-curly-in-string
14+
t.snapshot(error);
15+
};
16+
17+
test('negotiates ava-6 protocol', withProvider, t => t.plan(2));
18+
19+
test('main() config validation: throw when config is not a plain object', withProvider, (t, provider) => {
20+
validateConfig(t, provider, false);
21+
validateConfig(t, provider, true);
22+
validateConfig(t, provider, null);
23+
validateConfig(t, provider, []);
24+
});
25+
26+
test('main() config validation: throw when config contains keys other than \'extensions\', \'rewritePaths\' or \'compile\'', withProvider, (t, provider) => {
27+
validateConfig(t, provider, {compile: false, foo: 1, rewritePaths: {'src/': 'build/'}});
28+
});
29+
30+
test('main() config validation: throw when config.extensions contains empty strings', withProvider, (t, provider) => {
31+
validateConfig(t, provider, {extensions: ['']});
32+
});
33+
34+
test('main() config validation: throw when config.extensions contains non-strings', withProvider, (t, provider) => {
35+
validateConfig(t, provider, {extensions: [1]});
36+
});
37+
38+
test('main() config validation: throw when config.extensions contains duplicates', withProvider, (t, provider) => {
39+
validateConfig(t, provider, {extensions: ['ts', 'ts']});
40+
});
41+
42+
test('main() config validation: config may not be an empty object', withProvider, (t, provider) => {
43+
validateConfig(t, provider, {});
44+
});
45+
46+
test('main() config validation: throw when config.compile is invalid', withProvider, (t, provider) => {
47+
validateConfig(t, provider, {rewritePaths: {'src/': 'build/'}, compile: 1});
48+
validateConfig(t, provider, {rewritePaths: {'src/': 'build/'}, compile: undefined});
49+
});
50+
51+
test('main() config validation: rewrite paths must end in a /', withProvider, (t, provider) => {
52+
validateConfig(t, provider, {rewritePaths: {src: 'build/', compile: false}});
53+
validateConfig(t, provider, {rewritePaths: {'src/': 'build', compile: false}});
54+
});
55+
56+
test('main() extensions: defaults to [\'ts\', \'cts\', \'mts\']', withProvider, (t, provider) => {
57+
t.deepEqual(provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}}).extensions, ['ts', 'cts', 'mts']);
58+
});
59+
60+
test('main() extensions: returns configured extensions', withProvider, (t, provider) => {
61+
const extensions = ['tsx'];
62+
t.deepEqual(provider.main({config: {extensions, rewritePaths: {'src/': 'build/'}, compile: false}}).extensions, extensions);
63+
});
64+
65+
test('main() extensions: always returns new arrays', withProvider, (t, provider) => {
66+
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}});
67+
t.not(main.extensions, main.extensions);
68+
});
69+
70+
test('main() updateGlobs()', withProvider, (t, provider) => {
71+
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}});
72+
t.snapshot(main.updateGlobs({
73+
filePatterns: ['src/test.ts'],
74+
ignoredByWatcherPatterns: ['assets/**'],
75+
}));
76+
});
77+
78+
test('main() interpretChange() without compilation', withProvider, (t, provider) => {
79+
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}});
80+
t.is(main.interpretChange(path.join(projectDir, 'src/foo.ts')), main.changeInterpretations.waitForOutOfBandCompilation);
81+
t.is(main.interpretChange(path.join(projectDir, 'build/foo.js')), main.changeInterpretations.unspecified);
82+
t.is(main.interpretChange(path.join(projectDir, 'src/foo.txt')), main.changeInterpretations.unspecified);
83+
});
84+
85+
test('main() interpretChange() with compilation', withProvider, (t, provider) => {
86+
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: 'tsc'}});
87+
t.is(main.interpretChange(path.join(projectDir, 'src/foo.ts')), main.changeInterpretations.unspecified);
88+
t.is(main.interpretChange(path.join(projectDir, 'build/foo.js')), main.changeInterpretations.ignoreCompiled);
89+
t.is(main.interpretChange(path.join(projectDir, 'src/foo.txt')), main.changeInterpretations.unspecified);
90+
});
91+
92+
test('main() resolvePossibleOutOfBandCompilationSources() with compilation', withProvider, (t, provider) => {
93+
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: 'tsc'}});
94+
t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.js')), null);
95+
});
96+
97+
test('main() resolvePossibleOutOfBandCompilationSources() unknown extension', withProvider, (t, provider) => {
98+
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}});
99+
t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.bar')), null);
100+
});
101+
102+
test('main() resolvePossibleOutOfBandCompilationSources() not a build path', withProvider, (t, provider) => {
103+
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}});
104+
t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'lib/foo.js')), null);
105+
});
106+
107+
test('main() resolvePossibleOutOfBandCompilationSources() .cjs but .cts not configured', withProvider, (t, provider) => {
108+
const main = provider.main({config: {extensions: ['ts'], rewritePaths: {'src/': 'build/'}, compile: false}});
109+
t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.cjs')), null);
110+
});
111+
112+
test('main() resolvePossibleOutOfBandCompilationSources() .mjs but .mts not configured', withProvider, (t, provider) => {
113+
const main = provider.main({config: {extensions: ['ts'], rewritePaths: {'src/': 'build/'}, compile: false}});
114+
t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.mjs')), null);
115+
});
116+
117+
test('main() resolvePossibleOutOfBandCompilationSources() .js but .ts not configured', withProvider, (t, provider) => {
118+
const main = provider.main({config: {extensions: ['cts'], rewritePaths: {'src/': 'build/'}, compile: false}});
119+
t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.js')), null);
120+
});
121+
122+
test('main() resolvePossibleOutOfBandCompilationSources() .cjs and .cjs and .cts configured', withProvider, (t, provider) => {
123+
const main = provider.main({config: {extensions: ['cjs', 'cts'], rewritePaths: {'src/': 'build/'}, compile: false}});
124+
t.deepEqual(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.cjs')), [path.join(projectDir, 'src/foo.cjs'), path.join(projectDir, 'src/foo.cts')]);
125+
});
126+
127+
test('main() resolvePossibleOutOfBandCompilationSources() .mjs and .mjs and .mts configured', withProvider, (t, provider) => {
128+
const main = provider.main({config: {extensions: ['mjs', 'mts'], rewritePaths: {'src/': 'build/'}, compile: false}});
129+
t.deepEqual(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.mjs')), [path.join(projectDir, 'src/foo.mjs'), path.join(projectDir, 'src/foo.mts')]);
130+
});
131+
132+
test('main() resolvePossibleOutOfBandCompilationSources() .js and .js, .ts and .tsx configured', withProvider, (t, provider) => {
133+
const main = provider.main({config: {extensions: ['js', 'ts', 'tsx'], rewritePaths: {'src/': 'build/'}, compile: false}});
134+
t.deepEqual(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.js')), [path.join(projectDir, 'src/foo.js'), path.join(projectDir, 'src/foo.ts'), path.join(projectDir, 'src/foo.tsx')]);
135+
});
136+
137+
test('main() resolvePossibleOutOfBandCompilationSources() returns the first possible path that exists', withProvider, (t, provider) => {
138+
const main = provider.main({config: {extensions: ['js', 'ts', 'tsx'], rewritePaths: {'fixtures/load/': 'fixtures/load/compiled/'}, compile: false}});
139+
t.deepEqual(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'fixtures/load/compiled/index.js')), [path.join(projectDir, 'fixtures/load/index.ts')]);
140+
});

0 commit comments

Comments
 (0)
0