Files
scrypted/common/src/eval/scrypted-eval.ts
2022-03-14 00:58:13 -07:00

233 lines
7.3 KiB
TypeScript

import type { TranspileOptions } from "typescript";
import sdk, { ScryptedDeviceBase, MixinDeviceBase, ScryptedInterface, ScryptedDeviceType } from "@scrypted/sdk";
import vm from "vm";
import fs from 'fs';
import { newThread } from '../../../server/src/threading';
import { ScriptDevice } from "./monaco/script-device";
import { ScryptedInterfaceDescriptors } from "@scrypted/sdk/types";
import fetch from 'node-fetch-commonjs';
const { systemManager, deviceManager, mediaManager, endpointManager } = sdk;
function tsCompile(source: string, options: TranspileOptions = null): string {
const ts = require("typescript");
const { ScriptTarget } = ts;
// Default options -- you could also perform a merge, or use the project tsconfig.json
if (null === options) {
options = {
compilerOptions: {
target: ScriptTarget.ESNext,
module: ts.ModuleKind.CommonJS
}
};
}
return ts.transpileModule(source, options).outputText;
}
async function tsCompileThread(source: string, options: TranspileOptions = null): Promise<string> {
return newThread({
source, options,
customRequire: '__webpack_require__',
}, ({ source, options }) => {
const ts = global.require("typescript");
const { ScriptTarget } = ts;
// Default options -- you could also perform a merge, or use the project tsconfig.json
if (null === options) {
options = {
compilerOptions: {
target: ScriptTarget.ESNext,
module: ts.ModuleKind.CommonJS
}
};
}
return ts.transpileModule(source, options).outputText;
});
}
function getTypeDefs() {
const scryptedTypesDefs = fs.readFileSync('@types/sdk/types.d.ts').toString();
const scryptedIndexDefs = fs.readFileSync('@types/sdk/index.d.ts').toString();
return {
scryptedIndexDefs,
scryptedTypesDefs,
};
}
export async function scryptedEval(device: ScryptedDeviceBase, script: string, extraLibs: { [lib: string]: string }, params: { [name: string]: any }) {
const libs = Object.assign({
types: getTypeDefs().scryptedTypesDefs,
}, extraLibs);
const allScripts = Object.values(libs).join('\n').toString() + script;
let compiled: string;
try {
compiled = await tsCompileThread(allScripts);
}
catch (e) {
device.log.e('Error compiling typescript.');
device.console.error(e);
throw e;
}
const allParams = Object.assign({}, params, {
fetch,
ScryptedDeviceBase,
MixinDeviceBase,
systemManager,
deviceManager,
endpointManager,
mediaManager,
log: device.log,
console: device.console,
localStorage: device.storage,
device,
exports: {},
ScryptedInterface,
ScryptedDeviceType,
});
const asyncWrappedCompiled = `return (async function() {\n${compiled}\n})`;
let asyncFunction: any;
try {
const functionGenerator = vm.compileFunction(asyncWrappedCompiled, Object.keys(allParams), {
filename: 'script.js',
});
asyncFunction = functionGenerator(...Object.values(allParams));
}
catch (e) {
device.log.e('Error evaluating javascript.');
device.console.error(e);
throw e;
}
try {
return await asyncFunction();
}
catch (e) {
device.log.e('Error running script.');
device.console.error(e);
throw e;
}
}
export function createMonacoEvalDefaults(extraLibs: { [lib: string]: string }) {
const bufferTypeDefs = fs.readFileSync('@types/node/buffer.d.ts').toString();
const safeLibs = {
bufferTypeDefs,
};
const libs = Object.assign(getTypeDefs(), extraLibs);
function monacoEvalDefaultsFunction(monaco: any, safeLibs: any, libs: any) {
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(
Object.assign(
{},
monaco.languages.typescript.typescriptDefaults.getDiagnosticsOptions(),
{
diagnosticCodesToIgnore: [1108, 1375, 1378],
}
)
);
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
Object.assign(
{},
monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
{
moduleResolution:
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
}
)
);
const catLibs = Object.values(libs).join('\n');
const catlibsNoExport = Object.keys(libs).filter(lib => lib !== 'sdk')
.map(lib => libs[lib]).map(lib =>
lib.toString().replace(/export /g, '').replace(/import.*?/g, ''))
.join('\n');
monaco.languages.typescript.typescriptDefaults.addExtraLib(`
${catLibs}
declare global {
${catlibsNoExport}
const log: Logger;
const deviceManager: DeviceManager;
const endpointManager: EndpointManager;
const mediaManager: MediaManager;
const systemManager: SystemManager;
const mqtt: MqttClient;
const device: ScryptedDeviceBase & { pathname : string };
}
`,
"node_modules/@types/scrypted__sdk/types/index.d.ts"
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
libs['sdk'],
"node_modules/@types/scrypted__sdk/index.d.ts"
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
safeLibs.bufferTypeDefs,
"node_modules/@types/node/buffer.d.ts"
);
}
return `(function() {
const safeLibs = ${JSON.stringify(safeLibs)};
const libs = ${JSON.stringify(libs)};
return (monaco) => {
(${monacoEvalDefaultsFunction})(monaco, safeLibs, libs);
}
})();
`;
}
export interface ScriptDeviceImpl extends ScriptDevice {
mergeHandler(device: ScryptedDeviceBase): string[];
}
const methodInterfaces = new Map<string, string>();
for (const desc of Object.values(ScryptedInterfaceDescriptors)) {
for (const method of desc.methods) {
methodInterfaces.set(method, desc.name);
}
}
export function createScriptDevice(baseInterfaces: string[]): ScriptDeviceImpl {
let scriptHandler: any;
const allInterfaces = baseInterfaces.slice();
return {
handle: <T>(handler?: T & object) => {
scriptHandler = handler;
},
handleTypes: (...interfaces: ScryptedInterface[]) => {
allInterfaces.push(...interfaces);
},
mergeHandler: (device: ScryptedDeviceBase) => {
const handler = scriptHandler || {};
let keys: string[];
if (handler.constructor === Object)
keys = Object.keys(handler);
else
keys = Object.getOwnPropertyNames(handler.__proto__);
for (const method of keys) {
const iface = methodInterfaces.get(method);
if (iface) {
allInterfaces.push(iface);
(device as any)[method] = handler[method].bind(handler);
}
}
return allInterfaces;
},
};
}