499 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			499 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 
 | |
| /**
 | |
|  * @description The compiler object
 | |
|  */
 | |
| let COMPILER = {
 | |
| 
 | |
|     //
 | |
|     //
 | |
|     //    VIRTUAL FILE SYSTEM
 | |
|     //
 | |
|     //
 | |
| 
 | |
|     /**
 | |
|      * The map of all source files to their content and compiled value
 | |
|      */
 | |
|     fileMap: { },
 | |
| 
 | |
|     /**
 | |
|      * The list of all source files to compile
 | |
|      */
 | |
|     sourceFiles: [ ],
 | |
| 
 | |
|     /**
 | |
|      * The top level directory, "/"
 | |
|      */
 | |
|     topLevelDirectory: {
 | |
|         //as required by our framework
 | |
|         Scripts: {
 | |
|             compiler: {
 | |
|                 "host_access.js": {
 | |
|                     content: "",
 | |
|                     version: 0,
 | |
|                 },
 | |
|                 version: 0,
 | |
|                 isDir: true,
 | |
|             },
 | |
|             version: 0,
 | |
|             isDir: true,
 | |
|         },
 | |
|         //as required by language service
 | |
|         node_modules: {
 | |
|             "@types": {
 | |
|                 "lib.d.ts": {
 | |
|                     content: "",
 | |
|                     version: 0,
 | |
|                     isDir: false,
 | |
|                 },
 | |
|                 version: 0,
 | |
|                 isDir: true,
 | |
|             },
 | |
|             version: 0,
 | |
|             isDir: true,
 | |
|         },
 | |
|         version: 0,
 | |
|         isDir: true,
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * The current directory, "/"
 | |
|      */
 | |
|     currentDirectory : { },
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Preloads a file from the host system's cache
 | |
|      * @param {*} fileName The name of the file
 | |
|      * @param {*} content The content of the file
 | |
|      */
 | |
|     preloadFile: (fileName, content) => {
 | |
|         COMPILER.fileMap[fileName] = COMPILER.createFile(fileName, content)
 | |
|         COMPILER.fileMap[fileName].moduleContent = COMPILER.getModuleContent(content)
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Gets the module content from generic file content
 | |
|      * @param {*} content The file content
 | |
|      * @returns The module content
 | |
|      */
 | |
|     getModuleContent: (content) => {
 | |
|         return "let exports = { }\n" +
 | |
|         content + "\n" +
 | |
|         "return exports"
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Registers a file with the compiler
 | |
|      * @param {string} fileName The file's name
 | |
|      * @param {string} content The content of the file
 | |
|      * @returns {string[]} The list of all files that still need to be registered by the host
 | |
|      */
 | |
|     registerFile: (fileName, content) => {
 | |
| 
 | |
|         //the list of files that are imported by this file
 | |
|         let dependentFiles = []
 | |
|     
 | |
|         loggerScripts.INFO('REGISTER FILE ' + fileName)
 | |
|         if(!COMPILER.fileMap[fileName]){
 | |
|             //create the virtual file
 | |
|             COMPILER.fileMap[fileName] = COMPILER.createFile(fileName,content)
 | |
|             //register the file itself
 | |
|             COMPILER.fileMap[fileName].tsSourceFile = ts.createSourceFile(
 | |
|                 fileName,
 | |
|                 content,
 | |
|                 ts.ScriptTarget.Latest,
 | |
|             )
 | |
|             COMPILER.sourceFiles.push(fileName)
 | |
|             /**
 | |
|              * The preprocessed info about the file
 | |
|              * {
 | |
|              *   referencedFiles: ?,
 | |
|              *   typeReferenceDirectives: ?,
 | |
|              *   libReferenceDirectives: ?,
 | |
|              *   importedFiles: Array<{
 | |
|              *     fileName: string, //the path (without file ending) of the file that is imported by this file
 | |
|              *     pos: ?,
 | |
|              *     end: ?,
 | |
|              *   }>,
 | |
|              *   isLibFile: boolean,
 | |
|              *   ambientExternalModules: ?,
 | |
|              * }
 | |
|              */
 | |
|             const fileInfo = ts.preProcessFile(content)
 | |
|             loggerScripts.INFO('==========================')
 | |
|             loggerScripts.INFO(fileName)
 | |
|             loggerScripts.INFO('Registered file depends on:')
 | |
|             fileInfo.importedFiles.forEach(module => {
 | |
|                 let extension = ".ts"
 | |
|                 /**
 | |
|                  * {
 | |
|                  *   resolvedModule: ?,
 | |
|                  *   failedLookupLocations: Array<string>,
 | |
|                  *   affectingLocations: ?,
 | |
|                  *   resolutionDiagnostics: ?,
 | |
|                  *   alternateResult: ?,
 | |
|                  * }
 | |
|                  */
 | |
|                 const resolvedImport = ts.resolveModuleName(module.fileName,fileName,COMPILER.compilerOptions,COMPILER.customCompilerHost)
 | |
|                 if(resolvedImport?.resolvedModule){
 | |
|                     /**
 | |
|                      * undefined
 | |
|                      * OR
 | |
|                      * {
 | |
|                      *   resolvedFileName: ?,
 | |
|                      *   originalPath: ?,
 | |
|                      *   extension: string, (ie ".js", ".ts", etc)
 | |
|                      *   isExternalLibraryImport: boolean,
 | |
|                      *   packageId: ?,
 | |
|                      *   resolvedUsingTsExtension: boolean,
 | |
|                      * }
 | |
|                      */
 | |
|                     const module = resolvedImport.resolvedModule
 | |
|                     extension = module.extension
 | |
|                 }
 | |
|                 //am assuming we're always importing typescript for the time being
 | |
|                 const dependentFile = module.fileName + extension
 | |
|                 const normalizedDependentFilePath = FILE_RESOLUTION_getFilePath(dependentFile,false)
 | |
|                 if(!COMPILER.fileMap[normalizedDependentFilePath]){
 | |
|                     dependentFiles.push(normalizedDependentFilePath)
 | |
|                     loggerScripts.INFO(" - " + normalizedDependentFilePath)
 | |
|                 }
 | |
|             })
 | |
|     
 | |
|             //If the compiler has already run once, run the language service against only this file
 | |
|             if(!!COMPILER.compilerHasRun){
 | |
|                 COMPILER.emitFile(fileName)
 | |
|             }
 | |
|         }
 | |
|         return dependentFiles;
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Creates a file object for a given path
 | |
|      * @param string} fileName The name of the file
 | |
|      * @param {string} content The content of the file
 | |
|      * @returns The file object
 | |
|      */
 | |
|     createFile: (fileName, content) => {
 | |
|         //get the file path array
 | |
|         const filePathArray = COMPILER.getPath(fileName)
 | |
|         let mutableArray = filePathArray
 | |
|     
 | |
|         //the current folder as we recursively create folders to populate this file
 | |
|         let currentFolder = COMPILER.topLevelDirectory
 | |
|     
 | |
|         //recursively create directories until our file is written
 | |
|         while(mutableArray.length > 1){
 | |
|             let nextDirName = mutableArray.shift()
 | |
|             if(!currentFolder?.[nextDirName]){
 | |
|                 //create directory
 | |
|                 currentFolder[nextDirName] = {
 | |
|                     isDir: true,
 | |
|                     "..": currentFolder,
 | |
|                 }
 | |
|             }
 | |
|             currentFolder = currentFolder?.[nextDirName]
 | |
|         }
 | |
|     
 | |
|         //create the actual file
 | |
|         currentFolder[mutableArray[0]] = {
 | |
|             isDir: false,
 | |
|             dir: currentFolder,
 | |
|             content: content,
 | |
|             version: 0,
 | |
|         }
 | |
|     
 | |
|         //return the file
 | |
|         return currentFolder[mutableArray[0]]
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Gets the path for the file
 | |
|      * @param {string} fullyQualifiedFilePath The fully qualified file path
 | |
|      * @returns {string[]} The array of directories ending with the name of the file
 | |
|      */
 | |
|     getPath: (fullyQualifiedFilePath) => {
 | |
|         let modifiedFileName = fullyQualifiedFilePath
 | |
|         //remove leading "/"
 | |
|         if(modifiedFileName.startsWith("/")){
 | |
|             modifiedFileName = modifiedFileName.substring(1)
 | |
|         }
 | |
|         //split
 | |
|         return modifiedFileName.split("/")
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Gets the path for the file
 | |
|      * @param {stringp[]} filePathArray The fully qualified file path
 | |
|      * @returns The array of directories ending with the name of the file
 | |
|      */
 | |
|     getFileByPath: (filePathArray) => {
 | |
|         let currentFolder = COMPILER.topLevelDirectory
 | |
|         let mutableArray = filePathArray
 | |
|     
 | |
|         //illegal state
 | |
|         if(mutableArray?.length < 1){
 | |
|             throw new Error("Trying to get a file with a path array of length 0!")
 | |
|         }
 | |
|     
 | |
|         while(mutableArray?.length > 1){
 | |
|             let nextDirName = mutableArray.shift()
 | |
|             currentFolder = currentFolder?.[nextDirName]
 | |
|             if(!currentFolder){
 | |
|                 let errorMessage = "Trying to get file in directory that doesn't exist! \n" +
 | |
|                 nextDirName
 | |
|                 throw new Error(errorMessage)
 | |
|             }
 | |
|         }
 | |
|         return currentFolder[mutableArray?.[0]]
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Checks if a file exists
 | |
|      * @param {string[]} filePathArray The file path array
 | |
|      * @returns true if it exists, false otherwise
 | |
|      */
 | |
|     fileExists: (filePathArray) => {
 | |
|         let currentFolder = COMPILER.topLevelDirectory
 | |
|         let mutableArray = filePathArray
 | |
|     
 | |
|         //illegal state
 | |
|         if(mutableArray?.length < 1){
 | |
|             throw new Error("Trying to get a file with a path array of length 0!")
 | |
|         }
 | |
|     
 | |
|         while(mutableArray.length > 1){
 | |
|             let nextDirName = mutableArray.shift()
 | |
|             currentFolder = currentFolder?.[nextDirName]
 | |
|             if(!currentFolder){
 | |
|                 return false
 | |
|             }
 | |
|         }
 | |
|         return !!currentFolder?.[mutableArray[0]]
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * The callback invoked when the compiler host tries to read a file
 | |
|      * @param {string} fileName The name of the file
 | |
|      * @param {*} languageVersion The language version
 | |
|      * @returns The file if it exists, null otherwise
 | |
|      */
 | |
|     getSourceFile: (fileName, languageVersion) => {
 | |
|         if(!!COMPILER.fileMap[fileName]){
 | |
|             return COMPILER.fileMap[fileName].tsSourceFile
 | |
|         } else {
 | |
|             return null
 | |
|         }
 | |
|     },
 | |
| 
 | |
| 
 | |
|     //
 | |
|     //
 | |
|     //   COMPILATION
 | |
|     //
 | |
|     //
 | |
| 
 | |
|     /**
 | |
|      * The compiler options
 | |
|      */
 | |
|     compilerOptions: { },
 | |
| 
 | |
|     /**
 | |
|      * Tracks whether the compiler has run or not
 | |
|      */
 | |
|     compilerHasRun: false,
 | |
| 
 | |
|     /**
 | |
|      * The typescript compiler host definition
 | |
|      */
 | |
|     customCompilerHost: null,
 | |
| 
 | |
|     /**
 | |
|      * The typescript program
 | |
|      */
 | |
|     program: null,
 | |
| 
 | |
|     /**
 | |
|      * Emits a file
 | |
|      * @param {string} fileName The name of the file
 | |
|      * @returns {void}
 | |
|      */
 | |
|     emitFile: (fileName) => {
 | |
|         loggerScripts.DEBUG('Compiler evaluating source path ' + fileName)
 | |
|         /**
 | |
|          * {
 | |
|          *   outputFiles: [ ],
 | |
|          *   emitSkipped: boolean,
 | |
|          *   diagnostics: { },
 | |
|          * }
 | |
|          */
 | |
|         const output = COMPILER.program.getEmitOutput(fileName)
 | |
|         if (!output.emitSkipped) {
 | |
|             output.outputFiles.forEach(outputFile => {
 | |
|                 loggerScripts.DEBUG(`[ts] Emitting ${outputFile}`);
 | |
|                 COMPILER.customCompilerHost.writeFile(outputFile.name, outputFile.text)
 | |
|             })
 | |
|         } else {
 | |
|             loggerScripts.DEBUG(`[ts] Emitting ${fileName} failed`);
 | |
|             COMPILER.logEmitError(fileName);
 | |
|         }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Logs errors raised during emission of files
 | |
|      * @param {string} fileName The name of the file to log errors about
 | |
|      * @returns {void}
 | |
|      */
 | |
|     logEmitError: (fileName) => {
 | |
|         loggerScripts.DEBUG('[ts] logErrors ' + fileName)
 | |
|         let allDiagnostics = services
 | |
|             .getCompilerOptionsDiagnostics()
 | |
|             .concat(services.getSyntacticDiagnostics(fileName))
 | |
|             .concat(services.getSemanticDiagnostics(fileName));
 | |
|     
 | |
|         allDiagnostics.forEach(diagnostic => {
 | |
|             let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
 | |
|             if (diagnostic.file) {
 | |
|                 let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
 | |
|                     diagnostic.start
 | |
|                 );
 | |
|                 loggerScripts.DEBUG(`[ts]  Error ${diagnostic.file.fileName} (${line + 1},${character +1}): ${message}`);
 | |
|             } else {
 | |
|                 loggerScripts.DEBUG(`[ts]  Error: ${message}`);
 | |
|             }
 | |
|         });
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Instructs Typescript to emit the final compiled value
 | |
|      */
 | |
|     run: () => {
 | |
|         loggerScripts.INFO('COMPILE ALL REGISTERED FILES')
 | |
|     
 | |
|         if(!COMPILER.program){
 | |
|             COMPILER.program = ts.createLanguageService(COMPILER.customCompilerHost, ts.createDocumentRegistry());
 | |
|         }
 | |
|     
 | |
|         //Emit all currently known files
 | |
|         COMPILER.sourceFiles.forEach(sourcePath => {
 | |
|             COMPILER.emitFile(sourcePath)
 | |
|         })
 | |
|     
 | |
|         //flag that the compiler has run (ie only incrementally compile when new files are added, now)
 | |
|         COMPILER.compilerHasRun = true
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Loads a file
 | |
|      * @param {*} fileName The name of the file to load (preferably already has .ts at the end)
 | |
|      */
 | |
|     runFile: (fileName) => {
 | |
|         let normalizedFilePath = FILE_RESOLUTION_getFilePath(fileName)
 | |
|         if(!!COMPILER.fileMap[normalizedFilePath]){
 | |
|             loggerScripts.INFO('RUN FILE ' + normalizedFilePath)
 | |
|             eval(COMPILER.fileMap[normalizedFilePath].content)
 | |
|         } else {
 | |
|             const message = 'FAILED TO RESOLVE FILE ' + normalizedFilePath
 | |
|             loggerScripts.WARNING(message)
 | |
|             throw new Error(message)
 | |
|         }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Loads a file
 | |
|      * @param {*} fileName The name of the file to load (preferably already has .ts at the end)
 | |
|      */
 | |
|     printSource: (fileName) => {
 | |
|         let normalizedFilePath = FILE_RESOLUTION_getFilePath(fileName)
 | |
|         if(!!COMPILER.fileMap[normalizedFilePath]){
 | |
|             loggerScripts.INFO('FILE CONTENT ' + normalizedFilePath)
 | |
|         } else {
 | |
|             const message = 'FAILED TO RESOLVE FILE ' + normalizedFilePath
 | |
|             loggerScripts.WARNING(message)
 | |
|             loggerScripts.WARNING('file map content:')
 | |
|             loggerScripts.WARNING(OBject.keys(COMPILER.fileMap) + "")
 | |
|             throw new Error(message)
 | |
|         }
 | |
|     },
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * Constructs the compiler host
 | |
|  * https://www.typescriptlang.org/tsconfig/#compilerOptions
 | |
|  * 
 | |
|  * Examples:
 | |
|  * https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API
 | |
|  * 
 | |
|  */
 | |
| COMPILER.customCompilerHost = {
 | |
|     getSourceFile: COMPILER.getSourceFile,
 | |
|     writeFile: (fileName, data) => {
 | |
|         loggerScripts.INFO("EMIT FILE " + fileName)
 | |
|         //wrap in require logic
 | |
|         let finalData = COMPILER.getModuleContent(data)
 | |
| 
 | |
|         //create file
 | |
|         COMPILER.createFile(fileName,finalData)
 | |
|         
 | |
|         //register in file map
 | |
|         COMPILER.fileMap[fileName] = {
 | |
|             content: data, //to be eval'd from top level
 | |
|             moduleContent: finalData, //to be eval'd from require()
 | |
|         }
 | |
|     },
 | |
|     getDefaultLibFileName: ts.getDefaultLibFileName,
 | |
|     useCaseSensitiveFileNames: () => false,
 | |
|     getCanonicalFileName: filename => filename,
 | |
|     getCurrentDirectory: () => "/",
 | |
|     getNewLine: () => "\n",
 | |
|     getDirectories: (path) => {
 | |
|         loggerScripts.DEBUG('[ts] getDirectories ' + path)
 | |
|         const dirs = Object.keys(COMPILER.getFileByPath(COMPILER.getPath(path)))
 | |
|         loggerScripts.DEBUG('[ts] dirs: ' + dirs)
 | |
|         return dirs
 | |
|     },
 | |
|     directoryExists: (path) => {
 | |
|         let exists = COMPILER.fileExists(COMPILER.getPath(path))
 | |
|         if(exists){
 | |
|             exists = COMPILER.getFileByPath(COMPILER.getPath(path))?.isDir
 | |
|         }
 | |
|         loggerScripts.DEBUG('[ts] directoryExists ' + path + " - " + exists)
 | |
|         return false
 | |
|     },
 | |
|     fileExists: (path) => {
 | |
|         const exists = COMPILER.fileExists(COMPILER.getPath(path))
 | |
|         loggerScripts.DEBUG('[ts] fileExists ' + path + " - " + exists)
 | |
|         return exists
 | |
|     },
 | |
|     readFile: (path) => {
 | |
|         loggerScripts.DEBUG('[ts] readFile ' + path)
 | |
|         const file = COMPILER.getFileByPath(COMPILER.getPath(path))
 | |
|         loggerScripts.DEBUG('[ts] readFile (content): ' + file.content)
 | |
|         return file.content
 | |
|     },
 | |
|     getScriptFileNames: () => {
 | |
|         loggerScripts.DEBUG('[ts] getScriptFileNames')
 | |
|         return COMPILER.sourceFiles
 | |
|     },
 | |
|     getScriptVersion: (fileName) => {
 | |
|         loggerScripts.DEBUG('[ts] getScriptVersion: ' + fileName)
 | |
|         const file = COMPILER.getFileByPath(COMPILER.getPath(fileName))
 | |
|         return file?.version
 | |
|     },
 | |
|     //https://github.com/microsoft/TypeScript/wiki/Using-the-Language-Service-API#scriptsnapshot
 | |
|     getScriptSnapshot: (fileName) => {
 | |
|         loggerScripts.DEBUG('[ts] getScriptSnapshot: ' + fileName)
 | |
|         const file = COMPILER.getFileByPath(COMPILER.getPath(fileName))
 | |
|         if(file){
 | |
|             return ts.ScriptSnapshot.fromString(file.content)
 | |
|         } else {
 | |
|             return undefined
 | |
|         }
 | |
|     },
 | |
|     getCompilationSettings: () => COMPILER.compilerOptions,
 | |
| }
 | |
| 
 | |
| //initialized CWD
 | |
| COMPILER.currentDirectory = COMPILER.topLevelDirectory
 |