From 4d6d09b5cbf2d85a21defc90245481a89f58c460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20W=C3=BCrfel?= Date: Tue, 8 Oct 2019 00:25:35 +0200 Subject: [PATCH] feat(codegen): angular element support (#244) Add Angular Element code generation support. The solution tries to streamline the process described in `ngx-build-plus` by Manfred Steyer (https://github.com/manfredsteyer/ngx-build-plus). If you generate the assets with the editor and follow the README you will create a minimal toolchain to create Angular Elements in general. Closes #244 --- .../src/lib/angular-aggregator.service.ts | 6 +- .../lib/angular-element-aggregator.service.ts | 286 ++++++++++++++++++ .../src/lib/web-codegen.service.ts | 7 + src/app/app.component.ts | 1 + .../angular-element-codegen.service.ts | 27 ++ .../codegen/codegen.module.ts | 2 + .../codegen/codegen.service.ts | 14 + .../editor-container.component.html | 10 +- .../editor-container.component.ts | 5 + src/assets/codegen/angularElement.svg | 14 + 10 files changed, 368 insertions(+), 4 deletions(-) create mode 100644 projects/web-codegen/src/lib/angular-element-aggregator.service.ts create mode 100644 src/app/editor/code/editor-container/codegen/angular-element-codegen.service.ts create mode 100644 src/assets/codegen/angularElement.svg diff --git a/projects/web-codegen/src/lib/angular-aggregator.service.ts b/projects/web-codegen/src/lib/angular-aggregator.service.ts index 4be1712c5..030efad3f 100644 --- a/projects/web-codegen/src/lib/angular-aggregator.service.ts +++ b/projects/web-codegen/src/lib/angular-aggregator.service.ts @@ -46,14 +46,14 @@ export class AngularAggregatorService { }, { kind: 'angular', - value: this.renderSpec(current.name, options), + value: this.renderSpec(current.name), language: 'typescript', uri: `${options.componentDir}/${fileName}.spec.ts` } ]; } - private renderComponent(name: string, options: WebCodeGenOptions) { + renderComponent(name: string, options: WebCodeGenOptions) { const className = this.formatService.className(name); const normalizedName = this.formatService.normalizeName(name); const tagName = `${options.xmlPrefix}${normalizedName}`; @@ -68,7 +68,7 @@ import { Component } from '@angular/core'; export class ${className}Component {}`; } - private renderSpec(name: string, options: WebCodeGenOptions) { + private renderSpec(name: string) { const className = this.formatService.className(name); const fileName = this.formatService.normalizeName(name); return `\ diff --git a/projects/web-codegen/src/lib/angular-element-aggregator.service.ts b/projects/web-codegen/src/lib/angular-element-aggregator.service.ts new file mode 100644 index 000000000..5cf4cdace --- /dev/null +++ b/projects/web-codegen/src/lib/angular-element-aggregator.service.ts @@ -0,0 +1,286 @@ +import { Injectable } from '@angular/core'; +import { WebCodeGenOptions } from './web-codegen'; +import { WebAggregatorService } from './web-aggregator.service'; +import { FormatService } from '@xlayers/sketch-lib'; +import { AngularAggregatorService } from './angular-aggregator.service'; + +const ELEMENT_MODULE_FILENAME = 'app.module.ts'; +const EXTRA_WEBPACK_CONFIG_FILENAME = 'webpack.extra.js'; +const COPY_BUNDLES_SCRIPT_FILENAME = 'copy.bundles.js'; +const DIST_FOLDER_NAME = 'dist'; +const BUNDLES_TARGET_PATH = `${DIST_FOLDER_NAME}/bundles`; +const SAMPLE_INDEX_FILENAME = 'index.html'; +const ELEMENT_BUNDLE_FILENAME = 'main.js'; +const ELEMENT_CREATOR_APPNAME = 'element-creator'; + +@Injectable({ + providedIn: 'root' +}) +export class AngularElementAggregatorService { + constructor( + private readonly formatService: FormatService, + private readonly webAggretatorService: WebAggregatorService, + private readonly angularAggregatorService: AngularAggregatorService + ) {} + + aggreate(current: SketchMSLayer, options: WebCodeGenOptions) { + const fileName = this.formatService.normalizeName(current.name); + const componentPathName = `${options.componentDir}/${fileName}.component`; + return [ + { + uri: 'README.md', + value: this.renderReadme(current.name, options), + language: 'markdown', + kind: 'text' + }, + ...this.webAggretatorService.aggreate(current, options).map(file => { + switch (file.language) { + case 'html': + return { + ...file, + kind: 'angular', + uri: `${options.componentDir}/${fileName}.component.html` + }; + + case 'css': + return { + ...file, + kind: 'angular', + uri: `${options.componentDir}/${fileName}.component.css` + }; + + default: + return { + ...file, + kind: 'angularElement' + }; + } + }), + { + uri: `${componentPathName}.ts`, + value: this.angularAggregatorService.renderComponent(current.name, options), + language: 'typescript', + kind: 'angular', + }, + { + uri: ELEMENT_MODULE_FILENAME, + value: this.renderElementModule(current.name, options, componentPathName), + language: 'typescript', + kind: 'angularElement' + }, + { + uri: EXTRA_WEBPACK_CONFIG_FILENAME, + value: this.renderWebpackExtra(), + language: 'javascript', + kind: 'angularElement' + }, + { + uri: COPY_BUNDLES_SCRIPT_FILENAME, + value: this.renderCopyUmdBundlesScript(), + language: 'javascript', + kind: 'angularElement' + }, + { + uri: SAMPLE_INDEX_FILENAME, + value: this.renderSampleIndexHtml(current.name, options), + language: 'html', + kind: 'angularElement' + } + ]; + } + + + private renderReadme(name: string, options: WebCodeGenOptions) { + const className = this.formatService.className(name); + const normalizedName = this.formatService.normalizeName(name); + const tagName = `${options.xmlPrefix}${normalizedName}`; + const codeSpan = text => '`' + text + '`'; + const codeBlock = '```'; + return ` +**Notice:** +The current implement of [Angular Elements](https://angular.io/guide/elements) is in MVP (minimum viable product) state. +Some features like content projection are not supported yet. +Keep in mind that the following build process and feature support will be improved in the future. +The creation of the bundled Angular Element is based on the process defined by [Manfred Steyer](https://twitter.com/manfredsteyer)'s example from +[${codeSpan('ngx-build-plus')}](https://github.com/manfredsteyer/ngx-build-plus). + +## How to use the ${codeSpan(name)} Angular Element + +1. In order to use an Angular Element as a web component, it first needs to be created, e.g. in the following way: + + a) Use the Angular CLI to create a minimal app which will be used to create the Angular Element: + ${codeBlock} + ng new ${ELEMENT_CREATOR_APPNAME} --minimal --style css --routing false + cd ${ELEMENT_CREATOR_APPNAME} + ${codeBlock} + + b) Add necessary dependencies for the following steps: + ${codeBlock} + ng add @angular/elements + ng add ngx-build-plus + npm install @webcomponents/custom-elements --save + npm install --save-dev copy + ${codeBlock} + + c) Download and extract the files of this generation. Place the files of the ${codeSpan(className)} + into your standard ${codeSpan('src/app')} folder. Replace the ${codeSpan(ELEMENT_MODULE_FILENAME)} and remove the sample ${codeSpan('app.component.ts')}. + Extract the files ${codeSpan(EXTRA_WEBPACK_CONFIG_FILENAME)} and ${codeSpan(COPY_BUNDLES_SCRIPT_FILENAME)} into the project root. + + e) Build the Angular Element: + ${codeBlock} + ng build --prod --extraWebpackConfig ${EXTRA_WEBPACK_CONFIG_FILENAME} --output-hashing none --single-bundle true + ${codeBlock} + +2. After the creation of the Angular Element, it can be found as single file +web component ${codeSpan(DIST_FOLDER_NAME + '/' + ELEMENT_CREATOR_APPNAME + '/' + ELEMENT_BUNDLE_FILENAME)} and can be consumed in the following way: +${codeBlock} + // index.html + + <${tagName}> +${codeBlock} + +However due to the bundle splitting approach, the different dependent bundles must be added manually, +e.g. like described in the exported sample file ${codeSpan(SAMPLE_INDEX_FILENAME)}. +In order to get those script you can run the script ${codeSpan(COPY_BUNDLES_SCRIPT_FILENAME)} file. +${codeBlock} + node ./${COPY_BUNDLES_SCRIPT_FILENAME} +${codeBlock} + +> For more information about [web components and browser support](https://github.com/WebComponents/webcomponentsjs#browser-support) +`; + } + + private renderElementModule(name: string, options: WebCodeGenOptions, componentPathName: string) { + const className = this.formatService.className(name); + const componentName = `${className}Component`; + const normalizedName = this.formatService.normalizeName(name); + const tagName = `${options.xmlPrefix}${normalizedName}`; + return ( + '' + + ` +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule, Injector } from '@angular/core'; +import { createCustomElement } from '@angular/elements'; + +import { ${componentName} } from './${componentPathName}'; + +@NgModule({ + imports: [ + BrowserModule + ], + declarations: [ + ${componentName} + ], + entryComponents: [ + ${componentName} + ], +}) +export class AppModule { + constructor(injector: Injector) { + const element = createCustomElement(${componentName} , { injector }); + customElements.define('${tagName}', element); + } + + ngDoBootstrap() { } +} + ` + ); + } + + private renderWebpackExtra() { + return ` +module.exports = { + "externals": { + "rxjs": "rxjs", + "@angular/core": "ng.core", + "@angular/common": "ng.common", + "@angular/platform-browser": "ng.platformBrowser", + "@angular/elements": "ng.elements" + } +} + `; + } + + private renderCopyUmdBundlesScript() { + return ` +// +// This script copies over UMD bundles to the folder dist/bundles +// If you call it manually, call it from your projects root +// > node /${COPY_BUNDLES_SCRIPT_FILENAME} +// + +const copy = require('copy'); + +console.log('Copy UMD bundles ...'); + +copy('node_modules/@angular/*/bundles/*.umd.js', '${BUNDLES_TARGET_PATH}', {}, _ => {}); +copy('node_modules/rxjs/bundles/*.js', '${BUNDLES_TARGET_PATH}/rxjs', {}, _ => {}); +copy('node_modules/zone.js/dist/*.js', '${BUNDLES_TARGET_PATH}/zone.js', {}, _ => {}); +copy('node_modules/core-js/client/*.js', '${BUNDLES_TARGET_PATH}/core-js', {}, _ => {}); +copy('node_modules/@webcomponents/custom-elements/*.js', '${BUNDLES_TARGET_PATH}/custom-elements', {}, _ => {}); +copy('node_modules/@webcomponents/custom-elements/src/native-shim*.js', '${BUNDLES_TARGET_PATH}/custom-elements/src', {}, _ => {}); + `; + } + + private renderSampleIndexHtml(name: string, options: WebCodeGenOptions) { + const normalizedName = this.formatService.normalizeName(name); + const tagName = `${options.xmlPrefix}${normalizedName}`; + + return ` + + + + +ElementsLoading + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +<${tagName}> + + + + `; + } +} diff --git a/projects/web-codegen/src/lib/web-codegen.service.ts b/projects/web-codegen/src/lib/web-codegen.service.ts index 372d405db..a048ca03d 100644 --- a/projects/web-codegen/src/lib/web-codegen.service.ts +++ b/projects/web-codegen/src/lib/web-codegen.service.ts @@ -5,6 +5,7 @@ import { WebParserService } from './web-parser.service'; import { WebAggregatorService } from './web-aggregator.service'; import { VueAggregatorService } from './vue-aggregator.service'; import { AngularAggregatorService } from './angular-aggregator.service'; +import { AngularElementAggregatorService } from './angular-element-aggregator.service'; import { ReactAggregatorService } from './react-aggregator.service'; import { LitElementAggregatorService } from './lit-element-aggregator.service'; import { WebCodeGenOptions } from './web-codegen.d'; @@ -23,6 +24,7 @@ export class WebCodeGenService { private readonly webComponentAggretatorService: WebComponentAggregatorService, private readonly vueAggretatorService: VueAggregatorService, private readonly angularAggretatorService: AngularAggregatorService, + private readonly angularElementAggretatorService: AngularElementAggregatorService, private readonly litElementAggretatorService: LitElementAggregatorService, private readonly reactAggretatorService: ReactAggregatorService, private readonly stencilAggretatorService: StencilAggregatorService, @@ -70,6 +72,11 @@ export class WebCodeGenService { this.angularAggretatorService.aggreate(current, options) ); + case 'angularElement': + return this.visitContent(current, data, options).concat( + this.angularElementAggretatorService.aggreate(current, options) + ); + case 'litElement': return this.visitContent(current, data, options).concat( this.litElementAggretatorService.aggreate(current, options) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 16b69f847..1c596ce28 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -28,6 +28,7 @@ export class AppComponent { readonly translate: TranslateService) { iconRegistry.addSvgIcon('angular', sanitizer.bypassSecurityTrustResourceUrl('assets/codegen/angular.svg')); + iconRegistry.addSvgIcon('angularElement', sanitizer.bypassSecurityTrustResourceUrl('assets/codegen/angularElement.svg')); iconRegistry.addSvgIcon('react', sanitizer.bypassSecurityTrustResourceUrl('assets/codegen/react.svg')); iconRegistry.addSvgIcon('vue', sanitizer.bypassSecurityTrustResourceUrl('assets/codegen/vue.svg')); iconRegistry.addSvgIcon('wc', sanitizer.bypassSecurityTrustResourceUrl('assets/codegen/wc.svg')); diff --git a/src/app/editor/code/editor-container/codegen/angular-element-codegen.service.ts b/src/app/editor/code/editor-container/codegen/angular-element-codegen.service.ts new file mode 100644 index 000000000..317926f22 --- /dev/null +++ b/src/app/editor/code/editor-container/codegen/angular-element-codegen.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { WebCodeGenService } from '@xlayers/web-codegen'; + +@Injectable({ + providedIn: 'root' +}) +export class AngularElementCodeGenService { + constructor( + private readonly webCodeGen: WebCodeGenService, + ) {} + + buttons() { + return { + stackblitz: false + }; + } + + generate(data: SketchMSData) { + const generatedFiles = data.pages.flatMap(page => + this.webCodeGen.aggreate(page, data, { mode: 'angularElement' }) + ); + + return [ + ...generatedFiles + ]; + } +} diff --git a/src/app/editor/code/editor-container/codegen/codegen.module.ts b/src/app/editor/code/editor-container/codegen/codegen.module.ts index 436fbb0e8..7eaac1c7a 100644 --- a/src/app/editor/code/editor-container/codegen/codegen.module.ts +++ b/src/app/editor/code/editor-container/codegen/codegen.module.ts @@ -5,6 +5,7 @@ import { VueCodeGenService } from './vue-codegen.service'; import { WebComponentCodeGenService } from './web-component-codegen.service'; import { StencilCodeGenService } from './stencil-codegen.service'; import { AngularCodeGenService } from './angular-codegen.service'; +import { AngularElementCodeGenService } from './angular-element-codegen.service'; import { LitElementCodeGenService } from './lit-element-codegen.service'; import { XamarinCodeGenService } from './xamarin-codegen.service'; @@ -12,6 +13,7 @@ import { XamarinCodeGenService } from './xamarin-codegen.service'; providers: [ CodeGenService, AngularCodeGenService, + AngularElementCodeGenService, ReactCodeGenService, VueCodeGenService, WebComponentCodeGenService, diff --git a/src/app/editor/code/editor-container/codegen/codegen.service.ts b/src/app/editor/code/editor-container/codegen/codegen.service.ts index a9de80978..761e7c8ab 100644 --- a/src/app/editor/code/editor-container/codegen/codegen.service.ts +++ b/src/app/editor/code/editor-container/codegen/codegen.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { AngularCodeGenService } from './angular-codegen.service'; +import { AngularElementCodeGenService } from './angular-element-codegen.service'; import { ReactCodeGenService } from './react-codegen.service'; import { VueCodeGenService } from './vue-codegen.service'; import { WebComponentCodeGenService } from './web-component-codegen.service'; @@ -17,6 +18,7 @@ export interface XlayersNgxEditorModel { kind: | 'png' | 'angular' + | 'angularElement' | 'react' | 'vue' | 'wc' @@ -42,6 +44,7 @@ export interface CodeGenFacade { export enum CodeGenKind { Unknown, Angular, + AngularElement, React, Vue, WC, @@ -59,6 +62,7 @@ export class CodeGenService { constructor( private readonly angular: AngularCodeGenService, + private readonly angularElement: AngularElementCodeGenService, private readonly react: ReactCodeGenService, private readonly vue: VueCodeGenService, private readonly wc: WebComponentCodeGenService, @@ -143,6 +147,15 @@ export class CodeGenService { content: this.addHeaderInfo(this.angular.generate(this.data)), buttons: this.angular.buttons() }; + + case CodeGenKind.AngularElement: + this.trackFrameworkKind(CodeGenKind.AngularElement); + return { + kind, + content: this.addHeaderInfo(this.angularElement.generate(this.data)), + buttons: this.angularElement.buttons() + }; + case CodeGenKind.React: this.trackFrameworkKind(CodeGenKind.React); return { @@ -150,6 +163,7 @@ export class CodeGenService { content: this.addHeaderInfo(this.react.generate(this.data)), buttons: this.react.buttons() }; + case CodeGenKind.Vue: this.trackFrameworkKind(CodeGenKind.Vue); return { diff --git a/src/app/editor/code/editor-container/editor-container.component.html b/src/app/editor/code/editor-container/editor-container.component.html index 37f38ac6c..26899ba08 100644 --- a/src/app/editor/code/editor-container/editor-container.component.html +++ b/src/app/editor/code/editor-container/editor-container.component.html @@ -8,6 +8,14 @@ + + +
+ + Angular Element +
+
+
@@ -82,4 +90,4 @@
- \ No newline at end of file + diff --git a/src/app/editor/code/editor-container/editor-container.component.ts b/src/app/editor/code/editor-container/editor-container.component.ts index 312182dc8..6fd5ee84b 100644 --- a/src/app/editor/code/editor-container/editor-container.component.ts +++ b/src/app/editor/code/editor-container/editor-container.component.ts @@ -45,6 +45,11 @@ export class EditorContainerComponent implements OnInit, AfterContentInit { this.updateState(); } + generateAngularElement() { + this.codeSetting = this.codegen.generate(CodeGenKind.AngularElement); + this.updateState(); + } + generateReact() { this.codeSetting = this.codegen.generate(CodeGenKind.React); this.updateState(); diff --git a/src/assets/codegen/angularElement.svg b/src/assets/codegen/angularElement.svg new file mode 100644 index 000000000..f0ae440a0 --- /dev/null +++ b/src/assets/codegen/angularElement.svg @@ -0,0 +1,14 @@ + + + + + components + + + + + + + + +