feat: remove sense legacy build path (#1244)

This commit is contained in:
Tobias Åström
2023-05-03 09:21:19 -07:00
committed by GitHub
parent 60a977f906
commit 6a5e34de7d
9 changed files with 2 additions and 618 deletions

View File

@@ -22,7 +22,6 @@ Options:
--output Destination directory [string] [default: "<name>-ext"]
--minify Minify and uglify code [boolean] [default: true]
--sourcemap Generate sourcemaps [boolean] [default: false]
--legacy Generate legacy extension [boolean] [default: false]
-h, --help Show help [boolean]
```
@@ -123,21 +122,3 @@ example:
"supernova": true
}
```
The `"supernova": true` attribute should not be added when building with the
--legacy option below.
### Legacy
Qlik Sense before 2020 does not support nebula supernova natively, so a special
build path is needed for visualizations to load properly in the client.
For this purpose, use the --legacy option.
```bash
nebula sense --legacy
```
You can find that the generated `QEXT` file does not include `supernova: true`.
Note:
For old Qlik Sense, not all features of extension are presented.

View File

@@ -1,116 +0,0 @@
const path = require('path');
const fs = require('fs-extra');
const rollup = require('rollup');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const common = require('@rollup/plugin-commonjs');
const replace = require('@rollup/plugin-replace');
const babel = require('@rollup/plugin-babel');
const terser = require('@rollup/plugin-terser');
async function build(argv) {
const cwd = process.cwd();
const supernovaPkg = require(path.resolve(cwd, 'package.json')); // eslint-disable-line
let extName = supernovaPkg.name.split('/').reverse()[0];
const { main } = supernovaPkg;
if (extName === main) {
extName = extName.replace(/\.js$/, '-ext.js');
}
const targetDir = argv.output !== '<name>-ext' ? argv.output : `${extName}-ext`;
const qextLegacyTargetDir = path.resolve(cwd, targetDir);
const qextFileName = path.resolve(qextLegacyTargetDir, `${extName}.qext`);
const qextFileNameJs = qextFileName.replace(/\.qext$/, '.js');
const extDefinition = argv.ext ? path.resolve(argv.ext) : '';
const relativeMainFile = `./${main.replace(/\.js$/, '')}`;
async function moveSnBundle() {
const code = await fs.readFile(path.resolve(cwd, main), { encoding: 'utf8' });
const replacedCode = code.replace(/@nebula.js\/stardust/g, '../nlib/@nebula.js/stardust/dist/stardust');
await fs.outputFile(path.resolve(qextLegacyTargetDir, main), replacedCode);
}
async function moveResources() {
await fs.copy(
path.resolve(path.dirname(require.resolve('@nebula.js/stardust')), 'dist'),
path.resolve(qextLegacyTargetDir, 'nlib/@nebula.js/stardust/dist')
);
}
async function createQextFiles() {
const qext = supernovaPkg.qext || {};
if (argv.meta) {
const meta = require(path.resolve(cwd, argv.meta)); // eslint-disable-line
Object.assign(qext, meta);
}
const contents = {
name: qext.name || extName,
version: supernovaPkg.version,
description: supernovaPkg.description,
author: supernovaPkg.author,
icon: qext.icon || 'extension',
preview: qext.preview,
type: 'visualization',
};
await fs.writeFile(qextFileName, JSON.stringify(contents, null, 2));
}
async function wrapIt() {
const bundle = await rollup.rollup({
input: path.resolve(__dirname, '../src/legacy/sn-ext.js'),
external: ['translator', 'qlik', './nlib/@nebula.js/stardust/dist/stardust', relativeMainFile],
plugins: [
replace({
__SN_DEF__: `${relativeMainFile}`,
__EXT_DEF__: `${extDefinition.replace(/\\/g, '/')}`,
preventAssignment: true,
}),
nodeResolve(),
common(),
babel({
babelHelpers: 'bundled',
babelrc: false,
exclude: [/node_modules/],
presets: [
[
'@babel/preset-env',
{
modules: false,
targets: {
browsers: ['chrome 62'],
},
},
],
],
}),
argv.minify &&
terser({
output: {
comments: /@license|@preserve|Copyright|license/,
},
}),
],
});
await bundle.write({
file: qextFileNameJs,
format: 'amd',
sourcemap: argv.sourcemap,
});
}
await moveSnBundle();
await moveResources();
await wrapIt();
await createQextFiles();
}
module.exports = build;

View File

@@ -27,12 +27,6 @@ const options = {
default: false,
desc: 'Generate sourcemaps',
},
legacy: {
type: 'boolean',
required: false,
default: false,
desc: 'Generate legacy extension',
},
};
module.exports = (yargs) => yargs.options(options);

View File

@@ -1,9 +1,9 @@
const buildLegacy = require('./build-legacy');
const build = require('./build');
function sense(argv) {
if (argv.legacy) {
return buildLegacy(argv);
console.error('Legacy sense-build support removed in 4.0, it is not required for sense compatability.');
process.exit(1);
}
return build(argv);
}

View File

@@ -1,63 +0,0 @@
import createSelections from '../selections';
describe('supernova-wrapper', () => {
describe('selections', () => {
let selectionsApi;
let selectionsApiActivateMock;
let selectionsApiDeactivateMock;
let backendApi;
let backendApiBeganSelectionMock;
let backendApiEndSelectionMock;
let backendApiResetSelectionMock;
let backendApiSwitchModalSelectionMock;
let selections;
let selectionsEmitMock;
beforeEach(() => {
selectionsApiActivateMock = jest.fn();
selectionsApiDeactivateMock = jest.fn();
backendApiBeganSelectionMock = jest.fn();
backendApiEndSelectionMock = jest.fn();
backendApiResetSelectionMock = jest.fn();
backendApiSwitchModalSelectionMock = jest.fn();
selectionsEmitMock = jest.fn();
selectionsApi = {
activated: selectionsApiActivateMock,
deactivated: selectionsApiDeactivateMock,
};
backendApi = {
beginSelections: backendApiBeganSelectionMock,
endSelections: backendApiEndSelectionMock,
model: {
resetMadeSelections: backendApiResetSelectionMock,
app: {
switchModalSelection: backendApiSwitchModalSelectionMock,
},
},
};
selections = createSelections({ selectionsApi, backendApi });
selections.emit = selectionsEmitMock;
});
it('should `begin`', () => {
selections.begin();
expect(selectionsApi.activated).toHaveBeenCalledWith(true);
expect(backendApi.beginSelections).toHaveBeenCalledWith();
expect(selections.emit).toHaveBeenCalledWith('activated');
});
it('should `begin` with paths', () => {
const paths = ['hypercubePath', 'otherHypercubePath'];
selections.begin(paths);
expect(selectionsApi.activated).toHaveBeenCalledWith(true);
expect(backendApi.beginSelections).toHaveBeenCalledTimes(0);
expect(backendApi.model.app.switchModalSelection).toHaveBeenCalledWith(backendApi.model, paths);
expect(selections.emit).toHaveBeenCalledWith('activated');
});
});
});

View File

@@ -1,3 +0,0 @@
const x = {};
export default x;

View File

@@ -1,36 +0,0 @@
const INTERACTION_STATES = {
STATIC: 0,
ANALYSIS: 1,
EDIT: 2,
};
function showTooltip(options) {
if (options.tooltips) {
return true;
}
return options.interactionState === INTERACTION_STATES.ANALYSIS && options.tooltips !== false;
}
function permissions(options, backendApi) {
const p = [];
if (showTooltip(options)) {
p.push('passive');
}
if (
options.interactionState === INTERACTION_STATES.ANALYSIS &&
options.tooltips !== false &&
options.limitedInteraction !== true
) {
p.push('interact');
}
if (!backendApi.isSnapshot) {
if (options.selections !== false) {
p.push('select');
}
p.push('fetch');
}
return p;
}
export default permissions;

View File

@@ -1,82 +0,0 @@
/* eslint no-param-reassign: 0 */
import EventEmitter from 'node-event-emitter';
export default (scope) => {
const mixin = (obj) => {
Object.keys(EventEmitter.prototype).forEach((key) => {
obj[key] = EventEmitter.prototype[key];
});
EventEmitter.init(obj);
return obj;
};
// TODO one and only one
const selectionAPI = {
begin(paths) {
const suppressBeginSelections = true;
scope.selectionsApi.activated(suppressBeginSelections);
if (paths) {
scope.backendApi.model.app.switchModalSelection(scope.backendApi.model, paths);
} else {
scope.backendApi.beginSelections();
}
selectionAPI.emit('activated');
},
clear() {
scope.backendApi.model.resetMadeSelections();
},
confirm() {
scope.selectionsApi.confirm();
},
cancel() {
scope.selectionsApi.cancel();
},
select(s) {
if (!scope.selectionsApi.active) {
scope.backendApi.beginSelections();
}
scope.backendApi.model[s.method](...s.params).then((qSuccess) => {
if (!qSuccess) {
scope.selectionsApi.selectionsMade = false;
this.clear();
}
});
scope.selectionsApi.selectionsMade = s.method !== 'resetMadeSelections';
},
isActive() {
return scope.selectionsApi.active;
},
isModal() {
return scope.backendApi.model === (scope.backendApi.model.app && scope.backendApi.model.app.modalSelectionObject);
},
refreshToolbar() {
scope.selectionsApi.refreshToolbar();
},
};
mixin(selectionAPI);
scope.selectionsApi.confirm = () => {
scope.backendApi.endSelections(true).then(() => {
scope.selectionsApi.deactivated();
selectionAPI.emit('confirmed');
selectionAPI.emit('deactivated');
});
};
scope.selectionsApi.clear = () => {
scope.backendApi.clearSelections();
scope.selectionsApi.selectionsMade = false;
selectionAPI.emit('cleared');
};
scope.selectionsApi.cancel = () => {
scope.backendApi.endSelections(false);
scope.selectionsApi.deactivated();
selectionAPI.emit('canceled');
selectionAPI.emit('deactivated');
};
return selectionAPI;
};

View File

@@ -1,291 +0,0 @@
/* eslint no-underscore-dangle: 0 */
/* eslint import/no-unresolved: 0 */
/* eslint import/extensions: 0 */
// Sense dependencies
import qlik from 'qlik';
import senseTranslator from 'translator';
// injected
import snDefinition from '__SN_DEF__';
import extDefinition from '__EXT_DEF__';
import emptyExtDefinition from './empty-ext';
// lib dependencies
import { __DO_NOT_USE__ } from './nlib/@nebula.js/stardust/dist/stardust';
// wrapper code
import permissions from './permissions';
import selectionsApi from './selections';
const { generator: supernova, theme: themeFn, locale: localeFn } = __DO_NOT_USE__;
// ------- locale ---------
// use nebula locale API to ensure all viz get the same API regardless
// if consumed in Sense or outside
const loc = localeFn({
initial: senseTranslator.language,
});
const translator = {
...loc.translator,
// wrap get() for this API in order to prefer strings provided by Sense
get(str, args) {
const s = senseTranslator.get(str, args);
if (s === str) {
return loc.translator.get(str, args);
}
return s;
},
};
// ------------------------
// Galaxy is usually provided by @nebula.js/nucleus when a viz is instantiated.
// Since nucleus is not being used in Sense yet, this interface MUST be provided explicitly here
// and it MUST have the same interface as the one provided by nucleus.
const galaxy = {
deviceType: 'auto',
translator,
flags: {
isEnabled: () => false,
},
anything: {},
};
const ALLOWED_OPTIONS = ['viewState', 'direction', 'renderer'];
function limitOptions(newOptions, oldOptions) {
const op = {};
let opChanged = false;
Object.keys(newOptions).forEach((key) => {
if (ALLOWED_OPTIONS.indexOf(key) === -1) {
return;
}
op[key] = newOptions[key];
if (oldOptions[key] !== newOptions[key]) {
opChanged = true;
}
});
if (opChanged) {
return op;
}
return oldOptions;
}
function createActions(actions) {
return actions.map((a) => {
const senseItem = {
isIcon: true,
buttonClass: 'sel-toolbar-icon-toggle',
isActive() {
return a.active;
},
isDisabled() {
return a.disabled;
},
action() {
a.action();
},
getSvgIcon() {
const icon = a.getSvgIconShape();
const { viewBox = '0 0 16 16' } = icon;
const shapes = icon.shapes
.map(
({ type, attrs }) =>
`<${type} ${Object.keys(attrs)
.map((k) => `${k}="${attrs[k]}"`)
.join(' ')}/>`
)
.join('');
return `<i style="display:inline-block;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.2em;text-rendering:optimizeLegibility;web-kit-font-smoothing:antialiased;moz-osx-font-smoothing:grayscale"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="${viewBox}" fill="currentColor">${shapes}</svg></i>`;
},
};
Object.defineProperties(senseItem, {
name: {
get: () => a.label,
},
hidden: {
get: () => a.hidden,
},
});
return senseItem;
});
}
function updateTheme(ref) {
return qlik
.currApp(ref)
.theme.getApplied()
.then((qTheme) => {
if (ref.nTheme.externalAPI.name() !== qTheme.id) {
ref.nTheme.internalAPI.setTheme(qTheme.properties, qTheme.id);
}
});
}
// ============= EXTENSON =====================================================
const snGenerator = supernova(snDefinition, galaxy);
const ext =
typeof extDefinition === 'function'
? extDefinition({ translator })
: extDefinition || snGenerator.definition.ext || emptyExtDefinition || {};
let data;
if (snGenerator.qae.data.targets[0]) {
const d = snGenerator.qae.data.targets[0];
// supernova uses the convention of 'added' instead of 'add' since the
// method is called after the dimension/measures has been added to the hypercube.
// we therefore need to map supernova callbacks to the ones supported in Sense
const map = (v) => ({
...v,
add: v.added,
remove: v.removed,
replace: v.replaced,
move: v.moved,
});
data = {};
if (d.dimensions) {
data.dimensions = map(d.dimensions);
}
if (d.measures) {
data.measures = map(d.measures);
}
}
const X = {
// overridable properties
definition: {
type: 'items',
component: 'accordion',
items: {
data: data
? {
uses: 'data',
}
: undefined,
sorting: data
? {
uses: 'sorting',
}
: undefined,
settings: {
uses: 'settings',
},
},
},
data,
// -- defaults for the following are injected in default-extension.js --
// support
// importProperties,
// exportProperties,
// ----
getSelectionToolbar() {
return [
...this.senseItems,
{
name: 'Tooltip.clearSelection',
tid: 'selection-toolbar.clear',
isIcon: true,
buttonClass: 'clear',
iconClass: 'lui-icon lui-icon--clear-selections',
action: function action() {
this.selectionsApi.clear();
},
isDisabled: function isDisabled() {
return !this.selectionsApi.selectionsMade;
},
},
];
},
// override with user config
...ext,
// =============================================
// non-overridable properties
initialProperties: snGenerator.qae.properties.initial,
template: '<div style="height: 100%;position: relative"></div>',
mounted($element) {
const element = $element[0].children[0];
const selectionAPI = selectionsApi(this.$scope);
const sn = snGenerator.create({
model: this.backendApi.model,
app: this.backendApi.model.session.app,
selections: selectionAPI,
explicitResize: true,
});
this.sn = sn;
this.snComponent = sn.component;
this.snComponent.created();
this.snComponent.mounted(element);
this.senseItems = [];
const { senseItems } = this;
this.snComponent.observeActions((actions) => {
senseItems.length = 0;
senseItems.push(...createActions(actions));
this.$scope.selectionsApi.refreshToolbar();
});
this.nTheme = themeFn();
this.initiatedTheme = updateTheme(this);
},
suppressOnPaint: () => false,
paint() {
const perms = permissions(this.options, this.backendApi);
const constraints = {
passive: perms.indexOf('passive') === -1 || undefined,
active: perms.indexOf('interact') === -1 || undefined,
select: perms.indexOf('select') === -1 || undefined,
edit: perms.indexOf('edit') === -1 || undefined,
};
const opts = limitOptions(this.options, this.snComponent.context.options);
this._pureLayout = this.backendApi.model.pureLayout || this.backendApi.model.layout;
return this.initiatedTheme.then(() => {
updateTheme(this);
return this.snComponent.render({
layout: this.backendApi.model.pureLayout || this.backendApi.model.layout,
context: {
appLayout: {
qLocaleInfo: this.backendApi.localeInfo,
},
constraints,
theme: this.nTheme.externalAPI,
},
options: opts,
});
});
},
resize() {
if (this._pureLayout !== this.backendApi.model.pureLayout) {
return this.paint();
}
const opts = limitOptions(this.options, this.snComponent.context.options);
this.snComponent.context.options = opts;
return this.snComponent.resize();
},
setInteractionState() {
this.paint();
},
setSnapshotData(layout) {
return this.snComponent.setSnapshotData(layout);
},
onContextMenu(menu, event) {
return this.snComponent.onContextMenu(menu, event);
},
getViewState() {
const ref = this.snComponent.getImperativeHandle();
if (ref && typeof ref.getViewState === 'function') {
return ref.getViewState();
}
return undefined;
},
destroy() {
this.sn.destroy();
if (this.snComponent) {
this.snComponent.willUnmount();
this.snComponent.destroy();
}
},
};
export default X;