├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .npmignore ├── README.md ├── build ├── compiler.d.ts ├── compiler.js ├── host.d.ts ├── host.js ├── index.d.ts ├── index.js ├── logger.d.ts ├── logger.js ├── server.d.ts ├── server.js ├── service-host.d.ts ├── service-host.js ├── util.d.ts ├── util.js ├── virtual-fs.d.ts └── virtual-fs.js ├── package-lock.json ├── package.json ├── src ├── compiler.ts ├── host.ts ├── index.test.ts ├── index.ts ├── logger.test.ts ├── logger.ts ├── server.ts ├── service-host.ts ├── util.ts ├── virtual-fs.test.ts └── virtual-fs.ts ├── test ├── ioBroker.d.ts └── testrun.js ├── tsconfig.json └── tslint.json /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 8 * * 3' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['javascript'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v1 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v1 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | 263 | # Visual Studio NodeJS Typings 264 | typings/ 265 | 266 | # VSCode files 267 | .vscode/ 268 | 269 | # Sourcemaps 270 | maps/ 271 | 272 | Thumbs.db 273 | 274 | # NYC coverage files 275 | coverage 276 | .nyc* 277 | 278 | # Compiled test files 279 | build/**/*.test.js 280 | build/**/*.test.d.ts 281 | build/**/*.test.js.map -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | Gruntfile.js 2 | tasks 3 | node_modules 4 | .idea 5 | .git 6 | /node_modules 7 | test/ 8 | src/ 9 | travis/ 10 | .travis.yml 11 | appveyor.yml 12 | obj/ 13 | bin/ 14 | .vs*/ 15 | tslint.json 16 | tsconfig.json 17 | *.sln 18 | *.njsproj* 19 | build/**/*.map 20 | 21 | # Local debugging password 22 | *_password.json 23 | deploy_local.* 24 | 25 | Thumbs.db 26 | 27 | # Backup & report files from converting an old project file 28 | # to a newer Visual Studio version. Backup files are not needed, 29 | # because we have git ;-) 30 | _UpgradeReport_Files/ 31 | Backup*/ 32 | UpgradeLog*.XML 33 | UpgradeLog*.htm 34 | 35 | # NYC coverage files 36 | coverage 37 | .nyc* 38 | 39 | # test files 40 | src/**/*.test.ts 41 | build/**/*.test.js 42 | build/**/*.test.d.ts 43 | build/**/*.test.map -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # virtual-tsc 2 | 3 | Provides means to compile TypeScript code to JavaScript in memory. 4 | Requires `typescript` >= v2.0 and `@types/node` as peer dependencies, where `@types/node` should match your NodeJS runtime. 5 | 6 | ## Usage 7 | 8 | ```TS 9 | import { compile } from "virtual-tsc"; 10 | import * as ts from "typescript"; 11 | const result: CompileResult = compile(sourceCode: string, compilerOptions?: ts.CompilerOptions, declarations?); 12 | ``` 13 | where `CompileResult` looks as follows: 14 | ```TS 15 | export interface CompileResult { 16 | success: boolean; 17 | diagnostics: Diagnostic[]; 18 | result?: string; 19 | declarations?: string; 20 | } 21 | 22 | export interface Diagnostic { 23 | type: "error" | "warning" | "message"; 24 | lineNr: number; 25 | charNr: number; 26 | sourceLine: string; 27 | description: string; 28 | annotatedSource: string; 29 | } 30 | ``` 31 | 32 | ## Ambient declarations 33 | `declarations` is an object of the type: 34 | ```JS 35 | { 36 | "filename1.d.ts": "file contents 1", 37 | // ... 38 | } 39 | ``` 40 | and is used to specify ambient declarations. Filenames must end in `.d.ts`. For instance you can declare a function `log` that exists in the global scope by providing a file like the following: 41 | ```TS 42 | import * as fs from "fs"; // dummy import 43 | declare global { 44 | function log(text: string); 45 | } 46 | ``` 47 | To support augmentation of the global scope (like in the above file), you must force TypeScript to treat the file as a module. This can be done by a dummy import of a core NodeJS module. 48 | 49 | ## Faster compilation with the Language Service API 50 | As of version 0.3.0, this library supports incremental compilation with the TypeScript Language Service API. In simple tests, compile times for recurring compilations could be **reduced by at least 99%**. The usage changes slightly: 51 | ```TS 52 | import { Server as TSServer } from "virtual-tsc"; 53 | 54 | // Create a new instance of the compiler with optional compiler options 55 | const tsserver = new TSServer(options?: ts.CompilerOptions); 56 | 57 | // optionally provide ambient declarations 58 | tsserver.provideAmbientDeclarations(declarations); 59 | 60 | // compile whenever the source file changes: 61 | const result = tsserver.compile( 62 | filename /* string */, 63 | source /* string */ 64 | ); 65 | ``` 66 | By providing a filename for the source, it is possible to compile multiple scripts on one instance of the compiler. 67 | 68 | ## Error-tolerant compilation 69 | 70 | By specifying `noEmitOnError: false` on the `compilerOptions` object, you can get a compiled result even if there were build errors. For example, the code 71 | ```TS 72 | const test: string = 1 73 | ``` 74 | then compiles to the valid JavaScript 75 | ```JS 76 | var test = 1 77 | ``` 78 | but you get the additional error message 79 | ```JS 80 | const test: string = 1 81 | ^ 82 | ERROR: Type '1' is not assignable to type 'string'. 83 | ``` 84 | 85 | ## Changelog 86 | 90 | ### 0.6.2 (2022-01-10) 91 | * (AlCalzone) Replaced corrupted `colors` dependency with `picocolors` 92 | 93 | ### 0.6.1 (2020-07-05) 94 | * (AlCalzone) Allow `package.json` as ambient declarations, use "" as the current directory 95 | 96 | ### 0.6.0 (2020-06-09) 97 | * (AlCalzone) Expose `setTypeScriptResolveOptions` to set the options for resolving TypeScript and its lib files. 98 | 99 | ### 0.5.0 (2020-01-28) 100 | * (AlCalzone) Passing `false` as the 2nd parameter to the Server constructor disables logging. 101 | 102 | ### 0.4.6 (2018-08-03) 103 | * (AlCalzone) Allow TypeScript v3+ as a peer dependency 104 | 105 | ### 0.4.5 (2018-05-30) 106 | * (AlCalzone) Fixed performance issues when `declaration` and `noEmitOnError` are both `true` 107 | 108 | ### 0.4.1 (2018-05-23) 109 | * (AlCalzone) Allow emitting only declaration files 110 | 111 | ### 0.4.0 (2018-05-23) 112 | * (AlCalzone) Emit declaration files (*.d.ts), enabled by default 113 | 114 | ### 0.3.4 (2017-11-26) 115 | * (AlCalzone) Added a custom logger output 116 | 117 | ### 0.3.3 (2017-11-14) 118 | * (AlCalzone) Fixed lib resolution for the LanguageServiceAPI 119 | 120 | ### 0.3.2 (2017-11-09) 121 | * (AlCalzone) Use the LanguageServiceAPI to speed up multiple compilations 122 | 123 | ### 0.2.3 (2017-10-13) 124 | * (AlCalzone) Fixed module resolution on Linux 125 | * (AlCalzone) Added async compile method 126 | 127 | ### 0.2.2 (2017-10-13) 128 | * (AlCalzone) support NodeJS 4 129 | 130 | ### 0.2.1 (2017-10-13) 131 | * (AlCalzone) support output of builds with errors 132 | 133 | ### 0.2.0 (2017-10-13) 134 | * (AlCalzone) support ambient declarations 135 | 136 | ### 0.1.0 (2017-10-13) 137 | * (AlCalzone) initial release. 138 | 139 | ## License 140 | The MIT License (MIT) 141 | 142 | Copyright (c) 2017 AlCalzone 143 | 144 | Permission is hereby granted, free of charge, to any person obtaining a copy 145 | of this software and associated documentation files (the "Software"), to deal 146 | in the Software without restriction, including without limitation the rights 147 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 148 | copies of the Software, and to permit persons to whom the Software is 149 | furnished to do so, subject to the following conditions: 150 | 151 | The above copyright notice and this permission notice shall be included in 152 | all copies or substantial portions of the Software. 153 | 154 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 155 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 156 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 157 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 158 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 159 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 160 | THE SOFTWARE. 161 | -------------------------------------------------------------------------------- /build/compiler.d.ts: -------------------------------------------------------------------------------- 1 | import { CompileResult } from "./util"; 2 | import type { CompilerOptions as tsCompilerOptions } from "typescript"; 3 | export declare function compileAsync(script: string, compilerOptions?: tsCompilerOptions, declarations?: { 4 | [filename: string]: string; 5 | }): Promise; 6 | export declare function compile(script: string, compilerOptions?: tsCompilerOptions, ambientDeclarations?: { 7 | [filename: string]: string; 8 | }): CompileResult; 9 | -------------------------------------------------------------------------------- /build/compiler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __spreadArrays = (this && this.__spreadArrays) || function () { 3 | for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; 4 | for (var r = Array(s), k = 0, i = 0; i < il; i++) 5 | for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) 6 | r[k] = a[j]; 7 | return r; 8 | }; 9 | Object.defineProperty(exports, "__esModule", { value: true }); 10 | exports.compile = exports.compileAsync = void 0; 11 | var host_1 = require("./host"); 12 | var util_1 = require("./util"); 13 | var virtual_fs_1 = require("./virtual-fs"); 14 | var SCRIPT_FILENAME = "__virtual-tsc__.ts"; 15 | function compileAsync(script, compilerOptions, declarations) { 16 | if (declarations === void 0) { declarations = {}; } 17 | return new Promise(function (res, rej) { 18 | setImmediate(function () { 19 | try { 20 | var ret = compile(script, compilerOptions, declarations); 21 | res(ret); 22 | } 23 | catch (e) { 24 | rej(e); 25 | } 26 | }); 27 | }); 28 | } 29 | exports.compileAsync = compileAsync; 30 | function compile(script, compilerOptions, ambientDeclarations) { 31 | if (ambientDeclarations === void 0) { ambientDeclarations = {}; } 32 | var ts = util_1.getTypeScript(); 33 | var sourceLines = script.split("\n"); 34 | // set default compiler options 35 | compilerOptions = compilerOptions || {}; 36 | compilerOptions.moduleResolution = ts.ModuleResolutionKind.NodeJs; 37 | // Don't emit faulty code (by default) 38 | if (compilerOptions.noEmitOnError == null) 39 | compilerOptions.noEmitOnError = true; 40 | // emit declarations if possible 41 | if (compilerOptions.declaration == null) 42 | compilerOptions.declaration = true; 43 | // According to https://github.com/Microsoft/TypeScript/issues/24444#issuecomment-392970120 44 | // combining noEmitOnError=true and declaration=true massively increases the work done 45 | // by the compiler. To work around it, we call the compiler with noEmitOnError=false 46 | // and use the actual value to determine if we continue with the emit 47 | var internalOptions = Object.assign({}, compilerOptions, { 48 | noEmitOnError: false, 49 | }); 50 | // provide the source file in the virtual fs 51 | var fs = new virtual_fs_1.VirtualFileSystem(); 52 | fs.writeFile(SCRIPT_FILENAME, script); 53 | // provide all ambient declaration files 54 | for (var _i = 0, _a = Object.keys(ambientDeclarations); _i < _a.length; _i++) { 55 | var ambientFile = _a[_i]; 56 | if (!/\.d\.ts$/.test(ambientFile)) 57 | throw new Error("Declarations must be .d.ts-files"); 58 | fs.writeFile(ambientFile, ambientDeclarations[ambientFile], true); 59 | } 60 | // create the virtual host 61 | var host = new host_1.InMemoryHost(fs, internalOptions); 62 | // create the compiler and provide nodejs typings 63 | var allFiles = __spreadArrays([ 64 | "@types/node/index.d.ts" 65 | ], Object.keys(ambientDeclarations), [ 66 | SCRIPT_FILENAME, 67 | ]); 68 | var program = ts.createProgram(allFiles, internalOptions, host); 69 | // compile the script 70 | var emitResult = program.emit(); 71 | // diagnose the compilation result 72 | var rawDiagnostics = internalOptions.noEmitOnError ? emitResult.diagnostics : ts.getPreEmitDiagnostics(program); 73 | var diagnostics = rawDiagnostics.map(function (diagnostic) { 74 | var _a; 75 | var lineNr = 0; 76 | var charNr = 0; 77 | if (diagnostic.file != null) { 78 | var _b = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start), line = _b.line, character = _b.character; 79 | _a = [line, character], lineNr = _a[0], charNr = _a[1]; 80 | } 81 | var description = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); 82 | var type = ts.DiagnosticCategory[diagnostic.category].toLowerCase(); 83 | var sourceLine = sourceLines[lineNr]; 84 | var annotatedSource = sourceLine + "\n" + util_1.repeatString(" ", charNr) + "^\n" + type.toUpperCase() + ": " + description; 85 | return { 86 | type: type, 87 | lineNr: lineNr + 1, 88 | charNr: charNr + 1, 89 | sourceLine: sourceLine, 90 | description: description, 91 | annotatedSource: annotatedSource, 92 | }; 93 | }); 94 | var hasError = ((diagnostics.find(function (d) { return d.type === "error"; }) != null 95 | || (emitResult.emitSkipped && !compilerOptions.emitDeclarationOnly)) 96 | && compilerOptions.noEmitOnError); 97 | var result; 98 | var resultFilename = SCRIPT_FILENAME.replace(/ts$/, "js"); 99 | var declarations; 100 | var declarationsFilename = SCRIPT_FILENAME.replace(/ts$/, "d.ts"); 101 | if (!hasError && fs.fileExists(resultFilename)) 102 | result = fs.readFile(resultFilename); 103 | if (!hasError && fs.fileExists(declarationsFilename)) 104 | declarations = fs.readFile(declarationsFilename); 105 | return { 106 | success: !hasError, 107 | diagnostics: diagnostics, 108 | result: result, 109 | declarations: declarations, 110 | }; 111 | } 112 | exports.compile = compile; 113 | -------------------------------------------------------------------------------- /build/host.d.ts: -------------------------------------------------------------------------------- 1 | import type { CompilerHost as tsCompilerHost, CompilerOptions as tsCompilerOptions, ScriptTarget as tsScriptTarget, SourceFile as tsSourceFile } from "typescript"; 2 | import type { VirtualFileSystem } from "./virtual-fs"; 3 | /** 4 | * Implementation of CompilerHost that works with in-memory-only source files 5 | */ 6 | export declare class InMemoryHost implements tsCompilerHost { 7 | private fs; 8 | private options; 9 | constructor(fs: VirtualFileSystem, options: tsCompilerOptions); 10 | private ts; 11 | getSourceFile(fileName: string, languageVersion: tsScriptTarget, onError?: (message: string) => void): tsSourceFile; 12 | getDefaultLibFileName(options: tsCompilerOptions): string; 13 | writeFile(path: string, content: string): void; 14 | getCurrentDirectory(): string; 15 | getDirectories(path: string): string[]; 16 | getCanonicalFileName(fileName: string): string; 17 | useCaseSensitiveFileNames(): boolean; 18 | getNewLine(): string; 19 | fileExists(fileName: string): boolean; 20 | readFile(fileName: string): string; 21 | } 22 | -------------------------------------------------------------------------------- /build/host.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 5 | }) : (function(o, m, k, k2) { 6 | if (k2 === undefined) k2 = k; 7 | o[k2] = m[k]; 8 | })); 9 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 10 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 11 | }) : function(o, v) { 12 | o["default"] = v; 13 | }); 14 | var __importStar = (this && this.__importStar) || function (mod) { 15 | if (mod && mod.__esModule) return mod; 16 | var result = {}; 17 | if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 18 | __setModuleDefault(result, mod); 19 | return result; 20 | }; 21 | Object.defineProperty(exports, "__esModule", { value: true }); 22 | exports.InMemoryHost = void 0; 23 | var nodePath = __importStar(require("path")); 24 | var logger_1 = require("./logger"); 25 | var util_1 = require("./util"); 26 | // reference: https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#customizing-module-resolution 27 | /** 28 | * Implementation of CompilerHost that works with in-memory-only source files 29 | */ 30 | var InMemoryHost = /** @class */ (function () { 31 | function InMemoryHost(fs, options) { 32 | this.fs = fs; 33 | this.options = options; 34 | this.ts = util_1.getTypeScript(); 35 | } 36 | InMemoryHost.prototype.getSourceFile = function (fileName, languageVersion, onError) { 37 | var fileContent; 38 | if (this.fs.fileExists(fileName)) { 39 | logger_1.log("getSourceFile(fileName=\"" + fileName + "\", version=" + languageVersion + ") => returning provided file", "debug"); 40 | fileContent = this.fs.readFile(fileName); 41 | } 42 | else if (/^lib\..*?d\.ts$/.test(fileName)) { 43 | // resolving lib file 44 | var libPath = nodePath.join(nodePath.dirname(require.resolve("typescript", util_1.getTypeScriptResolveOptions())), fileName); 45 | logger_1.log("getSourceFile(fileName=\"" + fileName + "\") => resolved lib file " + libPath, "debug"); 46 | fileContent = this.ts.sys.readFile(libPath); 47 | if (fileContent != null) 48 | this.fs.writeFile(fileName, fileContent, true); 49 | } 50 | else if (/\@types\/.+$/.test(fileName)) { 51 | // resolving a specific node module 52 | logger_1.log("getSourceFile(fileName=\"" + fileName + "\") => resolving typings", "debug"); 53 | fileName = util_1.resolveTypings(fileName); 54 | fileContent = this.ts.sys.readFile(fileName); 55 | if (fileContent != null) 56 | this.fs.writeFile(fileName, fileContent, true); 57 | } 58 | if (fileContent != null) { 59 | logger_1.log("file content is not null", "debug"); 60 | return this.ts.createSourceFile(fileName, this.fs.readFile(fileName), languageVersion); 61 | } 62 | else { 63 | logger_1.log("file content is null", "debug"); 64 | } 65 | }; 66 | InMemoryHost.prototype.getDefaultLibFileName = function (options) { 67 | options = options || this.options; 68 | logger_1.log("getDefaultLibFileName(" + JSON.stringify(options, null, 4) + ")", "debug"); 69 | return "lib.d.ts"; 70 | }; 71 | InMemoryHost.prototype.writeFile = function (path, content) { 72 | logger_1.log("writeFile(path=\"" + path + "\")", "debug"); 73 | this.fs.writeFile(path, content, true); 74 | }; 75 | InMemoryHost.prototype.getCurrentDirectory = function () { 76 | var ret = this.ts.sys.getCurrentDirectory(); 77 | logger_1.log("getCurrentDirectory() => " + ret, "debug"); 78 | return ret; 79 | }; 80 | InMemoryHost.prototype.getDirectories = function (path) { 81 | logger_1.log("getDirectories(" + path + ")", "debug"); 82 | throw new Error("Method not implemented."); 83 | }; 84 | InMemoryHost.prototype.getCanonicalFileName = function (fileName) { 85 | logger_1.log("getCanonicalFileName(" + fileName + ")", "debug"); 86 | return this.ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); 87 | }; 88 | InMemoryHost.prototype.useCaseSensitiveFileNames = function () { 89 | logger_1.log("useCaseSensitiveFileNames()", "debug"); 90 | return this.ts.sys.useCaseSensitiveFileNames; 91 | }; 92 | InMemoryHost.prototype.getNewLine = function () { 93 | logger_1.log("getNewLine()", "debug"); 94 | return this.ts.sys.newLine; 95 | }; 96 | // public resolveModuleNames?(moduleNames: string[], containingFile: string): ts.ResolvedModule[] { 97 | // log(`resolveModuleNames(${moduleNames})`); 98 | // return moduleNames.map(moduleName => { 99 | // { // try to use standard resolution 100 | // const result = ts.resolveModuleName( 101 | // moduleName, containingFile, 102 | // this.options, 103 | // { 104 | // fileExists: this.fileExists.bind(this), 105 | // readFile: this.readFile.bind(this), 106 | // }, 107 | // ); 108 | // if (result.resolvedModule) return result.resolvedModule; 109 | // } 110 | // try { // fall back to NodeJS resolution 111 | // const fileName = require.resolve(moduleName); 112 | // if (fileName === moduleName) return; // internal module 113 | // log(`resolved ${moduleName} => ${fileName}`); 114 | // return { 115 | // resolvedFileName: fileName, 116 | // } as ts.ResolvedModule; 117 | // } catch (e) { 118 | // /* Not found */ 119 | // } 120 | // }); 121 | // } 122 | InMemoryHost.prototype.fileExists = function (fileName) { 123 | logger_1.log("fileExists(" + fileName + ")", "debug"); 124 | return this.fs.fileExists(fileName); 125 | }; 126 | InMemoryHost.prototype.readFile = function (fileName) { 127 | logger_1.log("readFile(" + fileName + ")", "debug"); 128 | return this.fs.readFile(fileName); 129 | }; 130 | return InMemoryHost; 131 | }()); 132 | exports.InMemoryHost = InMemoryHost; 133 | -------------------------------------------------------------------------------- /build/index.d.ts: -------------------------------------------------------------------------------- 1 | export { compile, compileAsync } from "./compiler"; 2 | export { CompileResult, Diagnostic, setTypeScriptResolveOptions } from "./util"; 3 | export { Server } from "./server"; 4 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var compiler_1 = require("./compiler"); 4 | Object.defineProperty(exports, "compile", { enumerable: true, get: function () { return compiler_1.compile; } }); 5 | Object.defineProperty(exports, "compileAsync", { enumerable: true, get: function () { return compiler_1.compileAsync; } }); 6 | var util_1 = require("./util"); 7 | Object.defineProperty(exports, "setTypeScriptResolveOptions", { enumerable: true, get: function () { return util_1.setTypeScriptResolveOptions; } }); 8 | var server_1 = require("./server"); 9 | Object.defineProperty(exports, "Server", { enumerable: true, get: function () { return server_1.Server; } }); 10 | -------------------------------------------------------------------------------- /build/logger.d.ts: -------------------------------------------------------------------------------- 1 | export declare type SubNamespaces = "server" | "host" | "util" | "vfs"; 2 | export declare type Severity = "info" | "warn" | "debug" | "error" | "silly"; 3 | export declare type LoggerFunction = (message: string, severity?: Severity) => void; 4 | export declare function setCustomLogger(logger: LoggerFunction | false): void; 5 | export declare function log(message: string, severity: Severity): void; 6 | export declare function log(namespace: SubNamespaces, message: string, severity: Severity): void; 7 | -------------------------------------------------------------------------------- /build/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.log = exports.setCustomLogger = void 0; 7 | // tslint:disable-next-line:no-var-requires 8 | var debug_1 = __importDefault(require("debug")); 9 | var picocolors_1 = __importDefault(require("picocolors")); 10 | var defaultNamespace = "virtual-tsc"; 11 | var customLogger; 12 | function setCustomLogger(logger) { 13 | customLogger = logger; 14 | } 15 | exports.setCustomLogger = setCustomLogger; 16 | var formatters = { 17 | info: function (message) { return picocolors_1.default.blue(message); }, 18 | warn: function (message) { return picocolors_1.default.yellow(message); }, 19 | debug: function (message) { return picocolors_1.default.white(message); }, 20 | error: function (message) { return picocolors_1.default.red(message); }, 21 | silly: function (message) { return picocolors_1.default.white(message); }, 22 | }; 23 | function log() { 24 | var args = []; 25 | for (var _i = 0; _i < arguments.length; _i++) { 26 | args[_i] = arguments[_i]; 27 | } 28 | if (customLogger === false) 29 | return; 30 | // we only accept strings 31 | if (!args || !args.length || !args.every(function (arg) { return typeof arg === "string"; })) { 32 | throw new Error("Invalid arguments passed to log()"); 33 | } 34 | var namespace = ""; 35 | var message; 36 | var severity; 37 | if (args.length === 2) { 38 | (message = args[0], severity = args[1]); 39 | } 40 | else if (args.length === 3) { 41 | (namespace = args[0], message = args[1], severity = args[2]); 42 | // add the namespace separator to append the namespace to the default one 43 | if (typeof namespace === "string" && namespace !== "") 44 | namespace = ":" + namespace; 45 | } 46 | function defaultLogger() { 47 | var prefix = ""; 48 | if (severity !== "info") { 49 | prefix = "[" + severity.toUpperCase() + "] "; 50 | } 51 | debug_1.default(defaultNamespace + namespace)("" + prefix + formatters[severity](message)); 52 | } 53 | (customLogger || defaultLogger)(message, severity); 54 | } 55 | exports.log = log; 56 | -------------------------------------------------------------------------------- /build/server.d.ts: -------------------------------------------------------------------------------- 1 | import { LoggerFunction } from "./logger"; 2 | import { CompileResult } from "./util"; 3 | import type { CompilerOptions as tsCompilerOptions } from "typescript"; 4 | export declare class Server { 5 | private options?; 6 | private service; 7 | private fs; 8 | private host; 9 | private ts; 10 | constructor(options?: tsCompilerOptions, customLogger?: LoggerFunction | false); 11 | provideAmbientDeclarations(declarations?: { 12 | [filename: string]: string; 13 | }): void; 14 | compile(filename: string, scriptContent: string): CompileResult; 15 | } 16 | -------------------------------------------------------------------------------- /build/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 5 | }) : (function(o, m, k, k2) { 6 | if (k2 === undefined) k2 = k; 7 | o[k2] = m[k]; 8 | })); 9 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 10 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 11 | }) : function(o, v) { 12 | o["default"] = v; 13 | }); 14 | var __importStar = (this && this.__importStar) || function (mod) { 15 | if (mod && mod.__esModule) return mod; 16 | var result = {}; 17 | if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 18 | __setModuleDefault(result, mod); 19 | return result; 20 | }; 21 | Object.defineProperty(exports, "__esModule", { value: true }); 22 | exports.Server = void 0; 23 | var nodePath = __importStar(require("path")); 24 | var logger_1 = require("./logger"); 25 | var service_host_1 = require("./service-host"); 26 | var util_1 = require("./util"); 27 | var virtual_fs_1 = require("./virtual-fs"); 28 | var Server = /** @class */ (function () { 29 | function Server(options, customLogger) { 30 | this.options = options; 31 | this.ts = util_1.getTypeScript(); 32 | if (customLogger != null) 33 | logger_1.setCustomLogger(customLogger); 34 | // set default compiler options 35 | this.options = this.options || {}; 36 | this.options.moduleResolution = this.ts.ModuleResolutionKind.NodeJs; 37 | // Don't emit faulty code (by default) 38 | if (this.options.noEmitOnError == null) 39 | this.options.noEmitOnError = true; 40 | // emit declarations if possible 41 | if (this.options.declaration == null) 42 | this.options.declaration = true; 43 | // According to https://github.com/Microsoft/TypeScript/issues/24444#issuecomment-392970120 44 | // combining noEmitOnError=true and declaration=true massively increases the work done 45 | // by the compiler. To work around it, we call the compiler with noEmitOnError=false 46 | // and use the actual value to determine if we continue with the emit 47 | var internalOptions = Object.assign({}, this.options, { 48 | noEmitOnError: false, 49 | }); 50 | // set up the build pipeline 51 | this.fs = new virtual_fs_1.VirtualFileSystem(); 52 | this.host = new service_host_1.InMemoryServiceHost(this.fs, internalOptions); 53 | this.service = this.ts.createLanguageService(this.host, this.ts.createDocumentRegistry()); 54 | // provide the requested lib files 55 | if (!options.noLib) { 56 | var libFiles = util_1.enumLibFiles(); 57 | for (var _i = 0, libFiles_1 = libFiles; _i < libFiles_1.length; _i++) { 58 | var file = libFiles_1[_i]; 59 | var fileContent = this.ts.sys.readFile(file); 60 | if (fileContent != null) 61 | this.fs.writeFile(nodePath.basename(file), fileContent, true); 62 | } 63 | } 64 | // provide the most basic typings 65 | var basicTypings = [ 66 | "@types/node/index.d.ts", 67 | "@types/node/inspector.d.ts", 68 | ]; 69 | for (var _a = 0, basicTypings_1 = basicTypings; _a < basicTypings_1.length; _a++) { 70 | var typings = basicTypings_1[_a]; 71 | // resolving a specific node module 72 | var path = util_1.resolveTypings(typings); 73 | var fileContent = this.ts.sys.readFile(path); 74 | if (fileContent != null) 75 | this.fs.writeFile(typings, fileContent, true); 76 | } 77 | } 78 | Server.prototype.provideAmbientDeclarations = function (declarations) { 79 | if (declarations === void 0) { declarations = {}; } 80 | // provide all ambient declaration files 81 | for (var _i = 0, _a = Object.keys(declarations); _i < _a.length; _i++) { 82 | var ambientFile = _a[_i]; 83 | if (!ambientFile.endsWith(".d.ts") && !ambientFile.endsWith("package.json")) { 84 | throw new Error("Declarations must be .d.ts or package.json files"); 85 | } 86 | this.fs.writeFile(ambientFile, declarations[ambientFile], true); 87 | } 88 | }; 89 | Server.prototype.compile = function (filename, scriptContent) { 90 | var _this = this; 91 | var sourceLines = scriptContent.split("\n"); 92 | this.fs.writeFile(filename, scriptContent, true); 93 | var rawDiagnostics = []; 94 | rawDiagnostics.push.apply(rawDiagnostics, this.service.getSyntacticDiagnostics(filename)); 95 | rawDiagnostics.push.apply(rawDiagnostics, this.service.getSemanticDiagnostics(filename)); 96 | var emitResult = this.service.getEmitOutput(filename); 97 | rawDiagnostics.push.apply(rawDiagnostics, this.service.getCompilerOptionsDiagnostics()); 98 | var diagnostics = rawDiagnostics.map(function (diagnostic) { 99 | var _a; 100 | var lineNr = 0; 101 | var charNr = 0; 102 | if (diagnostic.file != null) { 103 | var _b = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start), line = _b.line, character = _b.character; 104 | _a = [line, character], lineNr = _a[0], charNr = _a[1]; 105 | } 106 | var description = _this.ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); 107 | var type = _this.ts.DiagnosticCategory[diagnostic.category].toLowerCase(); 108 | var sourceLine = sourceLines[lineNr]; 109 | var annotatedSource = sourceLine + "\n" + util_1.repeatString(" ", charNr) + "^\n" + type.toUpperCase() + ": " + description; 110 | return { 111 | type: type, 112 | lineNr: lineNr + 1, 113 | charNr: charNr + 1, 114 | sourceLine: sourceLine, 115 | description: description, 116 | annotatedSource: annotatedSource, 117 | }; 118 | }); 119 | var hasError = ((diagnostics.find(function (d) { return d.type === "error"; }) != null 120 | || (emitResult.emitSkipped && !this.options.emitDeclarationOnly)) 121 | && this.options.noEmitOnError); 122 | var result; 123 | var declarations; 124 | if (!hasError) { 125 | var resultFile = emitResult.outputFiles.find(function (f) { return f.name.endsWith(".js"); }); 126 | if (resultFile != null) 127 | result = resultFile.text; 128 | var declarationFile = emitResult.outputFiles.find(function (f) { return f.name.endsWith(".d.ts"); }); 129 | if (declarationFile != null) 130 | declarations = declarationFile.text; 131 | } 132 | return { 133 | success: !hasError, 134 | diagnostics: diagnostics, 135 | result: result, 136 | declarations: declarations, 137 | }; 138 | }; 139 | return Server; 140 | }()); 141 | exports.Server = Server; 142 | -------------------------------------------------------------------------------- /build/service-host.d.ts: -------------------------------------------------------------------------------- 1 | import type { VirtualFileSystem } from "./virtual-fs"; 2 | import type { CompilerOptions as tsCompilerOptions, LanguageServiceHost as tsLanguageServiceHost, IScriptSnapshot as tsIScriptSnapshot } from "typescript"; 3 | /** 4 | * Implementation of LanguageServiceHost that works with in-memory-only source files 5 | */ 6 | export declare class InMemoryServiceHost implements tsLanguageServiceHost { 7 | private fs; 8 | private options; 9 | private ts; 10 | constructor(fs: VirtualFileSystem, options: tsCompilerOptions); 11 | getCompilationSettings(): tsCompilerOptions; 12 | getScriptFileNames(): string[]; 13 | getScriptVersion(fileName: string): string; 14 | getScriptSnapshot(fileName: string): tsIScriptSnapshot; 15 | getCurrentDirectory(): string; 16 | getDefaultLibFileName(options: tsCompilerOptions): string; 17 | readFile(path: string, encoding?: string): string; 18 | fileExists(path: string): boolean; 19 | readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; 20 | getDirectories(directoryName: string): string[]; 21 | } 22 | -------------------------------------------------------------------------------- /build/service-host.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.InMemoryServiceHost = void 0; 4 | var logger_1 = require("./logger"); 5 | var util_1 = require("./util"); 6 | // https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#incremental-build-support-using-the-language-services 7 | /** 8 | * Implementation of LanguageServiceHost that works with in-memory-only source files 9 | */ 10 | var InMemoryServiceHost = /** @class */ (function () { 11 | function InMemoryServiceHost(fs, options) { 12 | this.fs = fs; 13 | this.options = options; 14 | this.ts = util_1.getTypeScript(); 15 | } 16 | InMemoryServiceHost.prototype.getCompilationSettings = function () { 17 | return this.options; 18 | }; 19 | InMemoryServiceHost.prototype.getScriptFileNames = function () { 20 | return this.fs 21 | .getFilenames() 22 | .filter(function (f) { return f.endsWith(".ts"); } /* && !f.endsWith(".d.ts") */); 23 | }; 24 | InMemoryServiceHost.prototype.getScriptVersion = function (fileName) { 25 | return this.fs.getFileVersion(fileName).toString(); 26 | }; 27 | InMemoryServiceHost.prototype.getScriptSnapshot = function (fileName) { 28 | if (!this.fs.fileExists(fileName)) 29 | return undefined; 30 | return this.ts.ScriptSnapshot.fromString(this.fs.readFile(fileName)); 31 | }; 32 | InMemoryServiceHost.prototype.getCurrentDirectory = function () { 33 | return ""; 34 | // return CWD; 35 | // return this.ts.sys.getCurrentDirectory(); 36 | }; 37 | InMemoryServiceHost.prototype.getDefaultLibFileName = function (options) { 38 | options = options || this.options; 39 | logger_1.log("host", "getDefaultLibFileName(" + JSON.stringify(options, null, 4) + ")", "debug"); 40 | return "lib.d.ts"; 41 | }; 42 | // log?(s: string): void { 43 | // throw new Error("Method not implemented."); 44 | // } 45 | // trace?(s: string): void { 46 | // throw new Error("Method not implemented."); 47 | // } 48 | // error?(s: string): void { 49 | // throw new Error("Method not implemented."); 50 | // } 51 | InMemoryServiceHost.prototype.readFile = function (path, encoding) { 52 | logger_1.log("host", "readFile(" + path + ")", "debug"); 53 | if (this.fs.fileExists(path)) { 54 | return this.fs.readFile(path); 55 | } 56 | else if (path.indexOf("node_modules") > -1) { 57 | return this.ts.sys.readFile(path); 58 | } 59 | }; 60 | InMemoryServiceHost.prototype.fileExists = function (path) { 61 | logger_1.log("host", "fileExists(" + path + ")", "debug"); 62 | var ret; 63 | if (this.fs.fileExists(path)) { 64 | ret = true; 65 | } 66 | else if (path.indexOf("node_modules") > -1) { 67 | ret = this.ts.sys.fileExists(path); 68 | } 69 | logger_1.log("host", "fileExists(" + path + ") => " + ret, "debug"); 70 | return ret; 71 | }; 72 | InMemoryServiceHost.prototype.readDirectory = function (path, extensions, exclude, include, depth) { 73 | logger_1.log("host", "readDirectory(\n\t" + path + ",\n\t" + (extensions ? JSON.stringify(extensions) : "null") + ",\n\t" + (exclude ? JSON.stringify(exclude) : "null") + ",\n\t" + (include ? JSON.stringify(include) : "null") + ",\n\t" + depth + ",\n", "debug"); 74 | return this.ts.sys.readDirectory(path, extensions, exclude, include, depth); 75 | }; 76 | InMemoryServiceHost.prototype.getDirectories = function (directoryName) { 77 | logger_1.log("host", "getDirectories(" + directoryName + ")", "debug"); 78 | // typings should be loaded from the virtual fs or we get problems 79 | if (directoryName.indexOf("node_modules/@types") > -1) { 80 | return []; 81 | } 82 | try { 83 | return this.ts.sys.getDirectories(directoryName); 84 | } 85 | catch (e) { 86 | return []; 87 | } 88 | }; 89 | return InMemoryServiceHost; 90 | }()); 91 | exports.InMemoryServiceHost = InMemoryServiceHost; 92 | -------------------------------------------------------------------------------- /build/util.d.ts: -------------------------------------------------------------------------------- 1 | export interface Diagnostic { 2 | type: "error" | "warning" | "message"; 3 | lineNr: number; 4 | charNr: number; 5 | sourceLine: string; 6 | description: string; 7 | annotatedSource: string; 8 | } 9 | export declare function repeatString(str: string, count: number): string; 10 | export interface CompileResult { 11 | success: boolean; 12 | diagnostics: Diagnostic[]; 13 | result?: string; 14 | declarations?: string; 15 | } 16 | export declare function startsWith(str: string, match: string): boolean; 17 | export declare function endsWith(str: string, match: string): boolean; 18 | export declare function setTypeScriptResolveOptions(options: { 19 | paths: string[]; 20 | } | undefined): void; 21 | export declare function getTypeScriptResolveOptions(): { 22 | paths: string[]; 23 | } | undefined; 24 | export declare function getTypeScript(): typeof import("typescript"); 25 | export declare function resolveTypings(typings: string): string; 26 | export declare function resolveLib(libFile: string): string; 27 | export declare function enumLibFiles(): string[]; 28 | -------------------------------------------------------------------------------- /build/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 5 | }) : (function(o, m, k, k2) { 6 | if (k2 === undefined) k2 = k; 7 | o[k2] = m[k]; 8 | })); 9 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 10 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 11 | }) : function(o, v) { 12 | o["default"] = v; 13 | }); 14 | var __importStar = (this && this.__importStar) || function (mod) { 15 | if (mod && mod.__esModule) return mod; 16 | var result = {}; 17 | if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 18 | __setModuleDefault(result, mod); 19 | return result; 20 | }; 21 | Object.defineProperty(exports, "__esModule", { value: true }); 22 | exports.enumLibFiles = exports.resolveLib = exports.resolveTypings = exports.getTypeScript = exports.getTypeScriptResolveOptions = exports.setTypeScriptResolveOptions = exports.endsWith = exports.startsWith = exports.repeatString = void 0; 23 | var nodeFS = __importStar(require("fs")); 24 | var nodePath = __importStar(require("path")); 25 | var logger_1 = require("./logger"); 26 | function repeatString(str, count) { 27 | // newer node versions 28 | if (str.repeat != null) 29 | return str.repeat(count); 30 | // older node versions 31 | var ret = ""; 32 | for (var i = 0; i < count; i++) 33 | ret += str; 34 | return ret; 35 | } 36 | exports.repeatString = repeatString; 37 | function startsWith(str, match) { 38 | return (str.length >= match.length && 39 | str.substr(0, match.length) === match); 40 | } 41 | exports.startsWith = startsWith; 42 | function endsWith(str, match) { 43 | return (str.length >= match.length && 44 | str.substr(-match.length) === match); 45 | } 46 | exports.endsWith = endsWith; 47 | var tsResolveOptions; 48 | function setTypeScriptResolveOptions(options) { 49 | tsResolveOptions = options; 50 | } 51 | exports.setTypeScriptResolveOptions = setTypeScriptResolveOptions; 52 | function getTypeScriptResolveOptions() { 53 | return tsResolveOptions; 54 | } 55 | exports.getTypeScriptResolveOptions = getTypeScriptResolveOptions; 56 | function getTypeScript() { 57 | return require(require.resolve("typescript", getTypeScriptResolveOptions())); 58 | } 59 | exports.getTypeScript = getTypeScript; 60 | function resolveTypings(typings) { 61 | if (!startsWith(typings, "@types") || nodePath.isAbsolute(typings)) { 62 | // this is an absolute path 63 | typings = typings.substr(typings.indexOf("@types")); 64 | } 65 | logger_1.log("resolveTypings(" + typings + ")", "debug"); 66 | if (!endsWith(typings, ".d.ts")) { 67 | typings = nodePath.join(typings, "index.d.ts"); 68 | } 69 | try { 70 | var ret = require.resolve(typings, getTypeScriptResolveOptions()); 71 | logger_1.log(" => " + ret, "debug"); 72 | return ret; 73 | } 74 | catch (e) { 75 | logger_1.log(" => no success: " + e, "debug"); 76 | return null; 77 | } 78 | } 79 | exports.resolveTypings = resolveTypings; 80 | function resolveLib(libFile) { 81 | logger_1.log("resolving lib file " + libFile, "debug"); 82 | var libPath = require.resolve("typescript/lib/" + libFile, getTypeScriptResolveOptions()); 83 | var ts = getTypeScript(); 84 | logger_1.log("libPath = " + libPath, "debug"); 85 | if (ts.sys.fileExists(libPath)) 86 | return libPath; 87 | } 88 | exports.resolveLib = resolveLib; 89 | function enumLibFiles() { 90 | logger_1.log("util", "enumLibFiles() =>", "debug"); 91 | var tsPath = require.resolve("typescript", getTypeScriptResolveOptions()); 92 | var libFiles = nodeFS.readdirSync(nodePath.dirname(tsPath)) 93 | .filter(function (name) { return /^lib(\.[\w\d]+)*?\.d\.ts$/.test(name); }) 94 | .map(function (file) { return nodePath.join(nodePath.dirname(tsPath), file); }); 95 | for (var _i = 0, libFiles_1 = libFiles; _i < libFiles_1.length; _i++) { 96 | var file = libFiles_1[_i]; 97 | logger_1.log("util", " " + file, "debug"); 98 | } 99 | return libFiles; 100 | } 101 | exports.enumLibFiles = enumLibFiles; 102 | -------------------------------------------------------------------------------- /build/virtual-fs.d.ts: -------------------------------------------------------------------------------- 1 | export declare class VirtualFileSystem { 2 | /** 3 | * Writes a file in the virtual FS 4 | * @param filename The path this file should be stored as 5 | * @param content The contents of the file 6 | * @param overwrite If existing files should be overwritten 7 | */ 8 | writeFile(filename: string, content: string, overwrite?: boolean): void; 9 | /** 10 | * Checks if a file exists in the virtual FS 11 | * @param filename The path of the file to look for 12 | */ 13 | fileExists(filename: string, suppressLog?: boolean): boolean; 14 | /** 15 | * Deletes a file in the virtual FS. If the file doesn't exist, nothing happens. 16 | * @param filename The path of the file to look for 17 | */ 18 | deleteFile(filename: string): void; 19 | /** 20 | * Reads a file's contents from the virtual FS 21 | * @param filename The path of the file to look for 22 | */ 23 | readFile(filename: string): string; 24 | /** 25 | * Returns the revision number of a file in the virtual FS 26 | * @param filename The path of the file to look for 27 | */ 28 | getFileVersion(filename: string): number; 29 | /** 30 | * Returns the file names of all files in the virtual fs 31 | */ 32 | getFilenames(): string[]; 33 | getDirectories(root: string): string[]; 34 | private files; 35 | } 36 | -------------------------------------------------------------------------------- /build/virtual-fs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.VirtualFileSystem = void 0; 4 | var logger_1 = require("./logger"); 5 | var VirtualFileSystem = /** @class */ (function () { 6 | function VirtualFileSystem() { 7 | this.files = {}; 8 | } 9 | /** 10 | * Writes a file in the virtual FS 11 | * @param filename The path this file should be stored as 12 | * @param content The contents of the file 13 | * @param overwrite If existing files should be overwritten 14 | */ 15 | VirtualFileSystem.prototype.writeFile = function (filename, content, overwrite) { 16 | if (overwrite === void 0) { overwrite = false; } 17 | logger_1.log("vfs", "writeFile(filename: \"" + filename + "\", content: length " + (content ? content.length : 0) + ", overwrite: " + overwrite, "debug"); 18 | var exists = this.fileExists(filename, true); 19 | if (!overwrite && exists) { 20 | throw new Error("The file " + filename + " already exists. Set overwrite to true if you want to override it"); 21 | } 22 | if (!exists) { 23 | logger_1.log("vfs", " creating new file with version 1", "debug"); 24 | this.files[filename] = { 25 | version: 1, 26 | content: content, 27 | }; 28 | } 29 | else if (this.files[filename].content !== content) { 30 | this.files[filename] = { 31 | version: this.files[filename].version + 1, 32 | content: content, 33 | }; 34 | logger_1.log("vfs", " updating file => version " + this.files[filename].version, "debug"); 35 | } 36 | }; 37 | /** 38 | * Checks if a file exists in the virtual FS 39 | * @param filename The path of the file to look for 40 | */ 41 | VirtualFileSystem.prototype.fileExists = function (filename, suppressLog) { 42 | if (suppressLog === void 0) { suppressLog = false; } 43 | var ret = filename in this.files; 44 | if (!suppressLog) 45 | logger_1.log("vfs", "fileExists(\"" + filename + "\") => " + ret, "debug"); 46 | return ret; 47 | }; 48 | /** 49 | * Deletes a file in the virtual FS. If the file doesn't exist, nothing happens. 50 | * @param filename The path of the file to look for 51 | */ 52 | VirtualFileSystem.prototype.deleteFile = function (filename) { 53 | logger_1.log("vfs", "deleteFile(\"" + filename + "\")", "debug"); 54 | if (this.fileExists(filename, true)) 55 | delete this.files[filename]; 56 | }; 57 | /** 58 | * Reads a file's contents from the virtual FS 59 | * @param filename The path of the file to look for 60 | */ 61 | VirtualFileSystem.prototype.readFile = function (filename) { 62 | if (!this.fileExists(filename, true)) { 63 | throw new Error("The file " + filename + " doesn't exist"); 64 | } 65 | var ret = this.files[filename].content; 66 | logger_1.log("vfs", "readFile(\"" + filename + "\") => length " + (ret ? ret.length : 0), "debug"); 67 | return ret; 68 | }; 69 | /** 70 | * Returns the revision number of a file in the virtual FS 71 | * @param filename The path of the file to look for 72 | */ 73 | VirtualFileSystem.prototype.getFileVersion = function (filename) { 74 | if (!this.fileExists(filename, true)) { 75 | throw new Error("The file " + filename + " doesn't exist"); 76 | } 77 | var ret = this.files[filename].version; 78 | logger_1.log("vfs", "getFileVersion(\"" + filename + "\") => " + ret, "debug"); 79 | return ret; 80 | }; 81 | /** 82 | * Returns the file names of all files in the virtual fs 83 | */ 84 | VirtualFileSystem.prototype.getFilenames = function () { 85 | logger_1.log("vfs", "getFilenames()", "debug"); 86 | return Object.keys(this.files); 87 | }; 88 | VirtualFileSystem.prototype.getDirectories = function (root) { 89 | logger_1.log("vfs", "fs.getDirectories(" + root + ")", "debug"); 90 | var paths = this.getFilenames(); 91 | logger_1.log("vfs", "fs.getDirectories => paths = " + paths, "debug"); 92 | paths = paths.filter(function (p) { return p.startsWith(root); }); 93 | logger_1.log("vfs", "fs.getDirectories => paths = " + paths, "debug"); 94 | paths = paths.map(function (p) { return p.substr(root.length + 1).split("/")[0]; }); 95 | logger_1.log("vfs", "fs.getDirectories => paths = " + paths, "debug"); 96 | return paths; 97 | }; 98 | return VirtualFileSystem; 99 | }()); 100 | exports.VirtualFileSystem = VirtualFileSystem; 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virtual-tsc", 3 | "version": "0.6.2", 4 | "description": "Provides means to compile TypeScript code in memory", 5 | "main": "build/index.js", 6 | "types": "./build/index.d.ts", 7 | "author": { 8 | "name": "AlCalzone", 9 | "email": "d.griesel@gmx.net" 10 | }, 11 | "license": "MIT", 12 | "scripts": { 13 | "compile": "tsc", 14 | "watch": "npm run compile -- --watch", 15 | "test:ts": "mocha --require ts-node/register --require source-map-support/register src/**/*.test.ts", 16 | "test": "npm run test:ts", 17 | "coverage": "nyc npm run test:ts", 18 | "lint:ts": "lint:ts", 19 | "lint": "npm run lint:ts \"src/**/*.ts\"", 20 | "release": "release-script" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/AlCalzone/virtual-tsc.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/AlCalzone/virtual-tsc/issues" 28 | }, 29 | "homepage": "https://github.com/AlCalzone/virtual-tsc#readme", 30 | "peerDependencies": { 31 | "@types/node": "*", 32 | "typescript": ">=2.0.0" 33 | }, 34 | "dependencies": { 35 | "debug": "^4.3.3", 36 | "picocolors": "1.0.0" 37 | }, 38 | "devDependencies": { 39 | "@alcalzone/release-script": "~1.10.1", 40 | "@types/chai": "^4.0.4", 41 | "@types/debug": "^4.1.7", 42 | "@types/mocha": "^2.2.43", 43 | "@types/node": "^8.0.34", 44 | "@types/sinon": "^4.0.0", 45 | "chai": "^4.1.2", 46 | "mocha": "^4.0.0", 47 | "nyc": "^11.2.1", 48 | "sinon": "^4.1.2", 49 | "source-map-support": "^0.5.0", 50 | "ts-node": "^3.3.0", 51 | "tslint": "^5.7.0", 52 | "typescript": ">=3.9.5" 53 | }, 54 | "nyc": { 55 | "include": [ 56 | "src/**/*.ts" 57 | ], 58 | "exclude": [ 59 | "src/**/*.test.ts" 60 | ], 61 | "extension": [ 62 | ".ts" 63 | ], 64 | "require": [ 65 | "ts-node/register" 66 | ], 67 | "reporter": [ 68 | "text-summary", 69 | "html" 70 | ], 71 | "sourceMap": true, 72 | "instrument": true 73 | } 74 | } -------------------------------------------------------------------------------- /src/compiler.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryHost } from "./host"; 2 | import { CompileResult, Diagnostic, repeatString, getTypeScript } from "./util"; 3 | import { VirtualFileSystem } from "./virtual-fs"; 4 | import type { CompilerOptions as tsCompilerOptions } from "typescript"; 5 | 6 | const SCRIPT_FILENAME: string = "__virtual-tsc__.ts"; 7 | 8 | export function compileAsync( 9 | script: string, 10 | compilerOptions?: tsCompilerOptions, 11 | declarations: {[filename: string]: string} = {}, 12 | ): Promise { 13 | return new Promise((res, rej) => { 14 | setImmediate(() => { 15 | try { 16 | const ret = compile(script, compilerOptions, declarations); 17 | res(ret); 18 | } catch (e) { 19 | rej(e); 20 | } 21 | }); 22 | }); 23 | } 24 | 25 | export function compile( 26 | script: string, 27 | compilerOptions?: tsCompilerOptions, 28 | ambientDeclarations: {[filename: string]: string} = {}, 29 | ): CompileResult { 30 | const ts = getTypeScript(); 31 | const sourceLines = script.split("\n"); 32 | 33 | // set default compiler options 34 | compilerOptions = compilerOptions || {}; 35 | compilerOptions.moduleResolution = ts.ModuleResolutionKind.NodeJs; 36 | // Don't emit faulty code (by default) 37 | if (compilerOptions.noEmitOnError == null) compilerOptions.noEmitOnError = true; 38 | // emit declarations if possible 39 | if (compilerOptions.declaration == null) compilerOptions.declaration = true; 40 | 41 | // According to https://github.com/Microsoft/TypeScript/issues/24444#issuecomment-392970120 42 | // combining noEmitOnError=true and declaration=true massively increases the work done 43 | // by the compiler. To work around it, we call the compiler with noEmitOnError=false 44 | // and use the actual value to determine if we continue with the emit 45 | const internalOptions = Object.assign({}, compilerOptions, { 46 | noEmitOnError: false, 47 | } as tsCompilerOptions); 48 | 49 | // provide the source file in the virtual fs 50 | const fs = new VirtualFileSystem(); 51 | fs.writeFile(SCRIPT_FILENAME, script); 52 | // provide all ambient declaration files 53 | for (const ambientFile of Object.keys(ambientDeclarations)) { 54 | if (!/\.d\.ts$/.test(ambientFile)) throw new Error("Declarations must be .d.ts-files"); 55 | fs.writeFile(ambientFile, ambientDeclarations[ambientFile], true); 56 | } 57 | 58 | // create the virtual host 59 | const host = new InMemoryHost(fs, internalOptions); 60 | // create the compiler and provide nodejs typings 61 | const allFiles = [ 62 | "@types/node/index.d.ts", 63 | ...Object.keys(ambientDeclarations), 64 | SCRIPT_FILENAME, 65 | ]; 66 | const program = ts.createProgram(allFiles, internalOptions, host); 67 | 68 | // compile the script 69 | const emitResult = program.emit(); 70 | 71 | // diagnose the compilation result 72 | const rawDiagnostics = internalOptions.noEmitOnError ? emitResult.diagnostics : ts.getPreEmitDiagnostics(program); 73 | const diagnostics = rawDiagnostics.map(diagnostic => { 74 | let lineNr = 0; 75 | let charNr = 0; 76 | if (diagnostic.file != null) { 77 | const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); 78 | [lineNr, charNr] = [line, character]; 79 | } 80 | const description = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); 81 | const type = ts.DiagnosticCategory[diagnostic.category].toLowerCase() as "error" | "warning" | "message"; 82 | const sourceLine = sourceLines[lineNr]; 83 | const annotatedSource = `${sourceLine} 84 | ${repeatString(" ", charNr)}^ 85 | ${type.toUpperCase()}: ${description}`; 86 | return { 87 | type, 88 | lineNr: lineNr + 1, 89 | charNr: charNr + 1, 90 | sourceLine, 91 | description, 92 | annotatedSource, 93 | } as Diagnostic; 94 | }); 95 | 96 | const hasError = ( 97 | ( 98 | diagnostics.find(d => d.type === "error") != null 99 | || (emitResult.emitSkipped && !compilerOptions.emitDeclarationOnly) 100 | ) 101 | && compilerOptions.noEmitOnError 102 | ); 103 | let result: string; 104 | const resultFilename = SCRIPT_FILENAME.replace(/ts$/, "js"); 105 | let declarations: string; 106 | const declarationsFilename = SCRIPT_FILENAME.replace(/ts$/, "d.ts"); 107 | if (!hasError && fs.fileExists(resultFilename)) result = fs.readFile(resultFilename); 108 | if (!hasError && fs.fileExists(declarationsFilename)) declarations = fs.readFile(declarationsFilename); 109 | 110 | return { 111 | success: !hasError, 112 | diagnostics, 113 | result, 114 | declarations, 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /src/host.ts: -------------------------------------------------------------------------------- 1 | import * as nodePath from "path"; 2 | import type { CompilerHost as tsCompilerHost, CompilerOptions as tsCompilerOptions, ScriptTarget as tsScriptTarget, SourceFile as tsSourceFile } from "typescript"; 3 | import { log } from "./logger"; 4 | import { resolveTypings, getTypeScript, getTypeScriptResolveOptions } from "./util"; 5 | import type { VirtualFileSystem } from "./virtual-fs"; 6 | 7 | // reference: https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#customizing-module-resolution 8 | 9 | /** 10 | * Implementation of CompilerHost that works with in-memory-only source files 11 | */ 12 | export class InMemoryHost implements tsCompilerHost { 13 | 14 | constructor( 15 | private fs: VirtualFileSystem, 16 | private options: tsCompilerOptions, 17 | ) { 18 | this.ts = getTypeScript(); 19 | } 20 | 21 | private ts: typeof import("typescript"); 22 | 23 | public getSourceFile(fileName: string, languageVersion: tsScriptTarget, onError?: (message: string) => void): tsSourceFile { 24 | let fileContent: string; 25 | if (this.fs.fileExists(fileName)) { 26 | log(`getSourceFile(fileName="${fileName}", version=${languageVersion}) => returning provided file`, "debug"); 27 | fileContent = this.fs.readFile(fileName); 28 | } else if (/^lib\..*?d\.ts$/.test(fileName)) { 29 | // resolving lib file 30 | const libPath = nodePath.join(nodePath.dirname(require.resolve("typescript", getTypeScriptResolveOptions())), fileName); 31 | log(`getSourceFile(fileName="${fileName}") => resolved lib file ${libPath}`, "debug"); 32 | fileContent = this.ts.sys.readFile(libPath); 33 | if (fileContent != null) this.fs.writeFile(fileName, fileContent, true); 34 | } else if (/\@types\/.+$/.test(fileName)) { 35 | // resolving a specific node module 36 | log(`getSourceFile(fileName="${fileName}") => resolving typings`, "debug"); 37 | fileName = resolveTypings(fileName); 38 | fileContent = this.ts.sys.readFile(fileName); 39 | if (fileContent != null) this.fs.writeFile(fileName, fileContent, true); 40 | } 41 | if (fileContent != null) { 42 | log("file content is not null", "debug"); 43 | return this.ts.createSourceFile(fileName, this.fs.readFile(fileName), languageVersion); 44 | } else { 45 | log("file content is null", "debug"); 46 | } 47 | } 48 | 49 | public getDefaultLibFileName(options: tsCompilerOptions): string { 50 | options = options || this.options; 51 | log(`getDefaultLibFileName(${JSON.stringify(options, null, 4)})`, "debug"); 52 | return "lib.d.ts"; 53 | } 54 | 55 | public writeFile(path: string, content: string) { 56 | log(`writeFile(path="${path}")`, "debug"); 57 | this.fs.writeFile(path, content, true); 58 | } 59 | 60 | public getCurrentDirectory(): string { 61 | const ret = this.ts.sys.getCurrentDirectory(); 62 | log(`getCurrentDirectory() => ${ret}`, "debug"); 63 | return ret; 64 | } 65 | 66 | public getDirectories(path: string): string[] { 67 | log(`getDirectories(${path})`, "debug"); 68 | throw new Error("Method not implemented."); 69 | } 70 | 71 | public getCanonicalFileName(fileName: string): string { 72 | log(`getCanonicalFileName(${fileName})`, "debug"); 73 | return this.ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); 74 | } 75 | 76 | public useCaseSensitiveFileNames(): boolean { 77 | log(`useCaseSensitiveFileNames()`, "debug"); 78 | return this.ts.sys.useCaseSensitiveFileNames; 79 | } 80 | public getNewLine(): string { 81 | log(`getNewLine()`, "debug"); 82 | return this.ts.sys.newLine; 83 | } 84 | 85 | // public resolveModuleNames?(moduleNames: string[], containingFile: string): ts.ResolvedModule[] { 86 | // log(`resolveModuleNames(${moduleNames})`); 87 | // return moduleNames.map(moduleName => { 88 | // { // try to use standard resolution 89 | // const result = ts.resolveModuleName( 90 | // moduleName, containingFile, 91 | // this.options, 92 | // { 93 | // fileExists: this.fileExists.bind(this), 94 | // readFile: this.readFile.bind(this), 95 | // }, 96 | // ); 97 | // if (result.resolvedModule) return result.resolvedModule; 98 | // } 99 | 100 | // try { // fall back to NodeJS resolution 101 | // const fileName = require.resolve(moduleName); 102 | // if (fileName === moduleName) return; // internal module 103 | // log(`resolved ${moduleName} => ${fileName}`); 104 | // return { 105 | // resolvedFileName: fileName, 106 | // } as ts.ResolvedModule; 107 | // } catch (e) { 108 | // /* Not found */ 109 | // } 110 | // }); 111 | // } 112 | 113 | public fileExists(fileName: string): boolean { 114 | log(`fileExists(${fileName})`, "debug"); 115 | return this.fs.fileExists(fileName); 116 | } 117 | public readFile(fileName: string): string { 118 | log(`readFile(${fileName})`, "debug"); 119 | return this.fs.readFile(fileName); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as fs from "fs"; 3 | import * as ts from "typescript"; 4 | import { compile, CompileResult } from "./"; 5 | import { log } from "./logger"; 6 | import { Server } from "./server"; 7 | 8 | // tslint:disable:no-unused-expression 9 | // tslint:disable:no-eval 10 | 11 | const options = { 12 | target: ts.ScriptTarget.ES2015, 13 | }; 14 | 15 | describe("compiler => ", function () { 16 | this.timeout(30000); 17 | 18 | it("it should not explode", () => { 19 | const result = compile("", options); 20 | expect(result.success).to.be.true; 21 | expect(result.diagnostics).to.be.empty; 22 | expect(result.result).to.equal(""); 23 | }); 24 | 25 | it("some basic stuff should work", () => { 26 | const source = `let ret: number = 0; 27 | for (const x of [1,2,3,4,5]) ret += x; 28 | ret;`; 29 | const result = compile(source); 30 | expect(result.success).to.be.true; 31 | expect(result.diagnostics).to.be.empty; 32 | expect(eval(result.result)).to.equal(15); 33 | }); 34 | 35 | it("it should report an error on type mismatch", () => { 36 | const result = compile(`const x: number = "1";`); 37 | expect(result.success).to.be.false; 38 | expect(result.diagnostics).to.have.length(1); 39 | expect(result.diagnostics[0].type).to.equal("error"); 40 | expect(result.result).to.be.undefined; 41 | }); 42 | 43 | it("it should report syntax errors", () => { 44 | const result = compile(`const x: number = ;`); 45 | expect(result.success).to.be.false; 46 | expect(result.diagnostics).to.have.length(1); 47 | expect(result.diagnostics[0].type).to.equal("error"); 48 | expect(result.result).to.be.undefined; 49 | }); 50 | 51 | it("it should detect functions in ambient declarations", () => { 52 | 53 | const ambient = `import * as fs from "fs"; // dummy import 54 | declare global { 55 | function log(text: string); 56 | }`; 57 | let result = compile(`log("this fails");`); 58 | expect(result.success).to.be.false; 59 | result = compile(`log("this succeeds");`, null, { "global.d.ts": ambient }); 60 | expect(result.success).to.be.true; 61 | 62 | }); 63 | 64 | it("it should force ambient declarations to be .d.ts files", () => { 65 | expect(() => compile("", null, { "global.ts": "" })).to.throw(); 66 | }); 67 | }); 68 | 69 | describe("performance check =>", function () { 70 | this.timeout(30000); 71 | 72 | it("compiler", () => { 73 | const ambient = fs.readFileSync("./test/ioBroker.d.ts", "utf8"); 74 | let result: CompileResult; 75 | for (let i = 0; i < 5; i++) { 76 | result = compile( 77 | `const buf = Buffer.alloc(${i} + 1); 78 | console.log(buf.length)`, 79 | null, { "global.d.ts": ambient }, 80 | ); 81 | expect(result.success).to.be.true; 82 | // call durations: ~1200..900 ms 83 | } 84 | }); 85 | 86 | it("service host", () => { 87 | const tsserver = new Server(options); 88 | const ambient = fs.readFileSync("./test/ioBroker.d.ts", "utf8"); 89 | tsserver.provideAmbientDeclarations({ "global.d.ts": ambient }); 90 | let result: CompileResult; 91 | for (let i = 0; i < 5; i++) { 92 | log("starting compilation", "info"); 93 | result = tsserver.compile("index.ts", 94 | `const buf = Buffer.alloc(${i} + 1); 95 | console.log(buf.length)`, 96 | ); 97 | log("compilation done!", "info"); 98 | expect(result.success).to.be.true; 99 | expect(result.declarations).to.equal("declare const buf: Buffer;\r\n"); 100 | // call durations: ~1200, then ~100..200 ms for the following calls 101 | } 102 | }); 103 | }); 104 | 105 | describe("error detection =>", function () { 106 | this.timeout(30000); 107 | 108 | const noEmitOnErrorOptions: ts.CompilerOptions = { 109 | declaration: true, 110 | target: ts.ScriptTarget.ES2015, 111 | noEmitOnError: true, 112 | }; 113 | const emitOnErrorOptions: ts.CompilerOptions = { 114 | declaration: true, 115 | target: ts.ScriptTarget.ES2015, 116 | noEmitOnError: false, 117 | }; 118 | 119 | const noEmitOnErrorServer = new Server(noEmitOnErrorOptions); 120 | const emitOnErrorServer = new Server(emitOnErrorOptions); 121 | const ambient = fs.readFileSync("./test/ioBroker.d.ts", "utf8"); 122 | noEmitOnErrorServer.provideAmbientDeclarations({ "global.d.ts": ambient }); 123 | emitOnErrorServer.provideAmbientDeclarations({ "global.d.ts": ambient }); 124 | 125 | let resultNoEmitOnError: CompileResult; 126 | let resultEmitOnError: CompileResult; 127 | 128 | describe("syntax errors => ", () => { 129 | it("should be detected, regardless of the noEmitOnError setting", () => { 130 | const codeWithSyntaxError = `const buf = Buffer.alloc(1 +); 131 | console.log(buf.length)`; 132 | 133 | resultEmitOnError = emitOnErrorServer.compile("index.ts", codeWithSyntaxError); 134 | expect(resultEmitOnError.success).to.be.true; 135 | expect(resultEmitOnError.diagnostics).to.have.length.of.at.least(1); 136 | 137 | resultNoEmitOnError = noEmitOnErrorServer.compile("index.ts", codeWithSyntaxError); 138 | expect(resultNoEmitOnError.success).to.be.false; 139 | }); 140 | 141 | it("when noEmitOnError == false, code should still be emitted", () => { 142 | expect(resultEmitOnError.result) 143 | .to.exist 144 | .and.to.equal("const buf = Buffer.alloc(1 + );\r\nconsole.log(buf.length);\r\n") 145 | ; 146 | expect(resultEmitOnError.declarations) 147 | .to.exist 148 | .and.to.equal("declare const buf: Buffer;\r\n") 149 | ; 150 | }); 151 | 152 | it("when noEmitOnError == true, code should NOT be emitted", () => { 153 | expect(resultNoEmitOnError.result).to.be.undefined; 154 | expect(resultNoEmitOnError.declarations).to.be.undefined; 155 | }); 156 | }); 157 | 158 | describe("semantic errors => ", () => { 159 | it("should be detected, regardless of the noEmitOnError setting", () => { 160 | const codeWithSyntaxError = `let buf: Buffer = "foo"`; 161 | 162 | resultEmitOnError = emitOnErrorServer.compile("index.ts", codeWithSyntaxError); 163 | expect(resultEmitOnError.success).to.be.true; 164 | expect(resultEmitOnError.diagnostics).to.have.length.of.at.least(1); 165 | 166 | resultNoEmitOnError = noEmitOnErrorServer.compile("index.ts", codeWithSyntaxError); 167 | expect(resultNoEmitOnError.success).to.be.false; 168 | }); 169 | 170 | it("when noEmitOnError == false, code should still be emitted", () => { 171 | expect(resultEmitOnError.result) 172 | .to.exist 173 | .and.to.equal("let buf = \"foo\";\r\n") 174 | ; 175 | expect(resultEmitOnError.declarations) 176 | .to.exist 177 | .and.to.equal("declare let buf: Buffer;\r\n") 178 | ; 179 | }); 180 | 181 | it("when noEmitOnError == true, code should NOT be emitted", () => { 182 | expect(resultNoEmitOnError.result).to.be.undefined; 183 | expect(resultNoEmitOnError.declarations).to.be.undefined; 184 | }); 185 | }); 186 | 187 | }); 188 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { compile, compileAsync } from "./compiler"; 2 | export { CompileResult, Diagnostic, setTypeScriptResolveOptions } from "./util"; 3 | export { Server } from "./server"; 4 | -------------------------------------------------------------------------------- /src/logger.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { spy, stub } from "sinon"; 3 | 4 | import { log, setCustomLogger } from "./logger"; 5 | 6 | // tslint:disable:no-unused-expression 7 | 8 | describe.skip("lib/logger => ", () => { 9 | 10 | let loggerStub: sinon.SinonSpy; 11 | 12 | it(`gets called with the correct arguments`, () => { 13 | loggerStub = spy(); 14 | setCustomLogger(loggerStub); 15 | 16 | // log("message", "debug"); 17 | 18 | expect(loggerStub.calledOnce).to.be.true; 19 | assert(loggerStub.calledWithExactly("message", "debug")); 20 | }); 21 | 22 | it(`has a default severity of "info"`, () => { 23 | loggerStub = spy(); 24 | setCustomLogger(loggerStub); 25 | 26 | // log("message"); 27 | 28 | assert(loggerStub.calledWithExactly("message", "info")); 29 | }); 30 | 31 | // were not testing the debug package redirection 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line:no-var-requires 2 | import debug from "debug"; 3 | import colors from "picocolors" 4 | 5 | const defaultNamespace = "virtual-tsc"; 6 | export type SubNamespaces = "server" | "host" | "util" | "vfs"; 7 | export type Severity = "info" | "warn" | "debug" | "error" | "silly"; 8 | 9 | export type LoggerFunction = (message: string, severity?: Severity) => void; 10 | 11 | let customLogger: LoggerFunction | false; 12 | export function setCustomLogger(logger: LoggerFunction | false): void { 13 | customLogger = logger; 14 | } 15 | 16 | const formatters = { 17 | info: (message: string) => colors.blue(message), 18 | warn: (message: string) => colors.yellow(message), 19 | debug: (message: string) => colors.white(message), 20 | error: (message: string) => colors.red(message), 21 | silly: (message: string) => colors.white(message), 22 | }; 23 | 24 | export function log(message: string, severity: Severity): void; 25 | export function log(namespace: SubNamespaces, message: string, severity: Severity): void; 26 | export function log(...args: any[]) { 27 | 28 | if (customLogger === false) return; 29 | 30 | // we only accept strings 31 | if (!args || !args.length || !args.every(arg => typeof arg === "string")) { 32 | throw new Error("Invalid arguments passed to log()"); 33 | } 34 | 35 | let namespace: string = ""; 36 | let message: string; 37 | let severity: Severity; 38 | if (args.length === 2) { 39 | ([message, severity] = args); 40 | } else if (args.length === 3) { 41 | ([namespace, message, severity] = args); 42 | // add the namespace separator to append the namespace to the default one 43 | if (typeof namespace === "string" && namespace !== "") namespace = ":" + namespace; 44 | } 45 | 46 | function defaultLogger() { 47 | let prefix: string = ""; 48 | if (severity !== "info") { 49 | prefix = `[${severity.toUpperCase()}] `; 50 | } 51 | debug(defaultNamespace + namespace)(`${prefix}${formatters[severity](message)}`); 52 | } 53 | 54 | (customLogger || defaultLogger)(message, severity); 55 | } 56 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as nodePath from "path"; 2 | import { log, LoggerFunction, setCustomLogger } from "./logger"; 3 | import { InMemoryServiceHost } from "./service-host"; 4 | import { CompileResult, Diagnostic, enumLibFiles, repeatString, resolveLib, resolveTypings, getTypeScript } from "./util"; 5 | import { VirtualFileSystem } from "./virtual-fs"; 6 | import type { LanguageService as tsLanguageService, CompilerOptions as tsCompilerOptions, Diagnostic as tsDiagnostic } from "typescript"; 7 | 8 | export class Server { 9 | 10 | private service: tsLanguageService; 11 | private fs: VirtualFileSystem; 12 | private host: InMemoryServiceHost; 13 | private ts: typeof import("typescript"); 14 | 15 | constructor( 16 | private options?: tsCompilerOptions, 17 | customLogger?: LoggerFunction | false, 18 | ) { 19 | this.ts = getTypeScript(); 20 | 21 | if (customLogger != null) setCustomLogger(customLogger); 22 | 23 | // set default compiler options 24 | this.options = this.options || {}; 25 | this.options.moduleResolution = this.ts.ModuleResolutionKind.NodeJs; 26 | // Don't emit faulty code (by default) 27 | if (this.options.noEmitOnError == null) this.options.noEmitOnError = true; 28 | // emit declarations if possible 29 | if (this.options.declaration == null) this.options.declaration = true; 30 | 31 | // According to https://github.com/Microsoft/TypeScript/issues/24444#issuecomment-392970120 32 | // combining noEmitOnError=true and declaration=true massively increases the work done 33 | // by the compiler. To work around it, we call the compiler with noEmitOnError=false 34 | // and use the actual value to determine if we continue with the emit 35 | const internalOptions = Object.assign({}, this.options, { 36 | noEmitOnError: false, 37 | } as tsCompilerOptions); 38 | 39 | // set up the build pipeline 40 | this.fs = new VirtualFileSystem(); 41 | this.host = new InMemoryServiceHost(this.fs, internalOptions); 42 | this.service = this.ts.createLanguageService(this.host, this.ts.createDocumentRegistry()); 43 | 44 | // provide the requested lib files 45 | if (!options.noLib) { 46 | const libFiles = enumLibFiles(); 47 | for (const file of libFiles) { 48 | const fileContent = this.ts.sys.readFile(file); 49 | if (fileContent != null) this.fs.writeFile(nodePath.basename(file), fileContent, true); 50 | } 51 | } 52 | 53 | // provide the most basic typings 54 | const basicTypings = [ 55 | "@types/node/index.d.ts", 56 | "@types/node/inspector.d.ts", 57 | ]; 58 | for (const typings of basicTypings) { 59 | // resolving a specific node module 60 | const path = resolveTypings(typings); 61 | const fileContent = this.ts.sys.readFile(path); 62 | if (fileContent != null) this.fs.writeFile(typings, fileContent, true); 63 | } 64 | } 65 | 66 | public provideAmbientDeclarations(declarations: { [filename: string]: string } = {}) { 67 | // provide all ambient declaration files 68 | for (const ambientFile of Object.keys(declarations)) { 69 | if (!ambientFile.endsWith(".d.ts") && !ambientFile.endsWith("package.json")) { 70 | throw new Error("Declarations must be .d.ts or package.json files"); 71 | } 72 | this.fs.writeFile(ambientFile, declarations[ambientFile], true); 73 | } 74 | } 75 | 76 | public compile(filename: string, scriptContent: string): CompileResult { 77 | const sourceLines = scriptContent.split("\n"); 78 | this.fs.writeFile(filename, scriptContent, true); 79 | 80 | const rawDiagnostics: tsDiagnostic[] = []; 81 | rawDiagnostics.push(...this.service.getSyntacticDiagnostics(filename)); 82 | rawDiagnostics.push(...this.service.getSemanticDiagnostics(filename)); 83 | 84 | const emitResult = this.service.getEmitOutput(filename); 85 | 86 | rawDiagnostics.push(...this.service.getCompilerOptionsDiagnostics()); 87 | 88 | const diagnostics = rawDiagnostics.map(diagnostic => { 89 | let lineNr = 0; 90 | let charNr = 0; 91 | if (diagnostic.file != null) { 92 | const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); 93 | [lineNr, charNr] = [line, character]; 94 | } 95 | const description = this.ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); 96 | const type = this.ts.DiagnosticCategory[diagnostic.category].toLowerCase() as "error" | "warning" | "message"; 97 | const sourceLine = sourceLines[lineNr]; 98 | const annotatedSource = `${sourceLine} 99 | ${repeatString(" ", charNr)}^ 100 | ${type.toUpperCase()}: ${description}`; 101 | return { 102 | type, 103 | lineNr: lineNr + 1, 104 | charNr: charNr + 1, 105 | sourceLine, 106 | description, 107 | annotatedSource, 108 | } as Diagnostic; 109 | }); 110 | 111 | const hasError = ( 112 | ( 113 | diagnostics.find(d => d.type === "error") != null 114 | || (emitResult.emitSkipped && !this.options.emitDeclarationOnly) 115 | ) 116 | && this.options.noEmitOnError 117 | ); 118 | let result: string; 119 | let declarations: string; 120 | if (!hasError) { 121 | const resultFile = emitResult.outputFiles.find(f => f.name.endsWith(".js")); 122 | if (resultFile != null) result = resultFile.text; 123 | const declarationFile = emitResult.outputFiles.find(f => f.name.endsWith(".d.ts")); 124 | if (declarationFile != null) declarations = declarationFile.text; 125 | } 126 | 127 | return { 128 | success: !hasError, 129 | diagnostics, 130 | result, 131 | declarations, 132 | }; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/service-host.ts: -------------------------------------------------------------------------------- 1 | import { log } from "./logger"; 2 | import type { VirtualFileSystem } from "./virtual-fs"; 3 | import type { CompilerOptions as tsCompilerOptions, LanguageServiceHost as tsLanguageServiceHost, IScriptSnapshot as tsIScriptSnapshot } from "typescript"; 4 | import { getTypeScript } from "./util"; 5 | 6 | // https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#incremental-build-support-using-the-language-services 7 | 8 | /** 9 | * Implementation of LanguageServiceHost that works with in-memory-only source files 10 | */ 11 | export class InMemoryServiceHost implements tsLanguageServiceHost { 12 | 13 | private ts: typeof import("typescript"); 14 | 15 | constructor( 16 | private fs: VirtualFileSystem, 17 | private options: tsCompilerOptions, 18 | ) { 19 | this.ts = getTypeScript(); 20 | } 21 | 22 | public getCompilationSettings(): tsCompilerOptions { 23 | return this.options; 24 | } 25 | 26 | public getScriptFileNames(): string[] { 27 | return this.fs 28 | .getFilenames() 29 | .filter(f => f.endsWith(".ts") /* && !f.endsWith(".d.ts") */) 30 | ; 31 | } 32 | 33 | public getScriptVersion(fileName: string): string { 34 | return this.fs.getFileVersion(fileName).toString(); 35 | } 36 | 37 | public getScriptSnapshot(fileName: string): tsIScriptSnapshot { 38 | if (!this.fs.fileExists(fileName)) return undefined; 39 | return this.ts.ScriptSnapshot.fromString(this.fs.readFile(fileName)); 40 | } 41 | 42 | public getCurrentDirectory(): string { 43 | return ""; 44 | // return CWD; 45 | // return this.ts.sys.getCurrentDirectory(); 46 | } 47 | 48 | public getDefaultLibFileName(options: tsCompilerOptions): string { 49 | options = options || this.options; 50 | log("host", `getDefaultLibFileName(${JSON.stringify(options, null, 4)})`, "debug"); 51 | return "lib.d.ts"; 52 | } 53 | // log?(s: string): void { 54 | // throw new Error("Method not implemented."); 55 | // } 56 | // trace?(s: string): void { 57 | // throw new Error("Method not implemented."); 58 | // } 59 | // error?(s: string): void { 60 | // throw new Error("Method not implemented."); 61 | // } 62 | 63 | public readFile(path: string, encoding?: string): string { 64 | log("host", `readFile(${path})`, "debug"); 65 | if (this.fs.fileExists(path)) { 66 | return this.fs.readFile(path); 67 | } else if (path.indexOf("node_modules") > -1) { 68 | return this.ts.sys.readFile(path); 69 | } 70 | } 71 | public fileExists(path: string): boolean { 72 | log("host", `fileExists(${path})`, "debug"); 73 | let ret: boolean; 74 | if (this.fs.fileExists(path)) { 75 | ret = true; 76 | } else if (path.indexOf("node_modules") > -1) { 77 | ret = this.ts.sys.fileExists(path); 78 | } 79 | log("host", `fileExists(${path}) => ${ret}`, "debug"); 80 | return ret; 81 | } 82 | 83 | public readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[] { 84 | log("host", `readDirectory( 85 | ${path}, 86 | ${extensions ? JSON.stringify(extensions) : "null"}, 87 | ${exclude ? JSON.stringify(exclude) : "null"}, 88 | ${include ? JSON.stringify(include) : "null"}, 89 | ${depth}, 90 | `, "debug"); 91 | return this.ts.sys.readDirectory(path, extensions, exclude, include, depth); 92 | } 93 | 94 | public getDirectories(directoryName: string): string[] { 95 | log("host", `getDirectories(${directoryName})`, "debug"); 96 | 97 | // typings should be loaded from the virtual fs or we get problems 98 | if (directoryName.indexOf("node_modules/@types") > -1) { 99 | return []; 100 | } 101 | 102 | try { 103 | return this.ts.sys.getDirectories(directoryName); 104 | } catch (e) { 105 | return []; 106 | } 107 | } 108 | 109 | // public resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames?: string[]): ts.ResolvedModule[] { 110 | // log(`resolveModuleNames( 111 | // ${JSON.stringify(moduleNames)}, 112 | // ${containingFile}, 113 | // ${reusedNames ? JSON.stringify(reusedNames) : "null"} 114 | // `); 115 | // throw new Error("Method not implemented."); 116 | // } 117 | 118 | // public resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ts.ResolvedTypeReferenceDirective[] { 119 | // const ret = typeDirectiveNames.map( 120 | // t => resolveTypings(`@types/${t}/index.d.ts`), 121 | // ); 122 | // log(`resolveTypeReferenceDirectives( 123 | // ${JSON.stringify(typeDirectiveNames)}, 124 | // ${containingFile} 125 | // ) => ${JSON.stringify(ret)}`); 126 | 127 | // return ret.map(f => ({ 128 | // primary: true, 129 | // resolvedFileName: f, 130 | // })); 131 | // } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import * as nodeFS from "fs"; 2 | import * as nodePath from "path"; 3 | import { log } from "./logger"; 4 | export interface Diagnostic { 5 | type: "error" | "warning" | "message"; 6 | lineNr: number; 7 | charNr: number; 8 | sourceLine: string; 9 | description: string; 10 | annotatedSource: string; 11 | } 12 | 13 | export function repeatString(str: string, count: number): string { 14 | // newer node versions 15 | if ((str as any).repeat != null) return (str as any).repeat(count); 16 | // older node versions 17 | let ret = ""; 18 | for (let i = 0; i < count; i++) ret += str; 19 | return ret; 20 | } 21 | 22 | export interface CompileResult { 23 | success: boolean; 24 | diagnostics: Diagnostic[]; 25 | result?: string; 26 | declarations?: string; 27 | } 28 | 29 | export function startsWith(str: string, match: string): boolean { 30 | return ( 31 | str.length >= match.length && 32 | str.substr(0, match.length) === match 33 | ); 34 | } 35 | 36 | export function endsWith(str: string, match: string): boolean { 37 | return ( 38 | str.length >= match.length && 39 | str.substr(-match.length) === match 40 | ); 41 | } 42 | 43 | let tsResolveOptions: { paths: string[] } | undefined; 44 | export function setTypeScriptResolveOptions(options: { paths: string[] } | undefined): void { 45 | tsResolveOptions = options; 46 | } 47 | export function getTypeScriptResolveOptions(): { paths: string[] } | undefined { 48 | return tsResolveOptions; 49 | } 50 | 51 | export function getTypeScript(): typeof import("typescript") { 52 | return require(require.resolve("typescript", getTypeScriptResolveOptions())); 53 | } 54 | 55 | export function resolveTypings(typings: string): string { 56 | if (!startsWith(typings, "@types") || nodePath.isAbsolute(typings)) { 57 | // this is an absolute path 58 | typings = typings.substr(typings.indexOf("@types")); 59 | } 60 | log(`resolveTypings(${typings})`, "debug"); 61 | if (!endsWith(typings, ".d.ts")) { 62 | typings = nodePath.join(typings, "index.d.ts"); 63 | } 64 | try { 65 | const ret = require.resolve(typings, getTypeScriptResolveOptions()); 66 | log(" => " + ret, "debug"); 67 | return ret; 68 | } catch (e) { 69 | log(" => no success: " + e, "debug"); 70 | return null; 71 | } 72 | } 73 | 74 | export function resolveLib(libFile: string): string { 75 | log(`resolving lib file ${libFile}`, "debug"); 76 | const libPath = require.resolve(`typescript/lib/${libFile}`, getTypeScriptResolveOptions()); 77 | const ts = getTypeScript(); 78 | log(`libPath = ${libPath}`, "debug"); 79 | if (ts.sys.fileExists(libPath)) return libPath; 80 | } 81 | 82 | export function enumLibFiles(): string[] { 83 | log("util", "enumLibFiles() =>", "debug"); 84 | const tsPath = require.resolve("typescript", getTypeScriptResolveOptions()); 85 | const libFiles = nodeFS.readdirSync(nodePath.dirname(tsPath)) 86 | .filter(name => /^lib(\.[\w\d]+)*?\.d\.ts$/.test(name)) 87 | .map(file => nodePath.join(nodePath.dirname(tsPath), file)) 88 | ; 89 | for (const file of libFiles) { 90 | log("util", " " + file, "debug"); 91 | } 92 | return libFiles; 93 | } 94 | -------------------------------------------------------------------------------- /src/virtual-fs.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { VirtualFileSystem } from "./virtual-fs"; 3 | // tslint:disable:no-unused-expression 4 | 5 | describe("virtual-fs => ", () => { 6 | 7 | let vfs: VirtualFileSystem; 8 | 9 | it("the constructor should not explode", () => { 10 | vfs = new VirtualFileSystem(); 11 | }); 12 | 13 | const dummyFile = "DUMMY"; 14 | const dummyFileName = "dummy.txt"; 15 | it("a file should exist after writing", () => { 16 | expect(vfs.fileExists(dummyFileName)).to.be.false; 17 | vfs.writeFile(dummyFileName, dummyFile); 18 | expect(vfs.fileExists(dummyFileName)).to.be.true; 19 | }); 20 | 21 | it("readFile should return the correct file contents", () => { 22 | expect(vfs.readFile(dummyFileName)).to.equal(dummyFile); 23 | }); 24 | 25 | it("readFile should throw when the file doesn't exist", () => { 26 | expect(() => vfs.readFile("does-not-exist.txt")).to.throw(); 27 | }); 28 | 29 | it("writeFile should throw when overwriting files without the parameter set to true", () => { 30 | expect(() => vfs.writeFile(dummyFileName, "SHOULD NOT BE HERE")).to.throw(); 31 | expect(vfs.readFile(dummyFileName)).to.equal(dummyFile); 32 | }); 33 | 34 | it("writeFile should correctly overwrite existing files if asked to", () => { 35 | expect(() => vfs.writeFile(dummyFileName, "NEW TEXT", true)).to.not.throw(); 36 | expect(vfs.readFile(dummyFileName)).to.equal("NEW TEXT"); 37 | }); 38 | 39 | it("a file should no longer exist after deleting it", () => { 40 | expect(vfs.fileExists(dummyFileName)).to.be.true; 41 | vfs.deleteFile(dummyFileName); 42 | expect(vfs.fileExists(dummyFileName)).to.be.false; 43 | }); 44 | 45 | it("deleteFile should silently return when the file doesn't exist", () => { 46 | expect(() => vfs.deleteFile("does-not-exist.txt")).to.not.throw(); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /src/virtual-fs.ts: -------------------------------------------------------------------------------- 1 | import { log } from "./logger"; 2 | 3 | interface File { 4 | content: string; 5 | version: number; 6 | } 7 | 8 | export class VirtualFileSystem { 9 | 10 | /** 11 | * Writes a file in the virtual FS 12 | * @param filename The path this file should be stored as 13 | * @param content The contents of the file 14 | * @param overwrite If existing files should be overwritten 15 | */ 16 | public writeFile(filename: string, content: string, overwrite: boolean = false): void { 17 | log("vfs", `writeFile(filename: "${filename}", content: length ${content ? content.length : 0}, overwrite: ${overwrite}`, "debug"); 18 | 19 | const exists = this.fileExists(filename, true); 20 | if (!overwrite && exists) { 21 | throw new Error(`The file ${filename} already exists. Set overwrite to true if you want to override it`); 22 | } 23 | 24 | if (!exists) { 25 | log("vfs", " creating new file with version 1", "debug"); 26 | this.files[filename] = { 27 | version: 1, 28 | content, 29 | }; 30 | } else if (this.files[filename].content !== content) { 31 | this.files[filename] = { 32 | version: this.files[filename].version + 1, 33 | content, 34 | }; 35 | log("vfs", ` updating file => version ${this.files[filename].version}`, "debug"); 36 | } 37 | } 38 | 39 | /** 40 | * Checks if a file exists in the virtual FS 41 | * @param filename The path of the file to look for 42 | */ 43 | public fileExists(filename: string, suppressLog: boolean = false): boolean { 44 | const ret = filename in this.files; 45 | if (!suppressLog) log("vfs", `fileExists("${filename}") => ${ret}`, "debug"); 46 | return ret; 47 | } 48 | 49 | /** 50 | * Deletes a file in the virtual FS. If the file doesn't exist, nothing happens. 51 | * @param filename The path of the file to look for 52 | */ 53 | public deleteFile(filename: string): void { 54 | log("vfs", `deleteFile("${filename}")`, "debug"); 55 | if (this.fileExists(filename, true)) delete this.files[filename]; 56 | } 57 | 58 | /** 59 | * Reads a file's contents from the virtual FS 60 | * @param filename The path of the file to look for 61 | */ 62 | public readFile(filename: string): string { 63 | if (!this.fileExists(filename, true)) { 64 | throw new Error(`The file ${filename} doesn't exist`); 65 | } 66 | 67 | const ret = this.files[filename].content; 68 | log("vfs", `readFile("${filename}") => length ${ret ? ret.length : 0}`, "debug"); 69 | return ret; 70 | } 71 | 72 | /** 73 | * Returns the revision number of a file in the virtual FS 74 | * @param filename The path of the file to look for 75 | */ 76 | public getFileVersion(filename: string): number { 77 | if (!this.fileExists(filename, true)) { 78 | throw new Error(`The file ${filename} doesn't exist`); 79 | } 80 | const ret = this.files[filename].version; 81 | log("vfs", `getFileVersion("${filename}") => ${ret}`, "debug"); 82 | return ret; 83 | } 84 | 85 | /** 86 | * Returns the file names of all files in the virtual fs 87 | */ 88 | public getFilenames(): string[] { 89 | log("vfs", `getFilenames()`, "debug"); 90 | return Object.keys(this.files); 91 | } 92 | 93 | public getDirectories(root: string): string[] { 94 | log("vfs", `fs.getDirectories(${root})`, "debug"); 95 | let paths = this.getFilenames(); 96 | log("vfs", `fs.getDirectories => paths = ${paths}`, "debug"); 97 | paths = paths.filter(p => p.startsWith(root)); 98 | log("vfs", `fs.getDirectories => paths = ${paths}`, "debug"); 99 | paths = paths.map(p => p.substr(root.length + 1).split("/")[0]); 100 | log("vfs", `fs.getDirectories => paths = ${paths}`, "debug"); 101 | return paths; 102 | } 103 | 104 | private files: {[filename: string]: File} = {}; 105 | 106 | } 107 | -------------------------------------------------------------------------------- /test/ioBroker.d.ts: -------------------------------------------------------------------------------- 1 | import fs = require("fs"); 2 | 3 | // tslint:disable:no-namespace 4 | declare global { 5 | namespace ioBroker { 6 | 7 | interface DictionaryLike { 8 | [id: string]: T; 9 | } 10 | 11 | enum StateQuality { 12 | good = 0x00, // or undefined or null 13 | bad = 0x01, 14 | general_problem = 0x01, 15 | general_device_problem = 0x41, 16 | general_sensor_problem = 0x81, 17 | device_not_connected = 0x42, 18 | sensor_not_connected = 0x82, 19 | device_reports_error = 0x44, 20 | sensor_reports_error = 0x84, 21 | } 22 | 23 | interface State { 24 | /** The value of the state. */ 25 | val: any; 26 | 27 | /** Direction flag: false for desired value and true for actual value. Default: false. */ 28 | ack: boolean; 29 | 30 | /** Unix timestamp. Default: current time */ 31 | ts: number; 32 | 33 | /** Unix timestamp of the last time the value changed */ 34 | lc: number; 35 | 36 | /** Name of the adapter instance which set the value, e.g. "system.adapter.web.0" */ 37 | from: string; 38 | 39 | /** Optional time in seconds after which the state is reset to null */ 40 | expire?: number; 41 | 42 | /** Optional quality of the state value */ 43 | q?: StateQuality; 44 | 45 | /** Optional comment */ 46 | c?: string; 47 | } 48 | 49 | /** Provides low-level access to the ioBroker states DB */ 50 | interface States { 51 | 52 | /** 53 | * Returns a list of states with the given ids 54 | * @param keys IDs of the states to be retrieved 55 | * @param callback Is called when the operation has finished (successfully or not) 56 | * @param dontModify unused 57 | */ 58 | getStates(keys: string[], callback: GetStatesCallback2, dontModify?: any): void; 59 | 60 | /** 61 | * Returns the state with the given id 62 | * @param id ID of the state to be retrieved 63 | * @param callback Is called when the operation has finished (successfully or not) 64 | */ 65 | getState(id: string, callback: GetStateCallback): void; 66 | 67 | /** 68 | * Stores a state in the db 69 | * @param id ID of the state to be stored 70 | * @param state The state to be stored in the db 71 | * @param callback Is called when the operation has finished (successfully or not) 72 | */ 73 | setState(id: string, state?: string | number | boolean | State | Partial, callback?: SetStateCallback): void; 74 | 75 | /** 76 | * Updates a state in memory without triggering a save 77 | * @param id ID of the state to be stored 78 | * @param state The state to be updated 79 | * @param callback Is called when the operation has finished (successfully or not) 80 | */ 81 | setRawState(id: string, state: State, callback?: SetStateCallback): void; 82 | 83 | /** 84 | * Deletes a state 85 | * @param id ID of the state to be stored 86 | * @param callback Is called when the operation has finished (successfully or not) 87 | */ 88 | delState(id: string, callback: DeleteStateCallback): void; 89 | 90 | /** 91 | * Retrieves all ids of states matching @link{pattern} 92 | * @param pattern The pattern to match against 93 | * @param callback Is called when the operation has finished (successfully or not) 94 | * @param dontModify unused 95 | */ 96 | getKeys(pattern: string, callback: GetConfigKeysCallback, dontModify?: any): void; 97 | 98 | /** 99 | * Subscribe to changes of all states matching @link{pattern} 100 | * @param pattern The pattern to match against 101 | * @param callback Is called when the operation has finished (successfully or not) 102 | */ 103 | subscribe(pattern: string, cb: EmptyCallback): void; 104 | /** 105 | * Unsubscribe from changes of all states matching @link{pattern} 106 | * @param pattern The pattern to match against 107 | * @param callback Is called when the operation has finished (successfully or not) 108 | */ 109 | unsubscribe(pattern: string, cb: EmptyCallback): void; 110 | 111 | /** 112 | * Register an adapter instance as subscribable. 113 | * This means that the instance can read information about all subscriptions to its states 114 | * and will be notified of changes. 115 | * @param instance Adapter instance to register, e.g. "admin.0" 116 | * @param cb Is called when the operation has finished (successfully or not) 117 | */ 118 | registerAdapterSubs(instance: string, cb?: (error: null, success: boolean) => void): void; 119 | 120 | /** 121 | * Unregister an adapter instance as subscribable. 122 | * @param instance Adapter instance to unregister, e.g. "admin.0" 123 | * @param cb Is called when the operation has finished (successfully or not) 124 | */ 125 | unregisterAdapterSubs(instance: string, cb?: (error: null, success: boolean) => void): void; 126 | 127 | /** 128 | * EDUCATED GUESS: Notify all clients about changes to an object 129 | * @param type object type 130 | * @param id State/object id 131 | * @param obj The changed object 132 | */ 133 | publishAll(type: string, id: string, obj: Message): void; 134 | 135 | // TODO: Documentation for these functions is missing 136 | pushMessage(id: string, state: Message, callback: SetStateCallback): void; 137 | lenMessage(id: string, callback: GenericCallback): void; 138 | getMessage(id: string, callback: GenericCallback): void; 139 | delMessage(id: string, messageId: number, callback: ErrorCallback): void; 140 | clearAllMessages(callback?: EmptyCallback): void; 141 | subscribeMessage(id: string, cb: EmptyCallback): void; 142 | unsubscribeMessage(id: string, cb: EmptyCallback): void; 143 | 144 | pushLog(id: string, log: Log, callback: SetStateCallback): void; 145 | lenLog(id: string, callback: GenericCallback): void; 146 | getLog(id: string, callback: GenericCallback): void; 147 | delLog(id: string, logId: string, callback: ErrorCallback): void; 148 | clearAllLogs(callback?: EmptyCallback): void; 149 | subscribeLog(id: string, cb: EmptyCallback): void; 150 | unsubscribeLog(id: string, cb: EmptyCallback): void; 151 | 152 | getSession(id: string, callback: GetSessionCallback): void; 153 | setSession(id: string, expire: number, callback?: EmptyCallback): void; 154 | setSession(id: string, expire: number, obj: Session, callback?: EmptyCallback): void; 155 | destroySession(id: string, callback?: EmptyCallback): void; 156 | 157 | /** 158 | * Retrieves a copy of the object with the given ID 159 | * @param id Id of the object to find 160 | * @param callback Is called when the operation has finished (successfully or not) 161 | */ 162 | getConfig(id: string, callback: GetObjectCallback): void; 163 | 164 | /** 165 | * Returns a list of config keys matching 166 | * @param pattern Pattern to match against 167 | * @param callback Is called when the operation has finished (successfully or not) 168 | * @param dontModify unused 169 | */ 170 | getConfigKeys(pattern: string, callback: GetConfigKeysCallback, dontModify?: any): void; 171 | 172 | /** 173 | * Returns a list of objects with the given ids 174 | * @param keys IDs of the objects to be retrieved 175 | * @param callback Is called when the operation has finished (successfully or not) 176 | * @param dontModify unused 177 | */ 178 | getConfigs(keys: string[], callback: GetObjectsCallback2, dontModify?: any): void; 179 | 180 | /** 181 | * Creates or overwrites a config object in the object db 182 | * @param id ID of the object 183 | * @param obj Object to store 184 | * @param callback Is called when the operation has finished (successfully or not) 185 | */ 186 | setConfig(id: string, obj: ioBroker.Object, callback: SetObjectCallback): void; 187 | 188 | /** 189 | * Deletes a config object in the object db 190 | * @param id ID of the object 191 | * @param callback Is called when the operation has finished (successfully or not) 192 | */ 193 | delConfig(id: string, callback: ErrorCallback): void; 194 | 195 | /** 196 | * Subscribe to config object changes 197 | * @param pattern The pattern to match against 198 | */ 199 | subscribeConfig(pattern: string, callback: EmptyCallback): void; 200 | 201 | /** 202 | * Unsubscribe from config object changes 203 | * @param pattern The pattern to match against 204 | */ 205 | unsubscribeConfig(pattern: string, callback: EmptyCallback): void; 206 | 207 | /** 208 | * Writes a binary state into Redis 209 | * @param id The id of the state 210 | * @param data The data to be written 211 | * @param callback Is called when the operation has finished (successfully or not) 212 | */ 213 | setBinaryState(id: string, data: Buffer, callback: SetStateCallback): void; 214 | 215 | /** 216 | * Reads a binary state from Redis 217 | * @param id The id of the state 218 | * @param callback Is called when the operation has finished (successfully or not) 219 | */ 220 | getBinaryState(id: string, callback: GetBinaryStateCallback): void; 221 | 222 | /** 223 | * Deletes a binary state from Redis 224 | * @param id The id of the state to be deleted 225 | * @param callback Is called when the operation has finished (successfully or not) 226 | */ 227 | delBinaryState(id: string, callback: DeleteStateCallback): void; 228 | 229 | /** Destructor of the class. Call this before shutting down */ 230 | destroy(): void; 231 | 232 | } // end interface States 233 | 234 | type Session = any; // TODO: implement 235 | 236 | type ObjectType = "state" | "channel" | "device"; 237 | type CommonType = "number" | "string" | "boolean" | "array" | "object" | "mixed" | "file"; 238 | 239 | // Maybe this should extend DictionaryLike, 240 | // but the extra properties aren't defined anywhere, 241 | // so I'd rather force the user to explicitly state 242 | // he knows what he's doing by casting to any 243 | interface ObjectCommon { 244 | /** name of this object */ 245 | name: string; 246 | 247 | // Icon and role aren't defined in SCHEMA.md, 248 | // but they are being used by some adapters 249 | /** Icon for this object */ 250 | icon?: string; 251 | /** role of the object */ 252 | role?: string; 253 | } 254 | 255 | interface StateCommon extends ObjectCommon { 256 | /** Type of this state. See https://github.com/ioBroker/ioBroker/blob/master/doc/SCHEMA.md#state-commonrole for a detailed description */ 257 | type?: CommonType; 258 | /** minimum value */ 259 | min?: number; 260 | /** maximum value */ 261 | max?: number; 262 | /** unit of the value */ 263 | unit?: string; 264 | /** the default value */ 265 | def?: any; 266 | /** description of this state */ 267 | desc?: string; 268 | 269 | /** if this state is readable */ 270 | read: boolean; 271 | /** if this state is writable */ 272 | write: boolean; 273 | /** role of the state (used in user interfaces to indicate which widget to choose) */ 274 | role: string; 275 | 276 | /** 277 | * Dictionary of possible values for this state in the form 278 | *
 279 | 			 * {
 280 | 			 *     "internal value 1": "displayed value 1",
 281 | 			 *     "internal value 2": "displayed value 2",
 282 | 			 *     ...
 283 | 			 * }
 284 | 			 * 
285 | * In old ioBroker versions, this could also be a string of the form 286 | * "val1:text1;val2:text2" (now deprecated) 287 | */ 288 | states?: DictionaryLike | string; 289 | 290 | /** ID of a helper state indicating if the handler of this state is working */ 291 | workingID?: string; 292 | 293 | /** attached history information */ 294 | history?: any; 295 | } 296 | interface ChannelCommon extends ObjectCommon { 297 | /** description of this channel */ 298 | desc?: string; 299 | } 300 | type OtherCommon = ObjectCommon & { 301 | [propName: string]: any; 302 | }; 303 | 304 | interface BaseObject { 305 | /** The ID of this object */ 306 | _id?: string; 307 | native: DictionaryLike; 308 | enums?: DictionaryLike; 309 | type: string; // specified in the derived interfaces 310 | common: ObjectCommon; 311 | acl?: ObjectACL; 312 | } 313 | 314 | interface StateObject extends BaseObject { 315 | type: "state"; 316 | common: StateCommon; 317 | acl?: StateACL; 318 | } 319 | interface PartialStateObject extends Partial> { 320 | common?: Partial; 321 | acl?: Partial; 322 | } 323 | 324 | interface ChannelObject extends BaseObject { 325 | type: "channel"; 326 | common: ChannelCommon; 327 | } 328 | interface PartialChannelObject extends Partial> { 329 | common?: Partial; 330 | } 331 | 332 | interface DeviceObject extends BaseObject { 333 | type: "device"; 334 | common: ObjectCommon; // TODO: any definition for device? 335 | } 336 | interface PartialDeviceObject extends Partial> { 337 | common?: Partial; 338 | } 339 | 340 | interface OtherObject extends BaseObject { 341 | type: "adapter" | "config" | "enum" | "group" | "host" | "info" | "instance" | "meta" | "script" | "user"; 342 | common: OtherCommon; 343 | } 344 | interface PartialOtherObject extends Partial> { 345 | common?: Partial; 346 | } 347 | 348 | type Object = StateObject | ChannelObject | DeviceObject | OtherObject; 349 | type PartialObject = PartialStateObject | PartialChannelObject | PartialDeviceObject | PartialOtherObject; 350 | 351 | /** Defines access rights for a single file */ 352 | interface FileACL { 353 | /** Full name of the user who owns this file, e.g. "system.user.admin" */ 354 | owner: string; 355 | /** Full name of the group who owns this file, e.g. "system.group.administrator" */ 356 | ownerGroup: string; 357 | /** Linux-type permissions defining access to this file */ 358 | permissions: number; 359 | } 360 | /** Defines access rights for a single file, applied to a user or group */ 361 | interface EvaluatedFileACL extends FileACL { 362 | /** Whether the user may read the file */ 363 | read: boolean; 364 | /** Whether the user may write the file */ 365 | write: boolean; 366 | } 367 | 368 | /** Defines access rights for a single object */ 369 | interface ObjectACL { 370 | /** Full name of the user who owns this object, e.g. "system.user.admin" */ 371 | owner: string; 372 | /** Full name of the group who owns this object, e.g. "system.group.administrator" */ 373 | ownerGroup: string; 374 | /** Linux-type permissions defining access to this object */ 375 | object: number; 376 | } 377 | /** Defines access rights for a single state object */ 378 | interface StateACL extends ObjectACL { 379 | /** Linux-type permissions defining access to this state */ 380 | state: number; 381 | } 382 | 383 | /** Defines access rights for a single object type */ 384 | interface ObjectOperationPermissions { 385 | /** Whether a user may enumerate objects of this type */ 386 | list: boolean; 387 | /** Whether a user may read objects of this type */ 388 | read: boolean; 389 | /** Whether a user may write objects of this type */ 390 | write: boolean; 391 | /** Whether a user may create objects of this type */ 392 | create: boolean; 393 | /** Whether a user may delete objects of this type */ 394 | "delete": boolean; 395 | } 396 | 397 | /** Defines the rights a user or group has to change objects */ 398 | interface ObjectPermissions { 399 | /** The access rights for files */ 400 | file: ObjectOperationPermissions; 401 | /** The access rights for objects */ 402 | object: ObjectOperationPermissions; 403 | /** The access rights for users/groups */ 404 | users: ObjectOperationPermissions; 405 | /** The access rights for states */ 406 | state?: ObjectOperationPermissions; 407 | } 408 | /** Defined the complete set of access rights a user has */ 409 | interface PermissionSet extends ObjectPermissions { 410 | /** The name of the user this ACL is for */ 411 | user: string; 412 | /** The name of the groups this ACL was merged from */ 413 | groups: string[]; 414 | /** The access rights for certain commands */ 415 | other: { 416 | execute: boolean, 417 | http: boolean, 418 | sendto: boolean, 419 | }; 420 | } 421 | 422 | interface Permission { 423 | /** The type of the permission */ 424 | type: string; 425 | /** Which kind of operation is required */ 426 | operation: string; 427 | } 428 | interface ObjectOrStatePermission extends Permission { 429 | type: "object" | "file" | "users" | "state"; 430 | operation: "list" | "read" | "write" | "create" | "delete"; 431 | } 432 | interface OtherPermission extends Permission { 433 | type: "other"; 434 | operation: "execute" | "http" | "sendto"; 435 | } 436 | interface CommandsPermissions { 437 | // TODO: Are all properties required or is a partial object ok? 438 | getObject: ObjectOrStatePermission; 439 | getObjects: ObjectOrStatePermission; 440 | getObjectView: ObjectOrStatePermission; 441 | setObject: ObjectOrStatePermission; 442 | subscribeObjects: ObjectOrStatePermission; 443 | unsubscribeObjects: ObjectOrStatePermission; 444 | getStates: ObjectOrStatePermission; 445 | getState: ObjectOrStatePermission; 446 | setState: ObjectOrStatePermission; 447 | getStateHistory: ObjectOrStatePermission; 448 | subscribe: ObjectOrStatePermission; 449 | unsubscribe: ObjectOrStatePermission; 450 | getVersion: Permission; 451 | httpGet: OtherPermission; 452 | sendTo: OtherPermission; 453 | sendToHost: OtherPermission; 454 | readFile: ObjectOrStatePermission; 455 | readFile64: ObjectOrStatePermission; 456 | writeFile: ObjectOrStatePermission; 457 | writeFile64: ObjectOrStatePermission; 458 | unlink: ObjectOrStatePermission; 459 | rename: ObjectOrStatePermission; 460 | mkdir: ObjectOrStatePermission; 461 | readDir: ObjectOrStatePermission; 462 | chmodFile: ObjectOrStatePermission; 463 | authEnabled: Permission; 464 | disconnect: Permission; 465 | listPermissions: Permission; 466 | getUserPermissions: ObjectOrStatePermission; 467 | } 468 | 469 | type UserGroup = any; // TODO find out how this looks like 470 | // interface UserGroup { } 471 | 472 | /** Contains information about a user */ 473 | interface User { 474 | /** Which groups this user belongs to */ 475 | groups: UserGroup[]; 476 | /** Access rights of this user */ 477 | acl: ObjectPermissions; 478 | } 479 | 480 | /** Parameters for @link{Objects.getObjectView} */ 481 | interface GetObjectViewParams { 482 | /** First id to include in the return list */ 483 | startkey: string; 484 | /** Last id to include in the return list */ 485 | endkey: string; 486 | } 487 | 488 | /** Parameters for @link{Objects.getObjectList} */ 489 | interface GetObjectListParams extends GetObjectViewParams { 490 | /** Whether docs should be included in the return list */ // TODO: What are docs? 491 | include_docs: boolean; 492 | } 493 | 494 | /** Provides low-level access to the ioBroker objects db */ 495 | interface Objects { 496 | /** 497 | * For a given user, returns the groups he belongs to, and his access rights 498 | * @param user Name of the user. Has to start with "system.user." 499 | * @param callback The callback function to be invoked with the return values 500 | */ 501 | getUserGroup(user: string, callback: GetUserGroupCallback): void; 502 | 503 | /** 504 | * Determines the mime type for a given file extension 505 | * @param ext File extension, including the leading dot, e.g. ".zip" 506 | */ 507 | getMimeType(ext: string): {mimeType: string, isBinary: boolean}; 508 | 509 | /** 510 | * Writes a file. 511 | * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" 512 | * @param name File name 513 | * @param data Contents of the file 514 | * @param options (optional) MIME type of the file (string). Or some internal options. 515 | * @param callback Is called when the operation has finished (successfully or not) 516 | */ 517 | writeFile(id: string, name: string, data: Buffer | string, callback: ErrorCallback): void; 518 | writeFile(id: string, name: string, data: Buffer | string, options: string | any, callback: ErrorCallback): void; 519 | 520 | /** 521 | * Reads a file. 522 | * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" 523 | * @param name File name 524 | * @param options (optional) Some internal options. 525 | * @param callback Is called when the operation has finished (successfully or not) 526 | */ 527 | readFile(id: string, name: string, callback: ReadFileCallback): void; 528 | readFile(id: string, name: string, options: any, callback: ReadFileCallback): void; 529 | 530 | /** 531 | * Deletes a file. 532 | * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" 533 | * @param name File name 534 | * @param options (optional) Some internal options. 535 | * @param callback Is called when the operation has finished (successfully or not) 536 | */ 537 | unlink(id: string, name: string, callback: ErrorCallback): void; 538 | unlink(id: string, name: string, options: any, callback: ErrorCallback): void; 539 | /** 540 | * Deletes a file. 541 | * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" 542 | * @param name File name 543 | * @param options (optional) Some internal options. 544 | * @param callback Is called when the operation has finished (successfully or not) 545 | */ 546 | delFile(id: string, name: string, callback: ErrorCallback): void; 547 | delFile(id: string, name: string, options: any, callback: ErrorCallback): void; 548 | 549 | /** 550 | * Finds all files and directories starting with 551 | * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" 552 | * @param name File or directory name 553 | * @param options (optional) Some internal options. 554 | * @param callback Is called when the operation has finished (successfully or not) 555 | */ 556 | readDir(id: string, name: string, callback: ReadDirCallback): void; 557 | readDir(id: string, name: string, options: any, callback: ReadDirCallback): void; 558 | 559 | /** 560 | * Renames a file or directory 561 | * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" 562 | * @param oldName Old file or directory name 563 | * @param newName Name to rename to 564 | * @param options (optional) Some internal options. 565 | * @param callback Is called when the operation has finished (successfully or not) 566 | */ 567 | rename(id: string, oldName: string, newName: string, callback: ErrorCallback): void; 568 | rename(id: string, oldName: string, newName: string, options: any, callback: ErrorCallback): void; 569 | 570 | /** 571 | * Creates an empty file with the given name 572 | * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" 573 | * @param name File name 574 | * @param options (optional) Some internal options. 575 | * @param callback Is called when the operation has finished (successfully or not) 576 | */ 577 | touch(id: string, name: string, callback: ErrorCallback): void; 578 | touch(id: string, name: string, options: any, callback: ErrorCallback): void; 579 | 580 | /** 581 | * Deletes all files in the root directory matching 582 | * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" 583 | * @param name Pattern to match against 584 | * @param options (optional) Some internal options. 585 | * @param callback Is called when the operation has finished (successfully or not) 586 | */ 587 | rm(id: string, name: string, callback: RmCallback): void; 588 | rm(id: string, name: string, options: any, callback: RmCallback): void; 589 | 590 | /** 591 | * Creates an empty directory with the given name 592 | * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" 593 | * @param name Directory name 594 | * @param options (optional) Some internal options. 595 | * @param callback Is called when the operation has finished (successfully or not) 596 | */ 597 | mkDir(id: string, name: string, callback: ErrorCallback): void; 598 | mkDir(id: string, name: string, options: any, callback: ErrorCallback): void; 599 | 600 | /** 601 | * Takes possession all files in the root directory matching 602 | * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" 603 | * @param name Pattern to match against 604 | * @param options (optional) Some internal options. 605 | * @param callback Is called when the operation has finished (successfully or not) 606 | */ 607 | chownFile(id: string, name: string, callback: ChownFileCallback): void; 608 | chownFile(id: string, name: string, options: any, callback: ChownFileCallback): void; 609 | 610 | /** 611 | * Changes access rights of all files in the root directory matching 612 | * @param id Name of the root directory. This should be the adapter instance, e.g. "admin.0" 613 | * @param name Pattern to match against 614 | * @param options Mode of the access change as a number or hexadecimal string 615 | * @param callback Is called when the operation has finished (successfully or not) 616 | */ 617 | chmodFile(id: string, name: string, options: {mode: number | string} | DictionaryLike, callback: ChownFileCallback): void; 618 | 619 | // not documented. enabled = true seems to disable the cache 620 | // enableFileCache(enabled, options, callback) 621 | 622 | /** 623 | * Subscribe to object changes 624 | * @param pattern The pattern to match against 625 | */ 626 | subscribeConfig(pattern: string, callback: EmptyCallback): void; 627 | subscribeConfig(pattern: string, options: any, callback: EmptyCallback): void; 628 | /** 629 | * Subscribe to object changes 630 | * @param pattern The pattern to match against 631 | */ 632 | subscribe(pattern: string, callback: EmptyCallback): void; 633 | subscribe(pattern: string, options: any, callback: EmptyCallback): void; 634 | 635 | /** 636 | * Unsubscribe from object changes 637 | * @param pattern The pattern to match against 638 | */ 639 | unsubscribeConfig(pattern: string, callback: EmptyCallback): void; 640 | unsubscribeConfig(pattern: string, options: any, callback: EmptyCallback): void; 641 | /** 642 | * Unsubscribe from object changes 643 | * @param pattern The pattern to match against 644 | */ 645 | unsubscribe(pattern: string, callback: EmptyCallback): void; 646 | unsubscribe(pattern: string, options: any, callback: EmptyCallback): void; 647 | 648 | /** 649 | * Takes possession of all objects matching 650 | * @param pattern Pattern to match against 651 | * @param options (optional) Some internal options. 652 | * @param callback Is called when the operation has finished (successfully or not) 653 | */ 654 | chownObject(pattern: string, callback: ChownObjectCallback): void; 655 | chownObject(pattern: string, options: any, callback: ChownObjectCallback): void; 656 | 657 | /** 658 | * Changes access rights of all objects matching 659 | * @param pattern Pattern to match against 660 | * @param options Mode of the access change as a number or hexadecimal string 661 | * @param callback Is called when the operation has finished (successfully or not) 662 | */ 663 | chmodObject(pattern: string, callback: ChownObjectCallback): void; 664 | chmodObject(pattern: string, options: any, callback: ChownObjectCallback): void; 665 | 666 | /** 667 | * Retrieves a copy of the object with the given ID 668 | * @param id Id of the object to find 669 | * @param options (optional) Some internal options. 670 | * @param callback Is called when the operation has finished (successfully or not) 671 | */ 672 | getObject(id: string, callback: GetObjectCallback): void; 673 | getObject(id: string, options: any, callback: GetObjectCallback): void; 674 | /** 675 | * Retrieves a copy of the object with the given ID 676 | * @param id Id of the object to find 677 | * @param options (optional) Some internal options. 678 | * @param callback Is called when the operation has finished (successfully or not) 679 | */ 680 | getConfig(id: string, callback: GetObjectCallback): void; 681 | getConfig(id: string, options: any, callback: GetObjectCallback): void; 682 | 683 | /** 684 | * Returns a list of config keys matching 685 | * @param pattern Pattern to match against 686 | * @param options (optional) Some internal options. 687 | * @param callback Is called when the operation has finished (successfully or not) 688 | * @param dontModify unused 689 | */ 690 | getConfigKeys(pattern: string, callback: GetConfigKeysCallback, dontModify?: any): void; 691 | getConfigKeys(pattern: string, options: any, callback: GetConfigKeysCallback, dontModify?: any): void; 692 | 693 | /** 694 | * Returns a list of objects with the given ids 695 | * @param keys IDs of the objects to be retrieved 696 | * @param options (optional) Some internal options. 697 | * @param callback Is called when the operation has finished (successfully or not) 698 | * @param dontModify unused 699 | */ 700 | getObjects(keys: string[], callback: GetObjectsCallback2, dontModify?: any): void; 701 | getObjects(keys: string[], options: any, callback: GetObjectsCallback2, dontModify?: any): void; 702 | /** 703 | * Returns a list of objects with the given ids 704 | * @param keys IDs of the objects to be retrieved 705 | * @param options (optional) Some internal options. 706 | * @param callback Is called when the operation has finished (successfully or not) 707 | * @param dontModify unused 708 | */ 709 | getConfigs(keys: string[], callback: GetObjectsCallback2, dontModify?: any): void; 710 | getConfigs(keys: string[], options: any, callback: GetObjectsCallback2, dontModify?: any): void; 711 | 712 | /** 713 | * Creates or overwrites an object in the object db 714 | * @param id ID of the object 715 | * @param obj Object to store 716 | * @param options (optional) Some internal options. 717 | * @param callback Is called when the operation has finished (successfully or not) 718 | */ 719 | setObject(id: string, obj: ioBroker.Object, callback: SetObjectCallback): void; 720 | setObject(id: string, obj: ioBroker.Object, options: any, callback: SetObjectCallback): void; 721 | /** 722 | * Creates or overwrites an object in the object db 723 | * @param id ID of the object 724 | * @param obj Object to store 725 | * @param options (optional) Some internal options. 726 | * @param callback Is called when the operation has finished (successfully or not) 727 | */ 728 | setConfig(id: string, obj: ioBroker.Object, callback: SetObjectCallback): void; 729 | setConfig(id: string, obj: ioBroker.Object, options: any, callback: SetObjectCallback): void; 730 | 731 | /** 732 | * Deletes an object in the object db 733 | * @param id ID of the object 734 | * @param options (optional) Some internal options. 735 | * @param callback Is called when the operation has finished (successfully or not) 736 | */ 737 | delObject(id: string, callback: ErrorCallback): void; 738 | delObject(id: string, options: any, callback: ErrorCallback): void; 739 | /** 740 | * Deletes an object in the object db 741 | * @param id ID of the object 742 | * @param options (optional) Some internal options. 743 | * @param callback Is called when the operation has finished (successfully or not) 744 | */ 745 | delConfig(id: string, callback: ErrorCallback): void; 746 | delConfig(id: string, options: any, callback: ErrorCallback): void; 747 | 748 | /** 749 | * Returns a list of objects with id between params.startkey and params.endkey 750 | * @param params Parameters determining the objects included in the return list. Null to include all objects 751 | * @param options (optional) If the returned list should be sorted. And some internal options. 752 | * @param callback Is called when the operation has finished (successfully or not) 753 | */ 754 | getObjectList(params: GetObjectListParams | null, callback: GetObjectListCallback): void; 755 | getObjectList(params: GetObjectListParams | null, options: { sorted?: boolean } | DictionaryLike, callback: GetObjectListCallback): void; 756 | 757 | /** 758 | * Query a predefined object view (similar to SQL stored procedures) and return the results 759 | * For a detailed description refer to https://github.com/ioBroker/ioBroker/wiki/Adapter-Development-Documentation#object-fields 760 | * or http://guide.couchdb.org/editions/1/en/views.html 761 | * @param design The namespace of the object view, as defined in io-package.json. Usually the adapter name, e.g. "hm-rpc" 762 | * @param search The name of the object view. 763 | * @param params Parameters to additionally filter out objects from the return list. Null to include all objects 764 | * @param options (optional) Some internal options. 765 | * @param callback Is called when the operation has finished (successfully or not) 766 | */ 767 | getObjectView(design: string, search: string, params: GetObjectViewParams | null, callback: GetObjectViewCallback): void; 768 | getObjectView(design: string, search: string, params: GetObjectViewParams | null, options: any, callback: GetObjectViewCallback): void; 769 | 770 | /** 771 | * Extends an object in the object db with new properties 772 | * @param id ID of the object 773 | * @param obj Object to extend the original one with. May be just parts of an object. 774 | * @param options (optional) Some internal options. 775 | * @param callback Is called when the operation has finished (successfully or not) 776 | */ 777 | extendObject(id: string, obj: PartialObject, callback: ExtendObjectCallback): void; 778 | extendObject(id: string, obj: PartialObject, options: any, callback: ExtendObjectCallback): void; 779 | 780 | /** 781 | * Finds an object by ID or name. If multiple objects were found, return the first one 782 | * @param idOrName ID or name of the object 783 | * @param type If != null, only return an object with a common.type equal to this 784 | * @param options (optional) Some internal options. 785 | * @param callback Is called when the operation has finished (successfully or not) 786 | */ 787 | findObject(idOrName: string, type: CommonType | null, callback: FindObjectCallback): void; 788 | findObject(idOrName: string, type: CommonType | null, options: any, callback: FindObjectCallback): void; 789 | 790 | // I'd rather not document a function with the name "destroyDB" 791 | 792 | /** Destructor of the class. Call this before shutting down. */ 793 | destroy(): void; 794 | 795 | } // end interface Objects 796 | 797 | interface Logger { 798 | /** log message with silly level */ 799 | silly(message: string): void; 800 | /** log message with debug level */ 801 | debug(message: string): void; 802 | /** log message with info level (default output level for all adapters) */ 803 | info(message: string): void; 804 | /** log message with warning severity */ 805 | warn(message: string): void; 806 | /** log message with error severity */ 807 | error(message: string): void; 808 | 809 | /** Verbosity of the log output */ 810 | level: "silly" | "debug" | "info" | "warn" | "error"; 811 | } 812 | 813 | interface Certificates { 814 | /** private key file */ 815 | key: string | Buffer; 816 | /** public certificate */ 817 | cert: string | Buffer; 818 | /** chained CA certificates */ 819 | ca: (string | Buffer)[]; 820 | } 821 | 822 | /** Callback information for a passed message */ 823 | interface MessageCallbackInfo { 824 | /** The original message payload */ 825 | message: string | object; 826 | /** ID of this callback */ 827 | id: number; 828 | // ??? 829 | ack: boolean; 830 | /** Timestamp of this message */ 831 | time: number; 832 | } 833 | type MessageCallback = (result?: any) => void; 834 | 835 | /** A message being passed between adapter instances */ 836 | interface Message { 837 | /** The command to be executed */ 838 | command: string; 839 | /** The message payload */ 840 | message: string | object; 841 | /** The source of this message */ 842 | from: string; 843 | /** ID of this message */ 844 | _id: number; 845 | /** Callback information. This is set when the source expects a response */ 846 | callback: MessageCallbackInfo; 847 | } 848 | 849 | type Log = any; // TODO: define this https://github.com/ioBroker/ioBroker.js-controller/blob/master/lib/states/statesInMemServer.js#L873 850 | 851 | type EnumList = string | string[]; 852 | 853 | type Enum = any; // TODO: implement this 854 | 855 | interface DirectoryEntry { 856 | file: string; 857 | stats: fs.Stats; 858 | isDir: boolean; 859 | acl: any; // access control list object 860 | modifiedAt: number; 861 | createdAt: number; 862 | } 863 | 864 | interface GetHistoryOptions { 865 | instance?: string; 866 | start?: number; 867 | end?: number; 868 | step?: number; 869 | count?: number; 870 | from?: boolean; 871 | ack?: boolean; 872 | q?: boolean; 873 | addID?: boolean; 874 | limit?: number; 875 | ignoreNull: boolean; 876 | sessionId?: any; 877 | aggregate?: "minmax" | "min" | "max" | "average" | "total" | "count" | "none"; 878 | } 879 | 880 | interface AdapterOptions { 881 | /** The name of the adapter */ 882 | name: string; 883 | 884 | /** path to adapter */ 885 | dirname?: string; 886 | 887 | /** if the global system config should be included in the created object. Default: false */ 888 | systemConfig?: boolean; 889 | 890 | /** provide alternative global configuration for the adapter. Default: null */ 891 | config?: any; 892 | 893 | /** instance of the created adapter. Default: null */ 894 | instance?: number; 895 | 896 | /** If the adapter needs access to the formatDate function to format dates according to the global settings. Default: false */ 897 | useFormatDate?: boolean; 898 | 899 | /** If the adapter collects logs from all adapters (experts only). Default: false */ 900 | logTransporter?: boolean; 901 | 902 | /** Handler for changes of subscribed objects */ 903 | objectChange?: ObjectChangeHandler; 904 | /** Handler for received adapter messages. Can only be used if messagebox in io-package.json is set to true. */ 905 | message?: MessageHandler; 906 | /** Handler for changes of subscribed states */ 907 | stateChange?: StateChangeHandler; 908 | /** Will be called when the adapter is intialized */ 909 | ready?: () => void; 910 | /** Will be called on adapter termination */ 911 | unload?: (callback: EmptyCallback) => void; 912 | 913 | /** if true, stateChange will be called with an id that has no namespace, e.g. "state" instead of "adapter.0.state". Default: false */ 914 | noNamespace?: boolean; 915 | } // end interface AdapterOptions 916 | 917 | interface Adapter { 918 | /** The name of the adapter */ 919 | name: string; 920 | /** The name of the host where the adapter is running */ 921 | host: string; 922 | /** instance number of this adapter instance */ 923 | instance: number; 924 | /** Namespace of adapter objects: "." */ 925 | readonly namespace: string; 926 | /** native part of the adapter settings */ 927 | config: any; 928 | /** common part of the adapter settings */ 929 | common: any; 930 | /** system part of the adapter settings */ 931 | systemConfig?: any; 932 | /** path to the adapter folder */ 933 | adapterDir: string; 934 | /** content of io-package.json */ 935 | ioPack: any; 936 | /** content of package.json */ 937 | pack: any; 938 | /** access to the logging functions */ 939 | log: Logger; 940 | /** adapter version */ 941 | version: any; 942 | states: States; 943 | objects: Objects; 944 | /** if the adapter is connected to the host */ 945 | connected: boolean; 946 | 947 | /* =============================== 948 | Functions defined in adapter.js 949 | =============================== */ 950 | 951 | /** 952 | * Helper function that looks for first free TCP port starting with the given one. 953 | * @param port - The port to start with 954 | * @param callback - gets called when a free port is found 955 | */ 956 | getPort(port: number, callback: (port: number) => void): void; 957 | 958 | /** Stops the adapter. Note: Is not always defined. */ 959 | stop?: () => void; 960 | 961 | // ============================== 962 | // GENERAL 963 | 964 | /** Validates username and password */ 965 | checkPassword(user: string, password: string, callback: (result: boolean) => void): void; 966 | checkPassword(user: string, password: string, options: any, callback: (result: boolean) => void): void; 967 | /** Sets a new password for the given user */ 968 | setPassword(user: string, password: string, options?: any, callback?: (err?: any) => void): void; 969 | /** Checks if a user exists and is in the given group. */ 970 | checkGroup(user: string, group: string, callback: (result: boolean) => void): void; 971 | checkGroup(user: string, group: string, options: any, callback: (result: boolean) => void): void; 972 | /** Determines the users permissions */ 973 | calculatePermissions(user: string, commandsPermissions: CommandsPermissions, callback: (result: PermissionSet) => void): void; 974 | calculatePermissions(user: string, commandsPermissions: CommandsPermissions, options: any, callback: (result: PermissionSet) => void): void; 975 | /** Returns SSL certificates by name (private key, public cert and chained certificate) for creation of HTTPS servers */ 976 | getCertificates(publicName: string, privateName: string, chainedName: string, callback: (err: string | null, certs?: Certificates, useLetsEncryptCert?: boolean) => void): void; 977 | 978 | /** 979 | * Sends a message to a specific instance or all instances of some specific adapter. 980 | * @param instanceName The instance to send this message to. 981 | * If the ID of an instance is given (e.g. "admin.0"), only this instance will receive the message. 982 | * If the name of an adapter is given (e.g. "admin"), all instances of this adapter will receive it. 983 | * @param command (optional) Command name of the target instance. Default: "send" 984 | * @param message The message (e.g. params) to send. 985 | */ 986 | sendTo(instanceName: string, message: string | object, callback?: MessageCallback | MessageCallbackInfo): void; 987 | sendTo(instanceName: string, command: string, message: string | object, callback?: MessageCallback | MessageCallbackInfo): void; 988 | 989 | /** 990 | * Sends a message to a specific host or all hosts. 991 | */ 992 | sendToHost(hostName: string, message: string | object, callback?: MessageCallback | MessageCallbackInfo): void; 993 | sendToHost(hostName: string, command: string, message: string | object, callback?: MessageCallback | MessageCallbackInfo): void; 994 | 995 | /** Convert ID to {device: D, channel: C, state: S} */ 996 | idToDCS(id: string): { 997 | device: string; 998 | channel: string; 999 | state: string; 1000 | }; 1001 | 1002 | // ============================== 1003 | // own objects 1004 | 1005 | /** Reads an object from the object db */ 1006 | getObject(id: string, callback: GetObjectCallback): void; 1007 | getObject(id: string, options: any, callback: GetObjectCallback): void; 1008 | /** Creates or overwrites an object in the object db */ 1009 | setObject(id: string, obj: ioBroker.Object, options?: any, callback?: SetObjectCallback): void; 1010 | /** Creates an object in the object db. Existing objects are not overwritten. */ 1011 | setObjectNotExists(id: string, obj: ioBroker.Object, options?: any, callback?: SetObjectCallback): void; 1012 | /** Get all states, channels and devices of this adapter */ 1013 | getAdapterObjects(callback: (objects: DictionaryLike) => void): void; 1014 | /** Extend an object and create it if it might not exist */ 1015 | extendObject(id: string, objPart: PartialObject, options?: any, callback?: SetObjectCallback): void; 1016 | /** 1017 | * Deletes an object from the object db 1018 | * @param id - The id of the object without namespace 1019 | */ 1020 | delObject(id: string, options?: any, callback?: ErrorCallback): void; 1021 | 1022 | // ============================== 1023 | // foreign objects 1024 | 1025 | // tslint:disable:unified-signatures 1026 | /** Reads an object (which might not belong to this adapter) from the object db */ 1027 | getForeignObject(id: string, callback: GetObjectCallback): void; 1028 | getForeignObject(id: string, options: any, callback: GetObjectCallback): void; 1029 | /** Get foreign objects by pattern, by specific type and resolve their enums. */ 1030 | getForeignObjects(pattern: string, callback: GetObjectsCallback): void; 1031 | getForeignObjects(pattern: string, options: any, callback: GetObjectsCallback): void; 1032 | getForeignObjects(pattern: string, type: ObjectType, callback: GetObjectsCallback): void; 1033 | getForeignObjects(pattern: string, type: ObjectType, enums: EnumList, callback: GetObjectsCallback): void; 1034 | getForeignObjects(pattern: string, type: ObjectType, options: any, callback: GetObjectsCallback): void; 1035 | getForeignObjects(pattern: string, type: ObjectType, enums: EnumList, options: any, callback: GetObjectsCallback): void; 1036 | /** Creates or overwrites an object (which might not belong to this adapter) in the object db */ 1037 | setForeignObject(id: string, obj: ioBroker.Object, options?: any, callback?: SetObjectCallback): void; 1038 | /** Creates an object (which might not belong to this adapter) in the object db. Existing objects are not overwritten. */ 1039 | setForeignObjectNotExists(id: string, obj: ioBroker.Object, options?: any, callback?: SetObjectCallback): void; 1040 | /** Extend an object (which might not belong to this adapter) and create it if it might not exist */ 1041 | extendForeignObject(id: string, objPart: PartialObject, options?: any, callback?: SetObjectCallback): void; 1042 | // tslint:enable:unified-signatures 1043 | /** 1044 | * Finds an object by its ID or name 1045 | * @param type - common.type of the state 1046 | */ 1047 | findForeignObject(idOrName: string, type: string, callback: FindObjectCallback): void; 1048 | findForeignObject(idOrName: string, type: string, options: any, callback: FindObjectCallback): void; 1049 | /** 1050 | * Deletes an object (which might not belong to this adapter) from the object db 1051 | * @param id - The id of the object including namespace 1052 | */ 1053 | delForeignObject(id: string, options?: any, callback?: ErrorCallback): void; 1054 | 1055 | // ============================== 1056 | // states 1057 | /** Writes a value into the states DB. */ 1058 | setState(id: string, state: string | number | boolean | State | Partial, ack?: boolean, options?: any, callback?: SetStateCallback): void; 1059 | /** Writes a value into the states DB only if it has changed. */ 1060 | setStateChanged(id: string, state: string | number | boolean | State | Partial, ack?: boolean, options?: any, callback?: SetStateChangedCallback): void; 1061 | /** Writes a value (which might not belong to this adapter) into the states DB. */ 1062 | setForeignState(id: string, state: string | number | boolean | State | Partial, ack?: boolean, options?: any, callback?: SetStateCallback): void; 1063 | /** Writes a value (which might not belong to this adapter) into the states DB only if it has changed. */ 1064 | setForeignStateChanged(id: string, state: string | number | boolean | State | Partial, ack?: boolean, options?: any, callback?: SetStateChangedCallback): void; 1065 | 1066 | /** Read a value from the states DB. */ 1067 | getState(id: string, callback: GetStateCallback): void; 1068 | getState(id: string, options: any, callback: GetStateCallback): void; 1069 | /** Read a value (which might not belong to this adapter) from the states DB. */ 1070 | getForeignState(id: string, callback: GetStateCallback): void; 1071 | getForeignState(id: string, options: any, callback: GetStateCallback): void; 1072 | /** Read all states of this adapter which match the given pattern */ 1073 | getStates(pattern: string, callback: GetStatesCallback): void; 1074 | getStates(pattern: string, options: any, callback: GetStatesCallback): void; 1075 | /** Read all states (which might not belong to this adapter) which match the given pattern */ 1076 | getForeignStates(pattern: string, callback: GetStatesCallback): void; 1077 | getForeignStates(pattern: string, options: any, callback: GetStatesCallback): void; 1078 | 1079 | /** Deletes a state from the states DB, but not the associated object. Consider using @link{deleteState} instead */ 1080 | delState(id: string, options?: any, callback?: ErrorCallback): void; 1081 | /** Deletes a state from the states DB, but not the associated object */ 1082 | delForeignState(id: string, options?: any, callback?: ErrorCallback): void; 1083 | 1084 | getHistory(id: string, options: GetHistoryOptions, callback: GetHistoryCallback): void; 1085 | 1086 | // MISSING: 1087 | // pushFifo and similar https://github.com/ioBroker/ioBroker.js-controller/blob/master/lib/adapter.js#L4105 1088 | // logRedirect https://github.com/ioBroker/ioBroker.js-controller/blob/master/lib/adapter.js#L4294 1089 | // requireLog https://github.com/ioBroker/ioBroker.js-controller/blob/master/lib/adapter.js#L4336 1090 | // processLog https://github.com/ioBroker/ioBroker.js-controller/blob/master/lib/adapter.js#L4360 1091 | 1092 | /** 1093 | * Writes a binary state into Redis 1094 | * @param id The id of the state 1095 | * @param binary The data to be written 1096 | * @param options (optional) Some internal options. 1097 | * @param callback Is called when the operation has finished (successfully or not) 1098 | */ 1099 | setBinaryState(id: string, binary: Buffer, callback: SetStateCallback): void; 1100 | setBinaryState(id: string, binary: Buffer, options: any, callback: SetStateCallback): void; 1101 | /** 1102 | * Reads a binary state from Redis 1103 | * @param id The id of the state 1104 | * @param options (optional) Some internal options. 1105 | * @param callback Is called when the operation has finished (successfully or not) 1106 | */ 1107 | getBinaryState(id: string, callback: GetBinaryStateCallback): void; 1108 | getBinaryState(id: string, options: any, callback: GetBinaryStateCallback): void; 1109 | 1110 | // ============================== 1111 | // enums 1112 | 1113 | /** Returns the enum tree, filtered by the optional enum name */ 1114 | getEnum(callback: GetEnumCallback): void; 1115 | getEnum(name: string, callback: GetEnumCallback): void; 1116 | getEnum(name: string, options: any, callback: GetEnumCallback): void; 1117 | getEnums(callback: GetEnumsCallback): void; 1118 | getEnums(enumList: EnumList, callback: GetEnumsCallback): void; 1119 | getEnums(enumList: EnumList, options: any, callback: GetEnumsCallback): void; 1120 | 1121 | addChannelToEnum(enumName: string, addTo: string, parentDevice: string, channelName: string, options?: any, callback?: ErrorCallback): void; 1122 | deleteChannelFromEnum(enumName: string, parentDevice: string, channelName: string, options?: any, callback?: ErrorCallback): void; 1123 | 1124 | addStateToEnum(enumName: string, addTo: string, parentDevice: string, parentChannel: string, stateName: string, options?: any, callback?: ErrorCallback): void; 1125 | deleteStateFromEnum(enumName: string, parentDevice: string, parentChannel: string, stateName: string, options?: any, callback?: ErrorCallback): void; 1126 | 1127 | // ============================== 1128 | // subscriptions 1129 | 1130 | /** Subscribe to changes of objects in this instance */ 1131 | subscribeObjects(pattern: string, options?: any): void; 1132 | /** Subscribe to changes of objects (which might not belong to this adapter) */ 1133 | subscribeForeignObjects(pattern: string, options?: any): void; 1134 | /** Unsubscribe from changes of objects in this instance */ 1135 | unsubscribeObjects(pattern: string, options?: any): void; 1136 | /** Unsubscribe from changes of objects (which might not belong to this adapter) */ 1137 | unsubscribeForeignObjects(pattern: string, options?: any): void; 1138 | 1139 | /** Subscribe to changes of states in this instance */ 1140 | subscribeStates(pattern: string, options?: any, callback?: ErrorCallback): void; 1141 | /** Subscribe to changes of states (which might not belong to this adapter) */ 1142 | subscribeForeignStates(pattern: string, options?: any, callback?: ErrorCallback): void; 1143 | /** 1144 | * Subscribe from changes of states in this instance 1145 | * @param pattern - Must match the pattern used to subscribe 1146 | */ 1147 | unsubscribeStates(pattern: string, options?: any, callback?: ErrorCallback): void; 1148 | /** 1149 | * Subscribe from changes of states (which might not belong to this adapter) 1150 | * @param pattern - Must match the pattern used to subscribe 1151 | */ 1152 | unsubscribeForeignStates(pattern: string, options?: any, callback?: ErrorCallback): void; 1153 | 1154 | // ============================== 1155 | // devices and channels 1156 | 1157 | /** creates an object with type device */ 1158 | createDevice(deviceName: string, common?: any, native?: any, options?: any, callback?: SetObjectCallback): void; 1159 | /** deletes a device, its channels and states */ 1160 | deleteDevice(deviceName: string, options?: any, callback?: ErrorCallback): void; 1161 | /** gets the devices of this instance */ 1162 | 1163 | /** creates an object with type channel */ 1164 | createChannel(parentDevice: string, channelName: string, roleOrCommon?: string | object, native?: any, options?: any, callback?: SetObjectCallback): void; 1165 | /** deletes a channel and its states */ 1166 | deleteChannel(channelName: string, options?: any, callback?: ErrorCallback): void; 1167 | deleteChannel(parentDevice: string, channelName: string, options?: any, callback?: ErrorCallback): void; 1168 | 1169 | /** creates a state and the corresponding object */ 1170 | createState(parentDevice: string, parentChannel: string, stateName: string, roleOrCommon?: string | object, native?: any, options?: any, callback?: SetObjectCallback): void; 1171 | /** deletes a state */ 1172 | deleteState(stateName: string, options?: any, callback?: ErrorCallback): void; 1173 | deleteState(parentChannel: string, stateName: string, options?: any, callback?: ErrorCallback): void; 1174 | deleteState(parentDevice: string, parentChannel: string, stateName: string, options?: any, callback?: ErrorCallback): void; 1175 | 1176 | /** 1177 | * Returns a list of all devices in this adapter instance 1178 | * @param options (optional) Some internal options. 1179 | * @param callback Is called when the operation has finished (successfully or not) 1180 | */ 1181 | getDevices(callback: GetObjectsCallback3): void; 1182 | getDevices(options: any, callback: GetObjectsCallback3): void; 1183 | 1184 | /** 1185 | * Returns a list of all channels in this adapter instance 1186 | * @param parentDevice (optional) Name of the parent device to filter the channels by 1187 | * @param options (optional) Some internal options. 1188 | * @param callback Is called when the operation has finished (successfully or not) 1189 | */ 1190 | getChannels(callback: GetObjectsCallback3): void; 1191 | getChannels(parentDevice: string | null, callback: GetObjectsCallback3): void; 1192 | getChannels(parentDevice: string | null, options: any, callback: GetObjectsCallback3): void; 1193 | /** 1194 | * Returns a list of all channels in this adapter instance 1195 | * @param parentDevice (optional) Name of the parent device to filter the channels by 1196 | * @param options (optional) Some internal options. 1197 | * @param callback Is called when the operation has finished (successfully or not) 1198 | */ 1199 | getChannelsOf(callback: GetObjectsCallback3): void; 1200 | getChannelsOf(parentDevice: string | null, callback: GetObjectsCallback3): void; 1201 | getChannelsOf(parentDevice: string | null, options: any, callback: GetObjectsCallback3): void; 1202 | 1203 | /** 1204 | * Returns a list of all states in this adapter instance 1205 | * @param parentDevice (optional) Name of the parent device to filter the channels by 1206 | * @param parentChannel (optional) Name of the parent channel to filter the channels by 1207 | * @param options (optional) Some internal options. 1208 | * @param callback Is called when the operation has finished (successfully or not) 1209 | */ 1210 | getStatesOf(callback: GetObjectsCallback3): void; 1211 | getStatesOf(parentDevice: string | null, callback: GetObjectsCallback3): void; 1212 | getStatesOf(parentDevice: string | null, parentChannel: string | null, callback: GetObjectsCallback3): void; 1213 | getStatesOf(parentDevice: string | null, parentChannel: string | null, options: any, callback: GetObjectsCallback3): void; 1214 | 1215 | // ============================== 1216 | // filesystem 1217 | 1218 | /** 1219 | * reads the content of directory from DB for given adapter and path 1220 | * @param adapter - adapter name. If adapter name is null, default will be the name of the current adapter. 1221 | * @param path - path to direcory without adapter name. E.g. If you want to read "/vis.0/main/views.json", here must be "/main/views.json" and _adapter must be equal to "vis.0". 1222 | */ 1223 | readDir(adapterName: string, path: string, callback: ReadDirCallback): void; 1224 | readDir(adapterName: string, path: string, options: any, callback: ReadDirCallback): void; 1225 | mkDir(adapterName: string, path: string, callback: ErrorCallback): void; 1226 | mkDir(adapterName: string, path: string, options: any, callback: ErrorCallback): void; 1227 | 1228 | readFile(adapterName: string, path: string, callback: ReadFileCallback): void; 1229 | readFile(adapterName: string, path: string, options: any, callback: ReadFileCallback): void; 1230 | writeFile(adapterName: string, path: string, data: Buffer | string, callback: ErrorCallback): void; 1231 | writeFile(adapterName: string, path: string, data: Buffer | string, options: any, callback: ErrorCallback): void; // options see https://github.com/ioBroker/ioBroker.js-controller/blob/master/lib/objects/objectsInMemServer.js#L599 1232 | 1233 | delFile(adapterName: string, path: string, callback: ErrorCallback): void; 1234 | delFile(adapterName: string, path: string, options: any, callback: ErrorCallback): void; 1235 | unlink(adapterName: string, path: string, callback: ErrorCallback): void; 1236 | unlink(adapterName: string, path: string, options: any, callback: ErrorCallback): void; 1237 | 1238 | rename(adapterName: string, oldName: string, newName: string, callback: ErrorCallback): void; 1239 | rename(adapterName: string, oldName: string, newName: string, options: any, callback: ErrorCallback): void; 1240 | 1241 | /** 1242 | * Changes access rights of all files in the adapter directory 1243 | * @param adapter Name of the adapter instance, e.g. "admin.0". Defaults to the namespace of this adapter. 1244 | * @param path Pattern to match the file path against 1245 | * @param options Mode of the access change as a number or hexadecimal string 1246 | * @param callback Is called when the operation has finished (successfully or not) 1247 | */ 1248 | chmodFile(adapter: string | null, path: string, options: {mode: number | string} | DictionaryLike, callback: ChownFileCallback): void; 1249 | 1250 | // ============================== 1251 | // formatting 1252 | 1253 | formatValue(value: number | string, format: any): string; 1254 | formatValue(value: number | string, decimals: number, format: any): string; 1255 | formatDate(dateObj: string | Date | number, format: string): string; 1256 | formatDate(dateObj: string | Date | number, isDuration: boolean | string, format: string): string; 1257 | } // end interface Adapter 1258 | 1259 | type ObjectChangeHandler = (id: string, obj: ioBroker.Object) => void; 1260 | type StateChangeHandler = (id: string, obj: State) => void; 1261 | type MessageHandler = (obj: Message) => void; 1262 | 1263 | type EmptyCallback = () => void; 1264 | type ErrorCallback = (err?: string) => void; 1265 | // TODO: Redefine callbacks as subclass of GenericCallback 1266 | type GenericCallback = (err: string | null, result?: T) => void; 1267 | 1268 | type SetObjectCallback = (err: string | null, obj: { id: string }) => void; 1269 | type GetObjectCallback = (err: string | null, obj: ioBroker.Object) => void; 1270 | type GetEnumCallback = (err: string | null, enums: DictionaryLike, requestedEnum: string) => void; 1271 | type GetEnumsCallback = ( 1272 | err: string | null, 1273 | result: { 1274 | [groupName: string]: DictionaryLike, 1275 | }, 1276 | ) => void; 1277 | type GetObjectsCallback = (err: string | null, objects: DictionaryLike) => void; 1278 | 1279 | type FindObjectCallback = ( 1280 | /** If an error happened, this contains the message */ 1281 | err: string | null, 1282 | /** If an object was found, this contains the ID */ 1283 | id?: string, 1284 | /** If an object was found, this contains the common.name */ 1285 | name?: string, 1286 | ) => void; 1287 | 1288 | interface GetObjectsItem { 1289 | /** The ID of this object */ 1290 | id: string; 1291 | /** A copy of the object from the DB */ 1292 | value: T; 1293 | } 1294 | // This is a version used by GetDevices/GetChannelsOf/GetStatesOf 1295 | type GetObjectsCallback3 = (err: string | null, result?: GetObjectsItem[]) => void; 1296 | 1297 | type GetStateCallback = (err: string | null, state: State) => void; 1298 | type GetStatesCallback = (err: string | null, states: DictionaryLike) => void; 1299 | /** Version of the callback used by States.getStates */ 1300 | type GetStatesCallback2 = (err: string | null, states: State[]) => void; 1301 | type GetBinaryStateCallback = (err: string | null, state?: Buffer) => void; 1302 | type SetStateCallback = (err: string | null, id?: string) => void; 1303 | type SetStateChangedCallback = (err: string | null, id: string, notChanged: boolean) => void; 1304 | type DeleteStateCallback = (err: string | null, id?: string) => void; 1305 | type GetHistoryCallback = (err: string | null, result: (State & { id?: string })[], step: number, sessionId?: string) => void; 1306 | 1307 | /** Contains the return values of readDir */ 1308 | interface ReadDirResult { 1309 | /** Name of the file or directory */ 1310 | file: string; 1311 | /** File system stats */ 1312 | stats: fs.Stats; 1313 | /** Whether this is a directory or a file */ 1314 | isDir: boolean; 1315 | /** Access rights */ 1316 | acl: EvaluatedFileACL; 1317 | /** Date of last modification */ 1318 | modifiedAt: number; 1319 | /** Date of creation */ 1320 | createdAt: number; 1321 | } 1322 | type ReadDirCallback = (err: string | null, entries?: ReadDirResult[]) => void; 1323 | type ReadFileCallback = (err: string | null, file?: Buffer | string, mimeType?: string) => void; 1324 | 1325 | /** Contains the return values of chownFile */ 1326 | interface ChownFileResult { 1327 | /** The parent directory of the processed file or directory */ 1328 | path: string; 1329 | /** Name of the file or directory */ 1330 | file: string; 1331 | /** File system stats */ 1332 | stats: fs.Stats; 1333 | /** Whether this is a directory or a file */ 1334 | isDir: boolean; 1335 | /** Access rights */ 1336 | acl: FileACL; 1337 | /** Date of last modification */ 1338 | modifiedAt: number; 1339 | /** Date of creation */ 1340 | createdAt: number; 1341 | } 1342 | type ChownFileCallback = (err: string | null, entries?: ChownFileResult[], id?: string) => void; 1343 | 1344 | /** Contains the return values of rm */ 1345 | interface RmResult { 1346 | /** The parent directory of the deleted file or directory */ 1347 | path: string; 1348 | /** The name of the deleted file or directory */ 1349 | file: string; 1350 | /** Whether the deleted object was a directory or a file */ 1351 | isDir: boolean; 1352 | } 1353 | type RmCallback = (err: string | null, entries?: RmResult[]) => void; 1354 | 1355 | type GetUserGroupCallback = (objectsInstance: Objects, user: User, groups: UserGroup[], acl: ObjectPermissions) => void; 1356 | 1357 | type ChownObjectCallback = (err: string | null, list?: ioBroker.Object[]) => void; 1358 | 1359 | type GetConfigKeysCallback = (err: string | null, list?: string[]) => void; 1360 | // this is a version of the callback used by Objects.getObjects 1361 | type GetObjectsCallback2 = (err: string | null, objects?: (ioBroker.Object | { err: string })[]) => void; 1362 | 1363 | interface GetObjectViewItem { 1364 | /** The ID of this object */ 1365 | id: string; 1366 | /** A copy of the object from the DB or some aggregation result */ 1367 | value: ioBroker.Object | any; 1368 | } 1369 | type GetObjectViewCallback = (err: string | null, result?: { rows: GetObjectViewItem[] }) => void; 1370 | 1371 | interface GetObjectListItem extends GetObjectViewItem { 1372 | /** A copy of the object */ 1373 | value: ioBroker.Object; 1374 | /** The same as @link{value} */ 1375 | doc: ioBroker.Object; 1376 | } 1377 | type GetObjectListCallback = (err: string | null, result?: { rows: GetObjectListItem[] }) => void; 1378 | 1379 | type ExtendObjectCallback = (err: string | null, result?: {id: string, value: ioBroker.Object}, id?: string ) => void; 1380 | 1381 | type GetSessionCallback = (session: Session) => void; 1382 | 1383 | } // end namespace ioBroker 1384 | } // end declare global 1385 | -------------------------------------------------------------------------------- /test/testrun.js: -------------------------------------------------------------------------------- 1 | const { Server } = require("../"); 2 | const ts = require("typescript"); 3 | 4 | const sourceCode = ` 5 | function foo() { } 6 | if ( 7 | ; 8 | foo(); 9 | function bar() {return "foo"} 10 | ` 11 | 12 | /** @type {ts.CompilerOptions} */ 13 | const compilerOptions = { 14 | emitDeclarationOnly: true, 15 | noEmitOnError: false, 16 | noImplicitAny: false, 17 | strict: false, 18 | //emitDeclarationOnly: true, 19 | // allowJs: true, 20 | // checkJs: true, 21 | // allowNonTsExtensions: true, 22 | }; 23 | 24 | const compiler = new Server(compilerOptions); 25 | 26 | const result = compiler.compile("foo.ts", sourceCode); 27 | 28 | console.dir(result); 29 | //console.log(result.result.toString()); 30 | //console.log(); 31 | //console.log(result.declarations.toString()); 32 | 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "declaration": true, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "noEmitOnError": true, 8 | "noImplicitAny": false, 9 | "outDir": "build/", 10 | "removeComments": false, 11 | // "sourceMap": false, 12 | // "inlineSourceMap": true, 13 | // "sourceRoot": "src/", 14 | "target": "es5", 15 | "lib": ["es2015"], 16 | "importsNotUsedAsValues": "error", 17 | "skipLibCheck": true, 18 | "esModuleInterop": true 19 | } 20 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "enable": true, 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "rules": { 7 | "indent": [true, "tabs", 4], 8 | "object-literal-sort-keys": false, 9 | "object-literal-shorthand": false, 10 | "array-type": [true, "array"], 11 | "max-line-length": [false], 12 | "interface-name": [false], 13 | "variable-name": [ 14 | true, 15 | "ban-keywords", 16 | "check-format", "allow-leading-underscore", "allow-trailing-underscore" 17 | ], 18 | "member-ordering": [false], 19 | "curly": [true, "ignore-same-line"], 20 | "triple-equals": [true, "allow-undefined-check", "allow-null-check"], 21 | "arrow-parens": false, 22 | "no-bitwise": false, 23 | "quotemark": [true, "double", "avoid-escape"], 24 | "no-console": [false], 25 | "space-before-function-paren": [true, { 26 | "anonymous": "always", 27 | "named": "never", 28 | "asyncArrow": "always" 29 | }] 30 | } 31 | } --------------------------------------------------------------------------------