@magento/pwa-buildpack
Version:
Build/Layout optimization tooling and Peregrine framework adapters for the Magento PWA
217 lines (198 loc) • 7.36 kB
JavaScript
const { inspect } = require('util');
const path = require('path');
const fs = require('fs');
class Operation {
get jsx() {
if (!this.params.jsx) {
return undefined;
}
if (!this._jsx) {
this._jsx = this.parser.parseElement(
this.parser.normalizeElement(this.params.jsx)
);
}
return this._jsx;
}
constructor(request, { parser, file, babel }) {
this.request = request;
this.babel = babel;
const { element, operation, params } = request.options;
this.params = params || {};
this.operation = operation;
this.element = element;
this.parser = parser;
this.file = file;
this.global = this.params.global;
if (typeof element !== 'string') {
throw new Error(
`JSX operation:\n${this}\n is invalid: first argument must be a string which will be used to find matching elements`
);
}
this.matcherText = this.parser.normalizeElement(element);
const matcherAST = this.parser.parseElement(this.matcherText);
this.matcherName = this._getSource(matcherAST.openingElement.name);
this.requiredAttributes = new Map();
for (const { name, value } of matcherAST.openingElement.attributes) {
this.requiredAttributes.set(
this._getSource(name),
this._getSource(value)
);
}
this.state = {};
this.setup(this.request);
}
_getSource(node) {
return this.matcherText.slice(node.start, node.end);
}
_shouldEnterElement(path) {
const tag = path.get('openingElement');
const elementName = tag.get('name').toString();
if (elementName !== this.matcherName) {
return false;
}
const numAttributesPresent = tag.node.attributes.length;
const numAttributesRequired = this.requiredAttributes.size;
if (numAttributesPresent < numAttributesRequired) {
// even if one matches, it won't be enough!
return false;
}
return true;
}
_matchesAttributes(attributePaths) {
const matchMap = new Map(this.requiredAttributes);
for (const attr of attributePaths) {
const attributeName = attr.get('name').toString();
if (!matchMap.has(attributeName)) {
// no requirement for this attribute, ignore
continue;
}
const expected = matchMap.get(attributeName);
const actual = attr.get('value').toString();
if (expected === actual) {
matchMap.delete(attributeName);
continue;
} else {
return false; // explicitly a rejection
}
}
const allRequiredAttributesMatch = matchMap.size === 0;
return allRequiredAttributesMatch;
}
match(path) {
return this.matchElement(path);
}
matchElement(path) {
return (
this._shouldEnterElement(path) &&
this._matchesAttributes(path.get('openingElement.attributes'))
);
}
run() {
throw new Error(
`${
this.constructor.name
} has not implemented a .run(path) method for the operation "${
this.operation
}".`
);
}
setup() {}
toString(indentSpaces = 2) {
const { element, operation, params } = this.request.options;
let args = inspect(element);
if (params) {
args += `, ${inspect(params)}`;
}
if (args.includes('\n')) {
args += '\n';
}
const indent = Array.from({ length: indentSpaces }, () => ' ').join('');
return `${indent}${operation}JSX(${args})`
.split('\n')
.join(`\n${indent}`);
}
/**
* Define a new JSX operation by name and implementation, which can then be requested by name by a transformRequest and executed by the Babel plugin.
*
* @static
* @param {string} opName - Name of the operation
* @param {typeof Operation} OperationType - A subclass of Operation with overrides for the `run` method and optionally the `match` and/or `setup` methods.
* @memberof Operation
*/
static define(opName, OperationType) {
Operation.defined[opName] = OperationType;
}
static fromRequest(request, { parser, file, babel }) {
const { operation } = request.options;
const MatchingOperation = this.defined[operation];
if (MatchingOperation) {
return new MatchingOperation(request, { parser, file, babel });
}
throw new Error(
`Invalid request ${inspect(
request
)}: operation name "${operation}" unrecognized`
);
}
}
// Several operations have such a similar implementation that we're just
// gonna use a tempate class for them.
class SimpleOperation extends Operation {
run(path) {
path[this.operation](this.jsx);
}
}
Operation.defined = {
insertAfter: SimpleOperation,
insertBefore: SimpleOperation,
remove: SimpleOperation
};
/**
* Register a new JSX operation just by creating a new file in this directory
* that exports a run function, or a { setup, match, run } tuple.
*
* The below code will scan this directory for those files, and then define
* operations named after the filenames themselves.
*
*/
const ignore = new Set(['index.js']);
for (const filename of fs.readdirSync(path.join(__dirname, 'operations'))) {
if (!ignore.has(filename)) {
Operation.define(
filename.split('.')[0],
require(`./operations/${filename}`)(Operation)
);
}
}
module.exports = Operation;
/** Type definitions related to: Operation */
/**
* Defines an operation that can be run on the source of React component during build.
*
* @interface OperationDefinition
*
*/
/**
* Run the actual operation, modifying the passed AST in place.
* @function
* @name OperationDefinition#run
* @param {BabelNodePath} path - [Babel `NodePath`](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#paths object to use, representing the current JSX element in the file's syntax tree.
* @returns {undefined}
*/
/**
* Test the current JSX element to see if this operation should run on it.
* Overrides the default implementation of `match`, which matches using the JSX
* name and properties in `this.request.element`. The default implementation is
* can be called at `this.defaultMatch(path)` or `self.defaultMatch()` using
* the second `self` argument.
* @function
* @name OperationDefinition#match
* @param {BabelNodePath} path - [Babel `NodePath`](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#paths object to match, representing the current JSX element in the file's syntax tree.
*/
/**
* Initialize the operation, setting up any custom data structures and reading
* any metadata about the file.
* @function
* @name OperationDefinition#setup
* @param {TransformRequest} request - The TransformRequest passed to this instance. Also available at `this.request`.
*/