@jesscss/plugin-less-compat 2.0.0-alpha.1
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 +139 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.js +10 -0
- package/lib/index.js.map +1 -0
- package/lib/less-compat-structures.d.ts +108 -0
- package/lib/less-compat-structures.js +519 -0
- package/lib/less-compat-structures.js.map +1 -0
- package/lib/nodes/at-rule.d.ts +6 -0
- package/lib/nodes/at-rule.js +72 -0
- package/lib/nodes/at-rule.js.map +1 -0
- package/lib/nodes/attribute-selector.d.ts +6 -0
- package/lib/nodes/attribute-selector.js +54 -0
- package/lib/nodes/attribute-selector.js.map +1 -0
- package/lib/nodes/call.d.ts +6 -0
- package/lib/nodes/call.js +83 -0
- package/lib/nodes/call.js.map +1 -0
- package/lib/nodes/color.d.ts +6 -0
- package/lib/nodes/color.js +57 -0
- package/lib/nodes/color.js.map +1 -0
- package/lib/nodes/combinator.d.ts +6 -0
- package/lib/nodes/combinator.js +34 -0
- package/lib/nodes/combinator.js.map +1 -0
- package/lib/nodes/comment.d.ts +6 -0
- package/lib/nodes/comment.js +41 -0
- package/lib/nodes/comment.js.map +1 -0
- package/lib/nodes/condition.d.ts +6 -0
- package/lib/nodes/condition.js +60 -0
- package/lib/nodes/condition.js.map +1 -0
- package/lib/nodes/declaration.d.ts +6 -0
- package/lib/nodes/declaration.js +81 -0
- package/lib/nodes/declaration.js.map +1 -0
- package/lib/nodes/dimension.d.ts +6 -0
- package/lib/nodes/dimension.js +47 -0
- package/lib/nodes/dimension.js.map +1 -0
- package/lib/nodes/expression.d.ts +6 -0
- package/lib/nodes/expression.js +44 -0
- package/lib/nodes/expression.js.map +1 -0
- package/lib/nodes/extend.d.ts +6 -0
- package/lib/nodes/extend.js +57 -0
- package/lib/nodes/extend.js.map +1 -0
- package/lib/nodes/import.d.ts +6 -0
- package/lib/nodes/import.js +63 -0
- package/lib/nodes/import.js.map +1 -0
- package/lib/nodes/index.d.ts +45 -0
- package/lib/nodes/index.js +308 -0
- package/lib/nodes/index.js.map +1 -0
- package/lib/nodes/keyword.d.ts +6 -0
- package/lib/nodes/keyword.js +36 -0
- package/lib/nodes/keyword.js.map +1 -0
- package/lib/nodes/list.d.ts +6 -0
- package/lib/nodes/list.js +150 -0
- package/lib/nodes/list.js.map +1 -0
- package/lib/nodes/mixin.d.ts +6 -0
- package/lib/nodes/mixin.js +62 -0
- package/lib/nodes/mixin.js.map +1 -0
- package/lib/nodes/negative.d.ts +6 -0
- package/lib/nodes/negative.js +42 -0
- package/lib/nodes/negative.js.map +1 -0
- package/lib/nodes/operation.d.ts +6 -0
- package/lib/nodes/operation.js +63 -0
- package/lib/nodes/operation.js.map +1 -0
- package/lib/nodes/paren.d.ts +6 -0
- package/lib/nodes/paren.js +42 -0
- package/lib/nodes/paren.js.map +1 -0
- package/lib/nodes/quoted.d.ts +6 -0
- package/lib/nodes/quoted.js +57 -0
- package/lib/nodes/quoted.js.map +1 -0
- package/lib/nodes/reference.d.ts +9 -0
- package/lib/nodes/reference.js +80 -0
- package/lib/nodes/reference.js.map +1 -0
- package/lib/nodes/ruleset.d.ts +6 -0
- package/lib/nodes/ruleset.js +108 -0
- package/lib/nodes/ruleset.js.map +1 -0
- package/lib/nodes/selector.d.ts +8 -0
- package/lib/nodes/selector.js +226 -0
- package/lib/nodes/selector.js.map +1 -0
- package/lib/nodes/sequence.d.ts +9 -0
- package/lib/nodes/sequence.js +75 -0
- package/lib/nodes/sequence.js.map +1 -0
- package/lib/nodes/url.d.ts +6 -0
- package/lib/nodes/url.js +42 -0
- package/lib/nodes/url.js.map +1 -0
- package/lib/nodes/var-declaration.d.ts +6 -0
- package/lib/nodes/var-declaration.js +60 -0
- package/lib/nodes/var-declaration.js.map +1 -0
- package/lib/plugin-utils.d.ts +20 -0
- package/lib/plugin-utils.js +100 -0
- package/lib/plugin-utils.js.map +1 -0
- package/lib/plugin.d.ts +92 -0
- package/lib/plugin.js +1027 -0
- package/lib/plugin.js.map +1 -0
- package/lib/transform/from-less.d.ts +30 -0
- package/lib/transform/from-less.js +170 -0
- package/lib/transform/from-less.js.map +1 -0
- package/lib/transform/index.d.ts +7 -0
- package/lib/transform/index.js +8 -0
- package/lib/transform/index.js.map +1 -0
- package/lib/transform/proxy.d.ts +32 -0
- package/lib/transform/proxy.js +138 -0
- package/lib/transform/proxy.js.map +1 -0
- package/lib/transform/to-less.d.ts +17 -0
- package/lib/transform/to-less.js +128 -0
- package/lib/transform/to-less.js.map +1 -0
- package/lib/transform/type-map.d.ts +27 -0
- package/lib/transform/type-map.js +105 -0
- package/lib/transform/type-map.js.map +1 -0
- package/lib/types.d.ts +33 -0
- package/lib/types.js +5 -0
- package/lib/types.js.map +1 -0
- package/package.json +56 -0
package/lib/plugin.js
ADDED
|
@@ -0,0 +1,1027 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { AbstractPlugin, Any, Declaration, Dimension, F_VISIBLE } from '@jesscss/core';
|
|
5
|
+
import { toLessNode, fromLessNode, fromLessPluginReturnValue } from './transform/index.js';
|
|
6
|
+
import { getJessNodeFromProxy } from './transform/proxy.js';
|
|
7
|
+
import { filterPlugins } from './plugin-utils.js';
|
|
8
|
+
import { LessVisitor as LessVisitorClass, LessPluginManager, LessTreeConstructors, createLessMock } from './less-compat-structures.js';
|
|
9
|
+
/** Global key set by @jesscss/plugin-js when loaded. @plugin scripts require plugin-js (Deno) to be present. */
|
|
10
|
+
const JESS_PLUGIN_JS_GLOBAL = '__JESS_PLUGIN_JS_AVAILABLE__';
|
|
11
|
+
function assertPluginJsPresent() {
|
|
12
|
+
if (typeof globalThis === 'undefined' || !globalThis[JESS_PLUGIN_JS_GLOBAL]) {
|
|
13
|
+
throw new Error('@plugin script execution requires @jesscss/plugin-js (scripts must be run via Deno). Install @jesscss/plugin-js.');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const isThenable = (v) => !!v && (typeof v === 'object' || typeof v === 'function') && typeof v.then === 'function';
|
|
17
|
+
/**
|
|
18
|
+
* Wrap a Less plugin function and add it to a Jess function registry.
|
|
19
|
+
* Used so that @plugin-loaded functions register into the Rules that contain the @plugin.
|
|
20
|
+
* Conversion of Less return values to Jess uses the shared fromLessPluginReturnValue.
|
|
21
|
+
*/
|
|
22
|
+
function addToJessRegistry(jessRegistry, name, func) {
|
|
23
|
+
if (!jessRegistry || typeof jessRegistry.add !== 'function') {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
name = name.toLowerCase();
|
|
28
|
+
const wrapped = function (...args) {
|
|
29
|
+
const maybeEvaldArgs = args.map((arg) => {
|
|
30
|
+
if (arg instanceof Any || arg instanceof Declaration || arg instanceof Dimension) {
|
|
31
|
+
// Fast path for common nodes that are safe to eval normally via .eval
|
|
32
|
+
}
|
|
33
|
+
if (arg instanceof Object && arg && typeof arg.eval === 'function' && arg.evaluated !== true) {
|
|
34
|
+
try {
|
|
35
|
+
return arg.eval(this);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return arg;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return arg;
|
|
42
|
+
});
|
|
43
|
+
const call = (finalArgs) => func.apply(this, finalArgs);
|
|
44
|
+
const maybeNeedsAwait = maybeEvaldArgs.some(isThenable);
|
|
45
|
+
const result = maybeNeedsAwait
|
|
46
|
+
? Promise.all(maybeEvaldArgs.map(a => isThenable(a) ? a : Promise.resolve(a))).then(call)
|
|
47
|
+
: call(maybeEvaldArgs);
|
|
48
|
+
const statementContext = this?.caller?.parent?.type === 'Rules';
|
|
49
|
+
const convertResult = (r) => fromLessPluginReturnValue(r, { statementContext });
|
|
50
|
+
return isThenable(result) ? result.then(convertResult) : convertResult(result);
|
|
51
|
+
};
|
|
52
|
+
Object.assign(wrapped, func);
|
|
53
|
+
jessRegistry.add(name, wrapped);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
void e;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Plugin that enables Less.js compatibility by transforming
|
|
61
|
+
* Jess nodes to Less-compatible format for visitor processing.
|
|
62
|
+
*/
|
|
63
|
+
export class LessCompatPlugin extends AbstractPlugin {
|
|
64
|
+
opts;
|
|
65
|
+
name = 'less-compat';
|
|
66
|
+
// Cache the visitor instance so it's reused across multiple calls
|
|
67
|
+
// This ensures that visitors added via @plugin are available for subsequent nodes
|
|
68
|
+
_cachedVisitor;
|
|
69
|
+
_lessPluginManager;
|
|
70
|
+
_currentFilePath;
|
|
71
|
+
_jessFunctionRegistry;
|
|
72
|
+
constructor(opts = {}) {
|
|
73
|
+
super();
|
|
74
|
+
this.opts = opts;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Return the visitor as a preEval visitor so it runs before evaluation.
|
|
78
|
+
* This ensures @plugin directives are processed early, allowing their visitors
|
|
79
|
+
* to run on subsequent nodes during the preEval phase.
|
|
80
|
+
*
|
|
81
|
+
* Less plugins can register visitors via:
|
|
82
|
+
* - addVisitor() - these will run during preEval (default)
|
|
83
|
+
* - addPreProcessor() - these will run during preEval
|
|
84
|
+
* - addPostProcessor() - these will run during postEval (after evaluation)
|
|
85
|
+
*/
|
|
86
|
+
get preEvalVisitor() {
|
|
87
|
+
// Cache the visitor instance so it's reused across multiple calls
|
|
88
|
+
// This ensures that visitors added via @plugin are available for subsequent nodes
|
|
89
|
+
if (!this._cachedVisitor) {
|
|
90
|
+
this._cachedVisitor = this.visitor;
|
|
91
|
+
}
|
|
92
|
+
return this._cachedVisitor;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Return postEval visitors from Less plugins that registered via addPostProcessor.
|
|
96
|
+
* These visitors will run after node.eval() completes.
|
|
97
|
+
*/
|
|
98
|
+
get postEvalVisitor() {
|
|
99
|
+
// Not used yet - post processors run via runPostProcessors()
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
runPostProcessors(css, extra = {}) {
|
|
103
|
+
const processors = this._lessPluginManager?.getPostProcessors() || [];
|
|
104
|
+
return processors.reduce((current, processor) => {
|
|
105
|
+
if (!processor || typeof processor.process !== 'function') {
|
|
106
|
+
return current;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const output = processor.process(current, extra);
|
|
110
|
+
return typeof output === 'string' ? output : current;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}, css);
|
|
116
|
+
}
|
|
117
|
+
setCurrentFilePath(filePath) {
|
|
118
|
+
this._currentFilePath = filePath;
|
|
119
|
+
}
|
|
120
|
+
setContext(context) {
|
|
121
|
+
try {
|
|
122
|
+
const root = context?.root;
|
|
123
|
+
if (root && typeof root.getRegistry === 'function') {
|
|
124
|
+
this._jessFunctionRegistry = root.getRegistry('function');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// ignore
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Filter and separate Less plugins from Jess plugins
|
|
133
|
+
* This allows mixed plugin arrays to be handled correctly
|
|
134
|
+
*
|
|
135
|
+
* @deprecated Use filterPlugins from './plugin-utils.js' instead
|
|
136
|
+
*/
|
|
137
|
+
static filterLessPlugins(plugins) {
|
|
138
|
+
return filterPlugins(plugins);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Return a visitor that wraps Less visitors
|
|
142
|
+
*
|
|
143
|
+
* This visitor intercepts each node, converts it to Less format,
|
|
144
|
+
* runs the Less visitors, and converts back if modified.
|
|
145
|
+
*/
|
|
146
|
+
get visitor() {
|
|
147
|
+
const cache = this.opts.cache !== false;
|
|
148
|
+
const cacheMap = cache ? new WeakMap() : undefined;
|
|
149
|
+
// Use our own Less.js-compatible structures (no dependency on actual Less.js library)
|
|
150
|
+
const LessVisitor = LessVisitorClass;
|
|
151
|
+
const LessPluginManagerClass = LessPluginManager;
|
|
152
|
+
// Collect all Less visitors
|
|
153
|
+
// Use an array that we'll iterate as an iterator to allow dynamic insertion
|
|
154
|
+
const lessVisitorInstances = [];
|
|
155
|
+
// References for @plugin processing - initialized early so visitor can access them
|
|
156
|
+
let pluginManagerRef = null;
|
|
157
|
+
let mockLessRef = null;
|
|
158
|
+
// Create an iterator function that allows dynamic visitor insertion
|
|
159
|
+
// This matches Less.js behavior where @plugin can add visitors during traversal
|
|
160
|
+
function* createVisitorIterator() {
|
|
161
|
+
let index = 0;
|
|
162
|
+
while (index < lessVisitorInstances.length) {
|
|
163
|
+
yield lessVisitorInstances[index];
|
|
164
|
+
index++;
|
|
165
|
+
// Check again after incrementing - new visitors might have been added
|
|
166
|
+
// This allows @plugin to insert visitors that will be processed on subsequent nodes
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Always create mockLess and pluginManager for @plugin processing
|
|
170
|
+
// Even if there are no initial plugins, @plugin directives might load plugins
|
|
171
|
+
let currentRealRegistry = this._jessFunctionRegistry;
|
|
172
|
+
const createFunctionRegistry = () => {
|
|
173
|
+
// Internal storage (matching Less.js _data structure)
|
|
174
|
+
const data = {};
|
|
175
|
+
let base = null;
|
|
176
|
+
const registry = {
|
|
177
|
+
get _data() {
|
|
178
|
+
return data;
|
|
179
|
+
},
|
|
180
|
+
get _base() {
|
|
181
|
+
return base;
|
|
182
|
+
},
|
|
183
|
+
set _base(v) {
|
|
184
|
+
base = v;
|
|
185
|
+
},
|
|
186
|
+
// Less.js API methods
|
|
187
|
+
add(name, func) {
|
|
188
|
+
name = name.toLowerCase();
|
|
189
|
+
data[name] = func;
|
|
190
|
+
if (currentRealRegistry) {
|
|
191
|
+
addToJessRegistry(currentRealRegistry, name, func);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
addMultiple(functions) {
|
|
195
|
+
Object.keys(functions).forEach((name) => {
|
|
196
|
+
this.add(name, functions[name]);
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
get(name) {
|
|
200
|
+
name = name.toLowerCase();
|
|
201
|
+
// Check local first
|
|
202
|
+
if (data[name]) {
|
|
203
|
+
return data[name];
|
|
204
|
+
}
|
|
205
|
+
// Then check base (for inheritance)
|
|
206
|
+
if (base) {
|
|
207
|
+
return base.get(name);
|
|
208
|
+
}
|
|
209
|
+
// If we have a real Jess registry, try that
|
|
210
|
+
if (currentRealRegistry) {
|
|
211
|
+
try {
|
|
212
|
+
return currentRealRegistry.get(name);
|
|
213
|
+
}
|
|
214
|
+
catch (_e) {
|
|
215
|
+
// Ignore errors
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
},
|
|
220
|
+
getLocalFunctions() {
|
|
221
|
+
return { ...data };
|
|
222
|
+
},
|
|
223
|
+
inherit() {
|
|
224
|
+
const child = createFunctionRegistry();
|
|
225
|
+
child._base = this;
|
|
226
|
+
return child;
|
|
227
|
+
},
|
|
228
|
+
create(base) {
|
|
229
|
+
const newRegistry = createFunctionRegistry();
|
|
230
|
+
newRegistry._base = base;
|
|
231
|
+
return newRegistry;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
// Wrap with Proxy to handle edge cases (like 'Call' constructor access)
|
|
235
|
+
return new Proxy(registry, {
|
|
236
|
+
get(target, prop) {
|
|
237
|
+
// First check if it's a method on the registry
|
|
238
|
+
if (prop in target) {
|
|
239
|
+
return target[prop];
|
|
240
|
+
}
|
|
241
|
+
// Handle Less.js tree constructors that plugins might access
|
|
242
|
+
// These are typically accessed as functionRegistry.Call, functionRegistry.Variable, etc.
|
|
243
|
+
if (typeof prop === 'string' && /^[A-Z]/.test(prop)) {
|
|
244
|
+
// Try to get the constructor from our Less.js-compatible structures
|
|
245
|
+
if (LessTreeConstructors[prop]) {
|
|
246
|
+
return LessTreeConstructors[prop];
|
|
247
|
+
}
|
|
248
|
+
// Fallback: return a no-op constructor that matches Less.js structure
|
|
249
|
+
return function (...args) {
|
|
250
|
+
return {
|
|
251
|
+
value: null,
|
|
252
|
+
type: prop,
|
|
253
|
+
name: args[0] || '',
|
|
254
|
+
args: args.slice(1) || [],
|
|
255
|
+
index: 0,
|
|
256
|
+
fileInfo: {},
|
|
257
|
+
accept: function (visitor) {
|
|
258
|
+
return visitor.visit(this);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// For any other property, return a no-op function
|
|
264
|
+
return function () {
|
|
265
|
+
return { value: null };
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
/**
|
|
271
|
+
* Create a mock function registry that forwards add/addMultiple/get to the given
|
|
272
|
+
* Jess registry. Used when loading @plugin scripts so functions register into the
|
|
273
|
+
* Rules that contain the @plugin directive.
|
|
274
|
+
*/
|
|
275
|
+
const createScopedFunctionRegistry = (jessRegistry) => {
|
|
276
|
+
const data = {};
|
|
277
|
+
const registry = {
|
|
278
|
+
add(name, func) {
|
|
279
|
+
name = name.toLowerCase();
|
|
280
|
+
data[name] = func;
|
|
281
|
+
addToJessRegistry(jessRegistry, name, func);
|
|
282
|
+
},
|
|
283
|
+
addMultiple(functions) {
|
|
284
|
+
Object.keys(functions).forEach((name) => {
|
|
285
|
+
this.add(name, functions[name]);
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
get(name) {
|
|
289
|
+
name = name.toLowerCase();
|
|
290
|
+
if (data[name]) {
|
|
291
|
+
return data[name];
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
return jessRegistry?.get?.(name);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
return new Proxy(registry, {
|
|
302
|
+
get(target, prop) {
|
|
303
|
+
if (prop in target) {
|
|
304
|
+
return target[prop];
|
|
305
|
+
}
|
|
306
|
+
if (typeof prop === 'string' && /^[A-Z]/.test(prop)) {
|
|
307
|
+
if (LessTreeConstructors[prop]) {
|
|
308
|
+
return LessTreeConstructors[prop];
|
|
309
|
+
}
|
|
310
|
+
return function (...args) {
|
|
311
|
+
return {
|
|
312
|
+
value: null,
|
|
313
|
+
type: prop,
|
|
314
|
+
name: args[0] || '',
|
|
315
|
+
args: args.slice(1) || [],
|
|
316
|
+
index: 0,
|
|
317
|
+
fileInfo: {},
|
|
318
|
+
accept: function (visitor) {
|
|
319
|
+
return visitor.visit(this);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return function () {
|
|
325
|
+
return { value: null };
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
const functionRegistry = createFunctionRegistry();
|
|
331
|
+
const mockLess = createLessMock(functionRegistry);
|
|
332
|
+
const pluginManager = new LessPluginManagerClass(mockLess, true);
|
|
333
|
+
this._lessPluginManager = pluginManager;
|
|
334
|
+
const loadPluginSource = (fullPath, registerPlugin, targetJessRegistry) => {
|
|
335
|
+
assertPluginJsPresent();
|
|
336
|
+
const contents = fs.readFileSync(fullPath, 'utf8');
|
|
337
|
+
const localModule = { exports: {} };
|
|
338
|
+
// When loading from an @plugin directive, pass a mock that registers to the Rules containing that @plugin
|
|
339
|
+
const functions = targetJessRegistry != null
|
|
340
|
+
? createScopedFunctionRegistry(targetJessRegistry)
|
|
341
|
+
: functionRegistry;
|
|
342
|
+
const loader = new Function('module', 'require', 'registerPlugin', 'functions', 'tree', 'less', 'fileInfo', contents);
|
|
343
|
+
loader(localModule, createRequire(fullPath), registerPlugin, functions, LessTreeConstructors, mockLess, { filename: fullPath });
|
|
344
|
+
return {
|
|
345
|
+
module: localModule.exports,
|
|
346
|
+
registered: null
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
const requirePluginFile = (fullPath, targetJessRegistry) => {
|
|
350
|
+
const registeredPlugins = [];
|
|
351
|
+
const registerPlugin = (plugin) => {
|
|
352
|
+
registeredPlugins.push(plugin);
|
|
353
|
+
};
|
|
354
|
+
const loaded = loadPluginSource(fullPath, registerPlugin, targetJessRegistry);
|
|
355
|
+
return {
|
|
356
|
+
module: loaded.module,
|
|
357
|
+
registered: registeredPlugins.length > 0 ? registeredPlugins[registeredPlugins.length - 1] : loaded.registered
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
// Initialize references for @plugin processing
|
|
361
|
+
pluginManagerRef = pluginManager;
|
|
362
|
+
mockLessRef = mockLess;
|
|
363
|
+
// Handle Less plugins - call their install() method
|
|
364
|
+
if (this.opts.plugins?.length) {
|
|
365
|
+
// Separate Less plugins from Jess plugins
|
|
366
|
+
// Jess plugins are ignored here - they should be handled by the main plugin system
|
|
367
|
+
const { lessPlugins } = filterPlugins(this.opts.plugins);
|
|
368
|
+
if (lessPlugins.length > 0) {
|
|
369
|
+
// Track visitors before installation
|
|
370
|
+
const visitorsBefore = pluginManager.visitors ? [...pluginManager.visitors] : [];
|
|
371
|
+
// Process plugins - handle functions that need to be instantiated
|
|
372
|
+
// In JavaScript, you can call any function with 'new', so we always try that first
|
|
373
|
+
const processedPlugins = [];
|
|
374
|
+
lessPlugins.forEach((plugin) => {
|
|
375
|
+
if (!plugin) {
|
|
376
|
+
// Skip undefined/null plugins
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// If plugin is a function, try calling it with 'new' first
|
|
380
|
+
// This handles both constructors and regular functions
|
|
381
|
+
if (typeof plugin === 'function') {
|
|
382
|
+
try {
|
|
383
|
+
const pluginInstance = new plugin({});
|
|
384
|
+
if (pluginInstance) {
|
|
385
|
+
processedPlugins.push(pluginInstance);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
// If new returns undefined/null, try calling as function
|
|
389
|
+
try {
|
|
390
|
+
const pluginInstance2 = plugin({});
|
|
391
|
+
if (pluginInstance2) {
|
|
392
|
+
processedPlugins.push(pluginInstance2);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// If both fail, use the function itself
|
|
396
|
+
processedPlugins.push(plugin);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
catch (_e2) {
|
|
400
|
+
// If function call fails, use the function itself
|
|
401
|
+
processedPlugins.push(plugin);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch (_e) {
|
|
406
|
+
// If 'new' fails, try calling as function
|
|
407
|
+
try {
|
|
408
|
+
const pluginInstance = plugin({});
|
|
409
|
+
if (pluginInstance) {
|
|
410
|
+
processedPlugins.push(pluginInstance);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
// If function call returns undefined, use the function itself
|
|
414
|
+
processedPlugins.push(plugin);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch (_e2) {
|
|
418
|
+
// If both fail, use the function itself
|
|
419
|
+
processedPlugins.push(plugin);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
// Not a function, use as-is
|
|
425
|
+
processedPlugins.push(plugin);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
processedPlugins.forEach((plugin) => {
|
|
429
|
+
if (!plugin) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
// CRITICAL: Many Less plugins (like autoprefix, clean-css) are BOTH plugins AND visitors
|
|
433
|
+
// We should ALWAYS try to wrap the plugin instance as a visitor FIRST
|
|
434
|
+
// because the plugin instance itself IS the visitor, regardless of install()
|
|
435
|
+
try {
|
|
436
|
+
const wrappedVisitor = new LessVisitor(plugin);
|
|
437
|
+
lessVisitorInstances.push(wrappedVisitor);
|
|
438
|
+
}
|
|
439
|
+
catch (e) {
|
|
440
|
+
// If wrapping fails, log for debugging but continue
|
|
441
|
+
// The error might be expected if the plugin doesn't have visit* methods
|
|
442
|
+
// We'll try other methods below
|
|
443
|
+
void e;
|
|
444
|
+
}
|
|
445
|
+
// Check if it's a plugin with install method (most common case)
|
|
446
|
+
if (typeof plugin.install === 'function') {
|
|
447
|
+
// Call the plugin's install method directly
|
|
448
|
+
// This might add additional visitors via pluginManager.addVisitor()
|
|
449
|
+
plugin.install(mockLess, pluginManager, mockLess.functions.functionRegistry);
|
|
450
|
+
}
|
|
451
|
+
else if (typeof plugin === 'function') {
|
|
452
|
+
// Some plugins are constructor functions (like autoprefix, CleanCSS)
|
|
453
|
+
// Check if the constructor's prototype has install
|
|
454
|
+
if (plugin.prototype && typeof plugin.prototype.install === 'function') {
|
|
455
|
+
// It's a constructor - instantiate it
|
|
456
|
+
try {
|
|
457
|
+
const pluginInstance = new plugin({});
|
|
458
|
+
if (pluginInstance && typeof pluginInstance.install === 'function') {
|
|
459
|
+
pluginInstance.install(mockLess, pluginManager, mockLess.functions.functionRegistry);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch (_e) {
|
|
463
|
+
// If constructor fails, try calling as a function
|
|
464
|
+
try {
|
|
465
|
+
const pluginInstance = plugin({});
|
|
466
|
+
if (pluginInstance && typeof pluginInstance.install === 'function') {
|
|
467
|
+
pluginInstance.install(mockLess, pluginManager, mockLess.functions.functionRegistry);
|
|
468
|
+
}
|
|
469
|
+
else if (pluginInstance) {
|
|
470
|
+
// Might be a visitor implementation directly
|
|
471
|
+
lessVisitorInstances.push(new LessVisitor(pluginInstance));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (_e2) {
|
|
475
|
+
// If all else fails, try wrapping it as a visitor
|
|
476
|
+
lessVisitorInstances.push(new LessVisitor(plugin));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
// Try calling as a function that returns a plugin instance
|
|
482
|
+
try {
|
|
483
|
+
const pluginInstance = plugin({});
|
|
484
|
+
if (pluginInstance && typeof pluginInstance.install === 'function') {
|
|
485
|
+
pluginInstance.install(mockLess, pluginManager, mockLess.functions.functionRegistry);
|
|
486
|
+
}
|
|
487
|
+
else if (pluginInstance) {
|
|
488
|
+
lessVisitorInstances.push(new LessVisitor(pluginInstance));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch (_e2) {
|
|
492
|
+
// If all else fails, try wrapping it as a visitor
|
|
493
|
+
lessVisitorInstances.push(new LessVisitor(plugin));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
// Plugin might be a visitor implementation directly
|
|
499
|
+
lessVisitorInstances.push(new LessVisitor(plugin));
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
// Collect visitors added by plugins via addVisitor
|
|
503
|
+
// Some plugins add visitors during install() via pluginManager.addVisitor()
|
|
504
|
+
if (pluginManager.visitors && pluginManager.visitors.length > visitorsBefore.length) {
|
|
505
|
+
const newVisitors = pluginManager.visitors.slice(visitorsBefore.length);
|
|
506
|
+
// Wrap raw visitors in LessVisitor instances
|
|
507
|
+
lessVisitorInstances.push(...newVisitors.map((v) => {
|
|
508
|
+
// If it's already a LessVisitor, use it directly
|
|
509
|
+
if (v instanceof LessVisitor) {
|
|
510
|
+
return v;
|
|
511
|
+
}
|
|
512
|
+
// Otherwise, wrap it
|
|
513
|
+
return new LessVisitor(v);
|
|
514
|
+
}));
|
|
515
|
+
}
|
|
516
|
+
// References are already set above (before the if block)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// Handle direct visitors (advanced usage)
|
|
520
|
+
/** @deprecated Less.js Visitor API - Direct visitor support for Less.js compatibility */
|
|
521
|
+
if (this.opts.visitors?.length) {
|
|
522
|
+
const lessVisitors = this.opts.visitors;
|
|
523
|
+
lessVisitors.forEach((visitorImpl) => {
|
|
524
|
+
// If it's already a Visitor instance, use it
|
|
525
|
+
if (visitorImpl instanceof LessVisitor) {
|
|
526
|
+
lessVisitorInstances.push(visitorImpl);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
// Otherwise, wrap the implementation in a Visitor
|
|
530
|
+
lessVisitorInstances.push(new LessVisitor(visitorImpl));
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
// Don't return undefined even if there are no initial visitors
|
|
535
|
+
// @plugin directives might add visitors during traversal
|
|
536
|
+
// We'll create a visitor that can handle @plugin processing
|
|
537
|
+
// If there are truly no visitors and no @plugin directives, the visitor will just pass through
|
|
538
|
+
// Track nodes currently being processed to prevent infinite loops
|
|
539
|
+
// This set persists for the entire visitor lifetime to prevent re-processing
|
|
540
|
+
const processing = new WeakSet();
|
|
541
|
+
// Track if we're currently inside a Less visitor traversal
|
|
542
|
+
// This prevents the plugin visitor from being triggered when visitArray calls visit()
|
|
543
|
+
let insideLessTraversal = false;
|
|
544
|
+
// Jess runs pre-eval visitors in two passes; ensure we only process each @plugin directive once.
|
|
545
|
+
const processedPluginDirectives = new WeakSet();
|
|
546
|
+
// Create a visitor object that implements the Visitor interface
|
|
547
|
+
const visitor = {
|
|
548
|
+
// Handle @plugin at-rules - these should be processed early (like Less.js preEval)
|
|
549
|
+
// In Less.js, @plugin is processed in preEval phase before the tree is evaluated
|
|
550
|
+
// This ensures plugins loaded via @plugin have their visitors available for subsequent nodes
|
|
551
|
+
atRule: (node, _ctx) => {
|
|
552
|
+
// Check if this is a @plugin directive
|
|
553
|
+
// In Less.js, @plugin syntax is: @plugin "plugin-name";
|
|
554
|
+
// Handle both AtRule (modern) and Directive (v2) node types
|
|
555
|
+
if (node && (node.type === 'AtRule' || node.type === 'Directive')) {
|
|
556
|
+
const atRuleName = node.value?.name;
|
|
557
|
+
let nameValue;
|
|
558
|
+
// Extract name value (could be string or node)
|
|
559
|
+
if (typeof atRuleName === 'string') {
|
|
560
|
+
nameValue = atRuleName;
|
|
561
|
+
}
|
|
562
|
+
else if (atRuleName?.value) {
|
|
563
|
+
nameValue = atRuleName.value;
|
|
564
|
+
}
|
|
565
|
+
else if (atRuleName?.type === 'Any' && atRuleName.value) {
|
|
566
|
+
nameValue = atRuleName.value;
|
|
567
|
+
}
|
|
568
|
+
// Check if this is a @plugin directive
|
|
569
|
+
// The name will be '@plugin' (with @ prefix) or 'plugin' (without)
|
|
570
|
+
// Less.js uses '@plugin' but we should handle both
|
|
571
|
+
const isPlugin = nameValue === 'plugin' || nameValue === '@plugin';
|
|
572
|
+
if (isPlugin) {
|
|
573
|
+
const pluginDirectiveNode = (getJessNodeFromProxy(node) || node);
|
|
574
|
+
if (processedPluginDirectives.has(pluginDirectiveNode)) {
|
|
575
|
+
return node;
|
|
576
|
+
}
|
|
577
|
+
processedPluginDirectives.add(pluginDirectiveNode);
|
|
578
|
+
const baseDir = this._currentFilePath ? path.dirname(this._currentFilePath) : undefined;
|
|
579
|
+
// Extract plugin path/name and options from prelude
|
|
580
|
+
// Handle both AtRule (value.prelude) and Directive (value.value) structures
|
|
581
|
+
// Less.js syntax: @plugin (options) "path"
|
|
582
|
+
const prelude = node.value?.prelude || node.value?.value;
|
|
583
|
+
let pluginPath;
|
|
584
|
+
let pluginOptions;
|
|
585
|
+
if (prelude) {
|
|
586
|
+
// Helper to extract string value from a node (Quoted, Url, or string)
|
|
587
|
+
const extractStringValue = (node) => {
|
|
588
|
+
if (!node) {
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
if (typeof node === 'string') {
|
|
592
|
+
return node;
|
|
593
|
+
}
|
|
594
|
+
if (node.type === 'Quoted' && node.value) {
|
|
595
|
+
// Quoted.value can be string | Any | Interpolated
|
|
596
|
+
if (typeof node.value === 'string') {
|
|
597
|
+
return node.value;
|
|
598
|
+
}
|
|
599
|
+
if (node.value?.value && typeof node.value.value === 'string') {
|
|
600
|
+
return node.value.value;
|
|
601
|
+
}
|
|
602
|
+
// Try valueOf() for Any nodes
|
|
603
|
+
if (typeof node.valueOf === 'function') {
|
|
604
|
+
const val = node.valueOf();
|
|
605
|
+
if (typeof val === 'string') {
|
|
606
|
+
return val;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (node.type === 'Url' && node.value) {
|
|
611
|
+
// Url.value can be Quoted, string, or other
|
|
612
|
+
if (typeof node.value === 'string') {
|
|
613
|
+
return node.value;
|
|
614
|
+
}
|
|
615
|
+
if (node.value.type === 'Quoted' && node.value.value) {
|
|
616
|
+
if (typeof node.value.value === 'string') {
|
|
617
|
+
return node.value.value;
|
|
618
|
+
}
|
|
619
|
+
if (node.value.value?.value && typeof node.value.value.value === 'string') {
|
|
620
|
+
return node.value.value.value;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
// Try valueOf() for Url nodes
|
|
624
|
+
if (typeof node.valueOf === 'function') {
|
|
625
|
+
const val = node.valueOf();
|
|
626
|
+
if (typeof val === 'string') {
|
|
627
|
+
return val;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return undefined;
|
|
632
|
+
};
|
|
633
|
+
// Prelude might contain options in parentheses followed by the plugin path
|
|
634
|
+
// Less.js syntax: @plugin (options) "path"
|
|
635
|
+
// The prelude might be a Sequence with options and path, or just the path
|
|
636
|
+
if (typeof prelude === 'string') {
|
|
637
|
+
pluginPath = prelude;
|
|
638
|
+
}
|
|
639
|
+
else if (prelude.type === 'Quoted' || prelude.type === 'Url') {
|
|
640
|
+
pluginPath = extractStringValue(prelude);
|
|
641
|
+
}
|
|
642
|
+
else if (prelude.type === 'Sequence' && Array.isArray(prelude.value)) {
|
|
643
|
+
// Sequence might contain: [options in parens, quoted path]
|
|
644
|
+
// Look for Quoted or Url (the path) and any preceding options
|
|
645
|
+
for (let i = 0; i < prelude.value.length; i++) {
|
|
646
|
+
const item = prelude.value[i];
|
|
647
|
+
if (item && (item.type === 'Quoted' || item.type === 'Url')) {
|
|
648
|
+
pluginPath = extractStringValue(item);
|
|
649
|
+
// Check if there's an options node before this (e.g., in parentheses)
|
|
650
|
+
if (i > 0) {
|
|
651
|
+
const prevItem = prelude.value[i - 1];
|
|
652
|
+
// Options might be in a Paren node or as a string
|
|
653
|
+
if (prevItem && prevItem.type === 'Paren' && prevItem.value) {
|
|
654
|
+
const optionsValue = prevItem.value.valueOf ? prevItem.value.valueOf() : prevItem.value.toString();
|
|
655
|
+
if (typeof optionsValue === 'string') {
|
|
656
|
+
pluginOptions = optionsValue.trim();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
else if (prevItem && typeof prevItem.valueOf === 'function') {
|
|
660
|
+
const optionsValue = prevItem.valueOf();
|
|
661
|
+
if (typeof optionsValue === 'string' && optionsValue.includes('=')) {
|
|
662
|
+
pluginOptions = optionsValue.trim();
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (pluginPath) {
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
else if (prelude.type === 'Expression' && prelude.value) {
|
|
673
|
+
// Expression might contain options and a Quoted or Url node
|
|
674
|
+
const values = Array.isArray(prelude.value) ? prelude.value : [prelude.value];
|
|
675
|
+
for (let i = 0; i < values.length; i++) {
|
|
676
|
+
const item = values[i];
|
|
677
|
+
if (item && (item.type === 'Quoted' || item.type === 'Url')) {
|
|
678
|
+
pluginPath = extractStringValue(item);
|
|
679
|
+
// Check for options before the path
|
|
680
|
+
if (i > 0) {
|
|
681
|
+
const prevItem = values[i - 1];
|
|
682
|
+
if (prevItem && prevItem.type === 'Paren' && prevItem.value) {
|
|
683
|
+
const optionsValue = prevItem.value.valueOf ? prevItem.value.valueOf() : prevItem.value.toString();
|
|
684
|
+
if (typeof optionsValue === 'string') {
|
|
685
|
+
pluginOptions = optionsValue.trim();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (pluginPath) {
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
else if (prelude.type === 'List' && prelude.value) {
|
|
696
|
+
// List might contain options and path
|
|
697
|
+
const items = Array.isArray(prelude.value) ? prelude.value : [prelude.value];
|
|
698
|
+
for (let i = 0; i < items.length; i++) {
|
|
699
|
+
const item = items[i];
|
|
700
|
+
if (item && (item.type === 'Quoted' || item.type === 'Url')) {
|
|
701
|
+
pluginPath = extractStringValue(item);
|
|
702
|
+
// Check for options before the path
|
|
703
|
+
if (i > 0) {
|
|
704
|
+
const prevItem = items[i - 1];
|
|
705
|
+
if (prevItem && prevItem.type === 'Paren' && prevItem.value) {
|
|
706
|
+
const optionsValue = prevItem.value.valueOf ? prevItem.value.valueOf() : prevItem.value.toString();
|
|
707
|
+
if (typeof optionsValue === 'string') {
|
|
708
|
+
pluginOptions = optionsValue.trim();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (pluginPath) {
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
else if (prelude.value && typeof prelude.value === 'string') {
|
|
719
|
+
// Fallback: direct string value
|
|
720
|
+
pluginPath = prelude.value;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (pluginPath) {
|
|
724
|
+
// Ensure pluginPath is a string and remove quotes if present
|
|
725
|
+
if (typeof pluginPath === 'string') {
|
|
726
|
+
pluginPath = pluginPath.replace(/^["']|["']$/g, '');
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
// If it's not a string, try to convert it
|
|
730
|
+
pluginPath = String(pluginPath).replace(/^["']|["']$/g, '');
|
|
731
|
+
}
|
|
732
|
+
const isExplicitLocalPath = pluginPath.startsWith('.')
|
|
733
|
+
|| pluginPath.startsWith('/')
|
|
734
|
+
|| pluginPath.includes('/')
|
|
735
|
+
|| pluginPath.includes(path.sep);
|
|
736
|
+
// Less.js will also resolve bare plugin names as local files relative to the current file,
|
|
737
|
+
// before falling back to npm package resolution. (Needed for test-data `plugin-transitive`.)
|
|
738
|
+
const localBasePath = (baseDir && !path.isAbsolute(pluginPath))
|
|
739
|
+
? path.resolve(baseDir, pluginPath)
|
|
740
|
+
: (path.isAbsolute(pluginPath) ? pluginPath : undefined);
|
|
741
|
+
let resolvedLocalPluginFile;
|
|
742
|
+
if (localBasePath) {
|
|
743
|
+
const candidates = [
|
|
744
|
+
localBasePath,
|
|
745
|
+
`${localBasePath}.js`,
|
|
746
|
+
`${localBasePath}.cjs`,
|
|
747
|
+
`${localBasePath}.mjs`
|
|
748
|
+
];
|
|
749
|
+
resolvedLocalPluginFile = candidates.find(p => fs.existsSync(p));
|
|
750
|
+
}
|
|
751
|
+
const isLocalPath = isExplicitLocalPath || !!resolvedLocalPluginFile;
|
|
752
|
+
// IMPORTANT: Less allows @plugin to be loaded multiple times in different scopes.
|
|
753
|
+
// Do NOT globally dedupe by pluginPath; this breaks Less's scoping rules.
|
|
754
|
+
if (pluginManagerRef && mockLessRef) {
|
|
755
|
+
try {
|
|
756
|
+
// Scope: register Less plugin functions into the nearest Rules scope,
|
|
757
|
+
// so nested @plugin shadowing matches Less.js behavior.
|
|
758
|
+
const scopeNode = getJessNodeFromProxy(node) || node;
|
|
759
|
+
let scopeRules = scopeNode;
|
|
760
|
+
while (scopeRules && scopeRules.type !== 'Rules') {
|
|
761
|
+
scopeRules = scopeRules.parent;
|
|
762
|
+
}
|
|
763
|
+
if (scopeRules && typeof scopeRules.getRegistry === 'function') {
|
|
764
|
+
// Root-level @plugin should behave as global registration (Less.js behavior),
|
|
765
|
+
// even when encountered in an imported file.
|
|
766
|
+
if (!scopeRules.parent) {
|
|
767
|
+
currentRealRegistry = this._jessFunctionRegistry;
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
currentRealRegistry = scopeRules.getRegistry('function');
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
currentRealRegistry = this._jessFunctionRegistry;
|
|
775
|
+
}
|
|
776
|
+
// Try to load plugin from registry (for testing and explicit registration)
|
|
777
|
+
let pluginInstance = null;
|
|
778
|
+
if (this.opts.pluginRegistry && this.opts.pluginRegistry[pluginPath]) {
|
|
779
|
+
const pluginFactory = this.opts.pluginRegistry[pluginPath];
|
|
780
|
+
pluginInstance = typeof pluginFactory === 'function' ? pluginFactory() : pluginFactory;
|
|
781
|
+
}
|
|
782
|
+
else if (isLocalPath && resolvedLocalPluginFile) {
|
|
783
|
+
const { module: pluginModule, registered } = requirePluginFile(resolvedLocalPluginFile, currentRealRegistry);
|
|
784
|
+
const PluginClass = pluginModule.default || pluginModule;
|
|
785
|
+
pluginInstance = registered || PluginClass;
|
|
786
|
+
}
|
|
787
|
+
else if (!isLocalPath && this.opts.autoLoadPlugins !== false) {
|
|
788
|
+
// Auto-load plugins from npm/node_modules (Less.js behavior)
|
|
789
|
+
// Expand plugin name to try multiple variations (e.g., "clean-css" -> ["clean-css", "less-plugin-clean-css"])
|
|
790
|
+
// Less.js tries prefixes: 'less-plugin-{name}' and '{name}'
|
|
791
|
+
const prefixes = ['less-plugin-', ''];
|
|
792
|
+
const packageNamesToTry = prefixes.map(prefix => prefix + pluginPath);
|
|
793
|
+
let loaded = false;
|
|
794
|
+
// Try to use node-modules plugin if available (synchronous resolution)
|
|
795
|
+
if (this.opts.nodeModulesPlugin) {
|
|
796
|
+
for (const packageName of packageNamesToTry) {
|
|
797
|
+
const resolvedPath = this.opts.nodeModulesPlugin.resolvePackage(packageName);
|
|
798
|
+
if (resolvedPath) {
|
|
799
|
+
try {
|
|
800
|
+
// Load the module using require
|
|
801
|
+
const { module: pluginModule, registered } = requirePluginFile(resolvedPath, currentRealRegistry);
|
|
802
|
+
// Get the plugin - could be default export or direct export
|
|
803
|
+
let PluginClass = pluginModule.default || pluginModule;
|
|
804
|
+
pluginInstance = registered || PluginClass;
|
|
805
|
+
loaded = true;
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
catch { }
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
// Fallback to direct require() if node-modules plugin not available or didn't find it
|
|
813
|
+
if (!loaded) {
|
|
814
|
+
for (const fullName of packageNamesToTry) {
|
|
815
|
+
try {
|
|
816
|
+
// Try to require the plugin from node_modules
|
|
817
|
+
// This uses Node's module resolution (similar to Less.js)
|
|
818
|
+
if (typeof require !== 'undefined') {
|
|
819
|
+
const { module: pluginModule, registered } = requirePluginFile(fullName, currentRealRegistry);
|
|
820
|
+
// Get the plugin - could be default export or direct export
|
|
821
|
+
let PluginClass = pluginModule.default || pluginModule;
|
|
822
|
+
// Less.js pattern: if plugin is a function, instantiate it with new (no args)
|
|
823
|
+
// This matches Less.js's validatePlugin() behavior (line 138)
|
|
824
|
+
pluginInstance = registered || PluginClass;
|
|
825
|
+
loaded = true;
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
catch (e) {
|
|
830
|
+
// Module not found - try next name
|
|
831
|
+
if (e.code !== 'MODULE_NOT_FOUND') {
|
|
832
|
+
// Some other error - continue trying alternative names.
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
// Auto-loading disabled - skip
|
|
840
|
+
}
|
|
841
|
+
// If we have a plugin instance, register it synchronously
|
|
842
|
+
// This allows the plugin's visitors to be added to the iterator
|
|
843
|
+
// and they will be processed on subsequent nodes (Less.js behavior)
|
|
844
|
+
if (pluginInstance) {
|
|
845
|
+
// Less.js pattern: if plugin is a constructor function, instantiate it with `new` (no args)
|
|
846
|
+
if (typeof pluginInstance === 'function') {
|
|
847
|
+
try {
|
|
848
|
+
pluginInstance = new pluginInstance();
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
// ignore, fall back to using the function itself
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// Set options if provided (Less.js pattern - see abstract-plugin-loader.js trySetOptions)
|
|
855
|
+
// Less.js calls setOptions both before and after addPlugin
|
|
856
|
+
if (pluginOptions && typeof pluginInstance.setOptions === 'function') {
|
|
857
|
+
try {
|
|
858
|
+
pluginInstance.setOptions(pluginOptions);
|
|
859
|
+
}
|
|
860
|
+
catch { }
|
|
861
|
+
}
|
|
862
|
+
const visitorsBefore = pluginManagerRef.visitors.length;
|
|
863
|
+
pluginManagerRef.registerPlugin(pluginInstance);
|
|
864
|
+
// Set options again after registration (Less.js pattern - plugin might have functions now)
|
|
865
|
+
if (pluginOptions && typeof pluginInstance.setOptions === 'function') {
|
|
866
|
+
try {
|
|
867
|
+
pluginInstance.setOptions(pluginOptions);
|
|
868
|
+
}
|
|
869
|
+
catch { }
|
|
870
|
+
}
|
|
871
|
+
// Collect any new visitors added by this plugin
|
|
872
|
+
// These will be automatically included in the iterator via .next() calls
|
|
873
|
+
if (pluginManagerRef.visitors.length > visitorsBefore) {
|
|
874
|
+
const newVisitors = pluginManagerRef.visitors.slice(visitorsBefore);
|
|
875
|
+
// Wrap raw visitors in LessVisitor instances
|
|
876
|
+
lessVisitorInstances.push(...newVisitors.map((v) => {
|
|
877
|
+
// If it's already a LessVisitor, use it directly
|
|
878
|
+
if (v instanceof LessVisitor) {
|
|
879
|
+
return v;
|
|
880
|
+
}
|
|
881
|
+
// Otherwise, wrap it
|
|
882
|
+
return new LessVisitor(v);
|
|
883
|
+
}));
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
catch (e) {
|
|
888
|
+
// Plugin loading failed - continue without it
|
|
889
|
+
void e;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
// After processing @plugin directive (whether pluginPath was found or not),
|
|
894
|
+
// mark it as invisible so it doesn't appear in output
|
|
895
|
+
// This must happen for ALL @plugin directives, not just ones that successfully load
|
|
896
|
+
const jessNode = getJessNodeFromProxy(node) || node;
|
|
897
|
+
if (jessNode && typeof jessNode.removeFlag === 'function') {
|
|
898
|
+
jessNode.removeFlag(F_VISIBLE);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
// Continue normal processing for all AtRules
|
|
903
|
+
return node;
|
|
904
|
+
},
|
|
905
|
+
visit: (node) => {
|
|
906
|
+
if (!node) {
|
|
907
|
+
return node;
|
|
908
|
+
}
|
|
909
|
+
// Get underlying Jess node if this is a Less proxy
|
|
910
|
+
// This allows us to check the processing WeakSet correctly
|
|
911
|
+
const jessNode = getJessNodeFromProxy(node) || node;
|
|
912
|
+
// CRITICAL: For AtRule nodes, we need to call atRule() FIRST to process @plugin directives
|
|
913
|
+
// before running Less visitors. Since our visitor is a plain object (not a class extending Visitor),
|
|
914
|
+
// visit() doesn't automatically call atRule() via _visit(). We need to call it manually.
|
|
915
|
+
if ((node.type === 'AtRule' || node.type === 'Directive') && visitor.atRule) {
|
|
916
|
+
// Call atRule() to process @plugin directives and add visitors
|
|
917
|
+
// This must happen before we run Less visitors, so that newly added visitors
|
|
918
|
+
// are available for subsequent nodes
|
|
919
|
+
const atRuleResult = visitor.atRule(node, undefined);
|
|
920
|
+
// Use the result if atRule() returned a different node (and it's not a symbol)
|
|
921
|
+
if (atRuleResult && typeof atRuleResult !== 'symbol' && atRuleResult !== node) {
|
|
922
|
+
node = atRuleResult;
|
|
923
|
+
// Update jessNode if node was replaced
|
|
924
|
+
const newJessNode = getJessNodeFromProxy(node) || node;
|
|
925
|
+
if (newJessNode !== jessNode) {
|
|
926
|
+
// If node was replaced, we need to update our reference
|
|
927
|
+
// But we'll continue with the original jessNode for processing tracking
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
// If we're already inside a Less visitor traversal, don't process again
|
|
932
|
+
// This prevents infinite loops when visitArray calls visit() on child nodes
|
|
933
|
+
if (insideLessTraversal) {
|
|
934
|
+
return node;
|
|
935
|
+
}
|
|
936
|
+
// Prevent recursion - if we're already processing this node, skip
|
|
937
|
+
// This prevents infinite loops when visitArray calls visit() on nodes that are already being processed
|
|
938
|
+
if (processing.has(jessNode)) {
|
|
939
|
+
return node;
|
|
940
|
+
}
|
|
941
|
+
// Mark as processing (keep in set for entire visitor lifetime)
|
|
942
|
+
processing.add(jessNode);
|
|
943
|
+
try {
|
|
944
|
+
// CRITICAL: For AtRule nodes, ensure @plugin is processed BEFORE running Less visitors
|
|
945
|
+
// In Jess's visitor system, atRule() is called as part of visit() via _visit(),
|
|
946
|
+
// but we need to ensure @plugin processing happens before Less visitors run.
|
|
947
|
+
// Since atRule() is a separate method that's called by the visitor system,
|
|
948
|
+
// we need to check if this is a @plugin directive and process it here too
|
|
949
|
+
// (as a fallback, in case atRule() hasn't been called yet).
|
|
950
|
+
// However, the normal flow should be: atRule() is called first, then visit().
|
|
951
|
+
// So we'll rely on atRule() to process @plugin, and visit() will run Less visitors.
|
|
952
|
+
// Convert Jess node to Less format (use underlying node if proxy)
|
|
953
|
+
const lessNode = toLessNode(jessNode, { cache: cacheMap });
|
|
954
|
+
// Mark that we're inside Less visitor traversal
|
|
955
|
+
// This prevents child nodes from triggering the plugin visitor again
|
|
956
|
+
insideLessTraversal = true;
|
|
957
|
+
try {
|
|
958
|
+
// Run all Less visitors using iterator pattern
|
|
959
|
+
// This allows new visitors to be added during iteration (e.g., from @plugin)
|
|
960
|
+
// and they will automatically be processed via .next() calls
|
|
961
|
+
let result = lessNode;
|
|
962
|
+
// Use iterator pattern - visitors can be inserted during iteration
|
|
963
|
+
// This matches Less.js behavior where @plugin-loaded visitors are inserted
|
|
964
|
+
// into the visitor chain and processed on subsequent nodes
|
|
965
|
+
// Only run visitors if we have any (including those added via @plugin)
|
|
966
|
+
if (lessVisitorInstances.length > 0) {
|
|
967
|
+
const visitorIterator = createVisitorIterator();
|
|
968
|
+
let iteratorResult = visitorIterator.next();
|
|
969
|
+
let iterationCount = 0;
|
|
970
|
+
while (!iteratorResult.done) {
|
|
971
|
+
iterationCount++;
|
|
972
|
+
if (iterationCount > 100) {
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
const lessVisitor = iteratorResult.value;
|
|
976
|
+
// Less Visitor.visit() calls the appropriate visit* method
|
|
977
|
+
// Less.js Visitor class handles isReplacing internally:
|
|
978
|
+
// - If isReplacing=false: return value is ignored, node modified in place, returns original node
|
|
979
|
+
// - If isReplacing=true: return value replaces node (undefined = remove)
|
|
980
|
+
// @deprecated Less.js Visitor API - Using Less.js visitor pattern for compatibility
|
|
981
|
+
//
|
|
982
|
+
// Handle Less.js v2 "Directive" nodes - if node is Directive type,
|
|
983
|
+
// LessVisitor.visit() will route to visitDirective() which maps to visitAtRule()
|
|
984
|
+
result = lessVisitor.visit(result);
|
|
985
|
+
// If result is undefined, a replacing visitor wants to remove this node
|
|
986
|
+
// (Non-replacing visitors can't return undefined - Less.js ignores their return value)
|
|
987
|
+
if (result === undefined) {
|
|
988
|
+
return undefined;
|
|
989
|
+
}
|
|
990
|
+
// Get next visitor - if new visitors were added during this iteration,
|
|
991
|
+
// they will be included in subsequent .next() calls
|
|
992
|
+
// This allows @plugin-loaded visitors to be processed immediately
|
|
993
|
+
iteratorResult = visitorIterator.next();
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
// If visitor returned a different node, convert back to Jess
|
|
997
|
+
if (result !== lessNode) {
|
|
998
|
+
const converted = fromLessNode(result, { cache: cacheMap });
|
|
999
|
+
// Return converted node if it's different, otherwise return original
|
|
1000
|
+
return converted !== jessNode ? converted : jessNode;
|
|
1001
|
+
}
|
|
1002
|
+
return jessNode;
|
|
1003
|
+
}
|
|
1004
|
+
finally {
|
|
1005
|
+
// Unmark when Less visitor traversal is complete
|
|
1006
|
+
insideLessTraversal = false;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
finally {
|
|
1010
|
+
// Keep node in processing set - don't delete it
|
|
1011
|
+
// This ensures we never process the same node twice during the entire visitor run
|
|
1012
|
+
// The WeakSet will be garbage collected when the visitor is done
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
return visitor;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Create a Less.js compatibility plugin
|
|
1021
|
+
*/
|
|
1022
|
+
const lessCompatPlugin = ((opts) => {
|
|
1023
|
+
return new LessCompatPlugin(opts);
|
|
1024
|
+
});
|
|
1025
|
+
export default lessCompatPlugin;
|
|
1026
|
+
export { lessCompatPlugin };
|
|
1027
|
+
//# sourceMappingURL=plugin.js.map
|