@jesscss/plugin-js 2.0.0-alpha.1 → 2.0.0-alpha.11

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.
@@ -0,0 +1,892 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ require("./index.cjs");
3
+ let node_url = require("node:url");
4
+ //#region src/runtime-worker.ts
5
+ const encoder = new TextEncoder();
6
+ const decoder = new TextDecoder();
7
+ const moduleCache = /* @__PURE__ */ new Map();
8
+ const lessPluginFunctionCache = /* @__PURE__ */ new Map();
9
+ const runtimeApi = Deno.args.includes("--runtime-api=less") ? "less" : "module";
10
+ /**
11
+ * A deliberate, attributable failure for a `less.tree` member the Jess
12
+ * compatibility shim cannot honour. Structural Less 4 nodes (rulesets,
13
+ * selectors, at-rules, imports, extends) have no representation on the value
14
+ * bridge that carries plugin results back to the engine, so exposing a
15
+ * look-alike constructor would silently produce wrong CSS. Throwing names the
16
+ * member and points at the supported surface instead.
17
+ */
18
+ var UnsupportedTreeNodeError = class extends Error {
19
+ constructor(name, reason) {
20
+ super(`Less @plugin: "tree.${name}" is not supported by the Jess less-compat shim.\n${reason}\nSupported members: Node, Anonymous, Keyword, Quoted, Dimension, Unit, Color, Expression, Value, Declaration, Variable, Property, Operation, Paren, Negative, Call, URL, Comment, Assignment, UnicodeDescriptor, Ruleset, DetachedRuleset, Mixin, Nil.`);
21
+ this.name = "UnsupportedTreeNodeError";
22
+ }
23
+ };
24
+ /**
25
+ * The Less 4 `Node` base. `find` is the member bootstrap-era plugins reach for
26
+ * through `tree.Variable.prototype.find(frames, cb)`; it is defined here (and
27
+ * inherited) exactly as less.js defines it, so the prototype lookup that those
28
+ * plugins perform resolves to the real helper rather than `undefined`.
29
+ */
30
+ var Node = class {
31
+ eval() {
32
+ return this;
33
+ }
34
+ find(obj, fun) {
35
+ for (let index = 0, result; index < obj.length; index++) {
36
+ result = fun.call(obj, obj[index]);
37
+ if (result) return result;
38
+ }
39
+ return null;
40
+ }
41
+ toCSS() {
42
+ return String(this.value ?? "");
43
+ }
44
+ toString() {
45
+ return this.toCSS();
46
+ }
47
+ valueOf() {
48
+ return this.value;
49
+ }
50
+ };
51
+ /**
52
+ * Less's unit model, reduced to the single fact plugin authors read or pass on:
53
+ * the unit string. `Dimension` accepts either a plain string or a `Unit`, which
54
+ * is how `new tree.Dimension(next.value - 0.02, next.unit)` keeps its unit.
55
+ */
56
+ var Unit = class Unit extends Node {
57
+ type = "Unit";
58
+ constructor(numerator) {
59
+ super();
60
+ this.numerator = typeof numerator === "string" ? numerator ? [numerator] : [] : Array.isArray(numerator) ? numerator.slice() : [];
61
+ this.denominator = [];
62
+ }
63
+ clone() {
64
+ return new Unit(this.numerator);
65
+ }
66
+ isEmpty() {
67
+ return this.numerator.length === 0 && this.denominator.length === 0;
68
+ }
69
+ toString() {
70
+ return this.numerator.join("*");
71
+ }
72
+ toCSS() {
73
+ return this.toString();
74
+ }
75
+ };
76
+ const unitOf = (unit) => {
77
+ if (unit instanceof Unit) return unit.clone();
78
+ return new Unit(unit == null ? "" : String(unit));
79
+ };
80
+ var Dimension = class Dimension extends Node {
81
+ type = "Dimension";
82
+ /**
83
+ * Mirrors less.js: the numeric part is `parseFloat`d off `value` and the unit
84
+ * comes only from the second argument. `new tree.Dimension('25%')` is
85
+ * therefore the unitless `25` — the same value less.js produces — not `25%`.
86
+ */
87
+ constructor(value, unit) {
88
+ super();
89
+ this.value = parseFloat(value);
90
+ if (Number.isNaN(this.value)) throw new Error(`Less @plugin: tree.Dimension received a non-numeric value (${JSON.stringify(value)}).`);
91
+ this._unit = unitOf(unit);
92
+ }
93
+ get unit() {
94
+ return this._unit.toString();
95
+ }
96
+ set unit(next) {
97
+ this._unit = unitOf(next);
98
+ }
99
+ valueOf() {
100
+ return this.unit ? `${this.value}${this.unit}` : this.value;
101
+ }
102
+ toString() {
103
+ return String(this.valueOf());
104
+ }
105
+ toCSS() {
106
+ return `${this.value}${this.unit}`;
107
+ }
108
+ /**
109
+ * less.js `Dimension.compare`: numbers are comparable when their units match
110
+ * or when either side is unitless. Anything else is `undefined`, which is the
111
+ * signal bootstrap's `valid-calc` plugin uses to emit a `calc()` instead.
112
+ */
113
+ compare(other) {
114
+ if (!(other instanceof Dimension)) return;
115
+ const a = this.unit;
116
+ const b = other.unit;
117
+ if (a !== b && a !== "" && b !== "") return;
118
+ if (other.value > this.value) return -1;
119
+ return other.value < this.value ? 1 : 0;
120
+ }
121
+ operate(context, op, other) {
122
+ if (!(other instanceof Dimension)) throw new Error(`Less @plugin: tree.Dimension.operate("${op}") needs a Dimension operand.`);
123
+ const a = this.unit;
124
+ const b = other.unit;
125
+ if ((op === "+" || op === "-") && a !== b && a !== "" && b !== "") throw new Error(`Less @plugin: incompatible units "${a}" and "${b}" in a "${op}" operation.`);
126
+ const value = op === "+" ? this.value + other.value : op === "-" ? this.value - other.value : op === "*" ? this.value * other.value : op === "/" ? this.value / other.value : void 0;
127
+ if (value === void 0) throw new Error(`Less @plugin: tree.Dimension.operate does not implement "${op}".`);
128
+ return new Dimension(value, a || b);
129
+ }
130
+ };
131
+ const clampByte = (n) => Math.min(255, Math.max(0, Math.round(n)));
132
+ const hexPair = (n) => clampByte(n).toString(16).padStart(2, "0");
133
+ var Color = class extends Node {
134
+ type = "Color";
135
+ /**
136
+ * less.js accepts an RGB triple OR a hex string WITHOUT the leading `#`
137
+ * (`new tree.Color(someColor.toCSS().substr(1))`), which is precisely the
138
+ * form bootstrap's `theme-color-level` plugin round-trips through.
139
+ */
140
+ constructor(rgb, alpha = 1) {
141
+ super();
142
+ if (Array.isArray(rgb)) {
143
+ this.rgb = rgb.slice(0, 3).map(clampByte);
144
+ this.alpha = typeof alpha === "number" ? alpha : 1;
145
+ return;
146
+ }
147
+ const text = String(rgb).replace(/^#/, "");
148
+ const expanded = text.length >= 6 ? [
149
+ text.slice(0, 2),
150
+ text.slice(2, 4),
151
+ text.slice(4, 6),
152
+ text.slice(6, 8)
153
+ ] : [
154
+ text[0] + text[0],
155
+ text[1] + text[1],
156
+ text[2] + text[2],
157
+ text[3] ? text[3] + text[3] : void 0
158
+ ];
159
+ const channels = expanded.slice(0, 3).map((pair) => parseInt(pair, 16));
160
+ if (channels.some(Number.isNaN)) throw new Error(`Less @plugin: tree.Color received an unparsable hex value (${JSON.stringify(rgb)}).`);
161
+ this.rgb = channels;
162
+ this.alpha = expanded[3] === void 0 ? typeof alpha === "number" ? alpha : 1 : parseInt(expanded[3], 16) / 255;
163
+ }
164
+ get value() {
165
+ return this.toCSS();
166
+ }
167
+ /** less.js renders an opaque colour as hex and a translucent one as `rgba()`. */
168
+ toCSS() {
169
+ const [r, g, b] = this.rgb;
170
+ return this.alpha === 1 ? `#${hexPair(r)}${hexPair(g)}${hexPair(b)}` : `rgba(${clampByte(r)}, ${clampByte(g)}, ${clampByte(b)}, ${this.alpha})`;
171
+ }
172
+ };
173
+ var Quoted = class extends Node {
174
+ type = "Quoted";
175
+ constructor(quote, value, escaped = false) {
176
+ super();
177
+ this.quote = quote === void 0 ? "\"" : String(quote);
178
+ this.value = value === void 0 || value === null ? "" : String(value);
179
+ this.escaped = escaped;
180
+ }
181
+ toCSS() {
182
+ return `${this.escaped ? "~" : ""}${this.quote}${this.value}${this.quote}`;
183
+ }
184
+ };
185
+ var Anonymous = class extends Node {
186
+ type = "Anonymous";
187
+ constructor(value) {
188
+ super();
189
+ this.value = value;
190
+ }
191
+ toCSS() {
192
+ return String(this.value);
193
+ }
194
+ };
195
+ var Keyword = class extends Anonymous {
196
+ type = "Keyword";
197
+ };
198
+ var Comment = class extends Anonymous {
199
+ type = "Comment";
200
+ };
201
+ var UnicodeDescriptor = class extends Anonymous {
202
+ type = "UnicodeDescriptor";
203
+ };
204
+ var Property = class extends Node {
205
+ type = "Property";
206
+ constructor(name) {
207
+ super();
208
+ this.name = name;
209
+ this.value = name;
210
+ }
211
+ toCSS() {
212
+ return String(this.name);
213
+ }
214
+ };
215
+ /**
216
+ * `tree.Variable` exists so plugins can (a) reach `Variable.prototype.find` —
217
+ * the `Node` helper every bootstrap-era scope lookup goes through — and (b)
218
+ * name a variable for the shim to resolve against the live evaluation frames.
219
+ */
220
+ var Variable = class extends Node {
221
+ type = "Variable";
222
+ constructor(name, index, currentFileInfo) {
223
+ super();
224
+ this.name = name;
225
+ this.index = index;
226
+ this.currentFileInfo = currentFileInfo;
227
+ }
228
+ eval(context) {
229
+ const frames = context?.frames ?? [];
230
+ for (const frame of frames) {
231
+ const hit = frame.variable?.(this.name);
232
+ if (hit?.value !== void 0) return hit.value;
233
+ }
234
+ throw new Error(`Less @plugin: variable "${this.name}" is undefined.`);
235
+ }
236
+ toCSS() {
237
+ return String(this.name);
238
+ }
239
+ };
240
+ var Declaration = class extends Node {
241
+ type = "Declaration";
242
+ constructor(name, value, important = "") {
243
+ super();
244
+ this.name = name;
245
+ this.value = value;
246
+ this.important = important;
247
+ }
248
+ eval() {
249
+ return this;
250
+ }
251
+ toCSS() {
252
+ const v = this.value?.toCSS ? this.value.toCSS() : String(this.value);
253
+ return `${this.name}: ${v}${this.important}`;
254
+ }
255
+ };
256
+ var Nil = class extends Node {
257
+ type = "Nil";
258
+ value = "";
259
+ };
260
+ /**
261
+ * A read-only view of a declaration list. Less plugins reach `ruleset.rules`
262
+ * (and `instanceof tree.Declaration` on each entry); nothing on the value
263
+ * bridge can express a ruleset's selectors or nesting, so the shim exposes the
264
+ * declaration list only and refuses anything that would need more.
265
+ */
266
+ var Ruleset = class extends Node {
267
+ type = "Ruleset";
268
+ constructor(selectors, rules) {
269
+ super();
270
+ if (selectors !== void 0 && selectors !== null && (!Array.isArray(selectors) || selectors.length > 0)) throw new UnsupportedTreeNodeError("Ruleset", "A selector-bearing ruleset cannot cross the plugin value boundary; only an anonymous declaration list can.");
271
+ this.selectors = [];
272
+ this.rules = Array.isArray(rules) ? rules : [];
273
+ }
274
+ };
275
+ var DetachedRuleset = class extends Node {
276
+ type = "DetachedRuleset";
277
+ constructor(ruleset) {
278
+ super();
279
+ this.ruleset = ruleset ?? new Ruleset(null, []);
280
+ }
281
+ };
282
+ var Mixin = class extends Node {
283
+ type = "Mixin";
284
+ name = new Nil();
285
+ args = new Nil();
286
+ ruleset;
287
+ constructor(rules) {
288
+ super();
289
+ this.ruleset = { rules };
290
+ }
291
+ };
292
+ var Value = class extends Node {
293
+ type = "Value";
294
+ constructor(value, separator = ",") {
295
+ super();
296
+ this.value = value;
297
+ this.separator = separator;
298
+ }
299
+ toCSS() {
300
+ const sep = this.separator === " " ? " " : `${this.separator} `;
301
+ return this.value.map((item) => item?.toCSS ? item.toCSS() : String(item)).join(sep);
302
+ }
303
+ };
304
+ var Expression = class extends Value {
305
+ type = "Expression";
306
+ constructor(value) {
307
+ super(value, " ");
308
+ }
309
+ };
310
+ var Paren = class extends Node {
311
+ type = "Paren";
312
+ constructor(value) {
313
+ super();
314
+ this.value = value;
315
+ }
316
+ toCSS() {
317
+ return `(${this.value?.toCSS ? this.value.toCSS() : String(this.value)})`;
318
+ }
319
+ };
320
+ var Negative = class extends Node {
321
+ type = "Negative";
322
+ constructor(value) {
323
+ super();
324
+ this.value = value;
325
+ }
326
+ toCSS() {
327
+ return `-${this.value?.toCSS ? this.value.toCSS() : String(this.value)}`;
328
+ }
329
+ };
330
+ const cssOf = (item) => item?.toCSS ? item.toCSS() : String(item);
331
+ var Operation = class extends Node {
332
+ type = "Operation";
333
+ constructor(op, operands) {
334
+ super();
335
+ this.op = String(op).trim();
336
+ this.operands = operands ?? [];
337
+ }
338
+ eval(context) {
339
+ const [left, right] = this.operands;
340
+ if (left?.operate) return left.operate(context, this.op, right);
341
+ throw new Error(`Less @plugin: tree.Operation cannot evaluate "${this.op}" on this operand.`);
342
+ }
343
+ toCSS() {
344
+ return this.operands.map(cssOf).join(` ${this.op} `);
345
+ }
346
+ };
347
+ var Call = class extends Node {
348
+ type = "Call";
349
+ constructor(name, args) {
350
+ super();
351
+ this.name = name;
352
+ this.args = args ?? [];
353
+ }
354
+ toCSS() {
355
+ return `${this.name}(${this.args.map(cssOf).join(", ")})`;
356
+ }
357
+ };
358
+ var URL = class extends Node {
359
+ type = "Url";
360
+ constructor(value) {
361
+ super();
362
+ this.value = value;
363
+ }
364
+ toCSS() {
365
+ return `url(${cssOf(this.value)})`;
366
+ }
367
+ };
368
+ var Assignment = class extends Node {
369
+ type = "Assignment";
370
+ constructor(key, value) {
371
+ super();
372
+ this.key = key;
373
+ this.value = value;
374
+ }
375
+ toCSS() {
376
+ return `${this.key}=${cssOf(this.value)}`;
377
+ }
378
+ };
379
+ /**
380
+ * Structural Less 4 nodes with no value-bridge representation. Each is exposed
381
+ * as a constructor that fails loudly and names itself, so a 4.x plugin reaching
382
+ * for one gets an attributable error instead of a silently wrong value.
383
+ */
384
+ const UNSUPPORTED_TREE_NODES = {
385
+ AtRule: "At-rules are statements, not values; a plugin function can only return a value.",
386
+ Attribute: "Attribute selectors are part of selector structure, which the value bridge does not carry.",
387
+ Combinator: "Combinators are part of selector structure, which the value bridge does not carry.",
388
+ Condition: "Guard conditions are evaluated by the engine, not reconstructed by a plugin.",
389
+ Element: "Selector elements are part of selector structure, which the value bridge does not carry.",
390
+ Extend: ":extend is resolved by the engine's extend pass, not by plugin values.",
391
+ Import: "@import is resolved during document loading, before plugin functions run.",
392
+ JavaScript: "Inline JavaScript evaluation was removed in Jess; use an @plugin function instead.",
393
+ Media: "@media is a statement, not a value; a plugin function can only return a value.",
394
+ MixinCall: "Mixin calls are dispatched by the engine, not constructed by plugin values.",
395
+ MixinDefinition: "Mixin definitions are statements, not values.",
396
+ NamespaceValue: "Namespace lookups are resolved by the engine's scope walk.",
397
+ Selector: "Selectors are part of statement structure, which the value bridge does not carry.",
398
+ VariableCall: "Detached-ruleset calls are dispatched by the engine, not by plugin values."
399
+ };
400
+ const treeNamespace = {
401
+ Anonymous,
402
+ Assignment,
403
+ Call,
404
+ Color,
405
+ Comment,
406
+ Declaration,
407
+ DetachedRuleset,
408
+ Dimension,
409
+ Expression,
410
+ Keyword,
411
+ Mixin,
412
+ Negative,
413
+ Nil,
414
+ Node,
415
+ Operation,
416
+ Paren,
417
+ Property,
418
+ Quoted,
419
+ Ruleset,
420
+ UnicodeDescriptor,
421
+ Unit,
422
+ URL,
423
+ Value,
424
+ Variable
425
+ };
426
+ for (const [name, reason] of Object.entries(UNSUPPORTED_TREE_NODES)) Object.defineProperty(treeNamespace, name, {
427
+ enumerable: true,
428
+ configurable: false,
429
+ get() {
430
+ throw new UnsupportedTreeNodeError(name, reason);
431
+ }
432
+ });
433
+ /**
434
+ * `less.logger` is captured by a plugin at LOAD time but must reach the host
435
+ * diagnostics of whichever CALL is in flight, so the facade's logger forwards to
436
+ * a per-invocation sink installed around each plugin function call.
437
+ */
438
+ let activeLogSink = null;
439
+ const lessFacade = {
440
+ tree: treeNamespace,
441
+ logger: {
442
+ warn: (message) => activeLogSink?.push({
443
+ level: "warn",
444
+ message: String(message)
445
+ }),
446
+ error: (message) => activeLogSink?.push({
447
+ level: "error",
448
+ message: String(message)
449
+ }),
450
+ info: (message) => activeLogSink?.push({
451
+ level: "info",
452
+ message: String(message)
453
+ }),
454
+ debug: (message) => activeLogSink?.push({
455
+ level: "debug",
456
+ message: String(message)
457
+ })
458
+ },
459
+ dimension(value, unit) {
460
+ return new Dimension(value, unit);
461
+ },
462
+ value(values, separator = ",") {
463
+ return new Value(values, separator);
464
+ },
465
+ anonymous(value) {
466
+ return new Anonymous(value);
467
+ },
468
+ color(rgb, alpha) {
469
+ return new Color(rgb, alpha);
470
+ },
471
+ quoted(quote, value, escaped = false) {
472
+ return new Quoted(quote, value, escaped);
473
+ }
474
+ };
475
+ if (runtimeApi === "less") {
476
+ globalThis.less ??= lessFacade;
477
+ globalThis.Less ??= lessFacade;
478
+ }
479
+ const isBridgeValue = (value) => value && typeof value === "object" && value.__jessBridge === true && typeof value.kind === "string";
480
+ const decodeBridgeValue = (value) => {
481
+ if (!isBridgeValue(value)) return value;
482
+ switch (value.kind) {
483
+ case "scalar": return value.value;
484
+ case "dimension": return new Dimension(value.value, value.unit ?? "");
485
+ case "color": return new Color(value.rgb, value.alpha ?? 1);
486
+ case "quoted": return new Quoted(value.quote ?? "\"", value.value, value.escaped === true);
487
+ case "anonymous": return new Anonymous(value.value);
488
+ case "list": return new Value((value.items ?? []).map(decodeBridgeValue), value.separator ?? ",");
489
+ case "expression": return new Expression((value.items ?? []).map(decodeBridgeValue));
490
+ case "mixin": return new Mixin((value.rules ?? []).map((decl) => {
491
+ const decoded = decodeBridgeValue(decl.value);
492
+ const cssText = decoded?.toCSS ? decoded.toCSS() : String(decoded);
493
+ return new Declaration(decl.name, new Anonymous(cssText));
494
+ }));
495
+ default: return value;
496
+ }
497
+ };
498
+ const encodeBridgeChildValue = (value) => {
499
+ const encoded = encodeBridgeValue(value);
500
+ if (isBridgeValue(encoded)) return encoded;
501
+ return {
502
+ __jessBridge: true,
503
+ kind: "scalar",
504
+ value: typeof encoded === "string" || typeof encoded === "number" || typeof encoded === "boolean" ? encoded : String(encoded)
505
+ };
506
+ };
507
+ const encodeBridgeValue = (value) => {
508
+ if (value instanceof Dimension) return {
509
+ __jessBridge: true,
510
+ kind: "dimension",
511
+ value: value.value,
512
+ unit: value.unit || void 0
513
+ };
514
+ if (value instanceof Color) return {
515
+ __jessBridge: true,
516
+ kind: "color",
517
+ rgb: value.rgb,
518
+ alpha: value.alpha
519
+ };
520
+ if (value instanceof Quoted) return value.quote === "" ? {
521
+ __jessBridge: true,
522
+ kind: "anonymous",
523
+ value: String(value.value)
524
+ } : {
525
+ __jessBridge: true,
526
+ kind: "quoted",
527
+ value: String(value.value),
528
+ quote: value.quote,
529
+ escaped: value.escaped
530
+ };
531
+ if (value instanceof Keyword || value instanceof Anonymous) return {
532
+ __jessBridge: true,
533
+ kind: "anonymous",
534
+ value: String(value.value)
535
+ };
536
+ if (value instanceof Expression) return {
537
+ __jessBridge: true,
538
+ kind: "expression",
539
+ items: value.value.map(encodeBridgeChildValue)
540
+ };
541
+ if (value instanceof Value) return {
542
+ __jessBridge: true,
543
+ kind: "list",
544
+ items: value.value.map(encodeBridgeChildValue),
545
+ separator: value.separator === "/" || value.separator === ";" ? value.separator : ","
546
+ };
547
+ if (value instanceof Mixin || value instanceof Ruleset || value instanceof DetachedRuleset) return {
548
+ __jessBridge: true,
549
+ kind: "mixin",
550
+ rules: (value instanceof Mixin ? value.ruleset.rules : value instanceof DetachedRuleset ? value.ruleset?.rules ?? [] : value.rules).filter((rule) => rule instanceof Declaration && typeof rule.name === "string").map((rule) => ({
551
+ name: rule.name,
552
+ value: encodeBridgeChildValue(rule.value)
553
+ }))
554
+ };
555
+ if (value instanceof Node) return {
556
+ __jessBridge: true,
557
+ kind: "anonymous",
558
+ value: value.toCSS()
559
+ };
560
+ return value;
561
+ };
562
+ const isJsonValue = (value) => {
563
+ try {
564
+ JSON.stringify(value);
565
+ return true;
566
+ } catch {
567
+ return false;
568
+ }
569
+ };
570
+ const send = (payload) => {
571
+ Deno.stdout.writeSync(encoder.encode(`${JSON.stringify(payload)}\n`));
572
+ };
573
+ const loadModule = async (modulePath) => {
574
+ let mod = moduleCache.get(modulePath);
575
+ if (!mod) {
576
+ mod = await import((0, node_url.pathToFileURL)(modulePath).href);
577
+ moduleCache.set(modulePath, mod);
578
+ }
579
+ const exports = [];
580
+ for (const [name, value] of Object.entries(mod)) {
581
+ if (typeof value === "function") {
582
+ exports.push({
583
+ name,
584
+ kind: "function"
585
+ });
586
+ continue;
587
+ }
588
+ if (isJsonValue(value)) exports.push({
589
+ name,
590
+ kind: "value",
591
+ value
592
+ });
593
+ }
594
+ return exports;
595
+ };
596
+ const createLegacyLessPluginRuntime = (modulePath, options) => {
597
+ const localFunctions = /* @__PURE__ */ new Map();
598
+ const functions = {
599
+ add(name, fn) {
600
+ if (typeof name === "string" && typeof fn === "function") localFunctions.set(name.toLowerCase(), fn);
601
+ },
602
+ addMultiple(items) {
603
+ for (const [name, fn] of Object.entries(items ?? {})) this.add(name, fn);
604
+ },
605
+ get(name) {
606
+ return localFunctions.get(String(name).toLowerCase());
607
+ }
608
+ };
609
+ const manager = {
610
+ visitors: [],
611
+ addVisitor(visitor) {
612
+ this.visitors.push(visitor);
613
+ },
614
+ addPreProcessor() {},
615
+ addPostProcessor() {},
616
+ registerPlugin(plugin) {
617
+ installPlugin(plugin);
618
+ }
619
+ };
620
+ const less = {
621
+ ...lessFacade,
622
+ functions: { functionRegistry: functions }
623
+ };
624
+ const installPlugin = (plugin) => {
625
+ if (!plugin) return;
626
+ const candidate = typeof plugin === "function" ? (() => {
627
+ try {
628
+ return new plugin();
629
+ } catch {
630
+ return plugin;
631
+ }
632
+ })() : plugin;
633
+ if (candidate && options != null && typeof candidate.setOptions === "function") candidate.setOptions(options);
634
+ if (candidate && typeof candidate.install === "function") candidate.install(less, manager, functions);
635
+ if (candidate && options != null && typeof candidate.setOptions === "function") candidate.setOptions(options);
636
+ if (candidate && typeof candidate.use === "function") candidate.use(less);
637
+ if (candidate && typeof candidate.eval === "function") candidate.eval(less);
638
+ };
639
+ const require = (specifier) => {
640
+ throw new Error(`Less @plugin require("${specifier}") is not supported in the Deno sandbox yet.`);
641
+ };
642
+ const registerPlugin = (plugin) => {
643
+ installPlugin(plugin);
644
+ };
645
+ return {
646
+ exports: {},
647
+ functions,
648
+ localFunctions,
649
+ manager,
650
+ require,
651
+ registerPlugin,
652
+ fileInfo: { filename: modulePath }
653
+ };
654
+ };
655
+ const lessPluginCacheKey = (modulePath, options) => `${modulePath}\u0000${options ?? ""}`;
656
+ const loadLessPlugin = async (modulePath, options = null) => {
657
+ const cacheKey = lessPluginCacheKey(modulePath, options);
658
+ let functions = lessPluginFunctionCache.get(cacheKey);
659
+ if (functions) return Array.from(functions.keys());
660
+ const source = await Deno.readTextFile(modulePath);
661
+ const runtime = createLegacyLessPluginRuntime(modulePath, options);
662
+ const module = { exports: runtime.exports };
663
+ new Function("module", "exports", "require", "registerPlugin", "functions", "tree", "less", "fileInfo", "process", source)(module, module.exports, runtime.require, runtime.registerPlugin, runtime.functions, lessFacade.tree, {
664
+ ...lessFacade,
665
+ functions: { functionRegistry: runtime.functions }
666
+ }, runtime.fileInfo, void 0);
667
+ const exported = module.exports?.default ?? module.exports;
668
+ if (typeof exported === "function" || exported && typeof exported.install === "function") runtime.registerPlugin(exported);
669
+ functions = runtime.localFunctions;
670
+ lessPluginFunctionCache.set(cacheKey, functions);
671
+ return Array.from(functions.keys());
672
+ };
673
+ /**
674
+ * The sandbox cannot call back into the compiler synchronously, but a Less 4
675
+ * plugin body reads scope (`frames[i].variable('@x')`) and built-in functions
676
+ * (`functionRegistry.get('mix')`) synchronously. Rather than eagerly shipping
677
+ * the whole variable scope on every call, an unsatisfied read raises this
678
+ * signal: the worker reports exactly what it needs, the host resolves it
679
+ * against the LIVE evaluation frame, and the call is replayed with the answer
680
+ * in hand. The host remembers which names a given function asked for, so after
681
+ * the first call the needed facts travel with the request and no replay occurs.
682
+ */
683
+ var HostFactNeeded = class {
684
+ constructor(need) {
685
+ this.need = need;
686
+ }
687
+ };
688
+ const hostCallKey = (name, args) => `${String(name).toLowerCase()}${JSON.stringify(args)}`;
689
+ /**
690
+ * Builds the `this` a Less 4 plugin function body expects: `this.context` with
691
+ * `frames` / `importantScope` / `pluginManager`, plus `this.currentFileInfo`.
692
+ * Without this the body's `this` is the sandbox global and every `this.context`
693
+ * read is `undefined`.
694
+ */
695
+ const createPluginCallContext = (facts, logs) => {
696
+ const importantScope = [{ important: "" }];
697
+ const frame = {
698
+ variable(name) {
699
+ const key = String(name);
700
+ if (!Object.prototype.hasOwnProperty.call(facts.vars, key)) throw new HostFactNeeded({
701
+ kind: "variable",
702
+ name: key
703
+ });
704
+ const entry = facts.vars[key];
705
+ if (entry === null || entry === void 0) return null;
706
+ return {
707
+ value: decodeBridgeValue(entry.value),
708
+ important: entry.important ? "!important" : ""
709
+ };
710
+ },
711
+ property() {
712
+ return null;
713
+ },
714
+ functionRegistry: null
715
+ };
716
+ const functionRegistry = {
717
+ get(name) {
718
+ const fnName = String(name);
719
+ return (...callArgs) => {
720
+ const encoded = callArgs.map(encodeBridgeChildValue);
721
+ const key = hostCallKey(fnName, encoded);
722
+ if (!Object.prototype.hasOwnProperty.call(facts.calls, key)) throw new HostFactNeeded({
723
+ kind: "call",
724
+ name: fnName,
725
+ args: encoded,
726
+ key
727
+ });
728
+ const answer = facts.calls[key];
729
+ if (answer === null || answer === void 0) throw new Error(`Less @plugin: built-in function "${fnName}" could not be evaluated with these arguments.`);
730
+ return decodeBridgeValue(answer);
731
+ };
732
+ },
733
+ add() {
734
+ throw new Error("Less @plugin: functions cannot be registered from inside a function body.");
735
+ }
736
+ };
737
+ const pluginManager = {
738
+ less: {
739
+ ...lessFacade,
740
+ functions: { functionRegistry }
741
+ },
742
+ getPreProcessors: () => [],
743
+ getPostProcessors: () => [],
744
+ getVisitors: () => [],
745
+ getFileManagers: () => []
746
+ };
747
+ return {
748
+ context: {
749
+ frames: [frame],
750
+ importantScope,
751
+ pluginManager,
752
+ compress: false,
753
+ numPrecision: 8
754
+ },
755
+ currentFileInfo: facts.fileInfo ?? {
756
+ filename: "",
757
+ entryPath: ""
758
+ },
759
+ index: 0,
760
+ importantScope,
761
+ pluginManager
762
+ };
763
+ };
764
+ const invokeLessPluginFunction = async (modulePath, functionName, args, options = null, facts = null) => {
765
+ const cacheKey = lessPluginCacheKey(modulePath, options);
766
+ let functions = lessPluginFunctionCache.get(cacheKey);
767
+ if (!functions) {
768
+ await loadLessPlugin(modulePath, options);
769
+ functions = lessPluginFunctionCache.get(cacheKey);
770
+ }
771
+ const fn = functions?.get(String(functionName).toLowerCase());
772
+ if (typeof fn !== "function") throw new Error(`Less @plugin function "${functionName}" is not registered.`);
773
+ const logs = [];
774
+ const callContext = createPluginCallContext(facts ?? {
775
+ vars: {},
776
+ calls: {},
777
+ fileInfo: null
778
+ }, logs);
779
+ const previousSink = activeLogSink;
780
+ activeLogSink = logs;
781
+ let raw;
782
+ try {
783
+ raw = await fn.apply(callContext, args.map(decodeBridgeValue));
784
+ } catch (err) {
785
+ if (err instanceof HostFactNeeded) return {
786
+ need: err.need,
787
+ logs
788
+ };
789
+ throw err;
790
+ } finally {
791
+ activeLogSink = previousSink;
792
+ }
793
+ const result = encodeBridgeValue(raw);
794
+ if (!isJsonValue(result)) throw new Error(`Result for Less @plugin function "${functionName}" is not JSON-serializable.`);
795
+ return {
796
+ value: result,
797
+ logs,
798
+ important: callContext.importantScope.some((entry) => Boolean(entry?.important))
799
+ };
800
+ };
801
+ const invokeExport = async (modulePath, exportName, args) => {
802
+ const mod = moduleCache.get(modulePath) ?? await import((0, node_url.pathToFileURL)(modulePath).href);
803
+ moduleCache.set(modulePath, mod);
804
+ const target = mod?.[exportName];
805
+ if (typeof target !== "function") throw new Error(`Export "${exportName}" is not callable.`);
806
+ const result = encodeBridgeValue(await target(...args.map(decodeBridgeValue)));
807
+ if (!isJsonValue(result)) throw new Error(`Result for "${exportName}" is not JSON-serializable.`);
808
+ return result;
809
+ };
810
+ const computeResponse = async (req) => {
811
+ try {
812
+ if (req.type === "load") {
813
+ const exports = await loadModule(req.modulePath);
814
+ return {
815
+ id: req.id,
816
+ ok: true,
817
+ exports
818
+ };
819
+ }
820
+ if (req.type === "invoke") {
821
+ const value = await invokeExport(req.modulePath, req.exportName, req.args ?? []);
822
+ return {
823
+ id: req.id,
824
+ ok: true,
825
+ value
826
+ };
827
+ }
828
+ if (req.type === "loadLessPlugin") {
829
+ const functions = await loadLessPlugin(req.modulePath, req.options ?? null);
830
+ return {
831
+ id: req.id,
832
+ ok: true,
833
+ functions
834
+ };
835
+ }
836
+ if (req.type === "invokeLessPluginFunction") {
837
+ const outcome = await invokeLessPluginFunction(req.modulePath, req.functionName, req.args ?? [], req.options ?? null, req.facts ?? null);
838
+ return outcome.need ? {
839
+ id: req.id,
840
+ ok: false,
841
+ need: outcome.need,
842
+ logs: outcome.logs
843
+ } : {
844
+ id: req.id,
845
+ ok: true,
846
+ value: outcome.value,
847
+ logs: outcome.logs,
848
+ important: outcome.important
849
+ };
850
+ }
851
+ return {
852
+ id: req.id,
853
+ ok: false,
854
+ error: `Unknown request type "${req.type}".`
855
+ };
856
+ } catch (err) {
857
+ return {
858
+ id: req.id,
859
+ ok: false,
860
+ error: err?.message ?? String(err),
861
+ errorName: err?.name ?? "Error",
862
+ stack: typeof err?.stack === "string" ? err.stack : void 0
863
+ };
864
+ }
865
+ };
866
+ const handleRequest = async (req) => {
867
+ if (!req || typeof req.id !== "number" || typeof req.type !== "string") return;
868
+ send(await computeResponse(req));
869
+ };
870
+ async function main() {
871
+ send({ type: "ready" });
872
+ let buffer = "";
873
+ for await (const chunk of Deno.stdin.readable) {
874
+ buffer += decoder.decode(chunk, { stream: true });
875
+ let idx = buffer.indexOf("\n");
876
+ while (idx >= 0) {
877
+ const line = buffer.slice(0, idx).trim();
878
+ buffer = buffer.slice(idx + 1);
879
+ idx = buffer.indexOf("\n");
880
+ if (!line) continue;
881
+ let req;
882
+ try {
883
+ req = JSON.parse(line);
884
+ } catch {
885
+ continue;
886
+ }
887
+ await handleRequest(req);
888
+ }
889
+ }
890
+ }
891
+ main();
892
+ //#endregion