279 lines
7.5 KiB
JavaScript
279 lines
7.5 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import {pathToFileURL} from 'node:url';
|
|
import escapeStringRegexp from 'escape-string-regexp';
|
|
import {execa} from 'execa';
|
|
import pkg from './package.json' with {type: 'json'};
|
|
|
|
const help = `See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md`;
|
|
|
|
function isPlainObject(x) {
|
|
return x !== null && typeof x === 'object' && Reflect.getPrototypeOf(x) === Object.prototype;
|
|
}
|
|
|
|
function validate(target, properties) {
|
|
for (const key of Object.keys(properties)) {
|
|
const {required, isValid} = properties[key];
|
|
const missing = !Reflect.has(target, key);
|
|
|
|
if (missing) {
|
|
if (required) {
|
|
throw new Error(`Missing '${key}' property in TypeScript configuration for AVA. ${help}`);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!isValid(target[key])) {
|
|
throw new Error(`Invalid '${key}' property in TypeScript configuration for AVA. ${help}`);
|
|
}
|
|
}
|
|
|
|
for (const key of Object.keys(target)) {
|
|
if (!Reflect.has(properties, key)) {
|
|
throw new Error(`Unexpected '${key}' property in TypeScript configuration for AVA. ${help}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function compileTypeScript(projectDirectory) {
|
|
return execa({preferLocal: true, cwd: projectDirectory})`tsc --incremental`;
|
|
}
|
|
|
|
const configProperties = {
|
|
compile: {
|
|
required: true,
|
|
isValid(compile) {
|
|
return compile === false || compile === 'tsc';
|
|
},
|
|
},
|
|
rewritePaths: {
|
|
required: true,
|
|
isValid(rewritePaths) {
|
|
if (!isPlainObject(rewritePaths)) {
|
|
return false;
|
|
}
|
|
|
|
return Object.entries(rewritePaths).every(([from, to]) => from.endsWith('/') && typeof to === 'string' && to.endsWith('/'));
|
|
},
|
|
},
|
|
extensions: {
|
|
required: false,
|
|
isValid(extensions) {
|
|
return Array.isArray(extensions)
|
|
&& extensions.length > 0
|
|
&& extensions.every(extension => typeof extension === 'string' && extension !== '')
|
|
&& new Set(extensions).size === extensions.length;
|
|
},
|
|
},
|
|
};
|
|
|
|
const changeInterpretations = Object.freeze(Object.assign(Object.create(null), {
|
|
unspecified: 0,
|
|
ignoreCompiled: 1,
|
|
waitForOutOfBandCompilation: 2,
|
|
}));
|
|
|
|
const defaultExtensions = ['ts', 'mts'];
|
|
const defaultAva6Extensions = ['ts', 'cts', 'mts'];
|
|
|
|
export default function typescriptProvider({negotiateProtocol}) {
|
|
const protocol = negotiateProtocol(['ava-6', 'ava-8'], {version: pkg.version});
|
|
if (protocol === null) {
|
|
return;
|
|
}
|
|
|
|
const isAva8 = protocol.identifier === 'ava-8';
|
|
|
|
return {
|
|
main({config}) {
|
|
if (!isPlainObject(config)) {
|
|
throw new Error(`Unexpected Typescript configuration for AVA. ${help}`);
|
|
}
|
|
|
|
validate(config, configProperties);
|
|
|
|
const {
|
|
extensions = isAva8 ? defaultExtensions : defaultAva6Extensions,
|
|
rewritePaths: relativeRewritePaths,
|
|
compile,
|
|
} = config;
|
|
|
|
const rewritePaths = Object.entries(relativeRewritePaths).map(([from, to]) => [
|
|
path.join(protocol.projectDir, from),
|
|
path.join(protocol.projectDir, to),
|
|
]);
|
|
const testFileExtension = new RegExp(String.raw`\.(${extensions.map(extension => escapeStringRegexp(extension)).join('|')})$`, 'v');
|
|
|
|
const watchMode = {
|
|
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$/v.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$/v, extension: 'cjs'});
|
|
}
|
|
|
|
if (extensions.includes('cts')) {
|
|
possibleExtensions.push({replace: /\.cjs$/v, extension: 'cts'});
|
|
}
|
|
|
|
if (possibleExtensions.length === 0) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (filePath.endsWith('.mjs')) {
|
|
if (extensions.includes('mjs')) {
|
|
possibleExtensions.push({replace: /\.mjs$/v, extension: 'mjs'});
|
|
}
|
|
|
|
if (extensions.includes('mts')) {
|
|
possibleExtensions.push({replace: /\.mjs$/v, extension: 'mts'});
|
|
}
|
|
|
|
if (possibleExtensions.length === 0) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (filePath.endsWith('.js')) {
|
|
if (extensions.includes('js')) {
|
|
possibleExtensions.push({replace: /\.js$/v, extension: 'js'});
|
|
}
|
|
|
|
if (extensions.includes('ts')) {
|
|
possibleExtensions.push({replace: /\.js$/v, extension: 'ts'});
|
|
}
|
|
|
|
if (extensions.includes('tsx')) {
|
|
possibleExtensions.push({replace: /\.js$/v, 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);
|
|
}
|
|
|
|
return {
|
|
extensions: [...extensions],
|
|
rewritePaths: [...rewritePaths],
|
|
};
|
|
},
|
|
|
|
get extensions() {
|
|
return [...extensions];
|
|
},
|
|
|
|
updateGlobs({filePatterns, ignoredByWatcherPatterns}) {
|
|
return {
|
|
filePatterns: [
|
|
...filePatterns,
|
|
'!**/*.d.ts',
|
|
...Object.values(relativeRewritePaths).map(to => `!${to}**`),
|
|
],
|
|
ignoredByWatcherPatterns: [
|
|
...ignoredByWatcherPatterns,
|
|
...Object.values(relativeRewritePaths).flatMap(to => [
|
|
`${to}**/*.js.map`,
|
|
`${to}**/*.cjs.map`,
|
|
`${to}**/*.mjs.map`,
|
|
]),
|
|
],
|
|
};
|
|
},
|
|
};
|
|
},
|
|
|
|
worker({extensionsToLoadAsModules, state: {extensions, rewritePaths}}) {
|
|
const importJs = isAva8 || extensionsToLoadAsModules.includes('js');
|
|
const testFileExtension = new RegExp(String.raw`\.(${extensions.map(extension => escapeStringRegexp(extension)).join('|')})$`, 'v');
|
|
|
|
return {
|
|
canLoad(reference) {
|
|
return testFileExtension.test(reference) && rewritePaths.some(([from]) => reference.startsWith(from));
|
|
},
|
|
|
|
async load(reference, {requireFn} = {}) {
|
|
const [from, to] = rewritePaths.find(([from]) => reference.startsWith(from));
|
|
let rewritten = `${to}${reference.slice(from.length)}`;
|
|
let useImport = true;
|
|
if (reference.endsWith('.cts')) {
|
|
rewritten = rewritten.replace(/\.cts$/v, '.cjs');
|
|
useImport = isAva8;
|
|
} else if (reference.endsWith('.mts')) {
|
|
rewritten = rewritten.replace(/\.mts$/v, '.mjs');
|
|
} else {
|
|
rewritten = rewritten.replace(testFileExtension, '.js');
|
|
useImport = importJs;
|
|
}
|
|
|
|
return useImport ? import(pathToFileURL(rewritten)) : requireFn(rewritten);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|