From 65519d0701b3c5d60585468b8220159cbbfbe6b8 Mon Sep 17 00:00:00 2001 From: "Vladislav A. Ivanov" <33333080+yungvldai@users.noreply.github.com> Date: Wed, 16 Nov 2022 19:04:48 +0100 Subject: [PATCH] feat: add function support for locals (loader) (#985) --- src/loader.js | 13 +-- src/utils.js | 10 +++ .../app/index.js | 4 + .../app/mockLoader.js | 14 +++ .../app/style.css | 7 ++ .../expected/main.css | 2 + .../expected/main.js | 87 +++++++++++++++++++ .../webpack.config.js | 21 +++++ test/stringifyLocal.test.js | 25 ++++++ 9 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 test/cases/custom-loader-with-functional-exports/app/index.js create mode 100644 test/cases/custom-loader-with-functional-exports/app/mockLoader.js create mode 100644 test/cases/custom-loader-with-functional-exports/app/style.css create mode 100644 test/cases/custom-loader-with-functional-exports/expected/main.css create mode 100644 test/cases/custom-loader-with-functional-exports/expected/main.js create mode 100644 test/cases/custom-loader-with-functional-exports/webpack.config.js create mode 100644 test/stringifyLocal.test.js diff --git a/src/loader.js b/src/loader.js index 6de4e3d6..b3006b4a 100644 --- a/src/loader.js +++ b/src/loader.js @@ -8,6 +8,7 @@ const { BASE_URI, SINGLE_DOT_PATH_SEGMENT, stringifyRequest, + stringifyLocal, } = require("./utils"); const schema = require("./loader-options.json"); @@ -22,6 +23,7 @@ const MiniCssExtractPlugin = require("./index"); /** @typedef {import("webpack").AssetInfo} AssetInfo */ /** @typedef {import("webpack").NormalModule} NormalModule */ /** @typedef {import("./index.js").LoaderOptions} LoaderOptions */ +/** @typedef {{ [key: string]: string | function }} Locals */ /** @typedef {any} TODO */ @@ -38,7 +40,7 @@ const MiniCssExtractPlugin = require("./index"); /** * @param {string} content - * @param {{ loaderContext: import("webpack").LoaderContext, options: LoaderOptions, locals: {[key: string]: string } | undefined }} context + * @param {{ loaderContext: import("webpack").LoaderContext, options: LoaderOptions, locals: Locals | undefined }} context * @returns {string} */ function hotLoader(content, context) { @@ -95,7 +97,7 @@ function pitch(request) { * @returns {void} */ const handleExports = (originalExports, compilation, assets, assetsInfo) => { - /** @type {{[key: string]: string } | undefined} */ + /** @type {Locals | undefined} */ let locals; let namedExport; @@ -170,7 +172,7 @@ function pitch(request) { locals = {}; } - locals[key] = originalExports[key]; + /** @type {Locals} */ (locals)[key] = originalExports[key]; } }); } else { @@ -228,9 +230,8 @@ function pitch(request) { ? Object.keys(locals) .map( (key) => - `\nexport var ${key} = ${JSON.stringify( - /** @type {{[key: string]: string }} */ - (locals)[key] + `\nexport var ${key} = ${stringifyLocal( + /** @type {Locals} */ (locals)[key] )};` ) .join("") diff --git a/src/utils.js b/src/utils.js index 416b38c1..68ab5f08 100644 --- a/src/utils.js +++ b/src/utils.js @@ -205,6 +205,15 @@ function getUndoPath(filename, outputPath, enforceRelative) { : append; } +/** + * + * @param {string | function} value + * @returns {string} + */ +function stringifyLocal(value) { + return typeof value === "function" ? value.toString() : JSON.stringify(value); +} + module.exports = { trueFn, findModuleById, @@ -216,5 +225,6 @@ module.exports = { BASE_URI, SINGLE_DOT_PATH_SEGMENT, stringifyRequest, + stringifyLocal, getUndoPath, }; diff --git a/test/cases/custom-loader-with-functional-exports/app/index.js b/test/cases/custom-loader-with-functional-exports/app/index.js new file mode 100644 index 00000000..7a1999bb --- /dev/null +++ b/test/cases/custom-loader-with-functional-exports/app/index.js @@ -0,0 +1,4 @@ +import { cnA, cnB } from "./style.css"; + +// eslint-disable-next-line no-console +console.log(cnA(), cnB()); diff --git a/test/cases/custom-loader-with-functional-exports/app/mockLoader.js b/test/cases/custom-loader-with-functional-exports/app/mockLoader.js new file mode 100644 index 00000000..e2fc6775 --- /dev/null +++ b/test/cases/custom-loader-with-functional-exports/app/mockLoader.js @@ -0,0 +1,14 @@ +export default function loader() { + const callback = this.async(); + + callback( + null, + `export default [ + [module.id, ".class-name-a {background: red;}", ""], + [module.id, ".class-name-b {background: blue;}", ""], +]; + +export var cnA = () => "class-name-a"; +export var cnB = () => "class-name-b";` + ); +} diff --git a/test/cases/custom-loader-with-functional-exports/app/style.css b/test/cases/custom-loader-with-functional-exports/app/style.css new file mode 100644 index 00000000..ce7bd16d --- /dev/null +++ b/test/cases/custom-loader-with-functional-exports/app/style.css @@ -0,0 +1,7 @@ +.class-name-a { + background: red; +} + +.class-name-b { + background: blue; +} diff --git a/test/cases/custom-loader-with-functional-exports/expected/main.css b/test/cases/custom-loader-with-functional-exports/expected/main.css new file mode 100644 index 00000000..b45259b8 --- /dev/null +++ b/test/cases/custom-loader-with-functional-exports/expected/main.css @@ -0,0 +1,2 @@ +.class-name-a {background: red;} +.class-name-b {background: blue;} diff --git a/test/cases/custom-loader-with-functional-exports/expected/main.js b/test/cases/custom-loader-with-functional-exports/expected/main.js new file mode 100644 index 00000000..9deffc2a --- /dev/null +++ b/test/cases/custom-loader-with-functional-exports/expected/main.js @@ -0,0 +1,87 @@ +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ([ +/* 0 */, +/* 1 */ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "cnA": () => (/* binding */ cnA), +/* harmony export */ "cnB": () => (/* binding */ cnB) +/* harmony export */ }); +// extracted by mini-css-extract-plugin +var cnA = () => "class-name-a"; +var cnB = () => "class-name-b"; + +/***/ }) +/******/ ]); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. +(() => { +__webpack_require__.r(__webpack_exports__); +/* harmony import */ var _style_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); + + +// eslint-disable-next-line no-console +console.log((0,_style_css__WEBPACK_IMPORTED_MODULE_0__.cnA)(), (0,_style_css__WEBPACK_IMPORTED_MODULE_0__.cnB)()); + +})(); + +/******/ })() +; \ No newline at end of file diff --git a/test/cases/custom-loader-with-functional-exports/webpack.config.js b/test/cases/custom-loader-with-functional-exports/webpack.config.js new file mode 100644 index 00000000..f2f951eb --- /dev/null +++ b/test/cases/custom-loader-with-functional-exports/webpack.config.js @@ -0,0 +1,21 @@ +import path from "path"; + +import Self from "../../../src"; + +module.exports = { + entry: "./index.js", + context: path.resolve(__dirname, "app"), + module: { + rules: [ + { + test: /\.css$/, + use: [Self.loader, "./mockLoader"], + }, + ], + }, + plugins: [ + new Self({ + filename: "[name].css", + }), + ], +}; diff --git a/test/stringifyLocal.test.js b/test/stringifyLocal.test.js new file mode 100644 index 00000000..20335337 --- /dev/null +++ b/test/stringifyLocal.test.js @@ -0,0 +1,25 @@ +import { stringifyLocal } from "../src/utils"; + +describe("stringifyLocal", () => { + it(`primitive`, async () => { + const testObj = "classA"; + + expect(stringifyLocal(testObj)).toBe('"classA"'); + }); + + it(`arrow function`, async () => { + const testFn = () => "classA"; + + expect(stringifyLocal(testFn)).toBe('() => "classA"'); + }); + + it(`function`, async () => { + const testFn = function () { + return "classA"; + }; + + expect(stringifyLocal(testFn)).toBe( + 'function () {\n return "classA";\n }' + ); + }); +});