Repository: rollup/rollup (2025-08-16T15:31:53Z)

Issue:
second argument of dynamic import is wrongly treeshaked
### Rollup Version

v4.46.2

### Operating System (or Browser)

any

### Node Version (if applicable)

_No response_

### Link To Reproduction

https://rollupjs.org/repl/?version=4.46.2&shareable=JTdCJTIyZXhhbXBsZSUyMiUzQW51bGwlMkMlMjJtb2R1bGVzJTIyJTNBJTVCJTdCJTIyY29kZSUyMiUzQSUyMmNvbnN0JTIwcyUzRCU1QyUyMnMlNUMlMjIlM0IlNUNuY29uc3QlMjBlJTNEJTdCd2l0aCUzQSU3QnR5cGUlM0ElNUMlMjJqc29uJTVDJTIyJTdEJTdEJTNCJTVDbmltcG9ydChzJTJDZSklM0IlMjIlMkMlMjJpc0VudHJ5JTIyJTNBdHJ1ZSUyQyUyMm5hbWUlMjIlM0ElMjJtYWluLmpzJTIyJTdEJTVEJTJDJTIyb3B0aW9ucyUyMiUzQSU3QiU3RCU3RA==

### Expected Behaviour

keep external original

### Actual Behaviour

the second argument disapear


Discussion:
This issue has been resolved via #6071 as part of rollup@4.48.0. You can test it via `npm install rollup`.



Patch:
diff --git a/src/Chunk.ts b/src/Chunk.ts
index 91412503e48..7e551fdd786 100644
--- a/src/Chunk.ts
+++ b/src/Chunk.ts
@@ -1018,15 +1018,19 @@ export default class Chunk {
 		fileName: string,
 		node: ImportExpression
 	): [importPath: string, attributes: string | null | true] {
+		const { externalImportAttributes } = this.outputOptions;
+		const keepExternalImportAttributes =
+			['es', 'cjs'].includes(this.outputOptions.format) && externalImportAttributes;
 		if (resolution instanceof ExternalModule) {
 			const chunk = this.externalChunkByModule.get(resolution)!;
-			return [`'${chunk.getImportPath(fileName)}'`, chunk.getImportAttributes(this.snippets)];
+			const dynamicAttributes = chunk.getImportAttributes(this.snippets);
+			return [
+				`'${chunk.getImportPath(fileName)}'`,
+				dynamicAttributes || (keepExternalImportAttributes ? true : null)
+			];
 		}
 		let attributes: string | true | null = null;
-		if (
-			['es', 'cjs'].includes(this.outputOptions.format) &&
-			this.outputOptions.externalImportAttributes
-		) {
+		if (keepExternalImportAttributes) {
 			const attributesFromImportAttributes = getAttributesFromImportExpression(node);
 			attributes =
 				attributesFromImportAttributes === EMPTY_OBJECT
diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts
index b1aea19e92f..dd9aa725418 100644
--- a/src/ModuleLoader.ts
+++ b/src/ModuleLoader.ts
@@ -567,7 +567,10 @@ export class ModuleLoader {
 				module.id,
 				getAttributesFromImportExpression(dynamicImport.node)
 			);
-			if (resolvedId && typeof resolvedId === 'object') {
+			if (!resolvedId || typeof resolvedId === 'string') {
+				dynamicImport.node.shouldIncludeDynamicAttributes = true;
+			} else {
+				dynamicImport.node.shouldIncludeDynamicAttributes = !!resolvedId.external;
 				dynamicImport.id = resolvedId.id;
 			}
 			return [dynamicImport, resolvedId] as const;
diff --git a/src/ast/nodes/ImportExpression.ts b/src/ast/nodes/ImportExpression.ts
index 9224f045e0a..1d9706f65e7 100644
--- a/src/ast/nodes/ImportExpression.ts
+++ b/src/ast/nodes/ImportExpression.ts
@@ -21,7 +21,7 @@ import { findFirstOccurrenceOutsideComment, type RenderOptions } from '../../uti
 import type { InclusionContext } from '../ExecutionContext';
 import type ChildScope from '../scopes/ChildScope';
 import type { ObjectPath } from '../utils/PathTracker';
-import { UnknownKey } from '../utils/PathTracker';
+import { UNKNOWN_PATH, UnknownKey } from '../utils/PathTracker';
 import type NamespaceVariable from '../variables/NamespaceVariable';
 import ArrowFunctionExpression from './ArrowFunctionExpression';
 import AwaitExpression from './AwaitExpression';
@@ -68,6 +68,14 @@ export default class ImportExpression extends NodeBase {
 	private resolution: Module | ExternalModule | string | null = null;
 	private resolutionString: string | null = null;
 
+	get shouldIncludeDynamicAttributes() {
+		return isFlagSet(this.flags, Flag.shouldIncludeDynamicAttributes);
+	}
+
+	set shouldIncludeDynamicAttributes(value: boolean) {
+		this.flags = setFlag(this.flags, Flag.shouldIncludeDynamicAttributes, value);
+	}
+
 	get withinTopLevelAwait() {
 		return isFlagSet(this.flags, Flag.withinTopLevelAwait);
 	}
@@ -76,9 +84,9 @@ export default class ImportExpression extends NodeBase {
 		this.flags = setFlag(this.flags, Flag.withinTopLevelAwait, value);
 	}
 
-	// Do not bind attributes
 	bind(): void {
 		this.source.bind();
+		this.options?.bind();
 	}
 
 	/**
@@ -185,18 +193,21 @@ export default class ImportExpression extends NodeBase {
 	}
 
 	include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void {
-		if (!this.included) this.includeNode();
+		if (!this.included) this.includeNode(context);
 		this.source.include(context, includeChildrenRecursively);
+		if (this.shouldIncludeDynamicAttributes)
+			this.options?.include(context, includeChildrenRecursively);
 	}
 
-	includeNode() {
+	includeNode(context: InclusionContext) {
 		this.included = true;
+		if (this.shouldIncludeDynamicAttributes) this.options?.includePath(UNKNOWN_PATH, context);
 		this.scope.context.includeDynamicImport(this);
 		this.scope.addAccessedDynamicImport(this);
 	}
 
-	includePath(path: ObjectPath): void {
-		if (!this.included) this.includeNode();
+	includePath(path: ObjectPath, context: InclusionContext): void {
+		if (!this.included) this.includeNode(context);
 		// Technically, this is not correct as dynamic imports return a Promise.
 		if (this.hasUnknownAccessedKey) return;
 		if (path[0] === UnknownKey) {
diff --git a/src/ast/nodes/shared/BitFlags.ts b/src/ast/nodes/shared/BitFlags.ts
index b627923d550..63813bf9207 100644
--- a/src/ast/nodes/shared/BitFlags.ts
+++ b/src/ast/nodes/shared/BitFlags.ts
@@ -27,7 +27,8 @@ export const enum Flag {
 	hasDeoptimizedCache = 1 << 25,
 	hasEffects = 1 << 26,
 	withinTopLevelAwait = 1 << 27,
-	checkedForWarnings = 1 << 28
+	checkedForWarnings = 1 << 28,
+	shouldIncludeDynamicAttributes = 1 << 29
 }
 
 export function isFlagSet(flags: number, flag: Flag): boolean {
diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts
index ed743879369..1993c265e9a 100644
--- a/src/ast/variables/LocalVariable.ts
+++ b/src/ast/variables/LocalVariable.ts
@@ -246,7 +246,7 @@ export default class LocalVariable extends Variable {
 					declaration.parent.parent.callee.property.name === 'then' &&
 					isImportExpressionNode(declaration.parent.parent.callee.object)
 				) {
-					declaration.parent.parent.callee.object.includePath(path);
+					declaration.parent.parent.callee.object.includePath(path, context);
 				}
 			}
 			// We need to make sure we include the correct path of the init
diff --git a/src/utils/logs.ts b/src/utils/logs.ts
index 3b92f8565fa..df55140e749 100644
--- a/src/utils/logs.ts
+++ b/src/utils/logs.ts
@@ -608,7 +608,7 @@ export function logImportAttributeIsInvalid(importer: string): RollupLog {
 		code: INVALID_IMPORT_ATTRIBUTE,
 		message: `Rollup could not statically analyze an import attribute of a dynamic import in "${relativeId(
 			importer
-		)}". Import attributes need to have string keys and values. The attribute will be removed.`
+		)}". Import attributes need to have string keys and values.`
 	};
 }
 


Test patch:
diff --git a/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_config.js b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_config.js
new file mode 100644
index 00000000000..b1195c2e2a1
--- /dev/null
+++ b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_config.js
@@ -0,0 +1,14 @@
+module.exports = defineTest({
+	description: 'Remove the attribute declarations for internal dynamic imports',
+	expectedWarnings: ['INVALID_IMPORT_ATTRIBUTE'],
+	options: {
+		plugins: [
+			{
+				resolveDynamicImport() {
+					return { id: './foo.js' };
+				}
+			}
+		]
+	},
+	verifyAst: false
+});
diff --git a/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/amd/generated-foo.js b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/amd/generated-foo.js
new file mode 100644
index 00000000000..377f0e06f80
--- /dev/null
+++ b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/amd/generated-foo.js
@@ -0,0 +1,8 @@
+define(['exports'], (function (exports) { 'use strict';
+
+	const foo = 'foo';
+	console.log(foo);
+
+	exports.foo = foo;
+
+}));
diff --git a/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/amd/main.js b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/amd/main.js
new file mode 100644
index 00000000000..30c5f54efda
--- /dev/null
+++ b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/amd/main.js
@@ -0,0 +1,5 @@
+define(['require'], (function (require) { 'use strict';
+
+	new Promise(function (resolve, reject) { require(['./generated-foo'], resolve, reject); });
+
+}));
diff --git a/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/cjs/generated-foo.js b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/cjs/generated-foo.js
new file mode 100644
index 00000000000..186abffa7c4
--- /dev/null
+++ b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/cjs/generated-foo.js
@@ -0,0 +1,6 @@
+'use strict';
+
+const foo = 'foo';
+console.log(foo);
+
+exports.foo = foo;
diff --git a/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/cjs/main.js b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/cjs/main.js
new file mode 100644
index 00000000000..6fbd51e3ad2
--- /dev/null
+++ b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/cjs/main.js
@@ -0,0 +1,3 @@
+'use strict';
+
+Promise.resolve().then(function () { return require('./generated-foo.js'); });
diff --git a/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/es/generated-foo.js b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/es/generated-foo.js
new file mode 100644
index 00000000000..00ab90252de
--- /dev/null
+++ b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/es/generated-foo.js
@@ -0,0 +1,4 @@
+const foo = 'foo';
+console.log(foo);
+
+export { foo };
diff --git a/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/es/main.js b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/es/main.js
new file mode 100644
index 00000000000..da05d25c6b2
--- /dev/null
+++ b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/es/main.js
@@ -0,0 +1,1 @@
+import('./generated-foo.js');
diff --git a/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/system/generated-foo.js b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/system/generated-foo.js
new file mode 100644
index 00000000000..4ff31e577cf
--- /dev/null
+++ b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/system/generated-foo.js
@@ -0,0 +1,11 @@
+System.register([], (function (exports) {
+	'use strict';
+	return {
+		execute: (function () {
+
+			const foo = exports("foo", 'foo');
+			console.log(foo);
+
+		})
+	};
+}));
diff --git a/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/system/main.js b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/system/main.js
new file mode 100644
index 00000000000..e2adf5ca7cc
--- /dev/null
+++ b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/_expected/system/main.js
@@ -0,0 +1,10 @@
+System.register([], (function (exports, module) {
+	'use strict';
+	return {
+		execute: (function () {
+
+			module.import('./generated-foo.js');
+
+		})
+	};
+}));
diff --git a/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/foo.js b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/foo.js
new file mode 100644
index 00000000000..e07c34b9e09
--- /dev/null
+++ b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/foo.js
@@ -0,0 +1,2 @@
+export const foo = 'foo';
+console.log(foo);
\ No newline at end of file
diff --git a/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/main.js b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/main.js
new file mode 100644
index 00000000000..323ed9d4023
--- /dev/null
+++ b/test/chunking-form/samples/remove-attribute-declarations-for-internal-dynamic-imports/main.js
@@ -0,0 +1,3 @@
+const extra = { with: { type: 'json' } };
+import(specifier, extra);
+
diff --git a/test/form/samples/import-attributes/attribute-shapes/_expected.js b/test/form/samples/import-attributes/attribute-shapes/_expected.js
index b5d8cbaa42d..3e8611361b1 100644
--- a/test/form/samples/import-attributes/attribute-shapes/_expected.js
+++ b/test/form/samples/import-attributes/attribute-shapes/_expected.js
@@ -1,4 +1,4 @@
 import('external-a', { assert: { type: 'json' } });
-import('external-b');
-import('external-c');
-import('external-d');
+import('external-b', { with: {} });
+import('external-c', { ...{} });
+import('external-d', {});
diff --git a/test/form/samples/import-attributes/keep-attribute-declarations-for-external-dynamic-imports/_config.js b/test/form/samples/import-attributes/keep-attribute-declarations-for-external-dynamic-imports/_config.js
new file mode 100644
index 00000000000..de7010b0538
--- /dev/null
+++ b/test/form/samples/import-attributes/keep-attribute-declarations-for-external-dynamic-imports/_config.js
@@ -0,0 +1,19 @@
+module.exports = defineTest({
+	description: 'Keep the attribute declarations for external dynamic imports',
+	expectedWarnings: ['INVALID_IMPORT_ATTRIBUTE'],
+	options: {
+		plugins: [
+			{
+				resolveDynamicImport(specifier) {
+					if (specifier === 'specifier3' || specifier === 'specifier4') {
+						return { id: specifier, external: true };
+					}
+					if (specifier.name === 'specifier5' || specifier.name === 'specifier6') {
+						return `'${specifier.name}-replacement'`;
+					}
+				}
+			}
+		]
+	},
+	verifyAst: false
+});
diff --git a/test/form/samples/import-attributes/keep-attribute-declarations-for-external-dynamic-imports/_expected.js b/test/form/samples/import-attributes/keep-attribute-declarations-for-external-dynamic-imports/_expected.js
new file mode 100644
index 00000000000..97618386fd4
--- /dev/null
+++ b/test/form/samples/import-attributes/keep-attribute-declarations-for-external-dynamic-imports/_expected.js
@@ -0,0 +1,17 @@
+const extra1 = { with: { type: 'json' } };
+import(specifier1, extra1);
+
+const extra2 = { with: { type: 'json' } };
+import(specifier2, { with: extra2.with });
+
+const extra3 = { with: { type: 'json' } };
+import('specifier3', extra3);
+
+const extra4 = { with: { type: 'json' } };
+import('specifier4', { with: extra4.with });
+
+const extra5 = { with: { type: 'json' } };
+import('specifier5-replacement', extra5);
+
+const extra6 = { with: { type: 'json' } };
+import('specifier6-replacement', { with: extra6.with });
diff --git a/test/form/samples/import-attributes/keep-attribute-declarations-for-external-dynamic-imports/main.js b/test/form/samples/import-attributes/keep-attribute-declarations-for-external-dynamic-imports/main.js
new file mode 100644
index 00000000000..e369b75088d
--- /dev/null
+++ b/test/form/samples/import-attributes/keep-attribute-declarations-for-external-dynamic-imports/main.js
@@ -0,0 +1,17 @@
+const extra1 = { with: { type: 'json' } };
+import(specifier1, extra1);
+
+const extra2 = { with: { type: 'json' } };
+import(specifier2, { with: extra2.with });
+
+const extra3 = { with: { type: 'json' } };
+import('specifier3', extra3);
+
+const extra4 = { with: { type: 'json' } };
+import('specifier4', { with: extra4.with });
+
+const extra5 = { with: { type: 'json' } };
+import(specifier5, extra5);
+
+const extra6 = { with: { type: 'json' } };
+import(specifier6, { with: extra6.with });
diff --git a/test/function/samples/import-assertions/warn-unresolvable-assertions/_config.js b/test/function/samples/import-assertions/warn-unresolvable-assertions/_config.js
index a246159ce05..a27f9c79c31 100644
--- a/test/function/samples/import-assertions/warn-unresolvable-assertions/_config.js
+++ b/test/function/samples/import-assertions/warn-unresolvable-assertions/_config.js
@@ -4,6 +4,11 @@ const ID_MAIN = path.join(__dirname, 'main.js');
 module.exports = defineTest({
 	description: 'warns for dynamic import attributes that cannot be resolved',
 	verifyAst: false,
+	context: {
+		bar: 'bar',
+		foo: 'foo',
+		invalid: 'invalid'
+	},
 	options: {
 		external: id => id.startsWith('external')
 	},
@@ -11,7 +16,7 @@ module.exports = defineTest({
 		{
 			code: 'INVALID_IMPORT_ATTRIBUTE',
 			message:
-				'main.js (1:0): Rollup could not statically analyze an import attribute of a dynamic import in "main.js". Import attributes need to have string keys and values. The attribute will be removed.',
+				'main.js (1:0): Rollup could not statically analyze an import attribute of a dynamic import in "main.js". Import attributes need to have string keys and values.',
 			id: ID_MAIN,
 			pos: 0,
 			loc: {
@@ -28,7 +33,7 @@ module.exports = defineTest({
 		{
 			code: 'INVALID_IMPORT_ATTRIBUTE',
 			message:
-				'main.js (2:0): Rollup could not statically analyze an import attribute of a dynamic import in "main.js". Import attributes need to have string keys and values. The attribute will be removed.',
+				'main.js (2:0): Rollup could not statically analyze an import attribute of a dynamic import in "main.js". Import attributes need to have string keys and values.',
 			id: ID_MAIN,
 			pos: 32,
 			loc: {
@@ -46,7 +51,7 @@ module.exports = defineTest({
 		{
 			code: 'INVALID_IMPORT_ATTRIBUTE',
 			message:
-				'main.js (4:30): Rollup could not statically analyze an import attribute of a dynamic import in "main.js". Import attributes need to have string keys and values. The attribute will be removed.',
+				'main.js (4:30): Rollup could not statically analyze an import attribute of a dynamic import in "main.js". Import attributes need to have string keys and values.',
 			id: ID_MAIN,
 			pos: 133,
 			loc: {
@@ -65,7 +70,7 @@ module.exports = defineTest({
 		{
 			code: 'INVALID_IMPORT_ATTRIBUTE',
 			message:
-				'main.js (5:30): Rollup could not statically analyze an import attribute of a dynamic import in "main.js". Import attributes need to have string keys and values. The attribute will be removed.',
+				'main.js (5:30): Rollup could not statically analyze an import attribute of a dynamic import in "main.js". Import attributes need to have string keys and values.',
 			id: ID_MAIN,
 			pos: 173,
 			loc: {
@@ -84,7 +89,7 @@ module.exports = defineTest({
 		{
 			code: 'INVALID_IMPORT_ATTRIBUTE',
 			message:
-				'main.js (6:30): Rollup could not statically analyze an import attribute of a dynamic import in "main.js". Import attributes need to have string keys and values. The attribute will be removed.',
+				'main.js (6:30): Rollup could not statically analyze an import attribute of a dynamic import in "main.js". Import attributes need to have string keys and values.',
 			id: ID_MAIN,
 			pos: 218,
 			loc: {
@@ -103,7 +108,7 @@ module.exports = defineTest({
 		{
 			code: 'INVALID_IMPORT_ATTRIBUTE',
 			message:
-				'main.js (7:30): Rollup could not statically analyze an import attribute of a dynamic import in "main.js". Import attributes need to have string keys and values. The attribute will be removed.',
+				'main.js (7:30): Rollup could not statically analyze an import attribute of a dynamic import in "main.js". Import attributes need to have string keys and values.',
 			id: ID_MAIN,
 			pos: 263,
 			loc: {