Support for GWT to PolyGerrit integration plugin migration

Detect migration scenario based on UI script files to have same name and
different extensions (.html and .js).

Allow for multiple `Gerrit.install()` calls, reusing `plugin` instance
for purposes of migration.

Cleanup plugin interface between tests.

Change-Id: I1a98a2b8660ce4a1700766dc722d587817ede884
This commit is contained in:
Viktar Donich 2017-12-07 16:04:38 -08:00 committed by Paladox none
parent 7ec4946223
commit 8b3455518a
7 changed files with 229 additions and 21 deletions

View File

@ -2577,7 +2577,7 @@ displayed as part of the index page, if present in the manifest:
Compiled plugins and extensions can be deployed to a running Gerrit
server using the link:cmd-plugin-install.html[plugin install] command.
Web UI plugins distributed as a single `.js` file (or `.html' file for
Web UI plugins distributed as a single `.js` file (or `.html` file for
Polygerrit) can be deployed without the overhead of JAR packaging. For
more information refer to link:cmd-plugin-install.html[plugin install]
command.

View File

@ -4,6 +4,9 @@ CAUTION: Work in progress. Hard hat area. Please
link:https://bugs.chromium.org/p/gerrit/issues/entry?template=PolyGerrit%20plugins[send
feedback] if something's not right.
For migrating existing GWT UI plugins, please check out the
link:pg-plugin-migration.html#migration[migration guide].
[[loading]]
== Plugin loading and initialization

View File

@ -0,0 +1,153 @@
= Gerrit Code Review - PolyGerrit Plugin Development
CAUTION: Work in progress. Hard hat area. Please
link:https://bugs.chromium.org/p/gerrit/issues/entry?template=PolyGerrit%20plugins[send
feedback] if something's not right.
[[migration]]
== Incremental migration of existing GWT UI plugins
link:pg-plugin-dev.html[PolyGerrit plugin API] operates different concepts and
provides different type of API compared to ones available to GWT
plugins. Depending on the plugin, it might require significant modifications to
existing UI scripts to fully take advantage of benefits PolyGerrit API
provides.
To make migration easier, PolyGerrit recommends incremental migration
strategy. Starting with a .js file that works for GWT UI, plugin author can
incrementally migrate deprecated APIs to new plugin API.
The goal for this guide is to provide migration path from .js-based UI script to
.html-based.
NOTE: Web UI plugins distributed as a single .js file are not covered in this
guide.
Let's start with a basic plugin that has an UI module. Commonly, file tree
should look like this:
├── BUILD
├── LICENSE
└── src
└── main
├── java
│   └── com
│   └── foo
│   └── SamplePluginModule.java
└── resources
└── static
└── sampleplugin.js
For simplicity's sake, let's assume SamplePluginModule.java has following
content:
``` java
public class SamplePluginModule extends AbstractModule {
@Override
protected void configure() {
DynamicSet.bind(binder(), WebUiPlugin.class)
.toInstance(new JavaScriptPlugin("sampleplugin.js"));
}
}
```
=== Step 1: Create `sampleplugin.html`
As a first step, create starter `sampleplugin.html` and include UI script in the
module file.
NOTE: GWT UI ignore .html since it's not supported.
``` java
@Override
protected void configure() {
DynamicSet.bind(binder(), WebUiPlugin.class)
.toInstance(new JavaScriptPlugin("sampleplugin.js"));
DynamicSet.bind(binder(), WebUiPlugin.class)
.toInstance(new JavaScriptPlugin("sampleplugin.html"));
}
```
Here's recommended starter code for `sampleplugin.html`:
NOTE: By specification, the `id` attribute of `dom-module` *must* contain a dash
(-).
``` html
<dom-module id="sample-plugin">
<script>
Gerrit.install(plugin => {
// Setup block, is executed before sampleplugin.js
// Install deprecated JS APIs (onAction, popup, etc)
plugin.deprecated.install();
});
</script>
<script src="./sampleplugin.js"></script>
<script>
Gerrit.install(plugin => {
// Cleanup block, is executed after sampleplugin.js
});
</script>
</dom-module>
```
Here's how this works:
- PolyGerrit detects migration scenario because UI scripts have same filename
and different extensions
* PolyGerrit will load `sampleplugin.html` and skip `sampleplugin.js`
* PolyGerrit will reuse `plugin` (aka `self`) instance for `Gerrit.install()`
callbacks
- `sampleplugin.js` is loaded since it's referenced in `sampleplugin.html`
- setup script tag code is executed before `sampleplugin.js`
- cleanup script tag code is executed after `sampleplugin.js`
- `plugin.deprecated.install()` enables deprecated APIs (onAction(), popup(),
etc) before `sampleplugin.js` is loaded
So the purpose is to share plugin instance between .html-based and .js-based
code, making it possible to gradually and incrementally transfer code to new API.
=== Step 2: Create cut-off marker in `sampleplugin.js`
Commonly, window.Polymer is being used to detect in GWT UI script if it's being
executed inside PolyGerrit. This could be used to separate code that was already
migrated to new APIs from the one that hasn't been migrated yet.
During incremental migration, some of the UI code will be reimplemented using
PolyGerrit plugin API. However, old code still could be required for the plugin
to work in GWT UI.
To handle this case, add following code to be the last thing in installation
callback in `sampleplugin.js`
``` js
Gerrit.install(function(self) {
// Existing code here, not modified.
if (window.Polymer) { return; } // Cut-off marker
// Everything below was migrated to PolyGerrit plugin API.
// Code below is still needed for the plugin to work in GWT UI.
});
```
=== Step 3: Migrate!
The code that uses deprecated APIs should be eventually rewritten using
non-deprecated counterparts. Duplicated pieces could be kept under cut-off
marker to work in GWT UI.
If some data or functions needs to be shared between code in .html and .js, it
could be stored on `plugin` (aka `self`) object that's shared between both
=== Step 4: Cleanup
Once deprecated APIs are migrated, `sampleplugin.js` will only contain
duplicated code that's required for GWT UI to work. With sudden but inevitable
GWT code removal from Gerrit that file can be simply deleted, along with script
tag loading it.

View File

@ -30,8 +30,9 @@
_configChanged(config) {
const plugins = config.plugin;
const jsPlugins = plugins.js_resource_paths || [];
const htmlPlugins = plugins.html_resource_paths || [];
const jsPlugins = this._handleMigrations(plugins.js_resource_paths || [],
htmlPlugins);
const defaultTheme = config.default_theme;
if (defaultTheme) {
// Make theme first to be first to load.
@ -42,6 +43,17 @@
this._importHtmlPlugins(htmlPlugins);
},
/**
* Omit .js plugins that have .html counterparts.
* For example, if plugin provides foo.js and foo.html, skip foo.js.
*/
_handleMigrations(jsPlugins, htmlPlugins) {
return jsPlugins.filter(url => {
const counterpart = url.replace(/\.js$/, '.html');
return !htmlPlugins.includes(counterpart);
});
},
/**
* @suppress {checkTypes}
* States that it expects no more than 3 parameters, but that's not true.

View File

@ -70,6 +70,13 @@ limitations under the License.
plugin = null;
});
test('reuse plugin for install calls', () => {
let otherPlugin;
Gerrit.install(p => { otherPlugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
assert.strictEqual(plugin, otherPlugin);
});
test('url', () => {
assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/');
assert.equal(plugin.url('/static/test.js'),

View File

@ -18,6 +18,11 @@
console.warn('Plugin API method ' + (opt_name || '') + ' is not supported');
};
/**
* Hash of loaded and installed plugins, name to Plugin object.
*/
const plugins = {};
const stubbedMethods = ['_loadedGwt', 'screen', 'settingsScreen', 'panel'];
const GWT_PLUGIN_STUB = {};
for (const name of stubbedMethods) {
@ -76,6 +81,20 @@
// http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html
window.$wnd = window;
function getPluginNameFromUrl(url) {
const base = Gerrit.BaseUrlBehavior.getBaseUrl();
const pathname = url.pathname.replace(base, '');
// Site theme is server from predefined path.
if (pathname === '/static/gerrit-theme.html') {
return 'gerrit-theme';
} else if (!pathname.startsWith('/plugins')) {
console.warn('Plugin not being loaded from /plugins base path:',
url.href, '— Unable to determine name.');
return;
}
return pathname.split('/')[2];
}
function Plugin(opt_url) {
this._domHooks = new GrDomHooksManager(this);
@ -84,26 +103,14 @@
'Unable to determine name.');
return;
}
const base = Gerrit.BaseUrlBehavior.getBaseUrl();
this._url = new URL(opt_url);
const pathname = this._url.pathname.replace(base, '');
// Site theme is server from predefined path.
if (pathname === '/static/gerrit-theme.html') {
this._name = 'gerrit-theme';
} else if (!pathname.startsWith('/plugins')) {
console.warn('Plugin not being loaded from /plugins base path:',
this._url.href, '— Unable to determine name.');
return;
}
this._name = pathname.split('/')[2];
this.deprecated = {
install: deprecatedAPI.install.bind(this),
popup: deprecatedAPI.popup.bind(this),
onAction: deprecatedAPI.onAction.bind(this),
};
this._url = new URL(opt_url);
this._name = getPluginNameFromUrl(this._url);
}
Plugin._sharedAPIElement = document.createElement('gr-js-api-interface');
@ -265,6 +272,17 @@
const Gerrit = window.Gerrit || {};
// Provide reset plugins function to clear installed plugins between tests.
const app = document.querySelector('#app');
if (!app) {
// No gr-app found (running tests)
Gerrit._resetPlugins = () => {
for (const k of Object.keys(plugins)) {
delete plugins[k];
}
};
}
// Number of plugins to initialize, -1 means 'not yet known'.
Gerrit._pluginsPending = -1;
@ -296,15 +314,15 @@
return;
}
// TODO(andybons): Polyfill currentScript for IE10/11 (edge supports it).
const src = opt_src || (document.currentScript &&
(document.currentScript.src || document.currentScript.baseURI));
const plugin = new Plugin(src);
const name = getPluginNameFromUrl(new URL(src));
const plugin = plugins[name] || new Plugin(src);
try {
callback(plugin);
plugins[name] = plugin;
} catch (e) {
console.warn(plugin.getPluginName() + ' install failed: ' +
e.name + ': ' + e.message);
console.warn(`${name} install failed: ${e.name}: ${e.message}`);
}
Gerrit._pluginInstalled();
};

View File

@ -44,6 +44,21 @@ limitations under the License.
return promise;
};
</script>
<script>
(function() {
setup(() => {
if (!window.Gerrit) { return; }
Gerrit._pluginsPending = -1;
Gerrit._allPluginsPromise = undefined;
if (Gerrit._resetPlugins) {
Gerrit._resetPlugins();
}
if (Gerrit._endpoints) {
Gerrit._endpoints = new GrPluginEndpoints();
}
});
})();
</script>
<link rel="import"
href="../bower_components/iron-test-helpers/iron-test-helpers.html" />
<link rel="import" href="test-router.html" />