diff --git a/.eslintignore b/.eslintignore index 96eab461a6a..7ce02292986 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,3 +6,5 @@ node-tests/fixtures/**/*.js dist/ tmp/ smoke-tests/ +types/ +type-tests/preview diff --git a/package.json b/package.json index af8309ccb97..1139694c356 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "dist/ember-testing.js", "dist/ember-testing.map", "docs/data.json", - "lib" + "lib", + "types" ], "repository": { "type": "git", @@ -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", @@ -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", @@ -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", @@ -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" + ] + } } } diff --git a/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts b/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts index d5555d8f0a2..2c7ccfdea70 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts @@ -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 | undefined; register(fullName: string, factory: Factory | object, options?: TypeOptions): void; @@ -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); diff --git a/packages/@ember/array/index.ts b/packages/@ember/array/index.ts index f3c1d72de66..3de3d1605e8 100644 --- a/packages/@ember/array/index.ts +++ b/packages/@ember/array/index.ts @@ -279,6 +279,10 @@ interface EmberArray extends Enumerable { ): NativeArray; filterBy(key: string, value?: unknown): NativeArray; rejectBy(key: string, value?: unknown): NativeArray; + find( + predicate: (this: void, value: T, index: number, obj: T[]) => value is S, + thisArg?: Target + ): S | undefined; find( callback: (this: Target, item: T, index: number, arr: this) => unknown, target?: Target @@ -1907,7 +1911,8 @@ const MutableArray = Mixin.create(EmberArray, MutableEnumerable, { */ interface NativeArray extends Omit, 'every' | 'filter' | 'find' | 'forEach' | 'map' | 'reduce' | 'slice'>, - MutableArray {} + MutableArray, + Observable {} let NativeArray = Mixin.create(MutableArray, Observable, { objectAt(idx: number) { diff --git a/tsconfig.json b/tsconfig.json index 240bde985ab..7b0a17746e1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,5 +35,5 @@ "include": ["packages/**/*.ts"], - "exclude": ["dist", "node_modules", "tmp"] + "exclude": ["dist", "node_modules", "tmp", "types"] } diff --git a/type-tests/preview/@ember/application-test/application-instance.ts b/type-tests/preview/@ember/application-test/application-instance.ts new file mode 100644 index 00000000000..07bbf776dee --- /dev/null +++ b/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`

Hello World

`); +appInstance.register('templates:foo/bar', hbs`

Hello World

`, { + singleton: true, +}); +appInstance.register('templates:foo/bar', hbs`

Hello World

`, { + instantiate: true, +}); +appInstance.register('templates:foo/bar', hbs`

Hello World

`, { + singleton: true, + instantiate: true, +}); +appInstance.register('templates:foo/bar', hbs`

Hello World

`, { + // @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(); +})(); diff --git a/type-tests/preview/@ember/application-test/application.ts b/type-tests/preview/@ember/application-test/application.ts new file mode 100755 index 00000000000..fa90f68b203 --- /dev/null +++ b/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(); +expectTypeOf(App3.buildInstance({ foo: 'bar' })).toEqualTypeOf(); diff --git a/type-tests/preview/@ember/application-test/deprecations.ts b/type-tests/preview/@ember/application-test/deprecations.ts new file mode 100644 index 00000000000..414e38af111 --- /dev/null +++ b/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' }, () => {}); diff --git a/type-tests/preview/@ember/application-test/index.ts b/type-tests/preview/@ember/application-test/index.ts new file mode 100644 index 00000000000..aa0d14e762d --- /dev/null +++ b/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(); + +// Confirm that random subclasses work as expected. +declare class MyService extends Service { + withStuff: true; +} +declare let myService: MyService; +expectTypeOf(getOwner(myService)).toEqualTypeOf(); + +// @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(); diff --git a/type-tests/preview/@ember/array-test/array-proxy.ts b/type-tests/preview/@ember/array-test/array-proxy.ts new file mode 100644 index 00000000000..9b2c81782ff --- /dev/null +++ b/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, 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).objectAt(idx)?.toUpperCase(); + }, +}); + +overridden.get('firstObject'); // 'DOG' + +class MyNewProxy extends ArrayProxy { + isNew = true; +} + +const x = MyNewProxy.create({ content: A([1, 2, 3]) }) as MyNewProxy; +expectTypeOf(x.get('firstObject')).toEqualTypeOf(); +expectTypeOf(x.isNew).toBeBoolean(); + +// Custom EmberArray +interface MyArray extends EmberObject, EmberArray {} +class MyArray extends EmberObject.extend(EmberArray) { + constructor(content: ArrayLike) { + super(); + this._content = content; + } + + _content: ArrayLike; + + 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' diff --git a/type-tests/preview/@ember/array-test/array.ts b/type-tests/preview/@ember/array-test/array.ts new file mode 100755 index 00000000000..8f7df294f56 --- /dev/null +++ b/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(); +expectTypeOf(people.get('firstObject')).toEqualTypeOf(); +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(); +expectTypeOf(people.objectsAt([1, 2, 3])).toEqualTypeOf>(); + +expectTypeOf(people.filterBy('isHappy')).toMatchTypeOf(); +expectTypeOf(people.filterBy('isHappy')).toMatchTypeOf>(); +expectTypeOf(people.rejectBy('isHappy')).toMatchTypeOf(); +expectTypeOf(people.rejectBy('isHappy')).toMatchTypeOf>(); +expectTypeOf(people.filter((person) => person.get('name') === 'Yehuda')).toMatchTypeOf(); +expectTypeOf(people.filter((person) => person.get('name') === 'Yehuda')).toMatchTypeOf< + MutableArray +>(); + +expectTypeOf(people.get('[]')).toEqualTypeOf(); +expectTypeOf(people.get('[]').get('firstObject')).toEqualTypeOf(); + +expectTypeOf(people.mapBy('isHappy')).toMatchTypeOf(); +expectTypeOf(people.mapBy('name.length')).toMatchTypeOf(); + +const last = people.get('lastObject'); +expectTypeOf(last).toEqualTypeOf(); +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(); + return item.charCodeAt(0); +}); +expectTypeOf(codes).toMatchTypeOf(); + +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'); diff --git a/type-tests/preview/@ember/component-test/built-ins.ts b/type-tests/preview/@ember/component-test/built-ins.ts new file mode 100644 index 00000000000..e920b295baa --- /dev/null +++ b/type-tests/preview/@ember/component-test/built-ins.ts @@ -0,0 +1,6 @@ +import { Input, Textarea } from '@ember/component'; +import { expectTypeOf } from 'expect-type'; + +// Minimal check that we are exporting both type and value +expectTypeOf(Input).toEqualTypeOf(); +expectTypeOf(Textarea).toEqualTypeOf