Skip to content

Commit f7d1561

Browse files
committed
feat: add moduleFederation option
1 parent ede5558 commit f7d1561

File tree

3 files changed

+236
-133
lines changed

3 files changed

+236
-133
lines changed

packages/babel-plugin/src/index.js

Lines changed: 171 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { declare } from "@babel/helper-plugin-utils";
1+
import { declare } from '@babel/helper-plugin-utils'
22
import syntaxDynamicImport from '@babel/plugin-syntax-dynamic-import'
33
import chunkNameProperty from './properties/chunkName'
44
import isReadyProperty from './properties/isReady'
@@ -20,138 +20,194 @@ const properties = [
2020

2121
const LOADABLE_COMMENT = '#__LOADABLE__'
2222

23-
const loadablePlugin = declare((api, { defaultImportSpecifier = 'loadable' }) => {
24-
const { types: t } = api
25-
26-
function collectImportCallPaths(startPath) {
27-
const imports = []
28-
startPath.traverse({
29-
Import(importPath) {
30-
imports.push(importPath.parentPath)
31-
},
32-
})
33-
return imports
34-
}
23+
/**
24+
* The options for the plugin
25+
* @typedef {Object} LoadabelBabelPluginOptions
26+
* @property {string} [defaultImportSpecifier] - The default import specifier, defaults to `loadable`
27+
* @property {boolean} [moduleFederation] - Whether to use module federation
28+
*/
29+
30+
/**
31+
* The options for the plugin
32+
* @typedef {Object} PropertyFactoryOptions
33+
* @property {import('@babel/core').NodePath<import('@babel/types').CallExpression>} callPath
34+
* @property {import('@babel/core').NodePath<import('@babel/types').FunctionExpression | import('@babel/types').ArrowFunctionExpression | import('@babel/types').ObjectMethod>} funcPath
35+
* @property {import('@babel/core').NodePath<import('@babel/types').ObjectExpression>} path
36+
*/
37+
38+
/**
39+
* The factory for a property
40+
* @typedef {Function} PropertyFactory
41+
* @param {PropertyFactoryOptions} options
42+
* @returns {import('@babel/types').ObjectMethod}
43+
*/
44+
45+
const loadablePlugin = declare(
46+
/**
47+
*
48+
* @param {import('@babel/helper-plugin-utils').BabelAPI} api
49+
* @param {LoadabelBabelPluginOptions} babelOptions
50+
* @returns {import('@babel/core').PluginObj}
51+
*/
52+
(api, babelOptions) => {
53+
const { defaultImportSpecifier = 'loadable' } = babelOptions
54+
55+
const { types: t } = api
56+
57+
function collectImportCallPaths(startPath) {
58+
const imports = []
59+
startPath.traverse({
60+
Import(importPath) {
61+
imports.push(importPath.parentPath)
62+
},
63+
})
64+
return imports
65+
}
3566

36-
const propertyFactories = properties.map(init => init(api))
67+
const propertyFactories = properties.map(init => init(api, babelOptions))
3768

38-
function isValidIdentifier(path, loadableImportSpecifier, lazyImportSpecifier) {
39-
// `loadable()`
40-
if (loadableImportSpecifier && path.get('callee').isIdentifier({ name: loadableImportSpecifier })) {
41-
return true
69+
function isValidIdentifier(
70+
path,
71+
loadableImportSpecifier,
72+
lazyImportSpecifier,
73+
) {
74+
// `loadable()`
75+
if (
76+
loadableImportSpecifier &&
77+
path.get('callee').isIdentifier({ name: loadableImportSpecifier })
78+
) {
79+
return true
80+
}
81+
82+
// `lazy()`
83+
if (
84+
lazyImportSpecifier &&
85+
path.get('callee').isIdentifier({ name: lazyImportSpecifier })
86+
) {
87+
return true
88+
}
89+
90+
// `loadable.lib()`
91+
return (
92+
loadableImportSpecifier &&
93+
path.get('callee').isMemberExpression() &&
94+
path
95+
.get('callee.object')
96+
.isIdentifier({ name: loadableImportSpecifier }) &&
97+
path.get('callee.property').isIdentifier({ name: 'lib' })
98+
)
4299
}
43100

44-
// `lazy()`
45-
if (lazyImportSpecifier && path.get('callee').isIdentifier({ name: lazyImportSpecifier })) {
101+
function hasLoadableComment(path) {
102+
const comments = path.get('leadingComments')
103+
const comment = comments.find(
104+
({ node }) =>
105+
node && node.value && String(node.value).includes(LOADABLE_COMMENT),
106+
)
107+
if (!comment) return false
108+
comment.remove()
46109
return true
47110
}
48111

49-
// `loadable.lib()`
50-
return (
51-
loadableImportSpecifier &&
52-
path.get('callee').isMemberExpression() &&
53-
path.get('callee.object').isIdentifier({ name: loadableImportSpecifier }) &&
54-
path.get('callee.property').isIdentifier({ name: 'lib' })
55-
)
56-
}
57-
58-
function hasLoadableComment(path) {
59-
const comments = path.get('leadingComments')
60-
const comment = comments.find(
61-
({ node }) =>
62-
node && node.value && String(node.value).includes(LOADABLE_COMMENT),
63-
)
64-
if (!comment) return false
65-
comment.remove()
66-
return true
67-
}
68-
69-
function getFuncPath(path) {
70-
const funcPath = path.isCallExpression() ? path.get('arguments.0') : path
71-
if (
72-
!funcPath.isFunctionExpression() &&
73-
!funcPath.isArrowFunctionExpression() &&
74-
!funcPath.isObjectMethod()
75-
) {
76-
return null
112+
function getFuncPath(path) {
113+
const funcPath = path.isCallExpression() ? path.get('arguments.0') : path
114+
if (
115+
!funcPath.isFunctionExpression() &&
116+
!funcPath.isArrowFunctionExpression() &&
117+
!funcPath.isObjectMethod()
118+
) {
119+
return null
120+
}
121+
return funcPath
77122
}
78-
return funcPath
79-
}
80-
81-
function transformImport(path) {
82-
const callPaths = collectImportCallPaths(path)
83123

84-
// Ignore loadable function that does not have any "import" call
85-
if (callPaths.length === 0) return
124+
function transformImport(path) {
125+
const callPaths = collectImportCallPaths(path)
86126

87-
// Multiple imports call is not supported
88-
if (callPaths.length > 1) {
89-
throw new Error(
90-
'loadable: multiple import calls inside `loadable()` function are not supported.',
91-
)
92-
}
127+
// Ignore loadable function that does not have any "import" call
128+
if (callPaths.length === 0) return
93129

94-
const [callPath] = callPaths
130+
// Multiple imports call is not supported
131+
if (callPaths.length > 1) {
132+
throw new Error(
133+
'loadable: multiple import calls inside `loadable()` function are not supported.',
134+
)
135+
}
95136

96-
const funcPath = getFuncPath(path)
97-
if (!funcPath) return
137+
const [callPath] = callPaths
98138

99-
funcPath.node.params = funcPath.node.params || []
139+
const funcPath = getFuncPath(path)
140+
if (!funcPath) return
100141

101-
const object = t.objectExpression(
102-
propertyFactories.map(getProperty =>
103-
getProperty({ path, callPath, funcPath }),
104-
),
105-
)
142+
funcPath.node.params = funcPath.node.params || []
106143

107-
if (funcPath.isObjectMethod()) {
108-
funcPath.replaceWith(
109-
t.objectProperty(funcPath.node.key, object, funcPath.node.computed),
144+
const object = t.objectExpression(
145+
propertyFactories.map(getProperty =>
146+
getProperty({ path, callPath, funcPath }),
147+
),
110148
)
111-
} else {
112-
funcPath.replaceWith(object)
149+
150+
if (funcPath.isObjectMethod()) {
151+
funcPath.replaceWith(
152+
t.objectProperty(funcPath.node.key, object, funcPath.node.computed),
153+
)
154+
} else {
155+
funcPath.replaceWith(object)
156+
}
113157
}
114-
}
115-
116-
117-
return {
118-
inherits: syntaxDynamicImport,
119-
visitor: {
120-
Program: {
121-
enter(programPath) {
122-
let loadableImportSpecifier = defaultImportSpecifier
123-
let lazyImportSpecifier = false
124-
125-
programPath.traverse({
126-
ImportDefaultSpecifier(path) {
127-
if (!loadableImportSpecifier) {
128-
const { parent } = path
129-
const { local } = path.node
130-
loadableImportSpecifier = parent.source.value == '@loadable/component' &&
131-
local && local.name
132-
}
133-
},
134-
ImportSpecifier(path) {
135-
if (!lazyImportSpecifier) {
136-
const { parent } = path
137-
const { imported, local } = path.node
138-
lazyImportSpecifier = parent.source.value == '@loadable/component' &&
139-
imported && imported.name == 'lazy' && local && local.name
140-
}
141-
},
142-
CallExpression(path) {
143-
if (!isValidIdentifier(path, loadableImportSpecifier, lazyImportSpecifier)) return
144-
transformImport(path)
145-
},
146-
'ArrowFunctionExpression|FunctionExpression|ObjectMethod': path => {
147-
if (!hasLoadableComment(path)) return
148-
transformImport(path)
149-
},
150-
})
158+
159+
return {
160+
inherits: syntaxDynamicImport,
161+
visitor: {
162+
Program: {
163+
enter(programPath) {
164+
let loadableImportSpecifier = defaultImportSpecifier
165+
let lazyImportSpecifier = false
166+
167+
programPath.traverse({
168+
ImportDefaultSpecifier(path) {
169+
if (!loadableImportSpecifier) {
170+
const { parent } = path
171+
const { local } = path.node
172+
loadableImportSpecifier =
173+
parent.source.value == '@loadable/component' &&
174+
local &&
175+
local.name
176+
}
177+
},
178+
ImportSpecifier(path) {
179+
if (!lazyImportSpecifier) {
180+
const { parent } = path
181+
const { imported, local } = path.node
182+
lazyImportSpecifier =
183+
parent.source.value == '@loadable/component' &&
184+
imported &&
185+
imported.name == 'lazy' &&
186+
local &&
187+
local.name
188+
}
189+
},
190+
CallExpression(path) {
191+
if (
192+
!isValidIdentifier(
193+
path,
194+
loadableImportSpecifier,
195+
lazyImportSpecifier,
196+
)
197+
)
198+
return
199+
transformImport(path)
200+
},
201+
'ArrowFunctionExpression|FunctionExpression|ObjectMethod': path => {
202+
if (!hasLoadableComment(path)) return
203+
transformImport(path)
204+
},
205+
})
206+
},
151207
},
152208
},
153-
},
154-
}
155-
})
209+
}
210+
},
211+
)
156212

157213
export default loadablePlugin

packages/babel-plugin/src/properties/resolve.js

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
11
import { getImportArg } from '../util'
22

3-
export default function resolveProperty({ types: t, template }) {
4-
const buildStatements = template`
5-
if (require.resolveWeak) {
6-
return require.resolveWeak(ID)
7-
}
8-
9-
return eval('require.resolve')(ID)
10-
`
3+
/**
4+
*
5+
* @param {import('@babel/helper-plugin-utils').BabelAPI} api
6+
* @param {import('../index').LoadabelBabelPluginOptions} options
7+
* @returns {import('../index').PropertyFactory}
8+
*/
9+
export default function resolveProperty(
10+
{ types: t, template },
11+
{ moduleFederation },
12+
) {
13+
const templates = {
14+
federated: template`
15+
require(ID)
16+
17+
if (require.resolveWeak) {
18+
return require.resolveWeak(ID)
19+
}
20+
21+
return eval('require.resolve')(ID)
22+
`,
23+
standard: template`
24+
if (require.resolveWeak) {
25+
return require.resolveWeak(ID)
26+
}
27+
28+
return eval('require.resolve')(ID)
29+
`,
30+
}
1131

1232
function getCallValue(callPath) {
1333
const importArg = getImportArg(callPath)
@@ -27,11 +47,38 @@ export default function resolveProperty({ types: t, template }) {
2747
return t.stringLiteral(importArg.node.value)
2848
}
2949

30-
return ({ callPath, funcPath }) =>
31-
t.objectMethod(
50+
/**
51+
* @type {import('../index').PropertyFactory}
52+
*/
53+
const factory = (
54+
/**
55+
* @type {import('../index').PropertyFactoryOptions}
56+
*/
57+
{ callPath, funcPath, path },
58+
) => {
59+
const properties = path.get('arguments.1.properties')
60+
61+
const isFederated =
62+
moduleFederation ||
63+
(Array.isArray(properties) &&
64+
properties.some(
65+
prop =>
66+
prop.isObjectProperty() &&
67+
prop.get('key').isIdentifier({ name: 'federated' }) &&
68+
prop.get('value').isBooleanLiteral({ value: true }),
69+
))
70+
71+
const targetTemplate = isFederated ? 'federated' : 'standard'
72+
73+
return t.objectMethod(
3274
'method',
3375
t.identifier('resolve'),
3476
funcPath.node.params,
35-
t.blockStatement(buildStatements({ ID: getCallValue(callPath) })),
77+
t.blockStatement(
78+
templates[targetTemplate]({ ID: getCallValue(callPath) }),
79+
),
3680
)
81+
}
82+
83+
return factory
3784
}

0 commit comments

Comments
 (0)