From 2970aaee28a42a37e4c2362fa137847dc8ee47df Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 24 Dec 2024 23:31:44 -0500 Subject: [PATCH 01/12] in-progress is thish better? maybe tests will tell me ope ope The builder had to be registered to the helper manager Be more robust with owner detection Maybe progress it works, now for memory leak testing --- .../src/intermediate-representation.ts | 189 ++++++++++++++++++ ember-resources/src/owner.ts | 22 ++ ember-resources/src/resource-manager.ts | 122 ++--------- ember-resources/src/resource.ts | 54 +---- ember-resources/src/use.ts | 29 +-- ember-resources/src/utils.ts | 51 +++-- test-app/tests/core/rendering-test.gts | 22 +- 7 files changed, 290 insertions(+), 199 deletions(-) create mode 100644 ember-resources/src/intermediate-representation.ts create mode 100644 ember-resources/src/owner.ts diff --git a/ember-resources/src/intermediate-representation.ts b/ember-resources/src/intermediate-representation.ts new file mode 100644 index 000000000..630348341 --- /dev/null +++ b/ember-resources/src/intermediate-representation.ts @@ -0,0 +1,189 @@ +// @ts-ignore +import { createCache, getValue } from '@glimmer/tracking/primitives/cache'; +import { assert } from '@ember/debug'; +import { associateDestroyableChild, destroy, registerDestructor } from '@ember/destroyable'; +// @ts-ignore +import { invokeHelper, setHelperManager } from '@ember/helper'; + +import { ReadonlyCell } from './cell.ts'; +import { ResourceManagerFactory } from './resource-manager.ts'; +import { INTERNAL } from './types.ts'; +import { registerUsable, TYPE_KEY } from './use.ts'; +import { shallowFlat } from './utils.ts'; + +import type { Destructor, Reactive, ResourceFunction } from './types.ts'; +import type Owner from '@ember/owner'; + +export const CREATE_KEY = Symbol.for('__configured-resource-key__'); +export const DEBUG_NAME = Symbol.for('DEBUG_NAME'); +export const RESOURCE_CACHE = Symbol.for('__resource_cache__'); + +import { compatOwner } from './owner.ts'; + +const getOwner = compatOwner.getOwner; +const setOwner = compatOwner.setOwner; + +/** + * The return value from resource() + * + * This is semi-public API, and is meant to de-magic the intermediary + * value returned from resource(), allowing us to both document how to + * - manually create a resource (instance) + * - explain how the helper manager interacts with the methods folks + * can use to manually create a resource + * + * + * With an owner, you can manually create a resource this way: + * ```js + * import { destroy } from '@ember/destroyable'; + * import { resource } from 'ember-resources'; + * + * const builder = resource(() => {}); // builder can be invoked multiple times + * const owner = {}; + * const state = builder.create(owner); // state can be created any number of times + * + * state.current // the current value + * destroy(state); // some time later, calls cleanup + * ``` + */ +export class Builder { + #fn: ResourceFunction; + + [TYPE_KEY] = TYPE; + + constructor(fn: ResourceFunction, key: Symbol) { + assert( + `Cannot instantiate ConfiguredResource without using the resource() function.`, + key === CREATE_KEY, + ); + + this.#fn = fn; + } + + create() { + return new Resource(this.#fn); + } +} + +const TYPE = 'function-based'; + +registerUsable(TYPE, (context: object, config: Builder) => { + let instance = config.create(); + + instance.link(context); + + return instance[RESOURCE_CACHE]; +}); + +/** + * TODO: + */ +export class Resource { + #originalFn: ResourceFunction; + #owner: Owner | undefined; + #previousFn: object | undefined; + #usableCache = new WeakMap>(); + #cache: ReturnType; + + constructor(fn: ResourceFunction) { + /** + * We have to copy the `fn` in case there are multiple + * usages or invocations of the function. + * + * This copy is what we'll ultimately work with and eventually + * destroy. + */ + this.#originalFn = fn.bind(null); + + this.#cache = createCache(() => { + if (this.#previousFn) { + destroy(this.#previousFn); + } + + let currentFn = this.#originalFn.bind(null); + + associateDestroyableChild(this.#originalFn, currentFn); + this.#previousFn = currentFn; + + assert( + `Cannot create a resource without an owner. Must have previously called .link()`, + this.#owner, + ); + + let maybeValue = currentFn({ + on: { + cleanup: (destroyer: Destructor) => { + registerDestructor(currentFn, destroyer); + }, + }, + use: (usable) => { + assert( + `Expected the resource's \`use(...)\` utility to have been passed an object, but a \`${typeof usable}\` was passed.`, + typeof usable === 'object', + ); + assert( + `Expected the resource's \`use(...)\` utility to have been passed a truthy value, instead was passed: ${usable}.`, + usable, + ); + assert( + `Expected the resource's \`use(...)\` utility to have been passed another resource, but something else was passed.`, + INTERNAL in usable || usable instanceof Builder, + ); + + let previousCache = this.#usableCache.get(usable); + + if (previousCache) { + destroy(previousCache); + } + + let nestedCache = invokeHelper(this.#cache, usable); + + associateDestroyableChild(currentFn, nestedCache as object); + + this.#usableCache.set(usable, nestedCache); + + return new ReadonlyCell(() => { + let cache = this.#usableCache.get(usable); + + assert(`Cache went missing while evaluating the result of a resource.`, cache); + + return getValue(cache); + }); + }, + owner: this.#owner, + }); + + return maybeValue; + }); + } + link(context: object) { + let owner = getOwner(context); + + assert(`Cannot link without an owner`, owner); + + this.#owner = owner; + + associateDestroyableChild(context, this.#cache); + associateDestroyableChild(context, this.#originalFn); + + setOwner(this.#cache, this.#owner); + } + + get [RESOURCE_CACHE](): unknown { + return this.#cache; + } + + get fn() { + return this.#originalFn; + } + + get current() { + return shallowFlat(this.#cache); + } + + [DEBUG_NAME]() { + return `Resource Function`; + } +} + +setHelperManager(ResourceManagerFactory, Builder.prototype); diff --git a/ember-resources/src/owner.ts b/ember-resources/src/owner.ts new file mode 100644 index 000000000..9fa537c6e --- /dev/null +++ b/ember-resources/src/owner.ts @@ -0,0 +1,22 @@ +import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; + +import type Owner from '@ember/owner'; + +interface CompatOwner { + getOwner: (context: unknown) => Owner | undefined; + setOwner: (context: unknown, owner: Owner) => void; +} + +export const compatOwner = {} as CompatOwner; + +if (macroCondition(dependencySatisfies('ember-source', '>=4.12.0'))) { + // In no version of ember where `@ember/owner` tried to be imported did it exist + // if (macroCondition(false)) { + // Using 'any' here because importSync can't lookup types correctly + compatOwner.getOwner = (importSync('@ember/owner') as any).getOwner; + compatOwner.setOwner = (importSync('@ember/owner') as any).setOwner; +} else { + // Using 'any' here because importSync can't lookup types correctly + compatOwner.getOwner = (importSync('@ember/application') as any).getOwner; + compatOwner.setOwner = (importSync('@ember/application') as any).setOwner; +} diff --git a/ember-resources/src/resource-manager.ts b/ember-resources/src/resource-manager.ts index 382f9a0b5..3e9ec057f 100644 --- a/ember-resources/src/resource-manager.ts +++ b/ember-resources/src/resource-manager.ts @@ -1,22 +1,10 @@ -// @ts-ignore -import { createCache, getValue } from '@glimmer/tracking/primitives/cache'; import { assert } from '@ember/debug'; -import { associateDestroyableChild, destroy, registerDestructor } from '@ember/destroyable'; -// @ts-ignore -import { invokeHelper } from '@ember/helper'; // @ts-ignore import { capabilities as helperCapabilities } from '@ember/helper'; -import { ReadonlyCell } from './cell.ts'; -import { compatOwner } from './ember-compat.ts'; -import { CURRENT, INTERNAL } from './types.ts'; +import { compatOwner } from './owner.ts'; -import type { - Destructor, - InternalFunctionResourceConfig, - Reactive, - ResourceFunction, -} from './types.ts'; +import type { Builder, Resource } from './intermediate-representation.ts'; import type Owner from '@ember/owner'; const setOwner = compatOwner.setOwner; @@ -25,119 +13,37 @@ const setOwner = compatOwner.setOwner; * Note, a function-resource receives on object, hooks. * We have to build that manually in this helper manager */ -class FunctionResourceManager { +class FunctionResourceManager { capabilities: ReturnType = helperCapabilities('3.23', { hasValue: true, hasDestroyable: true, }); - constructor(protected owner: Owner) {} + constructor(protected owner: Owner) { + setOwner(this, owner); + } /** * Resources do not take args. * However, they can access tracked data */ - createHelper(config: InternalFunctionResourceConfig): { - fn: InternalFunctionResourceConfig['definition']; - cache: ReturnType; - } { - let { definition: fn } = config; - /** - * We have to copy the `fn` in case there are multiple - * usages or invocations of the function. - * - * This copy is what we'll ultimately work with and eventually - * destroy. - */ - let thisFn = fn.bind(null); - let previousFn: object; - let usableCache = new WeakMap>(); - let owner = this.owner; - - let cache = createCache(() => { - if (previousFn) { - destroy(previousFn); - } - - let currentFn = thisFn.bind(null); - - associateDestroyableChild(thisFn, currentFn); - previousFn = currentFn; - - let maybeValue = currentFn({ - on: { - cleanup: (destroyer: Destructor) => { - registerDestructor(currentFn, destroyer); - }, - }, - use: (usable) => { - assert( - `Expected the resource's \`use(...)\` utility to have been passed an object, but a \`${typeof usable}\` was passed.`, - typeof usable === 'object', - ); - assert( - `Expected the resource's \`use(...)\` utility to have been passed a truthy value, instead was passed: ${usable}.`, - usable, - ); - assert( - `Expected the resource's \`use(...)\` utility to have been passed another resource, but something else was passed.`, - INTERNAL in usable, - ); - - let previousCache = usableCache.get(usable); - - if (previousCache) { - destroy(previousCache); - } - - let nestedCache = invokeHelper(cache, usable); - - associateDestroyableChild(currentFn, nestedCache as object); + createHelper(builder: Builder): Resource { + let instance = builder.create(); - usableCache.set(usable, nestedCache); + instance.link(this); - return new ReadonlyCell(() => { - let cache = usableCache.get(usable); - - assert(`Cache went missing while evaluating the result of a resource.`, cache); - - return getValue(cache); - }); - }, - owner: this.owner, - }); - - return maybeValue; - }); - - setOwner(cache, owner); - - return { fn: thisFn, cache }; + return instance; } - getValue({ cache }: { fn: ResourceFunction; cache: ReturnType }) { - let maybeValue = getValue(cache); - - if (typeof maybeValue === 'function') { - return maybeValue(); - } - - if (isReactive(maybeValue)) { - return maybeValue[CURRENT]; - } - - return maybeValue; + getValue(state: Resource) { + return state.current; } - getDestroyable({ fn }: { fn: ResourceFunction }) { - return fn; + getDestroyable(state: Resource) { + return state.fn; } } -function isReactive(maybe: unknown): maybe is Reactive { - return typeof maybe === 'object' && maybe !== null && CURRENT in maybe; -} - export const ResourceManagerFactory = (owner: Owner | undefined) => { assert(`Cannot create resource without an owner`, owner); diff --git a/ember-resources/src/resource.ts b/ember-resources/src/resource.ts index b26310ebd..22863c59c 100644 --- a/ember-resources/src/resource.ts +++ b/ember-resources/src/resource.ts @@ -1,19 +1,9 @@ import { assert } from '@ember/debug'; -// @ts-ignore -import { invokeHelper, setHelperManager } from '@ember/helper'; -import { ResourceManagerFactory } from './resource-manager.ts'; -import { INTERNAL } from './types.ts'; -import { registerUsable } from './use.ts'; +import { Builder, CREATE_KEY } from './intermediate-representation.ts'; import { wrapForPlainUsage } from './utils.ts'; -import type { InternalFunctionResourceConfig, ResourceFn, ResourceFunction } from './types.ts'; - -const TYPE = 'function-based'; - -registerUsable(TYPE, (context: object, config: InternalFunctionResourceConfig) => { - return invokeHelper(context, config); -}); +import type { ResourceFn, ResourceFunction } from './types.ts'; /** * `resource` provides a single reactive read-only value with lifetime and may have cleanup. @@ -174,7 +164,7 @@ export function resource(context: object, setup: ResourceFunction) export function resource( context: object | ResourceFunction, setup?: ResourceFunction, -): Value | InternalFunctionResourceConfig | ResourceFn { +): Value | Builder | ResourceFn { if (!setup) { assert( `When using \`resource\` with @use, ` + @@ -183,20 +173,6 @@ export function resource( typeof context === 'function', ); - let internalConfig: InternalFunctionResourceConfig = { - definition: context as ResourceFunction, - type: 'function-based', - name: 'Resource', - [INTERNAL]: true, - }; - - /** - * Functions have a different identity every time they are defined. - * The primary purpose of the `resource` wrapper is to individually - * register each function with our helper manager. - */ - setHelperManager(ResourceManagerFactory, internalConfig); - /** * With only one argument, we have to do a bunch of lying to * TS, because we need a special object to pass to `@use` @@ -205,7 +181,10 @@ export function resource( * using vanilla functions as resources without the resource wrapper * */ - return internalConfig as unknown as ResourceFn; + return new Builder( + context as ResourceFunction, + CREATE_KEY, + ) as unknown as ResourceFn; } assert( @@ -220,22 +199,7 @@ export function resource( typeof setup === 'function', ); - let internalConfig: InternalFunctionResourceConfig = { - definition: setup as ResourceFunction, - type: TYPE, - name: getDebugName(setup), - [INTERNAL]: true, - }; - - setHelperManager(ResourceManagerFactory, internalConfig); - - return wrapForPlainUsage(context, internalConfig); -} - -function getDebugName(obj: object) { - if ('name' in obj) { - return `Resource Function: ${obj.name}`; - } + let configured = new Builder(setup, CREATE_KEY); - return `Resource Function`; + return wrapForPlainUsage(context, configured); } diff --git a/ember-resources/src/use.ts b/ember-resources/src/use.ts index a8c11a1a9..fe1fb2ed3 100644 --- a/ember-resources/src/use.ts +++ b/ember-resources/src/use.ts @@ -9,6 +9,7 @@ import { associateDestroyableChild } from '@ember/destroyable'; import { invokeHelper } from '@ember/helper'; import { ReadonlyCell } from './cell.ts'; +import { getCurrentValue, shallowFlat } from './utils.ts'; import type { INTERNAL, @@ -107,17 +108,6 @@ export function use( assert(`Unknown arity for \`use\`. Received ${args.length} arguments`, false); } -function getCurrentValue(value: Value | Reactive): Value { - /** - * If we are working with a cell, forward the '.current' call to it. - */ - if (typeof value === 'object' && value !== null && 'current' in value) { - return value.current; - } - - return value; -} - function classContextLink( context: object, definition: Value | (() => Value), @@ -160,18 +150,15 @@ function argumentToDecorator(definition: Value | (() => Value)): Property }; } -interface UsableConfig { - type: string; - definition: unknown; -} - -export type UsableFn = ( +export type UsableFn = ( context: object, config: Usable, // This return type *would be* ReturnType // But the DT types for @ember/helper don't have *any* of the helper-manager things. ) => unknown; +export const TYPE_KEY = Symbol.for(`__RESOURCE_TYPE__`); + const USABLES = new Map>(); /** @@ -181,7 +168,7 @@ const USABLES = new Map>(); * * The return type must be a "Cache" returned from `invokeHelper` so that `@use`'s usage of `getValue` gets the value (as determined by the helper manager you wrote for your usable). */ -export function registerUsable( +export function registerUsable( /** * The key to register the usable under. * @@ -215,7 +202,7 @@ function descriptorGetter(initializer: unknown | (() => unknown)) { typeof initializer === 'function' ? initializer.call(this) : initializer ) as Config; - let usable = USABLES.get(config.type); + let usable = USABLES.get(config.type) || USABLES.get(config[TYPE_KEY]); assert( `Expected the initialized value with @use to have been a registerd "usable". Available usables are: ${[ @@ -232,9 +219,7 @@ function descriptorGetter(initializer: unknown | (() => unknown)) { associateDestroyableChild(this, cache); } - let value = getValue(cache); - - return getCurrentValue(value); + return shallowFlat(cache); }, }; } diff --git a/ember-resources/src/utils.ts b/ember-resources/src/utils.ts index 1509f6525..e11130f6f 100644 --- a/ember-resources/src/utils.ts +++ b/ember-resources/src/utils.ts @@ -1,25 +1,50 @@ // @ts-ignore -import { getValue } from '@glimmer/tracking/primitives/cache'; +import { type createCache, getValue } from '@glimmer/tracking/primitives/cache'; import { assert } from '@ember/debug'; -// @ts-ignore -import { invokeHelper } from '@ember/helper'; -import { INTERMEDIATE_VALUE } from './types.ts'; +import { CURRENT, INTERMEDIATE_VALUE, type Reactive } from './types.ts'; + +import type { Builder, Resource } from './intermediate-representation.ts'; +import type Owner from '@ember/owner'; + +export function shallowFlat(cache: ReturnType): Value { + let maybeValue = getValue(cache); + + if (typeof maybeValue === 'function') { + return maybeValue(); + } + + if (isReactive(maybeValue)) { + return maybeValue[CURRENT] as Value; + } + + return maybeValue as Value; +} -import type { InternalFunctionResourceConfig } from './types.ts'; +export function isReactive(maybe: unknown): maybe is Reactive { + return typeof maybe === 'object' && maybe !== null && CURRENT in maybe; +} + +export function getCurrentValue(value: Value | Reactive): Value { + /** + * If we are working with a cell, forward the '.current' call to it. + */ + if (typeof value === 'object' && value !== null && 'current' in value) { + return value.current; + } + + return value; +} /** - * This is what allows resource to be used withotu @use. + * This is what allows resource to be used without @use. * The caveat though is that a property must be accessed * on the return object. * * A resource not using use *must* be an object. */ -export function wrapForPlainUsage( - context: object, - setup: InternalFunctionResourceConfig, -) { - let cache: ReturnType; +export function wrapForPlainUsage(context: object, builder: Builder) { + let cache: Resource; /* * Having an object that we use invokeHelper + getValue on @@ -30,12 +55,12 @@ export function wrapForPlainUsage( const target = { get [INTERMEDIATE_VALUE]() { if (!cache) { - cache = invokeHelper(context, setup); + cache = builder.create(context as Owner); } // SAFETY: the types for the helper manager APIs aren't fully defined to infer // nor allow passing the value. - return getValue(cache as any); + return cache.current; }, }; diff --git a/test-app/tests/core/rendering-test.gts b/test-app/tests/core/rendering-test.gts index ce3906aa8..ba1995390 100644 --- a/test-app/tests/core/rendering-test.gts +++ b/test-app/tests/core/rendering-test.gts @@ -9,29 +9,29 @@ import { cell, resource, resourceFactory } from 'ember-resources'; module('Core | Resource | rendering', function (hooks) { setupRenderingTest(hooks); - module('cleanup with wrapping factory/blueprint', function() { + module('cleanup with wrapping factory/blueprint', function () { test('a generated interval can be cleared', async function (assert) { const id = cell(0); const condition = cell(true); - const poll = resourceFactory((id: number) => { + const poll = (id: number) => { return resource(({ on }) => { assert.step(`setup: ${id}`); on.cleanup(() => assert.step(`cleanup: ${id}`)); return id; }); - }); + }; - await render( - - ); + await render(); assert.verifySteps(['setup: 0']); From 2400805277fc05a23e2e0455015d5e4b61e817f3 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 3 Jan 2025 12:16:25 -0500 Subject: [PATCH 02/12] Updates --- ember-resources/src/intermediate-representation.ts | 2 +- ember-resources/src/resource-manager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ember-resources/src/intermediate-representation.ts b/ember-resources/src/intermediate-representation.ts index 630348341..eced7d0e8 100644 --- a/ember-resources/src/intermediate-representation.ts +++ b/ember-resources/src/intermediate-representation.ts @@ -18,7 +18,7 @@ export const CREATE_KEY = Symbol.for('__configured-resource-key__'); export const DEBUG_NAME = Symbol.for('DEBUG_NAME'); export const RESOURCE_CACHE = Symbol.for('__resource_cache__'); -import { compatOwner } from './owner.ts'; +import { compatOwner } from './ember-compat.ts'; const getOwner = compatOwner.getOwner; const setOwner = compatOwner.setOwner; diff --git a/ember-resources/src/resource-manager.ts b/ember-resources/src/resource-manager.ts index 3e9ec057f..70ea0c819 100644 --- a/ember-resources/src/resource-manager.ts +++ b/ember-resources/src/resource-manager.ts @@ -2,7 +2,7 @@ import { assert } from '@ember/debug'; // @ts-ignore import { capabilities as helperCapabilities } from '@ember/helper'; -import { compatOwner } from './owner.ts'; +import { compatOwner } from './ember-compat.ts'; import type { Builder, Resource } from './intermediate-representation.ts'; import type Owner from '@ember/owner'; From 57f64d4027b47d11539078a208efd42e8b34b443 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 3 Jan 2025 14:33:09 -0500 Subject: [PATCH 03/12] Remove extraneous file --- ember-resources/src/owner.ts | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 ember-resources/src/owner.ts diff --git a/ember-resources/src/owner.ts b/ember-resources/src/owner.ts deleted file mode 100644 index 9fa537c6e..000000000 --- a/ember-resources/src/owner.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; - -import type Owner from '@ember/owner'; - -interface CompatOwner { - getOwner: (context: unknown) => Owner | undefined; - setOwner: (context: unknown, owner: Owner) => void; -} - -export const compatOwner = {} as CompatOwner; - -if (macroCondition(dependencySatisfies('ember-source', '>=4.12.0'))) { - // In no version of ember where `@ember/owner` tried to be imported did it exist - // if (macroCondition(false)) { - // Using 'any' here because importSync can't lookup types correctly - compatOwner.getOwner = (importSync('@ember/owner') as any).getOwner; - compatOwner.setOwner = (importSync('@ember/owner') as any).setOwner; -} else { - // Using 'any' here because importSync can't lookup types correctly - compatOwner.getOwner = (importSync('@ember/application') as any).getOwner; - compatOwner.setOwner = (importSync('@ember/application') as any).setOwner; -} From e560d228358c9486276e244df8bc527cb1f733b2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 24 Jan 2025 09:11:18 -0500 Subject: [PATCH 04/12] Start trying to work with the new API --- ember-resources/src/resource.ts | 2 ++ ember-resources/src/use.ts | 2 +- ember-resources/src/utils.ts | 3 +- test-app/tests/core/raw-api-test.gts | 46 ++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 test-app/tests/core/raw-api-test.gts diff --git a/ember-resources/src/resource.ts b/ember-resources/src/resource.ts index 22863c59c..113e15f35 100644 --- a/ember-resources/src/resource.ts +++ b/ember-resources/src/resource.ts @@ -201,5 +201,7 @@ export function resource( let configured = new Builder(setup, CREATE_KEY); + return configured; + return wrapForPlainUsage(context, configured); } diff --git a/ember-resources/src/use.ts b/ember-resources/src/use.ts index fe1fb2ed3..f0f341aad 100644 --- a/ember-resources/src/use.ts +++ b/ember-resources/src/use.ts @@ -202,7 +202,7 @@ function descriptorGetter(initializer: unknown | (() => unknown)) { typeof initializer === 'function' ? initializer.call(this) : initializer ) as Config; - let usable = USABLES.get(config.type) || USABLES.get(config[TYPE_KEY]); + let usable = USABLES.get(config.type) || USABLES.get((config as any)[TYPE_KEY]); assert( `Expected the initialized value with @use to have been a registerd "usable". Available usables are: ${[ diff --git a/ember-resources/src/utils.ts b/ember-resources/src/utils.ts index e11130f6f..1b1c5ee25 100644 --- a/ember-resources/src/utils.ts +++ b/ember-resources/src/utils.ts @@ -55,7 +55,8 @@ export function wrapForPlainUsage(context: object, builder: Builder 2); + let parent = {}; + + setOwner(parent, this.owner); + + let instance = thing.create(); + + instance.link(parent); + assert.strictEqual(instance.current, 2); + }); + + test('owner required', async function (assert) { + let thing = resource(() => 2); + + assert.throws(() => { + let instance = thing.create(); + + instance.current; + }, /Cannot create a resource without an owner/); + }); + + test('owner missing', async function (assert) { + let thing = resource(() => 2); + + assert.throws(() => { + let instance = thing.create(); + + instance.link({}); + + instance.current; + }, /Cannot link without an owner/); + }); +}); From be0597dee0c5c273557c565bb2fc042fc3f9bae2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 25 Jan 2025 11:52:47 -0500 Subject: [PATCH 05/12] Oh, can I actually delete these hacks? --- ember-resources/src/resource.ts | 2 - ember-resources/src/utils.ts | 63 ---------------------------- test-app/tests/core/raw-api-test.gts | 2 +- 3 files changed, 1 insertion(+), 66 deletions(-) diff --git a/ember-resources/src/resource.ts b/ember-resources/src/resource.ts index 113e15f35..b6dce18d0 100644 --- a/ember-resources/src/resource.ts +++ b/ember-resources/src/resource.ts @@ -202,6 +202,4 @@ export function resource( let configured = new Builder(setup, CREATE_KEY); return configured; - - return wrapForPlainUsage(context, configured); } diff --git a/ember-resources/src/utils.ts b/ember-resources/src/utils.ts index 1b1c5ee25..f452533db 100644 --- a/ember-resources/src/utils.ts +++ b/ember-resources/src/utils.ts @@ -35,66 +35,3 @@ export function getCurrentValue(value: Value | Reactive): Value { return value; } - -/** - * This is what allows resource to be used without @use. - * The caveat though is that a property must be accessed - * on the return object. - * - * A resource not using use *must* be an object. - */ -export function wrapForPlainUsage(context: object, builder: Builder) { - let cache: Resource; - - /* - * Having an object that we use invokeHelper + getValue on - * is how we convert the "function" in to a reactive utility - * (along with the following proxy for accessing anything on this 'value') - * - */ - const target = { - get [INTERMEDIATE_VALUE]() { - if (!cache) { - cache = builder.create(); - cache.link(context); - } - - // SAFETY: the types for the helper manager APIs aren't fully defined to infer - // nor allow passing the value. - return cache.current; - }, - }; - - /** - * This proxy takes everything called on or accessed on "target" - * and forwards it along to target[INTERMEDIATE_VALUE] (where the actual resource instance is) - * - * It's important to only access .[INTERMEDIATE_VALUE] within these proxy-handler methods so that - * consumers "reactively entangle with" the Resource. - */ - return new Proxy(target, { - get(target, key): unknown { - const state = target[INTERMEDIATE_VALUE]; - - assert('[BUG]: it should not have been possible for this to be undefined', state); - - return Reflect.get(state, key, state); - }, - - ownKeys(target): (string | symbol)[] { - const value = target[INTERMEDIATE_VALUE]; - - assert('[BUG]: it should not have been possible for this to be undefined', value); - - return Reflect.ownKeys(value); - }, - - getOwnPropertyDescriptor(target, key): PropertyDescriptor | undefined { - const value = target[INTERMEDIATE_VALUE]; - - assert('[BUG]: it should not have been possible for this to be undefined', value); - - return Reflect.getOwnPropertyDescriptor(value, key); - }, - }) as never as Value; -} diff --git a/test-app/tests/core/raw-api-test.gts b/test-app/tests/core/raw-api-test.gts index 881adc1b6..d3b468301 100644 --- a/test-app/tests/core/raw-api-test.gts +++ b/test-app/tests/core/raw-api-test.gts @@ -5,7 +5,7 @@ import { click, render } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupRenderingTest, setupTest } from 'ember-qunit'; -import { cell, resource, resourceFactory } from 'ember-resources'; +import { resource } from 'ember-resources'; module('RAW', function (hooks) { setupTest(hooks); From f2962754786cffa8dbfe00885ef8233ca3099740 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 25 Jan 2025 11:53:18 -0500 Subject: [PATCH 06/12] reou --- ember-resources/src/resource.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ember-resources/src/resource.ts b/ember-resources/src/resource.ts index b6dce18d0..1675954ff 100644 --- a/ember-resources/src/resource.ts +++ b/ember-resources/src/resource.ts @@ -1,7 +1,6 @@ import { assert } from '@ember/debug'; import { Builder, CREATE_KEY } from './intermediate-representation.ts'; -import { wrapForPlainUsage } from './utils.ts'; import type { ResourceFn, ResourceFunction } from './types.ts'; From b1b9545a063abf432f13e03f75c22d18443f6059 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 25 Jan 2025 14:35:55 -0500 Subject: [PATCH 07/12] ope --- test-app/tests/core/raw-api-test.gts | 13 +++++++------ test-app/tests/helpers.ts | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 test-app/tests/helpers.ts diff --git a/test-app/tests/core/raw-api-test.gts b/test-app/tests/core/raw-api-test.gts index d3b468301..939bb8bcc 100644 --- a/test-app/tests/core/raw-api-test.gts +++ b/test-app/tests/core/raw-api-test.gts @@ -1,12 +1,10 @@ -// @ts-ignore @ember/modifier does not provide types :( -import { on } from '@ember/modifier'; -import { setOwner } from '@ember/owner'; -import { click, render } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { setupRenderingTest, setupTest } from 'ember-qunit'; +import { setupTest } from 'ember-qunit'; import { resource } from 'ember-resources'; +import { compatOwner } from '../helpers'; + module('RAW', function (hooks) { setupTest(hooks); @@ -14,8 +12,9 @@ module('RAW', function (hooks) { let thing = resource(() => 2); let parent = {}; - setOwner(parent, this.owner); + compatOwner.setOwner(parent, this.owner); + // @ts-expect-error - not sure what to bo about the type discrepency atm let instance = thing.create(); instance.link(parent); @@ -26,6 +25,7 @@ module('RAW', function (hooks) { let thing = resource(() => 2); assert.throws(() => { + // @ts-expect-error - not sure what to bo about the type discrepency atm let instance = thing.create(); instance.current; @@ -36,6 +36,7 @@ module('RAW', function (hooks) { let thing = resource(() => 2); assert.throws(() => { + // @ts-expect-error - not sure what to bo about the type discrepency atm let instance = thing.create(); instance.link({}); diff --git a/test-app/tests/helpers.ts b/test-app/tests/helpers.ts new file mode 100644 index 000000000..9fa537c6e --- /dev/null +++ b/test-app/tests/helpers.ts @@ -0,0 +1,22 @@ +import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; + +import type Owner from '@ember/owner'; + +interface CompatOwner { + getOwner: (context: unknown) => Owner | undefined; + setOwner: (context: unknown, owner: Owner) => void; +} + +export const compatOwner = {} as CompatOwner; + +if (macroCondition(dependencySatisfies('ember-source', '>=4.12.0'))) { + // In no version of ember where `@ember/owner` tried to be imported did it exist + // if (macroCondition(false)) { + // Using 'any' here because importSync can't lookup types correctly + compatOwner.getOwner = (importSync('@ember/owner') as any).getOwner; + compatOwner.setOwner = (importSync('@ember/owner') as any).setOwner; +} else { + // Using 'any' here because importSync can't lookup types correctly + compatOwner.getOwner = (importSync('@ember/application') as any).getOwner; + compatOwner.setOwner = (importSync('@ember/application') as any).setOwner; +} From b399ae050c731aaa61b5af71a0166c0049c5bba8 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 25 Jan 2025 15:01:56 -0500 Subject: [PATCH 08/12] Add rendering test --- test-app/tests/core/raw-api-test.gts | 46 +++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/test-app/tests/core/raw-api-test.gts b/test-app/tests/core/raw-api-test.gts index 939bb8bcc..2fc6a7fae 100644 --- a/test-app/tests/core/raw-api-test.gts +++ b/test-app/tests/core/raw-api-test.gts @@ -1,5 +1,8 @@ +/** eslint-disable @typescript-eslint/ban-ts-comment */ +import { cached, tracked } from '@glimmer/tracking'; +import { render, settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; +import { setupRenderingTest, setupTest } from 'ember-qunit'; import { resource } from 'ember-resources'; @@ -14,7 +17,7 @@ module('RAW', function (hooks) { compatOwner.setOwner(parent, this.owner); - // @ts-expect-error - not sure what to bo about the type discrepency atm + // @ts-expect-error let instance = thing.create(); instance.link(parent); @@ -25,7 +28,7 @@ module('RAW', function (hooks) { let thing = resource(() => 2); assert.throws(() => { - // @ts-expect-error - not sure what to bo about the type discrepency atm + // @ts-expect-error let instance = thing.create(); instance.current; @@ -36,7 +39,7 @@ module('RAW', function (hooks) { let thing = resource(() => 2); assert.throws(() => { - // @ts-expect-error - not sure what to bo about the type discrepency atm + // @ts-expect-error let instance = thing.create(); instance.link({}); @@ -45,3 +48,38 @@ module('RAW', function (hooks) { }, /Cannot link without an owner/); }); }); + +module('RAW Rendering', function (hooks) { + setupRenderingTest(hooks); + + test('is reactive', async function (assert) { + class Context { + @tracked count = 0; + + #thing = resource(() => this.count); + + @cached + get thing() { + // @ts-expect-error + let instance = this.#thing.create(); + + instance.link(this); + + return instance; + } + } + + let ctx = new Context(); + + compatOwner.setOwner(ctx, this.owner); + + await render(); + + assert.dom().hasText('0'); + + ctx.count++; + await settled(); + + assert.dom().hasText('1'); + }); +}); From d89a79b94cb4ed0e7df1b5cc8e3685dd6f2eeeb2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 25 Jan 2025 17:56:29 -0500 Subject: [PATCH 09/12] Add rendering test --- test-app/tests/core/raw-api-test.gts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test-app/tests/core/raw-api-test.gts b/test-app/tests/core/raw-api-test.gts index 2fc6a7fae..becf6fabe 100644 --- a/test-app/tests/core/raw-api-test.gts +++ b/test-app/tests/core/raw-api-test.gts @@ -1,4 +1,4 @@ -/** eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-ignore - I don't want to deal with DT/native type compatibility here import { cached, tracked } from '@glimmer/tracking'; import { render, settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; @@ -17,7 +17,7 @@ module('RAW', function (hooks) { compatOwner.setOwner(parent, this.owner); - // @ts-expect-error + // @ts-expect-error - types are a lie due to decorators let instance = thing.create(); instance.link(parent); @@ -28,7 +28,7 @@ module('RAW', function (hooks) { let thing = resource(() => 2); assert.throws(() => { - // @ts-expect-error + // @ts-expect-error - types are a lie due to decorators let instance = thing.create(); instance.current; @@ -39,7 +39,7 @@ module('RAW', function (hooks) { let thing = resource(() => 2); assert.throws(() => { - // @ts-expect-error + // @ts-expect-error - types are a lie due to decorators let instance = thing.create(); instance.link({}); @@ -60,7 +60,7 @@ module('RAW Rendering', function (hooks) { @cached get thing() { - // @ts-expect-error + // @ts-expect-error - types are a lie due to decorators let instance = this.#thing.create(); instance.link(this); From 06a644e37abfe2978beae355ecd69b166c401f48 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 26 Jan 2025 10:49:09 -0500 Subject: [PATCH 10/12] Does ember <4 not support private fields? --- test-app/tests/core/raw-api-test.gts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-app/tests/core/raw-api-test.gts b/test-app/tests/core/raw-api-test.gts index becf6fabe..e67f7e073 100644 --- a/test-app/tests/core/raw-api-test.gts +++ b/test-app/tests/core/raw-api-test.gts @@ -56,12 +56,12 @@ module('RAW Rendering', function (hooks) { class Context { @tracked count = 0; - #thing = resource(() => this.count); + _thing = resource(() => this.count); @cached get thing() { // @ts-expect-error - types are a lie due to decorators - let instance = this.#thing.create(); + let instance = this._thing.create(); instance.link(this); From 64dd82a8dd12915873f35c202a2dbc81c367fb8b Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 26 Jan 2025 10:54:29 -0500 Subject: [PATCH 11/12] I need to be able to debug --- test-app/app/app.ts | 2 ++ test-app/ember-cli-build.js | 7 +++++++ test-app/package.json | 3 ++- test-app/pnpm-lock.yaml | 13 +++++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/test-app/app/app.ts b/test-app/app/app.ts index 909d1b587..dca09c813 100644 --- a/test-app/app/app.ts +++ b/test-app/app/app.ts @@ -1,3 +1,5 @@ +import 'decorator-transforms/globals'; + import Application from '@ember/application'; // CI isn't finding the types for this diff --git a/test-app/ember-cli-build.js b/test-app/ember-cli-build.js index 2241e557c..b88dfd251 100644 --- a/test-app/ember-cli-build.js +++ b/test-app/ember-cli-build.js @@ -17,6 +17,13 @@ module.exports = function (defaults) { }, 'ember-cli-babel': { enableTypeScriptTransform: true, + disableDecoratorTransforms: true, + }, + babel: { + plugins: [ + // add the new transform. + require.resolve('decorator-transforms'), + ], }, name: 'test-app', }); diff --git a/test-app/package.json b/test-app/package.json index 563a3e417..437817b69 100644 --- a/test-app/package.json +++ b/test-app/package.json @@ -29,6 +29,7 @@ "@embroider/macros": "^1.16.10", "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", + "decorator-transforms": "^2.3.0", "ember-functions-as-helper-polyfill": "^2.0.1", "ember-resources": "workspace:*", "tracked-built-ins": "^3.1.0" @@ -50,7 +51,6 @@ "@embroider/test-setup": "^4.0.0", "@embroider/webpack": "^4.0.9", "@glint/core": "^1.5.0", - "babel-plugin-ember-template-compilation": "^2.3.0", "@glint/environment-ember-loose": "^1.5.0", "@glint/environment-ember-template-imports": "^1.0.2", "@glint/template": "^1.0.2", @@ -61,6 +61,7 @@ "@types/rsvp": "^4.0.4", "@typescript-eslint/eslint-plugin": "^5.54.0", "@typescript-eslint/parser": "^5.54.0", + "babel-plugin-ember-template-compilation": "^2.3.0", "broccoli-asset-rev": "^3.0.0", "concurrently": "^7.6.0", "ember-auto-import": "2.6.1", diff --git a/test-app/pnpm-lock.yaml b/test-app/pnpm-lock.yaml index 0e6bc1a1f..1c544d28a 100644 --- a/test-app/pnpm-lock.yaml +++ b/test-app/pnpm-lock.yaml @@ -31,6 +31,9 @@ importers: '@glimmer/tracking': specifier: ^1.1.2 version: 1.1.2 + decorator-transforms: + specifier: ^2.3.0 + version: 2.3.0(@babel/core@7.23.7) ember-functions-as-helper-polyfill: specifier: ^2.0.1 version: 2.1.2(ember-source@5.12.0(@glimmer/component@1.1.2(@babel/core@7.23.7))(@glint/template@1.2.2)(rsvp@4.8.5)(webpack@5.89.0)) @@ -3384,6 +3387,9 @@ packages: resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} engines: {node: '>=4'} + decorator-transforms@2.3.0: + resolution: {integrity: sha512-jo8c1ss9yFPudHuYYcrJ9jpkDZIoi+lOGvt+Uyp9B+dz32i50icRMx9Bfa8hEt7TnX1FyKWKkjV+cUdT/ep2kA==} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -12416,6 +12422,13 @@ snapshots: dependencies: mimic-response: 1.0.1 + decorator-transforms@2.3.0(@babel/core@7.23.7): + dependencies: + '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.23.7) + babel-import-util: 3.0.0 + transitivePeerDependencies: + - '@babel/core' + deep-extend@0.6.0: {} deep-is@0.1.4: {} From 876abecf33a8cfc84c02e46a905c64c821f9e25d Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:03:01 -0500 Subject: [PATCH 12/12] Ah, need the cached polyfill for this test --- test-app/package.json | 1 + test-app/pnpm-lock.yaml | 43 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/test-app/package.json b/test-app/package.json index 437817b69..40b1aefff 100644 --- a/test-app/package.json +++ b/test-app/package.json @@ -30,6 +30,7 @@ "@glimmer/component": "^1.1.2", "@glimmer/tracking": "^1.1.2", "decorator-transforms": "^2.3.0", + "ember-cached-decorator-polyfill": "^1.0.2", "ember-functions-as-helper-polyfill": "^2.0.1", "ember-resources": "workspace:*", "tracked-built-ins": "^3.1.0" diff --git a/test-app/pnpm-lock.yaml b/test-app/pnpm-lock.yaml index 1c544d28a..2c41c5645 100644 --- a/test-app/pnpm-lock.yaml +++ b/test-app/pnpm-lock.yaml @@ -34,6 +34,9 @@ importers: decorator-transforms: specifier: ^2.3.0 version: 2.3.0(@babel/core@7.23.7) + ember-cached-decorator-polyfill: + specifier: ^1.0.2 + version: 1.0.2(@babel/core@7.23.7)(@glint/template@1.2.2)(ember-source@5.12.0(@glimmer/component@1.1.2(@babel/core@7.23.7))(@glint/template@1.2.2)(rsvp@4.8.5)(webpack@5.89.0)) ember-functions-as-helper-polyfill: specifier: ^2.0.1 version: 2.1.2(ember-source@5.12.0(@glimmer/component@1.1.2(@babel/core@7.23.7))(@glint/template@1.2.2)(rsvp@4.8.5)(webpack@5.89.0)) @@ -2444,6 +2447,10 @@ packages: resolution: {integrity: sha512-CtWYYHU/MgK88rxMrLfkD356dApswtR/kWZ/c6JifG1m10e7tBBrs/366dFzWMAoqYmG5/JSh+94tUSpIwh+ag==} engines: {node: '>= 12.*'} + babel-import-util@1.4.1: + resolution: {integrity: sha512-TNdiTQdPhXlx02pzG//UyVPSKE7SNWjY0n4So/ZnjQpWwaM5LvWBLkWa1JKll5u06HNscHD91XZPuwrMg1kadQ==} + engines: {node: '>= 12.*'} + babel-import-util@2.1.1: resolution: {integrity: sha512-3qBQWRjzP9NreSH/YrOEU1Lj5F60+pWSLP0kIdCWxjFHH7pX2YPHIxQ67el4gnMNfYoDxSDGcT0zpVlZ+gVtQA==} engines: {node: '>= 12.*'} @@ -3516,6 +3523,16 @@ packages: resolution: {integrity: sha512-pkWIljmJClYL17YBk8FjO7NrZPQoY9v0b+FooJvaHf/xlDQIBYVP7OaDHbNuNbpj7+wAwSDAnnwxjCoLsmm4cw==} engines: {node: 12.* || 14.* || >= 16} + ember-cache-primitive-polyfill@1.0.1: + resolution: {integrity: sha512-hSPcvIKarA8wad2/b6jDd/eU+OtKmi6uP+iYQbzi5TQpjsqV6b4QdRqrLk7ClSRRKBAtdTuutx+m+X+WlEd2lw==} + engines: {node: 10.* || >= 12} + + ember-cached-decorator-polyfill@1.0.2: + resolution: {integrity: sha512-hUX6OYTKltAPAu8vsVZK02BfMTV0OUXrPqvRahYPhgS7D0I6joLjlskd7mhqJMcaXLywqceIy8/s+x8bxF8bpQ==} + engines: {node: 14.* || >= 16} + peerDependencies: + ember-source: ^3.13.0 || ^4.0.0 || >= 5.0.0 + ember-cli-app-version@7.0.0: resolution: {integrity: sha512-zWIkxvlRrW7w1/vp+bGkmS27QsVum7NKp8N9DgAjhFMWuKewVqGyl/jeYaujMS/I4WSKBzSG9WHwBy2rjbUWxA==} engines: {node: '>= 18'} @@ -11283,6 +11300,8 @@ snapshots: babel-import-util@0.2.0: {} + babel-import-util@1.4.1: {} + babel-import-util@2.1.1: {} babel-import-util@3.0.0: {} @@ -12606,6 +12625,30 @@ snapshots: - supports-color - webpack + ember-cache-primitive-polyfill@1.0.1(@babel/core@7.23.7): + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-version-checker: 5.1.2 + ember-compatibility-helpers: 1.2.7(@babel/core@7.23.7) + silent-error: 1.1.1 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-cached-decorator-polyfill@1.0.2(@babel/core@7.23.7)(@glint/template@1.2.2)(ember-source@5.12.0(@glimmer/component@1.1.2(@babel/core@7.23.7))(@glint/template@1.2.2)(rsvp@4.8.5)(webpack@5.89.0)): + dependencies: + '@embroider/macros': 1.16.10(@glint/template@1.2.2) + '@glimmer/tracking': 1.1.2 + babel-import-util: 1.4.1 + ember-cache-primitive-polyfill: 1.0.1(@babel/core@7.23.7) + ember-cli-babel: 7.26.11 + ember-cli-babel-plugin-helpers: 1.1.1 + ember-source: 5.12.0(@glimmer/component@1.1.2(@babel/core@7.23.7))(@glint/template@1.2.2)(rsvp@4.8.5)(webpack@5.89.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + ember-cli-app-version@7.0.0(ember-source@5.12.0(@glimmer/component@1.1.2(@babel/core@7.23.7))(@glint/template@1.2.2)(rsvp@4.8.5)(webpack@5.89.0)): dependencies: ember-cli-babel: 7.26.11