From c56e1904f4c090437d4d1f796aac089af5e33c4e Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 20 May 2026 17:57:01 +0300 Subject: [PATCH 1/2] refactor: prepare code before release --- .changeset/add-jsdoc-types.md | 4 +- .changeset/track-sync-loaded-dependencies.md | 2 +- .github/workflows/nodejs.yml | 6 + package-lock.json | 26 ++- package.json | 12 +- src/index.js | 29 +-- src/utils.js | 181 ++++++++++--------- tsconfig.json | 25 +++ types/index.d.ts | 21 +++ types/utils.d.ts | 112 ++++++++++++ 10 files changed, 305 insertions(+), 113 deletions(-) create mode 100644 tsconfig.json create mode 100644 types/index.d.ts create mode 100644 types/utils.d.ts diff --git a/.changeset/add-jsdoc-types.md b/.changeset/add-jsdoc-types.md index 8cdf9763..1c444dd5 100644 --- a/.changeset/add-jsdoc-types.md +++ b/.changeset/add-jsdoc-types.md @@ -1,5 +1,5 @@ --- -"less-loader": patch +"less-loader": minor --- -Add JSDoc type annotations to `src/index.js` and `src/utils.js` so editors and downstream consumers get IntelliSense without a TypeScript toolchain. +Added types. diff --git a/.changeset/track-sync-loaded-dependencies.md b/.changeset/track-sync-loaded-dependencies.md index e4b0ccdb..f0288bb7 100644 --- a/.changeset/track-sync-loaded-dependencies.md +++ b/.changeset/track-sync-loaded-dependencies.md @@ -2,4 +2,4 @@ "less-loader": patch --- -Track files loaded synchronously by Less (e.g. `data-uri()` and custom functions installed via `@plugin`) as webpack file dependencies. Previously these reads were delegated to Less's default file manager and never registered with webpack, so persistent caching could keep a stale build when only the sync-loaded file changed. See [#492](https://github.com/webpack/less-loader/issues/492). +Track files loaded synchronously by Less (e.g. `data-uri()` and custom functions installed via `@plugin`) as webpack file dependencies. See [#492](https://github.com/webpack/less-loader/issues/492). diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 86365716..03f838b2 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -52,6 +52,12 @@ jobs: - name: Security audit run: npm run security + - name: Build types + run: npm run build:types + + - name: Check types + run: if [ -n "$(git status types --porcelain)" ]; then echo "Missing types. Update types by running 'npm run build:types'"; exit 1; else echo "All types are valid"; fi + test: name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }}, Webpack ${{ matrix.webpack-version }} diff --git a/package-lock.json b/package-lock.json index 06b1299f..5df82b56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,16 @@ "name": "less-loader", "version": "12.3.3", "license": "MIT", + "dependencies": { + "@types/less": "^3.0.8" + }, "devDependencies": { "@babel/cli": "^7.24.7", "@babel/core": "^7.24.7", "@babel/preset-env": "^7.29.5", "@changesets/cli": "^2.30.0", "@changesets/get-github-info": "^0.8.0", + "@types/node": "^22.13.4", "cspell": "^10.0.0", "del": "^8.0.1", "del-cli": "^7.0.0", @@ -26,6 +30,7 @@ "memfs": "^4.57.2", "npm-run-all": "^4.1.5", "prettier": "^3.8.3", + "typescript": "^6.0.3", "webpack": "^5.107.0" }, "engines": { @@ -3831,6 +3836,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/less": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/less/-/less-3.0.8.tgz", + "integrity": "sha512-Gjm4+H9noDJgu5EdT3rUw5MhPBag46fiOy27BefvWkNL8mlZnKnCaVVVTLKj6RYXed9b62CPKnPav9govyQDzA==", + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -3849,13 +3860,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz", - "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==", + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/unist": { @@ -13705,7 +13716,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13758,9 +13768,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 9a32e1b6..81775bd9 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "type": "module", "exports": { ".": { + "types": "./types/index.d.ts", "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js", "default": "./dist/esm/index.js" @@ -31,8 +32,10 @@ }, "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", + "types": "./types/index.d.ts", "files": [ - "dist" + "dist", + "types" ], "scripts": { "start": "npm run build -- -w", @@ -40,11 +43,13 @@ "prebuild": "npm run clean", "build:esm": "babel src -d dist/esm --env-name esm --copy-files --no-copy-ignored && node -e \"require('fs').writeFileSync('dist/esm/package.json','{\\\"type\\\":\\\"module\\\"}\\n')\"", "build:cjs": "babel src -d dist/cjs --env-name cjs --copy-files --no-copy-ignored && node -e \"const fs=require('fs');fs.writeFileSync('dist/cjs/package.json','{\\\"type\\\":\\\"commonjs\\\"}\\n');fs.appendFileSync('dist/cjs/index.js','module.exports = exports.default;\\nmodule.exports.default = exports.default;\\n')\"", + "build:types": "tsc && prettier \"types/**/*.ts\" --write", "build": "npm-run-all -p \"build:*\"", "security": "npm audit --production", "lint": "npm-run-all -l -p \"lint:**\" && npm run fmt:check", "lint:code": "eslint --cache .", "lint:spelling": "cspell --cache --no-must-find-files --quiet \"**/*.*\"", + "lint:types": "tsc --pretty --noEmit", "fmt": "npm run fmt:check -- --write", "fmt:check": "prettier --list-different --cache --ignore-unknown .", "fix": "npm run fix:code && npm run fmt", @@ -63,6 +68,7 @@ "@babel/preset-env": "^7.29.5", "@changesets/cli": "^2.30.0", "@changesets/get-github-info": "^0.8.0", + "@types/node": "^22.13.4", "cspell": "^10.0.0", "del": "^8.0.1", "del-cli": "^7.0.0", @@ -75,6 +81,7 @@ "memfs": "^4.57.2", "npm-run-all": "^4.1.5", "prettier": "^3.8.3", + "typescript": "^6.0.3", "webpack": "^5.107.0" }, "peerDependencies": { @@ -92,5 +99,8 @@ }, "engines": { "node": ">= 22.11.0" + }, + "dependencies": { + "@types/less": "^3.0.8" } } diff --git a/src/index.js b/src/index.js index 3a1710dc..842ef5bc 100644 --- a/src/index.js +++ b/src/index.js @@ -9,20 +9,20 @@ import { normalizeSourceMap, } from "./utils.js"; -/** @typedef {import("webpack").LoaderContext} LoaderContext */ -/** @typedef {import("./utils.js").LessLoaderOptions} LessLoaderOptions */ +/** @typedef {import("webpack").LoaderContext} LoaderContext */ +/** @typedef {import("schema-utils/declarations/validate").Schema} Schema */ +/** @typedef {import("./utils.js").LoaderOptions} LoaderOptions */ /** @typedef {import("./utils.js").LessError} LessError */ /** @typedef {import("./utils.js").SourceMap} SourceMap */ /** * Webpack loader that compiles Less to CSS. - * * @this {LoaderContext} - * @param {string} source - * @returns {Promise} + * @param {string} content content + * @returns {Promise} loader result */ -async function lessLoader(source) { - const options = /** @type {LessLoaderOptions} */ (this.getOptions(schema)); +async function lessLoader(content) { + const options = this.getOptions(/** @type {Schema} */ (schema)); const callback = this.async(); let implementation; @@ -56,11 +56,12 @@ async function lessLoader(source) { lessOptions.sourceMap = { sourceMapBasepath: "", outputSourceFiles: true, + // @ts-expect-error bad types disableSourcemapAnnotation: true, }; } - let data = source; + let data = content; if (typeof options.additionalData !== "undefined") { data = @@ -72,7 +73,7 @@ async function lessLoader(source) { const logger = this.getLogger("less-loader"); const loaderContext = this; const loggerListener = { - /** @param {string} message */ + /** @param {string} message message */ error(message) { // TODO enable by default in the next major release if (options.lessLogAsWarnOrErr) { @@ -81,7 +82,7 @@ async function lessLoader(source) { logger.error(message); } }, - /** @param {string} message */ + /** @param {string} message message */ warn(message) { // TODO enable by default in the next major release if (options.lessLogAsWarnOrErr) { @@ -90,16 +91,17 @@ async function lessLoader(source) { logger.warn(message); } }, - /** @param {string} message */ + /** @param {string} message message */ info(message) { logger.log(message); }, - /** @param {string} message */ + /** @param {string} message message */ debug(message) { logger.debug(message); }, }; + // @ts-expect-error bad types implementation.logger.addListener(loggerListener); let result; @@ -124,9 +126,12 @@ async function lessLoader(source) { return; } finally { // Fix memory leaks in `less` + // @ts-expect-error bad types implementation.logger.removeListener(loggerListener); + // @ts-expect-error we need it to reset loader context delete lessOptions.pluginManager.webpackLoaderContext; + // @ts-expect-error we need it to reset loader context delete lessOptions.pluginManager; } diff --git a/src/utils.js b/src/utils.js index cfbb9532..15fdc227 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,49 +1,33 @@ import path from "node:path"; import url from "node:url"; -/** @typedef {import("webpack").LoaderContext} LoaderContext */ +/** @typedef {import("webpack").LoaderContext} LoaderContext */ /** @typedef {import("less")} Less */ + +/** @typedef {Parameters[1]} LessOptions */ +/** @typedef {NonNullable[number]} LessPlugin */ /** @typedef {Less["FileManager"]} LessFileManager */ +/** @typedef {InstanceType} PluginManager */ /** @typedef {InstanceType} LessFileManagerInstance */ -/** @typedef {Parameters} LoadFileArgs */ +/** @typedef {Parameters["loadFile"]>[2]} LoadFileOptions */ +/** @typedef {Parameters["loadFile"]>[3]} Environment */ /** @typedef {Awaited>} LoadFileResult */ /** - * @typedef {object} LessPluginManager - * @property {(fileManager: LessFileManagerInstance) => void} addFileManager - * @property {LoaderContext} [webpackLoaderContext] - */ - -/** - * @typedef {object} LessPlugin - * @property {(lessInstance: Less, pluginManager: LessPluginManager) => void} install - * @property {[number, number, number]} [minVersion] - */ - -/** - * @typedef {object} LessOptions - * @property {LessPlugin[]} plugins - * @property {boolean} [relativeUrls] - * @property {string} [filename] - * @property {LessPluginManager} [pluginManager] - * @property {{ sourceMapBasepath: string, outputSourceFiles: boolean, disableSourcemapAnnotation: boolean }} [sourceMap] - */ - -/** - * @typedef {object} LessLoaderOptions - * @property {LessOptions | ((loaderContext: LoaderContext) => LessOptions)} [lessOptions] - * @property {string | ((source: string, loaderContext: LoaderContext) => string | Promise)} [additionalData] - * @property {boolean} [sourceMap] - * @property {boolean | "only"} [webpackImporter] - * @property {string | Less} [implementation] - * @property {boolean} [lessLogAsWarnOrErr] + * @typedef {object} LoaderOptions + * @property {LessOptions | ((loaderContext: LoaderContext) => LessOptions)=} lessOptions less options + * @property {string | ((source: string, loaderContext: LoaderContext) => string | Promise)=} additionalData additional data + * @property {boolean=} sourceMap true when need to generate source map, otherwise false + * @property {boolean | "only"=} webpackImporter true when need to use webpack importer, otherwise false + * @property {string | Less=} implementation implementation + * @property {boolean=} lessLogAsWarnOrErr true when need to log less warnings and errors as webpack warnings and errors */ /** * @typedef {object} SourceMap - * @property {string} [file] - * @property {string} [sourceRoot] - * @property {string[]} sources + * @property {string=} file file + * @property {string=} sourceRoot source root + * @property {string[]} sources sources */ /** @@ -73,11 +57,10 @@ const MODULE_REQUEST_REGEX = /^[^?]*~/; /** * Creates a Less plugin that uses webpack's resolving engine that is provided by the loaderContext. - * - * @param {LoaderContext} loaderContext - * @param {Less} implementation - * @param {Array>} pendingDependencyTasks - * @returns {LessPlugin} + * @param {LoaderContext} loaderContext loader context + * @param {Less} implementation implementation + * @param {Promise[]} pendingDependencyTasks pending dependency tasks + * @returns {LessPlugin} less plugin */ function createWebpackLessPlugin( loaderContext, @@ -85,7 +68,7 @@ function createWebpackLessPlugin( pendingDependencyTasks, ) { const lessOptions = - /** @type {LessLoaderOptions} */ + /** @type {LoaderOptions} */ (loaderContext.getOptions()); const resolve = loaderContext.getResolve({ dependencyType: "less", @@ -98,8 +81,8 @@ function createWebpackLessPlugin( class WebpackFileManager extends implementation.FileManager { /** - * @param {string} filename - * @returns {boolean} + * @param {string} filename filename + * @returns {boolean} true when filename is supported, otherwise false */ supports(filename) { if (filename[0] === "/" || IS_NATIVE_WIN32_PATH.test(filename)) { @@ -122,18 +105,18 @@ function createWebpackLessPlugin( // persistent cache won't invalidate when a sync-loaded file changes. // See https://github.com/webpack/less-loader/issues/492. /** - * @returns {boolean} + * @returns {boolean} true when support sync, otherwise false */ supportsSync() { return true; } /** - * @param {string} filename - * @param {string} currentDirectory - * @param {{ [key: string]: unknown }} options - * @param {unknown} environment - * @returns {LoadFileResult} + * @param {string} filename filename + * @param {string} currentDirectory current directory + * @param {LoadFileOptions} options options + * @param {Environment} environment environment + * @returns {LoadFileResult} loaded file */ loadFileSync(filename, currentDirectory, options, environment) { // The default Less `loadFileSync` internally dispatches to @@ -141,11 +124,14 @@ function createWebpackLessPlugin( // override `loadFile` (async), dynamic dispatch would land back in // our async version and break the sync contract. Invoke the parent // `loadFile` directly with the sync flag instead. - const result = super.loadFile( - filename, - currentDirectory, - { ...options, syncImport: true }, - environment, + // @ts-expect-error bad types in less + const result = /** @type {LoadFileResult} */ ( + super.loadFile( + filename, + currentDirectory, + { ...options, syncImport: true }, + environment, + ) ); if (result && result.filename) { @@ -182,9 +168,9 @@ function createWebpackLessPlugin( } /** - * @param {string} filename - * @param {string} currentDirectory - * @returns {Promise} + * @param {string} filename filename + * @param {string} currentDirectory current directory + * @returns {Promise} resolved filename */ async resolveFilename(filename, currentDirectory) { // Less is giving us trailing slashes, but the context should have no trailing slash @@ -205,9 +191,9 @@ function createWebpackLessPlugin( } /** - * @param {string} context - * @param {string[]} possibleRequests - * @returns {Promise} + * @param {string} context context + * @param {string[]} possibleRequests possible requests + * @returns {Promise} resolved requests */ async resolveRequests(context, possibleRequests) { if (possibleRequests.length === 0) { @@ -232,11 +218,13 @@ function createWebpackLessPlugin( } /** - * @param {string} filename - * @param {LoadFileArgs} args - * @returns {Promise} + * @param {string} filename filename + * @param {string} currentDirectory current directory + * @param {LoadFileOptions} options options + * @param {Environment} environment environment + * @returns {Promise} loaded file */ - async loadFile(filename, ...args) { + async loadFile(filename, currentDirectory, options, environment) { let result; try { @@ -251,7 +239,12 @@ function createWebpackLessPlugin( throw error; } - result = await super.loadFile(filename, ...args); + result = await super.loadFile( + filename, + currentDirectory, + options, + environment, + ); } catch (error) { const lessError = /** @type {LessError} */ (error); @@ -260,7 +253,7 @@ function createWebpackLessPlugin( } try { - result = await this.resolveFilename(filename, ...args); + result = await this.resolveFilename(filename, currentDirectory); } catch (err) { lessError.message = `Less resolver error:\n${lessError.message}\n\n` + @@ -272,7 +265,7 @@ function createWebpackLessPlugin( loaderContext.addDependency(result); - return super.loadFile(result, ...args); + return super.loadFile(result, currentDirectory, options, environment); } const absoluteFilename = path.isAbsolute(result.filename) @@ -286,7 +279,11 @@ function createWebpackLessPlugin( } return { - install(lessInstance, pluginManager) { + /** + * @param {Less} less less + * @param {PluginManager} pluginManager plugin manager + */ + install(less, pluginManager) { pluginManager.addFileManager(new WebpackFileManager()); }, minVersion: [3, 0, 0], @@ -295,11 +292,10 @@ function createWebpackLessPlugin( /** * Get the `less` options from the loader context and normalizes its values - * - * @param {LoaderContext} loaderContext - * @param {LessLoaderOptions} loaderOptions - * @param {Less} implementation - * @returns {{ lessOptions: LessOptions, pendingDependencyTasks: Array> }} + * @param {LoaderContext} loaderContext loader context + * @param {LoaderOptions} loaderOptions loader options + * @param {Less} implementation implementation + * @returns {{ lessOptions: LessOptions, pendingDependencyTasks: Promise[] }} implementation and pending tasks */ function getLessOptions(loaderContext, loaderOptions, implementation) { const options = @@ -309,7 +305,7 @@ function getLessOptions(loaderContext, loaderOptions, implementation) { /** @type {LessOptions} */ const lessOptions = { - plugins: [], + // @ts-expect-error bad types relativeUrls: true, // We need to set the filename because otherwise our WebpackFileManager will receive an undefined path for the entry filename: loaderContext.resourcePath, @@ -320,10 +316,11 @@ function getLessOptions(loaderContext, loaderOptions, implementation) { // synchronous Less file loads (e.g. `data-uri()`, `@plugin`). The loader // awaits these before completing so webpack's dependency snapshot is // accurate. - /** @type {Array>} */ + /** @type {Promise[]} */ const pendingDependencyTasks = []; - const plugins = [...lessOptions.plugins]; + /** @type {LessPlugin[]} */ + const plugins = [...(lessOptions.plugins || [])]; const shouldUseWebpackImporter = typeof loaderOptions.webpackImporter === "boolean" || loaderOptions.webpackImporter === "only" @@ -341,9 +338,14 @@ function getLessOptions(loaderContext, loaderOptions, implementation) { } plugins.unshift({ - install(lessProcessor, pluginManager) { + /** + * @param {Less} less less + * @param {PluginManager} pluginManager plugin manager + */ + install(less, pluginManager) { + // @ts-expect-error to provide loader context into plugin pluginManager.webpackLoaderContext = loaderContext; - + // @ts-expect-error to reset it after execution lessOptions.pluginManager = pluginManager; }, }); @@ -354,8 +356,8 @@ function getLessOptions(loaderContext, loaderOptions, implementation) { } /** - * @param {string} url - * @returns {boolean} + * @param {string} url url + * @returns {boolean} true when url is unsupported, otherwise false */ function isUnsupportedUrl(url) { // Is Windows path @@ -369,8 +371,8 @@ function isUnsupportedUrl(url) { } /** - * @param {SourceMap} map - * @returns {SourceMap} + * @param {SourceMap} map map + * @returns {SourceMap} normalized source map */ function normalizeSourceMap(map) { const newMap = map; @@ -390,8 +392,8 @@ function normalizeSourceMap(map) { } /** - * @param {string} specifier - * @returns {string} + * @param {string} specifier specifier + * @returns {string} resolved specifier */ function normalizeImportSpecifier(specifier) { if (specifier.startsWith("file:")) { @@ -406,9 +408,9 @@ function normalizeImportSpecifier(specifier) { } /** - * @param {LoaderContext} loaderContext - * @param {string | Less | undefined} implementation - * @returns {Promise} + * @param {LoaderContext} loaderContext loader context + * @param {string | Less | undefined} implementation implementation + * @returns {Promise} less implementation */ async function getLessImplementation(loaderContext, implementation) { let resolvedImplementation = implementation; @@ -424,8 +426,8 @@ async function getLessImplementation(loaderContext, implementation) { } /** - * @param {LessError} error - * @returns {string[]} + * @param {LessError} error error + * @returns {string[]} file excerpt */ function getFileExcerptIfPossible(error) { if (typeof error.extract === "undefined") { @@ -445,8 +447,8 @@ function getFileExcerptIfPossible(error) { } /** - * @param {LessError} error - * @returns {Error} + * @param {LessError} error error + * @returns {Error} built error */ function errorFactory(error) { const message = [ @@ -464,6 +466,7 @@ function errorFactory(error) { new Error(message, { cause: error }) ); + // @ts-expect-error avoid extra stack for less obj.stack = null; return obj; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..9879d50a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + + "resolveJsonModule": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": false, + + "types": ["node"], + + "rootDir": "./src", + "outDir": "./types", + + "allowJs": true, + "checkJs": true, + "strict": true, + + "declaration": true, + "emitDeclarationOnly": true + }, + "include": ["./src/**/*"] +} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 00000000..c70d5f02 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,21 @@ +export default lessLoader; +export type LoaderContext = import("webpack").LoaderContext; +export type Schema = import("schema-utils/declarations/validate").Schema; +export type LoaderOptions = import("./utils.js").LoaderOptions; +export type LessError = import("./utils.js").LessError; +export type SourceMap = import("./utils.js").SourceMap; +/** @typedef {import("webpack").LoaderContext} LoaderContext */ +/** @typedef {import("schema-utils/declarations/validate").Schema} Schema */ +/** @typedef {import("./utils.js").LoaderOptions} LoaderOptions */ +/** @typedef {import("./utils.js").LessError} LessError */ +/** @typedef {import("./utils.js").SourceMap} SourceMap */ +/** + * Webpack loader that compiles Less to CSS. + * @this {LoaderContext} + * @param {string} content content + * @returns {Promise} loader result + */ +declare function lessLoader( + this: import("webpack").LoaderContext, + content: string, +): Promise; diff --git a/types/utils.d.ts b/types/utils.d.ts new file mode 100644 index 00000000..52f43a92 --- /dev/null +++ b/types/utils.d.ts @@ -0,0 +1,112 @@ +export type LoaderContext = import("webpack").LoaderContext; +export type Less = LessStatic; +export type LessOptions = Parameters[1]; +export type LessPlugin = NonNullable[number]; +export type LessFileManager = Less["FileManager"]; +export type PluginManager = InstanceType; +export type LessFileManagerInstance = InstanceType; +export type LoadFileOptions = Parameters< + InstanceType["loadFile"] +>[2]; +export type Environment = Parameters< + InstanceType["loadFile"] +>[3]; +export type LoadFileResult = Awaited< + ReturnType +>; +export type LoaderOptions = { + /** + * less options + */ + lessOptions?: + | (LessOptions | ((loaderContext: LoaderContext) => LessOptions)) + | undefined; + /** + * additional data + */ + additionalData?: + | ( + | string + | (( + source: string, + loaderContext: LoaderContext, + ) => string | Promise) + ) + | undefined; + /** + * true when need to generate source map, otherwise false + */ + sourceMap?: boolean | undefined; + /** + * true when need to use webpack importer, otherwise false + */ + webpackImporter?: (boolean | "only") | undefined; + /** + * implementation + */ + implementation?: (string | Less) | undefined; + /** + * true when need to log less warnings and errors as webpack warnings and errors + */ + lessLogAsWarnOrErr?: boolean | undefined; +}; +export type SourceMap = { + /** + * file + */ + file?: string | undefined; + /** + * source root + */ + sourceRoot?: string | undefined; + /** + * sources + */ + sources: string[]; +}; +export type LessError = Error & { + type?: string; + filename?: string; + line?: number; + column?: number; + extract?: string[]; +}; +/** + * @param {LessError} error error + * @returns {Error} built error + */ +export function errorFactory(error: LessError): Error; +/** + * @param {LoaderContext} loaderContext loader context + * @param {string | Less | undefined} implementation implementation + * @returns {Promise} less implementation + */ +export function getLessImplementation( + loaderContext: LoaderContext, + implementation: string | Less | undefined, +): Promise; +/** + * Get the `less` options from the loader context and normalizes its values + * @param {LoaderContext} loaderContext loader context + * @param {LoaderOptions} loaderOptions loader options + * @param {Less} implementation implementation + * @returns {{ lessOptions: LessOptions, pendingDependencyTasks: Promise[] }} implementation and pending tasks + */ +export function getLessOptions( + loaderContext: LoaderContext, + loaderOptions: LoaderOptions, + implementation: Less, +): { + lessOptions: LessOptions; + pendingDependencyTasks: Promise[]; +}; +/** + * @param {string} url url + * @returns {boolean} true when url is unsupported, otherwise false + */ +export function isUnsupportedUrl(url: string): boolean; +/** + * @param {SourceMap} map map + * @returns {SourceMap} normalized source map + */ +export function normalizeSourceMap(map: SourceMap): SourceMap; From 46234002eda858dce5280cb49032ac247c17c5cf Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Wed, 20 May 2026 18:06:00 +0300 Subject: [PATCH 2/2] refactor: fix --- package.json | 6 +++--- test/helpers/getCodeFromBundle.js | 6 ++++++ test/helpers/getCodeFromLess.js | 6 ++++++ test/helpers/normalizeErrors.js | 4 ++++ test/helpers/readAssets.js | 5 +++++ test/helpers/testLoader.cjs | 5 +++++ test/validate-options.test.js | 10 ++++++++++ 7 files changed, 39 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 81775bd9..d3ca4ff2 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,9 @@ "version": "changeset version", "release": "npm run build && changeset publish" }, + "dependencies": { + "@types/less": "^3.0.8" + }, "devDependencies": { "@babel/cli": "^7.24.7", "@babel/core": "^7.24.7", @@ -99,8 +102,5 @@ }, "engines": { "node": ">= 22.11.0" - }, - "dependencies": { - "@types/less": "^3.0.8" } } diff --git a/test/helpers/getCodeFromBundle.js b/test/helpers/getCodeFromBundle.js index ee3ac3bd..a48d2518 100644 --- a/test/helpers/getCodeFromBundle.js +++ b/test/helpers/getCodeFromBundle.js @@ -2,6 +2,12 @@ import vm from "node:vm"; import readAsset from "./readAsset.js"; +/** + * @param {Stats} stats stats + * @param {Compiler} compiler compiler + * @param {name} asset asset name + * @returns {Record} code from bundle + */ function getCodeFromBundle(stats, compiler, asset) { let code = null; diff --git a/test/helpers/getCodeFromLess.js b/test/helpers/getCodeFromLess.js index 0d855b45..ccf70a2d 100644 --- a/test/helpers/getCodeFromLess.js +++ b/test/helpers/getCodeFromLess.js @@ -177,6 +177,12 @@ class CustomImportPlugin { } } +/** + * @param {string} testId test ID + * @param {Options} options options + * @param {Context} context context + * @returns {{ css: string, map: RawSourceMap }} CSS and source map (if exist) + */ async function getCodeFromLess(testId, options = {}, context = {}) { let pathToFile; diff --git a/test/helpers/normalizeErrors.js b/test/helpers/normalizeErrors.js index 55316385..2e7e1211 100644 --- a/test/helpers/normalizeErrors.js +++ b/test/helpers/normalizeErrors.js @@ -1,3 +1,7 @@ +/** + * @param {string} str str + * @returns {string} str without cwd + */ function removeCWD(str) { const isWin = process.platform === "win32"; let cwd = process.cwd(); diff --git a/test/helpers/readAssets.js b/test/helpers/readAssets.js index b98b7a99..4517cf24 100644 --- a/test/helpers/readAssets.js +++ b/test/helpers/readAssets.js @@ -1,5 +1,10 @@ import readAsset from "./readAsset.js"; +/** + * @param {Compiler} compiler compiler + * @param {Stats} stats stats + * @returns {Record} assets + */ export default function readAssets(compiler, stats) { const assets = {}; diff --git a/test/helpers/testLoader.cjs b/test/helpers/testLoader.cjs index 982591e6..b40adc4c 100644 --- a/test/helpers/testLoader.cjs +++ b/test/helpers/testLoader.cjs @@ -1,5 +1,10 @@ "use strict"; +/** + * @param {string} content content + * @param {RawSourceMap} sourceMap source map + * @returns {string} code for tests + */ function testLoader(content, sourceMap) { const result = { css: content }; diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 6218e0dd..53858232 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -43,6 +43,10 @@ describe("validate options", () => { }, }; + /** + * @param {EXPECTED_ANY} value value + * @returns {string} stringified value + */ function stringifyValue(value) { if ( Array.isArray(value) || @@ -54,6 +58,12 @@ describe("validate options", () => { return value; } + /** + * @param {string} key key + * @param {EXPECTED_ANY} value value + * @param {string} type type + * @returns {Promise} created test case + */ function createTestCase(key, value, type) { it(`should ${ type === "success" ? "successfully validate" : "throw an error on"