Skip to content

Commit b2601cb

Browse files
author
winjo
committed
feat(cli): 支持加载公网和本地调试扩展
1 parent 3bcb730 commit b2601cb

36 files changed

Lines changed: 1172 additions & 599 deletions

packages/alex/src/api/createApp.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { EXT_WORKER_HOST, WEBVIEW_ENDPOINT } from '../core/env';
3434

3535
export { SlotLocation, SlotRenderer, BoxPanel, SplitPanel };
3636

37-
const getDefaultAppConfig = (): IAppOpts => ({
37+
export const getDefaultAppConfig = (): IAppOpts => ({
3838
modules,
3939
useCdnIcon: true,
4040
noExtHost: true,

packages/alex/src/api/exports.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { REPORT_NAME, BrowserFSFileType } from '@alipay/alex-core';
1+
import { REPORT_NAME, BrowserFSFileType, HOME_ROOT, WORKSPACE_ROOT } from '@alipay/alex-core';
22

33
export { getDefaultLayoutConfig } from '../core/layout';
44

5-
export { REPORT_NAME, BrowserFSFileType };
5+
export { REPORT_NAME, BrowserFSFileType, HOME_ROOT, WORKSPACE_ROOT };
66

77
export * from '../core/env';

packages/alex/src/api/register.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { LanguagesContribution, GrammarsContribution } from '@ali/ide-monaco';
2-
import { centerRegistry } from '@alipay/alex-registry';
2+
import { Registry } from '@alipay/alex-registry';
33

4-
export { centerRegistry };
4+
export { Registry };
55

66
/**
77
* @deprecated please import language by path directly
@@ -10,7 +10,7 @@ export { centerRegistry };
1010
* use `import "@alipay/alex/languages"` to import all languages
1111
*/
1212
export const registerLanguage = (contrib: LanguagesContribution) => {
13-
centerRegistry.register<LanguagesContribution>('language', contrib);
13+
Registry.register<LanguagesContribution>('language', contrib);
1414
};
1515

1616
/**
@@ -20,5 +20,5 @@ export const registerLanguage = (contrib: LanguagesContribution) => {
2020
* use `import "@alipay/alex/languages"` to import all languages
2121
*/
2222
export const registerGrammar = (contrib: GrammarsContribution) => {
23-
centerRegistry.register<GrammarsContribution>('grammar', contrib);
23+
Registry.register<GrammarsContribution>('grammar', contrib);
2424
};

packages/cli/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
"@ali/ide-extension-installer": "^2.0.0",
2222
"@ali/ide-kaitian-extension": "2.2.0",
2323
"@alipay/alex-shared": "1.0.0-rc.2",
24-
"commander": "^6.2.0",
24+
"commander": "^7.2.0",
2525
"fs-extra": "^9.0.1",
2626
"lodash.pick": "^4.4.0",
27+
"portfinder": "^1.0.28",
2728
"rxjs": "^6.6.3",
2829
"semver": "^7.3.2",
30+
"send": "^0.17.1",
2931
"signale": "^1.4.0",
3032
"tslib": "^2.2.0"
3133
},
@@ -38,6 +40,7 @@
3840
"@types/fs-extra": "^9.0.4",
3941
"@types/lodash.pick": "^4.4.6",
4042
"@types/semver": "^7.3.4",
43+
"@types/send": "^0.14.7",
4144
"@types/signale": "^1.4.1"
4245
}
4346
}

packages/cli/src/commander.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import program from 'commander';
2-
import { install, uninstall } from './extension';
1+
import program, { Option } from 'commander';
2+
import { install, uninstall, installLocalExtensions } from './extension';
33
import { log } from './util/log';
44

55
program.version(`alex ${require('../package').version}`).usage('<command> [options]');
@@ -33,11 +33,12 @@ version can be ignored, then will use latest version under current kaitian frame
3333
extensionProgram
3434
.command('install <extensions...>')
3535
.alias('i')
36+
.addOption(new Option('-m, --mode [type]', 'extension env mode').choices(['internal', 'public']))
3637
.description(
3738
'install single or multiple extension, eg. kaitian.ide-dark-theme, kaitian.ide-dark-theme@2.0.0'
3839
)
39-
.action((extensions: string[]) => {
40-
install(extensions).catch((err) => console.error(err));
40+
.action((extensions: string[], options) => {
41+
install(extensions, options).catch((err) => console.error(err));
4142
});
4243

4344
extensionProgram
@@ -47,4 +48,12 @@ extensionProgram
4748
uninstall(extensions).catch((err) => console.error(err));
4849
});
4950

51+
extensionProgram
52+
.command('link <extensionDirs...>')
53+
.description('link local extension for dev')
54+
.option('-h, --host', 'local extension static file service host, default: `localhost`')
55+
.action((extensionDirs, options) => {
56+
installLocalExtensions(extensionDirs, options).catch((err) => console.error(err));
57+
});
58+
5059
program.parse(process.argv);

packages/cli/src/extension.ts

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
11
import * as path from 'path';
2+
import * as os from 'os';
23
import { ExtensionInstaller, Extension } from '@ali/ide-extension-installer';
34
import * as fse from 'fs-extra';
45
import { from, of } from 'rxjs';
5-
import { mergeMap, filter } from 'rxjs/operators';
6+
import { mergeMap, filter, map } from 'rxjs/operators';
7+
import { IExtensionMode } from '@alipay/alex-shared';
68
import { EXTENSION_DIR, EXTENSION_METADATA_DIR, EXTENSION_FIELD } from './util/constant';
79
import { getExtension } from './extension/scanner';
8-
import { IExtensionBasicMetadata, IExtensionDesc, IExtensionIdentity } from './extension/type';
10+
import {
11+
IExtensionBasicMetadata,
12+
IExtensionDesc,
13+
IExtensionIdentity,
14+
IExtensionServerOptions,
15+
} from './extension/type';
916
import { log, error } from './util/log';
1017
import checkFramework from './util/check-framework';
18+
import { createServer, getHttpUri } from './util/serve-file';
1119
import { formatExtension } from './util';
1220
import { createMetadataType } from './extension/metadata-type';
1321

1422
let extensionInstaller: ExtensionInstaller;
1523
let shouldWriteConfig = false;
1624

17-
export const install = async (extensionId?: string[], options?: { silent: boolean }) => {
25+
export const install = async (
26+
extensionId?: string[],
27+
options?: { silent: boolean; mode?: 'public' | 'internal' }
28+
) => {
1829
checkFramework();
1930

2031
createInstaller();
2132

2233
let extensions: IExtensionDesc[] = [];
2334

2435
if (extensionId?.length) {
25-
extensions = parseExtensionId(extensionId);
36+
extensions = parseExtensionId(extensionId, options?.mode);
2637
shouldWriteConfig = true;
2738
await Promise.all(extensions.map((ext) => removeExtensionById(ext)));
2839
} else {
@@ -49,8 +60,12 @@ export const install = async (extensionId?: string[], options?: { silent: boolea
4960
from(extensions)
5061
.pipe(
5162
mergeMap(installExtension, 5), // 限制并发数 5
52-
mergeMap((extPath) => (Array.isArray(extPath) ? from(extPath) : of(extPath))),
53-
mergeMap(getExtension, 5),
63+
mergeMap(([extPath, mode]) =>
64+
Array.isArray(extPath)
65+
? from(extPath.map((p) => [p, mode] as const))
66+
: of([extPath, mode] as const)
67+
),
68+
mergeMap(([extPath, mode]) => getExtension(extPath, mode), 5),
5469
filter((data) => !!data),
5570
mergeMap(writeMetadata)
5671
)
@@ -72,6 +87,53 @@ export const install = async (extensionId?: string[], options?: { silent: boolea
7287
);
7388
};
7489

90+
/**
91+
* 从本地安装扩展
92+
* @param dirs 扩展目录
93+
*/
94+
export const installLocalExtensions = async (dirs: string[], options?: IExtensionServerOptions) => {
95+
checkFramework();
96+
97+
if (!dirs.length) {
98+
return;
99+
}
100+
101+
const absoluteDirs = dirs.map((dir) => path.resolve(dir));
102+
103+
log.start('开始安装本地扩展\n');
104+
const homedir = os.homedir();
105+
absoluteDirs.forEach((dir) => {
106+
let readablePath = dir;
107+
if (dir.startsWith(homedir)) {
108+
readablePath = dir.replace(homedir, '~');
109+
}
110+
console.log(` * ${readablePath}`);
111+
});
112+
console.log();
113+
114+
const httpUri = await getHttpUri(options);
115+
116+
from(absoluteDirs)
117+
.pipe(
118+
mergeMap((localExtPath) => getExtension(localExtPath, 'local', httpUri), 5),
119+
filter((data) => !!data),
120+
mergeMap(writeMetadata)
121+
)
122+
.subscribe(
123+
(ext) => {
124+
log.info(`${formatExtension(ext)} 安装完成`);
125+
},
126+
(err) => {
127+
log.error('本地扩展安装失败,请重试');
128+
console.error(err);
129+
},
130+
() => {
131+
log.success('本地扩展安装成功');
132+
createServer(absoluteDirs, httpUri);
133+
}
134+
);
135+
};
136+
75137
async function createInstaller() {
76138
const pkgJSON = fse.readJSONSync(path.join(__dirname, '../package.json'));
77139
extensionInstaller = new ExtensionInstaller({
@@ -141,7 +203,7 @@ function checkExtensionConfig(extensions: Extension[]) {
141203
}
142204
}
143205

144-
function parseExtensionId(extensionIds: string[]) {
206+
function parseExtensionId(extensionIds: string[], mode?: IExtensionMode) {
145207
const extensions: IExtensionDesc[] = [];
146208
for (const extId of extensionIds) {
147209
const reg = /^([a-zA-Z][0-9a-zA-Z_-]*)\.([a-zA-Z][0-9a-zA-Z_-]*)(?:@(\d+\.\d+\.\d+.*))?$/;
@@ -150,17 +212,18 @@ function parseExtensionId(extensionIds: string[]) {
150212
return error(`${extId} 格式不合法,请输入 publisher.name[@version] 的形式`);
151213
}
152214
const [, publisher, name, version] = matched;
153-
extensions.push({ publisher, name, version });
215+
extensions.push({ publisher, name, version, mode });
154216
}
155217
return extensions;
156218
}
157219

158220
async function installExtension(extension: IExtensionDesc) {
159-
return extensionInstaller.install({
221+
const extensionPath = await extensionInstaller.install({
160222
publisher: extension.publisher,
161223
name: extension.name,
162224
version: extension.version,
163225
});
226+
return [extensionPath, extension.mode] as const;
164227
}
165228

166229
async function writeMetadata(metadata: IExtensionBasicMetadata) {

packages/cli/src/extension/scanner.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
import * as path from 'path';
66
import * as fse from 'fs-extra';
77
import { mergeContributes } from '@ali/ide-kaitian-extension/lib/node/merge-contributes';
8+
import { Uri } from '@ali/ide-core-common';
89
import pick from 'lodash.pick';
910

10-
import { NLSInfo, IExtensionBasicMetadata } from './type';
11+
import { NLSInfo, IExtensionBasicMetadata, IExtensionMode } from './type';
1112

1213
export async function getExtension(
13-
extensionPath: string
14+
extensionPath: string,
15+
mode?: IExtensionMode,
16+
localUri?: Uri
1417
): Promise<IExtensionBasicMetadata | undefined> {
1518
if (!(await fse.pathExists(extensionPath))) {
1619
return undefined;
@@ -97,7 +100,15 @@ export async function getExtension(
97100
packageJSON.contributes
98101
);
99102

100-
const { publisher, name } = getExtensionIdByPath(extensionPath, packageJSON.version);
103+
// 本地扩展的 publisher 和 name 从 package.json 获取,
104+
// 远程扩展的 publisher 和 name 从目录名获取
105+
const { publisher, name } =
106+
mode === 'local' ? packageJSON : getExtensionIdByPath(extensionPath, packageJSON.version);
107+
108+
let uri: string | undefined;
109+
if (mode === 'local' && localUri) {
110+
uri = localUri.with({ path: path.join(localUri.path, extensionPath) }).toString();
111+
}
101112

102113
const metadata: IExtensionBasicMetadata = {
103114
extension: {
@@ -115,6 +126,8 @@ export async function getExtension(
115126
pkgNlsJSON,
116127
nlsList,
117128
extendConfig,
129+
mode,
130+
uri,
118131
};
119132
return metadata;
120133
}

packages/cli/src/extension/type.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ export {
77
IExtensionDesc,
88
JSONType,
99
IExtensionMetadata,
10+
IExtensionMode,
1011
} from '@alipay/alex-shared';
1112

1213
export interface IExtensionContributions extends JSONType {}
1314

1415
export interface IKaitianExtensionContributions extends JSONType {}
16+
17+
export interface IExtensionServerOptions {
18+
host?: string;
19+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import http from 'http';
2+
import url from 'url';
3+
import portfinder from 'portfinder';
4+
import send from 'send';
5+
import { Uri } from '@ali/ide-core-common';
6+
import { log } from './log';
7+
import { IExtensionServerOptions } from '../extension/type';
8+
9+
export const getHttpUri: (options?: IExtensionServerOptions) => Promise<Uri> = async (options) => {
10+
const port = await findPort();
11+
12+
return Uri.from({
13+
scheme: 'http',
14+
authority: `${options?.host || 'localhost'}:${port}`,
15+
path: '/assets',
16+
query: '',
17+
fragment: '',
18+
});
19+
};
20+
21+
export const createServer = async (dirs: string[], uri: Uri) => {
22+
const server = http.createServer((req, res) => {
23+
const pathname = decodeURIComponent(url.parse(req.url!).pathname!);
24+
const targetDir = dirs.find((dir) => pathname.startsWith(`${uri.path}${dir}`));
25+
if (!targetDir) {
26+
res.statusCode = 404;
27+
res.end();
28+
return;
29+
}
30+
const filepath = pathname.slice(`${uri.path}${targetDir}`.length);
31+
send(req, filepath, {
32+
cacheControl: false,
33+
root: targetDir,
34+
})
35+
.on('headers', (res: http.ServerResponse) => {
36+
res.setHeader('Access-Control-Allow-Origin', '*');
37+
})
38+
.pipe(res);
39+
});
40+
41+
server.listen(Number(uri.authority.split(':')[1]), () => {
42+
log.info(`Local Extension Server: ${uri.scheme}://${uri.authority}`);
43+
});
44+
};
45+
46+
const BASE_PORT = 30000;
47+
async function findPort() {
48+
try {
49+
const port = await portfinder.getPortPromise({ port: BASE_PORT });
50+
return port;
51+
} catch (err) {
52+
throw err;
53+
}
54+
}

0 commit comments

Comments
 (0)