From d827d216ef69cb6207f6ef6f23b7ddce4ae76b6b Mon Sep 17 00:00:00 2001 From: "Xiao(Bill) Li" Date: Mon, 13 Jun 2022 10:41:20 -0400 Subject: [PATCH] feat: set up table package and add createTableItems method (#124) - config typescript & rollup - config jest - config eslint - add unit tests for createTableItems method - updated .eslintrc.js at project root level fix: update tsconfig path in eslintrc for Table package. - remove unused variables. fix: add tests for dataFilters.ts - update descriptions for tests in createTableItems.spec.ts - add one test case for getting data point from aggregated datastream fix: remove unnecessary describe block fix: use object to replace CellItem class - add createCellItem function - add unit tests for createCellItem - rename SC_DataStream to SynchroChartsDataStream for better consistency fix: remove unused import - update createCellItem tests. --- .eslintrc.js | 4 + packages/table/.eslintrc.js | 29 +++ packages/table/.gitignore | 8 + packages/table/global.d.ts | 2 + packages/table/jest.config.js | 22 ++ packages/table/package.json | 62 +++++ packages/table/rollup.config.js | 20 ++ packages/table/src/index.ts | 2 + .../table/src/utils/createCellItem.spec.ts | 25 ++ packages/table/src/utils/createCellItem.ts | 34 +++ .../table/src/utils/createTableItems.spec.ts | 215 ++++++++++++++++++ packages/table/src/utils/createTableItems.ts | 62 +++++ packages/table/src/utils/dataFilters.spec.ts | 45 ++++ packages/table/src/utils/dataFilters.ts | 28 +++ packages/table/src/utils/getDataPoints.ts | 17 ++ packages/table/src/utils/index.ts | 2 + packages/table/src/utils/types.ts | 31 +++ packages/table/tsconfig.json | 29 +++ 18 files changed, 637 insertions(+) create mode 100644 packages/table/.eslintrc.js create mode 100644 packages/table/.gitignore create mode 100644 packages/table/global.d.ts create mode 100644 packages/table/jest.config.js create mode 100644 packages/table/package.json create mode 100644 packages/table/rollup.config.js create mode 100644 packages/table/src/index.ts create mode 100644 packages/table/src/utils/createCellItem.spec.ts create mode 100644 packages/table/src/utils/createCellItem.ts create mode 100644 packages/table/src/utils/createTableItems.spec.ts create mode 100644 packages/table/src/utils/createTableItems.ts create mode 100644 packages/table/src/utils/dataFilters.spec.ts create mode 100644 packages/table/src/utils/dataFilters.ts create mode 100644 packages/table/src/utils/getDataPoints.ts create mode 100644 packages/table/src/utils/index.ts create mode 100644 packages/table/src/utils/types.ts create mode 100644 packages/table/tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js index e25b4330f..00dbb4861 100755 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,5 +44,9 @@ module.exports = { files: ['./packages/related-table/**/*'], extends: './packages/related-table/.eslintrc.js', }, + { + files: ['./packages/table/**/*'], + extends: './packages/table/.eslintrc.js', + }, ], }; diff --git a/packages/table/.eslintrc.js b/packages/table/.eslintrc.js new file mode 100644 index 000000000..acb5fdda6 --- /dev/null +++ b/packages/table/.eslintrc.js @@ -0,0 +1,29 @@ +const fs = require('fs'); +const path = require('path'); + +const tsConfig = fs.existsSync('tsconfig.json') + ? path.resolve('tsconfig.json') + : path.resolve('./packages/table/tsconfig.json'); + +module.exports = { + ignorePatterns: ['.storybook', 'stories', 'config', 'jest.config.ts', '**/*.js'], + plugins: [ + 'eslint-plugin-import', + 'eslint-plugin-jsx-a11y', + 'eslint-plugin-prettier', + 'eslint-plugin-react', + 'eslint-plugin-react-hooks', + ], + parserOptions: { + project: tsConfig, + }, + rules: { + 'import/prefer-default-export': 'off', + 'react/jsx-props-no-spreading': 'off', + '@typescript-eslint/comma-dangle': 'off', + 'no-param-reassign': 'warn', + 'react/no-array-index-key': 'warn', + 'no-plusplus': 'warn', + }, + extends: ['airbnb-typescript', 'airbnb/hooks', 'plugin:prettier/recommended'], +}; diff --git a/packages/table/.gitignore b/packages/table/.gitignore new file mode 100644 index 000000000..c23bc5093 --- /dev/null +++ b/packages/table/.gitignore @@ -0,0 +1,8 @@ +*.log +.DS_Store +node_modules +.cache +dist +build +storybook-static +coverage diff --git a/packages/table/global.d.ts b/packages/table/global.d.ts new file mode 100644 index 000000000..95a14658c --- /dev/null +++ b/packages/table/global.d.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import 'jest-extended'; diff --git a/packages/table/jest.config.js b/packages/table/jest.config.js new file mode 100644 index 000000000..b2b57d11e --- /dev/null +++ b/packages/table/jest.config.js @@ -0,0 +1,22 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + setupFilesAfterEnv: ['jest-extended/all'], + collectCoverageFrom: ['src/**/*.{ts,tsx}'], + testPathIgnorePatterns: ['/dist'], + coverageReporters: ['text-summary', 'cobertura', 'html', 'json', 'json-summary'], + moduleNameMapper: { + '\\.(css|scss|svg)$': 'identity-obj-proxy', + 'd3-array': '/node_modules/d3-array/dist/d3-array.min.js', + }, + + coverageThreshold: { + global: { + statements: 80, + branches: 80, + functions: 80, + lines: 80, + }, + }, +}; diff --git a/packages/table/package.json b/packages/table/package.json new file mode 100644 index 000000000..8b89a09a3 --- /dev/null +++ b/packages/table/package.json @@ -0,0 +1,62 @@ +{ + "name": "@iot-app-kit/table", + "publishConfig": { + "access": "public" + }, + "version": "1.3.0", + "description": "IoT Application Kit - Table component", + "license": "Apache-2.0", + "main": "./dist/index.cj.js", + "module": "./dist/index.js", + "types": "./dist/types/index.d.ts", + "directories": { + "dist": "dist" + }, + "files": [ + "dist/", + "CHANGELOG.md", + "*NOTICE" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com/" + }, + "scripts": { + "clean": "rm -rf dist && rm -rf screenshot", + "build": "yarn run clean && yarn run build:types && rollup --config rollup.config.js", + "build:types": "tsc --outDir dist/types --declaration true --emitDeclarationOnly true", + "test": "npm-run-all -p test:jest test:typescript", + "test:jest": "TZ=UTC jest --coverage", + "test.watch": "TZ=UTC jest --watchAll", + "jest": "TZ=UTC jest", + "test:typescript": "tsc --noEmit", + "copy:license": "cp ../../LICENSE LICENSE", + "copy:notice": "cp ../../NOTICE NOTICE", + "prepack": "yarn run copy:license && yarn run copy:notice", + "pack": "yarn pack" + }, + "devDependencies": { + "@aws-sdk/client-iotsitewise": "^3.39.0", + "@awsui/design-tokens": "^3.0.0", + "@rollup/plugin-typescript": "^8.3.2", + "@types/jest": "^28.1.0", + "eslint-config-airbnb": "^18.2.1", + "eslint-config-airbnb-typescript": "^12.3.1", + "jest": "^28.1.0", + "jest-cli": "^28.1.0", + "jest-extended": "^2.0.0", + "rollup-plugin-import-css": "^3.0.3", + "sass": "^1.30.0", + "ts-jest": "^28.0.4" + }, + "dependencies": { + "@awsui/collection-hooks": "^1.0.0", + "@awsui/components-react": "^3.0.0", + "@iot-app-kit/core": "^1.3.0", + "@synchro-charts/core": "^4.0.0", + "@synchro-charts/react": "^4.0.0", + "d3-array": "^3.1.6", + "react": ">=17.0.2", + "react-dom": ">=17.0.2" + } +} diff --git a/packages/table/rollup.config.js b/packages/table/rollup.config.js new file mode 100644 index 000000000..f43e96471 --- /dev/null +++ b/packages/table/rollup.config.js @@ -0,0 +1,20 @@ +import typescript from '@rollup/plugin-typescript'; +import pkg from './package.json'; +import css from 'rollup-plugin-import-css'; + +export default [ + { + input: 'src/index.ts', + output: [ + { + file: pkg.main, + format: 'cjs', + }, + { + file: pkg.module, + format: 'esm', + }, + ], + plugins: [typescript({ tsconfig: './tsconfig.json' }), css()], + }, +]; diff --git a/packages/table/src/index.ts b/packages/table/src/index.ts new file mode 100644 index 000000000..52f8fe894 --- /dev/null +++ b/packages/table/src/index.ts @@ -0,0 +1,2 @@ +// export * from './table'; WIP +export * from './utils'; diff --git a/packages/table/src/utils/createCellItem.spec.ts b/packages/table/src/utils/createCellItem.spec.ts new file mode 100644 index 000000000..e31c16d28 --- /dev/null +++ b/packages/table/src/utils/createCellItem.spec.ts @@ -0,0 +1,25 @@ +import { createCellItem } from './createCellItem'; +import { CellItem } from './types'; + +describe('createCellItem', () => { + it('creates CellItem', () => { + const props = { value: 10, error: undefined, isLoading: false, threshold: undefined }; + const item: CellItem = createCellItem(props); + expect(item).toMatchObject({ value: 10 }); + expect(item.valueOf()).toEqual(10); + }); + + it('creates CellItem that returns error message on error', () => { + const props = { value: 10, error: { msg: 'Some error' }, isLoading: false, threshold: undefined }; + const item: CellItem = createCellItem(props); + + expect(`${item}`).toBe('Some error'); + }); + + it('creates CellItem that returns loading message on loading', () => { + const props = { value: 10, isLoading: true, threshold: undefined }; + const item: CellItem = createCellItem(props); + + expect(`${item}`).toBe('Loading'); + }); +}); diff --git a/packages/table/src/utils/createCellItem.ts b/packages/table/src/utils/createCellItem.ts new file mode 100644 index 000000000..b687144c1 --- /dev/null +++ b/packages/table/src/utils/createCellItem.ts @@ -0,0 +1,34 @@ +import { Primitive, Threshold } from '@synchro-charts/core'; +import { ErrorDetails } from '@iot-app-kit/core'; +import { CellItem } from './types'; + +type CellProps = { + value?: Primitive; + error?: ErrorDetails; + isLoading?: boolean; + threshold?: Threshold; +}; +export const createCellItem: (props?: CellProps) => CellItem = ({ value, error, isLoading, threshold } = {}) => { + const valueOf = () => { + if (error) { + return error.msg; + } + if (isLoading) { + return 'Loading'; + } + return value; + }; + + const toString = () => { + return `${valueOf()}`; + }; + + return { + value, + error, + isLoading, + threshold, + valueOf, + toString, + }; +}; diff --git a/packages/table/src/utils/createTableItems.spec.ts b/packages/table/src/utils/createTableItems.spec.ts new file mode 100644 index 000000000..59fd299bd --- /dev/null +++ b/packages/table/src/utils/createTableItems.spec.ts @@ -0,0 +1,215 @@ +import { DataStream, Viewport } from '@iot-app-kit/core'; +import { Annotations, COMPARISON_OPERATOR, getThresholds } from '@synchro-charts/core'; +import { createTableItems } from './createTableItems'; + +const dataStreams: DataStream[] = [ + { + id: 'data-1', + data: [ + { y: 0, x: new Date(2021, 1, 1, 0, 0, 1).getTime() }, + { y: 1, x: new Date(2022, 1, 1, 0, 0, 2).getTime() }, + { y: 2, x: new Date(2022, 1, 1, 0, 0, 3).getTime() }, + { y: 3, x: new Date(2022, 1, 1, 0, 0, 4).getTime() }, + { y: 4, x: new Date(2022, 1, 1, 0, 0, 5).getTime() }, + ], + resolution: 0, + }, + { + id: 'data-2', + data: [{ y: 11, x: new Date(2022, 1, 1, 0, 0, 1).getTime() }], + resolution: 0, + }, + { + id: 'agg_data', + aggregates: { + 60: [{ y: 60, x: new Date(2022, 1, 1, 0, 0, 1).getTime() }], + }, + data: [{ y: 0, x: new Date(2022, 1, 1, 0, 0, 1).getTime() }], + resolution: 60, + }, +]; + +const viewport = { + duration: '1000', +}; + +const itemWithRef = [ + { + value1: { + $cellRef: { + id: 'data-1', + resolution: 0, + }, + noRef: 'No Ref', + }, + value2: { + $cellRef: { + id: 'data-2', + resolution: 0, + }, + }, + noRef: { + data: [1, 2, 3, 4], + }, + rawValue: 10, + }, + { + data: { + $cellRef: { + id: 'data-1', + resolution: 0, + }, + }, + invalid: { + $cellRef: { + id: 'invalid-data-stream', + resolution: 0, + }, + }, + aggregates: { + $cellRef: { + id: 'agg_data', + resolution: 60, + }, + }, + invalidAggregation: { + $cellRef: { + id: 'agg_data', + resolution: 55, + }, + }, + }, +]; + +it('creates table items', () => { + const items = createTableItems({ dataStreams, viewport, items: itemWithRef }); + expect(items).toMatchObject([ + { + value1: { value: 4 }, + value2: { value: 11 }, + noRef: { + value: { + data: [1, 2, 3, 4], + }, + }, + rawValue: { value: 10 }, + }, + { + data: { value: 4 }, + invalid: { value: undefined }, + aggregates: { value: 60 }, + invalidAggregation: { value: undefined }, + }, + ]); +}); + +it('returns value as it is a primitive value', () => { + const items = createTableItems({ dataStreams, viewport, items: itemWithRef }); + const data = items[0].value1; + expect((data as number) + 1).toBe(5); +}); + +it('gets different data points on different viewports on the same data stream', () => { + const viewport1: Viewport = { + start: new Date(2022, 1, 1, 0, 0, 1), + end: new Date(2022, 1, 1, 0, 0, 2), + }; + + const viewport2 = { + start: new Date(2022, 1, 1, 0, 0, 1), + end: new Date(2022, 1, 1, 0, 0, 3), + }; + + const itemDef = [ + { + value1: { + $cellRef: { + id: 'data-1', + resolution: 0, + }, + }, + }, + ]; + const items1 = createTableItems({ dataStreams, viewport: viewport1, items: itemDef }); + const items2 = createTableItems({ dataStreams, viewport: viewport2, items: itemDef }); + + expect(items1).not.toEqual(items2); +}); + +it('returns undefined value when no data points in data stream', () => { + // no data point would match this viewport + const viewport1: Viewport = { + start: new Date(2020, 1, 1, 0), + end: new Date(2021, 1, 1, 0), + }; + const itemDef = [ + { + noDataPoints: { + $cellRef: { + id: 'data-1', + resolution: 0, + }, + }, + }, + ]; + const items1 = createTableItems({ dataStreams, viewport: viewport1, items: itemDef }); + expect(items1).toMatchObject([{ noDataPoints: { value: undefined } }]); +}); + +it('contains breached threshold', () => { + const thresholdOne = { + value: 1, + color: 'red', + comparisonOperator: COMPARISON_OPERATOR.GREATER_THAN, + dataStreamIds: ['data-1'], + }; + const thresholdTwo = { + value: 0, + color: 'black', + comparisonOperator: COMPARISON_OPERATOR.GREATER_THAN, + }; + + const annotations: Annotations = { + y: [ + // only apply to data stream 'data-1' + thresholdOne, + + // apply to both data stream + thresholdTwo, + ], + }; + + const items = [ + { + itemOne: { + $cellRef: { + id: 'data-1', + resolution: 0, + }, + }, + itemTwo: { + $cellRef: { + id: 'data-2', + resolution: 0, + }, + }, + + noRef: 10, + }, + ]; + + const tableItems = createTableItems({ + dataStreams, + viewport, + items, + thresholds: getThresholds(annotations), + }); + + const { itemOne, itemTwo, noRef } = tableItems[0]; + + expect(itemOne.threshold).toMatchObject(thresholdOne); + expect(itemTwo.threshold).toMatchObject(thresholdTwo); + + // Item with no $cellRef does not support threshold + expect(noRef).toMatchObject({ threshold: undefined }); +}); diff --git a/packages/table/src/utils/createTableItems.ts b/packages/table/src/utils/createTableItems.ts new file mode 100644 index 000000000..ab628fec9 --- /dev/null +++ b/packages/table/src/utils/createTableItems.ts @@ -0,0 +1,62 @@ +import { Viewport, DataStream } from '@iot-app-kit/core'; +import { breachedThreshold, Primitive, Threshold, DataStream as SynchroChartsDataStream } from '@synchro-charts/core'; +import { getDataBeforeDate } from './dataFilters'; +import { getDataPoints } from './getDataPoints'; +import { CellItem, Item, ItemRef, TableItem } from './types'; +import { createCellItem } from './createCellItem'; + +export const createTableItems: (config: { + dataStreams: DataStream[]; + items: Item[]; + viewport: Viewport; + thresholds?: Threshold[]; +}) => TableItem[] = ({ dataStreams, viewport, items, thresholds = [] }) => { + return items.map((item) => { + const keys = Object.keys(item); + const keyDataPairs = keys.map<{ key: string; data: CellItem }>((key) => { + if (typeof item[key] === 'object' && Object.prototype.hasOwnProperty.call(item[key], '$cellRef')) { + const { $cellRef } = item[key] as ItemRef; + const dataStream = dataStreams.find(({ id }) => id === $cellRef.id); + + if (dataStream) { + const dataPoints = getDataPoints(dataStream, $cellRef.resolution); + const { error, isLoading } = dataStream; + + if ('end' in viewport && dataPoints) { + const point = getDataBeforeDate(dataPoints, viewport.end).pop(); + const value = point?.y; + const threshold = breachedThreshold({ + dataStream: dataStream as SynchroChartsDataStream, + dataStreams: dataStreams as SynchroChartsDataStream[], + value, + thresholds, + date: viewport.end, + }); + return { key, data: createCellItem({ value, error, isLoading, threshold }) }; + } + + const value = dataPoints.slice(-1)[0]?.y; + const threshold = breachedThreshold({ + dataStream: dataStream as SynchroChartsDataStream, + dataStreams: dataStreams as SynchroChartsDataStream[], + value, + thresholds, + date: new Date(Date.now()), + }); + + return { key, data: createCellItem({ value, error, isLoading, threshold }) }; + } + return { key, data: createCellItem() }; + } + return { key, data: createCellItem({ value: item[key] as Primitive }) }; + }); + + return keyDataPairs.reduce( + (previous, { key, data }) => ({ + ...previous, + [key]: data, + }), + {} + ); + }); +}; diff --git a/packages/table/src/utils/dataFilters.spec.ts b/packages/table/src/utils/dataFilters.spec.ts new file mode 100644 index 000000000..8a63f540a --- /dev/null +++ b/packages/table/src/utils/dataFilters.spec.ts @@ -0,0 +1,45 @@ +import { getDataBeforeDate } from './dataFilters'; + +describe('getDataBeforeDate', () => { + const DATE = new Date(2000, 0, 0).getTime(); + it('returns empty list when given no data', () => { + expect(getDataBeforeDate([], new Date())).toBeEmpty(); + }); + + it('returns empty list when one point is given, and is after the date', () => { + expect(getDataBeforeDate([{ x: new Date(2002, 0, 0).getTime(), y: 100 }], new Date(DATE))).toBeEmpty(); + }); + + it('returns data point when given one data point at the date', () => { + const DATA_POINT = { x: DATE, y: 100 }; + expect(getDataBeforeDate([DATA_POINT], new Date(DATE))).toEqual([DATA_POINT]); + }); + + it('returns empty list when all dates are after the given date', () => { + expect( + getDataBeforeDate( + [ + { x: new Date(2001, 0, 0).getTime(), y: 100 }, + { x: new Date(2002, 0, 0).getTime(), y: 100 }, + ], + new Date(DATE) + ) + ).toBeEmpty(); + }); + + it('returns all data points when all are at or before given date', () => { + const DATA = [ + { x: new Date(1999, 0, 0).getTime(), y: 100 }, + { x: DATE, y: 100 }, + ]; + expect(getDataBeforeDate(DATA, new Date(DATE))).toEqual(DATA); + }); + + it('filters out data that is after the date', () => { + const DATA = [ + { x: new Date(1999, 0, 0).getTime(), y: 100 }, + { x: new Date(2001, 0, 0).getTime(), y: 100 }, + ]; + expect(getDataBeforeDate(DATA, new Date(DATE))).toEqual([DATA[0]]); + }); +}); diff --git a/packages/table/src/utils/dataFilters.ts b/packages/table/src/utils/dataFilters.ts new file mode 100644 index 000000000..7cc05f0c0 --- /dev/null +++ b/packages/table/src/utils/dataFilters.ts @@ -0,0 +1,28 @@ +import { bisector } from 'd3-array'; +import { DataPoint, Primitive } from '@synchro-charts/core'; + +// By doing the mapping to a date within the bisector +// we eliminate the need to iterate over the entire data. +// (As opposed to mapping entire data to an array of dates) +export const pointBisector = bisector((p: DataPoint) => p.x); + +/** + * Returns all data before or at the given date. + * + * Assumes data is ordered chronologically. + */ +export const getDataBeforeDate = (data: DataPoint[], date: Date): DataPoint[] => { + // If there is no data + if (data.length === 0) { + return []; + } + // If all data is after the view port + if (date.getTime() < data[0].x) { + return []; + } + + // Otherwise return all the data within the viewport, plus an additional single data point that falls outside of + // the viewport in either direction. + const endIndex = Math.min(pointBisector.right(data, date) - 1, data.length - 1); + return data.slice(0, endIndex + 1); +}; diff --git a/packages/table/src/utils/getDataPoints.ts b/packages/table/src/utils/getDataPoints.ts new file mode 100644 index 000000000..6f76d4177 --- /dev/null +++ b/packages/table/src/utils/getDataPoints.ts @@ -0,0 +1,17 @@ +/** + * Get the points for a given resolution from a data stream + */ +import { DataStream } from '@iot-app-kit/core'; +import { DataPoint, Primitive, Resolution } from '@synchro-charts/core'; + +export const getDataPoints = (stream: DataStream, resolution: Resolution): DataPoint[] => { + if (resolution === 0) { + return stream.data; + } + + if (stream.aggregates == null) { + return []; + } + + return stream.aggregates[resolution] || []; +}; diff --git a/packages/table/src/utils/index.ts b/packages/table/src/utils/index.ts new file mode 100644 index 000000000..e3f571144 --- /dev/null +++ b/packages/table/src/utils/index.ts @@ -0,0 +1,2 @@ +export { createTableItems } from './createTableItems'; +export { CellItem, Item, ItemRef, TableItem } from './types'; diff --git a/packages/table/src/utils/types.ts b/packages/table/src/utils/types.ts new file mode 100644 index 000000000..404ef6a71 --- /dev/null +++ b/packages/table/src/utils/types.ts @@ -0,0 +1,31 @@ +import { Primitive, Threshold } from '@synchro-charts/core'; +import { ErrorDetails } from '@iot-app-kit/core'; + +export type ItemRef = { + $cellRef: { + id: string; + resolution: number; + }; +}; + +export type Item = { + [key in string]: ItemRef | unknown; +}; + +export type CellItemProps = { + value?: Primitive; + error?: ErrorDetails; + isLoading?: boolean; + threshold?: Threshold; +}; + +export type CellItem = { + value?: Primitive; + error?: ErrorDetails; + isLoading?: boolean; + threshold?: Threshold; + valueOf: () => Primitive | undefined; + toString: () => string; +}; + +export type TableItem = { [k in string]: CellItem }; diff --git a/packages/table/tsconfig.json b/packages/table/tsconfig.json new file mode 100644 index 000000000..4a423c786 --- /dev/null +++ b/packages/table/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false, + "experimentalDecorators": true, + "lib": [ + "dom", + "es2019" + ], + "moduleResolution": "node", + "module": "esnext", + "target": "es2017", + "esModuleInterop": true, + "strict": true, + "jsx": "react-jsx", + "strictPropertyInitialization": false, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules", + "dist", + "**.spec.ts" + ], + "files": ["global.d.ts"] +}