From 9b917c2d28c4d29f3c9cb41a143bd7cb0038c9fe Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Sun, 14 Sep 2025 02:50:06 +1000 Subject: [PATCH 01/51] chore: Changesets for changes introduced after 1.0.0 but prior to changesets being merged. --- .changeset/acher-neurogliosis-distance.md | 5 + .changeset/actinotoxemia-scryer-uncantoned.md | 5 + .changeset/arethuse-chlamydozoa-revere.md | 5 + .../asphyxiator-perceptionism-doorkeeper.md | 5 + .changeset/barter-uxorious-unsuspecting.md | 5 + .changeset/beamlike-wartless-endamage.md | 5 + .changeset/besiren-hodden-margarin.md | 25 ++++ .changeset/brighid-pit-mortcloth.md | 110 ++++++++++++++++++ .changeset/clancularly-rattlejack-maguey.md | 5 + .../cresylite-unjudiciously-misshapen.md | 5 + .../cylindrella-prediluvial-araneology.md | 5 + .changeset/decretorily-enveil-misexplain.md | 5 + .changeset/exosmotic-pedatisect-kaffir.md | 5 + .changeset/flanque-catastate-whirrey.md | 23 ++++ .../haussmannization-lassock-synedrous.md | 5 + .../illusiveness-metopomancy-fibroblast.md | 5 + .changeset/introversible-roupet-retinal.md | 5 + .changeset/laverania-euxine-coccygotomy.md | 5 + .../meiobar-poisonable-prussification.md | 8 ++ .../metascutellar-phaethontic-collop.md | 5 + .changeset/muscovitize-theirselves-livish.md | 18 +++ ...nperformer-sulfhydrate-stupefactiveness.md | 5 + .../nonsettlement-sobralite-devilish.md | 5 + .changeset/norseler-nozzler-swill.md | 5 + .changeset/precook-pipemouth-coelector.md | 5 + .../prefigurement-benziminazole-chokestrap.md | 5 + .../pyrrolidine-monocercous-nucleolinus.md | 5 + .changeset/quinqueradial-firer-longicone.md | 20 ++++ .changeset/roadmaster-grotto-disrespectful.md | 5 + .../sclerogenous-deliverance-drenchingly.md | 5 + .../sheepstealing-fraticellian-miscoinage.md | 18 +++ .changeset/shippo-tanh-luceres.md | 5 + .../somnifuge-formicicide-karyorrhexis.md | 5 + .changeset/sophia-thalessa-flanch.md | 5 + .changeset/subpoenal-lively-unconvenience.md | 5 + .changeset/unaproned-periorchitis-pierce.md | 5 + .changeset/undergrub-perclose-telotype.md | 5 + .changeset/unhandseled-pockily-acyloin.md | 5 + .../unromantically-analogon-monochloro.md | 6 + 39 files changed, 383 insertions(+) create mode 100644 .changeset/acher-neurogliosis-distance.md create mode 100644 .changeset/actinotoxemia-scryer-uncantoned.md create mode 100644 .changeset/arethuse-chlamydozoa-revere.md create mode 100644 .changeset/asphyxiator-perceptionism-doorkeeper.md create mode 100644 .changeset/barter-uxorious-unsuspecting.md create mode 100644 .changeset/beamlike-wartless-endamage.md create mode 100644 .changeset/besiren-hodden-margarin.md create mode 100644 .changeset/brighid-pit-mortcloth.md create mode 100644 .changeset/clancularly-rattlejack-maguey.md create mode 100644 .changeset/cresylite-unjudiciously-misshapen.md create mode 100644 .changeset/cylindrella-prediluvial-araneology.md create mode 100644 .changeset/decretorily-enveil-misexplain.md create mode 100644 .changeset/exosmotic-pedatisect-kaffir.md create mode 100644 .changeset/flanque-catastate-whirrey.md create mode 100644 .changeset/haussmannization-lassock-synedrous.md create mode 100644 .changeset/illusiveness-metopomancy-fibroblast.md create mode 100644 .changeset/introversible-roupet-retinal.md create mode 100644 .changeset/laverania-euxine-coccygotomy.md create mode 100644 .changeset/meiobar-poisonable-prussification.md create mode 100644 .changeset/metascutellar-phaethontic-collop.md create mode 100644 .changeset/muscovitize-theirselves-livish.md create mode 100644 .changeset/nonperformer-sulfhydrate-stupefactiveness.md create mode 100644 .changeset/nonsettlement-sobralite-devilish.md create mode 100644 .changeset/norseler-nozzler-swill.md create mode 100644 .changeset/precook-pipemouth-coelector.md create mode 100644 .changeset/prefigurement-benziminazole-chokestrap.md create mode 100644 .changeset/pyrrolidine-monocercous-nucleolinus.md create mode 100644 .changeset/quinqueradial-firer-longicone.md create mode 100644 .changeset/roadmaster-grotto-disrespectful.md create mode 100644 .changeset/sclerogenous-deliverance-drenchingly.md create mode 100644 .changeset/sheepstealing-fraticellian-miscoinage.md create mode 100644 .changeset/shippo-tanh-luceres.md create mode 100644 .changeset/somnifuge-formicicide-karyorrhexis.md create mode 100644 .changeset/sophia-thalessa-flanch.md create mode 100644 .changeset/subpoenal-lively-unconvenience.md create mode 100644 .changeset/unaproned-periorchitis-pierce.md create mode 100644 .changeset/undergrub-perclose-telotype.md create mode 100644 .changeset/unhandseled-pockily-acyloin.md create mode 100644 .changeset/unromantically-analogon-monochloro.md diff --git a/.changeset/acher-neurogliosis-distance.md b/.changeset/acher-neurogliosis-distance.md new file mode 100644 index 00000000..54d2e883 --- /dev/null +++ b/.changeset/acher-neurogliosis-distance.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** Statically resolved return type for `get_node()`/`getNode()`. diff --git a/.changeset/actinotoxemia-scryer-uncantoned.md b/.changeset/actinotoxemia-scryer-uncantoned.md new file mode 100644 index 00000000..c5c2e671 --- /dev/null +++ b/.changeset/actinotoxemia-scryer-uncantoned.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** `ResourceLoader.load()` overload type. diff --git a/.changeset/arethuse-chlamydozoa-revere.md b/.changeset/arethuse-chlamydozoa-revere.md new file mode 100644 index 00000000..c8a76335 --- /dev/null +++ b/.changeset/arethuse-chlamydozoa-revere.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Improved type conversion error messages. diff --git a/.changeset/asphyxiator-perceptionism-doorkeeper.md b/.changeset/asphyxiator-perceptionism-doorkeeper.md new file mode 100644 index 00000000..18250904 --- /dev/null +++ b/.changeset/asphyxiator-perceptionism-doorkeeper.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** `@export_` support for enums as keys of `GDictionary`. diff --git a/.changeset/barter-uxorious-unsuspecting.md b/.changeset/barter-uxorious-unsuspecting.md new file mode 100644 index 00000000..6c0792a0 --- /dev/null +++ b/.changeset/barter-uxorious-unsuspecting.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Background thread script instantiation (for 4.5 editor). diff --git a/.changeset/beamlike-wartless-endamage.md b/.changeset/beamlike-wartless-endamage.md new file mode 100644 index 00000000..acc8f679 --- /dev/null +++ b/.changeset/beamlike-wartless-endamage.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Handle `hint_string` if `@export_var` is a `GArray` with an element type provided via `details.class_`. diff --git a/.changeset/besiren-hodden-margarin.md b/.changeset/besiren-hodden-margarin.md new file mode 100644 index 00000000..eb433add --- /dev/null +++ b/.changeset/besiren-hodden-margarin.md @@ -0,0 +1,25 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Improved user project type configurability. + +- Codegen for scenes and resources now default to being stored in + `gen/types` rather than `typings/`. However, a setting has been + introduced to configure this. `typings/` is no longer used + because the directory is configured as a type root, which means + directories contained within are expected to be module + definitions. +- `"types": ["node"]` in the default tsconfig was hiding type errors. + This is no longer set. Essentially, this setting was disabling + all types except node types from our type roots. +- `@types/node` removed from the default `package.json`. It was + misleading and made it easy to accidentally use non-existent + functionality. We no longer need these types because... +- Our JS essentials (console, timeout and intervals APIs) are now + included in our `godot.minimal.d.ts`. +- No longer including `` in our TS + files. This seemed to be interfering with user tsconfig options, + and it was not required. Not that it's been removed you're able to + more freely make changes to your tsconfig. For example, you may set + `libs` (or `target`) to make use of newer JS APIs. diff --git a/.changeset/brighid-pit-mortcloth.md b/.changeset/brighid-pit-mortcloth.md new file mode 100644 index 00000000..ea49ab60 --- /dev/null +++ b/.changeset/brighid-pit-mortcloth.md @@ -0,0 +1,110 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Ergonomics overhaul. Camel-case, TS types, codegen + more. + +- There's now a project setting which can be toggled to swap to a + more idiomatic JS naming scheme for Godot bindings. We use + camel and pascal case to more closely align with typical + JavaScript/TypeScript conventions. For `@Decorators` we've gone + with pascal case, which is used in libraries like Angular. Camel + case is perhaps more popular, but pascal case allows us to avoid + reserved names, and thus we can cleanly write `@Export`, instead of + needing to include the trailing underscore on `@export_`. + +- TypeScript types have been improved. Particularly `Signal<>` and + `Callable<>`. `Signal1`, `Signal2`, etc. are now deprecated, as are + `AnySignal` and `AnyCallable`, since the new `Signal` and `Callable` + types handle an arbitrary number of parameters. Importantly, + `Callable.bind(...)` is now accurately typed, so you'll receive + type errors when connecting to signals. + +- `GArray` and `GDictionary` now have a static `.create()` method + which allows you to create nested data structures from literals + and benefit from full type checking. When a `GArray` or `GDictionary` + is expected as a property, a `.proxy()` can be provided in its + place. + +- Partially worked around https://github.com/microsoft/TypeScript/issues/43826 + whereby our proxied `GArray` and `GDictionary` always return proxied + nested values, but will accept non-proxied values when mutating + a property. Basically, there's now `GArrayReadProxy` and + `GDictionaryReadProxy`. These aren't runtime types, they're just + TS types that make it easier to work with proxies. Under normal + circumstances, you likely won't need to know these types exist. + +- Codegen leveled up. Any TS module can now export a function + named `codegen` with the type `CodeGenHandler`. This function + will be called during codegen to allow you to optionally + augment type generation involving user-defined types. Consider, + for example, the `SceneNodes` codegen which previously only knew + how to handle Godot/native types in the scene hierarchy. When + a user type was encountered, it'd write the native type, which + is still useful, but it'd be nice to be able to include user + types. The reason we don't by default is user types are not + required to follow our generic parameter convention where + each node is passed a `Map` argument. + + Let's see an example: + + ```ts + export default class CardCollection extends GameNode + ``` + + the type above does not take a `Map`. Perhaps more interesting, it + takes a different generic parameter, a `CardNode`. If we encounter + a `CardCollection` script attached to a node in the scene somewhere, + GodotJS' internal codegen can't possibly know what that generic + parameter ought to be. So we can help it out. In the same file + where `CardCollection` is defined, we could provide a `codegen` + handler like so: + + ```ts + export const codegen: CodeGenHandler = rawRequest => { + const request = rawRequest.proxy(); + + switch (request.type) { + case CodeGenType.ScriptNodeTypeDescriptor: { + const cardNodeScript = request.node.get('cardNodeScript'); + return GDictionary.create({ + type: DescriptorType.User, + name: 'CardCollection', + resource: 'res://src/card-collection.ts', + arguments: GArray.create([ + GDictionary.create({ + type: DescriptorType.User, + name: cardNodeScript?.getGlobalName() ?? 'CardNode', + resource: cardNodeScript?.resourcePath ?? 'res://src/card-node.ts', + }), + ]), + }); + } + } + + return undefined; + }; + ``` + + Above we handle the codegen request to determine the node type + of the provided `request.node`. What's *really* neat here is we + don't need to hard-code that generic. We've instead exported a + configurable `Script` reference for use in the editor: + + ```ts + @ExportObject(Script) + cardNodeScript: Script = ResourceLoader.load('res://src/card-node.ts') as Script; + ``` + + So the codegen logic simply grabs the type exported from the + chosen script, and provides it as a generic argument to + `CardCollection<>`. + + One thing worth noting, your class does NOT need to be a `@Tool`. + In the above example, `CardCollection` is not a `@Tool`, and + hence the node script is not instantiated during codegen, which + is why we've used `request.node.get('cardNodeScript')` rather + than trying to access the property directly. That said, if you + want, codegen can be combined with `@Tool`. + +- There's also a bunch of logging/error reporting improvements. diff --git a/.changeset/clancularly-rattlejack-maguey.md b/.changeset/clancularly-rattlejack-maguey.md new file mode 100644 index 00000000..3c3269d2 --- /dev/null +++ b/.changeset/clancularly-rattlejack-maguey.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Introduced a setting to control whether `.d.ts` for scenes are auto-generated on when scenes are saved in the Editor. diff --git a/.changeset/cresylite-unjudiciously-misshapen.md b/.changeset/cresylite-unjudiciously-misshapen.md new file mode 100644 index 00000000..12287605 --- /dev/null +++ b/.changeset/cresylite-unjudiciously-misshapen.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** Codegen + type checking for animations. diff --git a/.changeset/cylindrella-prediluvial-araneology.md b/.changeset/cylindrella-prediluvial-araneology.md new file mode 100644 index 00000000..8212fb14 --- /dev/null +++ b/.changeset/cylindrella-prediluvial-araneology.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** `CameraFeed` types. diff --git a/.changeset/decretorily-enveil-misexplain.md b/.changeset/decretorily-enveil-misexplain.md new file mode 100644 index 00000000..a5de01ed --- /dev/null +++ b/.changeset/decretorily-enveil-misexplain.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Support async module loader in quickjs.impl. diff --git a/.changeset/exosmotic-pedatisect-kaffir.md b/.changeset/exosmotic-pedatisect-kaffir.md new file mode 100644 index 00000000..70c1bf7c --- /dev/null +++ b/.changeset/exosmotic-pedatisect-kaffir.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature/Types:** Types + codegen for project input actions. diff --git a/.changeset/flanque-catastate-whirrey.md b/.changeset/flanque-catastate-whirrey.md new file mode 100644 index 00000000..6d498a57 --- /dev/null +++ b/.changeset/flanque-catastate-whirrey.md @@ -0,0 +1,23 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** `GDictionary`/`GArray` recursive handling and `toJSON()`/`toString()` + +`GArrayProxy` (now a named type) will now JSON encode like a regular +JS array i.e. `[1,2,3]` instead of `{"0": 1, "1": 2, "2": 3}`. + +We previously ensured that values accessed via a proxy were +themselves proxied. Thus allowing access to nested properties +via chained access e.g. dict_proxy.a.b. However, when setting or +inserting a proxy-wrapped value, we previously inserted the proxy +itself, rather than the wrapped target. This has now been rectified, +it's not safe to do something like: + +```ts +const a = new GDictionary().proxy(); +a.b = new Dictionary().proxy(); +``` + +The above will result in a `GDictionary` containing a `b` +property that is an empty `GDictionary`. diff --git a/.changeset/haussmannization-lassock-synedrous.md b/.changeset/haussmannization-lassock-synedrous.md new file mode 100644 index 00000000..bf542579 --- /dev/null +++ b/.changeset/haussmannization-lassock-synedrous.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Async module loader. diff --git a/.changeset/illusiveness-metopomancy-fibroblast.md b/.changeset/illusiveness-metopomancy-fibroblast.md new file mode 100644 index 00000000..d4539ddb --- /dev/null +++ b/.changeset/illusiveness-metopomancy-fibroblast.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** `godot.lib.api` `instanceof` checks against proxied classes. diff --git a/.changeset/introversible-roupet-retinal.md b/.changeset/introversible-roupet-retinal.md new file mode 100644 index 00000000..bb97be1c --- /dev/null +++ b/.changeset/introversible-roupet-retinal.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Performance:** Added LRU support for `StringNameCache`. diff --git a/.changeset/laverania-euxine-coccygotomy.md b/.changeset/laverania-euxine-coccygotomy.md new file mode 100644 index 00000000..f2c4c1a2 --- /dev/null +++ b/.changeset/laverania-euxine-coccygotomy.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Types**: Refined `SceneTree` `GArray` return types. diff --git a/.changeset/meiobar-poisonable-prussification.md b/.changeset/meiobar-poisonable-prussification.md new file mode 100644 index 00000000..32312d4f --- /dev/null +++ b/.changeset/meiobar-poisonable-prussification.md @@ -0,0 +1,8 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Introduced settings to ensure dependencies are included in exported builds. + +- "Referenced Node Modules" can be enabled to package an _entire_ node module when any file belonging to that module is referenced. +- "Include directories" is another setting that allows you to explicitly add additional directories you want included in your exported builds. diff --git a/.changeset/metascutellar-phaethontic-collop.md b/.changeset/metascutellar-phaethontic-collop.md new file mode 100644 index 00000000..3689f357 --- /dev/null +++ b/.changeset/metascutellar-phaethontic-collop.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Godot 4.4 support. diff --git a/.changeset/muscovitize-theirselves-livish.md b/.changeset/muscovitize-theirselves-livish.md new file mode 100644 index 00000000..facae3d8 --- /dev/null +++ b/.changeset/muscovitize-theirselves-livish.md @@ -0,0 +1,18 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Improved TypeScript class matching regex. + +It was failing in the presence of generics which contain an `extends` +clause e.g. + +```ts +export default class GameNode = {}> extends Node3D +``` + +The regex will now look for the last `extends` on the line in order +to detect the base class. This is only an improvement, it's not +fool-proof and will fail if the base class has a generic that +contains a conditional type expression. Since we only have access +to PCRE2, this is probably the best we can do with just regex. diff --git a/.changeset/nonperformer-sulfhydrate-stupefactiveness.md b/.changeset/nonperformer-sulfhydrate-stupefactiveness.md new file mode 100644 index 00000000..bf1347d6 --- /dev/null +++ b/.changeset/nonperformer-sulfhydrate-stupefactiveness.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix/Types:** Ensure that codegen quotes property keys when necessary. diff --git a/.changeset/nonsettlement-sobralite-devilish.md b/.changeset/nonsettlement-sobralite-devilish.md new file mode 100644 index 00000000..6798ca7e --- /dev/null +++ b/.changeset/nonsettlement-sobralite-devilish.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Support for importing `.json` files. diff --git a/.changeset/norseler-nozzler-swill.md b/.changeset/norseler-nozzler-swill.md new file mode 100644 index 00000000..4da09ff4 --- /dev/null +++ b/.changeset/norseler-nozzler-swill.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Always use thread-safe variant pool allocator. diff --git a/.changeset/precook-pipemouth-coelector.md b/.changeset/precook-pipemouth-coelector.md new file mode 100644 index 00000000..9a155897 --- /dev/null +++ b/.changeset/precook-pipemouth-coelector.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Module resolution now supports modules that utilize `exports` in their `package.json`. diff --git a/.changeset/prefigurement-benziminazole-chokestrap.md b/.changeset/prefigurement-benziminazole-chokestrap.md new file mode 100644 index 00000000..97e53392 --- /dev/null +++ b/.changeset/prefigurement-benziminazole-chokestrap.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Do not instantiate `GodotJSMonitor` if the JS runtime does not support `Performance`. diff --git a/.changeset/pyrrolidine-monocercous-nucleolinus.md b/.changeset/pyrrolidine-monocercous-nucleolinus.md new file mode 100644 index 00000000..80f65ec0 --- /dev/null +++ b/.changeset/pyrrolidine-monocercous-nucleolinus.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Added settings to optionally generate `.d.ts` files for Godot scene files in the Editor. diff --git a/.changeset/quinqueradial-firer-longicone.md b/.changeset/quinqueradial-firer-longicone.md new file mode 100644 index 00000000..aa2b2411 --- /dev/null +++ b/.changeset/quinqueradial-firer-longicone.md @@ -0,0 +1,20 @@ +--- +"@godot-js/editor": patch +--- + +fix: exported properties no longer leak into the base class. +feat: inherited properties now class categorized in the Editor. + +Previously sub-classes were reusing the same [[ClassProperties]] +and [[ClassSignals]] as super classes. Thus sub-classes were +exporting properties against super-classes. This also meant that +classes with a shared parent were receiving each others' +properties. Each class no longer looks up the prototype chain +for these objects. + +Consequently, to ensure properties are exported and appear in the +Editor, we now recurse in a similar fashion to GDScript. +Fortunately, we don't need to worry about the cycle detection +logic that GDScript implements, since TypeScript handles this for +us and cycles won't compile. Added benefit is now that properties +appear in the editor categorized appropriately by class. diff --git a/.changeset/roadmaster-grotto-disrespectful.md b/.changeset/roadmaster-grotto-disrespectful.md new file mode 100644 index 00000000..fbec9047 --- /dev/null +++ b/.changeset/roadmaster-grotto-disrespectful.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** `@export_dictionary` decorator. diff --git a/.changeset/sclerogenous-deliverance-drenchingly.md b/.changeset/sclerogenous-deliverance-drenchingly.md new file mode 100644 index 00000000..0c5273d5 --- /dev/null +++ b/.changeset/sclerogenous-deliverance-drenchingly.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** NIL is now permitted as convertible to any other variant types. diff --git a/.changeset/sheepstealing-fraticellian-miscoinage.md b/.changeset/sheepstealing-fraticellian-miscoinage.md new file mode 100644 index 00000000..aa56239a --- /dev/null +++ b/.changeset/sheepstealing-fraticellian-miscoinage.md @@ -0,0 +1,18 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** Godot/GDScript best effort typing for nullability of Object types. + +Although they're technically nullable everywhere (Ref<> in C++), +many APIs won't ever return to `null`. Unfortunately, properties do +not have flags to tell us this, so we are making best effort guesses +as follows: + +- Getters/setters are nullable. +- Function return types are nullable, unless the function name + starts with `"create"`. +- Function arguments are non-nullable. + +This seems to be a reasonable starting point, and we will continue to manually tweak +types from here. diff --git a/.changeset/shippo-tanh-luceres.md b/.changeset/shippo-tanh-luceres.md new file mode 100644 index 00000000..b282b852 --- /dev/null +++ b/.changeset/shippo-tanh-luceres.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Support for script instantiation from a ResourceLoader executing on another thread. diff --git a/.changeset/somnifuge-formicicide-karyorrhexis.md b/.changeset/somnifuge-formicicide-karyorrhexis.md new file mode 100644 index 00000000..c180b485 --- /dev/null +++ b/.changeset/somnifuge-formicicide-karyorrhexis.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Adhere to typical JS semantics and use default arg when undefined is passed (not just when the parameter is omitted). diff --git a/.changeset/sophia-thalessa-flanch.md b/.changeset/sophia-thalessa-flanch.md new file mode 100644 index 00000000..1f4e8f11 --- /dev/null +++ b/.changeset/sophia-thalessa-flanch.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Added `@export_object` shorthand decorator for exporting objects. diff --git a/.changeset/subpoenal-lively-unconvenience.md b/.changeset/subpoenal-lively-unconvenience.md new file mode 100644 index 00000000..d85bb575 --- /dev/null +++ b/.changeset/subpoenal-lively-unconvenience.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Ensure string representation of Godot objects include the script class name. diff --git a/.changeset/unaproned-periorchitis-pierce.md b/.changeset/unaproned-periorchitis-pierce.md new file mode 100644 index 00000000..3d1570ac --- /dev/null +++ b/.changeset/unaproned-periorchitis-pierce.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** Codegen — treat Signal args as output types, not input types. diff --git a/.changeset/undergrub-perclose-telotype.md b/.changeset/undergrub-perclose-telotype.md new file mode 100644 index 00000000..cb9f1124 --- /dev/null +++ b/.changeset/undergrub-perclose-telotype.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** TypeLoader `post_bind` now supports reentrancy. diff --git a/.changeset/unhandseled-pockily-acyloin.md b/.changeset/unhandseled-pockily-acyloin.md new file mode 100644 index 00000000..2b3decd4 --- /dev/null +++ b/.changeset/unhandseled-pockily-acyloin.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature/Types:** Codegen/types for resources in your project i.e., `ResourceLoader.load("res://whatever")` is now strongly typed depending on the file path. diff --git a/.changeset/unromantically-analogon-monochloro.md b/.changeset/unromantically-analogon-monochloro.md new file mode 100644 index 00000000..f752f198 --- /dev/null +++ b/.changeset/unromantically-analogon-monochloro.md @@ -0,0 +1,6 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** API to access `GDictionary`/`GArray` like JS objects/arrays (`.proxy()`) and +improved type definitions for `GDictionary` and `GArray`. From 5ebf45458629bef884cad3a26d07e18eb8f50f1c Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Tue, 3 Jun 2025 11:07:24 +1000 Subject: [PATCH 02/51] feat: ResolveNodePathMap utility type. This type makes it easier to dynamically define their own NodePathMap types derived from generated SceneNodes. It's also useful for creating Node scripts that don't live at the top level of a scene e.g. export default class Table extends Node3D> The above assumes a 'Table' node exists as a child of the root in scenes/example/table.tscn. --- .changeset/resweep-coambulant-squarely.md | 18 ++++++++++++++++++ scripts/typings/godot.mix.d.ts | 9 +++++++++ 2 files changed, 27 insertions(+) create mode 100644 .changeset/resweep-coambulant-squarely.md diff --git a/.changeset/resweep-coambulant-squarely.md b/.changeset/resweep-coambulant-squarely.md new file mode 100644 index 00000000..373f7c5f --- /dev/null +++ b/.changeset/resweep-coambulant-squarely.md @@ -0,0 +1,18 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** ResolveNodePathMap utility type. + +This type makes it easier to dynamically define your own `NodePathMap` types derived from generated SceneNodes. + +It's also useful for creating Node scripts that don't live at the top level of a scene e.g. + +```ts +export default class Table extends Node3D> { + // ... +} +``` + +The above assumes a `Table` node exists as a child of the root in scenes/example/table.tscn. Now +`this.get_node`/`this.getNode` will auto-complete (and provided types for) children of `Table`. diff --git a/scripts/typings/godot.mix.d.ts b/scripts/typings/godot.mix.d.ts index 40f4da0b..f0a67ee7 100644 --- a/scripts/typings/godot.mix.d.ts +++ b/scripts/typings/godot.mix.d.ts @@ -174,6 +174,15 @@ declare module "godot" { type StaticNodePath = StaticPath; type ResolveNodePath = ResolvePath; + type ResolveNodePathMap = Path extends keyof Map + ? Map[Path] extends Node + ? ChildMap + : Default + : Path extends `${infer Key extends keyof Map & string}/${infer SubPath}` + ? Map[Key] extends Node + ? ResolveNodePathMap + : Default + : Default; type AnimationMixerPathMap = PathMap; type StaticAnimationMixerPath = From a359d04b316368021912dd5e96cd478a47c9b680 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Tue, 3 Jun 2025 10:43:14 +1000 Subject: [PATCH 03/51] feat: Types Node direct child APIs --- .changeset/polyploidic-thanklessness-damson.md | 11 +++++++++++ scripts/jsb.editor/src/jsb.editor.codegen.ts | 5 +++++ scripts/typings/godot.mix.d.ts | 7 +++++++ 3 files changed, 23 insertions(+) create mode 100644 .changeset/polyploidic-thanklessness-damson.md diff --git a/.changeset/polyploidic-thanklessness-damson.md b/.changeset/polyploidic-thanklessness-damson.md new file mode 100644 index 00000000..b9159e5c --- /dev/null +++ b/.changeset/polyploidic-thanklessness-damson.md @@ -0,0 +1,11 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** Added types for `Node` direct child APIs: + +- add_child +- get_child +- get_children +- move_child +- remove_child diff --git a/scripts/jsb.editor/src/jsb.editor.codegen.ts b/scripts/jsb.editor/src/jsb.editor.codegen.ts index f6affa4f..e95eca7d 100644 --- a/scripts/jsb.editor/src/jsb.editor.codegen.ts +++ b/scripts/jsb.editor/src/jsb.editor.codegen.ts @@ -414,11 +414,16 @@ const TypeMutations: Record = { } ], property_overrides: { + add_child: mutate_parameter_type('node', 'NodePathMapChild'), + get_child: mutate_return_type('NodePathMapChild'), + get_children: mutate_return_type('GArray>'), get_node: ["get_node, Default = never>(path: Path): ResolveNodePath"], get_node_or_null: [ "get_node_or_null, Default = never>(path: Path): null | ResolveNodePath", "get_node_or_null(path: NodePath | string): null | Node", ], + move_child: mutate_parameter_type(names.get_parameter('child_node'), 'NodePathMapChild'), + remove_child: mutate_parameter_type('node', 'NodePathMapChild'), validate_property: mutate_parameter_type("property", "GDictionary"), }, }, diff --git a/scripts/typings/godot.mix.d.ts b/scripts/typings/godot.mix.d.ts index f0a67ee7..1e034e25 100644 --- a/scripts/typings/godot.mix.d.ts +++ b/scripts/typings/godot.mix.d.ts @@ -170,6 +170,12 @@ declare module "godot" { : never >; + type PathMapChild = IfAny< + Map, + Permitted, + Map[keyof Map] extends undefined | Permitted ? Exclude : Default + >; + type NodePathMap = PathMap; type StaticNodePath = StaticPath; type ResolveNodePath = @@ -183,6 +189,7 @@ declare module "godot" { ? ResolveNodePathMap : Default : Default; + type NodePathMapChild = PathMapChild; type AnimationMixerPathMap = PathMap; type StaticAnimationMixerPath = From 0f173d3d5fe2342bdba40d40ec3e731fad10bbb2 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Tue, 3 Jun 2025 10:44:08 +1000 Subject: [PATCH 04/51] fix: Ensure resource types are (re)generated when a scene saves --- .changeset/semideification-schoolish-oolitic.md | 5 +++++ weaver-editor/jsb_editor_plugin.cpp | 15 +++++++++++---- weaver-editor/jsb_editor_plugin.h | 4 ++-- 3 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 .changeset/semideification-schoolish-oolitic.md diff --git a/.changeset/semideification-schoolish-oolitic.md b/.changeset/semideification-schoolish-oolitic.md new file mode 100644 index 00000000..cc139183 --- /dev/null +++ b/.changeset/semideification-schoolish-oolitic.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Ensure resource types are (re)generated when a scene saves diff --git a/weaver-editor/jsb_editor_plugin.cpp b/weaver-editor/jsb_editor_plugin.cpp index 89ec8bad..82423aaa 100644 --- a/weaver-editor/jsb_editor_plugin.cpp +++ b/weaver-editor/jsb_editor_plugin.cpp @@ -48,8 +48,8 @@ void GodotJSEditorPlugin::_notification(int p_what) singleton_ = this; // stupid self watching, but there is no other way which work both in module and gdextension // EditorPlugin::notify_scene_saved() is not virtual, and not exposed to gdextension :( - connect("scene_saved", callable_mp(this, &GodotJSEditorPlugin::_generate_edited_scene_dts)); - connect("resource_saved", callable_mp(this, &GodotJSEditorPlugin::_generate_edited_resource_dts)); + connect("scene_saved", callable_mp(this, &GodotJSEditorPlugin::_on_scene_saved)); + connect("resource_saved", callable_mp(this, &GodotJSEditorPlugin::_on_resource_saved)); EditorFileSystem::get_singleton()->connect("resources_reimported", callable_mp(this, &GodotJSEditorPlugin::_generate_imported_resource_dts)); break; default: break; @@ -459,15 +459,22 @@ void GodotJSEditorPlugin::cleanup_invalid_files() JSB_LOG(Log, "%d files are deleted", deleted_num); } -void GodotJSEditorPlugin::_generate_edited_scene_dts(const String& p_path) +void GodotJSEditorPlugin::_on_scene_saved(const String& p_path) { if (!jsb::internal::Settings::get_autogen_scene_dts_on_save()) return; Vector paths = { p_path }; generate_scene_nodes_dts(paths); + + // Curiously, the "resource_saved" signal is not emitted for scenes even though they're resources. So we implement + // resource saved logic here too. + + if (!jsb::internal::Settings::get_autogen_resource_dts_on_save()) return; + + generate_resource_dts(paths); } -void GodotJSEditorPlugin::_generate_edited_resource_dts(const Ref& p_resource) +void GodotJSEditorPlugin::_on_resource_saved(const Ref& p_resource) { if (!jsb::internal::Settings::get_autogen_resource_dts_on_save()) return; diff --git a/weaver-editor/jsb_editor_plugin.h b/weaver-editor/jsb_editor_plugin.h index e8698ff8..f40f3e2a 100644 --- a/weaver-editor/jsb_editor_plugin.h +++ b/weaver-editor/jsb_editor_plugin.h @@ -53,8 +53,8 @@ class GodotJSEditorPlugin : public EditorPlugin std::shared_ptr tsc_; - void _generate_edited_scene_dts(const String& p_path); - void _generate_edited_resource_dts(const Ref& p_resource); + void _on_scene_saved(const String& p_path); + void _on_resource_saved(const Ref& p_resource); void _generate_imported_resource_dts(const Vector& p_resources); protected: From 0c5751735c3151b07bbb187c54c442b0e716e36c Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Tue, 3 Jun 2025 14:57:49 +1000 Subject: [PATCH 05/51] feat: UserTypeDescriptor resource type safety --- .changeset/scumless-boxbush-phytotoxin.md | 5 +++++ scripts/jsb.editor/src/jsb.editor.codegen.ts | 5 ++++- scripts/typings/godot.generated.d.ts | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/scumless-boxbush-phytotoxin.md diff --git a/.changeset/scumless-boxbush-phytotoxin.md b/.changeset/scumless-boxbush-phytotoxin.md new file mode 100644 index 00000000..63a49c21 --- /dev/null +++ b/.changeset/scumless-boxbush-phytotoxin.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** `UserTypeDescriptor` `resource` property is now type-safe and will auto-complete, accepting `Script` resources. diff --git a/scripts/jsb.editor/src/jsb.editor.codegen.ts b/scripts/jsb.editor/src/jsb.editor.codegen.ts index e95eca7d..ae968a03 100644 --- a/scripts/jsb.editor/src/jsb.editor.codegen.ts +++ b/scripts/jsb.editor/src/jsb.editor.codegen.ts @@ -1,4 +1,5 @@ import type { + ExtractValueKeys, FileAccess, GArray, GDictionary, @@ -6,6 +7,8 @@ import type { Node, PropertyInfo, Resource, + ResourceTypes, + Script, Variant, } from "godot"; @@ -1136,7 +1139,7 @@ export type UserTypeDescriptor = GDictionary<{ /** * res:// style path to the TypeScript module where this type is exported. */ - resource: string; + resource: ExtractValueKeys; /** * Preferred type name to use when importing. */ diff --git a/scripts/typings/godot.generated.d.ts b/scripts/typings/godot.generated.d.ts index 407090b3..bd1044cc 100644 --- a/scripts/typings/godot.generated.d.ts +++ b/scripts/typings/godot.generated.d.ts @@ -8,6 +8,9 @@ declare module "godot" { class Node = any> extends Object { } class Resource { } + class Script extends Resource { } + interface ResourceTypes { } + class GArray { static create(elements: [T] extends [GArray] ? Array> : Array>): [T] extends [GArray] From 18a6ff528d8f5dba78dda30035eef955199eedad Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Wed, 4 Jun 2025 00:38:59 +1000 Subject: [PATCH 06/51] feat: Ensure codegen files are type checked. We want users to be able to have skipLibCheck: true in their tsconfig. However, we do need to ensure that our codegen files are not skipped in such cases. So we no longer use the .d.ts extension. "Generate Godot d.ts" is now "Generate types" and in addition to generating all Godot and project types (which it already did), the autogen directory will now wiped of all files/directories before commencing generation. This ensures old generated files no longer pollute the project. --- .../epimachinae-anemonal-unbearableness.md | 11 +++ bridge/jsb_editor_utility_funcs.cpp | 2 +- bridge/jsb_environment.cpp | 2 +- internal/jsb_naming_util.cpp | 66 +++++++++++++++++- internal/jsb_naming_util.h | 4 +- scripts/jsb.editor/src/jsb.editor.codegen.ts | 34 +--------- weaver-editor/jsb_editor_helper.cpp | 17 ++++- weaver-editor/jsb_editor_helper.h | 1 + weaver-editor/jsb_editor_plugin.cpp | 67 +++++++++++-------- weaver-editor/jsb_editor_plugin.h | 9 ++- 10 files changed, 139 insertions(+), 74 deletions(-) create mode 100644 .changeset/epimachinae-anemonal-unbearableness.md diff --git a/.changeset/epimachinae-anemonal-unbearableness.md b/.changeset/epimachinae-anemonal-unbearableness.md new file mode 100644 index 00000000..c14a6e81 --- /dev/null +++ b/.changeset/epimachinae-anemonal-unbearableness.md @@ -0,0 +1,11 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** "Generate Godot d.ts" in the UI is now "Generate types" and in addition to +generating all Godot and project types (which it already did), the +autogen directory will now be wiped of all files/directories before +commencing generation. This ensures old generated files no longer +pollute the project. + + diff --git a/bridge/jsb_editor_utility_funcs.cpp b/bridge/jsb_editor_utility_funcs.cpp index 4a7bfb81..02849224 100644 --- a/bridge/jsb_editor_utility_funcs.cpp +++ b/bridge/jsb_editor_utility_funcs.cpp @@ -786,7 +786,7 @@ namespace jsb v8::HandleScope handle_scope(isolate); v8::Local context = isolate->GetCurrentContext(); - List exposed_class_list = internal::NamingUtil::get_exposed_class_list(); + List exposed_class_list = internal::NamingUtil::get_exposed_original_class_list(); v8::Local array = v8::Array::New(isolate, exposed_class_list.size()); int index = 0; diff --git a/bridge/jsb_environment.cpp b/bridge/jsb_environment.cpp index d00668ac..4bdb7f20 100644 --- a/bridge/jsb_environment.cpp +++ b/bridge/jsb_environment.cpp @@ -319,7 +319,7 @@ namespace jsb // Populate StringNames replacement list so that classes can be lazily loaded by their exposed class name. if (internal::Settings::get_camel_case_bindings_enabled()) { - List exposed_class_list = internal::NamingUtil::get_exposed_class_list(); + List exposed_class_list = internal::NamingUtil::get_exposed_original_class_list(); for (auto it = exposed_class_list.begin(); it != exposed_class_list.end(); ++it) { diff --git a/internal/jsb_naming_util.cpp b/internal/jsb_naming_util.cpp index e5c1d1d9..32b98f1c 100644 --- a/internal/jsb_naming_util.cpp +++ b/internal/jsb_naming_util.cpp @@ -67,6 +67,27 @@ namespace jsb::internal { "XRAPI", "XRApi" }, }); + const HashSet omitted_original_classes_set = { + "IPUnix", + "ScriptEditorDebugger", + "Thread", + "Semaphore", + + // GodotJS related clases + "GodotJSEditorPlugin", + "GodotJSExportPlugin", + "GodotJSREPL", + "GodotJSScript", + "GodotJSEditorHelper", + "GodotJSEditorProgress", + + // GDScript related classes + "GDScript", + "GDScriptEditorTranslationParserPlugin", + "GDScriptNativeClass", + "GDScriptSyntaxHighlighter" + }; + String _get_pascal_case_part_override(String p_part, bool p_input_is_upper = true) { if (!p_input_is_upper) @@ -337,10 +358,10 @@ namespace jsb::internal return ret; } - List NamingUtil::get_exposed_class_list() + List NamingUtil::get_exposed_original_class_list() { #ifdef TOOLS_ENABLED - HashSet ignored_classes_set; + HashSet ignored_classes_set; if (internal::Settings::editor_settings_available()) { @@ -364,6 +385,12 @@ namespace jsb::internal { StringName class_name = *it; + if (omitted_original_classes_set.has(class_name)) + { + JSB_LOG(Verbose, "Omitted class '%s' as it's currently not usable from JavaScript", class_name); + continue; + } + #ifdef TOOLS_ENABLED if (ignored_classes_set.has(get_class_name(class_name))) { @@ -376,6 +403,7 @@ namespace jsb::internal if (api_type == ClassDB::API_NONE) { + JSB_LOG(Verbose, "Ignoring class '%s' because it's marked as API_NONE", class_name); continue; } @@ -396,4 +424,38 @@ namespace jsb::internal return exposed_class_names; } + + bool NamingUtil::is_original_class_exposed(const String& p_original_name) + { + if (omitted_original_classes_set.has(p_original_name)) + { + return false; + } + + ClassDB::APIType api_type = ClassDB::get_api_type(p_original_name); + + if (api_type == ClassDB::API_NONE) + { + return false; + } + + if (!ClassDB::is_class_exposed(p_original_name)) + { + return false; + } + + if (!ClassDB::is_class_enabled(p_original_name)) + { + return false; + } + +#ifdef TOOLS_ENABLED + if (internal::Settings::get_ignored_classes().find(p_original_name) >= 0) + { + return false; + } +#endif + + return true; + } } diff --git a/internal/jsb_naming_util.h b/internal/jsb_naming_util.h index b8391d06..54b4de7c 100644 --- a/internal/jsb_naming_util.h +++ b/internal/jsb_naming_util.h @@ -15,7 +15,9 @@ namespace jsb::internal static String snake_to_camel_case(const String &p_identifier, bool p_input_is_upper = false); - static List get_exposed_class_list(); + static List get_exposed_original_class_list(); + + static bool is_original_class_exposed(const String& p_original_name); static String get_class_name(const String& p_original_name) { diff --git a/scripts/jsb.editor/src/jsb.editor.codegen.ts b/scripts/jsb.editor/src/jsb.editor.codegen.ts index ae968a03..ef7e2f5e 100644 --- a/scripts/jsb.editor/src/jsb.editor.codegen.ts +++ b/scripts/jsb.editor/src/jsb.editor.codegen.ts @@ -728,33 +728,6 @@ const PrimitiveTypeNames: { [type: number]: string } = { const RemapTypes: { [name: string]: string } = { ["bool"]: "boolean", } -const IgnoredTypes = new Set([ - "IPUnix", - "ScriptEditorDebugger", - "Thread", - "Semaphore", - - // - // "GodotNavigationServer2D", - // "GodotPhysicsServer2D", - // "GodotPhysicsServer3D", - // "PhysicsServer2DExtension", - // "PhysicsServer3DExtension", - - // GodotJS related clases - "GodotJSEditorPlugin", - "GodotJSExportPlugin", - "GodotJSREPL", - "GodotJSScript", - "GodotJSEditorHelper", - "GodotJSEditorProgress", - - // GDScript related classes - "GDScript", - "GDScriptEditorTranslationParserPlugin", - "GDScriptNativeClass", - "GDScriptSyntaxHighlighter", -]) const GlobalUtilityFuncs = [ { description: "shorthand for getting project settings", @@ -2530,9 +2503,6 @@ export class TSDCodeGen { // godot classes for (let class_name in this._types.classes) { const cls = this._types.classes[class_name]; - if (IgnoredTypes.has(class_name)) { - continue; - } if (typeof this._types.singletons[class_name] !== "undefined") { // ignore the class if it's already defined as Singleton continue; @@ -2772,7 +2742,7 @@ export class SceneTSDCodeGen { private make_scene_path(scene_path: string, include_filename = true) { const relative_path = ( include_filename - ? scene_path.replace(/\.t?scn$/i, ".nodes.gen.d.ts") + ? scene_path.replace(/\.t?scn$/i, ".nodes.gen.ts") : scene_path.replace(/\/[^\/]+$/, "") ).replace(/^res:\/\/?/, ""); @@ -2877,7 +2847,7 @@ export class ResourceTSDCodeGen { private make_resource_path(resource_path: string, include_filename = true) { const relative_path = ( include_filename - ? resource_path + ".gen.d.ts" + ? resource_path + ".gen.ts" : resource_path.replace(/\/[^\/]+$/, "") ).replace(/^res:\/\/?/, ""); diff --git a/weaver-editor/jsb_editor_helper.cpp b/weaver-editor/jsb_editor_helper.cpp index 82062575..e0741ad3 100644 --- a/weaver-editor/jsb_editor_helper.cpp +++ b/weaver-editor/jsb_editor_helper.cpp @@ -87,6 +87,18 @@ bool GodotJSEditorHelper::_request_codegen(jsb::JSEnvironment& p_env, GodotJSScr return true; } +StringName GodotJSEditorHelper::_get_exposed_node_class_name(const StringName& class_name) +{ + StringName exposed_class_name = class_name; + + while (!jsb::internal::NamingUtil::is_original_class_exposed(exposed_class_name)) + { + exposed_class_name = ClassDB::get_parent_class(exposed_class_name); + } + + return jsb::internal::NamingUtil::get_class_name(exposed_class_name); +} + Dictionary GodotJSEditorHelper::_build_node_type_descriptor(jsb::JSEnvironment& p_env, Node* p_node, const String& scene_resource_path) { Dictionary descriptor; @@ -177,7 +189,7 @@ Dictionary GodotJSEditorHelper::_build_node_type_descriptor(jsb::JSEnvironment& } descriptor[jsb_string_name(type)] = (int32_t) DescriptorType::Godot; - descriptor[jsb_string_name(name)] = jsb::internal::NamingUtil::get_class_name(p_node->get_class_name()); + descriptor[jsb_string_name(name)] = GodotJSEditorHelper::_get_exposed_node_class_name(p_node->get_class_name()); descriptor[jsb_string_name(arguments)] = generic_arguments; } else @@ -332,9 +344,8 @@ Dictionary GodotJSEditorHelper::get_resource_type_descriptor(const String& p_pat if (script == nullptr || GodotJSScriptLanguage::get_singleton()->is_global_class_generic(script->get_path())) { - const StringName& resource_class = resource->get_class_name(); descriptor[jsb_string_name(type)] = (int32_t) DescriptorType::Godot; - descriptor[jsb_string_name(name)] = resource_class == jsb_string_name(GodotJSScript) ? "Script" : jsb::internal::NamingUtil::get_class_name(resource_class); + descriptor[jsb_string_name(name)] = GodotJSEditorHelper::_get_exposed_node_class_name(resource->get_class_name()); } else { diff --git a/weaver-editor/jsb_editor_helper.h b/weaver-editor/jsb_editor_helper.h index 7c92869d..56214485 100644 --- a/weaver-editor/jsb_editor_helper.h +++ b/weaver-editor/jsb_editor_helper.h @@ -9,6 +9,7 @@ class GodotJSEditorHelper : public Object private: static bool _request_codegen(jsb::JSEnvironment& p_env, GodotJSScript* p_script, const Dictionary& p_request, Dictionary& p_result); + static StringName _get_exposed_node_class_name(const StringName& class_name); static Dictionary _build_node_type_descriptor(jsb::JSEnvironment& p_env, Node* p_node, const String& scene_resource_path = String()); static void _log_load_error(const String &p_file, const String &p_type, Error p_error); diff --git a/weaver-editor/jsb_editor_plugin.cpp b/weaver-editor/jsb_editor_plugin.cpp index 82423aaa..3891b4f6 100644 --- a/weaver-editor/jsb_editor_plugin.cpp +++ b/weaver-editor/jsb_editor_plugin.cpp @@ -7,7 +7,7 @@ enum { MENU_ID_INSTALL_TS_PROJECT, - MENU_ID_GENERATE_GODOT_DTS, + MENU_ID_GENERATE_TYPES, MENU_ID_CLEANUP_INVALID_FILES, }; @@ -61,7 +61,7 @@ void GodotJSEditorPlugin::_on_menu_pressed(int p_what) switch (p_what) { case MENU_ID_INSTALL_TS_PROJECT: try_install_ts_project(); break; - case MENU_ID_GENERATE_GODOT_DTS: generate_godot_dts(); break; + case MENU_ID_GENERATE_TYPES: generate_godot_dts(); break; case MENU_ID_CLEANUP_INVALID_FILES: cleanup_invalid_files(); break; default: break; } @@ -75,10 +75,10 @@ GodotJSEditorPlugin::GodotJSEditorPlugin() // jsb::internal::Settings::on_editor_init(); PopupMenu *menu = memnew(PopupMenu); add_tool_submenu_item(TTR("GodotJS"), menu); - menu->add_item(TTR("Install Preset files"), MENU_ID_INSTALL_TS_PROJECT); - menu->add_item(TTR("Generate Godot d.ts"), MENU_ID_GENERATE_GODOT_DTS); + menu->add_item(TTR("Install Preset Files"), MENU_ID_INSTALL_TS_PROJECT); + menu->add_item(TTR("Generate Types"), MENU_ID_GENERATE_TYPES); menu->add_separator(); - menu->add_item(TTR("Cleanup invalid files"), MENU_ID_CLEANUP_INVALID_FILES); + menu->add_item(TTR("Cleanup Invalid Files"), MENU_ID_CLEANUP_INVALID_FILES); menu->connect("id_pressed", callable_mp(this, &GodotJSEditorPlugin::_on_menu_pressed)); confirm_dialog_ = memnew(InstallGodotJSPresetConfirmationDialog); @@ -464,14 +464,14 @@ void GodotJSEditorPlugin::_on_scene_saved(const String& p_path) if (!jsb::internal::Settings::get_autogen_scene_dts_on_save()) return; Vector paths = { p_path }; - generate_scene_nodes_dts(paths); + generate_scene_nodes_types(paths); // Curiously, the "resource_saved" signal is not emitted for scenes even though they're resources. So we implement // resource saved logic here too. if (!jsb::internal::Settings::get_autogen_resource_dts_on_save()) return; - generate_resource_dts(paths); + generate_resource_types(paths); } void GodotJSEditorPlugin::_on_resource_saved(const Ref& p_resource) @@ -479,14 +479,14 @@ void GodotJSEditorPlugin::_on_resource_saved(const Ref& p_resource) if (!jsb::internal::Settings::get_autogen_resource_dts_on_save()) return; Vector paths = { p_resource->get_path() }; - generate_resource_dts(paths); + generate_resource_types(paths); } void GodotJSEditorPlugin::_generate_imported_resource_dts(const Vector& p_resource) { if (!jsb::internal::Settings::get_autogen_resource_dts_on_save()) return; - generate_resource_dts(p_resource); + generate_resource_types(p_resource); } void GodotJSEditorPlugin::generate_godot_dts() @@ -494,22 +494,31 @@ void GodotJSEditorPlugin::generate_godot_dts() if (GodotJSEditorPlugin* editor_plugin = GodotJSEditorPlugin::get_singleton()) { install_files(filter_files(editor_plugin->install_files_, jsb::weaver::CH_D_TS)); - } - GodotJSScriptLanguage* lang = GodotJSScriptLanguage::get_singleton(); - jsb_check(lang); - Error err; - const bool use_project_settings = jsb::internal::Settings::get_codegen_use_project_settings(); - const String code = jsb_format( - R"--((function(){const mod = require("jsb.editor.codegen"); (new mod.TSDCodeGen("%s", %s)).emit();})())--", - "./" JSB_TYPE_ROOT, - use_project_settings ? "true" : "false" - ); - lang->eval_source(code, err).ignore(); - ERR_FAIL_COND_MSG(err != OK, "failed to evaluate jsb.editor.codegen"); + GodotJSScriptLanguage* lang = GodotJSScriptLanguage::get_singleton(); + jsb_check(lang); + Error err; + const bool use_project_settings = jsb::internal::Settings::get_codegen_use_project_settings(); + const String code = jsb_format( + R"--((function(){const mod = require("jsb.editor.codegen"); (new mod.TSDCodeGen("%s", %s)).emit();})())--", + "./" JSB_TYPE_ROOT, + use_project_settings ? "true" : "false" + ); + lang->eval_source(code, err).ignore(); + ERR_FAIL_COND_MSG(err != OK, "failed to evaluate jsb.editor.codegen"); + + String autogen_url = "res://" + jsb::internal::Settings::get_autogen_path(); + + // In case the user does something strange with their get_autogen_path, don't delete their project. + if (autogen_url.length() > 6 && FileAccess::exists(autogen_url.path_join(".gdignore"))) + { + DirAccess::open(autogen_url)->erase_contents_recursive(); + install_files(filter_files(editor_plugin->install_files_, jsb::weaver::CH_GDIGNORE)); + } - generate_all_scene_nodes_dts(); - generate_all_resource_dts(); + generate_all_scene_nodes_types(); + generate_all_resource_types(); + } } void GodotJSEditorPlugin::get_all_scenes(EditorFileSystemDirectory* p_dir, Vector& r_list) @@ -546,7 +555,7 @@ void GodotJSEditorPlugin::get_all_resources(EditorFileSystemDirectory* p_dir, Ve } } -void GodotJSEditorPlugin::generate_scene_nodes_dts(const Vector& p_paths) +void GodotJSEditorPlugin::generate_scene_nodes_types(const Vector& p_paths) { if (!jsb::internal::Settings::get_gen_scene_dts()) return; @@ -569,7 +578,7 @@ void GodotJSEditorPlugin::generate_scene_nodes_dts(const Vector& p_paths ERR_FAIL_COND_MSG(err != OK, "failed to evaluate jsb.editor.codegen"); } -void GodotJSEditorPlugin::generate_resource_dts(const Vector& p_paths) +void GodotJSEditorPlugin::generate_resource_types(const Vector& p_paths) { if (!jsb::internal::Settings::get_gen_resource_dts()) return; @@ -592,18 +601,18 @@ void GodotJSEditorPlugin::generate_resource_dts(const Vector& p_paths) ERR_FAIL_COND_MSG(err != OK, "failed to evaluate jsb.editor.codegen"); } -void GodotJSEditorPlugin::generate_all_scene_nodes_dts() +void GodotJSEditorPlugin::generate_all_scene_nodes_types() { Vector paths; get_all_scenes(EditorFileSystem::get_singleton()->get_filesystem(), paths); - generate_scene_nodes_dts(paths); + generate_scene_nodes_types(paths); } -void GodotJSEditorPlugin::generate_all_resource_dts() +void GodotJSEditorPlugin::generate_all_resource_types() { Vector paths; get_all_resources(EditorFileSystem::get_singleton()->get_filesystem(), paths); - generate_resource_dts(paths); + generate_resource_types(paths); } void GodotJSEditorPlugin::load_editor_entry_module() diff --git a/weaver-editor/jsb_editor_plugin.h b/weaver-editor/jsb_editor_plugin.h index f40f3e2a..836d44c5 100644 --- a/weaver-editor/jsb_editor_plugin.h +++ b/weaver-editor/jsb_editor_plugin.h @@ -75,8 +75,8 @@ class GodotJSEditorPlugin : public EditorPlugin static bool delete_file(const String& p_file); static void get_all_scenes(EditorFileSystemDirectory* p_dir, Vector& r_list); static void get_all_resources(EditorFileSystemDirectory* p_dir, Vector& r_list); - static void generate_scene_nodes_dts(const Vector& p_paths); - static void generate_resource_dts(const Vector& p_paths); + static void generate_scene_nodes_types(const Vector& p_paths); + static void generate_resource_types(const Vector& p_paths); public: GodotJSEditorPlugin(); @@ -91,14 +91,13 @@ class GodotJSEditorPlugin : public EditorPlugin bool verify_ts_project() const; void _ignore_node_modules(); void cleanup_invalid_files(); - void generate_edited_scene_dts(); // not really a singleton, but always get from `EditorNode` which assumed unique static GodotJSEditorPlugin* get_singleton(); static void generate_godot_dts(); - static void generate_all_scene_nodes_dts(); - static void generate_all_resource_dts(); + static void generate_all_scene_nodes_types(); + static void generate_all_resource_types(); static void ignore_node_modules(); static void collect_invalid_files(Vector& r_invalid_files); static void collect_invalid_files(const String& p_path, Vector& r_invalid_files); From 957d5ca52c76f0c8b6dc4b2b1a5e6813d481385b Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Wed, 4 Jun 2025 18:11:46 +1000 Subject: [PATCH 07/51] feat: Improved support for statically typed optional nodes --- .changeset/tanquam-colinephritis-kanten.md | 5 +++++ scripts/jsb.editor/src/jsb.editor.codegen.ts | 2 +- scripts/typings/godot.mix.d.ts | 12 +++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 .changeset/tanquam-colinephritis-kanten.md diff --git a/.changeset/tanquam-colinephritis-kanten.md b/.changeset/tanquam-colinephritis-kanten.md new file mode 100644 index 00000000..395433f0 --- /dev/null +++ b/.changeset/tanquam-colinephritis-kanten.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** Improved support for statically typed optional nodes. diff --git a/scripts/jsb.editor/src/jsb.editor.codegen.ts b/scripts/jsb.editor/src/jsb.editor.codegen.ts index ef7e2f5e..598bfd56 100644 --- a/scripts/jsb.editor/src/jsb.editor.codegen.ts +++ b/scripts/jsb.editor/src/jsb.editor.codegen.ts @@ -422,7 +422,7 @@ const TypeMutations: Record = { get_children: mutate_return_type('GArray>'), get_node: ["get_node, Default = never>(path: Path): ResolveNodePath"], get_node_or_null: [ - "get_node_or_null, Default = never>(path: Path): null | ResolveNodePath", + "get_node_or_null, Default = null>(path: Path): null | ResolveNodePath", "get_node_or_null(path: NodePath | string): null | Node", ], move_child: mutate_parameter_type(names.get_parameter('child_node'), 'NodePathMapChild'), diff --git a/scripts/typings/godot.mix.d.ts b/scripts/typings/godot.mix.d.ts index 1e034e25..630602cd 100644 --- a/scripts/typings/godot.mix.d.ts +++ b/scripts/typings/godot.mix.d.ts @@ -156,8 +156,10 @@ declare module "godot" { DummyKey extends any ? ( Path extends keyof Map - ? Map[Path] extends Permitted - ? Map[Path] + ? [Map[Path]] extends [Permitted] + ? [undefined] extends [Map[Path]] + ? null | Exclude + : Map[Path] : Default : Path extends `${infer Key extends Exclude & string}/${infer SubPath}` ? Map[Key] extends PathMappable @@ -177,9 +179,9 @@ declare module "godot" { >; type NodePathMap = PathMap; - type StaticNodePath = StaticPath; - type ResolveNodePath = - ResolvePath; + type StaticNodePath = StaticPath; + type ResolveNodePath = + ResolvePath; type ResolveNodePathMap = Path extends keyof Map ? Map[Path] extends Node ? ChildMap From f13ba87c2aaba17d984b88b3344acfbcddf85ba6 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Wed, 4 Jun 2025 18:12:18 +1000 Subject: [PATCH 08/51] chore: Fix indentation --- scripts/typings/godot.mix.d.ts | 40 +++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/scripts/typings/godot.mix.d.ts b/scripts/typings/godot.mix.d.ts index 630602cd..94d307d6 100644 --- a/scripts/typings/godot.mix.d.ts +++ b/scripts/typings/godot.mix.d.ts @@ -173,9 +173,9 @@ declare module "godot" { >; type PathMapChild = IfAny< - Map, - Permitted, - Map[keyof Map] extends undefined | Permitted ? Exclude : Default + Map, + Permitted, + Map[keyof Map] extends undefined | Permitted ? Exclude : Default >; type NodePathMap = PathMap; @@ -183,14 +183,14 @@ declare module "godot" { type ResolveNodePath = ResolvePath; type ResolveNodePathMap = Path extends keyof Map - ? Map[Path] extends Node - ? ChildMap - : Default - : Path extends `${infer Key extends keyof Map & string}/${infer SubPath}` - ? Map[Key] extends Node - ? ResolveNodePathMap - : Default - : Default; + ? Map[Path] extends Node + ? ChildMap + : Default + : Path extends `${infer Key extends keyof Map & string}/${infer SubPath}` + ? Map[Key] extends Node + ? ResolveNodePathMap + : Default + : Default; type NodePathMapChild = PathMapChild; type AnimationMixerPathMap = PathMap; @@ -224,7 +224,7 @@ declare module "godot" { /** * Appends new elements to the end of an array, and returns the new length of the array. * @param item New element to add to the array. - * @param additionalItems Additional new elements to add to the array. + * @param additionalItems Additional new elements to add to the array. */ push(item: T | GProxyValueWrap, ...additionalItems: Array>): number; /** @@ -258,17 +258,17 @@ declare module "godot" { : V; type GProxyValueUnwrap = V extends GArray - ? E - : V extends GDictionary - ? T - : V; + ? E + : V extends GDictionary + ? T + : V; type GWrappableValue = GAny | GWrappableValue[] | { [key: string]: GWrappableValue }; type GValueWrapUnchecked = V extends Array - ? GArray> - : V extends GAny - ? V - : GDictionary<{ [K in keyof V]: GValueWrapUnchecked }>; + ? GArray> + : V extends GAny + ? V + : GDictionary<{ [K in keyof V]: GValueWrapUnchecked }>; type GValueWrap = [V] extends [GWrappableValue] ? GValueWrapUnchecked : never; /** From 7626e736162764ee7c8dcd476cef6a6c83fd1b51 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Fri, 6 Jun 2025 21:27:00 +1000 Subject: [PATCH 09/51] fix: get_tree() will not return null, it'll error instead --- .changeset/undenominated-saberlike-atmidometry.md | 8 ++++++++ scripts/jsb.editor/src/jsb.editor.codegen.ts | 1 + 2 files changed, 9 insertions(+) create mode 100644 .changeset/undenominated-saberlike-atmidometry.md diff --git a/.changeset/undenominated-saberlike-atmidometry.md b/.changeset/undenominated-saberlike-atmidometry.md new file mode 100644 index 00000000..82ad0bdd --- /dev/null +++ b/.changeset/undenominated-saberlike-atmidometry.md @@ -0,0 +1,8 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** `get_tree()`/`getTree()` no longer types as returning `null`. + +Although it _will_ return `null` when not inside a tree. This is a runtime error, with an error message +being logged i.e., you should not ever call this function expecting a `null` result. diff --git a/scripts/jsb.editor/src/jsb.editor.codegen.ts b/scripts/jsb.editor/src/jsb.editor.codegen.ts index 598bfd56..20c15a29 100644 --- a/scripts/jsb.editor/src/jsb.editor.codegen.ts +++ b/scripts/jsb.editor/src/jsb.editor.codegen.ts @@ -425,6 +425,7 @@ const TypeMutations: Record = { "get_node_or_null, Default = null>(path: Path): null | ResolveNodePath", "get_node_or_null(path: NodePath | string): null | Node", ], + get_tree: mutate_return_type('SceneTree'), move_child: mutate_parameter_type(names.get_parameter('child_node'), 'NodePathMapChild'), remove_child: mutate_parameter_type('node', 'NodePathMapChild'), validate_property: mutate_parameter_type("property", "GDictionary"), From fd177b96836660b7bdd5c56ecb7c50edeb65e438 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Fri, 6 Jun 2025 21:27:57 +1000 Subject: [PATCH 10/51] fix: Duplicate Generating in codegen titles --- .changeset/ammonifier-lungi-godmaker.md | 5 +++++ scripts/jsb.editor/src/jsb.editor.codegen.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/ammonifier-lungi-godmaker.md diff --git a/.changeset/ammonifier-lungi-godmaker.md b/.changeset/ammonifier-lungi-godmaker.md new file mode 100644 index 00000000..cf4ede27 --- /dev/null +++ b/.changeset/ammonifier-lungi-godmaker.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Duplicate "Generating" label appeared in the UI during codegen. diff --git a/scripts/jsb.editor/src/jsb.editor.codegen.ts b/scripts/jsb.editor/src/jsb.editor.codegen.ts index 20c15a29..dd93326d 100644 --- a/scripts/jsb.editor/src/jsb.editor.codegen.ts +++ b/scripts/jsb.editor/src/jsb.editor.codegen.ts @@ -651,7 +651,7 @@ class CodegenTasks { const EditorProgress = godot.GodotJSEditorProgress; const progress = new EditorProgress(); let force_wait = 24; - progress.init(`codegen-${this._name}`, `Generating ${this._name}`, this.tasks.length); + progress.init(`codegen-${this._name}`, this._name, this.tasks.length); try { for (let i = 0; i < this.tasks.length; ++i) { @@ -2491,7 +2491,7 @@ export class TSDCodeGen { async emit() { await frame_step(); - const tasks = new CodegenTasks("godot.d.ts"); + const tasks = new CodegenTasks("Generating godot.d.ts"); // aliases tasks.add_task("Aliases", () => this.emit_aliases()); From 05647aca35a56ec0ce1ee5127db9e45678be2d91 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Tue, 1 Jul 2025 02:44:52 +1000 Subject: [PATCH 11/51] fix: Critical bug fixes (for crashes) that may occur due to the current isolate not being set. This went unnoticed because many V8 APIs don't access the current isolate. However, a easy to reproduce crash that I ran into was allocating a large array. Internally V8 takes a different code path for large arrays. This code path attempts to grab the current isolate. If these large arrays happened to be allocated during startup (JS module root), the current isolate was not set previously and this led to a crash. Fortunately, V8 has very nice debug mode features that will highlight all sorts of issues like this. V8 simply needs to be built with args (args.gn): v8_enable_backtrace = true v8_enable_slow_dchecks = true v8_optimized_debug = false These are VERY slow though, which is why we're not enabling these in our V8 distributables. --- .../femorocaudal-phrenological-pinguecula.md | 6 + bridge/jsb_environment.cpp | 152 +++++++++--------- 2 files changed, 84 insertions(+), 74 deletions(-) create mode 100644 .changeset/femorocaudal-phrenological-pinguecula.md diff --git a/.changeset/femorocaudal-phrenological-pinguecula.md b/.changeset/femorocaudal-phrenological-pinguecula.md new file mode 100644 index 00000000..683831d2 --- /dev/null +++ b/.changeset/femorocaudal-phrenological-pinguecula.md @@ -0,0 +1,6 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Critical bug fixes (for crashes) that may occur due to the +current isolate not being set. diff --git a/bridge/jsb_environment.cpp b/bridge/jsb_environment.cpp index 4bdb7f20..02b9a983 100644 --- a/bridge/jsb_environment.cpp +++ b/bridge/jsb_environment.cpp @@ -274,111 +274,112 @@ namespace jsb isolate_->AddGCPrologueCallback(&OnPreGCCallback); isolate_->AddGCEpilogueCallback(&OnPostGCCallback); #endif + { - v8::HandleScope handle_scope(isolate_); - for (int index = 0; index < Symbols::kNum; ++index) + v8::Isolate::Scope isolate_scope(isolate_); + + // create context { - symbols_[index].Reset(isolate_, v8::Symbol::New(isolate_)); - } - } + v8::HandleScope handle_scope(isolate_); - native_classes_.reserve(p_params.initial_class_slots); - script_classes_.reserve(p_params.initial_script_slots); + for (int index = 0; index < Symbols::kNum; ++index) + { + symbols_[index].Reset(isolate_, v8::Symbol::New(isolate_)); + } - module_loaders_.insert("godot", memnew(GodotModuleLoader)); - module_loaders_.insert("godot-jsb", memnew(BridgeModuleLoader)); - EnvironmentStore::get_shared().add(this); + native_classes_.reserve(p_params.initial_class_slots); + script_classes_.reserve(p_params.initial_script_slots); - // create context - { - JSB_BENCHMARK_SCOPE(JSRealm, Construct); + module_loaders_.insert("godot", memnew(GodotModuleLoader)); + module_loaders_.insert("godot-jsb", memnew(BridgeModuleLoader)); + EnvironmentStore::get_shared().add(this); - v8::Isolate::Scope isolate_scope(isolate_); - v8::HandleScope handle_scope(isolate_); + JSB_BENCHMARK_SCOPE(JSRealm, Construct); - const v8::Local context = v8::Context::New(isolate_); - const v8::Context::Scope context_scope(context); - const v8::Local global = context->Global(); + const v8::Local context = v8::Context::New(isolate_); + const v8::Context::Scope context_scope(context); + const v8::Local global = context->Global(); - context->SetAlignedPointerInEmbedderData(kContextEmbedderData, this); - context_.Reset(isolate_, context); + context->SetAlignedPointerInEmbedderData(kContextEmbedderData, this); + context_.Reset(isolate_, context); - // init module cache, and register the global 'require' function - { - const v8::Local cache_obj = v8::Object::New(isolate_); - const v8::Local require_func = JSB_NEW_FUNCTION(context, Builtins::_require, {}); - require_func->Set(context, jsb_name(this, cache), cache_obj).Check(); - require_func->Set(context, impl::Helper::new_string_ascii(isolate_, "moduleId"), v8::String::Empty(isolate_)).Check(); - global->Set(context, impl::Helper::new_string_ascii(isolate_, "require"), require_func).Check(); - global->Set(context, impl::Helper::new_string_ascii(isolate_, "define"), JSB_NEW_FUNCTION(context, Builtins::_define, {})).Check(); - module_cache_.init(isolate_, cache_obj); - } - - internal::StringNames& names = internal::StringNames::get_singleton(); + // init module cache, and register the global 'require' function + { + const v8::Local cache_obj = v8::Object::New(isolate_); + const v8::Local require_func = JSB_NEW_FUNCTION(context, Builtins::_require, {}); + require_func->Set(context, jsb_name(this, cache), cache_obj).Check(); + require_func->Set(context, impl::Helper::new_string_ascii(isolate_, "moduleId"), v8::String::Empty(isolate_)).Check(); + global->Set(context, impl::Helper::new_string_ascii(isolate_, "require"), require_func).Check(); + global->Set(context, impl::Helper::new_string_ascii(isolate_, "define"), JSB_NEW_FUNCTION(context, Builtins::_define, {})).Check(); + module_cache_.init(isolate_, cache_obj); + } - // Populate StringNames replacement list so that classes can be lazily loaded by their exposed class name. - if (internal::Settings::get_camel_case_bindings_enabled()) - { - List exposed_class_list = internal::NamingUtil::get_exposed_original_class_list(); + internal::StringNames& names = internal::StringNames::get_singleton(); - for (auto it = exposed_class_list.begin(); it != exposed_class_list.end(); ++it) + // Populate StringNames replacement list so that classes can be lazily loaded by their exposed class name. + if (internal::Settings::get_camel_case_bindings_enabled()) { - String exposed_name = internal::NamingUtil::get_class_name(*it); + List exposed_class_list = internal::NamingUtil::get_exposed_original_class_list(); - if (exposed_name != *it) + for (auto it = exposed_class_list.begin(); it != exposed_class_list.end(); ++it) { - names.add_replacement(*it, exposed_name); + String exposed_name = internal::NamingUtil::get_class_name(*it); + + if (exposed_name != *it) + { + names.add_replacement(*it, exposed_name); + } } - } - List singleton_list; - Engine::get_singleton()->get_singletons(&singleton_list); + List singleton_list; + Engine::get_singleton()->get_singletons(&singleton_list); - for (auto it = singleton_list.begin(); it != singleton_list.end(); ++it) - { - String exposed_name = internal::NamingUtil::get_class_name(it->name); - - if (exposed_name != it->name) + for (auto it = singleton_list.begin(); it != singleton_list.end(); ++it) { - names.add_replacement(it->name, exposed_name); + String exposed_name = internal::NamingUtil::get_class_name(it->name); + + if (exposed_name != it->name) + { + names.add_replacement(it->name, exposed_name); + } } - } - List utility_function_list; - Variant::get_utility_function_list(&utility_function_list); + List utility_function_list; + Variant::get_utility_function_list(&utility_function_list); - for (auto it = utility_function_list.begin(); it != utility_function_list.end(); ++it) - { - String exposed_name = internal::NamingUtil::get_member_name(*it); - - if (exposed_name != *it) + for (auto it = utility_function_list.begin(); it != utility_function_list.end(); ++it) { - names.add_replacement(*it, exposed_name); - } - } + String exposed_name = internal::NamingUtil::get_member_name(*it); - const int constant_count = CoreConstants::get_global_constant_count(); - for (int index = 0; index < constant_count; ++index) - { - const StringName enum_name = CoreConstants::get_global_constant_enum(index); - String exposed_name = internal::NamingUtil::get_class_name(enum_name); + if (exposed_name != *it) + { + names.add_replacement(*it, exposed_name); + } + } - if (exposed_name != enum_name) + const int constant_count = CoreConstants::get_global_constant_count(); + for (int index = 0; index < constant_count; ++index) { - names.add_replacement(enum_name, exposed_name); + const StringName enum_name = CoreConstants::get_global_constant_enum(index); + String exposed_name = internal::NamingUtil::get_class_name(enum_name); + + if (exposed_name != enum_name) + { + names.add_replacement(enum_name, exposed_name); + } } } - } #if !JSB_WITH_WEB && !JSB_WITH_JAVASCRIPTCORE - Worker::register_(context, global); + Worker::register_(context, global); #endif - Essentials::register_(context, global); - register_primitive_bindings(this); - } + Essentials::register_(context, global); + register_primitive_bindings(this); + } - //TODO call `start_debugger` at different stages for Editor/Game Runtimes. - start_debugger(p_params.debugger_port); + //TODO call `start_debugger` at different stages for Editor/Game Runtimes. + start_debugger(p_params.debugger_port); + } } // no JS code should be executed in the destructor. @@ -1203,6 +1204,7 @@ namespace jsb { this->check_internal_state(); v8::Isolate* isolate = get_isolate(); + v8::Isolate::Scope isolate_scope(isolate); v8::HandleScope handle_scope(isolate); v8::Local context = context_.Get(isolate); v8::Context::Scope context_scope(context); @@ -1470,6 +1472,7 @@ namespace jsb if (strong_ref.unref()) { v8::Isolate* isolate = get_isolate(); + v8::Isolate::Scope isolate_scope(isolate); v8::HandleScope handle_scope(isolate); if (jsb_likely(!strong_ref.object_.IsEmpty())) { @@ -1573,6 +1576,7 @@ namespace jsb } v8::Isolate* isolate = get_isolate(); + v8::Isolate::Scope isolate_scope(isolate); v8::HandleScope handle_scope(isolate); const v8::Local context = this->get_context(); v8::Context::Scope context_scope(context); From e35d35ec64cef36017b7596b6bc00fa735f81d0c Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Tue, 1 Jul 2025 02:52:33 +1000 Subject: [PATCH 12/51] fix: Duplicate PackedByteArray to_array_buffer() registration --- .changeset/epitrichium-dacite-fibrinolytic.md | 5 +++++ bridge/jsb_primitive_bindings_reflect.cpp | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/epitrichium-dacite-fibrinolytic.md diff --git a/.changeset/epitrichium-dacite-fibrinolytic.md b/.changeset/epitrichium-dacite-fibrinolytic.md new file mode 100644 index 00000000..3eeab376 --- /dev/null +++ b/.changeset/epitrichium-dacite-fibrinolytic.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Duplicate PackedByteArray to_array_buffer() registration diff --git a/bridge/jsb_primitive_bindings_reflect.cpp b/bridge/jsb_primitive_bindings_reflect.cpp index 3decb385..dedcc16d 100644 --- a/bridge/jsb_primitive_bindings_reflect.cpp +++ b/bridge/jsb_primitive_bindings_reflect.cpp @@ -784,7 +784,6 @@ namespace jsb } } #endif - ReflectAdditionalMethodRegister::register_(class_builder); // convert method info, and store const int collection_index = (int) GetVariantInfoCollection(p_env.env).methods.size(); @@ -826,6 +825,8 @@ namespace jsb } } } + + ReflectAdditionalMethodRegister::register_(class_builder); } // operators From 143980a9b955761b599e6b5d493af2646a509c87 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Thu, 3 Jul 2025 15:25:12 +1000 Subject: [PATCH 13/51] fix: Variant constructor failure on first bound Variant class. We were attempting to cast a value, that should always be a 32-bit unsigned int. However, binding an empty v8::Value for the first class we attempted to contruct. --- .changeset/resymbolize-unavowedly-ocque.md | 5 +++++ impl/v8/jsb_v8_class_builder.h | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/resymbolize-unavowedly-ocque.md diff --git a/.changeset/resymbolize-unavowedly-ocque.md b/.changeset/resymbolize-unavowedly-ocque.md new file mode 100644 index 00000000..3fcfd120 --- /dev/null +++ b/.changeset/resymbolize-unavowedly-ocque.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Variant constructor failure on first bound Variant class. diff --git a/impl/v8/jsb_v8_class_builder.h b/impl/v8/jsb_v8_class_builder.h index 38765146..1f8ce324 100644 --- a/impl/v8/jsb_v8_class_builder.h +++ b/impl/v8/jsb_v8_class_builder.h @@ -243,7 +243,7 @@ namespace jsb::impl template static ClassBuilder New(v8::Isolate* isolate, const StringName& name, const v8::FunctionCallback constructor, const uint32_t class_payload) { - const v8::Local data = class_payload != 0 ? v8::Uint32::NewFromUnsigned(isolate, class_payload).As() : v8::Local(); + const v8::Local data = v8::Uint32::NewFromUnsigned(isolate, class_payload).As(); ClassBuilder builder; builder.isolate_ = isolate; From 9c6988939f0adb906b87b0b7c52421deaf930f8c Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Mon, 7 Jul 2025 02:22:13 +1000 Subject: [PATCH 14/51] fix: Another missing Isolate::Scope --- .changeset/muslined-behavioristic-thanatist.md | 5 +++++ bridge/jsb_environment.cpp | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/muslined-behavioristic-thanatist.md diff --git a/.changeset/muslined-behavioristic-thanatist.md b/.changeset/muslined-behavioristic-thanatist.md new file mode 100644 index 00000000..a8b96485 --- /dev/null +++ b/.changeset/muslined-behavioristic-thanatist.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Another missing `Isolate::Scope` that could lead to a runtime crash. diff --git a/bridge/jsb_environment.cpp b/bridge/jsb_environment.cpp index 02b9a983..c12279c7 100644 --- a/bridge/jsb_environment.cpp +++ b/bridge/jsb_environment.cpp @@ -1550,6 +1550,7 @@ namespace jsb } v8::Isolate* isolate = get_isolate(); + v8::Isolate::Scope isolate_scope(isolate); v8::HandleScope handle_scope(isolate); const v8::Local context = this->get_context(); v8::Context::Scope context_scope(context); From e7a6713ee60240cd2b54c11391c644988602af77 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Fri, 11 Jul 2025 20:08:12 +1000 Subject: [PATCH 15/51] fix: NodePathMap should permit undefined/optional children --- .changeset/ethnobiological-elfenfolk-semola.md | 5 +++++ scripts/typings/godot.mix.d.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/ethnobiological-elfenfolk-semola.md diff --git a/.changeset/ethnobiological-elfenfolk-semola.md b/.changeset/ethnobiological-elfenfolk-semola.md new file mode 100644 index 00000000..5394021a --- /dev/null +++ b/.changeset/ethnobiological-elfenfolk-semola.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** `NodePathMap` now permits `undefined`/optional children. diff --git a/scripts/typings/godot.mix.d.ts b/scripts/typings/godot.mix.d.ts index 94d307d6..6e672133 100644 --- a/scripts/typings/godot.mix.d.ts +++ b/scripts/typings/godot.mix.d.ts @@ -178,7 +178,7 @@ declare module "godot" { Map[keyof Map] extends undefined | Permitted ? Exclude : Default >; - type NodePathMap = PathMap; + type NodePathMap = PathMap; type StaticNodePath = StaticPath; type ResolveNodePath = ResolvePath; From c01c0d401eae1054cce7f3a53d7ce7e34e821534 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Wed, 30 Jul 2025 21:23:47 +1000 Subject: [PATCH 16/51] fix: TStringNameCache v8::String reference loss https://github.com/godotjs/GodotJS/issues/110 --- .changeset/foreshadower-swashwork-appearer.md | 7 +++++++ bridge/jsb_string_name_cache.h | 18 +++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 .changeset/foreshadower-swashwork-appearer.md diff --git a/.changeset/foreshadower-swashwork-appearer.md b/.changeset/foreshadower-swashwork-appearer.md new file mode 100644 index 00000000..4d32477e --- /dev/null +++ b/.changeset/foreshadower-swashwork-appearer.md @@ -0,0 +1,7 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** TStringNameCache v8::String reference loss + +https://github.com/godotjs/GodotJS/issues/110 diff --git a/bridge/jsb_string_name_cache.h b/bridge/jsb_string_name_cache.h index 28b5bb90..d42dd31f 100644 --- a/bridge/jsb_string_name_cache.h +++ b/bridge/jsb_string_name_cache.h @@ -30,7 +30,7 @@ namespace jsb HashMap name_index; // JSValue => StringNameID - internal::TypeGen, StringNameID>::UnorderedMap value_index_; // backlink + internal::TypeGen, StringNameID>::UnorderedMap value_index_; // backlink // List< StringName+JSValue > // managed as a least recently used cache if max_size_ > 0 @@ -64,7 +64,7 @@ namespace jsb bool try_get_string_name(v8::Isolate* isolate, const v8::Local& p_value, StringName& r_string_name) const { - if (const auto& it = value_index_.find(TWeakRef(isolate, p_value)); it != value_index_.end()) + if (const auto& it = value_index_.find(TStrongRef(isolate, p_value)); it != value_index_.end()) { const StringNameID id = it->second; r_string_name = values_[id].name_; @@ -83,7 +83,7 @@ namespace jsb StringName get_string_name(v8::Isolate* isolate, const v8::Local& p_value) { - if (const auto& it = value_index_.find(TWeakRef(isolate, p_value)); it != value_index_.end()) + if (const auto& it = value_index_.find(TStrongRef(isolate, p_value)); it != value_index_.end()) { const StringNameID id = it->second; const StringName name = values_[id].name_; @@ -96,14 +96,14 @@ namespace jsb const StringName name = impl::Helper::to_string(isolate, p_value); const StringNameID id = get_string_id(isolate, name); Slot& slot = values_[id]; - if (slot.ref_ && slot.ref_ != TStrongRef(isolate, p_value)) + if (slot.ref_ && slot.ref_.object_ != p_value) { - const size_t removed = value_index_.erase(TWeakRef(isolate, slot.ref_.object_.Get(isolate))); - JSB_LOG(Warning, "(not recommended) update an existing string name %s", name); + const size_t removed = value_index_.erase(slot.ref_); + JSB_LOG(Verbose, "(not recommended) update an existing string name %s", name); jsb_check(removed == 1); } slot.ref_ = TStrongRef(isolate, p_value); - value_index_.insert(std::pair(TWeakRef(isolate, p_value), id)); + value_index_.insert(std::pair(TStrongRef(isolate, p_value), id)); JSB_LOG(VeryVerbose, "new string name pair (js) %s %d [slots:%d]", name, id, values_.size()); return name; } @@ -117,7 +117,7 @@ namespace jsb { const v8::Local str_val = impl::Helper::new_string(isolate, p_name); slot.ref_ = TStrongRef(isolate, str_val); - value_index_.insert(std::pair(TWeakRef(isolate, str_val), id)); + value_index_.insert(std::pair(TStrongRef(isolate, str_val), id)); JSB_LOG(VeryVerbose, "new string name pair (cpp) %s %d [slots:%d]", p_name, id, values_.size()); return str_val; } @@ -142,7 +142,7 @@ namespace jsb const StringNameID id = values_.get_first_index(); const Slot& slot = values_[id]; - value_index_.erase(TWeakRef(isolate, slot.ref_.object_)); + value_index_.erase(slot.ref_); name_index.erase(slot.name_); const StringNameID removed_id = values_.remove_first(); jsb_check(removed_id == id); From 8394f989bfca054b1481ed3b0331f660cde64864 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Thu, 31 Jul 2025 02:38:41 +1000 Subject: [PATCH 17/51] fix: Added missing PROPERTY_USAGE_SCRIPT_VARIABLE flag --- .changeset/equity-clarionet-subtenancy.md | 5 +++++ bridge/jsb_class_info.cpp | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/equity-clarionet-subtenancy.md diff --git a/.changeset/equity-clarionet-subtenancy.md b/.changeset/equity-clarionet-subtenancy.md new file mode 100644 index 00000000..7b8dd2ce --- /dev/null +++ b/.changeset/equity-clarionet-subtenancy.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Added missing `PROPERTY_USAGE_SCRIPT_VARIABLE` flag on exported variables. diff --git a/bridge/jsb_class_info.cpp b/bridge/jsb_class_info.cpp index ec144315..3e1d603a 100644 --- a/bridge/jsb_class_info.cpp +++ b/bridge/jsb_class_info.cpp @@ -222,7 +222,7 @@ namespace jsb property_info.type = (Variant::Type) obj->Get(context, jsb_name(environment, type)).ToLocalChecked()->Int32Value(context).ToChecked(); // int property_info.hint = BridgeHelper::to_enum(context, obj->Get(context, jsb_name(environment, hint)), PROPERTY_HINT_NONE); property_info.hint_string = impl::Helper::to_string(isolate, obj->Get(context, jsb_name(environment, hint_string)).ToLocalChecked()); - property_info.usage = BridgeHelper::to_enum(context, obj->Get(context, jsb_name(environment, usage)), PROPERTY_USAGE_DEFAULT); + property_info.usage = BridgeHelper::to_enum(context, obj->Get(context, jsb_name(environment, usage)), PROPERTY_USAGE_DEFAULT) | PROPERTY_USAGE_SCRIPT_VARIABLE; #ifdef TOOLS_ENABLED if (v8::Local val; !doc_map.IsEmpty() && doc_map->Get(p_context, prop_name).ToLocal(&val) && val->IsObject()) { From 8664bf26874996e478771d11b8d45874a222a0df Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Thu, 31 Jul 2025 19:18:40 +1000 Subject: [PATCH 18/51] fix: @ExportObject(Node). Was only working for sub-classes. --- .changeset/ammelide-dilute-nazarean.md | 5 +++++ scripts/jsb.runtime/src/godot.annotations.ts | 20 ++++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 .changeset/ammelide-dilute-nazarean.md diff --git a/.changeset/ammelide-dilute-nazarean.md b/.changeset/ammelide-dilute-nazarean.md new file mode 100644 index 00000000..0b5e809b --- /dev/null +++ b/.changeset/ammelide-dilute-nazarean.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** `@ExportObject(Node)` was only working for sub-classes, not `Node` itself. diff --git a/scripts/jsb.runtime/src/godot.annotations.ts b/scripts/jsb.runtime/src/godot.annotations.ts index 1c1367a5..df49a8a6 100644 --- a/scripts/jsb.runtime/src/godot.annotations.ts +++ b/scripts/jsb.runtime/src/godot.annotations.ts @@ -1,7 +1,7 @@ import type * as Godot from "godot"; import type * as GodotJsb from "godot-jsb"; -const { jsb, FloatType, IntegerType, Node, PropertyHint, PropertyUsageFlags, Resource, Variant } = require("godot.lib.api"); +const { jsb, FloatType, IntegerType, Node, PropertyHint, PropertyUsageFlags, ProxyTarget, Resource, Variant } = require("godot.lib.api"); function guess_type_name(type: any) { if (typeof type === "function") { @@ -146,12 +146,14 @@ function get_hint_string(clazz: any): string { } } - if (typeof clazz === "function" && typeof clazz.prototype !== "undefined") { - if (clazz.prototype instanceof Resource) { + if (typeof clazz === "function") { + const prototype = clazz.prototype; + + if (prototype instanceof Resource) { return `${Variant.Type.TYPE_OBJECT}/${PropertyHint.PROPERTY_HINT_RESOURCE_TYPE}:${clazz.name}`; - } else if (clazz.prototype instanceof Node) { + } else if (prototype instanceof Node || ((clazz as any)[ProxyTarget] ?? clazz) === (Node[ProxyTarget] ?? Node)) { return `${Variant.Type.TYPE_OBJECT}/${PropertyHint.PROPERTY_HINT_NODE_TYPE}:${clazz.name}`; - } else { + } else if (typeof prototype !== "undefined") { // other than Resource and Node, only primitive types and enum types are supported in gdscript //TODO but we barely know anything about the enum types and int/float/StringName/... in JS @@ -216,12 +218,14 @@ export function export_(type: Godot.Variant.Type, details?: { class_?: ClassDesc if (type === Variant.Type.TYPE_OBJECT) { const clazz = details.class_; - if (typeof clazz === "function" && typeof clazz.prototype !== "undefined") { - if (clazz.prototype instanceof Resource) { + if (typeof clazz === "function") { + const prototype = clazz.prototype; + + if (prototype instanceof Resource) { ebd.hint = PropertyHint.PROPERTY_HINT_RESOURCE_TYPE; ebd.hint_string = clazz.name; ebd.usage |= PropertyUsageFlags.PROPERTY_USAGE_SCRIPT_VARIABLE; - } else if (clazz.prototype instanceof Node) { + } else if (prototype instanceof Node || ((clazz as any)[ProxyTarget] ?? clazz) === (Node[ProxyTarget] ?? Node)) { ebd.hint = PropertyHint.PROPERTY_HINT_NODE_TYPE; ebd.hint_string = clazz.name; ebd.usage |= PropertyUsageFlags.PROPERTY_USAGE_SCRIPT_VARIABLE; From abd0137138f1896023dc41398195574e275e062a Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Sun, 3 Aug 2025 00:24:55 +1000 Subject: [PATCH 19/51] fix: JSWorker transfer crash A Context scope was not set, so attempts to access the isolate's active context (e.g. binding a new class) resulted in a crash. --- .changeset/gristliness-kipchak-intolerant.md | 5 +++++ bridge/jsb_environment.cpp | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/gristliness-kipchak-intolerant.md diff --git a/.changeset/gristliness-kipchak-intolerant.md b/.changeset/gristliness-kipchak-intolerant.md new file mode 100644 index 00000000..751997f2 --- /dev/null +++ b/.changeset/gristliness-kipchak-intolerant.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** `JSWorker` transfer crash. diff --git a/bridge/jsb_environment.cpp b/bridge/jsb_environment.cpp index c12279c7..f3f54f27 100644 --- a/bridge/jsb_environment.cpp +++ b/bridge/jsb_environment.cpp @@ -592,6 +592,7 @@ namespace jsb v8::Isolate::Scope isolate_scope(isolate_); v8::HandleScope handle_scope(isolate_); const v8::Local context = context_.Get(isolate_); + const v8::Context::Scope context_scope(context); _on_worker_transfer(context, transfer_data); } memdelete(transfer_data); From a14dfe00b5109dfd4f5ac44015cd5b8318a971ed Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Sun, 3 Aug 2025 02:43:23 +1000 Subject: [PATCH 20/51] feat: Expose Variant utility typeof() as godot_typeof(). --- .changeset/convolvulinolic-mascouten-evanesce.md | 5 +++++ bridge/jsb_environment.cpp | 12 ++++++++++++ scripts/jsb.editor/src/jsb.editor.codegen.ts | 13 ++++++++++--- weaver/jsb_script_language.cpp | 14 +++----------- weaver/jsb_script_language.h | 3 +++ 5 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 .changeset/convolvulinolic-mascouten-evanesce.md diff --git a/.changeset/convolvulinolic-mascouten-evanesce.md b/.changeset/convolvulinolic-mascouten-evanesce.md new file mode 100644 index 00000000..cef6657b --- /dev/null +++ b/.changeset/convolvulinolic-mascouten-evanesce.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Expose Godot Variant utility method typeof() as `godot_typeof()`/`godotTypeof()`. diff --git a/bridge/jsb_environment.cpp b/bridge/jsb_environment.cpp index f3f54f27..c54cd463 100644 --- a/bridge/jsb_environment.cpp +++ b/bridge/jsb_environment.cpp @@ -344,6 +344,8 @@ namespace jsb } } + Vector reserved_words = GodotJSScriptLanguage::get_singleton()->get_reserved_words(); + List utility_function_list; Variant::get_utility_function_list(&utility_function_list); @@ -351,6 +353,11 @@ namespace jsb { String exposed_name = internal::NamingUtil::get_member_name(*it); + if (reserved_words.find(exposed_name) >= 0) + { + exposed_name = internal::NamingUtil::get_member_name("godot_" + exposed_name); + } + if (exposed_name != *it) { names.add_replacement(*it, exposed_name); @@ -363,6 +370,11 @@ namespace jsb const StringName enum_name = CoreConstants::get_global_constant_enum(index); String exposed_name = internal::NamingUtil::get_class_name(enum_name); + if (reserved_words.find(exposed_name) >= 0) + { + exposed_name = internal::NamingUtil::get_member_name("godot_" + exposed_name); + } + if (exposed_name != enum_name) { names.add_replacement(enum_name, exposed_name); diff --git a/scripts/jsb.editor/src/jsb.editor.codegen.ts b/scripts/jsb.editor/src/jsb.editor.codegen.ts index dd93326d..5ed6bcae 100644 --- a/scripts/jsb.editor/src/jsb.editor.codegen.ts +++ b/scripts/jsb.editor/src/jsb.editor.codegen.ts @@ -1619,12 +1619,19 @@ class ModuleWriter extends IndentWriter { const args = this.types.make_args(method_info); const rval = this.types.make_return(method_info); + let exposed_name = method_info.name; + + if (typeof KeywordReplacement[exposed_name] !== "undefined") { + exposed_name = names.get_member("godot_" + exposed_name); + } + // some godot methods declared with special characters which can not be declared literally - if (!this.types.is_valid_method_name(method_info.name)) { - this.line(`// [INVALID_NAME]: function ${method_info.name}(${args}): ${rval}`); + if (!this.types.is_valid_method_name(exposed_name)) { + this.line(`// [INVALID_NAME]: function ${exposed_name}(${args}): ${rval}`); return; } - this.line(`function ${method_info.name}(${args}): ${rval}`); + + this.line(`function ${exposed_name}(${args}): ${rval}`); } } diff --git a/weaver/jsb_script_language.cpp b/weaver/jsb_script_language.cpp index 53311901..37f47e3f 100644 --- a/weaver/jsb_script_language.cpp +++ b/weaver/jsb_script_language.cpp @@ -183,7 +183,6 @@ bool GodotJSScriptLanguage::is_control_flow_keyword(ConstStringRefCompat p_keywo return collection.values.has(p_keyword); } -#if GODOT_4_5_OR_NEWER Vector GodotJSScriptLanguage::get_reserved_words() const { return Vector { @@ -196,6 +195,7 @@ Vector GodotJSScriptLanguage::get_reserved_words() const }; } +#if GODOT_4_5_OR_NEWER Vector GodotJSScriptLanguage::get_doc_comment_delimiters() const { return Vector { "///" }; @@ -213,17 +213,9 @@ Vector GodotJSScriptLanguage::get_string_delimiters() const #else void GodotJSScriptLanguage::get_reserved_words(List* p_words) const { - static const char* keywords[] = { - "return", "function", "interface", "class", "let", "break", "as", "any", "switch", "case", "if", "enum", - "throw", "else", "var", "number", "string", "get", "module", "instanceof", "typeof", "public", "private", - "while", "void", "null", "super", "this", "new", "in", "await", "async", "extends", "static", - "package", "implements", "interface", "continue", "yield", "const", "export", "finally", "for", - "import", "byte", "delete", "goto", - "default", - }; - for (int i = 0, n = std::size(keywords); i < n; ++i) + for (String keyword : get_reserved_words()) { - p_words->push_back(keywords[i]); + p_words->push_back(keyword); } } diff --git a/weaver/jsb_script_language.h b/weaver/jsb_script_language.h index 31db25f8..0822917f 100644 --- a/weaver/jsb_script_language.h +++ b/weaver/jsb_script_language.h @@ -161,10 +161,13 @@ class GodotJSScriptLanguage : public ScriptLanguage #if GODOT_4_5_OR_NEWER virtual Vector get_reserved_words() const override; + virtual Vector get_doc_comment_delimiters() const override; virtual Vector get_comment_delimiters() const override; virtual Vector get_string_delimiters() const override; #else + virtual Vector get_reserved_words() const; + virtual void get_reserved_words(List* p_words) const override; virtual void get_doc_comment_delimiters(List* p_delimiters) const override; virtual void get_comment_delimiters(List* p_delimiters) const override; From 3bd9856225b9c0a9d714629b07bf29eb845b4d35 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Sun, 3 Aug 2025 02:45:06 +1000 Subject: [PATCH 21/51] fix: GDictionary keys() return type --- .changeset/radioscopic-autotype-bipennated.md | 5 +++++ scripts/jsb.editor/src/jsb.editor.codegen.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/radioscopic-autotype-bipennated.md diff --git a/.changeset/radioscopic-autotype-bipennated.md b/.changeset/radioscopic-autotype-bipennated.md new file mode 100644 index 00000000..cd73281b --- /dev/null +++ b/.changeset/radioscopic-autotype-bipennated.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** GDictionary keys() return type diff --git a/scripts/jsb.editor/src/jsb.editor.codegen.ts b/scripts/jsb.editor/src/jsb.editor.codegen.ts index 5ed6bcae..db258e37 100644 --- a/scripts/jsb.editor/src/jsb.editor.codegen.ts +++ b/scripts/jsb.editor/src/jsb.editor.codegen.ts @@ -360,7 +360,7 @@ const TypeMutations: Record = { has_all: mutate_parameter_type("keys", "GArray"), find_key: chain_mutators(mutate_parameter_type("value", "T[keyof T]"), mutate_return_type("keyof T")), // This can be typed more accurately with a mapped type, but it seems excessive. erase: mutate_parameter_type("key", "keyof T"), - keys: mutate_return_type("Array"), + keys: mutate_return_type("GArray"), values: mutate_return_type("GArray"), duplicate: mutate_return_type("GDictionary"), get: chain_mutators(mutate_parameter_type("key", "K"), mutate_return_type("T[K]"), mutate_template("K extends keyof T")), From 453da8e799a555756f3702873c8a15d6d24f866c Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Sun, 3 Aug 2025 02:52:40 +1000 Subject: [PATCH 22/51] feat: Improved GObject types --- .changeset/catharticalness-amphicarpous-unshewed.md | 5 +++++ scripts/jsb.editor/src/jsb.editor.codegen.ts | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 .changeset/catharticalness-amphicarpous-unshewed.md diff --git a/.changeset/catharticalness-amphicarpous-unshewed.md b/.changeset/catharticalness-amphicarpous-unshewed.md new file mode 100644 index 00000000..f0b6f19b --- /dev/null +++ b/.changeset/catharticalness-amphicarpous-unshewed.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Types:** Improved GObject types diff --git a/scripts/jsb.editor/src/jsb.editor.codegen.ts b/scripts/jsb.editor/src/jsb.editor.codegen.ts index db258e37..b450b46f 100644 --- a/scripts/jsb.editor/src/jsb.editor.codegen.ts +++ b/scripts/jsb.editor/src/jsb.editor.codegen.ts @@ -431,6 +431,14 @@ const TypeMutations: Record = { validate_property: mutate_parameter_type("property", "GDictionary"), }, }, + // GObject: + [names.get_class("Object")]: { + property_overrides: { + get_property_list: mutate_return_type("GArray>"), + get_script: mutate_return_type("null | Script"), + set_script: mutate_parameter_type("script", "null | Script"), + } + }, PackedByteArray: { intro: [ "/** [jsb utility method] Converts a PackedByteArray to a JavaScript ArrayBuffer. */", From 39a01db4d440299f3160358d9a11c072bc7ba501 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Sun, 3 Aug 2025 02:54:51 +1000 Subject: [PATCH 23/51] fix: Ensure the GAny union type includes null --- .changeset/fasciolar-cloisonless-hymeniophore.md | 5 +++++ scripts/jsb.editor/src/jsb.editor.codegen.ts | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 .changeset/fasciolar-cloisonless-hymeniophore.md diff --git a/.changeset/fasciolar-cloisonless-hymeniophore.md b/.changeset/fasciolar-cloisonless-hymeniophore.md new file mode 100644 index 00000000..355a71ed --- /dev/null +++ b/.changeset/fasciolar-cloisonless-hymeniophore.md @@ -0,0 +1,5 @@ +--- +"@godot-js/editor": patch +--- + +**Types**: Ensure the GAny union type includes null diff --git a/scripts/jsb.editor/src/jsb.editor.codegen.ts b/scripts/jsb.editor/src/jsb.editor.codegen.ts index b450b46f..3d947cb6 100644 --- a/scripts/jsb.editor/src/jsb.editor.codegen.ts +++ b/scripts/jsb.editor/src/jsb.editor.codegen.ts @@ -2586,13 +2586,12 @@ export class TSDCodeGen { } if (GodotAnyType != "any") { - let gd_variant_alias = `type ${GodotAnyType} = `; + let gd_variant_alias = `type ${GodotAnyType} = undefined | null`; for (let i = godot.Variant.Type.TYPE_NIL + 1; i < godot.Variant.Type.TYPE_MAX; ++i) { const type_name = get_primitive_type_name(i); if (type_name == GodotAnyType || type_name == "any") continue; - gd_variant_alias += type_name + " | "; + gd_variant_alias += " | " + type_name; } - gd_variant_alias += "undefined"; cg.line(gd_variant_alias); } From 3369ccd318d3ed626bbc984ab5a66612057c19c5 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Tue, 5 Aug 2025 12:33:18 +1000 Subject: [PATCH 24/51] fix: Bind script instance during default prop evaluation If properties are implemented in JavaScript as properties (getters) they'll often want to call Godot methods on self. Without the script instance being bound this led to a crash. --- .changeset/chondrosteoma-keratoleukoma-tentaculocyst.md | 9 +++++++++ bridge/jsb_environment.cpp | 3 +++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/chondrosteoma-keratoleukoma-tentaculocyst.md diff --git a/.changeset/chondrosteoma-keratoleukoma-tentaculocyst.md b/.changeset/chondrosteoma-keratoleukoma-tentaculocyst.md new file mode 100644 index 00000000..e3199805 --- /dev/null +++ b/.changeset/chondrosteoma-keratoleukoma-tentaculocyst.md @@ -0,0 +1,9 @@ +--- +"@godot-js/editor": patch +--- + +**Fix:** Bind script instance during default prop evaluation. + +If properties are implemented in JavaScript as properties (getters) +they'll often want to call Godot methods on self. Without the +script instance being bound this led to a crash. diff --git a/bridge/jsb_environment.cpp b/bridge/jsb_environment.cpp index c54cd463..35015736 100644 --- a/bridge/jsb_environment.cpp +++ b/bridge/jsb_environment.cpp @@ -1655,7 +1655,10 @@ namespace jsb return; } + const v8::Local class_default_object = instance.As(); + ScriptClassInfo::instantiate(this, p_class_info.module_id, class_default_object); + // read from the class default object for (auto& prop_kv : p_class_info.properties) { From 24616145405be91245c76fd52e02f7a12db38359 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Wed, 6 Aug 2025 02:20:05 +1000 Subject: [PATCH 25/51] feat: Constructor params and GDScript compatible 'new' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support for `new` is very important for improving interop with other scripting languages, allowing them to instantiate objects from a Script reference. Crucially, this allows native GDExtension to instantiate Nodes/Objects/Resources implemented in GodotJS. Object construction has been refactored. Previously we had three cases to handle: 1. new SomeJSWrapperAroundAGodotObject() – JS construction 2. CDO (Class default object) construction - used to determine default parameters on a class (for use in the editor). 3. Cross binding. Which is when a Godot Object is constructed and our script latter needs to attach to it. In the past, case 1 was the only situation in which instantiating a JS class ought to also instantiate the Godot native object. However, in my previous commit I changed CDOs so also instantiate the underlying native object. So case 2 was eliminated. Case 3 (cross binding) is a common situation, it was previously implemented in a somewhat intrusive fashion. All JS objects were being constructed and passed a parameter as their first argument. This indicated whether the object was cross binding (or a CDO). This prevented users from (easily) implementing constructors, the user had to know about the internal parameter and pass this up through to super(). For the most part I imagine users (myself included) simply avoided using constructors. However, this complicated some code that would otherwise be trivial in GDScript or C# because the latter has constructors and the former _init. Consequently, I've implemented a new strategy to determine whether a constructor is being called from C++ (cross binding) or from JS. The implementation is quite straight forward, but arriving at this solution was not necessarily obvious. V8 doesn't expose APIs to intercept construction, and we can't naively use shared flags to mark a native constructor in progress because the solution needs to support reentrancy since during construction an object make instantiate other objects. Additionally, we can't just use different constructors (V8 templates) because there's issues with both sub-classing and `instanceof` detection. The solution was to take advantage of JS' Reflect.construct API. This allows us to call a constructor but have `new.target` set to an arbitrary constructable. `new.target` survives whilst traversing up through super() constructors (similar to how we previously passed arguments up). This let's us mark a particular instantiation as coming from C++. Reflect.construct instantiates `this` to match the prototype chain from the provided `newTarget` so we simply set it to the prototype of the original constructor and we're on our way. Using Reflect.construct also is also standard JS, so it's available on our support JS runtimes without needing to resort to any runtime specific code. --- .changeset/cantabrize-infrigidation-spousy.md | 61 ++++++++++ bridge/jsb_environment.cpp | 49 ++++++-- bridge/jsb_environment.h | 20 ++- bridge/jsb_object_bindings.cpp | 2 +- bridge/jsb_transpiler.h | 114 +++--------------- internal/jsb_string_names.def.h | 2 + weaver/jsb_script.cpp | 35 +++++- weaver/jsb_script.h | 12 +- 8 files changed, 178 insertions(+), 117 deletions(-) create mode 100644 .changeset/cantabrize-infrigidation-spousy.md diff --git a/.changeset/cantabrize-infrigidation-spousy.md b/.changeset/cantabrize-infrigidation-spousy.md new file mode 100644 index 00000000..86c7b06b --- /dev/null +++ b/.changeset/cantabrize-infrigidation-spousy.md @@ -0,0 +1,61 @@ +--- +"@godot-js/editor": patch +--- + +**Feature:** Constructor params support and GDScript compatible `new` + +Support for `new` is very important for improving interop with +other scripting languages, allowing them to instantiate objects +from a Script reference. Crucially, this allows native +GDExtension to instantiate Nodes/Objects/Resources implemented +in GodotJS. + +Object construction has been refactored. Previously we had three +cases to handle: + +1. new SomeJSWrapperAroundAGodotObject() – JS construction +2. CDO (Class default object) construction - used to determine + default parameters on a class (for use in the editor). +3. Cross binding. Which is when a Godot Object is constructed and + our script latter needs to attach to it. + +In the past, case 1 was the only situation in which instantiating +a JS class ought to also instantiate the Godot native object. +However, in my previous commit I changed CDOs so also instantiate +the underlying native object. So case 2 was eliminated. + +Case 3 (cross binding) is a common situation, it was previously +implemented in a somewhat intrusive fashion. All JS objects were +being constructed and passed a parameter as their first argument. +This indicated whether the object was cross binding (or a CDO). +This prevented users from (easily) implementing constructors, the +user had to know about the internal parameter and pass this +up through to super(). For the most part I imagine users (myself +included) simply avoided using constructors. However, this +complicated some code that would otherwise be trivial in GDScript +or C# because the latter has constructors and the former _init. + +Consequently, I've implemented a new strategy to determine whether +a constructor is being called from C++ (cross binding) or from JS. +The implementation is quite straight forward, but arriving at this +solution was not necessarily obvious. V8 doesn't expose APIs to +intercept construction, and we can't naively use shared flags +to mark a native constructor in progress because the solution +needs to support reentrancy since during construction an object +may instantiate other objects. Additionally, we can't just use +different constructors (V8 templates) because there's issues with +both sub-classing and `instanceof` detection. + +The solution was to take advantage of JS' `Reflect.construct` API. +This allows us to call a constructor but have `new.target` set to +an arbitrary constructable. `new.target` survives whilst +traversing up through super() constructors (similar to how we +previously passed arguments up). This let's us mark a particular +instantiation as coming from C++. `Reflect.construct` instantiates +`this` to match the prototype chain from the provided `newTarget` +so we simply set it to the prototype of the original constructor +and we're on our way. + +Using `Reflect.construct` is also standard JS, so it's available +on our support JS runtimes without needing to resort to any +runtime-specific code. diff --git a/bridge/jsb_environment.cpp b/bridge/jsb_environment.cpp index 35015736..4f701f0f 100644 --- a/bridge/jsb_environment.cpp +++ b/bridge/jsb_environment.cpp @@ -303,6 +303,10 @@ namespace jsb context->SetAlignedPointerInEmbedderData(kContextEmbedderData, this); context_.Reset(isolate_, context); + v8::Local js_only_constructor_template = v8::FunctionTemplate::New(isolate_); + js_only_constructor_template->InstanceTemplate()->SetInternalFieldCount(IF_ObjectFieldCount); + js_only_constructor_tag_.Reset(isolate_, js_only_constructor_template->GetFunction(context).ToLocalChecked()); + // init module cache, and register the global 'require' function { const v8::Local cache_obj = v8::Object::New(isolate_); @@ -1213,7 +1217,7 @@ namespace jsb return nullptr; } - NativeObjectID Environment::crossbind(Object* p_this, ScriptClassID p_class_id) + NativeObjectID Environment::crossbind(Object* p_this, ScriptClassID p_class_id, const Variant** p_args, int p_argcount) { this->check_internal_state(); v8::Isolate* isolate = get_isolate(); @@ -1225,7 +1229,7 @@ namespace jsb // In Editor, the script can be attached to an Object after it created in JS (e.g. 'enter_tree' as a child node of a script attached parent node) if (const NativeObjectID object_id = this->try_get_object_id(p_this)) { - JSB_LOG(Verbose, "crossbinding on a binded object %d (addr:%d), rebind it to script class %d", object_id, (uintptr_t) p_this, p_class_id); + JSB_LOG(Verbose, "crossbinding on previously bound object %d (addr:%d), rebind it to script class %d", object_id, (uintptr_t) p_this, p_class_id); //TODO may not work in this way _rebind(isolate, context, p_this, p_class_id); @@ -1245,12 +1249,42 @@ namespace jsb jsb_check(!class_obj->IsNullOrUndefined()); } + v8::Local arguments = v8::Array::New(isolate, p_argcount); + + for (int index = 0; index < p_argcount; ++index) + { + v8::Local argument; + + if (TypeConvert::gd_var_to_js(isolate, context, *p_args[index], argument)) + { + arguments->Set(context, index, argument); + } + else + { + return {}; + } + } + const impl::TryCatch try_catch_run(isolate); - v8::Local identifier = jsb_symbol(this, CrossBind); - const v8::MaybeLocal constructed_value = class_obj->CallAsConstructor(context, 1, &identifier); + + v8::Local js_only_constructor_tag = js_only_constructor_tag_.Get(isolate); + v8::Local class_prototype = class_obj->Get(context, jsb_name(this, prototype)).ToLocalChecked(); + js_only_constructor_tag->Set(context, jsb_name(this, prototype), class_prototype).Check(); + + v8::Local reflect = context->Global()->Get(context, jsb_name(this, Reflect)).ToLocalChecked().As(); + v8::Local reflect_construct = reflect->Get(context, jsb_name(this, construct)).ToLocalChecked().As(); + + v8::Local reflect_args[] = { + class_obj, + arguments, + js_only_constructor_tag + }; + + v8::MaybeLocal constructed_value = reflect_construct->Call(context, reflect, 3, reflect_args); + if (try_catch_run.has_caught()) { - JSB_LOG(Error, "something wrong when constructing '%s'\n%s", js_class_name, BridgeHelper::get_exception(try_catch_run)); + JSB_LOG(Error, "something went wrong when constructing '%s'\n%s", js_class_name, BridgeHelper::get_exception(try_catch_run)); return {}; } @@ -1635,10 +1669,9 @@ namespace jsb v8::Context::Scope context_scope(context); { - v8::Local identifier = jsb_symbol(this, CDO); const v8::Local class_obj = p_class_info.js_class.Get(isolate); const impl::TryCatch try_catch_run(isolate); - const v8::MaybeLocal constructed_value = class_obj->CallAsConstructor(context, 1, &identifier); + const v8::MaybeLocal constructed_value = class_obj->CallAsConstructor(context, 0, nullptr); if (try_catch_run.has_caught()) { @@ -1655,9 +1688,7 @@ namespace jsb return; } - const v8::Local class_default_object = instance.As(); - ScriptClassInfo::instantiate(this, p_class_info.module_id, class_default_object); // read from the class default object for (auto& prop_kv : p_class_info.properties) diff --git a/bridge/jsb_environment.h b/bridge/jsb_environment.h index f73181a5..48a34cee 100644 --- a/bridge/jsb_environment.h +++ b/bridge/jsb_environment.h @@ -46,7 +46,6 @@ namespace jsb MemberDocMap, CrossBind, // a symbol can only be used from C++ to indicate calling from cross-bind - CDO, // constructing class default object for a script // Exposed as properties on the `godot` module FloatType, @@ -198,6 +197,8 @@ namespace jsb internal::VariantInfoCollection variant_info_collection_; + v8::Global js_only_constructor_tag_; + public: enum class Type : uint8_t { @@ -287,7 +288,7 @@ namespace jsb ObjectCacheID get_cached_function(const v8::Local& p_func); bool release_function(ObjectCacheID p_func_id); - Variant call_function(void* p_pointer, ObjectCacheID p_func_id, const Variant **p_args, int p_argcount, Callable::CallError &r_error); + Variant call_function(void* p_pointer, ObjectCacheID p_func_id, const Variant** p_args, int p_argcount, Callable::CallError &r_error); /** * This method will not throw any JS exception. @@ -335,7 +336,7 @@ namespace jsb //TODO is there a simple way to compile (validate) the script without any side effect? bool validate_script(const String& p_path); - NativeObjectID crossbind(Object* p_this, ScriptClassID p_class_id); + NativeObjectID crossbind(Object* p_this, ScriptClassID p_class_id, const Variant** p_args, int p_argcount); void rebind(Object* p_this, ScriptClassID p_class_id); @@ -568,6 +569,19 @@ namespace jsb // NOTE: you can't get a shadow environment with this method static std::shared_ptr _access(); + /** + * Should a constructor call only construct a JavaScript object i.e. skip creation of the native Godot Object? + * This occurs when we have an existing Godot object that we wish to "cross bind" with its JavaScript + * implementation. For example, when instantiating a packed scene, Godot creates the objects first; we must then + * cross bind these existing objects with their JavaScript implementation. + * @param function + * @return + */ + bool is_js_only_constructor_tag(const v8::Local function) + { + return function == js_only_constructor_tag_; + } + private: void exec_async_calls(); void exec_async_call(AsyncCall::Type p_type, void* p_binding); diff --git a/bridge/jsb_object_bindings.cpp b/bridge/jsb_object_bindings.cpp index 4c4999c6..cf76e1cf 100644 --- a/bridge/jsb_object_bindings.cpp +++ b/bridge/jsb_object_bindings.cpp @@ -18,7 +18,7 @@ namespace jsb // construct type template { - impl::ClassBuilder class_builder = ClassTemplate::create(p_env, class_id); + impl::ClassBuilder class_builder = ObjectTemplate::create(p_env, class_id); //NOTE all singleton object will overwrite the class itself in 'godot' module, so we need make all things defined on PrototypeTemplate. const bool is_singleton_class = Engine::get_singleton()->has_singleton(p_class_info->name); diff --git a/bridge/jsb_transpiler.h b/bridge/jsb_transpiler.h index e984e30f..8ede1033 100644 --- a/bridge/jsb_transpiler.h +++ b/bridge/jsb_transpiler.h @@ -6,25 +6,6 @@ #include "jsb_type_convert.h" #include "jsb_object_handle.h" -#define JSB_CLASS_BOILERPLATE() \ - jsb_force_inline static impl::ClassBuilder create(Environment* env, internal::Index32 class_id)\ - {\ - v8::Isolate* isolate = env->get_isolate();\ - NativeClassInfoPtr class_info = env->get_native_class(class_id);\ - class_info->finalizer = &finalizer;\ - return impl::ClassBuilder::New(isolate, class_info->name, &constructor, *class_id);\ - } - -#define JSB_CLASS_BOILERPLATE_ARGS() \ - template\ - jsb_force_inline static impl::ClassBuilder create(Environment* env, internal::Index32 class_id)\ - {\ - v8::Isolate* isolate = env->get_isolate();\ - NativeClassInfoPtr class_info = env->get_native_class(class_id);\ - class_info->finalizer = &finalizer;\ - return impl::ClassBuilder::New(isolate, class_info->name, &constructor, *class_id);\ - } - #define JSB_CONTEXT_BOILERPLATE() \ v8::Isolate* isolate = info.GetIsolate();\ v8::Local context = isolate->GetCurrentContext();\ @@ -297,61 +278,17 @@ namespace jsb }; - template - struct ClassTemplate + struct ObjectTemplate { - JSB_CLASS_BOILERPLATE() - JSB_CLASS_BOILERPLATE_ARGS() - - static void constructor(const v8::FunctionCallbackInfo& info) + jsb_force_inline static impl::ClassBuilder create(Environment* env, internal::Index32 class_id) { - v8::Isolate* isolate = info.GetIsolate(); - v8::HandleScope handle_scope(isolate); - v8::Isolate::Scope isolate_scope(isolate); - v8::Local self = info.This(); - internal::Index32 class_id(info.Data().As()->Value()); - - TSelf* ptr = memnew(TSelf); - Environment* runtime = Environment::wrap(isolate); - runtime->bind_pointer(class_id, NativeClassType::Custom, ptr, self, 0); + v8::Isolate* isolate = env->get_isolate(); + NativeClassInfoPtr class_info = env->get_native_class(class_id); + class_info->finalizer = &finalizer; + return impl::ClassBuilder::New(isolate, class_info->name, &constructor, *class_id); } - template - static void constructor(const v8::FunctionCallbackInfo& info) - { - v8::Isolate* isolate = info.GetIsolate(); - v8::HandleScope handle_scope(isolate); - v8::Isolate::Scope isolate_scope(isolate); - v8::Local context = isolate->GetCurrentContext(); - v8::Local self = info.This(); - internal::Index32 class_id(info.Data().As()->Value()); - if (info.Length() != 3) - { - jsb_throw(isolate, "bad args"); - return; - } - P0 p0 = PrimitiveAccess::from(context, info[0]); - P1 p1 = PrimitiveAccess::from(context, info[1]); - P2 p2 = PrimitiveAccess::from(context, info[2]); - TSelf* ptr = memnew(TSelf(p0, p1, p2)); - Environment* runtime = Environment::wrap(isolate); - runtime->bind_pointer(class_id, NativeClassType::Custom, ptr, self, 0); - } - - static void finalizer(Environment* runtime, void* pointer, FinalizationType p_finalize) - { - TSelf* self = (TSelf*) pointer; - if (p_finalize != FinalizationType::None) - { - memdelete(self); - } - } - }; - - template<> - struct ClassTemplate - { - JSB_CLASS_BOILERPLATE() + private: static void constructor(const v8::FunctionCallbackInfo& info) { @@ -367,40 +304,21 @@ namespace jsb class_name = class_info->name; } - // We need to handle different cases of cross-binding here. - // 1. new SubClass() which defined in scripts - // 2. new CDO() from C++ - // 3. new SubClass() from C++ ResourcesLoader which needs to cross-bind an existing godot object instance with a newly constructed script instance + // We need to handle different binding scenarios: // - // The currently used solution is unsafe if the end user overrides the default constructor of a script. - // super(...arguments) must be called if constructor is explicitly defined in a script class. - - if (info.Length()) - { - const v8::Local identifier = info[0]; + // 1. Instantiating a new Godot object (e.g. new SubClass() in JS) with a new JS script class instance. + // 2. Instantiating a JS class to be attached to an existing Godot Object e.g., scene instantiation. - // (case-2) constructing CDO from C++ (nothing more to do, it's a pure javascript) - if (identifier == jsb_symbol(environment, CDO)) - { - JSB_LOG(Verbose, "constructing CDO from C++. %s(%d)", class_name, class_id); - return; - } - - // (case-3) constructing a cross-bind script object for the existing owner loaded from Resource. (nothing more to do) - if (identifier == jsb_symbol(environment, CrossBind)) - { - JSB_LOG(Verbose, "cross binding from C++. %s(%d)", class_name, class_id); - return; - } + jsb_check(info.NewTarget()->IsFunction()); + v8::Local context = isolate->GetCurrentContext(); + const v8::Local new_target = info.NewTarget().As(); - jsb_checkf(false, "unexpected identifier received. %s(%d)", class_name, class_id); + if (environment->is_js_only_constructor_tag(new_target)) + { + JSB_LOG(Verbose, "cross binding from C++. %s(%d)", class_name, class_id); return; } - jsb_check(info.NewTarget()->IsObject()); - v8::Local context = isolate->GetCurrentContext(); - const v8::Local new_target = info.NewTarget().As(); - // (case-0) directly instantiate from an underlying native class (it's usually called from scripts) if (new_target->HasOwnProperty(context, jsb_symbol(environment, ClassId)).ToChecked()) // if (class_info->clazz.NewTarget(isolate) == new_target) diff --git a/internal/jsb_string_names.def.h b/internal/jsb_string_names.def.h index e385d464..34fab62c 100644 --- a/internal/jsb_string_names.def.h +++ b/internal/jsb_string_names.def.h @@ -21,6 +21,8 @@ DEF(children) DEF(type) DEF(evaluator) DEF(_notification) +DEF(Reflect) +DEF(construct) // class names DEF(Object) diff --git a/weaver/jsb_script.cpp b/weaver/jsb_script.cpp index df455d06..4c52d119 100644 --- a/weaver/jsb_script.cpp +++ b/weaver/jsb_script.cpp @@ -139,8 +139,7 @@ ScriptInstance* GodotJSScript::instance_create(const v8::Local& p_th return instance; } -// this should only be called from godot internal -ScriptInstance* GodotJSScript::instance_create(Object* p_this, bool p_is_temp_allowed) +ScriptInstance* GodotJSScript::instance_construct(Object* p_this, bool p_is_temp_allowed, const Variant** p_args, int p_argcount) { jsb_check(is_valid()); jsb_check(loaded_); @@ -180,7 +179,9 @@ ScriptInstance* GodotJSScript::instance_create(Object* p_this, bool p_is_temp_al MutexLock lock(GodotJSScriptLanguage::get_singleton()->mutex_); instances_.insert(instance->owner_); } - instance->object_id_ = env->crossbind(p_this, instance->class_id_); + + instance->object_id_ = env->crossbind(p_this, instance->class_id_, p_args, p_argcount); + if (!instance->object_id_) { instance->script_ = Ref(); @@ -626,12 +627,34 @@ void GodotJSScript::_update_exports_values(List& r_props, HashMap< } } +Variant GodotJSScript::_new(const Variant** p_args, int p_argcount, Callable::CallError &r_error) +{ + if (!is_valid()) + { + JSB_LOG(Error, "Unable to create new instance. The script was not properly loaded (%s)", get_path()); + return Variant(); + } + + r_error.error = Callable::CallError::CALL_OK; + Object *owner = ClassDB::instantiate(script_class_info_.native_class_name); + + ScriptInstance *script_instance = instance_construct(owner, false, p_args, p_argcount); + + if (!script_instance) + { + memdelete(owner); + return Variant(); + } + + return owner; +} + bool GodotJSScript::_update_exports(PlaceHolderScriptInstance* p_instance_to_update) { // do not crash the engine if the script not loaded successfully if (!is_valid()) { - JSB_LOG(Error, "the script not properly loaded (%s)", get_path()); + JSB_LOG(Error, "script failed to load (%s)", get_path()); return false; } @@ -712,6 +735,10 @@ bool GodotJSScript::_update_exports(PlaceHolderScriptInstance* p_instance_to_upd return changed; } +void GodotJSScript::_bind_methods() { + ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "new", &GodotJSScript::_new, MethodInfo("new")); +} + void GodotJSScript::reload_from_file() { //TODO reload, maybe it's OK? diff --git a/weaver/jsb_script.h b/weaver/jsb_script.h index 3f9d435d..5e0aefdf 100644 --- a/weaver/jsb_script.h +++ b/weaver/jsb_script.h @@ -47,6 +47,8 @@ class GodotJSScript : public Script jsb_force_inline void ensure_module_loaded() const { if (jsb_unlikely(!loaded_)) const_cast(this)->load_module_immediately(); } jsb_force_inline bool _is_valid() const { return jsb::internal::VariantUtil::is_valid_name(script_class_info_.module_id); } + Variant _new(const Variant** p_args, int p_argcount, Callable::CallError &r_error); + bool _update_exports(PlaceHolderScriptInstance *p_instance_to_update); void _update_exports_values(List& r_props, HashMap& r_values); @@ -59,9 +61,13 @@ class GodotJSScript : public Script // Error attach_source(const String& p_path, bool p_take_over); Error load_source_code(const String &p_path); void load_module_if_missing(); + + // Creates a ScriptInstance and associates it with an existing JS object (instance of the script's JS class). ScriptInstance* instance_create(const v8::Local& p_this, Object* p_owner, bool p_is_temp_allowed); ScriptInstance* instance_create(const v8::Local& p_this, bool p_is_temp_allowed); - ScriptInstance* instance_create(Object* p_this, bool p_is_temp_allowed); + + // Creates a ScriptInstance and associates it with a newly constructed JS object (instance of script's class). + ScriptInstance* instance_construct(Object* p_this, bool p_is_temp_allowed, const Variant **p_args = nullptr, int p_argcount = 0); #pragma region Script Implementation virtual bool can_instantiate() const override; @@ -71,7 +77,7 @@ class GodotJSScript : public Script virtual bool inherits_script(const Ref