This commit is contained in:
2024-03-22 03:47:51 +05:30
parent 8bcf3d211e
commit 89819f6fe2
28440 changed files with 3211033 additions and 2 deletions

View File

@@ -0,0 +1,30 @@
{
"comments": false,
"presets": [
["@babel/preset-env", {
"loose": true
}],
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-transform-runtime",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-object-assign",
"dynamic-import-node",
"react-loadable/babel",
["module-resolver", {
"alias": {
"react-loadable-ssr-addon": "./source/index.js"
}
}]
],
"env": {
"production": {
"plugins": [
["transform-remove-console", {
"exclude": [ "error", "warn"]
}]
]
}
}
}

View File

@@ -0,0 +1,56 @@
version: 2
defaults: &defaults
docker:
- image: circleci/node:8.9.1
jobs:
test:
<<: *defaults
steps:
- checkout
- restore_cache:
keys:
- react-loadable-ssr-addon-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- react-loadable-ssr-addon-
- run: npm install
- run:
name: Run Tests
command: npm run test
- save_cache:
paths:
- node_modules
key: react-loadable-ssr-addon-{{ checksum "package.json" }}
- persist_to_workspace:
root: .
paths: .
publish:
<<: *defaults
steps:
- attach_workspace:
at: .
- run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ./.npmrc
- run: npm publish
workflows:
version: 2
main:
jobs:
- test:
filters:
tags:
only: /^v.*/
- publish:
requires:
- test
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/

View File

@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
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.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
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.
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.
## Scope
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.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@themgoncalves.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.
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.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -0,0 +1,39 @@
# Issue Title
<!--- Provide a general summary of the issue in the Title above -->
## Expected Behavior
<!--- Tell us what should happen -->
## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
## Steps to Reproduce
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1.
2.
3.
4.
## Context (Environment)
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
<!--- Provide a general summary of the issue in the Title above -->
### Environment
1. OS running: <!--- macOS mojave/Windows 10/Linux Ubunto... -->
2. Node version:
3. Webpack version:
## Detailed Description
<!--- Provide a detailed description of the change or addition you are proposing -->
## Possible Implementation
<!--- Not obligatory, but suggest an idea for implementing addition or change -->
## Other Comments

View File

@@ -0,0 +1,27 @@
## Summary
A few sentences describing the overall goals of the pull request's commits.
## Why
A few sentences describing the reasons behind this pull request.
## Checklist
- [ ] Your code builds clean without any `errors` or `warnings`
- [ ] You are using `approved terminology`
- [ ] You have added `unit tests`, if apply.
## Emojis for categorizing pull requests:
⚡️ New feature (`:zap:`)
🐛 Bug fix (`:bug:`)
🔥 P0 fix (`:fire:`)
✅ Tests (`:white_check_mark:`)
🚀 Performance improvements (`:rocket:`)
🖍 CSS / Styling (`:crayon:`)
♿ Accessibility (`:wheelchair:`)
🌐 Internationalization (`:globe_with_meridians:`)
📖 Documentation (`:book:`)
🏗 Infrastructure / Tooling / Builds / CI (`:building_construction:`)
⏪ Reverting a previous change (`:rewind:`)
♻️ Refactoring (like moving around code w/o any changes) (`:recycle:`)
🚮 Deleting code (`:put_litter_in_its_place:`)

View File

@@ -0,0 +1,14 @@
react-loadable-ssr-addon is authored by:
* Alexey Pyltsyn <lex61rus@gmail.com>
* Endi <endiliey@gmail.com>
* Jérémie Parker <gitkraken@jeremie-parker.com>
* Marcos <contact@themgoncalves.com>
* Marcos Gonçalves <contact@themgoncalves.com>
* Ngoc Phuong <nguyenngocphuongnb@gmail.com>
* Phuong Nguyen <nguyenngocphuongnb@gmail.com>
* Reece Dunham <me@rdil.rocks>
* Sébastien Lorber <slorber@users.noreply.github.com>
* dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* endiliey <endiliey@gmail.com>
* slorber <lorber.sebastien@gmail.com>

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Marcos Gonçalves
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

View File

@@ -0,0 +1,439 @@
# React Loadable SSR Add-on
> Server Side Render add-on for React Loadable. Load splitted chunks was never that easy.
[![NPM][npm-image]][npm-url]
[![CircleCI][circleci-image]][circleci-url]
[![GitHub All Releases][releases-image]][releases-url]
[![GitHub stars][stars-image]][stars-url]
[![Known Vulnerabilities][vulnerabilities-image]][vulnerabilities-url]
[![GitHub issues][issues-image]][issues-url]
[![Awesome][awesome-image]][awesome-url]
## Description
`React Loadable SSR Add-on` is a `server side render` add-on for [React Loadable](https://github.com/jamiebuilds/react-loadable)
that helps you to load dynamically all files dependencies, e.g. `splitted chunks`, `css`, etc.
Oh yeah, and we also **provide support for [SRI (Subresource Integrity)](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)**.
<br />
## Installation
**Download our NPM Package**
```sh
npm install react-loadable-ssr-addon
# or
yarn add react-loadable-ssr-addon
```
**Note**: `react-loadable-ssr-addon` **should not** be listed in the `devDependencies`.
<br />
## How to use
### 1 - Webpack Plugin
First we need to import the package into our component;
```javascript
const ReactLoadableSSRAddon = require('react-loadable-ssr-addon');
module.exports = {
entry: {
// ...
},
output: {
// ...
},
module: {
// ...
},
plugins: [
new ReactLoadableSSRAddon({
filename: 'assets-manifest.json',
}),
],
};
```
<br />
### 2 - On the Server
```js
// import `getBundles` to map required modules and its dependencies
import { getBundles } from 'react-loadable-ssr-addon';
// then import the assets manifest file generated by the Webpack Plugin
import manifest from './your-output-folder/assets-manifest.json';
...
// react-loadable ssr implementation
const modules = new Set();
const html = ReactDOMServer.renderToString(
<Loadable.Capture report={moduleName => modules.add(moduleName)}>
<App />
</Loadable.Capture>
);
...
// now we concatenate the loaded `modules` from react-loadable `Loadable.Capture` method
// with our application entry point
const modulesToBeLoaded = [...manifest.entrypoints, ...Array.from(modules)];
// also if you find your project still fetching the files after the placement
// maybe a good idea to switch the order from the implementation above to
// const modulesToBeLoaded = [...Array.from(modules), ...manifest.entrypoints];
// see the issue #6 regarding this thread
// https://github.com/themgoncalves/react-loadable-ssr-addon/issues/6
// after that, we pass the required modules to `getBundles` map it.
// `getBundles` will return all the required assets, group by `file type`.
const bundles = getBundles(manifest, modulesToBeLoaded);
// so it's easy to implement it
const styles = bundles.css || [];
const scripts = bundles.js || [];
res.send(`
<!doctype html>
<html lang="en">
<head>...</head>
${styles.map(style => {
return `<link href="/dist/${style.file}" rel="stylesheet" />`;
}).join('\n')}
<body>
<div id="app">${html}</div>
${scripts.map(script => {
return `<script src="/dist/${script.file}"></script>`
}).join('\n')}
</html>
`);
```
See how easy to implement it is?
<br />
## API Documentation
### Webpack Plugin options
#### `filename`
Type: `string`
Default: `react-loadable.json`
Assets manifest file name. May contain relative or absolute path.
#### `integrity`
Type: `boolean`
Default: `false`
Enable or disable generation of [Subresource Integrity (SRI).](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) hash.
#### `integrityAlgorithms`
Type: `array`
Default: `[ 'sha256', 'sha384', 'sha512' ]`
Algorithms to generate hash.
#### `integrityPropertyName`
Type: `string`
Default: `integrity`
Custom property name to be output in the assets manifest file.
**Full configuration example**
```js
new ReactLoadableSSRAddon({
filename: 'assets-manifest.json',
integrity: false,
integrityAlgorithms: [ 'sha256', 'sha384', 'sha512' ],
integrityPropertyName: 'integrity',
})
```
<br />
### Server Side
### `getBundles`
```js
import { getBundles } from 'react-loadable-ssr-addon';
/**
* getBundles
* @param {object} manifest - The assets manifest content generate by ReactLoadableSSRAddon
* @param {array} chunks - Chunks list to be loaded
* @returns {array} - Assets list group by file type
*/
const bundles = getBundles(manifest, modules);
const styles = bundles.css || [];
const scripts = bundles.js || [];
const xml = bundles.xml || [];
const json = bundles.json || [];
...
```
<br />
### Assets Manifest
#### `Basic Structure`
```json
{
"entrypoints": [ ],
"origins": {
"app": [ ]
},
"assets": {
"app": {
"js": [
{
"file": "",
"hash": "",
"publicPath": "",
"integrity": ""
}
]
}
}
}
```
#### `entrypoints`
Type: `array`
List of all application entry points defined in Webpack `entry`.
#### `origins`
Type: `array`
Origin name requested. List all assets required for the requested origin.
#### `assets`
Type: `array` of objects
Lists all application assets generate by Webpack, group by file type,
containing an `array of objects` with the following format:
```js
[file-type]: [
{
"file": "", // assets file
"hash": "", // file hash generated by Webpack
"publicPath": "", // assets file + webpack public path
"integrity": "" // integrity base64 hash, if enabled
}
]
```
<br />
### Assets Manifest Example
```json
{
"entrypoints": [
"app"
],
"origins": {
"./home": [
"home"
],
"./about": [
"about"
],
"app": [
"vendors",
"app"
],
"vendors": [
"app",
"vendors"
]
},
"assets": {
"home": {
"js": [
{
"file": "home.chunk.js",
"hash": "fdb00ffa16dfaf9cef0a",
"publicPath": "/dist/home.chunk.js",
"integrity": "sha256-Xxf7WVjPbdkJjgiZt7mvZvYv05+uErTC9RC2yCHF1RM= sha384-9OgouqlzN9KrqXVAcBzVMnlYOPxOYv/zLBOCuYtUAMoFxvmfxffbNIgendV4KXSJ sha512-oUxk3Swi0xIqvIxdWzXQIDRYlXo/V/aBqSYc+iWfsLcBftuIx12arohv852DruxKmlqtJhMv7NZp+5daSaIlnw=="
}
]
},
"about": {
"js": [
{
"file": "about.chunk.js",
"hash": "7e88ef606abbb82d7e82",
"publicPath": "/dist/about.chunk.js",
"integrity": "sha256-ZPrPWVJRjdS4af9F1FzkqTqqSGo1jYyXNyctwTOLk9o= sha384-J1wiEV8N1foqRF7W9SEvg2s/FhQbhpKFHBTNBJR8g1yEMNRMi38y+8XmjDV/Iu7w sha512-b16+PXStO68CP52R+0ZktccMiaI1v0jOy34l/DqyGN7kEae3DpV3xPNoC8vt1WfE1kCAH7dlnHDdp1XRVhZX+g=="
}
]
},
"app": {
"css": [
{
"file": "app.css",
"hash": "5888714915d8e89a8580",
"publicPath": "/dist/app.css",
"integrity": "sha256-3y4DyCC2cLII5sc2kaElHWhBIVMHdan/tA0akReI9qg= sha384-vCMVPKjSrrNpfnhmCD9E8SyHdfPdnM3DO/EkrbNI2vd0m2wH6BnfPja6gt43nDIF"
}
],
"js": [
{
"file": "app.bundle.js",
"hash": "0cbd05b10204597c781d",
"publicPath": "/dist/app.bundle.js",
"integrity": "sha256-sGdw+WVvXK1ZVQnYHI4FpecOcZtWZ99576OHCdrGil8= sha384-DZZzkPtPCTCR5UOWuGCyXQvsjyvZPoreCzqQGyrNV8+HyV9MdoYZawHX7NdGGLyi sha512-y29BlwBuwKB+BeXrrQYEBrK+mfWuOb4ok6F57kGbtrwa/Xq553Zb7lgss8RNvFjBSaMUdvXiJuhmP3HZA0jNeg=="
}
]
},
"vendors": {
"css": [
{
"file": "vendors.css",
"hash": "5a9586c29103a034feb5",
"publicPath": "/dist/vendors.css"
}
],
"js": [
{
"file": "vendors.chunk.js",
"hash": "5a9586c29103a034feb5",
"publicPath": "/dist/vendors.chunk.js"
}
]
}
}
}
```
<br />
## Release History
* 1.0.1
* FIX: [Webpack v5 deprecation warning](https://github.com/themgoncalves/react-loadable-ssr-addon/pull/27)
* 1.0.0
* BREAKING CHANGE: drop support for Webpack v3.
* NEW: add [support for Webpack v5](https://github.com/themgoncalves/react-loadable-ssr-addon/pull/26)
<details>
<summary>See older release note</summary>
* 0.3.0
* NEW: [`@babel/runtime` become an explicit dependency](https://github.com/themgoncalves/react-loadable-ssr-addon/pull/22) by [@RDIL](https://github.com/RDIL)
> Requirement for `yarn v2`.
* 0.2.3
* FIX: [Parsing `null` or `undefined` to object on `getBundles()`](https://github.com/themgoncalves/react-loadable-ssr-addon/pull/21) reported by [@slorber](https://github.com/slorber)
* 0.2.2
* FIX: As precaution measure, downgrade few dependencies due to node SemVer incompatibility.
* 0.2.1
* FIX: [Possible missing chunk](https://github.com/themgoncalves/react-loadable-ssr-addon/pull/20) reported by [@lex111](https://github.com/lex111)
* 0.2.0
* Improvement: Reduce memory consumption ([Issue #17](https://github.com/themgoncalves/react-loadable-ssr-addon/issues/17)) reported by [@endiliey](https://github.com/endiliey)
* 0.1.9
* FIX: [Missing entry in origins](https://github.com/themgoncalves/react-loadable-ssr-addon/pull/13) reported by [@p-j](https://github.com/p-j);
* 0.1.8
* Includes all features from deprecated v0.1.7;
* FIX: [Issue #11](https://github.com/themgoncalves/react-loadable-ssr-addon/issues/11) reported by [@endiliey](https://github.com/endiliey)
* ~~0.1.7 (_deprecated_)~~
* FIX: [`Cannot read property 'integrity' of undefined`](https://github.com/themgoncalves/react-loadable-ssr-addon/pull/10) reported by [@nguyenngocphuongnb](https://github.com/nguyenngocphuongnb);
* Minor improvements.
* 0.1.6
* FIX: `getManifestOutputPath` method when requested from `Webpack Dev Middleware`;
* 0.1.5
* FIX: [Issue #7](https://github.com/themgoncalves/react-loadable-ssr-addon/issues/7) reported by [@themgoncalves](https://github.com/themgoncalves) and [@tomkelsey](https://github.com/tomkelsey)
* 0.1.4
* FIX: [Issue #5](https://github.com/themgoncalves/react-loadable-ssr-addon/issues/5) reported by [@tomkelsey](https://github.com/tomkelsey)
* 0.1.3
* FIX: [Issue #4](https://github.com/themgoncalves/react-loadable-ssr-addon/issues/4) reported by [@tomkelsey](https://github.com/tomkelsey)
* 0.1.2
* FIX: [Issue #2](https://github.com/themgoncalves/react-loadable-ssr-addon/issues/2) reported by [@tatchi](https://github.com/tatchi)
* 0.1.1
* FIX: [Issue #1](https://github.com/themgoncalves/react-loadable-ssr-addon/issues/1) reported by [@tatchi](https://github.com/tatchi)
* 0.1.0
* First release
* NEW: Created `getBundles()` to retrieve required assets
* NEW: Created `ReactLoadableSSRAddon` Plugin for Webpack 3+
* 0.0.1
* Work in progress
</details>
<br />
## Meta
### Author
**Marcos Gonçalves** [LinkedIn](http://linkedin.com/in/themgoncalves/) [Website](http://www.themgoncalves.com)
### License
Distributed under the MIT license. [Click here](/LICENSE) for more information.
[https://github.com/themgoncalves/react-loadable-ssr-addon](https://github.com/themgoncalves/react-loadable-ssr-addon)
## Contributing
1. Fork it (<https://github.com/themgoncalves/react-loadable-ssr-addon/fork>)
2. Create your feature branch (`git checkout -b feature/fooBar`)
3. Commit your changes (`git commit -m ':zap: Add some fooBar'`)
4. Push to the branch (`git push origin feature/fooBar`)
5. Create a new Pull Request
### Emojis for categorizing commits:
⚡️ New feature (`:zap:`)
🐛 Bug fix (`:bug:`)
🔥 P0 fix (`:fire:`)
✅ Tests (`:white_check_mark:`)
🚀 Performance improvements (`:rocket:`)
🖍 CSS / Styling (`:crayon:`)
♿ Accessibility (`:wheelchair:`)
🌐 Internationalization (`:globe_with_meridians:`)
📖 Documentation (`:book:`)
🏗 Infrastructure / Tooling / Builds / CI (`:building_construction:`)
⏪ Reverting a previous change (`:rewind:`)
♻️ Refactoring (like moving around code w/o any changes) (`:recycle:`)
🚮 Deleting code (`:put_litter_in_its_place:`)
<!-- Markdown link & img dfn's -->
[circleci-image]:https://circleci.com/gh/themgoncalves/react-loadable-ssr-addon.svg?style=svg
[circleci-url]: https://circleci.com/gh/themgoncalves/react-loadable-ssr-addon
[vulnerabilities-image]: https://snyk.io/test/github/themgoncalves/react-loadable-ssr-addon/badge.svg
[vulnerabilities-url]: https://snyk.io/test/github/themgoncalves/react-loadable-ssr-addon
[issues-image]: https://img.shields.io/github/issues/themgoncalves/react-loadable-ssr-addon.svg
[issues-url]: https://github.com/themgoncalves/react-loadable-ssr-addon/issues
[stars-image]: https://img.shields.io/github/stars/themgoncalves/react-loadable-ssr-addon.svg
[stars-url]: https://github.com/themgoncalves/react-loadable-ssr-addon/stargazers
[forks-image]: https://img.shields.io/github/forks/themgoncalves/react-loadable-ssr-addon.svg
[forks-url]: https://github.com/themgoncalves/react-loadable-ssr-addon/network
[releases-image]: https://img.shields.io/npm/dm/react-loadable-ssr-addon.svg
[releases-url]: https://github.com/themgoncalves/react-loadable-ssr-addon
[awesome-image]: https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg
[awesome-url]: https://github.com/themgoncalves/react-loadable-ssr-addon
[npm-image]: https://img.shields.io/npm/v/react-loadable-ssr-addon.svg
[npm-url]: https://www.npmjs.com/package/react-loadable-ssr-addon

View File

@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Loadable from 'react-loadable';
import App from './components/App';
window.onload = () => {
Loadable.preloadReady().then(() => {
ReactDOM.hydrate(<App />, document.getElementById('app'));
});
};

View File

@@ -0,0 +1,28 @@
import React from 'react';
import Loadable from 'react-loadable';
import Loading from './Loading';
const HeaderExample = Loadable({
loader: () => import(/* webpackChunkName: "header" */'./Header'),
loading: Loading,
});
const ContentExample = Loadable({
loader: () => import(/* webpackChunkName: "content" */'./Content'),
loading: Loading,
});
const MultilevelExample = Loadable({
loader: () => import(/* webpackChunkName: "multilevel" */'./multilevel/Multilevel'),
loading: Loading,
});
export default function App() {
return (
<React.Fragment>
<HeaderExample />
<ContentExample />
<MultilevelExample />
</React.Fragment>
)
}

View File

@@ -0,0 +1,22 @@
import React from 'react';
import Loadable from "react-loadable";
import Loading from "./Loading";
const ContentNestedExample = Loadable({
loader: () => import(/* webpackChunkName: "content-nested" */'./ContentNested'),
loading: Loading,
});
export default function Content() {
return (
<div>
Bacon ipsum dolor amet pork belly minim pork loin reprehenderit incididunt aliquip hamburger chuck culpa mollit officia nisi pig duis.
Buffalo laboris duis ullamco flank.
Consectetur in excepteur elit ut aute adipisicing et tongue veniam labore dolore exercitation.
Swine consectetur boudin landjaeger, t-bone pork belly laborum.
Bacon ex ham ribeye sirloin et venison pariatur dolor non fugiat consequat.
Velit kevin non, jerky alcatra flank ball tip.
<ContentNestedExample />
</div>
);
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
export default function Content() {
return (
<React.Fragment>
<hr />
Eu prosciutto fugiat, meatloaf beef ribs jerky dolore commodo est chicken t-bone meatball capicola magna ipsum. Ribeye shankle mollit venison.
</React.Fragment>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
export default function Header() {
return (
<div>
<h1>React Loadable SSR Add-on</h1>
</div>
);
}

View File

@@ -0,0 +1,15 @@
import React from 'react';
export default function Loading(props) {
if (props.isLoading) {
if (props.timedOut) {
return <div>Loader timed out!</div>;
} else if (props.pastDelay) {
return <div>Loading...</div>;
}
return null;
} else if (props.error) {
return <div>Error! Component failed to load</div>;
}
return null;
}

View File

@@ -0,0 +1,26 @@
import React from 'react';
import Loadable from "react-loadable";
import Loading from "../Loading";
const SharedMultilevelExample = Loadable({
loader: () => import(/* webpackChunkName: "shared-multilevel" */'./SharedMultilevel'),
loading: Loading,
});
const DeeplevelExample = Loadable({
loader: () => import(/* webpackChunkName: "deeplevel" */'./level-1/level-2/DeepLevel'),
loading: Loading,
});
export default function Multilevel() {
return (
<div>
<hr />
Multilevel with Shared Component Example.
<SharedMultilevelExample />
Loading from a DeepLevel
<DeeplevelExample />
<hr />
</div>
);
}

View File

@@ -0,0 +1,13 @@
import React from 'react';
/* eslint-disable react/jsx-one-expression-per-line */
export default function SharedMultilevel() {
return (
<ul>
<li>
this is a <strong>shared multilevel</strong> component
</li>
</ul>
);
}
/* eslint-enable react/jsx-one-expression-per-line */

View File

@@ -0,0 +1,16 @@
import React from 'react';
import Loadable from "react-loadable";
import Loading from "../../../Loading";
const SharedMultilevelExample = Loadable({
loader: () => import(/* webpackChunkName: "shared-multilevel" */'../../SharedMultilevel'),
loading: Loading,
});
export default function DeepLevel() {
return (
<div>
<SharedMultilevelExample />
</div>
);
}

View File

@@ -0,0 +1,55 @@
import express from 'express';
import path from 'path';
import React from 'react';
import { renderToString } from 'react-dom/server';
import Loadable from 'react-loadable';
import { getBundles } from 'react-loadable-ssr-addon';
import App from './components/App';
const manifest = require('./dist/react-loadable-ssr-addon.json');
const server = express();
server.use('/dist', express.static(path.join(__dirname, 'dist')));
server.get('*', (req, res) => {
const modules = new Set();
const html = renderToString(
<Loadable.Capture report={moduleName => modules.add(moduleName)}>
<App/>
</Loadable.Capture>
);
const bundles = getBundles(manifest, [...manifest.entrypoints, ...Array.from(modules)]);
const styles = bundles.css || [];
const scripts = bundles.js || [];
res.send(`
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>React Loadable SSR Add-on Example</title>
${styles.map(style => {
return `<link href="/dist/${style.file}" rel="stylesheet" />`;
}).join('\n')}
</head>
<body>
<div id="app">${html}</div>
${scripts.map(script => {
return `<script src="/dist/${script.file}"></script>`
}).join('\n')}
</body>
</html>
`);
});
Loadable.preloadAll().then(() => {
server.listen(3003, () => {
console.log('Running on http://localhost:3003/');
});
}).catch(err => {
console.log(err);
});

View File

@@ -0,0 +1,311 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports["default"] = exports.defaultOptions = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _fs = _interopRequireDefault(require("fs"));
var _path = _interopRequireDefault(require("path"));
var _url = _interopRequireDefault(require("url"));
var _utils = require("./utils");
var PLUGIN_NAME = 'ReactLoadableSSRAddon';
var WEBPACK_VERSION = require('webpack/package.json').version;
var WEBPACK_5 = WEBPACK_VERSION.startsWith('5.');
var defaultOptions = {
filename: 'assets-manifest.json',
integrity: false,
integrityAlgorithms: ['sha256', 'sha384', 'sha512'],
integrityPropertyName: 'integrity'
};
exports.defaultOptions = defaultOptions;
var ReactLoadableSSRAddon = function () {
function ReactLoadableSSRAddon(options) {
if (options === void 0) {
options = defaultOptions;
}
this.options = (0, _extends2["default"])({}, defaultOptions, options);
this.compiler = null;
this.stats = null;
this.entrypoints = new Set();
this.assetsByName = new Map();
this.manifest = {};
}
var _proto = ReactLoadableSSRAddon.prototype;
_proto.getAssets = function getAssets(assetsChunk) {
for (var i = 0; i < assetsChunk.length; i += 1) {
var chunk = assetsChunk[i];
var id = chunk.id,
files = chunk.files,
_chunk$siblings = chunk.siblings,
siblings = _chunk$siblings === void 0 ? [] : _chunk$siblings,
hash = chunk.hash;
var keys = this.getChunkOrigin(chunk);
for (var j = 0; j < keys.length; j += 1) {
this.assetsByName.set(keys[j], {
id: id,
files: files,
hash: hash,
siblings: siblings
});
}
}
return this.assetsByName;
};
_proto.getEntrypoints = function getEntrypoints(entrypoints) {
var entry = Object.keys(entrypoints);
for (var i = 0; i < entry.length; i += 1) {
this.entrypoints.add(entry[i]);
}
return this.entrypoints;
};
_proto.getChunkOrigin = function getChunkOrigin(_ref) {
var id = _ref.id,
names = _ref.names,
modules = _ref.modules;
var origins = new Set();
if (!WEBPACK_5) {
for (var i = 0; i < modules.length; i += 1) {
var reasons = modules[i].reasons;
for (var j = 0; j < reasons.length; j += 1) {
var reason = reasons[j];
var type = reason.dependency ? reason.dependency.type : null;
var userRequest = reason.dependency ? reason.dependency.userRequest : null;
if (type === 'import()') {
origins.add(userRequest);
}
}
}
}
if (origins.size === 0) {
return [names[0] || id];
}
if (this.entrypoints.has(names[0])) {
origins.add(names[0]);
}
return Array.from(origins);
};
_proto.apply = function apply(compiler) {
this.compiler = compiler;
compiler.hooks.emit.tapAsync(PLUGIN_NAME, this.handleEmit.bind(this));
};
_proto.getMinimalStatsChunks = function getMinimalStatsChunks(compilationChunks, chunkGraph) {
var _this = this;
var compareId = function compareId(a, b) {
if (typeof a !== typeof b) {
return typeof a < typeof b ? -1 : 1;
}
if (a < b) return -1;
if (a > b) return 1;
return 0;
};
return this.ensureArray(compilationChunks).reduce(function (chunks, chunk) {
var siblings = new Set();
if (chunk.groupsIterable) {
var chunkGroups = Array.from(chunk.groupsIterable);
for (var i = 0; i < chunkGroups.length; i += 1) {
var group = Array.from(chunkGroups[i].chunks);
for (var j = 0; j < group.length; j += 1) {
var sibling = group[j];
if (sibling !== chunk) siblings.add(sibling.id);
}
}
}
chunk.ids.forEach(function (id) {
chunks.push({
id: id,
names: chunk.name ? [chunk.name] : [],
files: _this.ensureArray(chunk.files).slice(),
hash: chunk.renderedHash,
siblings: Array.from(siblings).sort(compareId),
modules: WEBPACK_5 ? chunkGraph.getChunkModules(chunk) : chunk.getModules()
});
});
return chunks;
}, []);
};
_proto.handleEmit = function handleEmit(compilation, callback) {
this.stats = compilation.getStats().toJson({
all: false,
entrypoints: true
}, true);
this.options.publicPath = (compilation.outputOptions ? compilation.outputOptions.publicPath : compilation.options.output.publicPath) || '';
this.getEntrypoints(this.stats.entrypoints);
this.getAssets(this.getMinimalStatsChunks(compilation.chunks, compilation.chunkGraph));
this.processAssets(compilation.assets);
this.writeAssetsFile();
callback();
};
_proto.processAssets = function processAssets(originAssets) {
var _this2 = this;
var assets = {};
var origins = {};
var entrypoints = this.entrypoints;
this.assetsByName.forEach(function (value, key) {
var files = value.files,
id = value.id,
siblings = value.siblings,
hash = value.hash;
if (!origins[key]) {
origins[key] = [];
}
siblings.push(id);
for (var i = 0; i < siblings.length; i += 1) {
var sibling = siblings[i];
if (!origins[key].includes(sibling)) {
origins[key].push(sibling);
}
}
for (var _i = 0; _i < files.length; _i += 1) {
var file = files[_i];
var currentAsset = originAssets[file] || {};
var ext = (0, _utils.getFileExtension)(file).replace(/^\.+/, '').toLowerCase();
if (!assets[id]) {
assets[id] = {};
}
if (!assets[id][ext]) {
assets[id][ext] = [];
}
if (!(0, _utils.hasEntry)(assets[id][ext], 'file', file)) {
var shouldComputeIntegrity = Object.keys(currentAsset) && _this2.options.integrity && !currentAsset[_this2.options.integrityPropertyName];
if (shouldComputeIntegrity) {
currentAsset[_this2.options.integrityPropertyName] = (0, _utils.computeIntegrity)(_this2.options.integrityAlgorithms, currentAsset.source());
}
assets[id][ext].push({
file: file,
hash: hash,
publicPath: _url["default"].resolve(_this2.options.publicPath || '', file),
integrity: currentAsset[_this2.options.integrityPropertyName]
});
}
}
});
this.manifest = {
entrypoints: Array.from(entrypoints),
origins: origins,
assets: assets
};
};
_proto.writeAssetsFile = function writeAssetsFile() {
var filePath = this.manifestOutputPath;
var fileDir = _path["default"].dirname(filePath);
var json = JSON.stringify(this.manifest, null, 2);
try {
if (!_fs["default"].existsSync(fileDir)) {
_fs["default"].mkdirSync(fileDir);
}
} catch (err) {
if (err.code !== 'EEXIST') {
throw err;
}
}
_fs["default"].writeFileSync(filePath, json);
};
_proto.ensureArray = function ensureArray(source) {
if (WEBPACK_5) {
return Array.from(source);
}
return source;
};
(0, _createClass2["default"])(ReactLoadableSSRAddon, [{
key: "isRequestFromDevServer",
get: function get() {
if (process.argv.some(function (arg) {
return arg.includes('webpack-dev-server');
})) {
return true;
}
var _this$compiler = this.compiler,
outputFileSystem = _this$compiler.outputFileSystem,
name = _this$compiler.outputFileSystem.constructor.name;
return outputFileSystem && name === 'MemoryFileSystem';
}
}, {
key: "manifestOutputPath",
get: function get() {
var filename = this.options.filename;
if (_path["default"].isAbsolute(filename)) {
return filename;
}
var _this$compiler2 = this.compiler,
outputPath = _this$compiler2.outputPath,
devServer = _this$compiler2.options.devServer;
if (this.isRequestFromDevServer && devServer) {
var devOutputPath = devServer.outputPath || outputPath || '/';
if (devOutputPath === '/') {
console.warn('Please use an absolute path in options.output when using webpack-dev-server.');
devOutputPath = this.compiler.context || process.cwd();
}
return _path["default"].resolve(devOutputPath, filename);
}
return _path["default"].resolve(outputPath, filename);
}
}]);
return ReactLoadableSSRAddon;
}();
var _default = ReactLoadableSSRAddon;
exports["default"] = _default;

View File

@@ -0,0 +1,97 @@
"use strict";
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
var _ava = _interopRequireDefault(require("ava"));
var _path = _interopRequireDefault(require("path"));
var _fs = _interopRequireDefault(require("fs"));
var _waitForExpect = _interopRequireDefault(require("wait-for-expect"));
var _webpack = _interopRequireDefault(require("webpack"));
var _webpack2 = _interopRequireDefault(require("../webpack.config"));
var _ReactLoadableSSRAddon = _interopRequireWildcard(require("./ReactLoadableSSRAddon"));
var outputPath;
var manifestOutputPath;
var runWebpack = function runWebpack(configuration, end, callback) {
(0, _webpack["default"])(configuration, function (err, stats) {
if (err) {
return end(err);
}
if (stats.hasErrors()) {
return end(stats.toString());
}
callback();
end();
});
};
_ava["default"].beforeEach(function () {
var publicPathSanitized = _webpack2["default"].output.publicPath.slice(1, -1);
outputPath = _path["default"].resolve('./example', publicPathSanitized);
manifestOutputPath = _path["default"].resolve(outputPath, _ReactLoadableSSRAddon.defaultOptions.filename);
});
_ava["default"].cb('outputs with default settings', function (t) {
_webpack2["default"].plugins = [new _ReactLoadableSSRAddon["default"]()];
runWebpack(_webpack2["default"], t.end, function () {
var feedback = _fs["default"].existsSync(manifestOutputPath) ? 'pass' : 'fail';
t[feedback]();
});
});
_ava["default"].cb('outputs with custom filename', function (t) {
var filename = 'new-assets-manifest.json';
_webpack2["default"].plugins = [new _ReactLoadableSSRAddon["default"]({
filename: filename
})];
runWebpack(_webpack2["default"], t.end, function () {
var feedback = _fs["default"].existsSync(manifestOutputPath.replace(_ReactLoadableSSRAddon.defaultOptions.filename, filename)) ? 'pass' : 'fail';
t[feedback]();
});
});
_ava["default"].cb('outputs with integrity', function (t) {
_webpack2["default"].plugins = [new _ReactLoadableSSRAddon["default"]({
integrity: true
})];
runWebpack(_webpack2["default"], t.end, (0, _asyncToGenerator2["default"])(_regenerator["default"].mark(function _callee() {
var manifest;
return _regenerator["default"].wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
manifest = require("" + manifestOutputPath);
_context.next = 3;
return (0, _waitForExpect["default"])(function () {
Object.keys(manifest.assets).forEach(function (asset) {
manifest.assets[asset].js.forEach(function (_ref2) {
var integrity = _ref2.integrity;
t.truthy(integrity);
});
});
});
case 3:
case "end":
return _context.stop();
}
}
}, _callee);
})));
});

View File

@@ -0,0 +1,35 @@
"use strict";
exports.__esModule = true;
exports["default"] = void 0;
var _utils = require("./utils");
function getBundles(manifest, chunks) {
if (!manifest || !chunks) {
return {};
}
var assetsKey = chunks.reduce(function (key, chunk) {
if (manifest.origins[chunk]) {
key = (0, _utils.unique)([].concat(key, manifest.origins[chunk]));
}
return key;
}, []);
return assetsKey.reduce(function (bundle, asset) {
Object.keys(manifest.assets[asset] || {}).forEach(function (key) {
var content = manifest.assets[asset][key];
if (!bundle[key]) {
bundle[key] = [];
}
bundle[key] = (0, _utils.unique)([].concat(bundle[key], content));
});
return bundle;
}, {});
}
var _default = getBundles;
exports["default"] = _default;

View File

@@ -0,0 +1,51 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _ava = _interopRequireDefault(require("ava"));
var _path = _interopRequireDefault(require("path"));
var _getBundles = _interopRequireDefault(require("./getBundles"));
var _webpack = _interopRequireDefault(require("../webpack.config"));
var _reactLoadableSsrAddon = _interopRequireDefault(require("../example/dist/react-loadable-ssr-addon"));
var modules = ['./Header', './multilevel/Multilevel', './SharedMultilevel', '../../SharedMultilevel'];
var fileType = ['js'];
var bundles;
_ava["default"].beforeEach(function () {
bundles = (0, _getBundles["default"])(_reactLoadableSsrAddon["default"], [].concat(_reactLoadableSsrAddon["default"].entrypoints, modules));
});
(0, _ava["default"])('returns the correct bundle size and content', function (t) {
t["true"](Object.keys(bundles).length === fileType.length);
fileType.forEach(function (type) {
return !!bundles[type];
});
});
(0, _ava["default"])('returns the correct bundle infos', function (t) {
fileType.forEach(function (type) {
bundles[type].forEach(function (bundle) {
var expectedPublichPath = _path["default"].resolve(_webpack["default"].output.publicPath, bundle.file);
t["true"](bundle.file !== '');
t["true"](bundle.hash !== '');
t["true"](bundle.publicPath === expectedPublichPath);
});
});
});
(0, _ava["default"])('returns nothing when there is no match', function (t) {
bundles = (0, _getBundles["default"])(_reactLoadableSsrAddon["default"], ['foo-bar', 'foo', null, undefined]);
t["true"](Object.keys(bundles).length === 0);
});
(0, _ava["default"])('should work even with null/undefined manifest or modules', function (t) {
bundles = (0, _getBundles["default"])(_reactLoadableSsrAddon["default"], null);
t["true"](Object.keys(bundles).length === 0);
bundles = (0, _getBundles["default"])(null, []);
t["true"](Object.keys(bundles).length === 0);
bundles = (0, _getBundles["default"])([], null);
t["true"](Object.keys(bundles).length === 0);
});

View File

@@ -0,0 +1,10 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _ReactLoadableSSRAddon = _interopRequireDefault(require("./ReactLoadableSSRAddon"));
var _getBundles = _interopRequireDefault(require("./getBundles"));
module.exports = _ReactLoadableSSRAddon["default"];
module.exports.getBundles = _getBundles["default"];

View File

@@ -0,0 +1,19 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports["default"] = void 0;
var _crypto = _interopRequireDefault(require("crypto"));
function computeIntegrity(algorithms, source) {
return Array.isArray(algorithms) ? algorithms.map(function (algorithm) {
var hash = _crypto["default"].createHash(algorithm).update(source, 'utf8').digest('base64');
return algorithm + "-" + hash;
}).join(' ') : '';
}
var _default = computeIntegrity;
exports["default"] = _default;

View File

@@ -0,0 +1,18 @@
"use strict";
exports.__esModule = true;
exports["default"] = void 0;
function getFileExtension(filename) {
if (!filename || typeof filename !== 'string') {
return '';
}
var fileExtRegex = /\.\w{2,4}\.(?:map|gz)$|\.\w+$/i;
var name = filename.split(/[?#]/)[0];
var ext = name.match(fileExtRegex);
return ext && ext.length ? ext[0] : '';
}
var _default = getFileExtension;
exports["default"] = _default;

View File

@@ -0,0 +1,32 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _ava = _interopRequireDefault(require("ava"));
var _getFileExtension = _interopRequireDefault(require("./getFileExtension"));
(0, _ava["default"])('returns the correct file extension', function (t) {
var extensions = ['.jpeg', '.js', '.css', '.json', '.xml'];
var filePath = 'source/static/images/hello-world';
extensions.forEach(function (ext) {
t["true"]((0, _getFileExtension["default"])("" + filePath + ext) === ext);
});
});
(0, _ava["default"])('sanitize file hash', function (t) {
var hashes = ['?', '#'];
var filePath = 'source/static/images/hello-world.jpeg';
hashes.forEach(function (hash) {
t["true"]((0, _getFileExtension["default"])("" + filePath + hash + "d587bbd6e38337f5accd") === '.jpeg');
});
});
(0, _ava["default"])('returns empty string when there is no file extension', function (t) {
var filePath = 'source/static/resource';
t["true"]((0, _getFileExtension["default"])(filePath) === '');
});
(0, _ava["default"])('should work even with null/undefined arg', function (t) {
var filePaths = ['', null, undefined];
filePaths.forEach(function (path) {
t["true"]((0, _getFileExtension["default"])(path) === '');
});
});

View File

@@ -0,0 +1,20 @@
"use strict";
exports.__esModule = true;
exports["default"] = hasEntry;
function hasEntry(target, targetKey, searchFor) {
if (!target) {
return false;
}
for (var i = 0; i < target.length; i += 1) {
var file = target[i][targetKey];
if (file === searchFor) {
return true;
}
}
return false;
}

View File

@@ -0,0 +1,38 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _ava = _interopRequireDefault(require("ava"));
var _hasEntry = _interopRequireDefault(require("./hasEntry"));
var assets = [{
file: 'content.chunk.js',
hash: 'd41d8cd98f00b204e9800998ecf8427e',
publicPath: './',
integrity: null
}, {
file: 'header.chunk.js',
hash: '699f4bd49870f2b90e1d1596d362efcb',
publicPath: './',
integrity: null
}, {
file: 'shared-multilevel.chunk.js',
hash: 'ab7b8b1c1d5083c17a39ccd2962202e1',
publicPath: './',
integrity: null
}];
(0, _ava["default"])('should flag as has entry', function (t) {
var fileName = 'header.chunk.js';
t["true"]((0, _hasEntry["default"])(assets, 'file', fileName));
});
(0, _ava["default"])('should flag as has no entry', function (t) {
var fileName = 'footer.chunk.js';
t["false"]((0, _hasEntry["default"])(assets, 'file', fileName));
});
(0, _ava["default"])('should work even with null/undefined target', function (t) {
var targets = [[], null, undefined];
targets.forEach(function (target) {
t["false"]((0, _hasEntry["default"])(target, 'file', 'foo.js'));
});
});

View File

@@ -0,0 +1,22 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports.hasEntry = exports.unique = exports.getFileExtension = exports.computeIntegrity = void 0;
var _computeIntegrity = _interopRequireDefault(require("./computeIntegrity"));
exports.computeIntegrity = _computeIntegrity["default"];
var _getFileExtension = _interopRequireDefault(require("./getFileExtension"));
exports.getFileExtension = _getFileExtension["default"];
var _unique = _interopRequireDefault(require("./unique"));
exports.unique = _unique["default"];
var _hasEntry = _interopRequireDefault(require("./hasEntry"));
exports.hasEntry = _hasEntry["default"];

View File

@@ -0,0 +1,10 @@
"use strict";
exports.__esModule = true;
exports["default"] = unique;
function unique(array) {
return array.filter(function (elem, pos, arr) {
return arr.indexOf(elem) === pos;
});
}

View File

@@ -0,0 +1,26 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _ava = _interopRequireDefault(require("ava"));
var _unique = _interopRequireDefault(require("./unique"));
(0, _ava["default"])('it filters duplicated entries', function (t) {
var duplicated = ['two', 'four'];
var raw = ['one', 'two', 'three', 'four'];
var filtered = (0, _unique["default"])([].concat(raw, duplicated));
duplicated.forEach(function (dup) {
t["true"](filtered.filter(function (item) {
return item === dup;
}).length === 1);
});
});
(0, _ava["default"])('should work with null/undefined values', function (t) {
var falsy = [null, undefined];
var raw = ['one', 'two', 'three', 'four'];
var filtered = (0, _unique["default"])([].concat(raw, falsy));
falsy.forEach(function (value) {
t["true"](filtered.includes(value));
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,100 @@
{
"name": "react-loadable-ssr-addon-v5-slorber",
"version": "1.0.1",
"description": "Server Side Render add-on for React Loadable. Load splitted chunks was never that easy.",
"main": "lib/index.js",
"repository": {
"type": "git",
"url": "https://github.com/themgoncalves/react-loadable-ssr-addon.git"
},
"keywords": [
"react",
"react-loadable",
"webpack",
"splitted-chunks",
"assets-manifest",
"server-side-render",
"ssr"
],
"author": "Marcos Gonçalves <contact@themgoncalves.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/themgoncalves/react-loadable-ssr-addon/issues"
},
"scripts": {
"authors": "echo 'react-loadable-ssr-addon is authored by: \n' > AUTHORS.md | git log --format='* %aN <%aE>' | sort -u >> AUTHORS.md",
"prepare": "npm run build && npm run authors",
"prepublishOnly": "npm test && npm run lint",
"preversion": "npm run lint",
"postversion": "git push && git push --tags",
"start": "npm run clean:example && npm run build && webpack && babel-node example/server.js",
"build": "NODE_ENV=production && rm -rf lib && babel source -d lib",
"clean:example": "rm -rf ./example/dist/",
"lint": "eslint --ext js --ext jsx source || exit 0",
"lint:fix": "eslint --ext js --ext jsx source --fix|| exit 0",
"test": "npm run clean:example && npm run build && webpack && ava; npm run clean:example",
"stats": "NODE_ENV=development webpack --profile --json > compilation-stats.json"
},
"engines": {
"node": ">=10.13.0"
},
"resolutions": {
"yargs-parser": "13.1.2",
"mem": "4.0.0"
},
"peerDependencies": {
"react-loadable": "*",
"webpack": ">=4.41.1 || 5.x"
},
"devDependencies": {
"@babel/cli": "^7.10.1",
"@babel/core": "^7.10.1",
"@babel/node": "^7.10.1",
"@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/plugin-proposal-object-rest-spread": "^7.10.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-classes": "^7.10.1",
"@babel/plugin-transform-object-assign": "^7.10.1",
"@babel/plugin-transform-runtime": "^7.10.1",
"@babel/preset-env": "^7.10.1",
"@babel/preset-react": "^7.10.1",
"@babel/register": "^7.10.1",
"ava": "^2.4.0",
"babel-loader": "^8.1.0",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-module-resolver": "^4.0.0",
"babel-plugin-transform-remove-console": "^6.9.4",
"babel-preset-minify": "^0.5.1",
"eslint": "^6.5.1",
"eslint-config-airbnb": "18.1.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.20.0",
"express": "^4.17.1",
"husky": "^3.0.9",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-loadable": "^5.5.0",
"wait-for-expect": "^3.0.2",
"webpack": "4.44.1",
"webpack-cli": "^4.5.0"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint",
"pre-push": "npm run test"
}
},
"ava": {
"files": [
"source/**/*.test.js"
],
"require": [
"@babel/register"
],
"concurrency": 5
},
"dependencies": {
"@babel/runtime": "^7.10.3"
}
}

View File

@@ -0,0 +1,359 @@
/**
* react-loadable-ssr-addon
* @author Marcos Gonçalves <contact@themgoncalves.com>
* @version 1.0.1
*/
import fs from 'fs';
import path from 'path';
import url from 'url';
import { getFileExtension, computeIntegrity, hasEntry } from './utils';
// Webpack plugin name
const PLUGIN_NAME = 'ReactLoadableSSRAddon';
const WEBPACK_VERSION = require('webpack/package.json').version;
const WEBPACK_5 = WEBPACK_VERSION.startsWith('5.');
// Default plugin options
const defaultOptions = {
filename: 'assets-manifest.json',
integrity: false,
integrityAlgorithms: ['sha256', 'sha384', 'sha512'],
integrityPropertyName: 'integrity',
};
/**
* React Loadable SSR Add-on for Webpack
* @class ReactLoadableSSRAddon
* @desc Generate application assets manifest with its dependencies.
*/
class ReactLoadableSSRAddon {
/**
* @constructs ReactLoadableSSRAddon
* @param options
*/
constructor(options = defaultOptions) {
this.options = { ...defaultOptions, ...options };
this.compiler = null;
this.stats = null;
this.entrypoints = new Set();
this.assetsByName = new Map();
this.manifest = {};
}
/**
* Check if request is from Dev Server
* aka webpack-dev-server
* @method isRequestFromDevServer
* @returns {boolean} - True or False
*/
get isRequestFromDevServer() {
if (process.argv.some((arg) => arg.includes('webpack-dev-server'))) { return true; }
const { outputFileSystem, outputFileSystem: { constructor: { name } } } = this.compiler;
return outputFileSystem && name === 'MemoryFileSystem';
}
/**
* Get assets manifest output path
* @readonly
* @method manifestOutputPath
* @returns {string} - Output path containing path + filename.
*/
get manifestOutputPath() {
const { filename } = this.options;
if (path.isAbsolute(filename)) {
return filename;
}
const { outputPath, options: { devServer } } = this.compiler;
if (this.isRequestFromDevServer && devServer) {
let devOutputPath = (devServer.outputPath || outputPath || '/');
if (devOutputPath === '/') {
console.warn('Please use an absolute path in options.output when using webpack-dev-server.');
devOutputPath = this.compiler.context || process.cwd();
}
return path.resolve(devOutputPath, filename);
}
return path.resolve(outputPath, filename);
}
/**
* Get application assets chunks
* @method getAssets
* @param {array} assetsChunk - Webpack application chunks
* @returns {Map<string, object>}
*/
getAssets(assetsChunk) {
for (let i = 0; i < assetsChunk.length; i += 1) {
const chunk = assetsChunk[i];
const {
id, files, siblings = [], hash,
} = chunk;
const keys = this.getChunkOrigin(chunk);
for (let j = 0; j < keys.length; j += 1) {
this.assetsByName.set(keys[j], {
id, files, hash, siblings,
});
}
}
return this.assetsByName;
}
/**
* Get Application Entry points
* @method getEntrypoints
* @param {object} entrypoints - Webpack entry points
* @returns {Set<string>} - Application Entry points
*/
getEntrypoints(entrypoints) {
const entry = Object.keys(entrypoints);
for (let i = 0; i < entry.length; i += 1) {
this.entrypoints.add(entry[i]);
}
return this.entrypoints;
}
/**
* Get application chunk origin
* @method getChunkOrigin
* @param {object} id - Webpack application chunk id
* @param {object} names - Webpack application chunk names
* @param {object} modules - Webpack application chunk modules
* @returns {array} Chunk Keys
*/
/* eslint-disable class-methods-use-this */
getChunkOrigin({ id, names, modules }) {
const origins = new Set();
if (!WEBPACK_5) {
// webpack 5 doesn't have 'reasons' on chunks any more
// this is a dirty solution to make it work without throwing
// an error, but does need tweaking to make everything work properly.
for (let i = 0; i < modules.length; i += 1) {
const { reasons } = modules[i];
for (let j = 0; j < reasons.length; j += 1) {
const reason = reasons[j];
const type = reason.dependency ? reason.dependency.type : null;
const userRequest = reason.dependency
? reason.dependency.userRequest
: null;
if (type === 'import()') {
origins.add(userRequest);
}
}
}
}
if (origins.size === 0) { return [names[0] || id]; }
if (this.entrypoints.has(names[0])) {
origins.add(names[0]);
}
return Array.from(origins);
}
/* eslint-enabled */
/**
* Webpack apply method.
* @method apply
* @param {object} compiler - Webpack compiler object
* It represents the fully configured Webpack environment.
* @See {@link https://webpack.js.org/concepts/plugins/#anatomy}
*/
apply(compiler) {
this.compiler = compiler;
// @See {@Link https://webpack.js.org/api/compiler-hooks/}
compiler.hooks.emit.tapAsync(PLUGIN_NAME, this.handleEmit.bind(this));
}
/**
* Get Minimal Stats Chunks
* @description equivalent of getting stats.chunks but much less in size & memory usage
* It tries to mimic https://github.com/webpack/webpack/blob/webpack-4/lib/Stats.js#L632
* implementation without expensive operations
* @param {array} compilationChunks
* @param {array} chunkGraph
* @returns {array}
*/
getMinimalStatsChunks(compilationChunks, chunkGraph) {
const compareId = (a, b) => {
if (typeof a !== typeof b) {
return typeof a < typeof b ? -1 : 1;
}
if (a < b) return -1;
if (a > b) return 1;
return 0;
};
return this.ensureArray(compilationChunks).reduce((chunks, chunk) => {
const siblings = new Set();
if (chunk.groupsIterable) {
const chunkGroups = Array.from(chunk.groupsIterable);
for (let i = 0; i < chunkGroups.length; i += 1) {
const group = Array.from(chunkGroups[i].chunks);
for (let j = 0; j < group.length; j += 1) {
const sibling = group[j];
if (sibling !== chunk) siblings.add(sibling.id);
}
}
}
chunk.ids.forEach((id) => {
chunks.push({
id,
names: chunk.name ? [chunk.name] : [],
files: this.ensureArray(chunk.files).slice(),
hash: chunk.renderedHash,
siblings: Array.from(siblings).sort(compareId),
// Webpack5 emit deprecation warning for chunk.getModules()
// "DEP_WEBPACK_CHUNK_GET_MODULES"
modules: WEBPACK_5 ? chunkGraph.getChunkModules(chunk) : chunk.getModules(),
});
});
return chunks;
}, []);
}
/**
* Handles emit event from Webpack
* @desc The Webpack Compiler begins with emitting the generated assets.
* Here plugins have the last chance to add assets to the `c.assets` array.
* @See {@Link https://github.com/webpack/docs/wiki/plugins#emitc-compilation-async}
* @method handleEmit
* @param {object} compilation
* @param {function} callback
*/
handleEmit(compilation, callback) {
this.stats = compilation.getStats().toJson({
all: false,
entrypoints: true,
}, true);
this.options.publicPath = (compilation.outputOptions
? compilation.outputOptions.publicPath
: compilation.options.output.publicPath)
|| '';
this.getEntrypoints(this.stats.entrypoints);
this.getAssets(this.getMinimalStatsChunks(compilation.chunks, compilation.chunkGraph));
this.processAssets(compilation.assets);
this.writeAssetsFile();
callback();
}
/**
* Process Application Assets Manifest
* @method processAssets
* @param {object} originAssets - Webpack raw compilations assets
*/
/* eslint-disable object-curly-newline, no-restricted-syntax */
processAssets(originAssets) {
const assets = {};
const origins = {};
const { entrypoints } = this;
this.assetsByName.forEach((value, key) => {
const { files, id, siblings, hash } = value;
if (!origins[key]) { origins[key] = []; }
siblings.push(id);
for (let i = 0; i < siblings.length; i += 1) {
const sibling = siblings[i];
if (!origins[key].includes(sibling)) {
origins[key].push(sibling);
}
}
for (let i = 0; i < files.length; i += 1) {
const file = files[i];
const currentAsset = originAssets[file] || {};
const ext = getFileExtension(file).replace(/^\.+/, '').toLowerCase();
if (!assets[id]) { assets[id] = {}; }
if (!assets[id][ext]) { assets[id][ext] = []; }
if (!hasEntry(assets[id][ext], 'file', file)) {
const shouldComputeIntegrity = Object.keys(currentAsset)
&& this.options.integrity
&& !currentAsset[this.options.integrityPropertyName];
if (shouldComputeIntegrity) {
currentAsset[this.options.integrityPropertyName] = computeIntegrity(
this.options.integrityAlgorithms,
currentAsset.source(),
);
}
assets[id][ext].push({
file,
hash,
publicPath: url.resolve(this.options.publicPath || '', file),
integrity: currentAsset[this.options.integrityPropertyName],
});
}
}
});
// create assets manifest object
this.manifest = {
entrypoints: Array.from(entrypoints),
origins,
assets,
};
}
/**
* Write Assets Manifest file
* @method writeAssetsFile
*/
writeAssetsFile() {
const filePath = this.manifestOutputPath;
const fileDir = path.dirname(filePath);
const json = JSON.stringify(this.manifest, null, 2);
try {
if (!fs.existsSync(fileDir)) {
fs.mkdirSync(fileDir);
}
} catch (err) {
if (err.code !== 'EEXIST') {
throw err;
}
}
fs.writeFileSync(filePath, json);
}
/**
* Ensure that given source is an array (webpack 5 switches a lot of Arrays to Sets)
* @method ensureArray
* @function
* @param {*[]|Set<any>} source
* @returns {*[]}
*/
ensureArray(source) {
if (WEBPACK_5) {
return Array.from(source);
}
return source;
}
}
export { defaultOptions };
export default ReactLoadableSSRAddon;

View File

@@ -0,0 +1,80 @@
import test from 'ava';
import path from 'path';
import fs from 'fs';
import waitForExpect from 'wait-for-expect';
import webpack from 'webpack';
import config from '../webpack.config';
import ReactLoadableSSRAddon, { defaultOptions } from './ReactLoadableSSRAddon';
/* eslint-disable consistent-return, import/no-dynamic-require, global-require */
let outputPath;
let manifestOutputPath;
const runWebpack = (configuration, end, callback) => {
webpack(configuration, (err, stats) => {
if (err) {
return end(err);
}
if (stats.hasErrors()) {
return end(stats.toString());
}
callback();
end();
});
};
test.beforeEach(() => {
const publicPathSanitized = config.output.publicPath.slice(1, -1);
outputPath = path.resolve('./example', publicPathSanitized);
manifestOutputPath = path.resolve(outputPath, defaultOptions.filename);
});
test.cb('outputs with default settings', (t) => {
config.plugins = [
new ReactLoadableSSRAddon(),
];
runWebpack(config, t.end, () => {
const feedback = fs.existsSync(manifestOutputPath) ? 'pass' : 'fail';
t[feedback]();
});
});
test.cb('outputs with custom filename', (t) => {
const filename = 'new-assets-manifest.json';
config.plugins = [
new ReactLoadableSSRAddon({
filename,
}),
];
runWebpack(config, t.end, () => {
const feedback = fs.existsSync(manifestOutputPath.replace(defaultOptions.filename, filename)) ? 'pass' : 'fail';
t[feedback]();
});
});
test.cb('outputs with integrity', (t) => {
config.plugins = [
new ReactLoadableSSRAddon({
integrity: true,
}),
];
runWebpack(config, t.end, async () => {
const manifest = require(`${manifestOutputPath}`);
await waitForExpect(() => {
Object.keys(manifest.assets).forEach((asset) => {
manifest.assets[asset].js.forEach(({ integrity }) => {
t.truthy(integrity);
});
});
});
});
});

View File

@@ -0,0 +1,37 @@
/**
* react-loadable-ssr-addon
* @author Marcos Gonçalves <contact@themgoncalves.com>
* @version 1.0.1
*/
import { unique } from './utils';
/**
* getBundles
* @param {object} manifest - The assets manifest content generate by ReactLoadableSSRAddon
* @param {array} chunks - Chunks list to be loaded
* @returns {array} - Assets list group by file type
*/
/* eslint-disable no-param-reassign */
function getBundles(manifest, chunks) {
if (!manifest || !chunks) { return {}; }
const assetsKey = chunks.reduce((key, chunk) => {
if (manifest.origins[chunk]) {
key = unique([...key, ...manifest.origins[chunk]]);
}
return key;
}, []);
return assetsKey.reduce((bundle, asset) => {
Object.keys(manifest.assets[asset] || {}).forEach((key) => {
const content = manifest.assets[asset][key];
if (!bundle[key]) { bundle[key] = []; }
bundle[key] = unique([...bundle[key], ...content]);
});
return bundle;
}, {});
}
/* eslint-enabled */
export default getBundles;

View File

@@ -0,0 +1,49 @@
import test from 'ava';
import path from 'path';
import getBundles from './getBundles';
import config from '../webpack.config';
import manifest from '../example/dist/react-loadable-ssr-addon'; // eslint-disable-line import/no-unresolved, import/extensions
const modules = ['./Header', './multilevel/Multilevel', './SharedMultilevel', '../../SharedMultilevel'];
const fileType = ['js'];
let bundles;
test.beforeEach(() => {
bundles = getBundles(manifest, [...manifest.entrypoints, ...modules]);
});
test('returns the correct bundle size and content', (t) => {
t.true(Object.keys(bundles).length === fileType.length);
fileType.forEach((type) => !!bundles[type]);
});
test('returns the correct bundle infos', (t) => {
fileType.forEach((type) => {
bundles[type].forEach((bundle) => {
const expectedPublichPath = path.resolve(config.output.publicPath, bundle.file);
t.true(bundle.file !== '');
t.true(bundle.hash !== '');
t.true(bundle.publicPath === expectedPublichPath);
});
});
});
test('returns nothing when there is no match', (t) => {
bundles = getBundles(manifest, ['foo-bar', 'foo', null, undefined]);
t.true(Object.keys(bundles).length === 0);
});
test('should work even with null/undefined manifest or modules', (t) => {
bundles = getBundles(manifest, null);
t.true(Object.keys(bundles).length === 0);
bundles = getBundles(null, []);
t.true(Object.keys(bundles).length === 0);
bundles = getBundles([], null);
t.true(Object.keys(bundles).length === 0);
});

View File

@@ -0,0 +1,11 @@
/**
* react-loadable-ssr-addon
* @author Marcos Gonçalves <contact@themgoncalves.com>
* @version 1.0.1
*/
import ReactLoadableSSRAddon from './ReactLoadableSSRAddon';
import getBundles from './getBundles';
module.exports = ReactLoadableSSRAddon;
module.exports.getBundles = getBundles;

View File

@@ -0,0 +1,29 @@
/**
* react-loadable-ssr-addon
* @author Marcos Gonçalves <contact@themgoncalves.com>
* @version 1.0.1
*/
import crypto from 'crypto';
/**
* Compute SRI Integrity
* @func computeIntegrity
* See {@link https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity Subresource Integrity} at MDN
* @param {array} algorithms - The algorithms you want to use when hashing `content`
* @param {string} source - File contents you want to hash
* @return {string} SRI hash
*/
function computeIntegrity(algorithms, source) {
return Array.isArray(algorithms)
? algorithms.map((algorithm) => {
const hash = crypto
.createHash(algorithm)
.update(source, 'utf8')
.digest('base64');
return `${algorithm}-${hash}`;
}).join(' ')
: '';
}
export default computeIntegrity;

View File

@@ -0,0 +1,25 @@
/**
* react-loadable-ssr-addon
* @author Marcos Gonçalves <contact@themgoncalves.com>
* @version 1.0.1
*/
/**
* Get file extension
* @method getFileExtension
* @static
* @param {string} filename - File name
* @returns {string} - File extension
*/
function getFileExtension(filename) {
if (!filename || typeof filename !== 'string') { return ''; }
const fileExtRegex = /\.\w{2,4}\.(?:map|gz)$|\.\w+$/i;
const name = filename.split(/[?#]/)[0]; // eslint-disable-line prefer-destructuring
const ext = name.match(fileExtRegex);
return ext && ext.length ? ext[0] : '';
}
export default getFileExtension;

View File

@@ -0,0 +1,36 @@
import test from 'ava';
import getFileExtension from './getFileExtension';
test('returns the correct file extension', (t) => {
const extensions = ['.jpeg', '.js', '.css', '.json', '.xml'];
const filePath = 'source/static/images/hello-world';
extensions.forEach((ext) => {
t.true(getFileExtension(`${filePath}${ext}`) === ext);
});
});
test('sanitize file hash', (t) => {
const hashes = ['?', '#'];
const filePath = 'source/static/images/hello-world.jpeg';
hashes.forEach((hash) => {
t.true(getFileExtension(`${filePath}${hash}d587bbd6e38337f5accd`) === '.jpeg');
});
});
test('returns empty string when there is no file extension', (t) => {
const filePath = 'source/static/resource';
t.true(getFileExtension(filePath) === '');
});
test('should work even with null/undefined arg', (t) => {
const filePaths = ['', null, undefined];
filePaths.forEach((path) => {
t.true(getFileExtension(path) === '');
});
});

View File

@@ -0,0 +1,25 @@
/**
* react-loadable-ssr-addon
* @author Marcos Gonçalves <contact@themgoncalves.com>
* @version 1.0.1
*/
/**
* Checks if object array already contains given value
* @method hasEntry
* @function
* @param {array} target - Object array to be inspected
* @param {string} targetKey - Object key to look for
* @param {string} searchFor - Value to search existence
* @returns {boolean}
*/
export default function hasEntry(target, targetKey, searchFor) {
if (!target) { return false; }
for (let i = 0; i < target.length; i += 1) {
const file = target[i][targetKey];
if (file === searchFor) { return true; }
}
return false;
}

View File

@@ -0,0 +1,46 @@
import test from 'ava';
import hasEntry from './hasEntry';
const assets = [
{
file: 'content.chunk.js',
hash: 'd41d8cd98f00b204e9800998ecf8427e',
publicPath: './',
integrity: null,
},
{
file: 'header.chunk.js',
hash: '699f4bd49870f2b90e1d1596d362efcb',
publicPath: './',
integrity: null,
},
{
file: 'shared-multilevel.chunk.js',
hash: 'ab7b8b1c1d5083c17a39ccd2962202e1',
publicPath: './',
integrity: null,
},
];
test('should flag as has entry', (t) => {
const fileName = 'header.chunk.js';
t.true(hasEntry(assets, 'file', fileName));
});
test('should flag as has no entry', (t) => {
const fileName = 'footer.chunk.js';
t.false(hasEntry(assets, 'file', fileName));
});
test('should work even with null/undefined target', (t) => {
const targets = [[], null, undefined];
targets.forEach((target) => {
t.false(hasEntry(target, 'file', 'foo.js'));
});
});

View File

@@ -0,0 +1,4 @@
export { default as computeIntegrity } from './computeIntegrity';
export { default as getFileExtension } from './getFileExtension';
export { default as unique } from './unique';
export { default as hasEntry } from './hasEntry';

View File

@@ -0,0 +1,16 @@
/**
* react-loadable-ssr-addon
* @author Marcos Gonçalves <contact@themgoncalves.com>
* @version 1.0.1
*/
/**
* Clean array to unique values
* @method unique
* @function
* @param {array} array - Array to be inspected
* @returns {array} - Array with unique values
*/
export default function unique(array) {
return array.filter((elem, pos, arr) => arr.indexOf(elem) === pos);
}

View File

@@ -0,0 +1,22 @@
import test from 'ava';
import unique from './unique';
test('it filters duplicated entries', (t) => {
const duplicated = ['two', 'four'];
const raw = ['one', 'two', 'three', 'four'];
const filtered = unique([...raw, ...duplicated]);
duplicated.forEach((dup) => {
t.true(filtered.filter((item) => item === dup).length === 1);
});
});
test('should work with null/undefined values', (t) => {
const falsy = [null, undefined];
const raw = ['one', 'two', 'three', 'four'];
const filtered = unique([...raw, ...falsy]);
falsy.forEach((value) => {
t.true(filtered.includes(value));
});
});

View File

@@ -0,0 +1,57 @@
const webpack = require('webpack');
const path = require('path');
const ReactLoadableSSRAddon = require('./lib');
const HOST = process.env.HOST || '127.0.0.1';
const PORT = process.env.PORT || '8080';
module.exports = {
mode: 'production',
target: 'web',
entry: {
index: './example/client.jsx',
},
devtool: 'eval-cheap-module-source-map',
output: {
publicPath: '/dist/',
path: path.join(__dirname, 'example', 'dist'),
filename: '[name].js',
chunkFilename: '[name].chunk.js',
},
resolve: {
extensions: ['.js', '.jsx'],
alias: {
'react-loadable-ssr-addon': path.resolve(__dirname, './source'),
},
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components|public\/)/,
use: {
loader: 'babel-loader',
options: {
babelrc: false,
presets: [
'@babel/preset-env',
'@babel/preset-react',
],
plugins: [
require('@babel/plugin-proposal-class-properties'),
require('@babel/plugin-proposal-object-rest-spread'),
require('@babel/plugin-syntax-dynamic-import'),
require('react-loadable/babel'),
],
},
},
},
],
},
plugins: [
new ReactLoadableSSRAddon({
filename: 'react-loadable-ssr-addon.json',
}),
],
performance: false,
};