Renderer/assets/Scripts/compiler/compiler.js
austin d0235a01d6
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good
major script engine work
2024-11-15 15:10:42 -05:00

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