@theia/application-manager 1.75.0-next.21 → 1.75.0-next.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/application-package-manager.d.ts +1 -0
- package/lib/application-package-manager.d.ts.map +1 -1
- package/lib/application-package-manager.js +9 -0
- package/lib/application-package-manager.js.map +1 -1
- package/lib/browser-only/prepare-browser-only-plugins.d.ts +12 -0
- package/lib/browser-only/prepare-browser-only-plugins.d.ts.map +1 -0
- package/lib/browser-only/prepare-browser-only-plugins.js +313 -0
- package/lib/browser-only/prepare-browser-only-plugins.js.map +1 -0
- package/lib/browser-only/prepare-browser-only-plugins.spec.d.ts +2 -0
- package/lib/browser-only/prepare-browser-only-plugins.spec.d.ts.map +1 -0
- package/lib/browser-only/prepare-browser-only-plugins.spec.js +136 -0
- package/lib/browser-only/prepare-browser-only-plugins.spec.js.map +1 -0
- package/lib/browser-only/write-browser-only-extensions-list.d.ts +7 -0
- package/lib/browser-only/write-browser-only-extensions-list.d.ts.map +1 -0
- package/lib/browser-only/write-browser-only-extensions-list.js +28 -0
- package/lib/browser-only/write-browser-only-extensions-list.js.map +1 -0
- package/package.json +6 -5
- package/src/application-package-manager.ts +11 -0
- package/src/browser-only/prepare-browser-only-plugins.spec.ts +169 -0
- package/src/browser-only/prepare-browser-only-plugins.ts +376 -0
- package/src/browser-only/write-browser-only-extensions-list.ts +27 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// *****************************************************************************
|
|
3
|
+
// Copyright (C) 2026 Maksim Kachurin and others.
|
|
4
|
+
//
|
|
5
|
+
// This program and the accompanying materials are made available under the
|
|
6
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
+
//
|
|
9
|
+
// This Source Code may also be made available under the following Secondary
|
|
10
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
11
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
12
|
+
// with the GNU Classpath Exception which is available at
|
|
13
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
|
+
// *****************************************************************************
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.writeBrowserOnlyExtensionsList = writeBrowserOnlyExtensionsList;
|
|
19
|
+
const fs = require("fs-extra");
|
|
20
|
+
/**
|
|
21
|
+
* Write `lib/frontend/extensions.json` (Theia extension packages) for the About dialog,
|
|
22
|
+
* matching the backend Theia extension build output.
|
|
23
|
+
*/
|
|
24
|
+
async function writeBrowserOnlyExtensionsList(applicationPackage) {
|
|
25
|
+
const extensions = applicationPackage.extensionPackages.map(({ name, version }) => ({ name, version }));
|
|
26
|
+
await fs.writeJson(applicationPackage.lib('frontend', 'extensions.json'), extensions, { spaces: 2 });
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=write-browser-only-extensions-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-browser-only-extensions-list.js","sourceRoot":"","sources":["../../src/browser-only/write-browser-only-extensions-list.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,iDAAiD;AACjD,EAAE;AACF,2EAA2E;AAC3E,mEAAmE;AACnE,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,uDAAuD;AACvD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;;AAShF,wEAGC;AAVD,+BAA+B;AAG/B;;;GAGG;AACI,KAAK,UAAU,8BAA8B,CAAC,kBAAsC;IACvF,MAAM,UAAU,GAAG,kBAAkB,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;IACxG,MAAM,EAAE,CAAC,SAAS,CAAC,kBAAkB,CAAC,GAAG,CAAC,UAAU,EAAE,iBAAiB,CAAC,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;AACzG,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theia/application-manager",
|
|
3
|
-
"version": "1.75.0-next.
|
|
3
|
+
"version": "1.75.0-next.28+ba1005c0bf8",
|
|
4
4
|
"description": "Theia application manager API.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -34,9 +34,10 @@
|
|
|
34
34
|
"@babel/plugin-transform-runtime": "^7.29.7",
|
|
35
35
|
"@babel/preset-env": "^7.29.7",
|
|
36
36
|
"@electron/rebuild": "^4.2.0",
|
|
37
|
-
"@theia/application-package": "1.75.0-next.
|
|
38
|
-
"@theia/bundle-plugin": "1.75.0-next.
|
|
39
|
-
"@theia/ffmpeg": "1.75.0-next.
|
|
37
|
+
"@theia/application-package": "1.75.0-next.28+ba1005c0bf8",
|
|
38
|
+
"@theia/bundle-plugin": "1.75.0-next.28+ba1005c0bf8",
|
|
39
|
+
"@theia/ffmpeg": "1.75.0-next.28+ba1005c0bf8",
|
|
40
|
+
"@theia/plugin-utils": "1.75.0-next.28+ba1005c0bf8",
|
|
40
41
|
"@types/fs-extra": "^4.0.15",
|
|
41
42
|
"@types/semver": "^7.8.0",
|
|
42
43
|
"babel-loader": "^8.4.1",
|
|
@@ -80,5 +81,5 @@
|
|
|
80
81
|
"nyc": {
|
|
81
82
|
"extends": "../../configs/nyc.json"
|
|
82
83
|
},
|
|
83
|
-
"gitHead": "
|
|
84
|
+
"gitHead": "ba1005c0bf87bd848c6ec51ce1b4e6bb06748e3c"
|
|
84
85
|
}
|
|
@@ -19,6 +19,8 @@ import * as fs from 'fs-extra';
|
|
|
19
19
|
import * as cp from 'child_process';
|
|
20
20
|
import * as semver from 'semver';
|
|
21
21
|
import { ApplicationPackage, ApplicationPackageOptions } from '@theia/application-package';
|
|
22
|
+
import { prepareBrowserOnlyPlugins } from './browser-only/prepare-browser-only-plugins';
|
|
23
|
+
import { writeBrowserOnlyExtensionsList } from './browser-only/write-browser-only-extensions-list';
|
|
22
24
|
import { BundlerGenerator, FrontendGenerator, BackendGenerator } from './generator';
|
|
23
25
|
import { ApplicationProcess } from './application-process';
|
|
24
26
|
import { GeneratorOptions } from './generator/abstract-generator';
|
|
@@ -111,6 +113,15 @@ export class ApplicationPackageManager {
|
|
|
111
113
|
async copy(): Promise<void> {
|
|
112
114
|
await fs.ensureDir(this.pck.lib('frontend'));
|
|
113
115
|
await fs.copy(this.pck.frontend('index.html'), this.pck.lib('frontend', 'index.html'));
|
|
116
|
+
|
|
117
|
+
if (this.pck.isBrowserOnly()) {
|
|
118
|
+
await this.prepareBrowserOnly();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
protected async prepareBrowserOnly(): Promise<void> {
|
|
123
|
+
await prepareBrowserOnlyPlugins(this.pck);
|
|
124
|
+
await writeBrowserOnlyExtensionsList(this.pck);
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
async build(args: string[] = [], options: GeneratorOptions = {}): Promise<void> {
|
|
@@ -0,0 +1,169 @@
|
|
|
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 { pathToFileURL } from 'url';
|
|
22
|
+
import { PLUGINS_BASE_PATH } from '@theia/plugin-utils/lib/common/constants';
|
|
23
|
+
import type { PluginManifest } from '@theia/plugin-utils/lib/common/manifest-types';
|
|
24
|
+
import {
|
|
25
|
+
resolvePluginEntryFileSync,
|
|
26
|
+
resolvePluginRoot,
|
|
27
|
+
shouldCopyPluginPath,
|
|
28
|
+
shouldIncludePluginInBrowserOnlyBuild,
|
|
29
|
+
getBrowserOnlySkipReason,
|
|
30
|
+
toHostedPluginUri,
|
|
31
|
+
} from './prepare-browser-only-plugins';
|
|
32
|
+
|
|
33
|
+
describe('prepare-browser-only-plugins helpers', () => {
|
|
34
|
+
|
|
35
|
+
let tmpRoot: string;
|
|
36
|
+
|
|
37
|
+
beforeEach(async () => {
|
|
38
|
+
tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'theia-prepare-plugins-'));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(async () => {
|
|
42
|
+
await fs.remove(tmpRoot);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('resolvePluginRoot', () => {
|
|
46
|
+
it('resolves root, extension/, and package/ layouts (root wins)', async () => {
|
|
47
|
+
await fs.writeJson(path.join(tmpRoot, 'package.json'), { name: 'direct' });
|
|
48
|
+
expect(resolvePluginRoot(tmpRoot)).to.equal(tmpRoot);
|
|
49
|
+
|
|
50
|
+
await fs.ensureDir(path.join(tmpRoot, 'extension'));
|
|
51
|
+
await fs.writeJson(path.join(tmpRoot, 'extension', 'package.json'), { name: 'nested' });
|
|
52
|
+
expect(resolvePluginRoot(tmpRoot)).to.equal(tmpRoot);
|
|
53
|
+
|
|
54
|
+
const onlyExt = path.join(tmpRoot, 'only-ext');
|
|
55
|
+
await fs.ensureDir(path.join(onlyExt, 'extension'));
|
|
56
|
+
await fs.writeJson(path.join(onlyExt, 'extension', 'package.json'), { name: 'ext' });
|
|
57
|
+
expect(resolvePluginRoot(onlyExt)).to.equal(path.join(onlyExt, 'extension'));
|
|
58
|
+
|
|
59
|
+
const onlyPkg = path.join(tmpRoot, 'only-pkg');
|
|
60
|
+
await fs.ensureDir(path.join(onlyPkg, 'package'));
|
|
61
|
+
await fs.writeJson(path.join(onlyPkg, 'package', 'package.json'), { name: 'pkg' });
|
|
62
|
+
expect(resolvePluginRoot(onlyPkg)).to.equal(path.join(onlyPkg, 'package'));
|
|
63
|
+
|
|
64
|
+
expect(resolvePluginRoot(path.join(tmpRoot, 'empty'))).to.equal(undefined);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('shouldIncludePluginInBrowserOnlyBuild', () => {
|
|
69
|
+
it('includes frontend/browser and declarative contributes; excludes backend-only even with contributes', () => {
|
|
70
|
+
expect(shouldIncludePluginInBrowserOnlyBuild({
|
|
71
|
+
name: 'fe', version: '1', packagePath: '', theiaPlugin: { frontend: 'lib/fe.js' },
|
|
72
|
+
})).to.equal(true);
|
|
73
|
+
expect(shouldIncludePluginInBrowserOnlyBuild({
|
|
74
|
+
name: 'br', version: '1', packagePath: '', browser: 'dist/br.js',
|
|
75
|
+
})).to.equal(true);
|
|
76
|
+
expect(shouldIncludePluginInBrowserOnlyBuild({
|
|
77
|
+
name: 'both', version: '1', packagePath: '', main: 'lib/node.js', browser: 'dist/br.js',
|
|
78
|
+
})).to.equal(true);
|
|
79
|
+
expect(shouldIncludePluginInBrowserOnlyBuild({
|
|
80
|
+
name: 'themes', version: '1', packagePath: '',
|
|
81
|
+
contributes: { themes: [{ path: './theme.json' }] },
|
|
82
|
+
} as PluginManifest)).to.equal(true);
|
|
83
|
+
|
|
84
|
+
expect(shouldIncludePluginInBrowserOnlyBuild({
|
|
85
|
+
name: '', version: '1', packagePath: '', browser: 'x.js',
|
|
86
|
+
})).to.equal(false);
|
|
87
|
+
expect(getBrowserOnlySkipReason({
|
|
88
|
+
name: '', version: '1', packagePath: '', browser: 'x.js',
|
|
89
|
+
})).to.equal('malformed');
|
|
90
|
+
expect(shouldIncludePluginInBrowserOnlyBuild({
|
|
91
|
+
name: 'be', version: '1', packagePath: '', theiaPlugin: { backend: 'lib/node.js' },
|
|
92
|
+
})).to.equal(false);
|
|
93
|
+
expect(shouldIncludePluginInBrowserOnlyBuild({
|
|
94
|
+
name: 'git', version: '1', packagePath: '', main: './out/main.js',
|
|
95
|
+
contributes: { commands: [{ command: 'git.status', title: 'Status' }] },
|
|
96
|
+
} as PluginManifest)).to.equal(false);
|
|
97
|
+
expect(shouldIncludePluginInBrowserOnlyBuild({
|
|
98
|
+
name: 'empty', version: '1', packagePath: '', contributes: {},
|
|
99
|
+
} as PluginManifest)).to.equal(false);
|
|
100
|
+
expect(getBrowserOnlySkipReason({
|
|
101
|
+
name: 'empty', version: '1', packagePath: '', contributes: {},
|
|
102
|
+
} as PluginManifest)).to.equal('no-browser-surface');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('resolvePluginEntryFileSync', () => {
|
|
107
|
+
it('resolves exact, extensionless, and .js→.cjs/.mjs fallbacks', async () => {
|
|
108
|
+
const exact = path.join(tmpRoot, 'main.js');
|
|
109
|
+
await fs.writeFile(exact, '');
|
|
110
|
+
expect(resolvePluginEntryFileSync(exact)).to.equal(exact);
|
|
111
|
+
|
|
112
|
+
const base = path.join(tmpRoot, 'entry');
|
|
113
|
+
await fs.writeFile(`${base}.js`, '');
|
|
114
|
+
expect(resolvePluginEntryFileSync(base)).to.equal(`${base}.js`);
|
|
115
|
+
|
|
116
|
+
const bothJs = path.join(tmpRoot, 'both.js');
|
|
117
|
+
const bothCjs = path.join(tmpRoot, 'both.cjs');
|
|
118
|
+
await fs.writeFile(bothJs, '');
|
|
119
|
+
await fs.writeFile(bothCjs, '');
|
|
120
|
+
expect(resolvePluginEntryFileSync(bothJs)).to.equal(bothJs);
|
|
121
|
+
|
|
122
|
+
const onlyCjs = path.join(tmpRoot, 'cjs-only.js');
|
|
123
|
+
await fs.writeFile(path.join(tmpRoot, 'cjs-only.cjs'), '');
|
|
124
|
+
expect(resolvePluginEntryFileSync(onlyCjs)).to.equal(path.join(tmpRoot, 'cjs-only.cjs'));
|
|
125
|
+
|
|
126
|
+
const onlyMjs = path.join(tmpRoot, 'mjs-only.js');
|
|
127
|
+
await fs.writeFile(path.join(tmpRoot, 'mjs-only.mjs'), '');
|
|
128
|
+
expect(resolvePluginEntryFileSync(onlyMjs)).to.equal(path.join(tmpRoot, 'mjs-only.mjs'));
|
|
129
|
+
|
|
130
|
+
expect(resolvePluginEntryFileSync(path.join(tmpRoot, 'missing.js'))).to.equal(undefined);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('toHostedPluginUri', () => {
|
|
135
|
+
it('rewrites file URIs under the plugin root; leaves others alone', () => {
|
|
136
|
+
const pluginRoot = path.join(tmpRoot, 'my-plugin');
|
|
137
|
+
const nested = path.join(pluginRoot, 'media', 'icon.png');
|
|
138
|
+
expect(toHostedPluginUri(pathToFileURL(nested).href, pluginRoot, 'id')).to.equal(
|
|
139
|
+
`${PLUGINS_BASE_PATH}/id/media/icon.png`
|
|
140
|
+
);
|
|
141
|
+
expect(toHostedPluginUri(pathToFileURL(pluginRoot).href, pluginRoot, 'id')).to.equal(
|
|
142
|
+
`${PLUGINS_BASE_PATH}/id/`
|
|
143
|
+
);
|
|
144
|
+
expect(toHostedPluginUri('https://example.com/icon.png', pluginRoot, 'id')).to.equal(
|
|
145
|
+
'https://example.com/icon.png'
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const outside = path.join(tmpRoot, 'other', 'license.txt');
|
|
149
|
+
const outsideUri = pathToFileURL(outside).href;
|
|
150
|
+
expect(toHostedPluginUri(outsideUri, pluginRoot, 'id')).to.equal(outsideUri);
|
|
151
|
+
|
|
152
|
+
// Sibling prefix must not match (`/plugin` vs `/plugin-evil`).
|
|
153
|
+
const sibling = path.join(tmpRoot, 'my-plugin-evil', 'readme.md');
|
|
154
|
+
const siblingUri = pathToFileURL(sibling).href;
|
|
155
|
+
expect(toHostedPluginUri(siblingUri, pluginRoot, 'id')).to.equal(siblingUri);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('shouldCopyPluginPath', () => {
|
|
160
|
+
it('allows plugin files and skips nested node_modules/.git even when root sits under node_modules', () => {
|
|
161
|
+
// Mimics post-realpath absolute paths under a workspace node_modules tree.
|
|
162
|
+
const pluginRoot = path.join(tmpRoot, 'node_modules', 'hosted', 'my-plugin');
|
|
163
|
+
expect(shouldCopyPluginPath(path.join(pluginRoot, 'dist', 'ext.js'), pluginRoot)).to.equal(true);
|
|
164
|
+
expect(shouldCopyPluginPath(pluginRoot, pluginRoot)).to.equal(true);
|
|
165
|
+
expect(shouldCopyPluginPath(path.join(pluginRoot, 'node_modules', 'dep', 'index.js'), pluginRoot)).to.equal(false);
|
|
166
|
+
expect(shouldCopyPluginPath(path.join(pluginRoot, '.git', 'config'), pluginRoot)).to.equal(false);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,376 @@
|
|
|
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 { realpath } from 'fs/promises';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
import * as fs from 'fs-extra';
|
|
21
|
+
import { ApplicationPackage } from '@theia/application-package';
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_PLUGINS_DIR,
|
|
24
|
+
LIST_JSON,
|
|
25
|
+
PLUGINS_BASE_PATH,
|
|
26
|
+
PLUGIN_COPY_IGNORE,
|
|
27
|
+
UNPUBLISHED,
|
|
28
|
+
} from '@theia/plugin-utils/lib/common/constants';
|
|
29
|
+
import { stripVscodeBuiltinNamePrefix } from '@theia/plugin-utils/lib/common/plugin-manifest';
|
|
30
|
+
import { updateActivationEvents } from '@theia/plugin-utils/lib/common/plugin-activation-events';
|
|
31
|
+
import {
|
|
32
|
+
applyTrustExtraction,
|
|
33
|
+
buildLifecycle,
|
|
34
|
+
buildModel,
|
|
35
|
+
getPluginId,
|
|
36
|
+
pickEngineType,
|
|
37
|
+
toPluginUrl
|
|
38
|
+
} from '@theia/plugin-utils/lib/common/plugin-model';
|
|
39
|
+
import { getPluginRootFileUrl } from '@theia/plugin-utils/lib/node/plugin-model';
|
|
40
|
+
import { normalizeContributions } from '@theia/plugin-utils/lib/node/normalize-contributions';
|
|
41
|
+
import { readGrammarFromDisk } from '@theia/plugin-utils/lib/node/read-grammars';
|
|
42
|
+
import { localizePackage } from '@theia/plugin-utils/lib/common/package-nls';
|
|
43
|
+
import { loadPackageTranslations } from '@theia/plugin-utils/lib/node/package-nls';
|
|
44
|
+
import { deepClone } from '@theia/plugin-utils/lib/common/utils';
|
|
45
|
+
import type {
|
|
46
|
+
NormalizedPluginContribution,
|
|
47
|
+
} from '@theia/plugin-utils/lib/common/contribution-types';
|
|
48
|
+
|
|
49
|
+
import {
|
|
50
|
+
PLUGIN_HOST_BACKEND,
|
|
51
|
+
PluginType,
|
|
52
|
+
type DeployedPlugin,
|
|
53
|
+
type PluginEntryPoint,
|
|
54
|
+
type PluginManifest,
|
|
55
|
+
type PluginModel,
|
|
56
|
+
} from '@theia/plugin-utils/lib/common/manifest-types';
|
|
57
|
+
|
|
58
|
+
export async function prepareBrowserOnlyPlugins(applicationPackage: ApplicationPackage): Promise<void> {
|
|
59
|
+
const hostedPluginDir = applicationPackage.lib('frontend', PLUGINS_BASE_PATH);
|
|
60
|
+
await fs.remove(hostedPluginDir);
|
|
61
|
+
await fs.ensureDir(hostedPluginDir);
|
|
62
|
+
|
|
63
|
+
const theiaPluginsDir = applicationPackage.pck.theiaPluginsDir;
|
|
64
|
+
const pluginsDir = path.resolve(
|
|
65
|
+
applicationPackage.projectPath,
|
|
66
|
+
typeof theiaPluginsDir === 'string' ? theiaPluginsDir : DEFAULT_PLUGINS_DIR
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const deployedPlugins: DeployedPlugin[] = [];
|
|
70
|
+
const skippedByReason: Partial<Record<NonNullable<ProcessPluginResult['skipReason']>, string[]>> = {};
|
|
71
|
+
const errored: string[] = [];
|
|
72
|
+
|
|
73
|
+
if (await fs.pathExists(pluginsDir)) {
|
|
74
|
+
const names = await fs.readdir(pluginsDir);
|
|
75
|
+
for (const name of names) {
|
|
76
|
+
const result = await processPlugin(path.join(pluginsDir, name), hostedPluginDir);
|
|
77
|
+
if (result?.plugin) {
|
|
78
|
+
deployedPlugins.push(result.plugin);
|
|
79
|
+
} else if (result?.skipReason) {
|
|
80
|
+
(skippedByReason[result.skipReason] ??= []).push(result.label ?? name);
|
|
81
|
+
} else {
|
|
82
|
+
errored.push(result?.label ?? name);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await fs.writeJson(path.join(hostedPluginDir, LIST_JSON), deployedPlugins);
|
|
88
|
+
|
|
89
|
+
const lines = [`browser-only: prepared ${deployedPlugins.length} plugins`];
|
|
90
|
+
for (const [reason, labels] of Object.entries(skippedByReason)) {
|
|
91
|
+
if (labels?.length) {
|
|
92
|
+
lines.push(` skipped ${labels.length} ${reason}:`);
|
|
93
|
+
lines.push(` ${labels.join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (errored.length > 0) {
|
|
97
|
+
lines.push(` ${errored.length} errors:`);
|
|
98
|
+
lines.push(` ${errored.join(', ')}`);
|
|
99
|
+
}
|
|
100
|
+
console.log(lines.join('\n'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface ProcessPluginResult {
|
|
104
|
+
plugin?: DeployedPlugin;
|
|
105
|
+
/** Set when the plugin was intentionally omitted (not a hard error). */
|
|
106
|
+
skipReason?: 'backend-only' | 'no-browser-surface' | 'no-package' | 'malformed';
|
|
107
|
+
/** Plugin id/name or source directory basename */
|
|
108
|
+
label?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function processPlugin(pluginSourceDir: string, hostedPluginDir: string): Promise<ProcessPluginResult | undefined> {
|
|
112
|
+
const dirLabel = path.basename(pluginSourceDir);
|
|
113
|
+
try {
|
|
114
|
+
const packageRoot = resolvePluginRoot(pluginSourceDir);
|
|
115
|
+
if (!packageRoot) {
|
|
116
|
+
return { skipReason: 'no-package', label: dirLabel };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const buildTimePackageRoot = await realpath(packageRoot);
|
|
120
|
+
const rawManifest = await fs.readJson(path.join(buildTimePackageRoot, 'package.json')) as PluginManifest;
|
|
121
|
+
stripVscodeBuiltinNamePrefix(rawManifest);
|
|
122
|
+
|
|
123
|
+
const skipReason = getBrowserOnlySkipReason(rawManifest);
|
|
124
|
+
if (skipReason) {
|
|
125
|
+
const label = rawManifest.name || dirLabel;
|
|
126
|
+
if (skipReason === 'backend-only') {
|
|
127
|
+
console.warn(`browser-only: skip '${label}' (backend-only)`);
|
|
128
|
+
} else if (skipReason === 'malformed') {
|
|
129
|
+
console.warn(`browser-only: skip '${dirLabel}' (malformed manifest: missing name)`);
|
|
130
|
+
}
|
|
131
|
+
return { skipReason, label };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let normalized = deepClone(rawManifest);
|
|
135
|
+
normalized.packagePath = buildTimePackageRoot;
|
|
136
|
+
let contributes = await normalizeManifestForBrowserOnly(normalized);
|
|
137
|
+
|
|
138
|
+
delete normalized.contributes;
|
|
139
|
+
|
|
140
|
+
const translations = await loadPackageTranslations(buildTimePackageRoot);
|
|
141
|
+
if (translations.default && Object.keys(translations.default).length > 0) {
|
|
142
|
+
const resolve = (_: string, defaultVal: string): string => defaultVal;
|
|
143
|
+
normalized = localizePackage(normalized, translations, resolve);
|
|
144
|
+
contributes = localizePackage(contributes, translations, resolve);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const engineType = pickEngineType(normalized);
|
|
148
|
+
const model = buildModel(normalized, engineType, { uiKind: 'web' });
|
|
149
|
+
model.licenseUrl = getPluginRootFileUrl(normalized, ['license', 'license.txt', 'license.md']);
|
|
150
|
+
model.readmeUrl = getPluginRootFileUrl(normalized, ['readme.md', 'readme.txt', 'readme']);
|
|
151
|
+
applyTrustExtraction(normalized, model);
|
|
152
|
+
const lifecycle = buildLifecycle(normalized, engineType);
|
|
153
|
+
|
|
154
|
+
const pluginId = getPluginId(normalized);
|
|
155
|
+
const dst = path.join(hostedPluginDir, pluginId);
|
|
156
|
+
|
|
157
|
+
await fs.copy(buildTimePackageRoot, dst, {
|
|
158
|
+
overwrite: true,
|
|
159
|
+
dereference: true,
|
|
160
|
+
filter: (src: string) => shouldCopyPluginPath(src, buildTimePackageRoot)
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
resolveHostedEntryPoint(model.entryPoint, dst);
|
|
164
|
+
rewriteModelPathsForHostedStatic(model, buildTimePackageRoot, pluginId);
|
|
165
|
+
|
|
166
|
+
// Raw VS Code-style package.json for worker rawModel (contributes stay unnormalized).
|
|
167
|
+
const diskManifest = deepClone(rawManifest);
|
|
168
|
+
prepareHostedPackageJson(diskManifest, pluginId, model.entryPoint);
|
|
169
|
+
await fs.writeJson(path.join(dst, 'package.json'), diskManifest, { spaces: 2 });
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
plugin: {
|
|
173
|
+
type: PluginType.System,
|
|
174
|
+
metadata: {
|
|
175
|
+
host: PLUGIN_HOST_BACKEND,
|
|
176
|
+
model,
|
|
177
|
+
lifecycle,
|
|
178
|
+
outOfSync: false
|
|
179
|
+
},
|
|
180
|
+
...(Object.keys(contributes).length > 0 ? { contributes } : {})
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.warn(`browser-only: skip plugin at ${pluginSourceDir}`, err);
|
|
185
|
+
return { label: dirLabel };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Copy filter: ignore node_modules/.git relative to the plugin root (not absolute path). */
|
|
190
|
+
export function shouldCopyPluginPath(src: string, pluginRoot: string): boolean {
|
|
191
|
+
return !PLUGIN_COPY_IGNORE.test(`${path.sep}${path.relative(pluginRoot, src)}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function resolvePluginRoot(dir: string): string | undefined {
|
|
195
|
+
const direct = path.join(dir, 'package.json');
|
|
196
|
+
if (fs.pathExistsSync(direct)) {
|
|
197
|
+
return dir;
|
|
198
|
+
}
|
|
199
|
+
const inExtension = path.join(dir, 'extension', 'package.json');
|
|
200
|
+
if (fs.pathExistsSync(inExtension)) {
|
|
201
|
+
return path.join(dir, 'extension');
|
|
202
|
+
}
|
|
203
|
+
const inPackage = path.join(dir, 'package', 'package.json');
|
|
204
|
+
if (fs.pathExistsSync(inPackage)) {
|
|
205
|
+
return path.join(dir, 'package');
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function hasContributes(pkg: PluginManifest): boolean {
|
|
211
|
+
const c = pkg.contributes;
|
|
212
|
+
return !!(c && typeof c === 'object' && Object.keys(c).length > 0);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function hasFrontendEntry(manifest: PluginManifest): boolean {
|
|
216
|
+
return !!manifest.theiaPlugin?.frontend || !!manifest.browser;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function hasBackendEntry(manifest: PluginManifest): boolean {
|
|
220
|
+
return !!manifest.main || !!manifest.theiaPlugin?.backend || !!manifest.theiaPlugin?.headless;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** `undefined` = include; backend-only even with contributes is excluded. */
|
|
224
|
+
export function getBrowserOnlySkipReason(
|
|
225
|
+
manifest: PluginManifest
|
|
226
|
+
): 'backend-only' | 'no-browser-surface' | 'malformed' | undefined {
|
|
227
|
+
if (!manifest.name) {
|
|
228
|
+
return 'malformed';
|
|
229
|
+
}
|
|
230
|
+
if (hasFrontendEntry(manifest)) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
if (hasBackendEntry(manifest)) {
|
|
234
|
+
return 'backend-only';
|
|
235
|
+
}
|
|
236
|
+
if (hasContributes(manifest)) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
return 'no-browser-surface';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function shouldIncludePluginInBrowserOnlyBuild(manifest: PluginManifest): boolean {
|
|
243
|
+
return getBrowserOnlySkipReason(manifest) === undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Drop Node/Electron-only entry fields shared by list.json and on-disk package.json. */
|
|
247
|
+
function stripNonFrontendHostFields(manifest: PluginManifest): void {
|
|
248
|
+
manifest.publisher ??= UNPUBLISHED;
|
|
249
|
+
delete manifest.main;
|
|
250
|
+
if (manifest.theiaPlugin) {
|
|
251
|
+
delete manifest.theiaPlugin.backend;
|
|
252
|
+
delete manifest.theiaPlugin.headless;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function normalizePluginPackageForBrowserOnly(manifest: PluginManifest): void {
|
|
257
|
+
stripNonFrontendHostFields(manifest);
|
|
258
|
+
|
|
259
|
+
if (!manifest.engines) {
|
|
260
|
+
manifest.engines = { theiaPlugin: '*' };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!manifest.theiaPlugin && manifest.browser) {
|
|
264
|
+
manifest.theiaPlugin = { frontend: manifest.browser };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function normalizeManifestForBrowserOnly(manifest: PluginManifest): Promise<NormalizedPluginContribution> {
|
|
269
|
+
normalizePluginPackageForBrowserOnly(manifest);
|
|
270
|
+
updateActivationEvents(manifest);
|
|
271
|
+
|
|
272
|
+
const contributes: NormalizedPluginContribution = {};
|
|
273
|
+
const onError = (type: string, err: unknown, detail?: unknown): void => {
|
|
274
|
+
console.warn(`browser-only: [${manifest.name}] contribution '${type}'`, detail, err);
|
|
275
|
+
};
|
|
276
|
+
const onWarn = (msg: string): void => {
|
|
277
|
+
console.warn(`browser-only: [${manifest.name}] ${msg}`);
|
|
278
|
+
};
|
|
279
|
+
await normalizeContributions({
|
|
280
|
+
plugin: manifest,
|
|
281
|
+
resolveUrl: relative => toPluginUrl(manifest, relative),
|
|
282
|
+
resolveUri: (pck, relative) => toPluginUrl(pck, relative),
|
|
283
|
+
readGrammars: async (grammars, pluginPath) => {
|
|
284
|
+
const result = [];
|
|
285
|
+
for (const rawGrammar of grammars) {
|
|
286
|
+
const grammar = await readGrammarFromDisk(rawGrammar, pluginPath, { onError });
|
|
287
|
+
if (grammar) {
|
|
288
|
+
result.push(grammar);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
},
|
|
293
|
+
onError,
|
|
294
|
+
onWarn,
|
|
295
|
+
}, contributes);
|
|
296
|
+
|
|
297
|
+
if (manifest.activationEvents?.length) {
|
|
298
|
+
contributes.activationEvents = [...manifest.activationEvents];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return contributes;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* `list.json` carries normalized contributes + plugin metadata (single source of truth for Theia).
|
|
306
|
+
* `hostedPlugin/<id>/package.json` stays a VS Code-style raw manifest for worker `rawModel`:
|
|
307
|
+
* name-prefix strip, `main` removed, entry paths synced, and `packagePath`/`packageUri` set to the
|
|
308
|
+
* static hosted root (needed so relative assets resolve via `toPluginUrl`).
|
|
309
|
+
*/
|
|
310
|
+
function prepareHostedPackageJson(manifest: PluginManifest, pluginId: string, entryPoint: PluginEntryPoint): void {
|
|
311
|
+
stripNonFrontendHostFields(manifest);
|
|
312
|
+
|
|
313
|
+
const packageRoot = `${PLUGINS_BASE_PATH}/${pluginId}/`;
|
|
314
|
+
manifest.packagePath = packageRoot;
|
|
315
|
+
manifest.packageUri = packageRoot;
|
|
316
|
+
|
|
317
|
+
if (entryPoint.frontend) {
|
|
318
|
+
if (manifest.theiaPlugin) {
|
|
319
|
+
manifest.theiaPlugin.frontend = entryPoint.frontend;
|
|
320
|
+
}
|
|
321
|
+
if (manifest.browser) {
|
|
322
|
+
manifest.browser = entryPoint.frontend;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function resolveHostedEntryPoint(entryPoint: PluginEntryPoint, pluginRoot: string): void {
|
|
328
|
+
if (!entryPoint.frontend) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const absoluteEntry = path.resolve(pluginRoot, entryPoint.frontend);
|
|
332
|
+
const resolved = resolvePluginEntryFileSync(absoluteEntry);
|
|
333
|
+
if (resolved) {
|
|
334
|
+
entryPoint.frontend = path.relative(pluginRoot, resolved).split(path.sep).join('/');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function rewriteModelPathsForHostedStatic(model: PluginModel, buildTimePackageRoot: string, pluginId: string): void {
|
|
339
|
+
model.packageUri = toHostedPluginUri(model.packageUri, buildTimePackageRoot, pluginId);
|
|
340
|
+
model.packagePath = model.packageUri;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function resolvePluginEntryFileSync(absolutePath: string): string | undefined {
|
|
344
|
+
const candidates = [absolutePath];
|
|
345
|
+
const pathExtension = path.extname(absolutePath).toLowerCase();
|
|
346
|
+
if (!pathExtension) {
|
|
347
|
+
candidates.push(absolutePath + '.js');
|
|
348
|
+
} else if (pathExtension === '.js') {
|
|
349
|
+
candidates.push(absolutePath.replace(/\.js$/i, '.cjs'));
|
|
350
|
+
candidates.push(absolutePath.replace(/\.js$/i, '.mjs'));
|
|
351
|
+
}
|
|
352
|
+
for (const candidate of candidates) {
|
|
353
|
+
if (fs.pathExistsSync(candidate)) {
|
|
354
|
+
return candidate;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function toHostedPluginUri(fileUri: string, pluginRoot: string, pluginId: string): string {
|
|
361
|
+
if (!fileUri.startsWith('file://')) {
|
|
362
|
+
return fileUri;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
const filePath = fileURLToPath(fileUri);
|
|
366
|
+
const normalizedRoot = path.resolve(pluginRoot);
|
|
367
|
+
const normalizedPath = path.resolve(filePath);
|
|
368
|
+
if (!normalizedPath.startsWith(normalizedRoot + path.sep) && normalizedPath !== normalizedRoot) {
|
|
369
|
+
return fileUri;
|
|
370
|
+
}
|
|
371
|
+
const relative = path.relative(normalizedRoot, normalizedPath);
|
|
372
|
+
return `${PLUGINS_BASE_PATH}/${pluginId}/${relative.split(path.sep).join('/')}`;
|
|
373
|
+
} catch {
|
|
374
|
+
return fileUri;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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 * as fs from 'fs-extra';
|
|
18
|
+
import { ApplicationPackage } from '@theia/application-package';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Write `lib/frontend/extensions.json` (Theia extension packages) for the About dialog,
|
|
22
|
+
* matching the backend Theia extension build output.
|
|
23
|
+
*/
|
|
24
|
+
export async function writeBrowserOnlyExtensionsList(applicationPackage: ApplicationPackage): Promise<void> {
|
|
25
|
+
const extensions = applicationPackage.extensionPackages.map(({ name, version }) => ({ name, version }));
|
|
26
|
+
await fs.writeJson(applicationPackage.lib('frontend', 'extensions.json'), extensions, { spaces: 2 });
|
|
27
|
+
}
|