UNPKG

@magento/pwa-buildpack

Version:

Build/Layout optimization tooling and Peregrine framework adapters for the Magento PWA

164 lines (153 loc) 5.86 kB
const util = require('util'); const babel = require('@babel/core'); const figures = require('figures'); class SingleImportError extends Error { constructor(statement, details) { const msg = `Bad import statement: ${util.inspect( statement )}. SingleImportStatement must be an ES Module static import statement of the form specified at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import, which imports exactly one binding.`; super(details ? `${msg} \n\nDetails: ${details}` : msg); Error.captureStackTrace(this, SingleImportStatement); } } /** * Represents a static import statement in an ES module. SingleImportStatemnts * are used inside TargetableESModule methods to keep track of the new * dependencies being added to the module, and to resolve conflicts when they * occur. * * The typical way to add new imports to a TargetableESModule is to pass a * static import statement. The import statement can accomplish two things: * * - It's already a familiar syntax * - It contains the module path, the exports of the module to import, and the local binding names for those imports * * That's _almost_ all we need to do the import management we need, including * deduping and scope conflict resolution. */ class SingleImportStatement { /** * @param {string} statement A static import statement */ constructor(statement) { this.originalStatement = statement; this.statement = this._normalizeStatement(statement); this.node = this._parse(); this.binding = this._getBinding(); this.source = this._getSource(); this.imported = this._getImported(); // must come after this._getBinding } /** * Creates a new SingleImportStatement object with a different binding. * * @param {string} newBinding - Binding to rename. * * @returns {SingleImportStatement} A new SingleImportStatement that is a copy * of this one, but with the binding renamed. The `originalStatement` and * `statement` properties are rewritten to use the new binding. */ changeBinding(newBinding) { const { imported, local } = this.node.specifiers[0]; let position = local; let binding = newBinding; const mustAlias = imported && imported.start === local.start; if (mustAlias) { // looks like we're exporting the imported identifier as local, so // amend it to alias to the new binding. // Don't replace any characters; start and end are the same index. position = { start: imported.end, end: imported.end }; binding = ` as ${newBinding}`; } const start = this.statement.slice(0, position.start); const end = this.statement.slice(position.end); return new SingleImportStatement(start + binding + end); } /** * When interpolated as a string, a SingleImportStatement becomes the value * of its `binding` property. * * @returns string */ toString() { return this.binding; } _normalizeStatement(statementArg) { if (typeof statementArg !== 'string') { throw new SingleImportError(statementArg); } let statement = statementArg.trim(); // it feels bad to modify arguments // semicolons because line breaks are no guarantee in a bundler if (!statement.endsWith(';')) { statement += ';'; } // affordance to add "import" so that you can say // `new ImportStatement('X from "x"')` which is less redundant than // `new ImportStatement('import X from "x"')` if (!statement.startsWith('import')) { statement = `import ${statement}`; } return statement + '\n'; } _parse() { let node; try { node = babel.parseSync(this.statement, { filename: 'import-statement.js', sourceType: 'module' }); } catch (e) { let msg = e.message; let indicator = '\n\t'; for (let index = 0; index < e.pos; index++) { indicator += figures.line; } msg += `${indicator}v\n\t${this.statement}`; throw new SingleImportError(this.originalStatement, msg); } try { node = node.program.body[0]; } catch (e) { throw new SingleImportError( this.originalStatement, `Unexpected AST structure: ${util.inspect(node, { depth: 1 })}` ); } if (node.type !== 'ImportDeclaration') { throw new SingleImportError( this.originalStatement, `Node type was ${node.type}` ); } return node; } _getBinding() { const bindings = this.node.specifiers.map(({ local }) => local.name); if (bindings.length !== 1) { throw new SingleImportError( this.originalStatement, `Import ${bindings.length} bindings: ${bindings.join( ', ' )}. Imports for these targets must have exactly one binding, which will be used in generated code.` ); } return bindings[0]; } _getSource() { return this.node.source.value; } _getImported() { const { type, imported } = this.node.specifiers[0]; switch (type) { case 'ImportNamespaceSpecifier': return '*'; case 'ImportDefaultSpecifier': return 'default'; default: return imported.name; } } } module.exports = SingleImportStatement;