-
Notifications
You must be signed in to change notification settings - Fork 585
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
Binding resolution refactored #5802
Conversation
coreOut("};"); | ||
} | ||
coreOut(` | ||
out("declare module 'realm/binding' {"); |
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.
This is one of the more important changes allowing us to import from "realm/binding" via paths
in the tsconfig and have the types resolve as expected.
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 think you are missing a word after "without"
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.
Thanks - I updated the comment 👍
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 still don't love that this file is picked up by the src/*.ts
glob and injects its contents into a virtual module rather than being found via an import
path. But I don't currently have a great idea for an alternative if we want to move away from rollup's replace plugin.
both("// Enums"); | ||
for (const e of spec.enums) { | ||
// This pattern is copied from the TS compiler | ||
both(`export var ${e.jsName};`); | ||
both(`(function (${e.jsName}) {`); | ||
both( | ||
...e.enumerators.map(({ jsName, value }) => `${e.jsName}[${e.jsName}["${jsName}"] = ${value}] = "${jsName}";`), | ||
); | ||
both(`})(${e.jsName} || (${e.jsName} = {}));`); | ||
} |
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.
Probably the most controversial change, but not a large price to pay to avoid relying on code replacements from the Rollup config.
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.
const enums don't generate any js code: https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBAYwgOwM72MgrgWzgMQgjgG8BYAKDmrgDMiAaOAIwEMom2AvSgXyA. It only adds declarations to the .d.ts file.
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 believe const enum
is only supposed to be used in non-declaration files.
Here's an example:
{
"name": "ts-const-enums",
"private": true,
"type": "module",
"scripts": {
"test": "tsx index.ts"
},
"dependencies": {
"tsx": "^3.12.7",
"typescript": "^5.0.4"
}
}
// index.ts
import { Foo } from "./enums";
console.log(Foo.bar);
// enums.d.ts
export const enum Foo {
bar = "bar"
}
// enums.js
// note how we provide no implementation
Running npm test
yeilds:
/Users/kraen.hansen/Projects/ts-const-enums/index.ts:1
import { Foo } from "./enums";
^
SyntaxError: The requested module './enums' does not provide an export named 'Foo'
at ModuleJob._instantiate (node:internal/modules/esm/module_job:123:21)
at async ModuleJob.run (node:internal/modules/esm/module_job:189:5)
at async Promise.all (index 0)
at async ESMLoader.import (node:internal/modules/esm/loader:527:24)
at async loadESM (node:internal/process/esm_loader:91:5)
at async handleMainPromise (node:internal/modules/run_main:65:12)
Even if we do provide a dummy implementation of Foo
in enums.js
:
export const Foo = {};
This simply prints undefined
.
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.
Maybe it behaves differently for string vs number const enum
s? When I tested locally with this patch, everything seems to work:
diff --git a/packages/realm/bindgen/src/templates/node-wrapper.ts b/packages/realm/bindgen/src/templates/node-wrapper.ts
index c66cf638c..364d7d840 100644
--- a/packages/realm/bindgen/src/templates/node-wrapper.ts
+++ b/packages/realm/bindgen/src/templates/node-wrapper.ts
@@ -131,17 +131,6 @@ export function generate({ spec: rawSpec, file }: TemplateContext): void {
}
`);
- both("// Enums");
- for (const e of spec.enums) {
- // This pattern is copied from the TS compiler
- both(`export var ${e.jsName};`);
- both(`(function (${e.jsName}) {`);
- both(
- ...e.enumerators.map(({ jsName, value }) => `${e.jsName}[${e.jsName}["${jsName}"] = ${value}] = "${jsName}";`),
- );
- both(`})(${e.jsName} || (${e.jsName} = {}));`);
- }
-
const injectables = [
"Long",
"ArrayBuffer",
diff --git a/packages/realm/bindgen/src/templates/typescript.ts b/packages/realm/bindgen/src/templates/typescript.ts
index 3210f1f5c..0091d5f34 100644
--- a/packages/realm/bindgen/src/templates/typescript.ts
+++ b/packages/realm/bindgen/src/templates/typescript.ts
@@ -203,7 +203,7 @@ export function generate({ spec: rawSpec, file }: TemplateContext): void {
out("// Enums");
for (const e of spec.enums) {
// Using const enum to avoid having to emit JS backing these
- out(`export enum ${e.jsName} {`);
+ out(`export const enum ${e.jsName} {`);
out(...e.enumerators.map(({ jsName, value }) => `${jsName} = ${value},\n`));
out("};");
}
Of course, I've often wondered whether we should be using enum
or const enum
for these. const enum
may provide some perf and size benefits, but I don't know how much that matters given the JIT in node and the bytecode compiler for hermes. But it certainly shouldn't be slower, and I don't think we've needed the features of a non-const enum yet (eg mapping numeric values to/from string names at runtime)
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.
When I apply this, the SDK unit test fails with:
✖ ERROR: TypeError: Cannot read properties of undefined (reading 'Get')
at <anonymous> (/Users/kraen.hansen/Projects/realm-js/packages/realm/src/platform/network.ts:35:23)
at Object.<anonymous> (/Users/kraen.hansen/Projects/realm-js/packages/realm/src/platform/network.ts:62:17)
Do you mind holding off on any changes to bindgen until I get #5769 over the line. I hope to have it merged today or tomorrow. |
5f08a18
to
a7ee197
Compare
// Using const enum to avoid having to emit JS backing these | ||
out(`export enum ${e.jsName} {`); |
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.
You lost the const
in const enum
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.
This is on purpose. Having a const enum
in a non .d.ts file made the tests fail as it didn't have values at runtime. I suppose TSC only inlines these if they're in a TS source file.
both("// Enums"); | ||
for (const e of spec.enums) { | ||
// This pattern is copied from the TS compiler | ||
both(`export var ${e.jsName};`); | ||
both(`(function (${e.jsName}) {`); | ||
both( | ||
...e.enumerators.map(({ jsName, value }) => `${e.jsName}[${e.jsName}["${jsName}"] = ${value}] = "${jsName}";`), | ||
); | ||
both(`})(${e.jsName} || (${e.jsName} = {}));`); | ||
} |
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.
const enums don't generate any js code: https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBAYwgOwM72MgrgWzgMQgjgG8BYAKDmrgDMiAaOAIwEMom2AvSgXyA. It only adds declarations to the .d.ts file.
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.
Delete line 132 if you remove core.ts.
a7ee197
to
3ffb6d6
Compare
I've pushed a commit pointing core to realm/realm-core#6604, which I'll change once the Core PR has been merged and released. |
bd6a8cf
to
c42166a
Compare
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.
Comments are just suggestions. Everything looks correct.
function both(...content: string[]) { | ||
reactLines.push(...content); | ||
nodeLines.push(...content); |
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.
This change now seems unnecessary. But also probably harmless. 🤷
@@ -20,14 +20,15 @@ import { bindModel, Property } from "@realm/bindgen/bound-model"; | |||
import { TemplateContext } from "@realm/bindgen/context"; | |||
|
|||
import { doJsPasses } from "../js-passes"; | |||
import { eslintFormatter } from "../formatters/eslint-formatter"; |
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'm not sure it is worth introducing a new directory for just this one file. Personally, since the formatters are tiny, I would just put all of them in a single ../formaters.ts
file, even if we had more than one.
But at the very least, lets remove some redundancy in src/formatters/eslint-formatter.ts
. If you want to keep one file per formatter, I'd go either src/formatters/eslint.ts
or just src/eslint-formatter.ts
. Slight preference for the latter if you don't want just src/formatters.ts
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.
Moved it one level up again 👍
@@ -31,6 +31,7 @@ import { | |||
Pointer, | |||
Template, | |||
} from "@realm/bindgen/bound-model"; | |||
import { clangFormatter } from "@realm/bindgen/formatter"; |
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.
It feels a bit weird to call this clangFormatter
since the tool is named clang-format
, and the tool called clang
doesn't do any formatting. I admit that clangFormatFormatter
sounds ridiculous though. Maybe we should just call the formatters by the name of the tool with no suffix, so just clangFormat
and eslint
?
@@ -162,7 +149,7 @@ export function generate({ spec: rawSpec, file }: TemplateContext): void { | |||
|
|||
const spec = doJsPasses(bindModel(rawSpec)); | |||
|
|||
const coreOut = file("core.ts", "eslint", "typescript-checker"); | |||
const coreOut = file("core.ts", eslintFormatter); |
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.
const coreOut = file("core.ts", eslintFormatter); | |
// This file is imported by the virtual path "realm/binding/core" in TS. | |
const coreOut = file("core.ts", eslintFormatter); |
coreOut("};"); | ||
} | ||
coreOut(` | ||
out("declare module 'realm/binding' {"); |
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 still don't love that this file is picked up by the src/*.ts
glob and injects its contents into a virtual module rather than being found via an import
path. But I don't currently have a great idea for an alternative if we want to move away from rollup's replace plugin.
@@ -216,7 +205,7 @@ export function generate({ spec: rawSpec, file }: TemplateContext): void { | |||
|
|||
out("// Opaque types (including Key types)"); | |||
for (const { jsName } of (spec.opaqueTypes as NamedType[]).concat(spec.keyTypes)) { | |||
out.lines("/** Using an empty enum to express a nominal type */", `export declare enum ${jsName} {}`); | |||
out.lines("/** Using an empty enum to express a nominal type */", `export enum ${jsName} {}`); |
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.
out.lines("/** Using an empty enum to express a nominal type */", `export enum ${jsName} {}`); | |
out.lines("/** Using an empty enum to express a nominal type */", `export const enum ${jsName} {}`); |
We should probably consider some of the alternative mechanisms we discovered for opaque types at some point. But at the very least, we shouldn't imply that there is any actual value with that name.
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.
How about just simply declaring these as abstract classes with a private brand
?
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.
You mean like we already do for classes? Yes, possibly something like that. I wish there was a way to tell TS to model the behavior of Object.create(null)
correctly, so that we could create a class that really had no known properties, but that doesn't seem possible right now.
|
||
/** @internal */ | ||
export * from "../generated/ts/native.mjs"; // core types are transitively exported. | ||
export * from "realm/binding"; |
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 know we just spend a long time figuring out how to make the re-export of every type from core.ts
work through native.d.mts
, but what if we just ... didn't. Do you think it would actually be cleaner to have this file do the export * from "../generated/ts/core";
rather than doing that in the wrapper and the .d.ts? That might let you remove the virtual realm/binding/core
path.
Feel free to ignore if you are just tired of fucking around with that file 😁
import { inject } from "../platform/file-system"; | ||
import { extendDebug } from "../debug"; | ||
import { inject } from "../file-system"; | ||
import { extendDebug } from "../../debug"; |
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.
We should consider defining a virtual path REALM/
or src/
so that imports look the same regardless of where you are in the directory structure. It is really unfortunate that when you move a file around, you need to modify its own imports, not just the imports of it. This is pretty common in C++ projects to always use "fully qualified" paths in #includes, so you use #include "realm/object-store/object.hpp"
rather than #include "./object.hpp"
if you are in realm/object-store/blah.hpp
.
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 think that's a good idea, but I'll leave it for a future PR 👍 Ideally I would also like to find a way to enforce that we import through the internal
module instead of relative paths (for all other files than the platform specific).
packages/realm/tsconfig.json
Outdated
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.
Is this file only used for editors and bundle.d.ts generation?
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 believe so.
Pulled in latest bindgen branch from core
af8ec30
to
0dda196
Compare
0dda196
to
49f3d4e
Compare
I'll merge now, as the two failing "Test Coverage" scripts seem very unrelated to the change I'm proposing. |
Follow-up to #5802 - we shouldn't use declare in a "declare module" block.
What, How & Why?
This fixes part of #5576 by declaring the output of the binding generator as a virtual "realm/binding" module, which we provide a path for implementation via tsconfig
paths
: A default fallback for tests (inpackages/realm/tsconfig.json
) as well as each of the platform specific directories.It also removes the
typescript-checker
, as we concluded this might just be deferred and will happen as as part of building the SDK anyway.