Rolf B: Anwendung vom ESM Modulen im Browser

Beitrag lesen

Update: Ich konnte file-type jetzt mit Webpack 5 zum Übersetzen bringen, aber es ist eine Quälerei. Der Autor pfeift auf den Browser und entwickelt primär für Node - es gibt keine Doku, was man alles tun muss, damit es im Browser funktioniert.

(1) Man muss seinen eigenen Code in ein ES6-Modul stecken

(2) Man muss entweder aus seinem Modul heraus eine globale Variable setzen, um auf exportierte Elemente zugreifen zu können, ODER man muss nach jeder Codeänderung Webpack neu laufen lassen. Denn ein export Statement wird von Webpack nicht ins Bundle übertragen.

(3) Man muss im Projektordner

npm install webpack --save-dev npm install webpack-cli --save-dev npm install buffer npm install file-type

ausführen, und nun kommt der Extraspaß: Das richtige Setup von Package und Webpack.

In der package.json wird gebraucht:

  • "private": "true"
  • "type": "module"

Die webpack.config.js, die in allen Beispielen mit module.exports hantiert, funktioniert in einem ESM-Projekt natürlich anders. Man muss das Config-Objekt nicht an module.exports zuweisen, sondern als Default exportieren:

const nodeResolverPlugin = ?!?!?!;
const distPath = ?!?!?!;

export default {
   entry: "./src/main.js",
   output: {
      path: distPath,
      filename: 'app.js'
   },
   mode: "production",
   plugins: [ nodeResolverPlugin ]
};

Im src Order steht der js Code, den man selbst schreibt. Webpack bündelt das zusammen mit allen node-modules und schreibt es in den output-Ordner. Unter path muss ein absoluter Path angegeben werden. Das kann man so lösen:

import path from 'path';
const distPath = path.resolve("./dist");

dist steht für Distribution, und die Angaben von ./src und ./dist setzen voraus, dass diese beiden Unterordner im gleichen Ordner wie die webpack.config.js stehen.

Für Code in der webpack.config.js kann man munter mit Node-Libraries herumhantieren, weil dieses Script von Webpack ausgeführt wird, also in der Node-Umgebung.

Das nodeResolverPlugin ist lebensnotwendig, das habe ich mühsam aus den Issues vom file-type Projekt herausgefunden. Einige der Dependencies von file-type verwenden nämlich `import xyz from 'node:stream' - und das will man nicht - der Code soll ja im Browser laufen. Deshalb schreibt man diesen Helper (1:1 aus den Issues kopiert):

import webpack from 'webpack';

const nodeResolverPlugin = new webpack.NormalModuleReplacementPlugin(/node:/, (resource) => {
  const mod = resource.request.replace(/^node:/, "");
  switch (mod) {
    case "buffer":
      resource.request = "buffer";
      break;
    case "stream":
      resource.request = "readable-stream";
      break;
    default:
      throw new Error(`Not found ${mod}`);
  }
});

Damit werden die diversen import ... from 'node:buffer' Anweisungen von der nodeJs Abhängigkeit befreit. readable-stream ist ein npm-Paket, das mit file-type mitkommen sollte, bei mir aber nicht immer mitkam. Wenn es fehlt, kann es explizit als readable-stream@3.6.2 nachinstalliert werden. Nicht die 4-er Version verwenden, die hat bei mir nur gekracht.

Damit konnte ich dann die Application durch Webpack durchlaufen lassen. Ohne Webpack geht gar nichts, weil die Import-Laufzeitumgebung von Node im Browser komplett fehlt.

Nur - ein Aufrufversuch schlug dann wieder fehl, weil eins der Module auf die Variable process zugreifen wollte, die in Node möglicherweise global ist und im Browser nicht existiert. Das muss man patchen, das scheint ein Fehler in readable-stream.js zu sein.

Man installiere noch das NPM-Modul process (Userland-Implementierung von 'node:process') und ändere in readable-stream/lib die Datei _stream_readable.js so ab:

'use strict';

module.exports = Readable;

var process = require('process');   // <<- diese Zeile einfügen

Und HEY, nach nur wenigen Stunden läuft dieser Dreck!!!

Meine Sourcen:

// main.js
import {getFileType} from "./getFileType.js";

const url = 'https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg';

console.log("Query file type for ", url);
try {
  const t = await getFileType('https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg');
  console.log("File Type is ", t);
}
catch (err) {
  console.log("that failed: ", err);
}
// getFileType.js
import { fileTypeFromStream } from "file-type";

export { getFileType };

async function getFileType(url) {

	const response = await fetch(url);
	const fileType = await fileTypeFromStream(response.body);
	
	return fileType;
}
// webpack.config.js
import path from 'path';
import webpack from 'webpack';

const distPath = path.resolve('./dist');
console.log("Distpath=",distPath);

const nodeResolverPlugin = new webpack.NormalModuleReplacementPlugin(/node:/, (resource) => {
  const mod = resource.request.replace(/^node:/, "");
  switch (mod) {
    case "buffer":
      resource.request = "buffer";
      break;
    case "stream":
      resource.request = "readable-stream";
      break;
    default:
      throw new Error(`Not found ${mod}`);
  }
})
export default {
  entry: './src/main.js',
  output: {
    path: distPath,
    filename: 'app.js',
  },
  mode: 'production',
  plugins: [
    nodeResolverPlugin
  ]
};

Und die package.json - in der sind aber nur die beide Zeilen mit private und type von mir ergänzt. Die Dependencies macht NPM. Wichtig ist nur, dass Webpack mit --save-dev installiert wird, damit es in den devDependencies erscheint. Es schadet tatächlich nichts, wenn es falsch steht, weil es in der App keinen Import darauf gibt und es deshalb vom Shaker wieder entfernt wird, aber es stört nur beim Build.

{
  "name": "file-type-test",
  "version": "1.0.0",
  "description": "",
  "private": "true",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "buffer": "^6.0.3",
    "file-type": "^18.4.0",
    "process": "^0.11.10"
  },
  "devDependencies": {
    "webpack": "^5.85.0",
    "webpack-cli": "^5.1.1"
  }
}

Test.html zum Einbinden:

<!doctype html>
<html>
<head>
<title>NPM Module</title>
<script type="module" src="./dist/app.js"></script>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>

Ist das jetzt schön? Nö. Aber göht - öhm - geht.

Rolf

--
sumpsi - posui - obstruxi