@byaga/cdk-patterns 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.fleet/run.json +11 -0
- package/lib/ApiCertificate.d.ts +2 -2
- package/lib/ApiCertificate.js +4 -5
- package/lib/CognitoApiGatewayAuthorizer.d.ts +19 -0
- package/lib/CognitoApiGatewayAuthorizer.js +24 -0
- package/lib/DeployStack.d.ts +20 -0
- package/lib/DeployStack.js +48 -0
- package/lib/DynamoDbTable.d.ts +12 -0
- package/lib/DynamoDbTable.js +34 -0
- package/lib/FunctionIntegration.d.ts +21 -0
- package/lib/FunctionIntegration.js +61 -0
- package/lib/ICorsConfig.d.ts +5 -0
- package/lib/ICorsConfig.js +2 -0
- package/lib/NodeJsLambda.d.ts +13 -0
- package/lib/NodeJsLambda.js +28 -0
- package/lib/Output.d.ts +5 -0
- package/lib/Output.js +13 -0
- package/lib/RestApi.d.ts +28 -0
- package/lib/RestApi.js +142 -0
- package/lib/Role.d.ts +2 -2
- package/lib/Role.js +2 -5
- package/lib/SsmParameter.d.ts +18 -0
- package/lib/SsmParameter.js +40 -0
- package/lib/StaticWebSite.d.ts +12 -0
- package/lib/StaticWebSite.js +107 -0
- package/lib/index.d.ts +7 -1
- package/lib/index.js +15 -3
- package/lib/methods/apply-honeycomb-to-lambda.d.ts +3 -0
- package/lib/methods/apply-honeycomb-to-lambda.js +22 -0
- package/lib/methods/build-node-source.d.ts +13 -0
- package/lib/methods/build-node-source.js +105 -0
- package/lib/methods/duration.d.ts +8 -0
- package/lib/methods/duration.js +22 -0
- package/lib/methods/empty-directory.d.ts +2 -0
- package/lib/methods/empty-directory.js +47 -0
- package/lib/methods/generate-hash.d.ts +15 -0
- package/lib/methods/generate-hash.js +59 -0
- package/lib/methods/get-source-directory.d.ts +4 -0
- package/lib/methods/get-source-directory.js +37 -0
- package/lib/methods/walk-directory.d.ts +14 -0
- package/lib/methods/walk-directory.js +48 -0
- package/package.json +1 -1
- package/src/CognitoApiGatewayAuthorizer.ts +25 -0
- package/src/DynamoDbTable.ts +40 -0
- package/src/FunctionIntegration.ts +83 -0
- package/src/ICorsConfig.ts +5 -0
- package/src/NodeJsLambda.ts +31 -0
- package/src/NodeJsLambdaLayer.ts +27 -0
- package/src/RestApi.ts +178 -0
- package/src/SsmParameter.ts +47 -0
- package/src/index.ts +9 -3
- package/src/methods/apply-honeycomb-to-lambda.ts +22 -0
- package/src/methods/build-node-source.ts +97 -0
- package/src/methods/duration.ts +24 -0
- package/src/methods/empty-directory.ts +19 -0
- package/src/methods/generate-hash.ts +49 -0
- package/src/methods/get-source-directory.ts +11 -0
- package/src/methods/walk-directory.ts +30 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
import {DeployStack} from "./DeployStack";
|
2
|
+
import {Function, FunctionProps} from "aws-cdk-lib/aws-lambda";
|
3
|
+
import {
|
4
|
+
JsonSchema,
|
5
|
+
LambdaIntegration,
|
6
|
+
MethodOptions,
|
7
|
+
Model,
|
8
|
+
RequestValidator,
|
9
|
+
IResource
|
10
|
+
} from "aws-cdk-lib/aws-apigateway";
|
11
|
+
import {CfnOutput, Duration} from "aws-cdk-lib";
|
12
|
+
import {applyHoneycombToLambda} from "./methods/apply-honeycomb-to-lambda";
|
13
|
+
import {RetentionDays} from "aws-cdk-lib/aws-logs";
|
14
|
+
import {RestApi} from "./RestApi";
|
15
|
+
|
16
|
+
interface AddToApiOptions {
|
17
|
+
requestSchema?: JsonSchema,
|
18
|
+
methodOptions?: MethodOptions
|
19
|
+
}
|
20
|
+
|
21
|
+
export interface FunctionIntegrationProps {
|
22
|
+
funcProps: FunctionProps,
|
23
|
+
timeout?: Duration
|
24
|
+
memory?: number
|
25
|
+
}
|
26
|
+
|
27
|
+
export class FunctionIntegration extends Function {
|
28
|
+
stack: DeployStack
|
29
|
+
name: string
|
30
|
+
|
31
|
+
constructor(stack: DeployStack, id: string, options: FunctionIntegrationProps) {
|
32
|
+
const props = applyHoneycombToLambda(stack, {
|
33
|
+
functionName: stack.genName(id),
|
34
|
+
memorySize: options.memory || 256,
|
35
|
+
timeout: options.timeout || Duration.seconds(16),
|
36
|
+
logRetention: RetentionDays.ONE_WEEK,
|
37
|
+
...options.funcProps
|
38
|
+
})
|
39
|
+
|
40
|
+
super(stack, stack.genId(id, 'lambda'), props);
|
41
|
+
this.stack = stack;
|
42
|
+
this.name = id;
|
43
|
+
stack.set('lambda', id, this)
|
44
|
+
new CfnOutput(stack, stack.genId(id, 'function-name'), {
|
45
|
+
value: this.functionName,
|
46
|
+
exportName: stack.genName(id, 'function-name')
|
47
|
+
});
|
48
|
+
}
|
49
|
+
|
50
|
+
attach(api: RestApi, httpMethod: string, path: string, props?: AddToApiOptions) {
|
51
|
+
const resource: IResource = api.path(path)
|
52
|
+
const integration = new LambdaIntegration(this, {
|
53
|
+
requestTemplates: {
|
54
|
+
'application/json': '{ "statusCode": "200" }'
|
55
|
+
}
|
56
|
+
})
|
57
|
+
|
58
|
+
let options: MethodOptions = props?.methodOptions || {}
|
59
|
+
const schema = props?.requestSchema
|
60
|
+
if (schema) options = applySchema(this.stack, this.name, schema, options, resource)
|
61
|
+
|
62
|
+
const method = resource.addMethod(httpMethod, integration, options)
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
function applySchema(stack: DeployStack, name: string, schema: JsonSchema, options: MethodOptions, path: IResource): MethodOptions {
|
67
|
+
return {
|
68
|
+
...options,
|
69
|
+
requestValidator: new RequestValidator(stack, stack.genId(name, 'validator'), {
|
70
|
+
restApi: path.api,
|
71
|
+
requestValidatorName: stack.genName(name, 'validator'),
|
72
|
+
validateRequestBody: true
|
73
|
+
}),
|
74
|
+
requestModels: {
|
75
|
+
'application/json': new Model(stack, stack.genId(name, 'model'), {
|
76
|
+
schema: schema,
|
77
|
+
contentType: 'application/json',
|
78
|
+
restApi: path.api,
|
79
|
+
modelName: stack.genId(name, 'model')
|
80
|
+
})
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import {Code, FunctionOptions, Runtime} from "aws-cdk-lib/aws-lambda"
|
2
|
+
import {DeployStack} from "./DeployStack";
|
3
|
+
import {FunctionIntegration} from "./FunctionIntegration";
|
4
|
+
import {buildNodeSource} from "./methods/build-node-source";
|
5
|
+
import {Duration} from "aws-cdk-lib";
|
6
|
+
import duration from "./methods/duration";
|
7
|
+
|
8
|
+
interface NodeFunctionProps {
|
9
|
+
funcProps?: FunctionOptions
|
10
|
+
timeout?: Duration
|
11
|
+
memory?: number
|
12
|
+
}
|
13
|
+
|
14
|
+
export class NodeJsLambda extends FunctionIntegration {
|
15
|
+
constructor(stack: DeployStack, id: string, options?: NodeFunctionProps) {
|
16
|
+
console.log('Defining Node Lambda', id)
|
17
|
+
const done = duration()
|
18
|
+
const {buildDir} = buildNodeSource('lambda', id)
|
19
|
+
console.log('Total Build Duration (ms)', done())
|
20
|
+
|
21
|
+
super(stack, id, {
|
22
|
+
...options,
|
23
|
+
funcProps: {
|
24
|
+
...options?.funcProps,
|
25
|
+
code: Code.fromAsset(buildDir),
|
26
|
+
handler: `${id}.handler`,
|
27
|
+
runtime: Runtime.NODEJS_18_X
|
28
|
+
}
|
29
|
+
})
|
30
|
+
}
|
31
|
+
}
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import {Code, LayerVersion, Runtime} from "aws-cdk-lib/aws-lambda";
|
2
|
+
import {IDeployStack} from "./IDeployStack";
|
3
|
+
import {LayerVersionOptions} from "aws-cdk-lib/aws-lambda/lib/layers";
|
4
|
+
import {Architecture} from "aws-cdk-lib/aws-lambda/lib/architecture";
|
5
|
+
import {buildNodeSource} from "./methods/build-node-source";
|
6
|
+
import duration from "./methods/duration";
|
7
|
+
|
8
|
+
interface NodeJsLambdaLayerProps extends LayerVersionOptions {
|
9
|
+
readonly compatibleRuntimes?: Runtime[];
|
10
|
+
readonly compatibleArchitectures?: Architecture[];
|
11
|
+
}
|
12
|
+
|
13
|
+
export class NodeJsLambdaLayer extends LayerVersion {
|
14
|
+
constructor(stack: IDeployStack, id: string, props: NodeJsLambdaLayerProps = {}) {
|
15
|
+
console.log("Building Lambda Layer", id);
|
16
|
+
const done = duration()
|
17
|
+
const buildDir = buildNodeSource('lambda-layer', id, 'nodejs')
|
18
|
+
console.log('Build Duration (ms)', done())
|
19
|
+
|
20
|
+
super(stack, stack.genId(id), {
|
21
|
+
...props,
|
22
|
+
layerVersionName: stack.genName(id),
|
23
|
+
code: Code.fromAsset(buildDir)
|
24
|
+
});
|
25
|
+
stack.set('lambda-layer', id, this)
|
26
|
+
}
|
27
|
+
}
|
package/src/RestApi.ts
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
import {
|
2
|
+
RestApi as RestApiBase,
|
3
|
+
BasePathMapping,
|
4
|
+
ContentHandling,
|
5
|
+
DomainName,
|
6
|
+
EndpointType,
|
7
|
+
MockIntegration,
|
8
|
+
IDomainName,
|
9
|
+
IResource
|
10
|
+
} from "aws-cdk-lib/aws-apigateway";
|
11
|
+
import {DeployStack} from "./DeployStack"
|
12
|
+
import {ICorsConfig} from "./ICorsConfig";
|
13
|
+
import {Duration, Size} from "aws-cdk-lib";
|
14
|
+
import {Effect, Policy, PolicyStatement, Role} from "aws-cdk-lib/aws-iam";
|
15
|
+
|
16
|
+
interface IRestApiConfig {
|
17
|
+
cors?: ICorsConfig,
|
18
|
+
allowCredentials: boolean
|
19
|
+
}
|
20
|
+
|
21
|
+
interface IBasePathMappingConfig {
|
22
|
+
domain?: IDomainName,
|
23
|
+
domainName?: string,
|
24
|
+
hostedZoneId?: string
|
25
|
+
}
|
26
|
+
|
27
|
+
interface IResourceNode {
|
28
|
+
children: { [key: string]: IResourceNode }
|
29
|
+
node: IResource
|
30
|
+
}
|
31
|
+
|
32
|
+
const DEFAULT_EXPOSE_HEADERS = ['Date']
|
33
|
+
|
34
|
+
export class RestApi extends RestApiBase {
|
35
|
+
_id: string
|
36
|
+
_tree: IResourceNode
|
37
|
+
|
38
|
+
|
39
|
+
constructor(stack: DeployStack, id: string, props: IRestApiConfig) {
|
40
|
+
const {stage} = stack
|
41
|
+
|
42
|
+
console.log('Defining Rest API', stack.genId(id, 'api'))
|
43
|
+
|
44
|
+
const cors: ICorsConfig = props.cors || {}
|
45
|
+
const allowOrigins: string[] = Array.isArray(cors.allowOrigin) ? cors.allowOrigin : [cors.allowOrigin || '*']
|
46
|
+
super(stack, stack.genId(id, 'api'), {
|
47
|
+
restApiName: stack.genName('api'),
|
48
|
+
deploy: true,
|
49
|
+
deployOptions: {
|
50
|
+
stageName: stage,
|
51
|
+
tracingEnabled: true
|
52
|
+
},
|
53
|
+
minCompressionSize: Size.bytes(1000),
|
54
|
+
endpointConfiguration: {
|
55
|
+
types: [EndpointType.EDGE]
|
56
|
+
},
|
57
|
+
defaultCorsPreflightOptions: {
|
58
|
+
allowHeaders: ['*'],
|
59
|
+
allowMethods: ['*'],
|
60
|
+
allowCredentials: props.allowCredentials,
|
61
|
+
allowOrigins,
|
62
|
+
maxAge: Duration.seconds(cors.maxAge || 86400),
|
63
|
+
exposeHeaders: cors.exposeHeaders || DEFAULT_EXPOSE_HEADERS
|
64
|
+
}
|
65
|
+
})
|
66
|
+
this._id = id
|
67
|
+
|
68
|
+
// APIs MUST have at least one endpoint, so this will give it one
|
69
|
+
console.log('Adding health endpoint', '/health')
|
70
|
+
const health = this.root.addResource('health')
|
71
|
+
health.addMethod("GET", new MockIntegration({
|
72
|
+
requestTemplates: {
|
73
|
+
"application/json": "{\"statusCode\": 200}"
|
74
|
+
},
|
75
|
+
integrationResponses: [{
|
76
|
+
statusCode: '200',
|
77
|
+
contentHandling: ContentHandling.CONVERT_TO_TEXT,
|
78
|
+
responseTemplates: {
|
79
|
+
'application/json': `
|
80
|
+
## See http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html
|
81
|
+
## This template will pass through all parameters including path, querystring, header, stage variables, and context through to the integration endpoint via the body/payload
|
82
|
+
#set($allParams = $input.params())
|
83
|
+
{
|
84
|
+
"body-json" : $input.json('$'),
|
85
|
+
"params" : {
|
86
|
+
#foreach($type in $allParams.keySet())
|
87
|
+
#set($params = $allParams.get($type))
|
88
|
+
"$type" : {
|
89
|
+
#foreach($paramName in $params.keySet())
|
90
|
+
"$paramName" : "$util.escapeJavaScript($params.get($paramName))"
|
91
|
+
#if($foreach.hasNext),#end
|
92
|
+
#end
|
93
|
+
}
|
94
|
+
#if($foreach.hasNext),#end
|
95
|
+
#end
|
96
|
+
},
|
97
|
+
"stage-variables" : {
|
98
|
+
#foreach($key in $stageVariables.keySet())
|
99
|
+
"$key" : "$util.escapeJavaScript($stageVariables.get($key))"
|
100
|
+
#if($foreach.hasNext),#end
|
101
|
+
#end
|
102
|
+
},
|
103
|
+
"context" : {
|
104
|
+
"account-id" : "$context.identity.accountId",
|
105
|
+
"api-id" : "$context.apiId",
|
106
|
+
"api-key" : "$context.identity.apiKey",
|
107
|
+
"authorizer-principal-id" : "$context.authorizer.principalId",
|
108
|
+
"caller" : "$context.identity.caller",
|
109
|
+
"cognito-authentication-provider" : "$context.identity.cognitoAuthenticationProvider",
|
110
|
+
"cognito-authentication-type" : "$context.identity.cognitoAuthenticationType",
|
111
|
+
"cognito-identity-id" : "$context.identity.cognitoIdentityId",
|
112
|
+
"cognito-identity-pool-id" : "$context.identity.cognitoIdentityPoolId",
|
113
|
+
"http-method" : "$context.httpMethod",
|
114
|
+
"stage" : "$context.stage",
|
115
|
+
"source-ip" : "$context.identity.sourceIp",
|
116
|
+
"user" : "$context.identity.user",
|
117
|
+
"user-agent" : "$context.identity.userAgent",
|
118
|
+
"user-arn" : "$context.identity.userArn",
|
119
|
+
"request-id" : "$context.requestId",
|
120
|
+
"resource-id" : "$context.resourceId",
|
121
|
+
"resource-path" : "$context.resourcePath"
|
122
|
+
}
|
123
|
+
}`
|
124
|
+
}
|
125
|
+
}]
|
126
|
+
}), {
|
127
|
+
methodResponses: [{statusCode: "200"}]
|
128
|
+
})
|
129
|
+
|
130
|
+
this._tree = {node: this.root, children: {}}
|
131
|
+
}
|
132
|
+
|
133
|
+
addBasePathMapping(stack: DeployStack, config: IBasePathMappingConfig) {
|
134
|
+
let domain = config.domain
|
135
|
+
if (!domain && config.domainName && config.hostedZoneId) {
|
136
|
+
domain = DomainName.fromDomainNameAttributes(stack, stack.genName('domain-name'), {
|
137
|
+
domainName: config.domainName,
|
138
|
+
domainNameAliasHostedZoneId: config.hostedZoneId,
|
139
|
+
domainNameAliasTarget: this.restApiRootResourceId
|
140
|
+
})
|
141
|
+
}
|
142
|
+
if (!domain) throw new Error('Missing Domain Configuration For Base Path Mapping')
|
143
|
+
|
144
|
+
new BasePathMapping(stack, stack.genId(this._id, 'base-path-mapping'), {
|
145
|
+
domainName: domain,
|
146
|
+
stage: this.deploymentStage,
|
147
|
+
restApi: this,
|
148
|
+
basePath: this._id
|
149
|
+
})
|
150
|
+
}
|
151
|
+
|
152
|
+
addAuthorizedRole(stack: DeployStack, roleArn: string): PolicyStatement {
|
153
|
+
const policyStatement = new PolicyStatement({
|
154
|
+
effect: Effect.ALLOW,
|
155
|
+
actions: ['execute-api:Invoke']
|
156
|
+
})
|
157
|
+
|
158
|
+
const authRole = Role.fromRoleArn(stack, stack.genId('authenticated-role'), roleArn)
|
159
|
+
authRole.attachInlinePolicy(new Policy(stack, stack.genId('api-access-policy'), {
|
160
|
+
statements: [policyStatement]
|
161
|
+
}))
|
162
|
+
return policyStatement;
|
163
|
+
}
|
164
|
+
|
165
|
+
path(uri: string): IResource {
|
166
|
+
const {node} = uri.split('/').reduce((resource, path) => {
|
167
|
+
if (!resource.children[path]) {
|
168
|
+
resource.children[path] = {
|
169
|
+
children: {},
|
170
|
+
node: resource.node.addResource(path)
|
171
|
+
}
|
172
|
+
}
|
173
|
+
return resource.children[path];
|
174
|
+
}, this._tree);
|
175
|
+
|
176
|
+
return node
|
177
|
+
}
|
178
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import {DeployStack} from "./DeployStack";
|
2
|
+
import {IStringParameter, StringParameter} from "aws-cdk-lib/aws-ssm";
|
3
|
+
import {Effect, IGrantable, PolicyStatement} from "aws-cdk-lib/aws-iam";
|
4
|
+
import {IKey} from "aws-cdk-lib/aws-kms";
|
5
|
+
|
6
|
+
const paramCache: { [name: string]: IStringParameter } = {}
|
7
|
+
|
8
|
+
interface SsmParameterOptions {
|
9
|
+
decryptWithKey?: IKey
|
10
|
+
}
|
11
|
+
|
12
|
+
export class SsmParameter {
|
13
|
+
parameterName: string
|
14
|
+
parameterArn: string
|
15
|
+
decryptWithKey?: IKey
|
16
|
+
_stack: DeployStack
|
17
|
+
|
18
|
+
constructor(stack: DeployStack, name: string, options?: SsmParameterOptions) {
|
19
|
+
if (name[0] !== '/') name = '/' + name
|
20
|
+
this.parameterName = `/${stack.name}${name}`;
|
21
|
+
this.parameterArn = `arn:aws:ssm:${stack.region}:${stack.account}:parameter${this.parameterName}`
|
22
|
+
this._stack = stack;
|
23
|
+
this.decryptWithKey = options?.decryptWithKey
|
24
|
+
}
|
25
|
+
|
26
|
+
grantRead(grantee: IGrantable) {
|
27
|
+
grantee.grantPrincipal.addToPrincipalPolicy(new PolicyStatement({
|
28
|
+
effect: Effect.ALLOW,
|
29
|
+
actions: ['ssm:GetParameter'],
|
30
|
+
resources: [this.parameterArn]
|
31
|
+
}))
|
32
|
+
if (this.decryptWithKey) {
|
33
|
+
this.decryptWithKey.grantDecrypt(grantee)
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
get stringValue() {
|
38
|
+
return this.getParameter().stringValue
|
39
|
+
}
|
40
|
+
|
41
|
+
getParameter() {
|
42
|
+
if (!paramCache[this.parameterArn]) {
|
43
|
+
paramCache[this.parameterArn] = StringParameter.fromStringParameterName(this._stack, this._stack.genId(this.parameterName.substring(1).replace('/', '_')), this.parameterName)
|
44
|
+
}
|
45
|
+
return paramCache[this.parameterArn]
|
46
|
+
}
|
47
|
+
}
|
package/src/index.ts
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
export {ApiCertificate} from "./ApiCertificate";
|
2
2
|
export {DeployStack} from "./DeployStack";
|
3
|
-
export {
|
3
|
+
export {DynamoDbTable} from "./DynamoDbTable";
|
4
|
+
export {FunctionIntegration} from "./FunctionIntegration";
|
4
5
|
export {IHostedZoneConfig} from "./IHostedZoneConfig";
|
6
|
+
export {IDomainConfig} from "./IDomainConfig";
|
5
7
|
export {IStackArguments} from "./IStackArguments";
|
6
|
-
export {
|
7
|
-
export {
|
8
|
+
export {NodeJsLambda} from "./NodeJsLambda";
|
9
|
+
export {NodeJsLambdaLayer} from "./NodeJsLambdaLayer";
|
8
10
|
export {Output} from "./Output";
|
11
|
+
export {RestApi} from "./RestApi";
|
12
|
+
export {Role} from "./Role";
|
13
|
+
export {SsmParameter} from "./SsmParameter";
|
14
|
+
export {StaticWebSite} from "./StaticWebSite";
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import {FunctionProps, ILayerVersion, LayerVersion} from "aws-cdk-lib/aws-lambda"
|
2
|
+
import {DeployStack} from "../DeployStack";
|
3
|
+
import {SsmParameter} from "../SsmParameter";
|
4
|
+
|
5
|
+
let honeyCombLayer: ILayerVersion
|
6
|
+
export function applyHoneycombToLambda(stack: DeployStack, props: FunctionProps): FunctionProps {
|
7
|
+
if (!honeyCombLayer) honeyCombLayer = LayerVersion.fromLayerVersionArn(stack, stack.genId('hc-layer'), `arn:aws:lambda:${stack.region}:702835727665:layer:honeycomb-lambda-extension-x86_64-v11-1-1:1`)
|
8
|
+
|
9
|
+
const layers = props.layers ? [...props.layers] : []
|
10
|
+
const environment = props.environment || {}
|
11
|
+
|
12
|
+
layers.push(honeyCombLayer)
|
13
|
+
environment.LIBHONEY_API_KEY = new SsmParameter(stack, `/honeycomb/api-key`).stringValue
|
14
|
+
environment.LIBHONEY_DATASET =new SsmParameter(stack, `/honeycomb/dataset`).stringValue
|
15
|
+
environment.LOGS_API_DISABLE_PLATFORM_MSGS = "true"
|
16
|
+
|
17
|
+
return {
|
18
|
+
...props,
|
19
|
+
layers,
|
20
|
+
environment
|
21
|
+
}
|
22
|
+
}
|
@@ -0,0 +1,97 @@
|
|
1
|
+
import * as path from 'path'
|
2
|
+
import * as fse from "fs-extra";
|
3
|
+
import * as fs from "fs";
|
4
|
+
import {execSync} from "child_process";
|
5
|
+
import {generateHash, HashResult} from "./generate-hash"
|
6
|
+
import {getBuildDirectory, getSourceDirectory} from './get-source-directory'
|
7
|
+
import duration from "./duration";
|
8
|
+
import emptyDirSync from './empty-directory'
|
9
|
+
import {IIgnoreOptions} from "./walk-directory";
|
10
|
+
|
11
|
+
const distributionRoot = path.resolve(process.cwd(), "../dist")
|
12
|
+
const hashRoot = path.join(distributionRoot, "hash")
|
13
|
+
fse.ensureDirSync(hashRoot)
|
14
|
+
|
15
|
+
export const defaultIgnore: IIgnoreOptions = {
|
16
|
+
included: (p: fs.Dirent): boolean => {
|
17
|
+
return !p.name.startsWith(".") &&
|
18
|
+
!p.name.endsWith(".md") &&
|
19
|
+
!p.name.includes(".test.") &&
|
20
|
+
p.name !== 'package-lock.json';
|
21
|
+
},
|
22
|
+
childrenIncluded: (p: fs.Dirent): boolean => {
|
23
|
+
return p.name === "node_modules";
|
24
|
+
}
|
25
|
+
} as IIgnoreOptions
|
26
|
+
|
27
|
+
interface IBuildOptions {
|
28
|
+
dir?: string,
|
29
|
+
ignore?: IIgnoreOptions
|
30
|
+
}
|
31
|
+
|
32
|
+
export function buildNodeSource(type: string, id: string, options: IBuildOptions = {}): {
|
33
|
+
buildDir: string,
|
34
|
+
moduleChanged: boolean,
|
35
|
+
packageChanged: boolean,
|
36
|
+
sourceChanged: boolean
|
37
|
+
} {
|
38
|
+
const {dir = ''} = options
|
39
|
+
const ignore = {
|
40
|
+
...defaultIgnore, ...options.ignore
|
41
|
+
}
|
42
|
+
const srcDir = getSourceDirectory(type, id)
|
43
|
+
const buildDir = getBuildDirectory(type, id)
|
44
|
+
const hashFilePath = path.join(hashRoot, `${type}-${id}-hash.txt`)
|
45
|
+
|
46
|
+
const folderExists = fs.existsSync(buildDir)
|
47
|
+
const hashFileExists = folderExists && fse.existsSync(hashFilePath)
|
48
|
+
const prevHash: HashResult = (hashFileExists ? fse.readJsonSync(hashFilePath) : {}) as HashResult
|
49
|
+
const hash: string = generateHash(srcDir, {ignore});
|
50
|
+
const nextHash: HashResult = JSON.parse(generateHash(srcDir, {ignore})) as HashResult;
|
51
|
+
const moduleChanged = JSON.stringify(prevHash, null, '\t') !== hash;
|
52
|
+
let packageChanged = false, sourceChanged = false;
|
53
|
+
|
54
|
+
if (moduleChanged) {
|
55
|
+
const prevPackageHash = findPackageHash(prevHash);
|
56
|
+
const packageHash = findPackageHash(nextHash);
|
57
|
+
packageChanged = prevPackageHash !== packageHash;
|
58
|
+
|
59
|
+
sourceChanged = sourceWasUpdated(prevHash, nextHash);
|
60
|
+
if (sourceChanged) {
|
61
|
+
const rmComplete = duration()
|
62
|
+
if (folderExists) emptyDirSync(buildDir, {childrenExcluded: folder => folder.name === 'node_modules'})
|
63
|
+
console.log('Cleanup Duration (ms)', rmComplete())
|
64
|
+
|
65
|
+
const copyComplete = duration()
|
66
|
+
fse.copySync(srcDir, buildDir, {
|
67
|
+
filter: src => !~src.indexOf("node_modules") && !src.endsWith('.test.js')
|
68
|
+
});
|
69
|
+
console.log('Copy Duration (ms)', copyComplete())
|
70
|
+
}
|
71
|
+
|
72
|
+
if (packageChanged) {
|
73
|
+
const installComplete = duration()
|
74
|
+
execSync("npm i --omit=dev --omit=optional --omit=peer --quite", {
|
75
|
+
cwd: dir ? path.resolve(buildDir, dir) : buildDir
|
76
|
+
});
|
77
|
+
console.log('NPM Install Duration (ms)', installComplete())
|
78
|
+
}
|
79
|
+
fs.writeFileSync(hashFilePath, hash);
|
80
|
+
}
|
81
|
+
|
82
|
+
return {buildDir, packageChanged, sourceChanged, moduleChanged}
|
83
|
+
}
|
84
|
+
|
85
|
+
function findPackageHash(hash: HashResult): string | undefined {
|
86
|
+
return hash?.children?.find(file => file.name === 'package.json')?.hash;
|
87
|
+
}
|
88
|
+
|
89
|
+
function sourceWasUpdated(prevHash: HashResult, nextHash: HashResult): boolean {
|
90
|
+
const fileCountChanged = prevHash.children?.length != nextHash.children?.length
|
91
|
+
const updatedItem = prevHash.children?.find(prevFile => {
|
92
|
+
const nextFile = nextHash.children?.find(f => f.name = prevFile.name);
|
93
|
+
return prevFile.name !== 'package.json' && (!nextFile || nextFile.hash !== prevFile.hash)
|
94
|
+
});
|
95
|
+
|
96
|
+
return fileCountChanged || !!updatedItem
|
97
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
const hrDuration = () => {
|
2
|
+
const startTime: [number, number] = process.hrtime();
|
3
|
+
const onEnd = function duration(): number {
|
4
|
+
const hrTime:[number, number] = process.hrtime(startTime);
|
5
|
+
return hrTime[0] * 1000 + hrTime[1] / 1000000;
|
6
|
+
};
|
7
|
+
onEnd.time = startTime;
|
8
|
+
|
9
|
+
return onEnd;
|
10
|
+
};
|
11
|
+
const msDuration = () => {
|
12
|
+
const startTime: number = Date.now();
|
13
|
+
const onEnd = function duration(): number {
|
14
|
+
return Date.now() - startTime;
|
15
|
+
};
|
16
|
+
onEnd.time = startTime;
|
17
|
+
|
18
|
+
return onEnd;
|
19
|
+
};
|
20
|
+
|
21
|
+
// @ts-ignore
|
22
|
+
const duration= process.hrtime ? hrDuration : msDuration
|
23
|
+
|
24
|
+
export default duration
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import * as fse from "fs-extra";
|
2
|
+
import duration from "./duration";
|
3
|
+
import walkDirectory, {DirentExtended, IIgnoreOptions} from "./walk-directory";
|
4
|
+
|
5
|
+
export default function emptyDirSync(parentFolder: string, options: IIgnoreOptions) {
|
6
|
+
const rmComplete = duration()
|
7
|
+
const files = walkDirectory(parentFolder, options);
|
8
|
+
|
9
|
+
files
|
10
|
+
.forEach((file:DirentExtended) => {
|
11
|
+
try {
|
12
|
+
fse.removeSync(file.fullpath());
|
13
|
+
console.log('Deleted file:', file.fullpath());
|
14
|
+
} catch (err) {
|
15
|
+
console.error('Error deleting file:', err);
|
16
|
+
}
|
17
|
+
});
|
18
|
+
console.log('Empty Directory Complete (ms)', parentFolder, rmComplete())
|
19
|
+
}
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import * as crypto from 'crypto'
|
2
|
+
import * as path from 'path'
|
3
|
+
import * as fs from 'fs'
|
4
|
+
import walk, {IIgnoreOptions} from './walk-directory'
|
5
|
+
|
6
|
+
const ALGO = 'sha1';
|
7
|
+
const ENCODING = 'base64';
|
8
|
+
|
9
|
+
interface HashOptions {
|
10
|
+
ignore: IIgnoreOptions
|
11
|
+
}
|
12
|
+
|
13
|
+
export interface HashResult {
|
14
|
+
hash?: string
|
15
|
+
children?: HashItem[]
|
16
|
+
}
|
17
|
+
export interface HashItem {
|
18
|
+
hash: string,
|
19
|
+
path: string,
|
20
|
+
name: string
|
21
|
+
}
|
22
|
+
|
23
|
+
export function generateHash(folder: string, opt: HashOptions): string {
|
24
|
+
const files = walk(folder, opt.ignore);
|
25
|
+
|
26
|
+
const hash = crypto.createHash(ALGO);
|
27
|
+
const children = files
|
28
|
+
.sort((a, b) => a.fullpath().localeCompare(b.fullpath()))
|
29
|
+
.map((child) => {
|
30
|
+
const relPath = path.relative(process.cwd(), child.fullpath())
|
31
|
+
const fileHash = crypto.createHash(ALGO);
|
32
|
+
fileHash.update(relPath);
|
33
|
+
fileHash.update(fs.readFileSync(child.fullpath()));
|
34
|
+
|
35
|
+
const file = {
|
36
|
+
name: child.name,
|
37
|
+
path: relPath,
|
38
|
+
hash: fileHash.digest(ENCODING)
|
39
|
+
};
|
40
|
+
hash.update(file.hash);
|
41
|
+
|
42
|
+
return file;
|
43
|
+
});
|
44
|
+
|
45
|
+
return JSON.stringify({
|
46
|
+
hash: hash.digest(ENCODING),
|
47
|
+
children
|
48
|
+
}, null, '\t')
|
49
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import * as path from 'path'
|
2
|
+
|
3
|
+
export const sourceRoot = path.resolve(process.cwd(), "../src");
|
4
|
+
export const distributionRoot = path.resolve(process.cwd(), "../dist");
|
5
|
+
|
6
|
+
export function getSourceDirectory(type: string, id: string): string {
|
7
|
+
return path.join(sourceRoot, type, id)
|
8
|
+
}
|
9
|
+
export function getBuildDirectory(type: string, id: string): string {
|
10
|
+
return path.join(distributionRoot, type, id)
|
11
|
+
}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import * as path from 'path'
|
2
|
+
import * as fs from 'fs'
|
3
|
+
|
4
|
+
export interface IIgnoreOptions {
|
5
|
+
excluded?: (stat: fs.Dirent) => boolean,
|
6
|
+
included?: (stat: fs.Dirent) => boolean,
|
7
|
+
childrenExcluded?: (stat: fs.Dirent) => boolean,
|
8
|
+
childrenIncluded?: (stat: fs.Dirent) => boolean
|
9
|
+
}
|
10
|
+
export const yes = () => true
|
11
|
+
export const no = () => false
|
12
|
+
|
13
|
+
export default function walkDirectory(directory: string, options: IIgnoreOptions, filepaths: DirentExtended[] = []): DirentExtended[] {
|
14
|
+
const { excluded = no, included = yes, childrenExcluded = no, childrenIncluded = yes} = options;
|
15
|
+
const files = fs.readdirSync(directory, {withFileTypes: true}) as unknown[] as DirentExtended[];
|
16
|
+
for (let stats of files) {
|
17
|
+
const filepath = path.join(directory, stats.name)
|
18
|
+
stats.fullpath = () => filepath
|
19
|
+
if (stats.isDirectory() && !childrenExcluded(stats) && childrenIncluded(stats)) {
|
20
|
+
walkDirectory(filepath, options, filepaths)
|
21
|
+
} else if (stats.isFile() && !excluded(stats) && included(stats)) {
|
22
|
+
filepaths.push(stats)
|
23
|
+
}
|
24
|
+
}
|
25
|
+
return filepaths;
|
26
|
+
}
|
27
|
+
|
28
|
+
export interface DirentExtended extends fs.Dirent {
|
29
|
+
fullpath: () => string
|
30
|
+
}
|