├── .circleci ├── config.yml └── setup-puppeteer.sh ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── FAQ.md ├── FEATURES.md ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── BUG.md │ ├── DOCS.md │ ├── FEATURE.md │ ├── MODIFICATION.md │ └── SUPPORT.md ├── PULL_REQUEST_TEMPLATE.md ├── funding.yml └── labels.json ├── .gitignore ├── .nvmrc ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── assets ├── 404.png ├── serve.svg ├── status-beacons-top funky.gif ├── status-beacons.gif └── status-overlay.png ├── client.js ├── codecov.yml ├── commitlint.config.js ├── lib ├── client │ ├── ClientSocket.js │ ├── client.js │ ├── hmr.js │ ├── log.js │ └── overlays │ │ ├── progress-minimal.js │ │ ├── progress.js │ │ ├── status.js │ │ └── util.js ├── errors.js ├── helpers.js ├── index.js ├── log.js ├── middleware.js ├── plugins │ ├── hmr.js │ └── ramdisk.js ├── routes.js ├── server.js ├── validate.js └── ws.js ├── package.json ├── pnpm-lock.yaml ├── recipes ├── README.md ├── angular │ ├── .eslintrc │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app.ts │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.ts │ │ │ └── app.module.ts │ │ ├── index.html │ │ └── polyfills.ts │ ├── tsconfig.json │ ├── tslint.json │ └── webpack.config.js ├── bonjour-broadcast.md ├── builtin-middleware.md ├── create-react-app │ ├── .gitignore │ ├── README.md │ ├── config-overrides.js │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── index.css │ │ ├── index.js │ │ ├── logo.svg │ │ └── serviceWorker.js │ └── webpack.config.js ├── custom-headers.md ├── dynamic-port.md ├── entry-points.md ├── four0four.md ├── internal-ip.md ├── multi-compiler.md ├── multi-entry.md ├── open-custom-uri.md ├── proxies.md ├── react-ssr │ ├── .eslintrc │ ├── README.md │ ├── client │ │ ├── Root.js │ │ └── index.js │ ├── package-lock.json │ ├── package.json │ ├── server │ │ ├── index.js │ │ └── main.js │ └── webpack.config.js ├── react │ ├── .babelrc │ ├── .eslintrc │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── App.js │ │ ├── index.js │ │ └── style.css │ └── webpack.config.js ├── refresh-on-failure │ ├── .babelrc │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── App.jsx │ │ ├── another.js │ │ └── index.js │ └── webpack.config.js ├── static-html-files.md ├── vue │ ├── .babelrc │ ├── .eslintrc │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── App.vue │ │ └── index.js │ └── webpack.config.js └── watch-static-content.md └── test ├── .eslintrc ├── errors.test.js ├── fixtures ├── https │ ├── app.js │ ├── index.html │ ├── localhost.crt │ ├── localhost.key │ ├── localhost.pfx │ └── webpack.config.js ├── multi │ ├── app.js │ ├── component.js │ ├── index.html │ ├── webpack.config.js │ ├── work.js │ └── worker.js ├── proxy │ ├── app.js │ ├── component.js │ ├── index.html │ ├── proxy-rewrite.config.js │ ├── proxy-server.js │ └── webpack.config.js ├── ramdisk-empty-pkg │ ├── app.js │ ├── package.json │ └── webpack.config.js ├── ramdisk │ ├── app.js │ ├── component.js │ ├── config-context-error.js │ ├── config-cwd-error.js │ ├── custom-options.js │ ├── cwd-error │ │ └── .gitkeep │ ├── index.html │ └── webpack.config.js ├── simple │ ├── app.js │ ├── component.js │ ├── index.html │ └── webpack.config.js └── wait-for-build │ ├── app.js │ └── make-config.js ├── helpers.test.js ├── helpers ├── port.js └── puppeteer.js ├── https.test.js ├── integration ├── force-refresh.test.js ├── multi-hmr.test.js └── single-hmr.test.js ├── middleware.test.js ├── plugin.test.js ├── proxy-rewrite.test.js ├── proxy.test.js ├── ramdisk.test.js ├── routes.test.js ├── server.test.js ├── snapshots ├── errors.test.js.md ├── errors.test.js.snap ├── plugin.test.js.md ├── plugin.test.js.snap ├── proxy-rewrite.test.js.md ├── proxy-rewrite.test.js.snap ├── proxy.test.js.md ├── proxy.test.js.snap ├── ramdisk.test.js.md ├── ramdisk.test.js.snap ├── validate.test.js.md └── validate.test.js.snap ├── validate.test.js ├── wait-for-build.test.js └── ws.test.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | dependency_cache: 4 | docker: 5 | - image: rollupcabal/circleci-node-base:latest 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | key: dependency-cache-{{ checksum "package-lock.json" }} 10 | - run: 11 | name: Update NPM 12 | command: npm install npm@latest -g 13 | - run: 14 | name: Node Info 15 | command: node --version && npm --version 16 | - run: 17 | name: Install Dependencies 18 | command: npm install 19 | - save_cache: 20 | key: dependency-cache-{{ checksum "package-lock.json" }} 21 | paths: 22 | - ./node_modules 23 | node-v14-latest: 24 | machine: true 25 | steps: 26 | - checkout 27 | - restore_cache: 28 | key: dependency-cache-{{ checksum "package-lock.json" }} 29 | - run: 30 | name: Run Tests 31 | command: | 32 | sudo apt-get update 33 | sudo apt-get install -y libgbm-dev 34 | set +e 35 | touch $BASH_ENV 36 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash 37 | export NVM_DIR="/opt/circleci/.nvm" 38 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 39 | nvm install 14 40 | nvm alias default 14 41 | node --version && npm --version 42 | npm install 43 | npm run ci:coverage 44 | - run: 45 | name: Submit coverage data to codecov. 46 | command: bash <(curl -s https://codecov.io/bash) 47 | when: on_success 48 | analysis: 49 | docker: 50 | - image: rollupcabal/circleci-node-base:latest 51 | steps: 52 | - checkout 53 | - restore_cache: 54 | key: dependency-cache-{{ checksum "package-lock.json" }} 55 | - run: 56 | name: NPM Rebuild 57 | command: npm install 58 | - run: 59 | name: Run linting 60 | command: npm run lint 61 | - run: 62 | name: Validate Commit Messages 63 | command: npm run ci:lint:commits 64 | workflows: 65 | version: 2 66 | validate: 67 | jobs: 68 | - dependency_cache 69 | - analysis: 70 | requires: 71 | - dependency_cache 72 | filters: 73 | tags: 74 | only: /.*/ 75 | - node-v14-latest: 76 | requires: 77 | - analysis 78 | filters: 79 | tags: 80 | only: /.*/ 81 | -------------------------------------------------------------------------------- /.circleci/setup-puppeteer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo apt-get update 4 | sudo apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \ 5 | libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \ 6 | libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \ 7 | libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \ 8 | ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = true 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | dist 3 | *.snap 4 | output.js 5 | *-wps-hmr.* 6 | *.hot-update.js 7 | test/**/output 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["shellscape"], 3 | "globals": { 4 | "document": true, 5 | "ʎɐɹɔosǝʌɹǝs": true, 6 | "WebSocket": true, 7 | "window": true, 8 | "__webpack_hash__": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json -diff 2 | * text=auto 3 | bin/* eol=lf -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing in webpack-plugin-serve 2 | 3 | We 💛 contributions! The rules for contributing to this org are few: 4 | 5 | 1. Don't be a jerk 6 | 1. Search issues before opening a new one 7 | 1. Lint and run tests locally before submitting a PR 8 | 1. Adhere to the code style the org has chosen 9 | 10 | 11 | ## Before Committing 12 | 13 | 1. Use at least Node.js v10.11.0 or higher. [NVM](https://github.com/creationix/nvm) can be handy for switching between Node versions. 14 | 1. Lint your changes via `npm run lint`. Fix any errors and warnings before committing. 15 | 1. Test your changes via `npm run test`. Only Pull Requests with passing tests will be accepted. 16 | -------------------------------------------------------------------------------- /.github/FEATURES.md: -------------------------------------------------------------------------------- 1 | [wps-size]: https://badgen.net/packagephobia/install/webpack-plugin-serve?label=size&color=green 2 | [wps-size-url]: https://packagephobia.now.sh/result?p=webpack-plugin-serve 3 | 4 | [ws-size]: https://badgen.net/packagephobia/install/webpack-serve?label=size&color=orange 5 | [ws-size-url]: https://packagephobia.now.sh/result?p=webpack-serve 6 | 7 | [wds-size]: https://badgen.net/packagephobia/install/webpack-dev-server?label=size&color=red 8 | [wds-size-url]: https://packagephobia.now.sh/result?p=webpack-dev-server 9 | 10 | ## Feature Comparison 11 | 12 | The grid below represents a comparison of features from the available and most used Webpack development servers. 13 | 14 | #### Key 15 | 16 | ✅ - Feature Supported
17 | ✳️ - Best Feature Support
18 | ℹ️ - Supported, with caveats
19 | ⏺ - Feature Not Needed
20 | 21 | | | webpack-plugin-serve | webpack-dev-server | webpack-serve | 22 | | :--- | :---: | :---: | :---: | 23 | | Allowed Hosts | ⏺ | ✅ | | 24 | | Bonjour Broadcasting | ℹ️ | ✅ | ℹ️ | 25 | | Configurable Host | ✳️ | ✅ | ✅ | 26 | | Configurable Client Host | ✳️ | ✅ | | 27 | | Configurable Port | ✳️ | ✅ | ✅ | 28 | | Custom Headers | ℹ️ | ✅ | ℹ️ | 29 | | Custom Middleware | ✳️ | ℹ️ | ✅ | 30 | | History Fallback | ✅ | ✅ | ✅ | 31 | | HTTPS | ✅ | ✅ | ✅ | 32 | | HTTP/2 | ✅ | | ✅ | 33 | | HTTP/2 SSL | ✅ | | | 34 | | Hot Module Replacement | ✳️ | ✅ | ✅ | 35 | | Lazy Mode | | ✅ | | 36 | | Live Reload | ✅ | | | 37 | | Open in Browser | ✳️ | ✅ | ✅ | 38 | | Request Compression | ✅ | ✅ | ℹ️ | 39 | | Progress Overlay | ✳️ | ✅ | ℹ️ | 40 | | Proxying | ✅ | ✅ | ℹ️ | 41 | | Serverless WebSockets | ✅ | | | 42 | | Status Overlay | ✳️ | ✅ | ℹ️ | 43 | | Unix Sockets | | ✅ | | 44 | | WebSocket Reconnect | ✅ | ✅ | ✅ | 45 | | Package Size | [![size][wps-size]][wps-size-url] | [![size][wds-size]][wds-size-url] | [![size][ws-size]][ws-size-url] 46 | 47 | 48 | ## Standout Features 49 | 50 | - Fully functional **Hot Module Replacement with MultiCompilers**. `webpack-plugin-serve` is the only development server for Webpack which applies HMR updates from multiple compilers/configurations to the page _without a page reload_. 51 | - Host and Port can be set to a `Promise`, which allows for dynamic host and port resolution before the server starts. 52 | - The client (browser) WebSocket host can be set, allowing full customization for contain environments. 53 | - Fully customizable middleware, including execution order of built-in middleware. Users may implement whichever middleware they desire in the order which works best for them. 54 | - Leverages the `open` module and all of it's available options, without restriction, for opening the target application in the browser automatically. 55 | - A themed, UX-consistent Build Status (errors, warnings) overlay with minimized beacon mode for monitoring the status of builds during editing. 56 | - A themed, UX-consistent set of Build Progress overlay+indicator, with minimal option. 57 | - Superior serverless WebSocket connectivity. 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Bug Report 3 | about: Something went awry and you'd like to tell us about it. 4 | 5 | --- 6 | 7 | 16 | 17 | - Webpack Version: 18 | - Operating System (or Browser): 19 | - Node Version: 20 | - webpack-plugin-serve Version: 21 | 22 | ### How Do We Reproduce? 23 | 24 | 29 | 30 | 31 | ### Expected Behavior 32 | 33 | 34 | ### Actual Behavior 35 | 36 | 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/DOCS.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 📚 Documentation 3 | about: Are the docs lacking or missing something? Do they need some new 🔥 hotness? Tell us here. 4 | 5 | --- 6 | 7 | 16 | 17 | Documentation Is: 18 | 19 | 20 | 21 | - [ ] Missing 22 | - [ ] Needed 23 | - [ ] Confusing 24 | - [ ] Not Sure? 25 | 26 | ### Please Explain in Detail... 27 | 28 | 29 | ### Your Proposal for Changes 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature Request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | 16 | 17 | ### Feature Use Case 18 | 19 | 20 | ### Feature Proposal 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/MODIFICATION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🔧 Modification Request 3 | about: Would you like something work differently? Have an alternative approach? This is the template for you. 4 | 5 | --- 6 | 7 | 16 | 17 | 18 | ### Expected Behavior / Situation 19 | 20 | 21 | ### Actual Behavior / Situation 22 | 23 | 24 | ### Modification Proposal 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/SUPPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🆘 Support, Help, and Advice 3 | about: 👉🏽 If you want to ask how to do a thing with this project, this is the place for you. 4 | 5 | --- 6 | 7 | If you arrived here because you think this project's documentation is unclear, insufficient, or wrong, please consider creating an issue for the documentation instead. 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 14 | 15 | This PR contains: 16 | 17 | - [ ] bugfix 18 | - [ ] feature 19 | - [ ] refactor 20 | - [ ] tests 21 | - [ ] documentation 22 | - [ ] metadata 23 | 24 | ### Breaking Changes? 25 | 26 | - [ ] yes 27 | - [ ] no 28 | 29 | If yes, please describe the breakage. 30 | 31 | ### Please Describe Your Changes 32 | 33 | 38 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | patreon: shellscape 2 | custom: https://paypal.me/shellscape 3 | liberapay: shellscape 4 | -------------------------------------------------------------------------------- /.github/labels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "name": "💩 template incomplete", "color": "#4E342E" }, 3 | { "name": "💩 template removed", "color": "#4E342E" }, 4 | 5 | { "name": "c¹ ⋅ discussion", "color": "#1976D2" }, 6 | { "name": "c² ⋅ feedback wanted", "color": "#F9A825" }, 7 | { "name": "c³ ⋅ PR welcome", "color": "#1B5E20" }, 8 | { "name": "c⁴ ⋅ need more info", "color": "#6A1B9A" }, 9 | { "name": "c⁵ ⋅ question", "color": "#C2185B" }, 10 | { "name": "c⁶ ⋅ request for comments", "color": "#BBDEFB" }, 11 | 12 | { "name": "p¹ ⋅ electron", "color": "#B2DFDB" }, 13 | { "name": "p² ⋅ linux", "color": "#B2DFDB" }, 14 | { "name": "p³ ⋅ mac", "color": "#B2DFDB" }, 15 | { "name": "p⁴ ⋅ windows", "color": "#B2DFDB" }, 16 | 17 | { "name": "pr¹ 🔧 chore", "color": "#D7CCC8" }, 18 | { "name": "pr² 🔧 docs", "color": "#D7CCC8" }, 19 | { "name": "pr³ 🔧 feature", "color": "#D7CCC8" }, 20 | { "name": "pr⁴ 🔧 fix", "color": "#D7CCC8" }, 21 | { "name": "pr⁵ 🔧 performance", "color": "#D7CCC8" }, 22 | { "name": "pr⁶ 🔧 refactor", "color": "#D7CCC8" }, 23 | { "name": "pr⁷ 🔧 style", "color": "#D7CCC8" }, 24 | { "name": "pr⁸ 🔧 test", "color": "#D7CCC8" }, 25 | 26 | { "name": "s¹ 🔥🔥🔥 critical", "color": "#E53935" }, 27 | { "name": "s² 🔥🔥 important", "color": "#FB8C00" }, 28 | { "name": "s³ 🔥 nice to have", "color": "#FDD835" }, 29 | { "name": "s⁴ 💧 low", "color": "#039BE5" }, 30 | { "name": "s⁵ 💧💧 inconvenient", "color": "#c0e0f7" }, 31 | 32 | { "name": "t¹ 🐞 bug", "color": "#F44336" }, 33 | { "name": "t² 📚 documentation", "color": "#FDD835" }, 34 | { "name": "t³ ✨ enhancement", "color": "#03a9f4" }, 35 | { "name": "t⁴ ✨ feature", "color": "#8bc34A" }, 36 | { "name": "t⁵ ⋅ regression", "color": "#0052cc" }, 37 | { "name": "t⁶ ⋅ todo", "color": "#311B92" }, 38 | { "name": "t⁷ ⋅ waiting on upstream", "color": "#0D47A1" }, 39 | 40 | { "name": "v¹ ⋅ alpha", "color": "#CDDC39" }, 41 | { "name": "v² ⋅ beta", "color": "#FFEB3B" }, 42 | { "name": "v³ ⋅ major", "color": "#FF9800" }, 43 | { "name": "v⁴ ⋅ minor", "color": "#FFC107" }, 44 | { "name": "v⁵ ⋅ next", "color": "#CDDC39" }, 45 | 46 | { "name": "x¹ ⋅ abandoned", "color": "#CFD8DC" }, 47 | { "name": "x² ⋅ could not reproduce", "color": "#CFD8DC" }, 48 | { "name": "x³ ⋅ duplicate", "color": "#CFD8DC" }, 49 | { "name": "x⁴ ⋅ hold", "color": "#CFD8DC" }, 50 | { "name": "x⁵ ⋅ in progress", "color": "#4CAF50" }, 51 | { "name": "x⁶ ⋅ invalid", "color": "#CFD8DC" }, 52 | { "name": "x⁷ ⋅ wontfix", "color": "#CFD8DC" } 53 | ] 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | coverage 4 | node_modules 5 | .eslintcache 6 | .idea 7 | 8 | test/**/output* 9 | output.js 10 | output.js.map 11 | *.hot-update.* 12 | wps-hmr.* 13 | 14 | .nyc_output 15 | coverage.lcov 16 | 17 | dist 18 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "name": "Attach Debugger", 8 | "port": 9229 9 | }, 10 | { 11 | "type": "node", 12 | "request": "launch", 13 | "name": "Jest Debug", 14 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 15 | "cwd": "${workspaceRoot}", 16 | "args": [ 17 | "--runInBand" 18 | ], 19 | "runtimeArgs": [ 20 | "--nolazy" 21 | ], 22 | "env": { 23 | "NODE_ENV": "development" 24 | }, 25 | "sourceMaps": true, 26 | "console": "integratedTerminal" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.format.insertSpaceBeforeFunctionParenthesis": true, 3 | "typescript.format.insertSpaceAfterConstructor": true, 4 | "typescript.format.enable": true 5 | } -------------------------------------------------------------------------------- /assets/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/webpack-plugin-serve/b19fce1db5f881af603879b8bb9c555c16ae394f/assets/404.png -------------------------------------------------------------------------------- /assets/serve.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 9 | 10 | 11 | 18 | 19 | 20 | 23 | 27 | 28 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 48 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /assets/status-beacons-top funky.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/webpack-plugin-serve/b19fce1db5f881af603879b8bb9c555c16ae394f/assets/status-beacons-top funky.gif -------------------------------------------------------------------------------- /assets/status-beacons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/webpack-plugin-serve/b19fce1db5f881af603879b8bb9c555c16ae394f/assets/status-beacons.gif -------------------------------------------------------------------------------- /assets/status-overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/webpack-plugin-serve/b19fce1db5f881af603879b8bb9c555c16ae394f/assets/status-overlay.png -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | 12 | /** 13 | * @note This file exists merely as an easy reference for folks adding it to their configuration entries 14 | */ 15 | 16 | (() => { 17 | /* eslint-disable global-require */ 18 | const { run } = require('./lib/client/client'); 19 | let hash = ''; 20 | let options; 21 | try { 22 | options = ʎɐɹɔosǝʌɹǝs; 23 | } catch (e) { 24 | const { log } = require('./lib/client/log'); 25 | log.error( 26 | 'The entry for webpack-plugin-serve was included in your build, but it does not appear that the plugin was. Please check your configuration.' 27 | ); 28 | } 29 | 30 | try { 31 | // eslint-disable-next-line camelcase 32 | hash = __webpack_hash__; 33 | } catch (e) {} // eslint-disable-line no-empty 34 | 35 | run(hash, options); 36 | })(); 37 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: master 3 | coverage: 4 | precision: 2 5 | round: down 6 | range: 70...100 7 | status: 8 | project: 'no' 9 | patch: 'yes' 10 | comment: 'off' 11 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const Configuration = { 3 | extends: ['@commitlint/config-conventional'], 4 | 5 | rules: { 6 | 'body-leading-blank': [1, 'always'], 7 | 'footer-leading-blank': [1, 'always'], 8 | 'header-max-length': [2, 'always', 72], 9 | 'scope-case': [2, 'always', 'lower-case'], 10 | 'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']], 11 | 'subject-empty': [2, 'never'], 12 | 'subject-full-stop': [2, 'never', '.'], 13 | 'type-case': [2, 'always', 'lower-case'], 14 | 'type-empty': [2, 'never'], 15 | 'type-enum': [2, 'always', [ 16 | 'build', 17 | 'chore', 18 | 'ci', 19 | 'docs', 20 | 'feat', 21 | 'fix', 22 | 'perf', 23 | 'refactor', 24 | 'revert', 25 | 'style', 26 | 'test', 27 | ], 28 | ], 29 | }, 30 | }; 31 | 32 | module.exports = Configuration; -------------------------------------------------------------------------------- /lib/client/ClientSocket.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | const { error, refresh, warn } = require('./log')(); 12 | 13 | // ignore 1008 (HTTP 400 equivalent) and 1011 (HTTP 500 equivalent) 14 | const ignoreCodes = [1008, 1011]; 15 | const maxAttempts = 10; 16 | 17 | class ClientSocket { 18 | constructor(options, ...args) { 19 | this.args = args; 20 | this.attempts = 0; 21 | this.eventHandlers = []; 22 | this.options = options; 23 | this.retrying = false; 24 | 25 | this.connect(); 26 | } 27 | 28 | addEventListener(...args) { 29 | this.eventHandlers.push(args); 30 | this.socket.addEventListener(...args); 31 | } 32 | 33 | close() { 34 | this.socket.close(); 35 | } 36 | 37 | connect() { 38 | if (this.socket) { 39 | delete this.socket; 40 | } 41 | 42 | this.connecting = true; 43 | 44 | this.socket = new WebSocket(...this.args); 45 | 46 | if (this.options.retry) { 47 | this.socket.addEventListener('close', (event) => { 48 | if (ignoreCodes.includes(event.code)) { 49 | return; 50 | } 51 | 52 | if (!this.retrying) { 53 | warn(`The WebSocket was closed and will attempt to reconnect`); 54 | } 55 | 56 | this.reconnect(); 57 | }); 58 | } else { 59 | this.socket.onclose = () => warn(`The client WebSocket was closed. ${refresh}`); 60 | } 61 | 62 | this.socket.addEventListener('open', () => { 63 | this.attempts = 0; 64 | this.retrying = false; 65 | }); 66 | 67 | if (this.eventHandlers.length) { 68 | for (const [name, fn] of this.eventHandlers) { 69 | this.socket.addEventListener(name, fn); 70 | } 71 | } 72 | } 73 | 74 | reconnect() { 75 | this.attempts += 1; 76 | this.retrying = true; 77 | 78 | if (this.attempts > maxAttempts) { 79 | error(`The WebSocket could not be reconnected. ${refresh}`); 80 | this.retrying = false; 81 | return; 82 | } 83 | 84 | const timeout = 1000 * this.attempts ** 2; 85 | 86 | setTimeout(() => this.connect(this.args), timeout); 87 | } 88 | 89 | removeEventListener(...args) { 90 | const [, handler] = args; 91 | this.eventHandlers = this.eventHandlers.filter(([, fn]) => fn === handler); 92 | this.socket.removeEventListener(...args); 93 | } 94 | } 95 | 96 | module.exports = { ClientSocket }; 97 | -------------------------------------------------------------------------------- /lib/client/client.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | /* eslint-disable global-require */ 12 | const run = (buildHash, options) => { 13 | const { address, client = {}, hmr, progress, secure, status } = options; 14 | 15 | options.firstInstance = !window.webpackPluginServe; // eslint-disable-line no-param-reassign 16 | 17 | window.webpackPluginServe = window.webpackPluginServe || { 18 | compilers: {} 19 | }; 20 | window.webpackPluginServe.silent = !!client.silent; 21 | 22 | const { ClientSocket } = require('./ClientSocket'); 23 | const { replace } = require('./hmr'); 24 | const { error, info, warn } = require('./log')(); 25 | 26 | const protocol = secure ? 'wss' : 'ws'; 27 | const socket = new ClientSocket(client, `${client.protocol || protocol}://${client.address || address}/wps`); 28 | 29 | const { compilerName } = options; 30 | 31 | window.webpackPluginServe.compilers[compilerName] = {}; 32 | 33 | // prevents ECONNRESET errors on the server 34 | window.addEventListener('beforeunload', () => socket.close()); 35 | 36 | socket.addEventListener('message', (message) => { 37 | const { action, data = {} } = JSON.parse(message.data); 38 | const { errors, hash = '', warnings } = data || {}; 39 | const shortHash = hash.slice(0, 7); 40 | const identifier = options.compilerName ? `(Compiler: ${options.compilerName}) ` : ''; 41 | const compiler = window.webpackPluginServe.compilers[compilerName]; 42 | const { wpsId } = data; 43 | 44 | switch (action) { 45 | case 'build': 46 | compiler.done = false; 47 | break; 48 | case 'connected': 49 | info(`WebSocket connected ${identifier}`); 50 | break; 51 | case 'done': 52 | compiler.done = true; 53 | break; 54 | case 'problems': 55 | if (data.errors.length) { 56 | error(`${identifier}Build ${shortHash} produced errors:\n`, errors); 57 | } 58 | if (data.warnings.length) { 59 | warn(`${identifier}Build ${shortHash} produced warnings:\n`, warnings); 60 | } 61 | break; 62 | case 'reload': 63 | window.location.reload(); 64 | break; 65 | case 'replace': 66 | // actions with a wpsId in tow indicate actions that should only be executed when the wpsId sent 67 | // matches the wpsId set in options. this is how we can identify multiple compilers in the 68 | // client. 69 | if (wpsId && wpsId === options.wpsId) { 70 | replace(buildHash, hash, hmr === 'refresh-on-failure'); 71 | } 72 | break; 73 | default: 74 | } 75 | }); 76 | 77 | if (options.firstInstance) { 78 | if (progress === 'minimal') { 79 | const { init } = require('./overlays/progress-minimal'); 80 | init(options, socket); 81 | } else if (progress) { 82 | const { init } = require('./overlays/progress'); 83 | init(options, socket); 84 | } 85 | 86 | if (status) { 87 | const { init } = require('./overlays/status'); 88 | init(options, socket); 89 | } 90 | 91 | if (module.hot) { 92 | info('Hot Module Replacement is active'); 93 | 94 | if (options.liveReload) { 95 | info('Live Reload taking precedence over Hot Module Replacement'); 96 | } 97 | } else { 98 | warn('Hot Module Replacement is inactive'); 99 | } 100 | 101 | if (!module.hot && options.liveReload) { 102 | info('Live Reload is active'); 103 | } 104 | } 105 | }; 106 | 107 | module.exports = { run }; 108 | -------------------------------------------------------------------------------- /lib/client/hmr.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | const { error, info, refresh, warn } = require('./log')(); 12 | 13 | let latest = true; 14 | 15 | const hmr = (onFailure) => { 16 | return { 17 | onUnaccepted(data) { 18 | onFailure(); 19 | warn('Change in unaccepted module(s):\n', data); 20 | warn(data); 21 | }, 22 | onDeclined(data) { 23 | onFailure(); 24 | warn('Change in declined module(s):\n', data); 25 | }, 26 | onErrored(data) { 27 | onFailure(); 28 | error('Error in module(s):\n', data); 29 | } 30 | }; 31 | }; 32 | 33 | const replace = async (buildHash, hash, refreshOnFailure) => { 34 | const { apply, check, status } = module.hot; 35 | 36 | if (hash) { 37 | // eslint-disable-next-line no-undef 38 | latest = hash.includes(buildHash); 39 | } 40 | 41 | if (!latest) { 42 | const hmrStatus = status(); 43 | 44 | if (hmrStatus === 'abort' || hmrStatus === 'fail') { 45 | warn(`An HMR update was triggered, but ${hmrStatus}ed. ${refresh}`); 46 | return; 47 | } 48 | 49 | let modules; 50 | 51 | try { 52 | modules = await check(false); 53 | } catch (e) { 54 | // noop. this typically happens when a MultiCompiler has more than one compiler that includes 55 | // this script, and an update happens with a hash that isn't part of the compiler/module this 56 | // instance was loaded for. 57 | return; 58 | } 59 | 60 | if (!modules) { 61 | warn(`No modules found for replacement. ${refresh}`); 62 | return; 63 | } 64 | 65 | modules = await apply( 66 | hmr( 67 | refreshOnFailure 68 | ? () => { 69 | if (refreshOnFailure) { 70 | // eslint-disable-next-line no-undef 71 | location.reload(); 72 | } 73 | } 74 | : () => {} 75 | ) 76 | ); 77 | 78 | if (modules) { 79 | latest = true; 80 | info(`Build ${hash.slice(0, 7)} replaced:\n`, modules); 81 | } 82 | } 83 | }; 84 | 85 | module.exports = { replace }; 86 | -------------------------------------------------------------------------------- /lib/client/log.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | const { error, info, warn } = console; 12 | const log = { 13 | error: error.bind(console, '⬡ wps:'), 14 | info: info.bind(console, '⬡ wps:'), 15 | refresh: 'Please refresh the page', 16 | warn: warn.bind(console, '⬡ wps:') 17 | }; 18 | const noop = () => {}; 19 | const silent = { 20 | error: noop, 21 | info: noop, 22 | warn: noop 23 | }; 24 | 25 | module.exports = () => (window.webpackPluginServe.silent ? silent : log); 26 | -------------------------------------------------------------------------------- /lib/client/overlays/progress-minimal.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell, Matheus Gonçalves da Silva 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | const { addCss, addHtml } = require('./util'); 12 | 13 | const ns = 'wps-progress-minimal'; 14 | const html = ` 15 |
16 |
17 |
18 | `; 19 | const css = ` 20 | #${ns} { 21 | position: fixed; 22 | top: 0; 23 | left: 0; 24 | height: 4px; 25 | width: 100vw; 26 | z-index: 2147483645; 27 | } 28 | 29 | #${ns}-bar { 30 | width: 0%; 31 | height: 4px; 32 | background-color: rgb(186, 223, 172); 33 | } 34 | 35 | @keyframes ${ns}-fade { 36 | 0% { 37 | opacity: 1; 38 | } 39 | 100% { 40 | opacity: 0; 41 | } 42 | } 43 | 44 | .${ns}-disappear { 45 | animation: ${ns}-fade .3s; 46 | animation-fill-mode: forwards; 47 | animation-delay: .5s; 48 | } 49 | 50 | .${ns}-hidden { 51 | display: none; 52 | } 53 | `; 54 | 55 | let hideOnPageVisible = false; 56 | 57 | const update = (percent) => { 58 | const bar = document.querySelector(`#${ns}-bar`); 59 | bar.style.width = `${percent}%`; 60 | }; 61 | 62 | const reset = (wrapper) => { 63 | wrapper.classList.add(`${ns}-disappear`); 64 | }; 65 | 66 | const init = (options, socket) => { 67 | if (options.firstInstance) { 68 | document.addEventListener('DOMContentLoaded', () => { 69 | addCss(css); 70 | addHtml(html); 71 | 72 | const wrapper = document.querySelector(`#${ns}`); 73 | wrapper.addEventListener('animationend', () => { 74 | update(0); 75 | wrapper.classList.add(`${ns}-hidden`); 76 | }); 77 | }); 78 | 79 | document.addEventListener('visibilitychange', () => { 80 | if (!document.hidden && hideOnPageVisible) { 81 | const wrapper = document.querySelector(`#${ns}`); 82 | reset(wrapper); 83 | hideOnPageVisible = false; 84 | } 85 | }); 86 | } 87 | 88 | socket.addEventListener('message', (message) => { 89 | const { action, data } = JSON.parse(message.data); 90 | 91 | if (action !== 'progress') { 92 | return; 93 | } 94 | 95 | const percent = Math.floor(data.percent * 100); 96 | const wrapper = document.querySelector(`#${ns}`); 97 | 98 | if (wrapper) { 99 | wrapper.classList.remove(`${ns}-hidden`, `${ns}-disappear`); 100 | } 101 | 102 | if (data.percent === 1) { 103 | if (document.hidden) { 104 | hideOnPageVisible = true; 105 | } else { 106 | reset(wrapper); 107 | } 108 | } else { 109 | hideOnPageVisible = false; 110 | } 111 | 112 | update(percent); 113 | }); 114 | }; 115 | 116 | module.exports = { 117 | init 118 | }; 119 | -------------------------------------------------------------------------------- /lib/client/overlays/progress.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell, Matheus Gonçalves da Silva 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | const { addCss, addHtml } = require('./util'); 12 | 13 | const ns = 'wps-progress'; 14 | const css = ` 15 | #${ns}{ 16 | width: 200px; 17 | height: 200px; 18 | position: fixed; 19 | right: 5%; 20 | top: 5%; 21 | transition: opacity .25s ease-in-out; 22 | z-index: 2147483645; 23 | } 24 | 25 | #${ns}-bg { 26 | fill: #282d35; 27 | } 28 | 29 | #${ns}-fill { 30 | fill: rgba(0, 0, 0, 0); 31 | stroke: rgb(186, 223, 172); 32 | stroke-dasharray: 219.99078369140625; 33 | stroke-dashoffset: -219.99078369140625; 34 | stroke-width: 10; 35 | transform: rotate(90deg)translate(0px, -80px); 36 | } 37 | 38 | #${ns}-percent { 39 | font-family: 'Open Sans'; 40 | font-size: 18px; 41 | fill: #ffffff; 42 | } 43 | 44 | #${ns}-percent-value { 45 | dominant-baseline: middle; 46 | text-anchor: middle; 47 | } 48 | 49 | #${ns}-percent-super { 50 | fill: #bdc3c7; 51 | font-size: .45em; 52 | baseline-shift: 10%; 53 | } 54 | 55 | .${ns}-noselect { 56 | -webkit-touch-callout: none; 57 | -webkit-user-select: none; 58 | -khtml-user-select: none; 59 | -moz-user-select: none; 60 | -ms-user-select: none; 61 | user-select: none; 62 | cursor: default; 63 | } 64 | 65 | @keyframes ${ns}-fade { 66 | 0% { 67 | opacity: 1; 68 | transform: scale(1); 69 | -webkit-transform: scale(1); 70 | } 71 | 100% { 72 | opacity: 0; 73 | transform: scale(0); 74 | -webkit-transform: scale(0); 75 | } 76 | } 77 | 78 | .${ns}-disappear { 79 | animation: ${ns}-fade .3s; 80 | animation-fill-mode:forwards; 81 | animation-delay: .5s; 82 | } 83 | 84 | .${ns}-hidden { 85 | display: none; 86 | } 87 | 88 | /* Put google web font at the end, or you'll see FOUC in Firefox */ 89 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700'); 90 | `; 91 | 92 | const html = ` 93 | 94 | 95 | 96 | 0% 97 | 98 | `; 99 | 100 | let hideOnPageVisible = false; 101 | 102 | const update = (percent) => { 103 | const max = -219.99078369140625; 104 | const value = document.querySelector(`#${ns}-percent-value`); 105 | const track = document.querySelector(`#${ns}-fill`); 106 | const offset = ((100 - percent) / 100) * max; 107 | 108 | track.setAttribute('style', `stroke-dashoffset: ${offset}`); 109 | value.innerHTML = percent.toString(); 110 | }; 111 | 112 | const reset = (svg) => { 113 | svg.classList.add(`${ns}-disappear`); 114 | }; 115 | 116 | const init = (options, socket) => { 117 | if (options.firstInstance) { 118 | document.addEventListener('DOMContentLoaded', () => { 119 | addCss(css); 120 | addHtml(html); 121 | 122 | // Reset progress to zero after disappear animation 123 | const svg = document.querySelector(`#${ns}`); 124 | svg.addEventListener('animationend', () => { 125 | update(0); 126 | svg.classList.add(`${ns}-hidden`); 127 | }); 128 | }); 129 | 130 | document.addEventListener('visibilitychange', () => { 131 | if (!document.hidden && hideOnPageVisible) { 132 | const svg = document.querySelector(`#${ns}`); 133 | reset(svg); 134 | hideOnPageVisible = false; 135 | } 136 | }); 137 | } 138 | 139 | socket.addEventListener('message', (message) => { 140 | const { action, data } = JSON.parse(message.data); 141 | 142 | if (action !== 'progress') { 143 | return; 144 | } 145 | 146 | const percent = Math.floor(data.percent * 100); 147 | const svg = document.querySelector(`#${ns}`); 148 | 149 | if (!svg) { 150 | return; 151 | } 152 | 153 | // we can safely call this even if it doesn't have the class 154 | svg.classList.remove(`${ns}-disappear`, `${ns}-hidden`); 155 | 156 | if (data.percent === 1) { 157 | if (document.hidden) { 158 | hideOnPageVisible = true; 159 | } else { 160 | reset(svg); 161 | } 162 | } else { 163 | hideOnPageVisible = false; 164 | } 165 | 166 | update(percent); 167 | }); 168 | }; 169 | 170 | module.exports = { init }; 171 | -------------------------------------------------------------------------------- /lib/client/overlays/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | const addHtml = (html, parent) => { 12 | const div = document.createElement('div'); 13 | const nodes = []; 14 | 15 | div.innerHTML = html.trim(); 16 | 17 | while (div.firstChild) { 18 | nodes.push((parent || document.body).appendChild(div.firstChild)); 19 | } 20 | 21 | return nodes; 22 | }; 23 | 24 | const addCss = (css) => { 25 | const style = document.createElement('style'); 26 | 27 | style.type = 'text/css'; 28 | 29 | if (css.styleSheet) { 30 | style.styleSheet.cssText = css; 31 | } else { 32 | style.appendChild(document.createTextNode(css)); 33 | } 34 | 35 | // append the stylesheet for the svg 36 | document.head.appendChild(style); 37 | }; 38 | 39 | const socketMessage = (socket, handler) => { 40 | socket.addEventListener('message', (message) => { 41 | const { action, data = {} } = JSON.parse(message.data); 42 | handler(action, data); 43 | }); 44 | }; 45 | 46 | module.exports = { addCss, addHtml, socketMessage }; 47 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | class WebpackPluginServeError extends Error { 12 | constructor(...args) { 13 | super(...args); 14 | this.name = 'WebpackPluginServeError'; 15 | } 16 | } 17 | 18 | class PluginExistsError extends WebpackPluginServeError { 19 | constructor(...args) { 20 | super(...args); 21 | this.name = 'PluginExistsError'; 22 | this.code = 'ERR_PLUGIN_EXISTS'; 23 | } 24 | } 25 | 26 | class RamdiskPathError extends WebpackPluginServeError { 27 | constructor(...args) { 28 | super(...args); 29 | this.name = 'RamdiskPathError'; 30 | this.code = 'ERR_RAMDISK_PATH'; 31 | } 32 | } 33 | 34 | module.exports = { PluginExistsError, RamdiskPathError, WebpackPluginServeError }; 35 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getMajorVersion: (version) => 3 | typeof version === 'string' && version.includes('.') ? version.split('.')[0] : false 4 | }; 5 | -------------------------------------------------------------------------------- /lib/log.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | const chalk = require('chalk'); 12 | const loglevel = require('loglevelnext'); 13 | 14 | const symbols = { ok: '⬡', whoops: '⬢' }; 15 | const colors = { 16 | trace: 'cyan', 17 | debug: 'magenta', 18 | info: 'blue', 19 | warn: 'yellow', 20 | error: 'red' 21 | }; 22 | 23 | /* istanbul ignore next */ 24 | const forceError = (...args) => { 25 | const { error } = console; 26 | error(chalk.red(`${symbols.whoops} wps:`), ...args); 27 | }; 28 | 29 | const getLogger = (options) => { 30 | const prefix = { 31 | level: ({ level }) => { 32 | const color = colors[level]; 33 | /* istanbul ignore next */ 34 | const symbol = ['error', 'warn'].includes(level) ? symbols.whoops : symbols.ok; 35 | return chalk[color](`${symbol} wps: `); 36 | }, 37 | template: '{{level}}' 38 | }; 39 | 40 | /* istanbul ignore if */ 41 | if (options.timestamp) { 42 | prefix.template = `[{{time}}] ${prefix.template}`; 43 | } 44 | 45 | /* eslint-disable no-param-reassign */ 46 | options.prefix = prefix; 47 | options.name = 'webpack-plugin-serve'; 48 | 49 | const log = loglevel.create(options); 50 | 51 | return log; 52 | }; 53 | 54 | module.exports = { forceError, getLogger }; 55 | -------------------------------------------------------------------------------- /lib/middleware.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | const convert = require('koa-connect'); 12 | const historyApiFallback = require('connect-history-api-fallback'); 13 | const koaCompress = require('koa-compress'); 14 | const koaStatic = require('koa-static'); 15 | const onetime = require('onetime'); 16 | const { createProxyMiddleware } = require('http-proxy-middleware'); 17 | 18 | const { middleware: wsMiddleware } = require('./ws'); 19 | 20 | let staticPaths = []; 21 | 22 | const render404 = (ctx) => { 23 | const faqUrl = 24 | 'https://github.com/shellscape/webpack-plugin-serve/blob/master/.github/FAQ.md#what-does-the-not-found--404-error-mean'; 25 | const body = `

Not Found / 404

26 |

27 | You may be seeing this error due to misconfigured options.
28 | Please see this FAQ question for information on possible causes and remedies. 29 |

30 |

Static Paths

31 | ${staticPaths.join('')}`; 32 | const css = ``; 77 | const html = `${css}${body}`; 78 | 79 | ctx.body = html; // eslint-disable-line no-param-reassign 80 | ctx.status = 404; // eslint-disable-line no-param-reassign 81 | }; 82 | 83 | const getBuiltins = (app, options) => { 84 | const compress = (opts) => { 85 | // only enable compress middleware if the user has explictly enabled it 86 | if (opts || options.compress) { 87 | app.use(koaCompress(opts || options.compress)); 88 | } 89 | }; 90 | 91 | const four0four = (fn = () => {}) => { 92 | app.use(async (ctx, next) => { 93 | await next(); 94 | if (ctx.status === 404) { 95 | render404(ctx); 96 | fn(ctx); 97 | } 98 | }); 99 | }; 100 | 101 | const headers = (reqHeaders) => { 102 | const headrs = reqHeaders || (options.headers || {}); 103 | app.use(async (ctx, next) => { 104 | await next(); 105 | for (const headr of Object.keys(headrs)) { 106 | ctx.set(headr, headrs[headr]); 107 | } 108 | }); 109 | }; 110 | 111 | const historyFallback = () => { 112 | if (options.historyFallback) { 113 | // https://github.com/shellscape/webpack-plugin-serve/issues/94 114 | // When using Firefox, the browser sends an accept header for /wps when using connect-history-api-fallback 115 | app.use(async (ctx, next) => { 116 | if (ctx.path.match(/^\/wps/)) { 117 | const { accept, ...reqHeaders } = ctx.request.header; 118 | ctx.request.header = reqHeaders; // eslint-disable-line no-param-reassign 119 | } 120 | await next(); 121 | }); 122 | 123 | app.use(convert(historyApiFallback(options.historyFallback))); 124 | } 125 | }; 126 | 127 | const statik = (root, opts = {}) => { 128 | const paths = [].concat(root || options.static); 129 | staticPaths = paths; 130 | for (const path of paths) { 131 | app.use(koaStatic(path, opts)); 132 | } 133 | }; 134 | 135 | const proxy = (...args) => convert(createProxyMiddleware(...args)); 136 | const websocket = () => app.use(wsMiddleware); 137 | 138 | proxy.skip = true; 139 | 140 | // by default, middleware are executed in the order they appear here. 141 | // the order of the properties returned in this object are important. 142 | return { 143 | compress: onetime(compress), 144 | headers: onetime(headers), 145 | historyFallback: onetime(historyFallback), 146 | static: onetime(statik), 147 | websocket: onetime(websocket), 148 | proxy, 149 | four0four: onetime(four0four) 150 | }; 151 | }; 152 | 153 | module.exports = { getBuiltins }; 154 | -------------------------------------------------------------------------------- /lib/plugins/hmr.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | const { HotModuleReplacementPlugin, version } = require('webpack'); 12 | 13 | const { getMajorVersion } = require('../helpers'); 14 | 15 | const { PluginExistsError } = require('../errors'); 16 | 17 | const addPlugin = (compiler) => { 18 | const hmrPlugin = new HotModuleReplacementPlugin(); 19 | hmrPlugin.apply(compiler); 20 | }; 21 | 22 | const init = function init(compiler, log) { 23 | const webpackMajorVersion = getMajorVersion(version); 24 | // eslint-disable-next-line no-param-reassign 25 | compiler.options.output = Object.assign(compiler.options.output, { 26 | hotUpdateChunkFilename: `${compiler.wpsId}-[id]-wps-hmr.js`, 27 | hotUpdateMainFilename: 28 | webpackMajorVersion >= 5 29 | ? `[runtime]-${compiler.wpsId}-wps-hmr.json` 30 | : `${compiler.wpsId}-wps-hmr.json` 31 | }); 32 | 33 | const hasHMRPlugin = compiler.options.plugins.some( 34 | (plugin) => plugin instanceof HotModuleReplacementPlugin 35 | ); 36 | 37 | /* istanbul ignore else */ 38 | if (!hasHMRPlugin) { 39 | addPlugin(compiler); 40 | } else { 41 | log.error( 42 | 'webpack-plugin-serve adds HotModuleReplacementPlugin automatically. Please remove it from your config.' 43 | ); 44 | throw new PluginExistsError( 45 | 'HotModuleReplacementPlugin exists in the specified configuration.' 46 | ); 47 | } 48 | }; 49 | 50 | module.exports = { init }; 51 | -------------------------------------------------------------------------------- /lib/plugins/ramdisk.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | /* eslint-disable no-param-reassign */ 12 | const crypto = require('crypto'); 13 | const { symlinkSync, readFileSync } = require('fs'); 14 | const { basename, join, resolve } = require('path'); 15 | 16 | const isCwd = require('is-path-cwd'); 17 | const escalade = require('escalade/sync'); 18 | const rm = require('rimraf'); 19 | const { WebpackPluginRamdisk } = require('webpack-plugin-ramdisk'); 20 | 21 | const { PluginExistsError, RamdiskPathError } = require('../errors'); 22 | 23 | const readPkgName = () => { 24 | const pkgPath = escalade(process.cwd(), (dir, names) => { 25 | if (names.includes('package.json')) { 26 | return 'package.json'; 27 | } 28 | return false; 29 | }); 30 | // istanbul ignore if 31 | if (pkgPath == null) { 32 | return null; 33 | } 34 | try { 35 | const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); 36 | return pkg.name; 37 | } catch (error) { 38 | // istanbul ignore next 39 | return null; 40 | } 41 | }; 42 | 43 | module.exports = { 44 | init(compiler, options) { 45 | const hasPlugin = compiler.options.plugins.some( 46 | (plugin) => plugin instanceof WebpackPluginRamdisk 47 | ); 48 | 49 | /* istanbul ignore if */ 50 | if (hasPlugin) { 51 | this.log.error( 52 | 'webpack-plugin-serve adds WebpackRamdiskPlugin automatically. Please remove it from your config.' 53 | ); 54 | throw new PluginExistsError('WebpackRamdiskPlugin exists in the specified configuration.'); 55 | } 56 | 57 | let pkgName = readPkgName(); 58 | const { path } = compiler.options.output; 59 | const lastSegment = basename(path); 60 | const errorInfo = `The ramdisk option creates a symlink from \`output.path\`, to the ramdisk build output path, and must remove any existing \`output.path\` to do so.`; 61 | 62 | // if output.path is /batman/batcave, and the user is running the build with wsp from 63 | // /batman/batcave, then the process will try and delete cwd, which we don't want for a number 64 | // of reasons 65 | // console.log('output.path:', resolve(path)); 66 | // console.log('process.cwd:', process.cwd()); 67 | if (isCwd(path)) { 68 | throw new RamdiskPathError( 69 | `Cannot remove current working directory. ${errorInfo} Please run from another path, or choose a different \`output.path\`.` 70 | ); 71 | } 72 | 73 | // if output.path is /batman/batcave, and the compiler context is not set and cwd is 74 | // /batman/batcave, or the context is the same as output.path, then the process will try and 75 | // delete the context directory, which probably contains src, configs, etc. throw an error 76 | // and be overly cautious rather than let the user do something bad. oddly enough, webpack 77 | // doesn't protect against context === output.path. 78 | if (resolve(compiler.context) === resolve(path)) { 79 | throw new RamdiskPathError( 80 | `Cannot remove ${path}. ${errorInfo} Please set the \`context\` to a another path, choose a different \`output.path\`.` 81 | ); 82 | } 83 | 84 | if (!pkgName) { 85 | // use md5 for a short hash that'll be consistent between wps starts 86 | const md5 = crypto.createHash('md5'); 87 | md5.update(path); 88 | pkgName = md5.digest('hex'); 89 | } 90 | 91 | const newPath = join(pkgName, lastSegment); 92 | 93 | // clean up the output path in prep for the ramdisk plugin 94 | compiler.options.output.path = newPath; 95 | 96 | this.log.info(`Ramdisk enabled`); 97 | 98 | const defaultOptions = { name: 'wps' }; 99 | const plugin = new WebpackPluginRamdisk( 100 | typeof options === 'object' ? { ...options, ...defaultOptions } : defaultOptions 101 | ); 102 | plugin.apply(compiler); 103 | 104 | rm.sync(path); 105 | 106 | symlinkSync(compiler.options.output.path, path); 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /lib/routes.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | const router = require('koa-route'); 12 | const stringify = require('json-stringify-safe'); 13 | const stripAnsi = require('strip-ansi'); 14 | 15 | const prep = (data) => stringify(data); 16 | 17 | const statsOptions = { 18 | all: false, 19 | cached: true, 20 | children: true, 21 | hash: true, 22 | modules: true, 23 | timings: true, 24 | exclude: ['node_modules', 'bower_components', 'components'] 25 | }; 26 | 27 | const setupRoutes = function setupRoutes() { 28 | const { app, options } = this; 29 | const events = ['build', 'done', 'invalid', 'progress']; 30 | const connect = async (ctx) => { 31 | if (ctx.ws) { 32 | const socket = await ctx.ws(); 33 | const send = (data) => { 34 | if (socket.readyState !== 1) { 35 | return; 36 | } 37 | socket.send(data); 38 | }; 39 | 40 | socket.build = (compilerName = '', { wpsId }) => { 41 | send(prep({ action: 'build', data: { compilerName, wpsId } })); 42 | }; 43 | 44 | socket.done = (stats, { wpsId }) => { 45 | const { hash } = stats; 46 | 47 | if (socket.lastHash === hash) { 48 | return; 49 | } 50 | 51 | send(prep({ action: 'done', data: { hash, wpsId } })); 52 | 53 | socket.lastHash = hash; 54 | 55 | const { errors = [], warnings = [] } = stats.toJson(statsOptions); 56 | 57 | if (errors.length || warnings.length) { 58 | send( 59 | prep({ 60 | action: 'problems', 61 | data: { 62 | errors: errors.slice(0).map((e) => stripAnsi(e)), 63 | hash, 64 | warnings: warnings.slice(0).map((e) => stripAnsi(e)), 65 | wpsId 66 | } 67 | }) 68 | ); 69 | 70 | if (errors.length) { 71 | return; 72 | } 73 | } 74 | 75 | if (options.hmr || options.liveReload) { 76 | const action = options.liveReload ? 'reload' : 'replace'; 77 | send(prep({ action, data: { hash, wpsId } })); 78 | } 79 | }; 80 | 81 | socket.invalid = (filePath = '', compiler) => { 82 | const context = compiler.context || compiler.options.context || process.cwd(); 83 | const fileName = (filePath && filePath.replace && filePath.replace(context, '')) || filePath; 84 | const { wpsId } = compiler; 85 | 86 | send(prep({ action: 'invalid', data: { fileName, wpsId } })); 87 | }; 88 | 89 | socket.progress = (data) => { 90 | send(prep({ action: 'progress', data })); 91 | }; 92 | 93 | for (const event of events) { 94 | this.on(event, socket[event]); 95 | 96 | socket.on('close', () => { 97 | this.removeListener(event, socket[event]); 98 | }); 99 | } 100 | 101 | // #138. handle emitted events that don't have a listener registered, and forward the message 102 | // onto the client via the socket 103 | const unhandled = ({ eventName, data }) => send(prep({ action: eventName, data })); 104 | this.on('unhandled', unhandled); 105 | socket.on('close', () => { 106 | this.off('unhandled', unhandled); 107 | }); 108 | 109 | send(prep({ action: 'connected' })); 110 | } 111 | }; 112 | 113 | app.use(router.get('/wps', connect)); 114 | }; 115 | 116 | module.exports = { setupRoutes }; 117 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | const url = require('url'); 12 | const { createServer } = require('http'); 13 | 14 | const open = require('open'); 15 | 16 | const { getBuiltins } = require('./middleware'); 17 | const { setupRoutes } = require('./routes'); 18 | 19 | const select = (options, callback) => { 20 | /* eslint-disable global-require */ 21 | const types = { 22 | http: createServer, 23 | https: require('https').createServer, 24 | http2: require('http2').createServer, 25 | http2Secure: require('http2').createSecureServer 26 | }; 27 | const { http2, https } = options; 28 | let server; 29 | let secure = false; 30 | 31 | if (http2) { 32 | if (http2 === true) { 33 | server = types.http2({}, callback); 34 | } else if (http2.cert || http2.pfx) { 35 | secure = true; 36 | server = types.http2Secure(http2, callback); 37 | } else { 38 | server = types.http2(http2, callback); 39 | } 40 | } else if (https) { 41 | secure = true; 42 | server = types.https(https === true ? {} : https, callback); 43 | } else { 44 | server = types.http(callback); 45 | } 46 | 47 | return { secure, server }; 48 | }; 49 | 50 | const start = async function start() { 51 | if (this.listening) { 52 | return; 53 | } 54 | 55 | const { app } = this; 56 | const { host, middleware, port, waitForBuild } = this.options; 57 | const builtins = getBuiltins(app, this.options); 58 | 59 | this.options.host = await host; 60 | this.options.port = await port; 61 | 62 | if (waitForBuild) { 63 | app.use(async (ctx, next) => { 64 | await this.state.compiling; 65 | await next(); 66 | }); 67 | } 68 | 69 | // allow users to add and manipulate middleware in the config 70 | await middleware(app, builtins); 71 | 72 | // call each of the builtin middleware. methods are once'd so this has no ill-effects. 73 | for (const fn of Object.values(builtins)) { 74 | if (!fn.skip) { 75 | fn(); 76 | } 77 | } 78 | 79 | setupRoutes.bind(this)(); 80 | 81 | const { secure, server } = select(this.options, app.callback()); 82 | const emitter = this; 83 | 84 | this.options.secure = secure; 85 | 86 | server.listen({ host: this.options.host, port: this.options.port }); 87 | 88 | // wait for the server to fully spin up before asking it for details 89 | await { 90 | then(r, f) { 91 | server.on('listening', () => { 92 | emitter.emit('listening', server); 93 | r(); 94 | }); 95 | server.on('error', f); 96 | } 97 | }; 98 | 99 | this.listening = true; 100 | 101 | const protocol = secure ? 'https' : 'http'; 102 | const address = server.address(); 103 | 104 | address.hostname = address.address; 105 | 106 | // fix #131 - server address reported as 127.0.0.1 for localhost 107 | if (address.hostname !== this.options.host && this.options.host === 'localhost') { 108 | address.hostname = this.options.host; 109 | } 110 | 111 | // we set this so the client can use the actual hostname of the server. sometimes the net 112 | // will mutate the actual hostname value (e.g. :: -> [::]) 113 | this.options.address = url.format(address); 114 | 115 | const uri = `${protocol}://${this.options.address}`; 116 | 117 | this.log.info('Server Listening on:', uri); 118 | 119 | this.once('done', () => { 120 | if (this.options.open) { 121 | open(uri, this.options.open === true ? {} : this.options.open); 122 | } 123 | }); 124 | }; 125 | 126 | module.exports = { start }; 127 | -------------------------------------------------------------------------------- /lib/validate.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | const { 12 | refine, 13 | any, 14 | nullable, 15 | type, 16 | partial, 17 | array, 18 | union, 19 | integer, 20 | max, 21 | boolean, 22 | string, 23 | func, 24 | literal, 25 | never, 26 | validate 27 | } = require('superstruct'); 28 | const isPromise = require('is-promise'); 29 | 30 | const promise = refine(any(), 'promise-like', (value) => isPromise(value)); 31 | 32 | module.exports = { 33 | validate(options) { 34 | const schema = partial({ 35 | allowMany: boolean(), 36 | client: partial({ 37 | address: string(), 38 | protocol: union([literal('ws'), literal('wss')]), 39 | retry: boolean(), 40 | silent: boolean() 41 | }), 42 | compress: nullable(boolean()), 43 | headers: nullable(type({})), 44 | historyFallback: union([boolean(), type({})]), 45 | hmr: union([boolean(), literal('refresh-on-failure')]), 46 | host: nullable(union([promise, string()])), 47 | http2: union([boolean(), type({})]), 48 | https: nullable(type({})), 49 | liveReload: boolean(), 50 | log: partial({ level: string(), timestamp: boolean() }), 51 | middleware: func(), 52 | open: union([boolean(), type({})]), 53 | port: union([max(integer(), 65535), promise]), 54 | progress: union([boolean(), literal('minimal')]), 55 | publicPath: nullable(string()), 56 | ramdisk: union([boolean(), type({})]), 57 | secure: never(), 58 | static: nullable( 59 | union([string(), array(string()), partial({ glob: array(string()), options: type({}) })]) 60 | ), 61 | status: boolean(), 62 | waitForBuild: boolean() 63 | }); 64 | const [error, value] = validate(options, schema); 65 | 66 | return { error, value }; 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /lib/ws.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2018 Andrew Powell 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | 8 | The above copyright notice and this permission notice shall be 9 | included in all copies or substantial portions of this Source Code Form. 10 | */ 11 | const defer = require('p-defer'); 12 | const WebSocket = require('ws'); 13 | 14 | const socketServer = new WebSocket.Server({ noServer: true }); 15 | 16 | /* eslint-disable no-param-reassign */ 17 | const middleware = async (ctx, next) => { 18 | const deferred = defer(); 19 | const { upgrade = '' } = ctx.request.headers; 20 | const upgradable = upgrade 21 | .split(',') 22 | .map((header) => header.trim()) 23 | .includes('websocket'); 24 | 25 | if (upgradable) { 26 | ctx.ws = async () => { 27 | socketServer.handleUpgrade(ctx.req, ctx.request.socket, Buffer.alloc(0), deferred.resolve); 28 | ctx.respond = false; 29 | 30 | return deferred.promise; 31 | }; 32 | } 33 | 34 | await next(); 35 | }; 36 | 37 | module.exports = { middleware }; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-plugin-serve", 3 | "version": "1.6.0", 4 | "description": "A Development Server in a Webpack Plugin", 5 | "license": "MPL-2.0", 6 | "repository": "shellscape/webpack-plugin-serve", 7 | "author": "shellscape", 8 | "homepage": "https://github.com/shellscape/webpack-plugin-serve", 9 | "bugs": "https://github.com/shellscape/webpack-plugin-serve/issues", 10 | "bin": "", 11 | "main": "lib/index.js", 12 | "engines": { 13 | "node": ">= 10.0.0 < 10.14.0 || >= 10.15.0" 14 | }, 15 | "scripts": { 16 | "ci:coverage": "nyc npm run test && nyc report --reporter=text-lcov > coverage.lcov", 17 | "ci:lint": "npm run lint && npm run security", 18 | "ci:lint:commits": "commitlint --from=${CIRCLE_BRANCH} --to=${CIRCLE_SHA1}", 19 | "ci:test": "npm run test -- --verbose --timeout=30000", 20 | "commitlint": "commitlint", 21 | "commitmsg": "commitlint -e $GIT_PARAMS", 22 | "dev": "npm run dev:clean && node node_modules/webpack-nano/bin/wp --config test/fixtures/simple/webpack.config.js", 23 | "dev:clean": "del test/fixtures/multi/output/* test/fixtures/simple/output/*", 24 | "dev:multi": "npm run dev:clean && node node_modules/webpack-nano/bin/wp --config test/fixtures/multi/webpack.config.js", 25 | "faq:toc": "echo 'temporarily disabled' # markdown-toc -i .github/FAQ.md", 26 | "lint": "eslint --fix --cache lib test", 27 | "lint-staged": "lint-staged", 28 | "security": "npm audit --audit-level=moderate", 29 | "test": "ava" 30 | }, 31 | "files": [ 32 | "client.js", 33 | "lib/", 34 | "README.md", 35 | "LICENSE" 36 | ], 37 | "peerDependencies": { 38 | "webpack": "^4.20.2 || ^5.0.0" 39 | }, 40 | "dependencies": { 41 | "chalk": "^4.0.0", 42 | "connect-history-api-fallback": "^1.5.0", 43 | "escalade": "^3.1.0", 44 | "globby": "^11.0.0", 45 | "http-proxy-middleware": "^1.0.3", 46 | "is-path-cwd": "^2.2.0", 47 | "is-promise": "^4.0.0", 48 | "json-stringify-safe": "^5.0.1", 49 | "koa": "^2.5.3", 50 | "koa-compress": "^5.0.1", 51 | "koa-connect": "^2.0.1", 52 | "koa-route": "^3.2.0", 53 | "koa-static": "^5.0.0", 54 | "loglevelnext": "^4.0.1", 55 | "nanoid": "^3.1.3", 56 | "onetime": "^5.1.0", 57 | "open": "^7.0.3", 58 | "p-defer": "^3.0.0", 59 | "rimraf": "^3.0.2", 60 | "strip-ansi": "^6.0.0", 61 | "superstruct": "^0.12.1", 62 | "webpack-plugin-ramdisk": "^0.2.0", 63 | "ws": "^7.5.3" 64 | }, 65 | "devDependencies": { 66 | "@commitlint/cli": "^11.0.0", 67 | "@commitlint/config-conventional": "^11.0.0", 68 | "ava": "^3.13.0", 69 | "cpy": "^8.1.0", 70 | "del": "^6.0.0", 71 | "del-cli": "^3.0.1", 72 | "eslint-config-shellscape": "^2.1.0", 73 | "execa": "^4.0.0", 74 | "get-port": "^5.1.1", 75 | "lint-staged": "^10.2.2", 76 | "make-dir": "^3.0.0", 77 | "node-fetch": "^2.6.1", 78 | "nyc": "^15.0.1", 79 | "pre-commit": "^1.2.2", 80 | "puppeteer": "^5.5.0", 81 | "random-js": "^2.1.0", 82 | "react-refresh": "^0.9.0", 83 | "webpack": "^4.44.2", 84 | "webpack-nano": "^1.0.0" 85 | }, 86 | "keywords": [ 87 | "dev", 88 | "development", 89 | "devserver", 90 | "serve", 91 | "server", 92 | "webpack" 93 | ], 94 | "ava": { 95 | "files": [ 96 | "!**/fixtures/**", 97 | "!**/helpers/**", 98 | "!**/recipes/**" 99 | ] 100 | }, 101 | "lint-staged": { 102 | "*.js": [ 103 | "eslint --fix" 104 | ] 105 | }, 106 | "nyc": { 107 | "include": [ 108 | "lib/**/*.js" 109 | ], 110 | "exclude": [ 111 | "lib/client*.js", 112 | "test/" 113 | ] 114 | }, 115 | "pre-commit": "lint-staged" 116 | } 117 | -------------------------------------------------------------------------------- /recipes/README.md: -------------------------------------------------------------------------------- 1 | ## 🍲 `webpack-plugin-serve` Recipies 2 | 3 | Recipes are snippets of code and useful tidbits that may be helpful to developers working to accomplish goals in similar scenarios. If you would like to submit a recipe, please open a `documentation` Pull Request. 4 | 5 | 🍳 What's Cooking: 6 | 7 | [Builtin Middlewares](./builtin-middleware.md)
8 | [Bonjour Broadcast](./bonjour-broadcast.md)
9 | [Configuring Entry Points](./entry-points.md)
10 | [Custom Headers](./custom-headers.md)
11 | [Custom 404 Page](./four0four.md)
12 | [Multi Entry Setup](./multi-entry.md)
13 | [Proxies](./proxies.md)
14 | [Static HTML File](./static-html-files.md)
15 | [Using a Dynamic Port](./dynamic-port.md)
16 | [Using an Internal IP Address](./internal-ip.md)
17 | [Watching Static Content](./watch-static-content.md) 18 | 19 | Prepared dishes: 20 | 21 | [Angular](./angular/README.md)
22 | [create-react-app](./create-react-app/README.md) 23 | [refresh-on-failure](./refresh-on-failure/README.md) 24 | [React](./react/README.md)
25 | [React SSR](./react-ssr/README.md)
26 | [Vue](./vue/README.md) 27 | -------------------------------------------------------------------------------- /recipes/angular/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": "off", 4 | "import/no-unresolved": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /recipes/angular/.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directories 28 | node_modules 29 | jspm_packages 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | ### Linux template 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | ### JetBrains template 48 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 49 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 50 | 51 | # User-specific stuff: 52 | .idea/workspace.xml 53 | .idea/tasks.xml 54 | .idea/dictionaries 55 | .idea/vcs.xml 56 | .idea/jsLibraryMappings.xml 57 | 58 | # Sensitive or high-churn files: 59 | .idea/dataSources.ids 60 | .idea/dataSources.xml 61 | .idea/dataSources.local.xml 62 | .idea/sqlDataSources.xml 63 | .idea/dynamic.xml 64 | .idea/uiDesigner.xml 65 | 66 | # Gradle: 67 | .idea/gradle.xml 68 | .idea/libraries 69 | 70 | # Mongo Explorer plugin: 71 | .idea/mongoSettings.xml 72 | 73 | ## File-based project format: 74 | *.iws 75 | 76 | ## Plugin-specific files: 77 | 78 | # IntelliJ 79 | /out/ 80 | 81 | # mpeltonen/sbt-idea plugin 82 | .idea_modules/ 83 | 84 | # JIRA plugin 85 | atlassian-ide-plugin.xml 86 | 87 | # Crashlytics plugin (for Android Studio and IntelliJ) 88 | com_crashlytics_export_strings.xml 89 | crashlytics.properties 90 | crashlytics-build.properties 91 | fabric.properties 92 | # Created by .ignore support plugin (hsz.mobi) 93 | dist 94 | .idea/ 95 | typings/ 96 | -------------------------------------------------------------------------------- /recipes/angular/README.md: -------------------------------------------------------------------------------- 1 | ## 🍲 AngularJS 2 | 3 | In addition to individual recipes for feature implementation, it's often useful for folks at many levels of experience to see a framework wired up and functional with the plugin. This recipe combines a simple "Hello World" style [AngularJS](https://angularjs.org/) application with [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) and `webpack-plugin-serve`. 4 | 5 | This recipe leverages [webpack-nano](https://github.com/shellscape/webpack-nano), a very tiny, very clean webpack CLI. 6 | 7 | ## Meta 8 | 9 | The source for the original code in this recipe is https://github.com/edc4it/webpack-intro-tutorial and is licensed under GNU GPL v3.0. 10 | -------------------------------------------------------------------------------- /recipes/angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Angular-Recipe", 3 | "version": "1.0.0", 4 | "description": "A webpack-plugin-serve Recipe", 5 | "license": "GPL-3.0-or-later", 6 | "repository": "shellscape/webpack-plugin-serve/recipes/angular", 7 | "engines": { 8 | "node": ">= 10.11.0" 9 | }, 10 | "scripts": { 11 | "build": "wp --config webpack.config.js", 12 | "start": "SERVE=true wp --config webpack.config.js" 13 | }, 14 | "dependencies": { 15 | "@angularclass/hmr": "^2.1.3" 16 | }, 17 | "devDependencies": { 18 | "@angular/common": "^6.1.10", 19 | "@angular/compiler": "^6.1.10", 20 | "@angular/core": "^6.1.10", 21 | "@angular/forms": "^6.1.10", 22 | "@angular/http": "^6.1.10", 23 | "@angular/platform-browser": "^6.1.10", 24 | "@angular/platform-browser-dynamic": "^6.1.10", 25 | "@angular/router": "^6.1.10", 26 | "@types/core-js": "^2.5.0", 27 | "@types/jquery": "^3.3.16", 28 | "@types/node": "^10.11.7", 29 | "add": "^2.0.6", 30 | "awesome-typescript-loader": "^5.1.1", 31 | "core-js": "^2.5.7", 32 | "css-loader": "^1.0.0", 33 | "html-loader": "^0.5.5", 34 | "html-webpack-plugin": "^3.2.0", 35 | "jquery": "^3.3.1", 36 | "node-sass": "^4.9.3", 37 | "raw-loader": "^0.5.1", 38 | "rxjs": "^6.2.2", 39 | "sass-loader": "^7.1.0", 40 | "style-loader": "^0.23.1", 41 | "tslint": "^5.11.0", 42 | "tslint-loader": "^3.5.4", 43 | "typescript": "^3.1.3", 44 | "webpack": "^4.20.2", 45 | "webpack-filter-warnings-plugin": "^1.2.0", 46 | "webpack-nano": "^0.6.1", 47 | "webpack-plugin-serve": "^0.4.0", 48 | "zone.js": "^0.8.26" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /recipes/angular/src/app.ts: -------------------------------------------------------------------------------- 1 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 2 | import {AppModule} from './app/app.module'; 3 | 4 | platformBrowserDynamic().bootstrapModule(AppModule); // bootstrap with our module 5 | -------------------------------------------------------------------------------- /recipes/angular/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

{{message}}

2 | -------------------------------------------------------------------------------- /recipes/angular/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700'); 2 | 3 | h1 { 4 | font-family: 'Open Sans'; 5 | font-size: 3rem; 6 | text-align: center; 7 | } 8 | -------------------------------------------------------------------------------- /recipes/angular/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'message', 5 | styles: [require('./app.component.scss')], 6 | template: require('./app.component.html'), 7 | }) 8 | export class HelloComponent { 9 | message = 'na na na na na na na na BATMAN!'; 10 | } 11 | -------------------------------------------------------------------------------- /recipes/angular/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {FormsModule} from '@angular/forms'; 3 | import {BrowserModule} from '@angular/platform-browser'; 4 | import {HelloComponent} from './app.component'; 5 | 6 | @NgModule({ 7 | imports: [BrowserModule, FormsModule ], 8 | bootstrap: [HelloComponent], 9 | declarations: [HelloComponent], 10 | }) 11 | export class AppModule {} 12 | -------------------------------------------------------------------------------- /recipes/angular/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Webpack Plugin Serve Recipe: Angular 6 | 7 | 8 | loading... 9 | 10 | 11 | -------------------------------------------------------------------------------- /recipes/angular/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es7/reflect'; 2 | import 'zone.js/dist/zone'; 3 | -------------------------------------------------------------------------------- /recipes/angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2015", 4 | "target": "es5", 5 | "moduleResolution": "node", 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "lib": [ 9 | "es2015", 10 | "dom" 11 | ], 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "noImplicitAny": true, 16 | "sourceMap": true, 17 | "suppressImplicitAnyIndexErrors": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /recipes/angular/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "jsRules": {}, 4 | "rules": { 5 | "arrow-return-shorthand": true, 6 | "interface-over-type-literal": true, 7 | "no-string-literal": true, 8 | "no-unnecessary-type-assertion": true, 9 | "object-literal-shorthand": true, 10 | "ordered-imports": true, 11 | "prefer-const": [true, { "destructuring": "all" }], 12 | "prefer-object-spread": true, 13 | "quotemark": [true, "single", "avoid-escape", "avoid-template"] 14 | }, 15 | "rulesDirectory": [] 16 | } -------------------------------------------------------------------------------- /recipes/angular/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const { ContextReplacementPlugin } = require('webpack'); 5 | const FilterWarningsPlugin = require('webpack-filter-warnings-plugin'); 6 | 7 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve/client'); 8 | 9 | const outputPath = path.resolve(__dirname, 'dist'); 10 | const watch = process.env.SERVE === 'true'; 11 | 12 | module.exports = { 13 | entry: { 14 | polyfills: './src/polyfills.ts', 15 | main: ['./src/app.ts', 'webpack-plugin-serve/client'] 16 | }, 17 | mode: 'development', 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.ts$/, 22 | loader: 'awesome-typescript-loader' 23 | }, 24 | { 25 | test: /\.ts$/, 26 | enforce: 'pre', 27 | loader: 'tslint-loader' 28 | }, 29 | { 30 | test: /\.html$/, 31 | loader: 'html-loader' 32 | }, 33 | { 34 | test: /\.scss$/, 35 | loader: ['raw-loader', 'sass-loader?sourceMap'] 36 | } 37 | ] 38 | }, 39 | optimization: { 40 | splitChunks: { 41 | cacheGroups: { 42 | vendor: { 43 | chunks: 'initial', 44 | name: 'vendor', 45 | test: 'vendor' 46 | } 47 | } 48 | } 49 | }, 50 | output: { 51 | // output directory 52 | path: outputPath, 53 | // name of the generated bundle 54 | filename: '[name].js' 55 | }, 56 | plugins: [ 57 | new ContextReplacementPlugin( 58 | /@angular(\\|\/)core(\\|\/)fesm5/, 59 | path.resolve(__dirname, 'src'), 60 | {} 61 | ), 62 | new FilterWarningsPlugin({ 63 | exclude: /System.import/ 64 | }), 65 | new HtmlWebpackPlugin({ 66 | template: 'src/index.html', 67 | inject: 'body' 68 | }), 69 | new Serve({ 70 | historyFallback: true, 71 | hmr: false, 72 | liveReload: true, 73 | static: [outputPath] 74 | }) 75 | ], 76 | resolve: { 77 | extensions: ['.js', '.ts'] 78 | }, 79 | watch 80 | }; 81 | -------------------------------------------------------------------------------- /recipes/bonjour-broadcast.md: -------------------------------------------------------------------------------- 1 | ## 🍲 Bonjour Broadcast 2 | 3 | Bonjour is a popular choice for cross-network eventing. On systems or networks where an actor is awaiting an event to perform a particular action, events for `webpack-plugin-serve` can be leveraged to initiate a broadcast. 4 | 5 | ### Meat and Potatoes 6 | 7 | To get started, your `webpack` configuration should already be setup and building successfully without using `webpack-plugin-serve`. Next, you'll need the `bonjour` package installed: 8 | 9 | ```console 10 | $ npm install bonjour --save-dev 11 | ``` 12 | 13 | Next, let's get the plugin setup, and the events wired up for broadcasting: 14 | 15 | ```js 16 | const bonjour = require('bonjour')(); 17 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 18 | 19 | const serve = new Serve(); 20 | const port = 3000; // or whichever port your services are running on 21 | const type = 'http'; 22 | 23 | serve.on('listening', () => { 24 | bonjour.publish({ name: 'Batman is Listening', port, type }); 25 | }); 26 | 27 | module.exports = { 28 | ..., 29 | plugins: [serve], 30 | ... 31 | }; 32 | ``` 33 | 34 | There's certainly more than can be done with a `bonjour` broadcasting setup, but that's the bare basics of how to get it working with this plugin. 35 | 36 | If you find that your system requires clearing, or unpublishing, pending messages, you might choose to flush them when the process ends: 37 | 38 | ```js 39 | process.on('exit', () => bonjour.unpublishAll(bonjour.destroy)); 40 | ``` 41 | 42 | You'll find far more information on the capabilities of the `bonjour` module [in its documentation](https://www.npmjs.com/package/bonjour). 43 | 44 | ### 🍰 Dessert 45 | 46 | Go forth, and imbibe! 47 | -------------------------------------------------------------------------------- /recipes/builtin-middleware.md: -------------------------------------------------------------------------------- 1 | ## 🍲 Built-in Middleware 2 | 3 | Built-in middleware provides access to all of the underlying middleware that the plugin uses internally. This provides users with a maximum amount of control over behavior of the server when leveraging the `middleware` option. 4 | 5 | ### Meat and Potatoes 6 | 7 | To leverage built-in middleware, the available middleware methods need but be called with corresponding parameters from the [`Options`](../README.md#options) list. For example, if we were to want to use the `static` middleware, we'd call `builtins.static` with parameters that matched the [`static`](../README.md#static) option: 8 | 9 | ```js 10 | // webpack.config.js 11 | module.exports = { 12 | plugins: [ 13 | new WebpackPluginServe({ 14 | middleware: (app, builtins) => 15 | const glob = [ 16 | 'dist/**/public', 17 | 'dist/app/*' 18 | ]; 19 | builtins.static({ glob }); 20 | }) 21 | ] 22 | }; 23 | ``` 24 | 25 | _Note: There is no need to interact with `app`. The underlying method takes care of wiring up to `app.use`._ 26 | 27 | The same holds true for all of the available built-in middleware methods, except for `builtins.proxy`, which we'll cover in Dessert below. Currently supported built-in middleware that are available on the `builtins` parameter are: 28 | 29 | `compress` → forwards to [koa-compress](https://github.com/koajs/compress)
30 | `four0four` → handles requests that result in a 404 status. Check usage [here](./four0four.md)
31 | `headers` → applies specified custom headers to each request. Check usage [here](./custom-headers.md)
32 | `historyFallback` → forwards to [connect-history-api-fallback](https://github.com/bripkens/connect-history-api-fallback/)
33 | `proxy` → forwards to [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware)
34 | `static` → forwards to [koa-static](https://github.com/koajs/static)
35 | 36 | ### 🍰 Dessert 37 | 38 | The same patterns hold true for every built-in middleware, except for `proxy`. The `proxy` middleware is a special case, because it's very likely that users will want to add that to the `app` as they see fit, and possibly multiple times. For more proxy information, please see [Proxying](../README.md#proxying) in the README, or the [proxies recipe](./proxies.md). 39 | -------------------------------------------------------------------------------- /recipes/create-react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /recipes/create-react-app/README.md: -------------------------------------------------------------------------------- 1 | ## 🍲 Create-React-App App 2 | 3 | Since it was built to hide configuration from developers, `create-react-app` actually makes it harder to use great plugins like this one. In this recipe, we'll show how to use `create-react-app` with `webpack-plugin-serve` without having to eject. 4 | 5 | ### ☠️ Warning ☠️ 6 | 7 | This recipe is definitely neither vegan nor gluten-free. It can neither be confirmed nor denied whether it contains traces of lead or arsenic. Develop at your own risk. 8 | 9 | If you find that implementing this recipe causes unintended side effects or problems, please open a new [Documentation Issue](https://github.com/shellscape/webpack-plugin-serve/issues/new?template=DOCS.md). 10 | 11 | If you're working on a new project, *it is strongly advised* that you consider 12 | one of the following alternatives: 13 | * Neutrinojs [9.0 beta release candidate](https://github.com/neutrinojs/neutrino/issues/1129) 14 | * Finding/creating a project template and using [degit](https://github.com/Rich-Harris/degit) 15 | * A custom webpack config 16 | 17 | 🚨 You have been warned. 🚨 18 | 19 | ## Recipe 20 | 21 | With that out of the way, let's use `webpack-plugin-serve` with `create-react-app`. 22 | 23 | _Note: this entire section has already been taken care of, and the artifacts now live within this codebase. However, in order for you to effectively replicate what has been created here, it will help to write down these steps._ 24 | 25 | You'll need to create a new app using the latest version of `create-react-app`: 26 | 27 | ```console 28 | create-react-app my-app 29 | cd my-app 30 | 31 | # Install the rewiring plugins 32 | npm install -D react-app-rewired react-app-rewire-unplug 33 | 34 | # This should already be installed by create-react-app, but it's helpful to 35 | # keep track of this installation in package.json 36 | npm install -D mini-css-extract-plugin 37 | 38 | # Add webpack-plugin-serve specific packages 39 | npm install -D webpack-nano webpack-plugin-serve 40 | 41 | # Install react-hot-loader dependencies 42 | npm install -D @hot-loader/react-dom 43 | npm install react-hot-loader 44 | ``` 45 | 46 | Under the hood, `create-react-app` uses the package `react-scripts` to do most of the heavy lifting. We'll be overriding its [configuration files](https://github.com/facebook/create-react-app/tree/master/packages/react-scripts/config) using [react-app-rewired](https://github.com/timarney/react-app-rewired). 47 | 48 | _Note: these configurations change between versions of `react-scripts`, so any upgrade of it may cause these configuration overrides to break._ 49 | 50 | Also update your scripts within `package.json`: 51 | 52 | ```diff 53 | /* package.json */ 54 | "scripts": { 55 | - "start": "react-scripts start", 56 | + "start": "wp --config webpack.config.js", 57 | - "build": "react-scripts build", 58 | + "build": "react-app-rewired build", 59 | - "test": "react-scripts test", 60 | + "test": "react-app-rewired test", 61 | "eject": "react-scripts eject" 62 | } 63 | ``` 64 | 65 | ### Rewiring 66 | 67 | We've included two additional files within this recipe: 68 | * `config-overrides.js`, which defines how we're overriding the `react-scripts` webpack config 69 | * `webpack.config.js`, which just reads the overridden config for third party tools (namely, [wepack-nano](https://github.com/shellscape/webpack-nano)). 70 | 71 | The end solution will change nothing for production. Also note that the new `start` command is custom. See `package.json` `scripts` for more info. 72 | 73 | Run `npm start` to see it in action. Edit a `.css` file or a `.js` file within `src/` and you should see the application update in place. 74 | 75 | Note that none of the pre-defined `create-react-app` files have been changed, aside from `src/App.js` which now implements hot module replacement via `react-hot-loader`. 76 | -------------------------------------------------------------------------------- /recipes/create-react-app/config-overrides.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 6 | const removeWebpackPlugins = require('react-app-rewire-unplug'); 7 | const { paths } = require('react-app-rewired'); 8 | 9 | // cra-specific utility functions 10 | const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); 11 | const getClientEnvironment = require('react-scripts/config/env'); 12 | 13 | module.exports = { 14 | webpack: (config, env) => { 15 | if (env !== 'development') { 16 | // Default to create-react-app 17 | return config; 18 | } 19 | const serveOptions = { 20 | port: 3000, 21 | historyFallback: { 22 | verbose: true, 23 | index: path.join(paths.servedPath, 'index.html'), 24 | }, 25 | client: { 26 | retry: true, 27 | }, 28 | static: [ 29 | paths.appBuild, 30 | // Changes in servedPath may cause issues here, so this may require 31 | // modification 32 | paths.appPublic, 33 | ], 34 | }; 35 | config = removeWebpackPlugins(config, env, { 36 | pluginNames: [ 37 | // client environment needs to be replaced 38 | 'InterpolateHtmlPlugin', 39 | // client environment needs to be replaced 40 | 'DefinePlugin', 41 | ], 42 | verbose: true, 43 | }); 44 | // the loaders with oneOf at the time of writing are the main webpack loaders 45 | const loaders = config.module.rules.find( 46 | rule => Array.isArray(rule.oneOf) 47 | ).oneOf; 48 | // Here we're being lazy and just mutating loaders, which could be done in 49 | // a more verbose manner with an immutable operation. Note that this will 50 | // prepend the MiniCssExtractPlugin loader to make it the greediest 51 | loaders.splice( 52 | 0, 53 | 0, 54 | // This will have to be customized based on the css support you want 55 | { 56 | test: /\.css$/, 57 | use: [ 58 | { 59 | loader: MiniCssExtractPlugin.loader, 60 | options: { 61 | hmr: true, 62 | // if hmr does not work, this is a forceful method. 63 | reloadAll: true, 64 | }, 65 | }, 66 | 'css-loader', 67 | ], 68 | }, 69 | ); 70 | // mutating operation done 71 | 72 | // define clientEnvironment, which is used by a few plugins 73 | const publicUrl = paths.servedPath.replace(/\/$/, ''); 74 | const clientEnvironment = getClientEnvironment(publicUrl); 75 | 76 | const resolve = config.resolve || {}; 77 | const resolveAlias = resolve.alias || {}; 78 | return { 79 | ...config, 80 | mode: 'development', 81 | // Used by webpack-plugin-serve 82 | watch: true, 83 | entry: [ 84 | // webpackHotDevClient is removed here; other entries are the same 85 | paths.appIndexJs, 86 | //...config.entry, 87 | 'webpack-plugin-serve/client', 88 | ], 89 | resolve: { 90 | ...resolve, 91 | alias: { 92 | ...resolveAlias, 93 | // Adds in hot loader replacements for react-dom for better hmr 94 | 'react-dom': '@hot-loader/react-dom', 95 | }, 96 | }, 97 | plugins: [ 98 | ...(config.plugins || []), 99 | // Add back removed plugin with new client environment 100 | new InterpolateHtmlPlugin(HtmlWebpackPlugin, clientEnvironment.raw), 101 | // Add back removed plugin with new client environment 102 | new webpack.DefinePlugin(clientEnvironment.stringified), 103 | // This plugin is currently only configured on production in CRA 104 | // so we add it for development as well 105 | new MiniCssExtractPlugin({ 106 | filename: 'static/css/[name].css', 107 | chunkFilename: 'static/css/[id].css', 108 | }), 109 | new Serve(serveOptions), 110 | ], 111 | output: { 112 | // Required until https://github.com/facebook/create-react-app/pull/7259 113 | // or something like it gets merged 114 | path: paths.appBuild, 115 | publicPath: paths.servedPath, 116 | filename: 'static/js/bundle.js', 117 | chunkFilename: 'static/js/[name].chunk.js', 118 | }, 119 | } 120 | }, 121 | }; 122 | -------------------------------------------------------------------------------- /recipes/create-react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "/", 6 | "dependencies": { 7 | "react": "^16.8.6", 8 | "react-dom": "^16.8.6", 9 | "react-hot-loader": "^4.12.8", 10 | "react-scripts": "3.0.1" 11 | }, 12 | "scripts": { 13 | "start": "NODE_ENV=development wp --config webpack.config.js", 14 | "build": "rm -rf build && react-app-rewired build", 15 | "test": "react-app-rewired test", 16 | "eject": "react-scripts eject" 17 | }, 18 | "eslintConfig": { 19 | "extends": "react-app" 20 | }, 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | }, 33 | "devDependencies": { 34 | "@hot-loader/react-dom": "^16.8.6", 35 | "mini-css-extract-plugin": "^0.8.0", 36 | "react-app-rewire-unplug": "^0.9.0", 37 | "react-app-rewired": "^2.1.3", 38 | "webpack-nano": "^0.7.1", 39 | "webpack-plugin-serve": "^0.11.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /recipes/create-react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/webpack-plugin-serve/b19fce1db5f881af603879b8bb9c555c16ae394f/recipes/create-react-app/public/favicon.ico -------------------------------------------------------------------------------- /recipes/create-react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /recipes/create-react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /recipes/create-react-app/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /recipes/create-react-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hot } from 'react-hot-loader/root' 3 | import logo from './logo.svg'; 4 | import './App.css'; 5 | 6 | function App() { 7 | return ( 8 |
9 |
10 | logo 11 |

12 | Edit src/App.js and save to reload. 13 |

14 | 20 | Learn React 21 | 22 |
23 |
24 | ); 25 | } 26 | 27 | export default hot(App); 28 | -------------------------------------------------------------------------------- /recipes/create-react-app/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /recipes/create-react-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /recipes/create-react-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /recipes/create-react-app/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /recipes/create-react-app/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Merely reads the webpack config from react-scripts and exports the re-wired 3 | * config 4 | */ 5 | const { paths } = require('react-app-rewired'); 6 | const configFromEnv = require(paths.scriptVersion + '/config/webpack.config'); 7 | const overrides = require('./config-overrides'); 8 | 9 | const env = process.env.NODE_ENV || 'development'; 10 | const config = configFromEnv(env); 11 | 12 | module.exports = overrides.webpack(configFromEnv(config), env); 13 | -------------------------------------------------------------------------------- /recipes/custom-headers.md: -------------------------------------------------------------------------------- 1 | ## 🍲 Sending Custom Headers for Requests 2 | 3 | _(Update: As of v0.9.0, users may set Custom Headers using the `headers` option)_ 4 | 5 | Occasionally a development environment needs to mirror certain aspects of the production environment. That may mean adding custom headers to each request the server responds to. With `webpack-plugin-serve` you have two ways to do that: using [Koa](https://koajs.com) underpinnings or our custom middleware. 6 | 7 | ### Meat and Potatoes 8 | 9 | To get started, your `webpack` configuration should already be setup and building successfully without using `webpack-plugin-serve`. Next, let's get the plugin setup for custom headers using `koa` options directly: 10 | 11 | ```js 12 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 13 | 14 | // webpack.config.js 15 | module.exports = { 16 | plugins: [ 17 | new Serve({ 18 | middleware: (app, builtins) => 19 | app.use(async (ctx, next) => { 20 | await next(); 21 | ctx.set('X-Superhero', 'batman'); 22 | }) 23 | }) 24 | ] 25 | }; 26 | ``` 27 | 28 | Notice that we're tapping into the middleware chain to do our thing. In this example, we're adding a `X-Superhero: batman` header via `ctx.set('X-Superhero', 'batman');`. And that's about all there is to it. 29 | 30 | Or you could just use a simple `builtin` function we have added. To use this helper you just need an object containing all the headers and its values. 31 | 32 | ```js 33 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 34 | 35 | // webpack.config.js 36 | module.exports = { 37 | plugins: [ 38 | new Serve({ 39 | middleware: (app, builtins) => 40 | builtins.headers({ 41 | "X-Superhero": "batman" 42 | }) 43 | }) 44 | ] 45 | }; 46 | ``` 47 | 48 | ### 🍰 Dessert 49 | 50 | You deserve some cookies (or biscuits or whatever they're called in your neck of the woods). 51 | -------------------------------------------------------------------------------- /recipes/dynamic-port.md: -------------------------------------------------------------------------------- 1 | ## 🍲 Using a Dynamic Port 2 | 3 | Most of the time using a static, predefined port number will work just fine. There can be situations in which fetching an unused, free port for use with `webpack-plugin-server` can come in handy. We're going to use the [get-port](https://www.npmjs.com/package/get-port) module to demonstrate how that can be set that up with `webpack-plugin-serve`. 4 | 5 | ### Meat and Potatoes 6 | 7 | To get started, your `webpack` configuration should already be setup and building successfully without using `webpack-plugin-serve`. Next, let's get the plugin setup: 8 | 9 | ```js 10 | const getPort = require('get-port'); 11 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 12 | 13 | module.exports = { 14 | ..., 15 | plugins: [new Serve({ 16 | port: getPort() 17 | })], 18 | ... 19 | }; 20 | ``` 21 | 22 | Pretty straight-forward, right? Now, there's lots of magic going on here that users don't need to futz with; the big take-away here is that the `port` option accepts a `Promise` as a value, and `getPort` returns a `Promise`. Nothing more there to do! Bueno. Bien. Gut. 好. 23 | 24 | _Note: It's worth checking out the docs for `get-port`. You'll find that it accepts a few different options including setting a preferred port that it will default to if the port number is free._ 25 | 26 | ### 🍰 Dessert 27 | 28 | And there you have it. We have a sample app that will get an available port number and use that as the server's port. Cheers, and good eating. 29 | -------------------------------------------------------------------------------- /recipes/entry-points.md: -------------------------------------------------------------------------------- 1 | ## 🍲 Configuring `entry` With webpack-plugin-serve 2 | 3 | There are multiple patterns available for configuring the entry point for a `webpack` bundle, and these are [fairly well documented](https://webpack.js.org/concepts/entry-points/). However, adding an additional resource to an entry, as required by `webpack-plugin-serve`, may not be straight-forward for users unfamiliar with configuration customization. We'll outline several different methods below. 4 | 5 | ### Meat and Potatoes 6 | 7 | Consider the following `webpack` configurations, before and after adding the `webpack-plugin-serve` client entry: 8 | 9 | #### A single `String` entry 10 | 11 | ```js 12 | // before 13 | module.exports = { 14 | entry: 'dist/bundle.js' 15 | }; 16 | ``` 17 | 18 | ```js 19 | // after 20 | module.exports = { 21 | entry: ['dist/bundle.js', 'webpack-plugin-serve/client'] 22 | }; 23 | ``` 24 | 25 | #### An `Array` of `String` entry 26 | 27 | ```js 28 | // before 29 | module.exports = { 30 | entry: [ 31 | 'dist/bundle.js', 32 | 'dist/worker.js' 33 | ] 34 | }; 35 | ``` 36 | 37 | ```js 38 | // after 39 | module.exports = { 40 | entry: [ 41 | 'dist/bundle.js', 42 | 'dist/worker.js', 43 | 'webpack-plugin-serve/client' 44 | ] 45 | }; 46 | ``` 47 | 48 | #### An `Object` of defining a single `String` entry 49 | 50 | ```js 51 | // before 52 | module.exports = { 53 | entry: { 54 | main: 'dist/bundle.js' 55 | } 56 | }; 57 | ``` 58 | 59 | ```js 60 | // after 61 | module.exports = { 62 | entry: { 63 | main: ['dist/bundle.js', 'webpack-plugin-serve/client'] 64 | } 65 | }; 66 | ``` 67 | 68 | #### An `Object` of defining multiple `String` entries 69 | 70 | If there are multiple entry points defined for your bundle, and you'd like each entry point to support features like Hot Module Reloading, the `webpack-plugin-serve` client script must be added to each: 71 | 72 | ```js 73 | // before 74 | module.exports = { 75 | entry: { 76 | main: 'dist/bundle.js', 77 | worker: 'dist/worker.js' 78 | } 79 | }; 80 | ``` 81 | 82 | ```js 83 | // after 84 | module.exports = { 85 | entry: { 86 | main: ['dist/bundle.js', 'webpack-plugin-serve/client'], 87 | worker: ['dist/worker.js', 'webpack-plugin-serve/client'] 88 | } 89 | }; 90 | ``` 91 | 92 | ### 🍰 Dessert 93 | 94 | The examples above outline how the `webpack-plugin-serve/client` script can be added to several common `entry` patterns. The important bit is that the value, whether it be a single entry point or an `Object` with properties, be transformed into an array which includes an item with the `webpack-plugin-serve` client script. Cheers, and good eating. 95 | -------------------------------------------------------------------------------- /recipes/four0four.md: -------------------------------------------------------------------------------- 1 | ## Four0Four 2 | A middleware that handles requests that result in a 404 status 3 | 4 | ### Meat and Potatoes 5 | It requires a function that receives the `Koa` context! This function is executed every time the server receives a `404` error. 6 | 7 | With this middleware you can for example render a custom 404 page. 8 | 9 | ```js 10 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 11 | 12 | module.exports = { 13 | ..., 14 | plugins: [new Serve({ 15 | middleware: (app, builtins) => { 16 | builtins.four0four((ctx) => { 17 | // render Custom error page 18 | // do something else. 19 | }) 20 | } 21 | })], 22 | ... 23 | }; 24 | 25 | ``` 26 | 27 | ### 🍰 Dessert 28 | 29 | You deserve some cookies (or biscuits or whatever they're called in your neck of the woods). 30 | -------------------------------------------------------------------------------- /recipes/internal-ip.md: -------------------------------------------------------------------------------- 1 | ## 🍲 Using an Internal IP Address 2 | 3 | When on systems or setups which limit use of `localhost`, or in scenarios where the server should listen on the address of the internet-facing interface, the host can be set dynamically. We're going to use the [internal-ip](https://www.npmjs.com/package/internal-ip) module to demonstrate how to set that up with `webpack-plugin-serve`. 4 | 5 | ### Meat and Potatoes 6 | 7 | To get started, your `webpack` configuration should already be setup and building successfully without using `webpack-plugin-serve`. Next, let's get the plugin setup: 8 | 9 | ```js 10 | const internalIp = require('internal-ip'); 11 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 12 | 13 | module.exports = { 14 | ..., 15 | plugins: [new Serve({ 16 | host: internalIp.v4() 17 | })], 18 | ... 19 | }; 20 | ``` 21 | 22 | Pretty straight-forward, right? Now, there's lots of magic going on here that users don't need to futz with; the big take-away here is that the `host` option accepts a `Promise` as a value, and `internalIp.v4()` returns a `Promise`. Nothing more there to do! Bueno. Bien. Gut. 好. 23 | 24 | _Note: It's worth checking out the docs for `internal-ip`. You'll find that it also supports getting the IPv6 address if you'd rather use that._ 25 | 26 | ### 🍰 Dessert 27 | 28 | And there you have it. We have a sample app that will load the IP of the local machine and use that as the server's host. Cheers, and good eating. 29 | -------------------------------------------------------------------------------- /recipes/multi-compiler.md: -------------------------------------------------------------------------------- 1 | ## 🍲 MultiCompiler Configurations 2 | 3 | TODO: Note about secondary compilers needing refresh on change 4 | 5 | 6 | ### Meat and Potatoes 7 | 8 | 9 | 10 | ### 🍰 Dessert 11 | 12 | You deserve some cookies (or biscuits or whatever they're called in your neck of the woods). 13 | -------------------------------------------------------------------------------- /recipes/multi-entry.md: -------------------------------------------------------------------------------- 1 | ## 🍲 Setting up Multiple entries 2 | 3 | Certain applications or bundles may require several separate entries. Depending on a user's needs, it may be desirable to make Hot Module Replacement, live-reload, or any other client feature available for each entries, or specific entries other than the default. To accomplish that, the `webpack-plugin-serve/client` file must be included with entries on which client features are needed. We'll show you how that should look below. 4 | 5 | 6 | ### Meat and Potatoes 7 | 8 | Let's say for this example that you have an application which bundles two pages and a web worker separately: 9 | 10 | ```js 11 | 12 | 13 | module.exports = { 14 | entry: { 15 | landing: './landing.js', 16 | checkout: './checkout.js', 17 | worker: './worker.js' 18 | }, 19 | // ...rest of your config 20 | }; 21 | ``` 22 | 23 | ### Making the Meal 24 | 25 | We'll now add the client script to the entries we'd like to have client-side features added to: 26 | 27 | ```js 28 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 29 | 30 | const serve = new Serve(); 31 | 32 | module.exports = { 33 | entry: { 34 | landing: ['./landing.js', 'webpack-plugin-serve/client'], 35 | checkout: './checkout.js', 36 | worker: ['./worker.js', 'webpack-plugin-serve/client'] 37 | }, 38 | // ...rest of your config 39 | }; 40 | ``` 41 | 42 | Note that we left the client script off of one of the entries. It's not required that _all entries_ have the client script - only those you wish to use the client-side features for. 43 | 44 | 45 | ### 🍰 Dessert 46 | 47 | Go forth with a full belly and contentment, for you've enabled client-side goodies for your entries. 48 | -------------------------------------------------------------------------------- /recipes/open-custom-uri.md: -------------------------------------------------------------------------------- 1 | ## 🍲 Opening a Custom URI 2 | 3 | The [`open`](https://github.com/shellscape/webpack-plugin-serve#open) option provides users with the ability to instruct the plugin to open the root URI of an application after the server begins listening. In some circumstances users might need to open a custom URI. This can be done relatively quickly. 4 | 5 | _Note: `webpack-plugin-serve` endeavors to directly pass-through options for dependencies, rather than wrap custom options sets, parse them, and pass them onto dependencies. Such is the case for the `open` module and the `open` option._ 6 | 7 | ### Meat and Potatoes 8 | 9 | To get started, your `webpack` configuration should already be setup and building successfully without using `webpack-plugin-serve`. Next, let's get the plugin setup for opening a custom URI: 10 | 11 | ```js 12 | const open = require('open'); 13 | const { WebpackPluginServe } = require('webpack-plugin-serve'); 14 | 15 | const serve = new WebpackPluginServe(); 16 | 17 | // we'll listen for the `listening` event, which tells us the server is up and running 18 | serve.on('listening', () => { 19 | const uri = 'http://localhost:5555/#/local'; 20 | const opts = {}; 21 | open(uri, opts); 22 | }); 23 | 24 | // webpack.config.js 25 | module.exports = { 26 | plugins: [ serve ] 27 | }; 28 | ``` 29 | 30 | And that's all there is to it. 31 | 32 | ### 🍰 Dessert 33 | 34 | You deserve some cookies (or biscuits or whatever they're called in your neck of the woods). 35 | -------------------------------------------------------------------------------- /recipes/proxies.md: -------------------------------------------------------------------------------- 1 | ## 🍲 Proxies 2 | 3 | Proxying some URLs can be useful when you have an API backend server and you want to send API requests on the same domain or if during development you wan't to simulate the correct urls that are going to work in a real scenario. 4 | 5 | Proxying is supported via [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) module but it doesn't contain any fancy options processing for proxying it just provides access directly. 6 | 7 | _Note: The `app.use(...)` call here is slightly different than what Express users are used to seeing with `http-proxy-middleware`. This is due to subtle differences in how the module interacts with `Koa`, which is used under the hood in this plugin._ 8 | 9 | ### Meat and Potatoes 10 | 11 | To get started, your `webpack` configuration should be setup and building successfully. Next, you have to know which local path you wan't to proxy to which location target. We are going to setup this using the middleware option. 12 | 13 | _Note: We assume that the configuration for `webpack-plugin-serve` is the dafault one, i.e, `port:55555` and `host:localhost`_ 14 | 15 | #### Simple route 16 | 17 | We want to proxy our local urls (`/api`) to `http://localhost:3000`. When a request to `localhost:55555/api` is done, it is going to be proxied to `localhost:3000/api`. 18 | 19 | ```js 20 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 21 | 22 | module.exports = { 23 | ..., 24 | plugins: [new Serve({ 25 | middleware: (app, builtins) => { 26 | app.use(builtins.proxy('/api', { 27 | target: 'http://localhost:3000' 28 | })) 29 | } 30 | })], 31 | ... 32 | }; 33 | 34 | ``` 35 | 36 | Since `builtins.proxy` gives you access to `http-proxy-middleware`, you can doanything `http-proxy-middleware` accepts. 37 | 38 | This will proxy everything from `localhost:55555/api` to `localhost:3000/api` 39 | 40 | #### Path Rewrite 41 | 42 | Now, let's say you don't want to proxy to the same path that your local path, when requesting to `/api` you want to proxy to `localhost:3000` directly instead. You are looking for one of the `http-proxy-middleware` options that is called `pathRewrite`. 43 | 44 | ```js 45 | module.exports = { 46 | ..., 47 | plugins: [new Serve({ 48 | middleware: (app, builtins) => { 49 | app.use(builtins.proxy('/api', { 50 | target: 'http://localhost:3000', 51 | pathRewrite: { 52 | '/api': '' 53 | } 54 | })) 55 | } 56 | })], 57 | ... 58 | }; 59 | ``` 60 | 61 | When you request to `locahost:55555/api` it is going to be proxied to `localhost:3000` instead. Now you don't need to have the prefix `/api` on your target server to make it work. 62 | 63 | Simple? Don't you think? You can simply look at [`http-proxy-middleware`](https://github.com/chimurai/http-proxy-middleware) docs if you have any doubts. 64 | 65 | ### 🍰 Dessert 66 | 67 | And there you have it. Now you can proxy a different server into your local domain! 68 | -------------------------------------------------------------------------------- /recipes/react-ssr/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 9, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "jsx": true, 7 | "modules": true 8 | } 9 | }, 10 | "rules": { 11 | "import/no-extraneous-dependencies": "off", 12 | "import/no-unresolved": "off", 13 | "no-unused-vars": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /recipes/react-ssr/README.md: -------------------------------------------------------------------------------- 1 | ## 🍲 React App With SSR 2 | 3 | No so feature-rich react example, but with server-side rendering and server hot reloading 4 | -------------------------------------------------------------------------------- /recipes/react-ssr/client/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Root() { 4 | return

Hello world!!!

; 5 | } 6 | -------------------------------------------------------------------------------- /recipes/react-ssr/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import Root from './Root'; 5 | 6 | const elem = document.getElementById('react'); 7 | 8 | ReactDOM.hydrate(, elem); 9 | 10 | if (module.hot) { 11 | module.hot.accept('./Root', () => { 12 | const NextRoot = require('./Root').default; 13 | ReactDOM.hydrate(, elem); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /recipes/react-ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app-with-ssr", 3 | "version": "1.0.0", 4 | "description": "A webpack-plugin-serve recipe", 5 | "repository": "shellscape/webpack-plugin-serve/recipes/react-ssr", 6 | "main": "index.js", 7 | "engines": { 8 | "node": ">= 10.11.0" 9 | }, 10 | "scripts": { 11 | "start": "cross-env NODE_ENV=development wp --config ./webpack.config", 12 | "build": "cross-env NODE_ENV=production wp --config ./webpack.config", 13 | "prod": "npm run build && node ./server" 14 | }, 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@babel/core": "7.2.2", 18 | "@babel/preset-env": "7.3.1", 19 | "@babel/preset-react": "7.0.0", 20 | "babel-loader": "8.0.5", 21 | "cross-env": "5.2.0", 22 | "import-fresh": "3.0.0", 23 | "webpack": "4.29.1", 24 | "webpack-nano": "0.6.1", 25 | "webpack-node-externals": "1.7.2", 26 | "webpack-plugin-serve": "0.6.0" 27 | }, 28 | "dependencies": { 29 | "koa": "2.7.0", 30 | "koa-static": "5.0.0", 31 | "react": "16.7.0", 32 | "react-dom": "16.7.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /recipes/react-ssr/server/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const Koa = require('koa'); 4 | const statik = require('koa-static'); 5 | 6 | const DIST_DIR = path.resolve(__dirname, '..', 'dist'); 7 | const renderer = require(path.resolve(DIST_DIR, 'server.js')); 8 | 9 | const app = new Koa(); 10 | app.use(renderer); 11 | app.use(statik(DIST_DIR)); 12 | 13 | app.listen(3000, () => { 14 | console.log(`Server started at port ${3000}`); 15 | }); 16 | -------------------------------------------------------------------------------- /recipes/react-ssr/server/main.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const { renderToString } = require('react-dom/server'); 3 | 4 | const Root = require('../client/Root').default; 5 | 6 | function render() { 7 | const markup = renderToString(); 8 | 9 | return ` 10 | 11 | 12 | 13 |
${markup}
14 | 15 | 16 | 17 | `; 18 | } 19 | 20 | module.exports = async (ctx, next) => { 21 | ctx.body = `${render()}`; 22 | await next(); 23 | }; 24 | -------------------------------------------------------------------------------- /recipes/react-ssr/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 4 | const importFresh = require('import-fresh'); 5 | const nodeExternals = require('webpack-node-externals'); 6 | 7 | const SRC_DIR_CLIENT = path.resolve(__dirname, 'client'); 8 | const SRC_DIR_SERVER = path.resolve(__dirname, 'server'); 9 | const DIST_DIR = path.resolve(__dirname, 'dist'); 10 | 11 | const serve = new Serve({ 12 | port: 3000, 13 | static: [DIST_DIR], 14 | waitForBuild: true, 15 | middleware(app) { 16 | app.use(async (ctx, next) => { 17 | const renderer = importFresh(path.resolve(DIST_DIR, 'server.js')); 18 | await renderer(ctx, next); 19 | }); 20 | } 21 | }); 22 | 23 | function createConfig(opts) { 24 | const { name } = opts; 25 | const isServer = name === 'server'; 26 | const optimize = process.env.NODE_ENV === 'production'; 27 | 28 | return { 29 | name, 30 | mode: optimize ? 'production' : 'development', 31 | entry: isServer 32 | ? { server: './server/main.js' } 33 | : { client: ['./client/index.js', ...(optimize ? [] : ['webpack-plugin-serve/client'])] }, 34 | output: { 35 | path: DIST_DIR, 36 | filename: '[name].js', 37 | libraryTarget: isServer ? 'commonjs2' : 'var' 38 | }, 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.js$/, 43 | include: [SRC_DIR_SERVER, SRC_DIR_CLIENT], 44 | use: [ 45 | { 46 | loader: 'babel-loader', 47 | options: { 48 | presets: [ 49 | [ 50 | '@babel/preset-env', 51 | { 52 | modules: false, 53 | targets: isServer ? { node: 'current' } : '> 0.5%, last 2 versions, not dead' 54 | } 55 | ], 56 | ['@babel/preset-react', { development: !optimize }] 57 | ] 58 | } 59 | } 60 | ] 61 | } 62 | ] 63 | }, 64 | plugins: [isServer ? serve.attach() : serve], 65 | target: isServer ? 'node' : 'web', 66 | ...(isServer ? { externals: nodeExternals() } : {}), 67 | watch: !isServer && !optimize 68 | }; 69 | } 70 | 71 | module.exports = [ 72 | createConfig({ 73 | name: 'client' 74 | }), 75 | createConfig({ 76 | name: 'server' 77 | }) 78 | ]; 79 | -------------------------------------------------------------------------------- /recipes/react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "react-hot-loader/babel" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /recipes/react/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "jsx": true, 7 | "modules": true 8 | } 9 | }, 10 | "rules": { 11 | "import/no-extraneous-dependencies": "off", 12 | "import/no-unresolved": "off", 13 | "no-unused-vars": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /recipes/react/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Dan Abramov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /recipes/react/README.md: -------------------------------------------------------------------------------- 1 | ## 🍲 React App 2 | 3 | In addition to individual recipes for feature implementation, it's often useful for folks at many levels of experience to see a framework wired up and functional with the plugin. This recipe combines a simple "Hello World" style [React](https://reactjs.org) application with [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin), [react-hot-loader](https://github.com/gaearon/react-hot-loader) and `webpack-plugin-serve`. This project also has initial setup for fonts, images and css. 4 | 5 | This recipe leverages [webpack-nano](https://github.com/shellscape/webpack-nano), a very tiny, very clean webpack CLI. 6 | 7 | ## Meta 8 | 9 | This recipe was heavily inspired by https://github.com/gaearon/react-hot-loader/tree/master/examples/webpack-modern and some modifications made based on experience and personal preference. The original is licensed under MIT. 10 | -------------------------------------------------------------------------------- /recipes/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app", 3 | "version": "1.0.0", 4 | "description": "A webpack-plugin-serve Recipe", 5 | "repository": "shellscape/webpack-plugin-serve/recipes/react", 6 | "main": "index.js", 7 | "engines": { 8 | "node": ">= 10.11.0" 9 | }, 10 | "scripts": { 11 | "start": "cross-env NODE_ENV=development wp --config ./webpack.config", 12 | "build": "cross-env NODE_ENV=production wp --config ./webpack.config" 13 | }, 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@babel/core": "^7.1.2", 17 | "@babel/polyfill": "^7.0.0", 18 | "@babel/preset-env": "^7.1.0", 19 | "@babel/preset-react": "^7.0.0", 20 | "babel-loader": "^8.0.4", 21 | "cross-env": "^5.2.0", 22 | "css-loader": "^3.2.0", 23 | "file-loader": "^4.2.0", 24 | "html-webpack-plugin": "^3.2.0", 25 | "react": "^16.6.0", 26 | "react-dom": "^16.6.0", 27 | "react-hot-loader": "^4.3.11", 28 | "style-loader": "^1.0.0", 29 | "url-loader": "^2.1.0", 30 | "webpack": "^4.23.1", 31 | "webpack-nano": "^0.7.1", 32 | "webpack-plugin-serve": "^0.12.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /recipes/react/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hot } from 'react-hot-loader'; 3 | import './style.css'; 4 | 5 | const App = () =>

Hi my world

; 6 | 7 | export default hot(module)(App); 8 | -------------------------------------------------------------------------------- /recipes/react/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | const render = () => { 7 | const root = document.createElement('div'); 8 | document.body.appendChild(root); 9 | 10 | ReactDOM.render(, root); 11 | }; 12 | 13 | render(); 14 | -------------------------------------------------------------------------------- /recipes/react/src/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /recipes/react/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | const webpack = require('webpack'); 4 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | 7 | const watch = process.env.NODE_ENV === 'development'; 8 | const outputPath = resolve(__dirname, 'dist'); 9 | 10 | module.exports = { 11 | entry: ['@babel/polyfill', './src/index.js', 'webpack-plugin-serve/client'], 12 | mode: process.env.NODE_ENV, 13 | devtool: 'cheap-eval-source-map', 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(js|jsx)$/, 18 | exclude: /node_modules/, 19 | use: ['babel-loader'] 20 | }, 21 | { 22 | test: /\.css$/, 23 | use: ['style-loader', 'css-loader'] 24 | }, 25 | { 26 | test: /\.woff(\?.*)?$/, 27 | use: { 28 | loader: 'url-loader', 29 | options: { 30 | name: 'fonts/[name].[ext]', 31 | mimetype: 'application/font-woff' 32 | } 33 | } 34 | }, 35 | { 36 | test: /\.woff2(\?.*)?$/, 37 | use: { 38 | loader: 'url-loader', 39 | options: { 40 | name: 'fonts/[name].[ext]', 41 | mimetype: 'application/font-woff2' 42 | } 43 | } 44 | }, 45 | { 46 | test: /\.(otf)(\?.*)?$/, 47 | use: { 48 | loader: 'file-loader', 49 | options: { 50 | name: 'fonts/[name].[ext]' 51 | } 52 | } 53 | }, 54 | { 55 | test: /\.ttf(\?.*)?$/, 56 | use: { 57 | loader: 'url-loader', 58 | options: { 59 | name: 'fonts/[name].[ext]', 60 | mimetype: 'application/octet-stream' 61 | } 62 | } 63 | }, 64 | { 65 | test: /\.svg(\?.*)?$/, 66 | use: { 67 | loader: 'url-loader', 68 | options: { 69 | name: 'images/[name].[ext]', 70 | mimetype: 'image/svg+xml' 71 | } 72 | } 73 | }, 74 | { 75 | test: /\.(png|jpg)(\?.*)?$/, 76 | use: { 77 | loader: 'url-loader', 78 | options: { 79 | name: 'images/[name].[ext]' 80 | } 81 | } 82 | } 83 | ] 84 | }, 85 | output: { 86 | path: outputPath, 87 | publicPath: '/', 88 | filename: 'bundle.js' 89 | }, 90 | plugins: [ 91 | new HtmlWebpackPlugin(), 92 | new webpack.DefinePlugin({ 93 | 'process.env': { 94 | NODE_ENV: JSON.stringify(process.env.NODE_ENV) 95 | } 96 | }), 97 | new Serve({ 98 | // note: this value is true by default 99 | hmr: true, 100 | historyFallback: true, 101 | static: [outputPath] 102 | }) 103 | ], 104 | watch 105 | }; 106 | -------------------------------------------------------------------------------- /recipes/refresh-on-failure/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "modules": false 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /recipes/refresh-on-failure/README.md: -------------------------------------------------------------------------------- 1 | ## 🍲 refresh-on-failure 2 | 3 | In this recipe, you'll see how to leverage `refresh-on-failure` option to force a refresh when HMR fails. The example uses React. To see it in action, install the dependencies first and run the example (`npm start`). When it's running, try altering the React component (`App.jsx`) first to see regular HMR. If `another.js` is modified, it will revert to refresh behavior as the module isn't being hot replaced. 4 | 5 | This recipe leverages [webpack-nano](https://github.com/shellscape/webpack-nano), a very tiny, very clean webpack CLI. 6 | -------------------------------------------------------------------------------- /recipes/refresh-on-failure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "refresh-on-failure-demo", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "wp" 8 | }, 9 | "keywords": [], 10 | "author": "Juho Vepsäläinen", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@babel/core": "^7.11.6", 14 | "@babel/preset-env": "^7.11.5", 15 | "@babel/preset-react": "^7.10.4", 16 | "@pmmmwh/react-refresh-webpack-plugin": "^0.4.2", 17 | "babel-loader": "^8.1.0", 18 | "mini-html-webpack-plugin": "^3.1.0", 19 | "prop-types": "^15.7.2", 20 | "react": "^16.13.1", 21 | "react-dom": "^16.13.1", 22 | "react-refresh": "^0.8.3", 23 | "webpack-nano": "^1.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /recipes/refresh-on-failure/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | function App({ children }) { 5 | return
Hello {children}
; 6 | } 7 | 8 | App.propTypes = { 9 | children: PropTypes.node 10 | }; 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /recipes/refresh-on-failure/src/another.js: -------------------------------------------------------------------------------- 1 | const name = 'world'; 2 | 3 | export default name; 4 | -------------------------------------------------------------------------------- /recipes/refresh-on-failure/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import App from './App.jsx'; 5 | import name from './another'; 6 | 7 | render({name}, document.getElementById('app')); 8 | -------------------------------------------------------------------------------- /recipes/refresh-on-failure/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 4 | const { MiniHtmlWebpackPlugin } = require('mini-html-webpack-plugin'); 5 | 6 | const { WebpackPluginServe: Serve } = require('../../'); 7 | 8 | module.exports = { 9 | mode: 'development', 10 | entry: ['./src', '../../client'], 11 | plugins: [ 12 | new Serve({ hmr: 'refresh-on-failure', static: ['./dist'], status: false }), 13 | new ReactRefreshPlugin({ 14 | overlay: false 15 | }), 16 | new MiniHtmlWebpackPlugin({ 17 | context: { 18 | body: '
' 19 | } 20 | }) 21 | ], 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.jsx?$/, 26 | include: path.join(__dirname, 'src'), 27 | use: 'babel-loader' 28 | } 29 | ] 30 | }, 31 | resolve: { 32 | extensions: ['.js', '.jsx'] 33 | }, 34 | watch: true 35 | }; 36 | -------------------------------------------------------------------------------- /recipes/static-html-files.md: -------------------------------------------------------------------------------- 1 | ## Static HTML Files 2 | 3 | When setting up an HTML file to bootstrap a Single Page Application users typically choose between two popular options: 4 | 5 | 1. [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) 6 | 1. A static HTML file 7 | 8 | This recipe addresses setting up a static HTML file, rather than using a plugin to generate an HTML file to serve a bundle. 9 | 10 | ### Meat and Potatoes 11 | 12 | To get started, your `webpack` configuration should be setup and building successfully. Once that's done, you'll need to create an HTML file somewhere in your project directory structure. Be sure to place the file _outside of the `output` directory_. 13 | 14 | Let's use the following directory structure as an example: 15 | 16 | ``` 17 | /the-app 18 | |_ dist 19 | |_ js 20 | |_ app.js 21 | |_ static 22 | |_ index.html 23 | ``` 24 | 25 | Where `/the-app/dist` is the `output` webpack output directory. A configuration for this example setup would then resemble the following: 26 | 27 | ```js 28 | const path = require('path'); 29 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 30 | const outputPath = path.resolve('./dist'); 31 | 32 | module.exports = { 33 | entry: ['./js/app.js', 'webpack-plugin-serve/client'], 34 | output: { 35 | filename: 'bundle.js' 36 | path: outputPath 37 | }, 38 | plugins: [ 39 | new Serve({ 40 | static: [outputPath, path.resolve('./static')] 41 | }) 42 | ] 43 | } 44 | ``` 45 | 46 | An HTML file for this setup might resemble: 47 | 48 | ```html 49 | 50 | 51 | 52 |
53 | 54 | 55 | 56 | ``` 57 | 58 | ### 🍰 Dessert 59 | 60 | Webpack provides for a multitude of different output configurations, and your needs will likely vary from the examples in this recipe. The important parts to assert are configured correctly are: 61 | 62 | - the `static` path(s) for `WebpackPluginServe. Any path(s) specified will be be accessible from the website root. 63 | - the `output.publicPath` property, which should be set if your build directory is within a directory specified in `static` 64 | - the `src` property for the ` 10 | 11 | 16 | -------------------------------------------------------------------------------- /recipes/vue/src/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App'; 3 | 4 | const render = () => { 5 | const el = document.createElement('div'); 6 | document.body.appendChild(el); 7 | 8 | new Vue({ 9 | el, 10 | render: h => h(App) 11 | }) 12 | } 13 | 14 | render() 15 | -------------------------------------------------------------------------------- /recipes/vue/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | const webpack = require('webpack'); 4 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const { VueLoaderPlugin } = require('vue-loader'); 7 | 8 | const mode = process.env.NODE_ENV; 9 | const isDev = mode === 'development'; 10 | const outputPath = resolve(__dirname, 'dist'); 11 | 12 | module.exports = { 13 | entry: ['./src/index.js', ...(isDev ? ['webpack-plugin-serve/client'] : [])], 14 | mode, 15 | devtool: 'cheap-eval-source-map', 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.vue$/, 20 | exclude: /node_modules/, 21 | loader: 'vue-loader' 22 | }, 23 | { 24 | test: /\.jsx?$/, 25 | exclude: /node_modules/, 26 | loader: 'babel-loader?cacheDirectory' 27 | }, 28 | { 29 | test: /\.css$/, 30 | use: ['vue-style-loader', 'css-loader'] 31 | }, 32 | { 33 | test: /\.woff(\?.*)?$/, 34 | use: { 35 | loader: 'url-loader', 36 | options: { 37 | name: 'fonts/[name].[ext]', 38 | mimetype: 'application/font-woff' 39 | } 40 | } 41 | }, 42 | { 43 | test: /\.woff2(\?.*)?$/, 44 | use: { 45 | loader: 'url-loader', 46 | options: { 47 | name: 'fonts/[name].[ext]', 48 | mimetype: 'application/font-woff2' 49 | } 50 | } 51 | }, 52 | { 53 | test: /\.(otf)(\?.*)?$/, 54 | use: { 55 | loader: 'file-loader', 56 | options: { 57 | name: 'fonts/[name].[ext]' 58 | } 59 | } 60 | }, 61 | { 62 | test: /\.ttf(\?.*)?$/, 63 | use: { 64 | loader: 'url-loader', 65 | options: { 66 | name: 'fonts/[name].[ext]', 67 | mimetype: 'application/octet-stream' 68 | } 69 | } 70 | }, 71 | { 72 | test: /\.svg(\?.*)?$/, 73 | use: { 74 | loader: 'url-loader', 75 | options: { 76 | name: 'images/[name].[ext]', 77 | mimetype: 'image/svg+xml' 78 | } 79 | } 80 | }, 81 | { 82 | test: /\.(png|jpg)(\?.*)?$/, 83 | use: { 84 | loader: 'url-loader', 85 | options: { 86 | name: 'images/[name].[ext]' 87 | } 88 | } 89 | } 90 | ] 91 | }, 92 | output: { 93 | path: outputPath, 94 | publicPath: '/', 95 | filename: 'bundle.js' 96 | }, 97 | resolve: { 98 | alias: { 99 | vue$: 'vue/dist/vue.esm.js' 100 | }, 101 | extensions: ['*', '.js', '.vue', '.json'] 102 | }, 103 | plugins: [ 104 | new VueLoaderPlugin(), 105 | new HtmlWebpackPlugin(), 106 | new webpack.DefinePlugin({ 107 | 'process.env': { 108 | NODE_ENV: JSON.stringify(process.env.NODE_ENV) 109 | } 110 | }), 111 | ...(isDev 112 | ? [ 113 | new Serve({ 114 | // note: this value is true by default 115 | hmr: true, 116 | historyFallback: true, 117 | static: [outputPath] 118 | }) 119 | ] 120 | : []) 121 | ], 122 | watch: isDev 123 | }; 124 | -------------------------------------------------------------------------------- /recipes/watch-static-content.md: -------------------------------------------------------------------------------- 1 | ## 🍲 Watching Static Content 2 | 3 | Watching the static files (or _content_ or _assets_) of your application can be handy. These are typically files that aren't included in your webpack bundle and can be of a variety of uses. The most typical use for watching these files is to reload the app/page when something is added or changed on the filesystem where your static files are located. 4 | 5 | _Note: There are many options for utility modules that provide clean interfaces for watching file. For demonstration purposes, we're going to use [`sane`](https://www.npmjs.com/package/sane), which is a clean wrapper around `fs.watch`. Other options include [`chokidar`](https://www.npmjs.com/package/chokidar) and [`fb-watchman`](https://www.npmjs.com/package/fb-watchman)._ 6 | 7 | ### Meat and Potatoes 8 | 9 | We're going to assume that you're using a ["zero-config"](https://webpack.js.org/configuration/) setup, in which `webpack` assumes that your bundle entrypoint(s) are located in a `src` directory, and your bundle output will be written to a `dist` directory. So let's setup a `webpack` config file for `webpack-plugin-serve` with that in mind: 10 | 11 | ```js 12 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 13 | 14 | const serve = new Serve(); 15 | 16 | module.exports = { 17 | mode: 'development', 18 | plugins: [serve], 19 | watch: true 20 | }; 21 | ``` 22 | 23 | Pretty basic, right? Some important notes about the configuration above: 24 | 25 | - `mode: 'development'` is required or else `webpack` will spew forth an annoying warning message on each build/rebuild. 26 | - `watch: true` is required so that `webpack` will enter watch mode. Defining it here means that it doesn't matter [which](https://www.npmjs.com/package/webpack-nano) [CLI](https://www.npmjs.com/package/webpack-command) [you use](https://www.npmjs.com/package/webpack-cli), watch mode will always kick in. This is important to keep `webpack` running as a long-running process. For what it's worth, we recommend [webpack-nano](https://www.npmjs.com/package/webpack-nano). 27 | 28 | ### Making the Meal 29 | 30 | Now that we've got `webpack` building, we need to wire up our watcher. First, install `sane`: 31 | 32 | ```console 33 | $ npm install sane --save-dev 34 | ``` 35 | 36 | Then let's get our hands dirty and make this do something. We'll assume that your app is located at `/app` and that your static content is located at `/app/assets`. Let's also pretend that you're doing something fancy and relying on Markdown files for some aspect of your app, and that we want the app to reload when one of them changes: 37 | 38 | ```js 39 | const sane = require('sane'); 40 | const { WebpackPluginServe: Serve } = require('webpack-plugin-serve'); 41 | 42 | const serve = new Serve({ static: ['/app/assets'] }); 43 | const watcher = sane('/app/assets', { glob: [ '**/*.md' ] }); 44 | 45 | serve.on('listening', () => { 46 | watcher.on('change', (filePath, root, stat) => { 47 | console.log('file changed', filePath); 48 | }); 49 | }); 50 | 51 | serve.on('close', () => watcher.close()); 52 | 53 | module.exports = { 54 | mode: 'development', 55 | plugins: [serve], 56 | watch: true 57 | }; 58 | ``` 59 | 60 | So we've setup our `webpack-plugin-serve` instance to serve static content from `/app/assets`. That's important; otherwise the plugin will just assume the context (in this case the location of `webpack.config.js`) is the root for the static assets. 61 | 62 | Next, we've subscribed to two events: `listening`, which fires when the underlying `Koa` server is listening, and `close`, which will fire when `webpack` is done watching files and the process is about to end. The `listening` event fires at the correct time to start watching files. The `close` event is important so that `watcher.close()` can be called and the associated handles cleaned up. Failing to do so may result in funky error messages from Node. So that's all well and good, and we have some console output telling us that a file has changed. Let's switch that up so it's actually making a difference for us: 63 | 64 | ```js 65 | serve.on('listening', () => { 66 | watcher.on('change', (filePath, root, stat) => { 67 | serve.emit('reload', { source: 'config' }); 68 | }); 69 | }); 70 | ``` 71 | 72 | The important change there is `serve.emit('reload')`. The `reload` event isn't actually handled by the `WebpackPluginServe` instance, but the plugin _does_ forward on unhandled, emitted events to all connected `WebSocket`s. The `reload` event is already handled in the client scripts as part of the `liveReload` option, so we can utilize that `WebSocket` message to reload the page. Alternatively, you might choose to connect your own `WebSocket`, use a different action (such as `static-change`), and add more magic than a plain old `window.location.reload()`. 73 | 74 | We're also including some sugar data there in `{source: 'config' }` so that any listeners know where the event originated. That could be useful in an app that has multiple instruction points calling for reloads. 75 | 76 | ### 🍰 Dessert 77 | 78 | And there you have it. We have a sample app that will reload whenever Markdown files in the static assets directory changes. Cheers, and good eating. 79 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "document": true 4 | }, 5 | "rules": { 6 | "import/no-extraneous-dependencies": "off", 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/errors.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | 3 | const { PluginExistsError, WebpackPluginServeError } = require('../lib/errors'); 4 | 5 | test('errors', (t) => { 6 | t.snapshot(new PluginExistsError()); 7 | t.snapshot(new WebpackPluginServeError()); 8 | }); 9 | -------------------------------------------------------------------------------- /test/fixtures/https/app.js: -------------------------------------------------------------------------------- 1 | console.log('ok'); 2 | -------------------------------------------------------------------------------- /test/fixtures/https/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fixture: https 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/https/localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE7DCCAtSgAwIBAgIJANATvy4nMEs0MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAgFw0yMTAxMTcxNzM2MjVaGA8yMDUxMDExMDE3MzYyNVow 4 | FDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC 5 | CgKCAgEA6PovbbkZx+8LXTS9NDitCXII40WJ8qyyC38EMlnRsY8rZ3ptMacawZQa 6 | Q10Jvo6xswzM54bRvvTxHXrltNSW7G/9Qp+KkM99Ee1ZgO1NqVH5Z8RftXjUGvTW 7 | M6ZRNLoRtZNyoK4G2r/xvbMZhv/nW80/vvfXzXfqUWoqb/4hVeVC7cYPqWVXkyuy 8 | rB+BJmv7LMYSX1p8az9E+lP4tSQa8VzrlYWQhVr40jhZV/A/Ssxv6ENBvc9sRHbz 9 | GEqEHD0+rP7A0goLMSjHk0HZzaoC+mmgOzVUwbUvhuvzPBOrxkf0fy+p7zFH1ASN 10 | zC9pWggjJJJnMBd6ItZilZIvY9gprnu+buZGuHRhs4Byv90/34/r9o/eTdrYulzJ 11 | sXkmLp28YqwYXX8CxDtreQIzShjIPzQmGMnWE27xisL9rXWW3wI9lRyO6w+19aAL 12 | U+mpP30HGFETpefNHq/55dvZUGwe95oALVJaytoUheKChmw4uQlf8tY2U3h9TkD9 13 | 3qtDVEKvUWnLY1SKEFN1iH42f+gU/IDnQPvCtD69XbYb/ivY/EsppiLXs0/kzQ9w 14 | 8y+O0P70LPiaYg6hkCF+bgu3mPikrQEI9Ka9KRRICxh0yTRxOL0GHBNMxfUC4G+8 15 | k/MvENHAwQqK6oFF7TtIBYuW6gD10HUSGxDuPrQ5vjM78GYGAv0CAwEAAaM/MD0w 16 | CQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwIwYDVR0RBBwwGoINd3d3LmxvY2FsaG9z 17 | dIIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4ICAQDKvdg7NEWHBp/CKMr/Ombi 18 | AAJgDgjgMzhzz2aQqdskJfoU/AeKBYoQnh4tOHM3FKj+U4+pxHjbQMBshsDbwya9 19 | 177HYG8A9cBbrlZEseR/bWzOwy1V+td5jUT5aYv+ajMloS27U2KEieaSiLhFYKQY 20 | HY+sR3wkLAiS5rapq61xvLMNC6xmqeMcnn9PZYV1B8rBBPR5PPUunGSzEE2zRpv6 21 | jO28d18B+zhqKiJr7TJunUKkPwYBVHm/OYhikyt3SB/a0KwYJmzfqJh0hP6vCOz5 22 | 5CAkmPon6N33AEs73V5X7zpqR5d0jRGrD30Twx23KxJTdUeo+BQ5Fi5roxvnW1am 23 | 5BP++KEyEvoJtzft5GY5wb5fBNv9Ek5n8O5HBACfDnZN3N+1RLIbAXrjTkBpL6vx 24 | bFf5ZM5LmQ3JvcMPvoZlxHonmgaB9GDNzCllxbMCaR9oW/5h9XYwk8MXAk/SLrn6 25 | MXgokarf2Ij2FkPx40ilcL9D5GoHi2A9eIYdRPxschG2HsZeIv8R2BQsnYa+WwGN 26 | vxHxCHroqoDVAylo1W1OxdOxMUYaH+nHp5JE1H52jIq7pafBE62EAZut24FyGU6M 27 | zopgCwxi+ARV/HO+nmc8smP4+X1FBCW8qUsmZWqoLztQmO3ZfBO8UPv+aY5dA2Hz 28 | nCwe6GqeCrfvOQNVdDrV3Q== 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /test/fixtures/https/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDo+i9tuRnH7wtd 3 | NL00OK0JcgjjRYnyrLILfwQyWdGxjytnem0xpxrBlBpDXQm+jrGzDMznhtG+9PEd 4 | euW01Jbsb/1Cn4qQz30R7VmA7U2pUflnxF+1eNQa9NYzplE0uhG1k3Kgrgbav/G9 5 | sxmG/+dbzT++99fNd+pRaipv/iFV5ULtxg+pZVeTK7KsH4Ema/ssxhJfWnxrP0T6 6 | U/i1JBrxXOuVhZCFWvjSOFlX8D9KzG/oQ0G9z2xEdvMYSoQcPT6s/sDSCgsxKMeT 7 | QdnNqgL6aaA7NVTBtS+G6/M8E6vGR/R/L6nvMUfUBI3ML2laCCMkkmcwF3oi1mKV 8 | ki9j2Cmue75u5ka4dGGzgHK/3T/fj+v2j95N2ti6XMmxeSYunbxirBhdfwLEO2t5 9 | AjNKGMg/NCYYydYTbvGKwv2tdZbfAj2VHI7rD7X1oAtT6ak/fQcYUROl580er/nl 10 | 29lQbB73mgAtUlrK2hSF4oKGbDi5CV/y1jZTeH1OQP3eq0NUQq9RactjVIoQU3WI 11 | fjZ/6BT8gOdA+8K0Pr1dthv+K9j8SymmItezT+TND3DzL47Q/vQs+JpiDqGQIX5u 12 | C7eY+KStAQj0pr0pFEgLGHTJNHE4vQYcE0zF9QLgb7yT8y8Q0cDBCorqgUXtO0gF 13 | i5bqAPXQdRIbEO4+tDm+MzvwZgYC/QIDAQABAoICAQCUYaOLcnSlDe5i8o/+jopG 14 | lcZrWYK+eJb9E2yM1P/k4pZ2VVbuZMt5tuXzOXJXV3J94w0t+IPl5wMaD4f1X+Cv 15 | vbPDlvEdwciaflye7ISl7nu2Ry/d2JLeiElUo/zcZBWbW8mJ5MygzdcdRtBfVuT3 16 | ZwXOjvN2/YTTTZaxtV1t45J49chLzyuzpAUr8vTCj8ttfMl+yZash6Na+9hPJiuy 17 | xceVd3fQN1x6J0Ff7Lei4An2F7/rMAx4nnm9Ytg5VBhy/d246ISwTpStiSc7Aajb 18 | 7CINE65joQ4gFG7aOH0S0Ak8aBMHMI/azPuyrA+hM8WOKWBnFMzJG1m2gWCFastA 19 | CnOVLYp14MiNjrpgICQv4lF7S30/Zwfv7PytrZ6rUsHOnejjK8QtIBszQt/hHbpR 20 | 5QW569o48SKfiLpQQxBxXO8/nmveT5a8d523t4KUcM6uaEHD6HJBOhlkiIWI2U4k 21 | 95FSExBGmJZHEo5F+FPeYXQkXhBRWOpwAiYn6KfQQAKP/KWUwqysHa54r6GsH5OH 22 | HHgrB/crOWJyahj0J3XSLLvXvU8PaHPe0t7u/fS/nghSc/YXN1EJVVUTK1Sumr4h 23 | iB6l4qsPqzUCWKGBkSxqfAlR1lwTAX1Pv3msOiP0rZl0/r+PILpHdBEQCBi/V3Um 24 | IRV3N/36tFghhbqRgUW5gQKCAQEA/LgRwO4OX+3d7/THVYPtUhX4A/UzEXyCTIjB 25 | p1/KXcqn3zajye7qU3h2uiWJYhtnP3tqJRyL8L7XRS2GlwQIl1IG3hJdmSohJHif 26 | lRjPRAJWkVfa1Vom+nknuemqyR+C6iuhv4FHUp+Pv62R4CwcabBiXeEb5X/UUXr6 27 | A4wDF9i+yVoTdGVPzhn84gQLSjO92Cq9vlddBwnk5Qg8hz+dAQLd0700C8xdT7UW 28 | e5uu7b9HylJi7uGL6E4H83p7Pbi59JntuiC+gXmqkxupNU3tIk0hhkVVBEzmUzUM 29 | rvtuOprkkEHLPNTm22CoOc3xqca4wJMo1upqL4Vvmdod6drkNQKCAQEA7ACAtipy 30 | tsVwybpBR3TNCM/meU+1K37doT0qu7F73buA9CMl2BC65ROwxUyOjwJReIhw3DFL 31 | K7Jj73yi00i+7mmNhbulgjFgjPpcFAdfsP1NvWCajp74H7bFudru00KWlKiMO9E8 32 | bX7sg5sHsPW7CU8EOhbCmBgjSDVuMYTGHctDXbQnw0i/xT/ucKMJxMQ7APX4ug6c 33 | kYeunSUi6b65xouJLdHgJit6ej8Az04wc2+40t5dAKQR0xfc55dPu0kALzITQZY+ 34 | hewZ7g4dWu1yqGnAMHR7L3a/kkDzBAXQp01LSoqWt0VhQRymMwpnDlGG5g3+x0rJ 35 | fSfFhdp6tSNsqQKCAQBViV/I80o+Qd6Kp0kIsrySugjWHhoI4yGfEfKp2gw+rqcf 36 | L/lEEe+SDQXyf13+rNPHctg8c2sSMyXdrkkNuDnILeVRKNoxS0codwNcjacpl7kB 37 | PME+0WK8CGHf1S0PIfFHnTkD/pfLaWJYOEFVpNiBFUhWCilNBnUwlvUkWjF50szQ 38 | jgJjtR7L3ZPwU/oWRWkOT3klgaBlD34/3wfNp0RW4Ud/sVx6WdODdQGFGO6eXc3I 39 | txXxULnwCOk8CcrI20tldOw3wXl5jVqkrAoblqKap4yfS7w41mZm51/Boixu/Zo7 40 | EWnezxjMS+zkk8Yl9rL1y1lZVMJYD0v0wRS64HG5AoIBACZhRo1ZheJXZapI0gju 41 | 49FFDjQ4VDxIm353VhXEHWLH9Ya+mI702ZAbjanoc16qf9lcRw5oLdNjZYEzAb/e 42 | mr5nCA7xn072/L6wkHzpXhSIfTYJGdmL3Mo7jRhWlHRi5d4zMusfcfZUp94XPj73 43 | F33CDevse6UEd+hsWAlRkG6T+dapT7YghJ9tcDd9LD+FshrL5bFMYwX/zNPdkDn3 44 | PHsfgiWjdhJ/C6IJ0PXlv1dmmeYhJ3rNM+DihphbMSpCCveh6yPFbVGWPflK7lc1 45 | OmpNGGZI6yei5jDAw0SqNW6f0VjEBRK1A8X1BMpULMJVW9zZ9c8vSGlEKIlGKKKA 46 | qlECggEBAPXEJUEokNAAa1oBnE6/1VQIJjP+L+zT5BDAJtAQ9fQ0wy/k9uuQ+b2y 47 | f7+XVk0tY0JDuNc2k89haH0AlbRtC9kUpvm6j08mdpidVx51t++wzMC1vK4taHYZ 48 | 27F6yCiEJFB0rhxG+vLjZ3Fr2kedP+IxDPh6z8PmHSDJJ3zlnFrif5yqpkrDuevI 49 | Nohw40bgi6rRv0tqcbziM2T/2y3nMqi+3UkyWLDeQThQaSlTGoAptGBR+jZotfoC 50 | lpB1IdPcdoQLiFmEkPhUJnKaAjHCi2khZwyDY6kUfaEt4GfcKZyurAC8lPUZyqMn 51 | kyqwVw4XIqGsQkNCIfToMOEJ7Gpqef4= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /test/fixtures/https/localhost.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/webpack-plugin-serve/b19fce1db5f881af603879b8bb9c555c16ae394f/test/fixtures/https/localhost.pfx -------------------------------------------------------------------------------- /test/fixtures/https/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: ['./app.js', 'webpack-plugin-serve/client'], 6 | mode: 'development', 7 | output: { 8 | filename: './output.js', 9 | path: resolve(__dirname, './output'), 10 | publicPath: 'output/' 11 | }, 12 | resolve: { 13 | alias: { 14 | 'webpack-plugin-serve/client': resolve(__dirname, '../../../client') 15 | } 16 | }, 17 | watch: true 18 | }; 19 | -------------------------------------------------------------------------------- /test/fixtures/multi/app.js: -------------------------------------------------------------------------------- 1 | require('./component'); 2 | 3 | if (module.hot) { 4 | module.hot.accept((err) => { 5 | if (err) { 6 | console.error('HMR', err); 7 | } 8 | }); 9 | } 10 | 11 | // uncomment to produce a build error 12 | // if (!window) { 13 | // require('tests'); 14 | // } 15 | 16 | // uncomment to produce a build warning 17 | // console.log(require); 18 | -------------------------------------------------------------------------------- /test/fixtures/multi/component.js: -------------------------------------------------------------------------------- 1 | const main = document.querySelector('main'); 2 | main.innerHTML = 'main'; 3 | -------------------------------------------------------------------------------- /test/fixtures/multi/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fixture: multicompiler 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/fixtures/multi/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | const { getPort } = require('../../helpers/port'); 4 | 5 | const { WebpackPluginServe: Serve } = require('../../../lib/'); 6 | 7 | const serve = new Serve({ 8 | host: 'localhost', 9 | port: getPort() 10 | }); 11 | 12 | module.exports = [ 13 | { 14 | context: __dirname, 15 | entry: ['./app.js', 'webpack-plugin-serve/client'], 16 | mode: 'development', 17 | output: { 18 | filename: './dist-app.js', 19 | path: resolve(__dirname, './output'), 20 | publicPath: 'output/' 21 | }, 22 | plugins: [serve], 23 | resolve: { 24 | alias: { 25 | 'webpack-plugin-serve/client': resolve(__dirname, '../../../client') 26 | } 27 | }, 28 | watch: true 29 | }, 30 | { 31 | context: __dirname, 32 | entry: ['./worker.js', 'webpack-plugin-serve/client'], 33 | mode: 'development', 34 | output: { 35 | filename: './dist-worker.js', 36 | path: resolve(__dirname, './output'), 37 | publicPath: 'output/' 38 | }, 39 | plugins: [serve.attach()], 40 | resolve: { 41 | alias: { 42 | 'webpack-plugin-serve/client': resolve(__dirname, '../../../client') 43 | } 44 | } 45 | } 46 | ]; 47 | -------------------------------------------------------------------------------- /test/fixtures/multi/work.js: -------------------------------------------------------------------------------- 1 | const main = document.querySelector('#worker'); 2 | main.innerHTML = 'worker'; 3 | 4 | // console.log(require); 5 | -------------------------------------------------------------------------------- /test/fixtures/multi/worker.js: -------------------------------------------------------------------------------- 1 | require('./work'); 2 | 3 | if (module.hot) { 4 | module.hot.accept((err) => { 5 | if (err) { 6 | console.error('HMR', err); 7 | } 8 | }); 9 | } 10 | 11 | // uncomment to produce a build error 12 | // if (!window) { 13 | // require('tests'); 14 | // } 15 | 16 | // uncomment to produce a build warning 17 | // console.log(require); 18 | -------------------------------------------------------------------------------- /test/fixtures/proxy/app.js: -------------------------------------------------------------------------------- 1 | require('./component'); 2 | 3 | if (module.hot) { 4 | module.hot.accept((err) => { 5 | if (err) { 6 | console.error('HMR', err); 7 | } 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/proxy/component.js: -------------------------------------------------------------------------------- 1 | const main = document.querySelector('main'); 2 | main.innerHTML = 'main...'; 3 | -------------------------------------------------------------------------------- /test/fixtures/proxy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fixture: simple 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/proxy/proxy-rewrite.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | const { WebpackPluginServe: Serve } = require('../../../lib/'); 4 | 5 | const logLevel = 'silent'; 6 | 7 | module.exports = { 8 | context: __dirname, 9 | entry: ['./app.js', 'webpack-plugin-serve/client'], 10 | mode: 'development', 11 | output: { 12 | filename: './output.js', 13 | path: resolve(__dirname, './output'), 14 | publicPath: 'output/' 15 | }, 16 | plugins: [ 17 | new Serve({ 18 | log: { level: logLevel }, 19 | port: 55557, 20 | middleware: (app, builtins) => { 21 | app.use( 22 | builtins.proxy('/api', { 23 | logLevel, 24 | target: 'http://localhost:8889', 25 | pathRewrite: { '^/api': '' } 26 | }) 27 | ); 28 | } 29 | }) 30 | ], 31 | resolve: { 32 | alias: { 33 | 'webpack-plugin-serve/client': resolve(__dirname, '../../../lib/client') 34 | } 35 | }, 36 | watch: true 37 | }; 38 | -------------------------------------------------------------------------------- /test/fixtures/proxy/proxy-server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | const Koa = require('koa'); 3 | const router = require('koa-route'); 4 | 5 | const app = new Koa(); 6 | 7 | const defaultRoutes = [ 8 | { 9 | url: '/api', 10 | handler: async (ctx) => { 11 | ctx.body = '/api endpoint'; 12 | } 13 | }, 14 | { 15 | url: '/api/test', 16 | handler: async (ctx) => { 17 | ctx.body = '/api/test endpoint'; 18 | } 19 | } 20 | ]; 21 | 22 | const proxyServer = (routes = defaultRoutes) => { 23 | routes.forEach((route) => { 24 | app.use(router.get(route.url, route.handler)); 25 | }); 26 | return app; 27 | }; 28 | 29 | module.exports = { 30 | proxyServer 31 | }; 32 | -------------------------------------------------------------------------------- /test/fixtures/proxy/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | const { WebpackPluginServe: Serve } = require('../../../lib/'); 4 | 5 | const logLevel = 'silent'; 6 | 7 | module.exports = { 8 | context: __dirname, 9 | entry: ['./app.js', 'webpack-plugin-serve/client'], 10 | mode: 'development', 11 | output: { 12 | filename: './output.js', 13 | path: resolve(__dirname, './output'), 14 | publicPath: 'output/' 15 | }, 16 | plugins: [ 17 | new Serve({ 18 | port: 55556, 19 | log: { level: logLevel }, 20 | middleware: (app, builtins) => { 21 | app.use( 22 | builtins.proxy('/api', { 23 | logLevel, 24 | target: 'http://localhost:8888' 25 | }) 26 | ); 27 | app.use( 28 | builtins.proxy('/wps', { 29 | logLevel, 30 | target: 'http://localhost:8888' 31 | }) 32 | ); 33 | app.use( 34 | builtins.proxy('/wp', { 35 | logLevel, 36 | target: 'http://localhost:8888' 37 | }) 38 | ); 39 | } 40 | }) 41 | ], 42 | resolve: { 43 | alias: { 44 | 'webpack-plugin-serve/client': resolve(__dirname, '../../../lib/client') 45 | } 46 | }, 47 | watch: true 48 | }; 49 | -------------------------------------------------------------------------------- /test/fixtures/ramdisk-empty-pkg/app.js: -------------------------------------------------------------------------------- 1 | const main = document.querySelector('main'); 2 | main.innerHTML = 'main'; 3 | -------------------------------------------------------------------------------- /test/fixtures/ramdisk-empty-pkg/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/fixtures/ramdisk-empty-pkg/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | const { getPort } = require('../../helpers/port'); 4 | 5 | const { WebpackPluginServe: Serve } = require('../../../lib/'); 6 | 7 | module.exports = { 8 | context: __dirname, 9 | entry: ['./app.js', 'webpack-plugin-serve/client'], 10 | mode: 'development', 11 | output: { 12 | filename: './output.js', 13 | path: resolve(__dirname, './output'), 14 | publicPath: 'output/' 15 | }, 16 | plugins: [ 17 | new Serve({ 18 | host: 'localhost', 19 | port: getPort(), 20 | ramdisk: true 21 | }) 22 | ], 23 | resolve: { 24 | alias: { 25 | 'webpack-plugin-serve/client': resolve(__dirname, '../../../client') 26 | } 27 | }, 28 | watch: true 29 | }; 30 | -------------------------------------------------------------------------------- /test/fixtures/ramdisk/app.js: -------------------------------------------------------------------------------- 1 | require('./component'); 2 | 3 | if (module.hot) { 4 | module.hot.accept((err) => { 5 | if (err) { 6 | console.error('HMR', err); 7 | } 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/ramdisk/component.js: -------------------------------------------------------------------------------- 1 | const main = document.querySelector('main'); 2 | main.innerHTML = 'main'; 3 | -------------------------------------------------------------------------------- /test/fixtures/ramdisk/config-context-error.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | const { getPort } = require('../../helpers/port'); 4 | 5 | const { WebpackPluginServe: Serve } = require('../../../lib/'); 6 | 7 | module.exports = { 8 | context: __dirname, 9 | entry: ['./app.js', 'webpack-plugin-serve/client'], 10 | mode: 'development', 11 | output: { 12 | filename: './output.js', 13 | path: __dirname, 14 | publicPath: 'output/' 15 | }, 16 | plugins: [ 17 | new Serve({ 18 | host: 'localhost', 19 | port: getPort(), 20 | ramdisk: true 21 | }) 22 | ], 23 | resolve: { 24 | alias: { 25 | 'webpack-plugin-serve/client': resolve(__dirname, '../../../client') 26 | } 27 | }, 28 | watch: true 29 | }; 30 | -------------------------------------------------------------------------------- /test/fixtures/ramdisk/config-cwd-error.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | const { getPort } = require('../../helpers/port'); 4 | 5 | const { WebpackPluginServe: Serve } = require('../../../lib/'); 6 | 7 | module.exports = { 8 | context: __dirname, 9 | entry: ['./app.js', 'webpack-plugin-serve/client'], 10 | mode: 'development', 11 | output: { 12 | filename: './output.js', 13 | path: resolve(__dirname, 'cwd-error'), 14 | publicPath: 'output/' 15 | }, 16 | plugins: [ 17 | new Serve({ 18 | host: 'localhost', 19 | port: getPort(), 20 | ramdisk: true 21 | }) 22 | ], 23 | resolve: { 24 | alias: { 25 | 'webpack-plugin-serve/client': resolve(__dirname, '../../../client') 26 | } 27 | }, 28 | watch: true 29 | }; 30 | -------------------------------------------------------------------------------- /test/fixtures/ramdisk/custom-options.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | const { getPort } = require('../../helpers/port'); 4 | 5 | const { WebpackPluginServe: Serve } = require('../../../lib/'); 6 | 7 | module.exports = { 8 | context: __dirname, 9 | entry: ['./app.js', 'webpack-plugin-serve/client'], 10 | mode: 'development', 11 | output: { 12 | filename: './output.js', 13 | path: resolve(__dirname, './output'), 14 | publicPath: 'output/' 15 | }, 16 | plugins: [ 17 | new Serve({ 18 | host: 'localhost', 19 | port: getPort(), 20 | ramdisk: { 21 | bytes: 1024 * 1024 22 | } 23 | }) 24 | ], 25 | resolve: { 26 | alias: { 27 | 'webpack-plugin-serve/client': resolve(__dirname, '../../../client') 28 | } 29 | }, 30 | watch: true 31 | }; 32 | -------------------------------------------------------------------------------- /test/fixtures/ramdisk/cwd-error/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/webpack-plugin-serve/b19fce1db5f881af603879b8bb9c555c16ae394f/test/fixtures/ramdisk/cwd-error/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/ramdisk/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fixture: single 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/ramdisk/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | const { getPort } = require('../../helpers/port'); 4 | 5 | const { WebpackPluginServe: Serve } = require('../../../lib/'); 6 | 7 | module.exports = { 8 | context: __dirname, 9 | entry: ['./app.js', 'webpack-plugin-serve/client'], 10 | mode: 'development', 11 | output: { 12 | filename: './output.js', 13 | path: resolve(__dirname, './output'), 14 | publicPath: 'output/' 15 | }, 16 | plugins: [ 17 | new Serve({ 18 | host: 'localhost', 19 | port: getPort(), 20 | ramdisk: true 21 | }) 22 | ], 23 | resolve: { 24 | alias: { 25 | 'webpack-plugin-serve/client': resolve(__dirname, '../../../client') 26 | } 27 | }, 28 | watch: true 29 | }; 30 | -------------------------------------------------------------------------------- /test/fixtures/simple/app.js: -------------------------------------------------------------------------------- 1 | require('./component'); 2 | 3 | if (module.hot) { 4 | module.hot.accept((err) => { 5 | if (err) { 6 | console.error('HMR', err); 7 | } 8 | }); 9 | } 10 | 11 | // uncomment to produce a build error 12 | // if (!window) { 13 | // require('tests'); 14 | // } 15 | 16 | // uncomment to produce a build warning 17 | // console.log(require); 18 | 19 | // console.log(require); 20 | -------------------------------------------------------------------------------- /test/fixtures/simple/component.js: -------------------------------------------------------------------------------- 1 | const main = document.querySelector('main'); 2 | main.innerHTML = 'main'; 3 | -------------------------------------------------------------------------------- /test/fixtures/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fixture: single 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/simple/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | const { getPort } = require('../../helpers/port'); 4 | 5 | const { WebpackPluginServe: Serve } = require('../../../lib/'); 6 | 7 | module.exports = { 8 | context: __dirname, 9 | entry: ['./app.js', 'webpack-plugin-serve/client'], 10 | mode: 'development', 11 | output: { 12 | filename: './output.js', 13 | path: resolve(__dirname, './output'), 14 | publicPath: 'output/' 15 | }, 16 | plugins: [ 17 | new Serve({ 18 | headers: { 19 | 'X-Superhero': 'batman' 20 | }, 21 | host: 'localhost', 22 | port: getPort() 23 | }) 24 | ], 25 | resolve: { 26 | alias: { 27 | 'webpack-plugin-serve/client': resolve(__dirname, '../../../client') 28 | } 29 | }, 30 | watch: true 31 | }; 32 | -------------------------------------------------------------------------------- /test/fixtures/wait-for-build/app.js: -------------------------------------------------------------------------------- 1 | module.exports = 'hello'; 2 | -------------------------------------------------------------------------------- /test/fixtures/wait-for-build/make-config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | const { resolve } = require('path'); 3 | 4 | const { WebpackPluginServe: Serve } = require('../../../lib/'); 5 | 6 | const make = (port) => { 7 | const outputPath = resolve(__dirname, './output/output.js'); 8 | const serve = new Serve({ 9 | host: 'localhost', 10 | port, 11 | waitForBuild: true, 12 | middleware: (app) => { 13 | app.use(async (ctx, next) => { 14 | if (ctx.url === '/test') { 15 | try { 16 | // eslint-disable-next-line import/no-dynamic-require, global-require 17 | require(outputPath); 18 | ctx.body = 'success'; 19 | } catch (e) { 20 | ctx.body = 'error'; 21 | } 22 | } 23 | await next(); 24 | }); 25 | } 26 | }); 27 | 28 | const config = { 29 | context: __dirname, 30 | entry: ['./app.js'], 31 | mode: 'development', 32 | output: { 33 | filename: './output.js', 34 | path: resolve(__dirname, './output'), 35 | publicPath: 'output/', 36 | libraryTarget: 'commonjs2' 37 | }, 38 | plugins: [serve], 39 | target: 'node', 40 | watch: true 41 | }; 42 | 43 | return { serve, config }; 44 | }; 45 | 46 | module.exports = { make }; 47 | -------------------------------------------------------------------------------- /test/helpers.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | 3 | const { getMajorVersion } = require('../lib/helpers'); 4 | 5 | test('Get major version with correct value', (t) => { 6 | const majorVersion = getMajorVersion('5.2.3'); 7 | t.true(majorVersion === '5'); 8 | }); 9 | 10 | test('Get major version with incorrect value', (t) => { 11 | const majorVersion = getMajorVersion('5'); 12 | t.true(majorVersion === false); 13 | }); 14 | -------------------------------------------------------------------------------- /test/helpers/port.js: -------------------------------------------------------------------------------- 1 | const getPort = require('get-port'); 2 | const { Random } = require('random-js'); 3 | 4 | const random = new Random(); 5 | 6 | module.exports = { 7 | getPort: () => getPort({ port: random.integer(55000, 55555) }) 8 | }; 9 | -------------------------------------------------------------------------------- /test/helpers/puppeteer.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync } = require('fs'); 2 | const { join } = require('path'); 3 | 4 | const copy = require('cpy'); 5 | const mkdir = require('make-dir'); 6 | const puppeteer = require('puppeteer'); 7 | const strip = require('strip-ansi'); 8 | 9 | const getPort = (stdout) => { 10 | return { 11 | then(r, f) { 12 | stdout.on('data', (data) => { 13 | const content = strip(data.toString()); 14 | const test = 'Server Listening on: '; 15 | if (content.includes(test)) { 16 | r(content.slice(content.lastIndexOf(':') + 1)); 17 | } 18 | }); 19 | 20 | stdout.on('error', f); 21 | } 22 | }; 23 | }; 24 | 25 | const replace = (path, content) => { 26 | return { 27 | then(r) { 28 | writeFileSync(path, content); 29 | setTimeout(r, 5000); 30 | } 31 | }; 32 | }; 33 | 34 | const setup = async (base, name) => { 35 | const fixturesPath = join(__dirname, '../fixtures'); 36 | const src = join(fixturesPath, base); 37 | const dest = await mkdir(join(fixturesPath, `temp-${name}`)); 38 | await copy(`${src}/*`, dest); 39 | 40 | return dest; 41 | }; 42 | 43 | const waitForBuild = (stderr) => { 44 | return { 45 | then(r) { 46 | stderr.on('data', (data) => { 47 | const content = strip(data.toString()); 48 | if (/webpack: Hash:/.test(content)) { 49 | r(); 50 | } 51 | }); 52 | } 53 | }; 54 | }; 55 | 56 | const browser = async (t, run) => { 57 | const instance = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] }); 58 | const page = await instance.newPage(); 59 | const util = { 60 | getPort, 61 | replace, 62 | setup, 63 | waitForBuild 64 | }; 65 | try { 66 | await run(t, page, util); 67 | } finally { 68 | await page.close(); 69 | await instance.close(); 70 | } 71 | }; 72 | 73 | module.exports = { browser }; 74 | -------------------------------------------------------------------------------- /test/https.test.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | 3 | const { readFileSync } = require('fs'); 4 | 5 | const { resolve, join } = require('path'); 6 | 7 | const http2 = require('http2'); 8 | 9 | const webpack = require('webpack'); 10 | const test = require('ava'); 11 | const fetch = require('node-fetch'); 12 | const defer = require('p-defer'); 13 | const del = require('del'); 14 | 15 | const { WebpackPluginServe } = require('../lib'); 16 | 17 | const { getPort } = require('./helpers/port'); 18 | const webpackDefaultConfig = require('./fixtures/https/webpack.config'); 19 | 20 | const httpsFixturePath = resolve(__dirname, './fixtures/https'); 21 | 22 | const startServe = async (serve) => { 23 | const deferred = defer(); 24 | const compiler = webpack({ 25 | ...webpackDefaultConfig, 26 | plugins: [serve] 27 | }); 28 | const watcher = compiler.watch({}, () => {}); 29 | serve.on('listening', deferred.resolve); 30 | await deferred.promise; 31 | return watcher; 32 | }; 33 | 34 | const checkHttpsServed = async (t, serve, port) => { 35 | const watcher = await startServe(serve); 36 | const agent = new https.Agent({ 37 | rejectUnauthorized: false 38 | }); 39 | const response = await fetch(`https://localhost:${port}`, { agent }); 40 | watcher.close(); 41 | t.true(response.ok); 42 | }; 43 | 44 | const checkHttp2Served = async (t, serve, port) => { 45 | const watcher = await startServe(serve); 46 | const deferred = defer(); 47 | const client = http2.connect(`https://localhost:${port}`, { 48 | rejectUnauthorized: false 49 | }); 50 | client.on('error', (err) => { 51 | t.fail(err); 52 | deferred.reject(); 53 | }); 54 | 55 | const req = client.request({ ':path': '/' }); 56 | req.on('response', () => { 57 | t.pass(); 58 | watcher.close(); 59 | client.close(); 60 | deferred.resolve(); 61 | }); 62 | req.on('end', () => { 63 | client.close(); 64 | deferred.resolve(); 65 | }); 66 | 67 | await deferred.promise; 68 | }; 69 | 70 | test.after.always('remove build output', async () => { 71 | await del('./test/fixtures/https/output'); 72 | }); 73 | 74 | test('should start https with pem', async (t) => { 75 | const port = await getPort(); 76 | const key = readFileSync(join(httpsFixturePath, 'localhost.key')); 77 | const cert = readFileSync(join(httpsFixturePath, 'localhost.crt')); 78 | const serve = new WebpackPluginServe({ 79 | host: 'localhost', 80 | allowMany: true, 81 | port, 82 | waitForBuild: true, 83 | https: { key, cert } 84 | }); 85 | 86 | await checkHttpsServed(t, serve, port); 87 | }); 88 | 89 | test('should start http2 with pem', async (t) => { 90 | const port = await getPort(); 91 | const key = readFileSync(join(httpsFixturePath, 'localhost.key')); 92 | const cert = readFileSync(join(httpsFixturePath, 'localhost.crt')); 93 | const serve = new WebpackPluginServe({ 94 | host: 'localhost', 95 | allowMany: true, 96 | port, 97 | waitForBuild: true, 98 | http2: { key, cert } 99 | }); 100 | 101 | await checkHttp2Served(t, serve, port); 102 | }); 103 | 104 | test('should start https with pfx', async (t) => { 105 | const port = await getPort(); 106 | const pfx = readFileSync(join(httpsFixturePath, 'localhost.pfx')); 107 | const serve = new WebpackPluginServe({ 108 | host: 'localhost', 109 | allowMany: true, 110 | port, 111 | waitForBuild: true, 112 | https: { pfx, passphrase: 'password' } 113 | }); 114 | 115 | await checkHttpsServed(t, serve, port); 116 | }); 117 | 118 | test('should start http2 with pfx', async (t) => { 119 | const port = await getPort(); 120 | const pfx = readFileSync(join(httpsFixturePath, 'localhost.pfx')); 121 | const serve = new WebpackPluginServe({ 122 | host: 'localhost', 123 | allowMany: true, 124 | port, 125 | waitForBuild: true, 126 | http2: { pfx, passphrase: 'password' } 127 | }); 128 | 129 | await checkHttp2Served(t, serve, port); 130 | }); 131 | -------------------------------------------------------------------------------- /test/integration/force-refresh.test.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | const test = require('ava'); 4 | const del = require('del'); 5 | const execa = require('execa'); 6 | 7 | const { browser } = require('../helpers/puppeteer'); 8 | 9 | test('force refresh', browser, async (t, page, util) => { 10 | const { getPort, replace, setup, waitForBuild } = util; 11 | const fixturePath = await setup('simple', 'single-hmr'); 12 | const proc = execa('wp', [], { cwd: fixturePath }); 13 | const { stdout, stderr } = proc; 14 | const port = await getPort(stdout); 15 | const url = `http://localhost:${port}`; 16 | 17 | await waitForBuild(stderr); 18 | await page.goto(url, { 19 | waitUntil: 'networkidle0' 20 | }); 21 | 22 | const componentPath = join(fixturePath, 'component.js'); 23 | const content = `const main = document.querySelector('main'); main.innerHTML = 'test';`; 24 | 25 | await replace(componentPath, content, true); 26 | 27 | const value = await page.evaluate(() => document.querySelector('main').innerHTML); 28 | 29 | proc.kill('SIGTERM'); 30 | 31 | t.is(value, 'test'); 32 | 33 | await del(fixturePath); 34 | }); 35 | -------------------------------------------------------------------------------- /test/integration/multi-hmr.test.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | const test = require('ava'); 4 | const del = require('del'); 5 | const execa = require('execa'); 6 | 7 | const { browser } = require('../helpers/puppeteer'); 8 | 9 | test('multi compiler', browser, async (t, page, util) => { 10 | const { getPort, replace, setup, waitForBuild } = util; 11 | const fixturePath = await setup('multi', 'multi-hmr'); 12 | const proc = execa('wp', [], { cwd: fixturePath }); 13 | const { stdout, stderr } = proc; 14 | const port = await getPort(stdout); 15 | const url = `http://localhost:${port}`; 16 | 17 | await waitForBuild(stderr); 18 | await page.goto(url, { 19 | waitUntil: 'networkidle0' 20 | }); 21 | 22 | const componentPath = join(fixturePath, 'component.js'); 23 | const workerPath = join(fixturePath, 'work.js'); 24 | const componentContent = `const main = document.querySelector('main'); main.innerHTML = 'test';`; 25 | const workerContent = `const worker = document.querySelector('#worker'); worker.innerHTML = 'test';`; 26 | 27 | await replace(componentPath, componentContent); 28 | await replace(workerPath, workerContent); 29 | 30 | const componentValue = await page.evaluate(() => document.querySelector('main').innerHTML); 31 | const workValue = await page.evaluate(() => document.querySelector('#worker').innerHTML); 32 | 33 | proc.kill('SIGTERM'); 34 | 35 | t.is(componentValue, 'test'); 36 | t.is(workValue, 'test'); 37 | 38 | await del(fixturePath); 39 | }); 40 | -------------------------------------------------------------------------------- /test/integration/single-hmr.test.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | const test = require('ava'); 4 | const del = require('del'); 5 | const execa = require('execa'); 6 | 7 | const { browser } = require('../helpers/puppeteer'); 8 | 9 | test('single compiler', browser, async (t, page, util) => { 10 | const { getPort, replace, setup, waitForBuild } = util; 11 | const fixturePath = await setup('simple', 'single-hmr'); 12 | const proc = execa('wp', [], { cwd: fixturePath }); 13 | const { stdout, stderr } = proc; 14 | const port = await getPort(stdout); 15 | const url = `http://localhost:${port}`; 16 | 17 | await waitForBuild(stderr); 18 | await page.goto(url, { 19 | waitUntil: 'networkidle0' 20 | }); 21 | 22 | const componentPath = join(fixturePath, 'component.js'); 23 | const content = `const main = document.querySelector('main'); main.innerHTML = 'test';`; 24 | 25 | await replace(componentPath, content); 26 | 27 | const value = await page.evaluate(() => document.querySelector('main').innerHTML); 28 | 29 | proc.kill('SIGTERM'); 30 | 31 | t.is(value, 'test'); 32 | 33 | await del(fixturePath); 34 | }); 35 | -------------------------------------------------------------------------------- /test/middleware.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | 3 | test('placeholder', (t) => t.pass()); 4 | -------------------------------------------------------------------------------- /test/plugin.test.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | const test = require('ava'); 4 | 5 | const { WebpackPluginServe } = require('../lib'); 6 | 7 | const reCleanDir = /^.+(serve|project)\//g; 8 | const fixturePath = join(__dirname, 'fixtures').replace(reCleanDir, ''); 9 | 10 | test('defaults', (t) => { 11 | const plugin = new WebpackPluginServe(); 12 | t.snapshot(plugin.options); 13 | }); 14 | 15 | test('options manipulation', (t) => { 16 | const plugin = new WebpackPluginServe({ 17 | allowMany: true, 18 | compress: true, 19 | historyFallback: true, 20 | publicPath: 'dist' 21 | }); 22 | t.snapshot(plugin.options); 23 | }); 24 | 25 | test('allow https null', (t) => { 26 | const plugin = new WebpackPluginServe({ 27 | allowMany: true, 28 | https: null 29 | }); 30 | t.snapshot(plugin.options); 31 | }); 32 | 33 | test('static → string', (t) => { 34 | const { options } = new WebpackPluginServe({ 35 | allowMany: true, 36 | static: fixturePath 37 | }); 38 | t.snapshot(options.static); 39 | }); 40 | 41 | test('static → array(string)', (t) => { 42 | const { options } = new WebpackPluginServe({ 43 | allowMany: true, 44 | static: [fixturePath] 45 | }); 46 | t.snapshot(options.static); 47 | }); 48 | 49 | test('static → glob', (t) => { 50 | const { options } = new WebpackPluginServe({ 51 | allowMany: true, 52 | static: { 53 | glob: [join(__dirname, 'fixtures')], 54 | options: { onlyDirectories: true } 55 | } 56 | }); 57 | const res = options.static 58 | .map((p) => p.replace(reCleanDir, '')) 59 | .filter((p) => !/temp|output/.test(p)) 60 | .sort(); 61 | t.snapshot(res); 62 | }); 63 | -------------------------------------------------------------------------------- /test/proxy-rewrite.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | const webpack = require('webpack'); 3 | const test = require('ava'); 4 | const fetch = require('node-fetch'); 5 | const defer = require('p-defer'); 6 | 7 | const { proxyServer } = require('./fixtures/proxy/proxy-server'); 8 | const webpackConfig = require('./fixtures/proxy/proxy-rewrite.config'); 9 | 10 | const deferred = defer(); 11 | const compiler = webpack(webpackConfig); 12 | let server; 13 | let watcher; 14 | 15 | test.before(async () => { 16 | server = proxyServer([ 17 | { 18 | url: '/test', 19 | handler: async (ctx) => { 20 | ctx.body = '/test endpoint rewrite'; 21 | } 22 | } 23 | ]).listen(8889); 24 | watcher = compiler.watch({}, deferred.resolve); 25 | await deferred.promise; 26 | }); 27 | 28 | test.after.always(() => { 29 | server.close(); 30 | watcher.close(); 31 | }); 32 | 33 | test('should rewrite /api', async (t) => { 34 | const response = await fetch('http://localhost:55557/api/test'); 35 | const result = await response.text(); 36 | t.snapshot(result); 37 | }); 38 | -------------------------------------------------------------------------------- /test/proxy.test.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const test = require('ava'); 3 | const fetch = require('node-fetch'); 4 | const defer = require('p-defer'); 5 | 6 | const { proxyServer } = require('./fixtures/proxy/proxy-server'); 7 | const webpackConfig = require('./fixtures/proxy/webpack.config'); 8 | 9 | const deferred = defer(); 10 | const compiler = webpack(webpackConfig); 11 | let watcher; 12 | let server; 13 | 14 | test.before('Starting server', async () => { 15 | server = proxyServer().listen(8888); 16 | watcher = compiler.watch({}, deferred.resolve); 17 | await deferred.promise; 18 | }); 19 | 20 | test.after.always('Closing server', () => { 21 | server.close(); 22 | watcher.close(); 23 | }); 24 | 25 | test('should reach /api proxy endpoint', async (t) => { 26 | const response = await fetch('http://localhost:55556/api'); 27 | const result = await response.text(); 28 | t.snapshot(result); 29 | }); 30 | 31 | test('should reach /api/test proxy endpoint', async (t) => { 32 | const response = await fetch('http://localhost:55556/api/test'); 33 | const result = await response.text(); 34 | t.snapshot(result); 35 | }); 36 | -------------------------------------------------------------------------------- /test/ramdisk.test.js: -------------------------------------------------------------------------------- 1 | const { existsSync } = require('fs'); 2 | const { join, resolve } = require('path'); 3 | 4 | const test = require('ava'); 5 | const execa = require('execa'); 6 | const strip = require('strip-ansi'); 7 | 8 | const fixturePath = join(__dirname, 'fixtures/ramdisk'); 9 | 10 | const waitFor = (text, stream) => { 11 | return { 12 | then(r, f) { 13 | stream.on('data', (data) => { 14 | const content = strip(data.toString()); 15 | if (content.includes(text)) { 16 | r(content.slice(content.lastIndexOf(text) + text.length)); 17 | } 18 | }); 19 | 20 | stream.on('error', f); 21 | } 22 | }; 23 | }; 24 | 25 | test.serial('ramdisk', async (t) => { 26 | const proc = execa('wp', [], { cwd: fixturePath }); 27 | const { stderr, stdout } = proc; 28 | const pathTest = 'Build being written to '; 29 | const doneTest = '[emitted]'; 30 | 31 | const path = await waitFor(pathTest, stdout); 32 | 33 | t.regex(path, /(volumes|mnt)\/wps\/webpack-plugin-serve\/output/i); 34 | 35 | await waitFor(doneTest, stderr); 36 | 37 | const exists = existsSync(join(fixturePath, 'output/output.js')); 38 | 39 | t.truthy(exists); 40 | 41 | proc.kill('SIGTERM'); 42 | }); 43 | 44 | test.serial('ramdisk with options', async (t) => { 45 | const proc = execa('wp', ['--config', 'ramdisk/custom-options.js'], { 46 | cwd: resolve(fixturePath, '..') 47 | }); 48 | const { stderr, stdout } = proc; 49 | const pathTest = 'Build being written to '; 50 | const doneTest = '[emitted]'; 51 | 52 | const path = await waitFor(pathTest, stdout); 53 | 54 | t.regex(path, /(volumes|mnt)\/wps\/webpack-plugin-serve\/output/i); 55 | 56 | await waitFor(doneTest, stderr); 57 | 58 | const exists = existsSync(join(fixturePath, 'output/output.js')); 59 | 60 | t.truthy(exists); 61 | 62 | proc.kill('SIGTERM'); 63 | }); 64 | 65 | test.serial('context error', async (t) => { 66 | try { 67 | await execa('wp', ['--config', 'ramdisk/config-context-error.js'], { 68 | cwd: resolve(fixturePath, '..') 69 | }); 70 | } catch (e) { 71 | t.regex(e.stderr, /Please set the `context` to a another path/); 72 | t.is(e.exitCode, 1); 73 | return; 74 | } 75 | t.fail(); 76 | }); 77 | 78 | test.serial('cwd error', async (t) => { 79 | try { 80 | await execa('wp', ['--config', '../config-cwd-error.js'], { 81 | cwd: join(fixturePath, 'cwd-error') 82 | }); 83 | } catch (e) { 84 | t.regex(e.stderr, /Please run from another path/); 85 | t.is(e.exitCode, 1); 86 | return; 87 | } 88 | t.fail(); 89 | }); 90 | 91 | test.serial('ramdisk with empty package.json', async (t) => { 92 | const fixturePath = join(__dirname, 'fixtures/ramdisk-empty-pkg'); 93 | const proc = execa('wp', [], { cwd: fixturePath }); 94 | const { stderr, stdout } = proc; 95 | const pathTest = 'Build being written to '; 96 | const doneTest = '[emitted]'; 97 | 98 | const path = await waitFor(pathTest, stdout); 99 | 100 | t.regex(path, /(volumes|mnt)\/wps\/[a-f0-9]{32}\/output/i); 101 | 102 | await waitFor(doneTest, stderr); 103 | 104 | const exists = existsSync(join(fixturePath, 'output/output.js')); 105 | 106 | t.truthy(exists); 107 | }); 108 | -------------------------------------------------------------------------------- /test/routes.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | 3 | test('placeholder', (t) => t.pass()); 4 | -------------------------------------------------------------------------------- /test/server.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | 3 | test('placeholder', (t) => t.pass()); 4 | -------------------------------------------------------------------------------- /test/snapshots/errors.test.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/errors.test.js` 2 | 3 | The actual snapshot is saved in `errors.test.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## errors 8 | 9 | > Snapshot 1 10 | 11 | PluginExistsError { 12 | code: 'ERR_PLUGIN_EXISTS', 13 | message: '', 14 | } 15 | 16 | > Snapshot 2 17 | 18 | WebpackPluginServeError { 19 | message: '', 20 | } 21 | -------------------------------------------------------------------------------- /test/snapshots/errors.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/webpack-plugin-serve/b19fce1db5f881af603879b8bb9c555c16ae394f/test/snapshots/errors.test.js.snap -------------------------------------------------------------------------------- /test/snapshots/plugin.test.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/plugin.test.js` 2 | 3 | The actual snapshot is saved in `plugin.test.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## allow https null 8 | 9 | > Snapshot 1 10 | 11 | { 12 | allowMany: true, 13 | compress: null, 14 | headers: null, 15 | historyFallback: false, 16 | hmr: true, 17 | host: null, 18 | https: null, 19 | liveReload: false, 20 | log: { 21 | level: 'info', 22 | name: 'webpack-plugin-serve', 23 | prefix: { 24 | level: Function level {}, 25 | template: '{{level}}', 26 | }, 27 | }, 28 | middleware: Function middleware {}, 29 | open: false, 30 | port: { 31 | then: Function then {}, 32 | }, 33 | progress: true, 34 | publicPath: null, 35 | ramdisk: false, 36 | secure: false, 37 | static: [], 38 | status: true, 39 | } 40 | 41 | ## defaults 42 | 43 | > Snapshot 1 44 | 45 | { 46 | compress: null, 47 | headers: null, 48 | historyFallback: false, 49 | hmr: true, 50 | host: null, 51 | liveReload: false, 52 | log: { 53 | level: 'info', 54 | name: 'webpack-plugin-serve', 55 | prefix: { 56 | level: Function level {}, 57 | template: '{{level}}', 58 | }, 59 | }, 60 | middleware: Function middleware {}, 61 | open: false, 62 | port: { 63 | then: Function then {}, 64 | }, 65 | progress: true, 66 | publicPath: null, 67 | ramdisk: false, 68 | secure: false, 69 | static: [], 70 | status: true, 71 | } 72 | 73 | ## options manipulation 74 | 75 | > Snapshot 1 76 | 77 | { 78 | allowMany: true, 79 | compress: {}, 80 | headers: null, 81 | historyFallback: {}, 82 | hmr: true, 83 | host: null, 84 | liveReload: false, 85 | log: { 86 | level: 'info', 87 | name: 'webpack-plugin-serve', 88 | prefix: { 89 | level: Function level {}, 90 | template: '{{level}}', 91 | }, 92 | }, 93 | middleware: Function middleware {}, 94 | open: false, 95 | port: { 96 | then: Function then {}, 97 | }, 98 | progress: true, 99 | publicPath: 'dist', 100 | ramdisk: false, 101 | secure: false, 102 | static: [], 103 | status: true, 104 | } 105 | 106 | ## static → array(string) 107 | 108 | > Snapshot 1 109 | 110 | [ 111 | 'test/fixtures', 112 | ] 113 | 114 | ## static → glob 115 | 116 | > Snapshot 1 117 | 118 | [ 119 | 'test/fixtures/https', 120 | 'test/fixtures/multi', 121 | 'test/fixtures/proxy', 122 | 'test/fixtures/ramdisk', 123 | 'test/fixtures/ramdisk-empty-pkg', 124 | 'test/fixtures/ramdisk/cwd-error', 125 | 'test/fixtures/simple', 126 | 'test/fixtures/wait-for-build', 127 | ] 128 | 129 | ## static → string 130 | 131 | > Snapshot 1 132 | 133 | 'test/fixtures' 134 | -------------------------------------------------------------------------------- /test/snapshots/plugin.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/webpack-plugin-serve/b19fce1db5f881af603879b8bb9c555c16ae394f/test/snapshots/plugin.test.js.snap -------------------------------------------------------------------------------- /test/snapshots/proxy-rewrite.test.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/proxy-rewrite.test.js` 2 | 3 | The actual snapshot is saved in `proxy-rewrite.test.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## should rewrite /api 8 | 9 | > Snapshot 1 10 | 11 | '/test endpoint rewrite' 12 | -------------------------------------------------------------------------------- /test/snapshots/proxy-rewrite.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/webpack-plugin-serve/b19fce1db5f881af603879b8bb9c555c16ae394f/test/snapshots/proxy-rewrite.test.js.snap -------------------------------------------------------------------------------- /test/snapshots/proxy.test.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/proxy.test.js` 2 | 3 | The actual snapshot is saved in `proxy.test.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## should reach /api proxy endpoint 8 | 9 | > Snapshot 1 10 | 11 | '/api endpoint' 12 | 13 | ## should reach /api/test proxy endpoint 14 | 15 | > Snapshot 1 16 | 17 | '/api/test endpoint' 18 | -------------------------------------------------------------------------------- /test/snapshots/proxy.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/webpack-plugin-serve/b19fce1db5f881af603879b8bb9c555c16ae394f/test/snapshots/proxy.test.js.snap -------------------------------------------------------------------------------- /test/snapshots/ramdisk.test.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/ramdisk.test.js` 2 | 3 | The actual snapshot is saved in `ramdisk.test.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## ramdisk 8 | 9 | > Snapshot 1 10 | 11 | Error { 12 | all: `/Users/powella/code/webpack/webpack-plugin-serve/test/fixtures/ramdisk␊ 13 | /Users/powella/code/webpack/webpack-plugin-serve/test/fixtures␊ 14 | RamdiskPathError: Cannot remove /Users/powella/code/webpack/webpack-plugin-serve/test/fixtures/ramdisk. The ramdisk option creates a symlink from `output.path`, to the ramdisk build output path, and must remove any existing `output.path` to do so. Please set the `context` to a another path, choose a different `output.path`.␊ 15 | at WebpackPluginServe.init (/Users/powella/code/webpack/webpack-plugin-serve/lib/plugins/ramdisk.js:59:13)␊ 16 | at WebpackPluginServe.hook (/Users/powella/code/webpack/webpack-plugin-serve/lib/index.js:177:25)␊ 17 | at WebpackPluginServe.apply (/Users/powella/code/webpack/webpack-plugin-serve/lib/index.js:128:10)␊ 18 | at webpack (/Users/powella/code/webpack/webpack-plugin-serve/node_modules/webpack/lib/webpack.js:49:13)␊ 19 | at run (/Users/powella/code/webpack/webpack-plugin-serve/node_modules/webpack-nano/lib/compiler.js:16:20)␊ 20 | at doeet (/Users/powella/code/webpack/webpack-plugin-serve/node_modules/webpack-nano/bin/wp.js:78:3)`, 21 | command: 'wp --config ramdisk/config-context-error.js', 22 | exitCode: 1, 23 | exitCodeName: 'EPERM', 24 | failed: true, 25 | isCanceled: false, 26 | killed: false, 27 | signal: undefined, 28 | stderr: `RamdiskPathError: Cannot remove /Users/powella/code/webpack/webpack-plugin-serve/test/fixtures/ramdisk. The ramdisk option creates a symlink from `output.path`, to the ramdisk build output path, and must remove any existing `output.path` to do so. Please set the `context` to a another path, choose a different `output.path`.␊ 29 | at WebpackPluginServe.init (/Users/powella/code/webpack/webpack-plugin-serve/lib/plugins/ramdisk.js:59:13)␊ 30 | at WebpackPluginServe.hook (/Users/powella/code/webpack/webpack-plugin-serve/lib/index.js:177:25)␊ 31 | at WebpackPluginServe.apply (/Users/powella/code/webpack/webpack-plugin-serve/lib/index.js:128:10)␊ 32 | at webpack (/Users/powella/code/webpack/webpack-plugin-serve/node_modules/webpack/lib/webpack.js:49:13)␊ 33 | at run (/Users/powella/code/webpack/webpack-plugin-serve/node_modules/webpack-nano/lib/compiler.js:16:20)␊ 34 | at doeet (/Users/powella/code/webpack/webpack-plugin-serve/node_modules/webpack-nano/bin/wp.js:78:3)`, 35 | stdout: `/Users/powella/code/webpack/webpack-plugin-serve/test/fixtures/ramdisk␊ 36 | /Users/powella/code/webpack/webpack-plugin-serve/test/fixtures`, 37 | timedOut: false, 38 | message: 'Command failed with exit code 1 (EPERM): wp --config ramdisk/config-context-error.js', 39 | } 40 | -------------------------------------------------------------------------------- /test/snapshots/ramdisk.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/webpack-plugin-serve/b19fce1db5f881af603879b8bb9c555c16ae394f/test/snapshots/ramdisk.test.js.snap -------------------------------------------------------------------------------- /test/snapshots/validate.test.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/validate.test.js` 2 | 3 | The actual snapshot is saved in `validate.test.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## error 8 | 9 | > Snapshot 1 10 | 11 | StructError { 12 | branch: [ 13 | { 14 | foo: 'bar', 15 | }, 16 | 'bar', 17 | ], 18 | failures: Function {}, 19 | key: 'foo', 20 | path: [ 21 | 'foo', 22 | ], 23 | refinement: undefined, 24 | type: 'never', 25 | value: 'bar', 26 | message: 'At path: foo -- Expected a value of type `never` for `foo`, but received: `"bar"`', 27 | } 28 | 29 | ## promise 30 | 31 | > Snapshot 1 32 | 33 | StructError { 34 | branch: [ 35 | { 36 | host: 0, 37 | port: '0', 38 | }, 39 | 0, 40 | ], 41 | failures: Function {}, 42 | key: 'host', 43 | path: [ 44 | 'host', 45 | ], 46 | refinement: undefined, 47 | type: 'union', 48 | value: 0, 49 | message: 'At path: host -- Expected the value to satisfy a union of `any | string`, but received: 0', 50 | } 51 | 52 | > Snapshot 2 53 | 54 | { 55 | error: undefined, 56 | value: { 57 | host: Promise {}, 58 | port: Promise {}, 59 | }, 60 | } 61 | 62 | > Snapshot 3 63 | 64 | { 65 | error: undefined, 66 | value: { 67 | host: { 68 | then: Function then {}, 69 | }, 70 | port: { 71 | then: Function then {}, 72 | }, 73 | }, 74 | } 75 | 76 | ## throws 77 | 78 | > Snapshot 1 79 | 80 | StructError { 81 | branch: [ 82 | { 83 | batman: 'nanananana', 84 | }, 85 | 'nanananana', 86 | ], 87 | failures: Function {}, 88 | key: 'batman', 89 | path: [ 90 | 'batman', 91 | ], 92 | refinement: undefined, 93 | type: 'never', 94 | value: 'nanananana', 95 | message: 'At path: batman -- Expected a value of type `never` for `batman`, but received: `"nanananana"`', 96 | } 97 | -------------------------------------------------------------------------------- /test/snapshots/validate.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shellscape/webpack-plugin-serve/b19fce1db5f881af603879b8bb9c555c16ae394f/test/snapshots/validate.test.js.snap -------------------------------------------------------------------------------- /test/validate.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | 3 | const { defaults, WebpackPluginServe } = require('../lib'); 4 | const { validate } = require('../lib/validate'); 5 | 6 | test('defaults', (t) => { 7 | delete defaults.secure; 8 | const result = validate(defaults); 9 | t.falsy(result.error); 10 | }); 11 | 12 | test('client', (t) => { 13 | const result = validate({ client: { address: '0', protocol: "wss", retry: false, silent: false } }); 14 | t.falsy(result.error); 15 | 16 | const resultWs = validate({ client: { address: '0', protocol: "ws", retry: false, silent: false } }); 17 | t.falsy(resultWs.error); 18 | 19 | const resultProtocolBad = validate({ client: { address: '0', protocol: "lala", retry: false, silent: false } }); 20 | t.truthy(resultProtocolBad.error); 21 | }); 22 | 23 | test('error', (t) => { 24 | const result = validate({ foo: 'bar' }); 25 | t.snapshot(result.error); 26 | }); 27 | 28 | test('promise', (t) => { 29 | const promise = new Promise(() => {}); 30 | const thenable = { then() {} }; 31 | let result = validate({ host: 0, port: '0' }); 32 | t.truthy(result.error); 33 | t.snapshot(result.error); 34 | result = validate({ host: promise, port: promise }); 35 | t.falsy(result.error); 36 | t.snapshot(result); 37 | result = validate({ host: thenable, port: thenable }); 38 | t.falsy(result.error); 39 | t.snapshot(result); 40 | }); 41 | 42 | test('throws', (t) => { 43 | const error = t.throws(() => new WebpackPluginServe({ batman: 'nanananana' })); 44 | t.snapshot(error); 45 | }); 46 | -------------------------------------------------------------------------------- /test/wait-for-build.test.js: -------------------------------------------------------------------------------- 1 | const del = require('del'); 2 | const webpack = require('webpack'); 3 | const test = require('ava'); 4 | const fetch = require('node-fetch'); 5 | const defer = require('p-defer'); 6 | 7 | const { getPort } = require('./helpers/port'); 8 | const { make } = require('./fixtures/wait-for-build/make-config'); 9 | 10 | let watcher; 11 | let port; 12 | 13 | test.before('Starting server', async () => { 14 | const deferred = defer(); 15 | port = await getPort(); 16 | const { serve, config } = make(port); 17 | const compiler = webpack(config); 18 | watcher = compiler.watch({}, () => {}); 19 | serve.on('listening', deferred.resolve); 20 | await deferred.promise; 21 | }); 22 | 23 | test.after.always('Closing server', async () => { 24 | watcher.close(); 25 | await del('./test/fixtures/waitForBuild/output'); 26 | }); 27 | 28 | test('should wait until bundle is compiled', async (t) => { 29 | const response = await fetch(`http://localhost:${port}/test`); 30 | const text = await response.text(); 31 | t.is(text, 'success'); 32 | }); 33 | -------------------------------------------------------------------------------- /test/ws.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Koa = require('koa'); 3 | const router = require('koa-route'); 4 | const defer = require('p-defer'); 5 | const WebSocket = require('ws'); 6 | 7 | const { middleware } = require('../lib/ws'); 8 | 9 | const { getPort } = require('./helpers/port'); 10 | 11 | test('websocket middleware', async (t) => { 12 | const app = new Koa(); 13 | const port = await getPort(); 14 | const uri = `ws://localhost:${port}/test`; 15 | const routeDeferred = defer(); 16 | const resultDeferred = defer(); 17 | 18 | app.use(middleware); 19 | app.use( 20 | router.get('/test', async (ctx) => { 21 | t.truthy(ctx.ws); 22 | 23 | const socket = await ctx.ws(); 24 | 25 | t.truthy(socket); 26 | routeDeferred.resolve(); 27 | }) 28 | ); 29 | 30 | const server = app.listen(port); 31 | 32 | await { 33 | then(r, f) { 34 | server.on('listening', r); 35 | server.on('error', f); 36 | } 37 | }; 38 | 39 | const socket = new WebSocket(uri); 40 | 41 | socket.on('open', async () => { 42 | await routeDeferred.promise; 43 | socket.close(); 44 | resultDeferred.resolve(); 45 | }); 46 | 47 | await resultDeferred.promise; 48 | }); 49 | --------------------------------------------------------------------------------