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:
parent
7ec4946223
commit
8b3455518a
@ -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
|
Compiled plugins and extensions can be deployed to a running Gerrit
|
||||||
server using the link:cmd-plugin-install.html[plugin install] command.
|
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
|
Polygerrit) can be deployed without the overhead of JAR packaging. For
|
||||||
more information refer to link:cmd-plugin-install.html[plugin install]
|
more information refer to link:cmd-plugin-install.html[plugin install]
|
||||||
command.
|
command.
|
||||||
|
@ -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
|
link:https://bugs.chromium.org/p/gerrit/issues/entry?template=PolyGerrit%20plugins[send
|
||||||
feedback] if something's not right.
|
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]]
|
[[loading]]
|
||||||
== Plugin loading and initialization
|
== Plugin loading and initialization
|
||||||
|
|
||||||
|
153
Documentation/pg-plugin-migration.txt
Normal file
153
Documentation/pg-plugin-migration.txt
Normal 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.
|
@ -30,8 +30,9 @@
|
|||||||
|
|
||||||
_configChanged(config) {
|
_configChanged(config) {
|
||||||
const plugins = config.plugin;
|
const plugins = config.plugin;
|
||||||
const jsPlugins = plugins.js_resource_paths || [];
|
|
||||||
const htmlPlugins = plugins.html_resource_paths || [];
|
const htmlPlugins = plugins.html_resource_paths || [];
|
||||||
|
const jsPlugins = this._handleMigrations(plugins.js_resource_paths || [],
|
||||||
|
htmlPlugins);
|
||||||
const defaultTheme = config.default_theme;
|
const defaultTheme = config.default_theme;
|
||||||
if (defaultTheme) {
|
if (defaultTheme) {
|
||||||
// Make theme first to be first to load.
|
// Make theme first to be first to load.
|
||||||
@ -42,6 +43,17 @@
|
|||||||
this._importHtmlPlugins(htmlPlugins);
|
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}
|
* @suppress {checkTypes}
|
||||||
* States that it expects no more than 3 parameters, but that's not true.
|
* States that it expects no more than 3 parameters, but that's not true.
|
||||||
|
@ -70,6 +70,13 @@ limitations under the License.
|
|||||||
plugin = null;
|
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', () => {
|
test('url', () => {
|
||||||
assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/');
|
assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/');
|
||||||
assert.equal(plugin.url('/static/test.js'),
|
assert.equal(plugin.url('/static/test.js'),
|
||||||
|
@ -18,6 +18,11 @@
|
|||||||
console.warn('Plugin API method ' + (opt_name || '') + ' is not supported');
|
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 stubbedMethods = ['_loadedGwt', 'screen', 'settingsScreen', 'panel'];
|
||||||
const GWT_PLUGIN_STUB = {};
|
const GWT_PLUGIN_STUB = {};
|
||||||
for (const name of stubbedMethods) {
|
for (const name of stubbedMethods) {
|
||||||
@ -76,6 +81,20 @@
|
|||||||
// http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html
|
// http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html
|
||||||
window.$wnd = window;
|
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) {
|
function Plugin(opt_url) {
|
||||||
this._domHooks = new GrDomHooksManager(this);
|
this._domHooks = new GrDomHooksManager(this);
|
||||||
|
|
||||||
@ -84,26 +103,14 @@
|
|||||||
'Unable to determine name.');
|
'Unable to determine name.');
|
||||||
return;
|
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 = {
|
this.deprecated = {
|
||||||
install: deprecatedAPI.install.bind(this),
|
install: deprecatedAPI.install.bind(this),
|
||||||
popup: deprecatedAPI.popup.bind(this),
|
popup: deprecatedAPI.popup.bind(this),
|
||||||
onAction: deprecatedAPI.onAction.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');
|
Plugin._sharedAPIElement = document.createElement('gr-js-api-interface');
|
||||||
@ -265,6 +272,17 @@
|
|||||||
|
|
||||||
const Gerrit = window.Gerrit || {};
|
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'.
|
// Number of plugins to initialize, -1 means 'not yet known'.
|
||||||
Gerrit._pluginsPending = -1;
|
Gerrit._pluginsPending = -1;
|
||||||
|
|
||||||
@ -296,15 +314,15 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(andybons): Polyfill currentScript for IE10/11 (edge supports it).
|
|
||||||
const src = opt_src || (document.currentScript &&
|
const src = opt_src || (document.currentScript &&
|
||||||
(document.currentScript.src || document.currentScript.baseURI));
|
(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 {
|
try {
|
||||||
callback(plugin);
|
callback(plugin);
|
||||||
|
plugins[name] = plugin;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(plugin.getPluginName() + ' install failed: ' +
|
console.warn(`${name} install failed: ${e.name}: ${e.message}`);
|
||||||
e.name + ': ' + e.message);
|
|
||||||
}
|
}
|
||||||
Gerrit._pluginInstalled();
|
Gerrit._pluginInstalled();
|
||||||
};
|
};
|
||||||
|
@ -44,6 +44,21 @@ limitations under the License.
|
|||||||
return promise;
|
return promise;
|
||||||
};
|
};
|
||||||
</script>
|
</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"
|
<link rel="import"
|
||||||
href="../bower_components/iron-test-helpers/iron-test-helpers.html" />
|
href="../bower_components/iron-test-helpers/iron-test-helpers.html" />
|
||||||
<link rel="import" href="test-router.html" />
|
<link rel="import" href="test-router.html" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user