diff --git a/docs/web/docs/features/proposals/iterator-join.md b/docs/web/docs/features/proposals/iterator-join.md new file mode 100644 index 000000000000..88cc8df32d94 --- /dev/null +++ b/docs/web/docs/features/proposals/iterator-join.md @@ -0,0 +1,28 @@ +# Iterator join +[Specification](https://bakkot.github.io/proposal-iterator-join/)\ +[Proposal repo](https://github.com/bakkot/proposal-iterator-join) + +## Modules +[`esnext.iterator.join`](https://github.com/zloirock/core-js/blob/v4/packages/core-js/modules/esnext.iterator.join.js) + +## Built-ins signatures +```ts +class Iterator { + join(separator?: string | undefined): string; +} +``` + +## [Entry points]({docs-version}/docs/usage#h-entry-points) +```ts +core-js/proposals/iterator-join +core-js(-pure)/full/iterator/join +``` + +## Examples +```js +const digits = () => [1, 2, 3].values(); +digits().join(); // => '1,2,3' + +const words = () => ['Hello', 'core-js'].values(); +words().join(' '); // => 'Hello core-js' +``` diff --git a/docs/web/docs/menu.json b/docs/web/docs/menu.json index 6d134788fc38..881070958826 100644 --- a/docs/web/docs/menu.json +++ b/docs/web/docs/menu.json @@ -341,6 +341,10 @@ { "title": "Function.prototype.demethodize", "url": "{docs-version}/docs/features/proposals/function-prototype-demethodize" + }, + { + "title": "Iterator join", + "url": "{docs-version}/docs/features/proposals/iterator-join" } ] } diff --git a/packages/core-js-compat/src/data.mjs b/packages/core-js-compat/src/data.mjs index 0d886b963c81..c066e9fcbf0c 100644 --- a/packages/core-js-compat/src/data.mjs +++ b/packages/core-js-compat/src/data.mjs @@ -2347,6 +2347,8 @@ export const data = { }, 'esnext.iterator.concat': { }, + 'esnext.iterator.join': { + }, 'esnext.iterator.range': { }, 'esnext.iterator.to-async': { diff --git a/packages/core-js-pure/package.json b/packages/core-js-pure/package.json index 2c70dad28c41..947a1b84a72b 100644 --- a/packages/core-js-pure/package.json +++ b/packages/core-js-pure/package.json @@ -582,6 +582,8 @@ "./proposals/iterator-helpers.js": "./proposals/iterator-helpers.js", "./proposals/iterator-chunking": "./proposals/iterator-chunking.js", "./proposals/iterator-chunking.js": "./proposals/iterator-chunking.js", + "./proposals/iterator-join": "./proposals/iterator-join.js", + "./proposals/iterator-join.js": "./proposals/iterator-join.js", "./proposals/iterator-range": "./proposals/iterator-range.js", "./proposals/iterator-range.js": "./proposals/iterator-range.js", "./proposals/iterator-sequencing": "./proposals/iterator-sequencing.js", diff --git a/packages/core-js/modules/esnext.iterator.join.js b/packages/core-js/modules/esnext.iterator.join.js new file mode 100644 index 000000000000..bfe46ef53819 --- /dev/null +++ b/packages/core-js/modules/esnext.iterator.join.js @@ -0,0 +1,33 @@ +'use strict'; +var $ = require('../internals/export'); +var $toString = require('../internals/to-string'); +var anObject = require('../internals/an-object'); +var getIteratorDirect = require('../internals/get-iterator-direct'); +var isNullOrUndefined = require('../internals/is-null-or-undefined'); +var iterate = require('../internals/iterate'); +var iteratorClose = require('../internals/iterator-close'); +var uncurryThis = require('../internals/function-uncurry-this'); + +var $join = uncurryThis([].join); +var push = uncurryThis([].push); + +// `Iterator.prototype.join` method +// https://bakkot.github.io/proposal-iterator-join/ +// dependency: es.iterator.constructor +$({ target: 'Iterator', proto: true, real: true, forced: true }, { + join: function join(separator) { + var O = anObject(this); + var sep; + try { + sep = separator === undefined ? ',' : $toString(separator); + } catch (error) { + iteratorClose(O, 'throw', error); + } + var result = []; + var iterated = getIteratorDirect(O); + iterate(iterated, function (value) { + push(result, isNullOrUndefined(value) ? '' : $toString(value)); + }, { IS_RECORD: true }); + return $join(result, sep); + }, +}); diff --git a/packages/core-js/package.json b/packages/core-js/package.json index c1cf3a85ff30..383e1655697a 100644 --- a/packages/core-js/package.json +++ b/packages/core-js/package.json @@ -576,6 +576,8 @@ "./proposals/iterator-helpers.js": "./proposals/iterator-helpers.js", "./proposals/iterator-chunking": "./proposals/iterator-chunking.js", "./proposals/iterator-chunking.js": "./proposals/iterator-chunking.js", + "./proposals/iterator-join": "./proposals/iterator-join.js", + "./proposals/iterator-join.js": "./proposals/iterator-join.js", "./proposals/iterator-range": "./proposals/iterator-range.js", "./proposals/iterator-range.js": "./proposals/iterator-range.js", "./proposals/iterator-sequencing": "./proposals/iterator-sequencing.js", diff --git a/scripts/build-entries/entries-definitions.mjs b/scripts/build-entries/entries-definitions.mjs index 6d42ca6584c1..924ce71880ce 100644 --- a/scripts/build-entries/entries-definitions.mjs +++ b/scripts/build-entries/entries-definitions.mjs @@ -1306,6 +1306,18 @@ export const features = { namespace: 'Iterator', name: 'forEach', }, + 'iterator/join': { + modules: ['esnext.iterator.join'], + template: $uncurried, + namespace: 'Iterator', + name: 'join', + }, + 'iterator/prototype/join': { + modules: ['esnext.iterator.join'], + template: $prototype, + namespace: 'Iterator', + name: 'join', + }, 'iterator/map': { modules: ['es.iterator.map'], template: $uncurried, @@ -3457,6 +3469,13 @@ export const proposals = { 'esnext.iterator.windows', ], }, + 'iterator-join': { + link: 'https://github.com/bakkot/proposal-iterator-join', + stage: 0, + modules: [ + 'esnext.iterator.join', + ], + }, 'iterator-range': { link: 'https://github.com/tc39/proposal-Number.range', stage: 2, diff --git a/tests/babel-plugin/fixtures/entry-global/import-full-chrome-135-v4.0/output.mjs b/tests/babel-plugin/fixtures/entry-global/import-full-chrome-135-v4.0/output.mjs index 60b28c8d0b38..4dc0297d985c 100644 --- a/tests/babel-plugin/fixtures/entry-global/import-full-chrome-135-v4.0/output.mjs +++ b/tests/babel-plugin/fixtures/entry-global/import-full-chrome-135-v4.0/output.mjs @@ -33,6 +33,7 @@ import "core-js/modules/esnext.function.demethodize"; import "core-js/modules/esnext.function.metadata"; import "core-js/modules/esnext.iterator.chunks"; import "core-js/modules/esnext.iterator.concat"; +import "core-js/modules/esnext.iterator.join"; import "core-js/modules/esnext.iterator.range"; import "core-js/modules/esnext.iterator.to-async"; import "core-js/modules/esnext.iterator.windows"; diff --git a/tests/babel-plugin/fixtures/entry-global/import-full-chrome-135-v4.999/output.mjs b/tests/babel-plugin/fixtures/entry-global/import-full-chrome-135-v4.999/output.mjs index 60b28c8d0b38..4dc0297d985c 100644 --- a/tests/babel-plugin/fixtures/entry-global/import-full-chrome-135-v4.999/output.mjs +++ b/tests/babel-plugin/fixtures/entry-global/import-full-chrome-135-v4.999/output.mjs @@ -33,6 +33,7 @@ import "core-js/modules/esnext.function.demethodize"; import "core-js/modules/esnext.function.metadata"; import "core-js/modules/esnext.iterator.chunks"; import "core-js/modules/esnext.iterator.concat"; +import "core-js/modules/esnext.iterator.join"; import "core-js/modules/esnext.iterator.range"; import "core-js/modules/esnext.iterator.to-async"; import "core-js/modules/esnext.iterator.windows"; diff --git a/tests/babel-plugin/fixtures/entry-global/import-full-ie11-v4.0/output.mjs b/tests/babel-plugin/fixtures/entry-global/import-full-ie11-v4.0/output.mjs index 9d189725779d..802c02ed5444 100644 --- a/tests/babel-plugin/fixtures/entry-global/import-full-ie11-v4.0/output.mjs +++ b/tests/babel-plugin/fixtures/entry-global/import-full-ie11-v4.0/output.mjs @@ -299,6 +299,7 @@ import "core-js/modules/esnext.function.demethodize"; import "core-js/modules/esnext.function.metadata"; import "core-js/modules/esnext.iterator.chunks"; import "core-js/modules/esnext.iterator.concat"; +import "core-js/modules/esnext.iterator.join"; import "core-js/modules/esnext.iterator.range"; import "core-js/modules/esnext.iterator.to-async"; import "core-js/modules/esnext.iterator.windows"; diff --git a/tests/babel-plugin/fixtures/entry-global/import-full-ie11-v4.999/output.mjs b/tests/babel-plugin/fixtures/entry-global/import-full-ie11-v4.999/output.mjs index 9d189725779d..802c02ed5444 100644 --- a/tests/babel-plugin/fixtures/entry-global/import-full-ie11-v4.999/output.mjs +++ b/tests/babel-plugin/fixtures/entry-global/import-full-ie11-v4.999/output.mjs @@ -299,6 +299,7 @@ import "core-js/modules/esnext.function.demethodize"; import "core-js/modules/esnext.function.metadata"; import "core-js/modules/esnext.iterator.chunks"; import "core-js/modules/esnext.iterator.concat"; +import "core-js/modules/esnext.iterator.join"; import "core-js/modules/esnext.iterator.range"; import "core-js/modules/esnext.iterator.to-async"; import "core-js/modules/esnext.iterator.windows"; diff --git a/tests/babel-plugin/fixtures/entry-global/require-full-chrome-135-v4.0/output.mjs b/tests/babel-plugin/fixtures/entry-global/require-full-chrome-135-v4.0/output.mjs index 60b28c8d0b38..4dc0297d985c 100644 --- a/tests/babel-plugin/fixtures/entry-global/require-full-chrome-135-v4.0/output.mjs +++ b/tests/babel-plugin/fixtures/entry-global/require-full-chrome-135-v4.0/output.mjs @@ -33,6 +33,7 @@ import "core-js/modules/esnext.function.demethodize"; import "core-js/modules/esnext.function.metadata"; import "core-js/modules/esnext.iterator.chunks"; import "core-js/modules/esnext.iterator.concat"; +import "core-js/modules/esnext.iterator.join"; import "core-js/modules/esnext.iterator.range"; import "core-js/modules/esnext.iterator.to-async"; import "core-js/modules/esnext.iterator.windows"; diff --git a/tests/babel-plugin/fixtures/entry-global/require-full-chrome-135-v4.999/output.mjs b/tests/babel-plugin/fixtures/entry-global/require-full-chrome-135-v4.999/output.mjs index 60b28c8d0b38..4dc0297d985c 100644 --- a/tests/babel-plugin/fixtures/entry-global/require-full-chrome-135-v4.999/output.mjs +++ b/tests/babel-plugin/fixtures/entry-global/require-full-chrome-135-v4.999/output.mjs @@ -33,6 +33,7 @@ import "core-js/modules/esnext.function.demethodize"; import "core-js/modules/esnext.function.metadata"; import "core-js/modules/esnext.iterator.chunks"; import "core-js/modules/esnext.iterator.concat"; +import "core-js/modules/esnext.iterator.join"; import "core-js/modules/esnext.iterator.range"; import "core-js/modules/esnext.iterator.to-async"; import "core-js/modules/esnext.iterator.windows"; diff --git a/tests/babel-plugin/fixtures/entry-global/require-full-ie11-v4.0/output.mjs b/tests/babel-plugin/fixtures/entry-global/require-full-ie11-v4.0/output.mjs index 9d189725779d..802c02ed5444 100644 --- a/tests/babel-plugin/fixtures/entry-global/require-full-ie11-v4.0/output.mjs +++ b/tests/babel-plugin/fixtures/entry-global/require-full-ie11-v4.0/output.mjs @@ -299,6 +299,7 @@ import "core-js/modules/esnext.function.demethodize"; import "core-js/modules/esnext.function.metadata"; import "core-js/modules/esnext.iterator.chunks"; import "core-js/modules/esnext.iterator.concat"; +import "core-js/modules/esnext.iterator.join"; import "core-js/modules/esnext.iterator.range"; import "core-js/modules/esnext.iterator.to-async"; import "core-js/modules/esnext.iterator.windows"; diff --git a/tests/babel-plugin/fixtures/entry-global/require-full-ie11-v4.999/output.mjs b/tests/babel-plugin/fixtures/entry-global/require-full-ie11-v4.999/output.mjs index 9d189725779d..802c02ed5444 100644 --- a/tests/babel-plugin/fixtures/entry-global/require-full-ie11-v4.999/output.mjs +++ b/tests/babel-plugin/fixtures/entry-global/require-full-ie11-v4.999/output.mjs @@ -299,6 +299,7 @@ import "core-js/modules/esnext.function.demethodize"; import "core-js/modules/esnext.function.metadata"; import "core-js/modules/esnext.iterator.chunks"; import "core-js/modules/esnext.iterator.concat"; +import "core-js/modules/esnext.iterator.join"; import "core-js/modules/esnext.iterator.range"; import "core-js/modules/esnext.iterator.to-async"; import "core-js/modules/esnext.iterator.windows"; diff --git a/tests/compat/tests.js b/tests/compat/tests.js index 6490d28da986..25f95e877205 100644 --- a/tests/compat/tests.js +++ b/tests/compat/tests.js @@ -1837,6 +1837,9 @@ GLOBAL.tests = { 'esnext.iterator.concat': function () { return Iterator.concat; }, + 'esnext.iterator.join': function () { + return Iterator.prototype.join; + }, 'esnext.iterator.range': function () { return Iterator.range; }, diff --git a/tests/entries/unit.mjs b/tests/entries/unit.mjs index c695ca4cdd1f..400fc484ac48 100644 --- a/tests/entries/unit.mjs +++ b/tests/entries/unit.mjs @@ -742,6 +742,7 @@ for (PATH of ['@core-js/pure', 'core-js']) { ok(typeof load(NS, 'iterator/zip') == 'function'); ok(typeof load(NS, 'iterator/zip-keyed') == 'function'); ok(typeof load(NS, 'iterator/chunks') == 'function'); + ok(typeof load(NS, 'iterator/join') == 'function'); ok(typeof load(NS, 'iterator/to-async') == 'function'); ok(typeof load(NS, 'iterator/windows') == 'function'); ok(typeof load(NS, 'iterator/prototype/chunks') == 'function'); @@ -820,6 +821,7 @@ for (PATH of ['@core-js/pure', 'core-js']) { load('proposals/iterator-range'); load('proposals/iterator-sequencing'); load('proposals/iterator-chunking'); + load('proposals/iterator-join'); load('proposals/joint-iteration'); load('proposals/json-parse-with-source'); load('proposals/math-clamp'); diff --git a/tests/eslint/eslint.config.js b/tests/eslint/eslint.config.js index d366a323e4cb..2418696cbd77 100644 --- a/tests/eslint/eslint.config.js +++ b/tests/eslint/eslint.config.js @@ -1272,6 +1272,7 @@ const forbidCompletelyNonExistentBuiltIns = { ] }], 'es/no-nonstandard-iterator-prototype-properties': [ERROR, { allow: [ 'chunks', + 'join', 'sliding', 'toAsync', 'windows', diff --git a/tests/unit-global/esnext.iterator.join.js b/tests/unit-global/esnext.iterator.join.js new file mode 100644 index 000000000000..21957b54c977 --- /dev/null +++ b/tests/unit-global/esnext.iterator.join.js @@ -0,0 +1,40 @@ +import { createIterator } from '../helpers/helpers.js'; + +QUnit.test('Iterator#join', assert => { + const { join } = Iterator.prototype; + const symbol = Symbol('a'); + + assert.isFunction(join); + assert.arity(join, 1); + assert.name(join, 'join'); + assert.looksNative(join); + assert.nonEnumerable(Iterator.prototype, 'join'); + + const observableReturn = { + return() { + this.called = true; + return { done: true, value: undefined }; + }, + }; + const itObservable1 = createIterator([1, 2, 3], observableReturn); + + assert.strictEqual(join.call(itObservable1), '1,2,3', 'without separator attribute'); + assert.true(itObservable1.called, 'iterator closes'); + assert.strictEqual(join.call(createIterator([1, 2, 3]), '-'), '1-2-3', 'with separator attribute'); + assert.strictEqual(join.call(createIterator([1, null, 3]), '-'), '1--3', 'skips nullish values'); + assert.strictEqual(join.call(createIterator([1, undefined, 3]), '-'), '1--3', 'skips undefined values'); + assert.strictEqual(join.call(createIterator([])), '', 'empty iterator'); + + assert.throws(() => join.call(''), TypeError, 'iterable non-object this'); + assert.throws(() => join.call(undefined), TypeError, 'non-iterable-object this #1'); + assert.throws(() => join.call(null), TypeError, 'non-iterable-object this #2'); + assert.throws(() => join.call(5), TypeError, 'non-iterable-object this #3'); + + const itObservable2 = createIterator([1, 2, 3], observableReturn); + assert.throws(() => join.call(itObservable2, symbol), TypeError, 'throws on symbol separator'); + assert.true(itObservable2.called, 'iterator closes on argument validation error'); + + const itObservable3 = createIterator([1, symbol, 3], observableReturn); + assert.throws(() => join.call(itObservable3), TypeError, 'throws on non-stringable value'); + assert.true(itObservable3.called, 'iterator closes on value conversion error'); +}); diff --git a/tests/unit-pure/esnext.iterator.join.js b/tests/unit-pure/esnext.iterator.join.js new file mode 100644 index 000000000000..cb22ee745248 --- /dev/null +++ b/tests/unit-pure/esnext.iterator.join.js @@ -0,0 +1,42 @@ +import { createIterator } from '../helpers/helpers.js'; + +import Iterator from '@core-js/pure/full/iterator'; +import Symbol from '@core-js/pure/es/symbol'; + +QUnit.test('Iterator#join', assert => { + const { join } = Iterator.prototype; + + assert.isFunction(join); + assert.arity(join, 1); + assert.name(join, 'join'); + assert.nonEnumerable(Iterator.prototype, 'join'); + + const observableReturn = { + return() { + this.called = true; + return { done: true, value: undefined }; + }, + }; + + const itObservable1 = createIterator([1, 2, 3], observableReturn); + assert.strictEqual(join.call(itObservable1), '1,2,3', 'without separator attribute'); + assert.true(itObservable1.called, 'iterator closes'); + assert.strictEqual(join.call(createIterator([1, 2, 3]), '-'), '1-2-3', 'with separator attribute'); + assert.strictEqual(join.call(createIterator([1, null, 3]), '-'), '1--3', 'skips nullish values'); + assert.strictEqual(join.call(createIterator([1, undefined, 3]), '-'), '1--3', 'skips undefined values'); + assert.strictEqual(join.call(createIterator([])), '', 'empty iterator'); + + assert.throws(() => join.call(''), TypeError, 'iterable non-object this'); + assert.throws(() => join.call(undefined), TypeError, 'non-iterable-object this #1'); + assert.throws(() => join.call(null), TypeError, 'non-iterable-object this #2'); + assert.throws(() => join.call(5), TypeError, 'non-iterable-object this #3'); + + const symbol = Symbol('a'); + const itObservable2 = createIterator([1, 2, 3], observableReturn); + assert.throws(() => join.call(itObservable2, symbol), TypeError, 'throws on symbol separator'); + assert.true(itObservable2.called, 'iterator closes on argument validation error'); + + const itObservable3 = createIterator([1, symbol, 3], observableReturn); + assert.throws(() => join.call(itObservable3), TypeError, 'throws on non-stringable value'); + assert.true(itObservable3.called, 'iterator closes on value conversion error'); +});