UNPKG

babel-plugin-jay-universal-import

Version:

Babel plugin to transform import() into its Universal counterpart

276 lines (230 loc) 8.1 kB
'use-strict' const validMagicStrings = [ 'webpackMode', // 'webpackMagicChunkName' gets dealt with current implementation & naming/renaming strategy 'webpackInclude', 'webpackExclude', 'webpackIgnore', 'webpackPreload', 'webpackPrefetch' ] const { addDefault } = require('@babel/helper-module-imports') const path = require('path') const visited = Symbol('visited') const IMPORT_UNIVERSAL_DEFAULT = { id: Symbol('universalImportId'), source: 'babel-plugin-jay-universal-import/universalImport', nameHint: 'universalImport' } const IMPORT_PATH_DEFAULT = { id: Symbol('pathId'), source: 'path', nameHint: 'path' } function getImportArgPath(p) { return p.parentPath.get('arguments')[0] } function trimChunkNameBaseDir(baseDir) { return baseDir.replace(/^[./]+|(\.js$)/g, '') } function prepareChunkNamePath(path) { return path.replace(/\//g, '-') } function getImport(p, { source, nameHint }) { return addDefault(p, source, { nameHint }) } function createTrimmedChunkName(t, importArgNode) { if (importArgNode.quasis) { let quasis = importArgNode.quasis.slice(0) const baseDir = trimChunkNameBaseDir(quasis[0].value.cooked) quasis[0] = Object.assign({}, quasis[0], { value: { raw: baseDir, cooked: baseDir } }) quasis = quasis.map((quasi, i) => (i > 0 ? prepareQuasi(quasi) : quasi)) return Object.assign({}, importArgNode, { quasis }) } const moduleName = trimChunkNameBaseDir(importArgNode.value) return t.stringLiteral(moduleName) } function prepareQuasi(quasi) { const newPath = prepareChunkNamePath(quasi.value.cooked) return Object.assign({}, quasi, { value: { raw: newPath, cooked: newPath } }) } function getMagicWebpackComments(importArgNode) { const { leadingComments } = importArgNode const results = [] if (leadingComments && leadingComments.length) { leadingComments.forEach(comment => { try { const validMagicString = validMagicStrings.filter(str => new RegExp(`${str}\\w*:`).test(comment.value) ) // keep this comment if we found a match if (validMagicString && validMagicString.length === 1) { results.push(comment) } } catch (e) { // eat the error, but don't give up } }) } return results } function getMagicCommentChunkName(importArgNode) { const { quasis, expressions } = importArgNode if (!quasis) return trimChunkNameBaseDir(importArgNode.value) const baseDir = quasis[0].value.cooked const hasExpressions = expressions.length > 0 const chunkName = baseDir + (hasExpressions ? '[request]' : '') return trimChunkNameBaseDir(chunkName) } function getComponentId(t, importArgNode) { const { quasis, expressions } = importArgNode if (!quasis) return importArgNode.value return quasis.reduce((str, quasi, i) => { const q = quasi.value.cooked const id = expressions[i] && expressions[i].name str += id ? `${q}\${${id}}` : q return str }, '') } function existingMagicCommentChunkName(importArgNode) { const { leadingComments } = importArgNode if ( leadingComments && leadingComments.length && leadingComments[0].value.indexOf('webpackChunkName') !== -1 ) { try { return leadingComments[0].value .split('webpackChunkName:')[1] .replace(/["']/g, '') .trim() } catch (e) { return null } } return null } function idOption(t, importArgNode) { const id = getComponentId(t, importArgNode) return t.objectProperty(t.identifier('id'), t.stringLiteral(id)) } function fileOption(t, p) { return t.objectProperty( t.identifier('file'), t.stringLiteral( path.relative(__dirname, p.hub.file.opts.filename || '') || '' ) ) } function loadOption(t, loadTemplate, p, importArgNode) { const argPath = getImportArgPath(p) const generatedChunkName = getMagicCommentChunkName(importArgNode) const otherValidMagicComments = getMagicWebpackComments(importArgNode) const existingChunkName = t.existingChunkName const chunkName = existingChunkName || generatedChunkName delete argPath.node.leadingComments argPath.addComment('leading', ` webpackChunkName: '${chunkName}' `) otherValidMagicComments.forEach(validLeadingComment => argPath.addComment('leading', validLeadingComment.value) ) const load = loadTemplate({ IMPORT: argPath.parent }).expression return t.objectProperty(t.identifier('load'), load) } function pathOption(t, pathTemplate, p, importArgNode) { const path = pathTemplate({ PATH: getImport(p, IMPORT_PATH_DEFAULT), MODULE: importArgNode }).expression return t.objectProperty(t.identifier('path'), path) } function resolveOption(t, resolveTemplate, importArgNode) { const resolve = resolveTemplate({ MODULE: importArgNode }).expression return t.objectProperty(t.identifier('resolve'), resolve) } function chunkNameOption(t, chunkNameTemplate, importArgNode) { const existingChunkName = t.existingChunkName const generatedChunk = createTrimmedChunkName(t, importArgNode) const trimmedChunkName = existingChunkName ? t.stringLiteral(existingChunkName) : generatedChunk const chunkName = chunkNameTemplate({ MODULE: trimmedChunkName }).expression return t.objectProperty(t.identifier('chunkName'), chunkName) } function checkForNestedChunkName(node) { const generatedChunkName = getMagicCommentChunkName(node) const isNested = generatedChunkName.indexOf('[request]') === -1 && generatedChunkName.indexOf('/') > -1 return isNested && prepareChunkNamePath(generatedChunkName) } module.exports = function universalImportPlugin({ types: t, template }) { const chunkNameTemplate = template('() => MODULE') const pathTemplate = template('() => PATH.join(__dirname, MODULE)') const resolveTemplate = template('() => require.resolveWeak(MODULE)') const loadTemplate = template( '() => Promise.all([IMPORT]).then(proms => proms[0])' ) return { name: 'jaybe-universal-import', visitor: { Import(p) { if (p[visited]) return p[visited] = true const importArgNode = getImportArgPath(p).node t.existingChunkName = existingMagicCommentChunkName(importArgNode) // no existing chunkname, no problem - we will reuse that for fixing nested chunk names if (!t.existingChunkName) { t.existingChunkName = checkForNestedChunkName(importArgNode) } const universalImport = getImport(p, IMPORT_UNIVERSAL_DEFAULT) // if being used in an await statement, return load() promise if ( p.parentPath.parentPath.isYieldExpression() || // await transformed already t.isAwaitExpression(p.parentPath.parentPath.node) // await not transformed already ) { const func = t.callExpression(universalImport, [ loadOption(t, loadTemplate, p, importArgNode).value, t.booleanLiteral(false) ]) p.parentPath.replaceWith(func) return } const opts = (this.opts.babelServer ? [ idOption(t, importArgNode), this.opts.includeFileName ? fileOption(t, p) : undefined, pathOption(t, pathTemplate, p, importArgNode), resolveOption(t, resolveTemplate, importArgNode), chunkNameOption(t, chunkNameTemplate, importArgNode) ] : [ idOption(t, importArgNode), this.opts.includeFileName ? fileOption(t, p) : undefined, loadOption(t, loadTemplate, p, importArgNode), // only when not on a babel-server pathOption(t, pathTemplate, p, importArgNode), resolveOption(t, resolveTemplate, importArgNode), chunkNameOption(t, chunkNameTemplate, importArgNode) ] ).filter(Boolean) const options = t.objectExpression(opts) const func = t.callExpression(universalImport, [options]) delete t.existingChunkName p.parentPath.replaceWith(func) } } } }