From 284bb379ff64e44abf2ad072a1683730a57691c8 Mon Sep 17 00:00:00 2001 From: Philip Langer Date: Mon, 29 Aug 2022 16:59:36 +0200 Subject: [PATCH] Add generator for example VS Code extension The added example is called *Hello World (VS Code extension)* and produces a Theia application with a small example VS Code extension as part of the application. This shall demonstrate the second extension mechanism of Theia, ie via VS Code extensions alongside Theia extensions. The created application is set up for developing and building the VS Code extension alongside other VS Code or Theia extensions in the same mono repo by linking the VS Code extension in a `plugins` folder of the Theia application. Also, this setup enables debugging the included VS Code application. Lastly, this change extends the generator to optionally (option `-p`) * include the Theia extension required for the plugin mechanism to work * create a `plugins` folder that shall contain the plugins * extend the launch scripts and launch.json to set the plugins folder Contributed on behalf of STMicroelectronics. --- src/app/index.ts | 79 ++++++++++++++++------- templates/app-browser-package.json | 9 ++- templates/app-electron-package.json | 9 ++- templates/launch.json | 12 ++-- templates/root-package.json | 3 +- templates/vscode-hello-world/extension.ts | 41 ++++++++++++ templates/vscode-hello-world/gitignore | 1 + templates/vscode-hello-world/package.json | 55 ++++++++++++++++ 8 files changed, 176 insertions(+), 33 deletions(-) create mode 100644 templates/vscode-hello-world/extension.ts create mode 100644 templates/vscode-hello-world/gitignore create mode 100644 templates/vscode-hello-world/package.json diff --git a/src/app/index.ts b/src/app/index.ts index 3ebbbf8..6bd95bc 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -29,7 +29,8 @@ enum ExtensionType { TreeEditor = 'tree-editor', Empty = 'empty', Backend = 'backend', - DiagramEditor = 'diagram-editor' + DiagramEditor = 'diagram-editor', + VsCodeHelloWorld = 'vs-code-hello-world' } enum TemplateType { @@ -53,6 +54,7 @@ module.exports = class TheiaExtension extends Base { browser: boolean electron: boolean vscode: boolean + plugins: boolean theiaVersion: string lernaVersion: string skipInstall: boolean @@ -95,6 +97,12 @@ module.exports = class TheiaExtension extends Base { type: Boolean, default: true }) + this.option('plugins', { + alias: 'p', + description: 'Generate plugins folder and extend configs', + type: Boolean, + default: false + }) this.option('author', { alias: 'a', @@ -164,6 +172,7 @@ module.exports = class TheiaExtension extends Base { message: "The extension's type", choices: [ { value: ExtensionType.HelloWorld, name: 'Hello World' }, + { value: ExtensionType.VsCodeHelloWorld, name: 'Hello World (VS Code extension)' }, { value: ExtensionType.Widget, name: 'Widget (with unit tests)' }, { value: ExtensionType.LabelProvider, name: 'LabelProvider' }, { value: ExtensionType.TreeEditor, name: 'TreeEditor' }, @@ -188,10 +197,10 @@ module.exports = class TheiaExtension extends Base { (this.options as any).templateType = template; - if(template === TemplateType.Java) { + if (template === TemplateType.Java) { this.log('\x1b[32m%s\x1b[0m', 'The template will use an EMF source model on the server and generate a Theia extension ✓') } - if(template === TemplateType.Node) { + if (template === TemplateType.Node) { this.log('\x1b[32m%s\x1b[0m', 'The template will use a JSON based source model, node as a server and generate a Theia extension ✓') } } @@ -216,7 +225,7 @@ module.exports = class TheiaExtension extends Base { let unscopedExtensionName = '' let extensionPath = '' let extensionPrefix = '' - if(extensionName) { + if (extensionName) { unscopedExtensionName = extensionName[0] === '@' ? extensionName.substring(extensionName.indexOf('/') + 1) : extensionName; @@ -238,8 +247,10 @@ module.exports = class TheiaExtension extends Base { githubURL, theiaVersion: options["theia-version"], lernaVersion: options["lerna-version"], - backend: options["extensionType"] === ExtensionType.Backend - } + backend: options["extensionType"] === ExtensionType.Backend, + includeExtension: options["extensionType"] !== ExtensionType.VsCodeHelloWorld, + includePlugins: options.plugins || options["extensionType"] === ExtensionType.VsCodeHelloWorld, + }; this.params.dependencies = ''; this.params.browserDevDependencies = ''; if (this.params.extensionType === ExtensionType.TreeEditor) { @@ -249,7 +260,7 @@ module.exports = class TheiaExtension extends Base { if (this.params.extensionType === ExtensionType.Widget) { this.params.devdependencies = `,\n "@testing-library/react": "^11.2.7",\n "@types/jest": "^26.0.20",\n "jest": "^26.6.3",\n "ts-node": "^10.9.1",\n "ts-jest": "^26.5.6"`; this.params.scripts = `,\n "test": "jest --config configs/jest.config.ts"`; - this.params.rootscripts =`,\n "test": "cd ${this.params.extensionPath} && yarn test"`; + this.params.rootscripts = `,\n "test": "cd ${this.params.extensionPath} && yarn test"`; this.params.containsTests = true; } options.params = this.params @@ -298,12 +309,17 @@ module.exports = class TheiaExtension extends Base { { params: this.params } ); } + if (this.params.plugins || this.params.extensionType === ExtensionType.VsCodeHelloWorld) { + this.fs.write('plugins/.gitkeep', 'Folder of VS Code extensions (*.vsix) to be installed by default.'); + } + } + if (this.params.extensionType !== ExtensionType.VsCodeHelloWorld) { + this.fs.copyTpl( + this.templatePath('extension-package.json'), + this.extensionPath('package.json'), + { params: this.params } + ); } - this.fs.copyTpl( - this.templatePath('extension-package.json'), - this.extensionPath('package.json'), - { params: this.params } - ); this.fs.copyTpl( this.templatePath('tsconfig.json'), this.extensionPath('tsconfig.json'), @@ -330,6 +346,25 @@ module.exports = class TheiaExtension extends Base { ); } + /** vs-code-hello-world */ + if (this.params.extensionType === ExtensionType.VsCodeHelloWorld) { + this.fs.copyTpl( + this.templatePath('vscode-hello-world/package.json'), + this.extensionPath('package.json'), + { params: this.params } + ); + this.fs.copyTpl( + this.templatePath('vscode-hello-world/gitignore'), + this.extensionPath('.gitignore'), + { params: this.params } + ); + this.fs.copyTpl( + this.templatePath('vscode-hello-world/extension.ts'), + this.extensionPath(`src/extension.ts`), + { params: this.params } + ); + } + /** empty */ if (this.params.extensionType === ExtensionType.Empty) { this.fs.copyTpl( @@ -474,7 +509,7 @@ module.exports = class TheiaExtension extends Base { if (this.params.extensionType === ExtensionType.DiagramEditor) { const baseDir = `./glsp-examples-${glspExamplesRepositoryTag}`; let templatePath = ''; - if(this.params.templateType == TemplateType.Java) { + if (this.params.templateType == TemplateType.Java) { templatePath = '/project-templates/java-emf-theia'; } else if (this.params.templateType == TemplateType.Node) { templatePath = '/project-templates/node-json-theia'; @@ -483,9 +518,9 @@ module.exports = class TheiaExtension extends Base { } const done = this.async(); - request.get(`https://github.com/eclipse-glsp/glsp-examples/archive/refs/tags/${glspExamplesRepositoryTag}.tar.gz`).pipe(tar.x().on('close',() => { - fs.copy(baseDir+'/README.md', './README.md'); - fs.copy(baseDir+templatePath, './').then(() => { + request.get(`https://github.com/eclipse-glsp/glsp-examples/archive/refs/tags/${glspExamplesRepositoryTag}.tar.gz`).pipe(tar.x().on('close', () => { + fs.copy(baseDir + '/README.md', './README.md'); + fs.copy(baseDir + templatePath, './').then(() => { fs.rm(baseDir, { recursive: true }); done(); }); @@ -504,22 +539,22 @@ module.exports = class TheiaExtension extends Base { const command = this.spawnCommand('yarn', []); - command.on('close', (code:number) => { - if (code === 0 ) { + command.on('close', (code: number) => { + if (code === 0) { this.log( '\x1b[32m%s\x1b[0m', '\nThe DiagramEditor Example has been generated and all dependencies installed\n\nCheck the Readme to get started.' ); } else { - this.log('\x1b[31m%s\x1b[0m','Command "yarn" failed. Please see above for the reported error message.'); + this.log('\x1b[31m%s\x1b[0m', 'Command "yarn" failed. Please see above for the reported error message.'); process.exit(code); } }); } else { const command = this.spawnCommand('yarn', []); - - command.on('close', function(code: number){ - if (code !== 0 ) { + + command.on('close', function (code: number) { + if (code !== 0) { process.exit(code); } }) diff --git a/templates/app-browser-package.json b/templates/app-browser-package.json index 5a6d3f1..14fd695 100644 --- a/templates/app-browser-package.json +++ b/templates/app-browser-package.json @@ -13,15 +13,18 @@ "@theia/preferences": "<%= params.theiaVersion %>", "@theia/process": "<%= params.theiaVersion %>", "@theia/terminal": "<%= params.theiaVersion %>", - "@theia/workspace": "<%= params.theiaVersion %>", - "<%= params.extensionName %>": "<%= params.version %>" + "@theia/workspace": "<%= params.theiaVersion %>"<% if (params.includeExtension) { %>, + "<%= params.extensionName %>": "<%= params.version %>"<% } %><% if (params.includePlugins) { %>, + "@theia/plugin": "<%= params.theiaVersion %>", + "@theia/plugin-ext": "<%= params.theiaVersion %>", + "@theia/plugin-ext-vscode": "<%= params.theiaVersion %>"<% } %> }, "devDependencies": { "@theia/cli": "<%= params.theiaVersion %>"<% if (params.browserDevDependencies) { %><%- params.browserDevDependencies %><% } %> }, "scripts": { "prepare": "theia build --mode development", - "start": "theia start", + "start": "theia start<% if (params.includePlugins) { %> --plugins=local-dir:../plugins<% } %>", "watch": "theia build --watch --mode development" }, "theia": { diff --git a/templates/app-electron-package.json b/templates/app-electron-package.json index 90e0bff..ef00d87 100644 --- a/templates/app-electron-package.json +++ b/templates/app-electron-package.json @@ -14,8 +14,11 @@ "@theia/preferences": "<%= params.theiaVersion %>", "@theia/process": "<%= params.theiaVersion %>", "@theia/terminal": "<%= params.theiaVersion %>", - "@theia/workspace": "<%= params.theiaVersion %>", - "<%= params.extensionName %>": "<%= params.version %>" + "@theia/workspace": "<%= params.theiaVersion %>"<% if (params.includeExtension) { %>, + "<%= params.extensionName %>": "<%= params.version %>"<% } %><% if (params.includePlugins) { %>, + "@theia/plugin": "<%= params.theiaVersion %>", + "@theia/plugin-ext": "<%= params.theiaVersion %>", + "@theia/plugin-ext-vscode": "<%= params.theiaVersion %>"<% } %> }, "devDependencies": { "@theia/cli": "<%= params.theiaVersion %>", @@ -23,7 +26,7 @@ }, "scripts": { "prepare": "theia build --mode development", - "start": "theia start", + "start": "theia start<% if (params.includePlugins) { %> --plugins=local-dir:../plugins<% } %>", "watch": "theia build --watch --mode development" }, "theia": { diff --git a/templates/launch.json b/templates/launch.json index 58b45a2..d6645c5 100644 --- a/templates/launch.json +++ b/templates/launch.json @@ -12,10 +12,12 @@ "args": [ "--loglevel=debug", "--port=3000", - "--no-cluster" + "--no-cluster"<% if (params.includePlugins) { %>, + "--plugins=local-dir:${workspaceRoot}/plugins"<% } %> ], "env": { - "NODE_ENV": "development" + "NODE_ENV": "development", + "NODE_OPTIONS": "--enable-source-maps" }, "sourceMaps": true, "outFiles": [ @@ -40,10 +42,12 @@ "args": [ "--loglevel=debug", "--hostname=localhost", - "--no-cluster" + "--no-cluster"<% if (params.includePlugins) { %>, + "--plugins=local-dir:${workspaceRoot}/plugins"<% } %> ], "env": { - "NODE_ENV": "development" + "NODE_ENV": "development", + "NODE_OPTIONS": "--enable-source-maps" }, "sourceMaps": true, "outFiles": [ diff --git a/templates/root-package.json b/templates/root-package.json index 55e3961..df4a79a 100644 --- a/templates/root-package.json +++ b/templates/root-package.json @@ -13,5 +13,6 @@ }, "workspaces": [ "<%= params.extensionPath %>"<% if (params.browser) { %>, "browser-app"<% } %><% if (params.electron) { %>, "electron-app"<% } %> - ] + ]<% if (params.includePlugins) { %>, + "theiaPluginsDir": "plugins"<% } %> } diff --git a/templates/vscode-hello-world/extension.ts b/templates/vscode-hello-world/extension.ts new file mode 100644 index 0000000..3419151 --- /dev/null +++ b/templates/vscode-hello-world/extension.ts @@ -0,0 +1,41 @@ +// The following code is based on +// https://github.com/microsoft/vscode-extension-samples/blob/7b9a0a8c4c631e393862c3e767c7be26bb2233b2/.base-sample/src/extension.ts +// which was published under MIT License (https://github.com/microsoft/vscode-extension-samples/blob/main/LICENSE) +// For more information see https://code.visualstudio.com/api + +// The module 'vscode' contains the VS Code extensibility API +// Import the module and reference it with the alias vscode in your code below +import * as vscode from 'vscode'; + +// this method is called when your extension is activated +// your extension is activated the very first time the command is executed + +/** + * @param {vscode.ExtensionContext} context + */ +function activate(context: vscode.ExtensionContext) { + // Use the console to output diagnostic information (console.log) and errors (console.error) + // This line of code will only be executed once when your extension is activated + console.log('Congratulations, your extension "<%= params.extensionName %>" is now active!'); + + // The command has been defined in the package.json file + // Now provide the implementation of the command with registerCommand + // The commandId parameter must match the command field in package.json + let disposable = vscode.commands.registerCommand('<%= params.extensionName %>.helloWorld', () => { + // The code you place here will be executed every time your command is executed + + // Display a message box to the user + vscode.window.showInformationMessage('Hello World from a VS Code extension!'); + }); + + context.subscriptions.push(disposable); +} + +// this method is called when your extension is deactivated +function deactivate() {} + +// eslint-disable-next-line no-undef +module.exports = { + activate, + deactivate +} \ No newline at end of file diff --git a/templates/vscode-hello-world/gitignore b/templates/vscode-hello-world/gitignore new file mode 100644 index 0000000..dfacd4d --- /dev/null +++ b/templates/vscode-hello-world/gitignore @@ -0,0 +1 @@ +*.vsix \ No newline at end of file diff --git a/templates/vscode-hello-world/package.json b/templates/vscode-hello-world/package.json new file mode 100644 index 0000000..567db4b --- /dev/null +++ b/templates/vscode-hello-world/package.json @@ -0,0 +1,55 @@ +{ + "name": "<%= params.extensionName %>", + "displayName": "Hello World (VS Code extension)", + "engines": { + "vscode": "^1.32.0" + }, + "version": "<%= params.version %>",<% + if (params.description) { %> + "description": "<%= params.description %>", + <%} %><% + if (params.author) { %> + "author": "<%= params.author %>", + <% } %><% + if (params.license) { %> + "license": "<%= params.license %>", + <% } %><% + if (params.githubURL) { %> + "repository": { + "type": "git", + "url": "<%= params.githubURL %>.git" + }, + "bugs": { + "url": "<%= params.githubURL %>/issues" + }, + "homepage": "<%= params.githubURL %>",<% } %> + "categories": [ + "Other" + ], + "activationEvents": [ + "onCommand:<%= params.extensionName %>.helloWorld" + ], + "main": "./lib/extension.js", + "contributes": { + "commands": [ + { + "command": "<%= params.extensionName %>.helloWorld", + "title": "Hello World (VS Code extension)" + } + ] + }, + "scripts": { + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "prepare": "yarn run clean && yarn run build && yarn symlink", + "clean": "rimraf lib", + "build": "yarn run compile", + "symlink": "symlink-dir . ../plugins/<%= params.extensionName %>" + }, + "devDependencies": { + "@types/vscode": "^1.32.0", + "symlink-dir": "latest", + "rimraf": "latest", + "typescript": "latest" + } +} \ No newline at end of file