Skip to content

Commit

Permalink
Merge pull request #20180 from emberjs/preview-types
Browse files Browse the repository at this point in the history
[FEATURE] Introduce preview types
  • Loading branch information
chriskrycho authored Sep 6, 2022
2 parents bf16bd3 + 119b42c commit 669a8f8
Show file tree
Hide file tree
Showing 188 changed files with 11,647 additions and 19 deletions.
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

0 comments on commit 669a8f8

Please sign in to comment.