Sencha Ext JS: Beyond ES5

Photo by Dan Asaki on Unsplash


Moderne ECMAScript Syntax in Ext JS Projekten verwenden

 

Sencha Ext JS wird mit einem Closed-Source Build-Tool geliefert, welches den Closure Compiler beinhaltet; In einigen Fällen hindert dies Entwickler daran, moderne ECMAScript-Syntax zu verwenden. Dieser Artikel stellt @coon-js/delorean vor, ein Tool, das dieses Problem mithilfe zusätzlicher Transpilierung umgeht.

 

Leider versteht Ext JS nur ein Subset an moderner ECMAScript Syntax – bei der Entwicklung ist man  gut damit beraten, ein Gemenge von ES5 und ES6 zu verwenden, damit der Build mit Sencha CMD auch funktioniert. Denn was im schlimmsten Fall passieren kann, ist: Sobald Sencha CMD ein Paket oder eine Anwendung baut, wird Code, der in  dev oder während Tests funktionierte, nicht immer erfolgreich kompiliert, oder noch schlimmer: es wird JavaScript Source Code erstellt, der zwar initial lauffähig ist, im Live-Betrieb aber plötzliche Fehler wirft.

Bspw. ist folgender Source bekannt dafür, Sencha CMD Probleme zu bereiten:

 

Der Nullish coalescing operator:

 

const foo = null ?? "default string";

 

Destructuring assignment syntax mit Funktionsargumenten:

 

const fn = ([x, y, z]) => ({x, y, z});

 

Der Optional chaining operator:

 

const adventurer = {
 name: "Alice",
 cat: {
       name: "Dinah"
   }
};

const dogName = adventurer.dog?.name;

 

Der Grund dafür ist, dass Sencha CMD ein proprietäres Tool ist, das den Closure Compiler als Abhängigkeit integriert hat; leider bietet genau diese Abhängigkeit sowie die Art und Weise der Konfiguration der Tools untereinander keine Möglichkeit,  Build-Prozesse weiter zu optimieren. Außerdem benötigt der Closure Compiler offensichtlich Updates, um aktuelle ECMAScript-Standards verstehen zu können. Es gibt zwar einige Konfigurationsoptionen, die ein nachsichtigeres (sprich: kompatibles) Parsen und Transpilieren des Quellcodes ermöglichen. Allerdings gibt es Code, der das Kompilieren verhindert, selbst wenn der Code syntaktisch korrekt ist.

(Diese Übersicht enthält eine Liste, in der Kompatibilitäten zu Sprachstandards aufgeführt sind.)

Erwähnenswert ist außerdem, dass die neueste verfügbare Version des Closure Compilers nicht unbedingt mit der neuesten verfügbaren Version von Sencha CMD ausgeliefert wird: v7.6.0.87 von Sencha CMD wird mit v20220301 ausgeliefert:

 

@sencha\cmd> dir /s/b | findstr .*closure.*$

@sencha\cmd\dist\lib\closure-compiler-externs-v20220301.jar
@sencha\cmd\dist\lib\closure-compiler-v20220301.jar

 

Wenn etwas schief gehen kann...

Im folgenden betrachten wir ein Beispiel, das zeigt, wie korrekter JavaScript Code ein nicht lauffähiges Ext JS-Projekts verursacht:

 

const fn = ([a, b, c]) => ({a, b, c});

 

Diese Pfeilfunktion

 

Dieser Code sei nun Teil eines Ext JS Packages, das mit folgendem Befehl gebaut wird:

 

npx @sencha/cmd package build

 

Die verwendete Sencha CMD Version ist:

 

npx @sencha/cmd which

 Sencha Cmd v7.6.0.87

 

Die Konfiguration der sencha Sektion aus der package.json sieht folgendermaßen aus:

 

"output": {
 "base": "${package.dir}/build"
},

"compressor": {
 "type": "closure",
 "polyfills": "none"
},

 

Das Kompilieren schlägt fehl, da das output level per default auf ES5 gesetzt ist; es ist ein älterer ECMAScript Standard, der aber von von den meisten gängigen Browsern verstanden wird:

Wir können die Konfiguration anpassen und die gewünschte ES-Version spezifizieren: Wir wissen, dass NEXT die neuesten Features unterstützt, also ändern wir die Konfiguration dahingehend:

 

 "output": {
 "base": "${package.dir}/build",
 "js": {
 "version": "NEXT"
 }
 },
 "language": {
 "js": {
 "input": "NEXT",
 "output": "NEXT"
 }
 },
 "compressor": {
 "type": "closure",
 "polyfills": "none"
 },

 

Die Konsole teilt uns nun mit, dass NEXT verstanden wurde, allerdings schlägt das Kompilieren wieder fehl: Die Zieldatei bleibt leer.

Die language.js.input Konfiguration scheint das Problem zu sein: Den offiziellen Sencha docs nach unterstützt sie folgende Werte:

 

ES3 : ECMAScript 3 language level
ES5 : ECMAScript 5 language level
ES6 : ECMAScript 6 language level (2015)
ES7 : ECMAScript 7 language level (2016)
ES8 : ECMAScript 8 language level (2017)
NEXT : ECMAScript Next (or ES.Next) language level

 

wobei der Closure Compiler mit seiner language_in Option die folgenden specs unterstützt:

 

ECMASCRIPT3
ECMASCRIPT5
ECMASCRIPT5_STRICT
ECMASCRIPT_2015
ECMASCRIPT_2016
ECMASCRIPT_2017
ECMASCRIPT_2018
ECMASCRIPT_2019
ECMASCRIPT_2020
ECMASCRIPT_2021
STABLE
ECMASCRIPT_NEXT (latest features supported) (default: STABLE)

 

Leider schlägt allerdings jeder Versuch, den Sprachstandard auf Werte aus der von der Dokumentation von Closure Compiler zur Verfügung gestellten Liste zu stellen, fehl – auch, wenn man durch gängige Variationen wie ECMASCRIPT_2021, ES10…substituiert:

ES8 scheint der größte gemeinsame Teiler für den gleichzeitigen Einsatz von Sencha CMD und dem Closure Compiler zu sein, wie die folgende Konfiguration zeigt: Der Build läuft durch, und in der Zieldatei landet transpiliertes JavaScript:

 

"output": {
 "base": "${package.dir}/build",
 "js": {
 "version": "ES8"
 }
},
"language": {
 "js": {
 "input": "ES8",
 "output": "ES8"
 }
},
"compressor": {
 "type": "closure",
 "polyfills": "none"
}

 


Allerdings funktioniert das Laden des Builds nicht. Rufen wir uns noch einmal kurz das ursprüngliche JavaScript in Erinnerung:

 

const fn = ([a, b, c]) => ({a, b, c});

 

… und das ist der erzeugte JavaScriptCode, den wir bspw. im debug-build (bspw. build/project-debug.js) finden:

 

const fn = [a,b,c] => ({
    a,
    b,
    c
});

 

… und das ist die komprimierte, also minifizierte, Version des Codes (bspw. build/project.js):

 

const c=()=>({a,b,c});

 

Es dauert nicht lange um zu verstehen, warum dieser Code nicht funktioniert: Er ist syntaktisch falsch im ersten, und funktional falsch im zweiten Beispiel.

 

Vorgestellt: @coon-js/delorean

Moderne Sprach-Features können mit Hilfe von zusätzlicher Transpilierung auch für Sencha CMD lauffähig gemacht werden: Quellcode, der die neueste Syntax nach ES Spezifikationen nutzt, muß übersetzt werden, damit ein Transpilat produziert wird, das Sencha CMD und der Closure Compiler verstehen. Nur so erhalten wir am Ende einen syntaktisch und funktional korrekten Build.

@coon-js/deloreanist ein npm Package das zusätzliche Transpilierung von Ext JS Projekten ermöglicht, und zwar mit Hilfe von Babel, das bereits in unzähligen JavaScript-Projekten eingesetzt wird.

Damit das ganze funktioniert, ändert delorean kurzfristig die Projektkonfiguration des Packages oder der Anwendung und leitet die Projektdateien um, und zwar zu Verzeichnissen, in denen der von Babel transpilierte Code abgelegt wird.

Genau das erlaubt es dem Entwickler, in seinen Entwicklungs-Sourcen moderne Sprach-Features und –Syntax zu verwenden, und die unzähligen Plugins und Erweiterungen von Babel erlauben das integrieren zusätzlicher Funktionen. Die default-Konfiguration von delorean stellt sicher, dass Babel ES5 Code produziert:

delorean ist dabei bewußt minimal-invasiv implementiert: Es aktualisiert nur die Verzeichnis-Mappings in den Konfigurationsdateien, die von Sencha CMD benötigt werden, wenn dev-/ oder prod-builds erstellt werden. Dafür wird ein “.deloreanbuild”-Ordner in dem Projektverzeichnis erstellt, in dem die aus diesen Mappings entnommenen Verzeichnisse strukturell gespiegelt und mit transpilierten Sourcen zu finden sind: Die Original-Dateien der Entwicklung bleiben – sinnvolle Konfiguration vorausgesetzt – davon unberührt; auf diese wird nur lesen zugegriffen. Dadurch ist im Endeffekt eine nahtlose Integration in bestehende CI/CD-Pipelines möglich.

Die Änderungen an den Projektdatein sind dabei leicht nachzuvollziehen:

 

"classpath": [
- "${package.dir}/src",
- "${package.dir}/${toolkit.name}/src"
+ "${package.dir}/.deloreanbuild/src",
+ "${package.dir}/.deloreanbuild/${toolkit.name}/src"
 ],
 "overrides": [
- "${package.dir}/overrides",
- "${package.dir}/${toolkit.name}/overrides"
+ "${package.dir}/.deloreanbuild/overrides",
+ "${package.dir}/.deloreanbuild/${toolkit.name}/overrides"
 ],

 

 

Während der Entwicklung sollten Änderungen, die von delorean an den Konfigurationsdateien vorgenommen wurden, wieder rückgängig gemacht werden, damit die default-Einstellungen von  sencha app watch respektive webpack noch funktionieren — also auf die Verzeichnisse zugegriffen wird, in denen aktiv entwickelt wird.

(Anm.: der Erfahrung nach gibt es hier – trotz Warnungen des Closure Compilers weniger Probleme. Denkbar wäre die Integration einer Verzeichnisüberwachung, über die  auch die dev-Umgebung die Sourcen aus dem Transpilat bezieht, und in den Browser lädt).

Das ganze läßt sich mit einem simplen Aufruf von

 

npx delorean –r

 

Bewerkstelligen: Die Referenzen zu dem  .deloreanbuild Ordner werden dann aus der Projektkonfiguration entfernt.

Externe Packages

Das Tool berücksichtigt außerdem externe Packages, die per npm oder lokal für die ExtJS Anwendung konfiguriert sind. 

Damit der Closure Compiler auch hier aufgrund aktueller JavaScript-Syntax nicht aus dem Tritt kommt, sollte für die externen Resourcen delorean konfiguriert werden - entweder als npm-Package oder zumindest über eine delorean-Konfigurationsdatei: Findet sich in der zu transpilierenden Anwendung dann die Konfiguration externals, wird auf die dort aufgeführten Verzeichnisse ebenfalls ein Babel-Build angewendet.

 

CI/CD Integration

npx delorean -p und npx delorean -r lassen  sich leicht in die gängigen CI/CD pipelines integrieren, die mit Sencha Ext JS verwendet werden: Entweder als Teil der  build.xml eines Packages oder einer Applikation, oder indem man zusätzliche  scripts zu der package.json hinzufügt.

 

build.xml Strategie

Die build.xml-Datei  steht generell mit jedem Ext JS Package und jeder Ext JS Anwendung zur Verfügung und erlaubt zusätzliche Optionen und Hooks für  Ant, welches von Sencha CMD genutzt wird. Wir können hier von den  -before-build /-after-build targets  Gebrauch machen (hier gibt es mehr Infos hierzu).

Die Targets können für den Gebrauch mit delorean bspw. folgendermaßen angepaßt werden:

 

<target name="-before-build">
 <exec executable="cmd">
 <arg line="/c npx delorean -p"/>
 </exec>
</target>

<target name="-after-build">
 <exec executable="cmd">
 <arg line="/c npx delorean -r"/>
 </exec>
</target>

 

Das ganze startet  npx delorean -p bevor Sencha CMD das Projekt oder das Package baut (dewegen auch -before-build), und führt die gemachten Änderungen an den Konfigurationsdaten zurück, wenn der Build abgeschlossen wurde: npx delorean -r (deswegen -after-build).

 

package.json Strategie

Wenn bereits ein  build script in der package.json vorhanden ist, das auf Sencha CMD aufbaut, kann der Aufruf zu dem  build command auch mit zusätzlichen Aufrufen zu  delorean dekoriert werden:

 

{
 "scripts": [
 "build": "npx delorean -p && npm run senchabuild && npx delorean -r",
 "senchabuild": "npm run clean && cross-env webpack --env.profile=desktop --env.environment=production --env.treeshake=yes --env.cmdopts=--uses"
]
}

 

 

Zusätzliche Hinweise

Das Repository für das Projekt findet sich unter  https://github.com/coon-js/delorean. Die offizielle Dokumentation zu dem Projekt findet sich unter  https://www.conjoon.org/docs/api/misc/@coon-js/delorean.

Dieser Artikel erschien ursprünglich in englischer Sprache unter medium.com/@thorstensuckow/sencha-ext-js-beyond-es5-a0382916b7a6

Thorsten Suckow-Homberg

Full Stack Senior bei eyeworkers
kontakt@eyeworkers.de
+ 49 721 183960