From d7f6ddb38f9254b5e6a90629d40738dfc484badc Mon Sep 17 00:00:00 2001 From: Vlad Ivanov Date: Mon, 24 Oct 2022 23:57:16 +0200 Subject: [PATCH 1/4] feat(loader): add functions support for locals --- src/loader.js | 18 +++++++++++------- src/utils.js | 10 ++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/loader.js b/src/loader.js index 6de4e3d6..423a14f4 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 } | 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,8 @@ function pitch(request) { locals = {}; } - locals[key] = originalExports[key]; + /** @type {{ [key: string]: string }} */ (locals)[key] = + originalExports[key]; } }); } else { @@ -228,15 +231,16 @@ function pitch(request) { ? Object.keys(locals) .map( (key) => - `\nexport var ${key} = ${JSON.stringify( - /** @type {{[key: string]: string }} */ - (locals)[key] + `\nexport var ${key} = ${stringifyLocal( + /** @type {{ [key: string]: string | function }} */ (locals)[ + key + ] )};` ) .join("") : `\n${ esModule ? "export default" : "module.exports =" - } ${JSON.stringify(locals)};` + } ${stringifyLocal(/** @type {function} */ (locals))};` : esModule ? `\nexport {};` : ""; 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, }; From afc166cae44b6f8fdb93b51825364dd75dae6e54 Mon Sep 17 00:00:00 2001 From: Vlad Ivanov Date: Tue, 25 Oct 2022 20:42:36 +0200 Subject: [PATCH 2/4] test(loader): add tests for stringifyLocal --- test/stringifyLocal.test.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 test/stringifyLocal.test.js diff --git a/test/stringifyLocal.test.js b/test/stringifyLocal.test.js new file mode 100644 index 00000000..eb91e9a2 --- /dev/null +++ b/test/stringifyLocal.test.js @@ -0,0 +1,35 @@ +import { stringifyLocal } from "../src/utils"; + +describe("stringifyLocal", () => { + it(`object`, async () => { + const testObj = { classNameA: "classA", classNameB: "classB" }; + const actual = stringifyLocal(testObj); + + expect( + actual === '{"classNameA":"classA","classNameB":"classB"}' || + actual === '{"classNameB":"classB","classNameA":"classA"}' + ).toBe(true); + }); + + 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 }' + ); + }); +}); From 3401dca4f1561828cf8403a4e7622799d839eede Mon Sep 17 00:00:00 2001 From: Vlad Ivanov Date: Thu, 10 Nov 2022 22:33:48 +0100 Subject: [PATCH 3/4] test(loader): add integrations tests for css-loader functional exports --- .../app/index.js | 4 + .../app/mockLoader.js | 14 +++ .../app/style.css | 7 ++ .../expected/main.css | 2 + .../expected/main.js | 87 +++++++++++++++++++ .../webpack.config.js | 21 +++++ 6 files changed, 135 insertions(+) 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 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", + }), + ], +}; From c3dca47e359cb33ef5ad9c8ea72b2c7611968c7f Mon Sep 17 00:00:00 2001 From: Vlad Ivanov Date: Thu, 10 Nov 2022 22:45:02 +0100 Subject: [PATCH 4/4] fix(loader): remove handling a non-existent case --- src/loader.js | 11 ++++------- test/stringifyLocal.test.js | 10 ---------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/loader.js b/src/loader.js index 423a14f4..b3006b4a 100644 --- a/src/loader.js +++ b/src/loader.js @@ -23,7 +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 } | function} Locals */ +/** @typedef {{ [key: string]: string | function }} Locals */ /** @typedef {any} TODO */ @@ -172,8 +172,7 @@ function pitch(request) { locals = {}; } - /** @type {{ [key: string]: string }} */ (locals)[key] = - originalExports[key]; + /** @type {Locals} */ (locals)[key] = originalExports[key]; } }); } else { @@ -232,15 +231,13 @@ function pitch(request) { .map( (key) => `\nexport var ${key} = ${stringifyLocal( - /** @type {{ [key: string]: string | function }} */ (locals)[ - key - ] + /** @type {Locals} */ (locals)[key] )};` ) .join("") : `\n${ esModule ? "export default" : "module.exports =" - } ${stringifyLocal(/** @type {function} */ (locals))};` + } ${JSON.stringify(locals)};` : esModule ? `\nexport {};` : ""; diff --git a/test/stringifyLocal.test.js b/test/stringifyLocal.test.js index eb91e9a2..20335337 100644 --- a/test/stringifyLocal.test.js +++ b/test/stringifyLocal.test.js @@ -1,16 +1,6 @@ import { stringifyLocal } from "../src/utils"; describe("stringifyLocal", () => { - it(`object`, async () => { - const testObj = { classNameA: "classA", classNameB: "classB" }; - const actual = stringifyLocal(testObj); - - expect( - actual === '{"classNameA":"classA","classNameB":"classB"}' || - actual === '{"classNameB":"classB","classNameA":"classA"}' - ).toBe(true); - }); - it(`primitive`, async () => { const testObj = "classA";