From e18e37afd8b30455d3a786b9b0dae5f3bd0d442b Mon Sep 17 00:00:00 2001 From: Edward Kimmel Date: Wed, 5 Jul 2023 22:57:59 -0400 Subject: [PATCH] feat: Allow additional identifiers to be passed in to the babel plugin (#966) * Allow additional identifiers to be passed in to the babel plugin * Now allowing an array of name/package entries to be transformed --- .../src/__snapshots__/index.test.js.snap | 300 +++++++++++++++++- packages/babel-plugin/src/index.js | 36 ++- packages/babel-plugin/src/index.test.js | 51 ++- 3 files changed, 364 insertions(+), 23 deletions(-) diff --git a/packages/babel-plugin/src/__snapshots__/index.test.js.snap b/packages/babel-plugin/src/__snapshots__/index.test.js.snap index 64dddf36..cf529522 100644 --- a/packages/babel-plugin/src/__snapshots__/index.test.js.snap +++ b/packages/babel-plugin/src/__snapshots__/index.test.js.snap @@ -586,6 +586,244 @@ loadable({ });" `; +exports[`plugin custom signatures named signature should not match default import 1`] = ` +"import myLoadable from 'myLoadablePackage'; +myLoadable(() => import(\`./ModA\`));" +`; + +exports[`plugin custom signatures should match custom default signature 1`] = ` +"import myLoadable from 'myLoadablePackage'; +myLoadable({ + resolved: {}, + + chunkName() { + return \`ModA\`.replace(/[^a-zA-Z0-9_!§$()=\\\\-^°]+/g, \\"-\\"); + }, + + isReady(props) { + const key = this.resolve(props); + + if (this.resolved[key] !== true) { + return false; + } + + if (typeof __webpack_modules__ !== 'undefined') { + return !!__webpack_modules__[key]; + } + + return false; + }, + + importAsync: () => import( + /* webpackChunkName: \\"ModA\\" */ + \`./ModA\`), + + requireAsync(props) { + const key = this.resolve(props); + this.resolved[key] = false; + return this.importAsync(props).then(resolved => { + this.resolved[key] = true; + return resolved; + }); + }, + + requireSync(props) { + const id = this.resolve(props); + + if (typeof __webpack_require__ !== 'undefined') { + return __webpack_require__(id); + } + + return eval('module.require')(id); + }, + + resolve() { + if (require.resolveWeak) { + return require.resolveWeak(\`./ModA\`); + } + + return eval('require.resolve')(\`./ModA\`); + } + +});" +`; + +exports[`plugin custom signatures should match custom named signature 1`] = ` +"import { myLoadable } from 'myLoadablePackage'; +myLoadable({ + resolved: {}, + + chunkName() { + return \`ModA\`.replace(/[^a-zA-Z0-9_!§$()=\\\\-^°]+/g, \\"-\\"); + }, + + isReady(props) { + const key = this.resolve(props); + + if (this.resolved[key] !== true) { + return false; + } + + if (typeof __webpack_modules__ !== 'undefined') { + return !!__webpack_modules__[key]; + } + + return false; + }, + + importAsync: () => import( + /* webpackChunkName: \\"ModA\\" */ + \`./ModA\`), + + requireAsync(props) { + const key = this.resolve(props); + this.resolved[key] = false; + return this.importAsync(props).then(resolved => { + this.resolved[key] = true; + return resolved; + }); + }, + + requireSync(props) { + const id = this.resolve(props); + + if (typeof __webpack_require__ !== 'undefined') { + return __webpack_require__(id); + } + + return eval('module.require')(id); + }, + + resolve() { + if (require.resolveWeak) { + return require.resolveWeak(\`./ModA\`); + } + + return eval('require.resolve')(\`./ModA\`); + } + +});" +`; + +exports[`plugin custom signatures should match renamed default import 1`] = ` +"import renamedLoadable from '@loadable/component'; +renamedLoadable({ + resolved: {}, + + chunkName() { + return \`ModA\`.replace(/[^a-zA-Z0-9_!§$()=\\\\-^°]+/g, \\"-\\"); + }, + + isReady(props) { + const key = this.resolve(props); + + if (this.resolved[key] !== true) { + return false; + } + + if (typeof __webpack_modules__ !== 'undefined') { + return !!__webpack_modules__[key]; + } + + return false; + }, + + importAsync: () => import( + /* webpackChunkName: \\"ModA\\" */ + \`./ModA\`), + + requireAsync(props) { + const key = this.resolve(props); + this.resolved[key] = false; + return this.importAsync(props).then(resolved => { + this.resolved[key] = true; + return resolved; + }); + }, + + requireSync(props) { + const id = this.resolve(props); + + if (typeof __webpack_require__ !== 'undefined') { + return __webpack_require__(id); + } + + return eval('module.require')(id); + }, + + resolve() { + if (require.resolveWeak) { + return require.resolveWeak(\`./ModA\`); + } + + return eval('require.resolve')(\`./ModA\`); + } + +});" +`; + +exports[`plugin custom signatures should match simple default import 1`] = ` +"import loadable from '@loadable/component'; +loadable({ + resolved: {}, + + chunkName() { + return \`ModA\`.replace(/[^a-zA-Z0-9_!§$()=\\\\-^°]+/g, \\"-\\"); + }, + + isReady(props) { + const key = this.resolve(props); + + if (this.resolved[key] !== true) { + return false; + } + + if (typeof __webpack_modules__ !== 'undefined') { + return !!__webpack_modules__[key]; + } + + return false; + }, + + importAsync: () => import( + /* webpackChunkName: \\"ModA\\" */ + \`./ModA\`), + + requireAsync(props) { + const key = this.resolve(props); + this.resolved[key] = false; + return this.importAsync(props).then(resolved => { + this.resolved[key] = true; + return resolved; + }); + }, + + requireSync(props) { + const id = this.resolve(props); + + if (typeof __webpack_require__ !== 'undefined') { + return __webpack_require__(id); + } + + return eval('module.require')(id); + }, + + resolve() { + if (require.resolveWeak) { + return require.resolveWeak(\`./ModA\`); + } + + return eval('require.resolve')(\`./ModA\`); + } + +});" +`; + +exports[`plugin custom signatures should not match on undeclared specifiers 1`] = ` +"import myLoadable from 'myLoadablePackage'; +myLoadable(() => import(\`./ModA\`));" +`; + exports[`plugin loadable.lib should be transpiled too 1`] = ` "import loadable from '@loadable/component'; loadable.lib({ @@ -700,11 +938,6 @@ loadable({ });" `; -exports[`plugin simple import should not work with renamed specifier by default 1`] = ` -"import renamedLoadable from '@loadable/component'; -renamedLoadable(() => import(\`./ModA\`));" -`; - exports[`plugin simple import should transform path into "chunk-friendly" name 1`] = ` "import loadable from '@loadable/component'; loadable({ @@ -990,6 +1223,63 @@ renamedLazy({ });" `; +exports[`plugin simple import should work with renamed specifier by default 1`] = ` +"import renamedLoadable from '@loadable/component'; +renamedLoadable({ + resolved: {}, + + chunkName() { + return \`ModA\`.replace(/[^a-zA-Z0-9_!§$()=\\\\-^°]+/g, \\"-\\"); + }, + + isReady(props) { + const key = this.resolve(props); + + if (this.resolved[key] !== true) { + return false; + } + + if (typeof __webpack_modules__ !== 'undefined') { + return !!__webpack_modules__[key]; + } + + return false; + }, + + importAsync: () => import( + /* webpackChunkName: \\"ModA\\" */ + \`./ModA\`), + + requireAsync(props) { + const key = this.resolve(props); + this.resolved[key] = false; + return this.importAsync(props).then(resolved => { + this.resolved[key] = true; + return resolved; + }); + }, + + requireSync(props) { + const id = this.resolve(props); + + if (typeof __webpack_require__ !== 'undefined') { + return __webpack_require__(id); + } + + return eval('module.require')(id); + }, + + resolve() { + if (require.resolveWeak) { + return require.resolveWeak(\`./ModA\`); + } + + return eval('require.resolve')(\`./ModA\`); + } + +});" +`; + exports[`plugin simple import should work with template literal 1`] = ` "import loadable from '@loadable/component'; loadable({ diff --git a/packages/babel-plugin/src/index.js b/packages/babel-plugin/src/index.js index 24081f41..4b20b506 100644 --- a/packages/babel-plugin/src/index.js +++ b/packages/babel-plugin/src/index.js @@ -20,7 +20,12 @@ const properties = [ const LOADABLE_COMMENT = '#__LOADABLE__' -const loadablePlugin = declare((api, { defaultImportSpecifier = 'loadable' }) => { +const loadablePlugin = declare((api, { + signatures = [] + }) => { + if (!signatures.find(sig => sig.from == '@loadable/component')) { + signatures.push({name: 'default', from: '@loadable/component'}) + } const { types: t } = api function collectImportCallPaths(startPath) { @@ -35,9 +40,9 @@ const loadablePlugin = declare((api, { defaultImportSpecifier = 'loadable' }) => const propertyFactories = properties.map(init => init(api)) - function isValidIdentifier(path, loadableImportSpecifier, lazyImportSpecifier) { - // `loadable()` - if (loadableImportSpecifier && path.get('callee').isIdentifier({ name: loadableImportSpecifier })) { + function isValidIdentifier(path, loadableImportSpecifiers, lazyImportSpecifier) { + // loadable signatures + if (loadableImportSpecifiers.find(specifier => path.get('callee').isIdentifier({ name: specifier }))) { return true } @@ -48,9 +53,8 @@ const loadablePlugin = declare((api, { defaultImportSpecifier = 'loadable' }) => // `loadable.lib()` return ( - loadableImportSpecifier && path.get('callee').isMemberExpression() && - path.get('callee.object').isIdentifier({ name: loadableImportSpecifier }) && + loadableImportSpecifiers.find(specifier => path.get('callee.object').isIdentifier({ name: specifier })) && path.get('callee.property').isIdentifier({ name: 'lib' }) ) } @@ -119,28 +123,30 @@ const loadablePlugin = declare((api, { defaultImportSpecifier = 'loadable' }) => visitor: { Program: { enter(programPath) { - let loadableImportSpecifier = defaultImportSpecifier let lazyImportSpecifier = false + const loadableSpecifiers = [] programPath.traverse({ ImportDefaultSpecifier(path) { - if (!loadableImportSpecifier) { - const { parent } = path - const { local } = path.node - loadableImportSpecifier = parent.source.value == '@loadable/component' && - local && local.name + const { parent } = path + const { local } = path.node + if (local && signatures.find(signature => signature.name === 'default' && parent.source.value === signature.from)) { + loadableSpecifiers.push(local.name) } }, ImportSpecifier(path) { + const { parent } = path + const { imported, local } = path.node if (!lazyImportSpecifier) { - const { parent } = path - const { imported, local } = path.node lazyImportSpecifier = parent.source.value == '@loadable/component' && imported && imported.name == 'lazy' && local && local.name } + if (local && imported && signatures.find(signature => imported.name === signature.name && parent.source.value === signature.from)) { + loadableSpecifiers.push(local.name) + } }, CallExpression(path) { - if (!isValidIdentifier(path, loadableImportSpecifier, lazyImportSpecifier)) return + if (!isValidIdentifier(path, loadableSpecifiers, lazyImportSpecifier)) return transformImport(path) }, 'ArrowFunctionExpression|FunctionExpression|ObjectMethod': path => { diff --git a/packages/babel-plugin/src/index.test.js b/packages/babel-plugin/src/index.test.js index a1d63bab..cf9aa7d2 100644 --- a/packages/babel-plugin/src/index.test.js +++ b/packages/babel-plugin/src/index.test.js @@ -2,9 +2,9 @@ import { transform } from '@babel/core' import plugin from '.' -const testPlugin = code => { +const testPlugin = (code, options) => { const result = transform(code, { - plugins: [plugin], + plugins: [[plugin, options]], configFile: false, }) @@ -40,7 +40,7 @@ describe('plugin', () => { lazy(() => import(\`./ModA\`));" `) }) - it('should not work with renamed specifier by default', () => { + it('should work with renamed specifier by default', () => { const result = testPlugin(` import renamedLoadable from '@loadable/component' renamedLoadable(() => import(\`./ModA\`)) @@ -202,6 +202,51 @@ describe('plugin', () => { }) }) + describe('custom signatures', () => { + it('should match simple default import', () => { + const result = testPlugin(` + import loadable from '@loadable/component' + loadable(() => import(\`./ModA\`)) + `, { signatures: [{ name: 'default', from: '@loadable/component' }]}) + expect(result).toMatchSnapshot() + }) + it('should match renamed default import', () => { + const result = testPlugin(` + import renamedLoadable from '@loadable/component' + renamedLoadable(() => import(\`./ModA\`)) + `, { signatures: [{ name: 'default', from: '@loadable/component' }]}) + expect(result).toMatchSnapshot() + }) + it('should match custom default signature', () => { + const result = testPlugin(` + import myLoadable from 'myLoadablePackage' + myLoadable(() => import(\`./ModA\`)) + `, { signatures: [{ name: 'default', from: 'myLoadablePackage' }]}) + expect(result).toMatchSnapshot() + }) + it('should match custom named signature', () => { + const result = testPlugin(` + import { myLoadable } from 'myLoadablePackage' + myLoadable(() => import(\`./ModA\`)) + `, { signatures: [{ name: 'myLoadable', from: 'myLoadablePackage' }]}) + expect(result).toMatchSnapshot() + }) + it('named signature should not match default import', () => { + const result = testPlugin(` + import myLoadable from 'myLoadablePackage' + myLoadable(() => import(\`./ModA\`)) + `, { signatures: [{ name: 'myLoadable', from: 'myLoadablePackage' }]}) + expect(result).toMatchSnapshot() + }) + it('should not match on undeclared specifiers', () => { + const result = testPlugin(` + import myLoadable from 'myLoadablePackage' + myLoadable(() => import(\`./ModA\`)) + `) + expect(result).toMatchSnapshot() + }) + }) + describe('Magic comment', () => { it('should transpile shortand properties', () => { const result = testPlugin(`