All.js Uncaught TypeError: Cannot read properties of undefined (reading 'call') nach Update auf 6.4.11.1

„Eigentlich“ hatte ich alle Plugins durch. Aber noch einmal getestet und „Google Tag Manager + Enhanced E-Commerce Tracking“ hat das Problem bereitet. Hier liegt auch schon ein Update vor, dass das anscheinend repariert. Jedenfalls läuft es jetzt…
Danke für die Tipps.

Hier noch ein Beitrag aus Entwicklersicht, vielleicht hilft es dem ein oder anderen.

Bei mir in einem Plugin, welches genau diesen Fehler schmiss, hatte ich im JS folgenden Import genutzt:

import AjaxModalExtensionUtil from 'src/utility/modal-extension/ajax-modal-extension.util';

Nach dem Entfernen dieses Imports trat der Fehler nicht mehr auf. Nach kurzer Recherche scheint sich hier im Commit NEXT-16624 - Add style and test fixes was geändert zu haben. Die Klasse ist zwar noch da, weshalb ich nicht ganz verstehe warum der Fehler auftrat, aber es scheint irgendwie damit zusammenzuhängen.

1 „Gefällt mir“

Hallo liebe Leute!

Ich habe nun auch ziemlich mit der Problematik gekämpft und habe nach einigem Lesen und Code auseinanderpflücken eine Lösung gefunden.

Die Erklärung warum dieser Fehler passiert ist eigentlich relativ simpel:

Das Shopware 6 Storefront wird mit Webpack compiliert und in verschiedene „Chunks“ verpackt um das Caching Clientseitig zu verbessern. Wenn aber nun eine Ressource vom Shopware System in einem Plugin verwendet wird, die nicht explizit in dem Vendor-Shared-Chunk vorhanden ist, so werden die Chunk-Hashes durch Webpack angepasst. Der Shared Chunk wird mit folgenden Pfaden ausgestattet:

src/plugin-system
src/helper
src/utility
src/service

Man sieht: Wenn man ein Plugin extended [Override Existing Javascript | Shopware Documentation]( wie in der Doku beschrieben) mit einem einfachen import, dann werden in den meisten Fällen auch die Chunk-Hashes neu generiert.

Die Chunk-Hashes sollten aber in der Shopware Basis nicht verändert werden, da sonst Externe Plugins die Funktionalitäten aus dem Core nicht mehr „finden“ können, zwecks fehlender Referenz.

Der bessere weg in diesem Falle ist, das Plugin über den Pluginmanager zu holen und anschließend dieses zu extenden.

const PluginManager = window.PluginManager
const CookiePermissionPlugin = PluginManager.getPlugin('CookiePermissionPlugin ').get('class');

export default class MyCookiePermission extends CookiePermissionPlugin {
}

Soweit - so gut. Ich bin also hergegangen und habe jegliche Referenzen zu Dateien die nicht durch den Vendor-Shared Chunk abgedeckt wurden entfernt. Dies hat dieses eine Chunk-Problem gelöst.

Das nächst Problem lies aber nicht lange auf sich warten:
Mein Plugin verwendet NPM-Packages, die über das eigentliche Chunk-Size-Limit hinausgehen und somit als eigener Chunk in /vendor/shopware/storefront/Resources/app/storefront/dist/js/ abgelegt werden.
Das heißt, dass die Distribution des Plugin daran scheitern würde, da die Dependencies nicht im Plugin selbst mit enthalten wären.
Die Lösung: Das ChunkSplitting von Shopware modifizieren!
Hier habe ich einen eigene cacheGroup für alle Plugins erstellt, die in dieser Environment vorhanden sind.

const pluginsConfigPath = path.resolve(projectRootPath, 'var/plugins.json');
const plugins = require(pluginsConfigPath);

webpackConfig.optimization.splitChunks.cacheGroups.custom = {
    test: (content) => {
      // Ist die Source-File im "custom"-Ordner?
      return content.resource.includes('custom');
    },
    name: content => {
      // modifiziere den Chunk-Name auf den des Plugin um den Chunk 
      // wieder mit dem eigentlichen Plugin zusammenzuführen
      const parsed = path.parse(content.resource);
      let name = 'custom';
      Object.values(plugins).map(plugin => {
        const basePath = plugin.basePath.replace('/src/', '');
        if (basePath.includes('custom') && content.resource.includes(basePath)) {
            name = plugin.technicalName;
        }
      });
      return name;
    },
    reuseExistingChunk: true,
    chunks(chunk) {
      // Check-Funktion die prüft wohin der Chunk einsortiert werden soll
      return !Object.values(plugins).map(item => item.techicalName).includes(chunk.name);
    },
  };

Somit werden auch größere Dependencies mit im Plugin-Chunk geladen. Nun ist noch das Problem mit den TerserPlugin License-Files gewesen. Denn diese sollten auch mit dem Plugin ausgeliefert werden. Hierfür musste ich natürlich auch das TerserPlugin in der Config überschreiben.

// Überschreibe den Minimizer
 webpackConfig.optimization.minimizer = [
    new TerserPlugin({
      terserOptions: {
        ecma: 5,
        warnings: false,
      },
      parallel: true,
      extractComments: {
// Lege fest, wohin die License File kommen soll
        filename: (fileData) => {
          const plugin = Object.values(plugins).find(plugin => fileData.filename.includes(`${plugin.technicalName}`));
          return plugin ? '../../../../../../../' + path.relative(__dirname, path.resolve(plugin.basePath, 'Resources', 'public', 'static', `${fileData.filename}.LICENSE.txt${fileData.query}`)) : `${fileData.filename}.LICENSE.txt${fileData.query}`;
        },
        banner(file) {
          /*
           * Könnte ein Regulärer Ausdruck sein.... könnte...
          *  Schreibe einen Banner in die eigentliche Kompilierte Datei, wenn es ein Plugin ist.
           */
          const pluginName = file.replace('../../../../../../../custom/plugins/', '').replace(/^[a-z]*/i, '').replace('src/Resources/public/static/js/', '').replace('.js.LICENSE.txt', '').replace(/^\//, '');
          if (!pluginName.startsWith('.'))
            return `License information can be found in /bundles/${pluginName}/static/js/${pluginName}.js.LICENSE.txt`;
        },
      },
    }),
  ]

Ich hoffe ich konnte damit so manchen Leuten helfen.

(Ja, ich hätte auch einen Pull-Request machen können, aber das überlass ich gern anderen. Und die Erklärung finde ich hierbei wichtiger, als die Contribution selbst)

Viele Grüße

Martin

1 „Gefällt mir“

Kleiner Edit zu meinem letzten Kommentar in diesem Thread:

Ich musste etwas „Feintuning“ an der Webpack-Config betreiben, da nach Installation von weiteren Dependencies sich noch weitere Chunk-Probleme aufgetan haben.

Namentlich zum Beispiel Babel. Einer meiner Dependencies referenzierte @babel/runtime/helpers/asyncToGenerator und @babel/runtime/helpers/defineProperty

Dieses Teile des Babel-Packages werden im Storefront-Core nicht verwendet, und werden dann mit meiner vorherigen Lösung wieder in den Vendor-Chunk verfrachtet. Der Grund ist, da @Babel schon im Core als NodeModule vorhanden ist und somit dann auch damit verchunked wird. Somit würde in anderen Installationen das Problem der fehlenden oder nicht-matchenden Chunks erneut auftreten.

Die Lösung:

Zum einen habe ich in meiner build/webpack.config.js zwei Instanzen eines Plugins addiert:

    plugins: [
      new webpack.NormalModuleReplacementPlugin(
        /@babel\/runtime\/helpers\/asyncToGenerator/,
        resolve(join(__dirname, '..', 'node_modules', '@babel/runtime/helpers/asyncToGenerator.js'))
      ),
      new webpack.NormalModuleReplacementPlugin(
        /@babel\/runtime\/helpers\/defineProperty/,
        resolve(join(__dirname, '..', 'node_modules', '@babel/runtime/helpers/defineProperty.js'))
      ),
    ],

Somit wird festgelegt, dass der require-issuer definitiv mein Plugin ist und das Module aus meinem lokalen node_modules Ordner geladen wird.

Herausgefunden habe ich dies aber erst, nachdem ich die kompletten Requirements die von Webpack geladen werden in eine JSON-Datei gedumpt habe und dort nach den einzelnen Packages gesucht habe, die in meiner package-lock.json vorhanden sind.

In der webpack.config.js von shopware/storefront nehme ich nun auch den Require Issuer in die Chunk-Condition auf:


const pluginsConfigPath = path.resolve(projectRootPath, 'var/plugins.json');
const plugins = require(pluginsConfigPath);

if (isProdMode) {
    webpackConfig.optimization.minimizer = [
        new TerserPlugin({
            terserOptions: {
                ecma: 5,
                warnings: false,
            },
            parallel: true,
            extractComments: {
                filename: (fileData) => {
                    const plugin = Object.values(plugins).find(plugin => fileData.filename.includes(`${plugin.technicalName}`));
                    return plugin ? '../../../../../../../' + path.relative(__dirname, path.resolve(plugin.basePath, 'Resources', 'public', 'static', `${fileData.filename}.LICENSE.txt${fileData.query}`)) : `${fileData.filename}.LICENSE.txt${fileData.query}`;
                },
                banner(file) {
                    /**
                     * Could be replaced with a Regular Expression....
                     */
                    const pluginName = file.replace('../../../../../../../custom/plugins/', '').replace(/^[a-z]*/i, '').replace('src/Resources/public/static/js/', '').replace('.js.LICENSE.txt', '').replace(/^\//, '');
                    if (!pluginName.startsWith('.'))
                        return `For license information please see /bundles/${pluginName}/static/js/${pluginName}.js.LICENSE.txt`;
                    else {
                        return `For license information please see ${pluginName.replace('./js/', '')}.js.LICENSE.txt`
                    }
                },
            },
        }),
    ]
    webpackConfig.optimization.splitChunks.cacheGroups.custom = {
        test: (content) => {
            return content.resource.includes('custom/plugin') || content.resourceResolveData.context.issuer.includes('custom/plugin');
        },
        name: content => {
            let name = 'custom';
            Object.values(plugins).map(plugin => {
                const basePath = plugin.basePath.replace('/src/', '');
                if (basePath.includes('custom/plugin')) {
                    if (content.resource.includes(basePath)) {
                        name = plugin.technicalName;
                    }
                }
            });
            return name;
        },
        reuseExistingChunk: true,
        chunks(chunk) {
            return !Object.values(plugins).map(item => item.techicalName).includes(chunk.name);
        },
    };
}

Eventuell wäre es eine Gute Idee ein Dependency-Analyse Tool in das Shopware6 PHPStorm Plugin zu inkludieren… @shyim …just saying :wink:

Ich hoffe ich konnte helfen!

Grüße, Martin

Hallo Martin,
wenn man das Chunking anpasst, dann gilt das ja nur für die Maschine wo der Plugin-Hersteller das Plugin baut, oder läuft das Ganze auf einen Patch hinaus ?

Ich gehe hier jetzt mit den Weg externe „größere“ Libs nicht zu importieren, sondern ich markiere die Libs in der Web-Config als „externals“ und die Anbindung ist dann einfach der „normale“ Script-Tag mit dem Verweis auf die externe JS Datei.
Ich frage mich dann nur als passiert, wenn eine Lib, die so eingebunden wurde dann selbst depencies hat, die auch von Shopware genutzt werden. Nach meinem Verständnis wäre das Dependencies zwar mehrfach vorhanden, aber die Hashes sollten sich unterscheiden. Sprich gleicher Code aber unterschiedlichen „Namen“ (=namespaces).
Ich bin kein Webpack-Profi, daher hier die Frage an den augenscheinlichen Spezialisten :slight_smile:

LG

Carsten

Hallo Carsten!

Das gilt generell nur für „Pluginhersteller“. Das Chunking Problem rührt ja daher, dass die Chunk-Identifier nicht mehr die selben sind wie bei den Shopware eigenen Builds. Deshalb ist diese Lösung immer nur auf den entsprechenden DEV-Umgebungen anzuwenden.

Ich bin eher kein Freund davon, von extern (unpkg.org usw.) zu laden, da das zum ein Fass an DSGVO-Sorgen beim Kunden aufmachen kann, zum anderen aber je nach Quelle auch von der Zuverlässigkeit des Anbieters abhängig macht. Darüber hinaus würde es das Grundprinzip von Bundlern generell in Frage stellen, da bei diesen ja das Ziel ist das gesamte Package auszuliefern.

Die [PluginRoot]/Resources/app/storefront/build/webpack.config.js wird ja via dem WebpackMergePlugin in die Config von Shopware integriert. Das heißt, alles was du dort anpasst, wird auch Global angewendet. Wenn du nun also zum Beispiel in deinen externals flatpickr angeben würdest, würde der Flatpickr generell aus dem kompletten all.js-Bundle entfernt und über extern als Library erwartet. Dies würde dann wiederum dazu führen, dass die Chunk-Identifier bei Produktivumgebungen nicht mehr zusammenpassen, da die komplette Struktur nun eine andere ist.

Als andere Lösung wäre noch, dass die Library komplett als Asset mit dem Plugin ausgeliefert wird und im Template dann als solches Referenziert wird (siehe Add Custom Assets | Shopware Documentation)

Dies würde aber dann heißen, dass diese Library separat aktuell gehalten werden muss und das kann eventuell dazu führen, dass Inkompatibilitäten oder Deprecated-Api’s eher spät oder sogar gar nicht auffallen.

Viele Grüße

Martin

Ok, wenn sich das tatsächlich nur auf das DEV-System bezieht, könnte man dass natürlich machen. Ich ja aber doch vermute, dass der Hash des Chunks ein md5 über „Hersteller/PluginName“ ist oder nicht ? Weil dann müsste der Hash doch gleich sein egal wo dieser erzeugt wird.

Und mit den Abhängigkeiten meine ich eher libs die „Abhängigkeiten der Abhängigkeiten“. Und dann ergibt sich dadurch da vielleicht ein Vorteil. Bleiben wir mal bei Flatpickr und dann gibt es vielleicht eine Libs, die auch Flatpickr nutzt. Hier dann aber in einer Version, die mit der in Shopware „eingebauten“ inkompatibel ist.

Ich habe die Libs halt als installierbare Assets im Plugin drin. Sprich kein 3rd Party CDNs.

LG

Carsten

Guten Morgen Carsten!

Leider nicht, sonst wäre das dann doch etwas einfacher. Die ModuleId Option ist bei Shopware auf „deterministic“ (WebPack | Optimization.moduleIds) und chunkIds auf „named“ (WebPack | Optimization.chunkIds) gestellt. Das heißt, dass die ModuleIds um die es hier ja letztendlich geht von Webpack dann so zusammengeführt werden, dass das Caching für den Browser so effizient wie möglich verläuft. Daraus rührt aber auch das Problem mit den fehlerhaft referenzierten ModuleIds, da der eigentliche „Shopware-Chunk“ nicht mehr der ist, der mit Shopware Standard ausgeliefert wird.

Meine Lösung zielt ja darauf ab, das Deterministische Module-Naming so umzubiegen, dass die durch die Plugins hinzugefügte Modules auf jeden Fall separat behandelt werden, egal ob diese mit in den Core passen würden oder nicht. Die Module-Ids der Shopware-Files müssen unter allen Umständen nach dem Build genauso aussehen wie OOB, da wir ja sonst das genannte Problem haben.

Das führt dann nur zu einem unnötigen Overhead bei den Ressourcen. Der Flatpickr der als Asset geladen wird und seine eigenen Dependencies mit im Bundle mitbringt ist dadurch in der Dateigröße unnötig größer und muss vom Browser des Users natürlich mit geladen werden. In Zeiten von begrenztem Mobilem Datenverbrauch ist das eben noch immer ein Thema, auf das ein Entwickler achten sollte. In Sachen Kompatibilität ist das weniger ein Thema, da das SW-Bundle („all.js“) in sich abgeschlossen ist und keine Referenzen zu externen Libs hat, somit auch unbeeindruckt von externen Inhalten ist. Hier also auch wieder der Vorteil beim Bundler, da hier das Dependency-Management die zu ladende Datei so klein wie möglich hält.

Wie gesagt, kann man machen - ist aber wie schon gesagt mit eventuellem Overhead belastet.

Ich hoffe ich konnte dir helfen :slight_smile:

Viele Grüße

Martin