Initial TypeScript setup and conversion (CJS to ESM, ES5 class functions to ES6 classes)#705
Initial TypeScript setup and conversion (CJS to ESM, ES5 class functions to ES6 classes)#705ericyhwang wants to merge 12 commits intotypescriptfrom
Conversation
…ttached to the class functions for better codemod porting
Node 18 has been EoL for a year. Also, dev deps sinon 19+, chai 5+, and sinon-chai 4+ require at least Node 20, since require(esm) was only backported as far back as Node 20 - https://joyeecheung.github.io/blog/2025/12/30/require-esm-in-node-js-from-experiment-to-stability/\#Backporting-require-esm-to-v22-and-v20-LTS
…anscluded from sharedb-mongo tests
| - 18 | ||
| - 20 | ||
| - 22 | ||
| - 24 |
There was a problem hiding this comment.
To try to keep this PR smaller, can we move all the dependency / environment bumps into a separate PR that we can merge first?
There was a problem hiding this comment.
Sure, I can cherry-pick those. Do you have a preference for getting the dep updates into master, or just into the typescript staging branch?
There was a problem hiding this comment.
Let's get it on master. I'm generally of the opinion that the more stuff we can get into master as early as possible, the less painful things will be later.
| - name: Install | ||
| run: npm install | ||
| - name: Lint | ||
| if: github.ref_name != 'typescript' && github.base_ref != 'typescript' # Remove this condition after TS code lints cleanly |
There was a problem hiding this comment.
Again, to try to keep this as close to master as possible, can we just update our eslint config to ignore src/?
|
Also, I did see that in the sharedb-mongo tests, the Node process is left hanging after all the tests pass. I started looking into it with why-is-node-running yesterday, but didn't have time to get to the bottom of it before dinner. Current line of investigation is some interaction between newer sinon/fake-timers (which fakes more timer APIs) and the real Mongo driver. |
alecgibson
left a comment
There was a problem hiding this comment.
Some notes on the "[codemod] Run jscodeshift codemod-cjs-to-esm.ts and codemod-es5-classes.ts" commit — please disregard if addressed later.
Note to self: I've looked at everything in this commit except agent.ts, whose no-whitespace diff wouldn't render in GH.
I'll try to continue reviewing on Monday.
| _docs; | ||
| _forwarders; | ||
| _emitters; | ||
|
|
||
| constructor() { | ||
| this._docs = Object.create(null); | ||
| this._forwarders = Object.create(null); | ||
| this._emitters = Object.create(null); | ||
| } |
There was a problem hiding this comment.
Can we just inline these declaration statements and remove this unneeded constructor()?
There was a problem hiding this comment.
That we'd want to do manually later, and not via codemod here.
Field initializers have more restrictions than constructor assignments, e.g. they can't rely on constructor params, always run prior to the main constructor body, etc.. In a codemod that's aiming for safety, trying to infer whether it's OK to convert an assignment to an initializer is more trouble than it's worth.
| _docs; | ||
| _forwarders; | ||
| _emitters; |
There was a problem hiding this comment.
I guess again not one for a patch bump, but we may want to consider making these private at some point in the future.
There was a problem hiding this comment.
private fields would break any consumers who may use the internals. While the underscore indicates that they really shouldn't use those fields, I admit that for tests, I've sometimes hooked into the internals of libraries. 😅
| util.digAndRemove(this._emitters, doc.collection, doc.id); | ||
| util.digAndRemove(this._docs, doc.collection, doc.id); | ||
| }; | ||
| import util = require('../../util'); |
There was a problem hiding this comment.
I've honestly lost track in all of the CJS vs ESM nonsense — is import = require well behaved and interoperable? Any reason we're not using import ... from everywhere? I assume the latter requires type definitions(?)
There was a problem hiding this comment.
https://www.typescriptlang.org/docs/handbook/2/modules.html#es-module-syntax-with-commonjs-behavior
- The TS-specific syntax
import __ = require(__)directly compiles to a CJSrequire(). - For the ESM import syntax
import __ from __, when TS is outputting as CJS, it includes a shim in the each JS output file to enable importing modules both an ESM default exportsomeModule.defaultas well as a CJS "whole-module" exportsomeModule.
Here's a TS Playground snippet showing the difference:
https://www.typescriptlang.org/play/?target=1&module=1#code/JYWwDg9gTgLgBGAhjAFgUQM4gCIFMBmiArgDYwCS408+UEIcA5EqowNwBQoksCyKAYQBWGAEq4AjkWBRccALxxZUmbgAUzfowCUnDi3RY8hUhSqwAdEIjAAdmt37+wsZOmyrN+46A
Since I wanted to keep the JS output as close to the CJS original as possible for now, I opted for the direct analogue import __ = require.
If we want to output ESM instead of CJS in the future, then we'd have to switch to import __ from __ syntax, but that's easy enough to do with another codemod.
| RemotePresence.prototype._handleCreateDel = function() { | ||
| this._cacheOp(null); | ||
| this._setPendingPresence(); | ||
| }; | ||
|
|
||
| RemotePresence.prototype._handleLoad = function() { | ||
| this.value = null; | ||
| this._pending = null; | ||
| this._opCache = null; | ||
| this.presence._updateRemotePresence(this); |
There was a problem hiding this comment.
This got skipped by the converter, presumably because I typoed this in the original code 🙈
There was a problem hiding this comment.
Heh, yeah, it's sticking these onto the base class. We can either fix it afterwards, or fix it in the JS now in a separate PR and I'll re-run the codemods.
| DB.prototype.projectsSnapshots = false; | ||
| DB.prototype.disableSubscribe = false; |
There was a problem hiding this comment.
Do these need converting into instance properties?
There was a problem hiding this comment.
Naively switching to an instance property with field initializer would technically be a breaking change, since it's possible some user code is referencing DB.prototype.projectsSnapshots, to read or change the default value.
Prototype property related breakages were an issue we ran into with Derby-related conversions, so I'd updated our codemods back then to be more conservative with prototype properties. For these, I started with the safer approach from the get-go.
We will want to declare them as fields so the type system knows about them. I was going to do that manually later, but maybe the codemod can insert a field declaration...
| export = Backend; | ||
| emitter.mixin(Backend); | ||
|
|
||
| Backend.prototype.MIDDLEWARE_ACTIONS = { |
There was a problem hiding this comment.
Should this be an instance property?
There was a problem hiding this comment.
See above comment about how changing from prototype property to instance property is technically a breaking change.
| submit: 'submit' | ||
| }; | ||
|
|
||
| Backend.prototype.SNAPSHOT_TYPES = { |
| ShareDBError.prototype = Object.create(Error.prototype); | ||
| ShareDBError.prototype.constructor = ShareDBError; | ||
| ShareDBError.prototype.name = 'ShareDBError'; |
There was a problem hiding this comment.
This doesn't feel right
There was a problem hiding this comment.
This is unchanged from the original JS code:
Lines 11 to 13 in b6deed8
It's just relocated out of the class body.
I'm guessing it's so that you can do a check like if (errorInstance.name === 'ShareDBError')
|
I've moved the dev dependency upgrades to #706 and fixed the issue with the hanging Mocha process. I might create a new PR for the TS changes after the next round of feedback, instead of dealing with rebasing, since the codemods make it easy to re-construct the changes. TBD, I'll decide next week. |
This PR covers TypeScript setup, file renames, then using codemods to convert CommonJS to ES modules and ES5 class functions to ES6 classes.
It targets the
typescriptbranch, which we can use to stage the TypeScript changes until they're ready to merge into main. Making smaller intermediate PRs into that branch will make things easier to review.For now, my goal with the
typescriptstaging branch is to end up with TS source code that compiles into JS output compatible with the current JS source, so we could publish it a minor version. In a future major version, we can choose to update the compile target to drop ES5 support, especially if we want to start using things like async/await without the extra verboseness of downleveling.We should probably major version soon anyways, since Node 18's been EoL for a year, and Node 20's about to reach EoL. The newer testing libraries actually don't run on Node 18, so I'm dropping that version from our CI in this PR.
Reviewer tips and notes:
typescript-initialbranch down and locally view the commit's diff, e.g. in your editor.typescript-cjs-class-codemods-js-outputsbranch, in commit 640885e. Similarly, it's better to pull the branch and compare locally.Let me know if you want to schedule time to go through this on a call!
Changes
nextTickby default, which causes some tests using fake timers to fail. To fix them, I used the new optionclock.setTickMode({mode: 'nextAsync'});, which automatically progresses any immediate-style timers likenextTick.lib/**/*.jsfiles tosrc/**/*.tswith no changesgit blameand other history spelunking tools on the "new" TS files.module.exportsstatements up. Some class JSDoc comments were attached to themodule.exportslines. Moving them so the comments are attached to the class functions means the codemod (next step) can do a better job porting the comments over to the ES6 class.Next steps
First thing after this PR is to go through the many TS compilation errors and fix them. Most fall into these categories:
extendsThen it'll be adding stronger types, likely based on our DefinitelyTyped definitions; doing more cleanup as needed; and converting test files to TS.