aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Hausmann <simon.hausmann@qt.io>2018-08-16 10:57:25 +0200
committerSimon Hausmann <simon.hausmann@qt.io>2018-08-17 11:06:12 +0000
commitf43c1d902d908c6cd523b0174338ac0c98a30647 (patch)
tree50fa2c720dd86aa073efcb935e22bb967e01b5de
parent29275ef53ec8e1edb7aad66af94058786c7d1e2f (diff)
Add support for importing ES modules in .qml files
This is a straight-forward hook into the module implementation in QV4::ExecutionEngine. Modules are pre-compiled in the QML type loader thread. That thread keeps track of all pending loading scripts through the type loader's m_scriptCache. Once a module is compiled, it's thread-safely registered with the execution engine. Script instantiation and evaluation is done solely in the QQmlEngine's thread. ES Modules are identified in imports as well as qmldir files by the .mjs extension. Change-Id: Ie9c59785118afcb49f43a1e176a9f7db00f09428 Reviewed-by: Lars Knoll <lars.knoll@qt.io>
-rw-r--r--src/qml/compiler/qqmlirbuilder.cpp2
-rw-r--r--src/qml/compiler/qv4compileddata_p.h3
-rw-r--r--src/qml/jsruntime/qv4engine.cpp24
-rw-r--r--src/qml/jsruntime/qv4engine_p.h2
-rw-r--r--src/qml/jsruntime/qv4script.cpp18
-rw-r--r--src/qml/qml/qqmldirparser.cpp2
-rw-r--r--src/qml/qml/qqmlengine.cpp19
-rw-r--r--src/qml/qml/qqmlengine_p.h2
-rw-r--r--src/qml/qml/qqmltypeloader.cpp146
-rw-r--r--src/qml/qml/qqmltypeloader_p.h1
-rw-r--r--tests/auto/qml/qqmllanguage/data/importJsModule.1.mjs4
-rw-r--r--tests/auto/qml/qqmllanguage/data/importJsModule.1.qml6
-rw-r--r--tests/auto/qml/qqmllanguage/data/importJsModule.2.qml6
-rw-r--r--tests/auto/qml/qqmllanguage/data/importJsModule.3.indirect.mjs4
-rw-r--r--tests/auto/qml/qqmllanguage/data/importJsModule.3.mjs4
-rw-r--r--tests/auto/qml/qqmllanguage/data/importJsModule.3.qml6
-rw-r--r--tests/auto/qml/qqmllanguage/data/lib/org/qtproject/PureESJsModule/API.mjs4
-rw-r--r--tests/auto/qml/qqmllanguage/data/lib/org/qtproject/PureESJsModule/qmldir1
-rw-r--r--tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp33
19 files changed, 215 insertions, 72 deletions
diff --git a/src/qml/compiler/qqmlirbuilder.cpp b/src/qml/compiler/qqmlirbuilder.cpp
index ed3b55c2c6..624e3c42e7 100644
--- a/src/qml/compiler/qqmlirbuilder.cpp
+++ b/src/qml/compiler/qqmlirbuilder.cpp
@@ -619,7 +619,7 @@ bool IRBuilder::visit(QQmlJS::AST::UiImport *node)
if (!node->fileName.isNull()) {
uri = node->fileName.toString();
- if (uri.endsWith(QLatin1String(".js"))) {
+ if (uri.endsWith(QLatin1String(".js")) || uri.endsWith(QLatin1String(".mjs"))) {
import->type = QV4::CompiledData::Import::ImportScript;
} else {
import->type = QV4::CompiledData::Import::ImportFile;
diff --git a/src/qml/compiler/qv4compileddata_p.h b/src/qml/compiler/qv4compileddata_p.h
index f0043cc6af..4e71bd5c27 100644
--- a/src/qml/compiler/qv4compileddata_p.h
+++ b/src/qml/compiler/qv4compileddata_p.h
@@ -822,7 +822,8 @@ struct Unit
StaticData = 0x2, // Unit data persistent in memory?
IsSingleton = 0x4,
IsSharedLibrary = 0x8, // .pragma shared?
- PendingTypeCompilation = 0x10 // the QML data structures present are incomplete and require type compilation
+ IsESModule = 0x10,
+ PendingTypeCompilation = 0x20 // the QML data structures present are incomplete and require type compilation
};
quint32_le flags;
quint32_le stringTableSize;
diff --git a/src/qml/jsruntime/qv4engine.cpp b/src/qml/jsruntime/qv4engine.cpp
index 90c75b204d..14bc5f084e 100644
--- a/src/qml/jsruntime/qv4engine.cpp
+++ b/src/qml/jsruntime/qv4engine.cpp
@@ -1682,6 +1682,7 @@ QQmlRefPointer<CompiledData::CompilationUnit> ExecutionEngine::compileModule(boo
using namespace QV4::Compiler;
Compiler::Module compilerModule(debugMode);
+ compilerModule.unitFlags |= CompiledData::Unit::IsESModule;
JSUnitGenerator jsGenerator(&compilerModule);
Codegen cg(&jsGenerator, /*strictMode*/true);
cg.generateFromModule(url.fileName(), url.toString(), sourceCode, moduleNode, &compilerModule);
@@ -1697,22 +1698,43 @@ QQmlRefPointer<CompiledData::CompilationUnit> ExecutionEngine::compileModule(boo
void ExecutionEngine::injectModule(const QQmlRefPointer<CompiledData::CompilationUnit> &moduleUnit)
{
+ // Injection can happen from the QML type loader thread for example, but instantiation and
+ // evaluation must be limited to the ExecutionEngine's thread.
+ QMutexLocker moduleGuard(&moduleMutex);
modules.insert(moduleUnit->finalUrl(), moduleUnit);
}
+QQmlRefPointer<CompiledData::CompilationUnit> ExecutionEngine::moduleForUrl(const QUrl &_url, const CompiledData::CompilationUnit *referrer) const
+{
+ QUrl url = QQmlTypeLoader::normalize(_url);
+ if (referrer)
+ url = referrer->finalUrl().resolved(url);
+
+ QMutexLocker moduleGuard(&moduleMutex);
+ auto existingModule = modules.find(url);
+ if (existingModule == modules.end())
+ return nullptr;
+ return *existingModule;
+}
+
QQmlRefPointer<CompiledData::CompilationUnit> ExecutionEngine::loadModule(const QUrl &_url, const CompiledData::CompilationUnit *referrer)
{
QUrl url = QQmlTypeLoader::normalize(_url);
if (referrer)
url = referrer->finalUrl().resolved(url);
+ QMutexLocker moduleGuard(&moduleMutex);
auto existingModule = modules.find(url);
if (existingModule != modules.end())
return *existingModule;
+ moduleGuard.unlock();
+
auto newModule = compileModule(url);
- if (newModule)
+ if (newModule) {
+ moduleGuard.relock();
modules.insert(url, newModule);
+ }
return newModule;
}
diff --git a/src/qml/jsruntime/qv4engine_p.h b/src/qml/jsruntime/qv4engine_p.h
index 4cd7e40013..8312adee48 100644
--- a/src/qml/jsruntime/qv4engine_p.h
+++ b/src/qml/jsruntime/qv4engine_p.h
@@ -580,8 +580,10 @@ public:
QQmlRefPointer<CompiledData::CompilationUnit> compileModule(const QUrl &url, const QString &sourceCode);
static QQmlRefPointer<CompiledData::CompilationUnit> compileModule(bool debugMode, const QUrl &url, const QString &sourceCode, QList<QQmlJS::DiagnosticMessage> *diagnostics);
+ mutable QMutex moduleMutex;
QHash<QUrl, QQmlRefPointer<CompiledData::CompilationUnit>> modules;
void injectModule(const QQmlRefPointer<CompiledData::CompilationUnit> &moduleUnit);
+ QQmlRefPointer<CompiledData::CompilationUnit> moduleForUrl(const QUrl &_url, const CompiledData::CompilationUnit *referrer = nullptr) const;
QQmlRefPointer<CompiledData::CompilationUnit> loadModule(const QUrl &_url, const CompiledData::CompilationUnit *referrer = nullptr);
#endif
diff --git a/src/qml/jsruntime/qv4script.cpp b/src/qml/jsruntime/qv4script.cpp
index 37c4f27ca9..070c048c8f 100644
--- a/src/qml/jsruntime/qv4script.cpp
+++ b/src/qml/jsruntime/qv4script.cpp
@@ -185,23 +185,7 @@ QQmlRefPointer<QV4::CompiledData::CompilationUnit> Script::precompile(QV4::Compi
parser.parseProgram();
- QList<QQmlError> errors;
-
- const auto diagnosticMessages = parser.diagnosticMessages();
- for (const DiagnosticMessage &m : diagnosticMessages) {
- if (m.isWarning()) {
- qWarning("%s:%d : %s", qPrintable(fileName), m.loc.startLine, qPrintable(m.message));
- continue;
- }
-
- QQmlError error;
- error.setUrl(QUrl(fileName));
- error.setDescription(m.message);
- error.setLine(m.loc.startLine);
- error.setColumn(m.loc.startColumn);
- errors << error;
- }
-
+ QList<QQmlError> errors = QQmlEnginePrivate::qmlErrorFromDiagnostics(fileName, parser.diagnosticMessages());
if (!errors.isEmpty()) {
if (reportedErrors)
*reportedErrors << errors;
diff --git a/src/qml/qml/qqmldirparser.cpp b/src/qml/qml/qqmldirparser.cpp
index 8c89cf0e61..d87bf433b8 100644
--- a/src/qml/qml/qqmldirparser.cpp
+++ b/src/qml/qml/qqmldirparser.cpp
@@ -272,7 +272,7 @@ bool QQmlDirParser::parse(const QString &source)
if (parseVersion(sections[1], &major, &minor)) {
const QString &fileName = sections[2];
- if (fileName.endsWith(QLatin1String(".js"))) {
+ if (fileName.endsWith(QLatin1String(".js")) || fileName.endsWith(QLatin1String(".mjs"))) {
// A 'js' extension indicates a namespaced script import
const Script entry(sections[0], fileName, major, minor);
_scripts.append(entry);
diff --git a/src/qml/qml/qqmlengine.cpp b/src/qml/qml/qqmlengine.cpp
index 119120572c..e487aec4ab 100644
--- a/src/qml/qml/qqmlengine.cpp
+++ b/src/qml/qml/qqmlengine.cpp
@@ -2106,6 +2106,25 @@ void QQmlEnginePrivate::warning(QQmlEnginePrivate *engine, const QList<QQmlError
dumpwarning(error);
}
+QList<QQmlError> QQmlEnginePrivate::qmlErrorFromDiagnostics(const QString &fileName, const QList<DiagnosticMessage> &diagnosticMessages)
+{
+ QList<QQmlError> errors;
+ for (const DiagnosticMessage &m : diagnosticMessages) {
+ if (m.isWarning()) {
+ qWarning("%s:%d : %s", qPrintable(fileName), m.loc.startLine, qPrintable(m.message));
+ continue;
+ }
+
+ QQmlError error;
+ error.setUrl(QUrl(fileName));
+ error.setDescription(m.message);
+ error.setLine(m.loc.startLine);
+ error.setColumn(m.loc.startColumn);
+ errors << error;
+ }
+ return errors;
+}
+
void QQmlEnginePrivate::cleanupScarceResources()
{
// iterate through the list and release them all.
diff --git a/src/qml/qml/qqmlengine_p.h b/src/qml/qml/qqmlengine_p.h
index da52e01793..f606896953 100644
--- a/src/qml/qml/qqmlengine_p.h
+++ b/src/qml/qml/qqmlengine_p.h
@@ -245,6 +245,8 @@ public:
inline static QQmlEngine *get(QQmlEnginePrivate *p);
inline static QQmlEnginePrivate *get(QV4::ExecutionEngine *e);
+ static QList<QQmlError> qmlErrorFromDiagnostics(const QString &fileName, const QList<QQmlJS::DiagnosticMessage> &diagnosticMessages);
+
static void registerBaseTypes(const char *uri, int versionMajor, int versionMinor);
static void registerQtQuick2Types(const char *uri, int versionMajor, int versionMinor);
static void defineQtQuick2Module();
diff --git a/src/qml/qml/qqmltypeloader.cpp b/src/qml/qml/qqmltypeloader.cpp
index 2aa64f350a..daa4604dca 100644
--- a/src/qml/qml/qqmltypeloader.cpp
+++ b/src/qml/qml/qqmltypeloader.cpp
@@ -51,6 +51,7 @@
#include <private/qqmltypecompiler_p.h>
#include <private/qqmlpropertyvalidator_p.h>
#include <private/qqmlpropertycachecreator_p.h>
+#include <private/qv4module_p.h>
#include <QtCore/qdir.h>
#include <QtCore/qfile.h>
@@ -2855,8 +2856,19 @@ QV4::ReturnedValue QQmlScriptData::scriptValueForContext(QQmlContextData *parent
return m_value.value();
Q_ASSERT(parentCtxt && parentCtxt->engine);
- QQmlEnginePrivate *ep = QQmlEnginePrivate::get(parentCtxt->engine);
QV4::ExecutionEngine *v4 = parentCtxt->engine->handle();
+
+ if (m_precompiledScript->unitData()->flags & QV4::CompiledData::Unit::IsESModule) {
+ m_loaded = true;
+
+ m_value.set(v4, m_precompiledScript->instantiate(v4));
+ if (!m_value.isNullOrUndefined())
+ m_precompiledScript->evaluate();
+
+ return m_value.value();
+ }
+
+ QQmlEnginePrivate *ep = QQmlEnginePrivate::get(parentCtxt->engine);
QV4::Scope scope(v4);
bool shared = m_precompiledScript->unitData()->flags & QV4::CompiledData::Unit::IsSharedLibrary;
@@ -2945,7 +2957,8 @@ void QQmlScriptData::clear()
}
QQmlScriptBlob::QQmlScriptBlob(const QUrl &url, QQmlTypeLoader *loader)
-: QQmlTypeLoader::Blob(url, JavaScriptFile, loader)
+ : QQmlTypeLoader::Blob(url, JavaScriptFile, loader)
+ , m_isModule(url.path().endsWith(QLatin1String(".mjs")))
{
}
@@ -2979,9 +2992,6 @@ void QQmlScriptBlob::dataReceived(const SourceCodeData &data)
return;
}
- QmlIR::Document irUnit(isDebugging());
-
- irUnit.jsModule.sourceTimeStamp = data.sourceTimeStamp();
QString error;
QString source = data.readAll(&error);
if (!error.isEmpty()) {
@@ -2989,31 +2999,47 @@ void QQmlScriptBlob::dataReceived(const SourceCodeData &data)
return;
}
- QmlIR::ScriptDirectivesCollector collector(&irUnit);
- irUnit.jsParserEngine.setDirectives(&collector);
+ QQmlRefPointer<QV4::CompiledData::CompilationUnit> unit;
- QList<QQmlError> errors;
- QQmlRefPointer<QV4::CompiledData::CompilationUnit> unit = QV4::Script::precompile(
- &irUnit.jsModule, &irUnit.jsParserEngine, &irUnit.jsGenerator, urlString(), finalUrlString(),
- source, &errors);
- // No need to addref on unit, it's initial refcount is 1
- source.clear();
- if (!errors.isEmpty()) {
- setError(errors);
- return;
- }
- if (!unit) {
- unit.adopt(new QV4::CompiledData::CompilationUnit);
- }
- irUnit.javaScriptCompilationUnit = unit;
+ if (m_isModule) {
+ QList<QQmlJS::DiagnosticMessage> diagnostics;
+ unit = QV4::ExecutionEngine::compileModule(isDebugging(), url(), source, &diagnostics);
+ QList<QQmlError> errors = QQmlEnginePrivate::qmlErrorFromDiagnostics(urlString(), diagnostics);
+ if (!errors.isEmpty()) {
+ setError(errors);
+ return;
+ }
+ } else {
+ QmlIR::Document irUnit(isDebugging());
- QmlIR::QmlUnitGenerator qmlGenerator;
- qmlGenerator.generate(irUnit);
+ irUnit.jsModule.sourceTimeStamp = data.sourceTimeStamp();
- if ((!disableDiskCache() || forceDiskCache()) && !isDebugging()) {
- QString errorString;
- if (!unit->saveToDisk(url(), &errorString)) {
- qCDebug(DBG_DISK_CACHE()) << "Error saving cached version of" << unit->fileName() << "to disk:" << errorString;
+ QmlIR::ScriptDirectivesCollector collector(&irUnit);
+ irUnit.jsParserEngine.setDirectives(&collector);
+
+ QList<QQmlError> errors;
+ unit = QV4::Script::precompile(
+ &irUnit.jsModule, &irUnit.jsParserEngine, &irUnit.jsGenerator, urlString(), finalUrlString(),
+ source, &errors);
+ // No need to addref on unit, it's initial refcount is 1
+ source.clear();
+ if (!errors.isEmpty()) {
+ setError(errors);
+ return;
+ }
+ if (!unit) {
+ unit.adopt(new QV4::CompiledData::CompilationUnit);
+ }
+ irUnit.javaScriptCompilationUnit = unit;
+
+ QmlIR::QmlUnitGenerator qmlGenerator;
+ qmlGenerator.generate(irUnit);
+
+ if ((!disableDiskCache() || forceDiskCache()) && !isDebugging()) {
+ QString errorString;
+ if (!unit->saveToDisk(url(), &errorString)) {
+ qCDebug(DBG_DISK_CACHE()) << "Error saving cached version of" << unit->fileName() << "to disk:" << errorString;
+ }
}
}
@@ -3049,26 +3075,28 @@ void QQmlScriptBlob::done()
}
}
- m_scriptData->typeNameCache = new QQmlTypeNameCache(m_importCache);
+ if (!m_isModule) {
+ m_scriptData->typeNameCache = new QQmlTypeNameCache(m_importCache);
- QSet<QString> ns;
+ QSet<QString> ns;
- for (int scriptIndex = 0; scriptIndex < m_scripts.count(); ++scriptIndex) {
- const ScriptReference &script = m_scripts.at(scriptIndex);
+ for (int scriptIndex = 0; scriptIndex < m_scripts.count(); ++scriptIndex) {
+ const ScriptReference &script = m_scripts.at(scriptIndex);
- m_scriptData->scripts.append(script.script);
+ m_scriptData->scripts.append(script.script);
- if (!script.nameSpace.isNull()) {
- if (!ns.contains(script.nameSpace)) {
- ns.insert(script.nameSpace);
- m_scriptData->typeNameCache->add(script.nameSpace);
+ if (!script.nameSpace.isNull()) {
+ if (!ns.contains(script.nameSpace)) {
+ ns.insert(script.nameSpace);
+ m_scriptData->typeNameCache->add(script.nameSpace);
+ }
}
+ m_scriptData->typeNameCache->add(script.qualifier, scriptIndex, script.nameSpace);
}
- m_scriptData->typeNameCache->add(script.qualifier, scriptIndex, script.nameSpace);
+
+ m_importCache.populateCache(m_scriptData->typeNameCache);
}
m_scripts.clear();
-
- m_importCache.populateCache(m_scriptData->typeNameCache);
}
QString QQmlScriptBlob::stringAt(int index) const
@@ -3099,20 +3127,36 @@ void QQmlScriptBlob::initializeFromCompilationUnit(const QQmlRefPointer<QV4::Com
QQmlRefPointer<QV4::CompiledData::CompilationUnit> script = m_scriptData->m_precompiledScript;
- QList<QQmlError> errors;
- for (quint32 i = 0, count = script->importCount(); i < count; ++i) {
- const QV4::CompiledData::Import *import = script->importAt(i);
- if (!addImport(import, &errors)) {
- Q_ASSERT(errors.size());
- QQmlError error(errors.takeFirst());
- error.setUrl(m_importCache.baseUrl());
- error.setLine(import->location.line);
- error.setColumn(import->location.column);
- errors.prepend(error); // put it back on the list after filling out information.
- setError(errors);
- return;
+ if (!m_isModule) {
+ QList<QQmlError> errors;
+ for (quint32 i = 0, count = script->importCount(); i < count; ++i) {
+ const QV4::CompiledData::Import *import = script->importAt(i);
+ if (!addImport(import, &errors)) {
+ Q_ASSERT(errors.size());
+ QQmlError error(errors.takeFirst());
+ error.setUrl(m_importCache.baseUrl());
+ error.setLine(import->location.line);
+ error.setColumn(import->location.column);
+ errors.prepend(error); // put it back on the list after filling out information.
+ setError(errors);
+ return;
+ }
}
}
+
+ auto *v4 = QQmlEnginePrivate::getV4Engine(typeLoader()->engine());
+
+ v4->injectModule(unit);
+
+ for (const QString &request: unit->moduleRequests()) {
+ if (v4->moduleForUrl(QUrl(request), unit.data()))
+ continue;
+
+ const QUrl absoluteRequest = unit->finalUrl().resolved(QUrl(request));
+ QQmlRefPointer<QQmlScriptBlob> blob = typeLoader()->getScript(absoluteRequest);
+ addDependency(blob.data());
+ scriptImported(blob, /* ### */QV4::CompiledData::Location(), /*qualifier*/QString(), /*namespace*/QString());
+ }
}
QQmlQmldirData::QQmlQmldirData(const QUrl &url, QQmlTypeLoader *loader)
diff --git a/src/qml/qml/qqmltypeloader_p.h b/src/qml/qml/qqmltypeloader_p.h
index 98ac4b1bc1..26090c6af7 100644
--- a/src/qml/qml/qqmltypeloader_p.h
+++ b/src/qml/qml/qqmltypeloader_p.h
@@ -589,6 +589,7 @@ private:
QList<ScriptReference> m_scripts;
QQmlRefPointer<QQmlScriptData> m_scriptData;
+ const bool m_isModule;
};
class Q_AUTOTEST_EXPORT QQmlQmldirData : public QQmlTypeLoader::Blob
diff --git a/tests/auto/qml/qqmllanguage/data/importJsModule.1.mjs b/tests/auto/qml/qqmllanguage/data/importJsModule.1.mjs
new file mode 100644
index 0000000000..c17a351216
--- /dev/null
+++ b/tests/auto/qml/qqmllanguage/data/importJsModule.1.mjs
@@ -0,0 +1,4 @@
+
+export function ok() {
+ return true;
+}
diff --git a/tests/auto/qml/qqmllanguage/data/importJsModule.1.qml b/tests/auto/qml/qqmllanguage/data/importJsModule.1.qml
new file mode 100644
index 0000000000..cc988a6114
--- /dev/null
+++ b/tests/auto/qml/qqmllanguage/data/importJsModule.1.qml
@@ -0,0 +1,6 @@
+import QtQuick 2.0
+import "importJsModule.1.mjs" as JSModule
+
+Item {
+ property bool test: JSModule.ok()
+}
diff --git a/tests/auto/qml/qqmllanguage/data/importJsModule.2.qml b/tests/auto/qml/qqmllanguage/data/importJsModule.2.qml
new file mode 100644
index 0000000000..0799585622
--- /dev/null
+++ b/tests/auto/qml/qqmllanguage/data/importJsModule.2.qml
@@ -0,0 +1,6 @@
+import QtQuick 2.0
+import org.qtproject.PureESJsModule 1.0
+
+Item {
+ property bool test: ModuleAPI.ok()
+}
diff --git a/tests/auto/qml/qqmllanguage/data/importJsModule.3.indirect.mjs b/tests/auto/qml/qqmllanguage/data/importJsModule.3.indirect.mjs
new file mode 100644
index 0000000000..41d2f391bf
--- /dev/null
+++ b/tests/auto/qml/qqmllanguage/data/importJsModule.3.indirect.mjs
@@ -0,0 +1,4 @@
+
+import { indirectOk } from "./importJsModule.3.mjs";
+
+export function ok() { return indirectOk(); }
diff --git a/tests/auto/qml/qqmllanguage/data/importJsModule.3.mjs b/tests/auto/qml/qqmllanguage/data/importJsModule.3.mjs
new file mode 100644
index 0000000000..3623dc6f4f
--- /dev/null
+++ b/tests/auto/qml/qqmllanguage/data/importJsModule.3.mjs
@@ -0,0 +1,4 @@
+export function indirectOk() { return true; }
+
+export * from "./importJsModule.3.indirect.mjs";
+
diff --git a/tests/auto/qml/qqmllanguage/data/importJsModule.3.qml b/tests/auto/qml/qqmllanguage/data/importJsModule.3.qml
new file mode 100644
index 0000000000..3b187205c7
--- /dev/null
+++ b/tests/auto/qml/qqmllanguage/data/importJsModule.3.qml
@@ -0,0 +1,6 @@
+import QtQuick 2.0
+import "importJsModule.3.mjs" as JSModule
+
+Item {
+ property bool test: JSModule.ok()
+}
diff --git a/tests/auto/qml/qqmllanguage/data/lib/org/qtproject/PureESJsModule/API.mjs b/tests/auto/qml/qqmllanguage/data/lib/org/qtproject/PureESJsModule/API.mjs
new file mode 100644
index 0000000000..c17a351216
--- /dev/null
+++ b/tests/auto/qml/qqmllanguage/data/lib/org/qtproject/PureESJsModule/API.mjs
@@ -0,0 +1,4 @@
+
+export function ok() {
+ return true;
+}
diff --git a/tests/auto/qml/qqmllanguage/data/lib/org/qtproject/PureESJsModule/qmldir b/tests/auto/qml/qqmllanguage/data/lib/org/qtproject/PureESJsModule/qmldir
new file mode 100644
index 0000000000..59b4740f75
--- /dev/null
+++ b/tests/auto/qml/qqmllanguage/data/lib/org/qtproject/PureESJsModule/qmldir
@@ -0,0 +1 @@
+ModuleAPI 1.0 API.mjs
diff --git a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp
index f98233f153..97727c1794 100644
--- a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp
+++ b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp
@@ -39,6 +39,7 @@
#include <QQmlFileSelector>
#include <QFileSelector>
#include <QEasingCurve>
+#include <QScopeGuard>
#include <private/qqmlproperty_p.h>
#include <private/qqmlmetatype_p.h>
@@ -182,6 +183,8 @@ private slots:
void importIncorrectCase();
void importJs_data();
void importJs();
+ void importJsModule_data();
+ void importJsModule();
void explicitSelfImport();
void importInternalType();
@@ -3125,6 +3128,36 @@ void tst_qqmllanguage::importJs()
engine.setImportPathList(defaultImportPathList);
}
+void tst_qqmllanguage::importJsModule_data()
+{
+ QTest::addColumn<QString>("file");
+
+ QTest::newRow("plainImport")
+ << "importJsModule.1.qml";
+
+ QTest::newRow("ImportQmlStyle")
+ << "importJsModule.2.qml";
+
+ QTest::newRow("plainImportWithCycle")
+ << "importJsModule.3.qml";
+}
+
+void tst_qqmllanguage::importJsModule()
+{
+ QFETCH(QString, file);
+
+ engine.setImportPathList(QStringList(defaultImportPathList) << testFile("lib"));
+ auto importPathGuard = qScopeGuard([this]{
+ engine.setImportPathList(defaultImportPathList);
+ });
+
+ QQmlComponent component(&engine, testFileUrl(file));
+ QVERIFY2(!component.isError(), qPrintable(component.errorString()));
+ QScopedPointer<QObject> object(component.create());
+ QVERIFY(object != nullptr);
+ QCOMPARE(object->property("test").toBool(),true);
+}
+
void tst_qqmllanguage::explicitSelfImport()
{
engine.setImportPathList(QStringList(defaultImportPathList) << testFile("lib"));