2022-01-03 16:20:46 +01:00
/ * *
* Uses the acorn . js library to parse a script ' s code into an AST and
* recursively walk through that AST to replace import urls with blobs
* /
import * as walk from "acorn-walk" ;
import { parse } from "acorn" ;
2019-04-11 10:37:40 +02:00
import { makeRuntimeRejectMsg } from "./NetscriptEvaluator" ;
2021-03-11 09:02:05 +01:00
import { ScriptUrl } from "./Script/ScriptUrl" ;
2021-09-24 23:07:53 +02:00
import { WorkerScript } from "./Netscript/WorkerScript" ;
import { Script } from "./Script/Script" ;
2021-12-23 21:55:58 +01:00
import { areImportsEquals } from "./Terminal/DirectoryHelpers" ;
2022-01-05 01:09:34 +01:00
import { IPlayer } from "./PersonObjects/IPlayer" ;
2018-05-05 05:20:19 +02:00
// Makes a blob that contains the code of a given script.
2021-09-24 23:07:53 +02:00
function makeScriptBlob ( code : string ) : Blob {
2021-09-05 01:09:30 +02:00
return new Blob ( [ code ] , { type : "text/javascript" } ) ;
2018-05-05 05:20:19 +02:00
}
2022-01-05 01:09:34 +01:00
export async function compile ( player : IPlayer , script : Script , scripts : Script [ ] ) : Promise < void > {
2021-10-15 18:47:43 +02:00
if ( ! shouldCompile ( script , scripts ) ) return ;
// The URL at the top is the one we want to import. It will
// recursively import all the other modules in the urlStack.
//
// Webpack likes to turn the import into a require, which sort of
// but not really behaves like import. Particularly, it cannot
// load fully dynamic content. So we hide the import from webpack
// by placing it inside an eval call.
2022-01-05 01:09:34 +01:00
await script . updateRamUsage ( player , scripts ) ;
2021-10-15 18:47:43 +02:00
const uurls = _getScriptUrls ( script , scripts , [ ] ) ;
2021-12-13 01:39:53 +01:00
const url = uurls [ uurls . length - 1 ] . url ;
if ( script . url && script . url !== url ) {
2022-03-31 03:38:36 +02:00
URL . revokeObjectURL ( script . url ) ;
2021-12-13 01:39:53 +01:00
// Thoughts: Should we be revoking any URLs here?
// If a script is modified repeatedly between two states,
// we could reuse the blob at a later time.
// BlobCache.removeByValue(script.url);
// URL.revokeObjectURL(script.url);
// if (script.dependencies.length > 0) {
// script.dependencies.forEach((dep) => {
// removeBlobFromCache(dep.url);
// URL.revokeObjectURL(dep.url);
// });
// }
2021-12-12 20:49:02 +01:00
}
2022-03-31 03:40:51 +02:00
if ( script . dependencies . length > 0 ) script . dependencies . forEach ( ( dep ) = > URL . revokeObjectURL ( dep . url ) ) ;
script . url = uurls [ uurls . length - 1 ] . url ;
script . module = new Promise ( ( resolve ) = > resolve ( eval ( "import(uurls[uurls.length - 1].url)" ) ) ) ;
2021-10-15 18:47:43 +02:00
script . dependencies = uurls ;
}
2021-10-15 05:39:30 +02:00
2018-05-05 05:20:19 +02:00
// Begin executing a user JS script, and return a promise that resolves
// or rejects when the script finishes.
// - script is a script to execute (see Script.js). We depend only on .filename and .code.
// scripts is an array of other scripts on the server.
// env is the global environment that should be visible to all the scripts
// (i.e. hack, grow, etc.).
// When the promise returned by this resolves, we'll have finished
// running the main function of the script.
2022-01-05 01:09:34 +01:00
export async function executeJSScript (
player : IPlayer ,
scripts : Script [ ] = [ ] ,
workerScript : WorkerScript ,
) : Promise < void > {
2021-09-25 07:26:03 +02:00
const script = workerScript . getScript ( ) ;
2021-09-24 23:07:53 +02:00
if ( script === null ) throw new Error ( "script is null" ) ;
2022-01-05 01:09:34 +01:00
await compile ( player , script , scripts ) ;
2021-10-28 00:25:22 +02:00
workerScript . ramUsage = script . ramUsage ;
2021-09-25 08:36:49 +02:00
const loadedModule = await script . module ;
2018-05-12 02:45:40 +02:00
2021-09-25 07:26:03 +02:00
const ns = workerScript . env . vars ;
2018-05-05 05:20:19 +02:00
2021-09-24 23:07:53 +02:00
// TODO: putting await in a non-async function yields unhelpful
// "SyntaxError: unexpected reserved word" with no line number information.
if ( ! loadedModule . main ) {
throw makeRuntimeRejectMsg (
workerScript ,
` ${ script . filename } cannot be run because it does not have a main function. ` ,
) ;
2021-09-05 01:09:30 +02:00
}
2021-09-24 23:07:53 +02:00
return loadedModule . main ( ns ) ;
2018-05-05 05:20:19 +02:00
}
2022-01-09 09:50:36 +01:00
function isDependencyOutOfDate ( filename : string , scripts : Script [ ] , scriptModuleSequenceNumber : number ) : boolean {
const depScript = scripts . find ( ( s ) = > s . filename == filename ) ;
// If the script is not present on the server, we should recompile, if only to get any necessary
// compilation errors.
if ( ! depScript ) return true ;
const depIsMoreRecent = depScript . moduleSequenceNumber > scriptModuleSequenceNumber ;
return depIsMoreRecent ;
}
2019-06-02 21:21:08 +02:00
/ * * R e t u r n s w h e t h e r w e s h o u l d c o m p i l e t h e s c r i p t p a r a m e t e r .
*
* @param { Script } script
* @param { Script [ ] } scripts
* /
2021-09-24 23:07:53 +02:00
function shouldCompile ( script : Script , scripts : Script [ ] ) : boolean {
2021-09-05 01:09:30 +02:00
if ( script . module === "" ) return true ;
2022-01-09 09:50:36 +01:00
return script . dependencies . some ( ( dep ) = > isDependencyOutOfDate ( dep . filename , scripts , script . moduleSequenceNumber ) ) ;
2019-06-02 21:21:08 +02:00
}
2018-05-05 05:20:19 +02:00
// Gets a stack of blob urls, the top/right-most element being
// the blob url for the named script on the named server.
//
// - script -- the script for whom we are getting a URL.
// - scripts -- all the scripts available on this server
// - seen -- The modules above this one -- to prevent mutual dependency.
//
// TODO We don't make any effort to cache a given module when it is imported at
// different parts of the tree. That hasn't presented any problem with during
// testing, but it might be an idea for the future. Would require a topo-sort
// then url-izing from leaf-most to root-most.
2019-06-02 21:21:08 +02:00
/ * *
* @param { Script } script
* @param { Script [ ] } scripts
* @param { Script [ ] } seen
* @returns { ScriptUrl [ ] } All of the compiled scripts , with the final one
* in the list containing the blob corresponding to
* the script parameter .
* /
// BUG: apparently seen is never consulted. Oops.
2021-09-24 23:07:53 +02:00
function _getScriptUrls ( script : Script , scripts : Script [ ] , seen : Script [ ] ) : ScriptUrl [ ] {
2021-09-05 01:09:30 +02:00
// Inspired by: https://stackoverflow.com/a/43834063/91401
2022-01-09 09:50:36 +01:00
const urlStack : ScriptUrl [ ] = [ ] ;
// Seen contains the dependents of the current script. Make sure we include that in the script dependents.
for ( const dependent of seen ) {
2022-01-18 20:02:12 +01:00
if ( ! script . dependents . some ( ( s ) = > s . server === dependent . server && s . filename == dependent . filename ) ) {
2022-01-09 09:50:36 +01:00
script . dependents . push ( { server : dependent.server , filename : dependent.filename } ) ;
}
}
2021-09-05 01:09:30 +02:00
seen . push ( script ) ;
try {
// Replace every import statement with an import to a blob url containing
// the corresponding script. E.g.
//
// import {foo} from "bar.js";
//
// becomes
//
// import {foo} from "blob://<uuid>"
//
// Where the blob URL contains the script content.
2022-01-03 16:20:46 +01:00
// Parse the code into an ast tree
const ast : any = parse ( script . code , { sourceType : "module" , ecmaVersion : "latest" , ranges : true } ) ;
const importNodes : Array < any > = [ ] ;
// Walk the nodes of this tree and find any import declaration statements.
walk . simple ( ast , {
ImportDeclaration ( node : any ) {
// Push this import onto the stack to replace
importNodes . push ( {
filename : node.source.value ,
start : node.source.range [ 0 ] + 1 ,
2022-01-18 20:02:12 +01:00
end : node.source.range [ 1 ] - 1 ,
2022-01-03 16:20:46 +01:00
} ) ;
2022-01-10 21:54:57 +01:00
} ,
ExportNamedDeclaration ( node : any ) {
if ( node . source ) {
importNodes . push ( {
filename : node.source.value ,
start : node.source.range [ 0 ] + 1 ,
2022-01-18 20:02:12 +01:00
end : node.source.range [ 1 ] - 1 ,
2022-01-10 21:54:57 +01:00
} ) ;
}
} ,
ExportAllDeclaration ( node : any ) {
if ( node . source ) {
importNodes . push ( {
filename : node.source.value ,
start : node.source.range [ 0 ] + 1 ,
2022-01-18 20:02:12 +01:00
end : node.source.range [ 1 ] - 1 ,
2022-01-10 21:54:57 +01:00
} ) ;
}
2022-01-18 20:02:12 +01:00
} ,
2022-01-03 16:20:46 +01:00
} ) ;
// Sort the nodes from last start index to first. This replaces the last import with a blob first,
// preventing the ranges for other imports from being shifted.
importNodes . sort ( ( a , b ) = > b . start - a . start ) ;
let transformedCode = script . code ;
// Loop through each node and replace the script name with a blob url.
for ( const node of importNodes ) {
const filename = node . filename . startsWith ( "./" ) ? node . filename . substring ( 2 ) : node . filename ;
// Find the corresponding script.
const matchingScripts = scripts . filter ( ( s ) = > areImportsEquals ( s . filename , filename ) ) ;
if ( matchingScripts . length === 0 ) continue ;
const [ importedScript ] = matchingScripts ;
2022-01-20 22:11:48 +01:00
const urls = _getScriptUrls ( importedScript , scripts , seen ) ;
2022-01-03 16:20:46 +01:00
// The top url in the stack is the replacement import file for this script.
urlStack . push ( . . . urls ) ;
const blob = urls [ urls . length - 1 ] . url ;
// Replace the blob inside the import statement.
transformedCode = transformedCode . substring ( 0 , node . start ) + blob + transformedCode . substring ( node . end ) ;
}
2018-05-05 05:20:19 +02:00
2021-09-05 01:09:30 +02:00
// We automatically define a print function() in the NetscriptJS module so that
// accidental calls to window.print() do not bring up the "print screen" dialog
2022-04-12 03:46:17 +02:00
transformedCode += ` \ n \ nfunction print() {throw new Error("Invalid call to window.print(). Did you mean to use Netscript's print()?");} \ n//# sourceURL= ${ script . server } / ${ script . filename } ` ;
2018-05-13 08:06:44 +02:00
2022-01-20 22:11:48 +01:00
const blob = URL . createObjectURL ( makeScriptBlob ( transformedCode ) ) ;
2021-12-13 01:39:53 +01:00
// Push the blob URL onto the top of the stack.
2022-01-09 09:50:36 +01:00
urlStack . push ( new ScriptUrl ( script . filename , blob , script . moduleSequenceNumber ) ) ;
2021-09-05 01:09:30 +02:00
return urlStack ;
} catch ( err ) {
// If there is an error, we need to clean up the URLs.
2022-01-16 01:45:03 +01:00
for ( const url of urlStack ) URL . revokeObjectURL ( url . url ) ;
2021-09-05 01:09:30 +02:00
throw err ;
} finally {
seen . pop ( ) ;
}
2018-05-05 05:20:19 +02:00
}