diff --git a/packages/babel-plugin/src/__snapshots__/index.test.js.snap b/packages/babel-plugin/src/__snapshots__/index.test.js.snap
index 21b5aa42d..31c6dba59 100644
--- a/packages/babel-plugin/src/__snapshots__/index.test.js.snap
+++ b/packages/babel-plugin/src/__snapshots__/index.test.js.snap
@@ -580,12 +580,12 @@ exports[`plugin loadable.lib should be transpiled too 1`] = `
});"
`;
-exports[`plugin simple import in a complex promise should work 1`] = `
+exports[`plugin simple import should transform path into "chunk-friendly" name 1`] = `
"loadable({
resolved: {},
chunkName() {
- return \\"ModA\\";
+ return \\"foo-bar\\";
},
isReady(props) {
@@ -602,9 +602,9 @@ exports[`plugin simple import in a complex promise should work 1`] = `
return false;
},
- importAsync: () => timeout(import(
- /* webpackChunkName: \\"ModA\\" */
- './ModA'), 2000),
+ importAsync: () => import(
+ /* webpackChunkName: \\"foo-bar\\" */
+ '../foo/bar'),
requireAsync(props) {
const key = this.resolve(props);
@@ -627,21 +627,21 @@ exports[`plugin simple import in a complex promise should work 1`] = `
resolve() {
if (require.resolveWeak) {
- return require.resolveWeak(\\"./ModA\\");
+ return require.resolveWeak(\\"../foo/bar\\");
}
- return eval('require.resolve')(\\"./ModA\\");
+ return eval('require.resolve')(\\"../foo/bar\\");
}
});"
`;
-exports[`plugin simple import should transform path into "chunk-friendly" name 1`] = `
+exports[`plugin simple import should work with * in name 1`] = `
"loadable({
resolved: {},
chunkName() {
- return \\"foo-bar\\";
+ return \`foo\`.replace(/[^a-zA-Z0-9_!§$()=\\\\-^°]+/g, \\"-\\");
},
isReady(props) {
@@ -659,8 +659,8 @@ exports[`plugin simple import should transform path into "chunk-friendly" name 1
},
importAsync: () => import(
- /* webpackChunkName: \\"foo-bar\\" */
- '../foo/bar'),
+ /* webpackChunkName: \\"foo\\" */
+ \`./foo*\`),
requireAsync(props) {
const key = this.resolve(props);
@@ -683,21 +683,21 @@ exports[`plugin simple import should transform path into "chunk-friendly" name 1
resolve() {
if (require.resolveWeak) {
- return require.resolveWeak(\\"../foo/bar\\");
+ return require.resolveWeak(\`./foo*\`);
}
- return eval('require.resolve')(\\"../foo/bar\\");
+ return eval('require.resolve')(\`./foo*\`);
}
});"
`;
-exports[`plugin simple import should work with * in name 1`] = `
+exports[`plugin simple import should work with + concatenation 1`] = `
"loadable({
resolved: {},
chunkName() {
- return \`foo\`.replace(/[^a-zA-Z0-9_!§$()=\\\\-^°]+/g, \\"-\\");
+ return \\"\\";
},
isReady(props) {
@@ -715,8 +715,8 @@ exports[`plugin simple import should work with * in name 1`] = `
},
importAsync: () => import(
- /* webpackChunkName: \\"foo\\" */
- \`./foo*\`),
+ /* webpackChunkName: \\"\\" */
+ './Mod' + 'A'),
requireAsync(props) {
const key = this.resolve(props);
@@ -739,21 +739,21 @@ exports[`plugin simple import should work with * in name 1`] = `
resolve() {
if (require.resolveWeak) {
- return require.resolveWeak(\`./foo*\`);
+ return require.resolveWeak('./Mod' + 'A');
}
- return eval('require.resolve')(\`./foo*\`);
+ return eval('require.resolve')('./Mod' + 'A');
}
});"
`;
-exports[`plugin simple import should work with + concatenation 1`] = `
+exports[`plugin simple import should work with template literal 1`] = `
"loadable({
resolved: {},
chunkName() {
- return \\"\\";
+ return \`ModA\`.replace(/[^a-zA-Z0-9_!§$()=\\\\-^°]+/g, \\"-\\");
},
isReady(props) {
@@ -771,8 +771,8 @@ exports[`plugin simple import should work with + concatenation 1`] = `
},
importAsync: () => import(
- /* webpackChunkName: \\"\\" */
- './Mod' + 'A'),
+ /* webpackChunkName: \\"ModA\\" */
+ \`./ModA\`),
requireAsync(props) {
const key = this.resolve(props);
@@ -795,21 +795,21 @@ exports[`plugin simple import should work with + concatenation 1`] = `
resolve() {
if (require.resolveWeak) {
- return require.resolveWeak('./Mod' + 'A');
+ return require.resolveWeak(\`./ModA\`);
}
- return eval('require.resolve')('./Mod' + 'A');
+ return eval('require.resolve')(\`./ModA\`);
}
});"
`;
-exports[`plugin simple import should work with template literal 1`] = `
+exports[`plugin simple import with "webpackChunkName" comment should use it 1`] = `
"loadable({
resolved: {},
chunkName() {
- return \`ModA\`.replace(/[^a-zA-Z0-9_!§$()=\\\\-^°]+/g, \\"-\\");
+ return \\"ChunkA\\";
},
isReady(props) {
@@ -827,8 +827,8 @@ exports[`plugin simple import should work with template literal 1`] = `
},
importAsync: () => import(
- /* webpackChunkName: \\"ModA\\" */
- \`./ModA\`),
+ /* webpackChunkName: \\"ChunkA\\" */
+ './ModA'),
requireAsync(props) {
const key = this.resolve(props);
@@ -851,16 +851,16 @@ exports[`plugin simple import should work with template literal 1`] = `
resolve() {
if (require.resolveWeak) {
- return require.resolveWeak(\`./ModA\`);
+ return require.resolveWeak(\\"./ModA\\");
}
- return eval('require.resolve')(\`./ModA\`);
+ return eval('require.resolve')(\\"./ModA\\");
}
});"
`;
-exports[`plugin simple import with "webpackChunkName" comment should use it 1`] = `
+exports[`plugin simple import with "webpackChunkName" comment should use it even if comment is separated by "," 1`] = `
"loadable({
resolved: {},
@@ -883,7 +883,7 @@ exports[`plugin simple import with "webpackChunkName" comment should use it 1`]
},
importAsync: () => import(
- /* webpackChunkName: \\"ChunkA\\" */
+ /* webpackPrefetch: true, webpackChunkName: \\"ChunkA\\" */
'./ModA'),
requireAsync(props) {
@@ -916,12 +916,12 @@ exports[`plugin simple import with "webpackChunkName" comment should use it 1`]
});"
`;
-exports[`plugin simple import with "webpackChunkName" comment should use it even if comment is separated by "," 1`] = `
+exports[`plugin simple import with arrow function with body 1`] = `
"loadable({
resolved: {},
chunkName() {
- return \\"ChunkA\\";
+ return \\"ModA\\";
},
isReady(props) {
@@ -938,8 +938,66 @@ exports[`plugin simple import with "webpackChunkName" comment should use it even
return false;
},
- importAsync: () => import(
- /* webpackPrefetch: true, webpackChunkName: \\"ChunkA\\" */
+ importAsync: () => {
+ return 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 with async arrow function 1`] = `
+"loadable({
+ resolved: {},
+
+ chunkName() {
+ return \\"ModA\\";
+ },
+
+ isReady(props) {
+ const key = this.resolve(props);
+
+ if (this.resolved[key] === false) {
+ return false;
+ }
+
+ if (typeof __webpack_modules__ !== 'undefined') {
+ return !!__webpack_modules__[key];
+ }
+
+ return false;
+ },
+
+ importAsync: async () => import(
+ /* webpackChunkName: \\"ModA\\" */
'./ModA'),
requireAsync(props) {
diff --git a/packages/babel-plugin/src/index.js b/packages/babel-plugin/src/index.js
index 04e960f83..cca88903f 100644
--- a/packages/babel-plugin/src/index.js
+++ b/packages/babel-plugin/src/index.js
@@ -32,6 +32,49 @@ const loadablePlugin = api => {
return imports
}
+ function isImportCall(path) {
+ if (path.type === 'CallExpression') {
+ const { callee } = path;
+ if (callee.type === 'Import') {
+ return true;
+ }
+ }
+ return false
+ }
+
+ function isFunctionBodyWhichReturnsImportCall(path) {
+ if (path.type === 'BlockStatement') {
+ const { body: methodBody } = path;
+ if (methodBody.length === 1) {
+ const [statement] = methodBody;
+ if (statement.type === 'ReturnStatement') {
+ const { argument: returnExpression } = statement;
+ if (isImportCall(returnExpression)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ function isFunctionAndOnlyReturnsImport(importCreator) {
+ if (importCreator.type === 'ArrowFunctionExpression') {
+ const { body } = importCreator;
+ if (isImportCall(body) || isFunctionBodyWhichReturnsImportCall(body)) {
+ return true;
+ }
+ }
+
+ if (['ObjectMethod', 'FunctionExpression'].indexOf(importCreator.type) !== -1) {
+ const { body } = importCreator;
+ if (isFunctionBodyWhichReturnsImportCall(body)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
const propertyFactories = properties.map(init => init(api))
function isValidIdentifier(path) {
@@ -72,10 +115,18 @@ const loadablePlugin = api => {
}
function transformImport(path) {
- const callPaths = collectImportCallPaths(path)
+ const importCreator = path.node.type === 'CallExpression'
+ ? path.node.arguments[0] // loadable((...) => import(...)) or loadable.lib
+ : path.node; // /* #__LOADABLE__ */ () => import(...)
- // Ignore loadable function that does not have any "import" call
- if (callPaths.length === 0) return
+ if (!isFunctionAndOnlyReturnsImport(importCreator)) {
+ throw new Error(
+ 'The first argument to `loadable()` must be a function with a single statement that returns a call to `import()`' +
+ 'See https://loadable-components.com/docs/api-loadable-component/#loadfn for more information',
+ );
+ }
+
+ const callPaths = collectImportCallPaths(path)
// Multiple imports call is not supported
if (callPaths.length > 1) {
diff --git a/packages/babel-plugin/src/index.test.js b/packages/babel-plugin/src/index.test.js
index 1bab8750e..538b87c2f 100644
--- a/packages/babel-plugin/src/index.test.js
+++ b/packages/babel-plugin/src/index.test.js
@@ -73,13 +73,39 @@ describe('plugin', () => {
})
})
- describe('in a complex promise', () => {
- it('should work', () => {
- const result = testPlugin(`
+ it('with arrow function with body', () => {
+ const result = testPlugin(`
+ loadable(() => { return import('./ModA') })
+ `)
+
+ expect(result).toMatchSnapshot()
+ })
+
+ it('with async arrow function', () => {
+ const result = testPlugin(`
+ loadable(async () => import('./ModA'))
+ `)
+
+ expect(result).toMatchSnapshot()
+ })
+ })
+
+ describe('reject wrapping of import', () => {
+ describe('in a wrapped promise', () => {
+ it('should fail', () => {
+ expect(() => testPlugin(`
loadable(() => timeout(import('./ModA'), 2000))
- `)
+ `)).toThrow()
+ })
+ })
- expect(result).toMatchSnapshot()
+ describe('returning an await statement', () => {
+ it('should fail', () => {
+ expect(() => testPlugin(`
+ loadable(async () => {
+ return await import('./ModA');
+ })
+ `)).toThrow()
})
})
})
diff --git a/packages/component/src/createLoadable.js b/packages/component/src/createLoadable.js
index 261a09acf..103a32495 100644
--- a/packages/component/src/createLoadable.js
+++ b/packages/component/src/createLoadable.js
@@ -33,6 +33,13 @@ function createLoadable({ resolve = identity, render, onLoad }) {
return null
}
+ function guard(importPromise, props) {
+ if (options.guard) {
+ return options.guard(importPromise.then(() => {}), props)
+ }
+ return importPromise.then(() => {});
+ }
+
class InnerLoadable extends React.Component {
static getDerivedStateFromProps(props, state) {
const cacheKey = getCacheKey(props)
@@ -151,9 +158,10 @@ function createLoadable({ resolve = identity, render, onLoad }) {
loadAsync() {
if (!this.promise) {
const { __chunkExtractor, forwardedRef, ...props } = this.props
- this.promise = ctor
- .requireAsync(props)
- .then(loadedModule => {
+ const importPromise = ctor.requireAsync(props);
+ this.promise = Promise.all([importPromise, guard(importPromise, this.props)])
+ .then((results) => {
+ const loadedModule = results[0];
const result = resolve(loadedModule, { Loadable })
if (options.suspense) {
this.setCache(result)
diff --git a/packages/component/src/loadable.test.js b/packages/component/src/loadable.test.js
index a48083e84..a1115d6e4 100644
--- a/packages/component/src/loadable.test.js
+++ b/packages/component/src/loadable.test.js
@@ -17,6 +17,10 @@ function createLoadFunction() {
return fn
}
+function flushPromises() {
+ return new Promise((resolve) => setImmediate(resolve))
+}
+
class Catch extends React.Component {
state = { error: false }
@@ -67,13 +71,60 @@ describe('#loadable', () => {
expect(container).toHaveTextContent('prop fallback')
})
+ it('allows delaying load with guard', async () => {
+ const load = createLoadFunction()
+ const guard = createLoadFunction()
+ const Component = loadable(load, {
+ fallback: 'fallback',
+ guard,
+ })
+ const { container } = render(