From c51650745f01526d0f462e6436b8cb4f5268a06a Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 17 Jul 2018 22:07:13 -0700 Subject: [PATCH] [core/ui] bootstrap the legacy platform within the new platform (#20699) Fixes #20694 Implements super basic new platform `core` system, which includes two services: `core.injectedMetadata` and `core.legacyPlatform`. The `core` currently has two responsibilities: 1. read the metadata from the DOM and initialize the `ui/metadata` module with legacy metadata, proving out how we plan to expose data from the new platform through the existing APIs/modules to the legacy platform. 2. bootstrap the legacy platform by loading either `ui/chrome` or `ui/test_harness` Because `core` mutates the `ui/metadata` module before bootstrapping the legacy platform all existing consumers of `ui/metadata` won't be impacted by the fact that metadata loading was moved into the new platform. We plan to do this for many other services that will need to exist in both the legacy and new platforms, like `ui/chrome` (see #20696). --- .../lib/get_webpack_config.js | 1 - src/core/public/core_system.test.ts | 126 +++++++++++++++ src/core/public/core_system.ts | 59 +++++++ src/core/public/index.ts | 20 +++ .../__fixtures__/frozen_object_mutation.ts | 33 ++++ .../frozen_object_mutation.tsconfig.json | 13 ++ .../injected_metadata/deep_freeze.test.ts | 102 ++++++++++++ .../public/injected_metadata/deep_freeze.ts | 36 +++++ src/core/public/injected_metadata/index.ts | 24 +++ .../injected_metadata_service.test.ts | 53 ++++++ .../injected_metadata_service.ts | 50 ++++++ src/core/public/legacy_platform/index.ts | 20 +++ .../legacy_platform_service.test.ts | 151 ++++++++++++++++++ .../legacy_platform_service.ts | 72 +++++++++ .../public/tests/src/integration.test.js | 2 +- .../package.json | 2 +- src/core_plugins/tests_bundle/index.js | 12 +- .../tests_bundle/tests_entry_template.js | 23 ++- src/dev/file.ts | 6 +- src/dev/tslint/pick_files_to_lint.ts | 2 +- src/optimize/index.js | 8 +- src/optimize/watch/watch_optimizer.js | 5 +- .../ui_exports_replace_injected_vars.js | 4 +- src/ui/public/__tests__/metadata.js | 7 - src/ui/public/chrome/chrome.js | 21 +-- src/ui/public/metadata.js | 29 +--- src/ui/public/notify/__tests__/notifier.js | 7 +- src/ui/public/test_harness/index.js | 2 - src/ui/public/test_harness/test_harness.js | 8 +- src/ui/ui_apps/__tests__/ui_app.js | 20 ++- src/ui/ui_apps/ui_app.js | 4 +- .../__tests__/app_entry_template.js | 2 +- src/ui/ui_bundles/app_entry_template.js | 19 ++- src/ui/ui_bundles/ui_bundle.js | 32 ++-- src/ui/ui_bundles/ui_bundles_controller.js | 70 ++++---- src/ui/ui_exports/ui_export_defaults.js | 2 +- src/ui/ui_render/ui_render_mixin.js | 22 +-- src/ui/ui_render/views/chrome.jade | 53 +++--- src/ui/ui_render/views/ui_app.jade | 2 +- tsconfig.json | 3 + 40 files changed, 934 insertions(+), 193 deletions(-) create mode 100644 src/core/public/core_system.test.ts create mode 100644 src/core/public/core_system.ts create mode 100644 src/core/public/index.ts create mode 100644 src/core/public/injected_metadata/__fixtures__/frozen_object_mutation.ts create mode 100644 src/core/public/injected_metadata/__fixtures__/frozen_object_mutation.tsconfig.json create mode 100644 src/core/public/injected_metadata/deep_freeze.test.ts create mode 100644 src/core/public/injected_metadata/deep_freeze.ts create mode 100644 src/core/public/injected_metadata/index.ts create mode 100644 src/core/public/injected_metadata/injected_metadata_service.test.ts create mode 100644 src/core/public/injected_metadata/injected_metadata_service.ts create mode 100644 src/core/public/legacy_platform/index.ts create mode 100644 src/core/public/legacy_platform/legacy_platform_service.test.ts create mode 100644 src/core/public/legacy_platform/legacy_platform_service.ts diff --git a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js index f0425026d1fd..9ba116c66238 100755 --- a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js +++ b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js @@ -28,7 +28,6 @@ exports.getWebpackConfig = function(kibanaPath, projectRoot, config) { const alias = { // Kibana defaults https://github.com/elastic/kibana/blob/6998f074542e8c7b32955db159d15661aca253d7/src/ui/ui_bundler_env.js#L30-L36 ui: fromKibana('src/ui/public'), - ui_framework: fromKibana('ui_framework'), test_harness: fromKibana('src/test_harness/public'), querystring: 'querystring-browser', moment$: fromKibana('webpackShims/moment'), diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts new file mode 100644 index 000000000000..76d615b3a45e --- /dev/null +++ b/src/core/public/core_system.test.ts @@ -0,0 +1,126 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { InjectedMetadataService } from './injected_metadata'; +import { LegacyPlatformService } from './legacy_platform'; + +const MockLegacyPlatformService = jest.fn( + function _MockLegacyPlatformService(this: any) { + this.start = jest.fn(); + } +); +jest.mock('./legacy_platform', () => ({ + LegacyPlatformService: MockLegacyPlatformService, +})); + +const mockInjectedMetadataStartContract = {}; +const MockInjectedMetadataService = jest.fn( + function _MockInjectedMetadataService(this: any) { + this.start = jest.fn().mockReturnValue(mockInjectedMetadataStartContract); + } +); +jest.mock('./injected_metadata', () => ({ + InjectedMetadataService: MockInjectedMetadataService, +})); + +import { CoreSystem } from './core_system'; + +const defaultCoreSystemParams = { + rootDomElement: null!, + injectedMetadata: {} as any, + requireLegacyFiles: jest.fn(), +}; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('constructor', () => { + it('creates instances of services', () => { + // tslint:disable no-unused-expression + new CoreSystem({ + ...defaultCoreSystemParams, + }); + + expect(MockInjectedMetadataService).toHaveBeenCalledTimes(1); + expect(MockLegacyPlatformService).toHaveBeenCalledTimes(1); + }); + + it('passes injectedMetadata param to InjectedMetadataService', () => { + const injectedMetadata = { injectedMetadata: true } as any; + + // tslint:disable no-unused-expression + new CoreSystem({ + ...defaultCoreSystemParams, + injectedMetadata, + }); + + expect(MockInjectedMetadataService).toHaveBeenCalledTimes(1); + expect(MockInjectedMetadataService).toHaveBeenCalledWith({ + injectedMetadata, + }); + }); + + it('passes rootDomElement, requireLegacyFiles, and useLegacyTestHarness to LegacyPlatformService', () => { + const rootDomElement = { rootDomElement: true } as any; + const requireLegacyFiles = { requireLegacyFiles: true } as any; + const useLegacyTestHarness = { useLegacyTestHarness: true } as any; + + // tslint:disable no-unused-expression + new CoreSystem({ + ...defaultCoreSystemParams, + rootDomElement, + requireLegacyFiles, + useLegacyTestHarness, + }); + + expect(MockLegacyPlatformService).toHaveBeenCalledTimes(1); + expect(MockLegacyPlatformService).toHaveBeenCalledWith({ + rootDomElement, + requireLegacyFiles, + useLegacyTestHarness, + }); + }); +}); + +describe('#start()', () => { + function startCore() { + const core = new CoreSystem({ + ...defaultCoreSystemParams, + }); + + core.start(); + } + + it('calls injectedMetadata#start()', () => { + startCore(); + const [mockInstance] = MockInjectedMetadataService.mock.instances; + expect(mockInstance.start).toHaveBeenCalledTimes(1); + expect(mockInstance.start).toHaveBeenCalledWith(); + }); + + it('calls lifecycleSystem#start()', () => { + startCore(); + const [mockInstance] = MockLegacyPlatformService.mock.instances; + expect(mockInstance.start).toHaveBeenCalledTimes(1); + expect(mockInstance.start).toHaveBeenCalledWith({ + injectedMetadata: mockInjectedMetadataStartContract, + }); + }); +}); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts new file mode 100644 index 000000000000..cc659d74e01a --- /dev/null +++ b/src/core/public/core_system.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata'; +import { LegacyPlatformParams, LegacyPlatformService } from './legacy_platform'; + +interface Params { + injectedMetadata: InjectedMetadataParams['injectedMetadata']; + rootDomElement: LegacyPlatformParams['rootDomElement']; + requireLegacyFiles: LegacyPlatformParams['requireLegacyFiles']; + useLegacyTestHarness?: LegacyPlatformParams['useLegacyTestHarness']; +} + +/** + * The CoreSystem is the root of the new platform, and starts all parts + * of Kibana in the UI, including the LegacyPlatform which is managed + * by the LegacyPlatformService. As we migrate more things to the new + * platform the CoreSystem will get many more Services. + */ +export class CoreSystem { + private injectedMetadata: InjectedMetadataService; + private legacyPlatform: LegacyPlatformService; + + constructor(params: Params) { + const { rootDomElement, injectedMetadata, requireLegacyFiles, useLegacyTestHarness } = params; + + this.injectedMetadata = new InjectedMetadataService({ + injectedMetadata, + }); + + this.legacyPlatform = new LegacyPlatformService({ + rootDomElement, + requireLegacyFiles, + useLegacyTestHarness, + }); + } + + public start() { + this.legacyPlatform.start({ + injectedMetadata: this.injectedMetadata.start(), + }); + } +} diff --git a/src/core/public/index.ts b/src/core/public/index.ts new file mode 100644 index 000000000000..fcf7ffd908a4 --- /dev/null +++ b/src/core/public/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { CoreSystem } from './core_system'; diff --git a/src/core/public/injected_metadata/__fixtures__/frozen_object_mutation.ts b/src/core/public/injected_metadata/__fixtures__/frozen_object_mutation.ts new file mode 100644 index 000000000000..ab9fe4da923c --- /dev/null +++ b/src/core/public/injected_metadata/__fixtures__/frozen_object_mutation.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { deepFreeze } from '../deep_freeze'; + +const obj = deepFreeze({ + foo: { + bar: { + baz: 1, + }, + }, +}); + +delete obj.foo; +obj.foo = 1; +obj.foo.bar.baz = 2; +obj.foo.bar.box = false; diff --git a/src/core/public/injected_metadata/__fixtures__/frozen_object_mutation.tsconfig.json b/src/core/public/injected_metadata/__fixtures__/frozen_object_mutation.tsconfig.json new file mode 100644 index 000000000000..aaedce798435 --- /dev/null +++ b/src/core/public/injected_metadata/__fixtures__/frozen_object_mutation.tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "strict": true, + "skipLibCheck": true, + "lib": [ + "esnext" + ] + }, + "include": [ + "frozen_object_mutation.ts", + "../deep_freeze.ts" + ] +} diff --git a/src/core/public/injected_metadata/deep_freeze.test.ts b/src/core/public/injected_metadata/deep_freeze.test.ts new file mode 100644 index 000000000000..9086d697f9ce --- /dev/null +++ b/src/core/public/injected_metadata/deep_freeze.test.ts @@ -0,0 +1,102 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; + +import execa from 'execa'; + +import { deepFreeze } from './deep_freeze'; + +it('returns the first argument with all original references', () => { + const a = {}; + const b = {}; + const c = { a, b }; + + const frozen = deepFreeze(c); + expect(frozen).toBe(c); + expect(frozen.a).toBe(a); + expect(frozen.b).toBe(b); +}); + +it('prevents adding properties to argument', () => { + const frozen = deepFreeze({}); + expect(() => { + // @ts-ignore ts knows this shouldn't be possible, but just making sure + frozen.foo = true; + }).toThrowError(`object is not extensible`); +}); + +it('prevents changing properties on argument', () => { + const frozen = deepFreeze({ foo: false }); + expect(() => { + // @ts-ignore ts knows this shouldn't be possible, but just making sure + frozen.foo = true; + }).toThrowError(`read only property 'foo'`); +}); + +it('prevents changing properties on nested children of argument', () => { + const frozen = deepFreeze({ foo: { bar: { baz: { box: 1 } } } }); + expect(() => { + // @ts-ignore ts knows this shouldn't be possible, but just making sure + frozen.foo.bar.baz.box = 2; + }).toThrowError(`read only property 'box'`); +}); + +it('prevents adding items to a frozen array', () => { + const frozen = deepFreeze({ foo: [1] }); + expect(() => { + // @ts-ignore ts knows this shouldn't be possible, but just making sure + frozen.foo.push(2); + }).toThrowError(`object is not extensible`); +}); + +it('prevents reassigning items in a frozen array', () => { + const frozen = deepFreeze({ foo: [1] }); + expect(() => { + // @ts-ignore ts knows this shouldn't be possible, but just making sure + frozen.foo[0] = 2; + }).toThrowError(`read only property '0'`); +}); + +it('types return values to prevent mutations in typescript', async () => { + const result = await execa.stdout( + 'tsc', + [ + '--noEmit', + '--project', + resolve(__dirname, '__fixtures__/frozen_object_mutation.tsconfig.json'), + ], + { + cwd: resolve(__dirname, '__fixtures__'), + reject: false, + } + ); + + const errorCodeRe = /\serror\s(TS\d{4}):/g; + const errorCodes = []; + while (true) { + const match = errorCodeRe.exec(result); + if (!match) { + break; + } + errorCodes.push(match[1]); + } + + expect(errorCodes).toEqual(['TS2704', 'TS2540', 'TS2540', 'TS2339']); +}); diff --git a/src/core/public/injected_metadata/deep_freeze.ts b/src/core/public/injected_metadata/deep_freeze.ts new file mode 100644 index 000000000000..33948fccef95 --- /dev/null +++ b/src/core/public/injected_metadata/deep_freeze.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +type Freezable = { [k: string]: any } | any[]; + +type RecursiveReadOnly = T extends Freezable + ? Readonly<{ [K in keyof T]: RecursiveReadOnly }> + : T; + +export function deepFreeze(object: T) { + // for any properties that reference an object, makes sure that object is + // recursively frozen as well + for (const value of Object.values(object)) { + if (value !== null && typeof value === 'object') { + deepFreeze(value); + } + } + + return Object.freeze(object) as RecursiveReadOnly; +} diff --git a/src/core/public/injected_metadata/index.ts b/src/core/public/injected_metadata/index.ts new file mode 100644 index 000000000000..e8979fc0c377 --- /dev/null +++ b/src/core/public/injected_metadata/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + InjectedMetadataService, + InjectedMetadataParams, + InjectedMetadataStartContract, +} from './injected_metadata_service'; diff --git a/src/core/public/injected_metadata/injected_metadata_service.test.ts b/src/core/public/injected_metadata/injected_metadata_service.test.ts new file mode 100644 index 000000000000..59b3cc65db24 --- /dev/null +++ b/src/core/public/injected_metadata/injected_metadata_service.test.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { InjectedMetadataService } from './injected_metadata_service'; + +describe('#start()', () => { + it('deeply freezes its injectedMetadata param', () => { + const params = { + injectedMetadata: { foo: true } as any, + }; + + const injectedMetadata = new InjectedMetadataService(params); + + expect(() => { + params.injectedMetadata.foo = false; + }).not.toThrowError(); + + injectedMetadata.start(); + + expect(() => { + params.injectedMetadata.foo = true; + }).toThrowError(`read only property 'foo'`); + }); +}); + +describe('start.getLegacyMetadata()', () => { + it('returns injectedMetadata.legacyMetadata', () => { + const injectedMetadata = new InjectedMetadataService({ + injectedMetadata: { + legacyMetadata: 'foo', + } as any, + }); + + const contract = injectedMetadata.start(); + expect(contract.getLegacyMetadata()).toBe('foo'); + }); +}); diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts new file mode 100644 index 000000000000..2921d97fca66 --- /dev/null +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { deepFreeze } from './deep_freeze'; + +export interface InjectedMetadataParams { + injectedMetadata: { + legacyMetadata: { + [key: string]: any; + }; + }; +} + +/** + * Provides access to the metadata that is injected by the + * server into the page. The metadata is actually defined + * in the entry file for the bundle containing the new platform + * and is read from the DOM in most cases. + */ +export class InjectedMetadataService { + constructor(private readonly params: InjectedMetadataParams) {} + + public start() { + const state = deepFreeze(this.params.injectedMetadata); + + return { + getLegacyMetadata() { + return state.legacyMetadata; + }, + }; + } +} + +export type InjectedMetadataStartContract = ReturnType; diff --git a/src/core/public/legacy_platform/index.ts b/src/core/public/legacy_platform/index.ts new file mode 100644 index 000000000000..44862dcb4430 --- /dev/null +++ b/src/core/public/legacy_platform/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { LegacyPlatformService, LegacyPlatformParams } from './legacy_platform_service'; diff --git a/src/core/public/legacy_platform/legacy_platform_service.test.ts b/src/core/public/legacy_platform/legacy_platform_service.test.ts new file mode 100644 index 000000000000..3927d7f56149 --- /dev/null +++ b/src/core/public/legacy_platform/legacy_platform_service.test.ts @@ -0,0 +1,151 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const mockLoadOrder: string[] = []; + +const mockUiMetadataInit = jest.fn(); +jest.mock('ui/metadata', () => { + mockLoadOrder.push('ui/metadata'); + return { + __newPlatformInit__: mockUiMetadataInit, + }; +}); + +const mockUiChromeBootstrap = jest.fn(); +jest.mock('ui/chrome', () => { + mockLoadOrder.push('ui/chrome'); + return { + bootstrap: mockUiChromeBootstrap, + }; +}); + +const mockUiTestHarnessBootstrap = jest.fn(); +jest.mock('ui/test_harness', () => { + mockLoadOrder.push('ui/test_harness'); + return { + bootstrap: mockUiTestHarnessBootstrap, + }; +}); + +import { LegacyPlatformService } from './legacy_platform_service'; + +const injectedMetadataStartContract = { + getLegacyMetadata: jest.fn(), +}; + +const defaultParams = { + rootDomElement: { someDomElement: true } as any, + requireLegacyFiles: jest.fn(() => { + mockLoadOrder.push('legacy files'); + }), +}; + +afterEach(() => { + jest.clearAllMocks(); + injectedMetadataStartContract.getLegacyMetadata.mockReset(); + jest.resetModules(); + mockLoadOrder.length = 0; +}); + +describe('#start()', () => { + describe('default', () => { + it('passes legacy metadata from injectedVars to ui/metadata', () => { + const legacyMetadata = { isLegacyMetadata: true }; + injectedMetadataStartContract.getLegacyMetadata.mockReturnValue(legacyMetadata); + + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.start({ + injectedMetadata: injectedMetadataStartContract, + }); + + expect(mockUiMetadataInit).toHaveBeenCalledTimes(1); + expect(mockUiMetadataInit).toHaveBeenCalledWith(legacyMetadata); + }); + + describe('useLegacyTestHarness = false', () => { + it('passes the rootDomElement to ui/chrome', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.start({ + injectedMetadata: injectedMetadataStartContract, + }); + + expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled(); + expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1); + expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultParams.rootDomElement); + }); + }); + describe('useLegacyTestHarness = true', () => { + it('passes the rootDomElement to ui/test_harness', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + useLegacyTestHarness: true, + }); + + legacyPlatform.start({ + injectedMetadata: injectedMetadataStartContract, + }); + + expect(mockUiChromeBootstrap).not.toHaveBeenCalled(); + expect(mockUiTestHarnessBootstrap).toHaveBeenCalledTimes(1); + expect(mockUiTestHarnessBootstrap).toHaveBeenCalledWith(defaultParams.rootDomElement); + }); + }); + }); + + describe('load order', () => { + describe('useLegacyTestHarness = false', () => { + it('loads ui/modules before ui/chrome, and both before legacy files', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + expect(mockLoadOrder).toEqual([]); + + legacyPlatform.start({ + injectedMetadata: injectedMetadataStartContract, + }); + + expect(mockLoadOrder).toEqual(['ui/metadata', 'ui/chrome', 'legacy files']); + }); + }); + + describe('useLegacyTestHarness = true', () => { + it('loads ui/modules before ui/test_harness, and both before legacy files', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + useLegacyTestHarness: true, + }); + + expect(mockLoadOrder).toEqual([]); + + legacyPlatform.start({ + injectedMetadata: injectedMetadataStartContract, + }); + + expect(mockLoadOrder).toEqual(['ui/metadata', 'ui/test_harness', 'legacy files']); + }); + }); + }); +}); diff --git a/src/core/public/legacy_platform/legacy_platform_service.ts b/src/core/public/legacy_platform/legacy_platform_service.ts new file mode 100644 index 000000000000..ed7976bfa2a3 --- /dev/null +++ b/src/core/public/legacy_platform/legacy_platform_service.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { InjectedMetadataStartContract } from '../injected_metadata'; + +interface Deps { + injectedMetadata: InjectedMetadataStartContract; +} + +export interface LegacyPlatformParams { + rootDomElement: HTMLElement; + requireLegacyFiles: () => void; + useLegacyTestHarness?: boolean; +} + +/** + * The LegacyPlatformService is responsible for initializing + * the legacy platform by injecting parts of the new platform + * services into the legacy platform modules, like ui/modules, + * and then bootstraping the ui/chrome or ui/test_harness to + * start either the app or browser tests. + */ +export class LegacyPlatformService { + constructor(private readonly params: LegacyPlatformParams) {} + + public start({ injectedMetadata }: Deps) { + // Inject parts of the new platform into parts of the legacy platform + // so that legacy APIs/modules can mimic their new platform counterparts + require('ui/metadata').__newPlatformInit__(injectedMetadata.getLegacyMetadata()); + + // Load the bootstrap module before loading the legacy platform files so that + // the bootstrap module can modify the environment a bit first + const bootstrapModule = this.loadBootstrapModule(); + + // require the files that will tie into the legacy platform + this.params.requireLegacyFiles(); + + bootstrapModule.bootstrap(this.params.rootDomElement); + } + + private loadBootstrapModule(): { + bootstrap: (rootDomElement: HTMLElement) => void; + } { + if (this.params.useLegacyTestHarness) { + // wrapped in NODE_ENV check so the `ui/test_harness` module + // is not included in the distributable + if (process.env.NODE_ENV !== 'production') { + return require('ui/test_harness'); + } + + throw new Error('tests bundle is not available in the distributable'); + } + + return require('ui/chrome'); + } +} diff --git a/src/core_plugins/console/public/tests/src/integration.test.js b/src/core_plugins/console/public/tests/src/integration.test.js index 126c01a9e14d..410e933dfafd 100644 --- a/src/core_plugins/console/public/tests/src/integration.test.js +++ b/src/core_plugins/console/public/tests/src/integration.test.js @@ -33,7 +33,7 @@ describe('Integration', () => { beforeEach(() => { // Set up our document body document.body.innerHTML = - '
'; + '
'; input = initializeInput( $('#editor'), diff --git a/src/core_plugins/state_session_storage_redirect/package.json b/src/core_plugins/state_session_storage_redirect/package.json index 7eedbddb0832..21956e5d76d5 100644 --- a/src/core_plugins/state_session_storage_redirect/package.json +++ b/src/core_plugins/state_session_storage_redirect/package.json @@ -1,5 +1,5 @@ { "name": "state_session_storage_redirect", "version": "kibana", - "description": "When using the state:storeInSessionStorage setting with the short-urls, we need some way to get the full URL's hashed states into sessionStorage, this app will grab the URL from the kbn-initial-state and and put the URL hashed states into sessionStorage before redirecting the user." + "description": "When using the state:storeInSessionStorage setting with the short-urls, we need some way to get the full URL's hashed states into sessionStorage, this app will grab the URL from the injected state and and put the URL hashed states into sessionStorage before redirecting the user." } diff --git a/src/core_plugins/tests_bundle/index.js b/src/core_plugins/tests_bundle/index.js index b18de4b0dc6d..b78995ecfe64 100644 --- a/src/core_plugins/tests_bundle/index.js +++ b/src/core_plugins/tests_bundle/index.js @@ -17,8 +17,6 @@ * under the License. */ -import { union } from 'lodash'; - import { fromRoot } from '../../utils'; import findSourceFiles from './find_source_files'; @@ -36,7 +34,7 @@ export default (kibana) => { uiExports: { async __bundleProvider__(kbnServer) { - let modules = []; + const modules = new Set(); const { config, @@ -67,7 +65,7 @@ export default (kibana) => { // add the modules from all of this plugins apps for (const app of uiApps) { if (app.getPluginId() === pluginId) { - modules = union(modules, app.getModules()); + modules.add(app.getMainModuleId()); } } @@ -76,7 +74,7 @@ export default (kibana) => { } else { // add the modules from all of the apps for (const app of uiApps) { - modules = union(modules, app.getModules()); + modules.add(app.getMainModuleId()); } for (const plugin of plugins) { @@ -85,7 +83,7 @@ export default (kibana) => { } const testFiles = await findSourceFiles(testGlobs); - for (const f of testFiles) modules.push(f); + for (const f of testFiles) modules.add(f); if (config.get('tests_bundle.instrument')) { uiBundles.addPostLoader({ @@ -97,7 +95,7 @@ export default (kibana) => { uiBundles.add({ id: 'tests', - modules, + modules: [...modules], template: createTestEntryTemplate(uiSettingDefaults), }); }, diff --git a/src/core_plugins/tests_bundle/tests_entry_template.js b/src/core_plugins/tests_bundle/tests_entry_template.js index b22e7995eb73..ad2c5b817924 100644 --- a/src/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/core_plugins/tests_bundle/tests_entry_template.js @@ -27,7 +27,15 @@ export const createTestEntryTemplate = (defaultUiSettings) => (bundle) => ` * */ -window.__KBN__ = { +// import global polyfills before everything else +import 'babel-polyfill'; +import 'custom-event-polyfill'; +import 'whatwg-fetch'; +import 'abortcontroller-polyfill'; + +import { CoreSystem } from '__kibanaCore__' + +const legacyMetadata = { version: '1.2.3', buildNum: 1234, vars: { @@ -62,7 +70,14 @@ window.__KBN__ = { } }; -require('ui/test_harness'); -${bundle.getRequires().join('\n')} -require('ui/test_harness').bootstrap(/* go! */); +new CoreSystem({ + injectedMetadata: { + legacyMetadata + }, + rootDomElement: document.body, + useLegacyTestHarness: true, + requireLegacyFiles: () => { + ${bundle.getRequires().join('\n ')} + } +}).start() `; diff --git a/src/dev/file.ts b/src/dev/file.ts index b55e97ef34f4..2e576a02ed79 100644 --- a/src/dev/file.ts +++ b/src/dev/file.ts @@ -17,7 +17,7 @@ * under the License. */ -import { dirname, extname, join, relative, resolve } from 'path'; +import { dirname, extname, join, relative, resolve, sep } from 'path'; import { REPO_ROOT } from './constants'; @@ -48,6 +48,10 @@ export class File { return this.ext === '.ts' || this.ext === '.tsx'; } + public isFixture() { + return this.relativePath.split(sep).includes('__fixtures__'); + } + public getRelativeParentDirs() { const parents: string[] = []; diff --git a/src/dev/tslint/pick_files_to_lint.ts b/src/dev/tslint/pick_files_to_lint.ts index 49f77f48230e..7dc27c6a4d13 100644 --- a/src/dev/tslint/pick_files_to_lint.ts +++ b/src/dev/tslint/pick_files_to_lint.ts @@ -22,5 +22,5 @@ import { ToolingLog } from '@kbn/dev-utils'; import { File } from '../file'; export function pickFilesToLint(log: ToolingLog, files: File[]) { - return files.filter(file => file.isTypescript()); + return files.filter(file => file.isTypescript() && !file.isFixture()); } diff --git a/src/optimize/index.js b/src/optimize/index.js index 4ed117730de3..8675a4fd2e45 100644 --- a/src/optimize/index.js +++ b/src/optimize/index.js @@ -42,12 +42,6 @@ export default async (kbnServer, server, config) => { basePublicPath: config.get('server.basePath') })); - await uiBundles.writeEntryFiles(); - - // Not all entry files produce a css asset. Ensuring they exist prevents - // an error from occurring when the file is missing. - await uiBundles.ensureStyleFiles(); - // in prod, only bundle when something is missing or invalid const reuseCache = config.get('optimize.useBundleCache') ? await uiBundles.areAllBundleCachesValid() @@ -62,6 +56,8 @@ export default async (kbnServer, server, config) => { return; } + await uiBundles.resetBundleDir(); + // only require the FsOptimizer when we need to const optimizer = new FsOptimizer({ uiBundles, diff --git a/src/optimize/watch/watch_optimizer.js b/src/optimize/watch/watch_optimizer.js index ea07025301ce..215361a8cbf5 100644 --- a/src/optimize/watch/watch_optimizer.js +++ b/src/optimize/watch/watch_optimizer.js @@ -45,10 +45,7 @@ export default class WatchOptimizer extends BaseOptimizer { // log status changes this.status$.subscribe(this.onStatusChangeHandler); - - await this.uiBundles.writeEntryFiles(); - await this.uiBundles.ensureStyleFiles(); - + await this.uiBundles.resetBundleDir(); await this.initCompiler(); this.compiler.plugin('watch-run', this.compilerRunStartHandler); diff --git a/src/ui/__tests__/ui_exports_replace_injected_vars.js b/src/ui/__tests__/ui_exports_replace_injected_vars.js index b6df18aef8b6..b7762ef104b9 100644 --- a/src/ui/__tests__/ui_exports_replace_injected_vars.js +++ b/src/ui/__tests__/ui_exports_replace_injected_vars.js @@ -29,8 +29,8 @@ import KbnServer from '../../server/kbn_server'; const getInjectedVarsFromResponse = (resp) => { const $ = cheerio.load(resp.payload); - const data = $('kbn-initial-state').attr('data'); - return JSON.parse(data).vars; + const data = $('kbn-injected-metadata').attr('data'); + return JSON.parse(data).legacyMetadata.vars; }; const injectReplacer = (kbnServer, replacer) => { diff --git a/src/ui/public/__tests__/metadata.js b/src/ui/public/__tests__/metadata.js index 13418d3e8b8c..9cb5841f0d40 100644 --- a/src/ui/public/__tests__/metadata.js +++ b/src/ui/public/__tests__/metadata.js @@ -20,13 +20,6 @@ import expect from 'expect.js'; import { metadata } from '../metadata'; describe('ui/metadata', () => { - - - it('is same data as window.__KBN__', () => { - expect(metadata.version).to.equal(window.__KBN__.version); - expect(metadata.vars.kbnIndex).to.equal(window.__KBN__.vars.kbnIndex); - }); - it('is immutable', () => { expect(() => metadata.foo = 'something').to.throw; expect(() => metadata.version = 'something').to.throw; diff --git a/src/ui/public/chrome/chrome.js b/src/ui/public/chrome/chrome.js index 54bf84fe126d..741d1eb629b6 100644 --- a/src/ui/public/chrome/chrome.js +++ b/src/ui/public/chrome/chrome.js @@ -21,13 +21,6 @@ import _ from 'lodash'; import angular from 'angular'; import { metadata } from '../metadata'; - -// Polyfills -import 'babel-polyfill'; -import 'whatwg-fetch'; -import 'custom-event-polyfill'; -import 'abortcontroller-polyfill'; - import '../state_management/global_state'; import '../config'; import '../notify'; @@ -79,7 +72,7 @@ themeApi(chrome, internals); translationsApi(chrome, internals); const waitForBootstrap = new Promise(resolve => { - chrome.bootstrap = function () { + chrome.bootstrap = function (targetDomElement) { // import chrome nav controls and hacks now so that they are executed after // everything else, can safely import the chrome, and interact with services // and such setup by all other modules @@ -87,8 +80,10 @@ const waitForBootstrap = new Promise(resolve => { require('uiExports/hacks'); chrome.setupAngular(); - angular.bootstrap(document.body, ['kibana']); - resolve(); + targetDomElement.setAttribute('id', 'kibana-body'); + targetDomElement.setAttribute('kbn-chrome', 'true'); + angular.bootstrap(targetDomElement, ['kibana']); + resolve(targetDomElement); }; }); @@ -107,10 +102,10 @@ const waitForBootstrap = new Promise(resolve => { * tests. Look into 'src/test_utils/public/stub_get_active_injector' for more information. */ chrome.dangerouslyGetActiveInjector = () => { - return waitForBootstrap.then(() => { - const $injector = angular.element(document.body).injector(); + return waitForBootstrap.then((targetDomElement) => { + const $injector = angular.element(targetDomElement).injector(); if (!$injector) { - return Promise.reject('document.body had no angular context after bootstrapping'); + return Promise.reject('targetDomElement had no angular context after bootstrapping'); } return $injector; }); diff --git a/src/ui/public/metadata.js b/src/ui/public/metadata.js index ed65aec83fed..6701475d947d 100644 --- a/src/ui/public/metadata.js +++ b/src/ui/public/metadata.js @@ -17,29 +17,12 @@ * under the License. */ -import $ from 'jquery'; -import _ from 'lodash'; +export let metadata = null; -export const metadata = deepFreeze(getState()); - -function deepFreeze(object) { - // for any properties that reference an object, makes sure that object is - // recursively frozen as well - Object.keys(object).forEach(key => { - const value = object[key]; - if (_.isObject(value)) { - deepFreeze(value); - } - }); - - return Object.freeze(object); -} - -function getState() { - const stateKey = '__KBN__'; - if (!(stateKey in window)) { - const state = $('kbn-initial-state').attr('data'); - window[stateKey] = JSON.parse(state); +export function __newPlatformInit__(legacyMetadata) { + if (metadata === null) { + metadata = legacyMetadata; + } else { + throw new Error('ui/metadata can only be initialized once'); } - return window[stateKey]; } diff --git a/src/ui/public/notify/__tests__/notifier.js b/src/ui/public/notify/__tests__/notifier.js index d05e48116bc9..da31599d3f63 100644 --- a/src/ui/public/notify/__tests__/notifier.js +++ b/src/ui/public/notify/__tests__/notifier.js @@ -22,13 +22,12 @@ import ngMock from 'ng_mock'; import expect from 'expect.js'; import sinon from 'sinon'; import { Notifier } from '..'; +import { metadata } from 'ui/metadata'; describe('Notifier', function () { let $interval; let notifier; let params; - const version = window.__KBN__.version; - const buildNum = window.__KBN__.buildNum; const message = 'Oh, the humanity!'; const customText = 'fooMarkup'; const customParams = { @@ -334,13 +333,13 @@ describe('Notifier', function () { describe('when version is configured', function () { it('adds version to notification', function () { const notification = notify(fnName); - expect(notification.info.version).to.equal(version); + expect(notification.info.version).to.equal(metadata.version); }); }); describe('when build number is configured', function () { it('adds buildNum to notification', function () { const notification = notify(fnName); - expect(notification.info.buildNum).to.equal(buildNum); + expect(notification.info.buildNum).to.equal(metadata.buildNum); }); }); } diff --git a/src/ui/public/test_harness/index.js b/src/ui/public/test_harness/index.js index beb22897d90d..d66a4b1d6721 100644 --- a/src/ui/public/test_harness/index.js +++ b/src/ui/public/test_harness/index.js @@ -17,6 +17,4 @@ * under the License. */ -import './test_harness'; - export { bootstrap } from './test_harness'; diff --git a/src/ui/public/test_harness/test_harness.js b/src/ui/public/test_harness/test_harness.js index 73acc1aa303e..3b5144145de6 100644 --- a/src/ui/public/test_harness/test_harness.js +++ b/src/ui/public/test_harness/test_harness.js @@ -41,9 +41,6 @@ if (query && query.mocha) { setupTestSharding(); -// allows test_harness.less to have higher priority selectors -document.body.setAttribute('id', 'test-harness-body'); - before(() => { // prevent accidental ajax requests sinon.useFakeXMLHttpRequest(); @@ -84,7 +81,10 @@ afterEach(function () { }); // Kick off mocha, called at the end of test entry files -export function bootstrap() { +export function bootstrap(targetDomElement) { + // allows test_harness.less to have higher priority selectors + targetDomElement.setAttribute('id', 'test-harness-body'); + // load the hacks since we aren't actually bootstrapping the // chrome, which is where the hacks would normally be loaded require('uiExports/hacks'); diff --git a/src/ui/ui_apps/__tests__/ui_app.js b/src/ui/ui_apps/__tests__/ui_app.js index 7d121087a2ec..87b57a556ca4 100644 --- a/src/ui/ui_apps/__tests__/ui_app.js +++ b/src/ui/ui_apps/__tests__/ui_app.js @@ -86,8 +86,8 @@ describe('ui apps / UiApp', () => { expect(app.getNavLink()).to.be.a(UiNavLink); }); - it('has an empty modules list', () => { - expect(app.getModules()).to.eql([]); + it('has no main module', () => { + expect(app.getMainModuleId()).to.be(undefined); }); it('has no styleSheetPath', () => { @@ -135,10 +135,8 @@ describe('ui apps / UiApp', () => { expect(app.getNavLink()).to.be(undefined); }); - it('includes main and hack modules', () => { - expect(app.getModules()).to.eql([ - 'main.js', - ]); + it('has a main module', () => { + expect(app.getMainModuleId()).to.be('main.js'); }); it('has spec values in JSON representation', () => { @@ -303,15 +301,15 @@ describe('ui apps / UiApp', () => { }); }); - describe('#getModules', () => { - it('returns empty array by default', () => { + describe('#getMainModuleId', () => { + it('returns undefined by default', () => { const app = createUiApp({ id: 'foo' }); - expect(app.getModules()).to.eql([]); + expect(app.getMainModuleId()).to.be(undefined); }); - it('returns main module if not using appExtensions', () => { + it('returns main module id', () => { const app = createUiApp({ id: 'foo', main: 'bar' }); - expect(app.getModules()).to.eql(['bar']); + expect(app.getMainModuleId()).to.be('bar'); }); }); diff --git a/src/ui/ui_apps/ui_app.js b/src/ui/ui_apps/ui_app.js index a6621bcf1408..2469f9717d63 100644 --- a/src/ui/ui_apps/ui_app.js +++ b/src/ui/ui_apps/ui_app.js @@ -101,8 +101,8 @@ export class UiApp { } } - getModules() { - return this._main ? [this._main] : []; + getMainModuleId() { + return this._main; } getStyleSheetUrlPath() { diff --git a/src/ui/ui_bundles/__tests__/app_entry_template.js b/src/ui/ui_bundles/__tests__/app_entry_template.js index c026c3b75905..79b57955ebd6 100644 --- a/src/ui/ui_bundles/__tests__/app_entry_template.js +++ b/src/ui/ui_bundles/__tests__/app_entry_template.js @@ -44,6 +44,6 @@ describe('ui bundles / appEntryTemplate', () => { 'baz' ]; bundle.getRequires.returns(requires); - expect(appEntryTemplate(bundle)).to.contain(requires.join('\n')); + expect(appEntryTemplate(bundle)).to.contain(requires.join('\n ')); }); }); diff --git a/src/ui/ui_bundles/app_entry_template.js b/src/ui/ui_bundles/app_entry_template.js index 4c9dca0a3368..758f73bf10ec 100644 --- a/src/ui/ui_bundles/app_entry_template.js +++ b/src/ui/ui_bundles/app_entry_template.js @@ -19,15 +19,26 @@ export const appEntryTemplate = (bundle) => ` /** - * Test entry file + * Kibana entry file * * This is programmatically created and updated, do not modify * * context: ${bundle.getContext()} */ -require('ui/chrome'); -${bundle.getRequires().join('\n')} -require('ui/chrome').bootstrap(/* xoxo */); +// import global polyfills before everything else +import 'babel-polyfill'; +import 'custom-event-polyfill'; +import 'whatwg-fetch'; +import 'abortcontroller-polyfill'; +import { CoreSystem } from '__kibanaCore__' + +new CoreSystem({ + injectedMetadata: JSON.parse(document.querySelector('kbn-injected-metadata').getAttribute('data')), + rootDomElement: document.body, + requireLegacyFiles: () => { + ${bundle.getRequires().join('\n ')} + } +}).start() `; diff --git a/src/ui/ui_bundles/ui_bundle.js b/src/ui/ui_bundles/ui_bundle.js index 34aeaa2a4399..200235af633a 100644 --- a/src/ui/ui_bundles/ui_bundle.js +++ b/src/ui/ui_bundles/ui_bundle.js @@ -18,7 +18,7 @@ */ import { fromNode as fcb } from 'bluebird'; -import { readFile, writeFile, unlink, stat } from 'fs'; +import { readFile, writeFile, stat } from 'fs'; // We normalize all path separators to `/` in generated files function normalizePath(path) { @@ -86,31 +86,31 @@ export class UiBundle { )); } - async hasStyleFile() { - return await fcb(cb => { - return stat(this.getStylePath(), error => { - cb(null, !(error && error.code === 'ENOENT')); - }); - }); - } - async touchStyleFile() { return await fcb(cb => ( writeFile(this.getStylePath(), '', 'utf8', cb) )); } - async clearBundleFile() { - try { - await fcb(cb => unlink(this.getOutputPath(), cb)); - } catch (e) { - return null; + /** + * Determine if the cache for this bundle is valid by + * checking that the entry file exists, has the content we + * expect based on the argument for this bundle, and that both + * the style file and output for this bundle exist. In this + * scenario we assume the cache is valid. + * + * When the `optimize.useBundleCache` config is set to `false` + * (the default when running in development) we don't even call + * this method and bundles are always recreated. + */ + async isCacheValid() { + if (await this.readEntryFile() !== this.renderContent()) { + return false; } - } - async isCacheValid() { try { await fcb(cb => stat(this.getOutputPath(), cb)); + await fcb(cb => stat(this.getStylePath(), cb)); return true; } catch (e) { diff --git a/src/ui/ui_bundles/ui_bundles_controller.js b/src/ui/ui_bundles/ui_bundles_controller.js index 3a4b1385da4a..887e5f028899 100644 --- a/src/ui/ui_bundles/ui_bundles_controller.js +++ b/src/ui/ui_bundles/ui_bundles_controller.js @@ -17,15 +17,20 @@ * under the License. */ -import { createHash } from 'crypto'; import { resolve } from 'path'; +import { createHash } from 'crypto'; +import { promisify } from 'util'; +import { existsSync } from 'fs'; -import { UiBundle } from './ui_bundle'; -import { fromNode as fcb } from 'bluebird'; +import del from 'del'; import { makeRe } from 'minimatch'; import mkdirp from 'mkdirp'; + +import { UiBundle } from './ui_bundle'; import { appEntryTemplate } from './app_entry_template'; +const mkdirpAsync = promisify(mkdirp); + function getWebpackAliases(pluginSpecs) { return pluginSpecs.reduce((aliases, spec) => { const publicDir = spec.getPublicDir(); @@ -74,10 +79,11 @@ export class UiBundlesController { this._postLoaders = []; this._bundles = []; + // create a bundle for each uiApp for (const uiApp of uiApps) { this.add({ id: uiApp.getId(), - modules: uiApp.getModules(), + modules: [uiApp.getMainModuleId()], template: appEntryTemplate, }); } @@ -140,58 +146,48 @@ export class UiBundlesController { return resolve(this._workingDir, ...args); } + async resetBundleDir() { + if (!existsSync(this._workingDir)) { + // create a fresh working directory + await mkdirpAsync(this._workingDir); + } else { + // delete all children of the working directory + await del(this.resolvePath('*')); + } + + // write the entry/style files for each bundle + for (const bundle of this._bundles) { + await bundle.writeEntryFile(); + await bundle.touchStyleFile(); + } + } + getCacheDirectory(...subPath) { return this.resolvePath('../.cache', this.hashBundleEntries(), ...subPath); } getDescription() { - switch (this._bundles.length) { + const ids = this.getIds(); + switch (ids.length) { case 0: return '0 bundles'; case 1: - return `bundle for ${this._bundles[0].getId()}`; + return `bundle for ${ids[0]}`; default: - const ids = this.getIds(); const last = ids.pop(); const commas = ids.join(', '); return `bundles for ${commas} and ${last}`; } } - async ensureDir() { - await fcb(cb => mkdirp(this._workingDir, cb)); - } - - async writeEntryFiles() { - await this.ensureDir(); - - for (const bundle of this._bundles) { - const existing = await bundle.readEntryFile(); - const expected = bundle.renderContent(); - - if (existing !== expected) { - await bundle.writeEntryFile(); - await bundle.clearBundleFile(); - } - } - } - - async ensureStyleFiles() { - await this.ensureDir(); - - for (const bundle of this._bundles) { - if (!await bundle.hasStyleFile()) { - await bundle.touchStyleFile(); - } - } - } - hashBundleEntries() { const hash = createHash('sha1'); + for (const bundle of this._bundles) { hash.update(`bundleEntryPath:${bundle.getEntryPath()}`); hash.update(`bundleEntryContent:${bundle.renderContent()}`); } + return hash.digest('hex'); } @@ -216,8 +212,4 @@ export class UiBundlesController { return this._bundles .map(bundle => bundle.getId()); } - - toJSON() { - return this._bundles; - } } diff --git a/src/ui/ui_exports/ui_export_defaults.js b/src/ui/ui_exports/ui_export_defaults.js index 9b505c49dbc4..d7af50448792 100644 --- a/src/ui/ui_exports/ui_export_defaults.js +++ b/src/ui/ui_exports/ui_export_defaults.js @@ -28,7 +28,7 @@ export const UI_EXPORT_DEFAULTS = { webpackAliases: { ui: resolve(ROOT, 'src/ui/public'), - ui_framework: resolve(ROOT, 'ui_framework'), + '__kibanaCore__$': resolve(ROOT, 'src/core/public'), test_harness: resolve(ROOT, 'src/test_harness/public'), querystring: 'querystring-browser', moment$: resolve(ROOT, 'webpackShims/moment'), diff --git a/src/ui/ui_render/ui_render_mixin.js b/src/ui/ui_render/ui_render_mixin.js index a22686f6d762..54f8b842fd5d 100644 --- a/src/ui/ui_render/ui_render_mixin.js +++ b/src/ui/ui_render/ui_render_mixin.js @@ -105,7 +105,7 @@ export function uiRenderMixin(kbnServer, server, config) { } }); - async function getKibanaPayload({ app, request, includeUserProvidedConfig, injectedVarsOverrides }) { + async function getLegacyKibanaPayload({ app, request, includeUserProvidedConfig, injectedVarsOverrides }) { const uiSettings = request.getUiSettingsService(); const translations = await request.getUiTranslations(); @@ -140,17 +140,21 @@ export function uiRenderMixin(kbnServer, server, config) { try { const request = reply.request; const translations = await request.getUiTranslations(); + const basePath = config.get('server.basePath'); return reply.view('ui_app', { - app, - kibanaPayload: await getKibanaPayload({ - app, - request, - includeUserProvidedConfig, - injectedVarsOverrides - }), - bundlePath: `${config.get('server.basePath')}/bundles`, + uiPublicUrl: `${basePath}/ui`, + bootstrapScriptUrl: `${basePath}/bundles/app/${app.getId()}/bootstrap.js`, i18n: key => get(translations, key, ''), + + injectedMetadata: { + legacyMetadata: await getLegacyKibanaPayload({ + app, + request, + includeUserProvidedConfig, + injectedVarsOverrides + }), + }, }); } catch (err) { reply(err); diff --git a/src/ui/ui_render/views/chrome.jade b/src/ui/ui_render/views/chrome.jade index b449c101b99e..28a4a680947e 100644 --- a/src/ui/ui_render/views/chrome.jade +++ b/src/ui/ui_render/views/chrome.jade @@ -1,6 +1,3 @@ -- - var appName = 'kibana'; - block vars doctype html @@ -16,63 +13,63 @@ html(lang='en') font-style: normal; font-weight: 300; src: local('Open Sans Light'), local('OpenSans-Light'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_300.woff2') format('woff2'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_300.woff') format('woff'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_300.ttf') format('truetype'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_300.svg#OpenSans') format('svg'); + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_300.woff2') format('woff2'), + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_300.woff') format('woff'), + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_300.ttf') format('truetype'), + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_300.svg#OpenSans') format('svg'); } @font-face { font-family: 'Open Sans'; font-style: normal; font-weight: 400; src: local('Open Sans'), local('OpenSans'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_regular.woff2') format('woff2'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_regular.woff') format('woff'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_regular.ttf') format('truetype'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_regular.svg#OpenSans') format('svg'); + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_regular.woff2') format('woff2'), + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_regular.woff') format('woff'), + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_regular.ttf') format('truetype'), + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_regular.svg#OpenSans') format('svg'); } @font-face { font-family: 'Open Sans'; font-style: normal; font-weight: 600; src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_600.woff2') format('woff2'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_600.woff') format('woff'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_600.ttf') format('truetype'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_600.svg#OpenSans') format('svg'); + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_600.woff2') format('woff2'), + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_600.woff') format('woff'), + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_600.ttf') format('truetype'), + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_600.svg#OpenSans') format('svg'); } @font-face { font-family: 'Open Sans'; font-style: normal; font-weight: 700; src: local('Open Sans Bold'), local('OpenSans-Bold'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_700.woff2') format('woff2'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_700.woff') format('woff'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_700.ttf') format('truetype'), - url('#{kibanaPayload.basePath}/ui/fonts/open_sans/open_sans_v15_latin_700.svg#OpenSans') format('svg'); + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_700.woff2') format('woff2'), + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_700.woff') format('woff'), + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_700.ttf') format('truetype'), + url('#{uiPublicUrl}/fonts/open_sans/open_sans_v15_latin_700.svg#OpenSans') format('svg'); } //- Favicons (generated from http://realfavicongenerator.net/) link( - rel='apple-touch-icon' sizes='180x180' href='#{kibanaPayload.basePath}/ui/favicons/apple-touch-icon.png' + rel='apple-touch-icon' sizes='180x180' href='#{uiPublicUrl}/favicons/apple-touch-icon.png' ) link( - rel='icon' type='image/png' href='#{kibanaPayload.basePath}/ui/favicons/favicon-32x32.png' sizes='32x32' + rel='icon' type='image/png' href='#{uiPublicUrl}/favicons/favicon-32x32.png' sizes='32x32' ) link( - rel='icon' type='image/png' href='#{kibanaPayload.basePath}/ui/favicons/favicon-16x16.png' sizes='16x16' + rel='icon' type='image/png' href='#{uiPublicUrl}/favicons/favicon-16x16.png' sizes='16x16' ) link( - rel='manifest' href='#{kibanaPayload.basePath}/ui/favicons/manifest.json' + rel='manifest' href='#{uiPublicUrl}/favicons/manifest.json' ) link( - rel='mask-icon' href='#{kibanaPayload.basePath}/ui/favicons/safari-pinned-tab.svg' color='#e8488b' + rel='mask-icon' href='#{uiPublicUrl}/favicons/safari-pinned-tab.svg' color='#e8488b' ) link( - rel='shortcut icon' href='#{kibanaPayload.basePath}/ui/favicons/favicon.ico' + rel='shortcut icon' href='#{uiPublicUrl}/favicons/favicon.ico' ) meta( - name='msapplication-config' content='#{kibanaPayload.basePath}/ui/favicons/browserconfig.xml' + name='msapplication-config' content='#{uiPublicUrl}/favicons/browserconfig.xml' ) meta( name='theme-color' content='#ffffff' @@ -120,6 +117,6 @@ html(lang='en') //- good because we may use them to override EUI styles. style#themeCss - body(kbn-chrome, id='#{appName}-body') - kbn-initial-state(data=JSON.stringify(kibanaPayload)) + body + kbn-injected-metadata(data=JSON.stringify(injectedMetadata)) block content diff --git a/src/ui/ui_render/views/ui_app.jade b/src/ui/ui_render/views/ui_app.jade index 38942e7bf1ee..0726dd785dfd 100644 --- a/src/ui/ui_render/views/ui_app.jade +++ b/src/ui/ui_render/views/ui_app.jade @@ -110,4 +110,4 @@ block content .kibanaWelcomeText | #{i18n('UI-WELCOME_MESSAGE')} - script(src='#{bundlePath}/app/#{app.getId()}/bootstrap.js') + script(src=bootstrapScriptUrl) diff --git a/tsconfig.json b/tsconfig.json index 514932cdfbaf..663fd48239dd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -46,5 +46,8 @@ }, "include": [ "src/**/*" + ], + "exclude": [ + "src/**/__fixtures__/**/*" ] }