@voilabs/plugins 0.0.1-beta.0 → 0.0.1-beta.1

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/dist/manager.js CHANGED
@@ -63,6 +63,7 @@ export class PluginManager {
63
63
  autoSyncMarketplaces;
64
64
  marketplaceRefreshIntervalMs;
65
65
  github;
66
+ remotePluginAssets = new Map();
66
67
  constructor(options = {}) {
67
68
  this.marketplaces = options.marketplaces ?? [];
68
69
  this.redisUrl = options.redisUrl;
@@ -306,9 +307,70 @@ export class PluginManager {
306
307
  .map((component) => ({
307
308
  ...component,
308
309
  pluginId: plugin.id,
310
+ pluginName: plugin.name,
311
+ pluginVersion: plugin.version,
309
312
  }));
310
313
  });
311
314
  }
315
+ async listFrontendComponents(options = {}) {
316
+ await this.ready();
317
+ const installedOnly = options.installedOnly ?? true;
318
+ const enabledOnly = options.enabledOnly ?? true;
319
+ const tenantId = this.resolveTenantId(options);
320
+ const components = this.getFrontendComponents(options.slot).filter((component) => !options.pluginId || component.pluginId === options.pluginId);
321
+ if (!installedOnly && !enabledOnly) {
322
+ return components;
323
+ }
324
+ const installations = await this.listInstallations({ tenantId });
325
+ const installationByPluginId = new Map(installations.map((installation) => [installation.pluginId, installation]));
326
+ return components.filter((component) => {
327
+ const installation = installationByPluginId.get(component.pluginId);
328
+ if (installedOnly && !installation) {
329
+ return false;
330
+ }
331
+ if (enabledOnly && !installation?.enabled) {
332
+ return false;
333
+ }
334
+ return true;
335
+ });
336
+ }
337
+ async fetchPluginAsset(pluginId, assetPath) {
338
+ await this.ready();
339
+ if (!this.fetcher) {
340
+ throw new PluginManagerError("No fetch implementation is available.");
341
+ }
342
+ this.require(pluginId);
343
+ const source = this.remotePluginAssets.get(pluginId);
344
+ if (!source) {
345
+ throw new PluginManagerError(`Plugin "${pluginId}" does not expose remote marketplace assets.`);
346
+ }
347
+ const normalizedAssetPath = normalizeSafeAssetPath(assetPath);
348
+ const githubPath = joinGitHubPath(source.rootPath, normalizedAssetPath);
349
+ const response = await this.fetcher(githubRawFileUrl(source, githubPath), {
350
+ headers: source.headers,
351
+ });
352
+ if (!response.ok) {
353
+ return new Response(null, {
354
+ status: response.status,
355
+ statusText: response.statusText,
356
+ });
357
+ }
358
+ const headers = new Headers();
359
+ const contentType = guessContentType(normalizedAssetPath, response.headers.get("content-type") ?? undefined);
360
+ if (contentType) {
361
+ headers.set("content-type", contentType);
362
+ }
363
+ const cacheControl = response.headers.get("cache-control");
364
+ if (cacheControl) {
365
+ headers.set("cache-control", cacheControl);
366
+ }
367
+ headers.set("x-content-type-options", "nosniff");
368
+ return new Response(response.body, {
369
+ status: response.status,
370
+ statusText: response.statusText,
371
+ headers,
372
+ });
373
+ }
312
374
  async getInjections(options = {}) {
313
375
  await this.ready();
314
376
  const tenantId = this.resolveTenantId(options);
@@ -437,8 +499,8 @@ export class PluginManager {
437
499
  }
438
500
  successfulCandidates += 1;
439
501
  if (candidate.parser === "github-tree") {
440
- const pluginFileCandidates = await readGitHubPluginFileCandidates(response, candidate);
441
- for (const pluginFileCandidate of pluginFileCandidates) {
502
+ const pluginSchemaCandidates = await readGitHubPluginSchemaCandidates(response, candidate);
503
+ for (const pluginFileCandidate of pluginSchemaCandidates) {
442
504
  try {
443
505
  const pluginFileResponse = await this.fetcher(pluginFileCandidate.url, {
444
506
  headers: pluginFileCandidate.headers,
@@ -451,6 +513,7 @@ export class PluginManager {
451
513
  const payload = await readMarketplacePayload(pluginFileResponse, pluginFileCandidate);
452
514
  for (const plugin of extractMarketplacePlugins(payload)) {
453
515
  const normalizedPlugin = applyMarketplaceDefaults(plugin, pluginFileCandidate);
516
+ this.rememberRemotePluginAssetSource(normalizedPlugin, pluginFileCandidate);
454
517
  pluginsById.set(normalizedPlugin.id, normalizedPlugin);
455
518
  }
456
519
  }
@@ -463,6 +526,7 @@ export class PluginManager {
463
526
  const payload = await readMarketplacePayload(response, candidate);
464
527
  for (const plugin of extractMarketplacePlugins(payload)) {
465
528
  const normalizedPlugin = applyMarketplaceDefaults(plugin, candidate);
529
+ this.rememberRemotePluginAssetSource(normalizedPlugin, candidate);
466
530
  pluginsById.set(normalizedPlugin.id, normalizedPlugin);
467
531
  }
468
532
  }
@@ -479,6 +543,15 @@ export class PluginManager {
479
543
  }
480
544
  throw new PluginManagerError(`Marketplace "${marketplaceLabel(marketplaceSource)}" could not be loaded: ${String(lastError)}`);
481
545
  }
546
+ rememberRemotePluginAssetSource(plugin, candidate) {
547
+ if (!candidate.github) {
548
+ return;
549
+ }
550
+ this.remotePluginAssets.set(plugin.id, {
551
+ ...candidate.github,
552
+ rootPath: pluginRootPath(candidate),
553
+ });
554
+ }
482
555
  shouldSyncMarketplaces() {
483
556
  return Boolean(this.autoSyncMarketplaces &&
484
557
  this.marketplaces.length &&
@@ -740,7 +813,10 @@ function githubMarketplaceCandidates(location, options) {
740
813
  "plugins.json",
741
814
  "index.json",
742
815
  ];
743
- const pluginFileExtensions = normalizePluginFileExtensions(options.pluginFileExtensions);
816
+ const schemaFileNames = normalizeSchemaFileNames(options.schemaFileNames);
817
+ const legacyPluginFileExtensions = normalizePluginFileExtensions(options.pluginFileExtensions);
818
+ const includeSchemaFiles = options.schemaFiles !== false;
819
+ const includeLegacyPluginFiles = options.pluginFiles === true;
744
820
  const branchWasExplicit = Boolean(location.branch ?? options.branch);
745
821
  const branches = branchWasExplicit
746
822
  ? [location.branch ?? options.branch ?? "main"]
@@ -756,7 +832,7 @@ function githubMarketplaceCandidates(location, options) {
756
832
  : undefined);
757
833
  const candidates = [];
758
834
  for (const branch of branches) {
759
- if (!location.exactPath && options.pluginFiles !== false) {
835
+ if (!location.exactPath && (includeSchemaFiles || includeLegacyPluginFiles)) {
760
836
  candidates.push({
761
837
  url: `${apiBaseUrl}/repos/${location.owner}/${location.repo}/git/trees/${encodeURIComponent(branch)}?recursive=${options.recursivePluginFiles === false ? "0" : "1"}`,
762
838
  headers,
@@ -768,7 +844,9 @@ function githubMarketplaceCandidates(location, options) {
768
844
  pathPrefix: normalizePathPrefix(location.path),
769
845
  rawBaseUrl,
770
846
  apiBaseUrl,
771
- extensions: pluginFileExtensions,
847
+ schemaFileNames,
848
+ legacyPluginFileExtensions,
849
+ includeLegacyPluginFiles,
772
850
  avatarSize: options.avatarSize ?? 128,
773
851
  headers,
774
852
  },
@@ -776,10 +854,13 @@ function githubMarketplaceCandidates(location, options) {
776
854
  }
777
855
  for (const path of paths) {
778
856
  const normalizedPath = path.replace(/^\/+/, "");
857
+ const sourceKind = githubSourceKindForPath(normalizedPath, schemaFileNames, legacyPluginFileExtensions, includeLegacyPluginFiles);
779
858
  candidates.push({
780
859
  url: `${rawBaseUrl}/${location.owner}/${location.repo}/${branch}/${normalizedPath}`,
781
860
  headers,
782
861
  parser: "json",
862
+ sourcePath: normalizedPath,
863
+ sourceKind,
783
864
  github: {
784
865
  owner: location.owner,
785
866
  repo: location.repo,
@@ -787,7 +868,9 @@ function githubMarketplaceCandidates(location, options) {
787
868
  pathPrefix: normalizePathPrefix(location.path),
788
869
  rawBaseUrl,
789
870
  apiBaseUrl,
790
- extensions: pluginFileExtensions,
871
+ schemaFileNames,
872
+ legacyPluginFileExtensions,
873
+ includeLegacyPluginFiles,
791
874
  avatarSize: options.avatarSize ?? 128,
792
875
  headers,
793
876
  },
@@ -796,6 +879,8 @@ function githubMarketplaceCandidates(location, options) {
796
879
  url: `${apiBaseUrl}/repos/${location.owner}/${location.repo}/contents/${encodeUrlPath(normalizedPath)}?ref=${encodeURIComponent(branch)}`,
797
880
  headers,
798
881
  parser: "github-content",
882
+ sourcePath: normalizedPath,
883
+ sourceKind,
799
884
  github: {
800
885
  owner: location.owner,
801
886
  repo: location.repo,
@@ -803,7 +888,9 @@ function githubMarketplaceCandidates(location, options) {
803
888
  pathPrefix: normalizePathPrefix(location.path),
804
889
  rawBaseUrl,
805
890
  apiBaseUrl,
806
- extensions: pluginFileExtensions,
891
+ schemaFileNames,
892
+ legacyPluginFileExtensions,
893
+ includeLegacyPluginFiles,
807
894
  avatarSize: options.avatarSize ?? 128,
808
895
  headers,
809
896
  },
@@ -878,7 +965,7 @@ function extractMarketplacePlugins(payload) {
878
965
  ? [payload.plugin, ...(payload.plugins ?? payload.marketplace?.plugins ?? [])]
879
966
  : payload.plugins ?? payload.marketplace?.plugins ?? [];
880
967
  }
881
- async function readGitHubPluginFileCandidates(response, candidate) {
968
+ async function readGitHubPluginSchemaCandidates(response, candidate) {
882
969
  if (!candidate.github) {
883
970
  return [];
884
971
  }
@@ -886,20 +973,25 @@ async function readGitHubPluginFileCandidates(response, candidate) {
886
973
  const payload = (await response.json());
887
974
  return (payload.tree ?? [])
888
975
  .filter((item) => Boolean(item.path && item.type === "blob"))
889
- .filter((item) => isPluginFilePath(item.path, discovery))
976
+ .filter((item) => isPluginSchemaPath(item.path, discovery))
890
977
  .flatMap((item) => {
891
978
  const encodedPath = encodeUrlPath(item.path);
979
+ const sourceKind = githubSourceKindForPath(item.path, discovery.schemaFileNames, discovery.legacyPluginFileExtensions, discovery.includeLegacyPluginFiles);
892
980
  return [
893
981
  {
894
982
  url: `${discovery.rawBaseUrl}/${discovery.owner}/${discovery.repo}/${discovery.branch}/${encodedPath}`,
895
983
  headers: discovery.headers,
896
984
  parser: "json",
985
+ sourcePath: item.path,
986
+ sourceKind,
897
987
  github: discovery,
898
988
  },
899
989
  {
900
990
  url: `${discovery.apiBaseUrl}/repos/${discovery.owner}/${discovery.repo}/contents/${encodedPath}?ref=${encodeURIComponent(discovery.branch)}`,
901
991
  headers: discovery.headers,
902
992
  parser: "github-content",
993
+ sourcePath: item.path,
994
+ sourceKind,
903
995
  github: discovery,
904
996
  },
905
997
  ];
@@ -921,23 +1013,26 @@ function applyMarketplaceDefaults(plugin, candidate) {
921
1013
  }
922
1014
  return plugin;
923
1015
  }
1016
+ const normalizedPlugin = normalizeGitHubPluginReferences(plugin, candidate);
924
1017
  return {
925
- ...plugin,
1018
+ ...normalizedPlugin,
926
1019
  provider: github.owner,
927
1020
  iconUrl: githubAvatarUrl(github.owner, github.avatarSize),
928
- repositoryUrl: plugin.repositoryUrl ??
1021
+ repositoryUrl: normalizedPlugin.repositoryUrl ??
929
1022
  `https://github.com/${github.owner}/${github.repo}`,
930
1023
  meta: {
931
- ...plugin.meta,
1024
+ ...normalizedPlugin.meta,
932
1025
  github: {
933
1026
  owner: github.owner,
934
1027
  repo: github.repo,
935
1028
  branch: github.branch,
1029
+ rootPath: pluginRootPath(candidate),
1030
+ schemaPath: candidate.sourceKind === "schema" ? candidate.sourcePath : undefined,
936
1031
  },
937
1032
  },
938
1033
  };
939
1034
  }
940
- function isPluginFilePath(path, discovery) {
1035
+ function isPluginSchemaPath(path, discovery) {
941
1036
  const normalizedPath = path.replace(/^\/+/, "");
942
1037
  const prefix = discovery.pathPrefix;
943
1038
  if (prefix &&
@@ -945,7 +1040,205 @@ function isPluginFilePath(path, discovery) {
945
1040
  !normalizedPath.startsWith(`${prefix}/`)) {
946
1041
  return false;
947
1042
  }
948
- return discovery.extensions.some((extension) => normalizedPath.toLocaleLowerCase().endsWith(extension));
1043
+ const lowerPath = normalizedPath.toLocaleLowerCase();
1044
+ const fileName = lowerPath.split("/").pop() ?? lowerPath;
1045
+ if (discovery.schemaFileNames.includes(fileName)) {
1046
+ return true;
1047
+ }
1048
+ return (discovery.includeLegacyPluginFiles &&
1049
+ discovery.legacyPluginFileExtensions.some((extension) => lowerPath.endsWith(extension)));
1050
+ }
1051
+ function normalizeGitHubPluginReferences(plugin, candidate) {
1052
+ const github = candidate.github;
1053
+ if (!github) {
1054
+ return plugin;
1055
+ }
1056
+ const rootPath = pluginRootPath(candidate);
1057
+ const resolve = (value) => value ? resolveGitHubReferenceUrl(github, rootPath, value) : value;
1058
+ return {
1059
+ ...plugin,
1060
+ docsUrl: resolve(plugin.docsUrl),
1061
+ websiteUrl: resolve(plugin.websiteUrl),
1062
+ frontend: normalizeFrontendManifest(plugin.frontend, github, rootPath),
1063
+ injections: plugin.injections?.map((injection) => ({
1064
+ ...injection,
1065
+ src: resolve(injection.src),
1066
+ attributes: normalizeUrlAttributes(injection.attributes, resolve),
1067
+ })),
1068
+ assets: plugin.assets?.map((asset) => normalizePluginAsset(asset, resolve)),
1069
+ };
1070
+ }
1071
+ function normalizeFrontendManifest(frontend, github, rootPath) {
1072
+ if (!frontend) {
1073
+ return frontend;
1074
+ }
1075
+ return {
1076
+ ...frontend,
1077
+ registry: frontend.registry
1078
+ ? Object.fromEntries(Object.entries(frontend.registry).map(([key, component]) => [
1079
+ key,
1080
+ normalizeComponentReference(component, github, rootPath),
1081
+ ]))
1082
+ : frontend.registry,
1083
+ components: frontend.components?.map((component) => ({
1084
+ ...component,
1085
+ component: normalizeComponentReference(component.component, github, rootPath),
1086
+ })),
1087
+ };
1088
+ }
1089
+ function normalizeComponentReference(reference, github, rootPath) {
1090
+ if (reference.type !== "remote") {
1091
+ return reference;
1092
+ }
1093
+ const referencePath = reference.path ??
1094
+ (reference.url && isRelativeReference(reference.url)
1095
+ ? reference.url
1096
+ : undefined);
1097
+ const urlSource = reference.url ?? referencePath;
1098
+ return {
1099
+ ...reference,
1100
+ path: referencePath ? normalizeClientAssetPath(referencePath) : reference.path,
1101
+ url: urlSource
1102
+ ? resolveGitHubReferenceUrl(github, rootPath, urlSource)
1103
+ : reference.url,
1104
+ };
1105
+ }
1106
+ function normalizePluginAsset(asset, resolve) {
1107
+ return {
1108
+ ...asset,
1109
+ url: resolve(asset.url) ?? asset.url,
1110
+ };
1111
+ }
1112
+ function normalizeUrlAttributes(attributes, resolve) {
1113
+ if (!attributes) {
1114
+ return attributes;
1115
+ }
1116
+ const urlAttributes = new Set(["href", "src", "poster"]);
1117
+ return Object.fromEntries(Object.entries(attributes).map(([key, value]) => [
1118
+ key,
1119
+ typeof value === "string" && urlAttributes.has(key.toLocaleLowerCase())
1120
+ ? resolve(value)
1121
+ : value,
1122
+ ]));
1123
+ }
1124
+ function pluginRootPath(candidate) {
1125
+ if (!candidate.sourcePath) {
1126
+ return candidate.github?.pathPrefix;
1127
+ }
1128
+ return dirname(candidate.sourcePath) ?? candidate.github?.pathPrefix;
1129
+ }
1130
+ function githubSourceKindForPath(path, schemaFileNames, legacyPluginFileExtensions, includeLegacyPluginFiles) {
1131
+ const lowerPath = path.replace(/^\/+/, "").toLocaleLowerCase();
1132
+ const fileName = lowerPath.split("/").pop() ?? lowerPath;
1133
+ if (schemaFileNames.includes(fileName)) {
1134
+ return "schema";
1135
+ }
1136
+ if (includeLegacyPluginFiles &&
1137
+ legacyPluginFileExtensions.some((extension) => lowerPath.endsWith(extension))) {
1138
+ return "legacy-plugin";
1139
+ }
1140
+ return "marketplace";
1141
+ }
1142
+ function resolveGitHubReferenceUrl(github, rootPath, value) {
1143
+ if (!isRelativeReference(value)) {
1144
+ return value;
1145
+ }
1146
+ const { path, suffix } = splitReferenceSuffix(value);
1147
+ const basePath = path.startsWith("/") ? undefined : rootPath;
1148
+ const githubPath = joinGitHubPath(basePath, path);
1149
+ return `${githubRawFileUrl(github, githubPath)}${suffix}`;
1150
+ }
1151
+ function githubRawFileUrl(github, path) {
1152
+ return `${github.rawBaseUrl}/${github.owner}/${github.repo}/${github.branch}/${encodeUrlPath(path)}`;
1153
+ }
1154
+ function splitReferenceSuffix(value) {
1155
+ const index = value.search(/[?#]/);
1156
+ if (index === -1) {
1157
+ return { path: value, suffix: "" };
1158
+ }
1159
+ return {
1160
+ path: value.slice(0, index),
1161
+ suffix: value.slice(index),
1162
+ };
1163
+ }
1164
+ function isRelativeReference(value) {
1165
+ return (!/^[a-z][a-z0-9+.-]*:/i.test(value) &&
1166
+ !value.startsWith("//") &&
1167
+ !value.startsWith("#") &&
1168
+ !value.startsWith("{{"));
1169
+ }
1170
+ function normalizeClientAssetPath(path) {
1171
+ if (!isRelativeReference(path) || path.startsWith("/")) {
1172
+ return undefined;
1173
+ }
1174
+ const { path: withoutSuffix } = splitReferenceSuffix(path);
1175
+ if (withoutSuffix.split("/").some((part) => part === "..")) {
1176
+ return undefined;
1177
+ }
1178
+ return normalizeGitHubPath(withoutSuffix);
1179
+ }
1180
+ function normalizeSafeAssetPath(path) {
1181
+ const decoded = safeDecodeURIComponent(path).replace(/^\/+/, "");
1182
+ const parts = decoded.split("/").filter(Boolean);
1183
+ if (!parts.length) {
1184
+ throw new PluginManagerError("Plugin asset path is required.");
1185
+ }
1186
+ if (parts.some((part) => part === "." || part === "..")) {
1187
+ throw new PluginManagerError("Plugin asset path cannot leave the plugin folder.");
1188
+ }
1189
+ return parts.join("/");
1190
+ }
1191
+ function joinGitHubPath(basePath, path) {
1192
+ return normalizeGitHubPath([basePath, path.replace(/^\/+/, "")].filter(Boolean).join("/"));
1193
+ }
1194
+ function normalizeGitHubPath(path) {
1195
+ const parts = [];
1196
+ for (const part of path.replace(/^\/+/, "").split("/")) {
1197
+ if (!part || part === ".") {
1198
+ continue;
1199
+ }
1200
+ if (part === "..") {
1201
+ parts.pop();
1202
+ continue;
1203
+ }
1204
+ parts.push(part);
1205
+ }
1206
+ return parts.join("/");
1207
+ }
1208
+ function dirname(path) {
1209
+ const parts = normalizeGitHubPath(path).split("/");
1210
+ parts.pop();
1211
+ return parts.length ? parts.join("/") : undefined;
1212
+ }
1213
+ function safeDecodeURIComponent(value) {
1214
+ try {
1215
+ return decodeURIComponent(value);
1216
+ }
1217
+ catch {
1218
+ return value;
1219
+ }
1220
+ }
1221
+ function guessContentType(path, upstream) {
1222
+ const extension = path.split(".").pop()?.toLocaleLowerCase();
1223
+ const knownTypes = {
1224
+ css: "text/css; charset=utf-8",
1225
+ gif: "image/gif",
1226
+ html: "text/html; charset=utf-8",
1227
+ ico: "image/x-icon",
1228
+ jpeg: "image/jpeg",
1229
+ jpg: "image/jpeg",
1230
+ js: "application/javascript; charset=utf-8",
1231
+ json: "application/json; charset=utf-8",
1232
+ jsx: "application/javascript; charset=utf-8",
1233
+ mjs: "application/javascript; charset=utf-8",
1234
+ png: "image/png",
1235
+ svg: "image/svg+xml",
1236
+ txt: "text/plain; charset=utf-8",
1237
+ webp: "image/webp",
1238
+ woff: "font/woff",
1239
+ woff2: "font/woff2",
1240
+ };
1241
+ return (extension && knownTypes[extension]) || upstream;
949
1242
  }
950
1243
  function marketplaceLabel(source) {
951
1244
  if (typeof source === "string") {
@@ -992,6 +1285,9 @@ function normalizePathPrefix(path) {
992
1285
  const normalized = path?.replace(/^\/+|\/+$/g, "");
993
1286
  return normalized || undefined;
994
1287
  }
1288
+ function normalizeSchemaFileNames(fileNames) {
1289
+ return (fileNames?.length ? fileNames : ["schema.json"]).map((fileName) => fileName.replace(/^\/+/, "").toLocaleLowerCase());
1290
+ }
995
1291
  function normalizePluginFileExtensions(extensions) {
996
1292
  return (extensions?.length ? extensions : [".plugin"]).map((extension) => extension.startsWith(".")
997
1293
  ? extension.toLocaleLowerCase()