@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,126 @@
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 { isObject } from './utils';
18
+ import type { NormalizedPluginContribution, PluginManifestContribution } from './contribution-types';
19
+
20
+ export const PLUGIN_HOST_BACKEND = 'main' as const;
21
+
22
+ /** Whether a plugin is installed by the system or by a user. */
23
+ export enum PluginType {
24
+ System = 0,
25
+ User = 1
26
+ }
27
+
28
+ /** Same shape as `PluginPackage.capabilities` in `plugin-protocol.ts`. */
29
+ export interface PluginPackageCapabilities {
30
+ untrustedWorkspaces?: {
31
+ supported: boolean | 'limited';
32
+ description?: string;
33
+ restrictedConfigurations?: string[];
34
+ };
35
+ }
36
+
37
+ /**
38
+ * VS Code / Theia extension `package.json` manifest.
39
+ */
40
+ export interface PluginManifest {
41
+ name: string;
42
+ version: string;
43
+ publisher?: string;
44
+ displayName?: string;
45
+ description?: string;
46
+ l10n?: string;
47
+ packagePath: string;
48
+ packageUri?: string;
49
+ engines?: Record<string, string>;
50
+ theiaPlugin?: { frontend?: string; backend?: string; headless?: string };
51
+ main?: string;
52
+ browser?: string;
53
+ extensionKind?: Array<'ui' | 'workspace'>;
54
+ icon?: string;
55
+ type?: 'module' | 'commonjs';
56
+ extensionDependencies?: string[];
57
+ extensionPack?: string[];
58
+ capabilities?: PluginPackageCapabilities;
59
+ /** Raw `contributes` section from `package.json` before normalization. */
60
+ contributes?: PluginManifestContribution;
61
+ activationEvents?: string[];
62
+ }
63
+
64
+ export function rawContributes(manifest: Pick<PluginManifest, 'contributes'>): PluginManifestContribution {
65
+ return isObject(manifest.contributes) ? manifest.contributes as PluginManifestContribution : {};
66
+ }
67
+
68
+ export interface PluginEntryPoint {
69
+ frontend?: string;
70
+ backend?: string;
71
+ headless?: string;
72
+ }
73
+
74
+ /** Plugin model populated from `package.json`. */
75
+ export interface PluginModel {
76
+ id: string;
77
+ name: string;
78
+ publisher: string;
79
+ version: string;
80
+ displayName: string;
81
+ description: string;
82
+ engine: { type: string; version: string };
83
+ entryPoint: PluginEntryPoint;
84
+ packageUri: string;
85
+ /**
86
+ * @deprecated since 1.1.0 - because it lead to problems with getting a relative path
87
+ * needed by Icon Themes to correctly load Fonts, use packageUri instead.
88
+ */
89
+ packagePath: string;
90
+ iconUrl?: string;
91
+ l10n?: string;
92
+ readmeUrl?: string;
93
+ licenseUrl?: string;
94
+ untrustedWorkspacesSupport?: boolean | 'limited';
95
+ }
96
+
97
+ export interface PluginLifecycle {
98
+ startMethod: string;
99
+ stopMethod: string;
100
+ /** Frontend module name; the frontend plugin should expose this name. */
101
+ frontendModuleName?: string;
102
+ /** Script run before the frontend plugin is loaded. */
103
+ frontendInitPath?: string;
104
+ /** Script run before the backend plugin is loaded. */
105
+ backendInitPath?: string;
106
+ }
107
+
108
+ export interface PluginMetadata {
109
+ /** Plugin host id used for RPC routing (e.g. {@link PLUGIN_HOST_BACKEND}). */
110
+ host: string;
111
+ model: PluginModel;
112
+ lifecycle: PluginLifecycle;
113
+ outOfSync: boolean;
114
+ /** Set when the plugin is loaded from a development workspace (e.g. plugin-dev). */
115
+ isUnderDevelopment?: boolean;
116
+ }
117
+
118
+ /**
119
+ * Deployed plugin descriptor (runtime + browser-only `list.json`).
120
+ */
121
+ export interface DeployedPlugin<TContributes = NormalizedPluginContribution> {
122
+ /** Defaults to {@link PluginType.System}. */
123
+ type?: PluginType;
124
+ metadata: PluginMetadata;
125
+ contributes?: TContributes;
126
+ }
@@ -0,0 +1,122 @@
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 * as fs from 'fs-extra';
19
+ import * as os from 'os';
20
+ import * as path from 'path';
21
+ import { coerceLocalizations, localizePackage, localizeWithResolver } from './package-nls';
22
+ import { loadPackageTranslations } from '../node/package-nls';
23
+
24
+ describe('package-nls', () => {
25
+
26
+ describe('coerceLocalizations', () => {
27
+ it('passes through string values', () => {
28
+ expect(coerceLocalizations({ key: 'value' })).to.deep.equal({ key: 'value' });
29
+ });
30
+
31
+ it('extracts message from LocalizeInfo objects', () => {
32
+ expect(coerceLocalizations({ key: { message: 'Hello', comment: 'greeting' } })).to.deep.equal({ key: 'Hello' });
33
+ });
34
+
35
+ it('marks invalid values explicitly', () => {
36
+ expect(coerceLocalizations({ key: 42 as unknown as string })).to.deep.equal({ key: 'INVALID TRANSLATION VALUE' });
37
+ });
38
+ });
39
+
40
+ describe('localizeWithResolver', () => {
41
+ it('localizes nested structures and leaves unknown keys unchanged', () => {
42
+ const localized = localizeWithResolver(
43
+ {
44
+ title: '%title.key%',
45
+ nested: [{ label: '%nested.key%' }],
46
+ plain: 'unchanged'
47
+ },
48
+ key => (key === 'title.key' ? 'Title' : undefined)
49
+ );
50
+ expect(localized).to.deep.equal({
51
+ title: 'Title',
52
+ nested: [{ label: '%nested.key%' }],
53
+ plain: 'unchanged'
54
+ });
55
+ });
56
+
57
+ it('keeps %key% when resolve returns undefined or an empty string', () => {
58
+ expect(localizeWithResolver('%missing.key%', () => undefined)).to.equal('%missing.key%');
59
+ expect(localizeWithResolver('%empty.key%', () => '')).to.equal('%empty.key%');
60
+ });
61
+
62
+ it('ignores strings that are not %key% placeholders', () => {
63
+ expect(localizeWithResolver('%not-closed', () => 'x')).to.equal('%not-closed');
64
+ expect(localizeWithResolver('100%', () => 'x')).to.equal('100%');
65
+ });
66
+ });
67
+
68
+ describe('localizePackage', () => {
69
+ it('uses translation bundle before default bundle', () => {
70
+ const localized = localizePackage(
71
+ { displayName: '%name.key%' },
72
+ {
73
+ translation: { 'name.key': 'Translated' },
74
+ default: { 'name.key': 'Default' }
75
+ },
76
+ () => 'callback-should-not-run'
77
+ );
78
+ expect(localized).to.deep.equal({ displayName: 'Translated' });
79
+ });
80
+
81
+ it('invokes callback for default bundle when translation is missing', () => {
82
+ const localized = localizePackage(
83
+ { displayName: '%name.key%' },
84
+ { default: { 'name.key': 'Default value' } },
85
+ (_key, defaultValue) => `from-default:${defaultValue}`
86
+ );
87
+ expect(localized).to.deep.equal({ displayName: 'from-default:Default value' });
88
+ });
89
+ });
90
+
91
+ describe('loadPackageTranslations', () => {
92
+ let pluginRoot: string;
93
+
94
+ beforeEach(async () => {
95
+ pluginRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'package-nls-load-'));
96
+ });
97
+
98
+ afterEach(async () => {
99
+ await fs.remove(pluginRoot);
100
+ });
101
+
102
+ it('returns an empty object when package.nls.json is missing', async () => {
103
+ expect(await loadPackageTranslations(pluginRoot)).to.deep.equal({});
104
+ });
105
+
106
+ it('loads the default bundle without a locale', async () => {
107
+ await fs.writeJson(path.join(pluginRoot, 'package.nls.json'), { 'display.name': 'Sample' });
108
+ expect(await loadPackageTranslations(pluginRoot)).to.deep.equal({
109
+ default: { 'display.name': 'Sample' }
110
+ });
111
+ });
112
+
113
+ it('loads locale-specific translations when present', async () => {
114
+ await fs.writeJson(path.join(pluginRoot, 'package.nls.json'), { 'display.name': 'Sample' });
115
+ await fs.writeJson(path.join(pluginRoot, 'package.nls.de.json'), { 'display.name': 'Beispiel' });
116
+ expect(await loadPackageTranslations(pluginRoot, 'de')).to.deep.equal({
117
+ default: { 'display.name': 'Sample' },
118
+ translation: { 'display.name': 'Beispiel' }
119
+ });
120
+ });
121
+ });
122
+ });
@@ -0,0 +1,95 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2021 TypeFox 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 { isObject } from './utils';
18
+
19
+ export interface PackageTranslation {
20
+ translation?: Record<string, string>;
21
+ default?: Record<string, string>;
22
+ }
23
+
24
+ interface LocalizeInfo {
25
+ message: string;
26
+ comment?: string;
27
+ }
28
+
29
+ function isLocalizeInfo(obj: unknown): obj is LocalizeInfo {
30
+ return isObject(obj) && 'message' in obj && typeof (obj as { message?: unknown }).message === 'string';
31
+ }
32
+
33
+ export function coerceLocalizations(translations: Record<string, string | LocalizeInfo>): Record<string, string> {
34
+ const result: Record<string, string> = {};
35
+ for (const [key, value] of Object.entries(translations)) {
36
+ if (typeof value === 'string') {
37
+ result[key] = value;
38
+ } else if (isLocalizeInfo(value)) {
39
+ result[key] = value.message;
40
+ } else {
41
+ // Only strings or LocalizeInfo values are valid
42
+ result[key] = 'INVALID TRANSLATION VALUE';
43
+ }
44
+ }
45
+ return result;
46
+ }
47
+
48
+ /**
49
+ * Recursively walks `value` and replaces strings matching `%key%` with the result of `resolve(key)`.
50
+ * If `resolve(key)` returns `undefined` or an empty string, the original `%key%` is kept
51
+ * (same as the former worker `localize`: `translations[key] || value`).
52
+ */
53
+ export function localizeWithResolver<T>(value: T, resolve: (key: string) => string | undefined): T {
54
+ if (typeof value === 'string') {
55
+ if (value.length > 2 && value.startsWith('%') && value.endsWith('%')) {
56
+ const replaced = resolve(value.slice(1, -1));
57
+ return (replaced || value) as T;
58
+ }
59
+ return value;
60
+ }
61
+ if (Array.isArray(value)) {
62
+ return value.map(item => localizeWithResolver(item, resolve)) as T;
63
+ }
64
+ if (isObject(value)) {
65
+ const result: Record<string, unknown> = {};
66
+ for (const [name, item] of Object.entries(value)) {
67
+ result[name] = localizeWithResolver(item, resolve);
68
+ }
69
+ return result as T;
70
+ }
71
+ return value;
72
+ }
73
+
74
+ export type LocalizePackageCallback = (key: string, defaultValue: string) => string;
75
+
76
+ /**
77
+ * Recursively replaces %key% strings in value with translations.
78
+ * callback is used when only default (package.nls.json) exists; e.g. backend passes
79
+ * Theia's Localization.localize for fallback, build-time can use (_, d) => d.
80
+ */
81
+ export function localizePackage<T>(
82
+ value: T,
83
+ translations: PackageTranslation,
84
+ callback: LocalizePackageCallback
85
+ ): T {
86
+ return localizeWithResolver(value, key => {
87
+ if (translations.translation && key in translations.translation) {
88
+ return translations.translation[key];
89
+ }
90
+ if (translations.default && key in translations.default) {
91
+ return callback(key, translations.default[key]);
92
+ }
93
+ return undefined;
94
+ });
95
+ }
@@ -0,0 +1,104 @@
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 { updateActivationEvents } from './plugin-activation-events';
19
+ import type { PluginManifest } from './manifest-types';
20
+
21
+ function manifest(partial: Pick<PluginManifest, 'contributes' | 'activationEvents'>): Pick<PluginManifest, 'contributes' | 'activationEvents'> {
22
+ return {
23
+ activationEvents: partial.activationEvents ? [...partial.activationEvents] : undefined,
24
+ contributes: partial.contributes
25
+ };
26
+ }
27
+
28
+ describe('plugin-activation-events', () => {
29
+
30
+ it('no-ops when contributes is missing or invalid', () => {
31
+ const empty = manifest({});
32
+ updateActivationEvents(empty);
33
+ expect(empty.activationEvents).to.equal(undefined);
34
+
35
+ const invalid = manifest({ contributes: 'bad' as unknown as PluginManifest['contributes'] });
36
+ updateActivationEvents(invalid);
37
+ expect(invalid.activationEvents).to.equal(undefined);
38
+ });
39
+
40
+ it('derives activation events from contributed commands and languages', () => {
41
+ const pkg = manifest({
42
+ activationEvents: ['onStartupFinished'],
43
+ contributes: {
44
+ commands: [{ command: 'sample.run', title: 'Run' }],
45
+ languages: [{ id: 'typescript', extensions: ['.ts'] }]
46
+ }
47
+ });
48
+
49
+ updateActivationEvents(pkg);
50
+
51
+ expect(pkg.activationEvents).to.include.members([
52
+ 'onStartupFinished',
53
+ 'onCommand:sample.run',
54
+ 'onLanguage:typescript'
55
+ ]);
56
+ });
57
+
58
+ it('derives activation events from views, custom editors, auth, and notebooks', () => {
59
+ const pkg = manifest({
60
+ contributes: {
61
+ views: {
62
+ explorer: [{ id: 'sample.view', name: 'Sample' }]
63
+ },
64
+ customEditors: [{ viewType: 'sample.editor', displayName: 'Editor', selector: [] }],
65
+ authentication: [{ id: 'github', label: 'GitHub' }],
66
+ notebooks: [{ type: 'sample-notebook', displayName: 'Notebook' }]
67
+ }
68
+ });
69
+
70
+ updateActivationEvents(pkg);
71
+
72
+ expect(pkg.activationEvents).to.include.members([
73
+ 'onView:sample.view',
74
+ 'onCustomEditor:sample.editor',
75
+ 'onAuthenticationRequest:github',
76
+ 'onNotebookSerializer:sample-notebook'
77
+ ]);
78
+ });
79
+
80
+ it('supports a single command object instead of an array', () => {
81
+ const pkg = manifest({
82
+ contributes: {
83
+ commands: { command: 'solo.run', title: 'Solo' } as unknown as { command: string; title: string }
84
+ }
85
+ });
86
+
87
+ updateActivationEvents(pkg);
88
+
89
+ expect(pkg.activationEvents).to.include('onCommand:solo.run');
90
+ });
91
+
92
+ it('ignores malformed views contributions that are not plain objects', () => {
93
+ const pkg = manifest({
94
+ contributes: {
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
+ views: 'not-an-object' as any
97
+ }
98
+ });
99
+
100
+ updateActivationEvents(pkg);
101
+
102
+ expect(pkg.activationEvents ?? []).to.be.empty;
103
+ });
104
+ });
@@ -0,0 +1,102 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2023 TypeFox 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 { flatten, isObject, isStringArray } from './utils';
18
+ import { rawContributes, type PluginManifest } from './manifest-types';
19
+
20
+ /**
21
+ * Most activation events can be automatically deduced from the package manifest.
22
+ * This function will update the manifest based on the plugin contributions.
23
+ */
24
+ export function updateActivationEvents(manifest: Pick<PluginManifest, 'contributes' | 'activationEvents'>): void {
25
+ if (!isObject(manifest) || !isObject(manifest.contributes) || !manifest.contributes) {
26
+ return;
27
+ }
28
+
29
+ const activationEvents = new Set(isStringArray(manifest.activationEvents) ? manifest.activationEvents : []);
30
+ const contributes = rawContributes(manifest);
31
+
32
+ if (contributes.commands) {
33
+ const commands = Array.isArray(contributes.commands) ? contributes.commands : [contributes.commands];
34
+ updateCommandsContributions(commands, activationEvents);
35
+ }
36
+ if (isObject(contributes.views) && !Array.isArray(contributes.views)) {
37
+ const views = flatten(Object.values(contributes.views));
38
+ updateViewsContribution(views, activationEvents);
39
+ }
40
+ if (Array.isArray(contributes.customEditors)) {
41
+ updateCustomEditorsContribution(contributes.customEditors, activationEvents);
42
+ }
43
+ if (Array.isArray(contributes.authentication)) {
44
+ updateAuthenticationProviderContributions(contributes.authentication, activationEvents);
45
+ }
46
+ if (Array.isArray(contributes.languages)) {
47
+ updateLanguageContributions(contributes.languages, activationEvents);
48
+ }
49
+ if (Array.isArray(contributes.notebooks)) {
50
+ updateNotebookContributions(contributes.notebooks, activationEvents);
51
+ }
52
+
53
+ manifest.activationEvents = Array.from(activationEvents);
54
+ }
55
+
56
+ function updateViewsContribution(views: unknown[], activationEvents: Set<string>): void {
57
+ for (const view of views) {
58
+ if (isObject(view) && typeof view.id === 'string') {
59
+ activationEvents.add(`onView:${view.id}`);
60
+ }
61
+ }
62
+ }
63
+
64
+ function updateCustomEditorsContribution(customEditors: unknown[], activationEvents: Set<string>): void {
65
+ for (const customEditor of customEditors) {
66
+ if (isObject(customEditor) && typeof customEditor.viewType === 'string') {
67
+ activationEvents.add(`onCustomEditor:${customEditor.viewType}`);
68
+ }
69
+ }
70
+ }
71
+
72
+ function updateCommandsContributions(commands: unknown[], activationEvents: Set<string>): void {
73
+ for (const command of commands) {
74
+ if (isObject(command) && typeof command.command === 'string') {
75
+ activationEvents.add(`onCommand:${command.command}`);
76
+ }
77
+ }
78
+ }
79
+
80
+ function updateAuthenticationProviderContributions(authProviders: unknown[], activationEvents: Set<string>): void {
81
+ for (const authProvider of authProviders) {
82
+ if (isObject(authProvider) && typeof authProvider.id === 'string') {
83
+ activationEvents.add(`onAuthenticationRequest:${authProvider.id}`);
84
+ }
85
+ }
86
+ }
87
+
88
+ function updateLanguageContributions(languages: unknown[], activationEvents: Set<string>): void {
89
+ for (const language of languages) {
90
+ if (isObject(language) && typeof language.id === 'string') {
91
+ activationEvents.add(`onLanguage:${language.id}`);
92
+ }
93
+ }
94
+ }
95
+
96
+ function updateNotebookContributions(notebooks: unknown[], activationEvents: Set<string>): void {
97
+ for (const notebook of notebooks) {
98
+ if (isObject(notebook) && typeof notebook.type === 'string') {
99
+ activationEvents.add(`onNotebookSerializer:${notebook.type}`);
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,94 @@
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 { UNPUBLISHED, VSCODE_BUILTIN_NAME_PREFIX } from './constants';
19
+ import { prepareLoadedManifest, stripVscodeBuiltinNamePrefix } from './plugin-manifest';
20
+ import { loadManifest } from '../node/plugin-manifest';
21
+ import type { PluginManifest } from './manifest-types';
22
+ import * as fs from 'fs-extra';
23
+ import * as os from 'os';
24
+ import * as path from 'path';
25
+
26
+ describe('plugin-manifest', () => {
27
+
28
+ describe('stripVscodeBuiltinNamePrefix', () => {
29
+ it('removes the vscode builtin prefix from package names', () => {
30
+ const manifest: Pick<PluginManifest, 'name'> = {
31
+ name: `${VSCODE_BUILTIN_NAME_PREFIX}json`
32
+ };
33
+ stripVscodeBuiltinNamePrefix(manifest);
34
+ expect(manifest.name).to.equal('json');
35
+ });
36
+ });
37
+
38
+ describe('prepareLoadedManifest', () => {
39
+ it('defaults publisher and derives activation events', () => {
40
+ const manifest = prepareLoadedManifest({
41
+ name: 'sample',
42
+ version: '1.0.0',
43
+ packagePath: '/tmp/sample',
44
+ contributes: {
45
+ commands: [{ command: 'sample.run', title: 'Run' }]
46
+ }
47
+ } as PluginManifest);
48
+
49
+ expect(manifest.publisher).to.equal(UNPUBLISHED);
50
+ expect(manifest.activationEvents).to.include('onCommand:sample.run');
51
+ });
52
+
53
+ it('can skip activation event derivation', () => {
54
+ const manifest = prepareLoadedManifest({
55
+ name: 'sample',
56
+ version: '1.0.0',
57
+ packagePath: '/tmp/sample',
58
+ contributes: {
59
+ commands: [{ command: 'sample.run', title: 'Run' }]
60
+ }
61
+ } as PluginManifest, { updateActivationEvents: false });
62
+
63
+ expect(manifest.activationEvents).to.equal(undefined);
64
+ });
65
+ });
66
+
67
+ describe('loadManifest', () => {
68
+ let pluginRoot: string;
69
+
70
+ beforeEach(async () => {
71
+ pluginRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'plugin-manifest-'));
72
+ });
73
+
74
+ afterEach(async () => {
75
+ await fs.remove(pluginRoot);
76
+ });
77
+
78
+ it('reads and prepares package.json from disk', async () => {
79
+ await fs.writeJson(path.join(pluginRoot, 'package.json'), {
80
+ name: `${VSCODE_BUILTIN_NAME_PREFIX}json`,
81
+ version: '1.0.0',
82
+ contributes: {
83
+ commands: [{ command: 'json.format', title: 'Format JSON' }]
84
+ }
85
+ });
86
+
87
+ const manifest = await loadManifest(pluginRoot);
88
+
89
+ expect(manifest.name).to.equal('json');
90
+ expect(manifest.publisher).to.equal(UNPUBLISHED);
91
+ expect(manifest.activationEvents).to.include('onCommand:json.format');
92
+ });
93
+ });
94
+ });