Skip to content
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

[FEATURE] Introduce preview types #20180

Merged
merged 39 commits into from Sep 6, 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
6278a8b
[FEATURE] Introduce a preview types package
chriskrycho Aug 23, 2022
414a763
Begin iterating on object and application preview types
chriskrycho Aug 25, 2022
14acfd7
Fix many errors in types preview
chriskrycho Aug 26, 2022
061f2b3
Rework preview type tests for `@ember/object/core`
chriskrycho Aug 29, 2022
19507de
Support the same type tests in the Ember namespace as in `@ember/object`
chriskrycho Aug 29, 2022
f730d9e
Fix preview type tests for @ember/object/observable
chriskrycho Aug 29, 2022
66c28fc
type test fixes for 'ember-tests'
chriskrycho Aug 29, 2022
71e1e3f
Update preview type tests for Ember.DataAdapter
chriskrycho Aug 29, 2022
81655ae
Update preview type tests for EmberObject.create()
chriskrycho Aug 29, 2022
920901e
Update preview type tests for EmberObject.reopen()
chriskrycho Aug 29, 2022
a5e3642
Update preview type tests for many Mixin types
chriskrycho Aug 29, 2022
9ca3053
Update preview type tests for arrays/array mixins
chriskrycho Aug 29, 2022
d845d4a
Lint autofix and remove DT headers from a bunch of preview types
chriskrycho Aug 29, 2022
bb0811d
Update preview type tests for array and mutable
chriskrycho Aug 29, 2022
52abdf1
Update preview types for @ember/controller
chriskrycho Aug 29, 2022
2dab322
Fix more preview types and types tests for EmberArray
chriskrycho Aug 29, 2022
4730ecd
Fix preview types and tests for `Evented`
chriskrycho Aug 29, 2022
b450bc8
Fix preview types and tests for `.create()` and `.extend()`
chriskrycho Aug 29, 2022
cb5fb2b
Fix preview type tests for `@ember/object/observable`
chriskrycho Aug 29, 2022
fe030dc
Fix a few more preview type test issues around observable
chriskrycho Aug 29, 2022
d95a927
Fix preview type tests for Ember Component
chriskrycho Aug 29, 2022
69eda9f
Improve preview types and tests for helper
chriskrycho Aug 29, 2022
d32122b
Improve preview types and tests for controllers
chriskrycho Aug 29, 2022
20c6148
Fix preview types/tests for @ember/service
chriskrycho Aug 29, 2022
94716e4
Introduce type utilities for safe `get(Properties)`
chriskrycho Aug 30, 2022
2dc87a1
Add sundry improvements to preview types.
chriskrycho Aug 30, 2022
a0fd30a
Ignore a case where Ember.get is distinct from get
chriskrycho Aug 30, 2022
241a918
Simplify handling of preview types for get(Properties)
chriskrycho Aug 30, 2022
d7667a5
Finish getting preview types type checking
chriskrycho Aug 30, 2022
225c291
Run Prettier on types/preview directory
chriskrycho Aug 30, 2022
33fb32b
Implement publishable types preview layout
chriskrycho Aug 31, 2022
dc1d4fd
Add type checking for preview types to package scripts
chriskrycho Aug 31, 2022
434c714
Add array prototype extension preview types
chriskrycho Aug 31, 2022
17df5a1
Remove types for array prototype extensions
chriskrycho Sep 1, 2022
121ab1c
Types preview: prettier and remove tslint references
chriskrycho Sep 1, 2022
303505a
Types preview: import from 'ember-source/preview'
chriskrycho Sep 1, 2022
f8a9e83
Types preview: remove private DT route types
chriskrycho Sep 1, 2022
45a03df
Types: exclude from ESLint
chriskrycho Sep 1, 2022
119b42c
Types preview: remove dead imports
chriskrycho Sep 6, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintignore
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
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
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
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
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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');