@theia/plugin-utils 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 (122) hide show
  1. package/README.md +30 -0
  2. package/lib/common/constants.d.ts +12 -0
  3. package/lib/common/constants.d.ts.map +1 -0
  4. package/lib/common/constants.js +30 -0
  5. package/lib/common/constants.js.map +1 -0
  6. package/lib/common/constants.spec.d.ts +2 -0
  7. package/lib/common/constants.spec.d.ts.map +1 -0
  8. package/lib/common/constants.spec.js +33 -0
  9. package/lib/common/constants.spec.js.map +1 -0
  10. package/lib/common/contribution-types.d.ts +648 -0
  11. package/lib/common/contribution-types.d.ts.map +1 -0
  12. package/lib/common/contribution-types.js +30 -0
  13. package/lib/common/contribution-types.js.map +1 -0
  14. package/lib/common/manifest-types.d.ts +105 -0
  15. package/lib/common/manifest-types.d.ts.map +1 -0
  16. package/lib/common/manifest-types.js +31 -0
  17. package/lib/common/manifest-types.js.map +1 -0
  18. package/lib/common/manifest-types.spec.d.ts +2 -0
  19. package/lib/common/manifest-types.spec.d.ts.map +1 -0
  20. package/lib/common/manifest-types.spec.js +35 -0
  21. package/lib/common/manifest-types.spec.js.map +1 -0
  22. package/lib/common/package-nls.d.ts +24 -0
  23. package/lib/common/package-nls.d.ts.map +1 -0
  24. package/lib/common/package-nls.js +82 -0
  25. package/lib/common/package-nls.js.map +1 -0
  26. package/lib/common/package-nls.spec.d.ts +2 -0
  27. package/lib/common/package-nls.spec.d.ts.map +1 -0
  28. package/lib/common/package-nls.spec.js +98 -0
  29. package/lib/common/package-nls.spec.js.map +1 -0
  30. package/lib/common/plugin-activation-events.d.ts +7 -0
  31. package/lib/common/plugin-activation-events.d.ts.map +1 -0
  32. package/lib/common/plugin-activation-events.js +95 -0
  33. package/lib/common/plugin-activation-events.js.map +1 -0
  34. package/lib/common/plugin-activation-events.spec.d.ts +2 -0
  35. package/lib/common/plugin-activation-events.spec.d.ts.map +1 -0
  36. package/lib/common/plugin-activation-events.spec.js +89 -0
  37. package/lib/common/plugin-activation-events.spec.js.map +1 -0
  38. package/lib/common/plugin-manifest.d.ts +10 -0
  39. package/lib/common/plugin-manifest.d.ts.map +1 -0
  40. package/lib/common/plugin-manifest.js +35 -0
  41. package/lib/common/plugin-manifest.js.map +1 -0
  42. package/lib/common/plugin-manifest.spec.d.ts +2 -0
  43. package/lib/common/plugin-manifest.spec.d.ts.map +1 -0
  44. package/lib/common/plugin-manifest.spec.js +83 -0
  45. package/lib/common/plugin-manifest.spec.js.map +1 -0
  46. package/lib/common/plugin-model.d.ts +33 -0
  47. package/lib/common/plugin-model.d.ts.map +1 -0
  48. package/lib/common/plugin-model.js +166 -0
  49. package/lib/common/plugin-model.js.map +1 -0
  50. package/lib/common/plugin-model.spec.d.ts +2 -0
  51. package/lib/common/plugin-model.spec.d.ts.map +1 -0
  52. package/lib/common/plugin-model.spec.js +204 -0
  53. package/lib/common/plugin-model.spec.js.map +1 -0
  54. package/lib/common/protocol-shims.d.ts +128 -0
  55. package/lib/common/protocol-shims.d.ts.map +1 -0
  56. package/lib/common/protocol-shims.js +29 -0
  57. package/lib/common/protocol-shims.js.map +1 -0
  58. package/lib/common/test-helpers.d.ts +5 -0
  59. package/lib/common/test-helpers.d.ts.map +1 -0
  60. package/lib/common/test-helpers.js +41 -0
  61. package/lib/common/test-helpers.js.map +1 -0
  62. package/lib/common/utils.d.ts +20 -0
  63. package/lib/common/utils.d.ts.map +1 -0
  64. package/lib/common/utils.js +74 -0
  65. package/lib/common/utils.js.map +1 -0
  66. package/lib/common/utils.spec.d.ts +2 -0
  67. package/lib/common/utils.spec.d.ts.map +1 -0
  68. package/lib/common/utils.spec.js +77 -0
  69. package/lib/common/utils.spec.js.map +1 -0
  70. package/lib/node/normalize-contributions.d.ts +54 -0
  71. package/lib/node/normalize-contributions.d.ts.map +1 -0
  72. package/lib/node/normalize-contributions.js +985 -0
  73. package/lib/node/normalize-contributions.js.map +1 -0
  74. package/lib/node/normalize-contributions.spec.d.ts +2 -0
  75. package/lib/node/normalize-contributions.spec.d.ts.map +1 -0
  76. package/lib/node/normalize-contributions.spec.js +850 -0
  77. package/lib/node/normalize-contributions.spec.js.map +1 -0
  78. package/lib/node/package-nls.d.ts +3 -0
  79. package/lib/node/package-nls.d.ts.map +1 -0
  80. package/lib/node/package-nls.js +45 -0
  81. package/lib/node/package-nls.js.map +1 -0
  82. package/lib/node/plugin-manifest.d.ts +6 -0
  83. package/lib/node/plugin-manifest.d.ts.map +1 -0
  84. package/lib/node/plugin-manifest.js +26 -0
  85. package/lib/node/plugin-manifest.js.map +1 -0
  86. package/lib/node/plugin-model.d.ts +5 -0
  87. package/lib/node/plugin-model.d.ts.map +1 -0
  88. package/lib/node/plugin-model.js +36 -0
  89. package/lib/node/plugin-model.js.map +1 -0
  90. package/lib/node/read-grammars.d.ts +10 -0
  91. package/lib/node/read-grammars.d.ts.map +1 -0
  92. package/lib/node/read-grammars.js +83 -0
  93. package/lib/node/read-grammars.js.map +1 -0
  94. package/lib/node/read-grammars.spec.d.ts +2 -0
  95. package/lib/node/read-grammars.spec.d.ts.map +1 -0
  96. package/lib/node/read-grammars.spec.js +109 -0
  97. package/lib/node/read-grammars.spec.js.map +1 -0
  98. package/package.json +42 -0
  99. package/src/common/constants.spec.ts +34 -0
  100. package/src/common/constants.ts +35 -0
  101. package/src/common/contribution-types.ts +677 -0
  102. package/src/common/manifest-types.spec.ts +36 -0
  103. package/src/common/manifest-types.ts +126 -0
  104. package/src/common/package-nls.spec.ts +122 -0
  105. package/src/common/package-nls.ts +95 -0
  106. package/src/common/plugin-activation-events.spec.ts +104 -0
  107. package/src/common/plugin-activation-events.ts +102 -0
  108. package/src/common/plugin-manifest.spec.ts +94 -0
  109. package/src/common/plugin-manifest.ts +44 -0
  110. package/src/common/plugin-model.spec.ts +246 -0
  111. package/src/common/plugin-model.ts +194 -0
  112. package/src/common/protocol-shims.ts +148 -0
  113. package/src/common/test-helpers.ts +44 -0
  114. package/src/common/utils.spec.ts +86 -0
  115. package/src/common/utils.ts +81 -0
  116. package/src/node/normalize-contributions.spec.ts +931 -0
  117. package/src/node/normalize-contributions.ts +1098 -0
  118. package/src/node/package-nls.ts +45 -0
  119. package/src/node/plugin-manifest.ts +30 -0
  120. package/src/node/plugin-model.ts +33 -0
  121. package/src/node/read-grammars.spec.ts +128 -0
  122. package/src/node/read-grammars.ts +97 -0
@@ -0,0 +1,931 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Maksim Kachurin and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { CustomEditorPriority, type GrammarsContribution, type WalkthroughContribution } from '../common/contribution-types';
19
+ import { PreferenceScope } from '../common/protocol-shims';
20
+ import {
21
+ deriveDefaultForType,
22
+ extractPluginViewsIds,
23
+ getScope,
24
+ normalizeContributions,
25
+ readColors,
26
+ readCommand,
27
+ readConfiguration,
28
+ readCustomEditors,
29
+ readDebuggers,
30
+ readIconThemes,
31
+ readIcons,
32
+ readKeybinding,
33
+ readLanguages,
34
+ readLocalization,
35
+ readLocalizations,
36
+ readMenus,
37
+ readSnippets,
38
+ readSubmenus,
39
+ readTaskDefinition,
40
+ readTerminals,
41
+ readThemes,
42
+ readViews,
43
+ readViewsContainers,
44
+ readViewContainer,
45
+ readViewWelcome,
46
+ readViewsWelcome,
47
+ readWalkthroughs,
48
+ toSchema,
49
+ transformIconUrl
50
+ } from './normalize-contributions';
51
+ import { createNormalizeCtx, manifest } from '../common/test-helpers';
52
+ import type { PluginManifest } from '../common/manifest-types';
53
+
54
+ describe('normalize-contributions', () => {
55
+
56
+ describe('deriveDefaultForType', () => {
57
+ it('mirrors VS Code defaults for each json type', () => {
58
+ expect(deriveDefaultForType('boolean')).to.equal(false);
59
+ expect(deriveDefaultForType('integer')).to.equal(0);
60
+ expect(deriveDefaultForType('number')).to.equal(0);
61
+ expect(deriveDefaultForType('string')).to.equal('');
62
+ expect(deriveDefaultForType('array')).to.deep.equal([]);
63
+ expect(deriveDefaultForType('object')).to.deep.equal({});
64
+ // eslint-disable-next-line no-null/no-null
65
+ expect(deriveDefaultForType(undefined)).to.equal(null);
66
+ });
67
+
68
+ it('uses the first type when an array is provided', () => {
69
+ expect(deriveDefaultForType(['string', 'number'])).to.equal('');
70
+ });
71
+ });
72
+
73
+ describe('getScope', () => {
74
+ it('maps monaco scopes to preference scope and overridable flag', () => {
75
+ expect(getScope('machine-overridable')).to.deep.equal({ scope: PreferenceScope.Folder, overridable: false });
76
+ expect(getScope('language-overridable')).to.deep.equal({ scope: PreferenceScope.Folder, overridable: true });
77
+ expect(getScope('application')).to.deep.equal({ scope: PreferenceScope.User, overridable: false });
78
+ expect(getScope(undefined)).to.deep.equal({ scope: undefined, overridable: false });
79
+ });
80
+ });
81
+
82
+ describe('readConfiguration', () => {
83
+ it('derives defaults and property scopes', () => {
84
+ const schema = readConfiguration({
85
+ title: 'Sample',
86
+ scope: 'application',
87
+ properties: {
88
+ 'sample.enabled': { type: 'boolean', scope: 'language-overridable' },
89
+ 'sample.path': { type: 'string', default: '/tmp' }
90
+ }
91
+ }, '/tmp/plugin');
92
+
93
+ expect(schema?.scope).to.equal(PreferenceScope.User);
94
+ expect(schema?.properties['sample.enabled'].default).to.equal(false);
95
+ expect(schema?.properties['sample.enabled'].overridable).to.equal(true);
96
+ expect(schema?.properties['sample.path'].default).to.equal('/tmp');
97
+ });
98
+ });
99
+
100
+ describe('transformIconUrl and readCommand', () => {
101
+ it('resolves file icons and codicons', () => {
102
+ const ctx = createNormalizeCtx();
103
+ expect(transformIconUrl(ctx, ctx.plugin, './media/icon.png')).to.deep.equal({
104
+ iconUrl: 'hostedPlugin/acme_test_ext/media/icon.png'
105
+ });
106
+ expect(transformIconUrl(ctx, ctx.plugin, '$(zap)')).to.deep.equal({ themeIcon: '$(zap)' });
107
+ expect(transformIconUrl(ctx, ctx.plugin, {
108
+ light: './light.png',
109
+ dark: './dark.png'
110
+ })).to.deep.equal({
111
+ iconUrl: {
112
+ light: 'hostedPlugin/acme_test_ext/light.png',
113
+ dark: 'hostedPlugin/acme_test_ext/dark.png'
114
+ }
115
+ });
116
+
117
+ const command = readCommand(ctx, {
118
+ command: 'sample.run',
119
+ title: 'Run',
120
+ original: 'Original',
121
+ icon: '$(play)'
122
+ }, ctx.plugin);
123
+ expect(command).to.include({
124
+ command: 'sample.run',
125
+ title: 'Run',
126
+ originalTitle: 'Original',
127
+ themeIcon: '$(play)'
128
+ });
129
+ });
130
+
131
+ it('honors ctx.transformIconUrl from readCommand / readSubmenu', () => {
132
+ const ctx = createNormalizeCtx();
133
+ ctx.transformIconUrl = () => ({ themeIcon: '$(custom)' });
134
+ expect(readCommand(ctx, {
135
+ command: 'sample.run',
136
+ title: 'Run',
137
+ icon: '$(play)'
138
+ }, ctx.plugin).themeIcon).to.equal('$(custom)');
139
+ expect(readSubmenus(ctx, [{
140
+ id: 'sample.submenu',
141
+ label: 'Sample',
142
+ icon: './icon.png'
143
+ }], ctx.plugin)[0].icon).to.equal('$(custom)');
144
+ });
145
+ });
146
+
147
+ describe('readKeybinding and menus', () => {
148
+ it('copies keybinding fields', () => {
149
+ expect(readKeybinding({
150
+ key: 'ctrl+shift+p',
151
+ command: 'sample.run',
152
+ when: 'editorFocus',
153
+ mac: 'cmd+shift+p'
154
+ })).to.deep.equal({
155
+ keybinding: 'ctrl+shift+p',
156
+ command: 'sample.run',
157
+ when: 'editorFocus',
158
+ mac: 'cmd+shift+p',
159
+ linux: undefined,
160
+ win: undefined,
161
+ args: undefined
162
+ });
163
+ });
164
+
165
+ it('copies menu fields', () => {
166
+ expect(readMenus([{
167
+ command: 'sample.run',
168
+ group: 'navigation',
169
+ when: 'true'
170
+ }])).to.deep.equal([{
171
+ command: 'sample.run',
172
+ group: 'navigation',
173
+ when: 'true',
174
+ submenu: undefined,
175
+ alt: undefined
176
+ }]);
177
+ });
178
+ });
179
+
180
+ describe('views', () => {
181
+ it('extracts view ids across locations', () => {
182
+ expect(extractPluginViewsIds({
183
+ explorer: [{ id: 'a', name: 'A' }],
184
+ panel: [{ id: 'b', name: 'B' }]
185
+ })).to.deep.equal(['a', 'b']);
186
+ });
187
+
188
+ it('normalizes view containers, views, and welcome content order', () => {
189
+ const ctx = createNormalizeCtx();
190
+ const containers = readViewsContainers(ctx, [{
191
+ id: 'sample.container',
192
+ title: 'Sample',
193
+ icon: '$(folder)'
194
+ }], ctx.plugin);
195
+ expect(containers[0]).to.include({
196
+ id: 'sample.container',
197
+ themeIcon: '$(folder)'
198
+ });
199
+ expect(containers[0].iconUrl).to.equal('hostedPlugin/acme_test_ext/%24(folder)');
200
+
201
+ const views = readViews([{ id: 'sample.view', name: 'Sample View' }]);
202
+ expect(views).to.deep.equal([{
203
+ id: 'sample.view',
204
+ name: 'Sample View',
205
+ when: undefined,
206
+ type: undefined
207
+ }]);
208
+
209
+ const welcome = readViewsWelcome([{
210
+ view: 'sample.view',
211
+ contents: 'Welcome'
212
+ }], { explorer: [{ id: 'sample.view', name: 'Sample View' }] });
213
+ expect(welcome[0].order).to.equal(0);
214
+ expect(readViewWelcome({
215
+ view: 'missing.view',
216
+ contents: 'Missing'
217
+ }, ['sample.view']).order).to.equal(1);
218
+ });
219
+
220
+ it('skips view containers without a string icon and reports an error', () => {
221
+ const errors: unknown[] = [];
222
+ const ctx = createNormalizeCtx({}, {
223
+ onError: (_kind, message) => {
224
+ errors.push(message);
225
+ }
226
+ });
227
+ expect(readViewContainer(ctx, {
228
+ id: 'sample.container',
229
+ title: 'Sample'
230
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
231
+ } as any, ctx.plugin)).to.equal(undefined);
232
+ expect(readViewsContainers(ctx, [{
233
+ id: 'valid.container',
234
+ title: 'Valid',
235
+ icon: '$(folder)'
236
+ }, {
237
+ id: 'invalid.container',
238
+ title: 'Invalid'
239
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
240
+ } as any], ctx.plugin)).to.have.lengthOf(1);
241
+ expect(errors.length).to.be.greaterThan(0);
242
+ });
243
+ });
244
+
245
+ describe('custom editors and debuggers', () => {
246
+ it('normalizes custom editors with default priority', () => {
247
+ expect(readCustomEditors([{
248
+ viewType: 'sample.editor',
249
+ displayName: 'Editor'
250
+ }])).to.deep.equal([{
251
+ viewType: 'sample.editor',
252
+ displayName: 'Editor',
253
+ selector: [],
254
+ priority: CustomEditorPriority.default
255
+ }]);
256
+ });
257
+
258
+ it('copies debugger contributions', () => {
259
+ expect(readDebuggers([{
260
+ type: 'node',
261
+ label: 'Node Debug',
262
+ program: './out/debug.js',
263
+ configurationAttributes: {},
264
+ configurationSnippets: []
265
+ }])).to.deep.equal([{
266
+ type: 'node',
267
+ label: 'Node Debug',
268
+ program: './out/debug.js',
269
+ languages: undefined,
270
+ enableBreakpointsFor: undefined,
271
+ variables: undefined,
272
+ adapterExecutableCommand: undefined,
273
+ configurationSnippets: [],
274
+ win: undefined,
275
+ winx86: undefined,
276
+ windows: undefined,
277
+ osx: undefined,
278
+ linux: undefined,
279
+ args: undefined,
280
+ runtime: undefined,
281
+ runtimeArgs: undefined,
282
+ configurationAttributes: {}
283
+ }]);
284
+ });
285
+ });
286
+
287
+ describe('themes, snippets, icon themes, and colors', () => {
288
+ it('resolves theme and snippet uris', () => {
289
+ const ctx = createNormalizeCtx(undefined, undefined);
290
+ const plugin = manifest({
291
+ name: 'themes',
292
+ contributes: {
293
+ themes: [{ id: 'dark', label: 'Dark', path: './themes/dark.json' }],
294
+ snippets: [{ language: 'typescript', path: './snippets/ts.json' }],
295
+ iconThemes: [{ id: 'set', label: 'Set', path: './icons/theme.json' }]
296
+ }
297
+ });
298
+ ctx.plugin = plugin;
299
+
300
+ expect(readThemes(ctx, plugin)).to.deep.equal([{
301
+ id: 'dark',
302
+ label: 'Dark',
303
+ uri: 'hostedPlugin/acme_themes/themes/dark.json',
304
+ description: undefined,
305
+ uiTheme: undefined
306
+ }]);
307
+ expect(readSnippets(ctx, plugin)).to.deep.equal([{
308
+ language: 'typescript',
309
+ source: 'Test Extension',
310
+ uri: 'hostedPlugin/acme_themes/snippets/ts.json'
311
+ }]);
312
+ expect(readIconThemes(ctx, plugin)).to.deep.equal([{
313
+ id: 'set',
314
+ label: 'Set',
315
+ uri: 'hostedPlugin/acme_themes/icons/theme.json',
316
+ description: undefined,
317
+ uiTheme: undefined
318
+ }]);
319
+ });
320
+
321
+ it('validates color contributions and reports invalid ids', () => {
322
+ const warnings: string[] = [];
323
+ const ctx = createNormalizeCtx({ name: 'colors' }, {
324
+ onWarn: message => {
325
+ warnings.push(message);
326
+ }
327
+ });
328
+ const plugin = manifest({
329
+ name: 'colors',
330
+ contributes: {
331
+ colors: [
332
+ {
333
+ id: 'valid.color',
334
+ description: 'Valid',
335
+ defaults: { light: '#fff', dark: '#000', highContrast: '#111' }
336
+ },
337
+ { id: 'bad id', description: 'Bad', defaults: { light: '#fff', dark: '#000', highContrast: '#111' } }
338
+ ]
339
+ }
340
+ });
341
+ ctx.plugin = plugin;
342
+
343
+ const colors = readColors(ctx, plugin);
344
+ expect(colors).to.have.lengthOf(1);
345
+ expect(colors![0].defaults).to.deep.equal({ light: '#fff', dark: '#000', hc: '#111' });
346
+ expect(warnings.some(message => message.includes('word[.word]*'))).to.equal(true);
347
+ });
348
+
349
+ it('rejects color contributions with empty descriptions', () => {
350
+ const warnings: string[] = [];
351
+ const ctx = createNormalizeCtx({ name: 'colors' }, {
352
+ onWarn: message => {
353
+ warnings.push(message);
354
+ }
355
+ });
356
+ const plugin = manifest({
357
+ name: 'colors',
358
+ contributes: {
359
+ colors: [{
360
+ id: 'valid.color',
361
+ description: '',
362
+ defaults: { light: '#fff', dark: '#000', highContrast: '#111' }
363
+ }]
364
+ }
365
+ });
366
+ ctx.plugin = plugin;
367
+
368
+ expect(readColors(ctx, plugin)).to.deep.equal([]);
369
+ expect(warnings.some(message => message.includes('description'))).to.equal(true);
370
+ });
371
+ });
372
+
373
+ describe('icons', () => {
374
+ it('accepts icon references and font-based defaults', () => {
375
+ const ctx = createNormalizeCtx({ name: 'icons', publisher: 'acme' });
376
+ const plugin = manifest({
377
+ name: 'icons',
378
+ publisher: 'acme',
379
+ contributes: {
380
+ icons: {
381
+ 'sample-icon': {
382
+ description: 'Sample icon',
383
+ default: 'other-icon'
384
+ },
385
+ 'font-icon': {
386
+ description: 'Font icon',
387
+ default: {
388
+ fontPath: './media/icon.woff2',
389
+ fontCharacter: '\\E001'
390
+ }
391
+ }
392
+ }
393
+ } as unknown as PluginManifest['contributes']
394
+ });
395
+ ctx.plugin = plugin;
396
+
397
+ const icons = readIcons(ctx, plugin);
398
+ expect(icons).to.deep.equal([
399
+ {
400
+ id: 'sample-icon',
401
+ extensionId: 'acme.icons',
402
+ description: 'Sample icon',
403
+ defaults: { id: 'other-icon' }
404
+ },
405
+ {
406
+ id: 'font-icon',
407
+ extensionId: 'acme.icons',
408
+ description: 'Font icon',
409
+ defaults: {
410
+ fontCharacter: '\\E001',
411
+ location: 'hostedPlugin/acme_icons/media/icon.woff2'
412
+ }
413
+ }
414
+ ]);
415
+ });
416
+ });
417
+
418
+ describe('terminals, localizations, submenus, and tasks', () => {
419
+ it('reads terminal profiles with required fields only', () => {
420
+ const plugin = manifest({
421
+ name: 'terminal',
422
+ contributes: {
423
+ terminal: {
424
+ profiles: [
425
+ { id: 'bash', title: 'Bash' },
426
+ { id: 'invalid' } as unknown as { id: string; title: string },
427
+ { id: '', title: 'Empty' },
428
+ { id: 'empty-title', title: '' },
429
+ ]
430
+ }
431
+ }
432
+ });
433
+ expect(readTerminals(plugin)).to.deep.equal([{ id: 'bash', title: 'Bash' }]);
434
+ });
435
+
436
+ it('drops empty theme/snippet paths (avoids resolving to the plugin root)', () => {
437
+ const ctx = createNormalizeCtx(undefined, undefined);
438
+ const plugin = manifest({
439
+ name: 'empty-paths',
440
+ contributes: {
441
+ themes: [{ id: 'bad', path: '' }],
442
+ snippets: [{ language: 'ts', path: '' }],
443
+ iconThemes: [{ id: 'bad', path: '' }],
444
+ }
445
+ });
446
+ ctx.plugin = plugin;
447
+ expect(readThemes(ctx, plugin)).to.deep.equal([]);
448
+ expect(readSnippets(ctx, plugin)).to.deep.equal([]);
449
+ expect(readIconThemes(ctx, plugin)).to.deep.equal([]);
450
+ });
451
+
452
+ it('reads localization bundles', () => {
453
+ const plugin = manifest({
454
+ name: 'l10n',
455
+ packagePath: '/tmp/plugin',
456
+ contributes: {
457
+ localizations: [{
458
+ languageId: 'de',
459
+ languageName: 'German',
460
+ translations: [{ id: 'package', path: './package.nls.de.json' }]
461
+ }]
462
+ }
463
+ });
464
+ expect(readLocalizations(plugin)).to.deep.equal([{
465
+ languageId: 'de',
466
+ languageName: 'German',
467
+ localizedLanguageName: undefined,
468
+ minimalTranslations: undefined,
469
+ translations: [{ id: 'package', path: './package.nls.de.json' }]
470
+ }]);
471
+ });
472
+
473
+ it('honors readLocalization / readTranslation ctx hooks', () => {
474
+ const plugin = manifest({
475
+ name: 'l10n',
476
+ packagePath: '/tmp/plugin',
477
+ contributes: {
478
+ localizations: [{
479
+ languageId: 'de',
480
+ translations: [{ id: 'package', path: './package.nls.de.json' }]
481
+ }]
482
+ }
483
+ });
484
+ const ctx = createNormalizeCtx({ name: 'l10n', packagePath: '/tmp/plugin' });
485
+ ctx.readTranslation = (entry, pluginPath) => ({
486
+ id: entry.id,
487
+ path: `${pluginPath}/${entry.path}`
488
+ });
489
+ ctx.readLocalization = (entry, pluginPath) => readLocalization(entry, pluginPath, ctx);
490
+
491
+ expect(readLocalizations(plugin, ctx)).to.deep.equal([{
492
+ languageId: 'de',
493
+ languageName: undefined,
494
+ localizedLanguageName: undefined,
495
+ minimalTranslations: undefined,
496
+ translations: [{ id: 'package', path: '/tmp/plugin/./package.nls.de.json' }]
497
+ }]);
498
+ });
499
+
500
+ it('reads submenu icons via transformIconUrl', () => {
501
+ const ctx = createNormalizeCtx();
502
+ expect(readSubmenus(ctx, [{
503
+ id: 'sample.submenu',
504
+ label: 'Sample',
505
+ icon: '$(gear)'
506
+ }], ctx.plugin)).to.deep.equal([{
507
+ icon: '$(gear)',
508
+ id: 'sample.submenu',
509
+ label: 'Sample'
510
+ }]);
511
+ });
512
+
513
+ it('builds task definition schema with required type property', () => {
514
+ const definition = readTaskDefinition('sample.tasks', {
515
+ type: 'npm',
516
+ required: ['script'],
517
+ properties: {
518
+ script: { type: 'string' }
519
+ }
520
+ });
521
+ expect(definition.taskType).to.equal('npm');
522
+ expect(definition.source).to.equal('sample.tasks');
523
+ expect(definition.properties.all).to.deep.equal(['script']);
524
+ expect(toSchema({
525
+ type: 'npm',
526
+ required: [],
527
+ properties: { script: { type: 'string' } }
528
+ }).properties!.type).to.deep.equal({ type: 'string', const: 'npm' });
529
+ });
530
+
531
+ it('honors ctx.toSchema from readTaskDefinition', () => {
532
+ const customSchema = { type: 'object' as const, properties: { custom: { type: 'boolean' as const } } };
533
+ const definition = readTaskDefinition('sample.tasks', {
534
+ type: 'npm',
535
+ required: [],
536
+ properties: {}
537
+ }, {
538
+ toSchema: () => customSchema
539
+ });
540
+ expect(definition.properties.schema).to.equal(customSchema);
541
+ });
542
+ });
543
+
544
+ describe('readLanguage', () => {
545
+ it('loads language configuration from a relative json file', async () => {
546
+ const ctx = createNormalizeCtx({ name: 'lang', packagePath: '/tmp/plugin' }, {
547
+ readJsonFile: async () => ({
548
+ autoClosingPairs: [['{', '}'], { open: '[', close: ']', notIn: ['string'] }],
549
+ surroundingPairs: [['(', ')']]
550
+ })
551
+ });
552
+ const languages = await readLanguages(ctx, [{
553
+ id: 'sample',
554
+ configuration: './language-configuration.json'
555
+ }], ctx.plugin);
556
+
557
+ expect(languages[0].configuration?.autoClosingPairs).to.deep.equal([
558
+ { open: '{', close: '}' },
559
+ { open: '[', close: ']', notIn: ['string'] }
560
+ ]);
561
+ expect(languages[0].configuration?.surroundingPairs).to.deep.equal([{ open: '(', close: ')' }]);
562
+ });
563
+ });
564
+
565
+ describe('normalizeContributions', () => {
566
+ it('leaves output empty when the manifest has no contribution kinds', async () => {
567
+ const contributions = await normalizeContributions(createNormalizeCtx({ name: 'empty' }), {});
568
+ expect(Object.keys(contributions)).to.deep.equal([]);
569
+ });
570
+
571
+ it('normalizes supported contribution kinds via injectable readers', async () => {
572
+ const ctx = createNormalizeCtx({
573
+ name: 'full',
574
+ contributes: {
575
+ commands: [{ command: 'sample.run', title: 'Run' }],
576
+ keybindings: [{ key: 'ctrl+r', command: 'sample.run' }],
577
+ configuration: { title: 'Sample', properties: { enabled: { type: 'boolean' } } }
578
+ }
579
+ });
580
+
581
+ const contributions = await normalizeContributions(ctx, {});
582
+ expect(contributions).to.have.property('commands').with.lengthOf(1);
583
+ expect(contributions).to.have.property('keybindings').with.lengthOf(1);
584
+ expect(contributions).to.have.property('configuration').with.lengthOf(1);
585
+ });
586
+
587
+ it('runs the orchestrator for shell layout, passthrough, and async contribution kinds', async () => {
588
+ const readGrammars = async (): Promise<GrammarsContribution[]> => [{
589
+ language: 'sample-lang',
590
+ scope: 'source.sample',
591
+ format: 'json',
592
+ grammar: { scopeName: 'source.sample' },
593
+ grammarLocation: './grammar.json'
594
+ }];
595
+ const ctx = createNormalizeCtx({
596
+ name: 'orchestrator',
597
+ displayName: 'Orchestrator Extension',
598
+ contributes: {
599
+ configuration: [
600
+ { title: 'General', properties: { 'sample.enabled': { type: 'boolean' } } },
601
+ { title: 'Advanced', properties: { 'sample.count': { type: 'number' } } }
602
+ ],
603
+ configurationDefaults: { 'sample.enabled': true },
604
+ submenus: [{ id: 'sample.submenu', label: 'Sample', icon: '$(gear)' }],
605
+ customEditors: [{ viewType: 'sample.editor', displayName: 'Editor' }],
606
+ viewsContainers: {
607
+ activitybar: [{ id: 'sample.container', title: 'Sample', icon: './media/icon.png' }],
608
+ panel: [{ id: 'panel.container', title: 'Panel', icon: '$(panel)' }]
609
+ },
610
+ views: {
611
+ explorer: [{ id: 'sample.view', name: 'Sample View' }]
612
+ },
613
+ viewsWelcome: [{ view: 'sample.view', contents: 'Welcome' }],
614
+ commands: [{ command: 'sample.run', title: 'Run' }],
615
+ menus: {
616
+ 'editor/title': [{ command: 'sample.run', group: 'navigation' }]
617
+ },
618
+ keybindings: [{ key: 'ctrl+shift+r', command: 'sample.run' }],
619
+ debuggers: [{ type: 'node', label: 'Node Debug', program: './debug.js' }],
620
+ taskDefinitions: [{
621
+ type: 'npm',
622
+ required: ['script'],
623
+ properties: { script: { type: 'string' } }
624
+ }],
625
+ problemMatchers: [{ name: 'sample-matcher' }],
626
+ problemPatterns: [{ regexp: 'sample' }],
627
+ resourceLabelFormatters: [{ scheme: 'file' }],
628
+ authentication: [{ id: 'github', label: 'GitHub' }],
629
+ notebooks: [{ type: 'sample-notebook', displayName: 'Notebook' }],
630
+ notebookRenderer: [{ id: 'sample-renderer', displayName: 'Renderer' }],
631
+ notebookPreload: [{ type: 'sample-preload' }],
632
+ snippets: [{ language: 'typescript', path: './snippets/ts.json' }],
633
+ themes: [{ id: 'dark', label: 'Dark', path: './themes/dark.json' }],
634
+ colors: [{
635
+ id: 'sample.color',
636
+ description: 'Sample',
637
+ defaults: { light: '#fff', dark: '#000', highContrast: '#111' }
638
+ }],
639
+ iconThemes: [{ id: 'sample-icons', label: 'Icons', path: './icons/theme.json' }],
640
+ icons: {
641
+ 'sample-icon': {
642
+ description: 'Sample icon',
643
+ default: 'other-icon'
644
+ }
645
+ },
646
+ terminal: { profiles: [{ id: 'bash', title: 'Bash' }] },
647
+ localizations: [{
648
+ languageId: 'de',
649
+ languageName: 'German',
650
+ translations: [{ id: 'package', path: './package.nls.de.json' }]
651
+ }],
652
+ languages: [{ id: 'sample-lang', extensions: ['.sample'] }],
653
+ grammars: [{ language: 'sample-lang', scopeName: 'source.sample', path: './grammar.json' }]
654
+ } as unknown as PluginManifest['contributes']
655
+ }, { readGrammars });
656
+
657
+ const contributions = await normalizeContributions(ctx, {}) as Record<string, unknown>;
658
+
659
+ expect(contributions.configurationDefaults).to.deep.equal({ 'sample.enabled': true });
660
+
661
+ const configuration = contributions.configuration as Array<{ title?: string; properties: Record<string, { owner?: string; group?: string }> }>;
662
+ expect(configuration).to.have.lengthOf(2);
663
+ expect(configuration[0].properties['sample.enabled'].owner).to.equal('Orchestrator Extension');
664
+ expect(configuration[0].properties['sample.enabled'].group).to.equal('General');
665
+
666
+ expect(contributions.submenus).to.deep.equal([{
667
+ icon: '$(gear)',
668
+ id: 'sample.submenu',
669
+ label: 'Sample'
670
+ }]);
671
+ expect(contributions.customEditors).to.deep.equal([{
672
+ viewType: 'sample.editor',
673
+ displayName: 'Editor',
674
+ selector: [],
675
+ priority: CustomEditorPriority.default
676
+ }]);
677
+ expect(contributions.viewsContainers).to.deep.equal({
678
+ left: [{
679
+ id: 'sample.container',
680
+ title: 'Sample',
681
+ iconUrl: 'hostedPlugin/acme_orchestrator/media/icon.png',
682
+ themeIcon: undefined,
683
+ when: undefined
684
+ }],
685
+ bottom: [{
686
+ id: 'panel.container',
687
+ title: 'Panel',
688
+ iconUrl: 'hostedPlugin/acme_orchestrator/%24(panel)',
689
+ themeIcon: '$(panel)',
690
+ when: undefined
691
+ }]
692
+ });
693
+ expect(contributions.views).to.deep.equal({
694
+ explorer: [{ id: 'sample.view', name: 'Sample View', when: undefined, type: undefined }]
695
+ });
696
+ expect(contributions.viewsWelcome).to.deep.equal([{
697
+ view: 'sample.view',
698
+ content: 'Welcome',
699
+ when: undefined,
700
+ enablement: undefined,
701
+ order: 0
702
+ }]);
703
+ expect(contributions.menus).to.deep.equal({
704
+ 'editor/title': [{ command: 'sample.run', group: 'navigation', submenu: undefined, alt: undefined, when: undefined }]
705
+ });
706
+ expect(contributions.debuggers).to.deep.equal([{
707
+ type: 'node',
708
+ label: 'Node Debug',
709
+ program: './debug.js',
710
+ languages: undefined,
711
+ enableBreakpointsFor: undefined,
712
+ variables: undefined,
713
+ adapterExecutableCommand: undefined,
714
+ configurationSnippets: undefined,
715
+ win: undefined,
716
+ winx86: undefined,
717
+ windows: undefined,
718
+ osx: undefined,
719
+ linux: undefined,
720
+ args: undefined,
721
+ runtime: undefined,
722
+ runtimeArgs: undefined,
723
+ configurationAttributes: undefined
724
+ }]);
725
+ expect(contributions.taskDefinitions).to.deep.equal([{
726
+ taskType: 'npm',
727
+ source: 'orchestrator',
728
+ properties: {
729
+ required: ['script'],
730
+ all: ['script'],
731
+ schema: {
732
+ type: 'object',
733
+ required: ['script'],
734
+ properties: {
735
+ type: { type: 'string', const: 'npm' },
736
+ script: { type: 'string' }
737
+ }
738
+ }
739
+ }
740
+ }]);
741
+ expect(contributions.problemMatchers).to.deep.equal([{ name: 'sample-matcher' }]);
742
+ expect(contributions.problemPatterns).to.deep.equal([{ regexp: 'sample' }]);
743
+ expect(contributions.resourceLabelFormatters).to.deep.equal([{ scheme: 'file' }]);
744
+ expect(contributions.authentication).to.deep.equal([{ id: 'github', label: 'GitHub' }]);
745
+ expect(contributions.notebooks).to.deep.equal([{ type: 'sample-notebook', displayName: 'Notebook' }]);
746
+ expect(contributions.notebookRenderer).to.deep.equal([{ id: 'sample-renderer', displayName: 'Renderer' }]);
747
+ expect(contributions.notebookPreload).to.deep.equal([{ type: 'sample-preload' }]);
748
+ expect(contributions.snippets).to.deep.equal([{
749
+ language: 'typescript',
750
+ source: 'Orchestrator Extension',
751
+ uri: 'hostedPlugin/acme_orchestrator/snippets/ts.json'
752
+ }]);
753
+ expect(contributions.themes).to.deep.equal([{
754
+ id: 'dark',
755
+ label: 'Dark',
756
+ uri: 'hostedPlugin/acme_orchestrator/themes/dark.json',
757
+ description: undefined,
758
+ uiTheme: undefined
759
+ }]);
760
+ expect(contributions.colors).to.deep.equal([{
761
+ id: 'sample.color',
762
+ description: 'Sample',
763
+ defaults: { light: '#fff', dark: '#000', hc: '#111' }
764
+ }]);
765
+ expect(contributions.iconThemes).to.deep.equal([{
766
+ id: 'sample-icons',
767
+ label: 'Icons',
768
+ uri: 'hostedPlugin/acme_orchestrator/icons/theme.json',
769
+ description: undefined,
770
+ uiTheme: undefined
771
+ }]);
772
+ expect(contributions.icons).to.deep.equal([{
773
+ id: 'sample-icon',
774
+ extensionId: 'acme.orchestrator',
775
+ description: 'Sample icon',
776
+ defaults: { id: 'other-icon' }
777
+ }]);
778
+ expect(contributions.terminalProfiles).to.deep.equal([{ id: 'bash', title: 'Bash' }]);
779
+ expect(contributions.localizations).to.deep.equal([{
780
+ languageId: 'de',
781
+ languageName: 'German',
782
+ localizedLanguageName: undefined,
783
+ minimalTranslations: undefined,
784
+ translations: [{ id: 'package', path: './package.nls.de.json' }]
785
+ }]);
786
+ expect(contributions.languages).to.deep.equal([{
787
+ id: 'sample-lang',
788
+ aliases: undefined,
789
+ extensions: ['.sample'],
790
+ filenamePatterns: undefined,
791
+ filenames: undefined,
792
+ firstLine: undefined,
793
+ mimetypes: undefined,
794
+ icon: undefined
795
+ }]);
796
+ expect(contributions.grammars).to.deep.equal([{
797
+ language: 'sample-lang',
798
+ scope: 'source.sample',
799
+ format: 'json',
800
+ grammar: { scopeName: 'source.sample' },
801
+ grammarLocation: './grammar.json'
802
+ }]);
803
+ });
804
+
805
+ it('merges aliased view container locations and reports async reader failures', async () => {
806
+ const errors: Array<{ kind: string; message: unknown }> = [];
807
+ const ctx = createNormalizeCtx({
808
+ name: 'merge',
809
+ contributes: {
810
+ viewsContainers: {
811
+ activitybar: [{ id: 'left-a', title: 'A', icon: '$(a)' }],
812
+ secondarySidebar: [{ id: 'right-a', title: 'Right', icon: '$(b)' }]
813
+ },
814
+ languages: [{ id: 'broken', configuration: './missing.json' }],
815
+ grammars: [{ language: 'broken', scopeName: 'source.broken', path: './broken.json' }]
816
+ }
817
+ }, {
818
+ readJsonFile: async () => {
819
+ throw new Error('missing language config');
820
+ },
821
+ readGrammars: async () => {
822
+ throw new Error('missing grammar');
823
+ },
824
+ onError: (kind, message) => {
825
+ errors.push({ kind, message });
826
+ }
827
+ });
828
+
829
+ const contributions = await normalizeContributions(ctx, {}) as Record<string, unknown>;
830
+ expect(contributions.viewsContainers).to.deep.equal({
831
+ left: [{
832
+ id: 'left-a',
833
+ title: 'A',
834
+ iconUrl: 'hostedPlugin/acme_merge/%24(a)',
835
+ themeIcon: '$(a)',
836
+ when: undefined
837
+ }],
838
+ right: [{
839
+ id: 'right-a',
840
+ title: 'Right',
841
+ iconUrl: 'hostedPlugin/acme_merge/%24(b)',
842
+ themeIcon: '$(b)',
843
+ when: undefined
844
+ }]
845
+ });
846
+ expect(contributions).to.not.have.property('languages');
847
+ expect(contributions).to.not.have.property('grammars');
848
+ expect(errors.map(error => error.kind)).to.include.members(['languages', 'grammars']);
849
+ });
850
+
851
+ it('continues after reader errors and reports them', async () => {
852
+ const errors: Array<{ kind: string; message: unknown }> = [];
853
+ const ctx = createNormalizeCtx({
854
+ name: 'broken',
855
+ contributes: {
856
+ commands: [{ command: 'ok', title: 'OK' }],
857
+ configuration: { title: 'Broken' }
858
+ }
859
+ }, {
860
+ readConfiguration: () => {
861
+ throw new Error('boom');
862
+ },
863
+ onError: (kind, message) => {
864
+ errors.push({ kind, message });
865
+ }
866
+ });
867
+
868
+ const contributions = await normalizeContributions(ctx, {});
869
+ expect(contributions).to.have.property('commands').with.lengthOf(1);
870
+ expect(contributions).to.have.property('configuration').that.is.empty;
871
+ expect(errors).to.have.lengthOf(1);
872
+ expect(errors[0].kind).to.equal('configuration');
873
+ expect(errors[0].message).to.be.instanceOf(Error);
874
+ });
875
+ });
876
+
877
+ describe('readWalkthroughs', () => {
878
+ it('resolves media and plugin icon URLs for browser-only static hosting', async () => {
879
+ const ctx = createNormalizeCtx({
880
+ name: 'test-plugin',
881
+ publisher: 'test-publisher',
882
+ icon: 'images/icon.png',
883
+ contributes: {
884
+ walkthroughs: [{
885
+ id: 'getStarted',
886
+ title: 'Get Started',
887
+ description: 'Intro',
888
+ steps: [{
889
+ id: 'open',
890
+ title: 'Open a folder',
891
+ description: '',
892
+ media: { markdown: 'walkthroughs/open.md' }
893
+ }]
894
+ }]
895
+ }
896
+ });
897
+
898
+ const contributions = await normalizeContributions(ctx, {} as { walkthroughs?: WalkthroughContribution[] });
899
+ expect(contributions).to.have.property('walkthroughs').with.lengthOf(1);
900
+ const walkthrough = contributions.walkthroughs![0];
901
+ expect(walkthrough.pluginId).to.equal('test-publisher.test-plugin');
902
+ expect(walkthrough.pluginIcon).to.equal('hostedPlugin/test_publisher_test_plugin/images/icon.png');
903
+ expect(walkthrough.steps[0].media).to.deep.equal({
904
+ markdown: 'hostedPlugin/test_publisher_test_plugin/walkthroughs/open.md'
905
+ });
906
+ });
907
+
908
+ it('skips walkthroughs with a missing id', () => {
909
+ const warnings: string[] = [];
910
+ const ctx = createNormalizeCtx({
911
+ name: 'test-plugin',
912
+ publisher: 'test-publisher',
913
+ contributes: {
914
+ walkthroughs: [{
915
+ id: '',
916
+ title: 'Broken',
917
+ description: '',
918
+ steps: [{ id: 's1', title: 'S1', description: '' }]
919
+ }]
920
+ }
921
+ }, {
922
+ onWarn: msg => {
923
+ warnings.push(msg);
924
+ }
925
+ });
926
+
927
+ expect(readWalkthroughs(ctx.plugin, ctx)).to.be.undefined;
928
+ expect(warnings).to.have.lengthOf(1);
929
+ });
930
+ });
931
+ });