@lvce-editor/extension-detail-view 7.10.0 → 7.13.0

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.
@@ -1,13 +1,13 @@
1
1
  const emptyObject = {};
2
- const RE_PLACEHOLDER = /\{(PH\d+)\}/g;
3
2
  const i18nString = (key, placeholders = emptyObject) => {
4
3
  if (placeholders === emptyObject) {
5
4
  return key;
6
5
  }
7
- const replacer = (match, rest) => {
8
- return placeholders[rest];
9
- };
10
- return key.replaceAll(RE_PLACEHOLDER, replacer);
6
+ let result = key;
7
+ for (const [placeholder, replacement] of Object.entries(placeholders)) {
8
+ result = result.split(`{${placeholder}}`).join(String(replacement));
9
+ }
10
+ return result;
11
11
  };
12
12
 
13
13
  const ActivationEvents$1 = 'Activation Events';
@@ -26,6 +26,7 @@ const Disable$1 = 'Disable';
26
26
  const Elements = 'Elements';
27
27
  const EmptyCommandsArray = 'The extension contributes an empty commands array.';
28
28
  const Enable$1 = 'Enable';
29
+ const ExtensionNotAvailable = 'The extension "{PH1}" is not available in this version of LVCE Editor.';
29
30
  const Features$2 = 'Features';
30
31
  const FileExtensions = 'File Extensions';
31
32
  const FileMatch = 'File Match';
@@ -37,7 +38,7 @@ const Installation = 'Installation';
37
38
  const InvalidLink = 'Invalid link';
38
39
  const Issues = 'Issues';
39
40
  const JsonValidation$1 = 'Json Validation';
40
- const Label = 'Label';
41
+ const Label$1 = 'Label';
41
42
  const LastReleased = 'Last Released';
42
43
  const LastUpdated = 'Last Updated';
43
44
  const License = 'License';
@@ -67,6 +68,8 @@ const Status = 'Status';
67
68
  const StringMustNotBeEmpty = 'String must not be empty';
68
69
  const Theme$1 = 'Theme';
69
70
  const Uninstall$1 = 'Uninstall';
71
+ const UnableToLoadExtension = 'Unable to load extension';
72
+ const UnableToLoadExtensionWithError = 'The extension details could not be loaded: {PH1}';
70
73
  const UnsupportedFeature = 'Unsupported Feature';
71
74
  const Version = 'Version';
72
75
  const WebViews$1 = 'WebViews';
@@ -114,7 +117,7 @@ const openInNewTab = () => {
114
117
  return i18nString(OpenInNewTab);
115
118
  };
116
119
  const label = () => {
117
- return i18nString(Label);
120
+ return i18nString(Label$1);
118
121
  };
119
122
  const openImageInNewTab$1 = () => {
120
123
  return i18nString(OpenImageInNewTab);
@@ -137,6 +140,11 @@ const setColorTheme$2 = () => {
137
140
  const enable = () => {
138
141
  return i18nString(Enable$1);
139
142
  };
143
+ const extensionNotAvailable = extensionId => {
144
+ return i18nString(ExtensionNotAvailable, {
145
+ PH1: extensionId
146
+ });
147
+ };
140
148
  const theme = () => {
141
149
  return i18nString(Theme$1);
142
150
  };
@@ -188,6 +196,14 @@ const elements = () => {
188
196
  const uninstall = () => {
189
197
  return i18nString(Uninstall$1);
190
198
  };
199
+ const unableToLoadExtension = () => {
200
+ return i18nString(UnableToLoadExtension);
201
+ };
202
+ const unableToLoadExtensionWithError = errorMessage => {
203
+ return i18nString(UnableToLoadExtensionWithError, {
204
+ PH1: errorMessage
205
+ });
206
+ };
191
207
  const scrollToTop = () => {
192
208
  return i18nString(ScrollToTop$1);
193
209
  };
@@ -302,9 +318,14 @@ const Panel = 'panel';
302
318
  const Tab = 'tab';
303
319
  const TabList = 'tablist';
304
320
 
321
+ const Audio = 0;
305
322
  const Button$1 = 1;
323
+ const Col = 2;
324
+ const ColGroup = 3;
306
325
  const Div = 4;
307
326
  const H1 = 5;
327
+ const Input = 6;
328
+ const Kbd = 7;
308
329
  const Span = 8;
309
330
  const Table$1 = 9;
310
331
  const TBody = 10;
@@ -313,20 +334,159 @@ const Text$1 = 12;
313
334
  const Th = 13;
314
335
  const THead = 14;
315
336
  const Tr = 15;
337
+ const I = 16;
316
338
  const Img = 17;
339
+ const Root = 0;
340
+ const Ins = 20;
341
+ const Del = 21;
317
342
  const H2 = 22;
343
+ const H3 = 23;
344
+ const H4 = 24;
345
+ const H5 = 25;
346
+ const H6 = 26;
347
+ const Article = 27;
318
348
  const Aside$1 = 28;
349
+ const Footer = 29;
350
+ const Header = 30;
351
+ const Nav = 40;
352
+ const Section = 41;
353
+ const Search = 42;
319
354
  const Dd = 43;
320
355
  const Dl = 44;
356
+ const Figcaption = 45;
357
+ const Figure = 46;
358
+ const Hr = 47;
321
359
  const Li = 48;
360
+ const Ol = 49;
322
361
  const P = 50;
323
362
  const Pre = 51;
324
363
  const A = 53;
364
+ const Abbr = 54;
365
+ const Br = 55;
366
+ const Cite = 56;
367
+ const Data = 57;
368
+ const Time = 58;
369
+ const Tfoot = 59;
325
370
  const Ul = 60;
371
+ const Video = 61;
372
+ const TextArea = 62;
373
+ const Select = 63;
374
+ const Option = 64;
326
375
  const Code$2 = 65;
376
+ const Label = 66;
327
377
  const Dt = 67;
378
+ const Iframe = 68;
379
+ const Main = 69;
380
+ const Strong = 70;
381
+ const Em = 71;
382
+ const Style = 72;
383
+ const Html = 73;
384
+ const Head = 74;
385
+ const Title = 75;
386
+ const Meta = 76;
387
+ const Canvas = 77;
388
+ const Form = 78;
389
+ const BlockQuote = 79;
390
+ const Quote = 80;
391
+ const Circle = 81;
392
+ const Defs = 82;
393
+ const Ellipse = 83;
394
+ const G = 84;
395
+ const Line = 85;
396
+ const Path = 86;
397
+ const Polygon = 87;
398
+ const Polyline = 88;
399
+ const Rect = 89;
400
+ const Svg = 90;
401
+ const Use = 91;
328
402
  const Reference = 100;
329
403
 
404
+ const VirtualDomElements = {
405
+ __proto__: null,
406
+ A,
407
+ Abbr,
408
+ Article,
409
+ Aside: Aside$1,
410
+ Audio,
411
+ BlockQuote,
412
+ Br,
413
+ Button: Button$1,
414
+ Canvas,
415
+ Circle,
416
+ Cite,
417
+ Code: Code$2,
418
+ Col,
419
+ ColGroup,
420
+ Data,
421
+ Dd,
422
+ Defs,
423
+ Del,
424
+ Div,
425
+ Dl,
426
+ Dt,
427
+ Ellipse,
428
+ Em,
429
+ Figcaption,
430
+ Figure,
431
+ Footer,
432
+ Form,
433
+ G,
434
+ H1,
435
+ H2,
436
+ H3,
437
+ H4,
438
+ H5,
439
+ H6,
440
+ Head,
441
+ Header,
442
+ Hr,
443
+ Html,
444
+ I,
445
+ Iframe,
446
+ Img,
447
+ Input,
448
+ Ins,
449
+ Kbd,
450
+ Label,
451
+ Li,
452
+ Line,
453
+ Main,
454
+ Meta,
455
+ Nav,
456
+ Ol,
457
+ Option,
458
+ P,
459
+ Path,
460
+ Polygon,
461
+ Polyline,
462
+ Pre,
463
+ Quote,
464
+ Rect,
465
+ Reference,
466
+ Root,
467
+ Search,
468
+ Section,
469
+ Select,
470
+ Span,
471
+ Strong,
472
+ Style,
473
+ Svg,
474
+ TBody,
475
+ THead,
476
+ Table: Table$1,
477
+ Td,
478
+ Text: Text$1,
479
+ TextArea,
480
+ Tfoot,
481
+ Th,
482
+ Time,
483
+ Title,
484
+ Tr,
485
+ Ul,
486
+ Use,
487
+ Video
488
+ };
489
+
330
490
  const ClientX = 'event.clientX';
331
491
  const ClientY = 'event.clientY';
332
492
  const TargetHref = 'event.target.href';
@@ -371,6 +531,8 @@ const text = data => {
371
531
  };
372
532
  };
373
533
 
534
+ new Set(Object.values(VirtualDomElements));
535
+
374
536
  const SetText = 1;
375
537
  const Replace = 2;
376
538
  const SetAttribute = 3;
@@ -642,6 +804,7 @@ const Categories = 'Categories';
642
804
  const Category = 'Category';
643
805
  const Changelog$1 = 'Changelog';
644
806
  const MaskIcon = 'MaskIcon';
807
+ const MaskIconWarning = 'MaskIconWarning';
645
808
  const ResourceIcon = 'ResourceIcon';
646
809
  const Code$1 = 'Code';
647
810
  const DefaultMarkdown = 'DefaultMarkdown';
@@ -651,6 +814,11 @@ const DefinitionListItemHeading = 'DefinitionListItemHeading';
651
814
  const DefinitionListItemValue = 'DefinitionListItemValue';
652
815
  const ExtensionDetail = 'ExtensionDetail';
653
816
  const ExtensionDetailDescription = 'ExtensionDetailDescription';
817
+ const ExtensionDetailError = 'ExtensionDetailError';
818
+ const ExtensionDetailErrorCard = 'ExtensionDetailErrorCard';
819
+ const ExtensionDetailErrorIcon = 'ExtensionDetailErrorIcon';
820
+ const ExtensionDetailErrorMessage = 'ExtensionDetailErrorMessage';
821
+ const ExtensionDetailErrorTitle = 'ExtensionDetailErrorTitle';
654
822
  const ExtensionDetailHeader = 'ExtensionDetailHeader';
655
823
  const ExtensionDetailHeaderActions = 'ExtensionDetailHeaderActions';
656
824
  const ExtensionDetailHeaderDetails = 'ExtensionDetailHeaderDetails';
@@ -1375,7 +1543,7 @@ class AssertionError extends Error {
1375
1543
  const Object$1 = 1;
1376
1544
  const Number$1 = 2;
1377
1545
  const Array$1 = 3;
1378
- const String = 4;
1546
+ const String$1 = 4;
1379
1547
  const Boolean$1 = 5;
1380
1548
  const Function = 6;
1381
1549
  const Null = 7;
@@ -1387,7 +1555,7 @@ const getType = value => {
1387
1555
  case 'function':
1388
1556
  return Function;
1389
1557
  case 'string':
1390
- return String;
1558
+ return String$1;
1391
1559
  case 'object':
1392
1560
  if (value === null) {
1393
1561
  return Null;
@@ -1410,7 +1578,7 @@ const number = value => {
1410
1578
  };
1411
1579
  const string = value => {
1412
1580
  const type = getType(value);
1413
- if (type !== String) {
1581
+ if (type !== String$1) {
1414
1582
  throw new AssertionError('expected value to be of type string');
1415
1583
  }
1416
1584
  };
@@ -1457,7 +1625,6 @@ const walkValue = (value, transferrables, isTransferrable) => {
1457
1625
  for (const property of Object.values(value)) {
1458
1626
  walkValue(property, transferrables, isTransferrable);
1459
1627
  }
1460
- return;
1461
1628
  }
1462
1629
  };
1463
1630
  const getTransferrables = value => {
@@ -1611,7 +1778,14 @@ class IpcError extends VError {
1611
1778
  const cause = new Error(message);
1612
1779
  // @ts-ignore
1613
1780
  cause.code = code;
1614
- cause.stack = stack;
1781
+ if (stack) {
1782
+ Object.defineProperty(cause, 'stack', {
1783
+ configurable: true,
1784
+ enumerable: false,
1785
+ value: stack,
1786
+ writable: true
1787
+ });
1788
+ }
1615
1789
  super(cause, betterMessage);
1616
1790
  } else {
1617
1791
  super(betterMessage);
@@ -2810,6 +2984,7 @@ const HandleTabFocus = 18;
2810
2984
  const HandleResourceLinkClick = 19;
2811
2985
  const HandleMouseEnterEnable = 20;
2812
2986
  const HandleMouseLeaveEnable = 21;
2987
+ const HandleHeaderContextMenu = 22;
2813
2988
 
2814
2989
  const ActivationEvents = 'ActivationEvents';
2815
2990
  const Changelog = 'Changelog';
@@ -2851,19 +3026,22 @@ const {
2851
3026
  set: set$3
2852
3027
  } = MarkdownWorker;
2853
3028
 
3029
+ const addScrollToTopVirtualDom = dom => {
3030
+ const [firstNode, ...rest] = dom;
3031
+ const extraDom = getScrollToTopVirtualDom();
3032
+ return [{
3033
+ ...firstNode,
3034
+ childCount: firstNode.childCount + 1,
3035
+ onClick: HandleReadmeClick,
3036
+ onScroll: HandleReadmeScroll,
3037
+ onSelectionChange: HandleSelectionChange
3038
+ }, ...extraDom, ...rest];
3039
+ };
2854
3040
  const getMarkdownVirtualDom = async (html, options) => {
2855
3041
  string(html);
2856
3042
  const dom = await getVirtualDom(html);
2857
3043
  if (options?.scrollToTopEnabled) {
2858
- const [firstNode, ...rest] = dom;
2859
- const extraDom = getScrollToTopVirtualDom();
2860
- return [{
2861
- ...firstNode,
2862
- childCount: firstNode.childCount + 1,
2863
- onClick: HandleReadmeClick,
2864
- onScroll: HandleReadmeScroll,
2865
- onSelectionChange: HandleSelectionChange
2866
- }, ...extraDom, ...rest];
3044
+ return addScrollToTopVirtualDom(dom);
2867
3045
  }
2868
3046
  return dom;
2869
3047
  };
@@ -3431,6 +3609,8 @@ const create = (uid, uri, x, y, width, height, platform, assetDir) => {
3431
3609
  disabled: false,
3432
3610
  displaySize: '',
3433
3611
  downloadCount: 'n/a',
3612
+ errorMessage: '',
3613
+ errorTitle: '',
3434
3614
  extension: {},
3435
3615
  extensionId: '',
3436
3616
  extensionUri: '',
@@ -3703,6 +3883,96 @@ const getMenus = () => {
3703
3883
  }];
3704
3884
  };
3705
3885
 
3886
+ let mock;
3887
+ const getPage = url => {
3888
+ return Number(new URL(url).searchParams.get('page')) || 1;
3889
+ };
3890
+ const createGeneratedReleases = (url, count) => {
3891
+ const page = getPage(url);
3892
+ const start = (page - 1) * 100;
3893
+ const end = Math.min(start + 100, count);
3894
+ return Array.from({
3895
+ length: Math.max(0, end - start)
3896
+ }, (_, index) => {
3897
+ const releaseNumber = count - start - index;
3898
+ return {
3899
+ body: `Release notes for version ${releaseNumber}`,
3900
+ html_url: `https://github.com/test-owner/test-repository/releases/tag/v${releaseNumber}`,
3901
+ name: `Version ${releaseNumber}`,
3902
+ published_at: '2026-01-01T00:00:00Z',
3903
+ tag_name: `v${releaseNumber}`
3904
+ };
3905
+ });
3906
+ };
3907
+ const createMockResponse = (url, value) => {
3908
+ const status = value.status ?? 200;
3909
+ const statusText = value.statusText || getStatusText(status);
3910
+ const headers = new Headers(value.headers);
3911
+ if (value.type === 'invalid-json') {
3912
+ headers.set('content-type', 'text/html');
3913
+ return new Response('<!doctype html><title>Unexpected response</title>', {
3914
+ headers,
3915
+ status,
3916
+ statusText
3917
+ });
3918
+ }
3919
+ const body = value.type === 'generated' ? createGeneratedReleases(url, value.releaseCount ?? 0) : value.body ?? [];
3920
+ headers.set('content-type', 'application/json');
3921
+ return Response.json(body, {
3922
+ headers,
3923
+ status,
3924
+ statusText
3925
+ });
3926
+ };
3927
+ const getStatusText = status => {
3928
+ switch (status) {
3929
+ case 400:
3930
+ return 'Bad Request';
3931
+ case 401:
3932
+ return 'Unauthorized';
3933
+ case 403:
3934
+ return 'Forbidden';
3935
+ case 404:
3936
+ return 'Not Found';
3937
+ case 429:
3938
+ return 'Too Many Requests';
3939
+ case 500:
3940
+ return 'Internal Server Error';
3941
+ case 503:
3942
+ return 'Service Unavailable';
3943
+ default:
3944
+ return '';
3945
+ }
3946
+ };
3947
+ const request = url => {
3948
+ if (!mock) {
3949
+ return fetch(url, {
3950
+ headers: {
3951
+ Accept: 'application/vnd.github+json',
3952
+ 'X-GitHub-Api-Version': '2022-11-28'
3953
+ }
3954
+ });
3955
+ }
3956
+ if (mock.type === 'network-error') {
3957
+ return Promise.reject(new TypeError(mock.message || 'Failed to fetch'));
3958
+ }
3959
+ return Promise.resolve(createMockResponse(url, mock));
3960
+ };
3961
+ const mockGithubApi = value => {
3962
+ mock = value;
3963
+ };
3964
+ const handleMockGithubApi = (state, value) => {
3965
+ mockGithubApi(value);
3966
+ return state;
3967
+ };
3968
+ const resetGithubApiMock = () => {
3969
+ mock = undefined;
3970
+ };
3971
+ const handleResetGithubApiMock = state => {
3972
+ resetGithubApiMock();
3973
+ return state;
3974
+ };
3975
+
3706
3976
  const show2 = async (uid, menuId, x, y, args) => {
3707
3977
  await showContextMenu2(uid, menuId, x, y, args);
3708
3978
  };
@@ -3900,6 +4170,15 @@ const handleClickFeatures = async (state, name) => {
3900
4170
  };
3901
4171
 
3902
4172
  const handleClickScrollToTop = state => {
4173
+ if (state.selectedTab === Changelog) {
4174
+ if (state.changelogScrollTop === 0) {
4175
+ return state;
4176
+ }
4177
+ return {
4178
+ ...state,
4179
+ changelogScrollTop: 0
4180
+ };
4181
+ }
3903
4182
  const {
3904
4183
  readmeScrollTop
3905
4184
  } = state;
@@ -4033,11 +4312,69 @@ const getCurrentColorTheme = async () => {
4033
4312
  return '';
4034
4313
  };
4035
4314
 
4315
+ const getErrorMessage$1 = error => {
4316
+ if (error instanceof Error && error.message) {
4317
+ return error.message;
4318
+ }
4319
+ if (typeof error === 'string' && error) {
4320
+ return error;
4321
+ }
4322
+ return 'Unknown error';
4323
+ };
4324
+
4036
4325
  const getExtensionIdFromUri = uri => {
4037
4326
  const id = uri.slice('extension-detail://'.length);
4038
4327
  return id;
4039
4328
  };
4040
4329
 
4330
+ const getExtensionUri = (uri, platform, origin) => {
4331
+ if (platform !== Web || !uri) {
4332
+ return uri;
4333
+ }
4334
+ return new URL(uri, origin).href;
4335
+ };
4336
+
4337
+ const segmentRegex = /^[\w.-]+$/;
4338
+ const getRepositoryUrl = extension => {
4339
+ if (!extension || !hasProperty(extension, 'repository')) {
4340
+ return '';
4341
+ }
4342
+ const {
4343
+ repository
4344
+ } = extension;
4345
+ if (typeof repository === 'string') {
4346
+ return repository;
4347
+ }
4348
+ if (repository && hasProperty(repository, 'url') && typeof repository.url === 'string') {
4349
+ return repository.url;
4350
+ }
4351
+ return '';
4352
+ };
4353
+ const getGithubRepository = extension => {
4354
+ const rawUrl = getRepositoryUrl(extension).replace(/^git\+/, '');
4355
+ try {
4356
+ const url = new URL(rawUrl);
4357
+ if (url.protocol !== 'https:' || url.hostname.toLowerCase() !== 'github.com' || url.username || url.password || url.port) {
4358
+ return undefined;
4359
+ }
4360
+ const segments = url.pathname.split('/').filter(Boolean);
4361
+ if (segments.length !== 2) {
4362
+ return undefined;
4363
+ }
4364
+ const owner = segments[0];
4365
+ const repository = segments[1].replace(/\.git$/i, '');
4366
+ if (!owner || !repository || !segmentRegex.test(owner) || !segmentRegex.test(repository)) {
4367
+ return undefined;
4368
+ }
4369
+ return {
4370
+ owner,
4371
+ repository
4372
+ };
4373
+ } catch {
4374
+ return undefined;
4375
+ }
4376
+ };
4377
+
4041
4378
  const getLinkProtectionEnabled = async () => {
4042
4379
  try {
4043
4380
  const setting = await getPreference('application.linkProtectionEnabled');
@@ -4186,6 +4523,9 @@ const getIcon = (extension, platform, assetDir) => {
4186
4523
  }
4187
4524
  return `/remote/${extension.path}/${extension.icon}`; // TODO support windows paths
4188
4525
  }
4526
+ if (platform === Web) {
4527
+ return `${extension.path}/${extension.icon}`;
4528
+ }
4189
4529
  return '';
4190
4530
  };
4191
4531
 
@@ -4497,23 +4837,8 @@ const formatCreated = (created, now = Date.now()) => {
4497
4837
  return relativeTimeFormat.format(value, unit);
4498
4838
  };
4499
4839
 
4500
- const formatLastUpdated = lastUpdated => {
4501
- if (lastUpdated === null) {
4502
- return 'n/a';
4503
- }
4504
- try {
4505
- const date = new Date(lastUpdated * 1000);
4506
- if (Number.isNaN(date.getTime())) {
4507
- return 'n/a';
4508
- }
4509
- return date.toLocaleDateString(undefined, {
4510
- day: 'numeric',
4511
- month: 'short',
4512
- year: 'numeric'
4513
- });
4514
- } catch {
4515
- return 'n/a';
4516
- }
4840
+ const formatLastUpdated = (lastUpdated, now = Date.now()) => {
4841
+ return formatCreated(lastUpdated, now);
4517
4842
  };
4518
4843
 
4519
4844
  const getSizeEntries = (showSizeLink, displaySize, extensionUri) => {
@@ -4694,6 +5019,16 @@ const parseCreated = extension => {
4694
5019
  return timestamp;
4695
5020
  };
4696
5021
 
5022
+ const secondsToMillisecondsThreshold = 10_000_000_000;
5023
+ const normalizeTimestamp = timestamp => {
5024
+ if (!Number.isFinite(timestamp) || timestamp <= 0) {
5025
+ return null;
5026
+ }
5027
+ if (timestamp < secondsToMillisecondsThreshold) {
5028
+ return timestamp * 1000;
5029
+ }
5030
+ return timestamp;
5031
+ };
4697
5032
  const parseLastUpdated = extension => {
4698
5033
  if (!extension || typeof extension !== 'object') {
4699
5034
  return null;
@@ -4705,16 +5040,14 @@ const parseLastUpdated = extension => {
4705
5040
  return null;
4706
5041
  }
4707
5042
  if (typeof lastUpdated === 'number') {
4708
- if (Number.isFinite(lastUpdated) && lastUpdated > 0) {
4709
- return lastUpdated;
4710
- }
4711
- return null;
5043
+ return normalizeTimestamp(lastUpdated);
4712
5044
  }
4713
5045
  if (typeof lastUpdated === 'string') {
4714
- const parsed = Number.parseFloat(lastUpdated);
4715
- if (Number.isFinite(parsed) && parsed > 0) {
4716
- return parsed;
5046
+ const numericTimestamp = Number(lastUpdated);
5047
+ if (Number.isFinite(numericTimestamp)) {
5048
+ return normalizeTimestamp(numericTimestamp);
4717
5049
  }
5050
+ return normalizeTimestamp(Date.parse(lastUpdated));
4718
5051
  }
4719
5052
  return null;
4720
5053
  };
@@ -4767,7 +5100,7 @@ const restoreState = savedState => {
4767
5100
  const isEnabled = tab => {
4768
5101
  return tab.enabled;
4769
5102
  };
4770
- const loadContent = async (state, platform, savedState, isTest = false) => {
5103
+ const loadContentInternal = async (state, platform, savedState, isTest = false) => {
4771
5104
  if (isTest) {
4772
5105
  savedState = undefined;
4773
5106
  }
@@ -4789,13 +5122,14 @@ const loadContent = async (state, platform, savedState, isTest = false) => {
4789
5122
  description,
4790
5123
  downloadCount,
4791
5124
  extensionId,
4792
- extensionUri,
5125
+ extensionUri: unresolvedExtensionUri,
4793
5126
  extensionVersion,
4794
5127
  hasColorTheme,
4795
5128
  iconSrc,
4796
5129
  name,
4797
5130
  rating
4798
5131
  } = headerData;
5132
+ const extensionUri = getExtensionUri(unresolvedExtensionUri, platform, location.origin);
4799
5133
  const readmeUrl = join(extensionUri, 'README.md');
4800
5134
  const changelogUrl = join(extensionUri, 'CHANGELOG.md');
4801
5135
  const [hasReadme, hasChangelog] = await Promise.all([existsFile(readmeUrl), existsFile(changelogUrl)]);
@@ -4814,7 +5148,7 @@ const loadContent = async (state, platform, savedState, isTest = false) => {
4814
5148
  const detailsVirtualDom = await getMarkdownVirtualDom(readmeHtml, {
4815
5149
  scrollToTopEnabled: true
4816
5150
  });
4817
- const isBuiltin = extension?.isBuiltin;
5151
+ const isBuiltin = extension?.isBuiltin || extension?.builtin || false;
4818
5152
  const disabled = extension?.disabled;
4819
5153
  const extensionColorThemeId = getColorThemeId(extension) || '';
4820
5154
  const extensionColorThemeLabel = getColorThemeLabel(extension) || '';
@@ -4828,7 +5162,8 @@ const loadContent = async (state, platform, savedState, isTest = false) => {
4828
5162
  } = restoreState(savedState);
4829
5163
  const features = getFeatures(selectedFeature || Theme, extension);
4830
5164
  const hasFeatures = features.length > 0;
4831
- const tabs = getTabs(selectedTab, hasReadme, hasFeatures, hasChangelog);
5165
+ const hasGithubReleases = Boolean(getGithubRepository(extension));
5166
+ const tabs = getTabs(selectedTab, hasReadme, hasFeatures, hasChangelog || hasGithubReleases);
4832
5167
  const enabledTabs = tabs.filter(isEnabled);
4833
5168
  const sizeValue = getViewletSize(width || 0);
4834
5169
  const showSizeLink = platform !== Web$1;
@@ -4861,6 +5196,8 @@ const loadContent = async (state, platform, savedState, isTest = false) => {
4861
5196
  disabled,
4862
5197
  displaySize,
4863
5198
  downloadCount,
5199
+ errorMessage: '',
5200
+ errorTitle: '',
4864
5201
  extension,
4865
5202
  extensionId,
4866
5203
  extensionUri,
@@ -4897,6 +5234,21 @@ const loadContent = async (state, platform, savedState, isTest = false) => {
4897
5234
  tabs: enabledTabs
4898
5235
  };
4899
5236
  };
5237
+ const loadContent = async (state, platform, savedState, isTest = false) => {
5238
+ try {
5239
+ return await loadContentInternal(state, platform, savedState, isTest);
5240
+ } catch (error) {
5241
+ const extensionId = getExtensionIdFromUri(state.uri);
5242
+ const errorMessage = error instanceof ExtensionNotFoundError ? extensionNotAvailable(extensionId) : unableToLoadExtensionWithError(getErrorMessage$1(error));
5243
+ return {
5244
+ ...state,
5245
+ errorMessage,
5246
+ errorTitle: unableToLoadExtension(),
5247
+ extensionId,
5248
+ initial: false
5249
+ };
5250
+ }
5251
+ };
4900
5252
 
4901
5253
  const handleExtensionsChanged = async state => {
4902
5254
  const {
@@ -4916,6 +5268,10 @@ const handleExtensionsStatusUpdate = async state => {
4916
5268
  };
4917
5269
  };
4918
5270
 
5271
+ const handleHeaderContextMenu = async state => {
5272
+ return state;
5273
+ };
5274
+
4919
5275
  const handleIconError = state => {
4920
5276
  const {
4921
5277
  assetDir,
@@ -5046,6 +5402,13 @@ const handleResourceLinkClick = async (state, href) => {
5046
5402
 
5047
5403
  const handleScroll = (state, scrollTop, scrollSource = Script) => {
5048
5404
  const newScrollTop = Math.max(0, scrollTop);
5405
+ if (state.selectedTab === Changelog) {
5406
+ return {
5407
+ ...state,
5408
+ changelogScrollTop: newScrollTop,
5409
+ scrollSource
5410
+ };
5411
+ }
5049
5412
  return {
5050
5413
  ...state,
5051
5414
  readmeScrollTop: newScrollTop,
@@ -5072,6 +5435,39 @@ const handleTabFocus = (state, name) => {
5072
5435
  };
5073
5436
  };
5074
5437
 
5438
+ const escapeMarkdown = value => {
5439
+ return value.replaceAll('\\', '\\\\').replaceAll('[', '\\[').replaceAll(']', '\\]');
5440
+ };
5441
+ const getPublishedText = publishedAt => {
5442
+ if (!publishedAt) {
5443
+ return 'Publication date unavailable';
5444
+ }
5445
+ const date = new Date(publishedAt);
5446
+ if (Number.isNaN(date.getTime())) {
5447
+ return 'Publication date unavailable';
5448
+ }
5449
+ return `Published ${date.toLocaleDateString(undefined, {
5450
+ dateStyle: 'long'
5451
+ })}`;
5452
+ };
5453
+ const getGithubReleasesMarkdown = (releases, githubRepository) => {
5454
+ if (releases.length === 0) {
5455
+ return `# Changelog\n\nNo GitHub releases were found for **${escapeMarkdown(githubRepository.owner)}/${escapeMarkdown(githubRepository.repository)}**.`;
5456
+ }
5457
+ return releases.map(release => {
5458
+ const title = release.name || release.tagName;
5459
+ const body = release.body.trim() || '_No release notes were provided._';
5460
+ return `# [${escapeMarkdown(title)}](${release.htmlUrl})\n\n${getPublishedText(release.publishedAt)} · \`${release.tagName}\`\n\n${body}`;
5461
+ }).join('\n\n---\n\n');
5462
+ };
5463
+
5464
+ class GithubReleasesError extends Error {
5465
+ constructor(message) {
5466
+ super(message);
5467
+ this.name = 'GithubReleasesError';
5468
+ }
5469
+ }
5470
+
5075
5471
  const error = async error => {
5076
5472
  // TODO send message to error worker or log worker
5077
5473
  // @ts-ignore
@@ -5092,21 +5488,206 @@ const loadChangelogContent = async path => {
5092
5488
  }
5093
5489
  };
5094
5490
 
5491
+ const pageSize = 100;
5492
+ const maximumReleases = 100;
5493
+ const getErrorMessage = async response => {
5494
+ try {
5495
+ const body = await response.clone().json();
5496
+ if (body && hasProperty(body, 'message') && typeof body.message === 'string') {
5497
+ return body.message;
5498
+ }
5499
+ } catch {
5500
+ // The status code still provides a useful error when the error body is invalid.
5501
+ }
5502
+ return '';
5503
+ };
5504
+ const getHttpError = async response => {
5505
+ const apiMessage = await getErrorMessage(response);
5506
+ const rateLimitRemaining = response.headers.get('x-ratelimit-remaining');
5507
+ if (response.status === 429 || response.status === 403 && (rateLimitRemaining === '0' || /rate limit/i.test(apiMessage))) {
5508
+ const resetValue = Number(response.headers.get('x-ratelimit-reset'));
5509
+ const retry = Number.isFinite(resetValue) && resetValue > 0 ? ` Try again after ${new Date(resetValue * 1000).toLocaleString()}.` : ' Please try again later.';
5510
+ return new GithubReleasesError(`GitHub API rate limit exceeded.${retry}`);
5511
+ }
5512
+ if (response.status === 404) {
5513
+ return new GithubReleasesError('The GitHub repository could not be found or is not publicly reachable.');
5514
+ }
5515
+ if (response.status >= 500) {
5516
+ return new GithubReleasesError(`GitHub is temporarily unavailable (server error ${response.status}). Please try again later.`);
5517
+ }
5518
+ if (response.status >= 400) {
5519
+ const detail = apiMessage ? `: ${apiMessage}` : '';
5520
+ return new GithubReleasesError(`GitHub rejected the releases request (${response.status} ${response.statusText || 'Client Error'})${detail}.`);
5521
+ }
5522
+ return new GithubReleasesError(`GitHub returned an unexpected HTTP status (${response.status}).`);
5523
+ };
5524
+ const parseNullableString = value => {
5525
+ if (typeof value === 'string') {
5526
+ return value;
5527
+ }
5528
+ if (value === null) {
5529
+ return '';
5530
+ }
5531
+ return undefined;
5532
+ };
5533
+ const parseRelease = value => {
5534
+ if (!value || !hasProperty(value, 'body') || !hasProperty(value, 'html_url') || !hasProperty(value, 'name') || !hasProperty(value, 'published_at') || !hasProperty(value, 'tag_name')) {
5535
+ return undefined;
5536
+ }
5537
+ const body = parseNullableString(value.body);
5538
+ const name = parseNullableString(value.name);
5539
+ const publishedAt = parseNullableString(value.published_at);
5540
+ if (body === undefined || name === undefined || publishedAt === undefined || typeof value.html_url !== 'string' || typeof value.tag_name !== 'string') {
5541
+ return undefined;
5542
+ }
5543
+ try {
5544
+ const htmlUrl = new URL(value.html_url);
5545
+ if (htmlUrl.protocol !== 'https:' || htmlUrl.hostname !== 'github.com') {
5546
+ return undefined;
5547
+ }
5548
+ } catch {
5549
+ return undefined;
5550
+ }
5551
+ return {
5552
+ body,
5553
+ htmlUrl: value.html_url,
5554
+ name,
5555
+ publishedAt,
5556
+ tagName: value.tag_name
5557
+ };
5558
+ };
5559
+ const parsePage = async response => {
5560
+ let value;
5561
+ try {
5562
+ value = await response.json();
5563
+ } catch {
5564
+ throw new GithubReleasesError('GitHub returned an invalid response instead of JSON release data. Please try again later.');
5565
+ }
5566
+ if (!Array.isArray(value)) {
5567
+ throw new GithubReleasesError('GitHub returned invalid release data. Please try again later.');
5568
+ }
5569
+ const releases = value.map(parseRelease);
5570
+ if (releases.some(release => !release)) {
5571
+ throw new GithubReleasesError('GitHub returned invalid release data. Please try again later.');
5572
+ }
5573
+ return releases;
5574
+ };
5575
+ const loadGithubReleases = async ({
5576
+ owner,
5577
+ repository
5578
+ }) => {
5579
+ const releases = [];
5580
+ for (let page = 1;; page++) {
5581
+ const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/releases?per_page=${pageSize}&page=${page}`;
5582
+ let response;
5583
+ try {
5584
+ response = await request(url);
5585
+ } catch {
5586
+ throw new GithubReleasesError('GitHub releases could not be loaded because GitHub is not reachable. Check your connection and try again.');
5587
+ }
5588
+ if (!response.ok) {
5589
+ throw await getHttpError(response);
5590
+ }
5591
+ const pageReleases = await parsePage(response);
5592
+ const remaining = maximumReleases - releases.length;
5593
+ if (pageReleases.length > remaining) {
5594
+ releases.push(...pageReleases.slice(0, remaining));
5595
+ return {
5596
+ isTruncated: true,
5597
+ releases
5598
+ };
5599
+ }
5600
+ releases.push(...pageReleases);
5601
+ if (pageReleases.length < pageSize) {
5602
+ return {
5603
+ isTruncated: false,
5604
+ releases
5605
+ };
5606
+ }
5607
+ }
5608
+ };
5609
+
5610
+ const releaseBatchSize = 100;
5611
+ const mergeMarkdownVirtualDoms = chunks => {
5612
+ let root;
5613
+ const children = [];
5614
+ for (const chunk of chunks) {
5615
+ const [chunkRoot, ...chunkChildren] = chunk;
5616
+ if (!chunkRoot) {
5617
+ continue;
5618
+ }
5619
+ root = root ? {
5620
+ ...root,
5621
+ childCount: root.childCount + chunkRoot.childCount
5622
+ } : chunkRoot;
5623
+ children.push(...chunkChildren);
5624
+ }
5625
+ return root ? [root, ...children] : [];
5626
+ };
5627
+ const renderGithubReleases = async (result, githubRepository, languages, locationProtocol) => {
5628
+ const chunks = [];
5629
+ const {
5630
+ isTruncated,
5631
+ releases
5632
+ } = result;
5633
+ const releaseGroups = releases.length === 0 ? [[]] : Array.from({
5634
+ length: Math.ceil(releases.length / releaseBatchSize)
5635
+ }, (_, index) => {
5636
+ return releases.slice(index * releaseBatchSize, (index + 1) * releaseBatchSize);
5637
+ });
5638
+ const baseUrl = `https://github.com/${githubRepository.owner}/${githubRepository.repository}/blob/HEAD/`;
5639
+ for (const [index, releaseGroup] of releaseGroups.entries()) {
5640
+ const limitMessage = index === 0 && isTruncated ? `> Showing the newest ${releases.length} GitHub releases. Older releases are not displayed to keep the editor responsive.\n\n` : '';
5641
+ const markdown = limitMessage + getGithubReleasesMarkdown(releaseGroup, githubRepository);
5642
+ const html = await renderMarkdown(markdown, {
5643
+ baseUrl,
5644
+ languages,
5645
+ linksExternal: true,
5646
+ locationProtocol
5647
+ });
5648
+ chunks.push([...(await getMarkdownVirtualDom(html))]);
5649
+ }
5650
+ return addScrollToTopVirtualDom(mergeMarkdownVirtualDoms(chunks));
5651
+ };
5095
5652
  const selectTabChangelog = async state => {
5096
5653
  const {
5097
5654
  baseUrl,
5098
5655
  extension,
5656
+ extensionUri,
5099
5657
  languages,
5100
5658
  locationProtocol,
5101
5659
  tabs
5102
5660
  } = state;
5103
- const changelogContent = await loadChangelogContent(extension.path); // TODO use uri
5104
- const changelogMarkdownHtml = await renderMarkdown(changelogContent, {
5105
- baseUrl,
5106
- languages,
5107
- locationProtocol
5108
- });
5109
- const changelogDom = await getMarkdownVirtualDom(changelogMarkdownHtml);
5661
+ const githubRepository = getGithubRepository(extension);
5662
+ let changelogDom;
5663
+ if (githubRepository) {
5664
+ try {
5665
+ const result = await loadGithubReleases(githubRepository);
5666
+ changelogDom = await renderGithubReleases(result, githubRepository, languages, locationProtocol);
5667
+ } catch (error) {
5668
+ const message = error instanceof GithubReleasesError ? error.message : 'GitHub releases could not be loaded. Please try again later.';
5669
+ const html = await renderMarkdown(`# Changelog\n\n${message}`, {
5670
+ baseUrl,
5671
+ languages,
5672
+ linksExternal: true,
5673
+ locationProtocol
5674
+ });
5675
+ changelogDom = await getMarkdownVirtualDom(html, {
5676
+ scrollToTopEnabled: true
5677
+ });
5678
+ }
5679
+ } else {
5680
+ const changelogContent = await loadChangelogContent(extensionUri);
5681
+ const html = await renderMarkdown(changelogContent, {
5682
+ baseUrl,
5683
+ languages,
5684
+ linksExternal: true,
5685
+ locationProtocol
5686
+ });
5687
+ changelogDom = await getMarkdownVirtualDom(html, {
5688
+ scrollToTopEnabled: true
5689
+ });
5690
+ }
5110
5691
  const newTabs = tabs.map(tab => {
5111
5692
  return {
5112
5693
  ...tab,
@@ -5380,7 +5961,8 @@ const getChangelogVirtualDom = changelogDom => {
5380
5961
  // TODO set tabpanel role
5381
5962
  return [{
5382
5963
  childCount: 1,
5383
- className: Changelog$1,
5964
+ className: `${ExtensionDetailPanel} ${Changelog$1}`,
5965
+ role: Panel,
5384
5966
  type: Div
5385
5967
  }, ...changelogDom];
5386
5968
  };
@@ -5684,6 +6266,31 @@ const getExtensionDetailContentVirtualDom = (sanitizedReadmeHtml, selectedTab, w
5684
6266
  }
5685
6267
  };
5686
6268
 
6269
+ const getExtensionDetailErrorVirtualDom = (title, message) => {
6270
+ return [{
6271
+ childCount: 1,
6272
+ className: mergeClassNames(Viewlet, ExtensionDetail, ExtensionDetailError),
6273
+ type: Div
6274
+ }, {
6275
+ childCount: 3,
6276
+ className: ExtensionDetailErrorCard,
6277
+ role: 'alert',
6278
+ type: Div
6279
+ }, {
6280
+ childCount: 0,
6281
+ className: mergeClassNames(MaskIcon, MaskIconWarning, ExtensionDetailErrorIcon),
6282
+ type: Div
6283
+ }, {
6284
+ childCount: 1,
6285
+ className: ExtensionDetailErrorTitle,
6286
+ type: H1
6287
+ }, text(title), {
6288
+ childCount: 1,
6289
+ className: ExtensionDetailErrorMessage,
6290
+ type: P
6291
+ }, text(message)];
6292
+ };
6293
+
5687
6294
  const getExtensionDetailDescriptionVirtualDom = description => {
5688
6295
  return [{
5689
6296
  childCount: 1,
@@ -5796,6 +6403,7 @@ const getExtensionDetailHeaderVirtualDom = (name, iconSrc, description, badge, b
5796
6403
  }, getExtensionDetailIconVirtualDom(iconSrc), {
5797
6404
  childCount: 3,
5798
6405
  className: ExtensionDetailHeaderDetails,
6406
+ onContextMenu: HandleHeaderContextMenu,
5799
6407
  type: Div
5800
6408
  }, ...getExtensionDetailNameVirtualDom(name, badge), ...getExtensionDetailDescriptionVirtualDom(description), ...getExtensionDetailHeaderActionsVirtualDom(buttonDefs, settingsButtonEnabled)];
5801
6409
  return dom;
@@ -5852,6 +6460,9 @@ const getClassNames = size => {
5852
6460
  };
5853
6461
 
5854
6462
  const getExtensionDetailVirtualDom = (newState, selectedTab) => {
6463
+ if (newState.errorMessage) {
6464
+ return getExtensionDetailErrorVirtualDom(newState.errorTitle, newState.errorMessage);
6465
+ }
5855
6466
  // TODO move this to view model so that rendering occurs like
5856
6467
  // 1. state
5857
6468
  // 2. view model
@@ -6018,6 +6629,10 @@ const renderEventListeners = () => {
6018
6629
  name: HandleImageContextMenu,
6019
6630
  params: ['handleImageContextMenu', ClientX, ClientY],
6020
6631
  preventDefault: true
6632
+ }, {
6633
+ name: HandleHeaderContextMenu,
6634
+ params: ['handleHeaderContextMenu'],
6635
+ preventDefault: true
6021
6636
  }, {
6022
6637
  name: HandleReadmeScroll,
6023
6638
  params: ['handleScroll', 'event.target.scrollTop', User],
@@ -6130,6 +6745,7 @@ const commandMap = {
6130
6745
  'ExtensionDetail.handleExtensionsChanged': wrapCommand(handleExtensionsChanged),
6131
6746
  'ExtensionDetail.handleExtensionsStatusUpdate': wrapCommand(handleExtensionsStatusUpdate),
6132
6747
  'ExtensionDetail.handleFeaturesClick': wrapCommand(handleClickFeatures),
6748
+ 'ExtensionDetail.handleHeaderContextMenu': wrapCommand(handleHeaderContextMenu),
6133
6749
  'ExtensionDetail.handleIconError': wrapCommand(handleIconError),
6134
6750
  'ExtensionDetail.handleImageContextMenu': wrapCommand(handleImageContextMenu),
6135
6751
  'ExtensionDetail.handleMouseEnterEnable': wrapCommand(handleMouseEnterEnable),
@@ -6146,9 +6762,11 @@ const commandMap = {
6146
6762
  'ExtensionDetail.hideSizeLink': wrapCommand(hideSizeLink),
6147
6763
  'ExtensionDetail.initialize': initialize,
6148
6764
  'ExtensionDetail.loadContent2': wrapCommand(loadContent2),
6765
+ 'ExtensionDetail.mockGithubApi': wrapCommand(handleMockGithubApi),
6149
6766
  'ExtensionDetail.openImageInNewTab': wrapCommand(openImageInNewTab),
6150
6767
  'ExtensionDetail.render2': render2,
6151
6768
  'ExtensionDetail.renderEventListeners': renderEventListeners,
6769
+ 'ExtensionDetail.resetGithubApiMock': wrapCommand(handleResetGithubApiMock),
6152
6770
  'ExtensionDetail.resize': wrapCommand(resize),
6153
6771
  'ExtensionDetail.saveState': wrapGetter(saveState),
6154
6772
  'ExtensionDetail.selectTab': wrapCommand(selectTab),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/extension-detail-view",
3
- "version": "7.10.0",
3
+ "version": "7.13.0",
4
4
  "description": "Extension Detail View Worker",
5
5
  "repository": {
6
6
  "type": "git",