Closed kathy-phet closed 7 months ago
From #1206
Deno https://deno.land/ describes itself as: Deno is a simple, modern and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust.
It has built-in support for TypeScript, and looks to have a superior package manager system to node. We have numerous other issues with the existing package management system, and will one day want to be able to use TypeScript in our build side. Deno looks like a good place to investigate. A few hours of investigation may help us rule this out, but if we can go forward, this would be an epic-level issue, so I'll label it as such and add it to the project board.
@liam-mulhall said:
It seems like more JS runtimes are cropping up: https://bun.sh/. Deno might not be the winner despite being worked on by Ryan Dahl. It would probably be a good idea to keep an eye on the various Node alternatives before we pick one.
I used a star history chart maker to compare node, deno and bun https://star-history.com/#denoland/deno&nodejs/node&Jarred-Sumner/bun&Date
UPDATE from MK 4/11/24:
I'm wondering if an easier intermediate step would be to use our existing transpiler interface and convert chipper/perennial code to TypeScript and just invoke it like node ../chipper/dist/
etc. We could potentially have our own alias that forwards calls, like: tsnode ./myscript.ts
. But we would probably at least want to get decent sourcemaps. And changing a fundamental part of our toolchain like that will be nontrivial (whether going to node otherPath or tsnode or deno or https://typestrong.org/ts-node/). Importantly, would grunt work with these changes?
In https://github.com/phetsims/chipper/issues/990, all devs confirmed they are using node 14+, which supports import
, which seems a better fit for TypeScript.
See related issue https://github.com/phetsims/chipper/issues/861#issuecomment-723376852
UPDATE: grunt
is our limiting factor here. It requires require
statements from the grunt-cli. We could swap out grunt-cli for an alias-like function like so:
# https://stackoverflow.com/questions/7131670/make-a-bash-alias-that-takes-a-parameter
grunt2() {
node ../chipper/js/grunt2/grunt2.mjs $1 $2 $3 $4
}
But that is an invasive change that requires everything to be modules.
To step in the right direction toward ES6 modules/typeScript without having to replace 100% of grunt at the outset, we can change Transpiler to transform import statements to require statements (in chipper/perennial only!), then point Gruntfile at chipper/dist.
In doing so, we get advantages:
For these costs:
Anyways, here's a working prototype:
~/apache-document-root/main/mean-share-and-balance$ grunt lint
Running "lint" task
chip away
7
Done.
Here's a simpler change set that accomplishes the same thing.
Do that, then rename chipper/Gruntfile.js to Gruntfile.ts and run grunt
or grunt lint
from mean-share-and-balance. The only thing failing for that part is WebStorm can't find a lint configuration (works fine from the command line):
From today's dev meeting:
SR: OK if we start allowing *.ts in chipper? Context is https://github.com/phetsims/chipper/issues/1272
Grunt would still work but for scripts that use .ts you would have to launch from node chipper/dist/js/chipper/myscript.js
Stack traces wouldn’t line up in this version
Or should we wait for sprint/quarterly goal?
Transpiler would remain in 100% .js so there is no chicken-and-egg problem
MK: Have you thought about ts-node?
SR: It would improve consistency if chipper could use TypeScript. But there isn’t a lot of change happening in chipper right now. We could wait to do this until there is larger work to be done in chipper.
SR: Are developers OK using different commands/tools to run TS tasks?
That seems fine.
SR: 2-4 devs could do a 2 week sprint to port as much of chipper to TS as possible. OR we get TS working in chipper and sprinkle in porting over time.
SR: Deno will not work with grunt. Ts-node might not work with grunt. Migrating away from grunt is not trivial.
SR: I have a prototype that has grunt working with TypeScript. But it requires an additional step to work with the transpiler.
MK: I recommend pointing everything to chipper/dist, that will allow us to sprinkle in TypeScript at our discretion.
SR: Using chipper/dist for now would not prevent us from using Deno or TS-Node in the future.
JO: Using chipper/dist for now seems better for now so we can avoid technical debt and get into TS faster. Even though it’s somewhat clunky (chipper/dist) for now.
JG: Using TypeScript in chipper sounds really nice. Not sure of the amount of work to get there.
MS: chipper/dist sounds like a good plan for now.
AV: Yes, I don’t have enough data for a meaningful answer based on this.
JB: chipper/dist sounds fine
SR: The unsolved problem is a salmon banner related to linting.
KP: OK You have green light, go for it!
SR: Ok, thanks
Would it be OK for chipper typescript code to import files from phet-core? Uh-oh, that will interfere with grunt’s need to transpile imports to require().
In slack, @zepumph responded:
Yes probably. We have always had quite a separation about tooling and sim code. Maybe we could move optionize and IntentionalAny to perennial-alias?
To me, it would seem reasonable that chipper could use code from phet-core (which doesn't have any external dependencies).
Uh-oh, that will interfere with grunt’s need to transpile imports to require().
The hack above detected code in chipper and automatically converted require statements to import statements (in chipper files). If using files from other repos, this heuristic won't work anymore, and we shouldn't output 2 variants of each files (with imports vs requires).
So it seems that grunt is the main bottleneck preventing us from using es6 modules or typescript. Would we want to use typescript with require()? Maybe, but it is a half measure, and we are trying to get things more consistent across our systems. And that would make it so optionize or other exports couldn't be used by require code.
We also have the constraint that all our tooling relies on grunt-cli and grunt tooling. Checking out an old version of SHAs, you would still expect grunt --brands=phet,phet-io
to work perfectly. We could swap out grunt
for our own alias or executable, but that is confusing and would require each developer (and server machine) tinkering with their setup to make sure it is right. Instead, we can leave grunt
as an adapter layer that invokes our own processes. I tested this with clean
and it is working well, using TypeScript and import
. This could also be done with deno, but I think our workload will be simplified if that is considered as a potential later step, and may not be necessary if the port to TypeScript is good enough in itself.
Prototype so far:
Also, this strategy lets us opt-in one task at a time. Maybe lint would be good to do next, but it has usages in perennial and the lint hooks (as well as chipper). Maybe those sites can just call the new harness so they don't need to be ported all-or-none?
@marlitas and I reviewed the proposal above, and it seems promising. We would like to test on windows before committing it to master.
We reviewed the proposed implementation with @zepumph and tested on Windows. It seemed to have reasonable operational behavior on Windows (low overhead for the spawn), but @zepumph expressed concern about maintaining the grunt layer. I don't like the grunt layer at all but argued that it may be low enough cost to maintain until we are ready to break backward compatibility in our maintenance steps.
I committed the patch above. I also realized this means the transpiler process needs to be running for clean
to work, so we will need to abandon and rethink this approach or transpile chipper on startup. I realized this because of
Therefore, it seems reasonable to transpile chipper on Gruntfile startup. I'm concerned this could cause hiccups on a build server, so this should be tested.
Also, deno would work around that problem.
Lots of red in CT from the commits above:
acid-base-solutions : build
Build failed with status code 1:
Running "report-media" task
Running "clean" task
(node:33663) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
/data/share/phet/continuous-testing/ct-snapshots/1661167440414/chipper/dist/js/chipper/js/phet-build-script/phet-build-script.js:3
import * as fs from 'fs'; // eslint-disable-line bad-sim-text
^^^^^^
SyntaxError: Cannot use import statement outside a module
at wrapSafe (internal/modules/cjs/loader.js:979:16)
at Module._compile (internal/modules/cjs/loader.js:1027:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
at Module.load (internal/modules/cjs/loader.js:928:32)
at Function.Module._load (internal/modules/cjs/loader.js:769:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
at internal/main/run_main_module.js:17:47
Fatal error: Perennial task failed with code: 1
Snapshot from 8/22/2022, 5:24:00 AM
Bayes node version is: v14.15.0, my local version is: v16.13.1. That may explain the difference?
@marlitas and I saw that a package.json in a nested directory like chipper/js/phet-build-script did not seem to be respected. We cannot change chipper/package.json since there is a lot of require
code. We could output to *.mjs but that will require changes to Transpiler.js in 2 places. First, in the place that looks up the corresponding chipper/dist path, and second in pruneStaleDistFiles
. This seems easy enough and maybe we should do that. The other option would be to update everyone to Node 16.
@marlitas and I would like to try the *.mjs strategy first.
I was interested to hear that deno will support importing from the npm ecosystem within the next few months. That will make it a lot easier to move in that direction. https://deno.com/blog/changes#compatibility-with-node-and-npm
In experimenting with chipping away at this, I've considered that:
grunt
adapters would be unfortunate. (@zepumph described this eloquently)Developer experience about how build lifecycle commands are invoked. Do we want a central point that runs all things? Or should things just be scripts that can be run ad-hoc?
Some examples:
grunt --brands=phet,phet-io
node ../chipper/js/scripts/build.js --brands=phet,phet-io
node ../chipper/dist/js/chipper/scripts/build.js --brands=phet,phet-io
phetbuild --brands=phet,phet-io
deno run --allow-write --allow-read ../chipper/phetbuild.ts --brands=phet,phet-io
grunt
or phetbuild
, it could be implemented as a command line executable or an alias for something like node ../chipper/etc
. It would need to work on dev machines and server machines. If we want tasks to be invokable from each other and from command line, there would be some overhead (like if node ../chipper/js/lint needed a command line API as well as an export
API).My recommendation: Wait "3 months" until deno has better npm support, then prototype with it to see if it works with our npm modules. If that seems promising, then write a long-term proposal around using deno
as the entry point with the "bare minimum" of lifecycle commands centralized in separate scripts. deno ../chipper/js/phetbuild/build.ts
deno ../chipper/js/phetbuild/update.ts
etc. Document and formalize this API so it can be kept stable for maintenance builds. Allow arbitrary (unstable) scripts outside of the stable entry points, which do not need to be stable for maintenance builds.
The main advantages of this proposal:
npm install
in every repo every time, just to get grunt adapters. See https://github.com/phetsims/chipper/issues/494npm install grunt-cli
npm install
again in normal developmentThe main costs of this approach:
Deno 1.25 was released with initial support for importing node modules. I tested it out on lodash like so:
import _ from 'npm:lodash';
console.log( _.camelCase( 'Testing the camelCasingOfTheseWORDS' ) );
and it correctly outputted:
testingTheCamelCasingOfTheseWords
I tested it on
import { ESLint } from 'npm:eslint';
const eslint = new ESLint();
const results = await eslint.lintFiles( [ "src/**/*.js" ] );
const formatter = await eslint.loadFormatter( "stylish" );
const resultText = formatter.format( results );
console.log( resultText );
with this .eslintrc
{
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "double"]
}
}
And it correctly outputted:
/Users/samreid/apache-document-root/main/tempo/src/fakefile.js 1:14 error Strings must use doublequote quotes
✖ 1 problem (1 error, 0 warnings)
1 error and 0 warnings potentially fixable with the --fix
option.
I invoked deno like so:
deno --unstable run --allow-env --allow-read --allow-write src/lint.ts
Observations:
Summary:
grunt
prevents us from being able to use import
statements--it is locked to using require
. This also prevents us from being able to import code like optionize
, IntentionalAny
, etc.exec
and introduces nontrivial complexity/overhead/adapters.Summarizing questions from the above:
I feel the Q3 investigation quarterly goal has been accomplished, and it seems appropriate to schedule discussion around Q4 planning.
In https://github.com/phetsims/perennial/issues/284#issuecomment-1347658380, we identified a refactoring issue that has led to many issues and build errors in production builds. This type error could have been caught by a type checking lint rule we already use in sim code. For example:
async function generateReadme(): Promise<void> {
console.log( 'readme' );
}
generateReadme(); // ESLint Error: Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.(@typescript-eslint/no-floating-promises)
As a development team, it is important that we can perform refactoring in our build tools safely. This case is an example of why we should invest in upgrading our build/deployment tools to use TypeScript.
Let's check in at an upcoming development meeting.
CM: How does this fit into our priorities? SR: I don't think it should take priority over sim publishing backlog, but should be scheduled for once we clear that up. May hold back some chipper/perennial work until we have Deno set up as a foundation. SR: grunt is incompatible from TS, and is why we can't make some improvements there. Before making progress on Deno we should decide how we want to deal with grunt. JO: this will involve a lot of planning on how we handle the new tooling. JB: I was reading part of this article: https://www.keithcirkel.co.uk/why-we-should-stop-using-grunt/ Should we be using just NPM scripts? It would be nice to not have to rely on grunt
This will be brought up again during Quarterly/ monthly planning as it is tracked by the epic label. Moving to "done discussing"
Chipper/Perennial testing is also connected: https://github.com/phetsims/chipper/issues/1012
After reviewing this issue, I feel we should continue with grunt
as as our primary command line interface we take the first several steps forward. Redesigning our entire command line experience is not a prerequisite for making progress here. We can proceed as follows:
Most of this idea has been discussed/approved/vetted in https://github.com/phetsims/chipper/issues/1272#issuecomment-1212289150, but we turned away from that approach since we didn't have a plan to output phet-core to chipper/dist/js and chipper/dist/commonjs. But maybe that is the right direction to investigate.
Our progress in moving chipper to TypeScript would simplify a future transition to Deno, if we still want that as a future step.
Keep in mind the chipper/js/scripts/transpile should remain in *.js. Also, I'm concerned about other tooling that uses grunt output-js
and grunt output-js-project
and grunt output-js-all
. This code may be running on CT or the build server or for maintenance tasks. If we change it up so that those script cannot run without a prior chipper/js/scripts/transpile
we will have to visit each site and make sure it is good. Would be good to check with @jonathanolson on this detail.
UPDATE: There are 4 places where output-js is called:
Brainstorming one way around that problem: we could commit the dist/commonjs output to avoid the "have to transpile before running the transpiler" step. However, that would be a long-term headache that you would have to commit the source file and compiled file each time which is a bad practice.
There are also other ways to start getting into typescript without abandoning grunt, and without having to pre-transpile chipper before using it to transpile other code. One way is to have grunt tasks exec deno tasks as described in https://github.com/phetsims/chipper/issues/1272#issuecomment-1213565051.
There is another way around the problem of having tooling to make sure the dependencies are transpiled, kind of like how deno does it. We can just transpile during startup. This patch demonstrates using typescript for one file in the grunt build subsystem, and doesn't require any tooling steps, committing builds or anything like that. But it does (a) add a step during startup (b) a separate transpilation output directory and (c) and importing from the harness into the alternate transpilation directory and (d) would stack traces be incorrect without a sourcemap processor?
This patch demonstrates loading optionize
from a chipper/js/grunt *.ts file:
Other ideas to investigate:
This patch has improved caching and startup time. I'm looking into how to transpile the stack traces:
This patch is working very well:
This one has a better entry point:
@jonathanolson was working on Transpiler.js for as part of WGSL for https://github.com/phetsims/chipper/commit/6830a15b93b50334255253c67f6bc37ba58c2869, so we also checked in on the patch above.
We tested the startup overhead, and saw that on my Macbook Air M1 it was typically around 50ms-80ms of additional startup overhead when no files had changed, and 300ms-500ms when one large file (like Gruntfile.js) had changed. I believe we could reduce that further if we do transpilation more eagerly in a watch process (for devs), but the build server won't be able to use that as an optimization (no watch process). We would also want to test that on Windows since we have seen it have slower process on this side. Also, if we like everything about this except for the startup time, we could explore a faster transpiler like swc.
We also discussed that the proposal in the patch, and that it would allow us to start opting in to *.ts in our build tools, would be compatible with a future transition to deno, if and when we do that. Deno uses import instead of require, but none of our work converting to typescript would be wasted.
We also discussed the main.js harness used by grunt would not work for other (non-grunt) scripts, and we would need a solution for that. But as far as I know, most of the build server/CT/developer tasks that must be run in the normal lifecycle of development are through grunt, and we would be free to use a different startup strategy for other scripts.
But keep in mind you cannot mix and match import and require, so each dependency tree would need to use one or the other but not both.
I neglected to mention that the stack traces are not transpiled--I couldn't get that working.
Generally, @jonathanolson said it seems like a promising strategy. I think it would be good to try to get to a commit point for it.
We have a goal this iteration to do SOMETHING (anything) with this issue in the next 2 weeks. Adding assignees and noting that likely we will want to wait until after we do https://github.com/phetsims/phet-io/issues/1974 to start this.
@marlitas indicated @matthew-blackman and I would work on this in the coming iterations.
Over in https://github.com/phetsims/chipper/issues/1430#issuecomment-2034973604, @samreid and I found that we may need to provide custom settings to repos that use both Node and browser TypeScript, since right now, with options omitted, there is an auto discovery process that allows types to mix in the same file. See https://stackoverflow.com/a/70856713/3408502
@samreid reviewed the patch in https://github.com/phetsims/chipper/issues/1272#issuecomment-1730119939 and it looks very promising! We also discussed how this strategy (allowing baby step conversion to typescript) is likely inline with a lot of the work we would do if going "all in" on something like Deno. For example, chipper/js/grunt/fixEOL
will likely look the same as a deno TS file as it would as a piecemeal/custom TS file.
I refreshed the patch above and it is working well. I also have a demo porting lint.js to lint.ts and exercising it:
Patch that demonstrates using optionize from chipper grunt lint:
This issue was discussed in the 4/11/2024 developer meeting. Here are some notes:
See the developer notes document https://docs.google.com/document/d/11Gt3Ulalj0fCD2fFeCjPT5ni_9mM2WjkGc4ysisQmo8/edit for many more notes on this discussion.
The general conclusion is that we should definitely go for an incremental process, and @samreid will review everyone's input in the developer notes doc and we will subsequently discuss the incremental plan, and @marlitas will get this item on the planning list for the next iteration.
More changes and TODOs based on collaboration with @zepumph.
Also, the revelation that we can use import statements anywhere in the chain and everything will still work because the transpiler outputs require statements that grunt can consume.
I committed some changes. The new patch is:
I committed some more parts to simplify the remainder:
Next steps:
I swamped this issue with commits, so will continue in #1437. Closing.
Liam also noted this other one: https://bun.sh/