diff --git a/package.json b/package.json index db48313cc58a0..5e0e1f7ecd559 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "test:ui:runner": "node scripts/functional_test_runner", "test:server": "grunt test:server", "test:coverage": "grunt test:coverage", + "typespec": "typings-tester --config x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts", "checkLicenses": "grunt licenses --dev", "build": "node scripts/build --all-platforms", "start": "node --trace-warnings --trace-deprecation scripts/kibana --dev ", @@ -407,6 +408,7 @@ "tslint-microsoft-contrib": "^6.0.0", "tslint-plugin-prettier": "^2.0.0", "typescript": "^3.3.3333", + "typings-tester": "^0.3.2", "vinyl-fs": "^3.0.2", "xml2js": "^0.4.19", "xmlbuilder": "9.0.4", diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts new file mode 100644 index 0000000000000..274e0e64ba361 --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts @@ -0,0 +1,82 @@ +import { select } from '../../select'; +import { Json, Selector } from '../..'; + +/* + + Type checking isn't too useful if future commits can accidentally weaken the type constraints, because a + TypeScript linter will not complain - everything that passed before will continue to pass. The coder + will not have feedback that the original intent with the typing got compromised. To declare the intent + via passing and failing type checks, test cases are needed, some of which designed to expect a TS pass, + some of them to expect a TS complaint. It documents intent for peers too, as type specs are a tough read. + + Run compile-time type specification tests in the `kibana` root with: + + yarn typespec + + Test "cases" expecting to pass TS checks are not annotated, while ones we want TS to complain about + are prepended with the comment + + // typings:expect-error + + The test "suite" and "cases" are wrapped in IIFEs to prevent linters from complaining about the unused + binding. It can be structured internally as desired. + +*/ + +((): void => { + /** + * TYPE TEST SUITE + */ + + (function jsonTests(plain: Json): void { + // numbers are OK + plain = 1; + plain = NaN; + plain = Infinity; + plain = -Infinity; + plain = Math.pow(2, 6); + // other JSON primitive types are OK + plain = false; + plain = 'hello'; + plain = null; + // structures made of above and of structures are OK + plain = {}; + plain = []; + plain = { a: 1 }; + plain = [0, null, false, NaN, 3.14, 'one more']; + plain = { a: { b: 5, c: { d: [1, 'a', -Infinity, null], e: -1 }, f: 'b' }, g: false }; + + // typings:expect-error + plain = undefined; // it's undefined + // typings:expect-error + plain = a => a; // it's a function + // typings:expect-error + plain = [new Date()]; // it's a time + // typings:expect-error + plain = { a: Symbol('haha') }; // symbol isn't permitted either + // typings:expect-error + plain = window || void 0; + // typings:expect-error + plain = { a: { b: 5, c: { d: [1, 'a', undefined, null] } } }; // going deep into the structure + + return; // jsonTests + })(null); + + (function selectTests(selector: Selector): void { + selector = select((a: Json) => a); // one arg + selector = select((a: Json, b: Json): Json => `${a} and ${b}`); // more args + selector = select(() => 1); // zero arg + selector = select((...args: Json[]) => args); // variadic + + // typings:expect-error + selector = (a: Json) => a; // not a selector + // typings:expect-error + selector = select(() => {}); // should yield a JSON value, but it returns void + // typings:expect-error + selector = select((x: Json) => ({ a: x, b: undefined })); // should return a Json + + return; // selectTests + })(select((a: Json) => a)); + + return; // test suite +})(); diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/types.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/index.d.ts similarity index 53% rename from x-pack/plugins/canvas/public/lib/aeroelastic/types.ts rename to x-pack/plugins/canvas/public/lib/aeroelastic/index.d.ts index 2f58c9f12eeda..07c812b5bf648 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/types.ts +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/index.d.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +// linear algebra type f64 = number; // eventual AssemblyScript compatibility; doesn't hurt with vanilla TS either type f = f64; // shorthand @@ -15,16 +16,36 @@ export type transformMatrix2d = [f, f, f, f, f, f, f, f, f] & export type transformMatrix3d = [f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f] & ReadonlyArray & { __nominal: 'transformMatrix3d' }; +// plain, JSON-bijective value +export type Json = JsonPrimitive | JsonArray | JsonMap; +type JsonPrimitive = null | boolean | number | string; +interface JsonArray extends Array {} +interface JsonMap extends IMap {} +interface IMap { + [key: string]: T; +} + +// state object +export type State = JsonMap & WithActionId; +export type ActionId = number; +interface WithActionId { + primaryUpdate: { type: string; payload: { uid: ActionId; [propName: string]: Json } }; + [propName: string]: Json; // allow other arbitrary props +} + +// reselect-based data flow +export type PlainFun = (...args: Json[]) => Json; +export type Selector = (...fns: Resolve[]) => Resolve; +type Resolve = ((obj: State) => Json); + +// export interface Meta { silent: boolean; } -export type ActionId = number; export type TypeName = string; -export type NodeResult = any; -export type Payload = any; -export type NodeFunction = (...args: any[]) => any; -export type UpdaterFunction = (arg: NodeResult) => NodeResult; +export type Payload = JsonMap; +export type UpdaterFunction = (arg: State) => State; export type ChangeCallbackFunction = ( - { type, state }: { type: TypeName; state: NodeResult }, + { type, state }: { type: TypeName; state: State }, meta: Meta ) => void; diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/matrix.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/matrix.ts index 175bd27eef504..ae281d5a7b4c6 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/matrix.ts +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/matrix.ts @@ -24,7 +24,7 @@ * */ -import { transformMatrix3d, vector3d } from './types'; +import { transformMatrix3d, vector3d } from '.'; const NANMATRIX = [ NaN, diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/matrix2d.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/matrix2d.ts index bd23c5c3c6d68..8faae5420b91c 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/matrix2d.ts +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/matrix2d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { transformMatrix2d, vector2d } from './types'; +import { transformMatrix2d, vector2d } from '.'; export const ORIGIN = [0, 0, 1] as vector2d; diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/select.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/select.ts index cdb7587937b40..20af59809fa5d 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/select.ts +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/select.ts @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionId, NodeFunction, NodeResult } from './types'; +import { ActionId, Json, PlainFun, Selector, State } from '.'; -export const select = (fun: NodeFunction): NodeFunction => (...fns) => { +export const select = (fun: PlainFun): Selector => (...fns) => { let prevId: ActionId = NaN; - let cache: NodeResult = null; - const old = (object: NodeResult): boolean => - prevId === (prevId = object.primaryUpdate.payload.uid); - return (obj: NodeResult) => - old(obj) ? cache : (cache = fun(...fns.map(f => f(obj) as NodeResult))); + let cache: Json = null; + const old = (object: State): boolean => prevId === (prevId = object.primaryUpdate.payload.uid); + return (obj: State) => (old(obj) ? cache : (cache = fun(...fns.map(f => f(obj) as State)))); }; diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/store.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/store.ts index 162d591edfd61..43c3b72dcf28b 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/store.ts +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/store.ts @@ -8,16 +8,16 @@ import { ActionId, ChangeCallbackFunction, Meta, - NodeResult, Payload, + State, TypeName, UpdaterFunction, -} from './types'; +} from '.'; let counter = 0 as ActionId; export const createStore = ( - initialState: NodeResult, + initialState: State, updater: UpdaterFunction, onChangeCallback: ChangeCallbackFunction ) => { diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json b/x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json new file mode 100644 index 0000000000000..94081bdbd55a6 --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig", + "compilerOptions": { + "module": "commonjs", + "lib": ["esnext", "dom"], + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": false, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "baseUrl": ".", + "paths": { + "layout/*": ["aeroelastic/*"] + }, + "types": ["@kbn/x-pack/plugins/canvas/public/lib/aeroelastic"] + }, + "exclude": ["node_modules", "**/*.spec.ts", "node_modules/@types/mocha"] +} diff --git a/x-pack/plugins/canvas/public/lib/dom.ts b/x-pack/plugins/canvas/public/lib/dom.ts index 995015077cd4c..129fea72c9c4e 100644 --- a/x-pack/plugins/canvas/public/lib/dom.ts +++ b/x-pack/plugins/canvas/public/lib/dom.ts @@ -4,11 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { transformMatrix3d } from './aeroelastic/types'; +import { transformMatrix3d } from './aeroelastic'; // converts a transform matrix to a CSS string export const matrixToCSS = (transformMatrix: transformMatrix3d): string => transformMatrix ? 'matrix3d(' + transformMatrix.join(',') + ')' : 'translate3d(0,0,0)'; - -// converts to string, and adds `px` if non-zero -export const px = (value: number): string => (value === 0 ? '0' : value + 'px'); diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index c74d60e902326..dd3ab033718b8 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -8,7 +8,8 @@ "test_utils/**/*" ], "exclude": [ - "test/**/*" + "test/**/*", + "**/typespec_tests.ts" ], "compilerOptions": { "outDir": ".", diff --git a/yarn.lock b/yarn.lock index 8676c2a1d12ea..aeea5a7a48ea9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5759,7 +5759,7 @@ commander@0.6.1: resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" integrity sha1-+mihT2qUXVTbvlDYzbMyDp47GgY= -commander@2, commander@2.19.0, commander@^2.11.0, commander@^2.12.1, commander@^2.19.0, commander@^2.9.0: +commander@2, commander@2.19.0, commander@^2.11.0, commander@^2.12.1, commander@^2.12.2, commander@^2.19.0, commander@^2.9.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -21830,12 +21830,14 @@ typescript@^3.3.3333: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6" integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw== -ua-parser-js@^0.7.18: - version "0.7.18" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed" - integrity sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA== +typings-tester@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/typings-tester/-/typings-tester-0.3.2.tgz#04cc499d15ab1d8b2d14dd48415a13d01333bc5b" + integrity sha512-HjGoAM2UoGhmSKKy23TYEKkxlphdJFdix5VvqWFLzH1BJVnnwG38tpC6SXPgqhfFGfHY77RlN1K8ts0dbWBQ7A== + dependencies: + commander "^2.12.2" -ua-parser-js@^0.7.9: +ua-parser-js@^0.7.18, ua-parser-js@^0.7.9: version "0.7.19" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b" integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==