UNPKG

react-broker

Version:

Component lazy-loading + code splitting that works with SSR, Webpack 4, and Babel 7

177 lines (150 loc) 5.26 kB
'use strict' exports.__esModule = true exports.default = void 0 var _path = _interopRequireDefault(require('path')) var _babelPluginMacros = require('babel-plugin-macros') function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : {default: obj} } const pkgName = 'react-broker' var _default = (0, _babelPluginMacros.createMacro)(evaluateMacros) // cycles through each call to the macro and determines an output for each exports.default = _default function evaluateMacros({references, state, babel}) { references.default.forEach(referencePath => makeLegacyEnsure({ state, babel, referencePath, }) ) } // if Broker is defined in the scope then it will use that Broker, otherwise // it requires the module. function makeLegacyEnsure({state, babel, referencePath}) { const brokerTemplate = babel.template.smart( `require('${pkgName}').lazy(CHUNK_NAME, PROMISE, OPTIONS);`, { preserveComments: true, } ) const {promise, chunkName, options} = parseArguments( referencePath.parentPath.get('arguments'), state, babel ) // component options are always in the second argument // let options = referencePath.parentPath.get('arguments')[1] // options = options && options.expression // replaces the macro with the new broker template in the source code referencePath.parentPath.replaceWith( brokerTemplate({ CHUNK_NAME: babel.types.stringLiteral(chunkName), PROMISE: promise, OPTIONS: options, }) ) // this adds webpack magic comment for chunks names // .get('arguments')[1].get('body') is the import() call expression // .get('arguments')[0] is the first argument of the import(), which is the source referencePath.parentPath .get('arguments')[1] .get('body') .get('arguments')[0] .addComment('leading', ` webpackChunkName: "${chunkName}" `) } // relative packages are considered as such when they start with a period '.' const relativePkg = /^\./g // Parses the lazy() macro arguments to determine their type. String literals // are converted to require.ensure code-split imports. Arrow functions, // Identifers, and plain Functions, are all excluded from code-splitting and // are interpreted as-is. function parseArguments(args, state, babel) { const { file: { opts: {filename}, }, } = state const {types: t} = babel let chunkName, source, promise const options = args.length > 1 && args[args.length - 1].type !== 'StringLiteral' && args[args.length - 1].type !== 'TemplateLiteral' ? args[args.length - 1].node : void 0 args = options === void 0 ? args : args.slice(0, -1) for (let arg of args) { let value = '' switch (arg.type) { case 'StringLiteral': // string literals are interpreted as module paths that need to be // imported and code-split // const node = arg.node !== void 0 ? arg.node : arg value = arg.node.value // if the package source isn't relative it is interpreted as-is, // otherwise it is joined to the path of the filename being parsed by // Babel source = value.match(relativePkg) === null ? value : _path.default.join(_path.default.dirname(filename), value) chunkName = chunkNameCache.get(source) // duplicate imports are not allowed // creates a function that returns the import promise // SEE: https://babeljs.io/docs/en/babel-types#callexpression promise = t.arrowFunctionExpression( [], t.callExpression(t.identifier('import'), [t.stringLiteral(source)]) ) break default: throw new Error( `[Broker Error] Unrecognized argument type: ${arg.type}` ) } } return { promise, chunkName, options, } } // shortens the chunk name to its parent directory basename and its basename function getShortChunkName(source) { if (source.match(relativePkg) || _path.default.isAbsolute(source)) { return ( _path.default .dirname(source) .split(_path.default.sep) .slice(-2) .join('/') + '/' + _path.default.basename(source) ) } else { return source } } // This is the chunk name cache which maps sources to their respective // chunk names. This is necessary because you could import the same module // from different paths, but you obviously want to use the same chunk rather // than create two separate chunks in Webpack. class ChunkNameCache { constructor() { this.chunks = {} this.chunkNames = new Set() } get(source) { if (this.chunks[source]) { return this.chunks[source] } let name = getShortChunkName(source) let originalName = name let i = 0 // eslint-disable-next-line no-constant-condition while (true) { if (this.chunkNames.has(name)) { // if this chunk name is already in use by a different source then we // append a unique ID to it name = `${originalName}.${i}` i++ } else { break } } this.chunkNames.add(name) this.chunks[source] = name return name } } const chunkNameCache = new ChunkNameCache()