Skip to content

Commit 00db5bd

Browse files
authored
Merge pull request #1389 from gitKrystan/fix-canary
Support `@types`, preview types, and stable types for Ember.js
2 parents 06ae0ab + becbdd3 commit 00db5bd

15 files changed

Lines changed: 285 additions & 78 deletions

File tree

.github/workflows/ci-build.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ on:
44
push:
55
branches:
66
- master
7-
- 'v*'
7+
- "v*"
88
pull_request:
99
schedule:
10-
- cron: '0 3 * * *' # daily, at 3am
10+
- cron: "0 3 * * *" # daily, at 3am
1111

1212
jobs:
1313
test:
@@ -24,7 +24,7 @@ jobs:
2424

2525
floating-dependencies:
2626
timeout-minutes: 10
27-
name: 'Floating Dependencies'
27+
name: "Floating Dependencies"
2828
runs-on: ubuntu-latest
2929

3030
steps:
@@ -35,7 +35,7 @@ jobs:
3535

3636
try-scenarios:
3737
timeout-minutes: 10
38-
name: 'Try: ${{ matrix.ember-try-scenario }}'
38+
name: "Try: ${{ matrix.ember-try-scenario }}"
3939

4040
runs-on: ubuntu-latest
4141

@@ -47,7 +47,7 @@ jobs:
4747
ember-try-scenario:
4848
- ember-lts-4.4
4949
- ember-lts-4.8
50-
- ember-lts-5.0
50+
- ember-lts-4.12
5151
- ember-release
5252
- ember-beta
5353
- ember-canary
@@ -63,6 +63,9 @@ jobs:
6363
- name: test
6464
working-directory: addon
6565
run: node_modules/.bin/ember try:one ${{ matrix.ember-try-scenario }} --skip-cleanup
66+
- name: types compatibility
67+
working-directory: addon
68+
run: yarn lint:ts
6669

6770
types:
6871
runs-on: ubuntu-latest

addon/.eslintrc.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,6 @@ module.exports = {
5555
{
5656
files: ['addon-test-support/**/*.[jt]s'],
5757
plugins: ['disable-features'],
58-
rules: {
59-
'disable-features/disable-async-await': 'error',
60-
'disable-features/disable-generator-functions': 'error',
61-
},
6258
},
6359
{
6460
files: ['addon-test-support/**/*.[jt]s'],
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// These utter shenanigans allow this to work regardless of what types you are
2+
// importing from. The idea is: you supply multiple imports, each of which is
3+
// mutually exclusive with the others in terms of valid imports, then ignore the
4+
// imports so they type check, then "resolve" between them based on which one is
5+
// actually defined. 😭
6+
//
7+
// TODO: at the next major version (4.0), when we drop support for Ember < 5.1
8+
// or some later version and can therefore either depend *somewhat* more on
9+
// these import locations *or* can drop them entirely (depending on the path
10+
// taken in terms of the `Owner` stack), switch to the normal imports:
11+
// ```
12+
// import {
13+
// ContainerProxyMixin,
14+
// RegistryProxyMixin,
15+
// } from '@ember/-internals/runtime';
16+
// ```
17+
//
18+
// For now, we are stuck with this utterly horrifying hack to get the types for
19+
// these reliably.
20+
21+
// This pair of types lets us check whether the imported type is set. We can
22+
// then use that to pick between the imported types. The first type produces
23+
// `any` when the type is otherwise not defined at all, which means we cannot
24+
// use it directly because `any` will fall through, well, basically *any* check
25+
// we might come up with... other than checking whether `any extends` it. The
26+
// second uses that to get back a `never` so we can then "pick" between the
27+
// imported types.
28+
type AnyIfNeverElseT<T> = T extends never ? never : T;
29+
type IsNever<T> = any extends AnyIfNeverElseT<T> ? never : T;
30+
31+
// This mapped type does the "picking". You can pass it any number of types,
32+
// where only one should be available, and it will "resolve" that type.
33+
// Constraint: the `Array` type must include mutually exclusive types, such that
34+
// it should be impossible to ever get back more than one; otherwise, you will
35+
// end up with a union type of the two. That is very unlikely to be what you
36+
// want!
37+
type NonNever<Types extends Array<unknown>> = {
38+
[Index in keyof Types]: IsNever<Types[Index]>;
39+
}[number];
40+
41+
// Imports from `@types`
42+
// @ts-ignore
43+
import type CPM_DTS from '@ember/engine/-private/container-proxy-mixin.d.ts';
44+
// @ts-ignore
45+
import type RPM_DTS from '@ember/engine/-private/registry-proxy-mixin.d.ts';
46+
47+
// Imports from the preview types on 4.8
48+
// @ts-ignore
49+
import type CPM_4_8 from '@ember/engine/-private/container-proxy-mixin';
50+
// @ts-ignore
51+
import type RPM_4_8 from '@ember/engine/-private/registry-proxy-mixin';
52+
53+
// Imports from the preview types on 4.12
54+
// @ts-ignore
55+
import type CPM_4_12 from '@ember/-internals/runtime/lib/mixins/container_proxy';
56+
// @ts-ignore
57+
import type RPM_4_12 from '@ember/-internals/runtime/lib/mixins/registry_proxy';
58+
59+
// Imports available in the stable types
60+
// @ts-ignore
61+
import type { ContainerProxyMixin as CPM_stable } from '@ember/-internals/runtime';
62+
// @ts-ignore
63+
import type { RegistryProxyMixin as RPM_stable } from '@ember/-internals/runtime';
64+
65+
// We also resolve the *values* from a "stable" location. However, the *types*
66+
// for the Ember namespace do not consistently include this definition (they do
67+
// on the stable types, but not in the preview or DT types), so cast as `any` so
68+
// that it resolves regardless.
69+
import Ember from 'ember';
70+
export const ContainerProxyMixin = (Ember as any)._ContainerProxyMixin;
71+
export type ContainerProxyMixin = NonNever<
72+
[CPM_DTS, CPM_4_8, CPM_4_12, CPM_stable]
73+
>;
74+
75+
export const RegistryProxyMixin = (Ember as any)._RegistryProxyMixin;
76+
export type RegistryProxyMixin = NonNever<
77+
[RPM_DTS, RPM_4_8, RPM_4_12, RPM_stable]
78+
>;

addon/addon-test-support/@ember/test-helpers/-internal/build-registry.ts

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ import EmberObject from '@ember/object';
66
import require, { has } from 'require';
77
import Ember from 'ember';
88

9+
import { FullName } from '@ember/owner';
10+
11+
// These shenanigans work around the fact that the import locations are not
12+
// public API and are not stable, so we jump through hoops to get the right
13+
// types and values to use.
14+
import {
15+
ContainerProxyMixin,
16+
RegistryProxyMixin,
17+
} from './-owner-mixin-imports';
18+
919
/**
1020
* Adds methods that are normally only on registry to the container. This is largely to support the legacy APIs
1121
* that are not using `owner` (but are still using `this.container`).
@@ -40,12 +50,14 @@ function exposeRegistryMethodsWithoutDeprecations(container: any) {
4050
}
4151
}
4252

43-
const RegistryProxyMixin = (Ember as any)._RegistryProxyMixin;
44-
const ContainerProxyMixin = (Ember as any)._ContainerProxyMixin;
45-
53+
// NOTE: this is the same as what `EngineInstance`/`ApplicationInstance`
54+
// implement, and is thus a superset of the `InternalOwner` contract from Ember
55+
// itself.
56+
interface Owner extends RegistryProxyMixin, ContainerProxyMixin {}
4657
const Owner = EmberObject.extend(RegistryProxyMixin, ContainerProxyMixin, {
4758
_emberTestHelpersMockOwner: true,
4859

60+
/* eslint-disable valid-jsdoc */
4961
/**
5062
* Unregister a factory and its instance.
5163
*
@@ -57,13 +69,16 @@ const Owner = EmberObject.extend(RegistryProxyMixin, ContainerProxyMixin, {
5769
* @see {@link https://github.com/emberjs/ember.js/pull/12680}
5870
* @see {@link https://github.com/emberjs/ember.js/blob/v4.5.0-alpha.5/packages/%40ember/engine/instance.ts#L152-L167}
5971
*/
60-
unregister(fullName: string) {
61-
// @ts-expect-error
62-
this['__container__'].reset(fullName);
72+
/* eslint-enable valid-jsdoc */
73+
unregister(this: Owner, fullName: FullName) {
74+
// SAFETY: this is always present, but only the stable type definitions from
75+
// Ember actually preserve it, since it is private API.
76+
(this as any)['__container__'].reset(fullName);
6377

6478
// We overwrote this method from RegistryProxyMixin.
65-
// @ts-expect-error
66-
this['__registry__'].unregister(fullName);
79+
// SAFETY: this is always present, but only the stable type definitions from
80+
// Ember actually preserve it, since it is private API.
81+
(this as any)['__registry__'].unregister(fullName);
6782
},
6883
});
6984

@@ -72,46 +87,62 @@ const Owner = EmberObject.extend(RegistryProxyMixin, ContainerProxyMixin, {
7287
* @param {Object} resolver the resolver to use with the registry
7388
* @returns {Object} owner, container, registry
7489
*/
75-
export default function (resolver: Resolver) {
76-
let fallbackRegistry, registry, container;
77-
let namespace = EmberObject.create({
78-
// @ts-expect-error
79-
Resolver: {
80-
create() {
81-
return resolver;
82-
},
90+
export default function buildRegistry(resolver: Resolver) {
91+
let namespace = new Application();
92+
// @ts-ignore: this is actually the correcct type, but there was a typo in
93+
// Ember's docs for many years which meant that there was a matching problem
94+
// in the types for Ember's definition of `Engine`. Once we require at least
95+
// Ember 5.1 (in some future breaking change), this ts-ignore can be removed.
96+
namespace.Resolver = {
97+
create() {
98+
return resolver;
8399
},
84-
});
100+
};
85101

86-
fallbackRegistry = (Application as any).buildRegistry(namespace);
102+
// @ts-ignore: this is private API.
103+
let fallbackRegistry = Application.buildRegistry(namespace);
87104
// TODO: only do this on Ember < 3.13
88-
fallbackRegistry.register(
89-
'component-lookup:main',
90-
(Ember as any).ComponentLookup
91-
);
105+
// @ts-ignore: this is private API.
106+
fallbackRegistry.register('component-lookup:main', Ember.ComponentLookup);
92107

93-
registry = new (Ember as any).Registry({
108+
// @ts-ignore: this is private API.
109+
let registry = new Ember.Registry({
94110
fallback: fallbackRegistry,
95111
});
96112

97-
(ApplicationInstance as any).setupRegistry(registry);
113+
// @ts-ignore: this is private API.
114+
ApplicationInstance.setupRegistry(registry);
98115

99116
// these properties are set on the fallback registry by `buildRegistry`
100117
// and on the primary registry within the ApplicationInstance constructor
101118
// but we need to manually recreate them since ApplicationInstance's are not
102119
// exposed externally
120+
// @ts-ignore: this is private API.
103121
registry.normalizeFullName = fallbackRegistry.normalizeFullName;
122+
// @ts-ignore: this is private API.
104123
registry.makeToString = fallbackRegistry.makeToString;
124+
// @ts-ignore: this is private API.
105125
registry.describe = fallbackRegistry.describe;
106126

107127
let owner = Owner.create({
108-
// @ts-expect-error
128+
// @ts-ignore -- we do not have type safety for `Object.extend` so the type
129+
// of `Owner` here is just `EmberObject`, but we *do* constrain it to allow
130+
// only types from the actual class, so these fields are not accepted.
131+
// However, we can see that they are valid, based on the definition of
132+
// `Owner` above given that it fulfills the `InternalOwner` contract and
133+
// also extends it just as `EngineInstance` does internally.
134+
//
135+
// NOTE: we use an `ignore` directive rather than `expect-error` because in
136+
// *some* versions of the types, we *do* have (at least some of) this
137+
// safety, and maximal backwards compatibility means we have to account for
138+
// that.
109139
__registry__: registry,
110140
__container__: null,
111-
});
141+
}) as unknown as Owner;
112142

113-
container = registry.container({ owner: owner });
114-
// @ts-expect-error
143+
// @ts-ignore: this is private API.
144+
let container = registry.container({ owner: owner });
145+
// @ts-ignore: this is private API.
115146
owner.__container__ = container;
116147

117148
exposeRegistryMethodsWithoutDeprecations(container);

addon/addon-test-support/@ember/test-helpers/-internal/debug-info.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
// @ts-ignore: this is private API. This import will work Ember 5.1+ since it
2+
// "provides" this public API, but does not for earlier versions. As a result,
3+
// this type will be `any`.
14
import { _backburner } from '@ember/runloop';
2-
import {
3-
DebugInfo as BackburnerDebugInfo,
4-
QueueItem,
5-
} from '@ember/runloop/-private/backburner';
5+
import { DebugInfo as BackburnerDebugInfo } from '@ember/runloop/-private/backburner';
66
import { DebugInfoHelper, debugInfoHelpers } from './debug-info-helpers';
77
import { getPendingWaiterState, PendingWaiterState } from '@ember/test-waiters';
88

@@ -109,26 +109,29 @@ export class TestDebugInfo implements DebugInfo {
109109

110110
this._summaryInfo.pendingScheduledQueueItemCount =
111111
this._debugInfo.instanceStack
112-
.filter((q) => q)
112+
.filter(isNotNullable)
113113
.reduce((total, item) => {
114-
Object.keys(item).forEach((queueName) => {
115-
// SAFETY: this cast is *not* safe, but the underlying type is
116-
// not currently able to be safer than this because it was
117-
// built as a bag-of-queues *and* a structured item originally.
118-
total += (item[queueName] as QueueItem[]).length;
114+
Object.values(item).forEach((queueItems) => {
115+
// SAFETY: this cast is required for versions of Ember which do
116+
// not supply a correct definition of these types. It should
117+
// also be compatible with the version where Ember *does* supply
118+
// the types correctly.
119+
total +=
120+
(queueItems as Array<unknown> | undefined)?.length ?? 0;
119121
});
120122

121123
return total;
122124
}, 0);
123125
this._summaryInfo.pendingScheduledQueueItemStackTraces =
124126
this._debugInfo.instanceStack
125-
.filter((q) => q)
127+
.filter(isNotNullable)
126128
.reduce((stacks, deferredActionQueues) => {
127-
Object.keys(deferredActionQueues).forEach((queue) => {
128-
// SAFETY: this cast is *not* safe, but the underlying type is
129-
// not currently able to be safer than this because it was
130-
// built as a bag-of-queues *and* a structured item originally.
131-
(deferredActionQueues[queue] as QueueItem[]).forEach(
129+
Object.values(deferredActionQueues).forEach((queueItems) => {
130+
// SAFETY: this cast is required for versions of Ember which do
131+
// not supply a correct definition of these types. It should
132+
// also be compatible with the version where Ember *does* supply
133+
// the types correctly.
134+
(queueItems as Array<{ stack: string }> | undefined)?.forEach(
132135
(queueItem) => queueItem.stack && stacks.push(queueItem.stack)
133136
);
134137
});
@@ -222,3 +225,8 @@ export class TestDebugInfo implements DebugInfo {
222225
return `${title}: ${count}`;
223226
}
224227
}
228+
229+
// eslint-disable-next-line require-jsdoc
230+
function isNotNullable<T extends {}>(value: T | null | undefined): value is T {
231+
return value != null;
232+
}

addon/addon-test-support/@ember/test-helpers/-internal/is-component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { ComponentLike } from '@glint/template';
22

3+
// @ts-ignore: types for this API is not consistently available (via transitive
4+
// deps) and we do not currently want to make it an explicit dependency. It
5+
// does, however, consistently work at runtime. :sigh:
36
import { getInternalComponentManager as getComponentManager } from '@glimmer/manager';
47

58
/**

addon/addon-test-support/@ember/test-helpers/build-owner.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@ import Application from '@ember/application';
22
import type { Resolver } from '@ember/owner';
33

44
import legacyBuildRegistry from './-internal/build-registry';
5-
import ContainerProxyMixin from '@ember/engine/-private/container-proxy-mixin';
6-
import RegistryProxyMixin from '@ember/engine/-private/registry-proxy-mixin';
7-
import CoreObject from '@ember/object/core';
8-
9-
export interface Owner
10-
extends CoreObject,
11-
ContainerProxyMixin,
12-
RegistryProxyMixin {
5+
import EmberOwner from '@ember/owner';
6+
import { SimpleElement } from '@simple-dom/interface';
7+
8+
export interface Owner extends EmberOwner {
139
_emberTestHelpersMockOwner?: boolean;
14-
rootElement?: string | Element;
10+
rootElement?: string | Element | SimpleElement | null;
1511

1612
_lookupFactory?(key: string): any;
1713

14+
// Note: this should be the same as `Application['visit']`, but that *type* is
15+
// only available from Ember 4.12 on. Once we require Ember >= 5.1 and rely on
16+
// the stable types, this will not be necessary and the related `@ts-ignore`
17+
// below can also be removed.
1818
visit(url: string, options?: { [key: string]: any }): Promise<any>;
1919
}
2020

@@ -42,9 +42,11 @@ export default function buildOwner(
4242
resolver: Resolver | undefined | null
4343
): Promise<Owner> {
4444
if (application) {
45-
return application
46-
.boot()
47-
.then((app) => app.buildInstance().boot()) as unknown as Promise<Owner>;
45+
// @ts-ignore: this type is correct and will check against Ember 4.12 or 5.1
46+
// or later. However, the first round of preview types in Ember 4.8 does not
47+
// include the `visit` API (it was missing for many years!) and therefore
48+
// there is no way to make this assignable accross all supported versions.
49+
return application.boot().then((app) => app.buildInstance().boot());
4850
}
4951

5052
if (!resolver) {

0 commit comments

Comments
 (0)