-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
toLeopard: Avoid overwriting any browser globals, perhaps #138
Comments
Should we block browser globals? My argument on this is that we'd like to help users write "good" JavaScript code. Of course, "good" is subjective, and there's no one right answer. But creating a class called Which globals should we block? Looking at the lists of known globals, I think it would be overkill to block all of them. For example, if a human author were to write their own class called Can we generate (and update) the list automatically? I think it's a really, really good idea. My memory says that the most frequent kind of bug we've had to fix in sb-edit thus far is failing to update this list in response to changes in Leopard. To be honest, I think we could write a little script that pulls in Leopard's source code, parses it, and extracts all the method names. If you accept my previous point–that we should only block some browser globals–then updating the browser globals list is a different beast. Exactly which classes are reasonable to override is totally a vibes-based decision. If we want to automate the process of retrieving/generating these lists, we could probably come up with a metric that approximately matches our own personal ✨vibes✨. I propose a metric based on how commonly different keywords appear in JavaScript code on the web. I'm not sure if that data exists anywhere. |
We agree, but we aren't sure that source code parsing is an effective approach. We don't have experience with crawling an AST (although perhaps you do). We would rather import the Example script (this works in the Node.js REPL!)leopard = await import("leopard");
classNames = Object.keys(leopard.default);
/* [
'Color', 'Costume',
'Project', 'Sound',
'Sprite', 'Stage',
'Trigger', 'Watcher'
] */
getProtoProperties = proto =>
(proto
? Object.getOwnPropertyNames(proto)
.concat(getProtoProperties(Object.getPrototypeOf(proto)))
: []);
spriteProperties = getProtoProperties(leopard.default.Sprite.prototype).sort()
/* [
'__defineGetter__', '__defineSetter__', '__lookupGetter__',
'__lookupSetter__', '__proto__', 'andClones',
'answer', 'arrayIncludes', 'askAndWait',
'askAndWait', 'broadcast', 'broadcastAndWait',
'clearPen', 'colorTouching', 'compare',
'constructor', 'constructor', 'constructor',
'costume', 'costumeNumber', 'createClone',
'degToRad', 'degToScratch', 'deleteThisClone',
'direction', 'getSound', 'getSoundsPlayedByMe',
'glide', 'goto', 'hasOwnProperty',
'ifOnEdgeBounce', 'indexInArray', 'isPrototypeOf',
'itemOf', 'keyPressed', 'letterOf',
'loudness', 'mouse', 'move',
'moveAhead', 'moveBehind', 'nearestEdge',
'normalizeDeg', 'penColor', 'penDown',
'playSoundUntilDone', 'positionInFence', 'propertyIsEnumerable',
'radToDeg', 'radToScratch', 'random',
'restartTimer', 'say', 'sayAndWait',
'scratchTan', 'scratchToDeg', 'scratchToRad',
'sprites', 'stage', 'stamp',
'startSound', 'stopAllOfMySounds', 'stopAllSounds',
'stringIncludes', 'think', 'thinkAndWait',
'timer', 'toBoolean', 'toLocaleString',
'toNumber', 'toString', 'toString',
'touching', 'valueOf', 'vars',
'wait', 'warp', 'wrapClamp',
'x', 'y'
] */
We're OK in principle with a "ranking" or "popularity" usage based on keyword usage online. We swear W3C and TC39 (web API and ECMAScript standards organizations) make fairly frequent use of a tool that hooks into exactly this data, but we can't figure out the name of it right now! We'll look into it. We think that using this tool will be 100% based on, you know, how practical it is to hook into. Obviously if there's a periodically updated npm package that just says how popular JavaScript words are, that would be great. It's mainly a question of practicality. We don't want to introduce a system that could have a lot of edge cases or maintenance upkeep, even if it gives us a nice quantified metric ("used in >1.5% of pages processed by [tool]", etc). If it's just not practical, then we're opposed to using the full Important reminder that Maybe there would be a case in the Leopard editor to warn users that the sprite name (or global variable) that they have created overrides the same name as a "popular" JavaScript global? sb-edit can only affect a Leopard project at the time of its generation (and through the generated code's practical consequences), so it doesn't address e.g. a user deciding to create a sprite named |
From @PullJosh: #137 (comment)
We're really interested in this point. It's true that with the new PR, we avoid overwriting globals which in-scope generated Leopard code depends on, fixing certain project runtime bugs when the fates align poorly (#133).
But that's not the only reason to avoid overwriting globals. Like PullJosh wrote, users who are learning JavaScript will start to use lots of browser-provided globals. (And likely pretty quickly! BTW,
Object
is missing from your list ✨) It would be nice if generated code never overshadowed at least the popular ones.It would be pretty snazzy if the Leopard editor gracefully helped you rename sprites, but in the meanwhile (and certainly if you're working in your own text editor) it can be awkward to change a generates sprite named
Array
over to, well, some other name, when you want to start using array utilities... in the sprite... that is namedArray
. (You know, after you've introduced code in other sprites that already usesArray
, so now the string "Array" means two totally different things in your overall project code.)Some implementation thoughts:
Temporal
is a nice new global (good point) and more are getting added all the time. There's obviously a limit to what Leopard should handle, and we wouldn't be totally amiss to just select the most popular ones, or the ones that are part of the ECMAScript standard itself (rather than DOM, etc), or so on. However...globals
package. This is an abundantly popular package (75M+ weekly downloads, recommended by eslint, etc) and literally automatically up-to-date.2
.class Array2 extends Sprite
is silly. How aboutclass ArraySprite extends Sprite
? Or something else?Here are all the first-letter-capitalized names in
globals.browser
.AbortController AbortSignal AbsoluteOrientationSensor AbstractRange Accelerometer AnalyserNode Animation AnimationEffect AnimationEvent AnimationPlaybackEvent AnimationTimeline Attr Audio AudioBuffer AudioBufferSourceNode AudioContext AudioData AudioDecoder AudioDestinationNode AudioEncoder AudioListener AudioNode AudioParam AudioParamMap AudioProcessingEvent AudioScheduledSourceNode AudioSinkInfo AudioWorklet AudioWorkletGlobalScope AudioWorkletNode AudioWorkletProcessor AuthenticatorAssertionResponse AuthenticatorAttestationResponse AuthenticatorResponse BackgroundFetchManager BackgroundFetchRecord BackgroundFetchRegistration BarProp BaseAudioContext BatteryManager BeforeUnloadEvent BiquadFilterNode Blob BlobEvent Bluetooth BluetoothCharacteristicProperties BluetoothDevice BluetoothRemoteGATTCharacteristic BluetoothRemoteGATTDescriptor BluetoothRemoteGATTServer BluetoothRemoteGATTService BluetoothUUID BroadcastChannel BrowserCaptureMediaStreamTrack ByteLengthQueuingStrategy Cache CacheStorage CanvasCaptureMediaStream CanvasCaptureMediaStreamTrack CanvasGradient CanvasPattern CanvasRenderingContext2D CaptureController CaretPosition CDATASection ChannelMergerNode ChannelSplitterNode CharacterBoundsUpdateEvent CharacterData Clipboard ClipboardEvent ClipboardItem CloseEvent Comment CompositionEvent CompressionStream ConstantSourceNode ContentVisibilityAutoStateChangeEvent ConvolverNode CookieChangeEvent CookieDeprecationLabel CookieStore CookieStoreManager CountQueuingStrategy Credential CredentialsContainer CropTarget Crypto CryptoKey CSS CSSAnimation CSSConditionRule CSSContainerRule CSSCounterStyleRule CSSFontFaceRule CSSFontFeatureValuesRule CSSFontPaletteValuesRule CSSGroupingRule CSSImageValue CSSImportRule CSSKeyframeRule CSSKeyframesRule CSSKeywordValue CSSLayerBlockRule CSSLayerStatementRule CSSMathClamp CSSMathInvert CSSMathMax CSSMathMin CSSMathNegate CSSMathProduct CSSMathSum CSSMathValue CSSMatrixComponent CSSMediaRule CSSNamespaceRule CSSNumericArray CSSNumericValue CSSPageRule CSSPerspective CSSPositionTryDescriptors CSSPositionTryRule CSSPositionValue CSSPropertyRule CSSRotate CSSRule CSSRuleList CSSScale CSSScopeRule CSSSkew CSSSkewX CSSSkewY CSSStartingStyleRule CSSStyleDeclaration CSSStyleRule CSSStyleSheet CSSStyleValue CSSSupportsRule CSSTransformComponent CSSTransformValue CSSTransition CSSTranslate CSSUnitValue CSSUnparsedValue CSSVariableReferenceValue CustomElementRegistry CustomEvent CustomStateSet DataTransfer DataTransferItem DataTransferItemList DecompressionStream DelayNode DelegatedInkTrailPresenter DeviceMotionEvent DeviceMotionEventAcceleration DeviceMotionEventRotationRate DeviceOrientationEvent Document DocumentFragment DocumentPictureInPicture DocumentPictureInPictureEvent DocumentTimeline DocumentType DOMError DOMException DOMImplementation DOMMatrix DOMMatrixReadOnly DOMParser DOMPoint DOMPointReadOnly DOMQuad DOMRect DOMRectList DOMRectReadOnly DOMStringList DOMStringMap DOMTokenList DragEvent DynamicsCompressorNode EditContext Element ElementInternals EncodedAudioChunk EncodedVideoChunk ErrorEvent Event EventCounts EventSource EventTarget External EyeDropper FeaturePolicy FederatedCredential Fence FencedFrameConfig FetchLaterResult File FileList FileReader FileSystem FileSystemDirectoryEntry FileSystemDirectoryHandle FileSystemDirectoryReader FileSystemEntry FileSystemFileEntry FileSystemFileHandle FileSystemHandle FileSystemWritableFileStream FocusEvent FontData FontFace FontFaceSet FontFaceSetLoadEvent FormData FormDataEvent FragmentDirective GainNode Gamepad GamepadAxisMoveEvent GamepadButton GamepadButtonEvent GamepadEvent GamepadHapticActuator GamepadPose Geolocation GeolocationCoordinates GeolocationPosition GeolocationPositionError GPU GPUAdapter GPUAdapterInfo GPUBindGroup GPUBindGroupLayout GPUBuffer GPUBufferUsage GPUCanvasContext GPUColorWrite GPUCommandBuffer GPUCommandEncoder GPUCompilationInfo GPUCompilationMessage GPUComputePassEncoder GPUComputePipeline GPUDevice GPUDeviceLostInfo GPUError GPUExternalTexture GPUInternalError GPUMapMode GPUOutOfMemoryError GPUPipelineError GPUPipelineLayout GPUQuerySet GPUQueue GPURenderBundle GPURenderBundleEncoder GPURenderPassEncoder GPURenderPipeline GPUSampler GPUShaderModule GPUShaderStage GPUSupportedFeatures GPUSupportedLimits GPUTexture GPUTextureUsage GPUTextureView GPUUncapturedErrorEvent GPUValidationError GravitySensor Gyroscope HashChangeEvent Headers HID HIDConnectionEvent HIDDevice HIDInputReportEvent Highlight HighlightRegistry History HTMLAllCollection HTMLAnchorElement HTMLAreaElement HTMLAudioElement HTMLBaseElement HTMLBodyElement HTMLBRElement HTMLButtonElement HTMLCanvasElement HTMLCollection HTMLDataElement HTMLDataListElement HTMLDetailsElement HTMLDialogElement HTMLDirectoryElement HTMLDivElement HTMLDListElement HTMLDocument HTMLElement HTMLEmbedElement HTMLFencedFrameElement HTMLFieldSetElement HTMLFontElement HTMLFormControlsCollection HTMLFormElement HTMLFrameElement HTMLFrameSetElement HTMLHeadElement HTMLHeadingElement HTMLHRElement HTMLHtmlElement HTMLIFrameElement HTMLImageElement HTMLInputElement HTMLLabelElement HTMLLegendElement HTMLLIElement HTMLLinkElement HTMLMapElement HTMLMarqueeElement HTMLMediaElement HTMLMenuElement HTMLMetaElement HTMLMeterElement HTMLModElement HTMLObjectElement HTMLOListElement HTMLOptGroupElement HTMLOptionElement HTMLOptionsCollection HTMLOutputElement HTMLParagraphElement HTMLParamElement HTMLPictureElement HTMLPreElement HTMLProgressElement HTMLQuoteElement HTMLScriptElement HTMLSelectElement HTMLSlotElement HTMLSourceElement HTMLSpanElement HTMLStyleElement HTMLTableCaptionElement HTMLTableCellElement HTMLTableColElement HTMLTableElement HTMLTableRowElement HTMLTableSectionElement HTMLTemplateElement HTMLTextAreaElement HTMLTimeElement HTMLTitleElement HTMLTrackElement HTMLUListElement HTMLUnknownElement HTMLVideoElement IDBCursor IDBCursorWithValue IDBDatabase IDBFactory IDBIndex IDBKeyRange IDBObjectStore IDBOpenDBRequest IDBRequest IDBTransaction IDBVersionChangeEvent IdentityCredential IdentityCredentialError IdentityProvider IdleDeadline IdleDetector IIRFilterNode Image ImageBitmap ImageBitmapRenderingContext ImageCapture ImageData ImageDecoder ImageTrack ImageTrackList Ink InputDeviceCapabilities InputDeviceInfo InputEvent IntersectionObserver IntersectionObserverEntry Iterator Keyboard KeyboardEvent KeyboardLayoutMap KeyframeEffect LargestContentfulPaint LaunchParams LaunchQueue LayoutShift LayoutShiftAttribution LinearAccelerationSensor Location Lock LockManager MathMLElement MediaCapabilities MediaCapabilitiesInfo MediaDeviceInfo MediaDevices MediaElementAudioSourceNode MediaEncryptedEvent MediaError MediaKeyError MediaKeyMessageEvent MediaKeys MediaKeySession MediaKeyStatusMap MediaKeySystemAccess MediaList MediaMetadata MediaQueryList MediaQueryListEvent MediaRecorder MediaRecorderErrorEvent MediaSession MediaSource MediaSourceHandle MediaStream MediaStreamAudioDestinationNode MediaStreamAudioSourceNode MediaStreamEvent MediaStreamTrack MediaStreamTrackAudioSourceNode MediaStreamTrackAudioStats MediaStreamTrackEvent MediaStreamTrackGenerator MediaStreamTrackProcessor MediaStreamTrackVideoStats MessageChannel MessageEvent MessagePort MIDIAccess MIDIConnectionEvent MIDIInput MIDIInputMap MIDIMessageEvent MIDIOutput MIDIOutputMap MIDIPort MimeType MimeTypeArray ModelGenericSession ModelManager MouseEvent MutationEvent MutationObserver MutationRecord NamedNodeMap NavigateEvent Navigation NavigationActivation NavigationCurrentEntryChangeEvent NavigationDestination NavigationHistoryEntry NavigationPreloadManager NavigationTransition Navigator NavigatorLogin NavigatorManagedData NavigatorUAData NetworkInformation Node NodeFilter NodeIterator NodeList Notification NotifyPaintEvent NotRestoredReasonDetails NotRestoredReasons OfflineAudioCompletionEvent OfflineAudioContext OffscreenCanvas OffscreenCanvasRenderingContext2D Option OrientationSensor OscillatorNode OTPCredential OverconstrainedError PageRevealEvent PageSwapEvent PageTransitionEvent PannerNode PasswordCredential Path2D PaymentAddress PaymentManager PaymentMethodChangeEvent PaymentRequest PaymentRequestUpdateEvent PaymentResponse Performance PerformanceElementTiming PerformanceEntry PerformanceEventTiming PerformanceLongAnimationFrameTiming PerformanceLongTaskTiming PerformanceMark PerformanceMeasure PerformanceNavigation PerformanceNavigationTiming PerformanceObserver PerformanceObserverEntryList PerformancePaintTiming PerformanceResourceTiming PerformanceScriptTiming PerformanceServerTiming PerformanceTiming PeriodicSyncManager PeriodicWave Permissions PermissionStatus PERSISTENT PictureInPictureEvent PictureInPictureWindow Plugin PluginArray PointerEvent PopStateEvent Presentation PresentationAvailability PresentationConnection PresentationConnectionAvailableEvent PresentationConnectionCloseEvent PresentationConnectionList PresentationReceiver PresentationRequest PressureObserver PressureRecord ProcessingInstruction Profiler ProgressEvent PromiseRejectionEvent ProtectedAudience PublicKeyCredential PushManager PushSubscription PushSubscriptionOptions RadioNodeList Range ReadableByteStreamController ReadableStream ReadableStreamBYOBReader ReadableStreamBYOBRequest ReadableStreamDefaultController ReadableStreamDefaultReader RelativeOrientationSensor RemotePlayback ReportingObserver Request ResizeObserver ResizeObserverEntry ResizeObserverSize Response RTCCertificate RTCDataChannel RTCDataChannelEvent RTCDtlsTransport RTCDTMFSender RTCDTMFToneChangeEvent RTCEncodedAudioFrame RTCEncodedVideoFrame RTCError RTCErrorEvent RTCIceCandidate RTCIceTransport RTCPeerConnection RTCPeerConnectionIceErrorEvent RTCPeerConnectionIceEvent RTCRtpReceiver RTCRtpScriptTransform RTCRtpSender RTCRtpTransceiver RTCSctpTransport RTCSessionDescription RTCStatsReport RTCTrackEvent Scheduler Scheduling Screen ScreenDetailed ScreenDetails ScreenOrientation ScriptProcessorNode ScrollTimeline SecurityPolicyViolationEvent Selection Sensor SensorErrorEvent Serial SerialPort ServiceWorker ServiceWorkerContainer ServiceWorkerRegistration ShadowRoot SharedStorage SharedStorageWorklet SharedWorker SourceBuffer SourceBufferList SpeechSynthesis SpeechSynthesisErrorEvent SpeechSynthesisEvent SpeechSynthesisUtterance SpeechSynthesisVoice StaticRange StereoPannerNode Storage StorageBucket StorageBucketManager StorageEvent StorageManager StylePropertyMap StylePropertyMapReadOnly StyleSheet StyleSheetList SubmitEvent SubtleCrypto SVGAElement SVGAngle SVGAnimatedAngle SVGAnimatedBoolean SVGAnimatedEnumeration SVGAnimatedInteger SVGAnimatedLength SVGAnimatedLengthList SVGAnimatedNumber SVGAnimatedNumberList SVGAnimatedPreserveAspectRatio SVGAnimatedRect SVGAnimatedString SVGAnimatedTransformList SVGAnimateElement SVGAnimateMotionElement SVGAnimateTransformElement SVGAnimationElement SVGCircleElement SVGClipPathElement SVGComponentTransferFunctionElement SVGDefsElement SVGDescElement SVGElement SVGEllipseElement SVGFEBlendElement SVGFEColorMatrixElement SVGFEComponentTransferElement SVGFECompositeElement SVGFEConvolveMatrixElement SVGFEDiffuseLightingElement SVGFEDisplacementMapElement SVGFEDistantLightElement SVGFEDropShadowElement SVGFEFloodElement SVGFEFuncAElement SVGFEFuncBElement SVGFEFuncGElement SVGFEFuncRElement SVGFEGaussianBlurElement SVGFEImageElement SVGFEMergeElement SVGFEMergeNodeElement SVGFEMorphologyElement SVGFEOffsetElement SVGFEPointLightElement SVGFESpecularLightingElement SVGFESpotLightElement SVGFETileElement SVGFETurbulenceElement SVGFilterElement SVGForeignObjectElement SVGGElement SVGGeometryElement SVGGradientElement SVGGraphicsElement SVGImageElement SVGLength SVGLengthList SVGLinearGradientElement SVGLineElement SVGMarkerElement SVGMaskElement SVGMatrix SVGMetadataElement SVGMPathElement SVGNumber SVGNumberList SVGPathElement SVGPatternElement SVGPoint SVGPointList SVGPolygonElement SVGPolylineElement SVGPreserveAspectRatio SVGRadialGradientElement SVGRect SVGRectElement SVGScriptElement SVGSetElement SVGStopElement SVGStringList SVGStyleElement SVGSVGElement SVGSwitchElement SVGSymbolElement SVGTextContentElement SVGTextElement SVGTextPathElement SVGTextPositioningElement SVGTitleElement SVGTransform SVGTransformList SVGTSpanElement SVGUnitTypes SVGUseElement SVGViewElement SyncManager TaskAttributionTiming TaskController TaskPriorityChangeEvent TaskSignal TEMPORARY Text TextDecoder TextDecoderStream TextEncoder TextEncoderStream TextEvent TextFormat TextFormatUpdateEvent TextMetrics TextTrack TextTrackCue TextTrackCueList TextTrackList TextUpdateEvent TimeEvent TimeRanges ToggleEvent Touch TouchEvent TouchList TrackEvent TransformStream TransformStreamDefaultController TransitionEvent TreeWalker TrustedHTML TrustedScript TrustedScriptURL TrustedTypePolicy TrustedTypePolicyFactory UIEvent URL URLPattern URLSearchParams USB USBAlternateInterface USBConfiguration USBConnectionEvent USBDevice USBEndpoint USBInterface USBInTransferResult USBIsochronousInTransferPacket USBIsochronousInTransferResult USBIsochronousOutTransferPacket USBIsochronousOutTransferResult USBOutTransferResult UserActivation ValidityState VideoColorSpace VideoDecoder VideoEncoder VideoFrame VideoPlaybackQuality ViewTimeline ViewTransition ViewTransitionTypeSet VirtualKeyboard VirtualKeyboardGeometryChangeEvent VisibilityStateEntry VisualViewport VTTCue VTTRegion WakeLock WakeLockSentinel WaveShaperNode WebAssembly WebGL2RenderingContext WebGLActiveInfo WebGLBuffer WebGLContextEvent WebGLFramebuffer WebGLProgram WebGLQuery WebGLRenderbuffer WebGLRenderingContext WebGLSampler WebGLShader WebGLShaderPrecisionFormat WebGLSync WebGLTexture WebGLTransformFeedback WebGLUniformLocation WebGLVertexArrayObject WebSocket WebSocketError WebSocketStream WebTransport WebTransportBidirectionalStream WebTransportDatagramDuplexStream WebTransportError WebTransportReceiveStream WebTransportSendStream WGSLLanguageFeatures WheelEvent Window WindowControlsOverlay WindowControlsOverlayGeometryChangeEvent Worker Worklet WorkletGlobalScope WritableStream WritableStreamDefaultController WritableStreamDefaultWriter XMLDocument XMLHttpRequest XMLHttpRequestEventTarget XMLHttpRequestUpload XMLSerializer XPathEvaluator XPathExpression XPathResult XRAnchor XRAnchorSet XRBoundedReferenceSpace XRCamera XRCPUDepthInformation XRDepthInformation XRDOMOverlayState XRFrame XRHitTestResult XRHitTestSource XRInputSource XRInputSourceArray XRInputSourceEvent XRInputSourcesChangeEvent XRLayer XRLightEstimate XRLightProbe XRPose XRRay XRReferenceSpace XRReferenceSpaceEvent XRRenderState XRRigidTransform XRSession XRSessionEvent XRSpace XRSystem XRTransientInputHitTestResult XRTransientInputHitTestSource XRView XRViewerPose XRViewport XRWebGLBinding XRWebGLDepthInformation XRWebGLLayer XSLTProcessor
And here are just the ones under
globals.builtin
. (Please note that this is not a subset ofglobals.browser
, i.e. we need to combine them if we want to use both.)AggregateError Array ArrayBuffer Atomics BigInt BigInt64Array BigUint64Array Boolean DataView Date Error EvalError FinalizationRegistry Float32Array Float64Array Function Infinity Int16Array Int32Array Int8Array Intl JSON Map Math NaN Number Object Promise Proxy RangeError ReferenceError Reflect RegExp Set SharedArrayBuffer String Symbol SyntaxError TypeError Uint16Array Uint32Array Uint8Array Uint8ClampedArray URIError WeakMap WeakRef WeakSet
(Note that the list above includes recent additions like
AggregateError
.)So we have a few practical options:
globals.builtin
or a subset of the union [globals.builtin
,globals.browser
].globals.builtin
and combine that withglobals.browser
in its entirety.globals.builtin
in its entirety, don't bother withglobals.browser
.globals.builtin
andglobals.browser
in its entirety.I'm leaning towards the "don't hard-code anything" options, but could go either way on including
globals.browser
. There are a lot of names in that list, and most of them are extremely domain-specific. There's a decent chance that users themselves might code newSprite
subclasses with those names, and never run into a problem for it! But do feel free to convince us thatglobals.browser
should totally be part of the reserved sprite name list, if you think that's right. Or share some totally different direction, we're always interested! 🥳The text was updated successfully, but these errors were encountered: