diff --git a/jest.config.js b/jest.config.js index 56a4cf4554..56236e0dd1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -19,5 +19,6 @@ module.exports = { "^monaco-editor$": "/test/__mocks__/NullMock.js", "^monaco-vim$": "/test/__mocks__/NullMock.js", "/utils/Protections$": "/test/__mocks__/NullMock.js", + "@swc/wasm-web": "@swc/core", }, }; diff --git a/markdown/bitburner.ns.read.md b/markdown/bitburner.ns.read.md index da670f3211..1fc6f505d0 100644 --- a/markdown/bitburner.ns.read.md +++ b/markdown/bitburner.ns.read.md @@ -28,7 +28,7 @@ Data in the specified text file. RAM cost: 0 GB -This function is used to read data from a text file (.txt) or script (.js or .script). +This function is used to read data from a text file (.txt, .json) or script (.js, .jsx, .ts, .tsx, .script). This function will return the data in the specified file. If the file does not exist, an empty string will be returned. diff --git a/markdown/bitburner.ns.wget.md b/markdown/bitburner.ns.wget.md index 9f58d04c36..ef1349b154 100644 --- a/markdown/bitburner.ns.wget.md +++ b/markdown/bitburner.ns.wget.md @@ -30,7 +30,7 @@ True if the data was successfully retrieved from the URL, false otherwise. RAM cost: 0 GB -Retrieves data from a URL and downloads it to a file on the specified server. The data can only be downloaded to a script (.js or .script) or a text file (.txt). If the file already exists, it will be overwritten by this command. Note that it will not be possible to download data from many websites because they do not allow cross-origin resource sharing (CORS). +Retrieves data from a URL and downloads it to a file on the specified server. The data can only be downloaded to a script (.js, .jsx, .ts, .tsx, .script) or a text file (.txt, .json). If the file already exists, it will be overwritten by this command. Note that it will not be possible to download data from many websites because they do not allow cross-origin resource sharing (CORS). IMPORTANT: This is an asynchronous function that returns a Promise. The Promise’s resolved value will be a boolean indicating whether or not the data was successfully retrieved from the URL. Because the function is async and returns a Promise, it is recommended you use wget in NetscriptJS (Netscript 2.0). diff --git a/markdown/bitburner.ns.write.md b/markdown/bitburner.ns.write.md index 85605f1df7..7b1233e971 100644 --- a/markdown/bitburner.ns.write.md +++ b/markdown/bitburner.ns.write.md @@ -28,7 +28,7 @@ void RAM cost: 0 GB -This function can be used to write data to a text file (.txt) or a script (.js or .script). +This function can be used to write data to a text file (.txt, .json) or a script (.js, .jsx, .ts, .tsx, .script). This function will write data to that file. If the specified file does not exist, then it will be created. The third argument mode defines how the data will be written to the file. If mode is set to “w”, then the data is written in “write” mode which means that it will overwrite all existing data on the file. If mode is set to any other value then the data will be written in “append” mode which means that the data will be added at the end of the file. diff --git a/package-lock.json b/package-lock.json index f8ac885598..13b20dc303 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "SEE LICENSE IN license.txt", "dependencies": { + "@babel/standalone": "^7.24.4", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@material-ui/core": "^4.12.4", @@ -17,10 +18,12 @@ "@mui/material": "^5.14.12", "@mui/styles": "^5.14.12", "@mui/system": "^5.14.12", + "@swc/wasm-web": "^1.4.14", "@types/estree": "^1.0.2", "@types/react-syntax-highlighter": "^15.5.8", - "acorn": "^8.10.0", - "acorn-walk": "^8.2.0", + "acorn": "^8.11.3", + "acorn-jsx-walk": "^2.0.0", + "acorn-walk": "^8.3.2", "arg": "^5.0.2", "bcryptjs": "^2.4.3", "better-react-mathjax": "^2.0.3", @@ -53,6 +56,8 @@ "@microsoft/api-documenter": "^7.23.9", "@microsoft/api-extractor": "^7.38.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", + "@swc/core": "^1.4.14", + "@types/babel__standalone": "^7.1.7", "@types/bcryptjs": "^2.4.4", "@types/escodegen": "^0.0.7", "@types/file-saver": "^2.0.5", @@ -1866,6 +1871,14 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/standalone": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.24.4.tgz", + "integrity": "sha512-V4uqWeedadiuiCx5P5OHYJZ1PehdMpcBccNCEptKFGPiZIY3FI5f2ClxUl4r5wZ5U+ohcQ+4KW6jX2K6xXzq4Q==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -4118,6 +4131,224 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@swc/core": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.14.tgz", + "integrity": "sha512-tHXg6OxboUsqa/L7DpsCcFnxhLkqN/ht5pCwav1HnvfthbiNIJypr86rNx4cUnQDJepETviSqBTIjxa7pSpGDQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.2", + "@swc/types": "^0.1.5" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.4.14", + "@swc/core-darwin-x64": "1.4.14", + "@swc/core-linux-arm-gnueabihf": "1.4.14", + "@swc/core-linux-arm64-gnu": "1.4.14", + "@swc/core-linux-arm64-musl": "1.4.14", + "@swc/core-linux-x64-gnu": "1.4.14", + "@swc/core-linux-x64-musl": "1.4.14", + "@swc/core-win32-arm64-msvc": "1.4.14", + "@swc/core-win32-ia32-msvc": "1.4.14", + "@swc/core-win32-x64-msvc": "1.4.14" + }, + "peerDependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.14.tgz", + "integrity": "sha512-8iPfLhYNspBl836YYsfv6ErXwDUqJ7IMieddV3Ey/t/97JAEAdNDUdtTKDtbyP0j/Ebyqyn+fKcqwSq7rAof0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.14.tgz", + "integrity": "sha512-9CqSj8uRZ92cnlgAlVaWMaJJBdxtNvCzJxaGj5KuIseeG6Q0l1g+qk8JcU7h9dAsH9saHTNwNFBVGKQo0W0ujg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.14.tgz", + "integrity": "sha512-mfd5JArPITTzMjcezH4DwMw+BdjBV1y25Khp8itEIpdih9ei+fvxOOrDYTN08b466NuE2dF2XuhKtRLA7fXArQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.14.tgz", + "integrity": "sha512-3Lqlhlmy8MVRS9xTShMaPAp0oyUt0KFhDs4ixJsjdxKecE0NJSV/MInuDmrkij1C8/RQ2wySRlV9np5jK86oWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.14.tgz", + "integrity": "sha512-n0YoCa64TUcJrbcXIHIHDWQjdUPdaXeMHNEu7yyBtOpm01oMGTKP3frsUXIABLBmAVWtKvqit4/W1KVKn5gJzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.14.tgz", + "integrity": "sha512-CGmlwLWbfG1dB4jZBJnp2IWlK5xBMNLjN7AR5kKA3sEpionoccEnChOEvfux1UdVJQjLRKuHNV9yGyqGBTpxfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.14.tgz", + "integrity": "sha512-xq4npk8YKYmNwmr8fbvF2KP3kUVdZYfXZMQnW425gP3/sn+yFQO8Nd0bGH40vOVQn41kEesSe0Z5O/JDor2TgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.14.tgz", + "integrity": "sha512-imq0X+gU9uUe6FqzOQot5gpKoaC00aCUiN58NOzwp0QXEupn8CDuZpdBN93HiZswfLruu5jA1tsc15x6v9p0Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.14.tgz", + "integrity": "sha512-cH6QpXMw5D3t+lpx6SkErHrxN0yFzmQ0lgNAJxoDRiaAdDbqA6Col8UqUJwUS++Ul6aCWgNhCdiEYehPaoyDPA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.14.tgz", + "integrity": "sha512-FmZ4Tby4wW65K/36BKzmuu7mlq7cW5XOxzvufaSNVvQ5PN4OodAlqPjToe029oma4Av+ykJiif64scMttyNAzg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true + }, + "node_modules/@swc/types": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.6.tgz", + "integrity": "sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==", + "dev": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@swc/wasm-web": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@swc/wasm-web/-/wasm-web-1.4.14.tgz", + "integrity": "sha512-AE9TBrhFnV0bt38ZPfgkT8SmqhYY4RzxoZcf6eNKC7dRQXlobQLi4+qwdjHSp+BE3EQ02U3gUwctsK6Nr7Pqaw==" + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -4167,6 +4398,15 @@ "@babel/types": "^7.0.0" } }, + "node_modules/@types/babel__standalone": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/@types/babel__standalone/-/babel__standalone-7.1.7.tgz", + "integrity": "sha512-4RUJX9nWrP/emaZDzxo/+RYW8zzLJTXWJyp2k78HufG459HCz754hhmSymt3VFOU6/Wy+IZqfPvToHfLuGOr7w==", + "dev": true, + "dependencies": { + "@types/babel__core": "^7.1.0" + } + }, "node_modules/@types/babel__template": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.2.tgz", @@ -5215,9 +5455,9 @@ } }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "bin": { "acorn": "bin/acorn" }, @@ -5253,10 +5493,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-jsx-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz", + "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==" + }, "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "engines": { "node": ">=0.4.0" } diff --git a/package.json b/package.json index a7c4792c1d..c3298f3470 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "url": "https://github.com/bitburner-official/bitburner-src/issues" }, "dependencies": { + "@babel/standalone": "^7.24.4", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@material-ui/core": "^4.12.4", @@ -17,10 +18,12 @@ "@mui/material": "^5.14.12", "@mui/styles": "^5.14.12", "@mui/system": "^5.14.12", + "@swc/wasm-web": "^1.4.14", "@types/estree": "^1.0.2", "@types/react-syntax-highlighter": "^15.5.8", - "acorn": "^8.10.0", - "acorn-walk": "^8.2.0", + "acorn": "^8.11.3", + "acorn-jsx-walk": "^2.0.0", + "acorn-walk": "^8.3.2", "arg": "^5.0.2", "bcryptjs": "^2.4.3", "better-react-mathjax": "^2.0.3", @@ -54,6 +57,8 @@ "@microsoft/api-documenter": "^7.23.9", "@microsoft/api-extractor": "^7.38.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", + "@swc/core": "^1.4.14", + "@types/babel__standalone": "^7.1.7", "@types/bcryptjs": "^2.4.4", "@types/escodegen": "^0.0.7", "@types/file-saver": "^2.0.5", @@ -86,13 +91,13 @@ "prettier": "^2.8.8", "raw-loader": "^4.0.2", "react-refresh": "^0.14.0", + "rehype-mathjax": "^4.0.3", + "remark-math": "^5.1.1", "style-loader": "^3.3.3", "typescript": "^5.2.2", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^4.15.2", - "remark-math": "^5.1.1", - "rehype-mathjax": "^4.0.3" + "webpack-dev-server": "^4.15.2" }, "engines": { "node": ">=14" diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 154295e4b7..0e0475812a 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -11,3 +11,23 @@ declare module "*.png" { declare interface Document { achievements: string[]; } + +declare global { + /** + * We use Babel Parser. It's one of many internal packages of babel-standalone, and those packages are not exposed in + * the declaration file. + * Ref: https://babeljs.io/docs/babel-standalone#internal-packages + */ + declare module "@babel/standalone" { + export const packages: { + parser: { + parse: ( + code: string, + option: any, + ) => { + program: import("../utils/ScriptTransformer").BabelASTProgram; + }; + }; + }; + } +} diff --git a/src/Achievements/Achievements.ts b/src/Achievements/Achievements.ts index 7b37cf9dec..67a406b2cb 100644 --- a/src/Achievements/Achievements.ts +++ b/src/Achievements/Achievements.ts @@ -31,6 +31,7 @@ import { workerScripts } from "../Netscript/WorkerScripts"; import { getRecordValues } from "../Types/Record"; import { ServerConstants } from "../Server/data/Constants"; import { blackOpsArray } from "../Bladeburner/data/BlackOperations"; +import { isLegacyScript } from "../Paths/ScriptFilePath"; // Unable to correctly cast the JSON data into AchievementDataJson type otherwise... const achievementData = ((data)).achievements; @@ -206,7 +207,7 @@ export const achievements: Record = { NS2: { ...achievementData.NS2, Icon: "ns2", - Condition: () => [...Player.getHomeComputer().scripts.values()].some((s) => s.filename.endsWith(".js")), + Condition: () => [...Player.getHomeComputer().scripts.values()].some((s) => !isLegacyScript(s.filename)), }, FROZE: { ...achievementData.FROZE, diff --git a/src/Documentation/doc/basic/programs.md b/src/Documentation/doc/basic/programs.md index b044722e8e..82df59aacb 100644 --- a/src/Documentation/doc/basic/programs.md +++ b/src/Documentation/doc/basic/programs.md @@ -2,7 +2,7 @@ In Bitburner "Programs" refer specifically to the list of `.exe` files found in the Programs tab of the side menu. -Unlike `.js` [scripts](scripts.md) you write for yourself with JavaScript, Programs are supplied to you by Bitburner and are only "programs" in name; they do not require or allow you to access actual lines of code. Instead once you have a Program you will be able to use it directly as a function in the [Terminal](terminal.md) or scripts. +Unlike [scripts](scripts.md) you write for yourself with JavaScript, Programs are supplied to you by Bitburner and are only "programs" in name; they do not require or allow you to access actual lines of code. Instead once you have a Program you will be able to use it directly as a function in the [Terminal](terminal.md) or scripts. [n00dles /]> run BruteSSH.exe [n00dles /]> scan-analyze 10 diff --git a/src/Documentation/doc/basic/scripts.md b/src/Documentation/doc/basic/scripts.md index 577333afc7..b37e2ba677 100644 --- a/src/Documentation/doc/basic/scripts.md +++ b/src/Documentation/doc/basic/scripts.md @@ -114,7 +114,7 @@ Check how much [RAM](ram.md) a script requires to run with "n" threads **nano [script]** Create/Edit a script. -The name of a script must end with `.js`, but you can also create `.txt` files. +The name of a script must end with a script extension (.js, .jsx, .ts, .tsx, .script). You can also create a text file with a text extension (.txt, .json). **ps** diff --git a/src/NetscriptJSEvaluator.ts b/src/NetscriptJSEvaluator.ts index 1f6e98b339..cd42d9bcc6 100644 --- a/src/NetscriptJSEvaluator.ts +++ b/src/NetscriptJSEvaluator.ts @@ -5,9 +5,10 @@ import * as walk from "acorn-walk"; import { parse } from "acorn"; -import { LoadedModule, ScriptURL, ScriptModule } from "./Script/LoadedModule"; -import { Script } from "./Script/Script"; -import { ScriptFilePath, resolveScriptFilePath } from "./Paths/ScriptFilePath"; +import { LoadedModule, type ScriptURL, type ScriptModule } from "./Script/LoadedModule"; +import type { Script } from "./Script/Script"; +import type { ScriptFilePath } from "./Paths/ScriptFilePath"; +import { FileType, getFileType, getModuleScript, transformScript } from "./utils/ScriptTransformer"; // Acorn type def is straight up incomplete so we have to fill with our own. export type Node = any; @@ -82,8 +83,26 @@ function generateLoadedModule(script: Script, scripts: Map b.start - a.start); - let newCode = script.code; + let newCode = scriptCode; // Loop through each node and replace the script name with a blob url. for (const node of importNodes) { - const filename = resolveScriptFilePath(node.filename, script.filename, ".js"); - if (!filename) throw new Error(`Failed to parse import: ${node.filename}`); - - // Find the corresponding script. - const importedScript = scripts.get(filename); - if (!importedScript) continue; + const importedScript = getModuleScript(node.filename, script.filename, scripts); seenStack.push(script); importedScript.mod = generateLoadedModule(importedScript, scripts, seenStack); diff --git a/src/NetscriptWorker.ts b/src/NetscriptWorker.ts index 51fa94e665..44b64a881c 100644 --- a/src/NetscriptWorker.ts +++ b/src/NetscriptWorker.ts @@ -34,7 +34,7 @@ import { Terminal } from "./Terminal"; import { ScriptArg } from "@nsdefs"; import { CompleteRunOptions, getRunningScriptsByArgs } from "./Netscript/NetscriptHelpers"; import { handleUnknownError } from "./Netscript/ErrorMessages"; -import { resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath"; +import { isLegacyScript, legacyScriptExtension, resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath"; import { root } from "./Paths/Directory"; export const NetscriptPorts = new Map(); @@ -158,7 +158,7 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c walksimple(ast, { ImportDeclaration: (node: Node) => { hasImports = true; - const scriptName = resolveScriptFilePath(node.source.value, root, ".script"); + const scriptName = resolveScriptFilePath(node.source.value, root, legacyScriptExtension); if (!scriptName) throw new Error("'Import' failed due to invalid path: " + scriptName); const script = getScript(scriptName); if (!script) throw new Error("'Import' failed due to script not found: " + scriptName); @@ -326,7 +326,7 @@ Otherwise, this can also occur if you have attempted to launch a script from a t workerScripts.set(pid, workerScript); // Start the script's execution using the correct function for file type - (workerScript.name.endsWith(".js") ? startNetscript2Script : startNetscript1Script)(workerScript) + (isLegacyScript(workerScript.name) ? startNetscript1Script : startNetscript2Script)(workerScript) // Once the code finishes (either resolved or rejected, doesnt matter), set its // running status to false .then(function () { diff --git a/src/Paths/ProgramFilePath.ts b/src/Paths/ProgramFilePath.ts index ebc190544b..27d224ac14 100644 --- a/src/Paths/ProgramFilePath.ts +++ b/src/Paths/ProgramFilePath.ts @@ -1,7 +1,7 @@ import { Directory, isAbsolutePath } from "./Directory"; import { FilePath, isFilePath, resolveFilePath } from "./FilePath"; -/** Filepath with the additional constraint of having a .cct extension */ +/** Filepath with the additional constraint of having a .exe extension */ type WithProgramExtension = string & { __fileType: "Program" }; export type ProgramFilePath = FilePath & WithProgramExtension; diff --git a/src/Paths/ScriptFilePath.ts b/src/Paths/ScriptFilePath.ts index b365ef2e04..fe05e4c930 100644 --- a/src/Paths/ScriptFilePath.ts +++ b/src/Paths/ScriptFilePath.ts @@ -1,14 +1,16 @@ import { Directory } from "./Directory"; import { FilePath, resolveFilePath } from "./FilePath"; -/** Type for just checking a .js extension with no other verification*/ +/** Type for just checking a script extension with no other verification*/ type WithScriptExtension = string & { __fileType: "Script" }; /** Type for a valid absolute FilePath with a script extension */ export type ScriptFilePath = FilePath & WithScriptExtension; +export const legacyScriptExtension = ".script"; + /** Valid extensions. Used for some error messaging. */ -export type ScriptExtension = ".js" | ".script"; -export const validScriptExtensions: ScriptExtension[] = [".js", ".script"]; +export const validScriptExtensions = [".js", ".jsx", ".ts", ".tsx", legacyScriptExtension] as const; +export type ScriptExtension = (typeof validScriptExtensions)[number]; /** Sanitize a player input, resolve any relative paths, and for imports add the correct extension if missing * @param path The player-provided path to a file. Can contain relative parts. @@ -28,3 +30,7 @@ export function resolveScriptFilePath( export function hasScriptExtension(path: string): path is WithScriptExtension { return validScriptExtensions.some((extension) => path.endsWith(extension)); } + +export function isLegacyScript(path: string): boolean { + return path.endsWith(legacyScriptExtension); +} diff --git a/src/Paths/TextFilePath.ts b/src/Paths/TextFilePath.ts index b59a6e6da1..9e579ce22e 100644 --- a/src/Paths/TextFilePath.ts +++ b/src/Paths/TextFilePath.ts @@ -1,7 +1,7 @@ import { Directory } from "./Directory"; import { FilePath, resolveFilePath } from "./FilePath"; -/** Filepath with the additional constraint of having a .js extension */ +/** Filepath with the additional constraint of having a text extension */ type WithTextExtension = string & { __fileType: "Text" }; export type TextFilePath = FilePath & WithTextExtension; diff --git a/src/Script/RamCalculations.ts b/src/Script/RamCalculations.ts index a9b0abc32d..f09f409e5e 100644 --- a/src/Script/RamCalculations.ts +++ b/src/Script/RamCalculations.ts @@ -6,15 +6,26 @@ * the way */ import * as walk from "acorn-walk"; -import acorn, { parse } from "acorn"; +import * as acorn from "acorn"; +import { extendAcornWalkForTypeScriptNodes } from "../ThirdParty/acorn-typescript-walk"; +import { extend as extendAcornWalkForJsxNodes } from "acorn-jsx-walk"; import { RamCalculationErrorCode } from "./RamCalculationErrorCodes"; import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator"; -import { Script } from "./Script"; -import { Node } from "../NetscriptJSEvaluator"; -import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath"; -import { ServerName } from "../Types/strings"; +import type { Script } from "./Script"; +import type { Node } from "../NetscriptJSEvaluator"; +import type { ScriptFilePath } from "../Paths/ScriptFilePath"; +import type { ServerName } from "../Types/strings"; +import { + type AST, + type FileTypeFeature, + getFileType, + getFileTypeFeature, + getModuleScript, + parseAST, + ModuleResolutionError, +} from "../utils/ScriptTransformer"; export interface RamUsageEntry { type: "ns" | "dom" | "fn" | "misc"; @@ -38,6 +49,12 @@ export type RamCalculationFailure = { export type RamCalculation = RamCalculationSuccess | RamCalculationFailure; +// Extend acorn-walk to support TypeScript nodes. +extendAcornWalkForTypeScriptNodes(walk.base); + +// Extend acorn-walk to support JSX nodes. +extendAcornWalkForJsxNodes(walk.base); + // These special strings are used to reference the presence of a given logical // construct within a user script. const specialReferenceIF = "__SPECIAL_referenceIf"; @@ -55,17 +72,18 @@ function getNumericCost(cost: number | (() => number)): number { /** * Parses code into an AST and walks through it recursively to calculate * RAM usage. Also accounts for imported modules. - * @param otherScripts - All other scripts on the server. Used to account for imported scripts - * @param code - The code being parsed - * @param scriptname - The name of the script that ram needs to be added to + * @param ast - AST of the code being parsed + * @param scriptName - The name of the script that ram needs to be added to * @param server - Servername of the scripts for Error Message + * @param fileTypeFeature + * @param otherScripts - All other scripts on the server. Used to account for imported scripts * */ function parseOnlyRamCalculate( - otherScripts: Map, - code: string, - scriptname: ScriptFilePath, + ast: AST, + scriptName: ScriptFilePath, server: ServerName, - ns1?: boolean, + fileTypeFeature: FileTypeFeature, + otherScripts: Map, ): RamCalculation { /** * Maps dependent identifiers to their dependencies. @@ -85,14 +103,14 @@ function parseOnlyRamCalculate( // Scripts we've discovered that need to be parsed. const parseQueue: ScriptFilePath[] = []; // Parses a chunk of code with a given module name, and updates parseQueue and dependencyMap. - function parseCode(code: string, moduleName: ScriptFilePath): void { - const result = parseOnlyCalculateDeps(code, moduleName, ns1); + function parseCode(ast: AST, moduleName: ScriptFilePath, fileTypeFeatureOfModule: FileTypeFeature): void { + const result = parseOnlyCalculateDeps(ast, moduleName, fileTypeFeatureOfModule, otherScripts); completedParses.add(moduleName); // Add any additional modules to the parse queue; - for (let i = 0; i < result.additionalModules.length; ++i) { - if (!completedParses.has(result.additionalModules[i])) { - parseQueue.push(result.additionalModules[i]); + for (const additionalModule of result.additionalModules) { + if (!completedParses.has(additionalModule) && !parseQueue.includes(additionalModule)) { + parseQueue.push(additionalModule); } } @@ -101,24 +119,40 @@ function parseOnlyRamCalculate( } // Parse the initial module, which is the "main" script that is being run - const initialModule = scriptname; - parseCode(code, initialModule); + const initialModule = scriptName; + parseCode(ast, initialModule, fileTypeFeature); // Process additional modules, which occurs if the "main" script has any imports while (parseQueue.length > 0) { const nextModule = parseQueue.shift(); - if (nextModule === undefined) throw new Error("nextModule should not be undefined"); - if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue; + + if (nextModule === undefined) { + throw new Error("nextModule should not be undefined"); + } + if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) { + continue; + } const script = otherScripts.get(nextModule); if (!script) { return { errorCode: RamCalculationErrorCode.ImportError, - errorMessage: `File: "${nextModule}" not found on server: ${server}`, + errorMessage: `"${nextModule}" does not exist on server: ${server}`, }; } - - parseCode(script.code, nextModule); + const scriptFileType = getFileType(script.filename); + let moduleAST; + try { + moduleAST = parseAST(script.code, scriptFileType); + } catch (error) { + return { + errorCode: RamCalculationErrorCode.ImportError, + errorMessage: `Cannot parse module: ${nextModule}. Filename: ${script.filename}. Reason: ${ + error instanceof Error ? error.message : String(error) + }.`, + }; + } + parseCode(moduleAST, nextModule, getFileTypeFeature(scriptFileType)); } // Finally, walk the reference map and generate a ram cost. The initial set of keys to scan @@ -130,7 +164,9 @@ function parseOnlyRamCalculate( const loadedFns: Record = {}; while (unresolvedRefs.length > 0) { const ref = unresolvedRefs.shift(); - if (ref === undefined) throw new Error("ref should not be undefined"); + if (ref === undefined) { + throw new Error("ref should not be undefined"); + } // Check if this is one of the special keys, and add the appropriate ram cost if so. if (ref === "hacknet" && !resolvedRefs.has("hacknet")) { @@ -153,13 +189,17 @@ function parseOnlyRamCalculate( const prefix = ref.slice(0, ref.length - 2); for (const ident of Object.keys(dependencyMap).filter((k) => k.startsWith(prefix))) { for (const dep of dependencyMap[ident] || []) { - if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep); + if (!resolvedRefs.has(dep)) { + unresolvedRefs.push(dep); + } } } } else { // An exact reference. Add all dependencies of this ref. for (const dep of dependencyMap[ref] || []) { - if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep); + if (!resolvedRefs.has(dep)) { + unresolvedRefs.push(dep); + } } } @@ -178,14 +218,18 @@ function parseOnlyRamCalculate( obj: object, ref: string, ): { func: () => number | number; refDetail: string } | undefined => { - if (!obj) return; + if (!obj) { + return; + } const elem = Object.entries(obj).find(([key]) => key === ref); if (elem !== undefined && (typeof elem[1] === "function" || typeof elem[1] === "number")) { return { func: elem[1], refDetail: `${prefix}${ref}` }; } for (const [key, value] of Object.entries(obj)) { const found = findFunc(`${key}.`, value, ref); - if (found) return found; + if (found) { + return found; + } } return undefined; }; @@ -206,14 +250,7 @@ function parseOnlyRamCalculate( return { cost: ram, entries: detailedCosts.filter((e) => e.cost > 0) }; } -export function checkInfiniteLoop(code: string): number[] { - let ast: acorn.Node; - try { - ast = parse(code, { sourceType: "module", ecmaVersion: "latest" }); - } catch (e) { - // If code cannot be parsed, do not provide infinite loop detection warning - return []; - } +export function checkInfiniteLoop(ast: AST, code: string): number[] { function nodeHasTrueTest(node: acorn.Node): boolean { return node.type === "Literal" && "raw" in node && (node.raw === "true" || node.raw === "1"); } @@ -234,7 +271,7 @@ export function checkInfiniteLoop(code: string): number[] { const possibleLines: number[] = []; walk.recursive( - ast, + ast as acorn.Node, // Pretend that ast is an acorn node {}, { WhileStatement: (node: Node, st: unknown, walkDeeper: walk.WalkerCallback) => { @@ -266,8 +303,12 @@ interface ParseDepsResult { * for RAM usage calculations. It also returns an array of additional modules * that need to be parsed (i.e. are 'import'ed scripts). */ -function parseOnlyCalculateDeps(code: string, currentModule: ScriptFilePath, ns1?: boolean): ParseDepsResult { - const ast = parse(code, { sourceType: "module", ecmaVersion: "latest" }); +function parseOnlyCalculateDeps( + ast: AST, + currentModule: ScriptFilePath, + fileTypeFeature: FileTypeFeature, + otherScripts: Map, +): ParseDepsResult { // Everything from the global scope goes in ".". Everything else goes in ".function", where only // the outermost layer of functions counts. const globalKey = currentModule + memCheckGlobalKey; @@ -340,17 +381,17 @@ function parseOnlyCalculateDeps(code: string, currentModule: ScriptFilePath, ns1 } walk.recursive( - ast, + ast as acorn.Node, // Pretend that ast is an acorn node { key: globalKey }, Object.assign( { ImportDeclaration: (node: Node, st: State) => { - const importModuleName = resolveScriptFilePath(node.source.value, currentModule, ns1 ? ".script" : ".js"); - if (!importModuleName) - throw new Error( - `ScriptFilePath couldnt be resolved in ImportDeclaration. Value: ${node.source.value} ScriptFilePath: ${currentModule}`, - ); - + const rawImportModuleName = node.source.value; + // Skip these modules. They are popular path aliases of NetscriptDefinitions.d.ts. + if (fileTypeFeature.isTypeScript && (rawImportModuleName === "@nsdefs" || rawImportModuleName === "@ns")) { + return; + } + const importModuleName = getModuleScript(rawImportModuleName, currentModule, otherScripts).filename; additionalModules.push(importModuleName); // This module's global scope refers to that module's global scope, no matter how we @@ -407,27 +448,31 @@ function parseOnlyCalculateDeps(code: string, currentModule: ScriptFilePath, ns1 } /** - * Calculate's a scripts RAM Usage - * @param {string} code - The script's code - * @param {ScriptFilePath} scriptname - The script's name. Used to resolve relative paths - * @param {Script[]} otherScripts - All other scripts on the server. - * Used to account for imported scripts - * @param {ServerName} server - Servername of the scripts for Error Message - * @param {boolean} ns1 - Deprecated: is the fileExtension .script or .js + * Calculate RAM usage of a script + * + * @param input - Code's AST or code of the script + * @param scriptName - The script's name. Used to resolve relative paths + * @param server - Servername of the scripts for Error Message + * @param otherScripts - Other scripts on the server + * @returns */ export function calculateRamUsage( - code: string, - scriptname: ScriptFilePath, - otherScripts: Map, + input: AST | string, + scriptName: ScriptFilePath, server: ServerName, - ns1?: boolean, + otherScripts: Map, ): RamCalculation { try { - return parseOnlyRamCalculate(otherScripts, code, scriptname, server, ns1); - } catch (e) { + const fileType = getFileType(scriptName); + const ast = typeof input === "string" ? parseAST(input, fileType) : input; + return parseOnlyRamCalculate(ast, scriptName, server, getFileTypeFeature(fileType), otherScripts); + } catch (error) { return { - errorCode: RamCalculationErrorCode.SyntaxError, - errorMessage: e instanceof Error ? e.message : undefined, + errorCode: + error instanceof ModuleResolutionError + ? RamCalculationErrorCode.ImportError + : RamCalculationErrorCode.SyntaxError, + errorMessage: error instanceof Error ? error.message : String(error), }; } } diff --git a/src/Script/Script.ts b/src/Script/Script.ts index 45384c9c62..d5ee9d6cef 100644 --- a/src/Script/Script.ts +++ b/src/Script/Script.ts @@ -73,13 +73,7 @@ export class Script implements ContentFile { * @param {Script[]} otherScripts - Other scripts on the server. Used to process imports */ updateRamUsage(otherScripts: Map): void { - const ramCalc = calculateRamUsage( - this.code, - this.filename, - otherScripts, - this.server, - this.filename.endsWith(".script"), - ); + const ramCalc = calculateRamUsage(this.code, this.filename, this.server, otherScripts); if (ramCalc.cost && ramCalc.cost >= RamCostConstants.Base) { this.ramUsage = roundToTwo(ramCalc.cost); this.ramUsageEntries = ramCalc.entries as RamUsageEntry[]; diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts index c8145c8c18..ebbea33b1f 100644 --- a/src/ScriptEditor/NetscriptDefinitions.d.ts +++ b/src/ScriptEditor/NetscriptDefinitions.d.ts @@ -6774,7 +6774,7 @@ export interface NS { * @remarks * RAM cost: 0 GB * - * This function can be used to write data to a text file (.txt) or a script (.js or .script). + * This function can be used to write data to a text file (.txt, .json) or a script (.js, .jsx, .ts, .tsx, .script). * * This function will write data to that file. If the specified file does not exist, * then it will be created. The third argument mode defines how the data will be written to @@ -6820,7 +6820,7 @@ export interface NS { * @remarks * RAM cost: 0 GB * - * This function is used to read data from a text file (.txt) or script (.js or .script). + * This function is used to read data from a text file (.txt, .json) or script (.js, .jsx, .ts, .tsx, .script). * * This function will return the data in the specified file. * If the file does not exist, an empty string will be returned. @@ -7278,7 +7278,7 @@ export interface NS { * RAM cost: 0 GB * * Retrieves data from a URL and downloads it to a file on the specified server. - * The data can only be downloaded to a script (.js or .script) or a text file (.txt). + * The data can only be downloaded to a script (.js, .jsx, .ts, .tsx, .script) or a text file (.txt, .json). * If the file already exists, it will be overwritten by this command. * Note that it will not be possible to download data from many websites because they * do not allow cross-origin resource sharing (CORS). diff --git a/src/ScriptEditor/ScriptEditor.ts b/src/ScriptEditor/ScriptEditor.ts index bad9617aa1..daa3c76b37 100644 --- a/src/ScriptEditor/ScriptEditor.ts +++ b/src/ScriptEditor/ScriptEditor.ts @@ -60,6 +60,14 @@ export class ScriptEditor { const source = (libSource + "").replace(/export /g, ""); monaco.languages.typescript.javascriptDefaults.addExtraLib(source, "netscript.d.ts"); monaco.languages.typescript.typescriptDefaults.addExtraLib(source, "netscript.d.ts"); + /** + * Ignore these errors in the editor: + * - Cannot find module ''. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?(2792) + * - Cannot use JSX unless the '--jsx' flag is provided.(17004) + */ + monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ + diagnosticCodesToIgnore: [2792, 17004], + }); monaco.languages.json.jsonDefaults.setModeConfiguration({ ...monaco.languages.json.jsonDefaults.modeConfiguration, //completion should be disabled because the diff --git a/src/ScriptEditor/ui/ScriptEditorContext.tsx b/src/ScriptEditor/ui/ScriptEditorContext.tsx index 9453cc2140..dd06b7aa31 100644 --- a/src/ScriptEditor/ui/ScriptEditorContext.tsx +++ b/src/ScriptEditor/ui/ScriptEditorContext.tsx @@ -1,20 +1,21 @@ import React, { useContext, useState } from "react"; -import { Settings } from "../../Settings/Settings"; -import { calculateRamUsage } from "../../Script/RamCalculations"; import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes"; -import { formatRam } from "../../ui/formatNumber"; -import { useBoolean } from "../../ui/React/hooks"; +import { calculateRamUsage, type RamCalculationFailure } from "../../Script/RamCalculations"; import { BaseServer } from "../../Server/BaseServer"; +import { Settings } from "../../Settings/Settings"; +import { useBoolean } from "../../ui/React/hooks"; +import { formatRam } from "../../ui/formatNumber"; -import { Options } from "./Options"; -import { FilePath } from "../../Paths/FilePath"; -import { hasScriptExtension } from "../../Paths/ScriptFilePath"; +import type { AST } from "../../utils/ScriptTransformer"; +import type { Options } from "./Options"; +import { type ScriptFilePath } from "../../Paths/ScriptFilePath"; export interface ScriptEditorContextShape { ram: string; ramEntries: string[][]; - updateRAM: (newCode: string | null, filename: FilePath | null, server: BaseServer | null) => void; + showRAMError: (error?: RamCalculationFailure) => void; + updateRAM: (ast: AST, path: ScriptFilePath, server: BaseServer) => void; isUpdatingRAM: boolean; startUpdatingRAM: () => void; @@ -30,13 +31,30 @@ export function ScriptEditorContextProvider({ children, vim }: { children: React const [ram, setRAM] = useState("RAM: ???"); const [ramEntries, setRamEntries] = useState([["???", ""]]); - const updateRAM: ScriptEditorContextShape["updateRAM"] = (newCode, filename, server) => { - if (newCode == null || filename == null || server == null || !hasScriptExtension(filename)) { + const showRAMError: ScriptEditorContextShape["showRAMError"] = (error) => { + if (!error) { setRAM("N/A"); setRamEntries([["N/A", ""]]); return; } - const ramUsage = calculateRamUsage(newCode, filename, server.scripts, server.hostname); + let errorType; + switch (error.errorCode) { + case RamCalculationErrorCode.SyntaxError: + errorType = "Syntax Error"; + break; + case RamCalculationErrorCode.ImportError: + errorType = "Import Error"; + break; + default: + errorType = "Unknown Error"; + break; + } + setRAM(`RAM: ${errorType}`); + setRamEntries([[errorType, error.errorMessage ?? ""]]); + }; + + const updateRAM: ScriptEditorContextShape["updateRAM"] = (ast, path, server) => { + const ramUsage = calculateRamUsage(ast, path, server.hostname, server.scripts); if (ramUsage.cost && ramUsage.cost > 0) { const entries = ramUsage.entries?.sort((a, b) => b.cost - a.cost) ?? []; const entriesDisp = []; @@ -50,18 +68,10 @@ export function ScriptEditorContextProvider({ children, vim }: { children: React } if (ramUsage.errorCode !== undefined) { - setRamEntries([["Syntax Error", ramUsage.errorMessage ?? ""]]); - switch (ramUsage.errorCode) { - case RamCalculationErrorCode.ImportError: - setRAM("RAM: Import Error"); - break; - case RamCalculationErrorCode.SyntaxError: - setRAM("RAM: Syntax Error"); - break; - } + showRAMError(ramUsage); } else { - setRAM("RAM: Syntax Error"); - setRamEntries([["Syntax Error", ""]]); + setRAM("RAM: Unknown Error"); + setRamEntries([["Unknown Error", ""]]); } }; @@ -98,7 +108,17 @@ export function ScriptEditorContextProvider({ children, vim }: { children: React return ( {children} diff --git a/src/ScriptEditor/ui/ScriptEditorRoot.tsx b/src/ScriptEditor/ui/ScriptEditorRoot.tsx index 2025997038..d89a7968eb 100644 --- a/src/ScriptEditor/ui/ScriptEditorRoot.tsx +++ b/src/ScriptEditor/ui/ScriptEditorRoot.tsx @@ -30,6 +30,9 @@ import { NoOpenScripts } from "./NoOpenScripts"; import { ScriptEditorContextProvider, useScriptEditorContext } from "./ScriptEditorContext"; import { useVimEditor } from "./useVimEditor"; import { useCallback } from "react"; +import { type AST, getFileType, parseAST } from "../../utils/ScriptTransformer"; +import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes"; +import { hasScriptExtension, isLegacyScript } from "../../Paths/ScriptFilePath"; interface IProps { // Map of filename -> code @@ -44,7 +47,7 @@ function Root(props: IProps): React.ReactElement { const rerender = useRerender(); const editorRef = useRef(null); - const { options, updateRAM, startUpdatingRAM, finishUpdatingRAM } = useScriptEditorContext(); + const { options, showRAMError, updateRAM, startUpdatingRAM, finishUpdatingRAM } = useScriptEditorContext(); let decorations: monaco.editor.IEditorDecorationsCollection | undefined; @@ -112,11 +115,14 @@ function Root(props: IProps): React.ReactElement { return () => document.removeEventListener("keydown", keydown); }, [save]); - function infLoop(newCode: string): void { - if (editorRef.current === null || currentScript === null) return; - if (!decorations) decorations = editorRef.current.createDecorationsCollection(); - if (!currentScript.path.endsWith(".js")) return; - const possibleLines = checkInfiniteLoop(newCode); + function infLoop(ast: AST, code: string): void { + if (editorRef.current === null || currentScript === null || isLegacyScript(currentScript.path)) { + return; + } + if (!decorations) { + decorations = editorRef.current.createDecorationsCollection(); + } + const possibleLines = checkInfiniteLoop(ast, code); if (possibleLines.length !== 0) { decorations.set( possibleLines.map((awaitWarning) => ({ @@ -131,21 +137,34 @@ function Root(props: IProps): React.ReactElement { glyphMarginClassName: "myGlyphMarginClass", glyphMarginHoverMessage: { value: - "Possible infinite loop, await something. If this is a false-positive, use `// @ignore-infinite` to suppress.", + "Possible infinite loop, await something. If this is a false positive, use `// @ignore-infinite` to suppress.", }, }, })), ); - } else decorations.clear(); + } else { + decorations.clear(); + } } const debouncedCodeParsing = debounce((newCode: string) => { - infLoop(newCode); - updateRAM( - !currentScript || currentScript.isTxt ? null : newCode, - currentScript && currentScript.path, - currentScript && GetServer(currentScript.hostname), - ); + let server; + if (!currentScript || !hasScriptExtension(currentScript.path) || !(server = GetServer(currentScript.hostname))) { + showRAMError(); + return; + } + let ast; + try { + ast = parseAST(newCode, getFileType(currentScript.path)); + } catch (error) { + showRAMError({ + errorCode: RamCalculationErrorCode.SyntaxError, + errorMessage: error instanceof Error ? error.message : String(error), + }); + return; + } + infLoop(ast, newCode); + updateRAM(ast, currentScript.path, server); finishUpdatingRAM(); }, 300); diff --git a/src/ScriptEditor/ui/utils.ts b/src/ScriptEditor/ui/utils.ts index 3610c13071..c00c9c8c0b 100644 --- a/src/ScriptEditor/ui/utils.ts +++ b/src/ScriptEditor/ui/utils.ts @@ -1,6 +1,7 @@ import { GetServer } from "../../Server/AllServers"; import { editor, Uri } from "monaco-editor"; import { OpenScript } from "./OpenScript"; +import { getFileType, FileType } from "../../utils/ScriptTransformer"; function getServerCode(scripts: OpenScript[], index: number): string | null { const openScript = scripts[index]; @@ -26,7 +27,29 @@ function makeModel(hostname: string, filename: string, code: string) { scheme: "file", path: `${hostname}/${filename}`, }); - const language = filename.endsWith(".txt") ? "plaintext" : filename.endsWith(".json") ? "json" : "javascript"; + let language; + const fileType = getFileType(filename); + switch (fileType) { + case FileType.PLAINTEXT: + language = "plaintext"; + break; + case FileType.JSON: + language = "json"; + break; + case FileType.JS: + case FileType.JSX: + language = "javascript"; + break; + case FileType.TS: + case FileType.TSX: + language = "typescript"; + break; + case FileType.NS1: + language = "javascript"; + break; + default: + throw new Error(`Invalid file type: ${fileType}. Filename: ${filename}.`); + } //if somehow a model already exist return it return editor.getModel(uri) ?? editor.createModel(code, language, uri); } diff --git a/src/Terminal/HelpText.ts b/src/Terminal/HelpText.ts index a63d053d62..87f8ead125 100644 --- a/src/Terminal/HelpText.ts +++ b/src/Terminal/HelpText.ts @@ -50,10 +50,11 @@ const TemplatedHelpTexts: Record string[]> = { return [ `Usage: ${command} [file names...] | [glob]`, ` `, - `Opens up the specified file(s) in the Script Editor. Only scripts (.js, or .script) or text files (.txt) `, - `can be edited using the Script Editor. If a file does not exist a new one will be created`, + `Opens up the specified file(s) in the Script Editor. Only scripts (.js, .jsx, .ts, .tsx, .script) `, + `or text files (.txt, .json) can be edited using the Script Editor. If a file does not exist, a new `, + `one will be created.`, ` `, - `If provided a glob as the only argument, ${command} can spider directories and open all matching `, + `If a glob is provided as the only argument, ${command} can crawl directories and open all matching `, `files at once. ${command} cannot create files using globs, so your scripts must already exist.`, ` `, `Examples:`, @@ -413,8 +414,8 @@ export const HelpTexts: Record = { "Usage: scp [file names...] [target server]", " ", "Copies the specified file(s) from the current server to the target server. ", - "This command only works for script files (.script or .js extension), literature files (.lit extension), ", - "and text files (.txt extension). ", + "This command only works for script files (.js, .jsx, .ts, .tsx, .script), text files (.txt, .json), ", + "and literature files (.lit).", "The second argument passed in must be the hostname or IP of the target server. Examples:", " ", " scp foo.script n00dles", @@ -475,8 +476,8 @@ export const HelpTexts: Record = { "Usage: wget [url] [target file]", " ", "Retrieves data from a URL and downloads it to a file on the current server. The data can only ", - "be downloaded to a script (.script or .js) or a text file (.txt). If the file already exists, ", - "it will be overwritten by this command.", + "be downloaded to a script (.js, .jsx, .ts, .tsx, .script) or a text file (.txt, .json).", + "If the file already exists, it will be overwritten by this command.", " ", "Note that it will not be possible to download data from many websites because they do not allow ", "cross-origin resource sharing (CORS). Example:", diff --git a/src/Terminal/commands/cat.ts b/src/Terminal/commands/cat.ts index 7a18c0c4e6..5efd2e272b 100644 --- a/src/Terminal/commands/cat.ts +++ b/src/Terminal/commands/cat.ts @@ -20,7 +20,9 @@ export function cat(args: (string | number | boolean)[], server: BaseServer): vo return dialogBoxCreate(`${file.filename}\n\n${file.content}`); } if (!path.endsWith(".msg") && !path.endsWith(".lit")) { - return Terminal.error("Invalid file extension. Filename must end with .msg, .txt, .lit, .script or .js"); + return Terminal.error( + "Invalid file extension. Filename must end with .msg, .lit, a script extension (.js, .jsx, .ts, .tsx, .script) or a text extension (.txt, .json)", + ); } // Message diff --git a/src/Terminal/commands/common/editor.ts b/src/Terminal/commands/common/editor.ts index 5fc9124a9c..53190bbb6e 100644 --- a/src/Terminal/commands/common/editor.ts +++ b/src/Terminal/commands/common/editor.ts @@ -2,10 +2,11 @@ import { Terminal } from "../../../Terminal"; import { ScriptEditorRouteOptions, Page } from "../../../ui/Router"; import { Router } from "../../../ui/GameRoot"; import { BaseServer } from "../../../Server/BaseServer"; -import { ScriptFilePath, hasScriptExtension } from "../../../Paths/ScriptFilePath"; +import { type ScriptFilePath, hasScriptExtension, isLegacyScript } from "../../../Paths/ScriptFilePath"; import { TextFilePath, hasTextExtension } from "../../../Paths/TextFilePath"; import { getGlobbedFileMap } from "../../../Paths/GlobbedFiles"; import { sendDeprecationNotice } from "./deprecation"; +import { getFileType, getFileTypeFeature } from "../../../utils/ScriptTransformer"; // 2.3: Globbing implementation was removed from this file. Globbing will be reintroduced as broader functionality and integrated here. @@ -14,14 +15,22 @@ interface EditorParameters { server: BaseServer; } -function isNs2(filename: string): boolean { - return filename.endsWith(".js"); -} +function getScriptTemplate(path: string): string { + if (isLegacyScript(path)) { + return ""; + } + const fileTypeFeature = getFileTypeFeature(getFileType(path)); + if (fileTypeFeature.isTypeScript) { + return `export async function main(ns: NS) { -const newNs2Template = `/** @param {NS} ns */ +}`; + } else { + return `/** @param {NS} ns */ export async function main(ns) { }`; + } +} export function commonEditor( command: string, @@ -30,14 +39,16 @@ export function commonEditor( ): void { if (args.length < 1) return Terminal.error(`Incorrect usage of ${command} command. Usage: ${command} [scriptname]`); const files = new Map(); - let hasNs1 = false; + let hasLegacyScript = false; for (const arg of args) { const pattern = String(arg); // Glob of existing files if (pattern.includes("*") || pattern.includes("?")) { for (const [path, file] of getGlobbedFileMap(pattern, server, Terminal.currDir)) { - if (path.endsWith(".script")) hasNs1 = true; + if (isLegacyScript(path)) { + hasLegacyScript = true; + } files.set(path, file.content); } continue; @@ -49,12 +60,13 @@ export function commonEditor( if (!hasScriptExtension(path) && !hasTextExtension(path)) { return Terminal.error(`${command}: Only scripts or text files can be edited. Invalid file type: ${arg}`); } - if (path.endsWith(".script")) hasNs1 = true; + if (isLegacyScript(path)) { + hasLegacyScript = true; + } const file = server.getContentFile(path); - const content = file ? file.content : isNs2(path) ? newNs2Template : ""; - files.set(path, content); + files.set(path, file ? file.content : getScriptTemplate(path)); } - if (hasNs1) { + if (hasLegacyScript) { sendDeprecationNotice(); } Router.toPage(Page.ScriptEditor, { files, options }); diff --git a/src/Terminal/commands/run.ts b/src/Terminal/commands/run.ts index 6fb981e9af..4fd486d281 100644 --- a/src/Terminal/commands/run.ts +++ b/src/Terminal/commands/run.ts @@ -24,5 +24,5 @@ export function run(args: (string | number | boolean)[], server: BaseServer): vo } else if (hasProgramExtension(path)) { return runProgram(path, args, server); } - Terminal.error(`Invalid file extension. Only .js, .script, .cct, and .exe files can be ran.`); + Terminal.error(`Invalid file extension. Only .js, .jsx, .ts, .tsx, .script, .cct, and .exe files can be run.`); } diff --git a/src/Terminal/commands/runScript.ts b/src/Terminal/commands/runScript.ts index f2a7be22cd..4a217f2933 100644 --- a/src/Terminal/commands/runScript.ts +++ b/src/Terminal/commands/runScript.ts @@ -7,7 +7,7 @@ import libarg from "arg"; import { formatRam } from "../../ui/formatNumber"; import { ScriptArg } from "@nsdefs"; import { isPositiveInteger } from "../../types"; -import { ScriptFilePath } from "../../Paths/ScriptFilePath"; +import { ScriptFilePath, isLegacyScript } from "../../Paths/ScriptFilePath"; import { sendDeprecationNotice } from "./common/deprecation"; import { RamCostConstants } from "../../Netscript/RamCostGenerator"; @@ -60,7 +60,7 @@ export function runScript(path: ScriptFilePath, commandArgs: (string | number | const success = startWorkerScript(runningScript, server); if (!success) return Terminal.error(`Failed to start script`); - if (path.endsWith(".script")) { + if (isLegacyScript(path)) { sendDeprecationNotice(); } Terminal.print( diff --git a/src/Terminal/commands/scp.ts b/src/Terminal/commands/scp.ts index c56c82f0cd..d574b5650a 100644 --- a/src/Terminal/commands/scp.ts +++ b/src/Terminal/commands/scp.ts @@ -36,7 +36,7 @@ export function scp(args: (string | number | boolean)[], server: BaseServer): vo // Error for invalid filetype if (!hasScriptExtension(path) && !hasTextExtension(path)) { return Terminal.error( - `scp failed: ${path} has invalid extension. scp only works for scripts (.js or .script), text files (.txt), and literature files (.lit)`, + `scp failed: ${path} has invalid extension. scp only works for scripts (.js, .jsx, .ts, .tsx, .script), text files (.txt, .json), and literature files (.lit)`, ); } const sourceContentFile = server.getContentFile(path); diff --git a/src/Terminal/getTabCompletionPossibilities.ts b/src/Terminal/getTabCompletionPossibilities.ts index 223b59fb86..93560c55fa 100644 --- a/src/Terminal/getTabCompletionPossibilities.ts +++ b/src/Terminal/getTabCompletionPossibilities.ts @@ -9,7 +9,7 @@ import { Flags } from "../NetscriptFunctions/Flags"; import { AutocompleteData } from "@nsdefs"; import libarg from "arg"; import { getAllDirectories, resolveDirectory, root } from "../Paths/Directory"; -import { resolveScriptFilePath } from "../Paths/ScriptFilePath"; +import { isLegacyScript, resolveScriptFilePath } from "../Paths/ScriptFilePath"; // TODO: this shouldn't be hardcoded in two places with no typechecks to verify equivalence // An array of all Terminal commands @@ -298,7 +298,7 @@ export async function getTabCompletionPossibilities(terminalText: string, baseDi } const filepath = resolveScriptFilePath(filename, baseDir); if (!filepath) return; // Not a script path. - if (filepath.endsWith(".script")) return; // Doesn't work with ns1. + if (isLegacyScript(filepath)) return; // Doesn't work with ns1. const script = currServ.scripts.get(filepath); if (!script) return; // Doesn't exist. diff --git a/src/ThirdParty/acorn-jsx-walk.d.ts b/src/ThirdParty/acorn-jsx-walk.d.ts new file mode 100644 index 0000000000..41b4d7b50d --- /dev/null +++ b/src/ThirdParty/acorn-jsx-walk.d.ts @@ -0,0 +1 @@ +declare module "acorn-jsx-walk"; diff --git a/src/ThirdParty/acorn-typescript-walk/index.ts b/src/ThirdParty/acorn-typescript-walk/index.ts new file mode 100644 index 0000000000..91a575f5e4 --- /dev/null +++ b/src/ThirdParty/acorn-typescript-walk/index.ts @@ -0,0 +1,88 @@ +/** + * From isTypeScript() + * + * https://github.com/babel/babel/blob/main/packages/babel-types/src/validators/generated/index.ts + */ +const typescriptNodeTypes = [ + "TSParameterProperty", + "TSDeclareFunction", + "TSDeclareMethod", + "TSQualifiedName", + "TSCallSignatureDeclaration", + "TSConstructSignatureDeclaration", + "TSPropertySignature", + "TSMethodSignature", + "TSIndexSignature", + "TSAnyKeyword", + "TSBooleanKeyword", + "TSBigIntKeyword", + "TSIntrinsicKeyword", + "TSNeverKeyword", + "TSNullKeyword", + "TSNumberKeyword", + "TSObjectKeyword", + "TSStringKeyword", + "TSSymbolKeyword", + "TSUndefinedKeyword", + "TSUnknownKeyword", + "TSVoidKeyword", + "TSThisType", + "TSFunctionType", + "TSConstructorType", + "TSTypeReference", + "TSTypePredicate", + "TSTypeQuery", + "TSTypeLiteral", + "TSArrayType", + "TSTupleType", + "TSOptionalType", + "TSRestType", + "TSNamedTupleMember", + "TSUnionType", + "TSIntersectionType", + "TSConditionalType", + "TSInferType", + "TSParenthesizedType", + "TSTypeOperator", + "TSIndexedAccessType", + "TSMappedType", + "TSLiteralType", + "TSExpressionWithTypeArguments", + "TSInterfaceDeclaration", + "TSInterfaceBody", + "TSTypeAliasDeclaration", + "TSInstantiationExpression", + "TSAsExpression", + "TSSatisfiesExpression", + "TSTypeAssertion", + "TSEnumDeclaration", + "TSEnumMember", + "TSModuleDeclaration", + "TSModuleBlock", + "TSImportType", + "TSImportEqualsDeclaration", + "TSExternalModuleReference", + "TSNonNullExpression", + "TSExportAssignment", + "TSNamespaceExportDeclaration", + "TSTypeAnnotation", + "TSTypeParameterInstantiation", + "TSTypeParameterDeclaration", + "TSTypeParameter", +]; + +export function extendAcornWalkForTypeScriptNodes(base: any) { + // By default, we ignore all TypeScript nodes. + for (const nodeType of typescriptNodeTypes) { + if (base[nodeType]) { + continue; + } + base[nodeType] = base.EmptyStatement; + } + // Only walk relevant TypeScript nodes. + base.TSModuleBlock = base.BlockStatement; + base.TSAsExpression = base.TSNonNullExpression = base.ExpressionStatement; + base.TSModuleDeclaration = (node: any, state: any, callback: any) => { + callback(node.body, state); + }; +} diff --git a/src/ui/InteractiveTutorial/InteractiveTutorialRoot.tsx b/src/ui/InteractiveTutorial/InteractiveTutorialRoot.tsx index 86966943e1..b3f05911e2 100644 --- a/src/ui/InteractiveTutorial/InteractiveTutorialRoot.tsx +++ b/src/ui/InteractiveTutorial/InteractiveTutorialRoot.tsx @@ -321,7 +321,10 @@ export function InteractiveTutorialRoot(): React.ReactElement { {"[home /]> nano"} - Scripts must end with the .js extension. Let's make a script now by entering + + Scripts must end with a script extension (.js, .jsx, .ts, .tsx, .script). Let's make a script now by + entering + {`[home /]> nano ${tutorialScriptName}`} ), diff --git a/src/ui/LoadingScreen.tsx b/src/ui/LoadingScreen.tsx index aac89848b5..09e35dcb32 100644 --- a/src/ui/LoadingScreen.tsx +++ b/src/ui/LoadingScreen.tsx @@ -11,6 +11,7 @@ import { CONSTANTS } from "../Constants"; import { ActivateRecoveryMode } from "./React/RecoveryRoot"; import { hash } from "../hash/hash"; import { pushGameReady } from "../Electron"; +import initSwc from "@swc/wasm-web"; export function LoadingScreen(): React.ReactElement { const [show, setShow] = useState(false); @@ -33,6 +34,7 @@ export function LoadingScreen(): React.ReactElement { useEffect(() => { load().then(async (saveData) => { try { + await initSwc(); await Engine.load(saveData); } catch (error) { console.error(error); diff --git a/src/utils/ScriptTransformer.ts b/src/utils/ScriptTransformer.ts new file mode 100644 index 0000000000..45d461e3e6 --- /dev/null +++ b/src/utils/ScriptTransformer.ts @@ -0,0 +1,159 @@ +import * as babel from "@babel/standalone"; +import { transformSync, type ParserConfig } from "@swc/wasm-web"; +import * as acorn from "acorn"; +import { resolveScriptFilePath, validScriptExtensions, type ScriptFilePath } from "../Paths/ScriptFilePath"; +import type { Script } from "../Script/Script"; + +// This is only for testing. It will be removed after we decide between Babel and SWC. +declare global { + // eslint-disable-next-line no-var + var forceBabelTransform: boolean; +} + +export type AcornASTProgram = acorn.Program; +export type BabelASTProgram = object; +export type AST = AcornASTProgram | BabelASTProgram; + +export enum FileType { + PLAINTEXT, + JSON, + JS, + JSX, + TS, + TSX, + NS1, +} + +export interface FileTypeFeature { + isReact: boolean; + isTypeScript: boolean; +} + +export class ModuleResolutionError extends Error {} + +const supportedFileTypes = [FileType.JSX, FileType.TS, FileType.TSX] as const; + +export function getFileType(filename: string): FileType { + const extension = filename.substring(filename.lastIndexOf(".") + 1); + switch (extension) { + case "txt": + return FileType.PLAINTEXT; + case "json": + return FileType.JSON; + case "js": + return FileType.JS; + case "jsx": + return FileType.JSX; + case "ts": + return FileType.TS; + case "tsx": + return FileType.TSX; + case "script": + return FileType.NS1; + default: + throw new Error(`Invalid extension: ${extension}. Filename: ${filename}.`); + } +} + +export function getFileTypeFeature(fileType: FileType): FileTypeFeature { + const result: FileTypeFeature = { + isReact: false, + isTypeScript: false, + }; + if (fileType === FileType.JSX || fileType === FileType.TSX) { + result.isReact = true; + } + if (fileType === FileType.TS || fileType === FileType.TSX) { + result.isTypeScript = true; + } + return result; +} + +export function parseAST(code: string, fileType: FileType): AST { + const fileTypeFeature = getFileTypeFeature(fileType); + let ast: AST; + if (fileType === FileType.JS) { + ast = acorn.parse(code, { sourceType: "module", ecmaVersion: "latest" }); + } else { + const plugins = []; + if (fileTypeFeature.isReact) { + plugins.push("jsx"); + } + if (fileTypeFeature.isTypeScript) { + plugins.push("typescript"); + } + ast = babel.packages.parser.parse(code, { + sourceType: "module", + ecmaVersion: "latest", + plugins: [["estree", { classFeatures: true }], ...plugins], + }).program; + } + return ast; +} + +/** + * Simple module resolution algorithm: + * - Try each extension in validScriptExtensions + * - Return the first script found + */ +export function getModuleScript( + moduleName: string, + baseModule: ScriptFilePath, + scripts: Map, +): Script { + let script; + for (const extension of validScriptExtensions) { + const filename = resolveScriptFilePath(moduleName, baseModule, extension); + if (!filename) { + throw new ModuleResolutionError(`Invalid module: "${moduleName}". Base module: "${baseModule}".`); + } + script = scripts.get(filename); + if (script) { + break; + } + } + if (!script) { + throw new ModuleResolutionError(`Invalid module: "${moduleName}". Base module: "${baseModule}".`); + } + return script; +} + +export function transformScript(filename: string, code: string, fileType: FileType): string | null | undefined { + if (supportedFileTypes.every((v) => v !== fileType)) { + throw new Error(`Invalid file type: ${fileType}`); + } + const fileTypeFeature = getFileTypeFeature(fileType); + // This is only for testing. It will be removed after we decide between Babel and SWC. + if (globalThis.forceBabelTransform) { + const presets = []; + if (fileTypeFeature.isReact) { + presets.push("react"); + } + if (fileTypeFeature.isTypeScript) { + presets.push("typescript"); + } + return babel.transform(code, { filename: filename, presets: presets }).code; + } + let parserConfig: ParserConfig; + if (fileTypeFeature.isTypeScript) { + parserConfig = { + syntax: "typescript", + }; + if (fileTypeFeature.isReact) { + parserConfig.tsx = true; + } + } else { + parserConfig = { + syntax: "ecmascript", + }; + if (fileTypeFeature.isReact) { + parserConfig.jsx = true; + } + } + return transformSync(code, { + jsc: { + parser: parserConfig, + target: "es2020", + }, + }).code; +} diff --git a/src/utils/v2APIBreak.ts b/src/utils/v2APIBreak.ts index 2b2897c15c..68e331022f 100644 --- a/src/utils/v2APIBreak.ts +++ b/src/utils/v2APIBreak.ts @@ -1,3 +1,4 @@ +import { isLegacyScript } from "../Paths/ScriptFilePath"; import { TextFilePath } from "../Paths/TextFilePath"; import { saveObject } from "../SaveObject"; import { Script } from "../Script/Script"; @@ -277,7 +278,7 @@ const processScript = (rules: IRule[], script: Script) => { for (let i = 0; i < lines.length; i++) { for (const rule of rules) { const line = lines[i]; - const match = script.filename.endsWith(".script") ? rule.matchScript ?? rule.matchJS : rule.matchJS; + const match = isLegacyScript(script.filename) ? rule.matchScript ?? rule.matchJS : rule.matchJS; if (line.match(match)) { rule.offenders.push({ file: script.filename, diff --git a/test/jest/Netscript/RamCalculation.test.ts b/test/jest/Netscript/RamCalculation.test.ts index e505ac233e..31570278f6 100644 --- a/test/jest/Netscript/RamCalculation.test.ts +++ b/test/jest/Netscript/RamCalculation.test.ts @@ -72,7 +72,6 @@ describe("Netscript RAM Calculation/Generation Tests", function () { extraLayerCost = 0, ) { const code = `${fnPath.join(".")}();\n`.repeat(3); - const filename = "testfile.js" as ScriptFilePath; const fnName = fnPath[fnPath.length - 1]; const server = "testserver"; @@ -80,7 +79,7 @@ describe("Netscript RAM Calculation/Generation Tests", function () { expect(getRamCost(fnPath, true)).toEqual(expectedRamCost); // Static ram check - const staticCost = calculateRamUsage(code, filename, new Map(), server).cost; + const staticCost = calculateRamUsage(code, `${fnName}.js` as ScriptFilePath, server, new Map()).cost; expect(staticCost).toBeCloseTo(Math.min(baseCost + expectedRamCost + extraLayerCost, maxCost)); // reset workerScript for dynamic check diff --git a/test/jest/Netscript/StaticRamParsingCalculation.test.ts b/test/jest/Netscript/StaticRamParsingCalculation.test.ts index ebe3a09978..f71b4f4070 100644 --- a/test/jest/Netscript/StaticRamParsingCalculation.test.ts +++ b/test/jest/Netscript/StaticRamParsingCalculation.test.ts @@ -28,7 +28,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { const code = ` export async function main(ns) { } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, 0); }); @@ -38,7 +38,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { ns.print("Slum snakes r00l!"); } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, 0); }); @@ -48,7 +48,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await ns.hack("joesguns"); } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, HackCost); }); @@ -58,7 +58,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await X.hack("joesguns"); } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, HackCost); }); @@ -69,7 +69,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await ns.hack("joesguns"); } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, HackCost); }); @@ -80,7 +80,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await ns.grow("joesguns"); } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, HackCost + GrowCost); }); @@ -93,7 +93,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { await ns.hack("joesguns"); } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, HackCost); }); @@ -108,7 +108,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { async doHacking() { await this.ns.hack("joesguns"); } } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, HackCost); }); @@ -123,7 +123,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { async doHacking() { await this.#ns.hack("joesguns"); } } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, HackCost); }); }); @@ -136,7 +136,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { } function get() { return 0; } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, 0); }); @@ -147,7 +147,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { } function purchaseNode() { return 0; } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; // Works at present, because the parser checks the namespace only, not the function name expectCost(calculated, 0); }); @@ -160,7 +160,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { } function getTask() { return 0; } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, 0); }); }); @@ -172,7 +172,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { ns.hacknet.purchaseNode(0); } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, HacknetCost); }); @@ -182,7 +182,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { ns.sleeve.getTask(3); } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, SleeveGetTaskCost); }); }); @@ -203,8 +203,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () { const calculated = calculateRamUsage( code, filename, - new Map([["libTest.js" as ScriptFilePath, lib]]), server, + new Map([["libTest.js" as ScriptFilePath, lib]]), ).cost; expectCost(calculated, 0); }); @@ -224,8 +224,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () { const calculated = calculateRamUsage( code, filename, - new Map([["libTest.js" as ScriptFilePath, lib]]), server, + new Map([["libTest.js" as ScriptFilePath, lib]]), ).cost; expectCost(calculated, HackCost); }); @@ -246,8 +246,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () { const calculated = calculateRamUsage( code, filename, - new Map([["libTest.js" as ScriptFilePath, lib]]), server, + new Map([["libTest.js" as ScriptFilePath, lib]]), ).cost; expectCost(calculated, HackCost); }); @@ -268,8 +268,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () { const calculated = calculateRamUsage( code, filename, - new Map([["libTest.js" as ScriptFilePath, lib]]), server, + new Map([["libTest.js" as ScriptFilePath, lib]]), ).cost; expectCost(calculated, HackCost + GrowCost); }); @@ -291,7 +291,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () { ${lines.join("\n")}; } `; - const calculated = calculateRamUsage(code, filename, new Map(), server).cost; + const calculated = calculateRamUsage(code, filename, server, new Map()).cost; expectCost(calculated, MaxCost); }); @@ -316,8 +316,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () { const calculated = calculateRamUsage( code, filename, - new Map([["libTest.js" as ScriptFilePath, lib]]), server, + new Map([["libTest.js" as ScriptFilePath, lib]]), ).cost; expectCost(calculated, HackCost); }); @@ -347,8 +347,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () { const calculated = calculateRamUsage( code, filename, - new Map([["libTest.js" as ScriptFilePath, lib]]), server, + new Map([["libTest.js" as ScriptFilePath, lib]]), ).cost; expectCost(calculated, GrowCost); }); @@ -370,8 +370,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () { const calculated = calculateRamUsage( code, folderFilename, - new Map([["test/libTest.js" as ScriptFilePath, lib]]), server, + new Map([["test/libTest.js" as ScriptFilePath, lib]]), ).cost; expectCost(calculated, HackCost); }); @@ -404,11 +404,11 @@ describe("Parsing NetScript code to work out static RAM costs", function () { const calculated = calculateRamUsage( code, folderFilename, + server, new Map([ [libNameOne, libScriptOne], [libNameTwo, libScriptTwo], ]), - server, ).cost; expectCost(calculated, HackCost); }); @@ -449,12 +449,12 @@ describe("Parsing NetScript code to work out static RAM costs", function () { const calculated = calculateRamUsage( code, folderFilename, + server, new Map([ [libNameOne, libScriptOne], [libNameTwo, libScriptTwo], [incorrect_libNameTwo, incorrect_libScriptTwo], ]), - server, ).cost; expectCost(calculated, HackCost); });