@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,44 @@
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 { UNPUBLISHED, VSCODE_BUILTIN_NAME_PREFIX } from './constants';
18
+ import type { PluginManifest } from './manifest-types';
19
+ import { updateActivationEvents } from './plugin-activation-events';
20
+
21
+ export interface PrepareLoadedManifestOptions {
22
+ /** @default true */
23
+ updateActivationEvents?: boolean;
24
+ }
25
+
26
+ type LoadedManifest = Pick<PluginManifest, 'name' | 'publisher' | 'contributes' | 'activationEvents'>;
27
+
28
+ export function stripVscodeBuiltinNamePrefix(manifest: Pick<PluginManifest, 'name'>): void {
29
+ if (manifest?.name?.startsWith(VSCODE_BUILTIN_NAME_PREFIX)) {
30
+ manifest.name = manifest.name.substring(VSCODE_BUILTIN_NAME_PREFIX.length);
31
+ }
32
+ }
33
+
34
+ export function prepareLoadedManifest<T extends LoadedManifest>(
35
+ manifest: T,
36
+ options?: PrepareLoadedManifestOptions
37
+ ): T {
38
+ stripVscodeBuiltinNamePrefix(manifest);
39
+ manifest.publisher ??= UNPUBLISHED;
40
+ if (options?.updateActivationEvents !== false) {
41
+ updateActivationEvents(manifest);
42
+ }
43
+ return manifest;
44
+ }
@@ -0,0 +1,246 @@
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';
19
+ import * as os from 'os';
20
+ import * as path from 'path';
21
+ import {
22
+ UNPUBLISHED,
23
+ VSCODE_EXTENSION_ACTIVATE,
24
+ VSCODE_EXTENSION_DEACTIVATE,
25
+ VSCODE_FRONTEND_INIT,
26
+ THEIA_PLUGIN_START_METHOD,
27
+ THEIA_PLUGIN_STOP_METHOD
28
+ } from './constants';
29
+ import {
30
+ applyTrustExtraction,
31
+ buildEntryPointForTheia,
32
+ buildEntryPointForVsCode,
33
+ buildLifecycle,
34
+ buildModel,
35
+ buildModelForTheia,
36
+ buildModelForVsCode,
37
+ getPluginId,
38
+ pickEngineType,
39
+ toPluginUrl
40
+ } from './plugin-model';
41
+ import { getPluginRootFileUrl } from '../node/plugin-model';
42
+ import { manifest } from './test-helpers';
43
+
44
+ describe('plugin-model', () => {
45
+
46
+ describe('getPluginId', () => {
47
+ it('sanitizes publisher and name', () => {
48
+ expect(getPluginId({ publisher: 'dbaeumer', name: 'vscode-eslint' })).to.equal('dbaeumer_vscode_eslint');
49
+ });
50
+
51
+ it('uses unpublished publisher when publisher is missing', () => {
52
+ expect(getPluginId({ name: 'my-ext' })).to.equal(getPluginId({ publisher: UNPUBLISHED, name: 'my-ext' }));
53
+ });
54
+ });
55
+
56
+ describe('toPluginUrl', () => {
57
+ it('encodes each path segment and keeps separators for static hosting', () => {
58
+ const url = toPluginUrl({ publisher: 'vscode', name: 'theme-monokai' }, './themes/monokai-color-theme.json');
59
+ expect(url).to.equal('hostedPlugin/vscode_theme_monokai/themes/monokai-color-theme.json');
60
+ });
61
+
62
+ it('encodes special characters inside a segment', () => {
63
+ expect(toPluginUrl({ publisher: 'acme', name: 'ext' }, 'media/my icon.png'))
64
+ .to.equal('hostedPlugin/acme_ext/media/my%20icon.png');
65
+ expect(toPluginUrl({ publisher: 'acme', name: 'ext' }, '$(folder)'))
66
+ .to.equal('hostedPlugin/acme_ext/%24(folder)');
67
+ });
68
+
69
+ it('resolves .. within the plugin root and ignores leading ..', () => {
70
+ expect(toPluginUrl({ publisher: 'acme', name: 'ext' }, 'themes/../icons/a.png'))
71
+ .to.equal('hostedPlugin/acme_ext/icons/a.png');
72
+ expect(toPluginUrl({ publisher: 'acme', name: 'ext' }, '../secret.json'))
73
+ .to.equal('hostedPlugin/acme_ext/secret.json');
74
+ });
75
+ });
76
+
77
+ describe('pickEngineType', () => {
78
+ it('prefers theiaPlugin over vscode when both are declared', () => {
79
+ expect(pickEngineType(manifest({
80
+ name: 'both-engines',
81
+ engines: { vscode: '^1.0.0', theiaPlugin: '*' }
82
+ }))).to.equal('theiaPlugin');
83
+ });
84
+
85
+ it('returns vscode when only vscode engine is declared', () => {
86
+ expect(pickEngineType(manifest({
87
+ name: 'vscode-only',
88
+ engines: { vscode: '^1.0.0' }
89
+ }))).to.equal('vscode');
90
+ });
91
+
92
+ it('throws when no supported engine is declared', () => {
93
+ expect(() => pickEngineType(manifest({ name: 'no-engine' }))).to.throw(/No vscode or theiaPlugin engine/);
94
+ });
95
+ });
96
+
97
+ describe('applyTrustExtraction', () => {
98
+ it('copies untrusted workspace support when declared', () => {
99
+ const result: { untrustedWorkspacesSupport?: boolean | 'limited' } = {};
100
+ applyTrustExtraction(manifest({
101
+ name: 'trusted',
102
+ capabilities: { untrustedWorkspaces: { supported: 'limited' } }
103
+ }), result);
104
+ expect(result.untrustedWorkspacesSupport).to.equal('limited');
105
+ });
106
+
107
+ it('leaves result unchanged when capabilities are absent', () => {
108
+ const result: { untrustedWorkspacesSupport?: boolean | 'limited' } = {};
109
+ applyTrustExtraction(manifest({ name: 'plain' }), result);
110
+ expect(result.untrustedWorkspacesSupport).to.equal(undefined);
111
+ });
112
+ });
113
+
114
+ describe('getPluginRootFileUrl', () => {
115
+ let tmpDir: string;
116
+
117
+ before(() => {
118
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'plugin-utils-root-'));
119
+ fs.writeFileSync(path.join(tmpDir, 'README.md'), '# Readme');
120
+ });
121
+
122
+ after(() => {
123
+ fs.rmSync(tmpDir, { recursive: true, force: true });
124
+ });
125
+
126
+ it('finds readme files case-insensitively', () => {
127
+ const url = getPluginRootFileUrl(manifest({ name: 'docs', packagePath: tmpDir }), ['readme.md']);
128
+ expect(url).to.equal('hostedPlugin/acme_docs/README.md');
129
+ });
130
+
131
+ it('returns undefined when directory is missing', () => {
132
+ expect(getPluginRootFileUrl(manifest({ name: 'missing', packagePath: '/no/such/path' }), ['readme.md'])).to.equal(undefined);
133
+ });
134
+ });
135
+
136
+ describe('buildEntryPointForTheia', () => {
137
+ it('maps theiaPlugin entry points and headless script', () => {
138
+ const entryPoint = buildEntryPointForTheia(manifest({
139
+ name: 'theia-ext',
140
+ theiaPlugin: {
141
+ frontend: './lib/browser/main.js',
142
+ backend: './lib/node/main.js',
143
+ headless: './lib/headless.js'
144
+ }
145
+ }));
146
+ expect(entryPoint).to.deep.equal({
147
+ frontend: './lib/browser/main.js',
148
+ backend: './lib/node/main.js',
149
+ headless: './lib/headless.js'
150
+ });
151
+ });
152
+ });
153
+
154
+ describe('buildEntryPointForVsCode', () => {
155
+ it('prefers browser entry for ui-only extensions in web mode', () => {
156
+ const entryPoint = buildEntryPointForVsCode(manifest({
157
+ name: 'ui-extension',
158
+ browser: './dist/browser/extension.js',
159
+ main: './dist/node/extension.js',
160
+ extensionKind: ['ui']
161
+ }), { uiKind: 'web' });
162
+ expect(entryPoint.frontend).to.equal('./dist/browser/extension.js');
163
+ expect(entryPoint.backend).to.equal(undefined);
164
+ });
165
+
166
+ it('uses main as backend when browser is absent', () => {
167
+ const entryPoint = buildEntryPointForVsCode(manifest({
168
+ name: 'workspace-extension',
169
+ main: './dist/node/extension.js'
170
+ }), { uiKind: 'web' });
171
+ expect(entryPoint.backend).to.equal('./dist/node/extension.js');
172
+ expect(entryPoint.frontend).to.equal(undefined);
173
+ });
174
+
175
+ it('prefers backend when both main and browser exist without ui-only web hint', () => {
176
+ const entryPoint = buildEntryPointForVsCode(manifest({
177
+ name: 'hybrid',
178
+ browser: './dist/browser/extension.js',
179
+ main: './dist/node/extension.js',
180
+ extensionKind: ['workspace', 'ui']
181
+ }), { uiKind: 'web' });
182
+ expect(entryPoint.backend).to.equal('./dist/node/extension.js');
183
+ expect(entryPoint.frontend).to.equal(undefined);
184
+ });
185
+
186
+ it('forwards headless script from theiaPlugin section', () => {
187
+ const entryPoint = buildEntryPointForVsCode(manifest({
188
+ name: 'headless',
189
+ main: './main.js',
190
+ theiaPlugin: { headless: './headless.js' }
191
+ }));
192
+ expect(entryPoint.headless).to.equal('./headless.js');
193
+ });
194
+ });
195
+
196
+ describe('buildModel', () => {
197
+ it('builds theia plugin model with defaults', () => {
198
+ const model = buildModelForTheia(manifest({
199
+ name: 'Sample',
200
+ publisher: 'Acme',
201
+ packageUri: 'hostedPlugin/acme_Sample/'
202
+ }));
203
+ expect(model.id).to.equal('acme.sample');
204
+ expect(model.engine).to.deep.equal({ type: 'theiaPlugin', version: '*' });
205
+ expect(model.displayName).to.equal('Test Extension');
206
+ expect(model.packageUri).to.equal('hostedPlugin/acme_Sample/');
207
+ });
208
+
209
+ it('builds vscode model with icon url and web entry point', () => {
210
+ const model = buildModelForVsCode(manifest({
211
+ name: 'icon-ext',
212
+ icon: './media/icon.png',
213
+ browser: './browser.js',
214
+ extensionKind: ['ui'],
215
+ engines: { vscode: '^1.90.0' }
216
+ }), { uiKind: 'web' });
217
+ expect(model.engine).to.deep.equal({ type: 'vscode', version: '^1.90.0' });
218
+ expect(model.iconUrl).to.equal('hostedPlugin/acme_icon_ext/media/icon.png');
219
+ expect(model.entryPoint.frontend).to.equal('./browser.js');
220
+ });
221
+
222
+ it('uses UNPUBLISHED publisher and displayName fallback', () => {
223
+ const model = buildModel(manifest({ name: 'anon', publisher: undefined, displayName: undefined }), 'vscode');
224
+ expect(model.publisher).to.equal(UNPUBLISHED);
225
+ expect(model.displayName).to.equal('anon');
226
+ });
227
+ });
228
+
229
+ describe('buildLifecycle', () => {
230
+ it('uses theia start/stop for theiaPlugin engine', () => {
231
+ const lifecycle = buildLifecycle(manifest({ name: 'theia', publisher: 'acme' }), 'theiaPlugin');
232
+ expect(lifecycle.startMethod).to.equal(THEIA_PLUGIN_START_METHOD);
233
+ expect(lifecycle.stopMethod).to.equal(THEIA_PLUGIN_STOP_METHOD);
234
+ expect(lifecycle.frontendModuleName).to.equal('acme_theia');
235
+ expect(lifecycle.frontendInitPath).to.equal(undefined);
236
+ });
237
+
238
+ it('uses vscode activate/deactivate lifecycle', () => {
239
+ const lifecycle = buildLifecycle(manifest({ name: 'sample', publisher: 'acme' }), 'vscode');
240
+ expect(lifecycle.startMethod).to.equal(VSCODE_EXTENSION_ACTIVATE);
241
+ expect(lifecycle.stopMethod).to.equal(VSCODE_EXTENSION_DEACTIVATE);
242
+ expect(lifecycle.frontendInitPath).to.equal(VSCODE_FRONTEND_INIT);
243
+ expect(lifecycle.frontendModuleName).to.equal('acme_sample');
244
+ });
245
+ });
246
+ });
@@ -0,0 +1,194 @@
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 {
18
+ PLUGINS_BASE_PATH,
19
+ THEIA_PLUGIN_START_METHOD,
20
+ THEIA_PLUGIN_STOP_METHOD,
21
+ UNPUBLISHED,
22
+ VSCODE_EXTENSION_ACTIVATE,
23
+ VSCODE_EXTENSION_DEACTIVATE,
24
+ VSCODE_FRONTEND_INIT
25
+ } from './constants';
26
+ import type {
27
+ PluginEntryPoint,
28
+ PluginLifecycle,
29
+ PluginModel,
30
+ PluginManifest,
31
+ PluginPackageCapabilities
32
+ } from './manifest-types';
33
+
34
+ export interface PluginIdentifierSource {
35
+ publisher?: string;
36
+ name: string;
37
+ }
38
+
39
+ export interface VsCodeBuildOptions {
40
+ /** When `'web'`, UI-only extensions prefer the browser entry point (Codespaces behavior). */
41
+ uiKind?: 'web' | 'desktop';
42
+ }
43
+
44
+ export function getPluginId(plugin: PluginIdentifierSource): string {
45
+ return `${plugin.publisher ?? UNPUBLISHED}_${plugin.name}`.replace(/\W/g, '_');
46
+ }
47
+
48
+ /**
49
+ * Builds a static-friendly plugin asset URL: `hostedPlugin/<id>/<segments...>`.
50
+ * Path separators stay literal `/`; each segment is `encodeURIComponent`'d so plain
51
+ * static file servers (browser-only) resolve assets without needing Express `:path(*)` decoding.
52
+ * `.` is dropped and `..` is resolved within the plugin root (leading `..` is ignored).
53
+ */
54
+ export function toPluginUrl(pck: PluginIdentifierSource, relativePath: string): string {
55
+ const segments: string[] = [];
56
+ for (const segment of relativePath.replace(/\\/g, '/').split('/')) {
57
+ if (!segment || segment === '.') {
58
+ continue;
59
+ }
60
+ if (segment === '..') {
61
+ // Drop parent segment when present; ignore leading `..` so URLs never contain
62
+ // literal `..` (encodeURIComponent does not encode dots).
63
+ segments.pop();
64
+ continue;
65
+ }
66
+ segments.push(encodeURIComponent(segment));
67
+ }
68
+ return `${PLUGINS_BASE_PATH}/${getPluginId(pck)}/${segments.join('/')}`;
69
+ }
70
+
71
+ /**
72
+ * Explicit `theiaPlugin` engine takes priority over `vscode`.
73
+ */
74
+ export function pickEngineType(manifest: PluginManifest): 'theiaPlugin' | 'vscode' {
75
+ if (manifest.engines?.theiaPlugin !== undefined) {
76
+ return 'theiaPlugin';
77
+ }
78
+ if (manifest.engines?.vscode !== undefined) {
79
+ return 'vscode';
80
+ }
81
+ throw new Error(`No vscode or theiaPlugin engine in ${manifest.name}`);
82
+ }
83
+
84
+ export function applyTrustExtraction(
85
+ manifest: { capabilities?: PluginPackageCapabilities },
86
+ result: { untrustedWorkspacesSupport?: boolean | 'limited' }
87
+ ): void {
88
+ const untrustedWorkspacesSupport = manifest.capabilities?.untrustedWorkspaces?.supported;
89
+ if (untrustedWorkspacesSupport !== undefined) {
90
+ result.untrustedWorkspacesSupport = untrustedWorkspacesSupport;
91
+ }
92
+ }
93
+
94
+ /** Minimal `pathToFileURL(...).href` without importing Node's `url` (keeps this module worker-safe). */
95
+ function pathToFileUrlHref(absolutePath: string): string {
96
+ const normalized = absolutePath.replace(/\\/g, '/');
97
+ if (/^[a-zA-Z]:\//.test(normalized)) {
98
+ return `file:///${normalized}`;
99
+ }
100
+ return `file://${normalized.startsWith('/') ? normalized : `/${normalized}`}`;
101
+ }
102
+
103
+ export function buildEntryPointForTheia(manifest: PluginManifest): PluginEntryPoint {
104
+ const result: PluginEntryPoint = {
105
+ frontend: manifest.theiaPlugin?.frontend,
106
+ backend: manifest.theiaPlugin?.backend
107
+ };
108
+ if (manifest.theiaPlugin?.headless) {
109
+ result.headless = manifest.theiaPlugin.headless;
110
+ }
111
+ return result;
112
+ }
113
+
114
+ export function buildEntryPointForVsCode(manifest: PluginManifest, options?: VsCodeBuildOptions): PluginEntryPoint {
115
+ const entryPoint: PluginEntryPoint = {};
116
+ const preferFrontend = options?.uiKind === 'web'
117
+ && manifest.extensionKind?.length === 1
118
+ && manifest.extensionKind[0] === 'ui';
119
+
120
+ if (manifest.browser && (!manifest.main || preferFrontend)) {
121
+ entryPoint.frontend = manifest.browser;
122
+ } else {
123
+ entryPoint.backend = manifest.main;
124
+ }
125
+ if (manifest.theiaPlugin?.headless) {
126
+ entryPoint.headless = manifest.theiaPlugin.headless;
127
+ }
128
+ return entryPoint;
129
+ }
130
+
131
+ export function buildModelForTheia(manifest: PluginManifest): PluginModel {
132
+ const publisher = manifest.publisher ?? UNPUBLISHED;
133
+ const packageUri = manifest.packageUri ?? pathToFileUrlHref(manifest.packagePath);
134
+
135
+ return {
136
+ packagePath: manifest.packagePath,
137
+ packageUri,
138
+ id: `${publisher.toLowerCase()}.${manifest.name.toLowerCase()}`,
139
+ name: manifest.name,
140
+ publisher,
141
+ version: manifest.version,
142
+ displayName: manifest.displayName ?? manifest.name,
143
+ description: manifest.description ?? '',
144
+ l10n: manifest.l10n,
145
+ engine: {
146
+ type: 'theiaPlugin',
147
+ version: manifest.engines?.theiaPlugin ?? '*'
148
+ },
149
+ entryPoint: buildEntryPointForTheia(manifest),
150
+ };
151
+ }
152
+
153
+ export function buildModelForVsCode(manifest: PluginManifest, options?: VsCodeBuildOptions): PluginModel {
154
+ const publisher = manifest.publisher ?? UNPUBLISHED;
155
+ const packageUri = manifest.packageUri ?? pathToFileUrlHref(manifest.packagePath);
156
+ return {
157
+ packagePath: manifest.packagePath,
158
+ packageUri,
159
+ id: `${publisher.toLowerCase()}.${manifest.name.toLowerCase()}`,
160
+ name: manifest.name,
161
+ publisher,
162
+ version: manifest.version,
163
+ displayName: manifest.displayName ?? manifest.name,
164
+ description: manifest.description ?? '',
165
+ l10n: manifest.l10n,
166
+ engine: {
167
+ type: 'vscode',
168
+ version: manifest.engines?.vscode ?? '*'
169
+ },
170
+ entryPoint: buildEntryPointForVsCode(manifest, options),
171
+ iconUrl: manifest.icon ? toPluginUrl(manifest, manifest.icon) : undefined,
172
+ };
173
+ }
174
+
175
+ export function buildModel(manifest: PluginManifest, engineType: 'theiaPlugin' | 'vscode', options?: VsCodeBuildOptions): PluginModel {
176
+ return engineType === 'theiaPlugin' ? buildModelForTheia(manifest) : buildModelForVsCode(manifest, options);
177
+ }
178
+
179
+ export function buildLifecycle(manifest: PluginManifest, engineType: 'theiaPlugin' | 'vscode'): PluginLifecycle {
180
+ const frontendModuleName = getPluginId(manifest);
181
+ if (engineType === 'theiaPlugin') {
182
+ return {
183
+ startMethod: THEIA_PLUGIN_START_METHOD,
184
+ stopMethod: THEIA_PLUGIN_STOP_METHOD,
185
+ frontendModuleName,
186
+ };
187
+ }
188
+ return {
189
+ startMethod: VSCODE_EXTENSION_ACTIVATE,
190
+ stopMethod: VSCODE_EXTENSION_DEACTIVATE,
191
+ frontendModuleName,
192
+ frontendInitPath: VSCODE_FRONTEND_INIT
193
+ };
194
+ }
@@ -0,0 +1,148 @@
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
+ /**
18
+ * Structural subset of `@theia/core` / `@theia/task` types used by normalize-contributions.
19
+ * Keep in sync with:
20
+ * `packages/core/src/common/json-schema.ts`,
21
+ * `packages/core/src/common/preferences/preference-schema.ts`,
22
+ * `packages/core/src/common/color.ts`,
23
+ * `packages/task/src/common/task-protocol.ts`.
24
+ */
25
+
26
+ export type JSONPrimitive = boolean | number | string | null;
27
+
28
+ export type JSONValue = JSONPrimitive | JSONObject | JSONValue[];
29
+
30
+ export interface JSONObject {
31
+ [key: string]: JSONValue;
32
+ }
33
+
34
+ export type JsonType = 'string' | 'array' | 'number' | 'integer' | 'object' | 'boolean' | 'null';
35
+
36
+ export interface IJSONSchema {
37
+ id?: string;
38
+ $id?: string;
39
+ $schema?: string;
40
+ type?: JsonType | JsonType[];
41
+ owner?: string;
42
+ group?: string;
43
+ title?: string;
44
+ default?: JSONValue;
45
+ definitions?: IJSONSchemaMap;
46
+ description?: string;
47
+ properties?: IJSONSchemaMap;
48
+ patternProperties?: IJSONSchemaMap;
49
+ additionalProperties?: boolean | IJSONSchema;
50
+ minProperties?: number;
51
+ maxProperties?: number;
52
+ dependencies?: IJSONSchemaMap | { [prop: string]: string[] };
53
+ items?: IJSONSchema | IJSONSchema[];
54
+ prefixItems?: IJSONSchema[];
55
+ minItems?: number;
56
+ maxItems?: number;
57
+ uniqueItems?: boolean;
58
+ additionalItems?: boolean | IJSONSchema;
59
+ pattern?: string;
60
+ minLength?: number;
61
+ maxLength?: number;
62
+ minimum?: number;
63
+ maximum?: number;
64
+ exclusiveMinimum?: boolean | number;
65
+ exclusiveMaximum?: boolean | number;
66
+ multipleOf?: number;
67
+ required?: string[];
68
+ $ref?: string;
69
+ anyOf?: IJSONSchema[];
70
+ allOf?: IJSONSchema[];
71
+ oneOf?: IJSONSchema[];
72
+ not?: IJSONSchema;
73
+ enum?: JSONValue[];
74
+ format?: string;
75
+ const?: JSONValue;
76
+ contains?: IJSONSchema;
77
+ propertyNames?: IJSONSchema;
78
+ $comment?: string;
79
+ if?: IJSONSchema;
80
+ then?: IJSONSchema;
81
+ else?: IJSONSchema;
82
+ defaultSnippets?: IJSONSchemaSnippet[];
83
+ errorMessage?: string;
84
+ patternErrorMessage?: string;
85
+ deprecationMessage?: string;
86
+ markdownDeprecationMessage?: string;
87
+ enumItemLabels?: string[];
88
+ enumDescriptions?: string[];
89
+ markdownEnumDescriptions?: string[];
90
+ markdownDescription?: string;
91
+ doNotSuggest?: boolean;
92
+ allowComments?: boolean;
93
+ allowTrailingCommas?: boolean;
94
+ }
95
+
96
+ export interface IJSONSchemaMap {
97
+ [name: string]: IJSONSchema;
98
+ }
99
+
100
+ export interface IJSONSchemaSnippet {
101
+ label?: string;
102
+ description?: string;
103
+ body?: JSONValue;
104
+ bodyText?: string;
105
+ }
106
+
107
+ /** Keep numeric values aligned with `PreferenceScope` in `@theia/core`. */
108
+ export enum PreferenceScope {
109
+ Default = 0,
110
+ User = 1,
111
+ Workspace = 2,
112
+ Folder = 3,
113
+ }
114
+
115
+ export interface PreferenceDataProperty extends IJSONSchema {
116
+ overridable?: boolean;
117
+ included?: boolean;
118
+ hidden?: boolean;
119
+ scope?: PreferenceScope;
120
+ typeDetails?: unknown;
121
+ tags?: string[];
122
+ }
123
+
124
+ export interface PreferenceSchema {
125
+ scope?: PreferenceScope;
126
+ title?: string;
127
+ defaultOverridable?: boolean;
128
+ properties: { [name: string]: PreferenceDataProperty };
129
+ }
130
+
131
+ export interface ColorDefinition {
132
+ id: string;
133
+ defaults?: { light: string; dark: string; hc: string };
134
+ description: string;
135
+ }
136
+
137
+ export interface TaskDefinition {
138
+ taskType: string;
139
+ source: string;
140
+ properties: {
141
+ required?: string[];
142
+ all: string[];
143
+ schema: IJSONSchema;
144
+ };
145
+ }
146
+
147
+ /** Same segment as `CSSIcon.iconNameSegment` in `@theia/core`. */
148
+ export const ICON_NAME_SEGMENT = '[A-Za-z0-9]+';
@@ -0,0 +1,44 @@
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 type { NormalizeContributionsContext } from './contribution-types';
18
+ import type { PluginManifest } from './manifest-types';
19
+ import { toPluginUrl } from './plugin-model';
20
+
21
+ export function manifest(partial: Partial<PluginManifest> & Pick<PluginManifest, 'name'>): PluginManifest {
22
+ return {
23
+ version: '1.0.0',
24
+ publisher: 'acme',
25
+ packagePath: '/tmp/plugin',
26
+ displayName: 'Test Extension',
27
+ ...partial
28
+ };
29
+ }
30
+
31
+ export function createNormalizeCtx(
32
+ pluginOverrides?: Partial<PluginManifest>,
33
+ ctxOverrides?: Partial<NormalizeContributionsContext>
34
+ ): NormalizeContributionsContext {
35
+ const plugin = manifest({ name: 'test-ext', ...pluginOverrides });
36
+ return {
37
+ plugin,
38
+ resolveUrl: relativePath => toPluginUrl(plugin, relativePath),
39
+ resolveUri: (pck, relativePath) => toPluginUrl(pck, relativePath),
40
+ onError: () => undefined,
41
+ onWarn: () => undefined,
42
+ ...ctxOverrides
43
+ };
44
+ }