├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── build-and-test.yml ├── .gitignore ├── .nycrc ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── API Reference.md └── webpack3.md ├── examples ├── swagger-webpack3 │ ├── .eslintrc │ ├── index.html │ ├── index.js │ ├── package.json │ └── webpack.config.js ├── swagger-webpack4 │ ├── .eslintrc │ ├── index.html │ ├── index.js │ ├── package.json │ └── webpack.config.js └── swagger-webpack5 │ ├── .eslintrc │ ├── index.html │ ├── index.js │ ├── package.json │ └── webpack.config.js ├── jest.config.js ├── package.json ├── src ├── __tests__ │ ├── count-loader.js │ └── index.test.ts ├── index.ts └── virtual-stats.ts ├── tsconfig.build.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "modules": true 8 | } 9 | }, 10 | "plugins": [ 11 | "@typescript-eslint/eslint-plugin", 12 | "prettier", 13 | "jest" 14 | ], 15 | "env": { 16 | "es6": true, 17 | "node": true, 18 | "jest": true 19 | }, 20 | "extends": [ 21 | "eslint:recommended", 22 | "plugin:jest/recommended", 23 | "prettier" 24 | ], 25 | "rules": { 26 | "@typescript-eslint/no-explicit-any": 0, 27 | "@typescript-eslint/explicit-function-return-type": 0, 28 | "@typescript-eslint/no-var-requires": 0, 29 | "no-use-before-define": 0, 30 | "arrow-body-style": 0, 31 | "dot-notation": 0, 32 | "no-console": 0, 33 | "semi": 2, 34 | "prettier/prettier": [ 35 | "error", 36 | { 37 | "printWidth": 120, 38 | "singleQuote": true, 39 | "parser": "typescript" 40 | } 41 | ] 42 | }, 43 | "settings": { 44 | "@typescript-eslint/camelcase": ["error", {"allow": ["__non_webpack_module__", "__non_webpack_require__", "^npm(_[a-z]+)+$"]}], 45 | "@typescript-eslint/class-name-casing": 2, 46 | "@typescript-eslint/indent": ["error", 2, {"SwitchCase": 1}], 47 | "@typescript-eslint/no-unused-vars": [2, {"args": "none", "ignoreRestSiblings": true}], 48 | "array-bracket-spacing": 2, 49 | "arrow-spacing": 2, 50 | "comma-dangle": ["error", "always-multiline"], 51 | "computed-property-spacing": 2, 52 | "generator-star-spacing": ["error", {"before": true, "after": true}], 53 | "keyword-spacing": 2, 54 | "no-multiple-empty-lines": 2, 55 | "no-tabs": 2, 56 | "no-trailing-spaces": 2, 57 | "no-unused-vars": "off", 58 | "object-curly-spacing": 2, 59 | "padded-blocks": ["error", "never"], 60 | "prefer-template": 2, 61 | "rest-spread-spacing": 2, 62 | "template-curly-spacing": 2 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Create a report to help us improve 4 | title: "[Bug]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] I'd be willing to submit the fix 11 | 12 | **Describe the bug** 13 | 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | 18 | The _minimal_ information needed to reproduce your issue (ideally a very minimal project setup published on GitHub). 19 | Note that bugs without minimal reproductions might be closed. 20 | 21 | **Screenshots** 22 | 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Environment if relevant (please complete the following information):** 26 | 27 | - OS: [e.g. OSX, Linux, Windows, ...] 28 | - Node version [e.g. 8.15.0, 10.15.1, ...] 29 | - Mochapack version [e.g. 1.1.3, ...] 30 | - Webpack version [e.g. 4.26.1, ...] 31 | 32 | **Additional context** 33 | 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4AB Feature request" 3 | about: Suggest an idea for this project 4 | title: "[Feature]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] I'd be willing to implement this feature 11 | 12 | **Describe the user story** 13 | 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | **Describe the solution you'd like** 17 | 18 | A clear and concise description of what you want to happen. 19 | 20 | **Describe the drawbacks of your solution** 21 | 22 | This section is important not only to identify future issues, but also to see whether you thought your request through. When filling it, ask yourself "how can I break it". 23 | 24 | **Describe alternatives you've considered** 25 | 26 | A clear and concise description of any alternative solutions or features you've considered, and why you think they wouldn't be good enough. 27 | 28 | **Additional context** 29 | 30 | Add any other context or screenshots about the feature request here. 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **What's the problem this PR addresses?** 2 | 3 | ... 4 | 5 | **How did you fix it?** 6 | 7 | ... 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | open-pull-requests-limit: 0 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and test webpack-virtual-modules 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | webpack-version: [3, 4, 5] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: "Use Node.js 18.x" 20 | uses: actions/setup-node@master 21 | with: 22 | node-version: 18.x 23 | 24 | - name: "Install nari" 25 | run: | 26 | npm i -g nari 27 | 28 | - name: "Install" 29 | run: | 30 | nari 31 | 32 | - name: Install Webpack ${{ matrix.webpack-version }} 33 | run: | 34 | nari add webpack@${{ matrix.webpack-version }} 35 | 36 | - name: Run tests 37 | run: nari test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tmp 2 | lib 3 | node_modules 4 | dist 5 | .nyc_output 6 | .eslintcache 7 | .history 8 | .vscode 9 | .yarn 10 | npm-debug.log 11 | reports 12 | .idea/* 13 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | ".js" 4 | ], 5 | "exclude": [ 6 | ".tmp/**", 7 | "tests/**", 8 | "virtual-stats.js" 9 | ], 10 | "reporter": [ 11 | "lcov", 12 | "html", 13 | "text-summary" 14 | ], 15 | "report-dir": "./reports/coverage" 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 SysGears (Cyprus) Limited 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webpack Virtual Modules 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![License][license-src]][license-href] 6 | 7 | **Webpack Virtual Modules** is a plugin that allows for dynamical generation of in-memory virtual modules for JavaScript 8 | builds created with webpack. When virtual module is created all the parent virtual dirs that lead to the module filename are created too. This plugin supports watch mode meaning any write to a virtual module is seen by webpack as 9 | if a real file stored on disk has changed. 10 | 11 | ## Installation 12 | 13 | Use NPM or Yarn to install Webpack Virtual Modules as a development dependency: 14 | 15 | ```bash 16 | # with NPM 17 | npm install webpack-virtual-modules --save-dev 18 | 19 | # with Yarn 20 | yarn add webpack-virtual-modules --dev 21 | ``` 22 | 23 | ## Usage 24 | 25 | You can use Webpack Virtual Modules with webpack 5, 4 and 3. The examples below show the usage with webpack 5 or 4. If you want to use our plugin with webpack 3, check out a dedicated doc: 26 | 27 | * [Webpack Virtual Modules with Webpack 3] 28 | 29 | ### Generating static virtual modules 30 | 31 | Require the plugin in the webpack configuration file, then create and add virtual modules in the `plugins` array in the 32 | webpack configuration object: 33 | 34 | ```js 35 | var VirtualModulesPlugin = require('webpack-virtual-modules'); 36 | 37 | var virtualModules = new VirtualModulesPlugin({ 38 | 'node_modules/module-foo.js': 'module.exports = { foo: "foo" };', 39 | 'node_modules/module-bar.js': 'module.exports = { bar: "bar" };' 40 | }); 41 | 42 | module.exports = { 43 | // ... 44 | plugins: [ 45 | virtualModules 46 | ] 47 | }; 48 | ``` 49 | 50 | You can now import your virtual modules anywhere in the application and use them: 51 | 52 | ```js 53 | var moduleFoo = require('module-foo'); 54 | // You can now use moduleFoo 55 | console.log(moduleFoo.foo); 56 | ``` 57 | 58 | ### Generating dynamic virtual modules 59 | 60 | You can generate virtual modules **_dynamically_** with Webpack Virtual Modules. 61 | 62 | Here's an example of dynamic generation of a module. All you need to do is create new virtual modules using the plugin 63 | and add them to the `plugins` array. After that, you need to add a webpack hook. For using hooks, consult [webpack 64 | compiler hook documentation]. 65 | 66 | ```js 67 | var webpack = require('webpack'); 68 | var VirtualModulesPlugin = require('webpack-virtual-modules'); 69 | 70 | // Create an empty set of virtual modules 71 | const virtualModules = new VirtualModulesPlugin(); 72 | 73 | var compiler = webpack({ 74 | // ... 75 | plugins: [ 76 | virtualModules 77 | ] 78 | }); 79 | 80 | compiler.hooks.compilation.tap('MyPlugin', function(compilation) { 81 | virtualModules.writeModule('node_modules/module-foo.js', ''); 82 | }); 83 | 84 | compiler.watch(); 85 | ``` 86 | 87 | In other module or a Webpack plugin, you can write to the module `module-foo` whatever you need. After this write, 88 | webpack will "see" that `module-foo.js` has changed and will restart compilation. 89 | 90 | ```js 91 | virtualModules.writeModule( 92 | 'node_modules/module-foo.js', 93 | 'module.exports = { foo: "foo" };' 94 | ); 95 | ``` 96 | 97 | ## More Examples 98 | 99 | - [Swagger and JSDoc Example with Webpack 5] 100 | - [Swagger and JSDoc Example with Webpack 4] 101 | - [Swagger and JSDoc Example with Webpack 3] 102 | 103 | ## API Reference 104 | 105 | - [API Reference] 106 | 107 | ## Inspiration 108 | 109 | This project is inspired by [virtual-module-webpack-plugin]. 110 | 111 | ## Contributing 112 | 113 | This project uses [nari] package manager to have reproducible builds without resorting to lockfiles, it uses `lockTime` field in `package.json` instead. 114 | 115 | To install `nari` execute: 116 | `npm i -g nari` 117 | 118 | And then `nari` to install the project. 119 | 120 | To run project scripts use `nari script_name`, for example `nari test` to run unit tests. 121 | 122 | ## License 123 | 124 | Copyright © 2017 [SysGears (Cyprus) Limited]. This source code is licensed under the [MIT] license. 125 | 126 | [nari]: https://github.com/narijs/nari 127 | [webpack virtual modules with webpack 3]: https://github.com/sysgears/webpack-virtual-modules/tree/master/docs/webpack3.md 128 | [webpack compiler hook documentation]: https://webpack.js.org/api/compiler-hooks/ 129 | [swagger and jsdoc example with webpack 3]: https://github.com/sysgears/webpack-virtual-modules/tree/master/examples/swagger-webpack3 130 | [swagger and jsdoc example with webpack 4]: https://github.com/sysgears/webpack-virtual-modules/tree/master/examples/swagger-webpack4 131 | [swagger and jsdoc example with webpack 5]: https://github.com/sysgears/webpack-virtual-modules/tree/master/examples/swagger-webpack5 132 | [api reference]: https://github.com/sysgears/webpack-virtual-modules/tree/master/docs/API%20Reference.md 133 | [virtual-module-webpack-plugin]: https://github.com/rmarscher/virtual-module-webpack-plugin 134 | [MIT]: LICENSE 135 | [SysGears (Cyprus) Limited]: http://sysgears.com 136 | 137 | 138 | [npm-version-src]: https://img.shields.io/npm/v/webpack-virtual-modules?style=flat 139 | [npm-version-href]: https://npmjs.com/package/webpack-virtual-modules 140 | [npm-downloads-src]: https://img.shields.io/npm/dm/webpack-virtual-modules?style=flat 141 | [npm-downloads-href]: https://npmjs.com/package/webpack-virtual-modules 142 | [license-src]: https://img.shields.io/github/license/sysgears/webpack-virtual-modules.svg?style=flat 143 | [license-href]: https://github.com/sysgears/webpack-virtual-modules/blob/main/LICENSE 144 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | `@babel/plugin-transform-modules-commonjs`, 4 | `@babel/plugin-proposal-class-properties`, 5 | 'babel-plugin-replace-ts-export-assignment', 6 | ], 7 | presets: [`@babel/preset-typescript`], 8 | }; 9 | -------------------------------------------------------------------------------- /docs/API Reference.md: -------------------------------------------------------------------------------- 1 | # Webpack Virtual Modules API Reference 2 | 3 | There are few methods that you can use with Webpack Virtual Modules. If you've run [the examples] in the 4 | previous sections, then you've noticed the methods `apply()` and `writeModule()` that you can run on the instance of 5 | Webpack Virtual Modules. 6 | 7 | ## `apply` 8 | 9 | **Function** Attaches necessary hooks, in particular, `afterEnvironment`, `afterResolvers`, and `watchRun` hools, to 10 | respective events for the Webpack Virtual Modules plugin to ensure that the virtual files are added dynamically. 11 | 12 | **Parameters** 13 | 14 | `compiler`     `object`     required     The webpack compiler object 15 | 16 | **Returns** 17 | 18 | `void` 19 | 20 | Usage example 21 | 22 | ```js 23 | const virtualModules = new VirtualModulesPlugin({[customPluginFilePath]: JSON.stringify({ 24 | openapi: '3.0.0', 25 | info: info 26 | })}); 27 | 28 | // Passing the webpack compiler to the virtual module 29 | virtualModules.apply(compiler); 30 | ``` 31 | 32 | ## `writeModule` 33 | 34 | **Function** Writes a static or dynamic virtual module to a path. 35 | 36 | `writeModule(filePath: string, contents: string):` 37 | 38 | **Parameters** 39 | 40 | `filePath`     `string`     required     The path to the generated file 41 | where the virtual module will be stored. The path is relative to the webpack context (`path.join(webpackContext, 42 | filePath)` is used to determine module location.) 43 | 44 | `contents`     `string`     required     The string to be written into the 45 | file. The string can contain any code or text 46 | 47 | **Returns** 48 | 49 | `void` 50 | 51 | [the usage examples]: https://github.com/sysgears/webpack-virtual-modules/tree/master/examples/swagger-webpack4 52 | -------------------------------------------------------------------------------- /docs/webpack3.md: -------------------------------------------------------------------------------- 1 | # Using Webpack Virtual Modules with Webpack 3 2 | 3 | ## Installation 4 | 5 | Use NPM or Yarn to install Webpack Virtual Modules as a development dependency: 6 | 7 | ```bash 8 | # with NPM 9 | npm install webpack-virtual-modules --save-dev 10 | 11 | # with Yarm 12 | yarn add webpack-virtual-modules --dev 13 | ``` 14 | 15 | ## Usage 16 | 17 | You can use Webpack Virtual Modules with webpack 5, 4 and 3. The examples below show the usage with webpack 3. If you want 18 | to use our plugin with webpack 5 or 4, check out a dedicated section in `README.md`: 19 | 20 | * [Webpack Virtual Modules with Webpack 4 or 5] 21 | 22 | ### Generating static virtual modules 23 | 24 | Require the plugin in the webpack configuration file, then create and add virtual modules in the `plugins` array in the 25 | webpack configuration object: 26 | 27 | ```js 28 | var VirtualModulesPlugin = require("webpack-virtual-modules"); 29 | 30 | var virtualModules = new VirtualModulesPlugin({ 31 | 'node_modules/module-foo.js': 'module.exports = { foo: "foo" };' 32 | 'node_modules/module-bar.js': 'module.exports = { bar: "bar" };' 33 | }); 34 | 35 | module.exports = { 36 | // ... 37 | plugins: [ 38 | virtualModules 39 | ] 40 | }; 41 | ``` 42 | 43 | You can now import your virtual modules anywhere in the application and use them: 44 | 45 | ```js 46 | var moduleFoo = require('module-foo'); 47 | // You can now use moduleFoo in other file 48 | console.log(moduleFoo.foo); 49 | ``` 50 | 51 | ### Generating dynamic virtual modules 52 | 53 | You can generate virtual modules **_dynamically_** with Webpack Virtual Modules. 54 | 55 | Here's an example of dynamic generation of a module. All you need to do is create new virtual modules using the plugin 56 | and add them to the `plugins` array. After that, you need to add a webpack hook. For using hooks, consult [webpack 57 | compiler hook documentation]. 58 | 59 | ```js 60 | var webpack = require("webpack"); 61 | var VirtualModulesPlugin = require("webpack-virtual-modules"); 62 | 63 | var virtualModules = new VirtualModulesPlugin(); 64 | 65 | var compiler = webpack({ 66 | // ... 67 | plugins: [ 68 | virtualModules 69 | ] 70 | }); 71 | 72 | compiler.plugin('watch', function(callback) { 73 | virtualModules.writeModule('node_modules/module-foo.js', ''); 74 | callback(); 75 | }); 76 | 77 | compiler.watch(); 78 | ``` 79 | 80 | In other module or a Webpack plugin, you can write to the module `module-foo` whatever you need. After this write, 81 | webpack will "see" that `module-foo.js` has changed and will restart compilation. 82 | 83 | ```js 84 | virtualModules.writeModule( 85 | 'node_modules/module-foo.js', 86 | 'module.exports = { foo: "foo" };' 87 | ); 88 | ``` 89 | 90 | ## Examples 91 | 92 | - [Swagger JSDoc Example with Webpack 5] 93 | - [Swagger JSDoc Example with Webpack 4] 94 | - [Swagger JSDoc Example with Webpack 3] 95 | 96 | [webpack virtual modules with webpack 4 or 5]: https://github.com/sysgears/webpack-virtual-modules/blob/master/README.md#usage 97 | [swagger jsdoc example with webpack 3]: https://github.com/sysgears/webpack-virtual-modules/tree/master/examples/swagger-webpack3 98 | [swagger jsdoc example with webpack 4]: https://github.com/sysgears/webpack-virtual-modules/tree/master/examples/swagger-webpack4 99 | [swagger and jsdoc example with webpack 5]: https://github.com/sysgears/webpack-virtual-modules/tree/master/examples/swagger-webpack5 100 | -------------------------------------------------------------------------------- /examples/swagger-webpack3/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended" 4 | ], 5 | "env": { 6 | "browser": true 7 | }, 8 | "rules": { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/swagger-webpack3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Swagger UI with Webpack Virtual Modules and Webpack 3 6 | 7 | 8 |

9 |
10 | Loading... 11 |
12 |

13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/swagger-webpack3/index.js: -------------------------------------------------------------------------------- 1 | // Requiring virtual module generated in-memory by plugin 2 | const swaggerJson = require('swagger.json'); 3 | const swaggerUi = require('swagger-ui'); 4 | require('swagger-ui/dist/swagger-ui.css'); 5 | 6 | /** 7 | * @swagger 8 | * /api/hello: 9 | * get: 10 | * description: Returns hello message 11 | * parameters: 12 | * - name: subject 13 | * in: query 14 | * schema: 15 | * type: string 16 | * responses: 17 | * '200': 18 | * content: 19 | * application/json: 20 | * schema: 21 | * type: string 22 | */ 23 | function getHello(name) { 24 | // TODO: Change this with REST API call, when it will be implemented on backend 25 | return { message: 'Hello ' + name + '!' }; 26 | } 27 | 28 | var helloDiv = document.getElementById('hello'); 29 | helloDiv.innerHTML = getHello('World').message; 30 | 31 | swaggerUi({ 32 | spec: swaggerJson, dom_id: '#apiDocs', 33 | presets: [ 34 | swaggerUi.presets.apis, 35 | swaggerUi.SwaggerUIStandalonePreset 36 | ] 37 | }); 38 | -------------------------------------------------------------------------------- /examples/swagger-webpack3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swagger-webpack3", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "webpack-dev-server --open" 7 | }, 8 | "devDependencies": { 9 | "css-loader": "^2.1.1", 10 | "style-loader": "^0.23.1", 11 | "swagger-jsdoc": "^3.2.7", 12 | "webpack": "^3.0.0", 13 | "webpack-cli": "^3.0.0", 14 | "webpack-dev-server": "^2.0.0" 15 | }, 16 | "dependencies": { 17 | "swagger-ui": "^3.23.11" 18 | }, 19 | "resolutions": { 20 | "lodash": "^4.17.15" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/swagger-webpack3/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const VirtualModulesPlugin = require('../..'); 3 | const swaggerJsDoc = require('swagger-jsdoc'); 4 | 5 | function SwaggerPlugin() { 6 | } 7 | 8 | SwaggerPlugin.prototype.apply = function(compiler) { 9 | const pkgJsonModule = './package.json'; 10 | const pkgJsonPath = require.resolve(pkgJsonModule); 11 | const pkgJson = require(pkgJsonModule); 12 | // Path on file system for `swagger.json`, where webpack should "see" it 13 | const swaggerJsonPath = path.join(path.dirname(pkgJsonPath), 'node_modules', 'swagger.json'); 14 | // Creating virtual module with initial contents 15 | const virtualModules = new VirtualModulesPlugin({[swaggerJsonPath]: JSON.stringify({ 16 | openapi: '3.0.0', 17 | info: { title: pkgJson.name, version: pkgJson.version, description: pkgJson.description } 18 | })}); 19 | virtualModules.apply(compiler); 20 | 21 | compiler.plugin('compilation', function(compilation) { 22 | try { 23 | const swaggerJson = swaggerJsDoc({ 24 | swaggerDefinition: { 25 | openapi: '3.0.0', 26 | info: { title: pkgJson.name, version: pkgJson.version, description: pkgJson.description } 27 | }, 28 | apis: ['*.js', '!(node_modules)/**/*.js'] 29 | }); 30 | virtualModules.writeModule(swaggerJsonPath, JSON.stringify(swaggerJson)); 31 | } catch (e) { 32 | compilation.errors.push(e); 33 | } 34 | }); 35 | } 36 | 37 | module.exports = { 38 | entry: './index.js', 39 | plugins: [new SwaggerPlugin()], 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.css$/, 44 | use: ['style-loader', 'css-loader'] 45 | } 46 | ] 47 | } 48 | }; -------------------------------------------------------------------------------- /examples/swagger-webpack4/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended" 4 | ], 5 | "env": { 6 | "browser": true 7 | }, 8 | "rules": { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/swagger-webpack4/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Swagger UI with Webpack Virtual Modules and Webpack 4 6 | 7 | 8 |

9 |
10 | Loading... 11 |
12 |

13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/swagger-webpack4/index.js: -------------------------------------------------------------------------------- 1 | // Requiring virtual module generated in memory by the Webpack Virtual Modules plugin 2 | const swaggerJson = require('swagger.json'); 3 | const swaggerUi = require('swagger-ui'); 4 | require('swagger-ui/dist/swagger-ui.css'); 5 | 6 | /** 7 | * @swagger 8 | * /api/hello: 9 | * get: 10 | * description: Returns hello message 11 | * parameters: 12 | * - name: subject 13 | * in: query 14 | * schema: 15 | * type: string 16 | * responses: 17 | * '200': 18 | * content: 19 | * application/json: 20 | * schema: 21 | * type: string 22 | */ 23 | function getHello(name) { 24 | // TODO: Replace the code with a REST API call when it's implemented on the backend 25 | return { message: 'Hello ' + name + '!' }; 26 | } 27 | 28 | var helloDiv = document.getElementById('hello'); 29 | helloDiv.innerHTML = getHello('World').message; 30 | 31 | swaggerUi({ 32 | spec: swaggerJson, dom_id: '#apiDocs', 33 | presets: [ 34 | swaggerUi.presets.apis, 35 | swaggerUi.SwaggerUIStandalonePreset 36 | ] 37 | }); 38 | -------------------------------------------------------------------------------- /examples/swagger-webpack4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swagger-webpack4", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "webpack-dev-server --open" 7 | }, 8 | "devDependencies": { 9 | "css-loader": "^2.1.1", 10 | "style-loader": "^0.23.1", 11 | "swagger-jsdoc": "^3.2.7", 12 | "webpack": "^4.0.0", 13 | "webpack-cli": "^3.0.0", 14 | "webpack-dev-server": "^3.0.0" 15 | }, 16 | "dependencies": { 17 | "swagger-ui": "^3.24.0" 18 | }, 19 | "resolutions": { 20 | "lodash": "^4.17.15" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/swagger-webpack4/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const VirtualModulesPlugin = require('../..'); 3 | const swaggerJsDoc = require('swagger-jsdoc'); 4 | 5 | function SwaggerPlugin() {} 6 | 7 | SwaggerPlugin.prototype.apply = function(compiler) { 8 | const pkgJsonModule = './package.json'; 9 | const pkgJsonPath = require.resolve(pkgJsonModule); 10 | const pkgJson = require(pkgJsonModule); 11 | // Create some mock data for the virtual module 12 | const info = { 13 | title: pkgJson.name, version: pkgJson.version, description: pkgJson.description 14 | }; 15 | // Creating an absolute path for 'swagger.json' 16 | // Webpack will look up 'swagger.json' by this path 17 | const swaggerJsonPath = path.join(path.dirname(pkgJsonPath), 'node_modules', 'swagger.json'); 18 | // Creating a virtual module 'swagger.json' with initial content 19 | const virtualModules = new VirtualModulesPlugin({[swaggerJsonPath]: JSON.stringify({ 20 | openapi: '3.0.0', 21 | info: info 22 | })}); 23 | // Applying a webpack compiler to the virtual module 24 | virtualModules.apply(compiler); 25 | 26 | // Adding a webpack hook to create new virtual module with swaggerJsDoc() at compile time 27 | // Consult Swagger UI documentation for the settings passed to swaggerJsDoc() 28 | compiler.hooks.compilation.tap('SwaggerPlugin', function(compilation) { 29 | try { 30 | const swaggerJson = swaggerJsDoc({ 31 | swaggerDefinition: { 32 | openapi: '3.0.0', 33 | info: info 34 | }, 35 | apis: ['*.js', '!(node_modules)/**/*.js'] 36 | }); 37 | // Write new data to the virtual file at compile time 38 | virtualModules.writeModule(swaggerJsonPath, JSON.stringify(swaggerJson)); 39 | } catch (e) { 40 | compilation.errors.push(e); 41 | } 42 | }); 43 | }; 44 | 45 | module.exports = { 46 | entry: './index.js', 47 | plugins: [new SwaggerPlugin()], 48 | module: { 49 | rules: [ 50 | { 51 | test: /\.css$/, 52 | use: ['style-loader', 'css-loader'] 53 | } 54 | ] 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /examples/swagger-webpack5/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended" 4 | ], 5 | "env": { 6 | "browser": true 7 | }, 8 | "rules": { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/swagger-webpack5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Swagger UI with Webpack Virtual Modules and Webpack 4 6 | 7 | 8 |

9 |
10 | Loading... 11 |
12 |

13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/swagger-webpack5/index.js: -------------------------------------------------------------------------------- 1 | // Requiring virtual module generated in memory by the Webpack Virtual Modules plugin 2 | const swaggerJson = require('swagger.json'); 3 | const swaggerUi = require('swagger-ui'); 4 | require('swagger-ui/dist/swagger-ui.css'); 5 | 6 | /** 7 | * @swagger 8 | * /api/hello: 9 | * get: 10 | * description: Returns hello message 11 | * parameters: 12 | * - name: subject 13 | * in: query 14 | * schema: 15 | * type: string 16 | * responses: 17 | * '200': 18 | * content: 19 | * application/json: 20 | * schema: 21 | * type: string 22 | */ 23 | function getHello(name) { 24 | // TODO: Replace the code with a REST API call when it's implemented on the backend 25 | return { message: 'Hello ' + name + '!' }; 26 | } 27 | 28 | var helloDiv = document.getElementById('hello'); 29 | helloDiv.innerHTML = getHello('World').message; 30 | 31 | swaggerUi({ 32 | spec: swaggerJson, dom_id: '#apiDocs', 33 | presets: [ 34 | swaggerUi.presets.apis, 35 | swaggerUi.SwaggerUIStandalonePreset 36 | ] 37 | }); 38 | -------------------------------------------------------------------------------- /examples/swagger-webpack5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swagger-webpack5", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "webpack-dev-server --open" 7 | }, 8 | "devDependencies": { 9 | "css-loader": "^2.1.1", 10 | "style-loader": "^0.23.1", 11 | "swagger-jsdoc": "^3.2.7", 12 | "webpack": "^5.0.0-beta.29", 13 | "webpack-cli": "^3.3.12", 14 | "webpack-dev-server": "^3.0.0" 15 | }, 16 | "dependencies": { 17 | "buffer": "^5.6.0", 18 | "swagger-ui": "^3.24.0" 19 | }, 20 | "resolutions": { 21 | "lodash": "^4.17.15" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/swagger-webpack5/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const VirtualModulesPlugin = require('../..'); 3 | const webpack = require('webpack'); 4 | const swaggerJsDoc = require('swagger-jsdoc'); 5 | 6 | function SwaggerPlugin() {} 7 | 8 | SwaggerPlugin.prototype.apply = function(compiler) { 9 | const pkgJsonModule = './package.json'; 10 | const pkgJsonPath = require.resolve(pkgJsonModule); 11 | const pkgJson = require(pkgJsonModule); 12 | // Create some mock data for the virtual module 13 | const info = { 14 | title: pkgJson.name, version: pkgJson.version, description: pkgJson.description 15 | }; 16 | // Creating an absolute path for 'swagger.json' 17 | // Webpack will look up 'swagger.json' by this path 18 | const swaggerJsonPath = path.join(path.dirname(pkgJsonPath), 'node_modules', 'swagger.json'); 19 | // Creating a virtual module 'swagger.json' with initial content 20 | const virtualModules = new VirtualModulesPlugin({[swaggerJsonPath]: JSON.stringify({ 21 | openapi: '3.0.0', 22 | info: info 23 | })}); 24 | // Applying a webpack compiler to the virtual module 25 | virtualModules.apply(compiler); 26 | 27 | // Adding a webpack hook to create new virtual module with swaggerJsDoc() at compile time 28 | // Consult Swagger UI documentation for the settings passed to swaggerJsDoc() 29 | compiler.hooks.compilation.tap('SwaggerPlugin', function(compilation) { 30 | try { 31 | const swaggerJson = swaggerJsDoc({ 32 | swaggerDefinition: { 33 | openapi: '3.0.0', 34 | info: info 35 | }, 36 | apis: ['*.js', '!(node_modules)/**/*.js'] 37 | }); 38 | // Write new data to the virtual file at compile time 39 | virtualModules.writeModule(swaggerJsonPath, JSON.stringify(swaggerJson)); 40 | } catch (e) { 41 | compilation.errors.push(e); 42 | } 43 | }); 44 | }; 45 | 46 | module.exports = { 47 | entry: './index.js', 48 | plugins: [new SwaggerPlugin(), 49 | 50 | new webpack.ProvidePlugin({ 51 | Buffer: ['buffer', 'Buffer'], 52 | }) 53 | ], 54 | resolve:{ 55 | alias: { 56 | buffer: 'buffer', 57 | }, 58 | }, 59 | module: { 60 | rules: [ 61 | { 62 | test: /\.css$/, 63 | use: ['style-loader', 'css-loader'] 64 | } 65 | ] 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | testMatch: ['/src/**/*.(test|spec).ts'], 4 | testPathIgnorePatterns: ['/lib/'], 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-virtual-modules", 3 | "version": "0.6.2", 4 | "description": "Webpack Virtual Modules", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "clean": "rm -rf ./lib", 8 | "build": "tsc -p tsconfig.build.json", 9 | "watch": "tsc -p tsconfig.build.json -w", 10 | "tests": "jest", 11 | "tests:watch": "jest --watch", 12 | "test": "nari lint && nari tests", 13 | "lint": "eslint --fix src/**/*.ts", 14 | "prepack": "nari clean && nari build" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/sysgears/webpack-virtual-modules.git" 19 | }, 20 | "keywords": [ 21 | "webpack", 22 | "webpack-plugin", 23 | "virtual", 24 | "modules" 25 | ], 26 | "author": "SysGears INC", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/sysgears/webpack-virtual-modules/issues" 30 | }, 31 | "homepage": "https://github.com/sysgears/webpack-virtual-modules#readme", 32 | "devDependencies": { 33 | "@babel/core": "^7.4.5", 34 | "@babel/plugin-proposal-class-properties": "^7.4.4", 35 | "@babel/plugin-transform-modules-commonjs": "^7.4.4", 36 | "@babel/preset-typescript": "^7.3.3", 37 | "@babel/register": "^7.5.5", 38 | "@types/jest": "^24.0.6", 39 | "@types/node": "^22.7.4", 40 | "@types/tmp": "^0.1.0", 41 | "@types/webpack": "^5.28.5", 42 | "@typescript-eslint/eslint-plugin": "^5.26.0", 43 | "@typescript-eslint/parser": "^5.26.0", 44 | "babel-jest": "^29.0.3", 45 | "babel-plugin-replace-ts-export-assignment": "^0.0.2", 46 | "eslint": "^8.23.1", 47 | "eslint-config-prettier": "^8.5.0", 48 | "eslint-plugin-jest": "^27.0.4", 49 | "eslint-plugin-prettier": "^4.2.1", 50 | "husky": "^8.0.1", 51 | "jest": "^29.0.3", 52 | "lint-staged": "^13.0.3", 53 | "memory-fs": "^0.5.0", 54 | "prettier": "^2.7.1", 55 | "tmp": "^0.2.1", 56 | "typescript": "^4.8.3", 57 | "webpack": "^5.95.0" 58 | }, 59 | "files": [ 60 | "lib", 61 | "src", 62 | "!__tests__" 63 | ], 64 | "publishConfig": { 65 | "main": "lib/index.js", 66 | "types": "lib/index.d.ts" 67 | }, 68 | "lint-staged": { 69 | "*.ts": [ 70 | "eslint --fix -c tslint.json", 71 | "git add" 72 | ] 73 | }, 74 | "prettier": { 75 | "printWidth": 120, 76 | "singleQuote": true, 77 | "parser": "typescript" 78 | }, 79 | "husky": { 80 | "pre-commit": "lint-staged" 81 | }, 82 | "lockTime": "2024-10-02T08:19:16.905Z" 83 | } -------------------------------------------------------------------------------- /src/__tests__/count-loader.js: -------------------------------------------------------------------------------- 1 | let modulesBuilt = 0; 2 | 3 | module.exports = function(source) { 4 | this.cacheable(true); 5 | const callback = this.async(); 6 | modulesBuilt++; 7 | callback(null, source); 8 | } 9 | 10 | module.exports.loader = __filename; 11 | module.exports.modulesBuilt = function() { 12 | return modulesBuilt; 13 | }; 14 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import MemoryFileSystem from 'memory-fs'; 2 | import webpack from 'webpack'; 3 | import path from 'path'; 4 | import Plugin from '../index'; 5 | 6 | describe('webpack-virtual-modules', () => { 7 | it('should fail if not applied as plugin', () => { 8 | const plugin = new Plugin(); 9 | 10 | expect(() => plugin.writeModule('example.js', '')).toThrow(); 11 | }); 12 | 13 | it('should NOT fail if applied as plugin', () => { 14 | const plugin = new Plugin(); 15 | 16 | webpack({ 17 | plugins: [plugin], 18 | entry: './entry.js', 19 | }); 20 | 21 | expect(() => plugin.writeModule('example.js', '')).not.toThrow(); 22 | }); 23 | 24 | it('should return static modules', async () => { 25 | const options = { 'static_module1.js': 'const foo;' }; 26 | options[path.resolve('static_module2.js')] = 'const bar;'; 27 | const plugin = new Plugin(options); 28 | 29 | expect(plugin.getModuleList()).toMatchObject(options); 30 | }); 31 | 32 | it('should return dynamic and static modules', async () => { 33 | const options = { 'static_module.js': 'const foo;' }; 34 | const plugin = new Plugin(options); 35 | 36 | const compiler = webpack({ 37 | plugins: [plugin], 38 | entry: './entry.js', 39 | }); 40 | 41 | return new Promise((resolve) => { 42 | const watcher = compiler.watch({}, (err, stats) => { 43 | if (!stats) throw err; 44 | 45 | plugin.writeModule('dynamic_module.js', 'const baz;'); 46 | 47 | const fs = stats.compilation.inputFileSystem as MemoryFileSystem; 48 | fs.purge(); 49 | 50 | const finalOptions = { 51 | [path.resolve('static_module.js')]: 'const foo;', 52 | [path.resolve('dynamic_module.js')]: 'const baz;', 53 | }; 54 | 55 | expect(plugin.getModuleList()).toMatchObject(finalOptions); 56 | 57 | watcher.close(resolve); 58 | }); 59 | }); 60 | }); 61 | 62 | it('should write static modules to fs', async () => { 63 | const options = { 'static_module1.js': 'const foo;' }; 64 | options[path.resolve('static_module2.js')] = 'const bar;'; 65 | const plugin = new Plugin(options); 66 | 67 | return new Promise((resolve) => { 68 | webpack({ 69 | plugins: [plugin], 70 | entry: './entry.js', 71 | }).run(async (err, stats) => { 72 | if (!stats) throw err; 73 | 74 | const fs = stats.compilation.inputFileSystem as MemoryFileSystem; 75 | expect(fs.readFileSync(path.resolve('static_module1.js')).toString()).toEqual('const foo;'); 76 | expect(fs.readFileSync(path.resolve('static_module2.js')).toString()).toEqual('const bar;'); 77 | resolve(); 78 | }); 79 | }); 80 | }); 81 | 82 | it('static modules should survive fs purge', async () => { 83 | const plugin = new Plugin({ 84 | 'static_module.js': 'const foo;', 85 | }); 86 | 87 | return new Promise((resolve) => { 88 | webpack({ 89 | plugins: [plugin], 90 | entry: './entry.js', 91 | }).run((err, stats) => { 92 | if (!stats) throw err; 93 | 94 | const fs = stats.compilation.inputFileSystem as MemoryFileSystem; 95 | fs.purge(); 96 | expect(fs.readFileSync(path.resolve('static_module.js')).toString()).toEqual('const foo;'); 97 | resolve(); 98 | }); 99 | }); 100 | }); 101 | 102 | it('purge should work when no virtual files exist', async () => { 103 | const plugin = new Plugin(); 104 | 105 | return new Promise((resolve) => { 106 | webpack({ 107 | plugins: [plugin], 108 | entry: './entry.js', 109 | }).run((err, stats) => { 110 | if (!stats) throw err; 111 | 112 | expect(() => { 113 | const fs = stats.compilation.inputFileSystem as MemoryFileSystem; 114 | fs.purge(); 115 | }).not.toThrow(); 116 | 117 | resolve(); 118 | }); 119 | }); 120 | }); 121 | 122 | it('two instances of plugin should have no conflicts', async () => { 123 | const plugin1 = new Plugin({ 'static_module1.js': 'const foo;' }); 124 | const plugin2 = new Plugin({ 'static_module2.js': 'const bar;' }); 125 | 126 | const compiler = webpack({ 127 | plugins: [plugin1, plugin2], 128 | entry: './entry.js', 129 | }); 130 | 131 | return new Promise((resolve) => { 132 | compiler.run((err, stats) => { 133 | if (!stats) throw err; 134 | 135 | const fs = stats.compilation.inputFileSystem as MemoryFileSystem; 136 | expect(fs.readFileSync(path.resolve('static_module1.js')).toString()).toEqual('const foo;'); 137 | expect(fs.readFileSync(path.resolve('static_module2.js')).toString()).toEqual('const bar;'); 138 | 139 | resolve(); 140 | }); 141 | }); 142 | }); 143 | 144 | it('should write dynamic modules to fs', async () => { 145 | const plugin = new Plugin(); 146 | 147 | const compiler = webpack({ 148 | plugins: [plugin], 149 | entry: './entry.js', 150 | }); 151 | 152 | return new Promise((resolve) => { 153 | const watcher = compiler.watch({}, (err, stats) => { 154 | if (!stats) throw err; 155 | 156 | plugin.writeModule('dynamic_module.js', 'const baz;'); 157 | const fs = stats.compilation.inputFileSystem as MemoryFileSystem; 158 | fs.purge(); 159 | expect(fs.readFileSync(path.resolve('dynamic_module.js')).toString()).toEqual('const baz;'); 160 | watcher.close(resolve); 161 | }); 162 | }); 163 | }); 164 | 165 | it('should invalidate bundle on dynamic module write', async () => { 166 | const plugin = new Plugin({ 167 | 'entry.js': 'require("./dynamic_module.js");', 168 | 'dynamic_module.js': '', 169 | }); 170 | const compiler = webpack({ 171 | plugins: [plugin], 172 | entry: { bundle: './entry.js' }, 173 | }); 174 | compiler.outputFileSystem = new MemoryFileSystem(); 175 | let count = 0; 176 | 177 | const waiter = (callback) => { 178 | const fileWatchers = (compiler.watchFileSystem as any).watcher.fileWatchers; 179 | if (fileWatchers instanceof Map) { 180 | // Webpack v5 is a map 181 | if (!Array.from(fileWatchers.keys()).length) { 182 | setTimeout(function () { 183 | waiter(callback); 184 | }, 50); 185 | } else { 186 | callback(); 187 | } 188 | } else if (!Object.keys(fileWatchers).length) { 189 | setTimeout(function () { 190 | waiter(callback); 191 | }, 50); 192 | } else { 193 | callback(); 194 | } 195 | }; 196 | 197 | return new Promise((resolve) => { 198 | const watcher = compiler.watch({}, (err, stats) => { 199 | if (count === 0) { 200 | waiter(() => { 201 | if (!stats) throw err; 202 | 203 | plugin.writeModule('dynamic_module.js', 'const baz;'); 204 | const fs = stats.compilation.inputFileSystem as MemoryFileSystem; 205 | fs.purge(); 206 | 207 | // eslint-disable-next-line jest/no-conditional-expect 208 | expect(fs.readFileSync(path.resolve('dynamic_module.js')).toString()).toEqual('const baz;'); 209 | count++; 210 | }); 211 | } else { 212 | watcher.close(resolve); 213 | } 214 | }); 215 | }); 216 | }); 217 | 218 | it('should work with path which parent dir not exists', async () => { 219 | const plugin = new Plugin({ 220 | 'entry.js': 'const a = require("a").default; const b = require("b").default; export default a + b;', 221 | 'node_modules/a.js': 'export default 1;', 222 | 'node_modules/b.js': 'export default 2;', 223 | }); 224 | const compiler = webpack({ 225 | context: __dirname, 226 | plugins: [plugin], 227 | entry: './entry.js', 228 | output: { 229 | path: path.resolve(__dirname), 230 | filename: 'bundle.js', 231 | library: 'test', 232 | libraryTarget: 'umd', 233 | }, 234 | target: 'node', 235 | }); 236 | const fileSystem = new MemoryFileSystem(); 237 | compiler.outputFileSystem = fileSystem; 238 | 239 | return new Promise((resolve) => { 240 | compiler.run((err, stats) => { 241 | expect(stats).toBeDefined(); 242 | expect(err).toBeNull(); 243 | if (!stats) throw err; 244 | expect(stats.hasErrors()).toBeFalsy(); 245 | expect(stats.toJson().errors).toHaveLength(0); 246 | const outputPath = path.resolve(__dirname, 'bundle.js'); 247 | const outputFile = fileSystem.readFileSync(outputPath).toString(); 248 | const output = eval(outputFile + ' module.exports;').default; 249 | expect(output).toEqual(3); 250 | const fs = compiler.inputFileSystem as MemoryFileSystem; 251 | const rootEntries = fs.readdirSync(__dirname); 252 | expect(rootEntries).toContain('entry.js'); 253 | expect(rootEntries).toContain('node_modules'); 254 | const nmEntries = fs.readdirSync(path.join(__dirname, 'node_modules')); 255 | expect(nmEntries).toContain('a.js'); 256 | expect(nmEntries).toContain('b.js'); 257 | 258 | resolve(); 259 | }); 260 | }); 261 | }); 262 | 263 | const version = (webpack.version && parseInt(webpack.version.split('.')[0])) || 0; 264 | 265 | (version >= 4 ? it : it.skip)('should NOT rebuild all virtual modules on any change', async () => { 266 | const countLoader = require('./count-loader'); 267 | const plugin = new Plugin({ 268 | 'entry.js': 'require("./dep_one.js"); require("./dep_two.js");', 269 | 'dep_one.js': '', 270 | 'dep_two.js': '', 271 | }); 272 | const config = { 273 | plugins: [plugin], 274 | entry: { bundle: './entry.js' }, 275 | module: { 276 | rules: [ 277 | { 278 | test: /\.js$/, 279 | use: countLoader.loader, 280 | }, 281 | ], 282 | }, 283 | }; 284 | // webpack 3 doesn't have version field on its constructor and doesn't 285 | // support mode: development 286 | // left for future testing and possibility of enabling test for it 287 | if (webpack.version && typeof webpack.version === 'string') { 288 | // @ts-ignore TODO: It's enough? 289 | config.mode = 'development'; 290 | } 291 | const compiler = webpack(config); 292 | compiler.outputFileSystem = new MemoryFileSystem(); 293 | let count = 0; 294 | 295 | const waiter = (callback) => { 296 | const fileWatchers = (compiler.watchFileSystem as any).watcher.fileWatchers; 297 | if (fileWatchers instanceof Map) { 298 | // Webpack v5 is a map 299 | if (!Array.from(fileWatchers.keys()).length) { 300 | setTimeout(function () { 301 | waiter(callback); 302 | }, 50); 303 | } else { 304 | callback(); 305 | } 306 | } else if (!Object.keys(fileWatchers).length) { 307 | setTimeout(function () { 308 | waiter(callback); 309 | }, 50); 310 | } else { 311 | callback(); 312 | } 313 | }; 314 | 315 | return new Promise((resolve) => { 316 | const watcher = compiler.watch({}, (err, stats) => { 317 | if (count === 0) { 318 | waiter(() => { 319 | if (!stats) throw err; 320 | 321 | plugin.writeModule('dep_one.js', 'const baz;'); 322 | const fs = stats.compilation.inputFileSystem as MemoryFileSystem; 323 | fs.purge(); 324 | 325 | // eslint-disable-next-line jest/no-standalone-expect,jest/no-conditional-expect 326 | expect(fs.readFileSync(path.resolve('dep_one.js')).toString()).toEqual('const baz;'); 327 | count++; 328 | }); 329 | } else { 330 | watcher.close(resolve); 331 | 332 | // eslint-disable-next-line jest/no-standalone-expect,jest/no-conditional-expect 333 | expect(countLoader.modulesBuilt()).toEqual(4); 334 | } 335 | }); 336 | }); 337 | }); 338 | }); 339 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { VirtualStats } from './virtual-stats'; 3 | import type { Compiler } from 'webpack'; 4 | 5 | let inode = 45000000; 6 | const ALL = 'all'; 7 | const STATIC = 'static'; 8 | const DYNAMIC = 'dynamic'; 9 | 10 | type AvailableModules = typeof ALL | typeof STATIC | typeof DYNAMIC; 11 | 12 | function checkActivation(instance) { 13 | if (!instance._compiler) { 14 | throw new Error('You must use this plugin only after creating webpack instance!'); 15 | } 16 | } 17 | 18 | function getModulePath(filePath, compiler) { 19 | return path.isAbsolute(filePath) ? filePath : path.join(compiler.context, filePath); 20 | } 21 | 22 | function createWebpackData(result) { 23 | return (backendOrStorage) => { 24 | // In Webpack v5, this variable is a "Backend", and has the data stored in a field 25 | // _data. In V4, the `_` prefix isn't present. 26 | if (backendOrStorage._data) { 27 | const curLevelIdx = backendOrStorage._currentLevel; 28 | const curLevel = backendOrStorage._levels[curLevelIdx]; 29 | return { 30 | result, 31 | level: curLevel, 32 | }; 33 | } 34 | // Webpack 4 35 | return [null, result]; 36 | }; 37 | } 38 | 39 | function getData(storage, key) { 40 | // Webpack 5 41 | if (storage._data instanceof Map) { 42 | return storage._data.get(key); 43 | } else if (storage._data) { 44 | return storage.data[key]; 45 | } else if (storage.data instanceof Map) { 46 | // Webpack v4 47 | return storage.data.get(key); 48 | } else { 49 | return storage.data[key]; 50 | } 51 | } 52 | 53 | function setData(backendOrStorage, key, valueFactory) { 54 | const value = valueFactory(backendOrStorage); 55 | 56 | // Webpack v5 57 | if (backendOrStorage._data instanceof Map) { 58 | backendOrStorage._data.set(key, value); 59 | } else if (backendOrStorage._data) { 60 | backendOrStorage.data[key] = value; 61 | } else if (backendOrStorage.data instanceof Map) { 62 | // Webpack 4 63 | backendOrStorage.data.set(key, value); 64 | } else { 65 | backendOrStorage.data[key] = value; 66 | } 67 | } 68 | 69 | function getStatStorage(fileSystem) { 70 | if (fileSystem._statStorage) { 71 | // Webpack v4 72 | return fileSystem._statStorage; 73 | } else if (fileSystem._statBackend) { 74 | // webpack v5 75 | return fileSystem._statBackend; 76 | } else { 77 | // Unknown version? 78 | throw new Error("Couldn't find a stat storage"); 79 | } 80 | } 81 | 82 | function getFileStorage(fileSystem) { 83 | if (fileSystem._readFileStorage) { 84 | // Webpack v4 85 | return fileSystem._readFileStorage; 86 | } else if (fileSystem._readFileBackend) { 87 | // Webpack v5 88 | return fileSystem._readFileBackend; 89 | } else { 90 | throw new Error("Couldn't find a readFileStorage"); 91 | } 92 | } 93 | 94 | function getReadDirBackend(fileSystem) { 95 | if (fileSystem._readdirBackend) { 96 | return fileSystem._readdirBackend; 97 | } else if (fileSystem._readdirStorage) { 98 | return fileSystem._readdirStorage; 99 | } else { 100 | throw new Error("Couldn't find a readDirStorage from Webpack Internals"); 101 | } 102 | } 103 | 104 | function getRealpathBackend(fileSystem) { 105 | if (fileSystem._realpathBackend) { 106 | return fileSystem._realpathBackend; 107 | } 108 | 109 | // Nothing, because not all version of webpack support it 110 | } 111 | 112 | class VirtualModulesPlugin { 113 | private _staticModules: Record | null; 114 | private _compiler: Compiler | null = null; 115 | private _watcher: any = null; 116 | 117 | public constructor(modules?: Record) { 118 | this._staticModules = modules || null; 119 | } 120 | 121 | public getModuleList(filter: AvailableModules = ALL) { 122 | let modules = {}; 123 | const shouldGetStaticModules = filter === ALL || filter === STATIC; 124 | const shouldGetDynamicModules = filter === ALL || filter === DYNAMIC; 125 | 126 | if (shouldGetStaticModules) { 127 | // Get static modules 128 | modules = { 129 | ...modules, 130 | ...this._staticModules, 131 | }; 132 | } 133 | 134 | if (shouldGetDynamicModules) { 135 | // Get dynamic modules 136 | const finalInputFileSystem: any = this._compiler?.inputFileSystem; 137 | const virtualFiles = finalInputFileSystem?._virtualFiles ?? {}; 138 | 139 | const dynamicModules: Record = {}; 140 | Object.keys(virtualFiles).forEach((key: string) => { 141 | dynamicModules[key] = virtualFiles[key].contents; 142 | }); 143 | 144 | modules = { 145 | ...modules, 146 | ...dynamicModules, 147 | }; 148 | } 149 | 150 | return modules; 151 | } 152 | 153 | public writeModule(filePath: string, contents: string): void { 154 | if (!this._compiler) { 155 | throw new Error(`Plugin has not been initialized`); 156 | } 157 | 158 | checkActivation(this); 159 | 160 | const len = contents ? contents.length : 0; 161 | const time = Date.now(); 162 | const date = new Date(time); 163 | 164 | const stats = new VirtualStats({ 165 | dev: 8675309, 166 | nlink: 0, 167 | uid: 1000, 168 | gid: 1000, 169 | rdev: 0, 170 | blksize: 4096, 171 | ino: inode++, 172 | mode: 33188, 173 | size: len, 174 | blocks: Math.floor(len / 4096), 175 | atime: date, 176 | mtime: date, 177 | ctime: date, 178 | birthtime: date, 179 | }); 180 | const modulePath = getModulePath(filePath, this._compiler); 181 | 182 | if (process.env.WVM_DEBUG) 183 | // eslint-disable-next-line no-console 184 | console.log(this._compiler.name, 'Write virtual module:', modulePath, contents); 185 | 186 | // When using the WatchIgnorePlugin (https://github.com/webpack/webpack/blob/52184b897f40c75560b3630e43ca642fcac7e2cf/lib/WatchIgnorePlugin.js), 187 | // the original watchFileSystem is stored in `wfs`. The following "unwraps" the ignoring 188 | // wrappers, giving us access to the "real" watchFileSystem. 189 | let finalWatchFileSystem = this._watcher && this._watcher.watchFileSystem; 190 | 191 | while (finalWatchFileSystem && finalWatchFileSystem.wfs) { 192 | finalWatchFileSystem = finalWatchFileSystem.wfs; 193 | } 194 | 195 | let finalInputFileSystem: any = this._compiler.inputFileSystem; 196 | while (finalInputFileSystem && finalInputFileSystem._inputFileSystem) { 197 | finalInputFileSystem = finalInputFileSystem._inputFileSystem; 198 | } 199 | 200 | finalInputFileSystem._writeVirtualFile(modulePath, stats, contents); 201 | if ( 202 | finalWatchFileSystem && 203 | finalWatchFileSystem.watcher && 204 | (finalWatchFileSystem.watcher.fileWatchers.size || finalWatchFileSystem.watcher.fileWatchers.length) 205 | ) { 206 | const fileWatchers = 207 | finalWatchFileSystem.watcher.fileWatchers instanceof Map 208 | ? Array.from(finalWatchFileSystem.watcher.fileWatchers.values()) 209 | : finalWatchFileSystem.watcher.fileWatchers; 210 | for (let fileWatcher of fileWatchers) { 211 | if ('watcher' in fileWatcher) { 212 | fileWatcher = fileWatcher.watcher; 213 | } 214 | if (fileWatcher.path === modulePath) { 215 | if (process.env.DEBUG) 216 | // eslint-disable-next-line no-console 217 | console.log(this._compiler.name, 'Emit file change:', modulePath, time); 218 | delete fileWatcher.directoryWatcher._cachedTimeInfoEntries; 219 | fileWatcher.emit('change', time, null); 220 | } 221 | } 222 | } 223 | } 224 | 225 | public apply(compiler: Compiler) { 226 | this._compiler = compiler; 227 | 228 | const afterEnvironmentHook = () => { 229 | let finalInputFileSystem: any = compiler.inputFileSystem; 230 | while (finalInputFileSystem && finalInputFileSystem._inputFileSystem) { 231 | finalInputFileSystem = finalInputFileSystem._inputFileSystem; 232 | } 233 | 234 | if (!finalInputFileSystem._writeVirtualFile) { 235 | const originalPurge = finalInputFileSystem.purge; 236 | 237 | finalInputFileSystem.purge = () => { 238 | originalPurge.apply(finalInputFileSystem, []); 239 | if (finalInputFileSystem._virtualFiles) { 240 | Object.keys(finalInputFileSystem._virtualFiles).forEach((file) => { 241 | const data = finalInputFileSystem._virtualFiles[file]; 242 | finalInputFileSystem._writeVirtualFile(file, data.stats, data.contents); 243 | }); 244 | } 245 | }; 246 | 247 | finalInputFileSystem._writeVirtualFile = (file, stats, contents) => { 248 | const statStorage = getStatStorage(finalInputFileSystem); 249 | const fileStorage = getFileStorage(finalInputFileSystem); 250 | const readDirStorage = getReadDirBackend(finalInputFileSystem); 251 | const realPathStorage = getRealpathBackend(finalInputFileSystem); 252 | 253 | finalInputFileSystem._virtualFiles = finalInputFileSystem._virtualFiles || {}; 254 | finalInputFileSystem._virtualFiles[file] = { stats: stats, contents: contents }; 255 | setData(statStorage, file, createWebpackData(stats)); 256 | setData(fileStorage, file, createWebpackData(contents)); 257 | const segments = file.split(/[\\/]/); 258 | let count = segments.length - 1; 259 | const minCount = segments[0] ? 1 : 0; 260 | while (count > minCount) { 261 | const dir = segments.slice(0, count).join(path.sep) || path.sep; 262 | try { 263 | finalInputFileSystem.readdirSync(dir); 264 | } catch (e) { 265 | const time = Date.now(); 266 | const dirStats = new VirtualStats({ 267 | dev: 8675309, 268 | nlink: 0, 269 | uid: 1000, 270 | gid: 1000, 271 | rdev: 0, 272 | blksize: 4096, 273 | ino: inode++, 274 | mode: 16877, 275 | size: stats.size, 276 | blocks: Math.floor(stats.size / 4096), 277 | atime: time, 278 | mtime: time, 279 | ctime: time, 280 | birthtime: time, 281 | }); 282 | 283 | setData(readDirStorage, dir, createWebpackData([])); 284 | if (realPathStorage) { 285 | setData(realPathStorage, dir, createWebpackData(dir)); 286 | } 287 | setData(statStorage, dir, createWebpackData(dirStats)); 288 | } 289 | let dirData = getData(getReadDirBackend(finalInputFileSystem), dir); 290 | // Webpack v4 returns an array, webpack v5 returns an object 291 | dirData = dirData[1] || dirData.result; 292 | const filename = segments[count]; 293 | if (dirData.indexOf(filename) < 0) { 294 | const files = dirData.concat([filename]).sort(); 295 | setData(getReadDirBackend(finalInputFileSystem), dir, createWebpackData(files)); 296 | } else { 297 | break; 298 | } 299 | count--; 300 | } 301 | }; 302 | } 303 | }; 304 | const afterResolversHook = () => { 305 | if (this._staticModules) { 306 | for (const [filePath, contents] of Object.entries(this._staticModules)) { 307 | this.writeModule(filePath, contents); 308 | } 309 | this._staticModules = null; 310 | } 311 | }; 312 | 313 | // The webpack property is not exposed in webpack v4 314 | const version = typeof (compiler as any).webpack === 'undefined' ? 4 : 5; 315 | 316 | const watchRunHook = (watcher, callback) => { 317 | this._watcher = watcher.compiler || watcher; 318 | const virtualFiles = (compiler as any).inputFileSystem._virtualFiles; 319 | const fts = compiler.fileTimestamps as any; 320 | 321 | if (virtualFiles && fts && typeof fts.set === 'function') { 322 | Object.keys(virtualFiles).forEach((file) => { 323 | const mtime = +virtualFiles[file].stats.mtime; 324 | // fts is 325 | // Map in webpack 4 326 | // Map in webpack 5 327 | fts.set( 328 | file, 329 | version === 4 330 | ? mtime 331 | : { 332 | safeTime: mtime, 333 | timestamp: mtime, 334 | } 335 | ); 336 | }); 337 | } 338 | callback(); 339 | }; 340 | 341 | if (compiler.hooks) { 342 | compiler.hooks.afterEnvironment.tap('VirtualModulesPlugin', afterEnvironmentHook); 343 | compiler.hooks.afterResolvers.tap('VirtualModulesPlugin', afterResolversHook); 344 | compiler.hooks.watchRun.tapAsync('VirtualModulesPlugin', watchRunHook); 345 | } else { 346 | (compiler as any).plugin('after-environment', afterEnvironmentHook); 347 | (compiler as any).plugin('after-resolvers', afterResolversHook); 348 | (compiler as any).plugin('watch-run', watchRunHook); 349 | } 350 | } 351 | } 352 | 353 | export = VirtualModulesPlugin; 354 | -------------------------------------------------------------------------------- /src/virtual-stats.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used to cache a stats object for the virtual file. 3 | * Extracted from the `mock-fs` package. 4 | * 5 | * @author Tim Schaub http://tschaub.net/ 6 | * @author `webpack-virtual-modules` Contributors 7 | * @link https://github.com/tschaub/mock-fs/blob/master/lib/binding.js 8 | * @link https://github.com/tschaub/mock-fs/blob/master/license.md 9 | */ 10 | import constants from 'constants'; 11 | 12 | export class VirtualStats { 13 | /** 14 | * Create a new stats object. 15 | * 16 | * @param config Stats properties. 17 | */ 18 | public constructor(config) { 19 | for (const key in config) { 20 | if (!Object.prototype.hasOwnProperty.call(config, key)) { 21 | continue; 22 | } 23 | this[key] = config[key]; 24 | } 25 | } 26 | 27 | /** 28 | * Check if mode indicates property. 29 | */ 30 | private _checkModeProperty(property): boolean { 31 | return ((this as any).mode & constants.S_IFMT) === property; 32 | } 33 | 34 | public isDirectory(): boolean { 35 | return this._checkModeProperty(constants.S_IFDIR); 36 | } 37 | 38 | public isFile(): boolean { 39 | return this._checkModeProperty(constants.S_IFREG); 40 | } 41 | 42 | public isBlockDevice(): boolean { 43 | return this._checkModeProperty(constants.S_IFBLK); 44 | } 45 | 46 | public isCharacterDevice(): boolean { 47 | return this._checkModeProperty(constants.S_IFCHR); 48 | } 49 | 50 | public isSymbolicLink(): boolean { 51 | return this._checkModeProperty(constants.S_IFLNK); 52 | } 53 | 54 | public isFIFO(): boolean { 55 | return this._checkModeProperty(constants.S_IFIFO); 56 | } 57 | 58 | public isSocket(): boolean { 59 | return this._checkModeProperty(constants.S_IFSOCK); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["src/**/__tests__/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "target": "es2017", 5 | "esModuleInterop": true, 6 | "emitDecoratorMetadata": true, 7 | "module": "commonjs", 8 | "lib": ["es2016"], 9 | "moduleResolution": "node", 10 | "sourceMap": true, 11 | "declaration": true, 12 | "noImplicitAny": false, 13 | "experimentalDecorators": true, 14 | "pretty": true, 15 | "strict": true, 16 | "removeComments": true 17 | }, 18 | "exclude": [ 19 | "lib" 20 | ], 21 | "include": [ 22 | "src/**/*", 23 | "types/**/*" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------