From 864613c61632947be6ba0215253194c0a56d6259 Mon Sep 17 00:00:00 2001 From: catloversg <152669316+catloversg@users.noreply.github.com> Date: Mon, 15 Jul 2024 04:47:10 +0700 Subject: [PATCH] MISC: Support JSX, TS, TSX script files (#1216) --- jest.config.js | 1 + markdown/bitburner.ns.read.md | 2 +- markdown/bitburner.ns.wget.md | 2 +- markdown/bitburner.ns.write.md | 2 +- package-lock.json | 261 +++++++++++++++++- package.json | 9 +- src/@types/global.d.ts | 20 ++ src/Achievements/Achievements.ts | 3 +- src/Documentation/doc/basic/programs.md | 2 +- src/Documentation/doc/basic/scripts.md | 2 +- src/NetscriptJSEvaluator.ts | 36 ++- src/NetscriptWorker.ts | 6 +- src/Paths/ProgramFilePath.ts | 2 +- src/Paths/ScriptFilePath.ts | 12 +- src/Paths/TextFilePath.ts | 2 +- src/Script/RamCalculations.ts | 173 +++++++----- src/Script/Script.ts | 8 +- src/ScriptEditor/NetscriptDefinitions.d.ts | 6 +- src/ScriptEditor/ScriptEditor.ts | 12 + src/ScriptEditor/ui/ScriptEditorContext.tsx | 66 +++-- src/ScriptEditor/ui/ScriptEditorRoot.tsx | 47 +++- src/ScriptEditor/ui/utils.ts | 25 +- src/Terminal/HelpText.ts | 15 +- src/Terminal/commands/cat.ts | 4 +- src/Terminal/commands/common/editor.ts | 34 ++- src/Terminal/commands/run.ts | 2 +- src/Terminal/commands/runScript.ts | 4 +- src/Terminal/commands/scp.ts | 2 +- src/Terminal/getTabCompletionPossibilities.ts | 4 +- src/ThirdParty/acorn-jsx-walk.d.ts | 1 + src/ThirdParty/acorn-typescript-walk/index.ts | 88 ++++++ .../InteractiveTutorialRoot.tsx | 5 +- src/ui/LoadingScreen.tsx | 2 + src/utils/ScriptTransformer.ts | 176 ++++++++++++ src/utils/v2APIBreak.ts | 3 +- test/jest/Netscript/RamCalculation.test.ts | 3 +- .../StaticRamParsingCalculation.test.ts | 48 ++-- webpack.config.js | 6 + 38 files changed, 895 insertions(+), 201 deletions(-) create mode 100644 src/ThirdParty/acorn-jsx-walk.d.ts create mode 100644 src/ThirdParty/acorn-typescript-walk/index.ts create mode 100644 src/utils/ScriptTransformer.ts diff --git a/jest.config.js b/jest.config.js index c09b6cdc27..4e38aae385 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 420983723e..f37404eb2d 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", @@ -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", @@ -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 7924625028..d6615180e9 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", @@ -55,6 +58,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", 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 85da3e7331..bbf32092a3 100644 --- a/src/Achievements/Achievements.ts +++ b/src/Achievements/Achievements.ts @@ -30,6 +30,7 @@ import { workerScripts } from "../Netscript/WorkerScripts"; import { getRecordValues } from "../Types/Record"; import { ServerConstants } from "../Server/data/Constants"; import { canAccessBitNodeFeature, isBitNodeFinished, knowAboutBitverse } from "../BitNode/BitNodeUtils"; +import { isLegacyScript } from "../Paths/ScriptFilePath"; // Unable to correctly cast the JSON data into AchievementDataJson type otherwise... const achievementData = ((data)).achievements; @@ -190,7 +191,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 4fc3e3ed9f..ccaeaf78ee 100644 --- a/src/Script/RamCalculations.ts +++ b/src/Script/RamCalculations.ts @@ -1,21 +1,30 @@ /** * Implements RAM Calculation functionality. * - * Uses the acorn.js library to parse a script's code into an AST and - * recursively walk through that AST, calculating RAM usage along - * the way + * Uses acorn-walk to recursively walk through the AST, calculating RAM usage along the way. */ import * as walk from "acorn-walk"; -import acorn, { parse } from "acorn"; +import type * 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 { roundToTwo } from "../utils/helpers/roundToTwo"; +import { + type AST, + type FileTypeFeature, + getFileType, + getFileTypeFeature, + getModuleScript, + parseAST, + ModuleResolutionError, +} from "../utils/ScriptTransformer"; export interface RamUsageEntry { type: "ns" | "dom" | "fn" | "misc"; @@ -39,6 +48,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"; @@ -61,17 +76,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. @@ -91,14 +107,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); } } @@ -107,24 +123,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 @@ -136,7 +168,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"); + } if (ref.endsWith(specialReferenceRAM)) { if (ref !== initialModule + specialReferenceRAM) { @@ -169,13 +203,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); + } } } @@ -194,14 +232,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; }; @@ -222,14 +264,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"); } @@ -250,7 +285,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) => { @@ -282,8 +317,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; @@ -402,17 +441,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 @@ -472,27 +511,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 99a55df6e1..86f0f9d912 100644 --- a/src/ScriptEditor/NetscriptDefinitions.d.ts +++ b/src/ScriptEditor/NetscriptDefinitions.d.ts @@ -6919,7 +6919,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 @@ -6965,7 +6965,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. @@ -7430,7 +7430,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 1dd2d68a2a..9a5c50d84f 100644 --- a/src/ScriptEditor/ScriptEditor.ts +++ b/src/ScriptEditor/ScriptEditor.ts @@ -71,6 +71,18 @@ export class ScriptEditor { languageDefaults.addExtraLib(reactTypes, "react.d.ts"); languageDefaults.addExtraLib(reactDomTypes, "react-dom.d.ts"); } + monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ + ...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(), + jsx: monaco.languages.typescript.JsxEmit.ReactJSX, + allowUmdGlobalAccess: true, + }); + /** + * Ignore these errors in the editor: + * - Cannot find module ''. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option?(2792) + */ + monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ + diagnosticCodesToIgnore: [2792], + }); 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 11023a7c0e..5819740e1f 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 }: { children: React.Reac 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 }: { children: React.Reac } 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", ""]]); } }; @@ -96,7 +106,17 @@ export function ScriptEditorContextProvider({ children }: { children: React.Reac return ( {children} diff --git a/src/ScriptEditor/ui/ScriptEditorRoot.tsx b/src/ScriptEditor/ui/ScriptEditorRoot.tsx index 0ec659157b..0a8d81eb12 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 { updateRAM, startUpdatingRAM, finishUpdatingRAM } = useScriptEditorContext(); + const { 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 8eb183872b..1988667c3f 100644 --- a/src/Terminal/HelpText.ts +++ b/src/Terminal/HelpText.ts @@ -52,10 +52,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:`, @@ -456,8 +457,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", @@ -518,8 +519,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 93967923da..d34fcdbb67 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 { roundToTwo } from "../../utils/helpers/roundToTwo"; import { RamCostConstants } from "../../Netscript/RamCostGenerator"; @@ -61,7 +61,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 5b6f88ef60..4931e2cde4 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"; import { enums } from "../NetscriptFunctions"; // TODO: this shouldn't be hardcoded in two places with no typechecks to verify equivalence @@ -299,7 +299,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 350b51759a..700b829b2e 100644 --- a/src/ui/InteractiveTutorial/InteractiveTutorialRoot.tsx +++ b/src/ui/InteractiveTutorial/InteractiveTutorialRoot.tsx @@ -318,7 +318,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..6caa6e2308 --- /dev/null +++ b/src/utils/ScriptTransformer.ts @@ -0,0 +1,176 @@ +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; + /** + * acorn is much faster than babel-parser, especially when parsing many big JS files, so we use it to parse the AST of + * JS code. babel-parser is only useful when we have to parse JSX and TypeScript. + */ + 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", + /** + * The usage of the "estree" plugin is mandatory. We use acorn-walk to walk the AST. acorn-walk only supports the + * ESTree AST format, but babel-parser uses the Babel AST format by default. + */ + 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; +} + +/** + * This function must be synchronous to avoid race conditions. Check https://github.com/bitburner-official/bitburner-src/pull/1173#issuecomment-2026940461 + * for more information. + * + * @param filename + * @param code + * @param fileType + * @returns + */ +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 9536cdb700..eb49dc7351 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); }); diff --git a/webpack.config.js b/webpack.config.js index 876bc47454..7528697217 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -192,5 +192,11 @@ module.exports = (env, argv) => { fallback: { crypto: false }, }, stats: statsConfig, + ignoreWarnings: [ + { + module: /@babel\/standalone/, + message: /Critical dependency: the request of a dependency is an expression/, + }, + ], }; };