@jesscss/plugin-js 2.0.0-alpha.6 → 2.0.0-alpha.8

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/lib/index.js CHANGED
@@ -1,9 +1,102 @@
1
- import { AbstractPlugin } from "@jesscss/core";
1
+ import { AbstractPlugin, Any, Color, ColorFormat, Declaration, Dimension, List, Quoted, Rules, Sequence } 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
+ //#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
+ };
14
+ function encodeBridgeChildValue(value) {
15
+ const encoded = encodeBridgeValue(value);
16
+ if (isBridgeValue(encoded)) return encoded;
17
+ return {
18
+ __jessBridge: true,
19
+ kind: "scalar",
20
+ value: typeof encoded === "string" || typeof encoded === "number" || typeof encoded === "boolean" ? encoded : String(encoded)
21
+ };
22
+ }
23
+ 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 {
38
+ __jessBridge: true,
39
+ kind: "quoted",
40
+ value: String(value.value),
41
+ quote: value.quote,
42
+ escaped: value.escaped
43
+ };
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 {
67
+ __jessBridge: true,
68
+ kind: "detached",
69
+ rules
70
+ };
71
+ }
72
+ return value;
73
+ }
74
+ function decodeBridgeValue(value) {
75
+ if (!isBridgeValue(value)) return value;
76
+ 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)));
94
+ }
95
+ }
96
+ function encodeBridgeArgs(args) {
97
+ return args.map(encodeBridgeValue);
98
+ }
99
+ //#endregion
7
100
  //#region src/index.ts
8
101
  const SCRIPT_EXTENSIONS = new Set([
9
102
  ".js",
@@ -22,6 +115,33 @@ const RUNTIME_MISSING_MESSAGE = [
22
115
  const BOOT_TIMEOUT_MS = 8e3;
23
116
  const REQUEST_TIMEOUT_MS = 1e4;
24
117
  const IDLE_SHUTDOWN_MS = 5e3;
118
+ /**
119
+ * Environment variables that inject a Node.js debugger/inspector bootloader
120
+ * into child processes (VS Code / Cursor "Auto Attach", `node --inspect`, etc.).
121
+ *
122
+ * These must never leak into the sandboxed Deno worker: the injected bootloader
123
+ * tries to read the environment, the Jess Deno sandbox denies `env` permission,
124
+ * and the worker never signals ready — producing a spurious
125
+ * "Timed out waiting for Deno worker startup" failure. Deno does not use any of
126
+ * these vars, so stripping them is safe.
127
+ */
128
+ const DEBUG_ENV_KEY_RE = /^(NODE_OPTIONS|NODE_INSPECT|VSCODE_INSPECTOR_OPTIONS)/i;
129
+ const DEBUG_ENV_VALUE_RE = /js-debug|bootloader/i;
130
+ /**
131
+ * Returns a copy of `env` with Node debugger/inspector-attach variables removed,
132
+ * so a spawned Deno subprocess starts regardless of the parent's debug env.
133
+ * The Deno permission sandbox is untouched; this only stops leaking the debugger
134
+ * into it.
135
+ */
136
+ const sanitizeSpawnEnv = (env = process.env) => {
137
+ const clean = {};
138
+ for (const [key, value] of Object.entries(env)) {
139
+ if (DEBUG_ENV_KEY_RE.test(key)) continue;
140
+ if (typeof value === "string" && DEBUG_ENV_VALUE_RE.test(value)) continue;
141
+ clean[key] = value;
142
+ }
143
+ return clean;
144
+ };
25
145
  const isPathInside = (candidatePath, rootPath) => {
26
146
  const rel = path.relative(rootPath, candidatePath);
27
147
  return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
@@ -61,6 +181,7 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
61
181
  name = "js";
62
182
  supportedExtensions = Array.from(SCRIPT_EXTENSIONS);
63
183
  runtimeState = { status: "idle" };
184
+ shuttingDown = false;
64
185
  brokerServer;
65
186
  brokerSocketPath;
66
187
  worker;
@@ -104,7 +225,10 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
104
225
  this.idleTimer.unref?.();
105
226
  }
106
227
  ensureRuntimeAvailable() {
107
- if (spawnSync(this.opts.denoCommand ?? "deno", ["--version"], { stdio: "ignore" }).status !== 0) throw new Error(RUNTIME_MISSING_MESSAGE);
228
+ if (spawnSync(this.opts.denoCommand ?? "deno", ["--version"], {
229
+ stdio: "ignore",
230
+ env: sanitizeSpawnEnv(process.env)
231
+ }).status !== 0) throw new Error(RUNTIME_MISSING_MESSAGE);
108
232
  }
109
233
  ensureRuntime() {
110
234
  if (this.runtimeState.status === "ready") {
@@ -113,6 +237,7 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
113
237
  }
114
238
  if (this.runtimeState.status === "initializing") return this.runtimeState.promise;
115
239
  if (this.runtimeState.status === "failed") return Promise.reject(this.runtimeState.error);
240
+ if (this.runtimeState.status === "disposed") return Promise.reject(/* @__PURE__ */ new Error("Deno worker has been disposed."));
116
241
  const promise = this.startRuntime().then(() => {
117
242
  this.runtimeState = { status: "ready" };
118
243
  this.scheduleIdleShutdown();
@@ -177,6 +302,7 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
177
302
  const socketPath = this.createBrokerPath();
178
303
  if (process.platform !== "win32" && fs.existsSync(socketPath)) fs.unlinkSync(socketPath);
179
304
  const server = net.createServer((socket) => {
305
+ socket.unref();
180
306
  let buf = "";
181
307
  socket.setEncoding("utf8");
182
308
  socket.on("data", (chunk) => {
@@ -207,6 +333,7 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
207
333
  server.once("error", reject);
208
334
  server.listen(socketPath, () => resolve());
209
335
  });
336
+ server.unref();
210
337
  this.brokerServer = server;
211
338
  this.brokerSocketPath = socketPath;
212
339
  return socketPath;
@@ -219,7 +346,8 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
219
346
  const child = spawn(denoCommand, [
220
347
  "run",
221
348
  "--no-prompt",
222
- fs.existsSync(compiledWorkerPath) ? compiledWorkerPath : sourceWorkerPath
349
+ fs.existsSync(compiledWorkerPath) ? compiledWorkerPath : sourceWorkerPath,
350
+ `--runtime-api=${this.opts.runtimeApi ?? "module"}`
223
351
  ], {
224
352
  stdio: [
225
353
  "pipe",
@@ -227,15 +355,24 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
227
355
  "pipe"
228
356
  ],
229
357
  env: {
230
- ...process.env,
358
+ ...sanitizeSpawnEnv(process.env),
231
359
  DENO_PERMISSION_BROKER_PATH: socketPath
232
360
  }
233
361
  });
234
362
  this.worker = child;
363
+ child.unref();
364
+ child.stdin.unref?.();
365
+ child.stdout.unref?.();
366
+ child.stderr.unref?.();
235
367
  child.stdout.setEncoding("utf8");
236
368
  child.stderr.setEncoding("utf8");
237
369
  child.stdout.on("data", (chunk) => this.onWorkerStdout(chunk));
238
370
  child.on("exit", () => {
371
+ this.worker = void 0;
372
+ if (this.shuttingDown) {
373
+ this.shuttingDown = false;
374
+ return;
375
+ }
239
376
  const err = /* @__PURE__ */ new Error("Deno worker exited unexpectedly.");
240
377
  this.rejectAllPending(err);
241
378
  if (this.runtimeState.status !== "failed") this.runtimeState = {
@@ -244,9 +381,13 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
244
381
  };
245
382
  });
246
383
  return new Promise((resolve, reject) => {
384
+ let stderrText = "";
247
385
  const timer = setTimeout(() => {
248
- reject(/* @__PURE__ */ new Error("Timed out waiting for Deno worker startup."));
386
+ reject(/* @__PURE__ */ new Error(stderrText.trim() ? `Timed out waiting for Deno worker startup.\n${stderrText.trim()}` : "Timed out waiting for Deno worker startup."));
249
387
  }, BOOT_TIMEOUT_MS);
388
+ const onStderr = (chunk) => {
389
+ stderrText += chunk;
390
+ };
250
391
  const onData = (chunk) => {
251
392
  this.workerBuffer += chunk;
252
393
  let idx = this.workerBuffer.indexOf("\n");
@@ -259,6 +400,7 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
259
400
  if (JSON.parse(line).type === "ready") {
260
401
  clearTimeout(timer);
261
402
  child.stdout.off("data", onData);
403
+ child.stderr.off("data", onStderr);
262
404
  resolve();
263
405
  return;
264
406
  }
@@ -266,9 +408,11 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
266
408
  }
267
409
  };
268
410
  child.stdout.on("data", onData);
411
+ child.stderr.on("data", onStderr);
269
412
  child.once("error", (err) => {
270
413
  clearTimeout(timer);
271
414
  child.stdout.off("data", onData);
415
+ child.stderr.off("data", onStderr);
272
416
  reject(err);
273
417
  });
274
418
  });
@@ -337,7 +481,13 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
337
481
  }
338
482
  shutdown() {
339
483
  this.clearIdleTimer();
340
- if (this.worker && !this.worker.killed) this.worker.kill();
484
+ if (this.worker && !this.worker.killed) {
485
+ this.shuttingDown = true;
486
+ this.worker.stdin.destroy();
487
+ this.worker.stdout.destroy();
488
+ this.worker.stderr.destroy();
489
+ this.worker.kill();
490
+ }
341
491
  this.worker = void 0;
342
492
  if (this.brokerServer) {
343
493
  this.brokerServer.close();
@@ -350,7 +500,8 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
350
500
  }
351
501
  dispose() {
352
502
  this.shutdown();
353
- this.runtimeState = { status: "idle" };
503
+ JsPlugin.liveInstances.delete(this);
504
+ this.runtimeState = { status: "disposed" };
354
505
  }
355
506
  assertAllowedPath(absoluteFilePath) {
356
507
  const resolvedPath = path.resolve(absoluteFilePath);
@@ -379,10 +530,10 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
379
530
  type: "invoke",
380
531
  modulePath,
381
532
  exportName: item.name,
382
- args
533
+ args: encodeBridgeArgs(args)
383
534
  });
384
535
  if (!invokeResult.ok) throw new Error(invokeResult.error);
385
- return invokeResult.value;
536
+ return decodeBridgeValue(invokeResult.value);
386
537
  };
387
538
  else moduleObject[item.name] = item.value;
388
539
  return moduleObject;
@@ -392,6 +543,36 @@ var JsPlugin = class JsPlugin extends AbstractPlugin {
392
543
  for (const [key, value] of Object.entries(module)) if (typeof value === "function" || isJsonValue(value)) safeModule[key] = value;
393
544
  return safeModule;
394
545
  }
546
+ /**
547
+ * Loads a legacy Less `@plugin` wrapper file in Deno Less-compat mode.
548
+ *
549
+ * @deprecated Less `@plugin` is deprecated. Prefer `@-from` for
550
+ * ESM-style script imports or `@-use` for Sass-module-style namespace imports.
551
+ */
552
+ async importLessPlugin(absoluteFilePath) {
553
+ const ext = path.extname(absoluteFilePath);
554
+ if (!SCRIPT_EXTENSIONS.has(ext)) throw new Error(`Plugin "${this.name}" cannot import Less plugin "${absoluteFilePath}"`);
555
+ this.assertAllowedPath(absoluteFilePath);
556
+ await this.ensureRuntime();
557
+ const modulePath = path.resolve(absoluteFilePath);
558
+ const loadResult = await this.callWorker({
559
+ type: "loadLessPlugin",
560
+ modulePath
561
+ });
562
+ if (!loadResult.ok) throw new Error(loadResult.error);
563
+ const functions = {};
564
+ for (const functionName of loadResult.functions ?? []) functions[functionName] = async (...args) => {
565
+ const invokeResult = await this.callWorker({
566
+ type: "invokeLessPluginFunction",
567
+ modulePath,
568
+ functionName,
569
+ args: encodeBridgeArgs(args)
570
+ });
571
+ if (!invokeResult.ok) throw new Error(invokeResult.error);
572
+ return decodeBridgeValue(invokeResult.value);
573
+ };
574
+ return { functions };
575
+ }
395
576
  };
396
577
  /**
397
578
  * Global flag set when @jesscss/plugin-js is loaded.
@@ -403,4 +584,4 @@ const jsPlugin = ((opts) => {
403
584
  return new JsPlugin(opts);
404
585
  });
405
586
  //#endregion
406
- export { JESS_PLUGIN_JS_GLOBAL, JsPlugin, jsPlugin as default };
587
+ export { JESS_PLUGIN_JS_GLOBAL, JsPlugin, jsPlugin as default, sanitizeSpawnEnv };