@voilabs/plugins 0.0.1-beta.0 → 0.0.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +424 -32
- package/dist/client.d.ts +16 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +87 -5
- package/dist/client.js.map +1 -1
- package/dist/http.js +19 -3
- package/dist/http.js.map +1 -1
- package/dist/injection.d.ts +1 -1
- package/dist/injection.d.ts.map +1 -1
- package/dist/injection.js +66 -22
- package/dist/injection.js.map +1 -1
- package/dist/manager.d.ts +42 -13
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +454 -19
- package/dist/manager.js.map +1 -1
- package/dist/react.d.ts +35 -3
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +165 -1
- package/dist/react.js.map +1 -1
- package/dist/types.d.ts +13 -1
- package/dist/types.d.ts.map +1 -1
- package/github-examples/README.md +23 -0
- package/github-examples/google-tag-manager/assets/admin.css +27 -0
- package/github-examples/google-tag-manager/components/dashboard-card.js +10 -0
- package/github-examples/google-tag-manager/components/settings.js +21 -0
- package/github-examples/google-tag-manager/schema.json +139 -0
- package/package.json +2 -1
package/dist/manager.js
CHANGED
|
@@ -49,6 +49,88 @@ export class MemoryPluginDatabase {
|
|
|
49
49
|
this.records.delete(storageKey(pluginId, scope.tenantId));
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
export class RedisPluginDatabase {
|
|
53
|
+
client;
|
|
54
|
+
prefix;
|
|
55
|
+
constructor(client, options = {}) {
|
|
56
|
+
this.client = client;
|
|
57
|
+
this.prefix = options.prefix ?? "voilabs:plugins";
|
|
58
|
+
}
|
|
59
|
+
async list(scope = {}) {
|
|
60
|
+
const pluginIds = await this.readIndex(scope);
|
|
61
|
+
const installations = await Promise.all(pluginIds.map((pluginId) => this.load(pluginId, scope)));
|
|
62
|
+
return installations.filter((installation) => Boolean(installation));
|
|
63
|
+
}
|
|
64
|
+
async load(pluginId, scope = {}) {
|
|
65
|
+
const value = await this.client.get(this.installationKey(pluginId, scope));
|
|
66
|
+
if (!value) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return parseRedisJson(value);
|
|
70
|
+
}
|
|
71
|
+
async save(installation) {
|
|
72
|
+
const scope = { tenantId: installation.tenantId };
|
|
73
|
+
await this.client.set(this.installationKey(installation.pluginId, scope), JSON.stringify(installation));
|
|
74
|
+
await this.addToIndex(installation.pluginId, scope);
|
|
75
|
+
return installation;
|
|
76
|
+
}
|
|
77
|
+
async update(pluginId, patch, scope = {}) {
|
|
78
|
+
const current = await this.load(pluginId, scope);
|
|
79
|
+
if (!current) {
|
|
80
|
+
throw new PluginNotInstalledError(pluginId);
|
|
81
|
+
}
|
|
82
|
+
const updated = {
|
|
83
|
+
...current,
|
|
84
|
+
...patch,
|
|
85
|
+
pluginId,
|
|
86
|
+
tenantId: scope.tenantId,
|
|
87
|
+
updatedAt: new Date().toISOString(),
|
|
88
|
+
};
|
|
89
|
+
await this.save(updated);
|
|
90
|
+
return updated;
|
|
91
|
+
}
|
|
92
|
+
async delete(pluginId, scope = {}) {
|
|
93
|
+
await this.client.del(this.installationKey(pluginId, scope));
|
|
94
|
+
await this.removeFromIndex(pluginId, scope);
|
|
95
|
+
}
|
|
96
|
+
async readIndex(scope) {
|
|
97
|
+
if (this.client.smembers) {
|
|
98
|
+
const members = await this.client.smembers(this.indexKey(scope));
|
|
99
|
+
return normalizeRedisMembers(members);
|
|
100
|
+
}
|
|
101
|
+
const value = await this.client.get(this.indexKey(scope));
|
|
102
|
+
if (!value) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const parsed = parseRedisJson(value);
|
|
106
|
+
return Array.isArray(parsed)
|
|
107
|
+
? parsed.filter((item) => typeof item === "string")
|
|
108
|
+
: [];
|
|
109
|
+
}
|
|
110
|
+
async addToIndex(pluginId, scope) {
|
|
111
|
+
if (this.client.sadd) {
|
|
112
|
+
await this.client.sadd(this.indexKey(scope), pluginId);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const ids = new Set(await this.readIndex(scope));
|
|
116
|
+
ids.add(pluginId);
|
|
117
|
+
await this.client.set(this.indexKey(scope), JSON.stringify([...ids]));
|
|
118
|
+
}
|
|
119
|
+
async removeFromIndex(pluginId, scope) {
|
|
120
|
+
if (this.client.srem) {
|
|
121
|
+
await this.client.srem(this.indexKey(scope), pluginId);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const ids = (await this.readIndex(scope)).filter((id) => id !== pluginId);
|
|
125
|
+
await this.client.set(this.indexKey(scope), JSON.stringify(ids));
|
|
126
|
+
}
|
|
127
|
+
installationKey(pluginId, scope) {
|
|
128
|
+
return `${this.prefix}:installation:${scopeKey(scope.tenantId)}:${encodeKeyPart(pluginId)}`;
|
|
129
|
+
}
|
|
130
|
+
indexKey(scope) {
|
|
131
|
+
return `${this.prefix}:index:${scopeKey(scope.tenantId)}`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
52
134
|
export class PluginManager {
|
|
53
135
|
marketplaces;
|
|
54
136
|
redisUrl;
|
|
@@ -62,17 +144,27 @@ export class PluginManager {
|
|
|
62
144
|
marketplaceSyncPromise;
|
|
63
145
|
autoSyncMarketplaces;
|
|
64
146
|
marketplaceRefreshIntervalMs;
|
|
147
|
+
marketplaceRequestTimeoutMs;
|
|
65
148
|
github;
|
|
149
|
+
remotePluginAssets = new Map();
|
|
66
150
|
constructor(options = {}) {
|
|
67
151
|
this.marketplaces = options.marketplaces ?? [];
|
|
68
152
|
this.redisUrl = options.redisUrl;
|
|
69
|
-
this.database =
|
|
153
|
+
this.database =
|
|
154
|
+
options.database ??
|
|
155
|
+
(options.redis
|
|
156
|
+
? new RedisPluginDatabase(options.redis, {
|
|
157
|
+
prefix: options.redisPrefix,
|
|
158
|
+
})
|
|
159
|
+
: new MemoryPluginDatabase());
|
|
70
160
|
this.encryption = options.encryption;
|
|
71
161
|
this.fetcher = options.fetcher ?? globalThis.fetch?.bind(globalThis);
|
|
72
162
|
this.defaultTenantId = options.defaultTenantId;
|
|
73
163
|
this.autoSyncMarketplaces =
|
|
74
164
|
options.autoSyncMarketplaces ?? this.marketplaces.length > 0;
|
|
75
165
|
this.marketplaceRefreshIntervalMs = options.marketplaceRefreshIntervalMs;
|
|
166
|
+
this.marketplaceRequestTimeoutMs =
|
|
167
|
+
options.marketplaceRequestTimeoutMs ?? 15_000;
|
|
76
168
|
this.github = options.github ?? {};
|
|
77
169
|
if (options.plugins?.length) {
|
|
78
170
|
this.register(options.plugins);
|
|
@@ -306,9 +398,70 @@ export class PluginManager {
|
|
|
306
398
|
.map((component) => ({
|
|
307
399
|
...component,
|
|
308
400
|
pluginId: plugin.id,
|
|
401
|
+
pluginName: plugin.name,
|
|
402
|
+
pluginVersion: plugin.version,
|
|
309
403
|
}));
|
|
310
404
|
});
|
|
311
405
|
}
|
|
406
|
+
async listFrontendComponents(options = {}) {
|
|
407
|
+
await this.ready();
|
|
408
|
+
const installedOnly = options.installedOnly ?? true;
|
|
409
|
+
const enabledOnly = options.enabledOnly ?? true;
|
|
410
|
+
const tenantId = this.resolveTenantId(options);
|
|
411
|
+
const components = this.getFrontendComponents(options.slot).filter((component) => !options.pluginId || component.pluginId === options.pluginId);
|
|
412
|
+
if (!installedOnly && !enabledOnly) {
|
|
413
|
+
return components;
|
|
414
|
+
}
|
|
415
|
+
const installations = await this.listInstallations({ tenantId });
|
|
416
|
+
const installationByPluginId = new Map(installations.map((installation) => [installation.pluginId, installation]));
|
|
417
|
+
return components.filter((component) => {
|
|
418
|
+
const installation = installationByPluginId.get(component.pluginId);
|
|
419
|
+
if (installedOnly && !installation) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
if (enabledOnly && !installation?.enabled) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
return true;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
async fetchPluginAsset(pluginId, assetPath) {
|
|
429
|
+
await this.ready();
|
|
430
|
+
if (!this.fetcher) {
|
|
431
|
+
throw new PluginManagerError("No fetch implementation is available.");
|
|
432
|
+
}
|
|
433
|
+
this.require(pluginId);
|
|
434
|
+
const source = this.remotePluginAssets.get(pluginId);
|
|
435
|
+
if (!source) {
|
|
436
|
+
throw new PluginManagerError(`Plugin "${pluginId}" does not expose remote marketplace assets.`);
|
|
437
|
+
}
|
|
438
|
+
const normalizedAssetPath = normalizeSafeAssetPath(assetPath);
|
|
439
|
+
const githubPath = joinGitHubPath(source.rootPath, normalizedAssetPath);
|
|
440
|
+
const response = await fetchWithTimeout(this.fetcher, githubRawFileUrl(source, githubPath), {
|
|
441
|
+
headers: source.headers,
|
|
442
|
+
}, this.marketplaceRequestTimeoutMs);
|
|
443
|
+
if (!response.ok) {
|
|
444
|
+
return new Response(null, {
|
|
445
|
+
status: response.status,
|
|
446
|
+
statusText: response.statusText,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
const headers = new Headers();
|
|
450
|
+
const contentType = guessContentType(normalizedAssetPath, response.headers.get("content-type") ?? undefined);
|
|
451
|
+
if (contentType) {
|
|
452
|
+
headers.set("content-type", contentType);
|
|
453
|
+
}
|
|
454
|
+
const cacheControl = response.headers.get("cache-control");
|
|
455
|
+
if (cacheControl) {
|
|
456
|
+
headers.set("cache-control", cacheControl);
|
|
457
|
+
}
|
|
458
|
+
headers.set("x-content-type-options", "nosniff");
|
|
459
|
+
return new Response(response.body, {
|
|
460
|
+
status: response.status,
|
|
461
|
+
statusText: response.statusText,
|
|
462
|
+
headers,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
312
465
|
async getInjections(options = {}) {
|
|
313
466
|
await this.ready();
|
|
314
467
|
const tenantId = this.resolveTenantId(options);
|
|
@@ -428,21 +581,21 @@ export class PluginManager {
|
|
|
428
581
|
const pluginsById = new Map();
|
|
429
582
|
for (const candidate of candidates) {
|
|
430
583
|
try {
|
|
431
|
-
const response = await this.fetcher
|
|
584
|
+
const response = await fetchWithTimeout(this.fetcher, candidate.url, {
|
|
432
585
|
headers: candidate.headers,
|
|
433
|
-
});
|
|
586
|
+
}, this.marketplaceRequestTimeoutMs);
|
|
434
587
|
if (!response.ok) {
|
|
435
588
|
lastError = new Error(`${response.status} ${response.statusText}`);
|
|
436
589
|
continue;
|
|
437
590
|
}
|
|
438
591
|
successfulCandidates += 1;
|
|
439
592
|
if (candidate.parser === "github-tree") {
|
|
440
|
-
const
|
|
441
|
-
for (const pluginFileCandidate of
|
|
593
|
+
const pluginSchemaCandidates = await readGitHubPluginSchemaCandidates(response, candidate);
|
|
594
|
+
for (const pluginFileCandidate of pluginSchemaCandidates) {
|
|
442
595
|
try {
|
|
443
|
-
const pluginFileResponse = await this.fetcher
|
|
596
|
+
const pluginFileResponse = await fetchWithTimeout(this.fetcher, pluginFileCandidate.url, {
|
|
444
597
|
headers: pluginFileCandidate.headers,
|
|
445
|
-
});
|
|
598
|
+
}, this.marketplaceRequestTimeoutMs);
|
|
446
599
|
if (!pluginFileResponse.ok) {
|
|
447
600
|
lastError = new Error(`${pluginFileResponse.status} ${pluginFileResponse.statusText}`);
|
|
448
601
|
continue;
|
|
@@ -451,6 +604,7 @@ export class PluginManager {
|
|
|
451
604
|
const payload = await readMarketplacePayload(pluginFileResponse, pluginFileCandidate);
|
|
452
605
|
for (const plugin of extractMarketplacePlugins(payload)) {
|
|
453
606
|
const normalizedPlugin = applyMarketplaceDefaults(plugin, pluginFileCandidate);
|
|
607
|
+
this.rememberRemotePluginAssetSource(normalizedPlugin, pluginFileCandidate);
|
|
454
608
|
pluginsById.set(normalizedPlugin.id, normalizedPlugin);
|
|
455
609
|
}
|
|
456
610
|
}
|
|
@@ -463,6 +617,7 @@ export class PluginManager {
|
|
|
463
617
|
const payload = await readMarketplacePayload(response, candidate);
|
|
464
618
|
for (const plugin of extractMarketplacePlugins(payload)) {
|
|
465
619
|
const normalizedPlugin = applyMarketplaceDefaults(plugin, candidate);
|
|
620
|
+
this.rememberRemotePluginAssetSource(normalizedPlugin, candidate);
|
|
466
621
|
pluginsById.set(normalizedPlugin.id, normalizedPlugin);
|
|
467
622
|
}
|
|
468
623
|
}
|
|
@@ -479,6 +634,15 @@ export class PluginManager {
|
|
|
479
634
|
}
|
|
480
635
|
throw new PluginManagerError(`Marketplace "${marketplaceLabel(marketplaceSource)}" could not be loaded: ${String(lastError)}`);
|
|
481
636
|
}
|
|
637
|
+
rememberRemotePluginAssetSource(plugin, candidate) {
|
|
638
|
+
if (!candidate.github) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
this.remotePluginAssets.set(plugin.id, {
|
|
642
|
+
...candidate.github,
|
|
643
|
+
rootPath: pluginRootPath(candidate),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
482
646
|
shouldSyncMarketplaces() {
|
|
483
647
|
return Boolean(this.autoSyncMarketplaces &&
|
|
484
648
|
this.marketplaces.length &&
|
|
@@ -559,6 +723,27 @@ export class PluginManager {
|
|
|
559
723
|
function storageKey(pluginId, tenantId) {
|
|
560
724
|
return `${tenantId ?? "default"}:${pluginId}`;
|
|
561
725
|
}
|
|
726
|
+
function scopeKey(tenantId) {
|
|
727
|
+
return encodeKeyPart(tenantId ?? "default");
|
|
728
|
+
}
|
|
729
|
+
function encodeKeyPart(value) {
|
|
730
|
+
return encodeURIComponent(value).replace(/%/g, "~");
|
|
731
|
+
}
|
|
732
|
+
function parseRedisJson(value) {
|
|
733
|
+
if (typeof value === "string") {
|
|
734
|
+
return JSON.parse(value);
|
|
735
|
+
}
|
|
736
|
+
return value;
|
|
737
|
+
}
|
|
738
|
+
function normalizeRedisMembers(value) {
|
|
739
|
+
if (Array.isArray(value)) {
|
|
740
|
+
return value.filter((item) => typeof item === "string");
|
|
741
|
+
}
|
|
742
|
+
if (value instanceof Set) {
|
|
743
|
+
return [...value].filter((item) => typeof item === "string");
|
|
744
|
+
}
|
|
745
|
+
return [];
|
|
746
|
+
}
|
|
562
747
|
function toInstallationSummary(installation) {
|
|
563
748
|
return {
|
|
564
749
|
installed: true,
|
|
@@ -740,7 +925,10 @@ function githubMarketplaceCandidates(location, options) {
|
|
|
740
925
|
"plugins.json",
|
|
741
926
|
"index.json",
|
|
742
927
|
];
|
|
743
|
-
const
|
|
928
|
+
const schemaFileNames = normalizeSchemaFileNames(options.schemaFileNames);
|
|
929
|
+
const legacyPluginFileExtensions = normalizePluginFileExtensions(options.pluginFileExtensions);
|
|
930
|
+
const includeSchemaFiles = options.schemaFiles !== false;
|
|
931
|
+
const includeLegacyPluginFiles = options.pluginFiles === true;
|
|
744
932
|
const branchWasExplicit = Boolean(location.branch ?? options.branch);
|
|
745
933
|
const branches = branchWasExplicit
|
|
746
934
|
? [location.branch ?? options.branch ?? "main"]
|
|
@@ -756,7 +944,7 @@ function githubMarketplaceCandidates(location, options) {
|
|
|
756
944
|
: undefined);
|
|
757
945
|
const candidates = [];
|
|
758
946
|
for (const branch of branches) {
|
|
759
|
-
if (!location.exactPath &&
|
|
947
|
+
if (!location.exactPath && (includeSchemaFiles || includeLegacyPluginFiles)) {
|
|
760
948
|
candidates.push({
|
|
761
949
|
url: `${apiBaseUrl}/repos/${location.owner}/${location.repo}/git/trees/${encodeURIComponent(branch)}?recursive=${options.recursivePluginFiles === false ? "0" : "1"}`,
|
|
762
950
|
headers,
|
|
@@ -768,7 +956,9 @@ function githubMarketplaceCandidates(location, options) {
|
|
|
768
956
|
pathPrefix: normalizePathPrefix(location.path),
|
|
769
957
|
rawBaseUrl,
|
|
770
958
|
apiBaseUrl,
|
|
771
|
-
|
|
959
|
+
schemaFileNames,
|
|
960
|
+
legacyPluginFileExtensions,
|
|
961
|
+
includeLegacyPluginFiles,
|
|
772
962
|
avatarSize: options.avatarSize ?? 128,
|
|
773
963
|
headers,
|
|
774
964
|
},
|
|
@@ -776,10 +966,13 @@ function githubMarketplaceCandidates(location, options) {
|
|
|
776
966
|
}
|
|
777
967
|
for (const path of paths) {
|
|
778
968
|
const normalizedPath = path.replace(/^\/+/, "");
|
|
969
|
+
const sourceKind = githubSourceKindForPath(normalizedPath, schemaFileNames, legacyPluginFileExtensions, includeLegacyPluginFiles);
|
|
779
970
|
candidates.push({
|
|
780
971
|
url: `${rawBaseUrl}/${location.owner}/${location.repo}/${branch}/${normalizedPath}`,
|
|
781
972
|
headers,
|
|
782
973
|
parser: "json",
|
|
974
|
+
sourcePath: normalizedPath,
|
|
975
|
+
sourceKind,
|
|
783
976
|
github: {
|
|
784
977
|
owner: location.owner,
|
|
785
978
|
repo: location.repo,
|
|
@@ -787,7 +980,9 @@ function githubMarketplaceCandidates(location, options) {
|
|
|
787
980
|
pathPrefix: normalizePathPrefix(location.path),
|
|
788
981
|
rawBaseUrl,
|
|
789
982
|
apiBaseUrl,
|
|
790
|
-
|
|
983
|
+
schemaFileNames,
|
|
984
|
+
legacyPluginFileExtensions,
|
|
985
|
+
includeLegacyPluginFiles,
|
|
791
986
|
avatarSize: options.avatarSize ?? 128,
|
|
792
987
|
headers,
|
|
793
988
|
},
|
|
@@ -796,6 +991,8 @@ function githubMarketplaceCandidates(location, options) {
|
|
|
796
991
|
url: `${apiBaseUrl}/repos/${location.owner}/${location.repo}/contents/${encodeUrlPath(normalizedPath)}?ref=${encodeURIComponent(branch)}`,
|
|
797
992
|
headers,
|
|
798
993
|
parser: "github-content",
|
|
994
|
+
sourcePath: normalizedPath,
|
|
995
|
+
sourceKind,
|
|
799
996
|
github: {
|
|
800
997
|
owner: location.owner,
|
|
801
998
|
repo: location.repo,
|
|
@@ -803,7 +1000,9 @@ function githubMarketplaceCandidates(location, options) {
|
|
|
803
1000
|
pathPrefix: normalizePathPrefix(location.path),
|
|
804
1001
|
rawBaseUrl,
|
|
805
1002
|
apiBaseUrl,
|
|
806
|
-
|
|
1003
|
+
schemaFileNames,
|
|
1004
|
+
legacyPluginFileExtensions,
|
|
1005
|
+
includeLegacyPluginFiles,
|
|
807
1006
|
avatarSize: options.avatarSize ?? 128,
|
|
808
1007
|
headers,
|
|
809
1008
|
},
|
|
@@ -878,7 +1077,7 @@ function extractMarketplacePlugins(payload) {
|
|
|
878
1077
|
? [payload.plugin, ...(payload.plugins ?? payload.marketplace?.plugins ?? [])]
|
|
879
1078
|
: payload.plugins ?? payload.marketplace?.plugins ?? [];
|
|
880
1079
|
}
|
|
881
|
-
async function
|
|
1080
|
+
async function readGitHubPluginSchemaCandidates(response, candidate) {
|
|
882
1081
|
if (!candidate.github) {
|
|
883
1082
|
return [];
|
|
884
1083
|
}
|
|
@@ -886,20 +1085,25 @@ async function readGitHubPluginFileCandidates(response, candidate) {
|
|
|
886
1085
|
const payload = (await response.json());
|
|
887
1086
|
return (payload.tree ?? [])
|
|
888
1087
|
.filter((item) => Boolean(item.path && item.type === "blob"))
|
|
889
|
-
.filter((item) =>
|
|
1088
|
+
.filter((item) => isPluginSchemaPath(item.path, discovery))
|
|
890
1089
|
.flatMap((item) => {
|
|
891
1090
|
const encodedPath = encodeUrlPath(item.path);
|
|
1091
|
+
const sourceKind = githubSourceKindForPath(item.path, discovery.schemaFileNames, discovery.legacyPluginFileExtensions, discovery.includeLegacyPluginFiles);
|
|
892
1092
|
return [
|
|
893
1093
|
{
|
|
894
1094
|
url: `${discovery.rawBaseUrl}/${discovery.owner}/${discovery.repo}/${discovery.branch}/${encodedPath}`,
|
|
895
1095
|
headers: discovery.headers,
|
|
896
1096
|
parser: "json",
|
|
1097
|
+
sourcePath: item.path,
|
|
1098
|
+
sourceKind,
|
|
897
1099
|
github: discovery,
|
|
898
1100
|
},
|
|
899
1101
|
{
|
|
900
1102
|
url: `${discovery.apiBaseUrl}/repos/${discovery.owner}/${discovery.repo}/contents/${encodedPath}?ref=${encodeURIComponent(discovery.branch)}`,
|
|
901
1103
|
headers: discovery.headers,
|
|
902
1104
|
parser: "github-content",
|
|
1105
|
+
sourcePath: item.path,
|
|
1106
|
+
sourceKind,
|
|
903
1107
|
github: discovery,
|
|
904
1108
|
},
|
|
905
1109
|
];
|
|
@@ -921,23 +1125,26 @@ function applyMarketplaceDefaults(plugin, candidate) {
|
|
|
921
1125
|
}
|
|
922
1126
|
return plugin;
|
|
923
1127
|
}
|
|
1128
|
+
const normalizedPlugin = normalizeGitHubPluginReferences(plugin, candidate);
|
|
924
1129
|
return {
|
|
925
|
-
...
|
|
1130
|
+
...normalizedPlugin,
|
|
926
1131
|
provider: github.owner,
|
|
927
1132
|
iconUrl: githubAvatarUrl(github.owner, github.avatarSize),
|
|
928
|
-
repositoryUrl:
|
|
1133
|
+
repositoryUrl: normalizedPlugin.repositoryUrl ??
|
|
929
1134
|
`https://github.com/${github.owner}/${github.repo}`,
|
|
930
1135
|
meta: {
|
|
931
|
-
...
|
|
1136
|
+
...normalizedPlugin.meta,
|
|
932
1137
|
github: {
|
|
933
1138
|
owner: github.owner,
|
|
934
1139
|
repo: github.repo,
|
|
935
1140
|
branch: github.branch,
|
|
1141
|
+
rootPath: pluginRootPath(candidate),
|
|
1142
|
+
schemaPath: candidate.sourceKind === "schema" ? candidate.sourcePath : undefined,
|
|
936
1143
|
},
|
|
937
1144
|
},
|
|
938
1145
|
};
|
|
939
1146
|
}
|
|
940
|
-
function
|
|
1147
|
+
function isPluginSchemaPath(path, discovery) {
|
|
941
1148
|
const normalizedPath = path.replace(/^\/+/, "");
|
|
942
1149
|
const prefix = discovery.pathPrefix;
|
|
943
1150
|
if (prefix &&
|
|
@@ -945,7 +1152,205 @@ function isPluginFilePath(path, discovery) {
|
|
|
945
1152
|
!normalizedPath.startsWith(`${prefix}/`)) {
|
|
946
1153
|
return false;
|
|
947
1154
|
}
|
|
948
|
-
|
|
1155
|
+
const lowerPath = normalizedPath.toLocaleLowerCase();
|
|
1156
|
+
const fileName = lowerPath.split("/").pop() ?? lowerPath;
|
|
1157
|
+
if (discovery.schemaFileNames.includes(fileName)) {
|
|
1158
|
+
return true;
|
|
1159
|
+
}
|
|
1160
|
+
return (discovery.includeLegacyPluginFiles &&
|
|
1161
|
+
discovery.legacyPluginFileExtensions.some((extension) => lowerPath.endsWith(extension)));
|
|
1162
|
+
}
|
|
1163
|
+
function normalizeGitHubPluginReferences(plugin, candidate) {
|
|
1164
|
+
const github = candidate.github;
|
|
1165
|
+
if (!github) {
|
|
1166
|
+
return plugin;
|
|
1167
|
+
}
|
|
1168
|
+
const rootPath = pluginRootPath(candidate);
|
|
1169
|
+
const resolve = (value) => value ? resolveGitHubReferenceUrl(github, rootPath, value) : value;
|
|
1170
|
+
return {
|
|
1171
|
+
...plugin,
|
|
1172
|
+
docsUrl: resolve(plugin.docsUrl),
|
|
1173
|
+
websiteUrl: resolve(plugin.websiteUrl),
|
|
1174
|
+
frontend: normalizeFrontendManifest(plugin.frontend, github, rootPath),
|
|
1175
|
+
injections: plugin.injections?.map((injection) => ({
|
|
1176
|
+
...injection,
|
|
1177
|
+
src: resolve(injection.src),
|
|
1178
|
+
attributes: normalizeUrlAttributes(injection.attributes, resolve),
|
|
1179
|
+
})),
|
|
1180
|
+
assets: plugin.assets?.map((asset) => normalizePluginAsset(asset, resolve)),
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
function normalizeFrontendManifest(frontend, github, rootPath) {
|
|
1184
|
+
if (!frontend) {
|
|
1185
|
+
return frontend;
|
|
1186
|
+
}
|
|
1187
|
+
return {
|
|
1188
|
+
...frontend,
|
|
1189
|
+
registry: frontend.registry
|
|
1190
|
+
? Object.fromEntries(Object.entries(frontend.registry).map(([key, component]) => [
|
|
1191
|
+
key,
|
|
1192
|
+
normalizeComponentReference(component, github, rootPath),
|
|
1193
|
+
]))
|
|
1194
|
+
: frontend.registry,
|
|
1195
|
+
components: frontend.components?.map((component) => ({
|
|
1196
|
+
...component,
|
|
1197
|
+
component: normalizeComponentReference(component.component, github, rootPath),
|
|
1198
|
+
})),
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function normalizeComponentReference(reference, github, rootPath) {
|
|
1202
|
+
if (reference.type !== "remote") {
|
|
1203
|
+
return reference;
|
|
1204
|
+
}
|
|
1205
|
+
const referencePath = reference.path ??
|
|
1206
|
+
(reference.url && isRelativeReference(reference.url)
|
|
1207
|
+
? reference.url
|
|
1208
|
+
: undefined);
|
|
1209
|
+
const urlSource = reference.url ?? referencePath;
|
|
1210
|
+
return {
|
|
1211
|
+
...reference,
|
|
1212
|
+
path: referencePath ? normalizeClientAssetPath(referencePath) : reference.path,
|
|
1213
|
+
url: urlSource
|
|
1214
|
+
? resolveGitHubReferenceUrl(github, rootPath, urlSource)
|
|
1215
|
+
: reference.url,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
function normalizePluginAsset(asset, resolve) {
|
|
1219
|
+
return {
|
|
1220
|
+
...asset,
|
|
1221
|
+
url: resolve(asset.url) ?? asset.url,
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
function normalizeUrlAttributes(attributes, resolve) {
|
|
1225
|
+
if (!attributes) {
|
|
1226
|
+
return attributes;
|
|
1227
|
+
}
|
|
1228
|
+
const urlAttributes = new Set(["href", "src", "poster"]);
|
|
1229
|
+
return Object.fromEntries(Object.entries(attributes).map(([key, value]) => [
|
|
1230
|
+
key,
|
|
1231
|
+
typeof value === "string" && urlAttributes.has(key.toLocaleLowerCase())
|
|
1232
|
+
? resolve(value)
|
|
1233
|
+
: value,
|
|
1234
|
+
]));
|
|
1235
|
+
}
|
|
1236
|
+
function pluginRootPath(candidate) {
|
|
1237
|
+
if (!candidate.sourcePath) {
|
|
1238
|
+
return candidate.github?.pathPrefix;
|
|
1239
|
+
}
|
|
1240
|
+
return dirname(candidate.sourcePath) ?? candidate.github?.pathPrefix;
|
|
1241
|
+
}
|
|
1242
|
+
function githubSourceKindForPath(path, schemaFileNames, legacyPluginFileExtensions, includeLegacyPluginFiles) {
|
|
1243
|
+
const lowerPath = path.replace(/^\/+/, "").toLocaleLowerCase();
|
|
1244
|
+
const fileName = lowerPath.split("/").pop() ?? lowerPath;
|
|
1245
|
+
if (schemaFileNames.includes(fileName)) {
|
|
1246
|
+
return "schema";
|
|
1247
|
+
}
|
|
1248
|
+
if (includeLegacyPluginFiles &&
|
|
1249
|
+
legacyPluginFileExtensions.some((extension) => lowerPath.endsWith(extension))) {
|
|
1250
|
+
return "legacy-plugin";
|
|
1251
|
+
}
|
|
1252
|
+
return "marketplace";
|
|
1253
|
+
}
|
|
1254
|
+
function resolveGitHubReferenceUrl(github, rootPath, value) {
|
|
1255
|
+
if (!isRelativeReference(value)) {
|
|
1256
|
+
return value;
|
|
1257
|
+
}
|
|
1258
|
+
const { path, suffix } = splitReferenceSuffix(value);
|
|
1259
|
+
const basePath = path.startsWith("/") ? undefined : rootPath;
|
|
1260
|
+
const githubPath = joinGitHubPath(basePath, path);
|
|
1261
|
+
return `${githubRawFileUrl(github, githubPath)}${suffix}`;
|
|
1262
|
+
}
|
|
1263
|
+
function githubRawFileUrl(github, path) {
|
|
1264
|
+
return `${github.rawBaseUrl}/${github.owner}/${github.repo}/${github.branch}/${encodeUrlPath(path)}`;
|
|
1265
|
+
}
|
|
1266
|
+
function splitReferenceSuffix(value) {
|
|
1267
|
+
const index = value.search(/[?#]/);
|
|
1268
|
+
if (index === -1) {
|
|
1269
|
+
return { path: value, suffix: "" };
|
|
1270
|
+
}
|
|
1271
|
+
return {
|
|
1272
|
+
path: value.slice(0, index),
|
|
1273
|
+
suffix: value.slice(index),
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
function isRelativeReference(value) {
|
|
1277
|
+
return (!/^[a-z][a-z0-9+.-]*:/i.test(value) &&
|
|
1278
|
+
!value.startsWith("//") &&
|
|
1279
|
+
!value.startsWith("#") &&
|
|
1280
|
+
!value.startsWith("{{"));
|
|
1281
|
+
}
|
|
1282
|
+
function normalizeClientAssetPath(path) {
|
|
1283
|
+
if (!isRelativeReference(path) || path.startsWith("/")) {
|
|
1284
|
+
return undefined;
|
|
1285
|
+
}
|
|
1286
|
+
const { path: withoutSuffix } = splitReferenceSuffix(path);
|
|
1287
|
+
if (withoutSuffix.split("/").some((part) => part === "..")) {
|
|
1288
|
+
return undefined;
|
|
1289
|
+
}
|
|
1290
|
+
return normalizeGitHubPath(withoutSuffix);
|
|
1291
|
+
}
|
|
1292
|
+
function normalizeSafeAssetPath(path) {
|
|
1293
|
+
const decoded = safeDecodeURIComponent(path).replace(/^\/+/, "");
|
|
1294
|
+
const parts = decoded.split("/").filter(Boolean);
|
|
1295
|
+
if (!parts.length) {
|
|
1296
|
+
throw new PluginManagerError("Plugin asset path is required.");
|
|
1297
|
+
}
|
|
1298
|
+
if (parts.some((part) => part === "." || part === "..")) {
|
|
1299
|
+
throw new PluginManagerError("Plugin asset path cannot leave the plugin folder.");
|
|
1300
|
+
}
|
|
1301
|
+
return parts.join("/");
|
|
1302
|
+
}
|
|
1303
|
+
function joinGitHubPath(basePath, path) {
|
|
1304
|
+
return normalizeGitHubPath([basePath, path.replace(/^\/+/, "")].filter(Boolean).join("/"));
|
|
1305
|
+
}
|
|
1306
|
+
function normalizeGitHubPath(path) {
|
|
1307
|
+
const parts = [];
|
|
1308
|
+
for (const part of path.replace(/^\/+/, "").split("/")) {
|
|
1309
|
+
if (!part || part === ".") {
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (part === "..") {
|
|
1313
|
+
parts.pop();
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
parts.push(part);
|
|
1317
|
+
}
|
|
1318
|
+
return parts.join("/");
|
|
1319
|
+
}
|
|
1320
|
+
function dirname(path) {
|
|
1321
|
+
const parts = normalizeGitHubPath(path).split("/");
|
|
1322
|
+
parts.pop();
|
|
1323
|
+
return parts.length ? parts.join("/") : undefined;
|
|
1324
|
+
}
|
|
1325
|
+
function safeDecodeURIComponent(value) {
|
|
1326
|
+
try {
|
|
1327
|
+
return decodeURIComponent(value);
|
|
1328
|
+
}
|
|
1329
|
+
catch {
|
|
1330
|
+
return value;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function guessContentType(path, upstream) {
|
|
1334
|
+
const extension = path.split(".").pop()?.toLocaleLowerCase();
|
|
1335
|
+
const knownTypes = {
|
|
1336
|
+
css: "text/css; charset=utf-8",
|
|
1337
|
+
gif: "image/gif",
|
|
1338
|
+
html: "text/html; charset=utf-8",
|
|
1339
|
+
ico: "image/x-icon",
|
|
1340
|
+
jpeg: "image/jpeg",
|
|
1341
|
+
jpg: "image/jpeg",
|
|
1342
|
+
js: "application/javascript; charset=utf-8",
|
|
1343
|
+
json: "application/json; charset=utf-8",
|
|
1344
|
+
jsx: "application/javascript; charset=utf-8",
|
|
1345
|
+
mjs: "application/javascript; charset=utf-8",
|
|
1346
|
+
png: "image/png",
|
|
1347
|
+
svg: "image/svg+xml",
|
|
1348
|
+
txt: "text/plain; charset=utf-8",
|
|
1349
|
+
webp: "image/webp",
|
|
1350
|
+
woff: "font/woff",
|
|
1351
|
+
woff2: "font/woff2",
|
|
1352
|
+
};
|
|
1353
|
+
return (extension && knownTypes[extension]) || upstream;
|
|
949
1354
|
}
|
|
950
1355
|
function marketplaceLabel(source) {
|
|
951
1356
|
if (typeof source === "string") {
|
|
@@ -972,6 +1377,33 @@ function mergeHeaders(...headersList) {
|
|
|
972
1377
|
});
|
|
973
1378
|
return hasHeaders ? headers : undefined;
|
|
974
1379
|
}
|
|
1380
|
+
async function fetchWithTimeout(fetcher, input, init, timeoutMs) {
|
|
1381
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
1382
|
+
return fetcher(input, init);
|
|
1383
|
+
}
|
|
1384
|
+
const controller = new AbortController();
|
|
1385
|
+
let timeout;
|
|
1386
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
1387
|
+
timeout = setTimeout(() => {
|
|
1388
|
+
controller.abort();
|
|
1389
|
+
reject(new PluginManagerError(`Marketplace request timed out after ${timeoutMs}ms.`));
|
|
1390
|
+
}, timeoutMs);
|
|
1391
|
+
});
|
|
1392
|
+
try {
|
|
1393
|
+
return await Promise.race([
|
|
1394
|
+
fetcher(input, {
|
|
1395
|
+
...init,
|
|
1396
|
+
signal: controller.signal,
|
|
1397
|
+
}),
|
|
1398
|
+
timeoutPromise,
|
|
1399
|
+
]);
|
|
1400
|
+
}
|
|
1401
|
+
finally {
|
|
1402
|
+
if (timeout) {
|
|
1403
|
+
clearTimeout(timeout);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
975
1407
|
function decodeBase64(value) {
|
|
976
1408
|
const normalized = value.replace(/\s/g, "");
|
|
977
1409
|
const buffer = globalThis.Buffer;
|
|
@@ -992,6 +1424,9 @@ function normalizePathPrefix(path) {
|
|
|
992
1424
|
const normalized = path?.replace(/^\/+|\/+$/g, "");
|
|
993
1425
|
return normalized || undefined;
|
|
994
1426
|
}
|
|
1427
|
+
function normalizeSchemaFileNames(fileNames) {
|
|
1428
|
+
return (fileNames?.length ? fileNames : ["schema.json"]).map((fileName) => fileName.replace(/^\/+/, "").toLocaleLowerCase());
|
|
1429
|
+
}
|
|
995
1430
|
function normalizePluginFileExtensions(extensions) {
|
|
996
1431
|
return (extensions?.length ? extensions : [".plugin"]).map((extension) => extension.startsWith(".")
|
|
997
1432
|
? extension.toLocaleLowerCase()
|