diff --git a/package.json b/package.json index a59294a6c1..1c5b275408 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/chai": "^4.2.7", "@types/jasmine": "^3.5.0", "@types/node": "^13.1.2", + "@types/requirejs": "^2.1.31", "@types/sinon": "^7.5.1", "chai": "^4.2.0", "prettier": "1.18.2", @@ -60,6 +61,9 @@ "@angular/router": "^8.2.14", "@ngrx/effects": "^8.6.0", "@ngrx/store": "^8.6.0", + "monaco-editor-core": "^0.20.0", + "monaco-languages": "^1.10.0", + "requirejs": "^2.3.6", "rxjs": "^6.5.4", "zone.js": "^0.9.1" } diff --git a/tensorboard/BUILD b/tensorboard/BUILD index 8c5dec4463..88f764d476 100644 --- a/tensorboard/BUILD +++ b/tensorboard/BUILD @@ -285,6 +285,11 @@ tensorboard_zip_file( deps = [":assets"], ) +monaco_imports = [ + "//tensorboard/components/tf_imports:monaco_editor", + "//tensorboard/components/tf_imports:monaco_languages", +] + # TODO(stephanwlee): add ng_index.html to the srcs when it is ready for mass consumption. tf_web_library( name = "assets", @@ -302,7 +307,10 @@ tf_web_library( path = "/", deps = [ "@com_google_fonts_roboto", - ], + ] + select({ + "//tensorboard:dev_build": monaco_imports, + "//conditions:default": [], + }), ) py_library( diff --git a/tensorboard/components/tf_imports/BUILD b/tensorboard/components/tf_imports/BUILD index 75300f8102..1c6304f1b3 100644 --- a/tensorboard/components/tf_imports/BUILD +++ b/tensorboard/components/tf_imports/BUILD @@ -586,3 +586,36 @@ genrule( outs = ["d3-transition.d.ts"], cmd = "sed '/^declare module/d' $< | awk '/^}$$/ && !p {p++;next}1' >$@", ) + +tf_web_library( + name = "requirejs", + srcs = [ + "@npm//:node_modules/requirejs/require.js", + ], + path = "/tf-imports", + strip_prefix = "node_modules/requirejs", +) + +tf_web_library( + name = "monaco_editor", + srcs = [ + "@npm//:node_modules/monaco-editor-core/dev/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf", + "@npm//:node_modules/monaco-editor-core/dev/vs/base/worker/workerMain.js", + "@npm//:node_modules/monaco-editor-core/dev/vs/editor/editor.main.css", + "@npm//:node_modules/monaco-editor-core/dev/vs/editor/editor.main.js", + "@npm//:node_modules/monaco-editor-core/dev/vs/editor/editor.main.nls.js", + ], + path = "/tf-imports", + strip_prefix = "node_modules/monaco-editor-core/dev", +) + +tf_web_library( + name = "monaco_languages", + srcs = [ + "@npm//:node_modules/monaco-languages/release/dev/_.contribution.js", + "@npm//:node_modules/monaco-languages/release/dev/python/python.contribution.js", + "@npm//:node_modules/monaco-languages/release/dev/python/python.js", + ], + path = "/tf-imports/vs", + strip_prefix = "node_modules/monaco-languages/release/dev", +) diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/BUILD b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/BUILD index 6031dc90ea..2afa322003 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/BUILD +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/BUILD @@ -74,6 +74,7 @@ tf_ng_web_test_suite( "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/effects:debugger_effects_test_lib", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store:debugger_store_test_lib", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts:alerts_container_test_lib", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code:source_code_test_lib", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline:timeline_test", ], ) diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/BUILD b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/BUILD new file mode 100644 index 0000000000..a09fcaab81 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/BUILD @@ -0,0 +1,29 @@ +package(default_visibility = ["//tensorboard:internal"]) + +load("//tensorboard/defs:defs.bzl", "tf_ts_library") + +licenses(["notice"]) # Apache 2.0 + +tf_ts_library( + name = "load_monaco", + srcs = [ + "load_monaco_shim.ts", + ], + deps = [ + "@npm//@types/requirejs", + ], +) + +tf_ts_library( + name = "source_code_test_lib", + testonly = True, + srcs = [ + "load_monaco_shim_test.ts", + ], + tsconfig = "//:tsconfig-test", + deps = [ + ":load_monaco", + "@npm//@types/jasmine", + "@npm//@types/requirejs", + ], +) diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/load_monaco_shim.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/load_monaco_shim.ts new file mode 100644 index 0000000000..0ca95df470 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/load_monaco_shim.ts @@ -0,0 +1,72 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed 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. +==============================================================================*/ +/** + * Shim for the `loadMonaco()` function in different build environments. + * + * This file exports the version of `loadMonaco()` appropriate for the + * open-source environment. + */ + +// TODO(cais): Explore better typing by depending on external libraries. +export interface WindowWithRequireAndMonaco extends Window { + require?: Require; + monaco?: any; +} +export const windowWithRequireAndMonaco: WindowWithRequireAndMonaco = window; + +const MONACO_PATH_PREFIX = 'vs'; +const MONACO_IMPORT_PATH = '/tf-imports/vs'; + +/** + * require.js's require() wrapped as an async function that returns a Promise. + * + * This wrapped version does not support callback-function arguments. + * + * @param paths + */ +function requireAsPromise(paths: string[]): Promise { + const require = windowWithRequireAndMonaco.require!; + return new Promise((resolve) => { + require(paths, resolve); + }); +} + +/** + * If `window.monaco` is undefined, load the monaco-editor API object onto that + * global path dynamically using require.js. If `window.monaco` is already + * defined, this function is a no-op. + */ +export async function loadMonaco(): Promise { + if (windowWithRequireAndMonaco.monaco !== undefined) { + return; + } + + if (windowWithRequireAndMonaco.require) { + const require = windowWithRequireAndMonaco.require; + require.config({ + paths: { + [MONACO_PATH_PREFIX]: MONACO_IMPORT_PATH, + }, + }); + await requireAsPromise([`${MONACO_PATH_PREFIX}/editor/editor.main`]); + await requireAsPromise([ + `${MONACO_PATH_PREFIX}/python/python.contribution`, + ]); + } else { + throw new Error( + 'loadMonaco() failed because function require() is unavailable' + ); + } +} diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/load_monaco_shim_test.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/load_monaco_shim_test.ts new file mode 100644 index 0000000000..70805b7eb3 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/load_monaco_shim_test.ts @@ -0,0 +1,72 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed 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 {loadMonaco, windowWithRequireAndMonaco} from './load_monaco_shim'; + +describe('loadMonaco shim', () => { + function createFakeRequire(): Require { + let require = ((modules: string[], callback: Function) => { + callback(); + }) as any; + return Object.assign(require, { + config: () => {}, + toUrl: () => {}, + defined: () => {}, + specified: () => {}, + onError: () => {}, + undef: () => {}, + onResourceLoad: () => {}, + }); + } + + // TODO(cais): Explore better typing by depending on external libraries. + function createFakeMonaco() { + return {}; + } + + let requireSpy: jasmine.Spy; + beforeEach(() => { + windowWithRequireAndMonaco.require = createFakeRequire(); + requireSpy = spyOn(windowWithRequireAndMonaco, 'require').and.callThrough(); + }); + + afterEach(() => { + delete windowWithRequireAndMonaco.require; + delete windowWithRequireAndMonaco.monaco; + }); + + it('async function returns without error', async () => { + await loadMonaco(); + expect(requireSpy).toHaveBeenCalled(); + }); + + it('does not reload monaco module if already loaded', async () => { + windowWithRequireAndMonaco.monaco = createFakeMonaco(); + await loadMonaco(); + expect(requireSpy).not.toHaveBeenCalled(); + }); + + it('rejects if require.js is unavailable', async (done) => { + delete windowWithRequireAndMonaco.require; + // TODO(cais): Use async matchers such as toBeRejectedWithError once they + // are available. + try { + await loadMonaco(); + done.fail(); + } catch (e) { + done(); + } + }); +}); diff --git a/yarn.lock b/yarn.lock index 7246866b01..d30f945922 100644 --- a/yarn.lock +++ b/yarn.lock @@ -465,6 +465,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.17.tgz#7a183163a9e6ff720d86502db23ba4aade5999b8" integrity sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q== +"@types/requirejs@^2.1.31": + version "2.1.31" + resolved "https://registry.yarnpkg.com/@types/requirejs/-/requirejs-2.1.31.tgz#a24eaa0ee4f6b84feb8f521ca6550d48490b2bc6" + integrity sha512-b2soeyuU76rMbcRJ4e0hEl0tbMhFwZeTC0VZnfuWlfGlk6BwWNsev6kFu/twKABPX29wkX84wU2o+cEJoXsiTw== + "@types/sinon@^7.5.1": version "7.5.2" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.2.tgz#5e2f1d120f07b9cda07e5dedd4f3bf8888fccdb9" @@ -2858,6 +2863,16 @@ mkdirp@^0.5.0, mkdirp@^0.5.1: dependencies: minimist "^1.2.5" +monaco-editor-core@^0.20.0: + version "0.20.0" + resolved "https://registry.yarnpkg.com/monaco-editor-core/-/monaco-editor-core-0.20.0.tgz#d5ce01307d298dbca6ab9194812812b32b50433f" + integrity sha512-4mdmfEejTvRZzrEIn70jqqNl3g15vnkRdTkJ8uMK4jiljntlwhiSc5vknZOLt1QM8za16C3tDrSl2mTL9ma2Sg== + +monaco-languages@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/monaco-languages/-/monaco-languages-1.10.0.tgz#1e1b0f2b02c8c311b9db1ddb83f5c654f2f92fe1" + integrity sha512-ARAws17Xh0K4WsZYkJY6CqHn9EYdYN8CjzK6w/jgXIwU0owzCdUWxzu+FNJ/LeDLcKxL/YK3phcwGFj9IqX2yw== + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -3714,6 +3729,11 @@ requirejs@2.3.5: resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.5.tgz#617b9acbbcb336540ef4914d790323a8d4b861b0" integrity sha512-svnO+aNcR/an9Dpi44C7KSAy5fFGLtmPbaaCeQaklUz8BQhS64tWWIIlvEA5jrWICzlO/X9KSzSeXFnZdBu8nw== +requirejs@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9" + integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg== + requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"