Skip to content

Commit 4b35d7b

Browse files
committed
core: support qs. imports
1 parent 3d594e1 commit 4b35d7b

File tree

7 files changed

+124
-36
lines changed

7 files changed

+124
-36
lines changed

src/core/generation.cpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,16 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
3737
: rootPath(rootPath)
3838
, scanner(std::move(scanner))
3939
, urlInterceptor(this->rootPath)
40-
, interceptNetFactory(this->scanner.fileIntercepts)
40+
, interceptNetFactory(this->rootPath, this->scanner.fileIntercepts)
4141
, engine(new QQmlEngine()) {
4242
g_generations.insert(this->engine, this);
4343

4444
this->engine->setOutputWarningsToStandardError(false);
4545
QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings);
4646

4747
this->engine->addUrlInterceptor(&this->urlInterceptor);
48+
this->engine->addImportPath("qs:@/");
49+
4850
this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory);
4951
this->engine->setIncubationController(&this->delayedIncubationController);
5052

@@ -322,9 +324,11 @@ void EngineGeneration::incubationControllerDestroyed() {
322324
}
323325
}
324326

325-
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) const {
327+
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) {
326328
for (const auto& error: warnings) {
327-
auto rel = "**/" % this->rootPath.relativeFilePath(error.url().path());
329+
const auto& url = error.url();
330+
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
331+
: url.toString();
328332

329333
QString objectName;
330334
auto desc = error.description();

src/core/generation.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private slots:
8484
void onFileChanged(const QString& name);
8585
void onDirectoryChanged();
8686
void incubationControllerDestroyed();
87-
void onEngineWarnings(const QList<QQmlError>& warnings) const;
87+
static void onEngineWarnings(const QList<QQmlError>& warnings);
8888

8989
private:
9090
void postReload();

src/core/qsintercept.cpp

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "qsintercept.hpp"
22
#include <cstring>
33

4+
#include <qdir.h>
45
#include <qhash.h>
56
#include <qiodevice.h>
67
#include <qlogging.h>
@@ -25,27 +26,44 @@ QUrl QsUrlInterceptor::intercept(
2526
auto url = originalUrl;
2627

2728
if (url.scheme() == "root") {
28-
url.setScheme("qsintercept");
29+
url.setScheme("qs");
2930

3031
auto path = url.path();
3132
if (path.startsWith('/')) path = path.sliced(1);
32-
url.setPath(this->configRoot.filePath(path));
33+
url.setPath("@/qs/" % path);
3334

3435
qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url;
3536
}
3637

37-
// Some types such as Image take into account where they are loading from, and force
38-
// asynchronous loading over a network. qsintercept is considered to be over a network.
39-
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") {
40-
// Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which
41-
// case we want to keep the intercept, otherwise objects created from those paths
42-
// will not be able to use singletons.
43-
if (url.path().endsWith(".qml")) return url;
44-
45-
auto newUrl = url;
46-
newUrl.setScheme("file");
47-
qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl;
48-
return newUrl;
38+
if (url.scheme() == "qs") {
39+
auto path = url.path();
40+
41+
// Our import path is on "qs:@/".
42+
// We want to blackhole any import resolution outside of the config folder as it breaks Qt
43+
// but NOT file lookups that might be on "qs:/" due to a missing "file:/" prefix.
44+
if (path.startsWith("@/qs/")) {
45+
path = this->configRoot.filePath(path.sliced(5));
46+
} else if (!path.startsWith("/")) {
47+
qCDebug(logQsIntercept) << "Blackholed import URL" << url;
48+
return QUrl("qrc:/qs-blackhole");
49+
}
50+
51+
// Some types such as Image take into account where they are loading from, and force
52+
// asynchronous loading over a network. qs: is considered to be over a network.
53+
// In those cases we want to return a file:// url so asynchronous loading is not forced.
54+
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString) {
55+
// Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which
56+
// case we want to keep the intercept, otherwise objects created from those paths
57+
// will not be able to use singletons.
58+
if (path.endsWith(".qml")) return url;
59+
60+
auto newUrl = url;
61+
newUrl.setScheme("file");
62+
// above check asserts path starts with /qs/
63+
newUrl.setPath(path);
64+
qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl;
65+
return newUrl;
66+
}
4967
}
5068

5169
return url;
@@ -67,10 +85,12 @@ qint64 QsInterceptDataReply::readData(char* data, qint64 maxSize) {
6785
}
6886

6987
QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager(
88+
const QDir& configRoot,
7089
const QHash<QString, QString>& fileIntercepts,
7190
QObject* parent
7291
)
7392
: QNetworkAccessManager(parent)
93+
, configRoot(configRoot)
7494
, fileIntercepts(fileIntercepts) {}
7595

7696
QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
@@ -79,19 +99,26 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
7999
QIODevice* outgoingData
80100
) {
81101
auto url = req.url();
82-
if (url.scheme() == "qsintercept") {
102+
103+
if (url.scheme() == "qs") {
83104
auto path = url.path();
105+
106+
if (path.startsWith("@/qs/")) path = this->configRoot.filePath(path.sliced(5));
107+
// otherwise pass through to fs
108+
84109
qCDebug(logQsIntercept) << "Got intercept for" << path << "contains"
85110
<< this->fileIntercepts.value(path);
86-
auto data = this->fileIntercepts.value(path);
87-
if (data != nullptr) {
111+
112+
if (auto data = this->fileIntercepts.value(path); !data.isEmpty()) {
88113
return new QsInterceptDataReply(data, this);
89114
}
90115

91116
auto fileReq = req;
92117
auto fileUrl = req.url();
93118
fileUrl.setScheme("file");
119+
fileUrl.setPath(path);
94120
qCDebug(logQsIntercept) << "Passing through intercept" << url << "to" << fileUrl;
121+
95122
fileReq.setUrl(fileUrl);
96123
return this->QNetworkAccessManager::createRequest(op, fileReq, outgoingData);
97124
}
@@ -100,5 +127,5 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
100127
}
101128

102129
QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) {
103-
return new QsInterceptNetworkAccessManager(this->fileIntercepts, parent);
130+
return new QsInterceptNetworkAccessManager(this->configRoot, this->fileIntercepts, parent);
104131
}

src/core/qsintercept.hpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class QsInterceptNetworkAccessManager: public QNetworkAccessManager {
4545

4646
public:
4747
QsInterceptNetworkAccessManager(
48+
const QDir& configRoot,
4849
const QHash<QString, QString>& fileIntercepts,
4950
QObject* parent = nullptr
5051
);
@@ -57,15 +58,21 @@ class QsInterceptNetworkAccessManager: public QNetworkAccessManager {
5758
) override;
5859

5960
private:
61+
QDir configRoot;
6062
const QHash<QString, QString>& fileIntercepts;
6163
};
6264

6365
class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory {
6466
public:
65-
QsInterceptNetworkAccessManagerFactory(const QHash<QString, QString>& fileIntercepts)
66-
: fileIntercepts(fileIntercepts) {}
67+
QsInterceptNetworkAccessManagerFactory(
68+
const QDir& configRoot,
69+
const QHash<QString, QString>& fileIntercepts
70+
)
71+
: configRoot(configRoot)
72+
, fileIntercepts(fileIntercepts) {}
6773
QNetworkAccessManager* create(QObject* parent) override;
6874

6975
private:
76+
QDir configRoot;
7077
const QHash<QString, QString>& fileIntercepts;
7178
};

src/core/rootwrapper.cpp

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ RootWrapper::~RootWrapper() {
4343
}
4444

4545
void RootWrapper::reloadGraph(bool hard) {
46-
auto rootPath = QFileInfo(this->rootPath).dir();
46+
auto rootFile = QFileInfo(this->rootPath);
47+
auto rootPath = rootFile.dir();
4748
auto scanner = QmlScanner(rootPath);
4849
scanner.scanQmlFile(this->rootPath);
4950

@@ -58,9 +59,9 @@ void RootWrapper::reloadGraph(bool hard) {
5859

5960
QDir::setCurrent(this->originalWorkingDirectory);
6061

61-
auto url = QUrl::fromLocalFile(this->rootPath);
62-
// unless the original file comes from the qsintercept scheme
63-
url.setScheme("qsintercept");
62+
QUrl url;
63+
url.setScheme("qs");
64+
url.setPath("@/qs/" % rootFile.fileName());
6465
auto component = QQmlComponent(generation->engine, url);
6566

6667
if (!component.isReady()) {
@@ -69,7 +70,9 @@ void RootWrapper::reloadGraph(bool hard) {
6970

7071
auto errors = component.errors();
7172
for (auto& error: errors) {
72-
auto rel = "**/" % rootPath.relativeFilePath(error.url().path());
73+
const auto& url = error.url();
74+
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
75+
: url.toString();
7376
auto msg = " caused by " % rel % '[' % QString::number(error.line()) % ':'
7477
% QString::number(error.column()) % "]: " % error.description();
7578
errorString += '\n' % msg;

src/core/scan.cpp

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,36 @@ void QmlScanner::scanDir(const QString& path) {
4747
}
4848
}
4949

50-
// Due to the qsintercept:// protocol a qmldir is always required, even without singletons.
5150
if (!seenQmldir) {
5251
qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path << "singletons"
5352
<< singletons;
5453

5554
QString qmldir;
5655
auto stream = QTextStream(&qmldir);
5756

57+
// cant derive a module name if not in shell path
58+
if (path.startsWith(this->rootPath.path())) {
59+
auto end = path.sliced(this->rootPath.path().length());
60+
61+
// verify we have a valid module name.
62+
for (auto& c: end) {
63+
if (c == '/') c = '.';
64+
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
65+
|| c == '_')
66+
{
67+
} else {
68+
qCWarning(logQmlScanner)
69+
<< "Module path contains invalid characters for a module name: " << end;
70+
goto skipadd;
71+
}
72+
}
73+
74+
stream << "module qs" << end << '\n';
75+
skipadd:;
76+
} else {
77+
qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder.";
78+
}
79+
5880
for (auto& singleton: singletons) {
5981
stream << "singleton " << singleton.sliced(0, singleton.length() - 4) << " 1.0 " << singleton
6082
<< "\n";
@@ -92,15 +114,39 @@ bool QmlScanner::scanQmlFile(const QString& path) {
92114
qCDebug(logQmlScanner) << "Discovered singleton" << path;
93115
singleton = true;
94116
} else if (line.startsWith("import")) {
117+
// we dont care about "import qs" as we always load the root folder
118+
if (auto importCursor = line.indexOf(" qs."); importCursor != -1) {
119+
importCursor += 4;
120+
QString path;
121+
122+
while (importCursor != line.length()) {
123+
auto c = line.at(importCursor);
124+
if (c == '.') c = '/';
125+
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
126+
|| c == '_')
127+
{
128+
} else {
129+
qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line;
130+
goto next;
131+
}
132+
133+
path.append(c);
134+
importCursor += 1;
135+
}
95136

96-
auto startQuot = line.indexOf('"');
97-
if (startQuot == -1 || line.length() < startQuot + 3) continue;
98-
auto endQuot = line.indexOf('"', startQuot + 1);
99-
if (endQuot == -1) continue;
137+
imports.append(this->rootPath.filePath(path));
138+
} else if (auto startQuot = line.indexOf('"');
139+
startQuot != -1 && line.length() >= startQuot + 3)
140+
{
141+
auto endQuot = line.indexOf('"', startQuot + 1);
142+
if (endQuot == -1) continue;
100143

101-
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
102-
imports.push_back(name);
144+
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
145+
imports.push_back(name);
146+
}
103147
} else if (line.contains('{')) break;
148+
149+
next:;
104150
}
105151

106152
file.close();

src/core/scan.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class QmlScanner {
1616
QmlScanner() = default;
1717
QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
1818

19+
// path must be canonical
1920
void scanDir(const QString& path);
2021
// returns if the file has a singleton
2122
bool scanQmlFile(const QString& path);

0 commit comments

Comments
 (0)