Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/builder/lib/lbt/bundle/Builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -597,10 +597,11 @@ class BundleBuilder {
}
}

writeRequires(section) {
async writeRequires(section) {
if (section.modules.length === 0) {
return;
}

this.outW.ensureNewLine();
if (section.async === false) {
section.modules.forEach( (module) => {
Expand Down Expand Up @@ -767,7 +768,7 @@ async function rewriteDefine({moduleName, moduleContent, moduleSourceMap}) {
} catch (e) {
log.error(`Error while parsing ${moduleName}: ${e.message}`);
log.verbose(e.stack);
return {};
return null;
}

if ( ast.type === Syntax.Program &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class ResolvedBundleDefinition {
return pool.getModuleInfo(submodule).then(
(subinfo) => {
if (!bundleInfo.subModules.includes(subinfo.name) &&
subinfo.format !== ModuleInfo.Format.ESM &&
(!subinfo.requiresTopLevelScope ||
(subinfo.requiresTopLevelScope && allowStringBundling))) {
bundleInfo.addSubModule(subinfo);
Expand Down
11 changes: 11 additions & 0 deletions packages/builder/lib/lbt/bundle/Resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import topologicalSort from "../graph/topologicalSort.js";
import {getRendererName} from "../UI5ClientConstants.js";
import ModuleInfo from "../resources/ModuleInfo.js";
import ResourceFilterList from "../resources/ResourceFilterList.js";
import {SectionType} from "./BundleDefinition.js";
import ResolvedBundleDefinition from "./ResolvedBundleDefinition.js";
Expand Down Expand Up @@ -135,6 +136,16 @@ class BundleResolver {
const dependencyInfo = resource && resource.info;
let promises = [];

// Skip ESM modules — they can't be bundled
if (resource?.info?.format === ModuleInfo.Format.ESM) {
log.error(
`Module ${resourceName} is an ECMAScript Module (ESM), ` +
`which is not supported for bundling. ` +
`The module will be skipped.`
);
return;
}

if ( isBundle && !decomposable ) {
resource.info.subModules.forEach(
(included) => {
Expand Down
3 changes: 2 additions & 1 deletion packages/builder/lib/lbt/resources/ModuleInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const CONDITIONAL = 2;
const Format = {
UI5_LEGACY: "ui5-declare",
UI5_DEFINE: "ui5-define",
AMD: "amd"
AMD: "amd",
ESM: "esm"
};

/**
Expand Down
7 changes: 5 additions & 2 deletions packages/builder/lib/lbt/resources/ResourcePool.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,12 @@ async function determineDependencyInfo(resource, rawInfo, pool) {
try {
ast = parseJS(code, {comment: true});
} catch (err) {
log.error(`Failed to parse ${resource.name}: ${err.message}`);
log.verbose(`Failed to parse ${resource.name}: ${err.message}`);
log.verbose(err.stack);
if (err.message.includes("'import' and 'export' may appear only with 'sourceType: module'") ||
err.message.includes("Cannot use 'import.meta' outside a module")) {
info.format = ModuleInfo.Format.ESM;
}
}
if (ast) {
try {
Expand Down Expand Up @@ -243,4 +247,3 @@ class ResourcePool {
}

export default ResourcePool;

179 changes: 178 additions & 1 deletion packages/builder/test/lib/lbt/bundle/Builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ test.beforeEach(async (t) => {
t.context.BuilderWithStub = await esmock("../../../../lib/lbt/bundle/Builder", {
"@ui5/logger": {
getLogger: () => myLoggerInstance
}
},
"../../../../lib/lbt/bundle/Resolver": await esmock("../../../../lib/lbt/bundle/Resolver", {
"@ui5/logger": {
getLogger: () => myLoggerInstance
}
}),
});
});

Expand Down Expand Up @@ -2728,6 +2733,18 @@ test("rewriteDefine (with other module name as template literal)", async (t) =>
t.is(moduleSourceMap, undefined);
});

test("rewriteDefine (with ESM content)", async (t) => {
const {rewriteDefine} = __localFunctions__;

const result = await rewriteDefine({
moduleName: "my/test/esm-module.js",
moduleContent: `import foo from "./foo.js"; export default foo;`,
moduleSourceMap: undefined
});

t.is(result, null, "rewriteDefine should return null for ESM content that can't be parsed");
});

test("getSourceMapForModule: Source map resource named after module resource (no sourceMappingURL)", async (t) => {
const originalSourceMap = {
"version": 3,
Expand Down Expand Up @@ -3214,3 +3231,163 @@ test.serial("getEffectiveUi5MajorVersion with cache", async (t) => {
t.is(getEffectiveUi5MajorVersionUI5v2WithCacheSpy.getCall(1).returnValue, 2, "UI5 Major Version correctly determined");
t.is(mySemverParseSpy.callCount, 1, "semver.parse has been called only once");
});

test.serial("integration: createBundle with ESM module in preload section", async (t) => {
const {BuilderWithStub, errorLogStub} = t.context;
const pool = new ResourcePool();

pool.addResource({
name: "my/app/thirdparty/esm-module.js",
getPath: () => "my/app/thirdparty/esm-module.js",
string: function() {
return this.buffer();
},
buffer: async () => `import foo from "./foo.js"; export default foo;`
});

const bundleDefinition = {
name: `esm-bundle.js`,
defaultFileTypes: [".js"],
sections: [{
mode: "preload",
name: "preload-section",
filters: [
"my/app/thirdparty/esm-module.js"
]
}]
};

const builder = new BuilderWithStub(pool);
const oResult = await builder.createBundle(bundleDefinition, {
numberOfParts: 1,
decorateBootstrapModule: true
});

t.is(oResult.name, "esm-bundle.js");
t.truthy(oResult.content, "Bundle should be created");

// ESM module should not appear in the bundle content at all
t.false(oResult.content.includes("undefined"),
"Bundle should not contain 'undefined' string from ESM module");
t.false(oResult.content.includes("import foo"),
"Bundle should not contain ESM import statement");
t.false(oResult.content.includes("export default"),
"Bundle should not contain ESM export statement");
t.false(oResult.content.includes("esm-module"),
"Bundle should not contain ESM module name in preload content");

// ESM module should not be in bundleInfo subModules
t.false(oResult.bundleInfo.subModules.includes("my/app/thirdparty/esm-module.js"),
"ESM module should not be listed as subModule in bundleInfo");

// An error should be logged about ESM not being supported
t.true(errorLogStub.callCount >= 1,
"log.error should be called for ESM module");
t.true(
errorLogStub.getCalls().some((call) =>
call.args[0].includes("my/app/thirdparty/esm-module.js") &&
call.args[0].includes("ECMAScript Module (ESM)")
),
"log.error should mention the ESM module name and that it's an ESM module"
);
});

test.serial("integration: createBundle with ESM module in raw section", async (t) => {
const {BuilderWithStub, errorLogStub} = t.context;
const pool = new ResourcePool();

pool.addResource({
name: "my/app/thirdparty/esm-module.js",
getPath: () => "my/app/thirdparty/esm-module.js",
string: function() {
return this.buffer();
},
buffer: async () => `import foo from "./foo.js"; export default foo;`
});

const bundleDefinition = {
name: `esm-raw-bundle.js`,
defaultFileTypes: [".js"],
sections: [{
mode: "raw",
filters: [
"my/app/thirdparty/esm-module.js"
]
}]
};

const builder = new BuilderWithStub(pool);
const oResult = await builder.createBundle(bundleDefinition, {
numberOfParts: 1,
decorateBootstrapModule: false
});

t.is(oResult.name, "esm-raw-bundle.js");
t.truthy(oResult.content, "Bundle should be created");

// ESM module should not appear in the bundle content
t.false(oResult.content.includes("import foo"),
"Bundle should not contain ESM import statement");
t.false(oResult.content.includes("export default"),
"Bundle should not contain ESM export statement");

// An error should be logged
t.true(errorLogStub.callCount >= 1,
"log.error should be called for ESM module in raw section");
t.true(
errorLogStub.getCalls().some((call) =>
call.args[0].includes("my/app/thirdparty/esm-module.js") &&
call.args[0].includes("ECMAScript Module (ESM)")
),
"log.error should mention the ESM module name"
);
});

test.serial("integration: createBundle with ESM module in require section", async (t) => {
const {BuilderWithStub, errorLogStub} = t.context;
const pool = new ResourcePool();

pool.addResource({
name: "my/app/thirdparty/esm-module.js",
getPath: () => "my/app/thirdparty/esm-module.js",
string: function() {
return this.buffer();
},
buffer: async () => `import foo from "./foo.js"; export default foo;`
});

const bundleDefinition = {
name: `esm-require-bundle.js`,
defaultFileTypes: [".js"],
sections: [{
mode: "require",
filters: [
"my/app/thirdparty/esm-module.js"
]
}]
};

const builder = new BuilderWithStub(pool);
const oResult = await builder.createBundle(bundleDefinition, {
numberOfParts: 1,
decorateBootstrapModule: false
});

t.is(oResult.name, "esm-require-bundle.js");
t.truthy(oResult.content, "Bundle should be created");

// ESM module should not appear in require calls
t.false(oResult.content.includes("esm-module"),
"Bundle should not contain require call for ESM module");

// An error should be logged
t.true(errorLogStub.callCount >= 1,
"log.error should be called for ESM module in require section");
t.true(
errorLogStub.getCalls().some((call) =>
call.args[0].includes("my/app/thirdparty/esm-module.js") &&
call.args[0].includes("ECMAScript Module (ESM)")
),
"log.error should mention the ESM module name"
);
});
Loading
Loading