diff --git a/.changeset/sour-pears-tickle.md b/.changeset/sour-pears-tickle.md new file mode 100644 index 000000000..6afcefa4d --- /dev/null +++ b/.changeset/sour-pears-tickle.md @@ -0,0 +1,23 @@ +--- +"ember-resources": patch +--- + +Resolves: https://github.com/NullVoxPopuli/ember-resources/issues/958 + +`use`d Resources can now be immediately returned from other resources. + +```js +const Clock = resource(({ use }) => { + return use(Instant({ intervalMs: 1000 }); +}); + +const Stopwatch = resource(({ use }) => { + return use(Instant({ intervalMs: 0 }); +}); + + +``` diff --git a/ember-resources/src/core/class-based/manager.ts b/ember-resources/src/core/class-based/manager.ts index 1f37d2907..69784a07e 100644 --- a/ember-resources/src/core/class-based/manager.ts +++ b/ember-resources/src/core/class-based/manager.ts @@ -9,20 +9,6 @@ import { Resource } from './resource'; import type { ArgsWrapper } from '[core-types]'; import type Owner from '@ember/owner'; -/** - * - */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -// export interface Resource extends InstanceType< -// HelperLike<{ -// Args: { -// Named: NonNullable; -// Positional: NonNullable -// }; -// // Return: number -// }> -// > {} - class ResourceManager { capabilities = helperCapabilities('3.23', { hasValue: true, diff --git a/ember-resources/src/core/function-based/manager.ts b/ember-resources/src/core/function-based/manager.ts index 03851b565..80681d533 100644 --- a/ember-resources/src/core/function-based/manager.ts +++ b/ember-resources/src/core/function-based/manager.ts @@ -8,8 +8,8 @@ import { invokeHelper } from '@ember/helper'; import { capabilities as helperCapabilities } from '@ember/helper'; import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; -import { Cell } from '../../util/cell'; -import { INTERNAL } from './types'; +import { ReadonlyCell } from '../../util/cell'; +import { CURRENT, INTERNAL } from './types'; import type { Cache, @@ -22,15 +22,18 @@ import type { ResourceAPI } from './types'; import type Owner from '@ember/owner'; let getOwner: (context: unknown) => Owner | undefined; +let setOwner: (context: unknown, owner: Owner) => void; 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 getOwner = (importSync('@ember/owner') as any).getOwner; + setOwner = (importSync('@ember/owner') as any).setOwner; } else { // Using 'any' here because importSync can't lookup types correctly getOwner = (importSync('@ember/application') as any).getOwner; + setOwner = (importSync('@ember/application') as any).setOwner; } /** @@ -93,19 +96,17 @@ class FunctionResourceManager { destroy(previousCache); } - let cache = invokeHelper(owner, usable); + let nestedCache = invokeHelper(cache, usable); - associateDestroyableChild(currentFn, cache as object); + associateDestroyableChild(currentFn, nestedCache as object); - usableCache.set(usable, cache); + usableCache.set(usable, nestedCache); - return { - get current() { - let cache = usableCache.get(usable); + return new ReadonlyCell(() => { + let cache = usableCache.get(usable); - return getValue(cache); - }, - }; + return getValue(cache); + }); }; let maybeValue = currentFn({ @@ -121,6 +122,8 @@ class FunctionResourceManager { return maybeValue; }); + setOwner(cache, owner); + return { fn: thisFn, cache }; } @@ -131,8 +134,8 @@ class FunctionResourceManager { return maybeValue(); } - if (maybeValue instanceof Cell) { - return maybeValue.current; + if (isReactive(maybeValue)) { + return maybeValue[CURRENT]; } return maybeValue; @@ -143,4 +146,8 @@ class FunctionResourceManager { } } +function isReactive(maybe: unknown): maybe is Reactive { + return typeof maybe === 'object' && maybe !== null && CURRENT in maybe; +} + export const ResourceManagerFactory = (owner: Owner) => new FunctionResourceManager(owner); diff --git a/ember-resources/src/core/function-based/types.ts b/ember-resources/src/core/function-based/types.ts index 8a2cc0ee5..5ee72b371 100644 --- a/ember-resources/src/core/function-based/types.ts +++ b/ember-resources/src/core/function-based/types.ts @@ -10,9 +10,28 @@ export interface InternalFunctionResourceConfig { [INTERNAL]: true; } +export const CURRENT = Symbol('ember-resources::CURRENT'); + +export interface GlintRenderable { + /** + * Cells aren't inherently understood by Glint, + * so to work around that, we'll hook in to the fact that + * ContentValue (the type expected for all renderables), + * defines an interface with this signature. + * + * (SafeString) + * + * There *has* been interest in the community to formally support + * toString and toHTML APIs across all objects. An RFC needs to be + * written so that we can gather feedback / potential problems. + */ + toHTML(): string; +} + // Will need to be a class for .current flattening / auto-rendering -export interface Reactive { +export interface Reactive extends GlintRenderable { current: Value; + [CURRENT]: Value; [Invoke]?: Value; } diff --git a/ember-resources/src/core/use.ts b/ember-resources/src/core/use.ts index 59dda376d..397e6929b 100644 --- a/ember-resources/src/core/use.ts +++ b/ember-resources/src/core/use.ts @@ -8,6 +8,7 @@ import { associateDestroyableChild } from '@ember/destroyable'; // @ts-ignore import { invokeHelper } from '@ember/helper'; +import { ReadonlyCell } from '../util/cell'; import { INTERNAL } from './function-based/types'; import { normalizeThunk } from './utils'; @@ -120,19 +121,17 @@ function classContextLink( ): Reactive { let cache: ReturnType; - return { - get current() { - if (!cache) { - cache = invokeHelper(context, definition); + return new ReadonlyCell(() => { + if (!cache) { + cache = invokeHelper(context, definition); - associateDestroyableChild(context, cache); - } + associateDestroyableChild(context, cache); + } - let value = getValue(cache); + let value = getValue(cache); - return getCurrentValue(value); - }, - }; + return getCurrentValue(value); + }); } function argumentToDecorator(definition: Value | (() => Value)): PropertyDecorator { diff --git a/ember-resources/src/util/cell.ts b/ember-resources/src/util/cell.ts index 12596c238..b3fc09269 100644 --- a/ember-resources/src/util/cell.ts +++ b/ember-resources/src/util/cell.ts @@ -1,25 +1,35 @@ import { tracked } from '@glimmer/tracking'; import { assert } from '@ember/debug'; -interface GlintRenderable { - /** - * Cells aren't inherently understood by Glint, - * so to work around that, we'll hook in to the fact that - * ContentValue (the type expected for all renderables), - * defines an interface with this signature. - * - * (SafeString) - * - * There *has* been interest in the community to formally support - * toString and toHTML APIs across all objects. An RFC needs to be - * written so that we can gather feedback / potential problems. - */ - toHTML(): string; +export class ReadonlyCell implements Reactive { + #getter: () => Value; + + constructor(getter: () => Value) { + this.#getter = getter; + } + + toHTML(): string { + assert( + 'Not a valid API. Please access either .current or .read() if the value of this Cell is needed' + ); + } + + get [CURRENT](): Value { + return this.current; + } + + get current(): Value { + return this.#getter(); + } } -export class Cell implements GlintRenderable { +export class Cell implements Reactive { @tracked declare current: Value; + get [CURRENT](): Value { + return this.current; + } + toHTML(): string { assert( 'Not a valid API. Please access either .current or .read() if the value of this Cell is needed' @@ -132,6 +142,10 @@ export function cell(initialValue?: Value): Cell { // @ts-ignore import { capabilities as helperCapabilities, setHelperManager } from '@ember/helper'; +import { CURRENT } from '../core/function-based/types'; + +import type { GlintRenderable, Reactive } from '../core/function-based/types'; + class CellManager { capabilities = helperCapabilities('3.23', { hasValue: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42d347501..76b3f2450 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1352,7 +1352,7 @@ packages: debug: 4.3.4(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.2 - semver: 6.3.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -3226,13 +3226,6 @@ packages: regenerator-runtime: 0.13.11 dev: true - /@babel/runtime@7.22.5: - resolution: {integrity: sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.13.11 - dev: true - /@babel/runtime@7.22.6: resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==} engines: {node: '>=6.9.0'} @@ -3325,7 +3318,7 @@ packages: /@changesets/apply-release-plan@6.1.3: resolution: {integrity: sha512-ECDNeoc3nfeAe1jqJb5aFQX7CqzQhD2klXRez2JDb/aVpGUbX673HgKrnrgJRuQR/9f2TtLoYIzrGB9qwD77mg==} dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.22.6 '@changesets/config': 2.3.0 '@changesets/get-version-range-type': 0.3.2 '@changesets/git': 2.0.0 @@ -3337,18 +3330,18 @@ packages: outdent: 0.5.0 prettier: 2.8.4 resolve-from: 5.0.0 - semver: 5.7.1 + semver: 5.7.2 dev: true /@changesets/assemble-release-plan@5.2.3(patch_hash=oi6v6io33uxvojef6ezqzay4oy): resolution: {integrity: sha512-g7EVZCmnWz3zMBAdrcKhid4hkHT+Ft1n0mLussFMcB1dE2zCuwcvGoy9ec3yOgPGF4hoMtgHaMIk3T3TBdvU9g==} dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.22.6 '@changesets/errors': 0.1.4 '@changesets/get-dependents-graph': 1.3.5 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 - semver: 5.7.1 + semver: 5.7.2 dev: true patched: true @@ -3432,7 +3425,7 @@ packages: '@manypkg/get-packages': 1.1.3 chalk: 2.4.2 fs-extra: 7.0.1 - semver: 5.7.1 + semver: 5.7.2 dev: true /@changesets/get-github-info@0.5.2: @@ -3447,7 +3440,7 @@ packages: /@changesets/get-release-plan@3.0.16: resolution: {integrity: sha512-OpP9QILpBp1bY2YNIKFzwigKh7Qe9KizRsZomzLe6pK8IUo8onkAAVUD8+JRKSr8R7d4+JRuQrfSSNlEwKyPYg==} dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.22.6 '@changesets/assemble-release-plan': 5.2.3(patch_hash=oi6v6io33uxvojef6ezqzay4oy) '@changesets/config': 2.3.0 '@changesets/pre': 1.0.14 @@ -3463,7 +3456,7 @@ packages: /@changesets/git@2.0.0: resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==} dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.22.6 '@changesets/errors': 0.1.4 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 @@ -3488,7 +3481,7 @@ packages: /@changesets/pre@1.0.14: resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==} dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.22.6 '@changesets/errors': 0.1.4 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 @@ -3498,7 +3491,7 @@ packages: /@changesets/read@0.5.9: resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==} dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.22.6 '@changesets/git': 2.0.0 '@changesets/logger': 0.0.5 '@changesets/parse': 0.3.16 @@ -3519,7 +3512,7 @@ packages: /@changesets/write@0.2.3: resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==} dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.22.6 '@changesets/types': 5.2.1 fs-extra: 7.0.1 human-id: 1.0.2 @@ -4657,7 +4650,7 @@ packages: /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.22.6 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 @@ -4666,7 +4659,7 @@ packages: /@manypkg/get-packages@1.1.3: resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.22.6 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -11048,7 +11041,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.54.0(eslint@8.35.0)(typescript@5.0.3) + '@typescript-eslint/parser': 5.54.0(eslint@8.35.0)(typescript@4.8.2) debug: 3.2.7 eslint: 8.35.0 eslint-import-resolver-node: 0.3.7 @@ -11097,7 +11090,7 @@ packages: optional: true dependencies: '@babel/core': 7.22.9(supports-color@8.1.1) - '@babel/eslint-parser': 7.19.1(@babel/core@7.22.9)(eslint@8.35.0) + '@babel/eslint-parser': 7.19.1(@babel/core@7.21.4)(eslint@8.35.0) '@babel/plugin-proposal-decorators': 7.22.7(@babel/core@7.22.9) '@ember-data/rfc395-data': 0.0.4 ember-rfc176-data: 0.3.18 @@ -11162,7 +11155,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.54.0(eslint@8.35.0)(typescript@5.0.3) + '@typescript-eslint/parser': 5.54.0(eslint@8.35.0)(typescript@4.8.2) array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 @@ -14471,7 +14464,7 @@ packages: dependencies: hosted-git-info: 2.8.9 resolve: 1.22.2 - semver: 5.7.1 + semver: 5.7.2 validate-npm-package-license: 3.0.4 dev: true diff --git a/test-app/ember-cli-build.js b/test-app/ember-cli-build.js index 5711df926..bbaf03989 100644 --- a/test-app/ember-cli-build.js +++ b/test-app/ember-cli-build.js @@ -29,9 +29,9 @@ module.exports = function (defaults) { // please specify an object with the list of modules as keys // along with the exports of each module as its value. - const { maybeEmbroider } = require('@embroider/test-setup'); + const { Webpack } = require('@embroider/webpack'); - return maybeEmbroider(app, { + return require('@embroider/compat').compatBuild(app, Webpack, { packageRules: [ { package: 'test-app', @@ -44,5 +44,8 @@ module.exports = function (defaults) { }, }, ], + packagerOptions: { + webpackConfig: { devtool: 'source-map' }, + }, }); }; diff --git a/test-app/package.json b/test-app/package.json index d99886a96..5d6196662 100644 --- a/test-app/package.json +++ b/test-app/package.json @@ -20,7 +20,7 @@ "lint:prettier:fix": "prettier --write .", "lint:hbs:fix": "ember-template-lint . --fix --no-error-on-unmatched-pattern", "lint:js:fix": "eslint . --fix", - "start": "concurrently 'ember serve' 'pnpm _syncPnpm --watch' --names 'tests serve,tests sync deps'", + "start": "concurrently 'ember serve -p 4204' 'pnpm _syncPnpm --watch' --names 'tests serve,tests sync deps'", "test": "ember test", "test:ember": "ember test", "_syncPnpm": "DEBUG=true pnpm sync-dependencies-meta-injected" diff --git a/test-app/tests/core/function-resource/composition-test.gts b/test-app/tests/core/function-resource/composition-test.gts index 1a9647516..82c5854ec 100644 --- a/test-app/tests/core/function-resource/composition-test.gts +++ b/test-app/tests/core/function-resource/composition-test.gts @@ -1,4 +1,4 @@ -import { render, rerender, clearRender } from '@ember/test-helpers'; +import { render, rerender, clearRender, settled } from '@ember/test-helpers'; import { tracked } from '@glimmer/tracking'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; @@ -53,6 +53,52 @@ module('Core | (function) resource | use | rendering', function (hooks) { assert.notEqual(first, second); }); + test('it works with directly returning the resource', async function (assert) { + let controlledCount = cell(0); + + const Count = resource(() => { + return controlledCount; + }); + + const AlsoCount = resource(({ use }) => { + return use(Count); + }); + + await render(); + + assert.dom().hasText('0'); + + controlledCount.current++; + await settled(); + + assert.dom().hasText('1'); + }); + + test('it deeply works with directly returning the resource', async function (assert) { + let controlledCount = cell(0); + + const Count = resource(() => { + return controlledCount; + }); + + const AlsoCount = resource(({ use }) => { + return use(Count); + }); + + const DeeplyCount = resource(({ use }) => { + return use(AlsoCount); + }); + + await render(); + + assert.dom().hasText('0'); + + controlledCount.current++; + await settled(); + + assert.dom().hasText('1'); + }); + test('it works with the blueprint/factory', async function (assert) { let nowDate = Date.now(); let format = (time: Reactive) => formatter.format(time.current);