From 4b35d7b51b61f16a5f3d862419ba173783a21079 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 10 Jul 2025 00:48:15 -0700 Subject: [PATCH] core: support qs. imports --- src/core/generation.cpp | 10 +++++-- src/core/generation.hpp | 2 +- src/core/qsintercept.cpp | 61 +++++++++++++++++++++++++++++----------- src/core/qsintercept.hpp | 11 ++++++-- src/core/rootwrapper.cpp | 13 +++++---- src/core/scan.cpp | 60 ++++++++++++++++++++++++++++++++++----- src/core/scan.hpp | 1 + 7 files changed, 123 insertions(+), 35 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index d99409bb..332b7d24 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -37,7 +37,7 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) : rootPath(rootPath) , scanner(std::move(scanner)) , urlInterceptor(this->rootPath) - , interceptNetFactory(this->scanner.fileIntercepts) + , interceptNetFactory(this->rootPath, this->scanner.fileIntercepts) , engine(new QQmlEngine()) { g_generations.insert(this->engine, this); @@ -45,6 +45,8 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings); this->engine->addUrlInterceptor(&this->urlInterceptor); + this->engine->addImportPath("qs:@/"); + this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory); this->engine->setIncubationController(&this->delayedIncubationController); @@ -322,9 +324,11 @@ void EngineGeneration::incubationControllerDestroyed() { } } -void EngineGeneration::onEngineWarnings(const QList& warnings) const { +void EngineGeneration::onEngineWarnings(const QList& warnings) { for (const auto& error: warnings) { - auto rel = "**/" % this->rootPath.relativeFilePath(error.url().path()); + const auto& url = error.url(); + auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5) + : url.toString(); QString objectName; auto desc = error.description(); diff --git a/src/core/generation.hpp b/src/core/generation.hpp index df2c85a4..9889e3cf 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -84,7 +84,7 @@ private slots: void onFileChanged(const QString& name); void onDirectoryChanged(); void incubationControllerDestroyed(); - void onEngineWarnings(const QList& warnings) const; + static void onEngineWarnings(const QList& warnings); private: void postReload(); diff --git a/src/core/qsintercept.cpp b/src/core/qsintercept.cpp index 560331d9..6687681b 100644 --- a/src/core/qsintercept.cpp +++ b/src/core/qsintercept.cpp @@ -1,6 +1,7 @@ #include "qsintercept.hpp" #include +#include #include #include #include @@ -25,27 +26,44 @@ QUrl QsUrlInterceptor::intercept( auto url = originalUrl; if (url.scheme() == "root") { - url.setScheme("qsintercept"); + url.setScheme("qs"); auto path = url.path(); if (path.startsWith('/')) path = path.sliced(1); - url.setPath(this->configRoot.filePath(path)); + url.setPath("@/qs/" % path); qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url; } - // Some types such as Image take into account where they are loading from, and force - // asynchronous loading over a network. qsintercept is considered to be over a network. - if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") { - // Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which - // case we want to keep the intercept, otherwise objects created from those paths - // will not be able to use singletons. - if (url.path().endsWith(".qml")) return url; + if (url.scheme() == "qs") { + auto path = url.path(); - auto newUrl = url; - newUrl.setScheme("file"); - qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl; - return newUrl; + // Our import path is on "qs:@/". + // We want to blackhole any import resolution outside of the config folder as it breaks Qt + // but NOT file lookups that might be on "qs:/" due to a missing "file:/" prefix. + if (path.startsWith("@/qs/")) { + path = this->configRoot.filePath(path.sliced(5)); + } else if (!path.startsWith("/")) { + qCDebug(logQsIntercept) << "Blackholed import URL" << url; + return QUrl("qrc:/qs-blackhole"); + } + + // Some types such as Image take into account where they are loading from, and force + // asynchronous loading over a network. qs: is considered to be over a network. + // In those cases we want to return a file:// url so asynchronous loading is not forced. + if (type == QQmlAbstractUrlInterceptor::DataType::UrlString) { + // Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which + // case we want to keep the intercept, otherwise objects created from those paths + // will not be able to use singletons. + if (path.endsWith(".qml")) return url; + + auto newUrl = url; + newUrl.setScheme("file"); + // above check asserts path starts with /qs/ + newUrl.setPath(path); + qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl; + return newUrl; + } } return url; @@ -67,10 +85,12 @@ qint64 QsInterceptDataReply::readData(char* data, qint64 maxSize) { } QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager( + const QDir& configRoot, const QHash& fileIntercepts, QObject* parent ) : QNetworkAccessManager(parent) + , configRoot(configRoot) , fileIntercepts(fileIntercepts) {} QNetworkReply* QsInterceptNetworkAccessManager::createRequest( @@ -79,19 +99,26 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest( QIODevice* outgoingData ) { auto url = req.url(); - if (url.scheme() == "qsintercept") { + + if (url.scheme() == "qs") { auto path = url.path(); + + if (path.startsWith("@/qs/")) path = this->configRoot.filePath(path.sliced(5)); + // otherwise pass through to fs + qCDebug(logQsIntercept) << "Got intercept for" << path << "contains" << this->fileIntercepts.value(path); - auto data = this->fileIntercepts.value(path); - if (data != nullptr) { + + if (auto data = this->fileIntercepts.value(path); !data.isEmpty()) { return new QsInterceptDataReply(data, this); } auto fileReq = req; auto fileUrl = req.url(); fileUrl.setScheme("file"); + fileUrl.setPath(path); qCDebug(logQsIntercept) << "Passing through intercept" << url << "to" << fileUrl; + fileReq.setUrl(fileUrl); return this->QNetworkAccessManager::createRequest(op, fileReq, outgoingData); } @@ -100,5 +127,5 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest( } QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) { - return new QsInterceptNetworkAccessManager(this->fileIntercepts, parent); + return new QsInterceptNetworkAccessManager(this->configRoot, this->fileIntercepts, parent); } diff --git a/src/core/qsintercept.hpp b/src/core/qsintercept.hpp index f0e10981..c3d8b552 100644 --- a/src/core/qsintercept.hpp +++ b/src/core/qsintercept.hpp @@ -45,6 +45,7 @@ class QsInterceptNetworkAccessManager: public QNetworkAccessManager { public: QsInterceptNetworkAccessManager( + const QDir& configRoot, const QHash& fileIntercepts, QObject* parent = nullptr ); @@ -57,15 +58,21 @@ protected: ) override; private: + QDir configRoot; const QHash& fileIntercepts; }; class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory { public: - QsInterceptNetworkAccessManagerFactory(const QHash& fileIntercepts) - : fileIntercepts(fileIntercepts) {} + QsInterceptNetworkAccessManagerFactory( + const QDir& configRoot, + const QHash& fileIntercepts + ) + : configRoot(configRoot) + , fileIntercepts(fileIntercepts) {} QNetworkAccessManager* create(QObject* parent) override; private: + QDir configRoot; const QHash& fileIntercepts; }; diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index b51b4034..2968402e 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -43,7 +43,8 @@ RootWrapper::~RootWrapper() { } void RootWrapper::reloadGraph(bool hard) { - auto rootPath = QFileInfo(this->rootPath).dir(); + auto rootFile = QFileInfo(this->rootPath); + auto rootPath = rootFile.dir(); auto scanner = QmlScanner(rootPath); scanner.scanQmlFile(this->rootPath); @@ -58,9 +59,9 @@ void RootWrapper::reloadGraph(bool hard) { QDir::setCurrent(this->originalWorkingDirectory); - auto url = QUrl::fromLocalFile(this->rootPath); - // unless the original file comes from the qsintercept scheme - url.setScheme("qsintercept"); + QUrl url; + url.setScheme("qs"); + url.setPath("@/qs/" % rootFile.fileName()); auto component = QQmlComponent(generation->engine, url); if (!component.isReady()) { @@ -69,7 +70,9 @@ void RootWrapper::reloadGraph(bool hard) { auto errors = component.errors(); for (auto& error: errors) { - auto rel = "**/" % rootPath.relativeFilePath(error.url().path()); + const auto& url = error.url(); + auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5) + : url.toString(); auto msg = " caused by " % rel % '[' % QString::number(error.line()) % ':' % QString::number(error.column()) % "]: " % error.description(); errorString += '\n' % msg; diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 90b19b5a..b84b3d86 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -47,7 +47,6 @@ void QmlScanner::scanDir(const QString& path) { } } - // Due to the qsintercept:// protocol a qmldir is always required, even without singletons. if (!seenQmldir) { qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path << "singletons" << singletons; @@ -55,6 +54,29 @@ void QmlScanner::scanDir(const QString& path) { QString qmldir; auto stream = QTextStream(&qmldir); + // cant derive a module name if not in shell path + if (path.startsWith(this->rootPath.path())) { + auto end = path.sliced(this->rootPath.path().length()); + + // verify we have a valid module name. + for (auto& c: end) { + if (c == '/') c = '.'; + else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') + || c == '_') + { + } else { + qCWarning(logQmlScanner) + << "Module path contains invalid characters for a module name: " << end; + goto skipadd; + } + } + + stream << "module qs" << end << '\n'; + skipadd:; + } else { + qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder."; + } + for (auto& singleton: singletons) { stream << "singleton " << singleton.sliced(0, singleton.length() - 4) << " 1.0 " << singleton << "\n"; @@ -92,15 +114,39 @@ bool QmlScanner::scanQmlFile(const QString& path) { qCDebug(logQmlScanner) << "Discovered singleton" << path; singleton = true; } else if (line.startsWith("import")) { + // we dont care about "import qs" as we always load the root folder + if (auto importCursor = line.indexOf(" qs."); importCursor != -1) { + importCursor += 4; + QString path; - auto startQuot = line.indexOf('"'); - if (startQuot == -1 || line.length() < startQuot + 3) continue; - auto endQuot = line.indexOf('"', startQuot + 1); - if (endQuot == -1) continue; + while (importCursor != line.length()) { + auto c = line.at(importCursor); + if (c == '.') c = '/'; + else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') + || c == '_') + { + } else { + qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line; + goto next; + } - auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1); - imports.push_back(name); + path.append(c); + importCursor += 1; + } + + imports.append(this->rootPath.filePath(path)); + } else if (auto startQuot = line.indexOf('"'); + startQuot != -1 && line.length() >= startQuot + 3) + { + auto endQuot = line.indexOf('"', startQuot + 1); + if (endQuot == -1) continue; + + auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1); + imports.push_back(name); + } } else if (line.contains('{')) break; + + next:; } file.close(); diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 80b44ca0..6220baeb 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -16,6 +16,7 @@ public: QmlScanner() = default; QmlScanner(const QDir& rootPath): rootPath(rootPath) {} + // path must be canonical void scanDir(const QString& path); // returns if the file has a singleton bool scanQmlFile(const QString& path);