From 0ea1d360d8cc904d5d2077cf73f758c3c4c29c29 Mon Sep 17 00:00:00 2001 From: Sri Charan Date: Sun, 11 Oct 2026 20:41:59 +0530 Subject: [PATCH 1/3] fix(yaml): preserve numeric mapping keys in generated modules --- packages/yaml/src/index.js | 23 +++++++- .../test/fixtures/numeric-keys/config.yaml | 6 ++ .../yaml/test/fixtures/numeric-keys/main.js | 1 + .../fixtures/serialized-values/config.yaml | 7 +++ .../test/fixtures/serialized-values/main.js | 1 + packages/yaml/test/test.js | 57 +++++++++++++++++++ 6 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 packages/yaml/test/fixtures/numeric-keys/config.yaml create mode 100644 packages/yaml/test/fixtures/numeric-keys/main.js create mode 100644 packages/yaml/test/fixtures/serialized-values/config.yaml create mode 100644 packages/yaml/test/fixtures/serialized-values/main.js diff --git a/packages/yaml/src/index.js b/packages/yaml/src/index.js index 4980ad7e3..eb362ce87 100755 --- a/packages/yaml/src/index.js +++ b/packages/yaml/src/index.js @@ -8,6 +8,27 @@ const defaults = { extensions: ['.yaml', '.yml'] }; +function serialize(data, ancestors = []) { + if ( + data === null || + typeof data !== 'object' || + data instanceof Date || + data instanceof RegExp || + data instanceof Map || + data instanceof Set + ) { + return toSource(data); + } + if (ancestors.includes(data)) return '{$circularReference:1}'; + const parents = ancestors.concat([data]); + if (Array.isArray(data)) { + return `[${data.map((value) => serialize(value, parents)).join(',')}]`; + } + return `{${Object.keys(data) + .map((key) => `${JSON.stringify(key)}:${serialize(data[key], parents)}`) + .join(',')}}`; +} + export default function yaml(opts = {}) { const options = Object.assign({}, defaults, opts); const { documentMode, extensions } = options; @@ -48,7 +69,7 @@ export default function yaml(opts = {}) { } const keys = Object.keys(data).filter((key) => key === makeLegalIdentifier(key)); - const code = `var data = ${toSource(data)};\n\n`; + const code = `var data = ${serialize(data)};\n\n`; const exports = ['export default data;'] .concat(keys.map((key) => `export var ${key} = data.${key};`)) .join('\n'); diff --git a/packages/yaml/test/fixtures/numeric-keys/config.yaml b/packages/yaml/test/fixtures/numeric-keys/config.yaml new file mode 100644 index 000000000..91ab9a28f --- /dev/null +++ b/packages/yaml/test/fixtures/numeric-keys/config.yaml @@ -0,0 +1,6 @@ +'00': zeroes +'01': leading zero +'08': eight +'9007199254740993': large integer +nested: + '01': nested leading zero diff --git a/packages/yaml/test/fixtures/numeric-keys/main.js b/packages/yaml/test/fixtures/numeric-keys/main.js new file mode 100644 index 000000000..62a9af601 --- /dev/null +++ b/packages/yaml/test/fixtures/numeric-keys/main.js @@ -0,0 +1 @@ +export { default } from './config.yaml'; diff --git a/packages/yaml/test/fixtures/serialized-values/config.yaml b/packages/yaml/test/fixtures/serialized-values/config.yaml new file mode 100644 index 000000000..1f9561f0e --- /dev/null +++ b/packages/yaml/test/fixtures/serialized-values/config.yaml @@ -0,0 +1,7 @@ +timestamp: 2020-01-02T03:04:05Z +values: [.inf, -.inf, .nan, -0.0] +shared: &shared + '01': alias value +alias: *shared +loop: &loop + self: *loop diff --git a/packages/yaml/test/fixtures/serialized-values/main.js b/packages/yaml/test/fixtures/serialized-values/main.js new file mode 100644 index 000000000..5c7662903 --- /dev/null +++ b/packages/yaml/test/fixtures/serialized-values/main.js @@ -0,0 +1 @@ +export { default, shared } from './config.yaml'; diff --git a/packages/yaml/test/test.js b/packages/yaml/test/test.js index a5797a8f0..b45a7c7b6 100755 --- a/packages/yaml/test/test.js +++ b/packages/yaml/test/test.js @@ -49,6 +49,63 @@ test('generates named exports', async () => { return testBundle(undefined, bundle); }); +test('preserves quoted numeric mapping keys in an ES module', async () => { + const bundle = await rollup.rollup({ + input: 'fixtures/numeric-keys/main.js', + plugins: [yaml()] + }); + const { output } = await bundle.generate({ format: 'es' }); + expect(output[0].code).toContain('export'); + const { error, module } = await testBundle(undefined, bundle); + expect(error).toBeUndefined(); + expect(module.exports).toEqual({ + '00': 'zeroes', + '01': 'leading zero', + '08': 'eight', + '9007199254740993': 'large integer', + nested: { '01': 'nested leading zero' } + }); +}); + +test('preserves YAML timestamps, non-finite numbers and aliases', async () => { + const bundle = await rollup.rollup({ + input: 'fixtures/serialized-values/main.js', + plugins: [yaml()] + }); + const { error, module } = await testBundle(undefined, bundle, { + options: { exports: 'named' } + }); + expect(error).toBeUndefined(); + const data = module.exports.default; + expect(data.timestamp).toEqual(new Date('2020-01-02T03:04:05.000Z')); + expect(data.values).toEqual([Infinity, -Infinity, NaN, -0]); + expect(data.shared).toEqual({ '01': 'alias value' }); + expect(data.alias).toEqual(data.shared); + expect(module.exports.shared).toBe(data.shared); + expect(data.loop.self).toEqual({ $circularReference: 1 }); +}); + +test('preserves special values returned by a transform', async () => { + const data = { + regexp: /example/gi, + map: new Map([['key', 'value']]), + set: new Set(['value']), + fn: function value() { + return 42; + } + }; + const bundle = await rollup.rollup({ + input: 'fixtures/numeric-keys/main.js', + plugins: [yaml({ transform: () => data })] + }); + const { error, module } = await testBundle(undefined, bundle); + expect(error).toBeUndefined(); + expect(module.exports.regexp).toEqual(data.regexp); + expect(module.exports.map).toEqual(data.map); + expect(module.exports.set).toEqual(data.set); + expect(module.exports.fn()).toBe(42); +}); + test('resolves extensionless imports in conjunction with nodeResolve plugin', async () => { const bundle = await rollup.rollup({ input: 'fixtures/extensionless/main.js', From b09daf06c83cc62f1a687b6e969843d1dd2ce787 Mon Sep 17 00:00:00 2001 From: Sri Charan Date: Sun, 11 Oct 2026 20:54:08 +0530 Subject: [PATCH 2/3] fix(yaml): retain special object serialization --- packages/yaml/src/index.js | 13 ++--- .../test/fixtures/numeric-keys/config.yaml | 2 + packages/yaml/test/test.js | 47 +++++++++++++++++-- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/packages/yaml/src/index.js b/packages/yaml/src/index.js index eb362ce87..b190da1ad 100755 --- a/packages/yaml/src/index.js +++ b/packages/yaml/src/index.js @@ -9,15 +9,10 @@ const defaults = { }; function serialize(data, ancestors = []) { - if ( - data === null || - typeof data !== 'object' || - data instanceof Date || - data instanceof RegExp || - data instanceof Map || - data instanceof Set - ) { - return toSource(data); + if (data === null || typeof data !== 'object') return toSource(data); + if (!Array.isArray(data)) { + const prototype = Object.getPrototypeOf(data); + if (prototype !== Object.prototype && prototype !== null) return toSource(data); } if (ancestors.includes(data)) return '{$circularReference:1}'; const parents = ancestors.concat([data]); diff --git a/packages/yaml/test/fixtures/numeric-keys/config.yaml b/packages/yaml/test/fixtures/numeric-keys/config.yaml index 91ab9a28f..f7fb267a0 100644 --- a/packages/yaml/test/fixtures/numeric-keys/config.yaml +++ b/packages/yaml/test/fixtures/numeric-keys/config.yaml @@ -4,3 +4,5 @@ '9007199254740993': large integer nested: '01': nested leading zero +array: + - '01': array leading zero diff --git a/packages/yaml/test/test.js b/packages/yaml/test/test.js index b45a7c7b6..a25821206 100755 --- a/packages/yaml/test/test.js +++ b/packages/yaml/test/test.js @@ -56,14 +56,15 @@ test('preserves quoted numeric mapping keys in an ES module', async () => { }); const { output } = await bundle.generate({ format: 'es' }); expect(output[0].code).toContain('export'); - const { error, module } = await testBundle(undefined, bundle); + const { error, module } = await testBundle(null, bundle); expect(error).toBeUndefined(); expect(module.exports).toEqual({ '00': 'zeroes', '01': 'leading zero', '08': 'eight', '9007199254740993': 'large integer', - nested: { '01': 'nested leading zero' } + nested: { '01': 'nested leading zero' }, + array: [{ '01': 'array leading zero' }] }); }); @@ -72,7 +73,7 @@ test('preserves YAML timestamps, non-finite numbers and aliases', async () => { input: 'fixtures/serialized-values/main.js', plugins: [yaml()] }); - const { error, module } = await testBundle(undefined, bundle, { + const { error, module } = await testBundle(null, bundle, { options: { exports: 'named' } }); expect(error).toBeUndefined(); @@ -98,7 +99,7 @@ test('preserves special values returned by a transform', async () => { input: 'fixtures/numeric-keys/main.js', plugins: [yaml({ transform: () => data })] }); - const { error, module } = await testBundle(undefined, bundle); + const { error, module } = await testBundle(null, bundle); expect(error).toBeUndefined(); expect(module.exports.regexp).toEqual(data.regexp); expect(module.exports.map).toEqual(data.map); @@ -106,6 +107,44 @@ test('preserves special values returned by a transform', async () => { expect(module.exports.fn()).toBe(42); }); +test('preserves other object and array serialization returned by a transform', async () => { + const circular = {}; + circular.self = circular; + const circularArray = []; + circularArray.push(circularArray); + const custom = { + toSource: function customSource() { + return 'custom'; + } + }; + const data = { + boxedString: Object('abc'), + boxedNumber: Object(4), + boxedBoolean: Object(false), + error: new Error('example'), + custom, + sparse: Array(2).concat([1]), + proto: JSON.parse('{"__proto__":{"example":true}}'), + circularArray, + map: new Map([['key', circular]]) + }; + const bundle = await rollup.rollup({ + input: 'fixtures/numeric-keys/main.js', + plugins: [yaml({ transform: () => data })] + }); + const { error, module } = await testBundle(null, bundle); + expect(error).toBeUndefined(); + expect(module.exports.boxedString).toEqual({ 0: 'a', 1: 'b', 2: 'c' }); + expect(module.exports.boxedNumber).toEqual({}); + expect(module.exports.boxedBoolean).toEqual({}); + expect(module.exports.error).toEqual({}); + expect(module.exports.custom.toSource()).toBe('custom'); + expect(module.exports.sparse).toEqual(data.sparse); + expect(Object.getPrototypeOf(module.exports.proto)).toEqual({ example: true }); + expect(module.exports.circularArray).toEqual([{ $circularReference: 1 }]); + expect(module.exports.map.get('key')).toEqual({ self: { $circularReference: 1 } }); +}); + test('resolves extensionless imports in conjunction with nodeResolve plugin', async () => { const bundle = await rollup.rollup({ input: 'fixtures/extensionless/main.js', From 54fecebbb598900afdc1c4cb46e8d49f71b2ac26 Mon Sep 17 00:00:00 2001 From: Sri Charan Date: Sun, 11 Oct 2026 21:14:25 +0530 Subject: [PATCH 3/3] test(yaml): cover existing trailing-hole serialization --- packages/yaml/test/test.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/yaml/test/test.js b/packages/yaml/test/test.js index a25821206..988aa6a4a 100755 --- a/packages/yaml/test/test.js +++ b/packages/yaml/test/test.js @@ -145,6 +145,32 @@ test('preserves other object and array serialization returned by a transform', a expect(module.exports.map.get('key')).toEqual({ self: { $circularReference: 1 } }); }); +test('preserves existing serialization of arrays with trailing holes', async () => { + const data = { + allHoles: Array(2), + moreHoles: Array(3), + trailingHole: [1].concat(Array(1)), + trailingHoles: [1].concat(Array(2)) + }; + const bundle = await rollup.rollup({ + input: 'fixtures/numeric-keys/main.js', + plugins: [yaml({ transform: () => data })] + }); + const { error, module } = await testBundle(null, bundle); + expect(error).toBeUndefined(); + + // The existing tosource serializer drops one trailing hole. + for (const key of Object.keys(data)) { + const result = module.exports[key]; + expect(result.length).toBe(data[key].length - 1); + for (let index = 0; index < result.length; index += 1) { + expect(index in result).toBe(index in data[key]); + } + } + expect(module.exports.trailingHole[0]).toBe(1); + expect(module.exports.trailingHoles[0]).toBe(1); +}); + test('resolves extensionless imports in conjunction with nodeResolve plugin', async () => { const bundle = await rollup.rollup({ input: 'fixtures/extensionless/main.js',