@theia/plugin-ext 1.75.0-next.21 → 1.75.0-next.28

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.
Files changed (73) hide show
  1. package/lib/common/errors.d.ts +1 -14
  2. package/lib/common/errors.d.ts.map +1 -1
  3. package/lib/common/errors.js +3 -16
  4. package/lib/common/errors.js.map +1 -1
  5. package/lib/common/plugin-protocol.d.ts +45 -648
  6. package/lib/common/plugin-protocol.d.ts.map +1 -1
  7. package/lib/common/plugin-protocol.js +11 -34
  8. package/lib/common/plugin-protocol.js.map +1 -1
  9. package/lib/hosted/browser/worker/plugin-manifest-loader.d.ts.map +1 -1
  10. package/lib/hosted/browser/worker/plugin-manifest-loader.js +5 -36
  11. package/lib/hosted/browser/worker/plugin-manifest-loader.js.map +1 -1
  12. package/lib/hosted/browser/worker/worker-main.js +2 -1
  13. package/lib/hosted/browser/worker/worker-main.js.map +1 -1
  14. package/lib/hosted/node/hosted-plugin-localization-service.d.ts.map +1 -1
  15. package/lib/hosted/node/hosted-plugin-localization-service.js +4 -68
  16. package/lib/hosted/node/hosted-plugin-localization-service.js.map +1 -1
  17. package/lib/hosted/node/plugin-host-rpc.d.ts.map +1 -1
  18. package/lib/hosted/node/plugin-host-rpc.js +2 -2
  19. package/lib/hosted/node/plugin-host-rpc.js.map +1 -1
  20. package/lib/hosted/node/plugin-reader.d.ts.map +1 -1
  21. package/lib/hosted/node/plugin-reader.js +4 -3
  22. package/lib/hosted/node/plugin-reader.js.map +1 -1
  23. package/lib/hosted/node/scanners/grammars-reader.d.ts +3 -2
  24. package/lib/hosted/node/scanners/grammars-reader.d.ts.map +1 -1
  25. package/lib/hosted/node/scanners/grammars-reader.js +5 -23
  26. package/lib/hosted/node/scanners/grammars-reader.js.map +1 -1
  27. package/lib/hosted/node/scanners/scanner-theia.d.ts +38 -71
  28. package/lib/hosted/node/scanners/scanner-theia.d.ts.map +1 -1
  29. package/lib/hosted/node/scanners/scanner-theia.js +117 -768
  30. package/lib/hosted/node/scanners/scanner-theia.js.map +1 -1
  31. package/lib/hosted/node/scanners/scanner-theia.spec.js +191 -0
  32. package/lib/hosted/node/scanners/scanner-theia.spec.js.map +1 -1
  33. package/lib/main/browser/plugin-contribution-handler.js +1 -1
  34. package/lib/main/browser/plugin-contribution-handler.js.map +1 -1
  35. package/lib/main/browser/plugin-icon-service.d.ts.map +1 -1
  36. package/lib/main/browser/plugin-icon-service.js +2 -1
  37. package/lib/main/browser/plugin-icon-service.js.map +1 -1
  38. package/lib/main/browser/plugin-icon-theme-service.d.ts.map +1 -1
  39. package/lib/main/browser/plugin-icon-theme-service.js +2 -1
  40. package/lib/main/browser/plugin-icon-theme-service.js.map +1 -1
  41. package/lib/main/browser/plugin-shared-style.d.ts.map +1 -1
  42. package/lib/main/browser/plugin-shared-style.js +2 -1
  43. package/lib/main/browser/plugin-shared-style.js.map +1 -1
  44. package/package.json +31 -30
  45. package/src/common/errors.ts +1 -25
  46. package/src/common/plugin-protocol.ts +195 -662
  47. package/src/hosted/browser/worker/plugin-manifest-loader.ts +8 -41
  48. package/src/hosted/browser/worker/worker-main.ts +2 -1
  49. package/src/hosted/node/hosted-plugin-localization-service.ts +3 -82
  50. package/src/hosted/node/plugin-host-rpc.ts +3 -3
  51. package/src/hosted/node/plugin-reader.ts +4 -3
  52. package/src/hosted/node/scanners/grammars-reader.ts +14 -23
  53. package/src/hosted/node/scanners/scanner-theia.spec.ts +225 -1
  54. package/src/hosted/node/scanners/scanner-theia.ts +204 -881
  55. package/src/main/browser/plugin-contribution-handler.ts +1 -1
  56. package/src/main/browser/plugin-icon-service.ts +2 -1
  57. package/src/main/browser/plugin-icon-theme-service.ts +2 -1
  58. package/src/main/browser/plugin-shared-style.ts +2 -1
  59. package/lib/hosted/node/plugin-activation-events.d.ts +0 -7
  60. package/lib/hosted/node/plugin-activation-events.d.ts.map +0 -1
  61. package/lib/hosted/node/plugin-activation-events.js +0 -95
  62. package/lib/hosted/node/plugin-activation-events.js.map +0 -1
  63. package/lib/hosted/node/plugin-manifest-loader.d.ts +0 -3
  64. package/lib/hosted/node/plugin-manifest-loader.d.ts.map +0 -1
  65. package/lib/hosted/node/plugin-manifest-loader.js +0 -34
  66. package/lib/hosted/node/plugin-manifest-loader.js.map +0 -1
  67. package/lib/main/node/errors.spec.d.ts +0 -2
  68. package/lib/main/node/errors.spec.d.ts.map +0 -1
  69. package/lib/main/node/errors.spec.js +0 -36
  70. package/lib/main/node/errors.spec.js.map +0 -1
  71. package/src/hosted/node/plugin-activation-events.ts +0 -112
  72. package/src/hosted/node/plugin-manifest-loader.ts +0 -32
  73. package/src/main/node/errors.spec.ts +0 -37
@@ -16,11 +16,11 @@
16
16
 
17
17
  /* eslint-disable @typescript-eslint/no-explicit-any */
18
18
 
19
- import { PluginIdentifiers, PluginModel, PluginPackage } from '../../../common/plugin-protocol';
19
+ import { PluginModel, PluginPackage } from '../../../common/plugin-protocol';
20
20
  import { Endpoint } from '@theia/core/lib/browser/endpoint';
21
21
  import URI from '@theia/core/lib/common/uri';
22
-
23
- const NLS_REGEX = /^%([\w\d.-]+)%$/i;
22
+ import { localizeWithResolver } from '@theia/plugin-utils/lib/common/package-nls';
23
+ import { prepareLoadedManifest } from '@theia/plugin-utils/lib/common/plugin-manifest';
24
24
 
25
25
  function getUri(pluginModel: PluginModel, relativePath: string): URI {
26
26
  const ownURI = new Endpoint().getRestUrl();
@@ -54,23 +54,18 @@ function readContents(uri: string): Promise<string> {
54
54
 
55
55
  async function readPluginJson(pluginModel: PluginModel, relativePath: string): Promise<any> {
56
56
  const content = await readPluginFile(pluginModel, relativePath);
57
- const json = JSON.parse(content) as PluginPackage;
58
- json.publisher ??= PluginIdentifiers.UNPUBLISHED;
59
- return json;
57
+ return JSON.parse(content);
60
58
  }
61
59
 
62
60
  export async function loadManifest(pluginModel: PluginModel): Promise<any> {
63
61
  const [manifest, translations] = await Promise.all([
64
- readPluginJson(pluginModel, 'package.json'),
62
+ readPluginJson(pluginModel, 'package.json').then(raw =>
63
+ prepareLoadedManifest(raw as PluginPackage, { updateActivationEvents: false })
64
+ ),
65
65
  loadTranslations(pluginModel)
66
66
  ]);
67
- // translate vscode builtins, as they are published with a prefix.
68
- const built_prefix = '@theia/vscode-builtin-';
69
- if (manifest && manifest.name && manifest.name.startsWith(built_prefix)) {
70
- manifest.name = manifest.name.substring(built_prefix.length);
71
- }
72
67
  return manifest && translations && Object.keys(translations).length ?
73
- localize(manifest, translations) :
68
+ localizeWithResolver(manifest, key => translations[key]) :
74
69
  manifest;
75
70
  }
76
71
 
@@ -84,31 +79,3 @@ async function loadTranslations(pluginModel: PluginModel): Promise<any> {
84
79
  return {};
85
80
  }
86
81
  }
87
-
88
- function localize(value: any, translations: {
89
- [key: string]: string
90
- }): any {
91
- if (typeof value === 'string') {
92
- const match = NLS_REGEX.exec(value);
93
- return match && translations[match[1]] || value;
94
- }
95
- if (Array.isArray(value)) {
96
- const result = [];
97
- for (const item of value) {
98
- result.push(localize(item, translations));
99
- }
100
- return result;
101
- }
102
- if (value === null) {
103
- return value;
104
- }
105
- if (typeof value === 'object') {
106
- const result: { [key: string]: any } = {};
107
- // eslint-disable-next-line guard-for-in
108
- for (const propertyName in value) {
109
- result[propertyName] = localize(value[propertyName], translations);
110
- }
111
- return result;
112
- }
113
- return value;
114
- }
@@ -35,6 +35,7 @@ import { EnvExtImpl } from '../../../plugin/env';
35
35
  import { DebugExtImpl } from '../../../plugin/debug/debug-ext';
36
36
  import { LocalizationExtImpl } from '../../../plugin/localization-ext';
37
37
  import pluginHostModule from './worker-plugin-module';
38
+ import { PLUGINS_BASE_PATH } from '@theia/plugin-utils/lib/common/constants';
38
39
 
39
40
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
41
  const ctx = self as any;
@@ -70,7 +71,7 @@ pluginManager.setPluginHost({
70
71
  ctx.frontendModuleName = plugin.lifecycle.frontendModuleName;
71
72
  }
72
73
 
73
- ctx.importScripts('./hostedPlugin/' + getPluginId(plugin.model) + '/' + plugin.pluginPath);
74
+ ctx.importScripts(`./${PLUGINS_BASE_PATH}/` + getPluginId(plugin.model) + '/' + plugin.pluginPath);
74
75
  }
75
76
  }
76
77
 
@@ -22,9 +22,11 @@ import { inject, injectable, named } from '@theia/core/shared/inversify';
22
22
  import { DeployedPlugin, Localization as PluginLocalization, PluginIdentifiers, Translation } from '../../common';
23
23
  import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
24
24
  import { BackendApplicationContribution } from '@theia/core/lib/node';
25
- import { Disposable, DisposableCollection, isObject, MaybePromise, nls, Path, URI, ILogger } from '@theia/core';
25
+ import { Disposable, DisposableCollection, MaybePromise, nls, Path, URI, ILogger } from '@theia/core';
26
26
  import { Deferred } from '@theia/core/lib/common/promise-util';
27
27
  import { LanguagePackBundle, LanguagePackService } from '../../common/language-pack-service';
28
+ import { localizePackage } from '@theia/plugin-utils/lib/common/package-nls';
29
+ import { loadPackageTranslations } from '@theia/plugin-utils/lib/node/package-nls';
28
30
 
29
31
  export interface VSCodeNlsConfig {
30
32
  locale: string
@@ -327,84 +329,3 @@ async function loadTranslations(packagePath: Path, translations: Translation[]):
327
329
  function buildTranslationKey(pluginId: string, scope: string, key: string): string {
328
330
  return `${pluginId}/${Localization.transformKey(scope)}/${key}`;
329
331
  }
330
-
331
- // Localization logic for `package.json` entries
332
- // Extensions can use `package.nls.json` files to store translations for values in their package.json
333
- // This logic has not changed with the introduction of the vscode.l10n API
334
-
335
- interface PackageTranslation {
336
- translation?: Record<string, string>
337
- default?: Record<string, string>
338
- }
339
-
340
- async function loadPackageTranslations(pluginPath: string, locale: string): Promise<PackageTranslation> {
341
- const localizedPluginPath = path.join(pluginPath, `package.nls.${locale}.json`);
342
- try {
343
- const defaultValue = coerceLocalizations(await fs.readJson(path.join(pluginPath, 'package.nls.json')));
344
- if (await fs.pathExists(localizedPluginPath)) {
345
- return {
346
- translation: coerceLocalizations(await fs.readJson(localizedPluginPath)),
347
- default: defaultValue
348
- };
349
- }
350
- return {
351
- default: defaultValue
352
- };
353
- } catch (e) {
354
- if (e.code !== 'ENOENT') {
355
- throw e;
356
- }
357
- return {};
358
- }
359
- }
360
-
361
- interface LocalizeInfo {
362
- message: string
363
- comment?: string
364
- }
365
-
366
- function isLocalizeInfo(obj: unknown): obj is LocalizeInfo {
367
- return isObject(obj) && 'message' in obj || false;
368
- }
369
-
370
- function coerceLocalizations(translations: Record<string, string | LocalizeInfo>): Record<string, string> {
371
- for (const [key, value] of Object.entries(translations)) {
372
- if (isLocalizeInfo(value)) {
373
- translations[key] = value.message;
374
- } else if (typeof value !== 'string') {
375
- // Only strings or LocalizeInfo values are valid
376
- translations[key] = 'INVALID TRANSLATION VALUE';
377
- }
378
- }
379
- return translations as Record<string, string>;
380
- }
381
-
382
- function localizePackage(value: unknown, translations: PackageTranslation, callback: (key: string, defaultValue: string) => string): unknown {
383
- if (typeof value === 'string') {
384
- let result = value;
385
- if (value.length > 2 && value.startsWith('%') && value.endsWith('%')) {
386
- const key = value.slice(1, -1);
387
- if (translations.translation && key in translations.translation) {
388
- result = translations.translation[key];
389
- } else if (translations.default && key in translations.default) {
390
- result = callback(key, translations.default[key]);
391
- }
392
- }
393
- return result;
394
- }
395
- if (Array.isArray(value)) {
396
- const result = [];
397
- for (const item of value) {
398
- result.push(localizePackage(item, translations, callback));
399
- }
400
- return result;
401
- }
402
- if (isObject(value)) {
403
- const result: Record<string, unknown> = {};
404
- for (const [name, item] of Object.entries(value)) {
405
- result[name] = localizePackage(item, translations, callback);
406
- }
407
- return result;
408
- }
409
- return value;
410
- }
@@ -25,7 +25,7 @@ import {
25
25
  MAIN_RPC_CONTEXT, Plugin, PluginAPIFactory, PluginManager,
26
26
  LocalizationExt
27
27
  } from '../../common/plugin-api-rpc';
28
- import { PluginMetadata, PluginModel } from '../../common/plugin-protocol';
28
+ import { PluginMetadata, PluginModel, PluginPackage } from '../../common/plugin-protocol';
29
29
  import { createAPIFactory } from '../../plugin/plugin-context';
30
30
  import { EnvExtImpl } from '../../plugin/env';
31
31
  import { PreferenceRegistryExtImpl } from '../../plugin/preference-registry';
@@ -35,7 +35,7 @@ import { EditorsAndDocumentsExtImpl } from '../../plugin/editors-and-documents';
35
35
  import { WorkspaceExtImpl } from '../../plugin/workspace';
36
36
  import { MessageRegistryExt } from '../../plugin/message-registry';
37
37
  import { ClipboardExt } from '../../plugin/clipboard-ext';
38
- import { loadManifest } from './plugin-manifest-loader';
38
+ import { loadManifest } from '@theia/plugin-utils/lib/node/plugin-manifest';
39
39
  import { KeyValueStorageProxy } from '../../plugin/plugin-storage';
40
40
  import { WebviewsExtImpl } from '../../plugin/webviews';
41
41
  import { TerminalServiceExtImpl } from '../../plugin/terminal-ext';
@@ -214,7 +214,7 @@ export abstract class AbstractPluginHostRPC<PM extends AbstractPluginManagerExtI
214
214
  const pluginModel = plg.model;
215
215
  const pluginLifecycle = plg.lifecycle;
216
216
 
217
- const rawModel = await loadManifest(pluginModel.packagePath);
217
+ const rawModel = await loadManifest<PluginPackage>(pluginModel.packagePath);
218
218
  rawModel.packagePath = pluginModel.packagePath;
219
219
  if (pluginModel.entryPoint!.frontend) {
220
220
  foreign.push({
@@ -23,7 +23,8 @@ import { inject, injectable, optional, multiInject } from '@theia/core/shared/in
23
23
  import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
24
24
  import { PluginMetadata, getPluginId, MetadataProcessor, PluginPackage, PluginContribution } from '../../common/plugin-protocol';
25
25
  import { MetadataScanner } from './metadata-scanner';
26
- import { loadManifest } from './plugin-manifest-loader';
26
+ import { loadManifest } from '@theia/plugin-utils/lib/node/plugin-manifest';
27
+ import { PLUGINS_BASE_PATH } from '@theia/plugin-utils/lib/common/constants';
27
28
 
28
29
  @injectable()
29
30
  export class HostedPluginReader implements BackendApplicationContribution {
@@ -43,7 +44,7 @@ export class HostedPluginReader implements BackendApplicationContribution {
43
44
  protected pluginsIdsFiles: Map<string, string> = new Map();
44
45
 
45
46
  configure(app: express.Application): void {
46
- app.get('/hostedPlugin/:pluginId/:path(*)', async (req, res) => {
47
+ app.get(`/${PLUGINS_BASE_PATH}/:pluginId/:path(*)`, async (req, res) => {
47
48
  const pluginId = req.params.pluginId;
48
49
  const filePath = req.params.path;
49
50
 
@@ -141,7 +142,7 @@ export class HostedPluginReader implements BackendApplicationContribution {
141
142
  return undefined;
142
143
  }
143
144
  const resolvedPluginPath = await realpath(pluginPath);
144
- const manifest = await loadManifest(resolvedPluginPath);
145
+ const manifest = await loadManifest<PluginPackage>(resolvedPluginPath);
145
146
  if (!manifest) {
146
147
  return undefined;
147
148
  }
@@ -16,16 +16,20 @@
16
16
 
17
17
  import { injectable } from '@theia/core/shared/inversify';
18
18
  import { PluginPackageGrammarsContribution, GrammarsContribution } from '../../../common';
19
- import * as path from 'path';
20
- import * as fs from '@theia/core/shared/fs-extra';
19
+ import { readGrammarFromDisk } from '@theia/plugin-utils/lib/node/read-grammars';
20
+ import type { NormalizeContributionsContext } from '@theia/plugin-utils/lib/common/contribution-types';
21
21
 
22
22
  @injectable()
23
23
  export class GrammarsReader {
24
24
 
25
- async readGrammars(rawGrammars: PluginPackageGrammarsContribution[], pluginPath: string): Promise<GrammarsContribution[]> {
25
+ async readGrammars(
26
+ rawGrammars: readonly PluginPackageGrammarsContribution[],
27
+ pluginPath: string,
28
+ log?: Pick<NormalizeContributionsContext, 'onError'>
29
+ ): Promise<GrammarsContribution[]> {
26
30
  const result = new Array<GrammarsContribution>();
27
31
  for (const rawGrammar of rawGrammars) {
28
- const grammar = await this.readGrammar(rawGrammar, pluginPath);
32
+ const grammar = await this.readGrammar(rawGrammar, pluginPath, log);
29
33
  if (grammar) {
30
34
  result.push(grammar);
31
35
  }
@@ -34,24 +38,11 @@ export class GrammarsReader {
34
38
  return result;
35
39
  }
36
40
 
37
- private async readGrammar(rawGrammar: PluginPackageGrammarsContribution, pluginPath: string): Promise<GrammarsContribution | undefined> {
38
- // TODO: validate inputs
39
- let grammar: string | object;
40
-
41
- if (rawGrammar.path.endsWith('json')) {
42
- grammar = await fs.readJSON(path.resolve(pluginPath, rawGrammar.path));
43
- } else {
44
- grammar = await fs.readFile(path.resolve(pluginPath, rawGrammar.path), 'utf8');
45
- }
46
- return {
47
- language: rawGrammar.language,
48
- scope: rawGrammar.scopeName,
49
- format: rawGrammar.path.endsWith('json') ? 'json' : 'plist',
50
- grammar: grammar,
51
- grammarLocation: rawGrammar.path,
52
- injectTo: rawGrammar.injectTo,
53
- embeddedLanguages: rawGrammar.embeddedLanguages,
54
- tokenTypes: rawGrammar.tokenTypes
55
- };
41
+ protected async readGrammar(
42
+ rawGrammar: PluginPackageGrammarsContribution,
43
+ pluginPath: string,
44
+ log?: Pick<NormalizeContributionsContext, 'onError'>
45
+ ): Promise<GrammarsContribution | undefined> {
46
+ return readGrammarFromDisk(rawGrammar, pluginPath, log);
56
47
  }
57
48
  }
@@ -21,7 +21,7 @@ import * as os from 'os';
21
21
  import * as path from 'path';
22
22
  import * as fs from 'fs';
23
23
  import { AbstractPluginScanner, TheiaPluginScanner } from './scanner-theia';
24
- import { PluginPackage, PluginEntryPoint } from '../../../common/plugin-protocol';
24
+ import { PluginPackage, PluginEntryPoint, PluginPackageWalkthrough, WalkthroughContribution } from '../../../common/plugin-protocol';
25
25
  import { PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
26
26
  import URI from '@theia/core/lib/common/uri';
27
27
 
@@ -206,3 +206,227 @@ describe('TheiaPluginScanner configuration default derivation', () => {
206
206
  expect(props['ext.nullable'].default).to.equal(null);
207
207
  });
208
208
  });
209
+
210
+ describe('TheiaPluginScanner - readWalkthroughs', () => {
211
+ let scannerInstance: TheiaPluginScanner;
212
+
213
+ before(() => {
214
+ scannerInstance = new (TheiaPluginScanner as any)();
215
+ (scannerInstance as any).logger = { error: () => { }, warn: () => { }, info: () => { }, debug: () => { } };
216
+ });
217
+
218
+ function callReadWalkthroughs(
219
+ walkthroughs: PluginPackageWalkthrough[],
220
+ publisher = 'test-publisher',
221
+ name = 'test-plugin',
222
+ icon?: string
223
+ ): WalkthroughContribution[] | undefined {
224
+ const pkg = {
225
+ name,
226
+ publisher,
227
+ version: '1.0.0',
228
+ icon,
229
+ contributes: { walkthroughs }
230
+ } as any;
231
+ return (scannerInstance as any).readWalkthroughs(pkg);
232
+ }
233
+
234
+ it('should resolve the icon of the contributing extension', () => {
235
+ const result = callReadWalkthroughs([{
236
+ id: 'wt1',
237
+ title: 'WT',
238
+ description: 'desc',
239
+ steps: [{ id: 's1', title: 'S1', description: 'd1' }]
240
+ }], 'test-publisher', 'test-plugin', 'images/icon.png');
241
+
242
+ expect(result![0].pluginIcon).to.equal('hostedPlugin/test_publisher_test_plugin/images/icon.png');
243
+ });
244
+
245
+ it('should leave the extension icon undefined when the package declares none', () => {
246
+ const result = callReadWalkthroughs([{
247
+ id: 'wt1',
248
+ title: 'WT',
249
+ description: 'desc',
250
+ steps: [{ id: 's1', title: 'S1', description: 'd1' }]
251
+ }]);
252
+
253
+ expect(result![0].pluginIcon).to.be.undefined;
254
+ });
255
+
256
+ it('should read a valid walkthrough contribution', () => {
257
+ const result = callReadWalkthroughs([{
258
+ id: 'my-walkthrough',
259
+ title: 'My Walkthrough',
260
+ description: 'A test walkthrough',
261
+ steps: [{
262
+ id: 'step1',
263
+ title: 'Step 1',
264
+ description: 'First step',
265
+ completionEvents: ['onCommand:my.command']
266
+ }]
267
+ }]);
268
+
269
+ expect(result).to.not.be.undefined;
270
+ expect(result).to.have.lengthOf(1);
271
+ expect(result![0].id).to.equal('my-walkthrough');
272
+ expect(result![0].title).to.equal('My Walkthrough');
273
+ expect(result![0].pluginId).to.equal('test-publisher.test-plugin');
274
+ expect(result![0].steps).to.have.lengthOf(1);
275
+ expect(result![0].steps[0].id).to.equal('step1');
276
+ expect(result![0].steps[0].completionEvents).to.deep.equal(['onCommand:my.command']);
277
+ });
278
+
279
+ it('should skip walkthroughs with missing id', () => {
280
+ const result = callReadWalkthroughs([{
281
+ id: '',
282
+ title: 'No ID Walkthrough',
283
+ description: 'Missing id',
284
+ steps: []
285
+ }]);
286
+
287
+ expect(result).to.be.undefined;
288
+ });
289
+
290
+ it('should skip walkthroughs with missing title', () => {
291
+ const result = callReadWalkthroughs([{
292
+ id: 'valid-id',
293
+ title: '',
294
+ description: 'Missing title',
295
+ steps: []
296
+ }]);
297
+
298
+ expect(result).to.be.undefined;
299
+ });
300
+
301
+ it('should skip walkthrough steps with missing id', () => {
302
+ const result = callReadWalkthroughs([{
303
+ id: 'wt1',
304
+ title: 'WT1',
305
+ description: 'desc',
306
+ steps: [
307
+ { id: '', title: 'Step', description: 'desc' },
308
+ { id: 'valid-step', title: 'Valid Step', description: 'desc' }
309
+ ]
310
+ }]);
311
+
312
+ expect(result).to.have.lengthOf(1);
313
+ expect(result![0].steps).to.have.lengthOf(1);
314
+ expect(result![0].steps[0].id).to.equal('valid-step');
315
+ });
316
+
317
+ it('should return undefined when no walkthroughs are contributed', () => {
318
+ const pkg = { name: 'test', publisher: 'pub', contributes: {} } as any;
319
+ const result = (scannerInstance as any).readWalkthroughs(pkg);
320
+ expect(result).to.be.undefined;
321
+ });
322
+
323
+ it('should read multiple walkthroughs', () => {
324
+ const result = callReadWalkthroughs([
325
+ {
326
+ id: 'wt1',
327
+ title: 'Walkthrough 1',
328
+ description: 'First',
329
+ steps: [{ id: 's1', title: 'S1', description: 'd1' }]
330
+ },
331
+ {
332
+ id: 'wt2',
333
+ title: 'Walkthrough 2',
334
+ description: 'Second',
335
+ steps: [{ id: 's2', title: 'S2', description: 'd2' }]
336
+ }
337
+ ]);
338
+
339
+ expect(result).to.have.lengthOf(2);
340
+ expect(result![0].id).to.equal('wt1');
341
+ expect(result![1].id).to.equal('wt2');
342
+ });
343
+
344
+ it('should preserve walkthrough optional fields', () => {
345
+ const result = callReadWalkthroughs([{
346
+ id: 'wt-full',
347
+ title: 'Full Walkthrough',
348
+ description: 'Full desc',
349
+ steps: [{
350
+ id: 's1',
351
+ title: 'S1',
352
+ description: 'step desc',
353
+ when: 'isLinux',
354
+ media: { markdown: 'content.md' },
355
+ completionEvents: ['onCommand:test']
356
+ }],
357
+ when: 'workspacePlatform == linux',
358
+ icon: 'book'
359
+ }]);
360
+
361
+ expect(result).to.have.lengthOf(1);
362
+ expect(result![0].when).to.equal('workspacePlatform == linux');
363
+ expect(result![0].icon).to.equal('book');
364
+ expect(result![0].steps[0].when).to.equal('isLinux');
365
+ expect(result![0].steps[0].media).to.deep.equal({ markdown: 'hostedPlugin/test_publisher_test_plugin/content.md' });
366
+ });
367
+
368
+ it('should lowercase pluginId', () => {
369
+ const result = callReadWalkthroughs([{
370
+ id: 'wt1',
371
+ title: 'WT',
372
+ description: 'desc',
373
+ steps: [{ id: 's1', title: 'S1', description: 'd1' }]
374
+ }], 'MyPublisher', 'MyPlugin');
375
+
376
+ expect(result).to.have.lengthOf(1);
377
+ expect(result![0].pluginId).to.equal('mypublisher.myplugin');
378
+ });
379
+
380
+ it('should resolve media image paths through toPluginUrl', () => {
381
+ const result = callReadWalkthroughs([{
382
+ id: 'wt1',
383
+ title: 'WT',
384
+ description: 'desc',
385
+ steps: [{
386
+ id: 's1',
387
+ title: 'S1',
388
+ description: 'd1',
389
+ media: { image: 'media/image.png' }
390
+ }]
391
+ }]);
392
+
393
+ expect(result).to.have.lengthOf(1);
394
+ expect(result![0].steps[0].media).to.deep.equal({ image: 'hostedPlugin/test_publisher_test_plugin/media/image.png' });
395
+ });
396
+
397
+ it('should keep the alt text of svg media', () => {
398
+ const result = callReadWalkthroughs([{
399
+ id: 'wt1',
400
+ title: 'WT',
401
+ description: 'desc',
402
+ steps: [{
403
+ id: 's1',
404
+ title: 'S1',
405
+ description: 'd1',
406
+ media: { svg: 'media/icon.svg', altText: 'An icon' }
407
+ }]
408
+ }]);
409
+
410
+ expect(result![0].steps[0].media).to.deep.equal({
411
+ svg: 'hostedPlugin/test_publisher_test_plugin/media/icon.svg',
412
+ altText: 'An icon'
413
+ });
414
+ });
415
+
416
+ it('should resolve media svg paths through toPluginUrl', () => {
417
+ const result = callReadWalkthroughs([{
418
+ id: 'wt1',
419
+ title: 'WT',
420
+ description: 'desc',
421
+ steps: [{
422
+ id: 's1',
423
+ title: 'S1',
424
+ description: 'd1',
425
+ media: { svg: 'media/icon.svg' }
426
+ }]
427
+ }]);
428
+
429
+ expect(result).to.have.lengthOf(1);
430
+ expect(result![0].steps[0].media).to.deep.equal({ svg: 'hostedPlugin/test_publisher_test_plugin/media/icon.svg' });
431
+ });
432
+ });