Skip to content

[FEATURE beta] Introduce preview types #20193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
80d4a8c
[FEATURE] Introduce a preview types package
chriskrycho Aug 23, 2022
4cfec39
Begin iterating on object and application preview types
chriskrycho Aug 25, 2022
44225d2
Fix many errors in types preview
chriskrycho Aug 26, 2022
3a3f44f
Rework preview type tests for `@ember/object/core`
chriskrycho Aug 29, 2022
d74ec00
Support the same type tests in the Ember namespace as in `@ember/object`
chriskrycho Aug 29, 2022
f02fade
Fix preview type tests for @ember/object/observable
chriskrycho Aug 29, 2022
af95f0c
type test fixes for 'ember-tests'
chriskrycho Aug 29, 2022
50ba435
Update preview type tests for Ember.DataAdapter
chriskrycho Aug 29, 2022
4318136
Update preview type tests for EmberObject.create()
chriskrycho Aug 29, 2022
0ac44ad
Update preview type tests for EmberObject.reopen()
chriskrycho Aug 29, 2022
ae59a59
Update preview type tests for many Mixin types
chriskrycho Aug 29, 2022
2939f49
Update preview type tests for arrays/array mixins
chriskrycho Aug 29, 2022
d29df63
Lint autofix and remove DT headers from a bunch of preview types
chriskrycho Aug 29, 2022
d21c1dc
Update preview type tests for array and mutable
chriskrycho Aug 29, 2022
16855e9
Update preview types for @ember/controller
chriskrycho Aug 29, 2022
25462e1
Fix more preview types and types tests for EmberArray
chriskrycho Aug 29, 2022
c3b9e5a
Fix preview types and tests for `Evented`
chriskrycho Aug 29, 2022
d52d1ca
Fix preview types and tests for `.create()` and `.extend()`
chriskrycho Aug 29, 2022
ad2a026
Fix preview type tests for `@ember/object/observable`
chriskrycho Aug 29, 2022
f3cfdd5
Fix a few more preview type test issues around observable
chriskrycho Aug 29, 2022
e8a33f3
Fix preview type tests for Ember Component
chriskrycho Aug 29, 2022
a44f30e
Improve preview types and tests for helper
chriskrycho Aug 29, 2022
cd4da05
Improve preview types and tests for controllers
chriskrycho Aug 29, 2022
69aa362
Fix preview types/tests for @ember/service
chriskrycho Aug 29, 2022
b4a6d2c
Introduce type utilities for safe `get(Properties)`
chriskrycho Aug 30, 2022
3b04c24
Add sundry improvements to preview types.
chriskrycho Aug 30, 2022
82c4efa
Ignore a case where Ember.get is distinct from get
chriskrycho Aug 30, 2022
11a144f
Simplify handling of preview types for get(Properties)
chriskrycho Aug 30, 2022
c16c707
Finish getting preview types type checking
chriskrycho Aug 30, 2022
76d0b62
Run Prettier on types/preview directory
chriskrycho Aug 30, 2022
6636579
Implement publishable types preview layout
chriskrycho Aug 31, 2022
e320eaa
Add type checking for preview types to package scripts
chriskrycho Aug 31, 2022
074ad88
Add array prototype extension preview types
chriskrycho Aug 31, 2022
6b64fb2
Remove types for array prototype extensions
chriskrycho Sep 1, 2022
75b6126
Types preview: prettier and remove tslint references
chriskrycho Sep 1, 2022
6c75ebe
Types preview: import from 'ember-source/preview'
chriskrycho Sep 1, 2022
b12893c
Types preview: remove private DT route types
chriskrycho Sep 1, 2022
87b7cf5
Types: exclude from ESLint
chriskrycho Sep 1, 2022
99350b7
Types preview: remove dead imports
chriskrycho Sep 6, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ node-tests/fixtures/**/*.js
dist/
tmp/
smoke-tests/
types/
type-tests/preview
19 changes: 17 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"dist/ember-testing.js",
"dist/ember-testing.map",
"docs/data.json",
"lib"
"lib",
"types"
],
"repository": {
"type": "git",
Expand All @@ -40,7 +41,9 @@
"lint:docs": "qunit tests/docs/coverage-test.js",
"lint:eslint": "eslint --report-unused-disable-directives --cache .",
"lint:eslint:fix": "npm-run-all \"lint:eslint --fix\"",
"lint:tsc": "tsc --noEmit",
"lint:tsc:stable": "tsc --noEmit",
"lint:tsc:preview": "tsc --noEmit --project type-tests/preview",
"lint:tsc": "npm-run-all lint:tsc:*",
"lint:fix": "npm-run-all lint:*:fix",
"test": "node bin/run-tests.js",
"test:blueprints:js": "mocha node-tests/blueprints/**/*-test.js",
Expand Down Expand Up @@ -80,6 +83,7 @@
"devDependencies": {
"@babel/preset-env": "^7.16.11",
"@glimmer/compiler": "0.84.2",
"@glimmer/component": "^1.1.2",
"@glimmer/destroyable": "0.84.2",
"@glimmer/env": "^0.1.7",
"@glimmer/global-context": "0.84.2",
Expand All @@ -93,6 +97,7 @@
"@glimmer/runtime": "0.84.2",
"@glimmer/validator": "0.84.2",
"@simple-dom/document": "^1.4.0",
"@tsconfig/ember": "^1.0.1",
"@types/qunit": "^2.19.2",
"@types/rsvp": "^4.0.4",
"@typescript-eslint/eslint-plugin": "^5.22.0",
Expand Down Expand Up @@ -154,10 +159,20 @@
"testem-failure-only-reporter": "^1.0.0",
"typescript": "~4.6.4"
},
"peerDependencies": {
"@glimmer/component": "^1.1.2"
},
"engines": {
"node": ">= 12.*"
},
"ember-addon": {
"after": "ember-cli-legacy-blueprints"
},
"typesVersions": {
"*": {
"preview": [
"./types/preview"
]
}
}
}
16 changes: 8 additions & 8 deletions packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ import { assert } from '@ember/debug';
// This is defined as a separate interface so that it can be used in the definition of
// `Owner` without also including the `__registry__` property.
export interface IRegistry {
/**
Given a fullName return the corresponding factory.

@public
@method resolveRegistration
@param {String} fullName
@return {Function} fullName's factory
*/
resolveRegistration(fullName: string): Factory<object> | object | undefined;

register(fullName: string, factory: Factory<object> | object, options?: TypeOptions): void;
Expand Down Expand Up @@ -48,14 +56,6 @@ interface RegistryProxyMixin extends IRegistry {
const RegistryProxyMixin = Mixin.create({
__registry__: null,

/**
Given a fullName return the corresponding factory.

@public
@method resolveRegistration
@param {String} fullName
@return {Function} fullName's factory
*/
resolveRegistration(fullName: string) {
assert('fullName must be a proper full name', this.__registry__.isValidFullName(fullName));
return this.__registry__.resolve(fullName);
Expand Down
7 changes: 6 additions & 1 deletion packages/@ember/array/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ interface EmberArray<T> extends Enumerable {
): NativeArray<T>;
filterBy(key: string, value?: unknown): NativeArray<T>;
rejectBy(key: string, value?: unknown): NativeArray<T>;
find<S extends T, Target = void>(
predicate: (this: void, value: T, index: number, obj: T[]) => value is S,
thisArg?: Target
): S | undefined;
find<Target = void>(
callback: (this: Target, item: T, index: number, arr: this) => unknown,
target?: Target
Expand Down Expand Up @@ -1907,7 +1911,8 @@ const MutableArray = Mixin.create(EmberArray, MutableEnumerable, {
*/
interface NativeArray<T>
extends Omit<Array<T>, 'every' | 'filter' | 'find' | 'forEach' | 'map' | 'reduce' | 'slice'>,
MutableArray<T> {}
MutableArray<T>,
Observable {}

let NativeArray = Mixin.create(MutableArray, Observable, {
objectAt(idx: number) {
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@

"include": ["packages/**/*.ts"],

"exclude": ["dist", "node_modules", "tmp"]
"exclude": ["dist", "node_modules", "tmp", "types"]
}
45 changes: 45 additions & 0 deletions type-tests/preview/@ember/application-test/application-instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import ApplicationInstance from '@ember/application/instance';

declare function hbs(strings: TemplateStringsArray): object;

const appInstance = ApplicationInstance.create();
appInstance.register('some:injection', class Foo {});

appInstance.register('some:injection', class Foo {}, {
singleton: true,
});

appInstance.register('some:injection', class Foo {}, {
instantiate: false,
});

appInstance.register('templates:foo/bar', hbs`<h1>Hello World</h1>`);
appInstance.register('templates:foo/bar', hbs`<h1>Hello World</h1>`, {
singleton: true,
});
appInstance.register('templates:foo/bar', hbs`<h1>Hello World</h1>`, {
instantiate: true,
});
appInstance.register('templates:foo/bar', hbs`<h1>Hello World</h1>`, {
singleton: true,
instantiate: true,
});
appInstance.register('templates:foo/bar', hbs`<h1>Hello World</h1>`, {
// @ts-expect-error
singleton: 'true',
instantiate: true,
});

appInstance.register('some:injection', class Foo {}, {
singleton: false,
instantiate: true,
});

appInstance.factoryFor('router:main');
appInstance.lookup('route:basic');

appInstance.boot();

(async () => {
await appInstance.boot();
})();
44 changes: 44 additions & 0 deletions type-tests/preview/@ember/application-test/application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Application from '@ember/application';
import ApplicationInstance from '@ember/application/instance';
import EmberObject from '@ember/object';
import { expectTypeOf } from 'expect-type';

const BaseApp = Application.extend({
modulePrefix: 'my-app',
});

class Obj extends EmberObject.extend({ foo: 'bar' }) {}

BaseApp.initializer({
name: 'my-initializer',
initialize(app) {
app.register('foo:bar', Obj);
},
});

BaseApp.instanceInitializer({
name: 'my-instance-initializer',
initialize(app) {
(app.lookup('foo:bar') as Obj).get('foo');
},
});

const App1 = BaseApp.create({
rootElement: '#app-one',
customEvents: {
paste: 'paste',
},
});

const App2 = BaseApp.create({
rootElement: '#app-two',
customEvents: {
mouseenter: null,
mouseleave: null,
},
});

const App3 = BaseApp.create();

expectTypeOf(App3.buildInstance()).toEqualTypeOf<ApplicationInstance>();
expectTypeOf(App3.buildInstance({ foo: 'bar' })).toEqualTypeOf<ApplicationInstance>();
17 changes: 17 additions & 0 deletions type-tests/preview/@ember/application-test/deprecations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { deprecate, deprecateFunc } from '@ember/application/deprecations';

deprecate('this is no longer advised', false, {
id: 'no-longer-advised',
until: 'v4.0',
});
deprecate('this is no longer advised', false, {
id: 'no-longer-advised',
until: 'v4.0',
url: 'https://emberjs.com',
});
// @ts-expect-error
deprecate('this is no longer advised', false);

// @ts-expect-error
deprecateFunc('this is no longer advised', () => {});
deprecateFunc('this is no longer advised', { id: 'no-longer-do-this', until: 'v4.0' }, () => {});
27 changes: 27 additions & 0 deletions type-tests/preview/@ember/application-test/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getOwner, setOwner } from '@ember/application';
import EngineInstance from '@ember/engine/instance';
import Owner from '@ember/owner';
import ApplicationInstance from '@ember/application/instance';
import Service from '@ember/service';
import { expectTypeOf } from 'expect-type';

expectTypeOf(getOwner({})).toEqualTypeOf<Owner | undefined>();

// Confirm that random subclasses work as expected.
declare class MyService extends Service {
withStuff: true;
}
declare let myService: MyService;
expectTypeOf(getOwner(myService)).toEqualTypeOf<Owner>();

// @ts-expect-error
getOwner();

declare let baseOwner: Owner;
expectTypeOf(setOwner({}, baseOwner)).toBeVoid();

declare let engine: EngineInstance;
expectTypeOf(setOwner({}, engine)).toBeVoid();

declare let application: ApplicationInstance;
expectTypeOf(setOwner({}, application)).toBeVoid();
60 changes: 60 additions & 0 deletions type-tests/preview/@ember/array-test/array-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import ArrayProxy from '@ember/array/proxy';
import EmberArray, { A } from '@ember/array';
import EmberObject from '@ember/object';
import { expectTypeOf } from 'expect-type';

const pets = ['dog', 'cat', 'fish'];
const proxy = ArrayProxy.create({ content: A(pets) });

proxy.get('firstObject'); // 'dog'
proxy.set('content', A(['amoeba', 'paramecium']));
proxy.get('firstObject'); // 'amoeba'

const overridden = ArrayProxy.create({
content: A(pets),
objectAtContent(this: ArrayProxy<string>, idx: number): string | undefined {
// NOTE: cast is necessary because `this` is not managed correctly in the
// `.create()` body anymore.
return (this.get('content') as EmberArray<string>).objectAt(idx)?.toUpperCase();
},
});

overridden.get('firstObject'); // 'DOG'

class MyNewProxy<T> extends ArrayProxy<T> {
isNew = true;
}

const x = MyNewProxy.create({ content: A([1, 2, 3]) }) as MyNewProxy<number>;
expectTypeOf(x.get('firstObject')).toEqualTypeOf<number | undefined>();
expectTypeOf(x.isNew).toBeBoolean();

// Custom EmberArray
interface MyArray<T> extends EmberObject, EmberArray<T> {}
class MyArray<T> extends EmberObject.extend(EmberArray) {
constructor(content: ArrayLike<T>) {
super();
this._content = content;
}

_content: ArrayLike<T>;

get length() {
return this._content.length;
}

objectAt(idx: number) {
return this._content[idx];
}
}

const customArrayProxy = ArrayProxy.create({ content: new MyArray(pets) });
customArrayProxy.get('firstObject'); // 'dog'

// Vanilla array
const vanillaArrayProxy = ArrayProxy.create({ content: pets });
vanillaArrayProxy.get('firstObject'); // 'dog'

// Nested ArrayProxy
const nestedArrayProxy = ArrayProxy.create({ content: proxy });
nestedArrayProxy.get('firstObject'); // 'amoeba'
73 changes: 73 additions & 0 deletions type-tests/preview/@ember/array-test/array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import EmberObject from '@ember/object';
import type Array from '@ember/array';
import { A } from '@ember/array';
import type MutableArray from '@ember/array/mutable';
import { expectTypeOf } from 'expect-type';

class Person extends EmberObject {
name = '';
isHappy = false;
}

const people = A([
Person.create({ name: 'Yehuda', isHappy: true }),
Person.create({ name: 'Majd', isHappy: false }),
]);

expectTypeOf(people.get('length')).toBeNumber();
expectTypeOf(people.get('lastObject')).toEqualTypeOf<Person | undefined>();
expectTypeOf(people.get('firstObject')).toEqualTypeOf<Person | undefined>();
expectTypeOf(people.isAny('isHappy')).toBeBoolean();
expectTypeOf(people.isAny('isHappy', false)).toBeBoolean();
// @ts-expect-error -- string != boolean
people.isAny('isHappy', 'false');

expectTypeOf(people.objectAt(0)).toEqualTypeOf<Person | undefined>();
expectTypeOf(people.objectsAt([1, 2, 3])).toEqualTypeOf<Array<Person | undefined>>();

expectTypeOf(people.filterBy('isHappy')).toMatchTypeOf<Person[]>();
expectTypeOf(people.filterBy('isHappy')).toMatchTypeOf<MutableArray<Person>>();
expectTypeOf(people.rejectBy('isHappy')).toMatchTypeOf<Person[]>();
expectTypeOf(people.rejectBy('isHappy')).toMatchTypeOf<MutableArray<Person>>();
expectTypeOf(people.filter((person) => person.get('name') === 'Yehuda')).toMatchTypeOf<Person[]>();
expectTypeOf(people.filter((person) => person.get('name') === 'Yehuda')).toMatchTypeOf<
MutableArray<Person>
>();

expectTypeOf(people.get('[]')).toEqualTypeOf<typeof people>();
expectTypeOf(people.get('[]').get('firstObject')).toEqualTypeOf<Person | undefined>();

expectTypeOf(people.mapBy('isHappy')).toMatchTypeOf<boolean[]>();
expectTypeOf(people.mapBy('name.length')).toMatchTypeOf<unknown[]>();

const last = people.get('lastObject');
expectTypeOf(last).toEqualTypeOf<Person | undefined>();
if (last) {
expectTypeOf(last.get('name')).toBeString();
}

const first = people.get('lastObject');
if (first) {
expectTypeOf(first.get('isHappy')).toBeBoolean();
}

const letters = A(['a', 'b', 'c']);
const codes = letters.map((item, index, array) => {
expectTypeOf(item).toBeString();
expectTypeOf(index).toBeNumber();
expectTypeOf(array).toMatchTypeOf<string[]>();
return item.charCodeAt(0);
});
expectTypeOf(codes).toMatchTypeOf<number[]>();

const value = '1,2,3';
const filters = A(value.split(','));
filters.push('4');
filters.sort();

const multiSortArr = A([
{ k: 'a', v: 'z' },
{ k: 'a', v: 'y' },
{ k: 'b', v: 'c' },
]);
multiSortArr.sortBy('k', 'v');
Loading