Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: separately export __proto__ for compatibility with CJS Transpiler Re-exports #5380

Merged
merged 12 commits into from
Feb 15, 2024
1 change: 1 addition & 0 deletions cli/help.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Basic options:
--preserveModules Preserve module structure
--preserveModulesRoot Put preserved modules under this path at root level
--preserveSymlinks Do not follow symlinks when resolving files
--no-reexportProtoFromExternal Ignore `__proto__` in star re-exports
--no-sanitizeFileName Do not replace invalid characters in file names
--shimMissingExports Create shim variables for missing exports
--silent Don't print warnings
Expand Down
1 change: 1 addition & 0 deletions docs/command-line-interface/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ Many options have command line equivalents. In those cases, any arguments passed
--preserveModules Preserve module structure
--preserveModulesRoot Put preserved modules under this path at root level
--preserveSymlinks Do not follow symlinks when resolving files
--no-reexportProtoFromExternal Ignore `__proto__` in star re-exports
--no-sanitizeFileName Do not replace invalid characters in file names
--shimMissingExports Create shim variables for missing exports
--silent Don't print warnings
Expand Down
45 changes: 45 additions & 0 deletions docs/configuration-options/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1959,6 +1959,51 @@ export default {

This will generate an additional `noConflict` export to UMD bundles. When called in an IIFE scenario, this method will return the bundle exports while restoring the corresponding global variable to its previous value.

### output.reexportProtoFromExternal

| | |
| --: | :-- |
| Type: | `boolean` |
| CLI: | `--reexportProtoFromExternal`/`--no-reexportProtoFromExternal` |
| Default: | `true` |

This option is only effective when [`output.format`](#output-format) is set to one of `['amd', 'cjs', 'iife', 'umd']` and [`output.externalLiveBindings`](#output-externallivebindings) is set to false.

For maximum compatibility, Rollup reexports `__proto__` from an external module by default. However, for common use cases, it is strongly recommended to set this value to false as it effectively reduces the output size.

```js
// the input file
export * from 'rollup';
```

```js
// the output file if the output.format is cjs
'use strict';

// reexportProtoFromExternal is true
var rollup = require('rollup');

Object.prototype.hasOwnProperty.call(rollup, '__proto__') &&
!Object.prototype.hasOwnProperty.call(exports, '__proto__') &&
Object.defineProperty(exports, '__proto__', {
enumerable: true,
value: rollup['__proto__']
});

Object.keys(rollup).forEach(function (k) {
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k))
exports[k] = rollup[k];
});

// reexportProtoFromExternal is false
var rollup = require('rollup');

Object.keys(rollup).forEach(function (k) {
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k))
exports[k] = rollup[k];
});
```

### output.sanitizeFileName

| | |
Expand Down
1 change: 1 addition & 0 deletions docs/javascript-api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ const outputOptions = {
freeze,
indent,
noConflict,
reexportProtoFromExternal,
sanitizeFileName,
strict,
systemNullSetters,
Expand Down
7 changes: 7 additions & 0 deletions docs/repl/stores/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,12 @@ export const useOptions = defineStore('options2', () => {
available: optionOutputPreserveModules.value,
name: 'output.preserveModulesRoot'
});
const optionOutputReexportProtoFromExternal = getBoolean({
available: () =>
isInteropFormat.value && optionOutputExternalLiveBindings.value.value === false,
defaultValue: true,
name: 'output.reexportProtoFromExternal'
});
const optionOutputSanitizeFileName = getBoolean({
available: alwaysTrue,
defaultValue: true,
Expand Down Expand Up @@ -450,6 +456,7 @@ export const useOptions = defineStore('options2', () => {
optionOutputPaths,
optionOutputPreserveModules,
optionOutputPreserveModulesRoot,
optionOutputReexportProtoFromExternal,
optionOutputSourcemap,
optionOutputSourcemapFileNames,
optionOutputSanitizeFileName,
Expand Down
4 changes: 3 additions & 1 deletion src/finalisers/amd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default function amd(
freeze,
generatedCode: { symbols },
interop,
reexportProtoFromExternal,
strict
}: NormalizedOutputOptions
): void {
Expand Down Expand Up @@ -83,7 +84,8 @@ export default function amd(
interop,
snippets,
t,
externalLiveBindings
externalLiveBindings,
reexportProtoFromExternal
);
let namespaceMarkers = getNamespaceMarkers(
namedExportsMode && hasExports,
Expand Down
2 changes: 2 additions & 0 deletions src/finalisers/cjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default function cjs(
freeze,
interop,
generatedCode: { symbols },
reexportProtoFromExternal,
strict
}: NormalizedOutputOptions
): void {
Expand Down Expand Up @@ -66,6 +67,7 @@ export default function cjs(
snippets,
t,
externalLiveBindings,
reexportProtoFromExternal,
`module.exports${_}=${_}`
);

Expand Down
4 changes: 3 additions & 1 deletion src/finalisers/iife.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default function iife(
extend,
freeze,
externalLiveBindings,
reexportProtoFromExternal,
globals,
interop,
name,
Expand Down Expand Up @@ -116,7 +117,8 @@ export default function iife(
interop,
snippets,
t,
externalLiveBindings
externalLiveBindings,
reexportProtoFromExternal
);
let namespaceMarkers = getNamespaceMarkers(
namedExportsMode && hasExports,
Expand Down
17 changes: 12 additions & 5 deletions src/finalisers/shared/getExportBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function getExportBlock(
snippets: GenerateCodeSnippets,
t: string,
externalLiveBindings: boolean,
reexportProtoFromExternal: boolean,
mechanism = 'return '
): string {
const { _, getDirectReturnFunction, getFunctionIntro, getPropertyAccess, n, s } = snippets;
Expand Down Expand Up @@ -102,6 +103,16 @@ export function getExportBlock(
for (const specifier of reexports) {
if (specifier.reexported === '*') {
if (exportBlock) exportBlock += n;
if (!specifier.needsLiveBinding && reexportProtoFromExternal) {
const protoString = "'__proto__'";
exportBlock +=
`Object.prototype.hasOwnProperty.call(${name},${_}${protoString})${_}&&${n}` +
`${t}!Object.prototype.hasOwnProperty.call(exports,${_}${protoString})${_}&&${n}` +
`${t}Object.defineProperty(exports,${_}${protoString},${_}{${n}` +
`${t}${t}enumerable:${_}true,${n}` +
`${t}${t}value:${_}${name}[${protoString}]${n}` +
`${t}});${n}${n}`;
}
const copyPropertyIfNecessary = `{${n}${t}if${_}(k${_}!==${_}'default'${_}&&${_}!Object.prototype.hasOwnProperty.call(exports,${_}k))${_}${getDefineProperty(
name,
specifier.needsLiveBinding,
Expand Down Expand Up @@ -251,9 +262,5 @@ const getDefineProperty = (
`${t}${t}get:${_}${left}${name}[k]${right}${n}${t}})`
);
}
return (
`k${_}===${_}'__proto__'${_}?${_}Object.defineProperty(exports,${_}k,${_}{${n}` +
`${t}${t}enumerable:${_}true,${n}` +
`${t}${t}value:${_}${name}[k]${n}${t}})${_}:${_}exports[k]${_}=${_}${name}[k]`
);
return `exports[k]${_}=${_}${name}[k]`;
};
4 changes: 3 additions & 1 deletion src/finalisers/umd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export default function umd(
generatedCode: { symbols },
globals,
noConflict,
reexportProtoFromExternal,
strict
}: NormalizedOutputOptions
): void {
Expand Down Expand Up @@ -202,7 +203,8 @@ export default function umd(
interop,
snippets,
t,
externalLiveBindings
externalLiveBindings,
reexportProtoFromExternal
);
let namespaceMarkers = getNamespaceMarkers(
namedExportsMode && hasExports,
Expand Down
2 changes: 2 additions & 0 deletions src/rollup/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ export interface OutputOptions {
plugins?: OutputPluginOption;
preserveModules?: boolean;
preserveModulesRoot?: string;
reexportProtoFromExternal?: boolean;
sanitizeFileName?: boolean | ((fileName: string) => string);
sourcemap?: boolean | 'inline' | 'hidden';
sourcemapBaseUrl?: string;
Expand Down Expand Up @@ -776,6 +777,7 @@ export interface NormalizedOutputOptions {
plugins: OutputPlugin[];
preserveModules: boolean;
preserveModulesRoot: string | undefined;
reexportProtoFromExternal: boolean;
sanitizeFileName: (fileName: string) => string;
sourcemap: boolean | 'inline' | 'hidden';
sourcemapBaseUrl: string | undefined;
Expand Down
1 change: 1 addition & 0 deletions src/utils/options/mergeOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ async function mergeOutputOptions(
plugins: await normalizePluginOption(config.plugins),
preserveModules: getOption('preserveModules'),
preserveModulesRoot: getOption('preserveModulesRoot'),
reexportProtoFromExternal: getOption('reexportProtoFromExternal'),
sanitizeFileName: getOption('sanitizeFileName'),
sourcemap: getOption('sourcemap'),
sourcemapBaseUrl: getOption('sourcemapBaseUrl'),
Expand Down
1 change: 1 addition & 0 deletions src/utils/options/normalizeOutputOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export async function normalizeOutputOptions(
plugins: await normalizePluginOption(config.plugins),
preserveModules,
preserveModulesRoot: getPreserveModulesRoot(config),
reexportProtoFromExternal: config.reexportProtoFromExternal ?? true,
sanitizeFileName:
typeof config.sanitizeFileName === 'function'
? config.sanitizeFileName
Expand Down
50 changes: 50 additions & 0 deletions test/form/samples/cjs-transpiler-re-exports-1/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const { execSync } = require('node:child_process');

module.exports = defineTest({
description:
'Disable reexporting the __proto__ from the external module if both output.externalLiveBindings and output.reExportProtoFromExternal are false',
formats: ['cjs'],
options: {
output: {
externalLiveBindings: false,
reexportProtoFromExternal: false
},
plugins: [
{
resolveId(id) {
if (id.endsWith('main.js')) {
return { id };
}
return {
id,
external: true
};
},
buildEnd() {
this.emitFile({
type: 'prebuilt-chunk',
fileName: 'foo.js',
code:
'exports.foo = 1;\n' +
'Object.defineProperty(exports, "__proto__", {\n' +
' enumerable: true,\n' +
' value: 2\n' +
'});'
});
this.emitFile({
type: 'prebuilt-chunk',
fileName: 'execute.mjs',
code:
'import assert from "node:assert";\n' +
'import { foo, __proto__ } from "./cjs.js";\n' +
'assert.strictEqual(foo, 1);\n' +
'assert.strictEqual(__proto__, undefined);'
});
}
}
]
},
after() {
execSync('node _actual/execute.mjs', { cwd: __dirname });
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

var foo_js = require('./foo.js');



Object.keys(foo_js).forEach(function (k) {
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) exports[k] = foo_js[k];
});
1 change: 1 addition & 0 deletions test/form/samples/cjs-transpiler-re-exports-1/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './foo.js';
49 changes: 49 additions & 0 deletions test/form/samples/cjs-transpiler-re-exports/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const { execSync } = require('node:child_process');
TrickyPi marked this conversation as resolved.
Show resolved Hide resolved

module.exports = defineTest({
description:
'Compatibility with CJS Transpiler Re-exports if output.externalLiveBindings is false',
formats: ['cjs'],
options: {
output: {
externalLiveBindings: false
},
plugins: [
{
resolveId(id) {
if (id.endsWith('main.js')) {
return { id };
}
return {
id,
external: true
};
},
buildEnd() {
this.emitFile({
type: 'prebuilt-chunk',
fileName: 'foo.js',
code:
'exports.foo = 1;\n' +
'Object.defineProperty(exports, "__proto__", {\n' +
' enumerable: true,\n' +
' value: 2\n' +
'});'
});
this.emitFile({
type: 'prebuilt-chunk',
fileName: 'execute.mjs',
code:
'import assert from "node:assert";\n' +
'import { foo, __proto__ } from "./cjs.js";\n' +
'assert.strictEqual(foo, 1);\n' +
'assert.strictEqual(__proto__, 2);'
});
}
}
]
},
after() {
execSync('node _actual/execute.mjs', { cwd: __dirname });
}
});
16 changes: 16 additions & 0 deletions test/form/samples/cjs-transpiler-re-exports/_expected/cjs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

var foo_js = require('./foo.js');



Object.prototype.hasOwnProperty.call(foo_js, '__proto__') &&
!Object.prototype.hasOwnProperty.call(exports, '__proto__') &&
Object.defineProperty(exports, '__proto__', {
enumerable: true,
value: foo_js['__proto__']
});

Object.keys(foo_js).forEach(function (k) {
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) exports[k] = foo_js[k];
});
1 change: 1 addition & 0 deletions test/form/samples/cjs-transpiler-re-exports/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './foo.js';
12 changes: 8 additions & 4 deletions test/form/samples/export-__proto__/_expected/amd.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ define(['exports', 'x'], (function (exports, x$1) { 'use strict';
value: __proto__
});
exports.x = x;
Object.keys(x$1).forEach(function (k) {
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) k === '__proto__' ? Object.defineProperty(exports, k, {
Object.prototype.hasOwnProperty.call(x$1, '__proto__') &&
!Object.prototype.hasOwnProperty.call(exports, '__proto__') &&
Object.defineProperty(exports, '__proto__', {
enumerable: true,
value: x$1[k]
}) : exports[k] = x$1[k];
value: x$1['__proto__']
});

Object.keys(x$1).forEach(function (k) {
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) exports[k] = x$1[k];
});

}));