From 574b5f64ed0c8c608759fd334bb4a068f342d82d Mon Sep 17 00:00:00 2001 From: Evilebot Tnawi Date: Wed, 17 Jun 2020 18:20:45 +0300 Subject: [PATCH] refactor: `wrapper` option (#78) --- README.md | 116 +++++++++++++++--- src/index.js | 24 +++- src/options.json | 26 +++- test/__snapshots__/loader.test.js.snap | 33 +++-- .../validate-options.test.js.snap | 57 +++++++-- test/loader.test.js | 22 +++- test/validate-options.test.js | 19 ++- 7 files changed, 245 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 7b14593..bb5174c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,10 @@ The imports loader allows you to use modules that depend on specific global variables. -This is useful for third-party modules that rely on global variables like `$` or `this` being the `window` object. The imports loader can add the necessary `require('whatever')` calls, so those modules work with webpack. +This is useful for third-party modules that rely on global variables like `$` or `this` being the `window` object. +The imports loader can add the necessary `require('whatever')` calls, so those modules work with webpack. + +For further hints on compatibility issues, check out [Shimming](https://webpack.js.org/guides/shimming/) of the official docs. > ⚠ By default loader generate ES module named syntax. @@ -39,7 +42,7 @@ Given you have this file: $('img').doSomeAwesomeJqueryPluginStuff(); ``` -then you can inject the `jquery` value into the module by configuring the `imports-loader` using two approaches. +Then you can inject the `jquery` value into the module by configuring the `imports-loader` using two approaches. ### Inline @@ -82,7 +85,9 @@ import( // import angular from "angular"; // // (function () { -// code from example.js +// ... +// Code +// ... // }.call(window)); ``` @@ -139,10 +144,12 @@ And run `webpack` via your preferred method. ## Options -| Name | Type | Default | Description | -| :-----------------------: | :---------------------------------------: | :---------: | :-------------------------- | -| **[`type`](#type)** | `{String}` | `module` | Format of generated imports | -| **[`imports`](#imports)** | `{String\|Object\|Array}` | `undefined` | List of imports | +| Name | Type | Default | Description | +| :-------------------------------------: | :---------------------------------------: | :---------: | :--------------------------------------------------------------------- | +| **[`type`](#type)** | `{String}` | `module` | Format of generated imports | +| **[`imports`](#imports)** | `{String\|Object\|Array}` | `undefined` | List of imports | +| **[`wrapper`](#wrapper)** | `{Boolean\|String\|Object}` | `undefined` | Closes the module code in a function (`(function () { ... }).call();`) | +| **[`additionalCode`](#additionalcode)** | `{String}` | `undefined` | Adds custom code | ### `type` @@ -443,12 +450,16 @@ import 'lib_4'; // ... ``` -### wrapper +### `wrapper` -Type: `String|Array` +Type: `Boolean|String|Object` Default: `undefined` -Closes the module code in a function with a given `this` (`(function () { ... }).call(window);`). +Closes the module code in a function with a given `thisArg` and `args` (`(function () { ... }).call();`). + +> ⚠ Do not use this option if source code contains ES module import(s) + +#### `Boolean` ```js module.exports = { @@ -464,7 +475,7 @@ module.exports = { moduleName: 'jquery', name: '$', }, - wrapper: ['window', 'document'], + wrapper: true, }, }, ], @@ -483,12 +494,89 @@ import $ from 'jquery'; // ... // Code // ... -}.call(window, document)); +}.call()); ``` -> ⚠ Do not use this option if source code contains ES module import(s) +#### `String` + +```js +module.exports = { + module: { + rules: [ + { + test: require.resolve('example.js'), + use: [ + { + loader: 'imports-loader', + options: { + imports: { + moduleName: 'jquery', + name: '$', + }, + wrapper: 'window', + }, + }, + ], + }, + ], + }, +}; +``` + +Generate output: + +```js +import $ from 'jquery'; + +(function () { + // ... + // Code + // ... +}.call(window)); +``` + +#### `Object` + +```js +module.exports = { + module: { + rules: [ + { + test: require.resolve('example.js'), + use: [ + { + loader: 'imports-loader', + options: { + imports: { + moduleName: 'jquery', + name: '$', + }, + wrapper: { + thisArg: 'window', + args: ['myVariable', 'myOtherVariable'], + }, + }, + }, + ], + }, + ], + }, +}; +``` + +Generate output: + +```js +import $ from 'jquery'; + +(function (myVariable, myOtherVariable) { + // ... + // Code + // ... +}.call(window, myVariable, myOtherVariable)); +``` -### additionalCode +### `additionalCode` Type: `String` Default: `undefined` diff --git a/src/index.js b/src/index.js index 7899a91..7a4959b 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,7 @@ */ import { SourceNode, SourceMapConsumer } from 'source-map'; -import { getOptions, getCurrentRequest } from 'loader-utils'; +import { getOptions } from 'loader-utils'; import validateOptions from 'schema-utils'; import schema from './options.json'; @@ -54,8 +54,22 @@ export default function loader(content, sourceMap) { let codeAfterModule = ''; if (typeof options.wrapper !== 'undefined') { - importsCode += '\n(function() {'; - codeAfterModule += `\n}.call(${options.wrapper.toString()}));\n`; + let thisArg; + let args; + + if (typeof options.wrapper === 'boolean') { + thisArg = ''; + args = ''; + } else if (typeof options.wrapper === 'string') { + thisArg = options.wrapper; + args = ''; + } else { + ({ thisArg, args } = options.wrapper); + args = args.join(', '); + } + + importsCode += `\n(function(${args}) {`; + codeAfterModule += `\n}.call(${thisArg}${args ? `, ${args}` : ''}));\n`; } if (this.sourceMap && sourceMap) { @@ -67,9 +81,7 @@ export default function loader(content, sourceMap) { node.prepend(`${importsCode}\n`); node.add(codeAfterModule); - const result = node.toStringWithSourceMap({ - file: getCurrentRequest(this), - }); + const result = node.toStringWithSourceMap({ file: this.resourcePath }); callback(null, result.code, result.map.toJSON()); diff --git a/src/options.json b/src/options.json index c695718..77f9729 100644 --- a/src/options.json +++ b/src/options.json @@ -66,17 +66,31 @@ }, "wrapper": { "anyOf": [ + { + "type": "boolean" + }, { "type": "string", "minLength": 1 }, { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "minLength": 1 - } + "type": "object", + "additionalProperties": false, + "properties": { + "thisArg": { + "type": "string", + "minLength": 1 + }, + "args": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["thisArg"] } ] }, diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index 8663806..7f8a52d 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -1103,9 +1103,9 @@ var someCode = { exports[`loader should work with the "additionalCode" option: warnings 1`] = `Array []`; -exports[`loader should work with the "wrapper" option: errors 1`] = `Array []`; +exports[`loader should work with the "wrapper" option as a boolean notation: errors 1`] = `Array []`; -exports[`loader should work with the "wrapper" option: module 1`] = ` +exports[`loader should work with the "wrapper" option as a boolean notation: module 1`] = ` "/*** IMPORTS FROM imports-loader ***/ (function() { @@ -1114,15 +1114,15 @@ var someCode = { object: { existingSubProperty: 123 } }; -}.call(window)); +}.call()); " `; -exports[`loader should work with the "wrapper" option: warnings 1`] = `Array []`; +exports[`loader should work with the "wrapper" option as a boolean notation: warnings 1`] = `Array []`; -exports[`loader should work with the "wrapper" options and arguments: errors 1`] = `Array []`; +exports[`loader should work with the "wrapper" option as a string notation: errors 1`] = `Array []`; -exports[`loader should work with the "wrapper" options and arguments: module 1`] = ` +exports[`loader should work with the "wrapper" option as a string notation: module 1`] = ` "/*** IMPORTS FROM imports-loader ***/ (function() { @@ -1131,8 +1131,25 @@ var someCode = { object: { existingSubProperty: 123 } }; -}.call(window,document)); +}.call(window)); +" +`; + +exports[`loader should work with the "wrapper" option as a string notation: warnings 1`] = `Array []`; + +exports[`loader should work with the "wrapper" options as an object notation: errors 1`] = `Array []`; + +exports[`loader should work with the "wrapper" options as an object notation: module 1`] = ` +"/*** IMPORTS FROM imports-loader ***/ + +(function(myGlobalVariable, myOtherGlobalVariable) { +var someCode = { + number: 123, + object: { existingSubProperty: 123 } +}; + +}.call(window, myGlobalVariable, myOtherGlobalVariable)); " `; -exports[`loader should work with the "wrapper" options and arguments: warnings 1`] = `Array []`; +exports[`loader should work with the "wrapper" options as an object notation: warnings 1`] = `Array []`; diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index 5db0848..2c64d32 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -229,32 +229,63 @@ exports[`validate options should throw an error on the "unknown" option with "tr * options misses the property 'additionalCode' | should be any non-object." `; -exports[`validate options should throw an error on the "wrapper" option with "[""]" value 1`] = ` +exports[`validate options should throw an error on the "wrapper" option with "/test/" value 1`] = ` "Invalid options object. Imports Loader has been initialized using an options object that does not match the API schema. - - options.wrapper[0] should be an non-empty string." + - options.wrapper misses the property 'thisArg'. Should be: + non-empty string" `; -exports[`validate options should throw an error on the "wrapper" option with "[]" value 1`] = ` +exports[`validate options should throw an error on the "wrapper" option with "[""]" value 1`] = ` "Invalid options object. Imports Loader has been initialized using an options object that does not match the API schema. - - options.wrapper should be an non-empty array." + - options.wrapper should be one of these: + boolean | non-empty string | object { thisArg, args? } + Details: + * options.wrapper should be a boolean. + * options.wrapper should be a non-empty string. + * options.wrapper should be an object: + object { thisArg, args? }" `; -exports[`validate options should throw an error on the "wrapper" option with "false" value 1`] = ` +exports[`validate options should throw an error on the "wrapper" option with "[]" value 1`] = ` "Invalid options object. Imports Loader has been initialized using an options object that does not match the API schema. - options.wrapper should be one of these: - non-empty string | [non-empty string, ...] (should not have fewer than 1 item) + boolean | non-empty string | object { thisArg, args? } Details: + * options.wrapper should be a boolean. * options.wrapper should be a non-empty string. - * options.wrapper should be an array: - [non-empty string, ...] (should not have fewer than 1 item)" + * options.wrapper should be an object: + object { thisArg, args? }" +`; + +exports[`validate options should throw an error on the "wrapper" option with "{"thisArg":"window","args":[1,"bar"]}" value 1`] = ` +"Invalid options object. Imports Loader has been initialized using an options object that does not match the API schema. + - options.wrapper.args[0] should be a non-empty string." `; -exports[`validate options should throw an error on the "wrapper" option with "true" value 1`] = ` +exports[`validate options should throw an error on the "wrapper" option with "{"thisArg":"window","args":true}" value 1`] = ` +"Invalid options object. Imports Loader has been initialized using an options object that does not match the API schema. + - options.wrapper.args should be an array: + [non-empty string, ...] (should not have fewer than 1 item)" +`; + +exports[`validate options should throw an error on the "wrapper" option with "{"thisArg":1}" value 1`] = ` +"Invalid options object. Imports Loader has been initialized using an options object that does not match the API schema. + - options.wrapper.thisArg should be a non-empty string." +`; + +exports[`validate options should throw an error on the "wrapper" option with "{"unknown":true}" value 1`] = ` "Invalid options object. Imports Loader has been initialized using an options object that does not match the API schema. - options.wrapper should be one of these: - non-empty string | [non-empty string, ...] (should not have fewer than 1 item) + boolean | non-empty string | object { thisArg, args? } Details: - * options.wrapper should be a non-empty string. - * options.wrapper should be an array: - [non-empty string, ...] (should not have fewer than 1 item)" + * options.wrapper has an unknown property 'unknown'. These properties are valid: + object { thisArg, args? } + * options.wrapper misses the property 'thisArg'. Should be: + non-empty string" +`; + +exports[`validate options should throw an error on the "wrapper" option with "{}" value 1`] = ` +"Invalid options object. Imports Loader has been initialized using an options object that does not match the API schema. + - options.wrapper misses the property 'thisArg'. Should be: + non-empty string" `; diff --git a/test/loader.test.js b/test/loader.test.js index f808980..6b2f938 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -217,7 +217,20 @@ describe('loader', () => { expect(getWarnings(stats)).toMatchSnapshot('warnings'); }); - it('should work with the "wrapper" option', async () => { + it('should work with the "wrapper" option as a boolean notation', async () => { + const compiler = getCompiler('some-library.js', { + wrapper: true, + }); + const stats = await compile(compiler); + + expect(getModuleSource('./some-library.js', stats)).toMatchSnapshot( + 'module' + ); + expect(getErrors(stats)).toMatchSnapshot('errors'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + }); + + it('should work with the "wrapper" option as a string notation', async () => { const compiler = getCompiler('some-library.js', { wrapper: 'window', }); @@ -230,9 +243,12 @@ describe('loader', () => { expect(getWarnings(stats)).toMatchSnapshot('warnings'); }); - it('should work with the "wrapper" options and arguments', async () => { + it('should work with the "wrapper" options as an object notation', async () => { const compiler = getCompiler('some-library.js', { - wrapper: ['window', 'document'], + wrapper: { + thisArg: 'window', + args: ['myGlobalVariable', 'myOtherGlobalVariable'], + }, }); const stats = await compile(compiler); diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 6d1d615..c867ff2 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -84,8 +84,23 @@ describe('validate options', () => { ], }, wrapper: { - success: ['window', ['window', 'document']], - failure: [false, true, [], ['']], + success: [ + true, + false, + 'window', + { thisArg: 'window' }, + { thisArg: 'window', args: ['foo', 'bar'] }, + ], + failure: [ + [], + [''], + /test/, + {}, + { unknown: true }, + { thisArg: 1 }, + { thisArg: 'window', args: true }, + { thisArg: 'window', args: [1, 'bar'] }, + ], }, additionalCode: { success: ['var x = 2;'],