├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── feature.md │ ├── question.md │ └── regression.md ├── PULL_REQUEST_TEMPLATE.md ├── SUPPORT.md ├── opencollective.yml └── stale.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── BACKERS.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── examples ├── .DS_Store ├── client-side │ ├── .eslintrc.json │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── src │ │ ├── A.js │ │ ├── B.js │ │ ├── Hello.js │ │ └── index.js │ └── webpack.config.js ├── razzle │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── robots.txt │ ├── razzle.config.js │ └── src │ │ ├── About │ │ └── index.js │ │ ├── App.css │ │ ├── App.js │ │ ├── Contact │ │ └── index.js │ │ ├── Home │ │ ├── Home.css │ │ ├── Intro.js │ │ ├── Logo.js │ │ ├── Welcome.js │ │ ├── index.js │ │ └── react.svg │ │ ├── NotFound │ │ └── index.js │ │ ├── client.js │ │ ├── index.js │ │ └── server.js ├── server-side-rendering-async-node │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── client │ │ │ ├── App.js │ │ │ ├── Y │ │ │ │ └── file.js │ │ │ ├── letters │ │ │ │ ├── A.css │ │ │ │ ├── A.js │ │ │ │ ├── B.js │ │ │ │ ├── C.js │ │ │ │ ├── D.js │ │ │ │ ├── E.js │ │ │ │ ├── F.js │ │ │ │ ├── G.js │ │ │ │ └── Z │ │ │ │ │ └── file.js │ │ │ ├── main-async-node.js │ │ │ ├── main-web.js │ │ │ └── main.css │ │ └── server │ │ │ └── main.js │ └── webpack.config.babel.js ├── server-side-rendering │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── client │ │ │ ├── App.js │ │ │ ├── Y │ │ │ │ └── file.js │ │ │ ├── letters │ │ │ │ ├── A.css │ │ │ │ ├── A.js │ │ │ │ ├── B.js │ │ │ │ ├── C.js │ │ │ │ ├── D.js │ │ │ │ ├── E.js │ │ │ │ ├── F.js │ │ │ │ ├── G.js │ │ │ │ └── Z │ │ │ │ │ └── file.js │ │ │ ├── main-node.js │ │ │ ├── main-web.js │ │ │ └── main.css │ │ └── server │ │ │ └── main.js │ └── webpack.config.babel.js ├── suspense │ ├── .eslintrc.json │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── src │ │ ├── Hello.js │ │ └── index.js │ └── webpack.config.js ├── typescript │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── client │ │ │ ├── App.tsx │ │ │ ├── Page.tsx │ │ │ ├── PageWithParam.tsx │ │ │ ├── main-node.js │ │ │ └── main-web.js │ │ └── server │ │ │ └── main.js │ ├── tsconfig.json │ └── webpack.config.babel.js └── webpack │ ├── README.md │ ├── webpack4 │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── client │ │ │ ├── App.js │ │ │ ├── letters │ │ │ │ ├── A.css │ │ │ │ ├── A.js │ │ │ │ ├── B.js │ │ │ │ ├── C.js │ │ │ │ └── D.js │ │ │ ├── main-node.js │ │ │ └── main-web.js │ │ └── server │ │ │ └── main.js │ ├── webpack.config.babel.js │ └── yarn.lock │ └── webpack5 │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── nodemon.json │ ├── package.json │ ├── src │ ├── client │ │ ├── App.js │ │ ├── letters │ │ │ ├── A.css │ │ │ ├── A.js │ │ │ ├── B.js │ │ │ ├── C.js │ │ │ └── D.js │ │ ├── main-node.js │ │ └── main-web.js │ └── server │ │ └── main.js │ ├── webpack.config.babel.js │ └── yarn.lock ├── lerna.json ├── netlify.toml ├── package.json ├── packages ├── babel-plugin │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── __snapshots__ │ │ └── index.test.js.snap │ │ ├── index.js │ │ ├── index.test.js │ │ ├── properties │ │ ├── chunkName.js │ │ ├── importAsync.js │ │ ├── isReady.js │ │ ├── requireAsync.js │ │ ├── requireSync.js │ │ ├── resolve.js │ │ └── state.js │ │ └── util.js ├── codemod │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── bin │ │ ├── main.js │ │ └── utils │ │ │ └── CodemodError.js │ ├── package.json │ └── transforms │ │ ├── __testfixtures__ │ │ ├── react-loadable-to-loadable-component_arrow-no-params.input.js │ │ ├── react-loadable-to-loadable-component_arrow-no-params.output.js │ │ ├── react-loadable-to-loadable-component_arrow-w-params.input.js │ │ ├── react-loadable-to-loadable-component_arrow-w-params.output.js │ │ ├── react-loadable-to-loadable-component_expr.input.js │ │ └── react-loadable-to-loadable-component_expr.output.js │ │ ├── __tests__ │ │ └── react-loadable-to-loadable-component-test.js │ │ └── react-loadable-to-loadable-component.js ├── component │ ├── .npmignore │ ├── .size-snapshot.json │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ └── src │ │ ├── Context.js │ │ ├── createLoadable.js │ │ ├── index.js │ │ ├── library.js │ │ ├── loadable.js │ │ ├── loadable.test.js │ │ ├── loadableReady.js │ │ ├── resolvers.js │ │ ├── shared.js │ │ ├── sharedInternals.js │ │ └── util.js ├── server │ ├── .npmignore │ ├── .size-snapshot.json │ ├── CHANGELOG.md │ ├── README.md │ ├── __fixtures__ │ │ └── stats.json │ ├── package.json │ ├── rollup.config.js │ └── src │ │ ├── ChunkExtractor.js │ │ ├── ChunkExtractor.test.js │ │ ├── ChunkExtractorManager.js │ │ ├── index.js │ │ ├── sharedInternals.js │ │ ├── util.js │ │ └── util.test.js └── webpack-plugin │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ └── index.js ├── resources ├── loadable-components.png └── loadable-components.sketch ├── scripts ├── copy-stats-fixture.js ├── git-release.sh └── prepare.sh ├── website ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── README.md ├── _redirects ├── gatsby-config.js ├── gatsby-node.js ├── package.json ├── src │ ├── images │ │ ├── home-logo-dark.png │ │ ├── home-logo.png │ │ ├── logo.png │ │ └── social.jpg │ └── pages │ │ ├── docs │ │ ├── api-loadable-component.mdx │ │ ├── api-loadable-server.mdx │ │ ├── api-loadable-webpack-plugin.mdx │ │ ├── babel-plugin.mdx │ │ ├── code-splitting.mdx │ │ ├── component-splitting.mdx │ │ ├── delay.mdx │ │ ├── dynamic-import.mdx │ │ ├── error-boundaries.mdx │ │ ├── fallback.mdx │ │ ├── faq.mdx │ │ ├── getting-started.mdx │ │ ├── library-splitting.mdx │ │ ├── loadable-vs-react-lazy.mdx │ │ ├── migrate-from-react-loadable.mdx │ │ ├── prefetching.mdx │ │ ├── server-side-rendering.mdx │ │ ├── support.mdx │ │ ├── suspense.mdx │ │ └── timeout.mdx │ │ └── index.js └── yarn.lock └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /coverage/ 3 | dist/ 4 | lib/ 5 | build/ 6 | /website/.cache/ 7 | /website/public/ 8 | __testfixtures__/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "extends": ["airbnb", "prettier"], 5 | "env": { 6 | "jest": true 7 | }, 8 | "rules": { 9 | "react/jsx-one-expression-per-line": "off", 10 | "no-plusplus": "off", 11 | "no-param-reassign": "off", 12 | "no-nested-ternary": "off", 13 | "react/jsx-filename-extension": ["error", { "extensions": [".js"] }], 14 | "react/jsx-wrap-multilines": "off", 15 | "react/no-unused-state": "off", 16 | "react/destructuring-assignment": "off", 17 | "react/prop-types": "off", 18 | "react/sort-comp": "off", 19 | "react/jsx-props-no-spreading": "off", 20 | "react/state-in-constructor": "off", 21 | "import/extensions": "off", 22 | "import/prefer-default-export": "off" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: gregberge 2 | open_collective: loadable 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 👉 [Please follow one of these issue templates](https://github.com/gregberge/loadable-components/issues/new/choose) 👈 2 | 3 | Note: to keep the backlog clean and actionable, issues may be immediately closed if they do not follow one of the above issue templates. 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | ## 🐛 Bug Report 7 | 8 | A clear and concise description of what the bug is. 9 | 10 | ## To Reproduce 11 | 12 | Steps to reproduce the behavior: 13 | 14 | ## Expected behavior 15 | 16 | A clear and concise description of what you expected to happen. 17 | 18 | ## Link to repl or repo (highly encouraged) 19 | 20 | Please provide a minimal repository on GitHub. 21 | 22 | Issues without a reproduction link are likely to stall. 23 | 24 | ## Run `npx envinfo --system --binaries --npmPackages @loadable/component,@loadable/server,@loadable/webpack-plugin,@loadable/babel-plugin --markdown --clipboard` 25 | 26 | Paste the results here: 27 | 28 | ```bash 29 | 30 | ``` 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature Proposal 3 | about: Submit a proposal for a new feature 4 | --- 5 | 6 | ## 🚀 Feature Proposal 7 | 8 | A clear and concise description of what the feature is. 9 | 10 | ## Motivation 11 | 12 | Please outline the motivation for the proposal. 13 | 14 | ## Example 15 | 16 | Please provide an example for how this feature would be used. 17 | 18 | ## Pitch 19 | 20 | Why does this feature belong in the Loadable Component ecosystem? 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💬 Questions / Help 3 | about: If you have questions, please read full readme first 4 | --- 5 | 6 | ## 💬 Questions and Help 7 | 8 | Loadable Components project is young, but please before asking your question: 9 | 10 | - Read carefully the README of the project 11 | - Search if your answer has already been answered in old issues 12 | 13 | After you can submit your question and we will be happy to help you! 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/regression.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💥 Regression Report 3 | about: Report unexpected behavior that worked in previous versions 4 | --- 5 | 6 | ## 💥 Regression Report 7 | 8 | A clear and concise description of what the regression is. 9 | 10 | ## Last working version 11 | 12 | Worked up to version: 13 | 14 | Stopped working in version: 15 | 16 | ## To Reproduce 17 | 18 | Steps to reproduce the behavior: 19 | 20 | ## Expected behavior 21 | 22 | A clear and concise description of what you expected to happen. 23 | 24 | ## Link to repl or repo (highly encouraged) 25 | 26 | Please provide a minimal repository on GitHub. 27 | 28 | Issues without a reproduction link are likely to stall. 29 | 30 | ## Run `npx envinfo --system --binaries --npmPackages @loadable/component,@loadable/server,@loadable/webpack-plugin,@loadable/babel-plugin --markdown --clipboard` 31 | 32 | Paste the results here: 33 | 34 | ```bash 35 | 36 | ``` 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Summary 4 | 5 | 6 | 7 | ## Test plan 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | Please read carefully the README before asking questions. 2 | -------------------------------------------------------------------------------- /.github/opencollective.yml: -------------------------------------------------------------------------------- 1 | collective: loadable 2 | tiers: 3 | - tiers: '*' 4 | labels: ['backer'] 5 | message: | 6 | Hey :wave:, 7 | Thank you so much for supporting us on [Open Collective]() :heart:. 8 | We'll give a special attention to this issue. 9 | 10 | invitation: | 11 | Hey :wave:, 12 | Thank you for opening an issue. We'll get back to you as soon as we can. 13 | Please, consider supporting us on [Open Collective](). We give a special attention to issues opened by backers. 14 | If you use Loadable at work, you can also ask your company to sponsor us :heart:. 15 | 16 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Label to use when marking an issue as stale 6 | staleLabel: wontfix 7 | # Comment to post when marking an issue as stale. Set to `false` to disable 8 | markComment: > 9 | This issue has been automatically marked as stale because it has not had 10 | recent activity. It will be closed if no further activity occurs. Thank you 11 | for your contributions. 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | lib/ 4 | /coverage/ 5 | examples/*/yarn.lock -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/babel.js 3 | !/server.js 4 | !/server.d.ts 5 | !/dist/**/*.js 6 | !/src/**/*.js 7 | !/types/index.d.ts 8 | !/types/tsconfig.json 9 | __fixtures__ 10 | *.test.js 11 | .size-snapshot.json 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /coverage/ 3 | /dist/ 4 | __fixtures__/ 5 | CHANGELOG.md 6 | package.json 7 | lerna.json 8 | /website/.cache/ 9 | /website/public/ 10 | __testfixtures__/ 11 | examples/*/public 12 | **/dist/ 13 | **/lib/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 12 5 | 6 | notifications: 7 | email: false 8 | 9 | cache: 10 | yarn: true 11 | directories: 12 | - '.eslintcache' 13 | - 'node_modules' 14 | 15 | git: 16 | depth: 5 17 | 18 | branches: 19 | only: 20 | - master 21 | - stable 22 | 23 | script: 24 | - yarn ci 25 | - yarn --cwd examples/typescript && yarn --cwd examples/typescript build 26 | - yarn --cwd examples/webpack/webpack4 && yarn --cwd examples/webpack/webpack4 build 27 | - yarn --cwd examples/webpack/webpack5 && yarn --cwd examples/webpack/webpack5 build 28 | 29 | 30 | notifications: 31 | email: false 32 | -------------------------------------------------------------------------------- /BACKERS.md: -------------------------------------------------------------------------------- 1 | # Sponsors & Backers 2 | 3 |

Support Loadable Components development through donations.

4 | 5 | Loadable Components is an MIT-licensed open source project. It is created an maintained by a single person: Greg Bergé. 6 | 7 | - [Sponsor me on GitHub ❤️](https://github.com/sponsors/gregberge). 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project author at hey@gregberge.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | Loadable Components is a small project, it is widely used but has not a lot of contributors. We're still working out the kinks to make contributing to this project as easy and transparent as possible, but we're not quite there yet. Hopefully this document makes the process for contributing clear and answers some questions that you may have. 4 | 5 | ## [Code of Conduct](https://github.com/gregberge/loadable-components/blob/master/CODE_OF_CONDUCT.md) 6 | 7 | We expect project participants to adhere to our Code of Conduct. Please read [the full text](https://github.com/gregberge/loadable-components/blob/master/CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated. 8 | 9 | ## Open Development 10 | 11 | All work on Loadable Components happens directly on [GitHub](/). Both core team members and external contributors send pull requests which go through the same review process. 12 | 13 | ### Workflow and Pull Requests 14 | 15 | _Before_ submitting a pull request, please make sure the following is done… 16 | 17 | 1. Fork the repo and create your branch from `master`. A guide on how to fork a repository: https://help.github.com/articles/fork-a-repo/ 18 | 19 | Open terminal (e.g. Terminal, iTerm, Git Bash or Git Shell) and type: 20 | 21 | ```sh-session 22 | $ git clone https://github.com//loadable-components 23 | $ cd loadable-components 24 | $ git checkout -b my_branch 25 | ``` 26 | 27 | Note: Replace `` with your GitHub username 28 | 29 | 2. Loadable Components uses [Yarn](https://code.fb.com/web/yarn-a-new-package-manager-for-javascript/) for running development scripts. If you haven't already done so, please [install yarn](https://yarnpkg.com/en/docs/install). 30 | 31 | 3. Run `yarn install`. On Windows: To install [Yarn](https://yarnpkg.com/en/docs/install#windows-tab) on Windows you may need to download either node.js or Chocolatey
32 | 33 | ```sh 34 | yarn install 35 | ``` 36 | 37 | To check your version of Yarn and ensure it's installed you can type: 38 | 39 | ```sh 40 | yarn --version 41 | ``` 42 | 43 | 4. Run `yarn build` to bootstrap packages. 44 | 45 | ```sh 46 | yarn build 47 | ``` 48 | 49 | 5. If you've added code that should be tested, add tests. You can use watch mode that continuously transforms changed files to make your life easier. 50 | 51 | ```sh 52 | # in the background 53 | yarn run dev 54 | ``` 55 | 56 | 6. If you've changed APIs, update the documentation. 57 | 58 | 7. Ensure the linting is good via `yarn lint`. 59 | 60 | ```sh-session 61 | $ yarn lint 62 | ``` 63 | 64 | 8. Ensure the test suite passes via `yarn test`. 65 | 66 | ```sh-session 67 | $ yarn test:prepare # build example and generate fixtures before running tests 68 | $ yarn test 69 | ``` 70 | 71 | #### Testing with your own project 72 | 73 | You can use `yarn run release-to-git` to create releases as tags on github. This requires that: 74 | 75 | - Your git remote (where you want to publish the tags) is `origin` 76 | - Your commit messages follow the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. For example: `feat: add timeout option`. 77 | 78 | ## Bugs 79 | 80 | ### Where to Find Known Issues 81 | 82 | We will be using GitHub Issues for our public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new issue, try to make sure your problem doesn't already exist. 83 | 84 | ### Reporting New Issues 85 | 86 | The best way to get your bug fixed is to provide a reduced test case. Please provide a public repository with a runnable example. 87 | 88 | ## Code Conventions 89 | 90 | Please follow the `.prettierrc` in the project. 91 | 92 | ## License 93 | 94 | By contributing to Loadable Components, you agree that your contributions will be licensed under its MIT license. 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Greg Bergé 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | loadable-components 3 |

4 |

React code splitting made easy. Reduce your bundle size without stress ✂️✨.

5 | 6 | [![License](https://img.shields.io/npm/l/@loadable/component.svg)](https://github.com/gregberge/loadable-components/blob/master/LICENSE) 7 | [![npm package](https://img.shields.io/npm/v/@loadable/component/latest.svg)](https://www.npmjs.com/package/@loadable/component) 8 | [![npm downloads](https://img.shields.io/npm/dm/@loadable/component.svg)](https://www.npmjs.com/package/@loadable/component) 9 | [![Build Status](https://img.shields.io/travis/gregberge/loadable-components.svg)](https://travis-ci.org/gregberge/loadable-components) 10 | ![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) 11 | [![Dependencies](https://img.shields.io/david/gregberge/loadable-components.svg?path=packages%2Fcomponent)](https://david-dm.org/gregberge/loadable-components?path=packages/component) 12 | [![DevDependencies](https://img.shields.io/david/dev/gregberge/loadable-components.svg)](https://david-dm.org/gregberge/loadable-components?type=dev) 13 | [![Small size](https://img.badgesize.io/https://unpkg.com/@loadable/component/dist/loadable.min.js?compression=gzip)](https://unpkg.com/@loadable/component/dist/loadable.min.js) 14 | 15 | ```bash 16 | npm install @loadable/component 17 | ``` 18 | 19 | ## [Docs](https://loadable-components.com) 20 | 21 | **See the documentation at [loadable-components.com](https://loadable-components.com)** for more information about using Loadable Components! 22 | 23 | Quicklinks to some of the most-visited pages: 24 | 25 | - [**Getting started**](https://loadable-components.com/docs/getting-started/) 26 | - [Comparison with React.lazy](https://loadable-components.com/docs/loadable-vs-react-lazy/) 27 | - [Server Side Rendering](https://loadable-components.com/docs/server-side-rendering/) 28 | 29 | ## Example 30 | 31 | ```js 32 | import loadable from '@loadable/component' 33 | 34 | const OtherComponent = loadable(() => import('./OtherComponent')) 35 | 36 | function MyComponent() { 37 | return ( 38 |
39 | 40 |
41 | ) 42 | } 43 | ``` 44 | 45 | ## Supporting Loadable Components 46 | 47 | Loadable Components is an MIT-licensed open source project. It's an independent project with ongoing development made possible thanks to the support of these awesome [backers](/BACKERS.md). If you'd like to join them, please consider: 48 | 49 | - [Sponsor me on GitHub ❤️](https://github.com/sponsors/gregberge). 50 | 51 | ## License 52 | 53 | Licensed under the MIT License, Copyright © 2017-present Greg Bergé. 54 | 55 | See [LICENSE](./LICENSE) for more information. 56 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | function getTargets() { 2 | if (process.env.BUILD_TARGET === 'node') { 3 | return { node: '8' } 4 | } 5 | return undefined 6 | } 7 | 8 | function getModules() { 9 | if (process.env.MODULE_TARGET === 'cjs') { 10 | return 'cjs' 11 | } 12 | if (process.env.MODULE_TARGET === 'esm') { 13 | return false 14 | } 15 | return 'auto' 16 | } 17 | 18 | module.exports = { 19 | presets: [ 20 | ['@babel/preset-react', { useBuiltIns: true }], 21 | [ 22 | '@babel/preset-env', 23 | { loose: true, targets: getTargets(), modules: getModules() }, 24 | ], 25 | ], 26 | plugins: ['@babel/plugin-proposal-class-properties'], 27 | } 28 | -------------------------------------------------------------------------------- /examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregberge/loadable-components/18544decdd0e9ecb3e85772708e1f2726430fdeb/examples/.DS_Store -------------------------------------------------------------------------------- /examples/client-side/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "rules": { 6 | "import/no-unresolved": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/client-side/README.md: -------------------------------------------------------------------------------- 1 | # Get the client-side example running 2 | 3 | Steps: 4 | 5 | 1. Download repository 6 | 7 | ```bash 8 | git clone https://github.com/gregberge/loadable-components.git 9 | ``` 10 | 11 | 2. move into example directory 12 | 13 | ```bash 14 | cd ./loadable-components/examples/client-side 15 | ``` 16 | 17 | 3. install [https://yarnpkg.com/lang/en/docs/install](yarn) if haven't already 18 | 4. install project dependencies 19 | 20 | ```bash 21 | yarn 22 | ``` 23 | 24 | 5. run 25 | 26 | ```bash 27 | yarn dev 28 | # or 29 | yarn start 30 | ``` 31 | -------------------------------------------------------------------------------- /examples/client-side/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-react'], 3 | plugins: ['@babel/plugin-syntax-dynamic-import'], 4 | } 5 | -------------------------------------------------------------------------------- /examples/client-side/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "webpack-dev-server", 5 | "dev": "webpack-dev-server" 6 | }, 7 | "devDependencies": { 8 | "@babel/core": "^7.6.4", 9 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 10 | "@babel/preset-react": "^7.6.3", 11 | "babel-loader": "^8.0.4", 12 | "html-webpack-plugin": "^3.2.0", 13 | "webpack": "^4.30.0", 14 | "webpack-cli": "^3.3.2", 15 | "webpack-dev-server": "^3.3.1" 16 | }, 17 | "dependencies": { 18 | "@loadable/component": "^5.10.3", 19 | "moment": "^2.24.0", 20 | "react": "^16.8.6", 21 | "react-dom": "^16.8.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/client-side/src/A.js: -------------------------------------------------------------------------------- 1 | export default () => 'A' 2 | -------------------------------------------------------------------------------- /examples/client-side/src/B.js: -------------------------------------------------------------------------------- 1 | export default () => 'B' 2 | -------------------------------------------------------------------------------- /examples/client-side/src/Hello.js: -------------------------------------------------------------------------------- 1 | export default () => 'Hello' 2 | -------------------------------------------------------------------------------- /examples/client-side/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { render } from 'react-dom' 3 | import loadable from '@loadable/component' 4 | 5 | const Hello = loadable(() => import('./Hello')) 6 | const Dynamic = loadable(p => import(`./${p.name}`), { 7 | cacheKey: p => p.name, 8 | }) 9 | const Moment = loadable.lib(() => import('moment')) 10 | 11 | function App() { 12 | const [name, setName] = useState(null) 13 | 14 | return ( 15 |
16 | 19 | 22 | {name && } 23 | 24 | {({ default: moment }) => moment().format('HH:mm')} 25 |
26 | ) 27 | } 28 | 29 | const root = document.createElement('div') 30 | document.body.append(root) 31 | 32 | render(, root) 33 | -------------------------------------------------------------------------------- /examples/client-side/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | 3 | module.exports = { 4 | mode: 'development', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.js?$/, 9 | exclude: /node_modules/, 10 | use: 'babel-loader', 11 | }, 12 | ], 13 | }, 14 | plugins: [new HtmlWebpackPlugin()], 15 | } 16 | -------------------------------------------------------------------------------- /examples/razzle/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "rules": { 6 | "import/no-unresolved": "off", 7 | "import/no-extraneous-dependencies": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/razzle/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | .DS_Store 5 | 6 | coverage 7 | node_modules 8 | build 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local -------------------------------------------------------------------------------- /examples/razzle/README.md: -------------------------------------------------------------------------------- 1 | # Get the razzle example running 2 | 3 | Steps: 4 | 5 | 1. Download repository 6 | 7 | ```bash 8 | git clone https://github.com/gregberge/loadable-components.git 9 | ``` 10 | 11 | 2. move into example directory 12 | 13 | ```bash 14 | cd ./loadable-components/examples/razzle 15 | ``` 16 | 17 | 3. install [https://yarnpkg.com/lang/en/docs/install](yarn) if haven't already 18 | 4. install project dependencies 19 | 20 | ```bash 21 | yarn 22 | ``` 23 | 24 | 5. run locally or build and serve 25 | 26 | ```bash 27 | yarn dev 28 | # or 29 | yarn build 30 | yarn start:prod 31 | ``` 32 | -------------------------------------------------------------------------------- /examples/razzle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "razzle start", 5 | "build": "razzle build", 6 | "test": "razzle test --env=jsdom", 7 | "start:prod": "NODE_ENV=production node build/server.js" 8 | }, 9 | "dependencies": { 10 | "@babel/runtime": "^7.1.5", 11 | "@loadable/component": "^5.10.2", 12 | "@loadable/server": "^5.10.2", 13 | "@reach/router": "^1.2.1", 14 | "babel-jest": "^24.8.0", 15 | "common-tags": "^1.8.0", 16 | "express": "^4.16.2", 17 | "normalize.css": "^8.0.1", 18 | "razzle": "^3.0.0-alpha.0", 19 | "react": "^16.0.0", 20 | "react-dom": "^16.0.0" 21 | }, 22 | "devDependencies": { 23 | "@babel/template": "^7.4.4", 24 | "@loadable/babel-plugin": "^5.10.0", 25 | "@loadable/webpack-plugin": "^5.7.1", 26 | "check-prop-types": "^1.1.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/razzle/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregberge/loadable-components/18544decdd0e9ecb3e85772708e1f2726430fdeb/examples/razzle/public/favicon.ico -------------------------------------------------------------------------------- /examples/razzle/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | 3 | -------------------------------------------------------------------------------- /examples/razzle/razzle.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-object-spread */ 2 | const path = require('path') 3 | const LoadableWebpackPlugin = require('@loadable/webpack-plugin') 4 | const LoadableBabelPlugin = require('@loadable/babel-plugin') 5 | const babelPresetRazzle = require('razzle/babel') 6 | 7 | module.exports = { 8 | modify: (config, { dev, target }) => { 9 | const appConfig = Object.assign({}, config) 10 | 11 | if (target === 'web') { 12 | const filename = path.resolve(__dirname, 'build') 13 | 14 | appConfig.plugins = [ 15 | ...appConfig.plugins, 16 | new LoadableWebpackPlugin({ 17 | outputAsset: false, 18 | writeToDisk: { filename }, 19 | }), 20 | ] 21 | 22 | appConfig.output.filename = dev 23 | ? 'static/js/[name].js' 24 | : 'static/js/[name].[chunkhash:8].js' 25 | 26 | appConfig.node = { fs: 'empty' } // fix "Cannot find module 'fs'" problem. 27 | 28 | appConfig.optimization = Object.assign({}, appConfig.optimization, { 29 | runtimeChunk: true, 30 | splitChunks: { 31 | chunks: 'all', 32 | name: dev, 33 | }, 34 | }) 35 | } 36 | 37 | return appConfig 38 | }, 39 | 40 | modifyBabelOptions: () => ({ 41 | babelrc: false, 42 | presets: [babelPresetRazzle], 43 | plugins: [LoadableBabelPlugin], 44 | }), 45 | } 46 | -------------------------------------------------------------------------------- /examples/razzle/src/About/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const About = () =>
About page
4 | 5 | export default About 6 | -------------------------------------------------------------------------------- /examples/razzle/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } -------------------------------------------------------------------------------- /examples/razzle/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Router, Link } from '@reach/router' 3 | import loadable from '@loadable/component' 4 | import './App.css' 5 | 6 | const NotFound = loadable(() => import('./NotFound')) 7 | const Home = loadable(() => import('./Home')) 8 | const About = loadable(() => import('./About')) 9 | const Contact = loadable(() => import('./Contact')) 10 | 11 | const App = () => ( 12 | <> 13 | Home 14 | About 15 | Contact 16 | 17 | loading...} /> 18 | loading...} /> 19 | loading...} /> 20 | loading...} /> 21 | 22 | 23 | ) 24 | 25 | export default App 26 | -------------------------------------------------------------------------------- /examples/razzle/src/Contact/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Contact = () =>
Contact page
4 | 5 | export default Contact 6 | -------------------------------------------------------------------------------- /examples/razzle/src/Home/Home.css: -------------------------------------------------------------------------------- 1 | .Home { 2 | text-align: center; 3 | } 4 | 5 | .Home-logo { 6 | animation: Home-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .Home-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .Home-intro { 18 | font-size: large; 19 | } 20 | 21 | .Home-resources { 22 | list-style: none; 23 | } 24 | 25 | .Home-resources > li { 26 | display: inline-block; 27 | padding: 1rem; 28 | } 29 | 30 | @keyframes Home-logo-spin { 31 | from { transform: rotate(0deg); } 32 | to { transform: rotate(360deg); } 33 | } -------------------------------------------------------------------------------- /examples/razzle/src/Home/Intro.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-one-expression-per-line */ 2 | import React from 'react' 3 | 4 | const Intro = () => ( 5 |

6 | To get started, edit src/App.js or src/Home.js and 7 | save to reload. 8 |

9 | ) 10 | 11 | export default Intro 12 | -------------------------------------------------------------------------------- /examples/razzle/src/Home/Logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import logo from './react.svg' 3 | 4 | const Logo = () => logo 5 | 6 | export default Logo 7 | -------------------------------------------------------------------------------- /examples/razzle/src/Home/Welcome.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Welcome = () =>

Welcome to Razzle

4 | 5 | export default Welcome 6 | -------------------------------------------------------------------------------- /examples/razzle/src/Home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import loadable from '@loadable/component' 3 | import './Home.css' 4 | 5 | const Intro = loadable(() => import('./Intro')) 6 | const Welcome = loadable(() => import('./Welcome')) 7 | const Logo = loadable(() => import('./Logo')) 8 | 9 | const Home = () => ( 10 |
11 |
12 | 13 | 14 |
15 | 16 | 27 |
28 | ) 29 | 30 | export default Home 31 | -------------------------------------------------------------------------------- /examples/razzle/src/Home/react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/razzle/src/NotFound/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const NotFound = () =>
Page could not be found
4 | 5 | export default NotFound 6 | -------------------------------------------------------------------------------- /examples/razzle/src/client.js: -------------------------------------------------------------------------------- 1 | import 'normalize.css' 2 | import React from 'react' 3 | import { hydrate } from 'react-dom' 4 | import { loadableReady } from '@loadable/component' 5 | import App from './App' 6 | 7 | const root = document.getElementById('root') 8 | 9 | loadableReady(() => { 10 | hydrate(, root) 11 | }) 12 | 13 | if (module.hot) { 14 | module.hot.accept() 15 | } 16 | -------------------------------------------------------------------------------- /examples/razzle/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, global-require */ 2 | 3 | import http from 'http' 4 | 5 | let app = require('./server').default 6 | 7 | const server = http.createServer(app) 8 | 9 | let currentApp = app 10 | 11 | server.listen(process.env.PORT || 3000, error => { 12 | if (error) { 13 | console.log(error) 14 | } 15 | 16 | console.log('🚀 started') 17 | }) 18 | 19 | if (module.hot) { 20 | console.log('✅ Server-side HMR Enabled!') 21 | 22 | module.hot.accept('./server', () => { 23 | console.log('🔁 HMR Reloading `./server`...') 24 | 25 | try { 26 | app = require('./server').default 27 | server.removeListener('request', currentApp) 28 | server.on('request', app) 29 | currentApp = app 30 | } catch (error) { 31 | console.error(error) 32 | } 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /examples/razzle/src/server.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import React from 'react' 3 | import express from 'express' 4 | import { html as htmlTemplate, oneLineTrim } from 'common-tags' 5 | import { renderToString } from 'react-dom/server' 6 | import { ServerLocation } from '@reach/router' 7 | import { ChunkExtractor, ChunkExtractorManager } from '@loadable/server' 8 | import App from './App' 9 | 10 | const server = express() 11 | server 12 | .disable('x-powered-by') 13 | .use(express.static(process.env.RAZZLE_PUBLIC_DIR)) 14 | .get('/*', (req, res) => { 15 | const extractor = new ChunkExtractor({ 16 | statsFile: path.resolve('build/loadable-stats.json'), 17 | entrypoints: ['client'], 18 | }) 19 | 20 | const html = renderToString( 21 | 22 | 23 | 24 | 25 | , 26 | ) 27 | 28 | res.status(200).send( 29 | oneLineTrim(htmlTemplate` 30 | 31 | 32 | 33 | 34 | 35 | Welcome to Razzle 36 | 37 | ${extractor.getLinkTags()} 38 | ${extractor.getStyleTags()} 39 | 40 | 41 |
${html}
42 | ${extractor.getScriptTags()} 43 | 44 | 45 | `), 46 | ) 47 | }) 48 | 49 | export default server 50 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "rules": { 6 | "import/no-unresolved": "off", 7 | "import/no-extraneous-dependencies": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/.gitignore: -------------------------------------------------------------------------------- 1 | public/dist -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/README.md: -------------------------------------------------------------------------------- 1 | > WORK IN PROGRESS 2 | 3 | # Get the SSR example running 4 | 5 | Steps: 6 | 7 | 1. Download repository 8 | 9 | ```bash 10 | git clone https://github.com/gregberge/loadable-components.git 11 | ``` 12 | 13 | 2. move into example directory 14 | 15 | ```bash 16 | cd ./loadable-components/examples/server-side-rendering-async-node 17 | ``` 18 | 19 | 3. install [https://yarnpkg.com/lang/en/docs/install](yarn) if haven't already 20 | 4. install project dependencies 21 | 22 | ```bash 23 | yarn 24 | ``` 25 | 26 | 5. run locally or build and serve 27 | 28 | ```bash 29 | yarn dev 30 | 31 | # Or 32 | 33 | yarn build 34 | yarn start 35 | ``` 36 | 37 | 🍻 38 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/babel.config.js: -------------------------------------------------------------------------------- 1 | function isWebTarget(caller) { 2 | return Boolean(caller && caller.target === 'web') 3 | } 4 | 5 | function isWebpack(caller) { 6 | return Boolean(caller && caller.name === 'babel-loader') 7 | } 8 | 9 | module.exports = api => { 10 | const web = api.caller(isWebTarget) 11 | const webpack = api.caller(isWebpack) 12 | 13 | return { 14 | presets: [ 15 | '@babel/preset-react', 16 | [ 17 | '@babel/preset-env', 18 | { 19 | useBuiltIns: web ? 'entry' : undefined, 20 | corejs: web ? 'core-js@3' : false, 21 | targets: !web ? { node: 'current' } : undefined, 22 | modules: webpack ? false : 'commonjs', 23 | }, 24 | ], 25 | ], 26 | plugins: ['@babel/plugin-syntax-dynamic-import', '@loadable/babel-plugin'], 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["client", "public"], 3 | "execMap": { 4 | "js": "babel-node" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "nodemon src/server/main.js", 5 | "build": "NODE_ENV=production yarn build:webpack && yarn build:lib", 6 | "build:webpack": "webpack", 7 | "build:lib": "babel -d lib src", 8 | "start": "NODE_ENV=production node lib/server/main.js" 9 | }, 10 | "devDependencies": { 11 | "@babel/cli": "^7.4.4", 12 | "@babel/core": "^7.6.2", 13 | "@babel/node": "^7.0.0", 14 | "@babel/preset-env": "^7.6.2", 15 | "@babel/preset-react": "^7.0.0", 16 | "@loadable/babel-plugin": "^5.10.3", 17 | "@loadable/component": "^5.10.3", 18 | "@loadable/server": "^5.10.3", 19 | "@loadable/webpack-plugin": "^5.7.1", 20 | "babel-loader": "^8.0.6", 21 | "css-loader": "^2.1.1", 22 | "mini-css-extract-plugin": "^0.6.0", 23 | "nodemon": "^1.19.0", 24 | "webpack": "^5.0.0-beta.16", 25 | "webpack-cli": "^3.3.2", 26 | "webpack-dev-middleware": "^3.6.2", 27 | "webpack-node-externals": "^1.7.2" 28 | }, 29 | "dependencies": { 30 | "core-js": "^3.0.1", 31 | "express": "^4.16.4", 32 | "moment": "^2.24.0", 33 | "react": "^16.8.6", 34 | "react-dom": "^16.8.6" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | import loadable from '@loadable/component' 4 | import './main.css' 5 | 6 | const A = loadable(() => import('./letters/A')) 7 | const B = loadable(() => import('./letters/B')) 8 | const C = loadable(() => import(/* webpackPreload: true */ './letters/C')) 9 | const D = loadable(() => import(/* webpackPrefetch: true */ './letters/D')) 10 | const E = loadable(() => import('./letters/E'), { ssr: false }) 11 | const X = loadable(props => import(`./letters/${props.letter}`)) 12 | const Sub = loadable(props => import(`./letters/${props.letter}/file`)) 13 | const RootSub = loadable(props => import(`./${props.letter}/file`)) 14 | 15 | // Load the 'G' component twice: once in SSR and once fully client-side 16 | const GClient = loadable(() => import('./letters/G'), { 17 | ssr: false, 18 | fallback: ssr: false - Loading..., 19 | }) 20 | const GServer = loadable(() => import('./letters/G'), { 21 | ssr: true, 22 | fallback: ssr: true - Loading..., 23 | }) 24 | 25 | // We keep some references to prevent uglify remove 26 | A.C = C 27 | A.D = D 28 | 29 | const Moment = loadable.lib(() => import('moment')) 30 | 31 | const App = () => ( 32 | 53 | ) 54 | 55 | export default App 56 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/Y/file.js: -------------------------------------------------------------------------------- 1 | export default () => 'Y' 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/letters/A.css: -------------------------------------------------------------------------------- 1 | /* A CSS */ 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/letters/A.js: -------------------------------------------------------------------------------- 1 | // We simulate that "moment" is called in "A" and "B" 2 | import moment from 'moment' 3 | import './A.css' 4 | 5 | const A = () => 'A' 6 | 7 | // We keep a reference to prevent uglify remove 8 | A.moment = moment 9 | 10 | export default A 11 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/letters/B.js: -------------------------------------------------------------------------------- 1 | // We simulate that "moment" is called in "A" and "B" 2 | import moment from 'moment' 3 | 4 | const B = () => 'B' 5 | 6 | // We keep a reference to prevent uglify remove 7 | B.moment = moment 8 | 9 | export default B 10 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/letters/C.js: -------------------------------------------------------------------------------- 1 | export default () => 'C' 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/letters/D.js: -------------------------------------------------------------------------------- 1 | export default () => 'D' 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/letters/E.js: -------------------------------------------------------------------------------- 1 | export default () => 'E' 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/letters/F.js: -------------------------------------------------------------------------------- 1 | export default () => 'F' 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/letters/G.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const G = ({ prefix }) => {prefix} - G 4 | 5 | export default G 6 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/letters/Z/file.js: -------------------------------------------------------------------------------- 1 | export default () => 'Z' 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/main-async-node.js: -------------------------------------------------------------------------------- 1 | export { default } from './App' 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/main-web.js: -------------------------------------------------------------------------------- 1 | import 'core-js' 2 | import React from 'react' 3 | import { hydrate } from 'react-dom' 4 | // eslint-disable-next-line import/no-extraneous-dependencies 5 | import { loadableReady } from '@loadable/component' 6 | import App from './App' 7 | 8 | loadableReady(() => { 9 | const root = document.getElementById('main') 10 | hydrate(, root) 11 | }) 12 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/client/main.css: -------------------------------------------------------------------------------- 1 | /* Main CSS */ 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/src/server/main.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import express from 'express' 3 | import React from 'react' 4 | import { renderToString } from 'react-dom/server' 5 | import { ChunkExtractor } from '@loadable/server' 6 | 7 | const app = express() 8 | 9 | app.use(express.static(path.join(__dirname, '../../public'))) 10 | 11 | if (process.env.NODE_ENV !== 'production') { 12 | /* eslint-disable global-require, import/no-extraneous-dependencies */ 13 | const { default: webpackConfig } = require('../../webpack.config.babel') 14 | const webpackDevMiddleware = require('webpack-dev-middleware') 15 | const webpack = require('webpack') 16 | /* eslint-enable global-require, import/no-extraneous-dependencies */ 17 | 18 | const compiler = webpack(webpackConfig) 19 | 20 | app.use( 21 | webpackDevMiddleware(compiler, { 22 | logLevel: 'silent', 23 | publicPath: '/dist/web', 24 | writeToDisk(filePath) { 25 | return /dist\/node\//.test(filePath) || /loadable-stats/.test(filePath) 26 | }, 27 | }), 28 | ) 29 | } 30 | 31 | const nodeStats = path.resolve( 32 | __dirname, 33 | '../../public/dist/async-node/loadable-stats.json', 34 | ) 35 | 36 | const webStats = path.resolve( 37 | __dirname, 38 | '../../public/dist/web/loadable-stats.json', 39 | ) 40 | 41 | app.get('*', (req, res) => { 42 | const nodeExtractor = new ChunkExtractor({ statsFile: nodeStats }) 43 | const { default: App } = nodeExtractor.requireEntrypoint() 44 | 45 | const webExtractor = new ChunkExtractor({ statsFile: webStats }) 46 | const jsx = webExtractor.collectChunks() 47 | 48 | const html = renderToString(jsx) 49 | 50 | res.set('content-type', 'text/html') 51 | res.send(` 52 | 53 | 54 | 55 | ${webExtractor.getLinkTags()} 56 | ${webExtractor.getStyleTags()} 57 | 58 | 59 |
${html}
60 | ${webExtractor.getScriptTags()} 61 | 62 | 63 | `) 64 | }) 65 | 66 | // eslint-disable-next-line no-console 67 | app.listen(9000, () => console.log('Server started http://localhost:9000')) 68 | -------------------------------------------------------------------------------- /examples/server-side-rendering-async-node/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import nodeExternals from 'webpack-node-externals' 3 | import LoadablePlugin from '@loadable/webpack-plugin' 4 | import MiniCssExtractPlugin from 'mini-css-extract-plugin' 5 | 6 | const DIST_PATH = path.resolve(__dirname, 'public/dist') 7 | const production = process.env.NODE_ENV === 'production' 8 | const development = 9 | !process.env.NODE_ENV || process.env.NODE_ENV === 'development' 10 | 11 | const getConfig = target => ({ 12 | name: target, 13 | mode: development ? 'development' : 'production', 14 | target, 15 | entry: `./src/client/main-${target}.js`, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js?$/, 20 | exclude: /node_modules/, 21 | use: { 22 | loader: 'babel-loader', 23 | options: { 24 | caller: { target }, 25 | }, 26 | }, 27 | }, 28 | { 29 | test: /\.css$/, 30 | use: [ 31 | { 32 | loader: MiniCssExtractPlugin.loader, 33 | }, 34 | 'css-loader', 35 | ], 36 | }, 37 | ], 38 | }, 39 | externals: 40 | target === 'async-node' 41 | ? ['@loadable/component', nodeExternals()] 42 | : undefined, 43 | output: { 44 | path: path.join(DIST_PATH, target), 45 | filename: production ? '[name]-bundle-[chunkhash:8].js' : '[name].js', 46 | publicPath: `/dist/${target}/`, 47 | libraryTarget: target === 'async-node' ? 'commonjs2' : undefined, 48 | }, 49 | plugins: [new LoadablePlugin(), new MiniCssExtractPlugin()], 50 | }) 51 | 52 | export default [getConfig('web'), getConfig('async-node')] 53 | -------------------------------------------------------------------------------- /examples/server-side-rendering/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "rules": { 6 | "import/no-unresolved": "off", 7 | "import/no-extraneous-dependencies": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/server-side-rendering/.gitignore: -------------------------------------------------------------------------------- 1 | public/dist -------------------------------------------------------------------------------- /examples/server-side-rendering/README.md: -------------------------------------------------------------------------------- 1 | # Get the SSR example running 2 | 3 | Steps: 4 | 5 | 1. Download repository 6 | 7 | ```bash 8 | git clone https://github.com/gregberge/loadable-components.git 9 | ``` 10 | 11 | 2. Install [https://yarnpkg.com/lang/en/docs/install](yarn) if haven't already 12 | 3. Install libary dependencies and build library 13 | 14 | ```bash 15 | yarn 16 | yarn build 17 | ``` 18 | 19 | 4. Move into example directory 20 | 21 | ```bash 22 | cd ./loadable-components/examples/server-side-rendering 23 | ``` 24 | 25 | 5. Install project dependencies 26 | 27 | ```bash 28 | yarn 29 | ``` 30 | 31 | 5. Run locally or build and serve 32 | 33 | ```bash 34 | yarn dev 35 | 36 | # Or 37 | 38 | yarn build 39 | yarn start 40 | ``` 41 | 42 | 🍻 43 | -------------------------------------------------------------------------------- /examples/server-side-rendering/babel.config.js: -------------------------------------------------------------------------------- 1 | function isWebTarget(caller) { 2 | return Boolean(caller && caller.target === 'web') 3 | } 4 | 5 | function isWebpack(caller) { 6 | return Boolean(caller && caller.name === 'babel-loader') 7 | } 8 | 9 | module.exports = api => { 10 | const web = api.caller(isWebTarget) 11 | const webpack = api.caller(isWebpack) 12 | 13 | return { 14 | presets: [ 15 | '@babel/preset-react', 16 | [ 17 | '@babel/preset-env', 18 | { 19 | useBuiltIns: web ? 'entry' : undefined, 20 | corejs: web ? 'core-js@3' : false, 21 | targets: !web ? { node: 'current' } : undefined, 22 | modules: webpack ? false : 'commonjs', 23 | }, 24 | ], 25 | ], 26 | plugins: ['@babel/plugin-syntax-dynamic-import', '@loadable/babel-plugin'], 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/server-side-rendering/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["client", "public"], 3 | "execMap": { 4 | "js": "babel-node" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/server-side-rendering/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "nodemon src/server/main.js", 5 | "build": "rm -Rf ./public && NODE_ENV=production yarn build:webpack && yarn build:lib", 6 | "build:webpack": "webpack", 7 | "build:lib": "babel -d lib src", 8 | "start": "NODE_ENV=production node lib/server/main.js", 9 | "link:all": "yarn link @loadable/babel-plugin && yarn link @loadable/server && yarn link @loadable/component" 10 | }, 11 | "devDependencies": { 12 | "@babel/cli": "^7.4.4", 13 | "@babel/core": "^7.6.2", 14 | "@babel/node": "^7.0.0", 15 | "@babel/preset-env": "^7.6.2", 16 | "@babel/preset-react": "^7.0.0", 17 | "@loadable/babel-plugin": "file:./../../packages/babel-plugin", 18 | "@loadable/component": "file:./../../packages/component", 19 | "@loadable/server": "file:./../../packages/server", 20 | "@loadable/webpack-plugin": "file:./../../packages/webpack-plugin", 21 | "babel-loader": "^8.0.6", 22 | "css-loader": "^2.1.1", 23 | "mini-css-extract-plugin": "^0.6.0", 24 | "nodemon": "^1.19.0", 25 | "webpack": "^4.31.0", 26 | "webpack-cli": "^3.3.2", 27 | "webpack-dev-middleware": "^3.6.2", 28 | "webpack-node-externals": "^1.7.2" 29 | }, 30 | "dependencies": { 31 | "core-js": "^3.0.1", 32 | "express": "^4.16.4", 33 | "moment": "^2.24.0", 34 | "react": "^16.8.6", 35 | "react-dom": "^16.8.6" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | import loadable from '@loadable/component' 4 | import './main.css' 5 | 6 | const A = loadable(() => import('./letters/A')) 7 | const B = loadable(() => import('./letters/B')) 8 | const C = loadable(() => import(/* webpackPreload: true */ './letters/C')) 9 | const D = loadable(() => import(/* webpackPrefetch: true */ './letters/D')) 10 | const E = loadable(() => import('./letters/E?param'), { ssr: false }) 11 | const X = loadable(props => import(`./letters/${props.letter}`)) 12 | const Sub = loadable(props => import(`./letters/${props.letter}/file`)) 13 | const RootSub = loadable(props => import(`./${props.letter}/file`)) 14 | 15 | // Load the 'G' component twice: once in SSR and once fully client-side 16 | const GClient = loadable(() => import('./letters/G'), { 17 | ssr: false, 18 | fallback: ssr: false - Loading..., 19 | }) 20 | const GServer = loadable(() => import('./letters/G'), { 21 | ssr: true, 22 | fallback: ssr: true - Loading..., 23 | }) 24 | 25 | // We keep some references to prevent uglify remove 26 | A.C = C 27 | A.D = D 28 | 29 | const Moment = loadable.lib(() => import('moment'), { 30 | resolveComponent: moment => moment.default || moment, 31 | }) 32 | 33 | const App = () => ( 34 |
55 | ) 56 | 57 | export default App 58 | -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/Y/file.js: -------------------------------------------------------------------------------- 1 | export default () => 'Y' 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/letters/A.css: -------------------------------------------------------------------------------- 1 | /* A CSS */ 2 | body { 3 | background: pink; 4 | } -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/letters/A.js: -------------------------------------------------------------------------------- 1 | // We simulate that "moment" is called in "A" and "B" 2 | import moment from 'moment' 3 | import './A.css' 4 | 5 | const A = () => 'A' 6 | 7 | // We keep a reference to prevent uglify remove 8 | A.moment = moment 9 | 10 | export default A 11 | -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/letters/B.js: -------------------------------------------------------------------------------- 1 | // We simulate that "moment" is called in "A" and "B" 2 | import moment from 'moment' 3 | 4 | const B = () => 'B' 5 | 6 | // We keep a reference to prevent uglify remove 7 | B.moment = moment 8 | 9 | export default B 10 | -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/letters/C.js: -------------------------------------------------------------------------------- 1 | export default () => 'C' 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/letters/D.js: -------------------------------------------------------------------------------- 1 | export default () => 'D' 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/letters/E.js: -------------------------------------------------------------------------------- 1 | export default () => 'E' 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/letters/F.js: -------------------------------------------------------------------------------- 1 | export default () => 'F' 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/letters/G.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const G = ({ prefix }) => {prefix} - G 4 | 5 | export default G 6 | -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/letters/Z/file.js: -------------------------------------------------------------------------------- 1 | export default () => 'Z' 2 | -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/main-node.js: -------------------------------------------------------------------------------- 1 | export { default } from './App' 2 | 3 | export const hello = 'hello' 4 | -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/main-web.js: -------------------------------------------------------------------------------- 1 | import 'core-js' 2 | import React from 'react' 3 | import { hydrate } from 'react-dom' 4 | // eslint-disable-next-line import/no-extraneous-dependencies 5 | import { loadableReady } from '@loadable/component' 6 | import App from './App' 7 | 8 | loadableReady(() => { 9 | const root = document.getElementById('main') 10 | hydrate(, root) 11 | }) 12 | -------------------------------------------------------------------------------- /examples/server-side-rendering/src/client/main.css: -------------------------------------------------------------------------------- 1 | /* Main CSS */ 2 | h1 { 3 | color: cyan; 4 | } -------------------------------------------------------------------------------- /examples/server-side-rendering/src/server/main.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import express from 'express' 3 | import React from 'react' 4 | import { renderToString } from 'react-dom/server' 5 | import { ChunkExtractor } from '@loadable/server' 6 | 7 | const app = express() 8 | 9 | app.use(express.static(path.join(__dirname, '../../public'))) 10 | 11 | if (process.env.NODE_ENV !== 'production') { 12 | /* eslint-disable global-require, import/no-extraneous-dependencies */ 13 | const { default: webpackConfig } = require('../../webpack.config.babel') 14 | const webpackDevMiddleware = require('webpack-dev-middleware') 15 | const webpack = require('webpack') 16 | /* eslint-enable global-require, import/no-extraneous-dependencies */ 17 | 18 | const compiler = webpack(webpackConfig) 19 | 20 | app.use( 21 | webpackDevMiddleware(compiler, { 22 | logLevel: 'silent', 23 | publicPath: '/dist/web', 24 | writeToDisk(filePath) { 25 | return /dist\/node\//.test(filePath) || /loadable-stats/.test(filePath) 26 | }, 27 | }), 28 | ) 29 | } 30 | 31 | const nodeStats = path.resolve( 32 | __dirname, 33 | '../../public/dist/node/loadable-stats.json', 34 | ) 35 | 36 | const webStats = path.resolve( 37 | __dirname, 38 | '../../public/dist/web/loadable-stats.json', 39 | ) 40 | 41 | app.get('*', (req, res) => { 42 | const nodeExtractor = new ChunkExtractor({ statsFile: nodeStats }) 43 | const { default: App } = nodeExtractor.requireEntrypoint() 44 | 45 | const webExtractor = new ChunkExtractor({ statsFile: webStats }) 46 | const jsx = webExtractor.collectChunks() 47 | 48 | const html = renderToString(jsx) 49 | 50 | res.set('content-type', 'text/html') 51 | res.send(` 52 | 53 | 54 | 55 | ${webExtractor.getLinkTags()} 56 | ${webExtractor.getStyleTags()} 57 | 58 | 59 |
${html}
60 | ${webExtractor.getScriptTags()} 61 | 62 | 63 | `) 64 | }) 65 | 66 | // eslint-disable-next-line no-console 67 | app.listen(9000, () => console.log('Server started http://localhost:9000')) 68 | -------------------------------------------------------------------------------- /examples/server-side-rendering/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import nodeExternals from 'webpack-node-externals' 3 | import LoadablePlugin from '@loadable/webpack-plugin' 4 | import MiniCssExtractPlugin from 'mini-css-extract-plugin' 5 | 6 | const DIST_PATH = path.resolve(__dirname, 'public/dist') 7 | const production = process.env.NODE_ENV === 'production' 8 | const development = 9 | !process.env.NODE_ENV || process.env.NODE_ENV === 'development' 10 | 11 | const getConfig = target => ({ 12 | name: target, 13 | mode: development ? 'development' : 'production', 14 | target, 15 | entry: `./src/client/main-${target}.js`, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js?$/, 20 | exclude: /node_modules/, 21 | use: { 22 | loader: 'babel-loader', 23 | options: { 24 | caller: { target }, 25 | }, 26 | }, 27 | }, 28 | { 29 | test: /\.css$/, 30 | use: [ 31 | { 32 | loader: MiniCssExtractPlugin.loader, 33 | }, 34 | 'css-loader', 35 | ], 36 | }, 37 | ], 38 | }, 39 | optimization: { 40 | moduleIds: 'named', 41 | chunkIds: 'named', 42 | }, 43 | externals: 44 | target === 'node' ? ['@loadable/component', nodeExternals()] : undefined, 45 | output: { 46 | path: path.join(DIST_PATH, target), 47 | // filename: production ? '[name]-bundle-[chunkhash:8].js' : '[name].js', 48 | filename: production ? '[name].js' : '[name].js', 49 | publicPath: `/dist/${target}/`, 50 | libraryTarget: target === 'node' ? 'commonjs2' : undefined, 51 | }, 52 | plugins: [new LoadablePlugin(), new MiniCssExtractPlugin()], 53 | }) 54 | 55 | export default [getConfig('web'), getConfig('node')] 56 | -------------------------------------------------------------------------------- /examples/suspense/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "rules": { 6 | "import/no-unresolved": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/suspense/README.md: -------------------------------------------------------------------------------- 1 | # Get the suspense example running 2 | 3 | Steps: 4 | 5 | 1. Download repository 6 | 7 | ```bash 8 | git clone https://github.com/gregberge/loadable-components.git 9 | ``` 10 | 11 | 2. move into example directory 12 | 13 | ```bash 14 | cd ./loadable-components/examples/suspense 15 | ``` 16 | 17 | 3. install [https://yarnpkg.com/lang/en/docs/install](yarn) if haven't already 18 | 4. install project dependencies 19 | 20 | ```bash 21 | yarn 22 | ``` 23 | 24 | 5. run 25 | 26 | ```bash 27 | yarn dev 28 | # or 29 | yarn start 30 | ``` 31 | -------------------------------------------------------------------------------- /examples/suspense/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-react'], 3 | plugins: ['@babel/plugin-syntax-dynamic-import'], 4 | } 5 | -------------------------------------------------------------------------------- /examples/suspense/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "webpack-dev-server", 5 | "dev": "webpack-dev-server" 6 | }, 7 | "devDependencies": { 8 | "@babel/core": "^7.6.4", 9 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 10 | "@babel/preset-react": "^7.6.3", 11 | "babel-loader": "^8.0.4", 12 | "html-webpack-plugin": "^3.2.0", 13 | "webpack": "^4.29.5", 14 | "webpack-cli": "^3.2.3", 15 | "webpack-dev-server": "^3.2.0" 16 | }, 17 | "dependencies": { 18 | "@loadable/component": "^5.10.3", 19 | "moment": "^2.24.0", 20 | "react": "^16.8.3", 21 | "react-dom": "^16.8.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/suspense/src/Hello.js: -------------------------------------------------------------------------------- 1 | export default () => 'Hello' 2 | -------------------------------------------------------------------------------- /examples/suspense/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | import { render } from 'react-dom' 3 | import { lazy } from '@loadable/component' 4 | 5 | const Hello = lazy(() => import('./Hello')) 6 | const Moment = lazy.lib(() => import('moment')) 7 | 8 | const App = () => ( 9 |
10 | Loading...
}> 11 | 12 | {({ default: moment }) => moment().format('HH:mm')} 13 | 14 | 15 | ) 16 | 17 | const root = document.createElement('div') 18 | document.body.append(root) 19 | 20 | render(, root) 21 | -------------------------------------------------------------------------------- /examples/suspense/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | 3 | module.exports = { 4 | mode: 'development', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.js?$/, 9 | exclude: /node_modules/, 10 | use: 'babel-loader', 11 | }, 12 | ], 13 | }, 14 | plugins: [new HtmlWebpackPlugin()], 15 | } 16 | -------------------------------------------------------------------------------- /examples/typescript/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "rules": { 6 | "import/no-unresolved": "off", 7 | "import/no-extraneous-dependencies": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/typescript/.gitignore: -------------------------------------------------------------------------------- 1 | public/dist -------------------------------------------------------------------------------- /examples/typescript/README.md: -------------------------------------------------------------------------------- 1 | # Get the SSR example running 2 | 3 | Steps: 4 | 5 | 1. Download repository 6 | 7 | ```bash 8 | git clone https://github.com/gregberge/loadable-components.git 9 | ``` 10 | 11 | 2. move into example directory 12 | 13 | ```bash 14 | cd ./loadable-components/examples/server-side-rendering 15 | ``` 16 | 17 | 3. install [https://yarnpkg.com/lang/en/docs/install](yarn) if haven't already 18 | 4. install project dependencies 19 | 20 | ```bash 21 | yarn 22 | ``` 23 | 24 | 5. run locally or build and serve 25 | 26 | ```bash 27 | yarn dev 28 | 29 | # Or 30 | 31 | yarn build 32 | yarn start 33 | ``` 34 | 35 | 🍻 36 | -------------------------------------------------------------------------------- /examples/typescript/babel.config.js: -------------------------------------------------------------------------------- 1 | function isWebTarget(caller) { 2 | return Boolean(caller && caller.target === 'web') 3 | } 4 | 5 | function isWebpack(caller) { 6 | return Boolean(caller && caller.name === 'babel-loader') 7 | } 8 | 9 | module.exports = api => { 10 | const web = api.caller(isWebTarget) 11 | const webpack = api.caller(isWebpack) 12 | 13 | return { 14 | presets: [ 15 | '@babel/preset-typescript', 16 | '@babel/preset-react', 17 | [ 18 | '@babel/preset-env', 19 | { 20 | useBuiltIns: web ? 'entry' : undefined, 21 | corejs: web ? 'core-js@3' : false, 22 | targets: !web ? { node: 'current' } : undefined, 23 | modules: webpack ? false : 'commonjs', 24 | }, 25 | ], 26 | ], 27 | plugins: ['@babel/plugin-syntax-dynamic-import', '@loadable/babel-plugin'], 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/typescript/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["client", "public"], 3 | "execMap": { 4 | "js": "babel-node" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "nodemon src/server/main.js", 5 | "build": "NODE_ENV=production yarn build:webpack && yarn build:lib", 6 | "build:dev": "NODE_ENV=development yarn build:webpack && yarn build:lib", 7 | "build:webpack": "webpack", 8 | "build:lib": "babel -d lib src", 9 | "start": "NODE_ENV=production node lib/server/main.js", 10 | "start:dev": "NODE_ENV=development node -r @babel/register lib/server/main.js" 11 | }, 12 | "devDependencies": { 13 | "@babel/cli": "^7.4.4", 14 | "@babel/core": "^7.6.2", 15 | "@babel/node": "^7.0.0", 16 | "@babel/preset-env": "^7.6.2", 17 | "@babel/preset-react": "^7.0.0", 18 | "@babel/preset-typescript": "^7.12.1", 19 | "@types/loadable__component": "^5.13.1", 20 | "@types/react": "^16.9.53", 21 | "@types/react-router-dom": "^5.1.6", 22 | "babel-loader": "^8.0.6", 23 | "css-loader": "^2.1.1", 24 | "mini-css-extract-plugin": "^0.6.0", 25 | "nodemon": "^1.19.0", 26 | "react-router": "^5.2.0", 27 | "react-router-dom": "^5.2.0", 28 | "typescript": "^4.0.3", 29 | "webpack": "^4.31.0", 30 | "webpack-cli": "^3.3.2", 31 | "webpack-dev-middleware": "^3.6.2", 32 | "webpack-node-externals": "^1.7.2" 33 | }, 34 | "dependencies": { 35 | "@babel/register": "^7.12.1", 36 | "@loadable/babel-plugin": "file:./../../packages/babel-plugin", 37 | "@loadable/component": "file:./../../packages/component", 38 | "@loadable/server": "file:./../../packages/server", 39 | "@loadable/webpack-plugin": "file:./../../packages/webpack-plugin", 40 | "core-js": "^3.0.1", 41 | "express": "^4.16.4", 42 | "moment": "^2.24.0", 43 | "react": "^16.8.6", 44 | "react-dom": "^16.8.6" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/typescript/src/client/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import loadable from '@loadable/component' 3 | import {StaticRouter, Switch, Route} from 'react-router-dom'; 4 | 5 | const X = loadable(() => import(`./Page`)) 6 | const Y = loadable(() => import(`./PageWithParam`)) 7 | 8 | const App = () => ( 9 |
10 |

11 | Components loaded directly: 12 | 13 | 14 |

15 |
16 |
17 | 18 | 19 | 20 | 21 | 24 | // @ts-expect-error routeProperties do not exists on X - it accepts no props at all 25 | 26 | } 27 | /> 28 | 29 |
30 | 31 | 32 | 33 |
34 |
35 |
36 | ) 37 | 38 | export default App 39 | -------------------------------------------------------------------------------- /examples/typescript/src/client/Page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const Page:React.FC<{}> = () => (I am lazy page); 4 | 5 | export default Page; -------------------------------------------------------------------------------- /examples/typescript/src/client/PageWithParam.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const PageWithParam:React.FC<{x: number}> = ({x}) => (I am lazy page2 -{x||"missed prop"}-); 4 | 5 | export default PageWithParam; -------------------------------------------------------------------------------- /examples/typescript/src/client/main-node.js: -------------------------------------------------------------------------------- 1 | export { default } from './App.tsx' 2 | -------------------------------------------------------------------------------- /examples/typescript/src/client/main-web.js: -------------------------------------------------------------------------------- 1 | import 'core-js' 2 | import React from 'react' 3 | import { hydrate } from 'react-dom' 4 | import { loadableReady } from '@loadable/component' 5 | import App from './App.tsx' 6 | 7 | console.log('waiting for application ready...') 8 | loadableReady(() => { 9 | console.log('application is ready...') 10 | const root = document.getElementById('main') 11 | hydrate(, root) 12 | }) 13 | -------------------------------------------------------------------------------- /examples/typescript/src/server/main.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import express from 'express' 3 | import React from 'react' 4 | import { renderToString } from 'react-dom/server' 5 | import { ChunkExtractor } from '@loadable/server' 6 | 7 | const app = express() 8 | 9 | // https://github.com/gregberge/loadable-components/issues/634 10 | // app.use('*/runtime~main.js', async (req, res, next) => { 11 | // console.log('delaying runtime chunk'); 12 | // await new Promise(resolve => setTimeout(resolve, 2000)); 13 | // next(); 14 | // }); 15 | 16 | app.use(express.static(path.join(__dirname, '../../public'))) 17 | 18 | if (process.env.NODE_ENV !== 'production') { 19 | /* eslint-disable global-require, import/no-extraneous-dependencies */ 20 | const { default: webpackConfig } = require('../../webpack.config.babel') 21 | const webpackDevMiddleware = require('webpack-dev-middleware') 22 | const webpack = require('webpack') 23 | /* eslint-enable global-require, import/no-extraneous-dependencies */ 24 | 25 | const compiler = webpack(webpackConfig) 26 | 27 | app.use( 28 | webpackDevMiddleware(compiler, { 29 | logLevel: 'silent', 30 | publicPath: '/dist/web', 31 | writeToDisk(filePath) { 32 | return /dist\/node\//.test(filePath) || /loadable-stats/.test(filePath) 33 | }, 34 | }), 35 | ) 36 | } 37 | 38 | const nodeStats = path.resolve( 39 | __dirname, 40 | '../../public/dist/node/loadable-stats.json', 41 | ) 42 | 43 | const webStats = path.resolve( 44 | __dirname, 45 | '../../public/dist/web/loadable-stats.json', 46 | ) 47 | 48 | app.get('*', (req, res) => { 49 | const nodeExtractor = new ChunkExtractor({ statsFile: nodeStats }) 50 | const { default: App } = nodeExtractor.requireEntrypoint() 51 | 52 | const webExtractor = new ChunkExtractor({ statsFile: webStats }) 53 | const jsx = webExtractor.collectChunks() 54 | 55 | const html = renderToString(jsx) 56 | 57 | res.set('content-type', 'text/html') 58 | res.send(` 59 | 60 | 61 | 62 | ${webExtractor.getLinkTags()} 63 | ${webExtractor.getStyleTags()} 64 | 65 | 66 |
${html}
67 | 68 | ${webExtractor.getScriptTags()} 69 | 70 | 71 | 72 | `) 73 | }) 74 | 75 | // eslint-disable-next-line no-console 76 | app.listen(9000, () => console.log('Server started http://localhost:9000')) 77 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true /* Skip type checking of declaration files. */, 67 | "forceConsistentCasingInFileNames": true, 68 | /* Disallow inconsistently-cased references to the same file. */ 69 | "jsx": "react" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/typescript/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import nodeExternals from 'webpack-node-externals' 3 | import LoadablePlugin from '@loadable/webpack-plugin' 4 | import MiniCssExtractPlugin from 'mini-css-extract-plugin' 5 | 6 | const DIST_PATH = path.resolve(__dirname, 'public/dist') 7 | const production = process.env.NODE_ENV === 'production' 8 | const development = !production 9 | 10 | const getConfig = target => ({ 11 | name: target, 12 | mode: development ? 'development' : 'production', 13 | target, 14 | entry: `./src/client/main-${target}.js`, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.([jt])sx?$/, 19 | exclude: /node_modules/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | caller: { target }, 24 | }, 25 | }, 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: [ 30 | { 31 | loader: MiniCssExtractPlugin.loader, 32 | }, 33 | 'css-loader', 34 | ], 35 | }, 36 | ], 37 | }, 38 | externals: 39 | target === 'node' ? ['@loadable/component', nodeExternals()] : undefined, 40 | 41 | optimization: { 42 | runtimeChunk: target !== 'node', 43 | }, 44 | 45 | resolve: { 46 | extensions: ['.tsx', '.ts', '.js', '.css'], 47 | }, 48 | 49 | output: { 50 | path: path.join(DIST_PATH, target), 51 | filename: production ? '[name]-bundle-[chunkhash:8].js' : '[name].js', 52 | publicPath: `/dist/${target}/`, 53 | libraryTarget: target === 'node' ? 'commonjs2' : undefined, 54 | }, 55 | plugins: [new LoadablePlugin(), new MiniCssExtractPlugin()], 56 | }) 57 | 58 | export default [getConfig('web'), getConfig('node')] 59 | -------------------------------------------------------------------------------- /examples/webpack/README.md: -------------------------------------------------------------------------------- 1 | these examples are also **tests** 2 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "rules": { 6 | "import/no-unresolved": "off", 7 | "import/no-extraneous-dependencies": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/.gitignore: -------------------------------------------------------------------------------- 1 | public/dist -------------------------------------------------------------------------------- /examples/webpack/webpack4/README.md: -------------------------------------------------------------------------------- 1 | # Get the SSR example running 2 | 3 | Steps: 4 | 5 | 1. Download repository 6 | 7 | ```bash 8 | git clone https://github.com/gregberge/loadable-components.git 9 | ``` 10 | 11 | 2. move into example directory 12 | 13 | ```bash 14 | cd ./loadable-components/examples/server-side-rendering 15 | ``` 16 | 17 | 3. install [https://yarnpkg.com/lang/en/docs/install](yarn) if haven't already 18 | 4. install project dependencies 19 | 20 | ```bash 21 | yarn 22 | ``` 23 | 24 | 5. run locally or build and serve 25 | 26 | ```bash 27 | yarn dev 28 | 29 | # Or 30 | 31 | yarn build 32 | yarn start 33 | ``` 34 | 35 | 🍻 36 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/babel.config.js: -------------------------------------------------------------------------------- 1 | function isWebTarget(caller) { 2 | return Boolean(caller && caller.target === 'web') 3 | } 4 | 5 | function isWebpack(caller) { 6 | return Boolean(caller && caller.name === 'babel-loader') 7 | } 8 | 9 | module.exports = api => { 10 | const web = api.caller(isWebTarget) 11 | const webpack = api.caller(isWebpack) 12 | 13 | return { 14 | presets: [ 15 | '@babel/preset-react', 16 | [ 17 | '@babel/preset-env', 18 | { 19 | useBuiltIns: web ? 'entry' : undefined, 20 | corejs: web ? 'core-js@3' : false, 21 | targets: !web ? { node: 'current' } : undefined, 22 | modules: webpack ? false : 'commonjs', 23 | }, 24 | ], 25 | ], 26 | plugins: ['@babel/plugin-syntax-dynamic-import', '@loadable/babel-plugin'], 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["client", "public"], 3 | "execMap": { 4 | "js": "babel-node" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "nodemon src/server/main.js", 5 | "build": "NODE_ENV=production yarn build:webpack && yarn build:lib", 6 | "build:dev": "NODE_ENV=development yarn build:webpack && yarn build:lib", 7 | "build:webpack": "webpack", 8 | "build:lib": "babel -d lib src", 9 | "start": "NODE_ENV=production node lib/server/main.js", 10 | "start:dev": "NODE_ENV=development node -r @babel/register lib/server/main.js", 11 | "update": "rm ./node_modules/.yarn-integrity && yarn" 12 | }, 13 | "devDependencies": { 14 | "@babel/cli": "^7.4.4", 15 | "@babel/core": "^7.6.2", 16 | "@babel/node": "^7.0.0", 17 | "@babel/preset-env": "^7.6.2", 18 | "@babel/preset-react": "^7.0.0", 19 | "babel-loader": "^8.0.6", 20 | "css-loader": "^2.1.1", 21 | "mini-css-extract-plugin": "^0.6.0", 22 | "nodemon": "^1.19.0", 23 | "webpack": "^4.31.0", 24 | "webpack-cli": "^3.3.2", 25 | "webpack-dev-middleware": "^3.6.2", 26 | "webpack-node-externals": "^1.7.2" 27 | }, 28 | "dependencies": { 29 | "@babel/register": "^7.12.1", 30 | "@loadable/babel-plugin": "file:./../../../packages/babel-plugin", 31 | "@loadable/component": "file:./../../../packages/component", 32 | "@loadable/server": "file:./../../../packages/server", 33 | "@loadable/webpack-plugin": "file:./../../../packages/webpack-plugin", 34 | "core-js": "^3.0.1", 35 | "express": "^4.16.4", 36 | "moment": "^2.24.0", 37 | "react": "^16.8.6", 38 | "react-dom": "^16.8.6" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/src/client/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import loadable from '@loadable/component' 3 | 4 | const X = loadable(props => import(`./letters/${props.letter}`)) 5 | 6 | const ClientSideOnly = loadable(props => import(`./letters/${props.letter}`), { 7 | ssr: false, 8 | }) 9 | 10 | const Moment = loadable.lib(() => import('moment'), { 11 | resolveComponent: moment => moment.default || moment, 12 | }) 13 | 14 | const App = () => ( 15 |
16 |

17 | Lazy load letter A: 18 | 19 |

20 |

21 | Lazy load letter B: 22 | 23 |

24 |

25 | Lazy load letter only on Client C and D: 26 | + 27 | 28 |

29 |

30 | lazy load momentjs and format date: 31 | {moment => `now is : ${moment().format('HH:mm')}`} 32 |

33 |
34 | ) 35 | 36 | export default App 37 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/src/client/letters/A.css: -------------------------------------------------------------------------------- 1 | /* A CSS */ 2 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/src/client/letters/A.js: -------------------------------------------------------------------------------- 1 | import './A.css' 2 | 3 | const A = () => 'Lazy-Letter-A' 4 | 5 | export default A 6 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/src/client/letters/B.js: -------------------------------------------------------------------------------- 1 | const B = () => 'Lazy-Letter-B' 2 | 3 | export default B 4 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/src/client/letters/C.js: -------------------------------------------------------------------------------- 1 | const C = () => 'Lazy-Letter-C' 2 | 3 | export default C 4 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/src/client/letters/D.js: -------------------------------------------------------------------------------- 1 | // named as C 2 | const C = () => 'and-D' 3 | 4 | export default C 5 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/src/client/main-node.js: -------------------------------------------------------------------------------- 1 | export { default } from './App' 2 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/src/client/main-web.js: -------------------------------------------------------------------------------- 1 | import 'core-js' 2 | import React from 'react' 3 | import { hydrate } from 'react-dom' 4 | import { loadableReady } from '@loadable/component' 5 | import App from './App' 6 | 7 | console.log('waiting for application ready...') 8 | loadableReady(() => { 9 | console.log('application is ready...') 10 | const root = document.getElementById('main') 11 | hydrate(, root) 12 | }) 13 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/src/server/main.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import express from 'express' 3 | import React from 'react' 4 | import { renderToString } from 'react-dom/server' 5 | import { ChunkExtractor } from '@loadable/server' 6 | 7 | const app = express() 8 | 9 | // https://github.com/gregberge/loadable-components/issues/634 10 | app.use('*/runtime~main*.js', async (req, res, next) => { 11 | console.log('delaying runtime chunk') 12 | await new Promise(resolve => setTimeout(resolve, 2000)) 13 | next() 14 | }) 15 | 16 | app.use('*/letters*.js', async (req, res, next) => { 17 | console.log('delaying letters chunk') 18 | await new Promise(resolve => setTimeout(resolve, 1000)) 19 | next() 20 | }) 21 | 22 | app.use(express.static(path.join(__dirname, '../../public'))) 23 | 24 | if (process.env.NODE_ENV !== 'production') { 25 | /* eslint-disable global-require, import/no-extraneous-dependencies */ 26 | const { default: webpackConfig } = require('../../webpack.config.babel') 27 | const webpackDevMiddleware = require('webpack-dev-middleware') 28 | const webpack = require('webpack') 29 | /* eslint-enable global-require, import/no-extraneous-dependencies */ 30 | 31 | const compiler = webpack(webpackConfig) 32 | 33 | app.use( 34 | webpackDevMiddleware(compiler, { 35 | logLevel: 'silent', 36 | publicPath: '/dist/web', 37 | writeToDisk(filePath) { 38 | return /dist\/node\//.test(filePath) || /loadable-stats/.test(filePath) 39 | }, 40 | }), 41 | ) 42 | } 43 | 44 | const nodeStats = path.resolve( 45 | __dirname, 46 | '../../public/dist/node/loadable-stats.json', 47 | ) 48 | 49 | const webStats = path.resolve( 50 | __dirname, 51 | '../../public/dist/web/loadable-stats.json', 52 | ) 53 | 54 | app.get('*', (req, res) => { 55 | const nodeExtractor = new ChunkExtractor({ statsFile: nodeStats }) 56 | const { default: App } = nodeExtractor.requireEntrypoint() 57 | 58 | const webExtractor = new ChunkExtractor({ statsFile: webStats }) 59 | const jsx = webExtractor.collectChunks() 60 | 61 | const html = renderToString(jsx) 62 | 63 | res.set('content-type', 'text/html') 64 | res.send(` 65 | 66 | 67 | 68 | ${webExtractor.getLinkTags()} 69 | ${webExtractor.getStyleTags()} 70 | 71 | 72 |
${html}
73 | 74 | ${webExtractor.getScriptTags()} 75 | 76 | 77 | 78 | `) 79 | }) 80 | 81 | // eslint-disable-next-line no-console 82 | app.listen(9000, () => console.log('Server started http://localhost:9000')) 83 | -------------------------------------------------------------------------------- /examples/webpack/webpack4/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import nodeExternals from 'webpack-node-externals' 3 | import LoadablePlugin from '@loadable/webpack-plugin' 4 | import MiniCssExtractPlugin from 'mini-css-extract-plugin' 5 | 6 | const DIST_PATH = path.resolve(__dirname, 'public/dist') 7 | const production = process.env.NODE_ENV === 'production' 8 | const development = !production 9 | 10 | const getConfig = target => ({ 11 | name: target, 12 | mode: development ? 'development' : 'production', 13 | target, 14 | entry: `./src/client/main-${target}.js`, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js?$/, 19 | exclude: /node_modules/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | caller: { target }, 24 | }, 25 | }, 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: [ 30 | { 31 | loader: MiniCssExtractPlugin.loader, 32 | }, 33 | 'css-loader', 34 | ], 35 | }, 36 | ], 37 | }, 38 | externals: 39 | target === 'node' ? ['@loadable/component', nodeExternals()] : undefined, 40 | 41 | optimization: { 42 | runtimeChunk: target !== 'node', 43 | }, 44 | 45 | output: { 46 | path: path.join(DIST_PATH, target), 47 | filename: production ? '[name]-bundle-[chunkhash:8].js' : '[name].js', 48 | publicPath: `/dist/${target}/`, 49 | libraryTarget: target === 'node' ? 'commonjs2' : undefined, 50 | }, 51 | plugins: [new LoadablePlugin(), new MiniCssExtractPlugin()], 52 | }) 53 | 54 | export default [getConfig('web'), getConfig('node')] 55 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "rules": { 6 | "import/no-unresolved": "off", 7 | "import/no-extraneous-dependencies": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/.gitignore: -------------------------------------------------------------------------------- 1 | public/dist -------------------------------------------------------------------------------- /examples/webpack/webpack5/README.md: -------------------------------------------------------------------------------- 1 | # Get the SSR example running 2 | 3 | Steps: 4 | 5 | 1. Download repository 6 | 7 | ```bash 8 | git clone https://github.com/gregberge/loadable-components.git 9 | ``` 10 | 11 | 2. move into example directory 12 | 13 | ```bash 14 | cd ./loadable-components/examples/server-side-rendering 15 | ``` 16 | 17 | 3. install [https://yarnpkg.com/lang/en/docs/install](yarn) if haven't already 18 | 4. install project dependencies 19 | 20 | ```bash 21 | yarn 22 | ``` 23 | 24 | 5. run locally or build and serve 25 | 26 | ```bash 27 | yarn dev 28 | 29 | # Or 30 | 31 | yarn build 32 | yarn start 33 | ``` 34 | 35 | 🍻 36 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/babel.config.js: -------------------------------------------------------------------------------- 1 | function isWebTarget(caller) { 2 | return Boolean(caller && caller.target === 'web') 3 | } 4 | 5 | function isWebpack(caller) { 6 | return Boolean(caller && caller.name === 'babel-loader') 7 | } 8 | 9 | module.exports = api => { 10 | const web = api.caller(isWebTarget) 11 | const webpack = api.caller(isWebpack) 12 | 13 | return { 14 | presets: [ 15 | '@babel/preset-react', 16 | [ 17 | '@babel/preset-env', 18 | { 19 | useBuiltIns: web ? 'entry' : undefined, 20 | corejs: web ? 'core-js@3' : false, 21 | targets: !web ? { node: 'current' } : undefined, 22 | modules: webpack ? false : 'commonjs', 23 | }, 24 | ], 25 | ], 26 | plugins: ['@babel/plugin-syntax-dynamic-import', '@loadable/babel-plugin'], 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["client", "public"], 3 | "execMap": { 4 | "js": "babel-node" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "nodemon src/server/main.js", 5 | "build": "NODE_ENV=production yarn build:webpack && yarn build:lib", 6 | "build:dev": "NODE_ENV=development yarn build:webpack && yarn build:lib", 7 | "build:webpack": "webpack", 8 | "build:lib": "babel -d lib src", 9 | "start": "NODE_ENV=production node lib/server/main.js", 10 | "start:dev": "NODE_ENV=development node -r @babel/register lib/server/main.js", 11 | "update": "rm ./node_modules/.yarn-integrity && yarn" 12 | }, 13 | "devDependencies": { 14 | "@babel/cli": "^7.4.4", 15 | "@babel/core": "^7.6.2", 16 | "@babel/node": "^7.0.0", 17 | "@babel/preset-env": "^7.6.2", 18 | "@babel/preset-react": "^7.0.0", 19 | "babel-loader": "^8.1.0", 20 | "css-loader": "^5.0.0", 21 | "mini-css-extract-plugin": "^1.1.0", 22 | "nodemon": "^1.19.0", 23 | "webpack": "^5.17.0", 24 | "webpack-cli": "^4.1.0", 25 | "webpack-dev-middleware": "^3.7.2", 26 | "webpack-node-externals": "^2.5.2" 27 | }, 28 | "dependencies": { 29 | "@babel/register": "^7.12.1", 30 | "@loadable/babel-plugin": "file:./../../../packages/babel-plugin", 31 | "@loadable/component": "file:./../../../packages/component", 32 | "@loadable/server": "file:./../../../packages/server", 33 | "@loadable/webpack-plugin": "file:./../../../packages/webpack-plugin", 34 | "core-js": "^3.0.1", 35 | "express": "^4.16.4", 36 | "moment": "^2.24.0", 37 | "react": "^16.8.6", 38 | "react-dom": "^16.8.6" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/src/client/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import loadable from '@loadable/component' 3 | 4 | const X = loadable(props => import(`./letters/${props.letter}`)) 5 | 6 | const ClientSideOnly = loadable(props => import(`./letters/${props.letter}`), { 7 | ssr: false, 8 | }) 9 | 10 | const Moment = loadable.lib(() => import('moment'), { 11 | resolveComponent: moment => moment.default || moment, 12 | }) 13 | 14 | const App = () => ( 15 |
16 |

17 | Lazy load letter A: 18 | 19 |

20 |

21 | Lazy load letter B: 22 | 23 |

24 |

25 | Lazy load letter only on Client C and D: 26 | + 27 | 28 |

29 |

30 | lazy load momentjs and format date: 31 | {moment => `now is : ${moment().format('HH:mm')}`} 32 |

33 |
34 | ) 35 | 36 | export default App 37 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/src/client/letters/A.css: -------------------------------------------------------------------------------- 1 | /* A CSS */ 2 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/src/client/letters/A.js: -------------------------------------------------------------------------------- 1 | import './A.css' 2 | 3 | const A = () => 'Lazy-Letter-A' 4 | 5 | export default A 6 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/src/client/letters/B.js: -------------------------------------------------------------------------------- 1 | const B = () => 'Lazy-Letter-B' 2 | 3 | export default B 4 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/src/client/letters/C.js: -------------------------------------------------------------------------------- 1 | const C = () => 'Lazy-Letter-C' 2 | 3 | export default C 4 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/src/client/letters/D.js: -------------------------------------------------------------------------------- 1 | // named as C 2 | const C = () => 'and-D' 3 | 4 | export default C 5 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/src/client/main-node.js: -------------------------------------------------------------------------------- 1 | export { default } from './App' 2 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/src/client/main-web.js: -------------------------------------------------------------------------------- 1 | import 'core-js' 2 | import React from 'react' 3 | import { hydrate } from 'react-dom' 4 | import { loadableReady } from '@loadable/component' 5 | import App from './App' 6 | 7 | console.log('waiting for application ready...') 8 | loadableReady(() => { 9 | console.log('application is ready...') 10 | const root = document.getElementById('main') 11 | hydrate(, root) 12 | }) 13 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/src/server/main.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import express from 'express' 3 | import React from 'react' 4 | import { renderToString } from 'react-dom/server' 5 | import { ChunkExtractor } from '@loadable/server' 6 | 7 | const app = express() 8 | 9 | // https://github.com/gregberge/loadable-components/issues/634 10 | // app.use('*/runtime~main.js', async (req, res, next) => { 11 | // console.log('delaying runtime chunk'); 12 | // await new Promise(resolve => setTimeout(resolve, 2000)); 13 | // next(); 14 | // }); 15 | 16 | app.use(express.static(path.join(__dirname, '../../public'))) 17 | 18 | if (process.env.NODE_ENV !== 'production') { 19 | /* eslint-disable global-require, import/no-extraneous-dependencies */ 20 | const { default: webpackConfig } = require('../../webpack.config.babel') 21 | const webpackDevMiddleware = require('webpack-dev-middleware') 22 | const webpack = require('webpack') 23 | /* eslint-enable global-require, import/no-extraneous-dependencies */ 24 | 25 | const compiler = webpack(webpackConfig) 26 | 27 | app.use( 28 | webpackDevMiddleware(compiler, { 29 | logLevel: 'silent', 30 | publicPath: '/dist/web', 31 | writeToDisk(filePath) { 32 | return /dist\/node\//.test(filePath) || /loadable-stats/.test(filePath) 33 | }, 34 | }), 35 | ) 36 | } 37 | 38 | const nodeStats = path.resolve( 39 | __dirname, 40 | '../../public/dist/node/loadable-stats.json', 41 | ) 42 | 43 | const webStats = path.resolve( 44 | __dirname, 45 | '../../public/dist/web/loadable-stats.json', 46 | ) 47 | 48 | app.get('*', (req, res) => { 49 | const nodeExtractor = new ChunkExtractor({ statsFile: nodeStats }) 50 | const { default: App } = nodeExtractor.requireEntrypoint() 51 | 52 | const webExtractor = new ChunkExtractor({ statsFile: webStats }) 53 | const jsx = webExtractor.collectChunks() 54 | 55 | const html = renderToString(jsx) 56 | 57 | res.set('content-type', 'text/html') 58 | res.send(` 59 | 60 | 61 | 62 | ${webExtractor.getLinkTags()} 63 | ${webExtractor.getStyleTags()} 64 | 65 | 66 |
${html}
67 | 68 | ${webExtractor.getScriptTags()} 69 | 70 | 71 | 72 | `) 73 | }) 74 | 75 | // eslint-disable-next-line no-console 76 | app.listen(9000, () => console.log('Server started http://localhost:9000')) 77 | -------------------------------------------------------------------------------- /examples/webpack/webpack5/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import nodeExternals from 'webpack-node-externals' 3 | import LoadablePlugin from '@loadable/webpack-plugin' 4 | import MiniCssExtractPlugin from 'mini-css-extract-plugin' 5 | 6 | const DIST_PATH = path.resolve(__dirname, 'public/dist') 7 | const production = process.env.NODE_ENV === 'production' 8 | const development = !production 9 | 10 | const getConfig = target => ({ 11 | name: target, 12 | mode: development ? 'development' : 'production', 13 | target, 14 | entry: `./src/client/main-${target}.js`, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js?$/, 19 | exclude: /node_modules/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | caller: { target }, 24 | }, 25 | }, 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: [ 30 | { 31 | loader: MiniCssExtractPlugin.loader, 32 | }, 33 | 'css-loader', 34 | ], 35 | }, 36 | ], 37 | }, 38 | externals: 39 | target === 'node' ? ['@loadable/component', nodeExternals()] : undefined, 40 | 41 | optimization: { 42 | // this will lead to runtime error 43 | runtimeChunk: target !== 'node', 44 | }, 45 | 46 | output: { 47 | path: path.join(DIST_PATH, target), 48 | filename: production ? '[name]-bundle-[chunkhash:8].js' : '[name].js', 49 | publicPath: `/dist/${target}/`, 50 | libraryTarget: target === 'node' ? 'commonjs2' : undefined, 51 | }, 52 | plugins: [new LoadablePlugin(), new MiniCssExtractPlugin()], 53 | }) 54 | 55 | export default [getConfig('web'), getConfig('node')] 56 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.9.0", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "5.16.7", 7 | "npmClient": "yarn", 8 | "useWorkspaces": true 9 | } 10 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "website" 3 | command = "yarn build" 4 | publish = "website/public" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loadable-components", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "bundlesize": [ 8 | { 9 | "path": "./packages/component/dist/loadable.min.js", 10 | "maxSize": "3.5 kB" 11 | }, 12 | { 13 | "path": "./packages/component/dist/loadable.esm.js", 14 | "maxSize": "4.5 kB" 15 | } 16 | ], 17 | "scripts": { 18 | "build": "lerna run build", 19 | "ci": "yarn build && yarn lint && yarn test:prepare && yarn test --ci", 20 | "dev": "WATCH_MODE=true lerna run build --parallel -- --watch", 21 | "format": "prettier --write \"**/*.{js,json,md}\"", 22 | "lint": "eslint .", 23 | "release": "lerna publish --conventional-commits && conventional-github-releaser --preset angular", 24 | "release-to-git": "./scripts/git-release.sh", 25 | "test:prepare": "./scripts/prepare.sh", 26 | "test": "jest" 27 | }, 28 | "devDependencies": { 29 | "@babel/cli": "^7.7.7", 30 | "@babel/core": "^7.7.7", 31 | "@babel/node": "^7.7.7", 32 | "@babel/plugin-proposal-class-properties": "^7.7.4", 33 | "@babel/plugin-transform-runtime": "^7.7.6", 34 | "@babel/preset-env": "^7.7.7", 35 | "@babel/preset-react": "^7.7.4", 36 | "@hedgepigdaniel/npm-publish-git": "^0.1.0", 37 | "@testing-library/jest-dom": "^4.2.4", 38 | "@testing-library/react": "^9.4.0", 39 | "babel-core": "^7.0.0-bridge.0", 40 | "babel-eslint": "^10.0.3", 41 | "babel-jest": "^24.9.0", 42 | "babel-plugin-annotate-pure-calls": "^0.4.0", 43 | "conventional-github-releaser": "^3.1.2", 44 | "cross-env": "^6.0.3", 45 | "eslint": "^6.8.0", 46 | "eslint-config-airbnb": "^18.0.1", 47 | "eslint-config-prettier": "^6.9.0", 48 | "eslint-plugin-import": "^2.19.1", 49 | "eslint-plugin-jsx-a11y": "^6.2.1", 50 | "eslint-plugin-react": "^7.17.0", 51 | "jest": "^24.9.0", 52 | "lerna": "^3.20.2", 53 | "prettier": "^1.19.1", 54 | "react": "^16.12.0", 55 | "react-dom": "^16.12.0", 56 | "regenerator-runtime": "^0.13.3", 57 | "rollup": "^1.29.0", 58 | "rollup-plugin-babel": "^4.3.2", 59 | "rollup-plugin-commonjs": "^10.1.0", 60 | "rollup-plugin-node-resolve": "^5.0.1", 61 | "rollup-plugin-replace": "^2.2.0", 62 | "rollup-plugin-terser": "^5.1.3", 63 | "shx": "^0.3.2" 64 | }, 65 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 66 | } 67 | -------------------------------------------------------------------------------- /packages/babel-plugin/README.md: -------------------------------------------------------------------------------- 1 | # @loadable/babel-plugin 2 | 3 | This plugin is required only if you use Server Side Rendering in your application. [See `@loadable/server` for more information](https://loadable-components.com/docs/api-loadable-server/). 4 | 5 | ## Install 6 | 7 | ``` 8 | npm install --save-dev @loadable/babel-plugin 9 | ``` 10 | 11 | ## Documentation 12 | 13 | 👉 [See full documentation](https://loadable-components.com/) 14 | 15 | ## License 16 | 17 | MIT 18 | -------------------------------------------------------------------------------- /packages/babel-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@loadable/babel-plugin", 3 | "description": "Babel plugin for loadable (required for SSR).", 4 | "version": "5.16.1", 5 | "main": "lib/index.js", 6 | "repository": "git@github.com:gregberge/loadable-components.git", 7 | "author": "Greg Bergé ", 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "keywords": [ 12 | "loadable" 13 | ], 14 | "engines": { 15 | "node": ">=8" 16 | }, 17 | "funding": { 18 | "type": "github", 19 | "url": "https://github.com/sponsors/gregberge" 20 | }, 21 | "license": "MIT", 22 | "scripts": { 23 | "prebuild": "shx rm -rf lib", 24 | "build": "BUILD_TARGET=node babel --config-file ../../babel.config.js -d lib --ignore \"**/*.test.js\" src", 25 | "prepublishOnly": "yarn run build" 26 | }, 27 | "dependencies": { 28 | "@babel/plugin-syntax-dynamic-import": "^7.7.4" 29 | }, 30 | "peerDependencies": { 31 | "@babel/core": "^7.0.0-0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/babel-plugin/src/index.js: -------------------------------------------------------------------------------- 1 | import { declare } from "@babel/helper-plugin-utils"; 2 | import syntaxDynamicImport from '@babel/plugin-syntax-dynamic-import' 3 | import chunkNameProperty from './properties/chunkName' 4 | import isReadyProperty from './properties/isReady' 5 | import importAsyncProperty from './properties/importAsync' 6 | import requireAsyncProperty from './properties/requireAsync' 7 | import requireSyncProperty from './properties/requireSync' 8 | import resolveProperty from './properties/resolve' 9 | import stateProperty from './properties/state' 10 | 11 | const properties = [ 12 | stateProperty, 13 | chunkNameProperty, 14 | isReadyProperty, 15 | importAsyncProperty, 16 | requireAsyncProperty, 17 | requireSyncProperty, 18 | resolveProperty, 19 | ] 20 | 21 | const LOADABLE_COMMENT = '#__LOADABLE__' 22 | 23 | const DEFAULT_SIGNATURE = [{name: 'default', from: '@loadable/component'}]; 24 | 25 | const loadablePlugin = declare((api, { 26 | signatures = DEFAULT_SIGNATURE 27 | }) => { 28 | const { types: t } = api 29 | 30 | function collectImportCallPaths(startPath) { 31 | const imports = [] 32 | startPath.traverse({ 33 | Import(importPath) { 34 | imports.push(importPath.parentPath) 35 | }, 36 | }) 37 | return imports 38 | } 39 | 40 | const propertyFactories = properties.map(init => init(api)) 41 | 42 | function isValidIdentifier(path, loadableImportSpecifiers, lazyImportSpecifier) { 43 | // loadable signatures 44 | if (loadableImportSpecifiers.find(specifier => path.get('callee').isIdentifier({ name: specifier }))) { 45 | return true 46 | } 47 | 48 | // `lazy()` 49 | if (lazyImportSpecifier && path.get('callee').isIdentifier({ name: lazyImportSpecifier })) { 50 | return true 51 | } 52 | 53 | // `loadable.lib()` 54 | return ( 55 | path.get('callee').isMemberExpression() && 56 | loadableImportSpecifiers.find(specifier => path.get('callee.object').isIdentifier({ name: specifier })) && 57 | path.get('callee.property').isIdentifier({ name: 'lib' }) 58 | ) 59 | } 60 | 61 | function hasLoadableComment(path) { 62 | const comments = path.get('leadingComments') 63 | const comment = comments.find( 64 | ({ node }) => 65 | node && node.value && String(node.value).includes(LOADABLE_COMMENT), 66 | ) 67 | if (!comment) return false 68 | comment.remove() 69 | return true 70 | } 71 | 72 | function getFuncPath(path) { 73 | const funcPath = path.isCallExpression() ? path.get('arguments.0') : path 74 | if ( 75 | !funcPath.isFunctionExpression() && 76 | !funcPath.isArrowFunctionExpression() && 77 | !funcPath.isObjectMethod() 78 | ) { 79 | return null 80 | } 81 | return funcPath 82 | } 83 | 84 | function transformImport(path) { 85 | const callPaths = collectImportCallPaths(path) 86 | 87 | // Ignore loadable function that does not have any "import" call 88 | if (callPaths.length === 0) return 89 | 90 | // Multiple imports call is not supported 91 | if (callPaths.length > 1) { 92 | throw new Error( 93 | 'loadable: multiple import calls inside `loadable()` function are not supported.', 94 | ) 95 | } 96 | 97 | const [callPath] = callPaths 98 | 99 | const funcPath = getFuncPath(path) 100 | if (!funcPath) return 101 | 102 | funcPath.node.params = funcPath.node.params || [] 103 | 104 | const object = t.objectExpression( 105 | propertyFactories.map(getProperty => 106 | getProperty({ path, callPath, funcPath }), 107 | ), 108 | ) 109 | 110 | if (funcPath.isObjectMethod()) { 111 | funcPath.replaceWith( 112 | t.objectProperty(funcPath.node.key, object, funcPath.node.computed), 113 | ) 114 | } else { 115 | funcPath.replaceWith(object) 116 | } 117 | } 118 | 119 | 120 | return { 121 | inherits: syntaxDynamicImport, 122 | visitor: { 123 | Program: { 124 | enter(programPath) { 125 | let lazyImportSpecifier = false 126 | // default to "loadable" detection. Remove defaults if signatures are configured 127 | const loadableSpecifiers = signatures === DEFAULT_SIGNATURE ? ['loadable']: []; 128 | 129 | programPath.traverse({ 130 | ImportDefaultSpecifier(path) { 131 | const { parent } = path 132 | const { local } = path.node 133 | if (local && signatures.find(signature => signature.name === 'default' && parent.source.value === signature.from)) { 134 | loadableSpecifiers.push(local.name) 135 | } 136 | }, 137 | ImportSpecifier(path) { 138 | const { parent } = path 139 | const { imported, local } = path.node 140 | if (!lazyImportSpecifier) { 141 | lazyImportSpecifier = parent.source.value == '@loadable/component' && 142 | imported && imported.name == 'lazy' && local && local.name 143 | } 144 | if (local && imported && signatures.find(signature => imported.name === signature.name && parent.source.value === signature.from)) { 145 | loadableSpecifiers.push(local.name) 146 | } 147 | }, 148 | CallExpression(path) { 149 | if (!isValidIdentifier(path, loadableSpecifiers, lazyImportSpecifier)) return 150 | transformImport(path) 151 | }, 152 | 'ArrowFunctionExpression|FunctionExpression|ObjectMethod': path => { 153 | if (!hasLoadableComment(path)) return 154 | transformImport(path) 155 | }, 156 | }) 157 | }, 158 | }, 159 | }, 160 | } 161 | }) 162 | 163 | export default loadablePlugin 164 | -------------------------------------------------------------------------------- /packages/babel-plugin/src/properties/chunkName.js: -------------------------------------------------------------------------------- 1 | import vm from 'vm' 2 | import { getImportArg } from '../util' 3 | 4 | const JS_PATH_REGEXP = /^[./]+|(\.js$)/g 5 | const MATCH_LEFT_HYPHENS_REPLACE_REGEX = /^-/g 6 | // https://github.com/webpack/webpack/blob/master/lib/Template.js 7 | const WEBPACK_CHUNK_NAME_REGEXP = /webpackChunkName/ 8 | const WEBPACK_PATH_NAME_NORMALIZE_REPLACE_REGEX = /[^a-zA-Z0-9_!§$()=\-^°]+/g 9 | const WEBPACK_MATCH_PADDED_HYPHENS_REPLACE_REGEX = /^-|-$/g 10 | 11 | function readWebpackCommentValues(str) { 12 | try { 13 | const values = vm.runInNewContext(`(function(){return {${str}};})()`) 14 | return values 15 | } catch (e) { 16 | throw Error(`compilation error while processing: /*${str}*/: ${e.message}`) 17 | } 18 | } 19 | 20 | function writeWebpackCommentValues(values) { 21 | try { 22 | const str = Object.keys(values) 23 | .map(key => `${key}: ${JSON.stringify(values[key])}`) 24 | .join(', ') 25 | return ` ${str} ` 26 | } catch (e) { 27 | throw Error( 28 | `compilation error while processing: /*${values}*/: ${e.message}`, 29 | ) 30 | } 31 | } 32 | 33 | function getChunkNameComment(importArg) { 34 | if (!importArg.has('leadingComments')) return null 35 | return importArg 36 | .get('leadingComments') 37 | .find(comment => comment.node.value.match(WEBPACK_CHUNK_NAME_REGEXP)) 38 | } 39 | 40 | function getRawChunkNameFromCommments(importArg) { 41 | const chunkNameComment = getChunkNameComment(importArg) 42 | if (!chunkNameComment) return null 43 | return readWebpackCommentValues(chunkNameComment.node.value) 44 | } 45 | 46 | function moduleToChunk(str) { 47 | if (typeof str !== 'string') return '' 48 | return str 49 | .replace(JS_PATH_REGEXP, '') 50 | .replace(WEBPACK_PATH_NAME_NORMALIZE_REPLACE_REGEX, '-') 51 | .replace(WEBPACK_MATCH_PADDED_HYPHENS_REPLACE_REGEX, '') 52 | } 53 | 54 | function replaceQuasi(str, stripLeftHyphen) { 55 | if (!str) return '' 56 | const result = str.replace(WEBPACK_PATH_NAME_NORMALIZE_REPLACE_REGEX, '-') 57 | if (!stripLeftHyphen) return result 58 | return result.replace(MATCH_LEFT_HYPHENS_REPLACE_REGEX, '') 59 | } 60 | 61 | export default function chunkNameProperty({ types: t }) { 62 | function transformQuasi(quasi, first, single) { 63 | return t.templateElement( 64 | { 65 | raw: single 66 | ? moduleToChunk(quasi.value.raw) 67 | : replaceQuasi(quasi.value.raw, first), 68 | cooked: single 69 | ? moduleToChunk(quasi.value.cooked) 70 | : replaceQuasi(quasi.value.cooked, first), 71 | }, 72 | quasi.tail, 73 | ) 74 | } 75 | 76 | function sanitizeChunkNameTemplateLiteral(node) { 77 | return t.callExpression(t.memberExpression(node, t.identifier('replace')), [ 78 | t.regExpLiteral(WEBPACK_PATH_NAME_NORMALIZE_REPLACE_REGEX.source, 'g'), 79 | t.stringLiteral('-'), 80 | ]) 81 | } 82 | 83 | function combineExpressions(node) { 84 | const { expressions } = node 85 | const { length } = expressions 86 | 87 | if (length === 1) { 88 | return expressions[0] 89 | } 90 | 91 | return expressions 92 | .slice(1) 93 | .reduce((r, p) => t.binaryExpression('+', r, p), expressions[0]) 94 | } 95 | 96 | function generateChunkNameNode(callPath, prefix) { 97 | const importArg = getImportArg(callPath) 98 | if (importArg.isTemplateLiteral()) { 99 | return prefix 100 | ? t.binaryExpression( 101 | '+', 102 | t.stringLiteral(prefix), 103 | sanitizeChunkNameTemplateLiteral( 104 | combineExpressions(importArg.node), 105 | ), 106 | ) 107 | : t.templateLiteral( 108 | importArg.node.quasis.map((quasi, index) => 109 | transformQuasi( 110 | quasi, 111 | index === 0, 112 | importArg.node.quasis.length === 1, 113 | ), 114 | ), 115 | importArg.node.expressions, 116 | ) 117 | } 118 | return t.stringLiteral(moduleToChunk(importArg.node.value)) 119 | } 120 | 121 | function getExistingChunkNameComment(callPath) { 122 | const importArg = getImportArg(callPath) 123 | const values = getRawChunkNameFromCommments(importArg) 124 | return values 125 | } 126 | 127 | function isAgressiveImport(callPath) { 128 | const importArg = getImportArg(callPath) 129 | return ( 130 | importArg.isTemplateLiteral() && importArg.node.expressions.length > 0 131 | ) 132 | } 133 | 134 | function addOrReplaceChunkNameComment(callPath, values) { 135 | const importArg = getImportArg(callPath) 136 | const chunkNameComment = getChunkNameComment(importArg) 137 | if (chunkNameComment) { 138 | chunkNameComment.remove() 139 | } 140 | 141 | importArg.addComment('leading', writeWebpackCommentValues(values)) 142 | } 143 | 144 | function chunkNameFromTemplateLiteral(node) { 145 | const [q1] = node.quasis 146 | const v1 = q1 ? q1.value.cooked : '' 147 | if (!node.expressions.length) return v1 148 | return `${v1}[request]` 149 | } 150 | 151 | function getChunkNamePrefix(chunkName) { 152 | if (typeof chunkName !== 'string') return '' 153 | const match = chunkName.match(/(.+?)\[(request|index)\]$/) 154 | return match ? match[1] : '' 155 | } 156 | 157 | function replaceChunkName(callPath) { 158 | const agressiveImport = isAgressiveImport(callPath) 159 | const values = getExistingChunkNameComment(callPath) 160 | let { webpackChunkName } = values || {} 161 | 162 | if (!agressiveImport && values) { 163 | addOrReplaceChunkNameComment(callPath, values) 164 | return t.stringLiteral(webpackChunkName) 165 | } 166 | 167 | let chunkNameNode = generateChunkNameNode( 168 | callPath, 169 | getChunkNamePrefix(webpackChunkName), 170 | ) 171 | 172 | if (t.isTemplateLiteral(chunkNameNode)) { 173 | webpackChunkName = chunkNameFromTemplateLiteral(chunkNameNode) 174 | chunkNameNode = sanitizeChunkNameTemplateLiteral(chunkNameNode) 175 | } else if (t.isStringLiteral(chunkNameNode)) { 176 | webpackChunkName = chunkNameNode.value 177 | } 178 | 179 | addOrReplaceChunkNameComment(callPath, { ...values, webpackChunkName }) 180 | return chunkNameNode 181 | } 182 | 183 | return ({ callPath, funcPath }) => { 184 | const chunkName = replaceChunkName(callPath) 185 | 186 | return t.objectMethod( 187 | 'method', 188 | t.identifier('chunkName'), 189 | funcPath.node.params, 190 | t.blockStatement([t.returnStatement(chunkName)]), 191 | ) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /packages/babel-plugin/src/properties/importAsync.js: -------------------------------------------------------------------------------- 1 | export default function requireAsyncProperty({ types: t }) { 2 | function getFunc(funcPath) { 3 | if (funcPath.isObjectMethod()) { 4 | const { params, body, async } = funcPath.node 5 | return t.arrowFunctionExpression(params, body, async) 6 | } 7 | 8 | return funcPath.node 9 | } 10 | 11 | return ({ funcPath }) => 12 | t.objectProperty(t.identifier('importAsync'), getFunc(funcPath)) 13 | } 14 | -------------------------------------------------------------------------------- /packages/babel-plugin/src/properties/isReady.js: -------------------------------------------------------------------------------- 1 | export default function isReadyProperty({ types: t, template }) { 2 | const statements = template.ast(` 3 | const key=this.resolve(props) 4 | if (this.resolved[key] !== true) { 5 | return false 6 | } 7 | 8 | if (typeof __webpack_modules__ !== 'undefined') { 9 | return !!(__webpack_modules__[key]) 10 | } 11 | 12 | return false 13 | `) 14 | 15 | return () => 16 | t.objectMethod( 17 | 'method', 18 | t.identifier('isReady'), 19 | [t.identifier('props')], 20 | t.blockStatement(statements), 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/babel-plugin/src/properties/requireAsync.js: -------------------------------------------------------------------------------- 1 | export default function requireAsyncProperty({ types: t, template }) { 2 | const tracking = template.ast(` 3 | const key = this.resolve(props) 4 | this.resolved[key] = false 5 | return this.importAsync(props).then(resolved => { 6 | this.resolved[key] = true 7 | return resolved; 8 | }); 9 | `) 10 | 11 | return () => 12 | t.objectMethod( 13 | 'method', 14 | t.identifier('requireAsync'), 15 | [t.identifier('props')], 16 | t.blockStatement(tracking), 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/babel-plugin/src/properties/requireSync.js: -------------------------------------------------------------------------------- 1 | export default function requireSyncProperty({ types: t, template }) { 2 | const statements = template.ast(` 3 | const id = this.resolve(props) 4 | 5 | if (typeof __webpack_require__ !== 'undefined') { 6 | return __webpack_require__(id) 7 | } 8 | 9 | return eval('module.require')(id) 10 | `) 11 | 12 | return () => 13 | t.objectMethod( 14 | 'method', 15 | t.identifier('requireSync'), 16 | [t.identifier('props')], 17 | t.blockStatement(statements), 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/babel-plugin/src/properties/resolve.js: -------------------------------------------------------------------------------- 1 | import { getImportArg } from '../util' 2 | 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 | ` 11 | 12 | function getCallValue(callPath) { 13 | const importArg = getImportArg(callPath) 14 | if (importArg.isTemplateLiteral()) { 15 | return t.templateLiteral( 16 | importArg.node.quasis, 17 | importArg.node.expressions, 18 | ) 19 | } 20 | if (importArg.isBinaryExpression()) { 21 | return t.BinaryExpression( 22 | importArg.node.operator, 23 | importArg.node.left, 24 | importArg.node.right, 25 | ) 26 | } 27 | return t.stringLiteral(importArg.node.value) 28 | } 29 | 30 | return ({ callPath, funcPath }) => 31 | t.objectMethod( 32 | 'method', 33 | t.identifier('resolve'), 34 | funcPath.node.params, 35 | t.blockStatement(buildStatements({ ID: getCallValue(callPath) })), 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /packages/babel-plugin/src/properties/state.js: -------------------------------------------------------------------------------- 1 | export default function requireAsyncProperty({ types: t }) { 2 | return () => 3 | t.objectProperty(t.identifier('resolved'), t.objectExpression([])) 4 | } 5 | -------------------------------------------------------------------------------- /packages/babel-plugin/src/util.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | export function getImportArg(callPath) { 4 | return callPath.get('arguments.0') 5 | } 6 | -------------------------------------------------------------------------------- /packages/codemod/.npmignore: -------------------------------------------------------------------------------- 1 | transforms/__testfixtures__ 2 | transforms/__tests__ -------------------------------------------------------------------------------- /packages/codemod/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [5.13.2](https://github.com/gregberge/loadable-components/compare/v5.13.1...v5.13.2) (2020-09-14) 7 | 8 | **Note:** Version bump only for package loadable-codemod 9 | 10 | 11 | 12 | 13 | 14 | ## [5.13.1](https://github.com/gregberge/loadable-components/compare/v5.13.0...v5.13.1) (2020-07-02) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * expose used chunkNames from a server. Fixes [#587](https://github.com/gregberge/loadable-components/issues/587) ([831aec0](https://github.com/gregberge/loadable-components/commit/831aec03154ab16007db0d78fbf3559583c000fe)) 20 | 21 | 22 | 23 | 24 | 25 | # [5.13.0](https://github.com/gregberge/loadable-components/compare/v5.12.0...v5.13.0) (2020-06-29) 26 | 27 | **Note:** Version bump only for package loadable-codemod 28 | 29 | 30 | 31 | 32 | 33 | # [5.12.0](https://github.com/gregberge/loadable-components/compare/v5.11.0...v5.12.0) (2020-01-09) 34 | 35 | 36 | ### Features 37 | 38 | * add codemods to migrate from react-loadable ([#463](https://github.com/gregberge/loadable-components/issues/463)) ([a82d5ad](https://github.com/gregberge/loadable-components/commit/a82d5ad1e17cd64d3579aa207abfc18346ff0107)) 39 | -------------------------------------------------------------------------------- /packages/codemod/README.md: -------------------------------------------------------------------------------- 1 | # @loadable/codemod 2 | 3 | This package is a collection of codemod that can be used to help making big changes easier to a project, for example: migrating from `react-loadable` to `@loadable/component` 4 | 5 | ## Notes about `react-loadable-to-loadable-component` transform 6 | 7 | `react-loadable-to-loadable-component` transform will help codemod all of your `Loadable()` declaration to `loadable()` with mostly equivalent params, barring some behavior that do not exist in `@loadable/component` such as `Loadable.Map()`, `timeout`, `delay`, etc. 8 | 9 | After running the codemod, you will still need to update some of your code manually, namely: 10 | 11 | 1. Using `loadableReady` to hydrate your app on the client side. 12 | 2. Updating your webpack configuration to use `@loadable` 13 | 3. Updating your server side rendering code to use `ChunkExtractor` 14 | -------------------------------------------------------------------------------- /packages/codemod/bin/main.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable no-console */ 4 | const yargs = require('yargs') 5 | const execa = require('execa') 6 | const path = require('path') 7 | const fs = require('fs') 8 | const chalk = require('chalk') 9 | const CodemodError = require('./utils/CodemodError') 10 | 11 | const jscodeshiftExecutable = require.resolve('.bin/jscodeshift') 12 | const transformsDir = path.resolve(__dirname, '../transforms') 13 | 14 | const { argv } = yargs 15 | 16 | try { 17 | const selectedCodemod = argv._[0] 18 | const directoryToApplyTo = argv._[1] 19 | 20 | if (!selectedCodemod || !directoryToApplyTo) { 21 | throw new CodemodError({ 22 | type: 'Invalid params', 23 | }) 24 | } 25 | 26 | const availableTransforms = fs 27 | .readdirSync(transformsDir) 28 | .filter(v => v !== '__tests__' && v !== '__testfixtures__') 29 | .map(v => v.replace('.js', '')) 30 | 31 | if (!availableTransforms.some(t => t === selectedCodemod)) { 32 | throw new CodemodError({ 33 | type: 'Unrecognised transform', 34 | payload: selectedCodemod, 35 | }) 36 | } 37 | 38 | const result = execa.commandSync( 39 | `${jscodeshiftExecutable} -t ${transformsDir}/${selectedCodemod}.js ${directoryToApplyTo}`, 40 | { 41 | stdio: 'inherit', 42 | stripEof: false, 43 | }, 44 | ) 45 | 46 | if (result.error) { 47 | throw result.error 48 | } 49 | } catch (err) { 50 | if (err.type === 'Invalid params') { 51 | console.error(chalk.red('Invalid params passed!')) 52 | console.error( 53 | chalk.red( 54 | 'loadable-codemod requires 2 params to be passed, the name of the codemod, and a directory to apply the codemod to.', 55 | ), 56 | ) 57 | console.error( 58 | chalk.red( 59 | 'Example: npx loadable-codemod react-loadable-to-loadable-component ./src/client', 60 | ), 61 | ) 62 | 63 | process.exit(1) 64 | } 65 | 66 | if (err.type === 'Unrecognised transform') { 67 | console.error(chalk.red(`Unrecognised transform passed: '${err.payload}'`)) 68 | 69 | process.exit(2) 70 | } 71 | 72 | // For other errors, just re-throw it 73 | throw err 74 | } 75 | -------------------------------------------------------------------------------- /packages/codemod/bin/utils/CodemodError.js: -------------------------------------------------------------------------------- 1 | class CodemodError extends Error { 2 | constructor(args) { 3 | super(args) 4 | this.type = args.type 5 | this.payload = args.payload 6 | } 7 | } 8 | 9 | module.exports = CodemodError 10 | -------------------------------------------------------------------------------- /packages/codemod/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loadable-codemod", 3 | "description": "Various codemods related to @loadable/components for easier migration/upgrades.", 4 | "version": "5.13.2", 5 | "repository": "git@github.com:gregberge/loadable-components.git", 6 | "author": "Jacky Efendi ", 7 | "bin": { 8 | "loadable-codemod": "./bin/main.js" 9 | }, 10 | "publishConfig": { 11 | "access": "public" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "ssr", 16 | "webpack", 17 | "code-splitting", 18 | "react-router", 19 | "server-side-rendering", 20 | "dynamic-import", 21 | "react-loadable", 22 | "react-async-components", 23 | "codemod" 24 | ], 25 | "engines": { 26 | "node": ">=8" 27 | }, 28 | "license": "MIT", 29 | "dependencies": { 30 | "chalk": "^3.0.0", 31 | "execa": "^4.0.0", 32 | "jscodeshift": "0.7.0", 33 | "yargs": "^15.1.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/codemod/transforms/__testfixtures__/react-loadable-to-loadable-component_arrow-no-params.input.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import Loadable from 'react-loadable' 3 | 4 | const CustomLinkLoadable = Loadable({ 5 | loader: () => 6 | import(/* webpackChunkName: "custom-link" */ '@components/CustomLink/Link'), 7 | loading: () =>
loading...
, 8 | delay: 0, 9 | }) 10 | -------------------------------------------------------------------------------- /packages/codemod/transforms/__testfixtures__/react-loadable-to-loadable-component_arrow-no-params.output.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import loadable from '@loadable/component' 3 | 4 | const CustomLinkLoadable = loadable(() => 5 | import(/* webpackChunkName: "custom-link" */ '@components/CustomLink/Link'), { 6 | fallback: (() =>
loading...
)(), 7 | }) 8 | -------------------------------------------------------------------------------- /packages/codemod/transforms/__testfixtures__/react-loadable-to-loadable-component_arrow-w-params.input.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import Loadable from 'react-loadable' 3 | 4 | const CustomLinkLoadable = Loadable({ 5 | loader: () => 6 | import(/* webpackChunkName: "custom-link" */ '@components/CustomLink/Link'), 7 | loading: (props) => { 8 | if (props.error || props.timedOut) { 9 | throw new Error('Failed to load custom link chunk') 10 | } else if (props.loading) { 11 | return
loading...
; 12 | } 13 | }, 14 | delay: 0, 15 | }) 16 | -------------------------------------------------------------------------------- /packages/codemod/transforms/__testfixtures__/react-loadable-to-loadable-component_arrow-w-params.output.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import loadable from '@loadable/component' 3 | 4 | const CustomLinkLoadable = loadable(() => 5 | import(/* webpackChunkName: "custom-link" */ '@components/CustomLink/Link'), { 6 | fallback: (props => { 7 | if (props.error || props.timedOut) { 8 | throw new Error('Failed to load custom link chunk') 9 | } else if (props.loading) { 10 | return
loading...
; 11 | } 12 | })({ 13 | pastDelay: true, 14 | error: false, 15 | timedOut: false, 16 | }), 17 | }) 18 | -------------------------------------------------------------------------------- /packages/codemod/transforms/__testfixtures__/react-loadable-to-loadable-component_expr.input.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import Loadable from 'react-loadable' 3 | 4 | const Loading = props => { 5 | if (props.error || props.timedOut) { 6 | throw new Error('Failed to load custom link chunk') 7 | } else { 8 | return null 9 | } 10 | } 11 | 12 | const CustomLinkLoadable = Loadable({ 13 | loader: () => 14 | import(/* webpackChunkName: "custom-link" */ '@components/CustomLink/Link'), 15 | loading: Loading, 16 | delay: 0, 17 | }) 18 | -------------------------------------------------------------------------------- /packages/codemod/transforms/__testfixtures__/react-loadable-to-loadable-component_expr.output.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import loadable from '@loadable/component' 3 | 4 | const Loading = props => { 5 | if (props.error || props.timedOut) { 6 | throw new Error('Failed to load custom link chunk') 7 | } else { 8 | return null 9 | } 10 | } 11 | 12 | const CustomLinkLoadable = loadable(() => 13 | import(/* webpackChunkName: "custom-link" */ '@components/CustomLink/Link'), { 14 | fallback: Loading({ 15 | pastDelay: true, 16 | error: false, 17 | timedOut: false, 18 | }), 19 | }) 20 | -------------------------------------------------------------------------------- /packages/codemod/transforms/__tests__/react-loadable-to-loadable-component-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff() 2 | 3 | const { defineTest } = require('jscodeshift/dist/testUtils') 4 | 5 | defineTest( 6 | __dirname, 7 | 'react-loadable-to-loadable-component', 8 | null, 9 | 'react-loadable-to-loadable-component_expr', 10 | ) 11 | defineTest( 12 | __dirname, 13 | 'react-loadable-to-loadable-component', 14 | null, 15 | 'react-loadable-to-loadable-component_arrow-no-params', 16 | ) 17 | defineTest( 18 | __dirname, 19 | 'react-loadable-to-loadable-component', 20 | null, 21 | 'react-loadable-to-loadable-component_arrow-w-params', 22 | ) 23 | -------------------------------------------------------------------------------- /packages/codemod/transforms/react-loadable-to-loadable-component.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | /* eslint-disable no-console */ 3 | const chalk = require('chalk') 4 | 5 | const invokeWithMockedUpProp = (jscodeshift, file, prop) => { 6 | // We invoke the function previously passed as `loading` to react-loadable with this props 7 | // { 8 | // pastDelay: true, 9 | // error: false, 10 | // timedOut: false, 11 | // } 12 | const j = jscodeshift 13 | 14 | const defaultPropsObjProperties = [] 15 | 16 | defaultPropsObjProperties.push( 17 | j.objectProperty(j.identifier('pastDelay'), j.booleanLiteral(true)), 18 | ) 19 | defaultPropsObjProperties.push( 20 | j.objectProperty(j.identifier('error'), j.booleanLiteral(false)), 21 | ) 22 | defaultPropsObjProperties.push( 23 | j.objectProperty(j.identifier('timedOut'), j.booleanLiteral(false)), 24 | ) 25 | 26 | const defaultPropsObj = j.objectExpression(defaultPropsObjProperties) 27 | 28 | const callExpr = j.callExpression(prop.value, [defaultPropsObj]) 29 | 30 | prop.value = callExpr 31 | 32 | console.warn( 33 | chalk.yellow( 34 | `[WARN] '${file.path}' has some react-loadable specific logic in it. We could not codemod while keeping all the behaviors the same. Please check this file manually.`, 35 | ), 36 | ) 37 | } 38 | 39 | module.exports = (file, api) => { 40 | const { source } = file 41 | const { jscodeshift: j } = api 42 | 43 | const root = j(source) 44 | 45 | // Rename `import Loadable from 'react-loadable';` to `import loadable from '@loadable/component'; 46 | root.find(j.ImportDeclaration).forEach(({ node }) => { 47 | if ( 48 | node.specifiers[0] && 49 | node.specifiers[0].local.name === 'Loadable' && 50 | node.source.value === 'react-loadable' 51 | ) { 52 | node.specifiers[0].local.name = 'loadable' 53 | node.source.value = '@loadable/component' 54 | } 55 | }) 56 | 57 | // Change Loadable({ ... }) invocation to loadable(() => {}, { ... }) invocation 58 | root 59 | .find(j.CallExpression, { callee: { name: 'Loadable' } }) 60 | .forEach(path => { 61 | const { node } = path 62 | const initialArgsProps = node.arguments[0].properties 63 | let loader // this will be a function returning a dynamic import promise 64 | 65 | // loop through the first argument (object) passed to `Loadable({ ... })` 66 | const newProps = initialArgsProps 67 | .map(prop => { 68 | if (prop.key.name === 'loader') { 69 | /** 70 | * In react-loadable, this is the function that returns a dynamic import 71 | * We'll keep it to `loader` variable for now, and remove it from the arg object 72 | */ 73 | loader = prop.value 74 | 75 | return undefined 76 | } 77 | 78 | if (prop.key.name === 'loading') { 79 | prop.key.name = 'fallback' // rename to fallback 80 | 81 | /** 82 | * react-loadable accepts a Function that returns JSX as the `loading` arg. 83 | * @loadable/component accepts a React.Element (what returned from React.createElement() calls) 84 | * 85 | */ 86 | if (prop.value.type === 'ArrowFunctionExpression') { 87 | // if it's an ArrowFunctionExpression like `() =>
loading...
`, 88 | 89 | if ( 90 | (prop.value.params && prop.value.params.length > 0) || 91 | prop.value.type === 'Identifier' 92 | ) { 93 | // If the function accept props, we can invoke it and pass it a mocked-up props to get the component to 94 | // a should-be-acceptable default state, while also logs out a warning. 95 | // { 96 | // pastDelay: true, 97 | // error: false, 98 | // timedOut: false, 99 | // } 100 | 101 | invokeWithMockedUpProp(j, file, prop) 102 | } else { 103 | // If the function doesn't accept any params, we can safely just invoke it directly 104 | // we can change it to `(() =>
loading...
)()` 105 | const callExpr = j.callExpression(prop.value, []) 106 | 107 | prop.value = callExpr 108 | } 109 | } else if (prop.value.type === 'Identifier') { 110 | // if it's an identifier like `Loading`, let's just invoke it with a mocked-up props 111 | invokeWithMockedUpProp(j, file, prop) 112 | } 113 | 114 | return prop 115 | } 116 | 117 | // for all other props, just remove them 118 | return undefined 119 | }) 120 | .filter(Boolean) 121 | 122 | // add the function that return a dynamic import we stored earlier as the first argument to `loadable()` call 123 | node.arguments.unshift(loader) 124 | node.arguments[1].properties = newProps 125 | node.callee.name = 'loadable' 126 | }) 127 | 128 | return root.toSource({ quote: 'single', trailingComma: true }) 129 | } 130 | 131 | module.exports.parser = 'babylon' 132 | -------------------------------------------------------------------------------- /packages/component/.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | .* 3 | rollup.config.js -------------------------------------------------------------------------------- /packages/component/.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "dist/cjs/loadable.cjs.js": { 3 | "bundled": 16900, 4 | "minified": 7226, 5 | "gzipped": 2545 6 | }, 7 | "dist/esm/loadable.esm.mjs": { 8 | "bundled": 16517, 9 | "minified": 6917, 10 | "gzipped": 2490, 11 | "treeshaked": { 12 | "rollup": { 13 | "code": 259, 14 | "import_statements": 259 15 | }, 16 | "webpack": { 17 | "code": 5764 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/component/README.md: -------------------------------------------------------------------------------- 1 | # @loadable/component 2 | 3 | Enable Code Splitting in your React application. 4 | 5 | ## Install 6 | 7 | ``` 8 | npm install @loadable/component 9 | ``` 10 | 11 | ## Documentation 12 | 13 | 👉 [See full documentation](https://loadable-components.com/) 14 | 15 | ## License 16 | 17 | MIT 18 | -------------------------------------------------------------------------------- /packages/component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@loadable/component", 3 | "description": "React code splitting made easy.", 4 | "version": "5.16.7", 5 | "main": "./dist/cjs/loadable.cjs.js", 6 | "module": "./dist/esm/loadable.esm.mjs", 7 | "exports": { 8 | ".": { 9 | "require": "./dist/cjs/loadable.cjs.js", 10 | "import": "./dist/esm/loadable.esm.mjs", 11 | "default": "./dist/cjs/loadable.cjs.js" 12 | } 13 | }, 14 | "repository": "git@github.com:gregberge/loadable-components.git", 15 | "author": "Greg Bergé ", 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "ssr", 22 | "webpack", 23 | "code-splitting", 24 | "react-router", 25 | "server-side-rendering", 26 | "dynamic-import", 27 | "react-loadable", 28 | "react-async-components" 29 | ], 30 | "engines": { 31 | "node": ">=8" 32 | }, 33 | "funding": { 34 | "type": "github", 35 | "url": "https://github.com/sponsors/gregberge" 36 | }, 37 | "license": "MIT", 38 | "scripts": { 39 | "prebuild": "shx rm -rf dist", 40 | "build": "cross-env rollup -c && yarn create-cjs-package-json", 41 | "create-cjs-package-json": "echo '{\"type\": \"commonjs\"}' > ./dist/cjs/package.json", 42 | "prepublishOnly": "yarn run build" 43 | }, 44 | "peerDependencies": { 45 | "react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 46 | }, 47 | "dependencies": { 48 | "@babel/runtime": "^7.12.18", 49 | "hoist-non-react-statics": "^3.3.1", 50 | "react-is": "^16.12.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/component/rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import nodeResolve from 'rollup-plugin-node-resolve' 3 | import babel from 'rollup-plugin-babel' 4 | import replace from 'rollup-plugin-replace' 5 | import commonjs from 'rollup-plugin-commonjs' 6 | import { terser } from 'rollup-plugin-terser' 7 | import pkg from './package.json' 8 | 9 | const input = 'src/index.js' 10 | const name = 'loadable' 11 | const globals = { 12 | react: 'React', 13 | 'hoist-non-react-statics': 'hoistNonReactStatics', 14 | } 15 | 16 | const external = id => !id.startsWith('.') && !id.startsWith('/') 17 | 18 | const getBabelOptions = ({ useESModules }) => ({ 19 | exclude: '**/node_modules/**', 20 | runtimeHelpers: true, 21 | presets: [ 22 | ['@babel/preset-env', { loose: true }], 23 | ['@babel/preset-react', { useBuiltIns: true }], 24 | ], 25 | plugins: [ 26 | '@babel/plugin-proposal-class-properties', 27 | 'babel-plugin-annotate-pure-calls', 28 | ['@babel/plugin-transform-runtime', { useESModules }], 29 | ], 30 | }) 31 | 32 | export default [ 33 | // umd 34 | { 35 | input, 36 | output: { 37 | file: `dist/loadable.js`, 38 | format: 'umd', 39 | name, 40 | globals, 41 | exports: 'named', 42 | sourcemap: false, 43 | }, 44 | external: Object.keys(globals), 45 | plugins: [ 46 | babel(getBabelOptions({ useESModules: true })), 47 | nodeResolve(), 48 | commonjs(), 49 | replace({ 'process.env.NODE_ENV': JSON.stringify('development') }), 50 | ], 51 | }, 52 | // min 53 | { 54 | input, 55 | output: { 56 | file: 'dist/loadable.min.js', 57 | format: 'umd', 58 | name, 59 | globals, 60 | exports: 'named', 61 | sourcemap: false, 62 | }, 63 | external: Object.keys(globals), 64 | plugins: [ 65 | babel(getBabelOptions({ useESModules: true })), 66 | nodeResolve(), 67 | commonjs(), 68 | replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), 69 | terser(), 70 | ], 71 | }, 72 | // cjs 73 | { 74 | input, 75 | output: { file: pkg.main, format: 'cjs', exports: 'named' }, 76 | external, 77 | plugins: [babel(getBabelOptions({ useESModules: false }))], 78 | }, 79 | // esm 80 | { 81 | input, 82 | output: { file: pkg.module, format: 'esm' }, 83 | external, 84 | plugins: [babel(getBabelOptions({ useESModules: true }))], 85 | }, 86 | ] 87 | -------------------------------------------------------------------------------- /packages/component/src/Context.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default React.createContext() 4 | -------------------------------------------------------------------------------- /packages/component/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import * as sharedInternals from './sharedInternals' 3 | import * as loadableExports from './loadable' 4 | import * as libraryExports from './library' 5 | 6 | const { loadable } = loadableExports 7 | loadable.lib = libraryExports.loadable 8 | 9 | const { lazy } = loadableExports 10 | lazy.lib = libraryExports.lazy 11 | 12 | export default loadable 13 | export { lazy } 14 | 15 | export { default as loadableReady } from './loadableReady' 16 | export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = sharedInternals 17 | -------------------------------------------------------------------------------- /packages/component/src/library.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define, react/no-multi-comp */ 2 | import createLoadable from './createLoadable' 3 | 4 | export const { loadable, lazy } = createLoadable({ 5 | onLoad(result, props) { 6 | if (result && props.forwardedRef) { 7 | if (typeof props.forwardedRef === 'function') { 8 | props.forwardedRef(result) 9 | } else { 10 | props.forwardedRef.current = result 11 | } 12 | } 13 | }, 14 | render({ result, props }) { 15 | if (props.children) { 16 | return props.children(result) 17 | } 18 | 19 | return null 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /packages/component/src/loadable.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define, react/no-multi-comp */ 2 | import React from 'react' 3 | import createLoadable from './createLoadable' 4 | import { defaultResolveComponent } from './resolvers' 5 | 6 | export const { loadable, lazy } = createLoadable({ 7 | defaultResolveComponent, 8 | render({ result: Component, props }) { 9 | return 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /packages/component/src/loadableReady.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle, camelcase */ 2 | /* eslint-env browser */ 3 | import { warn } from './util' 4 | import { getRequiredChunkKey } from './sharedInternals' 5 | import { LOADABLE_SHARED } from './shared' 6 | 7 | const BROWSER = typeof window !== 'undefined' 8 | 9 | export default function loadableReady( 10 | done = () => {}, 11 | { namespace = '', chunkLoadingGlobal = '__LOADABLE_LOADED_CHUNKS__' } = {}, 12 | ) { 13 | if (!BROWSER) { 14 | warn('`loadableReady()` must be called in browser only') 15 | done() 16 | return Promise.resolve() 17 | } 18 | 19 | let requiredChunks = null 20 | if (BROWSER) { 21 | const id = getRequiredChunkKey(namespace) 22 | const dataElement = document.getElementById(id) 23 | if (dataElement) { 24 | requiredChunks = JSON.parse(dataElement.textContent) 25 | 26 | const extElement = document.getElementById(`${id}_ext`) 27 | if (extElement) { 28 | const { namedChunks } = JSON.parse(extElement.textContent) 29 | namedChunks.forEach(chunkName => { 30 | LOADABLE_SHARED.initialChunks[chunkName] = true 31 | }) 32 | } else { 33 | // version mismatch 34 | throw new Error( 35 | 'loadable-component: @loadable/server does not match @loadable/component', 36 | ) 37 | } 38 | } 39 | } 40 | 41 | if (!requiredChunks) { 42 | warn( 43 | '`loadableReady()` requires state, please use `getScriptTags` or `getScriptElements` server-side', 44 | ) 45 | done() 46 | return Promise.resolve() 47 | } 48 | 49 | let resolved = false 50 | 51 | return new Promise(resolve => { 52 | window[chunkLoadingGlobal] = window[chunkLoadingGlobal] || [] 53 | const loadedChunks = window[chunkLoadingGlobal] 54 | const originalPush = loadedChunks.push.bind(loadedChunks) 55 | 56 | function checkReadyState() { 57 | if ( 58 | requiredChunks.every(chunk => 59 | loadedChunks.some(([chunks]) => chunks.indexOf(chunk) > -1), 60 | ) 61 | ) { 62 | if (!resolved) { 63 | resolved = true 64 | resolve() 65 | } 66 | } 67 | } 68 | 69 | loadedChunks.push = (...args) => { 70 | originalPush(...args) 71 | checkReadyState() 72 | } 73 | 74 | checkReadyState() 75 | }).then(done) 76 | } 77 | -------------------------------------------------------------------------------- /packages/component/src/resolvers.js: -------------------------------------------------------------------------------- 1 | export function defaultResolveComponent(loadedModule) { 2 | // eslint-disable-next-line no-underscore-dangle 3 | return loadedModule.__esModule 4 | ? loadedModule.default 5 | : loadedModule.default || loadedModule 6 | } 7 | -------------------------------------------------------------------------------- /packages/component/src/shared.js: -------------------------------------------------------------------------------- 1 | export const LOADABLE_SHARED = { 2 | initialChunks: {}, 3 | } 4 | -------------------------------------------------------------------------------- /packages/component/src/sharedInternals.js: -------------------------------------------------------------------------------- 1 | export { invariant } from './util' 2 | export { default as Context } from './Context' 3 | const LOADABLE_REQUIRED_CHUNKS_KEY = '__LOADABLE_REQUIRED_CHUNKS__' 4 | export function getRequiredChunkKey(namespace) { 5 | return `${namespace}${LOADABLE_REQUIRED_CHUNKS_KEY}` 6 | } 7 | -------------------------------------------------------------------------------- /packages/component/src/util.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | export function invariant(condition, message) { 4 | if (condition) return 5 | const error = new Error(`loadable: ${message}`) 6 | error.framesToPop = 1 7 | error.name = 'Invariant Violation' 8 | throw error 9 | } 10 | 11 | export function warn(message) { 12 | // eslint-disable-next-line no-console 13 | console.warn(`loadable: ${message}`) 14 | } 15 | -------------------------------------------------------------------------------- /packages/server/.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | .* 3 | __fixtures__ -------------------------------------------------------------------------------- /packages/server/.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "dist/cjs/loadable-server.cjs.js": { 3 | "bundled": 18673, 4 | "minified": 10153, 5 | "gzipped": 2842 6 | }, 7 | "dist/esm/loadable-server.esm.mjs": { 8 | "bundled": 18308, 9 | "minified": 9868, 10 | "gzipped": 2768, 11 | "treeshaked": { 12 | "rollup": { 13 | "code": 288, 14 | "import_statements": 244 15 | }, 16 | "webpack": { 17 | "code": 13266 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/server/README.md: -------------------------------------------------------------------------------- 1 | # @loadable/server 2 | 3 | ## Install 4 | 5 | ``` 6 | npm install @loadable/server 7 | ``` 8 | 9 | ## Documentation 10 | 11 | 👉 [See full documentation](https://loadable-components.com/) 12 | 13 | ## License 14 | 15 | MIT 16 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@loadable/server", 3 | "description": "Server utilities for loadable.", 4 | "version": "5.16.7", 5 | "type": "module", 6 | "main": "./dist/cjs/loadable-server.cjs.js", 7 | "module": "./dist/esm/loadable-server.esm.mjs", 8 | "exports": { 9 | ".": { 10 | "require": "./dist/cjs/loadable-server.cjs.js", 11 | "import": "./dist/esm/loadable-server.esm.mjs", 12 | "default": "./dist/cjs/loadable-server.cjs.js" 13 | } 14 | }, 15 | "repository": "git@github.com:gregberge/loadable-components.git", 16 | "author": "Greg Bergé ", 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "keywords": [ 21 | "loadable" 22 | ], 23 | "engines": { 24 | "node": ">=8" 25 | }, 26 | "funding": { 27 | "type": "github", 28 | "url": "https://github.com/sponsors/gregberge" 29 | }, 30 | "license": "MIT", 31 | "scripts": { 32 | "prebuild": "shx rm -rf dist", 33 | "build": "cross-env rollup -c && yarn create-cjs-package-json", 34 | "create-cjs-package-json": "echo '{\"type\": \"commonjs\"}' > ./dist/cjs/package.json", 35 | "prepublishOnly": "yarn run build", 36 | "update-fixtures": "yarn --cwd ../../examples/__fixtures__ build:webpack && rm -rf ./__fixtures__ && cp -R ../../examples/__fixtures__/target ./__fixtures__ " 37 | }, 38 | "peerDependencies": { 39 | "@loadable/component": "^5.0.1", 40 | "react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 41 | }, 42 | "devDependencies": { 43 | "@loadable/component": "^5.16.7" 44 | }, 45 | "dependencies": { 46 | "lodash": "^4.17.15" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/server/rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import nodeResolve from 'rollup-plugin-node-resolve' 3 | import babel from 'rollup-plugin-babel' 4 | import replace from 'rollup-plugin-replace' 5 | import commonjs from 'rollup-plugin-commonjs' 6 | import { terser } from 'rollup-plugin-terser' 7 | import pkg from './package.json' 8 | 9 | const input = 'src/index.js' 10 | const name = 'loadable' 11 | const globals = { 12 | react: 'React', 13 | 'hoist-non-react-statics': 'hoistNonReactStatics', 14 | } 15 | 16 | const external = id => !id.startsWith('.') && !id.startsWith('/') 17 | 18 | const getBabelOptions = ({ useESModules }) => ({ 19 | exclude: '**/node_modules/**', 20 | runtimeHelpers: true, 21 | presets: [ 22 | ['@babel/preset-env', { loose: true }], 23 | ['@babel/preset-react', { useBuiltIns: true }], 24 | ], 25 | plugins: [ 26 | '@babel/plugin-proposal-class-properties', 27 | 'babel-plugin-annotate-pure-calls', 28 | ['@babel/plugin-transform-runtime', { useESModules }], 29 | ], 30 | }) 31 | 32 | export default [ 33 | // cjs 34 | { 35 | input, 36 | output: { file: pkg.main, format: 'cjs', exports: 'named' }, 37 | external, 38 | plugins: [babel(getBabelOptions({ useESModules: false }))], 39 | }, 40 | // esm 41 | { 42 | input, 43 | output: { file: pkg.module, format: 'esm' }, 44 | external, 45 | plugins: [babel(getBabelOptions({ useESModules: true }))], 46 | }, 47 | ] 48 | -------------------------------------------------------------------------------- /packages/server/src/ChunkExtractorManager.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Context } from './sharedInternals.js' 3 | 4 | const ChunkExtractorManager = ({ extractor, children }) => ( 5 | {children} 6 | ) 7 | 8 | export default ChunkExtractorManager 9 | -------------------------------------------------------------------------------- /packages/server/src/index.js: -------------------------------------------------------------------------------- 1 | export { default as ChunkExtractorManager } from './ChunkExtractorManager.js' 2 | export { default as ChunkExtractor } from './ChunkExtractor.js' 3 | -------------------------------------------------------------------------------- /packages/server/src/sharedInternals.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '@loadable/component' 3 | 4 | const { 5 | invariant, 6 | Context, 7 | getRequiredChunkKey, 8 | } = __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED 9 | 10 | export { invariant, Context, getRequiredChunkKey } 11 | -------------------------------------------------------------------------------- /packages/server/src/util.js: -------------------------------------------------------------------------------- 1 | // Use __non_webpack_require__ to prevent Webpack from compiling it 2 | // when the server-side code is compiled with Webpack 3 | // eslint-disable-next-line camelcase, no-undef, global-require, import/no-dynamic-require, no-eval 4 | const getRequire = () => 5 | typeof __non_webpack_require__ !== 'undefined' 6 | ? __non_webpack_require__ 7 | : eval('require') 8 | 9 | export const clearModuleCache = moduleName => { 10 | const { cache } = getRequire() 11 | const m = cache[moduleName] 12 | if (m) { 13 | // remove self from own parents 14 | if (m.parent && m.parent.children) { 15 | m.parent.children = m.parent.children.filter(x => x !== m) 16 | } 17 | // remove self from own children 18 | if (m.children) { 19 | m.children.forEach(child => { 20 | if (child.parent && child.parent === m) { 21 | child.parent = null 22 | } 23 | }) 24 | } 25 | delete cache[moduleName] 26 | } 27 | } 28 | 29 | export const smartRequire = modulePath => { 30 | if (process.env.NODE_ENV !== 'production' && module.hot) { 31 | clearModuleCache(modulePath) 32 | } 33 | 34 | return getRequire()(modulePath) 35 | } 36 | 37 | export const joinURLPath = (publicPath, filename) => { 38 | if (publicPath.substr(-1) === '/') { 39 | return `${publicPath}${filename}` 40 | } 41 | 42 | return `${publicPath}/${filename}` 43 | } 44 | 45 | export const readJsonFileSync = (inputFileSystem, jsonFilePath) => { 46 | return JSON.parse(inputFileSystem.readFileSync(jsonFilePath)) 47 | } 48 | -------------------------------------------------------------------------------- /packages/server/src/util.test.js: -------------------------------------------------------------------------------- 1 | import { joinURLPath } from './util.js' 2 | 3 | describe('util', () => { 4 | describe('#joinURLPath', () => { 5 | it('should join paths with relative public path', () => { 6 | expect(joinURLPath('public', 'style.css')).toBe('public/style.css') 7 | expect(joinURLPath('public/', 'style.css')).toBe('public/style.css') 8 | }) 9 | 10 | it('should join paths starting with "/"', () => { 11 | expect(joinURLPath('/foo', 'style.css')).toBe('/foo/style.css') 12 | expect(joinURLPath('/', 'style.css')).toBe('/style.css') 13 | }) 14 | 15 | it('should join paths with absolute public path', () => { 16 | const publicPath = 'http://localhost:3001/public' 17 | 18 | expect(joinURLPath(publicPath, 'style.css')).toBe( 19 | `http://localhost:3001/public/style.css`, 20 | ) 21 | expect(joinURLPath(`${publicPath}/`, 'style.css')).toBe( 22 | `http://localhost:3001/public/style.css`, 23 | ) 24 | }) 25 | 26 | it('should join paths with protocol free public path', () => { 27 | const publicPath = '//127.0.0.1/public' 28 | expect(joinURLPath(publicPath, 'style.css')).toBe( 29 | `//127.0.0.1/public/style.css`, 30 | ) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /packages/webpack-plugin/.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | .* 3 | -------------------------------------------------------------------------------- /packages/webpack-plugin/README.md: -------------------------------------------------------------------------------- 1 | # @loadable/webpack-plugin 2 | 3 | This plugin is required only if you use Server Side Rendering in your application. [See `@loadable/server` for more information](https://loadable-components.com/docs/api-loadable-server/). 4 | 5 | ## Install 6 | 7 | ``` 8 | npm install --save-dev @loadable/webpack-plugin 9 | ``` 10 | 11 | ## Documentation 12 | 13 | 👉 [See full documentation](https://loadable-components.com/) 14 | 15 | ## License 16 | 17 | MIT 18 | -------------------------------------------------------------------------------- /packages/webpack-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@loadable/webpack-plugin", 3 | "description": "Webpack plugin for loadable (required for SSR).", 4 | "version": "5.15.2", 5 | "main": "lib/index.js", 6 | "repository": "git@github.com:gregberge/loadable-components.git", 7 | "author": "Greg Bergé ", 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "keywords": [ 12 | "loadable" 13 | ], 14 | "engines": { 15 | "node": ">=8" 16 | }, 17 | "funding": { 18 | "type": "github", 19 | "url": "https://github.com/sponsors/gregberge" 20 | }, 21 | "license": "MIT", 22 | "scripts": { 23 | "prebuild": "shx rm -rf lib", 24 | "build": "BUILD_TARGET=node babel --config-file ../../babel.config.js -d lib --ignore \"**/*.test.js\" src", 25 | "prepublishOnly": "yarn run build" 26 | }, 27 | "peerDependencies": { 28 | "webpack": ">=4.6.0" 29 | }, 30 | "dependencies": { 31 | "make-dir": "^3.0.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/webpack-plugin/src/index.js: -------------------------------------------------------------------------------- 1 | const nodePath = require('path') 2 | const fs = require('fs') 3 | const makeDir = require('make-dir') 4 | 5 | const name = '@loadable/webpack-plugin' 6 | 7 | class LoadablePlugin { 8 | constructor({ 9 | filename = 'loadable-stats.json', 10 | path, 11 | writeToDisk, 12 | outputAsset = true, 13 | chunkLoadingGlobal = '__LOADABLE_LOADED_CHUNKS__', 14 | } = {}) { 15 | this.opts = { filename, writeToDisk, outputAsset, path, chunkLoadingGlobal } 16 | 17 | // The Webpack compiler instance 18 | this.compiler = null 19 | } 20 | 21 | handleEmit = compilation => { 22 | const stats = compilation.getStats().toJson({ 23 | all: false, 24 | assets: true, 25 | cachedAssets: true, 26 | chunks: false, 27 | chunkGroups: true, 28 | chunkGroupChildren: true, 29 | hash: true, 30 | ids: true, 31 | outputPath: true, 32 | publicPath: true, 33 | }) 34 | 35 | stats.generator = 'loadable-components' 36 | 37 | // we don't need all chunk information, only a type 38 | stats.chunks = [...compilation.chunks].map(chunk => { 39 | return { 40 | id: chunk.id, 41 | files: [...chunk.files], 42 | } 43 | }) 44 | 45 | // update namedChunkGroups with integrity from webpack-subresource-integrity if available 46 | Object.values(stats.namedChunkGroups).forEach(namedChunkGroup => { 47 | namedChunkGroup.assets.forEach(namedChunkGroupAsset => { 48 | if (!namedChunkGroupAsset.integrity) { 49 | const asset = 50 | stats.assets.find(a => a.name === namedChunkGroupAsset.name) || {} 51 | if (asset.integrity) { 52 | namedChunkGroupAsset.integrity = asset.integrity 53 | } 54 | } 55 | }) 56 | }) 57 | 58 | const result = JSON.stringify(stats, null, 2) 59 | 60 | if (this.opts.writeToDisk) { 61 | this.writeAssetsFile(result) 62 | } 63 | 64 | if (this.opts.outputAsset) { 65 | return { 66 | source() { 67 | return result 68 | }, 69 | size() { 70 | return result.length 71 | }, 72 | } 73 | } 74 | 75 | return null 76 | } 77 | 78 | /** 79 | * Write Assets Manifest file 80 | * @method writeAssetsFile 81 | */ 82 | writeAssetsFile = manifest => { 83 | const outputFolder = 84 | this.opts.writeToDisk.filename || this.compiler.options.output.path 85 | 86 | const outputFile = nodePath.resolve(outputFolder, this.opts.filename) 87 | 88 | try { 89 | if (!fs.existsSync(outputFolder)) { 90 | makeDir.sync(outputFolder) 91 | } 92 | } catch (err) { 93 | if (err.code !== 'EEXIST') { 94 | throw err 95 | } 96 | } 97 | 98 | fs.writeFileSync(outputFile, manifest) 99 | } 100 | 101 | apply(compiler) { 102 | this.compiler = compiler 103 | 104 | const version = 'jsonpFunction' in compiler.options.output ? 4 : 5 105 | 106 | // Add a custom chunk loading callback 107 | if (version === 4) { 108 | compiler.options.output.jsonpFunction = this.opts.chunkLoadingGlobal 109 | } else { 110 | compiler.options.output.chunkLoadingGlobal = this.opts.chunkLoadingGlobal 111 | } 112 | 113 | if (this.opts.outputAsset || this.opts.writeToDisk) { 114 | if (version === 4) { 115 | // webpack 4 116 | compiler.hooks.emit.tap(name, compilation => { 117 | const asset = this.handleEmit(compilation) 118 | if (asset) { 119 | compilation.assets[this.opts.filename] = asset 120 | } 121 | }) 122 | } else { 123 | // webpack 5 124 | compiler.hooks.make.tap(name, compilation => { 125 | compilation.hooks.processAssets.tap( 126 | { 127 | name, 128 | stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT, 129 | }, 130 | () => { 131 | const asset = this.handleEmit(compilation) 132 | if (asset) { 133 | compilation.emitAsset(this.opts.filename, asset) 134 | } 135 | }, 136 | ) 137 | }) 138 | } 139 | } 140 | } 141 | } 142 | 143 | module.exports = LoadablePlugin 144 | module.exports.default = LoadablePlugin 145 | -------------------------------------------------------------------------------- /resources/loadable-components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregberge/loadable-components/18544decdd0e9ecb3e85772708e1f2726430fdeb/resources/loadable-components.png -------------------------------------------------------------------------------- /resources/loadable-components.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregberge/loadable-components/18544decdd0e9ecb3e85772708e1f2726430fdeb/resources/loadable-components.sketch -------------------------------------------------------------------------------- /scripts/copy-stats-fixture.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const stats = fs.readFileSync( 5 | '../examples/server-side-rendering/public/dist/node/loadable-stats.json', 6 | 'utf-8', 7 | ) 8 | const localPath = path.resolve(__dirname, '..') 9 | // write cleaned from PII data 10 | fs.writeFileSync( 11 | '../packages/server/__fixtures__/stats.json', 12 | stats.split(localPath).join('../..'), 13 | ) 14 | -------------------------------------------------------------------------------- /scripts/git-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 6 | 7 | yarn run lerna version --conventional-prerelease --preid from-git --no-git-tag-version --no-push --allow-branch $BRANCH --yes 8 | git add . 9 | git commit -m "Publish to git" 10 | 11 | for DIR in $(yarn run -s lerna changed --parseable); do 12 | ( 13 | VERSION=$(cat "${DIR}/package.json" | jq -r '.version') 14 | NAME=$(cat "${DIR}/package.json" | jq -r '.name') 15 | 16 | ( 17 | cd "$DIR" 18 | yarn run prepublishOnly 19 | ) 20 | yarn run npm-publish-git --dir "$DIR" --tag "${NAME}/${BRANCH}/${VERSION}" 21 | ) 22 | done 23 | -------------------------------------------------------------------------------- /scripts/prepare.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xe 4 | 5 | echo "building example" 6 | (cd "$( dirname "${BASH_SOURCE[0]}" )/../examples/server-side-rendering" && yarn && yarn build) 7 | 8 | echo "generating fixtures" 9 | (cd "$( dirname "${BASH_SOURCE[0]}" )" && node ./copy-stats-fixture.js) 10 | 11 | echo "done" -------------------------------------------------------------------------------- /website/.eslintignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | node_modules/ 3 | /public/ -------------------------------------------------------------------------------- /website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-unresolved": "off", 4 | "import/extensions": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | node_modules/ 3 | /public/ -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Loadable Components website 2 | 3 | [Documentation site](https://loadable-components.com/) for [loadable components](https://github.com/gregberge/loadable-components). This website is running on [gatsbyjs](https://gatsbyjs.com/). 4 | 5 | ## Getting Started 6 | 7 | To install and run the docs site locally: 8 | 9 | ```bash 10 | yarn 11 | yarn dev 12 | ``` 13 | 14 | Then, open your favorite browser to [localhost:8000](http://localhost:8000/). GraphiQL runs at [localhost:8000/\_\_\_graphql](http://localhost:8000/___graphql). 15 | 16 | ## Contributing 17 | 18 | Build the site to test locally. 19 | 20 | ```bash 21 | yarn build 22 | ``` 23 | 24 | Serve the build. 25 | 26 | ```bash 27 | yarn serve 28 | ``` 29 | 30 | Then, open your favorite browser to [localhost:9000](http://localhost:9000/) to verify everything looks correct. 31 | -------------------------------------------------------------------------------- /website/_redirects: -------------------------------------------------------------------------------- 1 | https://loadable-components.netlify.com/* https://loadable-components.com/:splat 301! -------------------------------------------------------------------------------- /website/gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | { 4 | resolve: 'smooth-doc', 5 | options: { 6 | name: 'Loadable Components', 7 | slug: 'loadable-components', 8 | author: 'Greg Bergé', 9 | description: 'The recommended Code Splitting library for React.', 10 | siteUrl: 'https://loadable-components.com', 11 | github: 'https://github.com/gregberge/loadable-components', 12 | menu: ['Introduction', 'Guides', 'API'], 13 | nav: [{ title: 'Docs', url: '/docs/getting-started/' }], 14 | carbonAdUrl: 15 | '//cdn.carbonads.com/carbon.js?serve=CE7I5K3U&placement=loadable-componentscom', 16 | googleAnalytics: 'UA-154156493-1', 17 | algoliaDocSearch: { 18 | apiKey: 'e6a731577a7b94aefdbb1fb7dcc71e68', 19 | indexName: 'smooth-code-loadable-components', 20 | }, 21 | }, 22 | }, 23 | { 24 | resolve: '@bundle-analyzer/gatsby-plugin', 25 | options: { 26 | token: '385aa427a43b4e840d763a07f672131a57decf45', 27 | }, 28 | }, 29 | ], 30 | } 31 | -------------------------------------------------------------------------------- /website/gatsby-node.js: -------------------------------------------------------------------------------- 1 | module.exports.createPages = ({ actions }) => { 2 | actions.createRedirect({ 3 | fromPath: `/docs/`, 4 | toPath: `/docs/getting-started/`, 5 | redirectInBrowser: true, 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "gatsby build && cp _redirects public/", 5 | "dev": "gatsby develop", 6 | "serve": "gatsby serve" 7 | }, 8 | "dependencies": { 9 | "@bundle-analyzer/gatsby-plugin": "^0.5.1", 10 | "@xstyled/styled-components": "^1.17.1", 11 | "gatsby": "^2.24.2", 12 | "react": "^16.13.1", 13 | "react-dom": "^16.13.1", 14 | "react-helmet": "^6.1.0", 15 | "smooth-doc": "^4.0.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /website/src/images/home-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregberge/loadable-components/18544decdd0e9ecb3e85772708e1f2726430fdeb/website/src/images/home-logo-dark.png -------------------------------------------------------------------------------- /website/src/images/home-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregberge/loadable-components/18544decdd0e9ecb3e85772708e1f2726430fdeb/website/src/images/home-logo.png -------------------------------------------------------------------------------- /website/src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregberge/loadable-components/18544decdd0e9ecb3e85772708e1f2726430fdeb/website/src/images/logo.png -------------------------------------------------------------------------------- /website/src/images/social.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregberge/loadable-components/18544decdd0e9ecb3e85772708e1f2726430fdeb/website/src/images/social.jpg -------------------------------------------------------------------------------- /website/src/pages/docs/api-loadable-webpack-plugin.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: API 3 | title: '@loadable/webpack-plugin' 4 | order: 30 5 | --- 6 | 7 | # @loadable/webpack-plugin 8 | 9 | ### LoadablePlugin 10 | 11 | Create a webpack loadable plugin. 12 | 13 | | Arguments | Description | 14 | | ------------------------------ | -------------------------------------------------------------------------------------------- | 15 | | `options` | Optional options | 16 | | `options.filename` | Stats filename (default to `loadable-stats.json`) | 17 | | `options.outputAsset` | Always write stats file to the `output.path` directory. Defaults to `true` | 18 | | `options.writeToDisk` | Accepts `boolean` or `object`. Always write stats file to disk. Default to `false`. | 19 | | `options.writeToDisk.filename` | Write assets to disk at given `filename` location | 20 | | `options.chunkLoadingGlobal` | Overrides Webpack's `chunkLoadingGlobal` allowing multiple Webpack runtimes on the same page | 21 | 22 | ```js 23 | new LoadablePlugin({ filename: 'stats.json', writeToDisk: true }) 24 | ``` 25 | 26 | > Writing file to disk can be useful if you are using `razzle` or `webpack-dev-server`. 27 | -------------------------------------------------------------------------------- /website/src/pages/docs/babel-plugin.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guides 3 | title: Babel plugin 4 | order: 70 5 | --- 6 | 7 | # Babel plugin 8 | 9 | This plugin adds support for [Server Side Rendering](/docs/server-side-rendering) and automatic chunk names. 10 | 11 | ## Usage 12 | 13 | Install the babel plugin first: 14 | 15 | ```bash 16 | npm install --save-dev @loadable/babel-plugin 17 | ``` 18 | 19 | Then add it to your babel configuration like so: 20 | 21 | ```json 22 | { 23 | "plugins": ["@loadable/babel-plugin"] 24 | } 25 | ``` 26 | 27 | ## Transformation 28 | 29 | The plugin transforms your code to be ready for Server Side Rendering, it turns a loadable call: 30 | 31 | ```js 32 | import loadable from '@loadable/component' 33 | 34 | const OtherComponent = loadable(() => import('./OtherComponent')) 35 | ``` 36 | 37 | into another one with some informations required for Server Side Rendering: 38 | 39 | ```js 40 | import loadable from '@loadable/component' 41 | const OtherComponent = loadable({ 42 | chunkName() { 43 | return 'OtherComponent' 44 | }, 45 | 46 | isReady(props) { 47 | if (typeof __webpack_modules__ !== 'undefined') { 48 | return !!__webpack_modules__[this.resolve(props)] 49 | } 50 | 51 | return false 52 | }, 53 | 54 | requireAsync: () => 55 | import(/* webpackChunkName: "OtherComponent" */ 56 | './OtherComponent'), 57 | 58 | requireSync(props) { 59 | const id = this.resolve(props) 60 | 61 | if (typeof __webpack_require__ !== 'undefined') { 62 | return __webpack_require__(id) 63 | } 64 | 65 | return eval('module.require')(id) 66 | }, 67 | 68 | resolve() { 69 | if (require.resolveWeak) { 70 | return require.resolveWeak('./OtherComponent') 71 | } 72 | 73 | return require('path').resolve(__dirname, './OtherComponent') 74 | }, 75 | }) 76 | ``` 77 | 78 | As you can see the "webpackChunkName" annotation is automatically added. 79 | 80 | On client side, the two code are completely compatible. 81 | 82 | Please note that babel must not be configured [to strip comments](https://babeljs.io/docs/en/options#comments), since the chunk name is defined in a comment. 83 | 84 | ## Loadable Configuration (beta) 85 | > available since 5.16.0 86 | 87 | Sometimes you need to wrap loadable with your own custom logic. There are many use cases for it, from injecting telemetry to hiding external libraries behind facade. 88 | By default `loadable-components` are configured to transform dynamic imports used only inside loadable helpers, but can be configured to instrument any other function of your choice. 89 | ```json 90 | { 91 | "plugins": ["@loadable/babel-plugin", { 92 | "signatures": [ 93 | { "name": "default", "from": "myLoadableWrapper" } 94 | { "name": "myLoadableHelper", "from": "myLoadableWrapper" } 95 | ] 96 | }] 97 | } 98 | ``` 99 | ```tsx 100 | import {myLoadableHelper} from "myLoadableWrapper"; 101 | const Loadable = myLoadableHelper(() => import("./MyComponent")); 102 | // will behave similar to 103 | import loadable from '@loadable/component' 104 | const OtherComponent = loadable(() => import('./OtherComponent')) 105 | ``` 106 | 107 | ## Loadable detection (deprecated) 108 | > Please dont use this feature and prefer Loadable Configuration instead 109 | 110 | The detection of a loadable component is based on the keyword "loadable". It is an opinionated choice, it gives you flexibility but it could also be restrictive. 111 | 112 | This code will not be transformed by the babel plugin: 113 | 114 | ```js 115 | import load from '@loadable/component' 116 | const OtherComponent = load(() => import('./OtherComponent')) 117 | ``` 118 | 119 | The `load` function is not detected, you have to name it `loadable`. 120 | 121 | It is restrictive, yes but it could gives you some flexibility. You can create your own loadable function. In the following example we create a custom loadable function with a custom fallback: 122 | 123 | ```js 124 | import baseLoadable from '@loadable/component' 125 | 126 | function loadable(func) { 127 | return baseLoadable(func, { fallback:
Loading...
}) 128 | } 129 | 130 | const OtherComponent = loadable(() => import('./OtherComponent')) 131 | ``` 132 | 133 | ## Magic comments 134 | 135 | To gives you flexibility and portability, the babel plugin supports magic comment. This way you can create portable "load" functions. To create a "load" function, you have to add `/* #__LOADABLE__ */` comment above the declaration of your function (variable or property): 136 | 137 | ```js 138 | const loadOther = /* #__LOADABLE__ */ () => import('./OtherComponent') 139 | 140 | const OtherComponent = loadable(loadOther) 141 | ``` 142 | 143 | The `loadOther` function will be transformed into an object, this way you can manipulate function and it is still portable! 144 | -------------------------------------------------------------------------------- /website/src/pages/docs/code-splitting.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Introduction 3 | title: Code Splitting? 4 | order: 10 5 | --- 6 | 7 | # Code Splitting? 8 | 9 | Code Splitting is an efficient way to reduce your bundle size: it speeds up the loading of your application and reduces the payload size of your application. 10 | 11 | Bundling is great, but as your app grows, your bundle will grow too. Especially if you are including large third-party libraries. You need to keep an eye on the code you are including in your bundle so that you don’t accidentally make it so large that your app takes a long time to load. 12 | 13 | To avoid winding up with a large bundle, it’s good to get ahead of the problem and start “splitting” your bundle. Code-Splitting is a feature supported by bundlers like Webpack and Browserify (via factor-bundle) which can create multiple bundles that can be dynamically loaded at runtime. 14 | 15 | Code-splitting your app can help you “lazy-load” just the things that are currently needed by the user, which can dramatically improve the performance of your app. While you haven’t reduced the overall amount of code in your app, you’ve avoided loading code that the user may never need, and reduced the amount of code needed during the initial load. 16 | 17 | ## `import()` 18 | 19 | The best way to introduce code-splitting into your app is through the dynamic `import()` syntax. 20 | 21 | **Before:** 22 | 23 | ```js 24 | import { add } from './math' 25 | 26 | console.log(add(16, 26)) 27 | ``` 28 | 29 | **After:** 30 | 31 | ```js 32 | import('./math').then(math => { 33 | console.log(math.add(16, 26)) 34 | }) 35 | ``` 36 | 37 | > Note: 38 | > The dynamic import() syntax is officially a part of ECMAScript 2020 specification. However to use this syntax in prior language versions you still need to bundle your code with Webpack. 39 | 40 | When Webpack comes across this syntax, it automatically starts code-splitting your app. 41 | 42 | If you’re setting up Webpack yourself, you’ll probably want to read [Webpack’s guide on code splitting](https://webpack.js.org/guides/code-splitting/). 43 | 44 | When using Babel, you’ll need to make sure that Babel can parse the dynamic import syntax but is not transforming it. For that you will need [@babel/plugin-syntax-dynamic-import](https://www.npmjs.com/package/@babel/plugin-syntax-dynamic-import). 45 | 46 | ## Named `import()` 47 | 48 | Webpack's default behaviour, is to name them as `x.js` where x is an incremental number depending on how many dynamic 49 | chunks you are importing in your code. 50 | 51 | This will give a poor view of which file is loading what code. 52 | 53 | To fix that, webpack introduced magic comments, with which a chunk can be named as follows (ie: `math.js`). 54 | 55 | ```js 56 | import(/* webpackChunkName: "math" */ './math').then(math => { 57 | console.log(math.add(16, 26)) 58 | }) 59 | ``` 60 | 61 | >NOTE: 62 | >When using [Server Side Rendering](/docs/server-side-rendering/), make sure comment and file path are exactly in the same order as above. 63 | 64 | 65 | ## Code Splitting + React 66 | 67 | React supports code splitting out of the box with [`React.lazy`](https://reactjs.org/docs/code-splitting.html#reactlazy). However it has [some limitations](/docs/loadable-vs-react-lazy), this is why `@loadable/component` exists. 68 | 69 | In a React application, most of the time you want to split your components. Splitting a component implies the ability to wait for this component to be loaded (showing a fallback during loading) but also to handle errors. 70 | 71 | **Example of component splitting:** 72 | 73 | ```js 74 | import loadable from '@loadable/component' 75 | 76 | const OtherComponent = loadable(() => import('./OtherComponent')) 77 | 78 | function MyComponent() { 79 | return ( 80 |
81 | 82 |
83 | ) 84 | } 85 | ``` 86 | -------------------------------------------------------------------------------- /website/src/pages/docs/component-splitting.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guides 3 | title: Component Splitting 4 | order: 5 5 | --- 6 | 7 | # Component Splitting 8 | 9 | `loadable` lets you easily import components and reuse it in your code. 10 | 11 | ```js 12 | import loadable from '@loadable/component' 13 | 14 | const OtherComponent = loadable(() => import('./OtherComponent')) 15 | 16 | function MyComponent() { 17 | return ( 18 |
19 | 20 |
21 | ) 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /website/src/pages/docs/delay.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guides 3 | title: Delay 4 | order: 50 5 | --- 6 | 7 | # Delay 8 | 9 | To avoid flashing a loader if the loading is very fast, you could implement a minimum delay. There is no built-in API in `@loadable/component` but you could do it using [`p-min-delay`](https://github.com/sindresorhus/p-min-delay). 10 | 11 | ```js 12 | import loadable from '@loadable/component' 13 | import pMinDelay from 'p-min-delay' 14 | 15 | // Wait a minimum of 200ms before loading home. 16 | export const OtherComponent = loadable(() => 17 | pMinDelay(import('./OtherComponent'), 200) 18 | ) 19 | ``` 20 | -------------------------------------------------------------------------------- /website/src/pages/docs/dynamic-import.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guides 3 | title: Full dynamic import 4 | order: 20 5 | --- 6 | 7 | # Full dynamic import 8 | 9 | Webpack accepts [full dynamic imports](https://webpack.js.org/api/module-methods/#import-), you can use them to create a reusable Loadable Component. 10 | 11 | ```js 12 | import loadable from '@loadable/component' 13 | 14 | const AsyncPage = loadable(props => import(`./${props.page}`)) 15 | 16 | function MyComponent() { 17 | return ( 18 |
19 | 20 | 21 |
22 | ) 23 | } 24 | ``` 25 | 26 | ## Use a dynamic property 27 | 28 | If you use [Babel plugin](/docs/babel-plugin/), dynamic properties are supported out of the box. Else you have to add `cacheKey` function: it takes props and returns a cache key. 29 | 30 | ```js 31 | import loadable from '@loadable/component' 32 | 33 | const AsyncPage = loadable(props => import(`./${props.page}`), { 34 | cacheKey: props => props.page, 35 | }) 36 | 37 | function MyComponent() { 38 | const [page, setPage] = useState('Home') 39 | return ( 40 |
41 | 42 | 43 | {page && } 44 |
45 | ) 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /website/src/pages/docs/error-boundaries.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guides 3 | title: Error boundaries 4 | order: 45 5 | --- 6 | 7 | # Error Boundaries 8 | 9 | If the other module fails to load (for example, due to network failure), it will trigger an error. You can handle these errors to show a nice user experience and manage recovery with [Error Boundaries](https://reactjs.org/docs/error-boundaries.html). Once you’ve created your Error Boundary, you can use it anywhere above your lazy components to display an error state when there’s a network error. 10 | 11 | ```js 12 | import MyErrorBoundary from './MyErrorBoundary' 13 | const OtherComponent = loadable(() => import('./OtherComponent')) 14 | const AnotherComponent = loadable(() => import('./AnotherComponent')) 15 | 16 | const MyComponent = () => ( 17 |
18 | 19 |
20 | 21 | 22 |
23 |
24 |
25 | ) 26 | ``` 27 | -------------------------------------------------------------------------------- /website/src/pages/docs/fallback.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guides 3 | title: Fallback without Suspense 4 | order: 40 5 | --- 6 | 7 | # Fallback without Suspense 8 | 9 | You can specify a `fallback` in `loadable` options. 10 | 11 | ```js 12 | const OtherComponent = loadable(() => import('./OtherComponent'), { 13 | fallback:
Loading...
, 14 | }) 15 | 16 | function MyComponent() { 17 | return ( 18 |
19 | 20 |
21 | ) 22 | } 23 | ``` 24 | 25 | You can also specify a `fallback` in props: 26 | 27 | ```js 28 | const OtherComponent = loadable(() => import('./OtherComponent')) 29 | 30 | function MyComponent() { 31 | return ( 32 |
33 | Loading...
} /> 34 | 35 | ) 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /website/src/pages/docs/faq.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guides 3 | title: Frequently Asked Questions 4 | order: 80 5 | --- 6 | 7 | # Frequently Asked Questions 8 | 9 | ## Which react versions are supported? 10 | 11 | As defined in peer dependencies, Loadable Components supports React v16.3+. 12 | 13 | ## Which webpack versions are supported? 14 | 15 | As defined in peer dependencies, Loadable Components supports webpack v4.6+. 16 | 17 | ## Which browsers are supported? 18 | 19 | Loadable Components supports the same set of browsers as the current React version. 20 | 21 | - v5.x (React v16.3+): IE11, IE 9+ (with Map + Set polyfills), all evergreen browsers 22 | 23 | Evergreen browsers include Chrome and Firefox (and derivatives) as they can be updated regardless of operating system version. Edge and Safari should both also work fine since all versions for the last several years support the relevant APIs. 24 | 25 | ## Can I use Loadable Components with server-side rendering of React on top of Ruby on Rails? 26 | 27 | Yes. It works with [React on Rails](https://github.com/shakacode/react_on_rails), but it requires [React on Rails Pro](https://www.shakacode.com/react-on-rails-pro/). High traffic sites using React on Rails Pro with Loadable Components are [popmenu.com](https://get.popmenu.com/) and [egghead.io](https://egghead.io/). 28 | -------------------------------------------------------------------------------- /website/src/pages/docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Introduction 3 | title: Getting Started 4 | order: 5 5 | --- 6 | 7 | # Getting Started 8 | 9 | Follow these few steps to get ready with `@loadable/component`. 10 | 11 | ## Installation 12 | 13 | Installing `@loadable/component` only takes a single command and you're ready to roll: 14 | 15 | ```bash 16 | npm install @loadable/component 17 | # or use yarn 18 | yarn add @loadable/component 19 | ``` 20 | 21 | > `@loadable/babel-plugin` is required for [Server Side Rendering](/docs/server-side-rendering/) and automatic chunk names generation. `@loadable/server` and `@loadable/webpack-plugin` are only required for [Server Side Rendering](/docs/server-side-rendering/). 22 | 23 | ## Split your first component 24 | 25 | Loadable lets you render a dynamic import as a regular component. 26 | 27 | ```js 28 | import loadable from '@loadable/component' 29 | 30 | const OtherComponent = loadable(() => import('./OtherComponent')) 31 | 32 | function MyComponent() { 33 | return ( 34 |
35 | 36 |
37 | ) 38 | } 39 | ``` 40 | 41 | That's it, `OtherComponent` will now be loaded in a separated bundle! 42 | -------------------------------------------------------------------------------- /website/src/pages/docs/library-splitting.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guides 3 | title: Loading library 4 | order: 10 5 | --- 6 | 7 | # Library Splitting 8 | 9 | `loadable.lib` lets you defer the loading of a library. It takes a render props called when the library is loaded. 10 | 11 | ```js 12 | import loadable from '@loadable/component' 13 | 14 | const Moment = loadable.lib(() => import('moment')) 15 | 16 | function FromNow({ date }) { 17 | return ( 18 |
19 | 20 | {({ default: moment }) => moment(date).fromNow()} 21 | 22 |
23 | ) 24 | } 25 | ``` 26 | 27 | You can also use a `ref`, populated when the library is loaded. 28 | 29 | ```js 30 | import loadable from '@loadable/component' 31 | 32 | const Moment = loadable.lib(() => import('moment')) 33 | 34 | class MyComponent { 35 | moment = React.createRef() 36 | 37 | handleClick = () => { 38 | if (this.moment.current) { 39 | return alert(this.moment.current.default.format('HH:mm')) 40 | } 41 | } 42 | 43 | render() { 44 | return ( 45 |
46 | 47 | 48 |
49 | ) 50 | } 51 | } 52 | ``` 53 | 54 | > You can also pass a function to `ref`, called when the library is loaded. 55 | -------------------------------------------------------------------------------- /website/src/pages/docs/loadable-vs-react-lazy.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Introduction 3 | title: Comparison with React.lazy 4 | order: 20 5 | --- 6 | 7 | # Comparison with React.lazy 8 | 9 | What are the differences between `React.lazy` and `@loadable/components`? 10 | 11 | [`React.lazy`](https://reactjs.org/docs/code-splitting.html#reactlazy) is the recommended solution for Code Splitting. It uses Suspense and it is maintained by React. 12 | 13 | If you are already using `React.lazy` and if you are good with it, you don't need `@loadable/component`. 14 | 15 | If you feel limited or if you need SSR, then `@loadable/component` is the solution. 16 | 17 | ## Comparison table 18 | 19 | | Library | Suspense | SSR | Library splitting | `` import(`./${value}`) `` | 20 | | --------------------- | -------- | --- | ----------------- | -------------------------- | 21 | | `React.lazy` | ✅ | ❌ | ❌ | ❌ | 22 | | `@loadable/component` | ✅ | ✅ | ✅ | ✅ | 23 | 24 | ## Suspense 25 | 26 | Suspense is supported by `React.lazy` and by `@loadable/component`. `@loadable/component` can also be used without Suspense. 27 | 28 | ## Server Side Rendering 29 | 30 | Suspense is not available server-side and `React.lazy` can only work with Suspense. That's why today, `React.lazy` is not an option if you need Server Side Rendering. 31 | 32 | `@loadable/component` provides a complete solution to make [Server Side Rendering](/docs/server-side-rendering/) possible. 33 | 34 | ## Library splitting 35 | 36 | `@loadable/component` supports library splitting using render props. This is not possible with `React.lazy`. 37 | 38 | ## Full dynamic import 39 | 40 | Full dynamic import also called agressive code splitting is a feature supported by Webpack. It consists of passing a dynamic value to the dynamic `import()` function. 41 | 42 | ```js 43 | // All files that could match this pattern will be automatically code splitted. 44 | const loadFile = file => import(`./${file}`) 45 | ``` 46 | 47 | In React, it permits to create reusable components: 48 | 49 | ```js 50 | import loadable from '@loadable/component' 51 | 52 | const AsyncPage = loadable(props => import(`./${props.page}`)) 53 | 54 | function MyComponent() { 55 | return ( 56 |
57 | 58 | 59 |
60 | ) 61 | } 62 | ``` 63 | 64 | This feature is not supported by `React.lazy`. 65 | 66 | ## Note about `react-loadable` 67 | 68 | [react-loadable](https://github.com/jamiebuilds/react-loadable) was the recommended way for React code splitting for a long time. However, today it is not maintained any more and it is not compatible with Webpack v4+ and Babel v7+. 69 | 70 | If you use it, it is recommended to migrate to `React.lazy` or `@loadable/component`. 71 | -------------------------------------------------------------------------------- /website/src/pages/docs/migrate-from-react-loadable.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guides 3 | title: Migrate from react-loadable 4 | order: 90 5 | --- 6 | 7 | # Migrate from react-loadable 8 | 9 | Codemods are tools that can help us make changes in our code automatically. Think of it like 'find and replace', but more flexible. 10 | 11 | ## Usage 12 | 13 | ```sh 14 | npx loadable-codemod react-loadable-to-loadable-component ./client/src 15 | ``` 16 | 17 | ## Caveats 18 | 19 | Since `react-loadable` and `@loadable/component` do have differences, it is not possible to make a 100% fully automated codemod to handle all of the change needed. 20 | You still need to set up server-side rendering yourself. 21 | 22 | - Updating your server side rendering code to use `ChunkExtractor` 23 | - Updating your webpack configuration to use `@loadable` 24 | - Using `loadableReady` to hydrate your app on the client side. 25 | 26 | Please also note that `react-loadable` comes with stuffs like `Loadable.Map`, `pastDelay`, `timedOut`, `delay`, etc that do not exist in `@loadable/components`. 27 | If your code were using those features, you will still need to migrate them manually. 28 | 29 | For `loading` placeholder, this codemod already tries to render the loader as usual, but with a mocked up props as follows: 30 | 31 | ```javascript 32 | { 33 | pastDelay: true, 34 | error: false, 35 | timedOut: false, 36 | } 37 | ``` 38 | 39 | Your app should still works, but this means some of your app logic will be lost because `@loadable/component` simply does not support these features out of the box yet. 40 | -------------------------------------------------------------------------------- /website/src/pages/docs/prefetching.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guides 3 | title: Prefetching 4 | order: 60 5 | --- 6 | 7 | # Prefetching 8 | 9 | Loadable Components is fully compatible with [webpack hints `webpackPrefetch` and `webpackPreload`](https://webpack.js.org/guides/code-splitting/#prefetching-preloading-modules). 10 | 11 | Most of the time, you want to "prefetch" a component, it means it will be loaded when the browser is idle. You can do it by adding `/* webpackPrefetch: true */` inside your import statement. 12 | 13 | ```js 14 | import loadable from '@loadable/component' 15 | 16 | const OtherComponent = loadable(() => 17 | import(/* webpackPrefetch: true */ './OtherComponent'), 18 | ) 19 | ``` 20 | 21 | > You can extract prefetched resources server-side to add `` in your head. 22 | 23 | ## Manually preload a component 24 | 25 | It is possible to _force_ the preload of a component. It has the same effect as if the component is rendered for the first time. 26 | 27 | It can be useful to trigger a `preload` on mouse over: 28 | 29 | ```js 30 | import loadable from '@loadable/component' 31 | 32 | const Infos = loadable(() => import('./Infos')) 33 | 34 | function App() { 35 | const [show, setShow] = useState(false) 36 | return ( 37 |
43 | ) 44 | } 45 | ``` 46 | 47 | > `preload` is not available server-side, you should only call it client-side. If you want to use prefetching server-side, use webpack hints instead. 48 | 49 | > `preload` is aggressive and doesn't take care of network condition and data saving preference of the user. You should call it carefully. 50 | -------------------------------------------------------------------------------- /website/src/pages/docs/support.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: About 3 | title: Support 4 | order: 20 5 | --- 6 | 7 | # Support 8 | 9 | ## Supporting Loadable components 10 | 11 | Loadable Components is an MIT-licensed open source project. It is created an maintained by a single person: Greg Bergé. If you want to help me, then: 12 | 13 | - [Sponsor me on GitHub ❤️](https://github.com/sponsors/gregberge). 14 | -------------------------------------------------------------------------------- /website/src/pages/docs/suspense.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guides 3 | title: Suspense 4 | order: 30 5 | --- 6 | 7 | # Suspense 8 | 9 | `@loadable/component` exposes a `lazy` method that acts similarly as `React.lazy` one. 10 | 11 | ```js 12 | import React, { Suspense } from 'react' 13 | import { lazy } from '@loadable/component' 14 | 15 | const OtherComponent = lazy(() => import('./OtherComponent')) 16 | 17 | function MyComponent() { 18 | return ( 19 |
20 | Loading...
}> 21 | 22 | 23 | 24 | ) 25 | } 26 | ``` 27 | 28 | > Use `lazy.lib` for libraries. 29 | 30 | > ⚠️ Suspense is not yet available for server-side rendering. 31 | -------------------------------------------------------------------------------- /website/src/pages/docs/timeout.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | menu: Guides 3 | title: Timeout 4 | order: 55 5 | --- 6 | 7 | # Timeout 8 | 9 | Infinite loading is not good for user experience, to avoid it implementing a timeout is a good workaround. You can do it using a third party module like [`promise-timeout`](https://github.com/building5/promise-timeout): 10 | 11 | ```js 12 | import loadable from '@loadable/component' 13 | import { timeout } from 'promise-timeout' 14 | 15 | // Wait a maximum of 2s before sending an error. 16 | export const OtherComponent = loadable(() => 17 | timeout(import('./OtherComponent'), 2000) 18 | ) 19 | ``` 20 | -------------------------------------------------------------------------------- /website/src/pages/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-one-expression-per-line */ 2 | /* eslint-disable jsx-a11y/accessible-emoji */ 3 | import React from 'react' 4 | import { Box } from '@xstyled/styled-components' 5 | import Helmet from 'react-helmet' 6 | import { HomeHero, ShowCase, BaseLayout } from 'smooth-doc/components' 7 | 8 | export default function Index() { 9 | return ( 10 | 11 | 12 | Loadable Components - React code splitting 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

What is it?

22 | 36 |
37 | 38 |

Features

39 |
    40 |
  • 📚 Library splitting
  • 41 |
  • ⚡️ Prefetching
  • 42 |
  • 💫 Server Side Rendering
  • 43 |
  • 🎛 Full dynamic import
  • 44 |
45 |
46 |
47 |
48 |
49 |
50 | ) 51 | } 52 | --------------------------------------------------------------------------------