@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.
package/lib/index.js CHANGED
@@ -1,442 +1,783 @@
1
- import { AbstractPlugin } from '@jesscss/core';
2
- import * as fs from 'node:fs';
3
- import * as net from 'node:net';
4
- import * as path from 'node:path';
5
- import { fileURLToPath, pathToFileURL } from 'node:url';
6
- import { spawn, spawnSync } from 'node:child_process';
1
+ import { AbstractPlugin } from "@jesscss/core";
2
+ import * as fs from "node:fs";
3
+ import * as net from "node:net";
4
+ import * as path from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import { HEX, makeColorRgb, makeDimension, makeKeyword, makeList, makeQuoted, sniffLiteral } from "@jesscss/core/value";
8
+ //#region src/bridge.ts
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);
13
+ function encodeBridgeChildValue(value) {
14
+ const encoded = encodeBridgeValue(value);
15
+ if (isBridgeValue(encoded)) return encoded;
16
+ return {
17
+ __jessBridge: true,
18
+ kind: "scalar",
19
+ value: String(encoded)
20
+ };
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
+ }
84
+ function encodeBridgeValue(value) {
85
+ if (Array.isArray(value)) return {
86
+ __jessBridge: true,
87
+ kind: "expression",
88
+ items: value.map(encodeBridgeChildValue)
89
+ };
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
+ } : {
120
+ __jessBridge: true,
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
133
+ };
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;
144
+ return value;
145
+ }
146
+ function decodeValue(value) {
147
+ switch (value.kind) {
148
+ case "scalar": return typeof value.value === "number" ? makeDimension(value.value) : sniffLiteral(String(value.value));
149
+ case "dimension": return makeDimension(value.value, value.unit ?? "");
150
+ case "color": return makeColorRgb(value.rgb, value.alpha ?? 1, HEX);
151
+ case "quoted": return makeQuoted(value.value, value.quote ?? "\"", value.escaped === true);
152
+ case "anonymous": return sniffLiteral(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("");
156
+ }
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
+ }
185
+ function encodeBridgeArgs(args) {
186
+ return args.map(encodeBridgeValue);
187
+ }
188
+ //#endregion
189
+ //#region src/index.ts
190
+ /**
191
+ * A failure raised BY a `@plugin` script (its own `throw`, or a shim member it
192
+ * cannot use), as distinct from a value the engine simply could not compute.
193
+ * The distinction is what lets the compiler report a plugin fault loudly and
194
+ * attributably instead of preserving the call verbatim.
195
+ */
196
+ var PluginFunctionError = class extends Error {
197
+ functionName;
198
+ pluginStack;
199
+ originalName;
200
+ constructor(functionName, reason, pluginStack, originalName = "Error") {
201
+ super(`Less @plugin function "${functionName}" threw: ${reason}`);
202
+ this.name = "PluginFunctionError";
203
+ this.functionName = functionName;
204
+ this.pluginStack = pluginStack;
205
+ this.originalName = originalName;
206
+ }
207
+ };
208
+ /**
209
+ * Child-process stdio streams are typed as bare `Writable`/`Readable`, which do
210
+ * not declare `unref`, but the underlying pipe sockets expose it at runtime.
211
+ * The optional structural member lets any stream be passed without an unsafe
212
+ * cast; the runtime `?.` guard covers streams that genuinely lack the method.
213
+ */
214
+ const unrefStream = (stream) => {
215
+ if (typeof stream === "object" && stream !== null && "unref" in stream && typeof stream.unref === "function") stream.unref();
216
+ };
7
217
  const SCRIPT_EXTENSIONS = new Set([
8
- '.js',
9
- '.mjs',
10
- '.cjs',
11
- '.ts',
12
- '.mts',
13
- '.cts'
218
+ ".js",
219
+ ".mjs",
220
+ ".cjs",
221
+ ".ts",
222
+ ".mts",
223
+ ".cts"
14
224
  ]);
15
225
  const RUNTIME_MISSING_MESSAGE = [
16
- 'Deno runtime is required for @jesscss/plugin-js, but no usable Deno binary was found.',
17
- 'If using pnpm, approve build scripts for "deno" (pnpm approve-builds).',
18
- 'If using npm with ignored scripts, reinstall with lifecycle scripts enabled.',
19
- 'Or install native Deno and ensure "deno" is on PATH.'
20
- ].join('\n');
21
- const BOOT_TIMEOUT_MS = 8000;
22
- const REQUEST_TIMEOUT_MS = 10_000;
226
+ "Deno runtime is required for @jesscss/plugin-js, but no usable Deno binary was found.",
227
+ "If using pnpm, approve build scripts for \"deno\" (pnpm approve-builds).",
228
+ "If using npm with ignored scripts, reinstall with lifecycle scripts enabled.",
229
+ "Or install native Deno and ensure \"deno\" is on PATH."
230
+ ].join("\n");
231
+ const BOOT_TIMEOUT_MS = 8e3;
232
+ const REQUEST_TIMEOUT_MS = 1e4;
233
+ const IDLE_SHUTDOWN_MS = 5e3;
234
+ /**
235
+ * Environment variables that inject a Node.js debugger/inspector bootloader
236
+ * into child processes (VS Code / Cursor "Auto Attach", `node --inspect`, etc.).
237
+ *
238
+ * These must never leak into the sandboxed Deno worker: the injected bootloader
239
+ * tries to read the environment, the Jess Deno sandbox denies `env` permission,
240
+ * and the worker never signals ready — producing a spurious
241
+ * "Timed out waiting for Deno worker startup" failure. Deno does not use any of
242
+ * these vars, so stripping them is safe.
243
+ */
244
+ const DEBUG_ENV_KEY_RE = /^(NODE_OPTIONS|NODE_INSPECT|VSCODE_INSPECTOR_OPTIONS)/i;
245
+ const DEBUG_ENV_VALUE_RE = /js-debug|bootloader/i;
246
+ /**
247
+ * Returns a copy of `env` with Node debugger/inspector-attach variables removed,
248
+ * so a spawned Deno subprocess starts regardless of the parent's debug env.
249
+ * The Deno permission sandbox is untouched; this only stops leaking the debugger
250
+ * into it.
251
+ */
252
+ const sanitizeSpawnEnv = (env = process.env) => {
253
+ const clean = {};
254
+ for (const [key, value] of Object.entries(env)) {
255
+ if (DEBUG_ENV_KEY_RE.test(key)) continue;
256
+ if (typeof value === "string" && DEBUG_ENV_VALUE_RE.test(value)) continue;
257
+ clean[key] = value;
258
+ }
259
+ return clean;
260
+ };
23
261
  const isPathInside = (candidatePath, rootPath) => {
24
- const rel = path.relative(rootPath, candidatePath);
25
- return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
262
+ const rel = path.relative(rootPath, candidatePath);
263
+ return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
26
264
  };
27
265
  const canonicalPath = (p) => {
28
- try {
29
- return fs.realpathSync.native(p);
30
- }
31
- catch {
32
- return p;
33
- }
266
+ try {
267
+ return fs.realpathSync.native(p);
268
+ } catch {
269
+ return p;
270
+ }
34
271
  };
35
272
  const normalizePermissionPath = (value) => {
36
- if (!value) {
37
- return null;
38
- }
39
- if (value.startsWith('file://')) {
40
- try {
41
- return fileURLToPath(value);
42
- }
43
- catch {
44
- return value;
45
- }
46
- }
47
- return value;
273
+ if (!value) return null;
274
+ if (value.startsWith("file://")) try {
275
+ return fileURLToPath(value);
276
+ } catch {
277
+ return value;
278
+ }
279
+ return value;
48
280
  };
49
281
  const isFnsPath = (importPath) => {
50
- const normalized = importPath.replace(/\\/g, '/');
51
- const isFnsPackagePath = /(^|\/)(@jesscss\/fns|packages\/fns)(\/|$)/.test(normalized);
52
- return (normalized === '@jesscss/fns'
53
- || normalized.startsWith('@jesscss/fns/')
54
- || normalized === '#less'
55
- || normalized.startsWith('#less/')
56
- || normalized === '#sass'
57
- || normalized.startsWith('#sass/')
58
- || isFnsPackagePath);
282
+ const normalized = importPath.replace(/\\/g, "/");
283
+ const isFnsPackagePath = /(^|\/)(@jesscss\/fns|packages\/fns)(\/|$)/.test(normalized);
284
+ return normalized === "@jesscss/fns" || normalized.startsWith("@jesscss/fns/") || normalized === "#less" || normalized.startsWith("#less/") || normalized === "#sass" || normalized.startsWith("#sass/") || isFnsPackagePath;
59
285
  };
60
286
  const isJsonValue = (value) => {
61
- try {
62
- JSON.stringify(value);
63
- return true;
64
- }
65
- catch {
66
- return false;
67
- }
287
+ try {
288
+ JSON.stringify(value);
289
+ return true;
290
+ } catch {
291
+ return false;
292
+ }
68
293
  };
69
- export class JsPlugin extends AbstractPlugin {
70
- opts;
71
- name = 'js';
72
- supportedExtensions = Array.from(SCRIPT_EXTENSIONS);
73
- runtimeState = { status: 'idle' };
74
- brokerServer;
75
- brokerSocketPath;
76
- worker;
77
- workerBuffer = '';
78
- nextRequestId = 1;
79
- pending = new Map();
80
- constructor(opts = {}) {
81
- super();
82
- this.opts = opts;
83
- }
84
- prewarm() {
85
- return this.ensureRuntime().catch(() => undefined);
86
- }
87
- ensureRuntimeAvailable() {
88
- const denoCommand = this.opts.denoCommand ?? 'deno';
89
- const result = spawnSync(denoCommand, ['--version'], {
90
- stdio: 'ignore'
91
- });
92
- if (result.status !== 0) {
93
- throw new Error(RUNTIME_MISSING_MESSAGE);
94
- }
95
- }
96
- ensureRuntime() {
97
- if (this.runtimeState.status === 'ready') {
98
- return Promise.resolve();
99
- }
100
- if (this.runtimeState.status === 'initializing') {
101
- return this.runtimeState.promise;
102
- }
103
- if (this.runtimeState.status === 'failed') {
104
- return Promise.reject(this.runtimeState.error);
105
- }
106
- const promise = this.startRuntime().then(() => {
107
- this.runtimeState = { status: 'ready' };
108
- }, (err) => {
109
- this.runtimeState = { status: 'failed', error: err };
110
- throw err;
111
- });
112
- this.runtimeState = { status: 'initializing', promise };
113
- return promise;
114
- }
115
- createBrokerPath() {
116
- if (process.platform === 'win32') {
117
- const rand = `${process.pid}-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
118
- return `\\\\.\\pipe\\jess-deno-broker-${rand}`;
119
- }
120
- // macOS temp dir paths can exceed unix socket limits. Keep this short.
121
- const rand = Math.floor(Math.random() * 10000);
122
- return `/tmp/jd-${process.pid}-${Date.now()}-${rand}.sock`;
123
- }
124
- isReadAllowed(value) {
125
- const normalized = normalizePermissionPath(value);
126
- if (!normalized) {
127
- return false;
128
- }
129
- const requestedPath = canonicalPath(path.resolve(normalized));
130
- const jsReadRoot = this.opts.jsReadRoot ? canonicalPath(path.resolve(this.opts.jsReadRoot)) : undefined;
131
- if (jsReadRoot && isPathInside(requestedPath, jsReadRoot)) {
132
- return true;
133
- }
134
- return requestedPath.includes(`${path.sep}node_modules${path.sep}`);
135
- }
136
- isNetAllowed(value) {
137
- if (!this.opts.allowHttp) {
138
- return false;
139
- }
140
- const allowHosts = this.opts.allowNetHosts ?? [];
141
- if (allowHosts.length === 0) {
142
- return true;
143
- }
144
- if (!value) {
145
- return false;
146
- }
147
- const host = value.split(':')[0] ?? value;
148
- return allowHosts.includes(host);
149
- }
150
- handleBrokerRequest(request) {
151
- const deny = (reason) => ({
152
- id: request.id,
153
- result: 'deny',
154
- reason
155
- });
156
- switch (request.permission) {
157
- case 'read':
158
- return this.isReadAllowed(request.value)
159
- ? { id: request.id, result: 'allow' }
160
- : deny('Read access denied by Jess policy.');
161
- case 'net':
162
- return this.isNetAllowed(request.value)
163
- ? { id: request.id, result: 'allow' }
164
- : deny('Network access denied by Jess policy.');
165
- case 'env':
166
- case 'run':
167
- case 'ffi':
168
- case 'sys':
169
- case 'write':
170
- return deny(`${request.permission} permission denied by Jess policy.`);
171
- default:
172
- return deny(`Permission "${request.permission}" denied by Jess policy.`);
173
- }
174
- }
175
- async startBroker() {
176
- const socketPath = this.createBrokerPath();
177
- if (process.platform !== 'win32' && fs.existsSync(socketPath)) {
178
- fs.unlinkSync(socketPath);
179
- }
180
- const server = net.createServer((socket) => {
181
- let buf = '';
182
- socket.setEncoding('utf8');
183
- socket.on('data', (chunk) => {
184
- buf += chunk;
185
- let idx = buf.indexOf('\n');
186
- while (idx >= 0) {
187
- const line = buf.slice(0, idx).trim();
188
- buf = buf.slice(idx + 1);
189
- idx = buf.indexOf('\n');
190
- if (!line) {
191
- continue;
192
- }
193
- let req;
194
- try {
195
- req = JSON.parse(line);
196
- }
197
- catch {
198
- socket.write(JSON.stringify({
199
- id: -1,
200
- result: 'deny',
201
- reason: 'Malformed permission request.'
202
- }) + '\n');
203
- continue;
204
- }
205
- const response = this.handleBrokerRequest(req);
206
- socket.write(`${JSON.stringify(response)}\n`);
207
- }
208
- });
209
- });
210
- await new Promise((resolve, reject) => {
211
- server.once('error', reject);
212
- server.listen(socketPath, () => resolve());
213
- });
214
- this.brokerServer = server;
215
- this.brokerSocketPath = socketPath;
216
- return socketPath;
217
- }
218
- startWorker(socketPath) {
219
- const denoCommand = this.opts.denoCommand ?? 'deno';
220
- const moduleDir = path.dirname(fileURLToPath(import.meta.url));
221
- const compiledWorkerPath = path.join(moduleDir, 'runtime-worker.js');
222
- const sourceWorkerPath = path.join(moduleDir, 'runtime-worker.ts');
223
- const workerScriptPath = fs.existsSync(compiledWorkerPath)
224
- ? compiledWorkerPath
225
- : sourceWorkerPath;
226
- const child = spawn(denoCommand, ['run', '--no-prompt', workerScriptPath], {
227
- stdio: ['pipe', 'pipe', 'pipe'],
228
- env: {
229
- ...process.env,
230
- DENO_PERMISSION_BROKER_PATH: socketPath
231
- }
232
- });
233
- this.worker = child;
234
- child.stdout.setEncoding('utf8');
235
- child.stderr.setEncoding('utf8');
236
- child.stdout.on('data', chunk => this.onWorkerStdout(chunk));
237
- child.on('exit', () => {
238
- const err = new Error('Deno worker exited unexpectedly.');
239
- this.rejectAllPending(err);
240
- if (this.runtimeState.status !== 'failed') {
241
- this.runtimeState = { status: 'failed', error: err };
242
- }
243
- });
244
- return new Promise((resolve, reject) => {
245
- const timer = setTimeout(() => {
246
- reject(new Error('Timed out waiting for Deno worker startup.'));
247
- }, BOOT_TIMEOUT_MS);
248
- const onData = (chunk) => {
249
- this.workerBuffer += chunk;
250
- let idx = this.workerBuffer.indexOf('\n');
251
- while (idx >= 0) {
252
- const line = this.workerBuffer.slice(0, idx).trim();
253
- this.workerBuffer = this.workerBuffer.slice(idx + 1);
254
- idx = this.workerBuffer.indexOf('\n');
255
- if (!line) {
256
- continue;
257
- }
258
- try {
259
- const parsed = JSON.parse(line);
260
- if (parsed.type === 'ready') {
261
- clearTimeout(timer);
262
- child.stdout.off('data', onData);
263
- resolve();
264
- return;
265
- }
266
- }
267
- catch {
268
- // ignore until ready payload appears
269
- }
270
- }
271
- };
272
- child.stdout.on('data', onData);
273
- child.once('error', (err) => {
274
- clearTimeout(timer);
275
- child.stdout.off('data', onData);
276
- reject(err);
277
- });
278
- });
279
- }
280
- async startRuntime() {
281
- this.ensureRuntimeAvailable();
282
- const socketPath = await this.startBroker();
283
- try {
284
- await this.startWorker(socketPath);
285
- }
286
- catch (err) {
287
- this.shutdown();
288
- throw err instanceof Error ? err : new Error(String(err));
289
- }
290
- }
291
- onWorkerStdout(chunk) {
292
- this.workerBuffer += chunk;
293
- let idx = this.workerBuffer.indexOf('\n');
294
- while (idx >= 0) {
295
- const line = this.workerBuffer.slice(0, idx).trim();
296
- this.workerBuffer = this.workerBuffer.slice(idx + 1);
297
- idx = this.workerBuffer.indexOf('\n');
298
- if (!line) {
299
- continue;
300
- }
301
- let parsed;
302
- try {
303
- parsed = JSON.parse(line);
304
- }
305
- catch {
306
- continue;
307
- }
308
- if (!parsed || typeof parsed.id !== 'number') {
309
- continue;
310
- }
311
- const pending = this.pending.get(parsed.id);
312
- if (!pending) {
313
- continue;
314
- }
315
- this.pending.delete(parsed.id);
316
- clearTimeout(pending.timeout);
317
- pending.resolve(parsed);
318
- }
319
- }
320
- rejectAllPending(err) {
321
- for (const pending of this.pending.values()) {
322
- clearTimeout(pending.timeout);
323
- pending.reject(err);
324
- }
325
- this.pending.clear();
326
- }
327
- async callWorker(request) {
328
- await this.ensureRuntime();
329
- if (!this.worker || !this.worker.stdin.writable) {
330
- throw new Error('Deno worker is not available.');
331
- }
332
- const id = this.nextRequestId++;
333
- const payload = { ...request, id };
334
- return await new Promise((resolve, reject) => {
335
- const timeout = setTimeout(() => {
336
- this.pending.delete(id);
337
- reject(new Error('Timed out waiting for Deno worker response.'));
338
- }, REQUEST_TIMEOUT_MS);
339
- this.pending.set(id, { resolve, reject, timeout });
340
- this.worker.stdin.write(`${JSON.stringify(payload)}\n`);
341
- });
342
- }
343
- shutdown() {
344
- if (this.worker && !this.worker.killed) {
345
- this.worker.kill();
346
- }
347
- this.worker = undefined;
348
- if (this.brokerServer) {
349
- this.brokerServer.close();
350
- this.brokerServer = undefined;
351
- }
352
- if (this.brokerSocketPath && process.platform !== 'win32') {
353
- try {
354
- if (fs.existsSync(this.brokerSocketPath)) {
355
- fs.unlinkSync(this.brokerSocketPath);
356
- }
357
- }
358
- catch {
359
- // ignore
360
- }
361
- }
362
- this.brokerSocketPath = undefined;
363
- }
364
- dispose() {
365
- this.shutdown();
366
- this.runtimeState = { status: 'idle' };
367
- }
368
- assertAllowedPath(absoluteFilePath) {
369
- const resolvedPath = path.resolve(absoluteFilePath);
370
- const jsReadRoot = this.opts.jsReadRoot ? path.resolve(this.opts.jsReadRoot) : undefined;
371
- if (!jsReadRoot) {
372
- return;
373
- }
374
- if (isPathInside(resolvedPath, jsReadRoot)) {
375
- return;
376
- }
377
- // pnpm layouts may resolve package files outside project root.
378
- if (resolvedPath.includes(`${path.sep}node_modules${path.sep}`)) {
379
- return;
380
- }
381
- throw new Error(`Script path "${resolvedPath}" is outside jsReadRoot "${jsReadRoot}"`);
382
- }
383
- async import(absoluteFilePath) {
384
- const ext = path.extname(absoluteFilePath);
385
- if (!SCRIPT_EXTENSIONS.has(ext)) {
386
- throw new Error(`Plugin "${this.name}" cannot import "${absoluteFilePath}"`);
387
- }
388
- if (!isFnsPath(absoluteFilePath)) {
389
- this.assertAllowedPath(absoluteFilePath);
390
- await this.ensureRuntime();
391
- const modulePath = path.resolve(absoluteFilePath);
392
- const loadResult = await this.callWorker({ type: 'load', modulePath });
393
- if (!loadResult.ok) {
394
- throw new Error(loadResult.error);
395
- }
396
- const moduleObject = {};
397
- const exported = loadResult.exports ?? [];
398
- for (const item of exported) {
399
- if (item.kind === 'function') {
400
- moduleObject[item.name] = async (...args) => {
401
- const invokeResult = await this.callWorker({
402
- type: 'invoke',
403
- modulePath,
404
- exportName: item.name,
405
- args
406
- });
407
- if (!invokeResult.ok) {
408
- throw new Error(invokeResult.error);
409
- }
410
- return invokeResult.value;
411
- };
412
- }
413
- else {
414
- moduleObject[item.name] = item.value;
415
- }
416
- }
417
- return moduleObject;
418
- }
419
- const modulePath = pathToFileURL(path.resolve(absoluteFilePath)).href;
420
- const module = await import(modulePath);
421
- const safeModule = {};
422
- for (const [key, value] of Object.entries(module)) {
423
- if (typeof value === 'function' || isJsonValue(value)) {
424
- safeModule[key] = value;
425
- }
426
- }
427
- return safeModule;
428
- }
429
- }
430
294
  /**
431
- * Global flag set when @jesscss/plugin-js is loaded.
432
- * less-compat checks this before running @plugin scripts (scripts must be run when plugin-js/Deno is present).
433
- */
434
- export const JESS_PLUGIN_JS_GLOBAL = '__JESS_PLUGIN_JS_AVAILABLE__';
435
- if (typeof globalThis !== 'undefined') {
436
- globalThis[JESS_PLUGIN_JS_GLOBAL] = true;
295
+ * Ceiling on resolve-and-replay rounds for a single call. Each round satisfies
296
+ * exactly one fact, so this also bounds a pathological plugin that asks for an
297
+ * unbounded number of distinct variables.
298
+ */
299
+ const MAX_FACT_ROUNDS = 64;
300
+ const EMPTY_PLUGIN_CALL_CAPABILITIES = {};
301
+ function normalizePluginCallArgs(args) {
302
+ if (args === void 0) return [];
303
+ return Array.isArray(args) ? args : [args];
437
304
  }
305
+ var JsPlugin = class JsPlugin extends AbstractPlugin {
306
+ static cleanupRegistered = false;
307
+ static liveInstances = /* @__PURE__ */ new Set();
308
+ name = "js";
309
+ supportedExtensions = Array.from(SCRIPT_EXTENSIONS);
310
+ runtimeState = { status: "idle" };
311
+ shuttingDown = false;
312
+ brokerServer;
313
+ brokerSocketPath;
314
+ worker;
315
+ workerBuffer = "";
316
+ nextRequestId = 1;
317
+ pending = /* @__PURE__ */ new Map();
318
+ idleTimer;
319
+ /**
320
+ * Variable names each legacy plugin function has been observed to read,
321
+ * keyed by module+options+function. Prefetching them turns the steady-state
322
+ * call into a single round trip instead of one round trip per read.
323
+ */
324
+ factMemo = /* @__PURE__ */ new Map();
325
+ constructor(opts = {}) {
326
+ super();
327
+ this.opts = opts;
328
+ JsPlugin.liveInstances.add(this);
329
+ JsPlugin.registerProcessCleanup();
330
+ }
331
+ prewarm() {
332
+ return this.ensureRuntime().catch(() => void 0);
333
+ }
334
+ static registerProcessCleanup() {
335
+ if (JsPlugin.cleanupRegistered) return;
336
+ JsPlugin.cleanupRegistered = true;
337
+ const cleanup = () => {
338
+ for (const instance of JsPlugin.liveInstances) try {
339
+ instance.dispose();
340
+ } catch {}
341
+ };
342
+ process.once("beforeExit", cleanup);
343
+ process.once("exit", cleanup);
344
+ }
345
+ clearIdleTimer() {
346
+ if (this.idleTimer) {
347
+ clearTimeout(this.idleTimer);
348
+ this.idleTimer = void 0;
349
+ }
350
+ }
351
+ scheduleIdleShutdown() {
352
+ this.clearIdleTimer();
353
+ if (this.pending.size > 0 || this.runtimeState.status !== "ready") return;
354
+ this.idleTimer = setTimeout(() => {
355
+ this.shutdown();
356
+ this.runtimeState = { status: "idle" };
357
+ }, IDLE_SHUTDOWN_MS);
358
+ this.idleTimer.unref?.();
359
+ }
360
+ ensureRuntimeAvailable() {
361
+ if (spawnSync(this.opts.denoCommand ?? "deno", ["--version"], {
362
+ stdio: "ignore",
363
+ env: sanitizeSpawnEnv(process.env)
364
+ }).status !== 0) throw new Error(RUNTIME_MISSING_MESSAGE);
365
+ }
366
+ ensureRuntime() {
367
+ if (this.runtimeState.status === "ready") {
368
+ this.clearIdleTimer();
369
+ return Promise.resolve();
370
+ }
371
+ if (this.runtimeState.status === "initializing") return this.runtimeState.promise;
372
+ if (this.runtimeState.status === "failed") return Promise.reject(this.runtimeState.error);
373
+ if (this.runtimeState.status === "disposed") return Promise.reject(/* @__PURE__ */ new Error("Deno worker has been disposed."));
374
+ const promise = this.startRuntime().then(() => {
375
+ this.runtimeState = { status: "ready" };
376
+ this.scheduleIdleShutdown();
377
+ }, (err) => {
378
+ this.runtimeState = {
379
+ status: "failed",
380
+ error: err
381
+ };
382
+ throw err;
383
+ });
384
+ this.runtimeState = {
385
+ status: "initializing",
386
+ promise
387
+ };
388
+ return promise;
389
+ }
390
+ createBrokerPath() {
391
+ if (process.platform === "win32") return `\\\\.\\pipe\\jess-deno-broker-${`${process.pid}-${Date.now()}-${Math.floor(Math.random() * 1e4)}`}`;
392
+ const rand = Math.floor(Math.random() * 1e4);
393
+ return `/tmp/jd-${process.pid}-${Date.now()}-${rand}.sock`;
394
+ }
395
+ isReadAllowed(value) {
396
+ const normalized = normalizePermissionPath(value);
397
+ if (!normalized) return false;
398
+ const requestedPath = canonicalPath(path.resolve(normalized));
399
+ const jsReadRoot = this.opts.jsReadRoot ? canonicalPath(path.resolve(this.opts.jsReadRoot)) : void 0;
400
+ if (jsReadRoot && isPathInside(requestedPath, jsReadRoot)) return true;
401
+ return requestedPath.includes(`${path.sep}node_modules${path.sep}`);
402
+ }
403
+ isNetAllowed(value) {
404
+ if (!this.opts.allowHttp) return false;
405
+ const allowHosts = this.opts.allowNetHosts ?? [];
406
+ if (allowHosts.length === 0) return true;
407
+ if (!value) return false;
408
+ const host = value.split(":")[0] ?? value;
409
+ return allowHosts.includes(host);
410
+ }
411
+ handleBrokerRequest(request) {
412
+ const deny = (reason) => ({
413
+ id: request.id,
414
+ result: "deny",
415
+ reason
416
+ });
417
+ switch (request.permission) {
418
+ case "read": return this.isReadAllowed(request.value) ? {
419
+ id: request.id,
420
+ result: "allow"
421
+ } : deny("Read access denied by Jess policy.");
422
+ case "net": return this.isNetAllowed(request.value) ? {
423
+ id: request.id,
424
+ result: "allow"
425
+ } : deny("Network access denied by Jess policy.");
426
+ case "env":
427
+ case "run":
428
+ case "ffi":
429
+ case "sys":
430
+ case "write": return deny(`${request.permission} permission denied by Jess policy.`);
431
+ default: return deny(`Permission "${request.permission}" denied by Jess policy.`);
432
+ }
433
+ }
434
+ async startBroker() {
435
+ const socketPath = this.createBrokerPath();
436
+ if (process.platform !== "win32" && fs.existsSync(socketPath)) fs.unlinkSync(socketPath);
437
+ const server = net.createServer((socket) => {
438
+ socket.unref();
439
+ let buf = "";
440
+ socket.setEncoding("utf8");
441
+ socket.on("data", (chunk) => {
442
+ buf += chunk;
443
+ let idx = buf.indexOf("\n");
444
+ while (idx >= 0) {
445
+ const line = buf.slice(0, idx).trim();
446
+ buf = buf.slice(idx + 1);
447
+ idx = buf.indexOf("\n");
448
+ if (!line) continue;
449
+ let req;
450
+ try {
451
+ req = JSON.parse(line);
452
+ } catch {
453
+ socket.write(JSON.stringify({
454
+ id: -1,
455
+ result: "deny",
456
+ reason: "Malformed permission request."
457
+ }) + "\n");
458
+ continue;
459
+ }
460
+ const response = this.handleBrokerRequest(req);
461
+ socket.write(`${JSON.stringify(response)}\n`);
462
+ }
463
+ });
464
+ });
465
+ await new Promise((resolve, reject) => {
466
+ server.once("error", reject);
467
+ server.listen(socketPath, () => resolve());
468
+ });
469
+ server.unref();
470
+ this.brokerServer = server;
471
+ this.brokerSocketPath = socketPath;
472
+ return socketPath;
473
+ }
474
+ startWorker(socketPath) {
475
+ const denoCommand = this.opts.denoCommand ?? "deno";
476
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
477
+ const compiledWorkerPath = path.join(moduleDir, "runtime-worker.js");
478
+ const sourceWorkerPath = path.join(moduleDir, "runtime-worker.ts");
479
+ const child = spawn(denoCommand, [
480
+ "run",
481
+ "--no-prompt",
482
+ fs.existsSync(compiledWorkerPath) ? compiledWorkerPath : sourceWorkerPath,
483
+ `--runtime-api=${this.opts.runtimeApi ?? "module"}`
484
+ ], {
485
+ stdio: [
486
+ "pipe",
487
+ "pipe",
488
+ "pipe"
489
+ ],
490
+ env: {
491
+ ...sanitizeSpawnEnv(process.env),
492
+ DENO_PERMISSION_BROKER_PATH: socketPath
493
+ }
494
+ });
495
+ this.worker = child;
496
+ child.unref();
497
+ unrefStream(child.stdin);
498
+ unrefStream(child.stdout);
499
+ unrefStream(child.stderr);
500
+ child.stdout.setEncoding("utf8");
501
+ child.stderr.setEncoding("utf8");
502
+ child.stdout.on("data", (chunk) => this.onWorkerStdout(chunk));
503
+ child.on("exit", () => {
504
+ this.worker = void 0;
505
+ if (this.shuttingDown) {
506
+ this.shuttingDown = false;
507
+ return;
508
+ }
509
+ const err = /* @__PURE__ */ new Error("Deno worker exited unexpectedly.");
510
+ this.rejectAllPending(err);
511
+ if (this.runtimeState.status !== "failed") this.runtimeState = {
512
+ status: "failed",
513
+ error: err
514
+ };
515
+ });
516
+ return new Promise((resolve, reject) => {
517
+ let stderrText = "";
518
+ const timer = setTimeout(() => {
519
+ reject(/* @__PURE__ */ new Error(stderrText.trim() ? `Timed out waiting for Deno worker startup.\n${stderrText.trim()}` : "Timed out waiting for Deno worker startup."));
520
+ }, BOOT_TIMEOUT_MS);
521
+ const onStderr = (chunk) => {
522
+ stderrText += chunk;
523
+ };
524
+ const onData = (chunk) => {
525
+ this.workerBuffer += chunk;
526
+ let idx = this.workerBuffer.indexOf("\n");
527
+ while (idx >= 0) {
528
+ const line = this.workerBuffer.slice(0, idx).trim();
529
+ this.workerBuffer = this.workerBuffer.slice(idx + 1);
530
+ idx = this.workerBuffer.indexOf("\n");
531
+ if (!line) continue;
532
+ try {
533
+ if (JSON.parse(line).type === "ready") {
534
+ clearTimeout(timer);
535
+ child.stdout.off("data", onData);
536
+ child.stderr.off("data", onStderr);
537
+ resolve();
538
+ return;
539
+ }
540
+ } catch {}
541
+ }
542
+ };
543
+ child.stdout.on("data", onData);
544
+ child.stderr.on("data", onStderr);
545
+ child.once("error", (err) => {
546
+ clearTimeout(timer);
547
+ child.stdout.off("data", onData);
548
+ child.stderr.off("data", onStderr);
549
+ reject(err);
550
+ });
551
+ });
552
+ }
553
+ async startRuntime() {
554
+ this.ensureRuntimeAvailable();
555
+ const socketPath = await this.startBroker();
556
+ try {
557
+ await this.startWorker(socketPath);
558
+ } catch (err) {
559
+ this.shutdown();
560
+ throw err instanceof Error ? err : new Error(String(err));
561
+ }
562
+ }
563
+ onWorkerStdout(chunk) {
564
+ this.workerBuffer += chunk;
565
+ let idx = this.workerBuffer.indexOf("\n");
566
+ while (idx >= 0) {
567
+ const line = this.workerBuffer.slice(0, idx).trim();
568
+ this.workerBuffer = this.workerBuffer.slice(idx + 1);
569
+ idx = this.workerBuffer.indexOf("\n");
570
+ if (!line) continue;
571
+ let parsed;
572
+ try {
573
+ parsed = JSON.parse(line);
574
+ } catch {
575
+ continue;
576
+ }
577
+ if (!parsed || typeof parsed.id !== "number") continue;
578
+ const pending = this.pending.get(parsed.id);
579
+ if (!pending) continue;
580
+ this.pending.delete(parsed.id);
581
+ clearTimeout(pending.timeout);
582
+ pending.resolve(parsed);
583
+ if (this.pending.size === 0) this.scheduleIdleShutdown();
584
+ }
585
+ }
586
+ rejectAllPending(err) {
587
+ for (const pending of this.pending.values()) {
588
+ clearTimeout(pending.timeout);
589
+ pending.reject(err);
590
+ }
591
+ this.pending.clear();
592
+ }
593
+ async callWorker(request) {
594
+ await this.ensureRuntime();
595
+ this.clearIdleTimer();
596
+ if (!this.worker?.stdin.writable) throw new Error("Deno worker is not available.");
597
+ const id = this.nextRequestId++;
598
+ const payload = {
599
+ ...request,
600
+ id
601
+ };
602
+ return await new Promise((resolve, reject) => {
603
+ const timeout = setTimeout(() => {
604
+ this.pending.delete(id);
605
+ reject(/* @__PURE__ */ new Error("Timed out waiting for Deno worker response."));
606
+ }, REQUEST_TIMEOUT_MS);
607
+ this.pending.set(id, {
608
+ resolve,
609
+ reject,
610
+ timeout
611
+ });
612
+ this.worker.stdin.write(`${JSON.stringify(payload)}\n`);
613
+ });
614
+ }
615
+ shutdown() {
616
+ this.clearIdleTimer();
617
+ if (this.worker && !this.worker.killed) {
618
+ this.shuttingDown = true;
619
+ this.worker.stdin.destroy();
620
+ this.worker.stdout.destroy();
621
+ this.worker.stderr.destroy();
622
+ this.worker.kill();
623
+ }
624
+ this.worker = void 0;
625
+ if (this.brokerServer) {
626
+ this.brokerServer.close();
627
+ this.brokerServer = void 0;
628
+ }
629
+ if (this.brokerSocketPath && process.platform !== "win32") try {
630
+ if (fs.existsSync(this.brokerSocketPath)) fs.unlinkSync(this.brokerSocketPath);
631
+ } catch {}
632
+ this.brokerSocketPath = void 0;
633
+ }
634
+ dispose() {
635
+ this.shutdown();
636
+ JsPlugin.liveInstances.delete(this);
637
+ this.runtimeState = { status: "disposed" };
638
+ }
639
+ assertAllowedPath(absoluteFilePath) {
640
+ const resolvedPath = path.resolve(absoluteFilePath);
641
+ const jsReadRoot = this.opts.jsReadRoot ? path.resolve(this.opts.jsReadRoot) : void 0;
642
+ if (!jsReadRoot) return;
643
+ if (isPathInside(resolvedPath, jsReadRoot)) return;
644
+ if (resolvedPath.includes(`${path.sep}node_modules${path.sep}`)) return;
645
+ throw new Error(`Script path "${resolvedPath}" is outside jsReadRoot "${jsReadRoot}"`);
646
+ }
647
+ async import(absoluteFilePath) {
648
+ const ext = path.extname(absoluteFilePath);
649
+ if (!SCRIPT_EXTENSIONS.has(ext)) throw new Error(`Plugin "${this.name}" cannot import "${absoluteFilePath}"`);
650
+ if (!isFnsPath(absoluteFilePath)) {
651
+ this.assertAllowedPath(absoluteFilePath);
652
+ await this.ensureRuntime();
653
+ const modulePath = path.resolve(absoluteFilePath);
654
+ const loadResult = await this.callWorker({
655
+ type: "load",
656
+ modulePath
657
+ });
658
+ if (!loadResult.ok) throw new Error(loadResult.error);
659
+ const moduleObject = {};
660
+ const exported = loadResult.exports ?? [];
661
+ for (const item of exported) if (item.kind === "function") moduleObject[item.name] = async (...args) => {
662
+ const invokeResult = await this.callWorker({
663
+ type: "invoke",
664
+ modulePath,
665
+ exportName: item.name,
666
+ args: encodeBridgeArgs(args)
667
+ });
668
+ if (!invokeResult.ok) throw new Error(invokeResult.error);
669
+ return decodeBridgeValue(invokeResult.value);
670
+ };
671
+ else moduleObject[item.name] = item.value;
672
+ return moduleObject;
673
+ }
674
+ const module = await import(pathToFileURL(path.resolve(absoluteFilePath)).href);
675
+ const safeModule = {};
676
+ for (const [key, value] of Object.entries(module)) if (typeof value === "function" || isJsonValue(value)) safeModule[key] = value;
677
+ return safeModule;
678
+ }
679
+ /**
680
+ * Loads a legacy Less `@plugin` wrapper file in Deno Less-compat mode.
681
+ *
682
+ * @deprecated Less `@plugin` is deprecated. Prefer `@-from` for
683
+ * ESM-style script imports or `@-use` for Sass-module-style namespace imports.
684
+ */
685
+ async importLessPlugin(absoluteFilePath, options = null) {
686
+ const ext = path.extname(absoluteFilePath);
687
+ if (!SCRIPT_EXTENSIONS.has(ext)) throw new Error(`Plugin "${this.name}" cannot import Less plugin "${absoluteFilePath}"`);
688
+ this.assertAllowedPath(absoluteFilePath);
689
+ await this.ensureRuntime();
690
+ const modulePath = path.resolve(absoluteFilePath);
691
+ const loadResult = await this.callWorker({
692
+ type: "loadLessPlugin",
693
+ modulePath,
694
+ options
695
+ });
696
+ if (!loadResult.ok) throw new PluginFunctionError(path.basename(modulePath), loadResult.error ?? "an unknown sandbox failure", loadResult.stack);
697
+ const functions = {};
698
+ for (const functionName of loadResult.functions ?? []) functions[functionName] = (args, capabilities) => this.invokePluginFunction(modulePath, options, functionName, normalizePluginCallArgs(args), capabilities ?? EMPTY_PLUGIN_CALL_CAPABILITIES);
699
+ return { functions };
700
+ }
701
+ /**
702
+ * Runs one legacy `@plugin` function, resolving the scope/built-in facts its
703
+ * body reads. The sandbox reports one unmet fact at a time; the host answers
704
+ * it from the LIVE call-site capabilities and replays. The names a function
705
+ * asked for are remembered per function, so steady-state calls ship the facts
706
+ * up front and complete in a single round trip.
707
+ */
708
+ invokePluginFunction(modulePath, options, functionName, args, capabilities) {
709
+ const memoKey = `${modulePath} ${options ?? ""} ${functionName}`;
710
+ const known = this.factMemo.get(memoKey) ?? /* @__PURE__ */ new Set();
711
+ const facts = {
712
+ vars: {},
713
+ calls: {},
714
+ fileInfo: capabilities.currentFileInfo ?? null
715
+ };
716
+ for (const name of known) facts.vars[name] = this.resolveVariableFact(name, capabilities);
717
+ const request = {
718
+ type: "invokeLessPluginFunction",
719
+ modulePath,
720
+ options,
721
+ functionName,
722
+ args: encodeBridgeArgs([...args]),
723
+ facts
724
+ };
725
+ /**
726
+ * Folds one reply. Returns the decoded value, or `undefined` to signal that
727
+ * `facts` has been extended and the call must be replayed.
728
+ */
729
+ const settle = (result) => {
730
+ for (const record of result.logs ?? []) capabilities.log?.(record);
731
+ if (result.ok) {
732
+ if (result.important) capabilities.markImportant?.();
733
+ return { value: decodeBridgeValue(result.value) };
734
+ }
735
+ if (!result.need) throw new PluginFunctionError(functionName, result.error ?? "an unknown sandbox failure", result.stack, result.errorName);
736
+ if (result.need.kind === "variable") {
737
+ const name = result.need.name;
738
+ facts.vars[name] = this.resolveVariableFact(name, capabilities);
739
+ known.add(name);
740
+ this.factMemo.set(memoKey, known);
741
+ return;
742
+ }
743
+ facts.calls[result.need.key] = this.resolveCallFact(result.need, capabilities);
744
+ };
745
+ const exhausted = () => new PluginFunctionError(functionName, `resolving its scope reads did not settle after ${MAX_FACT_ROUNDS} rounds.`);
746
+ const replay = async () => {
747
+ for (let round = 0; round <= MAX_FACT_ROUNDS; round++) {
748
+ const settled = settle(await this.callWorker(request));
749
+ if (settled) return settled.value;
750
+ }
751
+ throw exhausted();
752
+ };
753
+ return replay();
754
+ }
755
+ resolveVariableFact(name, capabilities) {
756
+ const hit = capabilities.lookupVariable?.(name);
757
+ if (!hit) return null;
758
+ return {
759
+ value: encodeBridgeValue(hit.value),
760
+ important: hit.important === true
761
+ };
762
+ }
763
+ resolveCallFact(need, capabilities) {
764
+ const decoded = need.args.map(decodeBridgeValue);
765
+ const answer = capabilities.callFunction?.(need.name, decoded);
766
+ return answer === void 0 ? null : encodeBridgeValue(answer);
767
+ }
768
+ /** Context-facing executable-plugin capability used by the direct AST path. */
769
+ async importPlugin(absoluteFilePath, options = null) {
770
+ return this.importLessPlugin(absoluteFilePath, options);
771
+ }
772
+ };
773
+ /**
774
+ * Global flag set when @jesscss/plugin-js is loaded.
775
+ * less-compat checks this before running @plugin scripts (scripts must be run when plugin-js/Deno is present).
776
+ */
777
+ const JESS_PLUGIN_JS_GLOBAL = "__JESS_PLUGIN_JS_AVAILABLE__";
778
+ if (typeof globalThis !== "undefined") globalThis[JESS_PLUGIN_JS_GLOBAL] = true;
438
779
  const jsPlugin = ((opts) => {
439
- return new JsPlugin(opts);
780
+ return new JsPlugin(opts);
440
781
  });
441
- export default jsPlugin;
442
- //# sourceMappingURL=index.js.map
782
+ //#endregion
783
+ export { JESS_PLUGIN_JS_GLOBAL, JsPlugin, PluginFunctionError, jsPlugin as default, sanitizeSpawnEnv };