@jesscss/plugin-less 2.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/lib/index.cjs +237 -0
- package/lib/index.d.ts +42 -0
- package/lib/index.js +208 -0
- package/lib/options.d.ts +5 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Matthew Dean
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @jesscss/plugin-less
|
|
2
|
+
|
|
3
|
+
**The Less language engine for Jess — the Less parser wired in with Less v5
|
|
4
|
+
rendering defaults.**
|
|
5
|
+
|
|
6
|
+
This is the Less language engine behind the shipping alpha surface.
|
|
7
|
+
`plugin-less` layers the Less grammar (`@jesscss/less-parser`) onto the Jess
|
|
8
|
+
compiler, registers the Less built-in functions (`@jesscss/fns`), and sets the
|
|
9
|
+
current output defaults. The `jess` CLI loads it by default, so if you render
|
|
10
|
+
`.less` you are already using it — you don't need to install this package
|
|
11
|
+
separately for normal CLI use.
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
- Parses `.less` into the Jess AST and hands it to the engine for a single
|
|
16
|
+
evaluate-and-emit pass.
|
|
17
|
+
- Registers the Less/Sass style-function library so `lighten()`, `percentage()`,
|
|
18
|
+
string and list helpers, etc. are available during evaluation.
|
|
19
|
+
- Owns the current Less-facing output defaults used across the alpha surface.
|
|
20
|
+
|
|
21
|
+
## Current output defaults
|
|
22
|
+
|
|
23
|
+
The defaults this plugin applies:
|
|
24
|
+
|
|
25
|
+
- `collapseNesting: false` — **nesting is preserved by default.** Less 4.x
|
|
26
|
+
flattened selectors; in v5 that flattening is an explicit opt-in
|
|
27
|
+
(`--collapse-nesting` on the CLI).
|
|
28
|
+
- `mathMode: 'parens-division'`, `unitMode: 'preserve'`, `equalityMode: 'less'`,
|
|
29
|
+
`leakyScope: true`, `bubbleRootAtRules: true`.
|
|
30
|
+
|
|
31
|
+
These keep the current Less-facing surface aligned on one set of output
|
|
32
|
+
semantics.
|
|
33
|
+
|
|
34
|
+
## Status
|
|
35
|
+
|
|
36
|
+
**Alpha.** This is the shipping Less-facing engine in the current Jess alpha. It
|
|
37
|
+
renders real Less, but it is early software with known rendering gaps and
|
|
38
|
+
expected failures; don't ship it to production yet, and please
|
|
39
|
+
[report bugs](https://github.com/jesscss/jess/issues).
|
|
40
|
+
|
|
41
|
+
The programmatic plugin/compiler API is **not yet stabilized** — the `jess` CLI
|
|
42
|
+
is the documented public surface for the alpha. Watch the
|
|
43
|
+
[docs site](https://jesscss.github.io/) for the API once it settles.
|
|
44
|
+
|
|
45
|
+
- Project overview & positioning: <https://github.com/jesscss/jess#readme>
|
|
46
|
+
- Docs: <https://jesscss.github.io/> (currently pre-alpha content)
|
|
47
|
+
- Issues: <https://github.com/jesscss/jess/issues>
|
|
48
|
+
- License: MIT
|
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __create = Object.create;
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
14
|
+
key = keys[i];
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
16
|
+
get: ((k) => from[k]).bind(null, key),
|
|
17
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
//#endregion
|
|
27
|
+
let _jesscss_core = require("@jesscss/core");
|
|
28
|
+
let _jesscss_fns_less_registry = require("@jesscss/fns/less/registry");
|
|
29
|
+
let _jesscss_plugin_less_compat = require("@jesscss/plugin-less-compat");
|
|
30
|
+
let node_path = require("node:path");
|
|
31
|
+
node_path = __toESM(node_path);
|
|
32
|
+
let node_module = require("node:module");
|
|
33
|
+
let _jesscss_style_resolver = require("@jesscss/style-resolver");
|
|
34
|
+
let _jesscss_less_parser = require("@jesscss/less-parser");
|
|
35
|
+
//#region src/index.ts
|
|
36
|
+
/**
|
|
37
|
+
* The Less plugin's default option values — the single source of truth for the
|
|
38
|
+
* v5 defaults. The `LessPlugin` constructor fills any unset option from here,
|
|
39
|
+
* and the `lessc` CLI imports the same object so its defaults can never drift
|
|
40
|
+
* from the engine's. Note `collapseNesting: false` — v5 preserves nesting by
|
|
41
|
+
* default (Less 4.x flattened; that is now an explicit opt-in).
|
|
42
|
+
*/
|
|
43
|
+
const lessPluginDefaults = {
|
|
44
|
+
mathMode: "parens-division",
|
|
45
|
+
unitMode: "preserve",
|
|
46
|
+
equalityMode: "less",
|
|
47
|
+
leakyScope: true,
|
|
48
|
+
bubbleRootAtRules: true,
|
|
49
|
+
processImports: true,
|
|
50
|
+
collapseNesting: false
|
|
51
|
+
};
|
|
52
|
+
const lessValueEvaluator = (0, _jesscss_core.buildEvaluator)((0, _jesscss_fns_less_registry.makeLessRegistry)());
|
|
53
|
+
/** Match Less's URL normalization without treating URL text as an import path. */
|
|
54
|
+
function normalizeUrlPath(url) {
|
|
55
|
+
const segments = url.split("/");
|
|
56
|
+
const normalized = [];
|
|
57
|
+
for (const segment of segments) {
|
|
58
|
+
if (segment === ".") continue;
|
|
59
|
+
if (segment === "..") {
|
|
60
|
+
if (normalized.length === 0 || normalized[normalized.length - 1] === "..") normalized.push(segment);
|
|
61
|
+
else normalized.pop();
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
normalized.push(segment);
|
|
65
|
+
}
|
|
66
|
+
return normalized.join("/");
|
|
67
|
+
}
|
|
68
|
+
function isUrlRelative(url) {
|
|
69
|
+
if (url.startsWith("/") || url.startsWith("#")) return false;
|
|
70
|
+
const colon = url.indexOf(":");
|
|
71
|
+
if (colon < 0) return true;
|
|
72
|
+
for (let index = 0; index < colon; index++) {
|
|
73
|
+
const code = url.charCodeAt(index);
|
|
74
|
+
if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122) && url[index] !== "-") return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
function rewriteUrlPath(url, rootpath) {
|
|
79
|
+
const rewritten = normalizeUrlPath(rootpath + url);
|
|
80
|
+
return url.startsWith(".") && isUrlRelative(rootpath) && !rewritten.startsWith(".") ? `./${rewritten}` : rewritten;
|
|
81
|
+
}
|
|
82
|
+
function escapeUnquotedUrlPath(pathValue) {
|
|
83
|
+
let escaped = "";
|
|
84
|
+
for (const char of pathValue) escaped += char === "(" || char === ")" || char === "'" || char === "\"" || " \n\r\f".includes(char) ? `\\${char}` : char;
|
|
85
|
+
return escaped;
|
|
86
|
+
}
|
|
87
|
+
function jsDelivrPackageSpecifier(candidate) {
|
|
88
|
+
const absolute = candidate.match(/^https?:\/\/cdn\.jsdelivr\.net\/npm\/([^?#]+)(?:[?#].*)?$/i);
|
|
89
|
+
if (absolute?.[1]) return absolute[1];
|
|
90
|
+
return candidate.match(/^\/\/cdn\.jsdelivr\.net\/npm\/([^?#]+)(?:[?#].*)?$/i)?.[1] ?? null;
|
|
91
|
+
}
|
|
92
|
+
var LessPlugin = class extends _jesscss_core.AbstractPlugin {
|
|
93
|
+
name = "less";
|
|
94
|
+
supportedExtensions = [".less"];
|
|
95
|
+
mathMode;
|
|
96
|
+
unitMode;
|
|
97
|
+
equalityMode;
|
|
98
|
+
leakyScope;
|
|
99
|
+
bubbleRootAtRules;
|
|
100
|
+
processImports;
|
|
101
|
+
collapseNesting;
|
|
102
|
+
pluginHosts = /* @__PURE__ */ new WeakMap();
|
|
103
|
+
constructor(opts = {}) {
|
|
104
|
+
super();
|
|
105
|
+
this.opts = opts;
|
|
106
|
+
let mathMode;
|
|
107
|
+
if (opts.mathMode !== void 0) mathMode = opts.mathMode;
|
|
108
|
+
else if (opts.math !== void 0) if (opts.math === 0 || opts.math === "always") mathMode = "always";
|
|
109
|
+
else if (opts.math === 1 || opts.math === "parens-division") mathMode = "parens-division";
|
|
110
|
+
else if (opts.math === 2 || opts.math === "parens" || opts.math === "strict") mathMode = "parens";
|
|
111
|
+
else mathMode = "parens";
|
|
112
|
+
else mathMode = lessPluginDefaults.mathMode;
|
|
113
|
+
this.mathMode = mathMode;
|
|
114
|
+
let unitMode;
|
|
115
|
+
if (opts.unitMode !== void 0) unitMode = opts.unitMode;
|
|
116
|
+
else if (opts.strictUnits === true) unitMode = "strict";
|
|
117
|
+
else unitMode = lessPluginDefaults.unitMode;
|
|
118
|
+
this.unitMode = unitMode;
|
|
119
|
+
this.equalityMode = opts.equalityMode ?? lessPluginDefaults.equalityMode;
|
|
120
|
+
this.leakyScope = opts.leakyScope ?? lessPluginDefaults.leakyScope;
|
|
121
|
+
this.bubbleRootAtRules = opts.bubbleRootAtRules ?? lessPluginDefaults.bubbleRootAtRules;
|
|
122
|
+
this.processImports = opts.processImports ?? lessPluginDefaults.processImports;
|
|
123
|
+
this.collapseNesting = opts.collapseNesting ?? lessPluginDefaults.collapseNesting;
|
|
124
|
+
}
|
|
125
|
+
transformUrl({ value, quoted, fromFilePath, entryFilePath }) {
|
|
126
|
+
let transformed;
|
|
127
|
+
if (isUrlRelative(value)) {
|
|
128
|
+
const rewriteUrls = this.opts.rewriteUrls;
|
|
129
|
+
const local = value.startsWith(".");
|
|
130
|
+
if (rewriteUrls !== "local" || local) {
|
|
131
|
+
const rebasesImportedUrl = rewriteUrls === true || rewriteUrls === "all" || rewriteUrls === "local" && local;
|
|
132
|
+
let rootpath = this.opts.rootpath ?? "";
|
|
133
|
+
if (!quoted) rootpath = escapeUnquotedUrlPath(rootpath);
|
|
134
|
+
if (rebasesImportedUrl && fromFilePath && entryFilePath) {
|
|
135
|
+
const relativeDirectory = node_path.default.relative(node_path.default.dirname(entryFilePath), node_path.default.dirname(fromFilePath));
|
|
136
|
+
if (relativeDirectory) rootpath += `${relativeDirectory.split(node_path.default.sep).join("/")}/`;
|
|
137
|
+
}
|
|
138
|
+
transformed = rewriteUrlPath(value, rootpath);
|
|
139
|
+
} else transformed = normalizeUrlPath(value);
|
|
140
|
+
} else transformed = normalizeUrlPath(value);
|
|
141
|
+
if (this.opts.urlArgs && !value.trimStart().toLowerCase().startsWith("data:")) {
|
|
142
|
+
const args = `${transformed.includes("?") ? "&" : "?"}${this.opts.urlArgs}`;
|
|
143
|
+
const fragment = transformed.indexOf("#");
|
|
144
|
+
transformed = fragment < 0 ? transformed + args : transformed.slice(0, fragment) + args + transformed.slice(fragment);
|
|
145
|
+
}
|
|
146
|
+
return transformed;
|
|
147
|
+
}
|
|
148
|
+
expandImport(importPath, currentDir) {
|
|
149
|
+
return (0, _jesscss_style_resolver.expandLessImportCandidates)(importPath);
|
|
150
|
+
}
|
|
151
|
+
setContext(context) {
|
|
152
|
+
if (context.documentContext?.plugin !== this) return;
|
|
153
|
+
if (context.opts.mathMode === void 0) context.setOption("mathMode", this.mathMode);
|
|
154
|
+
if (context.opts.unitMode === void 0) context.setOption("unitMode", this.unitMode);
|
|
155
|
+
if (context.opts.equalityMode === void 0) context.setOption("equalityMode", this.equalityMode);
|
|
156
|
+
if (context.opts.leakyScope === void 0) context.setOption("leakyScope", this.leakyScope);
|
|
157
|
+
if (context.opts.bubbleRootAtRules === void 0) context.setOption("bubbleRootAtRules", this.bubbleRootAtRules);
|
|
158
|
+
if (context.opts.processImports === void 0) context.setOption("processImports", this.processImports);
|
|
159
|
+
context.registerValueEvaluator(lessValueEvaluator);
|
|
160
|
+
let host = this.pluginHosts.get(context);
|
|
161
|
+
if (!host) {
|
|
162
|
+
host = new _jesscss_plugin_less_compat.LessApiBridge(this.opts.plugins ?? []).createPluginHost({ loadPluginModule: async ({ specifier, options }) => {
|
|
163
|
+
if (context.opts.disableScriptModules || context.opts.disablePluginRule) throw _jesscss_core.ERR.pluginLoadFailed({
|
|
164
|
+
meta: {
|
|
165
|
+
specifier,
|
|
166
|
+
reason: "script module execution is disabled by disableScriptModules"
|
|
167
|
+
},
|
|
168
|
+
reason: `"@plugin \\"${specifier}\\"" loads and executes a script module, which this compile disabled.`,
|
|
169
|
+
fix: "Remove the @plugin statement, or stop setting disableScriptModules for this compile."
|
|
170
|
+
});
|
|
171
|
+
return (await context.getPluginModule(specifier, options)).module;
|
|
172
|
+
} });
|
|
173
|
+
this.pluginHosts.set(context, host);
|
|
174
|
+
}
|
|
175
|
+
const existingHost = context.pluginHost;
|
|
176
|
+
const globalFns = [...existingHost?.globalFns ?? [], ...host.globalFns ?? []];
|
|
177
|
+
context.pluginHost = {
|
|
178
|
+
...existingHost,
|
|
179
|
+
...host,
|
|
180
|
+
...globalFns.length === 0 ? {} : { globalFns },
|
|
181
|
+
loadPlugin: host.loadPlugin || existingHost?.loadPlugin ? (request) => Promise.all([existingHost?.loadPlugin?.(request) ?? [], host.loadPlugin?.(request) ?? []]).then(([existingFns, hostFns]) => [...existingFns, ...hostFns]) : void 0,
|
|
182
|
+
invokeRawFunction: (fn, args, ctx) => host.invokeRawFunction?.(fn, args, ctx) ?? existingHost?.invokeRawFunction?.(fn, args, ctx)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
resolve(filePath, currentDir, searchPaths) {
|
|
186
|
+
const mapped = (Array.isArray(filePath) ? filePath : [filePath]).map((candidate) => {
|
|
187
|
+
if (candidate.startsWith("@less/test-import-module/")) {
|
|
188
|
+
const after = candidate.slice(25);
|
|
189
|
+
const marker = `${node_path.default.sep}packages${node_path.default.sep}test-data${node_path.default.sep}`;
|
|
190
|
+
const idx = currentDir.indexOf(marker);
|
|
191
|
+
if (idx !== -1) {
|
|
192
|
+
const packagesRoot = currentDir.slice(0, idx + `${node_path.default.sep}packages`.length);
|
|
193
|
+
return node_path.default.join(packagesRoot, "test-import-module", after);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return jsDelivrPackageSpecifier(candidate) ?? candidate;
|
|
197
|
+
});
|
|
198
|
+
const out = [...super.resolve(mapped, currentDir, searchPaths)];
|
|
199
|
+
const bases = [
|
|
200
|
+
currentDir,
|
|
201
|
+
...searchPaths,
|
|
202
|
+
process.cwd()
|
|
203
|
+
];
|
|
204
|
+
const looksBareSpecifier = (p) => !node_path.default.isAbsolute(p) && !p.startsWith("./") && !p.startsWith("../") && !p.startsWith("/") && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(p);
|
|
205
|
+
for (const candidate of mapped) {
|
|
206
|
+
if (!looksBareSpecifier(candidate)) continue;
|
|
207
|
+
for (const base of bases) {
|
|
208
|
+
const baseDir = node_path.default.isAbsolute(base) ? base : node_path.default.resolve(currentDir, base);
|
|
209
|
+
try {
|
|
210
|
+
const resolvedModule = (0, node_module.createRequire)(node_path.default.join(baseDir, "__jess_resolve__.js")).resolve(candidate);
|
|
211
|
+
if (!out.includes(resolvedModule)) out.push(resolvedModule);
|
|
212
|
+
break;
|
|
213
|
+
} catch {
|
|
214
|
+
try {
|
|
215
|
+
const resolvedModuleLess = (0, node_module.createRequire)(node_path.default.join(baseDir, "__jess_resolve__.js")).resolve(`${candidate}.less`);
|
|
216
|
+
if (!out.includes(resolvedModuleLess)) out.push(resolvedModuleLess);
|
|
217
|
+
break;
|
|
218
|
+
} catch {}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
canResolveImport(specifier) {
|
|
225
|
+
return jsDelivrPackageSpecifier(specifier) !== null;
|
|
226
|
+
}
|
|
227
|
+
safeParse(filePath, source, parseOptions) {
|
|
228
|
+
return (0, _jesscss_less_parser.safeParse)(filePath, source);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const lessPlugin = ((opts) => {
|
|
232
|
+
return new LessPlugin(opts);
|
|
233
|
+
});
|
|
234
|
+
//#endregion
|
|
235
|
+
exports.LessPlugin = LessPlugin;
|
|
236
|
+
exports.default = lessPlugin;
|
|
237
|
+
exports.lessPluginDefaults = lessPluginDefaults;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AbstractPlugin, Context, type UrlTransformRequest, type ISafeParseResult, type SafeParseOptions } from '@jesscss/core';
|
|
2
|
+
import type { EqualityMode, MathMode, UnitMode, LessOptions } from 'styles-config';
|
|
3
|
+
export type LessPluginOptions = LessOptions;
|
|
4
|
+
/**
|
|
5
|
+
* The Less plugin's default option values — the single source of truth for the
|
|
6
|
+
* v5 defaults. The `LessPlugin` constructor fills any unset option from here,
|
|
7
|
+
* and the `lessc` CLI imports the same object so its defaults can never drift
|
|
8
|
+
* from the engine's. Note `collapseNesting: false` — v5 preserves nesting by
|
|
9
|
+
* default (Less 4.x flattened; that is now an explicit opt-in).
|
|
10
|
+
*/
|
|
11
|
+
export declare const lessPluginDefaults: {
|
|
12
|
+
readonly mathMode: MathMode;
|
|
13
|
+
readonly unitMode: UnitMode;
|
|
14
|
+
readonly equalityMode: EqualityMode;
|
|
15
|
+
readonly leakyScope: true;
|
|
16
|
+
readonly bubbleRootAtRules: true;
|
|
17
|
+
readonly processImports: true;
|
|
18
|
+
readonly collapseNesting: false;
|
|
19
|
+
};
|
|
20
|
+
export declare class LessPlugin extends AbstractPlugin {
|
|
21
|
+
opts: LessPluginOptions;
|
|
22
|
+
name: string;
|
|
23
|
+
supportedExtensions: string[];
|
|
24
|
+
mathMode: MathMode;
|
|
25
|
+
unitMode: UnitMode;
|
|
26
|
+
equalityMode: EqualityMode;
|
|
27
|
+
leakyScope: boolean;
|
|
28
|
+
bubbleRootAtRules: boolean;
|
|
29
|
+
processImports: boolean;
|
|
30
|
+
collapseNesting: boolean;
|
|
31
|
+
private readonly pluginHosts;
|
|
32
|
+
constructor(opts?: LessPluginOptions);
|
|
33
|
+
transformUrl({ value, quoted, fromFilePath, entryFilePath }: UrlTransformRequest): string;
|
|
34
|
+
expandImport(importPath: string, currentDir: string): string[];
|
|
35
|
+
setContext(context: Context): void;
|
|
36
|
+
resolve(filePath: string | string[], currentDir: string, searchPaths: string[]): string[];
|
|
37
|
+
canResolveImport(specifier: string): boolean;
|
|
38
|
+
safeParse(filePath: string, source: string, parseOptions?: SafeParseOptions): ISafeParseResult;
|
|
39
|
+
}
|
|
40
|
+
export type { LessOptions } from 'styles-config';
|
|
41
|
+
declare const lessPlugin: (opts?: LessPluginOptions) => LessPlugin;
|
|
42
|
+
export default lessPlugin;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { AbstractPlugin, ERR, buildEvaluator } from "@jesscss/core";
|
|
3
|
+
import { makeLessRegistry } from "@jesscss/fns/less/registry";
|
|
4
|
+
import { LessApiBridge } from "@jesscss/plugin-less-compat";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { expandLessImportCandidates } from "@jesscss/style-resolver";
|
|
7
|
+
import { safeParse } from "@jesscss/less-parser";
|
|
8
|
+
//#region src/index.ts
|
|
9
|
+
/**
|
|
10
|
+
* The Less plugin's default option values — the single source of truth for the
|
|
11
|
+
* v5 defaults. The `LessPlugin` constructor fills any unset option from here,
|
|
12
|
+
* and the `lessc` CLI imports the same object so its defaults can never drift
|
|
13
|
+
* from the engine's. Note `collapseNesting: false` — v5 preserves nesting by
|
|
14
|
+
* default (Less 4.x flattened; that is now an explicit opt-in).
|
|
15
|
+
*/
|
|
16
|
+
const lessPluginDefaults = {
|
|
17
|
+
mathMode: "parens-division",
|
|
18
|
+
unitMode: "preserve",
|
|
19
|
+
equalityMode: "less",
|
|
20
|
+
leakyScope: true,
|
|
21
|
+
bubbleRootAtRules: true,
|
|
22
|
+
processImports: true,
|
|
23
|
+
collapseNesting: false
|
|
24
|
+
};
|
|
25
|
+
const lessValueEvaluator = buildEvaluator(makeLessRegistry());
|
|
26
|
+
/** Match Less's URL normalization without treating URL text as an import path. */
|
|
27
|
+
function normalizeUrlPath(url) {
|
|
28
|
+
const segments = url.split("/");
|
|
29
|
+
const normalized = [];
|
|
30
|
+
for (const segment of segments) {
|
|
31
|
+
if (segment === ".") continue;
|
|
32
|
+
if (segment === "..") {
|
|
33
|
+
if (normalized.length === 0 || normalized[normalized.length - 1] === "..") normalized.push(segment);
|
|
34
|
+
else normalized.pop();
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
normalized.push(segment);
|
|
38
|
+
}
|
|
39
|
+
return normalized.join("/");
|
|
40
|
+
}
|
|
41
|
+
function isUrlRelative(url) {
|
|
42
|
+
if (url.startsWith("/") || url.startsWith("#")) return false;
|
|
43
|
+
const colon = url.indexOf(":");
|
|
44
|
+
if (colon < 0) return true;
|
|
45
|
+
for (let index = 0; index < colon; index++) {
|
|
46
|
+
const code = url.charCodeAt(index);
|
|
47
|
+
if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122) && url[index] !== "-") return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function rewriteUrlPath(url, rootpath) {
|
|
52
|
+
const rewritten = normalizeUrlPath(rootpath + url);
|
|
53
|
+
return url.startsWith(".") && isUrlRelative(rootpath) && !rewritten.startsWith(".") ? `./${rewritten}` : rewritten;
|
|
54
|
+
}
|
|
55
|
+
function escapeUnquotedUrlPath(pathValue) {
|
|
56
|
+
let escaped = "";
|
|
57
|
+
for (const char of pathValue) escaped += char === "(" || char === ")" || char === "'" || char === "\"" || " \n\r\f".includes(char) ? `\\${char}` : char;
|
|
58
|
+
return escaped;
|
|
59
|
+
}
|
|
60
|
+
function jsDelivrPackageSpecifier(candidate) {
|
|
61
|
+
const absolute = candidate.match(/^https?:\/\/cdn\.jsdelivr\.net\/npm\/([^?#]+)(?:[?#].*)?$/i);
|
|
62
|
+
if (absolute?.[1]) return absolute[1];
|
|
63
|
+
return candidate.match(/^\/\/cdn\.jsdelivr\.net\/npm\/([^?#]+)(?:[?#].*)?$/i)?.[1] ?? null;
|
|
64
|
+
}
|
|
65
|
+
var LessPlugin = class extends AbstractPlugin {
|
|
66
|
+
name = "less";
|
|
67
|
+
supportedExtensions = [".less"];
|
|
68
|
+
mathMode;
|
|
69
|
+
unitMode;
|
|
70
|
+
equalityMode;
|
|
71
|
+
leakyScope;
|
|
72
|
+
bubbleRootAtRules;
|
|
73
|
+
processImports;
|
|
74
|
+
collapseNesting;
|
|
75
|
+
pluginHosts = /* @__PURE__ */ new WeakMap();
|
|
76
|
+
constructor(opts = {}) {
|
|
77
|
+
super();
|
|
78
|
+
this.opts = opts;
|
|
79
|
+
let mathMode;
|
|
80
|
+
if (opts.mathMode !== void 0) mathMode = opts.mathMode;
|
|
81
|
+
else if (opts.math !== void 0) if (opts.math === 0 || opts.math === "always") mathMode = "always";
|
|
82
|
+
else if (opts.math === 1 || opts.math === "parens-division") mathMode = "parens-division";
|
|
83
|
+
else if (opts.math === 2 || opts.math === "parens" || opts.math === "strict") mathMode = "parens";
|
|
84
|
+
else mathMode = "parens";
|
|
85
|
+
else mathMode = lessPluginDefaults.mathMode;
|
|
86
|
+
this.mathMode = mathMode;
|
|
87
|
+
let unitMode;
|
|
88
|
+
if (opts.unitMode !== void 0) unitMode = opts.unitMode;
|
|
89
|
+
else if (opts.strictUnits === true) unitMode = "strict";
|
|
90
|
+
else unitMode = lessPluginDefaults.unitMode;
|
|
91
|
+
this.unitMode = unitMode;
|
|
92
|
+
this.equalityMode = opts.equalityMode ?? lessPluginDefaults.equalityMode;
|
|
93
|
+
this.leakyScope = opts.leakyScope ?? lessPluginDefaults.leakyScope;
|
|
94
|
+
this.bubbleRootAtRules = opts.bubbleRootAtRules ?? lessPluginDefaults.bubbleRootAtRules;
|
|
95
|
+
this.processImports = opts.processImports ?? lessPluginDefaults.processImports;
|
|
96
|
+
this.collapseNesting = opts.collapseNesting ?? lessPluginDefaults.collapseNesting;
|
|
97
|
+
}
|
|
98
|
+
transformUrl({ value, quoted, fromFilePath, entryFilePath }) {
|
|
99
|
+
let transformed;
|
|
100
|
+
if (isUrlRelative(value)) {
|
|
101
|
+
const rewriteUrls = this.opts.rewriteUrls;
|
|
102
|
+
const local = value.startsWith(".");
|
|
103
|
+
if (rewriteUrls !== "local" || local) {
|
|
104
|
+
const rebasesImportedUrl = rewriteUrls === true || rewriteUrls === "all" || rewriteUrls === "local" && local;
|
|
105
|
+
let rootpath = this.opts.rootpath ?? "";
|
|
106
|
+
if (!quoted) rootpath = escapeUnquotedUrlPath(rootpath);
|
|
107
|
+
if (rebasesImportedUrl && fromFilePath && entryFilePath) {
|
|
108
|
+
const relativeDirectory = path.relative(path.dirname(entryFilePath), path.dirname(fromFilePath));
|
|
109
|
+
if (relativeDirectory) rootpath += `${relativeDirectory.split(path.sep).join("/")}/`;
|
|
110
|
+
}
|
|
111
|
+
transformed = rewriteUrlPath(value, rootpath);
|
|
112
|
+
} else transformed = normalizeUrlPath(value);
|
|
113
|
+
} else transformed = normalizeUrlPath(value);
|
|
114
|
+
if (this.opts.urlArgs && !value.trimStart().toLowerCase().startsWith("data:")) {
|
|
115
|
+
const args = `${transformed.includes("?") ? "&" : "?"}${this.opts.urlArgs}`;
|
|
116
|
+
const fragment = transformed.indexOf("#");
|
|
117
|
+
transformed = fragment < 0 ? transformed + args : transformed.slice(0, fragment) + args + transformed.slice(fragment);
|
|
118
|
+
}
|
|
119
|
+
return transformed;
|
|
120
|
+
}
|
|
121
|
+
expandImport(importPath, currentDir) {
|
|
122
|
+
return expandLessImportCandidates(importPath);
|
|
123
|
+
}
|
|
124
|
+
setContext(context) {
|
|
125
|
+
if (context.documentContext?.plugin !== this) return;
|
|
126
|
+
if (context.opts.mathMode === void 0) context.setOption("mathMode", this.mathMode);
|
|
127
|
+
if (context.opts.unitMode === void 0) context.setOption("unitMode", this.unitMode);
|
|
128
|
+
if (context.opts.equalityMode === void 0) context.setOption("equalityMode", this.equalityMode);
|
|
129
|
+
if (context.opts.leakyScope === void 0) context.setOption("leakyScope", this.leakyScope);
|
|
130
|
+
if (context.opts.bubbleRootAtRules === void 0) context.setOption("bubbleRootAtRules", this.bubbleRootAtRules);
|
|
131
|
+
if (context.opts.processImports === void 0) context.setOption("processImports", this.processImports);
|
|
132
|
+
context.registerValueEvaluator(lessValueEvaluator);
|
|
133
|
+
let host = this.pluginHosts.get(context);
|
|
134
|
+
if (!host) {
|
|
135
|
+
host = new LessApiBridge(this.opts.plugins ?? []).createPluginHost({ loadPluginModule: async ({ specifier, options }) => {
|
|
136
|
+
if (context.opts.disableScriptModules || context.opts.disablePluginRule) throw ERR.pluginLoadFailed({
|
|
137
|
+
meta: {
|
|
138
|
+
specifier,
|
|
139
|
+
reason: "script module execution is disabled by disableScriptModules"
|
|
140
|
+
},
|
|
141
|
+
reason: `"@plugin \\"${specifier}\\"" loads and executes a script module, which this compile disabled.`,
|
|
142
|
+
fix: "Remove the @plugin statement, or stop setting disableScriptModules for this compile."
|
|
143
|
+
});
|
|
144
|
+
return (await context.getPluginModule(specifier, options)).module;
|
|
145
|
+
} });
|
|
146
|
+
this.pluginHosts.set(context, host);
|
|
147
|
+
}
|
|
148
|
+
const existingHost = context.pluginHost;
|
|
149
|
+
const globalFns = [...existingHost?.globalFns ?? [], ...host.globalFns ?? []];
|
|
150
|
+
context.pluginHost = {
|
|
151
|
+
...existingHost,
|
|
152
|
+
...host,
|
|
153
|
+
...globalFns.length === 0 ? {} : { globalFns },
|
|
154
|
+
loadPlugin: host.loadPlugin || existingHost?.loadPlugin ? (request) => Promise.all([existingHost?.loadPlugin?.(request) ?? [], host.loadPlugin?.(request) ?? []]).then(([existingFns, hostFns]) => [...existingFns, ...hostFns]) : void 0,
|
|
155
|
+
invokeRawFunction: (fn, args, ctx) => host.invokeRawFunction?.(fn, args, ctx) ?? existingHost?.invokeRawFunction?.(fn, args, ctx)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
resolve(filePath, currentDir, searchPaths) {
|
|
159
|
+
const mapped = (Array.isArray(filePath) ? filePath : [filePath]).map((candidate) => {
|
|
160
|
+
if (candidate.startsWith("@less/test-import-module/")) {
|
|
161
|
+
const after = candidate.slice(25);
|
|
162
|
+
const marker = `${path.sep}packages${path.sep}test-data${path.sep}`;
|
|
163
|
+
const idx = currentDir.indexOf(marker);
|
|
164
|
+
if (idx !== -1) {
|
|
165
|
+
const packagesRoot = currentDir.slice(0, idx + `${path.sep}packages`.length);
|
|
166
|
+
return path.join(packagesRoot, "test-import-module", after);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return jsDelivrPackageSpecifier(candidate) ?? candidate;
|
|
170
|
+
});
|
|
171
|
+
const out = [...super.resolve(mapped, currentDir, searchPaths)];
|
|
172
|
+
const bases = [
|
|
173
|
+
currentDir,
|
|
174
|
+
...searchPaths,
|
|
175
|
+
process.cwd()
|
|
176
|
+
];
|
|
177
|
+
const looksBareSpecifier = (p) => !path.isAbsolute(p) && !p.startsWith("./") && !p.startsWith("../") && !p.startsWith("/") && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(p);
|
|
178
|
+
for (const candidate of mapped) {
|
|
179
|
+
if (!looksBareSpecifier(candidate)) continue;
|
|
180
|
+
for (const base of bases) {
|
|
181
|
+
const baseDir = path.isAbsolute(base) ? base : path.resolve(currentDir, base);
|
|
182
|
+
try {
|
|
183
|
+
const resolvedModule = createRequire(path.join(baseDir, "__jess_resolve__.js")).resolve(candidate);
|
|
184
|
+
if (!out.includes(resolvedModule)) out.push(resolvedModule);
|
|
185
|
+
break;
|
|
186
|
+
} catch {
|
|
187
|
+
try {
|
|
188
|
+
const resolvedModuleLess = createRequire(path.join(baseDir, "__jess_resolve__.js")).resolve(`${candidate}.less`);
|
|
189
|
+
if (!out.includes(resolvedModuleLess)) out.push(resolvedModuleLess);
|
|
190
|
+
break;
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
canResolveImport(specifier) {
|
|
198
|
+
return jsDelivrPackageSpecifier(specifier) !== null;
|
|
199
|
+
}
|
|
200
|
+
safeParse(filePath, source, parseOptions) {
|
|
201
|
+
return safeParse(filePath, source);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const lessPlugin = ((opts) => {
|
|
205
|
+
return new LessPlugin(opts);
|
|
206
|
+
});
|
|
207
|
+
//#endregion
|
|
208
|
+
export { LessPlugin, lessPlugin as default, lessPluginDefaults };
|
package/lib/options.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jesscss/plugin-less",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "A Less stylesheet engine for Jess",
|
|
8
|
+
"version": "2.0.0-alpha.10",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
11
|
+
},
|
|
12
|
+
"main": "lib/index.cjs",
|
|
13
|
+
"types": "lib/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./lib/index.d.ts",
|
|
17
|
+
"import": "./lib/index.js",
|
|
18
|
+
"require": "./lib/index.cjs"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"lib"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@jesscss/fns": "2.0.0-alpha.10",
|
|
27
|
+
"@jesscss/less-parser": "2.0.0-alpha.10",
|
|
28
|
+
"@jesscss/plugin-less-compat": "2.0.0-alpha.10",
|
|
29
|
+
"@jesscss/core": "2.0.0-alpha.10",
|
|
30
|
+
"@jesscss/style-resolver": "2.0.0-alpha.10",
|
|
31
|
+
"styles-config": "2.0.0-alpha.10"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {},
|
|
34
|
+
"author": "Matthew Dean <matthew-dean@users.noreply.github.com>",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/jesscss/jess/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/jesscss/jess#readme",
|
|
40
|
+
"module": "lib/index.js",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"ci": "pnpm build && pnpm test",
|
|
43
|
+
"build": "pnpm compile",
|
|
44
|
+
"compile": "tsdown --tsconfig tsconfig.build.json --no-dts && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
45
|
+
"test": "vitest --run --passWithNoTests",
|
|
46
|
+
"lint:fix": "eslint --fix '**/*.{js,ts}'",
|
|
47
|
+
"lint": "eslint '**/*.{js,ts}'"
|
|
48
|
+
}
|
|
49
|
+
}
|