├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── angular.json ├── builders ├── builders.json ├── package.json ├── plugin-builder │ ├── index.ts │ └── schema.json └── tsconfig.builders.json ├── package-lock.json ├── package.json ├── projects ├── plugins │ ├── src │ │ ├── main.ts │ │ ├── plugin1 │ │ │ ├── plugin1.component.html │ │ │ ├── plugin1.component.ts │ │ │ └── plugin1.module.ts │ │ └── plugin2 │ │ │ ├── plugin2.component.html │ │ │ ├── plugin2.component.ts │ │ │ └── plugin2.module.ts │ └── tsconfig.app.json └── shared │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── button │ │ │ ├── button.component.scss │ │ │ └── button.component.ts │ │ ├── shared.component.ts │ │ ├── shared.module.ts │ │ └── tabs │ │ │ ├── tab.component.ts │ │ │ ├── tab.interface.ts │ │ │ ├── tabs.component.html │ │ │ ├── tabs.component.scss │ │ │ └── tabs.component.ts │ ├── public-api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── server.ts ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── app.server.module.ts │ └── services │ │ ├── plugin-loader │ │ ├── client-plugin-loader.service.ts │ │ ├── plugin-externals.ts │ │ ├── plugin-loader.service.ts │ │ └── server-plugin-loader.service.ts │ │ ├── plugins-config.provider.ts │ │ └── transfer-state.service.ts ├── assets │ ├── .gitkeep │ └── plugins-config.json ├── browserslist ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.server.ts ├── main.ts ├── polyfills.ts ├── styles.css ├── tsconfig.app.json ├── tsconfig.server.json └── typings.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── tslint.json └── webpack.server.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events.json 15 | speed-measure-plugin.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | 48 | /builders/plugin-builder/index.js 49 | /src/assets/plugins -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "semi": true 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AngularPluginArchitecture 2 | 3 | For **Angular 11** see [https://github.com/alexzuza/angular-plugin-architecture-with-module-federation](https://github.com/alexzuza/angular-plugin-architecture-with-module-federation) 4 | 5 | Example of building AOT compiled Angular 7 plugin that can be consumed on client and server sides(SSR) 6 | 7 | For Angular 8 see [cli8](https://github.com/alexzuza/angular-plugin-architecture/tree/cli8) branch 8 | 9 | ## Setup 10 | 11 | ``` 12 | npm install 13 | ``` 14 | 15 | Building shared plugin 16 | 17 | ``` 18 | npm run build:shared 19 | ``` 20 | 21 | Building plugins 22 | 23 | ``` 24 | npm run build:plugin1 25 | npm run build:plugin2 26 | ``` 27 | 28 | ## Run 29 | 30 | Dev mode 31 | 32 | ``` 33 | npm start 34 | ``` 35 | 36 | Server-side 37 | 38 | ``` 39 | npm run build:ssr 40 | npm run serve:ssr 41 | ``` 42 | 43 | ## License 44 | 45 | MIT 46 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-plugin-architecture": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/browser", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [ 29 | "node_modules/systemjs/dist/s.js", 30 | "node_modules/systemjs/dist/extras/named-register.js", 31 | "node_modules/systemjs/dist/extras/amd.js" 32 | ], 33 | "es5BrowserSupport": true 34 | }, 35 | "configurations": { 36 | "production": { 37 | "fileReplacements": [ 38 | { 39 | "replace": "src/environments/environment.ts", 40 | "with": "src/environments/environment.prod.ts" 41 | } 42 | ], 43 | "optimization": true, 44 | "outputHashing": "all", 45 | "sourceMap": false, 46 | "extractCss": true, 47 | "namedChunks": false, 48 | "aot": true, 49 | "extractLicenses": true, 50 | "vendorChunk": false, 51 | "buildOptimizer": true, 52 | "budgets": [ 53 | { 54 | "type": "initial", 55 | "maximumWarning": "2mb", 56 | "maximumError": "5mb" 57 | } 58 | ] 59 | } 60 | } 61 | }, 62 | "serve": { 63 | "builder": "@angular-devkit/build-angular:dev-server", 64 | "options": { 65 | "browserTarget": "angular-plugin-architecture:build" 66 | }, 67 | "configurations": { 68 | "production": { 69 | "browserTarget": "angular-plugin-architecture:build:production" 70 | } 71 | } 72 | }, 73 | "extract-i18n": { 74 | "builder": "@angular-devkit/build-angular:extract-i18n", 75 | "options": { 76 | "browserTarget": "angular-plugin-architecture:build" 77 | } 78 | }, 79 | "test": { 80 | "builder": "@angular-devkit/build-angular:karma", 81 | "options": { 82 | "main": "src/test.ts", 83 | "polyfills": "src/polyfills.ts", 84 | "tsConfig": "src/tsconfig.spec.json", 85 | "karmaConfig": "src/karma.conf.js", 86 | "styles": [ 87 | "src/styles.css" 88 | ], 89 | "scripts": [], 90 | "assets": [ 91 | "src/favicon.ico", 92 | "src/assets" 93 | ] 94 | } 95 | }, 96 | "lint": { 97 | "builder": "@angular-devkit/build-angular:tslint", 98 | "options": { 99 | "tsConfig": [ 100 | "src/tsconfig.app.json", 101 | "src/tsconfig.spec.json" 102 | ], 103 | "exclude": [ 104 | "**/node_modules/**" 105 | ] 106 | } 107 | }, 108 | "server": { 109 | "builder": "@angular-devkit/build-angular:server", 110 | "options": { 111 | "outputPath": "dist/server", 112 | "main": "src/main.server.ts", 113 | "tsConfig": "src/tsconfig.server.json" 114 | }, 115 | "configurations": { 116 | "production": { 117 | "fileReplacements": [ 118 | { 119 | "replace": "src/environments/environment.ts", 120 | "with": "src/environments/environment.prod.ts" 121 | } 122 | ] 123 | } 124 | } 125 | } 126 | } 127 | }, 128 | "plugins": { 129 | "root": "projects/plugins/", 130 | "sourceRoot": "projects/plugins/src", 131 | "projectType": "application", 132 | "prefix": "app", 133 | "schematics": {}, 134 | "architect": { 135 | "build": { 136 | "builder": "./builders:plugin", 137 | "options": { 138 | "outputPath": "dist/plugins", 139 | "index": "", 140 | "main": "projects/plugins/src/main.ts", 141 | "polyfills": "projects/plugins/src/polyfills.ts", 142 | "tsConfig": "projects/plugins/tsconfig.app.json", 143 | "assets": [], 144 | "styles": [], 145 | "scripts": [], 146 | "es5BrowserSupport": false 147 | }, 148 | "configurations": { 149 | "production": { 150 | "fileReplacements": [], 151 | "optimization": true, 152 | "outputHashing": "none", 153 | "sourceMap": false, 154 | "extractCss": true, 155 | "namedChunks": false, 156 | "aot": true, 157 | "extractLicenses": false, 158 | "vendorChunk": false, 159 | "buildOptimizer": true, 160 | "budgets": [ 161 | { 162 | "type": "initial", 163 | "maximumWarning": "2mb", 164 | "maximumError": "5mb" 165 | } 166 | ] 167 | } 168 | } 169 | } 170 | } 171 | }, 172 | "shared": { 173 | "root": "projects/shared", 174 | "sourceRoot": "projects/shared/src", 175 | "projectType": "library", 176 | "prefix": "lib", 177 | "architect": { 178 | "build": { 179 | "builder": "@angular-devkit/build-ng-packagr:build", 180 | "options": { 181 | "tsConfig": "projects/shared/tsconfig.lib.json", 182 | "project": "projects/shared/ng-package.json" 183 | } 184 | }, 185 | "test": { 186 | "builder": "@angular-devkit/build-angular:karma", 187 | "options": { 188 | "main": "projects/shared/src/test.ts", 189 | "tsConfig": "projects/shared/tsconfig.spec.json", 190 | "karmaConfig": "projects/shared/karma.conf.js" 191 | } 192 | }, 193 | "lint": { 194 | "builder": "@angular-devkit/build-angular:tslint", 195 | "options": { 196 | "tsConfig": [ 197 | "projects/shared/tsconfig.lib.json", 198 | "projects/shared/tsconfig.spec.json" 199 | ], 200 | "exclude": [ 201 | "**/node_modules/**" 202 | ] 203 | } 204 | } 205 | } 206 | } 207 | }, 208 | "defaultProject": "angular-plugin-architecture" 209 | } -------------------------------------------------------------------------------- /builders/builders.json: -------------------------------------------------------------------------------- 1 | { 2 | "builders": { 3 | "plugin": { 4 | "class": "./plugin-builder", 5 | "schema": "./plugin-builder/schema.json", 6 | "description": "Plugin Builder" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /builders/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "builders": "builders.json" 3 | } 4 | -------------------------------------------------------------------------------- /builders/plugin-builder/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BrowserBuilder, 3 | NormalizedBrowserBuilderSchema 4 | } from '@angular-devkit/build-angular'; 5 | import { Path, virtualFs } from '@angular-devkit/core'; 6 | import * as fs from 'fs'; 7 | import { Observable } from 'rxjs'; 8 | 9 | import { BuilderConfiguration, BuildEvent } from '@angular-devkit/architect'; 10 | import { tap } from 'rxjs/operators'; 11 | 12 | interface PluginBuilderSchema extends NormalizedBrowserBuilderSchema { 13 | /** 14 | * A string of the form `path/to/file#exportName` that acts as a path to include to bundle 15 | */ 16 | modulePath: string; 17 | 18 | /** 19 | * A name of compiled bundle 20 | */ 21 | pluginName: string; 22 | 23 | /** 24 | * A comma-delimited list of shared lib names used by current plugin 25 | */ 26 | sharedLibs: string; 27 | } 28 | export default class PluginBuilder extends BrowserBuilder { 29 | private options: PluginBuilderSchema; 30 | 31 | private entryPointPath: string; 32 | 33 | patchEntryPoint(contents: string) { 34 | fs.writeFileSync(this.entryPointPath, contents); 35 | } 36 | 37 | buildWebpackConfig( 38 | root: Path, 39 | projectRoot: Path, 40 | host: virtualFs.Host, 41 | options: PluginBuilderSchema 42 | ) { 43 | const { pluginName, sharedLibs } = this.options; 44 | 45 | if (!this.options.modulePath) { 46 | throw Error('Please define modulePath!'); 47 | } 48 | 49 | if (!pluginName) { 50 | throw Error('Please provide pluginName!'); 51 | } 52 | 53 | const config = super.buildWebpackConfig(root, projectRoot, host, options); 54 | 55 | // Make sure we are producing a single bundle 56 | delete config.entry.polyfills; 57 | delete config.optimization.runtimeChunk; 58 | delete config.optimization.splitChunks; 59 | delete config.entry.styles; 60 | 61 | config.externals = { 62 | rxjs: 'rxjs', 63 | '@angular/core': 'ng.core', 64 | '@angular/common': 'ng.common', 65 | '@angular/forms': 'ng.forms', 66 | '@angular/router': 'ng.router', 67 | tslib: 'tslib' 68 | // put here other common dependencies 69 | }; 70 | 71 | if (sharedLibs) { 72 | config.externals = [config.externals]; 73 | const sharedLibsArr = sharedLibs.split(','); 74 | sharedLibsArr.forEach(sharedLibName => { 75 | const factoryRegexp = new RegExp(`${sharedLibName}.ngfactory$`); 76 | config.externals[0][sharedLibName] = sharedLibName; // define external for code 77 | config.externals.push((context, request, callback) => { 78 | if (factoryRegexp.test(request)) { 79 | return callback(null, sharedLibName); // define external for factory 80 | } 81 | callback(); 82 | }); 83 | }); 84 | } 85 | 86 | const ngCompilerPluginInstance = config.plugins.find( 87 | x => x.constructor && x.constructor.name === 'AngularCompilerPlugin' 88 | ); 89 | if (ngCompilerPluginInstance) { 90 | ngCompilerPluginInstance._entryModule = this.options.modulePath; 91 | } 92 | 93 | // preserve path to entry point 94 | // so that we can clear use it within `run` method to clear that file 95 | this.entryPointPath = config.entry.main[0]; 96 | 97 | const [modulePath, moduleName] = this.options.modulePath.split('#'); 98 | 99 | const factoryPath = `${ 100 | modulePath.includes('.') ? modulePath : `${modulePath}/${modulePath}` 101 | }.ngfactory`; 102 | const entryPointContents = ` 103 | export * from '${modulePath}'; 104 | export * from '${factoryPath}'; 105 | import { ${moduleName}NgFactory } from '${factoryPath}'; 106 | export default ${moduleName}NgFactory; 107 | `; 108 | this.patchEntryPoint(entryPointContents); 109 | 110 | config.output.filename = `${pluginName}.js`; 111 | config.output.library = pluginName; 112 | config.output.libraryTarget = 'umd'; 113 | // workaround to support bundle on nodejs 114 | config.output.globalObject = `(typeof self !== 'undefined' ? self : this)`; 115 | 116 | return config; 117 | } 118 | 119 | run( 120 | builderConfig: BuilderConfiguration 121 | ): Observable { 122 | this.options = builderConfig.options; 123 | // I don't want to write it in my scripts every time so I keep it here 124 | builderConfig.options.deleteOutputPath = false; 125 | 126 | return super.run(builderConfig).pipe( 127 | tap(() => { 128 | // clear entry point so our main.ts is always empty 129 | this.patchEntryPoint(''); 130 | }) 131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /builders/plugin-builder/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "title": "Webpack browser schema for Build Facade.", 4 | "description": "Browser target options", 5 | "type": "object", 6 | "properties": { 7 | "modulePath": { 8 | "type": "string", 9 | "description": "Path to module like loadChildren", 10 | "default": "" 11 | }, 12 | "pluginName": { 13 | "type": "string", 14 | "description": "Name of bundled plugin", 15 | "default": "" 16 | }, 17 | "sharedLibs": { 18 | "type": "string", 19 | "description": "A comma-delimited list of shared lib names used by current plugin", 20 | "default": "" 21 | }, 22 | "assets": { 23 | "type": "array", 24 | "description": "List of static application assets.", 25 | "default": [], 26 | "items": { 27 | "$ref": "#/definitions/assetPattern" 28 | } 29 | }, 30 | "main": { 31 | "type": "string", 32 | "description": "The full path for the main entry point to the app, relative to the current workspace.", 33 | "$valueDescription": "fileName" 34 | }, 35 | "polyfills": { 36 | "type": "string", 37 | "description": "The full path for the polyfills file, relative to the current workspace." 38 | }, 39 | "tsConfig": { 40 | "type": "string", 41 | "description": "The full path for the TypeScript configuration file, relative to the current workspace." 42 | }, 43 | "scripts": { 44 | "description": "Global scripts to be included in the build.", 45 | "type": "array", 46 | "default": [], 47 | "items": { 48 | "$ref": "#/definitions/extraEntryPoint" 49 | } 50 | }, 51 | "styles": { 52 | "description": "Global styles to be included in the build.", 53 | "type": "array", 54 | "default": [], 55 | "items": { 56 | "$ref": "#/definitions/extraEntryPoint" 57 | } 58 | }, 59 | "stylePreprocessorOptions": { 60 | "description": "Options to pass to style preprocessors.", 61 | "type": "object", 62 | "properties": { 63 | "includePaths": { 64 | "description": "Paths to include. Paths will be resolved to project root.", 65 | "type": "array", 66 | "items": { 67 | "type": "string" 68 | }, 69 | "default": [] 70 | } 71 | }, 72 | "additionalProperties": false 73 | }, 74 | "optimization": { 75 | "description": "Enables optimization of the build output.", 76 | "oneOf": [ 77 | { 78 | "type": "object", 79 | "properties": { 80 | "scripts": { 81 | "type": "boolean", 82 | "description": "Enables optimization of the scripts output.", 83 | "default": true 84 | }, 85 | "styles": { 86 | "type": "boolean", 87 | "description": "Enables optimization of the styles output.", 88 | "default": true 89 | } 90 | }, 91 | "additionalProperties": false 92 | }, 93 | { 94 | "type": "boolean" 95 | } 96 | ] 97 | }, 98 | "fileReplacements": { 99 | "description": "Replace files with other files in the build.", 100 | "type": "array", 101 | "items": { 102 | "$ref": "#/definitions/fileReplacement" 103 | }, 104 | "default": [] 105 | }, 106 | "outputPath": { 107 | "type": "string", 108 | "description": "The full path for the new output directory, relative to the current workspace.\n\nBy default, writes output to a folder named dist/ in the current project." 109 | }, 110 | "resourcesOutputPath": { 111 | "type": "string", 112 | "description": "The path where style resources will be placed, relative to outputPath.", 113 | "default": "" 114 | }, 115 | "aot": { 116 | "type": "boolean", 117 | "description": "Build using Ahead of Time compilation.", 118 | "default": false 119 | }, 120 | "sourceMap": { 121 | "description": "Output sourcemaps.", 122 | "default": true, 123 | "oneOf": [ 124 | { 125 | "type": "object", 126 | "properties": { 127 | "scripts": { 128 | "type": "boolean", 129 | "description": "Output sourcemaps for all scripts.", 130 | "default": true 131 | }, 132 | "styles": { 133 | "type": "boolean", 134 | "description": "Output sourcemaps for all styles.", 135 | "default": true 136 | }, 137 | "hidden": { 138 | "type": "boolean", 139 | "description": "Output sourcemaps used for error reporting tools.", 140 | "default": false 141 | }, 142 | "vendor": { 143 | "type": "boolean", 144 | "description": "Resolve vendor packages sourcemaps.", 145 | "default": false 146 | } 147 | }, 148 | "additionalProperties": false 149 | }, 150 | { 151 | "type": "boolean" 152 | } 153 | ] 154 | }, 155 | "vendorSourceMap": { 156 | "type": "boolean", 157 | "description": "Resolve vendor packages sourcemaps.", 158 | "x-deprecated": true, 159 | "default": false 160 | }, 161 | "evalSourceMap": { 162 | "type": "boolean", 163 | "description": "Output in-file eval sourcemaps.", 164 | "default": false, 165 | "x-deprecated": true 166 | }, 167 | "vendorChunk": { 168 | "type": "boolean", 169 | "description": "Use a separate bundle containing only vendor libraries.", 170 | "default": true 171 | }, 172 | "commonChunk": { 173 | "type": "boolean", 174 | "description": "Use a separate bundle containing code used across multiple bundles.", 175 | "default": true 176 | }, 177 | "baseHref": { 178 | "type": "string", 179 | "description": "Base url for the application being built." 180 | }, 181 | "deployUrl": { 182 | "type": "string", 183 | "description": "URL where files will be deployed." 184 | }, 185 | "verbose": { 186 | "type": "boolean", 187 | "description": "Adds more details to output logging.", 188 | "default": false 189 | }, 190 | "progress": { 191 | "type": "boolean", 192 | "description": "Log progress to the console while building." 193 | }, 194 | "i18nFile": { 195 | "type": "string", 196 | "description": "Localization file to use for i18n." 197 | }, 198 | "i18nFormat": { 199 | "type": "string", 200 | "description": "Format of the localization file specified with --i18n-file." 201 | }, 202 | "i18nLocale": { 203 | "type": "string", 204 | "description": "Locale to use for i18n." 205 | }, 206 | "i18nMissingTranslation": { 207 | "type": "string", 208 | "description": "How to handle missing translations for i18n." 209 | }, 210 | "extractCss": { 211 | "type": "boolean", 212 | "description": "Extract css from global styles into css files instead of js ones.", 213 | "default": false 214 | }, 215 | "watch": { 216 | "type": "boolean", 217 | "description": "Run build when files change.", 218 | "default": false 219 | }, 220 | "outputHashing": { 221 | "type": "string", 222 | "description": "Define the output filename cache-busting hashing mode.", 223 | "default": "none", 224 | "enum": ["none", "all", "media", "bundles"] 225 | }, 226 | "poll": { 227 | "type": "number", 228 | "description": "Enable and define the file watching poll time period in milliseconds." 229 | }, 230 | "deleteOutputPath": { 231 | "type": "boolean", 232 | "description": "Delete the output path before building.", 233 | "default": true 234 | }, 235 | "preserveSymlinks": { 236 | "type": "boolean", 237 | "description": "Do not use the real path when resolving modules.", 238 | "default": false 239 | }, 240 | "extractLicenses": { 241 | "type": "boolean", 242 | "description": "Extract all licenses in a separate file.", 243 | "default": false 244 | }, 245 | "showCircularDependencies": { 246 | "type": "boolean", 247 | "description": "Show circular dependency warnings on builds.", 248 | "default": true 249 | }, 250 | "buildOptimizer": { 251 | "type": "boolean", 252 | "description": "Enables '@angular-devkit/build-optimizer' optimizations when using the 'aot' option.", 253 | "default": false 254 | }, 255 | "namedChunks": { 256 | "type": "boolean", 257 | "description": "Use file name for lazy loaded chunks.", 258 | "default": true 259 | }, 260 | "subresourceIntegrity": { 261 | "type": "boolean", 262 | "description": "Enables the use of subresource integrity validation.", 263 | "default": false 264 | }, 265 | "serviceWorker": { 266 | "type": "boolean", 267 | "description": "Generates a service worker config for production builds.", 268 | "default": false 269 | }, 270 | "ngswConfigPath": { 271 | "type": "string", 272 | "description": "Path to ngsw-config.json." 273 | }, 274 | "skipAppShell": { 275 | "type": "boolean", 276 | "description": "Flag to prevent building an app shell.", 277 | "default": false, 278 | "x-deprecated": true 279 | }, 280 | "index": { 281 | "type": "string", 282 | "description": "The name of the index HTML file." 283 | }, 284 | "statsJson": { 285 | "type": "boolean", 286 | "description": "Generates a 'stats.json' file which can be analyzed using tools such as 'webpack-bundle-analyzer'.", 287 | "default": false 288 | }, 289 | "forkTypeChecker": { 290 | "type": "boolean", 291 | "description": "Run the TypeScript type checker in a forked process.", 292 | "default": true 293 | }, 294 | "lazyModules": { 295 | "description": "List of additional NgModule files that will be lazy loaded. Lazy router modules will be discovered automatically.", 296 | "type": "array", 297 | "items": { 298 | "type": "string" 299 | }, 300 | "default": [] 301 | }, 302 | "budgets": { 303 | "description": "Budget thresholds to ensure parts of your application stay within boundaries which you set.", 304 | "type": "array", 305 | "items": { 306 | "$ref": "#/definitions/budget" 307 | }, 308 | "default": [] 309 | }, 310 | "profile": { 311 | "type": "boolean", 312 | "description": "Output profile events for Chrome profiler.", 313 | "default": false 314 | }, 315 | "es5BrowserSupport": { 316 | "description": "Enables conditionally loaded ES2015 polyfills.", 317 | "type": "boolean", 318 | "default": false 319 | }, 320 | "rebaseRootRelativeCssUrls": { 321 | "description": "Change root relative URLs in stylesheets to include base HREF and deploy URL. Use only for compatibility and transition. The behavior of this option is non-standard and will be removed in the next major release.", 322 | "type": "boolean", 323 | "default": false, 324 | "x-deprecated": true 325 | } 326 | }, 327 | "additionalProperties": false, 328 | "required": ["outputPath", "index", "main", "tsConfig"], 329 | "definitions": { 330 | "assetPattern": { 331 | "oneOf": [ 332 | { 333 | "type": "object", 334 | "properties": { 335 | "glob": { 336 | "type": "string", 337 | "description": "The pattern to match." 338 | }, 339 | "input": { 340 | "type": "string", 341 | "description": "The input directory path in which to apply 'glob'. Defaults to the project root." 342 | }, 343 | "ignore": { 344 | "description": "An array of globs to ignore.", 345 | "type": "array", 346 | "items": { 347 | "type": "string" 348 | } 349 | }, 350 | "output": { 351 | "type": "string", 352 | "description": "Absolute path within the output." 353 | } 354 | }, 355 | "additionalProperties": false, 356 | "required": ["glob", "input", "output"] 357 | }, 358 | { 359 | "type": "string" 360 | } 361 | ] 362 | }, 363 | "fileReplacement": { 364 | "oneOf": [ 365 | { 366 | "type": "object", 367 | "properties": { 368 | "src": { 369 | "type": "string" 370 | }, 371 | "replaceWith": { 372 | "type": "string" 373 | } 374 | }, 375 | "additionalProperties": false, 376 | "required": ["src", "replaceWith"] 377 | }, 378 | { 379 | "type": "object", 380 | "properties": { 381 | "replace": { 382 | "type": "string" 383 | }, 384 | "with": { 385 | "type": "string" 386 | } 387 | }, 388 | "additionalProperties": false, 389 | "required": ["replace", "with"] 390 | } 391 | ] 392 | }, 393 | "extraEntryPoint": { 394 | "oneOf": [ 395 | { 396 | "type": "object", 397 | "properties": { 398 | "input": { 399 | "type": "string", 400 | "description": "The file to include." 401 | }, 402 | "bundleName": { 403 | "type": "string", 404 | "description": "The bundle name for this extra entry point." 405 | }, 406 | "lazy": { 407 | "type": "boolean", 408 | "description": "If the bundle will be lazy loaded.", 409 | "default": false 410 | } 411 | }, 412 | "additionalProperties": false, 413 | "required": ["input"] 414 | }, 415 | { 416 | "type": "string", 417 | "description": "The file to include." 418 | } 419 | ] 420 | }, 421 | "budget": { 422 | "type": "object", 423 | "properties": { 424 | "type": { 425 | "type": "string", 426 | "description": "The type of budget.", 427 | "enum": ["all", "allScript", "any", "anyScript", "bundle", "initial"] 428 | }, 429 | "name": { 430 | "type": "string", 431 | "description": "The name of the bundle." 432 | }, 433 | "baseline": { 434 | "type": "string", 435 | "description": "The baseline size for comparison." 436 | }, 437 | "maximumWarning": { 438 | "type": "string", 439 | "description": "The maximum threshold for warning relative to the baseline." 440 | }, 441 | "maximumError": { 442 | "type": "string", 443 | "description": "The maximum threshold for error relative to the baseline." 444 | }, 445 | "minimumWarning": { 446 | "type": "string", 447 | "description": "The minimum threshold for warning relative to the baseline." 448 | }, 449 | "minimumError": { 450 | "type": "string", 451 | "description": "The minimum threshold for error relative to the baseline." 452 | }, 453 | "warning": { 454 | "type": "string", 455 | "description": "The threshold for warning relative to the baseline (min & max)." 456 | }, 457 | "error": { 458 | "type": "string", 459 | "description": "The threshold for error relative to the baseline (min & max)." 460 | } 461 | }, 462 | "additionalProperties": false, 463 | "required": ["type"] 464 | } 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /builders/tsconfig.builders.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "./plugin-builder", 6 | "importHelpers": false, 7 | "sourceMap": false, 8 | "target": "es6" 9 | }, 10 | "files": [ 11 | "./plugin-builder/index.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-plugin-architecture", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve -o", 7 | "build": "ng build", 8 | "lint": "ng lint", 9 | "postinstall": "tsc -p builders/tsconfig.builders.json", 10 | "build:shared": "ng build shared && ng build --project plugins --prod --modulePath=shared#SharedModule --pluginName=shared --outputPath=./src/assets/plugins", 11 | "build:plugin1": "ng build --project plugins --prod --modulePath=./plugin1/plugin1.module#Plugin1Module --pluginName=plugin1 --sharedLibs=shared --outputPath=./src/assets/plugins", 12 | "build:plugin2": "ng build --project plugins --prod --modulePath=./plugin2/plugin2.module#Plugin2Module --pluginName=plugin2 --sharedLibs=shared --outputPath=./src/assets/plugins ", 13 | "build:plugins": "npm run build:shared && npm run build:plugin1 && npm run build:plugin2", 14 | "compile:server": "webpack --config webpack.server.config.js --progress --colors", 15 | "serve:ssr": "node dist/server", 16 | "build:ssr": "npm run build:client-and-server-bundles && npm run compile:server", 17 | "build:client-and-server-bundles": "ng build --prod && ng run angular-plugin-architecture:server:production" 18 | }, 19 | "private": true, 20 | "dependencies": { 21 | "@angular/animations": "~7.2.0", 22 | "@angular/common": "~7.2.0", 23 | "@angular/compiler": "~7.2.0", 24 | "@angular/core": "~7.2.0", 25 | "@angular/forms": "~7.2.0", 26 | "@angular/http": "~7.2.0", 27 | "@angular/platform-browser": "~7.2.0", 28 | "@angular/platform-browser-dynamic": "~7.2.0", 29 | "@angular/platform-server": "~7.2.0", 30 | "@angular/router": "~7.2.0", 31 | "@nguniversal/express-engine": "^7.1.1", 32 | "@nguniversal/module-map-ngfactory-loader": "0.0.0", 33 | "core-js": "^2.5.4", 34 | "express": "^4.15.2", 35 | "rxjs": "~6.3.3", 36 | "systemjs": "3.0.2", 37 | "tslib": "^1.9.0", 38 | "zone.js": "~0.8.26" 39 | }, 40 | "devDependencies": { 41 | "@angular-devkit/build-angular": "~0.13.0", 42 | "@angular-devkit/build-ng-packagr": "~0.13.0", 43 | "@angular/cli": "~7.3.6", 44 | "@angular/compiler-cli": "~7.2.0", 45 | "@angular/language-service": "~7.2.0", 46 | "@types/node": "~8.9.4", 47 | "codelyzer": "^5.0.0-beta.1", 48 | "ng-packagr": "^4.2.0", 49 | "prettier": "^1.16.4", 50 | "ts-loader": "^5.2.0", 51 | "ts-node": "~7.0.0", 52 | "tsickle": ">=0.34.0", 53 | "tslib": "^1.9.0", 54 | "tslint": "~5.11.0", 55 | "typescript": "~3.2.2", 56 | "webpack-cli": "^3.1.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /projects/plugins/src/main.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexzuza/angular-plugin-architecture/9df09f3780261ddbf292ce1584d06258f3db90a1/projects/plugins/src/main.ts -------------------------------------------------------------------------------- /projects/plugins/src/plugin1/plugin1.component.html: -------------------------------------------------------------------------------- 1 |

Plugin 1

2 |

Hidden text

3 | 4 | 5 | 6 | Tab 1 contents 7 | 8 | 9 | Tab 2 contents 10 | 11 | 12 | Tab 3 contents 13 | 14 | -------------------------------------------------------------------------------- /projects/plugins/src/plugin1/plugin1.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-plugin-1', 5 | templateUrl: './plugin1.component.html' 6 | }) 7 | export class Plugin1Component { 8 | x = false; 9 | } 10 | -------------------------------------------------------------------------------- /projects/plugins/src/plugin1/plugin1.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { Plugin1Component } from './plugin1.component'; 5 | import { SharedModule } from 'shared'; 6 | 7 | @NgModule({ 8 | imports: [CommonModule, SharedModule], 9 | declarations: [Plugin1Component], 10 | entryComponents: [Plugin1Component] 11 | }) 12 | export class Plugin1Module { 13 | static entry = Plugin1Component; 14 | } 15 | -------------------------------------------------------------------------------- /projects/plugins/src/plugin2/plugin2.component.html: -------------------------------------------------------------------------------- 1 |

Plugin 2

2 | 3 | 4 | Tab 1 contents 5 | 6 | 7 | Tab 2 contents 8 | 9 | 10 | Tab 3 contents 11 | 12 | -------------------------------------------------------------------------------- /projects/plugins/src/plugin2/plugin2.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-plugin-2', 5 | templateUrl: './plugin2.component.html' 6 | }) 7 | export class Plugin2Component {} 8 | -------------------------------------------------------------------------------- /projects/plugins/src/plugin2/plugin2.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { Plugin2Component } from './plugin2.component'; 5 | import { SharedModule } from 'shared'; 6 | 7 | @NgModule({ 8 | imports: [CommonModule, SharedModule], 9 | declarations: [Plugin2Component], 10 | entryComponents: [Plugin2Component] 11 | }) 12 | export class Plugin2Module { 13 | static entry = Plugin2Component; 14 | } 15 | -------------------------------------------------------------------------------- /projects/plugins/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/plugins", 5 | "types": [] 6 | }, 7 | "files": ["./src/main.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /projects/shared/README.md: -------------------------------------------------------------------------------- 1 | # Shared 2 | 3 | This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.2.0. 4 | 5 | ## Code scaffolding 6 | 7 | Run `ng generate component component-name --project shared` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project shared`. 8 | 9 | > Note: Don't forget to add `--project shared` or else it will be added to the default project in your `angular.json` file. 10 | 11 | ## Build 12 | 13 | Run `ng build shared` to build the project. The build artifacts will be stored in the `dist/` directory. 14 | 15 | ## Publishing 16 | 17 | After building your library with `ng build shared`, go to the dist folder `cd dist/shared` and run `npm publish`. 18 | 19 | ## Running unit tests 20 | 21 | Run `ng test shared` to execute the unit tests via [Karma](https://karma-runner.github.io). 22 | 23 | ## Further help 24 | 25 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 26 | -------------------------------------------------------------------------------- /projects/shared/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/shared'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/shared/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/shared", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared", 3 | "version": "0.0.1", 4 | "peerDependencies": { 5 | "@angular/common": "^7.2.0", 6 | "@angular/core": "^7.2.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /projects/shared/src/lib/button/button.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | padding: 0 15px; 3 | border: 1px solid #d8dde6; 4 | border-radius: 6px; 5 | line-height: 40px; 6 | cursor: pointer; 7 | background: #fff; 8 | 9 | &:hover { 10 | background: #f3f7fb; 11 | } 12 | } -------------------------------------------------------------------------------- /projects/shared/src/lib/button/button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | // tslint:disable-next-line: component-selector 5 | selector: 'button[sharedBtn]', 6 | template: '', 7 | styleUrls: ['./button.component.scss'] 8 | }) 9 | export class ButtonComponent {} 10 | -------------------------------------------------------------------------------- /projects/shared/src/lib/shared.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'shared-component', 5 | template: ` 6 |

7 | Shared component 8 |

9 | `, 10 | styles: [] 11 | }) 12 | export class SharedComponent {} 13 | -------------------------------------------------------------------------------- /projects/shared/src/lib/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { SharedComponent } from './shared.component'; 5 | import { TabsComponent } from './tabs/tabs.component'; 6 | import { TabComponent } from './tabs/tab.component'; 7 | import { ButtonComponent } from './button/button.component'; 8 | 9 | const sharedComponents = [SharedComponent, ButtonComponent, TabComponent, TabsComponent]; 10 | 11 | @NgModule({ 12 | imports: [CommonModule], 13 | declarations: [...sharedComponents], 14 | exports: [...sharedComponents] 15 | }) 16 | export class SharedModule {} 17 | -------------------------------------------------------------------------------- /projects/shared/src/lib/tabs/tab.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, Input } from '@angular/core'; 2 | import { Tab } from './tab.interface'; 3 | import { TabsComponent } from './tabs.component'; 4 | 5 | @Component({ 6 | selector: 'shared-tab', 7 | template: '' 8 | }) 9 | export class TabComponent implements Tab { 10 | @Input() title: string; 11 | 12 | @HostBinding('hidden') hidden = true; 13 | 14 | constructor(tabsComponent: TabsComponent) { 15 | tabsComponent.addTab(this); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/shared/src/lib/tabs/tab.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Tab { 2 | title: string; 3 | hidden: boolean; 4 | } 5 | -------------------------------------------------------------------------------- /projects/shared/src/lib/tabs/tabs.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • 3 | {{tab.title}} 4 |
  • 5 |
6 |
7 | 8 |
-------------------------------------------------------------------------------- /projects/shared/src/lib/tabs/tabs.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } 4 | 5 | .tabs { 6 | display: flex; 7 | list-style: none; 8 | margin: 0; 9 | padding: 0; 10 | border-bottom: 1px solid #ebeef2; 11 | } 12 | 13 | .tab { 14 | position: relative; 15 | padding: 0 20px; 16 | line-height: 40px; 17 | cursor: pointer; 18 | 19 | &-body { 20 | padding: 20px; 21 | } 22 | 23 | &--active:before { 24 | content: ''; 25 | position: absolute; 26 | bottom: 0; 27 | left: 0; 28 | right: 0; 29 | height: 3px; 30 | background: #03a9f4; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /projects/shared/src/lib/tabs/tabs.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Output } from '@angular/core'; 2 | import { Tab } from './tab.interface'; 3 | 4 | @Component({ 5 | selector: 'shared-tabs', 6 | templateUrl: './tabs.component.html', 7 | styleUrls: ['./tabs.component.scss'] 8 | }) 9 | export class TabsComponent { 10 | tabs: Tab[] = []; 11 | 12 | @Output() selected = new EventEmitter(); 13 | 14 | addTab(tabComponent: Tab) { 15 | if (!this.tabs.length) { 16 | tabComponent.hidden = false; 17 | } 18 | this.tabs.push(tabComponent); 19 | } 20 | 21 | selectTab(tabComponent: Tab) { 22 | this.tabs.map(tab => (tab.hidden = true)); 23 | tabComponent.hidden = false; 24 | this.selected.emit(tabComponent); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/shared/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of shared 3 | */ 4 | 5 | export * from './lib/shared.component'; 6 | export * from './lib/shared.module'; 7 | -------------------------------------------------------------------------------- /projects/shared/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js/dist/zone'; 5 | import 'zone.js/dist/zone-testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting() 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /projects/shared/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "types": [], 15 | "lib": ["dom", "es2018"] 16 | }, 17 | "angularCompilerOptions": { 18 | "annotateForClosureCompiler": true, 19 | "skipTemplateCodegen": true, 20 | "strictMetadataEmit": true, 21 | "fullTemplateTypeCheck": true, 22 | "strictInjectionParameters": true, 23 | "enableResourceInlining": true 24 | }, 25 | "exclude": ["src/test.ts", "**/*.spec.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /projects/shared/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts"], 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /projects/shared/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "shared", "camelCase"], 5 | "component-selector": [true, "element", "shared", "kebab-case"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/zone-node'; 2 | import {enableProdMode} from '@angular/core'; 3 | // Express Engine 4 | import {ngExpressEngine} from '@nguniversal/express-engine'; 5 | // Import module map for lazy loading 6 | import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader'; 7 | 8 | import * as express from 'express'; 9 | import {join} from 'path'; 10 | 11 | // Faster server renders w/ Prod mode (dev mode never needed) 12 | enableProdMode(); 13 | 14 | // Express server 15 | const app = express(); 16 | 17 | const PORT = process.env.PORT || 4000; 18 | const DIST_FOLDER = join(process.cwd(), 'dist/browser'); 19 | 20 | // * NOTE :: leave this as require() since this file is built Dynamically from webpack 21 | const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main'); 22 | 23 | declare let __non_webpack_require__: typeof require; 24 | global['require'] = __non_webpack_require__; 25 | 26 | // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) 27 | app.engine('html', (_, options, callback) => { 28 | const protocol = options.req.protocol; 29 | const host = options.req.get('host'); 30 | 31 | const engine = ngExpressEngine({ 32 | bootstrap: AppServerModuleNgFactory, 33 | providers: [ 34 | provideModuleMap(LAZY_MODULE_MAP), 35 | { provide: 'APP_BASE_URL', useFactory: () => `${protocol}://${host}`, deps: [] }, 36 | ] 37 | }); 38 | engine(_, options, callback); 39 | }); 40 | 41 | app.set('view engine', 'html'); 42 | app.set('views', DIST_FOLDER); 43 | 44 | // Example Express Rest API endpoints 45 | // app.get('/api/**', (req, res) => { }); 46 | // Serve static files from /browser 47 | app.get('*.*', express.static(DIST_FOLDER, { 48 | maxAge: '1y' 49 | })); 50 | 51 | // All regular routes use the Universal engine 52 | app.get('*', (req, res) => { 53 | res.render('index', { req }); 54 | }); 55 | 56 | // Start up the Node server 57 | app.listen(PORT, () => { 58 | console.log(`Node Express server listening on http://localhost:${PORT}`); 59 | }); 60 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

Example of building AOT compiled plugin

2 | 3 |
4 |
5 | 6 |
7 |
8 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | max-width: 1200px; 4 | margin: 0 auto; 5 | } 6 | 7 | @media (max-width: 1200px) { 8 | :host { 9 | max-width: none; 10 | margin: 0 25px; 11 | } 12 | } 13 | 14 | .plugins { 15 | display: flex; 16 | flex-wrap: wrap; 17 | margin: 20px -10px; 18 | 19 | ::ng-deep { 20 | > * { 21 | width: calc((100% - 80px)/2); 22 | margin: 0 10px 20px 10px; 23 | padding: 20px; 24 | border-radius: 6px; 25 | background: #fff; 26 | box-shadow: 0 2px 12px 0 #dfe3eb; 27 | box-sizing: border-box; 28 | } 29 | } 30 | } 31 | 32 | 33 | button { 34 | border: none; 35 | background: #fff; 36 | line-height: 40px; 37 | padding: 0 15px; 38 | cursor: pointer; 39 | border-radius: 6px; 40 | box-shadow: 0 2px 12px 0 #dfe3eb; 41 | } -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Injector, 4 | OnInit, 5 | ViewChild, 6 | ViewContainerRef 7 | } from '@angular/core'; 8 | import { PluginLoaderService } from './services/plugin-loader/plugin-loader.service'; 9 | 10 | @Component({ 11 | selector: 'app-root', 12 | templateUrl: './app.component.html', 13 | styleUrls: ['./app.component.scss'] 14 | }) 15 | export class AppComponent implements OnInit { 16 | @ViewChild('targetRef', { read: ViewContainerRef }) vcRef: ViewContainerRef; 17 | 18 | constructor( 19 | private injector: Injector, 20 | private pluginLoader: PluginLoaderService 21 | ) {} 22 | 23 | ngOnInit() { 24 | this.loadPlugin('plugin1'); 25 | } 26 | 27 | loadPlugin(pluginName: string) { 28 | this.pluginLoader.load(pluginName).then(moduleFactory => { 29 | const moduleRef = moduleFactory.create(this.injector); 30 | const entryComponent = (moduleFactory.moduleType as any).entry; 31 | const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory( 32 | entryComponent 33 | ); 34 | this.vcRef.createComponent(compFactory); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BrowserModule, 3 | BrowserTransferStateModule 4 | } from '@angular/platform-browser'; 5 | import { APP_INITIALIZER, NgModule } from '@angular/core'; 6 | import { HttpClientModule } from '@angular/common/http'; 7 | 8 | import { AppComponent } from './app.component'; 9 | import { PluginLoaderService } from './services/plugin-loader/plugin-loader.service'; 10 | import { ClientPluginLoaderService } from './services/plugin-loader/client-plugin-loader.service'; 11 | import { PluginsConfigProvider } from './services/plugins-config.provider'; 12 | import { TransferStateService } from './services/transfer-state.service'; 13 | 14 | @NgModule({ 15 | declarations: [AppComponent], 16 | imports: [ 17 | HttpClientModule, 18 | BrowserModule.withServerTransition({ appId: 'serverApp' }), 19 | BrowserTransferStateModule 20 | ], 21 | providers: [ 22 | { provide: PluginLoaderService, useClass: ClientPluginLoaderService }, 23 | PluginsConfigProvider, 24 | { 25 | provide: APP_INITIALIZER, 26 | useFactory: (provider: PluginsConfigProvider) => () => 27 | provider 28 | .loadConfig() 29 | .toPromise() 30 | .then(config => (provider.config = config)), 31 | multi: true, 32 | deps: [PluginsConfigProvider] 33 | } 34 | ], 35 | bootstrap: [AppComponent] 36 | }) 37 | export class AppModule { 38 | constructor(transferStateService: TransferStateService) {} 39 | } 40 | -------------------------------------------------------------------------------- /src/app/app.server.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { 3 | ServerModule, 4 | ServerTransferStateModule 5 | } from '@angular/platform-server'; 6 | 7 | import { AppModule } from './app.module'; 8 | import { AppComponent } from './app.component'; 9 | import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; 10 | import { PluginLoaderService } from './services/plugin-loader/plugin-loader.service'; 11 | import { ServerPluginLoaderService } from './services/plugin-loader/server-plugin-loader.service'; 12 | 13 | @NgModule({ 14 | imports: [ 15 | AppModule, 16 | ServerModule, 17 | ServerTransferStateModule, 18 | ModuleMapLoaderModule 19 | ], 20 | providers: [ 21 | { provide: PluginLoaderService, useClass: ServerPluginLoaderService } 22 | ], 23 | bootstrap: [AppComponent] 24 | }) 25 | export class AppServerModule {} 26 | -------------------------------------------------------------------------------- /src/app/services/plugin-loader/client-plugin-loader.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgModuleFactory } from '@angular/core'; 2 | import { PluginLoaderService } from './plugin-loader.service'; 3 | import { PLUGIN_EXTERNALS_MAP } from './plugin-externals'; 4 | import { PluginsConfigProvider } from '../plugins-config.provider'; 5 | 6 | const SystemJs = window.System; 7 | 8 | @Injectable() 9 | export class ClientPluginLoaderService extends PluginLoaderService { 10 | constructor(private configProvider: PluginsConfigProvider) { 11 | super(); 12 | } 13 | 14 | provideExternals() { 15 | Object.keys(PLUGIN_EXTERNALS_MAP).forEach(externalKey => 16 | window.define(externalKey, [], () => PLUGIN_EXTERNALS_MAP[externalKey]) 17 | ); 18 | } 19 | 20 | load(pluginName): Promise> { 21 | const { config } = this.configProvider; 22 | if (!config[pluginName]) { 23 | throw Error(`Can't find appropriate plugin`); 24 | } 25 | 26 | const depsPromises = (config[pluginName].deps || []).map(dep => { 27 | return SystemJs.import(config[dep].path).then(m => { 28 | window['define'](dep, [], () => m.default); 29 | }); 30 | }); 31 | 32 | return Promise.all(depsPromises).then(() => { 33 | return SystemJs.import(config[pluginName].path).then( 34 | module => module.default.default 35 | ); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/services/plugin-loader/plugin-externals.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@angular/core'; 2 | import * as common from '@angular/common'; 3 | import * as forms from '@angular/forms'; 4 | import * as router from '@angular/router'; 5 | import * as rxjs from 'rxjs'; 6 | import * as tslib from 'tslib'; 7 | 8 | export const PLUGIN_EXTERNALS_MAP = { 9 | 'ng.core': core, 10 | 'ng.common': common, 11 | 'ng.forms': forms, 12 | 'ng.router': router, 13 | rxjs, 14 | tslib 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/services/plugin-loader/plugin-loader.service.ts: -------------------------------------------------------------------------------- 1 | import { NgModuleFactory } from '@angular/core'; 2 | 3 | export abstract class PluginLoaderService { 4 | protected constructor() { 5 | this.provideExternals(); 6 | } 7 | 8 | abstract provideExternals(): void; 9 | 10 | abstract load(pluginName): Promise>; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/services/plugin-loader/server-plugin-loader.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgModuleFactory } from '@angular/core'; 2 | import { PluginLoaderService } from './plugin-loader.service'; 3 | import { PLUGIN_EXTERNALS_MAP } from './plugin-externals'; 4 | import { PluginsConfigProvider } from '../plugins-config.provider'; 5 | 6 | declare let global: any; 7 | 8 | @Injectable() 9 | export class ServerPluginLoaderService extends PluginLoaderService { 10 | constructor(private configProvider: PluginsConfigProvider) { 11 | super(); 12 | } 13 | 14 | provideExternals() { 15 | const that = this; 16 | const Module = global['require']('module'); 17 | const originalRequire = Module.prototype.require; 18 | Module.prototype.require = function(name) { 19 | if (that.configProvider.config[name]) { 20 | return global['require']( 21 | `./browser${that.configProvider.config[name].path}` 22 | ); 23 | } 24 | return ( 25 | PLUGIN_EXTERNALS_MAP[name] || originalRequire.apply(this, arguments) 26 | ); 27 | }; 28 | } 29 | 30 | load(pluginName): Promise> { 31 | const { config } = this.configProvider; 32 | if (!config[pluginName]) { 33 | throw Error(`Can't find appropriate plugin`); 34 | } 35 | 36 | const factory = global['require'](`./browser${config[pluginName].path}`) 37 | .default; 38 | return Promise.resolve(factory); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/services/plugins-config.provider.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Optional, PLATFORM_ID } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { tap } from 'rxjs/operators'; 4 | import { preserveServerState } from './transfer-state.service'; 5 | import { isPlatformBrowser } from '@angular/common'; 6 | 7 | interface PluginsConfig { 8 | [key: string]: { 9 | name: string; 10 | path: string; 11 | deps: string[]; 12 | }; 13 | } 14 | 15 | @Injectable() 16 | export class PluginsConfigProvider { 17 | config: PluginsConfig; 18 | 19 | constructor( 20 | private http: HttpClient, 21 | @Inject(PLATFORM_ID) private platformId: {}, 22 | @Inject('APP_BASE_URL') @Optional() private readonly baseUrl: string 23 | ) { 24 | if (isPlatformBrowser(platformId)) { 25 | this.baseUrl = document.location.origin; 26 | } 27 | } 28 | 29 | @preserveServerState('PLUGIN_CONFIGS') 30 | loadConfig() { 31 | return this.http.get( 32 | `${this.baseUrl}/assets/plugins-config.json` 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/services/transfer-state.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; 2 | import { makeStateKey, TransferState } from '@angular/platform-browser'; 3 | import { isPlatformBrowser, isPlatformServer } from '@angular/common'; 4 | import { of } from 'rxjs'; 5 | import { tap } from 'rxjs/operators'; 6 | 7 | let isBrowser: boolean; 8 | let isServer: boolean; 9 | let transferState: TransferState; 10 | 11 | @Injectable({ 12 | providedIn: 'root' 13 | }) 14 | export class TransferStateService { 15 | constructor( 16 | private state: TransferState, 17 | @Inject(PLATFORM_ID) private platformId: any 18 | ) { 19 | transferState = state; 20 | isBrowser = isPlatformBrowser(this.platformId); 21 | isServer = isPlatformServer(this.platformId); 22 | } 23 | } 24 | 25 | export const preserveServerState = ( 26 | keyName: string, 27 | emptyResult: any = null 28 | ) => { 29 | const key = makeStateKey(keyName); 30 | return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { 31 | const method = descriptor.value; 32 | descriptor.value = function() { 33 | if (isBrowser && transferState.hasKey(key)) { 34 | const state = transferState.get(key, emptyResult); 35 | return of(state); 36 | } 37 | 38 | return method.apply(this, arguments).pipe( 39 | tap(res => { 40 | if (isServer) { 41 | transferState.set(key, res); 42 | } 43 | }) 44 | ); 45 | }; 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexzuza/angular-plugin-architecture/9df09f3780261ddbf292ce1584d06258f3db90a1/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/plugins-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin1": { 3 | "name": "Plugin 2", 4 | "path": "/assets/plugins/plugin1.js", 5 | "deps": ["shared"] 6 | }, 7 | "plugin2": { 8 | "name": "Plugin 2", 9 | "path": "/assets/plugins/plugin2.js", 10 | "deps": ["shared"] 11 | }, 12 | "shared": { 13 | "name": "Shared", 14 | "path": "/assets/plugins/shared.js" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexzuza/angular-plugin-architecture/9df09f3780261ddbf292ce1584d06258f3db90a1/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Plugin Architecture 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.server.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | 3 | import { environment } from './environments/environment'; 4 | 5 | if (environment.production) { 6 | enableProdMode(); 7 | } 8 | 9 | export { AppServerModule } from './app/app.server.module'; 10 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | document.addEventListener('DOMContentLoaded', () => { 12 | platformBrowserDynamic() 13 | .bootstrapModule(AppModule) 14 | .catch(err => console.error(err)); 15 | }); 16 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | body { 3 | margin: 0; 4 | font-family: sans-serif; 5 | font-size: 14px; 6 | background: #eef2f7; 7 | } 8 | 9 | h3 { 10 | margin-top: 0; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": ["test.ts", "**/*.spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app-server", 5 | "baseUrl": "." 6 | }, 7 | "angularCompilerOptions": { 8 | "entryModule": "app/app.server.module#AppServerModule" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | define: (name: string, deps: string[], definitionFn: () => any) => void; 3 | 4 | System: { 5 | import: (path) => Promise; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ], 21 | "paths": { 22 | "shared": [ 23 | "dist/shared" 24 | ], 25 | "shared/*": [ 26 | "dist/shared/*" 27 | ] 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["projects"] 4 | } 5 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "array-type": false, 8 | "arrow-parens": false, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "import-blacklist": [ 13 | true, 14 | "rxjs/Rx" 15 | ], 16 | "interface-name": false, 17 | "max-classes-per-file": false, 18 | "member-access": false, 19 | "member-ordering": [ 20 | true, 21 | { 22 | "order": [ 23 | "static-field", 24 | "instance-field", 25 | "static-method", 26 | "instance-method" 27 | ] 28 | } 29 | ], 30 | "no-consecutive-blank-lines": false, 31 | "no-console": [ 32 | true, 33 | "debug", 34 | "info", 35 | "time", 36 | "timeEnd", 37 | "trace" 38 | ], 39 | "no-empty": false, 40 | "no-inferrable-types": [ 41 | true, 42 | "ignore-params" 43 | ], 44 | "no-non-null-assertion": true, 45 | "no-redundant-jsdoc": true, 46 | "no-switch-case-fall-through": true, 47 | "no-use-before-declare": true, 48 | "no-var-requires": false, 49 | "no-string-literal": false, 50 | "object-literal-key-quotes": [ 51 | true, 52 | "as-needed" 53 | ], 54 | "object-literal-sort-keys": false, 55 | "ordered-imports": false, 56 | "quotemark": [ 57 | true, 58 | "single" 59 | ], 60 | "trailing-comma": false, 61 | "no-output-on-prefix": true, 62 | "use-input-property-decorator": true, 63 | "use-output-property-decorator": true, 64 | "use-host-property-decorator": true, 65 | "no-input-rename": true, 66 | "no-output-rename": true, 67 | "use-life-cycle-interface": true, 68 | "use-pipe-transform-interface": true, 69 | "component-class-suffix": true, 70 | "directive-class-suffix": true 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /webpack.server.config.js: -------------------------------------------------------------------------------- 1 | // Work around for https://github.com/angular/angular-cli/issues/7200 2 | 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | module.exports = { 7 | mode: 'none', 8 | entry: { 9 | // This is our Express server for Dynamic universal 10 | server: './server.ts' 11 | }, 12 | target: 'node', 13 | resolve: { extensions: ['.ts', '.js'] }, 14 | optimization: { 15 | minimize: false 16 | }, 17 | output: { 18 | // Puts the output at the root of the dist folder 19 | path: path.join(__dirname, 'dist'), 20 | filename: '[name].js' 21 | }, 22 | module: { 23 | rules: [ 24 | { test: /\.ts$/, loader: 'ts-loader', options: { configFile: 'tsconfig.node.json' } }, 25 | { 26 | // Mark files inside `@angular/core` as using SystemJS style dynamic imports. 27 | // Removing this will cause deprecation warnings to appear. 28 | test: /(\\|\/)@angular(\\|\/)core(\\|\/).+\.js$/, 29 | parser: { system: true }, 30 | }, 31 | ] 32 | }, 33 | plugins: [ 34 | new webpack.ContextReplacementPlugin( 35 | // fixes WARNING Critical dependency: the request of a dependency is an expression 36 | /(.+)?angular(\\|\/)core(.+)?/, 37 | path.join(__dirname, 'src'), // location of your src 38 | {} // a map of your routes 39 | ), 40 | new webpack.ContextReplacementPlugin( 41 | // fixes WARNING Critical dependency: the request of a dependency is an expression 42 | /(.+)?express(\\|\/)(.+)?/, 43 | path.join(__dirname, 'src'), 44 | {} 45 | ) 46 | ] 47 | }; 48 | --------------------------------------------------------------------------------