-
Notifications
You must be signed in to change notification settings - Fork 281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MISC: Support JSX, TS, TSX script files #1216
MISC: Support JSX, TS, TSX script files #1216
Conversation
For completeness, I mention another possible approach for parsing the AST: You can transform first (using whatever chosen tool), and then continue to parse using There is also the other approach possible, which is to use Typescript to transform (and maybe parse?) It will be slow for sure, also setting it up is way more complicated than any other approach. The only upside is that you get actual typechecking, instead of just checking the syntactic correctness, but that is a significant upside. (I.e. there isn't really any point to writing typescript in the in-game editor without typechecking, although there's still value in being able to edit other people's TS code in the in-game editor.) On the balance, I think I'm happy to endorse the tool choices you made. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only did a high-level pass so far, looking at the general design. Overall, I like it: Keeping things synchronous simplifies a lot.
These are my opinions about other approaches and why I don't choose them: Transform JSX/TS/TSX to JS, then parse with only
|
I think something we could consider with these "compiled" script files is not having them constantly perform ram checking and other such functions while the player types out the code. Instead, the player can be given a way to save + compile which would compile the script into native .js and would also update the ram usage. This would also reinforce to the player that these file types are not native code (whereas presenting them without any compilation might confuse players when real world .ts doesn't work that way). That would make the performance question a lot less relevant, as an opaque manual compilation step would be rare and could be a little slow. How does importing different types of files work? e.g. is a .js file allowed to import from a .ts file? |
What will happen if players create non-JS files, edit it, and run command like It's the same question when players add non-JS files via RFA. When the RFA server pushes I agree with "not having them constantly perform ram checking and other such functions while the player types out the code". We can do that by not constantly call For your last question, current module resolution algorithm is simple. It checks all possible filenames for the module and use the first one that it found. For example: |
Wait, how do you figure? As far as I'm aware, In other words, I agree that the primary benefit is getting typechecking in the editor, but I thought additional effort was needed to get that. It's certainly the case that typechecking cannot be done just on a single-file level, so I'm not sure how Edit: Looking at https://www.npmjs.com/package/monaco-typescript, I think I'm right that TSC is being bundled... also:
|
As you said, |
f1c4242
to
ae6b524
Compare
Ok, this has sat here languishing (due to us, the maintainers) and I'd like to make forward progress. From my side, I think the core architectural choices are sound. We could maybe make other choices that might be better, or might not, but these seem like they'll work and won't cause headaches down the road. To me. @Snarling what do you think? Can we get sign-off on the big parts of the overall design, and then I (or you) can look at this more closely, code-wise? |
496d4ca
to
481e569
Compare
Sorry @d0sboots @catloversg, been a while since I made time to look stuff over. I do think this is a huge change and it's pretty important that we do it right / right enough that we're not breaking things with future tweaks. So just an opinion (and probably an unpopular one), I would personally prefer for scripts in "languages" that require compilation to be a completely new category, and for there to be a player-visible compilation step to turn that file into a .js file so it can actually be run in the browser. But I think the overwhelming majority of people would probably prefer the implementation here, where they can just directly run .tsx/.ts/.jsx files just as if they were native js scripts. So even though it's not how I would do it, I'm not going to object to the core design here. As far as things we won't really be able to change much later, here's what I can think of just so we're pretty settled on the answers here:
Most other implementation details can be changed later on without breaking stuff. Since I know my preferred implementation is probably unpopular, I just want to make sure the answers to the questions above are intentional and generally wanted. For review, a couple things I noticed:
|
This PR supports
I'm not against your suggestion about ".js could only import .js scripts, where compiled scripts can import from any filetype", but I want to hear more opinions about it before adding the new constraint ".js could only import .js scripts". I also want to clarify the module resolution algorithm, just in case it's not clear in the PR.
I saw that warning, but I could not find a way to solve that problem. I think it's this one: babel/babel#14301. About JSX in the editor: |
That would definitely be a design that could work, and it would certainly be easier to implement. I'm going to explore what that design would look like a bit more, so we can compare/contrast. For external editor workflows, no change would be needed: compilation would take place outside the game, and only .js files would be uploaded. This is the easy case. For internal editor uses, it gets weirder. We'd need to introduce a new The compile experience would be a bit subpar, since our shell lacks the features of the overall npm ecosystem. For instance, we don't even have file timestamps (although there is a PR for that), so you can't easily tell if something needs to be recompiled. There's also the redundancy issue: The in-game editor will give errors about your ts, but then you have an extra compile step because... why? I think the real missing piece is "what does an extra compile step buy us," because all I see are downsides. |
First of all, if I'm saying something completely wrong, feel free to tell me otherwise. |
Can you be specific about what is over-engineered? There are 2 main places:
|
You were talking about all of the different parser options and then there were arguments about how importing from different file types and compiling would work. |
That's not simple as you say:
There are many of options with their own trade-offs. Please read the first comment carefully to see how many things we need to consider. |
You already mentioned the option I am suggesting before: Use a transformer (my suggestion being import * as ts from "typescript";
const code = `const test: number = 1 + 2`;
const transpiledCode = ts.transpileModule(code, tsconfig).outputText; The transpilation itself is literally one line. No need to re-write all of the existing parsing for now. Using I am in no way saying the other solutions are worse, I'm just saying this would be the simplest solution and we could go from there. |
That approach was discussed in this comment: #1216 (comment). Of course, you can disagree with my opinion there. It's fine. We are still discussing multiple approaches. The Babel approach has the same "easiness":
Choosing You said that things are over-engineered, but I have not seen anything that can be "skipped". Can you point out a specific part that we can cut off from this PR and make it simpler without any downside? |
Maybe over-engineered is the wrong word. So, there are a few things that bother me, not only about this PR, but the general way we treat the player scripts:
In general, the PR definitely would work. Maybe I feel too strongly about this, but adding/changing to this spaghetti seems like we are going in the wrong direction. It would certainly take a lot of effort, but I would say we should do the following to enable easier contributions to this in the future:
|
For your first point:
This goes back to these design choices:
We need to have a decision from the maintainers before trying to do anything that is "for these very different operations to be split up.". For your second point: For your last point:
It sounds good, except for the fact that it's kind of "I will make a big refactor, and it will be perfect this time". I appreciate your suggestion (it's a good one), and I will take on that mission, but only if Snarling or d0sboots demands it. I prefer making many small improvements iteratively on what we have right now over making a big refactor that touches too many things, especially in this complicated PR. That's why I made this PR as simple as possible. |
I think realistically if we are allowing players to directly run from .tsx/etc files (probably the right call) instead of manually transpiling, then we should not save filename.js as a visible file for the player, and if there's a reason for us to store the transpiled .js content, then it should be stored as a hidden property on the Script (and that property should not be included in the save file). As far as whether ram calc is better done as "Parse .tsx content" or "Transpile/transform to js -> Parse .js content," I don't have a strong opinion. I suspect parsing the .tsx directly would probably be more performant, which matters a lot when we are doing the ram calc frequently in the script editor. But if it's pretty close performance wise, then unifying the ram calc and script execution could be worthwhile. But those sort of hidden implementation details can always be optimized later on. The most important thing to get right in this PR is just the new functionality and how the player interacts with it. I'm good with the overall implementation, even though there are some cases where it doesn't match the reality of how things work outside the game. |
481e569
to
b58a9c8
Compare
b58a9c8
to
89dfa7a
Compare
Working on trying to merge this now...
This seems really serious to me. The way our project is configured, warnings are essentially errors, because of how bold they show up when the dev server reloads. We could change the warning behavior as a very last resort, but honestly I'd be more comfortable with switching packages to something other than babel/standalone, or (hopefully) finding some other way of working around it without having to switch.
I think maybe Snarling was suggesting only silencing the error when you are editing JSX/TSX files, instead of globally? |
I've looked the code over once in the past, and looked it over again now. It's a big change, but at the same time surprisingly small for how much it's doing. Nice job! Aside from the issues I re-raised, I don't think there are any blockers left. Snarling said he'd personally prefer an explicit-compile model, but also that most people would probably prefer this model and that he's OK with it. I think this is the right model. The architecture seems sound, and decisions about which plugins we are using can be changed later if need be. |
Yeah that was my thought. Based on the error, I assumed that there actually was a flag that could be sent for jsx/tsx files: But it looks like monaco just doesn't support JSX natively yet? Which makes that error message really weird. There might just not be a more elegant way to deal with this for the time being. The player will still get syntax errors in the ram calculation or when running the script if they try using JSX syntax in a .js/ts file. |
In the latest commit, I removed 17004 from the list of ignored diagnostic codes. We don't need to worry about warnings/errors in Test code: export const sum = (a, b) => a + b; multiply.ts: export const multiply = (a: number, b: number) => a * b; libJsx.jsx: export const LibText = <>LibJsx</> libTsx.tsx: export const LibText = <>LibTsx</> jsx.jsx: import { LibText } from "/libJsx";
import { sum } from "/sum";
import { multiply } from "/multiply";
/** @param {NS} ns */
export async function main(ns) {
ns.tprintRaw(LibText);
ns.tprintRaw(<>From Jsx</>);
ns.tprint(`sum: ${sum(1, 1)}`);
ns.tprint(`multiply: ${multiply(2, 3)}`);
} tsx.tsx: import { LibText } from "/libTsx";
import { sum } from "/sum";
import { multiply } from "/multiply";
export async function main(ns: NS) {
ns.tprintRaw(LibText);
ns.tprintRaw(<>From Tsx</>);
ns.tprint(`sum: ${sum(1, 1)}`);
ns.tprint(`multiply: ${multiply(2, 3)}`);
} |
About the problem of the warning from If you want to use another library instead of
|
I agree that the warning does not indicate an actual problem. However, that in and if itself is the problem: the way warnings are set up in our project, they appear high severity, always. If we submit this as-is, it will be a continual source of confusion to new developers, and annoyance to existing ones. It will also tend to cause "warning blindness." So, if we can't prevent the warning from occurring, I think we have only two choices: downgrade/ignore warnings in our project, or switch packages. If we could somehow downgrade/ignore only this warning, that would be ideal, but it seems unlikely. |
Aren't we already doing this? swc/wasm-web is already in package.json. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't need to be done in this PR, but I wonder if it would make sense to use babel to rewrite imports for us, rather than as a separate step. (We could also use this for JavaScript, at least in the non-legacy case).
I applied Snarling's suggestion to suppress the warning caused by babel-standalone. There is a small problem, though. I tried stats: statsConfig,
ignoreWarnings: [
{
file: /\.\/node_modules\/@babel\/standalone\/babel\.js/,
},
], This setting works: stats: statsConfig,
ignoreWarnings: [
{
module: /@babel\/standalone/,
message: /Critical dependency: the request of a dependency is an expression/
},
], |
It's only there for testing. If we decide to use |
I pushed an alternative way to supress the warning to catloversg#1. It works by
|
Either Tom's approach or the amended ignoreWarnings config seem good to me. I'd prefer not to replace a warning with another warning. :D |
swc/wasm-web is a regular dependency, not a devDependency, so currently it will be bundled with the app I'm pretty sure (even if not used). |
What I mean is that I'll remove all swc-related things (swc-related code, the |
We don't have any more blockers now. When the maintainers make the final decision on Babel vs. SWC, I'll finalize the code and amend the documentation. |
I don't have a strong opinion either way, and furthermore I think there's a decent chance that whatever we choose, it might get switched out in the future due to unforseen (or foreseen) reasons. I think how it is now is fine, which I think is swc for pure transform? The size comes primarily from the wasm file itself, right? That should mean that it is only network-loaded when needed, i.e. if you are running a ts/jsx file, and thus the size impact for js-only users (the vast majority) is only really for installed Steam size, where that much is nothing. The main thing is to document the architectural ideas (these are independent of which package we choose), and then the rationale of specific packages is a much smaller footnote. |
In order to use SWC, we need to call the async function try {
await initSwc();
await Engine.load(saveData);
} catch (error) {
console.error(error);
ActivateRecoveryMode(error);
await Engine.load("");
setLoaded(true);
return;
} The wasm file will always need to be loaded. I could not find a good way to make it load-only-when-needed while avoiding the race condition that you mentioned. If you find a good way to do it, just let me know. |
yolo |
This PR adds support for JSX, TS, TSX script files.
Transform: Babel vs esbuild-wasm vs swc-wasm
Firefox (SpiderMonkey):
Sample log:
Chrome/Electron app (V8):
Sample log:
esbuild-wasm has a big downside: its
transform
function is an asynchronous function. @d0sboots pointed out a race condition in #1173. Babel and swc-wasm have synchronous functions for transforming, so they don't have this problem.Using swc-wasm has a downside: wasm file of swc-wasm is fairly big (~19.5 MiB). Babel has an advantage in this case: it's relatively small (
babel.min.js
is ~2.71 MiB) and I already chose it to parse the AST (check the next part).I chose swc-wasm to transform scripts, but I also added the testing code for Babel (it can be enabled by setting
globalThis.forceBabelTransform
).Benchmark code:
@Snarling @d0sboots Please make your decision on which tool we should use.
Parse AST
We have 3 choices:
acorn
to parse the AST withacorn-typescript
plugin. Reuse the walking code that usesacorn-walk
.acorn-typescript
is slow (up to 2X parsing time compared tobabel-parser
).acorn-typescript
has bug(s). It cannot parse this code:let x = 0;x!++;
.acorn-typescript
when parsing TS code. When we parse TS code (not TSX code),acorn-typescript
cannot deal with the ambiguous syntax of TypeScript's type assertion and JSX tag:const a = <any> 0
. That code is invalid in TSX file but valid in TS file. However,acorn-typescript
cannot parse it because the JSX support is always enabled (v1.4.13).babel-parser
to parse the AST to Babel AST. Usebabel-traverse
to walk the AST.babel-parser
to parse the AST to estree AST (with theestree
plugin). Reuse the walking code that usesacorn-walk
with 2 small extensions. I choose this approach because of these reasons:Other changes
Fix a bug that parses module(s) multiple times
Search for
!parseQueue.includes(additionalModule)
inRamCalculations.ts
.Test case:
main.js
:lib1.js
:lib2.js
:common.js
:In this case,
common.js
is parsed twice.Fix wrong Import Error message
Avoid parsing AST twice when checking for infinite loop and RAM usage
Check the code of
debouncedCodeParsing()
inScriptEditorRoot.tsx
.Remove
CursorPositions.ts
This file was used long ago to keep track the cursor position in the editor, but it's not used anymore.
CursorPositions.saveCursor()
is still called when creating a new script (set default cursor position for the new script), butCursorPositions.getCursor()
is not called anywhere else.Caveat
I disable 2 error codes in the editor:
import { something } from /lib
,monaco-editor
tries to find the module/lib
and it won't be able to find it.Please ping me on Discord to discuss about these two problems.