1 | /**
|
2 | * This module must not import anything globally not workin in web-mode! if needed, require it within the functions
|
3 | *
|
4 | * NOTE, we Ignore the infrastructure-scripts libraries when bundling, so these can be used ...
|
5 | * We also put fs to empty! If you need other libs, add them to `node: { fs: empty }`
|
6 | */
|
7 | import { isServiceOrientedApp } from './soa-component';
|
8 | import { resolveAssetsPath } from '../libs/iso-libs';
|
9 | import * as deepmerge from 'deepmerge';
|
10 | import { IConfigParseResult } from '../libs/config-parse-result';
|
11 | import {IPlugin, forwardChildIamRoleStatements} from '../libs/plugin';
|
12 | import { PARSER_MODES } from '../libs/parser';
|
13 |
|
14 | import extractDomain from 'extract-domain';
|
15 |
|
16 | /**
|
17 | * Parameters that apply to the whole Plugin, passed by other plugins
|
18 | */
|
19 | export interface ISoaPlugin {
|
20 |
|
21 | /**
|
22 | * the stage is the environment to apply
|
23 | */
|
24 | stage: string,
|
25 |
|
26 | /**
|
27 | * one of the [[PARSER_MODES]]
|
28 | */
|
29 | parserMode: string,
|
30 |
|
31 | /**
|
32 | * path to a directory where we put the final bundles
|
33 | */
|
34 | buildPath: string,
|
35 |
|
36 | /**
|
37 | * path to the main config file
|
38 | */
|
39 | configFilePath: string
|
40 | }
|
41 |
|
42 | /**
|
43 | * A Plugin to detect SinglePage-App-Components
|
44 | * @param props
|
45 | */
|
46 | export const SoaPlugin = (props: ISoaPlugin): IPlugin => {
|
47 |
|
48 | //console.log("configFilePath: " , props.configFilePath);
|
49 |
|
50 | const result: IPlugin = {
|
51 | // identify Isomorphic-App-Components
|
52 | applies: (component): boolean => {
|
53 |
|
54 | return isServiceOrientedApp(component);
|
55 | },
|
56 |
|
57 | // convert the component into configuration parts
|
58 | process: (
|
59 | component: any,
|
60 | childConfigs: Array<IConfigParseResult>,
|
61 | infrastructureMode: string | undefined
|
62 | ): IConfigParseResult => {
|
63 |
|
64 | console.log("services: ", component.services);
|
65 | const path = require('path');
|
66 |
|
67 | // we use the hardcoded name `server` as name
|
68 | const serverName = "server";
|
69 |
|
70 | const serverBuildPath = path.join(require("../../../infrastructure-scripts/dist/infra-comp-utils/system-libs").currentAbsolutePath(), props.buildPath);
|
71 |
|
72 |
|
73 |
|
74 | // the service-oriented app has a server application
|
75 | const serverWebPack = require("../../../infrastructure-scripts/dist/infra-comp-utils/webpack-libs").complementWebpackConfig(
|
76 | require("../../../infrastructure-scripts/dist/infra-comp-utils/webpack-libs").createServerWebpackConfig(
|
77 | "./"+path.join("node_modules", "infrastructure-components", "dist" , "assets", "soa-server.js"), //entryPath: string,
|
78 | serverBuildPath, //use the buildpath from the parent plugin
|
79 | serverName, // name of the server
|
80 | {
|
81 | __CONFIG_FILE_PATH__: require("../../../infrastructure-scripts/dist/infra-comp-utils/system-libs").pathToConfigFile(props.configFilePath), // replace the IsoConfig-Placeholder with the real path to the main-config-bundle
|
82 |
|
83 | // required of data-layer, makes the context match!
|
84 | "infrastructure-components": path.join(
|
85 | require("../../../infrastructure-scripts/dist/infra-comp-utils/system-libs").currentAbsolutePath(),
|
86 | "node_modules", "infrastructure-components", "dist", "index.js"),
|
87 |
|
88 | // required of the routed-app
|
89 | "react-router-dom": path.join(
|
90 | require("../../../infrastructure-scripts/dist/infra-comp-utils/system-libs").currentAbsolutePath(),
|
91 | "node_modules", "react-router-dom"),
|
92 |
|
93 | // required of the data-layer / apollo
|
94 | "react-apollo": path.join(
|
95 | require("../../../infrastructure-scripts/dist/infra-comp-utils/system-libs").currentAbsolutePath(),
|
96 | "node_modules", "react-apollo"),
|
97 |
|
98 | }, {
|
99 | __SERVICEORIENTED_ID__: `"${component.instanceId}"`,
|
100 | __ISOFFLINE__: props.parserMode === PARSER_MODES.MODE_START,
|
101 | //__ASSETS_PATH__: `"${component.assetsPath}"`,
|
102 | __DATALAYER_ID__: `"${component.dataLayerId}"`,
|
103 |
|
104 | /*__RESOLVED_ASSETS_PATH__: `"${resolveAssetsPath(
|
105 | component.buildPath,
|
106 | serverName,
|
107 | component.assetsPath )
|
108 | }"`*/
|
109 |
|
110 | // TODO add replacements of datalayers here!
|
111 | },
|
112 | ),
|
113 | props.parserMode === PARSER_MODES.MODE_DEPLOY //isProd
|
114 | );
|
115 |
|
116 |
|
117 | const webappBuildPath = path.join(require("../../../infrastructure-scripts/dist/infra-comp-utils/system-libs").currentAbsolutePath(), props.buildPath);
|
118 |
|
119 | const soaWebPack = require("../../../infrastructure-scripts/dist/infra-comp-utils/webpack-libs")
|
120 | .complementWebpackConfig(require("../../../infrastructure-scripts/dist/infra-comp-utils/webpack-libs")
|
121 | .createClientWebpackConfig(
|
122 | "./"+path.join("node_modules", "infrastructure-components", "dist" , "assets", "soa.js"), //entryPath: string,
|
123 | path.join(require("../../../infrastructure-scripts/dist/infra-comp-utils/system-libs").currentAbsolutePath(), props.buildPath), //use the buildpath from the parent plugin
|
124 | component.id, //appName
|
125 | undefined, //assetsPath
|
126 | undefined, // stagePath: TODO take from Environment!
|
127 | {
|
128 | __CONFIG_FILE_PATH__: require("../../../infrastructure-scripts/dist/infra-comp-utils/system-libs").pathToConfigFile(props.configFilePath), // replace the IsoConfig-Placeholder with the real path to the main-config-bundle
|
129 |
|
130 | // required of the routed-app
|
131 | "react-router-dom": path.join(
|
132 | require("../../../infrastructure-scripts/dist/infra-comp-utils/system-libs").currentAbsolutePath(),
|
133 | "node_modules", "react-router-dom"),
|
134 |
|
135 | // required of the data-layer / apollo
|
136 | "react-apollo": path.join(
|
137 | require("../../../infrastructure-scripts/dist/infra-comp-utils/system-libs").currentAbsolutePath(),
|
138 | "node_modules", "react-apollo"),
|
139 | }, {
|
140 | }
|
141 | ),
|
142 | props.parserMode === PARSER_MODES.MODE_DEPLOY //isProd
|
143 | );
|
144 |
|
145 |
|
146 | // provide all client configs in a flat list
|
147 | const webpackConfigs: any = childConfigs.reduce((result, config) => result.concat(config.webpackConfigs), []);
|
148 |
|
149 | const copyAssetsPostBuild = () => {
|
150 | //console.log("check for >>copyAssetsPostBuild<<");
|
151 | /* if (props.parserMode !== PARSER_MODES.MODE_DOMAIN && props.parserMode !== PARSER_MODES.MODE_DEPLOY) {
|
152 | // always copy the assets, unless we setup the domain
|
153 | console.log("copyAssetsPostBuild: now copy the assets!");
|
154 |
|
155 | webpackConfigs.map(config => require("../../../infrastructure-scripts/dist/infra-comp-utils/system-libs").copyAssets( config.output.path, path.join(serverBuildPath, serverName, component.assetsPath)));
|
156 |
|
157 | } else {
|
158 | // delete the assets folder for we don't want to include all these bundled files in the deployment-package
|
159 | const rimraf = require("rimraf");
|
160 | rimraf.sync(path.join(serverBuildPath, serverName, component.assetsPath));
|
161 |
|
162 | }
|
163 | */
|
164 | };
|
165 |
|
166 | const environments = childConfigs.reduce((result, config) => (result !== undefined ? result : []).concat(config.environments !== undefined ? config.environments : []), []);
|
167 |
|
168 | // check whether we already created the domain of this environment
|
169 | const deployedDomain = process.env[`DOMAIN_${props.stage}`] !== undefined;
|
170 |
|
171 |
|
172 | const domain = childConfigs.map(config => config.domain).reduce((result, domain) => result !== undefined ? result : domain, undefined);
|
173 | const certArn = childConfigs.map(config => config.certArn).reduce((result, certArn) => result !== undefined ? result : certArn, undefined);
|
174 |
|
175 |
|
176 | const stagePath = props.parserMode === PARSER_MODES.MODE_DEPLOY &&
|
177 | domain == undefined &&
|
178 | environments !== undefined &&
|
179 | environments.length > 0 ? environments[0].name : undefined;
|
180 |
|
181 | const createHtml = ( { serviceEndpoints }) => {
|
182 |
|
183 | //console.log("check for >>copyAssetsPostBuild<<");
|
184 | //if (props.parserMode == PARSER_MODES.MODE_BUILD) {
|
185 | console.log("write the index.html!");
|
186 | console.log("serviceEndpoints: ", serviceEndpoints);
|
187 |
|
188 |
|
189 | // we need to get rid of the path of the endpoint
|
190 | const servicePath = serviceEndpoints && serviceEndpoints.length > 0 ? (
|
191 | stagePath ?
|
192 | // when we have a stagePath, we can remove anything behind it
|
193 | serviceEndpoints[0].substr(0, serviceEndpoints[0].indexOf(stagePath)+stagePath.length) :
|
194 | // when we don't have a stagePath - TODO
|
195 | serviceEndpoints[0]
|
196 | ) : undefined;
|
197 |
|
198 |
|
199 |
|
200 | console.log ("servicePath: " , servicePath);
|
201 |
|
202 |
|
203 | // TODO this should not be hard-coded
|
204 | const graphqlUrl = component.dataLayerId ? (
|
205 | props.parserMode === PARSER_MODES.MODE_START ? "http://localhost:3001/query" : servicePath+"/query"
|
206 | ) : undefined;
|
207 |
|
208 | //region: 'localhost',
|
209 | //endpoint: 'http://localhost:8000',
|
210 |
|
211 | require('fs').writeFileSync(path.join(webappBuildPath, component.stackName, "index.html"), `<!DOCTYPE html>
|
212 | <html lang="en">
|
213 | <head>
|
214 | <meta charset="utf-8" />
|
215 | <title>${component.stackName}</title>
|
216 | <style>
|
217 | body {
|
218 | display: block;
|
219 | margin: 0px;
|
220 | }
|
221 | </style>
|
222 | </head>
|
223 | <body>
|
224 | <noscript>You need to enable JavaScript to run this app.</noscript>
|
225 | <div id="root"></div>
|
226 | <script>
|
227 | ${graphqlUrl !== undefined ? `window.__GRAPHQL__ ="${graphqlUrl}"` : ""};
|
228 | ${servicePath !== undefined ? `window.__BASENAME__ ="${servicePath}"` : ""};
|
229 | </script>
|
230 | <script src="${component.stackName}.bundle.js"></script>
|
231 | </body>
|
232 | </html>`);
|
233 |
|
234 |
|
235 |
|
236 | };
|
237 |
|
238 |
|
239 |
|
240 |
|
241 | const invalidateCloudFrontCache = () => {
|
242 | if (deployedDomain && props.parserMode === PARSER_MODES.MODE_DEPLOY) {
|
243 | require("../../../infrastructure-scripts/dist/infra-comp-utils/sls-libs").invalidateCloudFrontCache(domain);
|
244 | }
|
245 | }
|
246 |
|
247 | const hostedZoneName = domain !== undefined ? extractDomain(domain.toString()) : {};
|
248 |
|
249 |
|
250 | /** post build function to write to the .env file that the domain has been deployed */
|
251 | const writeDomainEnv = () => {
|
252 | //console.log("check for >>writeDomainEnv<<");
|
253 |
|
254 | // we only write to the .env file when we are in domain mode, i.e. this script creates the domain
|
255 | // and we did not yet deployed the domain previously
|
256 | if (!deployedDomain && props.parserMode === PARSER_MODES.MODE_DOMAIN) {
|
257 | require('fs').appendFileSync(
|
258 | path.join(
|
259 | require("../../../infrastructure-scripts/dist/infra-comp-utils/system-libs").currentAbsolutePath(),
|
260 | ".env"),
|
261 | `\nDOMAIN_${props.stage}=TRUE`
|
262 | );
|
263 | }
|
264 | };
|
265 |
|
266 | /*
|
267 | const postDeploy = async () => {
|
268 | //console.log("check for >>showStaticPageName<<");
|
269 | if (props.parserMode === PARSER_MODES.MODE_DEPLOY) {
|
270 |
|
271 |
|
272 | await require('../libs/scripts-libs').fetchData("deploy", {
|
273 | proj: component.stackName,
|
274 | envi: props.stage,
|
275 | domain: domain,
|
276 | endp: `http://${component.stackName}-${props.stage}.s3-website-${component.region}.amazonaws.com`
|
277 | });
|
278 |
|
279 | console.log(`Your SinglePageApp is now available at: http://${component.stackName}-${props.stage}.s3-website-${component.region}.amazonaws.com`);
|
280 | }
|
281 |
|
282 |
|
283 | };*/
|
284 |
|
285 | async function deployWithDomain() {
|
286 | // start the sls-config
|
287 | if (props.parserMode === PARSER_MODES.MODE_DOMAIN) {
|
288 | await require("../../../infrastructure-scripts/dist/infra-comp-utils/sls-libs").deploySls(component.stackName);
|
289 | }
|
290 | }
|
291 |
|
292 | const additionalStatements: Array<any> = forwardChildIamRoleStatements(childConfigs).concat(
|
293 | component.iamRoleStatements ? component.iamRoleStatements : []
|
294 | );
|
295 |
|
296 | const iamRoleAssignment = {
|
297 | functions: {}
|
298 | };
|
299 |
|
300 | iamRoleAssignment.functions[serverName] = {
|
301 | role: "ServiceOrientedAppLambdaRole"
|
302 | }
|
303 |
|
304 |
|
305 | const iamPermissions = {
|
306 |
|
307 | resources: {
|
308 | Resources: {
|
309 | ServiceOrientedAppLambdaRole: {
|
310 | Type: "AWS::IAM::Role",
|
311 |
|
312 | Properties: {
|
313 | RoleName: "${self:service}-${self:provider.stage, env:STAGE, 'dev'}-ServiceOrientedAppLambdaRole",
|
314 | AssumeRolePolicyDocument: {
|
315 | Version: '"2012-10-17"',
|
316 | Statement: [
|
317 | {
|
318 | Effect: "Allow",
|
319 | Principal: {
|
320 | Service: ["lambda.amazonaws.com"]
|
321 | },
|
322 | Action: "sts:AssumeRole"
|
323 | }
|
324 | ]
|
325 | },
|
326 | Policies: [
|
327 | {
|
328 | PolicyName: "${self:service}-${self:provider.stage, env:STAGE, 'dev'}-ServiceOrientedAppLambdaPolicy",
|
329 | PolicyDocument: {
|
330 | Version: '"2012-10-17"',
|
331 | Statement: [
|
332 | {
|
333 | Effect: "Allow",
|
334 | Action: [
|
335 | '"logs:*"',
|
336 | '"cloudwatch:*"'
|
337 | ],
|
338 | Resource: '"*"'
|
339 | }, {
|
340 | Effect: "Allow",
|
341 | Action: [
|
342 | "s3:Get*",
|
343 | "s3:List*",
|
344 | "s3:Put*",
|
345 | "s3:Delete*"
|
346 | ],
|
347 | Resource: {
|
348 | "Fn::Join": '["", ["arn:aws:s3:::", {"Ref": "StaticBucket" }, "/*"]]'
|
349 | }
|
350 | },
|
351 |
|
352 | ].concat(additionalStatements)
|
353 | }
|
354 | }
|
355 | ]
|
356 | }
|
357 | },
|
358 | },
|
359 | }
|
360 | }
|
361 |
|
362 | // TODO this should rather be put into DataLayer-Plugin!!!
|
363 | const dataLayerService = component.dataLayerId !== undefined ? [{
|
364 | method: "ANY",
|
365 | path: "query"
|
366 | }] : [];
|
367 |
|
368 | /**
|
369 | * ONLY add the domain config if we are in domain mode!
|
370 | * TODO once the domain has been added, we need to add this with every deployment
|
371 | */
|
372 | const domainConfig = (props.parserMode === PARSER_MODES.MODE_DOMAIN || deployedDomain) &&
|
373 | domain !== undefined && certArn !== undefined ? {
|
374 |
|
375 | // required of the SPA-domain-alias
|
376 | provider: {
|
377 | customDomainName: domain,
|
378 | hostedZoneName: hostedZoneName,
|
379 | certArn: certArn
|
380 | },
|
381 |
|
382 | resources: {
|
383 | Resources: {
|
384 | WebAppCloudFrontDistribution: {
|
385 | Type: "AWS::CloudFront::Distribution",
|
386 | Properties: {
|
387 | DistributionConfig: {
|
388 | Origins: [
|
389 | {
|
390 | DomainName: "${self:provider.staticBucket}.s3.amazonaws.com",
|
391 | Id: component.stackName,
|
392 | CustomOriginConfig: {
|
393 | HTTPPort: 80,
|
394 | HTTPSPort: 443,
|
395 | OriginProtocolPolicy: "https-only",
|
396 | }
|
397 | }
|
398 | ],
|
399 | Enabled: "'true'",
|
400 |
|
401 | DefaultRootObject: "index.html",
|
402 | CustomErrorResponses: [{
|
403 | ErrorCode: 404,
|
404 | ResponseCode: 200,
|
405 | ResponsePagePath: "/index.html"
|
406 | }],
|
407 |
|
408 | DefaultCacheBehavior: {
|
409 | AllowedMethods: [
|
410 | "DELETE",
|
411 | "GET",
|
412 | "HEAD",
|
413 | "OPTIONS",
|
414 | "PATCH",
|
415 | "POST",
|
416 | "PUT"
|
417 | ],
|
418 | TargetOriginId: component.stackName,
|
419 | ForwardedValues: {
|
420 | QueryString: "'false'",
|
421 | Cookies: {
|
422 | Forward: "none"
|
423 | }
|
424 | },
|
425 | ViewerProtocolPolicy: "redirect-to-https"
|
426 | },
|
427 | ViewerCertificate: {
|
428 | AcmCertificateArn: "${self:provider.certArn}",
|
429 | SslSupportMethod: "sni-only",
|
430 | },
|
431 | Aliases: ["${self:provider.customDomainName}"]
|
432 |
|
433 | }
|
434 | }
|
435 | },
|
436 |
|
437 | DnsRecord: {
|
438 | Type: "AWS::Route53::RecordSet",
|
439 | Properties: {
|
440 | AliasTarget: {
|
441 | DNSName: "!GetAtt WebAppCloudFrontDistribution.DomainName",
|
442 | HostedZoneId: "Z2FDTNDATAQYW2"
|
443 | },
|
444 | HostedZoneName: "${self:provider.hostedZoneName}.",
|
445 | Name: "${self:provider.customDomainName}.",
|
446 | Type: "'A'"
|
447 | }
|
448 | }
|
449 | },
|
450 | Outputs: {
|
451 | WebAppCloudFrontDistributionOutput: {
|
452 | Value: {
|
453 | "Fn::GetAtt": "[ WebAppCloudFrontDistribution, DomainName ]"
|
454 | }
|
455 | }
|
456 | }
|
457 | }
|
458 |
|
459 | } : {};
|
460 |
|
461 | const envS3Config = {
|
462 | provider: {
|
463 | environment: {
|
464 | BUCKET_ID: "${self:provider.staticBucket}",
|
465 | }
|
466 | }
|
467 | };
|
468 |
|
469 | return {
|
470 | stackType: "SOA",
|
471 |
|
472 | slsConfigs: deepmerge.all([
|
473 | require("../../../infrastructure-scripts/dist/infra-comp-utils/sls-libs").toSoaSlsConfig(
|
474 | component.stackName,
|
475 | serverName,
|
476 | component.buildPath,
|
477 | component.assetsPath,
|
478 | component.region,
|
479 | dataLayerService.concat(component.services)
|
480 | ),
|
481 |
|
482 | // the datalayer (maybe a child-config) must load before the plugin serverless-offline!
|
483 | ...childConfigs.map(config => config.slsConfigs),
|
484 |
|
485 | // # allows running the stack locally on the dev-machine
|
486 | {
|
487 | plugins: ["serverless-offline", "serverless-pseudo-parameters"],
|
488 |
|
489 | custom: {
|
490 |
|
491 | "serverless-offline": {
|
492 | host: "0.0.0.0",
|
493 | port: "${self:provider.port, env:PORT, '3001'}"
|
494 | }
|
495 | }
|
496 |
|
497 | },
|
498 |
|
499 | domainConfig,
|
500 |
|
501 | // add the IAM-Role-Statements
|
502 | iamPermissions,
|
503 |
|
504 | // assign the role
|
505 | iamRoleAssignment,
|
506 |
|
507 | // set the bucket as an env
|
508 | envS3Config
|
509 | ]
|
510 | ),
|
511 |
|
512 | // add the server config
|
513 | webpackConfigs: webpackConfigs.concat([soaWebPack, serverWebPack]),
|
514 |
|
515 | postBuilds: childConfigs.reduce((result, config) => result.concat(config.postBuilds),
|
516 | [createHtml, writeDomainEnv, copyAssetsPostBuild, deployWithDomain, invalidateCloudFrontCache /*, postDeploy*/]),
|
517 |
|
518 | iamRoleStatements: [],
|
519 |
|
520 | environments: environments,
|
521 |
|
522 | stackName: component.stackName,
|
523 |
|
524 | assetsPath: undefined,
|
525 |
|
526 | buildPath: component.buildPath,
|
527 |
|
528 | region: component.region,
|
529 |
|
530 | domain: domain,
|
531 |
|
532 | certArn: certArn,
|
533 |
|
534 | supportOfflineStart: true,
|
535 | supportCreateDomain: true
|
536 | }
|
537 | }
|
538 | }
|
539 |
|
540 | return result;
|
541 |
|
542 | }; |
\ | No newline at end of file |