From 48cb35a6fec107fbe69824524a2e90c70ca46568 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 5 May 2022 10:28:04 -0600 Subject: [PATCH 1/5] Introduce ambient type definitions and type tests This allows users of the library to get TS-powered definitions for the library without installing `@types/ember-feature-flags`, and enables us to provide good SemVer guarantees for this library. Interesting/notable decisions made here: - We introduce a `FeaturesRegistry` which users can customize for improved type safety (specifically, around catching typos on their feature flag checks in e.g. `isEnabled` etc.). If they do not do anything with the registry, then any `string` key is allowed for the `setup`, `isEnabled`, `enable`, `disable`, and `get` functions as well as the `flags` property. - We do *not* provide types for making the template-side direct access to properties, e.g. `{{#if this.features.someFlag}}`, work. This is not possible to capture safely at this point in a way that will give the desired autocomplete in templates for [Glint][1] users while also *not* lying to users about what they can access directly *without* going through `get` when accessing the service in JS. (When, in the future, the library moves away from using `unknownProperty()` to using a native proxy, we *will* be able to support that.) For those scenarios, users will need to use `get` in the template, which will return `unknown`. That will be fine for type safety here, since there is no type narrowing. [1]: https://github.com/typed-ember/glint --- .eslintignore | 4 + package.json | 13 +- type-tests/index.ts | 45 +++++ type-tests/tsconfig.json | 73 ++++++++ type-tests/with-registry/index.ts | 90 ++++++++++ type-tests/with-registry/tsconfig.json | 4 + types/helpers/feature-flag.d.ts | 11 ++ types/services/features.d.ts | 83 +++++++++ types/test-support/helpers/with-feature.d.ts | 15 ++ types/test-support/index.d.ts | 19 ++ yarn.lock | 175 +++++++++++++++++++ 11 files changed, 531 insertions(+), 1 deletion(-) create mode 100644 type-tests/index.ts create mode 100644 type-tests/tsconfig.json create mode 100644 type-tests/with-registry/index.ts create mode 100644 type-tests/with-registry/tsconfig.json create mode 100644 types/helpers/feature-flag.d.ts create mode 100644 types/services/features.d.ts create mode 100644 types/test-support/helpers/with-feature.d.ts create mode 100644 types/test-support/index.d.ts diff --git a/.eslintignore b/.eslintignore index 72df373..e93e39b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -18,3 +18,7 @@ /.node_modules.ember-try/ /bower.json.ember-try /package.json.ember-try + +# don't bother linting types and type tests +types +type-tests diff --git a/package.json b/package.json index e91120c..56e6b67 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ }, "devDependencies": { "@ember/optional-features": "^1.0.0", + "@types/ember__component": "^4.0.8", + "@types/ember__service": "^4.0.0", "babel-eslint": "^10.0.3", "broccoli-asset-rev": "^3.0.0", "ember-cli": "~3.13.1", @@ -38,9 +40,11 @@ "ember-try": "^1.2.1", "eslint-plugin-ember": "^7.1.0", "eslint-plugin-node": "^10.0.0", + "expect-type": "^0.13.0", "lerna-changelog": "^0.8.2", "loader.js": "^4.7.0", - "qunit-dom": "^0.9.0" + "qunit-dom": "^0.9.0", + "typescript": "^4.6.4" }, "keywords": [ "ember-addon", @@ -76,5 +80,12 @@ "documentation": ":memo: Documentation", "internal": ":house: Internal" } + }, + "typesVersions": { + "*": { + "*": [ + "types/*" + ] + } } } diff --git a/type-tests/index.ts b/type-tests/index.ts new file mode 100644 index 0000000..9cdcad6 --- /dev/null +++ b/type-tests/index.ts @@ -0,0 +1,45 @@ +import { expectTypeOf } from "expect-type"; +import Helper from "@ember/component/helper"; +import type Features from "ember-feature-flags/services/features"; +import { + enableFeature, + disableFeature, +} from "ember-feature-flags/test-support"; + +// side-effect import for `withFeature` +import "ember-feature-flags/test-support/helpers/with-feature"; + +expectTypeOf(enableFeature).toEqualTypeOf<(name: string) => void>(); +expectTypeOf(enableFeature("hello")).toEqualTypeOf(); +expectTypeOf(disableFeature).toEqualTypeOf<(name: string) => void>(); +expectTypeOf(disableFeature("hello")).toEqualTypeOf(); + +// globally available because of the side effect import +expectTypeOf(withFeature).toEqualTypeOf<(name: string) => void>(); +expectTypeOf(withFeature("hello")).toEqualTypeOf(); + +// The default for all the methods, if you don't add to the registry, is to +// simply use a string key lookup. See `./with-registry/index.ts` for testing +// when there *is* a registry. +declare let features: Features; +expectTypeOf(features.setup).toEqualTypeOf< + (config: Record) => void +>(); + +expectTypeOf(features.isEnabled).toEqualTypeOf<(key: string) => boolean>(); +expectTypeOf(features.get).toEqualTypeOf<(key: string) => boolean>(); +expectTypeOf(features.enable).toEqualTypeOf< + (key: string, value: boolean) => void +>(); +expectTypeOf(features.disable).toEqualTypeOf< + (key: string, value: boolean) => void +>(); +expectTypeOf(features.flags).toEqualTypeOf>(); + +expectTypeOf().toEqualTypeOf< + Helper<{ + Args: { + Positional: [string]; + }; + }> +>(); diff --git a/type-tests/tsconfig.json b/type-tests/tsconfig.json new file mode 100644 index 0000000..67d726a --- /dev/null +++ b/type-tests/tsconfig.json @@ -0,0 +1,73 @@ +// Pulled from ember-cli-typescript's blueprint +{ + "compilerOptions": { + "target": "ES2021", + "module": "ES2020", + "moduleResolution": "node", + + // Trying to check Ember apps and addons with `allowJs: true` is a recipe + // for many unresolveable type errors, because with *considerable* extra + // configuration it ends up including many files which are *not* valid and + // cannot be: they *appear* to be resolve-able to TS, but are in fact not in + // valid Node-resolveable locations and may not have TS-ready types. This + // will likely improve over time + "allowJs": false, + + // --- TS for SemVer Types compatibility + // Strictness settings -- you should *not* change these: Ember code is not + // guaranteed to type check with these set to looser values. + "strict": true, + "noUncheckedIndexedAccess": true, + + // Interop: these are viral and will require anyone downstream of your + // package to *also* set them to true. If you *must* enable them to consume + // an upstream package, you should document that for downstream consumers to + // be aware of. + // + // These *are* safe for apps to enable, since they do not *have* downstream + // consumers; but leaving them off is still preferred when possible, since + // it makes it easier to switch between apps and addons and have the same + // rules for what can be imported and how. + "allowSyntheticDefaultImports": false, + "esModuleInterop": false, + + // --- Lint-style rules + + // TypeScript also supplies some lint-style checks; nearly all of them are + // better handled by ESLint with the `@typescript-eslint`. This one is more + // like a safety check, though, so we leave it on. + "noPropertyAccessFromIndexSignature": true, + + // --- Compilation/integration settings + // Setting `noEmitOnError` here allows ember-cli-typescript to catch errors + // and inject them into Ember CLI's build error reporting, which provides + // nice feedback for when + "noEmitOnError": true, + + // We use Babel for emitting runtime code, because it's very important that + // we always and only use the same transpiler for non-stable features, in + // particular decorators. If you were to change this to `true`, it could + // lead to accidentally generating code with `tsc` instead of Babel, and + // could thereby result in broken code at runtime. + "noEmit": true, + + // Ember makes heavy use of decorators; TS does not support them at all + // without this flag. + "experimentalDecorators": true, + + // Support generation of source maps. Note: you must *also* enable source + // maps in your `ember-cli-babel` config and/or `babel.config.js`. + "declaration": true, + "declarationMap": true, + "inlineSourceMap": true, + "inlineSources": true, + + // --- custom paths shenanigans since we don't have `exports` yet --- + "baseUrl": "../types", + "paths": { + "ember-feature-flags": ["."], + "ember-feature-flags/*": ["./*"] + } + }, + "include": ["index.ts", "../types/*"] +} diff --git a/type-tests/with-registry/index.ts b/type-tests/with-registry/index.ts new file mode 100644 index 0000000..b43b054 --- /dev/null +++ b/type-tests/with-registry/index.ts @@ -0,0 +1,90 @@ +import { expectTypeOf } from "expect-type"; +import Features from "ember-feature-flags/services/features"; +import Helper from "@ember/component/helper"; +import { + enableFeature, + disableFeature, +} from "ember-feature-flags/test-support"; +import FeatureFlag from "ember-feature-flags/helpers/feature-flag"; + +// @ts-expect-error -- we have *not* done the side-effect import in this test, +// so there is no `withFeature` in the global scope! +withFeature("whatever", true); + +declare module "ember-feature-flags/services/features" { + export interface FeaturesRegistry { + simple: boolean; + "with-dash": boolean; + } +} + +expectTypeOf(enableFeature).parameters.toEqualTypeOf< + ["simple" | "withDash" | "with-dash"] +>(); +expectTypeOf(disableFeature).parameters.toEqualTypeOf< + ["simple" | "withDash" | "with-dash"] +>(); + +declare let features: Features; + +// @ts-expect-error -- no calling it without args at all +features.setup(); +// no keys is allowed (though: why would you do that?) +expectTypeOf(features.setup({})).toEqualTypeOf(); +// any key actually set up is allowed +expectTypeOf(features.setup({ "with-dash": true })).toEqualTypeOf(); +expectTypeOf(features.setup({ simple: false })).toEqualTypeOf(); +expectTypeOf( + features.setup({ + "with-dash": false, + simple: true, + }) +).toEqualTypeOf(); +// But random keys are *not* allowed +// @ts-expect-error +features.setup({ otherShenanigans: false }); + +// Both kebab and camel-case are supported +expectTypeOf(features.isEnabled).parameters.toEqualTypeOf< + ["withDash" | "with-dash" | "simple"] +>(); +// But random keys are disallowed if using the registry +// @ts-expect-error +features.isEnabled("whatever"); + +// Both kebab and camel-case are supported +expectTypeOf(features.get).parameters.toEqualTypeOf< + ["withDash" | "with-dash" | "simple"] +>(); +// But random keys are disallowed if using the registry +// @ts-expect-error +features.get("whatever"); + +expectTypeOf(features.enable) + .parameter(0) + .toEqualTypeOf<"withDash" | "with-dash" | "simple">(); +expectTypeOf(features.enable).parameter(1).toEqualTypeOf(); +// But random keys are disallowed if using the registry +// @ts-expect-error +features.enable("whatever", true); + +expectTypeOf(features.disable) + .parameter(0) + .toEqualTypeOf<"withDash" | "with-dash" | "simple">(); +expectTypeOf(features.disable).parameter(1).toEqualTypeOf(); +// But random keys are disallowed if using the registry +// @ts-expect-error +features.disable("whatever", true); + +expectTypeOf(features.flags).toEqualTypeOf< + Array<"simple" | "withDash" | "with-dash"> +>(); + +expectTypeOf().toEqualTypeOf< + Helper<{ + Args: { + Positional: ["simple" | "withDash" | "with-dash"]; + }; + }> +>(); +declare let ff: FeatureFlag; diff --git a/type-tests/with-registry/tsconfig.json b/type-tests/with-registry/tsconfig.json new file mode 100644 index 0000000..fd8251e --- /dev/null +++ b/type-tests/with-registry/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "include": ["./index.ts", "../../types/*"] +} diff --git a/types/helpers/feature-flag.d.ts b/types/helpers/feature-flag.d.ts new file mode 100644 index 0000000..df5312c --- /dev/null +++ b/types/helpers/feature-flag.d.ts @@ -0,0 +1,11 @@ +import Helper from "@ember/component/helper"; +import { Keys } from "ember-feature-flags/services/features"; + +interface FeatureFlagSignature { + Args: { + Positional: [Keys]; + }; +} + +/** A helper named `feature-flag` to check features in templates */ +export default class FeatureFlag extends Helper {} diff --git a/types/services/features.d.ts b/types/services/features.d.ts new file mode 100644 index 0000000..90144d5 --- /dev/null +++ b/types/services/features.d.ts @@ -0,0 +1,83 @@ +import Service from "@ember/service"; + +/** + * If there are keys in the registry, require them to be the keys used in the + * config (though they can be left off and will default to false). + */ +type AllowedConfig = keyof FeaturesRegistry extends never + ? Record + : Partial>; + +/** + * Get a version of an object whose keys are camel-cased. + */ +type Camelized = { + [Key in CamelCased]: O[K]; +}; + +type Keys = keyof FeaturesRegistry extends never + ? string + : keyof FeaturesRegistry | keyof Camelized; + +type CamelCased = S extends string + ? S extends `${infer A}-${infer B}${infer C}` + ? `${Uncapitalize}${Capitalize}${CamelCased}` + : `${Uncapitalize}` + : never; + +/** + * A service named `features` available for injection into your routes, + * controllers, components, etc. + */ +// we are using an interface instead of a class since this is not intended to +// be subclassed and needs to change the behavior of `get` +export default interface Features extends Omit { + /** + * Enable or disable features in bulk. + * + * NOTE: `setup` methods reset previously setup flags and their state. + */ + setup(config: AllowedConfig): void; + + /** Enable a feature at runtime. */ + enable(key: K, value: boolean): void; + + /** Disable a feature at runtime. */ + disable(key: K, value: boolean): void; + + /** Check if a feature is enabled. */ + isEnabled(key: K): boolean; + + /** + * Features are also available as camel-cased properties of `features`, but + * behind a proxy, so you can only access them safely using `get`. + * + * ```ts + * import Controller from '@ember/controller'; + * import { service } from '@ember/service'; + * import type Features from 'ember-feature-flags/services/features'; + * + * export default class BillingPlansController extends Controller { + * @service declare features: Features; + * + * get plans() { + * if (this.features.get('newBillingPlans')) { + * // Return new plans + * } else { + * // Return old plans + * } + * } + * } + * ``` + */ + get(key: K): boolean; + + flags: Keys; +} + +/** + * Registry of settings. Provide the kebab-case (`"foo-bar"`) setting name here + * and both kebab– and camel-case will be available for type-powered + * autocomplete etc. + */ +export interface FeaturesRegistry {} diff --git a/types/test-support/helpers/with-feature.d.ts b/types/test-support/helpers/with-feature.d.ts new file mode 100644 index 0000000..8f54f5d --- /dev/null +++ b/types/test-support/helpers/with-feature.d.ts @@ -0,0 +1,15 @@ +declare global { + /** + * "Old"-style acceptance tests can utilize `withFeature` test helper to turn on + * a feature for the test. To use, import into your `test-helper.js`: `import + * 'ember-feature-flags/test-support/helpers/with-feature'` and add to your test + * `.jshintrc`, it will now be available in all of your tests. + * + * + */ + export function withFeature(name: string): void; +} + +// Shut off "exporting" mode so that the global enhancement works per the API +// as designed. +export {}; diff --git a/types/test-support/index.d.ts b/types/test-support/index.d.ts new file mode 100644 index 0000000..be04fd9 --- /dev/null +++ b/types/test-support/index.d.ts @@ -0,0 +1,19 @@ +import { Keys } from "ember-feature-flags/services/features"; + +/** + * Turns on or off a feature for the test in which it is called. Requires + * ember-cli-qunit >= 4.1.0 and the newer style of tests that use `setupTest`, + * `setupRenderingTest`, `setupApplicationTest`. + * + * @param name The feature to enable + */ +export function enableFeature(name: Keys): void; + +/** + * Turns on or off a feature for the test in which it is called. Requires + * ember-cli-qunit >= 4.1.0 and the newer style of tests that use `setupTest`, + * `setupRenderingTest`, `setupApplicationTest`. + * + * @param name The feature to disable + */ +export function disableFeature(name: Keys): void; diff --git a/yarn.lock b/yarn.lock index 41b0825..371b3a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -950,6 +950,161 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@types/ember-resolver@*": + version "5.0.11" + resolved "https://registry.yarnpkg.com/@types/ember-resolver/-/ember-resolver-5.0.11.tgz#db931fb5c2d6bda4e29adea132fb48c7ed17aa62" + integrity sha512-2BL9d8kBdNUO9Je6KBF7Q34BSwbQG6vzCzTeSopt8FAmLDfaDU9xDDdyZobpfy9GR36mCSeG9b9wr4bgYh/MYw== + dependencies: + "@types/ember__object" "*" + +"@types/ember@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember/-/ember-4.0.0.tgz#0c29294fa0e5aa07ba6090f60243707dde8fc411" + integrity sha512-IR4o8OkFgoiRKVLRI8URvyNhEBSkjO5DXp2900/TptxOl0Retu8/tKtFaRTwkqteg2a0/6zXAA1rpFb3BbxNpA== + dependencies: + "@types/ember__application" "*" + "@types/ember__array" "*" + "@types/ember__component" "*" + "@types/ember__controller" "*" + "@types/ember__debug" "*" + "@types/ember__engine" "*" + "@types/ember__error" "*" + "@types/ember__object" "*" + "@types/ember__polyfills" "*" + "@types/ember__routing" "*" + "@types/ember__runloop" "*" + "@types/ember__service" "*" + "@types/ember__string" "*" + "@types/ember__template" "*" + "@types/ember__test" "*" + "@types/ember__utils" "*" + "@types/htmlbars-inline-precompile" "*" + "@types/rsvp" "*" + +"@types/ember__application@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__application/-/ember__application-4.0.0.tgz#a4d2fead37845550dad83bb1fd8afd52052563a7" + integrity sha512-1Atwevfyu1/vjiezPPdP4s96BxWGelEQlCJRU5ZQV9WlzVuMTuCDPumZ1lQdS4/EYycFZeod030FjE3CT9mZFA== + dependencies: + "@types/ember" "*" + "@types/ember-resolver" "*" + "@types/ember__application" "*" + "@types/ember__engine" "*" + "@types/ember__object" "*" + "@types/ember__routing" "*" + +"@types/ember__array@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/ember__array/-/ember__array-4.0.1.tgz#b62126ed080b29351a5633bd28e595a5cfee27ce" + integrity sha512-fwrYcYmFbsEnu77Xn9z3WSAp6tqpwn8Wksx8RzGg5pib6VmFD/dkT5jefwoKtlcImsxUNEoP1VgWKrdrpGaQcg== + dependencies: + "@types/ember" "*" + "@types/ember__array" "*" + "@types/ember__object" "*" + +"@types/ember__component@*", "@types/ember__component@^4.0.8": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/ember__component/-/ember__component-4.0.8.tgz#09a5f954f734fcbe6c988a173f4de4fa09084470" + integrity sha512-YVGn/kpWtpZAu6I2XtS9fsZV+78/sON5NyKzK5EOUyMiCwwpbUr5XL8dTSdkHehYrsfzJikcYvqpmwbNZSJxGQ== + dependencies: + "@types/ember" "*" + "@types/ember__component" "*" + "@types/ember__object" "*" + +"@types/ember__controller@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__controller/-/ember__controller-4.0.0.tgz#f8891840ebb84cb54eba82385e3b2dbe805d692a" + integrity sha512-rxJt8McWaaIZFsu2z+IB7TvgSjglAPb07Pj0F7OGvZQ3j9NV7kreqqics/cHQIEBG3GgVAewBE+xI5D6PNq/vg== + dependencies: + "@types/ember__object" "*" + +"@types/ember__debug@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/ember__debug/-/ember__debug-4.0.1.tgz#1e4a8a1045484295dddc7bd4356d0b3014b0d509" + integrity sha512-qrKk6Ujh6oev7TSB0eB7AEmQWKCt5t84k/K3hDvJXUiLU3YueN0kyt7aPoIAkVjC111A9FqDugl9n60+N5yeEw== + dependencies: + "@types/ember-resolver" "*" + "@types/ember__debug" "*" + "@types/ember__object" "*" + +"@types/ember__engine@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__engine/-/ember__engine-4.0.0.tgz#e39c06d98c7a085912508e8257c48a70196c1a87" + integrity sha512-AfJHIWaBeZ+TZWJbSoUz7LK+z8uNPjMqmucz8C5u+EV2NDiaq02oGPTB4SeKInLNBMga8c5xvz0gVefZJnTBnQ== + dependencies: + "@types/ember-resolver" "*" + "@types/ember__engine" "*" + "@types/ember__object" "*" + +"@types/ember__error@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__error/-/ember__error-4.0.0.tgz#c73037e65c1c3d7060b97f98135ba73c712972b1" + integrity sha512-1WVMR65/QTqPzMWafK2vKEwGafILxRxItbWJng6eEJyKDHRvvHFCl3XzJ4dQjdFcfOlozsn0mmEYCpjKoyzMqA== + +"@types/ember__object@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/ember__object/-/ember__object-4.0.2.tgz#070f1a36961f7df777e1fceed2551a648db0fd23" + integrity sha512-m3xjqjs7bGVT0+QXlgIoDMsp/oqePobnf4IiVoFdXLBpGCICiOAEi7HuUtCLi57WTvx0lYsS9hE1vgGyZn9qnw== + dependencies: + "@types/ember" "*" + "@types/ember__object" "*" + "@types/rsvp" "*" + +"@types/ember__polyfills@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__polyfills/-/ember__polyfills-4.0.0.tgz#d83ae94ff2890ad47798315426d9916f39ff4ae6" + integrity sha512-Yk85J18y1Ys6agoIBLdJWu6ZkWe68oaC9JPyW7BhOINVNKm89PXrR/yxdOJ1/vN1Hj7ZZQKq+4X6fz3sxebavA== + +"@types/ember__routing@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/ember__routing/-/ember__routing-4.0.5.tgz#f4e5c3d0e966859594d0303aa863053538802928" + integrity sha512-9f5wWoDn1snfjwtgX4uCvuvJSbG71vUzQcD1LDH6k5oWdhjk3iUsIalN4xD9jWzWF9JkasrTuOv+0wyNR4ahFw== + dependencies: + "@types/ember" "*" + "@types/ember__controller" "*" + "@types/ember__object" "*" + "@types/ember__routing" "*" + "@types/ember__service" "*" + +"@types/ember__runloop@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/ember__runloop/-/ember__runloop-4.0.1.tgz#7f6e45af7dbf1158655ef3ad852852b0bf87065f" + integrity sha512-3HrsavVrdgxUkYptQUv/e9RwJG02cV9WbnJxKSvwl9ZYpeX4JbuDVucjTWk5BAvJUVtbiQLPGzLEHZ6daoCbbg== + dependencies: + "@types/ember" "*" + "@types/ember__runloop" "*" + +"@types/ember__service@*", "@types/ember__service@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__service/-/ember__service-4.0.0.tgz#ae6164e3b5d927fe17513b49867b52dc0222490d" + integrity sha512-FbN2y6tRb6NIV+kmzQcxRAoB17vH7qHCfzcKlxsmt2EI7fboLTcdeKpZKPBEromRXg83fx67QX1b95WcwSGtaw== + dependencies: + "@types/ember__object" "*" + +"@types/ember__string@*": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@types/ember__string/-/ember__string-3.0.9.tgz#669188ccea5a61777a36bf88a05ba6875dc9b7d7" + integrity sha512-v9QwhhfTTgJH6PCviWlz3JgcraYdSWQoTg2XN5Z7bPgXMJYXczxB/N22L9FnuFgDYdN87yXdTJv6E9rw2YGEhw== + +"@types/ember__template@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__template/-/ember__template-4.0.0.tgz#3423b6ddc3a6cf0b13a1e0fd5f1a84eec664a095" + integrity sha512-51bAEQecMKpDYRXMmVVfU7excrtxDJixRU7huUsAm4acBCqL2+TmMgTqZEkOQSNy6qnKUc2ktSzX28a9//C6pA== + +"@types/ember__test@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__test/-/ember__test-4.0.0.tgz#1a7dcbe24fedfc34fa60547b03f130a14397c4b6" + integrity sha512-vI/qhZkexJLN25lp1UAfjJv4R6pPtrQlAmPDXkKd8PNjwRk3KANFVRzdghN7HWhXgQ+s91PbvxEnZ3eZiRPdcQ== + dependencies: + "@types/ember__application" "*" + +"@types/ember__utils@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__utils/-/ember__utils-4.0.0.tgz#a7e9e11334b5e203324e6155ff74c2b33ec21567" + integrity sha512-gwSFUm+6t6StkQxSllbn9lqRms/dXMCQDieRfaTGN8IRatnKjJoEPME3A0T6O9afsU6viBQyqlPyFxsOWknYkg== + dependencies: + "@types/ember" "*" + "@types/events@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" @@ -964,6 +1119,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/htmlbars-inline-precompile@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/htmlbars-inline-precompile/-/htmlbars-inline-precompile-3.0.0.tgz#4d3f19eeb2af9f4605620e13a566dae3952a4f68" + integrity sha512-n1YwM/Q937KmS9W4Ytran71nzhhcT2FDQI00eRGBNUyeErLZspBdDBewEe1F8tcRlUdsCVo2AZBLJsRjEceTRg== + "@types/minimatch@*", "@types/minimatch@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -974,6 +1134,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.2.tgz#3452a24edf9fea138b48fad4a0a028a683da1e40" integrity sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA== +"@types/rsvp@*": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/rsvp/-/rsvp-4.0.4.tgz#55e93e7054027f1ad4b4ebc1e60e59eb091e2d32" + integrity sha512-J3Ol++HCC7/hwZhanDvggFYU/GtxHxE/e7cGRWxR04BF7Tt3TqJZ84BkzQgDxmX0uu8IagiyfmfoUlBACh2Ilg== + "@types/symlink-or-copy@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/symlink-or-copy/-/symlink-or-copy-1.2.0.tgz#4151a81b4052c80bc2becbae09f3a9ec010a9c7a" @@ -4321,6 +4486,11 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" +expect-type@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-0.13.0.tgz#916646a7a73f3ee77039a634ee9035efe1876eb2" + integrity sha512-CclevazQfrqo8EvbLPmP7osnb1SZXkw47XPPvUUpeMz4HuGzDltE7CaIt3RLyT9UQrwVK/LDn+KVcC0hcgjgDg== + express@^4.10.7, express@^4.16.4: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -8580,6 +8750,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@^4.6.4: + version "4.6.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" + integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" From f993b9ef8a05b2e0df94bd05de0254a03689851c Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 5 May 2022 10:38:14 -0600 Subject: [PATCH 2/5] Add type tests to GitHub Actions config - This tests against the same minimum version as the existing type definitions on DefinitelyTyped, so users can upgrade to at least the version supported there, and then switch over to using this. Note that type tests do *not* use Ember Try, and therefore do not drift the lockfile. This is intentional: the emitted types here are wholly unaffected by the rest of the dependencies other than those in `@types`, and for compatibility with TS we only care about the types. This makes that part of CI run *much* faster. - The type tests allow failures against the `typescript@next` version, just as the runtime tests do for Ember Canary releases. --- .github/workflows/CI.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index fc4695d..a4a7be4 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -76,3 +76,26 @@ jobs: run: yarn install --frozen-lockfile - name: Run Tests run: node_modules/.bin/ember try:one ${{ matrix.scenario }} --skip-cleanup + + types: + name: type tests + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + ts-version: + - 4.4 + - 4.5 + - 4.6 + - next + + steps: + - uses: actions/checkout@v3 + - run: yarn install --frozen-lockfile --non-interactive + - run: yarn add -D typescript@${{ matrix.ts-version }} + - run: ./node_modules/.bin/tsc --project type-tests + - run: ./node_modules/.bin/tsc --project type-tests/with-registry + + continue-on-error: ${{ matrix.ts-version == 'next' }} From 6b016fa7de214ebe545f700afd70244d29264521 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 5 May 2022 10:53:52 -0600 Subject: [PATCH 3/5] Add TS info to README --- README.md | 62 ++++++++++++++++++++++++++++++++++++ type-tests/index.ts | 1 + types/services/features.d.ts | 2 +- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cd7e2a..5e6b7f0 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,68 @@ moduleForComponent('my-component', 'Integration | Component | my component', { Note: for Ember before 2.3.0, you'll need to use [ember-getowner-polyfill](https://github.com/rwjblue/ember-getowner-polyfill). +### TypeScript + +The library ships with full support for TypeScript usage with the service, helper, and test helpers. The API described above works as expected, with one additional nicety and one caveat. + +**Nicety:** the library provides you the ability to define statically your known feature flags by using a *registry* (as you may be familiar with from the registries for Ember's services, Ember Data models, etc.). If you define your keys (in kebab case!) in a registry like this: + +```ts +// types/index.d.ts, with other types defined for your app + + +declare module 'ember-feature-flags/services/features' { + export interface FeaturesRegistry { + 'feature-a': boolean; + 'feature-b': boolean; + } +} +``` + +Then in your app code, you will get type checking: TS will require you to use one of those keys (or a camel case variant of it), and reject unknown keys. + +```ts +import Component from '@glimmer/component'; +import { service } from '@ember/service'; +import type Features from 'ember-feature-flags/services/features'; + +export default class Example extends Component { + @service declare features: Features; + + get shouldDoSomething() { + return this.features.isEnabled('feature-a'); // ✅ + } + + get whoops() { + return this.features.isEnabled('not-a-real-feature'); // ❌ + } +} +``` + +This applies to all the values. If you do *not* add any keys to the `FeatureFlags` interface, the types will fall back to simply allowing any string and returning a boolean value. + +**Caveat:** The runtime uses Ember's `unknownProperty` proxy handling to allow direct access on the service itself with the `get` helper. This allows you to access the features directly in a template: + +```hbs +{{#if this.features.featureA}} + {{! ... }} +{{/if}} +``` + +For [Glint](https://github.com/typed-ember/glint), this is impossible to support in a way which would not *also* suggest that you could write `this.features.featureA` in your TypeScript code. Doing that will always return `undefined` until we are able to update the library to use native proxies instead of Ember's `unknownProperty()` method. The `feature-flag` helper does *not* have this restriction, so you should prefer that instead. If you still want to use the service directly instead of using the helper, you can use `get`: + +```hbs +{{#if (get this.features 'featureA')}} + {{! ... }} +{{/if}} +``` + +This will *not* provide autocomplete or type safety, but will work. + +#### Stability + +This library provides type definitions and follows the current draft of the [Semantic Versioning for TypeScript Types](https://www.semver-ts.org) specification. The public API is all published types. It currently supports TypeScript 4.4, 4.5, and 4.6. + ### Development #### Installation diff --git a/type-tests/index.ts b/type-tests/index.ts index 9cdcad6..fb852e2 100644 --- a/type-tests/index.ts +++ b/type-tests/index.ts @@ -5,6 +5,7 @@ import { enableFeature, disableFeature, } from "ember-feature-flags/test-support"; +import FeatureFlag from "ember-feature-flags/helpers/feature-flag"; // side-effect import for `withFeature` import "ember-feature-flags/test-support/helpers/with-feature"; diff --git a/types/services/features.d.ts b/types/services/features.d.ts index 90144d5..4692d0c 100644 --- a/types/services/features.d.ts +++ b/types/services/features.d.ts @@ -72,7 +72,7 @@ export default interface Features extends Omit { */ get(key: K): boolean; - flags: Keys; + flags: Keys[]; } /** From b55f499b6df36986b27d7582cd8810f28bfc8d73 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Fri, 6 May 2022 10:06:16 -0600 Subject: [PATCH 4/5] Exclude type-tests from published infra --- .npmignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.npmignore b/.npmignore index 773de27..cb73824 100644 --- a/.npmignore +++ b/.npmignore @@ -33,3 +33,6 @@ /.node_modules.ember-try/ /bower.json.ember-try /package.json.ember-try + +# non-published TypeScript infrastructure +type-tests From b06344a7c13cc30ad16264f0322407fbd8d52ca1 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Tue, 17 May 2022 12:04:12 -0600 Subject: [PATCH 5/5] Remove nonsense `value` arg from types for enable/disable Co-authored-by: Eric Kelly <602204+HeroicEric@users.noreply.github.com> --- types/services/features.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/services/features.d.ts b/types/services/features.d.ts index 4692d0c..b7309c8 100644 --- a/types/services/features.d.ts +++ b/types/services/features.d.ts @@ -40,10 +40,10 @@ export default interface Features extends Omit { setup(config: AllowedConfig): void; /** Enable a feature at runtime. */ - enable(key: K, value: boolean): void; + enable(key: K): void; /** Disable a feature at runtime. */ - disable(key: K, value: boolean): void; + disable(key: K): void; /** Check if a feature is enabled. */ isEnabled(key: K): boolean;