@nx/jest
Version:
211 lines (210 loc) • 8.85 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.addOrUpdateProperty = addOrUpdateProperty;
exports.removeProperty = removeProperty;
exports.jestConfigObjectAst = jestConfigObjectAst;
exports.jestConfigObject = jestConfigObject;
const devkit_1 = require("@nx/devkit");
const vm_1 = require("vm");
const path_1 = require("path");
const ensure_typescript_1 = require("@nx/js/src/utils/typescript/ensure-typescript");
let tsModule;
function makeTextToInsert(value, precedingCommaNeeded) {
return `${precedingCommaNeeded ? ',' : ''}${value}`;
}
function findPropertyAssignment(object, propertyName) {
if (!tsModule) {
tsModule = (0, ensure_typescript_1.ensureTypescript)();
}
return object.properties.find((prop) => {
if (!tsModule.isPropertyAssignment(prop)) {
return false;
}
const propNameText = prop.name.getText();
if (propNameText.match(/^["'].+["']$/g)) {
return JSON.parse(propNameText.replace(/'/g, '"')) === propertyName;
}
return propNameText === propertyName;
});
}
function addOrUpdateProperty(tree, object, properties, value, path) {
if (!tsModule) {
tsModule = (0, ensure_typescript_1.ensureTypescript)();
}
const { SyntaxKind } = tsModule;
const propertyName = properties.shift();
const propertyAssignment = findPropertyAssignment(object, propertyName);
const originalContents = tree.read(path, 'utf-8');
if (propertyAssignment) {
if (propertyAssignment.initializer.kind === SyntaxKind.StringLiteral ||
propertyAssignment.initializer.kind === SyntaxKind.NumericLiteral ||
propertyAssignment.initializer.kind === SyntaxKind.FalseKeyword ||
propertyAssignment.initializer.kind === SyntaxKind.TrueKeyword) {
const updatedContents = (0, devkit_1.applyChangesToString)(originalContents, [
{
type: devkit_1.ChangeType.Delete,
start: propertyAssignment.initializer.pos,
length: propertyAssignment.initializer.getFullText().length,
},
{
type: devkit_1.ChangeType.Insert,
index: propertyAssignment.initializer.pos,
text: value,
},
]);
tree.write(path, updatedContents);
return;
}
if (propertyAssignment.initializer.kind === SyntaxKind.ArrayLiteralExpression) {
const arrayLiteral = propertyAssignment.initializer;
if (arrayLiteral.elements.some((element) => {
return element.getText().replace(/'/g, '"') === value;
})) {
return [];
}
if (arrayLiteral.elements.length === 0) {
const updatedContents = (0, devkit_1.applyChangesToString)(originalContents, [
{
type: devkit_1.ChangeType.Insert,
index: arrayLiteral.elements.end,
text: value,
},
]);
tree.write(path, updatedContents);
return;
}
else {
const text = makeTextToInsert(value, arrayLiteral.elements.length !== 0 &&
!arrayLiteral.elements.hasTrailingComma);
const updatedContents = (0, devkit_1.applyChangesToString)(originalContents, [
{
type: devkit_1.ChangeType.Insert,
index: arrayLiteral.elements.end,
text,
},
]);
tree.write(path, updatedContents);
return;
}
}
else if (propertyAssignment.initializer.kind === SyntaxKind.ObjectLiteralExpression) {
return addOrUpdateProperty(tree, propertyAssignment.initializer, properties, value, path);
}
}
else {
if (propertyName === undefined) {
throw new Error(`Please use dot delimited paths to update an existing object. Eg. object.property `);
}
const text = makeTextToInsert(`${JSON.stringify(propertyName)}: ${value}`, object.properties.length !== 0 && !object.properties.hasTrailingComma);
const updatedContents = (0, devkit_1.applyChangesToString)(originalContents, [
{
type: devkit_1.ChangeType.Insert,
index: object.properties.end,
text,
},
]);
tree.write(path, updatedContents);
return;
}
}
function removeProperty(object, properties) {
if (!tsModule) {
tsModule = (0, ensure_typescript_1.ensureTypescript)();
}
const propertyName = properties.shift();
const propertyAssignment = findPropertyAssignment(object, propertyName);
if (propertyAssignment) {
if (properties.length > 0 &&
propertyAssignment.initializer.kind ===
tsModule.SyntaxKind.ObjectLiteralExpression) {
return removeProperty(propertyAssignment.initializer, properties);
}
return propertyAssignment;
}
else {
return null;
}
}
function isModuleExport(node) {
if (!tsModule) {
tsModule = (0, ensure_typescript_1.ensureTypescript)();
}
return (tsModule.isExpressionStatement(node) &&
node.expression?.kind &&
tsModule.isBinaryExpression(node.expression) &&
node.expression.left.getText() === 'module.exports' &&
node.expression.operatorToken?.kind === tsModule.SyntaxKind.EqualsToken);
}
function isDefaultExport(node) {
if (!tsModule) {
tsModule = (0, ensure_typescript_1.ensureTypescript)();
}
return (tsModule.isExportAssignment(node) &&
node.expression?.kind &&
tsModule.isObjectLiteralExpression(node.expression) &&
node.getText().startsWith('export default'));
}
/**
* Should be used to get the jest config object as AST
*/
function jestConfigObjectAst(fileContent) {
if (!tsModule) {
tsModule = (0, ensure_typescript_1.ensureTypescript)();
}
const sourceFile = tsModule.createSourceFile('jest.config.ts', fileContent, tsModule.ScriptTarget.Latest, true);
const exportStatement = sourceFile.statements.find((statement) => isModuleExport(statement) || isDefaultExport(statement));
let ast;
if (tsModule.isExpressionStatement(exportStatement)) {
const moduleExports = exportStatement.expression;
if (!moduleExports) {
throw new Error(`
The provided jest config file does not have the expected 'module.exports' expression.
See https://jestjs.io/docs/en/configuration for more details.`);
}
ast = moduleExports.right;
}
else if (tsModule.isExportAssignment(exportStatement)) {
const defaultExport = exportStatement.expression;
if (!defaultExport) {
throw new Error(`
The provided jest config file does not have the expected 'export default' expression.
See https://jestjs.io/docs/en/configuration for more details.`);
}
ast = defaultExport;
}
if (!ast) {
throw new Error(`
The provided jest config file does not have the expected 'module.exports' or 'export default' expression.
See https://jestjs.io/docs/en/configuration for more details.`);
}
if (!tsModule.isObjectLiteralExpression(ast)) {
throw new Error(`The 'export default' or 'module.exports' expression is not an object literal.`);
}
return ast;
}
/**
* Returns the jest config object
* @param host
* @param path
*/
function jestConfigObject(host, path) {
const __filename = (0, path_1.join)(host.root, path);
const contents = host.read(path, 'utf-8');
let module = { exports: {} };
// transform the export default syntax to module.exports
// this will work for the default config, but will break if there are any other ts syntax
// TODO(caleb): use the AST to transform back to the module.exports syntax so this will keep working
// or deprecate and make a new method for getting the jest config object
const forcedModuleSyntax = contents.replace(/export\s+default/, 'module.exports =');
// Run the contents of the file with some stuff from this current context
// The module.exports will be mutated by the contents of the file...
(0, vm_1.runInContext)(forcedModuleSyntax, (0, vm_1.createContext)({
module,
require,
process,
__filename,
__dirname: (0, path_1.dirname)(__filename),
}));
// TODO: jest actually allows defining configs with async functions... we should be able to read that...
return module.exports;
}
;