Skip to content

Commit be44a2e

Browse files
committedJun 8, 2020
Reworked Streaming API behavior in browser environments to fix #204.
I removed conditional requires that were causing problems described in #204, so it shouldn't be a problem now. The reasoning behind these conditions was to avoid bloating browser bundles that usually don't require Streaming API. Streaming API is built upon 'streams' Node.js module that can grow the bundles up to ~100 Kb. To still keep this benefit while allowing users to explicitly enable Streaming API on the browser, I restructured the dependencies: 1. In package.json 'browser.stream' module is set to false, thus ignored by default (require('stream') returns empty object). 2. In index.js we check if require('stream') returns non-empty object and enable Streaming API by dependency-injecting this module to streams.js. 3. Otherwise, we create small shims for 'encodeStream' and 'decodeStream' functions that throw exceptions when called, with a message that helps users to explicitly enable Streaming APIs using 'iconv.enableStreamingAPI'. The idea is that this should be a very rare case. Also add tests for browser streaming behaviors with webpack/karma setup
1 parent b7288df commit be44a2e

11 files changed

+330
-208
lines changed
 

‎.travis.yml

+18-23
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
1-
sudo: false
2-
language: node_js
3-
node_js:
4-
- "0.10"
5-
- "0.11"
6-
- "0.12"
7-
- "iojs"
8-
- "4"
9-
- "6"
10-
- "8"
11-
- "10"
12-
- "12"
13-
- "node"
14-
15-
env:
16-
- CXX=g++-4.8
17-
addons:
18-
apt:
19-
sources:
20-
- ubuntu-toolchain-r-test
21-
packages:
22-
- gcc-4.8
23-
- g++-4.8
1+
language: node_js
2+
node_js:
3+
- "0.10"
4+
- "0.11"
5+
- "0.12"
6+
- "iojs"
7+
- "4"
8+
- "6"
9+
- "8"
10+
- "10"
11+
- "12"
12+
- "node"
2413

14+
jobs:
15+
include:
16+
- name: webpack
17+
node_js: "12"
18+
install: cd test/webpack; npm install
19+
script: npm test

‎README.md

+9-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
## Pure JS character encoding conversion
1+
## iconv-lite: Pure JS character encoding conversion
22

3-
* Doesn't need native code compilation. Works on Windows and in sandboxed environments like [Cloud9](http://c9.io).
3+
* No need for native code compilation. Quick to install, works on Windows and in sandboxed environments like [Cloud9](http://c9.io).
44
* Used in popular projects like [Express.js (body_parser)](https://github.com/expressjs/body-parser),
55
[Grunt](http://gruntjs.com/), [Nodemailer](http://www.nodemailer.com/), [Yeoman](http://yeoman.io/) and others.
66
* Faster than [node-iconv](https://github.com/bnoordhuis/node-iconv) (see below for performance comparison).
7-
* Intuitive encode/decode API
8-
* Streaming support for Node v0.10+
9-
* In-browser usage via [Browserify](https://github.com/substack/node-browserify) (~180k gzip compressed with Buffer shim included).
7+
* Intuitive encode/decode API, including Streaming support.
8+
* In-browser usage via [browserify](https://github.com/substack/node-browserify) or [webpack](https://webpack.js.org/) (~180kb gzip compressed with Buffer shim included).
109
* Typescript [type definition file](https://github.com/ashtuchkin/iconv-lite/blob/master/lib/index.d.ts) included.
11-
* React Native is supported (need to explicitly `npm install` two more modules: `buffer` and `stream`).
10+
* React Native is supported (need to install `stream` module to enable Streaming API).
1211
* License: MIT.
1312

1413
[![NPM Stats](https://nodei.co/npm/iconv-lite.png)](https://npmjs.org/package/iconv-lite/)
@@ -22,20 +21,20 @@
2221
```javascript
2322
var iconv = require('iconv-lite');
2423

25-
// Convert from an encoded buffer to js string.
24+
// Convert from an encoded buffer to a js string.
2625
str = iconv.decode(Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f]), 'win1251');
2726

28-
// Convert from js string to an encoded buffer.
27+
// Convert from a js string to an encoded buffer.
2928
buf = iconv.encode("Sample input string", 'win1251');
3029

3130
// Check if encoding is supported
3231
iconv.encodingExists("us-ascii")
3332
```
3433

35-
### Streaming API (Node v0.10+)
34+
### Streaming API
3635
```javascript
3736

38-
// Decode stream (from binary stream to js strings)
37+
// Decode stream (from binary data stream to js strings)
3938
http.createServer(function(req, res) {
4039
var converterStream = iconv.decodeStream('win1251');
4140
req.pipe(converterStream);

‎lib/index.js

+41-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"use strict";
22

3-
// Some environments don't have global Buffer (e.g. React Native).
4-
// Solution would be installing npm modules "buffer" and "stream" explicitly.
53
var Buffer = require("safer-buffer").Buffer;
64

75
var bomHandling = require("./bom-handling"),
@@ -133,18 +131,50 @@ iconv.getDecoder = function getDecoder(encoding, options) {
133131
return decoder;
134132
}
135133

134+
// Streaming API
135+
// NOTE: Streaming API naturally depends on 'stream' module from Node.js. Unfortunately in browser environments this module can add
136+
// up to 100Kb to the output bundle. To avoid unnecessary code bloat, we don't enable Streaming API in browser by default.
137+
// If you would like to enable it explicitly, please add the following code to your app:
138+
// > iconv.enableStreamingAPI(require('stream'));
139+
iconv.enableStreamingAPI = function enableStreamingAPI(stream_module) {
140+
if (iconv.supportsStreams)
141+
return;
142+
143+
// Dependency-inject stream module to create IconvLite stream classes.
144+
var streams = require("./streams")(stream_module);
145+
146+
// Not public API yet, but expose the stream classes.
147+
iconv.IconvLiteEncoderStream = streams.IconvLiteEncoderStream;
148+
iconv.IconvLiteDecoderStream = streams.IconvLiteDecoderStream;
149+
150+
// Streaming API.
151+
iconv.encodeStream = function encodeStream(encoding, options) {
152+
return new iconv.IconvLiteEncoderStream(iconv.getEncoder(encoding, options), options);
153+
}
136154

137-
// Load extensions in Node. All of them are omitted in Browserify build via 'browser' field in package.json.
138-
var nodeVer = typeof process !== 'undefined' && process.versions && process.versions.node;
139-
if (nodeVer) {
140-
141-
// Load streaming support in Node v0.10+
142-
var nodeVerArr = nodeVer.split(".").map(Number);
143-
if (nodeVerArr[0] > 0 || nodeVerArr[1] >= 10) {
144-
require("./streams")(iconv);
155+
iconv.decodeStream = function decodeStream(encoding, options) {
156+
return new iconv.IconvLiteDecoderStream(iconv.getDecoder(encoding, options), options);
145157
}
158+
159+
iconv.supportsStreams = true;
160+
}
161+
162+
// Enable Streaming API automatically if 'stream' module is available and non-empty (the majority of environments).
163+
var stream_module;
164+
try {
165+
stream_module = require("stream");
166+
} catch (e) {}
167+
168+
if (stream_module && stream_module.Transform) {
169+
iconv.enableStreamingAPI(stream_module);
170+
171+
} else {
172+
// In rare cases where 'stream' module is not available by default, throw a helpful exception.
173+
iconv.encodeStream = iconv.decodeStream = function() {
174+
throw new Error("iconv-lite Streaming API is not enabled. Use iconv.enableStreamingAPI(require('stream')); to enable it.");
175+
};
146176
}
147177

148178
if ("Ā" != "\u0100") {
149-
console.error("iconv-lite warning: javascript files use encoding different from utf-8. See https://github.com/ashtuchkin/iconv-lite/wiki/Javascript-source-file-encodings for more info.");
179+
console.error("iconv-lite warning: js files use non-utf8 encoding. See https://github.com/ashtuchkin/iconv-lite/wiki/Javascript-source-file-encodings for more info.");
150180
}

‎lib/streams.js

+89-101
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,109 @@
11
"use strict";
22

3-
var Buffer = require("buffer").Buffer,
4-
Transform = require("stream").Transform;
3+
var Buffer = require("safer-buffer").Buffer;
54

5+
// NOTE: Due to 'stream' module being pretty large (~100Kb, significant in browser environments),
6+
// we opt to dependency-inject it instead of creating a hard dependency.
7+
module.exports = function(stream_module) {
8+
var Transform = stream_module.Transform;
69

7-
// == Exports ==================================================================
8-
module.exports = function(iconv) {
9-
10-
// Additional Public API.
11-
iconv.encodeStream = function encodeStream(encoding, options) {
12-
return new IconvLiteEncoderStream(iconv.getEncoder(encoding, options), options);
10+
// == Encoder stream =======================================================
11+
12+
function IconvLiteEncoderStream(conv, options) {
13+
this.conv = conv;
14+
options = options || {};
15+
options.decodeStrings = false; // We accept only strings, so we don't need to decode them.
16+
Transform.call(this, options);
1317
}
1418

15-
iconv.decodeStream = function decodeStream(encoding, options) {
16-
return new IconvLiteDecoderStream(iconv.getDecoder(encoding, options), options);
19+
IconvLiteEncoderStream.prototype = Object.create(Transform.prototype, {
20+
constructor: { value: IconvLiteEncoderStream }
21+
});
22+
23+
IconvLiteEncoderStream.prototype._transform = function(chunk, encoding, done) {
24+
if (typeof chunk != 'string')
25+
return done(new Error("Iconv encoding stream needs strings as its input."));
26+
try {
27+
var res = this.conv.write(chunk);
28+
if (res && res.length) this.push(res);
29+
done();
30+
}
31+
catch (e) {
32+
done(e);
33+
}
1734
}
1835

19-
iconv.supportsStreams = true;
36+
IconvLiteEncoderStream.prototype._flush = function(done) {
37+
try {
38+
var res = this.conv.end();
39+
if (res && res.length) this.push(res);
40+
done();
41+
}
42+
catch (e) {
43+
done(e);
44+
}
45+
}
2046

47+
IconvLiteEncoderStream.prototype.collect = function(cb) {
48+
var chunks = [];
49+
this.on('error', cb);
50+
this.on('data', function(chunk) { chunks.push(chunk); });
51+
this.on('end', function() {
52+
cb(null, Buffer.concat(chunks));
53+
});
54+
return this;
55+
}
2156

22-
// Not published yet.
23-
iconv.IconvLiteEncoderStream = IconvLiteEncoderStream;
24-
iconv.IconvLiteDecoderStream = IconvLiteDecoderStream;
25-
iconv._collect = IconvLiteDecoderStream.prototype.collect;
26-
};
2757

58+
// == Decoder stream =======================================================
2859

29-
// == Encoder stream =======================================================
30-
function IconvLiteEncoderStream(conv, options) {
31-
this.conv = conv;
32-
options = options || {};
33-
options.decodeStrings = false; // We accept only strings, so we don't need to decode them.
34-
Transform.call(this, options);
35-
}
36-
37-
IconvLiteEncoderStream.prototype = Object.create(Transform.prototype, {
38-
constructor: { value: IconvLiteEncoderStream }
39-
});
40-
41-
IconvLiteEncoderStream.prototype._transform = function(chunk, encoding, done) {
42-
if (typeof chunk != 'string')
43-
return done(new Error("Iconv encoding stream needs strings as its input."));
44-
try {
45-
var res = this.conv.write(chunk);
46-
if (res && res.length) this.push(res);
47-
done();
48-
}
49-
catch (e) {
50-
done(e);
60+
function IconvLiteDecoderStream(conv, options) {
61+
this.conv = conv;
62+
options = options || {};
63+
options.encoding = this.encoding = 'utf8'; // We output strings.
64+
Transform.call(this, options);
5165
}
52-
}
5366

54-
IconvLiteEncoderStream.prototype._flush = function(done) {
55-
try {
56-
var res = this.conv.end();
57-
if (res && res.length) this.push(res);
58-
done();
59-
}
60-
catch (e) {
61-
done(e);
62-
}
63-
}
64-
65-
IconvLiteEncoderStream.prototype.collect = function(cb) {
66-
var chunks = [];
67-
this.on('error', cb);
68-
this.on('data', function(chunk) { chunks.push(chunk); });
69-
this.on('end', function() {
70-
cb(null, Buffer.concat(chunks));
67+
IconvLiteDecoderStream.prototype = Object.create(Transform.prototype, {
68+
constructor: { value: IconvLiteDecoderStream }
7169
});
72-
return this;
73-
}
74-
75-
76-
// == Decoder stream =======================================================
77-
function IconvLiteDecoderStream(conv, options) {
78-
this.conv = conv;
79-
options = options || {};
80-
options.encoding = this.encoding = 'utf8'; // We output strings.
81-
Transform.call(this, options);
82-
}
83-
84-
IconvLiteDecoderStream.prototype = Object.create(Transform.prototype, {
85-
constructor: { value: IconvLiteDecoderStream }
86-
});
87-
88-
IconvLiteDecoderStream.prototype._transform = function(chunk, encoding, done) {
89-
if (!Buffer.isBuffer(chunk))
90-
return done(new Error("Iconv decoding stream needs buffers as its input."));
91-
try {
92-
var res = this.conv.write(chunk);
93-
if (res && res.length) this.push(res, this.encoding);
94-
done();
95-
}
96-
catch (e) {
97-
done(e);
70+
71+
IconvLiteDecoderStream.prototype._transform = function(chunk, encoding, done) {
72+
if (!Buffer.isBuffer(chunk))
73+
return done(new Error("Iconv decoding stream needs buffers as its input."));
74+
try {
75+
var res = this.conv.write(chunk);
76+
if (res && res.length) this.push(res, this.encoding);
77+
done();
78+
}
79+
catch (e) {
80+
done(e);
81+
}
9882
}
99-
}
10083

101-
IconvLiteDecoderStream.prototype._flush = function(done) {
102-
try {
103-
var res = this.conv.end();
104-
if (res && res.length) this.push(res, this.encoding);
105-
done();
84+
IconvLiteDecoderStream.prototype._flush = function(done) {
85+
try {
86+
var res = this.conv.end();
87+
if (res && res.length) this.push(res, this.encoding);
88+
done();
89+
}
90+
catch (e) {
91+
done(e);
92+
}
10693
}
107-
catch (e) {
108-
done(e);
94+
95+
IconvLiteDecoderStream.prototype.collect = function(cb) {
96+
var res = '';
97+
this.on('error', cb);
98+
this.on('data', function(chunk) { res += chunk; });
99+
this.on('end', function() {
100+
cb(null, res);
101+
});
102+
return this;
109103
}
110-
}
111-
112-
IconvLiteDecoderStream.prototype.collect = function(cb) {
113-
var res = '';
114-
this.on('error', cb);
115-
this.on('data', function(chunk) { res += chunk; });
116-
this.on('end', function() {
117-
cb(null, res);
118-
});
119-
return this;
120-
}
121104

105+
return {
106+
IconvLiteEncoderStream: IconvLiteEncoderStream,
107+
IconvLiteDecoderStream: IconvLiteDecoderStream,
108+
};
109+
};

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"test": "mocha --reporter spec --grep ."
2727
},
2828
"browser": {
29-
"./lib/streams": false
29+
"stream": false
3030
},
3131
"devDependencies": {
3232
"mocha": "^3.1.0",

‎test/browserify-test.js

-62
This file was deleted.

‎test/webpack/basic-test.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
var assert = require('assert').strict;
2+
3+
describe("iconv-lite", function() {
4+
var iconv;
5+
6+
it("can be require-d successfully", function() {
7+
// Emulate more complex environments that are both web- and node.js-compatible (e.g. Electron renderer process).
8+
// See https://github.com/ashtuchkin/iconv-lite/issues/204 for details.
9+
process.versions.node = "12.0.0";
10+
11+
iconv = require(".").iconv;
12+
});
13+
14+
it("does not support streams by default", function() {
15+
assert(!iconv.supportsStreams);
16+
17+
assert.throws(function() {
18+
iconv.encodeStream()
19+
}, /Streaming API is not enabled/);
20+
});
21+
22+
it("can encode/decode sbcs encodings", function() {
23+
var buf = iconv.encode("abc", "win1251");
24+
var str = iconv.decode(buf, "win1251");
25+
assert.equal(str, "abc");
26+
});
27+
28+
it("can encode/decode dbcs encodings", function() {
29+
var buf = iconv.encode("abc", "shiftjis");
30+
var str = iconv.decode(buf, "shiftjis");
31+
assert.equal(str, "abc");
32+
});
33+
34+
it("can encode/decode internal encodings", function() {
35+
var buf = iconv.encode("💩", "utf8");
36+
var str = iconv.decode(buf, "utf8");
37+
assert.equal(str, "💩");
38+
});
39+
});
40+
41+
describe("stream module", function() {
42+
it("is not included in the bundle", function() {
43+
var stream_module_name = "stream";
44+
assert.throws(function() { return require(stream_module_name) }, /Cannot find module 'stream'/);
45+
});
46+
});

‎test/webpack/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
// Reexport iconv-lite for tests.
3+
exports.iconv = require('iconv-lite');

‎test/webpack/karma.conf.js

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Karma configuration
2+
// Generated on Sat May 23 2020 18:02:48 GMT-0400 (Eastern Daylight Time)
3+
4+
module.exports = function(config) {
5+
config.set({
6+
7+
// base path that will be used to resolve all patterns (eg. files, exclude)
8+
basePath: '',
9+
10+
11+
// frameworks to use
12+
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13+
frameworks: ['mocha'],
14+
15+
16+
// list of files / patterns to load in the browser
17+
files: [
18+
{ pattern: '*test.js', watched: false },
19+
],
20+
21+
// preprocess matching files before serving them to the browser
22+
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
23+
preprocessors: {
24+
'*test.js': ['webpack']
25+
},
26+
27+
webpack: {
28+
"mode": "development",
29+
// karma watches the test entry points
30+
// (you don't need to specify the entry option)
31+
// webpack watches dependencies
32+
// webpack configuration
33+
},
34+
35+
webpackMiddleware: {
36+
// Don't watch.
37+
"watchOptions": {
38+
ignored: ["**/*"],
39+
},
40+
},
41+
42+
// test results reporter to use
43+
// possible values: 'dots', 'progress'
44+
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
45+
reporters: ['progress'],
46+
47+
48+
// web server port
49+
port: 9876,
50+
51+
52+
// enable / disable colors in the output (reporters and logs)
53+
colors: true,
54+
55+
56+
// level of logging
57+
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
58+
logLevel: config.LOG_INFO,
59+
60+
61+
// enable / disable watching file and executing tests whenever any file changes
62+
autoWatch: false,
63+
64+
65+
// start these browsers
66+
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
67+
browsers: ['PhantomJS'],
68+
69+
70+
// Continuous Integration mode
71+
// if true, Karma captures browsers, runs the tests and exits
72+
singleRun: true,
73+
74+
// Concurrency level
75+
// how many browser should be started simultaneous
76+
concurrency: Infinity
77+
})
78+
}

‎test/webpack/package.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "webpack-test",
3+
"private": true,
4+
"version": "1.0.0",
5+
"scripts": {
6+
"test": "karma start"
7+
},
8+
"devDependencies": {
9+
"karma": "^5.0.9",
10+
"karma-mocha": "^2.0.1",
11+
"karma-phantomjs-launcher": "^1.0.4",
12+
"karma-webpack": "^4.0.2",
13+
"mocha": "^7.2.0",
14+
"phantomjs-prebuilt": "^2.1.16",
15+
"webpack": "^4.43.0"
16+
},
17+
"dependencies": {
18+
"iconv-lite": "file:../.."
19+
}
20+
}

‎test/webpack/stream-test.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
var assert = require('assert').strict;
2+
3+
describe("iconv-lite with streams", function() {
4+
var iconv = require(".").iconv;
5+
6+
it("supports streams when explicitly enabled", function() {
7+
iconv.enableStreamingAPI(require('stream'));
8+
assert(iconv.supportsStreams);
9+
});
10+
11+
it("can encode/decode in streaming mode", function(done) {
12+
var stream1 = iconv.encodeStream("win1251");
13+
var stream2 = iconv.decodeStream("win1251");
14+
stream1.pipe(stream2);
15+
16+
stream1.end("abc");
17+
stream2.collect(function(err, str) {
18+
if (err)
19+
return done(err);
20+
21+
assert.equal(str, "abc");
22+
done(null);
23+
});
24+
});
25+
});

0 commit comments

Comments
 (0)
Please sign in to comment.