-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate Blockly from goog.provide to goog.module #5026
Comments
A question came up in chat:
Answer: yes, in general the module name should be the same as the formerly-provided name. Possible exceptions:
I think such cases will be rare, and changes like this will require changing the code at any place the module is imported, so are probably best left for a second pass. |
There have been a few cases come up where there was uncertainty about what should be exported an how. One common situation that has caused doubt is where a module provides a class constructor as well as some other dubiously-related items exported via static properties on that constructor. For instance, from #5047, goog.provide('Blockly.utils.Coordinate');
Blockly.utils.Coordinate = function(x, y) { /* omitted */};
/**
* Compares coordinates for equality.
* @param {?Blockly.utils.Coordinate} a A Coordinate.
* @param {?Blockly.utils.Coordinate} b A Coordinate.
* @return {boolean} True iff the coordinates are equal, or if both are null.
*/
Blockly.utils.Coordinate.equals = function(a, b) { /* omitted */ };
Blockly.utils.Coordinate.distance = function(a, b) {
… In this case
It is therefore totally appropriate to leave goog.module('Blockly.utils.Coordinate');
goog.module.declareLegacyNamespace();
const Coordinate = function(x, y) { /* omitted */ };
Coordinate.equals = function(a, b) { /* omitted */ };
Coordinate.distance = function(a, b) { /* omitted */ };
// etc.
exports = Coordinate; A good clue that this situation applies is when the package path ends with a capitalised identifier ("….Coordinate")—though there will doubtless be cases (including "Blockly") where things have been misnamed. A related situation are enums, where one might quite reasonably have e.g. goog.provides('TrainStatus');
/** @enum {number} */
const TrainStatus = {
early: 0;
onTime: 1;
late: 2;
}
exports = TrainStatus; From the point of view of code importing this module it could just as easily have been written: goog.provides('trainStatus');
const EARLY = 0;
const ON_TIME = 1;
const LATE = 2;
exports = {EARLY, ON_TIME, LATE}; Except now there's nowhere to put the On the other hand: if you find a module which exports a class constructor that has some properties on it that do not seem to be too closely related to the class—for instance, something like a Please add any such files to the appropriate list in issue #5073 (where the "triage" list is the appropriate list if you're not sure!) |
@cpcallen and @rachel-fenichel can we close this issue? I think the only piece left is converting the tests to goog.module. Which I believe we decided does not need to be done in any hurry. |
Closing this issue since all of the remaining work is being tracked in other issues. |
This is a tracking bug for the work the Blockly team is doing in Q3 2021 to transition the codebase from
goog.provide
togoog.module
, as recently announced.This bug will track any outstanding tasks not covered by other more specific bugs, provide documentation for how the migration will be done and so on.
If you have questions about why we are doing this work and what the consequences will be for developers who use Blockly please ask them in the Blockly forum rather than commenting on this bug.
This work will be carried out on the
goog_module
branch. When working on migration, please branch from and submit PRs to merge back togoog_module
, notdevelop
.Module Conversion Guide
Most of the work required to migrate from
goog.provide
togoog.module
can be done on a module-by-module (and often file-by-file) basis.The end goal is that each
.js
file will contain a single module, with a clearly defined interface, that can be imported independently of any other modules—except of course for that module's own dependencies, which it will import for its own internal use. Modules should normally only reexport identifiers from other modules when they are aggregating a number of submodules together (possibly for legacy compatibility reasons).Since we're visiting every file in the project, we're also going to use this opportunity to begin the migration to ES6 and the new JavaScript styleguide.
Converting a single
.js
fileThere are several mostly-independent steps needed to convert a single
.js
source file. In most cases it will be preferable to start conversion with leaf modules—i.e., ones which do notrequire
any others—but within any given module that does have imports it will probably be easier to start by dealing with itsgoog.require
s before dealing with itsgoog.provide
s.Where possible you should present the conversion of a single
.js
file as a pull against thegoog_module
branch, consisting of four commits, as follows:goog.provide
statement asgoog.module
and explicitly enumerate exports.goog.requires
statements and add missing requires.clang-format
on the whole file.In situations where step 2 requires combining or splitting several related
.js
files (see details below) then it is appropriate that they be presented together in a single PR.1. Update the file to ES6 const/let
[Details TBC but the plan is to use an automated tool convert all
var
s tolet
orconst
. We are not proposing to convert ES5 classes to ES6class
syntax at the present time.]Suggested commit message:
Migrate path/to/filename.js to ES6 const/let
Or use
scripts/goog_module/convert-file.sh -c 1 path/to/filename.js
.(If you / your IDE does more than just
const
/let
then adjust message accordingly.)2. Rewrite the
goog.provide
statement asgoog.module
and explicitly enumerate exportsThis is the larger part of the work and will require probably require the most thought.
When converted:
Each
.js
file should begin with (and contain in total) a singlegoog.module
statement. When loaded, the entire file will be evaluated in a local scope, as if it were surrounded by an IIFE:Any other module importing this one will get the return value of this IIFE (i.e., the value of
exports
) as the return value ofgoog.require()
—but note that the module is evaluated only once, and the return value is cached by the module loader.The
goog.module
statement should be followed immediately by a call to declareLegacyNamespace:This is a transition measure which has the effect of causing any symbols exported by this module to be accessible from the global scope by the same paths as they were previously—i.e., if module
foo.bar
exportsbaz
then unmigrated code can still access it by:Anything that the module intends to export (classes, enums, functions, constants, and other symbols) should be named via a property on the (module-local)
exports
variable—in most cases after being declared as a top-levelconst
,class
orfunction
. Note that you will need to change any internal references as well. E.g.:becomes:
We've decided to prefer
const funcName = function() {...
tofunction funcName() {...
because it prevents accidental redefinition of the symbol and it is more consistent with how other module-local identifiers are declared.In cases where when the module contains a single class you can (and for compatibility in most cases should) just export that class directly:
or, equivalently:
Any top-level
@private
or otherwise internal identifiers should become top-level, module-local identifiers; they should not normally get marked@private
nor do their names end with underscore—but where it is necessary to export them for the time being in order to avoid breaking things, they should be exported under their original name and retain their access modifier tags. Access modifier tags remain with the main declaration rather than (as we initially tried) with the export. E.g.:becomes:
Much of this process can be automated using
scripts/goog_module/convert-file.sh -s 2 path/to/filename.js
.After converting the
goog.provide
statement and exports in this way you must runto update
tests/deps.js
(or letnpm test
do this for you) after which the code should again continue to work as before. Make sure to include the updateddeps.js
in the commit.Use
npm start
andnpm test
to verify that nothing is seriously broken before committing.Suggested commit message:
Migrate path/to/filename.js to goog.module
Or use
scripts/goog_module/convert-file.sh -c 1 path/to/filename.js
.Notes:
The most important—and probably challenging!—part of this work will be clearly establishing what the public API of each module is.
Contrary to the conventional pattern when using
goog.provide
, there should in general be few (if any) circumstances under which agoog.module
adds new properties to an object it has imported from another module. (This does not prohibit modules from using some part of the Blockly API suchdefineBlocksWithJsonArray
to do so, however.)There are existing
.js
files that have multiplegoog.provide
statements in them. They will need to be separated into separate.js
files each with a singlegoog.module
.Conversely, there are cases where what ought to be a single module is spread over multiple
.js
files. This must be also be rectified..js
file.Concatenation is probably the preferred solution, especially when the former multiple parts of a module share private identifiers which we do not wish the resulting module to export.
There are some
.js
files—notably the ones inblocks/
—whose main purpose is not to export identifiers but to call functions on other modules in order to carry out some specific action (e.g., creating blocks). Such a module might not export anything.3. Rewrite
goog.requires
statements and add missing requiresAnything required from another module should be given an explicitly name by assigning the return value of
goog.require
to a constant. For example, existing code of the form:should become:
Where importing a main export which was previously a module's default export, or where you need only a few particular identifiers from a module, you can import them directly:
However:
goog.require
s.This step can mostly be automated by using
scripts/goog_module/convert-file.sh -s 3 path/to/filename.js
.After converting all the
goog.require
statements in this way the code should continue to work as before.Use
npm start
andnpm test
to verify that nothing is seriously broken before committing.Suggested commit message:
Migrate path/to/filename.js named requires
Or use
scripts/goog_module/convert-file.sh -c 3 path/to/filename.js
.Notes:
goog.requires
and existing declarations in the file. It will be necessary to rename any clashing top-level identifiers (the import name takes precedence because it is mandated by the style guide); when a local variable shadows an imported name it is best to rename it too, even if the imported name isn't used in the scope of the shadowing variable.goog.require
statements in some parts of the codebase, so you will in some cases have to add these missing statements as you go along.foo.bar.baz.quux
"), it is not clear where the name of the module ends and where the names of identifiers exported by that module begins—particularly if that module has not yet been converted togoog.module
syntax. (This is one reason to prefer converting leaf modules first!)foo.bar.baz.quux
it more likely that the module is or should be namedfoo.bar.baz
andquux
is an identifier exported by than that the module is namedfoo.bar
andbaz
is the exported identifier for an object with a property namedquux
.4. Run
clang-format
on the whole fileSince we're touching everything, let's really touch everything. Two easy steps:
npx clang-format -i path/to/filename.js
(orscripts/goog_module/convert-file.sh -s 4 path/to/filename.js
) to format, andnpm run lint
to verify that clang-format has not broken any of eslint's rules. (Better to find out now than when GitHub's CI catches it!)Suggested commit message:
clang-format path/to/filename.js
Or use
scripts/goog_module/convert-file.sh -c 4 path/to/filename.js
.Submitting your Pull Request
There is a special pull request template for migration PRs; use it instead of the standard one.
General To Do List
This is a list of any and all other work that needs to be done to finish the migration.
First pass:
goog_module
branch.http-server
instead offile:
URLs.goog_module
branch to tip ofdevelop
branch before merging any PRs into it.eslint
to accept ES6 input.blockly.js
#5208First pass cleanup:
_
#5135Further cleanup:
(Please edit this bug to add any outstanding work as required.)
The text was updated successfully, but these errors were encountered: