UNPKG

24 kBPlain TextView Raw
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 */
7import { isServiceOrientedApp } from './soa-component';
8import { resolveAssetsPath } from '../libs/iso-libs';
9import * as deepmerge from 'deepmerge';
10import { IConfigParseResult } from '../libs/config-parse-result';
11import {IPlugin, forwardChildIamRoleStatements} from '../libs/plugin';
12import { PARSER_MODES } from '../libs/parser';
13
14import extractDomain from 'extract-domain';
15
16/**
17 * Parameters that apply to the whole Plugin, passed by other plugins
18 */
19export 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 */
46export 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