diff --git a/common/package-lock.json b/common/package-lock.json index 9c7b19b68..00d73eb3b 100644 --- a/common/package-lock.json +++ b/common/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.1", "license": "ISC", "dependencies": { - "@scrypted/sdk": "file:../sdk" + "@scrypted/sdk": "file:../sdk", + "typescript": "^4.4.3" }, "devDependencies": { "@types/node": "^16.9.0" @@ -17,19 +18,23 @@ }, "../sdk": { "name": "@scrypted/sdk", - "version": "0.0.69", + "version": "0.0.81", "license": "ISC", "dependencies": { "@babel/core": "^7.2.2", "@babel/plugin-proposal-class-properties": "^7.4.4", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-transform-modules-commonjs": "^7.2.0", "@babel/plugin-transform-typescript": "^7.15.0", "@babel/polyfill": "^7.2.5", "@babel/preset-env": "^7.2.3", "@babel/preset-typescript": "^7.15.0", + "@types/node": "^16.6.1", "adm-zip": "^0.4.13", - "axios": "^0.21.1", + "axios": "^0.21.4", "babel-loader": "^8.0.4", + "babel-plugin-const-enum": "^1.1.0", + "babel-plugin-minify-dead-code-elimination": "^0.5.1", "babel-polyfill": "^6.26.0", "babel-template": "^6.26.0", "browserify-buffertools": "^1.0.2", @@ -60,10 +65,7 @@ "scrypted-webpack": "bin/scrypted-webpack.js" }, "devDependencies": { - "@babel/plugin-proposal-optional-chaining": "^7.13.12", - "@types/node": "^16.6.1", - "babel-plugin-const-enum": "^1.1.0", - "babel-plugin-minify-dead-code-elimination": "^0.5.1" + "typescript-json-schema": "^0.50.1" } }, "node_modules/@scrypted/sdk": { @@ -75,6 +77,18 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz", "integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==", "dev": true + }, + "node_modules/typescript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } } }, "dependencies": { @@ -91,7 +105,7 @@ "@babel/preset-typescript": "^7.15.0", "@types/node": "^16.6.1", "adm-zip": "^0.4.13", - "axios": "^0.21.1", + "axios": "^0.21.4", "babel-loader": "^8.0.4", "babel-plugin-const-enum": "^1.1.0", "babel-plugin-minify-dead-code-elimination": "^0.5.1", @@ -112,6 +126,7 @@ "ts-loader": "^5.4.5", "tsconfig-paths-webpack-plugin": "^3.2.0", "typescript": "^4.3.5", + "typescript-json-schema": "^0.50.1", "webpack": "^4.28.1", "webpack-cli": "^3.1.2", "webpack-inject-plugin": "^1.0.2" @@ -122,6 +137,11 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz", "integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==", "dev": true + }, + "typescript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==" } } } diff --git a/common/package.json b/common/package.json index 113a1652f..6123491db 100644 --- a/common/package.json +++ b/common/package.json @@ -9,7 +9,8 @@ "author": "", "license": "ISC", "dependencies": { - "@scrypted/sdk": "file:../sdk" + "@scrypted/sdk": "file:../sdk", + "typescript": "^4.4.3" }, "devDependencies": { "@types/node": "^16.9.0" diff --git a/common/src/scrypted-eval.ts b/common/src/scrypted-eval.ts new file mode 100644 index 000000000..8aa7f6f49 --- /dev/null +++ b/common/src/scrypted-eval.ts @@ -0,0 +1,124 @@ +import ts, { ScriptTarget } from "typescript"; +import sdk, { ScryptedDeviceBase } from "@scrypted/sdk"; +import vm from "vm"; + +const { systemManager, deviceManager, mediaManager, endpointManager } = sdk; + +function tsCompile(source: string, options: ts.TranspileOptions = null): string { + // 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; +} + +export async function scryptedEval(device: ScryptedDeviceBase, script: string, extraLibs: { [lib: string]: string }, params: { [name: string]: any }) { + try { + const libs = Object.assign({ + types: require("!!raw-loader!!@scrypted/sdk/types.d.ts"), + }, extraLibs); + const allScripts = Object.values(libs).join('\n').toString() + script; + const compiled = tsCompile(allScripts); + + const allParams = Object.assign({}, params, { + systemManager, + deviceManager, + endpointManager, + mediaManager, + log: device.log, + console: device.console, + localStorage: device.storage, + device, + exports: {}, + }); + + try { + const f = vm.compileFunction(compiled, Object.keys(allParams), { + filename: 'script.js', + }); + + try { + return await f(...Object.values(allParams)); + } + catch (e) { + device.log.e('Error running script.'); + device.console.error(e); + throw e; + } + } + catch (e) { + device.log.e('Error evaluating script.'); + device.console.error(e); + throw e; + } + } + catch (e) { + device.log.e('Error compiling script.'); + device.console.error(e); + throw e; + } +} + +export function createMonacoEvalDefaults(extraLibs: { [lib: string]: string }) { + const libs = Object.assign({ + types: require("!!raw-loader!@scrypted/sdk/types.d.ts"), + sdk: require("!!raw-loader!@scrypted/sdk/index.d.ts"), + }, extraLibs); + + function monacoEvalDefaultsFunction(monaco, libs) { + 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.d.ts" + ); + + monaco.languages.typescript.typescriptDefaults.addExtraLib( + libs['sdk'], + "node_modules/@types/scrypted__sdk/index.d.ts" + ); + } + + return `(function() { + const libs = ${JSON.stringify(libs)}; + + return (monaco) => { + (${monacoEvalDefaultsFunction})(monaco, libs); + } + })(); + `; +}