├── .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 |
--------------------------------------------------------------------------------