Skip to content
Merged
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ node_modules
package-lock.json
jspm_packages

# TypeScript output
lib/

# Don't commit generated JS bundles
examples/**/static/dist/bundle.js
examples/**/dist
11 changes: 10 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const js = require("@eslint/js");
const jsEslint = require("@eslint/js");
const {
defineConfig,
globalIgnores,
} = require("eslint/config");
const eslintConfigGoogle = require('eslint-config-google');
const tsEslint = require('typescript-eslint');

// The ESLint ecmaVersion argument is inconsistently used. Some rules will ignore it entirely, so if the rule has
// been set, it will still error even if it's not applicable to that version number. Since Google sets these
Expand Down Expand Up @@ -54,6 +55,7 @@ var SHAREDB_RULES = {
module.exports = defineConfig([
{
extends: [eslintConfigGoogle],
files: ["**/*.js"],
ignores: ['eslint.config.js'],

languageOptions: {
Expand All @@ -67,6 +69,13 @@ module.exports = defineConfig([

rules: Object.assign({}, DISABLED_ES6_OPTIONS, SHAREDB_RULES),
},
{
extends: [
jsEslint.configs.recommended,
tsEslint.configs.recommended,
],
files: ["**/*.ts"],
},
globalIgnores(["docs/"]),
{
files: ["examples/counter-json1-vite/*.js"],
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^24.12.0",
"chai": "^6.2.2",
"eslint": "^10.1.0",
"eslint-config-google": "^0.14.0",
Expand All @@ -22,7 +23,9 @@
"rich-text": "^4.1.0",
"sharedb-legacy": "npm:sharedb@1.1.0",
"sinon": "^21.0.3",
"sinon-chai": "^4.0.1"
"sinon-chai": "^4.0.1",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.0"
},
"files": [
"lib/",
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
21 changes: 21 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"target": "ES5",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did you decide this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since a goal in this first phase is to keep the JS output compatible with the original JS code (prototype-based classes, var, no for-of), ES5 was really the only choice. An ES6 compile target would mean the TS compiler outputs class syntax, const/let as in the source instead of converting to var, etc.

This can't be ES3 because our JS code uses ES5-specific standard library functions like JSON.parse/stringify, Object.create(), .bind(). etc. Plus, TypeScript 5.5 removed support for ES3 output.


So why not go with ES6 output since all remotely modern browsers support ES6 now?

There would be a less immediately obvious breaking change, where you can't use old-style prototypical inheritance to call a ES6 superclass. If you try to do MyBaseClass.call(this) in the subclass constructor function, you'll get an error TypeError: Class constructor MyBaseClass cannot be invoked without 'new'. A couple references:

In practice what that means, is to upgrade to ES6 classes at runtime, you have to upgrade "leaf nodes" in the class tree to ES6 classes first, then upgrade their direct parents, and so forth up the class chain. (An ES6 class can call super() to invoke a function/prototype base class just fine.)

For ShareDB itself, we'd want to upgrade the leaf-node libraries like sharedb-mongo to ES6 class output first, then sharedb core. We'd also want to indicate in the major version release notes that consumers must either switch to ES6 class output themselves, or use a transpiler like Babel on sharedb code.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool sounds sensible, just wanted it documented. Thanks!

"module": "CommonJS",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Switching to ESM at some point is going to be fun 🙄 )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The require(esm) feature in Node 20+ makes it less of a pain than before, thankfully:
https://joyeecheung.github.io/blog/2025/12/30/require-esm-in-node-js-from-experiment-to-stability/

"types": [
"node"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I like this. On a selfish note, I think this will pollute our own code's types, because we run backend.js even in the browser 😅

But from a less selfish perspective, this shouldn't apply to client/ which is designed for browser consumption. I think we possibly want 2 different tsconfig.json files? One for backend stuff, and one for client?

In an ideal world I think backend and client would be 2 separate packages in a monorepo, but that's obviously out of scope of this change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't affect any consumer code, since we compile to JS (plus eventually d.ts) and publish that.

The TS compiler only cares about the main tsconfig you point it at. For any precompiled dependencies, it just reads the d.ts files.


ShareDB client code already assumes access to Node libraries, necessitating a bundler:

The "types": ["node"] just formally codifies that for the TS compiler.

If we really wanted to do an internal split of tsconfig files, I think we'd want three - client, server, and shared - but even then, we'd still need to reference the Node types in client/shared code due to the two points above. To eliminate that, we'd have to rewrite the shared/client code to avoid Node built-in libraries or functions.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay this is fine for now, but I'd like to sense check when we eventually ship type definitions that @types/node isn't being dragged into client code.

I think the DefinitelyTyped types currently achieve it with things like this:

import { Duplex } from "stream";

Which I think has the "expected behaviour" of:

  • in an environment where @types/node is available, everything is fine
  • in a browser/bundler environment, this fails compilation unless you've provided a polyfill and/or defined your own type stub (or do something hideous like skipLibCheck: true)

],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😢 Why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this enabled, tsc will still type-check d.ts files that are directly or indirectly referenced by your project code, it just skips the default behavior of checking all d.ts files in node_modules.

https://www.typescriptlang.org/tsconfig/#skipLibCheck

I've just gotten used to using it in larger projects, where this can speed up compilation times by quite a bit, but we probably don't need it for ShareDB's relatively small dependency set. I'll remove it.

"forceConsistentCasingInFileNames": true,
"ignoreDeprecations": "6.0",
"noImplicitAny": false,
"noImplicitThis": false,
},
"include": [
"src/**/*.ts"
],
}