Support the new watch mode in AVA 6

This commit is contained in:
Mark Wubben 2022-12-10 12:31:39 +01:00
parent e0a5cf0ae0
commit 2b6fb25e44
4 changed files with 402 additions and 30 deletions

175
index.js
View file

@ -68,8 +68,14 @@ const configProperties = {
},
};
const changeInterpretations = Object.freeze(Object.assign(Object.create(null), {
unspecified: 0,
ignoreCompiled: 1,
waitForOutOfBandCompilation: 2,
}));
export default function typescriptProvider({negotiateProtocol}) {
const protocol = negotiateProtocol(['ava-3.2'], {version: pkg.version});
const protocol = negotiateProtocol(['ava-6', 'ava-3.2'], {version: pkg.version});
if (protocol === null) {
return;
}
@ -94,7 +100,145 @@ export default function typescriptProvider({negotiateProtocol}) {
]);
const testFileExtension = new RegExp(`\\.(${extensions.map(ext => escapeStringRegexp(ext)).join('|')})$`);
const watchMode = protocol.identifier === 'ava-3.2'
? {
ignoreChange(filePath) {
if (!testFileExtension.test(filePath)) {
return false;
}
return rewritePaths.some(([from]) => filePath.startsWith(from));
},
resolveTestFile(testfile) { // Used under AVA 3.2 protocol by legacy watcher implementation.
if (!testFileExtension.test(testfile)) {
return testfile;
}
const rewrite = rewritePaths.find(([from]) => testfile.startsWith(from));
if (rewrite === undefined) {
return testfile;
}
const [from, to] = rewrite;
let newExtension = '.js';
if (testfile.endsWith('.cts')) {
newExtension = '.cjs';
} else if (testfile.endsWith('.mts')) {
newExtension = '.mjs';
}
return `${to}${testfile.slice(from.length)}`.replace(testFileExtension, newExtension);
},
}
: {
changeInterpretations,
interpretChange(filePath) {
if (config.compile === false) {
for (const [from] of rewritePaths) {
if (testFileExtension.test(filePath) && filePath.startsWith(from)) {
return changeInterpretations.waitForOutOfBandCompilation;
}
}
}
if (config.compile === 'tsc') {
for (const [, to] of rewritePaths) {
if (filePath.startsWith(to)) {
return changeInterpretations.ignoreCompiled;
}
}
}
return changeInterpretations.unspecified;
},
resolvePossibleOutOfBandCompilationSources(filePath) {
if (config.compile !== false) {
return null;
}
// Only recognize .cjs, .mjs and .js files.
if (!/\.(c|m)?js$/.test(filePath)) {
return null;
}
for (const [from, to] of rewritePaths) {
if (!filePath.startsWith(to)) {
continue;
}
const rewritten = `${from}${filePath.slice(to.length)}`;
const possibleExtensions = [];
if (filePath.endsWith('.cjs')) {
if (extensions.includes('cjs')) {
possibleExtensions.push({replace: /\.cjs$/, extension: 'cjs'});
}
if (extensions.includes('cts')) {
possibleExtensions.push({replace: /\.cjs$/, extension: 'cts'});
}
if (possibleExtensions.length === 0) {
return null;
}
}
if (filePath.endsWith('.mjs')) {
if (extensions.includes('mjs')) {
possibleExtensions.push({replace: /\.mjs$/, extension: 'mjs'});
}
if (extensions.includes('mts')) {
possibleExtensions.push({replace: /\.mjs$/, extension: 'mts'});
}
if (possibleExtensions.length === 0) {
return null;
}
}
if (filePath.endsWith('.js')) {
if (extensions.includes('js')) {
possibleExtensions.push({replace: /\.js$/, extension: 'js'});
}
if (extensions.includes('ts')) {
possibleExtensions.push({replace: /\.js$/, extension: 'ts'});
}
if (extensions.includes('tsx')) {
possibleExtensions.push({replace: /\.js$/, extension: 'tsx'});
}
if (possibleExtensions.length === 0) {
return null;
}
}
const possibleDeletedFiles = [];
for (const {replace, extension} of possibleExtensions) {
const possibleFilePath = rewritten.replace(replace, `.${extension}`);
// Pick the first file path that exists.
if (fs.existsSync(possibleFilePath)) {
return [possibleFilePath];
}
possibleDeletedFiles.push(possibleFilePath);
}
return possibleDeletedFiles;
}
return null;
},
};
return {
...watchMode,
async compile() {
if (compile === 'tsc') {
await compileTypeScript(protocol.projectDir);
@ -110,35 +254,6 @@ export default function typescriptProvider({negotiateProtocol}) {
return [...extensions];
},
ignoreChange(filePath) {
if (!testFileExtension.test(filePath)) {
return false;
}
return rewritePaths.some(([from]) => filePath.startsWith(from));
},
resolveTestFile(testfile) { // Used under AVA 3.2 protocol by legacy watcher implementation.
if (!testFileExtension.test(testfile)) {
return testfile;
}
const rewrite = rewritePaths.find(([from]) => testfile.startsWith(from));
if (rewrite === undefined) {
return testfile;
}
const [from, to] = rewrite;
let newExtension = '.js';
if (testfile.endsWith('.cts')) {
newExtension = '.cjs';
} else if (testfile.endsWith('.mts')) {
newExtension = '.mjs';
}
return `${to}${testfile.slice(from.length)}`.replace(testFileExtension, newExtension);
},
updateGlobs({filePatterns, ignoredByWatcherPatterns}) {
return {
filePatterns: [

140
test/protocol-ava-6.js Normal file
View file

@ -0,0 +1,140 @@
import fs from 'node:fs';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import test from 'ava';
import createProviderMacro from './_with-provider.js';
const projectDir = path.dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
const withProvider = createProviderMacro('ava-6', '5.3.0');
const validateConfig = (t, provider, config) => {
const error = t.throws(() => provider.main({config}));
error.message = error.message.replace(`v${pkg.version}`, 'v${pkg.version}'); // eslint-disable-line no-template-curly-in-string
t.snapshot(error);
};
test('negotiates ava-6 protocol', withProvider, t => t.plan(2));
test('main() config validation: throw when config is not a plain object', withProvider, (t, provider) => {
validateConfig(t, provider, false);
validateConfig(t, provider, true);
validateConfig(t, provider, null);
validateConfig(t, provider, []);
});
test('main() config validation: throw when config contains keys other than \'extensions\', \'rewritePaths\' or \'compile\'', withProvider, (t, provider) => {
validateConfig(t, provider, {compile: false, foo: 1, rewritePaths: {'src/': 'build/'}});
});
test('main() config validation: throw when config.extensions contains empty strings', withProvider, (t, provider) => {
validateConfig(t, provider, {extensions: ['']});
});
test('main() config validation: throw when config.extensions contains non-strings', withProvider, (t, provider) => {
validateConfig(t, provider, {extensions: [1]});
});
test('main() config validation: throw when config.extensions contains duplicates', withProvider, (t, provider) => {
validateConfig(t, provider, {extensions: ['ts', 'ts']});
});
test('main() config validation: config may not be an empty object', withProvider, (t, provider) => {
validateConfig(t, provider, {});
});
test('main() config validation: throw when config.compile is invalid', withProvider, (t, provider) => {
validateConfig(t, provider, {rewritePaths: {'src/': 'build/'}, compile: 1});
validateConfig(t, provider, {rewritePaths: {'src/': 'build/'}, compile: undefined});
});
test('main() config validation: rewrite paths must end in a /', withProvider, (t, provider) => {
validateConfig(t, provider, {rewritePaths: {src: 'build/', compile: false}});
validateConfig(t, provider, {rewritePaths: {'src/': 'build', compile: false}});
});
test('main() extensions: defaults to [\'ts\', \'cts\', \'mts\']', withProvider, (t, provider) => {
t.deepEqual(provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}}).extensions, ['ts', 'cts', 'mts']);
});
test('main() extensions: returns configured extensions', withProvider, (t, provider) => {
const extensions = ['tsx'];
t.deepEqual(provider.main({config: {extensions, rewritePaths: {'src/': 'build/'}, compile: false}}).extensions, extensions);
});
test('main() extensions: always returns new arrays', withProvider, (t, provider) => {
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}});
t.not(main.extensions, main.extensions);
});
test('main() updateGlobs()', withProvider, (t, provider) => {
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}});
t.snapshot(main.updateGlobs({
filePatterns: ['src/test.ts'],
ignoredByWatcherPatterns: ['assets/**'],
}));
});
test('main() interpretChange() without compilation', withProvider, (t, provider) => {
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}});
t.is(main.interpretChange(path.join(projectDir, 'src/foo.ts')), main.changeInterpretations.waitForOutOfBandCompilation);
t.is(main.interpretChange(path.join(projectDir, 'build/foo.js')), main.changeInterpretations.unspecified);
t.is(main.interpretChange(path.join(projectDir, 'src/foo.txt')), main.changeInterpretations.unspecified);
});
test('main() interpretChange() with compilation', withProvider, (t, provider) => {
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: 'tsc'}});
t.is(main.interpretChange(path.join(projectDir, 'src/foo.ts')), main.changeInterpretations.unspecified);
t.is(main.interpretChange(path.join(projectDir, 'build/foo.js')), main.changeInterpretations.ignoreCompiled);
t.is(main.interpretChange(path.join(projectDir, 'src/foo.txt')), main.changeInterpretations.unspecified);
});
test('main() resolvePossibleOutOfBandCompilationSources() with compilation', withProvider, (t, provider) => {
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: 'tsc'}});
t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.js')), null);
});
test('main() resolvePossibleOutOfBandCompilationSources() unknown extension', withProvider, (t, provider) => {
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}});
t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.bar')), null);
});
test('main() resolvePossibleOutOfBandCompilationSources() not a build path', withProvider, (t, provider) => {
const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}});
t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'lib/foo.js')), null);
});
test('main() resolvePossibleOutOfBandCompilationSources() .cjs but .cts not configured', withProvider, (t, provider) => {
const main = provider.main({config: {extensions: ['ts'], rewritePaths: {'src/': 'build/'}, compile: false}});
t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.cjs')), null);
});
test('main() resolvePossibleOutOfBandCompilationSources() .mjs but .mts not configured', withProvider, (t, provider) => {
const main = provider.main({config: {extensions: ['ts'], rewritePaths: {'src/': 'build/'}, compile: false}});
t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.mjs')), null);
});
test('main() resolvePossibleOutOfBandCompilationSources() .js but .ts not configured', withProvider, (t, provider) => {
const main = provider.main({config: {extensions: ['cts'], rewritePaths: {'src/': 'build/'}, compile: false}});
t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.js')), null);
});
test('main() resolvePossibleOutOfBandCompilationSources() .cjs and .cjs and .cts configured', withProvider, (t, provider) => {
const main = provider.main({config: {extensions: ['cjs', 'cts'], rewritePaths: {'src/': 'build/'}, compile: false}});
t.deepEqual(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.cjs')), [path.join(projectDir, 'src/foo.cjs'), path.join(projectDir, 'src/foo.cts')]);
});
test('main() resolvePossibleOutOfBandCompilationSources() .mjs and .mjs and .mts configured', withProvider, (t, provider) => {
const main = provider.main({config: {extensions: ['mjs', 'mts'], rewritePaths: {'src/': 'build/'}, compile: false}});
t.deepEqual(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'build/foo.mjs')), [path.join(projectDir, 'src/foo.mjs'), path.join(projectDir, 'src/foo.mts')]);
});
test('main() resolvePossibleOutOfBandCompilationSources() .js and .js, .ts and .tsx configured', withProvider, (t, provider) => {
const main = provider.main({config: {extensions: ['js', 'ts', 'tsx'], rewritePaths: {'src/': 'build/'}, compile: false}});
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')]);
});
test('main() resolvePossibleOutOfBandCompilationSources() returns the first possible path that exists', withProvider, (t, provider) => {
const main = provider.main({config: {extensions: ['js', 'ts', 'tsx'], rewritePaths: {'fixtures/load/': 'fixtures/load/compiled/'}, compile: false}});
t.deepEqual(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDir, 'fixtures/load/compiled/index.js')), [path.join(projectDir, 'fixtures/load/index.ts')]);
});

View file

@ -0,0 +1,117 @@
# Snapshot report for `test/protocol-ava-6.js`
The actual snapshot is saved in `protocol-ava-6.js.snap`.
Generated by [AVA](https://avajs.dev).
## main() config validation: throw when config is not a plain object
> Snapshot 1
Error {
message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md',
}
> Snapshot 2
Error {
message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md',
}
> Snapshot 3
Error {
message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md',
}
> Snapshot 4
Error {
message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md',
}
## main() config validation: throw when config contains keys other than 'extensions', 'rewritePaths' or 'compile'
> Snapshot 1
Error {
message: 'Unexpected \'foo\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md',
}
## main() config validation: throw when config.extensions contains empty strings
> Snapshot 1
Error {
message: 'Missing \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md',
}
## main() config validation: throw when config.extensions contains non-strings
> Snapshot 1
Error {
message: 'Missing \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md',
}
## main() config validation: throw when config.extensions contains duplicates
> Snapshot 1
Error {
message: 'Missing \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md',
}
## main() config validation: config may not be an empty object
> Snapshot 1
Error {
message: 'Missing \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md',
}
## main() config validation: throw when config.compile is invalid
> Snapshot 1
Error {
message: 'Invalid \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md',
}
> Snapshot 2
Error {
message: 'Invalid \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md',
}
## main() config validation: rewrite paths must end in a /
> Snapshot 1
Error {
message: 'Missing \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md',
}
> Snapshot 2
Error {
message: 'Missing \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md',
}
## main() updateGlobs()
> Snapshot 1
{
filePatterns: [
'src/test.ts',
'!**/*.d.ts',
'!build/**',
],
ignoredByWatcherPatterns: [
'assets/**',
'build/**/*.js.map',
'build/**/*.cjs.map',
'build/**/*.mjs.map',
],
}

Binary file not shown.