diff --git a/broccoli/amd-compat-entrypoints/ember.debug.js b/broccoli/amd-compat-entrypoints/ember.debug.js index 9674971a6a7..9b84e7013e3 100644 --- a/broccoli/amd-compat-entrypoints/ember.debug.js +++ b/broccoli/amd-compat-entrypoints/ember.debug.js @@ -455,7 +455,7 @@ d('@glimmer/reference', glimmerReference); import * as glimmerRuntime from '@glimmer/runtime'; d('@glimmer/runtime', glimmerRuntime); -import * as glimmerTrackingIndex from '@glimmer/tracking/index'; +import * as glimmerTrackingIndex from '@glimmer/tracking'; d('@glimmer/tracking/index', glimmerTrackingIndex); import * as glimmerTrackingPrimitivesCache from '@glimmer/tracking/primitives/cache'; diff --git a/package.json b/package.json index 80bdcecaaaa..7e4bb19ef05 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "url": "git+https://github.com/emberjs/ember.js.git" }, "scripts": { + "prepare": "pnpm build", "build:js": "rollup --config", "build:types": "node types/publish.mjs", "build": "npm-run-all build:*", @@ -113,6 +114,7 @@ "@babel/types": "^7.22.5", "@embroider/shared-internals": "^2.5.0", "@glimmer/component": "workspace:^", + "@glimmer/tracking": "workspace:^", "@rollup/plugin-babel": "^6.0.4", "@simple-dom/document": "^1.4.0", "@swc-node/register": "^1.6.8", @@ -343,8 +345,6 @@ "@glimmer/program/index.js": "ember-source/@glimmer/program/index.js", "@glimmer/reference/index.js": "ember-source/@glimmer/reference/index.js", "@glimmer/runtime/index.js": "ember-source/@glimmer/runtime/index.js", - "@glimmer/tracking/index.js": "ember-source/@glimmer/tracking/index.js", - "@glimmer/tracking/primitives/cache.js": "ember-source/@glimmer/tracking/primitives/cache.js", "@glimmer/util/index.js": "ember-source/@glimmer/util/index.js", "@glimmer/validator/index.js": "ember-source/@glimmer/validator/index.js", "@glimmer/vm/index.js": "ember-source/@glimmer/vm/index.js", @@ -399,4 +399,4 @@ "node": "16.20.0", "pnpm": "8.10.0" } -} +} \ No newline at end of file diff --git a/packages/@ember/-internals/glimmer/lib/components/abstract-input.ts b/packages/@ember/-internals/glimmer/lib/components/abstract-input.ts index 01bae28b626..da53a06ca9a 100644 --- a/packages/@ember/-internals/glimmer/lib/components/abstract-input.ts +++ b/packages/@ember/-internals/glimmer/lib/components/abstract-input.ts @@ -1,4 +1,4 @@ -import { tracked } from '@ember/-internals/metal'; +import { tracked } from '@glimmer/tracking'; import { assert } from '@ember/debug'; import { action } from '@ember/object'; import type { Reference } from '@glimmer/reference'; diff --git a/packages/@ember/-internals/glimmer/tests/integration/application/rendering-test.js b/packages/@ember/-internals/glimmer/tests/integration/application/rendering-test.js index 83028cc0c6e..282484856fb 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/application/rendering-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/application/rendering-test.js @@ -4,7 +4,7 @@ import Controller from '@ember/controller'; import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { Component } from '@ember/-internals/glimmer'; -import { tracked } from '@ember/-internals/metal'; +import { tracked } from '@glimmer/tracking'; import { set } from '@ember/object'; import { backtrackingMessageFor } from '../../utils/debug-stack'; import { runTask } from '../../../../../../internal-test-helpers/lib/run'; diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js index 9dfdf6e2ae1..15203d02913 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js @@ -13,7 +13,7 @@ import { import { action } from '@ember/object'; import { run } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; -import { tracked } from '@ember/-internals/metal'; +import { tracked } from '@glimmer/tracking'; import { alias } from '@ember/object/computed'; import { on } from '@ember/object/evented'; import Service, { service } from '@ember/service'; diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js index 72f12a716fe..971f4549f72 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js @@ -2,7 +2,7 @@ import EmberObject from '@ember/object'; import { A } from '@ember/array'; import ArrayProxy from '@ember/array/proxy'; import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; -import { tracked } from '@ember/-internals/metal'; +import { tracked } from '@glimmer/tracking'; import { computed, get, set } from '@ember/object'; import { Promise } from 'rsvp'; import { moduleFor, RenderingTestCase, strip, runTask } from 'internal-test-helpers'; diff --git a/packages/@ember/-internals/glimmer/tests/integration/custom-modifier-manager-test.js b/packages/@ember/-internals/glimmer/tests/integration/custom-modifier-manager-test.js index b26992d1ccd..c86e6b37c9f 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/custom-modifier-manager-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/custom-modifier-manager-test.js @@ -10,7 +10,7 @@ import { import { Component } from '@ember/-internals/glimmer'; import { setModifierManager, modifierCapabilities } from '@glimmer/manager'; import EmberObject, { set } from '@ember/object'; -import { tracked } from '@ember/-internals/metal'; +import { tracked } from '@glimmer/tracking'; import { backtrackingMessageFor } from '../utils/debug-stack'; class ModifierManagerTest extends RenderingTestCase { diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/helper-manager-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/helper-manager-test.js index d5798d6a287..064f4df85bb 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/helper-manager-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/helper-manager-test.js @@ -1,7 +1,7 @@ import { DEBUG } from '@glimmer/env'; import { helperCapabilities, setHelperManager, setModifierManager } from '@glimmer/manager'; import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers'; -import { tracked } from '@ember/-internals/metal'; +import { tracked } from '@glimmer/tracking'; import { set } from '@ember/object'; import { setOwner } from '@ember/-internals/owner'; import Service, { service } from '@ember/service'; diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/invoke-helper-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/invoke-helper-test.js index 9c37e87b36e..84d50673e65 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/invoke-helper-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/invoke-helper-test.js @@ -1,7 +1,7 @@ import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers'; import { helperCapabilities, setHelperManager } from '@glimmer/manager'; import { Helper, helper, Component as EmberComponent } from '@ember/-internals/glimmer'; -import { tracked } from '@ember/-internals/metal'; +import { tracked } from '@glimmer/tracking'; import { set } from '@ember/object'; import { getOwner } from '@ember/-internals/owner'; import Service, { service } from '@ember/service'; diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js index 993b7942af6..ccc3741dc0f 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js @@ -1,11 +1,8 @@ import EmberObject from '@ember/object'; import { A } from '@ember/array'; import MutableArray from '@ember/array/mutable'; -import { - tracked, - nativeDescDecorator as descriptor, - notifyPropertyChange, -} from '@ember/-internals/metal'; +import { nativeDescDecorator as descriptor, notifyPropertyChange } from '@ember/-internals/metal'; +import { tracked } from '@glimmer/tracking'; import Service, { service } from '@ember/service'; import { moduleFor, RenderingTestCase, strip, runTask } from 'internal-test-helpers'; diff --git a/packages/@ember/-internals/metal/index.ts b/packages/@ember/-internals/metal/index.ts index 608bfa75037..fae91f43fb5 100644 --- a/packages/@ember/-internals/metal/index.ts +++ b/packages/@ember/-internals/metal/index.ts @@ -70,8 +70,6 @@ export { } from './lib/observer'; export { default as inject, DEBUG_INJECTION_FUNCTIONS } from './lib/injected_property'; export { tagForProperty, tagForObject, markObjectAsDirty } from './lib/tags'; -export { tracked, TrackedDescriptor } from './lib/tracked'; -export { cached } from './lib/cached'; export { createCache, getValue, isConst } from './lib/cache'; export { diff --git a/packages/@ember/-internals/metal/lib/cached.ts b/packages/@ember/-internals/metal/lib/cached.ts deleted file mode 100644 index b40ccf4a2de..00000000000 --- a/packages/@ember/-internals/metal/lib/cached.ts +++ /dev/null @@ -1,146 +0,0 @@ -// NOTE: copied from: https://github.com/glimmerjs/glimmer.js/pull/358 -// Both glimmerjs/glimmer.js and emberjs/ember.js have the exact same implementation -// of @cached, so any changes made to one should also be made to the other -import { DEBUG } from '@glimmer/env'; -import { createCache, getValue } from '@glimmer/validator'; - -/** - * @decorator - * - Gives the getter a caching behavior. The return value of the getter - will be cached until any of the properties it is entangled with - are invalidated. This is useful when a getter is expensive and - used very often. - - For instance, in this `GuestList` class, we have the `sortedGuests` - getter that sorts the guests alphabetically: - - ```javascript - import { tracked } from '@glimmer/tracking'; - - class GuestList { - @tracked guests = ['Zoey', 'Tomster']; - - get sortedGuests() { - return this.guests.slice().sort() - } - } - ``` - - Every time `sortedGuests` is accessed, a new array will be created and sorted, - because JavaScript getters do not cache by default. When the guest list - is small, like the one in the example, this is not a problem. However, if - the guest list were to grow very large, it would mean that we would be doing - a large amount of work each time we accessed `sortedGuests`. With `@cached`, - we can cache the value instead: - - ```javascript - import { tracked, cached } from '@glimmer/tracking'; - - class GuestList { - @tracked guests = ['Zoey', 'Tomster']; - - @cached - get sortedGuests() { - return this.guests.slice().sort() - } - } - ``` - - Now the `sortedGuests` getter will be cached based on autotracking. - It will only rerun and create a new sorted array when the guests tracked - property is updated. - - - ### Tradeoffs - - Overuse is discouraged. - - In general, you should avoid using `@cached` unless you have confirmed that - the getter you are decorating is computationally expensive, since `@cached` - adds a small amount of overhead to the getter. - While the individual costs are small, a systematic use of the `@cached` - decorator can add up to a large impact overall in your app. - Many getters and tracked properties are only accessed once during rendering, - and then never rerendered, so adding `@cached` when unnecessary can - negatively impact performance. - - Also, `@cached` may rerun even if the values themselves have not changed, - since tracked properties will always invalidate. - For example updating an integer value from `5` to an other `5` will trigger - a rerun of the cached properties building from this integer. - - Avoiding a cache invalidation in this case is not something that can - be achieved on the `@cached` decorator itself, but rather when updating - the underlying tracked values, by applying some diff checking mechanisms: - - ```javascript - if (nextValue !== this.trackedProp) { - this.trackedProp = nextValue; - } - ``` - - Here equal values won't update the property, therefore not triggering - the subsequent cache invalidations of the `@cached` properties who were - using this `trackedProp`. - - Remember that setting tracked data should only be done during initialization, - or as the result of a user action. Setting tracked data during render - (such as in a getter), is not supported. - - @method cached - @static - @for @glimmer/tracking - @public - */ -export const cached: MethodDecorator = (...args: any[]) => { - const [target, key, descriptor] = args; - - // Error on `@cached()`, `@cached(...args)`, and `@cached propName = value;` - if (DEBUG && target === undefined) throwCachedExtraneousParens(); - if ( - DEBUG && - (typeof target !== 'object' || - typeof key !== 'string' || - typeof descriptor !== 'object' || - args.length !== 3) - ) { - throwCachedInvalidArgsError(args); - } - if (DEBUG && (!('get' in descriptor) || typeof descriptor.get !== 'function')) { - throwCachedGetterOnlyError(key); - } - - const caches = new WeakMap(); - const getter = descriptor.get; - - descriptor.get = function (): unknown { - if (!caches.has(this)) { - caches.set(this, createCache(getter.bind(this))); - } - - return getValue(caches.get(this)); - }; -}; - -function throwCachedExtraneousParens(): never { - throw new Error( - 'You attempted to use @cached(), which is not necessary nor supported. Remove the parentheses and you will be good to go!' - ); -} - -function throwCachedGetterOnlyError(key: string): never { - throw new Error(`The @cached decorator must be applied to getters. '${key}' is not a getter.`); -} - -function throwCachedInvalidArgsError(args: unknown[] = []): never { - throw new Error( - `You attempted to use @cached on with ${ - args.length > 1 ? 'arguments' : 'an argument' - } ( @cached(${args - .map((d) => `'${d}'`) - .join( - ', ' - )}), which is not supported. Dependencies are automatically tracked, so you can just use ${'`@cached`'}` - ); -} diff --git a/packages/@ember/-internals/metal/lib/tracked.ts b/packages/@ember/-internals/metal/lib/tracked.ts deleted file mode 100644 index f5f696997f5..00000000000 --- a/packages/@ember/-internals/metal/lib/tracked.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { meta as metaFor } from '@ember/-internals/meta'; -import { isEmberArray } from '@ember/array/-internals'; -import { assert } from '@ember/debug'; -import { DEBUG } from '@glimmer/env'; -import { consumeTag, dirtyTagFor, tagFor, trackedData } from '@glimmer/validator'; -import type { ElementDescriptor } from '..'; -import { CHAIN_PASS_THROUGH } from './chain-tags'; -import type { ExtendedMethodDecorator, DecoratorPropertyDescriptor } from './decorator'; -import { COMPUTED_SETTERS, isElementDescriptor, setClassicDecorator } from './decorator'; -import { SELF_TAG } from './tags'; - -/** - @decorator - @private - - Marks a property as tracked. - - By default, a component's properties are expected to be static, - meaning you are not able to update them and have the template update accordingly. - Marking a property as tracked means that when that property changes, - a rerender of the component is scheduled so the template is kept up to date. - - There are two usages for the `@tracked` decorator, shown below. - - @example No dependencies - - If you don't pass an argument to `@tracked`, only changes to that property - will be tracked: - - ```typescript - import Component from '@glimmer/component'; - import { tracked } from '@glimmer/tracking'; - - export default class MyComponent extends Component { - @tracked - remainingApples = 10 - } - ``` - - When something changes the component's `remainingApples` property, the rerender - will be scheduled. - - @example Dependents - - In the case that you have a computed property that depends other - properties, you want to track both so that when one of the - dependents change, a rerender is scheduled. - - In the following example we have two properties, - `eatenApples`, and `remainingApples`. - - ```typescript - import Component from '@glimmer/component'; - import { tracked } from '@glimmer/tracking'; - - const totalApples = 100; - - export default class MyComponent extends Component { - @tracked - eatenApples = 0 - - get remainingApples() { - return totalApples - this.eatenApples; - } - - increment() { - this.eatenApples = this.eatenApples + 1; - } - } - ``` - - @param dependencies Optional dependents to be tracked. -*/ -export function tracked(propertyDesc: { - value: any; - initializer: () => any; -}): ExtendedMethodDecorator; -export function tracked(target: object, key: string): void; -export function tracked( - target: object, - key: string, - desc: DecoratorPropertyDescriptor -): DecoratorPropertyDescriptor; -export function tracked(...args: any[]): ExtendedMethodDecorator | DecoratorPropertyDescriptor { - assert( - `@tracked can only be used directly as a native decorator. If you're using tracked in classic classes, add parenthesis to call it like a function: tracked()`, - !(isElementDescriptor(args.slice(0, 3)) && args.length === 5 && args[4] === true) - ); - - if (!isElementDescriptor(args)) { - let propertyDesc = args[0]; - - assert( - `tracked() may only receive an options object containing 'value' or 'initializer', received ${propertyDesc}`, - args.length === 0 || (typeof propertyDesc === 'object' && propertyDesc !== null) - ); - - if (DEBUG && propertyDesc) { - let keys = Object.keys(propertyDesc); - - assert( - `The options object passed to tracked() may only contain a 'value' or 'initializer' property, not both. Received: [${keys}]`, - keys.length <= 1 && - (keys[0] === undefined || keys[0] === 'value' || keys[0] === 'initializer') - ); - - assert( - `The initializer passed to tracked must be a function. Received ${propertyDesc.initializer}`, - !('initializer' in propertyDesc) || typeof propertyDesc.initializer === 'function' - ); - } - - let initializer = propertyDesc ? propertyDesc.initializer : undefined; - let value = propertyDesc ? propertyDesc.value : undefined; - - let decorator = function ( - target: object, - key: string, - _desc?: DecoratorPropertyDescriptor, - _meta?: any, - isClassicDecorator?: boolean - ): DecoratorPropertyDescriptor { - assert( - `You attempted to set a default value for ${key} with the @tracked({ value: 'default' }) syntax. You can only use this syntax with classic classes. For native classes, you can use class initializers: @tracked field = 'default';`, - isClassicDecorator - ); - - let fieldDesc = { - initializer: initializer || (() => value), - }; - - return descriptorForField([target, key, fieldDesc]); - }; - - setClassicDecorator(decorator); - - return decorator; - } - - return descriptorForField(args); -} - -if (DEBUG) { - // Normally this isn't a classic decorator, but we want to throw a helpful - // error in development so we need it to treat it like one - setClassicDecorator(tracked); -} - -function descriptorForField([target, key, desc]: ElementDescriptor): DecoratorPropertyDescriptor { - assert( - `You attempted to use @tracked on ${key}, but that element is not a class field. @tracked is only usable on class fields. Native getters and setters will autotrack add any tracked fields they encounter, so there is no need mark getters and setters with @tracked.`, - !desc || (!desc.value && !desc.get && !desc.set) - ); - - let { getter, setter } = trackedData(key, desc ? desc.initializer : undefined); - - function get(this: object): unknown { - let value = getter(this); - - // Add the tag of the returned value if it is an array, since arrays - // should always cause updates if they are consumed and then changed - if (Array.isArray(value) || isEmberArray(value)) { - consumeTag(tagFor(value, '[]')); - } - - return value; - } - - function set(this: object, newValue: unknown): void { - setter(this, newValue); - dirtyTagFor(this, SELF_TAG); - } - - let newDesc = { - enumerable: true, - configurable: true, - isTracked: true, - - get, - set, - }; - - COMPUTED_SETTERS.add(set); - - metaFor(target).writeDescriptors(key, new TrackedDescriptor(get, set)); - - return newDesc; -} - -export class TrackedDescriptor { - constructor(private _get: () => unknown, private _set: (value: unknown) => void) { - CHAIN_PASS_THROUGH.add(this); - } - - get(obj: object): unknown { - return this._get.call(obj); - } - - set(obj: object, _key: string, value: unknown): void { - this._set.call(obj, value); - } -} diff --git a/packages/@ember/-internals/metal/tests/cached/get_test.js b/packages/@ember/-internals/metal/tests/cached/get_test.js index 1b6ca256d8b..b7330da2cf5 100644 --- a/packages/@ember/-internals/metal/tests/cached/get_test.js +++ b/packages/@ember/-internals/metal/tests/cached/get_test.js @@ -1,5 +1,5 @@ import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; -import { cached, tracked } from '../..'; +import { cached, tracked } from '@glimmer/tracking'; moduleFor( '@cached decorator: get', diff --git a/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js b/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js index 550bf55d330..f9c661c7500 100644 --- a/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js @@ -1,5 +1,6 @@ import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; -import { defineProperty, tracked, nativeDescDecorator } from '../..'; +import { defineProperty, nativeDescDecorator } from '../..'; +import { tracked } from '@glimmer/tracking'; import { track, valueForTag, validateTag } from '@glimmer/validator'; diff --git a/packages/@ember/-internals/metal/tests/tracked/get_test.js b/packages/@ember/-internals/metal/tests/tracked/get_test.js index 024b7f1b19c..668c12bc570 100644 --- a/packages/@ember/-internals/metal/tests/tracked/get_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/get_test.js @@ -1,5 +1,6 @@ import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; -import { get, tracked } from '../..'; +import { get } from '../..'; +import { tracked } from '@glimmer/tracking'; let createObj = function () { class Obj { diff --git a/packages/@ember/-internals/metal/tests/tracked/set_test.js b/packages/@ember/-internals/metal/tests/tracked/set_test.js index 389bcf0ec6a..a736316bb38 100644 --- a/packages/@ember/-internals/metal/tests/tracked/set_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/set_test.js @@ -1,5 +1,6 @@ import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; -import { get, set, tracked } from '../..'; +import { get, set } from '../..'; +import { tracked } from '@glimmer/tracking'; let createObj = () => { class Obj { diff --git a/packages/@ember/-internals/metal/tests/tracked/validation_test.js b/packages/@ember/-internals/metal/tests/tracked/validation_test.js index 307c35dcc4e..d1d13f223ce 100644 --- a/packages/@ember/-internals/metal/tests/tracked/validation_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/validation_test.js @@ -1,12 +1,5 @@ -import { - computed, - defineProperty, - get, - set, - tagForProperty, - tracked, - notifyPropertyChange, -} from '../..'; +import { computed, defineProperty, get, set, tagForProperty, notifyPropertyChange } from '../..'; +import { tracked } from '@glimmer/tracking'; import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; import { track, valueForTag, validateTag } from '@glimmer/validator'; diff --git a/packages/@ember/-internals/package.json b/packages/@ember/-internals/package.json index a6015038caa..a3c1df37286 100644 --- a/packages/@ember/-internals/package.json +++ b/packages/@ember/-internals/package.json @@ -54,6 +54,7 @@ "@glimmer/reference": "0.92.3", "@glimmer/runtime": "0.92.4", "@glimmer/syntax": "0.92.3", + "@glimmer/tracking": "workspace:^", "@glimmer/util": "0.92.3", "@glimmer/validator": "0.92.3", "@glimmer/vm": "0.92.3", diff --git a/packages/@ember/object/package.json b/packages/@ember/object/package.json index 77553fadfe4..0de5a91ece2 100644 --- a/packages/@ember/object/package.json +++ b/packages/@ember/object/package.json @@ -30,6 +30,7 @@ "@glimmer/env": "^0.1.7", "@glimmer/manager": "0.92.4", "@glimmer/owner": "0.92.3", + "@glimmer/tracking": "workspace:*", "@glimmer/util": "0.92.3", "@glimmer/validator": "0.92.3", "expect-type": "^0.15.0", diff --git a/packages/@ember/object/tests/computed/dependent-key-compat-test.js b/packages/@ember/object/tests/computed/dependent-key-compat-test.js index 7a1fc958621..afe9fa56327 100644 --- a/packages/@ember/object/tests/computed/dependent-key-compat-test.js +++ b/packages/@ember/object/tests/computed/dependent-key-compat-test.js @@ -1,5 +1,5 @@ import EmberObject, { computed, observer } from '@ember/object'; -import { tracked } from '@ember/-internals/metal'; +import { tracked } from '@glimmer/tracking'; import { dependentKeyCompat } from '@ember/object/compat'; import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; diff --git a/packages/@glimmer/tracking/addon-main.cjs b/packages/@glimmer/tracking/addon-main.cjs new file mode 100644 index 00000000000..9ed282ea5f3 --- /dev/null +++ b/packages/@glimmer/tracking/addon-main.cjs @@ -0,0 +1,5 @@ +/* eslint-env node */ +'use strict'; + +const { addonV1Shim } = require('@embroider/addon-shim'); +module.exports = addonV1Shim(__dirname); diff --git a/packages/@glimmer/tracking/index.ts b/packages/@glimmer/tracking/index.ts deleted file mode 100644 index 629afe60472..00000000000 --- a/packages/@glimmer/tracking/index.ts +++ /dev/null @@ -1,204 +0,0 @@ -export { tracked, cached } from '@ember/-internals/metal'; - -/** - In order to tell Ember a value might change, we need to mark it as trackable. - Trackable values are values that: - - - Can change over their component’s lifetime and - - Should cause Ember to rerender if and when they change - - We can do this by marking the field with the `@tracked` decorator. - - @module @glimmer/tracking - @public -*/ - -/** - Marks a property as tracked. By default, values that are rendered in Ember app - templates are _static_, meaning that updates to them won't cause the - application to rerender. Marking a property as tracked means that when that - property changes, any templates that used that property, directly or - indirectly, will rerender. For instance, consider this component: - - ```handlebars -
Count: {{this.count}}
-
Times Ten: {{this.timesTen}}
-
- -
- ``` - - ```javascript - import Component from '@glimmer/component'; - import { tracked } from '@glimmer/tracking'; - import { action } from '@ember/object'; - - export default class CounterComponent extends Component { - @tracked count = 0; - - get timesTen() { - return this.count * 10; - } - - @action - plusOne() { - this.count += 1; - } - } - ``` - - Both the `{{this.count}}` and the `{{this.timesTen}}` properties in the - template will update whenever the button is clicked. Any tracked properties - that are used in any way to calculate a value that is used in the template - will cause a rerender when updated - this includes through method calls and - other means: - - ```javascript - import Component from '@glimmer/component'; - import { tracked } from '@glimmer/tracking'; - - class Entry { - @tracked name; - @tracked phoneNumber; - - constructor(name, phoneNumber) { - this.name = name; - this.phoneNumber = phoneNumber; - } - } - - export default class PhoneBookComponent extends Component { - entries = [ - new Entry('Pizza Palace', 5551234), - new Entry('1st Street Cleaners', 5554321), - new Entry('Plants R Us', 5552468), - ]; - - // Any usage of this property will update whenever any of the names in the - // entries arrays are updated - get names() { - return this.entries.map(e => e.name); - } - - // Any usage of this property will update whenever any of the numbers in the - // entries arrays are updated - get numbers() { - return this.getFormattedNumbers(); - } - - getFormattedNumbers() { - return this.entries - .map(e => e.phoneNumber) - .map(number => { - let numberString = '' + number; - - return numberString.slice(0, 3) + '-' + numberString.slice(3); - }); - } - } - ``` - - It's important to note that setting tracked properties will always trigger an - update, even if the property is set to the same value as it was before. - - ```js - let entry = new Entry('Pizza Palace', 5551234); - // if entry was used when rendering, this would cause a rerender, even though - // the name is being set to the same value as it was before - entry.name = entry.name; - ``` - - `tracked` can also be used with the classic Ember object model in a similar - manner to classic computed properties: - - ```javascript - import EmberObject from '@ember/object'; - import { tracked } from '@glimmer/tracking'; - - const Entry = EmberObject.extend({ - name: tracked(), - phoneNumber: tracked() - }); - ``` - - Often this is unnecessary, but to ensure robust auto-tracking behavior it is - advisable to mark tracked state appropriately wherever possible. - This form of `tracked` also accepts an optional configuration object - containing either an initial `value` or an `initializer` function (but not - both). - - ```javascript - import EmberObject from '@ember/object'; - import { tracked } from '@glimmer/tracking'; - - const Entry = EmberObject.extend({ - name: tracked({ value: 'Zoey' }), - favoriteSongs: tracked({ - initializer: () => ['Raspberry Beret', 'Time After Time'] - }) - }); - ``` - - @method tracked - @static - @for @glimmer/tracking - @public -*/ - -/** - The `@cached` decorator can be used on getters in order to cache the return - value of the getter. This is useful when a getter is expensive and used very - often. For instance, in this guest list class, we have the `sortedGuests` - getter that sorts the guests alphabetically: - - ```js - import { tracked } from '@glimmer/tracking'; - - class GuestList { - @tracked guests = ['Zoey', 'Tomster']; - - get sortedGuests() { - return this.guests.slice().sort() - } - } - ``` - - Every time `sortedGuests` is accessed, a new array will be created and sorted, - because JavaScript getters do not cache by default. When the guest list is - small, like the one in the example, this is not a problem. However, if the guest - list were to grow very large, it would mean that we would be doing a large - amount of work each time we accessed `sortedGetters`. With `@cached`, we can - cache the value instead: - - ```js - import { tracked, cached } from '@glimmer/tracking'; - - class GuestList { - @tracked guests = ['Zoey', 'Tomster']; - - @cached - get sortedGuests() { - return this.guests.slice().sort() - } - } - ``` - - Now the `sortedGuests` getter will be cached based on _autotracking_. It will - only rerun and create a new sorted array when the `guests` tracked property is - updated. - - In general, you should avoid using `@cached` unless you have confirmed that the - getter you are decorating is computationally expensive. `@cached` adds a small - amount of overhead to the getter, making it more expensive. While this overhead - is small, if `@cached` is overused it can add up to a large impact overall in - your app. Many getters and tracked properties are only accessed once, rendered, - and then never rerendered, so adding `@cached` when it is unnecessary can - negatively impact performance. - - @method cached - @static - @for @glimmer/tracking - @public - */ diff --git a/packages/@glimmer/tracking/package.json b/packages/@glimmer/tracking/package.json index 988575e68ac..1733b5da387 100644 --- a/packages/@glimmer/tracking/package.json +++ b/packages/@glimmer/tracking/package.json @@ -1,12 +1,47 @@ { "name": "@glimmer/tracking", - "private": true, + "version": "2.0.0", + "description": "Glimmer tracking library", + "license": "MIT", "type": "module", + "keywords": [ + "ember-addon" + ], "exports": { - ".": "./index.ts", - "./primitives/cache": "./primitives/cache.ts" + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./primitives/cache": { + "types": "./dist/primitives/cache.d.ts", + "default": "./dist/primitives/cache.js" + } + }, + "files": [ + "src", + "dist" + ], + "typesVersions": { + "*": { + "*": [ + "dist/*" + ] + } }, "dependencies": { - "@ember/-internals": "workspace:*" + "@embroider/addon-shim": "^1.8.9", + "@glimmer/env": "^0.1.7" + }, + "devDependencies": { + "typescript": "5.1" + }, + "ember": { + "edition": "octane" + }, + "ember-addon": { + "version": 2, + "type": "addon", + "main": "addon-main.cjs" } -} \ No newline at end of file +} + diff --git a/packages/@ember/-internals/glimmer/lib/glimmer-tracking-docs.ts b/packages/@glimmer/tracking/src/index.ts similarity index 53% rename from packages/@ember/-internals/glimmer/lib/glimmer-tracking-docs.ts rename to packages/@glimmer/tracking/src/index.ts index fa7776f65fc..beb39057dbf 100644 --- a/packages/@ember/-internals/glimmer/lib/glimmer-tracking-docs.ts +++ b/packages/@glimmer/tracking/src/index.ts @@ -19,6 +19,31 @@ @public */ +import { meta as metaFor } from '@ember/-internals/meta'; +import { isEmberArray } from '@ember/array/-internals'; +import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; +import { + consumeTag, + dirtyTagFor, + tagFor, + trackedData, + createCache, + getValue, +} from '@glimmer/validator'; +import type { ElementDescriptor } from '@ember/-internals/metal'; +import { CHAIN_PASS_THROUGH } from '@ember/-internals/metal/lib/chain-tags'; +import type { + ExtendedMethodDecorator, + DecoratorPropertyDescriptor, +} from '@ember/-internals/metal/lib/decorator'; +import { + COMPUTED_SETTERS, + isElementDescriptor, + setClassicDecorator, +} from '@ember/-internals/metal/lib/decorator'; +import { SELF_TAG } from '@ember/-internals/metal/lib/tags'; + /** Marks a property as tracked. By default, values that are rendered in Ember app templates are _static_, meaning that updates to them won't cause the @@ -111,7 +136,6 @@ ```js let entry = new Entry('Pizza Palace', 5551234); - // if entry was used when rendering, this would cause a rerender, even though // the name is being set to the same value as it was before entry.name = entry.name; @@ -132,7 +156,6 @@ Often this is unnecessary, but to ensure robust auto-tracking behavior it is advisable to mark tracked state appropriately wherever possible. - This form of `tracked` also accepts an optional configuration object containing either an initial `value` or an `initializer` function (but not both). @@ -154,6 +177,135 @@ @for @glimmer/tracking @public */ +export function tracked(propertyDesc: { + value: any; + initializer: () => any; +}): ExtendedMethodDecorator; +export function tracked(target: object, key: string): void; +export function tracked( + target: object, + key: string, + desc: DecoratorPropertyDescriptor +): DecoratorPropertyDescriptor; +export function tracked(...args: any[]): ExtendedMethodDecorator | DecoratorPropertyDescriptor { + assert( + `@tracked can only be used directly as a native decorator. If you're using tracked in classic classes, add parenthesis to call it like a function: tracked()`, + !(isElementDescriptor(args.slice(0, 3)) && args.length === 5 && args[4] === true) + ); + + if (!isElementDescriptor(args)) { + let propertyDesc = args[0]; + + assert( + `tracked() may only receive an options object containing 'value' or 'initializer', received ${propertyDesc}`, + args.length === 0 || (typeof propertyDesc === 'object' && propertyDesc !== null) + ); + + if (DEBUG && propertyDesc) { + let keys = Object.keys(propertyDesc); + + assert( + `The options object passed to tracked() may only contain a 'value' or 'initializer' property, not both. Received: [${keys}]`, + keys.length <= 1 && + (keys[0] === undefined || keys[0] === 'value' || keys[0] === 'initializer') + ); + + assert( + `The initializer passed to tracked must be a function. Received ${propertyDesc.initializer}`, + !('initializer' in propertyDesc) || typeof propertyDesc.initializer === 'function' + ); + } + + let initializer = propertyDesc ? propertyDesc.initializer : undefined; + let value = propertyDesc ? propertyDesc.value : undefined; + + let decorator = function ( + target: object, + key: string, + _desc?: DecoratorPropertyDescriptor, + _meta?: any, + isClassicDecorator?: boolean + ): DecoratorPropertyDescriptor { + assert( + `You attempted to set a default value for ${key} with the @tracked({ value: 'default' }) syntax. You can only use this syntax with classic classes. For native classes, you can use class initializers: @tracked field = 'default';`, + isClassicDecorator + ); + + let fieldDesc = { + initializer: initializer || (() => value), + }; + + return descriptorForField([target, key, fieldDesc]); + }; + + setClassicDecorator(decorator); + + return decorator; + } + + return descriptorForField(args); +} + +if (DEBUG) { + // Normally this isn't a classic decorator, but we want to throw a helpful + // error in development so we need it to treat it like one + setClassicDecorator(tracked); +} + +function descriptorForField([target, key, desc]: ElementDescriptor): DecoratorPropertyDescriptor { + assert( + `You attempted to use @tracked on ${key}, but that element is not a class field. @tracked is only usable on class fields. Native getters and setters will autotrack add any tracked fields they encounter, so there is no need mark getters and setters with @tracked.`, + !desc || (!desc.value && !desc.get && !desc.set) + ); + + let { getter, setter } = trackedData(key, desc ? desc.initializer : undefined); + + function get(this: object): unknown { + let value = getter(this); + + // Add the tag of the returned value if it is an array, since arrays + // should always cause updates if they are consumed and then changed + if (Array.isArray(value) || isEmberArray(value)) { + consumeTag(tagFor(value, '[]')); + } + + return value; + } + + function set(this: object, newValue: unknown): void { + setter(this, newValue); + dirtyTagFor(this, SELF_TAG); + } + + let newDesc = { + enumerable: true, + configurable: true, + isTracked: true, + + get, + set, + }; + + COMPUTED_SETTERS.add(set); + + metaFor(target).writeDescriptors(key, new TrackedDescriptor(get, set)); + + return newDesc; +} + +export class TrackedDescriptor { + constructor(private _get: () => unknown, private _set: (value: unknown) => void) { + CHAIN_PASS_THROUGH.add(this); + } + + get(obj: object): unknown { + return this._get.call(obj); + } + + set(obj: object, _key: string, value: unknown): void { + this._set.call(obj, value); + } +} /** Gives the getter a caching behavior. The return value of the getter @@ -243,4 +395,54 @@ @public */ -export {}; +export const cached: MethodDecorator = (...args: any[]) => { + const [target, key, descriptor] = args; + + // Error on `@cached()`, `@cached(...args)`, and `@cached propName = value;` + if (DEBUG && target === undefined) throwCachedExtraneousParens(); + if ( + DEBUG && + (typeof target !== 'object' || + typeof key !== 'string' || + typeof descriptor !== 'object' || + args.length !== 3) + ) { + throwCachedInvalidArgsError(args); + } + if (DEBUG && (!('get' in descriptor) || typeof descriptor.get !== 'function')) { + throwCachedGetterOnlyError(key); + } + + const caches = new WeakMap(); + const getter = descriptor.get; + + descriptor.get = function (): unknown { + if (!caches.has(this)) { + caches.set(this, createCache(getter.bind(this))); + } + + return getValue(caches.get(this)); + }; +}; + +function throwCachedExtraneousParens(): never { + throw new Error( + 'You attempted to use @cached(), which is not necessary nor supported. Remove the parentheses and you will be good to go!' + ); +} + +function throwCachedGetterOnlyError(key: string): never { + throw new Error(`The @cached decorator must be applied to getters. '${key}' is not a getter.`); +} + +function throwCachedInvalidArgsError(args: unknown[] = []): never { + throw new Error( + `You attempted to use @cached on with ${ + args.length > 1 ? 'arguments' : 'an argument' + } ( @cached(${args + .map((d) => `'${d}'`) + .join( + ', ' + )}), which is not supported. Dependencies are automatically tracked, so you can just use ${'`@cached`'}` + ); +} diff --git a/packages/@glimmer/tracking/primitives/cache.ts b/packages/@glimmer/tracking/src/primitives/cache.ts similarity index 100% rename from packages/@glimmer/tracking/primitives/cache.ts rename to packages/@glimmer/tracking/src/primitives/cache.ts diff --git a/packages/@glimmer/tracking/tsconfig.json b/packages/@glimmer/tracking/tsconfig.json new file mode 100644 index 00000000000..665c5855755 --- /dev/null +++ b/packages/@glimmer/tracking/tsconfig.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "moduleResolution": "node", + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + "paths": { + "@ember/array/*": ["../../../types/stable/@ember/array/*"], + "@ember/debug": ["../../../types/stable/@ember/debug/index.d.ts"], + "@ember/-internals/*": ["../../../types/stable/@ember/-internals/*"] + }, + "lib": ["es2020", "dom"], + "declarationDir": "./dist", + "skipLibCheck": true + }, + "include": ["./src/**/*.ts"] +} diff --git a/packages/ember/barrel.ts b/packages/ember/barrel.ts index 45d6fcf4c4d..0c440e87f42 100644 --- a/packages/ember/barrel.ts +++ b/packages/ember/barrel.ts @@ -2,6 +2,7 @@ @module ember */ +import { tracked } from '@glimmer/tracking'; import { getENV, getLookup, setLookup } from '@ember/-internals/environment'; import * as utils from '@ember/-internals/utils'; import { @@ -177,7 +178,7 @@ namespace Ember { export const _descriptor = metal.nativeDescDecorator; export const _getPath = metal._getPath; export const _setClassicDecorator = metal.setClassicDecorator; - export const _tracked = metal.tracked; // Also exported from @glimmer/tracking + export const _tracked = tracked; export const beginPropertyChanges = metal.beginPropertyChanges; export const changeProperties = metal.changeProperties; export const endPropertyChanges = metal.endPropertyChanges; diff --git a/packages/ember/tests/routing/query_params_test.js b/packages/ember/tests/routing/query_params_test.js index 9246c311462..7d289180b61 100644 --- a/packages/ember/tests/routing/query_params_test.js +++ b/packages/ember/tests/routing/query_params_test.js @@ -5,7 +5,7 @@ import { RSVP } from '@ember/-internals/runtime'; import { A as emberA } from '@ember/array'; import { run } from '@ember/runloop'; import { peekMeta } from '@ember/-internals/meta'; -import { tracked } from '@ember/-internals/metal'; +import { tracked } from '@glimmer/tracking'; import Route from '@ember/routing/route'; import { PARAMS_SYMBOL } from 'router_js'; import { service } from '@ember/service'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a05ec48595..fb55906e872 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -157,6 +157,9 @@ importers: '@glimmer/component': specifier: workspace:^ version: link:packages/@glimmer/component + '@glimmer/tracking': + specifier: workspace:^ + version: link:packages/@glimmer/tracking '@rollup/plugin-babel': specifier: ^6.0.4 version: 6.0.4(@babel/core@7.24.4)(rollup@4.16.4) @@ -412,6 +415,9 @@ importers: '@glimmer/syntax': specifier: 0.92.3 version: 0.92.3 + '@glimmer/tracking': + specifier: workspace:^ + version: link:../../@glimmer/tracking '@glimmer/util': specifier: 0.92.3 version: 0.92.3 @@ -904,6 +910,9 @@ importers: '@glimmer/owner': specifier: 0.92.3 version: 0.92.3 + '@glimmer/tracking': + specifier: workspace:* + version: link:../../@glimmer/tracking '@glimmer/util': specifier: 0.92.3 version: 0.92.3 @@ -1307,9 +1316,16 @@ importers: packages/@glimmer/tracking: dependencies: - '@ember/-internals': - specifier: workspace:* - version: link:../../@ember/-internals + '@embroider/addon-shim': + specifier: ^1.8.9 + version: 1.9.0 + '@glimmer/env': + specifier: ^0.1.7 + version: 0.1.7 + devDependencies: + typescript: + specifier: '5.1' + version: 5.1.6 packages/ember: dependencies: @@ -7465,6 +7481,9 @@ packages: /ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependenciesMeta: + ajv: + optional: true dependencies: ajv: 8.12.0 @@ -13217,7 +13236,7 @@ packages: function-bind: 1.1.2 /hawk@1.1.1: - resolution: {integrity: sha1-h81JH5tG5OKurKM1QWdmiF0tHtk=} + resolution: {integrity: sha512-am8sVA2bCJIw8fuuVcKvmmNnGFUGW8spTkVtj2fXTEZVkfN42bwFZFtDem57eFi+NSxurJB8EQ7Jd3uCHLn8Vw==} engines: {node: '>=0.8.0'} deprecated: This module moved to @hapi/hawk. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues. requiresBuild: true diff --git a/rollup.config.mjs b/rollup.config.mjs index e62651f977e..a0e47ea655e 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -18,6 +18,7 @@ const testDependencies = ['qunit', 'vite']; let configs = [ esmConfig(), + glimmerTracking(), legacyBundleConfig('./broccoli/amd-compat-entrypoints/ember.debug.js', 'ember.debug.js', { isDeveloping: true, }), @@ -80,6 +81,32 @@ function esmConfig() { }; } +function glimmerTracking() { + return { + onLog: handleRollupWarnings, + input: { + index: './packages/@glimmer/tracking/src/index.ts', + 'primitives/cache': './packages/@glimmer/tracking/src/primitives/cache.ts', + }, + output: { + format: 'es', + dir: 'packages/@glimmer/tracking/dist', + hoistTransitiveImports: false, + generatedCode: 'es2015', + }, + plugins: [ + babel({ + babelHelpers: 'bundled', + extensions: ['.js', '.ts'], + configFile: false, + ...sharedBabelConfig, + }), + resolveTS(), + externalizePackages({ ...exposedDependencies(), ...hiddenDependencies() }), + ], + }; +} + function glimmerComponent() { return { onLog: handleRollupWarnings, @@ -172,6 +199,7 @@ function packages() { // this is a real package that publishes by itself '@glimmer/component/**', + '@glimmer/tracking/**', // exclude these so we can add only their entrypoints below ...rolledUpPackages().map((name) => `${name}/**`), @@ -358,6 +386,11 @@ export function resolvePackages(deps, isExternal) { } } + // Where does this go? + if (source.startsWith('@glimmer/tracking')) { + return { external: true, id: source }; + } + if (testDependencies.includes(pkgName)) { // these are allowed to fall through and get resolved noramlly by vite // within our test suite. diff --git a/types/publish.mjs b/types/publish.mjs index cf6c3ac925f..20509a77e4d 100755 --- a/types/publish.mjs +++ b/types/publish.mjs @@ -156,6 +156,16 @@ async function main() { process.exit(1); } }); + // @glimmer/tracking publishes as a separate package. We need to build its + // types after building the ember-source types. + doOrDie(() => { + let result = spawnSync('pnpm', ['tsc'], { cwd: 'packages/@glimmer/tracking' }); + if (result.status !== 0) { + console.log(`@glimmer/tracking types build failed:`); + console.error(result.output.toString()); + process.exit(1); + } + }); process.exit(status === 'success' ? 0 : 1); }