Skip to content

Commit

Permalink
refactor(tests): Use goog.bootstrap to allow loading ES modules in …
Browse files Browse the repository at this point in the history
…playground (google#5931)

* chore(deps): Update closure/goog/base.js, add goog.js

  * Update base.js from the latest version (20220104.0.0).
  * Also copy over goog.js, which provides access to asuitable subset
    of goog.* via an importable module).

* refactor(tests): Have playground.html load Blockly as a module

  N.B.:

  * We still need a preparation step, in order to load base.js and
    deps.js via <script> tags in uncompiled mode; in compiled mode
    it will instead load all the *_compressed.js files via <script>
    tags.

    Acess to the Blockly object is via:

        import Blockly from './playgrounds/blockly.mjs';

    (N.B: no "* as", since blockly.mjs has only a default export.)

  * There remain two serious defects when running in uncompiled mode:
    * It does not attempt to load msg/messages.js, causing startup to
      fail.
    * Module loading only works if there are no ES Modules; if there
      are, something goes wrong with base.js's attempt to sequence
      module loads causing goog.modules that import ES modules to get
      a null exports object for that import.  X-(

* fix(tests): Have playground.html load messages.js before generators

  This fixes the issue caused by missing messages when loading
  the generators.

* fix(tests): Move bootsrap calls to prepare.js

  Move the calls to goog.bootstrap from blockly.mjs to prepare.mjs.
  This is needed to work around a bug in the Cosure Library debug
  loader (google/closure-library#1152).

  This gets a bit ugly because most of the code has to go in a
  <script> (because it needs goog.bootstrap, which was loaded by
  an earlier <script> tag).

* fix(documentation): Minor comment corrections for PR google#5931
  • Loading branch information
cpcallen authored Feb 23, 2022
1 parent 4e64d0e commit b1f0a6a
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 43 deletions.
51 changes: 14 additions & 37 deletions closure/goog/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,14 @@ goog.global.CLOSURE_UNCOMPILED_DEFINES;
* var CLOSURE_DEFINES = {'goog.DEBUG': false} ;
* </pre>
*
* @type {Object<string, (string|number|boolean)>|undefined}
* Currently the Closure Compiler will only recognize very simple definitions of
* this value when looking for values to apply to compiled code and ignore all
* other references. Specifically, it looks the value defined at the variable
* declaration, as with the example above.
*
* TODO(user): Improve the recognized definitions.
*
* @type {!Object<string, (string|number|boolean)>|null|undefined}
*/
goog.global.CLOSURE_DEFINES;

Expand Down Expand Up @@ -3175,23 +3182,10 @@ if (!COMPILED && goog.DEPENDENCIES_ENABLED) {
scriptEl.nonce = nonce;
}

if (goog.DebugLoader_.IS_OLD_IE_) {
// Execution order is not guaranteed on old IE, halt loading and write
// these scripts one at a time, after each loads.
controller.pause();
scriptEl.onreadystatechange = function() {
if (scriptEl.readyState == 'loaded' ||
scriptEl.readyState == 'complete') {
controller.loaded();
controller.resume();
}
};
} else {
scriptEl.onload = function() {
scriptEl.onload = null;
controller.loaded();
};
}
scriptEl.onload = function() {
scriptEl.onload = null;
controller.loaded();
};

scriptEl.src = goog.TRUSTED_TYPES_POLICY_ ?
goog.TRUSTED_TYPES_POLICY_.createScriptURL(this.path) :
Expand Down Expand Up @@ -3502,13 +3496,6 @@ if (!COMPILED && goog.DEPENDENCIES_ENABLED) {
// If one thing is pending it is this.
var anythingElsePending = controller.pending().length > 1;

// If anything else is loading we need to lazy load due to bugs in old IE.
// Specifically script tags with src and script tags with contents could
// execute out of order if document.write is used, so we cannot use
// document.write. Do not pause here; it breaks old IE as well.
var useOldIeWorkAround =
anythingElsePending && goog.DebugLoader_.IS_OLD_IE_;

// Additionally if we are meant to defer scripts but the page is still
// loading (e.g. an ES6 module is loading) then also defer. Or if we are
// meant to defer and anything else is pending then defer (those may be
Expand All @@ -3517,7 +3504,7 @@ if (!COMPILED && goog.DEPENDENCIES_ENABLED) {
var needsAsyncLoading = goog.Dependency.defer_ &&
(anythingElsePending || goog.isDocumentLoading_());

if (useOldIeWorkAround || needsAsyncLoading) {
if (needsAsyncLoading) {
// Note that we only defer when we have to rather than 100% of the time.
// Always defering would work, but then in theory the order of
// goog.require calls would then matter. We want to enforce that most of
Expand Down Expand Up @@ -3561,8 +3548,7 @@ if (!COMPILED && goog.DEPENDENCIES_ENABLED) {
};
} else {
// Always eval on old IE.
if (goog.DebugLoader_.IS_OLD_IE_ || !goog.inHtmlDocument_() ||
!goog.isDocumentLoading_()) {
if (!goog.inHtmlDocument_() || !goog.isDocumentLoading_()) {
load();
} else {
fetchInOwnScriptThenLoad();
Expand Down Expand Up @@ -3706,15 +3692,6 @@ if (!COMPILED && goog.DEPENDENCIES_ENABLED) {
};


/**
* Whether the browser is IE9 or earlier, which needs special handling
* for deferred modules.
* @const @private {boolean}
*/
goog.DebugLoader_.IS_OLD_IE_ = !!(
!goog.global.atob && goog.global.document && goog.global.document['all']);


/**
* @param {string} relPath
* @param {!Array<string>|undefined} provides
Expand Down
99 changes: 99 additions & 0 deletions closure/goog/goog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2018 The Closure Library 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.

/**
* @fileoverview ES6 module that exports symbols from base.js so that ES6
* modules do not need to use globals and so that is clear if a project is using
* Closure's base.js file. It is also a subset of properties in base.js, meaning
* it should be clearer what should not be used in ES6 modules
* (goog.module/provide are not exported here, for example). Though that is not
* to say that everything in this file should be used in an ES6 module; some
* depreciated functions are exported to make migration easier (e.g.
* goog.scope).
*
* Note that this does not load Closure's base.js file, it is still up to the
* programmer to include it. Nor does the fact that this is an ES6 module mean
* that projects no longer require deps.js files for debug loading - they do.
* Closure will need to load your ES6 modules for you if you have any Closure
* file (goog.provide/goog.module) dependencies, as they need to be available
* before the ES6 module evaluates.
*
* Also note that this file has special compiler handling! It is okay to export
* anything from this file, but the name also needs to exist on the global goog.
* This special compiler pass enforces that you always import this file as
* `import * as goog`, as many tools use regex based parsing to find
* goog.require calls.
*/

export const global = goog.global;
export const require = goog.require;
export const define = goog.define;
export const DEBUG = goog.DEBUG;
export const LOCALE = goog.LOCALE;
export const TRUSTED_SITE = goog.TRUSTED_SITE;
export const DISALLOW_TEST_ONLY_CODE = goog.DISALLOW_TEST_ONLY_CODE;
export const getGoogModule = goog.module.get;
export const setTestOnly = goog.setTestOnly;
export const forwardDeclare = goog.forwardDeclare;
export const getObjectByName = goog.getObjectByName;
export const basePath = goog.basePath;
export const addSingletonGetter = goog.addSingletonGetter;
export const typeOf = goog.typeOf;
export const isArrayLike = goog.isArrayLike;
export const isDateLike = goog.isDateLike;
export const isObject = goog.isObject;
export const getUid = goog.getUid;
export const hasUid = goog.hasUid;
export const removeUid = goog.removeUid;
export const mixin = goog.mixin;
export const now = Date.now;
export const globalEval = goog.globalEval;
export const getCssName = goog.getCssName;
export const setCssNameMapping = goog.setCssNameMapping;
export const getMsg = goog.getMsg;
export const getMsgWithFallback = goog.getMsgWithFallback;
export const exportSymbol = goog.exportSymbol;
export const exportProperty = goog.exportProperty;
export const nullFunction = goog.nullFunction;
export const abstractMethod = goog.abstractMethod;
export const cloneObject = goog.cloneObject;
export const bind = goog.bind;
export const partial = goog.partial;
export const inherits = goog.inherits;
export const scope = goog.scope;
export const defineClass = goog.defineClass;
export const declareModuleId = goog.declareModuleId;

// Export select properties of module. Do not export the function itself or
// goog.module.declareLegacyNamespace.
export const module = {
get: goog.module.get,
};

// Omissions include:
// goog.ENABLE_DEBUG_LOADER - define only used in base.
// goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING - define only used in base.
// goog.provide - ES6 modules do not provide anything.
// goog.module - ES6 modules cannot be goog.modules.
// goog.module.declareLegacyNamespace - ES6 modules cannot declare namespaces.
// goog.addDependency - meant to only be used by dependency files.
// goog.DEPENDENCIES_ENABLED - constant only used in base.
// goog.TRANSPILE - define only used in base.
// goog.TRANSPILER - define only used in base.
// goog.loadModule - should not be called by any ES6 module; exists for
// generated bundles.
// goog.LOAD_MODULE_USING_EVAL - define only used in base.
// goog.SEAL_MODULE_EXPORTS - define only used in base.
// goog.DebugLoader - used rarely, only outside of compiled code.
// goog.Transpiler - used rarely, only outside of compiled code.
1 change: 1 addition & 0 deletions tests/deps.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 12 additions & 6 deletions tests/playground.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
<title>Blockly Playground</title>

<!-- This script loads uncompressed when on localhost and loads compressed when it is being hosted.
Please add any other dependencies to playgrounds/load_all.js.-->
<script src="playgrounds/load_all.js"></script>
<script>
'use strict';
Please add any other dependencies to playgrounds/prepare.js.-->
<script src="playgrounds/prepare.js"></script>
<script type=module>
import Blockly from './playgrounds/blockly.mjs';

const IS_UNCOMPRESSED = !!document.getElementById('blockly-uncompressed-script');
const IS_UNCOMPRESSED = Boolean(window.BlocklyLoader); // See prepare.js
var workspace = null;

function start() {
Expand Down Expand Up @@ -381,6 +381,12 @@
'next': { }
});

// Call start(). Because this <script> has type=module, it is
// automatically deferred, so it will not be run until after the
// document has been parsed, but before firing DOMContentLoaded.

start();

</script>

<style>
Expand Down Expand Up @@ -430,7 +436,7 @@
}
</style>
</head>
<body onload="start()">
<body>

<div id="blocklyDiv"></div>

Expand Down
38 changes: 38 additions & 0 deletions tests/playgrounds/blockly.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* @fileoverview Finishes loading Blockly and exports it as this
* module's default export.
*
* It is exported as the default export to avoid having to
* re-export each property on Blockly individually, because you
* can't do:
*
* export * from <dynamically computed source>; // SYNTAX ERROR
*
* You must use a <script> tag to load prepare.js first, before
* importing this module in a <script type=module> to obtain the
* loaded value.
*
* See tests/playground.html for example usage.
*/

let Blockly;

if (window.BlocklyLoader) {
// Uncompiled mode. Use top-level await
// (https://v8.dev/features/top-level-await) to block loading of
// this module until goog.bootstrap()ping of Blockly is finished.
Blockly = await window.BlocklyLoader;
} else if (window.Blockly) {
// Compiled mode. Retrieve the pre-installed Blockly global.
Blockly = globalThis.Blockly;
} else {
throw new Error('neither window.Blockly nor window.BlocklyLoader found');
}

export default Blockly;
85 changes: 85 additions & 0 deletions tests/playgrounds/prepare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* @fileoverview Load this file in a <script> tag to prepare for
* importing Blockly into a web page.
*
* You must use a <script> tag to load this script first, then
* import blockly.mjs in a <script type=module> to obtain the
* loaded value.
*
* See tests/playground.html for example usage.
*/
'use strict';

(function() {
// Decide whether we can load Blockly uncompiled, or must load the
// compiled version. Please see issue #5557 for more information.
const isIe = navigator.userAgent.indexOf('MSIE') !== -1 ||
navigator.appVersion.indexOf('Trident/') > -1;
const localhosts = ['localhost', '127.0.0.1', '[::1]'];

if (localhosts.includes(location.hostname) && !isIe) {
// We can load Blockly in uncompiled mode.

// Disable loading of closure/goog/deps.js (which doesn't exist).
window.CLOSURE_NO_DEPS = true;
// Load the Closure Library's base.js (the only part of the
// libary we use, mainly for goog.require / goog.provide /
// goog.module).
document.write('<script src="../../closure/goog/base.js"></script>');
// Load dependency graph info from test/deps.js. To update
// deps.js, run `npm run build:deps`.
document.write('<script src="../../tests/deps.js"></script>');

// Msg loading kludge. This should go away once #5409 and/or
// #1895 are fixed.

// Load messages into a temporary Blockly.Msg object, deleting it
// afterwards (after saving the messages!)
window.Blockly = {Msg: Object.create(null)};
document.write('<script src="../../msg/messages.js"></script>');
document.write(`
<script>
window.BlocklyMsg = window.Blockly.Msg;
delete window.Blockly;
</script>`);

document.write(`
<script>
window.BlocklyLoader = new Promise((resolve, reject) => {
goog.bootstrap(
[
'Blockly',
'Blockly.blocks.all',
'Blockly.Dart.all',
'Blockly.JavaScript.all',
'Blockly.Lua.all',
'Blockly.PHP.all',
'Blockly.Python.all',
], resolve);
}).then(() => {
// Copy Messages from temporary Blockly.Msg object to the real one:
Object.assign(goog.module.get('Blockly').Msg, window.BlocklyMsg);
}).then(() => {
return goog.module.get('Blockly');
});
</script>`);
} else {
// We need to load Blockly in compiled mode.

// Load blockly_compressed.js et al. using <script> tags.
document.write('<script src="../../blockly_compressed.js"></script>');
document.write('<script src="../../dart_compressed.js"></script>');
document.write('<script src="../../javascript_compressed.js"></script>');
document.write('<script src="../../lua_compressed.js"></script>');
document.write('<script src="../../php_compressed.js"></script>');
document.write('<script src="../../python_compressed.js"></script>');
document.write('<script src="../../blocks_compressed.js"></script>');
document.write('<script src="../../msg/messages.js"></script>');
}
})();

0 comments on commit b1f0a6a

Please sign in to comment.