From 99eba4a4c308032b44d36fed8d760a8fd2c5196d Mon Sep 17 00:00:00 2001 From: DougBanksPersonal Date: Wed, 29 May 2019 15:26:07 -0700 Subject: [PATCH 01/65] updated example to correctly increment currenttime (#212) --- docs/guide/state-and-lifecycle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/state-and-lifecycle.md b/docs/guide/state-and-lifecycle.md index ed9a2f96..de027253 100644 --- a/docs/guide/state-and-lifecycle.md +++ b/docs/guide/state-and-lifecycle.md @@ -26,7 +26,7 @@ There's another form of `setState` we can use. When the new state we want our co function MyComponent:didMount() self:setState(function(state) return { - currentTime = currentTime + state.currentTime + currentTime = 1 + state.currentTime } end) end From cea50574d97a4f937b349e9ac4c6de8969448329 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Wed, 29 May 2019 15:34:42 -0700 Subject: [PATCH 02/65] Docs: Fix extra CSS for MkDocs-material 4.x --- docs/extra.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/extra.css b/docs/extra.css index ad498ccb..a0d882bd 100644 --- a/docs/extra.css +++ b/docs/extra.css @@ -4,13 +4,13 @@ align-items: center; vertical-align: middle; - padding: 0.8rem 1.2rem 0.8rem 1rem; + padding: 0.4rem 0.6rem 0.4rem 0.5rem; background-color: #efffec; - border-left: 0.4rem solid green; - border-radius: 0.2rem; + border-left: 0.2rem solid green; + border-radius: 0.1rem; line-height: 1; - font-size: 1.28rem; + font-size: 0.64rem; font-weight: bold; box-shadow: @@ -20,7 +20,7 @@ } .api-addition::before { - flex: 0 0 3rem; + flex: 0 0 1.5rem; content: "+"; color: green; font-size: 1.5em; From 8e89c4202103e58dac7d877820c4070c002d1ee4 Mon Sep 17 00:00:00 2001 From: Lily <31936135+AmaranthineCodices@users.noreply.github.com> Date: Thu, 30 May 2019 12:03:38 -0700 Subject: [PATCH 03/65] Remove children when updating a host node to an element with nil children (#210) * Revert performance improvement This caused an issue when updating an element that had children - when an element went from having children to having nil children, the rendered objects would not be removed. * add test to verify that #209 is fixed * update changelog * Dial back bugfix. Reintroduce performance consideration, add additional check to resolve the bug. * Preempt review comments * Update src/RobloxRenderer.lua Co-Authored-By: Lucien Greathouse --- CHANGELOG.md | 4 ++++ src/RobloxRenderer.lua | 4 ++-- src/RobloxRenderer.spec.lua | 27 +++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f8518d..04061efe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Roact Changelog +## Unreleased + +* Fixed an issue where updating a host element with children to an element with `nil` children caused the old children to not be unmounted. ([#210](https://github.com/Roblox/roact/pull/210)) + ## [1.0.0](https://github.com/Roblox/roact/releases/tag/v1.0.0) This release significantly reworks Roact internals to enable new features and optimizations. diff --git a/src/RobloxRenderer.lua b/src/RobloxRenderer.lua index 5cd67cf9..4f528ad5 100644 --- a/src/RobloxRenderer.lua +++ b/src/RobloxRenderer.lua @@ -269,7 +269,7 @@ function RobloxRenderer.updateHostNode(reconciler, virtualNode, newElement) end local children = newElement.props[Children] - if children ~= nil then + if children ~= nil or oldProps[Children] ~= nil then reconciler.updateVirtualNodeWithChildren(virtualNode, virtualNode.hostObject, children) end @@ -280,4 +280,4 @@ function RobloxRenderer.updateHostNode(reconciler, virtualNode, newElement) return virtualNode end -return RobloxRenderer \ No newline at end of file +return RobloxRenderer diff --git a/src/RobloxRenderer.spec.lua b/src/RobloxRenderer.spec.lua index 2b3f36e4..1da3161c 100644 --- a/src/RobloxRenderer.spec.lua +++ b/src/RobloxRenderer.spec.lua @@ -419,6 +419,33 @@ return function() expect(message:find("RobloxRenderer%.spec")).to.be.ok() end) end) + + it("should delete instances when reconciling to nil children", function() + local parent = Instance.new("Folder") + local key = "Some Key" + + local element = createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0), + }, { + child = createElement("Frame"), + }) + + local node = reconciler.createVirtualNode(element, parent, key) + + RobloxRenderer.mountHostNode(reconciler, node) + + expect(#parent:GetChildren()).to.equal(1) + + local instance = parent:GetChildren()[1] + expect(#instance:GetChildren()).to.equal(1) + + local newElement = createElement("Frame", { + Size = UDim2.new(0.5, 0, 0.5, 0), + }) + + RobloxRenderer.updateHostNode(reconciler, node, newElement) + expect(#instance:GetChildren()).to.equal(0) + end) end) describe("unmountHostNode", function() From 7a6a2dd2321bf3ac439ebe7de709a3d19c6d9eb4 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Mon, 3 Jun 2019 11:32:16 -0700 Subject: [PATCH 04/65] Refactor bindings, add joinBindings (#208) Refactored bindings to make them adhere to a common interface instead of having an implicit kind based on combinations of variables being nil! Implemented joinBindings in this new paradigm, which became much simpler. --- CHANGELOG.md | 4 +- docs/api-reference.md | 50 ++++++++++ src/Binding.lua | 220 ++++++++++++++++++++++-------------------- src/Binding.spec.lua | 140 ++++++++++++++++++++++++++- src/init.lua | 1 + src/init.spec.lua | 1 + 6 files changed, 307 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04061efe..4bcb6755 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Roact Changelog -## Unreleased - +## Unreleased Changes * Fixed an issue where updating a host element with children to an element with `nil` children caused the old children to not be unmounted. ([#210](https://github.com/Roblox/roact/pull/210)) +* Added `Roact.joinBindings`, which allows combining multiple bindings into a single binding that can be mapped. ([#208](https://github.com/Roblox/roact/pull/208)) ## [1.0.0](https://github.com/Roblox/roact/releases/tag/v1.0.0) This release significantly reworks Roact internals to enable new features and optimizations. diff --git a/docs/api-reference.md b/docs/api-reference.md index f5ea03e0..e1488c4c 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -120,6 +120,56 @@ Returns a new binding that maps the existing binding's value to something else. --- +### Roact.joinBindings +
Unreleased API
+ +``` +Roact.joinBindings(bindings) -> Binding +where + bindings: { [any]: Binding } +``` + +Combines multiple bindings into a single binding. The new binding's value will have the same keys as the input table of bindings. + +`joinBindings` is usually used alongside `Binding:map`: + +```lua +local function Flex() + local aSize, setASize = Roact.createBinding(Vector2.new()) + local bSize, setBSize = Roact.createBinding(Vector2.new()) + + return Roact.createElement("Frame", { + Size = Roact.joinBindings({aSize, bSize}):map(function(sizes) + local sum = Vector2.new() + + for _, size in ipairs(sizes) do + sum = sum + size + end + + return UDim2.new(0, sum.X, 0, sum.Y) + end), + }, { + A = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 30), + [Roact.Change.AbsoluteSize] = function(instance) + setASize(instance.Size) + end, + }), + B = Roact.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 30), + Position = aSize:map(function(size) + return UDim2.new(0, 0, 0, size.Y) + end), + [Roact.Change.AbsoluteSize] = function(instance) + setBSize(instance.Size) + end, + }), + }) +end +``` + +--- + ### Roact.createRef ``` Roact.createRef() -> Ref diff --git a/src/Binding.lua b/src/Binding.lua index 06a9e267..b50acf61 100644 --- a/src/Binding.lua +++ b/src/Binding.lua @@ -4,146 +4,154 @@ local Type = require(script.Parent.Type) local config = require(script.Parent.GlobalConfig).get() ---[[ - Default mapping function used for non-mapped bindings -]] -local function identity(value) - return value +local BindingImpl = Symbol.named("BindingImpl") + +local BindingInternalApi = {} + +local bindingPrototype = {} + +function bindingPrototype:getValue() + return BindingInternalApi.getValue(self) end -local Binding = {} +function bindingPrototype:map(predicate) + return BindingInternalApi.map(self, predicate) +end ---[[ - Set of keys for fields that are internal to Bindings -]] -local InternalData = Symbol.named("InternalData") +local BindingPublicMeta = { + __index = bindingPrototype, + __tostring = function(self) + return string.format("RoactBinding(%s)", tostring(self:getValue())) + end, +} -local bindingPrototype = {} -bindingPrototype.__index = bindingPrototype -bindingPrototype.__tostring = function(self) - return ("RoactBinding(%s)"):format(tostring(self[InternalData].value)) +function BindingInternalApi.update(binding, newValue) + return binding[BindingImpl].update(newValue) end ---[[ - Get the current value from a binding -]] -function bindingPrototype:getValue() - local internalData = self[InternalData] +function BindingInternalApi.subscribe(binding, callback) + return binding[BindingImpl].subscribe(callback) +end - --[[ - If our source is another binding but we're not subscribed, we'll - return the mapped value from our upstream binding. +function BindingInternalApi.getValue(binding) + return binding[BindingImpl].getValue() +end - This allows us to avoid subscribing to our source until someone - has subscribed to us, and avoid creating dangling connections. - ]] - if internalData.upstreamBinding ~= nil and internalData.upstreamDisconnect == nil then - return internalData.valueTransform(internalData.upstreamBinding:getValue()) +function BindingInternalApi.create(initialValue) + local impl = { + value = initialValue, + changeSignal = createSignal(), + } + + function impl.subscribe(callback) + return impl.changeSignal:subscribe(callback) end - return internalData.value + function impl.update(newValue) + impl.value = newValue + impl.changeSignal:fire(newValue) + end + + function impl.getValue() + return impl.value + end + + return setmetatable({ + [Type] = Type.Binding, + [BindingImpl] = impl, + }, BindingPublicMeta), impl.update end ---[[ - Creates a new binding from this one with the given mapping. -]] -function bindingPrototype:map(valueTransform) +function BindingInternalApi.map(upstreamBinding, predicate) if config.typeChecks then - assert(typeof(valueTransform) == "function", "Bad arg #1 to binding:map: expected function") + assert(Type.of(upstreamBinding) == Type.Binding, "Expected arg #1 to be a binding") + assert(typeof(predicate) == "function", "Expected arg #1 to be a function") end - local binding = Binding.create(valueTransform(self:getValue())) + local impl = {} - binding[InternalData].valueTransform = valueTransform - binding[InternalData].upstreamBinding = self - - return binding -end + function impl.subscribe(callback) + return BindingInternalApi.subscribe(upstreamBinding, function(newValue) + callback(predicate(newValue)) + end) + end ---[[ - Update a binding's value. This is only accessible by Roact. -]] -function Binding.update(binding, newValue) - local internalData = binding[InternalData] + function impl.update(newValue) + error("Bindings created by Binding:map(fn) cannot be updated directly", 2) + end - newValue = internalData.valueTransform(newValue) + function impl.getValue() + return predicate(upstreamBinding:getValue()) + end - internalData.value = newValue - internalData.changeSignal:fire(newValue) + return setmetatable({ + [Type] = Type.Binding, + [BindingImpl] = impl, + }, BindingPublicMeta) end ---[[ - Subscribe to a binding's change signal. This is only accessible by Roact. -]] -function Binding.subscribe(binding, handler) - local internalData = binding[InternalData] - - --[[ - If this binding is mapped to another and does not have any subscribers, - we need to create a subscription to our source binding so that updates - get passed along to us - ]] - if internalData.upstreamBinding ~= nil and internalData.subscriberCount == 0 then - internalData.upstreamDisconnect = Binding.subscribe(internalData.upstreamBinding, function(value) - Binding.update(binding, value) - end) +function BindingInternalApi.join(upstreamBindings) + if config.typeChecks then + assert(typeof(upstreamBindings) == "table", "Expected arg #1 to be of type table") + + for key, value in pairs(upstreamBindings) do + if Type.of(value) ~= Type.Binding then + local message = ( + "Expected arg #1 to contain only bindings, but key %q had a non-binding value" + ):format( + tostring(key) + ) + error(message, 2) + end + end end - local disconnect = internalData.changeSignal:subscribe(handler) - internalData.subscriberCount = internalData.subscriberCount + 1 + local impl = {} - local disconnected = false + local function getValue() + local value = {} - --[[ - We wrap the disconnect function so that we can manage our subscriptions - when the disconnect is triggered - ]] - return function() - if disconnected then - return + for key, upstream in pairs(upstreamBindings) do + value[key] = upstream:getValue() end - disconnected = true - disconnect() - internalData.subscriberCount = internalData.subscriberCount - 1 - - --[[ - If our subscribers count drops to 0, we can safely unsubscribe from - our source binding - ]] - if internalData.subscriberCount == 0 and internalData.upstreamDisconnect ~= nil then - internalData.upstreamDisconnect() - internalData.upstreamDisconnect = nil - end + return value end -end ---[[ - Create a new binding object with the given starting value. This - function will be exposed to users of Roact. -]] -function Binding.create(initialValue) - local binding = { - [Type] = Type.Binding, + function impl.subscribe(callback) + local disconnects = {} - [InternalData] = { - value = initialValue, - changeSignal = createSignal(), - subscriberCount = 0, + for key, upstream in pairs(upstreamBindings) do + disconnects[key] = BindingInternalApi.subscribe(upstream, function(newValue) + callback(getValue()) + end) + end - valueTransform = identity, - upstreamBinding = nil, - upstreamDisconnect = nil, - }, - } + return function() + if disconnects == nil then + return + end - setmetatable(binding, bindingPrototype) + for _, disconnect in pairs(disconnects) do + disconnect() + end - local setter = function(newValue) - Binding.update(binding, newValue) + disconnects = nil + end end - return binding, setter + function impl.update(newValue) + error("Bindings created by joinBindings(...) cannot be updated directly", 2) + end + + function impl.getValue() + return getValue() + end + + return setmetatable({ + [Type] = Type.Binding, + [BindingImpl] = impl, + }, BindingPublicMeta) end -return Binding \ No newline at end of file +return BindingInternalApi \ No newline at end of file diff --git a/src/Binding.spec.lua b/src/Binding.spec.lua index d02fff84..ee0b77a5 100644 --- a/src/Binding.spec.lua +++ b/src/Binding.spec.lua @@ -118,5 +118,143 @@ return function() expect(isEvenLengthSpy.callCount).to.equal(1) expect(lengthSpy.callCount).to.equal(1) end) + + it("should throw when updated directly", function() + local source = Binding.create(1) + local mapped = source:map(function(v) + return v + end) + + expect(function() + Binding.update(mapped, 5) + end).to.throw() + end) + end) + + describe("Binding.join", function() + it("should have getValue", function() + local binding1 = Binding.create(1) + local binding2 = Binding.create(2) + local binding3 = Binding.create(3) + + local joinedBinding = Binding.join({ + binding1, + binding2, + foo = binding3, + }) + + local bindingValue = joinedBinding:getValue() + expect(bindingValue).to.be.a("table") + expect(bindingValue[1]).to.equal(1) + expect(bindingValue[2]).to.equal(2) + expect(bindingValue.foo).to.equal(3) + end) + + it("should update when any one of the subscribed bindings updates", function() + local binding1, update1 = Binding.create(1) + local binding2, update2 = Binding.create(2) + local binding3, update3 = Binding.create(3) + + local joinedBinding = Binding.join({ + binding1, + binding2, + foo = binding3, + }) + + local spy = createSpy() + Binding.subscribe(joinedBinding, spy.value) + + expect(spy.callCount).to.equal(0) + + update1(3) + expect(spy.callCount).to.equal(1) + + local args = spy:captureValues("value") + expect(args.value).to.be.a("table") + expect(args.value[1]).to.equal(3) + expect(args.value[2]).to.equal(2) + expect(args.value["foo"]).to.equal(3) + + update2(4) + expect(spy.callCount).to.equal(2) + + args = spy:captureValues("value") + expect(args.value).to.be.a("table") + expect(args.value[1]).to.equal(3) + expect(args.value[2]).to.equal(4) + expect(args.value["foo"]).to.equal(3) + + update3(8) + expect(spy.callCount).to.equal(3) + + args = spy:captureValues("value") + expect(args.value).to.be.a("table") + expect(args.value[1]).to.equal(3) + expect(args.value[2]).to.equal(4) + expect(args.value["foo"]).to.equal(8) + end) + + it("should disconnect from all upstream bindings", function() + local binding1, update1 = Binding.create(1) + local binding2, update2 = Binding.create(2) + + local joined = Binding.join({binding1, binding2}) + + local spy = createSpy() + local disconnect = Binding.subscribe(joined, spy.value) + + expect(spy.callCount).to.equal(0) + + update1(3) + expect(spy.callCount).to.equal(1) + + update2(3) + expect(spy.callCount).to.equal(2) + + disconnect() + update1(4) + expect(spy.callCount).to.equal(2) + + update2(2) + expect(spy.callCount).to.equal(2) + + local value = joined:getValue() + expect(value[1]).to.equal(4) + expect(value[2]).to.equal(2) + end) + + it("should be okay with calling disconnect multiple times", function() + local joined = Binding.join({}) + + local disconnect = Binding.subscribe(joined, function() end) + + disconnect() + disconnect() + end) + + it("should throw if updated directly", function() + local joined = Binding.join({}) + + expect(function() + Binding.update(joined, 0) + end) + end) + + it("should throw when a non-table value is passed", function() + expect(function() + Binding.join("hi") + end).to.throw() + end) + + it("should throw when a non-binding value is passed via table", function() + expect(function() + local binding = Binding.create(123) + + Binding.join({ + binding, + "abcde", + }) + end).to.throw() + end) end) -end +end \ No newline at end of file diff --git a/src/init.lua b/src/init.lua index cbaa6f0a..f002f975 100644 --- a/src/init.lua +++ b/src/init.lua @@ -22,6 +22,7 @@ local Roact = strict { Portal = require(script.Portal), createRef = require(script.createRef), createBinding = Binding.create, + joinBindings = Binding.join, Change = require(script.PropMarkers.Change), Children = require(script.PropMarkers.Children), diff --git a/src/init.spec.lua b/src/init.spec.lua index 7b23a173..7fcf79c8 100644 --- a/src/init.spec.lua +++ b/src/init.spec.lua @@ -7,6 +7,7 @@ return function() createFragment = "function", createRef = "function", createBinding = "function", + joinBindings = "function", mount = "function", unmount = "function", update = "function", From 2ce968976538b4ed03724c08a9286e4f999158d2 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Mon, 3 Jun 2019 14:52:54 -0700 Subject: [PATCH 05/65] Add rough release guide --- CONTRIBUTING.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d6cbcc8..064e555d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,4 +79,16 @@ When submitting a bug fix, create a test that verifies the broken behavior and t When submitting a new feature, add tests for all functionality. -We use [LuaCov](https://keplerproject.github.io/luacov) for keeping track of code coverage. We'd like it to be as close to 100% as possible, but it's not always possible. Adding tests just for the purpose of getting coverage isn't useful; we should strive to make only useful tests! \ No newline at end of file +We use [LuaCov](https://keplerproject.github.io/luacov) for keeping track of code coverage. We'd like it to be as close to 100% as possible, but it's not always possible. Adding tests just for the purpose of getting coverage isn't useful; we should strive to make only useful tests! + +## Release Checklist +When releasing a new version of Roact, do these things: + +1. Bump the version in `rotriever.toml` +2. Move the unreleased changes in `CHANGELOG.md` to a new heading +3. Update `docs/api-reference.md` to flag any unreleased APIs with the new version +4. Commit with a message like `Release v2.3.7` +5. Tag the commit: `git tag v2.3.7` +6. Push: `git push` +7. Push the new release tag: `git push origin v2.3.7` +8. Write a release on GitHub, copying release notes from `CHANGELOG.md` \ No newline at end of file From 4804737171fc284a77fed8f6323fbccb897fa81a Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Mon, 3 Jun 2019 14:54:37 -0700 Subject: [PATCH 06/65] Update CONTRIBUTING.md --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 064e555d..2bf6a15f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,6 +86,7 @@ When releasing a new version of Roact, do these things: 1. Bump the version in `rotriever.toml` 2. Move the unreleased changes in `CHANGELOG.md` to a new heading + - This heading should have a GitHub releases link and release date 3. Update `docs/api-reference.md` to flag any unreleased APIs with the new version 4. Commit with a message like `Release v2.3.7` 5. Tag the commit: `git tag v2.3.7` From 542a34683a27a512d4f40034fbb39ed827e0ec36 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Mon, 3 Jun 2019 14:55:09 -0700 Subject: [PATCH 07/65] Update repository documentation --- CONTRIBUTING.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2bf6a15f..8edba69e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,6 +90,6 @@ When releasing a new version of Roact, do these things: 3. Update `docs/api-reference.md` to flag any unreleased APIs with the new version 4. Commit with a message like `Release v2.3.7` 5. Tag the commit: `git tag v2.3.7` -6. Push: `git push` +6. Push commits: `git push` 7. Push the new release tag: `git push origin v2.3.7` 8. Write a release on GitHub, copying release notes from `CHANGELOG.md` \ No newline at end of file diff --git a/README.md b/README.md index 43c8b19c..94c2db2f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ * Rename the folder to `Roact` * Use a plugin like [Rojo](https://github.com/LPGhatguy/rojo) to sync the files into a place -## Usage +## [Documentation](https://roblox.github.io/roact) For a detailed guide and examples, check out [the official Roact documentation](https://roblox.github.io/roact). ```lua From 6c498e82748ebfca8df6021b7c02ed44df264930 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Mon, 3 Jun 2019 14:55:42 -0700 Subject: [PATCH 08/65] v1.1.0 --- CHANGELOG.md | 2 ++ docs/api-reference.md | 2 +- rotriever.toml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bcb6755..28c11a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Roact Changelog ## Unreleased Changes + +## [1.1.0](https://github.com/Roblox/roact/releases/tag/v1.1.0) (June 3rd, 2019) * Fixed an issue where updating a host element with children to an element with `nil` children caused the old children to not be unmounted. ([#210](https://github.com/Roblox/roact/pull/210)) * Added `Roact.joinBindings`, which allows combining multiple bindings into a single binding that can be mapped. ([#208](https://github.com/Roblox/roact/pull/208)) diff --git a/docs/api-reference.md b/docs/api-reference.md index e1488c4c..04d2f62f 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -121,7 +121,7 @@ Returns a new binding that maps the existing binding's value to something else. --- ### Roact.joinBindings -
Unreleased API
+
Added in 1.1.0
``` Roact.joinBindings(bindings) -> Binding diff --git a/rotriever.toml b/rotriever.toml index dd42da0f..f9b349ad 100644 --- a/rotriever.toml +++ b/rotriever.toml @@ -2,4 +2,4 @@ name = "Roact" author = "Roblox" license = "Apache-2.0" content_root = "src" -version = "1.0.0" \ No newline at end of file +version = "1.1.0" \ No newline at end of file From d9af05bb943a1fde818ac92e8cf0198aa7a475d1 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Mon, 3 Jun 2019 15:03:43 -0700 Subject: [PATCH 09/65] Update the release checklist --- CONTRIBUTING.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8edba69e..cd088337 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,12 +84,17 @@ We use [LuaCov](https://keplerproject.github.io/luacov) for keeping track of cod ## Release Checklist When releasing a new version of Roact, do these things: -1. Bump the version in `rotriever.toml` -2. Move the unreleased changes in `CHANGELOG.md` to a new heading - - This heading should have a GitHub releases link and release date -3. Update `docs/api-reference.md` to flag any unreleased APIs with the new version -4. Commit with a message like `Release v2.3.7` -5. Tag the commit: `git tag v2.3.7` -6. Push commits: `git push` -7. Push the new release tag: `git push origin v2.3.7` -8. Write a release on GitHub, copying release notes from `CHANGELOG.md` \ No newline at end of file +1. Bump the version in `rotriever.toml`. +2. Move the unreleased changes in `CHANGELOG.md` to a new heading. + - This heading should have a GitHub release link and release date! +3. Update `docs/api-reference.md` to flag any unreleased APIs with the new version. +5. Commit with Git: + - Commit: `git commit -m "Release v2.3.7"` + - Tag the commit: `git tag v2.3.7` + - Push commits: `git push` + - Push the tag: `git push origin v2.3.7` +6. Build a binary with Rojo: `rojo build -o Roact.rbxm` +7. Write a release on GitHub: + - Use the same format as the previous release + - Copy the release notes from `CHANGELOG.md` + - Attach the `Roact.rbxm` built with Rojo \ No newline at end of file From b1db3f82a2510c4e5e5b2bdc2c47298ea02dabd8 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Thu, 6 Jun 2019 13:20:53 -0700 Subject: [PATCH 10/65] Clean up documentation around RoactTree --- docs/api-reference.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 04d2f62f..a51216ca 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -32,7 +32,7 @@ Creates a new Roact fragment with the provided table of elements. Fragments allo ### Roact.mount ``` -Roact.mount(element, [parent, [key]]) -> ComponentInstanceHandle +Roact.mount(element, [parent, [key]]) -> RoactTree ``` !!! info @@ -40,13 +40,13 @@ Roact.mount(element, [parent, [key]]) -> ComponentInstanceHandle Creates a Roblox Instance given a Roact element, and optionally a `parent` to put it in, and a `key` to use as the instance's `Name`. -The result is a `ComponentInstanceHandle`, which is an opaque handle that represents this specific instance of the root component. You can pass this to APIs like `Roact.unmount` and the future debug API. +The result is a `RoactTree`, which is an opaque handle that represents a tree of components owned by Roact. You can pass this to APIs like `Roact.unmount`. It'll also be used for future debugging APIs. --- ### Roact.update ``` -Roact.update(instanceHandle, element) -> ComponentInstanceHandle +Roact.update(tree, element) -> RoactTree ``` !!! info @@ -56,22 +56,19 @@ Updates an existing instance handle with a new element, returning a new handle. `update` can be used to change the props of a component instance created with `mount` and is useful for putting Roact content into non-Roact applications. -!!! warning - `Roact.update` takes ownership of the `instanceHandle` passed into it and may unmount it and mount a new tree! - - Make sure to use the handle that `update` returns in any operations after `update`, including `unmount`. +As of Roact 1.0, the returned `RoactTree` object will always be the same value as the one passed in. --- ### Roact.unmount ``` -Roact.unmount(instance) -> void +Roact.unmount(tree) -> void ``` !!! info `Roact.unmount` is also available via the deprecated alias `Roact.teardown`. It will be removed in a future release. -Destroys the given `ComponentInstanceHandle` and all of its descendants. Does not operate on a Roblox Instance -- this must be given a handle that was returned by `Roact.mount`. +Destroys the given `RoactTree` and all of its descendants. Does not operate on a Roblox Instance -- this must be given a handle that was returned by `Roact.mount`. --- From 693e64e01b541f8080485033395c777e99523fd0 Mon Sep 17 00:00:00 2001 From: jeparlefrancais <35781636+jeparlefrancais@users.noreply.github.com> Date: Thu, 20 Jun 2019 13:39:06 -0700 Subject: [PATCH 11/65] Make Fragment an Element (#214) * Make Fragment an Element Fragments can not be used directly as children of a component: local function component() return Roact.createElement("Frame", {}, { fragments = Roact.createFragment({ A = Roact.createElement("Frame", {}, {}) }) }) end Turning Fragments into Elements make them have their own virtual node, so they can be reconciled as any other nodes. * Add fragment tests * Add empty fragment tests * Remove unused spy * Make test assertion clearer * Update changelog * Update CHANGELOG.md Co-Authored-By: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> * Update fragments tests --- CHANGELOG.md | 3 +- src/ElementKind.lua | 1 + src/ElementUtils.lua | 8 --- src/ElementUtils.spec.lua | 31 --------- src/RobloxRenderer.spec.lua | 119 ++++++++++++++++++++++++++++++++++ src/Type.lua | 1 - src/createFragment.lua | 4 +- src/createFragment.spec.lua | 21 ++++++ src/createReconciler.lua | 21 ++++++ src/createReconciler.spec.lua | 39 ++++++++++- 10 files changed, 205 insertions(+), 43 deletions(-) create mode 100644 src/createFragment.spec.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 28c11a23..1a80036e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Roact Changelog ## Unreleased Changes +* Fixed a bug where fragments could not be used as children of an element or another fragment. ([#214](https://github.com/Roblox/roact/pull/214)) ## [1.1.0](https://github.com/Roblox/roact/releases/tag/v1.1.0) (June 3rd, 2019) * Fixed an issue where updating a host element with children to an element with `nil` children caused the old children to not be unmounted. ([#210](https://github.com/Roblox/roact/pull/210)) @@ -52,4 +53,4 @@ This release significantly reworks Roact internals to enable new features and op * Got rid of installer scripts in favor of regular model files ## December 1, 2017 Prerelease -* Initial pre-release build \ No newline at end of file +* Initial pre-release build diff --git a/src/ElementKind.lua b/src/ElementKind.lua index d33f2194..22e1e539 100644 --- a/src/ElementKind.lua +++ b/src/ElementKind.lua @@ -19,6 +19,7 @@ local ElementKindInternal = { Host = Symbol.named("Host"), Function = Symbol.named("Function"), Stateful = Symbol.named("Stateful"), + Fragment = Symbol.named("Fragment"), } function ElementKindInternal.of(value) diff --git a/src/ElementUtils.lua b/src/ElementUtils.lua index 44e50abd..971b6b19 100644 --- a/src/ElementUtils.lua +++ b/src/ElementUtils.lua @@ -40,10 +40,6 @@ ElementUtils.UseParentKey = Symbol.named("UseParentKey") function ElementUtils.iterateElements(elementOrElements) local richType = Type.of(elementOrElements) - if richType == Type.Fragment then - return pairs(elementOrElements.elements) - end - -- Single child if richType == Type.Element then local called = false @@ -93,10 +89,6 @@ function ElementUtils.getElementByKey(elements, hostKey) return nil end - if Type.of(elements) == Type.Fragment then - return elements.elements[hostKey] - end - if typeof(elements) == "table" then return elements[hostKey] end diff --git a/src/ElementUtils.spec.lua b/src/ElementUtils.spec.lua index e35438f1..3457abb6 100644 --- a/src/ElementUtils.spec.lua +++ b/src/ElementUtils.spec.lua @@ -17,27 +17,6 @@ return function() expect(iteratedKey).to.equal(nil) end) - it("should iterate over fragments", function() - local children = createFragment({ - a = createElement("TextLabel"), - b = createElement("TextLabel"), - }) - - local seenChildren = {} - local count = 0 - - for key, child in ElementUtils.iterateElements(children) do - expect(typeof(key)).to.equal("string") - expect(Type.of(child)).to.equal(Type.Element) - seenChildren[child] = key - count = count + 1 - end - - expect(count).to.equal(2) - expect(seenChildren[children.elements.a]).to.equal("a") - expect(seenChildren[children.elements.b]).to.equal("b") - end) - it("should iterate over tables", function() local children = { a = createElement("TextLabel"), @@ -97,16 +76,6 @@ return function() end) end) - it("should return the corresponding element from a fragment", function() - local children = createFragment({ - a = createElement("TextLabel"), - b = createElement("TextLabel"), - }) - - expect(ElementUtils.getElementByKey(children, "a")).to.equal(children.elements.a) - expect(ElementUtils.getElementByKey(children, "b")).to.equal(children.elements.b) - end) - it("should return the corresponding element from a table", function() local children = { a = createElement("TextLabel"), diff --git a/src/RobloxRenderer.spec.lua b/src/RobloxRenderer.spec.lua index 1da3161c..d31421c9 100644 --- a/src/RobloxRenderer.spec.lua +++ b/src/RobloxRenderer.spec.lua @@ -4,6 +4,7 @@ return function() local Children = require(script.Parent.PropMarkers.Children) local Component = require(script.Parent.Component) local createElement = require(script.Parent.createElement) + local createFragment = require(script.Parent.createFragment) local createReconciler = require(script.Parent.createReconciler) local createRef = require(script.Parent.createRef) local createSpy = require(script.Parent.createSpy) @@ -686,6 +687,124 @@ return function() end) end) + describe("Fragments", function() + it("should parent the fragment's elements into the fragment's parent", function() + local hostParent = Instance.new("Folder") + + local fragment = createFragment({ + key = createElement("IntValue", { + Value = 1, + }), + key2 = createElement("IntValue", { + Value = 2, + }), + }) + + local node = reconciler.mountVirtualNode(fragment, hostParent, "test") + + expect(hostParent:FindFirstChild("key")).to.be.ok() + expect(hostParent.key.ClassName).to.equal("IntValue") + expect(hostParent.key.Value).to.equal(1) + + expect(hostParent:FindFirstChild("key2")).to.be.ok() + expect(hostParent.key2.ClassName).to.equal("IntValue") + expect(hostParent.key2.Value).to.equal(2) + + reconciler.unmountVirtualNode(node) + + expect(#hostParent:GetChildren()).to.equal(0) + end) + + it("should allow sibling fragment to have common keys", function() + local hostParent = Instance.new("Folder") + local hostKey = "Test" + + local function parent(props) + return createElement("IntValue", {}, { + fragmentA = createFragment({ + key = createElement("StringValue", { + Value = "A", + }), + key2 = createElement("StringValue", { + Value = "B", + }), + }), + fragmentB = createFragment({ + key = createElement("StringValue", { + Value = "C", + }), + key2 = createElement("StringValue", { + Value = "D", + }), + }), + }) + end + + local node = reconciler.mountVirtualNode(createElement(parent), hostParent, hostKey) + local parentChildren = hostParent[hostKey]:GetChildren() + + expect(#parentChildren).to.equal(4) + + local childValues = {} + + for _, child in pairs(parentChildren) do + expect(child.ClassName).to.equal("StringValue") + childValues[child.Value] = 1 + (childValues[child.Value] or 0) + end + + -- check if the StringValues have not collided + expect(childValues.A).to.equal(1) + expect(childValues.B).to.equal(1) + expect(childValues.C).to.equal(1) + expect(childValues.D).to.equal(1) + + reconciler.unmountVirtualNode(node) + + expect(#hostParent:GetChildren()).to.equal(0) + end) + + it("should render nested fragments", function() + local hostParent = Instance.new("Folder") + + local fragment = createFragment({ + key = createFragment({ + TheValue = createElement("IntValue", { + Value = 1, + }), + TheOtherValue = createElement("IntValue", { + Value = 2, + }) + }) + }) + + local node = reconciler.mountVirtualNode(fragment, hostParent, "Test") + + expect(hostParent:FindFirstChild("TheValue")).to.be.ok() + expect(hostParent.TheValue.ClassName).to.equal("IntValue") + expect(hostParent.TheValue.Value).to.equal(1) + + expect(hostParent:FindFirstChild("TheOtherValue")).to.be.ok() + expect(hostParent.TheOtherValue.ClassName).to.equal("IntValue") + expect(hostParent.TheOtherValue.Value).to.equal(2) + + reconciler.unmountVirtualNode(node) + + expect(#hostParent:GetChildren()).to.equal(0) + end) + + it("should not add any instances if the fragment is empty", function() + local hostParent = Instance.new("Folder") + + local node = reconciler.mountVirtualNode(createFragment({}), hostParent, "test") + + expect(#hostParent:GetChildren()).to.equal(0) + + reconciler.unmountVirtualNode(node) + + expect(#hostParent:GetChildren()).to.equal(0) + end) + end) + describe("Context", function() it("should pass context values through Roblox host nodes", function() local Consumer = Component:extend("Consumer") diff --git a/src/Type.lua b/src/Type.lua index 55acd213..156ee0ea 100644 --- a/src/Type.lua +++ b/src/Type.lua @@ -22,7 +22,6 @@ end addType("Binding") addType("Element") -addType("Fragment") addType("HostChangeEvent") addType("HostEvent") addType("StatefulComponentClass") diff --git a/src/createFragment.lua b/src/createFragment.lua index 3d0ceb0c..91554f39 100644 --- a/src/createFragment.lua +++ b/src/createFragment.lua @@ -1,8 +1,10 @@ +local ElementKind = require(script.Parent.ElementKind) local Type = require(script.Parent.Type) local function createFragment(elements) return { - [Type] = Type.Fragment, + [Type] = Type.Element, + [ElementKind] = ElementKind.Fragment, elements = elements, } end diff --git a/src/createFragment.spec.lua b/src/createFragment.spec.lua new file mode 100644 index 00000000..45de6c71 --- /dev/null +++ b/src/createFragment.spec.lua @@ -0,0 +1,21 @@ +return function() + local ElementKind = require(script.Parent.ElementKind) + local Type = require(script.Parent.Type) + + local createFragment = require(script.Parent.createFragment) + + it("should create new primitive elements", function() + local fragment = createFragment({}) + + expect(fragment).to.be.ok() + expect(Type.of(fragment)).to.equal(Type.Element) + expect(ElementKind.of(fragment)).to.equal(ElementKind.Fragment) + end) + + it("should accept children", function() + local subFragment = createFragment({}) + local fragment = createFragment({key = subFragment}) + + expect(fragment.elements.key).to.equal(subFragment) + end) +end \ No newline at end of file diff --git a/src/createReconciler.lua b/src/createReconciler.lua index c6a07798..bcab1ba2 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -138,6 +138,10 @@ local function createReconciler(renderer) for _, childNode in pairs(virtualNode.children) do unmountVirtualNode(childNode) end + elseif kind == ElementKind.Fragment then + for _, childNode in pairs(virtualNode.children) do + unmountVirtualNode(childNode) + end else error(("Unknown ElementKind %q"):format(tostring(kind), 2)) end @@ -170,6 +174,12 @@ local function createReconciler(renderer) return virtualNode end + local function updateFragmentVirtualNode(virtualNode, newElement) + updateVirtualNodeWithChildren(virtualNode, virtualNode.hostParent, newElement.elements) + + return virtualNode + end + --[[ Update the given virtual node using a new element describing what it should transform into. @@ -219,6 +229,8 @@ local function createReconciler(renderer) shouldContinueUpdate = virtualNode.instance:__update(newElement, newState) elseif kind == ElementKind.Portal then virtualNode = updatePortalVirtualNode(virtualNode, newElement) + elseif kind == ElementKind.Fragment then + virtualNode = updateFragmentVirtualNode(virtualNode, newElement) else error(("Unknown ElementKind %q"):format(tostring(kind), 2)) end @@ -283,6 +295,13 @@ local function createReconciler(renderer) updateVirtualNodeWithChildren(virtualNode, targetHostParent, children) end + local function mountFragmentVirtualNode(virtualNode) + local element = virtualNode.currentElement + local children = element.elements + + updateVirtualNodeWithChildren(virtualNode, virtualNode.hostParent, children) + end + --[[ Constructs a new virtual node and mounts it, but does not place it into the tree. @@ -317,6 +336,8 @@ local function createReconciler(renderer) element.component:__mount(reconciler, virtualNode) elseif kind == ElementKind.Portal then mountPortalVirtualNode(virtualNode) + elseif kind == ElementKind.Fragment then + mountFragmentVirtualNode(virtualNode) else error(("Unknown ElementKind %q"):format(tostring(kind), 2)) end diff --git a/src/createReconciler.spec.lua b/src/createReconciler.spec.lua index 42fa3cea..193dd256 100644 --- a/src/createReconciler.spec.lua +++ b/src/createReconciler.spec.lua @@ -5,6 +5,7 @@ return function() local createSpy = require(script.Parent.createSpy) local NoopRenderer = require(script.Parent.NoopRenderer) local Type = require(script.Parent.Type) + local ElementKind = require(script.Parent.ElementKind) local createReconciler = require(script.Parent.createReconciler) @@ -47,7 +48,7 @@ return function() end) end) - describe("elements and fragments", function() + describe("invalid elements", function() it("should throw errors when attempting to mount invalid elements", function() -- These function components return values with incorrect types local returnsString = function() @@ -286,4 +287,40 @@ return function() expect(childBComponentSpy.callCount).to.equal(1) end) end) + + describe("Fragments", function() + it("should mount fragments", function() + local fragment = createFragment({}) + local node = noopReconciler.mountVirtualNode(fragment, nil, "test") + + expect(node).to.be.ok() + expect(ElementKind.of(node.currentElement)).to.equal(ElementKind.Fragment) + end) + + it("should mount an empty fragment", function() + local emptyFragment = createFragment({}) + local node = noopReconciler.mountVirtualNode(emptyFragment, nil, "test") + + expect(node).to.be.ok() + expect(next(node.children)).to.never.be.ok() + end) + + it("should mount all fragment's children", function() + local childComponentSpy = createSpy(function(props) + return nil + end) + local elements = {} + local totalElements = 5 + + for i=1, totalElements do + elements["key"..tostring(i)] = createElement(childComponentSpy.value, {}) + end + + local fragments = createFragment(elements) + local node = noopReconciler.mountVirtualNode(fragments, nil, "test") + + expect(node).to.be.ok() + expect(childComponentSpy.callCount).to.equal(totalElements) + end) + end) end \ No newline at end of file From 590068ce2e41f3c2248ace859f26f07123d20b7e Mon Sep 17 00:00:00 2001 From: Michael Mc Donnell Date: Fri, 21 Jun 2019 14:14:55 -0700 Subject: [PATCH 12/65] Add return in reduce-conciliation.md (#221) Sample code fix in documentation --- docs/performance/reduce-reconciliation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/performance/reduce-reconciliation.md b/docs/performance/reduce-reconciliation.md index 817c4d23..9c89ae3f 100644 --- a/docs/performance/reduce-reconciliation.md +++ b/docs/performance/reduce-reconciliation.md @@ -30,7 +30,7 @@ function Item:render() local layoutOrder = self.props.layoutOrder -- Create a list item with the item's icon and name - Roact.createElement("ImageLabel", { + return Roact.createElement("ImageLabel", { LayoutOrder = layoutOrder, Image = icon, }) @@ -135,4 +135,4 @@ end Now the list of children is keyed by the stable, unique id of the item data. Their positions can change according to their LayoutOrder, but no other properties on the item need to be updated. When we add the third element to the list, Roact will set the `LayoutOrder` property on for each `ImageLabel` and only set the `Image` property on the newly added one! !!! info - Switching to static keys might seem insignificant for *this* example, but if our `Item` component becomes more complicated and our inventory gets bigger, it can make a significant difference! \ No newline at end of file + Switching to static keys might seem insignificant for *this* example, but if our `Item` component becomes more complicated and our inventory gets bigger, it can make a significant difference! From ec68e60ee7f20861cb552854292951017c1cd603 Mon Sep 17 00:00:00 2001 From: jeparlefrancais <35781636+jeparlefrancais@users.noreply.github.com> Date: Fri, 28 Jun 2019 00:47:21 -0400 Subject: [PATCH 13/65] Reorder render result checks and fix bug (#222) * Reorder render result checks and fix bug Since Fragment is an ElementKind and no longer a Type the check should have been removed. * Remove comment about the issue --- src/createReconciler.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/createReconciler.lua b/src/createReconciler.lua index bcab1ba2..fbae970d 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -101,11 +101,9 @@ local function createReconciler(renderer) end local function updateVirtualNodeWithRenderResult(virtualNode, hostParent, renderResult) - -- TODO: Consider reordering checks (https://github.com/Roblox/roact/issues/200) - if renderResult == nil + if Type.of(renderResult) == Type.Element + or renderResult == nil or typeof(renderResult) == "boolean" - or Type.of(renderResult) == Type.Element - or Type.of(renderResult) == Type.Fragment then updateChildren(virtualNode, hostParent, renderResult) else From 48f3a29d44cda6a08e44fd4c80fe2126fb8ee85f Mon Sep 17 00:00:00 2001 From: howmanysmall Date: Wed, 10 Jul 2019 15:10:17 -0600 Subject: [PATCH 14/65] Added trailing commas where missing. (#225) * Update Change.lua * Update Event.lua * Update init.lua * Update init.lua * Update init.lua * Update init.lua --- examples/changed-signal/init.lua | 12 ++++++------ examples/event/init.lua | 4 ++-- examples/ref/init.lua | 6 +++--- src/PropMarkers/Change.lua | 4 ++-- src/PropMarkers/Event.lua | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/changed-signal/init.lua b/examples/changed-signal/init.lua index 1011ca91..e947f256 100644 --- a/examples/changed-signal/init.lua +++ b/examples/changed-signal/init.lua @@ -11,11 +11,11 @@ return function() local onTextChanged = props.onTextChanged local layoutOrder = props.layoutOrder - return Roact.createElement("TextBox",{ + return Roact.createElement("TextBox", { LayoutOrder = layoutOrder, Text = "Type Here!", Size = UDim2.new(1, 0, 0.5, 0), - [Roact.Change.Text] = onTextChanged + [Roact.Change.Text] = onTextChanged, }) end @@ -26,7 +26,7 @@ return function() local inputText = props.inputText local layoutOrder = props.layoutOrder - return Roact.createElement("TextLabel",{ + return Roact.createElement("TextLabel", { LayoutOrder = layoutOrder, Size = UDim2.new(1, 0, 0.5, 0), Text = "Reversed: " .. inputText:reverse(), @@ -62,12 +62,12 @@ return function() self:setState({ text = rbx.Text or "", }) - end + end, }), ReversedText = Roact.createElement(ReversedText, { layoutOrder = 2, inputText = text, - }) + }), }) end @@ -82,4 +82,4 @@ return function() end return stop -end \ No newline at end of file +end diff --git a/examples/event/init.lua b/examples/event/init.lua index ac7b0ccc..598eb452 100644 --- a/examples/event/init.lua +++ b/examples/event/init.lua @@ -14,7 +14,7 @@ return function() -- followed by their normal event arguments. [Roact.Event.Activated] = function(rbx) print("The button was clicked!") - end + end, }), }) @@ -25,4 +25,4 @@ return function() end return stop -end \ No newline at end of file +end diff --git a/examples/ref/init.lua b/examples/ref/init.lua index 5193fbef..73dda14f 100644 --- a/examples/ref/init.lua +++ b/examples/ref/init.lua @@ -28,7 +28,7 @@ return function() [Roact.Event.Activated] = function() print("Button clicked; have the TextBox capture focus") self.textBoxRef:getValue():CaptureFocus() - end + end, }), SearchTextBox = Roact.createElement("TextBox", { @@ -36,7 +36,7 @@ return function() Position = UDim2.new(0, 50, 0, 0), -- Use Roact.Ref to get a reference to the underlying object - [Roact.Ref] = self.textBoxRef + [Roact.Ref] = self.textBoxRef, }), }) end @@ -52,4 +52,4 @@ return function() end return stop -end \ No newline at end of file +end diff --git a/src/PropMarkers/Change.lua b/src/PropMarkers/Change.lua index fdeae222..2a20adbf 100644 --- a/src/PropMarkers/Change.lua +++ b/src/PropMarkers/Change.lua @@ -25,7 +25,7 @@ setmetatable(Change, { __index = function(self, propertyName) local changeListener = { [Type] = Type.HostChangeEvent, - name = propertyName + name = propertyName, } setmetatable(changeListener, changeMetatable) @@ -35,4 +35,4 @@ setmetatable(Change, { end, }) -return Change \ No newline at end of file +return Change diff --git a/src/PropMarkers/Event.lua b/src/PropMarkers/Event.lua index 4ccacbc4..f9aba02b 100644 --- a/src/PropMarkers/Event.lua +++ b/src/PropMarkers/Event.lua @@ -35,7 +35,7 @@ setmetatable(Event, { Event[eventName] = event return event - end + end, }) -return Event \ No newline at end of file +return Event From 9493406f0a4892b55dff98386c1d891458f5975d Mon Sep 17 00:00:00 2001 From: jeparlefrancais <35781636+jeparlefrancais@users.noreply.github.com> Date: Thu, 25 Jul 2019 14:06:51 -0400 Subject: [PATCH 15/65] Better error message for invalid changed hook name (#216) * Improve error message for invalid changed hooks * Convert hook key to string to prevent possible bug * Add test case to verify that error is thrown * Change error message wording Co-Authored-By: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> * Reduce line length * Remove new line * Append original error message to assertion * Add changelog entry --- CHANGELOG.md | 1 + src/SingleEventManager.lua | 12 +++++++++++- src/SingleEventManager.spec.lua | 9 +++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a80036e..1b343718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Roact Changelog ## Unreleased Changes +* Improved the error message when an invalid changed hook name is used. ([#216](https://github.com/Roblox/roact/pull/216)) * Fixed a bug where fragments could not be used as children of an element or another fragment. ([#214](https://github.com/Roblox/roact/pull/214)) ## [1.1.0](https://github.com/Roblox/roact/releases/tag/v1.1.0) (June 3rd, 2019) diff --git a/src/SingleEventManager.lua b/src/SingleEventManager.lua index 0d2b3b82..bb579c78 100644 --- a/src/SingleEventManager.lua +++ b/src/SingleEventManager.lua @@ -53,7 +53,17 @@ function SingleEventManager:connectEvent(key, listener) end function SingleEventManager:connectPropertyChange(key, listener) - local event = self._instance:GetPropertyChangedSignal(key) + local success, event = pcall(function() + return self._instance:GetPropertyChangedSignal(key) + end) + + if not success then + error(("Cannot get changed signal on property %q: %s"):format( + tostring(key), + event + ), 0) + end + self:_connect(CHANGE_PREFIX .. key, event, listener) end diff --git a/src/SingleEventManager.spec.lua b/src/SingleEventManager.spec.lua index 41ebc262..9d87e271 100644 --- a/src/SingleEventManager.spec.lua +++ b/src/SingleEventManager.spec.lua @@ -226,5 +226,14 @@ return function() instance.Name = "baz" expect(eventSpy.callCount).to.equal(2) end) + + it("should throw an error if the property is invalid", function() + local instance = Instance.new("Folder") + local manager = SingleEventManager.new(instance) + + expect(function() + manager:connectPropertyChange("foo", function() end) + end).to.throw() + end) end) end \ No newline at end of file From be0f9ef1319f9455ef521b27556b3166c76b575a Mon Sep 17 00:00:00 2001 From: John Bacon Date: Thu, 1 Aug 2019 11:05:11 -0700 Subject: [PATCH 16/65] fix benchmarks - Habitat:loadFromFs does not know to create a module script when loading a folder with an init.server.lua in it - Each benchmark assumed it lived relative to Roact which is not necessarily true --- benchmarks/hello.bench.lua | 3 ++- benchmarks/update.bench.lua | 3 ++- bin/bench.lua | 10 +++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/benchmarks/hello.bench.lua b/benchmarks/hello.bench.lua index eec1ac1e..7b8766d9 100644 --- a/benchmarks/hello.bench.lua +++ b/benchmarks/hello.bench.lua @@ -1,4 +1,5 @@ -local Roact = require(script.Parent.Parent.Roact) +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Roact = require(ReplicatedStorage.Roact) return { iterations = 100000, diff --git a/benchmarks/update.bench.lua b/benchmarks/update.bench.lua index 5937c744..b450be4a 100644 --- a/benchmarks/update.bench.lua +++ b/benchmarks/update.bench.lua @@ -1,4 +1,5 @@ -local Roact = require(script.Parent.Parent.Roact) +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Roact = require(ReplicatedStorage.Roact) local tree diff --git a/bin/bench.lua b/bin/bench.lua index 4dcbecfb..7943ba8e 100644 --- a/bin/bench.lua +++ b/bin/bench.lua @@ -26,8 +26,12 @@ for _, module in ipairs(LOAD_MODULES) do container.Parent = root end +local runBenchMarks = habitat:loadFromFs("benchmarks/init.server.lua") +runBenchMarks.Name = "RoactBenchmark" +runBenchMarks.Parent = root + local benchmarks = habitat:loadFromFs("benchmarks") -benchmarks.Name = "Benchmark" -benchmarks.Parent = root +benchmarks.Name = "Benchmarks" +benchmarks.Parent = runBenchMarks -habitat:require(benchmarks) \ No newline at end of file +habitat:require(runBenchMarks) \ No newline at end of file From dad8c75b8cfc2c793f659542f3d0088e9f50b259 Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Fri, 9 Aug 2019 13:56:30 -0700 Subject: [PATCH 17/65] Fixes test that depends on specific config values to use the feature (#229) --- src/Binding.spec.lua | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/Binding.spec.lua b/src/Binding.spec.lua index ee0b77a5..f4fd03e9 100644 --- a/src/Binding.spec.lua +++ b/src/Binding.spec.lua @@ -1,6 +1,7 @@ return function() local createSpy = require(script.Parent.createSpy) local Type = require(script.Parent.Type) + local GlobalConfig = require(script.Parent.GlobalConfig) local Binding = require(script.Parent.Binding) @@ -241,20 +242,28 @@ return function() end) it("should throw when a non-table value is passed", function() - expect(function() - Binding.join("hi") - end).to.throw() + GlobalConfig.scoped({ + typeChecks = true, + }, function() + expect(function() + Binding.join("hi") + end).to.throw() + end) end) it("should throw when a non-binding value is passed via table", function() - expect(function() - local binding = Binding.create(123) - - Binding.join({ - binding, - "abcde", - }) - end).to.throw() + GlobalConfig.scoped({ + typeChecks = true, + }, function() + expect(function() + local binding = Binding.create(123) + + Binding.join({ + binding, + "abcde", + }) + end).to.throw() + end) end) end) end \ No newline at end of file From e8b473a3720a22c306902c6c3fbf3d51cfc8c5d1 Mon Sep 17 00:00:00 2001 From: John Bacon Date: Mon, 12 Aug 2019 09:10:30 -0700 Subject: [PATCH 18/65] Move benchmarks into a separate place file for studio users --- benchmarks.project.json | 36 ++++++++++++++++++++++++++++++++++++ place.project.json | 3 --- 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 benchmarks.project.json diff --git a/benchmarks.project.json b/benchmarks.project.json new file mode 100644 index 00000000..7a2d93b9 --- /dev/null +++ b/benchmarks.project.json @@ -0,0 +1,36 @@ +{ + "name": "Roact Benchmarks Place", + "tree": { + "$className": "DataModel", + + "ReplicatedStorage": { + "$className": "ReplicatedStorage", + + "Roact": { + "$path": "src" + } + }, + + "ServerScriptService": { + "$className": "ServerScriptService", + + "RoactBenchmark": { + "$path": "benchmarks" + } + }, + + "HttpService": { + "$className": "HttpService", + "$properties": { + "HttpEnabled": true + } + }, + + "Players": { + "$className": "Players", + "$properties": { + "CharacterAutoLoads": false + } + } + } +} \ No newline at end of file diff --git a/place.project.json b/place.project.json index eaec7943..7807792f 100644 --- a/place.project.json +++ b/place.project.json @@ -30,9 +30,6 @@ "ServerScriptService": { "$className": "ServerScriptService", - "RoactBenchmark": { - "$path": "benchmarks" - }, "RoactTests": { "$path": "bin/run-tests.server.lua" } From d316d03e5740963d939c604e46b310f96459e4af Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 13 Aug 2019 13:02:11 -0700 Subject: [PATCH 19/65] Add TweenInfo to .luacheckrc --- .luacheckrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.luacheckrc b/.luacheckrc index 582953d5..662f2c8c 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -18,6 +18,7 @@ stds.roblox = { "CFrame", "Enum", "Instance", + "TweenInfo", } } From 95a6c9dc8fe455fbcec696f038b195335d5f5e2c Mon Sep 17 00:00:00 2001 From: Olivier Trepanier Date: Tue, 13 Aug 2019 13:03:15 -0700 Subject: [PATCH 20/65] Fix space indentation to tabs --- .luacheckrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.luacheckrc b/.luacheckrc index 662f2c8c..cfd0d448 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -18,7 +18,7 @@ stds.roblox = { "CFrame", "Enum", "Instance", - "TweenInfo", + "TweenInfo", } } From 986c1d583bf378cf9cd9bd20f3fedc74f20ec3a4 Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Wed, 14 Aug 2019 09:39:08 -0700 Subject: [PATCH 21/65] Fix inconsistent behavior between setState in init and directly assigning to state (#232) --- CHANGELOG.md | 1 + src/Component.lua | 1 + .../getDerivedStateFromProps.spec.lua | 52 ++++++++++++++++++- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b343718..cfb6458c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Roact Changelog ## Unreleased Changes +* Fixed a bug where derived state was lost when assigning directly to state in init ([#232](https://github.com/Roblox/roact/pull/232/)) * Improved the error message when an invalid changed hook name is used. ([#216](https://github.com/Roblox/roact/pull/216)) * Fixed a bug where fragments could not be used as children of an element or another fragment. ([#214](https://github.com/Roblox/roact/pull/214)) diff --git a/src/Component.lua b/src/Component.lua index de4d3169..5782af29 100644 --- a/src/Component.lua +++ b/src/Component.lua @@ -280,6 +280,7 @@ function Component:__mount(reconciler, virtualNode) if instance.init ~= nil then instance:init(instance.props) + assign(instance.state, instance:__getDerivedState(instance.props, instance.state)) end -- It's possible for init() to redefine _context! diff --git a/src/Component.spec/getDerivedStateFromProps.spec.lua b/src/Component.spec/getDerivedStateFromProps.spec.lua index c34169bc..1f04cc8d 100644 --- a/src/Component.spec/getDerivedStateFromProps.spec.lua +++ b/src/Component.spec/getDerivedStateFromProps.spec.lua @@ -91,7 +91,12 @@ return function() someState = 2, }) - expect(getDerivedSpy.callCount).to.equal(3) + -- getDerivedStateFromProps will be called: + -- * Once on empty props + -- * Once during the self:setState in init + -- * Once more, defensively, on the resulting state AFTER init + -- * On updating with new state via updateVirtualNode + expect(getDerivedSpy.callCount).to.equal(4) local values = getDerivedSpy:captureValues("props", "state") @@ -123,7 +128,11 @@ return function() noopReconciler.mountVirtualNode(element, hostParent, hostKey) - expect(getDerivedSpy.callCount).to.equal(2) + -- getDerivedStateFromProps will be called: + -- * Once on empty props + -- * Once during the self:setState in init + -- * Once more, defensively, on the resulting state AFTER init + expect(getDerivedSpy.callCount).to.equal(3) local values = getDerivedSpy:captureValues("props", "state") @@ -228,4 +237,43 @@ return function() -- getDerivedStateFromProps is always called on initial state expect(stateDerivedSpy.callCount).to.equal(3) end) + + it("should have derived state after assigning to state in init", function() + local getStateCallback + local getDerivedSpy = createSpy(function() + return { + derived = true, + } + end) + local WithDerivedState = Component:extend("WithDerivedState") + + WithDerivedState.getDerivedStateFromProps = getDerivedSpy.value + + function WithDerivedState:init() + self.state = { + init = true, + } + + getStateCallback = function() + return self.state + end + end + + function WithDerivedState:render() + return nil + end + + local hostParent = nil + local hostKey = "WithDerivedState" + local element = createElement(WithDerivedState) + + noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + expect(getDerivedSpy.callCount).to.equal(2) + + assertDeepEqual(getStateCallback(), { + init = true, + derived = true, + }) + end) end \ No newline at end of file From 3de41375019750847c986183dc10e6d6dd033300 Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Fri, 6 Sep 2019 11:29:11 -0700 Subject: [PATCH 22/65] Update rotriever.toml to newest format (#234) * Update rotriever.toml to newest format * Update changelog --- CHANGELOG.md | 2 ++ rotriever.toml | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfb6458c..76191aaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Roact Changelog ## Unreleased Changes + +## [1.2.0](https://github.com/Roblox/roact/releases/tag/v1.2.0) (September 6th, 2019) * Fixed a bug where derived state was lost when assigning directly to state in init ([#232](https://github.com/Roblox/roact/pull/232/)) * Improved the error message when an invalid changed hook name is used. ([#216](https://github.com/Roblox/roact/pull/216)) * Fixed a bug where fragments could not be used as children of an element or another fragment. ([#214](https://github.com/Roblox/roact/pull/214)) diff --git a/rotriever.toml b/rotriever.toml index f9b349ad..058e40e9 100644 --- a/rotriever.toml +++ b/rotriever.toml @@ -1,5 +1,6 @@ -name = "Roact" +[package] +name = "roblox/roact" author = "Roblox" license = "Apache-2.0" content_root = "src" -version = "1.1.0" \ No newline at end of file +version = "1.2.0" \ No newline at end of file From deaa8000220252f6edc27ec15e5704266560ebe5 Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Tue, 24 Sep 2019 16:27:57 -0700 Subject: [PATCH 23/65] Fix some random CRLF line endings (#239) --- src/assertDeepEqual.spec.lua | 196 +++++++++++++++++------------------ src/createSignal.lua | 148 +++++++++++++------------- 2 files changed, 172 insertions(+), 172 deletions(-) diff --git a/src/assertDeepEqual.spec.lua b/src/assertDeepEqual.spec.lua index 484eeffd..bece8d73 100644 --- a/src/assertDeepEqual.spec.lua +++ b/src/assertDeepEqual.spec.lua @@ -1,99 +1,99 @@ -return function() - local assertDeepEqual = require(script.Parent.assertDeepEqual) - - it("should fail with a message when args are not equal", function() - local success, message = pcall(assertDeepEqual, 1, 2) - - expect(success).to.equal(false) - expect(message:find("first ~= second")).to.be.ok() - - success, message = pcall(assertDeepEqual, { - foo = 1, - }, { - foo = 2, - }) - - expect(success).to.equal(false) - expect(message:find("first%[foo%] ~= second%[foo%]")).to.be.ok() - end) - - it("should compare non-table values using standard '==' equality", function() - assertDeepEqual(1, 1) - assertDeepEqual("hello", "hello") - assertDeepEqual(nil, nil) - - local someFunction = function() end - local theSameFunction = someFunction - - assertDeepEqual(someFunction, theSameFunction) - - local A = { - foo = someFunction - } - local B = { - foo = theSameFunction - } - - assertDeepEqual(A, B) - end) - - it("should fail when types differ", function() - local success, message = pcall(assertDeepEqual, 1, "1") - - expect(success).to.equal(false) - expect(message:find("first is of type number, but second is of type string")).to.be.ok() - end) - - it("should compare (and report about) nested tables", function() - local A = { - foo = "bar", - nested = { - foo = 1, - bar = 2, - } - } - local B = { - foo = "bar", - nested = { - foo = 1, - bar = 2, - } - } - - assertDeepEqual(A, B) - - local C = { - foo = "bar", - nested = { - foo = 1, - bar = 3, - } - } - - local success, message = pcall(assertDeepEqual, A, C) - - expect(success).to.equal(false) - expect(message:find("first%[nested%]%[bar%] ~= second%[nested%]%[bar%]")).to.be.ok() - end) - - it("should be commutative", function() - local equalArgsA = { - foo = "bar", - hello = "world", - } - local equalArgsB = { - foo = "bar", - hello = "world", - } - - assertDeepEqual(equalArgsA, equalArgsB) - assertDeepEqual(equalArgsB, equalArgsA) - - local nonEqualArgs = { - foo = "bar", - } - - expect(function() assertDeepEqual(equalArgsA, nonEqualArgs) end).to.throw() - expect(function() assertDeepEqual(nonEqualArgs, equalArgsA) end).to.throw() - end) +return function() + local assertDeepEqual = require(script.Parent.assertDeepEqual) + + it("should fail with a message when args are not equal", function() + local success, message = pcall(assertDeepEqual, 1, 2) + + expect(success).to.equal(false) + expect(message:find("first ~= second")).to.be.ok() + + success, message = pcall(assertDeepEqual, { + foo = 1, + }, { + foo = 2, + }) + + expect(success).to.equal(false) + expect(message:find("first%[foo%] ~= second%[foo%]")).to.be.ok() + end) + + it("should compare non-table values using standard '==' equality", function() + assertDeepEqual(1, 1) + assertDeepEqual("hello", "hello") + assertDeepEqual(nil, nil) + + local someFunction = function() end + local theSameFunction = someFunction + + assertDeepEqual(someFunction, theSameFunction) + + local A = { + foo = someFunction + } + local B = { + foo = theSameFunction + } + + assertDeepEqual(A, B) + end) + + it("should fail when types differ", function() + local success, message = pcall(assertDeepEqual, 1, "1") + + expect(success).to.equal(false) + expect(message:find("first is of type number, but second is of type string")).to.be.ok() + end) + + it("should compare (and report about) nested tables", function() + local A = { + foo = "bar", + nested = { + foo = 1, + bar = 2, + } + } + local B = { + foo = "bar", + nested = { + foo = 1, + bar = 2, + } + } + + assertDeepEqual(A, B) + + local C = { + foo = "bar", + nested = { + foo = 1, + bar = 3, + } + } + + local success, message = pcall(assertDeepEqual, A, C) + + expect(success).to.equal(false) + expect(message:find("first%[nested%]%[bar%] ~= second%[nested%]%[bar%]")).to.be.ok() + end) + + it("should be commutative", function() + local equalArgsA = { + foo = "bar", + hello = "world", + } + local equalArgsB = { + foo = "bar", + hello = "world", + } + + assertDeepEqual(equalArgsA, equalArgsB) + assertDeepEqual(equalArgsB, equalArgsA) + + local nonEqualArgs = { + foo = "bar", + } + + expect(function() assertDeepEqual(equalArgsA, nonEqualArgs) end).to.throw() + expect(function() assertDeepEqual(nonEqualArgs, equalArgsA) end).to.throw() + end) end \ No newline at end of file diff --git a/src/createSignal.lua b/src/createSignal.lua index 2721269d..3db6354c 100644 --- a/src/createSignal.lua +++ b/src/createSignal.lua @@ -1,75 +1,75 @@ ---[[ - This is a simple signal implementation that has a dead-simple API. - - local signal = createSignal() - - local disconnect = signal:subscribe(function(foo) - print("Cool foo:", foo) - end) - - signal:fire("something") - - disconnect() -]] - -local function addToMap(map, addKey, addValue) - local new = {} - - for key, value in pairs(map) do - new[key] = value - end - - new[addKey] = addValue - - return new -end - -local function removeFromMap(map, removeKey) - local new = {} - - for key, value in pairs(map) do - if key ~= removeKey then - new[key] = value - end - end - - return new -end - -local function createSignal() - local connections = {} - - local function subscribe(self, callback) - assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") - - local connection = { - callback = callback, - } - - connections = addToMap(connections, callback, connection) - - local function disconnect() - assert(not connection.disconnected, "Listeners can only be disconnected once.") - - connection.disconnected = true - connections = removeFromMap(connections, callback) - end - - return disconnect - end - - local function fire(self, ...) - for callback, connection in pairs(connections) do - if not connection.disconnected then - callback(...) - end - end - end - - return { - subscribe = subscribe, - fire = fire, - } -end - +--[[ + This is a simple signal implementation that has a dead-simple API. + + local signal = createSignal() + + local disconnect = signal:subscribe(function(foo) + print("Cool foo:", foo) + end) + + signal:fire("something") + + disconnect() +]] + +local function addToMap(map, addKey, addValue) + local new = {} + + for key, value in pairs(map) do + new[key] = value + end + + new[addKey] = addValue + + return new +end + +local function removeFromMap(map, removeKey) + local new = {} + + for key, value in pairs(map) do + if key ~= removeKey then + new[key] = value + end + end + + return new +end + +local function createSignal() + local connections = {} + + local function subscribe(self, callback) + assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") + + local connection = { + callback = callback, + } + + connections = addToMap(connections, callback, connection) + + local function disconnect() + assert(not connection.disconnected, "Listeners can only be disconnected once.") + + connection.disconnected = true + connections = removeFromMap(connections, callback) + end + + return disconnect + end + + local function fire(self, ...) + for callback, connection in pairs(connections) do + if not connection.disconnected then + callback(...) + end + end + end + + return { + subscribe = subscribe, + fire = fire, + } +end + return createSignal \ No newline at end of file From 6cfb23ba3df10db48f06a377af4b510fa4d58375 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Mon, 13 Jan 2020 14:21:45 -0800 Subject: [PATCH 24/65] Scripts to run tests in roblox-cli --- bin/run-tests.server.lua | 7 ++++++- bin/test-roblox-cli.sh | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100755 bin/test-roblox-cli.sh diff --git a/bin/run-tests.server.lua b/bin/run-tests.server.lua index 3a14fe41..cc51dabb 100644 --- a/bin/run-tests.server.lua +++ b/bin/run-tests.server.lua @@ -1,6 +1,7 @@ -- luacheck: globals __LEMUR__ local ReplicatedStorage = game:GetService("ReplicatedStorage") +local isRobloxCli, ProcessService = pcall(game.GetService, game, "ProcessService") local Roact = require(ReplicatedStorage.Roact) local TestEZ = require(ReplicatedStorage.TestEZ) @@ -13,8 +14,12 @@ Roact.setGlobalConfig({ }) local results = TestEZ.TestBootstrap:run(ReplicatedStorage.Roact, TestEZ.Reporters.TextReporter) +local statusCode = results.failureCount == 0 and 0 or 1 + if __LEMUR__ then if results.failureCount > 0 then - os.exit(1) + os.exit(statusCode) end +elseif isRobloxCli then + ProcessService:Exit(statusCode) end \ No newline at end of file diff --git a/bin/test-roblox-cli.sh b/bin/test-roblox-cli.sh new file mode 100755 index 00000000..5040bb65 --- /dev/null +++ b/bin/test-roblox-cli.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# Usage: ./bin/test-roblox-cli.sh + +if [ ! -z ${LOCALAPPDATA+x} ]; then + # Probably Windows, look for any Roblox installation in the default path. + + VERSIONS_FOLDER="$LOCALAPPDATA/Roblox/Versions" + INSTALL=`find "$VERSIONS_FOLDER" -maxdepth 1 -name version-* | head -1` + CONTENT="$INSTALL/content" +else + # Probably macOS, look for Roblox Studio in its default path. + + CONTENT="/Applications/RobloxStudio.App/Contents/Resources/content" +fi + +rojo build place.project.json -o TestPlace.rbxlx +roblox-cli run --load.place TestPlace.rbxlx --assetFolder "$CONTENT" \ No newline at end of file From 31fa93dcd879504b48fbea7bcfe35becdcbb253e Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Tue, 14 Jan 2020 13:55:16 -0800 Subject: [PATCH 25/65] Add GitHub Actions for CI (#249) * First stab at GitHub Actions workflow * Submodules please * Different coverage reporting * Use luacov-coveralls master * Try explicitly installing luacov-coveralls from master * secret * Secrets take 2 * Try using dedicated secret * go back to GitHub token * Service name github * Explain weird coveralls config --- .github/workflows/ci.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..8cc34318 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + with: + submodules: true + + - uses: leafo/gh-actions-lua@v5 + with: + luaVersion: "5.1" + + - uses: leafo/gh-actions-luarocks@v2 + + - name: Install dependencies + run: | + luarocks install luafilesystem + luarocks install luacov + luarocks install luacov-coveralls --server=http://rocks.moonscript.org/dev + luarocks install luacheck + + - name: Test + run: | + lua -lluacov bin/spec.lua + luacheck src benchmarks examples + + # luacov-coveralls default settings do not function on GitHub Actions. + # We need to pass different service name and repo token explicitly + - name: Report to Coveralls + run: luacov-coveralls --repo-token $REPO_TOKEN --service-name github + env: + REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 6a0310952a0dd36d7296231f612e5356ca4e28f2 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Tue, 14 Jan 2020 15:13:19 -0800 Subject: [PATCH 26/65] Fix GitHub Actions workflow to work on PRs too --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cc34318..6c7c25b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,13 @@ name: CI -on: [push] +on: + push: + branches: + - master + + pull_request: + branches: + - master jobs: test: From 68e09328c2c0547726cce0679a533cb8059be0a8 Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Thu, 16 Jan 2020 11:15:31 -0800 Subject: [PATCH 27/65] New internal Context API (#247) Introduce a new internal Context api, exposed via Component and intended for use with a new public Context api --- src/Component.lua | 44 ++++- src/Component.spec/context.spec.lua | 110 ++++++++++-- src/Component.spec/legacyContext.spec.lua | 209 ++++++++++++++++++++++ src/RobloxRenderer.spec.lua | 75 +++++++- src/createReconciler.lua | 55 ++++-- 5 files changed, 466 insertions(+), 27 deletions(-) create mode 100644 src/Component.spec/legacyContext.spec.lua diff --git a/src/Component.lua b/src/Component.lua index 5782af29..1dfb5fb7 100644 --- a/src/Component.lua +++ b/src/Component.lua @@ -196,6 +196,46 @@ function Component:render() error(message, 0) end +--[[ + Retrieves the context value corresponding to the given key. Can return nil + if a requested context key is not present +]] +function Component:__getContext(key) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__getContext`") + internalAssert(key ~= nil, "Context key cannot be nil") + end + + local virtualNode = self[InternalData].virtualNode + local context = virtualNode.context + + return context[key] +end + +--[[ + Adds a new context entry to this component's context table (which will be + passed down to child components). +]] +function Component:__addContext(key, value) + if config.internalTypeChecks then + internalAssert(Type.of(self) == Type.StatefulComponentInstance, "Invalid use of `__addContext`") + end + local virtualNode = self[InternalData].virtualNode + + -- Make sure we store a reference to the component's original, unmodified + -- context the virtual node. In the reconciler, we'll restore the original + -- context if we need to replace the node (this happens when a node gets + -- re-rendered as a different component) + if virtualNode.originalContext == nil then + virtualNode.originalContext = virtualNode.context + end + + -- Build a new context table on top of the existing one, then apply it to + -- our virtualNode + local existing = virtualNode.context + virtualNode.context = assign({}, existing, { [key] = value }) +end + --[[ Performs property validation if the static method validateProps is declared. validateProps should follow assert's expected arguments: @@ -273,7 +313,7 @@ function Component:__mount(reconciler, virtualNode) instance.props = props - local newContext = assign({}, virtualNode.context) + local newContext = assign({}, virtualNode.legacyContext) instance._context = newContext instance.state = assign({}, instance:__getDerivedState(instance.props, {})) @@ -284,7 +324,7 @@ function Component:__mount(reconciler, virtualNode) end -- It's possible for init() to redefine _context! - virtualNode.context = instance._context + virtualNode.legacyContext = instance._context internalData.lifecyclePhase = ComponentLifecyclePhase.Render local renderResult = instance:render() diff --git a/src/Component.spec/context.spec.lua b/src/Component.spec/context.spec.lua index cfef942d..1346a335 100644 --- a/src/Component.spec/context.spec.lua +++ b/src/Component.spec/context.spec.lua @@ -3,16 +3,17 @@ return function() local createElement = require(script.Parent.Parent.createElement) local createReconciler = require(script.Parent.Parent.createReconciler) local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + local oneChild = require(script.Parent.Parent.oneChild) local Component = require(script.Parent.Parent.Component) local noopReconciler = createReconciler(NoopRenderer) - it("should be provided as a mutable self._context in Component:init", function() + it("should be provided as an internal api on Component", function() local Provider = Component:extend("Provider") function Provider:init() - self._context.foo = "bar" + self:__addContext("foo", "bar") end function Provider:render() @@ -35,7 +36,10 @@ return function() local capturedContext function Consumer:init() - capturedContext = self._context + capturedContext = { + hello = self:__getContext("hello"), + value = self:__getContext("value"), + } end function Consumer:render() @@ -67,7 +71,10 @@ return function() local capturedContext function Consumer:init() - capturedContext = self._context + capturedContext = { + hello = self:__getContext("hello"), + value = self:__getContext("value"), + } end function Consumer:render() @@ -92,12 +99,85 @@ return function() assertDeepEqual(capturedContext, context) end) + it("should not copy the context table if it doesn't need to", function() + local Parent = Component:extend("Parent") + + function Parent:init() + self:__addContext("parent", "I'm here!") + end + + function Parent:render() + -- Create some child element + return createElement(function() end) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local parentNode = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContext = { + parent = "I'm here!", + } + + assertDeepEqual(parentNode.context, expectedContext) + + local childNode = oneChild(parentNode.children) + + -- Parent and child should have the same context table + expect(parentNode.context).to.equal(childNode.context) + end) + + it("should not allow context to move up the tree", function() + local ChildProvider = Component:extend("ChildProvider") + + function ChildProvider:init() + self:__addContext("child", "I'm here too!") + end + + function ChildProvider:render() + end + + local ParentProvider = Component:extend("ParentProvider") + + function ParentProvider:init() + self:__addContext("parent", "I'm here!") + end + + function ParentProvider:render() + return createElement(ChildProvider) + end + + local element = createElement(ParentProvider) + local hostParent = nil + local hostKey = "Parent" + + local parentNode = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + local childNode = oneChild(parentNode.children) + + local expectedParentContext = { + parent = "I'm here!", + -- Context does not travel back up + } + + local expectedChildContext = { + parent = "I'm here!", + child = "I'm here too!" + } + + assertDeepEqual(parentNode.context, expectedParentContext) + assertDeepEqual(childNode.context, expectedChildContext) + end) + it("should contain values put into the tree by parent nodes", function() local Consumer = Component:extend("Consumer") local capturedContext function Consumer:init() - capturedContext = self._context + capturedContext = { + dont = self:__getContext("dont"), + frob = self:__getContext("frob"), + } end function Consumer:render() @@ -106,7 +186,7 @@ return function() local Provider = Component:extend("Provider") function Provider:init() - self._context.frob = "ulator" + self:__addContext("frob", "ulator") end function Provider:render() @@ -143,11 +223,19 @@ return function() it("should transfer context to children that are replaced", function() local ConsumerA = Component:extend("ConsumerA") + local function captureAllContext(component) + return { + A = component:__getContext("A"), + B = component:__getContext("B"), + frob = component:__getContext("frob"), + } + end + local capturedContextA function ConsumerA:init() - self._context.A = "hello" + self:__addContext("A", "hello") - capturedContextA = self._context + capturedContextA = captureAllContext(self) end function ConsumerA:render() @@ -157,9 +245,9 @@ return function() local capturedContextB function ConsumerB:init() - self._context.B = "hello" + self:__addContext("B", "hello") - capturedContextB = self._context + capturedContextB = captureAllContext(self) end function ConsumerB:render() @@ -168,7 +256,7 @@ return function() local Provider = Component:extend("Provider") function Provider:init() - self._context.frob = "ulator" + self:__addContext("frob", "ulator") end function Provider:render() diff --git a/src/Component.spec/legacyContext.spec.lua b/src/Component.spec/legacyContext.spec.lua new file mode 100644 index 00000000..e1014f21 --- /dev/null +++ b/src/Component.spec/legacyContext.spec.lua @@ -0,0 +1,209 @@ +return function() + local assertDeepEqual = require(script.Parent.Parent.assertDeepEqual) + local createElement = require(script.Parent.Parent.createElement) + local createReconciler = require(script.Parent.Parent.createReconciler) + local NoopRenderer = require(script.Parent.Parent.NoopRenderer) + + local Component = require(script.Parent.Parent.Component) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should be provided as a mutable self._context in Component:init", function() + local Provider = Component:extend("Provider") + + function Provider:init() + self._context.foo = "bar" + end + + function Provider:render() + end + + local element = createElement(Provider) + local hostParent = nil + local hostKey = "Provider" + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContext = { + foo = "bar", + } + + assertDeepEqual(node.legacyContext, expectedContext) + end) + + it("should be inherited from parent stateful nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + end + + local Parent = Component:extend("Parent") + + function Parent:render() + return createElement(Consumer) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local context = { + hello = "world", + value = 6, + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) + + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.legacyContext) + assertDeepEqual(node.legacyContext, context) + assertDeepEqual(capturedContext, context) + end) + + it("should be inherited from parent function nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + end + + local function Parent() + return createElement(Consumer) + end + + local element = createElement(Parent) + local hostParent = nil + local hostKey = "Parent" + local context = { + hello = "world", + value = 6, + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) + + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.legacyContext) + assertDeepEqual(node.legacyContext, context) + assertDeepEqual(capturedContext, context) + end) + + it("should contain values put into the tree by parent nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + end + + local Provider = Component:extend("Provider") + + function Provider:init() + self._context.frob = "ulator" + end + + function Provider:render() + return createElement(Consumer) + end + + local element = createElement(Provider) + local hostParent = nil + local hostKey = "Consumer" + local context = { + dont = "try it", + } + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) + + local initialContext = { + dont = "try it", + } + + local expectedContext = { + dont = "try it", + frob = "ulator", + } + + -- Because components mutate context, we're careful with equality + expect(node.legacyContext).never.to.equal(context) + expect(capturedContext).never.to.equal(context) + expect(capturedContext).never.to.equal(node.legacyContext) + + assertDeepEqual(context, initialContext) + assertDeepEqual(node.legacyContext, expectedContext) + assertDeepEqual(capturedContext, expectedContext) + end) + + it("should transfer context to children that are replaced", function() + local ConsumerA = Component:extend("ConsumerA") + + local capturedContextA + function ConsumerA:init() + self._context.A = "hello" + + capturedContextA = self._context + end + + function ConsumerA:render() + end + + local ConsumerB = Component:extend("ConsumerB") + + local capturedContextB + function ConsumerB:init() + self._context.B = "hello" + + capturedContextB = self._context + end + + function ConsumerB:render() + end + + local Provider = Component:extend("Provider") + + function Provider:init() + self._context.frob = "ulator" + end + + function Provider:render() + local useConsumerB = self.props.useConsumerB + + if useConsumerB then + return createElement(ConsumerB) + else + return createElement(ConsumerA) + end + end + + local hostParent = nil + local hostKey = "Consumer" + + local element = createElement(Provider) + local node = noopReconciler.mountVirtualNode(element, hostParent, hostKey) + + local expectedContextA = { + frob = "ulator", + A = "hello", + } + + assertDeepEqual(capturedContextA, expectedContextA) + + local expectedContextB = { + frob = "ulator", + B = "hello", + } + + local replacedElement = createElement(Provider, { + useConsumerB = true, + }) + noopReconciler.updateVirtualNode(node, replacedElement) + + assertDeepEqual(capturedContextB, expectedContextB) + end) +end \ No newline at end of file diff --git a/src/RobloxRenderer.spec.lua b/src/RobloxRenderer.spec.lua index d31421c9..1ea04cb2 100644 --- a/src/RobloxRenderer.spec.lua +++ b/src/RobloxRenderer.spec.lua @@ -811,7 +811,9 @@ return function() local capturedContext function Consumer:init() - capturedContext = self._context + capturedContext = { + hello = self:__getContext("hello") + } end function Consumer:render() @@ -833,6 +835,77 @@ return function() reconciler.unmountVirtualNode(node) end) + it("should pass context values through portal nodes", function() + local target = Instance.new("Folder") + + local Provider = Component:extend("Provider") + + function Provider:init() + self:__addContext("foo", "bar") + end + + function Provider:render() + return createElement("Folder", nil, self.props[Children]) + end + + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = { + foo = self:__getContext("foo"), + } + end + + function Consumer:render() + return nil + end + + local element = createElement(Provider, nil, { + Portal = createElement(Portal, { + target = target, + }, { + Consumer = createElement(Consumer), + }) + }) + local hostParent = nil + local hostKey = "Some Key" + reconciler.mountVirtualNode(element, hostParent, hostKey) + + assertDeepEqual(capturedContext, { + foo = "bar" + }) + end) + end) + + describe("Legacy context", function() + it("should pass context values through Roblox host nodes", function() + local Consumer = Component:extend("Consumer") + + local capturedContext + function Consumer:init() + capturedContext = self._context + end + + function Consumer:render() + end + + local element = createElement("Folder", nil, { + Consumer = createElement(Consumer) + }) + local hostParent = nil + local hostKey = "Context Test" + local context = { + hello = "world", + } + local node = reconciler.mountVirtualNode(element, hostParent, hostKey, nil, context) + + expect(capturedContext).never.to.equal(context) + assertDeepEqual(capturedContext, context) + + reconciler.unmountVirtualNode(node) + end) + it("should pass context values through portal nodes", function() local target = Instance.new("Folder") diff --git a/src/createReconciler.lua b/src/createReconciler.lua index fbae970d..e4e43c62 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -31,16 +31,21 @@ local function createReconciler(renderer) Unmount the given virtualNode, replacing it with a new node described by the given element. - Preserves host properties, depth, and context from parent. + Preserves host properties, depth, and legacyContext from parent. ]] local function replaceVirtualNode(virtualNode, newElement) local hostParent = virtualNode.hostParent local hostKey = virtualNode.hostKey local depth = virtualNode.depth - local parentContext = virtualNode.parentContext + + -- If the node that is being replaced has modified context, we need to + -- use the original *unmodified* context for the new node + -- The `originalContext` field will be nil if the context was unchanged + local context = virtualNode.originalContext or virtualNode.context + local parentLegacyContext = virtualNode.parentLegacyContext unmountVirtualNode(virtualNode) - local newNode = mountVirtualNode(newElement, hostParent, hostKey, parentContext) + local newNode = mountVirtualNode(newElement, hostParent, hostKey, context, parentLegacyContext) -- mountVirtualNode can return nil if the element is a boolean if newNode ~= nil then @@ -85,7 +90,13 @@ local function createReconciler(renderer) end if virtualNode.children[childKey] == nil then - local childNode = mountVirtualNode(newElement, hostParent, concreteKey, virtualNode.context) + local childNode = mountVirtualNode( + newElement, + hostParent, + concreteKey, + virtualNode.context, + virtualNode.legacyContext + ) -- mountVirtualNode can return nil if the element is a boolean if childNode ~= nil then @@ -247,10 +258,14 @@ local function createReconciler(renderer) --[[ Constructs a new virtual node but not does mount it. ]] - local function createVirtualNode(element, hostParent, hostKey, context) + local function createVirtualNode(element, hostParent, hostKey, context, legacyContext) if config.internalTypeChecks then internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") internalAssert(typeof(context) == "table" or context == nil, "Expected arg #4 to be of type table or nil") + internalAssert( + typeof(legacyContext) == "table" or legacyContext == nil, + "Expected arg #5 to be of type table or nil" + ) end if config.typeChecks then assert(hostKey ~= nil, "Expected arg #3 to be non-nil") @@ -267,10 +282,21 @@ local function createReconciler(renderer) children = {}, hostParent = hostParent, hostKey = hostKey, - context = context, - -- This copy of context is useful if the element gets replaced - -- with an element of a different component type - parentContext = context, + + -- Legacy Context API + -- A table of context values inherited from the parent node + legacyContext = legacyContext, + + -- A saved copy of the parent context, used when replacing a node + parentLegacyContext = legacyContext, + + -- Context API + -- A table of context values inherited from the parent node + context = context or {}, + + -- A saved copy of the unmodified context; this will be updated when + -- a component adds new context and used when a node is replaced + originalContext = nil, } end @@ -304,10 +330,13 @@ local function createReconciler(renderer) Constructs a new virtual node and mounts it, but does not place it into the tree. ]] - function mountVirtualNode(element, hostParent, hostKey, context) + function mountVirtualNode(element, hostParent, hostKey, context, legacyContext) if config.internalTypeChecks then internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") - internalAssert(typeof(context) == "table" or context == nil, "Expected arg #4 to be of type table or nil") + internalAssert( + typeof(legacyContext) == "table" or legacyContext == nil, + "Expected arg #5 to be of type table or nil" + ) end if config.typeChecks then assert(hostKey ~= nil, "Expected arg #3 to be non-nil") @@ -324,7 +353,7 @@ local function createReconciler(renderer) local kind = ElementKind.of(element) - local virtualNode = createVirtualNode(element, hostParent, hostKey, context) + local virtualNode = createVirtualNode(element, hostParent, hostKey, context, legacyContext) if kind == ElementKind.Host then renderer.mountHostNode(reconciler, virtualNode) @@ -424,4 +453,4 @@ local function createReconciler(renderer) return reconciler end -return createReconciler \ No newline at end of file +return createReconciler From d9b7f9661b26ff16db240f2fe8b0f8284303c61d Mon Sep 17 00:00:00 2001 From: Patrick Sewell Date: Tue, 21 Jan 2020 09:48:50 -0800 Subject: [PATCH 28/65] Add Context API (#246) * Added Context api Added createContext, provide, and consume APIs to Roact. * Remove provide and consume Removes provide and consume from the new Roact context API, so that createContext returns a Provider and Consumer. * Update from Code Review comments -Use fragment instead of oneChild -Use spies for testing -Use Component over PureComponent -Check for duplicates in didUpdate -nil check disconnect * Update to use new Internal Context API Updates the createContext API to internally use the new internal Context API. --- CHANGELOG.md | 1 + src/createContext.lua | 101 ++++++++++++++++++++++++++++++ src/createContext.spec.lua | 122 +++++++++++++++++++++++++++++++++++++ src/init.lua | 1 + src/init.spec.lua | 1 + 5 files changed, 226 insertions(+) create mode 100644 src/createContext.lua create mode 100644 src/createContext.spec.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 76191aaa..e2bdfc54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Roact Changelog ## Unreleased Changes +* Added Contexts, which enables easy handling of items that are provided and consumed throughout the tree. ## [1.2.0](https://github.com/Roblox/roact/releases/tag/v1.2.0) (September 6th, 2019) * Fixed a bug where derived state was lost when assigning directly to state in init ([#232](https://github.com/Roblox/roact/pull/232/)) diff --git a/src/createContext.lua b/src/createContext.lua new file mode 100644 index 00000000..260f8a4f --- /dev/null +++ b/src/createContext.lua @@ -0,0 +1,101 @@ +local Symbol = require(script.Parent.Symbol) +local Binding = require(script.Parent.Binding) +local createFragment = require(script.Parent.createFragment) +local Children = require(script.Parent.PropMarkers.Children) +local Component = require(script.Parent.Component) + +local function createProvider(context) + local Provider = Component:extend("Provider") + + function Provider:init(props) + self.binding, self.updateValue = Binding.create(props.value) + + local key = context.key + self:__addContext(key, self.binding) + end + + function Provider:didUpdate(prevProps) + if prevProps.value ~= self.props.value then + self.updateValue(self.props.value) + end + end + + function Provider:render() + return createFragment(self.props[Children]) + end + + return Provider +end + +local function createConsumer(context) + local Consumer = Component:extend("Consumer") + + function Consumer:init(props) + local key = context.key + local binding = self:__getContext(key) + + if binding ~= nil then + self.state = { + value = binding:getValue(), + } + + -- Update if the Context updated + self.disconnect = Binding.subscribe(binding, function() + self:setState({ + value = binding:getValue(), + }) + end) + else + -- Fall back to the default value if no Provider exists + self.state = { + value = context.defaultValue, + } + end + end + + function Consumer.validateProps(props) + if type(props.render) ~= "function" then + return false, "Consumer expects a `render` function" + else + return true + end + end + + function Consumer:render() + return self.props.render(self.state.value) + end + + function Consumer:willUnmount() + if self.disconnect ~= nil then + self.disconnect() + end + end + + return Consumer +end + +local Context = {} +Context.__index = Context + +function Context.new(defaultValue) + local self = { + defaultValue = defaultValue, + key = Symbol.named("ContextKey"), + } + setmetatable(self, Context) + return self +end + +function Context:__tostring() + return "RoactContext" +end + +local function createContext(defaultValue) + local context = Context.new(defaultValue) + return { + Provider = createProvider(context), + Consumer = createConsumer(context), + } +end + +return createContext diff --git a/src/createContext.spec.lua b/src/createContext.spec.lua new file mode 100644 index 00000000..ad365492 --- /dev/null +++ b/src/createContext.spec.lua @@ -0,0 +1,122 @@ +return function() + local createContext = require(script.Parent.createContext) + local createElement = require(script.Parent.createElement) + local NoopRenderer = require(script.Parent.NoopRenderer) + local createReconciler = require(script.Parent.createReconciler) + local createSpy = require(script.Parent.createSpy) + + local noopReconciler = createReconciler(NoopRenderer) + + it("should return a table", function() + local context = createContext("Test") + expect(context).to.be.ok() + expect(type(context)).to.equal("table") + end) + + it("should contain a Provider and a Consumer", function() + local context = createContext("Test") + expect(context.Provider).to.be.ok() + expect(context.Consumer).to.be.ok() + end) + + describe("Provider", function() + it("should render its children", function() + local context = createContext("Test") + + local Listener = createSpy(function() + return nil + end) + + local element = createElement(context.Provider, { + value = "Test", + }, { + Listener = createElement(Listener.value), + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + noopReconciler.unmountVirtualTree(tree) + + expect(Listener.callCount).to.equal(1) + end) + end) + + describe("Consumer", function() + it("should expect a render function", function() + local context = createContext("Test") + local element = createElement(context.Consumer) + + expect(function() + noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + end).to.throw() + end) + + it("should return the default value if there is no Provider", function() + local valueSpy = createSpy() + local context = createContext("Test") + + local element = createElement(context.Consumer, { + render = valueSpy.value, + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + noopReconciler.unmountVirtualTree(tree) + + valueSpy:assertCalledWith("Test") + end) + + it("should pass the value to the render function", function() + local valueSpy = createSpy() + local context = createContext("Test") + + local function Listener() + return createElement(context.Consumer, { + render = valueSpy.value, + }) + end + + local element = createElement(context.Provider, { + value = "NewTest", + }, { + Listener = createElement(Listener), + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + noopReconciler.unmountVirtualTree(tree) + + valueSpy:assertCalledWith("NewTest") + end) + + it("should update when the value updates", function() + local valueSpy = createSpy() + local context = createContext("Test") + + local function Listener() + return createElement(context.Consumer, { + render = valueSpy.value, + }) + end + + local element = createElement(context.Provider, { + value = "NewTest", + }, { + Listener = createElement(Listener), + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + + expect(valueSpy.callCount).to.equal(1) + valueSpy:assertCalledWith("NewTest") + + noopReconciler.updateVirtualTree(tree, createElement(context.Provider, { + value = "ThirdTest", + }, { + Listener = createElement(Listener), + })) + + expect(valueSpy.callCount).to.equal(3) + valueSpy:assertCalledWith("ThirdTest") + + noopReconciler.unmountVirtualTree(tree) + end) + end) +end diff --git a/src/init.lua b/src/init.lua index f002f975..0bac3010 100644 --- a/src/init.lua +++ b/src/init.lua @@ -23,6 +23,7 @@ local Roact = strict { createRef = require(script.createRef), createBinding = Binding.create, joinBindings = Binding.join, + createContext = require(script.createContext), Change = require(script.PropMarkers.Change), Children = require(script.PropMarkers.Children), diff --git a/src/init.spec.lua b/src/init.spec.lua index 7fcf79c8..652ee19a 100644 --- a/src/init.spec.lua +++ b/src/init.spec.lua @@ -13,6 +13,7 @@ return function() update = "function", oneChild = "function", setGlobalConfig = "function", + createContext = "function", -- These functions are deprecated and throw warnings! reify = "function", From a2f0047d9e51d04ae762ec2194e766d4efd35a2e Mon Sep 17 00:00:00 2001 From: MagiMaster Date: Thu, 6 Feb 2020 17:38:31 -0800 Subject: [PATCH 29/65] Swap getDerived and shouldUpdate in lifecycle diagram (#255) Also add a note about their interaction to the API reference --- docs/api-reference.md | 5 +- docs/images/lifecycle.gv | 2 +- docs/images/lifecycle.svg | 215 +++++++++++++++++++++----------------- 3 files changed, 126 insertions(+), 96 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index a51216ca..f356bbea 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -608,4 +608,7 @@ end As with `setState`, you can set use the constant `Roact.None` to remove a field from the state. !!! note - `getDerivedStateFromProps` is a *static* lifecycle method. It does not have access to `self`, and must be a pure function. \ No newline at end of file + `getDerivedStateFromProps` is a *static* lifecycle method. It does not have access to `self`, and must be a pure function. + +!!! caution + `getDerivedStateFromProps` runs before `shouldUpdate` and any non-nil return will cause the state table to no longer be shallow-equal. This means that a `PureComponent` will rerender even if nothing actually changed. Similarly, any component implementing both `getDerivedStateFromProps` and `shouldUpdate` needs to do so in a way that takes this in to account. diff --git a/docs/images/lifecycle.gv b/docs/images/lifecycle.gv index 61c3d4e9..aff4b6f2 100644 --- a/docs/images/lifecycle.gv +++ b/docs/images/lifecycle.gv @@ -25,7 +25,7 @@ digraph G {

Roact.update >]; - update -> shouldUpdate -> getDerivedStateFromProps -> willUpdate -> render -> updated -> didUpdate; + update -> getDerivedStateFromProps -> shouldUpdate -> willUpdate -> render -> updated -> didUpdate; unmounted [style="rounded", color="#000000", label=<Instances Destroyed>]; unmount [style="rounded", color="#000000", label=< diff --git a/docs/images/lifecycle.svg b/docs/images/lifecycle.svg index 088c301d..f34335fe 100644 --- a/docs/images/lifecycle.svg +++ b/docs/images/lifecycle.svg @@ -1,151 +1,178 @@ - - - + + G - + -render1 - -render + +render1 + +render -created - -Instances Created + +created + +Instances Created -render1->created - - + +render1->created + + -didMount - -didMount + +didMount + +didMount -created->didMount - - + +created->didMount + + -mount - -Mounting -Roact.mount + +mount + +Mounting +Roact.mount -init - -init + +init + +init -mount->init - - + +mount->init + + -init->render1 - - + +init->render1 + + -updated - -Instances Updated + +updated + +Instances Updated -didUpdate - -didUpdate + +didUpdate + +didUpdate -updated->didUpdate - - + +updated->didUpdate + + -update - -Updating -Roact.update + +update + +Updating +Roact.update - -shouldUpdate - -shouldUpdate + + +getDerivedStateFromProps + +getDerivedStateFromProps - -update->shouldUpdate - - + + +update->getDerivedStateFromProps + + - -getDerivedStateFromProps - -getDerivedStateFromProps + + +shouldUpdate + +shouldUpdate - -shouldUpdate->getDerivedStateFromProps - - + + +getDerivedStateFromProps->shouldUpdate + + -willUpdate - -willUpdate + +willUpdate + +willUpdate - -getDerivedStateFromProps->willUpdate - - + + +shouldUpdate->willUpdate + + -render - -render + +render + +render -willUpdate->render - - + +willUpdate->render + + -render->updated - - + +render->updated + + -unmounted - -Instances Destroyed + +unmounted + +Instances Destroyed -unmount - -Unmounting -Roact.unmount + +unmount + +Unmounting +Roact.unmount -willUnmount - -willUnmount + +willUnmount + +willUnmount -unmount->willUnmount - - + +unmount->willUnmount + + -willUnmount->unmounted - - + +willUnmount->unmounted + + From 492aa6cacfd20356b4ea4da9b194cbb4da83b59f Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Wed, 19 Feb 2020 13:50:10 -0800 Subject: [PATCH 30/65] Use workflow to deploy MkDocs documentation (#256) --- .github/workflows/deploy-docs.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/deploy-docs.yml diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 00000000..4293fcee --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,19 @@ +name: Deploy Documentation + +on: + push: + branches: + - master + +jobs: + build: + name: Deploy docs + runs-on: ubuntu-latest + steps: + - name: Checkout master + uses: actions/checkout@v2 + + - name: Deploy docs + uses: mhausenblas/mkdocs-deploy-gh-pages@1.11 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From a61d2939bfb29c7e87ef7e9edc4445aba88eeccd Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Wed, 19 Feb 2020 13:53:24 -0800 Subject: [PATCH 31/65] Document new context API and fix some docs mishaps (#253) * Document new context API and fix some docs mishaps * Add notice to validateProps * Flesh out rough guide * Update docs/advanced/context.md Co-Authored-By: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> * Update docs/api-reference.md Co-Authored-By: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> * Update docs/api-reference.md Co-Authored-By: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> * Fix createContext docs a little bit * More danger Co-authored-by: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> --- docs/advanced/context.md | 101 +++++++++++++++++++++++++++++++++++++-- docs/api-reference.md | 54 +++++++++++++++++++-- docs/extra.css | 29 +---------- 3 files changed, 149 insertions(+), 35 deletions(-) diff --git a/docs/advanced/context.md b/docs/advanced/context.md index 84c462ca..71ec0c9b 100644 --- a/docs/advanced/context.md +++ b/docs/advanced/context.md @@ -1,5 +1,98 @@ -!!! warning - Context is an unstable feature that's being *significantly* revised. See [issue #4](https://github.com/Roblox/roact/issues/4) for current progress. +!!! danger "Unreleased API" + This API is not yet available in a stable Roact release. -!!! info - This section is incomplete. It's possible that the context API will change before the existing API is ever documented. \ No newline at end of file + It may be available from a recent pre-release or Roact's master branch. + +[TOC] + +Roact supports a feature called context which enables passing values down the tree without having to pass them through props. Roact's Context API is based on [React's Context API](https://reactjs.org/docs/context.html). + +Context is commonly used to implement features like dependency injection, dynamic theming, and scoped state storage. + +## Basic Usage +```lua +local ThemeContext = Roact.createContext(defaultValue) +``` + +Context objects contain two components, `Consumer` and `Provider`. + +The `Consumer` component accepts a `render` function as its only prop, which is used to render its children. It's passed one argument, which is the context value from the nearest matching `Provider` ancestor. + +If there is no `Provider` ancestor, then `defaultValue` will be passed instead. + +```lua +local function ThemedButton(props) + return Roact.createElement(ThemeContext.Consumer, { + render = function(theme) + return Roact.createElement("TextButton", { + Size = UDim2.new(0, 100, 0, 100), + Text = "Click Me!", + TextColor3 = theme.foreground, + BackgroundColor3 = theme.background, + }) + end + }) +end +``` + +The `Provider` component accepts a `value` prop as well as children. Any of its descendants will have access to the value provided to it by using the `Consumer` component like above. + +Whenever the `Provider` receives a new `value` prop in an update, any attached `Consumer` components will re-render with the new value. This value could be externally controlled, or could be controlled by state in a component wrapping `Provider`: + +```lua +local ThemeController = Roact.Component:extend("ThemeController") + +function ThemeController:init() + self:setState({ + theme = { + foreground = Color3.new(1, 1, 1), + background = Color3.new(0, 0, 0), + } + }) +end + +function ThemeController:render() + return Roact.createElement(ThemeContext.Provider, { + value = self.state.theme, + }, self.props[Roact.Children]) +end +``` + +## Legacy Context +!!! danger + Legacy Context is a deprecated feature that will be removed in a future release of Roact. + +Roact also has a deprecated version of context that pre-dates the stable context API. + +Legacy context values **do not update dynamically** on their own. It is up to the context user to create their own mechanism for updates, probably using a wrapper component and `setState`. + +To use it, add new entries to `self._context` in `Component:init()` to create a provider: + +```lua +local Provider = Roact.Component:extend("FooProvider") + +-- Using a unique non-string key is recommended to avoid collisions. +local FooKey = {} + +function Provider:init() + self._context[FooKey] = { + value = 5, + } +end +``` + +...and read from that same value in `Component:init()` in your consumer component: + +```lua +local Consumer = Roact.Component:extend("FooConsumer") + +function Consumer:init() + self.foo = self._context[FooKey] +end + +function Consumer:render() + return Roact.createElement("TextLabel", { + Text = "Foo: " .. self.foo.value, + }) +end +``` \ No newline at end of file diff --git a/docs/api-reference.md b/docs/api-reference.md index f356bbea..404301ac 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -17,7 +17,8 @@ The `children` argument is shorthand for adding a `Roact.Children` key to `props --- ### Roact.createFragment -
Added in 1.0.0
+ +!!! success "Added in Roact 1.0.0" ``` Roact.createFragment(elements) -> RoactFragment @@ -84,7 +85,8 @@ If `children` is `nil` or contains no children, `oneChild` will return `nil`. --- ### Roact.createBinding -
Added in 1.0.0
+ +!!! success "Added in Roact 1.0.0" ``` Roact.createBinding(initialValue) -> Binding, updateFunction @@ -118,7 +120,8 @@ Returns a new binding that maps the existing binding's value to something else. --- ### Roact.joinBindings -
Added in 1.1.0
+ +!!! success "Added in Roact 1.1.0" ``` Roact.joinBindings(bindings) -> Binding @@ -176,6 +179,45 @@ Creates a new reference object that can be used with [Roact.Ref](#roactref). --- +### Roact.createContext + +!!! danger "Unreleased API" + This API is not yet available in a stable Roact release. + + It may be available from a recent pre-release or Roact's master branch. + +``` +Roact.createContext(defaultValue: any) -> RoactContext + +type RoactContext = { + Provider: Component, + Consumer: Component, + [private fields] +} +``` + +Creates a new context provider and consumer. For a usage guide, see [Advanced Concepts: Context](/advanced/context). + +`defaultValue` is given to consumers if they have no `Provider` ancestors. It is up to users of Roact's context API to turn this case into an error if it is an invalid state. + +`Provider` and `Consumer` are both Roact components. + +#### `Provider` +`Provider` accepts the following props: + +* `value`: The value to put into the tree for this context value. + * If the `Provider` is updated with a new `value`, any matching `Consumer` components will be re-rendered with the new value. +* `[Children]`: Any number of children to render underneath this provider. + * Descendants of this component can receive the provided context value by using `Consumer`. + +#### `Consumer` +`Consumer` accepts just one prop: + +* `render(value) -> RoactElement | nil`: A function that will be invoked to render any children. + * `render` will be called every time `Consumer` is rendered. + +--- + ### Roact.setGlobalConfig ``` Roact.setGlobalConfig(configValues: Dictionary) -> void @@ -503,7 +545,8 @@ By default, components are re-rendered any time a parent component updates, or w --- ### validateProps -
Added in 1.0.0
+ +!!! success "Added in Roact 1.0.0" ``` static validateProps(props) -> (false, message: string) | true @@ -523,6 +566,9 @@ Roact.setGlobalConfig({ See [setGlobalConfig](#roactsetglobalconfig) for more details. +!!! note + `validateProps` is a *static* lifecycle method. It does not have access to `self`, and must be a pure function. + !!! warning Depending on the implementation, `validateProps` can impact performance. Recommended practice is to enable prop validation during development and leave it off in production environments. diff --git a/docs/extra.css b/docs/extra.css index a0d882bd..8c8f8ce5 100644 --- a/docs/extra.css +++ b/docs/extra.css @@ -1,28 +1,3 @@ -.api-addition { - display: flex; - align-content: center; - align-items: center; - vertical-align: middle; - - padding: 0.4rem 0.6rem 0.4rem 0.5rem; - background-color: #efffec; - border-left: 0.2rem solid green; - border-radius: 0.1rem; - - line-height: 1; - font-size: 0.64rem; - font-weight: bold; - - box-shadow: - 0 2px 2px 0 rgba(0,0,0,.14), - 0 1px 5px 0 rgba(0,0,0,.12), - 0 3px 1px -2px rgba(0,0,0,.2); -} - -.api-addition::before { - flex: 0 0 1.5rem; - content: "+"; - color: green; - font-size: 1.5em; - text-align: center; +.md-typeset hr { + border-bottom: 2px solid rgba(0, 0, 0, 0.15); } \ No newline at end of file From f7d2f1ce1dd69120f457c2f8032bb184eb8f0c29 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Fri, 28 Feb 2020 13:55:37 -0800 Subject: [PATCH 32/65] Fix "advance concepts" link in docs (#258) --- docs/api-reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 404301ac..3274204f 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -196,7 +196,7 @@ type RoactContext = { } ``` -Creates a new context provider and consumer. For a usage guide, see [Advanced Concepts: Context](/advanced/context). +Creates a new context provider and consumer. For a usage guide, see [Advanced Concepts: Context](../advanced/context). `defaultValue` is given to consumers if they have no `Provider` ancestors. It is up to users of Roact's context API to turn this case into an error if it is an invalid state. From d1b1db25e3c4176f2b103d458711a3d968710b5c Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Thu, 19 Mar 2020 10:57:59 -0700 Subject: [PATCH 33/65] Fix prop-context update tearing (#260) This PR tries to fix the update tearing behavior I discovered and reported in #259 based on a solution that my team has been discussing. I introduced a fairly small and robust test that is currently failing to indicate the problem with our current state and the quick-fix solutions to this problem. Context values are no longer based on bindings and now instead use a small cell that contains a value and an update signal. Providers now update context values in two passes: 1. In `willUpdate`, the provider updates the value of the context entry but do not fire any signals. This should cause downstream consumers to be able to immediately read the new value on their next render. Their next render will occur immediately unless an intermediate component implements `shouldUpdate`. 2. In `didUpdate`, the provider fires the signal portion of the context entry. This will notify any consumers. Consumers are now a little dumber and simpler. They no longer track the current context value in state, but instead track the last value they were updated with. When an update signal is received from their corresponding provider's `didUpdate` hook, consumers now compare the new value with the value they were last updated with, stored in their `didUpdate` hook. If these values differ, we can assume that there was a component between the consumer and provider that returned `false` to `shouldUpdate`, and trigger an update. I dramatically increased the documentation around affected code to make these kinds of issues more clear to us in the future. --- src/createContext.lua | 114 +++++++++++++++++-------- src/createContext.spec.lua | 167 ++++++++++++++++++++++++++++++++++++- 2 files changed, 245 insertions(+), 36 deletions(-) diff --git a/src/createContext.lua b/src/createContext.lua index 260f8a4f..1d364752 100644 --- a/src/createContext.lua +++ b/src/createContext.lua @@ -1,22 +1,50 @@ local Symbol = require(script.Parent.Symbol) -local Binding = require(script.Parent.Binding) local createFragment = require(script.Parent.createFragment) +local createSignal = require(script.Parent.createSignal) local Children = require(script.Parent.PropMarkers.Children) local Component = require(script.Parent.Component) +--[[ + Construct the value that is assigned to Roact's context storage. +]] +local function createContextEntry(currentValue) + return { + value = currentValue, + onUpdate = createSignal(), + } +end + local function createProvider(context) local Provider = Component:extend("Provider") function Provider:init(props) - self.binding, self.updateValue = Binding.create(props.value) + self.contextEntry = createContextEntry(props.value) + self:__addContext(context.key, self.contextEntry) + end - local key = context.key - self:__addContext(key, self.binding) + function Provider:willUpdate(nextProps) + -- If the provided value changed, immediately update the context entry. + -- + -- During this update, any components that are reachable will receive + -- this updated value at the same time as any props and state updates + -- that are being applied. + if nextProps.value ~= self.props.value then + self.contextEntry.value = nextProps.value + end end function Provider:didUpdate(prevProps) + -- If the provided value changed, after we've updated every reachable + -- component, fire a signal to update the rest. + -- + -- This signal will notify all context consumers. It's expected that + -- they will compare the last context value they updated with and only + -- trigger an update on themselves if this value is different. + -- + -- This codepath will generally only update consumer components that has + -- a component implementing shouldUpdate between them and the provider. if prevProps.value ~= self.props.value then - self.updateValue(self.props.value) + self.contextEntry.onUpdate:fire(self.props.value) end end @@ -30,29 +58,6 @@ end local function createConsumer(context) local Consumer = Component:extend("Consumer") - function Consumer:init(props) - local key = context.key - local binding = self:__getContext(key) - - if binding ~= nil then - self.state = { - value = binding:getValue(), - } - - -- Update if the Context updated - self.disconnect = Binding.subscribe(binding, function() - self:setState({ - value = binding:getValue(), - }) - end) - else - -- Fall back to the default value if no Provider exists - self.state = { - value = context.defaultValue, - } - end - end - function Consumer.validateProps(props) if type(props.render) ~= "function" then return false, "Consumer expects a `render` function" @@ -61,8 +66,52 @@ local function createConsumer(context) end end + function Consumer:init(props) + -- This value may be nil, which indicates that our consumer is not a + -- descendant of a provider for this context item. + self.contextEntry = self:__getContext(context.key) + end + function Consumer:render() - return self.props.render(self.state.value) + -- Render using the latest available for this context item. + -- + -- We don't store this value in state in order to have more fine-grained + -- control over our update behavior. + local value + if self.contextEntry ~= nil then + value = self.contextEntry.value + else + value = context.defaultValue + end + + return self.props.render(value) + end + + function Consumer:didUpdate() + -- Store the value that we most recently updated with. + -- + -- This value is compared in the contextEntry onUpdate hook below. + self.lastValue = self.contextEntry.value + end + + function Consumer:didMount() + if self.contextEntry ~= nil then + -- When onUpdate is fired, a new value has been made available in + -- this context entry, but we may have already updated in the same + -- update cycle. + -- + -- To avoid sending a redundant update, we compare the new value + -- with the last value that we updated with (set in didUpdate) and + -- only update if they differ. This may happen when an update from a + -- provider was blocked by an intermediate component that returned + -- false from shouldUpdate. + self.disconnect = self.contextEntry.onUpdate:subscribe(function(newValue) + if newValue ~= self.lastValue then + -- Trigger a dummy state update. + self:setState({}) + end + end) + end end function Consumer:willUnmount() @@ -78,12 +127,10 @@ local Context = {} Context.__index = Context function Context.new(defaultValue) - local self = { + return setmetatable({ defaultValue = defaultValue, key = Symbol.named("ContextKey"), - } - setmetatable(self, Context) - return self + }, Context) end function Context:__tostring() @@ -92,6 +139,7 @@ end local function createContext(defaultValue) local context = Context.new(defaultValue) + return { Provider = createProvider(context), Consumer = createConsumer(context), diff --git a/src/createContext.spec.lua b/src/createContext.spec.lua index ad365492..dee4f760 100644 --- a/src/createContext.spec.lua +++ b/src/createContext.spec.lua @@ -1,7 +1,10 @@ return function() + local Component = require(script.Parent.Component) + local NoopRenderer = require(script.Parent.NoopRenderer) + local Children = require(script.Parent.PropMarkers.Children) local createContext = require(script.Parent.createContext) local createElement = require(script.Parent.createElement) - local NoopRenderer = require(script.Parent.NoopRenderer) + local createFragment = require(script.Parent.createFragment) local createReconciler = require(script.Parent.createReconciler) local createSpy = require(script.Parent.createSpy) @@ -113,10 +116,168 @@ return function() Listener = createElement(Listener), })) - expect(valueSpy.callCount).to.equal(3) + expect(valueSpy.callCount).to.equal(2) + valueSpy:assertCalledWith("ThirdTest") + + noopReconciler.unmountVirtualTree(tree) + end) + + --[[ + This test is the same as the one above, but with a component that + always blocks updates in the middle. We expect behavior to be the + same. + ]] + it("should update when the value updates through an update blocking component", function() + local valueSpy = createSpy() + local context = createContext("Test") + + local UpdateBlocker = Component:extend("UpdateBlocker") + + function UpdateBlocker:render() + return createFragment(self.props[Children]) + end + + function UpdateBlocker:shouldUpdate() + return false + end + + local function Listener() + return createElement(context.Consumer, { + render = valueSpy.value, + }) + end + + local element = createElement(context.Provider, { + value = "NewTest", + }, { + Blocker = createElement(UpdateBlocker, nil, { + Listener = createElement(Listener), + }), + }) + + local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree") + + expect(valueSpy.callCount).to.equal(1) + valueSpy:assertCalledWith("NewTest") + + noopReconciler.updateVirtualTree(tree, createElement(context.Provider, { + value = "ThirdTest", + }, { + Blocker = createElement(UpdateBlocker, nil, { + Listener = createElement(Listener), + }), + })) + + expect(valueSpy.callCount).to.equal(2) valueSpy:assertCalledWith("ThirdTest") noopReconciler.unmountVirtualTree(tree) end) end) -end + + describe("Update order", function() + --[[ + This test ensures that there is no scenario where we can observe + 'update tearing' when props and context are updated at the same + time. + + Update tearing is scenario where a single update is partially + applied in multiple steps instead of atomically. This is observable + by components and can lead to strange bugs or errors. + + This instance of update tearing happens when updating a prop and a + context value in the same update. Image we represent our tree's + state as the current prop and context versions. Our initial state + is: + + (prop_1, context_1) + + The next state we would like to update to is: + + (prop_2, context_2) + + Under the bug reported in issue 259, Roact reaches three different + states in sequence: + + 1: (prop_1, context_1) - the initial state + 2: (prop_2, context_1) - woops! + 3: (prop_2, context_2) - correct end state + + In state 2, a user component was added that tried to access the + current context value, which was not set at the time. This raised an + error, because this state is not valid! + + The first proposed solution was to move the context update to happen + before the props update. It is easy to show that this will still + result in update tearing: + + 1: (prop_1, context_1) + 2: (prop_1, context_2) + 3: (prop_2, context_2) + + Although the initial concern about newly added components observing + old context values is fixed, there is still a state + desynchronization between props and state. + + We would instead like the following update sequence: + + 1: (prop_1, context_1) + 2: (prop_2, context_2) + + This test tries to ensure that is the case. + + The initial bug report is here: + https://github.com/Roblox/roact/issues/259 + ]] + it("should update context at the same time as props", function() + -- These values are used to make sure we reach both the first and + -- second state combinations we want to visit. + local observedA = false + local observedB = false + local updateCount = 0 + + local context = createContext("default") + + local function Listener(props) + return createElement(context.Consumer, { + render = function(value) + updateCount = updateCount + 1 + + if value == "context_1" then + expect(props.someProp).to.equal("prop_1") + observedA = true + elseif value == "context_2" then + expect(props.someProp).to.equal("prop_2") + observedB = true + else + error("Unexpected context value") + end + end, + }) + end + + local element1 = createElement(context.Provider, { + value = "context_1", + }, { + Child = createElement(Listener, { + someProp = "prop_1", + }), + }) + + local element2 = createElement(context.Provider, { + value = "context_2", + }, { + Child = createElement(Listener, { + someProp = "prop_2", + }), + }) + + local tree = noopReconciler.mountVirtualTree(element1, nil, "UpdateObservationIsFun") + noopReconciler.updateVirtualTree(tree, element2) + + expect(updateCount).to.equal(2) + expect(observedA).to.equal(true) + expect(observedB).to.equal(true) + end) + end) +end \ No newline at end of file From f55538b529f59683eec1ee75bd8735bb6fdefc98 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Wed, 25 Mar 2020 15:44:35 -0700 Subject: [PATCH 34/65] Remove Travis-CI (#261) --- .github/workflows/ci.yml | 12 ++++++------ .travis.yml | 32 -------------------------------- README.md | 12 +++--------- 3 files changed, 9 insertions(+), 47 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c7c25b8..ee5bc83c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,17 +28,17 @@ jobs: run: | luarocks install luafilesystem luarocks install luacov - luarocks install luacov-coveralls --server=http://rocks.moonscript.org/dev + luarocks install luacov-reporter-lcov luarocks install luacheck - name: Test run: | lua -lluacov bin/spec.lua luacheck src benchmarks examples + luacov -r lcov - # luacov-coveralls default settings do not function on GitHub Actions. - # We need to pass different service name and repo token explicitly - name: Report to Coveralls - run: luacov-coveralls --repo-token $REPO_TOKEN --service-name github - env: - REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + uses: coverallsapp/github-action@v1.0.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: luacov.report.out \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a64ade5c..00000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -# http://kiki.to/blog/2016/02/04/talk-continuous-integration-with-lua/ -language: python -sudo: false - -branches: - only: - - master - -env: - - LUA="lua=5.1" - -before_install: - - pip install hererocks - - hererocks lua_install -r^ --$LUA - - export PATH=$PATH:$PWD/lua_install/bin - -install: - - luarocks install luafilesystem - - luarocks install busted - - luarocks install luacov - - luarocks install luacov-coveralls - - luarocks install luacheck - -script: - - luacheck src benchmarks examples - - lua -lluacov bin/spec.lua - -extra_css: - - extra.css - -after_success: - - luacov-coveralls -e $TRAVIS_BUILD_DIR/lua_install \ No newline at end of file diff --git a/README.md b/README.md index 94c2db2f..7015bb51 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,8 @@

Roact

- - Travis-CI Build Status - - - Coveralls Coverage - - - Documentation - + GitHub Actions Build Status + Coveralls Coverage + Documentation
From cd22f9c4f74f4eaa70a2686813c3f723ff74fc8b Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Thu, 23 Apr 2020 11:31:05 -0700 Subject: [PATCH 35/65] Fix missing nil check in context logic (#267) * Introduce breaking test * Add fix --- src/createContext.lua | 4 +++- src/createContext.spec.lua | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/createContext.lua b/src/createContext.lua index 1d364752..b21635ef 100644 --- a/src/createContext.lua +++ b/src/createContext.lua @@ -91,7 +91,9 @@ local function createConsumer(context) -- Store the value that we most recently updated with. -- -- This value is compared in the contextEntry onUpdate hook below. - self.lastValue = self.contextEntry.value + if self.contextEntry ~= nil then + self.lastValue = self.contextEntry.value + end end function Consumer:didMount() diff --git a/src/createContext.spec.lua b/src/createContext.spec.lua index dee4f760..432d39d4 100644 --- a/src/createContext.spec.lua +++ b/src/createContext.spec.lua @@ -173,6 +173,27 @@ return function() noopReconciler.unmountVirtualTree(tree) end) + + it("should behave correctly when the default value is nil", function() + local context = createContext(nil) + + local valueSpy = createSpy() + local function Listener() + return createElement(context.Consumer, { + render = valueSpy.value, + }) + end + + local tree = noopReconciler.mountVirtualTree(createElement(Listener), nil, "Provide Tree") + expect(valueSpy.callCount).to.equal(1) + valueSpy:assertCalledWith(nil) + + tree = noopReconciler.updateVirtualTree(tree, createElement(Listener)) + noopReconciler.unmountVirtualTree(tree) + + expect(valueSpy.callCount).to.equal(2) + valueSpy:assertCalledWith(nil) + end) end) describe("Update order", function() From 58af06b996061dd3ab0696fbe530e4b5070a35c4 Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Mon, 11 May 2020 13:22:50 -0700 Subject: [PATCH 36/65] Release v1.3.0 (#270) --- CHANGELOG.md | 2 ++ docs/advanced/context.md | 5 +---- docs/api-reference.md | 5 +---- rotriever.toml | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2bdfc54..3be762f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Roact Changelog ## Unreleased Changes + +## [1.3.0](https://github.com/Roblox/roact/releases/tag/v1.3.0) (May 5th, 2020) * Added Contexts, which enables easy handling of items that are provided and consumed throughout the tree. ## [1.2.0](https://github.com/Roblox/roact/releases/tag/v1.2.0) (September 6th, 2019) diff --git a/docs/advanced/context.md b/docs/advanced/context.md index 71ec0c9b..6308f3b9 100644 --- a/docs/advanced/context.md +++ b/docs/advanced/context.md @@ -1,7 +1,4 @@ -!!! danger "Unreleased API" - This API is not yet available in a stable Roact release. - - It may be available from a recent pre-release or Roact's master branch. +!!! success "Added in Roact 1.3.0" [TOC] diff --git a/docs/api-reference.md b/docs/api-reference.md index 3274204f..a5fd2f68 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -181,10 +181,7 @@ Creates a new reference object that can be used with [Roact.Ref](#roactref). ### Roact.createContext -!!! danger "Unreleased API" - This API is not yet available in a stable Roact release. - - It may be available from a recent pre-release or Roact's master branch. +!!! success "Added in Roact 1.3.0" ``` Roact.createContext(defaultValue: any) -> RoactContext diff --git a/rotriever.toml b/rotriever.toml index 058e40e9..09364c80 100644 --- a/rotriever.toml +++ b/rotriever.toml @@ -3,4 +3,4 @@ name = "roblox/roact" author = "Roblox" license = "Apache-2.0" content_root = "src" -version = "1.2.0" \ No newline at end of file +version = "1.3.0" \ No newline at end of file From f1840cfc5d010baf389ec17c1a82fd45c052f70f Mon Sep 17 00:00:00 2001 From: cliffchapmanrbx <47404136+cliffchapmanrbx@users.noreply.github.com> Date: Thu, 11 Jun 2020 16:14:42 -0700 Subject: [PATCH 37/65] Enable CLA bot (#274) * Enable CLA bot Adds workflow for CLA bot. See http://go/clabot for more details. * Add MisterUncloaked to whitelist * Add matthargett to whitelist --- .github/workflows/clabot.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/clabot.yml diff --git a/.github/workflows/clabot.yml b/.github/workflows/clabot.yml new file mode 100644 index 00000000..2da38dd7 --- /dev/null +++ b/.github/workflows/clabot.yml @@ -0,0 +1,21 @@ +name: "CLA Signature Bot" +on: + issue_comment: + types: [created] + pull_request: + types: [opened,closed,synchronize] + +jobs: + clabot: + runs-on: ubuntu-latest + steps: + - name: "CLA Signature Bot" + uses: roblox/cla-signature-bot@v2.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + whitelist: "LPGhatguy,ZoteTheMighty,cliffchapmanrbx,MisterUncloaked,matthargett" + use-remote-repo: true + remote-repo-name: "roblox/cla-bot-store" + remote-repo-pat: ${{ secrets.CLA_REMOTE_REPO_PAT }} + url-to-cladocument: "https://roblox.github.io/cla-bot-store/" From 10e7060750fd7b44880933ababd8eb00e687c95e Mon Sep 17 00:00:00 2001 From: cliffchapmanrbx <47404136+cliffchapmanrbx@users.noreply.github.com> Date: Thu, 11 Jun 2020 23:54:18 -0700 Subject: [PATCH 38/65] CLA bot to version 2.0.1 --- .github/workflows/clabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clabot.yml b/.github/workflows/clabot.yml index 2da38dd7..133585fe 100644 --- a/.github/workflows/clabot.yml +++ b/.github/workflows/clabot.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "CLA Signature Bot" - uses: roblox/cla-signature-bot@v2.0.0 + uses: roblox/cla-signature-bot@v2.0.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 60197ac8789840956c364edcf464c460d14cd092 Mon Sep 17 00:00:00 2001 From: Conor Griffin Date: Mon, 15 Jun 2020 10:41:20 -0700 Subject: [PATCH 39/65] Add component name to property validation error message (#275) * Add component name to property validation error message * Add changelog entry * Add test for component name in error message * Update test to only pass one value to expect * Add ConorGriffin37 to cla whitelist * Update CHANGELOG.md Co-authored-by: Lucien Greathouse Co-authored-by: Lucien Greathouse --- .github/workflows/clabot.yml | 2 +- CHANGELOG.md | 2 ++ src/Component.lua | 3 ++- src/Component.spec/validateProps.spec.lua | 29 +++++++++++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.github/workflows/clabot.yml b/.github/workflows/clabot.yml index 133585fe..fdc1f9d2 100644 --- a/.github/workflows/clabot.yml +++ b/.github/workflows/clabot.yml @@ -14,7 +14,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - whitelist: "LPGhatguy,ZoteTheMighty,cliffchapmanrbx,MisterUncloaked,matthargett" + whitelist: "LPGhatguy,ZoteTheMighty,cliffchapmanrbx,MisterUncloaked,matthargett,ConorGriffin37" use-remote-repo: true remote-repo-name: "roblox/cla-bot-store" remote-repo-pat: ${{ secrets.CLA_REMOTE_REPO_PAT }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3be762f8..713275f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased Changes +* Added component name to property validation error message ([#275](https://github.com/Roblox/roact/pull/275)) + ## [1.3.0](https://github.com/Roblox/roact/releases/tag/v1.3.0) (May 5th, 2020) * Added Contexts, which enables easy handling of items that are provided and consumed throughout the tree. diff --git a/src/Component.lua b/src/Component.lua index 1dfb5fb7..12833745 100644 --- a/src/Component.lua +++ b/src/Component.lua @@ -265,7 +265,8 @@ function Component:__validateProps(props) if not success then failureReason = failureReason or "" - error(("Property validation failed: %s\n\n%s"):format( + error(("Property validation failed in %s: %s\n\n%s"):format( + self.__componentName, tostring(failureReason), self:getElementTraceback() or ""), 0) diff --git a/src/Component.spec/validateProps.spec.lua b/src/Component.spec/validateProps.spec.lua index bcc8abbf..c7278e0f 100644 --- a/src/Component.spec/validateProps.spec.lua +++ b/src/Component.spec/validateProps.spec.lua @@ -164,6 +164,35 @@ return function() end) end) + it("should include the component name in the error message", function() + local config = { + propValidation = true, + } + + GlobalConfig.scoped(config, function() + local MyComponent = Component:extend("MyComponent") + MyComponent.validateProps = function() + return false + end + + function MyComponent:render() + return nil + end + + local element = createElement(MyComponent) + local hostParent = nil + local key = "Test" + + local success, error = pcall(function() + noopReconciler.mountVirtualNode(element, hostParent, key) + end) + + expect(success).to.equal(false) + local startIndex = error:find("MyComponent") + expect(startIndex).to.be.ok() + end) + end) + it("should be invoked after defaultProps are applied", function() local config = { propValidation = true, From 8f415a5bdc87d8dff0649203ad683e1e2ae03a69 Mon Sep 17 00:00:00 2001 From: Jayden Charbonneau Date: Sun, 28 Jun 2020 00:41:31 -0400 Subject: [PATCH 40/65] Fix typo in info box (#281) --- docs/guide/hello-roact.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/hello-roact.md b/docs/guide/hello-roact.md index b1d5b757..34f9a6ce 100644 --- a/docs/guide/hello-roact.md +++ b/docs/guide/hello-roact.md @@ -1,5 +1,5 @@ !!! info - These examples asssumes that you've successfully [installed Roact](installation.md) into `ReplicatedStorage`! + These examples assumes that you've successfully [installed Roact](installation.md) into `ReplicatedStorage`! Add a new `LocalScript` object to `StarterPlayer.StarterPlayerScripts` either in Roblox Studio, or via Rojo: @@ -19,4 +19,4 @@ local app = Roact.createElement("ScreenGui", {}, { Roact.mount(app, Players.LocalPlayer.PlayerGui) ``` -When you run your game, you should see a large gray label with the phrase 'Hello, Roact!' appear on screen! \ No newline at end of file +When you run your game, you should see a large gray label with the phrase 'Hello, Roact!' appear on screen! From 380c3d652f41d896d2f5ebcc128aeed99d176184 Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Thu, 19 Nov 2020 11:50:55 -0800 Subject: [PATCH 41/65] Create patch release 1.3.1 (#287) This includes a small fix to error messages thrown from prop validation. (also fixes CI) --- .github/workflows/ci.yml | 4 ++-- CHANGELOG.md | 1 + rotriever.toml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee5bc83c..69334fd4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,11 +18,11 @@ jobs: with: submodules: true - - uses: leafo/gh-actions-lua@v5 + - uses: leafo/gh-actions-lua@v8 with: luaVersion: "5.1" - - uses: leafo/gh-actions-luarocks@v2 + - uses: leafo/gh-actions-luarocks@v4 - name: Install dependencies run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 713275f5..00680016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased Changes +## [1.3.1](https://github.com/Roblox/roact/releases/tag/v1.3.0) (November 19th, 2020) * Added component name to property validation error message ([#275](https://github.com/Roblox/roact/pull/275)) ## [1.3.0](https://github.com/Roblox/roact/releases/tag/v1.3.0) (May 5th, 2020) diff --git a/rotriever.toml b/rotriever.toml index 09364c80..fd1bc7ad 100644 --- a/rotriever.toml +++ b/rotriever.toml @@ -3,4 +3,4 @@ name = "roblox/roact" author = "Roblox" license = "Apache-2.0" content_root = "src" -version = "1.3.0" \ No newline at end of file +version = "1.3.1" \ No newline at end of file From a5a2ac6fd0544d9bbe05c9ad8b000e2dd70f5ec5 Mon Sep 17 00:00:00 2001 From: Nickuhhh Date: Thu, 17 Dec 2020 18:02:41 -0500 Subject: [PATCH 42/65] Dark mode for documentation (#290) Documentation uses user preference to determine color scheme --- CHANGELOG.md | 1 + docs/extra.css | 2 +- mkdocs.yml | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00680016..8164af38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Roact Changelog ## Unreleased Changes +* Added color schemes for documentation based on user preference ([#290](https://github.com/Roblox/roact/pull/290)). ## [1.3.1](https://github.com/Roblox/roact/releases/tag/v1.3.0) (November 19th, 2020) * Added component name to property validation error message ([#275](https://github.com/Roblox/roact/pull/275)) diff --git a/docs/extra.css b/docs/extra.css index 8c8f8ce5..79bf679a 100644 --- a/docs/extra.css +++ b/docs/extra.css @@ -1,3 +1,3 @@ .md-typeset hr { border-bottom: 2px solid rgba(0, 0, 0, 0.15); -} \ No newline at end of file +} diff --git a/mkdocs.yml b/mkdocs.yml index 94430efe..aa7cef7f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,7 @@ theme: palette: primary: 'Light Blue' accent: 'Light Blue' + scheme: preference nav: - Home: index.md @@ -37,4 +38,4 @@ markdown_extensions: guess_lang: false - toc: permalink: true - - pymdownx.superfences \ No newline at end of file + - pymdownx.superfences From f6dd2b2af8986cddeb523e5fb9e2303db3c345c3 Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Thu, 17 Dec 2020 17:34:32 -0800 Subject: [PATCH 43/65] Update docs deploy to use a newer GH action (#291) The newer version of the action should deploy with a newer version of mkdocs-material, which should support the color scheme change in #290 --- .github/workflows/deploy-docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 4293fcee..7962ca11 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -14,6 +14,6 @@ jobs: uses: actions/checkout@v2 - name: Deploy docs - uses: mhausenblas/mkdocs-deploy-gh-pages@1.11 + uses: mhausenblas/mkdocs-deploy-gh-pages@1.16 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 9b94828ba5b122e17eb1b7dfffdd30aa66ddf666 Mon Sep 17 00:00:00 2001 From: Water Date: Wed, 24 Feb 2021 03:41:33 +1000 Subject: [PATCH 44/65] Update Installation Page (#296) https://github.com/Roblox/roact/issues/295 --- docs/guide/installation.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 348e98de..2849b7b8 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -3,10 +3,10 @@ There are two supported ways to get started with Roact. For our examples, we'll install `Roact` to `ReplicatedStorage`. In practice, it's okay to install Roact anywhere you want! ### Method 1: Model File (Roblox Studio) -* Download the `rbxmx` model file attached to the latest release from the [GitHub releases page](https://github.com/Roblox/Roact/releases). +* Download the `rbxm` model file attached to the latest release from the [GitHub releases page](https://github.com/Roblox/Roact/releases). * Insert the model into Studio into a place like `ReplicatedStorage` ### Method 2: Filesystem -* Copy the `lib` directory into your codebase +* Copy the `src` directory into your codebase * Rename the folder to `Roact` -* Use a plugin like [Rojo](https://github.com/LPGhatguy/rojo) to sync the files into a place \ No newline at end of file +* Use a plugin like [Rojo](https://github.com/LPGhatguy/rojo) to sync the files into a place From 15ca096ac4fbe6901233a9c22fd90e744915e810 Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Thu, 6 May 2021 17:21:43 -0700 Subject: [PATCH 45/65] Try bumping coveralls and checkout actions (#302) --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69334fd4..bd746a1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 with: submodules: true @@ -38,7 +38,7 @@ jobs: luacov -r lcov - name: Report to Coveralls - uses: coverallsapp/github-action@v1.0.1 + uses: coverallsapp/github-action@v1.1.2 with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: luacov.report.out \ No newline at end of file From b685faf65e328beaec74f053736f7eacf33c8d29 Mon Sep 17 00:00:00 2001 From: LoganDark Date: Fri, 7 May 2021 10:12:33 -0700 Subject: [PATCH 46/65] Fix usage of error() in createReconciler.lua (#297) * Fix usage of error() in createReconciler.lua * Update CHANGELOG.md --- CHANGELOG.md | 1 + src/createReconciler.lua | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8164af38..cd8338b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased Changes * Added color schemes for documentation based on user preference ([#290](https://github.com/Roblox/roact/pull/290)). +* Fixed stack trace level when throwing an error in `createReconciler` ([#297](https://github.com/Roblox/roact/pull/297)). ## [1.3.1](https://github.com/Roblox/roact/releases/tag/v1.3.0) (November 19th, 2020) * Added component name to property validation error message ([#275](https://github.com/Roblox/roact/pull/275)) diff --git a/src/createReconciler.lua b/src/createReconciler.lua index e4e43c62..928b9832 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -152,7 +152,7 @@ local function createReconciler(renderer) unmountVirtualNode(childNode) end else - error(("Unknown ElementKind %q"):format(tostring(kind), 2)) + error(("Unknown ElementKind %q"):format(tostring(kind)), 2) end end @@ -241,7 +241,7 @@ local function createReconciler(renderer) elseif kind == ElementKind.Fragment then virtualNode = updateFragmentVirtualNode(virtualNode, newElement) else - error(("Unknown ElementKind %q"):format(tostring(kind), 2)) + error(("Unknown ElementKind %q"):format(tostring(kind)), 2) end -- Stateful components can abort updates via shouldUpdate. If that @@ -366,7 +366,7 @@ local function createReconciler(renderer) elseif kind == ElementKind.Fragment then mountFragmentVirtualNode(virtualNode) else - error(("Unknown ElementKind %q"):format(tostring(kind), 2)) + error(("Unknown ElementKind %q"):format(tostring(kind)), 2) end return virtualNode From 52de074051d2f59f4afcc941c4d9e00512ba6918 Mon Sep 17 00:00:00 2001 From: yjia2 <77131672+yjia2@users.noreply.github.com> Date: Mon, 10 May 2021 17:28:47 -0700 Subject: [PATCH 47/65] Add yjia2 to CLA whitelist --- .github/workflows/clabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clabot.yml b/.github/workflows/clabot.yml index fdc1f9d2..e61c7763 100644 --- a/.github/workflows/clabot.yml +++ b/.github/workflows/clabot.yml @@ -14,7 +14,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - whitelist: "LPGhatguy,ZoteTheMighty,cliffchapmanrbx,MisterUncloaked,matthargett,ConorGriffin37" + whitelist: "LPGhatguy,ZoteTheMighty,cliffchapmanrbx,MisterUncloaked,matthargett,ConorGriffin37,yjia2" use-remote-repo: true remote-repo-name: "roblox/cla-bot-store" remote-repo-pat: ${{ secrets.CLA_REMOTE_REPO_PAT }} From 8312f84ee96c2f4703183999d8a02e111c4668bd Mon Sep 17 00:00:00 2001 From: yjia2 <77131672+yjia2@users.noreply.github.com> Date: Thu, 13 May 2021 12:20:44 -0700 Subject: [PATCH 48/65] Optimize createSignal memory usage (#304) * optimize the createSignal implementation to consume less memory --- CHANGELOG.md | 1 + src/createSignal.lua | 49 +++++++++++++---------------- src/createSignal.spec.lua | 65 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd8338b1..d79ca119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased Changes * Added color schemes for documentation based on user preference ([#290](https://github.com/Roblox/roact/pull/290)). * Fixed stack trace level when throwing an error in `createReconciler` ([#297](https://github.com/Roblox/roact/pull/297)). +* Optimized the memory usage of 'createSignal' implementation. ([#304](https://github.com/Roblox/roact/pull/304)) ## [1.3.1](https://github.com/Roblox/roact/releases/tag/v1.3.0) (November 19th, 2020) * Added component name to property validation error message ([#275](https://github.com/Roblox/roact/pull/275)) diff --git a/src/createSignal.lua b/src/createSignal.lua index 3db6354c..f3e0add2 100644 --- a/src/createSignal.lua +++ b/src/createSignal.lua @@ -12,58 +12,51 @@ disconnect() ]] -local function addToMap(map, addKey, addValue) - local new = {} - - for key, value in pairs(map) do - new[key] = value - end - - new[addKey] = addValue - - return new -end - -local function removeFromMap(map, removeKey) - local new = {} - - for key, value in pairs(map) do - if key ~= removeKey then - new[key] = value - end - end - - return new -end - local function createSignal() local connections = {} + local suspendedConnections = {} + local firing = false local function subscribe(self, callback) assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") local connection = { callback = callback, + disconnected = false, } - connections = addToMap(connections, callback, connection) + -- If the callback is already registered, don't add to the suspendedConnection. Otherwise, this will disable + -- the existing one. + if firing and not connections[callback] then + suspendedConnections[callback] = connection + end + + connections[callback] = connection local function disconnect() assert(not connection.disconnected, "Listeners can only be disconnected once.") connection.disconnected = true - connections = removeFromMap(connections, callback) + connections[callback] = nil + suspendedConnections[callback] = nil end return disconnect end local function fire(self, ...) + firing = true for callback, connection in pairs(connections) do - if not connection.disconnected then + if not connection.disconnected and not suspendedConnections[callback] then callback(...) end end + + firing = false + + for callback, _ in pairs(suspendedConnections) do + suspendedConnections[callback] = nil + end end return { @@ -72,4 +65,4 @@ local function createSignal() } end -return createSignal \ No newline at end of file +return createSignal diff --git a/src/createSignal.spec.lua b/src/createSignal.spec.lua index ed9cf977..822df8d4 100644 --- a/src/createSignal.spec.lua +++ b/src/createSignal.spec.lua @@ -86,4 +86,69 @@ return function() -- Exactly once listener should have been called. expect(spyA.callCount + spyB.callCount).to.equal(1) end) + + it("should allow adding listener in the middle of firing", function() + local signal = createSignal() + + local disconnectA + local spyA = createSpy() + local listener = function(a, b) + disconnectA = signal:subscribe(spyA.value) + end + + local disconnectListener = signal:subscribe(listener) + + expect(spyA.callCount).to.equal(0) + + local a = {} + local b = 67 + signal:fire(a, b) + + expect(spyA.callCount).to.equal(0) + + -- The new listener should be picked up in next fire. + signal:fire(b, a) + expect(spyA.callCount).to.equal(1) + spyA:assertCalledWith(b, a) + + disconnectA() + disconnectListener() + + signal:fire(a) + + expect(spyA.callCount).to.equal(1) + end) + + it("should have one connection instance when add the same listener multiple times", function() + local signal = createSignal() + + local spyA = createSpy() + local disconnect1 = signal:subscribe(spyA.value) + + expect(spyA.callCount).to.equal(0) + + local a = {} + local b = 67 + signal:fire(a, b) + + expect(spyA.callCount).to.equal(1) + spyA:assertCalledWith(a, b) + + local disconnect2 = signal:subscribe(spyA.value) + + signal:fire(b, a) + expect(spyA.callCount).to.equal(2) + spyA:assertCalledWith(b, a) + + disconnect2() + + signal:fire(a) + + expect(spyA.callCount).to.equal(2) + + -- should have no effect. + disconnect1() + signal:fire(a) + expect(spyA.callCount).to.equal(2) + end) end \ No newline at end of file From dcbeb36e3470f82e57477201fbd4ae081ad60034 Mon Sep 17 00:00:00 2001 From: Conor Griffin Date: Tue, 25 May 2021 18:39:09 +0100 Subject: [PATCH 49/65] Fix updateChildren re-rentrancy issue (#301) In certain circumstances it is possible for updateChildren in createReconicler to be re-rendered for a virtualNode while it is already happening causing two Roblox instances to be created for the same element. For an example of how this happens, see the added unit test. What happens in the test is: The components are rendered the first time and ChildComponent renders a Frame In the spawn in ChildComponent:didMount, firstTime is set to false ChildComponent's Frame is unmounted and a new TextLabel is added The DescendantAdded event is processed ChildComponent rerenders, the old virtualNode for the Frame is still in it's children. ChildComponent's Frame is unmounted again and a second TextLabel is added This example is kind of a contrived, but this did happen in the UIBlox GridView but with UI events such as AbsoluteContentSize Changed instead of DescentantAdded. Solution Suspend the events of parent nodes when updating a component outside of the standard lifecycle. Suspending the parent events means that no change we make can cause a parent node to reconcile while we are also reconciling. --- CHANGELOG.md | 2 + src/Component.lua | 13 ++++++ src/Config.lua | 4 ++ src/RobloxRenderer.spec.lua | 80 +++++++++++++++++++++++++++++++++++++ src/createReconciler.lua | 29 ++++++++++++++ 5 files changed, 128 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d79ca119..16fe6dcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Roact Changelog ## Unreleased Changes +* Fixed a bug where the Roact tree could get into a broken state when processing changes to child instances outside the standard lifecycle. + This change is behind the config value tempFixUpdateChildrenReEntrancy ([#301](https://github.com/Roblox/roact/pull/301)) * Added color schemes for documentation based on user preference ([#290](https://github.com/Roblox/roact/pull/290)). * Fixed stack trace level when throwing an error in `createReconciler` ([#297](https://github.com/Roblox/roact/pull/297)). * Optimized the memory usage of 'createSignal' implementation. ([#304](https://github.com/Roblox/roact/pull/304)) diff --git a/src/Component.lua b/src/Component.lua index 12833745..7c6b62a6 100644 --- a/src/Component.lua +++ b/src/Component.lua @@ -157,9 +157,22 @@ function Component:setState(mapState) internalData.pendingState = assign(newState, derivedState) elseif lifecyclePhase == ComponentLifecyclePhase.Idle then + -- Pause parent events when we are updated outside of our lifecycle + -- If these events are not paused, our setState can cause a component higher up the + -- tree to rerender based on events caused by our component while this reconciliation is happening. + -- This could cause the tree to become invalid. + local virtualNode = internalData.virtualNode + local reconciler = internalData.reconciler + if config.tempFixUpdateChildrenReEntrancy then + reconciler.suspendParentEvents(virtualNode) + end + -- Outside of our lifecycle, the state update is safe to make immediately self:__update(nil, newState) + if config.tempFixUpdateChildrenReEntrancy then + reconciler.resumeParentEvents(virtualNode) + end else local messageTemplate = invalidSetStateMessages.default diff --git a/src/Config.lua b/src/Config.lua index 6884bc47..54043503 100644 --- a/src/Config.lua +++ b/src/Config.lua @@ -22,6 +22,10 @@ local defaultConfig = { ["elementTracing"] = false, -- Enables validation of component props in stateful components. ["propValidation"] = false, + + -- Temporary config for enabling a bug fix for processing events based on updates to child instances + -- outside of the standard lifecycle. + ["tempFixUpdateChildrenReEntrancy"] = false, } -- Build a list of valid configuration values up for debug messages. diff --git a/src/RobloxRenderer.spec.lua b/src/RobloxRenderer.spec.lua index 1ea04cb2..9e8934ac 100644 --- a/src/RobloxRenderer.spec.lua +++ b/src/RobloxRenderer.spec.lua @@ -11,6 +11,7 @@ return function() local GlobalConfig = require(script.Parent.GlobalConfig) local Portal = require(script.Parent.Portal) local Ref = require(script.Parent.PropMarkers.Ref) + local Event = require(script.Parent.PropMarkers.Event) local RobloxRenderer = require(script.Parent.RobloxRenderer) @@ -946,4 +947,83 @@ return function() }) end) end) + + + describe("Integration Tests", function() + it("should not allow re-entrancy in updateChildren", function() + local configValues = { + tempFixUpdateChildrenReEntrancy = true, + } + + GlobalConfig.scoped(configValues, function() + local ChildComponent = Component:extend("ChildComponent") + + function ChildComponent:init() + self:setState({ + firstTime = true + }) + end + + local childCoroutine + + function ChildComponent:render() + if self.state.firstTime then + return createElement("Frame") + end + + return createElement("TextLabel") + end + + function ChildComponent:didMount() + childCoroutine = coroutine.create(function() + self:setState({ + firstTime = false + }) + end) + end + + local ParentComponent = Component:extend("ParentComponent") + + function ParentComponent:init() + self:setState({ + count = 1 + }) + + self.childAdded = function() + self:setState({ + count = self.state.count + 1, + }) + end + end + + function ParentComponent:render() + return createElement("Frame", { + [Event.ChildAdded] = self.childAdded, + }, { + ChildComponent = createElement(ChildComponent, { + count = self.state.count + }) + }) + end + + local parent = Instance.new("ScreenGui") + parent.Parent = game.CoreGui + + local tree = createElement(ParentComponent) + + local hostKey = "Some Key" + local instance = reconciler.mountVirtualNode(tree, parent, hostKey) + + coroutine.resume(childCoroutine) + + expect(#parent:GetChildren()).to.equal(1) + + local frame = parent:GetChildren()[1] + + expect(#frame:GetChildren()).to.equal(1) + + reconciler.unmountVirtualNode(instance) + end) + end) + end) end \ No newline at end of file diff --git a/src/createReconciler.lua b/src/createReconciler.lua index 928b9832..3ef3ee2f 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -37,6 +37,7 @@ local function createReconciler(renderer) local hostParent = virtualNode.hostParent local hostKey = virtualNode.hostKey local depth = virtualNode.depth + local parent = virtualNode.parent -- If the node that is being replaced has modified context, we need to -- use the original *unmodified* context for the new node @@ -50,6 +51,7 @@ local function createReconciler(renderer) -- mountVirtualNode can return nil if the element is a boolean if newNode ~= nil then newNode.depth = depth + newNode.parent = parent end return newNode @@ -101,6 +103,7 @@ local function createReconciler(renderer) -- mountVirtualNode can return nil if the element is a boolean if childNode ~= nil then childNode.depth = virtualNode.depth + 1 + childNode.parent = virtualNode virtualNode.children[childKey] = childNode end end @@ -279,6 +282,7 @@ local function createReconciler(renderer) [Type] = Type.VirtualNode, currentElement = element, depth = 1, + parent = nil, children = {}, hostParent = hostParent, hostKey = hostKey, @@ -437,6 +441,28 @@ local function createReconciler(renderer) return tree end + local function suspendParentEvents(virtualNode) + local parentNode = virtualNode.parent + while parentNode do + if parentNode.eventManager ~= nil then + parentNode.eventManager:suspend() + end + + parentNode = parentNode.parent + end + end + + local function resumeParentEvents(virtualNode) + local parentNode = virtualNode.parent + while parentNode do + if parentNode.eventManager ~= nil then + parentNode.eventManager:resume() + end + + parentNode = parentNode.parent + end + end + reconciler = { mountVirtualTree = mountVirtualTree, unmountVirtualTree = unmountVirtualTree, @@ -448,6 +474,9 @@ local function createReconciler(renderer) updateVirtualNode = updateVirtualNode, updateVirtualNodeWithChildren = updateVirtualNodeWithChildren, updateVirtualNodeWithRenderResult = updateVirtualNodeWithRenderResult, + + suspendParentEvents = suspendParentEvents, + resumeParentEvents = resumeParentEvents, } return reconciler From 058e3c62041bf17380d05905679d43c8e3dccb46 Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Tue, 25 May 2021 11:28:33 -0700 Subject: [PATCH 50/65] Forward ref (#307) This closes a gap in compatibility with upstream React. While the current Roact implementation doesn't support refs assigned to non-host components, it also doesn't provide a way for non-host components to forward refs idiomatically as described here: https://reactjs.org/docs/forwarding-refs.html This change introduces an upstream-compatible forwardRef API and loosely adapts some of the upstream tests as well. Checklist before submitting: Add a test to validate that the ref isn't also provided as a member of props Added entry to CHANGELOG.md Added/updated relevant tests Added/updated documentation --- CHANGELOG.md | 5 +- docs/advanced/bindings-and-refs.md | 57 +++++ docs/api-reference.md | 9 + src/forwardRef.lua | 28 +++ src/forwardRef.spec.lua | 341 +++++++++++++++++++++++++++++ src/init.lua | 1 + src/init.spec.lua | 1 + 7 files changed, 440 insertions(+), 2 deletions(-) create mode 100644 src/forwardRef.lua create mode 100644 src/forwardRef.spec.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 16fe6dcc..3daad252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,14 @@ # Roact Changelog ## Unreleased Changes +* Introduce forwardRef ([#307](https://github.com/Roblox/roact/pull/307)). * Fixed a bug where the Roact tree could get into a broken state when processing changes to child instances outside the standard lifecycle. - This change is behind the config value tempFixUpdateChildrenReEntrancy ([#301](https://github.com/Roblox/roact/pull/301)) + * This change is behind the config value tempFixUpdateChildrenReEntrancy ([#301](https://github.com/Roblox/roact/pull/301)) * Added color schemes for documentation based on user preference ([#290](https://github.com/Roblox/roact/pull/290)). * Fixed stack trace level when throwing an error in `createReconciler` ([#297](https://github.com/Roblox/roact/pull/297)). * Optimized the memory usage of 'createSignal' implementation. ([#304](https://github.com/Roblox/roact/pull/304)) -## [1.3.1](https://github.com/Roblox/roact/releases/tag/v1.3.0) (November 19th, 2020) +## [1.3.1](https://github.com/Roblox/roact/releases/tag/v1.3.1) (November 19th, 2020) * Added component name to property validation error message ([#275](https://github.com/Roblox/roact/pull/275)) ## [1.3.0](https://github.com/Roblox/roact/releases/tag/v1.3.0) (May 5th, 2020) diff --git a/docs/advanced/bindings-and-refs.md b/docs/advanced/bindings-and-refs.md index 2ace4202..6df8d720 100644 --- a/docs/advanced/bindings-and-refs.md +++ b/docs/advanced/bindings-and-refs.md @@ -137,6 +137,63 @@ end Since refs use bindings under the hood, they will be automatically updated whenever the ref changes. This means there's no need to worry about the order in which refs are assigned relative to when properties that use them get set. +### Ref Forwarding +In Roact 1.x, refs can only be applied to host components, _not_ stateful or function components. However, stateful or function components may accept a ref in order to pass it along to an underlying host component. In order to implement this, we wrap the given component with `Roact.forwardRef`. + +Suppose we have a styled TextBox component that still needs to accept a ref, so that users of the component can trigger functionality like `TextBox:CaptureFocus()`: + +```lua +local function FancyTextBox(props) + return Roact.createElement("TextBox", { + Multiline = true, + PlaceholderText = "Enter your text here", + PlaceholderColor3 = Color3.new(0.4, 0.4, 0.4), + [Roact.Change.Text] = props.onTextChange, + }) +end +``` + +If we were to create an element using the above component, we'd be unable to get a ref to point to the underlying "TextBox" Instance: + +```lua +local Form = Roact.Component:extend("Form") +function Form:init() + self.textBoxRef = Roact.createRef() +end + +function Form:render() + return React.createElement(FancyTextBox, { + onTextChange = function(value) + print("text value updated to:", value) + end + -- This doesn't actually get assigned to the underlying TextBox! + [Roact.Ref] = self.textBoxRef, + }) +end + +function Form:didMount() + -- Since self.textBoxRef never gets assigned to a host component, this + -- doesn't work, and in fact will be an attempt to access a nil reference! + self.textBoxRef.current:CaptureFocus() +end +``` + +In this instance, `FancyTextBox` simply doesn't do anything with the ref passed into it. However, we can easily update it using forwardRef: + +```lua +local FancyTextBox = React.forwardRef(function(props, ref) + return Roact.createElement("TextBox", { + Multiline = true, + PlaceholderText = "Enter your text here", + PlaceholderColor3 = Color3.new(0.4, 0.4, 0.4), + [Roact.Change.Text] = props.onTextChange, + [Roact.Ref] = ref, + }) +end) +``` + +With the above change, `FancyTextBox` now accepts a ref and assigns it to the "TextBox" host component that it renders under the hood. Our `Form` implementation will successfully capture focus on `didMount`. + ### Function Refs The original ref API was based on functions instead of objects (and does not use bindings). Its use is not recommended for most cases anymore. diff --git a/docs/api-reference.md b/docs/api-reference.md index a5fd2f68..72a07086 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -179,6 +179,15 @@ Creates a new reference object that can be used with [Roact.Ref](#roactref). --- +### Roact.forwardRef +``` +Roact.createRef(render: (props: table, ref: Ref) -> RoactElement) -> RoactComponent +``` + +Creates a new component given a render function that accepts both props and a ref, allowing a ref to be forwarded to an underlying host component via [Roact.Ref](#roactref). + +--- + ### Roact.createContext !!! success "Added in Roact 1.3.0" diff --git a/src/forwardRef.lua b/src/forwardRef.lua new file mode 100644 index 00000000..77fff90d --- /dev/null +++ b/src/forwardRef.lua @@ -0,0 +1,28 @@ +local assign = require(script.Parent.assign) +local None = require(script.Parent.None) +local Ref = require(script.Parent.PropMarkers.Ref) + +local config = require(script.Parent.GlobalConfig).get() + +local excludeRef = { + [Ref] = None, +} + +--[[ + Allows forwarding of refs to underlying host components. Accepts a render + callback which accepts props and a ref, and returns an element. +]] +local function forwardRef(render) + if config.typeChecks then + assert(typeof(render) == "function", "Expected arg #1 to be a function") + end + + return function(props) + local ref = props[Ref] + local propsWithoutRef = assign({}, props, excludeRef) + + return render(propsWithoutRef, ref) + end +end + +return forwardRef \ No newline at end of file diff --git a/src/forwardRef.spec.lua b/src/forwardRef.spec.lua new file mode 100644 index 00000000..6d65101c --- /dev/null +++ b/src/forwardRef.spec.lua @@ -0,0 +1,341 @@ +-- Tests loosely adapted from those found at: +-- * https://github.com/facebook/react/blob/v17.0.1/packages/react/src/__tests__/forwardRef-test.js +-- * https://github.com/facebook/react/blob/v17.0.1/packages/react/src/__tests__/forwardRef-test.internal.js +return function() + local assign = require(script.Parent.assign) + local createElement = require(script.Parent.createElement) + local createRef = require(script.Parent.createRef) + local forwardRef = require(script.Parent.forwardRef) + local createReconciler = require(script.Parent.createReconciler) + local Component = require(script.Parent.Component) + local GlobalConfig = require(script.Parent.GlobalConfig) + local Ref = require(script.Parent.PropMarkers.Ref) + + local RobloxRenderer = require(script.Parent.RobloxRenderer) + + local reconciler = createReconciler(RobloxRenderer) + + it("should update refs when switching between children", function() + local function FunctionComponent(props) + local forwardedRef = props.forwardedRef + local setRefOnDiv = props.setRefOnDiv + -- deviation: clearer to express this way, since we don't have real + -- ternaries + local firstRef, secondRef + if setRefOnDiv then + firstRef = forwardedRef + else + secondRef = forwardedRef + end + return createElement("Frame", nil, { + First = createElement("Frame", { + [Ref] = firstRef + }, { + Child = createElement("TextLabel", { + Text = "First" + }) + }), + Second = createElement("ScrollingFrame", { + [Ref] = secondRef + }, { + Child = createElement("TextLabel", { + Text = "Second" + }) + }) + }) + end + + local RefForwardingComponent = forwardRef(function(props, ref) + return createElement(FunctionComponent, assign({}, props, { forwardedRef = ref })) + end) + + local ref = createRef() + + local element = createElement(RefForwardingComponent, { + [Ref] = ref, + setRefOnDiv = true, + }) + local tree = reconciler.mountVirtualTree(element, nil, "switch refs") + expect(ref.current.ClassName).to.equal("Frame") + reconciler.unmountVirtualTree(tree) + + element = createElement(RefForwardingComponent, { + [Ref] = ref, + setRefOnDiv = false, + }) + tree = reconciler.mountVirtualTree(element, nil, "switch refs") + expect(ref.current.ClassName).to.equal("ScrollingFrame") + reconciler.unmountVirtualTree(tree) + end) + + it("should support rendering nil", function() + local RefForwardingComponent = forwardRef(function(props, ref) + return nil + end) + + local ref = createRef() + + local element = createElement(RefForwardingComponent, { [Ref] = ref }) + local tree = reconciler.mountVirtualTree(element, nil, "nil ref") + expect(ref.current).to.equal(nil) + reconciler.unmountVirtualTree(tree) + end) + + it("should support rendering nil for multiple children", function() + local RefForwardingComponent = forwardRef(function(props, ref) + return nil + end) + + local ref = createRef() + + local element = createElement("Frame", nil, { + NoRef1 = createElement("Frame"), + WithRef = createElement(RefForwardingComponent, { [Ref] = ref }), + NoRef2 = createElement("Frame"), + }) + local tree = reconciler.mountVirtualTree(element, nil, "multiple children nil ref") + expect(ref.current).to.equal(nil) + reconciler.unmountVirtualTree(tree) + end) + + -- We could support this by having forwardRef return a stateful component, + -- but it's likely not necessary + itSKIP("should support defaultProps", function() + local function FunctionComponent(props) + local forwardedRef = props.forwardedRef + local optional = props.optional + local required = props.required + return createElement("Frame", { + [Ref] = forwardedRef, + }, { + OptionalChild = optional, + RequiredChild = required, + }) + end + + local RefForwardingComponent = forwardRef(function(props, ref) + return createElement(FunctionComponent, assign({}, props, { + forwardedRef = ref + })) + end) + RefForwardingComponent.defaultProps = { + optional = createElement("TextLabel"), + } + + local ref = createRef() + + local element = createElement(RefForwardingComponent, { + [Ref] = ref, + optional = createElement("Frame"), + required = createElement("ScrollingFrame"), + }) + + local tree = reconciler.mountVirtualTree(element, nil, "with optional") + + expect(ref.current:FindFirstChild("OptionalChild").ClassName).to.equal("Frame") + expect(ref.current:FindFirstChild("RequiredChild").ClassName).to.equal("ScrollingFrame") + + reconciler.unmountVirtualTree(tree) + element = createElement(RefForwardingComponent, { + [Ref] = ref, + required = createElement("ScrollingFrame"), + }) + tree = reconciler.mountVirtualTree(element, nil, "with default") + + expect(ref.current:FindFirstChild("OptionalChild").ClassName).to.equal("TextLabel") + expect(ref.current:FindFirstChild("RequiredChild").ClassName).to.equal("ScrollingFrame") + reconciler.unmountVirtualTree(tree) + end) + + it("should error if not provided a callback when type checking is enabled", function() + GlobalConfig.scoped({ + typeChecks = true, + }, function() + expect(function() + forwardRef(nil) + end).to.throw() + end) + + GlobalConfig.scoped({ + typeChecks = true, + }, function() + expect(function() + forwardRef("foo") + end).to.throw() + end) + end) + + it("should work without a ref to be forwarded", function() + local function Child() + return nil + end + + local function Wrapper(props) + return createElement(Child, assign({}, props, { [Ref] = props.forwardedRef })) + end + + local RefForwardingComponent = forwardRef(function(props, ref) + return createElement(Wrapper, assign({}, props, { forwardedRef = ref })) + end) + + local element = createElement(RefForwardingComponent, { value = 123 }) + local tree = reconciler.mountVirtualTree(element, nil, "nil ref") + reconciler.unmountVirtualTree(tree) + end) + + it("should forward a ref for a single child", function() + local value + local function Child(props) + value = props.value + return createElement("Frame", { + [Ref] = props[Ref] + }) + end + + local function Wrapper(props) + return createElement(Child, assign({}, props, { [Ref] = props.forwardedRef })) + end + + local RefForwardingComponent = forwardRef(function(props, ref) + return createElement(Wrapper, assign({}, props, { forwardedRef = ref })) + end) + + local ref = createRef() + + local element = createElement(RefForwardingComponent, { [Ref] = ref, value = 123 }) + local tree = reconciler.mountVirtualTree(element, nil, "single child ref") + expect(value).to.equal(123) + expect(ref.current.ClassName).to.equal("Frame") + reconciler.unmountVirtualTree(tree) + end) + + it("should forward a ref for multiple children", function() + local function Child(props) + return createElement("Frame", { + [Ref] = props[Ref] + }) + end + + local function Wrapper(props) + return createElement(Child, assign({}, props, { [Ref] = props.forwardedRef })) + end + + local RefForwardingComponent = forwardRef(function(props, ref) + return createElement(Wrapper, assign({}, props, { forwardedRef = ref })) + end) + + local ref = createRef() + + local element = createElement("Frame", nil, { + NoRef1 = createElement("Frame"), + WithRef = createElement(RefForwardingComponent, { [Ref] = ref }), + NoRef2 = createElement("Frame"), + }) + local tree = reconciler.mountVirtualTree(element, nil, "multi child ref") + expect(ref.current.ClassName).to.equal("Frame") + reconciler.unmountVirtualTree(tree) + end) + + it("should maintain child instance and ref through updates", function() + local value + local function Child(props) + value = props.value + return createElement("Frame", { + [Ref] = props[Ref] + }) + end + + local function Wrapper(props) + return createElement(Child, assign({}, props, { [Ref] = props.forwardedRef })) + end + + local RefForwardingComponent = forwardRef(function(props, ref) + return createElement(Wrapper, assign({}, props, { forwardedRef = ref })) + end) + + local setRefCount = 0 + local refValue + + local setRef = function(r) + setRefCount = setRefCount + 1 + refValue = r + end + + local element = createElement(RefForwardingComponent, { [Ref] = setRef, value = 123 }) + local tree = reconciler.mountVirtualTree(element, nil, "maintains instance") + + expect(value).to.equal(123) + expect(refValue.ClassName).to.equal("Frame") + expect(setRefCount).to.equal(1) + + element = createElement(RefForwardingComponent, { [Ref] = setRef, value = 456 }) + tree = reconciler.updateVirtualTree(tree, element) + + expect(value).to.equal(456) + expect(setRefCount).to.equal(1) + reconciler.unmountVirtualTree(tree) + end) + + it("should not re-run the render callback on a deep setState", function() + local inst + local renders = {} + + local Inner = Component:extend("Inner") + function Inner:render() + table.insert(renders, "Inner") + inst = self + return createElement("Frame", { [Ref] = self.props.forwardedRef }) + end + + local function Middle(props) + table.insert(renders, "Middle") + return createElement(Inner, props) + end + + local Forward = forwardRef(function(props, ref) + table.insert(renders, "Forward") + return createElement(Middle, assign({}, props, { forwardedRef = ref })) + end) + + local function App() + table.insert(renders, "App") + return createElement(Forward) + end + + local tree = reconciler.mountVirtualTree(createElement(App), nil, "deep setState") + expect(#renders).to.equal(4) + expect(renders[1]).to.equal("App") + expect(renders[2]).to.equal("Forward") + expect(renders[3]).to.equal("Middle") + expect(renders[4]).to.equal("Inner") + + renders = {} + inst:setState({}) + expect(#renders).to.equal(1) + expect(renders[1]).to.equal("Inner") + reconciler.unmountVirtualTree(tree) + end) + + it("should not include the ref in the forwarded props", function() + local capturedProps + local function CaptureProps(props) + capturedProps = props + return createElement("Frame", { [Ref] = props.forwardedRef }) + end + + local RefForwardingComponent = forwardRef(function(props, ref) + return createElement(CaptureProps, assign({}, props, { forwardedRef = ref })) + end) + + local ref = createRef() + local element = createElement(RefForwardingComponent, { + [Ref] = ref, + }) + + local tree = reconciler.mountVirtualTree(element, nil, "no ref in props") + expect(capturedProps).to.be.ok() + expect(capturedProps.forwardedRef).to.equal(ref) + expect(capturedProps[Ref]).to.equal(nil) + reconciler.unmountVirtualTree(tree) + end) +end diff --git a/src/init.lua b/src/init.lua index 0bac3010..1696d235 100644 --- a/src/init.lua +++ b/src/init.lua @@ -21,6 +21,7 @@ local Roact = strict { None = require(script.None), Portal = require(script.Portal), createRef = require(script.createRef), + forwardRef = require(script.forwardRef), createBinding = Binding.create, joinBindings = Binding.join, createContext = require(script.createContext), diff --git a/src/init.spec.lua b/src/init.spec.lua index 652ee19a..23fef065 100644 --- a/src/init.spec.lua +++ b/src/init.spec.lua @@ -6,6 +6,7 @@ return function() createElement = "function", createFragment = "function", createRef = "function", + forwardRef = "function", createBinding = "function", joinBindings = "function", mount = "function", From fe67d5e890b3e43ffa0fd84ff7c9a704e07308fb Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Tue, 25 May 2021 12:05:18 -0700 Subject: [PATCH 51/65] Update changelog and versions, add note in doc for forwardRef (#309) --- CHANGELOG.md | 2 ++ docs/api-reference.md | 3 +++ rotriever.toml | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3daad252..6cb521ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Roact Changelog ## Unreleased Changes + +## [1.4.0](https://github.com/Roblox/roact/releases/tag/v1.4.0) (November 19th, 2020) * Introduce forwardRef ([#307](https://github.com/Roblox/roact/pull/307)). * Fixed a bug where the Roact tree could get into a broken state when processing changes to child instances outside the standard lifecycle. * This change is behind the config value tempFixUpdateChildrenReEntrancy ([#301](https://github.com/Roblox/roact/pull/301)) diff --git a/docs/api-reference.md b/docs/api-reference.md index 72a07086..35091e47 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -180,6 +180,9 @@ Creates a new reference object that can be used with [Roact.Ref](#roactref). --- ### Roact.forwardRef + +!!! success "Added in Roact 1.4.0" + ``` Roact.createRef(render: (props: table, ref: Ref) -> RoactElement) -> RoactComponent ``` diff --git a/rotriever.toml b/rotriever.toml index fd1bc7ad..3c39e3b5 100644 --- a/rotriever.toml +++ b/rotriever.toml @@ -3,4 +3,4 @@ name = "roblox/roact" author = "Roblox" license = "Apache-2.0" content_root = "src" -version = "1.3.1" \ No newline at end of file +version = "1.4.0" \ No newline at end of file From d4f4a7a77684688b32ef3744d9fe902e2c3206af Mon Sep 17 00:00:00 2001 From: Rimuy <46044567+Rimuy@users.noreply.github.com> Date: Fri, 9 Jul 2021 20:25:22 -0300 Subject: [PATCH 52/65] Fix Roact.forwardRef description (#312) Fixes the Roact.forwardRef description which is currentlyRoact.createRef instead. --- CHANGELOG.md | 1 + docs/api-reference.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb521ad..9eac0588 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Roact Changelog ## Unreleased Changes +* Fixed forwardRef description ([#312](https://github.com/Roblox/roact/pull/312)). ## [1.4.0](https://github.com/Roblox/roact/releases/tag/v1.4.0) (November 19th, 2020) * Introduce forwardRef ([#307](https://github.com/Roblox/roact/pull/307)). diff --git a/docs/api-reference.md b/docs/api-reference.md index 35091e47..ff551409 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -184,7 +184,7 @@ Creates a new reference object that can be used with [Roact.Ref](#roactref). !!! success "Added in Roact 1.4.0" ``` -Roact.createRef(render: (props: table, ref: Ref) -> RoactElement) -> RoactComponent +Roact.forwardRef(render: (props: table, ref: Ref) -> RoactElement) -> RoactComponent ``` Creates a new component given a render function that accepts both props and a ref, allowing a ref to be forwarded to an underlying host component via [Roact.Ref](#roactref). From 0bba19bcda0228165f993782f71871a6f958135f Mon Sep 17 00:00:00 2001 From: Conor Griffin Date: Wed, 11 Aug 2021 19:43:24 +0100 Subject: [PATCH 53/65] Further update children re-rentrancy problems (#315) This fixes the problem where nodes lower down the Roact tree can change outside of their standard lifecycle and then cause a callback or event to a node higher up the tree. That component can re-render while the node further down the tree is updating its children, causing duplicates of elements to be added. --- CHANGELOG.md | 1 + rotriever.toml | 2 +- src/Component.lua | 14 -- src/RobloxRenderer.spec.lua | 332 +++++++++++++++++++++++++++++++++++- src/createReconciler.lua | 68 +++++--- 5 files changed, 375 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eac0588..46b0613c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Roact Changelog ## Unreleased Changes +* Fixed a bug where the Roact tree could get into a broken state when using callbacks passed to a child component. Updated the tempFixUpdateChildrenReEntrancy config value to also handle this case. ([#315](https://github.com/Roblox/roact/pull/315)) * Fixed forwardRef description ([#312](https://github.com/Roblox/roact/pull/312)). ## [1.4.0](https://github.com/Roblox/roact/releases/tag/v1.4.0) (November 19th, 2020) diff --git a/rotriever.toml b/rotriever.toml index 3c39e3b5..15867ec9 100644 --- a/rotriever.toml +++ b/rotriever.toml @@ -3,4 +3,4 @@ name = "roblox/roact" author = "Roblox" license = "Apache-2.0" content_root = "src" -version = "1.4.0" \ No newline at end of file +version = "1.4.1" diff --git a/src/Component.lua b/src/Component.lua index 7c6b62a6..7e4d9186 100644 --- a/src/Component.lua +++ b/src/Component.lua @@ -157,22 +157,8 @@ function Component:setState(mapState) internalData.pendingState = assign(newState, derivedState) elseif lifecyclePhase == ComponentLifecyclePhase.Idle then - -- Pause parent events when we are updated outside of our lifecycle - -- If these events are not paused, our setState can cause a component higher up the - -- tree to rerender based on events caused by our component while this reconciliation is happening. - -- This could cause the tree to become invalid. - local virtualNode = internalData.virtualNode - local reconciler = internalData.reconciler - if config.tempFixUpdateChildrenReEntrancy then - reconciler.suspendParentEvents(virtualNode) - end - -- Outside of our lifecycle, the state update is safe to make immediately self:__update(nil, newState) - - if config.tempFixUpdateChildrenReEntrancy then - reconciler.resumeParentEvents(virtualNode) - end else local messageTemplate = invalidSetStateMessages.default diff --git a/src/RobloxRenderer.spec.lua b/src/RobloxRenderer.spec.lua index 9e8934ac..4735f78f 100644 --- a/src/RobloxRenderer.spec.lua +++ b/src/RobloxRenderer.spec.lua @@ -1025,5 +1025,335 @@ return function() reconciler.unmountVirtualNode(instance) end) end) + + it("should not allow re-entrancy in updateChildren even with callbacks", function() + local configValues = { + tempFixUpdateChildrenReEntrancy = true, + } + + GlobalConfig.scoped(configValues, function() + local LowestComponent = Component:extend("LowestComponent") + + function LowestComponent:render() + return createElement("Frame") + end + + function LowestComponent:didMount() + self.props.onDidMountCallback() + end + + local ChildComponent = Component:extend("ChildComponent") + + function ChildComponent:init() + self:setState({ + firstTime = true + }) + end + + local childCoroutine + + function ChildComponent:render() + if self.state.firstTime then + return createElement("Frame") + end + + return createElement(LowestComponent, { + onDidMountCallback = self.props.onDidMountCallback + }) + end + + function ChildComponent:didMount() + childCoroutine = coroutine.create(function() + self:setState({ + firstTime = false + }) + end) + end + + local ParentComponent = Component:extend("ParentComponent") + + local didMountCallbackCalled = 0 + + function ParentComponent:init() + self:setState({ + count = 1 + }) + + self.onDidMountCallback = function() + didMountCallbackCalled = didMountCallbackCalled + 1 + if self.state.count < 5 then + self:setState({ + count = self.state.count + 1, + }) + end + end + end + + function ParentComponent:render() + return createElement("Frame", { + + }, { + ChildComponent = createElement(ChildComponent, { + count = self.state.count, + onDidMountCallback = self.onDidMountCallback, + }) + }) + end + + local parent = Instance.new("ScreenGui") + parent.Parent = game.CoreGui + + local tree = createElement(ParentComponent) + + local hostKey = "Some Key" + local instance = reconciler.mountVirtualNode(tree, parent, hostKey) + + coroutine.resume(childCoroutine) + + expect(#parent:GetChildren()).to.equal(1) + + local frame = parent:GetChildren()[1] + + expect(#frame:GetChildren()).to.equal(1) + + -- In an ideal world, the didMount callback would probably be called only once. Since it is called by two different + -- LowestComponent instantiations 2 is also acceptable though. + expect(didMountCallbackCalled <= 2).to.equal(true) + + reconciler.unmountVirtualNode(instance) + end) + end) + + it("should never call unmount twice when tempFixUpdateChildrenReEntrancy is turned on", function() + local configValues = { + tempFixUpdateChildrenReEntrancy = true, + } + + GlobalConfig.scoped(configValues, function() + local unmountCounts = {} + + local function addUnmount(id) + unmountCounts[id] = unmountCounts[id] + 1 + end + + local function addInit(id) + unmountCounts[id] = 0 + end + + local LowestComponent = Component:extend("LowestComponent") + function LowestComponent:init() + addInit(tostring(self)) + end + + function LowestComponent:render() + return createElement("Frame") + end + + function LowestComponent:didMount() + self.props.onDidMountCallback() + end + + function LowestComponent:willUnmount() + addUnmount(tostring(self)) + end + + local FirstComponent = Component:extend("FirstComponent") + function FirstComponent:init() + addInit(tostring(self)) + end + + function FirstComponent:render() + return createElement("TextLabel") + end + + function FirstComponent:willUnmount() + addUnmount(tostring(self)) + end + + local ChildComponent = Component:extend("ChildComponent") + + function ChildComponent:init() + addInit(tostring(self)) + + self:setState({ + firstTime = true + }) + end + + local childCoroutine + + function ChildComponent:render() + if self.state.firstTime then + return createElement(FirstComponent) + end + + return createElement(LowestComponent, { + onDidMountCallback = self.props.onDidMountCallback + }) + end + + function ChildComponent:didMount() + childCoroutine = coroutine.create(function() + self:setState({ + firstTime = false + }) + end) + end + + function ChildComponent:willUnmount() + addUnmount(tostring(self)) + end + + local ParentComponent = Component:extend("ParentComponent") + + local didMountCallbackCalled = 0 + + function ParentComponent:init() + self:setState({ + count = 1 + }) + + self.onDidMountCallback = function() + didMountCallbackCalled = didMountCallbackCalled + 1 + if self.state.count < 5 then + self:setState({ + count = self.state.count + 1, + }) + end + end + end + + function ParentComponent:render() + return createElement("Frame", { + + }, { + ChildComponent = createElement(ChildComponent, { + count = self.state.count, + onDidMountCallback = self.onDidMountCallback, + }) + }) + end + + local parent = Instance.new("ScreenGui") + parent.Parent = game.CoreGui + + local tree = createElement(ParentComponent) + + local hostKey = "Some Key" + local instance = reconciler.mountVirtualNode(tree, parent, hostKey) + + coroutine.resume(childCoroutine) + + expect(#parent:GetChildren()).to.equal(1) + + local frame = parent:GetChildren()[1] + + expect(#frame:GetChildren()).to.equal(1) + + -- In an ideal world, the didMount callback would probably be called only once. Since it is called by two different + -- LowestComponent instantiations 2 is also acceptable though. + expect(didMountCallbackCalled <= 2).to.equal(true) + + reconciler.unmountVirtualNode(instance) + + for _, value in pairs(unmountCounts) do + expect(value).to.equal(1) + end + end) + end) + + it("should never unmount a node unnecesarily in the case of re-rentry", function() + local configValues = { + tempFixUpdateChildrenReEntrancy = true, + } + + GlobalConfig.scoped(configValues, function() + local LowestComponent = Component:extend("LowestComponent") + function LowestComponent:render() + return createElement("Frame") + end + + function LowestComponent:didUpdate(prevProps, prevState) + if prevProps.firstTime and not self.props.firstTime then + self.props.onChangedCallback() + end + end + + local ChildComponent = Component:extend("ChildComponent") + + function ChildComponent:init() + self:setState({ + firstTime = true + }) + end + + local childCoroutine + + function ChildComponent:render() + return createElement(LowestComponent, { + firstTime = self.state.firstTime, + onChangedCallback = self.props.onChangedCallback + }) + end + + function ChildComponent:didMount() + childCoroutine = coroutine.create(function() + self:setState({ + firstTime = false + }) + end) + end + + local ParentComponent = Component:extend("ParentComponent") + + local onChangedCallbackCalled = 0 + + function ParentComponent:init() + self:setState({ + count = 1 + }) + + self.onChangedCallback = function() + onChangedCallbackCalled = onChangedCallbackCalled + 1 + if self.state.count < 5 then + self:setState({ + count = self.state.count + 1, + }) + end + end + end + + function ParentComponent:render() + return createElement("Frame", { + + }, { + ChildComponent = createElement(ChildComponent, { + count = self.state.count, + onChangedCallback = self.onChangedCallback, + }) + }) + end + + local parent = Instance.new("ScreenGui") + parent.Parent = game.CoreGui + + local tree = createElement(ParentComponent) + + local hostKey = "Some Key" + local instance = reconciler.mountVirtualNode(tree, parent, hostKey) + + coroutine.resume(childCoroutine) + + expect(#parent:GetChildren()).to.equal(1) + + local frame = parent:GetChildren()[1] + + expect(#frame:GetChildren()).to.equal(1) + + expect(onChangedCallbackCalled).to.equal(1) + + reconciler.unmountVirtualNode(instance) + end) + end) end) -end \ No newline at end of file +end diff --git a/src/createReconciler.lua b/src/createReconciler.lua index 3ef3ee2f..2ff86467 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -45,7 +45,16 @@ local function createReconciler(renderer) local context = virtualNode.originalContext or virtualNode.context local parentLegacyContext = virtualNode.parentLegacyContext - unmountVirtualNode(virtualNode) + if config.tempFixUpdateChildrenReEntrancy then + -- If updating this node has caused a component higher up the tree to re-render + -- and updateChildren to be re-entered then this node could already have been + -- unmounted in the previous updateChildren pass. + if not virtualNode.wasUnmounted then + unmountVirtualNode(virtualNode) + end + else + unmountVirtualNode(virtualNode) + end local newNode = mountVirtualNode(newElement, hostParent, hostKey, context, parentLegacyContext) -- mountVirtualNode can return nil if the element is a boolean @@ -66,6 +75,10 @@ local function createReconciler(renderer) internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #1 to be of type VirtualNode") end + virtualNode.updateChildrenCount = virtualNode.updateChildrenCount + 1 + + local currentUpdateChildrenCount = virtualNode.updateChildrenCount + local removeKeys = {} -- Changed or removed children @@ -73,6 +86,18 @@ local function createReconciler(renderer) local newElement = ElementUtils.getElementByKey(newChildElements, childKey) local newNode = updateVirtualNode(childNode, newElement) + -- If updating this node has caused a component higher up the tree to re-render + -- and updateChildren to be re-entered for this virtualNode then + -- this result is invalid and needs to be disgarded. + if config.tempFixUpdateChildrenReEntrancy then + if virtualNode.updateChildrenCount ~= currentUpdateChildrenCount then + if newNode and newNode ~= virtualNode.children[childKey] then + unmountVirtualNode(newNode) + end + return + end + end + if newNode ~= nil then virtualNode.children[childKey] = newNode else @@ -100,6 +125,18 @@ local function createReconciler(renderer) virtualNode.legacyContext ) + -- If updating this node has caused a component higher up the tree to re-render + -- and updateChildren to be re-entered for this virtualNode then + -- this result is invalid and needs to be discarded. + if config.tempFixUpdateChildrenReEntrancy then + if virtualNode.updateChildrenCount ~= currentUpdateChildrenCount then + if childNode then + unmountVirtualNode(childNode) + end + return + end + end + -- mountVirtualNode can return nil if the element is a boolean if childNode ~= nil then childNode.depth = virtualNode.depth + 1 @@ -136,6 +173,8 @@ local function createReconciler(renderer) internalAssert(Type.of(virtualNode) == Type.VirtualNode, "Expected arg #1 to be of type VirtualNode") end + virtualNode.wasUnmounted = true + local kind = ElementKind.of(virtualNode.currentElement) if kind == ElementKind.Host then @@ -286,6 +325,8 @@ local function createReconciler(renderer) children = {}, hostParent = hostParent, hostKey = hostKey, + updateChildrenCount = 0, + wasUnmounted = false, -- Legacy Context API -- A table of context values inherited from the parent node @@ -441,28 +482,6 @@ local function createReconciler(renderer) return tree end - local function suspendParentEvents(virtualNode) - local parentNode = virtualNode.parent - while parentNode do - if parentNode.eventManager ~= nil then - parentNode.eventManager:suspend() - end - - parentNode = parentNode.parent - end - end - - local function resumeParentEvents(virtualNode) - local parentNode = virtualNode.parent - while parentNode do - if parentNode.eventManager ~= nil then - parentNode.eventManager:resume() - end - - parentNode = parentNode.parent - end - end - reconciler = { mountVirtualTree = mountVirtualTree, unmountVirtualTree = unmountVirtualTree, @@ -474,9 +493,6 @@ local function createReconciler(renderer) updateVirtualNode = updateVirtualNode, updateVirtualNodeWithChildren = updateVirtualNodeWithChildren, updateVirtualNodeWithRenderResult = updateVirtualNodeWithRenderResult, - - suspendParentEvents = suspendParentEvents, - resumeParentEvents = resumeParentEvents, } return reconciler From 4796c79e2015fae51f2734ba2eb4dac07b212ae2 Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Thu, 12 Aug 2021 10:18:21 -0700 Subject: [PATCH 54/65] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46b0613c..5d7b70bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Roact Changelog ## Unreleased Changes + +## [1.4.1](https://github.com/Roblox/roact/releases/tag/v1.4.1) (August 12th, 2021) * Fixed a bug where the Roact tree could get into a broken state when using callbacks passed to a child component. Updated the tempFixUpdateChildrenReEntrancy config value to also handle this case. ([#315](https://github.com/Roblox/roact/pull/315)) * Fixed forwardRef description ([#312](https://github.com/Roblox/roact/pull/312)). From 97a30b38c5e077c7d345813f872e750d802d2d8c Mon Sep 17 00:00:00 2001 From: oltrep <34498770+oltrep@users.noreply.github.com> Date: Thu, 16 Sep 2021 14:52:28 -0700 Subject: [PATCH 55/65] Update TestEZ and fix tests so they work in studio (#318) --- .luacheckrc | 3 ++- bin/run-tests.server.lua | 13 +++++++------ bin/spec.lua | 2 +- modules/testez | 2 +- place.project.json | 2 +- src/RobloxRenderer.spec.lua | 21 +++++++++++++++++---- 6 files changed, 29 insertions(+), 14 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index cfd0d448..2bb24248 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -26,8 +26,9 @@ stds.testez = { read_globals = { "describe", "it", "itFOCUS", "itSKIP", - "FOCUS", "SKIP", "HACK_NO_XPCALL", + "FOCUS", "SKIP", "expect", + "beforeEach", "afterEach", "beforeAll", "afterAll", } } diff --git a/bin/run-tests.server.lua b/bin/run-tests.server.lua index cc51dabb..d4df9f36 100644 --- a/bin/run-tests.server.lua +++ b/bin/run-tests.server.lua @@ -12,14 +12,15 @@ Roact.setGlobalConfig({ ["elementTracing"] = true, ["propValidation"] = true, }) -local results = TestEZ.TestBootstrap:run(ReplicatedStorage.Roact, TestEZ.Reporters.TextReporter) +local results = TestEZ.TestBootstrap:run( + { ReplicatedStorage.Roact }, + TestEZ.Reporters.TextReporter +) -local statusCode = results.failureCount == 0 and 0 or 1 +local statusCode = (results.failureCount == 0 and #results.errors == 0) and 0 or 1 if __LEMUR__ then - if results.failureCount > 0 then - os.exit(statusCode) - end + os.exit(statusCode) elseif isRobloxCli then - ProcessService:Exit(statusCode) + ProcessService:ExitAsync(statusCode) end \ No newline at end of file diff --git a/bin/spec.lua b/bin/spec.lua index b6d0f792..616a10f4 100644 --- a/bin/spec.lua +++ b/bin/spec.lua @@ -5,7 +5,7 @@ -- If you add any dependencies, add them to this table so they'll be loaded! local LOAD_MODULES = { {"src", "Roact"}, - {"modules/testez/lib", "TestEZ"}, + {"modules/testez/src", "TestEZ"}, } -- This makes sure we can load Lemur and other libraries that depend on init.lua diff --git a/modules/testez b/modules/testez index 5acef965..25d957d4 160000 --- a/modules/testez +++ b/modules/testez @@ -1 +1 @@ -Subproject commit 5acef9659a177d446800e986b60e4613a35eb418 +Subproject commit 25d957d4d5c4c02a52843ef43e72f21f973c2908 diff --git a/place.project.json b/place.project.json index 7807792f..96dc18f4 100644 --- a/place.project.json +++ b/place.project.json @@ -11,7 +11,7 @@ }, "TestEZ": { - "$path": "modules/testez/lib" + "$path": "modules/testez" } }, diff --git a/src/RobloxRenderer.spec.lua b/src/RobloxRenderer.spec.lua index 4735f78f..99e3a1af 100644 --- a/src/RobloxRenderer.spec.lua +++ b/src/RobloxRenderer.spec.lua @@ -1,4 +1,6 @@ return function() + local ReplicatedStorage = game:GetService("ReplicatedStorage") + local assertDeepEqual = require(script.Parent.assertDeepEqual) local Binding = require(script.Parent.Binding) local Children = require(script.Parent.PropMarkers.Children) @@ -950,6 +952,17 @@ return function() describe("Integration Tests", function() + local temporaryParent = nil + beforeEach(function() + temporaryParent = Instance.new("Folder") + temporaryParent.Parent = ReplicatedStorage + end) + + afterEach(function() + temporaryParent:Destroy() + temporaryParent = nil + end) + it("should not allow re-entrancy in updateChildren", function() local configValues = { tempFixUpdateChildrenReEntrancy = true, @@ -1007,7 +1020,7 @@ return function() end local parent = Instance.new("ScreenGui") - parent.Parent = game.CoreGui + parent.Parent = temporaryParent local tree = createElement(ParentComponent) @@ -1101,7 +1114,7 @@ return function() end local parent = Instance.new("ScreenGui") - parent.Parent = game.CoreGui + parent.Parent = temporaryParent local tree = createElement(ParentComponent) @@ -1235,7 +1248,7 @@ return function() end local parent = Instance.new("ScreenGui") - parent.Parent = game.CoreGui + parent.Parent = temporaryParent local tree = createElement(ParentComponent) @@ -1335,7 +1348,7 @@ return function() end local parent = Instance.new("ScreenGui") - parent.Parent = game.CoreGui + parent.Parent = temporaryParent local tree = createElement(ParentComponent) From a3adfa6f2544e020c135df820e472f3363a2b072 Mon Sep 17 00:00:00 2001 From: oltrep <34498770+oltrep@users.noreply.github.com> Date: Tue, 21 Sep 2021 12:19:11 -0700 Subject: [PATCH 56/65] Fix bug in context consumers willUnmount (#320) --- CHANGELOG.md | 2 + src/createContext.lua | 1 + src/createContext.spec.lua | 97 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d7b70bf..844a4a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased Changes +* Fixed `Listeners can only be disconnected once` from context consumers. ([#320](https://github.com/Roblox/roact/pull/320)) + ## [1.4.1](https://github.com/Roblox/roact/releases/tag/v1.4.1) (August 12th, 2021) * Fixed a bug where the Roact tree could get into a broken state when using callbacks passed to a child component. Updated the tempFixUpdateChildrenReEntrancy config value to also handle this case. ([#315](https://github.com/Roblox/roact/pull/315)) * Fixed forwardRef description ([#312](https://github.com/Roblox/roact/pull/312)). diff --git a/src/createContext.lua b/src/createContext.lua index b21635ef..e4dcae15 100644 --- a/src/createContext.lua +++ b/src/createContext.lua @@ -119,6 +119,7 @@ local function createConsumer(context) function Consumer:willUnmount() if self.disconnect ~= nil then self.disconnect() + self.disconnect = nil end end diff --git a/src/createContext.spec.lua b/src/createContext.spec.lua index 432d39d4..4b436b86 100644 --- a/src/createContext.spec.lua +++ b/src/createContext.spec.lua @@ -1,4 +1,6 @@ return function() + local ReplicatedStorage = game:GetService("ReplicatedStorage") + local Component = require(script.Parent.Component) local NoopRenderer = require(script.Parent.NoopRenderer) local Children = require(script.Parent.PropMarkers.Children) @@ -10,6 +12,9 @@ return function() local noopReconciler = createReconciler(NoopRenderer) + local RobloxRenderer = require(script.Parent.RobloxRenderer) + local robloxReconciler = createReconciler(RobloxRenderer) + it("should return a table", function() local context = createContext("Test") expect(context).to.be.ok() @@ -301,4 +306,96 @@ return function() expect(observedB).to.equal(true) end) end) + + -- issue https://github.com/Roblox/roact/issues/319 + it("does not throw if willUnmount is called twice on a context consumer", function() + local context = createContext({}) + + local LowestComponent = Component:extend("LowestComponent") + function LowestComponent:init() + end + + function LowestComponent:render() + return createElement("Frame") + end + + function LowestComponent:didMount() + self.props.onDidMountCallback() + end + + local FirstComponent = Component:extend("FirstComponent") + function FirstComponent:init() + end + + function FirstComponent:render() + return createElement(context.Consumer, { + render = function() + return createElement("TextLabel") + end, + }) + end + + local ChildComponent = Component:extend("ChildComponent") + + function ChildComponent:init() + self:setState({ firstTime = true }) + end + + local childCallback + + function ChildComponent:render() + if self.state.firstTime then + return createElement(FirstComponent) + end + + return createElement(LowestComponent, { + onDidMountCallback = self.props.onDidMountCallback + }) + end + + function ChildComponent:didMount() + childCallback = function() + self:setState({ firstTime = false }) + end + end + + local ParentComponent = Component:extend("ParentComponent") + + local didMountCallbackCalled = 0 + + function ParentComponent:init() + self:setState({ count = 1 }) + + self.onDidMountCallback = function() + didMountCallbackCalled = didMountCallbackCalled + 1 + if self.state.count < 5 then + self:setState({ count = self.state.count + 1 }) + end + end + end + + function ParentComponent:render() + return createElement("Frame", {}, { + Provider = createElement(context.Provider, { + value = {}, + }, { + ChildComponent = createElement(ChildComponent, { + count = self.state.count, + onDidMountCallback = self.onDidMountCallback, + }), + }) + }) + end + + local parent = Instance.new("ScreenGui") + parent.Parent = ReplicatedStorage + + local hostKey = "Some Key" + robloxReconciler.mountVirtualNode(createElement(ParentComponent), parent, hostKey) + + expect(function() + -- calling setState on ChildComponent will trigger `willUnmount` multiple times + childCallback() + end).never.to.throw() + end) end \ No newline at end of file From d37f842aa2fc449b3837296be686f4d86aea0e57 Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Thu, 23 Sep 2021 17:36:42 -0700 Subject: [PATCH 57/65] Update clabot.yml Add `bliang` to internal contributors list --- .github/workflows/clabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clabot.yml b/.github/workflows/clabot.yml index e61c7763..df6700e7 100644 --- a/.github/workflows/clabot.yml +++ b/.github/workflows/clabot.yml @@ -14,7 +14,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - whitelist: "LPGhatguy,ZoteTheMighty,cliffchapmanrbx,MisterUncloaked,matthargett,ConorGriffin37,yjia2" + whitelist: "LPGhatguy,ZoteTheMighty,cliffchapmanrbx,MisterUncloaked,matthargett,ConorGriffin37,yjia2,bliang" use-remote-repo: true remote-repo-name: "roblox/cla-bot-store" remote-repo-pat: ${{ secrets.CLA_REMOTE_REPO_PAT }} From b53b1db906296eb39c0b3b999b9d1175bb6c7cf9 Mon Sep 17 00:00:00 2001 From: Brian Liang Date: Thu, 23 Sep 2021 17:37:47 -0700 Subject: [PATCH 58/65] Fixed broken link for GetPropertyChangedSignal (#322) Fixed Broken link for GetPropertyChangedSignal. --- docs/api-reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index ff551409..1fa75903 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -346,7 +346,7 @@ See [the events guide](../guide/events) for more details. --- ### Roact.Change -Index into `Roact.Change` to receive a key that can be used to connect to [`GetPropertyChangedSignal`](http://wiki.roblox.com/index.php?title=API:Class/Instance/GetPropertyChangedSignal) events. +Index into `Roact.Change` to receive a key that can be used to connect to [`GetPropertyChangedSignal`](https://developer.roblox.com/en-us/api-reference/function/Instance/GetPropertyChangedSignal) events. It's similar to `Roact.Event`: From 103bd1831605c1ec32ee7f85bc703346ad6c1c45 Mon Sep 17 00:00:00 2001 From: chasedig <16283503+chasedig@users.noreply.github.com> Date: Fri, 24 Sep 2021 17:21:25 -0700 Subject: [PATCH 59/65] Silence indentation warning in script analysis (#313) * Silence indentation warning in script analysis "W006: (285,3) Statement spans multiple lines; use indentation to silence" Co-authored-by: Matt Hargett --- src/Component.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Component.lua b/src/Component.lua index 7e4d9186..04bdeaa7 100644 --- a/src/Component.lua +++ b/src/Component.lua @@ -268,7 +268,7 @@ function Component:__validateProps(props) self.__componentName, tostring(failureReason), self:getElementTraceback() or ""), - 0) + 0) end end @@ -504,4 +504,4 @@ function Component:__resolveUpdate(incomingProps, incomingState) return true end -return Component \ No newline at end of file +return Component From b2ba9cf4c219c2654e6572219a68d0bf1b541418 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Fri, 24 Sep 2021 17:23:33 -0700 Subject: [PATCH 60/65] Enforce code style and linting to eliminate Studio indentation warnings (#321) * Add foreman.toml that adds selene, stylua, and rojo. Run them in CI. * Update contribution document. Eliminate luacheck from CI * Fix a few Studio analysis warnings. Pin a few simple files for strict and nonstrict analysis. I think this is good enough for now. * Fix selene lints and format codebase using StyLua. Co-authored-by: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> --- .github/workflows/ci.yml | 26 +++++- .gitignore | 4 +- .luacheckrc | 43 --------- CONTRIBUTING.md | 29 ++++-- benchmarks/hello.bench.lua | 2 +- benchmarks/init.server.lua | 15 +--- benchmarks/update.bench.lua | 11 ++- bin/run-tests.server.lua | 6 +- examples/binding/init.lua | 2 +- examples/clock/init.lua | 20 +++-- examples/event/init.lua | 2 +- examples/hello-roact/init.lua | 2 +- examples/init.client.lua | 2 +- examples/stress-test/init.lua | 2 +- foreman.toml | 4 + selene.toml | 1 + src/Binding.lua | 12 ++- src/Binding.spec.lua | 4 +- src/Component.lua | 45 +++++----- src/Component.spec/context.spec.lua | 25 ++---- src/Component.spec/defaultProps.spec.lua | 5 +- src/Component.spec/didMount.spec.lua | 2 +- src/Component.spec/didUpdate.spec.lua | 5 +- src/Component.spec/extend.spec.lua | 2 +- .../getDerivedStateFromProps.spec.lua | 23 +++-- .../getElementTraceback.spec.lua | 2 +- src/Component.spec/init.spec.lua | 2 +- src/Component.spec/legacyContext.spec.lua | 20 ++--- src/Component.spec/render.spec.lua | 2 +- src/Component.spec/setState.spec.lua | 34 +++---- src/Component.spec/shouldUpdate.spec.lua | 2 +- src/Component.spec/validateProps.spec.lua | 6 +- src/Component.spec/willUnmount.spec.lua | 2 +- src/Component.spec/willUpdate.spec.lua | 8 +- src/ComponentLifecyclePhase.lua | 2 +- src/Config.lua | 18 ++-- src/Config.spec.lua | 2 +- src/ElementKind.lua | 2 +- src/ElementKind.spec.lua | 5 +- src/ElementUtils.lua | 2 +- src/ElementUtils.spec.lua | 2 +- src/GlobalConfig.lua | 2 +- src/GlobalConfig.spec.lua | 2 +- src/Logging.lua | 4 +- src/None.lua | 2 +- src/NoopRenderer.lua | 10 +-- src/Portal.lua | 2 +- src/PropMarkers/Change.lua | 2 +- src/PropMarkers/Change.spec.lua | 2 +- src/PropMarkers/Children.lua | 2 +- src/PropMarkers/Event.lua | 2 +- src/PropMarkers/Event.spec.lua | 2 +- src/PropMarkers/Ref.lua | 2 +- src/PureComponent.lua | 2 +- src/PureComponent.spec.lua | 2 +- src/RobloxRenderer.lua | 4 +- src/RobloxRenderer.spec.lua | 89 +++++++++---------- src/SingleEventManager.lua | 10 +-- src/SingleEventManager.spec.lua | 4 +- src/Symbol.lua | 3 +- src/Symbol.spec.lua | 2 +- src/Type.lua | 2 +- src/Type.spec.lua | 4 +- src/assertDeepEqual.lua | 11 +-- src/assertDeepEqual.spec.lua | 20 +++-- src/assign.lua | 2 +- src/assign.spec.lua | 2 +- src/createContext.lua | 2 +- src/createContext.spec.lua | 42 +++++---- src/createElement.lua | 2 +- src/createElement.spec.lua | 5 +- src/createFragment.lua | 2 +- src/createFragment.spec.lua | 4 +- src/createReconciler.lua | 28 +++--- src/createReconciler.spec.lua | 16 ++-- src/createReconcilerCompat.lua | 2 +- src/createReconcilerCompat.spec.lua | 2 +- src/createRef.lua | 8 +- src/createRef.spec.lua | 2 +- src/createSignal.lua | 4 +- src/createSignal.spec.lua | 4 +- src/createSpy.lua | 26 +++--- src/createSpy.spec.lua | 2 +- src/forwardRef.lua | 2 +- src/forwardRef.spec.lua | 33 +++---- src/getDefaultInstanceProperty.lua | 2 +- src/getDefaultInstanceProperty.spec.lua | 2 +- src/init.lua | 10 +-- src/init.spec.lua | 14 +-- src/internalAssert.lua | 2 +- src/invalidSetStateMessages.lua | 2 +- src/oneChild.lua | 2 +- src/oneChild.spec.lua | 2 +- src/strict.lua | 19 ++-- src/strict.spec.lua | 2 +- testez.toml | 79 ++++++++++++++++ 96 files changed, 481 insertions(+), 437 deletions(-) delete mode 100644 .luacheckrc create mode 100644 foreman.toml create mode 100644 selene.toml create mode 100644 testez.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd746a1a..dd6971b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,21 +24,41 @@ jobs: - uses: leafo/gh-actions-luarocks@v4 + - name: get foreman and run foreman install + uses: rojo-rbx/setup-foreman@v1 + with: + version: "^1.0.1" + token: ${{ secrets.GITHUB_TOKEN }} + + # useful for debugging differences between CI and local environment + - name: check versions + shell: bash + run: | + foreman list + + - name: use rojo to build project + shell: bash + run: rojo build default.project.json --output model.rbxmx + + - name: Linting and Style Checking + shell: bash + run: | + selene src benchmarks examples + stylua -c src benchmarks examples + - name: Install dependencies run: | luarocks install luafilesystem luarocks install luacov luarocks install luacov-reporter-lcov - luarocks install luacheck - name: Test run: | lua -lluacov bin/spec.lua - luacheck src benchmarks examples luacov -r lcov - name: Report to Coveralls uses: coverallsapp/github-action@v1.1.2 with: github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: luacov.report.out \ No newline at end of file + path-to-lcov: luacov.report.out diff --git a/.gitignore b/.gitignore index ce0b8c02..c4f8932f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ /*.rbxlx /*.rbxmx /*.rbxm -/*.rbxl \ No newline at end of file +/*.rbxl +# let selene re-generate roblox.toml so it updates automatically with selene +roblox.toml diff --git a/.luacheckrc b/.luacheckrc deleted file mode 100644 index 2bb24248..00000000 --- a/.luacheckrc +++ /dev/null @@ -1,43 +0,0 @@ -stds.roblox = { - globals = { - "game" - }, - read_globals = { - -- Roblox globals - "script", - - -- Extra functions - "tick", "warn", "spawn", - "wait", "settings", "typeof", - - -- Types - "Vector2", "Vector3", - "Color3", - "UDim", "UDim2", - "Rect", - "CFrame", - "Enum", - "Instance", - "TweenInfo", - } -} - -stds.testez = { - read_globals = { - "describe", - "it", "itFOCUS", "itSKIP", - "FOCUS", "SKIP", - "expect", - "beforeEach", "afterEach", "beforeAll", "afterAll", - } -} - -ignore = { - "212", -- unused arguments -} - -std = "lua51+roblox" - -files["**/*.spec.lua"] = { - std = "+testez", -} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd088337..26366f82 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,9 +22,20 @@ To get started working on Roact, you'll need: * Lua 5.1 * Lemur's dependencies: * [LuaFileSystem](https://keplerproject.github.io/luafilesystem/) (`luarocks install luafilesystem`) -* [Luacheck](https://github.com/mpeterv/luacheck) (`luarocks install luacheck`) * [LuaCov](https://keplerproject.github.io/luacov) (`luarocks install luacov`) +Foreman is an un-package manager that retrieves code directly from GitHub repositories. We'll use this to get a lua code analysis tool and other utilities. The Foreman packages are listed in `foreman.toml`. + +You can install `foreman` from its [releases page](https://github.com/rojo-rbx/foreman/releases). If you have the Rust tool `cargo` installed, you can also do `cargo install foreman`. Either way, be sure the foreman binary location is in your `PATH` environment variable. + +``` +foreman github-auth <[your GitHub API token](https://github.com/settings/tokens)> +foreman install +export PATH=$PATH:~/.foreman/bin/ # you might want to add this to your .bash_profile (or similarly appropriate shell configuration) file as well +``` + +After running `foreman install`, you should be able to run `stylua src` and `selene src` commands -- just like this repository's continuous integration steps do! This helps ensure that our code and your contributions are consistently formatted and are free of trivial bugs. + Make sure you have all of the Git submodules for Roact downloaded, which include a couple extra dependencies used for testing. Finally, you can run all of Roact's tests with: @@ -44,10 +55,10 @@ luacov Before starting a pull request, open an issue about the feature or bug. This helps us prevent duplicated and wasted effort. These issues are a great place to ask for help if you run into problems! Before you submit a new pull request, check: -* Code Style: Match the [official Roblox Lua style guide](https://roblox.github.io/lua-style-guide) and the local code style -* Changelog: Add an entry to [CHANGELOG.md](CHANGELOG.md) -* Luacheck: Run [Luacheck](https://github.com/mpeterv/luacheck) on your code, no warnings allowed! +* Code Style: Run [StyLua](https://github.com/JohnnyMorganz/StyLua) to ensure your code changes follow the [official Roblox Lua style guide](https://roblox.github.io/lua-style-guide) and the local code style +* selene: Run [Selene](https://github.com/kampfkarren/selene) on your code, no warnings allowed! * Tests: They all need to pass! +* Changelog: Add an entry to [CHANGELOG.md](CHANGELOG.md) ### Code Style Roblox has an [official Lua style guide](https://roblox.github.io/lua-style-guide) which should be the general guidelines for all new code. When modifying code, follow the existing style! @@ -58,7 +69,7 @@ In short: * Double quotes * One statement per line -Eventually we'll have a tool to check these things automatically. +Use `StyLua` (instructions below) to automatically format the code to follow the coding style ### Changelog Adding an entry to [CHANGELOG.md](CHANGELOG.md) alongside your commit makes it easier for everyone to keep track of what's been changed. @@ -67,10 +78,10 @@ Add a line under the "Current master" heading. When we make a new release, all o Add a link to your pull request in the entry. We don't need to link to the related GitHub issue, since pull requests will also link to them. -### Luacheck -We use [Luacheck](https://github.com/mpeterv/luacheck) for static analysis of Lua on all of our projects. +### Selene and StyLua +We use [Selene](https://github.com/kampfkarren/selene) and [StyLua](https://github.com/JohnnyMorganz/StyLua) for static analysis of Lua on all of our projects. -From the command line, just run `luacheck src` to check the Roact source. +From the command line, just run `selene src` and `stylua -c src` to check the Roact source. You'll need to install `foreman` and run `foreman install` first, which will make both the `selene` and `stylua` tools available. You should get it working on your system, and then get a plugin for the editor you use. There are plugins available for most popular editors! @@ -97,4 +108,4 @@ When releasing a new version of Roact, do these things: 7. Write a release on GitHub: - Use the same format as the previous release - Copy the release notes from `CHANGELOG.md` - - Attach the `Roact.rbxm` built with Rojo \ No newline at end of file + - Attach the `Roact.rbxm` built with Rojo diff --git a/benchmarks/hello.bench.lua b/benchmarks/hello.bench.lua index 7b8766d9..36195b1c 100644 --- a/benchmarks/hello.bench.lua +++ b/benchmarks/hello.bench.lua @@ -11,4 +11,4 @@ return { local handle = Roact.mount(hello) Roact.unmount(handle) end, -} \ No newline at end of file +} diff --git a/benchmarks/init.server.lua b/benchmarks/init.server.lua index 5cdde7d3..7111737c 100644 --- a/benchmarks/init.server.lua +++ b/benchmarks/init.server.lua @@ -11,8 +11,7 @@ local function findBenchmarkModules(root, moduleList) end end -local function noop() -end +local function noop() end local emptyTimes = {} local function getEmptyTime(iterations) @@ -42,11 +41,7 @@ table.sort(benchmarkModules, function(a, b) return a.Name < b.Name end) -local startMessage = ( - "Starting %d benchmarks..." -):format( - #benchmarkModules -) +local startMessage = ("Starting %d benchmarks..."):format(#benchmarkModules) print(startMessage) print() @@ -70,9 +65,7 @@ for _, module in ipairs(benchmarkModules) do local totalTime = (endTime - startTime) - getEmptyTime(benchmark.iterations) - local message = ( - "Benchmark %s:\n\t(%d iterations) took %f s (%f ns/iteration)" - ):format( + local message = ("Benchmark %s:\n\t(%d iterations) took %f s (%f ns/iteration)"):format( module.Name, benchmark.iterations, totalTime, @@ -83,4 +76,4 @@ for _, module in ipairs(benchmarkModules) do print() end -print("Benchmarks complete!") \ No newline at end of file +print("Benchmarks complete!") diff --git a/benchmarks/update.bench.lua b/benchmarks/update.bench.lua index b450be4a..240133be 100644 --- a/benchmarks/update.bench.lua +++ b/benchmarks/update.bench.lua @@ -14,8 +14,11 @@ return { Roact.unmount(tree) end, step = function(i) - Roact.update(tree, Roact.createElement("StringValue", { - Value = tostring(i), - })) + Roact.update( + tree, + Roact.createElement("StringValue", { + Value = tostring(i), + }) + ) end, -} \ No newline at end of file +} diff --git a/bin/run-tests.server.lua b/bin/run-tests.server.lua index d4df9f36..a15c9b2f 100644 --- a/bin/run-tests.server.lua +++ b/bin/run-tests.server.lua @@ -1,5 +1,3 @@ --- luacheck: globals __LEMUR__ - local ReplicatedStorage = game:GetService("ReplicatedStorage") local isRobloxCli, ProcessService = pcall(game.GetService, game, "ProcessService") @@ -19,8 +17,8 @@ local results = TestEZ.TestBootstrap:run( local statusCode = (results.failureCount == 0 and #results.errors == 0) and 0 or 1 -if __LEMUR__ then +if _G.__LEMUR__ then os.exit(statusCode) elseif isRobloxCli then ProcessService:ExitAsync(statusCode) -end \ No newline at end of file +end diff --git a/examples/binding/init.lua b/examples/binding/init.lua index 36835bff..bd677903 100644 --- a/examples/binding/init.lua +++ b/examples/binding/init.lua @@ -48,4 +48,4 @@ return function() end return stop -end \ No newline at end of file +end diff --git a/examples/clock/init.lua b/examples/clock/init.lua index f9eda0ef..cfec2c47 100644 --- a/examples/clock/init.lua +++ b/examples/clock/init.lua @@ -18,17 +18,23 @@ return function() local running = true local currentTime = 0 - local handle = Roact.mount(Roact.createElement(ClockApp, { - time = currentTime, - }), PlayerGui) + local handle = Roact.mount( + Roact.createElement(ClockApp, { + time = currentTime, + }), + PlayerGui + ) spawn(function() while running do currentTime = currentTime + 1 - handle = Roact.reconcile(handle, Roact.createElement(ClockApp, { - time = currentTime, - })) + handle = Roact.reconcile( + handle, + Roact.createElement(ClockApp, { + time = currentTime, + }) + ) wait(1) end @@ -40,4 +46,4 @@ return function() end return stop -end \ No newline at end of file +end diff --git a/examples/event/init.lua b/examples/event/init.lua index 598eb452..e4209dda 100644 --- a/examples/event/init.lua +++ b/examples/event/init.lua @@ -12,7 +12,7 @@ return function() -- Attach event listeners using `Roact.Event[eventName]` -- Event listeners get `rbx` as their first parameter -- followed by their normal event arguments. - [Roact.Event.Activated] = function(rbx) + [Roact.Event.Activated] = function(_rbx) print("The button was clicked!") end, }), diff --git a/examples/hello-roact/init.lua b/examples/hello-roact/init.lua index f67cbf14..09ad91b3 100644 --- a/examples/hello-roact/init.lua +++ b/examples/hello-roact/init.lua @@ -19,4 +19,4 @@ return function() end return stop -end \ No newline at end of file +end diff --git a/examples/init.client.lua b/examples/init.client.lua index a320546c..b181f26a 100644 --- a/examples/init.client.lua +++ b/examples/init.client.lua @@ -131,4 +131,4 @@ function Examples.makeExampleList() end Examples.exampleList = Examples.makeExampleList() -Examples.exampleList.Parent = PlayerGui \ No newline at end of file +Examples.exampleList.Parent = PlayerGui diff --git a/examples/stress-test/init.lua b/examples/stress-test/init.lua index 00f60e83..853bdede 100644 --- a/examples/stress-test/init.lua +++ b/examples/stress-test/init.lua @@ -81,4 +81,4 @@ return function() end return stop -end \ No newline at end of file +end diff --git a/foreman.toml b/foreman.toml new file mode 100644 index 00000000..dedd8e87 --- /dev/null +++ b/foreman.toml @@ -0,0 +1,4 @@ +[tools] +rojo = { source = "rojo-rbx/rojo", version = "6.2.0" } +selene = { source = "Kampfkarren/selene", version = "0.14" } +stylua = { source = "JohnnyMorganz/StyLua", version = "0.11" } diff --git a/selene.toml b/selene.toml new file mode 100644 index 00000000..49fb47e5 --- /dev/null +++ b/selene.toml @@ -0,0 +1 @@ +std = "roblox+testez" diff --git a/src/Binding.lua b/src/Binding.lua index b50acf61..96987abf 100644 --- a/src/Binding.lua +++ b/src/Binding.lua @@ -76,7 +76,7 @@ function BindingInternalApi.map(upstreamBinding, predicate) end) end - function impl.update(newValue) + function impl.update(_newValue) error("Bindings created by Binding:map(fn) cannot be updated directly", 2) end @@ -96,9 +96,7 @@ function BindingInternalApi.join(upstreamBindings) for key, value in pairs(upstreamBindings) do if Type.of(value) ~= Type.Binding then - local message = ( - "Expected arg #1 to contain only bindings, but key %q had a non-binding value" - ):format( + local message = ("Expected arg #1 to contain only bindings, but key %q had a non-binding value"):format( tostring(key) ) error(message, 2) @@ -122,7 +120,7 @@ function BindingInternalApi.join(upstreamBindings) local disconnects = {} for key, upstream in pairs(upstreamBindings) do - disconnects[key] = BindingInternalApi.subscribe(upstream, function(newValue) + disconnects[key] = BindingInternalApi.subscribe(upstream, function(_newValue) callback(getValue()) end) end @@ -140,7 +138,7 @@ function BindingInternalApi.join(upstreamBindings) end end - function impl.update(newValue) + function impl.update(_newValue) error("Bindings created by joinBindings(...) cannot be updated directly", 2) end @@ -154,4 +152,4 @@ function BindingInternalApi.join(upstreamBindings) }, BindingPublicMeta) end -return BindingInternalApi \ No newline at end of file +return BindingInternalApi diff --git a/src/Binding.spec.lua b/src/Binding.spec.lua index f4fd03e9..baf9d37f 100644 --- a/src/Binding.spec.lua +++ b/src/Binding.spec.lua @@ -199,7 +199,7 @@ return function() local binding1, update1 = Binding.create(1) local binding2, update2 = Binding.create(2) - local joined = Binding.join({binding1, binding2}) + local joined = Binding.join({ binding1, binding2 }) local spy = createSpy() local disconnect = Binding.subscribe(joined, spy.value) @@ -266,4 +266,4 @@ return function() end) end) end) -end \ No newline at end of file +end diff --git a/src/Component.lua b/src/Component.lua index 04bdeaa7..b35754b2 100644 --- a/src/Component.lua +++ b/src/Component.lua @@ -104,10 +104,11 @@ function Component:setState(mapState) to call `setState` as it will interfere with in-flight updates. It's also disallowed during unmounting ]] - if lifecyclePhase == ComponentLifecyclePhase.ShouldUpdate or - lifecyclePhase == ComponentLifecyclePhase.WillUpdate or - lifecyclePhase == ComponentLifecyclePhase.Render or - lifecyclePhase == ComponentLifecyclePhase.WillUnmount + if + lifecyclePhase == ComponentLifecyclePhase.ShouldUpdate + or lifecyclePhase == ComponentLifecyclePhase.WillUpdate + or lifecyclePhase == ComponentLifecyclePhase.Render + or lifecyclePhase == ComponentLifecyclePhase.WillUnmount then local messageTemplate = invalidSetStateMessages[internalData.lifecyclePhase] @@ -143,10 +144,10 @@ function Component:setState(mapState) -- If `setState` is called in `init`, we can skip triggering an update! local derivedState = self:__getDerivedState(self.props, newState) self.state = assign(newState, derivedState) - - elseif lifecyclePhase == ComponentLifecyclePhase.DidMount or - lifecyclePhase == ComponentLifecyclePhase.DidUpdate or - lifecyclePhase == ComponentLifecyclePhase.ReconcileChildren + elseif + lifecyclePhase == ComponentLifecyclePhase.DidMount + or lifecyclePhase == ComponentLifecyclePhase.DidUpdate + or lifecyclePhase == ComponentLifecyclePhase.ReconcileChildren then --[[ During certain phases of the component lifecycle, it's acceptable to @@ -155,7 +156,6 @@ function Component:setState(mapState) ]] local derivedState = self:__getDerivedState(self.props, newState) internalData.pendingState = assign(newState, derivedState) - elseif lifecyclePhase == ComponentLifecyclePhase.Idle then -- Outside of our lifecycle, the state update is safe to make immediately self:__update(nil, newState) @@ -188,9 +188,7 @@ end function Component:render() local internalData = self[InternalData] - local message = componentMissingRenderMessage:format( - tostring(internalData.componentClass) - ) + local message = componentMissingRenderMessage:format(tostring(internalData.componentClass)) error(message, 0) end @@ -254,21 +252,26 @@ function Component:__validateProps(props) end if typeof(validator) ~= "function" then - error(("validateProps must be a function, but it is a %s.\nCheck the definition of the component %q."):format( - typeof(validator), - self.__componentName - )) + error( + ("validateProps must be a function, but it is a %s.\nCheck the definition of the component %q."):format( + typeof(validator), + self.__componentName + ) + ) end local success, failureReason = validator(props) if not success then failureReason = failureReason or "" - error(("Property validation failed in %s: %s\n\n%s"):format( - self.__componentName, - tostring(failureReason), - self:getElementTraceback() or ""), - 0) + error( + ("Property validation failed in %s: %s\n\n%s"):format( + self.__componentName, + tostring(failureReason), + self:getElementTraceback() or "" + ), + 0 + ) end end diff --git a/src/Component.spec/context.spec.lua b/src/Component.spec/context.spec.lua index 1346a335..6ab5b6cc 100644 --- a/src/Component.spec/context.spec.lua +++ b/src/Component.spec/context.spec.lua @@ -16,8 +16,7 @@ return function() self:__addContext("foo", "bar") end - function Provider:render() - end + function Provider:render() end local element = createElement(Provider) local hostParent = nil @@ -42,8 +41,7 @@ return function() } end - function Consumer:render() - end + function Consumer:render() end local Parent = Component:extend("Parent") @@ -77,8 +75,7 @@ return function() } end - function Consumer:render() - end + function Consumer:render() end local function Parent() return createElement(Consumer) @@ -135,8 +132,7 @@ return function() self:__addContext("child", "I'm here too!") end - function ChildProvider:render() - end + function ChildProvider:render() end local ParentProvider = Component:extend("ParentProvider") @@ -162,7 +158,7 @@ return function() local expectedChildContext = { parent = "I'm here!", - child = "I'm here too!" + child = "I'm here too!", } assertDeepEqual(parentNode.context, expectedParentContext) @@ -180,8 +176,7 @@ return function() } end - function Consumer:render() - end + function Consumer:render() end local Provider = Component:extend("Provider") @@ -238,8 +233,7 @@ return function() capturedContextA = captureAllContext(self) end - function ConsumerA:render() - end + function ConsumerA:render() end local ConsumerB = Component:extend("ConsumerB") @@ -250,8 +244,7 @@ return function() capturedContextB = captureAllContext(self) end - function ConsumerB:render() - end + function ConsumerB:render() end local Provider = Component:extend("Provider") @@ -294,4 +287,4 @@ return function() assertDeepEqual(capturedContextB, expectedContextB) end) -end \ No newline at end of file +end diff --git a/src/Component.spec/defaultProps.spec.lua b/src/Component.spec/defaultProps.spec.lua index 40edca03..3de800ff 100644 --- a/src/Component.spec/defaultProps.spec.lua +++ b/src/Component.spec/defaultProps.spec.lua @@ -24,8 +24,7 @@ return function() capturedProps = self.props end - function Foo:render() - end + function Foo:render() end local initialProps = { b = 4, @@ -123,4 +122,4 @@ return function() assertDeepEqual(capturedProps, expectedProps) end) -end \ No newline at end of file +end diff --git a/src/Component.spec/didMount.spec.lua b/src/Component.spec/didMount.spec.lua index b7286298..e065349e 100644 --- a/src/Component.spec/didMount.spec.lua +++ b/src/Component.spec/didMount.spec.lua @@ -32,4 +32,4 @@ return function() expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) end) -end \ No newline at end of file +end diff --git a/src/Component.spec/didUpdate.spec.lua b/src/Component.spec/didUpdate.spec.lua index 269c6f4a..431b829d 100644 --- a/src/Component.spec/didUpdate.spec.lua +++ b/src/Component.spec/didUpdate.spec.lua @@ -66,8 +66,7 @@ return function() self:setState(initialState) end - function MyComponent:render() - end + function MyComponent:render() end local element = createElement(MyComponent) local hostParent = nil @@ -89,4 +88,4 @@ return function() assertDeepEqual(values.oldProps, {}) assertDeepEqual(values.oldState, initialState) end) -end \ No newline at end of file +end diff --git a/src/Component.spec/extend.spec.lua b/src/Component.spec/extend.spec.lua index 04a433a3..d5e7f18c 100644 --- a/src/Component.spec/extend.spec.lua +++ b/src/Component.spec/extend.spec.lua @@ -26,4 +26,4 @@ return function() expect(name).to.be.a("string") expect(name:find("FooBar")).to.be.ok() end) -end \ No newline at end of file +end diff --git a/src/Component.spec/getDerivedStateFromProps.spec.lua b/src/Component.spec/getDerivedStateFromProps.spec.lua index 1f04cc8d..cd3f49bd 100644 --- a/src/Component.spec/getDerivedStateFromProps.spec.lua +++ b/src/Component.spec/getDerivedStateFromProps.spec.lua @@ -49,13 +49,20 @@ return function() local hostParent = nil local hostKey = "WithDerivedState" - local node = noopReconciler.mountVirtualNode(createElement(WithDerivedState, { - someProp = 1, - }), hostParent, hostKey) - - noopReconciler.updateVirtualNode(node, createElement(WithDerivedState, { - someProp = 2, - })) + local node = noopReconciler.mountVirtualNode( + createElement(WithDerivedState, { + someProp = 1, + }), + hostParent, + hostKey + ) + + noopReconciler.updateVirtualNode( + node, + createElement(WithDerivedState, { + someProp = 2, + }) + ) expect(getDerivedSpy.callCount).to.equal(2) @@ -276,4 +283,4 @@ return function() derived = true, }) end) -end \ No newline at end of file +end diff --git a/src/Component.spec/getElementTraceback.spec.lua b/src/Component.spec/getElementTraceback.spec.lua index 1d3e0ac9..eaea310b 100644 --- a/src/Component.spec/getElementTraceback.spec.lua +++ b/src/Component.spec/getElementTraceback.spec.lua @@ -64,4 +64,4 @@ return function() expect(stackTrace).to.equal(nil) end) -end \ No newline at end of file +end diff --git a/src/Component.spec/init.spec.lua b/src/Component.spec/init.spec.lua index af50997a..e9117616 100644 --- a/src/Component.spec/init.spec.lua +++ b/src/Component.spec/init.spec.lua @@ -38,4 +38,4 @@ return function() expect(typeof(values.props)).to.equal("table") assertDeepEqual(values.props, props) end) -end \ No newline at end of file +end diff --git a/src/Component.spec/legacyContext.spec.lua b/src/Component.spec/legacyContext.spec.lua index e1014f21..8e254079 100644 --- a/src/Component.spec/legacyContext.spec.lua +++ b/src/Component.spec/legacyContext.spec.lua @@ -15,8 +15,7 @@ return function() self._context.foo = "bar" end - function Provider:render() - end + function Provider:render() end local element = createElement(Provider) local hostParent = nil @@ -38,8 +37,7 @@ return function() capturedContext = self._context end - function Consumer:render() - end + function Consumer:render() end local Parent = Component:extend("Parent") @@ -70,8 +68,7 @@ return function() capturedContext = self._context end - function Consumer:render() - end + function Consumer:render() end local function Parent() return createElement(Consumer) @@ -100,8 +97,7 @@ return function() capturedContext = self._context end - function Consumer:render() - end + function Consumer:render() end local Provider = Component:extend("Provider") @@ -150,8 +146,7 @@ return function() capturedContextA = self._context end - function ConsumerA:render() - end + function ConsumerA:render() end local ConsumerB = Component:extend("ConsumerB") @@ -162,8 +157,7 @@ return function() capturedContextB = self._context end - function ConsumerB:render() - end + function ConsumerB:render() end local Provider = Component:extend("Provider") @@ -206,4 +200,4 @@ return function() assertDeepEqual(capturedContextB, expectedContextB) end) -end \ No newline at end of file +end diff --git a/src/Component.spec/render.spec.lua b/src/Component.spec/render.spec.lua index 8dac00a6..c8387cb7 100644 --- a/src/Component.spec/render.spec.lua +++ b/src/Component.spec/render.spec.lua @@ -147,4 +147,4 @@ return function() itSKIP("Test defaultProps on initial render", function() end) itSKIP("Test defaultProps on prop update", function() end) itSKIP("Test defaultProps on state update", function() end) -end \ No newline at end of file +end diff --git a/src/Component.spec/setState.spec.lua b/src/Component.spec/setState.spec.lua index 88ae9f5d..7e62e507 100644 --- a/src/Component.spec/setState.spec.lua +++ b/src/Component.spec/setState.spec.lua @@ -19,7 +19,7 @@ return function() function InitComponent:init() self:setState({ - a = 1 + a = 1, }) end @@ -47,7 +47,7 @@ return function() function TestComponent:render() self:setState({ - a = 1 + a = 1, }) end @@ -69,7 +69,7 @@ return function() function TestComponent:shouldUpdate() self:setState({ - a = 1 + a = 1, }) end @@ -94,7 +94,7 @@ return function() function TestComponent:willUpdate() self:setState({ - a = 1 + a = 1, }) end @@ -118,7 +118,7 @@ return function() function TestComponent:willUnmount() self:setState({ - a = 1 + a = 1, }) end @@ -146,7 +146,7 @@ return function() end self:setState({ - value = 0 + value = 0, }) end @@ -160,7 +160,7 @@ return function() expect(getStateCallback().value).to.equal(0) setStateCallback({ - value = None + value = None, }) expect(getStateCallback().value).to.equal(nil) @@ -186,7 +186,7 @@ return function() end self:setState({ - value = 0 + value = 0, }) end @@ -204,7 +204,7 @@ return function() expect(props).to.equal(getPropsCallback()) return { - value = state.value + 1 + value = state.value + 1, } end) @@ -224,7 +224,7 @@ return function() end self:setState({ - value = 0 + value = 0, }) end @@ -237,7 +237,7 @@ return function() local instance = noopReconciler.mountVirtualNode(element, nil, "Test") expect(renderCount).to.equal(1) - setStateCallback(function(state, props) + setStateCallback(function(_state, _props) return nil end) @@ -272,7 +272,7 @@ return function() return createElement(Child, { callback = function() self:setState({ - foo = "bar" + foo = "bar", }) end, }) @@ -314,7 +314,7 @@ return function() -- This guards against a stack overflow that would be OUR fault if not self.state.foo then self:setState({ - foo = "bar" + foo = "bar", }) end end, @@ -511,7 +511,7 @@ return function() setComponentState(function(state) return { - counter = state.counter + 1 + counter = state.counter + 1, } end) @@ -565,7 +565,7 @@ return function() function MyComponent:init() self:setState({ - status = "initial mount" + status = "initial mount", }) self.isMounted = false @@ -577,13 +577,13 @@ return function() function MyComponent:didMount() self:setState({ - status = "mounted" + status = "mounted", }) self.isMounted = true end - function MyComponent:didUpdate(oldProps, oldState) + function MyComponent:didUpdate(_oldProps, oldState) expect(oldState.status).to.equal("initial mount") expect(self.state.status).to.equal("mounted") diff --git a/src/Component.spec/shouldUpdate.spec.lua b/src/Component.spec/shouldUpdate.spec.lua index 9d53b989..73cf6c9a 100644 --- a/src/Component.spec/shouldUpdate.spec.lua +++ b/src/Component.spec/shouldUpdate.spec.lua @@ -172,4 +172,4 @@ return function() expect(renderSpy.callCount).to.equal(1) end) -end \ No newline at end of file +end diff --git a/src/Component.spec/validateProps.spec.lua b/src/Component.spec/validateProps.spec.lua index c7278e0f..5759de7c 100644 --- a/src/Component.spec/validateProps.spec.lua +++ b/src/Component.spec/validateProps.spec.lua @@ -105,11 +105,11 @@ return function() noopReconciler.mountVirtualNode(element, hostParent, key) expect(validatePropsSpy.callCount).to.equal(1) validatePropsSpy:assertCalledWithDeepEqual({ - a = 1 + a = 1, }) setStateCallback({ - b = 1 + b = 1, }) expect(validatePropsSpy.callCount).to.equal(1) @@ -266,4 +266,4 @@ return function() expect(validatePropsSpy.callCount).to.equal(0) end) end) -end \ No newline at end of file +end diff --git a/src/Component.spec/willUnmount.spec.lua b/src/Component.spec/willUnmount.spec.lua index 590b61d1..e1448aa4 100644 --- a/src/Component.spec/willUnmount.spec.lua +++ b/src/Component.spec/willUnmount.spec.lua @@ -33,4 +33,4 @@ return function() expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) end) -end \ No newline at end of file +end diff --git a/src/Component.spec/willUpdate.spec.lua b/src/Component.spec/willUpdate.spec.lua index b83937fe..f50cd92d 100644 --- a/src/Component.spec/willUpdate.spec.lua +++ b/src/Component.spec/willUpdate.spec.lua @@ -60,7 +60,7 @@ return function() end self:setState({ - foo = 1 + foo = 1, }) end @@ -77,7 +77,7 @@ return function() expect(willUpdateSpy.callCount).to.equal(0) setComponentState({ - foo = 2 + foo = 2, }) expect(willUpdateSpy.callCount).to.equal(1) @@ -87,7 +87,7 @@ return function() expect(Type.of(values.self)).to.equal(Type.StatefulComponentInstance) assertDeepEqual(values.newProps, {}) assertDeepEqual(values.newState, { - foo = 2 + foo = 2, }) end) -end \ No newline at end of file +end diff --git a/src/ComponentLifecyclePhase.lua b/src/ComponentLifecyclePhase.lua index dd239635..4a1c95e8 100644 --- a/src/ComponentLifecyclePhase.lua +++ b/src/ComponentLifecyclePhase.lua @@ -16,4 +16,4 @@ local ComponentLifecyclePhase = strict({ Idle = Symbol.named("idle"), }, "ComponentLifecyclePhase") -return ComponentLifecyclePhase \ No newline at end of file +return ComponentLifecyclePhase diff --git a/src/Config.lua b/src/Config.lua index 54043503..0c73a25e 100644 --- a/src/Config.lua +++ b/src/Config.lua @@ -41,15 +41,13 @@ function Config.new() self._currentConfig = setmetatable({}, { __index = function(_, key) - local message = ( - "Invalid global configuration key %q. Valid configuration keys are: %s" - ):format( + local message = ("Invalid global configuration key %q. Valid configuration keys are: %s"):format( tostring(key), table.concat(defaultConfigKeys, ", ") ) error(message, 3) - end + end, }) -- We manually bind these methods here so that the Config's methods can be @@ -77,9 +75,7 @@ function Config:set(configValues) -- We only want to apply this configuration if it's valid! for key, value in pairs(configValues) do if defaultConfig[key] == nil then - local message = ( - "Invalid global configuration key %q (type %s). Valid configuration keys are: %s" - ):format( + local message = ("Invalid global configuration key %q (type %s). Valid configuration keys are: %s"):format( tostring(key), typeof(key), table.concat(defaultConfigKeys, ", ") @@ -92,11 +88,7 @@ function Config:set(configValues) if typeof(value) ~= "boolean" then local message = ( "Invalid value %q (type %s) for global configuration key %q. Valid values are: true, false" - ):format( - tostring(value), - typeof(value), - tostring(key) - ) + ):format(tostring(value), typeof(value), tostring(key)) error(message, 3) end @@ -124,4 +116,4 @@ function Config:scoped(configValues, callback) assert(success, result) end -return Config \ No newline at end of file +return Config diff --git a/src/Config.spec.lua b/src/Config.spec.lua index 08a884fc..6e5e6bfb 100644 --- a/src/Config.spec.lua +++ b/src/Config.spec.lua @@ -49,4 +49,4 @@ return function() expect(err:find(goodKey)).to.be.ok() expect(err:find(badValue)).to.be.ok() end) -end \ No newline at end of file +end diff --git a/src/ElementKind.lua b/src/ElementKind.lua index 22e1e539..649b2fde 100644 --- a/src/ElementKind.lua +++ b/src/ElementKind.lua @@ -48,4 +48,4 @@ getmetatable(ElementKind).__index = ElementKindInternal strict(ElementKindInternal, "ElementKind") -return ElementKind \ No newline at end of file +return ElementKind diff --git a/src/ElementKind.spec.lua b/src/ElementKind.spec.lua index 80f8c4e1..197e97f7 100644 --- a/src/ElementKind.spec.lua +++ b/src/ElementKind.spec.lua @@ -30,8 +30,7 @@ return function() end) it("should handle function components", function() - local function foo() - end + local function foo() end expect(ElementKind.fromComponent(foo)).to.equal(ElementKind.Function) end) @@ -51,4 +50,4 @@ return function() expect(ElementKind.fromComponent(newproxy(true))).to.equal(nil) end) end) -end \ No newline at end of file +end diff --git a/src/ElementUtils.lua b/src/ElementUtils.lua index 971b6b19..01f2b2e3 100644 --- a/src/ElementUtils.lua +++ b/src/ElementUtils.lua @@ -96,4 +96,4 @@ function ElementUtils.getElementByKey(elements, hostKey) error("Invalid elements") end -return ElementUtils \ No newline at end of file +return ElementUtils diff --git a/src/ElementUtils.spec.lua b/src/ElementUtils.spec.lua index 3457abb6..1b0ce6dd 100644 --- a/src/ElementUtils.spec.lua +++ b/src/ElementUtils.spec.lua @@ -92,4 +92,4 @@ return function() expect(ElementUtils.getElementByKey(children, "a")).to.equal(nil) end) end) -end \ No newline at end of file +end diff --git a/src/GlobalConfig.lua b/src/GlobalConfig.lua index 32198357..76f2d900 100644 --- a/src/GlobalConfig.lua +++ b/src/GlobalConfig.lua @@ -4,4 +4,4 @@ local Config = require(script.Parent.Config) -return Config.new() \ No newline at end of file +return Config.new() diff --git a/src/GlobalConfig.spec.lua b/src/GlobalConfig.spec.lua index 760a2a36..1fa95977 100644 --- a/src/GlobalConfig.spec.lua +++ b/src/GlobalConfig.spec.lua @@ -6,4 +6,4 @@ return function() expect(GlobalConfig.set).to.be.ok() expect(GlobalConfig.get).to.be.ok() end) -end \ No newline at end of file +end diff --git a/src/Logging.lua b/src/Logging.lua index 17a9d6de..13f7190e 100644 --- a/src/Logging.lua +++ b/src/Logging.lua @@ -48,7 +48,7 @@ local logInfoMetatable = {} more easily. ]] function logInfoMetatable:__tostring() - local outputBuffer = {"LogInfo {"} + local outputBuffer = { "LogInfo {" } local errorCount = #self.errors local warningCount = #self.warnings @@ -156,4 +156,4 @@ function Logging.warnOnce(messageTemplate, ...) Logging.warn(messageTemplate, ...) end -return Logging \ No newline at end of file +return Logging diff --git a/src/None.lua b/src/None.lua index 9f25d3ae..320bd3c0 100644 --- a/src/None.lua +++ b/src/None.lua @@ -4,4 +4,4 @@ local Symbol = require(script.Parent.Symbol) -- stored in tables. local None = Symbol.named("None") -return None \ No newline at end of file +return None diff --git a/src/NoopRenderer.lua b/src/NoopRenderer.lua index 8d19157e..d74f84f1 100644 --- a/src/NoopRenderer.lua +++ b/src/NoopRenderer.lua @@ -11,14 +11,12 @@ function NoopRenderer.isHostObject(target) return target == nil end -function NoopRenderer.mountHostNode(reconciler, node) -end +function NoopRenderer.mountHostNode(_reconciler, _node) end -function NoopRenderer.unmountHostNode(reconciler, node) -end +function NoopRenderer.unmountHostNode(_reconciler, _node) end -function NoopRenderer.updateHostNode(reconciler, node, newElement) +function NoopRenderer.updateHostNode(_reconciler, node, _newElement) return node end -return NoopRenderer \ No newline at end of file +return NoopRenderer diff --git a/src/Portal.lua b/src/Portal.lua index 4db0a37a..0bb9c329 100644 --- a/src/Portal.lua +++ b/src/Portal.lua @@ -2,4 +2,4 @@ local Symbol = require(script.Parent.Symbol) local Portal = Symbol.named("Portal") -return Portal \ No newline at end of file +return Portal diff --git a/src/PropMarkers/Change.lua b/src/PropMarkers/Change.lua index 2a20adbf..c31e9dee 100644 --- a/src/PropMarkers/Change.lua +++ b/src/PropMarkers/Change.lua @@ -22,7 +22,7 @@ local changeMetatable = { } setmetatable(Change, { - __index = function(self, propertyName) + __index = function(_self, propertyName) local changeListener = { [Type] = Type.HostChangeEvent, name = propertyName, diff --git a/src/PropMarkers/Change.spec.lua b/src/PropMarkers/Change.spec.lua index 903099d8..f073cf1f 100644 --- a/src/PropMarkers/Change.spec.lua +++ b/src/PropMarkers/Change.spec.lua @@ -16,4 +16,4 @@ return function() expect(a).to.equal(b) expect(a).never.to.equal(c) end) -end \ No newline at end of file +end diff --git a/src/PropMarkers/Children.lua b/src/PropMarkers/Children.lua index 8c320ddf..9dd105cc 100644 --- a/src/PropMarkers/Children.lua +++ b/src/PropMarkers/Children.lua @@ -2,4 +2,4 @@ local Symbol = require(script.Parent.Parent.Symbol) local Children = Symbol.named("Children") -return Children \ No newline at end of file +return Children diff --git a/src/PropMarkers/Event.lua b/src/PropMarkers/Event.lua index f9aba02b..64193b02 100644 --- a/src/PropMarkers/Event.lua +++ b/src/PropMarkers/Event.lua @@ -24,7 +24,7 @@ local eventMetatable = { } setmetatable(Event, { - __index = function(self, eventName) + __index = function(_self, eventName) local event = { [Type] = Type.HostEvent, name = eventName, diff --git a/src/PropMarkers/Event.spec.lua b/src/PropMarkers/Event.spec.lua index fc34e917..eee1dfa7 100644 --- a/src/PropMarkers/Event.spec.lua +++ b/src/PropMarkers/Event.spec.lua @@ -16,4 +16,4 @@ return function() expect(a).to.equal(b) expect(a).never.to.equal(c) end) -end \ No newline at end of file +end diff --git a/src/PropMarkers/Ref.lua b/src/PropMarkers/Ref.lua index a86e4c2e..6ee5d4f4 100644 --- a/src/PropMarkers/Ref.lua +++ b/src/PropMarkers/Ref.lua @@ -2,4 +2,4 @@ local Symbol = require(script.Parent.Parent.Symbol) local Ref = Symbol.named("Ref") -return Ref \ No newline at end of file +return Ref diff --git a/src/PureComponent.lua b/src/PureComponent.lua index 02832987..ff3e09e5 100644 --- a/src/PureComponent.lua +++ b/src/PureComponent.lua @@ -38,4 +38,4 @@ function PureComponent:shouldUpdate(newProps, newState) return false end -return PureComponent \ No newline at end of file +return PureComponent diff --git a/src/PureComponent.spec.lua b/src/PureComponent.spec.lua index b1644373..1f60d109 100644 --- a/src/PureComponent.spec.lua +++ b/src/PureComponent.spec.lua @@ -72,4 +72,4 @@ return function() noopReconciler.unmountVirtualTree(tree) end) -end \ No newline at end of file +end diff --git a/src/RobloxRenderer.lua b/src/RobloxRenderer.lua index 4f528ad5..c3fff28a 100644 --- a/src/RobloxRenderer.lua +++ b/src/RobloxRenderer.lua @@ -44,9 +44,7 @@ local function applyRef(ref, newHostObject) Binding.update(ref, newHostObject) else -- TODO (#197): Better error message - error(("Invalid ref: Expected type Binding but got %s"):format( - typeof(ref) - )) + error(("Invalid ref: Expected type Binding but got %s"):format(typeof(ref))) end end diff --git a/src/RobloxRenderer.spec.lua b/src/RobloxRenderer.spec.lua index 99e3a1af..5473f1de 100644 --- a/src/RobloxRenderer.spec.lua +++ b/src/RobloxRenderer.spec.lua @@ -194,10 +194,10 @@ return function() local defaultStringValue = Instance.new("StringValue").Value local element = createElement("StringValue", { - Value = firstValue + Value = firstValue, }, { ChildA = createElement("IntValue", { - Value = 1 + Value = 1, }), ChildB = createElement("BoolValue", { Value = true, @@ -207,7 +207,7 @@ return function() }), ChildD = createElement("StringValue", { Value = "test", - }) + }), }) local node = reconciler.createVirtualNode(element, parent, key) @@ -221,7 +221,7 @@ return function() }, { -- ChildA changes element type. ChildA = createElement("StringValue", { - Value = "test" + Value = "test", }), -- ChildB changes child properties. ChildB = createElement("BoolValue", { @@ -722,7 +722,7 @@ return function() local hostParent = Instance.new("Folder") local hostKey = "Test" - local function parent(props) + local function parent(_props) return createElement("IntValue", {}, { fragmentA = createFragment({ key = createElement("StringValue", { @@ -776,8 +776,8 @@ return function() }), TheOtherValue = createElement("IntValue", { Value = 2, - }) - }) + }), + }), }) local node = reconciler.mountVirtualNode(fragment, hostParent, "Test") @@ -815,15 +815,14 @@ return function() local capturedContext function Consumer:init() capturedContext = { - hello = self:__getContext("hello") + hello = self:__getContext("hello"), } end - function Consumer:render() - end + function Consumer:render() end local element = createElement("Folder", nil, { - Consumer = createElement(Consumer) + Consumer = createElement(Consumer), }) local hostParent = nil local hostKey = "Context Test" @@ -869,14 +868,14 @@ return function() target = target, }, { Consumer = createElement(Consumer), - }) + }), }) local hostParent = nil local hostKey = "Some Key" reconciler.mountVirtualNode(element, hostParent, hostKey) assertDeepEqual(capturedContext, { - foo = "bar" + foo = "bar", }) end) end) @@ -890,11 +889,10 @@ return function() capturedContext = self._context end - function Consumer:render() - end + function Consumer:render() end local element = createElement("Folder", nil, { - Consumer = createElement(Consumer) + Consumer = createElement(Consumer), }) local hostParent = nil local hostKey = "Context Test" @@ -938,19 +936,18 @@ return function() target = target, }, { Consumer = createElement(Consumer), - }) + }), }) local hostParent = nil local hostKey = "Some Key" reconciler.mountVirtualNode(element, hostParent, hostKey) assertDeepEqual(capturedContext, { - foo = "bar" + foo = "bar", }) end) end) - describe("Integration Tests", function() local temporaryParent = nil beforeEach(function() @@ -973,7 +970,7 @@ return function() function ChildComponent:init() self:setState({ - firstTime = true + firstTime = true, }) end @@ -990,7 +987,7 @@ return function() function ChildComponent:didMount() childCoroutine = coroutine.create(function() self:setState({ - firstTime = false + firstTime = false, }) end) end @@ -999,7 +996,7 @@ return function() function ParentComponent:init() self:setState({ - count = 1 + count = 1, }) self.childAdded = function() @@ -1014,8 +1011,8 @@ return function() [Event.ChildAdded] = self.childAdded, }, { ChildComponent = createElement(ChildComponent, { - count = self.state.count - }) + count = self.state.count, + }), }) end @@ -1059,7 +1056,7 @@ return function() function ChildComponent:init() self:setState({ - firstTime = true + firstTime = true, }) end @@ -1071,14 +1068,14 @@ return function() end return createElement(LowestComponent, { - onDidMountCallback = self.props.onDidMountCallback + onDidMountCallback = self.props.onDidMountCallback, }) end function ChildComponent:didMount() childCoroutine = coroutine.create(function() self:setState({ - firstTime = false + firstTime = false, }) end) end @@ -1089,7 +1086,7 @@ return function() function ParentComponent:init() self:setState({ - count = 1 + count = 1, }) self.onDidMountCallback = function() @@ -1103,13 +1100,11 @@ return function() end function ParentComponent:render() - return createElement("Frame", { - - }, { + return createElement("Frame", {}, { ChildComponent = createElement(ChildComponent, { count = self.state.count, onDidMountCallback = self.onDidMountCallback, - }) + }), }) end @@ -1189,7 +1184,7 @@ return function() addInit(tostring(self)) self:setState({ - firstTime = true + firstTime = true, }) end @@ -1201,14 +1196,14 @@ return function() end return createElement(LowestComponent, { - onDidMountCallback = self.props.onDidMountCallback + onDidMountCallback = self.props.onDidMountCallback, }) end function ChildComponent:didMount() childCoroutine = coroutine.create(function() self:setState({ - firstTime = false + firstTime = false, }) end) end @@ -1223,7 +1218,7 @@ return function() function ParentComponent:init() self:setState({ - count = 1 + count = 1, }) self.onDidMountCallback = function() @@ -1237,13 +1232,11 @@ return function() end function ParentComponent:render() - return createElement("Frame", { - - }, { + return createElement("Frame", {}, { ChildComponent = createElement(ChildComponent, { count = self.state.count, onDidMountCallback = self.onDidMountCallback, - }) + }), }) end @@ -1286,7 +1279,7 @@ return function() return createElement("Frame") end - function LowestComponent:didUpdate(prevProps, prevState) + function LowestComponent:didUpdate(prevProps, _prevState) if prevProps.firstTime and not self.props.firstTime then self.props.onChangedCallback() end @@ -1296,7 +1289,7 @@ return function() function ChildComponent:init() self:setState({ - firstTime = true + firstTime = true, }) end @@ -1305,14 +1298,14 @@ return function() function ChildComponent:render() return createElement(LowestComponent, { firstTime = self.state.firstTime, - onChangedCallback = self.props.onChangedCallback + onChangedCallback = self.props.onChangedCallback, }) end function ChildComponent:didMount() childCoroutine = coroutine.create(function() self:setState({ - firstTime = false + firstTime = false, }) end) end @@ -1323,7 +1316,7 @@ return function() function ParentComponent:init() self:setState({ - count = 1 + count = 1, }) self.onChangedCallback = function() @@ -1337,13 +1330,11 @@ return function() end function ParentComponent:render() - return createElement("Frame", { - - }, { + return createElement("Frame", {}, { ChildComponent = createElement(ChildComponent, { count = self.state.count, onChangedCallback = self.onChangedCallback, - }) + }), }) end diff --git a/src/SingleEventManager.lua b/src/SingleEventManager.lua index bb579c78..3c5b96e9 100644 --- a/src/SingleEventManager.lua +++ b/src/SingleEventManager.lua @@ -58,10 +58,7 @@ function SingleEventManager:connectPropertyChange(key, listener) end) if not success then - error(("Cannot get changed signal on property %q: %s"):format( - tostring(key), - event - ), 0) + error(("Cannot get changed signal on property %q: %s"):format(tostring(key), event), 0) end self:_connect(CHANGE_PREFIX .. key, event, listener) @@ -126,7 +123,8 @@ function SingleEventManager:resume() local success, result = coroutine.resume( listenerCo, self._instance, - unpack(eventInvocation, 3, 2 + argumentCount)) + unpack(eventInvocation, 3, 2 + argumentCount) + ) -- If the listener threw an error, we log it as a warning, since -- there's no way to write error text in Roblox Lua without killing @@ -144,4 +142,4 @@ function SingleEventManager:resume() self._suspendedEventQueue = {} end -return SingleEventManager \ No newline at end of file +return SingleEventManager diff --git a/src/SingleEventManager.spec.lua b/src/SingleEventManager.spec.lua index 9d87e271..d978282e 100644 --- a/src/SingleEventManager.spec.lua +++ b/src/SingleEventManager.spec.lua @@ -98,7 +98,7 @@ return function() manager:resume() expect(eventSpy.callCount).to.equal(4) - assertDeepEqual(recordedValues, {1, 2, 3, 4}) + assertDeepEqual(recordedValues, { 1, 2, 3, 4 }) end) it("should not invoke events fired during suspension but disconnected before resumption", function() @@ -236,4 +236,4 @@ return function() end).to.throw() end) end) -end \ No newline at end of file +end diff --git a/src/Symbol.lua b/src/Symbol.lua index 305d66a9..3e9b951e 100644 --- a/src/Symbol.lua +++ b/src/Symbol.lua @@ -1,3 +1,4 @@ +--!nonstrict --[[ A 'Symbol' is an opaque marker type. @@ -27,4 +28,4 @@ function Symbol.named(name) return self end -return Symbol \ No newline at end of file +return Symbol diff --git a/src/Symbol.spec.lua b/src/Symbol.spec.lua index e05061da..c9be1503 100644 --- a/src/Symbol.spec.lua +++ b/src/Symbol.spec.lua @@ -21,4 +21,4 @@ return function() expect(symbolA).never.to.equal(symbolB) end) end) -end \ No newline at end of file +end diff --git a/src/Type.lua b/src/Type.lua index 156ee0ea..8c5ce212 100644 --- a/src/Type.lua +++ b/src/Type.lua @@ -45,4 +45,4 @@ end strict(TypeInternal, "Type") -return Type \ No newline at end of file +return Type diff --git a/src/Type.spec.lua b/src/Type.spec.lua index f2477093..0883fa58 100644 --- a/src/Type.spec.lua +++ b/src/Type.spec.lua @@ -15,10 +15,10 @@ return function() it("should return the assigned type", function() local test = { - [Type] = Type.Element + [Type] = Type.Element, } expect(Type.of(test)).to.equal(Type.Element) end) end) -end \ No newline at end of file +end diff --git a/src/assertDeepEqual.lua b/src/assertDeepEqual.lua index 3f422d85..c43dd26a 100644 --- a/src/assertDeepEqual.lua +++ b/src/assertDeepEqual.lua @@ -8,10 +8,7 @@ local function deepEqual(a, b) if typeof(a) ~= typeof(b) then - local message = ("{1} is of type %s, but {2} is of type %s"):format( - typeof(a), - typeof(b) - ) + local message = ("{1} is of type %s, but {2} is of type %s"):format(typeof(a), typeof(b)) return false, message end @@ -60,9 +57,7 @@ local function assertDeepEqual(a, b) local success, innerMessageTemplate = deepEqual(a, b) if not success then - local innerMessage = innerMessageTemplate - :gsub("{1}", "first") - :gsub("{2}", "second") + local innerMessage = innerMessageTemplate:gsub("{1}", "first"):gsub("{2}", "second") local message = ("Values were not deep-equal.\n%s"):format(innerMessage) @@ -70,4 +65,4 @@ local function assertDeepEqual(a, b) end end -return assertDeepEqual \ No newline at end of file +return assertDeepEqual diff --git a/src/assertDeepEqual.spec.lua b/src/assertDeepEqual.spec.lua index bece8d73..67f5f16f 100644 --- a/src/assertDeepEqual.spec.lua +++ b/src/assertDeepEqual.spec.lua @@ -28,10 +28,10 @@ return function() assertDeepEqual(someFunction, theSameFunction) local A = { - foo = someFunction + foo = someFunction, } local B = { - foo = theSameFunction + foo = theSameFunction, } assertDeepEqual(A, B) @@ -50,14 +50,14 @@ return function() nested = { foo = 1, bar = 2, - } + }, } local B = { foo = "bar", nested = { foo = 1, bar = 2, - } + }, } assertDeepEqual(A, B) @@ -67,7 +67,7 @@ return function() nested = { foo = 1, bar = 3, - } + }, } local success, message = pcall(assertDeepEqual, A, C) @@ -93,7 +93,11 @@ return function() foo = "bar", } - expect(function() assertDeepEqual(equalArgsA, nonEqualArgs) end).to.throw() - expect(function() assertDeepEqual(nonEqualArgs, equalArgsA) end).to.throw() + expect(function() + assertDeepEqual(equalArgsA, nonEqualArgs) + end).to.throw() + expect(function() + assertDeepEqual(nonEqualArgs, equalArgsA) + end).to.throw() end) -end \ No newline at end of file +end diff --git a/src/assign.lua b/src/assign.lua index 704c1659..3a18dbcd 100644 --- a/src/assign.lua +++ b/src/assign.lua @@ -24,4 +24,4 @@ local function assign(target, ...) return target end -return assign \ No newline at end of file +return assign diff --git a/src/assign.spec.lua b/src/assign.spec.lua index 24784a16..6ebe5b75 100644 --- a/src/assign.spec.lua +++ b/src/assign.spec.lua @@ -65,4 +65,4 @@ return function() expect(target.foo).to.equal(source2.foo) end) -end \ No newline at end of file +end diff --git a/src/createContext.lua b/src/createContext.lua index e4dcae15..f8b71b15 100644 --- a/src/createContext.lua +++ b/src/createContext.lua @@ -66,7 +66,7 @@ local function createConsumer(context) end end - function Consumer:init(props) + function Consumer:init(_props) -- This value may be nil, which indicates that our consumer is not a -- descendant of a provider for this context item. self.contextEntry = self:__getContext(context.key) diff --git a/src/createContext.spec.lua b/src/createContext.spec.lua index 4b436b86..ed68961e 100644 --- a/src/createContext.spec.lua +++ b/src/createContext.spec.lua @@ -115,11 +115,14 @@ return function() expect(valueSpy.callCount).to.equal(1) valueSpy:assertCalledWith("NewTest") - noopReconciler.updateVirtualTree(tree, createElement(context.Provider, { - value = "ThirdTest", - }, { - Listener = createElement(Listener), - })) + noopReconciler.updateVirtualTree( + tree, + createElement(context.Provider, { + value = "ThirdTest", + }, { + Listener = createElement(Listener), + }) + ) expect(valueSpy.callCount).to.equal(2) valueSpy:assertCalledWith("ThirdTest") @@ -165,13 +168,16 @@ return function() expect(valueSpy.callCount).to.equal(1) valueSpy:assertCalledWith("NewTest") - noopReconciler.updateVirtualTree(tree, createElement(context.Provider, { - value = "ThirdTest", - }, { - Blocker = createElement(UpdateBlocker, nil, { - Listener = createElement(Listener), - }), - })) + noopReconciler.updateVirtualTree( + tree, + createElement(context.Provider, { + value = "ThirdTest", + }, { + Blocker = createElement(UpdateBlocker, nil, { + Listener = createElement(Listener), + }), + }) + ) expect(valueSpy.callCount).to.equal(2) valueSpy:assertCalledWith("ThirdTest") @@ -312,8 +318,7 @@ return function() local context = createContext({}) local LowestComponent = Component:extend("LowestComponent") - function LowestComponent:init() - end + function LowestComponent:init() end function LowestComponent:render() return createElement("Frame") @@ -324,8 +329,7 @@ return function() end local FirstComponent = Component:extend("FirstComponent") - function FirstComponent:init() - end + function FirstComponent:init() end function FirstComponent:render() return createElement(context.Consumer, { @@ -349,7 +353,7 @@ return function() end return createElement(LowestComponent, { - onDidMountCallback = self.props.onDidMountCallback + onDidMountCallback = self.props.onDidMountCallback, }) end @@ -383,7 +387,7 @@ return function() count = self.state.count, onDidMountCallback = self.onDidMountCallback, }), - }) + }), }) end @@ -398,4 +402,4 @@ return function() childCallback() end).never.to.throw() end) -end \ No newline at end of file +end diff --git a/src/createElement.lua b/src/createElement.lua index b902219f..b3cece14 100644 --- a/src/createElement.lua +++ b/src/createElement.lua @@ -71,4 +71,4 @@ local function createElement(component, props, children) return element end -return createElement \ No newline at end of file +return createElement diff --git a/src/createElement.spec.lua b/src/createElement.spec.lua index 6e05709a..f6ed6a9d 100644 --- a/src/createElement.spec.lua +++ b/src/createElement.spec.lua @@ -18,8 +18,7 @@ return function() end) it("should create new functional elements", function() - local element = createElement(function() - end) + local element = createElement(function() end) expect(element).to.be.ok() expect(Type.of(element)).to.equal(Type.Element) @@ -107,4 +106,4 @@ return function() expect(element.source).to.be.a("string") end) end) -end \ No newline at end of file +end diff --git a/src/createFragment.lua b/src/createFragment.lua index 91554f39..8af1b28c 100644 --- a/src/createFragment.lua +++ b/src/createFragment.lua @@ -9,4 +9,4 @@ local function createFragment(elements) } end -return createFragment \ No newline at end of file +return createFragment diff --git a/src/createFragment.spec.lua b/src/createFragment.spec.lua index 45de6c71..3f16669b 100644 --- a/src/createFragment.spec.lua +++ b/src/createFragment.spec.lua @@ -14,8 +14,8 @@ return function() it("should accept children", function() local subFragment = createFragment({}) - local fragment = createFragment({key = subFragment}) + local fragment = createFragment({ key = subFragment }) expect(fragment.elements.key).to.equal(subFragment) end) -end \ No newline at end of file +end diff --git a/src/createReconciler.lua b/src/createReconciler.lua index 2ff86467..0d7046fa 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -1,3 +1,4 @@ +--!nonstrict local Type = require(script.Parent.Type) local ElementKind = require(script.Parent.ElementKind) local ElementUtils = require(script.Parent.ElementUtils) @@ -152,16 +153,16 @@ local function createReconciler(renderer) end local function updateVirtualNodeWithRenderResult(virtualNode, hostParent, renderResult) - if Type.of(renderResult) == Type.Element - or renderResult == nil - or typeof(renderResult) == "boolean" - then + if Type.of(renderResult) == Type.Element or renderResult == nil or typeof(renderResult) == "boolean" then updateChildren(virtualNode, hostParent, renderResult) else - error(("%s\n%s"):format( - "Component returned invalid children:", - virtualNode.currentElement.source or "" - ), 0) + error( + ("%s\n%s"):format( + "Component returned invalid children:", + virtualNode.currentElement.source or "" + ), + 0 + ) end end @@ -177,6 +178,7 @@ local function createReconciler(renderer) local kind = ElementKind.of(virtualNode.currentElement) + -- selene: allow(if_same_then_else) if kind == ElementKind.Host then renderer.unmountHostNode(reconciler, virtualNode) elseif kind == ElementKind.Function then @@ -302,7 +304,10 @@ local function createReconciler(renderer) ]] local function createVirtualNode(element, hostParent, hostKey, context, legacyContext) if config.internalTypeChecks then - internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") + internalAssert( + renderer.isHostObject(hostParent) or hostParent == nil, + "Expected arg #2 to be a host object" + ) internalAssert(typeof(context) == "table" or context == nil, "Expected arg #4 to be of type table or nil") internalAssert( typeof(legacyContext) == "table" or legacyContext == nil, @@ -377,7 +382,10 @@ local function createReconciler(renderer) ]] function mountVirtualNode(element, hostParent, hostKey, context, legacyContext) if config.internalTypeChecks then - internalAssert(renderer.isHostObject(hostParent) or hostParent == nil, "Expected arg #2 to be a host object") + internalAssert( + renderer.isHostObject(hostParent) or hostParent == nil, + "Expected arg #2 to be a host object" + ) internalAssert( typeof(legacyContext) == "table" or legacyContext == nil, "Expected arg #5 to be of type table or nil" diff --git a/src/createReconciler.spec.lua b/src/createReconciler.spec.lua index 193dd256..e82a68aa 100644 --- a/src/createReconciler.spec.lua +++ b/src/createReconciler.spec.lua @@ -171,7 +171,7 @@ return function() describe("Function components", function() it("should mount and unmount function components", function() - local componentSpy = createSpy(function(props) + local componentSpy = createSpy(function(_props) return nil end) @@ -197,7 +197,7 @@ return function() end) it("should mount single children of function components", function() - local childComponentSpy = createSpy(function(props) + local childComponentSpy = createSpy(function(_props) return nil end) @@ -235,11 +235,11 @@ return function() end) it("should mount fragments returned by function components", function() - local childAComponentSpy = createSpy(function(props) + local childAComponentSpy = createSpy(function(_props) return nil end) - local childBComponentSpy = createSpy(function(props) + local childBComponentSpy = createSpy(function(_props) return nil end) @@ -306,14 +306,14 @@ return function() end) it("should mount all fragment's children", function() - local childComponentSpy = createSpy(function(props) + local childComponentSpy = createSpy(function(_props) return nil end) local elements = {} local totalElements = 5 - for i=1, totalElements do - elements["key"..tostring(i)] = createElement(childComponentSpy.value, {}) + for i = 1, totalElements do + elements["key" .. tostring(i)] = createElement(childComponentSpy.value, {}) end local fragments = createFragment(elements) @@ -323,4 +323,4 @@ return function() expect(childComponentSpy.callCount).to.equal(totalElements) end) end) -end \ No newline at end of file +end diff --git a/src/createReconcilerCompat.lua b/src/createReconcilerCompat.lua index e79cf5ac..d60c9879 100644 --- a/src/createReconcilerCompat.lua +++ b/src/createReconcilerCompat.lua @@ -44,4 +44,4 @@ local function createReconcilerCompat(reconciler) return compat end -return createReconcilerCompat \ No newline at end of file +return createReconcilerCompat diff --git a/src/createReconcilerCompat.spec.lua b/src/createReconcilerCompat.spec.lua index ea4d0789..ea0ceb6f 100644 --- a/src/createReconcilerCompat.spec.lua +++ b/src/createReconcilerCompat.spec.lua @@ -79,4 +79,4 @@ return function() expect(#logInfo.warnings).to.equal(1) expect(logInfo.warnings[1]:find("reconcile")).to.be.ok() end) -end \ No newline at end of file +end diff --git a/src/createRef.lua b/src/createRef.lua index c13e1b55..ad0ef4d1 100644 --- a/src/createRef.lua +++ b/src/createRef.lua @@ -13,21 +13,21 @@ local function createRef() A ref is just redirected to a binding via its metatable ]] setmetatable(ref, { - __index = function(self, key) + __index = function(_self, key) if key == "current" then return binding:getValue() else return binding[key] end end, - __newindex = function(self, key, value) + __newindex = function(_self, key, value) if key == "current" then error("Cannot assign to the 'current' property of refs", 2) end binding[key] = value end, - __tostring = function(self) + __tostring = function(_self) return ("RoactRef(%s)"):format(tostring(binding:getValue())) end, }) @@ -35,4 +35,4 @@ local function createRef() return ref end -return createRef \ No newline at end of file +return createRef diff --git a/src/createRef.spec.lua b/src/createRef.spec.lua index 553e79d5..1b3886b2 100644 --- a/src/createRef.spec.lua +++ b/src/createRef.spec.lua @@ -52,4 +52,4 @@ return function() expect(ref:getValue()).to.equal(10) expect(ref:getValue()).to.equal(ref.current) end) -end \ No newline at end of file +end diff --git a/src/createSignal.lua b/src/createSignal.lua index f3e0add2..8fec5c71 100644 --- a/src/createSignal.lua +++ b/src/createSignal.lua @@ -17,7 +17,7 @@ local function createSignal() local suspendedConnections = {} local firing = false - local function subscribe(self, callback) + local function subscribe(_self, callback) assert(typeof(callback) == "function", "Can only subscribe to signals with a function.") local connection = { @@ -44,7 +44,7 @@ local function createSignal() return disconnect end - local function fire(self, ...) + local function fire(_self, ...) firing = true for callback, connection in pairs(connections) do if not connection.disconnected and not suspendedConnections[callback] then diff --git a/src/createSignal.spec.lua b/src/createSignal.spec.lua index 822df8d4..1d59ba0b 100644 --- a/src/createSignal.spec.lua +++ b/src/createSignal.spec.lua @@ -92,7 +92,7 @@ return function() local disconnectA local spyA = createSpy() - local listener = function(a, b) + local listener = function(_a, _b) disconnectA = signal:subscribe(spyA.value) end @@ -151,4 +151,4 @@ return function() signal:fire(a) expect(spyA.callCount).to.equal(2) end) -end \ No newline at end of file +end diff --git a/src/createSpy.lua b/src/createSpy.lua index baeba1cf..bce0b137 100644 --- a/src/createSpy.lua +++ b/src/createSpy.lua @@ -1,3 +1,4 @@ +--!strict --[[ A utility used to create a function spy that can be used to robustly test that functions are invoked the correct number of times and with the correct @@ -9,30 +10,26 @@ local assertDeepEqual = require(script.Parent.assertDeepEqual) local function createSpy(inner) - local self = { - callCount = 0, - values = {}, - valuesLength = 0, - } - + local self = {} + self.callCount = 0 + self.values = {} + self.valuesLength = 0 self.value = function(...) self.callCount = self.callCount + 1 - self.values = {...} + self.values = { ... } self.valuesLength = select("#", ...) if inner ~= nil then return inner(...) end + return nil end self.assertCalledWith = function(_, ...) local len = select("#", ...) if self.valuesLength ~= len then - error(("Expected %d arguments, but was called with %d arguments"):format( - self.valuesLength, - len - ), 2) + error(("Expected %d arguments, but was called with %d arguments"):format(self.valuesLength, len), 2) end for i = 1, len do @@ -46,10 +43,7 @@ local function createSpy(inner) local len = select("#", ...) if self.valuesLength ~= len then - error(("Expected %d arguments, but was called with %d arguments"):format( - self.valuesLength, - len - ), 2) + error(("Expected %d arguments, but was called with %d arguments"):format(self.valuesLength, len), 2) end for i = 1, len do @@ -82,4 +76,4 @@ local function createSpy(inner) return self end -return createSpy \ No newline at end of file +return createSpy diff --git a/src/createSpy.spec.lua b/src/createSpy.spec.lua index 86936939..59f6b2ca 100644 --- a/src/createSpy.spec.lua +++ b/src/createSpy.spec.lua @@ -87,4 +87,4 @@ return function() expect(captured.b).to.equal(2) end) end) -end \ No newline at end of file +end diff --git a/src/forwardRef.lua b/src/forwardRef.lua index 77fff90d..c7446c7d 100644 --- a/src/forwardRef.lua +++ b/src/forwardRef.lua @@ -25,4 +25,4 @@ local function forwardRef(render) end end -return forwardRef \ No newline at end of file +return forwardRef diff --git a/src/forwardRef.spec.lua b/src/forwardRef.spec.lua index 6d65101c..c22fdb07 100644 --- a/src/forwardRef.spec.lua +++ b/src/forwardRef.spec.lua @@ -29,19 +29,19 @@ return function() end return createElement("Frame", nil, { First = createElement("Frame", { - [Ref] = firstRef + [Ref] = firstRef, }, { Child = createElement("TextLabel", { - Text = "First" - }) + Text = "First", + }), }), Second = createElement("ScrollingFrame", { - [Ref] = secondRef + [Ref] = secondRef, }, { Child = createElement("TextLabel", { - Text = "Second" - }) - }) + Text = "Second", + }), + }), }) end @@ -69,7 +69,7 @@ return function() end) it("should support rendering nil", function() - local RefForwardingComponent = forwardRef(function(props, ref) + local RefForwardingComponent = forwardRef(function(_props, _ref) return nil end) @@ -82,7 +82,7 @@ return function() end) it("should support rendering nil for multiple children", function() - local RefForwardingComponent = forwardRef(function(props, ref) + local RefForwardingComponent = forwardRef(function(_props, _ref) return nil end) @@ -114,9 +114,12 @@ return function() end local RefForwardingComponent = forwardRef(function(props, ref) - return createElement(FunctionComponent, assign({}, props, { - forwardedRef = ref - })) + return createElement( + FunctionComponent, + assign({}, props, { + forwardedRef = ref, + }) + ) end) RefForwardingComponent.defaultProps = { optional = createElement("TextLabel"), @@ -188,7 +191,7 @@ return function() local function Child(props) value = props.value return createElement("Frame", { - [Ref] = props[Ref] + [Ref] = props[Ref], }) end @@ -212,7 +215,7 @@ return function() it("should forward a ref for multiple children", function() local function Child(props) return createElement("Frame", { - [Ref] = props[Ref] + [Ref] = props[Ref], }) end @@ -241,7 +244,7 @@ return function() local function Child(props) value = props.value return createElement("Frame", { - [Ref] = props[Ref] + [Ref] = props[Ref], }) end diff --git a/src/getDefaultInstanceProperty.lua b/src/getDefaultInstanceProperty.lua index 9a6a0950..b6c5014f 100644 --- a/src/getDefaultInstanceProperty.lua +++ b/src/getDefaultInstanceProperty.lua @@ -51,4 +51,4 @@ local function getDefaultInstanceProperty(className, propertyName) return ok, defaultValue end -return getDefaultInstanceProperty \ No newline at end of file +return getDefaultInstanceProperty diff --git a/src/getDefaultInstanceProperty.spec.lua b/src/getDefaultInstanceProperty.spec.lua index a1268203..4db61fc8 100644 --- a/src/getDefaultInstanceProperty.spec.lua +++ b/src/getDefaultInstanceProperty.spec.lua @@ -30,4 +30,4 @@ return function() expect(defaultValue).to.equal(false) end) -end \ No newline at end of file +end diff --git a/src/init.lua b/src/init.lua index 1696d235..f58d32a5 100644 --- a/src/init.lua +++ b/src/init.lua @@ -1,3 +1,4 @@ +--~strict --[[ Packages up the internals of Roact and exposes a public API for it. ]] @@ -12,7 +13,7 @@ local Binding = require(script.Binding) local robloxReconciler = createReconciler(RobloxRenderer) local reconcilerCompat = createReconcilerCompat(robloxReconciler) -local Roact = strict { +local Roact = strict({ Component = require(script.Component), createElement = require(script.createElement), createFragment = require(script.createFragment), @@ -42,8 +43,7 @@ local Roact = strict { setGlobalConfig = GlobalConfig.set, -- APIs that may change in the future without warning - UNSTABLE = { - }, -} + UNSTABLE = {}, +}) -return Roact \ No newline at end of file +return Roact diff --git a/src/init.spec.lua b/src/init.spec.lua index 23fef065..4d003350 100644 --- a/src/init.spec.lua +++ b/src/init.spec.lua @@ -44,9 +44,11 @@ return function() if not success then local existence = typeof(valueType) == "boolean" and "present" or "of type " .. valueType - local message = ( - "Expected public API member %q to be %s, but instead it was of type %s" - ):format(tostring(key), existence, typeof(Roact[key])) + local message = ("Expected public API member %q to be %s, but instead it was of type %s"):format( + tostring(key), + existence, + typeof(Roact[key]) + ) error(message) end @@ -54,12 +56,10 @@ return function() for key in pairs(Roact) do if publicApi[key] == nil then - local message = ( - "Found unknown public API key %q!" - ):format(tostring(key)) + local message = ("Found unknown public API key %q!"):format(tostring(key)) error(message) end end end) -end \ No newline at end of file +end diff --git a/src/internalAssert.lua b/src/internalAssert.lua index 87c5dfcb..11ab8c23 100644 --- a/src/internalAssert.lua +++ b/src/internalAssert.lua @@ -4,4 +4,4 @@ local function internalAssert(condition, message) end end -return internalAssert \ No newline at end of file +return internalAssert diff --git a/src/invalidSetStateMessages.lua b/src/invalidSetStateMessages.lua index 34571cee..60294bc0 100644 --- a/src/invalidSetStateMessages.lua +++ b/src/invalidSetStateMessages.lua @@ -41,4 +41,4 @@ This is a bug in Roact. It was triggered by the component %q. ]] -return invalidSetStateMessages \ No newline at end of file +return invalidSetStateMessages diff --git a/src/oneChild.lua b/src/oneChild.lua index 285d519d..0f68df17 100644 --- a/src/oneChild.lua +++ b/src/oneChild.lua @@ -25,4 +25,4 @@ local function oneChild(children) return child end -return oneChild \ No newline at end of file +return oneChild diff --git a/src/oneChild.spec.lua b/src/oneChild.spec.lua index 6540ce2d..588a466c 100644 --- a/src/oneChild.spec.lua +++ b/src/oneChild.spec.lua @@ -32,4 +32,4 @@ return function() it("should handle being passed nil", function() expect(oneChild(nil)).to.equal(nil) end) -end \ No newline at end of file +end diff --git a/src/strict.lua b/src/strict.lua index c1d21a5b..0f32dedf 100644 --- a/src/strict.lua +++ b/src/strict.lua @@ -1,27 +1,20 @@ +--!nonstrict local function strict(t, name) name = name or tostring(t) return setmetatable(t, { - __index = function(self, key) - local message = ("%q (%s) is not a valid member of %s"):format( - tostring(key), - typeof(key), - name - ) + __index = function(_self, key) + local message = ("%q (%s) is not a valid member of %s"):format(tostring(key), typeof(key), name) error(message, 2) end, - __newindex = function(self, key, value) - local message = ("%q (%s) is not a valid member of %s"):format( - tostring(key), - typeof(key), - name - ) + __newindex = function(_self, key, _value) + local message = ("%q (%s) is not a valid member of %s"):format(tostring(key), typeof(key), name) error(message, 2) end, }) end -return strict \ No newline at end of file +return strict diff --git a/src/strict.spec.lua b/src/strict.spec.lua index fc44bff7..f4d75f36 100644 --- a/src/strict.spec.lua +++ b/src/strict.spec.lua @@ -22,4 +22,4 @@ return function() t.c = 3 end).to.throw() end) -end \ No newline at end of file +end diff --git a/testez.toml b/testez.toml new file mode 100644 index 00000000..b8c0ceeb --- /dev/null +++ b/testez.toml @@ -0,0 +1,79 @@ +[[afterAll.args]] +type = "function" + +[[afterEach.args]] +type = "function" + +[[beforeAll.args]] +type = "function" + +[[beforeEach.args]] +type = "function" + +[[describe.args]] +type = "string" + +[[describe.args]] +type = "function" + +[[describeFOCUS.args]] +type = "string" + +[[describeFOCUS.args]] +type = "function" + +[[describeSKIP.args]] +type = "string" + +[[describeSKIP.args]] +type = "function" + +[[expect.args]] +type = "any" + +[[FIXME.args]] +type = "string" +required = false + +[FOCUS] +args = [] + +[[it.args]] +type = "string" + +[[it.args]] +type = "function" + +[[itFIXME.args]] +type = "string" + +[[itFIXME.args]] +type = "function" + +[[itFOCUS.args]] +type = "string" + +[[itFOCUS.args]] +type = "function" + +[[fit.args]] +type = "string" + +[[fit.args]] +type = "function" + +[[itSKIP.args]] +type = "string" + +[[itSKIP.args]] +type = "function" + +[[xit.args]] +type = "string" + +[[xit.args]] +type = "function" + +[SKIP] +args = [] + From f889158a024b1c4d1cf1c6e96af860250c931eaa Mon Sep 17 00:00:00 2001 From: Christopher Chang Date: Fri, 24 Sep 2021 18:38:59 -0700 Subject: [PATCH 61/65] Fix doc forwardRef doc referencing React instead of Roact (#310) Fixes typos in the forwardRef doc code snippets referencing React instead of Roact. --- CHANGELOG.md | 3 ++- docs/advanced/bindings-and-refs.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 844a4a2d..57f601fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Roact Changelog ## Unreleased Changes +* Fixed forwardRef doc code referencing React instead of Roact ([#310](https://github.com/Roblox/roact/pull/310)). * Fixed `Listeners can only be disconnected once` from context consumers. ([#320](https://github.com/Roblox/roact/pull/320)) @@ -8,7 +9,7 @@ * Fixed a bug where the Roact tree could get into a broken state when using callbacks passed to a child component. Updated the tempFixUpdateChildrenReEntrancy config value to also handle this case. ([#315](https://github.com/Roblox/roact/pull/315)) * Fixed forwardRef description ([#312](https://github.com/Roblox/roact/pull/312)). -## [1.4.0](https://github.com/Roblox/roact/releases/tag/v1.4.0) (November 19th, 2020) +## [1.4.0](https://github.com/Roblox/roact/releases/tag/v1.4.0) (June 3rd, 2021) * Introduce forwardRef ([#307](https://github.com/Roblox/roact/pull/307)). * Fixed a bug where the Roact tree could get into a broken state when processing changes to child instances outside the standard lifecycle. * This change is behind the config value tempFixUpdateChildrenReEntrancy ([#301](https://github.com/Roblox/roact/pull/301)) diff --git a/docs/advanced/bindings-and-refs.md b/docs/advanced/bindings-and-refs.md index 6df8d720..dd9f2ade 100644 --- a/docs/advanced/bindings-and-refs.md +++ b/docs/advanced/bindings-and-refs.md @@ -162,7 +162,7 @@ function Form:init() end function Form:render() - return React.createElement(FancyTextBox, { + return Roact.createElement(FancyTextBox, { onTextChange = function(value) print("text value updated to:", value) end @@ -181,7 +181,7 @@ end In this instance, `FancyTextBox` simply doesn't do anything with the ref passed into it. However, we can easily update it using forwardRef: ```lua -local FancyTextBox = React.forwardRef(function(props, ref) +local FancyTextBox = Roact.forwardRef(function(props, ref) return Roact.createElement("TextBox", { Multiline = true, PlaceholderText = "Enter your text here", From 6ed4b8c2859df5deed32c7d8261b311630ded58c Mon Sep 17 00:00:00 2001 From: Conor Griffin Date: Fri, 1 Oct 2021 10:48:05 -0700 Subject: [PATCH 62/65] Remove tempFixUpdateChildrenReEntrancy (#324) We have had this turned on in a few projects for the last few months and it has proven sound. Remove tempFixUpdateChildrenReEntrancy so it is fixed by default. --- src/Config.lua | 4 - src/RobloxRenderer.spec.lua | 542 +++++++++++++++++------------------- src/createReconciler.lua | 32 +-- 3 files changed, 271 insertions(+), 307 deletions(-) diff --git a/src/Config.lua b/src/Config.lua index 0c73a25e..d97c8dce 100644 --- a/src/Config.lua +++ b/src/Config.lua @@ -22,10 +22,6 @@ local defaultConfig = { ["elementTracing"] = false, -- Enables validation of component props in stateful components. ["propValidation"] = false, - - -- Temporary config for enabling a bug fix for processing events based on updates to child instances - -- outside of the standard lifecycle. - ["tempFixUpdateChildrenReEntrancy"] = false, } -- Build a list of valid configuration values up for debug messages. diff --git a/src/RobloxRenderer.spec.lua b/src/RobloxRenderer.spec.lua index 5473f1de..19be63c0 100644 --- a/src/RobloxRenderer.spec.lua +++ b/src/RobloxRenderer.spec.lua @@ -961,403 +961,379 @@ return function() end) it("should not allow re-entrancy in updateChildren", function() - local configValues = { - tempFixUpdateChildrenReEntrancy = true, - } - - GlobalConfig.scoped(configValues, function() - local ChildComponent = Component:extend("ChildComponent") + local ChildComponent = Component:extend("ChildComponent") - function ChildComponent:init() - self:setState({ - firstTime = true, - }) - end - - local childCoroutine - - function ChildComponent:render() - if self.state.firstTime then - return createElement("Frame") - end + function ChildComponent:init() + self:setState({ + firstTime = true, + }) + end - return createElement("TextLabel") - end + local childCoroutine - function ChildComponent:didMount() - childCoroutine = coroutine.create(function() - self:setState({ - firstTime = false, - }) - end) + function ChildComponent:render() + if self.state.firstTime then + return createElement("Frame") end - local ParentComponent = Component:extend("ParentComponent") + return createElement("TextLabel") + end - function ParentComponent:init() + function ChildComponent:didMount() + childCoroutine = coroutine.create(function() self:setState({ - count = 1, + firstTime = false, }) + end) + end - self.childAdded = function() - self:setState({ - count = self.state.count + 1, - }) - end - end + local ParentComponent = Component:extend("ParentComponent") - function ParentComponent:render() - return createElement("Frame", { - [Event.ChildAdded] = self.childAdded, - }, { - ChildComponent = createElement(ChildComponent, { - count = self.state.count, - }), + function ParentComponent:init() + self:setState({ + count = 1, + }) + + self.childAdded = function() + self:setState({ + count = self.state.count + 1, }) end + end - local parent = Instance.new("ScreenGui") - parent.Parent = temporaryParent + function ParentComponent:render() + return createElement("Frame", { + [Event.ChildAdded] = self.childAdded, + }, { + ChildComponent = createElement(ChildComponent, { + count = self.state.count, + }), + }) + end - local tree = createElement(ParentComponent) + local parent = Instance.new("ScreenGui") + parent.Parent = temporaryParent - local hostKey = "Some Key" - local instance = reconciler.mountVirtualNode(tree, parent, hostKey) + local tree = createElement(ParentComponent) - coroutine.resume(childCoroutine) + local hostKey = "Some Key" + local instance = reconciler.mountVirtualNode(tree, parent, hostKey) - expect(#parent:GetChildren()).to.equal(1) + coroutine.resume(childCoroutine) - local frame = parent:GetChildren()[1] + expect(#parent:GetChildren()).to.equal(1) - expect(#frame:GetChildren()).to.equal(1) + local frame = parent:GetChildren()[1] - reconciler.unmountVirtualNode(instance) - end) + expect(#frame:GetChildren()).to.equal(1) + + reconciler.unmountVirtualNode(instance) end) it("should not allow re-entrancy in updateChildren even with callbacks", function() - local configValues = { - tempFixUpdateChildrenReEntrancy = true, - } + local LowestComponent = Component:extend("LowestComponent") - GlobalConfig.scoped(configValues, function() - local LowestComponent = Component:extend("LowestComponent") + function LowestComponent:render() + return createElement("Frame") + end - function LowestComponent:render() - return createElement("Frame") - end + function LowestComponent:didMount() + self.props.onDidMountCallback() + end - function LowestComponent:didMount() - self.props.onDidMountCallback() + local ChildComponent = Component:extend("ChildComponent") + + function ChildComponent:init() + self:setState({ + firstTime = true, + }) + end + + local childCoroutine + + function ChildComponent:render() + if self.state.firstTime then + return createElement("Frame") end - local ChildComponent = Component:extend("ChildComponent") + return createElement(LowestComponent, { + onDidMountCallback = self.props.onDidMountCallback, + }) + end - function ChildComponent:init() + function ChildComponent:didMount() + childCoroutine = coroutine.create(function() self:setState({ - firstTime = true, + firstTime = false, }) - end + end) + end - local childCoroutine + local ParentComponent = Component:extend("ParentComponent") - function ChildComponent:render() - if self.state.firstTime then - return createElement("Frame") - end + local didMountCallbackCalled = 0 - return createElement(LowestComponent, { - onDidMountCallback = self.props.onDidMountCallback, - }) - end + function ParentComponent:init() + self:setState({ + count = 1, + }) - function ChildComponent:didMount() - childCoroutine = coroutine.create(function() + self.onDidMountCallback = function() + didMountCallbackCalled = didMountCallbackCalled + 1 + if self.state.count < 5 then self:setState({ - firstTime = false, + count = self.state.count + 1, }) - end) + end end + end - local ParentComponent = Component:extend("ParentComponent") + function ParentComponent:render() + return createElement("Frame", {}, { + ChildComponent = createElement(ChildComponent, { + count = self.state.count, + onDidMountCallback = self.onDidMountCallback, + }), + }) + end - local didMountCallbackCalled = 0 + local parent = Instance.new("ScreenGui") + parent.Parent = temporaryParent - function ParentComponent:init() - self:setState({ - count = 1, - }) + local tree = createElement(ParentComponent) - self.onDidMountCallback = function() - didMountCallbackCalled = didMountCallbackCalled + 1 - if self.state.count < 5 then - self:setState({ - count = self.state.count + 1, - }) - end - end - end + local hostKey = "Some Key" + local instance = reconciler.mountVirtualNode(tree, parent, hostKey) - function ParentComponent:render() - return createElement("Frame", {}, { - ChildComponent = createElement(ChildComponent, { - count = self.state.count, - onDidMountCallback = self.onDidMountCallback, - }), - }) - end + coroutine.resume(childCoroutine) - local parent = Instance.new("ScreenGui") - parent.Parent = temporaryParent + expect(#parent:GetChildren()).to.equal(1) - local tree = createElement(ParentComponent) + local frame = parent:GetChildren()[1] - local hostKey = "Some Key" - local instance = reconciler.mountVirtualNode(tree, parent, hostKey) + expect(#frame:GetChildren()).to.equal(1) - coroutine.resume(childCoroutine) + -- In an ideal world, the didMount callback would probably be called only once. Since it is called by two different + -- LowestComponent instantiations 2 is also acceptable though. + expect(didMountCallbackCalled <= 2).to.equal(true) - expect(#parent:GetChildren()).to.equal(1) + reconciler.unmountVirtualNode(instance) + end) - local frame = parent:GetChildren()[1] + it("should never call unmount twice in the case of update children re-rentrancy", function() + local unmountCounts = {} - expect(#frame:GetChildren()).to.equal(1) + local function addUnmount(id) + unmountCounts[id] = unmountCounts[id] + 1 + end - -- In an ideal world, the didMount callback would probably be called only once. Since it is called by two different - -- LowestComponent instantiations 2 is also acceptable though. - expect(didMountCallbackCalled <= 2).to.equal(true) + local function addInit(id) + unmountCounts[id] = 0 + end - reconciler.unmountVirtualNode(instance) - end) - end) + local LowestComponent = Component:extend("LowestComponent") + function LowestComponent:init() + addInit(tostring(self)) + end - it("should never call unmount twice when tempFixUpdateChildrenReEntrancy is turned on", function() - local configValues = { - tempFixUpdateChildrenReEntrancy = true, - } + function LowestComponent:render() + return createElement("Frame") + end - GlobalConfig.scoped(configValues, function() - local unmountCounts = {} + function LowestComponent:didMount() + self.props.onDidMountCallback() + end - local function addUnmount(id) - unmountCounts[id] = unmountCounts[id] + 1 - end + function LowestComponent:willUnmount() + addUnmount(tostring(self)) + end - local function addInit(id) - unmountCounts[id] = 0 - end + local FirstComponent = Component:extend("FirstComponent") + function FirstComponent:init() + addInit(tostring(self)) + end - local LowestComponent = Component:extend("LowestComponent") - function LowestComponent:init() - addInit(tostring(self)) - end + function FirstComponent:render() + return createElement("TextLabel") + end - function LowestComponent:render() - return createElement("Frame") - end + function FirstComponent:willUnmount() + addUnmount(tostring(self)) + end - function LowestComponent:didMount() - self.props.onDidMountCallback() - end + local ChildComponent = Component:extend("ChildComponent") - function LowestComponent:willUnmount() - addUnmount(tostring(self)) - end + function ChildComponent:init() + addInit(tostring(self)) - local FirstComponent = Component:extend("FirstComponent") - function FirstComponent:init() - addInit(tostring(self)) - end + self:setState({ + firstTime = true, + }) + end - function FirstComponent:render() - return createElement("TextLabel") - end + local childCoroutine - function FirstComponent:willUnmount() - addUnmount(tostring(self)) + function ChildComponent:render() + if self.state.firstTime then + return createElement(FirstComponent) end - local ChildComponent = Component:extend("ChildComponent") - - function ChildComponent:init() - addInit(tostring(self)) + return createElement(LowestComponent, { + onDidMountCallback = self.props.onDidMountCallback, + }) + end + function ChildComponent:didMount() + childCoroutine = coroutine.create(function() self:setState({ - firstTime = true, + firstTime = false, }) - end + end) + end - local childCoroutine + function ChildComponent:willUnmount() + addUnmount(tostring(self)) + end - function ChildComponent:render() - if self.state.firstTime then - return createElement(FirstComponent) - end + local ParentComponent = Component:extend("ParentComponent") - return createElement(LowestComponent, { - onDidMountCallback = self.props.onDidMountCallback, - }) - end + local didMountCallbackCalled = 0 + + function ParentComponent:init() + self:setState({ + count = 1, + }) - function ChildComponent:didMount() - childCoroutine = coroutine.create(function() + self.onDidMountCallback = function() + didMountCallbackCalled = didMountCallbackCalled + 1 + if self.state.count < 5 then self:setState({ - firstTime = false, + count = self.state.count + 1, }) - end) - end - - function ChildComponent:willUnmount() - addUnmount(tostring(self)) - end - - local ParentComponent = Component:extend("ParentComponent") - - local didMountCallbackCalled = 0 - - function ParentComponent:init() - self:setState({ - count = 1, - }) - - self.onDidMountCallback = function() - didMountCallbackCalled = didMountCallbackCalled + 1 - if self.state.count < 5 then - self:setState({ - count = self.state.count + 1, - }) - end end end + end - function ParentComponent:render() - return createElement("Frame", {}, { - ChildComponent = createElement(ChildComponent, { - count = self.state.count, - onDidMountCallback = self.onDidMountCallback, - }), - }) - end + function ParentComponent:render() + return createElement("Frame", {}, { + ChildComponent = createElement(ChildComponent, { + count = self.state.count, + onDidMountCallback = self.onDidMountCallback, + }), + }) + end - local parent = Instance.new("ScreenGui") - parent.Parent = temporaryParent + local parent = Instance.new("ScreenGui") + parent.Parent = temporaryParent - local tree = createElement(ParentComponent) + local tree = createElement(ParentComponent) - local hostKey = "Some Key" - local instance = reconciler.mountVirtualNode(tree, parent, hostKey) + local hostKey = "Some Key" + local instance = reconciler.mountVirtualNode(tree, parent, hostKey) - coroutine.resume(childCoroutine) + coroutine.resume(childCoroutine) - expect(#parent:GetChildren()).to.equal(1) + expect(#parent:GetChildren()).to.equal(1) - local frame = parent:GetChildren()[1] + local frame = parent:GetChildren()[1] - expect(#frame:GetChildren()).to.equal(1) + expect(#frame:GetChildren()).to.equal(1) - -- In an ideal world, the didMount callback would probably be called only once. Since it is called by two different - -- LowestComponent instantiations 2 is also acceptable though. - expect(didMountCallbackCalled <= 2).to.equal(true) + -- In an ideal world, the didMount callback would probably be called only once. Since it is called by two different + -- LowestComponent instantiations 2 is also acceptable though. + expect(didMountCallbackCalled <= 2).to.equal(true) - reconciler.unmountVirtualNode(instance) + reconciler.unmountVirtualNode(instance) - for _, value in pairs(unmountCounts) do - expect(value).to.equal(1) - end - end) + for _, value in pairs(unmountCounts) do + expect(value).to.equal(1) + end end) it("should never unmount a node unnecesarily in the case of re-rentry", function() - local configValues = { - tempFixUpdateChildrenReEntrancy = true, - } + local LowestComponent = Component:extend("LowestComponent") + function LowestComponent:render() + return createElement("Frame") + end - GlobalConfig.scoped(configValues, function() - local LowestComponent = Component:extend("LowestComponent") - function LowestComponent:render() - return createElement("Frame") + function LowestComponent:didUpdate(prevProps, _prevState) + if prevProps.firstTime and not self.props.firstTime then + self.props.onChangedCallback() end + end - function LowestComponent:didUpdate(prevProps, _prevState) - if prevProps.firstTime and not self.props.firstTime then - self.props.onChangedCallback() - end - end + local ChildComponent = Component:extend("ChildComponent") - local ChildComponent = Component:extend("ChildComponent") + function ChildComponent:init() + self:setState({ + firstTime = true, + }) + end - function ChildComponent:init() - self:setState({ - firstTime = true, - }) - end + local childCoroutine - local childCoroutine + function ChildComponent:render() + return createElement(LowestComponent, { + firstTime = self.state.firstTime, + onChangedCallback = self.props.onChangedCallback, + }) + end - function ChildComponent:render() - return createElement(LowestComponent, { - firstTime = self.state.firstTime, - onChangedCallback = self.props.onChangedCallback, + function ChildComponent:didMount() + childCoroutine = coroutine.create(function() + self:setState({ + firstTime = false, }) - end - - function ChildComponent:didMount() - childCoroutine = coroutine.create(function() - self:setState({ - firstTime = false, - }) - end) - end + end) + end - local ParentComponent = Component:extend("ParentComponent") + local ParentComponent = Component:extend("ParentComponent") - local onChangedCallbackCalled = 0 + local onChangedCallbackCalled = 0 - function ParentComponent:init() - self:setState({ - count = 1, - }) + function ParentComponent:init() + self:setState({ + count = 1, + }) - self.onChangedCallback = function() - onChangedCallbackCalled = onChangedCallbackCalled + 1 - if self.state.count < 5 then - self:setState({ - count = self.state.count + 1, - }) - end + self.onChangedCallback = function() + onChangedCallbackCalled = onChangedCallbackCalled + 1 + if self.state.count < 5 then + self:setState({ + count = self.state.count + 1, + }) end end + end - function ParentComponent:render() - return createElement("Frame", {}, { - ChildComponent = createElement(ChildComponent, { - count = self.state.count, - onChangedCallback = self.onChangedCallback, - }), - }) - end + function ParentComponent:render() + return createElement("Frame", {}, { + ChildComponent = createElement(ChildComponent, { + count = self.state.count, + onChangedCallback = self.onChangedCallback, + }), + }) + end - local parent = Instance.new("ScreenGui") - parent.Parent = temporaryParent + local parent = Instance.new("ScreenGui") + parent.Parent = temporaryParent - local tree = createElement(ParentComponent) + local tree = createElement(ParentComponent) - local hostKey = "Some Key" - local instance = reconciler.mountVirtualNode(tree, parent, hostKey) + local hostKey = "Some Key" + local instance = reconciler.mountVirtualNode(tree, parent, hostKey) - coroutine.resume(childCoroutine) + coroutine.resume(childCoroutine) - expect(#parent:GetChildren()).to.equal(1) + expect(#parent:GetChildren()).to.equal(1) - local frame = parent:GetChildren()[1] + local frame = parent:GetChildren()[1] - expect(#frame:GetChildren()).to.equal(1) + expect(#frame:GetChildren()).to.equal(1) - expect(onChangedCallbackCalled).to.equal(1) + expect(onChangedCallbackCalled).to.equal(1) - reconciler.unmountVirtualNode(instance) - end) + reconciler.unmountVirtualNode(instance) end) end) end diff --git a/src/createReconciler.lua b/src/createReconciler.lua index 0d7046fa..92e22346 100644 --- a/src/createReconciler.lua +++ b/src/createReconciler.lua @@ -46,14 +46,10 @@ local function createReconciler(renderer) local context = virtualNode.originalContext or virtualNode.context local parentLegacyContext = virtualNode.parentLegacyContext - if config.tempFixUpdateChildrenReEntrancy then - -- If updating this node has caused a component higher up the tree to re-render - -- and updateChildren to be re-entered then this node could already have been - -- unmounted in the previous updateChildren pass. - if not virtualNode.wasUnmounted then - unmountVirtualNode(virtualNode) - end - else + -- If updating this node has caused a component higher up the tree to re-render + -- and updateChildren to be re-entered then this node could already have been + -- unmounted in the previous updateChildren pass. + if not virtualNode.wasUnmounted then unmountVirtualNode(virtualNode) end local newNode = mountVirtualNode(newElement, hostParent, hostKey, context, parentLegacyContext) @@ -90,13 +86,11 @@ local function createReconciler(renderer) -- If updating this node has caused a component higher up the tree to re-render -- and updateChildren to be re-entered for this virtualNode then -- this result is invalid and needs to be disgarded. - if config.tempFixUpdateChildrenReEntrancy then - if virtualNode.updateChildrenCount ~= currentUpdateChildrenCount then - if newNode and newNode ~= virtualNode.children[childKey] then - unmountVirtualNode(newNode) - end - return + if virtualNode.updateChildrenCount ~= currentUpdateChildrenCount then + if newNode and newNode ~= virtualNode.children[childKey] then + unmountVirtualNode(newNode) end + return end if newNode ~= nil then @@ -129,13 +123,11 @@ local function createReconciler(renderer) -- If updating this node has caused a component higher up the tree to re-render -- and updateChildren to be re-entered for this virtualNode then -- this result is invalid and needs to be discarded. - if config.tempFixUpdateChildrenReEntrancy then - if virtualNode.updateChildrenCount ~= currentUpdateChildrenCount then - if childNode then - unmountVirtualNode(childNode) - end - return + if virtualNode.updateChildrenCount ~= currentUpdateChildrenCount then + if childNode then + unmountVirtualNode(childNode) end + return end -- mountVirtualNode can return nil if the element is a boolean From b8f203352a3b5aeba8b85c262516562e0a3a4d36 Mon Sep 17 00:00:00 2001 From: oltrep <34498770+oltrep@users.noreply.github.com> Date: Thu, 7 Oct 2021 10:05:53 -0700 Subject: [PATCH 63/65] Release 1.4.2 (#325) --- CHANGELOG.md | 3 ++- rotriever.toml | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57f601fe..59de70a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Roact Changelog ## Unreleased Changes -* Fixed forwardRef doc code referencing React instead of Roact ([#310](https://github.com/Roblox/roact/pull/310)). +## [1.4.2](https://github.com/Roblox/roact/releases/tag/v1.4.2) (October 6th, 2021) +* Fixed forwardRef doc code referencing React instead of Roact ([#310](https://github.com/Roblox/roact/pull/310)). * Fixed `Listeners can only be disconnected once` from context consumers. ([#320](https://github.com/Roblox/roact/pull/320)) ## [1.4.1](https://github.com/Roblox/roact/releases/tag/v1.4.1) (August 12th, 2021) diff --git a/rotriever.toml b/rotriever.toml index 15867ec9..63f2759a 100644 --- a/rotriever.toml +++ b/rotriever.toml @@ -1,6 +1,7 @@ [package] -name = "roblox/roact" +name = "Roact" author = "Roblox" license = "Apache-2.0" content_root = "src" -version = "1.4.1" +version = "1.4.2" +files = ["*", "!*.spec.lua"] From 903bfe9b4bb2c825c397c05aab5acafeb9de9a26 Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Fri, 8 Oct 2021 19:50:40 -0700 Subject: [PATCH 64/65] Internal processes don't yet handle strict mode (#326) Some internal Roblox processes don't yet handle strict mode entirely correctly, so we need to roll this back for now --- src/createSpy.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/src/createSpy.lua b/src/createSpy.lua index bce0b137..09a53c9e 100644 --- a/src/createSpy.lua +++ b/src/createSpy.lua @@ -1,4 +1,3 @@ ---!strict --[[ A utility used to create a function spy that can be used to robustly test that functions are invoked the correct number of times and with the correct From c2d515d926d8f0df02afcf9af64625860ed7f93d Mon Sep 17 00:00:00 2001 From: Paul Doyle <37384169+ZoteTheMighty@users.noreply.github.com> Date: Fri, 8 Oct 2021 20:07:14 -0700 Subject: [PATCH 65/65] New release to fix analysis issue --- rotriever.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rotriever.toml b/rotriever.toml index 63f2759a..5448b689 100644 --- a/rotriever.toml +++ b/rotriever.toml @@ -3,5 +3,5 @@ name = "Roact" author = "Roblox" license = "Apache-2.0" content_root = "src" -version = "1.4.2" +version = "1.4.3" files = ["*", "!*.spec.lua"]