Skip to content

Commit 6774c1e

Browse files
Merge pull request #603 from NullVoxPopuli/improved-glint-support
Improved glint support with gts / <template>
2 parents 34af43c + c6e4c9b commit 6774c1e

File tree

17 files changed

+333
-108
lines changed

17 files changed

+333
-108
lines changed

ember-resources/src/core/class-based/resource.ts

+83-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { assert } from '@ember/debug';
55
// @ts-ignore
66
import { invokeHelper } from '@ember/helper';
77

8+
import { INTERNAL } from 'core/function-based/types';
9+
810
import { DEFAULT_THUNK, normalizeThunk } from '../utils';
911

1012
import type { Cache, Thunk } from '../types';
@@ -169,10 +171,89 @@ export class Resource<T = unknown> {
169171
*/
170172
static from<T extends new (...args: any) => any>(
171173
this: T,
172-
context: object,
174+
context: InstanceType<new (...args: any) => any>,
173175
thunk?: Thunk | (() => unknown)
176+
): InstanceType<T>;
177+
178+
/**
179+
* For use in the body of a class.
180+
*
181+
* `from` is what allows resources to be used in JS, they hide the reactivity APIs
182+
* from the consumer so that the surface API is smaller.
183+
*
184+
* ```js
185+
* import { Resource, use } from 'ember-resources';
186+
*
187+
* class SomeResource extends Resource {}
188+
*
189+
* class MyClass {
190+
* @use data = SomeResource.from(() => [ ... ]);
191+
* }
192+
* ```
193+
*/
194+
static from<T extends new (...args: any) => any>(
195+
this: T,
196+
thunk: Thunk | (() => unknown)
197+
): InstanceType<T>;
198+
199+
static from<T extends new (...args: any) => any>(
200+
this: T,
201+
contextOrThunk: InstanceType<new (...args: any) => any> | Thunk | (() => unknown),
202+
thunkOrUndefined?: undefined | Thunk | (() => unknown)
174203
): InstanceType<T> {
175-
return resourceOf(context, this, thunk);
204+
/**
205+
* This first branch is for
206+
*
207+
* ```js
208+
* class Foo {
209+
* @use foo = SomeResource.from(() => [ ... ])
210+
* }
211+
* ```
212+
*
213+
* and in order to support this, we need to defer the passed
214+
* thunk until when the decorator is accessed.
215+
*
216+
* The decorator mostly does what `resourceOf` is doing below, but
217+
* a little more simply, because we don't have to deal with a Proxy.
218+
*
219+
*/
220+
if (typeof contextOrThunk === 'function') {
221+
/**
222+
* This cast is a little weird, because the narrowing from the
223+
* typeof check, while removing `object` from `contextOrThunk` does
224+
* add in `Function` to the type union and I don't know of a better way
225+
* to manage the type narrowing here.
226+
*/
227+
let thunk = contextOrThunk as Thunk | (() => unknown);
228+
229+
/**
230+
* We have to lie here because TypeScript doesn't allow decorators
231+
* to alter the type of a property.
232+
*
233+
* This is private API that the `@use` decorator understands,
234+
* but is not supported for use by any other conusmer.
235+
*/
236+
return {
237+
thunk,
238+
definition: this,
239+
type: 'class-based',
240+
[INTERNAL]: true,
241+
} as unknown as InstanceType<T>;
242+
}
243+
244+
/**
245+
* This usage is for decorator-less usage
246+
*
247+
* ```js
248+
* class Foo {
249+
* foo = SomeResource.from(this, () => [ ... ])
250+
* }
251+
* ```
252+
*
253+
* The only tradeoff is that a `this` needs to be passed.
254+
*
255+
*/
256+
return resourceOf(contextOrThunk, this, thunkOrUndefined);
176257
}
177258

178259
// owner must be | unknown as to not

ember-resources/src/core/function-based/immediate-invocation.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { createCache, getValue } from '@glimmer/tracking/primitives/cache';
33
// @ts-ignore
44
import { capabilities as helperCapabilities, invokeHelper, setHelperManager } from '@ember/helper';
55

6-
import type { resource } from './resource';
76
import type { Cache } from './types';
87
import type Owner from '@ember/owner';
98

10-
type ResourceFactory = (...args: any[]) => ReturnType<typeof resource>;
9+
type SpreadFor<T> = T extends Array<any> ? T : [T];
10+
type ResourceFactory<Value = any, Args = any[]> = (...args: SpreadFor<Args>) => Value;
1111

1212
interface State {
1313
cache?: Cache;
@@ -130,10 +130,12 @@ class ResourceInvokerManager {
130130
* })
131131
* ```
132132
*/
133-
export function resourceFactory(wrapperFn: ResourceFactory) {
133+
export function resourceFactory<Value = any, Args = any>(
134+
wrapperFn: ResourceFactory<Value, Args>
135+
): (args: () => Args) => Value {
134136
setHelperManager(ResourceInvokerFactory, wrapperFn);
135137

136-
return wrapperFn;
138+
return wrapperFn as unknown as (args: () => Args) => Value;
137139
}
138140

139141
// Provide a singleton manager.

ember-resources/src/core/function-based/manager.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { associateDestroyableChild, destroy, registerDestructor } from '@ember/d
44
// @ts-ignore
55
import { capabilities as helperCapabilities } from '@ember/helper';
66

7-
import type { Cache, Destructor, ResourceFunction } from './types';
7+
import type { Cache, Destructor, InternalFunctionResourceConfig, ResourceFunction } from './types';
88
import type Owner from '@ember/owner';
99

1010
/**
@@ -23,7 +23,8 @@ class FunctionResourceManager {
2323
* Resources do not take args.
2424
* However, they can access tracked data
2525
*/
26-
createHelper(fn: ResourceFunction) {
26+
createHelper(config: InternalFunctionResourceConfig) {
27+
let { definition: fn } = config;
2728
/**
2829
* We have to copy the `fn` in case there are multiple
2930
* usages or invocations of the function.
@@ -58,7 +59,7 @@ class FunctionResourceManager {
5859
return { fn: thisFn, cache };
5960
}
6061

61-
getValue({ cache }: { cache: Cache }) {
62+
getValue({ cache }: { fn: ResourceFunction; cache: Cache }) {
6263
let maybeValue = getValue(cache);
6364

6465
if (typeof maybeValue === 'function') {

ember-resources/src/core/function-based/resource.ts

+19-8
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ResourceManagerFactory } from './manager';
66
import { INTERNAL } from './types';
77
import { wrapForPlainUsage } from './utils';
88

9-
import type { InternalIntermediate, ResourceFn, ResourceFunction } from './types';
9+
import type { InternalFunctionResourceConfig, ResourceFn, ResourceFunction } from './types';
1010

1111
/**
1212
* `resource` is an alternative API to the class-based `Resource`.
@@ -153,7 +153,7 @@ export function resource<Value>(context: object, setup: ResourceFunction<Value>)
153153
export function resource<Value>(
154154
context: object | ResourceFunction<Value>,
155155
setup?: ResourceFunction<Value>
156-
): Value | InternalIntermediate<Value> | ResourceFn<Value> {
156+
): Value | InternalFunctionResourceConfig<Value> | ResourceFn<Value> {
157157
if (!setup) {
158158
assert(
159159
`When using \`resource\` with @use, ` +
@@ -162,23 +162,28 @@ export function resource<Value>(
162162
typeof context === 'function'
163163
);
164164

165+
let internalConfig: InternalFunctionResourceConfig<Value> = {
166+
definition: context as ResourceFunction<Value>,
167+
type: 'function-based',
168+
[INTERNAL]: true,
169+
};
170+
165171
/**
166172
* Functions have a different identity every time they are defined.
167173
* The primary purpose of the `resource` wrapper is to individually
168174
* register each function with our helper manager.
169175
*/
170-
setHelperManager(ResourceManagerFactory, context);
176+
setHelperManager(ResourceManagerFactory, internalConfig);
171177

172178
/**
173179
* With only one argument, we have to do a bunch of lying to
174180
* TS, because we need a special object to pass to `@use`
175181
*
176182
* Add secret key to help @use assert against
177183
* using vanilla functions as resources without the resource wrapper
184+
*
178185
*/
179-
(context as any)[INTERNAL] = true;
180-
181-
return context as ResourceFn<Value>;
186+
return internalConfig as unknown as ResourceFn<Value>;
182187
}
183188

184189
assert(
@@ -193,7 +198,13 @@ export function resource<Value>(
193198
typeof setup === 'function'
194199
);
195200

196-
setHelperManager(ResourceManagerFactory, setup);
201+
let internalConfig: InternalFunctionResourceConfig<Value> = {
202+
definition: setup as ResourceFunction<Value>,
203+
type: 'function-based',
204+
[INTERNAL]: true,
205+
};
206+
207+
setHelperManager(ResourceManagerFactory, internalConfig);
197208

198-
return wrapForPlainUsage(context, setup);
209+
return wrapForPlainUsage(context, internalConfig);
199210
}

ember-resources/src/core/function-based/types.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
export const INTERMEDIATE_VALUE = '__Intermediate_Value__';
22
export const INTERNAL = '__INTERNAL__';
33

4-
/**
5-
* Secret args to allow `resource` to be used without
6-
* a decorator
7-
*/
8-
export interface InternalIntermediate<Value> {
4+
export interface InternalFunctionResourceConfig<Value = unknown> {
5+
definition: ResourceFunction<Value>;
6+
type: 'function-based';
97
[INTERNAL]: true;
10-
[INTERMEDIATE_VALUE]: ResourceFunction<Value>;
118
}
129

1310
export type Hooks = {

ember-resources/src/core/function-based/utils.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { invokeHelper } from '@ember/helper';
55

66
import { INTERMEDIATE_VALUE } from './types';
77

8-
import type { ResourceFunction } from './types';
8+
import type { InternalFunctionResourceConfig } from './types';
99

1010
/**
1111
* This is what allows resource to be used withotu @use.
@@ -14,7 +14,10 @@ import type { ResourceFunction } from './types';
1414
*
1515
* A resource not using use *must* be an object.
1616
*/
17-
export function wrapForPlainUsage<Value>(context: object, setup: ResourceFunction<Value>) {
17+
export function wrapForPlainUsage<Value>(
18+
context: object,
19+
setup: InternalFunctionResourceConfig<Value>
20+
) {
1821
let cache: Cache;
1922

2023
/*

ember-resources/src/core/use.ts

+33-13
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,23 @@ import { associateDestroyableChild } from '@ember/destroyable';
66
import { invokeHelper } from '@ember/helper';
77

88
import { INTERNAL } from './function-based/types';
9+
import { normalizeThunk } from './utils';
910

10-
import type { ResourceFunction } from './function-based/types';
11+
import type { InternalFunctionResourceConfig } from './function-based/types';
12+
import type { Thunk } from '[core-types]';
1113

1214
interface Descriptor {
1315
initializer: () => unknown;
1416
}
1517

16-
type ResourceInitializer = {
18+
interface ClassResourceConfig {
19+
thunk: Thunk;
20+
definition: unknown;
21+
type: 'class-based';
1722
[INTERNAL]: true;
18-
} & ResourceFunction<unknown>;
23+
}
24+
25+
type Config = ClassResourceConfig | InternalFunctionResourceConfig;
1926

2027
/**
2128
* The `@use` decorator has two responsibilities
@@ -51,29 +58,42 @@ export function use(_prototype: object, key: string, descriptor?: Descriptor): v
5158

5259
let { initializer } = descriptor;
5360

61+
assert(
62+
`@use may only be used on initialized properties. For example, ` +
63+
`\`@use foo = resource(() => { ... })\` or ` +
64+
`\`@use foo = SomeResource.from(() => { ... });\``,
65+
initializer
66+
);
67+
5468
// https://github.com/pzuraq/ember-could-get-used-to-this/blob/master/addon/index.js
5569
return {
5670
get(this: object) {
5771
let cache = caches.get(this);
5872

5973
if (!cache) {
60-
let fn = initializer.call(this);
74+
let config = initializer.call(this) as Config;
6175

6276
assert(
63-
`Expected initialized value under @use to have used the \`resource\` wrapper function`,
64-
isResourceInitializer(fn)
77+
`Expected initialized value under @use to have used either the \`resource\` wrapper function, or a \`Resource.from\` call`,
78+
INTERNAL in config
6579
);
6680

67-
cache = invokeHelper(this, fn);
68-
caches.set(this as object, cache);
69-
associateDestroyableChild(this, cache);
81+
if (config.type === 'function-based') {
82+
cache = invokeHelper(this, config);
83+
caches.set(this as object, cache);
84+
associateDestroyableChild(this, cache);
85+
} else if (config.type === 'class-based') {
86+
let { definition, thunk } = config;
87+
88+
cache = invokeHelper(this, definition, () => normalizeThunk(thunk));
89+
caches.set(this as object, cache);
90+
associateDestroyableChild(this, cache);
91+
}
92+
93+
assert(`Failed to create cache for internal resource configuration object`, cache);
7094
}
7195

7296
return getValue(cache);
7397
},
7498
} as unknown as void /* Thanks TS. */;
7599
}
76-
77-
function isResourceInitializer(obj: unknown): obj is ResourceInitializer {
78-
return typeof obj === 'function' && obj !== null && INTERNAL in obj;
79-
}

ember-resources/src/util/helper.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import { DEFAULT_THUNK, normalizeThunk } from '../core/utils';
88
import type { Cache, Thunk } from '../core/types';
99
import type ClassBasedHelper from '@ember/component/helper';
1010
import type { FunctionBasedHelper } from '@ember/component/helper';
11-
import type { Get, HelperLike } from '@glint/template';
11+
import type { HelperLike } from '@glint/template';
12+
13+
type Get<T, K, Otherwise = unknown> = K extends keyof T ? T[K] : Otherwise;
1214

1315
/**
1416
* @utility implemented with raw `invokeHelper` API, no classes from `ember-resources` used.

0 commit comments

Comments
 (0)