@jesscss/plugin-js 2.0.0-alpha.7 → 2.0.0-alpha.9

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/README.md CHANGED
@@ -1,6 +1,50 @@
1
1
  # @jesscss/plugin-js
2
2
 
3
- Jess plugin that enables JavaScript/TypeScript module imports.
3
+ **Import bridge for JavaScript/TypeScript modules in stylesheets — a seed of the
4
+ JavaScript-execution / CSS-in-JS story.**
4
5
 
5
- This package checks for a usable Deno runtime before executing non-`@jesscss/fns`
6
- modules. It is intended to be auto-loaded by `jess` when installed.
6
+ This plugin lets a stylesheet pull in JavaScript/TypeScript modules
7
+ (`.js`, `.mjs`, `.cjs`, `.ts`, `.mts`, `.cts`) the mechanism behind `@use` /
8
+ `@-from` script imports and legacy Less `@plugin` loading. When installed, it is
9
+ auto-loaded by `jess`.
10
+
11
+ ## Sandboxed execution
12
+
13
+ `plugin-js` does **not** run untrusted module code in your Node process. Before
14
+ executing anything, it checks for a usable **Deno** runtime (`deno --version`)
15
+ and runs the module in a Deno subprocess behind a permission broker:
16
+
17
+ - **read** is limited to `node_modules` (and an optional `jsReadRoot`),
18
+ - **net** is denied unless you opt in (`allowHttp`, optionally scoped to
19
+ `allowNetHosts`),
20
+ - **env / write / run / ffi / sys** are denied outright.
21
+
22
+ Values cross the boundary through a small typed bridge (dimensions, colors,
23
+ quoted strings, lists, detached rules, …). Built-in `@jesscss/fns` modules are
24
+ trusted and imported directly, without the worker. If no Deno binary is found,
25
+ the plugin fails with a clear message instead of falling back to unsandboxed
26
+ execution.
27
+
28
+ ## Why it exists — the convergence angle
29
+
30
+ One of the four tools [Jess](https://github.com/jesscss/jess) aims to converge is
31
+ **CSS-in-JS**: running real JavaScript inside stylesheets so styles can be
32
+ dynamic without leaving CSS files. This plugin — together with
33
+ [`@jesscss/plugin-node-modules`](../jess-plugin-node-modules), which resolves the
34
+ packages — is a seed of that story.
35
+
36
+ That convergence is **roadmap — being proven through the alpha, not claimed as
37
+ done.** Legacy Less `@plugin` is supported for compatibility but deprecated in
38
+ favor of `@-from` / `@-use`.
39
+
40
+ ## Status
41
+
42
+ **Alpha.** Part of Jess. Requires a Deno runtime for script execution. The
43
+ programmatic plugin/compiler API is **not yet stabilized** — the `jess` CLI is
44
+ the documented public surface for the alpha. Watch the
45
+ [docs site](https://jesscss.github.io/) for the API once it settles.
46
+
47
+ - Project overview & positioning: <https://github.com/jesscss/jess#readme>
48
+ - Docs: <https://jesscss.github.io/> (currently pre-alpha content)
49
+ - Issues: <https://github.com/jesscss/jess/issues>
50
+ - License: MIT
package/lib/bridge.d.ts CHANGED
@@ -16,17 +16,12 @@ export type JsBridgeValue = {
16
16
  kind: 'color';
17
17
  rgb: [number, number, number];
18
18
  alpha?: number;
19
- format?: string;
20
19
  } | {
21
20
  __jessBridge: true;
22
21
  kind: 'quoted';
23
22
  value: string;
24
23
  quote?: '"' | '\'';
25
24
  escaped?: boolean;
26
- } | {
27
- __jessBridge: true;
28
- kind: 'keyword';
29
- value: string;
30
25
  } | {
31
26
  __jessBridge: true;
32
27
  kind: 'anonymous';
@@ -35,17 +30,17 @@ export type JsBridgeValue = {
35
30
  __jessBridge: true;
36
31
  kind: 'list';
37
32
  items: JsBridgeValue[];
38
- separator?: ',' | ';' | '/';
33
+ separator: ',' | '/' | ';';
39
34
  } | {
40
35
  __jessBridge: true;
41
- kind: 'sequence';
36
+ kind: 'expression';
42
37
  items: JsBridgeValue[];
43
38
  } | {
44
39
  __jessBridge: true;
45
- kind: 'detached';
40
+ kind: 'mixin';
46
41
  rules: JsBridgeDeclaration[];
47
42
  };
48
43
  export declare function encodeBridgeValue(value: unknown): unknown;
49
44
  export declare function decodeBridgeValue(value: unknown): unknown;
50
- export declare function encodeBridgeArgs(args: unknown[]): unknown[];
45
+ export declare function encodeBridgeArgs(args: readonly unknown[]): unknown[];
51
46
  //# sourceMappingURL=bridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAcA,MAAM,MAAM,mBAAmB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,CAAC;AAEzE,MAAM,MAAM,aAAa,GACrB;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;CAAE,GACxE;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACvE;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACrG;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC5F;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,aAAa,EAAE,CAAC;IAAC,SAAS,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;CAAE,GACzF;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,aAAa,EAAE,CAAA;CAAE,GAChE;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAC;AAqC3E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmEzD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA+BzD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAE3D"}
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,mBAAmB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,CAAC;AAEzE,MAAM,MAAM,aAAa,GACrB;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;CAAE,GACxE;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACvE;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACpF;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC5F;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,aAAa,EAAE,CAAC;IAAC,SAAS,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;CAAE,GACxF;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,aAAa,EAAE,CAAA;CAAE,GAClE;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAC;AAiGxE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA8BzD;AAqCD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAKzD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,EAAE,CAEpE"}
package/lib/index.cjs CHANGED
@@ -33,100 +33,198 @@ let node_path = require("node:path");
33
33
  node_path = __toESM(node_path);
34
34
  let node_url = require("node:url");
35
35
  let node_child_process = require("node:child_process");
36
+ let _jesscss_core_value = require("@jesscss/core/value");
36
37
  //#region src/bridge.ts
37
- const isBridgeValue = (value) => typeof value === "object" && value !== null && value.__jessBridge === true && typeof value.kind === "string";
38
- const colorFormatFromString = (value) => {
39
- if (!value) return;
40
- if (value in _jesscss_core.ColorFormat) return _jesscss_core.ColorFormat[value];
41
- if (Object.values(_jesscss_core.ColorFormat).includes(value)) return value;
42
- };
38
+ const isRecord = (value) => typeof value === "object" && value !== null;
39
+ const isBridgeValue = (value) => isRecord(value) && value.__jessBridge === true && typeof value.kind === "string";
40
+ const isValueObj = (value) => isRecord(value) && typeof value.type === "string" && typeof value.bytes === "string";
41
+ const isDetached = (value) => isRecord(value) && value.type === "DetachedRuleset" && Array.isArray(value.rules);
43
42
  function encodeBridgeChildValue(value) {
44
43
  const encoded = encodeBridgeValue(value);
45
44
  if (isBridgeValue(encoded)) return encoded;
46
45
  return {
47
46
  __jessBridge: true,
48
47
  kind: "scalar",
49
- value: typeof encoded === "string" || typeof encoded === "number" || typeof encoded === "boolean" ? encoded : String(encoded)
48
+ value: String(encoded)
50
49
  };
51
50
  }
51
+ function encodeFacadeValue(value) {
52
+ switch (value.type) {
53
+ case "Dimension": return typeof value.value === "number" ? {
54
+ __jessBridge: true,
55
+ kind: "dimension",
56
+ value: value.value,
57
+ unit: typeof value.unit === "string" ? value.unit : void 0
58
+ } : void 0;
59
+ case "Color": return Array.isArray(value.rgb) && value.rgb.length === 3 ? {
60
+ __jessBridge: true,
61
+ kind: "color",
62
+ rgb: [
63
+ value.rgb[0],
64
+ value.rgb[1],
65
+ value.rgb[2]
66
+ ],
67
+ alpha: typeof value.alpha === "number" ? value.alpha : void 0
68
+ } : void 0;
69
+ case "Quoted": return typeof value.value === "string" ? {
70
+ __jessBridge: true,
71
+ kind: "quoted",
72
+ value: value.value,
73
+ quote: value.quote === "'" ? "'" : "\"",
74
+ escaped: value.escaped === true
75
+ } : void 0;
76
+ case "Anonymous":
77
+ case "Keyword": return typeof value.value === "string" ? {
78
+ __jessBridge: true,
79
+ kind: "anonymous",
80
+ value: value.value
81
+ } : void 0;
82
+ case "Expression": return Array.isArray(value.value) ? {
83
+ __jessBridge: true,
84
+ kind: "expression",
85
+ items: value.value.map(encodeBridgeChildValue)
86
+ } : void 0;
87
+ case "Value": return Array.isArray(value.value) ? {
88
+ __jessBridge: true,
89
+ kind: "list",
90
+ items: value.value.map(encodeBridgeChildValue),
91
+ separator: value.separator === "/" || value.separator === ";" ? value.separator : ","
92
+ } : void 0;
93
+ case "Mixin": {
94
+ const ruleset = value.ruleset;
95
+ if (!isRecord(ruleset) || !Array.isArray(ruleset.rules)) return;
96
+ const rules = [];
97
+ for (const rule of ruleset.rules) {
98
+ if (!isRecord(rule)) continue;
99
+ if (rule.type === "Declaration" && typeof rule.name === "string") rules.push({
100
+ name: rule.name,
101
+ value: encodeBridgeChildValue(rule.value)
102
+ });
103
+ }
104
+ return {
105
+ __jessBridge: true,
106
+ kind: "mixin",
107
+ rules
108
+ };
109
+ }
110
+ default: return;
111
+ }
112
+ }
52
113
  function encodeBridgeValue(value) {
53
- if (value instanceof _jesscss_core.Dimension) return {
54
- __jessBridge: true,
55
- kind: "dimension",
56
- value: value.number,
57
- unit: value.unit
58
- };
59
- if (value instanceof _jesscss_core.Color) return {
60
- __jessBridge: true,
61
- kind: "color",
62
- rgb: value.rgb,
63
- alpha: value.alpha,
64
- format: value.options.format === void 0 ? void 0 : _jesscss_core.ColorFormat[value.options.format]
65
- };
66
- if (value instanceof _jesscss_core.Quoted) return {
114
+ if (Array.isArray(value)) return {
67
115
  __jessBridge: true,
68
- kind: "quoted",
69
- value: String(value.value),
70
- quote: value.quote,
71
- escaped: value.escaped
116
+ kind: "expression",
117
+ items: value.map(encodeBridgeChildValue)
72
118
  };
73
- if (value instanceof _jesscss_core.Any) return {
74
- __jessBridge: true,
75
- kind: value.role === "keyword" ? "keyword" : "anonymous",
76
- value: value.value
77
- };
78
- if (value instanceof _jesscss_core.List) return {
79
- __jessBridge: true,
80
- kind: "list",
81
- items: value.value.map(encodeBridgeChildValue),
82
- separator: value.options.sep
83
- };
84
- if (value instanceof _jesscss_core.Sequence) return {
85
- __jessBridge: true,
86
- kind: "sequence",
87
- items: value.value.map(encodeBridgeChildValue)
88
- };
89
- if (value instanceof _jesscss_core.Rules) {
90
- const rules = [];
91
- for (const rule of value.rules ?? []) if (rule instanceof _jesscss_core.Declaration && typeof rule.name === "string") rules.push({
92
- name: rule.name,
93
- value: encodeBridgeChildValue(rule.value)
94
- });
95
- return {
119
+ if (isValueObj(value)) switch (value.type) {
120
+ case "Dimension": return {
121
+ __jessBridge: true,
122
+ kind: "dimension",
123
+ value: value.number,
124
+ unit: value.unit
125
+ };
126
+ case "Color": return {
127
+ __jessBridge: true,
128
+ kind: "color",
129
+ rgb: [
130
+ value.rgb[0],
131
+ value.rgb[1],
132
+ value.rgb[2]
133
+ ],
134
+ alpha: value.alpha
135
+ };
136
+ case "Quoted": return {
137
+ __jessBridge: true,
138
+ kind: "quoted",
139
+ value: value.value,
140
+ quote: value.quote === "'" ? "'" : "\"",
141
+ escaped: value.escaped
142
+ };
143
+ case "List": return value.sep === "," || value.sep === "/" ? {
144
+ __jessBridge: true,
145
+ kind: "list",
146
+ items: value.value.map(encodeBridgeChildValue),
147
+ separator: value.sep
148
+ } : {
96
149
  __jessBridge: true,
97
- kind: "detached",
98
- rules
150
+ kind: "anonymous",
151
+ value: value.bytes
152
+ };
153
+ case "Block": return {
154
+ __jessBridge: true,
155
+ kind: "anonymous",
156
+ value: value.bytes
157
+ };
158
+ default: return {
159
+ __jessBridge: true,
160
+ kind: "anonymous",
161
+ value: value.bytes
99
162
  };
100
163
  }
164
+ if (isDetached(value)) return {
165
+ __jessBridge: true,
166
+ kind: "mixin",
167
+ rules: value.rules.map((rule) => ({
168
+ name: rule.name,
169
+ value: encodeBridgeChildValue(rule.value)
170
+ }))
171
+ };
172
+ if (isRecord(value)) return encodeFacadeValue(value) ?? value;
101
173
  return value;
102
174
  }
103
- function decodeBridgeValue(value) {
104
- if (!isBridgeValue(value)) return value;
175
+ function decodeValue(value) {
105
176
  switch (value.kind) {
106
- case "scalar": return new _jesscss_core.Any(String(value.value));
107
- case "dimension": return new _jesscss_core.Dimension({
108
- number: value.value,
109
- unit: value.unit
110
- });
111
- case "color": return new _jesscss_core.Color({
112
- rgb: value.rgb,
113
- alpha: value.alpha
114
- }, { format: colorFormatFromString(value.format) });
115
- case "quoted": return new _jesscss_core.Quoted(value.value, {
116
- quote: value.quote,
117
- escaped: value.escaped
118
- });
119
- case "keyword": return new _jesscss_core.Any(value.value, { role: "keyword" });
120
- case "anonymous": return new _jesscss_core.Any(value.value);
121
- case "list": return new _jesscss_core.List(value.items.map((item) => decodeBridgeValue(item)), { sep: value.separator });
122
- case "sequence": return new _jesscss_core.Sequence(value.items.map((item) => decodeBridgeValue(item)));
177
+ case "scalar": return (0, _jesscss_core_value.makeKeyword)(String(value.value));
178
+ case "dimension": return (0, _jesscss_core_value.makeDimension)(value.value, value.unit ?? "");
179
+ case "color": return (0, _jesscss_core_value.makeColorRgb)(value.rgb, value.alpha ?? 1, _jesscss_core_value.RGB);
180
+ case "quoted": return (0, _jesscss_core_value.makeQuoted)(value.value, value.quote ?? "\"", value.escaped === true);
181
+ case "anonymous": return (0, _jesscss_core_value.makeKeyword)(value.value);
182
+ case "expression": return value.items.map(decodeValue);
183
+ case "list": return (0, _jesscss_core_value.makeList)(value.items.map(decodeValue), value.separator === ";" ? "," : value.separator);
184
+ case "mixin": return (0, _jesscss_core_value.makeKeyword)("");
123
185
  }
124
186
  }
187
+ function decodeMixin(value) {
188
+ const nil = {
189
+ type: "Nil",
190
+ value: ""
191
+ };
192
+ const mixin = {
193
+ type: "Mixin",
194
+ name: nil,
195
+ args: nil,
196
+ ruleset: { rules: value.rules.map((rule) => ({
197
+ type: "Declaration",
198
+ name: rule.name,
199
+ value: decodeValue(rule.value),
200
+ eval() {
201
+ return this;
202
+ }
203
+ })) },
204
+ eval() {
205
+ return mixin;
206
+ }
207
+ };
208
+ return mixin;
209
+ }
210
+ function decodeBridgeValue(value) {
211
+ if (!isBridgeValue(value)) return value;
212
+ return value.kind === "mixin" ? decodeMixin(value) : decodeValue(value);
213
+ }
125
214
  function encodeBridgeArgs(args) {
126
215
  return args.map(encodeBridgeValue);
127
216
  }
128
217
  //#endregion
129
218
  //#region src/index.ts
219
+ /**
220
+ * Child-process stdio streams are typed as bare `Writable`/`Readable`, which do
221
+ * not declare `unref`, but the underlying pipe sockets expose it at runtime.
222
+ * The optional structural member lets any stream be passed without an unsafe
223
+ * cast; the runtime `?.` guard covers streams that genuinely lack the method.
224
+ */
225
+ const unrefStream = (stream) => {
226
+ if (typeof stream === "object" && stream !== null && "unref" in stream && typeof stream.unref === "function") stream.unref();
227
+ };
130
228
  const SCRIPT_EXTENSIONS = new Set([
131
229
  ".js",
132
230
  ".mjs",
@@ -144,6 +242,33 @@ const RUNTIME_MISSING_MESSAGE = [
144
242
  const BOOT_TIMEOUT_MS = 8e3;
145
243
  const REQUEST_TIMEOUT_MS = 1e4;
146
244
  const IDLE_SHUTDOWN_MS = 5e3;
245
+ /**
246
+ * Environment variables that inject a Node.js debugger/inspector bootloader
247
+ * into child processes (VS Code / Cursor "Auto Attach", `node --inspect`, etc.).
248
+ *
249
+ * These must never leak into the sandboxed Deno worker: the injected bootloader
250
+ * tries to read the environment, the Jess Deno sandbox denies `env` permission,
251
+ * and the worker never signals ready — producing a spurious
252
+ * "Timed out waiting for Deno worker startup" failure. Deno does not use any of
253
+ * these vars, so stripping them is safe.
254
+ */
255
+ const DEBUG_ENV_KEY_RE = /^(NODE_OPTIONS|NODE_INSPECT|VSCODE_INSPECTOR_OPTIONS)/i;
256
+ const DEBUG_ENV_VALUE_RE = /js-debug|bootloader/i;
257
+ /**
258
+ * Returns a copy of `env` with Node debugger/inspector-attach variables removed,
259
+ * so a spawned Deno subprocess starts regardless of the parent's debug env.
260
+ * The Deno permission sandbox is untouched; this only stops leaking the debugger
261
+ * into it.
262
+ */
263
+ const sanitizeSpawnEnv = (env = process.env) => {
264
+ const clean = {};
265
+ for (const [key, value] of Object.entries(env)) {
266
+ if (DEBUG_ENV_KEY_RE.test(key)) continue;
267
+ if (typeof value === "string" && DEBUG_ENV_VALUE_RE.test(value)) continue;
268
+ clean[key] = value;
269
+ }
270
+ return clean;
271
+ };
147
272
  const isPathInside = (candidatePath, rootPath) => {
148
273
  const rel = node_path.relative(rootPath, candidatePath);
149
274
  return rel === "" || !rel.startsWith("..") && !node_path.isAbsolute(rel);
@@ -227,7 +352,10 @@ var JsPlugin = class JsPlugin extends _jesscss_core.AbstractPlugin {
227
352
  this.idleTimer.unref?.();
228
353
  }
229
354
  ensureRuntimeAvailable() {
230
- if ((0, node_child_process.spawnSync)(this.opts.denoCommand ?? "deno", ["--version"], { stdio: "ignore" }).status !== 0) throw new Error(RUNTIME_MISSING_MESSAGE);
355
+ if ((0, node_child_process.spawnSync)(this.opts.denoCommand ?? "deno", ["--version"], {
356
+ stdio: "ignore",
357
+ env: sanitizeSpawnEnv(process.env)
358
+ }).status !== 0) throw new Error(RUNTIME_MISSING_MESSAGE);
231
359
  }
232
360
  ensureRuntime() {
233
361
  if (this.runtimeState.status === "ready") {
@@ -354,15 +482,15 @@ var JsPlugin = class JsPlugin extends _jesscss_core.AbstractPlugin {
354
482
  "pipe"
355
483
  ],
356
484
  env: {
357
- ...process.env,
485
+ ...sanitizeSpawnEnv(process.env),
358
486
  DENO_PERMISSION_BROKER_PATH: socketPath
359
487
  }
360
488
  });
361
489
  this.worker = child;
362
490
  child.unref();
363
- child.stdin.unref?.();
364
- child.stdout.unref?.();
365
- child.stderr.unref?.();
491
+ unrefStream(child.stdin);
492
+ unrefStream(child.stdout);
493
+ unrefStream(child.stderr);
366
494
  child.stdout.setEncoding("utf8");
367
495
  child.stderr.setEncoding("utf8");
368
496
  child.stdout.on("data", (chunk) => this.onWorkerStdout(chunk));
@@ -548,7 +676,7 @@ var JsPlugin = class JsPlugin extends _jesscss_core.AbstractPlugin {
548
676
  * @deprecated Less `@plugin` is deprecated. Prefer `@-from` for
549
677
  * ESM-style script imports or `@-use` for Sass-module-style namespace imports.
550
678
  */
551
- async importLessPlugin(absoluteFilePath) {
679
+ async importLessPlugin(absoluteFilePath, options = null) {
552
680
  const ext = node_path.extname(absoluteFilePath);
553
681
  if (!SCRIPT_EXTENSIONS.has(ext)) throw new Error(`Plugin "${this.name}" cannot import Less plugin "${absoluteFilePath}"`);
554
682
  this.assertAllowedPath(absoluteFilePath);
@@ -556,7 +684,8 @@ var JsPlugin = class JsPlugin extends _jesscss_core.AbstractPlugin {
556
684
  const modulePath = node_path.resolve(absoluteFilePath);
557
685
  const loadResult = await this.callWorker({
558
686
  type: "loadLessPlugin",
559
- modulePath
687
+ modulePath,
688
+ options
560
689
  });
561
690
  if (!loadResult.ok) throw new Error(loadResult.error);
562
691
  const functions = {};
@@ -564,6 +693,7 @@ var JsPlugin = class JsPlugin extends _jesscss_core.AbstractPlugin {
564
693
  const invokeResult = await this.callWorker({
565
694
  type: "invokeLessPluginFunction",
566
695
  modulePath,
696
+ options,
567
697
  functionName,
568
698
  args: encodeBridgeArgs(args)
569
699
  });
@@ -572,6 +702,10 @@ var JsPlugin = class JsPlugin extends _jesscss_core.AbstractPlugin {
572
702
  };
573
703
  return { functions };
574
704
  }
705
+ /** Context-facing executable-plugin capability used by the direct AST path. */
706
+ async importPlugin(absoluteFilePath, options = null) {
707
+ return this.importLessPlugin(absoluteFilePath, options);
708
+ }
575
709
  };
576
710
  /**
577
711
  * Global flag set when @jesscss/plugin-js is loaded.
@@ -587,3 +721,4 @@ exports.JESS_PLUGIN_JS_GLOBAL = JESS_PLUGIN_JS_GLOBAL;
587
721
  exports.JsPlugin = JsPlugin;
588
722
  exports.__toESM = __toESM;
589
723
  exports.default = jsPlugin;
724
+ exports.sanitizeSpawnEnv = sanitizeSpawnEnv;
package/lib/index.d.ts CHANGED
@@ -14,6 +14,13 @@ export interface JsPluginOptions extends JavaScriptSandboxConfig {
14
14
  */
15
15
  runtimeApi?: 'module' | 'less';
16
16
  }
17
+ /**
18
+ * Returns a copy of `env` with Node debugger/inspector-attach variables removed,
19
+ * so a spawned Deno subprocess starts regardless of the parent's debug env.
20
+ * The Deno permission sandbox is untouched; this only stops leaking the debugger
21
+ * into it.
22
+ */
23
+ export declare const sanitizeSpawnEnv: (env?: NodeJS.ProcessEnv) => NodeJS.ProcessEnv;
17
24
  export declare class JsPlugin extends AbstractPlugin {
18
25
  opts: JsPluginOptions;
19
26
  private static cleanupRegistered;
@@ -56,7 +63,11 @@ export declare class JsPlugin extends AbstractPlugin {
56
63
  * @deprecated Less `@plugin` is deprecated. Prefer `@-from` for
57
64
  * ESM-style script imports or `@-use` for Sass-module-style namespace imports.
58
65
  */
59
- importLessPlugin(absoluteFilePath: string): Promise<{
66
+ importLessPlugin(absoluteFilePath: string, options?: string | null): Promise<{
67
+ functions: Record<string, (...args: unknown[]) => Promise<unknown>>;
68
+ }>;
69
+ /** Context-facing executable-plugin capability used by the direct AST path. */
70
+ importPlugin(absoluteFilePath: string, options?: string | null): Promise<{
60
71
  functions: Record<string, (...args: unknown[]) => Promise<unknown>>;
61
72
  }>;
62
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,cAAc,EACf,MAAM,eAAe,CAAC;AAQvB,MAAM,MAAM,uBAAuB,GAAG;IACpC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,WAAW,eAAgB,SAAQ,uBAAuB;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CAChC;AA8GD,qBAAa,QAAS,SAAQ,cAAc;IAoBvB,IAAI,EAAE,eAAe;IAnBxC,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAS;IACzC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAuB;IACnD,IAAI,SAAQ;IACZ,mBAAmB,WAAiC;IACpD,OAAO,CAAC,YAAY,CAAoC;IACxD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAyB;IAC7C,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,MAAM,CAA6C;IAC3D,OAAO,CAAC,YAAY,CAAM;IAC1B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,OAAO,CAIV;IAEL,OAAO,CAAC,SAAS,CAA6B;gBAE3B,IAAI,GAAE,eAAoB;IAM7C,OAAO;IAIP,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAkBrC,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,oBAAoB;IAY5B,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,aAAa;IA4BrB,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,mBAAmB;YA0Bb,WAAW;IA8CzB,OAAO,CAAC,WAAW;YAwFL,YAAY;IAW1B,OAAO,CAAC,cAAc;IAiCtB,OAAO,CAAC,gBAAgB;YAQV,UAAU;IAwBxB,OAAO,CAAC,QAAQ;IA0BhB,OAAO;IAMP,OAAO,CAAC,iBAAiB;IAgBnB,MAAM,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IA+CpE;;;;;OAKG;IACG,gBAAgB,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,CAAA;KAAE,CAAC;CA6BnI;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,iCAAiC,CAAC;AAOpE,QAAA,MAAM,QAAQ,UAAY,eAAe,aAEtB,CAAC;AAEpB,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,cAAc,EACf,MAAM,eAAe,CAAC;AAyBvB,MAAM,MAAM,uBAAuB,GAAG;IACpC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,WAAW,eAAgB,SAAQ,uBAAuB;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CAChC;AAmCD;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAC3B,MAAK,MAAM,CAAC,UAAwB,KACnC,MAAM,CAAC,UAYT,CAAC;AA0FF,qBAAa,QAAS,SAAQ,cAAc;IAoBvB,IAAI,EAAE,eAAe;IAnBxC,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAS;IACzC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAuB;IACnD,IAAI,SAAQ;IACZ,mBAAmB,WAAiC;IACpD,OAAO,CAAC,YAAY,CAAoC;IACxD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAyB;IAC7C,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,MAAM,CAA6C;IAC3D,OAAO,CAAC,YAAY,CAAM;IAC1B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,OAAO,CAIV;IAEL,OAAO,CAAC,SAAS,CAA6B;gBAE3B,IAAI,GAAE,eAAoB;IAM7C,OAAO;IAIP,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAkBrC,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,oBAAoB;IAY5B,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,aAAa;IA4BrB,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,mBAAmB;YA0Bb,WAAW;IA8CzB,OAAO,CAAC,WAAW;YAwFL,YAAY;IAW1B,OAAO,CAAC,cAAc;IAiCtB,OAAO,CAAC,gBAAgB;YAQV,UAAU;IAwBxB,OAAO,CAAC,QAAQ;IA0BhB,OAAO;IAMP,OAAO,CAAC,iBAAiB;IAgBnB,MAAM,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IA+CpE;;;;;OAKG;IACG,gBAAgB,CAAC,gBAAgB,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,GAAG,IAAW,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,CAAA;KAAE,CAAC;IA+BjK,+EAA+E;IACzE,YAAY,CAAC,gBAAgB,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,GAAG,IAAW,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,CAAA;KAAE,CAAC;CAG9J;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,iCAAiC,CAAC;AAOpE,QAAA,MAAM,QAAQ,UAAY,eAAe,aAEtB,CAAC;AAEpB,eAAe,QAAQ,CAAC"}
package/lib/index.js CHANGED
@@ -1,103 +1,201 @@
1
- import { AbstractPlugin, Any, Color, ColorFormat, Declaration, Dimension, List, Quoted, Rules, Sequence } from "@jesscss/core";
1
+ import { AbstractPlugin } from "@jesscss/core";
2
2
  import * as fs from "node:fs";
3
3
  import * as net from "node:net";
4
4
  import * as path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { spawn, spawnSync } from "node:child_process";
7
+ import { RGB, makeColorRgb, makeDimension, makeKeyword, makeList, makeQuoted } from "@jesscss/core/value";
7
8
  //#region src/bridge.ts
8
- const isBridgeValue = (value) => typeof value === "object" && value !== null && value.__jessBridge === true && typeof value.kind === "string";
9
- const colorFormatFromString = (value) => {
10
- if (!value) return;
11
- if (value in ColorFormat) return ColorFormat[value];
12
- if (Object.values(ColorFormat).includes(value)) return value;
13
- };
9
+ const isRecord = (value) => typeof value === "object" && value !== null;
10
+ const isBridgeValue = (value) => isRecord(value) && value.__jessBridge === true && typeof value.kind === "string";
11
+ const isValueObj = (value) => isRecord(value) && typeof value.type === "string" && typeof value.bytes === "string";
12
+ const isDetached = (value) => isRecord(value) && value.type === "DetachedRuleset" && Array.isArray(value.rules);
14
13
  function encodeBridgeChildValue(value) {
15
14
  const encoded = encodeBridgeValue(value);
16
15
  if (isBridgeValue(encoded)) return encoded;
17
16
  return {
18
17
  __jessBridge: true,
19
18
  kind: "scalar",
20
- value: typeof encoded === "string" || typeof encoded === "number" || typeof encoded === "boolean" ? encoded : String(encoded)
19
+ value: String(encoded)
21
20
  };
22
21
  }
22
+ function encodeFacadeValue(value) {
23
+ switch (value.type) {
24
+ case "Dimension": return typeof value.value === "number" ? {
25
+ __jessBridge: true,
26
+ kind: "dimension",
27
+ value: value.value,
28
+ unit: typeof value.unit === "string" ? value.unit : void 0
29
+ } : void 0;
30
+ case "Color": return Array.isArray(value.rgb) && value.rgb.length === 3 ? {
31
+ __jessBridge: true,
32
+ kind: "color",
33
+ rgb: [
34
+ value.rgb[0],
35
+ value.rgb[1],
36
+ value.rgb[2]
37
+ ],
38
+ alpha: typeof value.alpha === "number" ? value.alpha : void 0
39
+ } : void 0;
40
+ case "Quoted": return typeof value.value === "string" ? {
41
+ __jessBridge: true,
42
+ kind: "quoted",
43
+ value: value.value,
44
+ quote: value.quote === "'" ? "'" : "\"",
45
+ escaped: value.escaped === true
46
+ } : void 0;
47
+ case "Anonymous":
48
+ case "Keyword": return typeof value.value === "string" ? {
49
+ __jessBridge: true,
50
+ kind: "anonymous",
51
+ value: value.value
52
+ } : void 0;
53
+ case "Expression": return Array.isArray(value.value) ? {
54
+ __jessBridge: true,
55
+ kind: "expression",
56
+ items: value.value.map(encodeBridgeChildValue)
57
+ } : void 0;
58
+ case "Value": return Array.isArray(value.value) ? {
59
+ __jessBridge: true,
60
+ kind: "list",
61
+ items: value.value.map(encodeBridgeChildValue),
62
+ separator: value.separator === "/" || value.separator === ";" ? value.separator : ","
63
+ } : void 0;
64
+ case "Mixin": {
65
+ const ruleset = value.ruleset;
66
+ if (!isRecord(ruleset) || !Array.isArray(ruleset.rules)) return;
67
+ const rules = [];
68
+ for (const rule of ruleset.rules) {
69
+ if (!isRecord(rule)) continue;
70
+ if (rule.type === "Declaration" && typeof rule.name === "string") rules.push({
71
+ name: rule.name,
72
+ value: encodeBridgeChildValue(rule.value)
73
+ });
74
+ }
75
+ return {
76
+ __jessBridge: true,
77
+ kind: "mixin",
78
+ rules
79
+ };
80
+ }
81
+ default: return;
82
+ }
83
+ }
23
84
  function encodeBridgeValue(value) {
24
- if (value instanceof Dimension) return {
25
- __jessBridge: true,
26
- kind: "dimension",
27
- value: value.number,
28
- unit: value.unit
29
- };
30
- if (value instanceof Color) return {
31
- __jessBridge: true,
32
- kind: "color",
33
- rgb: value.rgb,
34
- alpha: value.alpha,
35
- format: value.options.format === void 0 ? void 0 : ColorFormat[value.options.format]
36
- };
37
- if (value instanceof Quoted) return {
85
+ if (Array.isArray(value)) return {
38
86
  __jessBridge: true,
39
- kind: "quoted",
40
- value: String(value.value),
41
- quote: value.quote,
42
- escaped: value.escaped
87
+ kind: "expression",
88
+ items: value.map(encodeBridgeChildValue)
43
89
  };
44
- if (value instanceof Any) return {
45
- __jessBridge: true,
46
- kind: value.role === "keyword" ? "keyword" : "anonymous",
47
- value: value.value
48
- };
49
- if (value instanceof List) return {
50
- __jessBridge: true,
51
- kind: "list",
52
- items: value.value.map(encodeBridgeChildValue),
53
- separator: value.options.sep
54
- };
55
- if (value instanceof Sequence) return {
56
- __jessBridge: true,
57
- kind: "sequence",
58
- items: value.value.map(encodeBridgeChildValue)
59
- };
60
- if (value instanceof Rules) {
61
- const rules = [];
62
- for (const rule of value.rules ?? []) if (rule instanceof Declaration && typeof rule.name === "string") rules.push({
63
- name: rule.name,
64
- value: encodeBridgeChildValue(rule.value)
65
- });
66
- return {
90
+ if (isValueObj(value)) switch (value.type) {
91
+ case "Dimension": return {
92
+ __jessBridge: true,
93
+ kind: "dimension",
94
+ value: value.number,
95
+ unit: value.unit
96
+ };
97
+ case "Color": return {
98
+ __jessBridge: true,
99
+ kind: "color",
100
+ rgb: [
101
+ value.rgb[0],
102
+ value.rgb[1],
103
+ value.rgb[2]
104
+ ],
105
+ alpha: value.alpha
106
+ };
107
+ case "Quoted": return {
108
+ __jessBridge: true,
109
+ kind: "quoted",
110
+ value: value.value,
111
+ quote: value.quote === "'" ? "'" : "\"",
112
+ escaped: value.escaped
113
+ };
114
+ case "List": return value.sep === "," || value.sep === "/" ? {
115
+ __jessBridge: true,
116
+ kind: "list",
117
+ items: value.value.map(encodeBridgeChildValue),
118
+ separator: value.sep
119
+ } : {
67
120
  __jessBridge: true,
68
- kind: "detached",
69
- rules
121
+ kind: "anonymous",
122
+ value: value.bytes
123
+ };
124
+ case "Block": return {
125
+ __jessBridge: true,
126
+ kind: "anonymous",
127
+ value: value.bytes
128
+ };
129
+ default: return {
130
+ __jessBridge: true,
131
+ kind: "anonymous",
132
+ value: value.bytes
70
133
  };
71
134
  }
135
+ if (isDetached(value)) return {
136
+ __jessBridge: true,
137
+ kind: "mixin",
138
+ rules: value.rules.map((rule) => ({
139
+ name: rule.name,
140
+ value: encodeBridgeChildValue(rule.value)
141
+ }))
142
+ };
143
+ if (isRecord(value)) return encodeFacadeValue(value) ?? value;
72
144
  return value;
73
145
  }
74
- function decodeBridgeValue(value) {
75
- if (!isBridgeValue(value)) return value;
146
+ function decodeValue(value) {
76
147
  switch (value.kind) {
77
- case "scalar": return new Any(String(value.value));
78
- case "dimension": return new Dimension({
79
- number: value.value,
80
- unit: value.unit
81
- });
82
- case "color": return new Color({
83
- rgb: value.rgb,
84
- alpha: value.alpha
85
- }, { format: colorFormatFromString(value.format) });
86
- case "quoted": return new Quoted(value.value, {
87
- quote: value.quote,
88
- escaped: value.escaped
89
- });
90
- case "keyword": return new Any(value.value, { role: "keyword" });
91
- case "anonymous": return new Any(value.value);
92
- case "list": return new List(value.items.map((item) => decodeBridgeValue(item)), { sep: value.separator });
93
- case "sequence": return new Sequence(value.items.map((item) => decodeBridgeValue(item)));
148
+ case "scalar": return makeKeyword(String(value.value));
149
+ case "dimension": return makeDimension(value.value, value.unit ?? "");
150
+ case "color": return makeColorRgb(value.rgb, value.alpha ?? 1, RGB);
151
+ case "quoted": return makeQuoted(value.value, value.quote ?? "\"", value.escaped === true);
152
+ case "anonymous": return makeKeyword(value.value);
153
+ case "expression": return value.items.map(decodeValue);
154
+ case "list": return makeList(value.items.map(decodeValue), value.separator === ";" ? "," : value.separator);
155
+ case "mixin": return makeKeyword("");
94
156
  }
95
157
  }
158
+ function decodeMixin(value) {
159
+ const nil = {
160
+ type: "Nil",
161
+ value: ""
162
+ };
163
+ const mixin = {
164
+ type: "Mixin",
165
+ name: nil,
166
+ args: nil,
167
+ ruleset: { rules: value.rules.map((rule) => ({
168
+ type: "Declaration",
169
+ name: rule.name,
170
+ value: decodeValue(rule.value),
171
+ eval() {
172
+ return this;
173
+ }
174
+ })) },
175
+ eval() {
176
+ return mixin;
177
+ }
178
+ };
179
+ return mixin;
180
+ }
181
+ function decodeBridgeValue(value) {
182
+ if (!isBridgeValue(value)) return value;
183
+ return value.kind === "mixin" ? decodeMixin(value) : decodeValue(value);
184
+ }
96
185
  function encodeBridgeArgs(args) {
97
186
  return args.map(encodeBridgeValue);
98
187
  }
99
188
  //#endregion
100
189
  //#region src/index.ts
190
+ /**
191
+ * Child-process stdio streams are typed as bare `Writable`/`Readable`, which do
192
+ * not declare `unref`, but the underlying pipe sockets expose it at runtime.
193
+ * The optional structural member lets any stream be passed without an unsafe
194
+ * cast; the runtime `?.` guard covers streams that genuinely lack the method.
195
+ */
196
+ const unrefStream = (stream) => {
197
+ if (typeof stream === "object" && stream !== null && "unref" in stream && typeof stream.unref === "function") stream.unref();
198
+ };
101
199
  const SCRIPT_EXTENSIONS = new Set([
102
200
  ".js",
103
201
  ".mjs",
@@ -115,6 +213,33 @@ const RUNTIME_MISSING_MESSAGE = [
115
213
  const BOOT_TIMEOUT_MS = 8e3;
116
214
  const REQUEST_TIMEOUT_MS = 1e4;
117
215
  const IDLE_SHUTDOWN_MS = 5e3;
216
+ /**
217
+ * Environment variables that inject a Node.js debugger/inspector bootloader
218
+ * into child processes (VS Code / Cursor "Auto Attach", `node --inspect`, etc.).
219
+ *
220
+ * These must never leak into the sandboxed Deno worker: the injected bootloader
221
+ * tries to read the environment, the Jess Deno sandbox denies `env` permission,
222
+ * and the worker never signals ready — producing a spurious
223
+ * "Timed out waiting for Deno worker startup" failure. Deno does not use any of
224
+ * these vars, so stripping them is safe.
225
+ */
226
+ const DEBUG_ENV_KEY_RE = /^(NODE_OPTIONS|NODE_INSPECT|VSCODE_INSPECTOR_OPTIONS)/i;
227
+ const DEBUG_ENV_VALUE_RE = /js-debug|bootloader/i;
228
+ /**
229
+ * Returns a copy of `env` with Node debugger/inspector-attach variables removed,
230
+ * so a spawned Deno subprocess starts regardless of the parent's debug env.
231
+ * The Deno permission sandbox is untouched; this only stops leaking the debugger
232
+ * into it.
233
+ */
234
+ const sanitizeSpawnEnv = (env = process.env) => {
235
+ const clean = {};
236
+ for (const [key, value] of Object.entries(env)) {
237
+ if (DEBUG_ENV_KEY_RE.test(key)) continue;
238
+ if (typeof value === "string" && DEBUG_ENV_VALUE_RE.test(value)) continue;
239
+ clean[key] = value;
240
+ }
241
+ return clean;
242
+ };
118
243
  const isPathInside = (candidatePath, rootPath) => {
119
244
  const rel = path.relative(rootPath, candidatePath);
120
245
  return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
@@ -198,7 +323,10 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
198
323
  this.idleTimer.unref?.();
199
324
  }
200
325
  ensureRuntimeAvailable() {
201
- if (spawnSync(this.opts.denoCommand ?? "deno", ["--version"], { stdio: "ignore" }).status !== 0) throw new Error(RUNTIME_MISSING_MESSAGE);
326
+ if (spawnSync(this.opts.denoCommand ?? "deno", ["--version"], {
327
+ stdio: "ignore",
328
+ env: sanitizeSpawnEnv(process.env)
329
+ }).status !== 0) throw new Error(RUNTIME_MISSING_MESSAGE);
202
330
  }
203
331
  ensureRuntime() {
204
332
  if (this.runtimeState.status === "ready") {
@@ -325,15 +453,15 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
325
453
  "pipe"
326
454
  ],
327
455
  env: {
328
- ...process.env,
456
+ ...sanitizeSpawnEnv(process.env),
329
457
  DENO_PERMISSION_BROKER_PATH: socketPath
330
458
  }
331
459
  });
332
460
  this.worker = child;
333
461
  child.unref();
334
- child.stdin.unref?.();
335
- child.stdout.unref?.();
336
- child.stderr.unref?.();
462
+ unrefStream(child.stdin);
463
+ unrefStream(child.stdout);
464
+ unrefStream(child.stderr);
337
465
  child.stdout.setEncoding("utf8");
338
466
  child.stderr.setEncoding("utf8");
339
467
  child.stdout.on("data", (chunk) => this.onWorkerStdout(chunk));
@@ -519,7 +647,7 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
519
647
  * @deprecated Less `@plugin` is deprecated. Prefer `@-from` for
520
648
  * ESM-style script imports or `@-use` for Sass-module-style namespace imports.
521
649
  */
522
- async importLessPlugin(absoluteFilePath) {
650
+ async importLessPlugin(absoluteFilePath, options = null) {
523
651
  const ext = path.extname(absoluteFilePath);
524
652
  if (!SCRIPT_EXTENSIONS.has(ext)) throw new Error(`Plugin "${this.name}" cannot import Less plugin "${absoluteFilePath}"`);
525
653
  this.assertAllowedPath(absoluteFilePath);
@@ -527,7 +655,8 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
527
655
  const modulePath = path.resolve(absoluteFilePath);
528
656
  const loadResult = await this.callWorker({
529
657
  type: "loadLessPlugin",
530
- modulePath
658
+ modulePath,
659
+ options
531
660
  });
532
661
  if (!loadResult.ok) throw new Error(loadResult.error);
533
662
  const functions = {};
@@ -535,6 +664,7 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
535
664
  const invokeResult = await this.callWorker({
536
665
  type: "invokeLessPluginFunction",
537
666
  modulePath,
667
+ options,
538
668
  functionName,
539
669
  args: encodeBridgeArgs(args)
540
670
  });
@@ -543,6 +673,10 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
543
673
  };
544
674
  return { functions };
545
675
  }
676
+ /** Context-facing executable-plugin capability used by the direct AST path. */
677
+ async importPlugin(absoluteFilePath, options = null) {
678
+ return this.importLessPlugin(absoluteFilePath, options);
679
+ }
546
680
  };
547
681
  /**
548
682
  * Global flag set when @jesscss/plugin-js is loaded.
@@ -554,4 +688,4 @@ const jsPlugin = ((opts) => {
554
688
  return new JsPlugin(opts);
555
689
  });
556
690
  //#endregion
557
- export { JESS_PLUGIN_JS_GLOBAL, JsPlugin, jsPlugin as default };
691
+ export { JESS_PLUGIN_JS_GLOBAL, JsPlugin, jsPlugin as default, sanitizeSpawnEnv };
@@ -78,8 +78,17 @@ var Declaration = class {
78
78
  return `${this.name}: ${v}`;
79
79
  }
80
80
  };
81
- var DetachedRuleset = class {
82
- type = "DetachedRuleset";
81
+ var Nil = class {
82
+ type = "Nil";
83
+ value = "";
84
+ eval() {
85
+ return this;
86
+ }
87
+ };
88
+ var Mixin = class {
89
+ type = "Mixin";
90
+ name = new Nil();
91
+ args = new Nil();
83
92
  ruleset;
84
93
  constructor(rules) {
85
94
  this.ruleset = { rules };
@@ -115,10 +124,11 @@ const lessFacade = {
115
124
  Anonymous,
116
125
  Color,
117
126
  Declaration,
118
- DetachedRuleset,
119
127
  Dimension,
120
128
  Expression,
121
129
  Keyword,
130
+ Mixin,
131
+ Nil,
122
132
  Quoted,
123
133
  Value
124
134
  },
@@ -150,11 +160,10 @@ const decodeBridgeValue = (value) => {
150
160
  case "dimension": return new Dimension(value.value, value.unit ?? "");
151
161
  case "color": return new Color(value.rgb, value.alpha ?? 1);
152
162
  case "quoted": return new Quoted(value.quote ?? "\"", value.value, value.escaped === true);
153
- case "keyword": return new Keyword(value.value);
154
163
  case "anonymous": return new Anonymous(value.value);
155
164
  case "list": return new Value((value.items ?? []).map(decodeBridgeValue), value.separator ?? ",");
156
- case "sequence": return new Expression((value.items ?? []).map(decodeBridgeValue));
157
- case "detached": return new DetachedRuleset((value.rules ?? []).map((decl) => {
165
+ case "expression": return new Expression((value.items ?? []).map(decodeBridgeValue));
166
+ case "mixin": return new Mixin((value.rules ?? []).map((decl) => {
158
167
  const decoded = decodeBridgeValue(decl.value);
159
168
  const cssText = decoded?.toCSS ? decoded.toCSS() : String(decoded);
160
169
  return new Declaration(decl.name, new Anonymous(cssText));
@@ -191,21 +200,29 @@ const encodeBridgeValue = (value) => {
191
200
  quote: value.quote,
192
201
  escaped: value.escaped
193
202
  };
194
- if (value instanceof Keyword) return {
203
+ if (value instanceof Keyword || value instanceof Anonymous) return {
195
204
  __jessBridge: true,
196
- kind: "keyword",
205
+ kind: "anonymous",
197
206
  value: String(value.value)
198
207
  };
199
- if (value instanceof Anonymous) return {
208
+ if (value instanceof Expression) return {
200
209
  __jessBridge: true,
201
- kind: "anonymous",
202
- value: String(value.value)
210
+ kind: "expression",
211
+ items: value.value.map(encodeBridgeChildValue)
203
212
  };
204
- if (value instanceof Value || value instanceof Expression) return {
213
+ if (value instanceof Value) return {
205
214
  __jessBridge: true,
206
- kind: value instanceof Expression ? "sequence" : "list",
215
+ kind: "list",
207
216
  items: value.value.map(encodeBridgeChildValue),
208
- separator: value instanceof Expression ? void 0 : value.separator
217
+ separator: value.separator === "/" || value.separator === ";" ? value.separator : ","
218
+ };
219
+ if (value instanceof Mixin) return {
220
+ __jessBridge: true,
221
+ kind: "mixin",
222
+ rules: value.ruleset.rules.filter((rule) => rule instanceof Declaration && typeof rule.name === "string").map((rule) => ({
223
+ name: rule.name,
224
+ value: encodeBridgeChildValue(rule.value)
225
+ }))
209
226
  };
210
227
  return value;
211
228
  };
@@ -243,7 +260,7 @@ const loadModule = async (modulePath) => {
243
260
  }
244
261
  return exports;
245
262
  };
246
- const createLegacyLessPluginRuntime = (modulePath) => {
263
+ const createLegacyLessPluginRuntime = (modulePath, options) => {
247
264
  const localFunctions = /* @__PURE__ */ new Map();
248
265
  const functions = {
249
266
  add(name, fn) {
@@ -280,7 +297,9 @@ const createLegacyLessPluginRuntime = (modulePath) => {
280
297
  return plugin;
281
298
  }
282
299
  })() : plugin;
300
+ if (candidate && options != null && typeof candidate.setOptions === "function") candidate.setOptions(options);
283
301
  if (candidate && typeof candidate.install === "function") candidate.install(less, manager, functions);
302
+ if (candidate && options != null && typeof candidate.setOptions === "function") candidate.setOptions(options);
284
303
  };
285
304
  const require = (specifier) => {
286
305
  throw new Error(`Less @plugin require("${specifier}") is not supported in the Deno sandbox yet.`);
@@ -298,11 +317,13 @@ const createLegacyLessPluginRuntime = (modulePath) => {
298
317
  fileInfo: { filename: modulePath }
299
318
  };
300
319
  };
301
- const loadLessPlugin = async (modulePath) => {
302
- let functions = lessPluginFunctionCache.get(modulePath);
320
+ const lessPluginCacheKey = (modulePath, options) => `${modulePath}\u0000${options ?? ""}`;
321
+ const loadLessPlugin = async (modulePath, options = null) => {
322
+ const cacheKey = lessPluginCacheKey(modulePath, options);
323
+ let functions = lessPluginFunctionCache.get(cacheKey);
303
324
  if (functions) return Array.from(functions.keys());
304
325
  const source = await Deno.readTextFile(modulePath);
305
- const runtime = createLegacyLessPluginRuntime(modulePath);
326
+ const runtime = createLegacyLessPluginRuntime(modulePath, options);
306
327
  const module = { exports: runtime.exports };
307
328
  new Function("module", "exports", "require", "registerPlugin", "functions", "tree", "less", "fileInfo", "process", source)(module, module.exports, runtime.require, runtime.registerPlugin, runtime.functions, lessFacade.tree, {
308
329
  ...lessFacade,
@@ -311,14 +332,15 @@ const loadLessPlugin = async (modulePath) => {
311
332
  const exported = module.exports?.default ?? module.exports;
312
333
  if (typeof exported === "function" || exported && typeof exported.install === "function") runtime.registerPlugin(exported);
313
334
  functions = runtime.localFunctions;
314
- lessPluginFunctionCache.set(modulePath, functions);
335
+ lessPluginFunctionCache.set(cacheKey, functions);
315
336
  return Array.from(functions.keys());
316
337
  };
317
- const invokeLessPluginFunction = async (modulePath, functionName, args) => {
318
- let functions = lessPluginFunctionCache.get(modulePath);
338
+ const invokeLessPluginFunction = async (modulePath, functionName, args, options = null) => {
339
+ const cacheKey = lessPluginCacheKey(modulePath, options);
340
+ let functions = lessPluginFunctionCache.get(cacheKey);
319
341
  if (!functions) {
320
- await loadLessPlugin(modulePath);
321
- functions = lessPluginFunctionCache.get(modulePath);
342
+ await loadLessPlugin(modulePath, options);
343
+ functions = lessPluginFunctionCache.get(cacheKey);
322
344
  }
323
345
  const fn = functions?.get(String(functionName).toLowerCase());
324
346
  if (typeof fn !== "function") throw new Error(`Less @plugin function "${functionName}" is not registered.`);
@@ -357,7 +379,7 @@ const handleRequest = async (req) => {
357
379
  return;
358
380
  }
359
381
  if (req.type === "loadLessPlugin") {
360
- const functions = await loadLessPlugin(req.modulePath);
382
+ const functions = await loadLessPlugin(req.modulePath, req.options ?? null);
361
383
  send({
362
384
  id: req.id,
363
385
  ok: true,
@@ -366,7 +388,7 @@ const handleRequest = async (req) => {
366
388
  return;
367
389
  }
368
390
  if (req.type === "invokeLessPluginFunction") {
369
- const value = await invokeLessPluginFunction(req.modulePath, req.functionName, req.args ?? []);
391
+ const value = await invokeLessPluginFunction(req.modulePath, req.functionName, req.args ?? [], req.options ?? null);
370
392
  send({
371
393
  id: req.id,
372
394
  ok: true,
@@ -76,8 +76,17 @@ var Declaration = class {
76
76
  return `${this.name}: ${v}`;
77
77
  }
78
78
  };
79
- var DetachedRuleset = class {
80
- type = "DetachedRuleset";
79
+ var Nil = class {
80
+ type = "Nil";
81
+ value = "";
82
+ eval() {
83
+ return this;
84
+ }
85
+ };
86
+ var Mixin = class {
87
+ type = "Mixin";
88
+ name = new Nil();
89
+ args = new Nil();
81
90
  ruleset;
82
91
  constructor(rules) {
83
92
  this.ruleset = { rules };
@@ -113,10 +122,11 @@ const lessFacade = {
113
122
  Anonymous,
114
123
  Color,
115
124
  Declaration,
116
- DetachedRuleset,
117
125
  Dimension,
118
126
  Expression,
119
127
  Keyword,
128
+ Mixin,
129
+ Nil,
120
130
  Quoted,
121
131
  Value
122
132
  },
@@ -148,11 +158,10 @@ const decodeBridgeValue = (value) => {
148
158
  case "dimension": return new Dimension(value.value, value.unit ?? "");
149
159
  case "color": return new Color(value.rgb, value.alpha ?? 1);
150
160
  case "quoted": return new Quoted(value.quote ?? "\"", value.value, value.escaped === true);
151
- case "keyword": return new Keyword(value.value);
152
161
  case "anonymous": return new Anonymous(value.value);
153
162
  case "list": return new Value((value.items ?? []).map(decodeBridgeValue), value.separator ?? ",");
154
- case "sequence": return new Expression((value.items ?? []).map(decodeBridgeValue));
155
- case "detached": return new DetachedRuleset((value.rules ?? []).map((decl) => {
163
+ case "expression": return new Expression((value.items ?? []).map(decodeBridgeValue));
164
+ case "mixin": return new Mixin((value.rules ?? []).map((decl) => {
156
165
  const decoded = decodeBridgeValue(decl.value);
157
166
  const cssText = decoded?.toCSS ? decoded.toCSS() : String(decoded);
158
167
  return new Declaration(decl.name, new Anonymous(cssText));
@@ -189,21 +198,29 @@ const encodeBridgeValue = (value) => {
189
198
  quote: value.quote,
190
199
  escaped: value.escaped
191
200
  };
192
- if (value instanceof Keyword) return {
201
+ if (value instanceof Keyword || value instanceof Anonymous) return {
193
202
  __jessBridge: true,
194
- kind: "keyword",
203
+ kind: "anonymous",
195
204
  value: String(value.value)
196
205
  };
197
- if (value instanceof Anonymous) return {
206
+ if (value instanceof Expression) return {
198
207
  __jessBridge: true,
199
- kind: "anonymous",
200
- value: String(value.value)
208
+ kind: "expression",
209
+ items: value.value.map(encodeBridgeChildValue)
201
210
  };
202
- if (value instanceof Value || value instanceof Expression) return {
211
+ if (value instanceof Value) return {
203
212
  __jessBridge: true,
204
- kind: value instanceof Expression ? "sequence" : "list",
213
+ kind: "list",
205
214
  items: value.value.map(encodeBridgeChildValue),
206
- separator: value instanceof Expression ? void 0 : value.separator
215
+ separator: value.separator === "/" || value.separator === ";" ? value.separator : ","
216
+ };
217
+ if (value instanceof Mixin) return {
218
+ __jessBridge: true,
219
+ kind: "mixin",
220
+ rules: value.ruleset.rules.filter((rule) => rule instanceof Declaration && typeof rule.name === "string").map((rule) => ({
221
+ name: rule.name,
222
+ value: encodeBridgeChildValue(rule.value)
223
+ }))
207
224
  };
208
225
  return value;
209
226
  };
@@ -241,7 +258,7 @@ const loadModule = async (modulePath) => {
241
258
  }
242
259
  return exports;
243
260
  };
244
- const createLegacyLessPluginRuntime = (modulePath) => {
261
+ const createLegacyLessPluginRuntime = (modulePath, options) => {
245
262
  const localFunctions = /* @__PURE__ */ new Map();
246
263
  const functions = {
247
264
  add(name, fn) {
@@ -278,7 +295,9 @@ const createLegacyLessPluginRuntime = (modulePath) => {
278
295
  return plugin;
279
296
  }
280
297
  })() : plugin;
298
+ if (candidate && options != null && typeof candidate.setOptions === "function") candidate.setOptions(options);
281
299
  if (candidate && typeof candidate.install === "function") candidate.install(less, manager, functions);
300
+ if (candidate && options != null && typeof candidate.setOptions === "function") candidate.setOptions(options);
282
301
  };
283
302
  const require = (specifier) => {
284
303
  throw new Error(`Less @plugin require("${specifier}") is not supported in the Deno sandbox yet.`);
@@ -296,11 +315,13 @@ const createLegacyLessPluginRuntime = (modulePath) => {
296
315
  fileInfo: { filename: modulePath }
297
316
  };
298
317
  };
299
- const loadLessPlugin = async (modulePath) => {
300
- let functions = lessPluginFunctionCache.get(modulePath);
318
+ const lessPluginCacheKey = (modulePath, options) => `${modulePath}\u0000${options ?? ""}`;
319
+ const loadLessPlugin = async (modulePath, options = null) => {
320
+ const cacheKey = lessPluginCacheKey(modulePath, options);
321
+ let functions = lessPluginFunctionCache.get(cacheKey);
301
322
  if (functions) return Array.from(functions.keys());
302
323
  const source = await Deno.readTextFile(modulePath);
303
- const runtime = createLegacyLessPluginRuntime(modulePath);
324
+ const runtime = createLegacyLessPluginRuntime(modulePath, options);
304
325
  const module = { exports: runtime.exports };
305
326
  new Function("module", "exports", "require", "registerPlugin", "functions", "tree", "less", "fileInfo", "process", source)(module, module.exports, runtime.require, runtime.registerPlugin, runtime.functions, lessFacade.tree, {
306
327
  ...lessFacade,
@@ -309,14 +330,15 @@ const loadLessPlugin = async (modulePath) => {
309
330
  const exported = module.exports?.default ?? module.exports;
310
331
  if (typeof exported === "function" || exported && typeof exported.install === "function") runtime.registerPlugin(exported);
311
332
  functions = runtime.localFunctions;
312
- lessPluginFunctionCache.set(modulePath, functions);
333
+ lessPluginFunctionCache.set(cacheKey, functions);
313
334
  return Array.from(functions.keys());
314
335
  };
315
- const invokeLessPluginFunction = async (modulePath, functionName, args) => {
316
- let functions = lessPluginFunctionCache.get(modulePath);
336
+ const invokeLessPluginFunction = async (modulePath, functionName, args, options = null) => {
337
+ const cacheKey = lessPluginCacheKey(modulePath, options);
338
+ let functions = lessPluginFunctionCache.get(cacheKey);
317
339
  if (!functions) {
318
- await loadLessPlugin(modulePath);
319
- functions = lessPluginFunctionCache.get(modulePath);
340
+ await loadLessPlugin(modulePath, options);
341
+ functions = lessPluginFunctionCache.get(cacheKey);
320
342
  }
321
343
  const fn = functions?.get(String(functionName).toLowerCase());
322
344
  if (typeof fn !== "function") throw new Error(`Less @plugin function "${functionName}" is not registered.`);
@@ -355,7 +377,7 @@ const handleRequest = async (req) => {
355
377
  return;
356
378
  }
357
379
  if (req.type === "loadLessPlugin") {
358
- const functions = await loadLessPlugin(req.modulePath);
380
+ const functions = await loadLessPlugin(req.modulePath, req.options ?? null);
359
381
  send({
360
382
  id: req.id,
361
383
  ok: true,
@@ -364,7 +386,7 @@ const handleRequest = async (req) => {
364
386
  return;
365
387
  }
366
388
  if (req.type === "invokeLessPluginFunction") {
367
- const value = await invokeLessPluginFunction(req.modulePath, req.functionName, req.args ?? []);
389
+ const value = await invokeLessPluginFunction(req.modulePath, req.functionName, req.args ?? [], req.options ?? null);
368
390
  send({
369
391
  id: req.id,
370
392
  ok: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jesscss/plugin-js",
3
- "version": "2.0.0-alpha.7",
3
+ "version": "2.0.0-alpha.9",
4
4
  "description": "Jess plugin for JavaScript/TypeScript module imports with Deno runtime checks",
5
5
  "main": "lib/index.cjs",
6
6
  "types": "lib/index.d.ts",
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "deno": "^2.6.8",
21
- "@jesscss/core": "2.0.0-alpha.7"
21
+ "@jesscss/core": "2.0.0-alpha.9"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^20.0.0",