MISC: Support JSX, TS, TSX script files (#1216)

This commit is contained in:
catloversg 2024-07-15 04:47:10 +07:00 committed by GitHub
parent 783120c886
commit 864613c616
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 895 additions and 201 deletions

@ -19,5 +19,6 @@ module.exports = {
"^monaco-editor$": "<rootDir>/test/__mocks__/NullMock.js", "^monaco-editor$": "<rootDir>/test/__mocks__/NullMock.js",
"^monaco-vim$": "<rootDir>/test/__mocks__/NullMock.js", "^monaco-vim$": "<rootDir>/test/__mocks__/NullMock.js",
"/utils/Protections$": "<rootDir>/test/__mocks__/NullMock.js", "/utils/Protections$": "<rootDir>/test/__mocks__/NullMock.js",
"@swc/wasm-web": "@swc/core",
}, },
}; };

@ -28,7 +28,7 @@ Data in the specified text file.
RAM cost: 0 GB 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. This function will return the data in the specified file. If the file does not exist, an empty string will be returned.

@ -30,7 +30,7 @@ True if the data was successfully retrieved from the URL, false otherwise.
RAM cost: 0 GB 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 Promises 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). IMPORTANT: This is an asynchronous function that returns a Promise. The Promises 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).

@ -28,7 +28,7 @@ void
RAM cost: 0 GB 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. 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.

261
package-lock.json generated

@ -10,6 +10,7 @@
"hasInstallScript": true, "hasInstallScript": true,
"license": "SEE LICENSE IN license.txt", "license": "SEE LICENSE IN license.txt",
"dependencies": { "dependencies": {
"@babel/standalone": "^7.24.4",
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@material-ui/core": "^4.12.4", "@material-ui/core": "^4.12.4",
@ -17,10 +18,12 @@
"@mui/material": "^5.14.12", "@mui/material": "^5.14.12",
"@mui/styles": "^5.14.12", "@mui/styles": "^5.14.12",
"@mui/system": "^5.14.12", "@mui/system": "^5.14.12",
"@swc/wasm-web": "^1.4.14",
"@types/estree": "^1.0.2", "@types/estree": "^1.0.2",
"@types/react-syntax-highlighter": "^15.5.8", "@types/react-syntax-highlighter": "^15.5.8",
"acorn": "^8.10.0", "acorn": "^8.11.3",
"acorn-walk": "^8.2.0", "acorn-jsx-walk": "^2.0.0",
"acorn-walk": "^8.3.2",
"arg": "^5.0.2", "arg": "^5.0.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"better-react-mathjax": "^2.0.3", "better-react-mathjax": "^2.0.3",
@ -54,6 +57,8 @@
"@microsoft/api-documenter": "^7.23.9", "@microsoft/api-documenter": "^7.23.9",
"@microsoft/api-extractor": "^7.38.0", "@microsoft/api-extractor": "^7.38.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@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/bcryptjs": "^2.4.4",
"@types/escodegen": "^0.0.7", "@types/escodegen": "^0.0.7",
"@types/file-saver": "^2.0.5", "@types/file-saver": "^2.0.5",
@ -1866,6 +1871,14 @@
"node": ">=6.9.0" "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": { "node_modules/@babel/template": {
"version": "7.22.15", "version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
@ -4118,6 +4131,224 @@
"@sinonjs/commons": "^3.0.0" "@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": { "node_modules/@szmarczak/http-timer": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@ -4167,6 +4398,15 @@
"@babel/types": "^7.0.0" "@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": { "node_modules/@types/babel__template": {
"version": "7.4.2", "version": "7.4.2",
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.2.tgz", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.2.tgz",
@ -5215,9 +5455,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.10.0", "version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -5253,10 +5493,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" "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": { "node_modules/acorn-walk": {
"version": "8.2.0", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
} }

@ -10,6 +10,7 @@
"url": "https://github.com/bitburner-official/bitburner-src/issues" "url": "https://github.com/bitburner-official/bitburner-src/issues"
}, },
"dependencies": { "dependencies": {
"@babel/standalone": "^7.24.4",
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@material-ui/core": "^4.12.4", "@material-ui/core": "^4.12.4",
@ -17,10 +18,12 @@
"@mui/material": "^5.14.12", "@mui/material": "^5.14.12",
"@mui/styles": "^5.14.12", "@mui/styles": "^5.14.12",
"@mui/system": "^5.14.12", "@mui/system": "^5.14.12",
"@swc/wasm-web": "^1.4.14",
"@types/estree": "^1.0.2", "@types/estree": "^1.0.2",
"@types/react-syntax-highlighter": "^15.5.8", "@types/react-syntax-highlighter": "^15.5.8",
"acorn": "^8.10.0", "acorn": "^8.11.3",
"acorn-walk": "^8.2.0", "acorn-jsx-walk": "^2.0.0",
"acorn-walk": "^8.3.2",
"arg": "^5.0.2", "arg": "^5.0.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"better-react-mathjax": "^2.0.3", "better-react-mathjax": "^2.0.3",
@ -55,6 +58,8 @@
"@microsoft/api-documenter": "^7.23.9", "@microsoft/api-documenter": "^7.23.9",
"@microsoft/api-extractor": "^7.38.0", "@microsoft/api-extractor": "^7.38.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@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/bcryptjs": "^2.4.4",
"@types/escodegen": "^0.0.7", "@types/escodegen": "^0.0.7",
"@types/file-saver": "^2.0.5", "@types/file-saver": "^2.0.5",

@ -11,3 +11,23 @@ declare module "*.png" {
declare interface Document { declare interface Document {
achievements: string[]; 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;
};
};
};
}
}

@ -30,6 +30,7 @@ import { workerScripts } from "../Netscript/WorkerScripts";
import { getRecordValues } from "../Types/Record"; import { getRecordValues } from "../Types/Record";
import { ServerConstants } from "../Server/data/Constants"; import { ServerConstants } from "../Server/data/Constants";
import { canAccessBitNodeFeature, isBitNodeFinished, knowAboutBitverse } from "../BitNode/BitNodeUtils"; import { canAccessBitNodeFeature, isBitNodeFinished, knowAboutBitverse } from "../BitNode/BitNodeUtils";
import { isLegacyScript } from "../Paths/ScriptFilePath";
// Unable to correctly cast the JSON data into AchievementDataJson type otherwise... // Unable to correctly cast the JSON data into AchievementDataJson type otherwise...
const achievementData = (<AchievementDataJson>(<unknown>data)).achievements; const achievementData = (<AchievementDataJson>(<unknown>data)).achievements;
@ -190,7 +191,7 @@ export const achievements: Record<string, Achievement> = {
NS2: { NS2: {
...achievementData.NS2, ...achievementData.NS2,
Icon: "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: { FROZE: {
...achievementData.FROZE, ...achievementData.FROZE,

@ -2,7 +2,7 @@
In Bitburner "Programs" refer specifically to the list of `.exe` files found in the Programs tab of the side menu. 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 /]> run BruteSSH.exe
[n00dles /]> scan-analyze 10 [n00dles /]> scan-analyze 10

@ -114,7 +114,7 @@ Check how much [RAM](ram.md) a script requires to run with "n" threads
**nano [script]** **nano [script]**
Create/Edit a 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** **ps**

@ -5,9 +5,10 @@
import * as walk from "acorn-walk"; import * as walk from "acorn-walk";
import { parse } from "acorn"; import { parse } from "acorn";
import { LoadedModule, ScriptURL, ScriptModule } from "./Script/LoadedModule"; import { LoadedModule, type ScriptURL, type ScriptModule } from "./Script/LoadedModule";
import { Script } from "./Script/Script"; import type { Script } from "./Script/Script";
import { ScriptFilePath, resolveScriptFilePath } from "./Paths/ScriptFilePath"; 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. // Acorn type def is straight up incomplete so we have to fill with our own.
export type Node = any; export type Node = any;
@ -82,8 +83,26 @@ function generateLoadedModule(script: Script, scripts: Map<ScriptFilePath, Scrip
return script.mod; return script.mod;
} }
let scriptCode;
const fileType = getFileType(script.filename);
switch (fileType) {
case FileType.JS:
scriptCode = script.code;
break;
case FileType.JSX:
case FileType.TS:
case FileType.TSX:
scriptCode = transformScript(script.filename, script.code, fileType);
break;
default:
throw new Error(`Invalid file type: ${fileType}. Filename: ${script.filename}, server: ${script.server}.`);
}
if (!scriptCode) {
throw new Error(`Cannot transform script. Filename: ${script.filename}, server: ${script.server}.`);
}
// Inspired by: https://stackoverflow.com/a/43834063/91401 // Inspired by: https://stackoverflow.com/a/43834063/91401
const ast = parse(script.code, { sourceType: "module", ecmaVersion: "latest", ranges: true }); const ast = parse(scriptCode, { sourceType: "module", ecmaVersion: "latest", ranges: true });
interface importNode { interface importNode {
filename: string; filename: string;
start: number; start: number;
@ -121,15 +140,10 @@ function generateLoadedModule(script: Script, scripts: Map<ScriptFilePath, Scrip
// Sort the nodes from last start index to first. This replaces the last import with a blob first, // Sort the nodes from last start index to first. This replaces the last import with a blob first,
// preventing the ranges for other imports from being shifted. // preventing the ranges for other imports from being shifted.
importNodes.sort((a, b) => b.start - a.start); importNodes.sort((a, b) => b.start - a.start);
let newCode = script.code; let newCode = scriptCode;
// Loop through each node and replace the script name with a blob url. // Loop through each node and replace the script name with a blob url.
for (const node of importNodes) { for (const node of importNodes) {
const filename = resolveScriptFilePath(node.filename, script.filename, ".js"); const importedScript = getModuleScript(node.filename, script.filename, scripts);
if (!filename) throw new Error(`Failed to parse import: ${node.filename}`);
// Find the corresponding script.
const importedScript = scripts.get(filename);
if (!importedScript) continue;
seenStack.push(script); seenStack.push(script);
importedScript.mod = generateLoadedModule(importedScript, scripts, seenStack); importedScript.mod = generateLoadedModule(importedScript, scripts, seenStack);

@ -34,7 +34,7 @@ import { Terminal } from "./Terminal";
import { ScriptArg } from "@nsdefs"; import { ScriptArg } from "@nsdefs";
import { CompleteRunOptions, getRunningScriptsByArgs } from "./Netscript/NetscriptHelpers"; import { CompleteRunOptions, getRunningScriptsByArgs } from "./Netscript/NetscriptHelpers";
import { handleUnknownError } from "./Netscript/ErrorMessages"; import { handleUnknownError } from "./Netscript/ErrorMessages";
import { resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath"; import { isLegacyScript, legacyScriptExtension, resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath";
import { root } from "./Paths/Directory"; import { root } from "./Paths/Directory";
export const NetscriptPorts = new Map<PortNumber, Port>(); export const NetscriptPorts = new Map<PortNumber, Port>();
@ -158,7 +158,7 @@ function processNetscript1Imports(code: string, workerScript: WorkerScript): { c
walksimple(ast, { walksimple(ast, {
ImportDeclaration: (node: Node) => { ImportDeclaration: (node: Node) => {
hasImports = true; 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); if (!scriptName) throw new Error("'Import' failed due to invalid path: " + scriptName);
const script = getScript(scriptName); const script = getScript(scriptName);
if (!script) throw new Error("'Import' failed due to script not found: " + 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); workerScripts.set(pid, workerScript);
// Start the script's execution using the correct function for file type // 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 // Once the code finishes (either resolved or rejected, doesnt matter), set its
// running status to false // running status to false
.then(function () { .then(function () {

@ -1,7 +1,7 @@
import { Directory, isAbsolutePath } from "./Directory"; import { Directory, isAbsolutePath } from "./Directory";
import { FilePath, isFilePath, resolveFilePath } from "./FilePath"; 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" }; type WithProgramExtension = string & { __fileType: "Program" };
export type ProgramFilePath = FilePath & WithProgramExtension; export type ProgramFilePath = FilePath & WithProgramExtension;

@ -1,14 +1,16 @@
import { Directory } from "./Directory"; import { Directory } from "./Directory";
import { FilePath, resolveFilePath } from "./FilePath"; 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 WithScriptExtension = string & { __fileType: "Script" };
/** Type for a valid absolute FilePath with a script extension */ /** Type for a valid absolute FilePath with a script extension */
export type ScriptFilePath = FilePath & WithScriptExtension; export type ScriptFilePath = FilePath & WithScriptExtension;
export const legacyScriptExtension = ".script";
/** Valid extensions. Used for some error messaging. */ /** Valid extensions. Used for some error messaging. */
export type ScriptExtension = ".js" | ".script"; export const validScriptExtensions = [".js", ".jsx", ".ts", ".tsx", legacyScriptExtension] as const;
export const validScriptExtensions: ScriptExtension[] = [".js", ".script"]; export type ScriptExtension = (typeof validScriptExtensions)[number];
/** Sanitize a player input, resolve any relative paths, and for imports add the correct extension if missing /** 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. * @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 { export function hasScriptExtension(path: string): path is WithScriptExtension {
return validScriptExtensions.some((extension) => path.endsWith(extension)); return validScriptExtensions.some((extension) => path.endsWith(extension));
} }
export function isLegacyScript(path: string): boolean {
return path.endsWith(legacyScriptExtension);
}

@ -1,7 +1,7 @@
import { Directory } from "./Directory"; import { Directory } from "./Directory";
import { FilePath, resolveFilePath } from "./FilePath"; 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" }; type WithTextExtension = string & { __fileType: "Text" };
export type TextFilePath = FilePath & WithTextExtension; export type TextFilePath = FilePath & WithTextExtension;

@ -1,21 +1,30 @@
/** /**
* Implements RAM Calculation functionality. * Implements RAM Calculation functionality.
* *
* Uses the acorn.js library to parse a script's code into an AST and * Uses acorn-walk to recursively walk through the AST, calculating RAM usage along the way.
* recursively walk through that AST, calculating RAM usage along
* the way
*/ */
import * as walk from "acorn-walk"; 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 { RamCalculationErrorCode } from "./RamCalculationErrorCodes";
import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator"; import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator";
import { Script } from "./Script"; import type { Script } from "./Script";
import { Node } from "../NetscriptJSEvaluator"; import type { Node } from "../NetscriptJSEvaluator";
import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath"; import type { ScriptFilePath } from "../Paths/ScriptFilePath";
import { ServerName } from "../Types/strings"; import type { ServerName } from "../Types/strings";
import { roundToTwo } from "../utils/helpers/roundToTwo"; import { roundToTwo } from "../utils/helpers/roundToTwo";
import {
type AST,
type FileTypeFeature,
getFileType,
getFileTypeFeature,
getModuleScript,
parseAST,
ModuleResolutionError,
} from "../utils/ScriptTransformer";
export interface RamUsageEntry { export interface RamUsageEntry {
type: "ns" | "dom" | "fn" | "misc"; type: "ns" | "dom" | "fn" | "misc";
@ -39,6 +48,12 @@ export type RamCalculationFailure = {
export type RamCalculation = RamCalculationSuccess | 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 // These special strings are used to reference the presence of a given logical
// construct within a user script. // construct within a user script.
const specialReferenceIF = "__SPECIAL_referenceIf"; 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 * Parses code into an AST and walks through it recursively to calculate
* RAM usage. Also accounts for imported modules. * RAM usage. Also accounts for imported modules.
* @param otherScripts - All other scripts on the server. Used to account for imported scripts * @param ast - AST of the code being parsed
* @param code - The code being parsed * @param scriptName - The name of the script that ram needs to be added to
* @param scriptname - The name of the script that ram needs to be added to
* @param server - Servername of the scripts for Error Message * @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( function parseOnlyRamCalculate(
otherScripts: Map<ScriptFilePath, Script>, ast: AST,
code: string, scriptName: ScriptFilePath,
scriptname: ScriptFilePath,
server: ServerName, server: ServerName,
ns1?: boolean, fileTypeFeature: FileTypeFeature,
otherScripts: Map<ScriptFilePath, Script>,
): RamCalculation { ): RamCalculation {
/** /**
* Maps dependent identifiers to their dependencies. * Maps dependent identifiers to their dependencies.
@ -91,14 +107,14 @@ function parseOnlyRamCalculate(
// Scripts we've discovered that need to be parsed. // Scripts we've discovered that need to be parsed.
const parseQueue: ScriptFilePath[] = []; const parseQueue: ScriptFilePath[] = [];
// Parses a chunk of code with a given module name, and updates parseQueue and dependencyMap. // Parses a chunk of code with a given module name, and updates parseQueue and dependencyMap.
function parseCode(code: string, moduleName: ScriptFilePath): void { function parseCode(ast: AST, moduleName: ScriptFilePath, fileTypeFeatureOfModule: FileTypeFeature): void {
const result = parseOnlyCalculateDeps(code, moduleName, ns1); const result = parseOnlyCalculateDeps(ast, moduleName, fileTypeFeatureOfModule, otherScripts);
completedParses.add(moduleName); completedParses.add(moduleName);
// Add any additional modules to the parse queue; // Add any additional modules to the parse queue;
for (let i = 0; i < result.additionalModules.length; ++i) { for (const additionalModule of result.additionalModules) {
if (!completedParses.has(result.additionalModules[i])) { if (!completedParses.has(additionalModule) && !parseQueue.includes(additionalModule)) {
parseQueue.push(result.additionalModules[i]); parseQueue.push(additionalModule);
} }
} }
@ -107,24 +123,40 @@ function parseOnlyRamCalculate(
} }
// Parse the initial module, which is the "main" script that is being run // Parse the initial module, which is the "main" script that is being run
const initialModule = scriptname; const initialModule = scriptName;
parseCode(code, initialModule); parseCode(ast, initialModule, fileTypeFeature);
// Process additional modules, which occurs if the "main" script has any imports // Process additional modules, which occurs if the "main" script has any imports
while (parseQueue.length > 0) { while (parseQueue.length > 0) {
const nextModule = parseQueue.shift(); 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); const script = otherScripts.get(nextModule);
if (!script) { if (!script) {
return { return {
errorCode: RamCalculationErrorCode.ImportError, errorCode: RamCalculationErrorCode.ImportError,
errorMessage: `File: "${nextModule}" not found on server: ${server}`, errorMessage: `"${nextModule}" does not exist on server: ${server}`,
}; };
} }
const scriptFileType = getFileType(script.filename);
parseCode(script.code, nextModule); 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 // 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<string, boolean> = {}; const loadedFns: Record<string, boolean> = {};
while (unresolvedRefs.length > 0) { while (unresolvedRefs.length > 0) {
const ref = unresolvedRefs.shift(); 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.endsWith(specialReferenceRAM)) {
if (ref !== initialModule + specialReferenceRAM) { if (ref !== initialModule + specialReferenceRAM) {
@ -169,13 +203,17 @@ function parseOnlyRamCalculate(
const prefix = ref.slice(0, ref.length - 2); const prefix = ref.slice(0, ref.length - 2);
for (const ident of Object.keys(dependencyMap).filter((k) => k.startsWith(prefix))) { for (const ident of Object.keys(dependencyMap).filter((k) => k.startsWith(prefix))) {
for (const dep of dependencyMap[ident] || []) { for (const dep of dependencyMap[ident] || []) {
if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep); if (!resolvedRefs.has(dep)) {
unresolvedRefs.push(dep);
}
} }
} }
} else { } else {
// An exact reference. Add all dependencies of this ref. // An exact reference. Add all dependencies of this ref.
for (const dep of dependencyMap[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, obj: object,
ref: string, ref: string,
): { func: () => number | number; refDetail: string } | undefined => { ): { func: () => number | number; refDetail: string } | undefined => {
if (!obj) return; if (!obj) {
return;
}
const elem = Object.entries(obj).find(([key]) => key === ref); const elem = Object.entries(obj).find(([key]) => key === ref);
if (elem !== undefined && (typeof elem[1] === "function" || typeof elem[1] === "number")) { if (elem !== undefined && (typeof elem[1] === "function" || typeof elem[1] === "number")) {
return { func: elem[1], refDetail: `${prefix}${ref}` }; return { func: elem[1], refDetail: `${prefix}${ref}` };
} }
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
const found = findFunc(`${key}.`, value, ref); const found = findFunc(`${key}.`, value, ref);
if (found) return found; if (found) {
return found;
}
} }
return undefined; return undefined;
}; };
@ -222,14 +264,7 @@ function parseOnlyRamCalculate(
return { cost: ram, entries: detailedCosts.filter((e) => e.cost > 0) }; return { cost: ram, entries: detailedCosts.filter((e) => e.cost > 0) };
} }
export function checkInfiniteLoop(code: string): number[] { export function checkInfiniteLoop(ast: AST, 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 [];
}
function nodeHasTrueTest(node: acorn.Node): boolean { function nodeHasTrueTest(node: acorn.Node): boolean {
return node.type === "Literal" && "raw" in node && (node.raw === "true" || node.raw === "1"); 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[] = []; const possibleLines: number[] = [];
walk.recursive( walk.recursive(
ast, ast as acorn.Node, // Pretend that ast is an acorn node
{}, {},
{ {
WhileStatement: (node: Node, st: unknown, walkDeeper: walk.WalkerCallback<any>) => { WhileStatement: (node: Node, st: unknown, walkDeeper: walk.WalkerCallback<any>) => {
@ -282,8 +317,12 @@ interface ParseDepsResult {
* for RAM usage calculations. It also returns an array of additional modules * for RAM usage calculations. It also returns an array of additional modules
* that need to be parsed (i.e. are 'import'ed scripts). * that need to be parsed (i.e. are 'import'ed scripts).
*/ */
function parseOnlyCalculateDeps(code: string, currentModule: ScriptFilePath, ns1?: boolean): ParseDepsResult { function parseOnlyCalculateDeps(
const ast = parse(code, { sourceType: "module", ecmaVersion: "latest" }); ast: AST,
currentModule: ScriptFilePath,
fileTypeFeature: FileTypeFeature,
otherScripts: Map<ScriptFilePath, Script>,
): ParseDepsResult {
// Everything from the global scope goes in ".". Everything else goes in ".function", where only // Everything from the global scope goes in ".". Everything else goes in ".function", where only
// the outermost layer of functions counts. // the outermost layer of functions counts.
const globalKey = currentModule + memCheckGlobalKey; const globalKey = currentModule + memCheckGlobalKey;
@ -402,17 +441,17 @@ function parseOnlyCalculateDeps(code: string, currentModule: ScriptFilePath, ns1
} }
walk.recursive<State>( walk.recursive<State>(
ast, ast as acorn.Node, // Pretend that ast is an acorn node
{ key: globalKey }, { key: globalKey },
Object.assign( Object.assign(
{ {
ImportDeclaration: (node: Node, st: State) => { ImportDeclaration: (node: Node, st: State) => {
const importModuleName = resolveScriptFilePath(node.source.value, currentModule, ns1 ? ".script" : ".js"); const rawImportModuleName = node.source.value;
if (!importModuleName) // Skip these modules. They are popular path aliases of NetscriptDefinitions.d.ts.
throw new Error( if (fileTypeFeature.isTypeScript && (rawImportModuleName === "@nsdefs" || rawImportModuleName === "@ns")) {
`ScriptFilePath couldnt be resolved in ImportDeclaration. Value: ${node.source.value} ScriptFilePath: ${currentModule}`, return;
); }
const importModuleName = getModuleScript(rawImportModuleName, currentModule, otherScripts).filename;
additionalModules.push(importModuleName); additionalModules.push(importModuleName);
// This module's global scope refers to that module's global scope, no matter how we // 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 * Calculate RAM usage of a script
* @param {string} code - The script's code *
* @param {ScriptFilePath} scriptname - The script's name. Used to resolve relative paths * @param input - Code's AST or code of the script
* @param {Script[]} otherScripts - All other scripts on the server. * @param scriptName - The script's name. Used to resolve relative paths
* Used to account for imported scripts * @param server - Servername of the scripts for Error Message
* @param {ServerName} server - Servername of the scripts for Error Message * @param otherScripts - Other scripts on the server
* @param {boolean} ns1 - Deprecated: is the fileExtension .script or .js * @returns
*/ */
export function calculateRamUsage( export function calculateRamUsage(
code: string, input: AST | string,
scriptname: ScriptFilePath, scriptName: ScriptFilePath,
otherScripts: Map<ScriptFilePath, Script>,
server: ServerName, server: ServerName,
ns1?: boolean, otherScripts: Map<ScriptFilePath, Script>,
): RamCalculation { ): RamCalculation {
try { try {
return parseOnlyRamCalculate(otherScripts, code, scriptname, server, ns1); const fileType = getFileType(scriptName);
} catch (e) { const ast = typeof input === "string" ? parseAST(input, fileType) : input;
return parseOnlyRamCalculate(ast, scriptName, server, getFileTypeFeature(fileType), otherScripts);
} catch (error) {
return { return {
errorCode: RamCalculationErrorCode.SyntaxError, errorCode:
errorMessage: e instanceof Error ? e.message : undefined, error instanceof ModuleResolutionError
? RamCalculationErrorCode.ImportError
: RamCalculationErrorCode.SyntaxError,
errorMessage: error instanceof Error ? error.message : String(error),
}; };
} }
} }

@ -73,13 +73,7 @@ export class Script implements ContentFile {
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports * @param {Script[]} otherScripts - Other scripts on the server. Used to process imports
*/ */
updateRamUsage(otherScripts: Map<ScriptFilePath, Script>): void { updateRamUsage(otherScripts: Map<ScriptFilePath, Script>): void {
const ramCalc = calculateRamUsage( const ramCalc = calculateRamUsage(this.code, this.filename, this.server, otherScripts);
this.code,
this.filename,
otherScripts,
this.server,
this.filename.endsWith(".script"),
);
if (ramCalc.cost && ramCalc.cost >= RamCostConstants.Base) { if (ramCalc.cost && ramCalc.cost >= RamCostConstants.Base) {
this.ramUsage = roundToTwo(ramCalc.cost); this.ramUsage = roundToTwo(ramCalc.cost);
this.ramUsageEntries = ramCalc.entries as RamUsageEntry[]; this.ramUsageEntries = ramCalc.entries as RamUsageEntry[];

@ -6919,7 +6919,7 @@ export interface NS {
* @remarks * @remarks
* RAM cost: 0 GB * 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, * 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 * 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 * @remarks
* RAM cost: 0 GB * 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. * This function will return the data in the specified file.
* If the file does not exist, an empty string will be returned. * If the file does not exist, an empty string will be returned.
@ -7430,7 +7430,7 @@ export interface NS {
* RAM cost: 0 GB * RAM cost: 0 GB
* *
* Retrieves data from a URL and downloads it to a file on the specified server. * 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. * 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 * Note that it will not be possible to download data from many websites because they
* do not allow cross-origin resource sharing (CORS). * do not allow cross-origin resource sharing (CORS).

@ -71,6 +71,18 @@ export class ScriptEditor {
languageDefaults.addExtraLib(reactTypes, "react.d.ts"); languageDefaults.addExtraLib(reactTypes, "react.d.ts");
languageDefaults.addExtraLib(reactDomTypes, "react-dom.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.setModeConfiguration({
...monaco.languages.json.jsonDefaults.modeConfiguration, ...monaco.languages.json.jsonDefaults.modeConfiguration,
//completion should be disabled because the //completion should be disabled because the

@ -1,20 +1,21 @@
import React, { useContext, useState } from "react"; import React, { useContext, useState } from "react";
import { Settings } from "../../Settings/Settings";
import { calculateRamUsage } from "../../Script/RamCalculations";
import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes"; import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes";
import { formatRam } from "../../ui/formatNumber"; import { calculateRamUsage, type RamCalculationFailure } from "../../Script/RamCalculations";
import { useBoolean } from "../../ui/React/hooks";
import { BaseServer } from "../../Server/BaseServer"; 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 type { AST } from "../../utils/ScriptTransformer";
import { FilePath } from "../../Paths/FilePath"; import type { Options } from "./Options";
import { hasScriptExtension } from "../../Paths/ScriptFilePath"; import { type ScriptFilePath } from "../../Paths/ScriptFilePath";
export interface ScriptEditorContextShape { export interface ScriptEditorContextShape {
ram: string; ram: string;
ramEntries: 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; isUpdatingRAM: boolean;
startUpdatingRAM: () => void; startUpdatingRAM: () => void;
@ -30,13 +31,30 @@ export function ScriptEditorContextProvider({ children }: { children: React.Reac
const [ram, setRAM] = useState("RAM: ???"); const [ram, setRAM] = useState("RAM: ???");
const [ramEntries, setRamEntries] = useState<string[][]>([["???", ""]]); const [ramEntries, setRamEntries] = useState<string[][]>([["???", ""]]);
const updateRAM: ScriptEditorContextShape["updateRAM"] = (newCode, filename, server) => { const showRAMError: ScriptEditorContextShape["showRAMError"] = (error) => {
if (newCode == null || filename == null || server == null || !hasScriptExtension(filename)) { if (!error) {
setRAM("N/A"); setRAM("N/A");
setRamEntries([["N/A", ""]]); setRamEntries([["N/A", ""]]);
return; 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) { if (ramUsage.cost && ramUsage.cost > 0) {
const entries = ramUsage.entries?.sort((a, b) => b.cost - a.cost) ?? []; const entries = ramUsage.entries?.sort((a, b) => b.cost - a.cost) ?? [];
const entriesDisp = []; const entriesDisp = [];
@ -50,18 +68,10 @@ export function ScriptEditorContextProvider({ children }: { children: React.Reac
} }
if (ramUsage.errorCode !== undefined) { if (ramUsage.errorCode !== undefined) {
setRamEntries([["Syntax Error", ramUsage.errorMessage ?? ""]]); showRAMError(ramUsage);
switch (ramUsage.errorCode) {
case RamCalculationErrorCode.ImportError:
setRAM("RAM: Import Error");
break;
case RamCalculationErrorCode.SyntaxError:
setRAM("RAM: Syntax Error");
break;
}
} else { } else {
setRAM("RAM: Syntax Error"); setRAM("RAM: Unknown Error");
setRamEntries([["Syntax Error", ""]]); setRamEntries([["Unknown Error", ""]]);
} }
}; };
@ -96,7 +106,17 @@ export function ScriptEditorContextProvider({ children }: { children: React.Reac
return ( return (
<ScriptEditorContext.Provider <ScriptEditorContext.Provider
value={{ ram, ramEntries, updateRAM, isUpdatingRAM, startUpdatingRAM, finishUpdatingRAM, options, saveOptions }} value={{
ram,
ramEntries,
showRAMError,
updateRAM,
isUpdatingRAM,
startUpdatingRAM,
finishUpdatingRAM,
options,
saveOptions,
}}
> >
{children} {children}
</ScriptEditorContext.Provider> </ScriptEditorContext.Provider>

@ -30,6 +30,9 @@ import { NoOpenScripts } from "./NoOpenScripts";
import { ScriptEditorContextProvider, useScriptEditorContext } from "./ScriptEditorContext"; import { ScriptEditorContextProvider, useScriptEditorContext } from "./ScriptEditorContext";
import { useVimEditor } from "./useVimEditor"; import { useVimEditor } from "./useVimEditor";
import { useCallback } from "react"; 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 { interface IProps {
// Map of filename -> code // Map of filename -> code
@ -44,7 +47,7 @@ function Root(props: IProps): React.ReactElement {
const rerender = useRerender(); const rerender = useRerender();
const editorRef = useRef<IStandaloneCodeEditor | null>(null); const editorRef = useRef<IStandaloneCodeEditor | null>(null);
const { updateRAM, startUpdatingRAM, finishUpdatingRAM } = useScriptEditorContext(); const { showRAMError, updateRAM, startUpdatingRAM, finishUpdatingRAM } = useScriptEditorContext();
let decorations: monaco.editor.IEditorDecorationsCollection | undefined; let decorations: monaco.editor.IEditorDecorationsCollection | undefined;
@ -112,11 +115,14 @@ function Root(props: IProps): React.ReactElement {
return () => document.removeEventListener("keydown", keydown); return () => document.removeEventListener("keydown", keydown);
}, [save]); }, [save]);
function infLoop(newCode: string): void { function infLoop(ast: AST, code: string): void {
if (editorRef.current === null || currentScript === null) return; if (editorRef.current === null || currentScript === null || isLegacyScript(currentScript.path)) {
if (!decorations) decorations = editorRef.current.createDecorationsCollection(); return;
if (!currentScript.path.endsWith(".js")) return; }
const possibleLines = checkInfiniteLoop(newCode); if (!decorations) {
decorations = editorRef.current.createDecorationsCollection();
}
const possibleLines = checkInfiniteLoop(ast, code);
if (possibleLines.length !== 0) { if (possibleLines.length !== 0) {
decorations.set( decorations.set(
possibleLines.map((awaitWarning) => ({ possibleLines.map((awaitWarning) => ({
@ -131,21 +137,34 @@ function Root(props: IProps): React.ReactElement {
glyphMarginClassName: "myGlyphMarginClass", glyphMarginClassName: "myGlyphMarginClass",
glyphMarginHoverMessage: { glyphMarginHoverMessage: {
value: 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) => { const debouncedCodeParsing = debounce((newCode: string) => {
infLoop(newCode); let server;
updateRAM( if (!currentScript || !hasScriptExtension(currentScript.path) || !(server = GetServer(currentScript.hostname))) {
!currentScript || currentScript.isTxt ? null : newCode, showRAMError();
currentScript && currentScript.path, return;
currentScript && GetServer(currentScript.hostname), }
); 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(); finishUpdatingRAM();
}, 300); }, 300);

@ -1,6 +1,7 @@
import { GetServer } from "../../Server/AllServers"; import { GetServer } from "../../Server/AllServers";
import { editor, Uri } from "monaco-editor"; import { editor, Uri } from "monaco-editor";
import { OpenScript } from "./OpenScript"; import { OpenScript } from "./OpenScript";
import { getFileType, FileType } from "../../utils/ScriptTransformer";
function getServerCode(scripts: OpenScript[], index: number): string | null { function getServerCode(scripts: OpenScript[], index: number): string | null {
const openScript = scripts[index]; const openScript = scripts[index];
@ -26,7 +27,29 @@ function makeModel(hostname: string, filename: string, code: string) {
scheme: "file", scheme: "file",
path: `${hostname}/${filename}`, 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 //if somehow a model already exist return it
return editor.getModel(uri) ?? editor.createModel(code, language, uri); return editor.getModel(uri) ?? editor.createModel(code, language, uri);
} }

@ -52,10 +52,11 @@ const TemplatedHelpTexts: Record<string, (command: string) => string[]> = {
return [ return [
`Usage: ${command} [file names...] | [glob]`, `Usage: ${command} [file names...] | [glob]`,
` `, ` `,
`Opens up the specified file(s) in the Script Editor. Only scripts (.js, or .script) or text files (.txt) `, `Opens up the specified file(s) in the Script Editor. Only scripts (.js, .jsx, .ts, .tsx, .script) `,
`can be edited using the Script Editor. If a file does not exist a new one will be created`, `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.`, `files at once. ${command} cannot create files using globs, so your scripts must already exist.`,
` `, ` `,
`Examples:`, `Examples:`,
@ -456,8 +457,8 @@ export const HelpTexts: Record<string, string[]> = {
"Usage: scp [file names...] [target server]", "Usage: scp [file names...] [target server]",
" ", " ",
"Copies the specified file(s) from the current server to the 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), ", "This command only works for script files (.js, .jsx, .ts, .tsx, .script), text files (.txt, .json), ",
"and text files (.txt extension). ", "and literature files (.lit).",
"The second argument passed in must be the hostname or IP of the target server. Examples:", "The second argument passed in must be the hostname or IP of the target server. Examples:",
" ", " ",
" scp foo.script n00dles", " scp foo.script n00dles",
@ -518,8 +519,8 @@ export const HelpTexts: Record<string, string[]> = {
"Usage: wget [url] [target file]", "Usage: wget [url] [target file]",
" ", " ",
"Retrieves data from a URL and downloads it to a file on the current server. The data can only ", "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, ", "be downloaded to a script (.js, .jsx, .ts, .tsx, .script) or a text file (.txt, .json).",
"it will be overwritten by this command.", "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 ", "Note that it will not be possible to download data from many websites because they do not allow ",
"cross-origin resource sharing (CORS). Example:", "cross-origin resource sharing (CORS). Example:",

@ -20,7 +20,9 @@ export function cat(args: (string | number | boolean)[], server: BaseServer): vo
return dialogBoxCreate(`${file.filename}\n\n${file.content}`); return dialogBoxCreate(`${file.filename}\n\n${file.content}`);
} }
if (!path.endsWith(".msg") && !path.endsWith(".lit")) { 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 // Message

@ -2,10 +2,11 @@ import { Terminal } from "../../../Terminal";
import { ScriptEditorRouteOptions, Page } from "../../../ui/Router"; import { ScriptEditorRouteOptions, Page } from "../../../ui/Router";
import { Router } from "../../../ui/GameRoot"; import { Router } from "../../../ui/GameRoot";
import { BaseServer } from "../../../Server/BaseServer"; 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 { TextFilePath, hasTextExtension } from "../../../Paths/TextFilePath";
import { getGlobbedFileMap } from "../../../Paths/GlobbedFiles"; import { getGlobbedFileMap } from "../../../Paths/GlobbedFiles";
import { sendDeprecationNotice } from "./deprecation"; 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. // 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; server: BaseServer;
} }
function isNs2(filename: string): boolean { function getScriptTemplate(path: string): string {
return filename.endsWith(".js"); 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 async function main(ns) {
}`; }`;
}
}
export function commonEditor( export function commonEditor(
command: string, command: string,
@ -30,14 +39,16 @@ export function commonEditor(
): void { ): void {
if (args.length < 1) return Terminal.error(`Incorrect usage of ${command} command. Usage: ${command} [scriptname]`); if (args.length < 1) return Terminal.error(`Incorrect usage of ${command} command. Usage: ${command} [scriptname]`);
const files = new Map<ScriptFilePath | TextFilePath, string>(); const files = new Map<ScriptFilePath | TextFilePath, string>();
let hasNs1 = false; let hasLegacyScript = false;
for (const arg of args) { for (const arg of args) {
const pattern = String(arg); const pattern = String(arg);
// Glob of existing files // Glob of existing files
if (pattern.includes("*") || pattern.includes("?")) { if (pattern.includes("*") || pattern.includes("?")) {
for (const [path, file] of getGlobbedFileMap(pattern, server, Terminal.currDir)) { 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); files.set(path, file.content);
} }
continue; continue;
@ -49,12 +60,13 @@ export function commonEditor(
if (!hasScriptExtension(path) && !hasTextExtension(path)) { if (!hasScriptExtension(path) && !hasTextExtension(path)) {
return Terminal.error(`${command}: Only scripts or text files can be edited. Invalid file type: ${arg}`); 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 file = server.getContentFile(path);
const content = file ? file.content : isNs2(path) ? newNs2Template : ""; files.set(path, file ? file.content : getScriptTemplate(path));
files.set(path, content);
} }
if (hasNs1) { if (hasLegacyScript) {
sendDeprecationNotice(); sendDeprecationNotice();
} }
Router.toPage(Page.ScriptEditor, { files, options }); Router.toPage(Page.ScriptEditor, { files, options });

@ -24,5 +24,5 @@ export function run(args: (string | number | boolean)[], server: BaseServer): vo
} else if (hasProgramExtension(path)) { } else if (hasProgramExtension(path)) {
return runProgram(path, args, server); 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.`);
} }

@ -7,7 +7,7 @@ import libarg from "arg";
import { formatRam } from "../../ui/formatNumber"; import { formatRam } from "../../ui/formatNumber";
import { ScriptArg } from "@nsdefs"; import { ScriptArg } from "@nsdefs";
import { isPositiveInteger } from "../../types"; import { isPositiveInteger } from "../../types";
import { ScriptFilePath } from "../../Paths/ScriptFilePath"; import { ScriptFilePath, isLegacyScript } from "../../Paths/ScriptFilePath";
import { sendDeprecationNotice } from "./common/deprecation"; import { sendDeprecationNotice } from "./common/deprecation";
import { roundToTwo } from "../../utils/helpers/roundToTwo"; import { roundToTwo } from "../../utils/helpers/roundToTwo";
import { RamCostConstants } from "../../Netscript/RamCostGenerator"; import { RamCostConstants } from "../../Netscript/RamCostGenerator";
@ -61,7 +61,7 @@ export function runScript(path: ScriptFilePath, commandArgs: (string | number |
const success = startWorkerScript(runningScript, server); const success = startWorkerScript(runningScript, server);
if (!success) return Terminal.error(`Failed to start script`); if (!success) return Terminal.error(`Failed to start script`);
if (path.endsWith(".script")) { if (isLegacyScript(path)) {
sendDeprecationNotice(); sendDeprecationNotice();
} }
Terminal.print( Terminal.print(

@ -36,7 +36,7 @@ export function scp(args: (string | number | boolean)[], server: BaseServer): vo
// Error for invalid filetype // Error for invalid filetype
if (!hasScriptExtension(path) && !hasTextExtension(path)) { if (!hasScriptExtension(path) && !hasTextExtension(path)) {
return Terminal.error( 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); const sourceContentFile = server.getContentFile(path);

@ -9,7 +9,7 @@ import { Flags } from "../NetscriptFunctions/Flags";
import { AutocompleteData } from "@nsdefs"; import { AutocompleteData } from "@nsdefs";
import libarg from "arg"; import libarg from "arg";
import { getAllDirectories, resolveDirectory, root } from "../Paths/Directory"; import { getAllDirectories, resolveDirectory, root } from "../Paths/Directory";
import { resolveScriptFilePath } from "../Paths/ScriptFilePath"; import { isLegacyScript, resolveScriptFilePath } from "../Paths/ScriptFilePath";
import { enums } from "../NetscriptFunctions"; import { enums } from "../NetscriptFunctions";
// TODO: this shouldn't be hardcoded in two places with no typechecks to verify equivalence // 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); const filepath = resolveScriptFilePath(filename, baseDir);
if (!filepath) return; // Not a script path. 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); const script = currServ.scripts.get(filepath);
if (!script) return; // Doesn't exist. if (!script) return; // Doesn't exist.

1
src/ThirdParty/acorn-jsx-walk.d.ts vendored Normal file

@ -0,0 +1 @@
declare module "acorn-jsx-walk";

@ -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);
};
}

@ -318,7 +318,10 @@ export function InteractiveTutorialRoot(): React.ReactElement {
</Typography> </Typography>
<Typography classes={{ root: classes.textfield }}>{"[home /]> nano"}</Typography> <Typography classes={{ root: classes.textfield }}>{"[home /]> nano"}</Typography>
<Typography>Scripts must end with the .js extension. Let's make a script now by entering </Typography> <Typography>
Scripts must end with a script extension (.js, .jsx, .ts, .tsx, .script). Let's make a script now by
entering
</Typography>
<Typography classes={{ root: classes.textfield }}>{`[home /]> nano ${tutorialScriptName}`}</Typography> <Typography classes={{ root: classes.textfield }}>{`[home /]> nano ${tutorialScriptName}`}</Typography>
</> </>
), ),

@ -11,6 +11,7 @@ import { CONSTANTS } from "../Constants";
import { ActivateRecoveryMode } from "./React/RecoveryRoot"; import { ActivateRecoveryMode } from "./React/RecoveryRoot";
import { hash } from "../hash/hash"; import { hash } from "../hash/hash";
import { pushGameReady } from "../Electron"; import { pushGameReady } from "../Electron";
import initSwc from "@swc/wasm-web";
export function LoadingScreen(): React.ReactElement { export function LoadingScreen(): React.ReactElement {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
@ -33,6 +34,7 @@ export function LoadingScreen(): React.ReactElement {
useEffect(() => { useEffect(() => {
load().then(async (saveData) => { load().then(async (saveData) => {
try { try {
await initSwc();
await Engine.load(saveData); await Engine.load(saveData);
} catch (error) { } catch (error) {
console.error(error); console.error(error);

@ -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<ScriptFilePath, Script>,
): 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;
}

@ -1,3 +1,4 @@
import { isLegacyScript } from "../Paths/ScriptFilePath";
import { TextFilePath } from "../Paths/TextFilePath"; import { TextFilePath } from "../Paths/TextFilePath";
import { saveObject } from "../SaveObject"; import { saveObject } from "../SaveObject";
import { Script } from "../Script/Script"; import { Script } from "../Script/Script";
@ -277,7 +278,7 @@ const processScript = (rules: IRule[], script: Script) => {
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
for (const rule of rules) { for (const rule of rules) {
const line = lines[i]; 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)) { if (line.match(match)) {
rule.offenders.push({ rule.offenders.push({
file: script.filename, file: script.filename,

@ -72,7 +72,6 @@ describe("Netscript RAM Calculation/Generation Tests", function () {
extraLayerCost = 0, extraLayerCost = 0,
) { ) {
const code = `${fnPath.join(".")}();\n`.repeat(3); const code = `${fnPath.join(".")}();\n`.repeat(3);
const filename = "testfile.js" as ScriptFilePath;
const fnName = fnPath[fnPath.length - 1]; const fnName = fnPath[fnPath.length - 1];
const server = "testserver"; const server = "testserver";
@ -80,7 +79,7 @@ describe("Netscript RAM Calculation/Generation Tests", function () {
expect(getRamCost(fnPath, true)).toEqual(expectedRamCost); expect(getRamCost(fnPath, true)).toEqual(expectedRamCost);
// Static ram check // 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)); expect(staticCost).toBeCloseTo(Math.min(baseCost + expectedRamCost + extraLayerCost, maxCost));
// reset workerScript for dynamic check // reset workerScript for dynamic check

@ -28,7 +28,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
const code = ` const code = `
export async function main(ns) { } 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); expectCost(calculated, 0);
}); });
@ -38,7 +38,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
ns.print("Slum snakes r00l!"); 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); expectCost(calculated, 0);
}); });
@ -48,7 +48,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
await ns.hack("joesguns"); 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); expectCost(calculated, HackCost);
}); });
@ -58,7 +58,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
await X.hack("joesguns"); 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); expectCost(calculated, HackCost);
}); });
@ -69,7 +69,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
await ns.hack("joesguns"); 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); expectCost(calculated, HackCost);
}); });
@ -80,7 +80,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
await ns.grow("joesguns"); 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); expectCost(calculated, HackCost + GrowCost);
}); });
@ -93,7 +93,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
await ns.hack("joesguns"); 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); 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"); } 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); 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"); } 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); expectCost(calculated, HackCost);
}); });
}); });
@ -136,7 +136,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
} }
function get() { return 0; } 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); expectCost(calculated, 0);
}); });
@ -147,7 +147,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
} }
function purchaseNode() { return 0; } 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 // Works at present, because the parser checks the namespace only, not the function name
expectCost(calculated, 0); expectCost(calculated, 0);
}); });
@ -160,7 +160,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
} }
function getTask() { return 0; } 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); expectCost(calculated, 0);
}); });
}); });
@ -172,7 +172,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
ns.hacknet.purchaseNode(0); 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); expectCost(calculated, HacknetCost);
}); });
@ -182,7 +182,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
ns.sleeve.getTask(3); 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); expectCost(calculated, SleeveGetTaskCost);
}); });
}); });
@ -203,8 +203,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
const calculated = calculateRamUsage( const calculated = calculateRamUsage(
code, code,
filename, filename,
new Map([["libTest.js" as ScriptFilePath, lib]]),
server, server,
new Map([["libTest.js" as ScriptFilePath, lib]]),
).cost; ).cost;
expectCost(calculated, 0); expectCost(calculated, 0);
}); });
@ -224,8 +224,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
const calculated = calculateRamUsage( const calculated = calculateRamUsage(
code, code,
filename, filename,
new Map([["libTest.js" as ScriptFilePath, lib]]),
server, server,
new Map([["libTest.js" as ScriptFilePath, lib]]),
).cost; ).cost;
expectCost(calculated, HackCost); expectCost(calculated, HackCost);
}); });
@ -246,8 +246,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
const calculated = calculateRamUsage( const calculated = calculateRamUsage(
code, code,
filename, filename,
new Map([["libTest.js" as ScriptFilePath, lib]]),
server, server,
new Map([["libTest.js" as ScriptFilePath, lib]]),
).cost; ).cost;
expectCost(calculated, HackCost); expectCost(calculated, HackCost);
}); });
@ -268,8 +268,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
const calculated = calculateRamUsage( const calculated = calculateRamUsage(
code, code,
filename, filename,
new Map([["libTest.js" as ScriptFilePath, lib]]),
server, server,
new Map([["libTest.js" as ScriptFilePath, lib]]),
).cost; ).cost;
expectCost(calculated, HackCost + GrowCost); expectCost(calculated, HackCost + GrowCost);
}); });
@ -291,7 +291,7 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
${lines.join("\n")}; ${lines.join("\n")};
} }
`; `;
const calculated = calculateRamUsage(code, filename, new Map(), server).cost; const calculated = calculateRamUsage(code, filename, server, new Map()).cost;
expectCost(calculated, MaxCost); expectCost(calculated, MaxCost);
}); });
@ -316,8 +316,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
const calculated = calculateRamUsage( const calculated = calculateRamUsage(
code, code,
filename, filename,
new Map([["libTest.js" as ScriptFilePath, lib]]),
server, server,
new Map([["libTest.js" as ScriptFilePath, lib]]),
).cost; ).cost;
expectCost(calculated, HackCost); expectCost(calculated, HackCost);
}); });
@ -347,8 +347,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
const calculated = calculateRamUsage( const calculated = calculateRamUsage(
code, code,
filename, filename,
new Map([["libTest.js" as ScriptFilePath, lib]]),
server, server,
new Map([["libTest.js" as ScriptFilePath, lib]]),
).cost; ).cost;
expectCost(calculated, GrowCost); expectCost(calculated, GrowCost);
}); });
@ -370,8 +370,8 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
const calculated = calculateRamUsage( const calculated = calculateRamUsage(
code, code,
folderFilename, folderFilename,
new Map([["test/libTest.js" as ScriptFilePath, lib]]),
server, server,
new Map([["test/libTest.js" as ScriptFilePath, lib]]),
).cost; ).cost;
expectCost(calculated, HackCost); expectCost(calculated, HackCost);
}); });
@ -404,11 +404,11 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
const calculated = calculateRamUsage( const calculated = calculateRamUsage(
code, code,
folderFilename, folderFilename,
server,
new Map([ new Map([
[libNameOne, libScriptOne], [libNameOne, libScriptOne],
[libNameTwo, libScriptTwo], [libNameTwo, libScriptTwo],
]), ]),
server,
).cost; ).cost;
expectCost(calculated, HackCost); expectCost(calculated, HackCost);
}); });
@ -449,12 +449,12 @@ describe("Parsing NetScript code to work out static RAM costs", function () {
const calculated = calculateRamUsage( const calculated = calculateRamUsage(
code, code,
folderFilename, folderFilename,
server,
new Map([ new Map([
[libNameOne, libScriptOne], [libNameOne, libScriptOne],
[libNameTwo, libScriptTwo], [libNameTwo, libScriptTwo],
[incorrect_libNameTwo, incorrect_libScriptTwo], [incorrect_libNameTwo, incorrect_libScriptTwo],
]), ]),
server,
).cost; ).cost;
expectCost(calculated, HackCost); expectCost(calculated, HackCost);
}); });

@ -192,5 +192,11 @@ module.exports = (env, argv) => {
fallback: { crypto: false }, fallback: { crypto: false },
}, },
stats: statsConfig, stats: statsConfig,
ignoreWarnings: [
{
module: /@babel\/standalone/,
message: /Critical dependency: the request of a dependency is an expression/,
},
],
}; };
}; };