feat(smartdeno): Run small scripts in-memory via stdin, fall back to temp files for large scripts; remove internal HTTP script server and simplify plugin dependencies; update API and docs.
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartdeno',
|
||||
version: '1.1.0',
|
||||
version: '1.2.0',
|
||||
description: 'A module to run Deno scripts from Node.js, including functionalities for downloading Deno and executing Deno scripts.'
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { ScriptServer } from './classes.scriptserver.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export type TDenoPermission =
|
||||
| 'all'
|
||||
| 'env'
|
||||
| 'ffi'
|
||||
| 'hrtime'
|
||||
| 'net'
|
||||
| 'read'
|
||||
| 'run'
|
||||
| 'sys'
|
||||
| 'write';
|
||||
|
||||
export interface IDenoExecutionOptions {
|
||||
permissions?: TDenoPermission[];
|
||||
denoBinaryPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class contains logic to execute deno commands in an ephemeral way
|
||||
*/
|
||||
export class DenoExecution {
|
||||
public id: string;
|
||||
public scriptserverRef: ScriptServer;
|
||||
public script: string;
|
||||
public options: IDenoExecutionOptions;
|
||||
|
||||
constructor(scriptserverRef: ScriptServer, scriptArg: string, options: IDenoExecutionOptions = {}) {
|
||||
this.scriptserverRef = scriptserverRef;
|
||||
this.script = scriptArg;
|
||||
this.options = options;
|
||||
this.id = plugins.smartunique.shortId();
|
||||
}
|
||||
|
||||
private buildPermissionFlags(): string {
|
||||
const permissions = this.options.permissions || [];
|
||||
if (permissions.length === 0) {
|
||||
return '';
|
||||
}
|
||||
if (permissions.includes('all')) {
|
||||
return '-A';
|
||||
}
|
||||
return permissions.map(p => `--allow-${p}`).join(' ');
|
||||
}
|
||||
|
||||
public async execute(): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
||||
this.scriptserverRef.executionMap.add(this);
|
||||
|
||||
try {
|
||||
const denoBinary = this.options.denoBinaryPath || 'deno';
|
||||
const permissionFlags = this.buildPermissionFlags();
|
||||
const port = this.scriptserverRef.getPort();
|
||||
const scriptUrl = `http://localhost:${port}/getscript/${this.id}`;
|
||||
|
||||
const command = `${denoBinary} run ${permissionFlags} ${scriptUrl}`.replace(/\s+/g, ' ').trim();
|
||||
const result = await this.scriptserverRef.smartshellInstance.exec(command);
|
||||
|
||||
return {
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
};
|
||||
} finally {
|
||||
// Clean up: remove from execution map after execution completes
|
||||
this.scriptserverRef.executionMap.remove(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import type { DenoExecution } from './classes.denoexecution.js';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export interface IScriptServerOptions {
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export class ScriptServer {
|
||||
private server: plugins.typedserver.servertools.Server;
|
||||
private port: number;
|
||||
public smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash'
|
||||
});
|
||||
|
||||
public executionMap = new plugins.lik.ObjectMap<DenoExecution>();
|
||||
|
||||
constructor(options: IScriptServerOptions = {}) {
|
||||
this.port = options.port ?? 3210;
|
||||
}
|
||||
|
||||
public getPort(): number {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
public async start() {
|
||||
this.server = new plugins.typedserver.servertools.Server({
|
||||
port: this.port,
|
||||
cors: true,
|
||||
});
|
||||
this.server.addRoute(
|
||||
'/getscript/:executionId',
|
||||
new plugins.typedserver.servertools.Handler('GET', async (req, res) => {
|
||||
const executionId = req.params.executionId;
|
||||
const denoExecution = await this.executionMap.find(async denoExecutionArg => {
|
||||
return denoExecutionArg.id === executionId;
|
||||
});
|
||||
if (!denoExecution) {
|
||||
res.statusCode = 404;
|
||||
res.write('Execution not found');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.write(denoExecution.script);
|
||||
res.end();
|
||||
})
|
||||
);
|
||||
await this.server.start();
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
if (this.server) {
|
||||
await this.server.stop();
|
||||
}
|
||||
this.executionMap.wipe();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,25 @@
|
||||
import { DenoDownloader } from './classes.denodownloader.js';
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import { ScriptServer } from './classes.scriptserver.js';
|
||||
import { DenoExecution, type TDenoPermission } from './classes.denoexecution.js';
|
||||
|
||||
const MAX_STDIN_SIZE = 2 * 1024 * 1024; // 2MB threshold for stdin execution
|
||||
|
||||
export type TDenoPermission =
|
||||
| 'all'
|
||||
| 'env'
|
||||
| 'ffi'
|
||||
| 'hrtime'
|
||||
| 'net'
|
||||
| 'read'
|
||||
| 'run'
|
||||
| 'sys'
|
||||
| 'write';
|
||||
|
||||
export interface ISmartDenoOptions {
|
||||
/**
|
||||
* Force downloading a local copy of Deno even if it's available in PATH
|
||||
*/
|
||||
forceLocalDeno?: boolean;
|
||||
/**
|
||||
* Port for the internal script server (default: 3210)
|
||||
*/
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export interface IExecuteScriptOptions {
|
||||
@@ -24,16 +31,14 @@ export interface IExecuteScriptOptions {
|
||||
|
||||
export class SmartDeno {
|
||||
private denoDownloader = new DenoDownloader();
|
||||
private scriptServer: ScriptServer;
|
||||
private denoBinaryPath: string | null = null;
|
||||
private isStarted = false;
|
||||
|
||||
constructor() {
|
||||
this.scriptServer = new ScriptServer();
|
||||
}
|
||||
private smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
});
|
||||
|
||||
/**
|
||||
* Starts the SmartDeno instance
|
||||
* Starts the SmartDeno instance (downloads Deno if needed)
|
||||
* @param optionsArg Configuration options
|
||||
*/
|
||||
public async start(optionsArg: ISmartDenoOptions = {}): Promise<void> {
|
||||
@@ -41,11 +46,8 @@ export class SmartDeno {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create script server with configured port
|
||||
this.scriptServer = new ScriptServer({ port: optionsArg.port });
|
||||
|
||||
const denoAlreadyInPath = await plugins.smartshell.which('deno', {
|
||||
nothrow: true
|
||||
nothrow: true,
|
||||
});
|
||||
|
||||
if (!denoAlreadyInPath || optionsArg.forceLocalDeno) {
|
||||
@@ -56,19 +58,13 @@ export class SmartDeno {
|
||||
this.denoBinaryPath = 'deno';
|
||||
}
|
||||
|
||||
await this.scriptServer.start();
|
||||
this.isStarted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the SmartDeno instance and cleans up resources
|
||||
* Stops the SmartDeno instance
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
if (!this.isStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.scriptServer.stop();
|
||||
this.isStarted = false;
|
||||
}
|
||||
|
||||
@@ -79,6 +75,19 @@ export class SmartDeno {
|
||||
return this.isStarted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build permission flags for Deno
|
||||
*/
|
||||
private buildPermissionFlags(permissions?: TDenoPermission[]): string {
|
||||
if (!permissions || permissions.length === 0) {
|
||||
return '';
|
||||
}
|
||||
if (permissions.includes('all')) {
|
||||
return '-A';
|
||||
}
|
||||
return permissions.map((p) => `--allow-${p}`).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Deno script
|
||||
* @param scriptArg The script content to execute
|
||||
@@ -93,11 +102,75 @@ export class SmartDeno {
|
||||
throw new Error('SmartDeno is not started. Call start() first.');
|
||||
}
|
||||
|
||||
const denoExecution = new DenoExecution(this.scriptServer, scriptArg, {
|
||||
permissions: options.permissions,
|
||||
denoBinaryPath: this.denoBinaryPath || undefined,
|
||||
});
|
||||
const denoBinary = this.denoBinaryPath || 'deno';
|
||||
const permissionFlags = this.buildPermissionFlags(options.permissions);
|
||||
const scriptSize = Buffer.byteLength(scriptArg, 'utf8');
|
||||
|
||||
return denoExecution.execute();
|
||||
if (scriptSize < MAX_STDIN_SIZE) {
|
||||
// Use stdin for small scripts (in-memory)
|
||||
return this.executeViaStdin(denoBinary, permissionFlags, scriptArg);
|
||||
} else {
|
||||
// Use temp file for large scripts
|
||||
return this.executeViaTempFile(denoBinary, permissionFlags, scriptArg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute script via stdin (in-memory, for scripts < 2MB)
|
||||
* Uses `deno run -` which reads from stdin and supports all permission flags
|
||||
*/
|
||||
private async executeViaStdin(
|
||||
denoBinary: string,
|
||||
permissionFlags: string,
|
||||
script: string
|
||||
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
||||
// Use base64 encoding to safely pass script through shell
|
||||
const base64Script = Buffer.from(script).toString('base64');
|
||||
const command = `echo '${base64Script}' | base64 -d | ${denoBinary} run ${permissionFlags} -`.trim().replace(/\s+/g, ' ');
|
||||
|
||||
const result = await this.smartshellInstance.exec(command);
|
||||
return {
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute script via temp file (for scripts >= 2MB)
|
||||
*/
|
||||
private async executeViaTempFile(
|
||||
denoBinary: string,
|
||||
permissionFlags: string,
|
||||
script: string
|
||||
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
||||
const tempFileName = `deno_script_${plugins.smartunique.shortId()}.ts`;
|
||||
const tempFilePath = plugins.path.join(paths.nogitDir, tempFileName);
|
||||
const fsInstance = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
||||
|
||||
try {
|
||||
// Ensure .nogit directory exists
|
||||
await fsInstance.directory(paths.nogitDir).create();
|
||||
|
||||
// Write script to temp file
|
||||
await fsInstance.file(tempFilePath).write(script);
|
||||
|
||||
// Execute the script
|
||||
const command = `${denoBinary} run ${permissionFlags} "${tempFilePath}"`.trim().replace(/\s+/g, ' ');
|
||||
const result = await this.smartshellInstance.exec(command);
|
||||
|
||||
return {
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
};
|
||||
} finally {
|
||||
// Clean up temp file
|
||||
try {
|
||||
await fsInstance.file(tempFilePath).delete();
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './classes.smartdeno.js';
|
||||
export type { TDenoPermission } from './classes.denoexecution.js';
|
||||
|
||||
@@ -5,15 +5,7 @@ export {
|
||||
path,
|
||||
}
|
||||
|
||||
// @api.global scope
|
||||
import * as typedserver from '@api.global/typedserver';
|
||||
|
||||
export {
|
||||
typedserver,
|
||||
}
|
||||
|
||||
// @push.rocks scope
|
||||
import * as lik from '@push.rocks/lik';
|
||||
import * as smartarchive from '@push.rocks/smartarchive';
|
||||
import * as smartfs from '@push.rocks/smartfs';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
@@ -21,10 +13,9 @@ import * as smartshell from '@push.rocks/smartshell';
|
||||
import * as smartunique from '@push.rocks/smartunique';
|
||||
|
||||
export {
|
||||
lik,
|
||||
smartarchive,
|
||||
smartfs,
|
||||
smartpath,
|
||||
smartshell,
|
||||
smartunique,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user