@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/README.md +48 -3
- package/lib/bridge.d.ts +51 -0
- package/lib/bridge.d.ts.map +1 -0
- package/lib/index.cjs +191 -8
- package/lib/index.d.ts +24 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +191 -10
- package/lib/runtime-worker.cjs +412 -0
- package/lib/runtime-worker.js +411 -0
- package/package.json +3 -4
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"], {
|
|
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)
|
|
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
|
|
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 };
|