common/scripts/babel-conditional-preprocess.js (172 lines of code) (raw):
// This is the babel plugin to strip out the statement/expression following a comment key word
// Used by `rushx preprocess` command in projects under /packages folder
Object.defineProperty(exports, '__esModule', { value: true });
const babelHelper = require('@babel/helper-plugin-utils');
const t = require('@babel/types');
// Note: This uses the non-greedy `*?` so that the first closing `)` finishes the tag.
const CONDITIONAL_FEATURE_RE = /@conditional-compile-remove\(.*?\)/g;
function createFeatureSet(features, inProgressFeatures) {
const featureSet = {}
if(inProgressFeatures !== undefined) {
features.forEach(f => featureSet[`@conditional-compile-remove(${f})`] = inProgressFeatures.includes(f));
} else {
features.forEach(f => featureSet[`@conditional-compile-remove(${f})`] = true);
}
return featureSet;
}
exports.default = babelHelper.declare((_api, opts) => {
const { betaReleaseMode, stable, beta, alpha } = opts;
const features = [...alpha, ...beta, ...stable];
const featureSet = createFeatureSet(features, betaReleaseMode ? alpha : undefined);
const stabilizedFeatureSet = createFeatureSet(stable);
return {
name: 'babel-conditional-preprocess',
// Check types/visitors supported: https://babeljs.io/docs/en/babel-types#typescript
visitor: {
ObjectProperty(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
FunctionDeclaration(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
Statement(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
VariableDeclaration(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
ImportDeclaration(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
ImportSpecifier(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
ExportNamedDeclaration(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
ExportAllDeclaration(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
JSXAttribute(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
TSPropertySignature(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
// TSType is fairly broad, but it is necessary for sanely extending existing types by adding disjuncts or conjucts.
// In other words, support this fairly common situation:
//
// stable build:
// type SomeType = StableTypeA & StableTypeB;
// type AwesomeType = StableTypeA | StableTypeB;
// beta build:
// type SomeType = StableTypeA & StableTypeB & BetaTypeC;
// type AwesomeType = StableTypeA | StableTypeB | BetaTypeC;
//
// As this only applies to TypeScript types, it is safe from a code-flow perspective: This does not enable any new
// conditional business logic flows.
TSType(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
TSDeclareMethod(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
TSMethodSignature(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
Expression(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
ClassMethod(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
ClassProperty(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
Identifier(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
SwitchCase(path) {
Handle(path, featureSet, stabilizedFeatureSet);
},
TSFunctionType(path) {
path.traverse({
Identifier(identifier_path) {
if (path.node.parameters.includes(identifier_path.node)) {
Handle(identifier_path, featureSet, stabilizedFeatureSet);
}
}
});
},
TSConditionalType(path) {
HandleConditionalType(path, featureSet, stabilizedFeatureSet);
},
// If no exports detected in the file after CC, add `export {}` to bypass --isolatedModules check
Program: {
exit(path) {
let hasExport = false;
path.node.body.forEach(node => {
hasExport = hasExport || t.isExportDeclaration(node);
});
if (!hasExport) {
const exportNode = t.exportNamedDeclaration();
t.addComment(exportNode, 'trailing', 'The above line is generated by conditional compilation, when no export detected after CC.');
path.node.body.push(exportNode);
}
}
},
}
};
});
function Handle(path, featureSet, stabilizedFeatureSet, relaceWith = undefined) {
let { node } = path;
// If Node has no comments, it hence has no conditional compilation directives, skip this node.
if (!node.leadingComments) {
return;
}
// If Node has comments, but none are conditional-compile directives, skip this node.
const firstConditionalCompileInstructionIdx = node.leadingComments.findIndex((comment) => nodeRemovalInstruction(node, comment, featureSet, stabilizedFeatureSet) !== 'none');
if (firstConditionalCompileInstructionIdx === -1) {
return;
}
// If Node has conditional-compile directives, but at least one of them is to keep the node, skip this node - but still remove the conditional compile line from the code.
const shouldKeepNode = node.leadingComments.some((comment) => nodeRemovalInstruction(node, comment, featureSet, stabilizedFeatureSet) === 'keep');
if (shouldKeepNode) {
// Strip conditional compile comments only
node.leadingComments.forEach((comment) => {
if (nodeRemovalInstruction(node, comment, featureSet, stabilizedFeatureSet) !== 'none') {
comment.ignore = true;
comment.value = '';
}
});
return;
}
// Node has conditional-compile directives. Remove the first comments starting at compile directive, and also remove all following comments so that
// those comments aren't added to the AST node that follows the removed node.
const firstRemovalInstructionIdx = node.leadingComments.findIndex((comment) => nodeRemovalInstruction(node, comment, featureSet, stabilizedFeatureSet) === 'remove');
if (firstRemovalInstructionIdx === -1) {
return;
}
node.leadingComments.slice(firstRemovalInstructionIdx).forEach((comment) => {
comment.ignore = true;
// Comment is inherited by next line even it is set to 'ignore'.
// Clear the conditional compilation directive to avoid removing the
// next line.
comment.value = '';
})
// We cannot remove Expression in JSXExpressionContainer cause it is not correct for AST
// Replacing it with jSXEmptyExpression will get us the same result
// There will always be only one expression under JSXExpressionContainer
if (path?.container?.type === 'JSXExpressionContainer') {
path.replaceWith(t.jSXEmptyExpression());
} else {
path.remove();
}
}
function HandleConditionalType(path, featureSet, stabilizedFeatureSet) {
let { node } = path;
let { trueType, falseType } = node;
if (trueType.leadingComments) {
const removalInstructions = trueType.leadingComments.map((comment) => nodeRemovalInstruction(trueType, comment, featureSet, stabilizedFeatureSet));
if (shouldRemoveNode(removalInstructions)) {
path.replaceWith(falseType);
}
}
if (falseType.leadingComments) {
const removalInstructions = falseType.leadingComments.map((comment) => nodeRemovalInstruction(falseType, comment, featureSet, stabilizedFeatureSet));
if (shouldRemoveNode(removalInstructions)) {
path.replaceWith(trueType);
}
}
}
function nodeRemovalInstruction(node, comment, featureSet, stabilizedFeatureSet) {
const featuresInComment = comment.value.match(CONDITIONAL_FEATURE_RE);
if (!featuresInComment) {
return 'none';
}
// Check for validity first to catch errors even when valid features exist.
const unknownFeatures = featuresInComment.filter((f) => !(featureSet[f] !== undefined || stabilizedFeatureSet[f]))
if (unknownFeatures.length > 0) {
throw new Error(`Unknown conditional compilation features ${unknownFeatures} in file ${node.loc?.filename} at line ${node.loc?.start?.line}`);
}
// If any of the directives reference a stabilized feature, do not remove the associated node.
// Justification: If a node is needed for more than one features, the first feature that is stabilized needs
// that node in the stable build.
if (featuresInComment.some(f => stabilizedFeatureSet[f] || featureSet[f] === false)) {
return 'keep';
}
return 'remove';
}
function shouldRemoveNode(instructions) {
// If any of the directives reference a stabilized feature, do not remove the associated node.
// Justification: If a node is needed for more than one features, the first feature that is stabilized needs
// that node in the stable build.
if (instructions.includes('keep')) {
return false;
}
return instructions.includes('remove');
}